[gnome-shell-extensions] alternate-tab: rework again



commit fb3df46a25bbeb6e687708d1ad1460a76aa42b4c
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Wed Aug 8 00:20:52 2012 +0200

    alternate-tab: rework again
    
    Kill the mode switch distinction, and kill the old and unmaintained
    Workspace&Icons mode.
    Introduce instead configurability for the All&Thumbnails code, which
    now independently allows choosing if only all windows or only those
    from current workspace should be shown, and in which way (thumbnail,
    icon, both).
    All configuration settings changed, so the extension must be configured
    again (although hopefully I chose reasonable defaults)

 extensions/alternate-tab/Makefile.am               |    2 +-
 extensions/alternate-tab/allThumbnails.js          |  353 ------------------
 extensions/alternate-tab/extension.js              |  377 +++++++++++++++++++-
 ...e.shell.extensions.alternate-tab.gschema.xml.in |   34 +-
 extensions/alternate-tab/prefs.js                  |   79 +----
 extensions/alternate-tab/workspaceIcons.js         |  274 --------------
 6 files changed, 396 insertions(+), 723 deletions(-)
---
diff --git a/extensions/alternate-tab/Makefile.am b/extensions/alternate-tab/Makefile.am
index d9e0f36..1f35392 100644
--- a/extensions/alternate-tab/Makefile.am
+++ b/extensions/alternate-tab/Makefile.am
@@ -1,6 +1,6 @@
 EXTENSION_ID = alternate-tab
 
-EXTRA_MODULES = allThumbnails.js workspaceIcons.js prefs.js
+EXTRA_MODULES = prefs.js
 
 include ../../extension.mk
 include ../../settings.mk
diff --git a/extensions/alternate-tab/extension.js b/extensions/alternate-tab/extension.js
index f3cbaba..febd040 100644
--- a/extensions/alternate-tab/extension.js
+++ b/extensions/alternate-tab/extension.js
@@ -28,33 +28,382 @@ const N_ = function(e) { return e };
 const ExtensionUtils = imports.misc.extensionUtils;
 const Me = ExtensionUtils.getCurrentExtension();
 const Convenience = Me.imports.convenience;
-const WorkspaceIcons = Me.imports.workspaceIcons;
-const AllThumbnails = Me.imports.allThumbnails;
 
 let settings;
 
