From 9b89ba6270838064dd2177c19bf003f80730e755 Mon Sep 17 00:00:00 2001 From: Owen Taylor Date: Fri, 23 Jan 2009 19:21:20 +0000 Subject: [PATCH] Allow windows to be dragged to other workspaces * Make updating of the clone title more of a state-machine - instead of showing/hiding/creating/raising the title all over the code, have a single Workspace._updateCloneTitle() method that looks at state bits and decides if the clone should be hidden or shown, and updates the stacking and position. * Move code to positioning of windows within a workspace in the overlay modeto a new method Workspace._positionWindows() * Add Workspace.addWindow()/removeWindow() to add and remove windows from the workspace on the fly. (Triggered manually: we still don't handle external changes to windows when the overlay is up.) * Hook up mouse-dragging for window actors and add a ::window-dragged signal to Workspace * Connect to ::window-dragged for each workspace, compute the new workspace, move it the window there, and animate everything into the new position. Snap back to the old location if the window didn't move. http://bugzilla.gnome.org/show_bug.cgi?id=568753 svn path=/trunk/; revision=164 --- js/ui/workspaces.js | 374 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 299 insertions(+), 75 deletions(-) diff --git a/js/ui/workspaces.js b/js/ui/workspaces.js index 99d393bfc..37bcfc919 100644 --- a/js/ui/workspaces.js +++ b/js/ui/workspaces.js @@ -2,8 +2,10 @@ const Tweener = imports.tweener.tweener; const Clutter = imports.gi.Clutter; -const Pango = imports.gi.Pango; +const Gtk = imports.gi.Gtk; const Lang = imports.lang; +const Pango = imports.gi.Pango; +const Signals = imports.signals; const Main = imports.ui.main; const Overlay = imports.ui.overlay; @@ -16,6 +18,7 @@ const GdkPixbuf = imports.gi.GdkPixbuf; // Windows are slightly translucent in the overlay mode const WINDOW_OPACITY = 0.9 * 255; const FOCUS_ANIMATION_TIME = 0.15; +const SNAP_BACK_ANIMATION_TIME = 0.25; const WINDOWCLONE_BG_COLOR = new Clutter.Color(); WINDOWCLONE_BG_COLOR.from_pixel(0x000000f0); @@ -53,7 +56,7 @@ Workspace.prototype = { let me = this; let global = Shell.Global.get(); - this._workspaceNum = workspaceNum; + this.workspaceNum = workspaceNum; this.actor = new Clutter.Group(); this.scale = 1.0; @@ -83,20 +86,7 @@ Workspace.prototype = { this._windows = [this._desktop]; for (let i = 0; i < windows.length; i++) { if (this._isOverlayWindow(windows[i])) { - let clone = this._makeClone(windows[i], i); - clone.connect("button-press-event", - function(clone, event) { - clone.raise_top(); - me._activateWindow(clone.realWindow, event.get_time()); - }); - clone.connect('enter-event', function (a, e) { - me._cloneEnter(clone, e); - }); - clone.connect('leave-event', function (a, e) { - me._cloneLeave(clone, e); - }); - this.actor.add_actor(clone); - this._windows.push(clone); + this._addWindowClone(windows[i]); } } @@ -207,10 +197,91 @@ Workspace.prototype = { this._desktop.height + 2 * FRAME_SIZE / this.actor.scale_y); }, - // Animate the full-screen to overlay transition. - zoomToOverlay : function() { + // Reposition all windows in their zoomed-to-overlay position. if workspaceZooming + // is true, then the workspace is moving at the same time and we need to take + // that into account + _positionWindows : function(workspaceZooming) { let global = Shell.Global.get(); + for (let i = 1; i < this._windows.length; i++) { + let clone = this._windows[i]; + + let [xCenter, yCenter, fraction] = this._computeWindowPosition(i); + xCenter = xCenter * global.screen_width; + yCenter = yCenter * global.screen_height; + + let size = Math.max(clone.width, clone.height); + let desiredSize = global.screen_width * fraction; + let scale = Math.min(desiredSize / size, 1.0); + + let tweenProperties = { + x: xCenter - 0.5 * scale * clone.width, + y: yCenter - 0.5 * scale * clone.height, + scale_x: scale, + scale_y: scale, + time: Overlay.ANIMATION_TIME, + opacity: WINDOW_OPACITY, + transition: "easeOutQuad", + onComplete: this._onCloneAnimationComplete, + onCompleteScope: this, + onCompleteParams: [clone] + }; + + // workspace_relative assumes that the workspace is zooming in our out + if (workspaceZooming) + tweenProperties['workspace_relative'] = this; + + Tweener.addTween(clone, tweenProperties); + clone._animationCount++; + } + }, + + // Remove a window from the workspace - this is called to fix up the visual + // display for changes to the window state that have already been made + removeWindow : function(win) { + // find the position of the window in our list + let index = - 1; + for (let i = 0; i < this._windows.length; i++) { + if (this._windows[i].realWindow == win) { + index = i; + break; + } + } + + if (index == -1) + return; + + let clone = this._windows[index]; + this._windows.splice(index, 1); + clone.destroy(); + if (clone.cloneTitle) + clone.cloneTitle.destroy(); + + // Adjust the index property for later windows + for (let j = index; j < this._windows.length; j++) { + this._windows[j].index--; + } + + this._positionWindows(); + }, + + // Add a window from the workspace - this is called to fix up the visual + // display for changes to the window state that have already been made. + // x/y/scale are used to give an initial position for the window (if the + // window was dropped on the workspace, say) - the window will then be + // animated to the final location. + addWindow : function(win, x, y, scale) { + let clone = this._addWindowClone(win); + clone.x = x; + clone.y = y; + clone.scale_x = scale; + clone.scale_y = scale; + + this._positionWindows(); + }, + + // Animate the full-screen to overlay transition. + zoomToOverlay : function() { // Move the workspace into size/position this.actor.set_position(this.fullSizeX, this.fullSizeY); Tweener.addTween(this.actor, @@ -223,28 +294,7 @@ Workspace.prototype = { }); // Likewise for each of the windows in the workspace. - for (let i = 1; i < this._windows.length; i++) { - let window = this._windows[i]; - - let [xCenter, yCenter, fraction] = this._computeWindowPosition(i); - xCenter = xCenter * global.screen_width; - yCenter = yCenter * global.screen_height; - - let size = Math.max(window.width, window.height); - let desiredSize = global.screen_width * fraction; - let scale = Math.min(desiredSize / size, 1.0); - - Tweener.addTween(window, - { x: xCenter - 0.5 * scale * window.width, - y: yCenter - 0.5 * scale * window.height, - scale_x: scale, - scale_y: scale, - workspace_relative: this, - time: Overlay.ANIMATION_TIME, - opacity: WINDOW_OPACITY, - transition: "easeOutQuad" - }); - } + this._positionWindows(true); this._visible = true; }, @@ -272,8 +322,12 @@ Workspace.prototype = { workspace_relative: this, time: Overlay.ANIMATION_TIME, opacity: 255, - transition: "easeOutQuad" + transition: "easeOutQuad", + onComplete: this._onCloneAnimComplete, + onCompleteScope: this, + onCompleteParams: [window] }); + window._animationCount++; } this._visible = false; @@ -344,7 +398,7 @@ Workspace.prototype = { // Tests if @win belongs to this workspaces _isMyWindow : function (win) { - return win.get_workspace() == this._workspaceNum || + return win.get_workspace() == this.workspaceNum || (win.get_meta_window() && win.get_meta_window().is_on_all_workspaces()); }, @@ -358,7 +412,7 @@ Workspace.prototype = { }, // Create a clone of a window to use in the overlay. - _makeClone : function(window, index) { + _makeClone : function(window) { let clone = new Clutter.CloneTexture({ parent_texture: window.get_texture(), reactive: true, x: window.x, @@ -366,7 +420,36 @@ Workspace.prototype = { clone.realWindow = window; clone.origX = window.x; clone.origY = window.y; - clone.index = index; + return clone; + }, + + // Create a clone of a (non-desktop) window and add it to the window list + _addWindowClone : function(win) { + let clone = this._makeClone(win); + clone.connect('button-press-event', + Lang.bind(this, this._onCloneButtonPress)); + clone.connect('button-release-event', + Lang.bind(this, this._onCloneButtonRelease)); + clone.connect('enter-event', + Lang.bind(this, this._onCloneEnter)); + clone.connect('leave-event', + Lang.bind(this, this._onCloneLeave)); + clone.connect('motion-event', + Lang.bind(this, this._onCloneMotion)); + + clone._havePointer = false; + clone._inDrag = false; + clone._buttonDown = false; + clone.index = this._windows.length; + // We track the number of animations we are doing for the clone so we can + // hide the floating title while animating. It seems like it should be + // possible to use Tweener.getTweenCount(clone), but that annoyingly only + // updates after onComplete is called. + clone._animationCount = 0; + + this.actor.add_actor(clone); + this._windows.push(clone); + return clone; }, @@ -412,33 +495,98 @@ Workspace.prototype = { return [xCenter, yCenter, fraction]; }, - - _cloneEnter: function (clone, event) { - if (Tweener.getTweenCount(this.actor)) + + _onCloneEnter: function (clone, event) { + clone._havePointer = true; + + if (this._overlappedMode && clone.index != this._windows.length - 1) + clone.raise_top(); + + this._updateCloneTitle(clone); + }, + + _onCloneLeave: function (clone, event) { + clone._havePointer = false; + + if (this._overlappedMode && clone.index != this._windows.length - 1) + clone.lower(this._windows[clone.index+1]); + + this._updateCloneTitle(clone); + }, + + _onCloneButtonPress : function (clone, event) { + this.actor.raise_top(); + clone.raise_top(); + + let [stageX, stageY] = event.get_coords(); + + clone._buttonDown = true; + clone._dragStartX = stageX; + clone._dragStartY = stageY; + clone._dragStartActorX = clone.x; + clone._dragStartActorY = clone.y; + + Clutter.grab_pointer(clone); + + this._updateCloneTitle(clone); + }, + + _onCloneButtonRelease : function (clone, event) { + Clutter.ungrab_pointer(); + let inDrag = clone._inDrag; + clone._buttonDown = false; + clone._inDrag = false; + + if (inDrag) { + let [stageX, stageY] = event.get_coords(); + + this.emit('window-dragged', clone, stageX, stageY, event.get_time()); + if (clone.realWindow.get_workspace() == this.workspaceNum) { + // Didn't get moved elsewhere, restore position + Tweener.addTween(clone, + { x: clone._dragStartActorX, + y: clone._dragStartActorY, + time: SNAP_BACK_ANIMATION_TIME, + transition: "easeOutQuad", + onComplete: this._onCloneAnimationComplete, + onCompleteScope: this, + onCompleteParams: [clone] + }); + clone._animationCount++; + } + } else { + this._activateWindow(clone.realWindow, event.get_time()); + } + }, + + _onCloneMotion : function (clone, event) { + if (!clone._buttonDown) return; - if (!clone.cloneTitle) - this._createCloneTitle(clone); - this._adjustCloneTitle(clone); - clone.cloneTitle.show(); - if (!this._overlappedMode) - return; - if (clone.index != this._windows.length-1) { - clone.raise_top(); - clone.cloneTitle.raise(clone); - } - }, - - _cloneLeave: function (clone, event) { - if (!clone.cloneTitle) + let [stageX, stageY] = event.get_coords(); + + // If we are already dragging, update the position + if (clone._inDrag) { + clone.x = clone._dragStartActorX + (stageX - clone._dragStartX) / this.scale; + clone.y = clone._dragStartActorY + (stageY - clone._dragStartY) / this.scale; + return; - clone.cloneTitle.hide(); - if (!this._overlappedMode) - return; - if (clone.index != this._windows.length-1) { - clone.lower(this._windows[clone.index+1]); - clone.cloneTitle.raise(clone); - } + } + + // If we haven't begun a drag, see if the user has moved the mouse enough + // to trigger a drag + let dragThreshold = Gtk.Settings.get_default().gtk_dnd_drag_threshold; + if (Math.abs(stageX - clone._dragStartX) > dragThreshold || + Math.abs(stageY - clone._dragStartY) > dragThreshold) { + clone._inDrag = true; + } + }, + + + _onCloneAnimationComplete : function (clone) { + clone._animationCount--; + if (clone._animationCount == 0) + this._updateCloneTitle(clone); }, _createCloneTitle : function (clone) { @@ -489,6 +637,33 @@ Workspace.prototype = { title.set_position(clone.x + xoff, clone.y); }, + _showCloneTitle : function (clone) { + if (!clone.cloneTitle) + this._createCloneTitle(clone); + + this._adjustCloneTitle(clone); + clone.cloneTitle.show(); + clone.cloneTitle.raise(clone); + }, + + _hideCloneTitle : function (clone) { + if (!clone.cloneTitle) + return; + + clone.cloneTitle.hide(); + }, + + _updateCloneTitle : function (clone) { + let shouldShow = (clone._havePointer && + !clone._buttonDown && + clone._animationCount == 0); + + if (shouldShow) + this._showCloneTitle(clone); + else + this._hideCloneTitle(clone); + }, + _activateWindow : function(w, time) { let global = Shell.Global.get(); let activeWorkspace = global.screen.get_active_workspace_index(); @@ -505,12 +680,14 @@ Workspace.prototype = { _removeSelf : function(actor, event) { let global = Shell.Global.get(); let screen = global.screen; - let workspace = screen.get_workspace_by_index(this._workspaceNum); + let workspace = screen.get_workspace_by_index(this.workspaceNum); screen.remove_workspace(workspace, event.get_time()); } }; +Signals.addSignalMethods(Workspace.prototype); + function Workspaces() { this._init(); } @@ -537,12 +714,11 @@ Workspaces.prototype = { // Create and position workspace objects for (let w = 0; w < global.screen.n_workspaces; w++) { - this._workspaces[w] = new Workspace(w); + this._addWorkspaceActor(w); if (w == activeWorkspaceIndex) { activeWorkspace = this._workspaces[w]; activeWorkspace.setSelected(true); } - this.actor.add_actor(this._workspaces[w].actor); } activeWorkspace.actor.raise_top(); this._positionWorkspaces(global, activeWorkspace); @@ -580,7 +756,7 @@ Workspaces.prototype = { reactive: true }); plus.set_from_file(global.imagedir + "add-workspace.svg"); - plus.connect('button-press-event', this._addWorkspace); + plus.connect('button-press-event', this._appendNewWorkspace); this.actor.add_actor(plus); plus.lower_bottom(); @@ -723,8 +899,7 @@ Workspaces.prototype = { if (newNumWorkspaces > oldNumWorkspaces) { // Create new workspace groups for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) { - this._workspaces[w] = new Workspace(w); - this.actor.add_actor(this._workspaces[w].actor); + this._addWorkspaceActor(w); } } else { @@ -772,10 +947,59 @@ Workspaces.prototype = { this._workspaces[to].setSelected(true); }, - _addWorkspace : function(actor, event) { + _addWorkspaceActor : function(workspaceNum) { + let workspace = new Workspace(workspaceNum); + this._workspaces[workspaceNum] = workspace; + workspace.connect('window-dragged', + Lang.bind(this, this._onWindowDragged)); + this.actor.add_actor(workspace.actor); + }, + + _appendNewWorkspace : function(actor, event) { let global = Shell.Global.get(); global.screen.append_new_workspace(false, event.get_time()); + }, + + _onWindowDragged : function(sourceWorkspace, clone, stageX, stageY, time) { + let global = Shell.Global.get(); + + // Positions in stage coordinates + let [myX, myY] = this.actor.get_transformed_position(); + let [windowX, windowY] = clone.get_transformed_position(); + + let targetWorkspace = null; + let targetX, targetY; + + for (let w = 0; w < this._workspaces.length; w++) { + let workspace = this._workspaces[w]; + + // Drag point relative to the new workspace + let relX = stageX - myX - workspace.gridX; + let relY = stageY - myY - workspace.gridY; + + if (relX > 0 && relY > 0 && + relX < global.screen_width * workspace.scale && + relY < global.screen_height * workspace.scale) + { + targetWorkspace = workspace; + break; + } + } + + if (targetWorkspace == null || targetWorkspace == sourceWorkspace) + return; + + // Window position relative to the new workspace + targetX = (windowX - myX - targetWorkspace.gridX) / targetWorkspace.scale; + targetY = (windowY - myY - targetWorkspace.gridY) / targetWorkspace.scale; + + let metaWindow = clone.realWindow.get_meta_window(); + metaWindow.change_workspace_by_index(targetWorkspace.workspaceNum, + false, // don't create workspace + time); + sourceWorkspace.removeWindow(clone.realWindow); + targetWorkspace.addWindow(clone.realWindow, targetX, targetY, clone.scale_x); } };