/* * This file is part of the Dash-To-Panel extension for Gnome 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Credits: * This file is based on code from the Dash to Dock extension by micheleg * * Some code was also adapted from the upstream Gnome Shell source code. */ import * as Intellihide from './intellihide.js' import * as Utils from './utils.js' import Clutter from 'gi://Clutter' import Gio from 'gi://Gio' import Shell from 'gi://Shell' import St from 'gi://St' import * as Main from 'resource:///org/gnome/shell/ui/main.js' import * as WindowManager from 'resource:///org/gnome/shell/ui/windowManager.js' import { WindowPreview } from 'resource:///org/gnome/shell/ui/windowPreview.js' import { InjectionManager } from 'resource:///org/gnome/shell/extensions/extension.js' import { SETTINGS } from './extension.js' const GS_HOTKEYS_KEY = 'switch-to-application-' // When the dash is shown, workspace window preview bottom labels go over it (default // gnome-shell behavior), but when the extension hides the dash, leave some space // so those labels don't go over a bottom panel const LABEL_MARGIN = 60 //timeout names const T1 = 'swipeEndTimeout' const T2 = 'numberOverlayTimeout' export const Overview = class { constructor() { this._injectionManager = new InjectionManager() this._numHotkeys = 10 } enable(primaryPanel) { this._panel = primaryPanel this.taskbar = primaryPanel.taskbar this._injectionsHandler = new Utils.InjectionsHandler() this._signalsHandler = new Utils.GlobalSignalsHandler() this._timeoutsHandler = new Utils.TimeoutsHandler() this._optionalWorkspaceIsolation() this._optionalHotKeys() this._optionalNumberOverlay() this._optionalClickToExit() this.toggleDash() this._adaptAlloc() this._signalsHandler.add([ SETTINGS, ['changed::stockgs-keep-dash', 'changed::panel-sizes'], () => this.toggleDash(), ]) } disable() { this._signalsHandler.destroy() this._injectionsHandler.destroy() this._timeoutsHandler.destroy() this._injectionManager.clear() this.toggleDash(true) // Remove key bindings this._disableHotKeys() this._disableExtraShortcut() this._disableClickToExit() } toggleDash(visible) { if (visible === undefined) { visible = SETTINGS.get_boolean('stockgs-keep-dash') } let visibilityFunc = visible ? 'show' : 'hide' let height = visible ? -1 : LABEL_MARGIN * Utils.getScaleFactor() let overviewControls = Main.overview._overview._controls overviewControls.dash[visibilityFunc]() overviewControls.dash.set_height(height) } _adaptAlloc() { let overviewControls = Main.overview._overview._controls this._injectionManager.overrideMethod( Object.getPrototypeOf(overviewControls), 'vfunc_allocate', (originalAllocate) => (box) => { let focusedPanel = this._panel.panelManager.focusedMonitorPanel if (focusedPanel) { let position = focusedPanel.geom.position let isBottom = position == St.Side.BOTTOM if (focusedPanel.intellihide?.enabled) { // Panel intellihide is enabled (struts aren't taken into account on overview allocation), // dynamically modify the overview box to follow the reveal/hide animation let { transitioning, finalState, progress } = overviewControls._stateAdjustment.getStateTransitionParams() let size = focusedPanel.geom[focusedPanel.checkIfVertical() ? 'w' : 'h'] * (transitioning ? Math.abs((finalState != 0 ? 0 : 1) - progress) : 1) if (isBottom || position == St.Side.RIGHT) box[focusedPanel.fixedCoord.c2] -= size else box[focusedPanel.fixedCoord.c1] += size } else if (isBottom) // The default overview allocation takes into account external // struts, everywhere but the bottom where the dash is usually fixed anyway. // If there is a bottom panel under the dash location, give it some space here box.y2 -= focusedPanel.geom.h } originalAllocate.call(overviewControls, box) }, ) } /** * Isolate overview to open new windows for inactive apps */ _optionalWorkspaceIsolation() { let label = 'optionalWorkspaceIsolation' let enable = () => { this._injectionsHandler.removeWithLabel(label) this._injectionsHandler.addWithLabel(label, [ Shell.App.prototype, 'activate', IsolatedOverview, ]) this._signalsHandler.removeWithLabel(label) this._signalsHandler.addWithLabel(label, [ global.window_manager, 'switch-workspace', () => this._panel.panelManager.allPanels.forEach((p) => p.taskbar.handleIsolatedWorkspaceSwitch(), ), ]) } let disable = () => { this._signalsHandler.removeWithLabel(label) this._injectionsHandler.removeWithLabel(label) } function IsolatedOverview() { // These lines take care of Nautilus for icons on Desktop let activeWorkspace = Utils.DisplayWrapper.getWorkspaceManager().get_active_workspace() let windows = this.get_windows().filter( (w) => w.get_workspace().index() == activeWorkspace.index(), ) if ( windows.length > 0 && (!(windows.length == 1 && windows[0].skip_taskbar) || this.is_on_workspace(activeWorkspace)) ) return Main.activateWindow(windows[0]) return this.open_new_window(-1) } this._signalsHandler.add([ SETTINGS, 'changed::isolate-workspaces', () => { this._panel.panelManager.allPanels.forEach((p) => p.taskbar.resetAppIcons(), ) if (SETTINGS.get_boolean('isolate-workspaces')) enable() else disable() }, ]) if (SETTINGS.get_boolean('isolate-workspaces')) enable() } // Hotkeys _activateApp(appIndex, modifiers) { let seenApps = {} let apps = [] this.taskbar._getAppIcons().forEach((appIcon) => { if (!seenApps[appIcon.app] || this.taskbar.allowSplitApps) { apps.push(appIcon) } seenApps[appIcon.app] = (seenApps[appIcon.app] || 0) + 1 }) this._showOverlay() if (appIndex < apps.length) { let appIcon = apps[appIndex] let seenAppCount = seenApps[appIcon.app] let windowCount = appIcon.window || appIcon._hotkeysCycle ? seenAppCount : appIcon._nWindows if ( SETTINGS.get_boolean('shortcut-previews') && windowCount > 1 && !( modifiers & ~(Clutter.ModifierType.MOD1_MASK | Clutter.ModifierType.SUPER_MASK) ) ) { //ignore the alt (MOD1_MASK) and super key (SUPER_MASK) if ( this._hotkeyPreviewCycleInfo && this._hotkeyPreviewCycleInfo.appIcon != appIcon ) { this._endHotkeyPreviewCycle() } if (!this._hotkeyPreviewCycleInfo) { this._hotkeyPreviewCycleInfo = { appIcon: appIcon, currentWindow: appIcon.window, keyFocusOutId: appIcon.connect('key-focus-out', () => appIcon.grab_key_focus(), ), capturedEventId: global.stage.connect( 'captured-event', (actor, e) => { if ( e.type() == Clutter.EventType.KEY_RELEASE && e.get_key_symbol() == (Clutter.KEY_Super_L || Clutter.Super_L) ) { this._endHotkeyPreviewCycle(true) } return Clutter.EVENT_PROPAGATE }, ), } appIcon._hotkeysCycle = appIcon.window appIcon.window = null appIcon._previewMenu.open(appIcon, true) appIcon.grab_key_focus() } appIcon._previewMenu.focusNext() } else { // Activate with button = 1, i.e. same as left click let button = 1 this._endHotkeyPreviewCycle() appIcon.activate(button, modifiers, !this.taskbar.allowSplitApps) } } } _endHotkeyPreviewCycle(focusWindow) { if (this._hotkeyPreviewCycleInfo) { global.stage.disconnect(this._hotkeyPreviewCycleInfo.capturedEventId) this._hotkeyPreviewCycleInfo.appIcon.disconnect( this._hotkeyPreviewCycleInfo.keyFocusOutId, ) if (focusWindow) { this._hotkeyPreviewCycleInfo.appIcon._previewMenu.activateFocused() } else this._hotkeyPreviewCycleInfo.appIcon._previewMenu.close() this._hotkeyPreviewCycleInfo.appIcon.window = this._hotkeyPreviewCycleInfo.currentWindow delete this._hotkeyPreviewCycleInfo.appIcon._hotkeysCycle this._hotkeyPreviewCycleInfo = 0 } } _optionalHotKeys() { this._hotKeysEnabled = false if (SETTINGS.get_boolean('hot-keys')) this._enableHotKeys() this._signalsHandler.add([ SETTINGS, 'changed::hot-keys', () => { if (SETTINGS.get_boolean('hot-keys')) this._enableHotKeys() else this._disableHotKeys() }, ]) } _resetHotkeys() { this._disableHotKeys() this._enableHotKeys() } _enableHotKeys() { if (this._hotKeysEnabled) return //3.32 introduced app hotkeys, disable them to prevent conflicts if (Main.wm._switchToApplication) { for (let i = 1; i < 10; ++i) { Utils.removeKeybinding(GS_HOTKEYS_KEY + i) } } // Setup keyboard bindings for taskbar elements let shortcutNumKeys = SETTINGS.get_string('shortcut-num-keys') let bothNumKeys = shortcutNumKeys == 'BOTH' let keys = [] let prefixModifiers = Clutter.ModifierType.SUPER_MASK if (SETTINGS.get_string('hotkey-prefix-text') == 'SuperAlt') prefixModifiers |= Clutter.ModifierType.MOD1_MASK if (bothNumKeys || shortcutNumKeys == 'NUM_ROW') { keys.push('app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-') // Regular numbers } if (bothNumKeys || shortcutNumKeys == 'NUM_KEYPAD') { keys.push('app-hotkey-kp-', 'app-shift-hotkey-kp-', 'app-ctrl-hotkey-kp-') // Key-pad numbers } keys.forEach(function (key) { let modifiers = prefixModifiers // for some reason, in gnome-shell >= 40 Clutter.get_current_event() is now empty // for keyboard events. Create here the modifiers that are needed in appicon.activate modifiers |= key.indexOf('-shift-') >= 0 ? Clutter.ModifierType.SHIFT_MASK : 0 modifiers |= key.indexOf('-ctrl-') >= 0 ? Clutter.ModifierType.CONTROL_MASK : 0 for (let i = 0; i < this._numHotkeys; i++) { let appNum = i Utils.addKeybinding(key + (i + 1), SETTINGS, () => this._activateApp(appNum, modifiers), ) } }, this) this._hotKeysEnabled = true if (SETTINGS.get_string('hotkeys-overlay-combo') === 'ALWAYS') this.taskbar.toggleNumberOverlay(true) } _disableHotKeys() { if (!this._hotKeysEnabled) return let keys = [ 'app-hotkey-', 'app-shift-hotkey-', 'app-ctrl-hotkey-', // Regular numbers 'app-hotkey-kp-', 'app-shift-hotkey-kp-', 'app-ctrl-hotkey-kp-', ] // Key-pad numbers keys.forEach(function (key) { for (let i = 0; i < this._numHotkeys; i++) { Utils.removeKeybinding(key + (i + 1)) } }, this) if (Main.wm._switchToApplication) { let gsSettings = new Gio.Settings({ schema_id: WindowManager.SHELL_KEYBINDINGS_SCHEMA, }) for (let i = 1; i < 10; ++i) { Utils.addKeybinding( GS_HOTKEYS_KEY + i, gsSettings, Main.wm._switchToApplication.bind(Main.wm), ) } } this._hotKeysEnabled = false this.taskbar.toggleNumberOverlay(false) } _optionalNumberOverlay() { // Enable extra shortcut if (SETTINGS.get_boolean('hot-keys')) this._enableExtraShortcut() this._signalsHandler.add( [SETTINGS, 'changed::hot-keys', this._checkHotkeysOptions.bind(this)], [ SETTINGS, 'changed::hotkeys-overlay-combo', () => { if ( SETTINGS.get_boolean('hot-keys') && SETTINGS.get_string('hotkeys-overlay-combo') === 'ALWAYS' ) this.taskbar.toggleNumberOverlay(true) else this.taskbar.toggleNumberOverlay(false) }, ], [SETTINGS, 'changed::shortcut-num-keys', () => this._resetHotkeys()], ) } _checkHotkeysOptions() { if (SETTINGS.get_boolean('hot-keys')) this._enableExtraShortcut() else this._disableExtraShortcut() } _enableExtraShortcut() { Utils.addKeybinding('shortcut', SETTINGS, () => this._showOverlay(true)) } _disableExtraShortcut() { Utils.removeKeybinding('shortcut') } _showOverlay(overlayFromShortcut) { //wait for intellihide timeout initialization if (!this._panel.intellihide) { return } // Restart the counting if the shortcut is pressed again let hotkey_option = SETTINGS.get_string('hotkeys-overlay-combo') if (hotkey_option === 'NEVER') return if (hotkey_option === 'TEMPORARILY' || overlayFromShortcut) this.taskbar.toggleNumberOverlay(true) this._panel.intellihide.revealAndHold(Intellihide.Hold.TEMPORARY) let timeout = SETTINGS.get_int('overlay-timeout') if (overlayFromShortcut) { timeout = SETTINGS.get_int('shortcut-timeout') } // Hide the overlay/dock after the timeout this._timeoutsHandler.add([ T2, timeout, () => { if (hotkey_option != 'ALWAYS') { this.taskbar.toggleNumberOverlay(false) } this._panel.intellihide.release(Intellihide.Hold.TEMPORARY) }, ]) } _optionalClickToExit() { this._clickToExitEnabled = false if (SETTINGS.get_boolean('overview-click-to-exit')) this._enableClickToExit() this._signalsHandler.add([ SETTINGS, 'changed::overview-click-to-exit', () => { if (SETTINGS.get_boolean('overview-click-to-exit')) this._enableClickToExit() else this._disableClickToExit() }, ]) } _enableClickToExit() { if (this._clickToExitEnabled) return this._signalsHandler.addWithLabel('click-to-exit', [ Main.layoutManager.overviewGroup, 'button-release-event', () => { let [x, y] = global.get_pointer() let pickedActor = global.stage.get_actor_at_pos( Clutter.PickMode.REACTIVE, x, y, ) if (pickedActor) { let parent = pickedActor.get_parent() if ( (pickedActor.has_style_class_name && pickedActor.has_style_class_name('apps-scroll-view') && !pickedActor.has_style_pseudo_class('first-child')) || (parent?.has_style_class_name && parent.has_style_class_name('window-picker')) || Main.overview._overview._controls._searchEntryBin.contains( pickedActor, ) || pickedActor instanceof WindowPreview ) return Clutter.EVENT_PROPAGATE } Main.overview.toggle() }, ]) this._clickToExitEnabled = true } _disableClickToExit() { if (!this._clickToExitEnabled) return this._signalsHandler.removeWithLabel('click-to-exit') this._clickToExitEnabled = false } _onSwipeBegin() { this._swiping = true return true } _onSwipeEnd() { this._timeoutsHandler.add([T1, 0, () => (this._swiping = false)]) return true } }