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