1//
2//  Copyright (C) 2011-2012 Robert Dyer, Rico Tzschichholz
3//
4//  This file is part of Plank.
5//
6//  Plank is free software: you can redistribute it and/or modify
7//  it under the terms of the GNU General Public License as published by
8//  the Free Software Foundation, either version 3 of the License, or
9//  (at your option) any later version.
10//
11//  Plank is distributed in the hope that it will be useful,
12//  but WITHOUT ANY WARRANTY; without even the implied warranty of
13//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14//  GNU General Public License for more details.
15//
16//  You should have received a copy of the GNU General Public License
17//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18//
19
20namespace Plank
21{
22	/**
23	 * Handles all of the drag'n'drop events for a dock.
24	 */
25	public class DragManager : GLib.Object
26	{
27		public DockController controller { private get; construct; }
28
29		public bool InternalDragActive { get; private set; default = false; }
30
31		public DockItem? DragItem { get; private set; default = null; }
32
33		public bool DragNeedsCheck { get; private set; default = true; }
34
35		bool external_drag_active = false;
36		public bool ExternalDragActive {
37			get { return external_drag_active; }
38			private set {
39				if (external_drag_active == value)
40					return;
41				external_drag_active = value;
42
43				if (!value) {
44					drag_known = false;
45					drag_data = null;
46					drag_data_requested = false;
47					DragNeedsCheck = true;
48				}
49			}
50		}
51
52		bool reposition_mode = false;
53		public bool RepositionMode {
54			get { return reposition_mode; }
55			private set {
56				if (reposition_mode == value)
57					return;
58				reposition_mode = value;
59
60				if (reposition_mode)
61					disable_drag_to (controller.window);
62				else
63					enable_drag_to (controller.window);
64			}
65		}
66
67		Gdk.Window? proxy_window = null;
68
69		bool drag_canceled = false;
70		bool drag_known = false;
71		bool drag_data_requested = false;
72		uint marker = 0U;
73		uint drag_hover_timer_id = 0U;
74
75		Gee.ArrayList<string>? drag_data = null;
76
77		int window_scale_factor = 1;
78		ulong drag_item_redraw_handler_id = 0UL;
79
80		/**
81		 * Creates a new instance of a DragManager, which handles
82		 * drag'n'drop interactions of a dock.
83		 *
84		 * @param controller the {@link DockController} to manage drag'n'drop for
85		 */
86		public DragManager (DockController controller)
87		{
88			GLib.Object (controller : controller);
89		}
90
91		/**
92		 * Initializes the drag-manager.  Call after the DockWindow is constructed.
93		 */
94		public void initialize ()
95			requires (controller.window != null)
96		{
97			unowned DockWindow window = controller.window;
98			unowned DockPreferences prefs = controller.prefs;
99
100			window.drag_motion.connect (drag_motion);
101			window.drag_begin.connect (drag_begin);
102			window.drag_data_received.connect (drag_data_received);
103			window.drag_data_get.connect (drag_data_get);
104			window.drag_drop.connect (drag_drop);
105			window.drag_end.connect (drag_end);
106			window.drag_leave.connect (drag_leave);
107			window.drag_failed.connect (drag_failed);
108
109			prefs.notify["LockItems"].connect (lock_items_changed);
110
111			enable_drag_to (window);
112			if (!prefs.LockItems)
113				enable_drag_from (window);
114		}
115
116		~DragManager ()
117		{
118			unowned DockWindow window = controller.window;
119
120			window.drag_motion.disconnect (drag_motion);
121			window.drag_begin.disconnect (drag_begin);
122			window.drag_data_received.disconnect (drag_data_received);
123			window.drag_data_get.disconnect (drag_data_get);
124			window.drag_drop.disconnect (drag_drop);
125			window.drag_end.disconnect (drag_end);
126			window.drag_leave.disconnect (drag_leave);
127			window.drag_failed.disconnect (drag_failed);
128
129			controller.prefs.notify["LockItems"].disconnect (lock_items_changed);
130
131			disable_drag_to (window);
132			disable_drag_from (window);
133		}
134
135		void lock_items_changed ()
136		{
137			unowned DockWindow window = controller.window;
138
139			if (controller.prefs.LockItems)
140				disable_drag_from (window);
141			else
142				enable_drag_from (window);
143		}
144
145		[CCode (instance_pos = -1)]
146		void drag_data_get (Gtk.Widget w, Gdk.DragContext context, Gtk.SelectionData selection_data, uint info, uint time_)
147		{
148			if (InternalDragActive && DragItem != null) {
149				string uri = "%s\r\n".printf (DragItem.as_uri ());
150				selection_data.set (selection_data.get_target (), 8, (uchar[]) uri.to_utf8 ());
151			}
152		}
153
154		/**
155		 * Whether the current dragged-data is accepted by the given dock-item
156		 *
157		 * @param item the dock-item
158		 */
159		public bool drop_is_accepted_by (DockItem item)
160		{
161			if (drag_data == null)
162				return false;
163
164			return item.can_accept_drop (drag_data);
165		}
166
167		void set_drag_icon (Gdk.DragContext context, DockItem? item, double opacity = 1.0)
168		{
169			if (item == null) {
170				Gtk.drag_set_icon_default (context);
171				return;
172			}
173
174			window_scale_factor = controller.window.get_window ().get_scale_factor ();
175			var drag_icon_size = (int) (1.2 * controller.position_manager.ZoomIconSize);
176			if (drag_icon_size % 2 == 1)
177				drag_icon_size++;
178			drag_icon_size *= window_scale_factor;
179			var drag_surface = new Surface (drag_icon_size, drag_icon_size);
180			drag_surface.Internal.set_device_scale (window_scale_factor, window_scale_factor);
181
182			var item_surface = item.get_surface_copy (drag_icon_size, drag_icon_size, drag_surface);
183			unowned Cairo.Context cr = drag_surface.Context;
184			if (window_scale_factor > 1) {
185				cr.save ();
186				cr.scale (1.0 / window_scale_factor, 1.0 / window_scale_factor);
187			}
188			cr.set_operator (Cairo.Operator.OVER);
189			cr.set_source_surface (item_surface.Internal, 0, 0);
190			cr.paint_with_alpha (opacity);
191			if (window_scale_factor > 1)
192				cr.restore ();
193
194			unowned Cairo.Surface surface = drag_surface.Internal;
195			surface.set_device_offset (-drag_icon_size / 2.0, -drag_icon_size / 2.0);
196			Gtk.drag_set_icon_surface (context, surface);
197		}
198
199		[CCode (instance_pos = -1)]
200		void drag_begin (Gtk.Widget w, Gdk.DragContext context)
201		{
202			unowned DockWindow window = controller.window;
203
204			window.notify["HoveredItem"].connect (hovered_item_changed);
205
206			InternalDragActive = true;
207			drag_canceled = false;
208
209			if (proxy_window != null) {
210				enable_drag_to (window);
211				proxy_window = null;
212			}
213
214			DragItem = window.HoveredItem;
215
216			if (RepositionMode)
217				DragItem = null;
218
219			if (DragItem == null) {
220				Gdk.drag_abort (context, Gtk.get_current_event_time ());
221				return;
222			}
223
224			set_drag_icon (context, DragItem, 0.8);
225			drag_item_redraw_handler_id = DragItem.needs_redraw.connect (() => {
226				set_drag_icon (context, DragItem, 0.8);
227			});
228
229			context.get_device ().grab (window.get_window (), Gdk.GrabOwnership.APPLICATION, true,
230				Gdk.EventMask.ALL_EVENTS_MASK, null, Gtk.get_current_event_time ());
231		}
232
233		[CCode (instance_pos = -1)]
234		void drag_data_received (Gtk.Widget w, Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time_)
235		{
236			if (drag_data_requested) {
237				unowned string? data = (string?) selection_data.get_data ();
238				if (data == null) {
239					drag_data_requested = false;
240					Gdk.drag_status (context, Gdk.DragAction.COPY, time_);
241					return;
242				}
243
244				var uris = Uri.list_extract_uris (data);
245
246				drag_data = new Gee.ArrayList<string> ();
247				foreach (unowned string s in uris) {
248					if (s.has_prefix (DOCKLET_URI_PREFIX)) {
249						drag_data.add (s);
250						continue;
251					}
252
253					var uri = File.new_for_uri (s).get_uri ();
254					if (uri != null)
255						drag_data.add (uri);
256				}
257
258				drag_data_requested = false;
259
260				if (drag_data.size == 1) {
261					var uri = drag_data[0];
262					DragNeedsCheck = !(uri.has_prefix (DOCKLET_URI_PREFIX) || uri.has_suffix (".desktop"));
263				} else {
264					DragNeedsCheck = true;
265				}
266
267				// Force initial redraw for ExternalDrag to pick up new
268				// drag_data for can_accept_drop check
269				controller.renderer.animated_draw ();
270
271				// Trigger this manually since we will miss to receive the very first emmit
272				// after entering the dock-window
273				hovered_item_changed ();
274			}
275
276			Gdk.drag_status (context, Gdk.DragAction.COPY, time_);
277		}
278
279		[CCode (instance_pos = -1)]
280		bool drag_drop (Gtk.Widget w, Gdk.DragContext context, int x, int y, uint time_)
281		{
282			Gtk.drag_finish (context, true, false, time_);
283
284			if (drag_hover_timer_id > 0U) {
285				GLib.Source.remove (drag_hover_timer_id);
286				drag_hover_timer_id = 0U;
287			}
288
289			if (drag_data == null)
290				return true;
291
292			unowned DockWindow window = controller.window;
293			unowned DockItem? item = window.HoveredItem;
294			unowned DockItemProvider? provider = window.HoveredItemProvider;
295
296			if (DragNeedsCheck && item != null && item.can_accept_drop (drag_data))
297				item.accept_drop (drag_data);
298			else if (!controller.prefs.LockItems && provider != null && provider.can_accept_drop (drag_data))
299				provider.accept_drop (drag_data);
300
301			ExternalDragActive = false;
302			return true;
303		}
304
305		[CCode (instance_pos = -1)]
306		void drag_end (Gtk.Widget w, Gdk.DragContext context)
307		{
308			unowned HideManager hide_manager = controller.hide_manager;
309
310			if (drag_item_redraw_handler_id > 0UL) {
311				if (DragItem != null)
312					GLib.SignalHandler.disconnect (DragItem, drag_item_redraw_handler_id);
313				drag_item_redraw_handler_id = 0UL;
314			}
315
316			if (!drag_canceled && DragItem != null) {
317				hide_manager.update_hovered ();
318				if (!hide_manager.Hovered) {
319					if (DragItem.can_be_removed ()) {
320						// Remove from dock
321						unowned ApplicationDockItem? app_item = (DragItem as ApplicationDockItem);
322						if (app_item == null || !(app_item.is_running () || app_item.has_unity_info ())) {
323							DragItem.IsVisible = false;
324							DragItem.Container.remove (DragItem);
325						}
326						DragItem.delete ();
327
328						int x, y;
329						context.get_device ().get_position (null, out x, out y);
330						PoofWindow.get_default ().show_at (x, y);
331					}
332				} else if (controller.window.HoveredItem == null) {
333					// Dropped somewhere on dock
334					// Pin this item if possible/needed, so we assume the user cares
335					// about this application when changing its position
336					if (controller.prefs.AutoPinning && DragItem is TransientDockItem) {
337						unowned DefaultApplicationDockItemProvider? provider = (DragItem.Container as DefaultApplicationDockItemProvider);
338						if (provider != null)
339							provider.pin_item (DragItem);
340					}
341				} else {
342					// Dropped onto another dockitem
343					/* TODO
344					DockItem item = controller.window.HoveredItem;
345					if (item != null && item.CanAcceptDrop (DragItem))
346						item.AcceptDrop (DragItem);
347					*/
348				}
349			}
350
351			InternalDragActive = false;
352			DragItem = null;
353			context.get_device ().ungrab (Gtk.get_current_event_time ());
354
355			controller.window.notify["HoveredItem"].disconnect (hovered_item_changed);
356
357			controller.hover.hide ();
358
359			// Force last redraw for InternalDrag
360			controller.renderer.animated_draw ();
361
362			// Make sure to hide the dock again if needed
363			hide_manager.update_hovered ();
364		}
365
366		[CCode (instance_pos = -1)]
367		void drag_leave (Gtk.Widget w, Gdk.DragContext context, uint time_)
368		{
369			if (drag_hover_timer_id > 0U) {
370				GLib.Source.remove (drag_hover_timer_id);
371				drag_hover_timer_id = 0U;
372			}
373
374			controller.hide_manager.update_hovered ();
375			drag_known = false;
376
377			if (ExternalDragActive) {
378				controller.window.notify["HoveredItem"].disconnect (hovered_item_changed);
379
380				// Make sure ExternalDragActive gets set to false to reactivate HideManager.
381				// This is needed while getting a leave event without followed by a drop.
382				// Delay it to preserve functionality in drag_drop.
383				Gdk.threads_add_idle (() => {
384					ExternalDragActive = false;
385
386					controller.hover.hide ();
387
388					// If an item was hovered we need it in drag_drop,
389					// so reset HoveredItem here not earlier.
390					controller.window.update_hovered (-1, -1);
391
392					// Force last redraw for ExternalDrag
393					controller.renderer.animated_draw ();
394
395					// Make sure to hide the dock again if needed
396					controller.hide_manager.update_hovered ();
397
398					return false;
399				});
400			}
401
402			if (DragItem == null)
403				return;
404
405			if (!controller.hide_manager.Hovered) {
406				controller.window.update_hovered (-1, -1);
407				controller.renderer.animated_draw ();
408			}
409		}
410
411		[CCode (instance_pos = -1)]
412		bool drag_failed (Gtk.Widget w, Gdk.DragContext context, Gtk.DragResult result)
413		{
414			drag_canceled = result == Gtk.DragResult.USER_CANCELLED;
415
416			return !drag_canceled;
417		}
418
419		[CCode (instance_pos = -1)]
420		bool drag_motion (Gtk.Widget w, Gdk.DragContext context, int x, int y, uint time_)
421		{
422			if (RepositionMode)
423				return true;
424
425			if (ExternalDragActive == InternalDragActive)
426				ExternalDragActive = !InternalDragActive;
427
428			if (marker != direct_hash (context)) {
429				marker = direct_hash (context);
430				drag_known = false;
431			}
432
433			unowned DockWindow window = controller.window;
434			unowned HideManager hide_manager = controller.hide_manager;
435
436			// we own the drag if InternalDragActive is true, lets not be silly
437			if (ExternalDragActive && !drag_known) {
438				drag_known = true;
439
440				window.notify["HoveredItem"].connect (hovered_item_changed);
441
442				Gdk.Atom atom = Gtk.drag_dest_find_target (window, context, Gtk.drag_dest_get_target_list (window));
443				if (atom.name () != Gdk.Atom.NONE.name ()) {
444					drag_data_requested = true;
445					Gtk.drag_get_data (window, context, atom, time_);
446				} else {
447					Gdk.drag_status (context, Gdk.DragAction.PRIVATE, time_);
448				}
449			} else {
450				Gdk.drag_status (context, Gdk.DragAction.COPY, time_);
451			}
452
453			if (ExternalDragActive) {
454				unowned PositionManager position_manager = controller.position_manager;
455				unowned DockItem hovered_item = window.HoveredItem;
456				unowned HoverWindow hover = controller.hover;
457				if (DragNeedsCheck && hovered_item != null && hovered_item.can_accept_drop (drag_data)) {
458					int hx, hy;
459					position_manager.get_hover_position (hovered_item, out hx, out hy);
460					hover.set_text (hovered_item.get_drop_text ());
461					hover.show_at (hx, hy, position_manager.Position);
462				} else if (hide_manager.Hovered && !controller.prefs.LockItems) {
463					int hx = x, hy = y;
464					position_manager.get_hover_position_at (ref hx, ref hy);
465					hover.set_text (_("Drop to add to dock"));
466					hover.show_at (hx, hy, position_manager.Position);
467				} else {
468					hover.hide ();
469				}
470			}
471
472			controller.renderer.update_local_cursor (x, y);
473			hide_manager.update_hovered_with_coords (x, y);
474			window.update_hovered (x, y);
475
476			return true;
477		}
478
479		void hovered_item_changed ()
480		{
481			unowned DockItem hovered_item = controller.window.HoveredItem;
482
483			if (InternalDragActive && DragItem != null && hovered_item != null
484				&& DragItem != hovered_item
485				&& DragItem.Container == hovered_item.Container) {
486				DragItem.Container.move_to (DragItem, hovered_item);
487			}
488
489			if (drag_hover_timer_id > 0U) {
490				GLib.Source.remove (drag_hover_timer_id);
491				drag_hover_timer_id = 0U;
492			}
493
494			if (ExternalDragActive && drag_data != null)
495				drag_hover_timer_id = Gdk.threads_add_timeout (1500, () => {
496					unowned DockItem item = controller.window.HoveredItem;
497					if (item != null)
498						item.scrolled (Gdk.ScrollDirection.DOWN, 0, Gtk.get_current_event_time ());
499					else
500						drag_hover_timer_id = 0U;
501					return item != null;
502				});
503		}
504
505		Gdk.Window? best_proxy_window ()
506		{
507			var window_stack = controller.window.get_screen ().get_window_stack ();
508			window_stack.reverse ();
509
510			foreach (var window in window_stack) {
511				int w_x, w_y, w_width, w_height;
512				window.get_position (out w_x, out w_y);
513				w_width = window.get_width ();
514				w_height = window.get_height ();
515				Gdk.Rectangle w_geo = { w_x, w_y, w_width, w_height };
516
517				int x, y;
518				controller.window.get_display ().get_device_manager ().get_client_pointer ().get_position (null, out x, out y);
519
520				if (window.is_visible () && w_geo.intersect ({ x, y, 0, 0 }, null))
521					return window;
522			}
523
524			return null;
525		}
526
527		public void ensure_proxy ()
528		{
529			// having a proxy window here is VERY bad ju-ju
530			if (InternalDragActive)
531				return;
532
533			if (controller.hide_manager.Hovered) {
534				if (proxy_window == null)
535					return;
536				proxy_window = null;
537				enable_drag_to (controller.window);
538				return;
539			}
540
541			Gdk.ModifierType mod;
542			double[] axes = {};
543			controller.window.get_display ().get_device_manager ().get_client_pointer ().get_state (controller.window.get_window (), axes, out mod);
544
545			if ((mod & Gdk.ModifierType.BUTTON1_MASK) == Gdk.ModifierType.BUTTON1_MASK) {
546				Gdk.Window bestProxy = best_proxy_window ();
547				if (bestProxy != null && proxy_window != bestProxy) {
548					proxy_window = bestProxy;
549					Gtk.drag_dest_set_proxy (controller.window, proxy_window, Gdk.DragProtocol.XDND, true);
550				}
551			}
552		}
553
554		void enable_drag_to (DockWindow window)
555		{
556			Gtk.TargetEntry te1 = { "text/uri-list", 0, 0 };
557			Gtk.TargetEntry te2 = { "text/plank-uri-list", 0, 0 };
558			Gtk.drag_dest_set (window, 0, {te1, te2}, Gdk.DragAction.COPY);
559		}
560
561		void disable_drag_to (DockWindow window)
562		{
563			Gtk.drag_dest_unset (window);
564		}
565
566		void enable_drag_from (DockWindow window)
567		{
568			// we dont really want to offer the drag to anything, merely pretend to, so we set a mimetype nothing takes
569			Gtk.TargetEntry te = { "text/plank-uri-list", Gtk.TargetFlags.SAME_APP, 0};
570			Gtk.drag_source_set (window, Gdk.ModifierType.BUTTON1_MASK, { te }, Gdk.DragAction.PRIVATE);
571		}
572
573		void disable_drag_from (DockWindow window)
574		{
575			Gtk.drag_source_unset (window);
576		}
577	}
578}
579