From 002b1b5d07affda9c0d03c0c8baa25aa416a63f7 Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Mon, 1 Aug 2011 15:36:45 +0200 Subject: [PATCH] alternate-tab: more alternative implementations Now alternate-tab shows a dialog on first usage, allowing to choose between native behavior (app icons from all worskpaces), app icons from current workspace only and window thumbnails only. Patch written by Joachim Bargsten --- extensions/alternate-tab/Makefile.am | 11 + extensions/alternate-tab/extension.js | 348 +++++++++++++++++- extensions/alternate-tab/metadata.json.in | 2 +- ...ll.extensions.alternate-tab.gschema.xml.in | 19 + po/POTFILES.in | 1 + 5 files changed, 367 insertions(+), 14 deletions(-) create mode 100644 extensions/alternate-tab/org.gnome.shell.extensions.alternate-tab.gschema.xml.in diff --git a/extensions/alternate-tab/Makefile.am b/extensions/alternate-tab/Makefile.am index b8fde766..2e3930e8 100644 --- a/extensions/alternate-tab/Makefile.am +++ b/extensions/alternate-tab/Makefile.am @@ -1,3 +1,14 @@ EXTENSION_ID = alternate-tab include ../../extension.mk + +gschemas_in = org.gnome.shell.extensions.alternate-tab.gschema.xml.in + +@INTLTOOL_XML_NOMERGE_RULE@ + +gsettings_SCHEMAS = $(gschemas_in:.xml.in=.xml) + +@GSETTINGS_RULES@ + +CLEANFILES += $(gschemas_in:.xml.in=.valid) $(gsettings_SCHEMAS) +EXTRA_DIST += $(gschemas_in) diff --git a/extensions/alternate-tab/extension.js b/extensions/alternate-tab/extension.js index 0e158c49..b71f17a2 100644 --- a/extensions/alternate-tab/extension.js +++ b/extensions/alternate-tab/extension.js @@ -1,16 +1,324 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ -const Clutter = imports.gi.Clutter; -const Lang = imports.lang; -const Mainloop = imports.mainloop; -const Shell= imports.gi.Shell; -const St = imports.gi.St; +/* most of the code is borrowed from + * > js/ui/altTab.js < + * of the gnome-shell source code + */ -const AltTab=imports.ui.altTab; +const AltTab = imports.ui.altTab; +const Clutter = imports.gi.Clutter; +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; const Main = imports.ui.main; +const Mainloop = imports.mainloop; +const ModalDialog = imports.ui.modalDialog; +const Shell = imports.gi.Shell; +const St = imports.gi.St; const Tweener = imports.ui.tweener; const WindowManager = imports.ui.windowManager; +const Gettext = imports.gettext.domain('gnome-shell-extensions'); +const _ = Gettext.gettext; +const N_ = function(e) { return e }; + +const POPUP_FADE_TIME = 0.1; // seconds + +const SETTINGS_SCHEMA = 'org.gnome.shell.extensions.alternate-tab'; +const SETTINGS_BEHAVIOUR_KEY = 'behaviour'; +const SETTINGS_FIRST_TIME_KEY = 'first-time'; + +const MODES = { + native: function() { + Main.wm._startAppSwitcher(); + }, + all_thumbnails: function() { + new AltTabPopup2(); + }, + workspace_icons: function() { + new AltTabPopupW().show(); + } +}; + +const MESSAGE = N_("This is the first time you use the Alternate Tab extension. \n\ +Please choose your preferred behaviour:\n\ +\n\ +All & Thumbnails:\n\ + This mode presents all applications from all workspaces in one selection \n\ + list. Instead of using the application icon of every window, it uses small \n\ + thumbnails resembling the window itself. \n\ +\n\ +Workspace & Icons:\n\ + This mode let's you switch between the applications of your current \n\ + workspace and gives you additionally the option to switch to the last used \n\ + application of your previous workspace. This is always the last symbol in \n\ + the list and is segregated by a separator/vertical line if available. \n\ + Every window is represented by its application icon. \n\ +\n\ +Native:\n\ + This mode is the native GNOME 3 behaviour or in other words: Clicking \n\ + native switches the Alternate Tab extension off. \n\ +"); + +function AltTabPopupW() { + this._init(); +} + +AltTabPopupW.prototype = { + __proto__ : AltTab.AltTabPopup.prototype, + + show : function(backward, switch_group) { + let tracker = Shell.WindowTracker.get_default(); + let apps = tracker.get_running_apps (''); + + if (!apps.length) + return false; + + if (!Main.pushModal(this.actor)) + return false; + this._haveModal = true; + + this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent)); + this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent)); + + this.actor.connect('button-press-event', Lang.bind(this, this._clickedOutside)); + this.actor.connect('scroll-event', Lang.bind(this, this._onScroll)); + + this._appSwitcher = new WindowSwitcher(apps, this); + this.actor.add_actor(this._appSwitcher.actor); + this._appSwitcher.connect('item-activated', Lang.bind(this, this._appActivated)); + this._appSwitcher.connect('item-entered', Lang.bind(this, this._appEntered)); + + this._appIcons = this._appSwitcher.icons; + + // Make the initial selection + if (switch_group) { + if (backward) { + this._select(0, this._appIcons[0].cachedWindows.length - 1); + } else { + if (this._appIcons[0].cachedWindows.length > 1) + this._select(0, 1); + else + this._select(0, 0); + } + } else if (this._appIcons.length == 1) { + this._select(0); + } else if (backward) { + this._select(this._appIcons.length - 1); + } else { + this._select(1); + } + + // There's a race condition; if the user released Alt before + // we got the grab, then we won't be notified. (See + // https://bugzilla.gnome.org/show_bug.cgi?id=596695 for + // details.) So we check now. (Have to do this after updating + // selection.) + let [x, y, mods] = global.get_pointer(); + if (!(mods & Gdk.ModifierType.MOD1_MASK)) { + this._finish(); + return false; + } + + this.actor.opacity = 0; + this.actor.show(); + Tweener.addTween(this.actor, + { opacity: 255, + time: POPUP_FADE_TIME, + transition: 'easeOutQuad' + }); + + return true; + }, + + + _finish : function() { + let app = this._appIcons[this._currentApp]; + Main.activateWindow(app.cachedWindows[0]); + this.destroy(); + } + +}; + +function AppIcon(app, window) { + this._init(app, window); +} + +AppIcon.prototype = { + __proto__ : AltTab.AppIcon.prototype, + + _init: function(app, window) { + this.app = app; + + this.cachedWindows = []; + this.cachedWindows.push(window); + + this.actor = new St.BoxLayout({ style_class: 'alt-tab-app', + vertical: true }); + this.icon = null; + this._iconBin = new St.Bin({ x_fill: true, y_fill: true }); + + this.actor.add(this._iconBin, { x_fill: false, y_fill: false } ); + + let title = window.get_title(); + if (title) { + this.label = new St.Label({ text: title }); + let bin = new St.Bin({ x_align: St.Align.MIDDLE }); + bin.add_actor(this.label); + this.actor.add(bin); + } + else { + this.label = new St.Label({ text: this.app.get_name() }); + this.actor.add(this.label, { x_fill: false }); + } + } +}; + +function WindowSwitcher(apps, altTabPopup) { + this._init(apps, altTabPopup); +} + +WindowSwitcher.prototype = { + __proto__ : AltTab.AppSwitcher.prototype, + + _init : function(apps, altTabPopup) { + AltTab.SwitcherList.prototype._init.call(this, true); + + // Construct the AppIcons, sort by time, add to the popup + let activeWorkspace = global.screen.get_active_workspace(); + let workspaceIcons = []; + let otherIcons = []; + for (let i = 0; i < apps.length; i++) { + // Cache the window list now; we don't handle dynamic changes here, + // and we don't want to be continually retrieving it + let windows = apps[i].get_windows(); + + for(let j = 0; j < windows.length; j++) { + let appIcon = new AppIcon(apps[i], windows[j]); + if (this._isWindowOnWorkspace(windows[j], activeWorkspace)) { + workspaceIcons.push(appIcon); + } + else { + otherIcons.push(appIcon); + } + } + } + + workspaceIcons.sort(Lang.bind(this, this._sortAppIcon)); + otherIcons.sort(Lang.bind(this, this._sortAppIcon)); + + if(otherIcons.length > 0) { + let mostRecentOtherIcon = otherIcons[0]; + otherIcons = []; + otherIcons.push(mostRecentOtherIcon); + } + + this.icons = []; + this._arrows = []; + for (let i = 0; i < workspaceIcons.length; i++) + this._addIcon(workspaceIcons[i]); + if (workspaceIcons.length > 0 && otherIcons.length > 0) + this.addSeparator(); + for (let i = 0; i < otherIcons.length; i++) + this._addIcon(otherIcons[i]); + + this._curApp = -1; + this._iconSize = 0; + this._altTabPopup = altTabPopup; + this._mouseTimeOutId = 0; + }, + + + _isWindowOnWorkspace: function(w, workspace) { + if (w.get_workspace() == workspace) + return true; + return false; + }, + + _sortAppIcon : function(appIcon1, appIcon2) { + let t1 = appIcon1.cachedWindows[0].get_user_time(); + let t2 = appIcon2.cachedWindows[0].get_user_time(); + if (t2 > t1) return 1; + else return -1; + } +}; + +function AltTabSettingsDialog() { + this._init(); +} + +AltTabSettingsDialog.prototype = { + __proto__: ModalDialog.ModalDialog.prototype, + + _init : function() { + ModalDialog.ModalDialog.prototype._init.call(this, { styleClass: null }); + + let mainContentBox = new St.BoxLayout({ style_class: 'polkit-dialog-main-layout', + vertical: false }); + this.contentLayout.add(mainContentBox, + { x_fill: true, + y_fill: true }); + + let messageBox = new St.BoxLayout({ style_class: 'polkit-dialog-message-layout', + vertical: true }); + mainContentBox.add(messageBox, + { y_align: St.Align.START }); + + this._subjectLabel = new St.Label({ style_class: 'polkit-dialog-headline', + text: _("Alt Tab Behaviour") }); + + messageBox.add(this._subjectLabel, + { y_fill: false, + y_align: St.Align.START }); + + this._descriptionLabel = new St.Label({ style_class: 'polkit-dialog-description', + text: Gettext.gettext(MESSAGE) }); + + messageBox.add(this._descriptionLabel, + { y_fill: true, + y_align: St.Align.START }); + + + this.setButtons([ + { + label: _("All & Thumbnails"), + action: Lang.bind(this, function() { + this.setBehaviour('all_thumbnails'); + this.close(); + }) + }, + { + label: _("Workspace & Icons"), + action: Lang.bind(this, function() { + this.setBehaviour('workspace_icons'); + this.close(); + }) + }, + { + label: _("Native"), + action: Lang.bind(this, function() { + this.setBehaviour('native'); + this.close(); + }) + }, + { + label: _("Cancel"), + action: Lang.bind(this, function() { + this.close(); + }), + key: Clutter.Escape + } + ]); + }, + + setBehaviour: function(behaviour) { + this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA }); + this._settings.set_string(SETTINGS_BEHAVIOUR_KEY, behaviour); + this._settings.set_boolean(SETTINGS_FIRST_TIME_KEY, false); + } +}; + function AltTabPopup2() { this._init(); } @@ -41,6 +349,7 @@ AltTabPopup2.prototype = { this.show(); Main.uiGroup.add_actor(this.actor); + this._select(0); }, show : function(backward) { @@ -54,9 +363,7 @@ AltTabPopup2.prototype = { for (let w = windows.length-1; w >= 0; w--) { let win = windows[w].get_meta_window(); - if (win.window_type == 0) { normal_windows.push(win); - } } normal_windows.sort(Lang.bind(this, this._sortWindows)); @@ -74,8 +381,10 @@ AltTabPopup2.prototype = { ap1 = new AltTab.AppIcon(apps[i]); } } - ap1.cachedWindows = [win]; - appIcons.push(ap1); + if (ap1 != null) { + ap1.cachedWindows = [win]; + appIcons.push(ap1); + } } if (!windows.length) @@ -93,13 +402,13 @@ AltTabPopup2.prototype = { this._appSwitcher = new WindowList(windows); this._appSwitcher._altTabPopup=this; - this._appSwitcher.highlight(0,false); this.actor.add_actor(this._appSwitcher.actor); this._appSwitcher.connect('item-activated', Lang.bind(this, this._appActivated)); this._appSwitcher.connect('item-entered', Lang.bind(this, this._appEntered)); this._appIcons = appIcons; + return true }, @@ -220,8 +529,10 @@ WindowList.prototype = { } } } + if (ap1 != null) { ap1.cachedWindows = [win]; this._addIcon(ap1); + } } }, @@ -230,8 +541,19 @@ WindowList.prototype = { } }; -function main() { +function main(metadata) { + imports.gettext.bindtextdomain('gnome-shell-extensions', metadata.localedir); + Main.wm.setKeybindingHandler('switch_windows', function() { - let alpopup = new AltTabPopup2(); + let settings = new Gio.Settings({ schema: SETTINGS_SCHEMA }); + + if(settings.get_boolean(SETTINGS_FIRST_TIME_KEY)) { + new AltTabSettingsDialog().open(); + } else { + let behaviour = settings.get_string(SETTINGS_BEHAVIOUR_KEY); + if(behaviour in MODES) { + MODES[behaviour](); + } + } }); } diff --git a/extensions/alternate-tab/metadata.json.in b/extensions/alternate-tab/metadata.json.in index aae37e2a..d811b547 100644 --- a/extensions/alternate-tab/metadata.json.in +++ b/extensions/alternate-tab/metadata.json.in @@ -2,7 +2,7 @@ "uuid": "@uuid@", "name": "AlternateTab", "description": "A replacement for Alt-Tab, allows to cycle between windows and does not group by application", -"original-author": "thomas.bouffon@gmail.com", +"original-authors": [ "jw@bargsten.org", "thomas.bouffon@gmail.com" ], "shell-version": [ "@shell_current@" ], "localedir": "@LOCALEDIR@", "url": "@url@" diff --git a/extensions/alternate-tab/org.gnome.shell.extensions.alternate-tab.gschema.xml.in b/extensions/alternate-tab/org.gnome.shell.extensions.alternate-tab.gschema.xml.in new file mode 100644 index 00000000..7dbbca7d --- /dev/null +++ b/extensions/alternate-tab/org.gnome.shell.extensions.alternate-tab.gschema.xml.in @@ -0,0 +1,19 @@ + + + + + + + + + 'native' + <_summary>The alt tab behaviour. + <_description>Sets the Alt-Tab behaviour. Possible values are: native, all_thumbnails and workspace_icons. + + + true + <_summary>Indicates if Alternate Tab is newly installed + <_description>Ask the user for a default behaviour if true. + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index 74678afa..d1be54bb 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,5 +1,6 @@ extensions/alternative-status-menu/extension.js extensions/alternate-tab/extension.js +extensions/alternate-tab/org.gnome.shell.extensions.alternate-tab.gschema.xml.in extensions/auto-move-windows/extension.js extensions/auto-move-windows/org.gnome.shell.extensions.auto-move-windows.gschema.xml.in extensions/dock/extension.js