Blob Blame History Raw
From 05833ca853e5e661cf43f59734ca0a29219159fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Thu, 18 Jul 2019 00:39:49 +0200
Subject: [PATCH 01/33] apps-menu: Add drop-shadow to application icons

... to make sure they are readable on light backgrounds.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/issues/168
---
 extensions/apps-menu/extension.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/extensions/apps-menu/extension.js b/extensions/apps-menu/extension.js
index 49a05c7..860cb77 100644
--- a/extensions/apps-menu/extension.js
+++ b/extensions/apps-menu/extension.js
@@ -112,7 +112,9 @@ class ApplicationMenuItem extends PopupMenu.PopupBaseMenuItem {
     }
 
     _updateIcon() {
-        this._iconBin.set_child(this.getDragActor());
+        let icon = this.getDragActor();
+        icon.style_class = 'icon-dropshadow';
+        this._iconBin.set_child(icon);
     }
 };
 
-- 
2.21.0


From 4ace6d5da8e0edc590706af8afb8cfc843a06af4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 29 May 2019 10:17:20 +0000
Subject: [PATCH 02/33] places-menu: Don't hardcode position

The extension currently assumes that we have the "Activities" button
at the left of the top bar. This is currently true, not only in the
regular session, but also in GNOME classic where the button is hidden
(but still present).

However this is about to change: We will stop taking over the button
from the apps-menu extension, and instead disable "Activities" from
the session mode definition.

Prepare for this by adding the places menu before the application menu
instead of assuming a hardcoded position.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/69
---
 extensions/places-menu/extension.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/extensions/places-menu/extension.js b/extensions/places-menu/extension.js
index 429e81d..cdecb7b 100644
--- a/extensions/places-menu/extension.js
+++ b/extensions/places-menu/extension.js
@@ -133,9 +133,9 @@ let _indicator;
 function enable() {
     _indicator = new PlacesMenu;
 
-    let pos = 1;
+    let pos = Main.sessionMode.panel.left.indexOf('appMenu');
     if ('apps-menu' in Main.panel.statusArea)
-        pos = 2;
+        pos++;
     Main.panel.addToStatusArea('places-menu', _indicator, pos, 'left');
 }
 
-- 
2.21.0


From 829929350bda3a03795c43d303273a7c9ff225b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Thu, 27 Jun 2019 03:57:53 +0200
Subject: [PATCH 03/33] apps-menu: Add missing chain-up

PanelMenu.Button is a bit weird in that it also "contains" its parent
actor. That container is supposed to be destroyed with the button, but
as we currently don't chain up to the parent class' _onDestroy(), we
leave behind an empty container every time the extension is disabled.

Fix this by adding the missing chain-up.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/75
---
 extensions/apps-menu/extension.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/extensions/apps-menu/extension.js b/extensions/apps-menu/extension.js
