1b4cff7cdc
Allow the user to switch the active workspace by using the mousewheel over the workspace thumbnails. https://bugzilla.gnome.org/show_bug.cgi?id=642303
945 lines
35 KiB
JavaScript
945 lines
35 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 Meta = imports.gi.Meta;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
const Signals = imports.signals;
|
|
const Gettext = imports.gettext.domain('gnome-shell');
|
|
const _ = Gettext.gettext;
|
|
|
|
const DND = imports.ui.dnd;
|
|
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
|
|
const MAX_WORKSPACES = 16;
|
|
|
|
|
|
const CONTROLS_POP_IN_TIME = 0.1;
|
|
|
|
|
|
function WorkspacesView(width, height, x, y, workspaces) {
|
|
this._init(width, height, x, y, workspaces);
|
|
}
|
|
|
|
WorkspacesView.prototype = {
|
|
_init: function(width, height, x, y, workspaces) {
|
|
this.actor = new St.Group({ style_class: 'workspaces-view' });
|
|
this.actor.set_clip(x, y, width, height);
|
|
|
|
// The actor itself isn't a drop target, so we don't want to pick on its area
|
|
this.actor.set_size(0, 0);
|
|
|
|
this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
|
|
|
|
this.actor.connect('style-changed', Lang.bind(this,
|
|
function() {
|
|
let node = this.actor.get_theme_node();
|
|
this._spacing = node.get_length('spacing');
|
|
this._computeWorkspacePositions();
|
|
}));
|
|
this.actor.connect('notify::mapped',
|
|
Lang.bind(this, this._onMappedChanged));
|
|
|
|
this._width = width;
|
|
this._height = height;
|
|
this._x = x;
|
|
this._y = y;
|
|
this._zoomScale = 1.0;
|
|
this._spacing = 0;
|
|
this._activeWorkspaceX = 0; // x offset of active ws while dragging
|
|
this._activeWorkspaceY = 0; // y offset of active ws while dragging
|
|
this._lostWorkspaces = [];
|
|
this._animating = false; // tweening
|
|
this._scrolling = false; // swipe-scrolling
|
|
this._animatingScroll = false; // programatically updating the adjustment
|
|
this._zoomOut = false; // zoom to a larger area
|
|
this._inDrag = false; // dragging a window
|
|
|
|
let activeWorkspaceIndex = global.screen.get_active_workspace_index();
|
|
this._workspaces = workspaces;
|
|
|
|
// Add workspace actors
|
|
for (let w = 0; w < global.screen.n_workspaces; w++)
|
|
this._workspaces[w].actor.reparent(this.actor);
|
|
this._workspaces[activeWorkspaceIndex].actor.raise_top();
|
|
|
|
// Position/scale the desktop windows and their children after the
|
|
// workspaces have been created. This cannot be done first because
|
|
// window movement depends on the Workspaces object being accessible
|
|
// as an Overview member.
|
|
this._overviewShowingId =
|
|
Main.overview.connect('showing',
|
|
Lang.bind(this, function() {
|
|
for (let w = 0; w < this._workspaces.length; w++)
|
|
this._workspaces[w].zoomToOverview();
|
|
}));
|
|
|
|
this._scrollAdjustment = new St.Adjustment({ value: activeWorkspaceIndex,
|
|
lower: 0,
|
|
page_increment: 1,
|
|
page_size: 1,
|
|
step_increment: 0,
|
|
upper: this._workspaces.length });
|
|
this._scrollAdjustment.connect('notify::value',
|
|
Lang.bind(this, this._onScroll));
|
|
|
|
this._timeoutId = 0;
|
|
|
|
this._switchWorkspaceNotifyId =
|
|
global.window_manager.connect('switch-workspace',
|
|
Lang.bind(this, this._activeWorkspaceChanged));
|
|
|
|
this._itemDragBeginId = Main.overview.connect('item-drag-begin',
|
|
Lang.bind(this, this._dragBegin));
|
|
this._itemDragEndId = Main.overview.connect('item-drag-end',
|
|
Lang.bind(this, this._dragEnd));
|
|
this._windowDragBeginId = Main.overview.connect('window-drag-begin',
|
|
Lang.bind(this, this._dragBegin));
|
|
this._windowDragEndId = Main.overview.connect('window-drag-end',
|
|
Lang.bind(this, this._dragEnd));
|
|
this._swipeScrollBeginId = 0;
|
|
this._swipeScrollEndId = 0;
|
|
},
|
|
|
|
setZoomScale: function(zoomScale) {
|
|
if (zoomScale == this._zoomScale)
|
|
return;
|
|
|
|
this._zoomScale = zoomScale;
|
|
if (this._zoomOut) {
|
|
// If we are already zoomed out, then we have to reposition.
|
|
// Note that when shown initially zoomOut is false, so we
|
|
// won't trigger this.
|
|
|
|
// setZoomScale can be invoked when the workspaces view is
|
|
// reallocated. Since we just want to animate things to the
|
|
// new position it seems OK to call updateWorkspaceActors
|
|
// immediately - adding a tween doesn't immediately cause
|
|
// a new allocation. But hide/show of the window overlays we
|
|
// do around animation does, so we need to do it later.
|
|
// This can be removed when we fix things to not hide/show
|
|
// the window overlay.
|
|
Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
|
|
Lang.bind(this, function() {
|
|
this._computeWorkspacePositions();
|
|
this._updateWorkspaceActors(true);
|
|
}));
|
|
}
|
|
},
|
|
|
|
_lookupWorkspaceForMetaWindow: function (metaWindow) {
|
|
for (let i = 0; i < this._workspaces.length; i++) {
|
|
if (this._workspaces[i].containsMetaWindow(metaWindow))
|
|
return this._workspaces[i];
|
|
}
|
|
return null;
|
|
},
|
|
|
|
getActiveWorkspace: function() {
|
|
let active = global.screen.get_active_workspace_index();
|
|
return this._workspaces[active];
|
|
},
|
|
|
|
hide: function() {
|
|
let activeWorkspaceIndex = global.screen.get_active_workspace_index();
|
|
let activeWorkspace = this._workspaces[activeWorkspaceIndex];
|
|
|
|
activeWorkspace.actor.raise_top();
|
|
|
|
for (let w = 0; w < this._workspaces.length; w++)
|
|
this._workspaces[w].zoomFromOverview();
|
|
},
|
|
|
|
destroy: function() {
|
|
this.actor.destroy();
|
|
},
|
|
|
|
getScale: function() {
|
|
return this._workspaces[0].scale;
|
|
},
|
|
|
|
syncStacking: function(stackIndices) {
|
|
for (let i = 0; i < this._workspaces.length; i++)
|
|
this._workspaces[i].syncStacking(stackIndices);
|
|
},
|
|
|
|
// Get the grid position of the active workspace.
|
|
getActiveWorkspacePosition: function() {
|
|
let activeWorkspaceIndex = global.screen.get_active_workspace_index();
|
|
let activeWorkspace = this._workspaces[activeWorkspaceIndex];
|
|
|
|
return [activeWorkspace.x, activeWorkspace.y];
|
|
},
|
|
|
|
zoomOut: function() {
|
|
if (this._zoomOut)
|
|
return;
|
|
|
|
this._zoomOut = true;
|
|
this._computeWorkspacePositions();
|
|
this._updateWorkspaceActors(true);
|
|
},
|
|
|
|
zoomIn: function() {
|
|
if (!this._zoomOut)
|
|
return;
|
|
|
|
this._zoomOut = false;
|
|
this._computeWorkspacePositions();
|
|
this._updateWorkspaceActors(true);
|
|
},
|
|
|
|
// Compute the position, scale and opacity of the workspaces, but don't
|
|
// actually change the actors to match
|
|
_computeWorkspacePositions: function() {
|
|
let active = global.screen.get_active_workspace_index();
|
|
let zoomScale = this._zoomOut ? this._zoomScale : 1;
|
|
let scale = zoomScale * this._width / global.screen_width;
|
|
|
|
let _width = this._workspaces[0].actor.width * scale;
|
|
let _height = this._workspaces[0].actor.height * scale;
|
|
|
|
this._activeWorkspaceX = (this._width - _width) / 2;
|
|
this._activeWorkspaceY = (this._height - _height) / 2;
|
|
|
|
for (let w = 0; w < this._workspaces.length; w++) {
|
|
let workspace = this._workspaces[w];
|
|
|
|
workspace.opacity = (this._inDrag && w != active) ? 200 : 255;
|
|
|
|
workspace.scale = scale;
|
|
workspace.x = this._x + this._activeWorkspaceX;
|
|
|
|
// We adjust the center because the zoomScale is to leave space for
|
|
// the expanded workspace control so we want to zoom to either the
|
|
// left part of the area or the right part of the area
|
|
let offset = 0.5 * (1 - this._zoomScale) * this._width;
|
|
let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
|
|
if (this._zoomOut)
|
|
workspace.x += rtl ? offset : - offset;
|
|
|
|
// We divide by zoomScale so that adjacent workspaces are always offscreen
|
|
// except when we are switching between workspaces
|
|
workspace.y = this._y + this._activeWorkspaceY
|
|
+ (w - active) * (_height + this._spacing) / zoomScale;
|
|
}
|
|
},
|
|
|
|
_scrollToActive: function(showAnimation) {
|
|
let active = global.screen.get_active_workspace_index();
|
|
|
|
this._computeWorkspacePositions();
|
|
this._updateWorkspaceActors(showAnimation);
|
|
this._updateScrollAdjustment(active, showAnimation);
|
|
},
|
|
|
|
// Update workspace actors parameters to the values calculated in
|
|
// _computeWorkspacePositions()
|
|
// @showAnimation: iff %true, transition between states
|
|
_updateWorkspaceActors: function(showAnimation) {
|
|
let active = global.screen.get_active_workspace_index();
|
|
let targetWorkspaceNewY = this._y + this._activeWorkspaceY;
|
|
let targetWorkspaceCurrentY = this._workspaces[active].y;
|
|
let dy = targetWorkspaceNewY - targetWorkspaceCurrentY;
|
|
|
|
this._animating = showAnimation;
|
|
|
|
for (let w = 0; w < this._workspaces.length; w++) {
|
|
let workspace = this._workspaces[w];
|
|
|
|
Tweener.removeTweens(workspace.actor);
|
|
|
|
workspace.y += dy;
|
|
|
|
if (showAnimation) {
|
|
let params = { x: workspace.x,
|
|
y: workspace.y,
|
|
scale_x: workspace.scale,
|
|
scale_y: workspace.scale,
|
|
opacity: workspace.opacity,
|
|
time: WORKSPACE_SWITCH_TIME,
|
|
transition: 'easeOutQuad'
|
|
};
|
|
// we have to call _updateVisibility() once before the
|
|
// animation and once afterwards - it does not really
|
|
// matter which tween we use, so we pick the first one ...
|
|
if (w == 0) {
|
|
this._updateVisibility();
|
|
params.onComplete = Lang.bind(this,
|
|
function() {
|
|
this._animating = false;
|
|
this._updateVisibility();
|
|
});
|
|
}
|
|
Tweener.addTween(workspace.actor, params);
|
|
} else {
|
|
workspace.actor.set_scale(workspace.scale, workspace.scale);
|
|
workspace.actor.set_position(workspace.x, workspace.y);
|
|
workspace.actor.opacity = workspace.opacity;
|
|
if (w == 0)
|
|
this._updateVisibility();
|
|
}
|
|
}
|
|
|
|
for (let l = 0; l < this._lostWorkspaces.length; l++) {
|
|
let workspace = this._lostWorkspaces[l];
|
|
|
|
Tweener.removeTweens(workspace.actor);
|
|
|
|
workspace.y += dy;
|
|
workspace.actor.show();
|
|
workspace.hideWindowsOverlays();
|
|
|
|
if (showAnimation) {
|
|
Tweener.addTween(workspace.actor,
|
|
{ y: workspace.x,
|
|
time: WORKSPACE_SWITCH_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this,
|
|
this._cleanWorkspaces)
|
|
});
|
|
} else {
|
|
this._cleanWorkspaces();
|
|
}
|
|
}
|
|
},
|
|
|
|
_updateVisibility: function() {
|
|
let active = global.screen.get_active_workspace_index();
|
|
|
|
for (let w = 0; w < this._workspaces.length; w++) {
|
|
let workspace = this._workspaces[w];
|
|
if (this._animating || this._scrolling) {
|
|
workspace.hideWindowsOverlays();
|
|
workspace.actor.show();
|
|
} else {
|
|
workspace.showWindowsOverlays();
|
|
if (this._inDrag)
|
|
workspace.actor.visible = (Math.abs(w - active) <= 1);
|
|
else
|
|
workspace.actor.visible = (w == active);
|
|
}
|
|
}
|
|
},
|
|
|
|
_cleanWorkspaces: function() {
|
|
if (this._lostWorkspaces.length == 0)
|
|
return;
|
|
|
|
for (let l = 0; l < this._lostWorkspaces.length; l++)
|
|
this._lostWorkspaces[l].destroy();
|
|
this._lostWorkspaces = [];
|
|
|
|
this._computeWorkspacePositions();
|
|
this._updateWorkspaceActors(false);
|
|
},
|
|
|
|
_updateScrollAdjustment: function(index, showAnimation) {
|
|
if (this._scrolling)
|
|
return;
|
|
|
|
this._animatingScroll = true;
|
|
|
|
if (showAnimation) {
|
|
Tweener.addTween(this._scrollAdjustment, {
|
|
value: index,
|
|
time: WORKSPACE_SWITCH_TIME,
|
|
transition: 'easeOutQuad',
|
|
onComplete: Lang.bind(this,
|
|
function() {
|
|
this._animatingScroll = false;
|
|
})
|
|
});
|
|
} else {
|
|
this._scrollAdjustment.value = index;
|
|
this._animatingScroll = false;
|
|
}
|
|
},
|
|
|
|
updateWorkspaces: function(oldNumWorkspaces, newNumWorkspaces, lostWorkspaces) {
|
|
let active = global.screen.get_active_workspace_index();
|
|
|
|
for (let l = 0; l < lostWorkspaces.length; l++)
|
|
lostWorkspaces[l].disconnectAll();
|
|
|
|
Tweener.addTween(this._scrollAdjustment,
|
|
{ upper: newNumWorkspaces,
|
|
time: WORKSPACE_SWITCH_TIME,
|
|
transition: 'easeOutQuad'
|
|
});
|
|
|
|
if (newNumWorkspaces > oldNumWorkspaces) {
|
|
for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++)
|
|
this.actor.add_actor(this._workspaces[w].actor);
|
|
|
|
this._computeWorkspacePositions();
|
|
this._updateWorkspaceActors(false);
|
|
} else {
|
|
this._lostWorkspaces = lostWorkspaces;
|
|
}
|
|
|
|
this._scrollToActive(true);
|
|
},
|
|
|
|
_activeWorkspaceChanged: function(wm, from, to, direction) {
|
|
if (this._scrolling)
|
|
return;
|
|
|
|
this._scrollToActive(true);
|
|
},
|
|
|
|
_onDestroy: function() {
|
|
this._scrollAdjustment.run_dispose();
|
|
Main.overview.disconnect(this._overviewShowingId);
|
|
global.window_manager.disconnect(this._switchWorkspaceNotifyId);
|
|
|
|
if (this._timeoutId) {
|
|
Mainloop.source_remove(this._timeoutId);
|
|
this._timeoutId = 0;
|
|
}
|
|
if (this._itemDragBeginId > 0) {
|
|
Main.overview.disconnect(this._itemDragBeginId);
|
|
this._itemDragBeginId = 0;
|
|
}
|
|
if (this._itemDragEndId > 0) {
|
|
Main.overview.disconnect(this._itemDragEndId);
|
|
this._itemDragEndId = 0;
|
|
}
|
|
if (this._windowDragBeginId > 0) {
|
|
Main.overview.disconnect(this._windowDragBeginId);
|
|
this._windowDragBeginId = 0;
|
|
}
|
|
if (this._windowDragEndId > 0) {
|
|
Main.overview.disconnect(this._windowDragEndId);
|
|
this._windowDragEndId = 0;
|
|
}
|
|
},
|
|
|
|
_onMappedChanged: function() {
|
|
if (this.actor.mapped) {
|
|
let direction = Overview.SwipeScrollDirection.VERTICAL;
|
|
Main.overview.setScrollAdjustment(this._scrollAdjustment,
|
|
direction);
|
|
this._swipeScrollBeginId = Main.overview.connect('swipe-scroll-begin',
|
|
Lang.bind(this, this._swipeScrollBegin));
|
|
this._swipeScrollEndId = Main.overview.connect('swipe-scroll-end',
|
|
Lang.bind(this, this._swipeScrollEnd));
|
|
} else {
|
|
Main.overview.disconnect(this._swipeScrollBeginId);
|
|
Main.overview.disconnect(this._swipeScrollEndId);
|
|
}
|
|
},
|
|
|
|
_dragBegin: function() {
|
|
if (this._scrolling)
|
|
return;
|
|
|
|
this._inDrag = true;
|
|
|
|
this._dragMonitor = {
|
|
dragMotion: Lang.bind(this, this._onDragMotion)
|
|
};
|
|
DND.addDragMonitor(this._dragMonitor);
|
|
},
|
|
|
|
_onDragMotion: function(dragEvent) {
|
|
if (Main.overview.animationInProgress)
|
|
return DND.DragMotionResult.CONTINUE;
|
|
|
|
let primary = global.get_primary_monitor();
|
|
|
|
let activeWorkspaceIndex = global.screen.get_active_workspace_index();
|
|
let topWorkspace, bottomWorkspace;
|
|
topWorkspace = this._workspaces[activeWorkspaceIndex - 1];
|
|
bottomWorkspace = this._workspaces[activeWorkspaceIndex + 1];
|
|
let hoverWorkspace = null;
|
|
|
|
// reactive monitor edges
|
|
let topEdge = primary.y;
|
|
let switchTop = (dragEvent.y <= topEdge && topWorkspace);
|
|
if (switchTop && this._dragOverLastY != topEdge) {
|
|
topWorkspace.metaWorkspace.activate(global.get_current_time());
|
|
topWorkspace.setReservedSlot(dragEvent.dragActor._delegate);
|
|
this._dragOverLastY = topEdge;
|
|
|
|
return DND.DragMotionResult.CONTINUE;
|
|
}
|
|
let bottomEdge = primary.y + primary.height - 1;
|
|
let switchBottom = (dragEvent.y >= bottomEdge && bottomWorkspace);
|
|
if (switchBottom && this._dragOverLastY != bottomEdge) {
|
|
bottomWorkspace.metaWorkspace.activate(global.get_current_time());
|
|
bottomWorkspace.setReservedSlot(dragEvent.dragActor._delegate);
|
|
this._dragOverLastY = bottomEdge;
|
|
|
|
return DND.DragMotionResult.CONTINUE;
|
|
}
|
|
this._dragOverLastY = dragEvent.y;
|
|
let result = DND.DragMotionResult.CONTINUE;
|
|
|
|
// check hover state of new workspace area / inactive workspaces
|
|
if (topWorkspace) {
|
|
if (topWorkspace.actor.contains(dragEvent.targetActor)) {
|
|
hoverWorkspace = topWorkspace;
|
|
topWorkspace.opacity = topWorkspace.actor.opacity = 255;
|
|
result = topWorkspace.handleDragOver(dragEvent.source, dragEvent.dragActor);
|
|
} else {
|
|
topWorkspace.opacity = topWorkspace.actor.opacity = 200;
|
|
}
|
|
}
|
|
|
|
if (bottomWorkspace) {
|
|
if (bottomWorkspace.actor.contains(dragEvent.targetActor)) {
|
|
hoverWorkspace = bottomWorkspace;
|
|
bottomWorkspace.opacity = bottomWorkspace.actor.opacity = 255;
|
|
result = bottomWorkspace.handleDragOver(dragEvent.source, dragEvent.dragActor);
|
|
} else {
|
|
bottomWorkspace.opacity = bottomWorkspace.actor.opacity = 200;
|
|
}
|
|
}
|
|
|
|
// handle delayed workspace switches
|
|
if (hoverWorkspace) {
|
|
if (!this._timeoutId)
|
|
this._timeoutId = Mainloop.timeout_add_seconds(1,
|
|
Lang.bind(this, function() {
|
|
hoverWorkspace.metaWorkspace.activate(global.get_current_time());
|
|
hoverWorkspace.setReservedSlot(dragEvent.dragActor._delegate);
|
|
return false;
|
|
}));
|
|
} else {
|
|
if (this._timeoutId) {
|
|
Mainloop.source_remove(this._timeoutId);
|
|
this._timeoutId = 0;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
_dragEnd: function() {
|
|
if (this._timeoutId) {
|
|
Mainloop.source_remove(this._timeoutId);
|
|
this._timeoutId = 0;
|
|
}
|
|
DND.removeMonitor(this._dragMonitor);
|
|
this._inDrag = false;
|
|
|
|
for (let i = 0; i < this._workspaces.length; i++)
|
|
this._workspaces[i].setReservedSlot(null);
|
|
},
|
|
|
|
_swipeScrollBegin: function() {
|
|
this._scrolling = true;
|
|
},
|
|
|
|
_swipeScrollEnd: function(overview, result) {
|
|
this._scrolling = false;
|
|
|
|
if (result == Overview.SwipeScrollResult.CLICK) {
|
|
let [x, y, mod] = global.get_pointer();
|
|
let actor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL,
|
|
x, y);
|
|
|
|
// Only switch to the workspace when there's no application
|
|
// windows open. The problem is that it's too easy to miss
|
|
// an app window and get the wrong one focused.
|
|
let active = global.screen.get_active_workspace_index();
|
|
if (this._workspaces[active].isEmpty() &&
|
|
this.actor.contains(actor))
|
|
Main.overview.hide();
|
|
}
|
|
|
|
if (result == Overview.SwipeScrollResult.SWIPE)
|
|
// The active workspace has changed; while swipe-scrolling
|
|
// has already taken care of the positioning, the cached
|
|
// positions need to be updated
|
|
this._computeWorkspacePositions();
|
|
|
|
// Make sure title captions etc are shown as necessary
|
|
this._updateVisibility();
|
|
},
|
|
|
|
// sync the workspaces' positions to the value of the scroll adjustment
|
|
// and change the active workspace if appropriate
|
|
_onScroll: function(adj) {
|
|
if (this._animatingScroll)
|
|
return;
|
|
|
|
let active = global.screen.get_active_workspace_index();
|
|
let current = Math.round(adj.value);
|
|
|
|
if (active != current) {
|
|
let metaWorkspace = this._workspaces[current].metaWorkspace;
|
|
metaWorkspace.activate(global.get_current_time());
|
|
}
|
|
|
|
let last = this._workspaces.length - 1;
|
|
let firstWorkspaceY = this._workspaces[0].actor.y;
|
|
let lastWorkspaceY = this._workspaces[last].actor.y;
|
|
let workspacesHeight = lastWorkspaceY - firstWorkspaceY;
|
|
|
|
if (adj.upper == 1)
|
|
return;
|
|
|
|
let currentY = firstWorkspaceY;
|
|
let newY = this._y - adj.value / (adj.upper - 1) * workspacesHeight;
|
|
|
|
let dy = newY - currentY;
|
|
|
|
for (let i = 0; i < this._workspaces.length; i++) {
|
|
this._workspaces[i].hideWindowsOverlays();
|
|
this._workspaces[i].actor.visible = Math.abs(i - adj.value) <= 1;
|
|
this._workspaces[i].actor.y += dy;
|
|
}
|
|
},
|
|
|
|
_getWorkspaceIndexToRemove: function() {
|
|
return global.screen.get_active_workspace_index();
|
|
}
|
|
};
|
|
Signals.addSignalMethods(WorkspacesView.prototype);
|
|
|
|
|
|
function WorkspacesDisplay() {
|
|
this._init();
|
|
}
|
|
|
|
WorkspacesDisplay.prototype = {
|
|
_init: function() {
|
|
this.actor = new Shell.GenericContainer();
|
|
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 controls = new St.Bin({ style_class: 'workspace-controls',
|
|
request_mode: Clutter.RequestMode.WIDTH_FOR_HEIGHT,
|
|
y_align: St.Align.START,
|
|
y_fill: true });
|
|
this._controls = controls;
|
|
this.actor.add_actor(controls);
|
|
|
|
controls.reactive = true;
|
|
controls.track_hover = true;
|
|
controls.connect('notify::hover',
|
|
Lang.bind(this, this._onControlsHoverChanged));
|
|
controls.connect('scroll-event',
|
|
Lang.bind(this, this._onScrollEvent));
|
|
|
|
|
|
this._thumbnailsBox = new WorkspaceThumbnail.ThumbnailsBox();
|
|
controls.add_actor(this._thumbnailsBox.actor);
|
|
|
|
this.workspacesView = null;
|
|
|
|
this._inDrag = false;
|
|
this._zoomOut = false;
|
|
this._zoomFraction = 0;
|
|
|
|
this._nWorkspacesNotifyId = 0;
|
|
this._switchWorkspaceNotifyId = 0;
|
|
|
|
this._itemDragBeginId = 0;
|
|
this._itemDragEndId = 0;
|
|
this._windowDragBeginId = 0;
|
|
this._windowDragEndId = 0;
|
|
},
|
|
|
|
show: function() {
|
|
this._controls.show();
|
|
this._thumbnailsBox.show();
|
|
|
|
this._workspaces = [];
|
|
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 rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
|
|
|
|
let totalAllocation = this.actor.allocation;
|
|
let totalWidth = totalAllocation.x2 - totalAllocation.x1;
|
|
let totalHeight = totalAllocation.y2 - totalAllocation.y1;
|
|
|
|
let controlsVisible = this._controls.get_theme_node().get_length('visible-width');
|
|
|
|
totalWidth -= controlsVisible;
|
|
|
|
// Workspaces expect to have the same ratio as the screen, so take
|
|
// this into account when fitting the workspace into the available space
|
|
let width, height;
|
|
let totalRatio = totalWidth / totalHeight;
|
|
let wsRatio = global.screen_width / global.screen_height;
|
|
if (wsRatio > totalRatio) {
|
|
width = totalWidth;
|
|
height = Math.floor(totalWidth / wsRatio);
|
|
} else {
|
|
width = Math.floor(totalHeight * wsRatio);
|
|
height = totalHeight;
|
|
}
|
|
|
|
// Position workspaces in the available space
|
|
let [x, y] = this.actor.get_transformed_position();
|
|
x = Math.floor(x + Math.abs(totalWidth - width) / 2);
|
|
y = Math.floor(y + Math.abs(totalHeight - height) / 2);
|
|
|
|
if (rtl)
|
|
x += controlsVisible;
|
|
|
|
let newView = new WorkspacesView(width, height, x, y, this._workspaces);
|
|
this._updateZoomScale();
|
|
|
|
if (this.workspacesView)
|
|
this.workspacesView.destroy();
|
|
this.workspacesView = newView;
|
|
|
|
this._nWorkspacesNotifyId =
|
|
global.screen.connect('notify::n-workspaces',
|
|
Lang.bind(this, this._workspacesChanged));
|
|
|
|
this._restackedNotifyId =
|
|
global.screen.connect('restacked',
|
|
Lang.bind(this, this._onRestacked));
|
|
|
|
if (this._itemDragBeginId == 0)
|
|
this._itemDragBeginId = Main.overview.connect('item-drag-begin',
|
|
Lang.bind(this, this._dragBegin));
|
|
if (this._itemDragEndId == 0)
|
|
this._itemDragEndId = Main.overview.connect('item-drag-end',
|
|
Lang.bind(this, this._dragEnd));
|
|
if (this._windowDragBeginId == 0)
|
|
this._windowDragBeginId = Main.overview.connect('window-drag-begin',
|
|
Lang.bind(this, this._dragBegin));
|
|
if (this._windowDragEndId == 0)
|
|
this._windowDragEndId = Main.overview.connect('window-drag-end',
|
|
Lang.bind(this, this._dragEnd));
|
|
|
|
this._onRestacked();
|
|
this._zoomOut = false;
|
|
this._zoomFraction = 0;
|
|
this._updateZoom();
|
|
},
|
|
|
|
hide: function() {
|
|
this._controls.hide();
|
|
this._thumbnailsBox.hide();
|
|
|
|
if (this._nWorkspacesNotifyId > 0) {
|
|
global.screen.disconnect(this._nWorkspacesNotifyId);
|
|
this._nWorkspacesNotifyId = 0;
|
|
}
|
|
if (this._restackedNotifyId > 0){
|
|
global.screen.disconnect(this._restackedNotifyId);
|
|
this._restackedNotifyId = 0;
|
|
}
|
|
if (this._itemDragBeginId > 0) {
|
|
Main.overview.disconnect(this._itemDragBeginId);
|
|
this._itemDragBeginId = 0;
|
|
}
|
|
if (this._itemEndBeginId > 0) {
|
|
Main.overview.disconnect(this._itemDragEndId);
|
|
this._itemDragEndId = 0;
|
|
}
|
|
if (this._windowDragBeginId > 0) {
|
|
Main.overview.disconnect(this._windowDragBeginId);
|
|
this._windowDragBeginId = 0;
|
|
}
|
|
if (this._windowDragEndId > 0) {
|
|
Main.overview.disconnect(this._windowDragEndId);
|
|
this._windowDragEndId = 0;
|
|
}
|
|
|
|
this.workspacesView.destroy();
|
|
this.workspacesView = null;
|
|
for (let w = 0; w < this._workspaces.length; w++) {
|
|
this._workspaces[w].disconnectAll();
|
|
this._workspaces[w].destroy();
|
|
}
|
|
},
|
|
|
|
// zoomFraction property allows us to tween the controls sliding in and out
|
|
set zoomFraction(fraction) {
|
|
this._zoomFraction = fraction;
|
|
this.actor.queue_relayout();
|
|
},
|
|
|
|
get zoomFraction() {
|
|
return this._zoomFraction;
|
|
},
|
|
|
|
_getPreferredWidth: function (actor, forHeight, alloc) {
|
|
// pass through the call in case the child needs it, but report 0x0
|
|
this._controls.get_preferred_width(forHeight);
|
|
},
|
|
|
|
_getPreferredHeight: function (actor, forWidth, alloc) {
|
|
// pass through the call in case the child needs it, but report 0x0
|
|
this._controls.get_preferred_height(forWidth);
|
|
},
|
|
|
|
_allocate: function (actor, box, flags) {
|
|
let childBox = new Clutter.ActorBox();
|
|
|
|
let totalWidth = box.x2 - box.x1;
|
|
|
|
// width of the controls
|
|
let [controlsMin, controlsNatural] = this._controls.get_preferred_width(box.y2 - box.y1);
|
|
|
|
// Amount of space on the screen we reserve for the visible control
|
|
let controlsVisible = this._controls.get_theme_node().get_length('visible-width');
|
|
let controlsReserved = controlsVisible * (1 - this._zoomFraction) + controlsNatural * this._zoomFraction;
|
|
|
|
let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
|
|
if (rtl) {
|
|
childBox.x2 = controlsReserved;
|
|
childBox.x1 = childBox.x2 - controlsNatural;
|
|
} else {
|
|
childBox.x1 = totalWidth - controlsReserved;
|
|
childBox.x2 = childBox.x1 + controlsNatural;
|
|
}
|
|
|
|
childBox.y1 = 0;
|
|
childBox.y2 = box.y2- box.y1;
|
|
this._controls.allocate(childBox, flags);
|
|
|
|
this._updateZoomScale();
|
|
},
|
|
|
|
_updateZoomScale: function() {
|
|
if (!this.workspacesView)
|
|
return;
|
|
|
|
let totalAllocation = this.actor.allocation;
|
|
let totalWidth = totalAllocation.x2 - totalAllocation.x1;
|
|
let totalHeight = totalAllocation.y2 - totalAllocation.y1;
|
|
|
|
let [controlsMin, controlsNatural] = this._controls.get_preferred_width(totalHeight);
|
|
let controlsVisible = this._controls.get_theme_node().get_length('visible-width');
|
|
|
|
let zoomScale = (totalWidth - controlsNatural) / (totalWidth - controlsVisible);
|
|
this.workspacesView.setZoomScale(zoomScale);
|
|
},
|
|
|
|
_onRestacked: function() {
|
|
let stack = global.get_window_actors();
|
|
let stackIndices = {};
|
|
|
|
for (let i = 0; i < stack.length; i++) {
|
|
// Use the stable sequence for an integer to use as a hash key
|
|
stackIndices[stack[i].get_meta_window().get_stable_sequence()] = i;
|
|
}
|
|
|
|
this.workspacesView.syncStacking(stackIndices);
|
|
this._thumbnailsBox.syncStacking(stackIndices);
|
|
},
|
|
|
|
_workspacesChanged: function() {
|
|
let oldNumWorkspaces = this._workspaces.length;
|
|
let newNumWorkspaces = global.screen.n_workspaces;
|
|
let active = global.screen.get_active_workspace_index();
|
|
|
|
if (oldNumWorkspaces == newNumWorkspaces)
|
|
return;
|
|
|
|
let lostWorkspaces = [];
|
|
if (newNumWorkspaces > oldNumWorkspaces) {
|
|
// Assume workspaces are only added at the end
|
|
for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
|
|
let metaWorkspace = global.screen.get_workspace_by_index(w);
|
|
this._workspaces[w] = new Workspace.Workspace(metaWorkspace);
|
|
}
|
|
|
|
this._thumbnailsBox.addThumbnails(oldNumWorkspaces, newNumWorkspaces - oldNumWorkspaces);
|
|
} else {
|
|
// Assume workspaces are only removed sequentially
|
|
// (e.g. 2,3,4 - not 2,4,7)
|
|
let removedIndex;
|
|
let removedNum = oldNumWorkspaces - newNumWorkspaces;
|
|
for (let w = 0; w < oldNumWorkspaces; w++) {
|
|
let metaWorkspace = global.screen.get_workspace_by_index(w);
|
|
if (this._workspaces[w].metaWorkspace != metaWorkspace) {
|
|
removedIndex = w;
|
|
break;
|
|
}
|
|
}
|
|
|
|
lostWorkspaces = this._workspaces.splice(removedIndex,
|
|
removedNum);
|
|
|
|
// Don't let the user try to select this workspace as it's
|
|
// making its exit.
|
|
for (let l = 0; l < lostWorkspaces.length; l++)
|
|
lostWorkspaces[l].setReactive(false);
|
|
|
|
this._thumbnailsBox.removeThumbmails(removedIndex, removedNum);
|
|
}
|
|
|
|
this.workspacesView.updateWorkspaces(oldNumWorkspaces,
|
|
newNumWorkspaces,
|
|
lostWorkspaces);
|
|
},
|
|
|
|
_updateZoom : function() {
|
|
if (Main.overview.animationInProgress)
|
|
return;
|
|
|
|
let shouldZoom = this._controls.hover || this._inDrag;
|
|
if (shouldZoom != this._zoomOut) {
|
|
this._zoomOut = shouldZoom;
|
|
|
|
if (!this.workspacesView)
|
|
return;
|
|
|
|
Tweener.addTween(this,
|
|
{ zoomFraction: this._zoomOut ? 1 : 0,
|
|
time: WORKSPACE_SWITCH_TIME,
|
|
transition: 'easeOutQuad' });
|
|
|
|
if (shouldZoom)
|
|
this.workspacesView.zoomOut();
|
|
else
|
|
this.workspacesView.zoomIn();
|
|
}
|
|
},
|
|
|
|
_onControlsHoverChanged: function() {
|
|
this._updateZoom();
|
|
},
|
|
|
|
_dragBegin: function() {
|
|
this._inDrag = true;
|
|
this._updateZoom();
|
|
},
|
|
|
|
_dragEnd: function() {
|
|
this._inDrag = false;
|
|
|
|
// We do this deferred because drag-end is emitted before dnd.js emits
|
|
// event/leave events that were suppressed during the drag. If we didn't
|
|
// defer this, we'd zoom out then immediately zoom in because of the
|
|
// enter event we received. That would normally be invisible but we
|
|
// might as well avoid it.
|
|
Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
|
|
Lang.bind(this, this._updateZoom));
|
|
},
|
|
|
|
_onScrollEvent: function (actor, event) {
|
|
switch ( event.get_scroll_direction() ) {
|
|
case Clutter.ScrollDirection.UP:
|
|
Main.wm.actionMoveWorkspaceUp();
|
|
break;
|
|
case Clutter.ScrollDirection.DOWN:
|
|
Main.wm.actionMoveWorkspaceDown();
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
Signals.addSignalMethods(WorkspacesDisplay.prototype);
|