import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import Shell from 'gi://Shell'; import {InputSourceManager} from 'resource:///org/gnome/shell/ui/status/keyboard.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import {ArcMenuManager} from './arcmenuManager.js'; import * as Constants from './constants.js'; import * as Keybinder from './keybinder.js'; import {MenuButton} from './menuButton.js'; import * as Theming from './theming.js'; import {StandaloneRunner} from './standaloneRunner.js'; import * as Utils from './utils.js'; export const MenuController = class { constructor(panelInfo, monitorIndex) { this.panelInfo = panelInfo; this.panel = panelInfo.panel; this.monitorIndex = monitorIndex; this.isPrimaryPanel = panelInfo.isPrimaryPanel; // Allow other extensions and DBus command to open/close ArcMenu if (!global.toggleArcMenu && this.isPrimaryPanel) { global.toggleArcMenu = () => this.toggleMenus(); this._service = new Utils.DBusService(); this._service.ToggleArcMenu = () => { this.toggleMenus(); }; } this._menuButton = new MenuButton(panelInfo, this.monitorIndex); if (this.isPrimaryPanel) { this._overrideOverlayKey = new Keybinder.OverrideOverlayKey(); this._customKeybinding = new Keybinder.CustomKeybinding(); this._appSystem = Shell.AppSystem.get_default(); this._updateHotKeyBinder(); this._initRecentAppsTracker(); this._inputSourceManagerOverride(); } this._setButtonAppearance(); this._setButtonText(); this._setButtonIcon(); this._setButtonIconSize(); this._setButtonIconPadding(); this._configureActivitiesButton(); } _inputSourceManagerOverride() { // Add ArcMenu as a valid option for "per window" input source switching. this._inputSourcesSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.input-sources'}); this._perWindowChangedId = this._inputSourcesSettings.connect('changed::per-window', () => { if (this._inputSourcesSettings.get_boolean('per-window')) return; const menus = this._getAllMenus(); menus.forEach(menu => { delete menu._inputSources; delete menu._currentSource; }); }); this._inputSourceManagerProto = InputSourceManager.prototype; this._origGetCurrentWindow = this._inputSourceManagerProto._getCurrentWindow; this._inputSourceManagerProto._getCurrentWindow = () => { const openedArcMenu = this._getOpenedArcMenu(); if (openedArcMenu) return openedArcMenu; else if (Main.overview.visible) return Main.overview; else return global.display.focus_window; }; } _getOpenedArcMenu() { const menus = this._getAllMenus(); for (let i = 0; i < menus.length; i++) { if (menus[i].isOpen) return menus[i]; } return null; } _getAllMenus() { const menus = []; for (let i = 0; i < ArcMenuManager.menuControllers.length; i++) { const menuButton = ArcMenuManager.menuControllers[i]._menuButton; menus.push(menuButton.arcMenu); } if (this.runnerMenu) menus.push(this.runnerMenu.arcMenu); return menus; } _connectSettings(settings, callback) { ArcMenuManager.settings.connectObject( ...settings.flatMap(setting => [`changed::${setting}`, callback]), this ); } connectSettingsEvents() { this._connectSettings( ['override-menu-theme', 'menu-background-color', 'menu-foreground-color', 'search-entry-border-radius', 'menu-border-color', 'menu-border-width', 'menu-border-radius', 'menu-font-size', 'menu-separator-color', 'menu-item-hover-bg-color', 'menu-item-hover-fg-color', 'menu-item-active-bg-color', 'menu-button-border-color', 'menu-item-active-fg-color', 'menu-button-fg-color', 'menu-button-bg-color', 'menu-arrow-rise', 'menu-button-hover-bg-color', 'menu-button-hover-fg-color', 'menu-button-active-bg-color', 'menu-button-active-fg-color', 'menu-button-border-radius', 'menu-button-border-width'], this._overrideMenuTheme.bind(this)); this._connectSettings(['arcmenu-hotkey', 'runner-hotkey'], this._updateHotKeyBinder.bind(this)); this._connectSettings(['position-in-panel', 'menu-button-position-offset'], this._setButtonPosition.bind(this)); this._connectSettings(['menu-button-icon', 'distro-icon', 'arc-menu-icon', 'custom-menu-button-icon'], this._setButtonIcon.bind(this)); this._connectSettings( ['directory-shortcuts', 'application-shortcuts', 'extra-categories', 'custom-grid-icon-size', 'power-options', 'show-external-devices', 'show-bookmarks', 'disable-user-avatar', 'runner-search-display-style', 'avatar-style', 'enable-activities-shortcut', 'enable-horizontal-flip', 'power-display-style', 'searchbar-default-bottom-location', 'searchbar-default-top-location', 'multi-lined-labels', 'apps-show-extra-details', 'show-search-result-details', 'search-provider-open-windows', 'search-provider-recent-files', 'misc-item-icon-size', 'windows-disable-pinned-apps', 'disable-scrollview-fade-effect', 'windows-disable-frequent-apps', 'default-menu-view', 'default-menu-view-tognee', 'group-apps-alphabetically-list-layouts', 'group-apps-alphabetically-grid-layouts', 'menu-item-grid-icon-size', 'menu-item-icon-size', 'button-item-icon-size', 'quicklinks-item-icon-size', 'menu-item-category-icon-size', 'category-icon-type', 'shortcut-icon-type', 'show-category-sub-menus', 'arcmenu-extra-categories-links', 'arcmenu-extra-categories-links-location', 'raven-search-display-style', 'runner-show-frequent-apps', 'default-menu-view-redmond', 'disable-recently-installed-apps'], this._recreateMenuLayout.bind(this)); this._connectSettings(['left-panel-width', 'right-panel-width', 'menu-width-adjustment'], this._updateMenuWidth.bind(this)); this._connectSettings(['pinned-apps', 'enable-weather-widget-unity', 'enable-clock-widget-unity', 'enable-weather-widget-raven', 'enable-clock-widget-raven'], this._updatePinnedApps.bind(this)); this._connectSettings(['menu-position-alignment'], this._setMenuPositionAlignment.bind(this)); this._connectSettings(['menu-button-appearance'], this._setButtonAppearance.bind(this)); this._connectSettings(['custom-menu-button-text'], this._setButtonText.bind(this)); this._connectSettings(['custom-menu-button-icon-size'], this._setButtonIconSize.bind(this)); this._connectSettings(['button-padding'], this._setButtonIconPadding.bind(this)); this._connectSettings(['menu-height'], this._updateMenuHeight.bind(this)); this._connectSettings(['enable-unity-homescreen'], this._setDefaultMenuView.bind(this)); this._connectSettings(['menu-layout'], this._changeMenuLayout.bind(this)); this._connectSettings(['runner-position'], this._updateLocation.bind(this)); this._connectSettings(['show-activities-button'], this._configureActivitiesButton.bind(this)); this._connectSettings(['force-menu-location'], this._forceMenuLocation.bind(this)); } _overrideMenuTheme() { if (!this.isPrimaryPanel) return; if (this._writeTimeoutId) GLib.source_remove(this._writeTimeoutId); this._writeTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 300, () => { Theming.updateStylesheet(); this._writeTimeoutId = null; return GLib.SOURCE_REMOVE; }); } _recreateMenuLayout() { this._menuButton.createMenuLayout(); if (this.runnerMenu) this.runnerMenu.createMenuLayout(); } _forceMenuLocation() { this._menuButton.forceMenuLocation(); } _initRecentAppsTracker() { this._appList = this._listAllApps(); this._appSystem.connectObject('installed-changed', () => this._setRecentlyInstalledApps(), this); } _setRecentlyInstalledApps() { const isDisabled = ArcMenuManager.settings.get_boolean('disable-recently-installed-apps'); if (isDisabled) return; const appList = this._listAllApps(); // Filter to find if a new application has been installed const newAppsList = appList.filter(app => !this._appList.includes(app)); this._appList = appList; if (newAppsList.length) { // A new app has been installed, Save it in settings const recentApps = ArcMenuManager.settings.get_strv('recently-installed-apps'); const newRecentApps = [...new Set(recentApps.concat(newAppsList))]; ArcMenuManager.settings.set_strv('recently-installed-apps', newRecentApps); } } _listAllApps() { const appList = this._appSystem.get_installed().filter(appInfo => { try { appInfo.get_id(); // catch invalid file encodings } catch (e) { return false; } return appInfo.should_show(); }); return appList.map(app => app.get_id()); } _updateLocation() { this._menuButton.updateLocation(); if (this.runnerMenu) this.runnerMenu.updateLocation(); } _changeMenuLayout() { this._menuButton.createMenuLayout(); } _setDefaultMenuView() { this._menuButton.setDefaultMenuView(); } toggleStandaloneRunner() { this._closeAllArcMenus(); if (this.runnerMenu) this.runnerMenu.toggleMenu(); } toggleMenus() { if (this.runnerMenu && this.runnerMenu.arcMenu.isOpen) this.runnerMenu.toggleMenu(); if (global.dashToPanel || global.azTaskbar) { const MultipleArcMenus = ArcMenuManager.menuControllers.length > 1; const ShowArcMenuOnPrimaryMonitor = ArcMenuManager.settings.get_boolean('hotkey-open-primary-monitor'); if (MultipleArcMenus && ShowArcMenuOnPrimaryMonitor) this._toggleMenuOnMonitor(Main.layoutManager.primaryMonitor); else if (MultipleArcMenus && !ShowArcMenuOnPrimaryMonitor) this._toggleMenuOnMonitor(Main.layoutManager.currentMonitor); else this._menuButton.toggleMenu(); } else { this._menuButton.toggleMenu(); } } _toggleMenuOnMonitor(monitor) { let currentMonitorIndex = 0; for (let i = 0; i < ArcMenuManager.menuControllers.length; i++) { const menuButton = ArcMenuManager.menuControllers[i]._menuButton; const {monitorIndex} = ArcMenuManager.menuControllers[i]; if (monitor.index === monitorIndex) { currentMonitorIndex = i; } else { if (menuButton.arcMenu.isOpen) menuButton.toggleMenu(); menuButton.closeContextMenu(); } } // open the current monitors menu ArcMenuManager.menuControllers[currentMonitorIndex]._menuButton.toggleMenu(); } _closeAllArcMenus() { for (let i = 0; i < ArcMenuManager.menuControllers.length; i++) { const menuButton = ArcMenuManager.menuControllers[i]._menuButton; if (menuButton.arcMenu.isOpen) menuButton.toggleMenu(); menuButton.closeContextMenu(); } } _updateMenuHeight() { this._menuButton.updateHeight(); } _updateMenuWidth() { this._menuButton.updateWidth(); } _updatePinnedApps() { this._menuButton.loadPinnedApps(); } _updateHotKeyBinder() { if (!this.isPrimaryPanel) return; const [runnerHotkey] = ArcMenuManager.settings.get_strv('runner-hotkey'); const [menuHotkey] = ArcMenuManager.settings.get_strv('arcmenu-hotkey'); this._customKeybinding.unbind('ToggleArcMenu'); this._customKeybinding.unbind('ToggleRunnerMenu'); this._overrideOverlayKey.disable(); if (runnerHotkey) { if (!this.runnerMenu) this.runnerMenu = new StandaloneRunner(); if (runnerHotkey === Constants.SUPER_L) { this._overrideOverlayKey.enable(() => this.toggleStandaloneRunner()); } else { this._customKeybinding.bind('ToggleRunnerMenu', 'runner-hotkey', () => this.toggleStandaloneRunner()); } } else if (this.runnerMenu) { this.runnerMenu.destroy(); this.runnerMenu = null; } if (menuHotkey === Constants.SUPER_L) { this._overrideOverlayKey.disable(); this._overrideOverlayKey.enable(() => this.toggleMenus()); } else if (menuHotkey) { this._customKeybinding.bind('ToggleArcMenu', 'arcmenu-hotkey', () => this.toggleMenus()); } } _setButtonPosition() { if (this._isButtonEnabled()) { this._removeMenuButtonFromPanel(); this._addMenuButtonToMainPanel(); this._setMenuPositionAlignment(); } } _setMenuPositionAlignment() { this._menuButton.setMenuPositionAlignment(); } _setButtonAppearance() { const menuButtonAppearance = ArcMenuManager.settings.get_enum('menu-button-appearance'); const {menuButtonWidget} = this._menuButton; this._menuButton.container.set_width(-1); this._menuButton.container.set_height(-1); menuButtonWidget.show(); switch (menuButtonAppearance) { case Constants.MenuButtonAppearance.TEXT: menuButtonWidget.showText(); menuButtonWidget.setLabelStyle(null); break; case Constants.MenuButtonAppearance.ICON_TEXT: menuButtonWidget.showIconText(); menuButtonWidget.setLabelStyle('padding-left: 5px;'); break; case Constants.MenuButtonAppearance.TEXT_ICON: menuButtonWidget.showTextIcon(); menuButtonWidget.setLabelStyle('padding-right: 5px;'); break; case Constants.MenuButtonAppearance.NONE: menuButtonWidget.hide(); this._menuButton.container.set_width(0); this._menuButton.container.set_height(0); break; case Constants.MenuButtonAppearance.ICON: default: menuButtonWidget.showIcon(); } } _setButtonText() { const {menuButtonWidget} = this._menuButton; const label = menuButtonWidget.getPanelLabel(); const customTextLabel = ArcMenuManager.settings.get_string('custom-menu-button-text'); label.set_text(customTextLabel); } _setButtonIcon() { const path = ArcMenuManager.settings.get_string('custom-menu-button-icon'); const {menuButtonWidget} = this._menuButton; const stIcon = menuButtonWidget.getPanelIcon(); const iconString = Utils.getMenuButtonIcon(path); stIcon.set_gicon(Gio.icon_new_for_string(iconString)); } _setButtonIconSize() { const {menuButtonWidget} = this._menuButton; const stIcon = menuButtonWidget.getPanelIcon(); const iconSize = ArcMenuManager.settings.get_double('custom-menu-button-icon-size'); const size = iconSize; stIcon.icon_size = size; } _setButtonIconPadding() { const padding = ArcMenuManager.settings.get_int('button-padding'); if (padding > -1) this._menuButton.style = `-natural-hpadding: ${padding * 2}px; -minimum-hpadding: ${padding}px;`; else this._menuButton.style = null; const parent = this._menuButton.get_parent(); if (!parent) return; const children = parent.get_children(); let actorIndex = 0; if (children.length > 1) actorIndex = children.indexOf(this._menuButton); parent.remove_child(this._menuButton); parent.insert_child_at_index(this._menuButton, actorIndex); } _getMenuPosition() { const offset = ArcMenuManager.settings.get_int('menu-button-position-offset'); const positionInPanel = ArcMenuManager.settings.get_enum('position-in-panel'); switch (positionInPanel) { case Constants.MenuPosition.CENTER: return [offset, 'center']; case Constants.MenuPosition.RIGHT: { // get number of childrens in rightBox (without arcmenu) let nChildren = this.panel._rightBox.get_n_children(); nChildren -= this.panel.statusArea.ArcMenu !== undefined; // position where icon should go, // offset = 0, icon should be last // offset = 1, icon should be second last const order = Math.clamp(nChildren - offset, 0, nChildren); return [order, 'right']; } case Constants.MenuPosition.LEFT: default: return [offset, 'left']; } } _configureActivitiesButton() { const showActivities = ArcMenuManager.settings.get_boolean('show-activities-button'); if (this.panel.statusArea.activities) this.panel.statusArea.activities.visible = showActivities; } _addMenuButtonToMainPanel() { const [position, box] = this._getMenuPosition(); this.panel.addToStatusArea('ArcMenu', this._menuButton, position, box); } _removeMenuButtonFromPanel() { this.panel.statusArea['ArcMenu'] = null; } enableButton() { this._addMenuButtonToMainPanel(); this._menuButton.initiate(); } _disableButton() { this._removeMenuButtonFromPanel(); if (this.panel.statusArea.activities) this.panel.statusArea.activities.visible = true; this._menuButton.destroy(); } _isButtonEnabled() { return this.panel && this.panel.statusArea['ArcMenu'] !== null; } destroy() { this._appList = null; if (this._service) { this._service.destroy(); this._service = null; } if (global.toggleArcMenu) delete global.toggleArcMenu; if (this._inputSourceManagerProto) { this._inputSourceManagerProto._getCurrentWindow = this._origGetCurrentWindow; delete this._inputSourceManagerProto; } if (this._perWindowChangedId) { this._inputSourcesSettings.disconnect(this._perWindowChangedId); this._perWindowChangedId = null; } this._inputSourcesSettings = null; if (this._writeTimeoutId) { GLib.source_remove(this._writeTimeoutId); this._writeTimeoutId = null; } if (this._appSystem) this._appSystem.disconnectObject(this); this._appSystem = null; if (this.runnerMenu) { this.runnerMenu.destroy(); this.runnerMenu = null; } ArcMenuManager.settings.disconnectObject(this); if (this._isButtonEnabled()) this._disableButton(); else this._menuButton.destroy(); if (this.isPrimaryPanel) { this._overrideOverlayKey.destroy(); this._overrideOverlayKey = null; this._customKeybinding.destroy(); this._customKeybinding = null; } this._menuButton = null; this.panelInfo = null; this.panel = null; this.isPrimaryPanel = null; } };