-const SETTINGS_BEHAVIOUR_KEY = 'behaviour';
-
-const MODES = {
-    all_thumbnails: AllThumbnails.AltTabPopupAllThumbnails,
-    workspace_icons: WorkspaceIcons.AltTabPopupWorkspaceIcons,
+const AppIconMode = {
+    THUMBNAIL_ONLY: 1,
+    APP_ICON_ONLY: 2,
+    BOTH: 3,
 };
 
-function doAltTab(display, screen, window, binding) {
-    let behaviour = settings.get_string(SETTINGS_BEHAVIOUR_KEY);
+const SETTINGS_APP_ICON_MODE = 'app-icon-mode';
+const SETTINGS_CURRENT_WORKSPACE_ONLY = 'current-workspace-only';
+
+function mod(a, b) {
+    return ((a+b) % b);
+}
+
+const AltTabPopup = new Lang.Class({
+    Name: 'AlternateTab.AltTabPopup',
+
+    _init : function(settings) {
+	this._settings = settings;
+
+        this.actor = new Shell.GenericContainer({ name: 'altTabPopup',
+                                                  reactive: true });
+
+        this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
+        this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
+        this.actor.connect('allocate', Lang.bind(this, this._allocate));
+
+        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+
+        this._haveModal = false;
+
+        this._currentWindow = 0;
+        this._motionTimeoutId = 0;
+
+        // Initially disable hover so we ignore the enter-event if
+        // the switcher appears underneath the current pointer location
+        this._disableHover();
+
+        Main.uiGroup.add_actor(this.actor);
+    },
+
+    _getPreferredWidth: function (actor, forHeight, alloc) {
+        alloc.min_size = global.screen_width;
+        alloc.natural_size = global.screen_width;
+    },
+
+    _getPreferredHeight: function (actor, forWidth, alloc) {
+        alloc.min_size = global.screen_height;
+        alloc.natural_size = global.screen_height;
+    },
+
+    _allocate: function (actor, box, flags) {
+        let childBox = new Clutter.ActorBox();
+        let primary = Main.layoutManager.primaryMonitor;
+
+        let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
+        let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
+        let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
+        let vPadding = this.actor.get_theme_node().get_vertical_padding();
+        let hPadding = leftPadding + rightPadding;
+
+        // Allocate the appSwitcher
+        // We select a size based on an icon size that does not overflow the screen
+        let [childMinHeight, childNaturalHeight] = this._appSwitcher.actor.get_preferred_height(primary.width - hPadding);
+        let [childMinWidth, childNaturalWidth] = this._appSwitcher.actor.get_preferred_width(childNaturalHeight);
+        childBox.x1 = Math.max(primary.x + leftPadding, primary.x + Math.floor((primary.width - childNaturalWidth) / 2));
+        childBox.x2 = Math.min(primary.x + primary.width - rightPadding, childBox.x1 + childNaturalWidth);
+        childBox.y1 = primary.y + Math.floor((primary.height - childNaturalHeight) / 2);
+        childBox.y2 = childBox.y1 + childNaturalHeight;
+        this._appSwitcher.actor.allocate(childBox, flags);
+    },
+
+    show : function(backward, binding, mask) {
+	let windows;
+
+	if (!settings.get_boolean(SETTINGS_CURRENT_WORKSPACE_ONLY)) {
+	    // This is roughly what meta_display_get_tab_list does, except
+	    // that it doesn't filter on workspace
+	    // See in particular src/core/window-private.h for the filters
+	    windows = global.get_window_actors().map(function(actor) {
+		return actor.meta_window;
+	    }).filter(function(win) {
+		return !win.is_override_redirect() &&
+		    win.get_window_type() != Meta.WindowType.DESKTOP &&
+		    win.get_window_type() != Meta.WindowType.DOCK;
+	    }).sort(function(one, two) {
+		return two.get_user_time() - one.get_user_time();
+	    });
+	} else {
+	    windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, global.screen,
+						      global.screen.get_active_workspace());
+	}
+
+        if (!windows.length) {
+            this.destroy();
+            return false;
+        }
+
+        if (!Main.pushModal(this.actor)) {
+            // Probably someone else has a pointer grab, try again with keyboard only
+            if (!Main.pushModal(this.actor, global.get_current_time(), Meta.ModalOptions.POINTER_ALREADY_GRABBED)) {
+                return false;
+            }
+        }
+        this._haveModal = true;
+        this._modifierMask = AltTab.primaryModifier(mask);
+
+        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 WindowList(windows, this._settings);
+        this.actor.add_actor(this._appSwitcher.actor);
+        this._appSwitcher.connect('item-activated', Lang.bind(this, this._windowActivated));
+        this._appSwitcher.connect('item-entered', Lang.bind(this, this._windowEntered));
+
+        // make the initial selection
+        if (backward)
+            this._select(windows.length - 1);
+        else
+            this._select(1);
+
+        this.actor.opacity = 0;
+        this.actor.show();
+
+        // 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 & this._modifierMask)) {
+            this._finish();
+            return false;
+        }
+
+        // We delay showing the popup so that fast Alt+Tab users aren't
+        // disturbed by the popup briefly flashing.
+        this._initialDelayTimeoutId = Mainloop.timeout_add(AltTab.POPUP_DELAY_TIMEOUT,
+                                                           Lang.bind(this, function () {
+                                                               this.actor.opacity = 255;
+                                                               this._initialDelayTimeoutId = 0;
+                                                           }));
+
+	return true
+    },
+
+    _windowActivated : function(thumbnailList, n) {
+	let win = this._appSwitcher.windows[n];
+        Main.activateWindow(win);
+        this.destroy();
+    },
+
+    _finish : function() {
+        let win = this._appSwitcher.windows[this._currentWindow];
+        Main.activateWindow(win);
+        this.destroy();
+    },
+
+    _keyPressEvent : function(actor, event) {
+        let keysym = event.get_key_symbol();
+        let event_state = event.get_state();
+        let backwards = event_state & Clutter.ModifierType.SHIFT_MASK;
+        let action = global.display.get_keybinding_action(event.get_key_code(), event_state);
+
+        this._disableHover();
+
+        if (keysym == Clutter.Escape) {
+            this.destroy();
+        } else if (action == Meta.KeyBindingAction.SWITCH_WINDOWS ||
+		   action == Meta.KeyBindingAction.SWITCH_GROUP) {
+            this._select(backwards ? this._previousWindow() : this._nextWindow());
+        } else if (action == Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD ||
+		  action == Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD) {
+            this._select(this._previousWindow());
+        } else {
+	    if (keysym == Clutter.Left)
+                this._select(this._previousWindow());
+            else if (keysym == Clutter.Right)
+                this._select(this._nextWindow());
+        }
+
+        return true;
+    },
 
-    // alt-tab having no effect is unexpected, even with wrong settings
-    if (!(behaviour in MODES))
-        behaviour = 'all_thumbnails';
+    _keyReleaseEvent : function(actor, event) {
+        let [x, y, mods] = global.get_pointer();
+        let state = mods & this._modifierMask;
 
+        if (state == 0)
+            this._finish();
+
+        return true;
+    },
+
+    _onScroll : function(actor, event) {
+        let direction = event.get_scroll_direction();
+        if (direction == Clutter.ScrollDirection.UP)
+            this._select(this._previousWindow());
+	else if (direction == Clutter.ScrollDirection.DOWN)
+            this._select(this._nextWindow());
+
+	return true;
+    },
+
+    _clickedOutside : function(actor, event) {
+        this.destroy();
+    },
+
+    _windowEntered : function(windowSwitcher, n) {
+        if (!this._mouseActive)
+            return;
+
+        this._select(n);
+    },
+
+    _disableHover : function() {
+        this._mouseActive = false;
+
+        if (this._motionTimeoutId != 0)
+            Mainloop.source_remove(this._motionTimeoutId);
+
+        this._motionTimeoutId = Mainloop.timeout_add(AltTab.DISABLE_HOVER_TIMEOUT, Lang.bind(this, this._mouseTimedOut));
+    },
+
+    _mouseTimedOut : function() {
+        this._motionTimeoutId = 0;
+        this._mouseActive = true;
+    },
+
+    _popModal: function() {
+        if (this._haveModal) {
+            Main.popModal(this.actor);
+            this._haveModal = false;
+        }
+    },
+
+    destroy : function() {
+        this._popModal();
+        if (this.actor.visible) {
+            Tweener.addTween(this.actor,
+                             { opacity: 0,
+                               time: AltTab.POPUP_FADE_OUT_TIME,
+                               transition: 'easeOutQuad',
+                               onComplete: Lang.bind(this,
+                                   function() {
+                                       this.actor.destroy();
+                                   })
+                             });
+        } else
+            this.actor.destroy();
+    },
+
+    _onDestroy : function() {
+        this._popModal();
+
+        if (this._motionTimeoutId != 0)
+            Mainloop.source_remove(this._motionTimeoutId);
+        if (this._initialDelayTimeoutId != 0)
+            Mainloop.source_remove(this._initialDelayTimeoutId);
+    },
+
+    _select : function(window) {
+        this._currentWindow = window;
+        this._appSwitcher.highlight(window);
+    },
+
+    _nextWindow: function() {
+	return mod(this._currentWindow + 1, this._appSwitcher.windows.length);
+    },
+
+    _previousWindow: function() {
+	return mod(this._currentWindow - 1, this._appSwitcher.windows.length);
+    },
+});
+
+const WindowIcon = new Lang.Class({
+    Name: 'AlternateTab.WindowIcon',
+
+    _init: function(window, settings) {
+	this.window = window;
+	this._settings = settings;
+
+        this.actor = new St.BoxLayout({ style_class: 'alt-tab-app',
+                                        vertical: true });
+        this.icon = null;
+        this._iconBin = new St.Widget({ layout_manager: new Clutter.BinLayout() });
+
+        this.actor.add(this._iconBin, { x_fill: false, y_fill: false } );
+        this.label = new St.Label({ text: window.get_title() });
+        this.actor.add(this.label, { x_fill: false });
+
+	let tracker = Shell.WindowTracker.get_default();
+	this.app = tracker.get_window_app(window);
+
+        let mutterWindow = this.window.get_compositor_private();
+        let windowTexture = mutterWindow.get_texture();
+        let [width, height] = windowTexture.get_size();
+        let scale, size;
+
+	this._iconBin.destroy_all_children();
+
+	switch (this._settings.get_enum(SETTINGS_APP_ICON_MODE)) {
+	case AppIconMode.THUMBNAIL_ONLY:
+	    scale = Math.min(1.0, 128 / width, 128 / height);
+	    size = 128;
+            this.clone = new Clutter.Clone({ source: windowTexture,
+					     width: width * scale,
+					     height: height * scale,
+					     // usual hack for the usual bug in ClutterBinLayout...
+				             x_expand: true,
+					     y_expand: true });
+	    this._iconBin.add_actor(this.clone);
+	    break;
+
+	case AppIconMode.BOTH:
+	    scale = Math.min(1.0, 128 / width, 128 / height);
+	    size = 128;
+            this.clone = new Clutter.Clone({ source: windowTexture,
+					     width: width * scale,
+					     height: height * scale,
+					     // usual hack for the usual bug in ClutterBinLayout...
+				             x_expand: true,
+					     y_expand: true });
+	    this._iconBin.add_actor(this.clone);
+
+	    this.appIcon = this.app.create_icon_texture(size / 2);
+	    this.appIcon.x_expand = this.appIcon.y_expand = true;
+	    this.appIcon.x_align = Clutter.ActorAlign.END;
+	    this.appIcon.y_align = Clutter.ActorAlign.END;
+	    this._iconBin.add_actor(this.appIcon);
+	    break;
+
+	case AppIconMode.APP_ICON_ONLY:
+	    size = 96;
+	    this.appIcon = this.app.create_icon_texture(size);
+	    this.appIcon.x_expand = this.appIcon.y_expand = true;
+	    this._iconBin.add_actor(this.appIcon);
+	}
+
+        this._iconBin.set_size(size, size);
+    }
+});
+
+const WindowList = new Lang.Class({
+    Name: 'AlternateTab.WindowList',
+    Extends: AltTab.SwitcherList,
+
+    _init : function(windows, settings) {
+        this.parent(true);
+
+        this.windows = windows;
+        this.icons = [];
+
+	for (let i = 0; i < windows.length; i++) {
+	    let win = windows[i];
+	    let icon = new WindowIcon(win, settings);
+
+            this.addItem(icon.actor, icon.label);
+            this.icons.push(icon);
+	}
+    }
+});
+
+function doAltTab(display, screen, window, binding) {
     if (Main.wm._workspaceSwitcherPopup)
         Main.wm._workspaceSwitcherPopup.actor.hide();
 
     let modifiers = binding.get_modifiers()
     let backwards = modifiers & Meta.VirtualModifier.SHIFT_MASK;
 
-    let constructor = MODES[behaviour];
-    let popup = new constructor(settings);
+    let popup = new AltTabPopup(settings);
     if (!popup.show(backwards, binding.get_name(), binding.get_mask()))
         popup.destroy();
 }
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
index 1eec02f..88ac798 100644
--- 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
@@ -1,31 +1,25 @@
 <schemalist gettext-domain="gnome-shell-extensions">
