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>
24#define BUILD(name) name{UI::get_widget<std::remove_reference_t<decltype(name)>>(builder, #name)}
31struct PointerTransparentWidget : Gtk::Widget
33 bool contains_vfunc(
double,
double)
const override {
return false; }
36std::optional<Geom::Point> get_current_pointer_pos(Glib::RefPtr<Gdk::Device>
const &pointer, Gtk::Widget &widget)
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);
50struct SimpleTab : Gtk::Box
56 bool _show_close_btn =
true;
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;
70 SimpleTab(Glib::RefPtr<Gtk::Builder>
const &
builder)
71 : BUILD(
name) , BUILD(close) , BUILD(icon)
73 set_name(
"SimpleTab");
78 get_style_context()->add_class(
"tab-active");
79 if (_show_close_btn) {
87 get_style_context()->remove_class(
"tab-active");
88 if (_show_close_btn) {
89 close.set_visible(
false);
92 name.set_visible(
false);
95 Glib::ustring get_label()
const {
return name.get_text(); }
97 void update(
bool is_active) {
98 close.set_visible(_show_close_btn && is_active);
107struct TabWidget : SimpleTab
113 set_has_tooltip(
true);
123 TabWidgetDrag(TabWidget *src,
Geom::Point const &
offset, Glib::RefPtr<Gdk::Device> device)
126 , _device{
std::move(device)}
134 void motion(std::optional<Geom::Point> pos)
136 constexpr int detach_dist = 25;
139 _drop_x = pos->x() -
static_cast<int>(std::round(_offset.x()));
140 _dst->queue_allocate();
142 _src->parent->_plus_btn.set_visible(
false);
156 if (!_tick_callback) {
157 _tick_callback = _dst->add_tick_callback([
this] (
auto&&) {
158 motion(get_current_pointer_pos(_device, *_dst));
166 if (_tick_callback) {
167 _dst->remove_tick_callback(_tick_callback);
173 void setDst(TabStrip *new_dst)
175 if (new_dst == _dst) {
180 _dst->_drag_dst = {};
181 _dst->queue_resize();
187 _dst->_drag_dst = _src->parent->_drag_src;
198 void finish(
bool cancel =
false)
204 auto const self_ref = std::move(_src->parent->_drag_src);
205 assert(self_ref.get() ==
this);
207 _dst->_drag_dst = {};
211 _src->set_visible(
true);
212 if (_src->parent->_plus_btn.get_popover()) {
213 _src->parent->_plus_btn.set_visible();
215 _src->parent->queue_resize();
217 if (_widget && _widget->get_parent() == _dst) {
220 _dst->queue_resize();
223 _src->parent->_signal_dnd_end.emit(cancel);
226 if (!_dst && _src->parent->_tabs.size() == 1) {
236 _drag->drag_drop_done(
true);
242 }
else if (_dst == _src->parent) {
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);
251 _src->parent->queue_resize();
259 _dst->_signal_move_tab.emit(*_src, _src->parent->get_tab_position(*_src), *_src->parent, *_drop_i);
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; }
275 TabWidget *
const _src;
277 Glib::RefPtr<Gdk::Device>
const _device;
280 std::optional<int> _drop_x;
281 std::optional<int> _drop_i;
283 sigc::scoped_connection _reparent_conn;
284 sigc::scoped_connection _cancel_conn;
285 sigc::scoped_connection _drop_conn;
286 Glib::RefPtr<Gdk::Drag> _drag;
287 std::unique_ptr<SimpleTab> _widget;
288 guint _tick_callback = 0;
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());
302 _cancel_conn = _drag->signal_cancel().connect([
this] (
auto reason) {
303 finish(reason == Gdk::DragCancelReason::USER_CANCELLED);
307 _drop_conn = _drag->signal_drop_performed().connect([
this] {
312 _src->set_visible(
false);
313 _src->parent->_plus_btn.set_visible();
316 _widget = std::make_unique<SimpleTab>(*_src);
318 _widget->set_active();
321 _src->parent->_signal_dnd_begin.emit();
326 void _queueReparent()
328 if (!_reparent_conn) {
329 _reparent_conn = Glib::signal_idle().connect([
this] { _reparentWidget();
return false; }, Glib::PRIORITY_HIGH);
333 void _reparentWidget()
335 auto drag_icon = Gtk::DragIcon::get_for_drag(_drag);
337 if (_widget.get() == drag_icon->get_child()) {
338 drag_icon->unset_child();
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()));
348 _widget->insert_before(*_dst, *_dst->_overlay);
349 _dst->queue_resize();
353 drag_icon->set_child(*_widget);
354 _drag->set_hotspot(_offset.
x(), _offset.
y());
361static std::shared_ptr<TabWidgetDrag>
get_tab_drag(Gtk::DropTarget &droptarget)
364 auto const drag = droptarget.get_drop()->get_drag();
368 auto const content = GlibValue::from_content_provider<std::weak_ptr<TabWidgetDrag>>(*drag->get_content());
372 return content->lock();
376 _overlay{
Gtk::make_managed<PointerTransparentWidget>()}
378 set_name(
"TabStrip");
379 set_overflow(Gtk::Overflow::HIDDEN);
383 _plus_btn.set_valign(Gtk::Align::CENTER);
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) {
398 auto const [tab_weak, tab_pos] = _tabAtPoint({x, y});
399 auto tab = tab_weak.lock();
402 switch (click->get_current_button()) {
403 case GDK_BUTTON_PRIMARY:
406 translate_coordinates(tab->close, x, y, xc, yc);
407 if (!tab->close.contains(xc, yc)) {
414 case GDK_BUTTON_SECONDARY: {
416 auto size = tab->get_allocation();
422 case GDK_BUTTON_MIDDLE:
431 click->signal_released().connect([
this] (
auto&&...) {
436 _finish_conn = Glib::signal_timeout().connect([
this] {
if (_drag_src) _drag_src->finish();
return false; }, 100);
439 add_controller(click);
441 auto motion = Gtk::EventControllerMotion::create();
442 motion->signal_motion().connect([
this, &motion = *motion] (
double x,
double y) {
444 auto const tab = _left_clicked.lock();
449 constexpr int drag_initiate_dist = 8;
457 translate_coordinates(*tab, _left_click_pos.x(), _left_click_pos.y(),
offset.x(),
offset.y());
460 _drag_src = _drag_dst = std::make_shared<TabWidgetDrag>(
463 motion.get_current_event_device()
467 tab->insert_before(*
this, _plus_btn);
470 if (!_drag_src->widget()) {
474 add_controller(motion);
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 {
479 tabdrag->cancelTick();
480 tabdrag->setDst(
this);
485 droptarget->signal_enter().connect(handler,
false);
486 droptarget->signal_motion().connect(handler,
false);
487 droptarget->signal_leave().connect([
this] {
489 _drag_dst->addTick();
492 add_controller(droptarget);
502 _drag_dst->setDst(
nullptr);
505 _drag_src->finish(
true);
509Gtk::Widget* TabStrip::add_tab(
const Glib::ustring&
label,
const Glib::ustring& icon,
int pos)
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;
518 auto ptr_tab = tab.get();
519 tab->close.signal_clicked().connect([
this, ptr_tab] { _signal_close_tab.emit(*ptr_tab); });
521 tab->signal_query_tooltip().connect([
this, ptr_tab] (
int,
int,
bool, Glib::RefPtr<Gtk::Tooltip>
const &tooltip) {
522 _setTooltip(*ptr_tab, tooltip);
529 else if (pos < 0 || pos >= _tabs.size()) {
532 assert(0 <= pos && pos <= _tabs.size());
534 tab->insert_before(*
this, _plus_btn);
535 _tabs.insert(_tabs.begin() + pos, tab);
541void TabStrip::remove_tab(
const Gtk::Widget& tab)
543 int const i = get_tab_position(tab);
546 g_warning(
"TabStrip:remove_tab(): attempt to remove a tab that doesn't belong to this widget");
550 if (_drag_src && _drag_src->src() == _tabs[i].get()) {
551 _drag_src->finish(
true);
554 _tabs[i]->unparent();
555 _tabs.erase(_tabs.begin() + i);
560void TabStrip::remove_tab_at(
int pos) {
561 if (
auto tab = get_tab_at(pos)) {
566bool TabStrip::is_tab_active(
const Gtk::Widget& tab)
const {
567 auto const active = _active.lock();
569 return active && active.get() == &tab;
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));
581GType TabStrip::get_dnd_source_type() {
582 return GlibValue::type<std::weak_ptr<TabWidgetDrag>>();
585std::optional<std::pair<TabStrip*, int>> TabStrip::unpack_drop_source(
const Glib::ValueBase& value) {
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();
591 return std::make_pair(ptr->src()->parent, ptr->src()->parent->get_tab_position(*ptr->src()));
602void TabStrip::select_tab(
const Gtk::Widget& tab)
604 auto const active = _active.lock();
606 if (active && active.get() == &tab) {
611 active->set_inactive();
615 int const i = get_tab_position(tab);
617 _tabs[i]->set_active();
622void TabStrip::select_tab_at(
int pos) {
623 if (
auto tab = get_tab_at(pos)) {
628int TabStrip::get_tab_position(
const Gtk::Widget& tab)
const
630 for (
int i = 0; i < _tabs.size(); i++) {
631 if (_tabs[i].get() == &tab) {
638Gtk::Widget* TabStrip::get_tab_at(
int i)
const
640 auto index =
static_cast<size_t>(i);
641 return index < _tabs.size() ? _tabs[
index].get() :
nullptr;
644void TabStrip::set_new_tab_popup(Gtk::Popover* popover) {
646 _plus_btn.set_popover(*popover);
647 _plus_btn.set_visible();
650 _plus_btn.unset_popover();
651 _plus_btn.set_visible(
false);
655void TabStrip::set_tabs_context_popup(Gtk::Popover* popover) {
656 if (_popover) _popover->unparent();
659 popover->set_parent(*
this);
664void TabStrip::enable_rearranging_tabs(
bool enable) {
665 _can_rearrange = enable;
669 _show_labels = labels;
671 for (
auto& tab : _tabs) {
672 tab->_show_labels = labels;
673 tab->update(is_tab_active(*tab));
678void TabStrip::_updateVisibility()
683TabWidget* TabStrip::find_tab(Gtk::Widget& tab) {
684 for (
auto& t : _tabs) {
685 if (t.get() == &tab) {
692Gtk::SizeRequestMode TabStrip::get_request_mode_vfunc()
const
694 return Gtk::SizeRequestMode::CONSTANT_SIZE;
697void TabStrip::measure_vfunc(Gtk::Orientation orientation,
int,
int &min,
int &nat,
int &,
int &)
const
699 if (orientation == Gtk::Orientation::VERTICAL) {
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);
705 for (
auto const &tab : _tabs) {
709 if (
auto widget = _drag_src->widget()) {
714 if (
auto widget = _drag_dst->widget()) {
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;
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;
745void shrink_sizes(std::vector<Size>& sizes,
int decrease) {
746 if (sizes.empty() || decrease <= 0)
return;
749 std::sort(begin(sizes),
end(sizes), [](
const Size& a,
const Size& b){
return a.delta > b.delta; });
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);
756 sizes.emplace_back(Size{ .minimum = 0, .delta = 0, .index = 99999 });
758 auto entry = &sizes.front();
759 while (decrease > 0) {
763 if (decrease == 0)
break;
765 if (entry[1].
delta > entry->delta) {
769 entry = &sizes.front();
774 std::sort(begin(sizes),
end(sizes), [](
const Size& a,
const Size& b){
return a.index < b.index; });
781 auto plus_w = _plus_btn.get_visible() ? _plus_btn.measure(Gtk::Orientation::HORIZONTAL, -1).sizes.natural : 0;
783 _overlay->size_allocate(Gtk::Allocation(0, 0,
width,
height), -1);
795 std::optional<Drop> drop;
798 std::vector<Size> alloc;
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;
808 alloc.emplace_back(Size{ .minimum =
minimum, .delta = natural -
minimum, .index = i });
814 for (
int i = 0; i < _tabs.size(); i++) {
818 else if (
width < total) {
820 shrink_sizes(alloc, total -
width);
826 if (_drag_dst && _drag_dst->dropX()) {
827 auto widget = !_drag_dst->widget()
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;
834 w = alloc[pos].size();
837 auto x = right > 0 ? std::clamp(*_drag_dst->dropX(), 0, right) : 0;
838 drop = Drop{ .x = x, .w =
w, .widget = widget };
844 for (
int i = 0; i < _tabs.size(); i++) {
845 const auto& a = alloc[i];
846 auto const tab = _tabs[i].get();
848 if (_drag_src && tab == _drag_src->src()) {
852 if (drop && !drop->done && x +
w / 2 > drop->x) {
854 _drag_dst->setDropI(i);
857 tab->size_allocate(Gtk::Allocation(x, 0,
w,
height), -1);
861 if (_plus_btn.get_visible()) {
862 _plus_btn.size_allocate(Gtk::Allocation(x, 0, plus_w,
height), -1);
872 _drag_dst->setDropI(_tabs.size());
874 drop->widget->size_allocate(Gtk::Allocation(drop->x, 0, drop->w,
height), -1);
878void TabStrip::_setTooltip(
const TabWidget& tab, Glib::RefPtr<Gtk::Tooltip>
const &tooltip)
880 tooltip->set_text(tab.name.get_text());
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);
890 if (it == _tabs.end()) {
893 return {*it, {xt, yt}};
896bool TabStrip::_reorderTab(
int from,
int to)
898 assert(0 <= from && from < _tabs.size());
899 assert(0 <= to && to <= _tabs.size());
901 if (from == to || from + 1 == to) {
905 auto tab = std::move(_tabs[from]);
906 _tabs.erase(_tabs.begin() + from);
907 _tabs.insert(_tabs.begin() + to - (to > from), std::move(tab));
C distanceSq(CPoint const &p) const
Get rectangle's distance SQUARED away from the given point.
Two-dimensional point that doubles as a vector.
constexpr Coord y() const noexcept
constexpr Coord x() const noexcept
Axis aligned, non-empty rectangle.
static char const *const parent
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.
Glib::ValueBase create(Args &&... args)
Return a value containing and owning a newly-created T instance.
Miscellaneous supporting code.
static void append(std::vector< T > &target, std::vector< T > &&source)
Generic tab strip widget.
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.
Wrapper for the GLib value API.