Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
tab-strip.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2#include "tab-strip.h"
3
4#include <cassert>
5#include <numeric>
6#include <glibmm/i18n.h>
7#include <glibmm/main.h>
8#include <gtk-4.0/gdk/gdkevents.h>
9#include <gtkmm/button.h>
10#include <gtkmm/dragicon.h>
11#include <gtkmm/droptarget.h>
12#include <gtkmm/eventcontrollermotion.h>
13#include <gtkmm/gestureclick.h>
14#include <gtkmm/image.h>
15#include <gtkmm/label.h>
16#include <gtkmm/tooltip.h>
17
18#include "ui/builder-utils.h"
19#include "ui/containerize.h"
20#include "ui/popup-menu.h"
21#include "ui/util.h"
22#include "util/value-utils.h"
23
24#define BUILD(name) name{UI::get_widget<std::remove_reference_t<decltype(name)>>(builder, #name)}
25
26using namespace Inkscape::Util;
27
28namespace Inkscape::UI::Widget {
29namespace {
30
31struct PointerTransparentWidget : Gtk::Widget
32{
33 bool contains_vfunc(double, double) const override { return false; }
34};
35
36std::optional<Geom::Point> get_current_pointer_pos(Glib::RefPtr<Gdk::Device> const &pointer, Gtk::Widget &widget)
37{
38 double x, y;
39 Gdk::ModifierType mask;
40 auto root = widget.get_root();
41 dynamic_cast<Gtk::Native &>(*root).get_surface()->get_device_position(pointer, x, y, mask);
42 dynamic_cast<Gtk::Widget &>(*root).translate_coordinates(widget, x, y, x, y);
43 return Geom::Point{x, y};
44}
45
46} // namespace
47
48
50struct SimpleTab : Gtk::Box
51{
52 Gtk::Label& name;
53 Gtk::Button& close;
54 Gtk::Image& icon;
56 bool _show_close_btn = true;
57
58 SimpleTab() : SimpleTab{create_builder("simple-tab.ui")}
59 {}
60
61 SimpleTab(const SimpleTab& src) : SimpleTab() {
62 name.set_text(src.name.get_text());
63 name.set_visible(src.name.get_visible());
64 icon.set_from_icon_name(src.icon.property_icon_name());
65 close.set_visible(src.close.get_visible());
66 _show_labels = src._show_labels;
67 _show_close_btn = src._show_close_btn;
68 }
69
70 SimpleTab(Glib::RefPtr<Gtk::Builder> const &builder)
71 : BUILD(name) , BUILD(close) , BUILD(icon)
72 {
73 set_name("SimpleTab");
74 append(get_widget<Gtk::Box>(builder, "root"));
75 }
76
77 void set_active() {
78 get_style_context()->add_class("tab-active");
79 if (_show_close_btn) {
80 close.set_visible();
81 }
82 if (_show_labels == TabStrip::ActiveOnly) {
83 name.set_visible();
84 }
85 }
86 void set_inactive() {
87 get_style_context()->remove_class("tab-active");
88 if (_show_close_btn) {
89 close.set_visible(false);
90 }
91 if (_show_labels == TabStrip::ActiveOnly) {
92 name.set_visible(false);
93 }
94 }
95 Glib::ustring get_label() const { return name.get_text(); }
96
97 void update(bool is_active) {
98 close.set_visible(_show_close_btn && is_active);
99
100 name.set_visible(
101 _show_labels != TabStrip::Never && (_show_labels == TabStrip::Always || (_show_labels == TabStrip::ActiveOnly && is_active))
102 );
103 }
104};
105
107struct TabWidget : SimpleTab
108{
109 TabStrip *const parent;
110
111 TabWidget(TabStrip* parent) : parent{parent}
112 {
113 set_has_tooltip(true);
114 }
115};
116
117class TabWidgetDrag
118{
119public:
123 TabWidgetDrag(TabWidget *src, Geom::Point const &offset, Glib::RefPtr<Gdk::Device> device)
124 : _src{src}
125 , _offset{offset}
126 , _device{std::move(device)}
127 , _dst{src->parent}
128 {}
129 ~TabWidgetDrag() {
130 }
131
134 void motion(std::optional<Geom::Point> pos)
135 {
136 constexpr int detach_dist = 25;
137 if (pos && Geom::Rect(0, 0, _dst->get_width(), _dst->get_height()).distanceSq(*pos) < Geom::sqr(detach_dist)) {
138 // Pointer is still sufficiently near dst - update drop position.
139 _drop_x = pos->x() - static_cast<int>(std::round(_offset.x()));
140 _dst->queue_allocate();
141 // temporarily hide (+) button too
142 _src->parent->_plus_btn.set_visible(false);
143 } else {
144 // Pointer is too far away from dst - detach from it.
145 cancelTick();
146 _ensureDrag();
147 setDst(nullptr);
148 }
149 }
150
154 void addTick()
155 {
156 if (!_tick_callback) {
157 _tick_callback = _dst->add_tick_callback([this] (auto&&) {
158 motion(get_current_pointer_pos(_device, *_dst));
159 return true;
160 });
161 }
162 }
163
164 void cancelTick()
165 {
166 if (_tick_callback) {
167 _dst->remove_tick_callback(_tick_callback);
168 _tick_callback = 0;
169 }
170 }
171
173 void setDst(TabStrip *new_dst)
174 {
175 if (new_dst == _dst) {
176 return;
177 }
178
179 if (_dst) {
180 _dst->_drag_dst = {};
181 _dst->queue_resize();
182 }
183
184 _dst = new_dst;
185
186 if (_dst) {
187 _dst->_drag_dst = _src->parent->_drag_src;
188 _drop_x = {};
189 _drop_i = {};
190 }
191
192 _queueReparent();
193 }
194
198 void finish(bool cancel = false)
199 {
200 // Cancel the tick callback if one is being used for motion polling.
201 cancelTick();
202
203 // Detach from source and destination, keeping self alive until end of function.
204 auto const self_ref = std::move(_src->parent->_drag_src);
205 assert(self_ref.get() == this);
206 if (_dst) {
207 _dst->_drag_dst = {};
208 }
209
210 // Undo widget modifications to source and destination.
211 _src->set_visible(true);
212 if (_src->parent->_plus_btn.get_popover()) {
213 _src->parent->_plus_btn.set_visible();
214 }
215 _src->parent->queue_resize();
216 if (_dst) {
217 if (_widget && _widget->get_parent() == _dst) {
218 _widget->unparent();
219 }
220 _dst->queue_resize();
221 }
222 //TODO
223 _src->parent->_signal_dnd_end.emit(cancel);
224 // TabStrip::Instances::get().removeHighlight();
225
226 if (!_dst && _src->parent->_tabs.size() == 1) {
227 cancel = true; // cancel if detaching lone tab
228 }
229
230 if (cancel) {
231 // _src->parent->_desktop_widget->get_window()->present();
232 return;
233 }
234
235 if (_drag) {
236 _drag->drag_drop_done(true); // suppress drag-failed animation
237 }
238
239 if (!_dst) {
240 // Detach
241 // _src->parent->_signal_float_tab.emit(*_src);
242 } else if (_dst == _src->parent) {
243 // Reorder
244 if (_drop_i) {
245 if (_src->parent->_can_rearrange) {
246 int const from = _src->parent->get_tab_position(*_src);
247 if (_src->parent->_reorderTab(from, *_drop_i)) {
248 _src->parent->_signal_tab_rearranged(from, *_drop_i);
249 }
250 else {
251 _src->parent->queue_resize();
252 }
253 }
254 }
255 } else {
256 // Migrate
257 if (_drop_i) {
258 //TODO
259 _dst->_signal_move_tab.emit(*_src, _src->parent->get_tab_position(*_src), *_src->parent, *_drop_i);
260 /*
261 auto const desktop = _src->desktop;
262 _src->desktop->getDesktopWidget()->removeDesktop(desktop); // deletes src
263 _dst->_desktop_widget->addDesktop(desktop, *_drop_i);
264 */
265 }
266 }
267 }
268
269 TabWidget *src() const { return _src; }
270 SimpleTab *widget() const { return _widget.get(); }
271 std::optional<int> const &dropX() const { return _drop_x; }
272 void setDropI(int i) { _drop_i = i; }
273
274private:
275 TabWidget *const _src; // The source tab.
276 Geom::Point const _offset; // The point within the tab that the drag started from.
277 Glib::RefPtr<Gdk::Device> const _device; // The pointing device that started the drag.
278
279 TabStrip *_dst; // The destination tabs widget, possibly null.
280 std::optional<int> _drop_x; // Position within dst where tab is dropped.
281 std::optional<int> _drop_i; // The index within dst where the tab is dropped.
282
283 sigc::scoped_connection _reparent_conn; // Used to defer reparenting of widget.
284 sigc::scoped_connection _cancel_conn; // Connection to the Gdk::Drag's cancel signal.
285 sigc::scoped_connection _drop_conn; // Connection to the Gdk::Drag's drop-perfomed signal.
286 Glib::RefPtr<Gdk::Drag> _drag; // The Gdk::Drag, lazily created at the same time as widget.
287 std::unique_ptr<SimpleTab> _widget; // The dummy tab that is placed inside the drag icon or dst.
288 guint _tick_callback = 0; // If nonzero, the tick callback used for motion polling.
289
290 void _ensureDrag()
291 {
292 if (_drag) {
293 return;
294 }
295
296 // Create the Gdk drag.
297 assert(_src->parent->_drag_src.get() == this);
298 auto content = Gdk::ContentProvider::create(GlibValue::create<std::weak_ptr<TabWidgetDrag>>(_src->parent->_drag_src));
299 _drag = _src->parent->get_native()->get_surface()->drag_begin_from_point(_device, content, Gdk::DragAction::MOVE, _offset.x(), _offset.y());
300
301 // Handle drag cancellation.
302 _cancel_conn = _drag->signal_cancel().connect([this] (auto reason) {
303 finish(reason == Gdk::DragCancelReason::USER_CANCELLED);
304 }, false);
305
306 // Some buggy clients accept the drop when they shouldn't. We interpret it as a drop on nothing.
307 _drop_conn = _drag->signal_drop_performed().connect([this] {
308 finish();
309 }, false);
310
311 // Hide the real tab.
312 _src->set_visible(false);
313 _src->parent->_plus_btn.set_visible();
314
315 // Create a visual replica of the tab.
316 _widget = std::make_unique<SimpleTab>(*_src);
317 // _widget->name.set_text(_src->get_label());// get_title(_src->desktop));
318 _widget->set_active();
319
320 // fire D&D event
321 _src->parent->_signal_dnd_begin.emit();
322 }
323
324 // Schedule widget to be reparented. GTK is picky about when this can be done, hence the need for an idle callback.
325 // In particular, widget hierarchy changes aren't allowed in a tick callback or an enter/leave handler.
326 void _queueReparent()
327 {
328 if (!_reparent_conn) {
329 _reparent_conn = Glib::signal_idle().connect([this] { _reparentWidget(); return false; }, Glib::PRIORITY_HIGH);
330 }
331 }
332
333 void _reparentWidget()
334 {
335 auto drag_icon = Gtk::DragIcon::get_for_drag(_drag);
336
337 if (_widget.get() == drag_icon->get_child()) {
338 drag_icon->unset_child();
339 // Fixme: Shouldn't be needed, but works around https://gitlab.gnome.org/GNOME/gtk/-/issues/7185
340 Gtk::DragIcon::set_from_paintable(_drag, to_texture(Cairo::ImageSurface::create(Cairo::ImageSurface::Format::ARGB32, 1, 1)), 0, 0);
341 } else if (_widget->get_parent()) {
342 assert(dynamic_cast<TabStrip *>(_widget->get_parent()));
343 _widget->unparent();
344 }
345
346 // Put the replica tab inside dst or the drag icon.
347 if (_dst) {
348 _widget->insert_before(*_dst, *_dst->_overlay);
349 _dst->queue_resize();
350 //TODO
351 // TabStrip::Instances::get().removeHighlight();
352 } else {
353 drag_icon->set_child(*_widget);
354 _drag->set_hotspot(_offset.x(), _offset.y());
355 //TODO
356 // TabStrip::Instances::get().addHighlight();
357 }
358 }
359};
360
361static std::shared_ptr<TabWidgetDrag> get_tab_drag(Gtk::DropTarget &droptarget)
362{
363 // Determine whether an in-app tab is being dragged and get information about it.
364 auto const drag = droptarget.get_drop()->get_drag();
365 if (!drag) {
366 return {}; // not in-app
367 }
368 auto const content = GlibValue::from_content_provider<std::weak_ptr<TabWidgetDrag>>(*drag->get_content());
369 if (!content) {
370 return {}; // not a tab
371 }
372 return content->lock();
373}
374
376 _overlay{Gtk::make_managed<PointerTransparentWidget>()}
377{
378 set_name("TabStrip");
379 set_overflow(Gtk::Overflow::HIDDEN);
380 containerize(*this);
381
382 _plus_btn.set_name("NewTabButton");
383 _plus_btn.set_valign(Gtk::Align::CENTER);
384 _plus_btn.set_has_frame(false);
385 _plus_btn.set_focusable(false);
386 _plus_btn.set_focus_on_click(false);
387 _plus_btn.set_can_focus(false);
388 _plus_btn.set_icon_name("list-add");
389 _plus_btn.insert_at_end(*this);
390
391 _overlay->insert_at_end(*this); // always kept topmost
392 _overlay->set_name("Overlay");
393
394 auto click = Gtk::GestureClick::create();
395 click->set_button(0);
396 click->signal_pressed().connect([this, click = click.get()] (int, double x, double y) {
397 // Find clicked tab.
398 auto const [tab_weak, tab_pos] = _tabAtPoint({x, y});
399 auto tab = tab_weak.lock();
400
401 // Handle button actions.
402 switch (click->get_current_button()) {
403 case GDK_BUTTON_PRIMARY:
404 if (tab) {
405 double xc, yc;
406 translate_coordinates(tab->close, x, y, xc, yc);
407 if (!tab->close.contains(xc, yc)) {
408 _left_clicked = tab_weak;
409 _left_click_pos = {x, y};
410 _signal_select_tab.emit(*tab);
411 }
412 }
413 break;
414 case GDK_BUTTON_SECONDARY: {
415 if (tab && _popover && is_tab_active(*tab)) {
416 auto size = tab->get_allocation();
417 // center/bottom location for a context menu tip
418 UI::popup_at(*_popover, *tab, size.get_width() / 2, size.get_height() - 7);
419 }
420 break;
421 }
422 case GDK_BUTTON_MIDDLE:
423 if (tab) {
424 _signal_close_tab.emit(*tab);
425 }
426 break;
427 default:
428 break;
429 }
430 });
431 click->signal_released().connect([this] (auto&&...) {
432 _left_clicked = {};
433 if (_drag_src) {
434 // too early to dismiss the drag widget, not read yet
435 // this fixed delay is lame, need to figure out better approach
436 _finish_conn = Glib::signal_timeout().connect([this] { if (_drag_src) _drag_src->finish(); return false; }, 100);
437 }
438 });
439 add_controller(click);
440
441 auto motion = Gtk::EventControllerMotion::create();
442 motion->signal_motion().connect([this, &motion = *motion] (double x, double y) {
443 if (!_drag_src) {
444 auto const tab = _left_clicked.lock();
445 if (!tab) {
446 return;
447 }
448
449 constexpr int drag_initiate_dist = 8;
450 if ((Geom::Point{x, y} - _left_click_pos).lengthSq() < Geom::sqr(drag_initiate_dist)) {
451 return;
452 }
453
454 _left_clicked = {};
455
457 translate_coordinates(*tab, _left_click_pos.x(), _left_click_pos.y(), offset.x(), offset.y());
458
459 // Start dragging.
460 _drag_src = _drag_dst = std::make_shared<TabWidgetDrag>(
461 tab.get(),
462 offset,
463 motion.get_current_event_device()
464 );
465
466 // Raise dragged tab to top.
467 tab->insert_before(*this, _plus_btn);
468 }
469
470 if (!_drag_src->widget()) {
471 _drag_src->motion(Geom::Point{x, y});
472 }
473 });
474 add_controller(motion);
475
476 auto droptarget = Gtk::DropTarget::create(GlibValue::type<std::weak_ptr<TabWidgetDrag>>(), Gdk::DragAction::MOVE);
477 auto handler = [this, &droptarget = *droptarget] (double x, double y) -> Gdk::DragAction {
478 if (auto tabdrag = get_tab_drag(droptarget)) {
479 tabdrag->cancelTick();
480 tabdrag->setDst(this);
481 tabdrag->motion(Geom::Point{x, y});
482 }
483 return {};
484 };
485 droptarget->signal_enter().connect(handler, false);
486 droptarget->signal_motion().connect(handler, false);
487 droptarget->signal_leave().connect([this] {
488 if (_drag_dst) {
489 _drag_dst->addTick();
490 }
491 });
492 add_controller(droptarget);
493
494 _updateVisibility();
495}
496
497TabStrip::~TabStrip()
498{
499 // Note: This code will fail if TabStrip becomes a managed widget, in which
500 // case it must be done on signal_destroy() instead.
501 if (_drag_dst) {
502 _drag_dst->setDst(nullptr);
503 }
504 if (_drag_src) {
505 _drag_src->finish(true);
506 }
507}
508
509Gtk::Widget* TabStrip::add_tab(const Glib::ustring& label, const Glib::ustring& icon, int pos)
510{
511 auto tab = std::make_shared<TabWidget>(this);
512 tab->name.set_text(label);
513 tab->icon.set_from_icon_name(icon);
514 tab->_show_close_btn = _show_close_btn;
515 tab->_show_labels = _show_labels;
516 tab->update(false);
517
518 auto ptr_tab = tab.get();
519 tab->close.signal_clicked().connect([this, ptr_tab] { _signal_close_tab.emit(*ptr_tab); });
520
521 tab->signal_query_tooltip().connect([this, ptr_tab] (int, int, bool, Glib::RefPtr<Gtk::Tooltip> const &tooltip) {
522 _setTooltip(*ptr_tab, tooltip);
523 return true; // show the tooltip
524 }, true);
525
526 if (pos == -1) {
527 pos = _tabs.size();
528 }
529 else if (pos < 0 || pos >= _tabs.size()) {
530 pos = _tabs.size();
531 }
532 assert(0 <= pos && pos <= _tabs.size());
533
534 tab->insert_before(*this, _plus_btn);
535 _tabs.insert(_tabs.begin() + pos, tab);
536
537 _updateVisibility();
538 return tab.get();
539}
540
541void TabStrip::remove_tab(const Gtk::Widget& tab)
542{
543 int const i = get_tab_position(tab);
544 assert(i != -1);
545 if (i < 0) {
546 g_warning("TabStrip:remove_tab(): attempt to remove a tab that doesn't belong to this widget");
547 return;
548 }
549
550 if (_drag_src && _drag_src->src() == _tabs[i].get()) {
551 _drag_src->finish(true);
552 }
553
554 _tabs[i]->unparent();
555 _tabs.erase(_tabs.begin() + i);
556
557 _updateVisibility();
558}
559
560void TabStrip::remove_tab_at(int pos) {
561 if (auto tab = get_tab_at(pos)) {
562 remove_tab(*tab);
563 }
564}
565
566bool TabStrip::is_tab_active(const Gtk::Widget& tab) const {
567 auto const active = _active.lock();
568
569 return active && active.get() == &tab;
570}
571
572void TabStrip::set_show_close_button(bool show) {
573 _show_close_btn = show;
574 for (auto& tab : _tabs) {
575 tab->_show_close_btn = show;
576 tab->update(is_tab_active(*tab));
577 }
578 queue_allocate();
579}
580
581GType TabStrip::get_dnd_source_type() {
582 return GlibValue::type<std::weak_ptr<TabWidgetDrag>>();
583}
584
585std::optional<std::pair<TabStrip*, int>> TabStrip::unpack_drop_source(const Glib::ValueBase& value) {
586
587 if (G_VALUE_HOLDS(value.gobj(), get_dnd_source_type())) {
588 auto weak = static_cast<const Glib::Value<std::weak_ptr<TabWidgetDrag>>&>(value);
589 auto ptr = weak.get().lock();
590 if (ptr) {
591 return std::make_pair(ptr->src()->parent, ptr->src()->parent->get_tab_position(*ptr->src()));
592 }
593
594 }
595
596 // if (auto tabdrag = get_tab_drag(droptarget)) {
597 // return std::make_pair(tabdrag->src()->parent, tabdrag->src()->parent->get_tab_position(*tabdrag->src()));
598 // }
599 return {};
600}
601
602void TabStrip::select_tab(const Gtk::Widget& tab)
603{
604 auto const active = _active.lock();
605
606 if (active && active.get() == &tab) {
607 return;
608 }
609
610 if (active) {
611 active->set_inactive();
612 _active = {};
613 }
614
615 int const i = get_tab_position(tab);
616 if (i != -1) {
617 _tabs[i]->set_active();
618 _active = _tabs[i];
619 }
620}
621
622void TabStrip::select_tab_at(int pos) {
623 if (auto tab = get_tab_at(pos)) {
624 select_tab(*tab);
625 }
626}
627
628int TabStrip::get_tab_position(const Gtk::Widget& tab) const
629{
630 for (int i = 0; i < _tabs.size(); i++) {
631 if (_tabs[i].get() == &tab) {
632 return i;
633 }
634 }
635 return -1;
636}
637
638Gtk::Widget* TabStrip::get_tab_at(int i) const
639{
640 auto index = static_cast<size_t>(i);
641 return index < _tabs.size() ? _tabs[index].get() : nullptr;
642}
643
644void TabStrip::set_new_tab_popup(Gtk::Popover* popover) {
645 if (popover) {
646 _plus_btn.set_popover(*popover);
647 _plus_btn.set_visible();
648 }
649 else {
650 _plus_btn.unset_popover();
651 _plus_btn.set_visible(false);
652 }
653}
654
655void TabStrip::set_tabs_context_popup(Gtk::Popover* popover) {
656 if (_popover) _popover->unparent();
657 _popover = nullptr;
658 if (popover) {
659 popover->set_parent(*this);
660 _popover = popover;
661 }
662}
663
664void TabStrip::enable_rearranging_tabs(bool enable) {
665 _can_rearrange = enable;
666}
667
668void TabStrip::set_show_labels(ShowLabels labels) {
669 _show_labels = labels;
670 // refresh tabs
671 for (auto& tab : _tabs) {
672 tab->_show_labels = labels;
673 tab->update(is_tab_active(*tab));
674 }
675 queue_allocate();
676}
677
678void TabStrip::_updateVisibility()
679{
680 // set_visible(_tabs.size() > 1);
681}
682
683TabWidget* TabStrip::find_tab(Gtk::Widget& tab) {
684 for (auto& t : _tabs) {
685 if (t.get() == &tab) {
686 return t.get();
687 }
688 }
689 return nullptr;
690}
691
692Gtk::SizeRequestMode TabStrip::get_request_mode_vfunc() const
693{
694 return Gtk::SizeRequestMode::CONSTANT_SIZE;
695}
696
697void TabStrip::measure_vfunc(Gtk::Orientation orientation, int, int &min, int &nat, int &, int &) const
698{
699 if (orientation == Gtk::Orientation::VERTICAL) {
700 min = 0;
701 auto consider = [&] (Gtk::Widget const &w) {
702 auto const m = w.measure(Gtk::Orientation::VERTICAL, -1);
703 min = std::max(min, m.sizes.minimum);
704 };
705 for (auto const &tab : _tabs) {
706 consider(*tab);
707 }
708 if (_drag_src) {
709 if (auto widget = _drag_src->widget()) {
710 consider(*widget);
711 }
712 }
713 if (_drag_dst) {
714 if (auto widget = _drag_dst->widget()) {
715 consider(*widget);
716 }
717 }
718 nat = min;
719 } else {
720 min = 0;
721 nat = 0;
722 for (auto const &tab : _tabs) {
723 const auto [sizes, baselines] = tab->measure(Gtk::Orientation::HORIZONTAL, -1);
724 min += sizes.minimum;
725 nat += sizes.natural;
726 }
727 if (_plus_btn.is_visible()) {
728 const auto [sizes, baselines] = _plus_btn.measure(Gtk::Orientation::HORIZONTAL, -1);
729 min += sizes.minimum;
730 nat += sizes.natural;
731 }
732 }
733}
734
735namespace {
736
737struct Size {
739 int delta; // value to shrink
740 int index; // original location
741 int size() const { return minimum + delta; }
742};
743
744// Decrease sizes until they meet target value by subtracting given amount
745void shrink_sizes(std::vector<Size>& sizes, int decrease) {
746 if (sizes.empty() || decrease <= 0) return;
747
748 // sort by size, so we start shrinking by reducing size of the longest components first
749 std::sort(begin(sizes), end(sizes), [](const Size& a, const Size& b){ return a.delta > b.delta; });
750
751 // how much space can we save?
752 int available = std::accumulate(begin(sizes), end(sizes), 0, [](int acc, auto& s){ acc += s.delta; return acc; });
753 decrease = std::min(available, decrease);
754
755 // sentry
756 sizes.emplace_back(Size{ .minimum = 0, .delta = 0, .index = 99999 });
757
758 auto entry = &sizes.front();
759 while (decrease > 0) {
760 //todo: improve performance
761 entry->delta -= 1;
762 decrease--;
763 if (decrease == 0) break;
764
765 if (entry[1].delta > entry->delta) {
766 ++entry;
767 }
768 else {
769 entry = &sizes.front();
770 }
771 }
772
773 // restore order
774 std::sort(begin(sizes), end(sizes), [](const Size& a, const Size& b){ return a.index < b.index; });
775}
776
777} // namespace
778
780{
781 auto plus_w = _plus_btn.get_visible() ? _plus_btn.measure(Gtk::Orientation::HORIZONTAL, -1).sizes.natural : 0;
782
783 _overlay->size_allocate(Gtk::Allocation(0, 0, width, height), -1);
784
785 // limit width by removing plus button's size
786 width -= plus_w;
787
788 struct Drop
789 {
790 int x;
791 int w;
792 SimpleTab *widget;
793 bool done = false;
794 };
795 std::optional<Drop> drop;
796
797 // measure all tabs and find all the deltas of natural size minus min size for each tab
798 std::vector<Size> alloc;
799 int minimum = 0;
800 int total = 0;
801 // gather all measurements
802 alloc.reserve(_tabs.size() + 1);
803 for (int i = 0; i < _tabs.size(); i++) {
804 auto const tab = _tabs[i].get();
805 auto [minimum, natural] = tab->measure(Gtk::Orientation::HORIZONTAL, -1).sizes;
806 total += natural;
807 minimum += minimum;
808 alloc.emplace_back(Size{ .minimum = minimum, .delta = natural - minimum, .index = i });
809 }
810
811 // check available width; restrict tab sizes if space is limited
812 if (width <= minimum) {
813 // shrink to the minimun size, there's no wiggle room
814 for (int i = 0; i < _tabs.size(); i++) {
815 alloc[i].delta = 0;
816 }
817 }
818 else if (width < total) {
819 // We shall have to economise, Gromit.
820 shrink_sizes(alloc, total - width);
821 }
822 else {
823 // no restrictions on size; all tabs fit
824 }
825
826 if (_drag_dst && _drag_dst->dropX()) {
827 auto widget = !_drag_dst->widget()
828 ? _drag_dst->src()
829 : _drag_dst->widget();
830 if (widget->get_parent() == this) {
831 int pos = get_tab_position(*widget);
832 int w = widget->measure(Gtk::Orientation::HORIZONTAL, -1).sizes.natural;
833 if (pos >= 0) {
834 w = alloc[pos].size();
835 }
836 auto right = width - w;
837 auto x = right > 0 ? std::clamp(*_drag_dst->dropX(), 0, right) : 0;
838 drop = Drop{ .x = x, .w = w, .widget = widget };
839 }
840 }
841
842 // position and size tabs
843 int x = 0;
844 for (int i = 0; i < _tabs.size(); i++) {
845 const auto& a = alloc[i];
846 auto const tab = _tabs[i].get();
847
848 if (_drag_src && tab == _drag_src->src()) {
849 continue;
850 }
851 int w = a.size();
852 if (drop && !drop->done && x + w / 2 > drop->x) {
853 x += drop->w;
854 _drag_dst->setDropI(i);
855 drop->done = true;
856 }
857 tab->size_allocate(Gtk::Allocation(x, 0, w, height), -1);
858 x += w;
859 }
860
861 if (_plus_btn.get_visible()) {
862 _plus_btn.size_allocate(Gtk::Allocation(x, 0, plus_w, height), -1);
863 }
864
865 // GTK burdens custom widgets with having to implement this manually.
866 if (_popover) {
867 _popover->present();
868 }
869
870 if (drop) {
871 if (!drop->done) {
872 _drag_dst->setDropI(_tabs.size());
873 }
874 drop->widget->size_allocate(Gtk::Allocation(drop->x, 0, drop->w, height), -1);
875 }
876}
877
878void TabStrip::_setTooltip(const TabWidget& tab, Glib::RefPtr<Gtk::Tooltip> const &tooltip)
879{
880 tooltip->set_text(tab.name.get_text());
881}
882
883std::pair<std::weak_ptr<TabWidget>, Geom::Point> TabStrip::_tabAtPoint(Geom::Point const &pos)
884{
885 double xt, yt;
886 auto const it = std::find_if(_tabs.begin(), _tabs.end(), [&] (auto const &tab) {
887 translate_coordinates(*tab, pos.x(), pos.y(), xt, yt);
888 return tab->contains(xt, yt);
889 });
890 if (it == _tabs.end()) {
891 return {};
892 }
893 return {*it, {xt, yt}};
894}
895
896bool TabStrip::_reorderTab(int from, int to)
897{
898 assert(0 <= from && from < _tabs.size());
899 assert(0 <= to && to <= _tabs.size());
900
901 if (from == to || from + 1 == to) {
902 return false;
903 }
904
905 auto tab = std::move(_tabs[from]);
906 _tabs.erase(_tabs.begin() + from);
907 _tabs.insert(_tabs.begin() + to - (to > from), std::move(tab));
908 return true;
909}
910
911} // namespace Inkscape::UI::Widget
Gtk builder utilities.
C distanceSq(CPoint const &p) const
Get rectangle's distance SQUARED away from the given point.
Two-dimensional point that doubles as a vector.
Definition point.h:66
constexpr Coord y() const noexcept
Definition point.h:106
constexpr Coord x() const noexcept
Definition point.h:104
Axis aligned, non-empty rectangle.
Definition rect.h:92
sigc::signal< void(Gtk::Widget &)> _signal_close_tab
Definition tab-strip.h:96
bool is_tab_active(const Gtk::Widget &tab) const
sigc::signal< void(Gtk::Widget &)> _signal_select_tab
Definition tab-strip.h:95
void size_allocate_vfunc(int width, int height, int baseline) override
Gtk::Widget *const _overlay
Definition tab-strip.h:87
std::weak_ptr< TabWidget > _left_clicked
Definition tab-strip.h:93
const double w
Definition conic-4.cpp:19
RootCluster root
static char const *const parent
Definition dir-util.cpp:70
double offset
Glib::ustring label
T sqr(const T &x)
Definition math-utils.h:57
Definition desktop.h:50
Custom widgets.
Definition desktop.h:126
static std::shared_ptr< TabWidgetDrag > get_tab_drag(Gtk::DropTarget &droptarget)
void containerize(Gtk::Widget &widget)
Make a custom widget implement sensible memory management for its children.
static void popup_at(Gtk::Popover &popover, Gtk::Widget &widget, double const x_offset, double const y_offset, int width, int height)
Glib::RefPtr< Gtk::Builder > create_builder(const char *filename)
GType type()
Returns the type used for storing an object of type T inside a value.
Definition value-utils.h:29
Glib::ValueBase create(Args &&... args)
Return a value containing and owning a newly-created T instance.
Miscellaneous supporting code.
Definition document.h:95
static void append(std::vector< T > &target, std::vector< T > &&source)
STL namespace.
int size
Helpers to connect signals to events that popup a menu in both GTK3 and GTK4.
Ocnode * parent
Definition quantize.cpp:31
int delta
int index
int minimum
Generic tab strip widget.
double height
double width
Glib::ustring name
Definition toolbars.cpp:55
Glib::RefPtr< Gtk::Builder > builder
Glib::RefPtr< Gdk::Texture > to_texture(Cairo::RefPtr< Cairo::Surface > const &surface)
Convert an image surface in ARGB32 format to a texture.
Definition util.cpp:509
Wrapper for the GLib value API.