Abstract drag-and-drop a bit and add application/recent document DND. #569717

This commit is contained in:
Dan Winship 2009-02-10 11:15:59 -05:00
parent e79c776c2e
commit 11561fd902
3 changed files with 271 additions and 147 deletions

181
js/ui/dnd.js Normal file
View File

@ -0,0 +1,181 @@
/* -*- 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 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);
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._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);
}
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();
}
// No target, so snap back
Tweener.addTween(actor,
{ x: this._dragStartX + this._dragOffsetX,
y: this._dragStartY + this._dragOffsetY,
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);
}

View File

@ -4,11 +4,14 @@ const Big = imports.gi.Big;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Pango = imports.gi.Pango;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const Tidy = imports.gi.Tidy;
const DND = imports.ui.dnd;
const ITEM_DISPLAY_NAME_COLOR = new Clutter.Color();
ITEM_DISPLAY_NAME_COLOR.from_pixel(0xffffffff);
const ITEM_DISPLAY_DESCRIPTION_COLOR = new Clutter.Color();
@ -37,16 +40,18 @@ function GenericDisplayItem(availableWidth) {
GenericDisplayItem.prototype = {
_init: function(availableWidth) {
this._availableWidth = availableWidth;
let me = this;
this.actor = new Clutter.Group({ reactive: true,
width: availableWidth,
height: ITEM_DISPLAY_HEIGHT });
this.actor.connect('button-press-event', function(group, e) {
me.emit('activate');
return true;
});
this.actor._delegate = this;
this.actor.connect('button-release-event',
Lang.bind(this,
function(draggable, e) {
this.activate();
}));
DND.makeDraggable(this.actor);
this._bg = new Big.Box({ background_color: ITEM_DISPLAY_BACKGROUND_COLOR,
corner_radius: 4,
x: 0, y: 0,
@ -58,6 +63,25 @@ GenericDisplayItem.prototype = {
this._icon = null;
},
//// Draggable interface ////
getDragActor: function(stageX, stageY) {
// FIXME: assumes this._icon is a Clutter.Texture
let icon = new Clutter.CloneTexture({ parent_texture: this._icon });
[icon.width, icon.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
// around the pointer
let [iconX, iconY] = this._icon.get_transformed_position();
let [iconWidth, iconHeight] = this._icon.get_transformed_size();
if (stageX > iconX && stageX <= iconX + iconWidth &&
stageY > iconY && stageY <= iconY + iconHeight)
icon.set_position(iconX, iconY);
else
icon.set_position(stageX - icon.width / 2, stageY - icon.height / 2);
return icon;
},
//// Public methods ////
// Highlights the item by setting a different background color than the default
@ -71,6 +95,11 @@ GenericDisplayItem.prototype = {
this._bg.background_color = color;
},
// Activates the item, as though it was clicked
activate: function() {
this.emit('activate');
},
//// Pure virtual public methods ////
// Performes an action associated with launching this item, such as opening a file or an application.

View File

@ -11,6 +11,8 @@ const Pango = imports.gi.Pango;
const Shell = imports.gi.Shell;
const Signals = imports.signals;
const DND = imports.ui.dnd;
const GenericDisplay = imports.ui.genericDisplay;
const Main = imports.ui.main;
const Overlay = imports.ui.overlay;
const Panel = imports.ui.panel;
@ -19,7 +21,6 @@ const Tweener = imports.ui.tweener;
// 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);
@ -64,20 +65,19 @@ WindowClone.prototype = {
this.origX = realWindow.x;
this.origY = realWindow.y;
this.actor.connect('button-press-event',
Lang.bind(this, this._onButtonPress));
this.actor.connect('button-release-event',
Lang.bind(this, this._onButtonRelease));
this.actor.connect('enter-event',
Lang.bind(this, this._onEnter));
this.actor.connect('leave-event',
Lang.bind(this, this._onLeave));
this.actor.connect('motion-event',
Lang.bind(this, this._onMotion));
this._havePointer = false;
this._draggable = DND.makeDraggable(this.actor);
this._draggable.connect('drag-begin', Lang.bind(this, this._onDragBegin));
this._draggable.connect('drag-end', Lang.bind(this, this._onDragEnd));
this._inDrag = false;
this._buttonDown = false;
},
destroy: function () {
@ -85,7 +85,7 @@ WindowClone.prototype = {
if (this._title)
this._title.destroy();
},
_onEnter: function (actor, event) {
// If the user drags faster than we can follow, he'll end up
// leaving the window temporarily and then re-entering it
@ -113,100 +113,25 @@ WindowClone.prototype = {
this._updateTitle();
},
_onButtonPress : function (actor, event) {
if (Tweener.isTweening(this.actor))
return;
actor.raise_top();
let [stageX, stageY] = event.get_coords();
this._buttonDown = true;
this._dragStartX = stageX;
this._dragStartY = stageY;
Clutter.grab_pointer(actor);
_onButtonRelease : function (actor, event) {
this.emit('selected', event.get_time());
},
_onDragBegin : function (draggable, time) {
this._inDrag = true;
this._updateTitle();
},
_onMotion : function (actor, event) {
if (!this._buttonDown)
return;
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 dragThreshold = Gtk.Settings.get_default().gtk_dnd_drag_threshold;
if (!this._inDrag &&
(Math.abs(stageX - this._dragStartX) > dragThreshold ||
Math.abs(stageY - this._dragStartY) > dragThreshold)) {
this._inDrag = true;
this._dragOrigParent = actor.get_parent();
this._dragOrigX = actor.x;
this._dragOrigY = actor.y;
this._dragOrigScale = actor.scale_x;
let [cloneStageX, cloneStageY] = actor.get_transformed_position();
this._dragOffsetX = cloneStageX - this._dragStartX;
this._dragOffsetY = cloneStageY - this._dragStartY;
// Reparent the clone onto the stage, but keeping the same scale.
// (the set_position call below will take care of position.)
let [scaledWidth, scaledHeight] = actor.get_transformed_size();
actor.reparent(actor.get_stage());
actor.raise_top();
actor.set_scale(scaledWidth / actor.width,
scaledHeight / actor.height);
}
// If we are dragging, update the position
if (this._inDrag) {
actor.set_position(stageX + this._dragOffsetX,
stageY + this._dragOffsetY);
}
},
_onButtonRelease : function (actor, event) {
Clutter.ungrab_pointer();
let inDrag = this._inDrag;
this._buttonDown = false;
_onDragEnd : function (draggable, time) {
this._inDrag = false;
if (inDrag) {
let [stageX, stageY] = event.get_coords();
let origWorkspace = this.realWindow.get_workspace();
this.emit('dragged', stageX, stageY, event.get_time());
if (this.realWindow.get_workspace() == origWorkspace) {
// Didn't get moved elsewhere, restore position
Tweener.addTween(this.actor,
{ x: this._dragStartX + this._dragOffsetX,
y: this._dragStartY + this._dragOffsetY,
time: SNAP_BACK_ANIMATION_TIME,
transition: "easeOutQuad",
onComplete: this._onSnapBackComplete,
onCompleteScope: this
});
// Most likely, the clone is going to move away from the
// pointer now. But that won't cause a leave-event, so
// do this by hand. Of course, if the window only snaps
// back a short distance, this might be wrong, but it's
// better to have the label mysteriously missing than
// mysteriously present
this._havePointer = false;
}
} else
this.emit('selected', event.get_time());
},
_onSnapBackComplete : function () {
this.actor.reparent(this._dragOrigParent);
this.actor.set_scale(this._dragOrigScale, this._dragOrigScale);
this.actor.set_position(this._dragOrigX, this._dragOrigY);
// Most likely, the clone is going to move away from the
// pointer now. But that won't cause a leave-event, so
// do this by hand. Of course, if the window only snaps
// back a short distance, this might be wrong, but it's
// better to have the label mysteriously missing than
// mysteriously present
this._havePointer = false;
},
// Called by Tweener
@ -290,7 +215,7 @@ WindowClone.prototype = {
_updateTitle : function () {
let shouldShow = (this._havePointer &&
!this._buttonDown &&
!this._inDrag &&
!Tweener.isTweening(this.actor));
if (shouldShow)
@ -345,6 +270,7 @@ Workspace.prototype = {
this._metaWorkspace = global.screen.get_workspace_by_index(workspaceNum);
this.actor = new Clutter.Group();
this.actor._delegate = this;
this.scale = 1.0;
let windows = global.get_windows().filter(this._isMyWindow, this);
@ -391,7 +317,6 @@ Workspace.prototype = {
this.leavingOverlay = false;
},
updateRemovable : function() {
let global = Shell.Global.get();
let removable = (this._windows.length == 1 /* just desktop */ &&
@ -535,6 +460,7 @@ Workspace.prototype = {
if (index == -1)
return;
this._windows.splice(index, 1);
// If metaWin.get_compositor_private() returned non-NULL, that
@ -714,6 +640,7 @@ Workspace.prototype = {
destroy : function() {
let global = Shell.Global.get();
Tweener.removeTweens(this.actor);
this.actor.destroy();
this.actor = null;
@ -797,6 +724,37 @@ Workspace.prototype = {
screen.remove_workspace(workspace, event.get_time());
return true;
},
// Draggable target interface
acceptDrop : function(source, actor, x, y, time) {
let global = Shell.Global.get();
if (source instanceof WindowClone) {
let win = source.realWindow;
if (this._isMyWindow(win))
return false;
// Set a hint on the Mutter.Window so its initial position
// in the new workspace will be correct
win._overlayHint = {
x: actor.x,
y: actor.y,
scale: actor.scale_x
};
let metaWindow = win.get_meta_window();
metaWindow.change_workspace_by_index(this.workspaceNum,
false, // don't create workspace
time);
return true;
} else if (source instanceof GenericDisplay.GenericDisplayItem) {
this._metaWorkspace.activate(time);
source.activate();
return true;
}
return false;
}
};
@ -1119,8 +1077,6 @@ Workspaces.prototype = {
_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);
},
@ -1128,48 +1084,6 @@ Workspaces.prototype = {
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.actor.get_transformed_position();
let targetWorkspace = null;
let targetX, targetY, targetScale;
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;
// Set a hint on the Mutter.Window so its initial position in the
// overlay will be correct
clone.realWindow._overlayHint = {
x: clone.actor.x,
y: clone.actor.y,
scale: clone.actor.scale_x
};
clone.metaWindow.change_workspace_by_index(targetWorkspace.workspaceNum,
false, // don't create workspace
time);
}
};