index 860cb77..08eda10 100644
--- a/extensions/apps-menu/extension.js
+++ b/extensions/apps-menu/extension.js
@@ -491,6 +491,8 @@ class ApplicationsButton extends PanelMenu.Button {
     }
 
     _onDestroy() {
+        super._onDestroy();
+
         Main.overview.disconnect(this._showingId);
         Main.overview.disconnect(this._hidingId);
         appSys.disconnect(this._installedChangedId);
-- 
2.21.0


From 83e93a115f7375991893877ea82f2409569fee0f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 29 May 2019 08:32:03 +0000
Subject: [PATCH 04/33] apps-menu: Stop taking over Activities button

We don't want the "Activities" button in GNOME Classic, but the current
way of handling it is confusing:

 - the button is hidden, but the corresponding hot corner
   sometimes works (when the application menu isn't open)

 - the button is effectively moved inside the menu, although
   it's clearly not an app or category

 - the apps-menu can be used independent from classic mode, in
   which case removing the "Activities" button may not be wanted

Address those points by removing any handling of the activities button
from the apps-menu extension. We will remove it again from the classic
session via a session mode tweak.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/69
---
 extensions/apps-menu/extension.js | 63 +++----------------------------
 1 file changed, 6 insertions(+), 57 deletions(-)

diff --git a/extensions/apps-menu/extension.js b/extensions/apps-menu/extension.js
index 08eda10..9e02946 100644
--- a/extensions/apps-menu/extension.js
+++ b/extensions/apps-menu/extension.js
@@ -30,22 +30,6 @@ const HORIZ_FACTOR = 5;
 const MENU_HEIGHT_OFFSET = 132;
 const NAVIGATION_REGION_OVERSHOOT = 50;
 
-class ActivitiesMenuItem extends PopupMenu.PopupBaseMenuItem {
-    constructor(button) {
-        super();
-        this._button = button;
-        let label = new St.Label({ text: _("Activities Overview") });
-        this.actor.add_child(label);
-        this.actor.label_actor = label;
-    }
-
-    activate(event) {
-        this._button.menu.toggle();
-        Main.overview.toggle();
-        super.activate(event);
-    }
-};
-
 class ApplicationMenuItem extends PopupMenu.PopupBaseMenuItem {
     constructor(button, app) {
         super();
@@ -244,21 +228,6 @@ class ApplicationsMenu extends PopupMenu.PopupMenu {
         return false;
     }
 
-    open(animate) {
-        this._button.hotCorner.setBarrierSize(0);
-        if (this._button.hotCorner.actor) // fallback corner
-            this._button.hotCorner.actor.hide();
-        super.open(animate);
-    }
-
-    close(animate) {
-        let size = Main.layoutManager.panelBox.height;
-        this._button.hotCorner.setBarrierSize(size);
-        if (this._button.hotCorner.actor) // fallback corner
-            this._button.hotCorner.actor.show();
-        super.close(animate);
-    }
-
     toggle() {
         if (this.isOpen) {
             this._button.selectCategory(null, null);
@@ -407,7 +376,7 @@ class DesktopTarget {
 Signals.addSignalMethods(DesktopTarget.prototype);
 
 class ApplicationsButton extends PanelMenu.Button {
-    constructor() {
+    constructor(includeIcon) {
         super(1.0, null, false);
 
         this.setMenu(new ApplicationsMenu(this.actor, 1.0, St.Side.TOP, this));
@@ -422,7 +391,8 @@ class ApplicationsButton extends PanelMenu.Button {
 
         let iconFile = Gio.File.new_for_path('/usr/share/icons/hicolor/scalable/apps/start-here.svg');
         this._icon = new St.Icon({ gicon: new Gio.FileIcon({ file: iconFile }),
-                                   style_class: 'panel-logo-icon' });
+                                   style_class: 'panel-logo-icon',
+                                   visible: includeIcon });
         hbox.add_actor(this._icon);
 
         this._label = new St.Label({ text: _("Applications"),
@@ -435,7 +405,6 @@ class ApplicationsButton extends PanelMenu.Button {
         this.actor.name = 'panelApplications';
         this.actor.label_actor = this._label;
 
-        this.actor.connect('captured-event', this._onCapturedEvent.bind(this));
         this.actor.connect('destroy', this._onDestroy.bind(this));
 
         this._showingId = Main.overview.connect('showing', () => {
@@ -479,10 +448,6 @@ class ApplicationsButton extends PanelMenu.Button {
         }
     }
 
-    get hotCorner() {
-        return Main.layoutManager.hotCorners[Main.layoutManager.primaryIndex];
-    }
-
     _createVertSeparator() {
         let separator = new St.DrawingArea({ style_class: 'calendar-vertical-separator',
                                              pseudo_class: 'highlighted' });
@@ -509,14 +474,6 @@ class ApplicationsButton extends PanelMenu.Button {
         this._desktopTarget.destroy();
     }
 
-    _onCapturedEvent(actor, event) {
-        if (event.type() == Clutter.EventType.BUTTON_PRESS) {
-            if (!Main.overview.shouldToggleByCornerOrButton())
-                return true;
-        }
-        return false;
-    }
-
     _onMenuKeyPress(actor, event) {
         let symbol = event.get_key_symbol();
         if (symbol == Clutter.KEY_Left || symbol == Clutter.KEY_Right) {
@@ -651,11 +608,6 @@ class ApplicationsButton extends PanelMenu.Button {
                                                      x_fill: true, y_fill: true,
                                                      y_align: St.Align.START });
 
-        let activities = new ActivitiesMenuItem(this);
-        this.leftBox.add(activities.actor, { expand: false,
-                                             x_fill: true, y_fill: false,
-                                             y_align: St.Align.START });
-
         this.applicationsBox = new St.BoxLayout({ vertical: true });
         this.applicationsScrollBox.add_actor(this.applicationsBox);
         this.categoriesBox = new St.BoxLayout({ vertical: true });
@@ -760,19 +712,16 @@ class ApplicationsButton extends PanelMenu.Button {
 };
 
 let appsMenuButton;
-let activitiesButton;
 
 function enable() {
-    activitiesButton = Main.panel.statusArea['activities'];
-    activitiesButton.container.hide();
-    appsMenuButton = new ApplicationsButton();
-    Main.panel.addToStatusArea('apps-menu', appsMenuButton, 1, 'left');
+    let index = Main.sessionMode.panel.left.indexOf('activities') + 1;
+    appsMenuButton = new ApplicationsButton(index == 0);
+    Main.panel.addToStatusArea('apps-menu', appsMenuButton, index, 'left');
 }
 
 function disable() {
     Main.panel.menuManager.removeMenu(appsMenuButton.menu);
     appsMenuButton.destroy();
-    activitiesButton.container.show();
 }
 
 function init(metadata) {
-- 
2.21.0


From ea66ad17fe7b5d1a8071a6aa693603be5e62ae27 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Fri, 7 Jun 2019 14:30:16 +0000
Subject: [PATCH 05/33] apps-menu: Stop hiding the overview when toggled

Now that the extension no longer doubles as the "Activities" button,
that behavior is confusing.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/69
---
 extensions/apps-menu/extension.js | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/extensions/apps-menu/extension.js b/extensions/apps-menu/extension.js
index 9e02946..50fe55a 100644
--- a/extensions/apps-menu/extension.js
+++ b/extensions/apps-menu/extension.js
@@ -229,12 +229,8 @@ class ApplicationsMenu extends PopupMenu.PopupMenu {
     }
 
     toggle() {
-        if (this.isOpen) {
+        if (this.isOpen)
             this._button.selectCategory(null, null);
-        } else {
-            if (Main.overview.visible)
-                Main.overview.hide();
-        }
         super.toggle();
     }
 };
-- 
2.21.0


From 8b850a7f9b17cafbd417f597650a2a13cf8cfefa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Fri, 7 Jun 2019 20:07:19 +0000
Subject: [PATCH 06/33] apps-menu: Hide overview when launching app

Now that we no longer hide the overview when the menu is opened,
it is possible to activate menu entries from the overview. Start
hiding the overview in that case, which is consistent with app
launching elsewhere.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/69
---
 extensions/apps-menu/extension.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/extensions/apps-menu/extension.js b/extensions/apps-menu/extension.js
index 50fe55a..8d59e6d 100644
--- a/extensions/apps-menu/extension.js
+++ b/extensions/apps-menu/extension.js
@@ -75,6 +75,8 @@ class ApplicationMenuItem extends PopupMenu.PopupBaseMenuItem {
         this._button.selectCategory(null, null);
         this._button.menu.toggle();
         super.activate(event);
+
+        Main.overview.hide();
     }
 
     setActive(active, params) {
-- 
2.21.0


From 9762c3507177fe3a420bb228549aca03757d73c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 29 May 2019 09:44:30 +0000
Subject: [PATCH 07/33] classic: Disable overview

The overview is one of the defining features of GNOME 3, and thus
almost by definition at odds with the classic session, which
emulates a traditional GNOME 2 desktop.

Even with the less prominent placement inside the application menu
it never quite fit in - it doesn't help that besides the different
UI paradigma, the overview keeps its "normal" styling which differs
greatly with classic's normal mode.

So besides removing the "Activities" button via the session mode
definition, now that the apps-menu extension doesn't replace it anymore,
disable the overview completely in the classic session.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/69
---
 data/classic.json.in | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/data/classic.json.in b/data/classic.json.in
index fdb3762..c1c0544 100644
--- a/data/classic.json.in
+++ b/data/classic.json.in
@@ -1,8 +1,9 @@
 {
     "parentMode": "user",
     "stylesheetName": "gnome-classic.css",
+    "hasOverview": false,
     "enabledExtensions": [@CLASSIC_EXTENSIONS@],
-    "panel": { "left": ["activities", "appMenu"],
+    "panel": { "left": ["appMenu"],
                "center": [],
                "right": ["a11y", "keyboard", "dateMenu", "aggregateMenu"]
              }
-- 
2.21.0


From 1fee42083afafef861fc57e5d886e4e00945a19c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Tue, 14 May 2019 19:51:22 +0200
Subject: [PATCH 08/33] window-list: Add window picker button
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

With the latest changes, GNOME Classic has become so classic that it
is bordering dull. Salvage at least a tiny piece of GNOME 3 in form
of a window-pick button which toggles an exposé-like reduced overview.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/73
---
 extensions/window-list/classic.css     |  20 +-
 extensions/window-list/extension.js    |  36 +++-
 extensions/window-list/meson.build     |   2 +-
 extensions/window-list/stylesheet.css  |  27 ++-
 extensions/window-list/windowPicker.js | 260 +++++++++++++++++++++++++
 5 files changed, 332 insertions(+), 13 deletions(-)
 create mode 100644 extensions/window-list/windowPicker.js

diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css
index f3c44a3..c506bea 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 716a324..f4610f9 100644
--- a/extensions/window-list/extension.js
+++ b/extensions/window-list/extension.js
@@ -8,12 +8,15 @@ const St = imports.gi.St;
 
 const DND = imports.ui.dnd;
 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 ExtensionUtils = imports.misc.extensionUtils;
 const Me = ExtensionUtils.getCurrentExtension();
 const Convenience = Me.imports.convenience;
+const { WindowPicker, WindowPickerToggle } = Me.imports.windowPicker;
 
 const Gettext = imports.gettext.domain('gnome-shell-extensions');
 const _ = Gettext.gettext;
@@ -769,6 +772,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',
                                            reactive: true,
@@ -931,6 +940,19 @@ class WindowList {
         this._workspaceIndicator.actor.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];
@@ -1205,7 +1227,7 @@ class WindowList {
 class Extension {
     constructor() {
         this._windowLists = null;
-        this._injections = {};
+        this._hideOverviewOrig = Main.overview.hide;
     }
 
     enable() {
@@ -1220,6 +1242,13 @@ class Extension {
             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();
     }
 
@@ -1250,6 +1279,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 b4aa4db..5b1f5f5 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 f5285cb..91383ab 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 0000000..59875d5
--- /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.screen.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.screen.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;
+    }
+});
-- 
2.21.0


From ea3f6be47066e1e4a2f86824576a17bc09bcd39d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Sat, 29 Jun 2019 02:52:45 +0200
Subject: [PATCH 09/33] window-list: Fix resetting handler ID

This is embarrassing, although destroy() is expected to only run once,
so the bug shouldn't have an effect in practice.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/78
---
 extensions/window-list/windowPicker.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/extensions/window-list/windowPicker.js b/extensions/window-list/windowPicker.js
index 59875d5..c8a3dd6 100644
--- a/extensions/window-list/windowPicker.js
+++ b/extensions/window-list/windowPicker.js
@@ -255,6 +255,6 @@ class WindowPickerToggle extends St.Button {
     _onDestroy() {
         if (this._overlayKeyId)
             global.display.disconnect(this._overlayKeyId);
-        this._overlayKeyId == 0;
+        this._overlayKeyId = 0;
     }
 });
-- 
2.21.0


From eaa96905569de6640bd59cb89aa47fcdb8f486c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Tue, 2 Jul 2019 17:39:55 +0200
Subject: [PATCH 10/33] window-list: Move super-key handling into WindowPicker

We have an option to put a window list on each monitor, so we may have
more than one window picker toggle. We don't want each of those try to
toggle the window picker simultanuously, so move handling of the super
key directly into the picker.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/80
---
 extensions/window-list/windowPicker.js | 34 ++++++++++++--------------
 1 file changed, 15 insertions(+), 19 deletions(-)

diff --git a/extensions/window-list/windowPicker.js b/extensions/window-list/windowPicker.js
index c8a3dd6..088ebf2 100644
--- a/extensions/window-list/windowPicker.js
+++ b/extensions/window-list/windowPicker.js
@@ -67,6 +67,8 @@ var WindowPicker = class {
         this._visible = false;
         this._modal = false;
 
+        this._overlayKeyId = 0;
+
         this.actor = new Clutter.Actor();
 
         this.actor.connect('destroy', this._onDestroy.bind(this));
@@ -101,6 +103,15 @@ var WindowPicker = class {
         this._updateBackgrounds();
 
         Main.uiGroup.insert_child_below(this.actor, global.window_group);
+
+        if (!Main.sessionMode.hasOverview) {
+            this._overlayKeyId = global.display.connect('overlay-key', () => {
+                if (!this._visible)
+                    this.open();
+                else
+                    this.close();
+            });
+        }
     }
 
     get visible() {
@@ -188,6 +199,10 @@ var WindowPicker = class {
         if (this._monitorsChangedId)
             Main.layoutManager.disconnect(this._monitorsChangedId);
         this._monitorsChangedId = 0;
+
+        if (this._overlayKeyId)
+            global.display.disconnect(this._overlayKeyId);
+        this._overlayKeyId = 0;
     }
 
     _updateBackgrounds() {
@@ -227,10 +242,6 @@ class WindowPickerToggle extends St.Button {
             toggle_mode: true
         });
 
-        this._overlayKeyId = 0;
-
-        this.connect('destroy', this._onDestroy.bind(this));
-
         this.connect('notify::checked', () => {
             if (this.checked)
                 Main.windowPicker.open();
@@ -238,23 +249,8 @@ class WindowPickerToggle extends St.Button {
                 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;
-    }
 });
-- 
2.21.0


From 381290b9938339768501e34aee6f21d7040e9394 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Tue, 2 Jul 2019 18:12:31 +0200
Subject: [PATCH 11/33] window-list: Handle closing window picker with Escape

Just like the overview can be closed with Escape, it makes sense to
allow the same for the window picker (in addition to pressing super
repeatedly).

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/80
---
 extensions/window-list/windowPicker.js | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/extensions/window-list/windowPicker.js b/extensions/window-list/windowPicker.js
index 088ebf2..461a4d1 100644
--- a/extensions/window-list/windowPicker.js
+++ b/extensions/window-list/windowPicker.js
@@ -68,6 +68,7 @@ var WindowPicker = class {
         this._modal = false;
 
         this._overlayKeyId = 0;
+        this._stageKeyPressId = 0;
 
         this.actor = new Clutter.Actor();
 
@@ -132,6 +133,16 @@ var WindowPicker = class {
         this._fakeOverviewAnimation();
         this._workspacesDisplay.show(false);
 
+        this._stageKeyPressId = global.stage.connect('key-press-event',
+            (a, event) => {
+                let sym = event.get_key_symbol();
+                if (sym == Clutter.KEY_Escape) {
+                    this.close();
+                    return Clutter.EVENT_STOP;
+                }
+                return Clutter.EVENT_PROPAGATE;
+            });
+
         this.emit('open-state-changed', this._visible);
     }
 
@@ -151,6 +162,9 @@ var WindowPicker = class {
             this._fakeOverviewVisible(false);
         });
 
+        global.stage.disconnect(this._stageKeyPressId);
+        this._stageKeyPressId = 0;
+
         this.emit('open-state-changed', this._visible);
     }
 
@@ -203,6 +217,10 @@ var WindowPicker = class {
         if (this._overlayKeyId)
             global.display.disconnect(this._overlayKeyId);
         this._overlayKeyId = 0;
+
+        if (this._stageKeyPressId)
+            global.stage.disconnect(this._stageKeyPressId);
+        this._stageKeyPressId = 0;
     }
 
     _updateBackgrounds() {
-- 
2.21.0


From 22a3bb1051ebda5cba050cdc95114673dae7dab8 Mon Sep 17 00:00:00 2001
From: Jakub Steiner <jimmac@gmail.com>
Date: Mon, 15 Jul 2019 23:40:09 +0200
Subject: [PATCH 12/33] classic: hover state for panel buttons

- prelight before active
- lighten up slightly, similar to what the default does (inverted)

Fixes https://gitlab.gnome.org/GNOME/gnome-shell-extensions/issues/169
---
 data/gnome-classic.scss | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/data/gnome-classic.scss b/data/gnome-classic.scss
index e8f4803..a231534 100644
--- a/data/gnome-classic.scss
+++ b/data/gnome-classic.scss
@@ -30,18 +30,20 @@ $variant: 'light';
     font-weight: normal;
     color: $fg_color;
     text-shadow: none;
+    &:hover {
+      color: lighten($fg_color,10%);
+      text-shadow: none;
+      & .system-status-icon { icon-shadow: none; }
+    }
     &:active, &:overview, &:focus, &:checked {
       // Trick due to St limitations. It needs a background to draw
       // a box-shadow
-      background-color: $selected_bg_color !important;
-      color: $selected_fg_color !important;
+      background-color: $selected_bg_color;
+      color: $selected_fg_color;
       box-shadow: none;
       & > .system-status-icon { icon-shadow: none; }
     }
-    &:hover {
-      text-shadow: none;
-      & .system-status-icon { icon-shadow: none; }
-    }
+
     .app-menu-icon { width: 0; height: 0; margin: 0; } // shell's display:none; :D
 
     .system-status-icon {
-- 
2.21.0


From da9d0ffad1b75413027f95c112be9d6a5e92ba78 Mon Sep 17 00:00:00 2001
From: Jakub Steiner <jimmac@gmail.com>
Date: Mon, 15 Jul 2019 23:03:41 +0200
Subject: [PATCH 13/33] classic: Update window-list styling

Make buttons flatter, rounder to match default styling.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/82
---
 extensions/window-list/classic.css | 25 +++++++++----------------
 1 file changed, 9 insertions(+), 16 deletions(-)

diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css
index c506bea..cc967e0 100644
--- a/extensions/window-list/classic.css
+++ b/extensions/window-list/classic.css
@@ -4,21 +4,18 @@
     border-top-width: 1px;
     border-bottom-width: 0px;
     height: 2.25em ;
+    padding: 2px;
   }
 
   .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;
     color: #2e3436;
     background-color: #eee;
-    border-radius: 2px;
+    border-radius: 3px;
     padding: 3px 6px 1px;
-    box-shadow: inset -1px -1px 1px rgba(0,0,0,0.5);
-    text-shadow: 0 0 transparent;
+    box-shadow: none;
+    text-shadow: none;
+    border: 1px solid rgba(0,0,0,0.2);
   }
 
   .bottom-panel .window-button > StWidget {
@@ -26,10 +23,6 @@
     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;
@@ -37,13 +30,13 @@
 
   .bottom-panel .window-button:active > StWidget,
   .bottom-panel .window-button:focus > StWidget {
-    box-shadow: inset 1px 1px 2px rgba(0,0,0,0.5);
+    box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
   }
 
   .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);
+    background-color: #ccc;
+    box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
   }
 
   .bottom-panel .window-button.focused:hover > StWidget {
@@ -52,5 +45,5 @@
 
   .bottom-panel .window-button.minimized > StWidget {
     color: #888;
-    box-shadow: inset -1px -1px 1px rgba(0,0,0,0.5);
+    box-shadow: none;
   }
-- 
2.21.0


From f9268e6d38e174d78a98f1e64734a00917724032 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 5 Jun 2019 00:23:13 +0000
Subject: [PATCH 14/33] window-list: Split out workspaceIndicator

The extension has grown unwieldily big, so before starting to improve
on the workspace indicator, move it to its own source file.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/70
---
 extensions/window-list/extension.js          | 115 +----------------
 extensions/window-list/meson.build           |   2 +-
 extensions/window-list/workspaceIndicator.js | 127 +++++++++++++++++++
 3 files changed, 129 insertions(+), 115 deletions(-)
 create mode 100644 extensions/window-list/workspaceIndicator.js

diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js
index f4610f9..061421a 100644
--- a/extensions/window-list/extension.js
+++ b/extensions/window-list/extension.js
@@ -9,7 +9,6 @@ const St = imports.gi.St;
 const DND = imports.ui.dnd;
 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;
 
@@ -17,6 +16,7 @@ const ExtensionUtils = imports.misc.extensionUtils;
 const Me = ExtensionUtils.getCurrentExtension();
 const Convenience = Me.imports.convenience;
 const { WindowPicker, WindowPickerToggle } = Me.imports.windowPicker;
+const { WorkspaceIndicator } = Me.imports.workspaceIndicator;
 
 const Gettext = imports.gettext.domain('gnome-shell-extensions');
 const _ = Gettext.gettext;
@@ -644,119 +644,6 @@ class AppButton extends BaseButton {
 };
 
 
-class WorkspaceIndicator extends PanelMenu.Button {
-    constructor() {
-        super(0.0, _("Workspace Indicator"), true);
-        this.setMenu(new PopupMenu.PopupMenu(this.actor, 0.0, St.Side.BOTTOM));
-        this.actor.add_style_class_name('window-list-workspace-indicator');
-        this.menu.actor.remove_style_class_name('panel-menu');
-
-        let container = new St.Widget({ layout_manager: new Clutter.BinLayout(),
-                                        x_expand: true, y_expand: true });
-        this.actor.add_actor(container);
-
-        this._currentWorkspace = global.screen.get_active_workspace().index();
-        this.statusLabel = new St.Label({ text: this._getStatusText(),
-                                          x_align: Clutter.ActorAlign.CENTER,
-                                          y_align: Clutter.ActorAlign.CENTER });
-        container.add_actor(this.statusLabel);
-
-        this.workspacesItems = [];
-
-        this._screenSignals = [];
-        this._screenSignals.push(global.screen.connect('notify::n-workspaces',
-                                                       this._updateMenu.bind(this)));
-        this._screenSignals.push(global.screen.connect_after('workspace-switched',
-                                                             this._updateIndicator.bind(this)));
-
-        this.actor.connect('scroll-event', this._onScrollEvent.bind(this));
-        this._updateMenu();
-
-        this._settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.wm.preferences' });
-        this._settingsChangedId =
-            this._settings.connect('changed::workspace-names',
-                                   this._updateMenu.bind(this));
-    }
-
-    destroy() {
-        for (let i = 0; i < this._screenSignals.length; i++)
-            global.screen.disconnect(this._screenSignals[i]);
-
-        if (this._settingsChangedId) {
-            this._settings.disconnect(this._settingsChangedId);
-            this._settingsChangedId = 0;
-        }
-
-        super.destroy();
-    }
-
-    _updateIndicator() {
-        this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
-        this._currentWorkspace = global.screen.get_active_workspace().index();
-        this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
-
-        this.statusLabel.set_text(this._getStatusText());
-    }
-
-    _getStatusText() {
-        let current = global.screen.get_active_workspace().index();
-        let total = global.screen.n_workspaces;
-
-        return '%d / %d'.format(current + 1, total);
-    }
-
-    _updateMenu() {
-        this.menu.removeAll();
-        this.workspacesItems = [];
-        this._currentWorkspace = global.screen.get_active_workspace().index();
-
-        for(let i = 0; i < global.screen.n_workspaces; i++) {
-            let name = Meta.prefs_get_workspace_name(i);
-            let item = new PopupMenu.PopupMenuItem(name);
-            item.workspaceId = i;
-
-            item.connect('activate', (item, event) => {
-                this._activate(item.workspaceId);
-            });
-
-            if (i == this._currentWorkspace)
-                item.setOrnament(PopupMenu.Ornament.DOT);
-
-            this.menu.addMenuItem(item);
-            this.workspacesItems[i] = item;
-        }
-
-        this.statusLabel.set_text(this._getStatusText());
-    }
-
-    _activate(index) {
-        if(index >= 0 && index < global.screen.n_workspaces) {
-            let metaWorkspace = global.screen.get_workspace_by_index(index);
-            metaWorkspace.activate(global.get_current_time());
-        }
-    }
-
-    _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 newIndex = this._currentWorkspace + diff;
-        this._activate(newIndex);
-    }
-
-    _allocate(actor, box, flags) {
-        if (actor.get_n_children() > 0)
-            actor.get_first_child().allocate(box, flags);
-    }
-};
-
 class WindowList {
     constructor(perMonitor, monitor) {
         this._perMonitor = perMonitor;
diff --git a/extensions/window-list/meson.build b/extensions/window-list/meson.build
index 5b1f5f5..34d7c3f 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', 'windowPicker.js')
+extension_sources += files('prefs.js', 'windowPicker.js', 'workspaceIndicator.js')
 extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')
 
 if classic_mode_enabled
diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
new file mode 100644
index 0000000..ac4037b
--- /dev/null
+++ b/extensions/window-list/workspaceIndicator.js
@@ -0,0 +1,127 @@
+/* exported WorkspaceIndicator */
+const { Clutter, Gio, GObject, Meta, St } = imports.gi;
+
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+const Gettext = imports.gettext.domain('gnome-shell-extensions');
+const _ = Gettext.gettext;
+
+var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
+    constructor() {
+        super(0.0, _('Workspace Indicator'), true);
+        this.setMenu(new PopupMenu.PopupMenu(this, 0.0, St.Side.BOTTOM));
+        this.actor.add_style_class_name('window-list-workspace-indicator');
+        this.menu.actor.remove_style_class_name('panel-menu');
+
+        let container = new St.Widget({
+            layout_manager: new Clutter.BinLayout(),
+            x_expand: true,
+            y_expand: true
+        });
+        this.actor.add_actor(container);
+
+        this._currentWorkspace = global.screen.get_active_workspace().index();
+        this.statusLabel = new St.Label({
+            text: this._getStatusText(),
+            x_align: Clutter.ActorAlign.CENTER,
+            y_align: Clutter.ActorAlign.CENTER
+        });
+        container.add_actor(this.statusLabel);
+
+        this.workspacesItems = [];
+
+        this._screenSignals = [
+            global.screen.connect('notify::n-workspaces',
+                this._updateMenu.bind(this)),
+            global.screen.connect_after('workspace-switched',
+                this._updateIndicator.bind(this))
+        ];
+
+        this.actor.connect('scroll-event', this._onScrollEvent.bind(this));
+        this._updateMenu();
+
+        this._settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.wm.preferences' });
+        this._settingsChangedId = this._settings.connect(
+            'changed::workspace-names', this._updateMenu.bind(this));
+    }
+
+    destroy() {
+        for (let i = 0; i < this._screenSignals.length; i++)
+            global.screen.disconnect(this._screenSignals[i]);
+
+        if (this._settingsChangedId) {
+            this._settings.disconnect(this._settingsChangedId);
+            this._settingsChangedId = 0;
+        }
+
+        super.destroy();
+    }
+
+    _updateIndicator() {
+        this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
+        this._currentWorkspace = global.screen.get_active_workspace().index();
+        this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
+
+        this.statusLabel.set_text(this._getStatusText());
+    }
+
+    _getStatusText() {
+        let current = global.screen.get_active_workspace().index();
+        let total = global.screen.n_workspaces;
+
+        return '%d / %d'.format(current + 1, total);
+    }
+
+    _updateMenu() {
+        this.menu.removeAll();
+        this.workspacesItems = [];
+        this._currentWorkspace = global.screen.get_active_workspace().index();
+
+        for (let i = 0; i < global.screen.n_workspaces; i++) {
+            let name = Meta.prefs_get_workspace_name(i);
+            let item = new PopupMenu.PopupMenuItem(name);
+            item.workspaceId = i;
+
+            item.connect('activate', (item, _event) => {
+                this._activate(item.workspaceId);
+            });
+
+            if (i == this._currentWorkspace)
+                item.setOrnament(PopupMenu.Ornament.DOT);
+
+            this.menu.addMenuItem(item);
+            this.workspacesItems[i] = item;
+        }
+
+        this.statusLabel.set_text(this._getStatusText());
+    }
+
+    _activate(index) {
+        if (index >= 0 && index < global.screen.n_workspaces) {
+            let metaWorkspace = global.screen.get_workspace_by_index(index);
+            metaWorkspace.activate(global.get_current_time());
+        }
+    }
+
+    _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 newIndex = this._currentWorkspace + diff;
+        this._activate(newIndex);
+    }
+
+    _allocate(actor, box, flags) {
+        if (actor.get_n_children() > 0)
+            actor.get_first_child().allocate(box, flags);
+    }
+};
+
-- 
2.21.0


From aa6079ff69f942420f806523463337fe81b23121 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 5 Jun 2019 04:54:50 +0200
Subject: [PATCH 15/33] window-list: Make some properties private

There's no reason why they should be public.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/70
---
 extensions/window-list/workspaceIndicator.js | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index ac4037b..cd3e394 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -22,14 +22,14 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         this.actor.add_actor(container);
 
         this._currentWorkspace = global.screen.get_active_workspace().index();
-        this.statusLabel = new St.Label({
+        this._statusLabel = new St.Label({
             text: this._getStatusText(),
             x_align: Clutter.ActorAlign.CENTER,
             y_align: Clutter.ActorAlign.CENTER
         });
-        container.add_actor(this.statusLabel);
+        container.add_actor(this._statusLabel);
 
-        this.workspacesItems = [];
+        this._workspacesItems = [];
 
         this._screenSignals = [
             global.screen.connect('notify::n-workspaces',
@@ -59,11 +59,11 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
     }
 
     _updateIndicator() {
-        this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
+        this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
         this._currentWorkspace = global.screen.get_active_workspace().index();
-        this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
+        this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
 
-        this.statusLabel.set_text(this._getStatusText());
+        this._statusLabel.set_text(this._getStatusText());
     }
 
     _getStatusText() {
@@ -75,7 +75,7 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
 
     _updateMenu() {
         this.menu.removeAll();
-        this.workspacesItems = [];
+        this._workspacesItems = [];
         this._currentWorkspace = global.screen.get_active_workspace().index();
 
         for (let i = 0; i < global.screen.n_workspaces; i++) {
@@ -91,10 +91,10 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
                 item.setOrnament(PopupMenu.Ornament.DOT);
 
             this.menu.addMenuItem(item);
-            this.workspacesItems[i] = item;
+            this._workspacesItems[i] = item;
         }
 
-        this.statusLabel.set_text(this._getStatusText());
+        this._statusLabel.set_text(this._getStatusText());
     }
 
     _activate(index) {
-- 
2.21.0


From 54f5bf585056267f63e2a28153ae709325afe9ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 5 Jun 2019 04:57:39 +0200
Subject: [PATCH 16/33] window-list: Update workspace names in-place

There's no good reason to rebuild the entire menu on workspace names
changes, we can simply update the labels in-place.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/70
---
 extensions/window-list/workspaceIndicator.js | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index cd3e394..5157e6e 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -43,7 +43,7 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
 
         this._settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.wm.preferences' });
         this._settingsChangedId = this._settings.connect(
-            'changed::workspace-names', this._updateMenu.bind(this));
+            'changed::workspace-names', this._updateMenuLabels.bind(this));
     }
 
     destroy() {
@@ -73,6 +73,14 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         return '%d / %d'.format(current + 1, total);
     }
 
+    _updateMenuLabels() {
+        for (let i = 0; i < this._workspacesItems.length; i++) {
+            let item = this._workspacesItems[i];
+            let name = Meta.prefs_get_workspace_name(i);
+            item.label.text = name;
+        }
+    }
+
     _updateMenu() {
         this.menu.removeAll();
         this._workspacesItems = [];
-- 
2.21.0


From a2fb1382b9ac7c50c556c1a41dc19ceca43bd320 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 5 Jun 2019 04:59:19 +0200
Subject: [PATCH 17/33] window-list: Minor cleanup

Mutter has a dedicated method for getting the index of the active
workspace, use that instead of getting first the active workspace
and then its index.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/70
---
 extensions/window-list/workspaceIndicator.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index 5157e6e..f9778f7 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -21,7 +21,7 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         });
         this.actor.add_actor(container);
 
-        this._currentWorkspace = global.screen.get_active_workspace().index();
+        this._currentWorkspace = global.screen.get_active_workspace_index();
         this._statusLabel = new St.Label({
             text: this._getStatusText(),
             x_align: Clutter.ActorAlign.CENTER,
@@ -60,14 +60,14 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
 
     _updateIndicator() {
         this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
-        this._currentWorkspace = global.screen.get_active_workspace().index();
+        this._currentWorkspace = global.screen.get_active_workspace_index();
         this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
 
         this._statusLabel.set_text(this._getStatusText());
     }
 
     _getStatusText() {
-        let current = global.screen.get_active_workspace().index();
+        let current = global.screen.get_active_workspace_index();
         let total = global.screen.n_workspaces;
 
         return '%d / %d'.format(current + 1, total);
@@ -84,7 +84,7 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
     _updateMenu() {
         this.menu.removeAll();
         this._workspacesItems = [];
-        this._currentWorkspace = global.screen.get_active_workspace().index();
+        this._currentWorkspace = global.screen.get_active_workspace_index();
 
         for (let i = 0; i < global.screen.n_workspaces; i++) {
             let name = Meta.prefs_get_workspace_name(i);
-- 
2.21.0


From fe93812275cc44f01a4c3877fc5efdb9121da78a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 5 Jun 2019 05:11:34 +0200
Subject: [PATCH 18/33] window-list: Improve workspace label styling

The border currently looks off - it extends all the way vertically
and leaves zero spacing to the label horizontally. Fix both issues
by setting appropriate padding/margins.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/70
---
 extensions/window-list/stylesheet.css        |  8 +++-----
 extensions/window-list/workspaceIndicator.js | 13 ++++++++-----
 2 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css
index 91383ab..bab8f76 100644
--- a/extensions/window-list/stylesheet.css
+++ b/extensions/window-list/stylesheet.css
@@ -85,13 +85,11 @@
   height: 24px;
 }
 
-.window-list-workspace-indicator {
-  padding: 3px;
-}
-
-.window-list-workspace-indicator > StWidget {
+.window-list-workspace-indicator .status-label-bin {
   background-color: rgba(200, 200, 200, .3);
   border: 1px solid #cccccc;
+  padding: 0 3px;
+  margin: 3px 0;
 }
 
 .notification {
diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index f9778f7..132d621 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -22,12 +22,15 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         this.actor.add_actor(container);
 
         this._currentWorkspace = global.screen.get_active_workspace_index();
-        this._statusLabel = new St.Label({
-            text: this._getStatusText(),
-            x_align: Clutter.ActorAlign.CENTER,
-            y_align: Clutter.ActorAlign.CENTER
+        this._statusLabel = new St.Label({ text: this._getStatusText() });
+
+        this._statusBin = new St.Bin({
+            style_class: 'status-label-bin',
+            x_expand: true,
+            y_expand: true,
+            child: this._statusLabel
         });
-        container.add_actor(this._statusLabel);
+        container.add_actor(this._statusBin);
 
         this._workspacesItems = [];
 
-- 
2.21.0


From 2edb23239c7d3c2120d59cfe74cba36674be515f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 5 Jun 2019 05:08:31 +0200
Subject: [PATCH 19/33] window-list: Refactor workspace signal handlers

We are about to support a separate representation if horizontal
workspaces are used. To prepare for that, rename the handlers to
something more generic and split out menu-specific bits into a
dedicated helper function.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/70
---
 extensions/window-list/workspaceIndicator.js | 22 +++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index 132d621..6352763 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -36,9 +36,9 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
 
         this._screenSignals = [
             global.screen.connect('notify::n-workspaces',
-                this._updateMenu.bind(this)),
+                this._nWorkspacesChanged.bind(this)),
             global.screen.connect_after('workspace-switched',
-                this._updateIndicator.bind(this))
+                this._onWorkspaceSwitched.bind(this))
         ];
 
         this.actor.connect('scroll-event', this._onScrollEvent.bind(this));
@@ -61,14 +61,26 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         super.destroy();
     }
 
-    _updateIndicator() {
-        this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
+    _onWorkspaceSwitched() {
         this._currentWorkspace = global.screen.get_active_workspace_index();
-        this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
+
+        this._updateMenuOrnament();
 
         this._statusLabel.set_text(this._getStatusText());
     }
 
+    _nWorkspacesChanged() {
+        this._updateMenu();
+    }
+
+    _updateMenuOrnament() {
+        for (let i = 0; i < this._workspacesItems.length; i++) {
+            this._workspacesItems[i].setOrnament(i == this._currentWorkspace
+                ? PopupMenu.Ornament.DOT
+                : PopupMenu.Ornament.NONE);
+        }
+    }
+
     _getStatusText() {
         let current = global.screen.get_active_workspace_index();
         let total = global.screen.n_workspaces;
-- 
2.21.0


From 1fa782c8ab1d3e69f99cbe0a68110a557d828c78 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 5 Jun 2019 02:53:38 +0000
Subject: [PATCH 20/33] window-list: Support horizontal workspace layout

Unlike in GNOME 2, the workspace indicator we display in the window list
isn't a workspace switcher, but a menu button that allows switching
workspaces via its menu. The reason for that is that a horizontal
in-place switcher would be at odds with the vertical workspace layout
used in GNOME 3.

However that reasoning doesn't apply when the layout is changed to a
horizontal one, so replace the button with a traditional workspace
switcher in that case.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/70
---
 extensions/window-list/classic.css           |  9 +++
 extensions/window-list/stylesheet.css        | 29 +++++++++
 extensions/window-list/workspaceIndicator.js | 63 +++++++++++++++++++-
 3 files changed, 100 insertions(+), 1 deletion(-)

diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css
index cc967e0..c533473 100644
--- a/extensions/window-list/classic.css
+++ b/extensions/window-list/classic.css
@@ -47,3 +47,12 @@
     color: #888;
     box-shadow: none;
   }
+
+/* workspace switcher */
+.window-list-workspace-indicator .workspace {
+  background-color: #ddd;
+}
+
+.window-list-workspace-indicator .workspace.active {
+  background-color: #ccc;
+}
diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css
index bab8f76..ad5978a 100644
--- a/extensions/window-list/stylesheet.css
+++ b/extensions/window-list/stylesheet.css
@@ -92,6 +92,35 @@
   margin: 3px 0;
 }
 
+.window-list-workspace-indicator .workspaces-box {
+  spacing: 3px;
+  padding: 3px;
+}
+
+.window-list-workspace-indicator .workspace {
+  border: 1px solid #cccccc;
+  width: 52px;
+}
+
+.window-list-workspace-indicator .workspace:first-child:last-child:ltr,
+.window-list-workspace-indicator .workspace:first-child:last-child:rtl {
+  border-radius: 4px;
+}
+
+.window-list-workspace-indicator .workspace:first-child:ltr,
+.window-list-workspace-indicator .workspace:last-child:rtl {
+  border-radius: 4px 0 0 4px;
+}
+
+.window-list-workspace-indicator .workspace:first-child:rtl,
+.window-list-workspace-indicator .workspace:last-child:ltr {
+  border-radius: 0 4px 4px 0;
+}
+
+.window-list-workspace-indicator .workspace.active {
+  background-color: rgba(200, 200, 200, .3);
+}
+
 .notification {
   font-weight: normal;
 }
diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index 6352763..77acf8a 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -7,6 +7,24 @@ const PopupMenu = imports.ui.popupMenu;
 const Gettext = imports.gettext.domain('gnome-shell-extensions');
 const _ = Gettext.gettext;
 
+let WorkspaceThumbnail = GObject.registerClass({
+    GTypeName: 'WindowListWorkspaceThumbnail'
+}, class WorkspaceThumbnail extends St.Button {
+    _init(index) {
+        super._init({
+            style_class: 'workspace'
+        });
+
+        this._index = index;
+    }
+
+    on_clicked() {
+        let ws = global.screen.get_workspace_by_index(this._index);
+        if (ws)
+            ws.activate(global.get_current_time());
+    }
+});
+
 var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
     constructor() {
         super(0.0, _('Workspace Indicator'), true);
@@ -32,17 +50,30 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         });
         container.add_actor(this._statusBin);
 
+        this._thumbnailsBox = new St.BoxLayout({
+            style_class: 'workspaces-box',
+            y_expand: true,
+            reactive: true
+        });
+        this._thumbnailsBox.connect('scroll-event',
+            this._onScrollEvent.bind(this));
+        container.add_actor(this._thumbnailsBox);
+
         this._workspacesItems = [];
 
         this._screenSignals = [
             global.screen.connect('notify::n-workspaces',
                 this._nWorkspacesChanged.bind(this)),
             global.screen.connect_after('workspace-switched',
-                this._onWorkspaceSwitched.bind(this))
+                this._onWorkspaceSwitched.bind(this)),
+            global.screen.connect('notify::layout-rows',
+                this._onWorkspaceOrientationChanged.bind(this))
         ];
 
         this.actor.connect('scroll-event', this._onScrollEvent.bind(this));
         this._updateMenu();
+        this._updateThumbnails();
+        this._onWorkspaceOrientationChanged();
 
         this._settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.wm.preferences' });
         this._settingsChangedId = this._settings.connect(
@@ -61,16 +92,26 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         super.destroy();
     }
 
+    _onWorkspaceOrientationChanged() {
+        let vertical = global.screen.layout_rows == -1;
+        this.reactive = vertical;
+
+        this._statusBin.visible = vertical;
+        this._thumbnailsBox.visible = !vertical;
+    }
+
     _onWorkspaceSwitched() {
         this._currentWorkspace = global.screen.get_active_workspace_index();
 
         this._updateMenuOrnament();
+        this._updateActiveThumbnail();
 
         this._statusLabel.set_text(this._getStatusText());
     }
 
     _nWorkspacesChanged() {
         this._updateMenu();
+        this._updateThumbnails();
     }
 
     _updateMenuOrnament() {
@@ -81,6 +122,16 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         }
     }
 
+    _updateActiveThumbnail() {
+        let thumbs = this._thumbnailsBox.get_children();
+        for (let i = 0; i < thumbs.length; i++) {
+            if (i == this._currentWorkspace)
+                thumbs[i].add_style_class_name('active');
+            else
+                thumbs[i].remove_style_class_name('active');
+        }
+    }
+
     _getStatusText() {
         let current = global.screen.get_active_workspace_index();
         let total = global.screen.n_workspaces;
@@ -120,6 +171,16 @@ var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
         this._statusLabel.set_text(this._getStatusText());
     }
 
+    _updateThumbnails() {
+        this._thumbnailsBox.destroy_all_children();
+
+        for (let i = 0; i < global.screen.n_workspaces; i++) {
+            let thumb = new WorkspaceThumbnail(i);
+            this._thumbnailsBox.add_actor(thumb);
+        }
+        this._updateActiveThumbnail();
+    }
+
     _activate(index) {
         if (index >= 0 && index < global.screen.n_workspaces) {
             let metaWorkspace = global.screen.get_workspace_by_index(index);
-- 
2.21.0


From 8ab04d7d1fe9807cdde949afa743de97539769cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Tue, 11 Jun 2019 23:01:20 +0000
Subject: [PATCH 21/33] window-list: Turn workspace thumbs into drop targets

It makes some sense to allow using the workspace indicator for moving
windows between workspaces as well as for workspace switching. This
applies particularly in GNOME classic after we disabled the overview
there, so that there is again a non-shortcut way of moving windows
between workspaces.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/74
---
 extensions/window-list/workspaceIndicator.js | 27 ++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index 77acf8a..edf176e 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -1,6 +1,8 @@
 /* exported WorkspaceIndicator */
 const { Clutter, Gio, GObject, Meta, St } = imports.gi;
 
+const DND = imports.ui.dnd;
+const Main = imports.ui.main;
 const PanelMenu = imports.ui.panelMenu;
 const PopupMenu = imports.ui.popupMenu;
 
@@ -16,6 +18,31 @@ let WorkspaceThumbnail = GObject.registerClass({
         });
 
         this._index = index;
+        this._delegate = this; // needed for DND
+    }
+
+    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;
+    }
+
+
+    _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() {
-- 
2.21.0


From a4d0ccc3122891f9e164b2b45c9540dbdebba0de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 26 Jun 2019 23:55:58 +0000
Subject: [PATCH 22/33] window-list: 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/74
---
 extensions/window-list/classic.css           |  10 ++
 extensions/window-list/stylesheet.css        |  10 ++
 extensions/window-list/workspaceIndicator.js | 153 ++++++++++++++++++-
 3 files changed, 172 insertions(+), 1 deletion(-)

diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css
index c533473..7079d3e 100644
--- a/extensions/window-list/classic.css
+++ b/extensions/window-list/classic.css
@@ -56,3 +56,13 @@
 .window-list-workspace-indicator .workspace.active {
   background-color: #ccc;
 }
+
+.window-list-window-preview {
+  background-color: #ededed;
+  border: 1px solid #ccc;
+}
+
+.window-list-window-preview.active {
+  background-color: #f6f5f4;
+  border: 2px solid #888;
+}
diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css
index ad5978a..79d56ba 100644
--- a/extensions/window-list/stylesheet.css
+++ b/extensions/window-list/stylesheet.css
@@ -121,6 +121,16 @@
   background-color: rgba(200, 200, 200, .3);
 }
 
+.window-list-window-preview {
+  background-color: #252525;
+  border: 1px solid #ccc;
+}
+
+.window-list-window-preview.active {
+  background-color: #353535;
+  border: 2px solid #ccc;
+}
+
 .notification {
   font-weight: normal;
 }
diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index edf176e..0bcee80 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -9,16 +9,130 @@ const PopupMenu = imports.ui.popupMenu;
 const Gettext = imports.gettext.domain('gnome-shell-extensions');
 const _ = Gettext.gettext;
 
+let WindowPreview = GObject.registerClass({
+    GTypeName: 'WindowListWindowPreview'
+}, class WindowPreview extends St.Button {
+    _init(window) {
+        super._init({
+            style_class: 'window-list-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.screen.connect('window-entered-monitor',
+            this._relayout.bind(this));
+        this._monitorLeftId = global.screen.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.screen.disconnect(this._monitorEnteredId);
+        global.screen.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: 'WindowListWorkspaceThumbnail'
 }, class WorkspaceThumbnail extends St.Button {
     _init(index) {
         super._init({
-            style_class: 'workspace'
+            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();
+
+        this._workspace = global.screen.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.screen.connect('restacked',
+            this._onRestacked.bind(this));
+
+        this._workspace.list_windows().forEach(w => this._addWindow(w));
+        this._onRestacked();
     }
 
     acceptDrop(source) {
@@ -37,6 +151,37 @@ let WorkspaceThumbnail = GObject.registerClass({
             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);
@@ -50,6 +195,12 @@ let WorkspaceThumbnail = GObject.registerClass({
         if (ws)
             ws.activate(global.get_current_time());
     }
+
+    _onDestroy() {
+        this._workspace.disconnect(this._windowAddedId);
+        this._workspace.disconnect(this._windowRemovedId);
+        global.screen.disconnect(this._restackedId);
+    }
 });
 
 var WorkspaceIndicator = class WorkspaceIndicator extends PanelMenu.Button {
-- 
2.21.0


From 73e31ec6a601b2f692cf58b5377a1d92fce8d5ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Wed, 5 Jun 2019 03:31:16 +0000
Subject: [PATCH 23/33] classic: Add 'horizontal-workspaces' extension

Vertical workspaces are another defining characteristics of GNOME 3,
and thus rather un-classic. That switch was driven by the overall
layout of the overview, and now that we disable the overview in
GNOME Classic, we can just return to the traditional workspace
layout as well.

Add a small extension that does just that.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/72
---
 extensions/horizontal-workspaces/extension.js | 38 +++++++++++++++++++
 extensions/horizontal-workspaces/meson.build  |  5 +++
 .../horizontal-workspaces/metadata.json.in    | 10 +++++
 .../horizontal-workspaces/stylesheet.css      |  1 +
 meson.build                                   |  1 +
 5 files changed, 55 insertions(+)
 create mode 100644 extensions/horizontal-workspaces/extension.js
 create mode 100644 extensions/horizontal-workspaces/meson.build
 create mode 100644 extensions/horizontal-workspaces/metadata.json.in
 create mode 100644 extensions/horizontal-workspaces/stylesheet.css

diff --git a/extensions/horizontal-workspaces/extension.js b/extensions/horizontal-workspaces/extension.js
new file mode 100644
index 0000000..bade48b
--- /dev/null
+++ b/extensions/horizontal-workspaces/extension.js
@@ -0,0 +1,38 @@
+/* exported init */
+const { Meta } = imports.gi;
+
+const { ThumbnailsBox } = imports.ui.workspaceThumbnail;
+
+class Extension {
+    constructor() {
+        this._origUpdateSwitcherVisibility =
+            ThumbnailsBox.prototype._updateSwitcherVisibility;
+    }
+
+    enable() {
+        global.screen.override_workspace_layout(
+            Meta.ScreenCorner.TOPLEFT,
+            false,
+            1,
+            -1);
+
+        ThumbnailsBox.prototype._updateSwitcherVisibility = function() {
+            this.hide();
+        };
+    }
+
+    disable() {
+        global.screen.override_workspace_layout(
+            Meta.ScreenCorner.TOPLEFT,
+            false,
+            -1,
+            1);
+
+        ThumbnailsBox.prototype._updateSwitcherVisibility =
+            this._origUpdateSwitcherVisibility;
+    }
+}
+
+function init() {
+    return new Extension();
+}
diff --git a/extensions/horizontal-workspaces/meson.build b/extensions/horizontal-workspaces/meson.build
new file mode 100644
index 0000000..48504f6
--- /dev/null
+++ b/extensions/horizontal-workspaces/meson.build
@@ -0,0 +1,5 @@
+extension_data += configure_file(
+  input: metadata_name + '.in',
+  output: metadata_name,
+  configuration: metadata_conf
+)
diff --git a/extensions/horizontal-workspaces/metadata.json.in b/extensions/horizontal-workspaces/metadata.json.in
new file mode 100644
index 0000000..f109e06
--- /dev/null
+++ b/extensions/horizontal-workspaces/metadata.json.in
@@ -0,0 +1,10 @@
+{
+"extension-id": "@extension_id@",
+"uuid": "@uuid@",
+"settings-schema": "@gschemaname@",
+"gettext-domain": "@gettext_domain@",
+"name": "Horizontal workspaces",
+"description": "Use a horizontal workspace layout",
+"shell-version": [ "@shell_current@" ],
+"url": "@url@"
+}
diff --git a/extensions/horizontal-workspaces/stylesheet.css b/extensions/horizontal-workspaces/stylesheet.css
new file mode 100644
index 0000000..25134b6
--- /dev/null
+++ b/extensions/horizontal-workspaces/stylesheet.css
@@ -0,0 +1 @@
+/* This extensions requires no special styling */
diff --git a/meson.build b/meson.build
index fa4aa9c..c1581a5 100644
--- a/meson.build
+++ b/meson.build
@@ -36,6 +36,7 @@ uuid_suffix = '@gnome-shell-extensions.gcampax.github.com'
 classic_extensions = [
   'alternate-tab',
   'apps-menu',
+  'horizontal-workspaces',
   'places-menu',
   'launch-new-instance',
   'top-icons',
-- 
2.21.0


From 1269e9226cf93903957acc58029f4db3e095d5ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Sat, 29 Jun 2019 01:24:54 +0200
Subject: [PATCH 24/33] workspace-indicator: Fix whitespace error

We only want a single space before and after operators, not at least
one. Unfortunately eslint only enforces the latter ...

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/71
---
 extensions/workspace-indicator/extension.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index ace1703..f6d9912 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -106,7 +106,7 @@ class WorkspaceIndicator extends PanelMenu.Button {
     }
 
     _activate(index) {
-        if(index >= 0 && index <  global.screen.n_workspaces) {
+        if(index >= 0 && index < global.screen.n_workspaces) {
             let metaWorkspace = global.screen.get_workspace_by_index(index);
             metaWorkspace.activate(global.get_current_time());
         }
-- 
2.21.0


From fee1399f6862ca99ed063b795fc3014b0d18414f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Sun, 9 Jun 2019 22:58:29 +0000
Subject: [PATCH 25/33] workspace-indicator: Make some properties private

There's no reason why they should be public.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/71
---
 extensions/workspace-indicator/extension.js | 35 +++++++++++----------
 1 file changed, 18 insertions(+), 17 deletions(-)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index f6d9912..67dec14 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -26,12 +26,14 @@ class WorkspaceIndicator extends PanelMenu.Button {
         super(0.0, _("Workspace Indicator"));
 
         this._currentWorkspace = global.screen.get_active_workspace().index();
-        this.statusLabel = new St.Label({ y_align: Clutter.ActorAlign.CENTER,
-                                          text: this._labelText() });
+        this._statusLabel = new St.Label({
+            y_align: Clutter.ActorAlign.CENTER,
+            text: this._labelText()
+        });
 
-        this.actor.add_actor(this.statusLabel);
+        this.add_actor(this._statusLabel);
 
-        this.workspacesItems = [];
+        this._workspacesItems = [];
         this._workspaceSection = new PopupMenu.PopupMenuSection();
         this.menu.addMenuItem(this._workspaceSection);
 
@@ -46,7 +48,7 @@ class WorkspaceIndicator extends PanelMenu.Button {
         this._createWorkspacesSection();
 
         //styling
-        this.statusLabel.add_style_class_name('panel-workspace-indicator');
+        this._statusLabel.add_style_class_name('panel-workspace-indicator');
 
         this._settings = new Gio.Settings({ schema_id: WORKSPACE_SCHEMA });
         this._settingsChangedId =
@@ -67,11 +69,11 @@ class WorkspaceIndicator extends PanelMenu.Button {
     }
 
     _updateIndicator() {
-        this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
+        this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
         this._currentWorkspace = global.screen.get_active_workspace().index();
-        this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
+        this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
 
-        this.statusLabel.set_text(this._labelText());
+        this._statusLabel.set_text(this._labelText());
     }
 
     _labelText(workspaceIndex) {
@@ -84,25 +86,24 @@ class WorkspaceIndicator extends PanelMenu.Button {
 
     _createWorkspacesSection() {
         this._workspaceSection.removeAll();
-        this.workspacesItems = [];
+        this._workspacesItems = [];
         this._currentWorkspace = global.screen.get_active_workspace().index();
 
         let i = 0;
         for(; i < global.screen.n_workspaces; i++) {
-            this.workspacesItems[i] = new PopupMenu.PopupMenuItem(this._labelText(i));
-            this._workspaceSection.addMenuItem(this.workspacesItems[i]);
-            this.workspacesItems[i].workspaceId = i;
-            this.workspacesItems[i].label_actor = this.statusLabel;
-            let self = this;
-            this.workspacesItems[i].connect('activate', (actor, event) => {
+            this._workspacesItems[i] = new PopupMenu.PopupMenuItem(this._labelText(i));
+            this._workspaceSection.addMenuItem(this._workspacesItems[i]);
+            this._workspacesItems[i].workspaceId = i;
+            this._workspacesItems[i].label_actor = this._statusLabel;
+            this._workspacesItems[i].connect('activate', (actor, _event) => {
                 this._activate(actor.workspaceId);
             });
 
             if (i == this._currentWorkspace)
-                this.workspacesItems[i].setOrnament(PopupMenu.Ornament.DOT);
+                this._workspacesItems[i].setOrnament(PopupMenu.Ornament.DOT);
         }
 
-        this.statusLabel.set_text(this._labelText());
+        this._statusLabel.set_text(this._labelText());
     }
 
     _activate(index) {
-- 
2.21.0


From 9f8beea12da86dfa0540fc022ef36de0fa67a932 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Sun, 9 Jun 2019 23:03:55 +0000
Subject: [PATCH 26/33] workspace-indicator: Update workspace names in-place

There's no good reason to rebuild the entire menu on workspace names
changes, we can simply update the labels in-place.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/71
---
 extensions/workspace-indicator/extension.js | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index 67dec14..3befe14 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -51,9 +51,9 @@ class WorkspaceIndicator extends PanelMenu.Button {
         this._statusLabel.add_style_class_name('panel-workspace-indicator');
 
         this._settings = new Gio.Settings({ schema_id: WORKSPACE_SCHEMA });
-        this._settingsChangedId =
-            this._settings.connect('changed::' + WORKSPACE_KEY,
-                                   this._createWorkspacesSection.bind(this));
+        this._settingsChangedId = this._settings.connect(
+            `changed::${WORKSPACE_KEY}`,
+            this._updateMenuLabels.bind(this));
     }
 
     destroy() {
@@ -84,6 +84,11 @@ class WorkspaceIndicator extends PanelMenu.Button {
         return Meta.prefs_get_workspace_name(workspaceIndex);
     }
 
+    _updateMenuLabels() {
+        for (let i = 0; i < this._workspacesItems.length; i++)
+            this._workspacesItems[i].label.text = this._labelText(i);
+    }
+
     _createWorkspacesSection() {
         this._workspaceSection.removeAll();
         this._workspacesItems = [];
-- 
2.21.0


From 2d2baa3981a3d061c7b32d57ad07da9a3553dfa3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Sun, 9 Jun 2019 23:05:00 +0000
Subject: [PATCH 27/33] workspace-indicator: Minor cleanup

Mutter has a dedicated method for getting the index of the active
workspace, use that instead of getting first the active workspace
and then its index.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/71
---
 extensions/workspace-indicator/extension.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index 3befe14..d341b93 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -25,7 +25,7 @@ class WorkspaceIndicator extends PanelMenu.Button {
     constructor() {
         super(0.0, _("Workspace Indicator"));
 
-        this._currentWorkspace = global.screen.get_active_workspace().index();
+        this._currentWorkspace = global.screen.get_active_workspace_index();
         this._statusLabel = new St.Label({
             y_align: Clutter.ActorAlign.CENTER,
             text: this._labelText()
@@ -70,7 +70,7 @@ class WorkspaceIndicator extends PanelMenu.Button {
 
     _updateIndicator() {
         this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
-        this._currentWorkspace = global.screen.get_active_workspace().index();
+        this._currentWorkspace = global.screen.get_active_workspace_index();
         this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
 
         this._statusLabel.set_text(this._labelText());
@@ -92,7 +92,7 @@ class WorkspaceIndicator extends PanelMenu.Button {
     _createWorkspacesSection() {
         this._workspaceSection.removeAll();
         this._workspacesItems = [];
-        this._currentWorkspace = global.screen.get_active_workspace().index();
+        this._currentWorkspace = global.screen.get_active_workspace_index();
 
         let i = 0;
         for(; i < global.screen.n_workspaces; i++) {
@@ -129,7 +129,7 @@ class WorkspaceIndicator extends PanelMenu.Button {
             return;
         }
 
-        let newIndex = global.screen.get_active_workspace().index() + diff;
+        let newIndex = global.screen.get_active_workspace_index() + diff;
         this._activate(newIndex);
     }
 };
-- 
2.21.0


From 7c3dfb2c2d5713f84a11d59dba9e34e50a51e204 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Sun, 9 Jun 2019 23:09:12 +0000
Subject: [PATCH 28/33] workspace-indicator: Refactor workspace signal handlers

We are about to support a separate representation if horizontal
workspaces are used. To prepare for that, rename the handlers to
something more generic and split out menu-specific bits into a
dedicated helper function.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/71
---
 extensions/workspace-indicator/extension.js | 30 ++++++++++++++-------
 1 file changed, 21 insertions(+), 9 deletions(-)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index d341b93..58ac865 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -37,12 +37,12 @@ class WorkspaceIndicator extends PanelMenu.Button {
         this._workspaceSection = new PopupMenu.PopupMenuSection();
         this.menu.addMenuItem(this._workspaceSection);
 
-        this._screenSignals = [];
-        this._screenSignals.push(global.screen.connect_after('workspace-added',                                                              this._createWorkspacesSection.bind(this)));
-        this._screenSignals.push(global.screen.connect_after('workspace-removed',
-                                                             this._createWorkspacesSection.bind(this)));
-        this._screenSignals.push(global.screen.connect_after('workspace-switched',
-                                                             this._updateIndicator.bind(this)));
+        this._screenSignals = [
+            global.screen.connect_after('notify::n-workspaces',
+                this._nWorkspacesChanged.bind(this)),
+            global.screen.connect_after('workspace-switched',
+                this._onWorkspaceSwitched.bind(this))
+        ];
 
         this.actor.connect('scroll-event', this._onScrollEvent.bind(this));
         this._createWorkspacesSection();
@@ -68,14 +68,26 @@ class WorkspaceIndicator extends PanelMenu.Button {
         super.destroy();
     }
 
-    _updateIndicator() {
-        this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
+    _onWorkspaceSwitched() {
         this._currentWorkspace = global.screen.get_active_workspace_index();
-        this._workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
+
+        this._updateMenuOrnament();
 
         this._statusLabel.set_text(this._labelText());
     }
 
+    _nWorkspacesChanged() {
+        this._createWorkspacesSection();
+    }
+
+    _updateMenuOrnament() {
+        for (let i = 0; i < this._workspacesItems.length; i++) {
+            this._workspacesItems[i].setOrnament(i == this._currentWorkspace
+                ? PopupMenu.Ornament.DOT
+                : PopupMenu.Ornament.NONE);
+        }
+    }
+
     _labelText(workspaceIndex) {
         if(workspaceIndex == undefined) {
             workspaceIndex = this._currentWorkspace;
-- 
2.21.0


From e5b8c870ff750973d615eb801e15b56ad8f614db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Sun, 9 Jun 2019 23:17:35 +0000
Subject: [PATCH 29/33] workspace-indicator: Minor cleanup

Pass the style class at construction time instead of setting it later.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/71
---
 extensions/workspace-indicator/extension.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index 58ac865..e6fffe4 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -27,6 +27,7 @@ class WorkspaceIndicator extends PanelMenu.Button {
 
         this._currentWorkspace = global.screen.get_active_workspace_index();
         this._statusLabel = new St.Label({
+            style_class: 'panel-workspace-indicator',
             y_align: Clutter.ActorAlign.CENTER,
             text: this._labelText()
         });
@@ -47,9 +48,6 @@ class WorkspaceIndicator extends PanelMenu.Button {
         this.actor.connect('scroll-event', this._onScrollEvent.bind(this));
         this._createWorkspacesSection();
 
-        //styling
-        this._statusLabel.add_style_class_name('panel-workspace-indicator');
-
         this._settings = new Gio.Settings({ schema_id: WORKSPACE_SCHEMA });
         this._settingsChangedId = this._settings.connect(
             `changed::${WORKSPACE_KEY}`,
-- 
2.21.0


From cce0e2b739cab74a166cfa75631836a7bfd06429 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Sun, 9 Jun 2019 23:45:24 +0000
Subject: [PATCH 30/33] workspace-indicator: Support horizontal workspace
 layout

Just like we did for the workspace indicator in the window-list, improve
the handling of horizontal workspace layouts by showing the switcher
in-place instead of delegating the functionality to a menu.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/71
---
 extensions/workspace-indicator/extension.js   | 74 ++++++++++++++++++-
 extensions/workspace-indicator/stylesheet.css | 27 ++++++-
 2 files changed, 98 insertions(+), 3 deletions(-)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index e6fffe4..32bb10f 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -1,6 +1,7 @@
 // -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
 
 const Gio = imports.gi.Gio;
+const GObject = imports.gi.GObject;
 const Meta = imports.gi.Meta;
 const Clutter = imports.gi.Clutter;
 const St = imports.gi.St;
@@ -21,10 +22,36 @@ const Convenience = Me.imports.convenience;
 const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences';
 const WORKSPACE_KEY = 'workspace-names';
 
+let WorkspaceThumbnail = GObject.registerClass({
+    GTypeName: 'WorkspaceIndicatorWorkspaceThumbnail'
+}, class WorkspaceThumbnail extends St.Button {
+    _init(index) {
+        super._init({
+            style_class: 'workspace',
+        });
+
+        this._index = index;
+    }
+
+    on_clicked() {
+        let ws = global.screen.get_workspace_by_index(this._index);
+        if (ws)
+            ws.activate(global.get_current_time());
+    }
+});
+
+
 class WorkspaceIndicator extends PanelMenu.Button {
     constructor() {
         super(0.0, _("Workspace Indicator"));
 
+        let container = new St.Widget({
+            layout_manager: new Clutter.BinLayout(),
+            x_expand: true,
+            y_expand: true
+        });
+        this.actor.add_actor(container);
+
         this._currentWorkspace = global.screen.get_active_workspace_index();
         this._statusLabel = new St.Label({
             style_class: 'panel-workspace-indicator',
@@ -32,7 +59,15 @@ class WorkspaceIndicator extends PanelMenu.Button {
             text: this._labelText()
         });
 
-        this.add_actor(this._statusLabel);
+        container.add_actor(this._statusLabel);
+
+        this._thumbnailsBox = new St.BoxLayout({
+            style_class: 'panel-workspace-indicator-box',
+            y_expand: true,
+            reactive: true
+        });
+
+        container.add_actor(this._thumbnailsBox);
 
         this._workspacesItems = [];
         this._workspaceSection = new PopupMenu.PopupMenuSection();
@@ -42,11 +77,16 @@ class WorkspaceIndicator extends PanelMenu.Button {
             global.screen.connect_after('notify::n-workspaces',
                 this._nWorkspacesChanged.bind(this)),
             global.screen.connect_after('workspace-switched',
-                this._onWorkspaceSwitched.bind(this))
+                this._onWorkspaceSwitched.bind(this)),
+            global.screen.connect('notify::layout-rows',
+                this._onWorkspaceOrientationChanged.bind(this))
         ];
 
         this.actor.connect('scroll-event', this._onScrollEvent.bind(this));
+        this._thumbnailsBox.connect('scroll-event', this._onScrollEvent.bind(this));
         this._createWorkspacesSection();
+        this._updateThumbnails();
+        this._onWorkspaceOrientationChanged();
 
         this._settings = new Gio.Settings({ schema_id: WORKSPACE_SCHEMA });
         this._settingsChangedId = this._settings.connect(
@@ -66,16 +106,26 @@ class WorkspaceIndicator extends PanelMenu.Button {
         super.destroy();
     }
 
+    _onWorkspaceOrientationChanged() {
+        let vertical = global.screen.layout_rows == -1;
+        this.actor.reactive = vertical;
+
+        this._statusLabel.visible = vertical;
+        this._thumbnailsBox.visible = !vertical;
+    }
+
     _onWorkspaceSwitched() {
         this._currentWorkspace = global.screen.get_active_workspace_index();
 
         this._updateMenuOrnament();
+        this._updateActiveThumbnail();
 
         this._statusLabel.set_text(this._labelText());
     }
 
     _nWorkspacesChanged() {
         this._createWorkspacesSection();
+        this._updateThumbnails();
     }
 
     _updateMenuOrnament() {
@@ -86,6 +136,16 @@ class WorkspaceIndicator extends PanelMenu.Button {
         }
     }
 
+    _updateActiveThumbnail() {
+        let thumbs = this._thumbnailsBox.get_children();
+        for (let i = 0; i < thumbs.length; i++) {
+            if (i == this._currentWorkspace)
+                thumbs[i].add_style_class_name('active');
+            else
+                thumbs[i].remove_style_class_name('active');
+        }
+    }
+
     _labelText(workspaceIndex) {
         if(workspaceIndex == undefined) {
             workspaceIndex = this._currentWorkspace;
@@ -121,6 +181,16 @@ class WorkspaceIndicator extends PanelMenu.Button {
         this._statusLabel.set_text(this._labelText());
     }
 
+    _updateThumbnails() {
+        this._thumbnailsBox.destroy_all_children();
+
+        for (let i = 0; i < global.screen.n_workspaces; i++) {
+            let thumb = new WorkspaceThumbnail(i);
+            this._thumbnailsBox.add_actor(thumb);
+        }
+        this._updateActiveThumbnail();
+    }
+
     _activate(index) {
         if(index >= 0 && index < global.screen.n_workspaces) {
             let metaWorkspace = global.screen.get_workspace_by_index(index);
diff --git a/extensions/workspace-indicator/stylesheet.css b/extensions/workspace-indicator/stylesheet.css
index 1271f1c..5118194 100644
--- a/extensions/workspace-indicator/stylesheet.css
+++ b/extensions/workspace-indicator/stylesheet.css
@@ -1,5 +1,30 @@
 .panel-workspace-indicator {
 	padding: 0 8px;
-	background-color: rgba(200, 200, 200, .5);
+}
+
+.panel-workspace-indicator-box {
+	padding: 2px 0;
+}
+
+.panel-workspace-indicator-box .workspace {
+	width: 40px;
+}
+
+.panel-workspace-indicator,
+.panel-workspace-indicator-box .workspace {
 	border: 1px solid #cccccc;
 }
+
+.panel-workspace-indicator,
+.panel-workspace-indicator-box .workspace.active {
+	background-color: rgba(200, 200, 200, .5);
+}
+
+.panel-workspace-indicator-box .workspace {
+    background-color: rgba(200, 200, 200, .3);
+    border-left-width: 0;
+}
+
+.panel-workspace-indicator-box .workspace:first-child {
+    border-left-width: 1px;
+}
-- 
2.21.0


From 98df229b8a62e575529ebc28e085df53154ebc42 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Fri, 28 Jun 2019 11:33:16 +0200
Subject: [PATCH 31/33] 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   | 179 +++++++++++++++++-
 extensions/workspace-indicator/stylesheet.css |  10 +
 2 files changed, 188 insertions(+), 1 deletion(-)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index 32bb10f..6b656c8 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -6,6 +6,8 @@ const Meta = imports.gi.Meta;
 const Clutter = imports.gi.Clutter;
 const St = imports.gi.St;
 const Mainloop = imports.mainloop;
+
+const DND = imports.ui.dnd;
 const PanelMenu = imports.ui.panelMenu;
 const PopupMenu = imports.ui.popupMenu;
 const Panel = imports.ui.panel;
@@ -22,15 +24,185 @@ const Convenience = Me.imports.convenience;
 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.screen.connect('window-entered-monitor',
+            this._relayout.bind(this));
+        this._monitorLeftId = global.screen.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.screen.disconnect(this._monitorEnteredId);
+        global.screen.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();
+
+        this._workspace = global.screen.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.screen.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() {
@@ -38,8 +210,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.screen.disconnect(this._restackedId);
+    }
+});
 
 class WorkspaceIndicator extends PanelMenu.Button {
     constructor() {
diff --git a/extensions/workspace-indicator/stylesheet.css b/extensions/workspace-indicator/stylesheet.css
index 5118194..8601c3e 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;
+}
-- 
2.21.0


From 9b1fb384f2f46a953c4838991ec58cd3c2872b31 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Fri, 6 Sep 2019 20:41:23 +0200
Subject: [PATCH 32/33] window-list: Exclude DESKTOP windows from window
 previews

While nautilus removed its desktop support a while ago in favor of an
extension, it's still possible that some external X11 desktop icon app
is used. As DESKTOP windows cannot be moved between workspaces or stacked,
and aren't perceived as regular windows, it doesn't make sense to show
them as previews in the workspace switcher.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/93
---
 extensions/window-list/workspaceIndicator.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js
index 0bcee80..c0dd65d 100644
--- a/extensions/window-list/workspaceIndicator.js
+++ b/extensions/window-list/workspaceIndicator.js
@@ -78,6 +78,7 @@ let WindowPreview = GObject.registerClass({
     _relayout() {
         let monitor = Main.layoutManager.findIndexForActor(this);
         this.visible = monitor == this._window.get_monitor() &&
+            this._window.window_type !== Meta.WindowType.DESKTOP &&
             this._window.showing_on_its_workspace();
 
         if (!this.visible)
-- 
2.21.0


From 59d4e3377072507f95562c8516913b1d257651c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
Date: Fri, 6 Sep 2019 20:47:56 +0200
Subject: [PATCH 33/33] workspace-indicator: Exclude DESKTOP windows from
 window previews

While nautilus removed its desktop support a while ago in favor of an
extension, it's still possible that some external X11 desktop icon app
is used. As DESKTOP windows cannot be moved between workspaces or stacked,
and aren't perceived as regular windows, it doesn't make sense to show
them as previews in the workspace switcher.

https://gitlab.gnome.org/GNOME/gnome-shell-extensions/merge_requests/93
---
 extensions/workspace-indicator/extension.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js
index 6b656c8..6b7947d 100644
--- a/extensions/workspace-indicator/extension.js
+++ b/extensions/workspace-indicator/extension.js
@@ -93,6 +93,7 @@ let WindowPreview = GObject.registerClass({
     _relayout() {
         let monitor = Main.layoutManager.findIndexForActor(this);
         this.visible = monitor == this._window.get_monitor() &&
+            this._window.window_type !== Meta.WindowType.DESKTOP &&
             this._window.showing_on_its_workspace();
 
         if (!this.visible)
-- 
2.21.0