0ef3f999d2
When we have more thumbnails than can fit in the vertical space, scale them down. This is implemented by using a generic container so we can compute positions and sizes on the fly and do the appropriate width-for-height behavior. The usage of clutter constraints to position the indicator is droppped since it less complicated to just position the indicator in the right place ourselves. https://bugzilla.gnome.org/show_bug.cgi?id=641879
519 lines
18 KiB
JavaScript
519 lines
18 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const Shell = imports.gi.Shell;
|
|
const Signals = imports.signals;
|
|
const St = imports.gi.St;
|
|
|
|
const DND = imports.ui.dnd;
|
|
const Main = imports.ui.main;
|
|
const Tweener = imports.ui.tweener;
|
|
const Workspace = imports.ui.workspace;
|
|
const WorkspacesView = imports.ui.workspacesView;
|
|
|
|
// The maximum size of a thumbnail is 1/8 the width and height of the screen
|
|
let MAX_THUMBNAIL_SCALE = 1/8.;
|
|
|
|
function WindowClone(realWindow) {
|
|
this._init(realWindow);
|
|
}
|
|
|
|
WindowClone.prototype = {
|
|
_init : function(realWindow) {
|
|
this.actor = new Clutter.Clone({ source: realWindow.get_texture(),
|
|
reactive: true });
|
|
this.actor._delegate = this;
|
|
this.realWindow = realWindow;
|
|
this.metaWindow = realWindow.meta_window;
|
|
|
|
this._positionChangedId = this.realWindow.connect('position-changed',
|
|
Lang.bind(this, this._onPositionChanged));
|
|
this._realWindowDestroyedId = this.realWindow.connect('destroy',
|
|
Lang.bind(this, this._disconnectRealWindowSignals));
|
|
this._onPositionChanged();
|
|
|
|
this.actor.connect('button-release-event',
|
|
Lang.bind(this, this._onButtonRelease));
|
|
|
|
this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
|
|
|
|
this._draggable = DND.makeDraggable(this.actor,
|
|
{ restoreOnSuccess: true,
|
|
dragActorMaxSize: Workspace.WINDOW_DND_SIZE,
|
|
dragActorOpacity: Workspace.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;
|
|
},
|
|
|
|
setStackAbove: function (actor) {
|
|
this._stackAbove = actor;
|
|
if (this._stackAbove == null)
|
|
this.actor.lower_bottom();
|
|
else
|
|
this.actor.raise(this._stackAbove);
|
|
},
|
|
|
|
destroy: function () {
|
|
this.actor.destroy();
|
|
},
|
|
|
|
_onPositionChanged: function() {
|
|
let rect = this.metaWindow.get_outer_rect();
|
|
this.actor.set_position(this.realWindow.x, this.realWindow.y);
|
|
},
|
|
|
|
_disconnectRealWindowSignals: function() {
|
|
if (this._positionChangedId != 0) {
|
|
this.realWindow.disconnect(this._positionChangedId);
|
|
this._positionChangedId = 0;
|
|
}
|
|
|
|
if (this._realWindowDestroyedId != 0) {
|
|
this.realWindow.disconnect(this._realWindowDestroyedId);
|
|
this._realWindowDestroyedId = 0;
|
|
}
|
|
},
|
|
|
|
_onDestroy: function() {
|
|
this._disconnectRealWindowSignals();
|
|
|
|
this.actor._delegate = null;
|
|
|
|
if (this.inDrag) {
|
|
this.emit('drag-end');
|
|
this.inDrag = false;
|
|
}
|
|
|
|
this.disconnectAll();
|
|
},
|
|
|
|
_onButtonRelease : function (actor, event) {
|
|
this.emit('selected', event.get_time());
|
|
|
|
return true;
|
|
},
|
|
|
|
_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) {
|
|
if (this._stackAbove == null)
|
|
this.actor.lower_bottom();
|
|
else
|
|
this.actor.raise(this._stackAbove);
|
|
}
|
|
|
|
|
|
this.emit('drag-end');
|
|
}
|
|
};
|
|
Signals.addSignalMethods(WindowClone.prototype);
|
|
|
|
|
|
/**
|
|
* @metaWorkspace: a #Meta.Workspace
|
|
*/
|
|
function WorkspaceThumbnail(metaWorkspace) {
|
|
this._init(metaWorkspace);
|
|
}
|
|
|
|
WorkspaceThumbnail.prototype = {
|
|
_init : function(metaWorkspace) {
|
|
this.metaWorkspace = metaWorkspace;
|
|
|
|
this.actor = new St.Bin({ reactive: true,
|
|
clip_to_allocation: true,
|
|
style_class: 'workspace-thumbnail' });
|
|
this.actor._delegate = this;
|
|
|
|
this._group = new Clutter.Group();
|
|
this.actor.add_actor(this._group);
|
|
|
|
this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
|
|
this.actor.connect('button-press-event', Lang.bind(this,
|
|
function(actor, event) {
|
|
return true;
|
|
}));
|
|
this.actor.connect('button-release-event', Lang.bind(this,
|
|
function(actor, event) {
|
|
this._activate();
|
|
return true;
|
|
}));
|
|
|
|
this._background = new Clutter.Clone({ source: global.background_actor });
|
|
this._group.add_actor(this._background);
|
|
this._group.set_size(global.screen_width, global.screen_height);
|
|
|
|
let windows = global.get_window_actors().filter(this._isMyWindow, this);
|
|
|
|
// Create clones for windows that should be visible in the Overview
|
|
this._windows = [];
|
|
for (let i = 0; i < windows.length; i++) {
|
|
if (this._isOverviewWindow(windows[i])) {
|
|
this._addWindowClone(windows[i]);
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
},
|
|
|
|
_lookupIndex: function (metaWindow) {
|
|
for (let i = 0; i < this._windows.length; i++) {
|
|
if (this._windows[i].metaWindow == metaWindow) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
},
|
|
|
|
syncStacking: function(stackIndices) {
|
|
this._windows.sort(function (a, b) { return stackIndices[a.metaWindow.get_stable_sequence()] - stackIndices[b.metaWindow.get_stable_sequence()]; });
|
|
|
|
for (let i = 0; i < this._windows.length; i++) {
|
|
let clone = this._windows[i];
|
|
let metaWindow = clone.metaWindow;
|
|
if (i == 0) {
|
|
clone.setStackAbove(this._background);
|
|
} else {
|
|
let previousClone = this._windows[i - 1];
|
|
clone.setStackAbove(previousClone.actor);
|
|
}
|
|
}
|
|
},
|
|
|
|
_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);
|
|
clone.destroy();
|
|
},
|
|
|
|
_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);
|
|
},
|
|
|
|
destroy : function() {
|
|
this.actor.destroy();
|
|
},
|
|
|
|
_onDestroy: function(actor) {
|
|
this.metaWorkspace.disconnect(this._windowAddedId);
|
|
this.metaWorkspace.disconnect(this._windowRemovedId);
|
|
|
|
this._windows = [];
|
|
this.actor = null;
|
|
},
|
|
|
|
// 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);
|
|
|
|
clone.connect('selected',
|
|
Lang.bind(this, this._activate));
|
|
clone.connect('drag-begin',
|
|
Lang.bind(this, function(clone) {
|
|
Main.overview.beginWindowDrag();
|
|
}));
|
|
clone.connect('drag-end',
|
|
Lang.bind(this, function(clone) {
|
|
Main.overview.endWindowDrag();
|
|
}));
|
|
this._group.add_actor(clone.actor);
|
|
|
|
this._windows.push(clone);
|
|
|
|
return clone;
|
|
},
|
|
|
|
_activate : function (clone, time) {
|
|
// a click on the already current workspace should go back to the main view
|
|
if (this.metaWorkspace == global.screen.get_active_workspace())
|
|
Main.overview.hide();
|
|
else
|
|
this.metaWorkspace.activate(time);
|
|
},
|
|
|
|
// Draggable target interface
|
|
handleDragOver : function(source, actor, x, y, time) {
|
|
if (source.realWindow)
|
|
return DND.DragMotionResult.MOVE_DROP;
|
|
if (source.shellWorkspaceLaunch)
|
|
return DND.DragMotionResult.COPY_DROP;
|
|
|
|
return DND.DragMotionResult.CONTINUE;
|
|
},
|
|
|
|
acceptDrop : function(source, actor, x, y, time) {
|
|
if (source.realWindow) {
|
|
let win = source.realWindow;
|
|
if (this._isMyWindow(win))
|
|
return false;
|
|
|
|
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) {
|
|
source.shellWorkspaceLaunch({ workspace: this.metaWorkspace,
|
|
timestamp: time });
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(WorkspaceThumbnail.prototype);
|
|
|
|
|
|
function ThumbnailsBox() {
|
|
this._init();
|
|
}
|
|
|
|
ThumbnailsBox.prototype = {
|
|
_init: function() {
|
|
this.actor = new Shell.GenericContainer({ style_class: 'workspace-thumbnails',
|
|
request_mode: Clutter.RequestMode.WIDTH_FOR_HEIGHT });
|
|
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));
|
|
|
|
let indicator = new St.Bin({ style_class: 'workspace-thumbnail-indicator',
|
|
fixed_position_set: true });
|
|
|
|
// We don't want the indicator to affect drag-and-drop
|
|
Shell.util_set_hidden_from_pick(indicator, true);
|
|
|
|
this._indicator = indicator;
|
|
this.actor.add_actor(indicator);
|
|
this._indicatorConstrained = false;
|
|
|
|
this._thumbnails = [];
|
|
},
|
|
|
|
show: function() {
|
|
this._switchWorkspaceNotifyId =
|
|
global.window_manager.connect('switch-workspace',
|
|
Lang.bind(this, this._activeWorkspaceChanged));
|
|
|
|
this.addThumbnails(0, global.screen.n_workspaces);
|
|
this._constrainThumbnailIndicator();
|
|
},
|
|
|
|
hide: function() {
|
|
this._unconstrainThumbnailIndicator();
|
|
|
|
if (this._switchWorkspaceNotifyId > 0) {
|
|
global.window_manager.disconnect(this._switchWorkspaceNotifyId);
|
|
this._switchWorkspaceNotifyId = 0;
|
|
}
|
|
|
|
for (let w = 0; w < this._thumbnails.length; w++)
|
|
this._thumbnails[w].destroy();
|
|
this._thumbnails = [];
|
|
},
|
|
|
|
addThumbnails: function(start, count) {
|
|
for (let k = start; k < start + count; k++) {
|
|
let metaWorkspace = global.screen.get_workspace_by_index(k);
|
|
let thumbnail = new WorkspaceThumbnail(metaWorkspace);
|
|
this._thumbnails[k] = thumbnail;
|
|
this.actor.add_actor(thumbnail.actor);
|
|
}
|
|
|
|
// The thumbnails indicator actually needs to be on top of the thumbnails
|
|
this._indicator.raise_top();
|
|
},
|
|
|
|
removeThumbmails: function(start, count) {
|
|
for (let k = start; k < start + count; k++)
|
|
this._thumbnails[k].destroy();
|
|
this._thumbnails.splice(start, count);
|
|
|
|
// If we removed the current workspace, then metacity will have already
|
|
// switched to an adjacent workspace. Leaving the animation we
|
|
// started in response to that around will look funny because it's an
|
|
// animation for the *old* workspace configuration. So, kill it.
|
|
// If we animate the workspace removal in the future, we should animate
|
|
// the indicator as part of that.
|
|
Tweener.removeTweens(this._thumbnailIndicator);
|
|
this._constrainThumbnailIndicator();
|
|
},
|
|
|
|
syncStacking: function(stackIndices) {
|
|
for (let i = 0; i < this._thumbnails.length; i++)
|
|
this._thumbnails[i].syncStacking(stackIndices);
|
|
},
|
|
|
|
_getPreferredHeight: function(actor, forWidth, alloc) {
|
|
// Note that for getPreferredWidth/Height we cheat a bit and skip propagating
|
|
// the size request to our children because we know how big they are and know
|
|
// that the actors aren't depending on the virtual functions being called.
|
|
|
|
if (this._thumbnails.length == 0)
|
|
return;
|
|
|
|
let spacing = this.actor.get_theme_node().get_length('spacing');
|
|
let totalSpacing = (this._thumbnails.length - 1) * spacing;
|
|
|
|
alloc.min_size = totalSpacing;
|
|
alloc.natural_size = totalSpacing + this._thumbnails.length * global.screen_height * MAX_THUMBNAIL_SCALE;
|
|
},
|
|
|
|
_getPreferredWidth: function(actor, forHeight, alloc) {
|
|
if (this._thumbnails.length == 0)
|
|
return;
|
|
|
|
let spacing = this.actor.get_theme_node().get_length('spacing');
|
|
let totalSpacing = (this._thumbnails.length - 1) * spacing;
|
|
let avail = forHeight - totalSpacing;
|
|
|
|
let scale = (avail / this._thumbnails.length) / global.screen_height;
|
|
scale = Math.min(scale, MAX_THUMBNAIL_SCALE);
|
|
|
|
alloc.min_size = alloc.natural_size = Math.round(global.screen_width * scale);
|
|
},
|
|
|
|
_allocate: function(actor, box, flags) {
|
|
let screenHeight = global.screen_height;
|
|
|
|
let spacing = this.actor.get_theme_node().get_length('spacing');
|
|
let totalSpacing = (this._thumbnails.length - 1) * spacing;
|
|
let avail = (box.y2 - box.y1) - totalSpacing;
|
|
|
|
let scale = (avail / this._thumbnails.length) / screenHeight;
|
|
scale = Math.min(scale, MAX_THUMBNAIL_SCALE);
|
|
|
|
let thumbnailHeight = screenHeight * scale;
|
|
|
|
let childBox = new Clutter.ActorBox();
|
|
|
|
let indicatorWorkspace = this._indicatorConstrained ? global.screen.get_active_workspace() : null;
|
|
let indicatorBox;
|
|
|
|
// Allocating a scaled actor is funny - x1/y1 correspond to the origin
|
|
// of the actor, but x2/y2 are increased by the *unscaled* size.
|
|
childBox.x1 = box.x1;
|
|
childBox.x2 = childBox.x1 + global.screen_width;
|
|
|
|
let y = box.y1;
|
|
|
|
for (let i = 0; i < this._thumbnails.length; i++) {
|
|
if (i > 0)
|
|
y += spacing + thumbnailHeight;
|
|
|
|
// We might end up with thumbnailHeight being something like 99.33
|
|
// pixels. To make this work and not end up with a gap at the bottom,
|
|
// we need some thumbnails to be 99 pixels and some 100 pixels height;
|
|
// we compute an actual scale separately for each thumbnail.
|
|
let y1 = Math.round(y);
|
|
let y2 = Math.round(y + thumbnailHeight);
|
|
let roundedScale = (y2 - y1) / screenHeight;
|
|
|
|
if (this._thumbnails[i].metaWorkspace == indicatorWorkspace) {
|
|
let indicatorBox = new Clutter.ActorBox();
|
|
indicatorBox.x1 = box.x1;
|
|
indicatorBox.x2 = box.x2;
|
|
indicatorBox.y1 = y1;
|
|
indicatorBox.y2 = y2;
|
|
|
|
this._indicator.allocate(indicatorBox, flags);
|
|
}
|
|
|
|
childBox.y1 = y1;
|
|
childBox.y2 = childBox.y1 + screenHeight;
|
|
|
|
this._thumbnails[i].actor.set_scale(roundedScale, roundedScale);
|
|
this._thumbnails[i].actor.allocate(childBox, flags);
|
|
}
|
|
|
|
if (indicatorWorkspace == null)
|
|
this._indicator.allocate_preferred_size(flags);
|
|
},
|
|
|
|
_constrainThumbnailIndicator: function() {
|
|
this._indicatorConstrained = true;
|
|
this.actor.queue_relayout();
|
|
},
|
|
|
|
_unconstrainThumbnailIndicator: function() {
|
|
this._indicatorConstrained = false;
|
|
},
|
|
|
|
_activeWorkspaceChanged: function(wm, from, to, direction) {
|
|
let active = global.screen.get_active_workspace_index();
|
|
let thumbnail = this._thumbnails[active];
|
|
|
|
this._unconstrainThumbnailIndicator();
|
|
let oldAllocation = this._indicator.allocation;
|
|
this._indicator.x = oldAllocation.x1;
|
|
this._indicator.y = oldAllocation.y1;
|
|
this._indicator.width = oldAllocation.x2 - oldAllocation.x1;
|
|
this._indicator.height = oldAllocation.y2 - oldAllocation.y1;
|
|
|
|
Tweener.addTween(this._indicator,
|
|
{ x: thumbnail.actor.allocation.x1,
|
|
y: thumbnail.actor.allocation.y1,
|
|
time: WorkspacesView.WORKSPACE_SWITCH_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this,
|
|
this._constrainThumbnailIndicator)
|
|
});
|
|
}
|
|
};
|