diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 1e876f6d8..59ba015ae 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -509,8 +509,9 @@ StTooltip StLabel { width: 60px; } -.dash-placeholder { +.placeholder { background-image: url("dash-placeholder.svg"); + height: 24px; } #viewSelector { diff --git a/js/ui/dash.js b/js/ui/dash.js index 36588c0d9..448a5ede9 100644 --- a/js/ui/dash.js +++ b/js/ui/dash.js @@ -231,7 +231,7 @@ DragPlaceholderItem.prototype = { _init: function() { DashItemContainer.prototype._init.call(this); - this.setChild(new St.Bin({ style_class: 'dash-placeholder' })); + this.setChild(new St.Bin({ style_class: 'placeholder' })); } }; diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js index 0369c3825..3885d85cc 100644 --- a/js/ui/workspaceThumbnail.js +++ b/js/ui/workspaceThumbnail.js @@ -20,6 +20,11 @@ let MAX_THUMBNAIL_SCALE = 1/8.; const RESCALE_ANIMATION_TIME = 0.2; const SLIDE_ANIMATION_TIME = 0.2; +// When we create workspaces by dragging, we add a "cut" into the top and +// bottom of each workspace so that the user doesn't have to hit the +// placeholder exactly. +const WORKSPACE_CUT_SIZE = 10; + function WindowClone(realWindow) { this._init(realWindow); } @@ -422,6 +427,11 @@ WorkspaceThumbnail.prototype = { if (this.state > ThumbnailState.NORMAL) return DND.DragMotionResult.CONTINUE; + let [w, h] = this.actor.get_transformed_size(); + // Bubble up if we're in the "workspace cut". + if (y < WORKSPACE_CUT_SIZE || y > h - WORKSPACE_CUT_SIZE) + return DND.DragMotionResult.CONTINUE; + if (source.realWindow && !this._isMyWindow(source.realWindow)) return DND.DragMotionResult.MOVE_DROP; if (source.shellWorkspaceLaunch) @@ -475,6 +485,7 @@ ThumbnailsBox.prototype = { 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)); + this.actor._delegate = this; // When we animate the scale, we don't animate the requested size of the thumbnails, rather // we ask for our final size and then animate within that size. This slightly simplifies the @@ -498,6 +509,10 @@ ThumbnailsBox.prototype = { this._indicator = indicator; this.actor.add_actor(indicator); + this._dropPlaceholderPos = -1; + this._dropPlaceholder = new St.Bin({ style_class: 'placeholder' }); + this.actor.add_actor(this._dropPlaceholder); + this._targetScale = 0; this._scale = 0; this._pendingScaleUpdate = false; @@ -512,6 +527,83 @@ ThumbnailsBox.prototype = { this._thumbnails = []; }, + // Draggable target interface + handleDragOver : function(source, actor, x, y, time) { + if (!source.realWindow && !source.shellWorkspaceLaunch) + return DND.DragMotionResult.CONTINUE; + + let spacing = this.actor.get_theme_node().get_length('spacing'); + let thumbHeight = this._porthole.height * this._scale; + + let workspace = -1; + let firstThumbY = this._thumbnails[0].actor.y; + for (let i = 0; i < this._thumbnails.length; i ++) { + let targetBase = firstThumbY + (thumbHeight + spacing) * i; + + // Allow the reorder target to have a 10px "cut" into + // each side of the thumbnail, to make dragging onto the + // placeholder easier + let targetTop = targetBase - spacing - WORKSPACE_CUT_SIZE; + let targetBottom = targetBase + WORKSPACE_CUT_SIZE; + + // Expand the target to include the placeholder, if it exists. + if (i == this._dropPlaceholderPos) + targetBottom += this._dropPlaceholder.get_height(); + + if (y > targetTop && y <= targetBottom) { + workspace = i; + break; + } + } + + this._dropPlaceholderPos = workspace; + this.actor.queue_relayout(); + + if (workspace == -1) + return DND.DragMotionResult.CONTINUE; + + return DND.DragMotionResult.MOVE_DROP; + }, + + acceptDrop: function(source, actor, x, y, time) { + if (this._dropPlaceholderPos == -1) + return false; + + if (!source.realWindow && !source.shellWorkspaceLaunch) + return false; + + let isWindow = !!source.realWindow; + + // To create a new workspace, we first slide all the windows on workspaces + // below us to the next workspace, leaving a blank workspace for us to recycle. + let newWorkspaceIndex; + [newWorkspaceIndex, this._dropPlaceholderPos] = [this._dropPlaceholderPos, -1]; + + // Nab all the windows below us. + let windows = global.get_window_actors().filter(function(win) { + if (isWindow) + return win.get_workspace() >= newWorkspaceIndex && win != source; + else + return win.get_workspace() >= newWorkspaceIndex; + }); + + // ... move them down one. + windows.forEach(function(win) { + win.meta_window.change_workspace_by_index(win.get_workspace() + 1, + true, time); + }); + + if (isWindow) + // ... and bam, a workspace, good as new. + source.metaWindow.change_workspace_by_index(newWorkspaceIndex, + true, time); + else (source.shellWorkspaceLaunch) + source.shellWorkspaceLaunch({ workspace: newWorkspaceIndex, + timestamp: time }); + + return true; + }, + show: function() { this._switchWorkspaceNotifyId = global.window_manager.connect('switch-workspace', @@ -840,20 +932,18 @@ ThumbnailsBox.prototype = { let y = contentBox.y1; + if (this._dropPlaceholderPos == -1) { + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { + this._dropPlaceholder.hide(); + })); + } + for (let i = 0; i < this._thumbnails.length; i++) { let thumbnail = this._thumbnails[i]; if (i > 0) y += spacing - Math.round(thumbnail.collapseFraction * spacing); - // 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 roundedVScale = (y2 - y1) / portholeHeight; - let x1, x2; if (rtl) { x1 = contentBox.x1 + slideOffset * thumbnail.slidePosition; @@ -863,6 +953,27 @@ ThumbnailsBox.prototype = { x2 = x1 + thumbnailWidth; } + if (i == this._dropPlaceholderPos) { + let [minHeight, placeholderHeight] = this._dropPlaceholder.get_preferred_height(-1); + childBox.x1 = x1; + childBox.x2 = x1 + thumbnailWidth; + childBox.y1 = Math.round(y); + childBox.y2 = Math.round(y + placeholderHeight); + this._dropPlaceholder.allocate(childBox, flags); + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { + this._dropPlaceholder.show(); + })); + y += placeholderHeight + spacing; + } + + // 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 roundedVScale = (y2 - y1) / portholeHeight; + if (thumbnail.metaWorkspace == indicatorWorkspace) indicatorY = y1;