diff --git a/js/ui/dnd.js b/js/ui/dnd.js index d72edf7da..940433940 100644 --- a/js/ui/dnd.js +++ b/js/ui/dnd.js @@ -1,10 +1,9 @@ /* -*- mode: js2; js2-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- */ -const Lang = imports.lang; -const Signals = imports.signals; - const Clutter = imports.gi.Clutter; const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Signals = imports.signals; const Tweener = imports.ui.tweener; const SNAP_BACK_ANIMATION_TIME = 0.25; @@ -80,6 +79,13 @@ _Draggable.prototype = { if (this.actor._delegate && this.actor._delegate.getDragActor) { this._dragActor = this.actor._delegate.getDragActor(this._dragStartX, this._dragStartY); + // Drag actor does not always have to be the same as actor. For example drag actor + // can be an image that's part of the actor. So to perform "snap back" correctly we need + // to know what was the drag actor source. + if (this.actor._delegate.getDragActorSource) + this._dragActorSource = this.actor._delegate.getDragActorSource(); + else + this._dragActorSource = this.actor; this._dragOrigParent = undefined; this._ungrabActor(actor); this._grabActor(this._dragActor); @@ -88,6 +94,7 @@ _Draggable.prototype = { this._dragOffsetY = this._dragActor.y - this._dragStartY; } else { this._dragActor = actor; + this._dragActorSource = undefined; this._dragOrigParent = actor.get_parent(); this._dragOrigX = this._dragActor.x; this._dragOrigY = this._dragActor.y; @@ -112,6 +119,24 @@ _Draggable.prototype = { if (this._dragActor) { this._dragActor.set_position(stageX + this._dragOffsetX, stageY + this._dragOffsetY); + // Because we want to find out what other actor is located at the current position of this._dragActor, + // we have to temporarily hide this._dragActor. + this._dragActor.hide(); + let target = actor.get_stage().get_actor_at_pos(stageX + this._dragOffsetX, stageY + this._dragOffsetY); + this._dragActor.show(); + while (target) { + if (target._delegate && target._delegate.handleDragOver) { + let [targX, targY] = target.get_transformed_position(); + // We currently loop through all parents on drag-over even if one of the children has handled it. + // We can check the return value of the function and break the loop if it's true if we don't want + // to continue checking the parents. + target._delegate.handleDragOver(this.actor._delegate, actor, + (stageX + this._dragOffsetX + this._xOffset - targX) / target.scale_x, + (stageY + this._dragOffsetY + this._yOffset - targY) / target.scale_y, + event.get_time()); + } + target = target.get_parent(); + } } return true; @@ -151,10 +176,19 @@ _Draggable.prototype = { target = target.get_parent(); } + // Snap back to the actor source if the source is still around, snap back + // to the original location if the actor itself was being dragged or the + // source is no longer around. + let snapBackX = this._dragStartX + this._dragOffsetX; + let snapBackY = this._dragStartY + this._dragOffsetY; + if (this._dragActorSource && this._dragActorSource.visible) { + [snapBackX, snapBackY] = this._dragActorSource.get_transformed_position(); + } + // No target, so snap back Tweener.addTween(actor, - { x: this._dragStartX + this._dragOffsetX, - y: this._dragStartY + this._dragOffsetY, + { x: snapBackX, + y: snapBackY, time: SNAP_BACK_ANIMATION_TIME, transition: "easeOutQuad", onComplete: this._onSnapBackComplete, @@ -178,4 +212,4 @@ Signals.addSignalMethods(_Draggable.prototype); function makeDraggable(actor) { return new _Draggable(actor); -} \ No newline at end of file +} diff --git a/js/ui/genericDisplay.js b/js/ui/genericDisplay.js index ba8222cd2..93ead6012 100644 --- a/js/ui/genericDisplay.js +++ b/js/ui/genericDisplay.js @@ -64,13 +64,17 @@ GenericDisplayItem.prototype = { this._name = null; this._description = null; this._icon = null; + + this.dragActor = null; }, - //// Draggable interface //// + //// Draggable object interface //// + + // Returns a cloned texture of the item's icon to represent the item as it + // is being dragged. getDragActor: function(stageX, stageY) { - // FIXME: assumes this._icon is a Clutter.Texture - let icon = new Clutter.Clone({ source: this._icon }); - [icon.width, icon.height] = this._icon.get_transformed_size(); + this.dragActor = new Clutter.Clone({ source: this._icon }); + [this.dragActor.width, this.dragActor.height] = this._icon.get_transformed_size(); // If the user dragged from the icon itself, then position // the dragActor over the original icon. Otherwise center it @@ -79,12 +83,18 @@ GenericDisplayItem.prototype = { let [iconWidth, iconHeight] = this._icon.get_transformed_size(); if (stageX > iconX && stageX <= iconX + iconWidth && stageY > iconY && stageY <= iconY + iconHeight) - icon.set_position(iconX, iconY); + this.dragActor.set_position(iconX, iconY); else - icon.set_position(stageX - icon.width / 2, stageY - icon.height / 2); - return icon; + this.dragActor.set_position(stageX - this.dragActor.width / 2, stageY - this.dragActor.height / 2); + return this.dragActor; }, + // Returns the original icon that is being used as a source for the cloned texture + // that represents the item as it is being dragged. + getDragActorSource: function() { + return this._icon; + }, + //// Public methods //// // Highlights the item by setting a different background color than the default @@ -388,7 +398,32 @@ GenericDisplay.prototype = { this.selectUp(); } - displayItem.actor.destroy(); + if (displayItem.dragActor) { + // The user might be handling a dragActor when the list of items + // changes (for example, if the dragging caused us to transition + // from an expanded overlay view to the regular view). So we need + // to keep the item around so that the drag and drop action initiated + // by the user can be completed. However, we remove the item from the list. + // + // For some reason, just removing the displayItem.actor + // is not enough to get displayItem._icon.visible + // to return false, so we hide the display item and + // all its children first. (We check displayItem._icon.visible + // when deciding if a dragActor has a place to snap back to + // in case the drop was not accepted by any actor.) + displayItem.actor.hide_all(); + this._grid.remove_actor(displayItem.actor); + // We should not destroy the actor up-front, because that would also + // destroy the icon that was used to clone the image for the drag actor. + // We destroy it once the dragActor is destroyed instead. + displayItem.dragActor.connect('destroy', + function(item) { + displayItem.actor.destroy(); + }); + + } else { + displayItem.actor.destroy(); + } delete this._displayedItems[itemId]; this._displayedItemsCount--; }, diff --git a/js/ui/overlay.js b/js/ui/overlay.js index 3d2624f26..cfb1b9458 100644 --- a/js/ui/overlay.js +++ b/js/ui/overlay.js @@ -568,6 +568,8 @@ Overlay.prototype = { } this._group = new Clutter.Group(); + this._group._delegate = this; + this.visible = false; let background = new Clutter.Rectangle({ color: OVERLAY_BACKGROUND_COLOR, @@ -604,7 +606,7 @@ Overlay.prototype = { { x: displayGridColumnWidth * asideXFactor, time: ANIMATION_TIME, transition: "easeOutQuad" - }); + }); } }); this._sideshow.connect('less-activated', function(sideshow) { @@ -621,6 +623,24 @@ Overlay.prototype = { }); }, + //// Draggable target interface //// + + // Unsets the expanded display mode if a GenericDisplayItem is being + // dragged over the overlay, i.e. as soon as it starts being dragged. + // This slides the workspaces back in and allows the user to place + // the item on any workspace. + handleDragOver : function(source, actor, x, y, time) { + if (source instanceof GenericDisplay.GenericDisplayItem) { + this._sideshow._unsetMoreAppsMode(); + this._sideshow._unsetMoreDocsMode(); + return true; + } + + return false; + }, + + //// Public methods //// + show : function() { if (this.visible) return; @@ -690,6 +710,8 @@ Overlay.prototype = { }); }, + //// Private methods //// + _hideDone: function() { let global = Shell.Global.get();