#
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
/*
|
||||
* Credits:
|
||||
* This file is based on code from the Dash to Panel extension by Jason DeRose
|
||||
* and code from the Taskbar extension by Zorin OS
|
||||
* Some code was also adapted from the upstream Gnome Shell source code.
|
||||
*/
|
||||
const Clutter = imports.gi.Clutter;
|
||||
const GLib = imports.gi.GLib;
|
||||
const GObject = imports.gi.GObject;
|
||||
const St = imports.gi.St;
|
||||
const Mainloop = imports.mainloop;
|
||||
const Main = imports.ui.main;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
|
||||
const Params = imports.misc.params;
|
||||
const PopupMenu = imports.ui.popupMenu;
|
||||
const Workspace = imports.ui.workspace;
|
||||
|
||||
const Me = imports.misc.extensionUtils.getCurrentExtension();
|
||||
const Utils = Me.imports.utils;
|
||||
|
||||
const PREVIEW_MAX_WIDTH = 250;
|
||||
const PREVIEW_MAX_HEIGHT = 150;
|
||||
|
||||
const PREVIEW_ANIMATION_DURATION = 250;
|
||||
|
||||
var WindowPreviewMenu = class DashToDock_WindowPreviewMenu extends PopupMenu.PopupMenu {
|
||||
|
||||
constructor(source) {
|
||||
let side = Utils.getPosition();
|
||||
super(source.actor, 0.5, side);
|
||||
|
||||
// We want to keep the item hovered while the menu is up
|
||||
this.blockSourceEvents = true;
|
||||
|
||||
this._source = source;
|
||||
this._app = this._source.app;
|
||||
let monitorIndex = this._source.monitorIndex;
|
||||
|
||||
this.actor.add_style_class_name('app-well-menu');
|
||||
this.actor.set_style('max-width: ' + (Main.layoutManager.monitors[monitorIndex].width - 22) + 'px; ' +
|
||||
'max-height: ' + (Main.layoutManager.monitors[monitorIndex].height - 22) + 'px;');
|
||||
this.actor.hide();
|
||||
|
||||
// Chain our visibility and lifecycle to that of the source
|
||||
this._mappedId = this._source.actor.connect('notify::mapped', () => {
|
||||
if (!this._source.actor.mapped)
|
||||
this.close();
|
||||
});
|
||||
this._destroyId = this._source.actor.connect('destroy', this.destroy.bind(this));
|
||||
|
||||
Main.uiGroup.add_actor(this.actor);
|
||||
|
||||
// Change the initialized side where required.
|
||||
this._arrowSide = side;
|
||||
this._boxPointer._arrowSide = side;
|
||||
this._boxPointer._userArrowSide = side;
|
||||
|
||||
this.connect('destroy', this._onDestroy.bind(this));
|
||||
}
|
||||
|
||||
_redisplay() {
|
||||
if (this._previewBox)
|
||||
this._previewBox.destroy();
|
||||
this._previewBox = new WindowPreviewList(this._source);
|
||||
this.addMenuItem(this._previewBox);
|
||||
this._previewBox._redisplay();
|
||||
}
|
||||
|
||||
popup() {
|
||||
let windows = this._source.getInterestingWindows();
|
||||
if (windows.length > 0) {
|
||||
this._redisplay();
|
||||
this.open();
|
||||
this.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
|
||||
this._source.emit('sync-tooltip');
|
||||
}
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
if (this._mappedId)
|
||||
this._source.actor.disconnect(this._mappedId);
|
||||
|
||||
if (this._destroyId)
|
||||
this._source.actor.disconnect(this._destroyId);
|
||||
}
|
||||
};
|
||||
|
||||
var WindowPreviewList = class DashToDock_WindowPreviewList extends PopupMenu.PopupMenuSection {
|
||||
|
||||
constructor(source) {
|
||||
super();
|
||||
this.actor = new St.ScrollView({ name: 'dashtodockWindowScrollview',
|
||||
hscrollbar_policy: Gtk.PolicyType.NEVER,
|
||||
vscrollbar_policy: Gtk.PolicyType.NEVER,
|
||||
enable_mouse_scrolling: true });
|
||||
|
||||
this.actor.connect('scroll-event', this._onScrollEvent.bind(this));
|
||||
|
||||
let position = Utils.getPosition();
|
||||
this.isHorizontal = position == St.Side.BOTTOM || position == St.Side.TOP;
|
||||
this.box.set_vertical(!this.isHorizontal);
|
||||
this.box.set_name('dashtodockWindowList');
|
||||
this.actor.add_actor(this.box);
|
||||
this.actor._delegate = this;
|
||||
|
||||
this._shownInitially = false;
|
||||
|
||||
this._source = source;
|
||||
this.app = source.app;
|
||||
|
||||
this._redisplayId = Main.initializeDeferredWork(this.actor, this._redisplay.bind(this));
|
||||
|
||||
this.actor.connect('destroy', this._onDestroy.bind(this));
|
||||
this._stateChangedId = this.app.connect('windows-changed',
|
||||
this._queueRedisplay.bind(this));
|
||||
}
|
||||
|
||||
_queueRedisplay () {
|
||||
Main.queueDeferredWork(this._redisplayId);
|
||||
}
|
||||
|
||||
_onScrollEvent(actor, event) {
|
||||
// Event coordinates are relative to the stage but can be transformed
|
||||
// as the actor will only receive events within his bounds.
|
||||
let stage_x, stage_y, ok, event_x, event_y, actor_w, actor_h;
|
||||
[stage_x, stage_y] = event.get_coords();
|
||||
[ok, event_x, event_y] = actor.transform_stage_point(stage_x, stage_y);
|
||||
[actor_w, actor_h] = actor.get_size();
|
||||
|
||||
// If the scroll event is within a 1px margin from
|
||||
// the relevant edge of the actor, let the event propagate.
|
||||
if (event_y >= actor_h - 2)
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
|
||||
// Skip to avoid double events mouse
|
||||
if (event.is_pointer_emulated())
|
||||
return Clutter.EVENT_STOP;
|
||||
|
||||
let adjustment, delta;
|
||||
|
||||
if (this.isHorizontal)
|
||||
adjustment = this.actor.get_hscroll_bar().get_adjustment();
|
||||
else
|
||||
adjustment = this.actor.get_vscroll_bar().get_adjustment();
|
||||
|
||||
let increment = adjustment.step_increment;
|
||||
|
||||
switch ( event.get_scroll_direction() ) {
|
||||
case Clutter.ScrollDirection.UP:
|
||||
delta = -increment;
|
||||
break;
|
||||
case Clutter.ScrollDirection.DOWN:
|
||||
delta = +increment;
|
||||
break;
|
||||
case Clutter.ScrollDirection.SMOOTH:
|
||||
let [dx, dy] = event.get_scroll_delta();
|
||||
delta = dy*increment;
|
||||
delta += dx*increment;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
adjustment.set_value(adjustment.get_value() + delta);
|
||||
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
this.app.disconnect(this._stateChangedId);
|
||||
this._stateChangedId = 0;
|
||||
}
|
||||
|
||||
_createPreviewItem(window) {
|
||||
let preview = new WindowPreviewMenuItem(window);
|
||||
return preview;
|
||||
}
|
||||
|
||||
_redisplay () {
|
||||
let children = this._getMenuItems().filter(function(actor) {
|
||||
return actor._window;
|
||||
});
|
||||
|
||||
// Windows currently on the menu
|
||||
let oldWin = children.map(function(actor) {
|
||||
return actor._window;
|
||||
});
|
||||
|
||||
// All app windows with a static order
|
||||
let newWin = this._source.getInterestingWindows().sort(function(a, b) {
|
||||
return a.get_stable_sequence() > b.get_stable_sequence();
|
||||
});
|
||||
|
||||
let addedItems = [];
|
||||
let removedActors = [];
|
||||
|
||||
let newIndex = 0;
|
||||
let oldIndex = 0;
|
||||
|
||||
while (newIndex < newWin.length || oldIndex < oldWin.length) {
|
||||
// No change at oldIndex/newIndex
|
||||
if (oldWin[oldIndex] &&
|
||||
oldWin[oldIndex] == newWin[newIndex]) {
|
||||
oldIndex++;
|
||||
newIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Window removed at oldIndex
|
||||
if (oldWin[oldIndex] &&
|
||||
newWin.indexOf(oldWin[oldIndex]) == -1) {
|
||||
removedActors.push(children[oldIndex]);
|
||||
oldIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Window added at newIndex
|
||||
if (newWin[newIndex] &&
|
||||
oldWin.indexOf(newWin[newIndex]) == -1) {
|
||||
addedItems.push({ item: this._createPreviewItem(newWin[newIndex]),
|
||||
pos: newIndex });
|
||||
newIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Window moved
|
||||
let insertHere = newWin[newIndex + 1] &&
|
||||
newWin[newIndex + 1] == oldWin[oldIndex];
|
||||
let alreadyRemoved = removedActors.reduce(function(result, actor) {
|
||||
let removedWin = actor._window;
|
||||
return result || removedWin == newWin[newIndex];
|
||||
}, false);
|
||||
|
||||
if (insertHere || alreadyRemoved) {
|
||||
addedItems.push({ item: this._createPreviewItem(newWin[newIndex]),
|
||||
pos: newIndex + removedActors.length });
|
||||
newIndex++;
|
||||
} else {
|
||||
removedActors.push(children[oldIndex]);
|
||||
oldIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < addedItems.length; i++)
|
||||
this.addMenuItem(addedItems[i].item,
|
||||
addedItems[i].pos);
|
||||
|
||||
for (let i = 0; i < removedActors.length; i++) {
|
||||
let item = removedActors[i];
|
||||
if (this._shownInitially)
|
||||
item._animateOutAndDestroy();
|
||||
else
|
||||
item.actor.destroy();
|
||||
}
|
||||
|
||||
// Skip animations on first run when adding the initial set
|
||||
// of items, to avoid all items zooming in at once
|
||||
let animate = this._shownInitially;
|
||||
|
||||
if (!this._shownInitially)
|
||||
this._shownInitially = true;
|
||||
|
||||
for (let i = 0; i < addedItems.length; i++)
|
||||
addedItems[i].item.show(animate);
|
||||
|
||||
// Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744
|
||||
// Without it, StBoxLayout may use a stale size cache
|
||||
this.box.queue_relayout();
|
||||
|
||||
if (newWin.length < 1)
|
||||
this._getTopMenu().close(~0);
|
||||
|
||||
// As for upstream:
|
||||
// St.ScrollView always requests space horizontally for a possible vertical
|
||||
// scrollbar if in AUTOMATIC mode. Doing better would require implementation
|
||||
// of width-for-height in St.BoxLayout and St.ScrollView. This looks bad
|
||||
// when we *don't* need it, so turn off the scrollbar when that's true.
|
||||
// Dynamic changes in whether we need it aren't handled properly.
|
||||
let needsScrollbar = this._needsScrollbar();
|
||||
let scrollbar_policy = needsScrollbar ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER;
|
||||
if (this.isHorizontal)
|
||||
this.actor.hscrollbar_policy = scrollbar_policy;
|
||||
else
|
||||
this.actor.vscrollbar_policy = scrollbar_policy;
|
||||
|
||||
if (needsScrollbar)
|
||||
this.actor.add_style_pseudo_class('scrolled');
|
||||
else
|
||||
this.actor.remove_style_pseudo_class('scrolled');
|
||||
}
|
||||
|
||||
_needsScrollbar() {
|
||||
let topMenu = this._getTopMenu();
|
||||
let topThemeNode = topMenu.actor.get_theme_node();
|
||||
if (this.isHorizontal) {
|
||||
let [topMinWidth, topNaturalWidth] = topMenu.actor.get_preferred_width(-1);
|
||||
let topMaxWidth = topThemeNode.get_max_width();
|
||||
return topMaxWidth >= 0 && topNaturalWidth >= topMaxWidth;
|
||||
} else {
|
||||
let [topMinHeight, topNaturalHeight] = topMenu.actor.get_preferred_height(-1);
|
||||
let topMaxHeight = topThemeNode.get_max_height();
|
||||
return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
isAnimatingOut() {
|
||||
return this.actor.get_children().reduce(function(result, actor) {
|
||||
return result || actor.animatingOut;
|
||||
}, false);
|
||||
}
|
||||
};
|
||||
|
||||
var WindowPreviewMenuItem = GObject.registerClass(
|
||||
class DashToDock_WindowPreviewMenuItem extends PopupMenu.PopupBaseMenuItem {
|
||||
_init(window, params) {
|
||||
super._init(params);
|
||||
|
||||
this._window = window;
|
||||
this._destroyId = 0;
|
||||
this._windowAddedId = 0;
|
||||
|
||||
// We don't want this: it adds spacing on the left of the item.
|
||||
this.remove_child(this._ornamentLabel);
|
||||
this.add_style_class_name('dashtodock-app-well-preview-menu-item');
|
||||
|
||||
this._cloneBin = new St.Bin();
|
||||
this._cloneBin.set_size(PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT);
|
||||
|
||||
// TODO: improve the way the closebutton is layout. Just use some padding
|
||||
// for the moment.
|
||||
this._cloneBin.set_style('padding-bottom: 0.5em');
|
||||
|
||||
this.closeButton = new St.Button({ style_class: 'window-close',
|
||||
x_expand: true,
|
||||
y_expand: true});
|
||||
this.closeButton.add_actor(new St.Icon({ icon_name: 'window-close-symbolic' }));
|
||||
this.closeButton.set_x_align(Clutter.ActorAlign.END);
|
||||
this.closeButton.set_y_align(Clutter.ActorAlign.START);
|
||||
|
||||
|
||||
this.closeButton.opacity = 0;
|
||||
this.closeButton.connect('clicked', this._closeWindow.bind(this));
|
||||
|
||||
let overlayGroup = new Clutter.Actor({layout_manager: new Clutter.BinLayout() });
|
||||
|
||||
overlayGroup.add_actor(this._cloneBin);
|
||||
overlayGroup.add_actor(this.closeButton);
|
||||
|
||||
let label = new St.Label({ text: window.get_title()});
|
||||
label.set_style('max-width: '+PREVIEW_MAX_WIDTH +'px');
|
||||
let labelBin = new St.Bin({ child: label,
|
||||
x_align: St.Align.MIDDLE});
|
||||
|
||||
this._windowTitleId = this._window.connect('notify::title', () => {
|
||||
label.set_text(this._window.get_title());
|
||||
});
|
||||
|
||||
let box = new St.BoxLayout({ vertical: true,
|
||||
reactive:true,
|
||||
x_expand:true });
|
||||
box.add(overlayGroup);
|
||||
box.add(labelBin);
|
||||
this.add_actor(box);
|
||||
|
||||
this.connect('enter-event', this._onEnter.bind(this));
|
||||
this.connect('leave-event', this._onLeave.bind(this));
|
||||
this.connect('key-focus-in', this._onEnter.bind(this));
|
||||
this.connect('key-focus-out', this._onLeave.bind(this));
|
||||
|
||||
this._cloneTexture(window);
|
||||
|
||||
}
|
||||
|
||||
_cloneTexture(metaWin){
|
||||
|
||||
let mutterWindow = metaWin.get_compositor_private();
|
||||
|
||||
// Newly-created windows are added to a workspace before
|
||||
// the compositor finds out about them...
|
||||
// Moreover sometimes they return an empty texture, thus as a workarounf also check for it size
|
||||
if (!mutterWindow || !mutterWindow.get_texture() || !mutterWindow.get_size()[0]) {
|
||||
this._cloneTextureId = Mainloop.idle_add(() => {
|
||||
// Check if there's still a point in getting the texture,
|
||||
// otherwise this could go on indefinitely
|
||||
if (metaWin.get_workspace())
|
||||
this._cloneTexture(metaWin);
|
||||
this._cloneTextureId = 0;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
GLib.Source.set_name_by_id(this._cloneTextureId, '[dash-to-dock] this._cloneTexture');
|
||||
return;
|
||||
}
|
||||
|
||||
let [width, height] = mutterWindow.get_size();
|
||||
let scale = Math.min(1.0, PREVIEW_MAX_WIDTH/width, PREVIEW_MAX_HEIGHT/height);
|
||||
let clone = new Clutter.Clone ({ source: mutterWindow,
|
||||
reactive: true,
|
||||
width: width * scale,
|
||||
height: height * scale });
|
||||
|
||||
// when the source actor is destroyed, i.e. the window closed, first destroy the clone
|
||||
// and then destroy the menu item (do this animating out)
|
||||
this._destroyId = mutterWindow.connect('destroy', () => {
|
||||
clone.destroy();
|
||||
this._destroyId = 0; // avoid to try to disconnect this signal from mutterWindow in _onDestroy(),
|
||||
// as the object was just destroyed
|
||||
this._animateOutAndDestroy();
|
||||
});
|
||||
|
||||
this._clone = clone;
|
||||
this._mutterWindow = mutterWindow;
|
||||
this._cloneBin.set_child(this._clone);
|
||||
|
||||
this._clone.connect('destroy', () => {
|
||||
if (this._destroyId) {
|
||||
mutterWindow.disconnect(this._destroyId);
|
||||
this._destroyId = 0;
|
||||
}
|
||||
this._clone = null;
|
||||
})
|
||||
}
|
||||
|
||||
_windowCanClose() {
|
||||
return this._window.can_close() &&
|
||||
!this._hasAttachedDialogs();
|
||||
}
|
||||
|
||||
_closeWindow(actor) {
|
||||
this._workspace = this._window.get_workspace();
|
||||
|
||||
// This mechanism is copied from the workspace.js upstream code
|
||||
// It forces window activation if the windows don't get closed,
|
||||
// for instance because asking user confirmation, by monitoring the opening of
|
||||
// such additional confirmation window
|
||||
this._windowAddedId = this._workspace.connect('window-added',
|
||||
this._onWindowAdded.bind(this));
|
||||
|
||||
this.deleteAllWindows();
|
||||
}
|
||||
|
||||
deleteAllWindows() {
|
||||
// Delete all windows, starting from the bottom-most (most-modal) one
|
||||
//let windows = this._window.get_compositor_private().get_children();
|
||||
let windows = this._clone.get_children();
|
||||
for (let i = windows.length - 1; i >= 1; i--) {
|
||||
let realWindow = windows[i].source;
|
||||
let metaWindow = realWindow.meta_window;
|
||||
|
||||
metaWindow.delete(global.get_current_time());
|
||||
}
|
||||
|
||||
this._window.delete(global.get_current_time());
|
||||
}
|
||||
|
||||
_onWindowAdded(workspace, win) {
|
||||
let metaWindow = this._window;
|
||||
|
||||
if (win.get_transient_for() == metaWindow) {
|
||||
workspace.disconnect(this._windowAddedId);
|
||||
this._windowAddedId = 0;
|
||||
|
||||
// use an idle handler to avoid mapping problems -
|
||||
// see comment in Workspace._windowAdded
|
||||
let activationEvent = Clutter.get_current_event();
|
||||
let id = Mainloop.idle_add(() => {
|
||||
this.emit('activate', activationEvent);
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
GLib.Source.set_name_by_id(id, '[dash-to-dock] this.emit');
|
||||
}
|
||||
}
|
||||
|
||||
_hasAttachedDialogs() {
|
||||
// count trasient windows
|
||||
let n=0;
|
||||
this._window.foreach_transient(function(){n++;});
|
||||
return n>0;
|
||||
}
|
||||
|
||||
_onEnter() {
|
||||
this._showCloseButton();
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
_onLeave() {
|
||||
if (!this._cloneBin.has_pointer &&
|
||||
!this.closeButton.has_pointer)
|
||||
this._hideCloseButton();
|
||||
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
_idleToggleCloseButton() {
|
||||
this._idleToggleCloseId = 0;
|
||||
|
||||
if (!this._cloneBin.has_pointer &&
|
||||
!this.closeButton.has_pointer)
|
||||
this._hideCloseButton();
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
_showCloseButton() {
|
||||
|
||||
if (this._windowCanClose()) {
|
||||
this.closeButton.show();
|
||||
this.closeButton.remove_all_transitions();
|
||||
this.closeButton.ease({
|
||||
opacity: 255,
|
||||
duration: Workspace.WINDOW_OVERLAY_FADE_TIME,
|
||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_hideCloseButton() {
|
||||
this.closeButton.remove_all_transitions();
|
||||
this.closeButton.ease({
|
||||
opacity: 0,
|
||||
duration: Workspace.WINDOW_OVERLAY_FADE_TIME,
|
||||
mode: Clutter.AnimationMode.EASE_IN_QUAD
|
||||
});
|
||||
}
|
||||
|
||||
show(animate) {
|
||||
let fullWidth = this.get_width();
|
||||
|
||||
this.opacity = 0;
|
||||
this.set_width(0);
|
||||
|
||||
let time = animate ? PREVIEW_ANIMATION_DURATION : 0;
|
||||
this.remove_all_transitions();
|
||||
this.ease({
|
||||
opacity: 255,
|
||||
width: fullWidth,
|
||||
duration: time,
|
||||
mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD,
|
||||
});
|
||||
}
|
||||
|
||||
_animateOutAndDestroy() {
|
||||
this.remove_all_transitions();
|
||||
this.ease({
|
||||
opacity: 0,
|
||||
duration: PREVIEW_ANIMATION_DURATION,
|
||||
});
|
||||
|
||||
this.ease({
|
||||
width: 0,
|
||||
height: 0,
|
||||
duration: PREVIEW_ANIMATION_DURATION,
|
||||
delay: PREVIEW_ANIMATION_DURATION,
|
||||
onComplete: () => this.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
activate() {
|
||||
this._getTopMenu().close();
|
||||
Main.activateWindow(this._window);
|
||||
}
|
||||
|
||||
_onDestroy() {
|
||||
super._onDestroy();
|
||||
|
||||
if (this._cloneTextureId) {
|
||||
GLib.source_remove(this._cloneTextureId);
|
||||
this._cloneTextureId = 0;
|
||||
}
|
||||
|
||||
if (this._windowAddedId > 0) {
|
||||
this._workspace.disconnect(this._windowAddedId);
|
||||
this._windowAddedId = 0;
|
||||
}
|
||||
|
||||
if (this._destroyId > 0) {
|
||||
this._mutterWindow.disconnect(this._destroyId);
|
||||
this._destroyId = 0;
|
||||
}
|
||||
|
||||
if (this._windowTitleId > 0) {
|
||||
this._window.disconnect(this._windowTitleId);
|
||||
this._windowTitleId = 0;
|
||||
}
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user