5#include <glibmm/i18n.h>
7#include <glibmm/main.h>
8#include <gtkmm/button.h>
9#include <gtkmm/dragicon.h>
10#include <gtkmm/dragsource.h>
11#include <gtkmm/droptarget.h>
12#include <gtkmm/eventcontrollermotion.h>
13#include <gtkmm/gestureclick.h>
14#include <gtkmm/label.h>
15#include <gtkmm/picture.h>
16#include <gtkmm/popovermenu.h>
17#include <gtkmm/snapshot.h>
18#include <gtkmm/tooltip.h>
32#define BUILD(name) name{UI::get_widget<std::remove_reference_t<decltype(name)>>(builder, #name)}
45 TooltipUI(Glib::RefPtr<Gtk::Builder>
const &
builder)
53 root.signal_unmap().connect([
this] {
68 struct CurrentDisplayInfo
72 auto operator<=>(CurrentDisplayInfo
const &)
const =
default;
77struct PointerTransparentWidget : Gtk::Widget
79 bool contains_vfunc(
double,
double)
const override {
return false; }
82Gtk::PopoverMenu to_popovermenu(Glib::RefPtr<Gio::Menu>
const &menu)
84 auto popovermenu = Gtk::PopoverMenu{menu};
85 popovermenu.set_has_arrow(
false);
86 popovermenu.set_position(Gtk::PositionType::BOTTOM);
87 popovermenu.set_halign(Gtk::Align::START);
91Gtk::PopoverMenu create_context_menu()
93 auto menu = Gio::Menu::create();
94 menu->append_item(Gio::MenuItem::create(_(
"Detach to New Window"),
"tabs.detach"));
95 menu->append_item(Gio::MenuItem::create(_(
"Close File"),
"tabs.close"));
96 return to_popovermenu(menu);
99Gtk::PopoverMenu &get_context_menu()
101 static auto menu = create_context_menu();
105Gtk::PopoverMenu create_context_menu_background()
107 auto menu = Gio::Menu::create();
108 auto sec1 = Gio::Menu::create();
109 sec1->append_item(Gio::MenuItem::create(_(
"Close"),
"win.document-close"));
110 menu->append_section(sec1);
111 auto sec2 = Gio::Menu::create();
112 sec2->append_item(Gio::MenuItem::create(_(
"New Document..."),
"win.document-new"));
113 sec2->append_item(Gio::MenuItem::create(_(
"Open Document..."),
"win.document-open"));
114 menu->append_section(sec2);
115 return to_popovermenu(menu);
118Gtk::PopoverMenu &get_context_menu_background()
120 static auto menu = create_context_menu_background();
124std::optional<Geom::Point> get_current_pointer_pos(Glib::RefPtr<Gdk::Device>
const &pointer, Gtk::Widget &widget)
127 Gdk::ModifierType mask;
128 auto root = widget.get_root();
129 dynamic_cast<Gtk::Native &
>(*root).get_surface()->get_device_position(pointer, x, y, mask);
130 dynamic_cast<Gtk::Widget &
>(*root).translate_coordinates(widget, x, y, x, y);
137 auto result = Glib::ustring{};
141 if (doc->isModifiedSinceSave()) {
149 result += Glib::ustring::format(v);
159class TabsWidget::Instances
162 static Instances &
get()
164 static Instances instance;
168 void add(TabsWidget *
w)
170 _instances.push_back(
w);
171 if (_instances.size() > 1) {
172 _updateVisibilityAll();
178 _instances.erase(std::find(_instances.begin(), _instances.end(),
w));
179 if (_instances.size() <= 1) {
180 _updateVisibilityAll();
184 bool forceVisible()
const {
return _instances.size() > 1; }
188 for (
auto w : _instances) {
189 w->add_css_class(
"drop-highlight");
193 void removeHighlight()
195 for (
auto w : _instances) {
196 w->remove_css_class(
"drop-highlight");
201 Instances() =
default;
203 void _updateVisibilityAll()
205 for (
auto w : _instances) {
206 w->_updateVisibility();
210 std::vector<TabsWidget *> _instances;
214struct DumbTab : Gtk::Box
223 DumbTab(Glib::RefPtr<Gtk::Builder>
const &
builder)
227 set_name(
"DocumentTab");
233 get_style_context()->add_class(
"tab_active");
238 get_style_context()->remove_class(
"tab_active");
248 Tab(
SPDesktop *desktop, TabsWidget *parent)
252 set_has_tooltip(
true);
265 , _device{
std::move(device)}
271 void motion(std::optional<Geom::Point> pos)
273 constexpr int detach_dist = 50;
276 _drop_x = pos->x() -
static_cast<int>(std::round(_offset.x()));
277 _dst->queue_allocate();
291 if (!_tick_callback) {
292 _tick_callback = _dst->add_tick_callback([
this] (
auto&&) {
293 motion(get_current_pointer_pos(_device, *_dst));
301 if (_tick_callback) {
302 _dst->remove_tick_callback(_tick_callback);
308 void setDst(TabsWidget *new_dst)
310 if (new_dst == _dst) {
315 _dst->_drag_dst = {};
316 _dst->queue_resize();
322 _dst->_drag_dst = _src->parent->_drag_src;
333 void finish(
bool cancel =
false)
339 auto const self_ref = std::move(_src->parent->_drag_src);
340 assert(self_ref.get() ==
this);
342 _dst->_drag_dst = {};
346 _src->set_visible(
true);
347 _src->parent->queue_resize();
349 if (_widget && _widget->get_parent() == _dst) {
352 _dst->queue_resize();
354 TabsWidget::Instances::get().removeHighlight();
356 if (!_dst && _src->parent->_tabs.size() == 1) {
361 _src->parent->_desktop_widget->get_window()->present();
366 _drag->drag_drop_done(
true);
372 }
else if (_dst == _src->parent) {
375 int const from = _src->parent->positionOfTab(_src->desktop);
376 _src->parent->_reorderTab(from, *_drop_i);
381 auto const desktop = _src->desktop;
383 _dst->_desktop_widget->addDesktop(
desktop, *_drop_i);
388 Tab *src()
const {
return _src; }
389 DumbTab *widget()
const {
return _widget.get(); }
390 std::optional<int>
const &dropX()
const {
return _drop_x; }
391 void setDropI(
int i) { _drop_i = i; }
396 Glib::RefPtr<Gdk::Device>
const _device;
399 std::optional<int> _drop_x;
400 std::optional<int> _drop_i;
402 sigc::scoped_connection _reparent_conn;
403 sigc::scoped_connection _cancel_conn;
404 sigc::scoped_connection _drop_conn;
405 Glib::RefPtr<Gdk::Drag> _drag;
406 std::unique_ptr<DumbTab> _widget;
407 guint _tick_callback = 0;
416 assert(_src->parent->_drag_src.get() ==
this);
417 auto content = Gdk::ContentProvider::create(
GlibValue::create<std::weak_ptr<TabDrag>>(_src->parent->_drag_src));
418 _drag = _src->parent->get_native()->get_surface()->drag_begin_from_point(_device, content, Gdk::DragAction::MOVE, _offset.
x(), _offset.
y());
421 _cancel_conn = _drag->signal_cancel().connect([
this] (
auto reason) {
422 finish(reason == Gdk::DragCancelReason::USER_CANCELLED);
426 _drop_conn = _drag->signal_drop_performed().connect([
this] {
431 _src->set_visible(
false);
434 _widget = std::make_unique<DumbTab>();
435 _widget->name.set_text(
get_title(_src->desktop));
436 _widget->set_active();
441 void _queueReparent()
443 if (!_reparent_conn) {
444 _reparent_conn = Glib::signal_idle().connect([
this] { _reparentWidget();
return false; }, Glib::PRIORITY_HIGH);
448 void _reparentWidget()
450 auto drag_icon = Gtk::DragIcon::get_for_drag(_drag);
452 if (_widget.get() == drag_icon->get_child()) {
453 drag_icon->unset_child();
455 Gtk::DragIcon::set_from_paintable(_drag,
to_texture(Cairo::ImageSurface::create(Cairo::ImageSurface::Format::ARGB32, 1, 1)), 0, 0);
456 }
else if (_widget->get_parent()) {
457 assert(
dynamic_cast<TabsWidget *
>(_widget->get_parent()));
463 _widget->insert_before(*_dst, *_dst->_overlay);
464 _dst->queue_resize();
465 TabsWidget::Instances::get().removeHighlight();
467 drag_icon->set_child(*_widget);
468 _drag->set_hotspot(_offset.
x(), _offset.
y());
469 TabsWidget::Instances::get().addHighlight();
474static std::shared_ptr<TabDrag>
get_tab_drag(Gtk::DropTarget &droptarget)
477 auto const drag = droptarget.get_drop()->get_drag();
481 auto const content = GlibValue::from_content_provider<std::weak_ptr<TabDrag>>(*drag->get_content());
485 return content->lock();
492 return tab ? tab->desktop :
nullptr;
496 : _desktop_widget{desktop_widget}
497 , _overlay{
Gtk::make_managed<PointerTransparentWidget>()}
499 set_name(
"DocumentTabsWidget");
500 set_overflow(Gtk::Overflow::HIDDEN);
506 auto click = Gtk::GestureClick::create();
507 click->set_button(0);
508 click->signal_pressed().connect([
this, click = click.get()] (
int,
double x,
double y) {
510 auto const [tab_weak, tab_pos] = _tabAtPoint({x, y});
511 auto tab = tab_weak.lock();
514 switch (click->get_current_button()) {
515 case GDK_BUTTON_PRIMARY:
518 translate_coordinates(tab->close, x, y, xc, yc);
519 if (!tab->close.contains(xc, yc)) {
526 case GDK_BUTTON_SECONDARY: {
527 auto &menu = tab ? get_context_menu() : get_context_menu_background();
528 auto old_parent =
dynamic_cast<TabsWidget *
>(menu.get_parent());
529 if (old_parent !=
this) {
534 menu.set_parent(*
this);
541 case GDK_BUTTON_MIDDLE:
550 click->signal_released().connect([
this] (
auto&&...) {
556 add_controller(click);
558 auto motion = Gtk::EventControllerMotion::create();
559 motion->signal_motion().connect([
this, &motion = *motion] (
double x,
double y) {
561 auto const tab = _left_clicked.lock();
566 constexpr int drag_initiate_dist = 5;
574 translate_coordinates(*tab, _left_click_pos.x(), _left_click_pos.y(),
offset.x(),
offset.y());
577 _drag_src = _drag_dst = std::make_shared<TabDrag>(
580 motion.get_current_event_device()
584 tab->insert_before(*
this, *_overlay);
587 if (!_drag_src->widget()) {
591 add_controller(motion);
593 auto droptarget = Gtk::DropTarget::create(
GlibValue::type<std::weak_ptr<TabDrag>>(), Gdk::DragAction::MOVE);
594 auto handler = [
this, &droptarget = *droptarget] (
double x,
double y) -> Gdk::DragAction {
596 tabdrag->cancelTick();
597 tabdrag->setDst(
this);
602 droptarget->signal_enter().connect(handler,
false);
603 droptarget->signal_motion().connect(handler,
false);
604 droptarget->signal_leave().connect([
this] {
606 _drag_dst->addTick();
609 add_controller(droptarget);
611 auto actiongroup = Gio::SimpleActionGroup::create();
612 actiongroup->add_action(
"detach", [
this] {
614 InkscapeApplication::instance()->detachDesktopToNewWindow(desktop);
617 actiongroup->add_action(
"close", [
this] {
619 InkscapeApplication::instance()->destroyDesktop(desktop);
622 insert_action_group(
"tabs", actiongroup);
624 Instances::get().add(
this);
628TabsWidget::~TabsWidget()
630 Instances::get().remove(
this);
635 _drag_dst->setDst(
nullptr);
638 _drag_src->finish(
true);
644 auto tab = std::make_shared<Tab>(
desktop,
this);
645 tab->name.set_text(get_title(
desktop));
649 tab->signal_query_tooltip().connect([
this,
desktop] (
int,
int,
bool, Glib::RefPtr<Gtk::Tooltip>
const &tooltip) {
654 assert(positionOfTab(
desktop) == -1);
659 assert(0 <= pos && pos <= _tabs.size());
661 tab->insert_before(*
this, *_overlay);
662 _tabs.insert(_tabs.begin() + pos, std::move(tab));
669 int const i = positionOfTab(
desktop);
672 if (_drag_src && _drag_src->src() == _tabs[i].get()) {
673 _drag_src->finish(
true);
676 _tabs[i]->unparent();
677 _tabs.erase(_tabs.begin() + i);
684 auto const active = _active.lock();
686 if (active && active->desktop ==
desktop) {
691 active->set_inactive();
695 int const i = positionOfTab(
desktop);
697 _tabs[i]->set_active();
704 int const i = positionOfTab(
desktop);
706 _tabs[i]->name.set_text(get_title(_tabs[i]->
desktop));
711 for (
int i = 0; i < _tabs.size(); i++) {
721 return _tabs[i]->desktop;
724void TabsWidget::_updateVisibility()
726 set_visible(_tabs.size() > 1 || Instances::get().forceVisible());
729Gtk::SizeRequestMode TabsWidget::get_request_mode_vfunc()
const
731 return Gtk::SizeRequestMode::CONSTANT_SIZE;
734void TabsWidget::measure_vfunc(Gtk::Orientation orientation,
int,
int &min,
int &nat,
int &,
int &)
const
736 if (orientation == Gtk::Orientation::VERTICAL) {
738 auto consider = [&] (Gtk::Widget
const &
w) {
739 auto const m =
w.measure(Gtk::Orientation::VERTICAL, -1);
740 min = std::max(min, m.sizes.minimum);
742 for (
auto const &tab : _tabs) {
746 if (
auto widget = _drag_src->widget()) {
751 if (
auto widget = _drag_dst->widget()) {
771 std::optional<Drop> drop;
772 if (_drag_dst && _drag_dst->dropX()) {
773 auto widget = !_drag_dst->widget()
775 : _drag_dst->widget();
776 if (widget->get_parent() ==
this) {
778 .x = *_drag_dst->dropX(),
779 .w = widget->measure(Gtk::Orientation::HORIZONTAL, -1).sizes.natural,
786 for (
int i = 0; i < _tabs.size(); i++) {
787 auto const tab = _tabs[i].get();
788 if (_drag_src && tab == _drag_src->src()) {
791 auto const w = tab->measure(Gtk::Orientation::HORIZONTAL, -1).sizes.natural;
792 if (drop && !drop->done && x +
w / 2 > drop->x) {
794 _drag_dst->setDropI(i);
797 tab->size_allocate(Gtk::Allocation(x, 0,
w,
height), -1);
801 _overlay->size_allocate(Gtk::Allocation(0, 0,
width,
height), -1);
810 _drag_dst->setDropI(_tabs.size());
812 drop->widget->size_allocate(Gtk::Allocation(drop->x, 0, drop->w,
height), -1);
819 static auto const tooltip_ui = std::make_unique<TooltipUI>();
821 auto const active = _active.lock();
823 auto const display_info = TooltipUI::CurrentDisplayInfo{
828 if (tooltip_ui->current_display_info != display_info) {
829 tooltip_ui->current_display_info = display_info;
836 tooltip_ui->preview.set_paintable({});
838 constexpr double scale = 0.2;
839 auto snapshot = Gtk::Snapshot::create();
842 tooltip_ui->preview.set_paintable(snapshot->to_paintable());
847 tooltip->set_custom(tooltip_ui->root);
853 auto const it = std::find_if(_tabs.begin(), _tabs.end(), [&] (
auto const &tab) {
854 translate_coordinates(*tab, pos.x(), pos.y(), xt, yt);
855 return tab->contains(xt, yt);
857 if (it == _tabs.end()) {
860 return {*it, {xt, yt}};
863void TabsWidget::_reorderTab(
int from,
int to)
865 assert(0 <= from && from < _tabs.size());
866 assert(0 <= to && to <= _tabs.size());
872 auto tab = std::move(_tabs[from]);
873 _tabs.erase(_tabs.begin() + from);
874 _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.
bool destroyDesktop(SPDesktop *desktop, bool keep_alive=false)
Destroy a window and close the document it contains.
static InkscapeApplication * instance()
Singleton instance.
void detachDesktopToNewWindow(SPDesktop *desktop)
To do: update description of desktop.
Inkscape::UI::Widget::Canvas * getCanvas() const
SPDesktopWidget * getDesktopWidget() const
char const * getDocumentName() const
basename or other human-readable label for the document.
Editable view implementation.
static char const *const parent
Inkscape - An SVG editor.
Glib::ustring get_title(const SPObject &object)
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.
T * get(GValue *value)
Returns a borrowed pointer to the T held by a value if it holds one, else nullptr.
Miscellaneous supporting code.
static void append(std::vector< T > &target, std::vector< T > &&source)
void remove(std::vector< T > &vec, T const &val)
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.