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