import AccountsService from 'gi://AccountsService'; import Atk from 'gi://Atk'; import Clutter from 'gi://Clutter'; import Cogl from 'gi://Cogl'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import GMenu from 'gi://GMenu'; import GObject from 'gi://GObject'; import Graphene from 'gi://Graphene'; import Pango from 'gi://Pango'; import Shell from 'gi://Shell'; import St from 'gi://St'; import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js'; import * as Config from 'resource:///org/gnome/shell/misc/config.js'; import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as Params from 'resource:///org/gnome/shell/misc/params.js'; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; import {showScreenshotUI} from 'resource:///org/gnome/shell/ui/screenshot.js'; import * as SystemActions from 'resource:///org/gnome/shell/misc/systemActions.js'; import * as Util from 'resource:///org/gnome/shell/misc/util.js'; import {AppContextMenu} from './appMenu.js'; import {ArcMenuManager} from './arcmenuManager.js'; import * as Constants from './constants.js'; import {DragLocation, IconGrid} from './iconGrid.js'; import * as Utils from './utils.js'; import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; const GDateMenu = Main.panel.statusArea.dateMenu; const GWeatherWidget = GDateMenu._weatherItem.constructor; const GWorldClocksWidget = GDateMenu._clocksItem.constructor; const INDICATOR_ICON_SIZE = 18; const USER_AVATAR_SIZE = 28; const TOOLTIP_SHOW_TIME = 150; const TOOLTIP_HIDE_TIME = 100; const [ShellVersion] = Config.PACKAGE_VERSION.split('.').map(s => Number(s)); /** * @param {Constants.PowerType} powerType The power type to activate */ function activatePowerOption(powerType) { const systemActions = SystemActions.getDefault(); switch (powerType) { case Constants.PowerType.POWER_OFF: systemActions.activatePowerOff(); break; case Constants.PowerType.RESTART: systemActions.activateRestart(); break; case Constants.PowerType.LOCK: systemActions.activateLockScreen(); break; case Constants.PowerType.LOGOUT: systemActions.activateLogout(); break; case Constants.PowerType.SUSPEND: systemActions.activateSuspend(); break; case Constants.PowerType.SWITCH_USER: systemActions.activateSwitchUser(); break; case Constants.PowerType.HYBRID_SLEEP: Utils.activateHibernateOrSleep(powerType); break; case Constants.PowerType.HIBERNATE: Utils.activateHibernateOrSleep(powerType); break; } } /** * @param {PowerMenuItem} powerMenuItem Bind visibility of the powermenu item */ export function bindPowerItemVisibility(powerMenuItem) { const {powerType} = powerMenuItem; const systemActions = SystemActions.getDefault(); switch (powerType) { case Constants.PowerType.POWER_OFF: return systemActions.bind_property('can-power-off', powerMenuItem, 'visible', GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE); case Constants.PowerType.RESTART: return systemActions.bind_property('can-restart', powerMenuItem, 'visible', GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE); case Constants.PowerType.LOCK: return systemActions.bind_property('can-lock-screen', powerMenuItem, 'visible', GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE); case Constants.PowerType.LOGOUT: return systemActions.bind_property('can-logout', powerMenuItem, 'visible', GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE); case Constants.PowerType.SUSPEND: return systemActions.bind_property('can-suspend', powerMenuItem, 'visible', GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE); case Constants.PowerType.SWITCH_USER: return systemActions.bind_property('can-switch-user', powerMenuItem, 'visible', GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE); case Constants.PowerType.HYBRID_SLEEP: Utils.canHibernateOrSleep('CanHybridSleep', result => { if (!powerMenuItem.isDestroyed) powerMenuItem.visible = result; }); return null; case Constants.PowerType.HIBERNATE: Utils.canHibernateOrSleep('CanHibernate', result => { if (!powerMenuItem.isDestroyed) powerMenuItem.visible = result; }); return null; default: return null; } } export class BaseMenuItem extends St.BoxLayout { static [GObject.properties] = { 'active': GObject.ParamSpec.boolean('active', 'active', 'active', GObject.ParamFlags.READWRITE, false), 'sensitive': GObject.ParamSpec.boolean('sensitive', 'sensitive', 'sensitive', GObject.ParamFlags.READWRITE, true), }; static [GObject.signals] = { 'activate': {param_types: [Clutter.Event.$gtype]}, }; static { GObject.registerClass(this); } constructor(menuLayout, params) { params = Params.parse(params, { reactive: true, activate: true, hover: true, style_class: null, can_focus: true, }); super({ style_class: 'popup-menu-item arcmenu-menu-item', x_align: Clutter.ActorAlign.FILL, x_expand: true, reactive: params.reactive, track_hover: params.reactive, can_focus: params.can_focus, pivot_point: new Graphene.Point({x: 0.5, y: 0.5}), accessible_role: Atk.Role.MENU_ITEM, }); this.hasContextMenu = false; this._delegate = this; this._menuButton = menuLayout.menuButton; this._arcMenu = menuLayout.arcMenu; this._menuLayout = menuLayout; this.tooltipLocation = Constants.TooltipLocation.BOTTOM; this.shouldShow = true; this._active = false; this._activatable = params.reactive && params.activate; this._sensitive = true; if (!this._activatable) this.add_style_class_name('popup-inactive-menu-item'); if (params.style_class) this.add_style_class_name(params.style_class); if (params.hover) this.connect('notify::hover', this._onHover.bind(this)); if (params.reactive && params.hover) this.bind_property('hover', this, 'active', GObject.BindingFlags.SYNC_CREATE); this._panAction = new Clutter.PanAction({interpolate: true}); this._panAction.connect('pan', this._onPan.bind(this)); this.add_action(this._panAction); this._clickAction = new Clutter.ClickAction({ enabled: this._activatable, }); this._clickAction.connect('clicked', this._onClicked.bind(this)); this._clickAction.connect('long-press', this._onLongPress.bind(this)); this._clickAction.connect('notify::pressed', () => { if (this._clickAction.pressed) this.add_style_pseudo_class('active'); else this.remove_style_pseudo_class('active'); }); this.add_action(this._clickAction); this.connect('destroy', () => this._onDestroy()); } _onPan(action) { let parent = this.get_parent(); while (!(parent instanceof St.ScrollView)) { if (!parent) return false; parent = parent.get_parent(); } this._clickAction.release(); return this._menuLayout._onPan(action, parent); } _onClicked(action) { const isPrimaryOrTouch = action.get_button() === Clutter.BUTTON_PRIMARY || action.get_button() === 0; if (isPrimaryOrTouch) { this.active = false; this._menuLayout.grab_key_focus(); this.remove_style_pseudo_class('active'); this.activate(Clutter.get_current_event()); } else if (action.get_button() === Clutter.BUTTON_SECONDARY) { if (this.hasContextMenu) this.popupContextMenu(); else this.remove_style_pseudo_class('active'); } else if (action.get_button() === 8) { const backButton = this._menuLayout.backButton; if (backButton && backButton.visible) { this.active = false; this._menuLayout.grab_key_focus(); this.remove_style_pseudo_class('active'); backButton.activate(Clutter.get_current_event()); } } } _onLongPress(action, theActor, state) { const isPrimaryOrTouch = action.get_button() === Clutter.BUTTON_PRIMARY || action.get_button() === 0; if (state === Clutter.LongPressState.QUERY) return isPrimaryOrTouch && this._menuLayout.arcMenu.isOpen && this.hasContextMenu; if (state === Clutter.LongPressState.ACTIVATE && isPrimaryOrTouch) this.popupContextMenu(); return true; } _updateIcon() { if (this.isDestroyed) return; if (!this._iconBin || !this.createIcon) return; const icon = this.createIcon(); if (icon) this._iconBin.set_child(icon); } get actor() { return this; } get active() { return this._active; } set active(active) { if (this.isDestroyed || !this.mapped) return; // Prevent a mouse hover event from setting a new active menu item, until next mouse move event. if (this.hover && this._menuLayout.blockHoverState) { this.hover = false; return; } const activeChanged = active !== this.active; if (activeChanged) { this._active = active; if (active) { const topSearchResult = this._menuLayout.searchResults?.getTopResult(); if (topSearchResult) topSearchResult.remove_style_pseudo_class('active'); // track the active menu item for keyboard navigation if (this._menuLayout.activeMenuItem !== this) { this._menuLayout.activeMenuItem = this; // Ensure the new activeMenuItem is visible in scroll view, only when not hovered. // We don't want to mouse to adjust the scrollview. if (!this.hover) Utils.ensureActorVisibleInScrollView(this); } this._setSelectedStyle(); if (this.can_focus) this.grab_key_focus(); } else { this._removeSelectedStyle(); if (!this.isActiveCategory) this.remove_style_pseudo_class('active'); } this.notify('active'); } } _setSelectedStyle() { if (ShellVersion >= 47) this.add_style_pseudo_class('selected'); else this.add_style_class_name('selected'); } _removeSelectedStyle() { if (ShellVersion >= 47) this.remove_style_pseudo_class('selected'); else this.remove_style_class_name('selected'); } setShouldShow() { // If a saved shortcut link is a desktop app, check if currently installed. // Do NOT display if application not found. if (this._command.endsWith('.desktop') && !Shell.AppSystem.get_default().lookup_app(this._command)) this.shouldShow = false; } _onHover() { if (!this._menuLayout.blockHoverState && this.hover && (this.label || this.tooltipText)) { const tooltipTitle = this.label || this.tooltipText; let {description} = this; if (this._app) description = this._app.get_description(); this._menuButton.tooltip.showTooltip(this, this.tooltipLocation, tooltipTitle, description, this._displayType ? this._displayType : -1); } else if (!this.hover || this._menuLayout.blockHoverState) { this._menuButton.tooltip.hide(); } } vfunc_motion_event() { // Prevent a mouse hover event from setting a new active menu item, until next mouse move event. if (this._menuLayout.blockHoverState) { this._menuLayout.blockHoverState = false; this.hover = true; } return Clutter.EVENT_PROPAGATE; } vfunc_key_focus_in() { super.vfunc_key_focus_in(); this.active = true; } vfunc_key_focus_out() { super.vfunc_key_focus_out(); if (this.contextMenu && this.contextMenu.isOpen) return; this.active = false; this.hover = false; } activate(event) { this.emit('activate', event); } vfunc_key_press_event(event) { this._menuLayout.blockHoverState = true; if (global.focus_manager.navigate_from_event(Clutter.get_current_event())) return Clutter.EVENT_STOP; if (!this._activatable) return super.vfunc_key_press_event(event); let state = event.get_state(); // if user has a modifier down (except capslock and numlock) // then don't handle the key press here state &= ~Clutter.ModifierType.LOCK_MASK; state &= ~Clutter.ModifierType.MOD2_MASK; state &= Clutter.ModifierType.MODIFIER_MASK; if (state) return Clutter.EVENT_PROPAGATE; const symbol = event.get_key_symbol(); if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_KP_Enter) { this.active = false; this._menuLayout.grab_key_focus(); this.activate(Clutter.get_current_event()); return Clutter.EVENT_STOP; } else if (symbol === Clutter.KEY_Menu && this.hasContextMenu) { this.popupContextMenu(); } return Clutter.EVENT_PROPAGATE; } _onDestroy() { if (this._menuButton.tooltip && this._menuButton.tooltip.sourceActor === this) this._menuButton.tooltip.hide(true); this.contextMenu = null; this.isDestroyed = true; this._menuButton = null; this._arcMenu = null; this._menuLayout = null; } } export class ArcMenuSeparator extends PopupMenu.PopupBaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, separatorLength, separatorAlignment, text) { super({ style_class: 'popup-separator-menu-item', reactive: false, can_focus: false, }); this.reactive = true; this.label = new St.Label({ text: text || '', style: 'font-weight: bold', }); this.add_child(this.label); this.label.connectObject('notify::text', this._syncLabelVisibility.bind(this), this); this._syncLabelVisibility(); this._separator = new St.Widget({ style_class: 'popup-separator-menu-item-separator separator-color-style', x_expand: true, y_expand: true, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this._separator); if (separatorAlignment === Constants.SeparatorAlignment.HORIZONTAL) { this.style = 'padding: 0px 5px; margin: 6px 0px;'; switch (separatorLength) { case Constants.SeparatorStyle.EMPTY: this._separator.visible = false; break; case Constants.SeparatorStyle.SHORT: this._separator.style = 'margin: 0px 45px;'; break; case Constants.SeparatorStyle.MEDIUM: this._separator.style = 'margin: 0px 15px;'; break; case Constants.SeparatorStyle.LONG: this._separator.style = 'margin: 0px 5px;'; this.style = 'padding: 0px 5px; margin: 1px 0px;'; break; case Constants.SeparatorStyle.MAX: this._separator.style = 'margin: 0px; padding: 0px;'; break; case Constants.SeparatorStyle.HEADER_LABEL: this._separator.style = 'margin: 0px 4px 0px 10px;'; this.style = 'padding: 5px 15px; margin: 6px 0px;'; break; } } else if (separatorAlignment === Constants.SeparatorAlignment.VERTICAL) { if (separatorLength === Constants.SeparatorStyle.LONG) { this._separator.style = 'margin: 5px 0px; width: 1px;'; this.style = 'padding: 5px 0px; margin: 1px 0px;'; } else if (separatorLength === Constants.SeparatorStyle.MAX) { this._separator.style = 'margin: 0px 0px; width: 1px;'; this.style = 'padding: 0px 0px; margin: 0px 0px;'; } else { this._syncVisibility(); ArcMenuManager.settings.connectObject('changed::vert-separator', this._syncVisibility.bind(this), this); this.style = 'padding: 0px 6px; margin: 6px 0px;'; this._separator.style = 'margin: 0px; width: 1px; height: -1px;'; } this.remove_child(this.label); this.x_expand = this._separator.x_expand = true; this.x_align = this._separator.x_align = Clutter.ActorAlign.CENTER; this.y_expand = this._separator.y_expand = true; this.y_align = this._separator.y_align = Clutter.ActorAlign.FILL; } this.connect('destroy', () => this._onDestroy()); } _onDestroy() { ArcMenuManager.settings.disconnectObject(this); this.label.destroy(); this.label = null; } _syncLabelVisibility() { this.label.visible = this.label.text !== ''; } _syncVisibility() { this._separator.visible = ArcMenuManager.settings.get_boolean('vert-separator'); } } export class ActivitiesMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout) { super(menuLayout); this._iconBin = new St.Bin(); this.add_child(this._iconBin); this._updateIcon(); this.label = new St.Label({ text: _('Activities Overview'), y_expand: true, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this.label); } createIcon() { const iconSizeEnum = ArcMenuManager.settings.get_enum('quicklinks-item-icon-size'); const iconSize = Utils.getIconSize(iconSizeEnum, this._menuLayout.quicklinks_icon_size); return new St.Icon({ icon_name: 'view-fullscreen-symbolic', style_class: 'popup-menu-icon', icon_size: iconSize, }); } activate(event) { Main.overview.show(); super.activate(event); this._menuLayout.arcMenu.toggle(); } } export class Tooltip extends St.Label { static { GObject.registerClass(this); } constructor(menuButton) { super({ name: 'ArcMenu_Tooltip', style_class: 'dash-label arcmenu-tooltip', opacity: 0, }); const {clutterText} = this; clutterText.set({ line_wrap: true, line_wrap_mode: Pango.WrapMode.WORD_CHAR, }); this._menuButton = menuButton; global.stage.add_child(this); this.hide(); this._useTooltips = !ArcMenuManager.settings.get_boolean('disable-tooltips'); ArcMenuManager.settings.connectObject('changed::disable-tooltips', this.disableTooltips.bind(this), this); this.connect('destroy', () => this._onDestroy()); } showTooltip(sourceActor, location, titleLabel, description, displayType) { if (!sourceActor) return; if (this.sourceActor === sourceActor) { this._showTimeout(titleLabel, description, displayType); return; } this.sourceActor = sourceActor; this.location = location; this._showTimeout(titleLabel, description, displayType); } disableTooltips() { this._useTooltips = !ArcMenuManager.settings.get_boolean('disable-tooltips'); } _setToolTipText(titleLabel, description, displayType) { let isEllipsized, titleText; if (titleLabel instanceof St.Label) { const lbl = titleLabel.clutter_text; lbl.get_allocation_box(); isEllipsized = lbl.get_layout().is_ellipsized(); titleText = titleLabel.text.replace(/\n/g, ' '); } else { titleText = titleLabel; } this.text = ''; if (displayType !== Constants.DisplayType.BUTTON) { if (isEllipsized && description) { const text = `${titleText}\n${description}`; this.clutter_text.set_markup(text); } else if (isEllipsized && !description) { this.text = titleText ?? ''; } else if (!isEllipsized && description) { this.text = description ?? ''; } } else if (displayType === Constants.DisplayType.BUTTON) { this.text = titleText ?? ''; } return !!this.text; } _showTimeout(titleLabel, description, displayType) { if (this._useTooltips) { this._menuButton.tooltipShowingID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 750, () => { const shouldShow = this._setToolTipText(titleLabel, description, displayType); if (!shouldShow) { this._menuButton.tooltipShowingID = null; return GLib.SOURCE_REMOVE; } this._show(); this._menuButton.tooltipShowing = true; this._menuButton.tooltipShowingID = null; return GLib.SOURCE_REMOVE; }); } } _show() { if (!this.sourceActor) return; if (this._useTooltips) { this.opacity = 0; this.show(); const [stageX, stageY] = this.sourceActor.get_transformed_position(); const itemWidth = this.sourceActor.allocation.x2 - this.sourceActor.allocation.x1; const itemHeight = this.sourceActor.allocation.y2 - this.sourceActor.allocation.y1; const labelWidth = this.get_width(); const labelHeight = this.get_height(); let x, y; const gap = 5; switch (this.location) { case Constants.TooltipLocation.BOTTOM_CENTERED: y = stageY + itemHeight + gap; x = stageX + Math.floor((itemWidth - labelWidth) / 2); break; case Constants.TooltipLocation.TOP_CENTERED: y = stageY - labelHeight - gap; x = stageX + Math.floor((itemWidth - labelWidth) / 2); break; case Constants.TooltipLocation.BOTTOM: default: y = stageY + itemHeight + gap; x = stageX + gap; break; } // keep the label inside the screen const monitor = Main.layoutManager.findMonitorForActor(this.sourceActor); if (x - monitor.x < gap) x += monitor.x - x + gap; else if (x + labelWidth > monitor.x + monitor.width - gap) x -= x + labelWidth - (monitor.x + monitor.width) + gap; else if (y - monitor.y < gap) y += monitor.y - y + gap; else if (y + labelHeight > monitor.y + monitor.height - gap) y -= y + labelHeight - (monitor.y + monitor.height) + gap; this.set_position(x, y); this.ease({ opacity: 255, duration: TOOLTIP_SHOW_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } } hide(instantHide) { if (this._useTooltips) { if (this._menuButton.tooltipShowingID) { GLib.source_remove(this._menuButton.tooltipShowingID); this._menuButton.tooltipShowingID = null; } this.sourceActor = null; this.ease({ opacity: 0, duration: instantHide ? 0 : TOOLTIP_HIDE_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => super.hide(), }); } } _onDestroy() { if (this._menuButton.tooltipShowingID) { GLib.source_remove(this._menuButton.tooltipShowingID); this._menuButton.tooltipShowingID = null; } global.stage.remove_child(this); this.sourceActor = null; this._menuButton = null; } } export class ArcMenuButtonItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, tooltipText, iconName, gicon) { super(menuLayout); this.set({ x_expand: false, x_align: Clutter.ActorAlign.CENTER, y_expand: false, y_align: Clutter.ActorAlign.CENTER, style_class: 'popup-menu-item arcmenu-button', }); this.tooltipLocation = Constants.TooltipLocation.BOTTOM_CENTERED; this.tooltipText = tooltipText; this.iconName = iconName; this.gicon = gicon; this._closeMenuOnActivate = true; this._displayType = Constants.DisplayType.BUTTON; if (this.iconName !== null) { this._iconBin = new St.Bin(); this.add_child(this._iconBin); this._updateIcon(); } } createIcon(overrideIconSize) { const iconSizeEnum = ArcMenuManager.settings.get_enum('button-item-icon-size'); const iconSize = Utils.getIconSize(iconSizeEnum, this._menuLayout.buttons_icon_size); return new St.Icon({ gicon: this.gicon ? this.gicon : Gio.icon_new_for_string(this.iconName), icon_size: overrideIconSize ? overrideIconSize : iconSize, x_expand: true, x_align: Clutter.ActorAlign.CENTER, }); } setIconSize(size) { if (!this._iconBin) return; this._iconBin.set_child(this.createIcon(size)); } activate(event) { if (this._closeMenuOnActivate) this._menuLayout.arcMenu.toggle(); super.activate(event); } _onDestroy() { this.gicon = null; super._onDestroy(); } } export class PowerOptionsBox extends St.ScrollView { static { GObject.registerClass(this); } constructor(menuLayout, vertical = false) { super({ x_expand: false, overlay_scrollbars: true, clip_to_allocation: true, }); this._orientation = vertical ? Clutter.Orientation.VERTICAL : Clutter.Orientation.HORIZONTAL; const box = new St.BoxLayout({ ...Utils.getOrientationProp(vertical), style: 'spacing: 6px;', }); Utils.addChildToParent(this, box); const powerOptions = ArcMenuManager.settings.get_value('power-options').deep_unpack(); for (let i = 0; i < powerOptions.length; i++) { const [powerType, shouldShow] = powerOptions[i]; if (shouldShow) { const powerButton = new PowerButton(menuLayout, powerType); powerButton.connectObject('key-focus-in', () => Utils.ensureActorVisibleInScrollView(powerButton, this._orientation), this); powerButton.style = 'margin: 0px;'; box.add_child(powerButton); } } } } // 'Power Off / Log Out' button with popupmenu that shows lock, power off, restart, etc export class LeaveButton extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, showLabel) { super(menuLayout); this._closeMenuOnActivate = false; this.iconName = 'system-shutdown-symbolic'; this.showLabel = showLabel; this._createLeaveMenu(); this._iconBin = new St.Bin(); this.add_child(this._iconBin); this._updateIcon(); if (showLabel) { this.label = new St.Label({ text: _('Power Off / Log Out'), y_expand: false, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this.label); } else { this.tooltipLocation = Constants.TooltipLocation.BOTTOM_CENTERED; this.style_class = 'popup-menu-item arcmenu-button'; this.set({ x_expand: false, x_align: Clutter.ActorAlign.CENTER, y_expand: false, y_align: Clutter.ActorAlign.CENTER, }); this._closeMenuOnActivate = true; this._displayType = Constants.DisplayType.BUTTON; this.tooltipText = _('Power Off / Log Out'); } } createIcon(overrideIconSize) { const iconSizeEnum = this.showLabel ? ArcMenuManager.settings.get_enum('quicklinks-item-icon-size') : ArcMenuManager.settings.get_enum('button-item-icon-size'); const defaultIconSize = this.showLabel ? this._menuLayout.quicklinks_icon_size : this._menuLayout.buttons_icon_size; const iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); return new St.Icon({ gicon: Gio.icon_new_for_string(this.iconName), icon_size: overrideIconSize ? overrideIconSize : iconSize, x_expand: !this.showLabel, x_align: this.showLabel ? Clutter.ActorAlign.START : Clutter.ActorAlign.CENTER, }); } setIconSize(size) { if (!this._iconBin) return; this._iconBin.set_child(this.createIcon(size)); } _createLeaveMenu() { this.leaveMenu = new PopupMenu.PopupMenu(this, 0.5, St.Side.BOTTOM); this.leaveMenu.blockSourceEvents = true; this.leaveMenu.actor.add_style_class_name('popup-menu arcmenu-menu'); const section = new PopupMenu.PopupMenuSection(); this.leaveMenu.addMenuItem(section); const box = new St.BoxLayout({...Utils.getOrientationProp(true)}); box._delegate = box; section.actor.add_child(box); const sessionBox = new St.BoxLayout({...Utils.getOrientationProp(true)}); sessionBox.add_child(this._menuLayout.createLabelRow(_('Session'))); box.add_child(sessionBox); const systemBox = new St.BoxLayout({...Utils.getOrientationProp(true)}); systemBox.add_child(this._menuLayout.createLabelRow(_('System'))); box.add_child(systemBox); let hasSessionOption, hasSystemOption; const powerOptions = ArcMenuManager.settings.get_value('power-options').deep_unpack(); for (let i = 0; i < powerOptions.length; i++) { const [powerType, shouldShow] = powerOptions[i]; if (shouldShow) { const powerButton = new PowerMenuItem(this._menuLayout, powerType); powerButton.connectObject('activate', () => { this.leaveMenu.toggle(); }, this); if (powerType === Constants.PowerType.LOCK || powerType === Constants.PowerType.LOGOUT || powerType === Constants.PowerType.SWITCH_USER) { hasSessionOption = true; sessionBox.add_child(powerButton); } else { hasSystemOption = true; systemBox.add_child(powerButton); } } } if (!hasSessionOption) sessionBox.hide(); if (!hasSystemOption) systemBox.hide(); this._menuLayout.subMenuManager.addMenu(this.leaveMenu); this.leaveMenu.actor.hide(); Main.uiGroup.add_child(this.leaveMenu.actor); this.leaveMenu.connect('open-state-changed', (menu, open) => { if (open) { this.add_style_pseudo_class('active'); if (this._menuButton.tooltipShowingID) { GLib.source_remove(this._menuButton.tooltipShowingID); this._menuButton.tooltipShowingID = null; this._menuButton.tooltipShowing = false; } if (this.tooltip) { this.tooltip.hide(); this._menuButton.tooltipShowing = false; } } else { this.remove_style_pseudo_class('active'); this.active = false; this.sync_hover(); this.hovered = this.hover; } }); } _onDestroy() { Main.uiGroup.remove_child(this.leaveMenu.actor); this.leaveMenu.destroy(); this.leaveMenu = null; } activate(event) { super.activate(event); this.leaveMenu.toggle(); } } export class PowerButton extends ArcMenuButtonItem { static { GObject.registerClass(this); } constructor(menuLayout, powerType) { super(menuLayout, Constants.PowerOptions[powerType].NAME, Constants.PowerOptions[powerType].ICON); this.powerType = powerType; const binding = bindPowerItemVisibility(this); this.connect('destroy', () => binding?.unbind()); } activate() { activatePowerOption(this.powerType); } } export class PowerMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, type) { super(menuLayout); this.powerType = type; const binding = bindPowerItemVisibility(this); this._iconBin = new St.Bin(); this.add_child(this._iconBin); this._updateIcon(); this.label = new St.Label({ text: _(Constants.PowerOptions[this.powerType].NAME), y_expand: false, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this.label); this.connect('destroy', () => binding?.unbind()); } createIcon() { const iconSizeEnum = ArcMenuManager.settings.get_enum('quicklinks-item-icon-size'); const iconSize = Utils.getIconSize(iconSizeEnum, this._menuLayout.quicklinks_icon_size); return new St.Icon({ gicon: Gio.icon_new_for_string(Constants.PowerOptions[this.powerType].ICON), style_class: 'popup-menu-icon', icon_size: iconSize, }); } activate(event) { super.activate(event); this._menuLayout.arcMenu.toggle(); activatePowerOption(this.powerType); } } export class NavigationButton extends ArcMenuButtonItem { static { GObject.registerClass(this); } constructor(menuLayout, text, arrowSymbolic, activateAction, arrowSide) { super(menuLayout, null, arrowSymbolic); this.activateAction = activateAction; this.set({ style: 'min-height: 28px; padding: 0px 8px;', x_expand: true, x_align: Clutter.ActorAlign.END, }); this._closeMenuOnActivate = false; this._label = new St.Label({ text: _(text), x_expand: true, x_align: Clutter.ActorAlign.FILL, y_expand: false, y_align: Clutter.ActorAlign.CENTER, }); if (arrowSide === St.Side.LEFT) this.add_child(this._label); else this.insert_child_at_index(this._label, 0); } createIcon() { const iconSizeEnum = ArcMenuManager.settings.get_enum('misc-item-icon-size'); const iconSize = Utils.getIconSize(iconSizeEnum, Constants.EXTRA_SMALL_ICON_SIZE); return new St.Icon({ gicon: this.gicon ? this.gicon : Gio.icon_new_for_string(this.iconName), icon_size: iconSize, x_expand: true, x_align: Clutter.ActorAlign.CENTER, }); } activate(event) { super.activate(event); this.activateAction(); } } export class GoNextButton extends NavigationButton { static { GObject.registerClass(this); } constructor(menuLayout, title, activateAction) { super(menuLayout, _(title), 'go-next-symbolic', () => activateAction()); } } export class GoPreviousButton extends NavigationButton { static { GObject.registerClass(this); } constructor(menuLayout, activateAction) { super(menuLayout, _('Back'), 'go-previous-symbolic', () => activateAction(), St.Side.LEFT); } } // Menu item to go back to category view export class BackButton extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout) { super(menuLayout); this._iconBin = new St.Bin({ x_expand: false, x_align: Clutter.ActorAlign.START, }); this.add_child(this._iconBin); this._updateIcon(); const label = new St.Label({ text: _('Back'), x_expand: false, x_align: Clutter.ActorAlign.START, y_expand: true, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(label); } createIcon() { const iconSizeEnum = ArcMenuManager.settings.get_enum('misc-item-icon-size'); const iconSize = Utils.getIconSize(iconSizeEnum, Constants.MISC_ICON_SIZE); return new St.Icon({ icon_name: 'go-previous-symbolic', icon_size: iconSize, style_class: 'popup-menu-icon', }); } activate(event) { const layout = ArcMenuManager.settings.get_enum('menu-layout'); if (layout === Constants.MenuLayout.ARCMENU) { // If the current page is inside a category and // previous page was the categories page, // go back to categories page if (this._menuLayout.previousCategoryType === Constants.CategoryType.CATEGORIES_LIST && (this._menuLayout.activeCategoryType <= 4 || this._menuLayout.activeCategoryType instanceof GMenu.TreeDirectory)) this._menuLayout.displayCategories(); else this._menuLayout.setDefaultMenuView(); } else if (layout === Constants.MenuLayout.TOGNEE) { this._menuLayout.setDefaultMenuView(); } super.activate(event); } } // Menu item to view all apps export class ViewAllAppsButton extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout) { super(menuLayout); const label = new St.Label({ text: _('All Apps'), x_expand: false, x_align: Clutter.ActorAlign.START, y_expand: true, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(label); this._iconBin = new St.Bin({ x_expand: false, x_align: Clutter.ActorAlign.START, }); this.add_child(this._iconBin); this._updateIcon(); } createIcon() { const iconSizeEnum = ArcMenuManager.settings.get_enum('misc-item-icon-size'); const iconSize = Utils.getIconSize(iconSizeEnum, Constants.MISC_ICON_SIZE); return new St.Icon({ icon_name: 'go-next-symbolic', icon_size: iconSize, x_align: Clutter.ActorAlign.START, style_class: 'popup-menu-icon', }); } activate(event) { const showAppsAction = ArcMenuManager.settings.get_enum('all-apps-button-action'); if (showAppsAction === Constants.AllAppsButtonAction.ALL_PROGRAMS) { this._menuLayout.displayAllApps(); super.activate(event); return; } const defaultMenuView = ArcMenuManager.settings.get_enum('default-menu-view'); if (defaultMenuView === Constants.DefaultMenuView.PINNED_APPS || defaultMenuView === Constants.DefaultMenuView.FREQUENT_APPS) this._menuLayout.displayCategories(); else this._menuLayout.displayAllApps(); super.activate(event); } } export class ShortcutMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, itemData, displayType, isContainedInCategory) { super(menuLayout); this._displayType = displayType; this.isContainedInCategory = isContainedInCategory; let name = itemData.name ?? ''; const {icon, id} = itemData; this._command = id ?? ''; this.iconName = icon ?? ''; const shortcutIconType = ArcMenuManager.settings.get_enum('shortcut-icon-type'); if (shortcutIconType === Constants.CategoryIconType.FULL_COLOR) this.add_style_class_name('regular-icons'); else this.add_style_class_name('symbolic-icons'); // Check for default commands-------- if (this._command === Constants.ShortcutCommands.SOFTWARE) this._command = Utils.findSoftwareManager(); if (!this._app) this._app = this._menuLayout.appSys.lookup_app(this._command); if (this._app && !this.iconName) { const appIcon = this._app.create_icon_texture(Constants.MEDIUM_ICON_SIZE); if (appIcon instanceof St.Icon) this.iconName = appIcon.gicon.to_string(); } if (!name && this._app) name = this._app.get_name(); // ------------------------------------- this.hasContextMenu = !!this._app; this._iconBin = new St.Bin(); this.add_child(this._iconBin); this._updateIcon(); this.label = new St.Label({ text: _(name), y_expand: true, y_align: Clutter.ActorAlign.CENTER, }); const layout = ArcMenuManager.settings.get_enum('menu-layout'); if (layout === Constants.MenuLayout.PLASMA && ArcMenuManager.settings.get_boolean('apps-show-extra-details') && this._app) { const labelBox = new St.BoxLayout({ ...Utils.getOrientationProp(true), }); const descriptionLabel = new St.Label({ text: this._app.get_description(), y_expand: true, y_align: Clutter.ActorAlign.CENTER, style: 'font-weight: lighter;', }); labelBox.add_child(this.label); if (this._app.get_description()) labelBox.add_child(descriptionLabel); this.add_child(labelBox); } else { this.add_child(this.label); } if (this._displayType === Constants.DisplayType.GRID) Utils.convertToGridLayout(this); else if (this._displayType === Constants.DisplayType.BUTTON) Utils.convertToButton(this); this.setShouldShow(); } createIcon() { let iconSizeEnum; if (this.isContainedInCategory) iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-icon-size'); else iconSizeEnum = ArcMenuManager.settings.get_enum('quicklinks-item-icon-size'); let defaultIconSize, iconSize; if (this._displayType === Constants.DisplayType.BUTTON) { iconSizeEnum = ArcMenuManager.settings.get_enum('button-item-icon-size'); defaultIconSize = this._menuLayout.buttons_icon_size; iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); this.style = `min-width: ${iconSize}px; min-height: ${iconSize}px;`; } else if (this._displayType === Constants.DisplayType.GRID) { iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-grid-icon-size'); defaultIconSize = this._menuLayout.icon_grid_size; ({iconSize} = Utils.getGridIconSize(iconSizeEnum, defaultIconSize)); } else { defaultIconSize = this.isContainedInCategory ? this._menuLayout.apps_icon_size : this._menuLayout.quicklinks_icon_size; iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); } return new St.Icon({ icon_name: this.iconName, gicon: Gio.icon_new_for_string(this.iconName), style_class: this._displayType === Constants.DisplayType.LIST ? 'popup-menu-icon' : '', icon_size: iconSize, }); } popupContextMenu() { if (this._app && this.contextMenu === undefined) { this.contextMenu = new AppContextMenu(this, this._menuLayout); if (this._menuLayout.context_menu_location === Constants.ContextMenuLocation.BOTTOM_CENTERED) this.contextMenu.centerBoxPointerPosition(); else if (this._menuLayout.context_menu_location === Constants.ContextMenuLocation.RIGHT) this.contextMenu.rightBoxPointerPosition(); if (this._app) this.contextMenu.setApp(this._app); else if (this.folderPath) this.contextMenu.setFolderPath(this.folderPath); } if (this.contextMenu !== undefined) { if (this.tooltip !== undefined) this.tooltip.hide(); this.contextMenu.open(BoxPointer.PopupAnimation.FULL); } } activate() { switch (this._command) { case Constants.ShortcutCommands.LOG_OUT: case Constants.ShortcutCommands.LOCK: case Constants.ShortcutCommands.POWER_OFF: case Constants.ShortcutCommands.RESTART: case Constants.ShortcutCommands.SUSPEND: case Constants.ShortcutCommands.HIBERNATE: case Constants.ShortcutCommands.HYBRID_SLEEP: case Constants.ShortcutCommands.SWITCH_USER: { const powerType = Utils.getPowerTypeFromShortcutCommand(this._command); activatePowerOption(powerType); break; } case Constants.ShortcutCommands.OVERVIEW: Main.overview.show(); break; case Constants.ShortcutCommands.RUN_COMMAND: Main.openRunDialog(); break; case Constants.ShortcutCommands.SHOW_APPS: Main.overview._overview._controls._toggleAppsPage(); break; default: { if (this._app) this._app.open_new_window(-1); else Util.spawnCommandLine(this._command); } } this._menuLayout.arcMenu.toggle(); } } export class AvatarMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, displayType) { super(menuLayout); this._displayType = displayType; if (ArcMenuManager.settings.get_enum('avatar-style') === Constants.AvatarStyle.ROUND) this.avatarStyle = 'arcmenu-avatar-round'; else this.avatarStyle = 'arcmenu-avatar-square'; const iconSizeEnum = ArcMenuManager.settings.get_enum('misc-item-icon-size'); const iconSize = Utils.getIconSize(iconSizeEnum, USER_AVATAR_SIZE); const avatarMenuIcon = new AvatarMenuIcon(menuLayout, iconSize, false); this.add_child(avatarMenuIcon); this.label = avatarMenuIcon.label; this.add_child(this.label); if (this._displayType === Constants.DisplayType.BUTTON) Utils.convertToButton(this); } activate(event) { Util.spawnCommandLine('gnome-control-center user-accounts'); this._menuLayout.arcMenu.toggle(); super.activate(event); } } export class AvatarMenuIcon extends St.Bin { static { GObject.registerClass(this); } constructor(menuLayout, iconSize, hasTooltip) { let avatarStyle; if (ArcMenuManager.settings.get_enum('avatar-style') === Constants.AvatarStyle.ROUND) avatarStyle = 'arcmenu-avatar-round'; else avatarStyle = 'arcmenu-avatar-square'; super({ style_class: `${avatarStyle} user-icon popup-menu-icon`, track_hover: true, reactive: true, x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, style: `width: ${iconSize}px; height: ${iconSize}px;`, }); this._menuButton = menuLayout.menuButton; this._menuLayout = menuLayout; this.iconSize = iconSize; this.tooltipLocation = Constants.TooltipLocation.BOTTOM_CENTERED; this._user = AccountsService.UserManager.get_default().get_user(GLib.get_user_name()); this.label = new St.Label({ text: GLib.get_real_name(), y_align: Clutter.ActorAlign.CENTER, }); this._user.connectObject('notify::is-loaded', this._onUserChanged.bind(this), this); this._user.connectObject('changed', this._onUserChanged.bind(this), this); if (hasTooltip) this.connect('notify::hover', this._onHover.bind(this)); this._onUserChanged(); this.connect('destroy', () => this._onDestroy()); } _onDestroy() { this._user.disconnectObject(this); this._menuButton = null; this._menuLayout = null; this._user = null; this.label = null; } _onHover() { if (this.hover) { this._menuButton.tooltip.showTooltip(this, this.tooltipLocation, GLib.get_real_name(), null, Constants.DisplayType.BUTTON); } else { this._menuButton.tooltip.hide(); } } _onUserChanged() { if (this._user.is_loaded) { this.label.set_text(this._user.get_real_name()); if (this.tooltip) this.tooltip.titleLabel.text = this._user.get_real_name(); let iconFile = this._user.get_icon_file(); if (iconFile && !GLib.file_test(iconFile, GLib.FileTest.EXISTS)) iconFile = null; if (iconFile) { if (this.child) this.child.destroy(); this.child = null; this.add_style_class_name('user-avatar'); this.style = `${'background-image: url("%s");'.format(iconFile)}width: ${this.iconSize}px; height: ${this.iconSize}px;`; } else { this.style = `width: ${this.iconSize}px; height: ${this.iconSize}px;`; this.child = new St.Icon({ icon_name: 'avatar-default-symbolic', icon_size: this.iconSize, style: `padding: 5px; width: ${this.iconSize}px; height: ${this.iconSize}px;`, }); } } } } export class DraggableMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, displayType, isDraggable = true) { super(menuLayout); this._displayType = displayType; this._folderPreviewId = 0; this._otherIconIsHovering = false; this._delayedMoveData = null; this._iconBin = new St.Bin(); this.add_child(this._iconBin); this.label = new St.Label({ x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.CENTER, }); if (isDraggable) { this.remove_action(this._panAction); this.remove_action(this._clickAction); this._panAction = null; this._draggable = DND.makeDraggable(this, {timeoutThreshold: 400}); this._draggable.addClickAction(this._clickAction); this._draggable._animateDragEnd = eventTime => { this._draggable._animationInProgress = true; this._draggable._onAnimationComplete(this._draggable._dragActor, eventTime); }; this._draggable.connect('drag-begin', this._onDragBegin.bind(this)); this._draggable.connect('drag-end', this._onDragEnd.bind(this)); } if (this._displayType === Constants.DisplayType.GRID) Utils.convertToGridLayout(this); } createIcon() { throw new Error('createIcon() Not yet implemented'); } _onDragBegin() { this.isDragging = true; if (this._menuButton.tooltipShowingID) { GLib.source_remove(this._menuButton.tooltipShowingID); this._menuButton.tooltipShowingID = null; this._menuButton.tooltipShowing = false; } if (this.tooltip) { this.tooltip.hide(); this._menuButton.tooltipShowing = false; } if (this.contextMenu && this.contextMenu.isOpen) this.contextMenu.toggle(); this.scaleAndFade(); this._dragMonitor = { dragMotion: this._onDragMotion.bind(this), }; DND.addDragMonitor(this._dragMonitor); } _onDragMotion(dragEvent) { const parent = this.get_parent(); const layoutManager = parent.layout_manager; const [success, x, y] = parent.transform_stage_point(dragEvent.x, dragEvent.y); if (!success) return DND.DragMotionResult.CONTINUE; const {source} = dragEvent; const [index, dragLocation] = this._getDropTarget(x, y, source, layoutManager); const item = index !== -1 ? layoutManager.getItemAt(index) : null; Utils.ensureActorVisibleInScrollView(this, Clutter.Orientation.VERTICAL, {x1: x, x2: x + 25, y1: y, y2: y + 25}); // Dragging over invalid parts of the grid cancels the timeout if (!item || item === source || dragLocation === DragLocation.INVALID || dragLocation === DragLocation.ON_ICON) { this._removeDelayedMove(); return DND.DragMotionResult.CONTINUE; } if (!this._delayedMoveData || this._delayedMoveData.index !== index) { // Update the item with a small delay this._removeDelayedMove(); this._delayedMoveData = { index, source, destroyId: source.connect('destroy', () => this._removeDelayedMove()), timeoutId: GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => { parent.moveItem(this, index); this._delayedMoveData.timeoutId = 0; this._removeDelayedMove(); return GLib.SOURCE_REMOVE; }), }; } return DND.DragMotionResult.CONTINUE; } _removeDelayedMove() { if (!this._delayedMoveData) return; const {source, destroyId, timeoutId} = this._delayedMoveData; if (timeoutId > 0) GLib.source_remove(timeoutId); if (destroyId > 0) source.disconnect(destroyId); this._delayedMoveData = null; } _getDropTarget(x, y, source, layoutManager) { const sourceIndex = layoutManager.getItemPosition(source); let [targetIndex, dragLocation] = layoutManager.getDropTarget(x, y); let reflowDirection = Clutter.ActorAlign.END; if (sourceIndex === targetIndex) reflowDirection = -1; if (targetIndex > sourceIndex) reflowDirection = Clutter.ActorAlign.START; else reflowDirection = Clutter.ActorAlign.END; if (dragLocation === DragLocation.START_EDGE && reflowDirection === Clutter.ActorAlign.START) { const nColumns = layoutManager.columns; const targetColumn = targetIndex % nColumns; if (targetColumn > 0) { targetIndex -= 1; dragLocation = DragLocation.END_EDGE; } } else if (dragLocation === DragLocation.END_EDGE && reflowDirection === Clutter.ActorAlign.END) { const nColumns = layoutManager.columns; const targetColumn = targetIndex % nColumns; if (targetColumn < nColumns - 1) { targetIndex += 1; dragLocation = DragLocation.START_EDGE; } } else if (dragLocation === DragLocation.TOP_EDGE && reflowDirection === Clutter.ActorAlign.START) { const nColumns = layoutManager.columns; const targetRow = Math.floor(targetIndex / nColumns); if (targetRow > 0) { targetIndex = Math.max(0, targetIndex - nColumns); dragLocation = DragLocation.BOTTOM_EDGE; } } else if (dragLocation === DragLocation.BOTTOM_EDGE && reflowDirection === Clutter.ActorAlign.END) { const nChildren = layoutManager.nChildren; const nColumns = layoutManager.columns; const nRows = Math.ceil(nChildren / nColumns); const targetRow = Math.floor(targetIndex / nColumns); if (targetRow < nRows - 1) { targetIndex = Math.min(nChildren - 1, targetIndex + nColumns); dragLocation = DragLocation.TOP_EDGE; } } return [targetIndex, dragLocation]; } _onDragEnd() { if (this._dragMonitor) { DND.removeDragMonitor(this._dragMonitor); this._dragMonitor = null; } if (!this.movedToFolder) this.undoScaleAndFade(); } scaleAndFade() { this.reactive = false; this.ease({ scale_x: 0.5, scale_y: 0.5, opacity: 0, }); } undoScaleAndFade() { this.reactive = true; this.ease({ scale_x: 1.0, scale_y: 1.0, opacity: 255, }); } _setHoveringByDnd(hovering) { if (this._otherIconIsHovering === hovering) return; this._otherIconIsHovering = hovering; if (hovering) { this._hoveringDragMonitor = { dragMotion: this._onHoveringDragMotion.bind(this), }; DND.addDragMonitor(this._hoveringDragMonitor); if (this._folderPreviewId > 0) return; this._folderPreviewId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { this.add_style_pseudo_class('drop'); this._showFolderPreview(); this._folderPreviewId = 0; return GLib.SOURCE_REMOVE; }); } else { DND.removeDragMonitor(this._hoveringDragMonitor); if (this._folderPreviewId > 0) { GLib.source_remove(this._folderPreviewId); this._folderPreviewId = 0; } this._hideFolderPreview(); this.remove_style_pseudo_class('drop'); } } _onHoveringDragMotion(dragEvent) { if (!this.contains(dragEvent.targetActor)) this._setHoveringByDnd(false); return DND.DragMotionResult.CONTINUE; } _showFolderPreview() { this.label.opacity = 0; this._iconBin.ease({ scale_x: .5, scale_y: .5, }); } _hideFolderPreview() { this.label.opacity = 255; this._iconBin.ease({ scale_x: 1.0, scale_y: 1.0, }); } handleDragOver(source, _actor, x, y) { if (source === this) return DND.DragMotionResult.NO_DROP; if (!this._canAccept(source)) return DND.DragMotionResult.CONTINUE; if (this._withinLeeways(x, y)) { this._setHoveringByDnd(false); return DND.DragMotionResult.CONTINUE; } this._setHoveringByDnd(true); return DND.DragMotionResult.MOVE_DROP; } _canAccept(source) { return source !== this && !this.disableAcceptDrop && !source.disableAcceptDrop && !source.disableAcceptDropAsSource; } _withinLeeways(x, y) { const leftDividerLeeway = Math.round(this.get_preferred_width(-1)[1] / 5); const rightDividerLeeway = Math.round(this.get_preferred_width(-1)[1] / 5); const topDividerLeeway = Math.round(this.get_preferred_height(-1)[1] / 5); const bottomDividerLeeway = Math.round(this.get_preferred_height(-1)[1] / 5); return x < leftDividerLeeway || x > this.width - rightDividerLeeway || y < topDividerLeeway || y > this.height - bottomDividerLeeway; } acceptDrop(source, _actor, x, y) { this._setHoveringByDnd(false); if (!this._canAccept(source)) return false; if (this._withinLeeways(x, y)) return false; return true; } getDragActor() { throw new Error('getDragActor() Not yet implemented'); } getDragActorSource() { return this; } cancelActions() { if (this._draggable) this._draggable.fakeRelease(); DND.removeDragMonitor(this._hoveringDragMonitor); if (this._dragMonitor) { DND.removeDragMonitor(this._dragMonitor); this._dragMonitor = null; } } _onDestroy() { this.cancelActions(); this._draggable = null; super._onDestroy(); if (this._folderPreviewId > 0) { GLib.source_remove(this._folderPreviewId); this._folderPreviewId = 0; } } } export class PinnedAppsFolderMenuItem extends DraggableMenuItem { static [GObject.signals] = {'pinned-apps-changed': {param_types: [GObject.TYPE_JSOBJECT]}}; static { GObject.registerClass(this); } constructor(menuLayout, pinnedAppData, folderSettings, folderAppList, displayType, isContainedInCategory) { super(menuLayout, displayType); this._displayType = displayType; this.isContainedInCategory = isContainedInCategory; this._folderAppList = folderAppList; this.folderSettings = folderSettings; this._name = pinnedAppData.name; this._command = pinnedAppData.id; this.appList = []; this._subMenuPopup = new FolderDialog(this, this._menuLayout); this.disableAcceptDropAsSource = true; this.folderSettings.connectObject('changed::pinned-apps', () => { this._folderAppList = folderSettings.get_value('pinned-apps').deepUnpack(); this._loadPinnedApps(); if (this.appList) this._updateIcon(); }, this); this._loadPinnedApps(); this.hasContextMenu = true; this._addFolderNameEntry(); this.add_child(this.label); this.updateData(pinnedAppData); } updateData(pinnedAppData) { this.pinnedAppData = pinnedAppData; this._name = pinnedAppData.name; this._command = pinnedAppData.id; this._folderNameLabel.text = this._name; this._entry.text = this._name; this.label.text = this._name; this._updateIcon(); } _addFolderNameEntry() { this._entryBox = new St.BoxLayout({ style_class: 'folder-name-container', style: 'padding: 10px 18px;', }); this._subMenuPopup.box.insert_child_at_index(this._entryBox, 0); // Empty actor to center the title const ghostButton = new Clutter.Actor(); this._entryBox.add_child(ghostButton); const stack = new Shell.Stack({ x_expand: true, x_align: Clutter.ActorAlign.CENTER, }); this._entryBox.add_child(stack); // Folder name label this._folderNameLabel = new St.Label({ x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, style_class: 'folder-header', }); stack.add_child(this._folderNameLabel); // Folder name entry this._entry = new St.Entry({ opacity: 0, reactive: false, style_class: 'folder-header', style: 'width: 12em;', }); this._entry.clutter_text.set({ x_expand: true, x_align: Clutter.ActorAlign.CENTER, }); this._entry.clutter_text.connect('activate', () => { this._showFolderLabel(); }); this._entry.clutter_text.connect('key-focus-out', () => { const hasKeyFocus = this._entry.clutter_text.has_key_focus(); if (!hasKeyFocus && this._editButton.checked) this._showFolderLabel(); }); stack.add_child(this._entry); // Edit button this._editButton = new St.Button({ style_class: 'icon-button', button_mask: St.ButtonMask.ONE, toggle_mode: true, reactive: true, can_focus: true, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, icon_name: 'document-edit-symbolic', }); this._editButton.connect('notify::checked', () => { if (this._editButton.checked) this._showFolderEntry(); else this._showFolderLabel(); }); this._entryBox.add_child(this._editButton); ghostButton.add_constraint(new Clutter.BindConstraint({ source: this._editButton, coordinate: Clutter.BindCoordinate.SIZE, })); this._subMenuPopup.connect('open-state-changed', (menu, isOpen) => { if (!isOpen) this._showFolderLabel(); }); } _switchActor(from, to) { to.reactive = true; to.ease({ opacity: 255, duration: 300, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); from.ease({ opacity: 0, duration: 300, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { from.reactive = false; }, }); } _showFolderLabel() { if (this._editButton.checked) this._editButton.checked = false; this._maybeUpdateFolderName(); this._switchActor(this._entry, this._folderNameLabel); } _showFolderEntry() { this._switchActor(this._folderNameLabel, this._entry); this._entry.clutter_text.set_selection(0, -1); this._entry.clutter_text.grab_key_focus(); } _maybeUpdateFolderName() { const folderName = this._name; const newFolderName = this._entry.text.trim(); if (newFolderName.length === 0 || newFolderName === folderName) return; const pinnedAppsList = ArcMenuManager.settings.get_value('pinned-apps').deepUnpack(); const parent = this.get_parent(); const index = parent.getItemPosition(this); pinnedAppsList[index].name = newFolderName; ArcMenuManager.settings.set_value('pinned-apps', new GLib.Variant('aa{ss}', pinnedAppsList)); } _loadPinnedApps() { if (this._menuButton.tooltipShowingID) { GLib.source_remove(this._menuButton.tooltipShowingID); this._menuButton.tooltipShowingID = null; this._menuButton.tooltipShowing = false; } if (this.tooltip) { this.tooltip.hide(); this._menuButton.tooltipShowing = false; } for (let i = this.appList.length - 1; i >= 0; --i) { const item = this.appList[i]; item.disconnectObject(this); item.destroy(); this.appList[i] = null; } this.appList = null; this.appList = []; // No apps in folder, clear folder data and remove from pinned apps if (this._folderAppList.length === 0) { const keys = this.folderSettings.settings_schema.list_keys(); for (const key of keys) this.folderSettings.reset(key); const mainPinnedAppsList = ArcMenuManager.settings.get_value('pinned-apps').deepUnpack(); for (let i = 0; i < mainPinnedAppsList.length; i++) { if (mainPinnedAppsList[i].id === this._command) { mainPinnedAppsList.splice(i, 1); ArcMenuManager.settings.set_value('pinned-apps', new GLib.Variant('aa{ss}', mainPinnedAppsList)); break; } } return; } for (let i = 0; i < this._folderAppList.length; i++) { const pinnedApp = this._folderAppList[i]; const pinnedAppsMenuItem = new PinnedAppsMenuItem(this._menuLayout, pinnedApp, Constants.DisplayType.GRID, this.isContainedInCategory); if (!pinnedAppsMenuItem.shouldShow) { pinnedAppsMenuItem.destroy(); continue; } pinnedAppsMenuItem.folderSettings = this.folderSettings; pinnedAppsMenuItem.disableAcceptDrop = true; pinnedAppsMenuItem.folderId = this._command; pinnedAppsMenuItem.connectObject('pinned-apps-changed', (_self, pinnedAppsList) => { const array = []; for (let j = 0; j < pinnedAppsList.length; j++) array.push(pinnedAppsList[j].pinnedAppData); this.folderSettings.set_value('pinned-apps', new GLib.Variant('aa{ss}', array)); }, this); this.appList.push(pinnedAppsMenuItem); } this.populateMenu(); } createIcon() { let iconSize; if (this._displayType === Constants.DisplayType.GRID) { this._iconBin.x_align = Clutter.ActorAlign.CENTER; const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-grid-icon-size'); const defaultIconSize = this._menuLayout.icon_grid_size; ({iconSize} = Utils.getGridIconSize(iconSizeEnum, defaultIconSize)); } else { const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-icon-size'); const defaultIconSize = this.isContainedInCategory ? this._menuLayout.apps_icon_size : this._menuLayout.pinned_apps_icon_size; iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); } if (!this.appList.length) { const icon = new St.Icon({ style_class: 'popup-menu-icon', icon_size: iconSize, icon_name: 'folder-directory-symbolic', }); return icon; } const layout = new Clutter.GridLayout({ row_homogeneous: true, column_homogeneous: true, }); const iconWidget = new St.Widget({ layout_manager: layout, style: `width: ${iconSize}px; height: ${iconSize}px;`, }); const subSize = Math.floor(.4 * iconSize); const numItems = this.appList.length; const rtl = iconWidget.get_text_direction() === Clutter.TextDirection.RTL; for (let i = 0; i < 4; i++) { const style = `width: ${subSize}px; height: ${subSize}px;`; const bin = new St.Bin({style}); if (i < numItems) { const icon = this.appList[i].createIcon(); icon.icon_size = subSize; bin.child = icon; } layout.attach(bin, rtl ? (i + 1) % 2 : i % 2, Math.floor(i / 2), 1, 1); } return iconWidget; } popupContextMenu() { if (this.contextMenu === undefined) { this.contextMenu = new AppContextMenu(this, this._menuLayout); if (this._displayType === Constants.DisplayType.GRID) this.contextMenu.centerBoxPointerPosition(); this.contextMenu.addUnpinItem(this._command, this.folderSettings); } if (this.tooltip !== undefined) this.tooltip.hide(); this.contextMenu.open(BoxPointer.PopupAnimation.FULL); } getDragActor() { return this.createIcon(); } _onDragEnd() { super._onDragEnd(); const parent = this.get_parent(); if (!parent) return; const layoutManager = parent.layout_manager; const pinnedAppsArray = layoutManager.getChildren(); this.emit('pinned-apps-changed', pinnedAppsArray); } acceptDrop(source, actor, x, y) { const acceptDrop = super.acceptDrop(source, actor, x, y); if (!acceptDrop) return false; const sourceData = source.pinnedAppData; source.cancelActions(); const parent = this.get_parent(); const layoutManager = parent.layout_manager; const pinnedAppsList = ArcMenuManager.settings.get_value('pinned-apps').deepUnpack(); const index = layoutManager.getItemPosition(source); pinnedAppsList.splice(index, 1); ArcMenuManager.settings.set_value('pinned-apps', new GLib.Variant('aa{ss}', pinnedAppsList)); // add to folder pinned app list const folderPinnedAppsList = this.folderSettings.get_value('pinned-apps').deepUnpack(); folderPinnedAppsList.push(sourceData); return this.folderSettings.set_value('pinned-apps', new GLib.Variant('aa{ss}', folderPinnedAppsList)); } _canAccept(source) { const canAccept = super._canAccept(source); for (let i = 0; i < this._folderAppList.length; i++) { if (this._folderAppList[i].id === source.pinnedAppData.id) return false; } return canAccept; } _showFolderPreview() { } _hideFolderPreview() { } populateMenu() { this._subMenuPopup.populateMenu(this.appList); } activate(event) { this._subMenuPopup.toggle(); super.activate(event); } _onDestroy() { this.folderSettings.disconnectObject(this); this.folderSettings = null; this._subMenuPopup.destroy(); this._subMenuPopup = null; this.appList = null; this.pinnedAppData = null; super._onDestroy(); } } export class PinnedAppsMenuItem extends DraggableMenuItem { static [GObject.signals] = {'pinned-apps-changed': {param_types: [GObject.TYPE_JSOBJECT]}}; static { GObject.registerClass(this); } constructor(menuLayout, pinnedAppData, displayType, isContainedInCategory) { super(menuLayout, displayType); this._displayType = displayType; this.isContainedInCategory = isContainedInCategory; this.hasContextMenu = true; this.updateData(pinnedAppData); const showExtraDetails = ArcMenuManager.settings.get_boolean('apps-show-extra-details'); if (this._displayType === Constants.DisplayType.LIST && showExtraDetails && this._app && this._app.get_description()) { const labelBox = new St.BoxLayout({...Utils.getOrientationProp(true)}); const descriptionLabel = new St.Label({ text: this._app.get_description(), y_expand: true, y_align: Clutter.ActorAlign.CENTER, style: 'font-weight: lighter;', }); labelBox.add_child(this.label); labelBox.add_child(descriptionLabel); this.add_child(labelBox); } else { this.add_child(this.label); } this.setShouldShow(); } updateData(pinnedAppData) { this.pinnedAppData = pinnedAppData; this._name = pinnedAppData.name ?? ''; this._icon = pinnedAppData.icon ?? ''; this._command = pinnedAppData.id ?? ''; this._iconString = this._icon; this._app = this._menuLayout.appSys.lookup_app(this._command); // Allows dragging the pinned app into the overview workspace thumbnail. this.app = this._app; if (this._iconString === Constants.ShortcutCommands.ARCMENU_ICON || this._iconString === `${ArcMenuManager.extension.path}/icons/arcmenu-logo-symbolic.svg`) this._iconString = `${ArcMenuManager.extension.path}/${Constants.ArcMenuLogoSymbolic}`; if (this._app && this._iconString === '') { const appIcon = this._app.create_icon_texture(Constants.MEDIUM_ICON_SIZE); if (appIcon instanceof St.Icon) { this._iconString = appIcon.gicon ? appIcon.gicon.to_string() : appIcon.fallback_icon_name; if (!this._iconString) this._iconString = ''; } } if (this._app && !this._name) this._name = this._app.get_name(); this.label.text = _(this._name); this._updateIcon(); this.setShouldShow(); } createIcon() { let iconSize; if (this._displayType === Constants.DisplayType.GRID) { const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-grid-icon-size'); const defaultIconSize = this._menuLayout.icon_grid_size; ({iconSize} = Utils.getGridIconSize(iconSizeEnum, defaultIconSize)); } else if (this._displayType === Constants.DisplayType.LIST) { const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-icon-size'); const defaultIconSize = this.isContainedInCategory ? this._menuLayout.apps_icon_size : this._menuLayout.pinned_apps_icon_size; iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); } return new St.Icon({ gicon: Gio.icon_new_for_string(this._iconString), icon_size: iconSize, style_class: this._displayType === Constants.DisplayType.GRID ? '' : 'popup-menu-icon', }); } popupContextMenu() { if (this.contextMenu === undefined) { this.contextMenu = new AppContextMenu(this, this._menuLayout); if (this._displayType === Constants.DisplayType.GRID) this.contextMenu.centerBoxPointerPosition(); if (this._app) this.contextMenu.setApp(this._app); else this.contextMenu.addUnpinItem(this._command); } if (this.tooltip !== undefined) this.tooltip.hide(); this.contextMenu.open(BoxPointer.PopupAnimation.FULL); } getDragActor() { const icon = new St.Icon({ gicon: Gio.icon_new_for_string(this._iconString), style_class: 'popup-menu-icon', icon_size: this._iconBin.get_child().icon_size, }); return icon; } _onDragEnd() { super._onDragEnd(); const parent = this.get_parent(); if (!parent) return; const layoutManager = parent.layout_manager; const pinnedAppsArray = layoutManager.getChildren(); this.emit('pinned-apps-changed', pinnedAppsArray); } acceptDrop(source, _actor, x, y) { const acceptDrop = super.acceptDrop(source, _actor, x, y); if (!acceptDrop) return false; const folderId = GLib.uuid_string_random(); // Create the new folder let folderSettings; try { folderSettings = this._menuLayout.getSettings(`${ArcMenuManager.settings.schema_id}.pinned-apps-folders`, `${ArcMenuManager.settings.path}pinned-apps-folders/${folderId}/`); } catch (e) { console.log(`Error creating new pinned apps folder: ${e}`); return false; } const folderPinnedAppsList = [this.pinnedAppData, source.pinnedAppData]; folderSettings.set_value('pinned-apps', new GLib.Variant('aa{ss}', folderPinnedAppsList)); const parent = this.get_parent(); const layoutManager = parent.layout_manager; source.cancelActions(); const pinnedAppsList = ArcMenuManager.settings.get_value('pinned-apps').deepUnpack(); const apps = []; if (source._app) apps.push(source._app); if (this._app) apps.push(this._app); let folderName; if (apps.length) folderName = Utils.findBestFolderName(apps); if (!folderName) folderName = _('Unnamed Folder'); let index = layoutManager.getItemPosition(this); pinnedAppsList.splice(index, 1, {'id': folderId, 'name': folderName, 'isFolder': 'true'}); index = layoutManager.getItemPosition(source); pinnedAppsList.splice(index, 1); return ArcMenuManager.settings.set_value('pinned-apps', new GLib.Variant('aa{ss}', pinnedAppsList)); } activate(event) { if (this._app) this._app.open_new_window(-1); else if (this._command === Constants.ShortcutCommands.SHOW_APPS) Main.overview._overview._controls._toggleAppsPage(); else Util.spawnCommandLine(this._command); this._menuLayout.arcMenu.toggle(); super.activate(event); } _onDestroy() { if (this.folderSettings) this.folderSettings = null; this.pinnedAppData = null; super._onDestroy(); } } export class ApplicationMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, app, displayType, metaInfo, isContainedInCategory) { super(menuLayout); this._app = app; this._displayType = displayType; this.metaInfo = metaInfo || {}; this.isContainedInCategory = isContainedInCategory; this.searchType = this._menuLayout.search_display_type; this.hasContextMenu = !!this._app; this.isSearchResult = !!Object.keys(this.metaInfo).length; if (this._app) { const disableRecentAppsIndicator = ArcMenuManager.settings.get_boolean('disable-recently-installed-apps'); if (!disableRecentAppsIndicator) { const recentApps = ArcMenuManager.settings.get_strv('recently-installed-apps'); this.isRecentlyInstalled = recentApps.some(appIter => appIter === this._app.get_id()); } } this._iconBin = new St.Bin({ x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this._iconBin); this._updateIcon(); this.label = new St.Label({ text: this._app ? this._app.get_name() : this.metaInfo['name'], x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.CENTER, }); this.description = this._app ? this._app.get_description() : this.metaInfo['description']; const showSearchDescriptions = ArcMenuManager.settings.get_boolean('show-search-result-details') && this.isSearchResult; const showAppDescriptions = ArcMenuManager.settings.get_boolean('apps-show-extra-details') && !this.isSearchResult; const isCalculatorProvider = this.metaInfo['provider-id'] === 'org.gnome.Calculator.desktop'; if (this._displayType === Constants.DisplayType.LIST && this.description && (showSearchDescriptions || showAppDescriptions || isCalculatorProvider)) { const labelBox = new St.BoxLayout({ ...Utils.getOrientationProp(true), x_expand: true, x_align: Clutter.ActorAlign.FILL, }); const [descriptionText] = this.description.split('\n'); this.descriptionLabel = new St.Label({ text: descriptionText, y_expand: true, y_align: Clutter.ActorAlign.CENTER, style: 'font-weight: lighter;', }); labelBox.add_child(this.label); labelBox.add_child(this.descriptionLabel); this.add_child(labelBox); } else { this.add_child(this.label); } if (this.isRecentlyInstalled) { this._indicator = new St.Label({ text: _('New'), style_class: 'arcmenu-text-indicator', x_expand: true, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this._indicator); } if (this._displayType === Constants.DisplayType.GRID) Utils.convertToGridLayout(this); this.connect('notify::hover', () => this.removeIndicator()); this.connect('key-focus-in', () => this.removeIndicator()); } set folderPath(value) { this.hasContextMenu = value; this._folderPath = value; } get folderPath() { return this._folderPath; } createIcon() { let iconSize; if (this._displayType === Constants.DisplayType.GRID) { this._iconBin.x_align = Clutter.ActorAlign.CENTER; const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-grid-icon-size'); const defaultIconSize = this._menuLayout.icon_grid_size; ({iconSize} = Utils.getGridIconSize(iconSizeEnum, defaultIconSize)); } else if (this._displayType === Constants.DisplayType.LIST) { const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-icon-size'); const defaultIconSize = this.isContainedInCategory || this.isSearchResult ? this._menuLayout.apps_icon_size : this._menuLayout.pinned_apps_icon_size; iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); } const icon = this.isSearchResult ? this.metaInfo['createIcon'](iconSize) : this._app.create_icon_texture(iconSize); if (icon) { icon.style_class = this._displayType === Constants.DisplayType.GRID ? '' : 'popup-menu-icon'; return icon; } else { return false; } } removeIndicator() { if (this.isRecentlyInstalled) { this.isRecentlyInstalled = false; const recentApps = ArcMenuManager.settings.get_strv('recently-installed-apps'); const index = recentApps.indexOf(this._app.get_id()); if (index > -1) recentApps.splice(index, 1); ArcMenuManager.settings.set_strv('recently-installed-apps', recentApps); this._indicator.hide(); this._menuLayout.setNewAppIndicator(); } } popupContextMenu() { this.removeIndicator(); if (this.tooltip) this.tooltip.hide(); if (!this._app && !this.folderPath) return; if (this.contextMenu === undefined) { this.contextMenu = new AppContextMenu(this, this._menuLayout); if (this._app) this.contextMenu.setApp(this._app); else if (this.folderPath) this.contextMenu.setFolderPath(this.folderPath); if (this._displayType === Constants.DisplayType.GRID) this.contextMenu.centerBoxPointerPosition(); } this.contextMenu.open(BoxPointer.PopupAnimation.FULL); } activateSearchResult(provider, metaInfo, terms) { if (provider.activateResult) { provider.activateResult(metaInfo.id, terms); if (metaInfo.clipboardText) St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, metaInfo.clipboardText); } else if (metaInfo.id.endsWith('.desktop')) { const app = this._menuLayout.appSys.lookup_app(metaInfo.id); if (app.can_open_new_window()) app.open_new_window(-1); else app.activate(); } else { this._menuLayout.arcMenu.itemActivated(BoxPointer.PopupAnimation.NONE); const systemActions = SystemActions.getDefault(); // SystemActions.activateAction('open-screenshot-ui') waits for // Main.overview to be hidden before launching ScreenshotUI. // Avoid that by directly calling Screenshot.showScreenshotUI(). if (metaInfo.id === 'open-screenshot-ui') { showScreenshotUI(); return; } systemActions.activateAction(metaInfo.id); } } activate(event) { this.removeIndicator(); if (this.isSearchResult) { this.activateSearchResult(this.provider, this.metaInfo, this.resultsView.terms, event); } else { this._app.open_new_window(-1); super.activate(event); } this._menuLayout.arcMenu.toggle(); } } export class FolderDialog extends PopupMenu.PopupMenu { constructor(sourceActor, menuLayout) { const dummyCursor = new St.Widget({width: 0, height: 0, opacity: 0}); super(dummyCursor, 0.5, St.Side.TOP); this.dummyCursor = dummyCursor; Main.uiGroup.add_child(this.dummyCursor); this._sourceActor = sourceActor; this._menuLayout = menuLayout; this._menuButton = this._menuLayout.menuButton; this._arcMenu = this._menuLayout.arcMenu; this.actor.add_style_class_name('popup-menu arcmenu-menu'); this.box.add_style_class_name('arcmenu-folder-dialog'); this._openStateId = this.connect('open-state-changed', this._subMenuOpenStateChanged.bind(this)); this._menuLayout.subMenuManager.addMenu(this); Main.uiGroup.add_child(this.actor); this.actor.hide(); this.connectObject('notify::mapped', () => { if (!this.mapped) this.close(); }, this); const hasColumnSpacing = this._menuLayout.columnSpacing !== 0; const hasRowSpacing = this._menuLayout.rowSpacing !== 0; this._grid = new IconGrid({ columns: 3, halign: Clutter.ActorAlign.CENTER, column_spacing: hasColumnSpacing ? this._menuLayout.columnSpacing : 4, row_spacing: hasRowSpacing ? this._menuLayout.rowSpacing : 4, }); this._scrollView = this._menuLayout._createScrollBox({ x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.START, style_class: this._menuLayout._disableFadeEffect ? '' : 'small-vfade', }); this._box = new St.BoxLayout({ style: 'padding: 0px 18px;', y_align: Clutter.ActorAlign.START, }); this._box.add_child(this._grid); Utils.addChildToParent(this._scrollView, this._box); this.box.add_child(this._scrollView); this.box.set({ pivot_point: new Graphene.Point({x: 0.5, y: 0.5}), x_expand: true, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.START, style: 'border-radius: 20px; padding: 0px;', }); } destroy() { if (this._openStateId) this.disconnect(this._openStateId); this._arcMenu._dimEffect.enabled = false; this.close(); this._destroyed = true; if (this.appList) { this.appList.forEach(item => { if (item instanceof BaseMenuItem) item.destroy(); }); this.appList = null; } this.dummyCursor.destroy(); this.dummyCursor = null; this._menuLayout = null; this._menuButton = null; this._arcMenu = null; this._sourceActor = null; super.destroy(); } _subMenuOpenStateChanged(menu, isOpen) { const [sourceX, sourceY] = this._arcMenu.actor.get_transformed_position(); const positionX = sourceX + (this._arcMenu.actor.width / 2); const positionY = sourceY + (this._arcMenu.actor.height / 2) - ((this.actor.height / 2)); this.dummyCursor.set_position(Math.round(positionX), Math.round(positionY)); this._setDimmed(isOpen); if (isOpen) { this.box.set({ scale_x: .3, scale_y: .3, opacity: 0, }); this.box.ease({ scale_x: 1, scale_y: 1, opacity: 255, duration: 150, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); this._sourceActor.add_style_pseudo_class('active'); if (this._menuButton.tooltipShowingID) { GLib.source_remove(this._menuButton.tooltipShowingID); this._menuButton.tooltipShowingID = null; this._menuButton.tooltipShowing = false; } if (this.tooltip) { this.tooltip.hide(); this._menuButton.tooltipShowing = false; } } else { this.box.ease({ scale_x: .3, scale_y: .3, opacity: 0, duration: 150, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); this._sourceActor.remove_style_pseudo_class('active'); this._sourceActor.active = false; this._sourceActor.sync_hover(); } } _setDimmed(dim) { if (this._destroyed) return; const DIM_BRIGHTNESS = -0.4; const POPUP_ANIMATION_TIME = 400; const val = 127 * (1 + (dim ? 1 : 0) * DIM_BRIGHTNESS); const colorValues = { red: val, green: val, blue: val, alpha: 255, }; const color = Clutter.Color ? new Clutter.Color(colorValues) : new Cogl.Color(colorValues); this._arcMenu._boxPointer.ease_property('@effects.dim.brightness', color, { mode: Clutter.AnimationMode.LINEAR, duration: POPUP_ANIMATION_TIME, onStopped: () => (this._arcMenu._dimEffect.enabled = dim), }); this._arcMenu._dimEffect.enabled = true; } populateMenu(appList) { this.appList = appList; this._grid.removeAllItems(); for (let i = 0; i < appList.length; i++) { const item = appList[i]; this._grid.appendItem(item); } if (appList.length) this._setSizeForChildSize(appList[0]); } _setSizeForChildSize(child) { const childHeight = child.get_height(); const childWidth = child.get_width(); const columnSpacing = this._grid.layoutManager.column_spacing; const rowSpacing = this._grid.layoutManager.row_spacing; const padding = 36; // Calculate a size to accommodate a 3x3 grid const width = (childWidth * 3) + (columnSpacing * 2) + padding; const height = (childHeight * 3) + (rowSpacing * 2); this._scrollView.style = `width: ${width}px; height: ${height}px; padding-bottom: 18px;`; } } export class SubCategoryMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, parentDirectory, category, displayType) { super(menuLayout); this._category = category; this._parentDirectory = parentDirectory; this._displayType = displayType; this.appList = []; this._name = ''; const categoryIconType = ArcMenuManager.settings.get_enum('category-icon-type'); if (categoryIconType === Constants.CategoryIconType.FULL_COLOR) this.add_style_class_name('regular-icons'); else this.add_style_class_name('symbolic-icons'); this._iconBin = new St.Bin(); this.add_child(this._iconBin); this.label = new St.Label({ text: this._name, x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this.label); if (this._displayType === Constants.DisplayType.GRID) Utils.convertToGridLayout(this); this.description = parentDirectory.get_name(); this._subMenuPopup = new FolderDialog(this, this._menuLayout); this._headerLabel = new St.Label({ style: 'padding-top: 10px; padding-bottom: 10px; text-align: center;', style_class: 'folder-header', text: this._name, x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, }); this._subMenuPopup.box.insert_child_at_index(this._headerLabel, 0); } createIcon() { let iconSize; if (this._displayType === Constants.DisplayType.GRID) { this._iconBin.x_align = Clutter.ActorAlign.CENTER; const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-grid-icon-size'); const defaultIconSize = this._menuLayout.icon_grid_size; ({iconSize} = Utils.getGridIconSize(iconSizeEnum, defaultIconSize)); } else { const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-icon-size'); const defaultIconSize = this._menuLayout.apps_icon_size; iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); } const [name, gicon, fallbackIcon] = Utils.getCategoryDetails(this._menuLayout.iconTheme, this._category); this._name = `${this._parentDirectory.get_name()} - ${name}`; this.label.text = `${name}`; this._headerLabel.text = `${this._parentDirectory.get_name()}\n${name}`; if (!gicon) { if (!this.appList.length) { const icon = new St.Icon({ style_class: 'popup-menu-icon', icon_size: iconSize, icon_name: 'folder-directory-symbolic', }); return icon; } const layout = new Clutter.GridLayout({ row_homogeneous: true, column_homogeneous: true, }); const icon = new St.Widget({ layout_manager: layout, style: `width: ${iconSize}px; height: ${iconSize}px;`, }); const subSize = Math.floor(.4 * iconSize); const numItems = this.appList.length; const rtl = icon.get_text_direction() === Clutter.TextDirection.RTL; for (let i = 0; i < 4; i++) { const style = `width: ${subSize}px; height: ${subSize}px;`; const bin = new St.Bin({style}); if (i < numItems) bin.child = this.appList[i]._app.create_icon_texture(subSize); layout.attach(bin, rtl ? (i + 1) % 2 : i % 2, Math.floor(i / 2), 1, 1); } return icon; } const icon = new St.Icon({ style_class: this._displayType === Constants.DisplayType.GRID ? '' : 'popup-menu-icon', icon_size: iconSize, gicon, fallback_gicon: fallbackIcon, }); return icon; } isExtraCategory() { for (const entry of Constants.Categories) { if (entry.CATEGORY === this._category) return true; } return false; } setNewAppIndicator() { } populateMenu() { this.appList.sort((a, b) => { const nameA = a._app.get_name(); const nameB = b._app.get_name(); return nameA.localeCompare(nameB); }); this._subMenuPopup.populateMenu(this.appList); } activate(event) { super.activate(event); this._subMenuPopup.toggle(); } _onDestroy() { this._headerLabel = null; this._subMenuPopup.destroy(); this._subMenuPopup = null; this.appList = null; super._onDestroy(); } } export class CategoryMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, category, displayType) { super(menuLayout); this._category = category; this._displayType = displayType; this.appList = []; this._name = ''; const categoryIconType = ArcMenuManager.settings.get_enum('category-icon-type'); if (categoryIconType === Constants.CategoryIconType.FULL_COLOR) this.add_style_class_name('regular-icons'); else this.add_style_class_name('symbolic-icons'); this._iconBin = new St.Bin(); this.add_child(this._iconBin); this.label = new St.Label({ text: this._name, y_expand: true, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this.label); this._updateIcon(); this._indicator = new St.Icon({ icon_name: 'starred-symbolic', style_class: 'arcmenu-indicator', icon_size: INDICATOR_ICON_SIZE, x_expand: true, y_expand: false, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER, }); if (this.isRecentlyInstalled) this.setNewAppIndicator(true); if (this._displayType === Constants.DisplayType.BUTTON) Utils.convertToButton(this); this.connect('motion-event', this._onMotionEvent.bind(this)); this.connect('enter-event', this._onEnterEvent.bind(this)); this.connect('leave-event', this._onLeaveEvent.bind(this)); } createIcon() { let iconSize; if (this._displayType === Constants.DisplayType.BUTTON) { const iconSizeEnum = ArcMenuManager.settings.get_enum('button-item-icon-size'); const defaultIconSize = this._menuLayout.buttons_icon_size; iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); this.style = `min-width: ${iconSize}px; min-height: ${iconSize}px;`; } else { const iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-category-icon-size'); const defaultIconSize = this._menuLayout.category_icon_size; iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); if (iconSize === Constants.ICON_HIDDEN) { this._iconBin.hide(); this.style = 'padding-top: 8px; padding-bottom: 8px;'; } } const [name, gicon, fallbackIcon] = Utils.getCategoryDetails(this._menuLayout.iconTheme, this._category); this._name = _(name); this.label.text = _(name); const icon = new St.Icon({ style_class: this._displayType === Constants.DisplayType.BUTTON ? '' : 'popup-menu-icon', icon_size: iconSize, gicon, fallback_gicon: fallbackIcon, }); return icon; } isExtraCategory() { for (const entry of Constants.Categories) { if (entry.CATEGORY === this._category) return true; } return false; } setNewAppIndicator(shouldShow) { if (this._displayType === Constants.DisplayType.BUTTON) return; this.isRecentlyInstalled = shouldShow; if (shouldShow && !this.contains(this._indicator)) this.add_child(this._indicator); else if (!shouldShow && this.contains(this._indicator)) this.remove_child(this._indicator); } displayAppList() { this._menuLayout.searchEntry?.clearWithoutSearchChangeEvent(); this._menuLayout._setCategoriesBoxInactive(false); this._menuLayout.activeCategoryName = this._name; switch (this._category) { case Constants.CategoryType.HOME_SCREEN: this._menuLayout.activeCategoryName = _('Pinned'); this._menuLayout.displayPinnedApps(); break; case Constants.CategoryType.PINNED_APPS: this._menuLayout.displayPinnedApps(); break; case Constants.CategoryType.RECENT_FILES: this._menuLayout.displayRecentFiles(); break; default: if (this._category === Constants.CategoryType.FREQUENT_APPS) this._menuLayout.populateFrequentAppsList(this); this._menuLayout.displayCategoryAppList(this.appList, this._category); break; } this._menuLayout.activeCategoryType = this._category; } activate(event) { super.activate(event); if (this._menuLayout.supports_category_hover_activation) this._menuLayout.setActiveCategory(this); this.displayAppList(); } _clearLeaveEventTimeout() { if (this._menuLayout.leaveEventTimeoutId) { GLib.source_remove(this._menuLayout.leaveEventTimeoutId); this._menuLayout.leaveEventTimeoutId = null; } } _shouldActivateOnHover() { const activateOnHover = ArcMenuManager.settings.get_boolean('activate-on-hover'); const supportsActivateOnHover = this._menuLayout.supports_category_hover_activation; const activeSearchResults = this._menuLayout.blockCategoryHoverActivation; return activateOnHover && supportsActivateOnHover && !activeSearchResults; } _onEnterEvent() { if (!this._shouldActivateOnHover()) return; this._clearLeaveEventTimeout(); } _onLeaveEvent() { if (!this._shouldActivateOnHover()) return; if (!this._menuLayout.leaveEventTimeoutId) { this._menuLayout.leaveEventTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => { this._menuLayout.initialMotionEventItem = null; if (this._menuLayout.activeCategoryType === Constants.CategoryType.SEARCH_RESULTS) this._menuLayout.activeCategoryType = -1; this._menuLayout.leaveEventTimeoutId = null; return GLib.SOURCE_REMOVE; }); } } _onMotionEvent(actor, event) { if (!this._shouldActivateOnHover()) return; if (!this._menuLayout.initialMotionEventItem) this._menuLayout.initialMotionEventItem = this; const inActivationZone = this._inActivationZone(event.get_coords()); if (inActivationZone) { this.activate(Clutter.get_current_event()); this._menuLayout.initialMotionEventItem = this; } } _inActivationZone([x, y]) { // no need to activate the category if its already active if (this._menuLayout.activeCategoryType === this._category) { this._menuLayout._oldX = x; this._menuLayout._oldY = y; return false; } if (!this._menuLayout.initialMotionEventItem) return false; const [posX, posY] = this._menuLayout.initialMotionEventItem.get_transformed_position(); // the mouse is on the initialMotionEventItem const onInitialMotionEventItem = this._menuLayout.initialMotionEventItem === this; if (onInitialMotionEventItem) { this._menuLayout._oldX = x; this._menuLayout._oldY = y; if (this._menuLayout.activeCategoryType !== Constants.CategoryType.SEARCH_RESULTS) return true; } const {width} = this._menuLayout.initialMotionEventItem; const {height} = this._menuLayout.initialMotionEventItem; const horizontalFlip = ArcMenuManager.settings.get_boolean('enable-horizontal-flip'); const maxX = horizontalFlip ? posX : posX + width; const maxY = posY + height; const distance = Math.abs(maxX - this._menuLayout._oldX); const point1 = [this._menuLayout._oldX, this._menuLayout._oldY]; const point2 = [maxX, posY - distance]; const point3 = [maxX, maxY + distance]; const area = Utils.areaOfTriangle(point1, point2, point3); const a1 = Utils.areaOfTriangle([x, y], point2, point3); const a2 = Utils.areaOfTriangle(point1, [x, y], point3); const a3 = Utils.areaOfTriangle(point1, point2, [x, y]); const outsideTriangle = area !== a1 + a2 + a3; return outsideTriangle; } _onDestroy() { this.appList = null; this._clearLeaveEventTimeout(); this._indicator.destroy(); this._indicator = null; super._onDestroy(); } } // Directory shorctuts. Home, Documents, Downloads, etc export class PlaceMenuItem extends BaseMenuItem { static { GObject.registerClass(this); } constructor(menuLayout, info, displayType, isContainedInCategory) { super(menuLayout); this._displayType = displayType; this._info = info; this.isContainedInCategory = isContainedInCategory; this.hasContextMenu = false; this._iconBin = new St.Bin(); this.add_child(this._iconBin); this._updateIcon(); this.label = new St.Label({ text: _(info.name), x_expand: true, y_expand: false, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.CENTER, }); this.add_child(this.label); if (this._displayType === Constants.DisplayType.BUTTON) Utils.convertToButton(this); if (info.isRemovable()) { this.hasContextMenu = true; this._additionalAction = info.eject.bind(info); if (info.canUnmount()) this._additionalActionName = _('Unmount Drive'); else this._additionalActionName = _('Eject Drive'); } if (info.isRemovable()) { this._ejectIcon = new St.Icon({ icon_name: 'media-eject-symbolic', style_class: 'popup-menu-icon', }); this._ejectButton = new St.Button({ child: this._ejectIcon, style_class: 'button arcmenu-small-button', }); this._ejectButton.connect('clicked', info.eject.bind(info)); this.add_child(this._ejectButton); } this._infoChangedId = this._info.connect('changed', this._propertiesChanged.bind(this), this); } set folderPath(value) { this.hasContextMenu = value; this._folderPath = value; } get folderPath() { return this._folderPath; } forceTitle(title) { this._foreTitle = true; if (this.label) this.label.text = _(title); } setAsRecentFile(recentFile, removeRecentFile) { const parentPath = recentFile.get_parent()?.get_path(); this.folderPath = parentPath; this.description = parentPath; this.fileUri = recentFile.get_uri(); this._additionalAction = () => { removeRecentFile(); this.destroy(); }; this._additionalActionName = _('Remove from Recent'); } _onDestroy() { if (this._infoChangedId) { this._info.disconnect(this._infoChangedId); this._infoChangedId = null; } if (this._info) this._info.destroy(); this._info = null; super._onDestroy(); } popupContextMenu() { if (this.tooltip) this.tooltip.hide(); if (this.contextMenu === undefined) { this.contextMenu = new AppContextMenu(this, this._menuLayout); if (this.folderPath) this.contextMenu.setFolderPath(this.folderPath); if (this._additionalAction) this.contextMenu.addAdditionalAction(_(this._additionalActionName), this._additionalAction); if (this._displayType === Constants.DisplayType.GRID) this.contextMenu.centerBoxPointerPosition(); } this.contextMenu.toggle(); } createIcon() { let iconSizeEnum; if (this.isContainedInCategory) iconSizeEnum = ArcMenuManager.settings.get_enum('menu-item-icon-size'); else iconSizeEnum = ArcMenuManager.settings.get_enum('quicklinks-item-icon-size'); const defaultIconSize = this.isContainedInCategory ? this._menuLayout.apps_icon_size : this._menuLayout.quicklinks_icon_size; let iconSize = Utils.getIconSize(iconSizeEnum, defaultIconSize); if (this._displayType === Constants.DisplayType.BUTTON) { const defaultButtonIconSize = this._menuLayout.buttons_icon_size; iconSizeEnum = ArcMenuManager.settings.get_enum('button-item-icon-size'); iconSize = Utils.getIconSize(iconSizeEnum, defaultButtonIconSize); this.style = `min-width: ${iconSize}px; min-height: ${iconSize}px;`; } return new St.Icon({ gicon: this._info.icon, icon_size: iconSize, style_class: this._displayType === Constants.DisplayType.BUTTON ? '' : 'popup-menu-icon', }); } activate(event) { this._info.launch(event.get_time()); this._menuLayout.arcMenu.toggle(); super.activate(event); } _propertiesChanged(info) { this._info = info; this._iconBin.set_child(this.createIcon()); if (this.label && !this._foreTitle) this.label.text = _(info.name); } } export class SearchEntry extends St.Entry { static [GObject.signals] = { 'search-changed': {param_types: [GObject.TYPE_STRING]}, 'entry-key-focus-in': { }, 'entry-key-press': {param_types: [Clutter.Event.$gtype]}, }; static { GObject.registerClass(this); } constructor(menuLayout) { super({ hint_text: _('Search…'), track_hover: true, can_focus: true, x_expand: true, x_align: Clutter.ActorAlign.FILL, y_align: Clutter.ActorAlign.START, name: 'ArcMenuSearchEntry', style_class: 'arcmenu-search-entry', }); this.searchResults = menuLayout.searchResults; this._menuLayout = menuLayout; this.triggerSearchChangeEvent = true; this._iconClickedId = 0; const iconSizeEnum = ArcMenuManager.settings.get_enum('misc-item-icon-size'); const iconSize = Utils.getIconSize(iconSizeEnum, Constants.EXTRA_SMALL_ICON_SIZE); this._findIcon = new St.Icon({ style_class: 'search-entry-icon', icon_name: 'edit-find-symbolic', icon_size: iconSize, }); this._clearIcon = new St.Icon({ style_class: 'search-entry-icon', icon_name: 'edit-clear-symbolic', icon_size: iconSize, }); this.set_primary_icon(this._findIcon); this._text = this.get_clutter_text(); this._text.connectObject('text-changed', this._onTextChanged.bind(this), this); this._text.connectObject('key-press-event', this._onKeyPress.bind(this), this); this._text.connectObject('key-focus-in', this._onKeyFocusIn.bind(this), this); this._text.connectObject('key-focus-out', this._onKeyFocusOut.bind(this), this); this.connect('destroy', this._onDestroy.bind(this)); } getText() { return this.get_text(); } setText(text) { this.set_text(text); } clearWithoutSearchChangeEvent() { this.triggerSearchChangeEvent = false; this.set_text(''); this.triggerSearchChangeEvent = true; } hasKeyFocus() { const keyFocus = global.stage.get_key_focus(); return keyFocus ? this.contains(keyFocus) : false; } clear() { this.set_text(''); } isEmpty() { return this.get_text().length === 0; } _onKeyFocusOut() { if (!this.isEmpty()) { this.add_style_pseudo_class('focus'); return Clutter.EVENT_STOP; } return Clutter.EVENT_PROPAGATE; } _onTextChanged() { if (!this.isEmpty()) { this.set_secondary_icon(this._clearIcon); if (this._iconClickedId === 0) { this._iconClickedId = this.connect('secondary-icon-clicked', () => this._menuLayout.setDefaultMenuView()); } if (!this.hasKeyFocus()) this.grab_key_focus(); if (!this.searchResults.getTopResult()?.has_style_pseudo_class('active')) this.searchResults.getTopResult()?.add_style_pseudo_class('active'); this.add_style_pseudo_class('focus'); } else { if (this._iconClickedId > 0) { this.disconnect(this._iconClickedId); this._iconClickedId = 0; } if (!this.hasKeyFocus()) this.remove_style_pseudo_class('focus'); this.set_secondary_icon(null); } if (this.triggerSearchChangeEvent) this.emit('search-changed', this.get_text()); } _onKeyPress(actor, event) { const symbol = event.get_key_symbol(); const searchResult = this.searchResults.getTopResult(); if (!this.isEmpty() && searchResult) { if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_KP_Enter) { searchResult.activate(event); return Clutter.EVENT_STOP; } else if (symbol === Clutter.KEY_Menu && searchResult.hasContextMenu) { searchResult.popupContextMenu(); return Clutter.EVENT_STOP; } } this.emit('entry-key-press', event); return Clutter.EVENT_PROPAGATE; } _onKeyFocusIn() { this.add_style_pseudo_class('focus'); this.emit('entry-key-focus-in'); return Clutter.EVENT_PROPAGATE; } _onDestroy() { if (this._iconClickedId) { this.disconnect(this._iconClickedId); this._iconClickedId = null; } this._findIcon.destroy(); this._findIcon = null; this._clearIcon.destroy(); this._clearIcon = null; this.searchResults = null; this._menuLayout = null; } } export const WorldClocksWidget = GObject.registerClass( class ArcMenuWorldClocksWidget extends GWorldClocksWidget { _init(menuLayout) { super._init(); this._menuLayout = menuLayout; this.connect('destroy', () => this._onDestroy()); this._syncID = GObject.signal_handler_find(this._appSystem, {signalId: 'installed-changed'}); this._clockChangedID = GObject.signal_handler_find(this._settings, {signalId: 'changed'}); } _onDestroy() { this._menuLayout = null; if (this._syncID) { this._appSystem.disconnect(this._syncID); this._syncID = null; } if (this._clockChangedID) { this._settings.disconnect(this._clockChangedID); this._clockChangedID = null; } if (this._clocksProxyID) { this._clocksProxy.disconnect(this._clocksProxyID); this._clocksProxyID = null; } if (this._clockNotifyId) { this._clock.disconnect(this._clockNotifyId); this._clockNotifyId = null; } if (this._tzNotifyId) { this._clock.disconnect(this._tzNotifyId); this._tzNotifyId = null; } } vfunc_clicked() { this._menuLayout.arcMenu.toggle(); if (this._clocksApp) this._clocksApp.activate(); } _onProxyReady(proxy, error) { if (error) { console.log(`Failed to create GNOME Clocks proxy: ${error}`); return; } this._clocksProxyID = this._clocksProxy.connect('g-properties-changed', this._onClocksPropertiesChanged.bind(this)); this._onClocksPropertiesChanged(); } }); export const WeatherWidget = GObject.registerClass( class ArcMenuWeatherWidget extends GWeatherWidget { _init(menuLayout) { super._init(); this._menuLayout = menuLayout; this.connect('destroy', () => this._onDestroy()); } _onDestroy() { this._weatherClient.disconnectAll(); this._weatherClient = null; delete this._weatherClient; this._menuLayout = null; } vfunc_clicked() { this._menuLayout.arcMenu.toggle(); this._weatherClient.activateApp(); } });