-  <enum id="org.gnome.shell.extensions.alternate-tab.BehaviourMode">
-    <value value="1" nick="all_thumbnails"/>
-    <value value="2" nick="workspace_icons"/>
+  <enum id="org.gnome.shell.extensions.alternate-tab.AppIconMode">
+    <value value="1" nick="thumbnail_only"/>
+    <value value="2" nick="app_icon_only"/>
+    <value value="3" nick="both"/>
   </enum>
   <schema id="org.gnome.shell.extensions.alternate-tab" path="/org/gnome/shell/extensions/alternate-tab/">
-    <key name="behaviour" enum="org.gnome.shell.extensions.alternate-tab.BehaviourMode">
-      <default>'all_thumbnails'</default>
-      <_summary>The alt tab behaviour.</_summary>
+    <key name="app-icon-mode" enum="org.gnome.shell.extensions.alternate-tab.AppIconMode">
+      <default>'both'</default>
+      <_summary>The application icon mode.</_summary>
       <_description>
-	Sets the Alt-Tab behaviour. Possible values are: all_thumbnails and workspace_icons.
-	See the configuration dialogs for details.
+	Configures how the windows are shown in the switcher. Valid possibilities
+	are 'thumbnail-only' (shows a thumbnail of the window), 'app-icon-only'
+	(shows only the application icon) or 'both'.
       </_description>
     </key>
