gnome-shell r164 - trunk/js/ui



Author: otaylor
Date: Fri Jan 23 19:21:20 2009
New Revision: 164
URL: http://svn.gnome.org/viewvc/gnome-shell?rev=164&view=rev

Log:
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

Modified:
   trunk/js/ui/workspaces.js

Modified: trunk/js/ui/workspaces.js
==============================================================================
--- trunk/js/ui/workspaces.js	(original)
+++ trunk/js/ui/workspaces.js	Fri Jan 23 19:21:20 2009
@@ -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 @@
 // 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 @@
         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 @@
         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 @@
                              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 @@
                          });
 
         // 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_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 @@
 
     // 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 @@
     },
 
     // 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 @@
         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 @@
 
         return [xCenter, yCenter, fraction];
     },
-    
-    _cloneEnter: function (clone, event) {
-        if (Tweener.getTweenCount(this.actor))
-            return;
 
-        if (!clone.cloneTitle)
-            this._createCloneTitle(clone);
-        this._adjustCloneTitle(clone);
-    	clone.cloneTitle.show();            
-    	if (!this._overlappedMode)
-    	    return;
-    	if (clone.index != this._windows.length-1) {
+    _onCloneEnter: function (clone, event) {
+        clone._havePointer = true;
+
+        if (this._overlappedMode && clone.index != this._windows.length - 1)
     	    clone.raise_top();
-    	    clone.cloneTitle.raise(clone);
-    	}
+
+        this._updateCloneTitle(clone);
     },
-    
-    _cloneLeave: function (clone, event) {
-        if (!clone.cloneTitle)
-            return;
-        clone.cloneTitle.hide();
-    	if (!this._overlappedMode)
-    	    return;    	
-    	if (clone.index != this._windows.length-1) {
+
+    _onCloneLeave: function (clone, event) {
+        clone._havePointer = false;
+
+        if (this._overlappedMode && clone.index != this._windows.length - 1)
     	    clone.lower(this._windows[clone.index+1]);
-    	    clone.cloneTitle.raise(clone);    	    
-    	}
+
+        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;
+
+        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;
+        }
+
+        // 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 @@
         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 @@
     _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 @@
 
         // 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 @@
                                          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 @@
         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 @@
         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);
     }
 };
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]