In All&Thumbnails mode, show an application icon above the thumbnail, for easier recognition of the window. The old behaviour can be restored in the preferences dialog.
354 lines
12 KiB
JavaScript
354 lines
12 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
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 Mainloop = imports.mainloop;
|
|
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 ModalDialog = imports.ui.modalDialog;
|
|
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 SETTINGS_SHOW_APP_ICON_KEY = 'show-app-icon';
|
|
|
|
function mod(a, b) {
|
|
return ((a+b) % b);
|
|
}
|
|
|
|
const AltTabPopupAllThumbnails = new Lang.Class({
|
|
Name: 'AlternateTab.AltTabPopup.AllThumbnails',
|
|
|
|
_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) {
|
|
// 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
|
|
let 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();
|
|
});
|
|
|
|
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;
|
|
},
|
|
|
|
_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: 'WindowIcon',
|
|
|
|
_init: function(window, settings) {
|
|
this.window = window;
|
|
|
|
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 });
|
|
|
|
if (settings.get_boolean(SETTINGS_SHOW_APP_ICON_KEY)) {
|
|
let tracker = Shell.WindowTracker.get_default();
|
|
this.app = tracker.get_window_app(window);
|
|
}
|
|
},
|
|
|
|
set_size: function(size) {
|
|
let mutterWindow = this.window.get_compositor_private();
|
|
let windowTexture = mutterWindow.get_texture();
|
|
let [width, height] = windowTexture.get_size();
|
|
let scale = Math.min(1.0, size / width, size / height);
|
|
|
|
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.set_size(size, size);
|
|
this._iconBin.destroy_all_children();
|
|
this._iconBin.add_actor(this.clone);
|
|
|
|
if (this.app) {
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
|
|
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);
|
|
icon.set_size(128);
|
|
|
|
this.addItem(icon.actor, icon.label);
|
|
this.icons.push(icon);
|
|
}
|
|
}
|
|
});
|