diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css index f3c44a3d..c506bea2 100644 --- a/extensions/window-list/classic.css +++ b/extensions/window-list/classic.css @@ -6,14 +6,13 @@ height: 2.25em ; } - .bottom-panel .window-button > StWidget { + .bottom-panel .window-button > StWidget, + .bottom-panel .window-picker-toggle > StWidget { background-gradient-drection: vertical; background-color: #fff; background-gradient-start: #fff; background-gradient-end: #eee; color: #000; - -st-natural-width: 18.7em; - max-width: 18.75em; color: #2e3436; background-color: #eee; border-radius: 2px; @@ -22,7 +21,17 @@ text-shadow: 0 0 transparent; } - .bottom-panel .window-button:hover > StWidget { + .bottom-panel .window-button > StWidget { + -st-natural-width: 18.7em; + max-width: 18.75em; + } + + .bottom-panel .window-picker-toggle > StWidet { + border: 1px solid rgba(0,0,0,0.3); + } + + .bottom-panel .window-button:hover > StWidget, + .bottom-panel .window-picker-toggle:hover > StWidget { background-color: #f9f9f9; } @@ -31,7 +40,8 @@ box-shadow: inset 1px 1px 2px rgba(0,0,0,0.5); } - .bottom-panel .window-button.focused > StWidget { + .bottom-panel .window-button.focused > StWidget, + .bottom-panel .window-picker-toggle:checked > StWidget { background-color: #ddd; box-shadow: inset 1px 1px 1px rgba(0,0,0,0.5); } diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js index 8a1d9563..f749078f 100644 --- a/extensions/window-list/extension.js +++ b/extensions/window-list/extension.js @@ -4,10 +4,13 @@ const { Clutter, Gio, GLib, GObject, Gtk, Meta, Shell, St } = imports.gi; const DND = imports.ui.dnd; const ExtensionUtils = imports.misc.extensionUtils; const Main = imports.ui.main; +const Overview = imports.ui.overview; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; +const Tweener = imports.ui.tweener; const Me = ExtensionUtils.getCurrentExtension(); +const { WindowPicker, WindowPickerToggle } = Me.imports.windowPicker; const Gettext = imports.gettext.domain('gnome-shell-extensions'); const _ = Gettext.gettext; @@ -776,6 +779,12 @@ class WindowList { let box = new St.BoxLayout({ x_expand: true, y_expand: true }); this.actor.add_actor(box); + let toggle = new WindowPickerToggle(); + box.add_actor(toggle); + + toggle.connect('notify::checked', + this._updateWindowListVisibility.bind(this)); + let layout = new Clutter.BoxLayout({ homogeneous: true }); this._windowList = new St.Widget({ style_class: 'window-list', @@ -920,6 +929,19 @@ class WindowList { this._workspaceIndicator.visible = hasWorkspaces && workspacesOnMonitor; } + _updateWindowListVisibility() { + let visible = !Main.windowPicker.visible; + + Tweener.addTween(this._windowList, { + opacity: visible ? 255 : 0, + transition: 'ease-out-quad', + time: Overview.ANIMATION_TIME + }); + + this._windowList.reactive = visible; + this._windowList.get_children().forEach(c => c.reactive = visible); + } + _getPreferredUngroupedWindowListWidth() { if (this._windowList.get_n_children() == 0) return this._windowList.get_preferred_width(-1)[1]; @@ -1187,7 +1209,7 @@ class WindowList { class Extension { constructor() { this._windowLists = null; - this._injections = {}; + this._hideOverviewOrig = Main.overview.hide; } enable() { @@ -1200,6 +1222,13 @@ class Extension { this._monitorsChangedId = Main.layoutManager.connect( 'monitors-changed', this._buildWindowLists.bind(this)); + Main.windowPicker = new WindowPicker(); + + Main.overview.hide = () => { + Main.windowPicker.close(); + this._hideOverviewOrig.call(Main.overview); + }; + this._buildWindowLists(); } @@ -1230,6 +1259,11 @@ class Extension { windowList.actor.destroy(); }); this._windowLists = null; + + Main.windowPicker.actor.destroy(); + delete Main.windowPicker; + + Main.overview.hide = this._hideOverviewOrig; } someWindowListContains(actor) { diff --git a/extensions/window-list/meson.build b/extensions/window-list/meson.build index b4aa4dba..5b1f5f51 100644 --- a/extensions/window-list/meson.build +++ b/extensions/window-list/meson.build @@ -4,7 +4,7 @@ extension_data += configure_file( configuration: metadata_conf ) -extension_sources += files('prefs.js') +extension_sources += files('prefs.js', 'windowPicker.js') extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') if classic_mode_enabled diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css index f5285cb3..91383ab6 100644 --- a/extensions/window-list/stylesheet.css +++ b/extensions/window-list/stylesheet.css @@ -26,9 +26,8 @@ spacing: 4px; } -.window-button > StWidget { - -st-natural-width: 18.75em; - max-width: 18.75em; +.window-button > StWidget, +.window-picker-toggle > StWidget { color: #bbb; background-color: black; border-radius: 4px; @@ -37,7 +36,21 @@ text-shadow: 1px 1px 4px rgba(0,0,0,0.8); } -.window-button:hover > StWidget { +.window-picker-toggle { + padding: 3px; +} + +.window-picker-toggle > StWidet { + border: 1px solid rgba(255,255,255,0.3); +} + +.window-button > StWidget { + -st-natural-width: 18.75em; + max-width: 18.75em; +} + +.window-button:hover > StWidget, +.window-picker-toggle:hover > StWidget { color: white; background-color: #1f1f1f; } @@ -47,12 +60,14 @@ box-shadow: inset 2px 2px 4px rgba(255,255,255,0.5); } -.window-button.focused > StWidget { +.window-button.focused > StWidget, +.window-picker-toggle:checked > StWidget { color: white; box-shadow: inset 1px 1px 4px rgba(255,255,255,0.7); } -.window-button.focused:active > StWidget { +.window-button.focused:active > StWidget, +.window-picker-toggle:checked:active > StWidget { box-shadow: inset 2px 2px 4px rgba(255,255,255,0.7); } diff --git a/extensions/window-list/windowPicker.js b/extensions/window-list/windowPicker.js new file mode 100644 index 00000000..024fd802 --- /dev/null +++ b/extensions/window-list/windowPicker.js @@ -0,0 +1,260 @@ +/* exported WindowPicker, WindowPickerToggle */ +const { Clutter, GLib, GObject, Meta, Shell, St } = imports.gi; +const Signals = imports.signals; + +const Layout = imports.ui.layout; +const Main = imports.ui.main; +const Overview = imports.ui.overview; +const { WorkspacesDisplay } = imports.ui.workspacesView; + +let MyWorkspacesDisplay = class extends WorkspacesDisplay { + constructor() { + super(); + + this.actor.add_constraint( + new Layout.MonitorConstraint({ + primary: true, + work_area: true + })); + + this.actor.connect('destroy', this._onDestroy.bind(this)); + + this._workareasChangedId = global.display.connect('workareas-changed', + this._onWorkAreasChanged.bind(this)); + this._onWorkAreasChanged(); + } + + show(...args) { + if (this._scrollEventId == 0) + this._scrollEventId = Main.windowPicker.connect('scroll-event', + this._onScrollEvent.bind(this)); + + super.show(...args); + } + + hide(...args) { + if (this._scrollEventId > 0) + Main.windowPicker.disconnect(this._scrollEventId); + this._scrollEventId = 0; + + super.hide(...args); + } + + _onWorkAreasChanged() { + let { primaryIndex } = Main.layoutManager; + let workarea = Main.layoutManager.getWorkAreaForMonitor(primaryIndex); + this.setWorkspacesFullGeometry(workarea); + } + + _updateWorkspacesViews() { + super._updateWorkspacesViews(); + + this._workspacesViews.forEach(v => { + Main.layoutManager.overviewGroup.remove_actor(v.actor); + Main.windowPicker.actor.add_actor(v.actor); + }); + } + + _onDestroy() { + if (this._workareasChangedId) + global.display.disconnect(this._workareasChangedId); + this._workareasChangedId = 0; + } +}; + +var WindowPicker = class { + constructor() { + this._visible = false; + this._modal = false; + + this.actor = new Clutter.Actor(); + + this.actor.connect('destroy', this._onDestroy.bind(this)); + + global.bind_property('screen-width', + this.actor, 'width', + GObject.BindingFlags.SYNC_CREATE); + global.bind_property('screen-height', + this.actor, 'height', + GObject.BindingFlags.SYNC_CREATE); + + this._backgroundGroup = new Meta.BackgroundGroup({ reactive: true }); + this.actor.add_child(this._backgroundGroup); + + this._backgroundGroup.connect('scroll-event', (a, ev) => { + this.emit('scroll-event', ev); + }); + + // Trick WorkspacesDisplay constructor into adding actions here + let addActionOrig = Main.overview.addAction; + Main.overview.addAction = a => this._backgroundGroup.add_action(a); + + this._workspacesDisplay = new MyWorkspacesDisplay(); + this.actor.add_child(this._workspacesDisplay.actor); + + Main.overview.addAction = addActionOrig; + + this._bgManagers = []; + + this._monitorsChangedId = Main.layoutManager.connect('monitors-changed', + this._updateBackgrounds.bind(this)); + this._updateBackgrounds(); + + Main.uiGroup.insert_child_below(this.actor, global.window_group); + } + + get visible() { + return this._visible; + } + + open() { + if (this._visible) + return; + + this._visible = true; + + if (!this._syncGrab()) + return; + + this._fakeOverviewVisible(true); + this._shadeBackgrounds(); + this._fakeOverviewAnimation(); + this._workspacesDisplay.show(false); + + this.emit('open-state-changed', this._visible); + } + + close() { + if (!this._visible) + return; + + this._visible = false; + + if (!this._syncGrab()) + return; + + this._workspacesDisplay.animateFromOverview(false); + this._unshadeBackgrounds(); + this._fakeOverviewAnimation(() => { + this._workspacesDisplay.hide(); + this._fakeOverviewVisible(false); + }); + + this.emit('open-state-changed', this._visible); + } + + _fakeOverviewAnimation(onComplete) { + Main.overview.animationInProgress = true; + GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + Overview.ANIMATION_TIME * 1000, + () => { + Main.overview.animationInProgress = false; + if (onComplete) + onComplete(); + }); + } + + _fakeOverviewVisible(visible) { + // Fake overview state for WorkspacesDisplay + Main.overview.visible = visible; + + // Hide real windows + Main.layoutManager._inOverview = visible; + Main.layoutManager._updateVisibility(); + } + + _syncGrab() { + if (this._visible) { + if (this._modal) + return true; + + this._modal = Main.pushModal(this.actor, { + actionMode: Shell.ActionMode.OVERVIEW + }); + + if (!this._modal) { + this.hide(); + return false; + } + } else if (this._modal) { + Main.popModal(this.actor); + this._modal = false; + } + return true; + } + + _onDestroy() { + if (this._monitorsChangedId) + Main.layoutManager.disconnect(this._monitorsChangedId); + this._monitorsChangedId = 0; + } + + _updateBackgrounds() { + Main.overview._updateBackgrounds.call(this); + } + + _shadeBackgrounds() { + Main.overview._shadeBackgrounds.call(this); + } + + _unshadeBackgrounds() { + Main.overview._unshadeBackgrounds.call(this); + } +}; +Signals.addSignalMethods(WindowPicker.prototype); + +var WindowPickerToggle = GObject.registerClass( +class WindowPickerToggle extends St.Button { + _init() { + let iconBin = new St.Widget({ + layout_manager: new Clutter.BinLayout() + }); + iconBin.add_child(new St.Icon({ + icon_name: 'focus-windows-symbolic', + icon_size: 16, + x_expand: true, + y_expand: true, + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER + })); + super._init({ + style_class: 'window-picker-toggle', + child: iconBin, + visible: !Main.sessionMode.hasOverview, + x_fill: true, + y_fill: true, + toggle_mode: true + }); + + this._overlayKeyId = 0; + + this.connect('destroy', this._onDestroy.bind(this)); + + this.connect('notify::checked', () => { + if (this.checked) + Main.windowPicker.open(); + else + Main.windowPicker.close(); + }); + + if (!Main.sessionMode.hasOverview) { + this._overlayKeyId = global.display.connect('overlay-key', () => { + if (!Main.windowPicker.visible) + Main.windowPicker.open(); + else + Main.windowPicker.close(); + }); + } + + Main.windowPicker.connect('open-state-changed', () => { + this.checked = Main.windowPicker.visible; + }); + } + + _onDestroy() { + if (this._overlayKeyId) + global.display.disconnect(this._overlayKeyId); + this._overlayKeyId == 0; + } +});