2025-05-09 09:48:24 +02:00

532 lines
20 KiB
JavaScript

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;
}
};