import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; import Clutter from 'gi://Clutter'; import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import St from 'gi://St'; import {AppMenu} from 'resource:///org/gnome/shell/ui/appMenu.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; import {ArcMenuManager} from './arcmenuManager.js'; import * as Utils from './utils.js'; const DESKTOP_ICONS_UUIDS = [ 'ding@rastersoft.com', 'gtk4-ding@smedius.gitlab.com', 'desktopicons-neo@darkdemon', ]; /** * * @param {Actor} child */ function isPopupMenuItemVisible(child) { if (child._delegate instanceof PopupMenu.PopupMenuSection) { if (child._delegate.isEmpty()) return false; } return child.visible; } export const AppContextMenu = class ArcMenuAppContextMenu extends AppMenu { constructor(sourceActor, menuLayout) { super(sourceActor, St.Side.TOP); this._menuLayout = menuLayout; this._menuButton = this._menuLayout.menuButton; this._pinnedAppData = this.sourceActor.pinnedAppData; this._enableFavorites = true; this._showSingleWindows = true; this.actor.add_style_class_name('arcmenu-menu app-menu'); Main.uiGroup.add_child(this.actor); this._menuLayout.contextMenuManager.addMenu(this); this.sourceActor.connect('destroy', () => { if (this.isOpen) this.close(); Main.uiGroup.remove_child(this.actor); this.destroy(); }); this.actor.connect('key-press-event', this._menuKeyPress.bind(this)); this._newWindowItem.connect('activate', () => this.closeMenus()); this._onGpuMenuItem.connect('activate', () => this.closeMenus()); this._detailsItem.connect('activate', () => this.closeMenus()); this._arcMenuPinnedItem = this._createMenuItem(_('Pin to ArcMenu'), 8, () => { this.close(); if (this._pinnedAppData) { let sourceSettings; const isFolder = this.sourceActor.folderSettings; if (isFolder) sourceSettings = this.sourceActor.folderSettings; else sourceSettings = ArcMenuManager.settings; const pinnedAppsList = sourceSettings.get_value('pinned-apps').deepUnpack(); for (let i = 0; i < pinnedAppsList.length; i++) { if (pinnedAppsList[i].id === this._app.get_id()) { pinnedAppsList.splice(i, 1); sourceSettings.set_value('pinned-apps', new GLib.Variant('aa{ss}', pinnedAppsList)); break; } } } else { const pinnedAppsList = ArcMenuManager.settings.get_value('pinned-apps').deepUnpack(); const newPinnedAppData = { id: this._app.get_id(), }; pinnedAppsList.push(newPinnedAppData); ArcMenuManager.settings.set_value('pinned-apps', new GLib.Variant('aa{ss}', pinnedAppsList)); } }); this._createDesktopShortcutItem = this._createMenuItem(_('Create Desktop Shortcut'), 7, () => { const [exists, src, dst] = this.getDesktopShortcut(); if (exists && src && dst) { try { dst.delete(null); } catch (e) { console.log(`Failed to delete shortcut: ${e.message}`); } } else if (src && dst) { try { src.copy(dst, Gio.FileCopyFlags.OVERWRITE, null, null); } catch (e) { console.log(`Failed to copy to desktop: ${e.message}`); } } this.close(); this._updateDesktopShortcutItem(); }); this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem(), 8); ArcMenuManager.settings.connectObject('changed::pinned-apps', () => this._updateArcMenuPinnedItem(), this.actor); Main.extensionManager.connectObject('extension-state-changed', (data, changedExtension) => { if (DESKTOP_ICONS_UUIDS.includes(changedExtension.uuid)) this._updateDesktopShortcutItem(); }); this.connect('active-changed', () => this._activeChanged()); } _activeChanged() { if (this._activeMenuItem) Utils.ensureActorVisibleInScrollView(this._activeMenuItem); } open(animate) { if (this._menuButton.tooltipShowingID) { GLib.source_remove(this._menuButton.tooltipShowingID); this._menuButton.tooltipShowingID = null; this._menuButton.tooltipShowing = false; } if (this.sourceActor.tooltip) { this.sourceActor.tooltip.hide(); this._menuButton.tooltipShowing = false; } super.open(animate); this.sourceActor.add_style_pseudo_class('active'); } destroy() { this.destroyed = true; this._createDesktopShortcutItem = null; this._arcMenuPinnedItem = null; this._disconnectSignals(); Main.extensionManager.disconnectObject(this); this._menuButton = null; this._pinnedAppData = null; this._menuLayout = null; super.destroy(); } closeMenus() { this.close(); this._menuLayout.arcMenu.toggle(); } _createMenuItem(labelText, position, callback) { const item = new PopupMenu.PopupMenuItem(labelText); item.connect('activate', () => callback()); this.addMenuItem(item, position); return item; } setApp(app) { if (this._app === app) return; this._app?.disconnectObject(this); if (this.destroyed) return; this._app = app; this._app?.connectObject('windows-changed', () => this._queueUpdateWindowsSection(), this); this._updateWindowsSection(); const appInfo = app?.app_info; const actions = appInfo?.list_actions() ?? []; this._actionSection.removeAll(); actions.forEach(action => { const label = appInfo.get_action_name(action); this._actionSection.addAction(label, event => { if (action === 'new-window') this._animateLaunch(); this._app.launch_action(action, event.get_time(), -1); this.closeMenus(); }); }); this._updateQuitItem(); this._updateNewWindowItem(); this._updateFavoriteItem(); this._updateGpuItem(); this._updateArcMenuPinnedItem(); this._updateDesktopShortcutItem(); } isDesktopActive() { let hasActiveDesktop = false; DESKTOP_ICONS_UUIDS.forEach(uuid => { const extension = Main.extensionManager.lookup(uuid); if (extension?.state === Utils.ExtensionState.ACTIVE) hasActiveDesktop = true; }); return hasActiveDesktop; } getDesktopShortcut() { if (!this._app) return [false, null, null]; const fileDestination = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP); const src = Gio.File.new_for_path(this._app.get_app_info().get_filename()); const dst = Gio.File.new_for_path(GLib.build_filenamev([fileDestination, src.get_basename()])); const exists = dst.query_exists(null); return [exists, src, dst]; } _updateDesktopShortcutItem() { const isDesktopActive = this.isDesktopActive(); if (!this._app || !isDesktopActive) { this._createDesktopShortcutItem.visible = false; return; } this._createDesktopShortcutItem.visible = true; const [exists, src_, dst_] = this.getDesktopShortcut(); this._createDesktopShortcutItem.label.text = exists ? _('Delete Desktop Shortcut') : _('Create Desktop Shortcut'); } // For Custom Shortcuts in Pinned Apps category. ie ArcMenu Settings addUnpinItem(id, folder = null) { this._disconnectSignals(); this.removeAll(); this._id = id; this._arcMenuPinnedItem = this._createMenuItem(_('Unpin from ArcMenu'), 0, () => { this.close(); let sourceSettings; if (!folder && this.sourceActor.folderSettings) sourceSettings = this.sourceActor.folderSettings; else sourceSettings = ArcMenuManager.settings; // Unpinned the folder, reset all folder settings keys if (folder) { const keys = folder.settings_schema.list_keys(); for (const key of keys) folder.reset(key); return; } const pinnedAppsList = sourceSettings.get_value('pinned-apps').deepUnpack(); for (let i = 0; i < pinnedAppsList.length; i++) { if (pinnedAppsList[i].id === this._id) { pinnedAppsList.splice(i, 1); sourceSettings.set_value('pinned-apps', new GLib.Variant('aa{ss}', pinnedAppsList)); break; } } }); } _updateArcMenuPinnedItem() { if (!this._app) { this._arcMenuPinnedItem.visible = false; return; } this._arcMenuPinnedItem.visible = this._menuLayout.hasPinnedApps; this._arcMenuPinnedItem.label.text = this._pinnedAppData ? _('Unpin from ArcMenu') : _('Pin to ArcMenu'); } _updateWindowsSection() { if (this._updateWindowsLaterId) { const laters = global.compositor.get_laters(); laters.remove(this._updateWindowsLaterId); } this._updateWindowsLaterId = 0; this._windowSection.removeAll(); this._openWindowsHeader.hide(); if (!this._app) return; const minWindows = this._showSingleWindows ? 1 : 2; const windows = this._app.get_windows().filter(w => !w.skip_taskbar); if (windows.length < minWindows) return; this._openWindowsHeader.show(); windows.forEach(window => { const title = window.title || this._app.get_name(); const item = this._windowSection.addAction(title, event => { this.closeMenus(); Main.activateWindow(window, event.get_time()); }); window.connectObject('notify::title', () => { item.label.text = window.title || this._app.get_name(); }, item); }); } setFolderPath(path) { this._disconnectSignals(); this.removeAll(); this._openFolderLocationItem = this._createMenuItem(_('Open Folder Location'), 0, () => { const file = Gio.File.new_for_path(path); const context = global.create_app_launch_context(Clutter.get_current_event().get_time(), -1); new Promise((resolve, reject) => { Gio.AppInfo.launch_default_for_uri_async(file.get_uri(), context, null, (o, res) => { try { Gio.AppInfo.launch_default_for_uri_finish(res); resolve(); } catch (e) { reject(e); } }); }); this.closeMenus(); }); } addAdditionalAction(name, action) { if (!this._openFolderLocationItem) { this._disconnectSignals(); this.removeAll(); } else { this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); } this._additionalAction = new PopupMenu.PopupMenuItem(_(name)); this._additionalAction.connect('activate', () => { this.close(); action(); }); this.addMenuItem(this._additionalAction); } isEmpty() { if (!this._app && !this._openFolderLocationItem && !this._id && !this._additionalAction) return true; const hasVisibleChildren = this.box.get_children().some(child => { if (child._delegate instanceof PopupMenu.PopupSeparatorMenuItem) return false; return isPopupMenuItemVisible(child); }); return !hasVisibleChildren; } centerBoxPointerPosition() { this._boxPointer.setSourceAlignment(.50); this._arrowAlignment = .5; this._boxPointer._border.queue_repaint(); } rightBoxPointerPosition() { this._arrowSide = St.Side.LEFT; this._boxPointer._arrowSide = St.Side.LEFT; this._boxPointer._userArrowSide = St.Side.LEFT; this._boxPointer.setSourceAlignment(.50); this._arrowAlignment = .5; this._boxPointer._border.queue_repaint(); } _disconnectSignals() { ArcMenuManager.settings.disconnectObject(this.actor); this._appSystem.disconnectObject(this.actor); this._parentalControlsManager.disconnectObject(this.actor); this._appFavorites.disconnectObject(this.actor); global.settings.disconnectObject(this.actor); global.disconnectObject(this.actor); } close(animate) { super.close(animate); this.sourceActor.remove_style_pseudo_class('active'); this.sourceActor.sync_hover(); } _menuKeyPress(actor, event) { const symbol = event.get_key_symbol(); if (symbol === Clutter.KEY_Menu) { this.toggle(); this.sourceActor.sync_hover(); } } _onKeyPress() { return Clutter.EVENT_PROPAGATE; } };