Add workspace thumbnails to the overview
Add workspace thumbnails to the workspace controls area. The user can click on the thumbnail to switch workspaces and can also drag windows out of the thumbnail to other workspaces. https://bugzilla.gnome.org/show_bug.cgi?id=640996
This commit is contained in:
parent
eda12542c0
commit
6514bc845f
@ -261,21 +261,29 @@ StTooltip StLabel {
|
||||
}
|
||||
|
||||
.workspace-controls {
|
||||
width: 48px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
border: 2px solid rgba(128, 128, 128, 0.4);
|
||||
border: 1px solid #424242;
|
||||
border-right: 0px;
|
||||
border-radius: 9px 0px 0px 9px;
|
||||
background: #071524;
|
||||
}
|
||||
|
||||
.workspace-thumbnails {
|
||||
spacing: 7px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.workspace-thumbnail-indicator {
|
||||
outline: 2px solid white;
|
||||
}
|
||||
|
||||
.add-workspace {
|
||||
background-color: rgba(128, 128, 128, 0.4);
|
||||
}
|
||||
|
||||
.add-workspace:hover {
|
||||
background-color: rgba(128, 128, 128, 0.6);
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.remove-workspace {
|
||||
|
@ -58,6 +58,7 @@ nobase_dist_js_DATA = \
|
||||
ui/windowAttentionHandler.js \
|
||||
ui/windowManager.js \
|
||||
ui/workspace.js \
|
||||
ui/workspaceThumbnail.js \
|
||||
ui/workspacesView.js \
|
||||
ui/workspaceSwitcherPopup.js \
|
||||
ui/xdndHandler.js
|
||||
|
295
js/ui/workspaceThumbnail.js
Normal file
295
js/ui/workspaceThumbnail.js
Normal file
@ -0,0 +1,295 @@
|
||||
/* -*- 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 Workspace = imports.ui.workspace;
|
||||
|
||||
// Fraction of original screen size for thumbnails
|
||||
let 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,
|
||||
x: realWindow.x,
|
||||
y: realWindow.y });
|
||||
this.actor._delegate = this;
|
||||
this.realWindow = realWindow;
|
||||
this.metaWindow = realWindow.meta_window;
|
||||
|
||||
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;
|
||||
|
||||
this._selected = 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();
|
||||
},
|
||||
|
||||
_onDestroy: function() {
|
||||
this.actor._delegate = null;
|
||||
|
||||
if (this.inDrag) {
|
||||
this.emit('drag-end');
|
||||
this.inDrag = false;
|
||||
}
|
||||
|
||||
this.disconnectAll();
|
||||
},
|
||||
|
||||
_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) {
|
||||
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.metaWorkspace.activate(event.get_time());
|
||||
return true;
|
||||
}));
|
||||
|
||||
this._background = new Clutter.Clone({ source: global.background_actor });
|
||||
this._group.add_actor(this._background);
|
||||
|
||||
this._group.set_size(THUMBNAIL_SCALE * global.screen_width, THUMBNAIL_SCALE * global.screen_height);
|
||||
this._group.set_scale(THUMBNAIL_SCALE, THUMBNAIL_SCALE);
|
||||
|
||||
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._onCloneSelected));
|
||||
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;
|
||||
},
|
||||
|
||||
_onCloneSelected : function (clone, time) {
|
||||
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) {
|
||||
this.metaWorkspace.activate(time);
|
||||
source.shellWorkspaceLaunch();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Signals.addSignalMethods(WorkspaceThumbnail.prototype);
|
@ -14,6 +14,7 @@ const Main = imports.ui.main;
|
||||
const Overview = imports.ui.overview;
|
||||
const Tweener = imports.ui.tweener;
|
||||
const Workspace = imports.ui.workspace;
|
||||
const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
|
||||
|
||||
const WORKSPACE_SWITCH_TIME = 0.25;
|
||||
// Note that mutter has a compile-time limit of 36
|
||||
@ -725,6 +726,25 @@ WorkspacesDisplay.prototype = {
|
||||
}));
|
||||
controls.add(this._removeButton);
|
||||
|
||||
this._thumbnailsBox = new St.BoxLayout({ vertical: true,
|
||||
style_class: 'workspace-thumbnails' });
|
||||
controls.add(this._thumbnailsBox, { expand: false });
|
||||
|
||||
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._thumbnailIndicator = indicator;
|
||||
this._thumbnailsBox.add(this._thumbnailIndicator);
|
||||
this._thumbnailIndicatorConstraints = [];
|
||||
this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.POSITION }));
|
||||
this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.SIZE }));
|
||||
this._thumbnailIndicatorConstraints.forEach(function(constraint) {
|
||||
indicator.add_constraint(constraint);
|
||||
});
|
||||
|
||||
this._addButton = new St.Button({ label: '+',
|
||||
style_class: 'add-workspace' });
|
||||
this._addButton.connect('clicked', Lang.bind(this, function() {
|
||||
@ -759,11 +779,22 @@ WorkspacesDisplay.prototype = {
|
||||
this._controls.show();
|
||||
|
||||
this._workspaces = [];
|
||||
this._workspaceThumbnails = [];
|
||||
for (let i = 0; i < global.screen.n_workspaces; i++) {
|
||||
let metaWorkspace = global.screen.get_workspace_by_index(i);
|
||||
this._workspaces[i] = new Workspace.Workspace(metaWorkspace);
|
||||
|
||||
let thumbnail = new WorkspaceThumbnail.WorkspaceThumbnail(metaWorkspace);
|
||||
this._workspaceThumbnails[i] = thumbnail;
|
||||
this._thumbnailsBox.add(thumbnail.actor);
|
||||
}
|
||||
|
||||
// The thumbnails indicator actually needs to be on top of the thumbnails, but
|
||||
// there is also something more subtle going on as well - actors in a StBoxLayout
|
||||
// are allocated from bottom to top (start to end), and we need the
|
||||
// thumnail indicator to be allocated after the actors it is constrained to.
|
||||
this._thumbnailIndicator.raise_top();
|
||||
|
||||
let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
|
||||
|
||||
let totalAllocation = this.actor.allocation;
|
||||
@ -810,6 +841,9 @@ WorkspacesDisplay.prototype = {
|
||||
this._nWorkspacesNotifyId =
|
||||
global.screen.connect('notify::n-workspaces',
|
||||
Lang.bind(this, this._workspacesChanged));
|
||||
this._switchWorkspaceNotifyId =
|
||||
global.window_manager.connect('switch-workspace',
|
||||
Lang.bind(this, this._activeWorkspaceChanged));
|
||||
|
||||
this._restackedNotifyId =
|
||||
global.screen.connect('restacked',
|
||||
@ -829,6 +863,9 @@ WorkspacesDisplay.prototype = {
|
||||
Lang.bind(this, this._dragEnd));
|
||||
|
||||
this._onRestacked();
|
||||
this._constrainThumbnailIndicator();
|
||||
this._zoomOut = false;
|
||||
this._updateZoom();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
@ -838,6 +875,10 @@ WorkspacesDisplay.prototype = {
|
||||
global.screen.disconnect(this._nWorkspacesNotifyId);
|
||||
this._nWorkspacesNotifyId = 0;
|
||||
}
|
||||
if (this._switchWorkspaceNotifyId > 0) {
|
||||
global.window_manager.disconnect(this._switchWorkspaceNotifyId);
|
||||
this._switchWorkspaceNotifyId = 0;
|
||||
}
|
||||
if (this._restackedNotifyId > 0){
|
||||
global.screen.disconnect(this._restackedNotifyId);
|
||||
this._restackedNotifyId = 0;
|
||||
@ -861,12 +902,51 @@ WorkspacesDisplay.prototype = {
|
||||
|
||||
this.workspacesView.destroy();
|
||||
this.workspacesView = null;
|
||||
this._unconstrainThumbnailIndicator();
|
||||
for (let w = 0; w < this._workspaces.length; w++) {
|
||||
this._workspaces[w].disconnectAll();
|
||||
this._workspaces[w].destroy();
|
||||
this._workspaceThumbnails[w].destroy();
|
||||
}
|
||||
},
|
||||
|
||||
_constrainThumbnailIndicator: function() {
|
||||
let active = global.screen.get_active_workspace_index();
|
||||
let thumbnail = this._workspaceThumbnails[active];
|
||||
|
||||
this._thumbnailIndicatorConstraints.forEach(function(constraint) {
|
||||
constraint.set_source(thumbnail.actor);
|
||||
constraint.set_enabled(true);
|
||||
});
|
||||
},
|
||||
|
||||
_unconstrainThumbnailIndicator: function() {
|
||||
this._thumbnailIndicatorConstraints.forEach(function(constraint) {
|
||||
constraint.set_enabled(false);
|
||||
});
|
||||
},
|
||||
|
||||
_activeWorkspaceChanged: function(wm, from, to, direction) {
|
||||
let active = global.screen.get_active_workspace_index();
|
||||
let thumbnail = this._workspaceThumbnails[active];
|
||||
|
||||
this._unconstrainThumbnailIndicator();
|
||||
let oldAllocation = this._thumbnailIndicator.allocation;
|
||||
this._thumbnailIndicator.x = oldAllocation.x1;
|
||||
this._thumbnailIndicator.y = oldAllocation.y1;
|
||||
this._thumbnailIndicator.width = oldAllocation.x2 - oldAllocation.x1;
|
||||
this._thumbnailIndicator.height = oldAllocation.y2 - oldAllocation.y1;
|
||||
|
||||
Tweener.addTween(this._thumbnailIndicator,
|
||||
{ x: thumbnail.actor.allocation.x1,
|
||||
y: thumbnail.actor.allocation.y1,
|
||||
time: WORKSPACE_SWITCH_TIME,
|
||||
transition: 'easeOutQuad',
|
||||
onComplete: Lang.bind(this,
|
||||
this._constrainThumbnailIndicator)
|
||||
});
|
||||
},
|
||||
|
||||
_onRestacked: function() {
|
||||
let stack = global.get_window_actors();
|
||||
let stackIndices = {};
|
||||
@ -877,6 +957,8 @@ WorkspacesDisplay.prototype = {
|
||||
}
|
||||
|
||||
this.workspacesView.syncStacking(stackIndices);
|
||||
for (let i = 0; i < this._workspaceThumbnails.length; i++)
|
||||
this._workspaceThumbnails[i].syncStacking(stackIndices);
|
||||
},
|
||||
|
||||
_workspacesChanged: function() {
|
||||
@ -893,7 +975,12 @@ WorkspacesDisplay.prototype = {
|
||||
for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
|
||||
let metaWorkspace = global.screen.get_workspace_by_index(w);
|
||||
this._workspaces[w] = new Workspace.Workspace(metaWorkspace);
|
||||
|
||||
let thumbnail = new WorkspaceThumbnail.WorkspaceThumbnail(metaWorkspace);
|
||||
this._workspaceThumbnails[w] = thumbnail;
|
||||
this._thumbnailsBox.add(thumbnail.actor);
|
||||
}
|
||||
this._thumbnailIndicator.raise_top();
|
||||
} else {
|
||||
// Assume workspaces are only removed sequentially
|
||||
// (e.g. 2,3,4 - not 2,4,7)
|
||||
@ -914,6 +1001,10 @@ WorkspacesDisplay.prototype = {
|
||||
// making its exit.
|
||||
for (let l = 0; l < lostWorkspaces.length; l++)
|
||||
lostWorkspaces[l].setReactive(false);
|
||||
|
||||
for (let k = removedIndex; k < removedIndex + removedNum; k++)
|
||||
this._workspaceThumbnails[k].destroy();
|
||||
this._workspaceThumbnails.splice(removedIndex, removedNum);
|
||||
}
|
||||
|
||||
this.workspacesView.updateWorkspaces(oldNumWorkspaces,
|
||||
|
Loading…
Reference in New Issue
Block a user