[gnome-shell] ctrlAltTabManager: implement Ctrl-Alt-Tab



commit d3de4e3fbda046c844f32d61c494960c5b1bc52d
Author: Dan Winship <danw gnome org>
Date:   Thu Jul 1 14:13:42 2010 -0400

    ctrlAltTabManager: implement Ctrl-Alt-Tab
    
    Add CtrlAltTabManager, which allows tabbing between StFocusManager
    groups, and fix up the panel to be keyboard navigable.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=618885

 data/theme/gnome-shell.css |    2 +-
 js/Makefile.am             |    1 +
 js/ui/ctrlAltTab.js        |  254 ++++++++++++++++++++++++++++++++++++++++++++
 js/ui/main.js              |   15 +++
 4 files changed, 271 insertions(+), 1 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 6e777fd..72c672d 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -214,7 +214,7 @@ StTooltip StLabel {
     transition-duration: 100;
 }
 
-.panel-button:active, .panel-button:checked, .panel-button:pressed {
+.panel-button:active, .panel-button:checked, .panel-button:pressed, .panel-button:focus {
     background-gradient-direction: vertical;
     background-gradient-start: #3c3c3c;
     background-gradient-end: #131313;
diff --git a/js/Makefile.am b/js/Makefile.am
index 64c9ef9..462f81b 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -18,6 +18,7 @@ nobase_dist_js_DATA = 	\
 	ui/boxpointer.js	\
 	ui/calendar.js		\
 	ui/chrome.js		\
+	ui/ctrlAltTab.js	\
 	ui/dash.js		\
 	ui/dnd.js		\
 	ui/docDisplay.js	\
diff --git a/js/ui/ctrlAltTab.js b/js/ui/ctrlAltTab.js
new file mode 100644
index 0000000..8faa6c5
--- /dev/null
+++ b/js/ui/ctrlAltTab.js
@@ -0,0 +1,254 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Clutter = imports.gi.Clutter;
+const Gdk = imports.gi.Gdk;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Meta = imports.gi.Meta;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+
+const AltTab = imports.ui.altTab;
+const Main = imports.ui.main;
+const Tweener = imports.ui.tweener;
+
+const POPUP_APPICON_SIZE = 96;
+const POPUP_FADE_TIME = 0.1; // seconds
+
+function CtrlAltTabManager() {
+    this._init();
+}
+
+CtrlAltTabManager.prototype = {
+    _init: function() {
+        this._items = [];
+        this._focusManager = St.FocusManager.get_for_stage(global.stage);
+        Main.wm.setKeybindingHandler('switch_panels', Lang.bind(this,
+            function (shellwm, binding, window, backwards) {
+                this.popup(backwards);
+            }));
+    },
+
+    addGroup: function(root, name, icon) {
+        this._items.push({ root: root, name: name, iconName: icon });
+        root.connect('destroy', Lang.bind(this, function() { this.removeGroup(root); }));
+        this._focusManager.add_group(root);
+    },
+
+    removeGroup: function(root) {
+        this._focusManager.remove_group(root);
+        for (let i = 0; i < this._items.length; i++) {
+            if (this._items[i].root == root) {
+                this._items.splice(i, 1);
+                return;
+            }
+        }
+    },
+
+    focusGroup: function(root) {
+        if (global.stage_input_mode == Shell.StageInputMode.NONREACTIVE ||
+            global.stage_input_mode == Shell.StageInputMode.NORMAL)
+            global.set_stage_input_mode(Shell.StageInputMode.FOCUSED);
+        root.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
+    },
+
+    popup: function(backwards) {
+        // Start with the set of focus groups that are currently mapped
+        let items = this._items.filter(function (item) { return item.root.mapped; });
+
+        // And add the windows metacity would show in its Ctrl-Alt-Tab list
+        let screen = global.screen;
+        let display = screen.get_display();
+        let windows = display.get_tab_list(Meta.TabList.DOCKS, screen, screen.get_active_workspace ());
+        let windowTracker = Shell.WindowTracker.get_default();
+        let textureCache = St.TextureCache.get_default();
+        for (let i = 0; i < windows.length; i++) {
+            let icon;
+            let app = windowTracker.get_window_app(windows[i]);
+            if (app)
+                icon = app.create_icon_texture(POPUP_APPICON_SIZE);
+            else
+                icon = textureCache.bind_pixbuf_property(windows[i], 'icon');
+            items.push({ window: windows[i],
+                         name: windows[i].title,
+                         iconActor: icon });
+        }
+
+        if (!items.length)
+            return;
+
+        new CtrlAltTabPopup().show(items, backwards);
+    }
+};
+
+function mod(a, b) {
+    return (a + b) % b;
+}
+
+function CtrlAltTabPopup() {
+    this._init();
+}
+
+CtrlAltTabPopup.prototype = {
+    _init : function() {
+        let primary = global.get_primary_monitor();
+        this.actor = new St.BoxLayout({ name: 'ctrlAltTabPopup',
+                                        reactive: true,
+                                        x: primary.x + primary.width / 2,
+                                        y: primary.y + primary.height / 2,
+                                        anchor_gravity: Clutter.Gravity.CENTER });
+
+        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+
+        this._haveModal = false;
+        this._selection = 0;
+
+        Main.uiGroup.add_actor(this.actor);
+    },
+
+    show : function(items, startBackwards) {
+        if (!Main.pushModal(this.actor))
+            return false;
+        this._haveModal = true;
+
+        this._keyPressEventId = this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent));
+        this._keyReleaseEventId = this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent));
+
+        this._items = items;
+        this._switcher = new CtrlAltTabSwitcher(items);
+        this.actor.add_actor(this._switcher.actor);
+
+        if (startBackwards)
+            this._selection = this._items.length - 1;
+        this._select(this._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;
+    },
+
+    _next : function() {
+        return mod(this._selection + 1, this._items.length);
+    },
+
+    _previous : function() {
+        return mod(this._selection - 1, this._items.length);
+    },
+
+    _keyPressEvent : function(actor, event) {
+        let keysym = event.get_key_symbol();
+        let shift = (Shell.get_event_state(event) & Clutter.ModifierType.SHIFT_MASK);
+        if (shift && keysym == Clutter.KEY_Tab)
+            keysym = Clutter.ISO_Left_Tab;
+
+        if (keysym == Clutter.KEY_Escape)
+            this.destroy();
+        else if (keysym == Clutter.KEY_Tab)
+            this._select(this._next());
+        else if (keysym == Clutter.KEY_ISO_Left_Tab)
+            this._select(this._previous());
+        else if (keysym == Clutter.KEY_Left)
+            this._select(this._previous());
+        else if (keysym == Clutter.KEY_Right)
+            this._select(this._next());
+
+        return true;
+    },
+
+    _keyReleaseEvent : function(actor, event) {
+        let [x, y, mods] = global.get_pointer();
+        let state = mods & Clutter.ModifierType.MOD1_MASK;
+
+        if (state == 0)
+            this._finish();
+
+        return true;
+    },
+
+    _finish : function() {
+        this.destroy();
+
+        let item = this._items[this._selection];
+        if (item.root)
+            Main.ctrlAltTabManager.focusGroup(item.root);
+        else
+            Main.activateWindow(item.window);
+    },
+
+    _popModal: function() {
+        if (this._haveModal) {
+            Main.popModal(this.actor);
+            this._haveModal = false;
+        }
+    },
+
+    destroy : function() {
+        this._popModal();
+        Tweener.addTween(this.actor,
+                         { opacity: 0,
+                           time: POPUP_FADE_TIME,
+                           transition: 'easeOutQuad',
+                           onComplete: Lang.bind(this,
+                               function() {
+                                   this.actor.destroy();
+                               })
+                         });
+    },
+
+    _onDestroy : function() {
+        if (this._keyPressEventId)
+            this.actor.disconnect(this._keyPressEventId);
+        if (this._keyReleaseEventId)
+            this.actor.disconnect(this._keyReleaseEventId);
+    },
+
+    _select : function(num) {
+        this._selection = num;
+        this._switcher.highlight(num);
+    }
+};
+
+function CtrlAltTabSwitcher(items) {
+    this._init(items);
+}
+
+CtrlAltTabSwitcher.prototype = {
+    __proto__ : AltTab.SwitcherList.prototype,
+
+    _init : function(items) {
+        AltTab.SwitcherList.prototype._init.call(this, true);
+
+        for (let i = 0; i < items.length; i++)
+            this._addIcon(items[i]);
+    },
+
+    _addIcon : function(item) {
+        let box = new St.BoxLayout({ style_class: 'alt-tab-app',
+                                     vertical: true });
+
+        let icon = item.iconActor;
+        if (!icon) {
+            icon = new St.Icon({ icon_name: item.iconName,
+                                 icon_type: St.IconType.SYMBOLIC,
+                                 icon_size: POPUP_APPICON_SIZE });
+        }
+        box.add(icon, { x_fill: false, y_fill: false } );
+
+        let text = new St.Label({ text: item.name });
+        box.add(text, { x_fill: false });
+
+        this.addItem(box);
+    }
+};
diff --git a/js/ui/main.js b/js/ui/main.js
index f76c33b..4a8f8ef 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -17,8 +17,11 @@ const Mainloop = imports.mainloop;
 const Meta = imports.gi.Meta;
 const Shell = imports.gi.Shell;
 const St = imports.gi.St;
