From c6d2063f4df19d384bfe60179a6339f10462cff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Fri, 28 Jun 2019 11:33:16 +0200 Subject: [PATCH] workspace-indicator: Show previews in workspace switcher Currently the new horizontal workspace switcher only shows a series of buttons, with no indication of the workspaces' contents. Go full GNOME 2 and add tiny draggable preview rectangles that represent the windows on a particular workspace. https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/77 --- extensions/workspace-indicator/extension.js | 187 +++++++++++++++++- extensions/workspace-indicator/stylesheet.css | 10 + 2 files changed, 196 insertions(+), 1 deletion(-) diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js index 3e6e939b..4a9e5c19 100644 --- a/extensions/workspace-indicator/extension.js +++ b/extensions/workspace-indicator/extension.js @@ -3,6 +3,7 @@ const { Clutter, Gio, GObject, Meta, St } = imports.gi; +const DND = imports.ui.dnd; const ExtensionUtils = imports.misc.extensionUtils; const Main = imports.ui.main; const PanelMenu = imports.ui.panelMenu; @@ -14,15 +15,186 @@ const _ = Gettext.gettext; const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences'; const WORKSPACE_KEY = 'workspace-names'; +let WindowPreview = GObject.registerClass({ + GTypeName: 'WorkspaceIndicatorWindowPreview' +}, class WindowPreview extends St.Button { + _init(window) { + super._init({ + style_class: 'workspace-indicator-window-preview' + }); + + this._delegate = this; + DND.makeDraggable(this, { restoreOnSuccess: true }); + + this._window = window; + + this.connect('destroy', this._onDestroy.bind(this)); + + this._sizeChangedId = this._window.connect('size-changed', + this._relayout.bind(this)); + this._positionChangedId = this._window.connect('position-changed', + this._relayout.bind(this)); + this._minimizedChangedId = this._window.connect('notify::minimized', + this._relayout.bind(this)); + this._monitorEnteredId = global.display.connect('window-entered-monitor', + this._relayout.bind(this)); + this._monitorLeftId = global.display.connect('window-left-monitor', + this._relayout.bind(this)); + + // Do initial layout when we get a parent + let id = this.connect('parent-set', () => { + this.disconnect(id); + if (!this.get_parent()) + return; + this._laterId = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { + this._laterId = 0; + this._relayout(); + return false; + }); + }); + + this._focusChangedId = global.display.connect('notify::focus-window', + this._onFocusChanged.bind(this)); + this._onFocusChanged(); + } + + // needed for DND + get realWindow() { + return this._window.get_compositor_private(); + } + + _onDestroy() { + this._window.disconnect(this._sizeChangedId); + this._window.disconnect(this._positionChangedId); + this._window.disconnect(this._minimizedChangedId); + global.display.disconnect(this._monitorEnteredId); + global.display.disconnect(this._monitorLeftId); + global.display.disconnect(this._focusChangedId); + if (this._laterId) + Meta.later_remove(this._laterId); + } + + _onFocusChanged() { + if (global.display.focus_window == this._window) + this.add_style_class_name('active'); + else + this.remove_style_class_name('active'); + } + + _relayout() { + let monitor = Main.layoutManager.findIndexForActor(this); + this.visible = monitor == this._window.get_monitor() && + this._window.showing_on_its_workspace(); + + if (!this.visible) + return; + + let workArea = Main.layoutManager.getWorkAreaForMonitor(monitor); + let hscale = this.get_parent().allocation.get_width() / workArea.width; + let vscale = this.get_parent().allocation.get_height() / workArea.height; + + let frameRect = this._window.get_frame_rect(); + this.set_size( + Math.round(Math.min(frameRect.width, workArea.width) * hscale), + Math.round(Math.min(frameRect.height, workArea.height) * vscale)); + this.set_position( + Math.round(frameRect.x * hscale), + Math.round(frameRect.y * vscale)); + } +}); + let WorkspaceThumbnail = GObject.registerClass({ GTypeName: 'WorkspaceIndicatorWorkspaceThumbnail' }, class WorkspaceThumbnail extends St.Button { _init(index) { super._init({ style_class: 'workspace', + child: new Clutter.Actor({ + layout_manager: new Clutter.BinLayout(), + clip_to_allocation: true + }), + x_fill: true, + y_fill: true }); + this.connect('destroy', this._onDestroy.bind(this)); + this._index = index; + this._delegate = this; // needed for DND + + this._windowPreviews = new Map(); + + let workspaceManager = global.workspace_manager; + this._workspace = workspaceManager.get_workspace_by_index(index); + + this._windowAddedId = this._workspace.connect('window-added', + (ws, window) => { + this._addWindow(window); + }); + this._windowRemovedId = this._workspace.connect('window-removed', + (ws, window) => { + this._removeWindow(window); + }); + this._restackedId = global.display.connect('restacked', + this._onRestacked.bind(this)); + + this._workspace.list_windows().forEach(w => this._addWindow(w)); + this._onRestacked(); + } + + acceptDrop(source) { + if (!source.realWindow) + return false; + + let window = source.realWindow.get_meta_window(); + this._moveWindow(window); + return true; + } + + handleDragOver(source) { + if (source.realWindow) + return DND.DragMotionResult.MOVE_DROP; + else + return DND.DragMotionResult.CONTINUE; + } + + _addWindow(window) { + if (this._windowPreviews.has(window)) + return; + + let preview = new WindowPreview(window); + preview.connect('clicked', (a, btn) => this.emit('clicked', btn)); + this._windowPreviews.set(window, preview); + this.child.add_child(preview); + } + + _removeWindow(window) { + let preview = this._windowPreviews.get(window); + if (!preview) + return; + + this._windowPreviews.delete(window); + preview.destroy(); + } + + _onRestacked() { + let lastPreview = null; + let windows = global.get_window_actors().map(a => a.meta_window); + for (let i = 0; i < windows.length; i++) { + let preview = this._windowPreviews.get(windows[i]); + if (!preview) + continue; + + this.child.set_child_above_sibling(preview, lastPreview); + lastPreview = preview; + } + } + + _moveWindow(window) { + let monitorIndex = Main.layoutManager.findIndexForActor(this); + if (monitorIndex != window.get_monitor()) + window.move_to_monitor(monitorIndex); + window.change_workspace_by_index(this._index, false); } on_clicked() { @@ -30,8 +202,13 @@ let WorkspaceThumbnail = GObject.registerClass({ if (ws) ws.activate(global.get_current_time()); } -}); + _onDestroy() { + this._workspace.disconnect(this._windowAddedId); + this._workspace.disconnect(this._windowRemovedId); + global.display.disconnect(this._restackedId); + } +}); let WorkspaceIndicator = GObject.registerClass( class WorkspaceIndicator extends PanelMenu.Button { @@ -98,6 +275,8 @@ class WorkspaceIndicator extends PanelMenu.Button { this._settingsChangedId = 0; } + Main.panel.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); + super._onDestroy(); } @@ -107,6 +286,12 @@ class WorkspaceIndicator extends PanelMenu.Button { this._statusLabel.visible = vertical; this._thumbnailsBox.visible = !vertical; + + // Disable offscreen-redirect when showing the workspace switcher + // so that clip-to-allocation works + Main.panel.set_offscreen_redirect(vertical + ? Clutter.OffscreenRedirect.ALWAYS + : Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY); } _onWorkspaceSwitched() { diff --git a/extensions/workspace-indicator/stylesheet.css b/extensions/workspace-indicator/stylesheet.css index 51181948..8601c3e5 100644 --- a/extensions/workspace-indicator/stylesheet.css +++ b/extensions/workspace-indicator/stylesheet.css @@ -28,3 +28,13 @@ .panel-workspace-indicator-box .workspace:first-child { border-left-width: 1px; } + +.workspace-indicator-window-preview { + background-color: #252525; + border: 1px solid #ccc; +} + +.workspace-indicator-window-preview.active { + background-color: #353535; + border: 2px solid #ccc; +}