-    <key type="b" name="highlight-selected">
+    <key type="b" name="current-workspace-only">
       <default>false</default>
-      <summary>Bring each selected window to the front.</summary>
+      <summary>Limit switcher to current workspace.</summary>
       <description>
-	Bring each window to the front in turn as Alt+Tab is pressed.
-	This setting applies only when the selected mode is 'workspace_icons'.
-      </description>
-    </key>
-    <key type="b" name="show-app-icon">
-      <default>true</default>
-      <summary>Show small application icon.</summary>
-      <description>
-	If true, a small application icon is overlayed to each window thumbnail.
-	This setting applies only when the selected mode is 'all_thumbnails'.
+	If true, only windows from the current workspace are shown in the switcher.
+	Otherwise, all windows are included.
       </description>
     </key>
   </schema>
diff --git a/extensions/alternate-tab/prefs.js b/extensions/alternate-tab/prefs.js
index f5d3456..6f1856b 100644
--- a/extensions/alternate-tab/prefs.js
+++ b/extensions/alternate-tab/prefs.js
@@ -19,31 +19,13 @@ const ExtensionUtils = imports.misc.extensionUtils;
 const Me = ExtensionUtils.getCurrentExtension();
 const Convenience = Me.imports.convenience;
 
-const SETTINGS_BEHAVIOUR_KEY = 'behaviour';
-const SETTINGS_HIGHLIGHT_KEY = 'highlight-selected';
-const SETTINGS_SHOW_APP_ICON_KEY = 'show-app-icon';
+const SETTINGS_APP_ICON_MODE = 'app-icon-mode';
+const SETTINGS_CURRENT_WORKSPACE_ONLY = 'current-workspace-only';
 
 const MODES = {
-    all_thumbnails: {
-        name: N_("All & Thumbnails"),
-        description: N_("This mode presents all applications from all workspaces in one selection \
-list. Instead of using the application icon of every window, it uses small \
-thumbnails resembling the window itself."),
-        extra_widgets: [
-	    { label: N_("Show overlaid application icon"), key: SETTINGS_SHOW_APP_ICON_KEY }
-	]
-    },
-    workspace_icons: {
-        name: N_("Workspace & Icons"),
-        description: N_("This mode lets you switch between the applications of your current \
-workspace and gives you additionally the option to switch to the last used \
-application of your previous workspace. This is always the last symbol in \
-the list and is separated by a separator/vertical line if available. \n\
-Every window is represented by its application icon."),
-        extra_widgets: [
-            { label: N_("Move current selection to front before closing the popup"), key: SETTINGS_HIGHLIGHT_KEY }
-        ]
-    }
+    thumbnail_only: N_("Thumbnail only"),
+    app_icon_only: N_("Application icon only"),
+    both: N_("Thumbnail and application icon"),
 };
 
 const AltTabSettingsWidget = new GObject.Class({
@@ -53,65 +35,40 @@ const AltTabSettingsWidget = new GObject.Class({
 
     _init : function(params) {
         this.parent(params);
-        this.column_spacing = 10;
         this.margin = 10;
+	this.orientation = Gtk.Orientation.VERTICAL;
 
         this._settings = Convenience.getSettings();
 
-        let introLabel = _("The Alternate Tab can be used in different modes, that \
-affect the way windows are chosen and presented.");
-
-        this.attach(new Gtk.Label({ label: introLabel, wrap: true, sensitive: true,
-                                    margin_bottom: 10, margin_top: 5 }),
-                    0, 0, 2, 1);
+        let presentLabel = _("Present windows as");
+        this.add(new Gtk.Label({ label: presentLabel, sensitive: true,
+                                 margin_bottom: 10, margin_top: 5 }));
 
         let top = 1;
         let radio = null;
-        let currentMode = this._settings.get_string(SETTINGS_BEHAVIOUR_KEY);
+        let currentMode = this._settings.get_string(SETTINGS_APP_ICON_MODE);
         for (let mode in MODES) {
             // copy the mode variable because it has function scope, not block scope
             // so cannot be used in a closure
             let modeCapture = mode;
-            let obj = MODES[mode];
-            let name = Gettext.gettext(obj.name);
-            let description = Gettext.gettext(obj.description);
-            let nextra = obj.extra_widgets.length;
+            let name = Gettext.gettext(MODES[mode]);
 
             radio = new Gtk.RadioButton({ group: radio, label: name, valign: Gtk.Align.START });
             radio.connect('toggled', Lang.bind(this, function(widget) {
                 if (widget.active)
-                    this._settings.set_string(SETTINGS_BEHAVIOUR_KEY, modeCapture);
-                this._updateSensitivity(widget, widget.active);
+                    this._settings.set_string(SETTINGS_APP_ICON_MODE, modeCapture);
             }));
-            this.attach(radio, 0, top, 1, nextra + 1);
-
-            let descriptionLabel = new Gtk.Label({ label: description, wrap: true, sensitive: true,
-                                                   xalign: 0.0, justify: Gtk.Justification.FILL });
-            this.attach(descriptionLabel, 1, top, 1, 1);
-
-            radio._extra = [];
-            for (let i = 0; i < nextra; i++) {
-                let key = obj.extra_widgets[i].key;
-                let label = Gettext.gettext(obj.extra_widgets[i].label);
-
-                let extra = new Gtk.CheckButton({ label: label });
-                this._settings.bind(key, extra, 'active', Gio.SettingsBindFlags.DEFAULT);
-
-                radio._extra.push(extra);
-                this.attach(extra, 1, top + i + 1, 1, 1);                            
-            }
+            this.add(radio);
 
             if (mode == currentMode)
                 radio.active = true;
-            this._updateSensitivity(radio, radio.active);
-
-            top += nextra + 1;
+            top += 1;
         }
-    },
 
-    _updateSensitivity: function(widget, active) {
-        for (let i = 0; i < widget._extra.length; i++)
-            widget._extra[i].sensitive = active;
+	let check = new Gtk.CheckButton({ label: _("Show only windows in the current workspace"),
+					  margin_top: 12 });
+	this._settings.bind(SETTINGS_CURRENT_WORKSPACE_ONLY, check, 'active', Gio.SettingsBindFlags.DEFAULT);
+	this.add(check);
     },
 });
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]