+const Gettext = imports.gettext.domain('gnome-shell');
+const _ = Gettext.gettext;
 
 const Chrome = imports.ui.chrome;
+const CtrlAltTab = imports.ui.ctrlAltTab;
 const Environment = imports.ui.environment;
 const ExtensionSystem = imports.ui.extensionSystem;
 const MessageTray = imports.ui.messageTray;
@@ -50,6 +53,7 @@ let messageTray = null;
 let notificationDaemon = null;
 let windowAttentionHandler = null;
 let telepathyClient = null;
+let ctrlAltTabManager = null;
 let recorder = null;
 let shellDBusService = null;
 let modalCount = 0;
@@ -137,6 +141,9 @@ function start() {
     telepathyClient = new TelepathyClient.Client();
     panel.startStatusArea();
 
+    ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager();
+    ctrlAltTabManager.addGroup(panel.actor, _("Panel"), 'gnome-panel');
+
     _startDate = new Date();
 
     let recorderSettings = new Gio.Settings({ schema: 'org.gnome.shell.recorder' });
@@ -330,6 +337,14 @@ function _globalKeyPressHandler(actor, event) {
         case Meta.KeyBindingAction.COMMAND_2:
             getRunDialog().open();
             return true;
+        case Meta.KeyBindingAction.SWITCH_PANELS:
+            // Only intercept this when we're in the overview, and don't
+            // intercept it if something beyond that (like, say, the
+            // ctrl-alt-tab popup!) is visible
+            if (overview.visible && modalCount == 1) {
+                ctrlAltTabManager.popup(modifierState & Clutter.ModifierType.SHIFT_MASK);
+                return true;
+            }
     }
 
     return false;



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