summaryrefslogtreecommitdiffstats
path: root/src/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'src/widgets')
-rw-r--r--src/widgets/CMakeLists.txt45
-rw-r--r--src/widgets/README10
-rw-r--r--src/widgets/desktop-widget.cpp2570
-rw-r--r--src/widgets/desktop-widget.h354
-rw-r--r--src/widgets/ege-paint-def.cpp316
-rw-r--r--src/widgets/ege-paint-def.h112
-rw-r--r--src/widgets/fill-n-stroke-factory.h38
-rw-r--r--src/widgets/fill-style.cpp832
-rw-r--r--src/widgets/fill-style.h39
-rw-r--r--src/widgets/gradient-image.cpp285
-rw-r--r--src/widgets/gradient-image.h65
-rw-r--r--src/widgets/gradient-selector.cpp660
-rw-r--r--src/widgets/gradient-selector.h152
-rw-r--r--src/widgets/gradient-vector.cpp1314
-rw-r--r--src/widgets/gradient-vector.h90
-rw-r--r--src/widgets/ink-action.cpp200
-rw-r--r--src/widgets/ink-action.h61
-rw-r--r--src/widgets/mappings.xml334
-rw-r--r--src/widgets/paint-selector.cpp1601
-rw-r--r--src/widgets/paint-selector.h171
-rw-r--r--src/widgets/sp-attribute-widget.cpp303
-rw-r--r--src/widgets/sp-attribute-widget.h172
-rw-r--r--src/widgets/sp-color-selector.cpp334
-rw-r--r--src/widgets/sp-color-selector.h105
-rw-r--r--src/widgets/sp-xmlview-tree.cpp866
-rw-r--r--src/widgets/sp-xmlview-tree.h64
-rw-r--r--src/widgets/spinbutton-events.cpp163
-rw-r--r--src/widgets/spinbutton-events.h32
-rw-r--r--src/widgets/spw-utilities.cpp160
-rw-r--r--src/widgets/spw-utilities.h48
-rw-r--r--src/widgets/stroke-marker-selector.cpp567
-rw-r--r--src/widgets/stroke-marker-selector.h118
-rw-r--r--src/widgets/stroke-style.cpp1346
-rw-r--r--src/widgets/stroke-style.h227
-rw-r--r--src/widgets/style-utils.h35
-rw-r--r--src/widgets/swatch-selector.cpp168
-rw-r--r--src/widgets/swatch-selector.h68
-rw-r--r--src/widgets/toolbox.cpp842
-rw-r--r--src/widgets/toolbox.h84
-rw-r--r--src/widgets/widget-sizes.h57
40 files changed, 15008 insertions, 0 deletions
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt
new file mode 100644
index 0000000..3ed2a37
--- /dev/null
+++ b/src/widgets/CMakeLists.txt
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set(widgets_SRC
+ desktop-widget.cpp
+ ege-paint-def.cpp
+ fill-style.cpp
+ gradient-image.cpp
+ gradient-selector.cpp
+ gradient-vector.cpp
+ ink-action.cpp
+ paint-selector.cpp
+ sp-attribute-widget.cpp
+ sp-color-selector.cpp
+ sp-xmlview-tree.cpp
+ spinbutton-events.cpp
+ spw-utilities.cpp
+ stroke-marker-selector.cpp
+ stroke-style.cpp
+ swatch-selector.cpp
+ toolbox.cpp
+
+ # -------
+ # Headers
+ desktop-widget.h
+ ege-paint-def.h
+ fill-n-stroke-factory.h
+ fill-style.h
+ gradient-image.h
+ gradient-selector.h
+ gradient-vector.h
+ ink-action.h
+ paint-selector.h
+ sp-attribute-widget.h
+ sp-color-selector.h
+ sp-xmlview-tree.h
+ spinbutton-events.h
+ spw-utilities.h
+ stroke-marker-selector.h
+ stroke-style.h
+ swatch-selector.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..491ce41
--- /dev/null
+++ b/src/widgets/desktop-widget.cpp
@@ -0,0 +1,2570 @@
+// 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 "file.h"
+#include "inkscape-version.h"
+#include "verbs.h"
+
+#include "display/canvas-arena.h"
+#include "display/canvas-axonomgrid.h"
+#include "display/guideline.h"
+#include "display/sp-canvas.h"
+
+#include "extension/db.h"
+
+#include "helper/action.h"
+
+#include "object/sp-image.h"
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+
+#include "ui/dialog/dialog-manager.h"
+#include "ui/dialog/swatches.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/tools/box3d-tool.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/button.h"
+#include "ui/widget/dock.h"
+#include "ui/widget/ink-ruler.h"
+#include "ui/widget/layer-selector.h"
+#include "ui/widget/selected-style.h"
+#include "ui/widget/spin-button-tool-item.h"
+#include "ui/widget/unit-tracker.h"
+
+// TEMP
+#include "ui/desktop/menubar.h"
+
+#include "util/ege-appear-time-tracker.h"
+#include "util/units.h"
+
+// We're in the "widgets" directory, so no need to explicitly prefix these:
+#include "spinbutton-events.h"
+#include "spw-utilities.h"
+#include "toolbox.h"
+#include "widget-sizes.h"
+
+#ifdef GDK_WINDOWING_QUARTZ
+#include <gtkosxapplication.h>
+#endif
+
+using Inkscape::DocumentUndo;
+using Inkscape::UI::Widget::UnitTracker;
+using Inkscape::UI::UXManager;
+using Inkscape::UI::ToolboxFactory;
+using ege::AppearTimeTracker;
+using Inkscape::Util::unit_table;
+
+
+//---------------------------------------------------------------------
+/* SPDesktopWidget */
+
+static void sp_desktop_widget_class_init (SPDesktopWidgetClass *klass);
+
+static void sp_desktop_widget_size_allocate (GtkWidget *widget, GtkAllocation *allocation);
+static void sp_desktop_widget_realize (GtkWidget *widget);
+
+static void sp_desktop_widget_adjustment_value_changed (GtkAdjustment *adj, SPDesktopWidget *dtw);
+
+static gdouble sp_dtw_zoom_value_to_display (gdouble value);
+static gdouble sp_dtw_zoom_display_to_value (gdouble value);
+static void sp_dtw_zoom_menu_handler (SPDesktop *dt, gdouble factor);
+
+SPViewWidgetClass *dtw_parent_class;
+
+class CMSPrefWatcher {
+public:
+ CMSPrefWatcher() :
+ _dpw(*this),
+ _spw(*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);
+ prefs->addObserver(_spw);
+ }
+ 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());
+ _pw._refreshAll();
+ }
+ private:
+ CMSPrefWatcher &_pw;
+ };
+
+ DisplayProfileWatcher _dpw;
+
+ class SoftProofWatcher : public Inkscape::Preferences::Observer {
+ public:
+ SoftProofWatcher(CMSPrefWatcher &pw) : Observer("/options/softproof"), _pw(pw) {}
+ void notify(Inkscape::Preferences::Entry const &) override {
+ _pw._refreshAll();
+ }
+ private:
+ CMSPrefWatcher &_pw;
+ };
+
+ SoftProofWatcher _spw;
+
+ void _refreshAll();
+ void _setCmsSensitive(bool value);
+
+ std::list<SPDesktopWidget*> _widget_list;
+ EgeColorProfTracker *_tracker;
+
+ friend class DisplayProfileWatcher;
+ friend class SoftproofWatcher;
+};
+
+#if defined(HAVE_LIBLCMS2)
+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 );
+}
+#else
+void CMSPrefWatcher::hook(EgeColorProfTracker * /*tracker*/, gint /*monitor*/, CMSPrefWatcher * /*watcher*/)
+{
+}
+#endif // defined(HAVE_LIBLCMS2)
+
+/// @todo Use conditional compilation in saner places. The whole PrefWatcher
+/// object is unnecessary if defined(HAVE_LIBLCMS2) is not defined.
+void CMSPrefWatcher::_refreshAll()
+{
+#if defined(HAVE_LIBLCMS2)
+ for (auto & it : _widget_list) {
+ it->requestCanvasUpdate();
+ }
+#endif // defined(HAVE_LIBLCMS2)
+}
+
+void CMSPrefWatcher::_setCmsSensitive(bool enabled)
+{
+#if defined(HAVE_LIBLCMS2)
+ for ( auto dtw : _widget_list ) {
+ auto cms_adj = dtw->get_cms_adjust();
+ if ( cms_adj->get_sensitive() != enabled ) {
+ dtw->cms_adjust_set_sensitive(enabled);
+ }
+ }
+#else
+ (void) enabled;
+#endif // defined(HAVE_LIBLCMS2)
+}
+
+static CMSPrefWatcher* watcher = nullptr;
+
+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());
+}
+
+Geom::Point
+SPDesktopWidget::window_get_pointer()
+{
+ int x, y;
+ auto window = Glib::wrap(GTK_WIDGET(_canvas))->get_window();
+ auto display = window->get_display();
+ auto seat = display->get_default_seat();
+ auto device = seat->get_pointer();
+ Gdk::ModifierType m;
+ window->get_device_position(device, x, y, m);
+
+ return Geom::Point(x, y);
+}
+
+static GTimer *overallTimer = nullptr;
+
+/**
+ * Registers SPDesktopWidget class and returns its type number.
+ */
+GType SPDesktopWidget::getType()
+{
+ static GType type = 0;
+ if (!type) {
+ GTypeInfo info = {
+ sizeof(SPDesktopWidgetClass),
+ nullptr, // base_init
+ nullptr, // base_finalize
+ (GClassInitFunc)sp_desktop_widget_class_init,
+ nullptr, // class_finalize
+ nullptr, // class_data
+ sizeof(SPDesktopWidget),
+ 0, // n_preallocs
+ (GInstanceInitFunc)SPDesktopWidget::init,
+ nullptr // value_table
+ };
+ type = g_type_register_static(SP_TYPE_VIEW_WIDGET, "SPDesktopWidget", &info, static_cast<GTypeFlags>(0));
+ // Begin a timer to watch for the first desktop to appear on-screen
+ overallTimer = g_timer_new();
+ }
+ return type;
+}
+
+/**
+ * SPDesktopWidget vtable initialization
+ */
+static void
+sp_desktop_widget_class_init (SPDesktopWidgetClass *klass)
+{
+ dtw_parent_class = SP_VIEW_WIDGET_CLASS(g_type_class_peek_parent(klass));
+
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+ object_class->dispose = SPDesktopWidget::dispose;
+
+ widget_class->size_allocate = sp_desktop_widget_size_allocate;
+ widget_class->realize = sp_desktop_widget_realize;
+}
+
+/**
+ * Callback for changes in size of the canvas table (i.e. the container for
+ * the canvas, the rulers etc).
+ *
+ * This adjusts the range of the rulers when the dock container is adjusted
+ * (fixes lp:950552)
+ *
+ * This fix was causing the rulers to be completely redrawn when not needed.
+ * Added check to see if allocation really changed.
+ *
+ *(Question, why is the callback being called when allocation not changed?)
+ */
+void
+SPDesktopWidget::canvas_tbl_size_allocate(Gtk::Allocation& allocation)
+{
+ if (_allocation == allocation) {
+ return;
+ }
+
+ _allocation = allocation;
+ update_rulers();
+}
+
+/**
+ * Callback for SPDesktopWidget object initialization.
+ */
+void SPDesktopWidget::init( SPDesktopWidget *dtw )
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ new (&dtw->modified_connection) sigc::connection();
+
+ dtw->_ruler_clicked = false;
+ dtw->_ruler_dragged = false;
+ dtw->_active_guide = nullptr;
+ dtw->_xp = 0;
+ dtw->_yp = 0;
+ dtw->window = nullptr;
+ dtw->desktop = nullptr;
+ dtw->_interaction_disabled_counter = 0;
+
+ /* Main table */
+ dtw->_vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ dtw->_vbox->set_name("DesktopMainTable");
+ gtk_container_add( GTK_CONTAINER(dtw), GTK_WIDGET(dtw->_vbox->gobj()) );
+
+ /* Status bar */
+ dtw->_statusbar = Gtk::manage(new Gtk::Box());
+ dtw->_statusbar->set_name("DesktopStatusBar");
+ dtw->_vbox->pack_end(*dtw->_statusbar, false, true);
+
+ /* Swatches panel */
+ {
+ dtw->_panels = 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->_vbox->pack_end(*dtw->_hbox, true, true);
+
+ /* Toolboxes */
+ dtw->aux_toolbox = ToolboxFactory::createAuxToolbox();
+ dtw->_vbox->pack_end(*Glib::wrap(dtw->aux_toolbox), false, true);
+
+ dtw->snap_toolbox = ToolboxFactory::createSnapToolbox();
+ ToolboxFactory::setOrientation( dtw->snap_toolbox, GTK_ORIENTATION_VERTICAL );
+ dtw->_hbox->pack_end(*Glib::wrap(dtw->snap_toolbox), false, true);
+
+ dtw->commands_toolbox = ToolboxFactory::createCommandsToolbox();
+ dtw->_vbox->pack_end(*Glib::wrap(dtw->commands_toolbox), false, true);
+
+ dtw->tool_toolbox = ToolboxFactory::createToolToolbox();
+ ToolboxFactory::setOrientation( dtw->tool_toolbox, GTK_ORIENTATION_VERTICAL );
+ dtw->_hbox->pack_start(*Glib::wrap(dtw->tool_toolbox), false, true);
+
+ /* Canvas table wrapper */
+ auto tbl_wrapper = Gtk::manage(new Gtk::Grid()); // Is this widget really needed? No!
+ tbl_wrapper->set_name("CanvasTableWrapper");
+ dtw->_hbox->pack_start(*tbl_wrapper, true, true, 1);
+
+ /* Canvas table */
+ dtw->_canvas_tbl = Gtk::manage(new Gtk::Grid());
+ dtw->_canvas_tbl->set_name("CanvasTable");
+ // Added to table wrapper later either directly or via paned window shared with dock.
+
+
+
+ // Lock guides button
+ dtw->_guides_lock = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU,
+ Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE,
+ nullptr,
+ INKSCAPE_ICON("object-locked"),
+ _("Toggle lock of all guides in the document")));
+
+ auto guides_lock_style_provider = Gtk::CssProvider::create();
+ guides_lock_style_provider->load_from_data("GtkWidget { padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; }");
+ dtw->_guides_lock->set_name("LockGuides");
+ auto context = dtw->_guides_lock->get_style_context();
+ context->add_provider(guides_lock_style_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ dtw->_guides_lock->signal_toggled().connect(sigc::mem_fun(dtw, &SPDesktopWidget::update_guides_lock));
+ dtw->_canvas_tbl->attach(*dtw->_guides_lock, 0, 0, 1, 1);
+
+ /* Rulers */
+ Inkscape::Util::Unit const *pt = unit_table.getUnit("pt");
+
+ /* Horizontal ruler */
+ dtw->_hruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_HORIZONTAL));
+ dtw->_hruler->set_unit(pt);
+
+ // We should probably get rid of this and attach the signals directly rulers.
+ dtw->_hruler_box = Gtk::manage(new Gtk::EventBox());
+ dtw->_hruler_box->set_tooltip_text(gettext(pt->name_plural.c_str()));
+ dtw->_hruler_box->add(*dtw->_hruler);
+ dtw->_hruler_box->signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_press_event), dtw->_hruler_box, true));
+ dtw->_hruler_box->signal_button_release_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_release_event), dtw->_hruler_box, true));
+ dtw->_hruler_box->signal_motion_notify_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), dtw->_hruler_box, true));
+
+ dtw->_canvas_tbl->attach(*dtw->_hruler_box, 1, 0, 1, 1);
+
+ /* Vertical ruler */
+ dtw->_vruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_VERTICAL));
+ dtw->_vruler->set_unit(pt);
+
+ dtw->_vruler_box = Gtk::manage(new Gtk::EventBox());
+ dtw->_vruler_box->set_tooltip_text(gettext(pt->name_plural.c_str()));
+ dtw->_vruler_box->add(*dtw->_vruler);
+ dtw->_vruler_box->signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_press_event), dtw->_vruler_box, false));
+ dtw->_vruler_box->signal_button_release_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_release_event), dtw->_vruler_box, false));
+ dtw->_vruler_box->signal_motion_notify_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), dtw->_vruler_box, false));
+
+ dtw->_canvas_tbl->attach(*dtw->_vruler_box, 0, 1, 1, 1);
+
+ // Horizontal scrollbar
+ dtw->_hadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0);
+ dtw->_hscrollbar = Gtk::manage(new Gtk::Scrollbar(dtw->_hadj));
+ dtw->_hscrollbar->set_name("HorizontalScrollbar");
+ dtw->_canvas_tbl->attach(*dtw->_hscrollbar, 1, 2, 1, 1);
+
+ // By packing the sticky zoom button and vertical scrollbar in a box it allows the canvas to
+ // expand fully to the top if the rulers are hidden.
+ // (Otherwise, the canvas is pushed down by the height of the sticky zoom button.)
+
+ // Vertical Scrollbar box
+ dtw->_vscrollbar_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ dtw->_canvas_tbl->attach(*dtw->_vscrollbar_box, 2, 0, 1, 2);
+
+ // Sticky zoom button
+ dtw->_sticky_zoom = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU,
+ Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE,
+ nullptr,
+ INKSCAPE_ICON("zoom-original"),
+ _("Zoom drawing if window size changes")));
+ dtw->_sticky_zoom->set_name("StickyZoom");
+ dtw->_sticky_zoom->set_active(prefs->getBool("/options/stickyzoom/value"));
+ dtw->_sticky_zoom->signal_toggled().connect(sigc::mem_fun(dtw, &SPDesktopWidget::sticky_zoom_toggled));
+ dtw->_vscrollbar_box->pack_start(*dtw->_sticky_zoom, false, false);
+
+ // Vertical scrollbar
+ dtw->_vadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0);
+ dtw->_vscrollbar = Gtk::manage(new Gtk::Scrollbar(dtw->_vadj, Gtk::ORIENTATION_VERTICAL));
+ dtw->_vscrollbar->set_name("VerticalScrollbar");
+ dtw->_vscrollbar_box->pack_start(*dtw->_vscrollbar, true, true, 0);
+
+ gchar const* tip = "";
+ Inkscape::Verb* verb = Inkscape::Verb::get( SP_VERB_VIEW_CMS_TOGGLE );
+ if ( verb ) {
+ SPAction *act = verb->get_action( Inkscape::ActionContext( dtw->viewwidget.view ) );
+ if ( act && act->tip ) {
+ tip = act->tip;
+ }
+ }
+ dtw->_cms_adjust = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU,
+ Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE,
+ nullptr,
+ INKSCAPE_ICON("color-management"),
+ tip ));
+ dtw->_cms_adjust->set_name("CMS_Adjust");
+
+#if defined(HAVE_LIBLCMS2)
+ {
+ Glib::ustring current = prefs->getString("/options/displayprofile/uri");
+ bool enabled = current.length() > 0;
+ dtw->cms_adjust_set_sensitive(enabled );
+ if ( enabled ) {
+ bool active = prefs->getBool("/options/displayprofile/enable");
+ if ( active ) {
+ dtw->_cms_adjust->toggle_set_down(true);
+ }
+ }
+ }
+ g_signal_connect_after( G_OBJECT(dtw->_cms_adjust->gobj()), "clicked", G_CALLBACK(SPDesktopWidget::cms_adjust_toggled), dtw );
+#else
+ dtw->cms_adjust_set_sensitive(false);
+#endif // defined(HAVE_LIBLCMS2)
+
+ dtw->_canvas_tbl->attach(*dtw->_cms_adjust, 2, 2, 1, 1);
+ {
+ if (!watcher) {
+ watcher = new CMSPrefWatcher();
+ }
+ watcher->add(dtw);
+ }
+ /* Canvas */
+ dtw->_canvas = SP_CANVAS(SPCanvas::createAA());
+#if defined(HAVE_LIBLCMS2)
+ dtw->_canvas->_enable_cms_display_adj = prefs->getBool("/options/displayprofile/enable");
+#endif // defined(HAVE_LIBLCMS2)
+ gtk_widget_set_can_focus (GTK_WIDGET (dtw->_canvas), TRUE);
+
+ dtw->_hruler->add_track_widget(*Glib::wrap(GTK_WIDGET(dtw->_canvas)));
+ dtw->_vruler->add_track_widget(*Glib::wrap(GTK_WIDGET(dtw->_canvas)));
+
+ auto css_provider = gtk_css_provider_new();
+ auto style_context = gtk_widget_get_style_context(GTK_WIDGET(dtw->_canvas));
+
+ gtk_css_provider_load_from_data(css_provider,
+ "SPCanvas {\n"
+ " background-color: white;\n"
+ "}\n",
+ -1, nullptr);
+
+ gtk_style_context_add_provider(style_context,
+ GTK_STYLE_PROVIDER(css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+ g_signal_connect(G_OBJECT(dtw->_canvas), "event", G_CALLBACK(SPDesktopWidget::event), dtw);
+
+ gtk_widget_set_hexpand(GTK_WIDGET(dtw->_canvas), TRUE);
+ gtk_widget_set_vexpand(GTK_WIDGET(dtw->_canvas), TRUE);
+ dtw->_canvas_tbl->attach(*Glib::wrap(GTK_WIDGET(dtw->_canvas)), 1, 1, 1, 1);
+
+ /* Dock */
+ bool create_dock =
+ prefs->getIntLimited("/options/dialogtype/value", Inkscape::UI::Dialog::FLOATING, 0, 1) ==
+ Inkscape::UI::Dialog::DOCK;
+
+ if (create_dock) {
+ dtw->_dock = new Inkscape::UI::Widget::Dock();
+ auto paned = new Gtk::Paned();
+ paned->set_name("Canvas_and_Dock");
+
+ paned->pack1(*dtw->_canvas_tbl);
+ paned->pack2(dtw->_dock->getWidget(), Gtk::FILL);
+
+ /* Prevent the paned from catching F6 and F8 by unsetting the default callbacks */
+ if (GtkPanedClass *paned_class = GTK_PANED_CLASS (G_OBJECT_GET_CLASS (paned->gobj()))) {
+ paned_class->cycle_child_focus = nullptr;
+ paned_class->cycle_handle_focus = nullptr;
+ }
+
+ paned->set_hexpand(true);
+ paned->set_vexpand(true);
+ tbl_wrapper->attach(*paned, 1, 1, 1, 1);
+ } else {
+ dtw->_canvas_tbl->set_hexpand(true);
+ dtw->_canvas_tbl->set_vexpand(true);
+ tbl_wrapper->attach(*(dtw->_canvas_tbl), 1, 1, 1, 1);
+ }
+
+ // connect scrollbar signals
+ dtw->_hadj->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::on_adjustment_value_changed));
+ dtw->_vadj->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::on_adjustment_value_changed));
+
+ // --------------- Status Tool Bar ------------------//
+
+ // Selected Style (Fill/Stroke/Opacity)
+ dtw->_selected_style = new Inkscape::UI::Widget::SelectedStyle(true);
+ dtw->_statusbar->pack_start(*dtw->_selected_style, false, false);
+
+ // Separator
+ dtw->_statusbar->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)),
+ false, false);
+
+ // Layer Selector
+ dtw->layer_selector = new Inkscape::UI::Widget::LayerSelector(nullptr);
+ // FIXME: need to unreference on container destruction to avoid leak
+ dtw->layer_selector->reference();
+ dtw->_statusbar->pack_start(*dtw->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);
+
+
+ // 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 Gtk::SpinButton(zoom_adj));
+
+ dtw->_zoom_status->set_data("dtw", 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));
+ g_signal_connect (G_OBJECT (dtw->_zoom_status->gobj()), "focus-in-event", G_CALLBACK (spinbutton_focus_in), dtw->_zoom_status->gobj());
+ g_signal_connect (G_OBJECT (dtw->_zoom_status->gobj()), "key-press-event", G_CALLBACK (spinbutton_keypress), dtw->_zoom_status->gobj());
+ 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);
+
+ // Rotate status spinbutton ---------------
+ auto rotation_adj = Gtk::Adjustment::create(0, -360.0, 360.0, 1.0);
+ dtw->_rotation_status = Gtk::manage(new Gtk::SpinButton(rotation_adj));
+ dtw->_rotation_status->set_data("dtw", 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_input_connection = dtw->_rotation_status->signal_input().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_input));
+ dtw->_rotation_status_output_connection = dtw->_rotation_status->signal_output().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_output));
+ g_signal_connect (G_OBJECT (dtw->_rotation_status->gobj()), "focus-in-event", G_CALLBACK (spinbutton_focus_in), dtw->_rotation_status->gobj());
+ g_signal_connect (G_OBJECT (dtw->_rotation_status->gobj()), "key-press-event", G_CALLBACK (spinbutton_keypress), dtw->_rotation_status->gobj());
+ 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 ");
+
+ auto label_z = Gtk::manage(new Gtk::Label(_("Z:")));
+ label_z->set_name("ZLabel");
+ auto label_r = Gtk::manage(new Gtk::Label(_("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->attach(*label_z, 3, 0, 1, 2);
+ dtw->_coord_status->attach(*dtw->_zoom_status, 4, 0, 1, 2);
+
+ dtw->_coord_status->attach(*label_r, 5, 0, 1, 2);
+ dtw->_coord_status->attach(*dtw->_rotation_status, 6, 0, 1, 2);
+
+ dtw->_statusbar->pack_end(*dtw->_coord_status, false, false);
+
+ // --------------- Color Management ---------------- //
+ dtw->_tracker = ege_color_prof_tracker_new(GTK_WIDGET(dtw->layer_selector->gobj()));
+#if defined(HAVE_LIBLCMS2)
+ bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display");
+ if ( fromDisplay ) {
+ Glib::ustring id = Inkscape::CMSSystem::getDisplayId( 0 );
+
+ bool enabled = false;
+ dtw->_canvas->_cms_key = id;
+ enabled = !dtw->_canvas->_cms_key.empty();
+ dtw->cms_adjust_set_sensitive(enabled);
+ }
+ g_signal_connect( G_OBJECT(dtw->_tracker), "changed", G_CALLBACK(SPDesktopWidget::color_profile_event), dtw );
+#endif // defined(HAVE_LIBLCMS2)
+
+ // ------------------ Finish Up -------------------- //
+ dtw->_vbox->show_all();
+
+ gtk_widget_grab_focus (GTK_WIDGET(dtw->_canvas));
+
+ // If this is the first desktop created, report the time it takes to show up
+ if ( overallTimer ) {
+ if ( prefs->getBool("/dialogs/debug/trackAppear", false) ) {
+ // Time tracker takes ownership of the timer.
+ AppearTimeTracker *tracker = new AppearTimeTracker(overallTimer, GTK_WIDGET(dtw), "first SPDesktopWidget");
+ tracker->setAutodelete(true);
+ } else {
+ g_timer_destroy(overallTimer);
+ }
+ overallTimer = nullptr;
+ }
+ // Ensure that ruler ranges are updated correctly whenever the canvas table
+ // is resized
+ dtw->_canvas_tbl_size_allocate_connection = dtw->_canvas_tbl->signal_size_allocate().connect(sigc::mem_fun(dtw, &SPDesktopWidget::canvas_tbl_size_allocate));
+}
+
+/**
+ * Called before SPDesktopWidget destruction.
+ */
+void
+SPDesktopWidget::dispose(GObject *object)
+{
+ SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (object);
+
+ if (dtw == nullptr) {
+ return;
+ }
+
+ UXManager::getInstance()->delTrack(dtw);
+
+ if (dtw->desktop) {
+ if ( watcher ) {
+ watcher->remove(dtw);
+ }
+
+ // Zoom
+ dtw->_zoom_status_input_connection.disconnect();
+ dtw->_zoom_status_output_connection.disconnect();
+ g_signal_handlers_disconnect_matched (G_OBJECT (dtw->_zoom_status->gobj()), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, dtw->_zoom_status);
+ 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_matched (G_OBJECT (dtw->_rotation_status->gobj()), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, dtw->_rotation_status);
+ dtw->_rotation_status_value_changed_connection.disconnect();
+ dtw->_rotation_status_populate_popup_connection.disconnect();
+
+ // Canvas
+ g_signal_handlers_disconnect_by_func (G_OBJECT (dtw->_canvas), (gpointer) G_CALLBACK (SPDesktopWidget::event), dtw);
+ dtw->_canvas_tbl_size_allocate_connection.disconnect();
+
+ dtw->layer_selector->setDesktop(nullptr);
+ dtw->layer_selector->unreference();
+ 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;
+ }
+
+ dtw->modified_connection.~connection();
+
+ if (G_OBJECT_CLASS (dtw_parent_class)->dispose) {
+ (* G_OBJECT_CLASS (dtw_parent_class)->dispose) (object);
+ }
+}
+
+/**
+ * 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();
+
+ std::string Name;
+ if (doc->isModifiedSinceSave()) {
+ Name += "*";
+ }
+
+ Name += uri;
+
+ if (desktop->number > 1) {
+ Name += ": ";
+ Name += std::to_string(desktop->number);
+ }
+ Name += " (";
+
+ if (desktop->getMode() == Inkscape::RENDERMODE_OUTLINE) {
+ Name += N_("outline");
+ } else if (desktop->getMode() == Inkscape::RENDERMODE_NO_FILTERS) {
+ Name += N_("no filters");
+ } else if (desktop->getMode() == Inkscape::RENDERMODE_VISIBLE_HAIRLINES) {
+ Name += N_("visible hairlines");
+ }
+
+ if (desktop->getColorMode() != Inkscape::COLORMODE_NORMAL &&
+ desktop->getMode() != Inkscape::RENDERMODE_NORMAL) {
+ Name += ", ";
+ }
+
+ if (desktop->getColorMode() == Inkscape::COLORMODE_GRAYSCALE) {
+ Name += N_("grayscale");
+ } else if (desktop->getColorMode() == 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);
+ }
+}
+
+Inkscape::UI::Widget::Dock*
+SPDesktopWidget::getDock()
+{
+ return _dock;
+}
+
+/**
+ * Callback to allocate space for desktop widget.
+ */
+static void
+sp_desktop_widget_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
+{
+ SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (widget);
+ GtkAllocation widg_allocation;
+ gtk_widget_get_allocation(widget, &widg_allocation);
+
+ if ((allocation->x == widg_allocation.x) &&
+ (allocation->y == widg_allocation.y) &&
+ (allocation->width == widg_allocation.width) &&
+ (allocation->height == widg_allocation.height)) {
+ if (GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate)
+ GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate (widget, allocation);
+ return;
+ }
+
+ if (gtk_widget_get_realized (widget)) {
+ Geom::Rect const area = dtw->desktop->get_display_area();
+ Geom::Rect const d_canvas = dtw->desktop->getCanvas()->getViewbox();
+ Geom::Point midpoint = dtw->desktop->w2d(d_canvas.midpoint());
+
+ double zoom = dtw->desktop->current_zoom();
+
+ if (GTK_WIDGET_CLASS(dtw_parent_class)->size_allocate) {
+ GTK_WIDGET_CLASS(dtw_parent_class)->size_allocate (widget, allocation);
+ }
+
+ if (dtw->get_sticky_zoom_active()) {
+ /* Find new visible area */
+ Geom::Rect newarea = dtw->desktop->get_display_area();
+ /* Calculate adjusted zoom */
+ double oldshortside = MIN( area.width(), area.height());
+ double newshortside = MIN(newarea.width(), newarea.height());
+ zoom *= newshortside / oldshortside;
+ }
+ dtw->desktop->zoom_absolute_center_point (midpoint, zoom);
+
+ // TODO - Should call show_dialogs() from sp_namedview_window_from_document only.
+ // But delaying the call to here solves dock sizing issues on OS X, (see #171579)
+ dtw->desktop->show_dialogs();
+
+ } else {
+ if (GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate) {
+ GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate (widget, allocation);
+ }
+// this->size_allocate (widget, allocation);
+ }
+}
+
+#ifdef GDK_WINDOWING_QUARTZ
+static GtkMenuItem *_get_help_menu(GtkMenuShell *menu)
+{
+ // Assume "Help" is the last child in menu
+ GtkMenuItem *last = nullptr;
+ auto callback = [](GtkWidget *widget, gpointer data) {
+ *static_cast<GtkMenuItem **>(data) = GTK_MENU_ITEM(widget);
+ };
+ gtk_container_foreach(GTK_CONTAINER(menu), callback, &last);
+ return last;
+}
+#endif
+
+/**
+ * Callback to realize desktop widget.
+ */
+static void
+sp_desktop_widget_realize (GtkWidget *widget)
+{
+
+ SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (widget);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (GTK_WIDGET_CLASS (dtw_parent_class)->realize)
+ (* GTK_WIDGET_CLASS (dtw_parent_class)->realize) (widget);
+
+ 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();
+ gchar *gtkThemeName;
+ gboolean gtkApplicationPreferDarkTheme;
+ GtkSettings *settings = gtk_settings_get_default();
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ if (settings && window) {
+ g_object_get(settings, "gtk-theme-name", &gtkThemeName, NULL);
+ g_object_get(settings, "gtk-application-prefer-dark-theme", &gtkApplicationPreferDarkTheme, NULL);
+ bool dark = Glib::ustring(gtkThemeName).find(":dark") != std::string::npos;
+ if (!dark) {
+ Glib::RefPtr<Gtk::StyleContext> stylecontext = window->get_style_context();
+ Gdk::RGBA rgba;
+ bool background_set = stylecontext->lookup_color("theme_bg_color", rgba);
+ if (background_set && (0.299 * rgba.get_red() + 0.587 * rgba.get_green() + 0.114 * rgba.get_blue()) < 0.5) {
+ dark = true;
+ }
+ }
+ if (dark) {
+ window->get_style_context()->add_class("dark");
+ window->get_style_context()->remove_class("bright");
+ } else {
+ window->get_style_context()->add_class("bright");
+ window->get_style_context()->remove_class("dark");
+ }
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ window->get_style_context()->add_class("symbolic");
+ window->get_style_context()->remove_class("regular");
+ } else {
+ window->get_style_context()->add_class("regular");
+ window->get_style_context()->remove_class("symbolic");
+ }
+ INKSCAPE.signal_change_theme.emit();
+ }
+
+#ifdef GDK_WINDOWING_QUARTZ
+ // native macOS menu
+ auto osxapp = gtkosx_application_get();
+ auto menushell = static_cast<Gtk::MenuShell *>(dtw->menubar());
+ if (osxapp && menushell && window) {
+ menushell->set_parent(*window);
+ gtkosx_application_set_menu_bar(osxapp, menushell->gobj());
+ // using quartz accelerators gives menu shortcuts priority over everything else,
+ // messes up text input because Inkscape has single key shortcuts (e.g. 1-6).
+ gtkosx_application_set_use_quartz_accelerators(osxapp, false);
+ gtkosx_application_set_help_menu(osxapp, _get_help_menu(menushell->gobj()));
+
+ // Window menu disabled because hidden windows which are brought back
+ // from the menu are non-functional.
+ // https://gitlab.com/inkscape/inkscape/-/issues/1105
+#if 0
+ gtkosx_application_set_window_menu(osxapp, nullptr);
+#endif
+
+ // move some items to "Inkscape" menu
+ unsigned app_menu_verbs[] = {
+ SP_VERB_NONE,
+ SP_VERB_DIALOG_INPUT,
+ SP_VERB_DIALOG_DISPLAY,
+ SP_VERB_NONE,
+ SP_VERB_HELP_ABOUT,
+ };
+ for (auto verb : app_menu_verbs) {
+ GtkWidget *menuitem = nullptr;
+ if (verb == SP_VERB_NONE) {
+ menuitem = gtk_separator_menu_item_new();
+ } else if (auto item = get_menu_item_for_verb(verb, dtw->desktop)) {
+ menuitem = static_cast<Gtk::Widget *>(item)->gobj();
+ } else {
+ continue;
+ }
+ // Don't use index 0 because it appends the app name. Index 1
+ // seems to work perfectly with inserting items in reverse order.
+ gtkosx_application_insert_app_menu_item(osxapp, menuitem, 1);
+ }
+ }
+#endif
+}
+
+/* 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
+ gtk_widget_grab_focus (GTK_WIDGET(dtw->_canvas));
+ }
+
+ if ((event->type == GDK_BUTTON_PRESS) && (event->button.button == 3)) {
+ if (event->button.state & GDK_SHIFT_MASK) {
+ sp_canvas_arena_set_sticky (SP_CANVAS_ARENA (dtw->desktop->drawing), TRUE);
+ } else {
+ sp_canvas_arena_set_sticky (SP_CANVAS_ARENA (dtw->desktop->drawing), FALSE);
+ }
+ }
+
+ if (GTK_WIDGET_CLASS (dtw_parent_class)->event) {
+ return (* GTK_WIDGET_CLASS (dtw_parent_class)->event) (widget, event);
+ } else {
+ // 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->_current_item) {
+ return sp_desktop_root_handler (nullptr, event, dtw->desktop);
+ }
+ }
+
+ return FALSE;
+}
+
+#if defined(HAVE_LIBLCMS2)
+void
+SPDesktopWidget::color_profile_event(EgeColorProfTracker */*tracker*/, SPDesktopWidget *dtw)
+{
+ // Handle profile changes
+ GdkScreen* screen = gtk_widget_get_screen(GTK_WIDGET(dtw));
+ GdkWindow *window = gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(dtw)));
+
+ // 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 );
+ bool enabled = false;
+ dtw->_canvas->_cms_key = id;
+ dtw->requestCanvasUpdate();
+ enabled = !dtw->_canvas->_cms_key.empty();
+ dtw->cms_adjust_set_sensitive(enabled);
+}
+#else // defined(HAVE_LIBLCMS2)
+void sp_dtw_color_profile_event(EgeColorProfTracker */*tracker*/, SPDesktopWidget * /*dtw*/)
+{
+}
+#endif // defined(HAVE_LIBLCMS2)
+
+void
+SPDesktopWidget::update_guides_lock()
+{
+ bool down = _guides_lock->get_active();
+
+ auto doc = desktop->getDocument();
+ auto nv = desktop->getNamedView();
+ auto repr = nv->getRepr();
+
+ if ( down != nv->lockguides ) {
+ nv->lockguides = down;
+ sp_namedview_guides_toggle_lock(doc, nv);
+ if (down) {
+ setMessage (Inkscape::NORMAL_MESSAGE, _("Locked all guides"));
+ } else {
+ setMessage (Inkscape::NORMAL_MESSAGE, _("Unlocked all guides"));
+ }
+ }
+}
+
+#if defined(HAVE_LIBLCMS2)
+void
+SPDesktopWidget::cms_adjust_toggled( GtkWidget */*button*/, gpointer data )
+{
+ SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(data);
+
+ bool down = dtw->_cms_adjust->get_active();
+ if ( down != dtw->_canvas->_enable_cms_display_adj ) {
+ dtw->_canvas->_enable_cms_display_adj = down;
+ dtw->desktop->redrawDesktop();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/displayprofile/enable", down);
+ if (down) {
+ dtw->setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>enabled</b> in this window"));
+ } else {
+ dtw->setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>disabled</b> in this window"));
+ }
+ }
+}
+#endif // defined(HAVE_LIBLCMS2)
+
+void
+SPDesktopWidget::cms_adjust_set_sensitive(bool enabled)
+{
+ Inkscape::Verb* verb = Inkscape::Verb::get( SP_VERB_VIEW_CMS_TOGGLE );
+ if ( verb ) {
+ SPAction *act = verb->get_action( Inkscape::ActionContext(viewwidget.view) );
+ if ( act ) {
+ sp_action_set_sensitive( act, enabled );
+ }
+ }
+ _cms_adjust->set_sensitive(enabled);
+}
+
+void
+sp_dtw_desktop_activate (SPDesktopWidget */*dtw*/)
+{
+ /* update active desktop indicator */
+}
+
+void
+sp_dtw_desktop_deactivate (SPDesktopWidget */*dtw*/)
+{
+ /* update inactive desktop indicator */
+}
+
+/**
+ * Shuts down the desktop object for the view being closed. It checks
+ * to see if the document has been edited, and if so prompts the user
+ * to save, discard, or cancel. Returns TRUE if the shutdown operation
+ * is cancelled or if the save is cancelled or fails, FALSE otherwise.
+ */
+bool
+SPDesktopWidget::shutdown()
+{
+ g_assert(desktop != nullptr);
+
+ if (INKSCAPE.sole_desktop_for_document(*desktop)) {
+ SPDocument *doc = desktop->doc();
+ if (doc->isModifiedSinceSave()) {
+ Gtk::Window *toplevel_window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this))));
+ Glib::ustring message = g_markup_printf_escaped(
+ _("<span weight=\"bold\" size=\"larger\">Save changes to document \"%s\" before closing?</span>\n\n"
+ "If you close without saving, your changes will be discarded."),
+ doc->getDocumentName());
+ Gtk::MessageDialog dialog = Gtk::MessageDialog(*toplevel_window, message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE);
+ dialog.property_destroy_with_parent() = true;
+
+ // fix for bug lp:168809
+ Gtk::Container *ma = dialog.get_message_area();
+ std::vector<Gtk::Widget*> ma_labels = ma->get_children();
+ ma_labels[0]->set_can_focus(false);
+
+ Gtk::Button close_button(_("Close _without saving"), true);
+ close_button.show();
+ dialog.add_action_widget(close_button, Gtk::RESPONSE_NO);
+
+ dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ dialog.add_button(_("_Save"), Gtk::RESPONSE_YES);
+ dialog.set_default_response(Gtk::RESPONSE_YES);
+
+ gint response = dialog.run();
+
+ switch (response) {
+ case GTK_RESPONSE_YES:
+ {
+ doc->doRef();
+ sp_namedview_document_from_window(desktop);
+ if (sp_file_save_document(*window, doc)) {
+ doc->doUnref();
+ } else { // save dialog cancelled or save failed
+ doc->doUnref();
+ return TRUE;
+ }
+
+ break;
+ }
+ case GTK_RESPONSE_NO:
+ break;
+ default: // cancel pressed, or dialog was closed
+ return TRUE;
+ break;
+ }
+ }
+ /* Code to check data loss */
+ bool allow_data_loss = FALSE;
+ while (doc->getReprRoot()->attribute("inkscape:dataloss") != nullptr && allow_data_loss == FALSE) {
+ Gtk::Window *toplevel_window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this))));
+ Glib::ustring message = g_markup_printf_escaped(
+ _("<span weight=\"bold\" size=\"larger\">The file \"%s\" was saved with a format that may cause data loss!</span>\n\n"
+ "Do you want to save this file as Inkscape SVG?"),
+ doc->getDocumentName() ? doc->getDocumentName() : "Unnamed");
+ Gtk::MessageDialog dialog = Gtk::MessageDialog(*toplevel_window, message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE);
+ dialog.property_destroy_with_parent() = true;
+
+ // fix for bug lp:168809
+ Gtk::Container *ma = dialog.get_message_area();
+ std::vector<Gtk::Widget*> ma_labels = ma->get_children();
+ ma_labels[0]->set_can_focus(false);
+
+ Gtk::Button close_button(_("Close _without saving"), true);
+ close_button.show();
+ dialog.add_action_widget(close_button, Gtk::RESPONSE_NO);
+
+ dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+
+ Gtk::Button save_button(_("_Save as Inkscape SVG"), true);
+ save_button.set_can_default(true);
+ save_button.show();
+ dialog.add_action_widget(save_button, Gtk::RESPONSE_YES);
+ dialog.set_default_response(Gtk::RESPONSE_YES);
+
+ gint response = dialog.run();
+
+ switch (response) {
+ case GTK_RESPONSE_YES:
+ {
+ doc->doRef();
+
+ if (sp_file_save_dialog(*window, doc, Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG)) {
+ doc->doUnref();
+ } else { // save dialog cancelled or save failed
+ doc->doUnref();
+ return TRUE;
+ }
+
+ break;
+ }
+ case GTK_RESPONSE_NO:
+ allow_data_loss = TRUE;
+ break;
+ default: // cancel pressed, or dialog was closed
+ return TRUE;
+ break;
+ }
+ }
+ }
+
+ /* Save window geometry to prefs for use as a default.
+ * Use depends on setting of "options.savewindowgeometry".
+ * But we save the info here regardless of the setting.
+ */
+ storeDesktopPosition();
+
+ return FALSE;
+}
+
+/**
+ * \store dessktop position
+ */
+void SPDesktopWidget::storeDesktopPosition()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool maxed = desktop->is_maximized();
+ bool full = desktop->is_fullscreen();
+ prefs->setBool("/desktop/geometry/fullscreen", full);
+ prefs->setBool("/desktop/geometry/maximized", maxed);
+ gint w, h, x, y;
+ desktop->getWindowGeometry(x, y, w, h);
+ // Don't save geom for maximized windows. It
+ // just tells you the current maximized size, which is not
+ // as useful as whatever value it had previously.
+ if (!maxed && !full) {
+ prefs->setInt("/desktop/geometry/width", w);
+ prefs->setInt("/desktop/geometry/height", h);
+ prefs->setInt("/desktop/geometry/x", x);
+ prefs->setInt("/desktop/geometry/y", y);
+ }
+}
+
+/**
+ * \pre this->desktop->main != 0
+ */
+void
+SPDesktopWidget::requestCanvasUpdate() {
+ // ^^ also this->desktop != 0
+ g_return_if_fail(this->desktop != nullptr);
+ g_return_if_fail(this->desktop->main != nullptr);
+ gtk_widget_queue_draw (GTK_WIDGET (SP_CANVAS_ITEM (this->desktop->main)->canvas));
+}
+
+void
+SPDesktopWidget::requestCanvasUpdateAndWait() {
+ requestCanvasUpdate();
+
+ while (gtk_events_pending())
+ gtk_main_iteration_do(FALSE);
+
+}
+
+void
+SPDesktopWidget::enableInteraction()
+{
+ g_return_if_fail(_interaction_disabled_counter > 0);
+
+ _interaction_disabled_counter--;
+
+ if (_interaction_disabled_counter == 0) {
+ gtk_widget_set_sensitive(GTK_WIDGET(this), TRUE);
+ }
+}
+
+void
+SPDesktopWidget::disableInteraction()
+{
+ if (_interaction_disabled_counter == 0) {
+ gtk_widget_set_sensitive(GTK_WIDGET(this), 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);
+ }
+}
+
+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);
+ }
+}
+
+#ifdef __APPLE__
+/**
+ * This should be a no-op, but on macOS it raises the given transient window to the
+ * top of other transient windows from the same parent.
+ */
+static gboolean transient_focus_in_callback(GtkWindow *window, GdkEvent *, gpointer)
+{
+ gtk_window_set_transient_for(window, gtk_window_get_transient_for(window));
+ return FALSE;
+}
+#endif
+
+/**
+ * \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());
+
+#ifdef __APPLE__
+ // Workaround for https://gitlab.gnome.org/GNOME/gtk/issues/2436
+ // The first time this window is made transient, connect the focus-in event to
+ // re-transientize the window in order to raise it above other transient windows.
+ if (gtk_window_get_transient_for(GTK_WINDOW(p)) == nullptr) {
+ g_signal_connect(GTK_WIDGET(p), "focus-in-event", G_CALLBACK(transient_focus_in_callback), nullptr);
+ }
+#endif
+
+ 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 == 2)
+ // without this, a transient window not always emerges on top
+ gtk_window_present (w);
+ }
+}
+
+void
+SPDesktopWidget::presentWindow()
+{
+ GtkWindow *w =GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this)));
+ if (w)
+ gtk_window_present (w);
+}
+
+bool SPDesktopWidget::showInfoDialog( Glib::ustring const &message )
+{
+ bool result = false;
+ Gtk::Window *window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this))));
+ 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)));
+ 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)));
+ if (GTK_IS_WINDOW(topw)) {
+ if (desktop->is_maximized()) {
+ gtk_window_unmaximize(topw);
+ } else {
+ // Save geometry to prefs before maximizing so that
+ // something useful is stored there, because GTK doesn't maintain
+ // a separate non-maximized size.
+ if (!desktop->is_iconified() && !desktop->is_fullscreen())
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint w = -1;
+ gint h, x, y;
+ getWindowGeometry(x, y, w, h);
+ g_assert(w != -1);
+ prefs->setInt("/desktop/geometry/width", w);
+ prefs->setInt("/desktop/geometry/height", h);
+ prefs->setInt("/desktop/geometry/x", x);
+ prefs->setInt("/desktop/geometry/y", y);
+ }
+ gtk_window_maximize(topw);
+ }
+ }
+}
+
+void
+SPDesktopWidget::fullscreen()
+{
+ GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas)));
+ if (GTK_IS_WINDOW(topw)) {
+ if (desktop->is_fullscreen()) {
+ gtk_window_unfullscreen(topw);
+ // widget layout is triggered by the resulting window_state_event
+ } else {
+ // Save geometry to prefs before maximizing so that
+ // something useful is stored there, because GTK doesn't maintain
+ // a separate non-maximized size.
+ if (!desktop->is_iconified() && !desktop->is_maximized())
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint w, h, x, y;
+ getWindowGeometry(x, y, w, h);
+ prefs->setInt("/desktop/geometry/width", w);
+ prefs->setInt("/desktop/geometry/height", h);
+ prefs->setInt("/desktop/geometry/x", x);
+ prefs->setInt("/desktop/geometry/y", y);
+ }
+ 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
+ */
+void SPDesktopWidget::layoutWidgets()
+{
+ SPDesktopWidget *dtw = this;
+ Glib::ustring pref_root;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (dtw->desktop->is_focusMode()) {
+ pref_root = "/focus/";
+ } else if (dtw->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();
+ }
+
+ if (!prefs->getBool(pref_root + "scrollbars/state", true)) {
+ dtw->_hscrollbar->hide();
+ dtw->_vscrollbar_box->hide();
+ dtw->_cms_adjust->hide();
+ } else {
+ dtw->_hscrollbar->show_all();
+ dtw->_vscrollbar_box->show_all();
+ dtw->_cms_adjust->show_all();
+ }
+
+ if (!prefs->getBool(pref_root + "rulers/state", true)) {
+ dtw->_guides_lock->hide();
+ dtw->_hruler->hide();
+ dtw->_vruler->hide();
+ } else {
+ dtw->_guides_lock->show_all();
+ dtw->_hruler->show_all();
+ dtw->_vruler->show_all();
+ }
+}
+
+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;
+ gpointer thing = sp_search_by_data_recursive(aux_toolbox, (gpointer) id);
+ if ( !thing ) {
+ //g_message( "Unable to locate item for {%s}", id );
+ } else if ( GTK_IS_TOGGLE_BUTTON(thing) ) {
+ GtkToggleButton *b = GTK_TOGGLE_BUTTON(thing);
+ isActive = gtk_toggle_button_get_active( b ) != 0;
+ } else if ( GTK_IS_TOGGLE_ACTION(thing) ) {
+ GtkToggleAction* act = GTK_TOGGLE_ACTION(thing);
+ isActive = gtk_toggle_action_get_active( act ) != 0;
+ } else if ( GTK_IS_TOGGLE_TOOL_BUTTON(thing) ) {
+ GtkToggleToolButton *b = GTK_TOGGLE_TOOL_BUTTON(thing);
+ isActive = gtk_toggle_tool_button_get_active( b ) != 0;
+ } else {
+ //g_message( "Item for {%s} is of an unsupported type", id );
+ }
+
+ return isActive;
+}
+
+void SPDesktopWidget::setToolboxPosition(Glib::ustring const& id, GtkPositionType pos)
+{
+ // Note - later on these won't be individual member variables.
+ GtkWidget* toolbox = nullptr;
+ if (id == "ToolToolbar") {
+ toolbox = tool_toolbox;
+ } else if (id == "AuxToolbar") {
+ toolbox = aux_toolbox;
+ } else if (id == "CommandsToolbar") {
+ toolbox = commands_toolbox;
+ } else if (id == "SnapToolbar") {
+ toolbox = snap_toolbox;
+ }
+
+
+ if (toolbox) {
+ switch(pos) {
+ case GTK_POS_TOP:
+ case GTK_POS_BOTTOM:
+ if ( gtk_widget_is_ancestor(toolbox, GTK_WIDGET(_hbox->gobj())) ) {
+ // Removing a widget can reduce ref count to zero
+ g_object_ref(G_OBJECT(toolbox));
+ _hbox->remove(*Glib::wrap(toolbox));
+ _vbox->add(*Glib::wrap(toolbox));
+ g_object_unref(G_OBJECT(toolbox));
+
+ // Function doesn't seem to be in Gtkmm wrapper yet
+ gtk_box_set_child_packing(_vbox->gobj(), toolbox, FALSE, TRUE, 0, GTK_PACK_START);
+ }
+ ToolboxFactory::setOrientation(toolbox, GTK_ORIENTATION_HORIZONTAL);
+ break;
+ case GTK_POS_LEFT:
+ case GTK_POS_RIGHT:
+ if ( !gtk_widget_is_ancestor(toolbox, GTK_WIDGET(_hbox->gobj())) ) {
+ g_object_ref(G_OBJECT(toolbox));
+ _vbox->remove(*Glib::wrap(toolbox));
+ _hbox->add(*Glib::wrap(toolbox));
+ g_object_unref(G_OBJECT(toolbox));
+
+ // Function doesn't seem to be in Gtkmm wrapper yet
+ gtk_box_set_child_packing(_hbox->gobj(), toolbox, FALSE, TRUE, 0, GTK_PACK_START);
+ if (pos == GTK_POS_LEFT) {
+ _hbox->reorder_child(*Glib::wrap(toolbox), 0 );
+ }
+ }
+ ToolboxFactory::setOrientation(toolbox, GTK_ORIENTATION_VERTICAL);
+ break;
+ }
+ }
+}
+
+
+SPDesktopWidget *sp_desktop_widget_new(SPDocument *document)
+{
+ SPDesktopWidget* dtw = SPDesktopWidget::createInstance(document);
+ return dtw;
+}
+
+SPDesktopWidget* SPDesktopWidget::createInstance(SPDocument *document)
+{
+ SPDesktopWidget *dtw = static_cast<SPDesktopWidget*>(g_object_new(SP_TYPE_DESKTOP_WIDGET, nullptr));
+
+ SPNamedView *namedview = sp_document_namedview(document, nullptr);
+
+ dtw->_dt2r = 1. / namedview->display_units->factor;
+
+ dtw->_ruler_origin = Geom::Point(0,0); //namedview->gridorigin; Why was the grid origin used here?
+
+ dtw->desktop = new SPDesktop();
+ dtw->stub = new SPDesktopWidget::WidgetStub (dtw);
+ dtw->desktop->init (namedview, dtw->_canvas, dtw->stub);
+ 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->update_rulers();
+
+ sp_view_widget_set_view (SP_VIEW_WIDGET (dtw), dtw->desktop);
+
+ /* Listen on namedview modification */
+ dtw->modified_connection = namedview->connectModified(sigc::mem_fun(*dtw, &SPDesktopWidget::namedviewModified));
+
+ dtw->layer_selector->setDesktop(dtw->desktop);
+
+ // TEMP
+ dtw->_menubar = build_menubar(dtw->desktop);
+ dtw->_menubar->set_name("MenuBar");
+ dtw->_menubar->show_all();
+
+#ifdef GDK_WINDOWING_QUARTZ
+ // native macOS menu: do this later because we don't have the window handle yet
+#else
+ dtw->_vbox->pack_start(*dtw->_menubar, false, false);
+#endif
+
+ dtw->layoutWidgets();
+
+ std::vector<GtkWidget *> toolboxes;
+ toolboxes.push_back(dtw->tool_toolbox);
+ toolboxes.push_back(dtw->aux_toolbox);
+ toolboxes.push_back(dtw->commands_toolbox);
+ toolboxes.push_back(dtw->snap_toolbox);
+
+ dtw->_panels->setDesktop( dtw->desktop );
+
+ UXManager::getInstance()->addTrack(dtw);
+ UXManager::getInstance()->connectToDesktop( toolboxes, dtw->desktop );
+
+ return dtw;
+}
+
+
+void
+SPDesktopWidget::update_rulers()
+{
+ Geom::Rect viewbox = desktop->get_display_area(true);
+ // "true" means: Use integer values of the canvas for calculating the display area, similar
+ // to the integer values used for positioning the grid lines. (see SPCanvas::scrollTo(),
+ // where ix and iy are rounded integer values; these values are stored in SPCanvasBuf->rect,
+ // and used for drawing the grid). By using the integer values here too, the ruler ticks
+ // will be perfectly aligned to the grid
+
+ double lower_x = _dt2r * (viewbox.left() - _ruler_origin[Geom::X]);
+ double upper_x = _dt2r * (viewbox.right() - _ruler_origin[Geom::X]);
+ _hruler->set_range(lower_x, upper_x);
+
+ double lower_y = _dt2r * (viewbox.bottom() - _ruler_origin[Geom::Y]);
+ double upper_y = _dt2r * (viewbox.top() - _ruler_origin[Geom::Y]);
+ if (desktop->is_yaxisdown()) {
+ std::swap(lower_y, upper_y);
+ }
+ _vruler->set_range(lower_y, upper_y);
+}
+
+
+void SPDesktopWidget::namedviewModified(SPObject *obj, guint flags)
+{
+ SPNamedView *nv=SP_NAMEDVIEW(obj);
+
+ if (flags & SP_OBJECT_MODIFIED_FLAG) {
+ _dt2r = 1. / nv->display_units->factor;
+ _ruler_origin = Geom::Point(0,0); //nv->gridorigin; Why was the grid origin used here?
+
+ _vruler->set_unit(nv->getDisplayUnit());
+ _hruler->set_unit(nv->getDisplayUnit());
+
+ /* This loops through all the grandchildren of aux toolbox,
+ * and for each that it finds, it performs an sp_search_by_data_recursive(),
+ * looking for widgets that hold some "tracker" data (this is used by
+ * all toolboxes to refer to the unit selector). The default document units
+ * is then selected within these unit selectors.
+ *
+ * Of course it would be nice to be able to refer to the toolbox and the
+ * unit selector directly by name, but I don't yet see a way to do that.
+ *
+ * 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 (GTK_IS_CONTAINER(i->gobj())) {
+ std::vector<Gtk::Widget*> grch = dynamic_cast<Gtk::Container*>(i)->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")
+ continue;
+
+ gpointer t = sp_search_by_data_recursive(GTK_WIDGET(j->gobj()), (gpointer) "unit-tracker");
+ if (t == nullptr) // didn't find any tracker data
+ continue;
+
+ UnitTracker *tracker = reinterpret_cast<UnitTracker*>( t );
+ if (tracker == nullptr) // it's null when inkscape is first opened
+ continue;
+
+ tracker->setActiveUnit( nv->display_units );
+ } // grandchildren
+ } // if child is a container
+ } // children
+ } // if aux_toolbox is a container
+
+ _hruler_box->set_tooltip_text(gettext(nv->display_units->name_plural.c_str()));
+ _vruler_box->set_tooltip_text(gettext(nv->display_units->name_plural.c_str()));
+
+ update_rulers();
+ ToolboxFactory::updateSnapToolbox(this->desktop, nullptr, this->snap_toolbox);
+ }
+}
+
+void
+SPDesktopWidget::on_adjustment_value_changed()
+{
+ if (update)
+ return;
+
+ update = 1;
+
+ // Do not call canvas->scrollTo directly... messes up 'offset'.
+ desktop->scroll_absolute( Geom::Point(_hadj->get_value(),
+ _vadj->get_value()), false);
+
+ update = 0;
+}
+
+/* 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) {
+ SPImage* image = SP_IMAGE(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)
+{
+ gchar *b = g_strdup(_zoom_status->get_text().c_str());
+
+ gchar *comma = g_strstr_len (b, -1, ",");
+ if (comma) {
+ *comma = '.';
+ }
+
+ char *oldlocale = g_strdup (setlocale(LC_NUMERIC, nullptr));
+ setlocale (LC_NUMERIC, "C");
+ gdouble new_typed = atof (b);
+ setlocale (LC_NUMERIC, oldlocale);
+ g_free (oldlocale);
+ g_free (b);
+
+ *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()
+{
+ double const zoom_factor = pow (2, _zoom_status->get_value());
+
+ // Zoom around center of window
+ Geom::Rect const d_canvas = desktop->getCanvas()->getViewbox();
+ Geom::Point midpoint = desktop->w2d(d_canvas.midpoint());
+ _zoom_status_value_changed_connection.block();
+ desktop->zoom_absolute_center_point (midpoint, zoom_factor);
+ _zoom_status_value_changed_connection.unblock();
+
+ spinbutton_defocus(GTK_WIDGET(_zoom_status->gobj()));
+}
+
+void
+SPDesktopWidget::zoom_menu_handler(double factor)
+{
+ Geom::Rect const d = desktop->get_display_area();
+ desktop->zoom_absolute_center_point (d.midpoint(), factor);
+}
+
+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(sigc::mem_fun(desktop, &SPDesktop::zoom_page));
+ 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(sigc::mem_fun(desktop, &SPDesktop::zoom_center_page));
+ menu->append(*item_center_page);
+
+ menu->show_all();
+}
+
+
+void
+SPDesktopWidget::sticky_zoom_toggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/stickyzoom/value", _sticky_zoom->get_active());
+}
+
+
+void
+SPDesktopWidget::update_zoom()
+{
+ _zoom_status_value_changed_connection.block();
+ _zoom_status->set_value(log(desktop->current_zoom()) / log(2));
+ _zoom_status->queue_draw();
+ _zoom_status_value_changed_connection.unblock();
+}
+
+
+// ---------------------- Rotation ------------------------
+int
+SPDesktopWidget::rotation_input(double *new_val)
+{
+ auto *b = g_strdup(_rotation_status->get_text().c_str());
+
+ gchar *comma = g_strstr_len (b, -1, ",");
+ if (comma) {
+ *comma = '.';
+ }
+
+ char *oldlocale = g_strdup (setlocale(LC_NUMERIC, nullptr));
+ setlocale (LC_NUMERIC, "C");
+ gdouble new_value = atof (b);
+ setlocale (LC_NUMERIC, oldlocale);
+ g_free (oldlocale);
+ g_free (b);
+
+ *new_val = new_value;
+ return true;
+}
+
+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 = desktop->getCanvas()->getViewbox();
+ _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();
+
+ spinbutton_defocus(GTK_WIDGET(_rotation_status->gobj()));
+}
+
+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_rulers()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (_guides_lock->get_visible()) {
+ _guides_lock->hide();
+ _hruler->hide();
+ _vruler->hide();
+ prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/rulers/state" : "/window/rulers/state", false);
+ } else {
+ _guides_lock->show_all();
+ _hruler->show_all();
+ _vruler->show_all();
+ prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/rulers/state" : "/window/rulers/state", true);
+ }
+}
+
+void
+SPDesktopWidget::toggle_scrollbars()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (_hscrollbar->get_visible()) {
+ _hscrollbar->hide();
+ _vscrollbar_box->hide();
+ _cms_adjust->hide();
+ prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/scrollbars/state" : "/window/scrollbars/state", false);
+ } else {
+ _hscrollbar->show_all();
+ _vscrollbar_box->show_all();
+ _cms_adjust->show_all();
+ prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/scrollbars/state" : "/window/scrollbars/state", true);
+ }
+}
+
+bool
+SPDesktopWidget::get_color_prof_adj_enabled() const
+{
+ return _cms_adjust->get_sensitive() && _cms_adjust->get_active();
+}
+
+void
+SPDesktopWidget::toggle_color_prof_adj()
+{
+ if (_cms_adjust->get_sensitive()) {
+ if (_cms_adjust->get_active()) {
+ _cms_adjust->toggle_set_down(false);
+ } else {
+ _cms_adjust->toggle_set_down(true);
+ }
+ }
+}
+
+static void
+set_adjustment (Glib::RefPtr<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 = 1;
+
+ /* The desktop region we always show unconditionally */
+ SPDocument *doc = desktop->doc();
+ Geom::Rect darea ( Geom::Point(-doc->getWidth().value("px"), -doc->getHeight().value("px")),
+ Geom::Point(2 * doc->getWidth().value("px"), 2 * doc->getHeight().value("px")) );
+
+ Geom::OptRect deskarea;
+ if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) {
+ deskarea = darea | doc->getRoot()->desktopVisualBounds();
+ } else {
+ deskarea = darea | 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->getViewbox();
+
+ /* Viewbox is always included into scrollable region */
+ carea = Geom::unify(carea, viewbox);
+
+ 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 = 0;
+}
+
+bool
+SPDesktopWidget::get_sticky_zoom_active() const
+{
+ return _sticky_zoom->get_active();
+}
+
+double
+SPDesktopWidget::get_hruler_thickness() const
+{
+ auto allocation = _hruler->get_allocation();
+ return allocation.get_height();
+}
+
+double
+SPDesktopWidget::get_vruler_thickness() const
+{
+ auto allocation = _vruler->get_allocation();
+ return allocation.get_width();
+}
+
+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(GTK_EVENT_BOX(widget)), horiz);
+ break;
+ case GDK_MOTION_NOTIFY:
+ dtw->on_ruler_box_motion_notify_event(&event->motion, Glib::wrap(GTK_EVENT_BOX(widget)), horiz);
+ break;
+ case GDK_BUTTON_RELEASE:
+ dtw->on_ruler_box_button_release_event(&event->button, Glib::wrap(GTK_EVENT_BOX(widget)), horiz);
+ break;
+ default:
+ break;
+ }
+
+ return FALSE;
+}
+
+bool
+SPDesktopWidget::on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::EventBox *widget, bool horiz)
+{
+ if (horiz) {
+ sp_event_context_snap_delay_handler(desktop->event_context, (gpointer) widget->gobj(), (gpointer) this, event, Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_HRULER);
+ }
+ else {
+ sp_event_context_snap_delay_handler(desktop->event_context, (gpointer) widget->gobj(), (gpointer) this, event, Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_VRULER);
+ }
+
+ int wx, wy;
+
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas));
+
+ 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 const event_w(sp_canvas_window_to_world(_canvas, 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->setGuides(true);
+ }
+
+ if (!(event->state & GDK_SHIFT_MASK)) {
+ ruler_snap_new_guide(desktop, _active_guide, event_dt, _normal);
+ }
+ sp_guideline_set_normal(SP_GUIDELINE(_active_guide), _normal);
+ sp_guideline_set_position(SP_GUIDELINE(_active_guide), event_dt);
+
+ desktop->set_coordinate_status(event_dt);
+ }
+
+ return false;
+}
+
+bool
+SPDesktopWidget::on_ruler_box_button_release_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz)
+{
+ int wx, wy;
+
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas));
+
+ 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) {
+ sp_event_context_discard_delayed_snap_event(desktop->event_context);
+
+ auto seat = gdk_device_get_seat(event->device);
+ gdk_seat_ungrab(seat);
+
+ Geom::Point const event_w(sp_canvas_window_to_world(_canvas, event_win));
+ Geom::Point event_dt(desktop->w2d(event_w));
+
+ if (!(event->state & GDK_SHIFT_MASK)) {
+ ruler_snap_new_guide(desktop, _active_guide, event_dt, _normal);
+ }
+
+ sp_canvas_item_destroy(_active_guide);
+ _active_guide = nullptr;
+ 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;
+ }
+ sp_repr_set_point(repr, "position", Geom::Point( newx, newy ));
+ sp_repr_set_point(repr, "orientation", _normal);
+ desktop->namedview->appendChild(repr);
+ Inkscape::GC::release(repr);
+ DocumentUndo::done(desktop->getDocument(), SP_VERB_NONE,
+ _("Create guide"));
+ }
+ desktop->set_coordinate_status(event_dt);
+
+ if (!_ruler_dragged) {
+ // Ruler click (without drag) toggle the guide visibility on and off
+ Inkscape::XML::Node *repr = desktop->namedview->getRepr();
+ sp_namedview_toggle_guides(desktop->getDocument(), desktop->namedview);
+ }
+
+ _ruler_clicked = false;
+ _ruler_dragged = false;
+ }
+
+ return false;
+}
+
+bool
+SPDesktopWidget::on_ruler_box_button_press_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz)
+{
+ if (_ruler_clicked) // event triggerred on a double click: do no process the click
+ return false;
+
+ int wx, wy;
+
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas));
+
+ 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(sp_canvas_window_to_world(_canvas, 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();
+ Inkscape::CanvasGrid * grid = sp_namedview_get_first_enabled_grid(desktop->namedview);
+ if (grid){
+ if (grid->getGridType() == Inkscape::GRID_AXONOMETRIC ) {
+ Inkscape::CanvasAxonomGrid *axonomgrid = dynamic_cast<Inkscape::CanvasAxonomGrid *>(grid);
+ if (event->state & GDK_CONTROL_MASK) {
+ // guidelines normal to gridlines
+ normal_bl_to_tr = Geom::Point::polar(-axonomgrid->angle_rad[0], 1.0);
+ normal_tr_to_bl = Geom::Point::polar(axonomgrid->angle_rad[2], 1.0);
+ } else {
+ normal_bl_to_tr = rot90(Geom::Point::polar(axonomgrid->angle_rad[2], 1.0));
+ normal_tr_to_bl = rot90(Geom::Point::polar(-axonomgrid->angle_rad[0], 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 = sp_guideline_new(desktop->guides, nullptr, event_dt, _normal);
+ sp_guideline_set_color(SP_GUIDELINE(_active_guide), desktop->namedview->guidehicolor);
+
+ 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;
+}
+
+GtkAllocation
+SPDesktopWidget::get_canvas_allocation() const
+{
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(GTK_WIDGET(_canvas), &allocation);
+ return allocation;
+}
+
+void
+SPDesktopWidget::ruler_snap_new_guide(SPDesktop *desktop, SPCanvasItem * /*guide*/, Geom::Point &event_dt, Geom::Point &normal)
+{
+ 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.getSnapPerp();
+ bool pref_tang = m.snapprefs.getSnapTang();
+ m.snapprefs.setSnapPerp(false);
+ m.snapprefs.setSnapTang(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.setSnapPerp(pref_perp);
+ m.snapprefs.setSnapTang(pref_tang);
+ m.unSetup();
+}
+
+/*
+ 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..e61cdd5
--- /dev/null
+++ b/src/widgets/desktop-widget.h
@@ -0,0 +1,354 @@
+// 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 <gtkmm.h>
+
+#include "message.h"
+#include "ui/view/view-widget.h"
+#include "ui/view/edit-widget-interface.h"
+
+#include <cstddef>
+#include <sigc++/connection.h>
+#include <2geom/point.h>
+
+// forward declaration
+typedef struct _EgeColorProfTracker EgeColorProfTracker;
+struct SPCanvas;
+struct SPCanvasItem;
+class SPDocument;
+class SPDesktop;
+struct SPDesktopWidget;
+class SPObject;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+class SwatchesPanel;
+} // namespace Dialog
+
+namespace Widget {
+class Button;
+class LayerSelector;
+class SelectedStyle;
+class Ruler;
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#define SP_TYPE_DESKTOP_WIDGET SPDesktopWidget::getType()
+#define SP_DESKTOP_WIDGET(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_DESKTOP_WIDGET, SPDesktopWidget))
+#define SP_DESKTOP_WIDGET_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_DESKTOP_WIDGET, SPDesktopWidgetClass))
+#define SP_IS_DESKTOP_WIDGET(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_DESKTOP_WIDGET))
+#define SP_IS_DESKTOP_WIDGET_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_DESKTOP_WIDGET))
+
+/**
+ * Create a new SPDesktopWidget
+ */
+SPDesktopWidget *sp_desktop_widget_new(SPDocument* document);
+
+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);
+
+void sp_dtw_desktop_activate (SPDesktopWidget *dtw);
+void sp_dtw_desktop_deactivate (SPDesktopWidget *dtw);
+
+/// A GtkEventBox on an SPDesktop.
+struct SPDesktopWidget {
+ SPViewWidget viewwidget;
+
+ unsigned int update : 1;
+
+ sigc::connection modified_connection;
+
+ SPDesktop *desktop;
+
+ Gtk::Window *window;
+
+ static void dispose(GObject *object);
+
+private:
+ // Flags for ruler event handling
+ bool _ruler_clicked; ///< True if the ruler has been clicked
+ bool _ruler_dragged; ///< True if a drag on the ruler is occurring
+
+ SPCanvasItem *_active_guide; ///< The guide currently being handled during a ruler event
+ Geom::Point _normal; ///< Normal to the guide currently being handled during ruler event
+ int _xp; ///< x coordinate for start of drag
+ int _yp; ///< y coordinate for start of drag
+
+ // The root vbox of the window layout.
+ Gtk::Box *_vbox;
+
+ Gtk::Box *_hbox;
+
+ Gtk::MenuBar *_menubar; // TEMP
+ Gtk::Box *_statusbar;
+
+ Inkscape::UI::Dialog::SwatchesPanel *_panels;
+
+ Glib::RefPtr<Gtk::Adjustment> _hadj;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+
+ Gtk::ToggleButton *_guides_lock;
+
+ Inkscape::UI::Widget::Button *_cms_adjust;
+ Gtk::ToggleButton *_sticky_zoom;
+ Gtk::Grid *_coord_status;
+
+ Gtk::Label *_coord_status_x;
+ Gtk::Label *_coord_status_y;
+ Gtk::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::Label *_select_status;
+ Gtk::SpinButton *_rotation_status;
+
+ 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::Dock *_dock;
+
+ Gtk::Scrollbar *_hscrollbar;
+ Gtk::Scrollbar *_vscrollbar;
+ Gtk::Box *_vscrollbar_box;
+
+ Inkscape::UI::Widget::SelectedStyle *_selected_style;
+
+ /** A table for displaying the canvas, rulers etc */
+ Gtk::Grid *_canvas_tbl;
+ sigc::connection _canvas_tbl_size_allocate_connection;
+
+ Gtk::EventBox *_hruler_box;
+ Gtk::EventBox *_vruler_box; // eventboxes for setting tooltips
+
+ /* Rulers */
+ Inkscape::UI::Widget::Ruler *_hruler;
+ Inkscape::UI::Widget::Ruler *_vruler;
+ Gtk::Allocation _allocation;
+
+ unsigned int _interaction_disabled_counter;
+
+ Geom::Point _ruler_origin;
+ double _dt2r;
+
+ SPCanvas *_canvas;
+
+public:
+ Inkscape::UI::Widget::LayerSelector *layer_selector;
+
+ EgeColorProfTracker* _tracker;
+
+ struct WidgetStub : public Inkscape::UI::View::EditWidgetInterface {
+ SPDesktopWidget *_dtw;
+ WidgetStub (SPDesktopWidget* dtw) : _dtw(dtw) {}
+
+ void setTitle (gchar const *uri) override
+ { _dtw->updateTitle (uri); }
+ Gtk::Window* getWindow() override
+ { return _dtw->window; }
+
+ void layout() override {
+ _dtw->layoutWidgets();
+ }
+
+ void present() override
+ { _dtw->presentWindow(); }
+ void getGeometry (gint &x, gint &y, gint &w, gint &h) override
+ { _dtw->getWindowGeometry (x, y, w, h); }
+ void setSize (gint w, gint h) override
+ { _dtw->setWindowSize (w, h); }
+ void setPosition (Geom::Point p) override
+ { _dtw->setWindowPosition (p); }
+ void setTransient (void* p, int transient_policy) override
+ { _dtw->setWindowTransient (p, transient_policy); }
+ Geom::Point getPointer() override
+ { return _dtw->window_get_pointer(); }
+ void setIconified() override
+ { _dtw->iconify(); }
+ void setMaximized() override
+ { _dtw->maximize(); }
+ void setFullscreen() override
+ { _dtw->fullscreen(); }
+ bool shutdown() override
+ { return _dtw->shutdown(); }
+ void destroy() override
+ {
+ if(_dtw->window != nullptr)
+ delete _dtw->window;
+ _dtw->window = nullptr;
+ }
+
+ void storeDesktopPosition() override { _dtw->storeDesktopPosition(); }
+ void requestCanvasUpdate() override { _dtw->requestCanvasUpdate(); }
+ void requestCanvasUpdateAndWait() override { _dtw->requestCanvasUpdateAndWait(); }
+ void enableInteraction() override { _dtw->enableInteraction(); }
+ void disableInteraction() override { _dtw->disableInteraction(); }
+ void activateDesktop() override { sp_dtw_desktop_activate(_dtw); }
+ void deactivateDesktop() override { sp_dtw_desktop_deactivate(_dtw); }
+ void updateRulers() override { _dtw->update_rulers(); }
+ void updateScrollbars(double scale) override { _dtw->update_scrollbars(scale); }
+ void toggleRulers() override { _dtw->toggle_rulers(); }
+ void toggleScrollbars() override { _dtw->toggle_scrollbars(); }
+ void toggleColorProfAdjust() override { _dtw->toggle_color_prof_adj(); }
+ bool colorProfAdjustEnabled() override { return _dtw->get_color_prof_adj_enabled(); }
+ void updateZoom() override { _dtw->update_zoom(); }
+ void letZoomGrabFocus() override { _dtw->letZoomGrabFocus(); }
+ void updateRotation() override { _dtw->update_rotation(); }
+ Gtk::Toolbar* get_toolbar_by_name(const Glib::ustring& name) override {return _dtw->get_toolbar_by_name(name);}
+ void setToolboxFocusTo(const gchar *id) override { _dtw->setToolboxFocusTo(id); }
+ void setToolboxAdjustmentValue(const gchar *id, double val) override
+ { _dtw->setToolboxAdjustmentValue (id, val); }
+ bool isToolboxButtonActive (gchar const* id) override
+ { return _dtw->isToolboxButtonActive (id); }
+ void setCoordinateStatus (Geom::Point p) override
+ { _dtw->setCoordinateStatus (p); }
+ void setMessage (Inkscape::MessageType type, gchar const* msg) override
+ { _dtw->setMessage (type, msg); }
+
+ bool showInfoDialog( Glib::ustring const &message ) override {
+ return _dtw->showInfoDialog( message );
+ }
+
+ bool warnDialog (Glib::ustring const &text) override
+ { return _dtw->warnDialog (text); }
+
+ Inkscape::UI::Widget::Dock* getDock () override
+ { return _dtw->getDock(); }
+ };
+
+ WidgetStub *stub;
+
+ void setMessage(Inkscape::MessageType type, gchar const *message);
+ Geom::Point window_get_pointer();
+ bool shutdown();
+ 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 setToolboxPosition(Glib::ustring const& id, GtkPositionType pos);
+ void setCoordinateStatus(Geom::Point p);
+ void storeDesktopPosition();
+ void requestCanvasUpdate();
+ void requestCanvasUpdateAndWait();
+ void enableInteraction();
+ void disableInteraction();
+ void updateTitle(gchar const *uri);
+ bool onFocusInEvent(GdkEventFocus*);
+
+ Gtk::MenuBar *menubar() { return _menubar; }
+
+ Inkscape::UI::Widget::Dock* getDock();
+
+ static GType getType();
+ static SPDesktopWidget* createInstance(SPDocument *document);
+
+ void updateNamedview();
+ void update_guides_lock();
+
+ /// Get the CMS adjustment button widget
+ decltype(_cms_adjust) get_cms_adjust() const {return _cms_adjust;}
+
+ void cms_adjust_set_sensitive(bool enabled);
+ bool get_color_prof_adj_enabled() const;
+ void toggle_color_prof_adj();
+ bool get_sticky_zoom_active() const;
+ void update_zoom();
+ void update_rotation();
+ void update_rulers();
+ double get_hruler_thickness() const;
+ double get_vruler_thickness() const;
+ GtkAllocation get_canvas_allocation() const;
+ void iconify();
+ void maximize();
+ void fullscreen();
+ static gint ruler_event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw, bool horiz);
+
+ private:
+ GtkWidget *tool_toolbox;
+ GtkWidget *aux_toolbox;
+ GtkWidget *commands_toolbox;
+ GtkWidget *snap_toolbox;
+
+ static void init(SPDesktopWidget *widget);
+ void layoutWidgets();
+
+ void namedviewModified(SPObject *obj, guint flags);
+ void on_adjustment_value_changed();
+ void toggle_scrollbars();
+ void update_scrollbars(double scale);
+ void toggle_rulers();
+ void sticky_zoom_toggled();
+ 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);
+ int rotation_input(double *new_val);
+ bool rotation_output();
+ void rotation_value_changed();
+ void rotation_populate_popup(Gtk::Menu *menu);
+ void canvas_tbl_size_allocate(Gtk::Allocation &allocation);
+
+#if defined(HAVE_LIBLCMS2)
+ static void cms_adjust_toggled( GtkWidget *button, gpointer data );
+ static void color_profile_event(EgeColorProfTracker *tracker, SPDesktopWidget *dtw);
+#endif
+ static void ruler_snap_new_guide(SPDesktop *desktop, SPCanvasItem *guide, Geom::Point &event_dt, Geom::Point &normal);
+ static gint event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw);
+ bool on_ruler_box_button_press_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz);
+ bool on_ruler_box_button_release_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz);
+ bool on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::EventBox *widget, bool horiz);
+};
+
+/// The SPDesktopWidget vtable
+struct SPDesktopWidgetClass {
+ SPViewWidgetClass parent_class;
+};
+
+#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/ege-paint-def.cpp b/src/widgets/ege-paint-def.cpp
new file mode 100644
index 0000000..fa654cb
--- /dev/null
+++ b/src/widgets/ege-paint-def.cpp
@@ -0,0 +1,316 @@
+// 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 <libintl.h>
+
+#include <cstdint>
+#include <string>
+#include <iostream>
+#include <sstream>
+#include <cstring>
+#include <cstdio>
+#include <utility>
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+
+#if !defined(_)
+#define _(s) gettext(s)
+#endif // !defined(_)
+
+#include "ege-paint-def.h"
+
+namespace ege
+{
+
+static std::string mimeTEXT("text/plain");
+static std::string mimeX_COLOR("application/x-color");
+static std::string mimeOSWB_COLOR("application/x-oswb-color");
+
+PaintDef::PaintDef() :
+ descr(_("none")),
+ type(NONE),
+ r(0),
+ g(0),
+ b(0),
+ editable(false),
+ _listeners()
+{
+}
+
+PaintDef::PaintDef( ColorType type ) :
+ descr(),
+ type(type),
+ r(0),
+ g(0),
+ b(0),
+ editable(false),
+ _listeners()
+{
+ switch (type) {
+ case CLEAR:
+ descr = _("remove");
+ break;
+ case NONE:
+ descr = _("none");
+ break;
+ case RGB:
+ descr = "";
+ break;
+ }
+}
+
+PaintDef::PaintDef( unsigned int r, unsigned int g, unsigned int b, std::string description ) :
+ descr(std::move(description)),
+ type(RGB),
+ r(r),
+ g(g),
+ b(b),
+ editable(false),
+ _listeners()
+{
+}
+
+PaintDef::~PaintDef()
+= default;
+
+PaintDef::PaintDef( PaintDef const &other )
+{
+ if ( this != &other ) {
+ *this = other;
+ }
+}
+
+PaintDef& PaintDef::operator=( PaintDef const &other )
+{
+ if ( this != & other )
+ {
+ type = other.type;
+ r = other.r;
+ g = other.g;
+ b = other.b;
+ descr = other.descr;
+ editable = other.editable;
+ //TODO: _listeners should be assigned a value
+ }
+ return *this;
+}
+
+class PaintDef::HookData {
+public:
+ HookData( ColorCallback cb, void* data ) {_cb = cb; _data = data;}
+ ColorCallback _cb;
+ void* _data;
+};
+
+
+std::vector<std::string> PaintDef::getMIMETypes()
+{
+ std::vector<std::string> listing;
+ listing.push_back(mimeOSWB_COLOR);
+ listing.push_back(mimeX_COLOR);
+ listing.push_back(mimeTEXT);
+ return listing;
+}
+
+void PaintDef::getMIMEData(std::string const & type, char*& dest, int& len, int& format)
+{
+ if ( type == mimeTEXT ) {
+ dest = new char[8];
+ snprintf( dest, 8, "#%02x%02x%02x", getR(), getG(), getB() );
+ dest[7] = 0;
+ len = 8;
+ format = 8;
+ } else if ( type == mimeX_COLOR ) {
+ uint16_t* tmp = new uint16_t[4];
+ tmp[0] = (getR() << 8) | getR();
+ tmp[1] = (getG() << 8) | getG();
+ tmp[2] = (getB() << 8) | getB();
+ tmp[3] = 0xffff;
+ dest = reinterpret_cast<char*>(tmp);
+ len = 8;
+ format = 16;
+ } else if ( type == mimeOSWB_COLOR ) {
+ std::string tmp("<paint>");
+ switch ( getType() ) {
+ case ege::PaintDef::NONE:
+ {
+ tmp += "<nocolor/>";
+ }
+ break;
+ case ege::PaintDef::CLEAR:
+ {
+ tmp += "<clear/>";
+ }
+ break;
+ default:
+ {
+ tmp += std::string("<color name=\"") + descr + "\">";
+ tmp += "<sRGB r=\"";
+ tmp += Glib::Ascii::dtostr(getR()/255.0);
+ tmp += "\" g=\"";
+ tmp += Glib::Ascii::dtostr(getG()/255.0);
+ tmp += "\" b=\"";
+ tmp += Glib::Ascii::dtostr(getB()/255.0);
+ tmp += "\"/>";
+ tmp += "</color>";
+ }
+ }
+ tmp += "</paint>";
+ len = tmp.size();
+ dest = new char[len];
+ // Note that this is not null-terminated:
+ memcpy(dest, tmp.c_str(), len);
+ format = 8;
+ } else {
+ // nothing
+ dest = nullptr;
+ len = 0;
+ }
+}
+
+bool PaintDef::fromMIMEData(std::string const & type, char const * data, int len, int /*format*/)
+{
+ bool worked = false;
+ bool changed = false;
+ if ( type == mimeTEXT ) {
+ } else if ( type == mimeX_COLOR ) {
+ } else if ( type == mimeOSWB_COLOR ) {
+ std::string xml(data, len);
+ if ( xml.find("<nocolor/>") != std::string::npos ) {
+ if ( (this->type != ege::PaintDef::NONE)
+ || (this->r != 0)
+ || (this->g != 0)
+ || (this->b != 0) ) {
+ this->type = ege::PaintDef::NONE;
+ this->r = 0;
+ this->g = 0;
+ this->b = 0;
+ changed = true;
+ }
+ worked = true;
+ } else {
+ size_t pos = xml.find("<sRGB");
+ if ( pos != std::string::npos ) {
+ size_t endPos = xml.find(">", pos);
+ std::string srgb = xml.substr(pos, endPos);
+ this->type = ege::PaintDef::RGB;
+ size_t numPos = srgb.find("r=");
+ if (numPos != std::string::npos) {
+ double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3));
+ this->r = static_cast<int>(255 * dbl);
+ }
+ numPos = srgb.find("g=");
+ if (numPos != std::string::npos) {
+ double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3));
+ this->g = static_cast<int>(255 * dbl);
+ }
+ numPos = srgb.find("b=");
+ if (numPos != std::string::npos) {
+ double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3));
+ this->b = static_cast<int>(255 * dbl);
+ }
+
+ size_t pos = xml.find("<color ");
+ if ( pos != std::string::npos ) {
+ size_t endPos = xml.find(">", pos);
+ std::string colorTag = xml.substr(pos, endPos);
+
+ size_t namePos = colorTag.find("name=");
+ if (namePos != std::string::npos) {
+ char quote = colorTag[namePos + 5];
+ endPos = colorTag.find(quote, namePos + 6);
+ descr = colorTag.substr(namePos + 6, endPos - (namePos + 6));
+ }
+ }
+ changed = true;
+ worked = true;
+ }
+ }
+ }
+ if ( changed ) {
+ // beware of callbacks changing things
+ for (auto & _listener : _listeners)
+ {
+ if ( _listener->_cb )
+ {
+ _listener->_cb( _listener->_data );
+ }
+ }
+ }
+ return worked;
+}
+
+void PaintDef::setRGB( unsigned int r, unsigned int g, unsigned int b )
+{
+ if ( r != this->r || g != this->g || b != this->b ) {
+ this->r = r;
+ this->g = g;
+ this->b = b;
+
+ // beware of callbacks changing things
+ for (auto & _listener : _listeners)
+ {
+ if ( _listener->_cb )
+ {
+ _listener->_cb( _listener->_data );
+ }
+ }
+ }
+}
+
+void PaintDef::addCallback( ColorCallback cb, void* data )
+{
+ _listeners.push_back( new HookData(cb, data) );
+}
+
+void PaintDef::removeCallback( ColorCallback /*cb*/, void* /*data*/ )
+{
+}
+
+
+} // namespace ege
+
+/*
+ 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/ege-paint-def.h b/src/widgets/ege-paint-def.h
new file mode 100644
index 0000000..63643c5
--- /dev/null
+++ b/src/widgets/ege-paint-def.h
@@ -0,0 +1,112 @@
+// 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 SEEN_EGE_PAINT_DEF_H
+#define SEEN_EGE_PAINT_DEF_H
+
+#include <string>
+#include <vector>
+
+namespace ege
+{
+
+typedef void (*ColorCallback)( void* data );
+
+
+/**
+ * Pure data representation of a color definition.
+ */
+class PaintDef
+{
+public:
+ enum ColorType{CLEAR, NONE, RGB};
+
+ PaintDef();
+ PaintDef(ColorType type);
+ PaintDef( unsigned int r, unsigned int g, unsigned int b, std::string description );
+ virtual ~PaintDef();
+
+ PaintDef( PaintDef const &other );
+ virtual PaintDef& operator=( PaintDef const &other );
+
+ ColorType getType() const { return type; }
+
+ std::vector<std::string> getMIMETypes();
+ void getMIMEData(std::string const & type, char*& dest, int& len, int& format);
+ bool fromMIMEData(std::string const & type, char const * data, int len, int format);
+
+ void setRGB( unsigned int r, unsigned int g, unsigned int b );
+ unsigned int getR() const { return r; }
+ unsigned int getG() const { return g; }
+ unsigned int getB() const { return b; }
+
+ void addCallback( ColorCallback cb, void* data );
+ void removeCallback( ColorCallback cb, void* data );
+
+ bool isEditable() const { return editable; }
+ void setEditable( bool edit ) { editable = edit; }
+
+ std::string descr;
+
+protected:
+ ColorType type;
+ unsigned int r;
+ unsigned int g;
+ unsigned int b;
+ bool editable;
+
+private:
+ class HookData;
+
+ std::vector<HookData*> _listeners;
+};
+
+
+} // namespace ege
+
+#endif // SEEN_EGE_PAINT_DEF_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/fill-n-stroke-factory.h b/src/widgets/fill-n-stroke-factory.h
new file mode 100644
index 0000000..5ca9daa
--- /dev/null
+++ b/src/widgets/fill-n-stroke-factory.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_FILL_N_STROKE_FACTORY_H
+#define SEEN_FILL_N_STROKE_FACTORY_H
+/* Authors:
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "fill-or-stroke.h"
+
+namespace Gtk {
+class Widget;
+}
+
+namespace Inkscape {
+namespace Widgets {
+
+Gtk::Widget *createStyleWidget( FillOrStroke kind );
+Gtk::Widget *createStrokeStyleWidget( );
+
+} // namespace Widgets
+} // namespace Inkscape
+
+#endif // !SEEN_FILL_N_STROKE_FACTORY_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/fill-style.cpp b/src/widgets/fill-style.cpp
new file mode 100644
index 0000000..f35a148
--- /dev/null
+++ b/src/widgets/fill-style.cpp
@@ -0,0 +1,832 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Fill style widget.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noSP_FS_VERBOSE
+
+#include <gtkmm/box.h>
+#include <glibmm/i18n.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "fill-n-stroke-factory.h"
+#include "fill-style.h"
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-text.h"
+#include "style.h"
+
+#include "display/sp-canvas.h"
+
+#include "widgets/paint-selector.h"
+
+
+// These can be deleted once we sort out the libart dependence.
+
+#define ART_WIND_RULE_NONZERO 0
+
+/* Fill */
+
+
+Gtk::Widget *sp_fill_style_widget_new()
+{
+ return Inkscape::Widgets::createStyleWidget( FILL );
+}
+
+
+namespace Inkscape {
+
+class FillNStroke : public Gtk::VBox
+{
+public:
+ FillNStroke( FillOrStroke k );
+ ~FillNStroke() override;
+
+ void setFillrule( SPPaintSelector::FillRule mode );
+
+ void setDesktop(SPDesktop *desktop);
+
+private:
+ static void paintModeChangeCB(SPPaintSelector *psel, SPPaintSelector::Mode mode, FillNStroke *self);
+ static void paintChangedCB(SPPaintSelector *psel, FillNStroke *self);
+ static void paintDraggedCB(SPPaintSelector *psel, FillNStroke *self);
+ static gboolean dragDelayCB(gpointer data);
+
+ static void fillruleChangedCB( SPPaintSelector *psel, SPPaintSelector::FillRule mode, FillNStroke *self );
+
+ void selectionModifiedCB(guint flags);
+ void eventContextCB(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext);
+
+ void dragFromPaint();
+ void updateFromPaint();
+
+ void performUpdate();
+
+ FillOrStroke kind;
+ SPDesktop *desktop;
+ SPPaintSelector *psel;
+ guint32 lastDrag;
+ guint dragId;
+ bool update;
+ sigc::connection selectChangedConn;
+ sigc::connection subselChangedConn;
+ sigc::connection selectModifiedConn;
+ sigc::connection eventContextConn;
+};
+
+} // namespace Inkscape
+
+void sp_fill_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop)
+{
+ Inkscape::FillNStroke *fs = dynamic_cast<Inkscape::FillNStroke*>(widget);
+ if (fs) {
+ fs->setDesktop(desktop);
+ }
+}
+
+namespace Inkscape {
+
+/**
+ * Create the fill or stroke style widget, and hook up all the signals.
+ */
+Gtk::Widget *Inkscape::Widgets::createStyleWidget( FillOrStroke kind )
+{
+ FillNStroke *filler = new FillNStroke(kind);
+
+ return filler;
+}
+
+FillNStroke::FillNStroke( FillOrStroke k ) :
+ Gtk::VBox(),
+ kind(k),
+ desktop(nullptr),
+ psel(nullptr),
+ lastDrag(0),
+ dragId(0),
+ update(false),
+ selectChangedConn(),
+ subselChangedConn(),
+ selectModifiedConn(),
+ eventContextConn()
+{
+ // Add and connect up the paint selector widget:
+ psel = sp_paint_selector_new(kind);
+ gtk_widget_show(GTK_WIDGET(psel));
+ gtk_container_add(GTK_CONTAINER(gobj()), GTK_WIDGET(psel));
+ g_signal_connect( G_OBJECT(psel), "mode_changed",
+ G_CALLBACK(paintModeChangeCB),
+ this );
+
+ g_signal_connect( G_OBJECT(psel), "dragged",
+ G_CALLBACK(paintDraggedCB),
+ this );
+
+ g_signal_connect( G_OBJECT(psel), "changed",
+ G_CALLBACK(paintChangedCB),
+ this );
+ if (kind == FILL) {
+ g_signal_connect( G_OBJECT(psel), "fillrule_changed",
+ G_CALLBACK(fillruleChangedCB),
+ this );
+ }
+
+ performUpdate();
+}
+
+FillNStroke::~FillNStroke()
+{
+ if (dragId) {
+ g_source_remove(dragId);
+ dragId = 0;
+ }
+ psel = nullptr;
+ selectModifiedConn.disconnect();
+ subselChangedConn.disconnect();
+ selectChangedConn.disconnect();
+ eventContextConn.disconnect();
+}
+
+/**
+ * On signal modified, invokes an update of the fill or stroke style paint object.
+ */
+void FillNStroke::selectionModifiedCB( guint flags )
+{
+ if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) {
+#ifdef SP_FS_VERBOSE
+ g_message("selectionModifiedCB(%d) on %p", flags, this);
+#endif
+ performUpdate();
+ }
+}
+
+void FillNStroke::setDesktop(SPDesktop *desktop)
+{
+ if (this->desktop != desktop) {
+ if (dragId) {
+ g_source_remove(dragId);
+ dragId = 0;
+ }
+ if (this->desktop) {
+ selectModifiedConn.disconnect();
+ subselChangedConn.disconnect();
+ selectChangedConn.disconnect();
+ eventContextConn.disconnect();
+ }
+ this->desktop = desktop;
+ if (desktop && desktop->selection) {
+ selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate)));
+ subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate)));
+ eventContextConn = desktop->connectEventContextChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &FillNStroke::eventContextCB), (Inkscape::UI::Tools::ToolBase *)nullptr)));
+
+ // Must check flags, so can't call performUpdate() directly.
+ selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &FillNStroke::selectionModifiedCB)));
+ }
+ performUpdate();
+ }
+}
+
+/**
+ * Listen to this "change in tool" event, in case a subselection tool (such as Gradient or Node) selection
+ * is changed back to a selection tool - especially needed for selected gradient stops.
+ */
+void FillNStroke::eventContextCB(SPDesktop * /*desktop*/, Inkscape::UI::Tools::ToolBase * /*eventcontext*/)
+{
+ performUpdate();
+}
+
+
+/**
+ * Gets the active fill or stroke style property, then sets the appropriate
+ * color, alpha, gradient, pattern, etc. for the paint-selector.
+ *
+ * @param sel Selection to use, or NULL.
+ */
+void FillNStroke::performUpdate()
+{
+ if ( update || !desktop ) {
+ return;
+ }
+
+ if ( dragId ) {
+ // local change; do nothing, but reset the flag
+ g_source_remove(dragId);
+ dragId = 0;
+ return;
+ }
+
+ update = true;
+
+ // create temporary style
+ SPStyle query(desktop->doc());
+
+ // query style from desktop into it. This returns a result flag and fills query with the style of subselection, if any, or selection
+ int result = sp_desktop_query_style(desktop, &query, (kind == FILL) ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE);
+
+ SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL);
+ SPIScale24 &targOpacity = *(kind == FILL ? query.fill_opacity.upcast() : query.stroke_opacity.upcast());
+
+ switch (result) {
+ case QUERY_STYLE_NOTHING:
+ {
+ /* No paint at all */
+ psel->setMode(SPPaintSelector::MODE_EMPTY);
+ break;
+ }
+
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently, e.g. display "averaged" somewhere in paint selector
+ case QUERY_STYLE_MULTIPLE_SAME:
+ {
+ SPPaintSelector::Mode pselmode = SPPaintSelector::getModeForStyle(query, kind);
+ psel->setMode(pselmode);
+
+ if (kind == FILL) {
+ psel->setFillrule(query.fill_rule.computed == ART_WIND_RULE_NONZERO?
+ SPPaintSelector::FILLRULE_NONZERO : SPPaintSelector::FILLRULE_EVENODD);
+ }
+
+ if (targPaint.set && targPaint.isColor()) {
+ psel->setColorAlpha(targPaint.value.color, SP_SCALE24_TO_FLOAT(targOpacity.value));
+ } else if (targPaint.set && targPaint.isPaintserver()) {
+
+ SPPaintServer *server = (kind == FILL) ? query.getFillPaintServer() : query.getStrokePaintServer();
+
+ if (server) {
+ if (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ psel->setSwatch( vector );
+ } else if (SP_IS_LINEARGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ psel->setGradientLinear( vector );
+
+ SPLinearGradient *lg = SP_LINEARGRADIENT(server);
+ psel->setGradientProperties( lg->getUnits(),
+ lg->getSpread() );
+ } else if (SP_IS_RADIALGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ psel->setGradientRadial( vector );
+
+ SPRadialGradient *rg = SP_RADIALGRADIENT(server);
+ psel->setGradientProperties( rg->getUnits(),
+ rg->getSpread() );
+#ifdef WITH_MESH
+ } else if (SP_IS_MESHGRADIENT(server)) {
+ SPGradient *array = SP_GRADIENT(server)->getArray();
+ psel->setGradientMesh( SP_MESHGRADIENT(array) );
+ SPMeshGradient *mg = SP_MESHGRADIENT(server);
+ psel->updateMeshList( SP_MESHGRADIENT( array ));
+#endif
+ } else if (SP_IS_PATTERN(server)) {
+ SPPattern *pat = SP_PATTERN(server)->rootPattern();
+ psel->updatePatternList( pat );
+ }
+ }
+ }
+ break;
+ }
+
+ case QUERY_STYLE_MULTIPLE_DIFFERENT:
+ {
+ psel->setMode(SPPaintSelector::MODE_MULTIPLE);
+ break;
+ }
+ }
+
+ update = false;
+}
+
+/**
+ * When the mode is changed, invoke a regular changed handler.
+ */
+void FillNStroke::paintModeChangeCB( SPPaintSelector * /*psel*/,
+ SPPaintSelector::Mode /*mode*/,
+ FillNStroke *self )
+{
+#ifdef SP_FS_VERBOSE
+ g_message("paintModeChangeCB(psel, mode, self:%p)", self);
+#endif
+ if (self && !self->update) {
+ self->updateFromPaint();
+ }
+}
+
+void FillNStroke::fillruleChangedCB( SPPaintSelector * /*psel*/,
+ SPPaintSelector::FillRule mode,
+ FillNStroke *self )
+{
+ if (self) {
+ self->setFillrule(mode);
+ }
+}
+
+void FillNStroke::setFillrule( SPPaintSelector::FillRule mode )
+{
+ if (!update && desktop) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-rule", (mode == SPPaintSelector::FILLRULE_EVENODD) ? "evenodd":"nonzero");
+
+ sp_desktop_set_style(desktop, css);
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(desktop->doc(), SP_VERB_DIALOG_FILL_STROKE,
+ _("Change fill rule"));
+ }
+}
+
+static gchar const *undo_F_label_1 = "fill:flatcolor:1";
+static gchar const *undo_F_label_2 = "fill:flatcolor:2";
+
+static gchar const *undo_S_label_1 = "stroke:flatcolor:1";
+static gchar const *undo_S_label_2 = "stroke:flatcolor:2";
+
+static gchar const *undo_F_label = undo_F_label_1;
+static gchar const *undo_S_label = undo_S_label_1;
+
+
+void FillNStroke::paintDraggedCB(SPPaintSelector * /*psel*/, FillNStroke *self)
+{
+#ifdef SP_FS_VERBOSE
+ g_message("paintDraggedCB(psel, spw:%p)", self);
+#endif
+ if (self && !self->update) {
+ self->dragFromPaint();
+ }
+}
+
+
+gboolean FillNStroke::dragDelayCB(gpointer data)
+{
+ gboolean keepGoing = TRUE;
+ if (data) {
+ FillNStroke *self = reinterpret_cast<FillNStroke*>(data);
+ if (!self->update) {
+ if (self->dragId) {
+ g_source_remove(self->dragId);
+ self->dragId = 0;
+
+ self->dragFromPaint();
+ self->performUpdate();
+ }
+ keepGoing = FALSE;
+ }
+ } else {
+ keepGoing = FALSE;
+ }
+ return keepGoing;
+}
+
+/**
+ * This is called repeatedly while you are dragging a color slider, only for flat color
+ * modes. Previously it set the color in style but did not update the repr for efficiency, however
+ * this was flakey and didn't buy us almost anything. So now it does the same as _changed, except
+ * lumps all its changes for undo.
+ */
+void FillNStroke::dragFromPaint()
+{
+ if (!desktop || update) {
+ return;
+ }
+
+ guint32 when = gtk_get_current_event_time();
+
+ // Don't attempt too many updates per second.
+ // Assume a base 15.625ms resolution on the timer.
+ if (!dragId && lastDrag && when && ((when - lastDrag) < 32)) {
+ // local change, do not update from selection
+ dragId = g_timeout_add_full(G_PRIORITY_DEFAULT, 33, dragDelayCB, this, nullptr);
+ }
+
+ if (dragId) {
+ // previous local flag not cleared yet;
+ // this means dragged events come too fast, so we better skip this one to speed up display
+ // (it's safe to do this in any case)
+ return;
+ }
+ lastDrag = when;
+
+ update = true;
+
+ switch (psel->mode) {
+ case SPPaintSelector::MODE_SOLID_COLOR:
+ {
+ // local change, do not update from selection
+ dragId = g_timeout_add_full(G_PRIORITY_DEFAULT, 100, dragDelayCB, this, nullptr);
+ psel->setFlatColor( desktop, (kind == FILL) ? "fill" : "stroke", (kind == FILL) ? "fill-opacity" : "stroke-opacity" );
+ DocumentUndo::maybeDone(desktop->doc(), (kind == FILL) ? undo_F_label : undo_S_label, SP_VERB_DIALOG_FILL_STROKE,
+ (kind == FILL) ? _("Set fill color") : _("Set stroke color"));
+ break;
+ }
+
+ default:
+ g_warning( "file %s: line %d: Paint %d should not emit 'dragged'",
+ __FILE__, __LINE__, psel->mode );
+ break;
+ }
+ update = false;
+}
+
+/**
+This is called (at least) when:
+1 paint selector mode is switched (e.g. flat color -> gradient)
+2 you finished dragging a gradient node and released mouse
+3 you changed a gradient selector parameter (e.g. spread)
+Must update repr.
+ */
+void FillNStroke::paintChangedCB( SPPaintSelector * /*psel*/, FillNStroke *self )
+{
+#ifdef SP_FS_VERBOSE
+ g_message("paintChangedCB(psel, spw:%p)", self);
+#endif
+ if (self && !self->update) {
+ self->updateFromPaint();
+ }
+}
+
+void FillNStroke::updateFromPaint()
+{
+ if (!desktop) {
+ return;
+ }
+ update = true;
+
+ SPDocument *document = desktop->getDocument();
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ std::vector<SPItem*> const items(selection->items().begin(), selection->items().end());
+
+ switch (psel->mode) {
+ case SPPaintSelector::MODE_EMPTY:
+ // This should not happen.
+ g_warning( "file %s: line %d: Paint %d should not emit 'changed'",
+ __FILE__, __LINE__, psel->mode);
+ break;
+ case SPPaintSelector::MODE_MULTIPLE:
+ // This happens when you switch multiple objects with different gradients to flat color;
+ // nothing to do here.
+ break;
+
+ case SPPaintSelector::MODE_NONE:
+ {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", "none");
+
+ sp_desktop_set_style(desktop, css);
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE,
+ (kind == FILL) ? _("Remove fill") : _("Remove stroke"));
+ break;
+ }
+
+ case SPPaintSelector::MODE_SOLID_COLOR:
+ {
+ if (kind == FILL) {
+ // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed; here it results in losing release events
+ desktop->getCanvas()->forceFullRedrawAfterInterruptions(0);
+ }
+
+ psel->setFlatColor( desktop,
+ (kind == FILL) ? "fill" : "stroke",
+ (kind == FILL) ? "fill-opacity" : "stroke-opacity" );
+ DocumentUndo::maybeDone(desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label, SP_VERB_DIALOG_FILL_STROKE,
+ (kind == FILL) ? _("Set fill color") : _("Set stroke color"));
+
+ if (kind == FILL) {
+ // resume interruptibility
+ desktop->getCanvas()->endForcedFullRedraws();
+ }
+
+ // on release, toggle undo_label so that the next drag will not be lumped with this one
+ if (undo_F_label == undo_F_label_1) {
+ undo_F_label = undo_F_label_2;
+ undo_S_label = undo_S_label_2;
+ } else {
+ undo_F_label = undo_F_label_1;
+ undo_S_label = undo_S_label_1;
+ }
+
+ break;
+ }
+
+ case SPPaintSelector::MODE_GRADIENT_LINEAR:
+ case SPPaintSelector::MODE_GRADIENT_RADIAL:
+ case SPPaintSelector::MODE_SWATCH:
+ if (!items.empty()) {
+ SPGradientType const gradient_type = ( psel->mode != SPPaintSelector::MODE_GRADIENT_RADIAL
+ ? SP_GRADIENT_TYPE_LINEAR
+ : SP_GRADIENT_TYPE_RADIAL );
+ bool createSwatch = (psel->mode == SPPaintSelector::MODE_SWATCH);
+
+ SPCSSAttr *css = nullptr;
+ if (kind == FILL) {
+ // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs
+ css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-opacity", "1.0");
+ }
+
+ SPGradient *vector = psel->getGradientVector();
+ if (!vector) {
+ /* No vector in paint selector should mean that we just changed mode */
+
+ SPStyle query(desktop->doc());
+ int result = objects_query_fillstroke(items, &query, kind == FILL);
+ if (result == QUERY_STYLE_MULTIPLE_SAME) {
+ SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL);
+ SPColor common;
+ if (!targPaint.isColor()) {
+ common = sp_desktop_get_color(desktop, kind == FILL);
+ } else {
+ common = targPaint.value.color;
+ }
+ vector = sp_document_default_gradient_vector( document, common, createSwatch );
+ if ( vector && createSwatch ) {
+ vector->setSwatch();
+ }
+ }
+
+ for(auto item : items){
+ //FIXME: see above
+ if (kind == FILL) {
+ sp_repr_css_change_recursive(item->getRepr(), css, "style");
+ }
+
+ if (!vector) {
+ SPGradient *gr = sp_gradient_vector_for_object( document,
+ desktop,
+ reinterpret_cast<SPObject*>(item),
+ (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE,
+ createSwatch );
+ if ( gr && createSwatch ) {
+ gr->setSwatch();
+ }
+ sp_item_set_gradient(item,
+ gr,
+ gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE);
+ } else {
+ sp_item_set_gradient(item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE);
+ }
+ }
+ } else {
+ // We have changed from another gradient type, or modified spread/units within
+ // this gradient type.
+ vector = sp_gradient_ensure_vector_normalized(vector);
+ for(auto item : items){
+ //FIXME: see above
+ if (kind == FILL) {
+ sp_repr_css_change_recursive(item->getRepr(), css, "style");
+ }
+
+ SPGradient *gr = sp_item_set_gradient(item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE);
+ psel->pushAttrsToGradient( gr );
+ }
+ }
+
+ if (css) {
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+
+ DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE,
+ (kind == FILL) ? _("Set gradient on fill") : _("Set gradient on stroke"));
+ }
+ break;
+
+#ifdef WITH_MESH
+ case SPPaintSelector::MODE_GRADIENT_MESH:
+
+ if (!items.empty()) {
+ SPGradientType const gradient_type = SP_GRADIENT_TYPE_MESH;
+
+ SPCSSAttr *css = nullptr;
+ if (kind == FILL) {
+ // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs
+ css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-opacity", "1.0");
+ }
+
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ SPDefs *defs = document->getDefs();
+
+ SPMeshGradient * mesh = psel->getMeshGradient();
+
+ for(auto item : items){
+
+ //FIXME: see above
+ if (kind == FILL) {
+ sp_repr_css_change_recursive(item->getRepr(), css, "style");
+ }
+
+ // Check if object already has mesh.
+ bool has_mesh = false;
+ SPStyle *style = item->style;
+ if (style) {
+ SPPaintServer *server =
+ (kind==FILL) ? style->getFillPaintServer():style->getStrokePaintServer();
+ if (server && SP_IS_MESHGRADIENT(server))
+ has_mesh = true;
+ }
+
+ if (!mesh || !has_mesh) {
+ // No mesh in document or object does not already have mesh ->
+ // Create new mesh.
+
+ // Create mesh element
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:meshgradient");
+
+ // privates are garbage-collectable
+ repr->setAttribute("inkscape:collect", "always");
+
+ // Attach to document
+ defs->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // Get corresponding object
+ SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(repr));
+ mg->array.create(mg, item, (kind==FILL) ?
+ item->geometricBounds() : item->visualBounds());
+
+ bool isText = SP_IS_TEXT(item);
+ sp_style_set_property_url (item, ((kind == FILL) ? "fill":"stroke"),
+ mg, isText);
+
+ // (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG);
+
+ } else {
+ // Using found mesh
+
+ // Duplicate
+ Inkscape::XML::Node *mesh_repr = mesh->getRepr();
+ Inkscape::XML::Node *copy_repr = mesh_repr->duplicate(xml_doc);
+
+ // privates are garbage-collectable
+ copy_repr->setAttribute("inkscape:collect", "always");
+
+ // Attach to document
+ defs->getRepr()->appendChild(copy_repr);
+ Inkscape::GC::release(copy_repr);
+
+ // Get corresponding object
+ SPMeshGradient *mg =
+ static_cast<SPMeshGradient *>(document->getObjectByRepr(copy_repr));
+ // std::cout << " " << (mg->getId()?mg->getId():"null") << std::endl;
+ mg->array.read(mg);
+
+ Geom::OptRect item_bbox = (kind==FILL) ?
+ item->geometricBounds() : item->visualBounds();
+ mg->array.fill_box( item_bbox );
+
+ bool isText = SP_IS_TEXT(item);
+ sp_style_set_property_url (item, ((kind == FILL) ? "fill":"stroke"),
+ mg, isText);
+ }
+ }
+
+ if (css) {
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+
+ DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE,
+ (kind == FILL) ? _("Set mesh on fill") : _("Set mesh on stroke"));
+ }
+ break;
+#endif
+
+ case SPPaintSelector::MODE_PATTERN:
+
+ if (!items.empty()) {
+
+ SPPattern *pattern = psel->getPattern();
+ if (!pattern) {
+
+ /* No Pattern in paint selector should mean that we just
+ * changed mode - don't do jack.
+ */
+
+ } else {
+ Inkscape::XML::Node *patrepr = pattern->getRepr();
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ gchar *urltext = g_strdup_printf("url(#%s)", patrepr->attribute("id"));
+ sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", urltext);
+
+ // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs
+ if (kind == FILL) {
+ sp_repr_css_set_property(css, "fill-opacity", "1.0");
+ }
+
+ // cannot just call sp_desktop_set_style, because we don't want to touch those
+ // objects who already have the same root pattern but through a different href
+ // chain. FIXME: move this to a sp_item_set_pattern
+ for(auto item : items){
+ Inkscape::XML::Node *selrepr = item->getRepr();
+ if ( (kind == STROKE) && !selrepr) {
+ continue;
+ }
+ SPObject *selobj = item;
+
+ SPStyle *style = selobj->style;
+ if (style && ((kind == FILL) ? style->fill.isPaintserver() : style->stroke.isPaintserver())) {
+ SPPaintServer *server = (kind == FILL) ?
+ selobj->style->getFillPaintServer() :
+ selobj->style->getStrokePaintServer();
+ if (SP_IS_PATTERN(server) && SP_PATTERN(server)->rootPattern() == pattern)
+ // only if this object's pattern is not rooted in our selected pattern, apply
+ continue;
+ }
+
+ if (kind == FILL) {
+ sp_desktop_apply_css_recursive(selobj, css, true);
+ } else {
+ sp_repr_css_change_recursive(selrepr, css, "style");
+ }
+ }
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ g_free(urltext);
+
+ } // end if
+
+ DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE,
+ (kind == FILL) ? _("Set pattern on fill") :
+ _("Set pattern on stroke"));
+ } // end if
+
+ break;
+
+ case SPPaintSelector::MODE_UNSET:
+ if (!items.empty()) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (kind == FILL) {
+ sp_repr_css_unset_property(css, "fill");
+ } else {
+ sp_repr_css_unset_property(css, "stroke");
+ sp_repr_css_unset_property(css, "stroke-opacity");
+ sp_repr_css_unset_property(css, "stroke-width");
+ sp_repr_css_unset_property(css, "stroke-miterlimit");
+ sp_repr_css_unset_property(css, "stroke-linejoin");
+ sp_repr_css_unset_property(css, "stroke-linecap");
+ sp_repr_css_unset_property(css, "stroke-dashoffset");
+ sp_repr_css_unset_property(css, "stroke-dasharray");
+ }
+
+ sp_desktop_set_style(desktop, css);
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE,
+ (kind == FILL) ? _("Unset fill") : _("Unset stroke"));
+ }
+ break;
+
+ default:
+ g_warning( "file %s: line %d: Paint selector should not be in "
+ "mode %d",
+ __FILE__, __LINE__,
+ psel->mode );
+ break;
+ }
+
+ update = false;
+}
+
+} // namespace Inkscape
+
+/*
+ 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/fill-style.h b/src/widgets/fill-style.h
new file mode 100644
index 0000000..3a64f2c
--- /dev/null
+++ b/src/widgets/fill-style.h
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Fill style configuration
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ * Copyright (C) 2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_DIALOGS_SP_FILL_STYLE_H
+#define SEEN_DIALOGS_SP_FILL_STYLE_H
+
+namespace Gtk {
+class Widget;
+}
+
+class SPDesktop;
+
+Gtk::Widget *sp_fill_style_widget_new();
+
+void sp_fill_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop);
+
+#endif // SEEN_DIALOGS_SP_FILL_STYLE_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/gradient-image.cpp b/src/widgets/gradient-image.cpp
new file mode 100644
index 0000000..1a0e253
--- /dev/null
+++ b/src/widgets/gradient-image.cpp
@@ -0,0 +1,285 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * A simple gradient preview
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <sigc++/sigc++.h>
+
+#include <glibmm/refptr.h>
+#include <gdkmm/pixbuf.h>
+
+#include <cairomm/surface.h>
+
+#include "gradient-image.h"
+
+#include "display/cairo-utils.h"
+
+#include "object/sp-gradient.h"
+#include "object/sp-stop.h"
+
+static void sp_gradient_image_size_request (GtkWidget *widget, GtkRequisition *requisition);
+
+static void sp_gradient_image_destroy(GtkWidget *object);
+static void sp_gradient_image_get_preferred_width(GtkWidget *widget,
+ gint *minimal_width,
+ gint *natural_width);
+
+static void sp_gradient_image_get_preferred_height(GtkWidget *widget,
+ gint *minimal_height,
+ gint *natural_height);
+static gboolean sp_gradient_image_draw(GtkWidget *widget, cairo_t *cr);
+static void sp_gradient_image_gradient_release (SPObject *, SPGradientImage *im);
+static void sp_gradient_image_gradient_modified (SPObject *, guint flags, SPGradientImage *im);
+static void sp_gradient_image_update (SPGradientImage *img);
+
+G_DEFINE_TYPE(SPGradientImage, sp_gradient_image, GTK_TYPE_WIDGET);
+
+static void sp_gradient_image_class_init(SPGradientImageClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+ widget_class->get_preferred_width = sp_gradient_image_get_preferred_width;
+ widget_class->get_preferred_height = sp_gradient_image_get_preferred_height;
+ widget_class->draw = sp_gradient_image_draw;
+ widget_class->destroy = sp_gradient_image_destroy;
+}
+
+static void
+sp_gradient_image_init (SPGradientImage *image)
+{
+ gtk_widget_set_has_window (GTK_WIDGET(image), FALSE);
+
+ image->gradient = nullptr;
+
+ new (&image->release_connection) sigc::connection();
+ new (&image->modified_connection) sigc::connection();
+}
+
+static void sp_gradient_image_destroy(GtkWidget *object)
+{
+ SPGradientImage *image = SP_GRADIENT_IMAGE (object);
+
+ if (image->gradient) {
+ image->release_connection.disconnect();
+ image->modified_connection.disconnect();
+ image->gradient = nullptr;
+ }
+
+ image->release_connection.~connection();
+ image->modified_connection.~connection();
+
+ if (GTK_WIDGET_CLASS(sp_gradient_image_parent_class)->destroy)
+ GTK_WIDGET_CLASS(sp_gradient_image_parent_class)->destroy(object);
+}
+
+static void sp_gradient_image_size_request(GtkWidget * /*widget*/, GtkRequisition *requisition)
+{
+ requisition->width = 54;
+ requisition->height = 12;
+}
+
+static void sp_gradient_image_get_preferred_width(GtkWidget *widget, gint *minimal_width, gint *natural_width)
+{
+ GtkRequisition requisition;
+ sp_gradient_image_size_request(widget, &requisition);
+ *minimal_width = *natural_width = requisition.width;
+}
+
+static void sp_gradient_image_get_preferred_height(GtkWidget *widget, gint *minimal_height, gint *natural_height)
+{
+ GtkRequisition requisition;
+ sp_gradient_image_size_request(widget, &requisition);
+ *minimal_height = *natural_height = requisition.height;
+}
+
+static gboolean sp_gradient_image_draw(GtkWidget *widget, cairo_t *ct)
+{
+ SPGradientImage *image = SP_GRADIENT_IMAGE(widget);
+ SPGradient *gr = image->gradient;
+ GtkAllocation allocation;
+ gtk_widget_get_allocation(widget, &allocation);
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_pattern_destroy(check);
+
+ if (gr) {
+ cairo_pattern_t *p = gr->create_preview_pattern(allocation.width);
+ cairo_set_source(ct, p);
+ cairo_paint(ct);
+ cairo_pattern_destroy(p);
+ }
+
+ return TRUE;
+}
+
+GtkWidget *
+sp_gradient_image_new (SPGradient *gradient)
+{
+ SPGradientImage *image = SP_GRADIENT_IMAGE(g_object_new(SP_TYPE_GRADIENT_IMAGE, nullptr));
+
+ sp_gradient_image_set_gradient (image, gradient);
+
+ return GTK_WIDGET(image);
+}
+
+GdkPixbuf*
+sp_gradient_to_pixbuf (SPGradient *gr, int width, int height)
+{
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_pattern_destroy(check);
+
+ if (gr) {
+ cairo_pattern_t *p = gr->create_preview_pattern(width);
+ cairo_set_source(ct, p);
+ cairo_paint(ct);
+ cairo_pattern_destroy(p);
+ }
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ // no need to free s - the call below takes ownership
+ GdkPixbuf *pixbuf = ink_pixbuf_create_from_cairo_surface(s);
+ return pixbuf;
+}
+
+
+Glib::RefPtr<Gdk::Pixbuf>
+sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height)
+{
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_pattern_destroy(check);
+
+ if (gr) {
+ cairo_pattern_t *p = gr->create_preview_pattern(width);
+ cairo_set_source(ct, p);
+ cairo_paint(ct);
+ cairo_pattern_destroy(p);
+ }
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s));
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf =
+ Gdk::Pixbuf::create(sref, 0, 0, width, height);
+
+ cairo_surface_destroy(s);
+
+ return pixbuf;
+}
+
+
+Glib::RefPtr<Gdk::Pixbuf>
+sp_gradstop_to_pixbuf_ref (SPStop *stop, int width, int height)
+{
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ /* Checkerboard background */
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ cairo_rectangle(ct, 0, 0, width, height);
+ cairo_set_source(ct, check);
+ cairo_fill_preserve(ct);
+ cairo_pattern_destroy(check);
+
+ if (stop) {
+ /* Alpha area */
+ cairo_rectangle(ct, 0, 0, width/2, height);
+ ink_cairo_set_source_rgba32(ct, stop->get_rgba32());
+ cairo_fill(ct);
+
+ /* Solid area */
+ cairo_rectangle(ct, width/2, 0, width, height);
+ ink_cairo_set_source_rgba32(ct, stop->get_rgba32() | 0xff);
+ cairo_fill(ct);
+ }
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s));
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf =
+ Gdk::Pixbuf::create(sref, 0, 0, width, height);
+
+ cairo_surface_destroy(s);
+
+ return pixbuf;
+}
+
+
+void
+sp_gradient_image_set_gradient (SPGradientImage *image, SPGradient *gradient)
+{
+ if (image->gradient) {
+ image->release_connection.disconnect();
+ image->modified_connection.disconnect();
+ }
+
+ image->gradient = gradient;
+
+ if (gradient) {
+ image->release_connection = gradient->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gradient_image_gradient_release), image));
+ image->modified_connection = gradient->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gradient_image_gradient_modified), image));
+ }
+
+ sp_gradient_image_update (image);
+}
+
+static void
+sp_gradient_image_gradient_release (SPObject *, SPGradientImage *image)
+{
+ if (image->gradient) {
+ image->release_connection.disconnect();
+ image->modified_connection.disconnect();
+ }
+
+ image->gradient = nullptr;
+
+ sp_gradient_image_update (image);
+}
+
+static void
+sp_gradient_image_gradient_modified (SPObject *, guint /*flags*/, SPGradientImage *image)
+{
+ sp_gradient_image_update (image);
+}
+
+static void
+sp_gradient_image_update (SPGradientImage *image)
+{
+ if (gtk_widget_is_drawable (GTK_WIDGET(image))) {
+ gtk_widget_queue_draw (GTK_WIDGET (image));
+ }
+}
+
+/*
+ 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/gradient-image.h b/src/widgets/gradient-image.h
new file mode 100644
index 0000000..4fcc4ca
--- /dev/null
+++ b/src/widgets/gradient-image.h
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_GRADIENT_IMAGE_H
+#define SEEN_SP_GRADIENT_IMAGE_H
+
+/**
+ * A simple gradient preview
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtk/gtk.h>
+#include <glibmm/refptr.h>
+
+class SPGradient;
+class SPStop;
+namespace Gdk {
+ class Pixbuf;
+}
+
+#include <sigc++/connection.h>
+
+#define SP_TYPE_GRADIENT_IMAGE (sp_gradient_image_get_type ())
+#define SP_GRADIENT_IMAGE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_IMAGE, SPGradientImage))
+#define SP_GRADIENT_IMAGE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_IMAGE, SPGradientImageClass))
+#define SP_IS_GRADIENT_IMAGE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_IMAGE))
+#define SP_IS_GRADIENT_IMAGE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_IMAGE))
+
+struct SPGradientImage {
+ GtkWidget widget;
+ SPGradient *gradient;
+
+ sigc::connection release_connection;
+ sigc::connection modified_connection;
+};
+
+struct SPGradientImageClass {
+ GtkWidgetClass parent_class;
+};
+
+GType sp_gradient_image_get_type ();
+
+GtkWidget *sp_gradient_image_new (SPGradient *gradient);
+GdkPixbuf *sp_gradient_to_pixbuf (SPGradient *gr, int width, int height);
+Glib::RefPtr<Gdk::Pixbuf> sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height);
+Glib::RefPtr<Gdk::Pixbuf> sp_gradstop_to_pixbuf_ref (SPStop *gr, int width, int height);
+void sp_gradient_image_set_gradient (SPGradientImage *gi, SPGradient *gr);
+
+#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 :
diff --git a/src/widgets/gradient-selector.cpp b/src/widgets/gradient-selector.cpp
new file mode 100644
index 0000000..387f077
--- /dev/null
+++ b/src/widgets/gradient-selector.cpp
@@ -0,0 +1,660 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gradient vector widget
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gtkmm/treeview.h>
+#include <vector>
+
+#include "document-undo.h"
+#include "document.h"
+#include "gradient-chemistry.h"
+#include "gradient-vector.h"
+#include "id-clash.h"
+#include "inkscape.h"
+#include "paint-selector.h"
+#include "preferences.h"
+#include "verbs.h"
+
+#include "object/sp-defs.h"
+#include "style.h"
+
+#include "helper/action.h"
+#include "ui/icon-loader.h"
+
+#include "ui/icon-names.h"
+
+enum {
+ GRABBED,
+ DRAGGED,
+ RELEASED,
+ CHANGED,
+ LAST_SIGNAL
+};
+
+
+static void sp_gradient_selector_dispose(GObject *object);
+
+/* Signal handlers */
+static void sp_gradient_selector_vector_set (SPGradientVectorSelector *gvs, SPGradient *gr, SPGradientSelector *sel);
+static void sp_gradient_selector_edit_vector_clicked (GtkWidget *w, SPGradientSelector *sel);
+static void sp_gradient_selector_add_vector_clicked (GtkWidget *w, SPGradientSelector *sel);
+static void sp_gradient_selector_delete_vector_clicked (GtkWidget *w, SPGradientSelector *sel);
+
+static guint signals[LAST_SIGNAL] = {0};
+
+G_DEFINE_TYPE(SPGradientSelector, sp_gradient_selector, GTK_TYPE_BOX);
+
+static void sp_gradient_selector_class_init(SPGradientSelectorClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+ signals[GRABBED] = g_signal_new ("grabbed",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET (SPGradientSelectorClass, grabbed),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[DRAGGED] = g_signal_new ("dragged",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET (SPGradientSelectorClass, dragged),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[RELEASED] = g_signal_new ("released",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET (SPGradientSelectorClass, released),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[CHANGED] = g_signal_new ("changed",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET (SPGradientSelectorClass, changed),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ object_class->dispose = sp_gradient_selector_dispose;
+}
+
+static void gradsel_style_button(GtkWidget *gtkbtn, char const *iconName)
+{
+ Gtk::Button *btn = Glib::wrap(GTK_BUTTON(gtkbtn));
+ GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_widget_show(child);
+ btn->add(*manage(Glib::wrap(child)));
+ btn->set_relief(Gtk::RELIEF_NONE);
+}
+
+static void sp_gradient_selector_init(SPGradientSelector *sel)
+{
+ sel->safelyInit = true;
+ sel->blocked = false;
+
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(sel), GTK_ORIENTATION_VERTICAL);
+
+ new (&sel->nonsolid) std::vector<GtkWidget*>();
+ new (&sel->swatch_widgets) std::vector<GtkWidget*>();
+
+ sel->mode = SPGradientSelector::MODE_LINEAR;
+
+ sel->gradientUnits = SP_GRADIENT_UNITS_USERSPACEONUSE;
+ sel->gradientSpread = SP_GRADIENT_SPREAD_PAD;
+
+ /* Vectors */
+ sel->vectors = sp_gradient_vector_selector_new (nullptr, nullptr);
+ SPGradientVectorSelector *gvs = SP_GRADIENT_VECTOR_SELECTOR(sel->vectors);
+ sel->store = gvs->store;
+ sel->columns = gvs->columns;
+
+ sel->treeview = Gtk::manage(new Gtk::TreeView());
+ sel->treeview->set_model(gvs->store);
+ sel->treeview->set_headers_clickable (true);
+ sel->treeview->set_search_column(1);
+ sel->treeview->set_vexpand();
+ sel->icon_renderer = Gtk::manage(new Gtk::CellRendererPixbuf());
+ sel->text_renderer = Gtk::manage(new Gtk::CellRendererText());
+
+ sel->treeview->append_column(_("Gradient"), *sel->icon_renderer);
+ Gtk::TreeView::Column* icon_column = sel->treeview->get_column(0);
+ icon_column->add_attribute(sel->icon_renderer->property_pixbuf(), sel->columns->pixbuf);
+ icon_column->set_sort_column(sel->columns->color);
+ icon_column->set_clickable(true);
+
+ sel->treeview->append_column(_("Name"), *sel->text_renderer);
+ Gtk::TreeView::Column* name_column = sel->treeview->get_column(1);
+ sel->text_renderer->property_editable() = true;
+ name_column->add_attribute(sel->text_renderer->property_text(), sel->columns->name);
+ name_column->set_min_width(180);
+ name_column->set_clickable(true);
+ name_column->set_resizable(true);
+
+ sel->treeview->append_column("#", sel->columns->refcount);
+ Gtk::TreeView::Column* count_column = sel->treeview->get_column(2);
+ count_column->set_clickable(true);
+ count_column->set_resizable(true);
+
+ sel->treeview->signal_key_press_event().connect(sigc::mem_fun(*sel, &SPGradientSelector::onKeyPressEvent), false);
+
+ sel->treeview->show();
+
+ icon_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeColorColClick) );
+ name_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeNameColClick) );
+ count_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeCountColClick) );
+
+ gvs->tree_select_connection = sel->treeview->get_selection()->signal_changed().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeSelection) );
+ sel->text_renderer->signal_edited().connect( sigc::mem_fun(*sel, &SPGradientSelector::onGradientRename) );
+
+ sel->scrolled_window = Gtk::manage(new Gtk::ScrolledWindow());
+ sel->scrolled_window->add(*sel->treeview);
+ sel->scrolled_window->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ sel->scrolled_window->set_shadow_type(Gtk::SHADOW_IN);
+ sel->scrolled_window->set_size_request(0, 180);
+ sel->scrolled_window->set_hexpand();
+ sel->scrolled_window->show();
+
+ gtk_box_pack_start (GTK_BOX (sel), GTK_WIDGET(sel->scrolled_window->gobj()), TRUE, TRUE, 4);
+
+
+ /* Create box for buttons */
+ auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_set_homogeneous(GTK_BOX(hb), FALSE);
+ gtk_box_pack_start( GTK_BOX(sel), hb, FALSE, FALSE, 0 );
+
+ sel->add = gtk_button_new();
+ gradsel_style_button(sel->add, INKSCAPE_ICON("list-add"));
+
+ sel->nonsolid.push_back(sel->add);
+ gtk_box_pack_start (GTK_BOX (hb), sel->add, FALSE, FALSE, 0);
+
+ g_signal_connect (G_OBJECT (sel->add), "clicked", G_CALLBACK (sp_gradient_selector_add_vector_clicked), sel);
+ gtk_widget_set_sensitive (sel->add, FALSE);
+ gtk_button_set_relief(GTK_BUTTON(sel->add), GTK_RELIEF_NONE);
+ gtk_widget_set_tooltip_text( sel->add, _("Create a duplicate gradient"));
+
+ sel->edit = gtk_button_new();
+ gradsel_style_button(sel->edit, INKSCAPE_ICON("edit"));
+
+ sel->nonsolid.push_back(sel->edit);
+ gtk_box_pack_start (GTK_BOX (hb), sel->edit, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (sel->edit), "clicked", G_CALLBACK (sp_gradient_selector_edit_vector_clicked), sel);
+ gtk_widget_set_sensitive (sel->edit, FALSE);
+ gtk_button_set_relief(GTK_BUTTON(sel->edit), GTK_RELIEF_NONE);
+ gtk_widget_set_tooltip_text( sel->edit, _("Edit gradient"));
+
+ sel->del = gtk_button_new ();
+ gradsel_style_button(sel->del, INKSCAPE_ICON("list-remove"));
+
+ sel->swatch_widgets.push_back(sel->del);
+ gtk_box_pack_start (GTK_BOX (hb), sel->del, FALSE, FALSE, 0);
+ g_signal_connect (G_OBJECT (sel->del), "clicked", G_CALLBACK (sp_gradient_selector_delete_vector_clicked), sel);
+ gtk_widget_set_sensitive (sel->del, FALSE);
+ gtk_button_set_relief(GTK_BUTTON(sel->del), GTK_RELIEF_NONE);
+ gtk_widget_set_tooltip_text( sel->del, _("Delete swatch"));
+
+ gtk_widget_show_all(hb);
+}
+
+static void sp_gradient_selector_dispose(GObject *object)
+{
+ SPGradientSelector *sel = SP_GRADIENT_SELECTOR( object );
+
+ if ( sel->safelyInit ) {
+ sel->safelyInit = false;
+ sel->nonsolid.~vector<GtkWidget*>();
+ sel->swatch_widgets.~vector<GtkWidget*>();
+ }
+
+ if (sel->icon_renderer) {
+ delete sel->icon_renderer;
+ sel->icon_renderer = nullptr;
+ }
+ if (sel->text_renderer) {
+ delete sel->text_renderer;
+ sel->text_renderer = nullptr;
+ }
+
+ if ((G_OBJECT_CLASS(sp_gradient_selector_parent_class))->dispose) {
+ (G_OBJECT_CLASS(sp_gradient_selector_parent_class))->dispose(object);
+ }
+}
+
+void SPGradientSelector::setSpread(SPGradientSpread spread)
+{
+ gradientSpread = spread;
+ //gtk_combo_box_set_active (GTK_COMBO_BOX(this->spread), gradientSpread);
+}
+
+
+GtkWidget *sp_gradient_selector_new()
+{
+ SPGradientSelector *sel = SP_GRADIENT_SELECTOR(g_object_new (SP_TYPE_GRADIENT_SELECTOR, nullptr));
+
+ return GTK_WIDGET(sel);
+}
+
+void SPGradientSelector::setMode(SelectorMode mode)
+{
+ if (mode != this->mode) {
+ this->mode = mode;
+ if (mode == MODE_SWATCH) {
+ for (auto & it : nonsolid)
+ {
+ gtk_widget_hide(it);
+ }
+ for (auto & swatch_widget : swatch_widgets)
+ {
+ gtk_widget_show_all(swatch_widget);
+ }
+
+ Gtk::TreeView::Column* icon_column = treeview->get_column(0);
+ icon_column->set_title(_("Swatch"));
+
+ SPGradientVectorSelector* vs = SP_GRADIENT_VECTOR_SELECTOR(vectors);
+ vs->setSwatched();
+ } else {
+ for (auto & it : nonsolid)
+ {
+ gtk_widget_show_all(it);
+ }
+ for (auto & swatch_widget : swatch_widgets)
+ {
+ gtk_widget_hide(swatch_widget);
+ }
+ Gtk::TreeView::Column* icon_column = treeview->get_column(0);
+ icon_column->set_title(_("Gradient"));
+
+ }
+ }
+}
+
+void SPGradientSelector::setUnits(SPGradientUnits units)
+{
+ gradientUnits = units;
+}
+
+SPGradientUnits SPGradientSelector::getUnits()
+{
+ return gradientUnits;
+}
+
+SPGradientSpread SPGradientSelector::getSpread()
+{
+ return gradientSpread;
+}
+
+void SPGradientSelector::onGradientRename( const Glib::ustring& path_string, const Glib::ustring& new_text)
+{
+ Gtk::TreePath path(path_string);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ if( iter )
+ {
+ Gtk::TreeModel::Row row = *iter;
+ if ( row ) {
+ SPObject* obj = row[columns->data];
+ if ( obj ) {
+ row[columns->name] = gr_prepare_label(obj);
+ if (!new_text.empty() && new_text != row[columns->name]) {
+ rename_id(obj, new_text );
+ Inkscape::DocumentUndo::done(obj->document, SP_VERB_CONTEXT_GRADIENT,
+ _("Rename gradient"));
+ }
+ }
+ }
+ }
+}
+
+void SPGradientSelector::onTreeColorColClick() {
+ Gtk::TreeView::Column* column = treeview->get_column(0);
+ column->set_sort_column(columns->color);
+}
+
+void SPGradientSelector::onTreeNameColClick() {
+ Gtk::TreeView::Column* column = treeview->get_column(1);
+ column->set_sort_column(columns->name);
+}
+
+
+void SPGradientSelector::onTreeCountColClick() {
+ Gtk::TreeView::Column* column = treeview->get_column(2);
+ column->set_sort_column(columns->refcount);
+}
+
+void SPGradientSelector::moveSelection(int amount, bool down, bool toEnd)
+{
+ Glib::RefPtr<Gtk::TreeSelection> select = treeview->get_selection();
+ auto iter = select->get_selected();
+
+ if (amount < 0) {
+ down = !down;
+ amount = -amount;
+ }
+
+ auto canary = iter;
+ if (down) {
+ ++canary;
+ } else {
+ --canary;
+ }
+ while (canary && (toEnd || amount > 0)) {
+ --amount;
+ if (down) {
+ ++canary;
+ ++iter;
+ } else {
+ --canary;
+ --iter;
+ }
+ }
+
+ select->select(iter);
+ treeview->scroll_to_row(store->get_path(iter), 0.5);
+}
+
+bool SPGradientSelector::onKeyPressEvent(GdkEventKey *event)
+{
+ bool consume = false;
+ auto display = Gdk::Display::get_default();
+ auto keymap = display->get_keymap();
+ guint key = 0;
+ gdk_keymap_translate_keyboard_state(keymap, event->hardware_keycode,
+ static_cast<GdkModifierType>(event->state),
+ 0, &key, 0, 0, 0);
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up: {
+ moveSelection(-1);
+ consume = true;
+ break;
+ }
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down: {
+ moveSelection(1);
+ consume = true;
+ break;
+ }
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up: {
+ moveSelection(-5);
+ consume = true;
+ break;
+ }
+
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down: {
+ moveSelection(5);
+ consume = true;
+ break;
+ }
+
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End: {
+ moveSelection(0, true, true);
+ consume = true;
+ break;
+ }
+
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home: {
+ moveSelection(0, false, true);
+ consume = true;
+ break;
+ }
+ }
+ return consume;
+}
+
+void SPGradientSelector::onTreeSelection()
+{
+ if (!treeview) {
+ return;
+ }
+
+ if (blocked) {
+ return;
+ }
+
+ if (!treeview->has_focus()) {
+ /* Workaround for GTK bug on Windows/OS X
+ * When the treeview initially doesn't have focus and is clicked
+ * sometimes get_selection()->signal_changed() has the wrong selection
+ */
+ treeview->grab_focus();
+ }
+
+ const Glib::RefPtr<Gtk::TreeSelection> sel = treeview->get_selection();
+ if (!sel) {
+ return;
+ }
+
+ SPGradient *obj = nullptr;
+ /* Single selection */
+ Gtk::TreeModel::iterator iter = sel->get_selected();
+ if ( iter ) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[columns->data];
+ }
+
+ if (obj) {
+ sp_gradient_selector_vector_set (nullptr, SP_GRADIENT(obj), this);
+ }
+}
+
+bool SPGradientSelector::_checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPGradient *vector)
+{
+ bool found = false;
+
+ Gtk::TreeModel::Row row = *iter;
+ if ( vector == row[columns->data] )
+ {
+ treeview->scroll_to_row(path, 0.5);
+ Glib::RefPtr<Gtk::TreeSelection> select = treeview->get_selection();
+ bool wasBlocked = blocked;
+ blocked = true;
+ select->select(iter);
+ blocked = wasBlocked;
+ found = true;
+ }
+
+ return found;
+}
+
+void SPGradientSelector::selectGradientInTree(SPGradient *vector)
+{
+ store->foreach( sigc::bind<SPGradient*>(sigc::mem_fun(*this, &SPGradientSelector::_checkForSelected), vector) );
+}
+
+void SPGradientSelector::setVector(SPDocument *doc, SPGradient *vector)
+{
+ g_return_if_fail(!vector || SP_IS_GRADIENT(vector));
+ g_return_if_fail(!vector || (vector->document == doc));
+
+ if (vector && !vector->hasStops()) {
+ return;
+ }
+
+ sp_gradient_vector_selector_set_gradient(SP_GRADIENT_VECTOR_SELECTOR(vectors), doc, vector);
+
+ selectGradientInTree(vector);
+
+ if (vector) {
+ if ( (mode == MODE_SWATCH) && vector->isSwatch() ) {
+ if ( vector->isSolid() ) {
+ for (auto & it : nonsolid)
+ {
+ gtk_widget_hide(it);
+ }
+ } else {
+ for (auto & it : nonsolid)
+ {
+ gtk_widget_show_all(it);
+ }
+ }
+ } else if (mode != MODE_SWATCH) {
+
+ for (auto & swatch_widget : swatch_widgets)
+ {
+ gtk_widget_hide(swatch_widget);
+ }
+ for (auto & it : nonsolid)
+ {
+ gtk_widget_show_all(it);
+ }
+
+ }
+
+ if (edit) {
+ gtk_widget_set_sensitive(edit, TRUE);
+ }
+ if (add) {
+ gtk_widget_set_sensitive(add, TRUE);
+ }
+ if (del) {
+ gtk_widget_set_sensitive(del, TRUE);
+ }
+ } else {
+ if (edit) {
+ gtk_widget_set_sensitive(edit, FALSE);
+ }
+ if (add) {
+ gtk_widget_set_sensitive(add, (doc != nullptr));
+ }
+ if (del) {
+ gtk_widget_set_sensitive(del, FALSE);
+ }
+ }
+}
+
+SPGradient *SPGradientSelector::getVector()
+{
+ /* fixme: */
+ return SP_GRADIENT_VECTOR_SELECTOR(vectors)->gr;
+}
+
+
+static void sp_gradient_selector_vector_set(SPGradientVectorSelector * /*gvs*/, SPGradient *gr, SPGradientSelector *sel)
+{
+
+ if (!sel->blocked) {
+ sel->blocked = TRUE;
+ gr = sp_gradient_ensure_vector_normalized (gr);
+ sel->setVector((gr) ? gr->document : nullptr, gr);
+ g_signal_emit (G_OBJECT (sel), signals[CHANGED], 0, gr);
+ sel->blocked = FALSE;
+ }
+}
+
+
+static void
+sp_gradient_selector_delete_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel)
+{
+ const Glib::RefPtr<Gtk::TreeSelection> selection = sel->treeview->get_selection();
+ if (!selection) {
+ return;
+ }
+
+ SPGradient *obj = nullptr;
+ /* Single selection */
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if ( iter ) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[sel->columns->data];
+ }
+
+ if (obj) {
+ sp_gradient_unset_swatch(SP_ACTIVE_DESKTOP, obj->getId());
+ }
+
+}
+
+static void
+sp_gradient_selector_edit_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/dialogs/gradienteditor/showlegacy", false)) {
+ // Legacy gradient dialog
+ GtkWidget *dialog;
+ dialog = sp_gradient_vector_editor_new (SP_GRADIENT_VECTOR_SELECTOR (sel->vectors)->gr);
+ gtk_widget_show (dialog);
+ } else {
+ // Invoke the gradient tool
+ Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_CONTEXT_GRADIENT );
+ if ( verb ) {
+ SPAction *action = verb->get_action( Inkscape::ActionContext( ( Inkscape::UI::View::View * ) SP_ACTIVE_DESKTOP ) );
+ if ( action ) {
+ sp_action_perform( action, nullptr );
+ }
+ }
+ }
+}
+
+static void
+sp_gradient_selector_add_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel)
+{
+ SPDocument *doc = sp_gradient_vector_selector_get_document (SP_GRADIENT_VECTOR_SELECTOR (sel->vectors));
+
+ if (!doc)
+ return;
+
+ SPGradient *gr = sp_gradient_vector_selector_get_gradient( SP_GRADIENT_VECTOR_SELECTOR (sel->vectors));
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+
+ Inkscape::XML::Node *repr = nullptr;
+
+ if (gr) {
+ repr = gr->getRepr()->duplicate(xml_doc);
+ // Rename the new gradients id to be similar to the cloned gradients
+ Glib::ustring old_id = gr->getId();
+ rename_id(gr, old_id);
+ doc->getDefs()->getRepr()->addChild(repr, nullptr);
+ } else {
+ repr = xml_doc->createElement("svg:linearGradient");
+ Inkscape::XML::Node *stop = xml_doc->createElement("svg:stop");
+ stop->setAttribute("offset", "0");
+ stop->setAttribute("style", "stop-color:#000;stop-opacity:1;");
+ repr->appendChild(stop);
+ Inkscape::GC::release(stop);
+ stop = xml_doc->createElement("svg:stop");
+ stop->setAttribute("offset", "1");
+ stop->setAttribute("style", "stop-color:#fff;stop-opacity:1;");
+ repr->appendChild(stop);
+ Inkscape::GC::release(stop);
+ doc->getDefs()->getRepr()->addChild(repr, nullptr);
+ gr = SP_GRADIENT(doc->getObjectByRepr(repr));
+ }
+
+ sp_gradient_vector_selector_set_gradient( SP_GRADIENT_VECTOR_SELECTOR (sel->vectors), doc, gr);
+
+ sel->selectGradientInTree(gr);
+
+ Inkscape::GC::release(repr);
+}
+
+/*
+ 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/gradient-selector.h b/src/widgets/gradient-selector.h
new file mode 100644
index 0000000..c787552
--- /dev/null
+++ b/src/widgets/gradient-selector.h
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_SELECTOR_H
+#define SEEN_GRADIENT_SELECTOR_H
+
+/*
+ * Gradient vector and position widget
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+
+#include <vector>
+#include "object/sp-gradient-spread.h"
+#include "object/sp-gradient-units.h"
+
+class SPDocument;
+class SPGradient;
+
+namespace Gtk {
+class CellRendererPixbuf;
+class CellRendererText;
+class ScrolledWindow;
+class TreeView;
+}
+
+#define SP_TYPE_GRADIENT_SELECTOR (sp_gradient_selector_get_type ())
+#define SP_GRADIENT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_SELECTOR, SPGradientSelector))
+#define SP_GRADIENT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_SELECTOR, SPGradientSelectorClass))
+#define SP_IS_GRADIENT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_SELECTOR))
+#define SP_IS_GRADIENT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_SELECTOR))
+
+
+
+struct SPGradientSelector {
+ GtkBox vbox;
+
+ enum SelectorMode {
+ MODE_LINEAR,
+ MODE_RADIAL,
+ MODE_SWATCH
+ };
+
+ SelectorMode mode;
+
+ SPGradientUnits gradientUnits;
+ SPGradientSpread gradientSpread;
+
+ /* Vector selector */
+ GtkWidget *vectors;
+
+ /* Tree */
+ bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPGradient *vector);
+ bool onKeyPressEvent(GdkEventKey *event);
+ void onTreeSelection();
+ void onGradientRename( const Glib::ustring& path_string, const Glib::ustring& new_text);
+ void onTreeNameColClick();
+ void onTreeColorColClick();
+ void onTreeCountColClick();
+
+ Gtk::TreeView *treeview;
+ Gtk::ScrolledWindow *scrolled_window;
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ ModelColumns()
+ {
+ add(name);
+ add(refcount);
+ add(color);
+ add(data);
+ add(pixbuf);
+ }
+ ~ModelColumns() override = default;
+
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<unsigned long> color;
+ Gtk::TreeModelColumn<gint> refcount;
+ Gtk::TreeModelColumn<SPGradient*> data;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > pixbuf;
+
+ };
+
+ ModelColumns *columns;
+ Glib::RefPtr<Gtk::ListStore> store;
+ Gtk::CellRendererPixbuf* icon_renderer;
+ Gtk::CellRendererText* text_renderer;
+
+ /* Editing buttons */
+ GtkWidget *edit;
+ GtkWidget *add;
+ GtkWidget *del;
+ GtkWidget *merge;
+
+ /* Position widget */
+ GtkWidget *position;
+
+ bool safelyInit;
+ bool blocked;
+
+ std::vector<GtkWidget*> nonsolid;
+ std::vector<GtkWidget*> swatch_widgets;
+
+ void setMode(SelectorMode mode);
+ void setUnits(SPGradientUnits units);
+ void setSpread(SPGradientSpread spread);
+ void setVector(SPDocument *doc, SPGradient *vector);
+ void selectGradientInTree(SPGradient *vector);
+ void moveSelection(int amount, bool down = true, bool toEnd = false);
+
+ SPGradientUnits getUnits();
+ SPGradientSpread getSpread();
+ SPGradient *getVector();
+};
+
+struct SPGradientSelectorClass {
+ GtkBoxClass parent_class;
+
+ void (* grabbed) (SPGradientSelector *sel);
+ void (* dragged) (SPGradientSelector *sel);
+ void (* released) (SPGradientSelector *sel);
+ void (* changed) (SPGradientSelector *sel);
+};
+
+GType sp_gradient_selector_get_type();
+
+GtkWidget *sp_gradient_selector_new ();
+
+void sp_gradient_selector_set_bbox (SPGradientSelector *sel, gdouble x0, gdouble y0, gdouble x1, gdouble y1);
+
+#endif // SEEN_GRADIENT_SELECTOR_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/gradient-vector.cpp b/src/widgets/gradient-vector.cpp
new file mode 100644
index 0000000..a2ca973
--- /dev/null
+++ b/src/widgets/gradient-vector.cpp
@@ -0,0 +1,1314 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gradient vector selection widget
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * MenTaLguY <mental@rydia.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2004 Monash University
+ * Copyright (C) 2004 David Turner
+ * Copyright (C) 2006 MenTaLguY
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include <set>
+
+#include <glibmm.h>
+#include <glibmm/i18n.h>
+
+
+
+
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "gradient-vector.h"
+#include "layer-manager.h"
+#include "include/macros.h"
+#include "selection-chemistry.h"
+#include "verbs.h"
+
+#include "io/resource.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-root.h"
+#include "object/sp-stop.h"
+#include "style.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/dialog-events.h"
+#include "ui/selected-color.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/color-preview.h"
+
+#include "widgets/gradient-image.h"
+
+#include "xml/repr.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::UI::SelectedColor;
+
+enum {
+ VECTOR_SET,
+ LAST_SIGNAL
+};
+
+static void sp_gradient_vector_selector_destroy(GtkWidget *object);
+
+static void sp_gvs_gradient_release(SPObject *obj, SPGradientVectorSelector *gvs);
+static void sp_gvs_defs_release(SPObject *defs, SPGradientVectorSelector *gvs);
+static void sp_gvs_defs_modified(SPObject *defs, guint flags, SPGradientVectorSelector *gvs);
+
+static void sp_gvs_rebuild_gui_full(SPGradientVectorSelector *gvs);
+static SPStop *get_selected_stop( GtkWidget *vb);
+void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount );
+unsigned long sp_gradient_to_hhssll(SPGradient *gr);
+
+static guint signals[LAST_SIGNAL] = {0};
+
+// TODO FIXME kill these globals!!!
+static GtkWidget *dlg = nullptr;
+static win_data wd;
+static gint x = -1000, y = -1000, w = 0, h = 0; // impossible original values to make sure they are read from prefs
+static Glib::ustring const prefs_path = "/dialogs/gradienteditor/";
+
+G_DEFINE_TYPE(SPGradientVectorSelector, sp_gradient_vector_selector, GTK_TYPE_BOX);
+
+static void sp_gradient_vector_selector_class_init(SPGradientVectorSelectorClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+
+ signals[VECTOR_SET] = g_signal_new( "vector_set",
+ G_TYPE_FROM_CLASS(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET(SPGradientVectorSelectorClass, vector_set),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1,
+ G_TYPE_POINTER);
+
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+ widget_class->destroy = sp_gradient_vector_selector_destroy;
+}
+
+static void sp_gradient_vector_selector_init(SPGradientVectorSelector *gvs)
+{
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(gvs), GTK_ORIENTATION_VERTICAL);
+
+ gvs->idlabel = TRUE;
+
+ gvs->swatched = false;
+
+ gvs->doc = nullptr;
+ gvs->gr = nullptr;
+
+ new (&gvs->gradient_release_connection) sigc::connection();
+ new (&gvs->defs_release_connection) sigc::connection();
+ new (&gvs->defs_modified_connection) sigc::connection();
+
+ gvs->columns = new SPGradientSelector::ModelColumns();
+ gvs->store = Gtk::ListStore::create(*gvs->columns);
+ new (&gvs->tree_select_connection) sigc::connection();
+
+}
+
+static void sp_gradient_vector_selector_destroy(GtkWidget *object)
+{
+ SPGradientVectorSelector *gvs = SP_GRADIENT_VECTOR_SELECTOR(object);
+
+ if (gvs->gr) {
+ gvs->gradient_release_connection.disconnect();
+ gvs->tree_select_connection.disconnect();
+ gvs->gr = nullptr;
+ }
+
+ if (gvs->doc) {
+ gvs->defs_release_connection.disconnect();
+ gvs->defs_modified_connection.disconnect();
+ gvs->doc = nullptr;
+ }
+
+ gvs->gradient_release_connection.~connection();
+ gvs->defs_release_connection.~connection();
+ gvs->defs_modified_connection.~connection();
+ gvs->tree_select_connection.~connection();
+
+ if ((GTK_WIDGET_CLASS(sp_gradient_vector_selector_parent_class))->destroy) {
+ (GTK_WIDGET_CLASS(sp_gradient_vector_selector_parent_class))->destroy(object);
+ }
+}
+
+GtkWidget *sp_gradient_vector_selector_new(SPDocument *doc, SPGradient *gr)
+{
+ GtkWidget *gvs;
+
+ g_return_val_if_fail(!gr || SP_IS_GRADIENT(gr), NULL);
+ g_return_val_if_fail(!gr || (gr->document == doc), NULL);
+
+ gvs = static_cast<GtkWidget*>(g_object_new(SP_TYPE_GRADIENT_VECTOR_SELECTOR, nullptr));
+
+ if (doc) {
+ sp_gradient_vector_selector_set_gradient(SP_GRADIENT_VECTOR_SELECTOR(gvs), doc, gr);
+ } else {
+ sp_gvs_rebuild_gui_full(SP_GRADIENT_VECTOR_SELECTOR(gvs));
+ }
+
+ return gvs;
+}
+
+void sp_gradient_vector_selector_set_gradient(SPGradientVectorSelector *gvs, SPDocument *doc, SPGradient *gr)
+{
+// g_message("sp_gradient_vector_selector_set_gradient(%p, %p, %p) [%s] %d %d", gvs, doc, gr,
+// (gr ? gr->getId():"N/A"),
+// (gr ? gr->isSwatch() : -1),
+// (gr ? gr->isSolid() : -1));
+ static gboolean suppress = FALSE;
+
+ g_return_if_fail(gvs != nullptr);
+ g_return_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs));
+ g_return_if_fail(!gr || (doc != nullptr));
+ g_return_if_fail(!gr || SP_IS_GRADIENT(gr));
+ g_return_if_fail(!gr || (gr->document == doc));
+ g_return_if_fail(!gr || gr->hasStops());
+
+ if (doc != gvs->doc) {
+ /* Disconnect signals */
+ if (gvs->gr) {
+ gvs->gradient_release_connection.disconnect();
+ gvs->gr = nullptr;
+ }
+ if (gvs->doc) {
+ gvs->defs_release_connection.disconnect();
+ gvs->defs_modified_connection.disconnect();
+ gvs->doc = nullptr;
+ }
+
+ // Connect signals
+ if (doc) {
+ gvs->defs_release_connection = doc->getDefs()->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gvs_defs_release), gvs));
+ gvs->defs_modified_connection = doc->getDefs()->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gvs_defs_modified), gvs));
+ }
+ if (gr) {
+ gvs->gradient_release_connection = gr->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gvs_gradient_release), gvs));
+ }
+ gvs->doc = doc;
+ gvs->gr = gr;
+ sp_gvs_rebuild_gui_full(gvs);
+ if (!suppress) g_signal_emit(G_OBJECT(gvs), signals[VECTOR_SET], 0, gr);
+ } else if (gr != gvs->gr) {
+ // Harder case - keep document, rebuild list and stuff
+ // fixme: (Lauris)
+ suppress = TRUE;
+ sp_gradient_vector_selector_set_gradient(gvs, nullptr, nullptr);
+ sp_gradient_vector_selector_set_gradient(gvs, doc, gr);
+ suppress = FALSE;
+ g_signal_emit(G_OBJECT(gvs), signals[VECTOR_SET], 0, gr);
+ }
+ /* The case of setting NULL -> NULL is not very interesting */
+}
+
+SPDocument *sp_gradient_vector_selector_get_document(SPGradientVectorSelector *gvs)
+{
+ g_return_val_if_fail(gvs != nullptr, NULL);
+ g_return_val_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs), NULL);
+
+ return gvs->doc;
+}
+
+SPGradient *sp_gradient_vector_selector_get_gradient(SPGradientVectorSelector *gvs)
+{
+ g_return_val_if_fail(gvs != nullptr, NULL);
+ g_return_val_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs), NULL);
+
+ return gvs->gr;
+}
+
+Glib::ustring gr_prepare_label (SPObject *obj)
+{
+ const gchar *id = obj->label() ? obj->label() : obj->getId();
+ if (!id) {
+ id = obj->getRepr()->name();
+ }
+
+ if (strlen(id) > 14 && (!strncmp (id, "linearGradient", 14) || !strncmp (id, "radialGradient", 14)))
+ return gr_ellipsize_text (g_strdup_printf ("%s", id+14), 35);
+ return gr_ellipsize_text (id, 35);
+}
+
+/*
+ * Ellipse text if longer than maxlen, "50% start text + ... + ~50% end text"
+ * Text should be > length 8 or just return the original text
+ */
+Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen)
+{
+ if (src.length() > maxlen && maxlen > 8) {
+ size_t p1 = (size_t) maxlen / 2;
+ size_t p2 = (size_t) src.length() - (maxlen - p1 - 1);
+ return src.substr(0, p1) + "…" + src.substr(p2);
+ }
+ return src;
+}
+
+static void sp_gvs_rebuild_gui_full(SPGradientVectorSelector *gvs)
+{
+
+ gvs->tree_select_connection.block();
+
+ /* Clear old list, if there is any */
+ gvs->store->clear();
+
+ /* Pick up all gradients with vectors */
+ std::vector<SPGradient *> gl;
+ if (gvs->gr) {
+ std::vector<SPObject *> gradients = gvs->gr->document->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( grad->hasStops() && (grad->isSwatch() == gvs->swatched) ) {
+ gl.push_back(SP_GRADIENT(gradient));
+ }
+ }
+ }
+
+ /* Get usage count of all the gradients */
+ std::map<SPGradient *, gint> usageCount;
+ gr_get_usage_counts(gvs->doc, &usageCount);
+
+ if (!gvs->doc) {
+ Gtk::TreeModel::Row row = *(gvs->store->append());
+ row[gvs->columns->name] = _("No document selected");
+
+ } else if (gl.empty()) {
+ Gtk::TreeModel::Row row = *(gvs->store->append());
+ row[gvs->columns->name] = _("No gradients in document");
+
+ } else if (!gvs->gr) {
+ Gtk::TreeModel::Row row = *(gvs->store->append());
+ row[gvs->columns->name] = _("No gradient selected");
+
+ } else {
+ for (auto gr:gl) {
+ unsigned long hhssll = sp_gradient_to_hhssll(gr);
+ GdkPixbuf *pixb = sp_gradient_to_pixbuf (gr, 64, 18);
+ Glib::ustring label = gr_prepare_label(gr);
+
+ Gtk::TreeModel::Row row = *(gvs->store->append());
+ row[gvs->columns->name] = label.c_str();
+ row[gvs->columns->color] = hhssll;
+ row[gvs->columns->refcount] = usageCount[gr];
+ row[gvs->columns->data] = gr;
+ row[gvs->columns->pixbuf] = Glib::wrap(pixb);
+ }
+ }
+
+ gvs->tree_select_connection.unblock();
+
+}
+
+/*
+ * Return a "HHSSLL" version of the first stop color so we can sort by it
+ */
+unsigned long sp_gradient_to_hhssll(SPGradient *gr)
+{
+ SPStop *stop = gr->getFirstStop();
+ unsigned long rgba = stop->get_rgba32();
+ float hsl[3];
+ SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+
+ return ((int)(hsl[0]*100 * 10000)) + ((int)(hsl[1]*100 * 100)) + ((int)(hsl[2]*100 * 1));
+}
+
+static void get_all_doc_items(std::vector<SPItem*> &list, SPObject *from)
+{
+ for (auto& child: from->children) {
+ if (SP_IS_ITEM(&child)) {
+ list.push_back(SP_ITEM(&child));
+ }
+ get_all_doc_items(list, &child);
+ }
+}
+
+/*
+ * Return a SPItem's gradient
+ */
+static SPGradient * gr_item_get_gradient(SPItem *item, gboolean fillorstroke)
+{
+ SPIPaint *item_paint = item->style->getFillOrStroke(fillorstroke);
+ if (item_paint->isPaintserver()) {
+
+ SPPaintServer *item_server = (fillorstroke) ?
+ item->style->getFillPaintServer() : item->style->getStrokePaintServer();
+
+ if (SP_IS_LINEARGRADIENT(item_server) || SP_IS_RADIALGRADIENT(item_server) ||
+ (SP_IS_GRADIENT(item_server) && SP_GRADIENT(item_server)->getVector()->isSwatch())) {
+
+ return SP_GRADIENT(item_server)->getVector();
+ }
+ }
+
+ return nullptr;
+}
+
+/*
+ * Map each gradient to its usage count for both fill and stroke styles
+ */
+void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount )
+{
+ if (!doc)
+ return;
+
+ std::vector<SPItem *> all_list;
+ get_all_doc_items(all_list, doc->getRoot());
+
+ for (auto item:all_list) {
+ if (!item->getId())
+ continue;
+ SPGradient *gr = nullptr;
+ gr = gr_item_get_gradient(item, true); // fill
+ if (gr) {
+ mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1;
+ }
+ gr = gr_item_get_gradient(item, false); // stroke
+ if (gr) {
+ mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1;
+ }
+ }
+}
+
+static void sp_gvs_gradient_release(SPObject */*obj*/, SPGradientVectorSelector *gvs)
+{
+ /* Disconnect gradient */
+ if (gvs->gr) {
+ gvs->gradient_release_connection.disconnect();
+ gvs->gr = nullptr;
+ }
+
+ /* Rebuild GUI */
+ sp_gvs_rebuild_gui_full(gvs);
+}
+
+static void sp_gvs_defs_release(SPObject */*defs*/, SPGradientVectorSelector *gvs)
+{
+ gvs->doc = nullptr;
+
+ gvs->defs_release_connection.disconnect();
+ gvs->defs_modified_connection.disconnect();
+
+ /* Disconnect gradient as well */
+ if (gvs->gr) {
+ gvs->gradient_release_connection.disconnect();
+ gvs->gr = nullptr;
+ }
+
+ /* Rebuild GUI */
+ sp_gvs_rebuild_gui_full(gvs);
+}
+
+static void sp_gvs_defs_modified(SPObject */*defs*/, guint /*flags*/, SPGradientVectorSelector *gvs)
+{
+ /* fixme: We probably have to check some flags here (Lauris) */
+ sp_gvs_rebuild_gui_full(gvs);
+}
+
+void SPGradientVectorSelector::setSwatched()
+{
+ swatched = true;
+ sp_gvs_rebuild_gui_full(this);
+}
+
+/*##################################################################
+ ### Vector Editing Widget
+ ##################################################################*/
+
+#include "widgets/widget-sizes.h"
+#include "xml/node-event-vector.h"
+#include "svg/svg-color.h"
+
+#define PAD 4
+
+static GtkWidget *sp_gradient_vector_widget_new(SPGradient *gradient, SPStop *stop);
+
+static void sp_gradient_vector_widget_load_gradient(GtkWidget *widget, SPGradient *gradient);
+static gint sp_gradient_vector_dialog_delete(GtkWidget *widget, GdkEvent *event, GtkWidget *dialog);
+static void sp_gradient_vector_dialog_destroy(GtkWidget *object, gpointer data);
+static void sp_gradient_vector_widget_destroy(GtkWidget *object, gpointer data);
+static void sp_gradient_vector_gradient_release(SPObject *obj, GtkWidget *widget);
+static void sp_gradient_vector_gradient_modified(SPObject *obj, guint flags, GtkWidget *widget);
+static void sp_gradient_vector_color_dragged(Inkscape::UI::SelectedColor *selected_color, GObject *object);
+static void sp_gradient_vector_color_changed(Inkscape::UI::SelectedColor *selected_color, GObject *object);
+static void update_stop_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop);
+
+static gboolean blocked = FALSE;
+
+static void grad_edit_dia_stop_added_or_removed(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, gpointer data)
+{
+ GtkWidget *vb = GTK_WIDGET(data);
+ SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient"));
+ update_stop_list(vb, gradient, nullptr);
+}
+
+//FIXME!!! We must also listen to attr changes on all children (i.e. stops) too,
+//otherwise the dialog does not reflect undoing color or offset change. This is a major
+//hassle, unless we have a "one of the descendants changed in some way" signal.
+static Inkscape::XML::NodeEventVector grad_edit_dia_repr_events =
+{
+ grad_edit_dia_stop_added_or_removed, /* child_added */
+ grad_edit_dia_stop_added_or_removed, /* child_removed */
+ nullptr, /* attr_changed*/
+ nullptr, /* content_changed */
+ nullptr /* order_changed */
+};
+
+static void verify_grad(SPGradient *gradient)
+{
+ int i = 0;
+ SPStop *stop = nullptr;
+ /* count stops */
+ for (auto& ochild: gradient->children) {
+ if (SP_IS_STOP(&ochild)) {
+ i++;
+ stop = SP_STOP(&ochild);
+ }
+ }
+
+ Inkscape::XML::Document *xml_doc;
+ xml_doc = gradient->getRepr()->document();
+
+ if (i < 1) {
+ Inkscape::CSSOStringStream os;
+ os << "stop-color: #000000;stop-opacity:" << 1.0 << ";";
+
+ Inkscape::XML::Node *child;
+
+ child = xml_doc->createElement("svg:stop");
+ sp_repr_set_css_double(child, "offset", 0.0);
+ child->setAttribute("style", os.str());
+ gradient->getRepr()->addChild(child, nullptr);
+ Inkscape::GC::release(child);
+
+ child = xml_doc->createElement("svg:stop");
+ sp_repr_set_css_double(child, "offset", 1.0);
+ child->setAttribute("style", os.str());
+ gradient->getRepr()->addChild(child, nullptr);
+ Inkscape::GC::release(child);
+ return;
+ }
+ if (i < 2) {
+ sp_repr_set_css_double(stop->getRepr(), "offset", 0.0);
+ Inkscape::XML::Node *child = stop->getRepr()->duplicate(gradient->getRepr()->document());
+ sp_repr_set_css_double(child, "offset", 1.0);
+ gradient->getRepr()->addChild(child, stop->getRepr());
+ Inkscape::GC::release(child);
+ }
+}
+
+static void select_stop_in_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop)
+{
+ GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box"));
+
+ int i = 0;
+ for (auto& ochild: gradient->children) {
+ if (SP_IS_STOP(&ochild)) {
+ if (&ochild == new_stop) {
+ gtk_combo_box_set_active (GTK_COMBO_BOX(combo_box) , i);
+ break;
+ }
+ i++;
+ }
+ }
+}
+
+static void update_stop_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop)
+{
+
+ if (!SP_IS_GRADIENT(gradient)) {
+ return;
+ }
+
+ blocked = TRUE;
+
+ /* Clear old list, if there is any */
+ GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box"));
+ if (!combo_box) {
+ return;
+ }
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box)));
+ if (!store) {
+ return;
+ }
+ gtk_list_store_clear(store);
+ GtkTreeIter iter;
+
+ /* Populate the combobox store */
+ std::vector<SPStop *> sl;
+ if ( gradient->hasStops() ) {
+ for (auto& ochild: gradient->children) {
+ if (SP_IS_STOP(&ochild)) {
+ sl.push_back(SP_STOP(&ochild));
+ }
+ }
+ }
+ if (sl.empty()) {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, NULL, 1, _("No stops in gradient"), 2, NULL, -1);
+ gtk_widget_set_sensitive (combo_box, FALSE);
+
+ } else {
+
+ for (auto stop:sl) {
+ Inkscape::XML::Node *repr = stop->getRepr();
+ Inkscape::UI::Widget::ColorPreview *cpv = Gtk::manage(new Inkscape::UI::Widget::ColorPreview(stop->get_rgba32()));
+ GdkPixbuf *pb = cpv->toPixbuf(64, 16);
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, pb, 1, repr->attribute("id"), 2, stop, -1);
+ gtk_widget_set_sensitive (combo_box, FALSE);
+ }
+ gtk_widget_set_sensitive(combo_box, TRUE);
+ }
+
+ /* Set history */
+ if (new_stop == nullptr) {
+ gtk_combo_box_set_active (GTK_COMBO_BOX(combo_box) , 0);
+ } else {
+ select_stop_in_list(vb, gradient, new_stop);
+ }
+
+ blocked = FALSE;
+}
+
+
+// user selected existing stop from list
+static void sp_grad_edit_combo_box_changed (GtkComboBox * /*widget*/, GtkWidget *tbl)
+{
+ SPStop *stop = get_selected_stop(tbl);
+ if (!stop) {
+ return;
+ }
+
+ blocked = TRUE;
+
+ SelectedColor *csel = static_cast<SelectedColor*>(g_object_get_data(G_OBJECT(tbl), "cselector"));
+ // set its color, from the stored array
+ g_object_set_data(G_OBJECT(tbl), "updating_color", reinterpret_cast<void*>(1));
+ csel->setColorAlpha(stop->getColor(), stop->getOpacity());
+ g_object_set_data(G_OBJECT(tbl), "updating_color", reinterpret_cast<void*>(0));
+ GtkWidget *offspin = GTK_WIDGET(g_object_get_data(G_OBJECT(tbl), "offspn"));
+ GtkWidget *offslide =GTK_WIDGET(g_object_get_data(G_OBJECT(tbl), "offslide"));
+
+ GtkAdjustment *adj = static_cast<GtkAdjustment*>(g_object_get_data(G_OBJECT(tbl), "offset"));
+
+ bool isEndStop = false;
+
+ SPStop *prev = nullptr;
+ prev = stop->getPrevStop();
+ if (prev != nullptr ) {
+ gtk_adjustment_set_lower (adj, prev->offset);
+ } else {
+ isEndStop = true;
+ gtk_adjustment_set_lower (adj, 0);
+ }
+
+ SPStop *next = nullptr;
+ next = stop->getNextStop();
+ if (next != nullptr ) {
+ gtk_adjustment_set_upper (adj, next->offset);
+ } else {
+ isEndStop = true;
+ gtk_adjustment_set_upper (adj, 1.0);
+ }
+
+ //fixme: does this work on all possible input gradients?
+ if (!isEndStop) {
+ gtk_widget_set_sensitive(offslide, TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(offspin), TRUE);
+ } else {
+ gtk_widget_set_sensitive(offslide, FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(offspin), FALSE);
+ }
+
+ gtk_adjustment_set_value(adj, stop->offset);
+
+ blocked = FALSE;
+}
+
+static SPStop *get_selected_stop( GtkWidget *vb)
+{
+ SPStop *stop = nullptr;
+ GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box"));
+ if (combo_box) {
+ GtkTreeIter iter;
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo_box), &iter)) {
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box)));
+ gtk_tree_model_get (GTK_TREE_MODEL(store), &iter, 2, &stop, -1);
+ }
+ }
+ return stop;
+}
+
+static void offadjustmentChanged( GtkAdjustment *adjustment, GtkWidget *vb)
+{
+ if (!blocked) {
+ blocked = TRUE;
+
+ SPStop *stop = get_selected_stop(vb);
+ if (stop) {
+ stop->offset = gtk_adjustment_get_value (adjustment);
+ sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset);
+
+ DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", SP_VERB_CONTEXT_GRADIENT,
+ _("Change gradient stop offset"));
+
+ }
+
+ blocked = FALSE;
+ }
+}
+
+guint32 sp_average_color(guint32 c1, guint32 c2, gdouble p/* = 0.5*/)
+{
+ guint32 r = (guint32) (SP_RGBA32_R_U(c1) * p + SP_RGBA32_R_U(c2) * (1 - p));
+ guint32 g = (guint32) (SP_RGBA32_G_U(c1) * p + SP_RGBA32_G_U(c2) * (1 - p));
+ guint32 b = (guint32) (SP_RGBA32_B_U(c1) * p + SP_RGBA32_B_U(c2) * (1 - p));
+ guint32 a = (guint32) (SP_RGBA32_A_U(c1) * p + SP_RGBA32_A_U(c2) * (1 - p));
+
+ return SP_RGBA32_U_COMPOSE(r, g, b, a);
+}
+
+
+static void sp_grd_ed_add_stop(GtkWidget */*widget*/, GtkWidget *vb)
+{
+ SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient"));
+ verify_grad(gradient);
+
+ SPStop *stop = get_selected_stop(vb);
+ if (!stop) {
+ return;
+ }
+
+ Inkscape::XML::Node *new_stop_repr = nullptr;
+
+ SPStop *next = stop->getNextStop();
+
+ if (next == nullptr) {
+ SPStop *prev = stop->getPrevStop();
+ if (prev != nullptr) {
+ next = stop;
+ stop = prev;
+ }
+ }
+
+ if (next != nullptr) {
+ new_stop_repr = stop->getRepr()->duplicate(gradient->getRepr()->document());
+ gradient->getRepr()->addChild(new_stop_repr, stop->getRepr());
+ } else {
+ next = stop;
+ new_stop_repr = stop->getPrevStop()->getRepr()->duplicate(gradient->getRepr()->document());
+ gradient->getRepr()->addChild(new_stop_repr, stop->getPrevStop()->getRepr());
+ }
+
+ SPStop *newstop = reinterpret_cast<SPStop *>(gradient->document->getObjectByRepr(new_stop_repr));
+
+ newstop->offset = (stop->offset + next->offset) * 0.5 ;
+
+ guint32 const c1 = stop->get_rgba32();
+ guint32 const c2 = next->get_rgba32();
+ guint32 cnew = sp_average_color(c1, c2);
+
+ Inkscape::CSSOStringStream os;
+ gchar c[64];
+ sp_svg_write_color(c, sizeof(c), cnew);
+ gdouble opacity = static_cast<gdouble>(SP_RGBA32_A_F(cnew));
+ os << "stop-color:" << c << ";stop-opacity:" << opacity <<";";
+ newstop->setAttribute("style", os.str());
+ sp_repr_set_css_double( newstop->getRepr(), "offset", (double)newstop->offset);
+
+ sp_gradient_vector_widget_load_gradient(vb, gradient);
+ Inkscape::GC::release(new_stop_repr);
+ update_stop_list(GTK_WIDGET(vb), gradient, newstop);
+ GtkWidget *offspin = GTK_WIDGET(g_object_get_data(G_OBJECT(vb), "offspn"));
+ GtkWidget *offslide =GTK_WIDGET(g_object_get_data(G_OBJECT(vb), "offslide"));
+ gtk_widget_set_sensitive(offslide, TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(offspin), TRUE);
+ DocumentUndo::done(gradient->document, SP_VERB_CONTEXT_GRADIENT,
+ _("Add gradient stop"));
+}
+
+static void sp_grd_ed_del_stop(GtkWidget */*widget*/, GtkWidget *vb)
+{
+ SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient"));
+
+ SPStop *stop = get_selected_stop(vb);
+ if (!stop) {
+ return;
+ }
+
+ if (gradient->vector.stops.size() > 2) { // 2 is the minimum
+
+ // if we delete first or last stop, move the next/previous to the edge
+ if (stop->offset == 0) {
+ SPStop *next = stop->getNextStop();
+ if (next) {
+ next->offset = 0;
+ sp_repr_set_css_double(next->getRepr(), "offset", 0);
+ }
+ } else if (stop->offset == 1) {
+ SPStop *prev = stop->getPrevStop();
+ if (prev) {
+ prev->offset = 1;
+ sp_repr_set_css_double(prev->getRepr(), "offset", 1);
+ }
+ }
+
+ gradient->getRepr()->removeChild(stop->getRepr());
+ sp_gradient_vector_widget_load_gradient(vb, gradient);
+ update_stop_list(GTK_WIDGET(vb), gradient, nullptr);
+ DocumentUndo::done(gradient->document, SP_VERB_CONTEXT_GRADIENT,
+ _("Delete gradient stop"));
+ }
+
+}
+
+static GtkWidget * sp_gradient_vector_widget_new(SPGradient *gradient, SPStop *select_stop)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ GtkWidget *vb, *w, *f;
+
+ g_return_val_if_fail(gradient != nullptr, NULL);
+ g_return_val_if_fail(SP_IS_GRADIENT(gradient), NULL);
+
+ vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, PAD);
+ gtk_box_set_homogeneous(GTK_BOX(vb), FALSE);
+ g_signal_connect(G_OBJECT(vb), "destroy", G_CALLBACK(sp_gradient_vector_widget_destroy), NULL);
+
+ w = sp_gradient_image_new(gradient);
+ g_object_set_data(G_OBJECT(vb), "preview", w);
+ gtk_widget_show(w);
+ gtk_box_pack_start(GTK_BOX(vb), w, TRUE, TRUE, PAD);
+
+ sp_repr_add_listener(gradient->getRepr(), &grad_edit_dia_repr_events, vb);
+
+ /* ComboBox of stops with 3 columns,
+ * The color preview, the label and a pointer to the SPStop
+ */
+ GtkListStore *store = gtk_list_store_new (3, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_POINTER);
+ GtkWidget *combo_box = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store));
+
+ GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, FALSE);
+ gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer, "pixbuf", 0, NULL);
+ gtk_cell_renderer_set_padding(renderer, 5, 0);
+
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, TRUE);
+ gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer, "text", 1, NULL);
+ gtk_widget_show(combo_box);
+ gtk_box_pack_start(GTK_BOX(vb), combo_box, FALSE, FALSE, 0);
+ g_object_set_data(G_OBJECT(vb), "combo_box", combo_box);
+
+ update_stop_list(GTK_WIDGET(vb), gradient, nullptr);
+
+ g_signal_connect(G_OBJECT(combo_box), "changed", G_CALLBACK(sp_grad_edit_combo_box_changed), vb);
+
+ /* Add and Remove buttons */
+ auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1);
+ gtk_box_set_homogeneous(GTK_BOX(hb), FALSE);
+ // TRANSLATORS: "Stop" means: a "phase" of a gradient
+ GtkWidget *b = gtk_button_new_with_label(_("Add stop"));
+ gtk_widget_show(b);
+ gtk_container_add(GTK_CONTAINER(hb), b);
+ gtk_widget_set_tooltip_text(b, _("Add another control stop to gradient"));
+ g_signal_connect(G_OBJECT(b), "clicked", G_CALLBACK(sp_grd_ed_add_stop), vb);
+ b = gtk_button_new_with_label(_("Delete stop"));
+ gtk_widget_show(b);
+ gtk_container_add(GTK_CONTAINER(hb), b);
+ gtk_widget_set_tooltip_text(b, _("Delete current control stop from gradient"));
+ g_signal_connect(G_OBJECT(b), "clicked", G_CALLBACK(sp_grd_ed_del_stop), vb);
+
+ gtk_widget_show(hb);
+ gtk_box_pack_start(GTK_BOX(vb),hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS);
+
+ /* Offset Slider and stuff */
+ hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_set_homogeneous(GTK_BOX(hb), FALSE);
+
+ /* Label */
+ GtkWidget *l = gtk_label_new(C_("Gradient","Offset:"));
+ gtk_widget_set_halign(l, GTK_ALIGN_END);
+
+ gtk_box_pack_start(GTK_BOX(hb),l, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS);
+ gtk_widget_show(l);
+
+ /* Adjustment */
+ GtkAdjustment *Offset_adj = nullptr;
+ Offset_adj= GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 1.0, 0.01, 0.01, 0.0));
+ g_object_set_data(G_OBJECT(vb), "offset", Offset_adj);
+
+ SPStop *stop = get_selected_stop(vb);
+ if (!stop) {
+ return nullptr;
+ }
+
+ gtk_adjustment_set_value(Offset_adj, stop->offset);
+
+ /* Slider */
+ auto slider = gtk_scale_new(GTK_ORIENTATION_HORIZONTAL, Offset_adj);
+ gtk_scale_set_draw_value( GTK_SCALE(slider), FALSE );
+ gtk_widget_show(slider);
+ gtk_box_pack_start(GTK_BOX(hb),slider, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS);
+ g_object_set_data(G_OBJECT(vb), "offslide", slider);
+
+ /* Spinbutton */
+ GtkWidget *sbtn = gtk_spin_button_new(GTK_ADJUSTMENT(Offset_adj), 0.01, 2);
+ sp_dialog_defocus_on_enter(sbtn);
+ gtk_widget_show(sbtn);
+ gtk_box_pack_start(GTK_BOX(hb),sbtn, FALSE, TRUE, AUX_BETWEEN_BUTTON_GROUPS);
+ g_object_set_data(G_OBJECT(vb), "offspn", sbtn);
+
+ if (stop->offset>0 && stop->offset<1) {
+ gtk_widget_set_sensitive(slider, TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(sbtn), TRUE);
+ } else {
+ gtk_widget_set_sensitive(slider, FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(sbtn), FALSE);
+ }
+
+
+ /* Signals */
+ g_signal_connect(G_OBJECT(Offset_adj), "value_changed",
+ G_CALLBACK(offadjustmentChanged), vb);
+
+ // g_signal_connect(G_OBJECT(slider), "changed", G_CALLBACK(offsliderChanged), vb);
+ gtk_widget_show(hb);
+ gtk_box_pack_start(GTK_BOX(vb), hb, FALSE, FALSE, PAD);
+
+ // TRANSLATORS: "Stop" means: a "phase" of a gradient
+ f = gtk_frame_new(_("Stop Color"));
+ gtk_widget_show(f);
+ gtk_box_pack_start(GTK_BOX(vb), f, TRUE, TRUE, PAD);
+
+ Inkscape::UI::SelectedColor *selected_color = new Inkscape::UI::SelectedColor;
+ g_object_set_data(G_OBJECT(vb), "cselector", selected_color);
+ g_object_set_data(G_OBJECT(vb), "updating_color", reinterpret_cast<void*>(0));
+ selected_color->signal_changed.connect(sigc::bind(sigc::ptr_fun(&sp_gradient_vector_color_changed), selected_color, G_OBJECT(vb)));
+ selected_color->signal_dragged.connect(sigc::bind(sigc::ptr_fun(&sp_gradient_vector_color_changed), selected_color, G_OBJECT(vb)));
+
+ Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(*selected_color));
+ color_selector->show();
+ gtk_container_add(GTK_CONTAINER(f), color_selector->gobj());
+
+ /*
+ gtk_widget_show(csel);
+ gtk_container_add(GTK_CONTAINER(f), csel);
+ g_signal_connect(G_OBJECT(csel), "dragged", G_CALLBACK(sp_gradient_vector_color_dragged), vb);
+ g_signal_connect(G_OBJECT(csel), "changed", G_CALLBACK(sp_gradient_vector_color_changed), vb);
+ */
+
+ gtk_widget_show(vb);
+
+ sp_gradient_vector_widget_load_gradient(vb, gradient);
+
+ if (select_stop) {
+ select_stop_in_list(GTK_WIDGET(vb), gradient, select_stop);
+ }
+
+ return vb;
+}
+
+
+
+GtkWidget * sp_gradient_vector_editor_new(SPGradient *gradient, SPStop *stop)
+{
+ if (dlg == nullptr) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ dlg = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title ((GtkWindow *) dlg, _("Gradient editor"));
+ gtk_window_set_resizable ((GtkWindow *) dlg, true);
+
+ if (x == -1000 || y == -1000) {
+ x = prefs->getInt(prefs_path + "x", -1000);
+ y = prefs->getInt(prefs_path + "y", -1000);
+ }
+ if (w ==0 || h == 0) {
+ w = prefs->getInt(prefs_path + "w", 0);
+ h = prefs->getInt(prefs_path + "h", 0);
+ }
+
+ if (x<0) {
+ x=0;
+ }
+ if (y<0) {
+ y=0;
+ }
+
+ if (x != 0 || y != 0) {
+ gtk_window_move(reinterpret_cast<GtkWindow *>(dlg), x, y);
+ } else {
+ gtk_window_set_position(GTK_WINDOW(dlg), GTK_WIN_POS_CENTER);
+ }
+ if (w && h) {
+ gtk_window_resize(reinterpret_cast<GtkWindow *>(dlg), w, h);
+ }
+ sp_transientize(dlg);
+ wd.win = dlg;
+ wd.stop = 0;
+
+ GObject *obj = G_OBJECT(dlg);
+ sigc::connection *conn = nullptr;
+
+ conn = new sigc::connection(INKSCAPE.signal_activate_desktop.connect(sigc::bind(sigc::ptr_fun(&sp_transientize_callback), &wd)));
+ g_object_set_data(obj, "desktop-activate-connection", conn);
+
+ g_signal_connect(obj, "event", G_CALLBACK(sp_dialog_event_handler), dlg);
+ g_signal_connect(obj, "destroy", G_CALLBACK(sp_gradient_vector_dialog_destroy), dlg);
+ g_signal_connect(obj, "delete_event", G_CALLBACK(sp_gradient_vector_dialog_delete), dlg);
+
+ conn = new sigc::connection(INKSCAPE.signal_shut_down.connect(
+ sigc::hide_return(
+ sigc::bind(sigc::ptr_fun(&sp_gradient_vector_dialog_delete), (GtkWidget *) nullptr, (GdkEvent *) nullptr, (GtkWidget *) nullptr)
+ )));
+ g_object_set_data(obj, "shutdown-connection", conn);
+
+ conn = new sigc::connection(INKSCAPE.signal_dialogs_hide.connect(sigc::bind(sigc::ptr_fun(&gtk_widget_hide), dlg)));
+ g_object_set_data(obj, "dialog-hide-connection", conn);
+
+ conn = new sigc::connection(INKSCAPE.signal_dialogs_unhide.connect(sigc::bind(sigc::ptr_fun(&gtk_widget_show), dlg)));
+ g_object_set_data(obj, "dialog-unhide-connection", conn);
+
+ gtk_container_set_border_width(GTK_CONTAINER(dlg), PAD);
+
+ GtkWidget *wid = static_cast<GtkWidget*>(sp_gradient_vector_widget_new(gradient, stop));
+ g_object_set_data(G_OBJECT(dlg), "gradient-vector-widget", wid);
+ /* Connect signals */
+ gtk_widget_show(wid);
+ gtk_container_add(GTK_CONTAINER(dlg), wid);
+ } else {
+ // FIXME: temp fix for 0.38
+ // Simply load_gradient into the editor does not work for multi-stop gradients,
+ // as the stop list and other widgets are in a wrong state and crash readily.
+ // Instead we just delete the window (by sending the delete signal)
+ // and call sp_gradient_vector_editor_new again, so it creates the window anew.
+
+ GdkEventAny event;
+ GtkWidget *widget = static_cast<GtkWidget *>(dlg);
+ event.type = GDK_DELETE;
+ event.window = gtk_widget_get_window (widget);
+ event.send_event = TRUE;
+ g_object_ref(G_OBJECT(event.window));
+ gtk_main_do_event(reinterpret_cast<GdkEvent*>(&event));
+ g_object_unref(G_OBJECT(event.window));
+
+ g_assert(dlg == nullptr);
+ sp_gradient_vector_editor_new(gradient, stop);
+ }
+
+ return dlg;
+}
+
+static void sp_gradient_vector_widget_load_gradient(GtkWidget *widget, SPGradient *gradient)
+{
+ blocked = TRUE;
+
+ SPGradient *old;
+
+ old = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(widget), "gradient"));
+
+ if (old != gradient) {
+ sigc::connection *release_connection;
+ sigc::connection *modified_connection;
+
+ release_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(widget), "gradient_release_connection"));
+ modified_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(widget), "gradient_modified_connection"));
+
+ if (old) {
+ g_assert( release_connection != nullptr );
+ g_assert( modified_connection != nullptr );
+ release_connection->disconnect();
+ modified_connection->disconnect();
+ sp_signal_disconnect_by_data(old, widget);
+ }
+
+ if (gradient) {
+ if (!release_connection) {
+ release_connection = new sigc::connection();
+ }
+ if (!modified_connection) {
+ modified_connection = new sigc::connection();
+ }
+ *release_connection = gradient->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gradient_vector_gradient_release), widget));
+ *modified_connection = gradient->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gradient_vector_gradient_modified), widget));
+ } else {
+ if (release_connection) {
+ delete release_connection;
+ release_connection = nullptr;
+ }
+ if (modified_connection) {
+ delete modified_connection;
+ modified_connection = nullptr;
+ }
+ }
+
+ g_object_set_data(G_OBJECT(widget), "gradient_release_connection", release_connection);
+ g_object_set_data(G_OBJECT(widget), "gradient_modified_connection", modified_connection);
+ }
+
+ g_object_set_data(G_OBJECT(widget), "gradient", gradient);
+
+ if (gradient) {
+ gtk_widget_set_sensitive(widget, TRUE);
+
+ gradient->ensureVector();
+
+ SPStop *stop = get_selected_stop(widget);
+ if (!stop) {
+ return;
+ }
+
+ // get the color selector
+ SelectedColor *csel = static_cast<SelectedColor*>(g_object_get_data(G_OBJECT(widget), "cselector"));
+
+ g_object_set_data(G_OBJECT(widget), "updating_color", reinterpret_cast<void*>(1));
+ csel->setColorAlpha(stop->getColor(), stop->getOpacity());
+ g_object_set_data(G_OBJECT(widget), "updating_color", reinterpret_cast<void*>(0));
+
+ /* Fill preview */
+ GtkWidget *w = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(widget), "preview"));
+ sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(w), gradient);
+
+ update_stop_list(GTK_WIDGET(widget), gradient, nullptr);
+
+ // Once the user edits a gradient, it stops being auto-collectable
+ if (gradient->getRepr()->attribute("inkscape:collect")) {
+ SPDocument *document = gradient->document;
+ DocumentUndo::ScopedInsensitive _no_undo(document);
+ gradient->removeAttribute("inkscape:collect");
+ }
+ } else { // no gradient, disable everything
+ gtk_widget_set_sensitive(widget, FALSE);
+ }
+
+ blocked = FALSE;
+}
+
+static void sp_gradient_vector_dialog_destroy(GtkWidget * /*object*/, gpointer /*data*/)
+{
+ GObject *obj = G_OBJECT(dlg);
+ assert(obj != NULL);
+
+ sigc::connection *conn = static_cast<sigc::connection *>(g_object_get_data(obj, "desktop-activate-connection"));
+ assert(conn != NULL);
+ conn->disconnect();
+ delete conn;
+
+ conn = static_cast<sigc::connection *>(g_object_get_data(obj, "shutdown-connection"));
+ assert(conn != NULL);
+ conn->disconnect();
+ delete conn;
+
+ conn = static_cast<sigc::connection *>(g_object_get_data(obj, "dialog-hide-connection"));
+ assert(conn != NULL);
+ conn->disconnect();
+ delete conn;
+
+ conn = static_cast<sigc::connection *>(g_object_get_data(obj, "dialog-unhide-connection"));
+ assert(conn != NULL);
+ conn->disconnect();
+ delete conn;
+
+ wd.win = dlg = nullptr;
+ wd.stop = 0;
+}
+
+static gboolean sp_gradient_vector_dialog_delete(GtkWidget */*widget*/, GdkEvent */*event*/, GtkWidget */*dialog*/)
+{
+ gtk_window_get_position(GTK_WINDOW(dlg), &x, &y);
+ gtk_window_get_size(GTK_WINDOW(dlg), &w, &h);
+
+ if (x<0) {
+ x=0;
+ }
+ if (y<0) {
+ y=0;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt(prefs_path + "x", x);
+ prefs->setInt(prefs_path + "y", y);
+ prefs->setInt(prefs_path + "w", w);
+ prefs->setInt(prefs_path + "h", h);
+
+ return FALSE; // which means, go ahead and destroy it
+}
+
+/* Widget destroy handler */
+static void sp_gradient_vector_widget_destroy(GtkWidget *object, gpointer /*data*/)
+{
+ SPObject *gradient = SP_OBJECT(g_object_get_data(G_OBJECT(object), "gradient"));
+
+ sigc::connection *release_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(object), "gradient_release_connection"));
+ sigc::connection *modified_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(object), "gradient_modified_connection"));
+
+ if (gradient) {
+ g_assert( release_connection != nullptr );
+ g_assert( modified_connection != nullptr );
+ release_connection->disconnect();
+ modified_connection->disconnect();
+ sp_signal_disconnect_by_data(gradient, object);
+
+ if (gradient->getRepr()) {
+ sp_repr_remove_listener_by_data(gradient->getRepr(), object);
+ }
+ }
+
+ SelectedColor *selected_color = static_cast<SelectedColor *>(g_object_get_data(G_OBJECT(object), "cselector"));
+ if (selected_color) {
+ delete selected_color;
+ g_object_set_data(G_OBJECT(object), "cselector", nullptr);
+ }
+}
+
+static void sp_gradient_vector_gradient_release(SPObject */*object*/, GtkWidget *widget)
+{
+ sp_gradient_vector_widget_load_gradient(widget, nullptr);
+}
+
+static void sp_gradient_vector_gradient_modified(SPObject *object, guint /*flags*/, GtkWidget *widget)
+{
+ SPGradient *gradient=SP_GRADIENT(object);
+ if (!blocked) {
+ blocked = TRUE;
+ sp_gradient_vector_widget_load_gradient(widget, gradient);
+ blocked = FALSE;
+ }
+}
+
+static void sp_gradient_vector_color_dragged(Inkscape::UI::SelectedColor *selected_color, GObject *object)
+{
+ SPGradient *gradient, *ngr;
+
+ if (blocked) {
+ return;
+ }
+
+ gradient = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(object), "gradient"));
+ if (!gradient) {
+ return;
+ }
+
+ blocked = TRUE;
+
+ ngr = sp_gradient_ensure_vector_normalized(gradient);
+ if (ngr != gradient) {
+ /* Our master gradient has changed */
+ sp_gradient_vector_widget_load_gradient(GTK_WIDGET(object), ngr);
+ }
+
+ ngr->ensureVector();
+
+ SPStop *stop = get_selected_stop(GTK_WIDGET(object));
+ if (!stop) {
+ return;
+ }
+
+ SPColor color = stop->getColor();
+ gfloat opacity = stop->getOpacity();
+ selected_color->colorAlpha(color, opacity);
+ stop->style->stop_color.currentcolor = false;
+
+ blocked = FALSE;
+}
+
+static void sp_gradient_vector_color_changed(Inkscape::UI::SelectedColor *selected_color, GObject *object)
+{
+ (void)selected_color;
+
+ void* updating_color = g_object_get_data(G_OBJECT(object), "updating_color");
+ if (updating_color) {
+ return;
+ }
+
+ if (blocked) {
+ return;
+ }
+
+ SPGradient *gradient = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(object), "gradient"));
+ if (!gradient) {
+ return;
+ }
+
+ blocked = TRUE;
+
+ SPGradient *ngr = sp_gradient_ensure_vector_normalized(gradient);
+ if (ngr != gradient) {
+ /* Our master gradient has changed */
+ sp_gradient_vector_widget_load_gradient(GTK_WIDGET(object), ngr);
+ }
+
+ ngr->ensureVector();
+
+ /* Set start parameters */
+ /* We rely on normalized vector, i.e. stops HAVE to exist */
+ g_return_if_fail(ngr->getFirstStop() != nullptr);
+
+ SPStop *stop = get_selected_stop(GTK_WIDGET(object));
+ if (!stop) {
+ return;
+ }
+
+ SelectedColor *csel = static_cast<SelectedColor *>(g_object_get_data(G_OBJECT(object), "cselector"));
+ SPColor color;
+ float alpha = 0;
+ csel->colorAlpha(color, alpha);
+
+ sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset);
+ Inkscape::CSSOStringStream os;
+ os << "stop-color:" << color.toString() << ";stop-opacity:" << static_cast<gdouble>(alpha) <<";";
+ stop->setAttribute("style", os.str());
+ // g_snprintf(c, 256, "stop-color:#%06x;stop-opacity:%g;", rgb >> 8, static_cast<gdouble>(alpha));
+ //stop->setAttribute("style", c);
+
+ DocumentUndo::done(ngr->document, SP_VERB_CONTEXT_GRADIENT,
+ _("Change gradient stop color"));
+
+ blocked = FALSE;
+
+ // Set the color in the selected stop after change
+ GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(object), "combo_box"));
+ if (combo_box) {
+ GtkTreeIter iter;
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo_box), &iter)) {
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box)));
+
+ Inkscape::UI::Widget::ColorPreview *cp = Gtk::manage(new Inkscape::UI::Widget::ColorPreview(stop->get_rgba32()));
+ GdkPixbuf *pb = cp->toPixbuf(64, 16);
+
+ gtk_list_store_set (store, &iter, 0, pb, /*1, repr->attribute("id"),*/ 2, stop, -1);
+ }
+ }
+
+}
+
+/*
+ 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/gradient-vector.h b/src/widgets/gradient-vector.h
new file mode 100644
index 0000000..c30cb32
--- /dev/null
+++ b/src/widgets/gradient-vector.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_VECTOR_H
+#define SEEN_GRADIENT_VECTOR_H
+
+/*
+ * Gradient vector selection widget
+ *
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ * Copyright (C) 2001-2002 Lauris Kaplinski
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/liststore.h>
+#include <sigc++/connection.h>
+#include "gradient-selector.h"
+
+#define SP_TYPE_GRADIENT_VECTOR_SELECTOR (sp_gradient_vector_selector_get_type ())
+#define SP_GRADIENT_VECTOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_VECTOR_SELECTOR, SPGradientVectorSelector))
+#define SP_GRADIENT_VECTOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_VECTOR_SELECTOR, SPGradientVectorSelectorClass))
+#define SP_IS_GRADIENT_VECTOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_VECTOR_SELECTOR))
+#define SP_IS_GRADIENT_VECTOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_VECTOR_SELECTOR))
+
+class SPDocument;
+class SPObject;
+class SPGradient;
+class SPStop;
+
+struct SPGradientVectorSelector {
+ GtkBox vbox;
+
+ guint idlabel : 1;
+
+ bool swatched;
+
+ SPDocument *doc;
+ SPGradient *gr;
+
+ /* Gradient vectors store */
+ Glib::RefPtr<Gtk::ListStore> store;
+ SPGradientSelector::ModelColumns *columns;
+
+ sigc::connection gradient_release_connection;
+ sigc::connection defs_release_connection;
+ sigc::connection defs_modified_connection;
+ sigc::connection tree_select_connection;
+
+ void setSwatched();
+};
+
+struct SPGradientVectorSelectorClass {
+ GtkBoxClass parent_class;
+
+ void (* vector_set) (SPGradientVectorSelector *gvs, SPGradient *gr);
+};
+
+GType sp_gradient_vector_selector_get_type();
+
+GtkWidget *sp_gradient_vector_selector_new (SPDocument *doc, SPGradient *gradient);
+
+void sp_gradient_vector_selector_set_gradient (SPGradientVectorSelector *gvs, SPDocument *doc, SPGradient *gr);
+
+SPDocument *sp_gradient_vector_selector_get_document (SPGradientVectorSelector *gvs);
+SPGradient *sp_gradient_vector_selector_get_gradient (SPGradientVectorSelector *gvs);
+
+/* fixme: rethink this (Lauris) */
+GtkWidget *sp_gradient_vector_editor_new (SPGradient *gradient, SPStop *stop = nullptr);
+
+guint32 sp_average_color(guint32 c1, guint32 c2, gdouble p = 0.5);
+
+Glib::ustring gr_prepare_label (SPObject *obj);
+Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen);
+
+#endif // SEEN_GRADIENT_VECTOR_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/ink-action.cpp b/src/widgets/ink-action.cpp
new file mode 100644
index 0000000..0696228
--- /dev/null
+++ b/src/widgets/ink-action.cpp
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include "ink-action.h"
+#include "ui/icon-loader.h"
+#include <gtk/gtk.h>
+
+static void ink_action_finalize( GObject* obj );
+static void ink_action_get_property( GObject* obj, guint propId, GValue* value, GParamSpec * pspec );
+static void ink_action_set_property( GObject* obj, guint propId, const GValue *value, GParamSpec* pspec );
+
+static GtkWidget* ink_action_create_menu_item( GtkAction* action );
+static GtkWidget* ink_action_create_tool_item( GtkAction* action );
+
+typedef struct
+{
+ gchar* iconId;
+ GtkIconSize iconSize;
+} InkActionPrivate;
+
+#define INK_ACTION_GET_PRIVATE( o ) \
+ reinterpret_cast<InkActionPrivate *>(ink_action_get_instance_private (o))
+
+G_DEFINE_TYPE_WITH_PRIVATE(InkAction, ink_action, GTK_TYPE_ACTION);
+
+enum {
+ PROP_INK_ID = 1,
+ PROP_INK_SIZE
+};
+
+static void ink_action_class_init( InkActionClass* klass )
+{
+ if ( klass ) {
+ GObjectClass * objClass = G_OBJECT_CLASS( klass );
+
+ objClass->finalize = ink_action_finalize;
+ objClass->get_property = ink_action_get_property;
+ objClass->set_property = ink_action_set_property;
+
+ klass->parent_class.create_menu_item = ink_action_create_menu_item;
+ klass->parent_class.create_tool_item = ink_action_create_tool_item;
+ /*klass->parent_class.connect_proxy = connect_proxy;*/
+ /*klass->parent_class.disconnect_proxy = disconnect_proxy;*/
+
+ g_object_class_install_property( objClass,
+ PROP_INK_ID,
+ g_param_spec_string( "iconId",
+ "Icon ID",
+ "The id for the icon",
+ "",
+ (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) );
+
+ g_object_class_install_property( objClass,
+ PROP_INK_SIZE,
+ g_param_spec_int( "iconSize",
+ "Icon Size",
+ "The size the icon",
+ (int)GTK_ICON_SIZE_MENU,
+ (int)GTK_ICON_SIZE_DIALOG,
+ (int)GTK_ICON_SIZE_SMALL_TOOLBAR,
+ (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) );
+ }
+}
+
+static void ink_action_init( InkAction* action )
+{
+ auto priv = INK_ACTION_GET_PRIVATE (action);
+ priv->iconId = nullptr;
+ priv->iconSize = GTK_ICON_SIZE_SMALL_TOOLBAR;
+}
+
+static void ink_action_finalize( GObject* obj )
+{
+ InkAction* action = INK_ACTION( obj );
+ auto priv = INK_ACTION_GET_PRIVATE (action);
+
+ g_free( priv->iconId );
+ g_free( priv );
+
+}
+
+//Any strings passed in should already be localised
+InkAction* ink_action_new( const gchar *name,
+ const gchar *label,
+ const gchar *tooltip,
+ const gchar *inkId,
+ GtkIconSize size )
+{
+ GObject* obj = (GObject*)g_object_new( INK_ACTION_TYPE,
+ "name", name,
+ "label", label,
+ "tooltip", tooltip,
+ "iconId", inkId,
+ "iconSize", size,
+ NULL );
+
+ InkAction* action = INK_ACTION( obj );
+
+ return action;
+}
+
+static void ink_action_get_property( GObject* obj, guint propId, GValue* value, GParamSpec * pspec )
+{
+ InkAction* action = INK_ACTION( obj );
+ auto priv = INK_ACTION_GET_PRIVATE (action);
+
+ switch ( propId ) {
+ case PROP_INK_ID:
+ {
+ g_value_set_string( value, priv->iconId );
+ }
+ break;
+
+ case PROP_INK_SIZE:
+ {
+ g_value_set_int( value, priv->iconSize );
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( obj, propId, pspec );
+ }
+}
+
+void ink_action_set_property( GObject* obj, guint propId, const GValue *value, GParamSpec* pspec )
+{
+ InkAction* action = INK_ACTION( obj );
+ auto priv = INK_ACTION_GET_PRIVATE (action);
+
+ switch ( propId ) {
+ case PROP_INK_ID:
+ {
+ gchar* tmp = priv->iconId;
+ priv->iconId = g_value_dup_string( value );
+ g_free( tmp );
+ }
+ break;
+
+ case PROP_INK_SIZE:
+ {
+ priv->iconSize = (GtkIconSize)g_value_get_int( value );
+ }
+ break;
+
+ default:
+ {
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( obj, propId, pspec );
+ }
+ }
+}
+
+static GtkWidget* ink_action_create_menu_item( GtkAction* action )
+{
+ InkAction* act = INK_ACTION( action );
+ GtkWidget* item = GTK_ACTION_CLASS(ink_action_parent_class)->create_menu_item( action );
+
+ return item;
+}
+
+static GtkWidget* ink_action_create_tool_item( GtkAction* action )
+{
+ InkAction* act = INK_ACTION( action );
+ auto priv = INK_ACTION_GET_PRIVATE (act);
+ GtkWidget* item = GTK_ACTION_CLASS(ink_action_parent_class)->create_tool_item(action);
+
+ if ( priv->iconId ) {
+ if ( GTK_IS_TOOL_BUTTON(item) ) {
+ GtkToolButton* button = GTK_TOOL_BUTTON(item);
+
+ GtkWidget *child = sp_get_icon_image(priv->iconId, priv->iconSize);
+ gtk_tool_button_set_icon_widget( button, child );
+ } else {
+ // For now trigger a warning but don't do anything else
+ GtkToolButton* button = GTK_TOOL_BUTTON(item);
+ (void)button;
+ }
+ }
+
+ // TODO investigate if needed
+ gtk_widget_show_all( item );
+
+ return item;
+}
+
+/*
+ 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/ink-action.h b/src/widgets/ink-action.h
new file mode 100644
index 0000000..d97afe1
--- /dev/null
+++ b/src/widgets/ink-action.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2017 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_INK_ACTION
+#define SEEN_INK_ACTION
+
+#include <gtk/gtk.h>
+#include "attributes.h"
+
+/* Equivalent to GTK Actions of the same type, but can support Inkscape SVG icons */
+
+G_BEGIN_DECLS
+
+#define INK_ACTION_TYPE ( ink_action_get_type() )
+#define INK_ACTION( obj ) ( G_TYPE_CHECK_INSTANCE_CAST( (obj), INK_ACTION_TYPE, InkAction) )
+#define INK_ACTION_CLASS( klass ) ( G_TYPE_CHECK_CLASS_CAST( (klass), INK_ACTION_TYPE, InkActionClass) )
+#define IS_INK_ACTION( obj ) ( G_TYPE_CHECK_INSTANCE_TYPE( (obj), INK_ACTION_TYPE) )
+#define IS_INK_ACTION_CLASS( klass ) ( G_TYPE_CHECK_CLASS_TYPE( (klass), INK_ACTION_TYPE) )
+#define INK_ACTION_GET_CLASS( obj ) ( G_TYPE_INSTANCE_GET_CLASS( (obj), INK_ACTION_TYPE, InkActionClass) )
+
+typedef struct _InkAction InkAction;
+typedef struct _InkActionClass InkActionClass;
+
+struct _InkAction
+{
+ GtkAction action;
+};
+
+struct _InkActionClass
+{
+ GtkActionClass parent_class;
+};
+
+GType ink_action_get_type( void );
+
+InkAction* ink_action_new( const gchar *name,
+ const gchar *label,
+ const gchar *tooltip,
+ const gchar *inkId,
+ GtkIconSize size );
+
+
+G_END_DECLS
+
+#endif /* SEEN_INK_ACTION */
+/*
+ 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/mappings.xml b/src/widgets/mappings.xml
new file mode 100644
index 0000000..3ea9cb3
--- /dev/null
+++ b/src/widgets/mappings.xml
@@ -0,0 +1,334 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<mappings>
+
+ <!-- file menu -->
+ <remap id='file_import' newid='document-import'/>
+ <remap id='file_export' newid='document-export'/>
+ <remap id='ocal_import' newid='document-import-ocal'/>
+ <remap id='ocal_export' newid='document-export-ocal'/>
+ <remap id='document_metadata' newid='document-metadata'/>
+ <remap id='input_devices' newid='dialog-input-devices'/>
+
+ <!-- edit menu -->
+ <remap id='edit_duplicate' newid='edit-duplicate'/>
+ <remap id='edit_clone' newid='edit-clone'/>
+ <remap id='edit_unlink_clone' newid='edit-clone-unlink'/>
+ <remap id='edit_select_original' newid='edit-select-original'/>
+ <remap id='edit_undo_history' newid='edit-undo-history'/>
+ <remap id='selection_paste_in_place' newid='edit-paste-in-place'/>
+ <remap id='selection_paste_style' newid='edit-paste-style'/>
+ <remap id='selection_bitmap' newid='selection-make-bitmap-copy'/>
+ <remap id='selection_select_all' newid='edit-select-all'/>
+ <remap id='selection_select_all_in_all_layers' newid='edit-select-all-layers'/>
+ <remap id='selection_invert' newid='edit-select-invert'/>
+ <remap id='selection_deselect' newid='edit-select-none'/>
+ <remap id='xml_editor' newid='dialog-xml-editor'/>
+
+ <!-- view menu -->
+ <!-- submenu: zoom -->
+ <remap id='zoom_1_to_1' newid='zoom-original'/>
+ <remap id='zoom_1_to_2' newid='zoom-half-size'/>
+ <remap id='zoom_2_to_1' newid='zoom-double-size'/>
+ <remap id='zoom_select' newid='zoom-fit-selection'/>
+ <remap id='zoom_draw' newid='zoom-fit-drawing'/>
+ <remap id='zoom_page' newid='zoom-fit-page'/>
+ <remap id='zoom_pagewidth' newid='zoom-fit-width'/>
+ <remap id='zoom_previous' newid='zoom-previous'/>
+ <remap id='zoom_next' newid='zoom-next'/>
+ <remap id='zoom_in' newid='zoom-in'/>
+ <remap id='zoom_out' newid='zoom-out'/>
+
+ <remap id='grid' newid='show-grid'/>
+ <remap id='guides' newid='show-guides'/>
+ <remap id='color_management' newid='color-management'/>
+ <remap id='dialog_toggle' newid='show-dialogs'/>
+ <remap id='messages' newid='dialog-messages'/>
+ <remap id='scripts' newid='dialog-scripts'/>
+ <remap id='window_previous' newid='window-previous'/>
+ <remap id='window_next' newid='window-next'/>
+ <remap id='view_icon_preview' newid='dialog-icon-preview'/>
+ <remap id='view_new' newid='window-new'/>
+ <remap id='fullscreen' newid='view-fullscreen'/>
+
+ <!-- layers menu -->
+ <remap id='new_layer' newid='layer-new'/>
+ <remap id='rename_layer' newid='layer-rename'/>
+ <remap id='switch_to_layer_above' newid='layer-previous'/>
+ <remap id='switch_to_layer_below' newid='layer-next'/>
+ <remap id='move_selection_above' newid='selection-move-to-layer-above'/>
+ <remap id='move_selection_below' newid='selection-move-to-layer-below'/>
+ <remap id='raise_layer' newid='layer-raise'/>
+ <remap id='lower_layer' newid='layer-lower'/>
+ <remap id='layer_to_top' newid='layer-top'/>
+ <remap id='layer_to_bottom' newid='layer-bottom'/>
+ <remap id='delete_layer' newid='layer-delete'/>
+ <remap id='layers' newid='dialog-layers'/>
+
+ <!-- object menu -->
+ <remap id='fill_and_stroke' newid='dialog-fill-and-stroke'/>
+ <remap id='dialog_item_properties' newid='dialog-object-properties'/>
+ <remap id='selection_group' newid='object-group'/>
+ <remap id='selection_ungroup' newid='object-ungroup'/>
+ <remap id='selection_up' newid='selection-raise'/>
+ <remap id='selection_down' newid='selection-lower'/>
+ <remap id='selection_top' newid='selection-top'/>
+ <remap id='selection_bot' newid='selection-bottom'/>
+ <remap id='object_rotate_90_CCW' newid='object-rotate-left'/>
+ <remap id='object_rotate_90_CW' newid='object-rotate-right'/>
+ <remap id='object_flip_hor' newid='object-flip-horizontal'/>
+ <remap id='object_flip_ver' newid='object-flip-vertical'/>
+ <remap id='object_trans' newid='dialog-transform'/>
+ <remap id='object_align' newid='dialog-align-and-distribute'/>
+ <remap id='grid_arrange' newid='dialog-rows-and-columns'/>
+
+ <!-- path menu -->
+ <remap id='object_tocurve' newid='object-to-path'/>
+ <remap id='stroke_tocurve' newid='stroke-to-path'/>
+ <remap id='selection_trace' newid='bitmap-trace'/>
+ <remap id='union' newid='path-union'/>
+ <remap id='difference' newid='path-difference'/>
+ <remap id='intersection' newid='path-intersection'/>
+ <remap id='exclusion' newid='path-exclusion'/>
+ <remap id='division' newid='path-division'/>
+ <remap id='cut_path' newid='path-cut'/>
+ <remap id='selection_combine' newid='path-combine'/>
+ <remap id='selection_break' newid='path-break-apart'/>
+ <remap id='outset_path' newid='path-outset'/>
+ <remap id='inset_path' newid='path-inset'/>
+ <remap id='dynamic_offset' newid='path-offset-dynamic'/>
+ <remap id='linked_offset' newid='path-offset-linked'/>
+ <remap id='simplify' newid='path-simplify'/>
+ <remap id='selection_reverse' newid='path-reverse'/>
+
+
+ <!-- text menu -->
+ <remap id='object_font' newid='dialog-text-and-font'/>
+ <remap id='put_on_path' newid='text-put-on-path'/>
+ <remap id='remove_from_path' newid='text-remove-from-path'/>
+ <remap id='flow_into_frame' newid='text-flow-into-frame'/>
+ <remap id='unflow' newid='text-unflow'/>
+ <remap id='convert_to_text' newid='text-convert-to-regular'/>
+ <remap id='remove_manual_kerns' newid='text-unkern'/>
+
+ <!-- help menu -->
+ <remap id='help_keys' newid='help-keyboard-shortcuts'/>
+ <remap id='help_tutorials' newid='help-contents'/>
+ <remap id='inkscape_options' newid='inkscape-logo'/>
+ <remap id='about_memory' newid='dialog-memory'/>
+
+ <!-- tools -->
+ <remap id='draw_select' newid='tool-pointer'/>
+ <remap id='draw_node' newid='tool-node-editor'/>
+ <remap id='draw_tweak' newid='tool-tweak'/>
+ <remap id='draw_zoom' newid='zoom'/>
+ <remap id='draw_rect' newid='draw-rectangle'/>
+ <remap id='draw_3dbox' newid='draw-cuboid'/>
+ <remap id='draw_arc' newid='draw-ellipse'/>
+ <remap id='draw_star' newid='draw-polygon-star'/>
+ <remap id='draw_spiral' newid='draw-spiral'/>
+ <remap id='draw_freehand' newid='draw-freehand'/>
+ <remap id='draw_pen' newid='draw-path'/>
+ <remap id='draw_calligraphic' newid='draw-calligraphic'/>
+ <remap id='draw_erase' newid='draw-eraser'/>
+ <remap id='draw_paintbucket' newid='color-fill'/>
+ <remap id='draw_text' newid='draw-text'/>
+ <remap id='draw_connector' newid='draw-connector'/>
+ <remap id='draw_gradient' newid='color-gradient'/>
+ <remap id='draw_dropper' newid='color-picker'/>
+
+ <!-- TOOLBARS -->
+ <!-- select toolbar -->
+ <remap id='transform_stroke' newid='transform-affect-stroke'/>
+ <remap id='transform_corners' newid='transform-affect-rounded-corners'/>
+ <remap id='transform_gradient' newid='transform-affect-gradient'/>
+ <remap id='transform_pattern' newid='transform-affect-pattern'/>
+
+ <!-- node editor toolbar -->
+ <remap id='node_insert' newid='node-add'/>
+ <remap id='node_delete' newid='node-delete'/>
+ <remap id='node_join' newid='node-join'/>
+ <remap id='node_break' newid='node-break'/>
+ <remap id='node_join_segment' newid='node-join-segment'/>
+ <remap id='node_delete_segment' newid='node-delete-segment'/>
+ <remap id='node_cusp' newid='node-type-cusp'/>
+ <remap id='node_smooth' newid='node-type-smooth'/>
+ <remap id='node_symmetric' newid='node-type-symmetric'/>
+ <remap id='node_auto' newid='node-type-auto-smooth'/>
+ <remap id='node_curve' newid='node-segment-curve'/>
+ <remap id='node_line' newid='node-segment-line'/>
+ <remap id='nodes_show_handles' newid='show-node-handles'/>
+ <remap id='edit_next_parameter' newid='path-effect-parameter-next'/>
+ <remap id='nodes_show_helperpath' newid='show-path-outline'/>
+ <remap id='nodeedit-clippath' newid='path-clip-edit'/>
+ <remap id='nodeedit-mask' newid='path-mask-edit'/>
+ <remap id='node_cusp' newid='node-type-cusp'/>
+
+ <!-- tweak toolbar -->
+ <remap id='tweak_move_mode' newid='object-tweak-push'/>
+ <remap id='tweak_move_mode_inout' newid='object-tweak-attract'/>
+ <remap id='tweak_move_mode_jitter' newid='object-tweak-randomize'/>
+ <remap id='tweak_scale_mode' newid='object-tweak-shrink'/>
+ <remap id='tweak_rotate_mode' newid='object-tweak-rotate'/>
+ <remap id='tweak_moreless_mode' newid='object-tweak-duplicate'/>
+ <remap id='tweak_move_mode' newid='object-tweak-push'/>
+ <remap id='tweak_push_mode' newid='path-tweak-push'/>
+ <remap id='tweak_shrink_mode' newid='path-tweak-shrink'/>
+ <remap id='tweak_attract_mode' newid='path-tweak-attract'/>
+ <remap id='tweak_roughen_mode' newid='path-tweak-roughen'/>
+ <remap id='tweak_colorpaint_mode' newid='object-tweak-paint'/>
+ <remap id='tweak_colorjitter_mode' newid='object-tweak-jitter-color'/>
+ <remap id='tweak_blur_mode' newid='object-tweak-blur'/>
+
+ <!-- rectangle toolbar -->
+ <remap id='squared_corner' newid='rectangle-make-corners-sharp'/>
+
+ <!-- cuboid toolbar -->
+ <remap id='toggle_vp_x' newid='perspective-parallel'/>
+
+ <!-- ellipse toolbar -->
+ <remap id='reset_circle' newid='draw-ellipse-whole'/>
+ <remap id='circle_closed_arc' newid='draw-ellipse-segment'/>
+ <remap id='circle_open_arc' newid='draw-ellipse-arc'/>
+
+ <!-- polygon toolbar -->
+ <remap id='star_flat' newid='draw-polygon'/>
+ <remap id='star_angled' newid='draw-star'/>
+
+ <!-- bezier toolbar -->
+ <remap id='bezier_mode' newid='path-mode-bezier'/>
+ <remap id='spiro_splines_mode' newid='path-mode-spiro'/>
+ <remap id='bspline_mode' newid='path-mode-bspline'/>
+ <remap id='polylines_mode' newid='path-mode-polyline'/>
+ <remap id='paraxial_lines_mode' newid='path-mode-polyline-paraxial'/>
+
+ <!-- calligraphic toolbar -->
+ <remap id='guse_tilt' newid='draw-use-tilt'/>
+ <remap id='guse_pressure' newid='draw-use-pressure'/>
+ <remap id='trace_background' newid='draw-trace-background'/>
+
+ <!-- eraser toolbar -->
+ <remap id='delete_object' newid='draw-eraser-delete-objects'/>
+
+ <!-- text toolbar -->
+ <remap id='writing_mode_tb' newid='format-text-direction-vertical'/>
+ <remap id='writing_mode_lr' newid='format-text-direction-horizontal'/>
+
+ <!-- connector toolbar -->
+ <remap id='connector_avoid' newid='connector-avoid'/>
+ <remap id='connector_ignore' newid='connector-ignore'/>
+
+ <!-- gradient toolbar -->
+ <remap id='controls_fill' newid='object-fill'/>
+ <remap id='controls_stroke' newid='object-stroke'/>
+
+ <!-- lpe toolbar -->
+ <remap id='lpetool_show_bbox' newid='show-bounding-box'/>
+ <remap id='all_inactive_old' newid='draw-geometry-inactive'/>
+ <remap id='angle_bisector' newid='draw-geometry-angle-bisector'/>
+ <remap id='circle_3pts' newid='draw-geometry-circle-from-three-points'/>
+ <remap id='line_segment' newid='draw-geometry-line-segment'/>
+ <remap id='mirror_symmetry' newid='draw-geometry-mirror'/>
+ <remap id='parralel' newid='draw-geometry-line-parallel'/>
+ <remap id='perp_bisector' newid='draw-geometry-line-perpendicular'/>
+
+ <!-- snapping toolbar -->
+ <remap id='toggle_snap_global' newid='snap'/>
+ <remap id='toggle_snap_bbox' newid='snap-bounding-box'/>
+ <remap id='toggle_snap_to_bbox_path' newid='snap-bounding-box-edges'/>
+ <remap id='toggle_snap_to_bbox_node' newid='snap-bounding-box-corners'/>
+ <remap id='toggle_snap_to_bbox_edge_midpoints' newid='snap-bounding-box-midpoints'/>
+ <remap id='toggle_snap_to_bbox_midpoints' newid='snap-bounding-box-center'/>
+ <remap id='toggle_snap_nodes' newid='snap-nodes'/>
+ <remap id='toggle_snap_to_paths' newid='snap-nodes-path'/>
+ <remap id='toggle_snap_to_nodes' newid='snap-nodes-cusp'/>
+ <remap id='toggle_snap_to_smooth_nodes' newid='snap-nodes-smooth'/>
+ <remap id='toggle_snap_to_midpoints' newid='snap-nodes-midpoint'/>
+ <remap id='toggle_snap_to_path_intersections' newid='snap-nodes-intersection'/>
+ <remap id='toggle_snap_to_bbox_midpoints-3' newid='snap-nodes-center'/>
+ <remap id='toggle_snap_center' newid='snap-nodes-rotation-center'/>
+ <remap id='toggle_snap_page_border' newid='snap-page'/>
+ <remap id='toggle_snap_grid_guide_intersections' newid='snap-grid-guide-intersections'/>
+
+ <!-- DIALOGS -->
+ <!-- align and distribute dialog -->
+ <remap id='al_left_out' newid='align-horizontal-right-to-anchor'/>
+ <remap id='al_left_in' newid='align-horizontal-left'/>
+ <remap id='al_center_hor' newid='align-horizontal-center'/>
+ <remap id='al_right_in' newid='align-horizontal-right'/>
+ <remap id='al_right_out' newid='align-horizontal-left-to-anchor'/>
+ <remap id='al_baselines_vert' newid='align-horizontal-baseline'/>
+
+ <remap id='al_top_out' newid='align-vertical-bottom-to-anchor'/>
+ <remap id='al_top_in' newid='align-vertical-top'/>
+ <remap id='al_center_ver' newid='align-vertical-center'/>
+ <remap id='al_bottom_in' newid='align-vertical-bottom'/>
+ <remap id='al_bottom_out' newid='align-vertical-top-to-anchor'/>
+ <remap id='al_baselines_hor' newid='align-vertical-baseline'/>
+
+ <remap id='distribute_left' newid='distribute-horizontal-left'/>
+ <remap id='distribute_hcentre' newid='distribute-horizontal-center'/>
+ <remap id='distribute_right' newid='distribute-horizontal-right'/>
+ <remap id='distrobute-hdist' newid='distribute-horizontal-gaps'/>
+ <remap id='distribute_baselines_hor' newid='distribute-horizontal-baseline'/>
+
+ <remap id='distribute_bottom' newid='distribute-vertical-bottom'/>
+ <remap id='distribute_vcentre' newid='distribute-vertical-center'/>
+ <remap id='distribute_top' newid='distribute-vertical-top'/>
+ <remap id='distrobute-vdist' newid='distribute-vertical-gaps'/>
+ <remap id='distribute_baselines_vert' newid='distribute-vertical-baseline'/>
+
+ <remap id='distribute_randomize' newid='distribute-randomize'/>
+ <remap id='unclump' newid='distribute-unclump'/>
+ <remap id='graph_layout' newid='distribute-graph'/>
+ <remap id='directed_graph' newid='distribute-graph-directed'/>
+ <remap id='remove_overlaps' newid='distribute-remove-overlaps'/>
+
+ <remap id='node_valign' newid='align-horizontal-node'/>
+ <remap id='node_halign' newid='align-vertical-node'/>
+ <remap id='node_vdistribute' newid='distribute-vertical-node'/>
+ <remap id='node_hdistribute' newid='distribute-horizontal-node'/>
+
+ <!-- XML editor -->
+ <remap id='add_xml_element_node' newid='xml-element-new'/>
+ <remap id='add_xml_text_node' newid='xml-text-new'/>
+ <remap id='delete_xml_node' newid='xml-node-delete'/>
+ <remap id='duplicate_xml_node' newid='xml-node-duplicate'/>
+ <remap id='delete_xml_attribute' newid='xml-attribute-delete'/>
+
+ <!-- transform dialog -->
+ <remap id='arrows_hor' newid='transform-move-horizontal'/>
+ <remap id='arrows_ver' newid='transform-move-vertical'/>
+ <remap id='transform_scale_hor' newid='transform-scale-horizontal'/>
+ <remap id='transform_scale_ver' newid='transform-scale-vertical'/>
+ <remap id='transform_scew_hor' newid='transform-skew-horizontal'/>
+ <remap id='transform_scew_ver' newid='transform-skew-vertical'/>
+
+ <!-- fill and stroke dialog -->
+ <remap id='properties_fill' newid='object-fill'/>
+ <remap id='properties_stroke_paint' newid='object-stroke'/>
+ <remap id='properties_stroke' newid='object-stroke-style'/>
+ <remap id='fill_none' newid='paint-none'/>
+ <remap id='fill_solid' newid='paint-solid'/>
+ <remap id='fill_gradient' newid='paint-gradient-linear'/>
+ <remap id='fill_radial' newid='paint-gradient-radial'/>
+ <remap id='fill_pattern' newid='paint-pattern'/>
+ <remap id='fill_unset' newid='paint-unknown'/>
+ <remap id='fillrule_evenodd' newid='fill-rule-even-odd'/>
+ <remap id='fillrule_nonzero' newid='fill-rule-nonzero'/>
+ <remap id='join_miter' newid='stroke-join-miter'/>
+ <remap id='join_bevel' newid='stroke-join-bevel'/>
+ <remap id='join_round' newid='stroke-join-round'/>
+ <remap id='cap_butt' newid='stroke-cap-butt'/>
+ <remap id='cap_square' newid='stroke-cap-square'/>
+ <remap id='cap_round' newid='stroke-cap-round'/>
+
+ <!-- MISCELLANEOUS -->
+ <remap id='guide' newid='guides'/>
+ <remap id='grid_xy' newid='grid-rectangular'/>
+ <remap id='grid_axonom' newid='grid-axonometric'/>
+ <remap id='visible' newid='object-visible'/>
+ <remap id='hidden' newid='object-hidden'/>
+ <remap id='lock_unlocked' newid='object-unlocked'/>
+ <remap id='width_height_lock' newid='object-locked'/>
+ <remap id='sticky_zoom' newid='zoom'/>
+</mappings>
diff --git a/src/widgets/paint-selector.cpp b/src/widgets/paint-selector.cpp
new file mode 100644
index 0000000..1e1f99e
--- /dev/null
+++ b/src/widgets/paint-selector.cpp
@@ -0,0 +1,1601 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * SPPaintSelector: Generic paint selector widget.
+ *//*
+ * Authors:
+ * see git history
+ * Lauris Kaplinski
+ * bulia byak <buliabyak@users.sf.net>
+ * John Cliff <simarilius@yahoo.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noSP_PS_VERBOSE
+
+#include <cstring>
+#include <string>
+#include <vector>
+
+#include <glibmm/i18n.h>
+
+#include "desktop-style.h"
+#include "gradient-selector.h"
+#include "inkscape.h"
+#include "paint-selector.h"
+#include "path-prefix.h"
+
+#include "helper/stock-items.h"
+#include "ui/icon-loader.h"
+
+#include "style.h"
+
+#include "io/sys.h"
+
+#include "object/sp-hatch.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+#include "object/sp-stop.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/color-notebook.h"
+
+#include "widgets/swatch-selector.h"
+#include "widgets/widget-sizes.h"
+
+#include "xml/repr.h"
+
+#ifdef SP_PS_VERBOSE
+#include "svg/svg-icc-color.h"
+#endif // SP_PS_VERBOSE
+
+using Inkscape::Widgets::SwatchSelector;
+using Inkscape::UI::SelectedColor;
+
+enum {
+ MODE_CHANGED,
+ GRABBED,
+ DRAGGED,
+ RELEASED,
+ CHANGED,
+ FILLRULE_CHANGED,
+ LAST_SIGNAL
+};
+
+static void sp_paint_selector_dispose(GObject *object);
+
+static GtkWidget *sp_paint_selector_style_button_add(SPPaintSelector *psel, gchar const *px, SPPaintSelector::Mode mode, gchar const *tip);
+static void sp_paint_selector_style_button_toggled(GtkToggleButton *tb, SPPaintSelector *psel);
+static void sp_paint_selector_fillrule_toggled(GtkToggleButton *tb, SPPaintSelector *psel);
+
+static void sp_paint_selector_set_mode_empty(SPPaintSelector *psel);
+static void sp_paint_selector_set_mode_multiple(SPPaintSelector *psel);
+static void sp_paint_selector_set_mode_none(SPPaintSelector *psel);
+static void sp_paint_selector_set_mode_color(SPPaintSelector *psel, SPPaintSelector::Mode mode);
+static void sp_paint_selector_set_mode_gradient(SPPaintSelector *psel, SPPaintSelector::Mode mode);
+#ifdef WITH_MESH
+static void sp_paint_selector_set_mode_mesh(SPPaintSelector *psel, SPPaintSelector::Mode mode);
+#endif
+static void sp_paint_selector_set_mode_pattern(SPPaintSelector *psel, SPPaintSelector::Mode mode);
+static void sp_paint_selector_set_mode_hatch(SPPaintSelector *psel, SPPaintSelector::Mode mode);
+static void sp_paint_selector_set_mode_swatch(SPPaintSelector *psel, SPPaintSelector::Mode mode);
+static void sp_paint_selector_set_mode_unset(SPPaintSelector *psel);
+
+
+static void sp_paint_selector_set_style_buttons(SPPaintSelector *psel, GtkWidget *active);
+
+static guint psel_signals[LAST_SIGNAL] = {0};
+
+#ifdef SP_PS_VERBOSE
+static gchar const* modeStrings[] = {
+ "MODE_EMPTY",
+ "MODE_MULTIPLE",
+ "MODE_NONE",
+ "MODE_SOLID_COLOR",
+ "MODE_GRADIENT_LINEAR",
+ "MODE_GRADIENT_RADIAL",
+#ifdef WITH_MESH
+ "MODE_GRADIENT_MESH",
+#endif
+ "MODE_PATTERN",
+ "MODE_SWATCH",
+ "MODE_UNSET",
+ ".",
+ ".",
+};
+#endif
+
+
+static bool isPaintModeGradient(SPPaintSelector::Mode mode)
+{
+ bool isGrad = (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) ||
+ (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) ||
+ (mode == SPPaintSelector::MODE_SWATCH);
+
+ return isGrad;
+}
+
+static SPGradientSelector *getGradientFromData(SPPaintSelector const *psel)
+{
+ SPGradientSelector *grad = nullptr;
+ if (psel->mode == SPPaintSelector::MODE_SWATCH) {
+ SwatchSelector *swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(psel->selector), "swatch-selector"));
+ if (swatchsel) {
+ grad = swatchsel->getGradientSelector();
+ }
+ } else {
+ grad = reinterpret_cast<SPGradientSelector*>(g_object_get_data(G_OBJECT(psel->selector), "gradient-selector"));
+ }
+ return grad;
+}
+
+G_DEFINE_TYPE(SPPaintSelector, sp_paint_selector, GTK_TYPE_BOX);
+
+static void
+sp_paint_selector_class_init(SPPaintSelectorClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+ psel_signals[MODE_CHANGED] = g_signal_new("mode_changed",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPPaintSelectorClass, mode_changed),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ psel_signals[GRABBED] = g_signal_new("grabbed",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPPaintSelectorClass, grabbed),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ psel_signals[DRAGGED] = g_signal_new("dragged",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPPaintSelectorClass, dragged),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ psel_signals[RELEASED] = g_signal_new("released",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPPaintSelectorClass, released),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ psel_signals[CHANGED] = g_signal_new("changed",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPPaintSelectorClass, changed),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ psel_signals[FILLRULE_CHANGED] = g_signal_new("fillrule_changed",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPPaintSelectorClass, fillrule_changed),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+
+ object_class->dispose = sp_paint_selector_dispose;
+}
+
+#define XPAD 4
+#define YPAD 1
+
+static void
+sp_paint_selector_init(SPPaintSelector *psel)
+{
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(psel), GTK_ORIENTATION_VERTICAL);
+
+ psel->mode = static_cast<SPPaintSelector::Mode>(-1); // huh? do you mean 0xff? -- I think this means "not in the enum"
+
+ /* Paint style button box */
+ psel->style = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_set_homogeneous(GTK_BOX(psel->style), FALSE);
+ gtk_widget_set_name(psel->style,"PaintSelector");
+ gtk_widget_show(psel->style);
+ gtk_container_set_border_width(GTK_CONTAINER(psel->style), 4);
+ gtk_box_pack_start(GTK_BOX(psel), psel->style, FALSE, FALSE, 0);
+
+ /* Buttons */
+ psel->none = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-none"),
+ SPPaintSelector::MODE_NONE, _("No paint"));
+ psel->solid = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-solid"),
+ SPPaintSelector::MODE_SOLID_COLOR, _("Flat color"));
+ psel->gradient = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-linear"),
+ SPPaintSelector::MODE_GRADIENT_LINEAR, _("Linear gradient"));
+ psel->radial = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-radial"),
+ SPPaintSelector::MODE_GRADIENT_RADIAL, _("Radial gradient"));
+#ifdef WITH_MESH
+ psel->mesh = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-mesh"),
+ SPPaintSelector::MODE_GRADIENT_MESH, _("Mesh gradient"));
+#endif
+ psel->pattern = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-pattern"),
+ SPPaintSelector::MODE_PATTERN, _("Pattern"));
+ psel->swatch = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-swatch"),
+ SPPaintSelector::MODE_SWATCH, _("Swatch"));
+ psel->unset = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-unknown"),
+ SPPaintSelector::MODE_UNSET, _("Unset paint (make it undefined so it can be inherited)"));
+
+ /* Fillrule */
+ {
+ psel->fillrulebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_set_homogeneous(GTK_BOX(psel->fillrulebox), FALSE);
+ gtk_box_pack_end(GTK_BOX(psel->style), psel->fillrulebox, FALSE, FALSE, 0);
+
+ GtkWidget *w;
+ psel->evenodd = gtk_radio_button_new(nullptr);
+ gtk_button_set_relief(GTK_BUTTON(psel->evenodd), GTK_RELIEF_NONE);
+ gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(psel->evenodd), FALSE);
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty
+ gtk_widget_set_tooltip_text(psel->evenodd, _("Any path self-intersections or subpaths create holes in the fill (fill-rule: evenodd)"));
+ g_object_set_data(G_OBJECT(psel->evenodd), "mode", GUINT_TO_POINTER(SPPaintSelector::FILLRULE_EVENODD));
+ w = sp_get_icon_image("fill-rule-even-odd", GTK_ICON_SIZE_MENU);
+ gtk_container_add(GTK_CONTAINER(psel->evenodd), w);
+ gtk_box_pack_start(GTK_BOX(psel->fillrulebox), psel->evenodd, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(psel->evenodd), "toggled", G_CALLBACK(sp_paint_selector_fillrule_toggled), psel);
+
+ psel->nonzero = gtk_radio_button_new(gtk_radio_button_get_group(GTK_RADIO_BUTTON(psel->evenodd)));
+ gtk_button_set_relief(GTK_BUTTON(psel->nonzero), GTK_RELIEF_NONE);
+ gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(psel->nonzero), FALSE);
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty
+ gtk_widget_set_tooltip_text(psel->nonzero, _("Fill is solid unless a subpath is counterdirectional (fill-rule: nonzero)"));
+ g_object_set_data(G_OBJECT(psel->nonzero), "mode", GUINT_TO_POINTER(SPPaintSelector::FILLRULE_NONZERO));
+ w = sp_get_icon_image("fill-rule-nonzero", GTK_ICON_SIZE_MENU);
+ gtk_container_add(GTK_CONTAINER(psel->nonzero), w);
+ gtk_box_pack_start(GTK_BOX(psel->fillrulebox), psel->nonzero, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(psel->nonzero), "toggled", G_CALLBACK(sp_paint_selector_fillrule_toggled), psel);
+ }
+
+ /* Frame */
+ psel->label = gtk_label_new("");
+ auto lbbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_box_set_homogeneous(GTK_BOX(lbbox), FALSE);
+ gtk_widget_show(psel->label);
+ gtk_box_pack_start(GTK_BOX(lbbox), psel->label, false, false, 4);
+ gtk_box_pack_start(GTK_BOX(psel), lbbox, false, false, 4);
+
+ psel->frame = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_box_set_homogeneous(GTK_BOX(psel->frame), FALSE);
+ gtk_widget_show(psel->frame);
+ //gtk_container_set_border_width(GTK_CONTAINER(psel->frame), 0);
+ gtk_box_pack_start(GTK_BOX(psel), psel->frame, TRUE, TRUE, 0);
+
+
+ /* Last used color */
+ psel->selected_color = new SelectedColor;
+ psel->updating_color = false;
+
+ psel->selected_color->signal_grabbed.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorGrabbed));
+ psel->selected_color->signal_dragged.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorDragged));
+ psel->selected_color->signal_released.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorReleased));
+ psel->selected_color->signal_changed.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorChanged));
+}
+
+static void sp_paint_selector_dispose(GObject *object)
+{
+ SPPaintSelector *psel = SP_PAINT_SELECTOR(object);
+
+ // clean up our long-living pattern menu
+ g_object_set_data(G_OBJECT(psel),"patternmenu",nullptr);
+
+#ifdef WITH_MESH
+ // clean up our long-living mesh menu
+ g_object_set_data(G_OBJECT(psel),"meshmenu",nullptr);
+#endif
+
+ if (psel->selected_color) {
+ delete psel->selected_color;
+ psel->selected_color = nullptr;
+ }
+
+ if ((G_OBJECT_CLASS(sp_paint_selector_parent_class))->dispose)
+ (G_OBJECT_CLASS(sp_paint_selector_parent_class))->dispose(object);
+}
+
+static GtkWidget *sp_paint_selector_style_button_add(SPPaintSelector *psel,
+ gchar const *pixmap, SPPaintSelector::Mode mode,
+ gchar const *tip)
+{
+ GtkWidget *b, *w;
+
+ b = gtk_toggle_button_new();
+ gtk_widget_set_tooltip_text(b, tip);
+ gtk_widget_show(b);
+
+ gtk_container_set_border_width(GTK_CONTAINER(b), 0);
+
+ gtk_button_set_relief(GTK_BUTTON(b), GTK_RELIEF_NONE);
+
+ gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(b), FALSE);
+ g_object_set_data(G_OBJECT(b), "mode", GUINT_TO_POINTER(mode));
+
+ w = sp_get_icon_image(pixmap, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add(GTK_CONTAINER(b), w);
+
+ gtk_box_pack_start(GTK_BOX(psel->style), b, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(b), "toggled", G_CALLBACK(sp_paint_selector_style_button_toggled), psel);
+
+ return b;
+}
+
+static void
+sp_paint_selector_style_button_toggled(GtkToggleButton *tb, SPPaintSelector *psel)
+{
+ if (!psel->update && gtk_toggle_button_get_active(tb)) {
+ psel->setMode(static_cast<SPPaintSelector::Mode>(GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(tb), "mode"))));
+ }
+}
+
+static void
+sp_paint_selector_fillrule_toggled(GtkToggleButton *tb, SPPaintSelector *psel)
+{
+ if (!psel->update && gtk_toggle_button_get_active(tb)) {
+ SPPaintSelector::FillRule fr = static_cast<SPPaintSelector::FillRule>(GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(tb), "mode")));
+ g_signal_emit(G_OBJECT(psel), psel_signals[FILLRULE_CHANGED], 0, fr);
+ }
+}
+
+static void
+sp_paint_selector_show_fillrule(SPPaintSelector *psel, bool is_fill)
+{
+ if (psel->fillrulebox) {
+ if (is_fill) {
+ gtk_widget_show_all(psel->fillrulebox);
+ } else {
+ gtk_widget_destroy(psel->fillrulebox);
+ psel->fillrulebox = nullptr;
+ }
+ }
+}
+
+
+SPPaintSelector *sp_paint_selector_new(FillOrStroke kind)
+{
+ SPPaintSelector *psel = static_cast<SPPaintSelector*>(g_object_new(SP_TYPE_PAINT_SELECTOR, nullptr));
+
+ psel->setMode(SPPaintSelector::MODE_MULTIPLE);
+
+ // This silliness is here because I don't know how to pass a parameter to the
+ // GtkObject "constructor" (sp_paint_selector_init). Remove it when paint_selector
+ // becomes a normal class.
+ sp_paint_selector_show_fillrule(psel, kind == FILL);
+
+ return psel;
+}
+
+void SPPaintSelector::setMode(Mode mode)
+{
+ if (this->mode != mode) {
+ update = TRUE;
+#ifdef SP_PS_VERBOSE
+ g_print("Mode change %d -> %d %s -> %s\n", this->mode, mode, modeStrings[this->mode], modeStrings[mode]);
+#endif
+ switch (mode) {
+ case MODE_EMPTY:
+ sp_paint_selector_set_mode_empty(this);
+ break;
+ case MODE_MULTIPLE:
+ sp_paint_selector_set_mode_multiple(this);
+ break;
+ case MODE_NONE:
+ sp_paint_selector_set_mode_none(this);
+ break;
+ case MODE_SOLID_COLOR:
+ sp_paint_selector_set_mode_color(this, mode);
+ break;
+ case MODE_GRADIENT_LINEAR:
+ case MODE_GRADIENT_RADIAL:
+ sp_paint_selector_set_mode_gradient(this, mode);
+ break;
+#ifdef WITH_MESH
+ case MODE_GRADIENT_MESH:
+ sp_paint_selector_set_mode_mesh(this, mode);
+ break;
+#endif
+ case MODE_PATTERN:
+ sp_paint_selector_set_mode_pattern(this, mode);
+ break;
+ case MODE_HATCH:
+ sp_paint_selector_set_mode_hatch(this, mode);
+ break;
+ case MODE_SWATCH:
+ sp_paint_selector_set_mode_swatch(this, mode);
+ break;
+ case MODE_UNSET:
+ sp_paint_selector_set_mode_unset(this);
+ break;
+ default:
+ g_warning("file %s: line %d: Unknown paint mode %d", __FILE__, __LINE__, mode);
+ break;
+ }
+ this->mode = mode;
+ g_signal_emit(G_OBJECT(this), psel_signals[MODE_CHANGED], 0, this->mode);
+ update = FALSE;
+ }
+}
+
+void SPPaintSelector::setFillrule(FillRule fillrule)
+{
+ if (fillrulebox) {
+ // TODO this flips widgets but does not use a member to store state. Revisit
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(evenodd), (fillrule == FILLRULE_EVENODD));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(nonzero), (fillrule == FILLRULE_NONZERO));
+ }
+}
+
+void SPPaintSelector::setColorAlpha(SPColor const &color, float alpha)
+{
+ g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) );
+/*
+ guint32 rgba = 0;
+
+ if ( sp_color_get_colorspace_type(color) == SP_COLORSPACE_TYPE_CMYK )
+ {
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set CMYKA\n");
+#endif
+ sp_paint_selector_set_mode(psel, MODE_COLOR_CMYK);
+ }
+ else
+*/
+ {
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set RGBA\n");
+#endif
+ setMode(MODE_SOLID_COLOR);
+ }
+
+ updating_color = true;
+ selected_color->setColorAlpha(color, alpha);
+ updating_color = false;
+ //rgba = color.toRGBA32( alpha );
+}
+
+void SPPaintSelector::setSwatch(SPGradient *vector )
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set SWATCH\n");
+#endif
+ setMode(MODE_SWATCH);
+
+ SwatchSelector *swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(selector), "swatch-selector"));
+ if (swatchsel) {
+ swatchsel->setVector( (vector) ? vector->document : nullptr, vector );
+ }
+}
+
+void SPPaintSelector::setGradientLinear(SPGradient *vector)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT LINEAR\n");
+#endif
+ setMode(MODE_GRADIENT_LINEAR);
+
+ SPGradientSelector *gsel = getGradientFromData(this);
+
+ gsel->setMode(SPGradientSelector::MODE_LINEAR);
+ gsel->setVector((vector) ? vector->document : nullptr, vector);
+}
+
+void SPPaintSelector::setGradientRadial(SPGradient *vector)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT RADIAL\n");
+#endif
+ setMode(MODE_GRADIENT_RADIAL);
+
+ SPGradientSelector *gsel = getGradientFromData(this);
+
+ gsel->setMode(SPGradientSelector::MODE_RADIAL);
+
+ gsel->setVector((vector) ? vector->document : nullptr, vector);
+}
+
+#ifdef WITH_MESH
+void SPPaintSelector::setGradientMesh(SPMeshGradient *array)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT MESH\n");
+#endif
+ setMode(MODE_GRADIENT_MESH);
+
+ // SPGradientSelector *gsel = getGradientFromData(this);
+
+ // gsel->setMode(SPGradientSelector::MODE_GRADIENT_MESH);
+ // gsel->setVector((mesh) ? mesh->document : 0, mesh);
+}
+#endif
+
+void SPPaintSelector::setGradientProperties( SPGradientUnits units, SPGradientSpread spread )
+{
+ g_return_if_fail(isPaintModeGradient(mode));
+
+ SPGradientSelector *gsel = getGradientFromData(this);
+ gsel->setUnits(units);
+ gsel->setSpread(spread);
+}
+
+void SPPaintSelector::getGradientProperties( SPGradientUnits &units, SPGradientSpread &spread) const
+{
+ g_return_if_fail(isPaintModeGradient(mode));
+
+ SPGradientSelector *gsel = getGradientFromData(this);
+ units = gsel->getUnits();
+ spread = gsel->getSpread();
+}
+
+
+/**
+ * \post (alpha == NULL) || (*alpha in [0.0, 1.0]).
+ */
+void SPPaintSelector::getColorAlpha(SPColor &color, gfloat &alpha) const
+{
+ selected_color->colorAlpha(color, alpha);
+
+ g_assert( ( 0.0 <= alpha )
+ && ( alpha <= 1.0 ) );
+}
+
+SPGradient *SPPaintSelector::getGradientVector()
+{
+ SPGradient* vect = nullptr;
+
+ if (isPaintModeGradient(mode)) {
+ SPGradientSelector *gsel = getGradientFromData(this);
+ vect = gsel->getVector();
+ }
+
+ return vect;
+}
+
+
+void SPPaintSelector::pushAttrsToGradient( SPGradient *gr ) const
+{
+ SPGradientUnits units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX;
+ SPGradientSpread spread = SP_GRADIENT_SPREAD_PAD;
+ getGradientProperties( units, spread );
+ gr->setUnits(units);
+ gr->setSpread(spread);
+ gr->updateRepr();
+}
+
+static void
+sp_paint_selector_clear_frame(SPPaintSelector *psel)
+{
+ g_return_if_fail( psel != nullptr);
+
+ if (psel->selector) {
+
+ //This is a hack to work around GtkNotebook bug in ColorSelector. Is sends signal switch-page on destroy
+ //The widget is hidden first so it can recognize that it should not process signals from notebook child
+ gtk_widget_set_visible(psel->selector, false);
+ gtk_widget_destroy(psel->selector);
+ psel->selector = nullptr;
+ }
+}
+
+static void
+sp_paint_selector_set_mode_empty(SPPaintSelector *psel)
+{
+ sp_paint_selector_set_style_buttons(psel, nullptr);
+ gtk_widget_set_sensitive(psel->style, FALSE);
+
+ sp_paint_selector_clear_frame(psel);
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>No objects</b>"));
+}
+
+static void
+sp_paint_selector_set_mode_multiple(SPPaintSelector *psel)
+{
+ sp_paint_selector_set_style_buttons(psel, nullptr);
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ sp_paint_selector_clear_frame(psel);
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Multiple styles</b>"));
+}
+
+static void
+sp_paint_selector_set_mode_unset(SPPaintSelector *psel)
+{
+ sp_paint_selector_set_style_buttons(psel, psel->unset);
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ sp_paint_selector_clear_frame(psel);
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Paint is undefined</b>"));
+}
+
+static void
+sp_paint_selector_set_mode_none(SPPaintSelector *psel)
+{
+ sp_paint_selector_set_style_buttons(psel, psel->none);
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ sp_paint_selector_clear_frame(psel);
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>No paint</b>"));
+
+}
+
+/* Color paint */
+
+void SPPaintSelector::onSelectedColorGrabbed() {
+ g_signal_emit(G_OBJECT(this), psel_signals[GRABBED], 0);
+}
+
+void SPPaintSelector::onSelectedColorDragged() {
+ if (updating_color) {
+ return;
+ }
+ g_signal_emit(G_OBJECT(this), psel_signals[DRAGGED], 0);
+}
+
+void SPPaintSelector::onSelectedColorReleased() {
+ g_signal_emit(G_OBJECT(this), psel_signals[RELEASED], 0);
+}
+
+void SPPaintSelector::onSelectedColorChanged() {
+ if (updating_color) {
+ return;
+ }
+
+ if (mode == MODE_SOLID_COLOR) {
+ g_signal_emit(G_OBJECT(this), psel_signals[CHANGED], 0);
+ } else {
+ g_warning("SPPaintSelector::onSelectedColorChanged(): selected color changed while not in color selection mode");
+ }
+}
+
+static void sp_paint_selector_set_mode_color(SPPaintSelector *psel, SPPaintSelector::Mode /*mode*/)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ if ((psel->mode == SPPaintSelector::MODE_SWATCH)
+ || (psel->mode == SPPaintSelector::MODE_GRADIENT_LINEAR)
+ || (psel->mode == SPPaintSelector::MODE_GRADIENT_RADIAL) ) {
+ SPGradientSelector *gsel = getGradientFromData(psel);
+ if (gsel) {
+ SPGradient *gradient = gsel->getVector();
+
+ // Gradient can be null if object paint is changed externally (ie. with a color picker tool)
+ if (gradient)
+ {
+ SPColor color = gradient->getFirstStop()->getColor();
+ float alpha = gradient->getFirstStop()->getOpacity();
+ psel->selected_color->setColorAlpha(color, alpha, false);
+ }
+ }
+ }
+
+ sp_paint_selector_set_style_buttons(psel, psel->solid);
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ if (psel->mode == SPPaintSelector::MODE_SOLID_COLOR) {
+ /* Already have color selector */
+ // Do nothing
+ } else {
+
+ sp_paint_selector_clear_frame(psel);
+ /* Create new color selector */
+ /* Create vbox */
+ auto vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_box_set_homogeneous(GTK_BOX(vb), FALSE);
+ gtk_widget_show(vb);
+
+ /* Color selector */
+ Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(*(psel->selected_color)));
+ color_selector->show();
+ gtk_box_pack_start(GTK_BOX(vb), color_selector->gobj(), TRUE, TRUE, 0);
+
+ /* Pack everything to frame */
+ gtk_container_add(GTK_CONTAINER(psel->frame), vb);
+
+ psel->selector = vb;
+ }
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Flat color</b>"));
+
+#ifdef SP_PS_VERBOSE
+ g_print("Color req\n");
+#endif
+}
+
+/* Gradient */
+
+static void sp_paint_selector_gradient_grabbed(SPGradientSelector * /*csel*/, SPPaintSelector *psel)
+{
+ g_signal_emit(G_OBJECT(psel), psel_signals[GRABBED], 0);
+}
+
+static void sp_paint_selector_gradient_dragged(SPGradientSelector * /*csel*/, SPPaintSelector *psel)
+{
+ g_signal_emit(G_OBJECT(psel), psel_signals[DRAGGED], 0);
+}
+
+static void sp_paint_selector_gradient_released(SPGradientSelector * /*csel*/, SPPaintSelector *psel)
+{
+ g_signal_emit(G_OBJECT(psel), psel_signals[RELEASED], 0);
+}
+
+static void sp_paint_selector_gradient_changed(SPGradientSelector * /*csel*/, SPPaintSelector *psel)
+{
+ g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0);
+}
+
+static void sp_paint_selector_set_mode_gradient(SPPaintSelector *psel, SPPaintSelector::Mode mode)
+{
+ GtkWidget *gsel;
+
+ /* fixme: We do not need function-wide gsel at all */
+
+ if (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) {
+ sp_paint_selector_set_style_buttons(psel, psel->gradient);
+ } else if (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) {
+ sp_paint_selector_set_style_buttons(psel, psel->radial);
+ }
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ if ((psel->mode == SPPaintSelector::MODE_GRADIENT_LINEAR) || (psel->mode == SPPaintSelector::MODE_GRADIENT_RADIAL)) {
+ /* Already have gradient selector */
+ gsel = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "gradient-selector"));
+ } else {
+ sp_paint_selector_clear_frame(psel);
+ /* Create new gradient selector */
+ gsel = sp_gradient_selector_new();
+ gtk_widget_show(gsel);
+ g_signal_connect(G_OBJECT(gsel), "grabbed", G_CALLBACK(sp_paint_selector_gradient_grabbed), psel);
+ g_signal_connect(G_OBJECT(gsel), "dragged", G_CALLBACK(sp_paint_selector_gradient_dragged), psel);
+ g_signal_connect(G_OBJECT(gsel), "released", G_CALLBACK(sp_paint_selector_gradient_released), psel);
+ g_signal_connect(G_OBJECT(gsel), "changed", G_CALLBACK(sp_paint_selector_gradient_changed), psel);
+ /* Pack everything to frame */
+ gtk_container_add(GTK_CONTAINER(psel->frame), gsel);
+ psel->selector = gsel;
+ g_object_set_data(G_OBJECT(psel->selector), "gradient-selector", gsel);
+ }
+
+ /* Actually we have to set option menu history here */
+ if (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) {
+ SP_GRADIENT_SELECTOR(gsel)->setMode(SPGradientSelector::MODE_LINEAR);
+ //sp_gradient_selector_set_mode(SP_GRADIENT_SELECTOR(gsel), SP_GRADIENT_SELECTOR_MODE_LINEAR);
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Linear gradient</b>"));
+ } else if (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) {
+ SP_GRADIENT_SELECTOR(gsel)->setMode(SPGradientSelector::MODE_RADIAL);
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Radial gradient</b>"));
+ }
+
+#ifdef SP_PS_VERBOSE
+ g_print("Gradient req\n");
+#endif
+}
+
+// ************************* MESH ************************
+#ifdef WITH_MESH
+static void sp_psel_mesh_destroy(GtkWidget *widget, SPPaintSelector * /*psel*/)
+{
+ // drop our reference to the mesh menu widget
+ g_object_unref( G_OBJECT(widget) );
+}
+
+static void sp_psel_mesh_change(GtkWidget * /*widget*/, SPPaintSelector *psel)
+{
+ g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0);
+}
+
+
+/**
+ * Returns a list of meshes in the defs of the given source document as a vector
+ */
+static std::vector<SPMeshGradient *>
+ink_mesh_list_get (SPDocument *source)
+{
+ std::vector<SPMeshGradient *> pl;
+ if (source == nullptr)
+ return pl;
+
+
+ std::vector<SPObject *> meshes = source->getResourceList("gradient");
+ for (auto meshe : meshes) {
+ if (SP_IS_MESHGRADIENT(meshe) &&
+ SP_GRADIENT(meshe) == SP_GRADIENT(meshe)->getArray()) { // only if this is a root mesh
+ pl.push_back(SP_MESHGRADIENT(meshe));
+ }
+ }
+ return pl;
+}
+
+/**
+ * Adds menu items for mesh list.
+ */
+static void
+sp_mesh_menu_build (GtkWidget *combo, std::vector<SPMeshGradient *> &mesh_list, SPDocument */*source*/)
+{
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ GtkTreeIter iter;
+
+ for (auto i:mesh_list) {
+
+ Inkscape::XML::Node *repr = i->getRepr();
+
+ gchar const *meshid = repr->attribute("id");
+ gchar const *label = meshid;
+
+ // Only relevant if we supply a set of canned meshes.
+ gboolean stockid = false;
+ if (repr->attribute("inkscape:stockid")) {
+ label = _(repr->attribute("inkscape:stockid"));
+ stockid = true;
+ }
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter,
+ COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_MESH, meshid, COMBO_COL_SEP, FALSE, -1);
+
+ }
+}
+
+/**
+ * Pick up all meshes from source, except those that are in
+ * current_doc (if non-NULL), and add items to the mesh menu.
+ */
+static void sp_mesh_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source, SPDocument * /*mesh_doc*/)
+{
+ std::vector<SPMeshGradient *> pl = ink_mesh_list_get(source);
+ sp_mesh_menu_build (combo, pl, source);
+}
+
+
+static void
+ink_mesh_menu_populate_menu(GtkWidget *combo, SPDocument *doc)
+{
+ static SPDocument *meshes_doc = nullptr;
+
+ // If we ever add a list of canned mesh gradients, uncomment following:
+
+ // find and load meshes.svg
+ // if (meshes_doc == NULL) {
+ // char *meshes_source = g_build_filename(INKSCAPE_MESHESDIR, "meshes.svg", NULL);
+ // if (Inkscape::IO::file_test(meshes_source, G_FILE_TEST_IS_REGULAR)) {
+ // meshes_doc = SPDocument::createNewDoc(meshes_source, FALSE);
+ // }
+ // g_free(meshes_source);
+ // }
+
+ // suck in from current doc
+ sp_mesh_list_from_doc ( combo, nullptr, doc, meshes_doc );
+
+ // add separator
+ // {
+ // GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ // GtkTreeIter iter;
+ // gtk_list_store_append (store, &iter);
+ // gtk_list_store_set(store, &iter,
+ // COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_MESH, "", COMBO_COL_SEP, true, -1);
+ // }
+
+ // suck in from meshes.svg
+ // if (meshes_doc) {
+ // doc->ensureUpToDate();
+ // sp_mesh_list_from_doc ( combo, doc, meshes_doc, NULL );
+ // }
+
+}
+
+
+static GtkWidget*
+ink_mesh_menu(GtkWidget *combo)
+{
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ GtkTreeIter iter;
+
+ if (!doc) {
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false, COMBO_COL_MESH, "", COMBO_COL_SEP, false, -1);
+ gtk_widget_set_sensitive(combo, FALSE);
+
+ } else {
+
+ ink_mesh_menu_populate_menu(combo, doc);
+ gtk_widget_set_sensitive(combo, TRUE);
+
+ }
+
+ // Select the first item that is not a separator
+ if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL(store), &iter)) {
+ gboolean sep = false;
+ gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1);
+ if (sep) {
+ gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
+ }
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter);
+ }
+
+ return combo;
+}
+
+
+/*update mesh list*/
+void SPPaintSelector::updateMeshList( SPMeshGradient *mesh )
+{
+ if (update) {
+ return;
+ }
+
+ GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "meshmenu"));
+ g_assert( combo != nullptr );
+
+ /* Clear existing menu if any */
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
+ gtk_list_store_clear(GTK_LIST_STORE(store));
+
+ ink_mesh_menu(combo);
+
+ /* Set history */
+
+ if (mesh && !g_object_get_data(G_OBJECT(combo), "update")) {
+
+ g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(TRUE));
+ gchar const *meshname = mesh->getRepr()->attribute("id");
+
+ // Find this mesh and set it active in the combo_box
+ GtkTreeIter iter ;
+ gchar *meshid = nullptr;
+ bool valid = gtk_tree_model_get_iter_first (store, &iter);
+ if (!valid) {
+ return;
+ }
+ gtk_tree_model_get (store, &iter, COMBO_COL_MESH, &meshid, -1);
+ while (valid && strcmp(meshid, meshname) != 0) {
+ valid = gtk_tree_model_iter_next (store, &iter);
+ g_free(meshid);
+ meshid = nullptr;
+ gtk_tree_model_get (store, &iter, COMBO_COL_MESH, &meshid, -1);
+ }
+
+ if (valid) {
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter);
+ }
+
+ g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(FALSE));
+ g_free(meshid);
+ }
+}
+
+static void sp_paint_selector_set_mode_mesh(SPPaintSelector *psel, SPPaintSelector::Mode mode)
+{
+ if (mode == SPPaintSelector::MODE_GRADIENT_MESH) {
+ sp_paint_selector_set_style_buttons(psel, psel->mesh);
+ }
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ GtkWidget *tbl = nullptr;
+
+ if (psel->mode == SPPaintSelector::MODE_GRADIENT_MESH) {
+ /* Already have mesh menu */
+ tbl = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "mesh-selector"));
+ } else {
+ sp_paint_selector_clear_frame(psel);
+
+ /* Create vbox */
+ tbl = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_box_set_homogeneous(GTK_BOX(tbl), FALSE);
+ gtk_widget_show(tbl);
+
+ {
+ auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1);
+ gtk_box_set_homogeneous(GTK_BOX(hb), FALSE);
+
+ /**
+ * Create a combo_box and store with 4 columns,
+ * The label, a pointer to the mesh, is stockid or not, is a separator or not.
+ */
+ GtkListStore *store = gtk_list_store_new (COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN);
+ GtkWidget *combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store));
+ gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), SPPaintSelector::isSeparator, nullptr, nullptr);
+
+ GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_renderer_set_padding (renderer, 2, 0);
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
+ gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer, "text", COMBO_COL_LABEL, NULL);
+
+ ink_mesh_menu(combo);
+ g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(sp_psel_mesh_change), psel);
+ g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(sp_psel_mesh_destroy), psel);
+ g_object_set_data(G_OBJECT(psel), "meshmenu", combo);
+ g_object_ref( G_OBJECT(combo));
+
+ gtk_container_add(GTK_CONTAINER(hb), combo);
+ gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS);
+
+ g_object_unref( G_OBJECT(store));
+ }
+
+ {
+ auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_set_homogeneous(GTK_BOX(hb), FALSE);
+ auto l = gtk_label_new(nullptr);
+ gtk_label_set_markup(GTK_LABEL(l), _("Use the <b>Mesh tool</b> to modify the mesh."));
+ gtk_label_set_line_wrap(GTK_LABEL(l), true);
+ gtk_widget_set_size_request(l, 180, -1);
+ gtk_box_pack_start(GTK_BOX(hb), l, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS);
+ gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS);
+ }
+
+ gtk_widget_show_all(tbl);
+
+ gtk_container_add(GTK_CONTAINER(psel->frame), tbl);
+ psel->selector = tbl;
+ g_object_set_data(G_OBJECT(psel->selector), "mesh-selector", tbl);
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Mesh fill</b>"));
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Mesh req\n");
+#endif
+}
+
+SPMeshGradient *SPPaintSelector::getMeshGradient()
+{
+ g_return_val_if_fail((mode == MODE_GRADIENT_MESH) , NULL);
+
+ GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "meshmenu"));
+
+ /* no mesh menu if we were just selected */
+ if ( combo == nullptr ) {
+ return nullptr;
+ }
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
+
+ /* Get the selected mesh */
+ GtkTreeIter iter;
+ if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo), &iter) ||
+ !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) {
+ return nullptr;
+ }
+
+ gchar *meshid = nullptr;
+ gboolean stockid = FALSE;
+ // gchar *label = nullptr;
+ gtk_tree_model_get (store, &iter, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, -1);
+ // gtk_tree_model_get (store, &iter, COMBO_COL_LABEL, &label, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, -1);
+ // std::cout << " .. meshid: " << (meshid?meshid:"null") << " label: " << (label?label:"null") << std::endl;
+ // g_free(label);
+ if (meshid == nullptr) {
+ return nullptr;
+ }
+
+ SPMeshGradient *mesh = nullptr;
+ if (strcmp(meshid, "none")){
+
+ gchar *mesh_name;
+ if (stockid) {
+ mesh_name = g_strconcat("urn:inkscape:mesh:", meshid, NULL);
+ } else {
+ mesh_name = g_strdup(meshid);
+ }
+
+ SPObject *mesh_obj = get_stock_item(mesh_name);
+ if (mesh_obj && SP_IS_MESHGRADIENT(mesh_obj)) {
+ mesh = SP_MESHGRADIENT(mesh_obj);
+ }
+ g_free(mesh_name);
+ } else {
+ std::cerr << "SPPaintSelector::getMeshGradient: Unexpected meshid value." << std::endl;
+ }
+
+ g_free(meshid);
+
+ return mesh;
+}
+
+#endif
+// ************************ End Mesh ************************
+
+static void
+sp_paint_selector_set_style_buttons(SPPaintSelector *psel, GtkWidget *active)
+{
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->none), (active == psel->none));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->solid), (active == psel->solid));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->gradient), (active == psel->gradient));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->radial), (active == psel->radial));
+#ifdef WITH_MESH
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->mesh), (active == psel->mesh));
+#endif
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->pattern), (active == psel->pattern));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->swatch), (active == psel->swatch));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->unset), (active == psel->unset));
+}
+
+static void sp_psel_pattern_destroy(GtkWidget *widget, SPPaintSelector * /*psel*/)
+{
+ // drop our reference to the pattern menu widget
+ g_object_unref( G_OBJECT(widget) );
+}
+
+static void sp_psel_pattern_change(GtkWidget * /*widget*/, SPPaintSelector *psel)
+{
+ g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0);
+}
+
+
+
+/**
+ * Returns a list of patterns in the defs of the given source document as a vector
+ */
+static std::vector<SPPattern*>
+ink_pattern_list_get (SPDocument *source)
+{
+ std::vector<SPPattern *> pl;
+ if (source == nullptr)
+ return pl;
+
+ std::vector<SPObject *> patterns = source->getResourceList("pattern");
+ for (auto pattern : patterns) {
+ if (SP_PATTERN(pattern) == SP_PATTERN(pattern)->rootPattern()) { // only if this is a root pattern
+ pl.push_back(SP_PATTERN(pattern));
+ }
+ }
+
+ return pl;
+}
+
+/**
+ * Adds menu items for pattern list - derived from marker code, left hb etc in to make addition of previews easier at some point.
+ */
+static void
+sp_pattern_menu_build (GtkWidget *combo, std::vector<SPPattern *> &pl, SPDocument */*source*/)
+{
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ GtkTreeIter iter;
+
+ for (auto i=pl.rbegin(); i!=pl.rend(); ++i) {
+
+ Inkscape::XML::Node *repr = (*i)->getRepr();
+
+ // label for combobox
+ gchar const *label;
+ if (repr->attribute("inkscape:stockid")) {
+ label = _(repr->attribute("inkscape:stockid"));
+ } else {
+ label = _(repr->attribute("id"));
+ }
+
+ gchar const *patid = repr->attribute("id");
+
+ gboolean stockid = false;
+ if (repr->attribute("inkscape:stockid")) {
+ stockid = true;
+ }
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter,
+ COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_PATTERN, patid, COMBO_COL_SEP, FALSE, -1);
+
+ }
+}
+
+/**
+ * Pick up all patterns from source, except those that are in
+ * current_doc (if non-NULL), and add items to the pattern menu.
+ */
+static void sp_pattern_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source, SPDocument * /*pattern_doc*/)
+{
+ std::vector<SPPattern *> pl = ink_pattern_list_get(source);
+ sp_pattern_menu_build (combo, pl, source);
+}
+
+
+static void
+ink_pattern_menu_populate_menu(GtkWidget *combo, SPDocument *doc)
+{
+ static SPDocument *patterns_doc = nullptr;
+
+ // find and load patterns.svg
+ if (patterns_doc == nullptr) {
+ char *patterns_source = g_build_filename(INKSCAPE_PAINTDIR, "patterns.svg", NULL);
+ if (Inkscape::IO::file_test(patterns_source, G_FILE_TEST_IS_REGULAR)) {
+ patterns_doc = SPDocument::createNewDoc(patterns_source, FALSE);
+ }
+ g_free(patterns_source);
+ }
+
+ // suck in from current doc
+ sp_pattern_list_from_doc ( combo, nullptr, doc, patterns_doc );
+
+ // add separator
+ {
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ GtkTreeIter iter;
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set(store, &iter,
+ COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_PATTERN, "", COMBO_COL_SEP, true, -1);
+ }
+
+ // suck in from patterns.svg
+ if (patterns_doc) {
+ doc->ensureUpToDate();
+ sp_pattern_list_from_doc ( combo, doc, patterns_doc, nullptr );
+ }
+
+}
+
+
+static GtkWidget*
+ink_pattern_menu(GtkWidget *combo)
+{
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ GtkTreeIter iter;
+
+ if (!doc) {
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false, COMBO_COL_PATTERN, "", COMBO_COL_SEP, false, -1);
+ gtk_widget_set_sensitive(combo, FALSE);
+
+ } else {
+
+ ink_pattern_menu_populate_menu(combo, doc);
+ gtk_widget_set_sensitive(combo, TRUE);
+
+ }
+
+ // Select the first item that is not a separator
+ if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL(store), &iter)) {
+ gboolean sep = false;
+ gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1);
+ if (sep) {
+ gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter);
+ }
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter);
+ }
+
+ return combo;
+}
+
+
+/*update pattern list*/
+void SPPaintSelector::updatePatternList( SPPattern *pattern )
+{
+ if (update) {
+ return;
+ }
+ GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "patternmenu"));
+ g_assert( combo != nullptr );
+
+ /* Clear existing menu if any */
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
+ gtk_list_store_clear(GTK_LIST_STORE(store));
+
+ ink_pattern_menu(combo);
+
+ /* Set history */
+
+ if (pattern && !g_object_get_data(G_OBJECT(combo), "update")) {
+
+ g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(TRUE));
+ gchar const *patname = pattern->getRepr()->attribute("id");
+
+ // Find this pattern and set it active in the combo_box
+ GtkTreeIter iter ;
+ gchar *patid = nullptr;
+ bool valid = gtk_tree_model_get_iter_first (store, &iter);
+ if (!valid) {
+ return;
+ }
+ gtk_tree_model_get (store, &iter, COMBO_COL_PATTERN, &patid, -1);
+ while (valid && strcmp(patid, patname) != 0) {
+ valid = gtk_tree_model_iter_next (store, &iter);
+ g_free(patid);
+ patid = nullptr;
+ gtk_tree_model_get (store, &iter, COMBO_COL_PATTERN, &patid, -1);
+ }
+ g_free(patid);
+
+ if (valid) {
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter);
+ }
+
+ g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(FALSE));
+ }
+}
+
+static void sp_paint_selector_set_mode_pattern(SPPaintSelector *psel, SPPaintSelector::Mode mode)
+{
+ if (mode == SPPaintSelector::MODE_PATTERN) {
+ sp_paint_selector_set_style_buttons(psel, psel->pattern);
+ }
+
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ GtkWidget *tbl = nullptr;
+
+ if (psel->mode == SPPaintSelector::MODE_PATTERN) {
+ /* Already have pattern menu */
+ tbl = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "pattern-selector"));
+ } else {
+ sp_paint_selector_clear_frame(psel);
+
+ /* Create vbox */
+ tbl = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_box_set_homogeneous(GTK_BOX(tbl), FALSE);
+ gtk_widget_show(tbl);
+
+ {
+ auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1);
+ gtk_box_set_homogeneous(GTK_BOX(hb), FALSE);
+
+ /**
+ * Create a combo_box and store with 4 columns,
+ * The label, a pointer to the pattern, is stockid or not, is a separator or not.
+ */
+ GtkListStore *store = gtk_list_store_new (COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN);
+ GtkWidget *combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store));
+ gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), SPPaintSelector::isSeparator, nullptr, nullptr);
+
+ GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_renderer_set_padding (renderer, 2, 0);
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
+ gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer, "text", COMBO_COL_LABEL, NULL);
+
+ ink_pattern_menu(combo);
+ g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(sp_psel_pattern_change), psel);
+ g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(sp_psel_pattern_destroy), psel);
+ g_object_set_data(G_OBJECT(psel), "patternmenu", combo);
+ g_object_ref( G_OBJECT(combo));
+
+ gtk_container_add(GTK_CONTAINER(hb), combo);
+ gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS);
+
+ g_object_unref( G_OBJECT(store));
+ }
+
+ {
+ auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_set_homogeneous(GTK_BOX(hb), FALSE);
+ auto l = gtk_label_new(nullptr);
+ gtk_label_set_markup(GTK_LABEL(l), _("Use the <b>Node tool</b> to adjust position, scale, and rotation of the pattern on canvas. Use <b>Object &gt; Pattern &gt; Objects to Pattern</b> to create a new pattern from selection."));
+ gtk_label_set_line_wrap(GTK_LABEL(l), true);
+ gtk_widget_set_size_request(l, 180, -1);
+ gtk_box_pack_start(GTK_BOX(hb), l, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS);
+ gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS);
+ }
+
+ gtk_widget_show_all(tbl);
+
+ gtk_container_add(GTK_CONTAINER(psel->frame), tbl);
+ psel->selector = tbl;
+ g_object_set_data(G_OBJECT(psel->selector), "pattern-selector", tbl);
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Pattern fill</b>"));
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Pattern req\n");
+#endif
+}
+
+static void sp_paint_selector_set_mode_hatch(SPPaintSelector *psel, SPPaintSelector::Mode mode)
+{
+ if (mode == SPPaintSelector::MODE_HATCH) {
+ sp_paint_selector_set_style_buttons(psel, psel->unset);
+ }
+
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ if (psel->mode == SPPaintSelector::MODE_HATCH) {
+ /* Already have hatch menu, for the moment unset */
+ } else {
+ sp_paint_selector_clear_frame(psel);
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Hatch fill</b>"));
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Hatch req\n");
+#endif
+}
+
+gboolean SPPaintSelector::isSeparator (GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) {
+
+ gboolean sep = FALSE;
+ gtk_tree_model_get(model, iter, COMBO_COL_SEP, &sep, -1);
+ return sep;
+}
+
+SPPattern *SPPaintSelector::getPattern()
+{
+ SPPattern *pat = nullptr;
+ g_return_val_if_fail(mode == MODE_PATTERN, NULL);
+
+ GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "patternmenu"));
+
+ /* no pattern menu if we were just selected */
+ if (combo == nullptr) {
+ return nullptr;
+ }
+
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo));
+
+ /* Get the selected pattern */
+ GtkTreeIter iter;
+ if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter) ||
+ !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) {
+ return nullptr;
+ }
+
+ gchar *patid = nullptr;
+ gboolean stockid = FALSE;
+ // gchar *label = nullptr;
+ gtk_tree_model_get(store, &iter,
+ // COMBO_COL_LABEL, &label,
+ COMBO_COL_STOCK, &stockid,
+ COMBO_COL_PATTERN, &patid, -1);
+ // g_free(label);
+ if (patid == nullptr) {
+ return nullptr;
+ }
+
+ if (strcmp(patid, "none") != 0) {
+ gchar *paturn;
+
+ if (stockid) {
+ paturn = g_strconcat("urn:inkscape:pattern:", patid, NULL);
+ } else {
+ paturn = g_strdup(patid);
+ }
+ SPObject *pat_obj = get_stock_item(paturn);
+ if (pat_obj) {
+ pat = SP_PATTERN(pat_obj);
+ }
+ g_free(paturn);
+ } else {
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ SPObject *pat_obj = doc->getObjectById(patid);
+
+ if (pat_obj && SP_IS_PATTERN(pat_obj)) {
+ pat = SP_PATTERN(pat_obj)->rootPattern();
+ }
+ }
+
+ g_free(patid);
+
+ return pat;
+}
+
+static void sp_paint_selector_set_mode_swatch(SPPaintSelector *psel, SPPaintSelector::Mode mode)
+{
+ if (mode == SPPaintSelector::MODE_SWATCH) {
+ sp_paint_selector_set_style_buttons(psel, psel->swatch);
+ }
+
+ gtk_widget_set_sensitive(psel->style, TRUE);
+
+ if (psel->mode == SPPaintSelector::MODE_SWATCH){
+ // swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(psel->selector), "swatch-selector"));
+ } else {
+ sp_paint_selector_clear_frame(psel);
+ // Create new gradient selector
+ SwatchSelector *swatchsel = new SwatchSelector();
+ swatchsel->show();
+
+ swatchsel->connectGrabbedHandler( G_CALLBACK(sp_paint_selector_gradient_grabbed), psel );
+ swatchsel->connectDraggedHandler( G_CALLBACK(sp_paint_selector_gradient_dragged), psel );
+ swatchsel->connectReleasedHandler( G_CALLBACK(sp_paint_selector_gradient_released), psel );
+ swatchsel->connectchangedHandler( G_CALLBACK(sp_paint_selector_gradient_changed), psel );
+
+ // Pack everything to frame
+ gtk_container_add(GTK_CONTAINER(psel->frame), GTK_WIDGET(swatchsel->gobj()));
+ psel->selector = GTK_WIDGET(swatchsel->gobj());
+ g_object_set_data(G_OBJECT(psel->selector), "swatch-selector", swatchsel);
+
+ gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Swatch fill</b>"));
+ }
+
+#ifdef SP_PS_VERBOSE
+ g_print("Swatch req\n");
+#endif
+}
+
+// TODO this seems very bad to be taking in a desktop pointer to muck with. Logic probably belongs elsewhere
+void SPPaintSelector::setFlatColor( SPDesktop *desktop, gchar const *color_property, gchar const *opacity_property )
+{
+ SPCSSAttr *css = sp_repr_css_attr_new();
+
+ SPColor color;
+ gfloat alpha = 0;
+ getColorAlpha( color, alpha );
+
+ std::string colorStr = color.toString();
+
+#ifdef SP_PS_VERBOSE
+ guint32 rgba = color.toRGBA32( alpha );
+ g_message("sp_paint_selector_set_flat_color() to '%s' from 0x%08x::%s",
+ colorStr.c_str(),
+ rgba,
+ (color.icc ? color.icc->colorProfile.c_str():"<null>") );
+#endif // SP_PS_VERBOSE
+
+ sp_repr_css_set_property(css, color_property, colorStr.c_str());
+ Inkscape::CSSOStringStream osalpha;
+ osalpha << alpha;
+ sp_repr_css_set_property(css, opacity_property, osalpha.str().c_str());
+
+ sp_desktop_set_style(desktop, css);
+
+ sp_repr_css_attr_unref(css);
+}
+
+SPPaintSelector::Mode SPPaintSelector::getModeForStyle(SPStyle const & style, FillOrStroke kind)
+{
+ Mode mode = MODE_UNSET;
+ SPIPaint const &target = *style.getFillOrStroke(kind == FILL);
+
+ if ( !target.set ) {
+ mode = MODE_UNSET;
+ } else if ( target.isPaintserver() ) {
+ SPPaintServer const *server = (kind == FILL) ? style.getFillPaintServer() : style.getStrokePaintServer();
+
+#ifdef SP_PS_VERBOSE
+ g_message("SPPaintSelector::getModeForStyle(%p, %d)", &style, kind);
+ g_message("==== server:%p %s grad:%s swatch:%s", server, server->getId(), (SP_IS_GRADIENT(server)?"Y":"n"), (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()?"Y":"n"));
+#endif // SP_PS_VERBOSE
+
+
+ if (server && SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) {
+ mode = MODE_SWATCH;
+ } else if (SP_IS_LINEARGRADIENT(server)) {
+ mode = MODE_GRADIENT_LINEAR;
+ } else if (SP_IS_RADIALGRADIENT(server)) {
+ mode = MODE_GRADIENT_RADIAL;
+#ifdef WITH_MESH
+ } else if (SP_IS_MESHGRADIENT(server)) {
+ mode = MODE_GRADIENT_MESH;
+#endif
+ } else if (SP_IS_PATTERN(server)) {
+ mode = MODE_PATTERN;
+ } else if (SP_IS_HATCH(server)) {
+ mode = MODE_HATCH;
+ } else {
+ g_warning( "file %s: line %d: Unknown paintserver", __FILE__, __LINE__ );
+ mode = MODE_NONE;
+ }
+ } else if ( target.isColor() ) {
+ // TODO this is no longer a valid assertion:
+ mode = MODE_SOLID_COLOR; // so far only rgb can be read from svg
+ } else if ( target.isNone() ) {
+ mode = MODE_NONE;
+ } else {
+ g_warning( "file %s: line %d: Unknown paint type", __FILE__, __LINE__ );
+ mode = MODE_NONE;
+ }
+
+ return mode;
+}
+
+/*
+ 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/paint-selector.h b/src/widgets/paint-selector.h
new file mode 100644
index 0000000..7d142f3
--- /dev/null
+++ b/src/widgets/paint-selector.h
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Generic paint selector widget
+ *//*
+ * Authors:
+ * Lauris
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_PAINT_SELECTOR_H
+#define SEEN_SP_PAINT_SELECTOR_H
+
+#include <glib.h>
+#include <gtk/gtk.h>
+
+#include "color.h"
+#include "fill-or-stroke.h"
+
+#include "object/sp-gradient-spread.h"
+#include "object/sp-gradient-units.h"
+
+#include "ui/selected-color.h"
+
+class SPGradient;
+#ifdef WITH_MESH
+class SPMeshGradient;
+#endif
+class SPDesktop;
+class SPPattern;
+class SPStyle;
+
+#define SP_TYPE_PAINT_SELECTOR (sp_paint_selector_get_type ())
+#define SP_PAINT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_PAINT_SELECTOR, SPPaintSelector))
+#define SP_PAINT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_PAINT_SELECTOR, SPPaintSelectorClass))
+#define SP_IS_PAINT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_PAINT_SELECTOR))
+#define SP_IS_PAINT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_PAINT_SELECTOR))
+
+/**
+ * Generic paint selector widget.
+ */
+struct SPPaintSelector {
+ GtkBox vbox;
+
+ enum Mode {
+ MODE_EMPTY,
+ MODE_MULTIPLE,
+ MODE_NONE,
+ MODE_SOLID_COLOR,
+ MODE_GRADIENT_LINEAR,
+ MODE_GRADIENT_RADIAL,
+#ifdef WITH_MESH
+ MODE_GRADIENT_MESH,
+#endif
+ MODE_PATTERN,
+ MODE_HATCH,
+ MODE_SWATCH,
+ MODE_UNSET
+ } ;
+
+ enum FillRule {
+ FILLRULE_NONZERO,
+ FILLRULE_EVENODD
+ } ;
+
+ guint update : 1;
+
+ Mode mode;
+
+ GtkWidget *style;
+ GtkWidget *none;
+ GtkWidget *solid;
+ GtkWidget *gradient;
+ GtkWidget *radial;
+#ifdef WITH_MESH
+ GtkWidget *mesh;
+#endif
+ GtkWidget *pattern;
+ GtkWidget *swatch;
+ GtkWidget *unset;
+
+ GtkWidget *fillrulebox;
+ GtkWidget *evenodd, *nonzero;
+
+ GtkWidget *frame, *selector;
+ GtkWidget *label;
+
+ Inkscape::UI::SelectedColor *selected_color;
+ bool updating_color;
+
+ static Mode getModeForStyle(SPStyle const & style, FillOrStroke kind);
+
+ void setMode( Mode mode );
+ void setFillrule( FillRule fillrule );
+
+ void setColorAlpha( SPColor const &color, float alpha );
+ void getColorAlpha( SPColor &color, gfloat &alpha ) const;
+
+ void setGradientLinear( SPGradient *vector );
+ void setGradientRadial( SPGradient *vector );
+#ifdef WITH_MESH
+ void setGradientMesh(SPMeshGradient *array);
+#endif
+ void setSwatch( SPGradient *vector );
+
+ void setGradientProperties( SPGradientUnits units, SPGradientSpread spread );
+ void getGradientProperties( SPGradientUnits &units, SPGradientSpread &spread ) const;
+
+ void pushAttrsToGradient( SPGradient *gr ) const;
+ SPGradient *getGradientVector();
+
+#ifdef WITH_MESH
+ SPMeshGradient * getMeshGradient();
+ void updateMeshList( SPMeshGradient *pat );
+#endif
+
+ SPPattern * getPattern();
+ void updatePatternList( SPPattern *pat );
+
+ static gboolean isSeparator (GtkTreeModel *model, GtkTreeIter *iter, gpointer data);
+
+ // TODO move this elsewhere:
+ void setFlatColor( SPDesktop *desktop, const gchar *color_property, const gchar *opacity_property );
+
+ void onSelectedColorGrabbed();
+ void onSelectedColorDragged();
+ void onSelectedColorReleased();
+ void onSelectedColorChanged();
+};
+
+enum {
+ COMBO_COL_LABEL = 0,
+ COMBO_COL_STOCK = 1,
+ COMBO_COL_PATTERN = 2,
+ COMBO_COL_MESH = COMBO_COL_PATTERN,
+ COMBO_COL_SEP = 3,
+ COMBO_N_COLS = 4
+};
+
+/// The SPPaintSelector vtable
+struct SPPaintSelectorClass {
+ GtkBoxClass parent_class;
+
+ void (* mode_changed) (SPPaintSelector *psel, SPPaintSelector::Mode mode);
+
+ void (* grabbed) (SPPaintSelector *psel);
+ void (* dragged) (SPPaintSelector *psel);
+ void (* released) (SPPaintSelector *psel);
+ void (* changed) (SPPaintSelector *psel);
+ void (* fillrule_changed) (SPPaintSelector *psel, SPPaintSelector::FillRule fillrule);
+};
+
+GType sp_paint_selector_get_type ();
+
+SPPaintSelector *sp_paint_selector_new(FillOrStroke kind);
+
+
+
+#endif // SEEN_SP_PAINT_SELECTOR_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..a82d06d
--- /dev/null
+++ b/src/widgets/sp-attribute-widget.cpp
@@ -0,0 +1,303 @@
+// 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 "verbs.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 0
+
+
+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 || SP_IS_OBJECT (object));
+ 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();
+ ll->set_margin_start(XPAD);
+ ll->set_margin_end(XPAD);
+ ll->set_margin_top(XPAD);
+ ll->set_margin_bottom(XPAD);
+ 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();
+ ee->set_margin_start(XPAD);
+ ee->set_margin_end(XPAD);
+ ee->set_margin_top(XPAD);
+ ee->set_margin_bottom(XPAD);
+ 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)
+{
+ g_return_if_fail (!object || SP_IS_OBJECT (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, SP_VERB_NONE,
+ _("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..f43fe84
--- /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/widget.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::Widget {
+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-color-selector.cpp b/src/widgets/sp-color-selector.cpp
new file mode 100644
index 0000000..c1c6975
--- /dev/null
+++ b/src/widgets/sp-color-selector.cpp
@@ -0,0 +1,334 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors:
+ * see git history
+ * bulia byak <buliabyak@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cmath>
+#include <gtk/gtk.h>
+#include <glibmm/i18n.h>
+#include "sp-color-selector.h"
+
+enum {
+ GRABBED,
+ DRAGGED,
+ RELEASED,
+ CHANGED,
+ LAST_SIGNAL
+};
+
+#define noDUMP_CHANGE_INFO
+#define FOO_NAME(x) g_type_name( G_TYPE_FROM_INSTANCE(x) )
+
+static void sp_color_selector_dispose(GObject *object);
+
+static void sp_color_selector_show_all( GtkWidget *widget );
+static void sp_color_selector_hide( GtkWidget *widget );
+
+static guint csel_signals[LAST_SIGNAL] = {0};
+
+double ColorSelector::_epsilon = 1e-4;
+
+G_DEFINE_TYPE(SPColorSelector, sp_color_selector, GTK_TYPE_BOX);
+
+void sp_color_selector_class_init( SPColorSelectorClass *klass )
+{
+ static const gchar* nameset[] = {N_("Unnamed"), nullptr};
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ GtkWidgetClass *widget_class;
+ widget_class = GTK_WIDGET_CLASS(klass);
+
+ csel_signals[GRABBED] = g_signal_new( "grabbed",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPColorSelectorClass, grabbed),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0 );
+ csel_signals[DRAGGED] = g_signal_new( "dragged",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPColorSelectorClass, dragged),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0 );
+ csel_signals[RELEASED] = g_signal_new( "released",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPColorSelectorClass, released),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0 );
+ csel_signals[CHANGED] = g_signal_new( "changed",
+ G_TYPE_FROM_CLASS(object_class),
+ (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE),
+ G_STRUCT_OFFSET(SPColorSelectorClass, changed),
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0 );
+
+ klass->name = nameset;
+ klass->submode_count = 1;
+
+ object_class->dispose = sp_color_selector_dispose;
+
+ widget_class->show_all = sp_color_selector_show_all;
+ widget_class->hide = sp_color_selector_hide;
+
+}
+
+void sp_color_selector_init( SPColorSelector *csel )
+{
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(csel), GTK_ORIENTATION_VERTICAL);
+
+ if ( csel->base )
+ {
+ csel->base->init();
+ }
+/* g_signal_connect(G_OBJECT(csel->rgbae), "changed", G_CALLBACK(sp_color_selector_rgba_entry_changed), csel); */
+}
+
+void sp_color_selector_dispose(GObject *object)
+{
+ SPColorSelector *csel = SP_COLOR_SELECTOR( object );
+ if ( csel->base )
+ {
+ delete csel->base;
+ csel->base = nullptr;
+ }
+
+ if ((G_OBJECT_CLASS(sp_color_selector_parent_class))->dispose ) {
+ (G_OBJECT_CLASS(sp_color_selector_parent_class))->dispose(object);
+ }
+}
+
+void sp_color_selector_show_all( GtkWidget *widget )
+{
+ gtk_widget_show( widget );
+}
+
+void sp_color_selector_hide(GtkWidget *widget)
+{
+ gtk_widget_hide( widget );
+}
+
+GtkWidget *sp_color_selector_new( GType selector_type )
+{
+ g_return_val_if_fail( g_type_is_a( selector_type, SP_TYPE_COLOR_SELECTOR ), NULL );
+
+ SPColorSelector *csel = SP_COLOR_SELECTOR( g_object_new( selector_type, nullptr ) );
+
+ return GTK_WIDGET( csel );
+}
+
+void ColorSelector::setSubmode( guint /*submode*/ )
+{
+}
+
+guint ColorSelector::getSubmode() const
+{
+ guint mode = 0;
+ return mode;
+}
+
+ColorSelector::ColorSelector( SPColorSelector* csel )
+ : _csel(csel),
+ _color( 0 ),
+ _alpha(1.0),
+ _held(FALSE),
+ virgin(true)
+{
+ g_return_if_fail( SP_IS_COLOR_SELECTOR(_csel) );
+}
+
+ColorSelector::~ColorSelector()
+= default;
+
+void ColorSelector::init()
+{
+ _csel->base = new ColorSelector( _csel );
+}
+
+void ColorSelector::setColor( const SPColor& color )
+{
+ setColorAlpha( color, _alpha );
+}
+
+SPColor ColorSelector::getColor() const
+{
+ return _color;
+}
+
+void ColorSelector::setAlpha( gfloat alpha )
+{
+ g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) );
+ setColorAlpha( _color, alpha );
+}
+
+gfloat ColorSelector::getAlpha() const
+{
+ return _alpha;
+}
+
+/**
+Called from the outside to set the color; optionally emits signal (only when called from
+downstream, e.g. the RGBA value field, but not from the rest of the program)
+*/
+void ColorSelector::setColorAlpha( const SPColor& color, gfloat alpha, bool emit )
+{
+#ifdef DUMP_CHANGE_INFO
+ g_message("ColorSelector::setColorAlpha( this=%p, %f, %f, %f, %s, %f, %s) in %s", this, color.v.c[0], color.v.c[1], color.v.c[2], (color.icc?color.icc->colorProfile.c_str():"<null>"), alpha, (emit?"YES":"no"), FOO_NAME(_csel));
+#endif
+ g_return_if_fail( _csel != nullptr );
+ g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) );
+
+#ifdef DUMP_CHANGE_INFO
+ g_message("---- ColorSelector::setColorAlpha virgin:%s !close:%s alpha is:%s in %s",
+ (virgin?"YES":"no"),
+ (!color.isClose( _color, _epsilon )?"YES":"no"),
+ ((fabs((_alpha) - (alpha)) >= _epsilon )?"YES":"no"),
+ FOO_NAME(_csel)
+ );
+#endif
+
+ if ( virgin || !color.isClose( _color, _epsilon ) ||
+ (fabs((_alpha) - (alpha)) >= _epsilon )) {
+
+ virgin = false;
+
+ _color = color;
+ _alpha = alpha;
+ _colorChanged();
+
+ if (emit) {
+ g_signal_emit(G_OBJECT(_csel), csel_signals[CHANGED], 0);
+ }
+#ifdef DUMP_CHANGE_INFO
+ } else {
+ g_message("++++ ColorSelector::setColorAlpha color:%08x ==> _color:%08X isClose:%s in %s", color.toRGBA32(alpha), _color.toRGBA32(_alpha),
+ (color.isClose( _color, _epsilon )?"YES":"no"), FOO_NAME(_csel));
+#endif
+ }
+}
+
+void ColorSelector::_grabbed()
+{
+ _held = TRUE;
+#ifdef DUMP_CHANGE_INFO
+ g_message("%s:%d: About to signal %s in %s", __FILE__, __LINE__,
+ "GRABBED",
+ FOO_NAME(_csel));
+#endif
+ g_signal_emit(G_OBJECT(_csel), csel_signals[GRABBED], 0);
+}
+
+void ColorSelector::_released()
+{
+ _held = false;
+#ifdef DUMP_CHANGE_INFO
+ g_message("%s:%d: About to signal %s in %s", __FILE__, __LINE__,
+ "RELEASED",
+ FOO_NAME(_csel));
+#endif
+ g_signal_emit(G_OBJECT(_csel), csel_signals[RELEASED], 0);
+ g_signal_emit(G_OBJECT(_csel), csel_signals[CHANGED], 0);
+}
+
+// Called from subclasses to update color and broadcast if needed
+void ColorSelector::_updateInternals( const SPColor& color, gfloat alpha, gboolean held )
+{
+ g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) );
+ gboolean colorDifferent = ( !color.isClose( _color, _epsilon )
+ || ( fabs((_alpha) - (alpha)) >= _epsilon ) );
+
+ gboolean grabbed = held && !_held;
+ gboolean released = !held && _held;
+
+ // Store these before emitting any signals
+ _held = held;
+ if ( colorDifferent )
+ {
+ _color = color;
+ _alpha = alpha;
+ }
+
+ if ( grabbed )
+ {
+#ifdef DUMP_CHANGE_INFO
+ g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__,
+ "GRABBED",
+ color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel));
+#endif
+ g_signal_emit(G_OBJECT(_csel), csel_signals[GRABBED], 0);
+ }
+ else if ( released )
+ {
+#ifdef DUMP_CHANGE_INFO
+ g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__,
+ "RELEASED",
+ color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel));
+#endif
+ g_signal_emit(G_OBJECT(_csel), csel_signals[RELEASED], 0);
+ }
+
+ if ( colorDifferent || released )
+ {
+#ifdef DUMP_CHANGE_INFO
+ g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__,
+ (_held ? "CHANGED" : "DRAGGED" ),
+ color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel));
+#endif
+ g_signal_emit(G_OBJECT(_csel), csel_signals[_held ? DRAGGED : CHANGED], 0);
+ }
+}
+
+/**
+ * Called once the color actually changes. Allows subclasses to react to changes.
+ */
+void ColorSelector::_colorChanged()
+{
+}
+
+void ColorSelector::getColorAlpha( SPColor &color, gfloat &alpha ) const
+{
+ gint i = 0;
+
+ color = _color;
+ alpha = _alpha;
+
+ // Try to catch uninitialized value usage
+ if ( color.v.c[0] )
+ {
+ i++;
+ }
+ if ( color.v.c[1] )
+ {
+ i++;
+ }
+ if ( color.v.c[2] )
+ {
+ i++;
+ }
+ if ( alpha )
+ {
+ i++;
+ }
+}
+
+/*
+ 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-color-selector.h b/src/widgets/sp-color-selector.h
new file mode 100644
index 0000000..eb77250
--- /dev/null
+++ b/src/widgets/sp-color-selector.h
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_COLOR_SELECTOR_H
+#define SEEN_SP_COLOR_SELECTOR_H
+
+#include <gtk/gtk.h>
+#include "color.h"
+
+struct SPColorSelector;
+
+class ColorSelector
+{
+public:
+ ColorSelector( SPColorSelector* csel );
+ virtual ~ColorSelector();
+
+ virtual void init();
+
+ void setColor( const SPColor& color );
+ SPColor getColor() const;
+
+ void setAlpha( gfloat alpha );
+ gfloat getAlpha() const;
+
+ void setColorAlpha( const SPColor& color, gfloat alpha, bool emit = false );
+ void getColorAlpha( SPColor &color, gfloat &alpha ) const;
+
+ virtual void setSubmode( guint submode );
+ virtual guint getSubmode() const;
+
+protected:
+ void _grabbed();
+ void _released();
+ void _updateInternals( const SPColor& color, gfloat alpha, gboolean held );
+ gboolean _isHeld() const { return _held; }
+
+ virtual void _colorChanged();
+
+ static double _epsilon;
+
+ SPColorSelector* _csel;
+ SPColor _color;
+ gfloat _alpha; // guaranteed to be in [0, 1].
+
+private:
+ // By default, disallow copy constructor and assignment operator
+ ColorSelector( const ColorSelector& obj ) = delete;
+ ColorSelector& operator=( const ColorSelector& obj ) = delete;
+
+ gboolean _held;
+
+ bool virgin; // if true, no color is set yet
+};
+
+
+
+#define SP_TYPE_COLOR_SELECTOR (sp_color_selector_get_type ())
+#define SP_COLOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_COLOR_SELECTOR, SPColorSelector))
+#define SP_COLOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_COLOR_SELECTOR, SPColorSelectorClass))
+#define SP_IS_COLOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_COLOR_SELECTOR))
+#define SP_IS_COLOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_COLOR_SELECTOR))
+#define SP_COLOR_SELECTOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SP_TYPE_COLOR_SELECTOR, SPColorSelectorClass))
+
+struct SPColorSelector {
+ GtkBox vbox;
+ ColorSelector* base;
+};
+
+struct SPColorSelectorClass {
+ GtkBoxClass parent_class;
+
+ const gchar **name;
+ guint submode_count;
+
+ void (* grabbed) (SPColorSelector *rgbsel);
+ void (* dragged) (SPColorSelector *rgbsel);
+ void (* released) (SPColorSelector *rgbsel);
+ void (* changed) (SPColorSelector *rgbsel);
+};
+
+GType sp_color_selector_get_type();
+
+GtkWidget *sp_color_selector_new( GType selector_type );
+
+
+
+#endif // SEEN_SP_COLOR_SELECTOR_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-xmlview-tree.cpp b/src/widgets/sp-xmlview-tree.cpp
new file mode 100644
index 0000000..6f45e64
--- /dev/null
+++ b/src/widgets/sp-xmlview-tree.cpp
@@ -0,0 +1,866 @@
+// 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 <cstring>
+
+#include "xml/node-event-vector.h"
+#include "sp-xmlview-tree.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;
+
+ 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_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 void element_child_added (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer data);
+static void element_attr_changed (Inkscape::XML::Node * repr, const gchar * key, const gchar * old_value, const gchar * new_value, bool is_interactive, gpointer data);
+static void element_child_removed (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer data);
+static void element_order_changed (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * oldref, Inkscape::XML::Node * newref, gpointer data);
+static void element_name_changed (Inkscape::XML::Node* repr, gchar const* oldname, gchar const* newname, gpointer data);
+static void element_attr_or_name_change_update(Inkscape::XML::Node* repr, NodeData* data);
+
+static void text_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data);
+static void comment_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data);
+static void pi_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data);
+
+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 const Inkscape::XML::NodeEventVector element_repr_events = {
+ element_child_added,
+ element_child_removed,
+ element_attr_changed,
+ nullptr, /* content_changed */
+ element_order_changed,
+ element_name_changed
+};
+
+static const Inkscape::XML::NodeEventVector text_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ nullptr, /* attr_changed */
+ text_content_changed,
+ nullptr /* order_changed */,
+ nullptr /* element_name_changed */
+};
+
+static const Inkscape::XML::NodeEventVector comment_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ nullptr, /* attr_changed */
+ comment_content_changed,
+ nullptr /* order_changed */,
+ nullptr /* element_name_changed */
+};
+
+static const Inkscape::XML::NodeEventVector pi_repr_events = {
+ nullptr, /* child_added */
+ nullptr, /* child_removed */
+ nullptr, /* attr_changed */
+ pi_content_changed,
+ nullptr /* order_changed */,
+ nullptr /* element_name_changed */
+};
+
+/**
+ * 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;
+ sp_repr_synthesize_events(data->repr, &element_repr_events, data);
+ }
+
+ return false;
+}
+
+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));
+
+ 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);
+
+ GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
+ GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes ("", renderer, "text", STORE_TEXT_COL, NULL);
+ 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);
+
+ 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;
+
+ // Signal for when a tree drag and drop has completed
+ g_signal_new ( "tree_move",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ nullptr, nullptr,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1,
+ G_TYPE_UINT);
+}
+
+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);
+
+ 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)
+{
+ const Inkscape::XML::NodeEventVector * vec;
+
+ 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::TEXT_NODE ) {
+ vec = &text_repr_events;
+ } else if ( repr->type() == Inkscape::XML::COMMENT_NODE ) {
+ vec = &comment_repr_events;
+ } else if ( repr->type() == Inkscape::XML::PI_NODE ) {
+ vec = &pi_repr_events;
+ } else if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) {
+ vec = &element_repr_events;
+ } else {
+ vec = nullptr;
+ }
+
+ if (vec) {
+ /* cheat a little to get the text updated on nodes without id */
+ if (repr->type() == Inkscape::XML::ELEMENT_NODE && repr->attribute("id") == nullptr) {
+ element_attr_changed (repr, "id", nullptr, nullptr, false, data);
+ }
+ sp_repr_add_listener (repr, vec, data);
+ sp_repr_synthesize_events (repr, vec, data);
+ }
+}
+
+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) {
+ sp_repr_remove_listener_by_data(repr, this);
+ Inkscape::GC::release(repr);
+ }
+ gtk_tree_row_reference_free(rowref);
+}
+
+void element_child_added (Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer ptr)
+{
+ NodeData *data = static_cast<NodeData *>(ptr);
+ GtkTreeIter before;
+
+ if (data->tree->blocked) return;
+
+ if (!ref_to_sibling (data, ref, &before)) {
+ return;
+ }
+
+ GtkTreeIter data_iter;
+ tree_ref_to_iter(data->tree, &data_iter, data->rowref);
+
+ if (!data->expanded) {
+ auto model = GTK_TREE_MODEL(data->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 (data->tree, &data_iter, &before, child);
+}
+
+void element_attr_changed(
+ Inkscape::XML::Node* repr, gchar const* key,
+ gchar const* /*old_value*/, gchar const* /*new_value*/, bool /*is_interactive*/,
+ gpointer ptr)
+{
+ if (0 != strcmp (key, "id") && 0 != strcmp (key, "inkscape:label"))
+ return;
+ element_attr_or_name_change_update(repr, static_cast<NodeData*>(ptr));
+}
+
+void element_name_changed(
+ Inkscape::XML::Node* repr,
+ gchar const* /*oldname*/, gchar const* /*newname*/, gpointer ptr)
+{
+ element_attr_or_name_change_update(repr, static_cast<NodeData*>(ptr));
+}
+
+void element_attr_or_name_change_update(Inkscape::XML::Node* repr, NodeData* data)
+{
+ if (data->tree->blocked) {
+ return;
+ }
+
+ gchar const* node_name = repr->name();
+ gchar const* id_value = repr->attribute("id");
+ gchar const* label_value = repr->attribute("inkscape:label");
+ gchar* display_text;
+
+ if (id_value && label_value) {
+ display_text = g_strdup_printf ("<%s id=\"%s\" inkscape:label=\"%s\">", node_name, id_value, label_value);
+ } else if (id_value) {
+ display_text = g_strdup_printf ("<%s id=\"%s\">", node_name, id_value);
+ } else if (label_value) {
+ display_text = g_strdup_printf ("<%s inkscape:label=\"%s\">", node_name, label_value);
+ } else {
+ display_text = g_strdup_printf ("<%s>", node_name);
+ }
+
+ GtkTreeIter iter;
+ if (tree_ref_to_iter(data->tree, &iter, data->rowref)) {
+ gtk_tree_store_set (GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, display_text, -1);
+ }
+
+ g_free(display_text);
+}
+
+void element_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node * /*ref*/,
+ gpointer ptr)
+{
+ NodeData *data = static_cast<NodeData *>(ptr);
+
+ if (data->tree->blocked) return;
+
+ GtkTreeIter iter;
+ if (repr_to_child(data, child, &iter)) {
+ delete sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(data->tree->store), &iter);
+ gtk_tree_store_remove(data->tree->store, &iter);
+ } else if (!repr->firstChild() && get_first_child(data, &iter)) {
+ // remove dummy when all children gone
+ remove_dummy_rows(data->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(data->tree)));
+#endif
+}
+
+void element_order_changed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node * child, Inkscape::XML::Node * /*oldref*/, Inkscape::XML::Node * newref, gpointer ptr)
+{
+ NodeData *data = static_cast<NodeData *>(ptr);
+ GtkTreeIter before, node;
+
+ if (data->tree->blocked) return;
+
+ ref_to_sibling (data, newref, &before);
+ repr_to_child (data, child, &node);
+
+ if (gtk_tree_store_iter_is_valid(data->tree->store, &before)) {
+ gtk_tree_store_move_before (data->tree->store, &node, &before);
+ } else {
+ repr_to_child (data, newref, &before);
+ gtk_tree_store_move_after (data->tree->store, &node, &before);
+ }
+}
+
+Glib::ustring sp_remove_newlines_and_tabs(Glib::ustring val)
+{
+ int pos;
+ Glib::ustring newlinesign = "␤";
+ Glib::ustring tabsign = "⇥";
+ while ((pos = val.find("\r\n")) != std::string::npos) {
+ val.erase(pos, 2);
+ val.insert(pos, newlinesign);
+ }
+ while ((pos = val.find('\n')) != std::string::npos) {
+ val.erase(pos, 1);
+ val.insert(pos, newlinesign);
+ }
+ while ((pos = val.find('\t')) != std::string::npos) {
+ val.erase(pos, 1);
+ val.insert(pos, tabsign);
+ }
+ return val;
+}
+
+void text_content_changed(Inkscape::XML::Node * /*repr*/, const gchar * /*old_content*/, const gchar * new_content, gpointer ptr)
+{
+ NodeData *data = static_cast<NodeData *>(ptr);
+
+ if (data->tree->blocked) return;
+
+ gchar *label = g_strdup_printf ("\"%s\"", new_content);
+ Glib::ustring nolinecontent = label;
+ nolinecontent = sp_remove_newlines_and_tabs(nolinecontent);
+
+ GtkTreeIter iter;
+ if (tree_ref_to_iter(data->tree, &iter, data->rowref)) {
+ gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1);
+ }
+
+ g_free (label);
+}
+
+void comment_content_changed(Inkscape::XML::Node * /*repr*/, const gchar * /*old_content*/, const gchar *new_content, gpointer ptr)
+{
+ NodeData *data = static_cast<NodeData*>(ptr);
+
+ if (data->tree->blocked) return;
+
+ gchar *label = g_strdup_printf ("<!--%s-->", new_content);
+ Glib::ustring nolinecontent = label;
+ nolinecontent = sp_remove_newlines_and_tabs(nolinecontent);
+
+ GtkTreeIter iter;
+ if (tree_ref_to_iter(data->tree, &iter, data->rowref)) {
+ gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1);
+ }
+ g_free (label);
+}
+
+void pi_content_changed(Inkscape::XML::Node *repr, const gchar * /*old_content*/, const gchar *new_content, gpointer ptr)
+{
+ NodeData *data = static_cast<NodeData *>(ptr);
+
+ if (data->tree->blocked) return;
+
+ gchar *label = g_strdup_printf ("<?%s %s?>", repr->name(), new_content);
+ Glib::ustring nolinecontent = label;
+ nolinecontent = sp_remove_newlines_and_tabs(nolinecontent);
+
+ GtkTreeIter iter;
+ if (tree_ref_to_iter(data->tree, &iter, data->rowref)) {
+ gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1);
+ }
+ g_free (label);
+}
+
+/*
+ * 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 happended 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
+ g_signal_emit_by_name(G_OBJECT(tree), "tree_move", GUINT_TO_POINTER(1));
+ }
+}
+
+/*
+ * 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::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);
+
+ 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..8839ee9
--- /dev/null
+++ b/src/widgets/sp-xmlview-tree.h
@@ -0,0 +1,64 @@
+// 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>
+
+/**
+ * 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;
+
+struct SPXMLViewTree
+{
+ GtkTreeView tree;
+ GtkTreeStore *store;
+ Inkscape::XML::Node * repr;
+ gint blocked;
+};
+
+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/spinbutton-events.cpp b/src/widgets/spinbutton-events.cpp
new file mode 100644
index 0000000..9800cf5
--- /dev/null
+++ b/src/widgets/spinbutton-events.cpp
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Common callbacks for spinbuttons
+ *
+ * Authors:
+ * bulia byak <bulia@users.sourceforge.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2013 authors
+ * Copyright (C) 2003 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "ui/tools/tool-base.h"
+
+#include "spinbutton-events.h"
+
+gboolean spinbutton_focus_in(GtkWidget *w, GdkEventKey * /*event*/, gpointer /*data*/)
+{
+ gdouble *ini = static_cast<gdouble *>(g_object_get_data(G_OBJECT(w), "ini"));
+ if (ini) {
+ g_free(ini); // free the old value if any
+ }
+
+ // retrieve the value
+ ini = g_new(gdouble, 1);
+ *ini = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w));
+
+ // remember it
+ g_object_set_data(G_OBJECT(w), "ini", ini);
+
+ return FALSE; // I didn't consume the event
+}
+
+void spinbutton_undo(GtkWidget *w)
+{
+ gdouble *ini = static_cast<gdouble *>(g_object_get_data(G_OBJECT(w), "ini"));
+ if (ini) {
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), *ini);
+ }
+}
+
+void spinbutton_defocus(GtkWidget *container)
+{
+ // defocus spinbuttons by moving focus to the canvas, unless "stay" is on
+ gboolean stay = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(container), "stay"));
+ if (stay) {
+ g_object_set_data(G_OBJECT(container), "stay", GINT_TO_POINTER(FALSE));
+ } else {
+ GtkWidget *canvas = GTK_WIDGET(g_object_get_data(G_OBJECT(container), "dtw"));
+ if (canvas) {
+ gtk_widget_grab_focus(GTK_WIDGET(canvas));
+ }
+ }
+}
+
+gboolean spinbutton_keypress(GtkWidget *w, GdkEventKey *event, gpointer /*data*/)
+{
+ gboolean result = FALSE; // I didn't consume the event
+
+ switch (Inkscape::UI::Tools::get_latin_keyval(event)) {
+ case GDK_KEY_Escape: // defocus
+ spinbutton_undo(w);
+ spinbutton_defocus(w);
+ result = TRUE; // I consumed the event
+ break;
+ case GDK_KEY_Return: // defocus
+ case GDK_KEY_KP_Enter:
+ spinbutton_defocus(w);
+ result = TRUE; // I consumed the event
+ break;
+ case GDK_KEY_Tab:
+ case GDK_KEY_ISO_Left_Tab:
+ // set the flag meaning "do not leave toolbar when changing value"
+ g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE));
+ result = FALSE; // I didn't consume the event
+ break;
+
+ // The following keys are processed manually because GTK implements them in strange ways
+ // (increments start with double step value and seem to grow as you press the key continuously)
+
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ {
+ g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE));
+ gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w));
+ gdouble step = 0;
+ gdouble page = 0;
+ gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page);
+ v += step;
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v);
+ result = TRUE; // I consumed the event
+ break;
+ }
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ {
+ g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE));
+ gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w));
+ gdouble step = 0;
+ gdouble page = 0;
+ gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page);
+ v -= step;
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v);
+ result = TRUE; // I consumed the event
+ break;
+ }
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up:
+ {
+ g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE));
+ gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w));
+ gdouble step = 0;
+ gdouble page = 0;
+ gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page);
+ v += page;
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v);
+ result = TRUE; // I consumed the event
+ break;
+ }
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down:
+ {
+ g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE));
+ gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w));
+ gdouble step = 0;
+ gdouble page = 0;
+ gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page);
+ v -= page;
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v);
+ result = TRUE; // I consumed the event
+ break;
+ }
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE));
+ if (event->state & GDK_CONTROL_MASK) {
+ spinbutton_undo(w);
+ result = TRUE; // I consumed the event
+ }
+ break;
+ default:
+ result = FALSE;
+ break;
+ }
+
+ return result;
+}
+
+/*
+ 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/spinbutton-events.h b/src/widgets/spinbutton-events.h
new file mode 100644
index 0000000..e32ef18
--- /dev/null
+++ b/src/widgets/spinbutton-events.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Common callbacks for spinbuttons
+ *
+ * Authors:
+ * bulia byak <bulia@users.sourceforge.net>
+ *
+ * Copyright (C) 2003 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glib.h>
+
+typedef struct _GdkEventKey GdkEventKey;
+typedef struct _GtkWidget GtkWidget;
+
+gboolean spinbutton_focus_in (GtkWidget *w, GdkEventKey *event, gpointer data);
+void spinbutton_undo (GtkWidget *w);
+gboolean spinbutton_keypress (GtkWidget *w, GdkEventKey *event, gpointer data);
+void spinbutton_defocus (GtkWidget *container);
+
+/*
+ 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.cpp b/src/widgets/spw-utilities.cpp
new file mode 100644
index 0000000..38759ef
--- /dev/null
+++ b/src/widgets/spw-utilities.cpp
@@ -0,0 +1,160 @@
+// 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::HBox * spw_hbox(Gtk::Grid * table, int width, int col, int row)
+{
+ /* Create a new hbox with a 4-pixel spacing between children */
+ Gtk::HBox *hb = new Gtk::HBox(false, 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;
+}
+
+/*
+ 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..4751e81
--- /dev/null
+++ b/src/widgets/spw-utilities.h
@@ -0,0 +1,48 @@
+// 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.
+*/
+
+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::HBox * 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);
+#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/stroke-marker-selector.cpp b/src/widgets/stroke-marker-selector.cpp
new file mode 100644
index 0000000..337ba16
--- /dev/null
+++ b/src/widgets/stroke-marker-selector.cpp
@@ -0,0 +1,567 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Combobox for selecting dash patterns - implementation.
+ */
+/* Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "stroke-marker-selector.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/icontheme.h>
+
+#include "desktop-style.h"
+#include "path-prefix.h"
+#include "stroke-style.h"
+
+#include "helper/stock-items.h"
+#include "ui/icon-loader.h"
+
+#include "io/sys.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-marker.h"
+#include "object/sp-root.h"
+#include "style.h"
+
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/dialog-events.h"
+#include "ui/util.h"
+#include "ui/widget/spinbutton.h"
+
+static Inkscape::UI::Cache::SvgPreview svg_preview_cache;
+
+MarkerComboBox::MarkerComboBox(gchar const *id, int l) :
+ Gtk::ComboBox(),
+ combo_id(id),
+ loc(l),
+ updating(false),
+ markerCount(0)
+{
+
+ marker_store = Gtk::ListStore::create(marker_columns);
+ set_model(marker_store);
+ pack_start(image_renderer, false);
+ set_cell_data_func(image_renderer, sigc::mem_fun(*this, &MarkerComboBox::prepareImageRenderer));
+ gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(gobj()), MarkerComboBox::separator_cb, nullptr, nullptr);
+ empty_image = sp_get_icon_image("no-marker", Gtk::ICON_SIZE_SMALL_TOOLBAR);
+
+ sandbox = ink_markers_preview_doc ();
+
+ init_combo();
+ this->get_style_context()->add_class("combobright");
+
+ show();
+}
+
+MarkerComboBox::~MarkerComboBox() {
+ delete combo_id;
+ delete sandbox;
+ delete empty_image;
+
+ if (doc) {
+ modified_connection.disconnect();
+ }
+}
+
+void MarkerComboBox::setDocument(SPDocument *document)
+{
+ if (doc != document) {
+
+ if (doc) {
+ modified_connection.disconnect();
+ }
+
+ doc = document;
+
+ if (doc) {
+ modified_connection = doc->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&MarkerComboBox::handleDefsModified), this))) );
+ }
+
+ refreshHistory();
+ }
+}
+
+void
+MarkerComboBox::handleDefsModified(MarkerComboBox *self)
+{
+ self->refreshHistory();
+}
+
+void
+MarkerComboBox::refreshHistory()
+{
+ if (updating)
+ return;
+
+ updating = true;
+
+ std::vector<SPMarker *> ml = get_marker_list(doc);
+
+ /*
+ * Seems to be no way to get notified of changes just to markers,
+ * so listen to changes in all defs and check if the number of markers has changed here
+ * to avoid unnecessary refreshes when things like gradients change
+ */
+ if (markerCount != ml.size()) {
+ const char *active = get_active()->get_value(marker_columns.marker);
+ sp_marker_list_from_doc(doc, true);
+ set_selected(active);
+ markerCount = ml.size();
+ }
+
+ updating = false;
+}
+
+/**
+ * Init the combobox widget to display markers from markers.svg
+ */
+void
+MarkerComboBox::init_combo()
+{
+ if (updating)
+ return;
+
+ static SPDocument *markers_doc = nullptr;
+
+ // add separator
+ Gtk::TreeModel::Row row_sep = *(marker_store->append());
+ row_sep[marker_columns.label] = "Separator";
+ row_sep[marker_columns.marker] = g_strdup("None");
+ row_sep[marker_columns.image] = NULL;
+ row_sep[marker_columns.stock] = false;
+ row_sep[marker_columns.history] = false;
+ row_sep[marker_columns.separator] = true;
+
+ // find and load markers.svg
+ if (markers_doc == nullptr) {
+ char *markers_source = g_build_filename(INKSCAPE_MARKERSDIR, "markers.svg", NULL);
+ if (Inkscape::IO::file_test(markers_source, G_FILE_TEST_IS_REGULAR)) {
+ markers_doc = SPDocument::createNewDoc(markers_source, FALSE);
+ }
+ g_free(markers_source);
+ }
+
+ // load markers from markers.svg
+ if (markers_doc) {
+ sp_marker_list_from_doc(markers_doc, false);
+ }
+
+ set_sensitive(true);
+}
+
+/**
+ * Sets the current marker in the marker combobox.
+ */
+void MarkerComboBox::set_current(SPObject *marker)
+{
+ updating = true;
+
+ if (marker != nullptr) {
+ gchar *markname = g_strdup(marker->getRepr()->attribute("id"));
+ set_selected(markname);
+ g_free (markname);
+ }
+ else {
+ set_selected(nullptr);
+ }
+
+ updating = false;
+
+}
+/**
+ * Return a uri string representing the current selected marker used for setting the marker style in the document
+ */
+const gchar * MarkerComboBox::get_active_marker_uri()
+{
+ /* Get Marker */
+ const gchar *markid = get_active()->get_value(marker_columns.marker);
+ if (!markid)
+ {
+ return nullptr;
+ }
+
+ gchar const *marker = "";
+ if (strcmp(markid, "none")) {
+ bool stockid = get_active()->get_value(marker_columns.stock);
+
+ gchar *markurn;
+ if (stockid)
+ {
+ markurn = g_strconcat("urn:inkscape:marker:",markid,NULL);
+ }
+ else
+ {
+ markurn = g_strdup(markid);
+ }
+ SPObject *mark = get_stock_item(markurn, stockid);
+ g_free(markurn);
+ if (mark) {
+ Inkscape::XML::Node *repr = mark->getRepr();
+ marker = g_strconcat("url(#", repr->attribute("id"), ")", NULL);
+ }
+ } else {
+ marker = g_strdup(markid);
+ }
+
+ return marker;
+}
+
+void MarkerComboBox::set_active_history() {
+
+ const gchar *markid = get_active()->get_value(marker_columns.marker);
+
+ // If forked from a stockid, add the stockid
+ SPObject const *marker = doc->getObjectById(markid);
+ if (marker && marker->getRepr()->attribute("inkscape:stockid")) {
+ markid = marker->getRepr()->attribute("inkscape:stockid");
+ }
+
+ set_selected(markid);
+}
+
+
+void MarkerComboBox::set_selected(const gchar *name, gboolean retry/*=true*/) {
+
+ if (!name) {
+ set_active(0);
+ return;
+ }
+
+ for(Gtk::TreeIter iter = marker_store->children().begin();
+ iter != marker_store->children().end(); ++iter) {
+ Gtk::TreeModel::Row row = (*iter);
+ if (row[marker_columns.marker] &&
+ !strcmp(row[marker_columns.marker], name)) {
+ set_active(iter);
+ return;
+ }
+ }
+
+ // Didn't find it in the list, try refreshing from the doc
+ if (retry) {
+ sp_marker_list_from_doc(doc, true);
+ set_selected(name, false);
+ }
+}
+
+
+/**
+ * Pick up all markers from source, except those that are in
+ * current_doc (if non-NULL), and add items to the combo.
+ */
+void MarkerComboBox::sp_marker_list_from_doc(SPDocument *source, gboolean history)
+{
+ std::vector<SPMarker *> ml = get_marker_list(source);
+
+ remove_markers(history); // Seem to need to remove 2x
+ remove_markers(history);
+ add_markers(ml, source, history);
+}
+
+/**
+ * Returns a list of markers in the defs of the given source document as a vector
+ * Returns NULL if there are no markers in the document.
+ */
+std::vector<SPMarker *> MarkerComboBox::get_marker_list (SPDocument *source)
+{
+ std::vector<SPMarker *> ml;
+ if (source == nullptr)
+ return ml;
+
+ SPDefs *defs = source->getDefs();
+ if (!defs) {
+ return ml;
+ }
+
+ for (auto& child: defs->children)
+ {
+ if (SP_IS_MARKER(&child)) {
+ ml.push_back(SP_MARKER(&child));
+ }
+ }
+ return ml;
+}
+
+/**
+ * Remove history or non-history markers from the combo
+ */
+void MarkerComboBox::remove_markers (gboolean history)
+{
+ // Having the model set causes assertions when erasing rows, temporarily disconnect
+ unset_model();
+ for(Gtk::TreeIter iter = marker_store->children().begin();
+ iter != marker_store->children().end(); ++iter) {
+ Gtk::TreeModel::Row row = (*iter);
+ if (row[marker_columns.history] == history && row[marker_columns.separator] == false) {
+ marker_store->erase(iter);
+ iter = marker_store->children().begin();
+ }
+ }
+
+ set_model(marker_store);
+}
+
+/**
+ * Adds markers in marker_list to the combo
+ */
+void MarkerComboBox::add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history)
+{
+ // Do this here, outside of loop, to speed up preview generation:
+ Inkscape::Drawing drawing;
+ unsigned const visionkey = SPItem::display_key_new(1);
+ drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
+ // Find the separator,
+ Gtk::TreeIter sep_iter;
+ for(Gtk::TreeIter iter = marker_store->children().begin();
+ iter != marker_store->children().end(); ++iter) {
+ Gtk::TreeModel::Row row = (*iter);
+ if (row[marker_columns.separator]) {
+ sep_iter = iter;
+ }
+ }
+
+ if (history) {
+ // add "None"
+ Gtk::TreeModel::Row row = *(marker_store->prepend());
+ row[marker_columns.label] = C_("Marker", "None");
+ row[marker_columns.stock] = false;
+ row[marker_columns.marker] = g_strdup("None");
+ row[marker_columns.image] = NULL;
+ row[marker_columns.history] = true;
+ row[marker_columns.separator] = false;
+ }
+
+ for (auto i:marker_list) {
+
+ Inkscape::XML::Node *repr = i->getRepr();
+ gchar const *markid = repr->attribute("inkscape:stockid") ? repr->attribute("inkscape:stockid") : repr->attribute("id");
+
+ // generate preview
+ Gtk::Image *prv = create_marker_image (24, repr->attribute("id"), source, drawing, visionkey);
+ prv->show();
+
+ // Add history before separator, others after
+ Gtk::TreeModel::Row row;
+ if (history)
+ row = *(marker_store->insert(sep_iter));
+ else
+ row = *(marker_store->append());
+
+ row[marker_columns.label] = ink_ellipsize_text(markid, 20);
+ // Non "stock" markers can also have "inkscape:stockid" (when using extension ColorMarkers),
+ // So use !is_history instead to determine is it is "stock" (ie in the markers.svg file)
+ row[marker_columns.stock] = !history;
+ row[marker_columns.marker] = repr->attribute("id");
+ row[marker_columns.image] = prv;
+ row[marker_columns.history] = history;
+ row[marker_columns.separator] = false;
+
+ }
+
+ sandbox->getRoot()->invoke_hide(visionkey);
+}
+
+/*
+ * Remove from the cache and recreate a marker image
+ */
+void
+MarkerComboBox::update_marker_image(gchar const *mname)
+{
+ gchar *cache_name = g_strconcat(combo_id, mname, NULL);
+ Glib::ustring key = svg_preview_cache.cache_key(doc->getDocumentURI(), cache_name, 24);
+ g_free (cache_name);
+ svg_preview_cache.remove_preview_from_cache(key);
+
+ Inkscape::Drawing drawing;
+ unsigned const visionkey = SPItem::display_key_new(1);
+ drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
+ Gtk::Image *prv = create_marker_image(24, mname, doc, drawing, visionkey);
+ if (prv) {
+ prv->show();
+ }
+ sandbox->getRoot()->invoke_hide(visionkey);
+
+ for(const auto & iter : marker_store->children()) {
+ Gtk::TreeModel::Row row = iter;
+ if (row[marker_columns.marker] && row[marker_columns.history] &&
+ !strcmp(row[marker_columns.marker], mname)) {
+ row[marker_columns.image] = prv;
+ return;
+ }
+ }
+
+}
+/**
+ * Creates a copy of the marker named mname, determines its visible and renderable
+ * area in the bounding box, and then renders it. This allows us to fill in
+ * preview images of each marker in the marker combobox.
+ */
+Gtk::Image *
+MarkerComboBox::create_marker_image(unsigned psize, gchar const *mname,
+ SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/)
+{
+ // Retrieve the marker named 'mname' from the source SVG document
+ SPObject const *marker = source->getObjectById(mname);
+ if (marker == nullptr) {
+ return nullptr;
+ }
+
+ // Create a copy repr of the marker with id="sample"
+ Inkscape::XML::Document *xml_doc = sandbox->getReprDoc();
+ Inkscape::XML::Node *mrepr = marker->getRepr()->duplicate(xml_doc);
+ mrepr->setAttribute("id", "sample");
+
+ // Replace the old sample in the sandbox by the new one
+ Inkscape::XML::Node *defsrepr = sandbox->getObjectById("defs")->getRepr();
+ SPObject *oldmarker = sandbox->getObjectById("sample");
+ if (oldmarker) {
+ oldmarker->deleteObject(false);
+ }
+
+ // TODO - This causes a SIGTRAP on windows
+ defsrepr->appendChild(mrepr);
+
+ Inkscape::GC::release(mrepr);
+
+ // If the marker color is a url link to a pattern or gradient copy that too
+ SPObject *mk = source->getObjectById(mname);
+ SPCSSAttr *css_marker = sp_css_attr_from_object(mk->firstChild(), SP_STYLE_FLAG_ALWAYS);
+ //const char *mfill = sp_repr_css_property(css_marker, "fill", "none");
+ const char *mstroke = sp_repr_css_property(css_marker, "fill", "none");
+
+ if (!strncmp (mstroke, "url(", 4)) {
+ SPObject *linkObj = getMarkerObj(mstroke, source);
+ if (linkObj) {
+ Inkscape::XML::Node *grepr = linkObj->getRepr()->duplicate(xml_doc);
+ SPObject *oldmarker = sandbox->getObjectById(linkObj->getId());
+ if (oldmarker) {
+ oldmarker->deleteObject(false);
+ }
+ defsrepr->appendChild(grepr);
+ Inkscape::GC::release(grepr);
+
+ if (SP_IS_GRADIENT(linkObj)) {
+ SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (SP_GRADIENT(linkObj), false);
+ if (vector) {
+ Inkscape::XML::Node *grepr = vector->getRepr()->duplicate(xml_doc);
+ SPObject *oldmarker = sandbox->getObjectById(vector->getId());
+ if (oldmarker) {
+ oldmarker->deleteObject(false);
+ }
+ defsrepr->appendChild(grepr);
+ Inkscape::GC::release(grepr);
+ }
+ }
+ }
+ }
+
+// Uncomment this to get the sandbox documents saved (useful for debugging)
+ //FILE *fp = fopen (g_strconcat(combo_id, mname, ".svg", NULL), "w");
+ //sp_repr_save_stream(sandbox->getReprDoc(), fp);
+ //fclose (fp);
+
+ // object to render; note that the id is the same as that of the combo we're building
+ SPObject *object = sandbox->getObjectById(combo_id);
+ sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ sandbox->ensureUpToDate();
+
+ if (object == nullptr || !SP_IS_ITEM(object)) {
+ return nullptr; // sandbox broken?
+ }
+
+ SPItem *item = SP_ITEM(object);
+ // Find object's bbox in document
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if (!dbox) {
+ return nullptr;
+ }
+
+ /* Update to renderable state */
+ gchar *cache_name = g_strconcat(combo_id, mname, NULL);
+ Glib::ustring key = svg_preview_cache.cache_key(source->getDocumentURI(), cache_name, psize);
+ g_free (cache_name);
+ GdkPixbuf *pixbuf = svg_preview_cache.get_preview_from_cache(key); // no ref created
+
+ if (!pixbuf) {
+ pixbuf = render_pixbuf(drawing, 0.8, *dbox, psize);
+ svg_preview_cache.set_preview_in_cache(key, pixbuf);
+ g_object_unref(pixbuf); // reference is held by svg_preview_cache
+ }
+
+ // Create widget
+ Gtk::Image *pb = Glib::wrap(GTK_IMAGE(gtk_image_new_from_pixbuf(pixbuf)));
+ return pb;
+}
+
+void MarkerComboBox::prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ) {
+
+ Gtk::Image *image = (*row)[marker_columns.image];
+ if (image)
+ image_renderer.property_pixbuf() = image->get_pixbuf();
+ else
+ image_renderer.property_pixbuf() = empty_image->get_pixbuf();
+}
+
+gboolean MarkerComboBox::separator_cb (GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) {
+
+ gboolean sep = FALSE;
+ gtk_tree_model_get(model, iter, 4, &sep, -1);
+ return sep;
+}
+
+/**
+ * Returns a new document containing default start, mid, and end markers.
+ */
+SPDocument *MarkerComboBox::ink_markers_preview_doc ()
+{
+gchar const *buffer = R"A(
+ <svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ id="MarkerSample">
+
+ <defs id="defs"/>
+
+ <g id="marker-start">
+ <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-start:url(#sample)"
+ d="M 12.5,13 L 25,13"/>
+ <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/>
+ </g>
+
+ <g id="marker-mid">
+ <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-mid:url(#sample)"
+ d="M 0,113 L 12.5,113 L 25,113"/>
+ <rect x="0" y="100" width="25" height="25" style="fill:none;stroke:none"/>
+ </g>
+
+ <g id="marker-end">
+ <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-end:url(#sample)"
+ d="M 0,213 L 12.5,213"/>
+ <rect x="0" y="200" width="25" height="25" style="fill:none;stroke:none"/>
+ </g>
+
+ </svg>
+)A";
+
+ return SPDocument::createNewDocFromMem (buffer, strlen(buffer), 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/stroke-marker-selector.h b/src/widgets/stroke-marker-selector.h
new file mode 100644
index 0000000..c9bd2e8
--- /dev/null
+++ b/src/widgets/stroke-marker-selector.h
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_MARKER_SELECTOR_NEW_H
+#define SEEN_SP_MARKER_SELECTOR_NEW_H
+
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Maximilian Albert <maximilian.albert> (gtkmm-ification)
+ *
+ * Copyright (C) 2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <vector>
+
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+
+#include <sigc++/signal.h>
+
+#include "document.h"
+#include "inkscape.h"
+
+#include "display/drawing.h"
+
+class SPMarker;
+
+namespace Gtk {
+
+class Container;
+class Adjustment;
+}
+
+/**
+ * ComboBox derived class for selecting stroke markers.
+ */
+
+class MarkerComboBox : public Gtk::ComboBox {
+public:
+ MarkerComboBox(gchar const *id, int loc);
+ ~MarkerComboBox() override;
+
+ void setDocument(SPDocument *);
+
+ sigc::signal<void> changed_signal;
+
+ void set_current(SPObject *marker);
+ void set_active_history();
+ void set_selected(const gchar *name, gboolean retry=true);
+ const gchar *get_active_marker_uri();
+ bool update() { return updating; };
+ gchar const *get_id() { return combo_id; };
+ void update_marker_image(gchar const *mname);
+ int get_loc() { return loc; };
+
+private:
+
+
+ Glib::RefPtr<Gtk::ListStore> marker_store;
+ gchar const *combo_id;
+ int loc;
+ bool updating;
+ guint markerCount;
+ SPDocument *doc = nullptr;
+ SPDocument *sandbox;
+ Gtk::Image *empty_image;
+ Gtk::CellRendererPixbuf image_renderer;
+
+ class MarkerColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<const gchar *> marker; // ustring doesn't work here on windows due to unicode
+ Gtk::TreeModelColumn<gboolean> stock;
+ Gtk::TreeModelColumn<Gtk::Image *> image;
+ Gtk::TreeModelColumn<gboolean> history;
+ Gtk::TreeModelColumn<gboolean> separator;
+
+ MarkerColumns() {
+ add(label); add(stock); add(marker); add(history); add(separator); add(image);
+ }
+ };
+ MarkerColumns marker_columns;
+
+ void init_combo();
+ void set_history(Gtk::TreeModel::Row match_row);
+ void sp_marker_list_from_doc(SPDocument *source, gboolean history);
+ std::vector <SPMarker*> get_marker_list (SPDocument *source);
+ void add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history);
+ void remove_markers (gboolean history);
+ SPDocument *ink_markers_preview_doc ();
+ Gtk::Image * create_marker_image(unsigned psize, gchar const *mname,
+ SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/);
+
+ /*
+ * Callbacks for drawing the combo box
+ */
+ void prepareImageRenderer( Gtk::TreeModel::const_iterator const &row );
+ static gboolean separator_cb (GtkTreeModel *model, GtkTreeIter *iter, gpointer data);
+
+ static void handleDefsModified(MarkerComboBox *self);
+
+ void refreshHistory();
+
+ sigc::connection modified_connection;
+};
+
+#endif // SEEN_SP_MARKER_SELECTOR_NEW_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/stroke-style.cpp b/src/widgets/stroke-style.cpp
new file mode 100644
index 0000000..f2fe0e1
--- /dev/null
+++ b/src/widgets/stroke-style.cpp
@@ -0,0 +1,1346 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Bryce Harrington <brycehar@bryceharrington.org>
+ * bulia byak <buliabyak@users.sf.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Josh Andler <scislac@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2001-2005 authors
+ * Copyright (C) 2001 Ximian, Inc.
+ * Copyright (C) 2004 John Cliff
+ * Copyright (C) 2008 Maximilian Albert (gtkmm-ification)
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noSP_SS_VERBOSE
+
+#include "desktop-widget.h"
+#include "stroke-style.h"
+
+#include "object/sp-marker.h"
+#include "object/sp-namedview.h"
+#include "object/sp-rect.h"
+#include "object/sp-stop.h"
+#include "object/sp-text.h"
+
+#include "svg/svg-color.h"
+
+#include "ui/icon-loader.h"
+#include "ui/widget/dash-selector.h"
+#include "ui/widget/unit-menu.h"
+
+#include "widgets/style-utils.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+
+/**
+ * Creates a new widget for the line stroke paint.
+ */
+Gtk::Widget *sp_stroke_style_paint_widget_new()
+{
+ return Inkscape::Widgets::createStyleWidget( STROKE );
+}
+
+/**
+ * Creates a new widget for the line stroke style.
+ */
+Gtk::Widget *sp_stroke_style_line_widget_new()
+{
+ return Inkscape::Widgets::createStrokeStyleWidget();
+}
+
+void sp_stroke_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop)
+{
+ Inkscape::StrokeStyle *ss = dynamic_cast<Inkscape::StrokeStyle*>(widget);
+ if (ss) {
+ ss->setDesktop(desktop);
+ }
+}
+
+
+/**
+ * Extract the actual name of the link
+ * e.g. get mTriangle from url(#mTriangle).
+ * \return Buffer containing the actual name, allocated from GLib;
+ * the caller should free the buffer when they no longer need it.
+ */
+SPObject* getMarkerObj(gchar const *n, SPDocument *doc)
+{
+ gchar const *p = n;
+ while (*p != '\0' && *p != '#') {
+ p++;
+ }
+
+ if (*p == '\0' || p[1] == '\0') {
+ return nullptr;
+ }
+
+ p++;
+ int c = 0;
+ while (p[c] != '\0' && p[c] != ')') {
+ c++;
+ }
+
+ if (p[c] == '\0') {
+ return nullptr;
+ }
+
+ gchar* b = g_strdup(p);
+ b[c] = '\0';
+
+ // FIXME: get the document from the object and let the caller pass it in
+ SPObject *marker = doc->getObjectById(b);
+
+ g_free(b);
+ return marker;
+}
+
+namespace Inkscape {
+
+
+/**
+ * Construct a stroke-style radio button with a given icon
+ *
+ * \param[in] grp The Gtk::RadioButtonGroup to which to add the new button
+ * \param[in] icon The icon to use for the button
+ * \param[in] button_type The type of stroke-style radio button (join/cap)
+ * \param[in] stroke_style The style attribute to associate with the button
+ */
+StrokeStyle::StrokeStyleButton::StrokeStyleButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style)
+ :
+ Gtk::RadioButton(grp),
+ button_type(button_type),
+ stroke_style(stroke_style)
+{
+ show();
+ set_mode(false);
+
+ auto px = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR));
+ g_assert(px != nullptr);
+ px->show();
+ add(*px);
+}
+
+/**
+ * Create the fill or stroke style widget, and hook up all the signals.
+ */
+Gtk::Widget *Inkscape::Widgets::createStrokeStyleWidget( )
+{
+ StrokeStyle *strokeStyle = new StrokeStyle();
+
+ return strokeStyle;
+}
+
+StrokeStyle::StrokeStyle() :
+ Gtk::Box(),
+ miterLimitSpin(),
+ widthSpin(),
+ unitSelector(),
+ joinMiter(),
+ joinRound(),
+ joinBevel(),
+ capButt(),
+ capRound(),
+ capSquare(),
+ dashSelector(),
+ update(false),
+ desktop(nullptr),
+ selectChangedConn(),
+ selectModifiedConn(),
+ startMarkerConn(),
+ midMarkerConn(),
+ endMarkerConn(),
+ _old_unit(nullptr)
+{
+ table = new Gtk::Grid();
+ table->set_border_width(4);
+ table->set_row_spacing(4);
+ table->set_hexpand(false);
+ table->set_halign(Gtk::ALIGN_CENTER);
+ table->show();
+ add(*table);
+
+ Gtk::HBox *hb;
+ gint i = 0;
+
+ //spw_label(t, C_("Stroke width", "_Width:"), 0, i);
+
+ hb = spw_hbox(table, 3, 1, i);
+
+// TODO: when this is gtkmmified, use an Inkscape::UI::Widget::ScalarUnit instead of the separate
+// spinbutton and unit selector for stroke width. In sp_stroke_style_line_update, use
+// setHundredPercent to remember the averaged width corresponding to 100%. Then the
+// stroke_width_set_unit will be removed (because ScalarUnit takes care of conversions itself), and
+// with it, the two remaining calls of stroke_average_width, allowing us to get rid of that
+// function in desktop-style.
+ widthAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(1.0, 0.0, 1000.0, 0.1, 10.0, 0.0));
+ widthSpin = new Inkscape::UI::Widget::SpinButton(*widthAdj, 0.1, 3);
+ widthSpin->set_tooltip_text(_("Stroke width"));
+ widthSpin->show();
+ spw_label(table, C_("Stroke width", "_Width:"), 0, i, widthSpin);
+
+ sp_dialog_defocus_on_enter_cpp(widthSpin);
+
+ hb->pack_start(*widthSpin, false, false, 0);
+ unitSelector = new Inkscape::UI::Widget::UnitMenu();
+ unitSelector->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR);
+ Gtk::Widget *us = Gtk::manage(unitSelector);
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ unitSelector->addUnit(*unit_table.getUnit("%"));
+ _old_unit = unitSelector->getUnit();
+ if (desktop) {
+ unitSelector->setUnit(desktop->getNamedView()->display_units->abbr);
+ _old_unit = desktop->getNamedView()->display_units;
+ }
+ widthSpin->setUnitMenu(unitSelector);
+ unitChangedConn = unitSelector->signal_changed().connect(sigc::mem_fun(*this, &StrokeStyle::unitChangedCB));
+
+ us->show();
+
+ hb->pack_start(*us, FALSE, FALSE, 0);
+ (*widthAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::widthChangedCB));
+ i++;
+
+ /* Dash */
+ spw_label(table, _("Dashes:"), 0, i, nullptr); //no mnemonic for now
+ //decide what to do:
+ // implement a set_mnemonic_source function in the
+ // Inkscape::UI::Widget::DashSelector class, so that we do not have to
+ // expose any of the underlying widgets?
+ dashSelector = Gtk::manage(new Inkscape::UI::Widget::DashSelector);
+
+ dashSelector->show();
+ dashSelector->set_hexpand();
+ dashSelector->set_halign(Gtk::ALIGN_FILL);
+ dashSelector->set_valign(Gtk::ALIGN_CENTER);
+ table->attach(*dashSelector, 1, i, 3, 1);
+ dashSelector->changed_signal.connect(sigc::mem_fun(*this, &StrokeStyle::lineDashChangedCB));
+
+ i++;
+
+ /* Drop down marker selectors*/
+ // TRANSLATORS: Path markers are an SVG feature that allows you to attach arbitrary shapes
+ // (arrowheads, bullets, faces, whatever) to the start, end, or middle nodes of a path.
+
+ spw_label(table, _("Markers:"), 0, i, nullptr);
+
+ hb = spw_hbox(table, 1, 1, i);
+ i++;
+
+ startMarkerCombo = Gtk::manage(new MarkerComboBox("marker-start", SP_MARKER_LOC_START));
+ startMarkerCombo->set_tooltip_text(_("Start Markers are drawn on the first node of a path or shape"));
+ startMarkerConn = startMarkerCombo->signal_changed().connect(
+ sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>(
+ sigc::ptr_fun(&StrokeStyle::markerSelectCB), startMarkerCombo, this, SP_MARKER_LOC_START));
+ startMarkerCombo->show();
+
+ hb->pack_start(*startMarkerCombo, true, true, 0);
+
+ midMarkerCombo = Gtk::manage(new MarkerComboBox("marker-mid", SP_MARKER_LOC_MID));
+ midMarkerCombo->set_tooltip_text(_("Mid Markers are drawn on every node of a path or shape except the first and last nodes"));
+ midMarkerConn = midMarkerCombo->signal_changed().connect(
+ sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>(
+ sigc::ptr_fun(&StrokeStyle::markerSelectCB), midMarkerCombo, this, SP_MARKER_LOC_MID));
+ midMarkerCombo->show();
+
+ hb->pack_start(*midMarkerCombo, true, true, 0);
+
+ endMarkerCombo = Gtk::manage(new MarkerComboBox("marker-end", SP_MARKER_LOC_END));
+ endMarkerCombo->set_tooltip_text(_("End Markers are drawn on the last node of a path or shape"));
+ endMarkerConn = endMarkerCombo->signal_changed().connect(
+ sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>(
+ sigc::ptr_fun(&StrokeStyle::markerSelectCB), endMarkerCombo, this, SP_MARKER_LOC_END));
+ endMarkerCombo->show();
+
+ hb->pack_start(*endMarkerCombo, true, true, 0);
+
+ i++;
+
+ /* Join type */
+ // TRANSLATORS: The line join style specifies the shape to be used at the
+ // corners of paths. It can be "miter", "round" or "bevel".
+ spw_label(table, _("Join:"), 0, i, nullptr);
+
+ hb = spw_hbox(table, 3, 1, i);
+
+ Gtk::RadioButtonGroup joinGrp;
+
+ joinRound = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-round"),
+ hb, STROKE_STYLE_BUTTON_JOIN, "round");
+
+ // TRANSLATORS: Round join: joining lines with a rounded corner.
+ // For an example, draw a triangle with a large stroke width and modify the
+ // "Join" option (in the Fill and Stroke dialog).
+ joinRound->set_tooltip_text(_("Round join"));
+
+ joinBevel = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-bevel"),
+ hb, STROKE_STYLE_BUTTON_JOIN, "bevel");
+
+ // TRANSLATORS: Bevel join: joining lines with a blunted (flattened) corner.
+ // For an example, draw a triangle with a large stroke width and modify the
+ // "Join" option (in the Fill and Stroke dialog).
+ joinBevel->set_tooltip_text(_("Bevel join"));
+
+ joinMiter = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-miter"),
+ hb, STROKE_STYLE_BUTTON_JOIN, "miter");
+
+ // TRANSLATORS: Miter join: joining lines with a sharp (pointed) corner.
+ // For an example, draw a triangle with a large stroke width and modify the
+ // "Join" option (in the Fill and Stroke dialog).
+ joinMiter->set_tooltip_text(_("Miter join"));
+
+ /* Miterlimit */
+ // TRANSLATORS: Miter limit: only for "miter join", this limits the length
+ // of the sharp "spike" when the lines connect at too sharp an angle.
+ // When two line segments meet at a sharp angle, a miter join results in a
+ // spike that extends well beyond the connection point. The purpose of the
+ // miter limit is to cut off such spikes (i.e. convert them into bevels)
+ // when they become too long.
+ //spw_label(t, _("Miter _limit:"), 0, i);
+ miterLimitAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(4.0, 0.0, 100.0, 0.1, 10.0, 0.0));
+ miterLimitSpin = new Inkscape::UI::Widget::SpinButton(*miterLimitAdj, 0.1, 2);
+ miterLimitSpin->set_tooltip_text(_("Maximum length of the miter (in units of stroke width)"));
+ miterLimitSpin->show();
+ sp_dialog_defocus_on_enter_cpp(miterLimitSpin);
+
+ hb->pack_start(*miterLimitSpin, false, false, 0);
+ (*miterLimitAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::miterLimitChangedCB));
+ i++;
+
+ /* Cap type */
+ // TRANSLATORS: cap type specifies the shape for the ends of lines
+ //spw_label(t, _("_Cap:"), 0, i);
+ spw_label(table, _("Cap:"), 0, i, nullptr);
+
+ hb = spw_hbox(table, 3, 1, i);
+
+ Gtk::RadioButtonGroup capGrp;
+
+ capButt = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-butt"),
+ hb, STROKE_STYLE_BUTTON_CAP, "butt");
+
+ // TRANSLATORS: Butt cap: the line shape does not extend beyond the end point
+ // of the line; the ends of the line are square
+ capButt->set_tooltip_text(_("Butt cap"));
+
+ capRound = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-round"),
+ hb, STROKE_STYLE_BUTTON_CAP, "round");
+
+ // TRANSLATORS: Round cap: the line shape extends beyond the end point of the
+ // line; the ends of the line are rounded
+ capRound->set_tooltip_text(_("Round cap"));
+
+ capSquare = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-square"),
+ hb, STROKE_STYLE_BUTTON_CAP, "square");
+
+ // TRANSLATORS: Square cap: the line shape extends beyond the end point of the
+ // line; the ends of the line are square
+ capSquare->set_tooltip_text(_("Square cap"));
+
+ i++;
+
+ /* Paint order */
+ // TRANSLATORS: Paint order determines the order the 'fill', 'stroke', and 'markers are painted.
+ spw_label(table, _("Order:"), 0, i, nullptr);
+
+ hb = spw_hbox(table, 4, 1, i);
+
+ Gtk::RadioButtonGroup paintOrderGrp;
+
+ paintOrderFSM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fsm"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "normal");
+ paintOrderFSM->set_tooltip_text(_("Fill, Stroke, Markers"));
+
+ paintOrderSFM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-sfm"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "stroke fill markers");
+ paintOrderSFM->set_tooltip_text(_("Stroke, Fill, Markers"));
+
+ paintOrderFMS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fms"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "fill markers stroke");
+ paintOrderFMS->set_tooltip_text(_("Fill, Markers, Stroke"));
+
+ i++;
+
+ hb = spw_hbox(table, 4, 1, i);
+
+ paintOrderMFS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-mfs"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "markers fill stroke");
+ paintOrderMFS->set_tooltip_text(_("Markers, Fill, Stroke"));
+
+ paintOrderSMF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-smf"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "stroke markers fill");
+ paintOrderSMF->set_tooltip_text(_("Stroke, Markers, Fill"));
+
+ paintOrderMSF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-msf"),
+ hb, STROKE_STYLE_BUTTON_ORDER, "markers stroke fill");
+ paintOrderMSF->set_tooltip_text(_("Markers, Stroke, Fill"));
+
+ i++;
+}
+
+StrokeStyle::~StrokeStyle()
+{
+ selectModifiedConn.disconnect();
+ selectChangedConn.disconnect();
+}
+
+void StrokeStyle::setDesktop(SPDesktop *desktop)
+{
+ if (this->desktop != desktop) {
+
+ if (this->desktop) {
+ selectModifiedConn.disconnect();
+ selectChangedConn.disconnect();
+ _document_replaced_connection.disconnect();
+ }
+ this->desktop = desktop;
+
+ if (!desktop) {
+ return;
+ }
+
+ if (desktop->selection) {
+ selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &StrokeStyle::selectionChangedCB)));
+ selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &StrokeStyle::selectionModifiedCB)));
+ }
+
+ _document_replaced_connection =
+ desktop->connectDocumentReplaced(sigc::mem_fun(this, &StrokeStyle::_handleDocumentReplaced));
+
+ _handleDocumentReplaced(nullptr, desktop->getDocument());
+
+ updateLine();
+ }
+}
+
+void StrokeStyle::_handleDocumentReplaced(SPDesktop *, SPDocument *document)
+{
+ for (MarkerComboBox *combo : { startMarkerCombo, midMarkerCombo, endMarkerCombo }) {
+ combo->setDocument(document);
+ }
+}
+
+
+/**
+ * Helper function for creating stroke-style radio buttons.
+ *
+ * \param[in] grp The Gtk::RadioButtonGroup in which to add the button
+ * \param[in] icon The icon for the button
+ * \param[in] hb The Gtk::Box container in which to add the button
+ * \param[in] button_type The type (join/cap) for the button
+ * \param[in] stroke_style The style attribute to associate with the button
+ *
+ * \details After instantiating the button, it is added to a container box and
+ * a handler for the toggle event is connected.
+ */
+StrokeStyle::StrokeStyleButton *
+StrokeStyle::makeRadioButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ Gtk::HBox *hb,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style)
+{
+ g_assert(icon != nullptr);
+ g_assert(hb != nullptr);
+
+ StrokeStyleButton *tb = new StrokeStyleButton(grp, icon, button_type, stroke_style);
+
+ hb->pack_start(*tb, false, false, 0);
+ set_data(icon, tb);
+
+ tb->signal_toggled().connect(sigc::bind<StrokeStyleButton *, StrokeStyle *>(
+ sigc::ptr_fun(&StrokeStyle::buttonToggledCB), tb, this));
+
+ return tb;
+}
+
+bool StrokeStyle::shouldMarkersBeUpdated()
+{
+ return startMarkerCombo->update() || midMarkerCombo->update() ||
+ endMarkerCombo->update();
+}
+
+/**
+ * Handles when user selects one of the markers from the marker combobox.
+ * Gets the marker uri string and applies it to all selected
+ * items in the current desktop.
+ */
+void StrokeStyle::markerSelectCB(MarkerComboBox *marker_combo, StrokeStyle *spw, SPMarkerLoc const /*which*/)
+{
+ if (spw->update || spw->shouldMarkersBeUpdated()) {
+ return;
+ }
+
+ spw->update = true;
+
+ SPDocument *document = spw->desktop->getDocument();
+ if (!document) {
+ return;
+ }
+
+ /* Get Marker */
+ gchar const *marker = marker_combo->get_active_marker_uri();
+
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ gchar const *combo_id = marker_combo->get_id();
+ sp_repr_css_set_property(css, combo_id, marker);
+
+ // Also update the marker combobox, so the document's markers
+ // show up at the top of the combobox
+// sp_stroke_style_line_update( SP_WIDGET(spw), desktop ? desktop->getSelection() : NULL);
+ //spw->updateMarkerHist(which);
+
+ Inkscape::Selection *selection = spw->desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (!SP_IS_SHAPE(item) || SP_IS_RECT(item)) { // can't set marker to rect, until it's converted to using <path>
+ continue;
+ }
+ Inkscape::XML::Node *selrepr = item->getRepr();
+ if (selrepr) {
+ sp_repr_css_change_recursive(selrepr, css, "style");
+ SPObject *markerObj = getMarkerObj(marker, document);
+ spw->setMarkerColor(markerObj, marker_combo->get_loc(), item);
+ }
+
+ item->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+
+ DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, _("Set markers"));
+ }
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ spw->update = false;
+};
+
+void StrokeStyle::updateMarkerHist(SPMarkerLoc const which)
+{
+ switch (which) {
+ case SP_MARKER_LOC_START:
+ startMarkerConn.block();
+ startMarkerCombo->set_active_history();
+ startMarkerConn.unblock();
+ break;
+
+ case SP_MARKER_LOC_MID:
+ midMarkerConn.block();
+ midMarkerCombo->set_active_history();
+ midMarkerConn.unblock();
+ break;
+
+ case SP_MARKER_LOC_END:
+ endMarkerConn.block();
+ endMarkerCombo->set_active_history();
+ endMarkerConn.unblock();
+ break;
+ default:
+ g_assert_not_reached();
+ }
+}
+
+/**
+ * Callback for when UnitMenu widget is modified.
+ * Triggers update action.
+ */
+void StrokeStyle::unitChangedCB()
+{
+ Inkscape::Util::Unit const *new_unit = unitSelector->getUnit();
+ if (new_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) {
+ widthSpin->set_value(100);
+ }
+ widthSpin->set_value(Inkscape::Util::Quantity::convert(widthSpin->get_value(), _old_unit, new_unit));
+ _old_unit = new_unit;
+}
+
+/**
+ * Callback for when stroke style widget is modified.
+ * Triggers update action.
+ */
+void
+StrokeStyle::selectionModifiedCB(guint flags)
+{
+ // We care deeply about only updating when the style is updated
+ // if we update on other flags, we slow inkscape down when dragging
+ if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+ updateLine();
+ }
+}
+
+/**
+ * Callback for when stroke style widget is changed.
+ * Triggers update action.
+ */
+void
+StrokeStyle::selectionChangedCB()
+{
+ updateLine();
+}
+
+/*
+ * Fork marker if necessary and set the referencing items url to the new marker
+ * Return the new marker
+ */
+SPObject *
+StrokeStyle::forkMarker(SPObject *marker, int loc, SPItem *item)
+{
+ if (!item || !marker) {
+ return nullptr;
+ }
+
+ gchar const *marker_id = SPMarkerNames[loc].key;
+
+ /*
+ * Optimization when all the references to this marker are from this item
+ * then we can reuse it and don't need to fork
+ */
+ Glib::ustring urlId = Glib::ustring::format("url(#", marker->getRepr()->attribute("id"), ")");
+ unsigned int refs = 0;
+ for (int i = SP_MARKER_LOC_START; i < SP_MARKER_LOC_QTY; i++) {
+ if (item->style->marker_ptrs[i]->set &&
+ !strcmp(urlId.c_str(), item->style->marker_ptrs[i]->value())) {
+ refs++;
+ }
+ }
+ if (marker->hrefcount <= refs) {
+ return marker;
+ }
+
+ marker = sp_marker_fork_if_necessary(marker);
+
+ // Update the items url to new marker
+ Inkscape::XML::Node *mark_repr = marker->getRepr();
+ SPCSSAttr *css_item = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css_item, marker_id, g_strconcat("url(#", mark_repr->attribute("id"), ")", NULL));
+ sp_repr_css_change_recursive(item->getRepr(), css_item, "style");
+
+ sp_repr_css_attr_unref(css_item);
+ css_item = nullptr;
+
+ return marker;
+}
+
+/**
+ * Change the color of the marker to match the color of the item.
+ * Marker stroke color is set to item stroke color.
+ * Fill color :
+ * 1. If the item has fill, use that for the marker fill,
+ * 2. If the marker has same fill and stroke assume its solid, use item stroke for both fill and stroke the line stroke
+ * 3. If the marker has fill color, use the marker fill color
+ *
+ */
+void
+StrokeStyle::setMarkerColor(SPObject *marker, int loc, SPItem *item)
+{
+
+ if (!item || !marker) {
+ return;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gboolean colorStock = prefs->getBool("/options/markers/colorStockMarkers", true);
+ gboolean colorCustom = prefs->getBool("/options/markers/colorCustomMarkers", false);
+ const gchar *stock = marker->getRepr()->attribute("inkscape:isstock");
+ gboolean isStock = (stock && !strcmp(stock,"true"));
+
+ if (isStock ? !colorStock : !colorCustom) {
+ return;
+ }
+
+ // Check if we need to fork this marker
+ marker = forkMarker(marker, loc, item);
+
+ Inkscape::XML::Node *repr = marker->getRepr()->firstChild();
+ if (!repr) {
+ return;
+ };
+
+ // Current line style
+ SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS);
+ const char *lstroke = getItemColorForMarker(item, FOR_STROKE, loc);
+ const char *lstroke_opacity = sp_repr_css_property(css_item, "stroke-opacity", "1");
+ const char *lfill = getItemColorForMarker(item, FOR_FILL, loc);
+ const char *lfill_opacity = sp_repr_css_property(css_item, "fill-opacity", "1");
+
+ // Current marker style
+ SPCSSAttr *css_marker = sp_css_attr_from_object(marker->firstChild(), SP_STYLE_FLAG_ALWAYS);
+ const char *mfill = sp_repr_css_property(css_marker, "fill", "none");
+ const char *mstroke = sp_repr_css_property(css_marker, "fill", "none");
+
+ // Create new marker style with the lines stroke
+ SPCSSAttr *css = sp_repr_css_attr_new();
+
+ sp_repr_css_set_property(css, "stroke", lstroke);
+ sp_repr_css_set_property(css, "stroke-opacity", lstroke_opacity);
+
+ if (strcmp(lfill, "none") ) {
+ // 1. If the line has fill, use that for the marker fill
+ sp_repr_css_set_property(css, "fill", lfill);
+ sp_repr_css_set_property(css, "fill-opacity", lfill_opacity);
+ }
+ else if (mfill && mstroke && !strcmp(mfill, mstroke) && mfill[0] == '#' && strcmp(mfill, "#ffffff")) {
+ // 2. If the marker has same fill and stroke assume its solid. use line stroke for both fill and stroke the line stroke
+ sp_repr_css_set_property(css, "fill", lstroke);
+ sp_repr_css_set_property(css, "fill-opacity", lstroke_opacity);
+ }
+ else if (mfill && mfill[0] == '#' && strcmp(mfill, "#000000")) {
+ // 3. If the marker has fill color, use the marker fill color
+ sp_repr_css_set_property(css, "fill", mfill);
+ //sp_repr_css_set_property(css, "fill-opacity", mfill_opacity);
+ }
+
+ sp_repr_css_change_recursive(marker->firstChild()->getRepr(), css, "style");
+
+ // Tell the combos to update its image cache of this marker
+ gchar const *mid = marker->getRepr()->attribute("id");
+ startMarkerCombo->update_marker_image(mid);
+ midMarkerCombo->update_marker_image(mid);
+ endMarkerCombo->update_marker_image(mid);
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+
+}
+
+/*
+ * Get the fill or stroke color of the item
+ * If its a gradient, then return first or last stop color
+ */
+const char *
+StrokeStyle::getItemColorForMarker(SPItem *item, Inkscape::PaintTarget fill_or_stroke, int loc)
+{
+ SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS);
+ const char *color;
+ if (fill_or_stroke == FOR_FILL)
+ color = sp_repr_css_property(css_item, "fill", "none");
+ else
+ color = sp_repr_css_property(css_item, "stroke", "none");
+
+ if (!strncmp (color, "url(", 4)) {
+ // If the item has a gradient use the first stop color for the marker
+
+ SPGradient *grad = getGradient(item, fill_or_stroke);
+ if (grad) {
+ SPGradient *vector = grad->getVector(FALSE);
+ SPStop *stop = vector->getFirstStop();
+ if (loc == SP_MARKER_LOC_END) {
+ stop = sp_last_stop(vector);
+ }
+ if (stop) {
+ guint32 const c1 = stop->get_rgba32();
+ gchar c[64];
+ sp_svg_write_color(c, sizeof(c), c1);
+ color = g_strdup(c);
+ //lstroke_opacity = Glib::ustring::format(stop->opacity).c_str();
+ }
+ }
+ }
+ return color;
+}
+/**
+ * Sets selector widgets' dash style from an SPStyle object.
+ */
+void
+StrokeStyle::setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style)
+{
+ if (!style->stroke_dasharray.values.empty()) {
+ double d[64];
+ size_t len = MIN(style->stroke_dasharray.values.size(), 64);
+ /* Set dash */
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gboolean scale = prefs->getBool("/options/dash/scale", true);
+ double scaledash = 1.0;
+ if (scale) {
+ scaledash = style->stroke_width.computed;
+ }
+ for (unsigned i = 0; i < len; i++) {
+ if (style->stroke_width.computed != 0)
+ d[i] = style->stroke_dasharray.values[i].value / scaledash;
+ else
+ d[i] = style->stroke_dasharray.values[i].value; // is there a better thing to do for stroke_width==0?
+ }
+ dsel->set_dash(len, d,
+ style->stroke_width.computed != 0 ? style->stroke_dashoffset.value / scaledash
+ : style->stroke_dashoffset.value);
+ } else {
+ dsel->set_dash(0, nullptr, 0.0);
+ }
+}
+
+/**
+ * Sets the join type for a line, and updates the stroke style widget's buttons
+ */
+void
+StrokeStyle::setJoinType (unsigned const jointype)
+{
+ Gtk::RadioButton *tb = nullptr;
+ switch (jointype) {
+ case SP_STROKE_LINEJOIN_MITER:
+ tb = joinMiter;
+ break;
+ case SP_STROKE_LINEJOIN_ROUND:
+ tb = joinRound;
+ break;
+ case SP_STROKE_LINEJOIN_BEVEL:
+ tb = joinBevel;
+ break;
+ default:
+ // Should not happen
+ std::cerr << "StrokeStyle::setJoinType(): Invalid value: " << jointype << std::endl;
+ tb = joinMiter;
+ break;
+ }
+ setJoinButtons(tb);
+}
+
+/**
+ * Sets the cap type for a line, and updates the stroke style widget's buttons
+ */
+void
+StrokeStyle::setCapType (unsigned const captype)
+{
+ Gtk::RadioButton *tb = nullptr;
+ switch (captype) {
+ case SP_STROKE_LINECAP_BUTT:
+ tb = capButt;
+ break;
+ case SP_STROKE_LINECAP_ROUND:
+ tb = capRound;
+ break;
+ case SP_STROKE_LINECAP_SQUARE:
+ tb = capSquare;
+ break;
+ default:
+ // Should not happen
+ std::cerr << "StrokeStyle::setCapType(): Invalid value: " << captype << std::endl;
+ tb = capButt;
+ break;
+ }
+ setCapButtons(tb);
+}
+
+/**
+ * Sets the cap type for a line, and updates the stroke style widget's buttons
+ */
+void
+StrokeStyle::setPaintOrder (gchar const *paint_order)
+{
+ Gtk::RadioButton *tb = paintOrderFSM;
+
+ SPIPaintOrder temp;
+ temp.read( paint_order );
+
+ if (temp.layer[0] != SP_CSS_PAINT_ORDER_NORMAL) {
+
+ if (temp.layer[0] == SP_CSS_PAINT_ORDER_FILL) {
+ if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) {
+ tb = paintOrderFSM;
+ } else {
+ tb = paintOrderFMS;
+ }
+ } else if (temp.layer[0] == SP_CSS_PAINT_ORDER_STROKE) {
+ if (temp.layer[1] == SP_CSS_PAINT_ORDER_FILL) {
+ tb = paintOrderSFM;
+ } else {
+ tb = paintOrderSMF;
+ }
+ } else {
+ if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) {
+ tb = paintOrderMSF;
+ } else {
+ tb = paintOrderMFS;
+ }
+ }
+
+ }
+ setPaintOrderButtons(tb);
+}
+
+/**
+ * Callback for when stroke style widget is updated, including markers, cap type,
+ * join type, etc.
+ */
+void
+StrokeStyle::updateLine()
+{
+ if (update) {
+ return;
+ }
+
+ update = true;
+
+ Inkscape::Selection *sel = desktop ? desktop->getSelection() : nullptr;
+
+ FillOrStroke kind = GPOINTER_TO_INT(get_data("kind")) ? FILL : STROKE;
+
+ // create temporary style
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ // query into it
+ int result_sw = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH);
+ int result_ml = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEMITERLIMIT);
+ int result_cap = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKECAP);
+ int result_join = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEJOIN);
+
+ int result_order = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_PAINTORDER);
+
+ SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL);
+
+ if (!sel || sel->isEmpty()) {
+ // Nothing selected, grey-out all controls in the stroke-style dialog
+ table->set_sensitive(false);
+
+ update = false;
+
+ return;
+ } else {
+ table->set_sensitive(true);
+
+ if (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED) {
+ unitSelector->setUnit("%");
+ } else {
+ // same width, or only one object; no sense to keep percent, switch to absolute
+ Inkscape::Util::Unit const *tempunit = unitSelector->getUnit();
+ if (tempunit->type != Inkscape::Util::UNIT_TYPE_LINEAR) {
+ unitSelector->setUnit(SP_ACTIVE_DESKTOP->getNamedView()->display_units->abbr);
+ }
+ }
+
+ Inkscape::Util::Unit const *unit = unitSelector->getUnit();
+
+ if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
+ double avgwidth = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", unit);
+ (*widthAdj)->set_value(avgwidth);
+ } else {
+ (*widthAdj)->set_value(100);
+ }
+
+ // if none of the selected objects has a stroke, than quite some controls should be disabled
+ // The markers might still be shown though, so these will not be disabled
+ bool enabled = (result_sw != QUERY_STYLE_NOTHING) && !targPaint.isNoneSet();
+ /* No objects stroked, set insensitive */
+ joinMiter->set_sensitive(enabled);
+ joinRound->set_sensitive(enabled);
+ joinBevel->set_sensitive(enabled);
+
+ miterLimitSpin->set_sensitive(enabled);
+
+ capButt->set_sensitive(enabled);
+ capRound->set_sensitive(enabled);
+ capSquare->set_sensitive(enabled);
+
+ dashSelector->set_sensitive(enabled);
+ }
+
+ if (result_ml != QUERY_STYLE_NOTHING)
+ (*miterLimitAdj)->set_value(query.stroke_miterlimit.value); // TODO: reflect averagedness?
+
+ using Inkscape::is_query_style_updateable;
+ if (! is_query_style_updateable(result_join)) {
+ setJoinType(query.stroke_linejoin.value);
+ } else {
+ setJoinButtons(nullptr);
+ }
+
+ if (! is_query_style_updateable(result_cap)) {
+ setCapType (query.stroke_linecap.value);
+ } else {
+ setCapButtons(nullptr);
+ }
+
+ if (! is_query_style_updateable(result_order)) {
+ setPaintOrder (query.paint_order.value);
+ } else {
+ setPaintOrder (nullptr);
+ }
+
+ std::vector<SPItem*> const objects(sel->items().begin(), sel->items().end());
+ if (objects.size()) {
+ SPObject *const object = objects[0];
+ SPStyle *const style = object->style;
+ /* Markers */
+ updateAllMarkers(objects, true); // FIXME: make this desktop query too
+
+ /* Dash */
+ setDashSelectorFromStyle(dashSelector, style); // FIXME: make this desktop query too
+ }
+ table->set_sensitive(true);
+
+ update = false;
+}
+
+/**
+ * Sets a line's dash properties in a CSS style object.
+ */
+void
+StrokeStyle::setScaledDash(SPCSSAttr *css,
+ int ndash, double *dash, double offset,
+ double scale)
+{
+ if (ndash > 0) {
+ Inkscape::CSSOStringStream osarray;
+ for (int i = 0; i < ndash; i++) {
+ osarray << dash[i] * scale;
+ if (i < (ndash - 1)) {
+ osarray << ",";
+ }
+ }
+ sp_repr_css_set_property(css, "stroke-dasharray", osarray.str().c_str());
+
+ Inkscape::CSSOStringStream osoffset;
+ osoffset << offset * scale;
+ sp_repr_css_set_property(css, "stroke-dashoffset", osoffset.str().c_str());
+ } else {
+ sp_repr_css_set_property(css, "stroke-dasharray", "none");
+ sp_repr_css_set_property(css, "stroke-dashoffset", nullptr);
+ }
+}
+
+static inline double calcScaleLineWidth(const double width_typed, SPItem *const item, Inkscape::Util::Unit const *const unit)
+{
+ if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
+ return Inkscape::Util::Quantity::convert(width_typed, unit, "px");
+ } else { // percentage
+ const gdouble old_w = item->style->stroke_width.computed;
+ return old_w * width_typed / 100;
+ }
+}
+
+/**
+ * Sets line properties like width, dashes, markers, etc. on all currently selected items.
+ */
+void
+StrokeStyle::scaleLine()
+{
+ if (!desktop) {
+ return;
+ }
+
+ if (update) {
+ return;
+ }
+
+ update = true;
+
+ SPDocument *document = desktop->getDocument();
+ Inkscape::Selection *selection = desktop->getSelection();
+ auto items= selection->items();
+
+ /* TODO: Create some standardized method */
+ SPCSSAttr *css = sp_repr_css_attr_new();
+
+ if (!items.empty()) {
+ double width_typed = (*widthAdj)->get_value();
+ double const miterlimit = (*miterLimitAdj)->get_value();
+
+ Inkscape::Util::Unit const *const unit = unitSelector->getUnit();
+
+ double *dash, offset;
+ int ndash;
+ dashSelector->get_dash(&ndash, &dash, &offset);
+
+ for(auto i=items.begin();i!=items.end();++i){
+ /* Set stroke width */
+ const double width = calcScaleLineWidth(width_typed, (*i), unit);
+
+ {
+ Inkscape::CSSOStringStream os_width;
+ os_width << width;
+ sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str());
+ }
+
+ {
+ Inkscape::CSSOStringStream os_ml;
+ os_ml << miterlimit;
+ sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str());
+ }
+
+ /* Set dash */
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gboolean scale = prefs->getBool("/options/dash/scale", true);
+ if (scale) {
+ setScaledDash(css, ndash, dash, offset, width);
+ }
+ else {
+ setScaledDash(css, ndash, dash, offset, document->getDocumentScale()[0]);
+ }
+ sp_desktop_apply_css_recursive ((*i), css, true);
+ }
+
+ g_free(dash);
+
+ if (unit->type != Inkscape::Util::UNIT_TYPE_LINEAR) {
+ // reset to 100 percent
+ (*widthAdj)->set_value(100.0);
+ }
+
+ }
+
+ // we have already changed the items, so set style without changing selection
+ // FIXME: move the above stroke-setting stuff, including percentages, to desktop-style
+ sp_desktop_set_style (desktop, css, false);
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE,
+ _("Set stroke style"));
+
+ update = false;
+}
+
+/**
+ * Callback for when the stroke style's width changes.
+ * Causes all line styles to be applied to all selected items.
+ */
+void
+StrokeStyle::widthChangedCB()
+{
+ if (update) {
+ return;
+ }
+
+ scaleLine();
+}
+
+/**
+ * Callback for when the stroke style's miterlimit changes.
+ * Causes all line styles to be applied to all selected items.
+ */
+void
+StrokeStyle::miterLimitChangedCB()
+{
+ if (update) {
+ return;
+ }
+
+ scaleLine();
+}
+
+/**
+ * Callback for when the stroke style's dash changes.
+ * Causes all line styles to be applied to all selected items.
+ */
+
+void
+StrokeStyle::lineDashChangedCB()
+{
+ if (update) {
+ return;
+ }
+
+ scaleLine();
+}
+
+/**
+ * This routine handles toggle events for buttons in the stroke style dialog.
+ *
+ * When activated, this routine gets the data for the various widgets, and then
+ * calls the respective routines to update css properties, etc.
+ *
+ */
+void StrokeStyle::buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw)
+{
+ if (spw->update) {
+ return;
+ }
+
+ if (tb->get_active()) {
+ if (tb->get_button_type() == STROKE_STYLE_BUTTON_JOIN) {
+ spw->miterLimitSpin->set_sensitive(!strcmp(tb->get_stroke_style(), "miter"));
+ }
+
+ /* TODO: Create some standardized method */
+ SPCSSAttr *css = sp_repr_css_attr_new();
+
+ switch (tb->get_button_type()) {
+ case STROKE_STYLE_BUTTON_JOIN:
+ sp_repr_css_set_property(css, "stroke-linejoin", tb->get_stroke_style());
+ sp_desktop_set_style (spw->desktop, css);
+ spw->setJoinButtons(tb);
+ break;
+ case STROKE_STYLE_BUTTON_CAP:
+ sp_repr_css_set_property(css, "stroke-linecap", tb->get_stroke_style());
+ sp_desktop_set_style (spw->desktop, css);
+ spw->setCapButtons(tb);
+ break;
+ case STROKE_STYLE_BUTTON_ORDER:
+ sp_repr_css_set_property(css, "paint-order", tb->get_stroke_style());
+ sp_desktop_set_style (spw->desktop, css);
+ //spw->setPaintButtons(tb);
+ }
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(spw->desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, _("Set stroke style"));
+ }
+}
+
+/**
+ * Updates the join style toggle buttons
+ */
+void
+StrokeStyle::setJoinButtons(Gtk::ToggleButton *active)
+{
+ joinMiter->set_active(active == joinMiter);
+ miterLimitSpin->set_sensitive(active == joinMiter);
+ joinRound->set_active(active == joinRound);
+ joinBevel->set_active(active == joinBevel);
+}
+
+/**
+ * Updates the cap style toggle buttons
+ */
+void
+StrokeStyle::setCapButtons(Gtk::ToggleButton *active)
+{
+ capButt->set_active(active == capButt);
+ capRound->set_active(active == capRound);
+ capSquare->set_active(active == capSquare);
+}
+
+
+/**
+ * Updates the paint order style toggle buttons
+ */
+void
+StrokeStyle::setPaintOrderButtons(Gtk::ToggleButton *active)
+{
+ paintOrderFSM->set_active(active == paintOrderFSM);
+ paintOrderSFM->set_active(active == paintOrderSFM);
+ paintOrderFMS->set_active(active == paintOrderFMS);
+ paintOrderMFS->set_active(active == paintOrderMFS);
+ paintOrderSMF->set_active(active == paintOrderSMF);
+ paintOrderMSF->set_active(active == paintOrderMSF);
+}
+
+
+/**
+ * Recursively builds a simple list from an arbitrarily complex selection
+ * of items and grouped items
+ */
+static void buildGroupedItemList(SPObject *element, std::vector<SPObject*> &simple_list)
+{
+ if (SP_IS_GROUP(element)) {
+ for (SPObject *i = element->firstChild(); i; i = i->getNext()) {
+ buildGroupedItemList(i, simple_list);
+ }
+ } else {
+ simple_list.push_back(element);
+ }
+}
+
+
+/**
+ * Updates the marker combobox to highlight the appropriate marker and scroll to
+ * that marker.
+ */
+void
+StrokeStyle::updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo)
+{
+ struct { MarkerComboBox *key; int loc; } const keyloc[] = {
+ { startMarkerCombo, SP_MARKER_LOC_START },
+ { midMarkerCombo, SP_MARKER_LOC_MID },
+ { endMarkerCombo, SP_MARKER_LOC_END }
+ };
+
+ bool all_texts = true;
+
+ auto simplified_list = std::vector<SPObject *>();
+ for (SPItem *item : objects) {
+ buildGroupedItemList(item, simplified_list);
+ }
+
+ for (SPObject *object : simplified_list) {
+ if (!SP_IS_TEXT(object)) {
+ all_texts = false;
+ break;
+ }
+ }
+
+ // We show markers of the last object in the list only
+ // FIXME: use the first in the list that has the marker of each type, if any
+
+ // -1 means prefs haven't been queried yet
+ int update = -1;
+
+ for (auto const &markertype : keyloc) {
+ // For all three marker types,
+
+ // find the corresponding combobox item
+ MarkerComboBox *combo = markertype.key;
+
+ // Quit if we're in update state
+ if (combo->update()) {
+ return;
+ }
+
+ // Per SVG spec, text objects cannot have markers; disable combobox if only texts are selected
+ combo->set_sensitive(!all_texts);
+
+ SPObject *marker = nullptr;
+
+ if (!all_texts) {
+ for (SPObject *object : simplified_list) {
+ char const *value = object->style->marker_ptrs[markertype.loc]->value();
+
+ // If the object has this type of markers,
+ if (value == nullptr)
+ continue;
+
+ // Extract the name of the marker that the object uses
+ marker = getMarkerObj(value, object->document);
+
+ // Set the marker color
+ if (update < 0) {
+ // query prefs (only once)
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ update = prefs->getBool("/options/markers/colorUpdateMarkers", true) ? 1 : 0;
+ }
+
+ if (update > 0) {
+ setMarkerColor(marker, markertype.loc, SP_ITEM(object));
+
+ if (!skip_undo) {
+ SPDocument *document = desktop->getDocument();
+ DocumentUndo::maybeDone(document, "UaM", SP_VERB_DIALOG_FILL_STROKE,
+ _("Set marker color"));
+ }
+ }
+ }
+ }
+
+ // Scroll the combobox to that marker
+ combo->set_current(marker);
+ }
+
+}
+
+
+
+} // namespace Inkscape
+
+
+/*
+ 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/stroke-style.h b/src/widgets/stroke-style.h
new file mode 100644
index 0000000..d11c1ae
--- /dev/null
+++ b/src/widgets/stroke-style.h
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Widgets used in the stroke style dialog.
+ */
+/* Author:
+ * Lauris Kaplinski <lauris@ximian.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+// WHOA! talk about header bloat!
+
+#ifndef SEEN_DIALOGS_STROKE_STYLE_H
+#define SEEN_DIALOGS_STROKE_STYLE_H
+
+#include <glibmm/i18n.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/radiobutton.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "fill-n-stroke-factory.h"
+#include "fill-style.h" // to get sp_fill_style_widget_set_desktop
+#include "gradient-chemistry.h"
+
+#include "inkscape.h"
+#include "io/sys.h"
+#include "path-prefix.h"
+#include "preferences.h"
+#include "selection.h"
+#include "verbs.h"
+
+#include "display/canvas-bpath.h" // for SP_STROKE_LINEJOIN_*
+#include "display/drawing.h"
+
+#include "helper/stock-items.h"
+
+#include "style.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-names.h"
+#include "ui/widget/spinbutton.h"
+
+#include "widgets/paint-selector.h"
+#include "widgets/spw-utilities.h"
+#include "widgets/stroke-marker-selector.h"
+
+#include "xml/repr.h"
+
+namespace Gtk {
+class Widget;
+class Container;
+}
+
+namespace Inkscape {
+ namespace Util {
+ class Unit;
+ }
+ namespace UI {
+ namespace Widget {
+ class DashSelector;
+ class UnitMenu;
+ }
+ }
+}
+
+struct { gchar const *key; gint value; } const SPMarkerNames[] = {
+ {"marker-all", SP_MARKER_LOC},
+ {"marker-start", SP_MARKER_LOC_START},
+ {"marker-mid", SP_MARKER_LOC_MID},
+ {"marker-end", SP_MARKER_LOC_END},
+ {"", SP_MARKER_LOC_QTY},
+ {nullptr, -1}
+};
+
+/**
+ * Creates an instance of a paint style widget.
+ */
+Gtk::Widget *sp_stroke_style_paint_widget_new();
+
+/**
+ * Creates an instance of a line style widget.
+ */
+Gtk::Widget *sp_stroke_style_line_widget_new();
+
+/**
+ * Switches a line or paint style widget to track the given desktop.
+ */
+void sp_stroke_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop);
+
+SPObject *getMarkerObj(gchar const *n, SPDocument *doc);
+
+namespace Inkscape {
+class StrokeStyleButton;
+
+class StrokeStyle : public Gtk::Box
+{
+public:
+ StrokeStyle();
+ ~StrokeStyle() override;
+ void setDesktop(SPDesktop *desktop);
+
+private:
+ /** List of valid types for the stroke-style radio-button widget */
+ enum StrokeStyleButtonType {
+ STROKE_STYLE_BUTTON_JOIN, ///< A button to set the line-join style
+ STROKE_STYLE_BUTTON_CAP, ///< A button to set the line-cap style
+ STROKE_STYLE_BUTTON_ORDER ///< A button to set the paint-order style
+ };
+
+ /**
+ * A custom radio-button for setting the stroke style. It can be configured
+ * to set either the join or cap style by setting the button_type field.
+ */
+ class StrokeStyleButton : public Gtk::RadioButton {
+ public:
+ StrokeStyleButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style);
+
+ /** Get the type (line/cap) of the stroke-style button */
+ inline StrokeStyleButtonType get_button_type() {return button_type;}
+
+ /** Get the stroke style attribute associated with the button */
+ inline gchar const * get_stroke_style() {return stroke_style;}
+
+ private:
+ StrokeStyleButtonType button_type; ///< The type (line/cap) of the button
+ gchar const *stroke_style; ///< The stroke style associated with the button
+ };
+
+ void updateLine();
+ void updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo = false);
+ void updateMarkerHist(SPMarkerLoc const which);
+ void setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style);
+ void setJoinType (unsigned const jointype);
+ void setCapType (unsigned const captype);
+ void setPaintOrder (gchar const *paint_order);
+ void setJoinButtons(Gtk::ToggleButton *active);
+ void setCapButtons(Gtk::ToggleButton *active);
+ void setPaintOrderButtons(Gtk::ToggleButton *active);
+ void scaleLine();
+ void setScaledDash(SPCSSAttr *css, int ndash, double *dash, double offset, double scale);
+ void setMarkerColor(SPObject *marker, int loc, SPItem *item);
+ SPObject *forkMarker(SPObject *marker, int loc, SPItem *item);
+ const char *getItemColorForMarker(SPItem *item, Inkscape::PaintTarget fill_or_stroke, int loc);
+
+ StrokeStyleButton * makeRadioButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ Gtk::HBox *hb,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style);
+
+ // Callback functions
+ void selectionModifiedCB(guint flags);
+ void selectionChangedCB();
+ void widthChangedCB();
+ void miterLimitChangedCB();
+ void lineDashChangedCB();
+ void unitChangedCB();
+ bool shouldMarkersBeUpdated();
+ static void markerSelectCB(MarkerComboBox *marker_combo, StrokeStyle *spw, SPMarkerLoc const which);
+ static void buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw);
+
+
+ MarkerComboBox *startMarkerCombo;
+ MarkerComboBox *midMarkerCombo;
+ MarkerComboBox *endMarkerCombo;
+ Gtk::Grid *table;
+ Glib::RefPtr<Gtk::Adjustment> *widthAdj;
+ Glib::RefPtr<Gtk::Adjustment> *miterLimitAdj;
+ Inkscape::UI::Widget::SpinButton *miterLimitSpin;
+ Inkscape::UI::Widget::SpinButton *widthSpin;
+ Inkscape::UI::Widget::UnitMenu *unitSelector;
+ StrokeStyleButton *joinMiter;
+ StrokeStyleButton *joinRound;
+ StrokeStyleButton *joinBevel;
+ StrokeStyleButton *capButt;
+ StrokeStyleButton *capRound;
+ StrokeStyleButton *capSquare;
+ StrokeStyleButton *paintOrderFSM;
+ StrokeStyleButton *paintOrderSFM;
+ StrokeStyleButton *paintOrderFMS;
+ StrokeStyleButton *paintOrderMFS;
+ StrokeStyleButton *paintOrderSMF;
+ StrokeStyleButton *paintOrderMSF;
+ Inkscape::UI::Widget::DashSelector *dashSelector;
+
+ gboolean update;
+ SPDesktop *desktop;
+ sigc::connection selectChangedConn;
+ sigc::connection selectModifiedConn;
+ sigc::connection startMarkerConn;
+ sigc::connection midMarkerConn;
+ sigc::connection endMarkerConn;
+ sigc::connection unitChangedConn;
+
+ Inkscape::Util::Unit const *_old_unit;
+
+ void _handleDocumentReplaced(SPDesktop *, SPDocument *);
+ sigc::connection _document_replaced_connection;
+};
+
+} // namespace Inkscape
+
+#endif // SEEN_DIALOGS_STROKE_STYLE_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/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/swatch-selector.cpp b/src/widgets/swatch-selector.cpp
new file mode 100644
index 0000000..aeead1c
--- /dev/null
+++ b/src/widgets/swatch-selector.cpp
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "swatch-selector.h"
+
+#include <glibmm/i18n.h>
+
+#include "document-undo.h"
+#include "document.h"
+#include "gradient-chemistry.h"
+#include "gradient-selector.h"
+#include "verbs.h"
+
+#include "object/sp-stop.h"
+
+#include "svg/css-ostringstream.h"
+#include "svg/svg-color.h"
+
+#include "ui/widget/color-notebook.h"
+
+#include "xml/node.h"
+
+namespace Inkscape
+{
+namespace Widgets
+{
+
+SwatchSelector::SwatchSelector() :
+ Gtk::VBox(),
+ _gsel(nullptr),
+ _updating_color(false)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ GtkWidget *gsel = sp_gradient_selector_new();
+ _gsel = SP_GRADIENT_SELECTOR(gsel);
+ g_object_set_data( G_OBJECT(gobj()), "base", this );
+ _gsel->setMode(SPGradientSelector::MODE_SWATCH);
+
+ gtk_widget_show(gsel);
+
+ pack_start(*Gtk::manage(Glib::wrap(gsel)));
+
+ Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(_selected_color));
+ color_selector->show();
+ pack_start(*color_selector);
+
+ //_selected_color.signal_grabbed.connect(sigc::mem_fun(this, &SwatchSelector::_grabbedCb));
+ _selected_color.signal_dragged.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb));
+ _selected_color.signal_released.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb));
+ // signal_changed doesn't get called if updating shape with colour.
+ //_selected_color.signal_changed.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb));
+}
+
+SwatchSelector::~SwatchSelector()
+{
+ _gsel = nullptr;
+}
+
+SPGradientSelector *SwatchSelector::getGradientSelector()
+{
+ return _gsel;
+}
+
+void SwatchSelector::_changedCb()
+{
+ if (_updating_color) {
+ return;
+ }
+ // TODO might have to block cycles
+
+ if (_gsel && _gsel->getVector()) {
+ SPGradient *gradient = _gsel->getVector();
+ SPGradient *ngr = sp_gradient_ensure_vector_normalized(gradient);
+ if (ngr != gradient) {
+ /* Our master gradient has changed */
+ // TODO replace with proper - sp_gradient_vector_widget_load_gradient(GTK_WIDGET(swsel->_gsel), ngr);
+ }
+
+ ngr->ensureVector();
+
+
+ SPStop* stop = ngr->getFirstStop();
+ if (stop) {
+ SPColor color = _selected_color.color();
+ gfloat alpha = _selected_color.alpha();
+ guint32 rgb = color.toRGBA32( 0x00 );
+
+ // TODO replace with generic shared code that also handles icc-color
+ Inkscape::CSSOStringStream os;
+ gchar c[64];
+ sp_svg_write_color(c, sizeof(c), rgb);
+ os << "stop-color:" << c << ";stop-opacity:" << static_cast<gdouble>(alpha) <<";";
+ stop->setAttribute("style", os.str());
+
+ DocumentUndo::done(ngr->document, SP_VERB_CONTEXT_GRADIENT,
+ _("Change swatch color"));
+ }
+ }
+}
+
+void SwatchSelector::connectGrabbedHandler( GCallback handler, void *data )
+{
+ GObject* obj = G_OBJECT(_gsel);
+ g_signal_connect( obj, "grabbed", handler, data );
+}
+
+void SwatchSelector::connectDraggedHandler( GCallback handler, void *data )
+{
+ GObject* obj = G_OBJECT(_gsel);
+ g_signal_connect( obj, "dragged", handler, data );
+}
+
+void SwatchSelector::connectReleasedHandler( GCallback handler, void *data )
+{
+ GObject* obj = G_OBJECT(_gsel);
+ g_signal_connect( obj, "released", handler, data );
+}
+
+void SwatchSelector::connectchangedHandler( GCallback handler, void *data )
+{
+ GObject* obj = G_OBJECT(_gsel);
+ g_signal_connect( obj, "changed", handler, data );
+}
+
+void SwatchSelector::setVector(SPDocument */*doc*/, SPGradient *vector)
+{
+ //GtkVBox * box = gobj();
+ _gsel->setVector((vector) ? vector->document : nullptr, vector);
+
+ if ( vector && vector->isSolid() ) {
+ SPStop* stop = vector->getFirstStop();
+
+ guint32 const colorVal = stop->get_rgba32();
+ _updating_color = true;
+ _selected_color.setValue(colorVal);
+ _updating_color = false;
+ // gtk_widget_show_all( GTK_WIDGET(_csel) );
+ } else {
+ //gtk_widget_hide( GTK_WIDGET(_csel) );
+ }
+
+/*
+*/
+}
+
+} // namespace Widgets
+} // namespace Inkscape
+
+
+
+/*
+ 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/swatch-selector.h b/src/widgets/swatch-selector.h
new file mode 100644
index 0000000..88e7ad6
--- /dev/null
+++ b/src/widgets/swatch-selector.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_SWATCH_SELECTOR_H
+#define SEEN_SP_SWATCH_SELECTOR_H
+
+#include <gtkmm/box.h>
+#include "ui/selected-color.h"
+
+class SPDocument;
+class SPGradient;
+struct SPColorSelector;
+struct SPGradientSelector;
+
+namespace Inkscape
+{
+namespace Widgets
+{
+
+class SwatchSelector : public Gtk::VBox
+{
+public:
+ SwatchSelector();
+ ~SwatchSelector() override;
+
+ void connectGrabbedHandler( GCallback handler, void *data );
+ void connectDraggedHandler( GCallback handler, void *data );
+ void connectReleasedHandler( GCallback handler, void *data );
+ void connectchangedHandler( GCallback handler, void *data );
+
+ void setVector(SPDocument *doc, SPGradient *vector);
+
+ SPGradientSelector *getGradientSelector();
+
+private:
+ void _grabbedCb();
+ void _draggedCb();
+ void _releasedCb();
+ void _changedCb();
+
+ SPGradientSelector *_gsel;
+ Inkscape::UI::SelectedColor _selected_color;
+ bool _updating_color;
+};
+
+
+} // namespace Widgets
+} // namespace Inkscape
+
+#endif // SEEN_SP_SWATCH_SELECTOR_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..6248dec
--- /dev/null
+++ b/src/widgets/toolbox.cpp
@@ -0,0 +1,842 @@
+// 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 <gtkmm/box.h>
+#include <gtkmm/action.h>
+#include <gtkmm/actiongroup.h>
+#include <gtkmm/toolitem.h>
+#include <glibmm/i18n.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "inkscape.h"
+#include "verbs.h"
+
+#include "ink-action.h"
+
+#include "helper/action.h"
+#include "helper/verb-action.h"
+
+#include "include/gtkmm_version.h"
+
+#include "io/resource.h"
+
+#include "object/sp-namedview.h"
+
+#include "ui/icon-names.h"
+#include "ui/tools-switch.h"
+#include "ui/uxmanager.h"
+#include "ui/widget/button.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/style-swatch.h"
+#include "ui/widget/unit-tracker.h"
+
+#include "widgets/spinbutton-events.h"
+#include "widgets/spw-utilities.h"
+#include "widgets/widget-sizes.h"
+
+#include "xml/attribute-record.h"
+#include "xml/node-event-vector.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/rect-toolbar.h"
+#include "ui/toolbar/paintbucket-toolbar.h"
+#include "ui/toolbar/pencil-toolbar.h"
+#include "ui/toolbar/select-toolbar.h"
+#include "ui/toolbar/snap-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 "toolbox.h"
+
+#include "ui/tools/tool-base.h"
+
+//#define DEBUG_TEXT
+
+using Inkscape::UI::UXManager;
+using Inkscape::DocumentUndo;
+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"
+
+GtkIconSize ToolboxFactory::prefToSize( Glib::ustring const &path, int base ) {
+ static GtkIconSize 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];
+}
+
+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;
+ gchar const *data_name;
+ sp_verb_t verb;
+ sp_verb_t doubleclick_verb;
+} const tools[] = {
+ { "/tools/select", "select_tool", SP_VERB_CONTEXT_SELECT, SP_VERB_CONTEXT_SELECT_PREFS},
+ { "/tools/nodes", "node_tool", SP_VERB_CONTEXT_NODE, SP_VERB_CONTEXT_NODE_PREFS },
+ { "/tools/tweak", "tweak_tool", SP_VERB_CONTEXT_TWEAK, SP_VERB_CONTEXT_TWEAK_PREFS },
+ { "/tools/spray", "spray_tool", SP_VERB_CONTEXT_SPRAY, SP_VERB_CONTEXT_SPRAY_PREFS },
+ { "/tools/zoom", "zoom_tool", SP_VERB_CONTEXT_ZOOM, SP_VERB_CONTEXT_ZOOM_PREFS },
+ { "/tools/measure", "measure_tool", SP_VERB_CONTEXT_MEASURE, SP_VERB_CONTEXT_MEASURE_PREFS },
+ { "/tools/shapes/rect", "rect_tool", SP_VERB_CONTEXT_RECT, SP_VERB_CONTEXT_RECT_PREFS },
+ { "/tools/shapes/3dbox", "3dbox_tool", SP_VERB_CONTEXT_3DBOX, SP_VERB_CONTEXT_3DBOX_PREFS },
+ { "/tools/shapes/arc", "arc_tool", SP_VERB_CONTEXT_ARC, SP_VERB_CONTEXT_ARC_PREFS },
+ { "/tools/shapes/star", "star_tool", SP_VERB_CONTEXT_STAR, SP_VERB_CONTEXT_STAR_PREFS },
+ { "/tools/shapes/spiral", "spiral_tool", SP_VERB_CONTEXT_SPIRAL, SP_VERB_CONTEXT_SPIRAL_PREFS },
+ { "/tools/freehand/pencil", "pencil_tool", SP_VERB_CONTEXT_PENCIL, SP_VERB_CONTEXT_PENCIL_PREFS },
+ { "/tools/freehand/pen", "pen_tool", SP_VERB_CONTEXT_PEN, SP_VERB_CONTEXT_PEN_PREFS },
+ { "/tools/calligraphic", "dyna_draw_tool", SP_VERB_CONTEXT_CALLIGRAPHIC, SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS },
+ { "/tools/lpetool", "lpetool_tool", SP_VERB_CONTEXT_LPETOOL, SP_VERB_CONTEXT_LPETOOL_PREFS },
+ { "/tools/eraser", "eraser_tool", SP_VERB_CONTEXT_ERASER, SP_VERB_CONTEXT_ERASER_PREFS },
+ { "/tools/paintbucket", "paintbucket_tool", SP_VERB_CONTEXT_PAINTBUCKET, SP_VERB_CONTEXT_PAINTBUCKET_PREFS },
+ { "/tools/text", "text_tool", SP_VERB_CONTEXT_TEXT, SP_VERB_CONTEXT_TEXT_PREFS },
+ { "/tools/connector","connector_tool", SP_VERB_CONTEXT_CONNECTOR, SP_VERB_CONTEXT_CONNECTOR_PREFS },
+ { "/tools/gradient", "gradient_tool", SP_VERB_CONTEXT_GRADIENT, SP_VERB_CONTEXT_GRADIENT_PREFS },
+ { "/tools/mesh", "mesh_tool", SP_VERB_CONTEXT_MESH, SP_VERB_CONTEXT_MESH_PREFS },
+ { "/tools/dropper", "dropper_tool", SP_VERB_CONTEXT_DROPPER, SP_VERB_CONTEXT_DROPPER_PREFS },
+ { nullptr, nullptr, 0, 0 }
+};
+
+static struct {
+ gchar const *type_name;
+ gchar const *data_name;
+ GtkWidget *(*create_func)(SPDesktop *desktop);
+ gchar const *ui_name;
+ gint swatch_verb_id;
+ gchar const *swatch_tool;
+ gchar const *swatch_tip;
+} const aux_toolboxes[] = {
+ { "/tools/select", "select_toolbox", Inkscape::UI::Toolbar::SelectToolbar::create, "SelectToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/nodes", "node_toolbox", Inkscape::UI::Toolbar::NodeToolbar::create, "NodeToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/tweak", "tweak_toolbox", Inkscape::UI::Toolbar::TweakToolbar::create, "TweakToolbar",
+ SP_VERB_CONTEXT_TWEAK_PREFS, "/tools/tweak", N_("Color/opacity used for color tweaking")},
+ { "/tools/spray", "spray_toolbox", Inkscape::UI::Toolbar::SprayToolbar::create, "SprayToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/zoom", "zoom_toolbox", Inkscape::UI::Toolbar::ZoomToolbar::create, "ZoomToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ // If you change MeasureToolbar here, change it also in desktop-widget.cpp
+ { "/tools/measure", "measure_toolbox", Inkscape::UI::Toolbar::MeasureToolbar::create, "MeasureToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/shapes/star", "star_toolbox", Inkscape::UI::Toolbar::StarToolbar::create, "StarToolbar",
+ SP_VERB_CONTEXT_STAR_PREFS, "/tools/shapes/star", N_("Style of new stars")},
+ { "/tools/shapes/rect", "rect_toolbox", Inkscape::UI::Toolbar::RectToolbar::create, "RectToolbar",
+ SP_VERB_CONTEXT_RECT_PREFS, "/tools/shapes/rect", N_("Style of new rectangles")},
+ { "/tools/shapes/3dbox", "3dbox_toolbox", Inkscape::UI::Toolbar::Box3DToolbar::create, "3DBoxToolbar",
+ SP_VERB_CONTEXT_3DBOX_PREFS, "/tools/shapes/3dbox", N_("Style of new 3D boxes")},
+ { "/tools/shapes/arc", "arc_toolbox", Inkscape::UI::Toolbar::ArcToolbar::create, "ArcToolbar",
+ SP_VERB_CONTEXT_ARC_PREFS, "/tools/shapes/arc", N_("Style of new ellipses")},
+ { "/tools/shapes/spiral", "spiral_toolbox", Inkscape::UI::Toolbar::SpiralToolbar::create, "SpiralToolbar",
+ SP_VERB_CONTEXT_SPIRAL_PREFS, "/tools/shapes/spiral", N_("Style of new spirals")},
+ { "/tools/freehand/pencil", "pencil_toolbox", Inkscape::UI::Toolbar::PencilToolbar::create_pencil, "PencilToolbar",
+ SP_VERB_CONTEXT_PENCIL_PREFS, "/tools/freehand/pencil", N_("Style of new paths created by Pencil")},
+ { "/tools/freehand/pen", "pen_toolbox", Inkscape::UI::Toolbar::PencilToolbar::create_pen, "PenToolbar",
+ SP_VERB_CONTEXT_PEN_PREFS, "/tools/freehand/pen", N_("Style of new paths created by Pen")},
+ { "/tools/calligraphic", "calligraphy_toolbox", Inkscape::UI::Toolbar::CalligraphyToolbar::create, "CalligraphyToolbar",
+ SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS, "/tools/calligraphic", N_("Style of new calligraphic strokes")},
+ { "/tools/eraser", "eraser_toolbox", Inkscape::UI::Toolbar::EraserToolbar::create, "EraserToolbar",
+ SP_VERB_CONTEXT_ERASER_PREFS, "/tools/eraser", _("TBD")},
+ { "/tools/lpetool", "lpetool_toolbox", Inkscape::UI::Toolbar::LPEToolbar::create, "LPEToolToolbar",
+ SP_VERB_CONTEXT_LPETOOL_PREFS, "/tools/lpetool", _("TBD")},
+ // If you change TextToolbar here, change it also in desktop-widget.cpp
+ { "/tools/text", "text_toolbox", Inkscape::UI::Toolbar::TextToolbar::create, "TextToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/dropper", "dropper_toolbox", Inkscape::UI::Toolbar::DropperToolbar::create, "DropperToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/connector", "connector_toolbox", Inkscape::UI::Toolbar::ConnectorToolbar::create, "ConnectorToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/gradient", "gradient_toolbox", Inkscape::UI::Toolbar::GradientToolbar::create, "GradientToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/mesh", "mesh_toolbox", Inkscape::UI::Toolbar::MeshToolbar::create, "MeshToolbar",
+ SP_VERB_INVALID, nullptr, nullptr},
+ { "/tools/paintbucket", "paintbucket_toolbox", Inkscape::UI::Toolbar::PaintbucketToolbar::create, "PaintbucketToolbar",
+ SP_VERB_CONTEXT_PAINTBUCKET_PREFS, "/tools/paintbucket", N_("Style of Paint Bucket fill objects")},
+ { nullptr, nullptr, nullptr, nullptr,
+ SP_VERB_INVALID, nullptr, nullptr }
+};
+
+
+static Glib::RefPtr<Gtk::ActionGroup> create_or_fetch_actions( SPDesktop* desktop );
+
+static void setup_snap_toolbox(GtkWidget *toolbox, SPDesktop *desktop);
+
+static void setup_tool_toolbox(GtkWidget *toolbox, SPDesktop *desktop);
+static void update_tool_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox);
+
+static void setup_aux_toolbox(GtkWidget *toolbox, SPDesktop *desktop);
+static void update_aux_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox);
+
+static void setup_commands_toolbox(GtkWidget *toolbox, SPDesktop *desktop);
+static void update_commands_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox);
+
+static void trigger_sp_action( GtkAction* /*act*/, gpointer user_data )
+{
+ SPAction* targetAction = SP_ACTION(user_data);
+ if ( targetAction ) {
+ sp_action_perform( targetAction, nullptr );
+ }
+}
+
+static GtkAction* create_action_for_verb( Inkscape::Verb* verb, Inkscape::UI::View::View* view, GtkIconSize size )
+{
+ GtkAction* act = nullptr;
+
+ SPAction* targetAction = verb->get_action(Inkscape::ActionContext(view));
+ InkAction* inky = ink_action_new( verb->get_id(), _(verb->get_name()), verb->get_tip(), verb->get_image(), size );
+ act = GTK_ACTION(inky);
+ gtk_action_set_sensitive( act, targetAction->sensitive );
+
+ g_signal_connect( G_OBJECT(inky), "activate", G_CALLBACK(trigger_sp_action), targetAction );
+
+ // FIXME: memory leak: this is not unrefed anywhere
+ g_object_ref(G_OBJECT(targetAction));
+ g_object_set_data_full(G_OBJECT(inky), "SPAction", (void*) targetAction, (GDestroyNotify) &g_object_unref);
+ targetAction->signal_set_sensitive.connect(
+ sigc::bind<0>(
+ sigc::ptr_fun(&gtk_action_set_sensitive),
+ GTK_ACTION(inky)));
+
+ return act;
+}
+
+static std::map<SPDesktop*, Glib::RefPtr<Gtk::ActionGroup> > groups;
+
+static void desktopDestructHandler(SPDesktop *desktop)
+{
+ std::map<SPDesktop*, Glib::RefPtr<Gtk::ActionGroup> >::iterator it = groups.find(desktop);
+ if (it != groups.end())
+ {
+ groups.erase(it);
+ }
+}
+
+static Glib::RefPtr<Gtk::ActionGroup> create_or_fetch_actions( SPDesktop* desktop )
+{
+ Inkscape::UI::View::View *view = desktop;
+ gint verbsToUse[] = {
+ // disabled until we have icons for them:
+ //find
+ //SP_VERB_EDIT_TILE,
+ //SP_VERB_EDIT_UNTILE,
+ SP_VERB_DIALOG_ALIGN_DISTRIBUTE,
+ SP_VERB_DIALOG_DISPLAY,
+ SP_VERB_DIALOG_FILL_STROKE,
+ SP_VERB_DIALOG_NAMEDVIEW,
+ SP_VERB_DIALOG_TEXT,
+ SP_VERB_DIALOG_XML_EDITOR,
+ SP_VERB_DIALOG_SELECTORS,
+ SP_VERB_DIALOG_LAYERS,
+ SP_VERB_EDIT_CLONE,
+ SP_VERB_EDIT_COPY,
+ SP_VERB_EDIT_CUT,
+ SP_VERB_EDIT_DUPLICATE,
+ SP_VERB_EDIT_PASTE,
+ SP_VERB_EDIT_REDO,
+ SP_VERB_EDIT_UNDO,
+ SP_VERB_EDIT_UNLINK_CLONE,
+ //SP_VERB_FILE_EXPORT,
+ SP_VERB_DIALOG_EXPORT,
+ SP_VERB_FILE_IMPORT,
+ SP_VERB_FILE_NEW,
+ SP_VERB_FILE_OPEN,
+ SP_VERB_FILE_PRINT,
+ SP_VERB_FILE_SAVE,
+ SP_VERB_OBJECT_TO_CURVE,
+ SP_VERB_SELECTION_GROUP,
+ SP_VERB_SELECTION_OUTLINE,
+ SP_VERB_SELECTION_UNGROUP,
+ SP_VERB_ZOOM_1_1,
+ SP_VERB_ZOOM_1_2,
+ SP_VERB_ZOOM_2_1,
+ SP_VERB_ZOOM_DRAWING,
+ SP_VERB_ZOOM_IN,
+ SP_VERB_ZOOM_NEXT,
+ SP_VERB_ZOOM_OUT,
+ SP_VERB_ZOOM_PAGE,
+ SP_VERB_ZOOM_PAGE_WIDTH,
+ SP_VERB_ZOOM_PREV,
+ SP_VERB_ZOOM_SELECTION,
+ SP_VERB_ZOOM_CENTER_PAGE
+ };
+
+ GtkIconSize toolboxSize = ToolboxFactory::prefToSize("/toolbox/small");
+ Glib::RefPtr<Gtk::ActionGroup> mainActions;
+ if (desktop == nullptr)
+ {
+ return mainActions;
+ }
+
+ if ( groups.find(desktop) != groups.end() ) {
+ mainActions = groups[desktop];
+ }
+
+ if ( !mainActions ) {
+ mainActions = Gtk::ActionGroup::create("main");
+ groups[desktop] = mainActions;
+ desktop->connectDestroy(&desktopDestructHandler);
+ }
+
+ for (int i : verbsToUse) {
+ Inkscape::Verb* verb = Inkscape::Verb::get(i);
+ if ( verb ) {
+ if (!mainActions->get_action(verb->get_id())) {
+ GtkAction* act = create_action_for_verb( verb, view, toolboxSize );
+ mainActions->add(Glib::wrap(act));
+ }
+ }
+ }
+
+ if ( !mainActions->get_action("ToolZoom") ) {
+ for ( guint i = 0; i < G_N_ELEMENTS(tools) && tools[i].type_name; i++ ) {
+ Glib::RefPtr<VerbAction> va = VerbAction::create(Inkscape::Verb::get(tools[i].verb), Inkscape::Verb::get(tools[i].doubleclick_verb), view);
+ if ( va ) {
+ mainActions->add(va);
+ if ( i == 0 ) {
+ va->set_active(true);
+ }
+ } else {
+ // This creates a blank action using the data_name, this can replace
+ // tools that have been disabled by compile time options.
+ Glib::RefPtr<Gtk::Action> act = Gtk::Action::create(Glib::ustring(tools[i].data_name));
+ act->set_sensitive(false);
+ mainActions->add(act);
+ }
+ }
+ }
+
+ return mainActions;
+}
+
+
+static GtkWidget* toolboxNewCommon( GtkWidget* tb, BarId id, GtkPositionType /*handlePos*/ )
+{
+ g_object_set_data(G_OBJECT(tb), "desktop", nullptr);
+
+ gtk_widget_set_sensitive(tb, FALSE);
+
+ 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()
+{
+ auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(tb, "ToolToolbox");
+ gtk_box_set_homogeneous(GTK_BOX(tb), FALSE);
+
+ return toolboxNewCommon( tb, BAR_TOOL, GTK_POS_TOP );
+}
+
+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 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(tb, "CommandsToolbox");
+ gtk_box_set_homogeneous(GTK_BOX(tb), FALSE);
+
+ return toolboxNewCommon( tb, BAR_COMMANDS, GTK_POS_LEFT );
+}
+
+GtkWidget *ToolboxFactory::createSnapToolbox()
+{
+ auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(tb, "SnapToolbox");
+ gtk_box_set_homogeneous(GTK_BOX(tb), FALSE);
+
+ return toolboxNewCommon( tb, BAR_SNAP, GTK_POS_LEFT );
+}
+
+static GtkWidget* createCustomSlider( GtkAdjustment *adjustment, gdouble climbRate, guint digits, Inkscape::UI::Widget::UnitTracker *unit_tracker)
+{
+ auto adj = Glib::wrap(adjustment, true);
+ auto inkSpinner = new Inkscape::UI::Widget::SpinButton(adj, climbRate, digits);
+ inkSpinner->addUnitTracker(unit_tracker);
+ inkSpinner = Gtk::manage( inkSpinner );
+ GtkWidget *widget = GTK_WIDGET( inkSpinner->gobj() );
+ return widget;
+}
+
+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 = setup_tool_toolbox;
+ update_func = 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 = setup_commands_toolbox;
+ update_func = update_commands_toolbox;
+ break;
+
+ case BAR_SNAP:
+ setup_func = setup_snap_toolbox;
+ update_func = updateSnapToolbox;
+ 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, FALSE);
+ }
+
+} // end of sp_toolbox_set_desktop()
+
+
+static void setupToolboxCommon( GtkWidget *toolbox,
+ SPDesktop *desktop,
+ gchar const *ui_file,
+ gchar const* toolbarName,
+ gchar const* sizePref )
+{
+ Glib::RefPtr<Gtk::ActionGroup> mainActions = create_or_fetch_actions( desktop );
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ GtkUIManager* mgr = gtk_ui_manager_new();
+ GError* err = nullptr;
+
+ GtkOrientation orientation = GTK_ORIENTATION_HORIZONTAL;
+
+ gtk_ui_manager_insert_action_group( mgr, mainActions->gobj(), 0 );
+
+ Glib::ustring filename = get_filename(UIS, ui_file);
+ gtk_ui_manager_add_ui_from_file( mgr, filename.c_str(), &err );
+ if(err) {
+ g_warning("Failed to load %s: %s", filename.c_str(), err->message);
+ g_error_free(err);
+ return;
+ }
+
+ GtkWidget* toolBar = gtk_ui_manager_get_widget( mgr, toolbarName );
+ if ( prefs->getBool("/toolbox/icononly", true) ) {
+ gtk_toolbar_set_style( GTK_TOOLBAR(toolBar), GTK_TOOLBAR_ICONS );
+ }
+
+ GtkIconSize toolboxSize = ToolboxFactory::prefToSize(sizePref);
+ gtk_toolbar_set_icon_size( GTK_TOOLBAR(toolBar), static_cast<GtkIconSize>(toolboxSize) );
+
+ GtkPositionType pos = static_cast<GtkPositionType>(GPOINTER_TO_INT(g_object_get_data( G_OBJECT(toolbox), HANDLE_POS_MARK )));
+ orientation = ((pos == GTK_POS_LEFT) || (pos == GTK_POS_RIGHT)) ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
+ gtk_orientable_set_orientation (GTK_ORIENTABLE(toolBar), orientation);
+ gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolBar), TRUE);
+
+ g_object_set_data(G_OBJECT(toolBar), "desktop", nullptr);
+
+ GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox));
+ if ( child ) {
+ gtk_container_remove( GTK_CONTAINER(toolbox), child );
+ }
+
+ gtk_container_add( GTK_CONTAINER(toolbox), toolBar );
+}
+
+#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 );
+ }
+ }
+ }
+}
+
+void setup_tool_toolbox(GtkWidget *toolbox, SPDesktop *desktop)
+{
+ setupToolboxCommon(toolbox, desktop, "toolbar-tool.ui", "/ui/ToolToolbar", "/toolbox/tools/small");
+}
+
+void update_tool_toolbox( SPDesktop *desktop, ToolBase *eventcontext, GtkWidget * /*toolbox*/ )
+{
+ gchar const *const tname = ( eventcontext
+ ? eventcontext->getPrefsPath().c_str() //g_type_name(G_OBJECT_TYPE(eventcontext))
+ : nullptr );
+ Glib::RefPtr<Gtk::ActionGroup> mainActions = create_or_fetch_actions( desktop );
+
+ for (int i = 0 ; tools[i].type_name ; i++ ) {
+ Glib::RefPtr<Gtk::Action> act = mainActions->get_action( Inkscape::Verb::get(tools[i].verb)->get_id() );
+ if ( act ) {
+ bool setActive = tname && !strcmp(tname, tools[i].type_name);
+ Glib::RefPtr<VerbAction> verbAct = Glib::RefPtr<VerbAction>::cast_dynamic(act);
+ if ( verbAct ) {
+ verbAct->set_active(setActive);
+ }
+ }
+ }
+}
+
+/**
+ * \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);
+ 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 );
+ }
+
+ GtkIconSize toolboxSize = ToolboxFactory::prefToSize("/toolbox/small");
+ gtk_toolbar_set_icon_size( GTK_TOOLBAR(sub_toolbox), static_cast<GtkIconSize>(toolboxSize) );
+ gtk_widget_set_hexpand(sub_toolbox, TRUE);
+
+ // Add a swatch widget if one was specified
+ if ( aux_toolboxes[i].swatch_verb_id != SP_VERB_INVALID ) {
+ auto swatch = new Inkscape::UI::Widget::StyleSwatch( nullptr, _(aux_toolboxes[i].swatch_tip) );
+ swatch->setDesktop( desktop );
+ swatch->setClickVerb( aux_toolboxes[i].swatch_verb_id );
+ swatch->setWatchedTool( aux_toolboxes[i].swatch_tool, 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);
+ gtk_widget_set_name( holder, aux_toolboxes[i].ui_name );
+
+ // 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].data_name, holder);
+ gtk_widget_show(sub_toolbox);
+ gtk_widget_show(holder);
+ } else if (aux_toolboxes[i].swatch_verb_id != SP_VERB_NONE) {
+ g_warning("Could not create toolbox %s", aux_toolboxes[i].ui_name);
+ }
+ }
+}
+
+void update_aux_toolbox(SPDesktop * /*desktop*/, ToolBase *eventcontext, GtkWidget *toolbox)
+{
+ gchar const *tname = ( eventcontext
+ ? eventcontext->getPrefsPath().c_str() //g_type_name(G_OBJECT_TYPE(eventcontext))
+ : nullptr );
+ 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].data_name));
+ if (tname && !strcmp(tname, 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 setup_commands_toolbox(GtkWidget *toolbox, SPDesktop *desktop)
+{
+ setupToolboxCommon(toolbox, desktop, "toolbar-commands.ui", "/ui/CommandsToolbar", "/toolbox/small");
+}
+
+void update_commands_toolbox(SPDesktop * /*desktop*/, ToolBase * /*eventcontext*/, GtkWidget * /*toolbox*/)
+{
+}
+
+void setup_snap_toolbox(GtkWidget *toolbox, SPDesktop *desktop)
+{
+ Glib::ustring sizePref("/toolbox/secondary");
+ auto toolBar = Inkscape::UI::Toolbar::SnapToolbar::create(desktop);
+ auto prefs = Inkscape::Preferences::get();
+
+ if ( prefs->getBool("/toolbox/icononly", true) ) {
+ gtk_toolbar_set_style( GTK_TOOLBAR(toolBar), GTK_TOOLBAR_ICONS );
+ }
+
+ GtkIconSize toolboxSize = ToolboxFactory::prefToSize(sizePref.c_str());
+ gtk_toolbar_set_icon_size( GTK_TOOLBAR(toolBar), static_cast<GtkIconSize>(toolboxSize) );
+
+ GtkPositionType pos = static_cast<GtkPositionType>(GPOINTER_TO_INT(g_object_get_data( G_OBJECT(toolbox), HANDLE_POS_MARK )));
+ auto orientation = ((pos == GTK_POS_LEFT) || (pos == GTK_POS_RIGHT)) ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL;
+ gtk_orientable_set_orientation (GTK_ORIENTABLE(toolBar), orientation);
+ gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolBar), TRUE);
+
+ GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox));
+ if ( child ) {
+ gtk_container_remove( GTK_CONTAINER(toolbox), child );
+ }
+
+ gtk_container_add( GTK_CONTAINER(toolbox), toolBar );
+}
+
+Glib::ustring ToolboxFactory::getToolboxName(GtkWidget* toolbox)
+{
+ Glib::ustring name;
+ BarId id = static_cast<BarId>( GPOINTER_TO_INT(g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY)) );
+ switch(id) {
+ case BAR_TOOL:
+ name = "ToolToolbar";
+ break;
+ case BAR_AUX:
+ name = "AuxToolbar";
+ break;
+ case BAR_COMMANDS:
+ name = "CommandsToolbar";
+ break;
+ case BAR_SNAP:
+ name = "SnapToolbar";
+ break;
+ }
+
+ return name;
+}
+
+void ToolboxFactory::updateSnapToolbox(SPDesktop *desktop, ToolBase * /*eventcontext*/, GtkWidget *toolbox)
+{
+ auto tb = dynamic_cast<Inkscape::UI::Toolbar::SnapToolbar*>(Glib::wrap(GTK_TOOLBAR(gtk_bin_get_child(GTK_BIN(toolbox)))));
+
+ if (!tb) {
+ std::cerr << "Can't get snap toolbar" << std::endl;
+ return;
+ }
+
+ Inkscape::UI::Toolbar::SnapToolbar::update(tb);
+}
+
+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);
+}
+
+#define MODE_LABEL_WIDTH 70
+
+
+/*
+ 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..3a1bb4c
--- /dev/null
+++ b/src/widgets/toolbox.h
@@ -0,0 +1,84 @@
+// 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 "preferences.h"
+
+#define TOOLBAR_SLIDER_HINT "compact"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+class ToolBase;
+
+}
+}
+}
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+ class UnitTracker;
+}
+
+/**
+ * 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();
+ static GtkWidget *createAuxToolbox();
+ static GtkWidget *createCommandsToolbox();
+ static GtkWidget *createSnapToolbox();
+
+
+ static Glib::ustring getToolboxName(GtkWidget* toolbox);
+
+ static void updateSnapToolbox(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext, GtkWidget *toolbox);
+
+ static GtkIconSize prefToSize(Glib::ustring const &path, int base = 0 );
+ static Gtk::IconSize prefToSize_mm(Glib::ustring const &path, int base = 0);
+
+ ToolboxFactory() = delete;
+};
+
+
+
+} // 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..8dd8c6c
--- /dev/null
+++ b/src/widgets/widget-sizes.h
@@ -0,0 +1,57 @@
+// 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 AUX_BETWEEN_SPINBUTTONS 0
+#define AUX_SPINBUTTON_WIDTH 62
+#define AUX_SPINBUTTON_WIDTH_SMALL 56
+#define AUX_SPINBUTTON_HEIGHT 20
+#define AUX_OPTION_MENU_WIDTH 55
+#define AUX_OPTION_MENU_HEIGHT 20
+#define AUX_MENU_ITEM_WIDTH 32
+#define AUX_MENU_ITEM_HEIGHT 18
+
+#define SPIN_STEP 0.1
+#define SPIN_PAGE_STEP 5.0
+
+#define BOTTOM_BAR_HEIGHT 20
+#define BOTTOM_BUTTON_SIZE 14
+
+#define STATUS_BAR_FONT_SIZE 10000
+
+#define STATUS_ZOOM_WIDTH 57
+#define STATUS_ROTATION_WIDTH 57
+
+#define SELECTED_STYLE_SB_WIDTH 48
+#define SELECTED_STYLE_WIDTH 190
+#define STYLE_SWATCH_WIDTH 135
+
+#define STATUS_LAYER_FONT_SIZE 7700
+
+/*
+ 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 :