summaryrefslogtreecommitdiffstats
path: root/src/widgets
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/widgets
parentInitial commit. (diff)
downloadinkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz
inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/widgets')
-rw-r--r--src/widgets/CMakeLists.txt22
-rw-r--r--src/widgets/README10
-rw-r--r--src/widgets/desktop-widget.cpp1937
-rw-r--r--src/widgets/desktop-widget.h266
-rw-r--r--src/widgets/paintdef.cpp190
-rw-r--r--src/widgets/paintdef.h91
-rw-r--r--src/widgets/sp-attribute-widget.cpp297
-rw-r--r--src/widgets/sp-attribute-widget.h172
-rw-r--r--src/widgets/sp-xmlview-tree.cpp936
-rw-r--r--src/widgets/sp-xmlview-tree.h80
-rw-r--r--src/widgets/spw-utilities.cpp216
-rw-r--r--src/widgets/spw-utilities.h61
-rw-r--r--src/widgets/style-utils.h35
-rw-r--r--src/widgets/toolbox.cpp641
-rw-r--r--src/widgets/toolbox.h80
-rw-r--r--src/widgets/widget-sizes.h38
16 files changed, 5072 insertions, 0 deletions
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt
new file mode 100644
index 0000000..3701fc5
--- /dev/null
+++ b/src/widgets/CMakeLists.txt
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set(widgets_SRC
+ desktop-widget.cpp
+ paintdef.cpp
+ sp-attribute-widget.cpp
+ sp-xmlview-tree.cpp
+ spw-utilities.cpp
+ toolbox.cpp
+
+ # -------
+ # Headers
+ desktop-widget.h
+ paintdef.h
+ sp-attribute-widget.h
+ sp-xmlview-tree.h
+ spw-utilities.h
+ toolbox.h
+ widget-sizes.h
+)
+
+add_inkscape_source("${widgets_SRC}")
diff --git a/src/widgets/README b/src/widgets/README
new file mode 100644
index 0000000..b1c905a
--- /dev/null
+++ b/src/widgets/README
@@ -0,0 +1,10 @@
+
+
+This directory contains widgets written in 'C'.
+
+To do:
+
+* Replace 'C' widgets by 'C++' widgets.
+* Temporary move to 'ui/widgets/legacy'.
+
+
diff --git a/src/widgets/desktop-widget.cpp b/src/widgets/desktop-widget.cpp
new file mode 100644
index 0000000..edb4200
--- /dev/null
+++ b/src/widgets/desktop-widget.cpp
@@ -0,0 +1,1937 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * Desktop widget implementation
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * MenTaLguY <mental@rydia.net>
+ * bulia byak <buliabyak@users.sf.net>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * John Bintz <jcoswell@coswellproductions.org>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2007 Johan Engelen
+ * Copyright (C) 2006 John Bintz
+ * Copyright (C) 2004 MenTaLguY
+ * Copyright (C) 1999-2002 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <2geom/rect.h>
+
+#include "attributes.h"
+#include "cms-system.h"
+#include "conn-avoid-ref.h"
+#include "desktop-events.h"
+#include "desktop-widget.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "ege-color-prof-tracker.h"
+#include "enums.h"
+#include "file.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+#include "inkscape-version.h"
+
+#include "display/control/canvas-item-drawing.h"
+#include "display/control/canvas-item-guideline.h"
+
+#include "extension/db.h"
+
+#include "object/sp-image.h"
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+#include "object/sp-grid.h"
+
+#include "ui/shortcuts.h"
+#include "ui/dialog/swatches.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/monitor.h" // Monitor aspect ratio
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/dialog-multipaned.h"
+#include "ui/dialog/dialog-window.h"
+#include "ui/tools/box3d-tool.h"
+#include "ui/tools/text-tool.h"
+#include "ui/util.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/canvas-grid.h"
+#include "ui/widget/combo-tool-item.h"
+#include "ui/widget/ink-ruler.h"
+#include "ui/widget/layer-selector.h"
+#include "ui/widget/page-selector.h"
+#include "ui/widget/selected-style.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+#include "ui/themes.h"
+
+#include "util/units.h"
+
+// We're in the "widgets" directory, so no need to explicitly prefix these:
+#include "spw-utilities.h"
+#include "toolbox.h"
+#include "widget-sizes.h"
+
+#include "ui/widget/color-palette.h"
+#include "widgets/paintdef.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::UI::Dialog::DialogContainer;
+using Inkscape::UI::Dialog::DialogMultipaned;
+using Inkscape::UI::Dialog::DialogWindow;
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::UI::ToolboxFactory;
+using Inkscape::Util::unit_table;
+
+
+//---------------------------------------------------------------------
+/* SPDesktopWidget */
+
+class CMSPrefWatcher {
+public:
+ CMSPrefWatcher() :
+ _dpw(*this),
+ _tracker(ege_color_prof_tracker_new(nullptr))
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ g_signal_connect( G_OBJECT(_tracker), "modified", G_CALLBACK(hook), this );
+ prefs->addObserver(_dpw);
+ }
+ virtual ~CMSPrefWatcher() = default;
+
+ //virtual void notify(PrefValue &);
+ void add( SPDesktopWidget* dtw ) {
+ _widget_list.push_back(dtw);
+ }
+ void remove( SPDesktopWidget* dtw ) {
+ _widget_list.remove(dtw);
+ }
+
+private:
+ static void hook(EgeColorProfTracker *tracker, gint b, CMSPrefWatcher *watcher);
+
+ class DisplayProfileWatcher : public Inkscape::Preferences::Observer {
+ public:
+ DisplayProfileWatcher(CMSPrefWatcher &pw) : Observer("/options/displayprofile"), _pw(pw) {}
+ void notify(Inkscape::Preferences::Entry const &/*val*/) override {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _pw._setCmsSensitive(!prefs->getString("/options/displayprofile/uri").empty());
+ }
+ private:
+ CMSPrefWatcher &_pw;
+ };
+
+ DisplayProfileWatcher _dpw;
+
+ void _setCmsSensitive(bool value);
+
+ std::list<SPDesktopWidget*> _widget_list;
+ EgeColorProfTracker *_tracker;
+
+ friend class DisplayProfileWatcher;
+};
+
+void CMSPrefWatcher::hook(EgeColorProfTracker * /*tracker*/, gint monitor, CMSPrefWatcher * /*watcher*/)
+{
+ unsigned char* buf = nullptr;
+ guint len = 0;
+
+ ege_color_prof_tracker_get_profile_for( monitor, reinterpret_cast<gpointer*>(&buf), &len );
+ Glib::ustring id = Inkscape::CMSSystem::setDisplayPer( buf, len, monitor );
+}
+
+void CMSPrefWatcher::_setCmsSensitive(bool enabled)
+{
+ for ( auto dtw : _widget_list ) {
+ auto cms_adj = dtw->get_canvas_grid()->GetCmsAdjust();
+ if ( cms_adj->get_sensitive() != enabled ) {
+ dtw->cms_adjust_set_sensitive(enabled);
+ }
+ }
+}
+
+static CMSPrefWatcher* watcher = nullptr;
+
+SPDesktopWidget::SPDesktopWidget(InkscapeWindow* inkscape_window)
+ : window (inkscape_window)
+{
+ auto *const dtw = this;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /* Main table */
+ dtw->_vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ dtw->_vbox->set_name("DesktopMainTable");
+ dtw->add(*dtw->_vbox);
+
+ /* Status bar */
+ dtw->_statusbar = Gtk::manage(new Gtk::Box());
+ dtw->_statusbar->set_name("DesktopStatusBar");
+ dtw->_vbox->pack_end(*dtw->_statusbar, false, true);
+
+ /* Swatch Bar */
+ dtw->_panels = Gtk::manage(new Inkscape::UI::Dialog::SwatchesPanel("/embedded/swatches"));
+ dtw->_panels->set_vexpand(false);
+ dtw->_vbox->pack_end(*dtw->_panels, false, true);
+
+ /* DesktopHBox (Vertical toolboxes, canvas) */
+ dtw->_hbox = Gtk::manage(new Gtk::Box());
+ dtw->_hbox->set_name("DesktopHbox");
+
+ dtw->_tbbox = Gtk::manage(new Gtk::Paned(Gtk::ORIENTATION_HORIZONTAL));
+ dtw->_tbbox->set_name("ToolboxCanvasPaned");
+ dtw->_hbox->pack_start(*dtw->_tbbox, true, true);
+
+ dtw->_vbox->pack_end(*dtw->_hbox, true, true);
+
+ dtw->_top_toolbars = Gtk::make_managed<Gtk::Grid>();
+ dtw->_top_toolbars->set_name("TopToolbars");
+ dtw->_vbox->pack_end(*dtw->_top_toolbars, false, true);
+
+ /* Toolboxes */
+ dtw->aux_toolbox = ToolboxFactory::createAuxToolbox();
+
+ dtw->snap_toolbox = ToolboxFactory::createSnapToolbox();
+
+ dtw->commands_toolbox = ToolboxFactory::createCommandsToolbox();
+ auto cmd = Glib::wrap(dtw->commands_toolbox);
+ dtw->_top_toolbars->attach(*cmd, 0, 0);
+
+ dtw->_top_toolbars->attach(*Glib::wrap(dtw->aux_toolbox), 0, 1);
+
+ dtw->tool_toolbox = ToolboxFactory::createToolToolbox(inkscape_window);
+ ToolboxFactory::setOrientation( dtw->tool_toolbox, GTK_ORIENTATION_VERTICAL );
+ dtw->_tbbox->pack1(*Glib::wrap(dtw->tool_toolbox), false, true);
+
+ _tb_snap_pos = prefs->createObserver("/toolbox/simplesnap", sigc::mem_fun(*this, &SPDesktopWidget::repack_snaptoolbar));
+ repack_snaptoolbar();
+
+ auto tbox_width = prefs->getEntry("/toolbox/tools/width");
+ if (tbox_width.isValid()) {
+ _tbbox->set_position(tbox_width.getIntLimited(32, 8, 500));
+ }
+
+ auto set_visible_buttons = [=](GtkWidget* tb) {
+ int buttons_before_separator = 0;
+ Gtk::Widget* last_sep = nullptr;
+ Gtk::FlowBox* last_box = nullptr;
+ sp_traverse_widget_tree(Glib::wrap(tb), [&](Gtk::Widget* widget) {
+ if (auto flowbox = dynamic_cast<Gtk::FlowBox*>(widget)) {
+ flowbox->show();
+ flowbox->set_no_show_all();
+ flowbox->set_max_children_per_line(1);
+ last_box = flowbox;
+ }
+ else if (auto btn = dynamic_cast<Gtk::Button*>(widget)) {
+ auto name = sp_get_action_target(widget);
+ auto show = prefs->getBool(ToolboxFactory::get_tool_visible_buttons_path(name), true);
+ auto parent = btn->get_parent();
+ if (show) {
+ parent->show();
+ ++buttons_before_separator;
+ // keep the max_children up to date improves display.
+ last_box->set_max_children_per_line(buttons_before_separator);
+ last_sep = nullptr;
+ }
+ else {
+ parent->hide();
+ }
+ }
+ else if (auto sep = dynamic_cast<Gtk::Separator*>(widget)) {
+ if (buttons_before_separator <= 0) {
+ sep->hide();
+ }
+ else {
+ sep->show();
+ buttons_before_separator = 0;
+ last_sep = sep;
+ }
+ }
+ return false;
+ });
+ if (last_sep) {
+ // hide trailing separator
+ last_sep->hide();
+ }
+ };
+ auto set_toolbar_prefs = [=]() {
+ int min = ToolboxFactory::min_pixel_size;
+ int max = ToolboxFactory::max_pixel_size;
+ int s = prefs->getIntLimited(ToolboxFactory::tools_icon_size, min, min, max);
+ Inkscape::UI::set_icon_sizes(tool_toolbox, s);
+ };
+
+ // watch for changes
+ _tb_icon_sizes1 = prefs->createObserver(ToolboxFactory::tools_icon_size, [=]() { set_toolbar_prefs(); });
+ _tb_icon_sizes2 = prefs->createObserver(ToolboxFactory::ctrlbars_icon_size, [=]() { apply_ctrlbar_settings(); });
+ _tb_visible_buttons = prefs->createObserver(ToolboxFactory::tools_visible_buttons, [=]() { set_visible_buttons(tool_toolbox); });
+
+ // restore preferences
+ set_toolbar_prefs();
+ apply_ctrlbar_settings();
+ set_visible_buttons(tool_toolbox);
+
+ /* Canvas Grid (canvas, rulers, scrollbars, etc.) */
+ // desktop widgets owns it
+ _canvas_grid = new Inkscape::UI::Widget::CanvasGrid(this);
+
+ /* Canvas */
+ dtw->_canvas = _canvas_grid->GetCanvas();
+ dtw->_canvas->set_cms_active(prefs->getBool("/options/displayprofile/enable"));
+
+ _ds_sticky_zoom = prefs->createObserver("/options/stickyzoom/value", [=]() { sticky_zoom_updated(); });
+ sticky_zoom_updated();
+
+ /* Dialog Container */
+ _container = Gtk::manage(new DialogContainer(inkscape_window));
+ _columns = _container->get_columns();
+ _columns->set_dropzone_sizes(2, -1);
+ dtw->_tbbox->pack2(*_container, true, true);
+
+ _canvas_grid->set_hexpand(true);
+ _canvas_grid->set_vexpand(true);
+ _columns->append(_canvas_grid);
+
+ // --------------- Status Tool Bar ------------------//
+
+ // Selected Style (Fill/Stroke/Opacity)
+ dtw->_selected_style = Gtk::manage(new Inkscape::UI::Widget::SelectedStyle(true));
+ dtw->_statusbar->pack_start(*dtw->_selected_style, false, false);
+ _selected_style->show_all();
+ _selected_style->set_no_show_all();
+
+ // Layer Selector
+ _layer_selector = Gtk::manage(new Inkscape::UI::Widget::LayerSelector(nullptr));
+ // separate layer selector buttons from status text
+ auto vseparator = Gtk::make_managed<Gtk::Separator>(Gtk::ORIENTATION_VERTICAL);
+ vseparator->set_margin_end(6);
+ vseparator->set_margin_top(6);
+ vseparator->set_margin_bottom(6);
+ _layer_selector->pack_end(*vseparator);
+ _layer_selector->show_all();
+ _layer_selector->set_no_show_all();
+ dtw->_statusbar->pack_start(*_layer_selector, false, false, 1);
+
+ // Select Status
+ dtw->_select_status = Gtk::manage(new Gtk::Label());
+ dtw->_select_status->set_name("SelectStatus");
+ dtw->_select_status->set_ellipsize(Pango::ELLIPSIZE_END);
+ dtw->_select_status->set_line_wrap(true);
+ dtw->_select_status->set_lines(2);
+ dtw->_select_status->set_halign(Gtk::ALIGN_START);
+ dtw->_select_status->set_size_request(1, -1);
+
+ // Display the initial welcome message in the statusbar
+ dtw->_select_status->set_markup(_("<b>Welcome to Inkscape!</b> Use shape or freehand tools to create objects; use selector (arrow) to move or transform them."));
+
+ dtw->_statusbar->pack_start(*dtw->_select_status, true, true);
+
+ dtw->_zoom_status_box = Gtk::make_managed<Gtk::Box>();
+ // Zoom status spinbutton ---------------
+ auto zoom_adj = Gtk::Adjustment::create(100.0, log(SP_DESKTOP_ZOOM_MIN)/log(2), log(SP_DESKTOP_ZOOM_MAX)/log(2), 0.1);
+ dtw->_zoom_status = Gtk::manage(new Inkscape::UI::Widget::SpinButton(zoom_adj));
+
+ dtw->_zoom_status->set_defocus_widget(dtw->_canvas);
+ dtw->_zoom_status->set_tooltip_text(_("Zoom"));
+ dtw->_zoom_status->set_size_request(STATUS_ZOOM_WIDTH, -1);
+ dtw->_zoom_status->set_width_chars(6);
+ dtw->_zoom_status->set_numeric(false);
+ dtw->_zoom_status->set_update_policy(Gtk::UPDATE_ALWAYS);
+
+ // Callbacks
+ dtw->_zoom_status_input_connection = dtw->_zoom_status->signal_input().connect(sigc::mem_fun(*dtw, &SPDesktopWidget::zoom_input));
+ dtw->_zoom_status_output_connection = dtw->_zoom_status->signal_output().connect(sigc::mem_fun(*dtw, &SPDesktopWidget::zoom_output));
+ dtw->_zoom_status_value_changed_connection = dtw->_zoom_status->signal_value_changed().connect(sigc::mem_fun(*dtw, &SPDesktopWidget::zoom_value_changed));
+ dtw->_zoom_status_populate_popup_connection = dtw->_zoom_status->signal_populate_popup().connect(sigc::mem_fun(*dtw, &SPDesktopWidget::zoom_populate_popup));
+
+ // Style
+ auto css_provider_spinbutton = Gtk::CssProvider::create();
+ css_provider_spinbutton->load_from_data("* { padding-left: 2px; padding-right: 2px; padding-top: 0px; padding-bottom: 0px;}"); // Shouldn't this be in a style sheet? Used also by rotate.
+
+ dtw->_zoom_status->set_name("ZoomStatus");
+ auto context_zoom = dtw->_zoom_status->get_style_context();
+ context_zoom->add_provider(css_provider_spinbutton, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ dtw->_rotation_status_box = Gtk::make_managed<Gtk::Box>();
+ dtw->_rotation_status_box->set_margin_start(10);
+ // Rotate status spinbutton ---------------
+ auto rotation_adj = Gtk::Adjustment::create(0, -360.0, 360.0, 1.0);
+
+ dtw->_rotation_status = Gtk::manage(new Inkscape::UI::Widget::SpinButton(rotation_adj));
+
+ // FIXME: This is a bit of a hack, to avoid the ExpressionEvaluator struggling to parse the
+ // degree symbol. It would be better to improve ExpressionEvaluator so it copes
+ dtw->_rotation_status->set_dont_evaluate(true);
+
+ dtw->_rotation_status->set_defocus_widget(dtw->_canvas);
+ dtw->_rotation_status->set_tooltip_text(_("Rotation. (Also Ctrl+Shift+Scroll)"));
+ dtw->_rotation_status->set_size_request(STATUS_ROTATION_WIDTH, -1);
+ dtw->_rotation_status->set_width_chars(7);
+ dtw->_rotation_status->set_numeric(false);
+ dtw->_rotation_status->set_digits(2);
+ dtw->_rotation_status->set_increments(1.0, 15.0);
+ dtw->_rotation_status->set_update_policy(Gtk::UPDATE_ALWAYS);
+
+ // Callbacks
+ dtw->_rotation_status_output_connection = dtw->_rotation_status->signal_output().connect(sigc::mem_fun(*dtw, &SPDesktopWidget::rotation_output));
+ dtw->_rotation_status_value_changed_connection = dtw->_rotation_status->signal_value_changed().connect(sigc::mem_fun(*dtw, &SPDesktopWidget::rotation_value_changed));
+ dtw->_rotation_status_populate_popup_connection = dtw->_rotation_status->signal_populate_popup().connect(sigc::mem_fun(*dtw, &SPDesktopWidget::rotation_populate_popup));
+
+ // Style
+ dtw->_rotation_status->set_name("RotationStatus");
+ auto context_rotation = dtw->_rotation_status->get_style_context();
+ context_rotation->add_provider(css_provider_spinbutton, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ // Cursor coordinates
+ dtw->_coord_status = Gtk::manage(new Gtk::Grid());
+ dtw->_coord_status->set_name("CoordinateAndZStatus");
+ dtw->_coord_status->set_row_spacing(0);
+ dtw->_coord_status->set_column_spacing(10);
+ dtw->_coord_status->set_margin_end(10);
+ auto sep = Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL));
+ sep->set_name("CoordinateSeparator");
+ dtw->_coord_status->attach(*sep, 0, 0, 1, 2);
+
+ dtw->_coord_status->set_tooltip_text(_("Cursor coordinates"));
+ auto label_x = Gtk::manage(new Gtk::Label(_("X:")));
+ auto label_y = Gtk::manage(new Gtk::Label(_("Y:")));
+ label_x->set_halign(Gtk::ALIGN_START);
+ label_y->set_halign(Gtk::ALIGN_START);
+ dtw->_coord_status->attach(*label_x, 1, 0, 1, 1);
+ dtw->_coord_status->attach(*label_y, 1, 1, 1, 1);
+ dtw->_coord_status_x = Gtk::manage(new Gtk::Label());
+ dtw->_coord_status_y = Gtk::manage(new Gtk::Label());
+ dtw->_coord_status_x->set_name("CoordinateStatusX");
+ dtw->_coord_status_y->set_name("CoordinateStatusY");
+ dtw->_coord_status_x->set_markup(" 0.00 ");
+ dtw->_coord_status_y->set_markup(" 0.00 ");
+
+ // TRANSLATORS: Abbreviation for canvas zoom level
+ auto label_z = Gtk::manage(new Gtk::Label(C_("canvas", "Z:")));
+ label_z->set_name("ZLabel");
+ // TRANSLATORS: Abbreviation for canvas rotation
+ auto label_r = Gtk::manage(new Gtk::Label(C_("canvas", "R:")));
+ label_r->set_name("RLabel");
+
+ dtw->_coord_status_x->set_halign(Gtk::ALIGN_END);
+ dtw->_coord_status_y->set_halign(Gtk::ALIGN_END);
+ dtw->_coord_status->attach(*dtw->_coord_status_x, 2, 0, 1, 1);
+ dtw->_coord_status->attach(*dtw->_coord_status_y, 2, 1, 1, 1);
+ dtw->_coord_status->show_all();
+ dtw->_coord_status->set_no_show_all();
+
+ dtw->_zoom_status_box->pack_start(*label_z, true, true);
+ dtw->_zoom_status_box->pack_end(*dtw->_zoom_status, true, true);
+ dtw->_zoom_status_box->show_all();
+
+ dtw->_rotation_status_box->pack_start(*label_r, true, true);
+ dtw->_rotation_status_box->pack_end(*dtw->_rotation_status, true, true);
+ dtw->_rotation_status_box->show_all();
+ dtw->_rotation_status_box->set_no_show_all();
+
+ dtw->_statusbar->pack_end(*dtw->_rotation_status_box, false, false);
+ dtw->_statusbar->pack_end(*dtw->_zoom_status_box, false, false);
+ dtw->_statusbar->pack_end(*dtw->_coord_status, false, false);
+
+ update_statusbar_visibility();
+
+ _statusbar_preferences_observer = prefs->createObserver("/statusbar/visibility", [=]() {
+ update_statusbar_visibility();
+ });
+
+ // --------------- Color Management ---------------- //
+ dtw->_tracker = ege_color_prof_tracker_new(GTK_WIDGET(_layer_selector->gobj()));
+ bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display");
+ if ( fromDisplay ) {
+ auto id = Inkscape::CMSSystem::getDisplayId(0);
+ dtw->_canvas->set_cms_key(id);
+ dtw->cms_adjust_set_sensitive(!id.empty());
+ }
+ g_signal_connect( G_OBJECT(dtw->_tracker), "changed", G_CALLBACK(SPDesktopWidget::color_profile_event), dtw );
+
+ // ------------------ Finish Up -------------------- //
+ dtw->_vbox->show_all();
+ dtw->_canvas_grid->ShowCommandPalette(false);
+
+ dtw->_canvas->grab_focus();
+}
+
+void SPDesktopWidget::apply_ctrlbar_settings() {
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+ int min = ToolboxFactory::min_pixel_size;
+ int max = ToolboxFactory::max_pixel_size;
+ int size = prefs->getIntLimited(ToolboxFactory::ctrlbars_icon_size, min, min, max);
+ Inkscape::UI::set_icon_sizes(snap_toolbox, size);
+ Inkscape::UI::set_icon_sizes(commands_toolbox, size);
+ Inkscape::UI::set_icon_sizes(aux_toolbox, size);
+}
+
+void SPDesktopWidget::update_statusbar_visibility() {
+ auto prefs = Inkscape::Preferences::get();
+ Glib::ustring path("/statusbar/visibility/");
+
+ _coord_status->set_visible(prefs->getBool(path + "coordinates", true));
+ _rotation_status_box->set_visible(prefs->getBool(path + "rotation", true));
+ _layer_selector->set_visible(prefs->getBool(path + "layer", true));
+ _selected_style->set_visible(prefs->getBool(path + "style", true));
+}
+
+void
+SPDesktopWidget::setMessage (Inkscape::MessageType type, const gchar *message)
+{
+ _select_status->set_markup(message ? message : "");
+
+ // make sure the important messages are displayed immediately!
+ if (type == Inkscape::IMMEDIATE_MESSAGE && _select_status->get_is_drawable()) {
+ _select_status->queue_draw();
+ }
+
+ _select_status->set_tooltip_text(_select_status->get_text());
+}
+
+/**
+ * Called before SPDesktopWidget destruction.
+ * (Might be called more than once)
+ */
+void
+SPDesktopWidget::on_unrealize()
+{
+ auto dtw = this;
+
+ if (_tbbox) {
+ Inkscape::Preferences::get()->setInt("/toolbox/tools/width", _tbbox->get_position());
+ }
+
+ if (dtw->desktop) {
+ if ( watcher ) {
+ watcher->remove(dtw);
+ }
+
+ for (auto &conn : dtw->_connections) {
+ conn.disconnect();
+ }
+
+ // Canvas
+ dtw->_canvas->set_drawing(nullptr); // Ensures deactivation
+ dtw->_canvas->set_desktop(nullptr); // Todo: Remove desktop dependency.
+
+ // Zoom
+ dtw->_zoom_status_input_connection.disconnect();
+ dtw->_zoom_status_output_connection.disconnect();
+ g_signal_handlers_disconnect_by_data(G_OBJECT(dtw->_zoom_status->gobj()), dtw->_zoom_status->gobj());
+ dtw->_zoom_status_value_changed_connection.disconnect();
+ dtw->_zoom_status_populate_popup_connection.disconnect();
+
+ // Rotation
+ dtw->_rotation_status_input_connection.disconnect();
+ dtw->_rotation_status_output_connection.disconnect();
+ g_signal_handlers_disconnect_by_data(G_OBJECT(dtw->_rotation_status->gobj()), dtw->_rotation_status->gobj());
+ dtw->_rotation_status_value_changed_connection.disconnect();
+ dtw->_rotation_status_populate_popup_connection.disconnect();
+
+ dtw->_panels->setDesktop(nullptr);
+
+ delete _container; // will unrealize dtw->_canvas
+
+ _layer_selector->setDesktop(nullptr);
+ INKSCAPE.remove_desktop(dtw->desktop); // clears selection and event_context
+ dtw->modified_connection.disconnect();
+ dtw->desktop->destroy();
+ Inkscape::GC::release (dtw->desktop);
+ dtw->desktop = nullptr;
+ }
+
+ parent_type::on_unrealize();
+}
+
+SPDesktopWidget::~SPDesktopWidget() {
+ delete _canvas_grid;
+}
+
+/**
+ * Set the title in the desktop-window (if desktop has an own window).
+ *
+ * The title has form file name: desktop number - Inkscape.
+ * The desktop number is only shown if it's 2 or higher,
+ */
+void
+SPDesktopWidget::updateTitle(gchar const* uri)
+{
+ if (window) {
+
+ SPDocument *doc = this->desktop->doc();
+ auto namedview = doc->getNamedView();
+
+ std::string Name;
+ if (doc->isModifiedSinceSave()) {
+ Name += "*";
+ }
+
+ Name += uri;
+
+ if (namedview->viewcount > 1) {
+ Name += ": ";
+ Name += std::to_string(namedview->viewcount);
+ }
+ Name += " (";
+
+ auto render_mode = desktop->getCanvas()->get_render_mode();
+ auto color_mode = desktop->getCanvas()->get_color_mode();
+
+ if (render_mode == Inkscape::RenderMode::OUTLINE) {
+ Name += N_("outline");
+ } else if (render_mode == Inkscape::RenderMode::NO_FILTERS) {
+ Name += N_("no filters");
+ } else if (render_mode == Inkscape::RenderMode::VISIBLE_HAIRLINES) {
+ Name += N_("enhance thin lines");
+ } else if (render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) {
+ Name += N_("outline overlay");
+ }
+
+ if (color_mode != Inkscape::ColorMode::NORMAL &&
+ render_mode != Inkscape::RenderMode::NORMAL) {
+ Name += ", ";
+ }
+
+ if (color_mode == Inkscape::ColorMode::GRAYSCALE) {
+ Name += N_("grayscale");
+ } else if (color_mode == Inkscape::ColorMode::PRINT_COLORS_PREVIEW) {
+ Name += N_("print colors preview");
+ }
+
+ if (*Name.rbegin() == '(') { // Can not use C++11 .back() or .pop_back() with ustring!
+ Name.erase(Name.size() - 2);
+ } else {
+ Name += ")";
+ }
+
+ Name += " - Inkscape";
+
+ // Name += " (";
+ // Name += Inkscape::version_string;
+ // Name += ")";
+
+ window->set_title (Name);
+ }
+}
+
+DialogContainer *SPDesktopWidget::getDialogContainer()
+{
+ return _container;
+}
+
+void SPDesktopWidget::showNotice(Glib::ustring const &msg, unsigned timeout)
+{
+ _canvas_grid->showNotice(msg, timeout);
+}
+
+/**
+ * Callback to realize desktop widget.
+ */
+void SPDesktopWidget::on_realize()
+{
+ SPDesktopWidget *dtw = this;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ parent_type::on_realize();
+
+ Geom::Rect d = Geom::Rect::from_xywh(Geom::Point(0,0), (dtw->desktop->doc())->getDimensions());
+
+ if (d.width() < 1.0 || d.height() < 1.0) return;
+
+ dtw->desktop->set_display_area (d, 10);
+
+ dtw->updateNamedview();
+ Gtk::Container *window = get_toplevel();
+ if (window) {
+ bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window));
+ prefs->setBool("/theme/darkTheme", dark);
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+ }
+}
+
+/* This is just to provide access to common functionality from sp_desktop_widget_realize() above
+ as well as from SPDesktop::change_document() */
+void SPDesktopWidget::updateNamedview()
+{
+ // Listen on namedview modification
+ // originally (prior to the sigc++ conversion) the signal was simply
+ // connected twice rather than disconnecting the first connection
+ modified_connection.disconnect();
+
+ modified_connection = desktop->namedview->connectModified(sigc::mem_fun(*this, &SPDesktopWidget::namedviewModified));
+ namedviewModified(desktop->namedview, SP_OBJECT_MODIFIED_FLAG);
+
+ updateTitle( desktop->doc()->getDocumentName() );
+}
+
+/**
+ * Callback to handle desktop widget event.
+ */
+gint
+SPDesktopWidget::event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw)
+{
+ if (event->type == GDK_BUTTON_PRESS) {
+ // defocus any spinbuttons
+ dtw->_canvas->grab_focus();
+ }
+
+ if ((event->type == GDK_BUTTON_PRESS) && (event->button.button == 3)) {
+ if (event->button.state & GDK_SHIFT_MASK) {
+ dtw->desktop->getCanvasDrawing()->set_sticky(true);
+ } else {
+ dtw->desktop->getCanvasDrawing()->set_sticky(false);
+ }
+ }
+
+ {
+ // The key press/release events need to be passed to desktop handler explicitly,
+ // because otherwise the event contexts only receive key events when the mouse cursor
+ // is over the canvas. This redirection is only done for key events and only if there's no
+ // current item on the canvas, because item events and all mouse events are caught
+ // and passed on by the canvas acetate (I think). --bb
+
+ if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)
+ && !dtw->_canvas->get_current_canvas_item()) {
+ return (gint)sp_desktop_root_handler (event, dtw->desktop);
+ }
+ }
+
+ return FALSE;
+}
+
+void
+SPDesktopWidget::color_profile_event(EgeColorProfTracker */*tracker*/, SPDesktopWidget *dtw)
+{
+ // Handle profile changes
+ GdkWindow *window = dtw->get_window()->gobj();
+
+ // Figure out the ID for the monitor
+ auto display = gdk_display_get_default();
+ auto monitor = gdk_display_get_monitor_at_window(display, window);
+
+ int n_monitors = gdk_display_get_n_monitors(display);
+
+ int monitorNum = -1;
+
+ // Now loop through the set of monitors and figure out whether this monitor matches
+ for (int i_monitor = 0; i_monitor < n_monitors; ++i_monitor) {
+ auto monitor_at_index = gdk_display_get_monitor(display, i_monitor);
+ if (monitor_at_index == monitor) monitorNum = i_monitor;
+ }
+
+ Glib::ustring id = Inkscape::CMSSystem::getDisplayId( monitorNum );
+ dtw->_canvas->set_cms_key(id);
+ dtw->cms_adjust_set_sensitive(!id.empty());
+}
+
+void
+SPDesktopWidget::update_guides_lock()
+{
+ bool down = _canvas_grid->GetGuideLock()->get_active();
+ auto nv = desktop->getNamedView();
+ bool lock = nv->getLockGuides();
+
+ if (down != lock) {
+ nv->toggleLockGuides();
+ setMessage(Inkscape::NORMAL_MESSAGE, down ? _("Locked all guides") : _("Unlocked all guides"));
+ }
+}
+
+void
+SPDesktopWidget::cms_adjust_toggled()
+{
+ auto _cms_adjust = _canvas_grid->GetCmsAdjust();
+
+ bool down = _cms_adjust->get_active();
+ if ( down != _canvas->get_cms_active() ) {
+ _canvas->set_cms_active(down);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/displayprofile/enable", down);
+ if (down) {
+ setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>enabled</b> in this window"));
+ } else {
+ setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>disabled</b> in this window"));
+ }
+ }
+}
+
+void
+SPDesktopWidget::cms_adjust_set_sensitive(bool enabled)
+{
+ _canvas_grid->GetCmsAdjust()->set_sensitive(enabled);
+}
+
+void
+SPDesktopWidget::enableInteraction()
+{
+ g_return_if_fail(_interaction_disabled_counter > 0);
+
+ _interaction_disabled_counter--;
+
+ if (_interaction_disabled_counter == 0) {
+ this->set_sensitive();
+ }
+}
+
+void
+SPDesktopWidget::disableInteraction()
+{
+ if (_interaction_disabled_counter == 0) {
+ this->set_sensitive(false);
+ }
+
+ _interaction_disabled_counter++;
+}
+
+void
+SPDesktopWidget::setCoordinateStatus(Geom::Point p)
+{
+ gchar *cstr;
+ cstr = g_strdup_printf("%7.2f", _dt2r * p[Geom::X]);
+ _coord_status_x->set_markup(cstr);
+ g_free(cstr);
+
+ cstr = g_strdup_printf("%7.2f", _dt2r * p[Geom::Y]);
+ _coord_status_y->set_markup(cstr);
+ g_free(cstr);
+}
+
+void
+SPDesktopWidget::letZoomGrabFocus()
+{
+ if (_zoom_status) _zoom_status->grab_focus();
+}
+
+void
+SPDesktopWidget::getWindowGeometry (gint &x, gint &y, gint &w, gint &h)
+{
+ if (window) {
+ window->get_size (w, h);
+ window->get_position (x, y);
+ // The get_positon is very unreliable (see Gtk docs) and will often return zero.
+ if (!x && !y) {
+ if (Glib::RefPtr<Gdk::Window> w = window->get_window()) {
+ Gdk::Rectangle rect;
+ w->get_frame_extents(rect);
+ x = rect.get_x();
+ y = rect.get_y();
+ }
+ }
+ }
+}
+
+void
+SPDesktopWidget::setWindowPosition (Geom::Point p)
+{
+ if (window)
+ {
+ window->move (gint(round(p[Geom::X])), gint(round(p[Geom::Y])));
+ }
+}
+
+void
+SPDesktopWidget::setWindowSize (gint w, gint h)
+{
+ if (window)
+ {
+ window->set_default_size (w, h);
+ window->resize (w, h);
+ }
+}
+
+/**
+ * \note transientizing does not work on windows; when you minimize a document
+ * and then open it back, only its transient emerges and you cannot access
+ * the document window. The document window must be restored by rightclicking
+ * the taskbar button and pressing "Restore"
+ */
+void
+SPDesktopWidget::setWindowTransient (void *p, int transient_policy)
+{
+ if (window)
+ {
+ GtkWindow *w = GTK_WINDOW(window->gobj());
+ gtk_window_set_transient_for (GTK_WINDOW(p), w);
+
+ /*
+ * This enables "aggressive" transientization,
+ * i.e. dialogs always emerging on top when you switch documents. Note
+ * however that this breaks "click to raise" policy of a window
+ * manager because the switched-to document will be raised at once
+ * (so that its transients also could raise)
+ */
+ if (transient_policy == PREFS_DIALOGS_WINDOWS_AGGRESSIVE)
+ // without this, a transient window not always emerges on top
+ gtk_window_present (w);
+ }
+}
+
+void
+SPDesktopWidget::presentWindow()
+{
+ if (window)
+ window->present();
+}
+
+bool SPDesktopWidget::showInfoDialog( Glib::ustring const &message )
+{
+ bool result = false;
+ if (window)
+ {
+ Gtk::MessageDialog dialog(*window, message, false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK);
+ dialog.property_destroy_with_parent() = true;
+ dialog.set_name("InfoDialog");
+ dialog.set_title(_("Note:")); // probably want to take this as a parameter.
+ dialog.run();
+ }
+ return result;
+}
+
+bool SPDesktopWidget::warnDialog (Glib::ustring const &text)
+{
+ Gtk::MessageDialog dialog (*window, text, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL);
+ gint response = dialog.run();
+ if (response == Gtk::RESPONSE_OK)
+ return true;
+ else
+ return false;
+}
+
+void
+SPDesktopWidget::iconify()
+{
+ GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas->gobj())));
+ if (GTK_IS_WINDOW(topw)) {
+ if (desktop->is_iconified()) {
+ gtk_window_deiconify(topw);
+ } else {
+ gtk_window_iconify(topw);
+ }
+ }
+}
+
+void
+SPDesktopWidget::maximize()
+{
+ GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas->gobj())));
+ if (GTK_IS_WINDOW(topw)) {
+ if (desktop->is_maximized()) {
+ gtk_window_unmaximize(topw);
+ } else {
+ gtk_window_maximize(topw);
+ }
+ }
+}
+
+void
+SPDesktopWidget::fullscreen()
+{
+ GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas->gobj())));
+ if (GTK_IS_WINDOW(topw)) {
+ if (desktop->is_fullscreen()) {
+ gtk_window_unfullscreen(topw);
+ // widget layout is triggered by the resulting window_state_event
+ } else {
+ gtk_window_fullscreen(topw);
+ // widget layout is triggered by the resulting window_state_event
+ }
+ }
+}
+
+/**
+ * Hide whatever the user does not want to see in the window.
+ * Also move command toolbar to top or side as required.
+ */
+void SPDesktopWidget::layoutWidgets()
+{
+ SPDesktopWidget *dtw = this;
+ Glib::ustring pref_root;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (desktop && desktop->is_focusMode()) {
+ pref_root = "/focus/";
+ } else if (desktop && desktop->is_fullscreen()) {
+ pref_root = "/fullscreen/";
+ } else {
+ pref_root = "/window/";
+ }
+
+ if (!prefs->getBool(pref_root + "commands/state", true)) {
+ gtk_widget_hide (dtw->commands_toolbox);
+ } else {
+ gtk_widget_show_all (dtw->commands_toolbox);
+ }
+
+ if (!prefs->getBool(pref_root + "snaptoolbox/state", true)) {
+ gtk_widget_hide (dtw->snap_toolbox);
+ } else {
+ gtk_widget_show_all (dtw->snap_toolbox);
+ }
+
+ if (!prefs->getBool(pref_root + "toppanel/state", true)) {
+ gtk_widget_hide (dtw->aux_toolbox);
+ } else {
+ // we cannot just show_all because that will show all tools' panels;
+ // this is a function from toolbox.cpp that shows only the current tool's panel
+ ToolboxFactory::showAuxToolbox(dtw->aux_toolbox);
+ }
+
+ if (!prefs->getBool(pref_root + "toolbox/state", true)) {
+ gtk_widget_hide (dtw->tool_toolbox);
+ } else {
+ gtk_widget_show_all (dtw->tool_toolbox);
+ }
+
+ if (!prefs->getBool(pref_root + "statusbar/state", true)) {
+ dtw->_statusbar->hide();
+ } else {
+ dtw->_statusbar->show_all();
+ }
+
+ if (!prefs->getBool(pref_root + "panels/state", true)) {
+ dtw->_panels->hide();
+ } else {
+ dtw->_panels->show_all();
+ }
+
+ _canvas_grid->ShowScrollbars(prefs->getBool(pref_root + "scrollbars/state", true));
+ _canvas_grid->ShowRulers( prefs->getBool(pref_root + "rulers/state", true));
+
+ // Move command toolbar as required.
+
+ // If interface_mode unset, use screen aspect ratio. Needs to be synced with "canvas-interface-mode" action.
+ Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary();
+ double const width = monitor_geometry.get_width();
+ double const height = monitor_geometry.get_height();
+ bool widescreen = (height > 0 && width/height > 1.65);
+ widescreen = prefs->getInt(pref_root + "task/taskset", widescreen ? 2 : 0) == 2; // legacy
+ widescreen = prefs->getBool(pref_root + "interface_mode", widescreen);
+
+ auto commands_toolbox_cpp = dynamic_cast<Gtk::Bin *>(Glib::wrap(commands_toolbox));
+ if (commands_toolbox_cpp) {
+
+ // Unlink command toolbar.
+ commands_toolbox_cpp->reference(); // So toolbox is not deleted.
+ auto parent = commands_toolbox_cpp->get_parent();
+ parent->remove(*commands_toolbox_cpp);
+
+ auto orientation = Gtk::ORIENTATION_HORIZONTAL;
+ auto orientation_c = GTK_ORIENTATION_HORIZONTAL;
+ // Link command toolbar back.
+ if (!widescreen) {
+ _top_toolbars->attach(*commands_toolbox_cpp, 0, 0); // Always first
+ gtk_box_set_child_packing(_vbox->gobj(), commands_toolbox, false, true, 0, GTK_PACK_START); // expand, fill, padding, pack_type
+ orientation = Gtk::ORIENTATION_HORIZONTAL;
+ orientation_c = GTK_ORIENTATION_HORIZONTAL;
+ commands_toolbox_cpp->set_hexpand(true);
+ } else {
+ _hbox->add(*commands_toolbox_cpp);
+ gtk_box_set_child_packing(_hbox->gobj(), commands_toolbox, false, true, 0, GTK_PACK_START); // expand, fill, padding, pack_type
+ orientation = Gtk::ORIENTATION_VERTICAL;
+ orientation_c = GTK_ORIENTATION_VERTICAL;
+ commands_toolbox_cpp->set_hexpand(false);
+ }
+ commands_toolbox_cpp->unreference();
+
+ auto box = dynamic_cast<Gtk::Box *>(commands_toolbox_cpp->get_child());
+ if (box) {
+ box->set_orientation(orientation);
+ for (auto child : box->get_children()) {
+ if (auto toolbar = dynamic_cast<Gtk::Toolbar *>(child)) {
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(toolbar->gobj()), orientation_c);
+ //toolbar->set_orientation(orientation); // Missing in C++ interface!
+ }
+ }
+ }
+ } else {
+ std::cerr << "SPDesktopWidget::layoutWidgets(): Wrong widget type for command toolbar!" << std::endl;
+ }
+
+ // Temporary for Gtk3: Gtk toolbar resets icon sizes, so reapply them.
+ // TODO: remove this call in Gtk4 after Gtk::Toolbar is eliminated.
+ apply_ctrlbar_settings();
+ repack_snaptoolbar();
+
+ Inkscape::UI::resize_widget_children(_top_toolbars);
+}
+
+Gtk::Toolbar *
+SPDesktopWidget::get_toolbar_by_name(const Glib::ustring& name)
+{
+ // The name is actually attached to the GtkGrid that contains
+ // the toolbar, so we need to get the grid first
+ auto widget = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), name);
+ auto grid = dynamic_cast<Gtk::Grid*>(widget);
+
+ if (!grid) return nullptr;
+
+ auto child = grid->get_child_at(0,0);
+ auto tb = dynamic_cast<Gtk::Toolbar*>(child);
+
+ return tb;
+}
+
+void
+SPDesktopWidget::setToolboxFocusTo (const gchar* label)
+{
+ // First try looking for a named widget
+ auto hb = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), label);
+
+ // Fallback to looking for a named data member (deprecated)
+ if (!hb) {
+ hb = Glib::wrap(GTK_WIDGET(sp_search_by_data_recursive(aux_toolbox, (gpointer) label)));
+ }
+
+ if (hb)
+ {
+ hb->grab_focus();
+ }
+}
+
+void
+SPDesktopWidget::setToolboxAdjustmentValue (gchar const *id, double value)
+{
+ // First try looking for a named widget
+ auto hb = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), id);
+
+ // Fallback to looking for a named data member (deprecated)
+ if (!hb) {
+ hb = Glib::wrap(GTK_WIDGET(sp_search_by_data_recursive(aux_toolbox, (gpointer)id)));
+ }
+
+ if (hb) {
+ auto sb = dynamic_cast<Inkscape::UI::Widget::SpinButtonToolItem *>(hb);
+ auto a = sb->get_adjustment();
+
+ if(a) a->set_value(value);
+ }
+
+ else g_warning ("Could not find GtkAdjustment for %s\n", id);
+}
+
+
+bool
+SPDesktopWidget::isToolboxButtonActive (const gchar* id)
+{
+ bool isActive = false;
+ auto thing = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), id);
+
+ // The toolbutton could be a few different types so try casting to
+ // each of them.
+ // TODO: This will be simpler in Gtk+ 4 when ToolItems have gone
+ auto toggle_button = dynamic_cast<Gtk::ToggleButton *>(thing);
+ auto toggle_tool_button = dynamic_cast<Gtk::ToggleToolButton *>(thing);
+
+ if ( !thing ) {
+ //g_message( "Unable to locate item for {%s}", id );
+ } else if (toggle_button) {
+ isActive = toggle_button->get_active();
+ } else if (toggle_tool_button) {
+ isActive = toggle_tool_button->get_active();
+ } else {
+ //g_message( "Item for {%s} is of an unsupported type", id );
+ }
+
+ return isActive;
+}
+
+SPDesktopWidget::SPDesktopWidget(InkscapeWindow *inkscape_window, SPDocument *document)
+ : SPDesktopWidget(inkscape_window)
+{
+ set_name("SPDesktopWidget");
+
+ SPDesktopWidget *dtw = this;
+
+ SPNamedView *namedview = document->getNamedView();
+
+ dtw->_dt2r = 1. / namedview->display_units->factor;
+
+ // This section seems backwards!
+ dtw->desktop = new SPDesktop();
+ dtw->desktop->init (namedview, dtw->_canvas, this);
+ dtw->_canvas->set_desktop(desktop);
+ INKSCAPE.add_desktop (dtw->desktop);
+
+ // Add the shape geometry to libavoid for autorouting connectors.
+ // This needs desktop set for its spacing preferences.
+ init_avoided_shape_geometry(dtw->desktop);
+
+ dtw->_selected_style->setDesktop(dtw->desktop);
+
+ /* Once desktop is set, we can update rulers */
+ dtw->_canvas_grid->UpdateRulers();
+
+ dtw->setView(dtw->desktop);
+
+ /* Listen on namedview modification */
+ dtw->modified_connection = namedview->connectModified(sigc::mem_fun(*dtw, &SPDesktopWidget::namedviewModified));
+
+ _layer_selector->setDesktop(dtw->desktop);
+
+ // We never want a page widget if there's no desktop.
+ _page_selector = Gtk::manage(new Inkscape::UI::Widget::PageSelector(desktop));
+ _statusbar->pack_end(*_page_selector, false, false);
+
+ ToolboxFactory::setToolboxDesktop(dtw->aux_toolbox, dtw->desktop);
+
+ dtw->layoutWidgets();
+
+ dtw->_panels->setDesktop(dtw->desktop);
+}
+
+/**
+ * Choose where to pack the snap toolbar.
+ */
+void SPDesktopWidget::repack_snaptoolbar()
+{
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+ bool is_perm = prefs->getInt("/toolbox/simplesnap", 1) == 2;
+ auto& aux = *Glib::wrap(aux_toolbox);
+ auto& snap = *Glib::wrap(snap_toolbox);
+
+ // Only remove from the parent if the status has changed
+ auto parent = snap.get_parent();
+ if (parent && ((is_perm && parent != _hbox) || (!is_perm && parent != _top_toolbars))) {
+ parent->remove(snap);
+ }
+
+ // Only repack if there's no parent widget now.
+ if (!snap.get_parent()) {
+ if (is_perm) {
+ ToolboxFactory::setOrientation(snap_toolbox, GTK_ORIENTATION_VERTICAL);
+ _hbox->pack_end(snap, false, true);
+ } else {
+ ToolboxFactory::setOrientation(snap_toolbox, GTK_ORIENTATION_HORIZONTAL);
+ _top_toolbars->attach(snap, 1, 0, 1, 2);
+ }
+ }
+
+ // Always reset the various constraints, even if not repacked.
+ if (is_perm) {
+ snap.set_valign(Gtk::ALIGN_START);
+ } else {
+ // This ensures that the Snap toolbox is on the top and only takes the needed space.
+ if (_top_toolbars->get_children().size() == 3 && gtk_widget_get_visible(commands_toolbox)) {
+ _top_toolbars->child_property_width(aux) = 2;
+ _top_toolbars->child_property_height(snap) = 1;
+ snap.set_valign(Gtk::ALIGN_START);
+ }
+ else {
+ _top_toolbars->child_property_width(aux) = 1;
+ _top_toolbars->child_property_height(snap) = 2;
+ snap.set_valign(Gtk::ALIGN_CENTER);
+ }
+ }
+}
+
+void
+SPDesktopWidget::update_rulers()
+{
+ _canvas_grid->UpdateRulers();
+}
+
+
+void SPDesktopWidget::namedviewModified(SPObject *obj, guint flags)
+{
+ auto nv = cast<SPNamedView>(obj);
+
+ if (flags & SP_OBJECT_MODIFIED_FLAG) {
+ _dt2r = 1. / nv->display_units->factor;
+
+ _canvas_grid->GetVRuler()->set_unit(nv->getDisplayUnit());
+ _canvas_grid->GetHRuler()->set_unit(nv->getDisplayUnit());
+ _canvas_grid->GetVRuler()->set_tooltip_text(gettext(nv->display_units->name_plural.c_str()));
+ _canvas_grid->GetHRuler()->set_tooltip_text(gettext(nv->display_units->name_plural.c_str()));
+ _canvas_grid->UpdateRulers();
+
+ /* This loops through all the grandchildren of aux toolbox,
+ * and for each that it finds, it performs an sp_search_by_name_recursive(),
+ * looking for widgets named "unit-tracker" (this is used by
+ * all toolboxes to refer to the unit selector). The default document units
+ * is then selected within these unit selectors.
+ *
+ * This should solve: https://bugs.launchpad.net/inkscape/+bug/362995
+ */
+ if (GTK_IS_CONTAINER(aux_toolbox)) {
+ std::vector<Gtk::Widget*> ch = Glib::wrap(GTK_CONTAINER(aux_toolbox))->get_children();
+ for (auto i:ch) {
+ if (auto container = dynamic_cast<Gtk::Container *>(i)) {
+ std::vector<Gtk::Widget*> grch = container->get_children();
+ for (auto j:grch) {
+
+ if (!GTK_IS_WIDGET(j->gobj())) // wasn't a widget
+ continue;
+
+ // Don't apply to text toolbar. We want to be able to
+ // use different units for text. (Bug 1562217)
+ const Glib::ustring name = j->get_name();
+ if ( name == "TextToolbar" || name == "MeasureToolbar" || name == "CalligraphicToolbar" )
+ continue;
+
+ auto tracker = dynamic_cast<Inkscape::UI::Widget::ComboToolItem*>(sp_search_by_name_recursive(j, "unit-tracker"));
+
+ if (tracker) { // it's null when inkscape is first opened
+ if (auto ptr = static_cast<UnitTracker*>(tracker->get_data(Glib::Quark("unit-tracker")))) {
+ ptr->setActiveUnit(nv->display_units);
+ }
+ }
+ } // grandchildren
+ } // if child is a container
+ } // children
+ } // if aux_toolbox is a container
+ }
+}
+
+void
+SPDesktopWidget::on_adjustment_value_changed()
+{
+ if (update)
+ return;
+
+ update = true;
+
+ // Do not call canvas->scrollTo directly... messes up 'offset'.
+ desktop->scroll_absolute(Geom::Point(_canvas_grid->GetHAdj()->get_value(),
+ _canvas_grid->GetVAdj()->get_value()));
+
+ update = false;
+}
+
+/* we make the desktop window with focus active, signal is connected in interface.c */
+bool SPDesktopWidget::onFocusInEvent(GdkEventFocus*)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/bitmapautoreload/value", true)) {
+ std::vector<SPObject *> imageList = (desktop->doc())->getResourceList("image");
+ for (auto it : imageList) {
+ auto image = cast<SPImage>(it);
+ image->refresh_if_outdated();
+ }
+ }
+
+ INKSCAPE.activate_desktop (desktop);
+
+ return false;
+}
+
+// ------------------------ Zoom ------------------------
+static gdouble
+sp_dtw_zoom_value_to_display (gdouble value)
+{
+ return floor (10 * (pow (2, value) * 100.0 + 0.05)) / 10;
+}
+
+static gdouble
+sp_dtw_zoom_display_to_value (gdouble value)
+{
+ return log (value / 100.0) / log (2);
+}
+
+int
+SPDesktopWidget::zoom_input(double *new_val)
+{
+ double new_typed = g_strtod (_zoom_status->get_text().c_str(), nullptr);
+ *new_val = sp_dtw_zoom_display_to_value (new_typed);
+ return true;
+}
+
+bool
+SPDesktopWidget::zoom_output()
+{
+ gchar b[64];
+ double val = sp_dtw_zoom_value_to_display (_zoom_status->get_value());
+ if (val < 10) {
+ g_snprintf (b, 64, "%4.1f%%", val);
+ } else {
+ g_snprintf (b, 64, "%4.0f%%", val);
+ }
+ _zoom_status->set_text(b);
+ return true;
+}
+
+void
+SPDesktopWidget::zoom_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double const zoom_factor = pow (2, _zoom_status->get_value());
+
+ // Zoom around center of window
+ Geom::Rect const d_canvas = _canvas->get_area_world();
+ Geom::Point midpoint = desktop->w2d(d_canvas.midpoint());
+
+ _zoom_status_value_changed_connection.block();
+ if(prefs->getDouble("/options/zoomcorrection/shown", true)) {
+ desktop->zoom_realworld(midpoint, zoom_factor);
+ } else {
+ desktop->zoom_absolute(midpoint, zoom_factor);
+ }
+ _zoom_status_value_changed_connection.unblock();
+ _zoom_status->defocus();
+}
+
+void
+SPDesktopWidget::zoom_menu_handler(double factor)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if(prefs->getDouble("/options/zoomcorrection/shown", true)) {
+ desktop->zoom_realworld(desktop->current_center(), factor);
+ } else {
+ desktop->zoom_absolute(desktop->current_center(), factor, false);
+ }
+}
+
+
+
+void
+SPDesktopWidget::zoom_populate_popup(Gtk::Menu *menu)
+{
+ for ( auto iter : menu->get_children()) {
+ menu->remove(*iter);
+ }
+
+ auto item_1000 = Gtk::manage(new Gtk::MenuItem("1000%"));
+ auto item_500 = Gtk::manage(new Gtk::MenuItem("500%"));
+ auto item_200 = Gtk::manage(new Gtk::MenuItem("200%"));
+ auto item_100 = Gtk::manage(new Gtk::MenuItem("100%"));
+ auto item_50 = Gtk::manage(new Gtk::MenuItem( "50%"));
+ auto item_25 = Gtk::manage(new Gtk::MenuItem( "25%"));
+ auto item_10 = Gtk::manage(new Gtk::MenuItem( "10%"));
+
+ item_1000->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &SPDesktopWidget::zoom_menu_handler), 10.00));
+ item_500->signal_activate().connect( sigc::bind(sigc::mem_fun(*this, &SPDesktopWidget::zoom_menu_handler), 5.00));
+ item_200->signal_activate().connect( sigc::bind(sigc::mem_fun(*this, &SPDesktopWidget::zoom_menu_handler), 2.00));
+ item_100->signal_activate().connect( sigc::bind(sigc::mem_fun(*this, &SPDesktopWidget::zoom_menu_handler), 1.00));
+ item_50->signal_activate().connect( sigc::bind(sigc::mem_fun(*this, &SPDesktopWidget::zoom_menu_handler), 0.50));
+ item_25->signal_activate().connect( sigc::bind(sigc::mem_fun(*this, &SPDesktopWidget::zoom_menu_handler), 0.25));
+ item_10->signal_activate().connect( sigc::bind(sigc::mem_fun(*this, &SPDesktopWidget::zoom_menu_handler), 0.10));
+
+ menu->append(*item_1000);
+ menu->append(*item_500);
+ menu->append(*item_200);
+ menu->append(*item_100);
+ menu->append(*item_50);
+ menu->append(*item_25);
+ menu->append(*item_10);
+
+ auto sep = Gtk::manage(new Gtk::SeparatorMenuItem());
+ menu->append(*sep);
+
+ auto item_page = Gtk::manage(new Gtk::MenuItem(_("Page")));
+ item_page->signal_activate().connect([=]() { desktop->getDocument()->getPageManager().zoomToSelectedPage(desktop); });
+ menu->append(*item_page);
+
+ auto item_drawing = Gtk::manage(new Gtk::MenuItem(_("Drawing")));
+ item_drawing->signal_activate().connect(sigc::mem_fun(*desktop, &SPDesktop::zoom_drawing));
+ menu->append(*item_drawing);
+
+ auto item_selection = Gtk::manage(new Gtk::MenuItem(_("Selection")));
+ item_selection->signal_activate().connect(sigc::mem_fun(*desktop, &SPDesktop::zoom_selection));
+ menu->append(*item_selection);
+
+ auto item_center_page = Gtk::manage(new Gtk::MenuItem(_("Centre Page")));
+ item_center_page->signal_activate().connect([=]() { desktop->getDocument()->getPageManager().centerToSelectedPage(desktop); });
+ menu->append(*item_center_page);
+
+ menu->show_all();
+}
+
+
+void
+SPDesktopWidget::sticky_zoom_toggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/stickyzoom/value", _canvas_grid->GetStickyZoom()->get_active());
+}
+
+void
+SPDesktopWidget::sticky_zoom_updated()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _canvas_grid->GetStickyZoom()->set_active(prefs->getBool("/options/stickyzoom/value", false));
+}
+
+void
+SPDesktopWidget::update_zoom()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // It's very important that the value used in set_value is the same as the one
+ // set as it otherwise creates an infinate loop between the spin button and update_zoom
+ double correction = 1.0;
+ if(prefs->getDouble("/options/zoomcorrection/shown", true)) {
+ correction = prefs->getDouble("/options/zoomcorrection/value", 1.0);
+ }
+ _zoom_status_value_changed_connection.block();
+ _zoom_status->set_value(log(desktop->current_zoom() / correction) / log(2));
+ _zoom_status->queue_draw();
+ _zoom_status_value_changed_connection.unblock();
+}
+
+
+// ---------------------- Rotation ------------------------
+
+bool
+SPDesktopWidget::rotation_output()
+{
+ gchar b[64];
+ double val = _rotation_status->get_value();
+
+ if (val < -180) val += 360;
+ if (val > 180) val -= 360;
+
+ g_snprintf (b, 64, "%7.2f°", val);
+
+ _rotation_status->set_text(b);
+ return true;
+}
+
+void
+SPDesktopWidget::rotation_value_changed()
+{
+ double const rotate_factor = M_PI / 180.0 * _rotation_status->get_value();
+ // std::cout << "SPDesktopWidget::rotation_value_changed: "
+ // << _rotation_status->get_value()
+ // << " (" << rotate_factor << ")" <<std::endl;
+
+ // Rotate around center of window
+ Geom::Rect const d_canvas = _canvas->get_area_world();
+ _rotation_status_value_changed_connection.block();
+ Geom::Point midpoint = desktop->w2d(d_canvas.midpoint());
+ desktop->rotate_absolute_center_point (midpoint, rotate_factor);
+ _rotation_status_value_changed_connection.unblock();
+
+ _rotation_status->defocus();
+}
+
+void
+SPDesktopWidget::rotation_populate_popup(Gtk::Menu *menu)
+{
+ for ( auto iter : menu->get_children()) {
+ menu->remove(*iter);
+ }
+
+ auto item_m135 = Gtk::manage(new Gtk::MenuItem("-135°"));
+ auto item_m90 = Gtk::manage(new Gtk::MenuItem( "-90°"));
+ auto item_m45 = Gtk::manage(new Gtk::MenuItem( "-45°"));
+ auto item_0 = Gtk::manage(new Gtk::MenuItem( "0°"));
+ auto item_p45 = Gtk::manage(new Gtk::MenuItem( "45°"));
+ auto item_p90 = Gtk::manage(new Gtk::MenuItem( "90°"));
+ auto item_p135 = Gtk::manage(new Gtk::MenuItem( "135°"));
+ auto item_p180 = Gtk::manage(new Gtk::MenuItem( "180°"));
+
+ item_m135->signal_activate().connect(sigc::bind(sigc::mem_fun(*_rotation_status, &Gtk::SpinButton::set_value), -135));
+ item_m90->signal_activate().connect( sigc::bind(sigc::mem_fun(*_rotation_status, &Gtk::SpinButton::set_value), -90));
+ item_m45->signal_activate().connect( sigc::bind(sigc::mem_fun(*_rotation_status, &Gtk::SpinButton::set_value), -45));
+ item_0->signal_activate().connect( sigc::bind(sigc::mem_fun(*_rotation_status, &Gtk::SpinButton::set_value), 0));
+ item_p45->signal_activate().connect( sigc::bind(sigc::mem_fun(*_rotation_status, &Gtk::SpinButton::set_value), 45));
+ item_p90->signal_activate().connect( sigc::bind(sigc::mem_fun(*_rotation_status, &Gtk::SpinButton::set_value), 90));
+ item_p135->signal_activate().connect(sigc::bind(sigc::mem_fun(*_rotation_status, &Gtk::SpinButton::set_value), 135));
+ item_p180->signal_activate().connect(sigc::bind(sigc::mem_fun(*_rotation_status, &Gtk::SpinButton::set_value), 180));
+
+ menu->append(*item_m135);
+ menu->append(*item_m90);
+ menu->append(*item_m45);
+ menu->append(*item_0);
+ menu->append(*item_p45);
+ menu->append(*item_p90);
+ menu->append(*item_p135);
+ menu->append(*item_p180);
+
+ menu->show_all();
+}
+
+
+void
+SPDesktopWidget::update_rotation()
+{
+ _rotation_status_value_changed_connection.block();
+ _rotation_status->set_value(desktop->current_rotation() / M_PI * 180.0);
+ _rotation_status->queue_draw();
+ _rotation_status_value_changed_connection.unblock();
+
+}
+
+
+// --------------- Rulers/Scrollbars/Etc. -----------------
+void
+SPDesktopWidget::toggle_command_palette() {
+ // TODO: Turn into action and remove this function.
+ _canvas_grid->ToggleCommandPalette();
+}
+
+void
+SPDesktopWidget::toggle_rulers()
+{
+ // TODO: Turn into action and remove this function.
+ _canvas_grid->ToggleRulers();
+}
+
+void
+SPDesktopWidget::toggle_scrollbars()
+{
+ // TODO: Turn into action and remove this function.
+ _canvas_grid->ToggleScrollbars();
+}
+
+bool
+SPDesktopWidget::get_color_prof_adj_enabled() const
+{
+ auto _cms_adjust = _canvas_grid->GetCmsAdjust();
+ return _cms_adjust->get_sensitive() && _cms_adjust->get_active();
+}
+
+void
+SPDesktopWidget::toggle_color_prof_adj()
+{
+ auto _cms_adjust = _canvas_grid->GetCmsAdjust();
+ if (_cms_adjust->get_sensitive()) {
+ bool active = _cms_adjust->get_active();
+ _cms_adjust->set_active(!active);
+ }
+}
+
+static void
+set_adjustment (Gtk::Adjustment *adj, double l, double u, double ps, double si, double pi)
+{
+ if ((l != adj->get_lower()) ||
+ (u != adj->get_upper()) ||
+ (ps != adj->get_page_size()) ||
+ (si != adj->get_step_increment()) ||
+ (pi != adj->get_page_increment())) {
+ adj->set_lower(l);
+ adj->set_upper(u);
+ adj->set_page_size(ps);
+ adj->set_step_increment(si);
+ adj->set_page_increment(pi);
+ }
+}
+
+void
+SPDesktopWidget::update_scrollbars(double scale)
+{
+ if (update) return;
+ update = true;
+
+ /* The desktop region we always show unconditionally */
+ SPDocument *doc = desktop->doc();
+
+ auto deskarea = doc->preferredBounds();
+ deskarea->expandBy(doc->getDimensions()); // Double size
+
+ /* The total size of pages should be added unconditionally */
+ deskarea->unionWith(doc->getPageManager().getDesktopRect());
+
+ if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) {
+ deskarea->unionWith(doc->getRoot()->desktopVisualBounds());
+ } else {
+ deskarea->unionWith(doc->getRoot()->desktopGeometricBounds());
+ }
+
+ /* Canvas region we always show unconditionally */
+ double const y_dir = desktop->yaxisdir();
+ Geom::Rect carea( Geom::Point(deskarea->left() * scale - 64, (deskarea->top() * scale + 64) * y_dir),
+ Geom::Point(deskarea->right() * scale + 64, (deskarea->bottom() * scale - 64) * y_dir) );
+
+ Geom::Rect viewbox = _canvas->get_area_world();
+
+ /* Viewbox is always included into scrollable region */
+ carea = Geom::unify(carea, viewbox);
+
+ auto _hadj = _canvas_grid->GetHAdj();
+ auto _vadj = _canvas_grid->GetVAdj();
+ set_adjustment(_hadj, carea.min()[Geom::X], carea.max()[Geom::X],
+ viewbox.dimensions()[Geom::X],
+ 0.1 * viewbox.dimensions()[Geom::X],
+ viewbox.dimensions()[Geom::X]);
+ _hadj->set_value(viewbox.min()[Geom::X]);
+
+ set_adjustment(_vadj, carea.min()[Geom::Y], carea.max()[Geom::Y],
+ viewbox.dimensions()[Geom::Y],
+ 0.1 * viewbox.dimensions()[Geom::Y],
+ viewbox.dimensions()[Geom::Y]);
+ _vadj->set_value(viewbox.min()[Geom::Y]);
+
+ update = false;
+}
+
+gint
+SPDesktopWidget::ruler_event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw, bool horiz)
+{
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ dtw->on_ruler_box_button_press_event(&event->button, Glib::wrap(widget), horiz);
+ break;
+ case GDK_MOTION_NOTIFY:
+ dtw->on_ruler_box_motion_notify_event(&event->motion, Glib::wrap(widget), horiz);
+ break;
+ case GDK_BUTTON_RELEASE:
+ dtw->on_ruler_box_button_release_event(&event->button, Glib::wrap(widget), horiz);
+ break;
+ default:
+ break;
+ }
+
+ return FALSE;
+}
+
+bool
+SPDesktopWidget::on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::Widget *widget, bool horiz)
+{
+ auto origin = horiz ? Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_HRULER
+ : Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_VRULER;
+ desktop->event_context->snap_delay_handler(widget->gobj(), this, event, origin);
+
+ int wx, wy;
+
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas->gobj()));
+
+ gint width, height;
+
+ gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr);
+ gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height);
+
+ Geom::Point const event_win(wx, wy);
+
+ if (_ruler_clicked) {
+ Geom::Point event_w(_canvas->canvas_to_world(event_win));
+ Geom::Point event_dt(desktop->w2d(event_w));
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100);
+ if ( ( abs( (gint) event->x - _xp ) < tolerance )
+ && ( abs( (gint) event->y - _yp ) < tolerance ) ) {
+ return false;
+ }
+
+ _ruler_dragged = true;
+
+ // explicitly show guidelines; if I draw a guide, I want them on
+ if ((horiz ? wy : wx) >= 0) {
+ desktop->namedview->setShowGuides(true);
+ }
+
+ Geom::Point normal = _normal;
+ if (!(event->state & GDK_SHIFT_MASK)) {
+ ruler_snap_new_guide(desktop, event_dt, normal);
+ }
+ _active_guide->set_normal(normal);
+ _active_guide->set_origin(event_dt);
+
+ desktop->set_coordinate_status(event_dt);
+ }
+
+ return false;
+}
+
+// End guide creation or toggle guides on/off.
+bool
+SPDesktopWidget::on_ruler_box_button_release_event(GdkEventButton *event, Gtk::Widget *widget, bool horiz)
+{
+ int wx, wy;
+
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas->gobj()));
+
+ gint width, height;
+
+ gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr);
+ gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height);
+
+ Geom::Point const event_win(wx, wy);
+
+ if (_ruler_clicked && event->button == 1) {
+ desktop->event_context->discard_delayed_snap_event();
+
+ auto seat = gdk_device_get_seat(event->device);
+ gdk_seat_ungrab(seat);
+
+ Geom::Point const event_w(_canvas->canvas_to_world(event_win));
+ Geom::Point event_dt(desktop->w2d(event_w));
+
+ Geom::Point normal = _normal;
+ if (!(event->state & GDK_SHIFT_MASK)) {
+ ruler_snap_new_guide(desktop, event_dt, normal);
+ }
+
+ _active_guide.reset();
+ if ((horiz ? wy : wx) >= 0) {
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("sodipodi:guide");
+
+ // If root viewBox set, interpret guides in terms of viewBox (90/96)
+ double newx = event_dt.x();
+ double newy = event_dt.y();
+
+ // <sodipodi:guide> stores inverted y-axis coordinates
+ if (desktop->is_yaxisdown()) {
+ newy = desktop->doc()->getHeight().value("px") - newy;
+ normal[Geom::Y] *= -1.0;
+ }
+
+ SPRoot *root = desktop->doc()->getRoot();
+ if( root->viewBox_set ) {
+ newx = newx * root->viewBox.width() / root->width.computed;
+ newy = newy * root->viewBox.height() / root->height.computed;
+ }
+ repr->setAttributePoint("position", Geom::Point( newx, newy ));
+ repr->setAttributePoint("orientation", normal);
+ desktop->namedview->appendChild(repr);
+ Inkscape::GC::release(repr);
+ DocumentUndo::done(desktop->getDocument(), _("Create guide"), "");
+ }
+ desktop->set_coordinate_status(event_dt);
+
+ if (!_ruler_dragged) {
+ // Ruler click (without drag) toggle the guide visibility on and off
+ desktop->namedview->toggleShowGuides();
+ }
+
+ _ruler_clicked = false;
+ _ruler_dragged = false;
+ }
+
+ return false;
+}
+
+// Start guide creation by dragging from ruler.
+bool
+SPDesktopWidget::on_ruler_box_button_press_event(GdkEventButton *event, Gtk::Widget *widget, bool horiz)
+{
+ if (_ruler_clicked) // event triggered on a double click: do no process the click
+ return false;
+
+ int wx, wy;
+
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas->gobj()));
+
+ gint width, height;
+
+ gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr);
+ gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height);
+
+ Geom::Point const event_win(wx, wy);
+
+ if (event->button == 1) {
+ _ruler_clicked = true;
+ _ruler_dragged = false;
+ // save click origin
+ _xp = (gint) event->x;
+ _yp = (gint) event->y;
+
+ Geom::Point const event_w(_canvas->canvas_to_world(event_win));
+ Geom::Point const event_dt(desktop->w2d(event_w));
+
+ // calculate the normal of the guidelines when dragged from the edges of rulers.
+ auto const y_dir = desktop->yaxisdir();
+ Geom::Point normal_bl_to_tr(1., y_dir); //bottomleft to topright
+ Geom::Point normal_tr_to_bl(-1., y_dir); //topright to bottomleft
+ normal_bl_to_tr.normalize();
+ normal_tr_to_bl.normalize();
+ SPGrid * grid = desktop->namedview->getFirstEnabledGrid();
+ if (grid) {
+ if (grid->getType() == GridType::AXONOMETRIC ) {
+ auto angle_x = Geom::rad_from_deg(grid->getAngleX());
+ auto angle_z = Geom::rad_from_deg(grid->getAngleZ());
+ if (event->state & GDK_CONTROL_MASK) {
+ // guidelines normal to gridlines
+ normal_bl_to_tr = Geom::Point::polar(-angle_x, 1.0);
+ normal_tr_to_bl = Geom::Point::polar(angle_z, 1.0);
+ } else {
+ normal_bl_to_tr = Geom::rot90(Geom::Point::polar(angle_z, 1.0));
+ normal_tr_to_bl = Geom::rot90(Geom::Point::polar(-angle_x, 1.0));
+ }
+ }
+ }
+ if (horiz) {
+ if (wx < 50) {
+ _normal = normal_bl_to_tr;
+ } else if (wx > width - 50) {
+ _normal = normal_tr_to_bl;
+ } else {
+ _normal = Geom::Point(0.,1.);
+ }
+ } else {
+ if (wy < 50) {
+ _normal = normal_bl_to_tr;
+ } else if (wy > height - 50) {
+ _normal = normal_tr_to_bl;
+ } else {
+ _normal = Geom::Point(1.,0.);
+ }
+ }
+
+ _active_guide = make_canvasitem<Inkscape::CanvasItemGuideLine>(desktop->getCanvasGuides(), Glib::ustring(), event_dt, _normal);
+ _active_guide->set_stroke(desktop->namedview->guidehicolor);
+
+ // Ruler grabs all events until button release.
+ auto window = widget->get_window()->gobj();
+ auto seat = gdk_device_get_seat(event->device);
+ gdk_seat_grab(seat,
+ window,
+ GDK_SEAT_CAPABILITY_ALL_POINTING,
+ FALSE,
+ nullptr,
+ (GdkEvent*)event,
+ nullptr,
+ nullptr);
+ }
+
+ return false;
+}
+
+void
+SPDesktopWidget::ruler_snap_new_guide(SPDesktop *desktop, Geom::Point &event_dt, Geom::Point &normal)
+{
+ desktop->getCanvas()->grab_focus();
+ SnapManager &m = desktop->namedview->snap_manager;
+ m.setup(desktop);
+ // We're dragging a brand new guide, just pulled of the rulers seconds ago. When snapping to a
+ // path this guide will change it slope to become either tangential or perpendicular to that path. It's
+ // therefore not useful to try tangential or perpendicular snapping, so this will be disabled temporarily
+ bool pref_perp = m.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_PERPENDICULAR);
+ bool pref_tang = m.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_TANGENTIAL);
+ m.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_PERPENDICULAR, false);
+ m.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_TANGENTIAL, false);
+ // We only have a temporary guide which is not stored in our document yet.
+ // Because the guide snapper only looks in the document for guides to snap to,
+ // we don't have to worry about a guide snapping to itself here
+ Geom::Point normal_orig = normal;
+ m.guideFreeSnap(event_dt, normal, false, false);
+ // After snapping, both event_dt and normal have been modified accordingly; we'll take the normal (of the
+ // curve we snapped to) to set the normal the guide. And rotate it by 90 deg. if needed
+ if (pref_perp) { // Perpendicular snapping to paths is requested by the user, so let's do that
+ if (normal != normal_orig) {
+ normal = Geom::rot90(normal);
+ }
+ }
+ if (!(pref_tang || pref_perp)) { // if we don't want to snap either perpendicularly or tangentially, then
+ normal = normal_orig; // we must restore the normal to it's original state
+ }
+ // Restore the preferences
+ m.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_PERPENDICULAR, pref_perp);
+ m.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_TANGENTIAL, pref_tang);
+ m.unSetup();
+}
+
+Gio::ActionMap* SPDesktopWidget::get_action_map() {
+ return window;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/widgets/desktop-widget.h b/src/widgets/desktop-widget.h
new file mode 100644
index 0000000..70f4613
--- /dev/null
+++ b/src/widgets/desktop-widget.h
@@ -0,0 +1,266 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_DESKTOP_WIDGET_H
+#define SEEN_SP_DESKTOP_WIDGET_H
+
+/** \file
+ * SPDesktopWidget: handling Gtk events on a desktop.
+ *
+ * Authors:
+ * Jon A. Cruz <jon@joncruz.org> (c) 2010
+ * John Bintz <jcoswell@coswellproductions.org> (c) 2006
+ * Ralf Stephan <ralf@ark.in-berlin.de> (c) 2005
+ * Abhishek Sharma
+ * ? -2004
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <cstddef>
+#include <2geom/point.h>
+#include <sigc++/connection.h>
+#include <gtkmm.h>
+
+#include "message.h"
+#include "preferences.h"
+#include "ui/view/view-widget.h"
+#include "display/control/canvas-item-ptr.h"
+
+// forward declaration
+typedef struct _EgeColorProfTracker EgeColorProfTracker;
+
+class InkscapeWindow;
+struct SPCanvasItem;
+class SPDocument;
+class SPDesktop;
+struct SPDesktopWidget;
+class SPObject;
+
+namespace Inkscape {
+ class CanvasItemGuideLine;
+namespace UI {
+namespace Dialog {
+class DialogContainer;
+class DialogMultipaned;
+class SwatchesPanel;
+} // namespace Dialog
+
+namespace Widget {
+ class Button;
+ class Canvas;
+ class CanvasGrid;
+ class LayerSelector;
+ class PageSelector;
+ class SelectedStyle;
+ class SpinButton;
+ class Ruler;
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#define SP_DESKTOP_WIDGET(o) dynamic_cast<SPDesktopWidget*>(o)
+#define SP_IS_DESKTOP_WIDGET(o) bool(dynamic_cast<SPDesktopWidget const *>(o))
+
+void sp_desktop_widget_show_decorations(SPDesktopWidget *dtw, gboolean show);
+void sp_desktop_widget_update_hruler (SPDesktopWidget *dtw);
+void sp_desktop_widget_update_vruler (SPDesktopWidget *dtw);
+
+/* Show/hide rulers & scrollbars */
+void sp_desktop_widget_update_scrollbars (SPDesktopWidget *dtw, double scale);
+
+/// A GtkEventBox on an SPDesktop.
+class SPDesktopWidget : public SPViewWidget {
+ using parent_type = SPViewWidget;
+
+ SPDesktopWidget(InkscapeWindow *inkscape_window);
+
+public:
+ SPDesktopWidget(InkscapeWindow *inkscape_window, SPDocument *document);
+ ~SPDesktopWidget() override;
+
+ Inkscape::UI::Widget::CanvasGrid *get_canvas_grid() { return _canvas_grid; } // Temp, I hope!
+ Inkscape::UI::Widget::Canvas *get_canvas() { return _canvas; }
+
+ Gio::ActionMap* get_action_map();
+
+ void on_realize() override;
+ void on_unrealize() override;
+
+ sigc::connection modified_connection;
+
+ SPDesktop *desktop = nullptr;
+
+ InkscapeWindow *window = nullptr;
+ Gtk::MenuBar *_menubar;
+private:
+ // Flags for ruler event handling
+ bool _ruler_clicked = false; ///< True if the ruler has been clicked
+ bool _ruler_dragged = false; ///< True if a drag on the ruler is occurring
+
+ bool update = false;
+
+ CanvasItemPtr<Inkscape::CanvasItemGuideLine> _active_guide; ///< The guide being handled during a ruler event
+ Geom::Point _normal; ///< Normal to the guide currently being handled during ruler event
+ int _xp = 0; ///< x coordinate for start of drag
+ int _yp = 0; ///< y coordinate for start of drag
+
+ // The root vbox of the window layout.
+ Gtk::Box *_vbox;
+
+ Gtk::Paned *_tbbox;
+ Gtk::Box *_hbox;
+ Inkscape::UI::Dialog::DialogContainer *_container = nullptr;
+ Inkscape::UI::Dialog::DialogMultipaned *_columns;
+ Gtk::Grid* _top_toolbars;
+
+ Gtk::Box *_statusbar;
+
+ Inkscape::UI::Dialog::SwatchesPanel *_panels;
+
+ Glib::RefPtr<Gtk::Adjustment> _hadj;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+
+ Gtk::Grid *_coord_status;
+
+ Gtk::Label *_select_status;
+ Gtk::Label *_coord_status_x;
+ Gtk::Label *_coord_status_y;
+
+ Gtk::Box* _zoom_status_box;
+ Inkscape::UI::Widget::SpinButton *_zoom_status;
+ sigc::connection _zoom_status_input_connection;
+ sigc::connection _zoom_status_output_connection;
+ sigc::connection _zoom_status_value_changed_connection;
+ sigc::connection _zoom_status_populate_popup_connection;
+
+ Gtk::Box* _rotation_status_box;
+ Inkscape::UI::Widget::SpinButton *_rotation_status = nullptr;
+ sigc::connection _rotation_status_input_connection;
+ sigc::connection _rotation_status_output_connection;
+ sigc::connection _rotation_status_value_changed_connection;
+ sigc::connection _rotation_status_populate_popup_connection;
+
+
+ Inkscape::UI::Widget::SelectedStyle *_selected_style;
+
+ /** A grid for display the canvas, rulers, and scrollbars. */
+ Inkscape::UI::Widget::CanvasGrid *_canvas_grid;
+
+ unsigned int _interaction_disabled_counter = 0;
+
+public:
+ double _dt2r;
+
+private:
+ Inkscape::UI::Widget::Canvas *_canvas = nullptr;
+ std::vector<sigc::connection> _connections;
+ Inkscape::PrefObserver _statusbar_preferences_observer;
+ Inkscape::UI::Widget::LayerSelector* _layer_selector;
+ Inkscape::UI::Widget::PageSelector* _page_selector;
+
+public:
+ EgeColorProfTracker* _tracker;
+
+ void setMessage(Inkscape::MessageType type, gchar const *message);
+ void viewSetPosition (Geom::Point p);
+ void letZoomGrabFocus();
+ void getWindowGeometry (gint &x, gint &y, gint &w, gint &h);
+ void setWindowPosition (Geom::Point p);
+ void setWindowSize (gint w, gint h);
+ void setWindowTransient (void *p, int transient_policy);
+ void presentWindow();
+ bool showInfoDialog( Glib::ustring const &message );
+ bool warnDialog (Glib::ustring const &text);
+ Gtk::Toolbar* get_toolbar_by_name(const Glib::ustring& name);
+ void setToolboxFocusTo (gchar const *);
+ void setToolboxAdjustmentValue (gchar const * id, double value);
+ bool isToolboxButtonActive (gchar const *id);
+ void setCoordinateStatus(Geom::Point p);
+ void enableInteraction();
+ void disableInteraction();
+ void updateTitle(gchar const *uri);
+ bool onFocusInEvent(GdkEventFocus *);
+ Inkscape::UI::Dialog::DialogContainer *getDialogContainer();
+ void showNotice(Glib::ustring const &msg, unsigned timeout = 0);
+
+ Gtk::MenuBar *menubar() { return _menubar; }
+
+ void updateNamedview();
+ void update_guides_lock();
+
+ // Canvas Grid Widget
+ void cms_adjust_set_sensitive(bool enabled);
+ bool get_color_prof_adj_enabled() const;
+ void toggle_color_prof_adj();
+ void update_zoom();
+ void update_rotation();
+ void update_rulers();
+ void repack_snaptoolbar();
+
+ void iconify();
+ void maximize();
+ void fullscreen();
+ static gint ruler_event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw, bool horiz);
+
+ void layoutWidgets();
+ void toggle_scrollbars();
+ void update_scrollbars(double scale);
+ void toggle_command_palette();
+ void toggle_rulers();
+ void sticky_zoom_toggled();
+ void sticky_zoom_updated();
+
+ Gtk::Widget *get_tool_toolbox() const { return Glib::wrap(tool_toolbox); }
+private:
+ GtkWidget *tool_toolbox;
+ GtkWidget *aux_toolbox;
+ GtkWidget *commands_toolbox;
+ GtkWidget *snap_toolbox;
+ Inkscape::PrefObserver _tb_snap_pos;
+ Inkscape::PrefObserver _tb_icon_sizes1;
+ Inkscape::PrefObserver _tb_icon_sizes2;
+ Inkscape::PrefObserver _tb_visible_buttons;
+ Inkscape::PrefObserver _ds_sticky_zoom;
+
+ void namedviewModified(SPObject *obj, guint flags);
+ int zoom_input(double *new_val);
+ bool zoom_output();
+ void zoom_value_changed();
+ void zoom_menu_handler(double factor);
+ void zoom_populate_popup(Gtk::Menu *menu);
+ bool rotation_output();
+ void rotation_value_changed();
+ void rotation_populate_popup(Gtk::Menu *menu);
+ //void canvas_tbl_size_allocate(Gtk::Allocation &allocation);
+ void update_statusbar_visibility();
+ void apply_ctrlbar_settings();
+
+public:
+ void cms_adjust_toggled();
+private:
+ static void color_profile_event(EgeColorProfTracker *tracker, SPDesktopWidget *dtw);
+ static void ruler_snap_new_guide(SPDesktop *desktop, Geom::Point &event_dt, Geom::Point &normal);
+ static gint event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw);
+
+public: // Move to CanvasGrid
+ bool on_ruler_box_button_press_event(GdkEventButton *event, Gtk::Widget *widget, bool horiz);
+ bool on_ruler_box_button_release_event(GdkEventButton *event, Gtk::Widget *widget, bool horiz);
+ bool on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::Widget *widget, bool horiz);
+ void on_adjustment_value_changed();
+};
+
+#endif /* !SEEN_SP_DESKTOP_WIDGET_H */
+
+/*
+ Local Variables:
+mode:c++
+c-file-style:"stroustrup"
+c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+indent-tabs-mode:nil
+fill-column:99
+End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/widgets/paintdef.cpp b/src/widgets/paintdef.cpp
new file mode 100644
index 0000000..a3fba3f
--- /dev/null
+++ b/src/widgets/paintdef.cpp
@@ -0,0 +1,190 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Eek Color Definition.
+ *
+ * The Initial Developer of the Original Code is
+ * Jon A. Cruz.
+ * Portions created by the Initial Developer are Copyright (C) 2006
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "paintdef.h"
+
+#include <memory>
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <cstdio>
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/regex.h>
+
+static char const *mimeOSWB_COLOR = "application/x-oswb-color";
+static char const *mimeX_COLOR = "application/x-color";
+static char const *mimeTEXT = "text/plain";
+
+PaintDef::PaintDef()
+ : description(_("none"))
+ , type(NONE)
+ , rgb({0, 0, 0})
+{
+}
+
+PaintDef::PaintDef(std::array<unsigned, 3> const &rgb, std::string description)
+ : description(std::move(description))
+ , type(RGB)
+ , rgb(rgb)
+{
+}
+
+std::string PaintDef::get_color_id() const
+{
+ if (type == NONE) {
+ return "none";
+ }
+ if (!description.empty() && description[0] != '#') {
+ auto name = Glib::ustring(std::move(description));
+ // Convert description to ascii, strip out symbols, remove duplicate dashes and prefixes
+ static auto const reg1 = Glib::Regex::create("[^[:alnum:]]");
+ name = reg1->replace(name, 0, "-", static_cast<Glib::RegexMatchFlags>(0));
+ static auto const reg2 = Glib::Regex::create("-{2,}");
+ name = reg2->replace(name, 0, "-", static_cast<Glib::RegexMatchFlags>(0));
+ static auto const reg3 = Glib::Regex::create("(^-|-$)");
+ name = reg3->replace(name, 0, "", static_cast<Glib::RegexMatchFlags>(0));
+ // Move important numbers from the start where they are invalid xml, to the end.
+ static auto const reg4 = Glib::Regex::create("^(\\d+)(-?)([^\\d]*)");
+ name = reg4->replace(name, 0, "\\3\\2\\1", static_cast<Glib::RegexMatchFlags>(0));
+ return name.lowercase();
+ }
+ auto [r, g, b] = rgb;
+ char buf[12];
+ std::snprintf(buf, 12, "rgb%02x%02x%02x", r, g, b);
+ return std::string(buf);
+}
+
+std::vector<std::string> const &PaintDef::getMIMETypes()
+{
+ static std::vector<std::string> mimetypes = {mimeOSWB_COLOR, mimeX_COLOR, mimeTEXT};
+ return mimetypes;
+}
+
+std::pair<std::vector<char>, int> PaintDef::getMIMEData(std::string const &type) const
+{
+ auto from_data = [] (void const *p, int len) {
+ std::vector<char> v(len);
+ std::memcpy(v.data(), p, len);
+ return v;
+ };
+
+ auto [r, g, b] = rgb;
+
+ if (type == mimeTEXT) {
+ std::array<char, 8> tmp;
+ std::snprintf(tmp.data(), 8, "#%02x%02x%02x", r, g, b);
+ return std::make_pair(from_data(tmp.data(), 8), 8);
+ } else if (type == mimeX_COLOR) {
+ std::array<uint16_t, 4> tmp = {(uint16_t)((r << 8) | r), (uint16_t)((g << 8) | g), (uint16_t)((b << 8) | b), 0xffff};
+ return std::make_pair(from_data(tmp.data(), 8), 16);
+ } else if (type == mimeOSWB_COLOR) {
+ std::string tmp("<paint>");
+ switch (get_type()) {
+ case PaintDef::NONE:
+ tmp += "<nocolor/>";
+ break;
+ default:
+ tmp += std::string("<color name=\"") + description + "\">";
+ tmp += "<sRGB r=\"";
+ tmp += Glib::Ascii::dtostr(r / 255.0);
+ tmp += "\" g=\"";
+ tmp += Glib::Ascii::dtostr(g / 255.0);
+ tmp += "\" b=\"";
+ tmp += Glib::Ascii::dtostr(b / 255.0);
+ tmp += "\"/>";
+ tmp += "</color>";
+ }
+ tmp += "</paint>";
+ return std::make_pair(from_data(tmp.c_str(), tmp.size()), 8);
+ } else {
+ return {{}, 0};
+ }
+}
+
+bool PaintDef::fromMIMEData(std::string const &type_str, char const *data, int len)
+{
+ if (type_str == mimeTEXT) {
+ // unused
+ } else if (type_str == mimeX_COLOR) {
+ // unused
+ } else if (type_str == mimeOSWB_COLOR) {
+ std::string xml(data, len);
+ if (xml.find("<nocolor/>") != std::string::npos) {
+ type = PaintDef::NONE;
+ rgb = {0, 0, 0};
+ return true;
+ } else if (auto pos = xml.find("<sRGB"); pos != std::string::npos) {
+ std::string srgb = xml.substr(pos, xml.find(">", pos));
+ type = PaintDef::RGB;
+ if (auto numPos = srgb.find("r="); numPos != std::string::npos) {
+ double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3));
+ rgb[0] = static_cast<int>(255 * dbl);
+ }
+ if (auto numPos = srgb.find("g="); numPos != std::string::npos) {
+ double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3));
+ rgb[1] = static_cast<int>(255 * dbl);
+ }
+ if (auto numPos = srgb.find("b="); numPos != std::string::npos) {
+ double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3));
+ rgb[2] = static_cast<int>(255 * dbl);
+ }
+ if (auto pos = xml.find("<color "); pos != std::string::npos) {
+ std::string colorTag = xml.substr(pos, xml.find(">", pos));
+ if (auto namePos = colorTag.find("name="); namePos != std::string::npos) {
+ char quote = colorTag[namePos + 5];
+ auto endPos = colorTag.find(quote, namePos + 6);
+ description = colorTag.substr(namePos + 6, endPos - (namePos + 6));
+ }
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/paintdef.h b/src/widgets/paintdef.h
new file mode 100644
index 0000000..f66ae03
--- /dev/null
+++ b/src/widgets/paintdef.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Eek Color Definition.
+ *
+ * The Initial Developer of the Original Code is
+ * Jon A. Cruz.
+ * Portions created by the Initial Developer are Copyright (C) 2006
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef INKSCAPE_WIDGETS_PAINTDEF_H
+#define INKSCAPE_WIDGETS_PAINTDEF_H
+
+#include <string>
+#include <vector>
+#include <array>
+#include <utility>
+
+/**
+ * Pure data representation of a color definition.
+ */
+class PaintDef
+{
+public:
+ enum ColorType
+ {
+ NONE,
+ RGB
+ };
+
+ /// Create a color of type NONE
+ PaintDef();
+
+ /// Create a color of type RGB
+ PaintDef(std::array<unsigned, 3> const &rgb, std::string description);
+
+ std::string get_color_id() const;
+
+ std::string const &get_description() const { return description; }
+ ColorType get_type() const { return type; }
+ std::array<unsigned, 3> const &get_rgb() const { return rgb; }
+
+ static std::vector<std::string> const &getMIMETypes();
+ std::pair<std::vector<char>, int> getMIMEData(std::string const &type) const;
+ bool fromMIMEData(std::string const &type, char const *data, int len);
+
+protected:
+ std::string description;
+ ColorType type;
+ std::array<unsigned, 3> rgb;
+};
+
+#endif // INKSCAPE_WIDGETS_PAINTDEF_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/sp-attribute-widget.cpp b/src/widgets/sp-attribute-widget.cpp
new file mode 100644
index 0000000..90ca6ea
--- /dev/null
+++ b/src/widgets/sp-attribute-widget.cpp
@@ -0,0 +1,297 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Base widget for user input of object properties.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@ximian.com>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2012, authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/grid.h>
+
+#include "sp-attribute-widget.h"
+
+#include "include/macros.h"
+#include "document.h"
+#include "document-undo.h"
+
+#include "include/gtkmm_version.h"
+
+#include "object/sp-object.h"
+
+#include "xml/repr.h"
+
+using Inkscape::DocumentUndo;
+
+/**
+ * Callback for user input in one of the entries.
+ *
+ * sp_attribute_table_entry_changed set the object property
+ * to the new value and updates history. It is a callback from
+ * the entries created by SPAttributeTable.
+ *
+ * @param editable pointer to the entry box.
+ * @param spat pointer to the SPAttributeTable instance.
+ */
+static void sp_attribute_table_entry_changed (Gtk::Entry *editable, SPAttributeTable *spat);
+/**
+ * Callback for a modification of the selected object (size, color, properties, etc.).
+ *
+ * sp_attribute_table_object_modified rereads the object properties
+ * and shows the values in the entry boxes. It is a callback from a
+ * connection of the SPObject.
+ *
+ * @param object the SPObject to which this instance is referring to.
+ * @param flags gives the applied modifications
+ * @param spat pointer to the SPAttributeTable instance.
+ */
+static void sp_attribute_table_object_modified (SPObject *object, guint flags, SPAttributeTable *spaw);
+/**
+ * Callback for the deletion of the selected object.
+ *
+ * sp_attribute_table_object_release invalidates all data of
+ * SPAttributeTable and disables the widget.
+ */
+static void sp_attribute_table_object_release (SPObject */*object*/, SPAttributeTable *spat);
+
+#define XPAD 4
+#define YPAD 2
+
+
+SPAttributeTable::SPAttributeTable () :
+ _object(nullptr),
+ blocked(false),
+ table(nullptr),
+ _attributes(),
+ _entries(),
+ modified_connection(),
+ release_connection()
+{
+}
+
+SPAttributeTable::SPAttributeTable (SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent) :
+ _object(nullptr),
+ blocked(false),
+ table(nullptr),
+ _attributes(),
+ _entries(),
+ modified_connection(),
+ release_connection()
+{
+ set_object(object, labels, attributes, parent);
+}
+
+SPAttributeTable::~SPAttributeTable ()
+{
+ clear();
+}
+
+void SPAttributeTable::clear()
+{
+ if (table)
+ {
+ std::vector<Gtk::Widget*> ch = table->get_children();
+ for (int i = (ch.size())-1; i >=0 ; i--)
+ {
+ Gtk::Widget *w = ch[i];
+ ch.pop_back();
+ if (w != nullptr)
+ {
+ try
+ {
+ sp_signal_disconnect_by_data (w->gobj(), this);
+ delete w;
+ }
+ catch(...)
+ {
+ }
+ }
+ }
+ ch.clear();
+ _attributes.clear();
+ _entries.clear();
+
+ delete table;
+ table = nullptr;
+ }
+
+ if (_object)
+ {
+ modified_connection.disconnect();
+ release_connection.disconnect();
+ _object = nullptr;
+ }
+}
+
+void SPAttributeTable::set_object(SPObject *object,
+ std::vector<Glib::ustring> &labels,
+ std::vector<Glib::ustring> &attributes,
+ GtkWidget* parent)
+{
+ g_return_if_fail (!object || !labels.empty() || !attributes.empty());
+ g_return_if_fail (labels.size() == attributes.size());
+
+ clear();
+ _object = object;
+
+ if (object) {
+ blocked = true;
+
+ // Set up object
+ modified_connection = object->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_attribute_table_object_modified), this));
+ release_connection = object->connectRelease (sigc::bind<1>(sigc::ptr_fun(&sp_attribute_table_object_release), this));
+
+ // Create table
+ table = new Gtk::Grid();
+
+ if (!(parent == nullptr))
+ gtk_container_add(GTK_CONTAINER(parent), (GtkWidget*)table->gobj());
+
+ // Fill rows
+ _attributes = attributes;
+ for (guint i = 0; i < (attributes.size()); i++) {
+ Gtk::Label *ll = new Gtk::Label (_(labels[i].c_str()));
+ ll->show();
+ ll->set_halign(Gtk::ALIGN_START);
+ ll->set_valign(Gtk::ALIGN_CENTER);
+ ll->set_vexpand(false);
+ ll->set_margin_end(XPAD);
+ ll->set_margin_top(YPAD);
+ ll->set_margin_bottom(YPAD);
+ table->attach(*ll, 0, i, 1, 1);
+
+ Gtk::Entry *ee = new Gtk::Entry();
+ ee->show();
+ const gchar *val = object->getRepr()->attribute(attributes[i].c_str());
+ ee->set_text (val ? val : (const gchar *) "");
+ ee->set_hexpand();
+ ee->set_vexpand(false);
+ ee->set_margin_start(XPAD);
+ ee->set_margin_top(YPAD);
+ ee->set_margin_bottom(YPAD);
+ table->attach(*ee, 1, i, 1, 1);
+
+ _entries.push_back(ee);
+ g_signal_connect ( ee->gobj(), "changed",
+ G_CALLBACK (sp_attribute_table_entry_changed),
+ this );
+ }
+ /* Show table */
+ table->show ();
+ blocked = false;
+ }
+}
+
+void SPAttributeTable::change_object(SPObject *object)
+{
+ if (_object)
+ {
+ modified_connection.disconnect();
+ release_connection.disconnect();
+ _object = nullptr;
+ }
+
+ _object = object;
+ if (_object) {
+ blocked = true;
+
+ // Set up object
+ modified_connection = _object->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_attribute_table_object_modified), this));
+ release_connection = _object->connectRelease (sigc::bind<1>(sigc::ptr_fun(&sp_attribute_table_object_release), this));
+ for (guint i = 0; i < (_attributes.size()); i++) {
+ const gchar *val = _object->getRepr()->attribute(_attributes[i].c_str());
+ _entries[i]->set_text(val ? val : "");
+ }
+
+ blocked = false;
+ }
+
+}
+
+void SPAttributeTable::reread_properties()
+{
+ blocked = true;
+ for (guint i = 0; i < (_attributes.size()); i++)
+ {
+ const gchar *val = _object->getRepr()->attribute(_attributes[i].c_str());
+ _entries[i]->set_text(val ? val : "");
+ }
+ blocked = false;
+}
+
+static void sp_attribute_table_object_modified ( SPObject */*object*/,
+ guint flags,
+ SPAttributeTable *spat )
+{
+ if (flags & SP_OBJECT_MODIFIED_FLAG)
+ {
+ std::vector<Glib::ustring> attributes = spat->get_attributes();
+ std::vector<Gtk::Entry *> entries = spat->get_entries();
+ Glib::ustring text="";
+ for (guint i = 0; i < (attributes.size()); i++) {
+ Gtk::Entry* e = entries[i];
+ const gchar *val = spat->_object->getRepr()->attribute(attributes[i].c_str());
+ text = e->get_text ();
+ if (val || !text.empty()) {
+ if (text != val) {
+ // We are different
+ spat->blocked = true;
+ e->set_text (val ? val : (const gchar *) "");
+ spat->blocked = false;
+ }
+ }
+ }
+ }
+
+} // end of sp_attribute_table_object_modified()
+
+static void sp_attribute_table_entry_changed ( Gtk::Entry *editable,
+ SPAttributeTable *spat )
+{
+ if (!spat->blocked)
+ {
+ std::vector<Glib::ustring> attributes = spat->get_attributes();
+ std::vector<Gtk::Entry *> entries = spat->get_entries();
+ for (guint i = 0; i < (attributes.size()); i++) {
+ Gtk::Entry *e = entries[i];
+ if ((GtkWidget*)editable == (GtkWidget*)e->gobj()) {
+ spat->blocked = true;
+ Glib::ustring text = e->get_text ();
+ if (spat->_object) {
+ spat->_object->getRepr()->setAttribute(attributes[i], text);
+ DocumentUndo::done(spat->_object->document, _("Set attribute"), "");
+ }
+ spat->blocked = false;
+ return;
+ }
+ }
+ g_warning ("file %s: line %d: Entry signalled change, but there is no such entry", __FILE__, __LINE__);
+ }
+
+} // end of sp_attribute_table_entry_changed()
+
+static void sp_attribute_table_object_release (SPObject */*object*/, SPAttributeTable *spat)
+{
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> attributes;
+ spat->set_object (nullptr, labels, attributes, nullptr);
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/sp-attribute-widget.h b/src/widgets/sp-attribute-widget.h
new file mode 100644
index 0000000..891db0a
--- /dev/null
+++ b/src/widgets/sp-attribute-widget.h
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Widget that listens and modifies repr attributes.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2002,2011-2012 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_DIALOGS_SP_ATTRIBUTE_WIDGET_H
+#define SEEN_DIALOGS_SP_ATTRIBUTE_WIDGET_H
+
+#include <gtkmm/bin.h>
+#include <cstddef>
+#include <sigc++/connection.h>
+
+namespace Gtk {
+class Entry;
+class Grid;
+}
+
+namespace Inkscape {
+namespace XML {
+class Node;
+}
+}
+
+class SPObject;
+
+/**
+ * A base class for dialogs to enter the value of several properties.
+ *
+ * SPAttributeTable is used if you want to alter several properties of
+ * an object. For each property, it creates an entry next to a label and
+ * positiones these labels and entries one by one below each other.
+ */
+class SPAttributeTable : public Gtk::Bin {
+public:
+ /**
+ * Constructor defaulting to no content.
+ */
+ SPAttributeTable ();
+
+ /**
+ * Constructor referring to a specific object.
+ *
+ * This constructor initializes all data fields and creates the necessary widgets.
+ * set_object is called for this purpose.
+ *
+ * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttributeTable instance.
+ * @param labels list of labels to be shown for the different attributes.
+ * @param attributes list of attributes whose value can be edited.
+ * @param parent the parent object owning the SPAttributeTable instance.
+ *
+ * @see set_object
+ */
+ SPAttributeTable (SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent);
+
+ ~SPAttributeTable () override;
+
+ /**
+ * Sets class properties and creates child widgets
+ *
+ * set_object initializes all data fields, creates links to the
+ * SPOject item and creates the necessary widgets. For n properties
+ * n labels and n entries are created and shown in tabular format.
+ *
+ * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttribuTable instance.
+ * @param labels list of labels to be shown for the different attributes.
+ * @param attributes list of attributes whose value can be edited.
+ * @param parent the parent object owning the SPAttributeTable instance.
+ */
+ void set_object(SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent);
+
+ /**
+ * Update values in entry boxes on change of object.
+ *
+ * change_object updates the values of the entry boxes in case the user
+ * of Inkscape selects an other object.
+ * change_object is a subset of set_object and should only be called by
+ * the parent class (holding the SPAttributeTable instance). This function
+ * should only be called when the number of properties/entries nor
+ * the labels do not change.
+ *
+ * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttribuTable instance.
+ */
+ void change_object(SPObject *object);
+
+ /**
+ * Clears data of SPAttributeTable instance, destroys all child widgets and closes connections.
+ */
+ void clear();
+
+ /**
+ * Reads the object attributes.
+ *
+ * Reads the object attributes and shows the new object attributes in the
+ * entry boxes. Caution: function should only be used when which there is
+ * no change in which objects are selected.
+ */
+ void reread_properties();
+
+ /**
+ * Gives access to the attributes list.
+ */
+ std::vector<Glib::ustring> get_attributes() {return _attributes;};
+
+ /**
+ * Gives access to the Gtk::Entry list.
+ */
+ std::vector<Gtk::Entry *> get_entries() {return _entries;};
+
+ /**
+ * Stores pointer to the selected object.
+ */
+ SPObject *_object;
+
+ /**
+ * Indicates whether SPAttributeTable is processing callbacks and whether it should accept any updating.
+ */
+ bool blocked;
+
+private:
+ /**
+ * Container widget for the dynamically created child widgets (labels and entry boxes).
+ */
+ Gtk::Grid *table;
+
+ /**
+ * List of attributes.
+ *
+ * _attributes stores the attribute names of the selected object that
+ * are valid and can be modified through this widget.
+ */
+ std::vector<Glib::ustring> _attributes;
+ /**
+ * List of pointers to the respective entry boxes.
+ *
+ * _entries stores pointers to the dynamically created entry boxes in which
+ * the user can midify the attributes of the selected object.
+ */
+ std::vector<Gtk::Entry *> _entries;
+
+ /**
+ * Sets the callback for a modification of the selection.
+ */
+ sigc::connection modified_connection;
+
+ /**
+ * Sets the callback for the deletion of the selected object.
+ */
+ sigc::connection release_connection;
+};
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/sp-xmlview-tree.cpp b/src/widgets/sp-xmlview-tree.cpp
new file mode 100644
index 0000000..a64e25d
--- /dev/null
+++ b/src/widgets/sp-xmlview-tree.cpp
@@ -0,0 +1,936 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Specialization of GtkTreeView for the XML tree view
+ *
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 2002 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "sp-xmlview-tree.h"
+
+#include <cstring>
+#include <glibmm/markup.h>
+#include <glibmm/property.h>
+#include <glibmm/ustring.h>
+#include <gmodule.h>
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/cellrenderertext.h>
+#include <memory>
+#include <string>
+
+#include "ui/syntax.h"
+#include "xml/node-observer.h"
+#include "xml/node.h"
+
+namespace {
+
+struct NodeData
+{
+ SPXMLViewTree *tree;
+ GtkTreeRowReference *rowref;
+ Inkscape::XML::Node *repr;
+ bool expanded = false; //< true if tree view has been expanded to this node
+ bool dragging = false;
+ std::unique_ptr<Inkscape::XML::NodeObserver> observer;
+
+ NodeData(SPXMLViewTree *tree, GtkTreeIter *node, Inkscape::XML::Node *repr);
+ ~NodeData();
+};
+
+// currently dragged node
+Inkscape::XML::Node *dragging_repr = nullptr;
+
+} // namespace
+
+enum { STORE_TEXT_COL = 0, STORE_DATA_COL, STORE_MARKUP_COL, STORE_N_COLS };
+
+static void sp_xmlview_tree_destroy(GtkWidget * object);
+
+static NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter);
+
+static void add_node(SPXMLViewTree *tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node *repr);
+
+static gboolean ref_to_sibling (NodeData *node, Inkscape::XML::Node * ref, GtkTreeIter *);
+static gboolean repr_to_child (NodeData *node, Inkscape::XML::Node * repr, GtkTreeIter *);
+static GtkTreeRowReference *tree_iter_to_ref(SPXMLViewTree *, GtkTreeIter *);
+static gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference *ref);
+
+static gboolean search_equal_func(GtkTreeModel *, gint column, const gchar *key, GtkTreeIter *, gpointer search_data);
+static gboolean foreach_func(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data);
+
+static void on_row_changed(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data);
+static void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata);
+static void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata);
+static gboolean do_drag_motion(GtkWidget *, GdkDragContext *, gint x, gint y, guint time, gpointer user_data);
+
+static bool get_first_child(NodeData *data, GtkTreeIter *child_iter);
+static void remove_dummy_rows(GtkTreeStore *store, GtkTreeIter *iter);
+static void sp_remove_newlines_and_tabs(std::string &val, size_t const maxlen = 200);
+
+namespace {
+
+static auto null_to_empty(char const *str)
+{
+ return str ? str : "";
+}
+
+class ElementNodeObserver final : public Inkscape::XML::NodeObserver
+{
+ NodeData *_nodedata;
+
+public:
+ ElementNodeObserver(NodeData *nodedata)
+ : _nodedata(nodedata)
+ {}
+
+ void notifyChildAdded(Inkscape::XML::Node&, Inkscape::XML::Node &child_, Inkscape::XML::Node *ref) override
+ {
+ GtkTreeIter before;
+ Inkscape::XML::Node *child = &child_;
+
+ if (_nodedata->tree->blocked) return;
+
+ if (!ref_to_sibling (_nodedata, ref, &before)) {
+ return;
+ }
+
+ GtkTreeIter data_iter;
+ tree_ref_to_iter(_nodedata->tree, &data_iter, _nodedata->rowref);
+
+ if (!_nodedata->expanded) {
+ auto model = GTK_TREE_MODEL(_nodedata->tree->store);
+ GtkTreeIter childiter;
+ if (!gtk_tree_model_iter_children(model, &childiter, &data_iter)) {
+ // no children yet, add a dummy
+ child = nullptr;
+ } else if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) {
+ // already has a dummy child
+ return;
+ }
+ }
+
+ add_node(_nodedata->tree, &data_iter, &before, child);
+ }
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark key_, Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared) override
+ {
+ auto const key = g_quark_to_string(key_);
+ if (std::strcmp(key, "id") != 0 && std::strcmp(key, "inkscape:label") != 0)
+ return;
+ elementAttrOrNameChangedUpdate(&node);
+ }
+
+ void notifyElementNameChanged(Inkscape::XML::Node &node, GQuark, GQuark) override
+ {
+ elementAttrOrNameChangedUpdate(&node);
+ }
+
+ void notifyChildOrderChanged(Inkscape::XML::Node &, Inkscape::XML::Node &child, Inkscape::XML::Node *,
+ Inkscape::XML::Node *newref) override
+ {
+ GtkTreeIter before, node;
+
+ if (_nodedata->tree->blocked)
+ return;
+
+ ref_to_sibling(_nodedata, newref, &before);
+ repr_to_child(_nodedata, &child, &node);
+
+ if (gtk_tree_store_iter_is_valid(_nodedata->tree->store, &before)) {
+ gtk_tree_store_move_before(_nodedata->tree->store, &node, &before);
+ } else {
+ repr_to_child(_nodedata, newref, &before);
+ gtk_tree_store_move_after(_nodedata->tree->store, &node, &before);
+ }
+ }
+
+ void notifyChildRemoved(Inkscape::XML::Node &repr, Inkscape::XML::Node &child, Inkscape::XML::Node *) override
+ {
+ if (_nodedata->tree->blocked)
+ return;
+
+ GtkTreeIter iter;
+ if (repr_to_child(_nodedata, &child, &iter)) {
+ delete sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(_nodedata->tree->store), &iter);
+ gtk_tree_store_remove(_nodedata->tree->store, &iter);
+ } else if (!repr.firstChild() && get_first_child(_nodedata, &iter)) {
+ // remove dummy when all children gone
+ remove_dummy_rows(_nodedata->tree->store, &iter);
+ } else {
+ return;
+ }
+
+#ifndef GTK_ISSUE_2510_IS_FIXED
+ // https://gitlab.gnome.org/GNOME/gtk/issues/2510
+ gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(GTK_TREE_VIEW(_nodedata->tree)));
+#endif
+ }
+
+ void elementAttrOrNameChangedUpdate(Inkscape::XML::Node *repr)
+ {
+ if (_nodedata->tree->blocked) {
+ return;
+ }
+
+ auto node_name = Glib::ustring(null_to_empty(repr->name()));
+ auto pos = node_name.find("svg:");
+ if (pos != Glib::ustring::npos) {
+ // Do not decorate element names the with default namespace "svg"; it is just visual noise.
+ node_name.erase(pos, 4);
+ }
+
+ // Build a plain-text and a markup-enabled representation of the node.
+ auto &formatter = *_nodedata->tree->formatter;
+ auto display_text = Glib::ustring::compose("<%1", node_name);
+ formatter.openTag(node_name.c_str());
+
+ if (auto id_value = repr->attribute("id")) {
+ display_text += Glib::ustring::compose(" id=\"%1\"", id_value);
+ formatter.addAttribute("id", id_value);
+ }
+ if (auto label_value = repr->attribute("inkscape:label")) {
+ display_text += Glib::ustring::compose(" inkscape:label=\"%1\"", label_value);
+ formatter.addAttribute("inkscape:label", label_value);
+ }
+ display_text += ">";
+ auto markup = formatter.finishTag();
+
+ GtkTreeIter iter;
+ if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) {
+ gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, display_text.c_str(), -1);
+ gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1);
+ }
+ }
+};
+
+class TextNodeObserver final : public Inkscape::XML::NodeObserver
+{
+ NodeData *_nodedata;
+
+public:
+ TextNodeObserver(NodeData *nodedata)
+ : _nodedata(nodedata)
+ {}
+
+ void notifyContentChanged(Inkscape::XML::Node &, Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared new_content) override
+ {
+ if (_nodedata->tree->blocked)
+ return;
+
+ auto text_content = std::string("\"").append(null_to_empty(new_content.pointer())).append("\"");
+ sp_remove_newlines_and_tabs(text_content);
+
+ auto &formatter = *_nodedata->tree->formatter;
+ auto markup = formatter.formatContent(text_content.c_str(), false);
+
+ GtkTreeIter iter;
+ if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) {
+ gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, text_content.c_str(), -1);
+ gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1);
+ }
+ }
+};
+
+class CommentNodeObserver final : public Inkscape::XML::NodeObserver
+{
+ NodeData *_nodedata;
+
+public:
+ CommentNodeObserver(NodeData *nodedata)
+ : _nodedata(nodedata)
+ {}
+ void notifyContentChanged(Inkscape::XML::Node &, Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared new_content) override
+ {
+ if (_nodedata->tree->blocked)
+ return;
+
+ auto comment = std::string("<!--").append(null_to_empty(new_content.pointer())).append("-->");
+ sp_remove_newlines_and_tabs(comment);
+
+ auto &formatter = *_nodedata->tree->formatter;
+ auto markup = formatter.formatComment(comment.c_str(), false);
+
+ GtkTreeIter iter;
+ if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) {
+ gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, comment.c_str(), -1);
+ gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1);
+ }
+ }
+};
+
+class PINodeObserver final : public Inkscape::XML::NodeObserver
+{
+ NodeData *_nodedata;
+
+public:
+ PINodeObserver(NodeData *nodedata)
+ : _nodedata(nodedata)
+ {}
+ void notifyContentChanged(Inkscape::XML::Node &repr, Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared new_content) override
+ {
+ if (_nodedata->tree->blocked)
+ return;
+
+ auto processing_instr = std::string("<?").append(repr.name()).append(" ").append(null_to_empty(new_content.pointer())).append("?>");
+ sp_remove_newlines_and_tabs(processing_instr);
+
+ auto &formatter = *_nodedata->tree->formatter;
+ auto markup = formatter.formatProlog(processing_instr.c_str());
+
+ GtkTreeIter iter;
+ if (tree_ref_to_iter(_nodedata->tree, &iter, _nodedata->rowref)) {
+ gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_TEXT_COL, processing_instr.c_str(), -1);
+ gtk_tree_store_set(GTK_TREE_STORE(_nodedata->tree->store), &iter, STORE_MARKUP_COL, markup.c_str(), -1);
+ }
+ }
+};
+
+} // namespace
+
+/**
+ * Get an iterator to the first child of `data`
+ * @param data handle which references a row
+ * @param[out] child_iter On success: valid iterator to first child
+ * @return False if the node has no children
+ */
+static bool get_first_child(NodeData *data, GtkTreeIter *child_iter)
+{
+ GtkTreeIter iter;
+ return tree_ref_to_iter(data->tree, &iter, data->rowref) &&
+ gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), child_iter, &iter);
+}
+
+/**
+ * @param iter First dummy row on that level
+ * @pre all rows on the same level are dummies
+ * @pre iter is valid
+ * @post iter is invalid
+ * @post level is empty
+ */
+static void remove_dummy_rows(GtkTreeStore *store, GtkTreeIter *iter)
+{
+ do {
+ g_assert(nullptr == sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(store), iter));
+ gtk_tree_store_remove(store, iter);
+ } while (gtk_tree_store_iter_is_valid(store, iter));
+}
+
+static gboolean on_test_expand_row( //
+ GtkTreeView *tree_view, //
+ GtkTreeIter *iter, //
+ GtkTreePath *path, //
+ gpointer)
+{
+ auto tree = SP_XMLVIEW_TREE(tree_view);
+ auto model = GTK_TREE_MODEL(tree->store);
+
+ GtkTreeIter childiter;
+ bool has_children = gtk_tree_model_iter_children(model, &childiter, iter);
+ g_assert(has_children);
+
+ if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) {
+ NodeData *data = sp_xmlview_tree_node_get_data(model, iter);
+
+ remove_dummy_rows(tree->store, &childiter);
+
+ // insert real rows
+ data->expanded = true;
+ ElementNodeObserver e(data);
+ data->repr->synthesizeEvents(e);
+ }
+
+ return false;
+}
+
+/** Node name renderer for XML tree.
+ * It knows to use markup, but falls back to plain text for selected nodes.
+ */
+class NodeRenderer : public Gtk::CellRendererText {
+public:
+ NodeRenderer():
+ Glib::ObjectBase(typeid(CellRendererText)),
+ Gtk::CellRendererText(),
+ _property_plain_text(*this, "plain", "-") {}
+
+ // "text" and "markup" properties from CellRendererText are in use for marked up text;
+ // we need a separate property for plain text (placeholder_text could be hijacked potentially)
+ Glib::Property<Glib::ustring> _property_plain_text;
+
+ void render_vfunc(const Cairo::RefPtr<Cairo::Context>& ctx,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) override {
+ if (flags & Gtk::CELL_RENDERER_SELECTED) {
+ // use plain text instead of marked up text to render selected nodes, so they are legible
+ property_text() = _property_plain_text.get_value();
+ }
+ Gtk::CellRendererText::render_vfunc(ctx, widget, background_area, cell_area, flags);
+ }
+};
+
+GtkWidget *sp_xmlview_tree_new(Inkscape::XML::Node * repr, void * /*factory*/, void * /*data*/)
+{
+ SPXMLViewTree *tree = SP_XMLVIEW_TREE(g_object_new (SP_TYPE_XMLVIEW_TREE, nullptr));
+ tree->_tree_move = new sigc::signal<void ()>();
+
+ gtk_tree_view_set_headers_visible (GTK_TREE_VIEW(tree), FALSE);
+ gtk_tree_view_set_reorderable (GTK_TREE_VIEW(tree), TRUE);
+ gtk_tree_view_set_enable_search (GTK_TREE_VIEW(tree), TRUE);
+ gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW(tree), search_equal_func, nullptr, nullptr);
+
+ auto r = new NodeRenderer();
+ tree->renderer = r;
+
+ GtkCellRenderer* renderer = r->Gtk::CellRenderer::gobj();
+ GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("", renderer,
+ "markup", STORE_MARKUP_COL, // marked up text for decorated color output
+ "plain", STORE_TEXT_COL, // plain text for searching and rendering selected nodes
+ nullptr);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
+ gtk_cell_renderer_set_padding (renderer, 2, 0);
+ gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
+
+ sp_xmlview_tree_set_repr (tree, repr);
+
+ g_signal_connect(GTK_TREE_VIEW(tree), "drag-begin", G_CALLBACK(on_drag_begin), tree);
+ g_signal_connect(GTK_TREE_VIEW(tree), "drag-end", G_CALLBACK(on_drag_end), tree);
+ g_signal_connect(GTK_TREE_VIEW(tree), "drag-motion", G_CALLBACK(do_drag_motion), tree);
+ g_signal_connect(GTK_TREE_VIEW(tree), "test-expand-row", G_CALLBACK(on_test_expand_row), nullptr);
+
+ tree->formatter = new Inkscape::UI::Syntax::XMLFormatter();
+
+ return GTK_WIDGET(tree);
+}
+
+G_DEFINE_TYPE(SPXMLViewTree, sp_xmlview_tree, GTK_TYPE_TREE_VIEW);
+
+void sp_xmlview_tree_class_init(SPXMLViewTreeClass * klass)
+{
+ auto widget_class = GTK_WIDGET_CLASS(klass);
+ widget_class->destroy = sp_xmlview_tree_destroy;
+}
+
+void
+sp_xmlview_tree_init (SPXMLViewTree * tree)
+{
+ tree->repr = nullptr;
+ tree->blocked = 0;
+}
+
+void sp_xmlview_tree_destroy(GtkWidget * object)
+{
+ SPXMLViewTree * tree = SP_XMLVIEW_TREE (object);
+
+ delete tree->renderer;
+ tree->renderer = nullptr;
+ delete tree->formatter;
+ tree->formatter = nullptr;
+ delete tree->_tree_move;
+ tree->_tree_move = nullptr;
+
+ sp_xmlview_tree_set_repr (tree, nullptr);
+
+ GTK_WIDGET_CLASS(sp_xmlview_tree_parent_class)->destroy (object);
+}
+
+/*
+ * Add a new row to the tree
+ */
+void
+add_node (SPXMLViewTree * tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node * repr)
+{
+ g_assert (tree != nullptr);
+
+ if (before && !gtk_tree_store_iter_is_valid(tree->store, before)) {
+ before = nullptr;
+ }
+
+ GtkTreeIter iter;
+ gtk_tree_store_insert_before (tree->store, &iter, parent, before);
+
+ if (!gtk_tree_store_iter_is_valid(tree->store, &iter)) {
+ return;
+ }
+
+ if (!repr) {
+ // no need to store any data
+ return;
+ }
+
+ auto data = new NodeData(tree, &iter, repr);
+
+ g_assert(data != nullptr);
+
+ gtk_tree_store_set(tree->store, &iter, STORE_DATA_COL, data, -1);
+
+ if (repr->type() == Inkscape::XML::NodeType::TEXT_NODE) {
+ data->observer = std::make_unique<TextNodeObserver>(data);
+ } else if (repr->type() == Inkscape::XML::NodeType::COMMENT_NODE) {
+ data->observer = std::make_unique<CommentNodeObserver>(data);
+ } else if (repr->type() == Inkscape::XML::NodeType::PI_NODE) {
+ data->observer = std::make_unique<PINodeObserver>(data);
+ } else if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE) {
+ data->observer = std::make_unique<ElementNodeObserver>(data);
+ }
+
+ if (data->observer) {
+ /* cheat a little to get the text updated on nodes without id */
+ if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE && repr->attribute("id") == nullptr) {
+ data->observer->notifyAttributeChanged(*repr, g_quark_from_static_string("id"),
+ Inkscape::Util::ptr_shared(), Inkscape::Util::ptr_shared());
+ }
+ repr->addObserver(*data->observer);
+ repr->synthesizeEvents(*data->observer);
+ }
+}
+
+static gboolean remove_all_listeners(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer)
+{
+ NodeData *data = sp_xmlview_tree_node_get_data(model, iter);
+ delete data;
+ return false;
+}
+
+NodeData::NodeData(SPXMLViewTree *tree, GtkTreeIter *iter, Inkscape::XML::Node *repr)
+ : tree(tree)
+ , rowref(tree_iter_to_ref(tree, iter))
+ , repr(repr)
+{
+ if (repr) {
+ Inkscape::GC::anchor(repr);
+ }
+}
+
+NodeData::~NodeData()
+{
+ if (repr) {
+ if (observer) {
+ repr->removeObserver(*observer);
+ }
+ Inkscape::GC::release(repr);
+ }
+ gtk_tree_row_reference_free(rowref);
+}
+
+/**
+ * Truncate `val` to `maxlen` unicode characters and replace newlines and tabs
+ * with placeholder symbols. The string is modified in place.
+ * @param[in,out] val String in UTF-8 encoding
+ */
+static void sp_remove_newlines_and_tabs(std::string &val, size_t const maxlen)
+{
+ if (g_utf8_strlen(val.data(), maxlen * 2) > maxlen) {
+ size_t newlen = g_utf8_offset_to_pointer(val.data(), maxlen - 3) - val.data();
+ val.resize(newlen);
+ val.append("…");
+ }
+
+ struct
+ {
+ const char *query;
+ const char *replacement;
+ } replacements[] = {
+ {"\r\n", "⏎"},
+ {"\n", "⏎"},
+ {"\t", "⇥"},
+ };
+
+ for (auto const &item : replacements) {
+ for (size_t pos = 0; (pos = val.find(item.query, pos)) != std::string::npos;) {
+ val.replace(pos, strlen(item.query), item.replacement);
+ }
+ }
+}
+
+/*
+ * Save the source path on drag start, will need it in on_row_changed() when moving a row
+ */
+void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata)
+{
+ SPXMLViewTree *tree = static_cast<SPXMLViewTree *>(userdata);
+ if (!tree) {
+ return;
+ }
+
+ GtkTreeModel *model = nullptr;
+ GtkTreeIter iter;
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
+ NodeData *data = sp_xmlview_tree_node_get_data(model, &iter);
+ if (data) {
+ data->dragging = true;
+ dragging_repr = data->repr;
+ }
+ }
+}
+
+/**
+ * Finalize what happened in `on_row_changed` and clean up what was set up in `on_drag_begin`
+ */
+void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata)
+{
+ if (!dragging_repr)
+ return;
+
+ auto tree = static_cast<SPXMLViewTree *>(userdata);
+ auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ bool failed = false;
+
+ GtkTreeIter iter;
+ if (sp_xmlview_tree_get_repr_node(tree, dragging_repr, &iter)) {
+ NodeData *data = sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(tree->store), &iter);
+
+ if (data && data->dragging) {
+ // dragging flag was not cleared in `on_row_changed`, this indicates a failed drag
+ data->dragging = false;
+ failed = true;
+ } else {
+ // Reselect the dragged row
+ gtk_tree_selection_select_iter(selection, &iter);
+ }
+ } else {
+#ifndef GTK_ISSUE_2510_IS_FIXED
+ // https://gitlab.gnome.org/GNOME/gtk/issues/2510
+ gtk_tree_selection_unselect_all(selection);
+#endif
+ }
+
+ dragging_repr = nullptr;
+
+ if (!failed) {
+ // Signal that a drag and drop has completed successfully
+ tree->_tree_move->emit();
+ }
+}
+
+/*
+ * Main drag & drop function
+ * Get the old and new paths, and change the Inkscape::XML::Node repr's
+ */
+void on_row_changed(GtkTreeModel *tree_model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
+{
+ NodeData *data = sp_xmlview_tree_node_get_data(tree_model, iter);
+
+ if (!data || !data->dragging) {
+ return;
+ }
+ data->dragging = false;
+
+ SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data);
+
+ gtk_tree_row_reference_free(data->rowref);
+ data->rowref = tree_iter_to_ref(tree, iter);
+
+ GtkTreeIter new_parent;
+ if (!gtk_tree_model_iter_parent(tree_model, &new_parent, iter)) {
+ //No parent of drop location
+ return;
+ }
+
+ Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(tree_model, iter);
+ Inkscape::XML::Node *before_repr = nullptr;
+
+ // Find the sibling node before iter
+ GtkTreeIter before_iter = *iter;
+ if (gtk_tree_model_iter_previous(tree_model, &before_iter)) {
+ before_repr = sp_xmlview_tree_node_get_repr(tree_model, &before_iter);
+ }
+
+ // Drop onto oneself causes assert in changeOrder() below, ignore
+ if (repr == before_repr)
+ return;
+
+ auto repr_old_parent = repr->parent();
+ auto repr_new_parent = sp_xmlview_tree_node_get_repr(tree_model, &new_parent);
+
+ tree->blocked++;
+
+ if (repr_old_parent == repr_new_parent) {
+ repr_old_parent->changeOrder(repr, before_repr);
+ } else {
+ repr_old_parent->removeChild(repr);
+ repr_new_parent->addChild(repr, before_repr);
+ }
+
+ NodeData *data_new_parent = sp_xmlview_tree_node_get_data(tree_model, &new_parent);
+ if (data_new_parent && data_new_parent->expanded) {
+ // Reselect the dragged row in `on_drag_end` instead of here, because of
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/2510
+ } else {
+ // convert to dummy node
+ delete data;
+ gtk_tree_store_set(tree->store, iter, STORE_DATA_COL, nullptr, -1);
+ }
+
+ tree->blocked--;
+}
+
+/*
+ * Set iter to ref or node data's child with the same repr or first child
+ */
+gboolean ref_to_sibling (NodeData *data, Inkscape::XML::Node *repr, GtkTreeIter *iter)
+{
+ if (repr) {
+ if (!repr_to_child (data, repr, iter)) {
+ return false;
+ }
+ gtk_tree_model_iter_next (GTK_TREE_MODEL(data->tree->store), iter);
+ } else {
+ GtkTreeIter data_iter;
+ if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) {
+ return false;
+ }
+ gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), iter, &data_iter);
+ }
+ return true;
+}
+
+/*
+ * Set iter to the node data's child with the same repr
+ */
+gboolean repr_to_child (NodeData *data, Inkscape::XML::Node * repr, GtkTreeIter *iter)
+{
+ GtkTreeIter data_iter;
+ GtkTreeModel *model = GTK_TREE_MODEL(data->tree->store);
+ gboolean valid = false;
+
+ if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) {
+ return false;
+ }
+
+ /*
+ * The node we are looking for is likely to be the last one, so check it first.
+ */
+ gint n_children = gtk_tree_model_iter_n_children (model, &data_iter);
+ if (n_children > 1) {
+ valid = gtk_tree_model_iter_nth_child (model, iter, &data_iter, n_children-1);
+ if (valid && sp_xmlview_tree_node_get_repr (model, iter) == repr) {
+ //g_message("repr_to_child hit %d", n_children);
+ return valid;
+ }
+ }
+
+ valid = gtk_tree_model_iter_children(model, iter, &data_iter);
+ while (valid && sp_xmlview_tree_node_get_repr (model, iter) != repr) {
+ valid = gtk_tree_model_iter_next(model, iter);
+ }
+
+ return valid;
+}
+
+/*
+ * Get a matching GtkTreeRowReference for a GtkTreeIter
+ */
+GtkTreeRowReference *tree_iter_to_ref (SPXMLViewTree * tree, GtkTreeIter* iter)
+{
+ GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), iter);
+ GtkTreeRowReference *ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(tree->store), path);
+ gtk_tree_path_free(path);
+
+ return ref;
+}
+
+/*
+ * Get a matching GtkTreeIter for a GtkTreeRowReference
+ */
+gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference *ref)
+{
+ GtkTreePath* path = gtk_tree_row_reference_get_path(ref);
+ if (!path) {
+ return false;
+ }
+ gboolean const valid = //
+ gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), iter, path);
+ gtk_tree_path_free(path);
+
+ return valid;
+}
+
+/*
+ * Disable drag and drop target on : root node and non-element nodes
+ */
+gboolean do_drag_motion(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data)
+{
+ GtkTreePath *path = nullptr;
+ GtkTreeViewDropPosition pos;
+ gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW(widget), x, y, &path, &pos);
+
+ int action = 0;
+
+ if (!dragging_repr) {
+ goto finally;
+ }
+
+ if (path) {
+ SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data);
+ GtkTreeIter iter;
+ gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), &iter, path);
+ auto repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), &iter);
+
+ bool const drop_into = pos != GTK_TREE_VIEW_DROP_BEFORE && //
+ pos != GTK_TREE_VIEW_DROP_AFTER;
+
+ // 0. don't drop on self (also handled by on_row_changed but nice to not have drop highlight for it)
+ if (repr == dragging_repr) {
+ goto finally;
+ }
+
+ // 1. only xml elements can have children
+ if (drop_into && repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE) {
+ goto finally;
+ }
+
+ // 3. elements must be at least children of the root <svg:svg> element
+ if (gtk_tree_path_get_depth(path) < 2) {
+ goto finally;
+ }
+
+ // 4. drag node specific limitations
+ {
+ // nodes which can't be re-parented (because the document holds pointers to them which must stay valid)
+ static GQuark const CODE_sodipodi_namedview = g_quark_from_static_string("sodipodi:namedview");
+ static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs");
+
+ bool const no_reparenting = dragging_repr->code() == CODE_sodipodi_namedview || //
+ dragging_repr->code() == CODE_svg_defs;
+
+ if (no_reparenting && (drop_into || dragging_repr->parent() != repr->parent())) {
+ goto finally;
+ }
+ }
+
+ action = GDK_ACTION_MOVE;
+ }
+
+finally:
+ if (action == 0) {
+ // remove drop highlight
+ gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(widget), nullptr, pos /* ignored */);
+ }
+
+ gtk_tree_path_free(path);
+ gdk_drag_status (context, (GdkDragAction)action, time);
+
+ return (action == 0);
+}
+
+/*
+ * Set the tree selection and scroll to the row with the given repr
+ */
+void
+sp_xmlview_tree_set_repr (SPXMLViewTree * tree, Inkscape::XML::Node * repr)
+{
+ if ( tree->repr == repr ) return;
+
+ if (tree->store) {
+ gtk_tree_view_set_model(GTK_TREE_VIEW(tree), nullptr);
+ gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), remove_all_listeners, nullptr);
+ g_object_unref(tree->store);
+ tree->store = nullptr;
+ }
+
+ if (tree->repr) {
+ Inkscape::GC::release(tree->repr);
+ }
+ tree->repr = repr;
+ if (repr) {
+ tree->store = gtk_tree_store_new(STORE_N_COLS, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_STRING);
+
+ Inkscape::GC::anchor(repr);
+ add_node(tree, nullptr, nullptr, repr);
+
+ // Set the tree model here, after all data is inserted
+ gtk_tree_view_set_model (GTK_TREE_VIEW(tree), GTK_TREE_MODEL(tree->store));
+ g_signal_connect(G_OBJECT(tree->store), "row-changed", G_CALLBACK(on_row_changed), tree);
+
+ GtkTreePath *path = gtk_tree_path_new_from_indices(0, -1);
+ gtk_tree_view_expand_to_path (GTK_TREE_VIEW(tree), path);
+ gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW(tree), path, nullptr, true, 0.5, 0.0);
+ gtk_tree_path_free(path);
+ }
+}
+
+/*
+ * Return the node data at a given GtkTreeIter position
+ */
+NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter)
+{
+ NodeData *data = nullptr;
+ gtk_tree_model_get(model, iter, STORE_DATA_COL, &data, -1);
+ return data;
+}
+
+/*
+ * Return the repr at a given GtkTreeIter position
+ */
+Inkscape::XML::Node *
+sp_xmlview_tree_node_get_repr (GtkTreeModel *model, GtkTreeIter * iter)
+{
+ NodeData *data = sp_xmlview_tree_node_get_data(model, iter);
+ return data ? data->repr : nullptr;
+}
+
+struct IterByReprData {
+ const Inkscape::XML::Node *repr; //< in
+ GtkTreeIter *iter; //< out
+};
+
+/*
+ * Find a GtkTreeIter position in the tree by repr
+ * @return True if the node was found
+ */
+gboolean
+sp_xmlview_tree_get_repr_node (SPXMLViewTree * tree, Inkscape::XML::Node * repr, GtkTreeIter *iter)
+{
+ iter->stamp = 0; // invalidate iterator
+ IterByReprData funcdata = { repr, iter };
+ gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), foreach_func, &funcdata);
+ return iter->stamp != 0;
+}
+
+gboolean foreach_func(GtkTreeModel *model, GtkTreePath * /*path*/, GtkTreeIter *iter, gpointer user_data)
+{
+ auto funcdata = static_cast<IterByReprData *>(user_data);
+ if (sp_xmlview_tree_node_get_repr(model, iter) == funcdata->repr) {
+ *funcdata->iter = *iter;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/*
+ * Callback function for string searches in the tree
+ * Return a match on any substring
+ */
+gboolean search_equal_func(GtkTreeModel *model, gint /*column*/, const gchar *key, GtkTreeIter *iter, gpointer /*search_data*/)
+{
+ gchar *text = nullptr;
+ gtk_tree_model_get(model, iter, STORE_TEXT_COL, &text, -1);
+
+ gboolean match = (strstr(text, key) != nullptr);
+
+ g_free(text);
+
+ return !match;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=4:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/widgets/sp-xmlview-tree.h b/src/widgets/sp-xmlview-tree.h
new file mode 100644
index 0000000..b9b2037
--- /dev/null
+++ b/src/widgets/sp-xmlview-tree.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 2002 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_SP_XMLVIEW_TREE_H
+#define SEEN_SP_XMLVIEW_TREE_H
+
+#include <gtk/gtk.h>
+#include <glib.h>
+#include <gtkmm/cellrenderertext.h>
+
+namespace Inkscape::XML {
+class Node;
+}
+
+/**
+ * Specialization of GtkTreeView for the XML editor
+ */
+
+#define SP_TYPE_XMLVIEW_TREE (sp_xmlview_tree_get_type ())
+#define SP_XMLVIEW_TREE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_XMLVIEW_TREE, SPXMLViewTree))
+#define SP_IS_XMLVIEW_TREE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_XMLVIEW_TREE))
+#define SP_XMLVIEW_TREE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_XMLVIEW_TREE))
+
+struct SPXMLViewTree;
+struct SPXMLViewTreeClass;
+namespace Inkscape::UI::Syntax { class XMLFormatter; }
+
+class SPXMLViewTree
+{
+public:
+ GtkTreeView tree;
+ GtkTreeStore *store;
+ Inkscape::XML::Node * repr;
+ gint blocked;
+ Gtk::CellRendererText* renderer;
+ Inkscape::UI::Syntax::XMLFormatter* formatter;
+
+ sigc::connection connectTreeMove(const sigc::slot<void ()> &slot)
+ {
+ return _tree_move->connect(slot);
+ }
+// private: Make private and not-pointer when refactoring to C++
+ sigc::signal<void ()> *_tree_move;
+};
+
+struct SPXMLViewTreeClass
+{
+ GtkTreeViewClass parent_class;
+};
+
+GType sp_xmlview_tree_get_type ();
+GtkWidget * sp_xmlview_tree_new (Inkscape::XML::Node * repr, void * factory, void * data);
+
+#define SP_XMLVIEW_TREE_REPR(tree) (SP_XMLVIEW_TREE (tree)->repr)
+
+void sp_xmlview_tree_set_repr (SPXMLViewTree * tree, Inkscape::XML::Node * repr);
+
+Inkscape::XML::Node * sp_xmlview_tree_node_get_repr (GtkTreeModel *model, GtkTreeIter * node);
+gboolean sp_xmlview_tree_get_repr_node (SPXMLViewTree * tree, Inkscape::XML::Node * repr, GtkTreeIter *node);
+
+
+#endif // !SEEN_SP_XMLVIEW_TREE_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/widgets/spw-utilities.cpp b/src/widgets/spw-utilities.cpp
new file mode 100644
index 0000000..a351223
--- /dev/null
+++ b/src/widgets/spw-utilities.cpp
@@ -0,0 +1,216 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape Widget Utilities
+ *
+ * Authors:
+ * Bryce W. Harrington <brycehar@bryceharrington.org>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 2003 Bryce W. Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/grid.h>
+
+#include "selection.h"
+
+#include "spw-utilities.h"
+
+/**
+ * Creates a label widget with the given text, at the given col, row
+ * position in the table.
+ */
+Gtk::Label * spw_label(Gtk::Grid *table, const gchar *label_text, int col, int row, Gtk::Widget* target)
+{
+ Gtk::Label *label_widget = new Gtk::Label();
+ g_assert(label_widget != nullptr);
+ if (target != nullptr) {
+ label_widget->set_text_with_mnemonic(label_text);
+ label_widget->set_mnemonic_widget(*target);
+ } else {
+ label_widget->set_text(label_text);
+ }
+ label_widget->show();
+
+ label_widget->set_halign(Gtk::ALIGN_START);
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ label_widget->set_margin_start(4);
+ label_widget->set_margin_end(4);
+
+ table->attach(*label_widget, col, row, 1, 1);
+
+ return label_widget;
+}
+
+/**
+ * Creates a horizontal layout manager with 4-pixel spacing between children
+ * and space for 'width' columns.
+ */
+Gtk::Box * spw_hbox(Gtk::Grid * table, int width, int col, int row)
+{
+ /* Create a new hbox with a 4-pixel spacing between children */
+ Gtk::Box *hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+ g_assert(hb != nullptr);
+ hb->show();
+ hb->set_hexpand();
+ hb->set_halign(Gtk::ALIGN_FILL);
+ hb->set_valign(Gtk::ALIGN_CENTER);
+ table->attach(*hb, col, row, width, 1);
+
+ return hb;
+}
+
+/**
+ * Finds the descendant of w which has the data with the given key and returns the data, or NULL if there's none.
+ */
+gpointer sp_search_by_data_recursive(GtkWidget *w, gpointer key)
+{
+ gpointer r = nullptr;
+
+ if (w && G_IS_OBJECT(w)) {
+ r = g_object_get_data(G_OBJECT(w), (gchar *) key);
+ }
+ if (r) return r;
+
+ if (GTK_IS_CONTAINER(w)) {
+ std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(w))->get_children();
+ for (auto i:children) {
+ r = sp_search_by_data_recursive(GTK_WIDGET(i->gobj()), key);
+ if (r) return r;
+ }
+ }
+
+ return nullptr;
+}
+
+/**
+ * Returns a named descendent of parent, which has the given name, or nullptr if there's none.
+ *
+ * \param[in] parent The widget to search
+ * \param[in] name The name of the desired child widget
+ *
+ * \return The specified child widget, or nullptr if it cannot be found
+ */
+Gtk::Widget *
+sp_search_by_name_recursive(Gtk::Widget *parent, const Glib::ustring& name)
+{
+ auto parent_bin = dynamic_cast<Gtk::Bin *>(parent);
+ auto parent_container = dynamic_cast<Gtk::Container *>(parent);
+
+ if (parent && parent->get_name() == name) {
+ return parent;
+ }
+ else if (parent_bin) {
+ auto child = parent_bin->get_child();
+ return sp_search_by_name_recursive(child, name);
+ }
+ else if (parent_container) {
+ auto children = parent_container->get_children();
+
+ for (auto child : children) {
+ auto tmp = sp_search_by_name_recursive(child, name);
+
+ if (tmp) {
+ return tmp;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+/**
+ * Returns the descendant of w which has the given key and value pair, or NULL if there's none.
+ */
+GtkWidget *sp_search_by_value_recursive(GtkWidget *w, gchar *key, gchar *value)
+{
+ gchar *r = nullptr;
+
+ if (w && G_IS_OBJECT(w)) {
+ r = (gchar *) g_object_get_data(G_OBJECT(w), key);
+ }
+ if (r && !strcmp (r, value)) return w;
+
+ if (GTK_IS_CONTAINER(w)) {
+ std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(w))->get_children();
+ for (auto i:children) {
+ GtkWidget *child = sp_search_by_value_recursive(GTK_WIDGET(i->gobj()), key, value);
+ if (child) return child;
+ }
+ }
+
+ return nullptr;
+}
+
+/**
+ * This function traverses a tree of widgets descending into bins and containers.
+ * It stops and returns a pointer to the first child widget for which 'eval' evaluates to true.
+ * If 'eval' never returns true then this function visits all widgets and returns nullptr.
+ *
+ * \param[in] widget The widget to start traversal from - top of the tree
+ * \param[in] eval The callback invoked for each visited widget
+ *
+ * \return The widget for which 'eval' returned true, or nullptr otherwise.
+ * Note: it could be a starting widget too.
+ */
+Gtk::Widget* sp_traverse_widget_tree(Gtk::Widget* widget, const std::function<bool (Gtk::Widget*)>& eval) {
+ if (!widget) return nullptr;
+
+ if (eval(widget)) return widget;
+
+ if (auto bin = dynamic_cast<Gtk::Bin*>(widget)) {
+ return sp_traverse_widget_tree(bin->get_child(), eval);
+ }
+ else if (auto container = dynamic_cast<Gtk::Container*>(widget)) {
+ auto&& children = container->get_children();
+ for (auto child : children) {
+ if (auto found = sp_traverse_widget_tree(child, eval)) {
+ return found;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+/**
+ * This function traverses a tree of widgets searching for first focusable widget.
+ *
+ * \param[in] widget The widget to start traversal from - top of the tree
+ *
+ * \return The first focusable widget or nullptr if none are focusable.
+ */
+Gtk::Widget* sp_find_focusable_widget(Gtk::Widget* widget) {
+ return sp_traverse_widget_tree(widget, [](Gtk::Widget* w) { return w->get_can_focus(); });
+}
+
+
+Glib::ustring sp_get_action_target(Gtk::Widget* widget) {
+ Glib::ustring target;
+
+ if (widget && GTK_IS_ACTIONABLE(widget->gobj())) {
+ auto variant = gtk_actionable_get_action_target_value(GTK_ACTIONABLE(widget->gobj()));
+ auto type = variant ? g_variant_get_type_string(variant) : nullptr;
+ if (type && strcmp(type, "s") == 0) {
+ target = g_variant_get_string(variant, nullptr);
+ }
+ }
+
+ return target;
+}
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/spw-utilities.h b/src/widgets/spw-utilities.h
new file mode 100644
index 0000000..fc37792
--- /dev/null
+++ b/src/widgets/spw-utilities.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __SPW_UTILITIES_H__
+#define __SPW_UTILITIES_H__
+
+/*
+ * Inkscape Widget Utilities
+ *
+ * Author:
+ * Bryce W. Harrington <brycehar@bryceharrington.org>
+ *
+ * Copyright (C) 2003 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/* The following are helper routines for making Inkscape dialog widgets.
+ All are prefixed with spw_, short for inkscape_widget. This is not to
+ be confused with SPWidget, an existing datatype associated with Inkscape::XML::Node/
+ SPObject, that reacts to modification.
+*/
+
+#include <glib.h>
+#include <glibmm/ustring.h>
+#include <gtk/gtk.h>
+#include <functional>
+
+namespace Gtk {
+ class Label;
+ class Grid;
+ class HBox;
+ class Widget;
+}
+
+Gtk::Label * spw_label(Gtk::Grid *table, gchar const *label_text, int col, int row, Gtk::Widget *target);
+Gtk::Box * spw_hbox(Gtk::Grid *table, int width, int col, int row);
+
+gpointer sp_search_by_data_recursive(GtkWidget *w, gpointer data);
+GtkWidget *sp_search_by_value_recursive(GtkWidget *w, gchar *key, gchar *value);
+
+Gtk::Widget * sp_search_by_name_recursive(Gtk::Widget *parent,
+ const Glib::ustring& name);
+
+Gtk::Widget* sp_traverse_widget_tree(Gtk::Widget* widget, const std::function<bool (Gtk::Widget*)>& eval);
+
+Gtk::Widget* sp_find_focusable_widget(Gtk::Widget* widget);
+
+// get string action target, if available
+Glib::ustring sp_get_action_target(Gtk::Widget* widget);
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/style-utils.h b/src/widgets/style-utils.h
new file mode 100644
index 0000000..1557218
--- /dev/null
+++ b/src/widgets/style-utils.h
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Common utility functions for manipulating style.
+ *//*
+ * Authors:
+ * see git history
+ * Shlomi Fish <shlomif@cpan.org>
+ *
+ * Copyright (C) 2016 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_DIALOGS_STYLE_UTILS_H
+#define SEEN_DIALOGS_STYLE_UTILS_H
+
+#include "desktop-style.h"
+
+namespace Inkscape {
+ inline bool is_query_style_updateable(const int style) {
+ return (style == QUERY_STYLE_MULTIPLE_DIFFERENT || style == QUERY_STYLE_NOTHING);
+ }
+} // namespace Inkscape
+
+#endif // SEEN_DIALOGS_STYLE_UTILS_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/toolbox.cpp b/src/widgets/toolbox.cpp
new file mode 100644
index 0000000..3847618
--- /dev/null
+++ b/src/widgets/toolbox.cpp
@@ -0,0 +1,641 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/**
+ * @file
+ * Inkscape toolbar definitions and general utility functions.
+ * Each tool should have its own xxx-toolbar implementation file
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Frank Felfe <innerspace@iname.com>
+ * John Cliff <simarilius@yahoo.com>
+ * David Turner <novalis@gnu.org>
+ * Josh Andler <scislac@scislac.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Tavmjong Bah <tavmjong@free.fr>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ * Jabiertxo Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2003 MenTaLguY
+ * Copyright (C) 1999-2015 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "toolbox.h"
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include "actions/actions-canvas-snapping.h"
+#include "actions/actions-tools.h"
+#include "io/resource.h"
+#include "ui/util.h"
+#include "ui/builder-utils.h"
+#include "ui/widget/style-swatch.h"
+#include "widgets/widget-sizes.h"
+
+#include "ui/toolbar/arc-toolbar.h"
+#include "ui/toolbar/box3d-toolbar.h"
+#include "ui/toolbar/calligraphy-toolbar.h"
+#include "ui/toolbar/connector-toolbar.h"
+#include "ui/toolbar/dropper-toolbar.h"
+#include "ui/toolbar/eraser-toolbar.h"
+#include "ui/toolbar/gradient-toolbar.h"
+#include "ui/toolbar/lpe-toolbar.h"
+#include "ui/toolbar/mesh-toolbar.h"
+#include "ui/toolbar/measure-toolbar.h"
+#include "ui/toolbar/node-toolbar.h"
+#include "ui/toolbar/booleans-toolbar.h"
+#include "ui/toolbar/rect-toolbar.h"
+#include "ui/toolbar/marker-toolbar.h"
+#include "ui/toolbar/page-toolbar.h"
+#include "ui/toolbar/paintbucket-toolbar.h"
+#include "ui/toolbar/pencil-toolbar.h"
+#include "ui/toolbar/select-toolbar.h"
+#include "ui/toolbar/spray-toolbar.h"
+#include "ui/toolbar/spiral-toolbar.h"
+#include "ui/toolbar/star-toolbar.h"
+#include "ui/toolbar/tweak-toolbar.h"
+#include "ui/toolbar/text-toolbar.h"
+#include "ui/toolbar/zoom-toolbar.h"
+
+#include "ui/tools/tool-base.h"
+
+//#define DEBUG_TEXT
+
+using Inkscape::UI::ToolboxFactory;
+using Inkscape::UI::Tools::ToolBase;
+
+using Inkscape::IO::Resource::get_filename;
+using Inkscape::IO::Resource::UIS;
+
+typedef void (*SetupFunction)(GtkWidget *toolbox, SPDesktop *desktop);
+typedef void (*UpdateFunction)(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox);
+
+enum BarId {
+ BAR_TOOL = 0,
+ BAR_AUX,
+ BAR_COMMANDS,
+ BAR_SNAP,
+};
+
+#define BAR_ID_KEY "BarIdValue"
+#define HANDLE_POS_MARK "x-inkscape-pos"
+
+int ToolboxFactory::prefToPixelSize(Glib::ustring const& path) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int size = prefs->getIntLimited(path, 16, 16, 48);
+ return size;
+}
+
+Gtk::IconSize ToolboxFactory::prefToSize_mm(Glib::ustring const &path, int base)
+{
+ static Gtk::IconSize sizeChoices[] = { Gtk::ICON_SIZE_LARGE_TOOLBAR, Gtk::ICON_SIZE_SMALL_TOOLBAR,
+ Gtk::ICON_SIZE_DND, Gtk::ICON_SIZE_DIALOG };
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int index = prefs->getIntLimited(path, base, 0, G_N_ELEMENTS(sizeChoices));
+ return sizeChoices[index];
+}
+
+static struct {
+ gchar const *type_name;
+ Glib::ustring const tool_name;
+ GtkWidget *(*create_func)(SPDesktop *desktop);
+ gchar const *swatch_tip;
+} const aux_toolboxes[] = {
+ // If you change the tool_name for Measure or Text here, change it also in desktop-widget.cpp.
+ // clang-format off
+ { "/tools/select", "Select", Inkscape::UI::Toolbar::SelectToolbar::create, nullptr},
+ { "/tools/nodes", "Node", Inkscape::UI::Toolbar::NodeToolbar::create, nullptr},
+ { "/tools/booleans", "Booleans", Inkscape::UI::Toolbar::BooleansToolbar::create, nullptr},
+ { "/tools/marker", "Marker", Inkscape::UI::Toolbar::MarkerToolbar::create, nullptr},
+ { "/tools/shapes/rect", "Rect", Inkscape::UI::Toolbar::RectToolbar::create, N_("Style of new rectangles")},
+ { "/tools/shapes/arc", "Arc", Inkscape::UI::Toolbar::ArcToolbar::create, N_("Style of new ellipses")},
+ { "/tools/shapes/star", "Star", Inkscape::UI::Toolbar::StarToolbar::create, N_("Style of new stars")},
+ { "/tools/shapes/3dbox", "3DBox", Inkscape::UI::Toolbar::Box3DToolbar::create, N_("Style of new 3D boxes")},
+ { "/tools/shapes/spiral", "Spiral", Inkscape::UI::Toolbar::SpiralToolbar::create, N_("Style of new spirals")},
+ { "/tools/freehand/pencil", "Pencil", Inkscape::UI::Toolbar::PencilToolbar::create_pencil, N_("Style of new paths created by Pencil")},
+ { "/tools/freehand/pen", "Pen", Inkscape::UI::Toolbar::PencilToolbar::create_pen, N_("Style of new paths created by Pen")},
+ { "/tools/calligraphic", "Calligraphic", Inkscape::UI::Toolbar::CalligraphyToolbar::create, N_("Style of new calligraphic strokes")},
+ { "/tools/text", "Text", Inkscape::UI::Toolbar::TextToolbar::create, nullptr},
+ { "/tools/gradient", "Gradient", Inkscape::UI::Toolbar::GradientToolbar::create, nullptr},
+ { "/tools/mesh", "Mesh", Inkscape::UI::Toolbar::MeshToolbar::create, nullptr},
+ { "/tools/zoom", "Zoom", Inkscape::UI::Toolbar::ZoomToolbar::create, nullptr},
+ { "/tools/measure", "Measure", Inkscape::UI::Toolbar::MeasureToolbar::create, nullptr},
+ { "/tools/dropper", "Dropper", Inkscape::UI::Toolbar::DropperToolbar::create, nullptr},
+ { "/tools/tweak", "Tweak", Inkscape::UI::Toolbar::TweakToolbar::create, N_("Color/opacity used for color tweaking")},
+ { "/tools/spray", "Spray", Inkscape::UI::Toolbar::SprayToolbar::create, nullptr},
+ { "/tools/connector", "Connector", Inkscape::UI::Toolbar::ConnectorToolbar::create, nullptr},
+ { "/tools/pages", "Pages", Inkscape::UI::Toolbar::PageToolbar::create, nullptr},
+ { "/tools/paintbucket", "Paintbucket", Inkscape::UI::Toolbar::PaintbucketToolbar::create, N_("Style of Paint Bucket fill objects")},
+ { "/tools/eraser", "Eraser", Inkscape::UI::Toolbar::EraserToolbar::create, _("TBD")},
+ { "/tools/lpetool", "LPETool", Inkscape::UI::Toolbar::LPEToolbar::create, _("TBD")},
+ { nullptr, "", nullptr, nullptr }
+ // clang-format on
+};
+
+
+static void setup_aux_toolbox(GtkWidget *toolbox, SPDesktop *desktop);
+static void update_aux_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox);
+
+static GtkWidget* toolboxNewCommon( GtkWidget* tb, BarId id, GtkPositionType /*handlePos*/ )
+{
+ g_object_set_data(G_OBJECT(tb), "desktop", nullptr);
+
+ gtk_widget_set_sensitive(tb, TRUE);
+
+ GtkWidget *hb = gtk_event_box_new(); // A simple, neutral container.
+ gtk_widget_set_name(hb, "ToolboxCommon");
+
+ gtk_container_add(GTK_CONTAINER(hb), tb);
+ gtk_widget_show(GTK_WIDGET(tb));
+
+ sigc::connection* conn = new sigc::connection;
+ g_object_set_data(G_OBJECT(hb), "event_context_connection", conn);
+
+ gpointer val = GINT_TO_POINTER(id);
+ g_object_set_data(G_OBJECT(hb), BAR_ID_KEY, val);
+
+ return hb;
+}
+
+GtkWidget *ToolboxFactory::createToolToolbox(InkscapeWindow *window)
+{
+ Gtk::Widget* toolbar = nullptr;
+
+ auto builder = Inkscape::UI::create_builder("toolbar-tool.ui");
+ builder->get_widget("tool-toolbar", toolbar);
+ if (!toolbar) {
+ std::cerr << "InkscapeWindow: Failed to load tool toolbar!" << std::endl;
+ }
+
+ _attachHandlers(builder, window);
+
+ return toolboxNewCommon( GTK_WIDGET(toolbar->gobj()), BAR_TOOL, GTK_POS_LEFT );
+}
+
+/**
+ * @brief Create a context menu for a tool button.
+ * @param tool_name The tool name (parameter to the tool-switch action)
+ * @param win The Inkscape window which will display the preferences dialog.
+ */
+Gtk::Menu *ToolboxFactory::_getContextMenu(Glib::ustring tool_name, InkscapeWindow *win)
+{
+ auto menu = new Gtk::Menu();
+ auto gio_menu = Gio::Menu::create();
+ auto action_group = Gio::SimpleActionGroup::create();
+ menu->insert_action_group("ctx", action_group);
+ action_group->add_action("open-tool-preferences", sigc::bind<Glib::ustring, InkscapeWindow *>(
+ sigc::ptr_fun(&tool_preferences), tool_name, win));
+
+ auto menu_item = Gio::MenuItem::create(_("Open tool preferences"), "ctx.open-tool-preferences");
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getInt("/theme/menuIcons", true)) {
+ auto _icon = Gio::Icon::create("preferences-system");
+ menu_item->set_icon(_icon);
+ }
+
+ gio_menu->append_item(menu_item);
+ menu->bind_model(gio_menu, true);
+ menu->show();
+ return menu;
+}
+
+/**
+ * @brief Attach handlers to all tool buttons, so that double-clicking on a tool
+ * in the toolbar opens up that tool's preferences, and a right click opens a
+ * context menu with the same functionality.
+ * @param builder The builder that contains a loaded UI structure containing RadioButton's.
+ * @param win The Inkscape window which will display the preferences dialog.
+ */
+void ToolboxFactory::_attachHandlers(Glib::RefPtr<Gtk::Builder> builder, InkscapeWindow *win)
+{
+ for (auto &object : builder->get_objects()) {
+ if (auto radio = dynamic_cast<Gtk::RadioButton *>(object.get())) {
+
+ Glib::VariantBase action_target;
+ radio->get_property("action-target", action_target);
+ if (!action_target.is_of_type(Glib::VARIANT_TYPE_STRING)) {
+ continue;
+ }
+
+ auto tool_name = Glib::ustring((gchar const *)action_target.get_data());
+
+ auto menu = _getContextMenu(tool_name, win);
+ menu->attach_to_widget(*radio);
+
+ radio->signal_button_press_event().connect([=](GdkEventButton *ev) -> bool {
+ // Open tool preferences upon double click
+ if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) {
+ tool_preferences(tool_name, win);
+ return true;
+ }
+ if (ev->button == 3) {
+ menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(ev));
+ }
+ return false;
+ });
+ }
+ }
+}
+
+GtkWidget *ToolboxFactory::createAuxToolbox()
+{
+ auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(tb, "AuxToolbox");
+ gtk_box_set_homogeneous(GTK_BOX(tb), FALSE);
+
+ return toolboxNewCommon( tb, BAR_AUX, GTK_POS_LEFT );
+}
+
+//####################################
+//# Commands Bar
+//####################################
+
+GtkWidget *ToolboxFactory::createCommandsToolbox()
+{
+ auto tb = new Gtk::Box();
+ tb->set_name("CommandsToolbox");
+ tb->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ tb->set_homogeneous(false);
+
+ Gtk::Toolbar* toolbar = nullptr;
+
+ auto builder = Inkscape::UI::create_builder("toolbar-commands.ui");
+ builder->get_widget("commands-toolbar", toolbar);
+ if (!toolbar) {
+ std::cerr << "ToolboxFactory: Failed to load commands toolbar!" << std::endl;
+ } else {
+ tb->pack_start(*toolbar, false, false);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if ( prefs->getBool("/toolbox/icononly", true) ) {
+ toolbar->set_toolbar_style( Gtk::TOOLBAR_ICONS );
+ }
+ }
+
+ return toolboxNewCommon(GTK_WIDGET(tb->gobj()), BAR_COMMANDS, GTK_POS_LEFT);
+}
+
+int show_popover(void* button) {
+ auto btn = static_cast<Gtk::MenuButton*>(button);
+ btn->get_popover()->show();
+ return false;
+}
+
+class SnapBar : public Gtk::Box {
+public:
+ SnapBar() = default;
+ ~SnapBar() override = default;
+
+ Inkscape::PrefObserver _observer;
+};
+
+GtkWidget *ToolboxFactory::createSnapToolbox()
+{
+ auto tb = new SnapBar();
+ tb->set_name("SnapToolbox");
+ tb->set_homogeneous(false);
+
+ bool simple_snap = true;
+ Gtk::Toolbar* toolbar = nullptr;
+
+ auto builder = Inkscape::UI::create_builder("toolbar-snap.ui");
+ builder->get_widget("snap-toolbar", toolbar);
+ if (!toolbar) {
+ std::cerr << "InkscapeWindow: Failed to load snap toolbar!" << std::endl;
+ } else {
+ tb->pack_start(*toolbar, false, false);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if ( prefs->getBool("/toolbox/icononly", true) ) {
+ toolbar->set_toolbar_style( Gtk::TOOLBAR_ICONS );
+ }
+ simple_snap = prefs->getBool("/toolbox/simplesnap", simple_snap);
+ }
+
+ Gtk::ToolItem* item_simple = nullptr;
+ Gtk::ToolItem* item_advanced = nullptr;
+ Gtk::MenuButton* btn_simple = nullptr;
+ Gtk::MenuButton* btn_advanced = nullptr;
+ Gtk::LinkButton* simple = nullptr;
+ Gtk::LinkButton* advanced = nullptr;
+ builder->get_widget("simple-link", simple);
+ builder->get_widget("advanced-link", advanced);
+ builder->get_widget("tool-item-advanced", item_advanced);
+ builder->get_widget("tool-item-simple", item_simple);
+ builder->get_widget("btn-simple", btn_simple);
+ builder->get_widget("btn-advanced", btn_advanced);
+ if (simple && advanced && item_simple && item_advanced && btn_simple && btn_advanced) {
+ // keep only one popup button visible
+ if (simple_snap) {
+ item_simple->show();
+ item_advanced->hide();
+ }
+ else {
+ item_advanced->show();
+ item_simple->hide();
+ }
+
+ // Watch snap bar preferences;
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+ tb->_observer = prefs->createObserver(ToolboxFactory::snap_bar_simple, [=](const Preferences::Entry& entry) {
+ if (entry.getBool(true)) {
+ item_advanced->hide();
+ item_simple->show();
+ // adjust snapping options when transitioning to simple scheme, since most are hidden
+ transition_to_simple_snapping();
+ }
+ else {
+ item_simple->hide();
+ item_advanced->show();
+ }
+ });
+
+ // switch to simple mode
+ simple->signal_activate_link().connect([=](){
+ g_timeout_add(250, &show_popover, btn_simple);
+ Inkscape::Preferences::get()->setBool(ToolboxFactory::snap_bar_simple, true);
+ return true;
+ }, false);
+
+ // switch to advanced mode
+ advanced->signal_activate_link().connect([=](){
+ g_timeout_add(250, &show_popover, btn_advanced);
+ Inkscape::Preferences::get()->setBool(ToolboxFactory::snap_bar_simple, false);
+ return true;
+ }, false);
+ }
+
+ return toolboxNewCommon(GTK_WIDGET(tb->gobj()), BAR_SNAP, GTK_POS_LEFT);
+}
+
+void ToolboxFactory::setToolboxDesktop(GtkWidget *toolbox, SPDesktop *desktop)
+{
+ sigc::connection *conn = static_cast<sigc::connection*>(g_object_get_data(G_OBJECT(toolbox),
+ "event_context_connection"));
+
+ BarId id = static_cast<BarId>( GPOINTER_TO_INT(g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY)) );
+
+ SetupFunction setup_func = nullptr;
+ UpdateFunction update_func = nullptr;
+
+ switch (id) {
+ case BAR_TOOL:
+ setup_func = nullptr; // setup_tool_toolbox;
+ update_func = nullptr; // update_tool_toolbox;
+ break;
+
+ case BAR_AUX:
+ toolbox = gtk_bin_get_child(GTK_BIN(toolbox));
+ setup_func = setup_aux_toolbox;
+ update_func = update_aux_toolbox;
+ break;
+
+ case BAR_COMMANDS:
+ setup_func = nullptr; // setup_commands_toolbox;
+ update_func = nullptr; // update_commands_toolbox;
+ break;
+
+ case BAR_SNAP:
+ setup_func = nullptr;
+ update_func = nullptr;
+ break;
+ default:
+ g_warning("Unexpected toolbox id encountered.");
+ }
+
+ gpointer ptr = g_object_get_data(G_OBJECT(toolbox), "desktop");
+ SPDesktop *old_desktop = static_cast<SPDesktop*>(ptr);
+
+ if (old_desktop) {
+ std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(toolbox))->get_children();
+ for ( auto i:children ) {
+ gtk_container_remove( GTK_CONTAINER(toolbox), i->gobj() );
+ }
+ }
+
+ g_object_set_data(G_OBJECT(toolbox), "desktop", (gpointer)desktop);
+
+ if (desktop && setup_func && update_func) {
+ gtk_widget_set_sensitive(toolbox, TRUE);
+ setup_func(toolbox, desktop);
+ update_func(desktop, desktop->event_context, toolbox);
+ *conn = desktop->connectEventContextChanged(sigc::bind (sigc::ptr_fun(update_func), toolbox));
+ } else {
+ gtk_widget_set_sensitive(toolbox, TRUE);
+ }
+
+} // end of sp_toolbox_set_desktop()
+
+
+#define noDUMP_DETAILS 1
+
+void ToolboxFactory::setOrientation(GtkWidget* toolbox, GtkOrientation orientation)
+{
+#if DUMP_DETAILS
+ g_message("Set orientation for %p to be %d", toolbox, orientation);
+ GType type = G_OBJECT_TYPE(toolbox);
+ g_message(" [%s]", g_type_name(type));
+ g_message(" %p", g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY));
+#endif
+
+ GtkPositionType pos = (orientation == GTK_ORIENTATION_HORIZONTAL) ? GTK_POS_LEFT : GTK_POS_TOP;
+
+ if (GTK_IS_BIN(toolbox)) {
+#if DUMP_DETAILS
+ g_message(" is a BIN");
+#endif // DUMP_DETAILS
+ GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox));
+ if (child) {
+#if DUMP_DETAILS
+ GType type2 = G_OBJECT_TYPE(child);
+ g_message(" child [%s]", g_type_name(type2));
+#endif // DUMP_DETAILS
+
+ if (GTK_IS_BOX(child)) {
+#if DUMP_DETAILS
+ g_message(" is a BOX");
+#endif // DUMP_DETAILS
+
+ std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(child))->get_children();
+ if (!children.empty()) {
+ for (auto curr:children) {
+ GtkWidget* child2 = curr->gobj();
+#if DUMP_DETAILS
+ GType type3 = G_OBJECT_TYPE(child2);
+ g_message(" child2 [%s]", g_type_name(type3));
+#endif // DUMP_DETAILS
+
+ if (GTK_IS_CONTAINER(child2)) {
+ std::vector<Gtk::Widget*> children2 = Glib::wrap(GTK_CONTAINER(child2))->get_children();
+ if (!children2.empty()) {
+ for (auto curr2:children2) {
+ GtkWidget* child3 = curr2->gobj();
+#if DUMP_DETAILS
+ GType type4 = G_OBJECT_TYPE(child3);
+ g_message(" child3 [%s]", g_type_name(type4));
+#endif // DUMP_DETAILS
+ if (GTK_IS_TOOLBAR(child3)) {
+ GtkToolbar* childBar = GTK_TOOLBAR(child3);
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(childBar), orientation);
+ }
+ }
+ }
+ }
+
+
+ if (GTK_IS_TOOLBAR(child2)) {
+ GtkToolbar* childBar = GTK_TOOLBAR(child2);
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(childBar), orientation);
+ } else {
+ g_message("need to add dynamic switch");
+ }
+ }
+ } else {
+ // The call is being made before the toolbox proper has been setup.
+ g_object_set_data(G_OBJECT(toolbox), HANDLE_POS_MARK, GINT_TO_POINTER(pos));
+ }
+ } else if (GTK_IS_TOOLBAR(child)) {
+ GtkToolbar* toolbar = GTK_TOOLBAR(child);
+ gtk_orientable_set_orientation( GTK_ORIENTABLE(toolbar), orientation );
+ }
+ }
+ }
+}
+
+/**
+ * \brief Generate the auxiliary toolbox
+ *
+ * \details This is the one that appears below the main menu, and contains
+ * tool-specific toolbars. Each toolbar is created here, using
+ * its "create" method.
+ *
+ * The actual method used for each toolbar is specified in the
+ * "aux_toolboxes" array, defined above.
+ */
+void setup_aux_toolbox(GtkWidget *toolbox, SPDesktop *desktop)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // Loop through all the toolboxes and create them using either
+ // their "create" methods.
+ for (int i = 0 ; aux_toolboxes[i].type_name ; i++ ) {
+ if (aux_toolboxes[i].create_func) {
+ GtkWidget *sub_toolbox = aux_toolboxes[i].create_func(desktop);
+ // center items vertically/horizontally to prevent stretching;
+ // all buttons will look uniform across toolbars if their original size is preserved
+ if (auto* tb = dynamic_cast<Gtk::Container*>(Glib::wrap(sub_toolbox))) {
+ for (auto&& item : tb->get_children()) {
+ if (dynamic_cast<Gtk::Button*>(item) ||
+ dynamic_cast<Gtk::SpinButton*>(item) ||
+ dynamic_cast<Gtk::ToolButton*>(item)) {
+ item->set_valign(Gtk::ALIGN_CENTER);
+ item->set_halign(Gtk::ALIGN_CENTER);
+ }
+ }
+ }
+ gtk_widget_set_name( sub_toolbox, "SubToolBox" );
+
+ auto holder = gtk_grid_new();
+ gtk_grid_attach(GTK_GRID(holder), sub_toolbox, 0, 0, 1, 1);
+
+ // This part is just for styling
+ if ( prefs->getBool( "/toolbox/icononly", true) ) {
+ gtk_toolbar_set_style( GTK_TOOLBAR(sub_toolbox), GTK_TOOLBAR_ICONS );
+ }
+
+ int pixel_size = ToolboxFactory::prefToPixelSize(ToolboxFactory::ctrlbars_icon_size);
+ Inkscape::UI::set_icon_sizes(sub_toolbox, pixel_size);
+ gtk_widget_set_hexpand(sub_toolbox, TRUE);
+
+ // Add a swatch widget if swatch tooltip is defined.
+ if ( aux_toolboxes[i].swatch_tip) {
+ auto swatch = new Inkscape::UI::Widget::StyleSwatch( nullptr, _(aux_toolboxes[i].swatch_tip) );
+ swatch->setDesktop( desktop );
+ swatch->setToolName(aux_toolboxes[i].tool_name);
+ // swatch->setClickVerb( aux_toolboxes[i].swatch_verb_id );
+ swatch->setWatchedTool( aux_toolboxes[i].type_name, true );
+ swatch->set_margin_start(AUX_BETWEEN_BUTTON_GROUPS);
+ swatch->set_margin_end(AUX_BETWEEN_BUTTON_GROUPS);
+ swatch->set_margin_top(AUX_SPACING);
+ swatch->set_margin_bottom(AUX_SPACING);
+
+ auto swatch_ = GTK_WIDGET( swatch->gobj() );
+ gtk_grid_attach( GTK_GRID(holder), swatch_, 1, 0, 1, 1);
+ }
+
+ // Add the new toolbar into the toolbox (i.e., make it the visible toolbar)
+ // and also store a pointer to it inside the toolbox. This allows the
+ // active toolbar to be changed.
+ gtk_container_add(GTK_CONTAINER(toolbox), holder);
+ Glib::ustring ui_name = aux_toolboxes[i].tool_name + "Toolbar"; // If you change "Toolbar" here, change it also in desktop-widget.cpp.
+ gtk_widget_set_name( holder, ui_name.c_str() );
+
+ // TODO: We could make the toolbox a custom subclass of GtkEventBox
+ // so that we can store a list of toolbars, rather than using
+ // GObject data
+ g_object_set_data(G_OBJECT(toolbox), aux_toolboxes[i].tool_name.c_str(), holder);
+ gtk_widget_show(sub_toolbox);
+ gtk_widget_show(holder);
+ } else if (aux_toolboxes[i].swatch_tip) {
+ g_warning("Could not create toolbox %s", aux_toolboxes[i].tool_name.c_str());
+ }
+ }
+}
+
+void update_aux_toolbox(SPDesktop * /*desktop*/, ToolBase *eventcontext, GtkWidget *toolbox)
+{
+ for (int i = 0 ; aux_toolboxes[i].type_name ; i++ ) {
+ GtkWidget *sub_toolbox = GTK_WIDGET(g_object_get_data(G_OBJECT(toolbox), aux_toolboxes[i].tool_name.c_str()));
+ if (eventcontext && eventcontext->getPrefsPath() == aux_toolboxes[i].type_name) {
+ gtk_widget_show_now(sub_toolbox);
+ g_object_set_data(G_OBJECT(toolbox), "shows", sub_toolbox);
+ } else {
+ gtk_widget_hide(sub_toolbox);
+ }
+ //FIX issue #Inkscape686
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(sub_toolbox, &allocation);
+ gtk_widget_size_allocate(sub_toolbox, &allocation);
+ }
+ //FIX issue #Inkscape125
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(toolbox, &allocation);
+ gtk_widget_size_allocate(toolbox, &allocation);
+}
+
+void ToolboxFactory::showAuxToolbox(GtkWidget *toolbox_toplevel)
+{
+ gtk_widget_show(toolbox_toplevel);
+ GtkWidget *toolbox = gtk_bin_get_child(GTK_BIN(toolbox_toplevel));
+
+ GtkWidget *shown_toolbox = GTK_WIDGET(g_object_get_data(G_OBJECT(toolbox), "shows"));
+ if (!shown_toolbox) {
+ return;
+ }
+ gtk_widget_show(toolbox);
+}
+
+Glib::ustring ToolboxFactory::get_tool_visible_buttons_path(const Glib::ustring& button_action_name) {
+ return Glib::ustring(ToolboxFactory::tools_visible_buttons) + "/show" + button_action_name;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/toolbox.h b/src/widgets/toolbox.h
new file mode 100644
index 0000000..c95250b
--- /dev/null
+++ b/src/widgets/toolbox.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_TOOLBOX_H
+#define SEEN_TOOLBOX_H
+
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 1999-2002 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+#include <gtk/gtk.h>
+#include <gtkmm.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/enums.h>
+
+#include "preferences.h"
+
+class InkscapeWindow;
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+
+/**
+ * Main toolbox source.
+ */
+class ToolboxFactory
+{
+public:
+ static void setToolboxDesktop(GtkWidget *toolbox, SPDesktop *desktop);
+ static void setOrientation(GtkWidget* toolbox, GtkOrientation orientation);
+ static void showAuxToolbox(GtkWidget* toolbox);
+
+ static GtkWidget *createToolToolbox(InkscapeWindow *window);
+ static GtkWidget *createAuxToolbox();
+ static GtkWidget *createCommandsToolbox();
+ static GtkWidget *createSnapToolbox();
+
+ static int prefToPixelSize(Glib::ustring const& path);
+ static Gtk::IconSize prefToSize_mm(Glib::ustring const &path, int base = 0);
+
+ ToolboxFactory() = delete;
+
+ static constexpr const char* tools_icon_size = "/toolbox/tools/iconsize";
+ static constexpr const char* tools_visible_buttons = "/toolbox/tools/buttons";
+ static constexpr const char* ctrlbars_icon_size = "/toolbox/controlbars/iconsize";
+ static constexpr const char* snap_bar_simple = "/toolbox/simplesnap";
+ static constexpr const int min_pixel_size = 16;
+ static constexpr const int max_pixel_size = 48;
+ static Glib::ustring get_tool_visible_buttons_path(const Glib::ustring& button_action_name);
+
+private:
+ static Gtk::Menu *_getContextMenu(Glib::ustring tool_name, InkscapeWindow *window);
+ static void _attachHandlers(Glib::RefPtr<Gtk::Builder> builder, InkscapeWindow *window);
+};
+
+
+
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* !SEEN_TOOLBOX_H */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/widgets/widget-sizes.h b/src/widgets/widget-sizes.h
new file mode 100644
index 0000000..e70b071
--- /dev/null
+++ b/src/widgets/widget-sizes.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2016 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+// #define TOOL_BUTTON_SIZE 28
+
+// GTK uses 24 for icon sizes by default. Spacing adjust to keep the
+// toolbar the same as other GTK applications. If we want that, use
+// these defines instead:
+//#define AUX_BUTTON_SIZE 24
+//#define AUX_SPACING 2
+
+// #define AUX_BUTTON_SIZE 20
+#define AUX_SPACING 3
+
+#define AUX_BETWEEN_BUTTON_GROUPS 7
+
+#define SPIN_STEP 0.1
+#define SPIN_PAGE_STEP 5.0
+
+#define STATUS_ZOOM_WIDTH 57
+#define STATUS_ROTATION_WIDTH 57
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :