292 lines
11 KiB
JavaScript
292 lines
11 KiB
JavaScript
/* -*- 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 Meta = imports.gi.Meta;
|
|
const Pango = imports.gi.Pango;
|
|
const Shell = imports.gi.Shell;
|
|
|
|
const Main = imports.ui.main;
|
|
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 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);
|
|
|
|
const SHOW_TIME = 0.05;
|
|
const SWITCH_TIME = 0.1;
|
|
|
|
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 16px",
|
|
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.NONE);
|
|
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. Actually
|
|
// contains four actors which can we rearrange to create
|
|
// a hole in the overlay.
|
|
this._overlay = new Clutter.Group({ reactive: true });
|
|
this._overlay_top = new Clutter.Rectangle({ color: OVERLAY_COLOR,
|
|
border_width: 0 });
|
|
this._overlay_bottom = new Clutter.Rectangle({ color: OVERLAY_COLOR,
|
|
border_width: 0 });
|
|
this._overlay_left = new Clutter.Rectangle({ color: OVERLAY_COLOR,
|
|
border_width: 0 });
|
|
this._overlay_right = new Clutter.Rectangle({ color: OVERLAY_COLOR,
|
|
border_width: 0 });
|
|
this._overlay.add_actor(this._overlay_top);
|
|
this._overlay.add_actor(this._overlay_bottom);
|
|
this._overlay.add_actor(this._overlay_left);
|
|
this._overlay.add_actor(this._overlay_right);
|
|
},
|
|
|
|
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.visible = item.metaWindow.showing_on_its_workspace();
|
|
|
|
if (!item.visible) {
|
|
let rect = new Meta.Rectangle();
|
|
if (item.metaWindow.get_icon_geometry(rect))
|
|
item.icon_rect = rect;
|
|
}
|
|
|
|
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.opacity = 0;
|
|
Tweener.addTween(this.actor, { opacity: 255,
|
|
time: SHOW_TIME,
|
|
transition: "easeOutQuad" });
|
|
|
|
this.actor.show_all();
|
|
this.actor.x = Math.floor((global.screen_width - this.actor.width) / 2);
|
|
this.actor.y = Math.floor((global.screen_height - this.actor.height) / 2);
|
|
|
|
this.select(initialSelection);
|
|
},
|
|
|
|
destroy : function() {
|
|
this.actor.destroy();
|
|
this._overlay.destroy();
|
|
|
|
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: SWITCH_TIME,
|
|
transition: "easeOutQuad" });
|
|
} 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) {
|
|
if (this._selected.visible)
|
|
this._selected.window.raise(this._overlay);
|
|
this._adjust_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);
|
|
},
|
|
|
|
_adjust_overlay : function() {
|
|
let global = Shell.Global.get();
|
|
|
|
if (this._selected && this._selected.icon_rect) {
|
|
// We want to highlight a specific rectangle within the
|
|
// task bar, so rearrange the pieces of the overlay to
|
|
// cover the whole screen except that rectangle
|
|
|
|
let rect = this._selected.icon_rect;
|
|
|
|
this._overlay_top.x = 0;
|
|
this._overlay_top.y = 0;
|
|
this._overlay_top.width = global.screen_width;
|
|
this._overlay_top.height = rect.y;
|
|
|
|
this._overlay_left.x = 0;
|
|
this._overlay_left.y = rect.y;
|
|
this._overlay_left.width = rect.x;
|
|
this._overlay_left.height = rect.height;
|
|
this._overlay_left.show();
|
|
|
|
this._overlay_right.x = rect.x + rect.width;
|
|
this._overlay_right.y = rect.y;
|
|
this._overlay_right.width = global.screen_width - rect.x - rect.width;
|
|
this._overlay_right.height = rect.height;
|
|
this._overlay_right.show();
|
|
|
|
this._overlay_bottom.x = 0;
|
|
this._overlay_bottom.y = rect.y + rect.height;
|
|
this._overlay_bottom.width = global.screen_width;
|
|
this._overlay_bottom.height = global.screen_height - rect.y - rect.height;
|
|
this._overlay_bottom.show();
|
|
} else {
|
|
// Either there's no current selection, or the selection
|
|
// is a visible window. Make the overlay cover the whole
|
|
// screen. select() will raise the selected window over
|
|
// the overlay.
|
|
|
|
this._overlay_top.x = 0;
|
|
this._overlay_top.y = 0;
|
|
this._overlay_top.width = global.screen_width;
|
|
this._overlay_top.height = global.screen_height;
|
|
this._overlay_top.show();
|
|
|
|
this._overlay_left.hide();
|
|
this._overlay_right.hide();
|
|
this._overlay_bottom.hide();
|
|
}
|
|
}
|
|
};
|