1532 lines
56 KiB
JavaScript
Executable File
1532 lines
56 KiB
JavaScript
Executable File
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
import {
|
|
Clutter,
|
|
Gio,
|
|
GLib,
|
|
GObject,
|
|
Meta,
|
|
Mtk,
|
|
Shell,
|
|
St,
|
|
} from './dependencies/gi.js';
|
|
|
|
import {
|
|
AppDisplay,
|
|
AppFavorites,
|
|
BoxPointer,
|
|
Dash,
|
|
Main,
|
|
PopupMenu,
|
|
} from './dependencies/shell/ui.js';
|
|
|
|
import {
|
|
ParentalControlsManager,
|
|
} from './dependencies/shell/misc.js';
|
|
|
|
import {Config} from './dependencies/shell/misc.js';
|
|
|
|
import {
|
|
AppIconIndicators,
|
|
DBusMenuUtils,
|
|
Docking,
|
|
Locations,
|
|
Theming,
|
|
Utils,
|
|
WindowPreview,
|
|
} from './imports.js';
|
|
|
|
import {Extension} from './dependencies/shell/extensions/extension.js';
|
|
|
|
// Use __ () and N__() for the extension gettext domain, and reuse
|
|
// the shell domain with the default _() and N_()
|
|
const {gettext: __, ngettext} = Extension;
|
|
|
|
const DBusMenu = await DBusMenuUtils.haveDBusMenu();
|
|
|
|
const tracker = Shell.WindowTracker.get_default();
|
|
|
|
const Labels = Object.freeze({
|
|
ISOLATE_MONITORS: Symbol('isolate-monitors'),
|
|
ISOLATE_WORKSPACES: Symbol('isolate-workspaces'),
|
|
URGENT_WINDOWS: Symbol('urgent-windows'),
|
|
});
|
|
|
|
const clickAction = Object.freeze({
|
|
SKIP: 0,
|
|
MINIMIZE: 1,
|
|
LAUNCH: 2,
|
|
CYCLE_WINDOWS: 3,
|
|
MINIMIZE_OR_OVERVIEW: 4,
|
|
PREVIEWS: 5,
|
|
MINIMIZE_OR_PREVIEWS: 6,
|
|
FOCUS_OR_PREVIEWS: 7,
|
|
FOCUS_OR_APP_SPREAD: 8,
|
|
FOCUS_MINIMIZE_OR_PREVIEWS: 9,
|
|
FOCUS_MINIMIZE_OR_APP_SPREAD: 10,
|
|
QUIT: 11,
|
|
});
|
|
|
|
const scrollAction = Object.freeze({
|
|
DO_NOTHING: 0,
|
|
CYCLE_WINDOWS: 1,
|
|
SWITCH_WORKSPACE: 2,
|
|
});
|
|
|
|
let recentlyClickedAppLoopId = 0;
|
|
let recentlyClickedApp = null;
|
|
let recentlyClickedAppWindows = null;
|
|
let recentlyClickedAppIndex = 0;
|
|
let recentlyClickedAppMonitor = -1;
|
|
|
|
/**
|
|
* Extend AppIcon
|
|
*
|
|
* - Apply a css class based on the number of windows of each application (#N);
|
|
* - Customized indicators for running applications in place of the default "dot" style which is hidden (#N);
|
|
* a class of the form "running#N" is applied to the AppWellIcon actor.
|
|
* like the original .running one.
|
|
* - Add a .focused style to the focused app
|
|
* - Customize click actions.
|
|
* - Update minimization animation target
|
|
* - Update menu if open on windows change
|
|
*/
|
|
const DockAbstractAppIcon = GObject.registerClass({
|
|
GTypeFlags: GObject.TypeFlags.ABSTRACT,
|
|
Properties: {
|
|
'focused': GObject.ParamSpec.boolean(
|
|
'focused', 'focused', 'focused',
|
|
GObject.ParamFlags.READWRITE,
|
|
false),
|
|
'running': GObject.ParamSpec.boolean(
|
|
'running', 'running', 'running',
|
|
GObject.ParamFlags.READWRITE,
|
|
false),
|
|
'urgent': GObject.ParamSpec.boolean(
|
|
'urgent', 'urgent', 'urgent',
|
|
GObject.ParamFlags.READWRITE,
|
|
false),
|
|
'windows-count': GObject.ParamSpec.uint(
|
|
'windows-count', 'windows-count', 'windows-count',
|
|
GObject.ParamFlags.READWRITE,
|
|
0, GLib.MAXUINT32, 0),
|
|
},
|
|
}, class DockAbstractAppIcon extends Dash.DashIcon {
|
|
// settings are required inside.
|
|
_init(app, monitorIndex, iconAnimator) {
|
|
super._init(app);
|
|
|
|
// a prefix is required to avoid conflicting with the parent class variable
|
|
this.monitorIndex = monitorIndex;
|
|
this._signalsHandler = new Utils.GlobalSignalsHandler(this);
|
|
this.iconAnimator = iconAnimator;
|
|
this._indicator = new AppIconIndicators.AppIconIndicator(this);
|
|
|
|
// Monitor windows-changes instead of app state.
|
|
// Keep using the same Id and function callback (that is extended)
|
|
if (this._stateChangedId > 0) {
|
|
this.app.disconnect(this._stateChangedId);
|
|
this._stateChangedId = 0;
|
|
}
|
|
|
|
this._signalsHandler.add(this.app, 'windows-changed', () => this._updateWindows());
|
|
this._signalsHandler.add(this.app, 'notify::state', () => this._updateRunningState());
|
|
this._signalsHandler.add(global.display, 'window-demands-attention', (_dpy, window) =>
|
|
this._onWindowDemandsAttention(window));
|
|
this._signalsHandler.add(global.display, 'window-marked-urgent', (_dpy, window) =>
|
|
this._onWindowDemandsAttention(window));
|
|
|
|
// In Wayland sessions, this signal is needed to track the state of windows dragged
|
|
// from one monitor to another. As this is triggered quite often (whenever a new winow
|
|
// of any application opened or moved to a different desktop),
|
|
// we restrict this signal to the case when 'isolate-monitors' is true,
|
|
// and if there are at least 2 monitors.
|
|
if (Docking.DockManager.settings.isolateMonitors &&
|
|
Main.layoutManager.monitors.length > 1) {
|
|
this._signalsHandler.addWithLabel(Labels.ISOLATE_MONITORS,
|
|
global.display,
|
|
'window-entered-monitor',
|
|
this._onWindowEntered.bind(this));
|
|
}
|
|
|
|
this.connect('notify::running', () => {
|
|
if (this.running)
|
|
this.add_style_class_name('running');
|
|
else
|
|
this.remove_style_class_name('running');
|
|
});
|
|
|
|
this.connect('notify::focused', () => {
|
|
if (this.focused)
|
|
this.add_style_class_name('focused');
|
|
else
|
|
this.remove_style_class_name('focused');
|
|
});
|
|
|
|
const {notificationsMonitor} = Docking.DockManager.getDefault();
|
|
|
|
this.connect('notify::urgent', () => {
|
|
const icon = this.icon._iconBin;
|
|
this._signalsHandler.removeWithLabel(Labels.URGENT_WINDOWS);
|
|
if (this.urgent) {
|
|
if (Docking.DockManager.settings.danceUrgentApplications &&
|
|
notificationsMonitor.enabled) {
|
|
icon.set_pivot_point(0.5, 0.5);
|
|
this.iconAnimator.addAnimation(icon, 'wiggle');
|
|
}
|
|
if (this.running && !this._urgentWindows.size) {
|
|
const urgentWindows = this.getInterestingWindows();
|
|
urgentWindows.forEach(w => (w._manualUrgency = true));
|
|
this._updateUrgentWindows(urgentWindows);
|
|
}
|
|
} else {
|
|
this.iconAnimator.removeAnimation(icon, 'wiggle');
|
|
icon.rotation_angle_z = 0;
|
|
this._urgentWindows.forEach(w => delete w._manualUrgency);
|
|
this._updateUrgentWindows();
|
|
}
|
|
});
|
|
|
|
this._urgentWindows = new Set();
|
|
this._progressOverlayArea = null;
|
|
this._progress = 0;
|
|
|
|
[
|
|
'apply-custom-theme',
|
|
'running-indicator-style',
|
|
'show-icons-emblems',
|
|
'show-icons-notifications-counter',
|
|
'application-counter-overrides-notifications',
|
|
].forEach(key => {
|
|
this._signalsHandler.add(
|
|
Docking.DockManager.settings,
|
|
`changed::${key}`, () => {
|
|
this._indicator.destroy();
|
|
this._indicator = new AppIconIndicators.AppIconIndicator(this);
|
|
}
|
|
);
|
|
});
|
|
|
|
this._signalsHandler.add(notificationsMonitor, 'state-changed', () => {
|
|
this._indicator.destroy();
|
|
this._indicator = new AppIconIndicators.AppIconIndicator(this);
|
|
});
|
|
|
|
this._updateState();
|
|
this._numberOverlay();
|
|
|
|
this._previewMenuManager = null;
|
|
this._previewMenu = null;
|
|
}
|
|
|
|
_onDestroy() {
|
|
super._onDestroy();
|
|
|
|
// This is necessary due to an upstream bug
|
|
// https://bugzilla.gnome.org/show_bug.cgi?id=757556
|
|
// It can be safely removed once it get solved upstrea.
|
|
if (this._menu)
|
|
this._menu.close(false);
|
|
}
|
|
|
|
ownsWindow(window) {
|
|
return this.app === tracker.get_window_app(window);
|
|
}
|
|
|
|
_onWindowEntered(metaScreen, monitorIndex, metaWin) {
|
|
if (this.ownsWindow(metaWin))
|
|
this._updateWindows();
|
|
}
|
|
|
|
vfunc_scroll_event(scrollEvent) {
|
|
const {settings} = Docking.DockManager;
|
|
const isEnabled = settings.scrollAction === scrollAction.CYCLE_WINDOWS;
|
|
if (!isEnabled)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
// We only activate windows of running applications, i.e. we never open new windows
|
|
// We check if the app is running, and that the # of windows is > 0 in
|
|
// case we use workspace isolation,
|
|
if (!this.running)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
if (this._optionalScrollCycleWindowsDeadTimeId) {
|
|
return Clutter.EVENT_PROPAGATE;
|
|
} else {
|
|
this._optionalScrollCycleWindowsDeadTimeId = GLib.timeout_add(
|
|
GLib.PRIORITY_DEFAULT, 250, () => {
|
|
this._optionalScrollCycleWindowsDeadTimeId = 0;
|
|
});
|
|
}
|
|
|
|
let direction = null;
|
|
|
|
switch (scrollEvent.direction) {
|
|
case Clutter.ScrollDirection.UP:
|
|
direction = Meta.MotionDirection.UP;
|
|
break;
|
|
case Clutter.ScrollDirection.DOWN:
|
|
direction = Meta.MotionDirection.DOWN;
|
|
break;
|
|
case Clutter.ScrollDirection.SMOOTH: {
|
|
const [, dy] = Clutter.get_current_event().get_scroll_delta();
|
|
if (dy < 0)
|
|
direction = Meta.MotionDirection.UP;
|
|
else if (dy > 0)
|
|
direction = Meta.MotionDirection.DOWN;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!Main.overview.visible) {
|
|
const reversed = direction === Meta.MotionDirection.UP;
|
|
if (this.focused && !this._urgentWindows.size) {
|
|
this._cycleThroughWindows(reversed);
|
|
} else {
|
|
// Activate the first window
|
|
const windows = this.getInterestingWindows();
|
|
if (windows.length > 0) {
|
|
const [w] = windows;
|
|
Main.activateWindow(w);
|
|
}
|
|
}
|
|
} else {
|
|
this.app.activate();
|
|
}
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
_updateWindows() {
|
|
if (this._menu && this._menu.isOpen)
|
|
this._menu.update();
|
|
|
|
this._updateState();
|
|
this.updateIconGeometry();
|
|
}
|
|
|
|
_updateState() {
|
|
this._urgentWindows.clear();
|
|
const interestingWindows = this.getInterestingWindows();
|
|
this.windowsCount = interestingWindows.length;
|
|
this._updateRunningState();
|
|
this._updateFocusState();
|
|
this._updateUrgentWindows(interestingWindows);
|
|
|
|
if (Docking.DockManager.settings.isolateWorkspaces) {
|
|
this._signalsHandler.removeWithLabel(Labels.ISOLATE_WORKSPACES);
|
|
interestingWindows.forEach(window =>
|
|
this._signalsHandler.addWithLabel(Labels.ISOLATE_WORKSPACES,
|
|
window, 'workspace-changed', () => this._updateWindows()));
|
|
}
|
|
}
|
|
|
|
_updateRunningState() {
|
|
this.running = (this.app.state === Shell.AppState.RUNNING) && this.windowsCount;
|
|
}
|
|
|
|
_updateFocusState() {
|
|
this.focused = tracker.focus_app === this.app && this.running;
|
|
}
|
|
|
|
_updateUrgentWindows(interestingWindows) {
|
|
this._signalsHandler.removeWithLabel(Labels.URGENT_WINDOWS);
|
|
this._urgentWindows.clear();
|
|
if (interestingWindows === undefined)
|
|
interestingWindows = this.getInterestingWindows();
|
|
interestingWindows.filter(isWindowUrgent).forEach(win => this._addUrgentWindow(win));
|
|
this.urgent = !!this._urgentWindows.size;
|
|
}
|
|
|
|
_onWindowDemandsAttention(window) {
|
|
if (this.ownsWindow(window) && isWindowUrgent(window))
|
|
this._addUrgentWindow(window);
|
|
}
|
|
|
|
_addUrgentWindow(window) {
|
|
if (this._urgentWindows.has(window))
|
|
return;
|
|
|
|
if (window._manualUrgency && window.has_focus()) {
|
|
delete window._manualUrgency;
|
|
return;
|
|
}
|
|
|
|
this._urgentWindows.add(window);
|
|
this.urgent = true;
|
|
|
|
const onDemandsAttentionChanged = () => {
|
|
if (!isWindowUrgent(window))
|
|
this._updateUrgentWindows();
|
|
};
|
|
|
|
if (window.demandsAttention) {
|
|
this._signalsHandler.addWithLabel(Labels.URGENT_WINDOWS, window,
|
|
'notify::demands-attention', () => onDemandsAttentionChanged());
|
|
}
|
|
if (window.urgent) {
|
|
this._signalsHandler.addWithLabel(Labels.URGENT_WINDOWS, window,
|
|
'notify::urgent', () => onDemandsAttentionChanged());
|
|
}
|
|
if (window._manualUrgency) {
|
|
this._signalsHandler.addWithLabel(Labels.URGENT_WINDOWS, window,
|
|
'focus', () => {
|
|
delete window._manualUrgency;
|
|
onDemandsAttentionChanged();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update target for minimization animation
|
|
*/
|
|
updateIconGeometry() {
|
|
// If (for unknown reason) the actor is not on the stage the reported size
|
|
// and position are random values, which might exceeds the integer range
|
|
// resulting in an error when assigned to the a rect. This is a more like
|
|
// a workaround to prevent flooding the system with errors.
|
|
if (!this.get_stage())
|
|
return;
|
|
|
|
const rect = new Mtk.Rectangle();
|
|
|
|
[rect.x, rect.y] = this.get_transformed_position();
|
|
[rect.width, rect.height] = this.get_transformed_size();
|
|
|
|
let windows = this.getWindows();
|
|
if (Docking.DockManager.settings.multiMonitor) {
|
|
const {monitorIndex} = this;
|
|
windows = windows.filter(w => w.get_monitor() === monitorIndex);
|
|
}
|
|
windows.forEach(w => w.set_icon_geometry(rect));
|
|
}
|
|
|
|
_updateRunningStyle() {
|
|
// The logic originally in this function has been moved to
|
|
// AppIconIndicatorBase._updateDefaultDot(). However it cannot be removed as
|
|
// it called by the parent constructor.
|
|
}
|
|
|
|
popupMenu() {
|
|
this._removeMenuTimeout();
|
|
this.fake_release();
|
|
this._draggable.fakeRelease();
|
|
|
|
if (!this._menu) {
|
|
this._menu = new DockAppIconMenu(this);
|
|
this._menu.connect('activate-window', (menu, window) => {
|
|
if (window) {
|
|
Main.activateWindow(window);
|
|
} else {
|
|
Main.overview.hide();
|
|
Main.panel.closeCalendar();
|
|
}
|
|
});
|
|
this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
|
|
if (!isPoppedUp) {
|
|
this._onMenuPoppedDown();
|
|
} else {
|
|
// Setting the max-height is s useful if part of the menu is
|
|
// scrollable so the minimum height is smaller than the natural height.
|
|
const monitorIndex = Main.layoutManager.findIndexForActor(this);
|
|
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
|
|
const position = Utils.getPosition();
|
|
const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
|
|
const isHorizontal = position === St.Side.TOP || position === St.Side.BOTTOM;
|
|
// If horizontal also remove the height of the dash
|
|
const {dockFixed: fixedDock} = Docking.DockManager.settings;
|
|
const additionalMargin = isHorizontal && !fixedDock ? Main.overview.dash.height : 0;
|
|
const verticalMargins = this._menu.actor.margin_top + this._menu.actor.margin_bottom;
|
|
const maxMenuHeight = workArea.height - additionalMargin - verticalMargins;
|
|
// Also set a max width to the menu, so long labels (long windows title) get truncated
|
|
this._menu.actor.style = 'max-width: 400px; ' +
|
|
`max-height: ${Math.round(maxMenuHeight / scaleFactor)}px;`;
|
|
}
|
|
});
|
|
const id = Main.overview.connect('hiding', () => {
|
|
this._menu.close();
|
|
});
|
|
this._menu.actor.connect('destroy', () => {
|
|
Main.overview.disconnect(id);
|
|
});
|
|
|
|
this._menuManager.addMenu(this._menu);
|
|
}
|
|
|
|
this.emit('menu-state-changed', true);
|
|
|
|
this.set_hover(true);
|
|
this._menu.popup();
|
|
this._menuManager.ignoreRelease();
|
|
this.emit('sync-tooltip');
|
|
|
|
return false;
|
|
}
|
|
|
|
activate(button) {
|
|
const event = Clutter.get_current_event();
|
|
let modifiers = event ? event.get_state() : 0;
|
|
|
|
// Only consider SHIFT and CONTROL as modifiers (exclude SUPER, CAPS-LOCK, etc.)
|
|
modifiers &= Clutter.ModifierType.SHIFT_MASK | Clutter.ModifierType.CONTROL_MASK;
|
|
|
|
// We don't change the CTRL-click behaviour: in such case we just chain
|
|
// up the parent method and return.
|
|
if (modifiers & Clutter.ModifierType.CONTROL_MASK) {
|
|
// Keep default behaviour: launch new window
|
|
// By calling the parent method I make it compatible
|
|
// with other extensions tweaking ctrl + click
|
|
super.activate(button);
|
|
return;
|
|
}
|
|
|
|
// We check what type of click we have and if the modifier SHIFT is
|
|
// being used. We then define what buttonAction should be for this
|
|
// event.
|
|
let buttonAction = 0;
|
|
const {settings} = Docking.DockManager;
|
|
if (button && button === 2) {
|
|
if (modifiers & Clutter.ModifierType.SHIFT_MASK)
|
|
buttonAction = settings.shiftMiddleClickAction;
|
|
else
|
|
buttonAction = settings.middleClickAction;
|
|
} else if (button && button === 1) {
|
|
if (modifiers & Clutter.ModifierType.SHIFT_MASK)
|
|
buttonAction = settings.shiftClickAction;
|
|
else
|
|
buttonAction = settings.clickAction;
|
|
}
|
|
|
|
switch (buttonAction) {
|
|
case clickAction.FOCUS_OR_APP_SPREAD:
|
|
if (!Docking.DockManager.getDefault().appSpread.supported)
|
|
buttonAction = clickAction.FOCUS_OR_PREVIEWS;
|
|
break;
|
|
|
|
case clickAction.FOCUS_MINIMIZE_OR_APP_SPREAD:
|
|
if (!Docking.DockManager.getDefault().appSpread.supported)
|
|
buttonAction = clickAction.FOCUS_MINIMIZE_OR_PREVIEWS;
|
|
break;
|
|
}
|
|
|
|
// We check if the app is running, and that the # of windows is > 0 in
|
|
// case we use workspace isolation.
|
|
const windows = this.getInterestingWindows();
|
|
|
|
// Some action modes (e.g. MINIMIZE_OR_OVERVIEW) require overview to remain open
|
|
// This variable keeps track of this
|
|
let shouldHideOverview = true;
|
|
|
|
// We customize the action only when the application is already running
|
|
if (this.running) {
|
|
const hasUrgentWindows = !!this._urgentWindows.size;
|
|
const singleOrUrgentWindows = windows.length === 1 || hasUrgentWindows;
|
|
switch (buttonAction) {
|
|
case clickAction.MINIMIZE:
|
|
// In overview just activate the app, unless the acion is explicitely
|
|
// requested with a keyboard modifier
|
|
if (!Main.overview.visible || modifiers) {
|
|
// If we have button=2 or a modifier, allow minimization even if
|
|
// the app is not focused
|
|
if (this.focused && !hasUrgentWindows || button === 2 ||
|
|
modifiers & Clutter.ModifierType.SHIFT_MASK) {
|
|
// minimize all windows on double click and always in
|
|
// the case of primary click without additional modifiers
|
|
let clickCount = 0;
|
|
if (Clutter.EventType.CLUTTER_BUTTON_PRESS)
|
|
clickCount = event.get_click_count();
|
|
const allWindows = (button === 1 && !modifiers) || clickCount > 1;
|
|
this._minimizeWindow(allWindows);
|
|
} else {
|
|
this._activateAllWindows();
|
|
}
|
|
} else {
|
|
const [w] = windows;
|
|
Main.activateWindow(w);
|
|
}
|
|
break;
|
|
|
|
case clickAction.MINIMIZE_OR_OVERVIEW:
|
|
// When a single window is present, toggle minimization
|
|
// If only one windows is present toggle minimization, but
|
|
// only when triggered with the simple click action
|
|
// (no modifiers, no middle click).
|
|
if (singleOrUrgentWindows && !modifiers && button === 1) {
|
|
const [w] = windows;
|
|
if (this.focused) {
|
|
if (buttonAction !== clickAction.FOCUS_OR_APP_SPREAD) {
|
|
// Window is raised, minimize it
|
|
this._minimizeWindow(w);
|
|
}
|
|
} else {
|
|
// Window is minimized, raise it
|
|
Main.activateWindow(w);
|
|
}
|
|
// Launch overview when multiple windows are present
|
|
// TODO: only show current app windows when gnome shell API will allow it
|
|
} else {
|
|
shouldHideOverview = false;
|
|
Main.overview.toggle();
|
|
}
|
|
break;
|
|
|
|
case clickAction.CYCLE_WINDOWS:
|
|
if (!Main.overview.visible) {
|
|
if (this.focused && !hasUrgentWindows) {
|
|
this._cycleThroughWindows();
|
|
} else {
|
|
// Activate the first window
|
|
const [w] = windows;
|
|
Main.activateWindow(w);
|
|
}
|
|
} else {
|
|
this.app.activate();
|
|
}
|
|
break;
|
|
|
|
case clickAction.FOCUS_OR_PREVIEWS:
|
|
if (this.focused && !hasUrgentWindows &&
|
|
(windows.length > 1 || modifiers || button !== 1)) {
|
|
this._windowPreviews();
|
|
} else {
|
|
// Activate the first window
|
|
const [w] = windows;
|
|
Main.activateWindow(w);
|
|
}
|
|
break;
|
|
|
|
case clickAction.FOCUS_MINIMIZE_OR_PREVIEWS:
|
|
if (this.focused && !hasUrgentWindows) {
|
|
if (windows.length > 1 || modifiers || button !== 1)
|
|
this._windowPreviews();
|
|
else if (!Main.overview.visible)
|
|
this._minimizeWindow();
|
|
} else {
|
|
// Activate the first window
|
|
const [w] = windows;
|
|
Main.activateWindow(w);
|
|
}
|
|
break;
|
|
|
|
case clickAction.LAUNCH:
|
|
this.launchNewWindow();
|
|
break;
|
|
|
|
case clickAction.PREVIEWS:
|
|
if (!Main.overview.visible) {
|
|
// If only one windows is present just switch to it,
|
|
// but only when trigggered with the simple click action
|
|
// (no modifiers, no middle click).
|
|
if (singleOrUrgentWindows && !modifiers && button === 1) {
|
|
const [w] = windows;
|
|
Main.activateWindow(w);
|
|
} else {
|
|
this._windowPreviews();
|
|
}
|
|
} else {
|
|
this.app.activate();
|
|
}
|
|
break;
|
|
|
|
case clickAction.MINIMIZE_OR_PREVIEWS:
|
|
// When a single window is present, toggle minimization
|
|
// If only one windows is present toggle minimization, but only
|
|
// when trigggered with the imple click action (no modifiers,
|
|
// no middle click).
|
|
if (!Main.overview.visible) {
|
|
if (singleOrUrgentWindows && !modifiers && button === 1) {
|
|
const [w] = windows;
|
|
if (this.focused) {
|
|
// Window is raised, minimize it
|
|
this._minimizeWindow(w);
|
|
} else {
|
|
// Window is minimized, raise it
|
|
Main.activateWindow(w);
|
|
}
|
|
} else {
|
|
// Launch previews when multiple windows are present
|
|
this._windowPreviews();
|
|
}
|
|
} else {
|
|
this.app.activate();
|
|
}
|
|
break;
|
|
|
|
case clickAction.FOCUS_OR_APP_SPREAD:
|
|
if (this.focused && !singleOrUrgentWindows && !modifiers && button === 1) {
|
|
shouldHideOverview = false;
|
|
Docking.DockManager.getDefault().appSpread.toggle(this.app);
|
|
} else {
|
|
// Activate the first window
|
|
Main.activateWindow(windows[0]);
|
|
}
|
|
break;
|
|
|
|
case clickAction.FOCUS_MINIMIZE_OR_APP_SPREAD:
|
|
if (this.focused && !singleOrUrgentWindows && !modifiers && button === 1) {
|
|
shouldHideOverview = false;
|
|
Docking.DockManager.getDefault().appSpread.toggle(this.app);
|
|
} else if (!this.focused) {
|
|
// Activate the first window
|
|
Main.activateWindow(windows[0]);
|
|
} else {
|
|
this._minimizeWindow();
|
|
}
|
|
break;
|
|
|
|
case clickAction.QUIT:
|
|
this.closeAllWindows();
|
|
break;
|
|
|
|
case clickAction.SKIP:
|
|
Main.activateWindow(windows[0]);
|
|
break;
|
|
}
|
|
} else {
|
|
this.launchNewWindow();
|
|
}
|
|
|
|
// Hide overview except when action mode requires it
|
|
if (shouldHideOverview)
|
|
Main.overview.hide();
|
|
}
|
|
|
|
shouldShowTooltip() {
|
|
return this.hover && (!this._menu || !this._menu.isOpen) &&
|
|
(!this._previewMenu || !this._previewMenu.isOpen) &&
|
|
!Docking.DockManager.settings.hideTooltip;
|
|
}
|
|
|
|
_windowPreviews() {
|
|
if (!this._previewMenu) {
|
|
this._previewMenuManager = new PopupMenu.PopupMenuManager(this);
|
|
|
|
this._previewMenu = new WindowPreview.WindowPreviewMenu(this);
|
|
|
|
this._previewMenuManager.addMenu(this._previewMenu);
|
|
|
|
this._previewMenu.connect('open-state-changed', (menu, isPoppedUp) => {
|
|
if (!isPoppedUp)
|
|
this._onMenuPoppedDown();
|
|
});
|
|
const id = Main.overview.connect('hiding', () => {
|
|
this._previewMenu.close();
|
|
});
|
|
this._previewMenu.actor.connect('destroy', () => {
|
|
Main.overview.disconnect(id);
|
|
});
|
|
}
|
|
|
|
this.emit('menu-state-changed', !this._previewMenu.isOpen);
|
|
|
|
if (this._previewMenu.isOpen)
|
|
this._previewMenu.close();
|
|
else
|
|
this._previewMenu.popup();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Try to do the right thing when attempting to launch a new window of an app. In
|
|
// particular, if the application doens't allow to launch a new window, activate
|
|
// the existing window instead.
|
|
launchNewWindow() {
|
|
if (this.app.state === Shell.AppState.RUNNING &&
|
|
this.app.can_open_new_window()) {
|
|
this.animateLaunch();
|
|
this.app.open_new_window(-1);
|
|
} else {
|
|
// Try to manually activate the first window. Otherwise, when the
|
|
// app is activated by switching to a different workspace, a launch
|
|
// spinning icon is shown and disappers only after a timeout.
|
|
const windows = this.getWindows();
|
|
if (windows.length > 0) {
|
|
Main.activateWindow(windows[0]);
|
|
} else {
|
|
this.app.activate();
|
|
this.animateLaunch();
|
|
}
|
|
}
|
|
}
|
|
|
|
_numberOverlay() {
|
|
// Add label for a Hot-Key visual aid
|
|
this._numberOverlayLabel = new St.Label();
|
|
this._numberOverlayBin = new St.Bin({
|
|
child: this._numberOverlayLabel,
|
|
x_align: Clutter.ActorAlign.START,
|
|
y_align: Clutter.ActorAlign.START,
|
|
x_expand: true, y_expand: true,
|
|
});
|
|
this._numberOverlayLabel.add_style_class_name('number-overlay');
|
|
this._numberOverlayOrder = -1;
|
|
this._numberOverlayBin.hide();
|
|
|
|
this._iconContainer.add_child(this._numberOverlayBin);
|
|
}
|
|
|
|
updateNumberOverlay() {
|
|
// We apply an overall scale factor that might come from a HiDPI monitor.
|
|
// Clutter dimensions are in physical pixels, but CSS measures are in logical
|
|
// pixels, so make sure to consider the scale.
|
|
const scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
|
|
// Set the font size to something smaller than the whole icon so it is
|
|
// still visible. The border radius is large to make the shape circular
|
|
const [minWidth_, natWidth] = this._iconContainer.get_preferred_width(-1);
|
|
const fontSize = Math.round(Math.max(12, 0.3 * natWidth) / scaleFactor);
|
|
const size = Math.round(fontSize * 1.2);
|
|
this._numberOverlayLabel.set_style(
|
|
`font-size: ${fontSize}px;` +
|
|
`border-radius: ${this.icon.iconSize}px;` +
|
|
`width: ${size}px; height: ${size}px;`
|
|
);
|
|
}
|
|
|
|
setNumberOverlay(number) {
|
|
this._numberOverlayOrder = number;
|
|
this._numberOverlayLabel.set_text(number.toString());
|
|
}
|
|
|
|
toggleNumberOverlay(activate) {
|
|
if (activate && this._numberOverlayOrder > -1) {
|
|
this.updateNumberOverlay();
|
|
this._numberOverlayBin.show();
|
|
} else {
|
|
this._numberOverlayBin.hide();
|
|
}
|
|
}
|
|
|
|
_minimizeWindow(param) {
|
|
// Param true make all app windows minimize
|
|
const windows = this.getInterestingWindows();
|
|
const currentWorkspace = global.workspace_manager.get_active_workspace();
|
|
for (let i = 0; i < windows.length; i++) {
|
|
const w = windows[i];
|
|
if (w.get_workspace() === currentWorkspace && w.showing_on_its_workspace()) {
|
|
w.minimize();
|
|
// Just minimize one window. By specification it should be the
|
|
// focused window on the current workspace.
|
|
if (!param)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// By default only non minimized windows are activated.
|
|
// This activates all windows in the current workspace.
|
|
_activateAllWindows() {
|
|
// First activate first window so workspace is switched if needed.
|
|
// We don't do this if isolation is on!
|
|
if (!Docking.DockManager.settings.isolateWorkspaces &&
|
|
!Docking.DockManager.settings.isolateMonitors) {
|
|
if (!this.running)
|
|
this.animateLaunch();
|
|
this.app.activate();
|
|
}
|
|
|
|
// then activate all other app windows in the current workspace
|
|
const windows = this.getInterestingWindows();
|
|
const activeWorkspace = global.workspace_manager.get_active_workspace_index();
|
|
|
|
if (windows.length <= 0)
|
|
return;
|
|
|
|
for (let i = windows.length - 1; i >= 0; i--) {
|
|
if (windows[i].get_workspace()?.index() === activeWorkspace)
|
|
Main.activateWindow(windows[i]);
|
|
}
|
|
}
|
|
|
|
// This closes all windows of the app.
|
|
closeAllWindows() {
|
|
const windows = this.getInterestingWindows();
|
|
const time = global.get_current_time();
|
|
windows.forEach(w => w.delete(time));
|
|
}
|
|
|
|
_cycleThroughWindows(reversed) {
|
|
// Store for a little amount of time last clicked app and its windows
|
|
// since the order changes upon window interaction
|
|
const MEMORY_TIME = 3000;
|
|
|
|
const appWindows = this.getInterestingWindows();
|
|
|
|
if (appWindows.length < 1)
|
|
return;
|
|
|
|
if (recentlyClickedAppLoopId > 0)
|
|
GLib.source_remove(recentlyClickedAppLoopId);
|
|
recentlyClickedAppLoopId = GLib.timeout_add(
|
|
GLib.PRIORITY_DEFAULT, MEMORY_TIME, this._resetRecentlyClickedApp);
|
|
|
|
// If there isn't already a list of windows for the current app,
|
|
// or the stored list is outdated, use the current windows list.
|
|
const monitorIsolation = Docking.DockManager.settings.isolateMonitors;
|
|
if (!recentlyClickedApp ||
|
|
recentlyClickedApp.get_id() !== this.app.get_id() ||
|
|
recentlyClickedAppWindows.length !== appWindows.length ||
|
|
(recentlyClickedAppMonitor !== this.monitorIndex && monitorIsolation)) {
|
|
recentlyClickedApp = this.app;
|
|
recentlyClickedAppWindows = appWindows;
|
|
recentlyClickedAppMonitor = this.monitorIndex;
|
|
recentlyClickedAppIndex = 0;
|
|
}
|
|
|
|
if (reversed) {
|
|
recentlyClickedAppIndex--;
|
|
if (recentlyClickedAppIndex < 0)
|
|
recentlyClickedAppIndex = recentlyClickedAppWindows.length - 1;
|
|
} else {
|
|
recentlyClickedAppIndex++;
|
|
}
|
|
const index = recentlyClickedAppIndex % recentlyClickedAppWindows.length;
|
|
const window = recentlyClickedAppWindows[index];
|
|
|
|
Main.activateWindow(window);
|
|
}
|
|
|
|
_resetRecentlyClickedApp() {
|
|
if (recentlyClickedAppLoopId > 0)
|
|
GLib.source_remove(recentlyClickedAppLoopId);
|
|
recentlyClickedAppLoopId = 0;
|
|
recentlyClickedApp = null;
|
|
recentlyClickedAppWindows = null;
|
|
recentlyClickedAppIndex = 0;
|
|
recentlyClickedAppMonitor = -1;
|
|
|
|
return false;
|
|
}
|
|
|
|
getWindows() {
|
|
return this.app.get_windows();
|
|
}
|
|
|
|
// Filter out unnecessary windows, for instance
|
|
// nautilus desktop window.
|
|
getInterestingWindows() {
|
|
const interestingWindows = getInterestingWindows(this.getWindows(),
|
|
this.monitorIndex);
|
|
|
|
if (!this._urgentWindows.size)
|
|
return interestingWindows;
|
|
|
|
return [...new Set([...this._urgentWindows, ...interestingWindows])];
|
|
}
|
|
});
|
|
|
|
const DockAppIcon = GObject.registerClass({
|
|
}, class DockAppIcon extends DockAbstractAppIcon {
|
|
_init(app, monitorIndex, iconAnimator) {
|
|
super._init(app, monitorIndex, iconAnimator);
|
|
|
|
this._signalsHandler.add(tracker, 'notify::focus-app', () => this._updateFocusState());
|
|
}
|
|
});
|
|
|
|
const DockLocationAppIcon = GObject.registerClass({
|
|
}, class DockLocationAppIcon extends DockAbstractAppIcon {
|
|
_init(app, monitorIndex, iconAnimator) {
|
|
if (!(app.appInfo instanceof Locations.LocationAppInfo))
|
|
throw new Error('Provided application %s is not a Location'.format(app));
|
|
|
|
super._init(app, monitorIndex, iconAnimator);
|
|
|
|
if (Docking.DockManager.settings.isolateLocations) {
|
|
this._signalsHandler.add(tracker, 'notify::focus-app', () => this._updateFocusState());
|
|
} else {
|
|
this._signalsHandler.add(global.display, 'notify::focus-window',
|
|
() => this._updateFocusState());
|
|
}
|
|
|
|
this._signalsHandler.add(this.app, 'notify::icon', () => this.icon.update());
|
|
}
|
|
|
|
get location() {
|
|
return this.app.location;
|
|
}
|
|
|
|
_updateFocusState() {
|
|
if (Docking.DockManager.settings.isolateLocations) {
|
|
super._updateFocusState();
|
|
return;
|
|
}
|
|
|
|
this.focused = this.app.isFocused && this.running;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @param app
|
|
* @param monitorIndex
|
|
* @param iconAnimator
|
|
*/
|
|
export function makeAppIcon(app, monitorIndex, iconAnimator) {
|
|
if (app.appInfo instanceof Locations.LocationAppInfo)
|
|
return new DockLocationAppIcon(app, monitorIndex, iconAnimator);
|
|
|
|
return new DockAppIcon(app, monitorIndex, iconAnimator);
|
|
}
|
|
|
|
/**
|
|
* DockAppIconMenu
|
|
*
|
|
* - set popup arrow side based on dash orientation
|
|
* - Add close windows option based on quitfromdash extension
|
|
* (https://github.com/deuill/shell-extension-quitfromdash)
|
|
* - Add open windows thumbnails instead of list
|
|
* - update menu when application windows change
|
|
*/
|
|
const DockAppIconMenu = class DockAppIconMenu extends PopupMenu.PopupMenu {
|
|
constructor(source) {
|
|
super(source, 0.5, Utils.getPosition());
|
|
|
|
this._signalsHandler = new Utils.GlobalSignalsHandler(this);
|
|
|
|
// We want to keep the item hovered while the menu is up
|
|
this.blockSourceEvents = true;
|
|
|
|
this._source = source;
|
|
this._parentalControlsManager = ParentalControlsManager.getDefault();
|
|
|
|
this.actor.add_style_class_name('app-menu');
|
|
this.actor.add_style_class_name('dock-app-menu');
|
|
|
|
// Chain our visibility and lifecycle to that of the source
|
|
this._signalsHandler.add(source, 'notify::mapped', () => {
|
|
if (!source.mapped)
|
|
this.close();
|
|
});
|
|
source.connect('destroy', () => this.destroy());
|
|
|
|
Main.uiGroup.add_child(this.actor);
|
|
|
|
const {remoteModel} = Docking.DockManager.getDefault();
|
|
const remoteModelApp = remoteModel?.lookupById(this._source?.app?.id);
|
|
if (remoteModelApp && DBusMenu) {
|
|
const [onQuicklist, onDynamicSection] = Utils.splitHandler((sender,
|
|
{quicklist}, dynamicSection) => {
|
|
dynamicSection.removeAll();
|
|
if (quicklist) {
|
|
quicklist.get_children().forEach(remoteItem =>
|
|
dynamicSection.addMenuItem(
|
|
DBusMenuUtils.makePopupMenuItem(remoteItem, false)));
|
|
}
|
|
});
|
|
|
|
this._signalsHandler.add([
|
|
remoteModelApp,
|
|
'quicklist-changed',
|
|
onQuicklist,
|
|
], [
|
|
this,
|
|
'dynamic-section-changed',
|
|
onDynamicSection,
|
|
]);
|
|
}
|
|
}
|
|
|
|
_appendSeparator() {
|
|
this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
|
}
|
|
|
|
_appendMenuItem(labelText) {
|
|
const item = new PopupMenu.PopupMenuItem(labelText);
|
|
this.addMenuItem(item);
|
|
return item;
|
|
}
|
|
|
|
popup(_activatingButton) {
|
|
this._rebuildMenu();
|
|
this.open(BoxPointer.PopupAnimation.FULL);
|
|
}
|
|
|
|
_rebuildMenu() {
|
|
this.removeAll();
|
|
|
|
if (Docking.DockManager.settings.showWindowsPreview) {
|
|
// Display the app windows menu items and the separator between windows
|
|
// of the current desktop and other windows.
|
|
const windows = this._source.getInterestingWindows();
|
|
|
|
this._allWindowsMenuItem = new PopupMenu.PopupSubMenuMenuItem(__('All Windows'), false);
|
|
if (this._allWindowsMenuItem.menu?.actor)
|
|
this._allWindowsMenuItem.menu.actor.overlayScrollbars = true;
|
|
this._allWindowsMenuItem.hide();
|
|
if (windows.length > 0)
|
|
this.addMenuItem(this._allWindowsMenuItem);
|
|
} else {
|
|
const windows = this._source.getInterestingWindows();
|
|
|
|
if (windows.length > 0) {
|
|
this.addMenuItem(
|
|
/* Translators: This is the heading of a list of open windows */
|
|
new PopupMenu.PopupSeparatorMenuItem(_('Open Windows')));
|
|
}
|
|
|
|
windows.forEach(window => {
|
|
const title = window.title
|
|
? window.title : this._source.app.get_name();
|
|
const item = this._appendMenuItem(title);
|
|
item.connect('activate', () => {
|
|
this.emit('activate-window', window);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!this._source.app.is_window_backed()) {
|
|
this._appendSeparator();
|
|
|
|
const appInfo = this._source.app.get_app_info();
|
|
const actions = appInfo.list_actions();
|
|
if (this._source.app.can_open_new_window() &&
|
|
actions.indexOf('new-window') === -1) {
|
|
this._newWindowMenuItem = this._appendMenuItem(_('New Window'));
|
|
this._newWindowMenuItem.connect('activate', () => {
|
|
if (this._source.app.state === Shell.AppState.STOPPED)
|
|
this._source.animateLaunch();
|
|
|
|
this._source.app.open_new_window(-1);
|
|
this.emit('activate-window', null);
|
|
});
|
|
this._appendSeparator();
|
|
}
|
|
|
|
if (Docking.DockManager.getDefault().discreteGpuAvailable &&
|
|
this._source.app.state === Shell.AppState.STOPPED) {
|
|
const appPrefersNonDefaultGPU = appInfo.get_boolean('PrefersNonDefaultGPU');
|
|
const gpuPref = appPrefersNonDefaultGPU
|
|
? Shell.AppLaunchGpu.DEFAULT
|
|
: Shell.AppLaunchGpu.DISCRETE;
|
|
this._onGpuMenuItem = this._appendMenuItem(appPrefersNonDefaultGPU
|
|
? _('Launch using Integrated Graphics Card')
|
|
: _('Launch using Discrete Graphics Card'));
|
|
this._onGpuMenuItem.connect('activate', () => {
|
|
this._source.animateLaunch();
|
|
this._source.app.launch(0, -1, gpuPref);
|
|
this.emit('activate-window', null);
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < actions.length; i++) {
|
|
const action = actions[i];
|
|
const item = this._appendMenuItem(appInfo.get_action_name(action));
|
|
item.sensitive = !appInfo.busy;
|
|
item.connect('activate', (emitter, event) => {
|
|
this._source.app.launch_action(action, event.get_time(), -1);
|
|
this.emit('activate-window', null);
|
|
});
|
|
}
|
|
|
|
const canFavorite = global.settings.is_writable('favorite-apps') &&
|
|
(this._source instanceof DockAppIcon) &&
|
|
this._parentalControlsManager.shouldShowApp(this._source.app.app_info);
|
|
|
|
if (canFavorite) {
|
|
this._appendSeparator();
|
|
|
|
const isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id());
|
|
const [majorVersion] = Config.PACKAGE_VERSION.split('.');
|
|
|
|
if (isFavorite) {
|
|
const label = majorVersion >= 42 ? _('Unpin')
|
|
: _('Remove from Favorites');
|
|
const item = this._appendMenuItem(label);
|
|
item.connect('activate', () => {
|
|
const favs = AppFavorites.getAppFavorites();
|
|
favs.removeFavorite(this._source.app.get_id());
|
|
});
|
|
} else {
|
|
const label = majorVersion >= 42 ? _('Pin to Dash')
|
|
: _('Add to Favorites');
|
|
const item = this._appendMenuItem(label);
|
|
item.connect('activate', () => {
|
|
const favs = AppFavorites.getAppFavorites();
|
|
favs.addFavorite(this._source.app.get_id());
|
|
});
|
|
}
|
|
}
|
|
|
|
if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop') &&
|
|
(this._source instanceof DockAppIcon)) {
|
|
this._appendSeparator();
|
|
const item = this._appendMenuItem(_('App Details'));
|
|
item.connect('activate', () => {
|
|
const id = this._source.app.get_id();
|
|
const args = GLib.Variant.new('(ss)', [id, '']);
|
|
Gio.DBus.get(Gio.BusType.SESSION, null,
|
|
(o, res) => {
|
|
const bus = Gio.DBus.get_finish(res);
|
|
bus.call('org.gnome.Software',
|
|
'/org/gnome/Software',
|
|
'org.gtk.Actions', 'Activate',
|
|
GLib.Variant.new('(sava{sv})',
|
|
['details', [args], null]),
|
|
null, 0, -1, null, null);
|
|
Main.overview.hide();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// dynamic menu
|
|
const items = this._getMenuItems();
|
|
let i = items.length;
|
|
if (Shell.AppSystem.get_default().lookup_app('org.gnome.Software.desktop'))
|
|
i -= 2;
|
|
|
|
if (global.settings.is_writable('favorite-apps'))
|
|
i -= 2;
|
|
|
|
if (i < 0)
|
|
i = 0;
|
|
|
|
const dynamicSection = new PopupMenu.PopupMenuSection();
|
|
this.addMenuItem(dynamicSection, i);
|
|
this.emit('dynamic-section-changed', dynamicSection);
|
|
|
|
// quit menu
|
|
this._appendSeparator();
|
|
this._quitMenuItem = this._appendMenuItem(_('Quit'));
|
|
this._quitMenuItem.connect('activate', () => this._source.closeAllWindows());
|
|
|
|
this.update();
|
|
}
|
|
|
|
// update menu content when application windows change. This is desirable as actions
|
|
// acting on windows (closing) are performed while the menu is shown.
|
|
update() {
|
|
// update, show or hide the quit menu
|
|
if (this._source.windowsCount > 0) {
|
|
if (this._source.windowsCount === 1) {
|
|
this._quitMenuItem.label.set_text(_('Quit'));
|
|
} else {
|
|
this._quitMenuItem.label.set_text(ngettext(
|
|
'Quit %d Window', 'Quit %d Windows', this._source.windowsCount).format(
|
|
this._source.windowsCount));
|
|
}
|
|
|
|
this._quitMenuItem.actor.show();
|
|
} else {
|
|
this._quitMenuItem.actor.hide();
|
|
}
|
|
|
|
if (Docking.DockManager.settings.showWindowsPreview) {
|
|
const windows = this._source.getInterestingWindows();
|
|
|
|
// update, show, or hide the allWindows menu
|
|
// Check if there are new windows not already displayed. In such case,
|
|
// repopulate the allWindows menu. Windows removal is already handled
|
|
// by each preview being connected to the destroy signal
|
|
const oldWindows = this._allWindowsMenuItem.menu._getMenuItems().map(item => {
|
|
return item._window;
|
|
});
|
|
|
|
const newWindows = windows.filter(w =>
|
|
oldWindows.indexOf(w) < 0);
|
|
if (newWindows.length > 0) {
|
|
this._populateAllWindowMenu(windows);
|
|
|
|
// Try to set the width to that of the submenu.
|
|
// TODO: can't get the actual size, getting a bit less.
|
|
// Temporary workaround: add 15px to compensate
|
|
this._allWindowsMenuItem.width = this._allWindowsMenuItem.menu.actor.width + 15;
|
|
}
|
|
|
|
// The menu is created hidden and never hidded after being shown.
|
|
// Instead, a signal connected to its items destroy will set is
|
|
// insensitive if no more windows preview are shown.
|
|
if (windows.length > 0) {
|
|
this._allWindowsMenuItem.show();
|
|
this._allWindowsMenuItem.setSensitive(true);
|
|
|
|
if (Docking.DockManager.settings.defaultWindowsPreviewToOpen)
|
|
this._allWindowsMenuItem.menu.open();
|
|
}
|
|
}
|
|
|
|
// Update separators
|
|
this._getMenuItems().forEach(item => {
|
|
if ('label' in item)
|
|
this._updateSeparatorVisibility(item);
|
|
});
|
|
}
|
|
|
|
_populateAllWindowMenu(windows) {
|
|
this._allWindowsMenuItem.menu.removeAll();
|
|
|
|
if (windows.length > 0) {
|
|
const activeWorkspace = global.workspace_manager.get_active_workspace();
|
|
let separatorShown = windows[0].get_workspace() !== activeWorkspace;
|
|
|
|
for (let i = 0; i < windows.length; i++) {
|
|
const window = windows[i];
|
|
if (!separatorShown && window.get_workspace() !== activeWorkspace) {
|
|
this._allWindowsMenuItem.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
|
separatorShown = true;
|
|
}
|
|
|
|
const item = new WindowPreview.WindowPreviewMenuItem(window,
|
|
St.Side.LEFT);
|
|
this._allWindowsMenuItem.menu.addMenuItem(item);
|
|
item.connect('activate', () => {
|
|
this.emit('activate-window', window);
|
|
});
|
|
|
|
// This is to achieve a more gracefull transition when the last windows is closed.
|
|
item.connect('destroy', () => {
|
|
// It's still counting the item just going to be destroyed
|
|
if (this._allWindowsMenuItem.menu._getMenuItems().length === 1)
|
|
this._allWindowsMenuItem.setSensitive(false);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param w
|
|
*/
|
|
function isWindowUrgent(w) {
|
|
return w.urgent || w.demandsAttention || w._manualUrgency;
|
|
}
|
|
|
|
/**
|
|
* Filter out unnecessary windows, for instance
|
|
* nautilus desktop window.
|
|
*
|
|
* @param windows
|
|
* @param monitorIndex
|
|
*/
|
|
export function getInterestingWindows(windows, monitorIndex) {
|
|
const {settings} = Docking.DockManager;
|
|
|
|
// When using workspace isolation, we filter out windows
|
|
// that are neither in the current workspace nor marked urgent
|
|
if (settings.isolateWorkspaces) {
|
|
const showUrgent = settings.workspaceAgnosticUrgentWindows;
|
|
const activeWorkspace = global.workspace_manager.get_active_workspace();
|
|
windows = windows.filter(w => {
|
|
const inWorkspace = w.get_workspace() === activeWorkspace;
|
|
return inWorkspace || (showUrgent && isWindowUrgent(w));
|
|
});
|
|
}
|
|
|
|
if (settings.isolateMonitors && monitorIndex >= 0) {
|
|
windows = windows.filter(w => {
|
|
return w.get_monitor() === monitorIndex;
|
|
});
|
|
}
|
|
|
|
return windows.filter(w => !w.skipTaskbar);
|
|
}
|
|
|
|
/**
|
|
* A ShowAppsIcon improved class.
|
|
*
|
|
* - set label position based on dash orientation
|
|
* Note: we are am reusing most machinery of the appIcon class.
|
|
* - implement a popupMenu based on the AppIcon code
|
|
* Note: we are reusing most machinery of the appIcon class)
|
|
*
|
|
*/
|
|
|
|
export const DockShowAppsIcon = GObject.registerClass({
|
|
Signals: {
|
|
'menu-state-changed': {param_types: [GObject.TYPE_BOOLEAN]},
|
|
'sync-tooltip': {},
|
|
},
|
|
}
|
|
, class DockShowAppsIcon extends Dash.ShowAppsIcon {
|
|
_init(position) {
|
|
super._init();
|
|
|
|
// Re-use appIcon methods
|
|
const {prototype: appIconPrototype} = AppDisplay.AppIcon;
|
|
this.toggleButton.y_expand = false;
|
|
this.toggleButton.connect('popup-menu', () =>
|
|
appIconPrototype._onKeyboardPopupMenu.call(this));
|
|
this.toggleButton.connect('clicked', () =>
|
|
this._removeMenuTimeout());
|
|
|
|
this.reactive = true;
|
|
this.toggleButton.popupMenu = (...args) =>
|
|
this.popupMenu(...args);
|
|
this.toggleButton._setPopupTimeout = (...args) =>
|
|
this._setPopupTimeout(...args);
|
|
this.toggleButton._removeMenuTimeout = (...args) =>
|
|
this._removeMenuTimeout(...args);
|
|
|
|
this.label?.add_style_class_name(Theming.PositionStyleClass[position]);
|
|
if (Docking.DockManager.settings.customThemeShrink)
|
|
this.label?.add_style_class_name('shrink');
|
|
|
|
this._menu = null;
|
|
this._menuManager = new PopupMenu.PopupMenuManager(this);
|
|
this._menuTimeoutId = 0;
|
|
}
|
|
|
|
_createIcon(size) {
|
|
this._iconActor = super._createIcon(size);
|
|
this._iconActor.fallbackIconName = this._iconActor.iconName;
|
|
this._iconActor.fallbackGicon = this._iconActor.gicon;
|
|
this._iconActor.iconName = `view-app-grid-${Main.sessionMode.currentMode}-symbolic`;
|
|
return this._iconActor;
|
|
}
|
|
|
|
vfunc_leave_event(...args) {
|
|
return AppDisplay.AppIcon.prototype.vfunc_leave_event.call(
|
|
this.toggleButton, ...args);
|
|
}
|
|
|
|
vfunc_button_press_event(...args) {
|
|
return AppDisplay.AppIcon.prototype.vfunc_button_press_event.call(
|
|
this.toggleButton, ...args);
|
|
}
|
|
|
|
vfunc_touch_event(...args) {
|
|
return AppDisplay.AppIcon.prototype.vfunc_touch_event.call(
|
|
this.toggleButton, ...args);
|
|
}
|
|
|
|
showLabel(...args) {
|
|
itemShowLabel.call(this, ...args);
|
|
}
|
|
|
|
setForcedHighlight(...args) {
|
|
AppDisplay.AppIcon.prototype.setForcedHighlight.call(this, ...args);
|
|
}
|
|
|
|
_onMenuPoppedDown(...args) {
|
|
AppDisplay.AppIcon.prototype._onMenuPoppedDown.call(this, ...args);
|
|
}
|
|
|
|
_setPopupTimeout(...args) {
|
|
AppDisplay.AppIcon.prototype._setPopupTimeout.call(this, ...args);
|
|
}
|
|
|
|
_removeMenuTimeout(...args) {
|
|
AppDisplay.AppIcon.prototype._removeMenuTimeout.call(this, ...args);
|
|
}
|
|
|
|
popupMenu() {
|
|
this._removeMenuTimeout();
|
|
this.toggleButton.fake_release();
|
|
|
|
if (!this._menu) {
|
|
this._menu = new DockShowAppsIconMenu(this);
|
|
this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
|
|
if (!isPoppedUp)
|
|
this._onMenuPoppedDown();
|
|
});
|
|
const id = Main.overview.connect('hiding', () => {
|
|
this._menu.close();
|
|
});
|
|
this._menu.actor.connect('destroy', () => {
|
|
Main.overview.disconnect(id);
|
|
});
|
|
this._menuManager.addMenu(this._menu);
|
|
}
|
|
|
|
this.emit('menu-state-changed', true);
|
|
|
|
this.toggleButton.set_hover(true);
|
|
this._menu.popup();
|
|
this._menuManager.ignoreRelease();
|
|
this.emit('sync-tooltip');
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* A menu for the showAppsIcon
|
|
*/
|
|
class DockShowAppsIconMenu extends DockAppIconMenu {
|
|
_rebuildMenu() {
|
|
this.removeAll();
|
|
|
|
/* Translators: %s is "Settings", which is automatically translated. You
|
|
can also translate the full message if this fits better your language. */
|
|
const name = __('Dash to Dock %s').format(_('Settings'));
|
|
const item = this._appendMenuItem(name);
|
|
|
|
item.connect('activate', () =>
|
|
Docking.DockManager.extension.openPreferences());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function is used for both DockShowAppsIcon and DockDashItemContainer
|
|
*/
|
|
export function itemShowLabel() {
|
|
/* eslint-disable no-invalid-this */
|
|
// Check if the label is still present at all. When switching workpaces, the
|
|
// item might have been destroyed in between.
|
|
if (!this._labelText || !this.label.get_stage())
|
|
return;
|
|
|
|
this.label.set_text(this._labelText);
|
|
this.label.opacity = 0;
|
|
this.label.show();
|
|
|
|
const [stageX, stageY] = this.get_transformed_position();
|
|
const node = this.label.get_theme_node();
|
|
|
|
const itemWidth = this.allocation.x2 - this.allocation.x1;
|
|
const itemHeight = this.allocation.y2 - this.allocation.y1;
|
|
|
|
const labelWidth = this.label.get_width();
|
|
const labelHeight = this.label.get_height();
|
|
|
|
let x, y, xOffset, yOffset;
|
|
|
|
const position = Utils.getPosition();
|
|
const labelOffset = node.get_length('-x-offset');
|
|
|
|
switch (position) {
|
|
case St.Side.LEFT:
|
|
yOffset = Math.floor((itemHeight - labelHeight) / 2);
|
|
y = stageY + yOffset;
|
|
xOffset = labelOffset;
|
|
x = stageX + this.get_width() + xOffset;
|
|
break;
|
|
case St.Side.RIGHT:
|
|
yOffset = Math.floor((itemHeight - labelHeight) / 2);
|
|
y = stageY + yOffset;
|
|
xOffset = labelOffset;
|
|
x = Math.round(stageX) - labelWidth - xOffset;
|
|
break;
|
|
case St.Side.TOP:
|
|
y = stageY + labelOffset + itemHeight;
|
|
xOffset = Math.floor((itemWidth - labelWidth) / 2);
|
|
x = stageX + xOffset;
|
|
break;
|
|
case St.Side.BOTTOM:
|
|
yOffset = labelOffset;
|
|
y = stageY - labelHeight - yOffset;
|
|
xOffset = Math.floor((itemWidth - labelWidth) / 2);
|
|
x = stageX + xOffset;
|
|
break;
|
|
}
|
|
|
|
// keep the label inside the screen border
|
|
// Only needed fot the x coordinate.
|
|
|
|
// Leave a few pixel gap
|
|
const gap = 5;
|
|
const monitor = Main.layoutManager.findMonitorForActor(this);
|
|
if (x - monitor.x < gap)
|
|
x += monitor.x - x + labelOffset;
|
|
else if (x + labelWidth > monitor.x + monitor.width - gap)
|
|
x -= x + labelWidth - (monitor.x + monitor.width) + gap;
|
|
|
|
this.label.remove_all_transitions();
|
|
this.label.set_position(x, y);
|
|
this.label.ease({
|
|
opacity: 255,
|
|
duration: Dash.DASH_ITEM_LABEL_SHOW_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
});
|
|
/* eslint-enable no-invalid-this */
|
|
}
|