1 // SPDX-License-Identifier: GPL-2.0-or-later
2
3 /** @file
4 * @brief A widget with multiple panes. Agnostic to type what kind of widgets panes contain.
5 *
6 * Authors: see git history
7 * Tavmjong Bah
8 *
9 * Copyright (c) 2020 Tavmjong Bah, Authors
10 *
11 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
12 */
13
14 #include "dialog-multipaned.h"
15
16 #include <glibmm/i18n.h>
17 #include <glibmm/objectbase.h>
18 #include <gtkmm/container.h>
19 #include <gtkmm/image.h>
20 #include <gtkmm/label.h>
21 #include <iostream>
22 #include <numeric>
23
24 #include "ui/dialog/dialog-notebook.h"
25 #include "ui/widget/canvas-grid.h"
26
27 #define DROPZONE_SIZE 16
28 #define HANDLE_SIZE 12
29 #define HANDLE_CROSS_SIZE 25
30
31 namespace Inkscape {
32 namespace UI {
33 namespace Dialog {
34
35 /*
36 * References:
37 * https://blog.gtk.org/2017/06/
38 * https://developer.gnome.org/gtkmm-tutorial/stable/sec-custom-containers.html.en
39 * https://wiki.gnome.org/HowDoI/Gestures
40 *
41 * The children widget sizes are "sticky". They change a minimal
42 * amount when the parent widget is resized or a child is added or
43 * removed.
44 *
45 * A gesture is used to track handle movement. This must be attached
46 * to the parent widget (the offset_x/offset_y values are relative to
47 * the widget allocation which changes for the handles as they are
48 * moved).
49 */
50
51 /* ============ MyDropZone ============ */
52
MyDropZone(Gtk::Orientation orientation,int size=DROPZONE_SIZE)53 MyDropZone::MyDropZone(Gtk::Orientation orientation, int size = DROPZONE_SIZE)
54 : Glib::ObjectBase("MultipanedDropZone")
55 , Gtk::Orientable()
56 , Gtk::EventBox()
57 {
58 set_name("MultipanedDropZone");
59 set_orientation(orientation);
60
61 if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
62 set_size_request(size, -1);
63 } else {
64 set_size_request(-1, size);
65 }
66 }
67
68 /* ============ MyHandle ============ */
69
MyHandle(Gtk::Orientation orientation,int size=HANDLE_SIZE)70 MyHandle::MyHandle(Gtk::Orientation orientation, int size = HANDLE_SIZE)
71 : Glib::ObjectBase("MultipanedHandle")
72 , Gtk::Orientable()
73 , Gtk::EventBox()
74 , _cross_size(0)
75 , _child(nullptr)
76 {
77 set_name("MultipanedHandle");
78 set_orientation(orientation);
79
80 Gtk::Image *image = Gtk::manage(new Gtk::Image());
81 if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
82 image->set_from_icon_name("view-more-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
83 set_size_request(size, -1);
84 } else {
85 image->set_from_icon_name("view-more-horizontal-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
86 set_size_request(-1, size);
87 }
88 image->set_pixel_size(size);
89 add(*image);
90
91 // Signal
92 signal_size_allocate().connect(sigc::mem_fun(*this, &MyHandle::resize_handler));
93
94 show_all();
95 }
96
97 /**
98 * Change the mouse pointer into a resize icon to show you can drag.
99 */
on_enter_notify_event(GdkEventCrossing * crossing_event)100 bool MyHandle::on_enter_notify_event(GdkEventCrossing *crossing_event)
101 {
102 auto window = get_window();
103 auto display = get_display();
104
105 if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
106 auto cursor = Gdk::Cursor::create(display, "col-resize");
107 window->set_cursor(cursor);
108 } else {
109 auto cursor = Gdk::Cursor::create(display, "row-resize");
110 window->set_cursor(cursor);
111 }
112
113 return false;
114 }
115
116 /**
117 * This allocation handler function is used to add/remove handle icons in order to be able
118 * to hide completely a transversal handle into the sides of a DialogMultipaned.
119 *
120 * The image has a specific size set up in the constructor and will not naturally shrink/hide.
121 * In conclusion, we remove it from the handle and save it into an internal reference.
122 */
resize_handler(Gtk::Allocation & allocation)123 void MyHandle::resize_handler(Gtk::Allocation &allocation)
124 {
125 int size = (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) ? allocation.get_height() : allocation.get_width();
126
127 if (_cross_size > size && HANDLE_CROSS_SIZE > size && !_child) {
128 _child = get_child();
129 remove();
130 } else if (_cross_size < size && HANDLE_CROSS_SIZE < size && _child) {
131 add(*_child);
132 _child = nullptr;
133 }
134
135 _cross_size = size;
136 }
137
138 /* ============ DialogMultipaned ============= */
139
DialogMultipaned(Gtk::Orientation orientation)140 DialogMultipaned::DialogMultipaned(Gtk::Orientation orientation)
141 : Glib::ObjectBase("DialogMultipaned")
142 , Gtk::Orientable()
143 , Gtk::Container()
144 , _empty_widget(nullptr)
145 , hide_multipaned(false)
146 {
147 set_name("DialogMultipaned");
148 set_orientation(orientation);
149 set_has_window(false);
150 set_redraw_on_allocate(false);
151
152 // ============= Add dropzones ==============
153 MyDropZone *dropzone_s = Gtk::manage(new MyDropZone(orientation));
154 MyDropZone *dropzone_e = Gtk::manage(new MyDropZone(orientation));
155
156 dropzone_s->set_parent(*this);
157 dropzone_e->set_parent(*this);
158
159 children.push_back(dropzone_s);
160 children.push_back(dropzone_e);
161
162 // ============ Connect signals =============
163 gesture = Gtk::GestureDrag::create(*this);
164
165 _connections.emplace_back(
166 gesture->signal_drag_begin().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_begin)));
167 _connections.emplace_back(gesture->signal_drag_end().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_end)));
168 _connections.emplace_back(
169 gesture->signal_drag_update().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_update)));
170
171 _connections.emplace_back(
172 signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_data)));
173 _connections.emplace_back(
174 dropzone_s->signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_prepend_drag_data)));
175 _connections.emplace_back(
176 dropzone_e->signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_append_drag_data)));
177
178 // add empty widget to initiate the container
179 add_empty_widget();
180
181 show_all();
182 }
183
~DialogMultipaned()184 DialogMultipaned::~DialogMultipaned()
185 {
186 // Disconnect all signals
187 for_each(_connections.begin(), _connections.end(), [&](auto c) { c.disconnect(); });
188 /*
189 for (std::vector<Gtk::Widget *>::iterator it = children.begin(); it != children.end();) {
190 if (dynamic_cast<DialogMultipaned *>(*it) || dynamic_cast<DialogNotebook *>(*it)) {
191 delete *it;
192 } else {
193 it++;
194 }
195 }
196 */
197
198 for (;;) {
199 auto it = std::find_if(children.begin(), children.end(), [](auto w) {
200 return dynamic_cast<DialogMultipaned *>(w) || dynamic_cast<DialogNotebook *>(w);
201 });
202 if (it != children.end()) {
203 // delete dialog multipanel or notebook; this action results in its removal from 'children'!
204 delete *it;
205 } else {
206 // no more dialog panels
207 break;
208 }
209 }
210
211 children.clear();
212 }
213
prepend(Gtk::Widget * child)214 void DialogMultipaned::prepend(Gtk::Widget *child)
215 {
216 remove_empty_widget(); // Will remove extra widget if existing
217
218 // If there are MyMultipane children that are empty, they will be removed
219 for (auto const &child1 : children) {
220 DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child1);
221 if (paned && paned->has_empty_widget()) {
222 remove(*child1);
223 remove_empty_widget();
224 }
225 }
226
227 if (child) {
228 // Add handle
229 if (children.size() > 2) {
230 MyHandle *my_handle = Gtk::manage(new MyHandle(get_orientation()));
231 my_handle->set_parent(*this);
232 children.insert(children.begin() + 1, my_handle); // After start dropzone
233 }
234
235 // Add child
236 children.insert(children.begin() + 1, child);
237 if (!child->get_parent())
238 child->set_parent(*this);
239
240 // Ideally, we would only call child->show() here and assume that the
241 // child has already configured visibility of all its own children.
242 child->show_all();
243 }
244 }
245
append(Gtk::Widget * child)246 void DialogMultipaned::append(Gtk::Widget *child)
247 {
248 remove_empty_widget(); // Will remove extra widget if existing
249
250 // If there are MyMultipane children that are empty, they will be removed
251 for (auto const &child1 : children) {
252 DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child1);
253 if (paned && paned->has_empty_widget()) {
254 remove(*child1);
255 remove_empty_widget();
256 }
257 }
258
259 if (child) {
260 // Add handle
261 if (children.size() > 2) {
262 MyHandle *my_handle = Gtk::manage(new MyHandle(get_orientation()));
263 my_handle->set_parent(*this);
264 children.insert(children.end() - 1, my_handle); // Before end dropzone
265 }
266
267 // Add child
268 children.insert(children.end() - 1, child);
269 if (!child->get_parent())
270 child->set_parent(*this);
271
272 // See comment in DialogMultipaned::prepend
273 child->show_all();
274 }
275 }
276
add_empty_widget()277 void DialogMultipaned::add_empty_widget()
278 {
279 const int EMPTY_WIDGET_SIZE = 60; // magic nummber
280
281 // The empty widget is a label
282 auto label = Gtk::manage(new Gtk::Label(_("You can drop dockable dialogs here.")));
283 label->set_line_wrap();
284 label->set_justify(Gtk::JUSTIFY_CENTER);
285 label->set_valign(Gtk::ALIGN_CENTER);
286 label->set_vexpand();
287
288 append(label);
289 _empty_widget = label;
290
291 if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
292 int dropzone_size = (get_height() - EMPTY_WIDGET_SIZE) / 2;
293 if (dropzone_size > DROPZONE_SIZE) {
294 set_dropzone_sizes(dropzone_size, dropzone_size);
295 }
296 }
297 }
298
remove_empty_widget()299 void DialogMultipaned::remove_empty_widget()
300 {
301 if (_empty_widget) {
302 auto it = std::find(children.begin(), children.end(), _empty_widget);
303 if (it != children.end()) {
304 children.erase(it);
305 }
306 _empty_widget->unparent();
307 _empty_widget = nullptr;
308 }
309
310 if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
311 set_dropzone_sizes(DROPZONE_SIZE, DROPZONE_SIZE);
312 }
313 }
314
get_first_widget()315 Gtk::Widget *DialogMultipaned::get_first_widget()
316 {
317 if (children.size() > 2) {
318 return children[1];
319 } else {
320 return nullptr;
321 }
322 }
323
get_last_widget()324 Gtk::Widget *DialogMultipaned::get_last_widget()
325 {
326 if (children.size() > 2) {
327 return children[children.size() - 2];
328 } else {
329 return nullptr;
330 }
331 }
332
333 /**
334 * Set the sizes of the DialogMultipaned dropzones.
335 * @param start, the size you want or -1 for the default `DROPZONE_SIZE`
336 * @param end, the size you want or -1 for the default `DROPZONE_SIZE`
337 */
set_dropzone_sizes(int start,int end)338 void DialogMultipaned::set_dropzone_sizes(int start, int end)
339 {
340 bool orientation = get_orientation() == Gtk::ORIENTATION_HORIZONTAL;
341
342 if (start == -1) {
343 start = DROPZONE_SIZE;
344 }
345
346 MyDropZone *dropzone_s = dynamic_cast<MyDropZone *>(children[0]);
347
348 if (dropzone_s) {
349 if (orientation) {
350 dropzone_s->set_size_request(start, -1);
351 } else {
352 dropzone_s->set_size_request(-1, start);
353 }
354 }
355
356 if (end == -1) {
357 end = DROPZONE_SIZE;
358 }
359
360 MyDropZone *dropzone_e = dynamic_cast<MyDropZone *>(children[children.size() - 1]);
361
362 if (dropzone_e) {
363 if (orientation) {
364 dropzone_e->set_size_request(end, -1);
365 } else {
366 dropzone_e->set_size_request(-1, end);
367 }
368 }
369 }
370
371 /**
372 * Hide all children of this container that are of type multipaned by setting their allocation on the main axis to 0.
373 */
toggle_multipaned_children()374 void DialogMultipaned::toggle_multipaned_children()
375 {
376 hide_multipaned = !hide_multipaned;
377 queue_allocate();
378 }
379
380 /**
381 * Ensure that this dialog container is visible.
382 */
ensure_multipaned_children()383 void DialogMultipaned::ensure_multipaned_children()
384 {
385 hide_multipaned = false;
386 queue_allocate();
387 }
388
389 // ****************** OVERRIDES ******************
390
391 // The following functions are here to define the behavior of our custom container
392
get_request_mode_vfunc() const393 Gtk::SizeRequestMode DialogMultipaned::get_request_mode_vfunc() const
394 {
395 if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
396 return Gtk::SIZE_REQUEST_WIDTH_FOR_HEIGHT;
397 } else {
398 return Gtk::SIZE_REQUEST_HEIGHT_FOR_WIDTH;
399 }
400 }
401
get_preferred_width_vfunc(int & minimum_width,int & natural_width) const402 void DialogMultipaned::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const
403 {
404 minimum_width = 0;
405 natural_width = 0;
406 for (auto const &child : children) {
407 if (child && child->is_visible()) {
408 int child_minimum_width = 0;
409 int child_natural_width = 0;
410 child->get_preferred_width(child_minimum_width, child_natural_width);
411 if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
412 minimum_width = std::max(minimum_width, child_minimum_width);
413 natural_width = std::max(natural_width, child_natural_width);
414 } else {
415 minimum_width += child_minimum_width;
416 natural_width += child_natural_width;
417 }
418 }
419 }
420 }
421
get_preferred_height_vfunc(int & minimum_height,int & natural_height) const422 void DialogMultipaned::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const
423 {
424 minimum_height = 0;
425 natural_height = 0;
426 for (auto const &child : children) {
427 if (child && child->is_visible()) {
428 int child_minimum_height = 0;
429 int child_natural_height = 0;
430 child->get_preferred_height(child_minimum_height, child_natural_height);
431 if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
432 minimum_height = std::max(minimum_height, child_minimum_height);
433 natural_height = std::max(natural_height, child_natural_height);
434 } else {
435 minimum_height += child_minimum_height;
436 natural_height += child_natural_height;
437 }
438 }
439 }
440 }
441
get_preferred_width_for_height_vfunc(int height,int & minimum_width,int & natural_width) const442 void DialogMultipaned::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const
443 {
444 minimum_width = 0;
445 natural_width = 0;
446 for (auto const &child : children) {
447 if (child && child->is_visible()) {
448 int child_minimum_width = 0;
449 int child_natural_width = 0;
450 child->get_preferred_width_for_height(height, child_minimum_width, child_natural_width);
451 if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
452 minimum_width = std::max(minimum_width, child_minimum_width);
453 natural_width = std::max(natural_width, child_natural_width);
454 } else {
455 minimum_width += child_minimum_width;
456 natural_width += child_natural_width;
457 }
458 }
459 }
460 }
461
get_preferred_height_for_width_vfunc(int width,int & minimum_height,int & natural_height) const462 void DialogMultipaned::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const
463 {
464 minimum_height = 0;
465 natural_height = 0;
466 for (auto const &child : children) {
467 if (child && child->is_visible()) {
468 int child_minimum_height = 0;
469 int child_natural_height = 0;
470 child->get_preferred_height_for_width(width, child_minimum_height, child_natural_height);
471 if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
472 minimum_height = std::max(minimum_height, child_minimum_height);
473 natural_height = std::max(natural_height, child_natural_height);
474 } else {
475 minimum_height += child_minimum_height;
476 natural_height += child_natural_height;
477 }
478 }
479 }
480 }
481
482 /**
483 * This function allocates the sizes of the children widgets (be them internal or not) from
484 * the container's allocated size.
485 *
486 * Natural width: The width the widget really wants.
487 * Minimum width: The minimum width for a widget to be useful.
488 * Minimum <= Natural.
489 */
on_size_allocate(Gtk::Allocation & allocation)490 void DialogMultipaned::on_size_allocate(Gtk::Allocation &allocation)
491 {
492 set_allocation(allocation);
493 bool horizontal = get_orientation() == Gtk::ORIENTATION_HORIZONTAL;
494
495 if (handle != -1) { // Exchange allocation between the widgets on either side of moved handle
496 // Allocation values calculated in on_drag_update();
497 children[handle - 1]->size_allocate(allocation1);
498 children[handle]->size_allocate(allocationh);
499 children[handle + 1]->size_allocate(allocation2);
500 } else {
501 std::vector<bool> expandables; // Is child expandable?
502 std::vector<int> sizes_minimums; // Difference between allocated space and minimum space.
503 std::vector<int> sizes_naturals; // Difference between allocated space and natural space.
504 std::vector<int> sizes(children.size(), 0); // The new allocation sizes
505 std::vector<int> sizes_current; // The current sizes along main axis
506 int left = horizontal ? allocation.get_width() : allocation.get_height();
507
508 int index = 0;
509
510 int canvas_index = -1;
511 for (auto &child : children) {
512 bool visible;
513 DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child);
514 Inkscape::UI::Widget::CanvasGrid *canvas = dynamic_cast<Inkscape::UI::Widget::CanvasGrid *>(child);
515 if (canvas) {
516 canvas_index = index;
517 }
518 if (hide_multipaned && paned) {
519 visible = false;
520 expandables.push_back(false);
521 sizes_minimums.push_back(0);
522 sizes_naturals.push_back(0);
523 } else {
524 visible = child->get_visible();
525 expandables.push_back(child->compute_expand(get_orientation()));
526
527 Gtk::Requisition req_minimum;
528 Gtk::Requisition req_natural;
529 child->get_preferred_size(req_minimum, req_natural);
530
531 sizes_minimums.push_back(visible ? horizontal ? req_minimum.width : req_minimum.height : 0);
532 sizes_naturals.push_back(visible ? horizontal ? req_natural.width : req_natural.height : 0);
533 }
534
535 Gtk::Allocation child_allocation = child->get_allocation();
536 sizes_current.push_back(visible ? horizontal ? child_allocation.get_width() : child_allocation.get_height()
537 : 0);
538 index++;
539 }
540
541 // Precalculate the minimum, natural and current totals
542 int sum_minimums = std::accumulate(sizes_minimums.begin(), sizes_minimums.end(), 0);
543 int sum_naturals = std::accumulate(sizes_naturals.begin(), sizes_naturals.end(), 0);
544 int sum_current = std::accumulate(sizes_current.begin(), sizes_current.end(), 0);
545
546 if (sum_naturals <= left) {
547 sizes = sizes_naturals;
548 left -= sum_naturals;
549 } else if (sum_minimums <= left && left < sum_naturals) {
550 sizes = sizes_minimums;
551 left -= sum_minimums;
552 }
553
554 if (canvas_index >= 0) { // give remaining space to canvas element
555 sizes[canvas_index] += left;
556 } else { // or, if in a sub-dialogmultipaned, give it evenly to widgets
557
558 int d = 0;
559 for (int i = 0; i < (int)children.size(); ++i) {
560 if (expandables[i]) {
561 d++;
562 }
563 }
564
565 if (d > 0) {
566 int idx = 0;
567 for (int i = 0; i < (int)children.size(); ++i) {
568 if (expandables[i]) {
569 sizes[i] += (left / d);
570 if (idx < (left % d))
571 sizes[i]++;
572 idx++;
573 }
574 }
575 }
576 }
577 left = 0;
578
579 // Check if we actually need to change the sizes on the main axis
580 left = horizontal ? allocation.get_width() : allocation.get_height();
581 if (left == sum_current) {
582 bool valid = true;
583 for (int i = 0; i < (int)children.size(); ++i) {
584 valid = valid && (sizes_minimums[i] <= sizes_current[i]) && // is it over the minimums?
585 (expandables[i] || sizes_current[i] <= sizes_naturals[i]); // but does it want to be expanded?
586 if (!valid)
587 break;
588 }
589 if (valid)
590 sizes = sizes_current; // The current sizes are good, don't change anything;
591 }
592
593 // Set x and y values of allocations (widths should be correct).
594 int current_x = allocation.get_x();
595 int current_y = allocation.get_y();
596
597 // Allocate
598 for (int i = 0; i < (int)children.size(); ++i) {
599 Gtk::Allocation child_allocation = children[i]->get_allocation();
600
601 child_allocation.set_x(current_x);
602 child_allocation.set_y(current_y);
603
604 int size = sizes[i];
605
606 if (horizontal) {
607 child_allocation.set_width(size);
608 current_x += size;
609 child_allocation.set_height(allocation.get_height());
610 } else {
611 child_allocation.set_height(size);
612 current_y += size;
613 child_allocation.set_width(allocation.get_width());
614 }
615
616 children[i]->size_allocate(child_allocation);
617 }
618 }
619 }
620
forall_vfunc(gboolean,GtkCallback callback,gpointer callback_data)621 void DialogMultipaned::forall_vfunc(gboolean, GtkCallback callback, gpointer callback_data)
622 {
623 for (auto const &child : children) {
624 if (child) {
625 callback(child->gobj(), callback_data);
626 }
627 }
628 }
629
on_add(Gtk::Widget * child)630 void DialogMultipaned::on_add(Gtk::Widget *child)
631 {
632 if (child) {
633 append(child);
634 }
635 }
636
637 /**
638 * Callback when a widget is removed from DialogMultipaned and executes the removal.
639 * It does not remove handles or dropzones.
640 */
on_remove(Gtk::Widget * child)641 void DialogMultipaned::on_remove(Gtk::Widget *child)
642 {
643 if (child) {
644 MyDropZone *dropzone = dynamic_cast<MyDropZone *>(child);
645 if (dropzone) {
646 return;
647 }
648 MyHandle *my_handle = dynamic_cast<MyHandle *>(child);
649 if (my_handle) {
650 return;
651 }
652
653 const bool visible = child->get_visible();
654 if (children.size() > 2) {
655 auto it = std::find(children.begin(), children.end(), child);
656 if (it != children.end()) { // child found
657 if (it + 2 != children.end()) { // not last widget
658 my_handle = dynamic_cast<MyHandle *>(*(it + 1));
659 my_handle->unparent();
660 child->unparent();
661 children.erase(it, it + 2);
662 } else { // last widget
663 if (children.size() == 3) { // only widget
664 child->unparent();
665 children.erase(it);
666 } else { // not only widget, delete preceding handle
667 my_handle = dynamic_cast<MyHandle *>(*(it - 1));
668 my_handle->unparent();
669 child->unparent();
670 children.erase(it - 1, it + 1);
671 }
672 }
673 }
674 }
675 if (visible) {
676 queue_resize();
677 }
678
679 if (children.size() == 2) {
680 add_empty_widget();
681 _empty_widget->set_size_request(300, -1);
682 _signal_now_empty.emit();
683 }
684 }
685 }
686
on_drag_begin(double start_x,double start_y)687 void DialogMultipaned::on_drag_begin(double start_x, double start_y)
688 {
689 // We clicked on handle.
690 bool found = false;
691 int child_number = 0;
692 Gtk::Allocation allocation = get_allocation();
693 for (auto const &child : children) {
694 MyHandle *my_handle = dynamic_cast<MyHandle *>(child);
695 if (my_handle) {
696 Gtk::Allocation child_allocation = my_handle->get_allocation();
697
698 // Did drag start in handle?
699 int x = child_allocation.get_x() - allocation.get_x();
700 int y = child_allocation.get_y() - allocation.get_y();
701 if (x < start_x && start_x < x + child_allocation.get_width() && y < start_y &&
702 start_y < y + child_allocation.get_height()) {
703 found = true;
704 break;
705 }
706 }
707 ++child_number;
708 }
709
710 if (!found) {
711 gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
712 return;
713 }
714
715 if (child_number < 1 || child_number > (int)(children.size() - 2)) {
716 std::cerr << "DialogMultipaned::on_drag_begin: Invalid child (" << child_number << "!!" << std::endl;
717 gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
718 return;
719 }
720
721 gesture->set_state(Gtk::EVENT_SEQUENCE_CLAIMED);
722
723 // Save for use in on_drag_update().
724 handle = child_number;
725 start_allocation1 = children[handle - 1]->get_allocation();
726 start_allocationh = children[handle]->get_allocation();
727 start_allocation2 = children[handle + 1]->get_allocation();
728 }
729
on_drag_end(double offset_x,double offset_y)730 void DialogMultipaned::on_drag_end(double offset_x, double offset_y)
731 {
732 gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
733 handle = -1;
734 }
735
on_drag_update(double offset_x,double offset_y)736 void DialogMultipaned::on_drag_update(double offset_x, double offset_y)
737 {
738 allocation1 = children[handle - 1]->get_allocation();
739 allocationh = children[handle]->get_allocation();
740 allocation2 = children[handle + 1]->get_allocation();
741 int minimum_size;
742 int natural_size;
743
744 // HACK: The bias prevents erratic resizing when dragging the handle fast, outside the bounds of the app.
745 const int BIAS = 1;
746
747 if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
748 children[handle - 1]->get_preferred_width(minimum_size, natural_size);
749 if (start_allocation1.get_width() + offset_x < minimum_size)
750 offset_x = -(start_allocation1.get_width() - minimum_size) + BIAS;
751 children[handle + 1]->get_preferred_width(minimum_size, natural_size);
752 if (start_allocation2.get_width() - offset_x < minimum_size)
753 offset_x = start_allocation2.get_width() - minimum_size - BIAS;
754
755 allocation1.set_width(start_allocation1.get_width() + offset_x);
756 allocationh.set_x(start_allocationh.get_x() + offset_x);
757 allocation2.set_x(start_allocation2.get_x() + offset_x);
758 allocation2.set_width(start_allocation2.get_width() - offset_x);
759 } else {
760 children[handle - 1]->get_preferred_height(minimum_size, natural_size);
761 if (start_allocation1.get_height() + offset_y < minimum_size)
762 offset_y = -(start_allocation1.get_height() - minimum_size) + BIAS;
763 children[handle + 1]->get_preferred_height(minimum_size, natural_size);
764 if (start_allocation2.get_height() - offset_y < minimum_size)
765 offset_y = start_allocation2.get_height() - minimum_size - BIAS;
766
767 allocation1.set_height(start_allocation1.get_height() + offset_y);
768 allocationh.set_y(start_allocationh.get_y() + offset_y);
769 allocation2.set_y(start_allocation2.get_y() + offset_y);
770 allocation2.set_height(start_allocation2.get_height() - offset_y);
771 }
772
773 if (hide_multipaned) {
774 DialogMultipaned *left = dynamic_cast<DialogMultipaned *>(children[handle - 1]);
775 DialogMultipaned *right = dynamic_cast<DialogMultipaned *>(children[handle + 1]);
776
777 if (left || right) {
778 return;
779 }
780 }
781
782 queue_allocate(); // Relayout DialogMultipaned content.
783 }
784
set_target_entries(const std::vector<Gtk::TargetEntry> & target_entries)785 void DialogMultipaned::set_target_entries(const std::vector<Gtk::TargetEntry> &target_entries)
786 {
787 drag_dest_set(target_entries);
788 ((MyDropZone *)children[0])->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
789 ((MyDropZone *)children[children.size() - 1])
790 ->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
791 }
792
on_drag_data(const Glib::RefPtr<Gdk::DragContext> context,int x,int y,const Gtk::SelectionData & selection_data,guint info,guint time)793 void DialogMultipaned::on_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
794 const Gtk::SelectionData &selection_data, guint info, guint time)
795 {
796 _signal_prepend_drag_data.emit(context);
797 }
798
on_prepend_drag_data(const Glib::RefPtr<Gdk::DragContext> context,int x,int y,const Gtk::SelectionData & selection_data,guint info,guint time)799 void DialogMultipaned::on_prepend_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
800 const Gtk::SelectionData &selection_data, guint info, guint time)
801 {
802 _signal_prepend_drag_data.emit(context);
803 }
804
on_append_drag_data(const Glib::RefPtr<Gdk::DragContext> context,int x,int y,const Gtk::SelectionData & selection_data,guint info,guint time)805 void DialogMultipaned::on_append_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
806 const Gtk::SelectionData &selection_data, guint info, guint time)
807 {
808 _signal_append_drag_data.emit(context);
809 }
810
811 // Signals
signal_prepend_drag_data()812 sigc::signal<void, const Glib::RefPtr<Gdk::DragContext>> DialogMultipaned::signal_prepend_drag_data()
813 {
814 resize_children();
815 return _signal_prepend_drag_data;
816 }
817
signal_append_drag_data()818 sigc::signal<void, const Glib::RefPtr<Gdk::DragContext>> DialogMultipaned::signal_append_drag_data()
819 {
820 resize_children();
821 return _signal_append_drag_data;
822 }
823
signal_now_empty()824 sigc::signal<void> DialogMultipaned::signal_now_empty()
825 {
826 return _signal_now_empty;
827 }
828
829 } // namespace Dialog
830 } // namespace UI
831 } // namespace Inkscape
832
833 /*
834 Local Variables:
835 mode:c++
836 c-file-style:"stroustrup"
837 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
838 indent-tabs-mode:nil
839 fill-column:99
840 End:
841 */
842 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
843