diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 04b63a8ec..241ac57b2 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -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 { diff --git a/js/Makefile.am b/js/Makefile.am index 21683af19..955c308ff 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -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 diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js new file mode 100644 index 000000000..3d21d2ca2 --- /dev/null +++ b/js/ui/workspaceThumbnail.js @@ -0,0 +1,298 @@ +/* -*- 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(), + clip_to_allocation: true, + 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.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.metaWindow._delegate = null; + 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) { + // When dragging a window, we use this slot for reserve space. + this.metaWorkspace = metaWorkspace; + + this.actor = new St.Bin({ reactive: 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); diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js index afc00e588..c7a22b128 100644 --- a/js/ui/workspacesView.js +++ b/js/ui/workspacesView.js @@ -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 @@ -794,6 +795,27 @@ 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.X })); + this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.Y })); + this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.WIDTH })); + this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.HEIGHT })); + 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() { @@ -828,11 +850,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 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; @@ -879,33 +912,112 @@ 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', 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._constrainThumbnailIndicator(); + this._zoomOut = false; + this._updateZoom(); }, hide: function() { - this._controlsContainer.hide(); + this._controls.hide(); - if (this._nWorkspacesNotifyId > 0) + if (this._nWorkspacesNotifyId > 0) { 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; } + 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; + 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 = {}; @@ -934,7 +1046,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) @@ -955,6 +1072,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,