Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
dialog-multipaned.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2
14#include <algorithm>
15#include <cassert>
16#include <iostream>
17#include <numeric>
18#include <glibmm/i18n.h>
19#include <glibmm/value.h>
20#include <gtkmm/box.h>
21#include <gtkmm/drawingarea.h>
22#include <gtkmm/droptarget.h>
23#include <gtkmm/eventcontrollermotion.h>
24#include <gtkmm/gestureclick.h>
25#include <gtkmm/gesturedrag.h>
26#include <gtkmm/image.h>
27#include <gtkmm/label.h>
28#include <gtkmm/overlay.h>
29#include <sigc++/functors/mem_fun.h>
30
31#include "dialog-multipaned.h"
32#include "dialog-window.h"
33#include "ui/controller.h"
35#include "ui/util.h"
37
38static constexpr int DROPZONE_SIZE = 5;
39static constexpr int DROPZONE_EXPANSION = 15;
40static constexpr int HANDLE_SIZE = 10; // note: match size of handle icon to avoid stretching
41static constexpr int HANDLE_CROSS_SIZE = 25;
42
43namespace Inkscape::UI::Dialog {
44
45/*
46 * References:
47 * https://blog.gtk.org/2017/06/
48 * https://developer.gnome.org/gtkmm-tutorial/stable/sec-custom-containers.html.en
49 * https://wiki.gnome.org/HowDoI/Gestures
50 *
51 * The children widget sizes are "sticky". They change a minimal
52 * amount when the parent widget is resized or a child is added or
53 * removed.
54 *
55 * A gesture is used to track handle movement. This must be attached
56 * to the parent widget (the offset_x/offset_y values are relative to
57 * the widget allocation which changes for the handles as they are
58 * moved).
59 */
60
62 return HANDLE_SIZE;
63}
64
65/* ============ MyDropZone ============ */
66
70class MyDropZone final
71 : public Gtk::Box
72{
73public:
74 MyDropZone(Gtk::Orientation orientation);
75 ~MyDropZone() final;
76
77 void set_gtypes(std::vector<GType> const &gtypes);
78 using DropSlot = sigc::slot<bool (Glib::ValueBase const &, double, double)>;
79 sigc::connection connect_drop(DropSlot slot);
80
81 static void add_highlight_instances();
82 static void remove_highlight_instances();
83
84private:
85 void set_size(int size);
86 bool _active = false;
87 void add_highlight();
88 void remove_highlight();
89
90 Glib::RefPtr<Gtk::DropTarget> const _zone_drop_target;
91
92 static std::vector<MyDropZone *> _instances_list;
93 friend class DialogMultipaned;
94};
95
96std::vector<MyDropZone *> MyDropZone::_instances_list;
97
98MyDropZone::MyDropZone(Gtk::Orientation orientation)
99 : Glib::ObjectBase("MultipanedDropZone")
100 , Gtk::Box{orientation}
101 , _zone_drop_target{Gtk::DropTarget::create(G_TYPE_INVALID, Gdk::DragAction::MOVE)}
102{
103 set_name("MultipanedDropZone");
104 set_size(DROPZONE_SIZE);
105
106 add_css_class("backgnd-passive");
107
108 _zone_drop_target->signal_motion().connect([this](double x, double y){
109 if (!_active) {
110 _active = true;
111 add_highlight();
113 }
114 return Gdk::DragAction::MOVE;
115 }, false); // before
116
117 _zone_drop_target->signal_leave().connect([this]{
118 if (_active) {
119 _active = false;
120 set_size(DROPZONE_SIZE);
121 }
122 }, false); // before
123 add_controller(_zone_drop_target);
124
125 _instances_list.push_back(this);
126}
127
128MyDropZone::~MyDropZone()
129{
130 auto const it = std::find(_instances_list.cbegin(), _instances_list.cend(), this);
131 assert(it != _instances_list.cend());
132 _instances_list.erase(it);
133}
134
135void MyDropZone::set_gtypes(std::vector<GType> const &gtypes)
136{
137 _zone_drop_target->set_gtypes(gtypes);
138}
139
140sigc::connection MyDropZone::connect_drop(DropSlot slot)
141{
142 return _zone_drop_target->signal_drop().connect(std::move(slot), false); // before
143}
144
145void MyDropZone::add_highlight_instances()
146{
147 for (auto *instance : _instances_list) {
148 instance->add_highlight();
149 }
150}
151
152void MyDropZone::remove_highlight_instances()
153{
154 for (auto *instance : _instances_list) {
155 instance->remove_highlight();
156 }
157}
158
159void MyDropZone::add_highlight()
160{
161 remove_css_class("backgnd-passive");
162 add_css_class ("backgnd-active" );
163}
164
165void MyDropZone::remove_highlight()
166{
167 remove_css_class("backgnd-active" );
168 add_css_class ("backgnd-passive");
169}
170
171void MyDropZone::set_size(int size)
172{
173 if (get_orientation() == Gtk::Orientation::HORIZONTAL) {
174 set_size_request(size, -1);
175 } else {
176 set_size_request(-1, size);
177 }
178}
179
180/* ============ MyHandle ============ */
181
185class MyHandle final
186 : public Gtk::Orientable
187 , public Gtk::Overlay
188{
189public:
190 MyHandle(Gtk::Orientation orientation, int size);
191 ~MyHandle() final = default;
192
193 void set_dragging (bool dragging);
194 void set_drag_updated(bool updated );
195
196private:
197 void on_motion_enter (double x, double y);
198 void on_motion_motion(double x, double y);
199 void on_motion_leave ();
200
201 Gtk::EventSequenceState on_click_pressed (Gtk::GestureClick const &gesture);
202 Gtk::EventSequenceState on_click_released(Gtk::GestureClick const &gesture);
203
204 void toggle_multipaned();
205 void update_click_indicator(double x, double y);
206 void show_click_indicator(bool show);
207 void draw_func(Cairo::RefPtr<Cairo::Context> const &cr, int width, int height);
208 Cairo::Rectangle get_active_click_zone();
209
210 Gtk::DrawingArea * const _drawing_area;
211 int _cross_size;
212 Gtk::Widget *_child;
213
214 void size_allocate_vfunc(int width, int height, int baseline) final;
215
216 bool is_click_resize_active() const;
217 bool _click = false;
218 bool _click_indicator = false;
219
220 bool _dragging = false;
221 bool _drag_updated = false;
222};
223
224MyHandle::MyHandle(Gtk::Orientation orientation, int size = get_handle_size())
225 : Glib::ObjectBase("MultipanedHandle")
226 , Gtk::Orientable()
227 , Gtk::Overlay{}
228 , _drawing_area{Gtk::make_managed<Gtk::DrawingArea>()}
229 , _cross_size(0)
230 , _child(nullptr)
231{
232 set_name("MultipanedHandle");
233 set_orientation(orientation);
234
235 auto const image = Gtk::make_managed<Gtk::Image>();
236 if (get_orientation() == Gtk::Orientation::HORIZONTAL) {
237 // vertical splitter resizing content horizontally
238 image->set_from_icon_name("resizing-handle-vertical-symbolic");
239 set_size_request(size, -1);
240 } else {
241 // horizontal splitter resizing content vertically
242 image->set_from_icon_name("resizing-handle-horizontal-symbolic");
243 set_size_request(-1, size);
244 }
245 image->set_pixel_size(size);
246 set_child(*image);
247
248 _drawing_area->set_draw_func(sigc::mem_fun(*this, &MyHandle::draw_func));
249 add_overlay(*_drawing_area);
250
251 auto const motion = Gtk::EventControllerMotion::create();
252 motion->set_propagation_phase(Gtk::PropagationPhase::TARGET);
253 motion->signal_enter().connect(sigc::mem_fun(*this, &MyHandle::on_motion_enter));
254 motion->signal_motion().connect(sigc::mem_fun(*this, &MyHandle::on_motion_motion));
255 motion->signal_leave().connect(sigc::mem_fun(*this, &MyHandle::on_motion_leave));
256 _drawing_area->add_controller(motion);
257
258 auto const click = Gtk::GestureClick::create();
259 click->set_button(0); // any
260 click->set_propagation_phase(Gtk::PropagationPhase::TARGET);
261 click->signal_pressed().connect(Controller::use_state([this, &click = *click](auto &&...) { return on_click_pressed(click); }, *click));
262 click->signal_released().connect(Controller::use_state([this, &click = *click](auto &&...) { return on_click_released(click); }, *click));
263 _drawing_area->add_controller(click);
264}
265
266// draw rectangle with rounded corners
267void rounded_rectangle(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double w, double h, double r) {
268 cr->begin_new_sub_path();
269 cr->arc(x + r, y + r, r, M_PI, 3 * M_PI / 2);
270 cr->arc(x + w - r, y + r, r, 3 * M_PI / 2, 2 * M_PI);
271 cr->arc(x + w - r, y + h - r, r, 0, M_PI / 2);
272 cr->arc(x + r, y + h - r, r, M_PI / 2, M_PI);
273 cr->close_path();
274}
275
276// part of the handle where clicking makes it automatically collapse/expand docked dialogs
277Cairo::Rectangle MyHandle::get_active_click_zone() {
278 const Gtk::Allocation& allocation = get_allocation();
279 double width = allocation.get_width();
280 double height = allocation.get_height();
281 double h = height / 5;
282 Cairo::Rectangle rect = { .x = 0, .y = (height - h) / 2, .width = width, .height = h };
283 return rect;
284}
285
286void MyHandle::draw_func(Cairo::RefPtr<Cairo::Context> const &cr, int /*width*/, int /*height*/)
287{
288 // show click indicator/highlight?
289 if (_click_indicator && is_click_resize_active() && !_dragging) {
290 auto rect = get_active_click_zone();
291 if (rect.width > 4 && rect.height > 0) {
292 auto const fg = get_color();
293 rounded_rectangle(cr, rect.x + 2, rect.y, rect.width - 4, rect.height, 3);
294 cr->set_source_rgba(fg.get_red(), fg.get_green(), fg.get_blue(), 0.18);
295 cr->fill();
296 }
297 }
298}
299
300void MyHandle::set_dragging(bool dragging) {
301 if (_dragging != dragging) {
302 _dragging = dragging;
303 if (_click_indicator) {
304 _drawing_area->queue_draw();
305 }
306 }
307}
308
309void MyHandle::set_drag_updated(bool const updated) {
310 _drag_updated = updated;
311}
312
316void MyHandle::on_motion_enter(double x, double y)
317{
318 if (get_orientation() == Gtk::Orientation::HORIZONTAL) {
319 set_cursor("col-resize");
320 } else {
321 set_cursor("row-resize");
322 }
323
324 update_click_indicator(x, y);
325}
326
327void MyHandle::on_motion_leave()
328{
329 set_cursor("");
330 show_click_indicator(false);
331}
332
333void MyHandle::show_click_indicator(bool show) {
334 if (!is_click_resize_active()) return;
335
336 if (show != _click_indicator) {
337 _click_indicator = show;
338 _drawing_area->queue_draw();
339 }
340}
341
342void MyHandle::update_click_indicator(double x, double y) {
343 if (!is_click_resize_active()) return;
344
345 auto rect = get_active_click_zone();
346 bool inside =
347 x >= rect.x && x < rect.x + rect.width &&
348 y >= rect.y && y < rect.y + rect.height;
349
350 show_click_indicator(inside);
351}
352
353bool MyHandle::is_click_resize_active() const {
354 return get_orientation() == Gtk::Orientation::HORIZONTAL;
355}
356
357Gtk::EventSequenceState MyHandle::on_click_pressed(Gtk::GestureClick const &gesture)
358{
359 // Detect single-clicks, except after a (moving/updated) drag
360 _click = !_drag_updated && gesture.get_current_button() == 1;
361 set_drag_updated(false);
362 return Gtk::EventSequenceState::NONE;
363}
364
365Gtk::EventSequenceState MyHandle::on_click_released(Gtk::GestureClick const &gesture)
366{
367 // single-click on active zone?
368 if (_click && gesture.get_current_button() == 1 && _click_indicator) {
369 _click = false;
370 _dragging = false;
371 // handle clicked
372 if (is_click_resize_active()) {
373 toggle_multipaned();
374 return Gtk::EventSequenceState::CLAIMED;
375 }
376 }
377
378 _click = false;
379 return Gtk::EventSequenceState::NONE;
380}
381
382void MyHandle::toggle_multipaned() {
383 // visibility toggle of multipaned in a floating dialog window doesn't make sense; skip
384 if (dynamic_cast<DialogWindow*>(get_root())) return;
385
386 auto panel = dynamic_cast<DialogMultipaned*>(get_parent());
387 if (!panel) return;
388
389 auto const &children = panel->get_multipaned_children();
390 Gtk::Widget* multi = nullptr; // multipaned widget to toggle
391 bool left_side = true; // panels to the left of canvas
392 size_t i = 0;
393
394 // find multipaned widget to resize; it is adjacent (sibling) to 'this' handle in widget hierarchy
395 for (auto const &widget : children) {
396 if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(widget.get())) {
397 // widget past canvas are on the right side (of canvas)
398 left_side = false;
399 }
400
401 if (widget.get() == this) {
402 if (left_side && i > 0) {
403 // handle to the left of canvas toggles preceeding panel
404 multi = dynamic_cast<DialogMultipaned*>(children[i - 1].get());
405 }
406 else if (!left_side && i + 1 < children.size()) {
407 // handle to the right of canvas toggles next panel
408 multi = dynamic_cast<DialogMultipaned*>(children[i + 1].get());
409 }
410
411 if (multi) {
412 if (multi->is_visible()) {
413 multi->set_visible(false);
414 }
415 else {
416 multi->set_visible(true);
417 }
418 // resize parent
419 panel->children_toggled();
420 }
421 break;
422 }
423
424 ++i;
425 }
426}
427
428void MyHandle::on_motion_motion(double x, double y)
429{
430 // motion invalidates click; it activates resizing
431 _click = false;
432 update_click_indicator(x, y);
433}
434
442void MyHandle::size_allocate_vfunc(int const width, int const height, int const baseline)
443{
444 Gtk::Overlay::size_allocate_vfunc(width, height, baseline);
445
446 auto const size = get_orientation() == Gtk::Orientation::HORIZONTAL ? height : width;
447
448 if (_cross_size > size && HANDLE_CROSS_SIZE > size && !_child) {
449 _child = get_child();
450 unset_child();
451 } else if (_cross_size < size && HANDLE_CROSS_SIZE < size && _child) {
452 set_child(*_child);
453 _child = nullptr;
454 }
455
456 _cross_size = size;
457}
458
459/* ============ DialogMultipaned ============= */
460
461DialogMultipaned::DialogMultipaned(Gtk::Orientation orientation)
462 : Glib::ObjectBase("DialogMultipaned")
463 , Gtk::Orientable()
464 , Gtk::Widget()
465{
466 set_name("DialogMultipaned");
467 set_orientation(orientation);
468 set_hexpand(true); // TODO: GTK4: seems to need this. Is it the correct fix?
469
470 // ============= Add dropzones ==============
471 auto const dropzone_s = static_cast<MyDropZone *>(_children.emplace_back(std::make_unique<MyDropZone>(orientation)).get());
472 auto const dropzone_e = static_cast<MyDropZone *>(_children.emplace_back(std::make_unique<MyDropZone>(orientation)).get());
473 dropzone_s->set_parent(*this);
474 dropzone_e->set_parent(*this);
475
476 // ============ Connect signals =============
477 // dialog resizing handle:
478 auto const drag = Gtk::GestureDrag::create();
479 drag->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
480 drag->signal_drag_begin() .connect(Controller::use_state([this](auto&, auto &&...args) { return on_drag_begin(args...); }, *drag));
481 drag->signal_drag_update().connect(Controller::use_state([this](auto&, auto &&...args) { return on_drag_update(args...); }, *drag));
482 drag->signal_drag_end() .connect(Controller::use_state([this](auto&, auto &&...args) { return on_drag_end(args...); }, *drag));
483 add_controller(drag);
484
485 _connections.emplace_back(
486 _drop_target->signal_drop().connect(
487 sigc::mem_fun(*this, &DialogMultipaned::on_drag_data_drop), false)); // before
488 _connections.emplace_back(
489 dropzone_s->connect_drop(sigc::mem_fun(*this, &DialogMultipaned::on_prepend_drag_data)));
490 _connections.emplace_back(
491 dropzone_e->connect_drop(sigc::mem_fun(*this, &DialogMultipaned::on_append_drag_data)));
492
493 add_controller(_drop_target);
494
495 // add empty widget to initiate the container
497}
498
500{
501 // Remove widgets that require special logic to remove.
502 // TODO: Understand why this is necessary.
503 while (true) {
504 auto const it = std::find_if(begin(_children), end(_children), [] (auto const &w) {
505 return dynamic_cast<DialogMultipaned *>(w.get()) || dynamic_cast<DialogNotebook *>(w.get());
506 });
507 if (it != _children.end()) {
508 remove(**it);
509 } else {
510 // no more dialog panels
511 break;
512 }
513 }
514
515 // Remove remaining widgets (DropZones, CanvasGrid).
516 for (auto const &child : _children) {
517 g_assert(child->get_parent() == this);
518 child->unparent();
519 }
520
521 _children.clear();
522}
523
524void DialogMultipaned::insert(int const pos, std::unique_ptr<Gtk::Widget> child)
525{
526 auto const parent = child->get_parent();
527 g_assert(!parent || parent == this);
528
529 // Zero/positive pos means insert @children[pos]. Negative means @children[children.size()-pos]
530 // We update children, so we must get iterator anew each time it is to be used. Check bound too
531 g_assert(pos >= 0 && pos <= _children.size() || // (prepending) inserting at 1-past-end is A-OK
532 pos < 0 && -pos <= _children.size()); // (appending) inserting@ 1-before-begin is NOT
533 auto const get_iter = [&]{ return (pos >= 0 ? _children.begin() : _children.end()) + pos; };
534
535 remove_empty_widget(); // Will remove extra widget if existing
536
537 // If there are MyMultipane children that are empty, they will be removed
538 while (true) {
539 auto const it = std::find_if(begin(_children), end(_children), [] (auto const &w) {
540 if (auto const paned = dynamic_cast<DialogMultipaned *>(w.get())) {
541 return paned->has_empty_widget();
542 }
543 return false;
544 });
545 if (it != _children.end()) {
546 remove(**it);
548 } else {
549 break;
550 }
551 }
552
553 // Add handle
554 if (_children.size() > 2) {
555 auto my_handle = std::make_unique<MyHandle>(get_orientation());
556 my_handle->set_parent(*this);
557 _children.insert(get_iter(), std::move(my_handle));
558 }
559
560 // Add child
561 if (!parent) {
562 child->set_parent(*this);
563 }
564 _children.insert(get_iter(), std::move(child));
565}
566
567void DialogMultipaned::prepend(std::unique_ptr<Gtk::Widget> child)
568{
569 insert(+1, std::move(child)); // After start dropzone
570}
571
572void DialogMultipaned::append(std::unique_ptr<Gtk::Widget> child)
573{
574 insert(-1, std::move(child)); // Before end dropzone
575}
576
578{
579 const int EMPTY_WIDGET_SIZE = 60; // magic number
580
581 // The empty widget is a label
582 auto label = std::make_unique<Gtk::Label>(_("You can drop dockable dialogs here."));
583 label->set_wrap();
584 label->set_justify(Gtk::Justification::CENTER);
585 label->set_valign(Gtk::Align::CENTER);
586 label->set_vexpand();
587
588 auto l = label.get();
589 append(std::move(label));
590 _empty_widget = l;
591
592 if (get_orientation() == Gtk::Orientation::VERTICAL) {
593 int dropzone_size = (get_height() - EMPTY_WIDGET_SIZE) / 2;
594 if (dropzone_size > DROPZONE_SIZE) {
595 set_dropzone_sizes(dropzone_size, dropzone_size);
596 }
597 }
598}
599
601{
602 if (_empty_widget) {
603 auto it = std::find_if(begin(_children), end(_children), [this] (auto &p) { return p.get() == _empty_widget; });
604 if (it != _children.end()) {
605 _empty_widget->unparent();
606 _children.erase(it);
607 }
608 _empty_widget = nullptr;
609 }
610
611 if (get_orientation() == Gtk::Orientation::VERTICAL) {
613 }
614}
615
617{
618 if (_children.size() > 2) {
619 return _children[1].get();
620 } else {
621 return nullptr;
622 }
623}
624
626{
627 if (_children.size() > 2) {
628 return _children[_children.size() - 2].get();
629 } else {
630 return nullptr;
631 }
632}
633
640{
641 bool horizontal = get_orientation() == Gtk::Orientation::HORIZONTAL;
642
643 if (start == -1) {
645 }
646
647 auto dropzone_s = dynamic_cast<MyDropZone *>(_children.front().get());
648
649 if (dropzone_s) {
650 if (horizontal) {
651 dropzone_s->set_size_request(start, -1);
652 } else {
653 dropzone_s->set_size_request(-1, start);
654 }
655 }
656
657 if (end == -1) {
659 }
660
661 auto dropzone_e = dynamic_cast<MyDropZone *>(_children.back().get());
662
663 if (dropzone_e) {
664 if (horizontal) {
665 dropzone_e->set_size_request(end, -1);
666 } else {
667 dropzone_e->set_size_request(-1, end);
668 }
669 }
670}
671
676{
677 _handle = -1;
678 _drag_handle = -1;
679
680 for (auto const &child : _children) {
681 if (auto panel = dynamic_cast<DialogMultipaned*>(child.get())) {
682 panel->set_visible(show);
683 }
684 }
685}
686
691{
693 // hide_multipaned = false;
694 // queue_allocate();
695}
696
697// ****************** OVERRIDES ******************
698
699// The following functions are here to define the behavior of our custom container
700
702{
703 if (get_orientation() == Gtk::Orientation::HORIZONTAL) {
704 return Gtk::SizeRequestMode::WIDTH_FOR_HEIGHT;
705 } else {
706 return Gtk::SizeRequestMode::HEIGHT_FOR_WIDTH;
707 }
708}
709
710void DialogMultipaned::measure_vfunc(Gtk::Orientation const orientation, int const for_size,
711 int &minimum, int &natural,
712 int &minimum_baseline, int &natural_baseline) const
713{
714 minimum = 0;
715 natural = 0;
716 minimum_baseline = -1;
717 natural_baseline = -1;
718
719 for (auto const &child : _children) {
720 if (child && child->is_visible()) {
721 int child_minimum, child_natural, ignore;
722 child->measure(orientation, for_size, child_minimum, child_natural, ignore, ignore);
723 if (get_orientation() != orientation) {
724 minimum = std::max(minimum, child_minimum);
725 natural = std::max(natural, child_natural);
726 } else {
727 minimum += child_minimum;
728 natural += child_natural;
729 }
730 }
731 }
732
733 if (orientation == Gtk::Orientation::HORIZONTAL) {
734 natural = std::max(natural, _natural_width);
735 }
736}
737
739 _handle = -1;
740 _drag_handle = -1;
741 queue_allocate();
742}
743
744[[nodiscard]] static int _get_size(auto const sizes, Gtk::Orientation const orientation)
745{
746 return orientation == Gtk::Orientation::HORIZONTAL ? sizes.get_width () : sizes.get_height();
747}
748
757void DialogMultipaned::size_allocate_vfunc(int const width, int const height, int const baseline)
758{
759 Gtk::Widget::size_allocate_vfunc(width, height, baseline);
760
761 auto const allocation = Gdk::Rectangle{0, 0, width, height};
762 auto const orientation = get_orientation();
763
764 if (_drag_handle != -1) { // Exchange allocation between the widgets on either side of moved handle
765 // Allocation values calculated in on_drag_update();
766 _children[_drag_handle - 1]->size_allocate(allocation1, baseline);
767 _children[_drag_handle ]->size_allocate(allocationh, baseline);
768 _children[_drag_handle + 1]->size_allocate(allocation2, baseline);
769 _drag_handle = -1;
770 }
771 // initially widgets get created with a 1x1 size; ignore it and wait for the final resize
772 else if (allocation.get_width() > 1 && allocation.get_height() > 1) {
773 _natural_width = allocation.get_width();
774 }
775
776 std::vector<bool> expandables; // Is child expandable?
777 std::vector<int> sizes_minimums; // Difference between allocated space and minimum space.
778 std::vector<int> sizes_naturals; // Difference between allocated space and natural space.
779 std::vector<int> sizes_current; // The current sizes along main axis
780 int left = _get_size(allocation, orientation);
781
782 int index = 0;
783 bool force_resize = false; // initially panels are not sized yet, so we will apply their natural sizes
784 int canvas_index = -1;
785 for (auto const &child : _children) {
786 bool visible = child->get_visible();
787
788 if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child.get())) {
789 canvas_index = index;
790 }
791
792 expandables.push_back(child->compute_expand(get_orientation()));
793
794 Gtk::Requisition req_minimum;
795 Gtk::Requisition req_natural;
796 child->get_preferred_size(req_minimum, req_natural);
797 if (child.get() == _resizing_widget1 || child.get() == _resizing_widget2) {
798 // ignore limits for widget being resized interactively and use their current size
799 req_minimum.set_width (0);
800 req_minimum.set_height(0);
801 auto alloc = child->get_allocation();
802 req_natural.set_width (alloc.get_width ());
803 req_natural.set_height(alloc.get_height());
804 }
805
806 sizes_minimums.push_back(visible ? _get_size(req_minimum, orientation) : 0);
807 sizes_naturals.push_back(visible ? _get_size(req_natural, orientation) : 0);
808
809 Gtk::Allocation child_allocation = child->get_allocation();
810 int size = 0;
811 if (visible) {
812 if (dynamic_cast<MyHandle*>(child.get())) {
813 // resizing handles should never be smaller than their min size:
814 size = _get_size(req_minimum, orientation);
815 }
816 else if (dynamic_cast<MyDropZone*>(child.get())) {
817 // don't upset drop zone sizes
818 size = _get_size(req_minimum, orientation);
819 }
820 else {
821 // all other widgets can get smaller than their min size
822 size = _get_size(child_allocation, orientation);
823 auto const min = _get_size(req_minimum, orientation);
824 auto natural = _get_size(req_natural, orientation);
825 if (size < min) {
826 if (size == 0 && natural >= min) {
827 // initially, widgets don't have their size established yet, so we should
828 // honor natural size request
829 size = natural;
830 }
831 else {
832 size = min;
833 }
834 }
835 }
836 }
837 sizes_current.push_back(size);
838 index++;
839
840 if (sizes_current.back() < sizes_minimums.back()) force_resize = true;
841 }
842
843 std::vector<int> sizes = sizes_current; // The new allocation sizes
844
845 const int sum_current = std::accumulate(sizes_current.begin(), sizes_current.end(), 0);
846 {
847 // Precalculate the minimum, natural and current totals
848 const int sum_minimums = std::accumulate(sizes_minimums.begin(), sizes_minimums.end(), 0);
849 const int sum_naturals = std::accumulate(sizes_naturals.begin(), sizes_naturals.end(), 0);
850
851 // initial resize requested?
852 if (force_resize && sum_naturals <= left) {
853 sizes = sizes_naturals;
854 left -= sum_naturals;
855 } else if (sum_minimums <= left && left < sum_current) {
856 // requested size exeeds available space; try shrinking it by starting from the last element
857 sizes = sizes_current;
858 auto excess = sum_current - left;
859 for (int i = static_cast<int>(sizes.size() - 1); excess > 0 && i >= 0; --i) {
860 auto extra = sizes_current[i] - sizes_minimums[i];
861 if (extra > 0) {
862 if (extra >= excess) {
863 // we are done, enough space found
864 sizes[i] -= excess;
865 excess = 0;
866 }
867 else {
868 // shrink as far as possible, then continue to the next panel
869 sizes[i] -= extra;
870 excess -= extra;
871 }
872 }
873 }
874
875 if (excess > 0) {
876 sizes = sizes_minimums;
877 left -= sum_minimums;
878 }
879 else {
880 left = 0;
881 }
882 }
883 else {
884 left = std::max(0, left - sum_current);
885 }
886 }
887
888 if (canvas_index >= 0) { // give remaining space to canvas element
889 sizes[canvas_index] += left;
890 } else { // or, if in a sub-dialogmultipaned, give it to the last panel
891
892 for (int i = static_cast<int>(_children.size()) - 1; i >= 0; --i) {
893 if (expandables[i]) {
894 sizes[i] += left;
895 break;
896 }
897 }
898 }
899
900 // Check if we actually need to change the sizes on the main axis
901 left = _get_size(allocation, orientation);
902 if (left == sum_current) {
903 bool valid = true;
904 for (size_t i = 0; i < _children.size(); ++i) {
905 valid = sizes_minimums[i] <= sizes_current[i] && // is it over the minimums?
906 (expandables[i] || sizes_current[i] <= sizes_naturals[i]); // but does it want to be expanded?
907 if (!valid)
908 break;
909 }
910 if (valid) {
911 sizes = sizes_current; // The current sizes are good, don't change anything;
912 }
913 }
914
915 // Set x and y values of allocations (widths should be correct).
916 int current_x = allocation.get_x();
917 int current_y = allocation.get_y();
918
919 // Allocate
920 for (size_t i = 0; i < _children.size(); ++i) {
921 Gtk::Allocation child_allocation = _children[i]->get_allocation();
922 child_allocation.set_x(current_x);
923 child_allocation.set_y(current_y);
924
925 int size = sizes[i];
926
927 if (orientation == Gtk::Orientation::HORIZONTAL) {
928 child_allocation.set_width(size);
929 current_x += size;
930 child_allocation.set_height(allocation.get_height());
931 } else {
932 child_allocation.set_height(size);
933 current_y += size;
934 child_allocation.set_width(allocation.get_width());
935 }
936
937 _children[i]->size_allocate(child_allocation, baseline);
938 }
939}
940
945void DialogMultipaned::remove(Gtk::Widget &widget)
946{
947 auto const child = &widget;
948
949 MyDropZone *dropzone = dynamic_cast<MyDropZone *>(child);
950 if (dropzone) {
951 return;
952 }
953 MyHandle *my_handle = dynamic_cast<MyHandle *>(child);
954 if (my_handle) {
955 return;
956 }
957
958 const bool visible = child->get_visible();
959 if (_children.size() > 2) {
960 auto const it = std::find_if(begin(_children), end(_children), [=] (auto &p) { return p.get() == child; });
961 if (it != _children.end()) { // child found
962 if (it + 2 != _children.end()) { // not last widget
963 my_handle = dynamic_cast<MyHandle *>((it + 1)->get());
964 g_assert(my_handle);
965 my_handle->unparent();
966 child->unparent();
967 _children.erase(it, it + 2);
968 } else { // last widget
969 if (_children.size() == 3) { // only widget
970 child->unparent();
971 _children.erase(it);
972 } else { // not only widget, delete preceding handle
973 my_handle = dynamic_cast<MyHandle *>((it - 1)->get());
974 g_assert(my_handle);
975 my_handle->unparent();
976 child->unparent();
977 _children.erase(it - 1, it + 1);
978 }
979 }
980 }
981 }
982
983 if (visible) {
984 queue_resize();
985 }
986
987 if (_children.size() == 2) {
989 _empty_widget->set_size_request(300, -1);
990 _signal_now_empty.emit();
991 }
992}
993
994Gtk::EventSequenceState DialogMultipaned::on_drag_begin(double start_x, double start_y)
995{
996 _hide_widget1 = _hide_widget2 = nullptr;
998 // We clicked on handle.
999 bool found = false;
1000 int child_number = 0;
1001 for (auto const &child : _children) {
1002 if (auto const my_handle = dynamic_cast<MyHandle *>(child.get())) {
1003 Gtk::Allocation child_allocation = my_handle->get_allocation();
1004
1005 // Did drag start in handle?
1006 int x = child_allocation.get_x();
1007 int y = child_allocation.get_y();
1008 if (x < start_x && start_x < x + child_allocation.get_width() &&
1009 y < start_y && start_y < y + child_allocation.get_height()) {
1010 found = true;
1011 my_handle->set_dragging(true);
1012 break;
1013 }
1014 }
1015 ++child_number;
1016 }
1017
1018 if (!found) {
1019 return Gtk::EventSequenceState::DENIED;
1020 }
1021
1022 if (child_number < 1 || child_number > (int)(_children.size() - 2)) {
1023 std::cerr << "DialogMultipaned::on_drag_begin: Invalid child (" << child_number << "!!" << std::endl;
1024 return Gtk::EventSequenceState::DENIED;
1025 }
1026
1027 // Save for use in on_drag_update().
1028 _handle = child_number;
1029 start_allocation1 = _children[_handle - 1]->get_allocation();
1030 if (!_children[_handle - 1]->is_visible()) {
1031 start_allocation1.set_width(0);
1032 start_allocation1.set_height(0);
1033 }
1034 start_allocationh = _children[_handle]->get_allocation();
1035 start_allocation2 = _children[_handle + 1]->get_allocation();
1036 if (!_children[_handle + 1]->is_visible()) {
1037 start_allocation2.set_width(0);
1038 start_allocation2.set_height(0);
1039 }
1040
1041 return Gtk::EventSequenceState::CLAIMED;
1042}
1043
1044Gtk::EventSequenceState DialogMultipaned::on_drag_end(double offset_x, double offset_y)
1045{
1046 if (_handle >= 0 && _handle < _children.size()) {
1047 if (auto my_handle = dynamic_cast<MyHandle*>(_children[_handle].get())) {
1048 my_handle->set_dragging(false);
1049 }
1050 }
1051
1052 _handle = -1;
1053 _drag_handle = -1;
1054 if (_hide_widget1) {
1055 _hide_widget1->set_visible(false);
1056 }
1057 if (_hide_widget2) {
1058 _hide_widget2->set_visible(false);
1059 }
1060 _hide_widget1 = nullptr;
1061 _hide_widget2 = nullptr;
1062 _resizing_widget1 = nullptr;
1063 _resizing_widget2 = nullptr;
1064
1065 queue_allocate(); // reimpose limits if any were bent during interactive resizing
1066
1067 return Gtk::EventSequenceState::DENIED;
1068}
1069
1070// docking panels in application window can be collapsed (to left or right side) to make more
1071// room for canvas; this functionality is only meaningful in app window, not in floating dialogs
1072bool can_collapse(Gtk::Widget* widget, Gtk::Widget* handle) {
1073 // can only collapse DialogMultipaned widgets
1074 if (!widget || dynamic_cast<DialogMultipaned*>(widget) == nullptr) return false;
1075
1076 // collapsing is not supported in floating dialogs
1077 if (dynamic_cast<DialogWindow*>(widget->get_root())) return false;
1078
1079 auto parent = handle->get_parent();
1080 if (!parent) return false;
1081
1082 // find where the resizing handle is in relation to canvas area: left or right side;
1083 // next, find where the panel is in relation to the handle: on its left or right
1084 bool left_side = true;
1085 bool left_handle = false;
1086 size_t panel_index = 0;
1087 size_t handle_index = 0;
1088 size_t i = 0;
1089 for (auto const child : UI::get_children(*parent)) {
1090 if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid *>(child)) {
1091 left_side = false;
1092 } else if (child == handle) {
1093 left_handle = left_side;
1094 handle_index = i;
1095 } else if (child == widget) {
1096 panel_index = i;
1097 }
1098 ++i;
1099 }
1100
1101 if (left_handle && panel_index < handle_index) {
1102 return true;
1103 }
1104 if (!left_handle && panel_index > handle_index) {
1105 return true;
1106 }
1107
1108 return false;
1109}
1110
1111// return minimum widget size; this fn works for hidden widgets too
1112int get_min_width(Gtk::Widget* widget) {
1113 bool hidden = !widget->is_visible();
1114 if (hidden) widget->set_visible(true);
1115 int minimum_size, natural_size, ignore;
1116 widget->measure(Gtk::Orientation::HORIZONTAL, -1, minimum_size, natural_size, ignore, ignore);
1117 if (hidden) widget->set_visible(false);
1118 return minimum_size;
1119}
1120
1121// Different docking resizing activities use easing functions to speed up or slow down certain phases of resizing
1122// Below are two picewise linear functions used for that purpose
1123
1124// easing function for revealing collapsed panels
1125double reveal_curve(double val, double size) {
1126 if (size > 0 && val <= size && val >= 0) {
1127 // slow start (resistance to opening) and then quick reveal
1128 auto x = val / size;
1129 auto pos = x;
1130 if (x <= 0.2) {
1131 pos = x * 0.25;
1132 }
1133 else {
1134 pos = x * 9.5 - 1.85;
1135 if (pos > 1) pos = 1;
1136 }
1137 return size * pos;
1138 }
1139
1140 return val;
1141}
1142
1143// easing function for collapsing panels
1144// note: factors for x dictate how fast resizing happens when moving mouse (with 1 being at the same speed);
1145// other constants are to make this fn produce values in 0..1 range and seamlessly connect three segments
1146double collapse_curve(double val, double size) {
1147 if (size > 0 && val <= size && val >= 0) {
1148 // slow start (resistance), short pause and then quick collapse
1149 auto x = val / size;
1150 auto pos = x;
1151 if (x < 0.5) {
1152 // fast collapsing
1153 pos = x * 10 - 5 + 0.92;
1154 if (pos < 0) {
1155 // panel collapsed
1156 pos = 0;
1157 }
1158 }
1159 else if (x < 0.6) {
1160 // pause
1161 pos = 0.2 * 0.6 + 0.8; // = 0.92;
1162 }
1163 else {
1164 // resistance to collapsing (move slow, x 0.2 decrease)
1165 pos = x * 0.2 + 0.8;
1166 }
1167 return size * pos;
1168 }
1169
1170 return val;
1171}
1172
1173Gtk::EventSequenceState DialogMultipaned::on_drag_update(double offset_x, double offset_y)
1174{
1175 if (_handle < 0) {
1176 return Gtk::EventSequenceState::NONE;
1177 }
1178 // Hack: drag update sends some fractional garbage x, y right after first click, leading to handle movement;
1179 // ignore them. The only downside is that we won't be able to return to the exact original location, once we move.
1180 if (abs(offset_y) < 1 || abs(offset_x) < 1) return Gtk::EventSequenceState::NONE;
1181
1182 auto child1 = _children[_handle - 1].get();
1183 auto child2 = _children[_handle + 1].get();
1184 allocation1 = _children[_handle - 1]->get_allocation();
1185 allocationh = _children[_handle ]->get_allocation();
1186 allocation2 = _children[_handle + 1]->get_allocation();
1187
1188 // HACK: The bias prevents erratic resizing when dragging the handle fast, outside the bounds of the app.
1189 const int BIAS = 1;
1190
1191 auto const handle = dynamic_cast<MyHandle *>(_children[_handle].get());
1192 handle->set_drag_updated(true);
1193
1194 if (get_orientation() == Gtk::Orientation::HORIZONTAL) {
1195 // function to resize panel
1196 auto resize_fn = [](Gtk::Widget* handle, Gtk::Widget* child, int start_width, double& offset_x) {
1197 int minimum_size = get_min_width(child);
1198 auto width = start_width + offset_x;
1199 bool resizing = false;
1200 Gtk::Widget* hide = nullptr;
1201
1202 if (!child->is_visible() && can_collapse(child, handle)) {
1203 child->set_visible(true);
1204 resizing = true;
1205 }
1206
1207 if (width < minimum_size) {
1208 if (can_collapse(child, handle)) {
1209 resizing = true;
1210 auto w = start_width == 0 ? reveal_curve(width, minimum_size) : collapse_curve(width, minimum_size);
1211 offset_x = w - start_width;
1212 // facilitate closing/opening panels: users don't have to drag handle all the
1213 // way to collapse/expand a panel, they just need to move it fraction of the way;
1214 // note: those thresholds correspond to the easing functions used
1215 auto threshold = start_width == 0 ? minimum_size * 0.20 : minimum_size * 0.42;
1216 hide = width <= threshold ? child : nullptr;
1217 }
1218 else {
1219 offset_x = -(start_width - minimum_size) + BIAS;
1220 }
1221 }
1222
1223 return std::make_pair(resizing, hide);
1224 };
1225
1226 /*
1227 TODO NOTE:
1228 Resizing should ideally take into account all columns, not just adjacent ones (left and right here).
1229 Without it, expanding second collapsed column does not work, since first one may already have min width,
1230 and cannot be shrunk anymore. Instead it should be pushed out of the way (canvas should be shrunk).
1231 */
1232
1233 // panel on the left
1234 auto action1 = resize_fn(handle, child1, start_allocation1.get_width(), offset_x);
1235 _resizing_widget1 = action1.first ? child1 : nullptr;
1236 _hide_widget1 = action1.second ? child1 : nullptr;
1237
1238 // panel on the right (needs reversing offset_x, so it can use the same logic)
1239 offset_x = -offset_x;
1240 auto action2 = resize_fn(handle, child2, start_allocation2.get_width(), offset_x);
1241 _resizing_widget2 = action2.first ? child2 : nullptr;
1242 _hide_widget2 = action2.second ? child2 : nullptr;
1243 offset_x = -offset_x;
1244
1245 // set new sizes; they may temporarily violate min size panel requirements
1246 // GTK is not happy about 0-size allocations
1247 allocation1.set_width(start_allocation1.get_width() + offset_x);
1248 allocationh.set_x(start_allocationh.get_x() + offset_x);
1249 allocation2.set_x(start_allocation2.get_x() + offset_x);
1250 allocation2.set_width(start_allocation2.get_width() - offset_x);
1251 } else {
1252 // nothing fancy about resizing in vertical direction; no panel collapsing happens here
1253 Gtk::Requisition minimum_req, ignore;
1254
1255 _children[_handle - 1]->get_preferred_size(minimum_req, ignore);
1256 int minimum_size = minimum_req.get_height();
1257 if (start_allocation1.get_height() + offset_y < minimum_size)
1258 offset_y = -(start_allocation1.get_height() - minimum_size) + BIAS;
1259
1260 _children[_handle + 1]->get_preferred_size(minimum_req, ignore);
1261 minimum_size = minimum_req.get_height();
1262 if (start_allocation2.get_height() - offset_y < minimum_size)
1263 offset_y = start_allocation2.get_height() - minimum_size - BIAS;
1264
1265 allocation1.set_height(start_allocation1.get_height() + offset_y);
1266 allocationh.set_y(start_allocationh.get_y() + offset_y);
1267 allocation2.set_y(start_allocation2.get_y() + offset_y);
1268 allocation2.set_height(start_allocation2.get_height() - offset_y);
1269 }
1270
1272 queue_allocate(); // Relayout DialogMultipaned content.
1273
1274 return Gtk::EventSequenceState::NONE;
1275}
1276
1277void DialogMultipaned::set_drop_gtypes(std::vector<GType> const &gtypes)
1278{
1279 auto &front = dynamic_cast<MyDropZone &>(*_children.at(0) );
1280 auto &back = dynamic_cast<MyDropZone &>(*_children.back());
1281 _drop_target->set_gtypes(gtypes);
1282 front.set_gtypes(gtypes);
1283 back .set_gtypes(gtypes);
1284}
1285
1286// extract page and its source notebook from d&d data
1287static std::optional<std::pair<Gtk::Widget*, DialogNotebook*>> unpack_page(const Glib::ValueBase& value) {
1288 if (auto source = UI::Widget::TabStrip::unpack_drop_source(value)) {
1289 auto tabs = source->first;
1290 auto pos = source->second;
1291 auto page = find_dialog_page(tabs, pos);
1292 if (!page) {
1293 std::cerr << "DialogContainer::unpack_page: page not found!" << std::endl;
1294 return {};
1295 }
1296 auto notebook = find_dialog_notebook(tabs);
1297 return std::make_pair(page, notebook);
1298 }
1299 return {};
1300}
1301
1302bool DialogMultipaned::on_drag_data_drop(Glib::ValueBase const &value, double x, double y)
1303{
1304 auto page = unpack_page(value);
1305 if (!page) return false;
1306
1307 // find notebook under (x, y)
1308 auto const it = std::find_if(begin(_children), end(_children), [=,this](const auto& w) {
1309 if (auto notebook = dynamic_cast<DialogNotebook*>(w.get())) {
1310 double cx, cy;
1311 if (translate_coordinates(*notebook, x, y, cx, cy)) {
1312 return notebook->contains(cx, cy);
1313 }
1314 }
1315 return false;
1316 });
1317 if (it != _children.end()) {
1318 // notebook found; move page into it
1319 auto dest_notebook = static_cast<DialogNotebook*>(it->get());
1320 return _signal_dock_dialog.emit(*page->first, *page->second, DialogContainer::Middle, dest_notebook);
1321 }
1322 else {
1323 // no notebook under (x, y) - float dialog
1324 auto src_notebook = page->second;
1325 return _signal_float_dialog.emit(*page->first, *src_notebook);
1326 }
1327}
1328
1329bool DialogMultipaned::on_prepend_drag_data(Glib::ValueBase const &value, double /*x*/, double /*y*/)
1330{
1331 if (auto page = unpack_page(value)) {
1332 return _signal_dock_dialog.emit(*page->first, *page->second, DialogContainer::Start, nullptr);
1333 }
1334 return false;
1335}
1336
1337bool DialogMultipaned::on_append_drag_data(Glib::ValueBase const &value, double /*x*/, double /*y*/)
1338{
1339 if (auto page = unpack_page(value)) {
1340 return _signal_dock_dialog.emit(*page->first, *page->second, DialogContainer::End, nullptr);
1341 }
1342 return false;
1343}
1344
1345sigc::signal<void ()> DialogMultipaned::signal_now_empty()
1346{
1347 return _signal_now_empty;
1348}
1349
1350void DialogMultipaned::set_restored_width(int width) {
1351 _natural_width = width;
1352}
1353
1354void DialogMultipaned::add_drop_zone_highlight_instances()
1355{
1356 MyDropZone::add_highlight_instances();
1357}
1358
1359void DialogMultipaned::remove_drop_zone_highlight_instances()
1360{
1361 MyDropZone::remove_highlight_instances();
1362}
1363
1364} // namespace Inkscape::UI::Dialog
1365
1366/*
1367 Local Variables:
1368 mode:c++
1369 c-file-style:"stroustrup"
1370 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1371 indent-tabs-mode:nil
1372 fill-column:99
1373 End:
1374*/
1375// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Rewrite of code originally in desktop-widget.cpp.
uint64_t page
Definition canvas.cpp:171
Geom::IntRect visible
Definition canvas.cpp:154
void size_allocate_vfunc(int width, int height, int baseline) final
This function allocates the sizes of the children widgets (be them internal or not) from the containe...
void toggle_multipaned_children(bool show)
Show/hide as requested all children of this container that are of type multipaned.
bool on_drag_data_drop(Glib::ValueBase const &value, double x, double y)
void measure_vfunc(Gtk::Orientation orientation, int for_size, int &minimum, int &natural, int &minimum_baseline, int &natural_baseline) const final
Gtk::EventSequenceState on_drag_begin(double start_x, double start_y)
void prepend(std::unique_ptr< Gtk::Widget > child)
std::vector< std::unique_ptr< Gtk::Widget > > _children
void remove(Gtk::Widget &child)
Removes a widget from DialogMultipaned.
bool on_append_drag_data(Glib::ValueBase const &value, double x, double y)
void set_dropzone_sizes(int start, int end)
Set the sizes of the DialogMultipaned dropzones.
Gtk::EventSequenceState on_drag_update(double offset_x, double offset_y)
const Glib::RefPtr< Gtk::DropTarget > _drop_target
std::vector< sigc::scoped_connection > _connections
Gtk::EventSequenceState on_drag_end(double offset_x, double offset_y)
void append(std::unique_ptr< Gtk::Widget > child)
Gtk::SizeRequestMode get_request_mode_vfunc() const override
void set_drop_gtypes(std::vector< GType > const &gtypes)
bool on_prepend_drag_data(Glib::ValueBase const &value, double x, double y)
void ensure_multipaned_children()
Ensure that this dialog container is visible.
void insert(int pos, std::unique_ptr< Gtk::Widget > child)
A widget that wraps a Gtk::Notebook with dialogs as pages.
DialogWindow holds DialogContainer instances for undocked dialogs.
A Gtk::Grid widget that contains rulers, scrollbars, buttons, and, of course, the canvas.
Definition canvas-grid.h:70
static std::optional< std::pair< TabStrip *, int > > unpack_drop_source(const Glib::ValueBase &value)
const double w
Definition conic-4.cpp:19
Utilities to more easily use Gtk::EventController & subclasses like Gesture.
Geom::IntPoint size
static constexpr int HANDLE_CROSS_SIZE
static constexpr int DROPZONE_SIZE
static constexpr int DROPZONE_EXPANSION
static constexpr int HANDLE_SIZE
A widget with multiple panes.
A wrapper for Gtk::Notebook.
A window for floating docks.
static char const *const parent
Definition dir-util.cpp:70
std::vector< ItemIterator > _active
std::unique_ptr< Magick::Image > image
Glib::ustring label
Control handle rendering/caching.
Definition desktop.h:50
auto use_state(Slot &&slot)
Definition controller.h:43
Dialog code.
Definition desktop.h:117
double reveal_curve(double val, double size)
bool can_collapse(Gtk::Widget *widget, Gtk::Widget *handle)
static std::optional< std::pair< Gtk::Widget *, DialogNotebook * > > unpack_page(const Glib::ValueBase &value)
DialogNotebook * find_dialog_notebook(Widget::TabStrip *tabs)
void rounded_rectangle(const Cairo::RefPtr< Cairo::Context > &cr, double x, double y, double w, double h, double r)
int get_min_width(Gtk::Widget *widget)
static int _get_size(auto const sizes, Gtk::Orientation const orientation)
Gtk::Widget * find_dialog_page(Widget::TabStrip *tabs, int position)
double collapse_curve(double val, double size)
static constexpr int height
std::vector< Gtk::Widget * > get_children(Gtk::Widget &widget)
Get a vector of the widgetʼs children, from get_first_child() through each get_next_sibling().
Definition util.cpp:156
STL namespace.
Ocnode * child[8]
Definition quantize.cpp:33
int index
int minimum
double height
double width