From a4fe08d53d42e7c996656683e6633ceefb644358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Thu, 24 Jan 2013 20:13:52 +0100 Subject: [PATCH] window-list: Add option for grouping windows by application https://bugzilla.gnome.org/show_bug.cgi?id=693171 --- extensions/window-list/Makefile.am | 1 + extensions/window-list/extension.js | 228 +++++++++++++++++- ...hell.extensions.window-list.gschema.xml.in | 18 ++ po/POTFILES.in | 1 + 4 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml.in diff --git a/extensions/window-list/Makefile.am b/extensions/window-list/Makefile.am index 48a10284..42470a90 100644 --- a/extensions/window-list/Makefile.am +++ b/extensions/window-list/Makefile.am @@ -1,3 +1,4 @@ EXTENSION_ID = window-list include ../../extension.mk +include ../../settings.mk diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js index 9cfa4421..fd08aec0 100644 --- a/extensions/window-list/extension.js +++ b/extensions/window-list/extension.js @@ -1,4 +1,5 @@ const Clutter = imports.gi.Clutter; +const Gtk = imports.gi.Gtk; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const St = imports.gi.St; @@ -7,6 +8,16 @@ const Hash = imports.misc.hash; const Lang = imports.lang; const Main = imports.ui.main; const MessageTray = imports.ui.messageTray; +const PopupMenu = imports.ui.popupMenu; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = ExtensionUtils.getCurrentExtension(); +const Convenience = Me.imports.convenience; + +const GroupingMode = { + NEVER: 0, + ALWAYS: 1 +}; function _minimizeOrActivateWindow(window) { @@ -130,6 +141,151 @@ const WindowButton = new Lang.Class({ }); +const AppButton = new Lang.Class({ + Name: 'AppButton', + + _init: function(app) { + this.app = app; + + let stack = new St.Widget({ layout_manager: new Clutter.BinLayout() }); + this.actor = new St.Button({ style_class: 'window-button', + x_fill: true, + can_focus: true, + child: stack }); + this.actor._delegate = this; + + this.actor.connect('allocation-changed', + Lang.bind(this, this._updateIconGeometry)); + + this._singleWindowTitle = new St.Bin({ x_expand: true, + x_align: St.Align.START }); + stack.add_actor(this._singleWindowTitle); + + this._multiWindowTitle = new St.BoxLayout({ x_expand: true }); + stack.add_actor(this._multiWindowTitle); + + let icon = new St.Bin({ style_class: 'window-button-icon', + child: app.create_icon_texture(24) }); + this._multiWindowTitle.add(icon); + this._multiWindowTitle.add(new St.Label({ text: app.get_name() })); + + this._menuManager = new PopupMenu.PopupMenuManager(this); + this._menu = new PopupMenu.PopupMenu(this.actor, 0.5, St.Side.BOTTOM); + this._menu.actor.hide(); + this._menu.connect('activate', Lang.bind(this, this._onMenuActivate)); + this._menuManager.addMenu(this._menu); + Main.uiGroup.add_actor(this._menu.actor); + + this.actor.connect('clicked', Lang.bind(this, this._onClicked)); + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); + + this._switchWorkspaceId = + global.window_manager.connect('switch-workspace', + Lang.bind(this, this._updateVisibility)); + this._updateVisibility(); + + this._windowsChangedId = + this.app.connect('windows-changed', + Lang.bind(this, this._windowsChanged)); + this._windowsChanged(); + + this._windowTracker = Shell.WindowTracker.get_default(); + this._notifyFocusId = + this._windowTracker.connect('notify::focus-app', + Lang.bind(this, this._updateStyle)); + this._updateStyle(); + }, + + _updateVisibility: function() { + let workspace = global.screen.get_active_workspace(); + this.actor.visible = this.app.is_on_workspace(workspace); + }, + + _updateStyle: function() { + if (this._windowTracker.focus_app == this.app) + this.actor.add_style_class_name('focused'); + else + this.actor.remove_style_class_name('focused'); + }, + + _updateIconGeometry: function() { + let rect = new Meta.Rectangle(); + + [rect.x, rect.y] = this.actor.get_transformed_position(); + [rect.width, rect.height] = this.actor.get_transformed_size(); + + let windows = this.app.get_windows(); + windows.forEach(function(w) { + w.set_icon_geometry(rect); + }); + }, + + + _getWindowList: function() { + let workspace = global.screen.get_active_workspace(); + return this.app.get_windows().filter(function(win) { + return win.located_on_workspace(workspace); + }); + }, + + _windowsChanged: function() { + let windows = this._getWindowList(); + this._singleWindowTitle.visible = windows.length == 1; + this._multiWindowTitle.visible = !this._singleWindowTitle.visible; + + if (this._singleWindowTitle.visible) { + if (!this._windowTitle) { + this._windowTitle = new WindowTitle(windows[0]); + this._singleWindowTitle.child = this._windowTitle.actor; + } + } else { + if (this._windowTitle) { + this._singleWindowTitle.child = null; + this._windowTitle = null; + } + } + }, + + _onClicked: function() { + if (this._menu.isOpen) { + this._menu.close(); + return; + } + + let windows = this._getWindowList(); + if (windows.length == 1) { + _minimizeOrActivateWindow(windows[0]); + } else { + this._menu.removeAll(); + + for (let i = 0; i < windows.length; i++) { + let windowTitle = new WindowTitle(windows[i]); + let item = new PopupMenu.PopupBaseMenuItem(); + item.addActor(windowTitle.actor); + item._window = windows[i]; + this._menu.addMenuItem(item); + } + this._menu.open(); + + let event = Clutter.get_current_event(); + if (event && event.type() == Clutter.EventType.KEY_RELEASE) + this._menu.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); + } + }, + + _onMenuActivate: function(menu, child) { + child._window.activate(global.get_current_time()); + }, + + _onDestroy: function() { + global.window_manager.disconnect(this._switchWorkspaceId); + this._windowTracker.disconnect(this._notifyFocusId); + this.app.disconnect(this._windowsChangedId); + this._menu.actor.destroy(); + } +}); + + const TrayButton = new Lang.Class({ Name: 'TrayButton', @@ -232,6 +388,11 @@ const WindowList = new Lang.Class({ trackFullscreen: true }); Main.ctrlAltTabManager.addGroup(this.actor, _('Window List'), 'start-here-symbolic'); + this._appSystem = Shell.AppSystem.get_default(); + this._appStateChangedId = + this._appSystem.connect('app-state-changed', + Lang.bind(this, this._onAppStateChanged)); + this._monitorsChangedId = Main.layoutManager.connect('monitors-changed', Lang.bind(this, this._updatePosition)); @@ -264,9 +425,30 @@ const WindowList = new Lang.Class({ this._updateKeyboardAnchor(); })); - let windows = Meta.get_window_actors(global.screen); - for (let i = 0; i < windows.length; i++) - this._onWindowAdded(null, windows[i].metaWindow); + this._settings = Convenience.getSettings(); + this._groupingModeChangedId = + this._settings.connect('changed::grouping-mode', + Lang.bind(this, this._groupingModeChanged)); + this._groupingModeChanged(); + }, + + _groupingModeChanged: function() { + this._groupingMode = this._settings.get_enum('grouping-mode'); + this._populateWindowList(); + }, + + _populateWindowList: function() { + this._windowList.destroy_all_children(); + + if (this._groupingMode == GroupingMode.NEVER) { + let windows = Meta.get_window_actors(global.screen); + for (let i = 0; i < windows.length; i++) + this._onWindowAdded(null, windows[i].metaWindow); + } else { + let apps = this._appSystem.get_running(); + for (let i = 0; i < apps.length; i++) + this._addApp(apps[i]); + } }, _updatePosition: function() { @@ -283,10 +465,41 @@ const WindowList = new Lang.Class({ Main.keyboard.actor.anchor_y = anchorY; }, + _onAppStateChanged: function(appSys, app) { + if (this._groupingMode != GroupingMode.ALWAYS) + return; + + if (app.state == Shell.AppState.RUNNING) + this._addApp(app); + else if (app.state == Shell.AppState.STOPPED) + this._removeApp(app); + }, + + _addApp: function(app) { + let button = new AppButton(app); + this._windowList.layout_manager.pack(button.actor, + true, true, true, + Clutter.BoxAlignment.START, + Clutter.BoxAlignment.START); + }, + + _removeApp: function(app) { + let children = this._windowList.get_children(); + for (let i = 0; i < children.length; i++) { + if (children[i]._delegate.app == app) { + children[i].destroy(); + return; + } + } + }, + _onWindowAdded: function(ws, win) { if (!Shell.WindowTracker.get_default().is_window_interesting(win)) return; + if (this._groupingMode != GroupingMode.NEVER) + return; + let button = new WindowButton(win); this._windowList.layout_manager.pack(button.actor, true, true, true, @@ -295,6 +508,9 @@ const WindowList = new Lang.Class({ }, _onWindowRemoved: function(ws, win) { + if (this._groupingMode != GroupingMode.NEVER) + return; + let children = this._windowList.get_children(); for (let i = 0; i < children.length; i++) { if (children[i]._delegate.metaWindow == win) { @@ -333,8 +549,12 @@ const WindowList = new Lang.Class({ }, _onDestroy: function() { + Main.ctrlAltTabManager.removeGroup(this.actor); + this._appSystem.disconnect(this._appStateChangedId); + this._appStateChangedId = 0; + Main.layoutManager.disconnect(this._monitorsChangedId); this._monitorsChangedId = 0; @@ -350,6 +570,8 @@ const WindowList = new Lang.Class({ Main.overview.disconnect(this._overviewShowingId); Main.overview.disconnect(this._overviewHidingId); + this._settings.disconnect(this._groupingModeChangedId); + let windows = Meta.get_window_actors(global.screen); for (let i = 0; i < windows.length; i++) windows[i].metaWindow.set_icon_geometry(null); diff --git a/extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml.in b/extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml.in new file mode 100644 index 00000000..6930d2ff --- /dev/null +++ b/extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml.in @@ -0,0 +1,18 @@ + + + + + + + + 'never' + <_summary>When to group windows + <_description> + Decides when to group windows from the same application on the + window list. Possible values are "never" and "always". + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index 6c752d63..8624edf8 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -19,6 +19,7 @@ extensions/places-menu/placeDisplay.js extensions/systemMonitor/extension.js extensions/user-theme/extension.js extensions/user-theme/org.gnome.shell.extensions.user-theme.gschema.xml.in +extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml.in extensions/windowsNavigator/extension.js extensions/workspace-indicator/extension.js extensions/workspace-indicator/prefs.js