Abstract drag-and-drop a bit and add application/recent document DND. #569717
This commit is contained in:
parent
e79c776c2e
commit
11561fd902
181
js/ui/dnd.js
Normal file
181
js/ui/dnd.js
Normal 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);
|
||||
}
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user