519 lines
18 KiB
JavaScript
519 lines
18 KiB
JavaScript
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
* 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 is very good and 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;
|
|
}
|
|
|
|
}
|