gnome-shell/js/ui/dnd.js
Colin Walters 7d8c3f1ecd Ensure our drag actor is positioned over the cursor
Previously we were looking at the source size, which is
not necessarily the same as the drag actor size.

https://bugzilla.gnome.org/show_bug.cgi?id=607821
2010-03-16 10:59:42 -04:00

334 lines
13 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;
let eventHandlerActor = null;
let currentDraggable = null;
function _getEventHandlerActor() {
if (!eventHandlerActor) {
eventHandlerActor = new Clutter.Rectangle();
eventHandlerActor.width = 0;
eventHandlerActor.height = 0;
global.stage.add_actor(eventHandlerActor);
// We connect to 'event' rather than 'captured-event' because the capturing phase doesn't happen
// when you've grabbed the pointer.
eventHandlerActor.connect('event',
function(actor, event) {
return currentDraggable._onEvent(actor, event);
});
}
return eventHandlerActor;
}
function _Draggable(actor, manualMode) {
this._init(actor, manualMode);
}
_Draggable.prototype = {
_init : function(actor, manualMode) {
this.actor = actor;
if (!manualMode)
this.actor.connect('button-press-event',
Lang.bind(this, this._onButtonPress));
this._onEventId = null;
this._buttonDown = false; // The mouse button has been pressed and has not yet been released.
this._dragInProgress = false; // The drag has been started, and has not been dropped or cancelled yet.
this._snapBackInProgress = false; // The drag has been cancelled and the item is in the process of snapping back.
},
_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._buttonDown = true;
this._grabActor();
let [stageX, stageY] = event.get_coords();
this._dragStartX = stageX;
this._dragStartY = stageY;
return false;
},
_grabActor: function() {
Clutter.grab_pointer(this.actor);
this._onEventId = this.actor.connect('event',
Lang.bind(this, this._onEvent));
},
_ungrabActor: function() {
Clutter.ungrab_pointer();
this.actor.disconnect(this._onEventId);
this._onEventId = null;
},
_grabEvents: function() {
Clutter.grab_pointer(_getEventHandlerActor());
Clutter.grab_keyboard(_getEventHandlerActor());
},
_ungrabEvents: function() {
Clutter.ungrab_pointer();
Clutter.ungrab_keyboard();
},
_onEvent: function(actor, event) {
// We intercept BUTTON_RELEASE event to know that the button was released in case we
// didn't start the drag, to drop the draggable in case the drag was in progress, and
// to complete the drag and ensure that whatever happens to be under the pointer does
// not get triggered if the drag was cancelled with Esc.
if (event.type() == Clutter.EventType.BUTTON_RELEASE) {
this._buttonDown = false;
if (this._dragInProgress) {
return this._dragActorDropped(event);
} else if (this._dragActor != null && !this._snapBackInProgress) {
// Drag must have been cancelled with Esc.
this._dragComplete();
return true;
} else {
// Drag has never started.
this._ungrabActor();
return false;
}
// We intercept MOTION event to figure out if the drag has started and to draw
// this._dragActor under the pointer when dragging is in progress
} else if (event.type() == Clutter.EventType.MOTION) {
if (this._dragInProgress) {
return this._updateDragPosition(event);
} else if (this._dragActor == null) {
return this._maybeStartDrag(event);
}
// We intercept KEY_PRESS event so that we can process Esc key press to cancel
// dragging and ignore all other key presses.
} else if (event.type() == Clutter.EventType.KEY_PRESS && this._dragInProgress) {
let symbol = event.get_key_symbol();
if (symbol == Clutter.Escape) {
this._cancelDrag(event.get_time());
return true;
}
}
return false;
},
/**
* startDrag:
* @stageX: X coordinate of event
* @stageY: Y coordinate of event
* @time: Event timestamp
*
* Directly initiate a drag and drop operation from the given actor.
* This function is useful to call if you've specified manualMode
* for the draggable.
*/
startDrag: function (stageX, stageY, time) {
currentDraggable = this;
this._dragInProgress = true;
this.emit('drag-begin', time);
if (this._onEventId)
this._ungrabActor();
this._grabEvents();
this._dragStartX = stageX;
this._dragStartY = stageY;
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();
// If the user dragged from the source, then position
// the dragActor over it. Otherwise, center it
// around the pointer
let [sourceX, sourceY] = this._dragActorSource.get_transformed_position();
let x, y;
if (stageX > sourceX && stageX <= sourceX + this._dragActor.width &&
stageY > sourceY && stageY <= sourceY + this._dragActor.height) {
x = sourceX;
y = sourceY;
} else {
x = stageX - this._dragActor.width / 2;
y = stageY - this._dragActor.height / 2;
}
this._dragActor.set_position(x, y);
} else {
this._dragActorSource = this.actor;
}
this._dragOrigParent = undefined;
this._dragOffsetX = this._dragActor.x - this._dragStartX;
this._dragOffsetY = this._dragActor.y - this._dragStartY;
} else {
this._dragActor = this.actor;
this._dragActorSource = undefined;
this._dragOrigParent = this.actor.get_parent();
this._dragOrigX = this._dragActor.x;
this._dragOrigY = this._dragActor.y;
this._dragOrigScale = this._dragActor.scale_x;
let [actorStageX, actorStageY] = this.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] = this.actor.get_transformed_size();
this.actor.set_scale(scaledWidth / this.actor.width,
scaledHeight / this.actor.height);
}
this._dragActor.reparent(this.actor.get_stage());
this._dragActor.raise_top();
},
_maybeStartDrag: function(event) {
let [stageX, stageY] = event.get_coords();
// See if the user has moved the mouse enough to trigger a drag
let threshold = Gtk.Settings.get_default().gtk_dnd_drag_threshold;
if ((Math.abs(stageX - this._dragStartX) > threshold ||
Math.abs(stageY - this._dragStartY) > threshold)) {
this.startDrag(stageX, stageY, event.get_time());
this._updateDragPosition(event);
}
return true;
},
_updateDragPosition : function (event) {
let [stageX, stageY] = event.get_coords();
// 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 = this._dragActor.get_stage().get_actor_at_pos(Clutter.PickMode.ALL,
stageX, stageY);
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, this._dragActor,
(stageX - targX) / target.scale_x,
(stageY - targY) / target.scale_y,
event.get_time());
}
target = target.get_parent();
}
}
return true;
},
_dragActorDropped: function(event) {
// Find a drop target. 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 [dropX, dropY] = event.get_coords();
let target = this._dragActor.get_stage().get_actor_at_pos(Clutter.PickMode.ALL,
dropX, dropY);
this._dragActor.show();
while (target) {
if (target._delegate && target._delegate.acceptDrop) {
let [targX, targY] = target.get_transformed_position();
if (target._delegate.acceptDrop(this.actor._delegate, this._dragActor,
(dropX - targX) / target.scale_x,
(dropY - targY) / target.scale_y,
event.get_time())) {
// If it accepted the drop without taking the actor,
// destroy it.
if (this._dragActor.get_parent() == this._dragActor.get_stage())
this._dragActor.destroy();
this._dragInProgress = false;
this.emit('drag-end', event.get_time(), true);
this._dragComplete();
return true;
}
}
target = target.get_parent();
}
this._cancelDrag(event.get_time());
return true;
},
_cancelDrag: function(eventTime) {
this._dragInProgress = false;
// 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();
}
this._snapBackInProgress = true;
// No target, so snap back
Tweener.addTween(this._dragActor,
{ x: snapBackX,
y: snapBackY,
time: SNAP_BACK_ANIMATION_TIME,
transition: "easeOutQuad",
onComplete: this._onSnapBackComplete,
onCompleteScope: this,
onCompleteParams: [this._dragActor, eventTime]
});
},
_onSnapBackComplete : function (dragActor, eventTime) {
if (this._dragOrigParent) {
dragActor.reparent(this._dragOrigParent);
dragActor.set_scale(this._dragOrigScale, this._dragOrigScale);
dragActor.set_position(this._dragOrigX, this._dragOrigY);
} else {
dragActor.destroy();
}
this.emit('drag-end', eventTime, false);
this._snapBackInProgress = false;
if (!this._buttonDown)
this._dragComplete();
},
_dragComplete: function() {
this._dragActor = undefined;
currentDraggable = null;
this._ungrabEvents();
}
};
Signals.addSignalMethods(_Draggable.prototype);
/**
* makeDraggable:
* @actor: Source actor
* @manualMode: If given, do not automatically start drag and drop on click
*
* Create an object which controls drag and drop for the given actor.
*/
function makeDraggable(actor, manualMode) {
return new _Draggable(actor, manualMode);
}