diff --git a/js/ui/altTab.js b/js/ui/altTab.js new file mode 100644 index 000000000..2cb794346 --- /dev/null +++ b/js/ui/altTab.js @@ -0,0 +1,221 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Big = imports.gi.Big; +const Clutter = imports.gi.Clutter; +const Lang = imports.lang; +const Pango = imports.gi.Pango; +const Shell = imports.gi.Shell; + +const Main = imports.ui.main; +const Overlay = imports.ui.overlay; +const Tweener = imports.ui.tweener; + +const POPUP_BG_COLOR = new Clutter.Color(); +POPUP_BG_COLOR.from_pixel(0x00000080); +const POPUP_INDICATOR_COLOR = new Clutter.Color(); +POPUP_INDICATOR_COLOR.from_pixel(0xf0f0f0ff); +const POPUP_TRANSPARENT = new Clutter.Color(); +POPUP_TRANSPARENT.from_pixel(0x00000000); + +const RED = new Clutter.Color(); +RED.from_pixel(0xff0000ff); +const GREEN = new Clutter.Color(); +GREEN.from_pixel(0x00ff00ff); +const BLUE = new Clutter.Color(); +BLUE.from_pixel(0x0000ffff); + +const POPUP_INDICATOR_WIDTH = 4; +const POPUP_GRID_SPACING = 8; +const POPUP_ICON_SIZE = 48; +const POPUP_NUM_COLUMNS = 5; + +const POPUP_LABEL_MAX_WIDTH = POPUP_NUM_COLUMNS * (POPUP_ICON_SIZE + POPUP_GRID_SPACING); + +const OVERLAY_COLOR = new Clutter.Color(); +OVERLAY_COLOR.from_pixel(0x00000044); + +function AltTabPopup() { + this._init(); +} + +AltTabPopup.prototype = { + _init : function() { + let global = Shell.Global.get(); + + this.actor = new Big.Box({ background_color : POPUP_BG_COLOR, + corner_radius: POPUP_GRID_SPACING, + padding: POPUP_GRID_SPACING, + spacing: POPUP_GRID_SPACING, + orientation: Big.BoxOrientation.VERTICAL }); + + // Icon grid. It would be nice to use Tidy.Grid for the this, + // but Tidy.Grid is lame in various ways. (Eg, it seems to + // have a minimum size of 200x200.) So we create a vertical + // Big.Box containing multiple horizontal Big.Boxes. + this._grid = new Big.Box({ spacing: POPUP_GRID_SPACING, + orientation: Big.BoxOrientation.VERTICAL }); + let gcenterbox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, + x_align: Big.BoxAlignment.CENTER }); + gcenterbox.append(this._grid, Big.BoxPackFlags.NONE); + this.actor.append(gcenterbox, Big.BoxPackFlags.NONE); + + // Selected-window label + this._label = new Clutter.Text({ font_name: "Sans 12", + ellipsize: Pango.EllipsizeMode.END }); + + let labelbox = new Big.Box({ background_color: POPUP_INDICATOR_COLOR, + corner_radius: POPUP_GRID_SPACING / 2, + padding: POPUP_GRID_SPACING / 2 }); + labelbox.append(this._label, Big.BoxPackFlags.EXPAND); + let lcenterbox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, + x_align: Big.BoxAlignment.CENTER, + width: POPUP_LABEL_MAX_WIDTH + POPUP_GRID_SPACING }); + lcenterbox.append(labelbox, Big.BoxPackFlags.NONE); + this.actor.append(lcenterbox, Big.BoxPackFlags.NONE); + + // Indicator around selected icon + this._indicator = new Big.Rectangle({ border_width: POPUP_INDICATOR_WIDTH, + corner_radius: POPUP_INDICATOR_WIDTH / 2, + border_color: POPUP_INDICATOR_COLOR, + color: POPUP_TRANSPARENT }); + this.actor.append(this._indicator, Big.BoxPackFlags.FIXED); + + this._items = []; + this._toplevels = global.window_group.get_children(); + + global.stage.add_actor(this.actor); + + // Dark translucent window used to cover all but the + // currently-selected window while Alt-Tabbing. + // FIXME: share overlay code with runDialog.js + this._overlay = new Clutter.Rectangle({ color: OVERLAY_COLOR, + width: global.screen_width, + height: global.screen_height, + border_width: 0, + reactive: true }); + }, + + addWindow : function(win) { + let item = { window: win, + metaWindow: win.get_meta_window() }; + + let pixbuf = item.metaWindow.icon; + item.icon = new Clutter.Texture({ width: POPUP_ICON_SIZE, + height: POPUP_ICON_SIZE, + keep_aspect_ratio: true }); + Shell.clutter_texture_set_from_pixbuf(item.icon, pixbuf); + + item.box = new Big.Box({ padding: POPUP_INDICATOR_WIDTH * 2 }); + item.box.append(item.icon, Big.BoxPackFlags.NONE); + + item.above = null; + for (let i = 1; i < this._toplevels.length; i++) { + if (this._toplevels[i] == win) { + item.above = this._toplevels[i - 1]; + break; + } + } + + item.n = this._items.length; + this._items.push(item); + + // Add it to the grid + if (!this._gridRow || this._gridRow.get_children().length == POPUP_NUM_COLUMNS) { + this._gridRow = new Big.Box({ spacing: POPUP_GRID_SPACING, + orientation: Big.BoxOrientation.HORIZONTAL }); + this._grid.append(this._gridRow, Big.BoxPackFlags.NONE); + } + this._gridRow.append(item.box, Big.BoxPackFlags.NONE); + }, + + show : function(initialSelection) { + let global = Shell.Global.get(); + + Main.startModal(); + + global.window_group.add_actor(this._overlay); + this._overlay.raise_top(); + this._overlay.show(); + + this.actor.show_all(); + this.actor.x = (global.screen_width - this.actor.width) / 2; + this.actor.y = (global.screen_height - this.actor.height) / 2; + + this.select(initialSelection); + }, + + destroy : function() { + this.actor.hide(); + this._overlay.hide(); + this._overlay.unparent(); + + Main.endModal(); + }, + + select : function(n) { + if (this._selected) { + // Unselect previous + + if (this._allocationChangedId) { + this._selected.box.disconnect(this._allocationChangedId); + delete this._allocationChangedId; + } + + if (this._selected.above) + this._selected.window.raise(this._selected.above); + else + this._selected.window.lower_bottom(); + } + + let item = this._items[n]; + let changed = this._selected && item != this._selected; + this._selected = item; + + if (this._selected) { + this._label.set_size(-1, -1); + this._label.text = this._selected.metaWindow.title; + if (this._label.width > POPUP_LABEL_MAX_WIDTH) + this._label.width = POPUP_LABEL_MAX_WIDTH; + + // Figure out this._selected.box's coordinates in terms of + // this.actor + let bx = this._selected.box.x, by = this._selected.box.y; + let actor = this._selected.box.get_parent(); + while (actor != this.actor) { + bx += actor.x; + by += actor.y; + actor = actor.get_parent(); + } + + if (changed) { + Tweener.addTween(this._indicator, + { x: bx, + y: by, + width: this._selected.box.width, + height: this._selected.box.height, + time: Overlay.ANIMATION_TIME }); + } else { + Tweener.removeTweens(this.indicator); + this._indicator.set_position(bx, by); + this._indicator.set_size(this._selected.box.width, + this._selected.box.height); + } + this._indicator.show(); + + if (this._overlay.visible) + this._selected.window.raise(this._overlay); + + this._allocationChangedId = + this._selected.box.connect('notify::allocation', + Lang.bind(this, this._allocationChanged)); + } else { + this._label.text = ""; + this._indicator.hide(); + } + }, + + _allocationChanged : function() { + if (this._selected) + this.select(this._selected.n); + } +}; diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js index 2568e6a87..8908cbb4f 100644 --- a/js/ui/windowManager.js +++ b/js/ui/windowManager.js @@ -5,6 +5,7 @@ const Mainloop = imports.mainloop; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; +const AltTab = imports.ui.altTab; const Main = imports.ui.main; const Tweener = imports.ui.tweener; @@ -77,6 +78,11 @@ WindowManager.prototype = { function(o, actor) { me._destroyWindowDone(actor); }); + + this._shellwm.connect('begin-alt-tab', + function(o, handler) { + me._beginAltTab(handler); + }); }, _shouldAnimate : function(actor) { @@ -365,6 +371,14 @@ WindowManager.prototype = { switchData.outGroup.destroy(); this._shellwm.completed_switch_workspace(); - } + }, + _beginAltTab : function(handler) { + let popup = new AltTab.AltTabPopup(); + + handler.connect('window-added', function(handler, window) { popup.addWindow(window); }); + handler.connect('show', function(handler, initialSelection) { popup.show(initialSelection); }); + handler.connect('destroy', function() { popup.destroy(); }); + handler.connect('notify::selected', function() { popup.select(handler.selected); }); + } }; diff --git a/src/Makefile.am b/src/Makefile.am index 7c53080b0..ac49049a8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -49,6 +49,8 @@ CLEANFILES += $(SHELL_STAMP_FILES) libgnome_shell_la_SOURCES = \ $(shell_built_sources) \ gnome-shell-plugin.c \ + shell-alttab.c \ + shell-alttab.h \ shell-app-monitor.c \ shell-app-monitor.h \ shell-app-system.c \ diff --git a/src/shell-alttab.c b/src/shell-alttab.c new file mode 100644 index 000000000..0924f310f --- /dev/null +++ b/src/shell-alttab.c @@ -0,0 +1,230 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "shell-alttab.h" +#include "shell-global.h" +#include "shell-wm.h" +#include + +/* Our MetaAltTabHandler implementation; ideally we would implement + * this directly from JavaScript, but for now we can't. So we register + * this glue class as our MetaAltTabHandler and then when mutter + * creates one, we pass it on to ShellWM, which emits a signal to hand + * it off to javascript code, which then connects to the signals on + * this object. + */ + +static void shell_alt_tab_handler_interface_init (MetaAltTabHandlerInterface *handler_iface); + +G_DEFINE_TYPE_WITH_CODE (ShellAltTabHandler, shell_alt_tab_handler, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (META_TYPE_ALT_TAB_HANDLER, + shell_alt_tab_handler_interface_init)) + +/* Signals */ +enum +{ + WINDOW_ADDED, + SHOW, + DESTROY, + + LAST_SIGNAL +}; +static guint signals [LAST_SIGNAL] = { 0 }; + +enum +{ + PROP_SELECTED = 1, + PROP_SCREEN, + PROP_IMMEDIATE +}; + +static void +shell_alt_tab_handler_init (ShellAltTabHandler *sth) +{ + sth->windows = g_ptr_array_new (); + sth->selected = -1; +} + +static void +shell_alt_tab_handler_constructed (GObject *object) +{ + ShellGlobal *global = shell_global_get (); + ShellWM *wm; + + g_object_get (G_OBJECT (global), "window-manager", &wm, NULL); + _shell_wm_begin_alt_tab (wm, SHELL_ALT_TAB_HANDLER (object)); + g_object_unref (wm); +} + +static void +shell_alt_tab_handler_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellAltTabHandler *sth = SHELL_ALT_TAB_HANDLER (object); + + switch (prop_id) + { + case PROP_SCREEN: + /* We don't care */ + break; + case PROP_IMMEDIATE: + sth->immediate_mode = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_alt_tab_handler_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellAltTabHandler *sth = SHELL_ALT_TAB_HANDLER (object); + + switch (prop_id) + { + case PROP_SELECTED: + g_value_set_int (value, sth->selected); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_alt_tab_handler_finalize (GObject *object) +{ + ShellAltTabHandler *sth = SHELL_ALT_TAB_HANDLER (object); + + g_ptr_array_free (sth->windows, FALSE); + + G_OBJECT_CLASS (shell_alt_tab_handler_parent_class)->finalize (object); +} + +static void +shell_alt_tab_handler_add_window (MetaAltTabHandler *handler, + MetaWindow *window) +{ + ShellAltTabHandler *sth = SHELL_ALT_TAB_HANDLER (handler); + + g_ptr_array_add (sth->windows, window); + g_signal_emit (handler, signals[WINDOW_ADDED], 0, + meta_window_get_compositor_private (window)); +} + +static void +shell_alt_tab_handler_show (MetaAltTabHandler *handler, + MetaWindow *initial_selection) +{ + ShellAltTabHandler *sth = SHELL_ALT_TAB_HANDLER (handler); + int i; + + sth->selected = -1; + for (i = 0; i < sth->windows->len; i++) + { + if (sth->windows->pdata[i] == (gpointer)initial_selection) + { + sth->selected = i; + break; + } + } + + g_signal_emit (handler, signals[SHOW], 0, sth->selected); +} + +static void +shell_alt_tab_handler_destroy (MetaAltTabHandler *handler) +{ + g_signal_emit (handler, signals[DESTROY], 0); +} + +static void +shell_alt_tab_handler_forward (MetaAltTabHandler *handler) +{ + ShellAltTabHandler *sth = SHELL_ALT_TAB_HANDLER (handler); + + sth->selected = (sth->selected + 1) % sth->windows->len; + g_object_notify (G_OBJECT (handler), "selected"); +} + +static void +shell_alt_tab_handler_backward (MetaAltTabHandler *handler) +{ + ShellAltTabHandler *sth = SHELL_ALT_TAB_HANDLER (handler); + + sth->selected = (sth->selected - 1) % sth->windows->len; + g_object_notify (G_OBJECT (handler), "selected"); +} + +static MetaWindow * +shell_alt_tab_handler_get_selected (MetaAltTabHandler *handler) +{ + ShellAltTabHandler *sth = SHELL_ALT_TAB_HANDLER (handler); + + if (sth->selected > -1) + return sth->windows->pdata[sth->selected]; + else + return NULL; +} + +static void +shell_alt_tab_handler_class_init (ShellAltTabHandlerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = shell_alt_tab_handler_constructed; + object_class->set_property = shell_alt_tab_handler_set_property; + object_class->get_property = shell_alt_tab_handler_get_property; + object_class->finalize = shell_alt_tab_handler_finalize; + + g_object_class_override_property (object_class, PROP_SCREEN, "screen"); + g_object_class_override_property (object_class, PROP_IMMEDIATE, "immediate"); + g_object_class_install_property (object_class, + PROP_SELECTED, + g_param_spec_int ("selected", + "Selected", + "Selected window", + -1, G_MAXINT, -1, + G_PARAM_READABLE)); + + + signals[WINDOW_ADDED] = g_signal_new ("window-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + MUTTER_TYPE_COMP_WINDOW); + signals[SHOW] = g_signal_new ("show", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + signals[DESTROY] = g_signal_new ("destroy", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +shell_alt_tab_handler_interface_init (MetaAltTabHandlerInterface *handler_iface) +{ + handler_iface->add_window = shell_alt_tab_handler_add_window; + handler_iface->show = shell_alt_tab_handler_show; + handler_iface->destroy = shell_alt_tab_handler_destroy; + handler_iface->forward = shell_alt_tab_handler_forward; + handler_iface->backward = shell_alt_tab_handler_backward; + handler_iface->get_selected = shell_alt_tab_handler_get_selected; +} diff --git a/src/shell-alttab.h b/src/shell-alttab.h new file mode 100644 index 000000000..4e04b3b58 --- /dev/null +++ b/src/shell-alttab.h @@ -0,0 +1,34 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#ifndef SHELL_ALT_TAB_HANDLER_H +#define SHELL_ALT_TAB_HANDLER_H + +#include + +#define SHELL_TYPE_ALT_TAB_HANDLER (shell_alt_tab_handler_get_type ()) +#define SHELL_ALT_TAB_HANDLER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_ALT_TAB_HANDLER, ShellAltTabHandler)) +#define SHELL_ALT_TAB_HANDLER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_ALT_TAB_HANDLER, ShellAltTabHandlerClass)) +#define SHELL_IS_ALT_TAB_HANDLER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_ALT_TAB_HANDLER_TYPE)) +#define SHELL_IS_ALT_TAB_HANDLER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_ALT_TAB_HANDLER)) +#define SHELL_ALT_TAB_HANDLER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_ALT_TAB_HANDLER, ShellAltTabHandlerClass)) + +typedef struct _ShellAltTabHandler ShellAltTabHandler; +typedef struct _ShellAltTabHandlerClass ShellAltTabHandlerClass; + +struct _ShellAltTabHandler { + GObject parent_instance; + + GPtrArray *windows; + int selected; + gboolean immediate_mode; +}; + +struct _ShellAltTabHandlerClass { + GObjectClass parent_class; + +}; + +GType shell_alt_tab_handler_get_type (void); + +#endif + diff --git a/src/shell-wm.c b/src/shell-wm.c index 7b77a0415..42ec233c5 100644 --- a/src/shell-wm.c +++ b/src/shell-wm.c @@ -27,6 +27,8 @@ enum SWITCH_WORKSPACE, KILL_SWITCH_WORKSPACE, + BEGIN_ALT_TAB, + LAST_SIGNAL }; @@ -40,6 +42,7 @@ static guint shell_wm_signals [LAST_SIGNAL] = { 0 }; static void shell_wm_init (ShellWM *wm) { + meta_alt_tab_handler_register (SHELL_TYPE_ALT_TAB_HANDLER); } static void @@ -166,6 +169,15 @@ shell_wm_class_init (ShellWMClass *klass) NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); + shell_wm_signals[BEGIN_ALT_TAB] = + g_signal_new ("begin-alt-tab", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + META_TYPE_ALT_TAB_HANDLER); } void @@ -379,6 +391,14 @@ _shell_wm_destroy (ShellWM *wm, g_signal_emit (wm, shell_wm_signals[DESTROY], 0, actor); } +/* Called from shell-alttab.c */ +void +_shell_wm_begin_alt_tab (ShellWM *wm, + ShellAltTabHandler *handler) +{ + g_signal_emit (wm, shell_wm_signals[BEGIN_ALT_TAB], 0, handler); +} + /** * shell_wm_new: * @plugin: the #MutterPlugin diff --git a/src/shell-wm.h b/src/shell-wm.h index f5b349f68..59387cff8 100644 --- a/src/shell-wm.h +++ b/src/shell-wm.h @@ -4,6 +4,8 @@ #include #include +#include "shell-alttab.h" + G_BEGIN_DECLS typedef struct _ShellWM ShellWM; @@ -71,6 +73,11 @@ void _shell_wm_kill_effect (ShellWM *wm, MutterWindow *actor, gulong events); +/* Called by ShellAltTabHandler */ + +void _shell_wm_begin_alt_tab (ShellWM *wm, + ShellAltTabHandler *handler); + G_END_DECLS #endif /* __SHELL_WM_H__ */