Implement cross overview drag & drop
The gnome-panel allows the user to hover over a tasklist entry while draging to activate a minimized or obscured window and drop onto it. Implement a similar behaviour by allowing draging to the activities button or the hotcorner (and thus opening the overview), which allows the user to activate any window (even on different workspaces) as a drop target. https://bugzilla.gnome.org/show_bug.cgi?id=601731
This commit is contained in:
@ -56,4 +56,5 @@ nobase_dist_js_DATA = \
|
||||
ui/windowManager.js \
|
||||
ui/workspace.js \
|
||||
ui/workspacesView.js \
|
||||
ui/workspaceSwitcherPopup.js
|
||||
ui/workspaceSwitcherPopup.js \
|
||||
ui/xdndHandler.js
|
||||
|
@ -37,6 +37,7 @@ const ShellDBus = imports.ui.shellDBus;
|
||||
const TelepathyClient = imports.ui.telepathyClient;
|
||||
const WindowManager = imports.ui.windowManager;
|
||||
const Magnifier = imports.ui.magnifier;
|
||||
const XdndHandler = imports.ui.xdndHandler;
|
||||
const StatusIconDispatcher = imports.ui.statusIconDispatcher;
|
||||
|
||||
const DEFAULT_BACKGROUND_COLOR = new Clutter.Color();
|
||||
@ -60,6 +61,7 @@ let modalCount = 0;
|
||||
let modalActorFocusStack = [];
|
||||
let uiGroup = null;
|
||||
let magnifier = null;
|
||||
let xdndHandler = null;
|
||||
let statusIconDispatcher = null;
|
||||
let _errorLogStack = [];
|
||||
let _startDate;
|
||||
@ -129,6 +131,7 @@ function start() {
|
||||
global.stage.add_actor(uiGroup);
|
||||
|
||||
placesManager = new PlaceDisplay.PlacesManager();
|
||||
xdndHandler = new XdndHandler.XdndHandler();
|
||||
overview = new Overview.Overview();
|
||||
chrome = new Chrome.Chrome();
|
||||
magnifier = new Magnifier.Magnifier();
|
||||
|
@ -9,9 +9,11 @@ const St = imports.gi.St;
|
||||
const Shell = imports.gi.Shell;
|
||||
const Gettext = imports.gettext.domain('gnome-shell');
|
||||
const _ = Gettext.gettext;
|
||||
const Gdk = imports.gi.Gdk;
|
||||
|
||||
const AppDisplay = imports.ui.appDisplay;
|
||||
const Dash = imports.ui.dash;
|
||||
const DND = imports.ui.dnd;
|
||||
const DocDisplay = imports.ui.docDisplay;
|
||||
const GenericDisplay = imports.ui.genericDisplay;
|
||||
const Lightbox = imports.ui.lightbox;
|
||||
@ -30,6 +32,8 @@ const ANIMATION_TIME = 0.25;
|
||||
const DASH_SPLIT_FRACTION = 0.1;
|
||||
|
||||
|
||||
const DND_WINDOW_SWITCH_TIMEOUT = 1250;
|
||||
|
||||
function Source() {
|
||||
this._init();
|
||||
}
|
||||
@ -178,9 +182,72 @@ Overview.prototype = {
|
||||
|
||||
this._coverPane.lower_bottom();
|
||||
|
||||
// XDND
|
||||
this._dragMonitor = {
|
||||
dragMotion: Lang.bind(this, this._onDragMotion)
|
||||
};
|
||||
|
||||
Main.xdndHandler.connect('drag-begin', Lang.bind(this, this._onDragBegin));
|
||||
Main.xdndHandler.connect('drag-end', Lang.bind(this, this._onDragEnd));
|
||||
|
||||
this._windowSwitchTimeoutId = 0;
|
||||
this._windowSwitchTimestamp = 0;
|
||||
this._lastActiveWorkspaceIndex = -1;
|
||||
this._needsFakePointerEvent = false;
|
||||
|
||||
this.workspaces = null;
|
||||
},
|
||||
|
||||
_onDragBegin: function() {
|
||||
DND.addDragMonitor(this._dragMonitor);
|
||||
// Remember the workspace we started from
|
||||
this._lastActiveWorkspaceIndex = global.screen.get_active_workspace_index();
|
||||
},
|
||||
|
||||
_onDragEnd: function(time) {
|
||||
// In case the drag was canceled while in the overview
|
||||
// we have to go back to where we started and hide
|
||||
// the overview
|
||||
if (this._shownTemporarily) {
|
||||
global.screen.get_workspace_by_index(this._lastActiveWorkspaceIndex).activate(time);
|
||||
this.hideTemporarily();
|
||||
}
|
||||
|
||||
DND.removeMonitor(this._dragMonitor);
|
||||
},
|
||||
|
||||
_fakePointerEvent: function() {
|
||||
let display = Gdk.Display.get_default();
|
||||
let deviceManager = display.get_device_manager();
|
||||
let pointer = deviceManager.get_client_pointer();
|
||||
let [screen, pointerX, pointerY] = display.get_device_state(pointer);
|
||||
|
||||
display.warp_device(pointer, screen, pointerX, pointerY);
|
||||
},
|
||||
|
||||
_onDragMotion: function(dragEvent) {
|
||||
if (this._windowSwitchTimeoutId != 0) {
|
||||
Mainloop.source_remove(this._windowSwitchTimeoutId);
|
||||
this._windowSwitchTimeoutId = 0;
|
||||
this._needsFakePointerEvent = false;
|
||||
}
|
||||
|
||||
if (dragEvent.targetActor &&
|
||||
dragEvent.targetActor._delegate &&
|
||||
dragEvent.targetActor._delegate.metaWindow) {
|
||||
this._windowSwitchTimestamp = global.get_current_time();
|
||||
this._windowSwitchTimeoutId = Mainloop.timeout_add(DND_WINDOW_SWITCH_TIMEOUT,
|
||||
Lang.bind(this, function() {
|
||||
this._needsFakePointerEvent = true;
|
||||
Main.activateWindow(dragEvent.targetActor._delegate.metaWindow,
|
||||
this._windowSwitchTimestamp);
|
||||
this.hideTemporarily();
|
||||
}));
|
||||
}
|
||||
|
||||
return DND.DragMotionResult.CONTINUE;
|
||||
},
|
||||
|
||||
_getDesktopClone: function() {
|
||||
let windows = global.get_window_actors().filter(function(w) {
|
||||
return w.meta_window.get_window_type() == Meta.WindowType.DESKTOP;
|
||||
@ -531,6 +598,12 @@ Overview.prototype = {
|
||||
this._animateVisible();
|
||||
|
||||
this._syncInputMode();
|
||||
|
||||
// Fake a pointer event if requested
|
||||
if (this._needsFakePointerEvent) {
|
||||
this._fakePointerEvent();
|
||||
this._needsFakePointerEvent = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
Signals.addSignalMethods(Overview.prototype);
|
||||
|
@ -26,6 +26,8 @@ const PANEL_ICON_SIZE = 24;
|
||||
|
||||
const HOT_CORNER_ACTIVATION_TIMEOUT = 0.5;
|
||||
|
||||
const BUTTON_DND_ACTIVATION_TIMEOUT = 250;
|
||||
|
||||
const ANIMATED_ICON_UPDATE_TIMEOUT = 100;
|
||||
const SPINNER_UPDATE_TIMEOUT = 130;
|
||||
const SPINNER_SPEED = 0.02;
|
||||
@ -728,7 +730,20 @@ Panel.prototype = {
|
||||
reactive: true,
|
||||
can_focus: true });
|
||||
this.button.set_child(label);
|
||||
|
||||
this.button._delegate = this.button;
|
||||
this.button._xdndTimeOut = 0;
|
||||
this.button.handleDragOver = Lang.bind(this,
|
||||
function(source, actor, x, y, time) {
|
||||
if (source == Main.xdndHandler) {
|
||||
if (this.button._xdndTimeOut != 0)
|
||||
Mainloop.source_remove(this.button._xdndTimeOut);
|
||||
this.button._xdndTimeOut = Mainloop.timeout_add(BUTTON_DND_ACTIVATION_TIMEOUT,
|
||||
Lang.bind(this,
|
||||
function() {
|
||||
this._xdndShowOverview(actor);
|
||||
}));
|
||||
}
|
||||
});
|
||||
this._leftBox.add(this.button);
|
||||
|
||||
// We use this flag to mark the case where the user has entered the
|
||||
@ -766,6 +781,18 @@ Panel.prototype = {
|
||||
this._hotCorner.connect('leave-event',
|
||||
Lang.bind(this, this._onHotCornerLeft));
|
||||
|
||||
this._hotCorner._delegate = this._hotCorner;
|
||||
this._hotCorner.handleDragOver = Lang.bind(this,
|
||||
function(source, actor, x, y, time) {
|
||||
if (source == Main.xdndHandler) {
|
||||
if(!Main.overview.visible && !Main.overview.animationInProgress) {
|
||||
this.rippleAnimation();
|
||||
Main.overview.showTemporarily();
|
||||
Main.overview.beginItemDrag(actor);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._boxContainer.add_actor(this._hotCornerEnvirons);
|
||||
this._boxContainer.add_actor(this._hotCorner);
|
||||
|
||||
@ -821,6 +848,25 @@ Panel.prototype = {
|
||||
Main.chrome.addActor(this.actor, { visibleInOverview: true });
|
||||
},
|
||||
|
||||
_xdndShowOverview: function (actor) {
|
||||
let [x, y, mask] = global.get_pointer();
|
||||
let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);
|
||||
|
||||
if (pickedActor != this.button) {
|
||||
Mainloop.source_remove(this.button._xdndTimeOut);
|
||||
this.button._xdndTimeOut = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Main.overview.visible && !Main.overview.animationInProgress) {
|
||||
Main.overview.showTemporarily();
|
||||
Main.overview.beginItemDrag(actor);
|
||||
}
|
||||
|
||||
Mainloop.source_remove(this.button._xdndTimeOut);
|
||||
this.button._xdndTimeOut = 0;
|
||||
},
|
||||
|
||||
startStatusArea: function() {
|
||||
for (let i = 0; i < STANDARD_TRAY_ICON_ORDER.length; i++) {
|
||||
let role = STANDARD_TRAY_ICON_ORDER[i];
|
||||
@ -915,6 +961,17 @@ Panel.prototype = {
|
||||
Main.uiGroup.add_actor(ripple);
|
||||
},
|
||||
|
||||
rippleAnimation: function() {
|
||||
// Show three concentric ripples expanding outwards; the exact
|
||||
// parameters were found by trial and error, so don't look
|
||||
// for them to make perfect sense mathematically
|
||||
|
||||
// delay time scale opacity => scale opacity
|
||||
this._addRipple(0.0, 0.83, 0.25, 1.0, 1.5, 0.0);
|
||||
this._addRipple(0.05, 1.0, 0.0, 0.7, 1.25, 0.0);
|
||||
this._addRipple(0.35, 1.0, 0.0, 0.3, 1, 0.0);
|
||||
},
|
||||
|
||||
_onHotCornerEntered : function() {
|
||||
if (this._menus.grabbed)
|
||||
return false;
|
||||
@ -923,14 +980,7 @@ Panel.prototype = {
|
||||
if (!Main.overview.animationInProgress) {
|
||||
this._hotCornerActivationTime = Date.now() / 1000;
|
||||
|
||||
// Show three concentric ripples expanding outwards; the exact
|
||||
// parameters were found by trial and error, so don't look
|
||||
// for them to make perfect sense mathematically
|
||||
|
||||
// delay time scale opacity => scale opacity
|
||||
this._addRipple(0.0, 0.83, 0.25, 1.0, 1.5, 0.0);
|
||||
this._addRipple(0.05, 1.0, 0.0, 0.7, 1.25, 0.0);
|
||||
this._addRipple(0.35, 1.0, 0.0, 0.3, 1, 0.0);
|
||||
this.rippleAnimation();
|
||||
Main.overview.toggle();
|
||||
}
|
||||
}
|
||||
|
@ -644,6 +644,9 @@ WorkspacesView.prototype = {
|
||||
},
|
||||
|
||||
_onDragMotion: function(dragEvent) {
|
||||
if (Main.overview.animationInProgress)
|
||||
return DND.DragMotionResult.CONTINUE;
|
||||
|
||||
let primary = global.get_primary_monitor();
|
||||
|
||||
let activeWorkspaceIndex = global.screen.get_active_workspace_index();
|
||||
|
130
js/ui/xdndHandler.js
Normal file
130
js/ui/xdndHandler.js
Normal file
@ -0,0 +1,130 @@
|
||||
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
||||
|
||||
const Clutter = imports.gi.Clutter;
|
||||
const Lang = imports.lang;
|
||||
const Shell = imports.gi.Shell;
|
||||
const Signals = imports.signals;
|
||||
const Mainloop = imports.mainloop;
|
||||
const DND = imports.ui.dnd;
|
||||
|
||||
function XdndHandler() {
|
||||
this._init();
|
||||
}
|
||||
|
||||
XdndHandler.prototype = {
|
||||
_init: function() {
|
||||
// Used to display a clone of the cursor window when the
|
||||
// window group is hidden (like it happens in the overview)
|
||||
this._cursorWindowClone = null;
|
||||
|
||||
// Used as a drag actor in case we don't have a cursor window clone
|
||||
this._dummy = new Clutter.Rectangle({ width: 1, height: 1, opacity: 0 });
|
||||
global.stage.add_actor(this._dummy);
|
||||
this._dummy.hide();
|
||||
|
||||
// Mutter delays the creation of the output window as long
|
||||
// as possible to avoid flicker. In case a plugin wants to
|
||||
// access it directly it has to connect to the stage's show
|
||||
// signal. (see comment in compositor.c:meta_compositor_manage_screen)
|
||||
global.stage.connect('show', function () {
|
||||
global.init_xdnd();
|
||||
return false;
|
||||
});
|
||||
|
||||
global.connect('xdnd-enter', Lang.bind(this, this._onEnter));
|
||||
global.connect('xdnd-position-changed', Lang.bind(this, this._onPositionChanged));
|
||||
global.connect('xdnd-leave', Lang.bind(this, this._onLeave));
|
||||
|
||||
this._windowGroupVisibilityHandlerId = 0;
|
||||
},
|
||||
|
||||
// Called when the user cancels the drag (i.e release the button)
|
||||
_onLeave: function() {
|
||||
if (this._windowGroupVisibilityHandlerId != 0) {
|
||||
Mainloop.source_remove(this._windowGroupVisibilityHandlerId);
|
||||
this._windowGroupVisibilityHandlerId = 0;
|
||||
}
|
||||
this.emit('drag-end');
|
||||
},
|
||||
|
||||
_onEnter: function() {
|
||||
this._windowGroupVisibilityHandlerId =
|
||||
global.window_group.connect('notify::visible',
|
||||
Lang.bind(this, this._onWindowGroupVisibilityChanged));
|
||||
|
||||
this.emit('drag-begin', global.get_current_time());
|
||||
},
|
||||
|
||||
_onWindowGroupVisibilityChanged: function() {
|
||||
if (!global.window_group.visible) {
|
||||
if (this._cursorWindowClone)
|
||||
return;
|
||||
|
||||
let windows = global.get_window_actors();
|
||||
let cursorWindow = windows[windows.length - 1];
|
||||
|
||||
// FIXME: more reliable way?
|
||||
if (!cursorWindow.is_override_redirect())
|
||||
return;
|
||||
|
||||
let constraint_x = new Clutter.BindConstraint({ coordinate : Clutter.BindCoordinate.X,
|
||||
source: cursorWindow});
|
||||
let constraint_y = new Clutter.BindConstraint({ coordinate : Clutter.BindCoordinate.Y,
|
||||
source: cursorWindow});
|
||||
|
||||
this._cursorWindowClone = new Clutter.Clone({ source: cursorWindow });
|
||||
global.overlay_group.add_actor(this._cursorWindowClone);
|
||||
Shell.util_set_hidden_from_pick(this._cursorWindowClone, true);
|
||||
|
||||
// Make sure that the clone has the same position as the source
|
||||
this._cursorWindowClone.add_constraint(constraint_x);
|
||||
this._cursorWindowClone.add_constraint(constraint_y);
|
||||
} else {
|
||||
if (this._cursorWindowClone)
|
||||
{
|
||||
this._cursorWindowClone.destroy();
|
||||
this._cursorWindowClone = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onPositionChanged: function(obj, x, y) {
|
||||
let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
|
||||
|
||||
// Make sure that the cursor window is on top
|
||||
if (this._cursorWindowClone)
|
||||
this._cursorWindowClone.raise_top();
|
||||
|
||||
let dragEvent = {
|
||||
x: x,
|
||||
y: y,
|
||||
dragActor: this._cursorWindowClone ? this._cursorWindowClone : this._dummy,
|
||||
source: this,
|
||||
targetActor: pickedActor
|
||||
};
|
||||
|
||||
for (let i = 0; i < DND.dragMonitors.length; i++) {
|
||||
let motionFunc = DND.dragMonitors[i].dragMotion;
|
||||
if (motionFunc) {
|
||||
let result = motionFunc(dragEvent);
|
||||
if (result != DND.DragMotionResult.CONTINUE)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (pickedActor) {
|
||||
if (pickedActor._delegate && pickedActor._delegate.handleDragOver) {
|
||||
let result = pickedActor._delegate.handleDragOver(this,
|
||||
dragEvent.dragActor,
|
||||
x,
|
||||
y,
|
||||
global.get_current_time());
|
||||
if (result != DND.DragMotionResult.CONTINUE)
|
||||
return;
|
||||
}
|
||||
pickedActor = pickedActor.get_parent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Signals.addSignalMethods(XdndHandler.prototype);
|
Reference in New Issue
Block a user