39eb563687
Sliding the workspaces back in when the user starts dragging an item in the expanded display mode allows the user to select a workspace in which the item should be launched and stay in the overlay mode. This patch adds code to dnd.js that handles notifying actors when a drag item is being dragged over them. Overlay code uses such notification to unset expanded display modes and trigger sliding in of the workspaces. If the drag is cancelled the drag item snaps back to its source or disappears at the original position of its source if the source is no longer displayed.
216 lines
8.7 KiB
JavaScript
216 lines
8.7 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil -*- */
|
|
|
|
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;
|
|
|
|
function _Draggable(actor) {
|
|
this._init(actor);
|
|
}
|
|
|
|
_Draggable.prototype = {
|
|
_init : function(actor) {
|
|
this.actor = actor;
|
|
this.actor.connect('button-press-event',
|
|
Lang.bind(this, this._onButtonPress));
|
|
},
|
|
|
|
_onButtonPress : function (actor, event) {
|
|
// FIXME: we should make sure it's button 1, but we can't currently
|
|
// check that from JavaScript
|
|
if (Tweener.getTweenCount(actor))
|
|
return false;
|
|
|
|
this._grabActor(actor);
|
|
|
|
let [stageX, stageY] = event.get_coords();
|
|
this._dragStartX = stageX;
|
|
this._dragStartY = stageY;
|
|
|
|
return false;
|
|
},
|
|
|
|
_grabActor : function (actor) {
|
|
Clutter.grab_pointer(actor);
|
|
|
|
// We intercept motion and button-release events so that when
|
|
// you release after dragging, the app doesn't see that and
|
|
// think you just clicked. We connect to 'event' rather than
|
|
// 'captured-event' because the capturing phase doesn't happen
|
|
// when you've grabbed the pointer.
|
|
this._onEventId = actor.connect('event',
|
|
Lang.bind(this, this._onEvent));
|
|
},
|
|
|
|
_ungrabActor : function (actor) {
|
|
Clutter.ungrab_pointer();
|
|
actor.disconnect(this._onEventId);
|
|
},
|
|
|
|
_onEvent : function (actor, event) {
|
|
if (this._dragActor) {
|
|
if (actor != this._dragActor )
|
|
return false;
|
|
} else if (actor != this.actor)
|
|
return false;
|
|
|
|
if (event.type() == Clutter.EventType.BUTTON_RELEASE)
|
|
return this._onButtonRelease(actor, event);
|
|
else if (event.type() == Clutter.EventType.MOTION)
|
|
return this._onMotion(actor, event);
|
|
else
|
|
return false;
|
|
},
|
|
|
|
_onMotion : function (actor, event) {
|
|
let [stageX, stageY] = event.get_coords();
|
|
|
|
// If we haven't begun a drag, see if the user has moved the
|
|
// mouse enough to trigger a drag
|
|
let threshold = Gtk.Settings.get_default().gtk_dnd_drag_threshold;
|
|
if (!this._dragActor &&
|
|
(Math.abs(stageX - this._dragStartX) > threshold ||
|
|
Math.abs(stageY - this._dragStartY) > threshold)) {
|
|
this.emit('drag-begin', event.get_time());
|
|
|
|
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);
|
|
|
|
this._dragOffsetX = this._dragActor.x - this._dragStartX;
|
|
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;
|
|
this._dragOrigScale = this._dragActor.scale_x;
|
|
|
|
let [actorStageX, actorStageY] = actor.get_transformed_position();
|
|
this._dragOffsetX = actorStageX - this._dragStartX;
|
|
this._dragOffsetY = actorStageY - this._dragStartY;
|
|
|
|
// Set the actor's scale such that it will keep the same
|
|
// transformed size when it's reparented to the stage
|
|
let [scaledWidth, scaledHeight] = actor.get_transformed_size();
|
|
actor.set_scale(scaledWidth / actor.width,
|
|
scaledHeight / actor.height);
|
|
}
|
|
|
|
this._dragActor.reparent(actor.get_stage());
|
|
this._dragActor.raise_top();
|
|
}
|
|
|
|
// If we are dragging, update the position
|
|
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;
|
|
},
|
|
|
|
_onButtonRelease : function (actor, event) {
|
|
this._ungrabActor(actor);
|
|
|
|
let dragging = (actor == this._dragActor);
|
|
this._dragActor = undefined;
|
|
|
|
if (!dragging)
|
|
return false;
|
|
|
|
this.emit('drag-end', event.get_time());
|
|
|
|
// Find a drop target
|
|
actor.hide();
|
|
let [dropX, dropY] = event.get_coords();
|
|
let target = actor.get_stage().get_actor_at_pos(dropX, dropY);
|
|
actor.show();
|
|
while (target) {
|
|
if (target._delegate && target._delegate.acceptDrop) {
|
|
let [targX, targY] = target.get_transformed_position();
|
|
if (target._delegate.acceptDrop(this.actor._delegate, actor,
|
|
(dropX + this._xOffset - targX) / target.scale_x,
|
|
(dropY + this._yOffset - targY) / target.scale_y,
|
|
event.get_time())) {
|
|
// If it accepted the drop without taking the actor,
|
|
// destroy it.
|
|
if (actor.get_parent() == actor.get_stage())
|
|
actor.destroy();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
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: snapBackX,
|
|
y: snapBackY,
|
|
time: SNAP_BACK_ANIMATION_TIME,
|
|
transition: "easeOutQuad",
|
|
onComplete: this._onSnapBackComplete,
|
|
onCompleteScope: this,
|
|
onCompleteParams: [actor]
|
|
});
|
|
return true;
|
|
},
|
|
|
|
_onSnapBackComplete : function (dragActor) {
|
|
if (this._dragOrigParent) {
|
|
dragActor.reparent(this._dragOrigParent);
|
|
dragActor.set_scale(this._dragOrigScale, this._dragOrigScale);
|
|
dragActor.set_position(this._dragOrigX, this._dragOrigY);
|
|
} else
|
|
dragActor.destroy();
|
|
}
|
|
};
|
|
|
|
Signals.addSignalMethods(_Draggable.prototype);
|
|
|
|
function makeDraggable(actor) {
|
|
return new _Draggable(actor);
|
|
}
|