748739ed9c
This fixes the style, and also makes it keyboard navigable https://bugzilla.gnome.org/show_bug.cgi?id=619541
1674 lines
59 KiB
JavaScript
1674 lines
59 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const GdkPixbuf = imports.gi.GdkPixbuf;
|
|
const Gdk = imports.gi.Gdk;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const Meta = imports.gi.Meta;
|
|
const Pango = imports.gi.Pango;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
const Signals = imports.signals;
|
|
|
|
const DND = imports.ui.dnd;
|
|
const Lightbox = imports.ui.lightbox;
|
|
const Main = imports.ui.main;
|
|
const Overview = imports.ui.overview;
|
|
const Panel = imports.ui.panel;
|
|
const Tweener = imports.ui.tweener;
|
|
|
|
const FOCUS_ANIMATION_TIME = 0.15;
|
|
|
|
const WINDOW_DND_SIZE = 256;
|
|
|
|
const FRAME_COLOR = new Clutter.Color();
|
|
FRAME_COLOR.from_pixel(0xffffffff);
|
|
|
|
const SCROLL_SCALE_AMOUNT = 100 / 5;
|
|
|
|
const LIGHTBOX_FADE_TIME = 0.2;
|
|
|
|
const DRAGGING_WINDOW_OPACITY = 100;
|
|
|
|
// Define a layout scheme for small window counts. For larger
|
|
// counts we fall back to an algorithm. We need more schemes here
|
|
// unless we have a really good algorithm.
|
|
|
|
// Each triplet is [xCenter, yCenter, scale] where the scale
|
|
// is relative to the width of the workspace.
|
|
const POSITIONS = {
|
|
1: [[0.5, 0.5, 0.95]],
|
|
2: [[0.25, 0.5, 0.48], [0.75, 0.5, 0.48]],
|
|
3: [[0.25, 0.25, 0.48], [0.75, 0.25, 0.48], [0.5, 0.75, 0.48]],
|
|
4: [[0.25, 0.25, 0.47], [0.75, 0.25, 0.47], [0.75, 0.75, 0.47], [0.25, 0.75, 0.47]],
|
|
5: [[0.165, 0.25, 0.32], [0.495, 0.25, 0.32], [0.825, 0.25, 0.32], [0.25, 0.75, 0.32], [0.75, 0.75, 0.32]]
|
|
};
|
|
// Used in _orderWindowsPermutations, 5! = 120 which is probably the highest we can go
|
|
const POSITIONING_PERMUTATIONS_MAX = 5;
|
|
|
|
function _interpolate(start, end, step) {
|
|
return start + (end - start) * step;
|
|
}
|
|
|
|
function _clamp(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
// Spacing between workspaces. At the moment, the same spacing is used
|
|
// in both zoomed-in and zoomed-out views; this is slightly
|
|
// metaphor-breaking, but the alternatives are also weird.
|
|
const GRID_SPACING = 15;
|
|
const FRAME_SIZE = GRID_SPACING / 3;
|
|
|
|
function ScaledPoint(x, y, scaleX, scaleY) {
|
|
[this.x, this.y, this.scaleX, this.scaleY] = arguments;
|
|
}
|
|
|
|
ScaledPoint.prototype = {
|
|
getPosition : function() {
|
|
return [this.x, this.y];
|
|
},
|
|
|
|
getScale : function() {
|
|
return [this.scaleX, this.scaleY];
|
|
},
|
|
|
|
setPosition : function(x, y) {
|
|
[this.x, this.y] = arguments;
|
|
},
|
|
|
|
setScale : function(scaleX, scaleY) {
|
|
[this.scaleX, this.scaleY] = arguments;
|
|
},
|
|
|
|
interpPosition : function(other, step) {
|
|
return [_interpolate(this.x, other.x, step),
|
|
_interpolate(this.y, other.y, step)];
|
|
},
|
|
|
|
interpScale : function(other, step) {
|
|
return [_interpolate(this.scaleX, other.scaleX, step),
|
|
_interpolate(this.scaleY, other.scaleY, step)];
|
|
}
|
|
};
|
|
|
|
|
|
function WindowClone(realWindow) {
|
|
this._init(realWindow);
|
|
}
|
|
|
|
WindowClone.prototype = {
|
|
_init : function(realWindow) {
|
|
this.actor = new Clutter.Clone({ source: realWindow.get_texture(),
|
|
reactive: true,
|
|
x: realWindow.x,
|
|
y: realWindow.y });
|
|
this.actor._delegate = this;
|
|
this.realWindow = realWindow;
|
|
this.metaWindow = realWindow.meta_window;
|
|
this.metaWindow._delegate = this;
|
|
this.origX = realWindow.x;
|
|
this.origY = realWindow.y;
|
|
|
|
this._stackAbove = null;
|
|
|
|
this.actor.connect('button-release-event',
|
|
Lang.bind(this, this._onButtonRelease));
|
|
|
|
this.actor.connect('scroll-event',
|
|
Lang.bind(this, this._onScroll));
|
|
|
|
this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
|
|
this.actor.connect('leave-event',
|
|
Lang.bind(this, this._onLeave));
|
|
|
|
this._draggable = DND.makeDraggable(this.actor,
|
|
{ restoreOnSuccess: true,
|
|
dragActorMaxSize: WINDOW_DND_SIZE,
|
|
dragActorOpacity: DRAGGING_WINDOW_OPACITY });
|
|
this._draggable.connect('drag-begin', Lang.bind(this, this._onDragBegin));
|
|
this._draggable.connect('drag-end', Lang.bind(this, this._onDragEnd));
|
|
this.inDrag = false;
|
|
|
|
this._windowIsZooming = false;
|
|
this._zooming = false;
|
|
this._selected = false;
|
|
},
|
|
|
|
setStackAbove: function (actor) {
|
|
this._stackAbove = actor;
|
|
if (this.inDrag || this._zooming)
|
|
// We'll fix up the stack after the drag/zooming
|
|
return;
|
|
this.actor.raise(this._stackAbove);
|
|
},
|
|
|
|
destroy: function () {
|
|
this.actor.destroy();
|
|
},
|
|
|
|
zoomFromOverview: function() {
|
|
if (this._zooming) {
|
|
// If the user clicked on the zoomed window, or we are
|
|
// returning there anyways, then we can zoom right to the
|
|
// window, but if we are going to some other window, then
|
|
// we need to cancel the zoom before animating, or it
|
|
// will look funny.
|
|
|
|
if (!this._selected &&
|
|
this.metaWindow != global.screen.get_display().focus_window)
|
|
this._zoomEnd();
|
|
}
|
|
},
|
|
|
|
_onDestroy: function() {
|
|
this.metaWindow._delegate = null;
|
|
this.actor._delegate = null;
|
|
if (this._zoomLightbox)
|
|
this._zoomLightbox.destroy();
|
|
|
|
if (this.inDrag) {
|
|
this.emit('drag-end');
|
|
this.inDrag = false;
|
|
}
|
|
|
|
this.disconnectAll();
|
|
},
|
|
|
|
_onLeave: function (actor, event) {
|
|
if (this._zoomStep)
|
|
this._zoomEnd();
|
|
},
|
|
|
|
_onScroll : function (actor, event) {
|
|
let direction = event.get_scroll_direction();
|
|
if (direction == Clutter.ScrollDirection.UP) {
|
|
if (this._zoomStep == undefined)
|
|
this._zoomStart();
|
|
if (this._zoomStep < 100) {
|
|
this._zoomStep += SCROLL_SCALE_AMOUNT;
|
|
this._zoomUpdate();
|
|
}
|
|
} else if (direction == Clutter.ScrollDirection.DOWN) {
|
|
if (this._zoomStep > 0) {
|
|
this._zoomStep -= SCROLL_SCALE_AMOUNT;
|
|
this._zoomStep = Math.max(0, this._zoomStep);
|
|
this._zoomUpdate();
|
|
}
|
|
if (this._zoomStep <= 0.0)
|
|
this._zoomEnd();
|
|
}
|
|
|
|
},
|
|
|
|
_zoomUpdate : function () {
|
|
[this.actor.x, this.actor.y] = this._zoomGlobalOrig.interpPosition(this._zoomTarget, this._zoomStep / 100);
|
|
[this.actor.scale_x, this.actor.scale_y] = this._zoomGlobalOrig.interpScale(this._zoomTarget, this._zoomStep / 100);
|
|
|
|
let [width, height] = this.actor.get_transformed_size();
|
|
|
|
this.actor.x = _clamp(this.actor.x, 0, global.screen_width - width);
|
|
this.actor.y = _clamp(this.actor.y, Panel.PANEL_HEIGHT, global.screen_height - height);
|
|
},
|
|
|
|
_zoomStart : function () {
|
|
this._zooming = true;
|
|
this.emit('zoom-start');
|
|
|
|
if (!this._zoomLightbox)
|
|
this._zoomLightbox = new Lightbox.Lightbox(global.stage,
|
|
{ fadeTime: LIGHTBOX_FADE_TIME });
|
|
this._zoomLightbox.show();
|
|
|
|
this._zoomLocalOrig = new ScaledPoint(this.actor.x, this.actor.y, this.actor.scale_x, this.actor.scale_y);
|
|
this._zoomGlobalOrig = new ScaledPoint();
|
|
let parent = this._origParent = this.actor.get_parent();
|
|
let [width, height] = this.actor.get_transformed_size();
|
|
this._zoomGlobalOrig.setPosition.apply(this._zoomGlobalOrig, this.actor.get_transformed_position());
|
|
this._zoomGlobalOrig.setScale(width / this.actor.width, height / this.actor.height);
|
|
|
|
this.actor.reparent(global.stage);
|
|
this._zoomLightbox.highlight(this.actor);
|
|
|
|
[this.actor.x, this.actor.y] = this._zoomGlobalOrig.getPosition();
|
|
[this.actor.scale_x, this.actor.scale_y] = this._zoomGlobalOrig.getScale();
|
|
|
|
this.actor.raise_top();
|
|
|
|
this._zoomTarget = new ScaledPoint(0, 0, 1.0, 1.0);
|
|
this._zoomTarget.setPosition(this.actor.x - (this.actor.width - width) / 2, this.actor.y - (this.actor.height - height) / 2);
|
|
this._zoomStep = 0;
|
|
|
|
this._zoomUpdate();
|
|
},
|
|
|
|
_zoomEnd : function () {
|
|
this._zooming = false;
|
|
this.emit('zoom-end');
|
|
|
|
this.actor.reparent(this._origParent);
|
|
// If the workspace has been destroyed while we were reparented to
|
|
// the stage, _stackAbove will be unparented and we can't raise our
|
|
// actor above it - as we are bound to be destroyed anyway in that
|
|
// case, we can skip that step
|
|
if (this._stackAbove && this._stackAbove.get_parent())
|
|
this.actor.raise(this._stackAbove);
|
|
|
|
[this.actor.x, this.actor.y] = this._zoomLocalOrig.getPosition();
|
|
[this.actor.scale_x, this.actor.scale_y] = this._zoomLocalOrig.getScale();
|
|
|
|
this._zoomLightbox.hide();
|
|
|
|
this._zoomLocalPosition = undefined;
|
|
this._zoomLocalScale = undefined;
|
|
this._zoomGlobalPosition = undefined;
|
|
this._zoomGlobalScale = undefined;
|
|
this._zoomTargetPosition = undefined;
|
|
this._zoomStep = undefined;
|
|
},
|
|
|
|
_onButtonRelease : function (actor, event) {
|
|
this._selected = true;
|
|
this.emit('selected', event.get_time());
|
|
},
|
|
|
|
_onDragBegin : function (draggable, time) {
|
|
this.inDrag = true;
|
|
this.emit('drag-begin');
|
|
},
|
|
|
|
_onDragEnd : function (draggable, time, snapback) {
|
|
this.inDrag = false;
|
|
|
|
// We may not have a parent if DnD completed successfully, in
|
|
// which case our clone will shortly be destroyed and replaced
|
|
// with a new one on the target workspace.
|
|
if (this.actor.get_parent() != null)
|
|
this.actor.raise(this._stackAbove);
|
|
|
|
this.emit('drag-end');
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(WindowClone.prototype);
|
|
|
|
|
|
function DesktopClone(window) {
|
|
this._init(window);
|
|
}
|
|
|
|
DesktopClone.prototype = {
|
|
_init : function(window) {
|
|
this.actor = new Clutter.Group({ reactive: true });
|
|
|
|
let background = new Clutter.Clone({ source: Main.background.source });
|
|
this.actor.add_actor(background);
|
|
|
|
if (window) {
|
|
this._desktop = new Clutter.Clone({ source: window.get_texture() });
|
|
this.actor.add_actor(this._desktop);
|
|
this._desktop.hide();
|
|
} else {
|
|
this._desktop = null;
|
|
}
|
|
|
|
this.actor.connect('button-release-event',
|
|
Lang.bind(this, this._onButtonRelease));
|
|
},
|
|
|
|
zoomFromOverview: function(fadeInIcons) {
|
|
if (this._desktop == null)
|
|
return;
|
|
|
|
if (fadeInIcons) {
|
|
this._desktop.opacity = 0;
|
|
this._desktop.show();
|
|
Tweener.addTween(this._desktop,
|
|
{ opacity: 255,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeOutQuad' });
|
|
}
|
|
},
|
|
|
|
zoomToOverview: function(fadeOutIcons) {
|
|
if (this._desktop == null)
|
|
return;
|
|
|
|
if (fadeOutIcons) {
|
|
this._desktop.opacity = 255;
|
|
this._desktop.show();
|
|
Tweener.addTween(this._desktop,
|
|
{ opacity: 0,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this,
|
|
function() {
|
|
this._desktop.hide();
|
|
})
|
|
});
|
|
} else {
|
|
this._desktop.hide();
|
|
}
|
|
},
|
|
|
|
_onButtonRelease : function (actor, event) {
|
|
this.emit('selected', event.get_time());
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(DesktopClone.prototype);
|
|
|
|
|
|
/**
|
|
* @windowClone: Corresponding window clone
|
|
* @parentActor: The actor which will be the parent of all overlay items
|
|
* such as app icon and window caption
|
|
*/
|
|
function WindowOverlay(windowClone, parentActor) {
|
|
this._init(windowClone, parentActor);
|
|
}
|
|
|
|
WindowOverlay.prototype = {
|
|
_init : function(windowClone, parentActor) {
|
|
let metaWindow = windowClone.metaWindow;
|
|
|
|
this._windowClone = windowClone;
|
|
this._parentActor = parentActor;
|
|
|
|
let title = new St.Label({ style_class: 'window-caption',
|
|
text: metaWindow.title });
|
|
title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
|
|
title._spacing = 0;
|
|
|
|
this._updateCaptionId = metaWindow.connect('notify::title',
|
|
Lang.bind(this, function(w) {
|
|
this.title.text = w.title;
|
|
}));
|
|
|
|
let button = new St.Button({ style_class: 'window-close' });
|
|
button._overlap = 0;
|
|
|
|
this._idleToggleCloseId = 0;
|
|
button.connect('clicked', Lang.bind(this, this._closeWindow));
|
|
|
|
windowClone.actor.connect('destroy', Lang.bind(this, this._onDestroy));
|
|
windowClone.actor.connect('enter-event',
|
|
Lang.bind(this, this._onEnter));
|
|
windowClone.actor.connect('leave-event',
|
|
Lang.bind(this, this._onLeave));
|
|
|
|
this._windowAddedId = 0;
|
|
windowClone.connect('zoom-start', Lang.bind(this, this.hide));
|
|
windowClone.connect('zoom-end', Lang.bind(this, this.show));
|
|
|
|
button.hide();
|
|
|
|
this.title = title;
|
|
this.closeButton = button;
|
|
|
|
parentActor.add_actor(this.title);
|
|
parentActor.add_actor(this.closeButton);
|
|
title.connect('style-changed',
|
|
Lang.bind(this, this._onStyleChanged));
|
|
button.connect('style-changed',
|
|
Lang.bind(this, this._onStyleChanged));
|
|
// force a style change if we are already on a stage - otherwise
|
|
// the signal will be emitted normally when we are added
|
|
if (parentActor.get_stage())
|
|
this._onStyleChanged();
|
|
},
|
|
|
|
hide: function() {
|
|
this.closeButton.hide();
|
|
this.title.hide();
|
|
},
|
|
|
|
show: function() {
|
|
let [x, y, mask] = global.get_pointer();
|
|
let actor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE,
|
|
x, y);
|
|
if (actor == this._windowClone.actor) {
|
|
this.closeButton.show();
|
|
}
|
|
this.title.show();
|
|
},
|
|
|
|
fadeIn: function() {
|
|
this.title.opacity = 0;
|
|
this.title.show();
|
|
this._parentActor.raise_top();
|
|
Tweener.addTween(this.title,
|
|
{ opacity: 255,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeOutQuad' });
|
|
},
|
|
|
|
chromeWidth: function () {
|
|
return this.closeButton.width - this.closeButton._overlap;
|
|
},
|
|
|
|
chromeHeights: function () {
|
|
return [this.closeButton.height - this.closeButton._overlap,
|
|
this.title.height + this.title._spacing];
|
|
},
|
|
|
|
/**
|
|
* @cloneX: x position of windowClone
|
|
* @cloneY: y position of windowClone
|
|
* @cloneWidth: width of windowClone
|
|
* @cloneHeight height of windowClone
|
|
*/
|
|
// These parameters are not the values retrieved with
|
|
// get_transformed_position() and get_transformed_size(),
|
|
// as windowClone might be moving.
|
|
// See Workspace._fadeInWindowOverlay
|
|
updatePositions: function(cloneX, cloneY, cloneWidth, cloneHeight) {
|
|
let button = this.closeButton;
|
|
let title = this.title;
|
|
|
|
let buttonX = cloneX + cloneWidth - button._overlap;
|
|
let buttonY = cloneY - button.height + button._overlap;
|
|
button.set_position(Math.floor(buttonX), Math.floor(buttonY));
|
|
|
|
if (!title.fullWidth)
|
|
title.fullWidth = title.width;
|
|
title.width = Math.min(title.fullWidth, cloneWidth);
|
|
|
|
let titleX = cloneX + (cloneWidth - title.width) / 2;
|
|
let titleY = cloneY + cloneHeight + title._spacing;
|
|
title.set_position(Math.floor(titleX), Math.floor(titleY));
|
|
},
|
|
|
|
_closeWindow: function(actor) {
|
|
let metaWindow = this._windowClone.metaWindow;
|
|
this._workspace = metaWindow.get_workspace();
|
|
|
|
this._windowAddedId = this._workspace.connect('window-added',
|
|
Lang.bind(this,
|
|
this._onWindowAdded));
|
|
|
|
metaWindow.delete(global.get_current_time());
|
|
},
|
|
|
|
_onWindowAdded: function(workspace, win) {
|
|
let metaWindow = this._windowClone.metaWindow;
|
|
|
|
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
|
|
Mainloop.idle_add(Lang.bind(this,
|
|
function() {
|
|
this._windowClone.emit('selected');
|
|
return false;
|
|
}));
|
|
}
|
|
},
|
|
|
|
_onDestroy: function() {
|
|
if (this._windowAddedId > 0) {
|
|
this._workspace.disconnect(this._windowAddedId);
|
|
this._windowAddedId = 0;
|
|
}
|
|
if (this._idleToggleCloseId > 0) {
|
|
Mainloop.source_remove(this._idleToggleCloseId);
|
|
this._idleToggleCloseId = 0;
|
|
}
|
|
this._windowClone.metaWindow.disconnect(this._updateCaptionId);
|
|
this.title.destroy();
|
|
this.closeButton.destroy();
|
|
},
|
|
|
|
_onEnter: function() {
|
|
this._parentActor.raise_top();
|
|
this.closeButton.show();
|
|
this.emit('show-close-button');
|
|
},
|
|
|
|
_onLeave: function() {
|
|
if (this._idleToggleCloseId == 0)
|
|
this._idleToggleCloseId = Mainloop.timeout_add(750, Lang.bind(this, this._idleToggleCloseButton));
|
|
},
|
|
|
|
_idleToggleCloseButton: function() {
|
|
this._idleToggleCloseId = 0;
|
|
let [x, y, mask] = global.get_pointer();
|
|
let actor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE,
|
|
x, y);
|
|
if (actor != this._windowClone.actor && actor != this.closeButton) {
|
|
this.closeButton.hide();
|
|
}
|
|
return false;
|
|
},
|
|
|
|
hideCloseButton: function() {
|
|
if (this._idleToggleCloseId > 0) {
|
|
Mainloop.source_remove(this._idleToggleCloseId);
|
|
this._idleToggleCloseId = 0;
|
|
}
|
|
this.closeButton.hide();
|
|
},
|
|
|
|
_onStyleChanged: function() {
|
|
let titleNode = this.title.get_theme_node();
|
|
|
|
let [success, len] = titleNode.get_length('-shell-caption-spacing',
|
|
false);
|
|
if (success)
|
|
this.title._spacing = len;
|
|
|
|
let closeNode = this.closeButton.get_theme_node();
|
|
|
|
[success, len] = closeNode.get_length('-shell-close-overlap',
|
|
false);
|
|
if (success)
|
|
this.closeButton._overlap = len;
|
|
|
|
this._parentActor.queue_relayout();
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(WindowOverlay.prototype);
|
|
|
|
const WindowPositionFlags = {
|
|
ZOOM: 1 << 0,
|
|
ANIMATE: 1 << 1
|
|
};
|
|
|
|
/**
|
|
* @metaWorkspace: a #Meta.Workspace
|
|
*/
|
|
function Workspace(metaWorkspace) {
|
|
this._init(metaWorkspace);
|
|
}
|
|
|
|
Workspace.prototype = {
|
|
_init : function(metaWorkspace) {
|
|
// When dragging a window, we use this slot for reserve space.
|
|
this._reservedSlot = null;
|
|
this.metaWorkspace = metaWorkspace;
|
|
this._windowOverlaysGroup = new Clutter.Group();
|
|
// Without this the drop area will be overlapped.
|
|
this._windowOverlaysGroup.set_size(0, 0);
|
|
|
|
this.actor = new Clutter.Group();
|
|
this.actor._delegate = this;
|
|
|
|
this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
|
|
|
|
// Items in _windowOverlaysGroup should not be scaled, so we don't
|
|
// add them to this.actor, but to its parent whenever it changes
|
|
this.actor.connect('parent-set', Lang.bind(this, this._onParentSet));
|
|
|
|
// Auto-sizing is unreliable in the presence of ClutterClone, so rather than
|
|
// implicitly counting on the workspace actor to be sized to the size of the
|
|
// included desktop actor clone, set the size explicitly to the screen size.
|
|
// See http://bugzilla.openedhand.com/show_bug.cgi?id=1755
|
|
this.actor.width = global.screen_width;
|
|
this.actor.height = global.screen_height;
|
|
this.scale = 1.0;
|
|
|
|
let windows = global.get_windows().filter(this._isMyWindow, this);
|
|
|
|
// Find the desktop window
|
|
for (let i = 0; i < windows.length; i++) {
|
|
if (windows[i].get_window_type() == Meta.WindowType.DESKTOP) {
|
|
this._desktop = new DesktopClone(windows[i]);
|
|
break;
|
|
}
|
|
}
|
|
// If there wasn't one, fake it
|
|
if (!this._desktop)
|
|
this._desktop = new DesktopClone();
|
|
|
|
this._desktop.connect('selected',
|
|
Lang.bind(this,
|
|
function(clone, time) {
|
|
// Only switch to the workspace when there's no application windows
|
|
// open (we always have one window for the desktop). The problem
|
|
// is that it's too easy to miss an app window and get the wrong
|
|
// one focused.
|
|
if (this._windows.length == 1) {
|
|
this.metaWorkspace.activate(time);
|
|
Main.overview.hide();
|
|
}
|
|
}));
|
|
this.actor.add_actor(this._desktop.actor);
|
|
|
|
// Create clones for remaining windows that should be
|
|
// visible in the Overview
|
|
this._windows = [this._desktop];
|
|
this._windowOverlays = [ null ];
|
|
for (let i = 0; i < windows.length; i++) {
|
|
if (this._isOverviewWindow(windows[i])) {
|
|
this._addWindowClone(windows[i]);
|
|
}
|
|
}
|
|
|
|
// A filter for what windows we display
|
|
this._showOnlyWindows = null;
|
|
|
|
// Track window changes
|
|
this._windowAddedId = this.metaWorkspace.connect('window-added',
|
|
Lang.bind(this, this._windowAdded));
|
|
this._windowRemovedId = this.metaWorkspace.connect('window-removed',
|
|
Lang.bind(this, this._windowRemoved));
|
|
this._repositionWindowsId = 0;
|
|
|
|
this._visible = false;
|
|
|
|
this._frame = null;
|
|
|
|
this.leavingOverview = false;
|
|
},
|
|
|
|
_lookupIndex: function (metaWindow) {
|
|
for (let i = 0; i < this._windows.length; i++) {
|
|
if (this._windows[i].metaWindow == metaWindow) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
},
|
|
|
|
_onParentSet: function(actor, old_parent) {
|
|
let new_parent = this.actor.get_parent();
|
|
if (new_parent == null)
|
|
return;
|
|
|
|
if (old_parent)
|
|
this._windowOverlaysGroup.reparent(new_parent);
|
|
else
|
|
new_parent.add_actor(this._windowOverlaysGroup);
|
|
},
|
|
|
|
/**
|
|
* lookupCloneForMetaWindow:
|
|
* @metaWindow: A #MetaWindow
|
|
*
|
|
* Given a #MetaWindow instance, find the WindowClone object
|
|
* which represents it in the workspaces display.
|
|
*/
|
|
lookupCloneForMetaWindow: function (metaWindow) {
|
|
let index = this._lookupIndex (metaWindow);
|
|
return index < 0 ? null : this._windows[index];
|
|
},
|
|
|
|
containsMetaWindow: function (metaWindow) {
|
|
return this._lookupIndex(metaWindow) >= 0;
|
|
},
|
|
|
|
setShowOnlyWindows: function(showOnlyWindows, reposition) {
|
|
this._showOnlyWindows = showOnlyWindows;
|
|
this._resetCloneVisibility();
|
|
if (reposition)
|
|
this.positionWindows(WindowPositionFlags.ANIMATE);
|
|
},
|
|
|
|
/**
|
|
* setLightboxMode:
|
|
* @showLightbox: If true, dim background and allow highlighting a specific window
|
|
*
|
|
* This function also resets the highlighted window state.
|
|
*/
|
|
setLightboxMode: function (showLightbox) {
|
|
if (!this._lightbox)
|
|
this._lightbox = new Lightbox.Lightbox(this.actor,
|
|
{ fadeTime: LIGHTBOX_FADE_TIME });
|
|
|
|
if (showLightbox)
|
|
this._lightbox.show();
|
|
else
|
|
this._lightbox.hide();
|
|
|
|
if (this._frame)
|
|
this._frame.set_opacity(showLightbox ? 150 : 255);
|
|
},
|
|
|
|
/**
|
|
* setHighlightWindow:
|
|
* @metaWindow: A #MetaWindow
|
|
*
|
|
* Draw the user's attention to the given window @metaWindow.
|
|
*/
|
|
setHighlightWindow: function (metaWindow) {
|
|
if (!this._lightbox)
|
|
return;
|
|
|
|
let actor;
|
|
if (metaWindow != null) {
|
|
let clone = this.lookupCloneForMetaWindow(metaWindow);
|
|
actor = clone.actor;
|
|
}
|
|
this._lightbox.highlight(actor);
|
|
},
|
|
|
|
// Mark the workspace selected/not-selected
|
|
setSelected : function(selected) {
|
|
// Don't draw a frame if we only have one workspace
|
|
if (selected && global.screen.n_workspaces > 1) {
|
|
if (this._frame)
|
|
return;
|
|
|
|
// FIXME: do something cooler-looking using clutter-cairo
|
|
this._frame = new Clutter.Rectangle({ color: FRAME_COLOR });
|
|
this.actor.add_actor(this._frame);
|
|
this._frame.set_position(this._desktop.actor.x - FRAME_SIZE / this.actor.scale_x,
|
|
this._desktop.actor.y - FRAME_SIZE / this.actor.scale_y);
|
|
this._frame.set_size(this._desktop.actor.width + 2 * FRAME_SIZE / this.actor.scale_x,
|
|
this._desktop.actor.height + 2 * FRAME_SIZE / this.actor.scale_y);
|
|
this._frame.lower_bottom();
|
|
|
|
this._framePosHandler = this.actor.connect('notify::scale-x', Lang.bind(this, this._updateFramePosition));
|
|
} else {
|
|
if (!this._frame)
|
|
return;
|
|
this.actor.disconnect(this._framePosHandler);
|
|
this._frame.destroy();
|
|
this._frame = null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* setReactive:
|
|
* @reactive: %true iff the workspace should be reactive
|
|
*
|
|
* Set the workspace (desktop) reactive
|
|
**/
|
|
setReactive: function(reactive) {
|
|
this._desktop.actor.reactive = reactive;
|
|
},
|
|
|
|
_updateFramePosition : function() {
|
|
this._frame.set_position(this._desktop.actor.x - FRAME_SIZE / this.actor.scale_x,
|
|
this._desktop.actor.y - FRAME_SIZE / this.actor.scale_y);
|
|
this._frame.set_size(this._desktop.actor.width + 2 * FRAME_SIZE / this.actor.scale_x,
|
|
this._desktop.actor.height + 2 * FRAME_SIZE / this.actor.scale_y);
|
|
},
|
|
|
|
_isCloneVisible: function(clone) {
|
|
return this._showOnlyWindows == null || (clone.metaWindow in this._showOnlyWindows);
|
|
},
|
|
|
|
/**
|
|
* _getVisibleClones:
|
|
*
|
|
* Returns a list WindowClone objects where the clone isn't filtered
|
|
* out by any application filter. The clone for the desktop is excluded.
|
|
* The returned array will always be newly allocated; it is not in any
|
|
* defined order, and thus it's convenient to call .sort() with your
|
|
* choice of sorting function.
|
|
*/
|
|
_getVisibleClones: function() {
|
|
let visible = [];
|
|
|
|
for (let i = 1; i < this._windows.length; i++) {
|
|
let clone = this._windows[i];
|
|
|
|
if (!this._isCloneVisible(clone))
|
|
continue;
|
|
|
|
visible.push(clone);
|
|
}
|
|
return visible;
|
|
},
|
|
|
|
_resetCloneVisibility: function () {
|
|
for (let i = 1; i < this._windows.length; i++) {
|
|
let clone = this._windows[i];
|
|
let overlay = this._windowOverlays[i];
|
|
|
|
if (!this._isCloneVisible(clone)) {
|
|
clone.actor.hide();
|
|
overlay.hide();
|
|
} else {
|
|
clone.actor.show();
|
|
}
|
|
}
|
|
},
|
|
|
|
// Only use this for n <= 20 say
|
|
_factorial: function(n) {
|
|
let result = 1;
|
|
for (let i = 2; i <= n; i++)
|
|
result *= i;
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* _permutation:
|
|
* @permutationIndex: An integer from [0, list.length!)
|
|
* @list: (inout): Array of objects to permute; will be modified in place
|
|
*
|
|
* Given an integer between 0 and length of array, re-order the array in-place
|
|
* into a permutation denoted by the index.
|
|
*/
|
|
_permutation: function(permutationIndex, list) {
|
|
for (let j = 2; j <= list.length; j++) {
|
|
let firstIndex = (permutationIndex % j);
|
|
let secondIndex = j - 1;
|
|
// Swap
|
|
let tmp = list[firstIndex];
|
|
list[firstIndex] = list[secondIndex];
|
|
list[secondIndex] = tmp;
|
|
permutationIndex = Math.floor(permutationIndex / j);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* _forEachPermutations:
|
|
* @list: Array
|
|
* @func: Function which takes a single array argument
|
|
*
|
|
* Call @func with each permutation of @list as an argument.
|
|
*/
|
|
_forEachPermutations: function(list, func) {
|
|
let nCombinations = this._factorial(list.length);
|
|
for (let i = 0; i < nCombinations; i++) {
|
|
let listCopy = list.concat();
|
|
this._permutation(i, listCopy);
|
|
func(listCopy);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* _computeWindowMotion:
|
|
* @actor: A #WindowClone's #ClutterActor
|
|
* @slot: An element of #POSITIONS
|
|
* @slotGeometry: Layout of @slot
|
|
*
|
|
* Returns a number corresponding to how much perceived motion
|
|
* would be involved in moving the window to the given slot.
|
|
* Currently this is the square of the distance between the
|
|
* centers.
|
|
*/
|
|
_computeWindowMotion: function (actor, slot) {
|
|
let [xCenter, yCenter, fraction] = slot;
|
|
let xDelta, yDelta, distanceSquared;
|
|
let actorWidth, actorHeight;
|
|
|
|
actorWidth = actor.width * actor.scale_x;
|
|
actorHeight = actor.height * actor.scale_y;
|
|
xDelta = actor.x + actorWidth / 2.0 - xCenter * global.screen_width;
|
|
yDelta = actor.y + actorHeight / 2.0 - yCenter * global.screen_height;
|
|
distanceSquared = xDelta * xDelta + yDelta * yDelta;
|
|
|
|
return distanceSquared;
|
|
},
|
|
|
|
/**
|
|
* _orderWindowsPermutations:
|
|
*
|
|
* Iterate over all permutations of the windows, and determine the
|
|
* permutation which has the least total motion.
|
|
*/
|
|
_orderWindowsPermutations: function (clones, slots) {
|
|
let minimumMotionPermutation = null;
|
|
let minimumMotion = -1;
|
|
let permIndex = 0;
|
|
this._forEachPermutations(clones, Lang.bind(this, function (permutation) {
|
|
let motion = 0;
|
|
for (let i = 0; i < permutation.length; i++) {
|
|
let cloneActor = permutation[i].actor;
|
|
let slot = slots[i];
|
|
|
|
let delta = this._computeWindowMotion(cloneActor, slot);
|
|
|
|
motion += delta;
|
|
}
|
|
|
|
if (minimumMotionPermutation == null || motion < minimumMotion) {
|
|
minimumMotionPermutation = permutation;
|
|
minimumMotion = motion;
|
|
}
|
|
permIndex++;
|
|
}));
|
|
return minimumMotionPermutation;
|
|
},
|
|
|
|
/**
|
|
* _orderWindowsGreedy:
|
|
*
|
|
* Iterate over available slots in order, placing into each one the window
|
|
* we find with the smallest motion to that slot.
|
|
*/
|
|
_orderWindowsGreedy: function(clones, slots) {
|
|
let result = [];
|
|
let slotIndex = 0;
|
|
// Copy since we mutate below
|
|
let clonesCopy = clones.concat();
|
|
for (let i = 0; i < slots.length; i++) {
|
|
let slot = slots[i];
|
|
let minimumMotionIndex = -1;
|
|
let minimumMotion = -1;
|
|
for (let j = 0; j < clonesCopy.length; j++) {
|
|
let cloneActor = clonesCopy[j].actor;
|
|
let delta = this._computeWindowMotion(cloneActor, slot);
|
|
if (minimumMotionIndex == -1 || delta < minimumMotion) {
|
|
minimumMotionIndex = j;
|
|
minimumMotion = delta;
|
|
}
|
|
}
|
|
result.push(clonesCopy[minimumMotionIndex]);
|
|
clonesCopy.splice(minimumMotionIndex, 1);
|
|
}
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* _orderWindowsByMotionAndStartup:
|
|
* @windows: Array of #MetaWindow
|
|
* @slots: Array of slots
|
|
*
|
|
* Returns a copy of @windows, ordered in such a way that they require least motion
|
|
* to move to the final screen coordinates of @slots. Ties are broken in a stable
|
|
* fashion by the order in which the windows were created.
|
|
*/
|
|
_orderWindowsByMotionAndStartup: function(clones, slots) {
|
|
clones.sort(function(w1, w2) {
|
|
return w2.metaWindow.get_stable_sequence() - w1.metaWindow.get_stable_sequence();
|
|
});
|
|
if (clones.length <= POSITIONING_PERMUTATIONS_MAX)
|
|
return this._orderWindowsPermutations(clones, slots);
|
|
else
|
|
return this._orderWindowsGreedy(clones, slots);
|
|
},
|
|
|
|
/**
|
|
* _getSlotRelativeGeometry:
|
|
* @slot: A layout slot
|
|
*
|
|
* Returns: the workspace-relative [x, y, width, height]
|
|
* of a given window layout slot.
|
|
*/
|
|
_getSlotRelativeGeometry: function(slot) {
|
|
let [xCenter, yCenter, fraction] = slot;
|
|
|
|
let width = global.screen_width * fraction;
|
|
let height = global.screen_height * fraction;
|
|
|
|
let x = xCenter * global.screen_width - width / 2;
|
|
let y = yCenter * global.screen_height - height / 2;
|
|
|
|
return [x, y, width, height];
|
|
},
|
|
|
|
/**
|
|
* _computeWindowRelativeLayout:
|
|
* @metaWindow: A #MetaWindow
|
|
* @slot: A layout slot
|
|
*
|
|
* Given a window and slot to fit it in, compute its
|
|
* workspace-relative [x, y, scale] where scale applies
|
|
* to both X and Y directions.
|
|
*/
|
|
_computeWindowRelativeLayout: function(metaWindow, slot) {
|
|
let [xCenter, yCenter, fraction] = slot;
|
|
let [x, y, width, height] = this._getSlotRelativeGeometry(slot);
|
|
|
|
xCenter = xCenter * global.screen_width;
|
|
|
|
let rect = new Meta.Rectangle();
|
|
metaWindow.get_outer_rect(rect);
|
|
let buttonOuterHeight, captionHeight;
|
|
let buttonOuterWidth = 0;
|
|
|
|
if (this._windowOverlays[1]) {
|
|
[buttonOuterHeight, captionHeight] = this._windowOverlays[1].chromeHeights();
|
|
buttonOuterWidth = this._windowOverlays[1].chromeWidth() / this.scale;
|
|
} else
|
|
[buttonOuterHeight, captionHeight] = [0, 0];
|
|
buttonOuterHeight /= this.scale;
|
|
captionHeight /= this.scale;
|
|
|
|
let desiredWidth = global.screen_width * fraction;
|
|
let desiredHeight = global.screen_height * fraction;
|
|
let scale = Math.min((desiredWidth - buttonOuterWidth) / rect.width,
|
|
(desiredHeight - buttonOuterHeight - captionHeight) / rect.height,
|
|
1.0 / this.scale);
|
|
|
|
x = Math.floor(xCenter - 0.5 * scale * rect.width);
|
|
|
|
// We want to center the window in case we have just one
|
|
if (metaWindow.get_workspace().n_windows == 1)
|
|
y = Math.floor(yCenter * global.screen_height - 0.5 * scale * rect.height);
|
|
else
|
|
y = Math.floor(y + height - rect.height * scale - captionHeight);
|
|
|
|
return [x, y, scale];
|
|
},
|
|
|
|
setReservedSlot: function(clone) {
|
|
if (clone && this.containsMetaWindow(clone.metaWindow)) {
|
|
this._reservedSlot = null;
|
|
this.positionWindows(WindowPositionFlags.ANIMATE);
|
|
return;
|
|
}
|
|
if (clone)
|
|
this._reservedSlot = clone;
|
|
else
|
|
this._reservedSlot = null;
|
|
this.positionWindows(WindowPositionFlags.ANIMATE);
|
|
},
|
|
|
|
/**
|
|
* positionWindows:
|
|
* @flags:
|
|
* ZOOM - workspace is moving at the same time and we need to take that into account.
|
|
* ANIMATE - Indicates that we need animate changing position.
|
|
*/
|
|
positionWindows : function(flags) {
|
|
if (this._repositionWindowsId > 0) {
|
|
Mainloop.source_remove(this._repositionWindowsId);
|
|
this._repositionWindowsId = 0;
|
|
}
|
|
|
|
let totalVisible = 0;
|
|
|
|
let visibleClones = this._getVisibleClones();
|
|
if (this._reservedSlot)
|
|
visibleClones.push(this._reservedSlot);
|
|
|
|
let workspaceZooming = flags & WindowPositionFlags.ZOOM;
|
|
let animate = flags & WindowPositionFlags.ANIMATE;
|
|
|
|
// Start the animations
|
|
let slots = this._computeAllWindowSlots(visibleClones.length);
|
|
visibleClones = this._orderWindowsByMotionAndStartup(visibleClones, slots);
|
|
|
|
for (let i = 0; i < visibleClones.length; i++) {
|
|
let slot = slots[i];
|
|
let clone = visibleClones[i];
|
|
let metaWindow = clone.metaWindow;
|
|
let mainIndex = this._lookupIndex(metaWindow);
|
|
let overlay = this._windowOverlays[mainIndex];
|
|
|
|
// Positioning a window currently being dragged must be avoided;
|
|
// we'll just leave a blank spot in the layout for it.
|
|
if (clone.inDrag)
|
|
continue;
|
|
|
|
let [x, y, scale] = this._computeWindowRelativeLayout(metaWindow, slot);
|
|
|
|
if (overlay)
|
|
overlay.hide();
|
|
if (animate) {
|
|
if (!metaWindow.showing_on_its_workspace()) {
|
|
/* Hidden windows should fade in and grow
|
|
* therefore we need to resize them now so they
|
|
* can be scaled up later */
|
|
if (workspaceZooming) {
|
|
clone.actor.opacity = 0;
|
|
clone.actor.scale_x = 0;
|
|
clone.actor.scale_y = 0;
|
|
clone.actor.x = x;
|
|
clone.actor.y = y;
|
|
}
|
|
|
|
// Make the window slightly transparent to indicate it's hidden
|
|
Tweener.addTween(clone.actor,
|
|
{ opacity: 255,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeInQuad'
|
|
});
|
|
}
|
|
|
|
Tweener.addTween(clone.actor,
|
|
{ x: x,
|
|
y: y,
|
|
scale_x: scale,
|
|
scale_y: scale,
|
|
workspace_relative: workspaceZooming ? this : null,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this, function() {
|
|
this._fadeInWindowOverlay(clone, overlay);
|
|
})
|
|
});
|
|
} else {
|
|
clone.actor.set_position(x, y);
|
|
clone.actor.set_scale(scale, scale);
|
|
this._fadeInWindowOverlay(clone, overlay);
|
|
}
|
|
}
|
|
},
|
|
|
|
syncStacking: function(stackIndices) {
|
|
let desktopClone = this._windows[0];
|
|
|
|
let visibleClones = this._getVisibleClones();
|
|
visibleClones.sort(function (a, b) { return stackIndices[a.metaWindow.get_stable_sequence()] - stackIndices[b.metaWindow.get_stable_sequence()]; });
|
|
|
|
for (let i = 0; i < visibleClones.length; i++) {
|
|
let clone = visibleClones[i];
|
|
let metaWindow = clone.metaWindow;
|
|
if (i == 0) {
|
|
clone.setStackAbove(desktopClone.actor);
|
|
} else {
|
|
let previousClone = visibleClones[i - 1];
|
|
clone.setStackAbove(previousClone.actor);
|
|
}
|
|
}
|
|
},
|
|
|
|
_fadeInWindowOverlay: function(clone, overlay) {
|
|
if (clone.inDrag)
|
|
return;
|
|
|
|
// This is a little messy and complicated because when we
|
|
// start the fade-in we may not have done the final positioning
|
|
// of the workspaces. (Tweener doesn't necessarily finish
|
|
// all animations before calling onComplete callbacks.)
|
|
// So we need to manually compute where the window will
|
|
// be after the workspace animation finishes.
|
|
let [cloneX, cloneY] = clone.actor.get_position();
|
|
let [cloneWidth, cloneHeight] = clone.actor.get_size();
|
|
cloneX = this.gridX + this.scale * cloneX;
|
|
cloneY = this.gridY + this.scale * cloneY;
|
|
cloneWidth = this.scale * clone.actor.scale_x * cloneWidth;
|
|
cloneHeight = this.scale * clone.actor.scale_y * cloneHeight;
|
|
|
|
if (!this._windowOverlaysGroup.visible)
|
|
this._windowOverlaysGroup.show();
|
|
|
|
if (overlay) {
|
|
overlay.updatePositions(cloneX, cloneY, cloneWidth, cloneHeight);
|
|
overlay.fadeIn();
|
|
}
|
|
},
|
|
|
|
_fadeInAllOverlays: function() {
|
|
for (let i = 1; i < this._windows.length; i++) {
|
|
let clone = this._windows[i];
|
|
let overlay = this._windowOverlays[i];
|
|
if (this._showOnlyWindows != null && !(clone.metaWindow in this._showOnlyWindows))
|
|
continue;
|
|
this._fadeInWindowOverlay(clone, overlay);
|
|
}
|
|
},
|
|
|
|
_hideAllOverlays: function() {
|
|
for (let i = 1; i< this._windows.length; i++) {
|
|
let overlay = this._windowOverlays[i];
|
|
overlay.hide();
|
|
}
|
|
},
|
|
|
|
_delayedWindowRepositioning: function() {
|
|
if (this._windowIsZooming)
|
|
return true;
|
|
|
|
let [x, y, mask] = global.get_pointer();
|
|
let wsWidth = this.actor.width * this.scale;
|
|
let wsHeight = this.actor.height * this.scale;
|
|
|
|
let pointerHasMoved = (this._cursorX != x && this._cursorY != y);
|
|
let inWorkspace = (this.gridX < x && x < this.gridX + wsWidth &&
|
|
this.gridY < y && y < this.gridY + wsHeight);
|
|
|
|
if (pointerHasMoved && inWorkspace) {
|
|
// store current cursor position
|
|
this._cursorX = x;
|
|
this._cursorY = y;
|
|
return true;
|
|
}
|
|
|
|
this.positionWindows(WindowPositionFlags.ANIMATE);
|
|
return false;
|
|
},
|
|
|
|
showWindowsOverlays: function() {
|
|
this._windowOverlaysGroup.show();
|
|
this._fadeInAllOverlays();
|
|
},
|
|
|
|
hideWindowsOverlays: function() {
|
|
this._windowOverlaysGroup.hide();
|
|
},
|
|
|
|
_windowRemoved : function(metaWorkspace, metaWin) {
|
|
let win = metaWin.get_compositor_private();
|
|
|
|
// find the position of the window in our list
|
|
let index = this._lookupIndex (metaWin);
|
|
|
|
if (index == -1)
|
|
return;
|
|
|
|
let clone = this._windows[index];
|
|
|
|
this._windows.splice(index, 1);
|
|
this._windowOverlays.splice(index, 1);
|
|
|
|
// If metaWin.get_compositor_private() returned non-NULL, that
|
|
// means the window still exists (and is just being moved to
|
|
// another workspace or something), so set its overviewHint
|
|
// accordingly. (If it returned NULL, then the window is being
|
|
// destroyed; we'd like to animate this, but it's too late at
|
|
// this point.)
|
|
if (win) {
|
|
let [stageX, stageY] = clone.actor.get_transformed_position();
|
|
let [stageWidth, stageHeight] = clone.actor.get_transformed_size();
|
|
win._overviewHint = {
|
|
x: stageX,
|
|
y: stageY,
|
|
scale: stageWidth / clone.actor.width
|
|
};
|
|
}
|
|
clone.destroy();
|
|
|
|
|
|
// We need to reposition the windows; to avoid shuffling windows
|
|
// around while the user is interacting with the workspace, we delay
|
|
// the positioning until the pointer remains still for at least 750 ms
|
|
// or is moved outside the workspace
|
|
|
|
// remove old handler
|
|
if (this._repositionWindowsId > 0) {
|
|
Mainloop.source_remove(this._repositionWindowsId);
|
|
this._repositionWindowsId = 0;
|
|
}
|
|
|
|
// setup new handler
|
|
let [x, y, mask] = global.get_pointer();
|
|
this._cursorX = x;
|
|
this._cursorY = y;
|
|
|
|
this._repositionWindowsId = Mainloop.timeout_add(750,
|
|
Lang.bind(this, this._delayedWindowRepositioning));
|
|
},
|
|
|
|
_windowAdded : function(metaWorkspace, metaWin) {
|
|
if (this.leavingOverview)
|
|
return;
|
|
|
|
let win = metaWin.get_compositor_private();
|
|
|
|
if (!win) {
|
|
// Newly-created windows are added to a workspace before
|
|
// the compositor finds out about them...
|
|
Mainloop.idle_add(Lang.bind(this,
|
|
function () {
|
|
if (this.actor && metaWin.get_compositor_private())
|
|
this._windowAdded(metaWorkspace, metaWin);
|
|
return false;
|
|
}));
|
|
return;
|
|
}
|
|
|
|
if (!this._isOverviewWindow(win))
|
|
return;
|
|
|
|
let clone = this._addWindowClone(win);
|
|
|
|
if (win._overviewHint) {
|
|
let x = (win._overviewHint.x - this.actor.x) / this.scale;
|
|
let y = (win._overviewHint.y - this.actor.y) / this.scale;
|
|
let scale = win._overviewHint.scale / this.scale;
|
|
delete win._overviewHint;
|
|
|
|
clone.actor.set_position (x, y);
|
|
clone.actor.set_scale (scale, scale);
|
|
}
|
|
|
|
this.positionWindows(WindowPositionFlags.ANIMATE);
|
|
},
|
|
|
|
// check for maximized windows on the workspace
|
|
_haveMaximizedWindows: function() {
|
|
for (let i = 1; i < this._windows.length; i++) {
|
|
let metaWindow = this._windows[i].metaWindow;
|
|
if (metaWindow.showing_on_its_workspace() &&
|
|
metaWindow.maximized_horizontally &&
|
|
metaWindow.maximized_vertically)
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// Animate the full-screen to Overview transition.
|
|
zoomToOverview : function() {
|
|
this.actor.set_position(this.gridX, this.gridY);
|
|
this.actor.set_scale(this.scale, this.scale);
|
|
|
|
// Position and scale the windows.
|
|
if (Main.overview.animationInProgress)
|
|
this.positionWindows(WindowPositionFlags.ANIMATE | WindowPositionFlags.ZOOM);
|
|
else
|
|
this.positionWindows(WindowPositionFlags.ZOOM);
|
|
|
|
let active = global.screen.get_active_workspace();
|
|
let fadeInIcons = (Main.overview.animationInProgress &&
|
|
active == this.metaWorkspace &&
|
|
!this._haveMaximizedWindows());
|
|
this._desktop.zoomToOverview(fadeInIcons);
|
|
|
|
this._visible = true;
|
|
},
|
|
|
|
// Animates the return from Overview mode
|
|
zoomFromOverview : function() {
|
|
this.leavingOverview = true;
|
|
|
|
this._hideAllOverlays();
|
|
|
|
if (this._repositionWindowsId > 0) {
|
|
Mainloop.source_remove(this._repositionWindowsId);
|
|
this._repositionWindowsId = 0;
|
|
}
|
|
this._overviewHiddenId = Main.overview.connect('hidden', Lang.bind(this,
|
|
this._doneLeavingOverview));
|
|
|
|
// Position and scale the windows.
|
|
for (let i = 1; i < this._windows.length; i++) {
|
|
let clone = this._windows[i];
|
|
|
|
clone.zoomFromOverview();
|
|
|
|
if (clone.metaWindow.showing_on_its_workspace()) {
|
|
Tweener.addTween(clone.actor,
|
|
{ x: clone.origX,
|
|
y: clone.origY,
|
|
scale_x: 1.0,
|
|
scale_y: 1.0,
|
|
workspace_relative: this,
|
|
time: Overview.ANIMATION_TIME,
|
|
opacity: 255,
|
|
transition: 'easeOutQuad'
|
|
});
|
|
} else {
|
|
// The window is hidden, make it shrink and fade it out
|
|
Tweener.addTween(clone.actor,
|
|
{ scale_x: 0,
|
|
scale_y: 0,
|
|
opacity: 0,
|
|
workspace_relative: this,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeOutQuad'
|
|
});
|
|
}
|
|
}
|
|
|
|
let active = global.screen.get_active_workspace();
|
|
let fadeOutIcons = (active == this.metaWorkspace &&
|
|
!this._haveMaximizedWindows());
|
|
this._desktop.zoomFromOverview(fadeOutIcons);
|
|
|
|
this._visible = false;
|
|
},
|
|
|
|
// Animates grid shrinking/expanding when a row or column
|
|
// of workspaces is added or removed
|
|
resizeToGrid : function (oldScale) {
|
|
this._hideAllOverlays();
|
|
Tweener.addTween(this.actor,
|
|
{ x: this.gridX,
|
|
y: this.gridY,
|
|
scale_x: this.scale,
|
|
scale_y: this.scale,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this, this._fadeInAllOverlays)
|
|
});
|
|
},
|
|
|
|
// Animates the addition of a new (empty) workspace
|
|
slideIn : function(oldScale) {
|
|
if (this.gridCol > this.gridRow) {
|
|
this.actor.set_position(global.screen_width, this.gridY);
|
|
this.actor.set_scale(oldScale, oldScale);
|
|
} else {
|
|
this.actor.set_position(this.gridX, global.screen_height);
|
|
this.actor.set_scale(this.scale, this.scale);
|
|
}
|
|
Tweener.addTween(this.actor,
|
|
{ x: this.gridX,
|
|
y: this.gridY,
|
|
scale_x: this.scale,
|
|
scale_y: this.scale,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeOutQuad'
|
|
});
|
|
|
|
this._visible = true;
|
|
},
|
|
|
|
// Animates the removal of a workspace
|
|
slideOut : function(onComplete) {
|
|
let destX = this.actor.x, destY = this.actor.y;
|
|
|
|
this._hideAllOverlays();
|
|
|
|
if (this.gridCol > this.gridRow)
|
|
destX = global.screen_width;
|
|
else
|
|
destY = global.screen_height;
|
|
Tweener.addTween(this.actor,
|
|
{ x: destX,
|
|
y: destY,
|
|
scale_x: this.scale,
|
|
scale_y: this.scale,
|
|
time: Overview.ANIMATION_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: onComplete
|
|
});
|
|
|
|
this._visible = false;
|
|
|
|
// Don't let the user try to select this workspace as it's
|
|
// making its exit.
|
|
this._desktop.reactive = false;
|
|
},
|
|
|
|
destroy : function() {
|
|
this.actor.destroy();
|
|
},
|
|
|
|
_onDestroy: function(actor) {
|
|
if (this._overviewHiddenId) {
|
|
Main.overview.disconnect(this._overviewHiddenId);
|
|
this._overviewHiddenId = 0;
|
|
}
|
|
Tweener.removeTweens(actor);
|
|
|
|
this.metaWorkspace.disconnect(this._windowAddedId);
|
|
this.metaWorkspace.disconnect(this._windowRemovedId);
|
|
|
|
if (this._repositionWindowsId > 0)
|
|
Mainloop.source_remove(this._repositionWindowsId);
|
|
|
|
// Usually, the windows will be destroyed automatically with
|
|
// their parent (this.actor), but we might have a zoomed window
|
|
// which has been reparented to the stage - _windows[0] holds
|
|
// the desktop window, which is never reparented
|
|
for (let w = 1; w < this._windows.length; w++)
|
|
this._windows[w].destroy();
|
|
this._windows = [];
|
|
},
|
|
|
|
// Sets this.leavingOverview flag to false.
|
|
_doneLeavingOverview : function() {
|
|
this.leavingOverview = false;
|
|
},
|
|
|
|
// Tests if @win belongs to this workspaces
|
|
_isMyWindow : function (win) {
|
|
return win.get_workspace() == this.metaWorkspace.index() ||
|
|
(win.get_meta_window() && win.get_meta_window().is_on_all_workspaces());
|
|
},
|
|
|
|
// Tests if @win should be shown in the Overview
|
|
_isOverviewWindow : function (win) {
|
|
let tracker = Shell.WindowTracker.get_default();
|
|
return tracker.is_window_interesting(win.get_meta_window());
|
|
},
|
|
|
|
// Create a clone of a (non-desktop) window and add it to the window list
|
|
_addWindowClone : function(win) {
|
|
let clone = new WindowClone(win);
|
|
let overlay = new WindowOverlay(clone, this._windowOverlaysGroup);
|
|
|
|
clone.connect('selected',
|
|
Lang.bind(this, this._onCloneSelected));
|
|
clone.connect('drag-begin',
|
|
Lang.bind(this, function(clone) {
|
|
this.emit('window-drag-begin', clone.actor);
|
|
overlay.hide();
|
|
}));
|
|
clone.connect('drag-end',
|
|
Lang.bind(this, function(clone) {
|
|
this.emit('window-drag-end', clone.actor);
|
|
overlay.show();
|
|
}));
|
|
clone.connect('zoom-start',
|
|
Lang.bind(this, function() {
|
|
this._windowIsZooming = true;
|
|
}));
|
|
clone.connect('zoom-end',
|
|
Lang.bind(this, function() {
|
|
this._windowIsZooming = false;
|
|
}));
|
|
|
|
this.actor.add_actor(clone.actor);
|
|
|
|
overlay.connect('show-close-button', Lang.bind(this, this._onShowOverlayClose));
|
|
|
|
this._windows.push(clone);
|
|
this._windowOverlays.push(overlay);
|
|
|
|
return clone;
|
|
},
|
|
|
|
_onShowOverlayClose: function (windowOverlay) {
|
|
for (let i = 1; i < this._windowOverlays.length; i++) {
|
|
let overlay = this._windowOverlays[i];
|
|
if (overlay == windowOverlay)
|
|
continue;
|
|
overlay.hideCloseButton();
|
|
}
|
|
},
|
|
|
|
_computeWindowSlot : function(windowIndex, numberOfWindows) {
|
|
if (numberOfWindows in POSITIONS)
|
|
return POSITIONS[numberOfWindows][windowIndex];
|
|
|
|
// If we don't have a predefined scheme for this window count,
|
|
// arrange the windows in a grid pattern.
|
|
let gridWidth = Math.ceil(Math.sqrt(numberOfWindows));
|
|
let gridHeight = Math.ceil(numberOfWindows / gridWidth);
|
|
|
|
let fraction = 0.95 * (1. / gridWidth);
|
|
|
|
let xCenter = (.5 / gridWidth) + ((windowIndex) % gridWidth) / gridWidth;
|
|
let yCenter = (.5 / gridHeight) + Math.floor((windowIndex / gridWidth)) / gridHeight;
|
|
|
|
return [xCenter, yCenter, fraction];
|
|
},
|
|
|
|
_computeAllWindowSlots: function(totalWindows) {
|
|
let slots = [];
|
|
for (let i = 0; i < totalWindows; i++) {
|
|
slots.push(this._computeWindowSlot(i, totalWindows));
|
|
}
|
|
return slots;
|
|
},
|
|
|
|
_onCloneSelected : function (clone, time) {
|
|
Main.activateWindow(clone.metaWindow, time,
|
|
this.metaWorkspace.index());
|
|
},
|
|
|
|
_removeSelf : function(actor, event) {
|
|
screen.remove_workspace(this.metaWorkspace, event.get_time());
|
|
return true;
|
|
},
|
|
|
|
// Draggable target interface
|
|
acceptDrop : function(source, actor, x, y, time) {
|
|
if (source instanceof WindowClone) {
|
|
let win = source.realWindow;
|
|
if (this._isMyWindow(win))
|
|
return false;
|
|
|
|
// Set a hint on the Mutter.Window so its initial position
|
|
// in the new workspace will be correct
|
|
win._overviewHint = {
|
|
x: actor.x,
|
|
y: actor.y,
|
|
scale: actor.scale_x
|
|
};
|
|
|
|
let metaWindow = win.get_meta_window();
|
|
metaWindow.change_workspace_by_index(this.metaWorkspace.index(),
|
|
false, // don't create workspace
|
|
time);
|
|
return true;
|
|
} else if (source.shellWorkspaceLaunch) {
|
|
this.metaWorkspace.activate(time);
|
|
source.shellWorkspaceLaunch();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(Workspace.prototype);
|
|
|
|
// Create a SpecialPropertyModifier to let us move windows in a
|
|
// straight line on the screen even though their containing workspace
|
|
// is also moving.
|
|
Tweener.registerSpecialPropertyModifier('workspace_relative', _workspaceRelativeModifier, _workspaceRelativeGet);
|
|
|
|
function _workspaceRelativeModifier(workspace) {
|
|
let [startX, startY] = Main.overview.getPosition();
|
|
let overviewPosX, overviewPosY, overviewScale;
|
|
|
|
if (!workspace)
|
|
return [];
|
|
|
|
if (workspace.leavingOverview) {
|
|
let [zoomedInX, zoomedInY] = Main.overview.getZoomedInPosition();
|
|
overviewPosX = { begin: startX, end: zoomedInX };
|
|
overviewPosY = { begin: startY, end: zoomedInY };
|
|
overviewScale = { begin: Main.overview.getScale(),
|
|
end: Main.overview.getZoomedInScale() };
|
|
} else {
|
|
overviewPosX = { begin: startX, end: 0 };
|
|
overviewPosY = { begin: startY, end: 0 };
|
|
overviewScale = { begin: Main.overview.getScale(), end: 1 };
|
|
}
|
|
|
|
return [ { name: 'x',
|
|
parameters: { workspacePos: workspace.gridX,
|
|
overviewPos: overviewPosX,
|
|
overviewScale: overviewScale } },
|
|
{ name: 'y',
|
|
parameters: { workspacePos: workspace.gridY,
|
|
overviewPos: overviewPosY,
|
|
overviewScale: overviewScale } }
|
|
];
|
|
}
|
|
|
|
function _workspaceRelativeGet(begin, end, time, params) {
|
|
let curOverviewPos = (1 - time) * params.overviewPos.begin +
|
|
time * params.overviewPos.end;
|
|
let curOverviewScale = (1 - time) * params.overviewScale.begin +
|
|
time * params.overviewScale.end;
|
|
|
|
// Calculate the screen position of the window.
|
|
let screen = (1 - time) *
|
|
((begin + params.workspacePos) * params.overviewScale.begin +
|
|
params.overviewPos.begin) +
|
|
time *
|
|
((end + params.workspacePos) * params.overviewScale.end +
|
|
params.overviewPos.end);
|
|
|
|
// Return the workspace coordinates.
|
|
return (screen - curOverviewPos) / curOverviewScale - params.workspacePos;
|
|
}
|