Files
gnome-shell-extensions/extensions/window-list/extension.js
T
Florian Müllner 62ccff1558 window-list: Untrack chrome while in overview
The window list is set up to track the monitor's fullscreen state.
Monitors are never considered in fullscreen while showing the overview,
so if shell's layout manager updates the fullscreen visibility after
we hid the bottom bar, it ends up being visible in the overview.

To avoid this, untrack the actor while the overview is visible.

Closes: https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/issues/509
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/364>
(cherry picked from commit 981e8e42e2)
2025-01-10 14:06:25 +01:00

1201 lines
38 KiB
JavaScript

// SPDX-FileCopyrightText: 2012 Florian Müllner <fmuellner@gnome.org>
// SPDX-FileCopyrightText: 2013 Giovanni Campagna <gcampagna@src.gnome.org>
// SPDX-FileCopyrightText: 2014 Sylvain Pasche <sylvain.pasche@gmail.com>
//
// SPDX-License-Identifier: GPL-2.0-or-later
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk';
import Meta from 'gi://Meta';
import Mtk from 'gi://Mtk';
import Shell from 'gi://Shell';
import St from 'gi://St';
import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
import * as DND from 'resource:///org/gnome/shell/ui/dnd.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import {WorkspaceIndicator} from './workspaceIndicator.js';
const ICON_TEXTURE_SIZE = 24;
const DND_ACTIVATE_TIMEOUT = 500;
const TOOLTIP_OFFSET = 6;
const TOOLTIP_ANIMATION_TIME = 150;
const GroupingMode = {
NEVER: 0,
AUTO: 1,
ALWAYS: 2,
};
/**
* @param {Shell.App} app - an app
* @returns {number} - the smallest stable sequence of the app's windows
*/
function _getAppStableSequence(app) {
let windows = app.get_windows().filter(w => !w.skip_taskbar);
return windows.reduce((prev, cur) => {
return Math.min(prev, cur.get_stable_sequence());
}, Infinity);
}
class WindowContextMenu extends PopupMenu.PopupMenu {
constructor(source, metaWindow) {
super(source, 0.5, St.Side.BOTTOM);
this._metaWindow = metaWindow;
this._minimizeItem = new PopupMenu.PopupMenuItem('');
this._minimizeItem.connect('activate', () => {
if (this._metaWindow.minimized)
this._metaWindow.unminimize();
else
this._metaWindow.minimize();
});
this.addMenuItem(this._minimizeItem);
this._maximizeItem = new PopupMenu.PopupMenuItem('');
this._maximizeItem.connect('activate', () => {
if (this._metaWindow.get_maximized() === Meta.MaximizeFlags.BOTH)
this._metaWindow.unmaximize(Meta.MaximizeFlags.BOTH);
else
this._metaWindow.maximize(Meta.MaximizeFlags.BOTH);
});
this.addMenuItem(this._maximizeItem);
this._closeItem = new PopupMenu.PopupMenuItem(_('Close'));
this._closeItem.connect('activate', () => {
this._metaWindow.delete(global.get_current_time());
});
this.addMenuItem(this._closeItem);
this._metaWindow.connectObject(
'notify::minimized', this._updateMinimizeItem.bind(this),
'notify::maximized-horizontally', this._updateMaximizeItem.bind(this),
'notify::maximized-vertically', this._updateMaximizeItem.bind(this),
this.actor);
this._updateMinimizeItem();
this._updateMaximizeItem();
this.connect('open-state-changed', () => {
if (!this.isOpen)
return;
this._minimizeItem.setSensitive(this._metaWindow.can_minimize());
this._maximizeItem.setSensitive(this._metaWindow.can_maximize());
this._closeItem.setSensitive(this._metaWindow.can_close());
});
}
_updateMinimizeItem() {
this._minimizeItem.label.text = this._metaWindow.minimized
? _('Unminimize') : _('Minimize');
}
_updateMaximizeItem() {
let maximized = this._metaWindow.maximized_vertically &&
this._metaWindow.maximized_horizontally;
this._maximizeItem.label.text = maximized
? _('Unmaximize') : _('Maximize');
}
}
class WindowTitle extends St.BoxLayout {
static {
GObject.registerClass(this);
}
constructor(metaWindow) {
super({
style_class: 'window-button-box',
x_expand: true,
y_expand: true,
});
this._metaWindow = metaWindow;
this._icon = new St.Bin({style_class: 'window-button-icon'});
this.add_child(this._icon);
this.label_actor = new St.Label({y_align: Clutter.ActorAlign.CENTER});
this.label_actor.clutter_text.single_line_mode = true;
this.add_child(this.label_actor);
this._textureCache = St.TextureCache.get_default();
this._textureCache.connectObject('icon-theme-changed',
() => this._updateIcon(), this);
this._metaWindow.connectObject(
'notify::wm-class',
() => this._updateIcon(), GObject.ConnectFlags.AFTER,
'notify::gtk-application-id',
() => this._updateIcon(), GObject.ConnectFlags.AFTER,
'notify::title', () => this._updateTitle(),
'notify::minimized', () => this._minimizedChanged(),
this);
this._updateIcon();
this._minimizedChanged();
}
_minimizedChanged() {
this._icon.opacity = this._metaWindow.minimized ? 128 : 255;
this._updateTitle();
}
_updateTitle() {
if (!this._metaWindow.title)
return;
if (this._metaWindow.minimized)
this.label_actor.text = '[%s]'.format(this._metaWindow.title);
else
this.label_actor.text = this._metaWindow.title;
}
_updateIcon() {
let app = Shell.WindowTracker.get_default().get_window_app(this._metaWindow);
if (app) {
this._icon.child = app.create_icon_texture(ICON_TEXTURE_SIZE);
} else {
this._icon.child = new St.Icon({
icon_name: 'icon-missing',
icon_size: ICON_TEXTURE_SIZE,
});
}
}
}
class BaseButton extends St.Button {
static {
GObject.registerClass({
GTypeFlags: GObject.TypeFlags.ABSTRACT,
Properties: {
'ignore-workspace': GObject.ParamSpec.boolean(
'ignore-workspace', 'ignore-workspace', 'ignore-workspace',
GObject.ParamFlags.READWRITE,
false),
},
}, this);
}
constructor(perMonitor, monitorIndex) {
super({
style_class: 'window-button',
can_focus: true,
x_expand: true,
button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
});
this._perMonitor = perMonitor;
this._monitorIndex = monitorIndex;
this._ignoreWorkspace = false;
this.connect('notify::allocation',
this._updateIconGeometry.bind(this));
this.connect('clicked', this._onClicked.bind(this));
this.connect('destroy', this._onDestroy.bind(this));
this.connect('popup-menu', this._onPopupMenu.bind(this));
this._contextMenuManager = new PopupMenu.PopupMenuManager(this);
global.window_manager.connectObject('switch-workspace',
() => this._updateVisibility(), this);
if (this._perMonitor) {
global.display.connectObject(
'window-entered-monitor',
this._windowEnteredOrLeftMonitor.bind(this),
'window-left-monitor',
this._windowEnteredOrLeftMonitor.bind(this),
this);
}
this._tooltip = new Tooltip(this, {
style_class: 'dash-label',
visible: false,
});
Main.uiGroup.add_child(this._tooltip);
}
get active() {
return this.has_style_class_name('focused');
}
// eslint-disable-next-line camelcase
get ignore_workspace() {
return this._ignoreWorkspace;
}
// eslint-disable-next-line camelcase
set ignore_workspace(ignore) {
if (this._ignoreWorkspace === ignore)
return;
this._ignoreWorkspace = ignore;
this.notify('ignore-workspace');
this._updateVisibility();
}
_setLongPressTimeout() {
if (this._longPressTimeoutId)
return;
const {longPressDuration} = Clutter.Settings.get_default();
this._longPressTimeoutId =
GLib.timeout_add(GLib.PRIORITY_DEFAULT, longPressDuration, () => {
delete this._longPressTimeoutId;
if (this._canOpenPopupMenu() && !this._contextMenu.isOpen)
this._openMenu(this._contextMenu);
return GLib.SOURCE_REMOVE;
});
}
_removeLongPressTimeout() {
if (!this._longPressTimeoutId)
return;
GLib.source_remove(this._longPressTimeoutId);
delete this._longPressTimeoutId;
}
vfunc_button_press_event(event) {
if (event.get_button() === 1)
this._setLongPressTimeout();
return super.vfunc_button_press_event(event);
}
vfunc_button_release_event(event) {
this._removeLongPressTimeout();
return super.vfunc_button_release_event(event);
}
vfunc_touch_event(event) {
if (event.type() === Clutter.EventType.TOUCH_BEGIN)
this._setLongPressTimeout();
else if (event.type() === Clutter.EventType.TOUCH_END)
this._removeLongPressTimeout();
return super.vfunc_touch_event(event);
}
activate() {
if (this.active)
return;
this._onClicked(this, 1);
}
_onClicked(_actor, _button) {
throw new GObject.NotImplementedError(
`_onClicked in ${this.constructor.name}`);
}
_canOpenPopupMenu() {
return true;
}
_openMenu(menu) {
menu.open();
let event = Clutter.get_current_event();
if (event && event.type() === Clutter.EventType.KEY_RELEASE)
menu.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
}
_minimizeOrActivateWindow(window) {
let focusWindow = global.display.focus_window;
if (focusWindow === window ||
focusWindow && focusWindow.get_transient_for() === window)
window.minimize();
else
window.activate(global.get_current_time());
}
_onMenuStateChanged(menu, isOpen) {
if (isOpen)
return;
const extension = Extension.lookupByURL(import.meta.url);
let [x, y] = global.get_pointer();
let actor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);
if (extension.someWindowListContains(actor))
actor.sync_hover();
}
_onPopupMenu(_actor) {
if (!this._canOpenPopupMenu() || this._contextMenu.isOpen)
return;
this._openMenu(this._contextMenu);
}
_isFocused() {
throw new GObject.NotImplementedError(
`_isFocused in ${this.constructor.name}`);
}
_updateStyle() {
if (this._isFocused())
this.add_style_class_name('focused');
else
this.remove_style_class_name('focused');
}
_windowEnteredOrLeftMonitor(_metaDisplay, _monitorIndex, _metaWindow) {
throw new GObject.NotImplementedError(
`_windowEnteredOrLeftMonitor in ${this.constructor.name}`);
}
_isWindowVisible(window) {
let workspace = global.workspace_manager.get_active_workspace();
return !window.skip_taskbar &&
(this._ignoreWorkspace || window.located_on_workspace(workspace)) &&
(!this._perMonitor || window.get_monitor() === this._monitorIndex);
}
_updateVisibility() {
throw new GObject.NotImplementedError(
`_updateVisibility in ${this.constructor.name}`);
}
_getIconGeometry() {
const rect = new Mtk.Rectangle();
[rect.x, rect.y] = this.get_transformed_position();
[rect.width, rect.height] = this.get_transformed_size();
return rect;
}
_updateIconGeometry() {
throw new GObject.NotImplementedError(
`_updateIconGeometry in ${this.constructor.name}`);
}
_onDestroy() {
this._tooltip.destroy();
}
}
class WindowButton extends BaseButton {
static {
GObject.registerClass(this);
}
constructor(metaWindow, perMonitor, monitorIndex) {
super(perMonitor, monitorIndex);
this.metaWindow = metaWindow;
metaWindow.connectObject(
'notify::skip-taskbar', () => this._updateVisibility(),
'workspace-changed', () => this._updateVisibility(),
this);
this._updateVisibility();
this._windowTitle = new WindowTitle(this.metaWindow);
this.set_child(this._windowTitle);
this.label_actor = this._windowTitle.label_actor;
this._contextMenu = new WindowContextMenu(this, this.metaWindow);
this._contextMenu.connect('open-state-changed',
this._onMenuStateChanged.bind(this));
this._contextMenu.actor.hide();
this._contextMenuManager.addMenu(this._contextMenu);
Main.uiGroup.add_child(this._contextMenu.actor);
global.display.connectObject('notify::focus-window',
() => this._updateStyle(), this);
this._updateStyle();
}
_onClicked(actor, button) {
if (this._contextMenu.isOpen) {
this._contextMenu.close();
return;
}
if (!button || button === 1)
this._minimizeOrActivateWindow(this.metaWindow);
else
this._openMenu(this._contextMenu);
}
_isFocused() {
return global.display.focus_window === this.metaWindow;
}
_updateStyle() {
super._updateStyle();
if (this.metaWindow.minimized)
this.add_style_class_name('minimized');
else
this.remove_style_class_name('minimized');
}
_windowEnteredOrLeftMonitor(metaDisplay, monitorIndex, metaWindow) {
if (monitorIndex === this._monitorIndex && metaWindow === this.metaWindow)
this._updateVisibility();
}
_updateVisibility() {
this.visible = this._isWindowVisible(this.metaWindow);
}
_updateIconGeometry() {
this.metaWindow.set_icon_geometry(this._getIconGeometry());
}
_onDestroy() {
super._onDestroy();
this._contextMenu.destroy();
}
}
class AppContextMenu extends PopupMenu.PopupMenu {
constructor(source) {
super(source, 0.5, St.Side.BOTTOM);
this._appButton = source;
this._minimizeItem = new PopupMenu.PopupMenuItem(_('Minimize all'));
this._minimizeItem.connect('activate', () => {
this._appButton.getWindowList().forEach(w => w.minimize());
});
this.addMenuItem(this._minimizeItem);
this._unminimizeItem = new PopupMenu.PopupMenuItem(_('Unminimize all'));
this._unminimizeItem.connect('activate', () => {
this._appButton.getWindowList().forEach(w => w.unminimize());
});
this.addMenuItem(this._unminimizeItem);
this._maximizeItem = new PopupMenu.PopupMenuItem(_('Maximize all'));
this._maximizeItem.connect('activate', () => {
this._appButton.getWindowList().forEach(w => {
w.maximize(Meta.MaximizeFlags.BOTH);
});
});
this.addMenuItem(this._maximizeItem);
this._unmaximizeItem = new PopupMenu.PopupMenuItem(_('Unmaximize all'));
this._unmaximizeItem.connect('activate', () => {
this._appButton.getWindowList().forEach(w => {
w.unmaximize(Meta.MaximizeFlags.BOTH);
});
});
this.addMenuItem(this._unmaximizeItem);
let item = new PopupMenu.PopupMenuItem(_('Close all'));
item.connect('activate', () => {
this._appButton.getWindowList().forEach(w => {
w.delete(global.get_current_time());
});
});
this.addMenuItem(item);
}
open(animate) {
let windows = this._appButton.getWindowList();
this._minimizeItem.visible = windows.some(w => !w.minimized);
this._unminimizeItem.visible = windows.some(w => w.minimized);
this._maximizeItem.visible = windows.some(w => {
return w.get_maximized() !== Meta.MaximizeFlags.BOTH;
});
this._unmaximizeItem.visible = windows.some(w => {
return w.get_maximized() === Meta.MaximizeFlags.BOTH;
});
super.open(animate);
}
}
class AppButton extends BaseButton {
static {
GObject.registerClass(this);
}
constructor(app, perMonitor, monitorIndex) {
super(perMonitor, monitorIndex);
this.app = app;
this._updateVisibility();
let stack = new St.Widget({layout_manager: new Clutter.BinLayout()});
this.set_child(stack);
this._singleWindowTitle = new St.Bin({
x_expand: true,
});
stack.add_child(this._singleWindowTitle);
this._multiWindowTitle = new St.BoxLayout({
style_class: 'window-button-box',
x_expand: true,
});
stack.add_child(this._multiWindowTitle);
this._icon = new St.Bin({
style_class: 'window-button-icon',
child: app.create_icon_texture(ICON_TEXTURE_SIZE),
});
this._multiWindowTitle.add_child(this._icon);
let label = new St.Label({
text: app.get_name(),
y_align: Clutter.ActorAlign.CENTER,
});
this._multiWindowTitle.add_child(label);
this._multiWindowTitle.label_actor = label;
this._menuManager = new PopupMenu.PopupMenuManager(this);
this._menu = new PopupMenu.PopupMenu(this, 0.5, St.Side.BOTTOM);
this._menu.connect('open-state-changed',
this._onMenuStateChanged.bind(this));
this._menu.actor.hide();
this._menu.connect('activate', this._onMenuActivate.bind(this));
this._menuManager.addMenu(this._menu);
Main.uiGroup.add_child(this._menu.actor);
this._appContextMenu = new AppContextMenu(this);
this._appContextMenu.connect('open-state-changed',
this._onMenuStateChanged.bind(this));
this._appContextMenu.actor.hide();
Main.uiGroup.add_child(this._appContextMenu.actor);
this._textureCache = St.TextureCache.get_default();
this._textureCache.connectObject('icon-theme-changed', () => {
this._icon.child = app.create_icon_texture(ICON_TEXTURE_SIZE);
}, this);
this.app.connectObject('windows-changed',
() => this._windowsChanged(), this);
this._windowsChanged();
this._windowTracker = Shell.WindowTracker.get_default();
this._windowTracker.connectObject('notify::focus-app',
() => this._updateStyle(), this);
this._updateStyle();
}
_windowEnteredOrLeftMonitor(metaDisplay, monitorIndex, metaWindow) {
if (this._windowTracker.get_window_app(metaWindow) === this.app &&
monitorIndex === this._monitorIndex) {
this._updateVisibility();
this._windowsChanged();
}
}
_updateVisibility() {
if (this._ignoreWorkspace) {
this.visible = true;
} else if (!this._perMonitor) {
// fast path: use ShellApp API to avoid iterating over all windows.
let workspace = global.workspace_manager.get_active_workspace();
this.visible = this.app.is_on_workspace(workspace);
} else {
this.visible = this.getWindowList().length >= 1;
}
}
_isFocused() {
return this._windowTracker.focus_app === this.app;
}
_updateIconGeometry() {
let rect = this._getIconGeometry();
let windows = this.app.get_windows();
windows.forEach(w => w.set_icon_geometry(rect));
}
getWindowList() {
return this.app.get_windows().filter(win => this._isWindowVisible(win));
}
_windowsChanged() {
let windows = this.getWindowList();
this._singleWindowTitle.visible = windows.length === 1;
this._multiWindowTitle.visible = !this._singleWindowTitle.visible;
if (this._singleWindowTitle.visible) {
if (!this._windowTitle) {
this.metaWindow = windows[0];
this._windowTitle = new WindowTitle(this.metaWindow);
this._singleWindowTitle.child = this._windowTitle;
this._windowContextMenu = new WindowContextMenu(this, this.metaWindow);
this._windowContextMenu.connect(
'open-state-changed', this._onMenuStateChanged.bind(this));
Main.uiGroup.add_child(this._windowContextMenu.actor);
this._windowContextMenu.actor.hide();
this._contextMenuManager.addMenu(this._windowContextMenu);
}
this._contextMenuManager.removeMenu(this._appContextMenu);
this._contextMenu = this._windowContextMenu;
this.label_actor = this._windowTitle.label_actor;
} else {
if (this._windowTitle) {
this.metaWindow = null;
this._singleWindowTitle.child = null;
this._windowTitle = null;
this._windowContextMenu.destroy();
this._windowContextMenu = null;
}
this._contextMenu = this._appContextMenu;
this._contextMenuManager.addMenu(this._appContextMenu);
this.label_actor = this._multiWindowTitle.label_actor;
}
}
_onClicked(actor, button) {
let menuWasOpen = this._menu.isOpen;
if (menuWasOpen)
this._menu.close();
let contextMenuWasOpen = this._contextMenu.isOpen;
if (contextMenuWasOpen)
this._contextMenu.close();
if (!button || button === 1) {
if (menuWasOpen)
return;
let windows = this.getWindowList();
if (windows.length === 1) {
if (contextMenuWasOpen)
return;
this._minimizeOrActivateWindow(windows[0]);
} else {
this._menu.removeAll();
for (let i = 0; i < windows.length; i++) {
let windowTitle = new WindowTitle(windows[i]);
let item = new PopupMenu.PopupBaseMenuItem();
item.add_child(windowTitle);
item._window = windows[i];
this._menu.addMenuItem(item);
}
this._openMenu(this._menu);
}
} else {
if (contextMenuWasOpen)
return;
this._openMenu(this._contextMenu);
}
}
_canOpenPopupMenu() {
return !this._menu.isOpen;
}
_onMenuActivate(menu, child) {
child._window.activate(global.get_current_time());
}
_onDestroy() {
super._onDestroy();
this._menu.destroy();
}
}
class WindowList extends St.Widget {
static {
GObject.registerClass(this);
}
constructor(perMonitor, monitor, settings) {
super({
name: 'panel',
style_class: 'bottom-panel solid',
reactive: true,
track_hover: true,
layout_manager: new Clutter.BinLayout(),
});
this.connect('destroy', this._onDestroy.bind(this));
this._perMonitor = perMonitor;
this._monitor = monitor;
let box = new St.BoxLayout({x_expand: true, y_expand: true});
this.add_child(box);
let layout = new Clutter.BoxLayout({homogeneous: true});
this._windowList = new St.Widget({
style_class: 'window-list',
reactive: true,
layout_manager: layout,
x_align: Clutter.ActorAlign.START,
x_expand: true,
y_expand: true,
});
box.add_child(this._windowList);
this._windowList.connect('style-changed', () => {
let node = this._windowList.get_theme_node();
let spacing = node.get_length('spacing');
this._windowList.layout_manager.spacing = spacing;
});
this._windowList.connect('scroll-event', this._onScrollEvent.bind(this));
let indicatorsBox = new St.BoxLayout({x_align: Clutter.ActorAlign.END});
box.add_child(indicatorsBox);
this._workspaceIndicator = new WorkspaceIndicator();
indicatorsBox.add_child(this._workspaceIndicator.container);
this._mutterSettings = new Gio.Settings({schema_id: 'org.gnome.mutter'});
this._mutterSettings.connectObject(
'changed::workspaces-only-on-primary',
() => this._updateWorkspaceIndicatorVisibility(),
'changed::dynamic-workspaces',
() => this._updateWorkspaceIndicatorVisibility(),
this);
this._updateWorkspaceIndicatorVisibility();
this._menuManager = new PopupMenu.PopupMenuManager(this);
this._menuManager.addMenu(this._workspaceIndicator.menu);
const chromeOptions = {
affectsStruts: true,
trackFullscreen: true,
};
Main.layoutManager.addChrome(this, chromeOptions);
Main.uiGroup.set_child_above_sibling(this, Main.layoutManager.panelBox);
Main.ctrlAltTabManager.addGroup(this, _('Window List'), 'start-here-symbolic');
this.width = this._monitor.width;
this.connect('notify::height', this._updatePosition.bind(this));
this._updatePosition();
this._appSystem = Shell.AppSystem.get_default();
this._appSystem.connectObject('app-state-changed',
this._onAppStateChanged.bind(this), this);
// Hack: OSK gesture is tied to visibility, piggy-back on that
Main.keyboard._bottomDragAction.connectObject('notify::enabled',
action => {
const visible = !action.enabled;
if (visible) {
Main.uiGroup.set_child_above_sibling(
this, Main.layoutManager.keyboardBox);
} else {
Main.uiGroup.set_child_above_sibling(
this, Main.layoutManager.panelBox);
}
this._updateKeyboardAnchor();
}, this);
let workspaceManager = global.workspace_manager;
workspaceManager.connectObject('notify::n-workspaces',
() => this._updateWorkspaceIndicatorVisibility(), this);
this._updateWorkspaceIndicatorVisibility();
global.window_manager.connectObject('switch-workspace',
() => this._checkGrouping(), this);
Main.overview.connectObject(
'showing', () => {
Main.layoutManager.untrackChrome(this);
this.hide();
this._updateKeyboardAnchor();
},
'hidden', () => {
Main.layoutManager.trackChrome(this);
this.visible = !this._monitor.inFullscreen;
this._updateKeyboardAnchor();
}, this);
global.display.connectObject('in-fullscreen-changed', () => {
// Work-around for initial change from unknown to !fullscreen
if (Main.overview.visible)
this.hide();
this._updateKeyboardAnchor();
}, this);
this._windowSignals = new Map();
global.display.connectObject(
'window-created', (dsp, win) => this._addWindow(win), this);
Main.xdndHandler.connectObject(
'drag-begin', () => this._monitorDrag(),
'drag-end', () => this._stopMonitoringDrag(),
this);
this._dragMonitor = {
dragMotion: this._onDragMotion.bind(this),
};
this._dndTimeoutId = 0;
this._dndWindow = null;
this._settings = settings;
this._settings.connectObject('changed::grouping-mode',
() => this._groupingModeChanged(), this);
this._grouped = undefined;
this._groupingModeChanged();
}
_onScrollEvent(actor, event) {
let direction = event.get_scroll_direction();
let diff = 0;
if (direction === Clutter.ScrollDirection.DOWN)
diff = 1;
else if (direction === Clutter.ScrollDirection.UP)
diff = -1;
else
return;
let children = this._windowList.get_children()
.filter(c => c.visible);
let active = children.findIndex(c => c.active);
let newActive = Math.max(0, Math.min(active + diff, children.length - 1));
children[newActive].activate();
}
_updatePosition() {
this.set_position(
this._monitor.x,
this._monitor.y + this._monitor.height - this.height);
}
_updateWorkspaceIndicatorVisibility() {
let workspaceManager = global.workspace_manager;
let hasWorkspaces = this._mutterSettings.get_boolean('dynamic-workspaces') ||
workspaceManager.n_workspaces > 1;
let workspacesOnMonitor = this._monitor === Main.layoutManager.primaryMonitor ||
!this._mutterSettings.get_boolean('workspaces-only-on-primary');
this._workspaceIndicator.visible = hasWorkspaces && workspacesOnMonitor;
}
_getPreferredUngroupedWindowListWidth() {
if (this._windowList.get_n_children() === 0)
return this._windowList.get_preferred_width(-1)[1];
let children = this._windowList.get_children();
let [, childWidth] = children[0].get_preferred_width(-1);
let {spacing} = this._windowList.layout_manager;
let workspace = global.workspace_manager.get_active_workspace();
let windows = global.display.get_tab_list(Meta.TabList.NORMAL, workspace);
if (this._perMonitor)
windows = windows.filter(w => w.get_monitor() === this._monitor.index);
let nWindows = windows.length;
if (nWindows === 0)
return this._windowList.get_preferred_width(-1)[1];
return nWindows * childWidth + (nWindows - 1) * spacing;
}
_getMaxWindowListWidth() {
let indicatorsBox = this._workspaceIndicator.get_parent();
return this.width - indicatorsBox.get_preferred_width(-1)[1];
}
_groupingModeChanged() {
this._groupingMode = this._settings.get_enum('grouping-mode');
if (this._groupingMode === GroupingMode.AUTO) {
this._checkGrouping();
} else {
this._grouped = this._groupingMode === GroupingMode.ALWAYS;
this._populateWindowList();
}
}
_checkGrouping() {
if (this._groupingMode !== GroupingMode.AUTO)
return;
let maxWidth = this._getMaxWindowListWidth();
let natWidth = this._getPreferredUngroupedWindowListWidth();
let grouped = maxWidth < natWidth;
if (this._grouped !== grouped) {
this._grouped = grouped;
this._populateWindowList();
}
}
_populateWindowList() {
this._windowList.destroy_all_children();
if (!this._grouped) {
let windows = global.get_window_actors().sort((w1, w2) => {
return w1.metaWindow.get_stable_sequence() -
w2.metaWindow.get_stable_sequence();
});
for (let i = 0; i < windows.length; i++)
this._addWindow(windows[i].metaWindow);
} else {
let apps = this._appSystem.get_running().sort((a1, a2) => {
return _getAppStableSequence(a1) -
_getAppStableSequence(a2);
});
for (let i = 0; i < apps.length; i++)
this._addApp(apps[i]);
}
}
_updateKeyboardAnchor() {
const translationY = Main.overview.visible ? 0 : this.height;
Main.layoutManager.keyboardBox.translation_y = -translationY;
}
_onAppStateChanged(appSys, app) {
if (!this._grouped)
return;
if (app.state === Shell.AppState.RUNNING)
this._addApp(app);
else if (app.state === Shell.AppState.STOPPED)
this._removeApp(app);
}
_addApp(app) {
let button = new AppButton(app, this._perMonitor, this._monitor.index);
this._settings.bind('display-all-workspaces',
button, 'ignore-workspace', Gio.SettingsBindFlags.GET);
this._windowList.add_child(button);
}
_removeApp(app) {
let children = this._windowList.get_children();
let child = children.find(c => c.app === app);
if (child)
child.destroy();
}
_addWindow(win) {
if (!this._grouped)
this._checkGrouping();
if (this._grouped)
return;
let children = this._windowList.get_children();
if (children.find(c => c.metaWindow === win))
return;
this._windowSignals.set(
win, win.connect('unmanaged', () => this._removeWindow(win)));
let button = new WindowButton(win, this._perMonitor, this._monitor.index);
this._settings.bind('display-all-workspaces',
button, 'ignore-workspace', Gio.SettingsBindFlags.GET);
this._windowList.add_child(button);
}
_removeWindow(win) {
if (this._grouped)
this._checkGrouping();
if (this._grouped)
return;
const id = this._windowSignals.get(win);
if (id)
win.disconnect(id);
this._windowSignals.delete(win);
let children = this._windowList.get_children();
let child = children.find(c => c.metaWindow === win);
if (child)
child.destroy();
}
_monitorDrag() {
DND.addDragMonitor(this._dragMonitor);
}
_stopMonitoringDrag() {
DND.removeDragMonitor(this._dragMonitor);
this._removeActivateTimeout();
}
_onDragMotion(dragEvent) {
if (Main.overview.visible ||
!this.contains(dragEvent.targetActor)) {
this._removeActivateTimeout();
return DND.DragMotionResult.CONTINUE;
}
let hoveredWindow = dragEvent.targetActor.metaWindow;
if (!hoveredWindow ||
this._dndWindow === hoveredWindow)
return DND.DragMotionResult.CONTINUE;
this._removeActivateTimeout();
this._dndWindow = hoveredWindow;
this._dndTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
DND_ACTIVATE_TIMEOUT, this._activateWindow.bind(this));
return DND.DragMotionResult.CONTINUE;
}
_removeActivateTimeout() {
if (this._dndTimeoutId)
GLib.source_remove(this._dndTimeoutId);
this._dndTimeoutId = 0;
this._dndWindow = null;
}
_activateWindow() {
let [x, y] = global.get_pointer();
let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
if (this._dndWindow && this.contains(pickedActor))
this._dndWindow.activate(global.get_current_time());
this._dndWindow = null;
this._dndTimeoutId = 0;
return false;
}
_onDestroy() {
this._workspaceIndicator.destroy();
Main.ctrlAltTabManager.removeGroup(this);
this._windowSignals.forEach((id, win) => win.disconnect(id));
this._windowSignals.clear();
this._stopMonitoringDrag();
this._settings.disconnectObject();
this._settings = null;
let windows = global.get_window_actors();
for (let i = 0; i < windows.length; i++)
windows[i].metaWindow.set_icon_geometry(null);
}
}
export default class WindowListExtension extends Extension {
constructor(metadata) {
super(metadata);
this._windowLists = null;
}
enable() {
this._windowLists = [];
this._settings = this.getSettings();
this._settings.connectObject('changed::show-on-all-monitors',
() => this._buildWindowLists(), this);
Main.layoutManager.connectObject('monitors-changed',
() => this._buildWindowLists(), this);
this._buildWindowLists();
}
_buildWindowLists() {
this._windowLists.forEach(list => list.destroy());
this._windowLists = [];
let showOnAllMonitors = this._settings.get_boolean('show-on-all-monitors');
Main.layoutManager.monitors.forEach(monitor => {
if (showOnAllMonitors || monitor === Main.layoutManager.primaryMonitor)
this._windowLists.push(new WindowList(showOnAllMonitors, monitor, this.getSettings()));
});
}
disable() {
if (!this._windowLists)
return;
Main.layoutManager.disconnectObject(this);
this._settings.disconnectObject(this);
this._settings = null;
this._windowLists.forEach(windowList => {
windowList.hide();
windowList.destroy();
});
this._windowLists = null;
}
someWindowListContains(actor) {
return this._windowLists.some(list => list.contains(actor));
}
}
class Tooltip extends St.Label {
static {
GObject.registerClass(this);
}
constructor(widget, params) {
super(params);
this._widget = widget;
this._widget.connect('notify::hover', () => {
if (this._widget.hover)
this.open();
else
this.close();
});
}
open() {
const buttonTitleWidget = this._widget.label_actor;
const [, , preferredTitleWidth] = buttonTitleWidget.get_preferred_size();
const maxTitleWidth = buttonTitleWidget.allocation.get_width();
const isTitleFullyShown = preferredTitleWidth <= maxTitleWidth;
if (isTitleFullyShown)
return;
this.set({
text: this._widget.label_actor.get_text(),
visible: true,
opacity: 0,
});
const [stageX, stageY] = this._widget.get_transformed_position();
const thumbWidth = this._widget.allocation.get_width();
const tipWidth = this.width;
const tipHeight = this.height;
const xOffset = Math.floor((thumbWidth - tipWidth) / 2);
const monitor = Main.layoutManager.findMonitorForActor(this);
const x = Math.clamp(
stageX + xOffset,
monitor.x,
monitor.x + monitor.width - tipWidth);
const y = stageY - tipHeight - TOOLTIP_OFFSET;
this.set_position(x, y);
this.ease({
opacity: 255,
duration: TOOLTIP_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => (this.visible = this._widget.hover),
});
}
close() {
this.ease({
opacity: 0,
duration: TOOLTIP_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => (this.visible = this._widget.hover),
});
}
}