summaryrefslogtreecommitdiffstats
path: root/src/ui/widget
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
commitcca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch)
tree146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/widget
parentInitial commit. (diff)
downloadinkscape-12fc8abae6d434cac7670a59ed3a67301cc2eb10.tar.xz
inkscape-12fc8abae6d434cac7670a59ed3a67301cc2eb10.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui/widget')
-rw-r--r--src/ui/widget/alignment-selector.cpp80
-rw-r--r--src/ui/widget/alignment-selector.h53
-rw-r--r--src/ui/widget/anchor-selector.cpp97
-rw-r--r--src/ui/widget/anchor-selector.h62
-rw-r--r--src/ui/widget/attr-widget.h185
-rw-r--r--src/ui/widget/canvas-grid.cpp318
-rw-r--r--src/ui/widget/canvas-grid.h113
-rw-r--r--src/ui/widget/canvas.cpp2797
-rw-r--r--src/ui/widget/canvas.h234
-rw-r--r--src/ui/widget/color-entry.cpp157
-rw-r--r--src/ui/widget/color-entry.h58
-rw-r--r--src/ui/widget/color-icc-selector.cpp1025
-rw-r--r--src/ui/widget/color-icc-selector.h75
-rw-r--r--src/ui/widget/color-notebook.cpp347
-rw-r--r--src/ui/widget/color-notebook.h100
-rw-r--r--src/ui/widget/color-palette.cpp618
-rw-r--r--src/ui/widget/color-palette.h111
-rw-r--r--src/ui/widget/color-picker.cpp169
-rw-r--r--src/ui/widget/color-picker.h119
-rw-r--r--src/ui/widget/color-preview.cpp172
-rw-r--r--src/ui/widget/color-preview.h57
-rw-r--r--src/ui/widget/color-scales.cpp1062
-rw-r--r--src/ui/widget/color-scales.h127
-rw-r--r--src/ui/widget/color-slider.cpp546
-rw-r--r--src/ui/widget/color-slider.h92
-rw-r--r--src/ui/widget/combo-box-entry-tool-item.cpp707
-rw-r--r--src/ui/widget/combo-box-entry-tool-item.h154
-rw-r--r--src/ui/widget/combo-enums.h224
-rw-r--r--src/ui/widget/combo-tool-item.cpp290
-rw-r--r--src/ui/widget/combo-tool-item.h136
-rw-r--r--src/ui/widget/dash-selector.cpp259
-rw-r--r--src/ui/widget/dash-selector.h116
-rw-r--r--src/ui/widget/entity-entry.cpp208
-rw-r--r--src/ui/widget/entity-entry.h85
-rw-r--r--src/ui/widget/entry.cpp30
-rw-r--r--src/ui/widget/entry.h45
-rw-r--r--src/ui/widget/export-lists.cpp287
-rw-r--r--src/ui/widget/export-lists.h102
-rw-r--r--src/ui/widget/export-preview.cpp235
-rw-r--r--src/ui/widget/export-preview.h87
-rw-r--r--src/ui/widget/fill-style.cpp714
-rw-r--r--src/ui/widget/fill-style.h85
-rw-r--r--src/ui/widget/filter-effect-chooser.cpp203
-rw-r--r--src/ui/widget/filter-effect-chooser.h97
-rw-r--r--src/ui/widget/font-button.cpp58
-rw-r--r--src/ui/widget/font-button.h63
-rw-r--r--src/ui/widget/font-selector-toolbar.cpp302
-rw-r--r--src/ui/widget/font-selector-toolbar.h120
-rw-r--r--src/ui/widget/font-selector.cpp471
-rw-r--r--src/ui/widget/font-selector.h165
-rw-r--r--src/ui/widget/font-variants.cpp1459
-rw-r--r--src/ui/widget/font-variants.h223
-rw-r--r--src/ui/widget/font-variations.cpp182
-rw-r--r--src/ui/widget/font-variations.h128
-rw-r--r--src/ui/widget/frame.cpp80
-rw-r--r--src/ui/widget/frame.h75
-rw-r--r--src/ui/widget/framecheck.cpp29
-rw-r--r--src/ui/widget/framecheck.h66
-rw-r--r--src/ui/widget/gradient-editor.cpp653
-rw-r--r--src/ui/widget/gradient-editor.h114
-rw-r--r--src/ui/widget/gradient-image.cpp247
-rw-r--r--src/ui/widget/gradient-image.h76
-rw-r--r--src/ui/widget/gradient-selector-interface.h32
-rw-r--r--src/ui/widget/gradient-selector.cpp612
-rw-r--r--src/ui/widget/gradient-selector.h160
-rw-r--r--src/ui/widget/gradient-vector-selector.cpp327
-rw-r--r--src/ui/widget/gradient-vector-selector.h97
-rw-r--r--src/ui/widget/gradient-with-stops.cpp552
-rw-r--r--src/ui/widget/gradient-with-stops.h117
-rw-r--r--src/ui/widget/icon-combobox.h73
-rw-r--r--src/ui/widget/iconrenderer.cpp119
-rw-r--r--src/ui/widget/iconrenderer.h84
-rw-r--r--src/ui/widget/imagetoggler.cpp135
-rw-r--r--src/ui/widget/imagetoggler.h92
-rw-r--r--src/ui/widget/ink-color-wheel.cpp1507
-rw-r--r--src/ui/widget/ink-color-wheel.h152
-rw-r--r--src/ui/widget/ink-flow-box.cpp144
-rw-r--r--src/ui/widget/ink-flow-box.h68
-rw-r--r--src/ui/widget/ink-ruler.cpp476
-rw-r--r--src/ui/widget/ink-ruler.h79
-rw-r--r--src/ui/widget/ink-spinscale.cpp288
-rw-r--r--src/ui/widget/ink-spinscale.h100
-rw-r--r--src/ui/widget/label-tool-item.cpp66
-rw-r--r--src/ui/widget/label-tool-item.h51
-rw-r--r--src/ui/widget/labelled.cpp105
-rw-r--r--src/ui/widget/labelled.h88
-rw-r--r--src/ui/widget/layer-selector.cpp219
-rw-r--r--src/ui/widget/layer-selector.h82
-rw-r--r--src/ui/widget/licensor.cpp156
-rw-r--r--src/ui/widget/licensor.h57
-rw-r--r--src/ui/widget/marker-combo-box.cpp1033
-rw-r--r--src/ui/widget/marker-combo-box.h175
-rw-r--r--src/ui/widget/notebook-page.cpp48
-rw-r--r--src/ui/widget/notebook-page.h57
-rw-r--r--src/ui/widget/object-composite-settings.cpp312
-rw-r--r--src/ui/widget/object-composite-settings.h78
-rw-r--r--src/ui/widget/page-properties.cpp515
-rw-r--r--src/ui/widget/page-properties.h59
-rw-r--r--src/ui/widget/page-selector.cpp196
-rw-r--r--src/ui/widget/page-selector.h91
-rw-r--r--src/ui/widget/page-size-preview.cpp185
-rw-r--r--src/ui/widget/page-size-preview.h50
-rw-r--r--src/ui/widget/paint-selector.cpp1476
-rw-r--r--src/ui/widget/paint-selector.h226
-rw-r--r--src/ui/widget/point.cpp180
-rw-r--r--src/ui/widget/point.h196
-rw-r--r--src/ui/widget/preferences-widget.cpp1118
-rw-r--r--src/ui/widget/preferences-widget.h345
-rw-r--r--src/ui/widget/preview.cpp511
-rw-r--r--src/ui/widget/preview.h165
-rw-r--r--src/ui/widget/random.cpp101
-rw-r--r--src/ui/widget/random.h125
-rw-r--r--src/ui/widget/registered-enums.h99
-rw-r--r--src/ui/widget/registered-widget.cpp843
-rw-r--r--src/ui/widget/registered-widget.h455
-rw-r--r--src/ui/widget/registry.cpp58
-rw-r--r--src/ui/widget/registry.h51
-rw-r--r--src/ui/widget/rendering-options.cpp122
-rw-r--r--src/ui/widget/rendering-options.h68
-rw-r--r--src/ui/widget/rotateable.cpp179
-rw-r--r--src/ui/widget/rotateable.h71
-rw-r--r--src/ui/widget/scalar-unit.cpp271
-rw-r--r--src/ui/widget/scalar-unit.h196
-rw-r--r--src/ui/widget/scalar.cpp186
-rw-r--r--src/ui/widget/scalar.h191
-rw-r--r--src/ui/widget/scroll-utils.cpp53
-rw-r--r--src/ui/widget/scroll-utils.h32
-rw-r--r--src/ui/widget/scrollprotected.h117
-rw-r--r--src/ui/widget/selected-style.cpp1435
-rw-r--r--src/ui/widget/selected-style.h302
-rw-r--r--src/ui/widget/shapeicon.cpp117
-rw-r--r--src/ui/widget/shapeicon.h101
-rw-r--r--src/ui/widget/spin-button-tool-item.cpp607
-rw-r--r--src/ui/widget/spin-button-tool-item.h130
-rw-r--r--src/ui/widget/spin-scale.cpp231
-rw-r--r--src/ui/widget/spin-scale.h114
-rw-r--r--src/ui/widget/spinbutton.cpp126
-rw-r--r--src/ui/widget/spinbutton.h112
-rw-r--r--src/ui/widget/stroke-style.cpp1232
-rw-r--r--src/ui/widget/stroke-style.h213
-rw-r--r--src/ui/widget/style-subject.cpp113
-rw-r--r--src/ui/widget/style-subject.h114
-rw-r--r--src/ui/widget/style-swatch.cpp386
-rw-r--r--src/ui/widget/style-swatch.h107
-rw-r--r--src/ui/widget/swatch-selector.cpp148
-rw-r--r--src/ui/widget/swatch-selector.h65
-rw-r--r--src/ui/widget/text.cpp60
-rw-r--r--src/ui/widget/text.h81
-rw-r--r--src/ui/widget/tolerance-slider.cpp215
-rw-r--r--src/ui/widget/tolerance-slider.h88
-rw-r--r--src/ui/widget/unit-menu.cpp152
-rw-r--r--src/ui/widget/unit-menu.h154
-rw-r--r--src/ui/widget/unit-tracker.cpp315
-rw-r--r--src/ui/widget/unit-tracker.h89
154 files changed, 40428 insertions, 0 deletions
diff --git a/src/ui/widget/alignment-selector.cpp b/src/ui/widget/alignment-selector.cpp
new file mode 100644
index 0000000..11d1166
--- /dev/null
+++ b/src/ui/widget/alignment-selector.cpp
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.cpp
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/alignment-selector.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void AlignmentSelector::setupButton(const Glib::ustring& icon, Gtk::Button& button) {
+ Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ buttonIcon->show();
+
+ button.set_relief(Gtk::RELIEF_NONE);
+ button.show();
+ button.add(*buttonIcon);
+ button.set_can_focus(false);
+}
+
+AlignmentSelector::AlignmentSelector()
+ : _container()
+{
+ set_halign(Gtk::ALIGN_CENTER);
+ // clang-format off
+ setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]);
+ setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]);
+ setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]);
+ setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]);
+ setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]);
+ setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]);
+ // clang-format on
+
+ _container.set_row_homogeneous();
+ _container.set_column_homogeneous(true);
+
+ for(int i = 0; i < 9; ++i) {
+ _buttons[i].signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(*this, &AlignmentSelector::btn_activated), i));
+
+ _container.attach(_buttons[i], i % 3, i / 3, 1, 1);
+ }
+
+ this->add(_container);
+}
+
+AlignmentSelector::~AlignmentSelector()
+{
+ // TODO Auto-generated destructor stub
+}
+
+void AlignmentSelector::btn_activated(int index)
+{
+ _alignmentClicked.emit(index);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/alignment-selector.h b/src/ui/widget/alignment-selector.h
new file mode 100644
index 0000000..5bcf0fa
--- /dev/null
+++ b/src/ui/widget/alignment-selector.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.h
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef ANCHOR_SELECTOR_H_
+#define ANCHOR_SELECTOR_H_
+
+#include <gtkmm/bin.h>
+#include <gtkmm/button.h>
+#include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AlignmentSelector : public Gtk::Bin
+{
+private:
+ Gtk::Button _buttons[9];
+ Gtk::Grid _container;
+
+ sigc::signal<void, int> _alignmentClicked;
+
+ void setupButton(const Glib::ustring &icon, Gtk::Button &button);
+ void btn_activated(int index);
+
+public:
+
+ sigc::signal<void, int> &on_alignmentClicked() { return _alignmentClicked; }
+
+ AlignmentSelector();
+ ~AlignmentSelector() override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* ANCHOR_SELECTOR_H_ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/anchor-selector.cpp b/src/ui/widget/anchor-selector.cpp
new file mode 100644
index 0000000..b151a81
--- /dev/null
+++ b/src/ui/widget/anchor-selector.cpp
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.cpp
+ *
+ * Created on: Mar 22, 2012
+ * Author: denis
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include "ui/widget/anchor-selector.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void AnchorSelector::setupButton(const Glib::ustring& icon, Gtk::ToggleButton& button) {
+ Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ buttonIcon->show();
+
+ button.set_relief(Gtk::RELIEF_NONE);
+ button.show();
+ button.add(*buttonIcon);
+ button.set_can_focus(false);
+}
+
+AnchorSelector::AnchorSelector()
+ : _container()
+{
+ set_halign(Gtk::ALIGN_CENTER);
+ setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]);
+ setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]);
+ setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]);
+ setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]);
+ setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]);
+ setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]);
+ setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]);
+
+ _container.set_row_homogeneous();
+ _container.set_column_homogeneous(true);
+
+ for (int i = 0; i < 9; ++i) {
+ _buttons[i].signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &AnchorSelector::btn_activated), i));
+
+ _container.attach(_buttons[i], i % 3, i / 3, 1, 1);
+ }
+ _selection = 4;
+ _buttons[4].set_active();
+
+ this->add(_container);
+}
+
+AnchorSelector::~AnchorSelector()
+{
+ // TODO Auto-generated destructor stub
+}
+
+void AnchorSelector::btn_activated(int index)
+{
+ if (_selection == index && _buttons[index].get_active() == false) {
+ _buttons[index].set_active(true);
+ }
+ else if (_selection != index && _buttons[index].get_active()) {
+ int old_selection = _selection;
+ _selection = index;
+ _buttons[old_selection].set_active(false);
+ _selectionChanged.emit();
+ }
+}
+
+void AnchorSelector::setAlignment(int horizontal, int vertical)
+{
+ int index = 3 * vertical + horizontal;
+ if (index >= 0 && index < 9) {
+ _buttons[index].set_active(!_buttons[index].get_active());
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/anchor-selector.h b/src/ui/widget/anchor-selector.h
new file mode 100644
index 0000000..49ce0b2
--- /dev/null
+++ b/src/ui/widget/anchor-selector.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * anchor-selector.h
+ *
+ * Created on: Mar 22, 2012
+ * Author: denis
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef ANCHOR_SELECTOR_H_
+#define ANCHOR_SELECTOR_H_
+
+#include <gtkmm/bin.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AnchorSelector : public Gtk::Bin
+{
+private:
+ Gtk::ToggleButton _buttons[9];
+ int _selection;
+ Gtk::Grid _container;
+
+ sigc::signal<void> _selectionChanged;
+
+ void setupButton(const Glib::ustring &icon, Gtk::ToggleButton &button);
+ void btn_activated(int index);
+
+public:
+
+ int getHorizontalAlignment() { return _selection % 3; }
+ int getVerticalAlignment() { return _selection / 3; }
+
+ sigc::signal<void> &on_selectionChanged() { return _selectionChanged; }
+
+ void setAlignment(int horizontal, int vertical);
+
+ AnchorSelector();
+ ~AnchorSelector() override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* ANCHOR_SELECTOR_H_ */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/attr-widget.h b/src/ui/widget/attr-widget.h
new file mode 100644
index 0000000..ce5fe12
--- /dev/null
+++ b/src/ui/widget/attr-widget.h
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Rodrigo Kumpera <kumpera@gmail.com>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ATTR_WIDGET_H
+#define INKSCAPE_UI_WIDGET_ATTR_WIDGET_H
+
+#include "attributes.h"
+#include "object/sp-object.h"
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+enum DefaultValueType
+{
+ T_NONE,
+ T_DOUBLE,
+ T_VECT_DOUBLE,
+ T_BOOL,
+ T_UINT,
+ T_CHARPTR
+};
+
+/**
+ * Very basic interface for classes that control attributes.
+ */
+class DefaultValueHolder
+{
+ DefaultValueType type;
+ union {
+ double d_val;
+ std::vector<double>* vt_val;
+ bool b_val;
+ unsigned int uint_val;
+ char* cptr_val;
+ } value;
+
+ //FIXME remove copy ctor and assignment operator as private to avoid double free of the vector
+public:
+ DefaultValueHolder () {
+ type = T_NONE;
+ }
+
+ DefaultValueHolder (double d) {
+ type = T_DOUBLE;
+ value.d_val = d;
+ }
+
+ DefaultValueHolder (std::vector<double>* d) {
+ type = T_VECT_DOUBLE;
+ value.vt_val = d;
+ }
+
+ DefaultValueHolder (char* c) {
+ type = T_CHARPTR;
+ value.cptr_val = c;
+ }
+
+ DefaultValueHolder (bool d) {
+ type = T_BOOL;
+ value.b_val = d;
+ }
+
+ DefaultValueHolder (unsigned int ui) {
+ type = T_UINT;
+ value.uint_val = ui;
+ }
+
+ ~DefaultValueHolder() {
+ if (type == T_VECT_DOUBLE)
+ delete value.vt_val;
+ }
+
+ unsigned int as_uint() {
+ g_assert (type == T_UINT);
+ return value.uint_val;
+ }
+
+ bool as_bool() {
+ g_assert (type == T_BOOL);
+ return value.b_val;
+ }
+
+ double as_double() {
+ g_assert (type == T_DOUBLE);
+ return value.d_val;
+ }
+
+ std::vector<double>* as_vector() {
+ g_assert (type == T_VECT_DOUBLE);
+ return value.vt_val;
+ }
+
+ char* as_charptr() {
+ g_assert (type == T_CHARPTR);
+ return value.cptr_val;
+ }
+};
+
+class AttrWidget
+{
+public:
+ AttrWidget(const SPAttr a, unsigned int value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttr a, double value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttr a, bool value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttr a, char* value)
+ : _attr(a),
+ _default(value)
+ {}
+
+ AttrWidget(const SPAttr a)
+ : _attr(a),
+ _default()
+ {}
+
+ virtual ~AttrWidget()
+ = default;
+
+ virtual Glib::ustring get_as_attribute() const = 0;
+ virtual void set_from_attribute(SPObject*) = 0;
+
+ SPAttr get_attribute() const
+ {
+ return _attr;
+ }
+
+ sigc::signal<void>& signal_attr_changed()
+ {
+ return _signal;
+ }
+protected:
+ DefaultValueHolder* get_default() { return &_default; }
+ const gchar* attribute_value(SPObject* o) const
+ {
+ const gchar* name = (const gchar*)sp_attribute_name(_attr);
+ if(name && o) {
+ const gchar* val = o->getRepr()->attribute(name);
+ return val;
+ }
+ return nullptr;
+ }
+
+private:
+ const SPAttr _attr;
+ DefaultValueHolder _default;
+ sigc::signal<void> _signal;
+};
+
+}
+}
+}
+
+#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/ui/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp
new file mode 100644
index 0000000..19b21d0
--- /dev/null
+++ b/src/ui/widget/canvas-grid.cpp
@@ -0,0 +1,318 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Rewrite of code originally in desktop-widget.cpp.
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+// The scrollbars, and canvas are tightly coupled so it makes sense to have a dedicated
+// widget to handle their interactions. The buttons are along for the ride. I don't see
+// how to add the buttons easily via a .ui file (which would allow the user to put any
+// buttons they want in their place).
+
+#include <glibmm/i18n.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/label.h>
+
+#include "canvas-grid.h"
+
+#include "desktop.h" // Hopefully temp.
+#include "desktop-events.h" // Hopefully temp.
+
+#include "display/control/canvas-item-drawing.h" // sticky
+
+#include "ui/icon-loader.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/ink-ruler.h"
+
+#include "widgets/desktop-widget.h" // Hopefully temp.
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+CanvasGrid::CanvasGrid(SPDesktopWidget *dtw)
+{
+ _dtw = dtw;
+ set_name("CanvasGrid");
+
+ // Canvas
+ _canvas = Gtk::manage(new Inkscape::UI::Widget::Canvas());
+ _canvas->set_hexpand(true);
+ _canvas->set_vexpand(true);
+ _canvas->set_can_focus(true);
+ _canvas->signal_event().connect(sigc::mem_fun(*this, &CanvasGrid::SignalEvent)); // TEMP
+
+ _canvas_overlay.add(*_canvas);
+ _canvas_overlay.add_overlay(*_command_palette.get_base_widget());
+
+ // Horizontal Scrollbar
+ _hadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0);
+ _hadj->signal_value_changed().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::on_adjustment_value_changed));
+ _hscrollbar = Gtk::manage(new Gtk::Scrollbar(_hadj, Gtk::ORIENTATION_HORIZONTAL));
+ _hscrollbar->set_name("CanvasScrollbar");
+ _hscrollbar->set_hexpand(true);
+ _hscrollbar->set_no_show_all();
+
+ // Vertical Scrollbar
+ _vadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0);
+ _vadj->signal_value_changed().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::on_adjustment_value_changed));
+ _vscrollbar = Gtk::manage(new Gtk::Scrollbar(_vadj, Gtk::ORIENTATION_VERTICAL));
+ _vscrollbar->set_name("CanvasScrollbar");
+ _vscrollbar->set_vexpand(true);
+ _vscrollbar->set_no_show_all();
+
+ // Horizontal Ruler
+ _hruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_HORIZONTAL));
+ _hruler->add_track_widget(*_canvas);
+ _hruler->set_hexpand(true);
+ // Tooltip/Unit set elsewhere.
+
+ // For creating guides, etc.
+ _hruler->signal_button_press_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _hruler, true));
+ _hruler->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _hruler, true));
+ _hruler->signal_motion_notify_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _hruler, true));
+
+ // Vertical Ruler
+ _vruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_VERTICAL));
+ _vruler->add_track_widget(*_canvas);
+ _vruler->set_vexpand(true);
+ // Tooltip/Unit set elsewhere.
+
+ // For creating guides, etc.
+ _vruler->signal_button_press_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _vruler, false));
+ _vruler->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _vruler, false));
+ _vruler->signal_motion_notify_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _vruler, false));
+
+ // Guide Lock
+ auto image1 = Gtk::manage(new Gtk::Image("object-locked", Gtk::ICON_SIZE_MENU));
+ _guide_lock = Gtk::manage(new Gtk::ToggleButton());
+ _guide_lock->set_name("LockGuides");
+ _guide_lock->add(*image1);
+ _guide_lock->set_no_show_all();
+ // To be replaced by Gio::Action:
+ _guide_lock->signal_toggled().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::update_guides_lock));
+ _guide_lock->set_tooltip_text(_("Toggle lock of all guides in the document"));
+
+ // CMS Adjust
+ auto image2 = Gtk::manage(new Gtk::Image("color-management", Gtk::ICON_SIZE_MENU));
+ _cms_adjust = Gtk::manage(new Gtk::ToggleButton());
+ _cms_adjust->set_name("CMS_Adjust");
+ _cms_adjust->add(*image2);
+ // Can't access via C++ API, fixed in Gtk4.
+ gtk_actionable_set_action_name( GTK_ACTIONABLE(_cms_adjust->gobj()), "win.canvas-color-manage");
+ _cms_adjust->set_tooltip_text(_("Toggle color-managed display for this document window"));
+ _cms_adjust->set_no_show_all();
+
+ // Sticky Zoom
+ auto image3 = Gtk::manage(sp_get_icon_image("zoom-original", Gtk::ICON_SIZE_MENU));
+ _sticky_zoom = Gtk::manage(new Gtk::ToggleButton());
+ _sticky_zoom->set_name("StickyZoom");
+ _sticky_zoom->add(*image3);
+ // To be replaced by Gio::Action:
+ _sticky_zoom->signal_toggled().connect(sigc::mem_fun(_dtw, &SPDesktopWidget::sticky_zoom_toggled));
+ _sticky_zoom->set_tooltip_text(_("Zoom drawing if window size changes"));
+ _sticky_zoom->set_no_show_all();
+
+ // Top row
+ attach(*_guide_lock, 0, 0, 1, 1);
+ attach(*_hruler, 1, 0, 1, 1);
+ attach(*_sticky_zoom, 2, 0, 1, 1);
+
+ // Middle row
+ attach(*_vruler, 0, 1, 1, 1);
+ attach(_canvas_overlay, 1, 1, 1, 1);
+ attach(*_vscrollbar, 2, 1, 1, 1);
+
+ // Bottom row
+ attach(*_hscrollbar, 1, 2, 1, 1);
+ attach(*_cms_adjust, 2, 2, 1, 1);
+
+ // Update rulers on size change.
+ signal_size_allocate().connect(sigc::mem_fun(*this, &CanvasGrid::OnSizeAllocate));
+
+ show_all();
+}
+
+CanvasGrid::~CanvasGrid() {
+}
+
+// _dt2r should be a member of _canvas.
+// get_display_area should be a member of _canvas.
+void
+CanvasGrid::UpdateRulers()
+{
+ Geom::Rect viewbox = _dtw->desktop->get_display_area().bounds();
+ // Use integer values of the canvas for calculating the display area, similar
+ // to the integer values used for positioning the grid lines. (see Canvas::scrollTo(),
+ // where ix and iy are rounded integer values; these values are stored in CanvasItemBuffer->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 _dt2r = _dtw->_dt2r;
+ Geom::Point _ruler_origin = _dtw->_ruler_origin;
+
+ 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 (_dtw->desktop->is_yaxisdown()) {
+ std::swap(lower_y, upper_y);
+ }
+ _vruler->set_range(lower_y, upper_y);
+}
+
+void
+CanvasGrid::ShowScrollbars(bool state)
+{
+ _show_scrollbars = state;
+
+ if (_show_scrollbars) {
+ // Show scrollbars
+ _hscrollbar->show();
+ _vscrollbar->show();
+ _cms_adjust->show();
+ _cms_adjust->show_all_children();
+ _sticky_zoom->show();
+ _sticky_zoom->show_all_children();
+ } else {
+ // Hide scrollbars
+ _hscrollbar->hide();
+ _vscrollbar->hide();
+ _cms_adjust->hide();
+ _sticky_zoom->hide();
+ }
+}
+
+void
+CanvasGrid::ToggleScrollbars()
+{
+ _show_scrollbars = !_show_scrollbars;
+ ShowScrollbars(_show_scrollbars);
+
+ // Will be replaced by actions
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/fullscreen/scrollbars/state", _show_scrollbars);
+ prefs->setBool("/window/scrollbars/state", _show_scrollbars);
+}
+
+void
+CanvasGrid::ShowRulers(bool state)
+{
+ // Sticky zoom button is always shown. We must adjust canvas when rulers are toggled or canvas
+ // won't expand fully.
+ _show_rulers = state;
+
+ if (_show_rulers) {
+ // Show rulers
+ _hruler->show();
+ _vruler->show();
+ _guide_lock->show();
+ _guide_lock->show_all_children();
+ remove(_canvas_overlay);
+ attach(_canvas_overlay, 1, 1, 1, 1);
+ } else {
+ // Hide rulers
+ _hruler->hide();
+ _vruler->hide();
+ _guide_lock->hide();
+ remove(_canvas_overlay);
+ attach(_canvas_overlay, 1, 0, 1, 2);
+ }
+}
+
+void
+CanvasGrid::ToggleRulers()
+{
+ _show_rulers = !_show_rulers;
+ ShowRulers(_show_rulers);
+
+ // Will be replaced by actions
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/fullscreen/rulers/state", _show_rulers);
+ prefs->setBool("/window/rulers/state", _show_rulers);
+}
+
+void
+CanvasGrid::ToggleCommandPalette() {
+ _command_palette.toggle();
+}
+
+void
+CanvasGrid::ShowCommandPalette(bool state)
+{
+ if (state) {
+ _command_palette.open();
+ }
+ _command_palette.close();
+}
+
+// Update rulers on change of widget size, but only if allocation really changed.
+void
+CanvasGrid::OnSizeAllocate(Gtk::Allocation& allocation)
+{
+ if (!(_allocation == allocation)) { // No != function defined!
+ _allocation = allocation;
+ UpdateRulers();
+ }
+}
+
+// This belong in Canvas class
+bool
+CanvasGrid::SignalEvent(GdkEvent *event)
+{
+ if (event->type == GDK_BUTTON_PRESS) {
+ _canvas->grab_focus();
+ _command_palette.close();
+ }
+
+ if (event->type == GDK_BUTTON_PRESS && event->button.button == 3) {
+ if (event->button.state & GDK_SHIFT_MASK) {
+ _dtw->desktop->getCanvasDrawing()->set_sticky(true);
+ } else {
+ _dtw->desktop->getCanvasDrawing()->set_sticky(false);
+ }
+ }
+
+ // Pass keyboard events back to the desktop root handler so TextTool can work
+ if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) && !_canvas->get_current_canvas_item()) {
+ return sp_desktop_root_handler(event, _dtw->desktop);
+ }
+ return false;
+}
+
+// TODO Add actions so we can set shortcuts.
+// * Sticky Zoom
+// * CMS Adjust
+// * Guide Lock
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/canvas-grid.h b/src/ui/widget/canvas-grid.h
new file mode 100644
index 0000000..17a986f
--- /dev/null
+++ b/src/ui/widget/canvas-grid.h
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_WIDGET_CANVASGRID_H
+#define INKSCAPE_UI_WIDGET_CANVASGRID_H
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include <gtkmm/label.h>
+#include <gtkmm/overlay.h>
+#include "ui/dialog/command-palette.h"
+
+class SPCanvas;
+class SPDesktopWidget;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Canvas;
+class Ruler;
+
+/**
+ * A Gtk::Grid widget that contains rulers, scrollbars, buttons, and, of course, the canvas.
+ * Canvas has an overlay to let us put stuff on the canvas.
+ */
+class CanvasGrid : public Gtk::Grid
+{
+public:
+
+ CanvasGrid(SPDesktopWidget *dtw);
+ ~CanvasGrid() override;
+
+ void ShowScrollbars(bool state = true);
+ void ToggleScrollbars();
+
+ void ShowRulers(bool state = true);
+ void ToggleRulers();
+ void UpdateRulers();
+
+ void ShowCommandPalette(bool state = true);
+ void ToggleCommandPalette();
+
+ Inkscape::UI::Widget::Canvas *GetCanvas() { return _canvas; };
+
+ // Hopefully temp.
+ Inkscape::UI::Widget::Ruler *GetHRuler() { return _vruler; };
+ Inkscape::UI::Widget::Ruler *GetVRuler() { return _hruler; };
+ Glib::RefPtr<Gtk::Adjustment> GetHAdj() { return _hadj; };
+ Glib::RefPtr<Gtk::Adjustment> GetVAdj() { return _vadj; };
+ Gtk::ToggleButton *GetGuideLock() { return _guide_lock; }
+ Gtk::ToggleButton *GetCmsAdjust() { return _cms_adjust; }
+ Gtk::ToggleButton *GetStickyZoom() { return _sticky_zoom; };
+
+private:
+
+ // Signal callbacks
+ void OnSizeAllocate(Gtk::Allocation& allocation);
+ bool SignalEvent(GdkEvent *event);
+
+ // The Widgets
+ Inkscape::UI::Widget::Canvas *_canvas;
+
+ Gtk::Overlay _canvas_overlay;
+
+ Dialog::CommandPalette _command_palette;
+
+ Glib::RefPtr<Gtk::Adjustment> _hadj;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+ Gtk::Scrollbar *_hscrollbar;
+ Gtk::Scrollbar *_vscrollbar;
+
+ Inkscape::UI::Widget::Ruler *_hruler;
+ Inkscape::UI::Widget::Ruler *_vruler;
+
+ Gtk::ToggleButton *_guide_lock;
+ Gtk::ToggleButton *_cms_adjust;
+ Gtk::ToggleButton *_sticky_zoom;
+
+ // To be replaced by stateful Gio::Actions
+ bool _show_scrollbars = true;
+ bool _show_rulers = true;
+
+ // Hopefully temp
+ SPDesktopWidget *_dtw;
+
+ // Store allocation so we don't redraw too often.
+ Gtk::Allocation _allocation;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif // INKSCAPE_UI_WIDGET_CANVASGRID_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/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp
new file mode 100644
index 0000000..8e6de8c
--- /dev/null
+++ b/src/ui/widget/canvas.cpp
@@ -0,0 +1,2797 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * Authors:
+ * Tavmjong Bah
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream> // Logging
+#include <algorithm> // Sort
+#include <set> // Coarsener
+
+#include <glibmm/i18n.h>
+
+#include <2geom/rect.h>
+
+#include "canvas.h"
+#include "canvas-grid.h"
+
+#include "color.h" // Background color
+#include "cms-system.h" // Color correction
+#include "desktop.h"
+#include "preferences.h"
+
+#include "display/cairo-utils.h" // Checkerboard background
+#include "display/drawing.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/snap-indicator.h"
+
+#include "ui/tools/tool-base.h" // Default cursor
+
+#include "framecheck.h" // For frame profiling
+#define framecheck_whole_function(D) auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event(__func__) : FrameCheck::Event();
+
+/*
+ * The canvas is responsible for rendering the SVG drawing with various "control"
+ * items below and on top of the drawing. Rendering is triggered by a call to one of:
+ *
+ *
+ * * redraw_all() Redraws the entire canvas by calling redraw_area() with the canvas area.
+ *
+ * * redraw_area() Redraws the indicated area. Use when there is a change that doesn't affect
+ * a CanvasItem's geometry or size.
+ *
+ * * request_update() Redraws after recalculating bounds for changed CanvasItems. Use if a
+ * CanvasItem's geometry or size has changed.
+ *
+ * The first three functions add a request to the Gtk's "idle" list via
+ *
+ * * add_idle() Which causes Gtk to call when resources are available:
+ *
+ * * on_idle() Which sets up the backing stores, divides the area of the canvas that has been marked
+ * unclean into rectangles that are small enough to render quickly, and renders them outwards
+ * from the mouse with a call to:
+ *
+ * * paint_rect_internal() Which paints the rectangle using paint_single_buffer(). It renders onto a Cairo
+ * surface "backing_store". After a piece is rendered there is a call to:
+ *
+ * * queue_draw_area() A Gtk function for marking areas of the window as needing a repaint, which when
+ * the time is right calls:
+ *
+ * * on_draw() Which blits the Cairo surface to the screen.
+ *
+ * The other responsibility of the canvas is to determine where to send GUI events. It does this
+ * by determining which CanvasItem is "picked" and then forwards the events to that item. Not all
+ * items can be picked. As a last resort, the "CatchAll" CanvasItem will be picked as it is the
+ * lowest CanvasItem in the stack (except for the "root" CanvasItem). With a small be of work, it
+ * should be possible to make the "root" CanvasItem a "CatchAll" eliminating the need for a
+ * dedicated "CatchAll" CanvasItem. There probably could be efficiency improvements as some
+ * items that are not pickable probably should be which would save having to effectively pick
+ * them "externally" (e.g. gradient CanvasItemCurves).
+ */
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*
+ * GDK event utilities
+ */
+
+// GdkEvents can only be safely copied using gdk_event_copy. However, this function allocates. Therefore, we need the following smart pointer to wrap the result.
+struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}};
+using GdkEventUniqPtr = std::unique_ptr<GdkEvent, GdkEventFreer>;
+
+// Copies a GdkEvent, returning the result as a smart pointer.
+auto make_unique_copy(const GdkEvent &ev) {return GdkEventUniqPtr(gdk_event_copy(&ev));}
+
+/*
+ * Preferences
+ */
+
+template<typename T>
+struct Pref {};
+
+template<typename T>
+struct PrefBase
+{
+ const char *path;
+ T t, def;
+ std::unique_ptr<Preferences::PreferencesObserver> obs;
+ std::function<void()> action;
+ operator T() const {return t;}
+ PrefBase(const char *path, T def) : path(path), def(def) {}
+ void act() {if (action) action();}
+ void enable() {t = static_cast<Pref<T>*>(this)->read(); act(); obs = Inkscape::Preferences::get()->createObserver(path, [this] (const Preferences::Entry &e) {t = static_cast<Pref<T>*>(this)->changed(e); act();});}
+ void disable() {t = def; act(); obs.reset();}
+ void set_enabled(bool enabled) {enabled ? enable() : disable();}
+};
+
+template<>
+struct Pref<bool> : PrefBase<bool>
+{
+ Pref(const char *path, bool def = false) : PrefBase(path, def) {enable();}
+ bool read() {return Inkscape::Preferences::get()->getBool(path, def);}
+ bool changed(const Preferences::Entry &e) {return e.getBool(def);}
+};
+
+template<>
+struct Pref<int> : PrefBase<int>
+{
+ int min, max;
+ Pref(const char *path, int def, int min, int max) : PrefBase(path, def), min(min), max(max) {enable();}
+ int read() {return Inkscape::Preferences::get()->getIntLimited(path, def, min, max);}
+ int changed(const Preferences::Entry &e) {return e.getIntLimited(def, min, max);}
+};
+
+template<>
+struct Pref<double> : PrefBase<double>
+{
+ double min, max;
+ Pref(const char *path, double def, double min, double max) : PrefBase(path, def), min(min), max(max) {enable();}
+ double read() {return Inkscape::Preferences::get()->getDoubleLimited(path, def, min, max);}
+ double changed(const Preferences::Entry &e) {return e.getDoubleLimited(def, min, max);}
+};
+
+struct Prefs
+{
+ // Original parameters
+ Pref<int> tile_size = Pref<int> ("/options/rendering/tile-size", 16, 1, 10000);
+ Pref<int> tile_multiplier = Pref<int> ("/options/rendering/tile-multiplier", 16, 1, 512);
+ Pref<int> x_ray_radius = Pref<int> ("/options/rendering/xray-radius", 100, 1, 1500);
+ Pref<bool> from_display = Pref<bool> ("/options/displayprofile/from_display");
+ Pref<int> grabsize = Pref<int> ("/options/grabsize/value", 3, 1, 15);
+ Pref<int> outline_overlay_opacity = Pref<int> ("/options/rendering/outline-overlay-opacity", 50, 1, 100);
+
+ // New parameters
+ Pref<int> update_strategy = Pref<int> ("/options/rendering/update_strategy", 3, 1, 3);
+ Pref<int> render_time_limit = Pref<int> ("/options/rendering/render_time_limit", 1000, 100, 1000000);
+ Pref<bool> use_new_bisector = Pref<bool> ("/options/rendering/use_new_bisector", true);
+ Pref<int> new_bisector_size = Pref<int> ("/options/rendering/new_bisector_size", 500, 1, 10000);
+ Pref<double> max_affine_diff = Pref<double>("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0);
+ Pref<int> pad = Pref<int> ("/options/rendering/pad", 200, 0, 1000);
+ Pref<int> coarsener_min_size = Pref<int> ("/options/rendering/coarsener_min_size", 200, 0, 1000);
+ Pref<int> coarsener_glue_size = Pref<int> ("/options/rendering/coarsener_glue_size", 80, 0, 1000);
+ Pref<double> coarsener_min_fullness = Pref<double>("/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0);
+
+ // Debug switches
+ Pref<bool> debug_framecheck = Pref<bool> ("/options/rendering/debug_framecheck");
+ Pref<bool> debug_logging = Pref<bool> ("/options/rendering/debug_logging");
+ Pref<bool> debug_slow_redraw = Pref<bool> ("/options/rendering/debug_slow_redraw");
+ Pref<int> debug_slow_redraw_time = Pref<int> ("/options/rendering/debug_slow_redraw_time", 50, 0, 1000000);
+ Pref<bool> debug_show_redraw = Pref<bool> ("/options/rendering/debug_show_redraw");
+ Pref<bool> debug_show_unclean = Pref<bool> ("/options/rendering/debug_show_unclean");
+ Pref<bool> debug_show_snapshot = Pref<bool> ("/options/rendering/debug_show_snapshot");
+ Pref<bool> debug_show_clean = Pref<bool> ("/options/rendering/debug_show_clean");
+ Pref<bool> debug_disable_redraw = Pref<bool> ("/options/rendering/debug_disable_redraw");
+ Pref<bool> debug_sticky_decoupled = Pref<bool> ("/options/rendering/debug_sticky_decoupled");
+
+ // Developer mode
+ Pref<bool> devmode = Pref<bool>("/options/rendering/devmode");
+ void set_devmode(bool on);
+};
+
+void Prefs::set_devmode(bool on)
+{
+ tile_size.set_enabled(on);
+ render_time_limit.set_enabled(on);
+ use_new_bisector.set_enabled(on);
+ new_bisector_size.set_enabled(on);
+ max_affine_diff.set_enabled(on);
+ pad.set_enabled(on);
+ coarsener_min_size.set_enabled(on);
+ coarsener_glue_size.set_enabled(on);
+ coarsener_min_fullness.set_enabled(on);
+ debug_framecheck.set_enabled(on);
+ debug_logging.set_enabled(on);
+ debug_slow_redraw.set_enabled(on);
+ debug_slow_redraw_time.set_enabled(on);
+ debug_show_redraw.set_enabled(on);
+ debug_show_unclean.set_enabled(on);
+ debug_show_snapshot.set_enabled(on);
+ debug_show_clean.set_enabled(on);
+ debug_disable_redraw.set_enabled(on);
+ debug_sticky_decoupled.set_enabled(on);
+}
+
+/*
+ * Conversion functions
+ */
+
+auto geom_to_cairo(Geom::IntRect rect)
+{
+ return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()};
+}
+
+auto cairo_to_geom(Cairo::RectangleInt rect)
+{
+ return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height);
+}
+
+auto geom_to_cairo(Geom::Affine affine)
+{
+ return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]);
+}
+
+auto geom_act(Geom::Affine a, Geom::IntPoint p)
+{
+ Geom::Point p2 = p;
+ p2 *= a;
+ return Geom::IntPoint(std::round(p2.x()), std::round(p2.y()));
+}
+
+void region_to_path(const Cairo::RefPtr<Cairo::Context> &cr, const Cairo::RefPtr<Cairo::Region> &reg)
+{
+ for (int i = 0; i < reg->get_num_rectangles(); i++) {
+ auto rect = reg->get_rectangle(i);
+ cr->rectangle(rect.x, rect.y, rect.width, rect.height);
+ }
+}
+
+/*
+ * Update strategy
+ */
+
+// A class hierarchy for controlling what order to update invalidated regions.
+class Updater
+{
+public:
+ // The subregion of the store with up-to-date content.
+ Cairo::RefPtr<Cairo::Region> clean_region;
+
+ Updater(Cairo::RefPtr<Cairo::Region> clean_region) : clean_region(std::move(clean_region)) {}
+
+ virtual void reset() {clean_region = Cairo::Region::create();} // Reset the clean region to empty.
+ virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position; clip everything to the new store rectangle.
+ virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract (geom_to_cairo(rect));} // Called on every invalidate event.
+ virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union (geom_to_cairo(rect));} // Called on every rectangle redrawn.
+
+ virtual Cairo::RefPtr<Cairo::Region> get_next_clean_region() {return clean_region;}; // Called by on_idle to determine what regions to consider clean for the current redraw.
+ virtual bool report_finished () {return false;} // Called in on_idle if the redraw has finished. Returns true to indicate that further redraws are required with a different clean region.
+ virtual void frame () {} // Called by on_draw to notify the updater of the display of the frame.
+ virtual ~Updater() = default;
+};
+
+// Responsive updater: As soon as a region is invalidated, redraw it.
+using ResponsiveUpdater = Updater;
+
+// Full redraw updater: When a region is invalidated, delay redraw until after the current redraw is completed.
+class FullredrawUpdater : public Updater
+{
+ // Whether we are currently in the middle of a redraw.
+ bool inprogress = false;
+
+ // Contains a copy of the old clean region if damage events occurred during the current redraw, otherwise null.
+ Cairo::RefPtr<Cairo::Region> old_clean_region;
+
+public:
+
+ FullredrawUpdater(Cairo::RefPtr<Cairo::Region> clean_region) : Updater(std::move(clean_region)) {}
+
+ void reset() override
+ {
+ Updater::reset();
+ inprogress = false;
+ old_clean_region.clear();
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ Updater::intersect(rect);
+ if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect));
+ }
+
+ void mark_dirty(const Geom::IntRect &rect) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ Updater::mark_dirty(rect);
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ Updater::mark_clean(rect);
+ if (old_clean_region) old_clean_region->do_union(geom_to_cairo(rect));
+ }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override
+ {
+ inprogress = true;
+ if (!old_clean_region) {
+ return clean_region;
+ } else {
+ return old_clean_region;
+ }
+ }
+
+ bool report_finished() override
+ {
+ assert(inprogress);
+ if (!old_clean_region) {
+ // Completed redraw without being damaged => finished.
+ inprogress = false;
+ return false;
+ } else {
+ // Completed redraw but damage events arrived => ask for another redraw, using the up-to-date clean region.
+ old_clean_region.clear();
+ return true;
+ }
+ }
+};
+
+// Multiscale updater: Updates tiles near the mouse faster. Gives the best of both.
+class MultiscaleUpdater : public Updater
+{
+ // Whether we are currently in the middle of a redraw.
+ bool inprogress = false;
+
+ // Whether damage events occurred during the current redraw.
+ bool activated = false;
+
+ int counter; // A steadily incrementing counter from which the current scale is derived.
+ int scale; // The current scale to process updates at.
+ int elapsed; // How much time has been spent at the current scale.
+ std::vector<Cairo::RefPtr<Cairo::Region>> blocked; // The region blocked from being updated at each scale.
+
+public:
+
+ MultiscaleUpdater(Cairo::RefPtr<Cairo::Region> clean_region) : Updater(std::move(clean_region)) {}
+
+ void reset() override
+ {
+ Updater::reset();
+ inprogress = activated = false;
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ Updater::intersect(rect);
+ if (activated) {
+ for (auto &reg : blocked) {
+ reg->intersect(geom_to_cairo(rect));
+ }
+ }
+ }
+
+ void mark_dirty(const Geom::IntRect &rect) override
+ {
+ Updater::mark_dirty(rect);
+ if (inprogress && !activated) {
+ counter = scale = elapsed = 0;
+ blocked = {Cairo::Region::create()};
+ activated = true;
+ }
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ Updater::mark_clean(rect);
+ if (activated) blocked[scale]->do_union(geom_to_cairo(rect));
+ }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override
+ {
+ inprogress = true;
+ if (!activated) {
+ return clean_region;
+ } else {
+ auto result = clean_region->copy();
+ result->do_union(blocked[scale]);
+ return result;
+ }
+ }
+
+ bool report_finished() override
+ {
+ assert(inprogress);
+ if (!activated) {
+ // Completed redraw without damage => finished.
+ inprogress = false;
+ return false;
+ } else {
+ // Completed redraw but damage events arrived => begin updating any remaining damaged regions.
+ activated = false;
+ blocked.clear();
+ return true;
+ }
+ }
+
+ void frame() override
+ {
+ if (!activated) return;
+
+ // Stay at the current scale for 2^scale frames.
+ elapsed++;
+ if (elapsed < (1 << scale)) return;
+ elapsed = 0;
+
+ // Adjust the counter, which causes scale to hop around the values 0, 1, 2... spending half as much time at each subsequent scale.
+ counter++;
+ scale = 0;
+ for (int tmp = counter; tmp % 2 == 1; tmp /= 2) {
+ scale++;
+ }
+
+ // Ensure sufficiently many blocked zones exist.
+ if (scale == blocked.size()) {
+ blocked.emplace_back();
+ }
+
+ // Recreate the current blocked zone as the union of the clean region and lower-scale blocked zones.
+ blocked[scale] = clean_region->copy();
+ for (int i = 0; i < scale; i++) {
+ blocked[scale]->do_union(blocked[i]);
+ }
+ }
+};
+
+std::unique_ptr<Updater>
+make_updater(int type, Cairo::RefPtr<Cairo::Region> clean_region = Cairo::Region::create())
+{
+ switch (type) {
+ case 1: return std::make_unique<ResponsiveUpdater>(std::move(clean_region));
+ case 2: return std::make_unique<FullredrawUpdater>(std::move(clean_region));
+ default:
+ case 3: return std::make_unique<MultiscaleUpdater>(std::move(clean_region));
+ }
+}
+
+/*
+ * Implementation class
+ */
+
+class CanvasPrivate
+{
+public:
+
+ friend class Canvas;
+ Canvas *q;
+ CanvasPrivate(Canvas *q) : q(q) {}
+
+ // Lifecycle
+ bool active = false;
+ void update_active();
+ void activate();
+ void deactivate();
+
+ // Preferences
+ Prefs prefs;
+
+ // Update strategy; tracks the unclean region and decides how to redraw it.
+ std::unique_ptr<Updater> updater;
+
+ // Event processor. Events that interact with the Canvas are buffered here until the start of the next frame. They are processed by a separate object so that deleting the Canvas mid-event can be done safely.
+ struct EventProcessor
+ {
+ std::vector<GdkEventUniqPtr> events;
+ int pos;
+ GdkEvent *ignore = nullptr;
+ CanvasPrivate *canvasprivate; // Nulled on destruction.
+ bool in_processing = false; // For handling recursion due to nested GTK main loops.
+ void process();
+ int gobble_key_events(guint keyval, guint mask);
+ void gobble_motion_events(guint mask);
+ };
+ std::shared_ptr<EventProcessor> eventprocessor; // Usually held by CanvasPrivate, but temporarily also held by itself while processing so that it is not deleted mid-event.
+ bool add_to_bucket(GdkEvent*);
+ bool process_bucketed_event(const GdkEvent&);
+ bool pick_current_item(const GdkEvent&);
+ bool emit_event(const GdkEvent&);
+ Inkscape::CanvasItem *pre_scroll_grabbed_item;
+
+ // State for determining when to run event processor.
+ bool pending_draw = false;
+ sigc::connection bucket_emptier;
+ std::optional<guint> bucket_emptier_tick_callback;
+ void schedule_bucket_emptier();
+
+ // Idle system. The high priority idle ensures at least one idle cycle between add_idle and on_draw.
+ void add_idle();
+ sigc::connection hipri_idle;
+ sigc::connection lopri_idle;
+ bool on_hipri_idle();
+ bool on_lopri_idle();
+ bool idle_running = false;
+
+ // Important global properties of all the stores. If these change, all the stores must be recreated.
+ int _device_scale = 1;
+ bool _store_solid_background;
+
+ // The backing store.
+ Geom::IntRect _store_rect;
+ Geom::Affine _store_affine;
+ Cairo::RefPtr<Cairo::ImageSurface> _backing_store, _outline_store;
+
+ // The snapshot store. Used to mask redraw delay on zoom/rotate.
+ Geom::IntRect _snapshot_rect;
+ Geom::Affine _snapshot_affine;
+ Geom::IntPoint _snapshot_static_offset = {0, 0};
+ Cairo::RefPtr<Cairo::ImageSurface> _snapshot_store, _snapshot_outline_store;
+ Cairo::RefPtr<Cairo::Region> _snapshot_clean_region;
+
+ Geom::Affine geom_affine; // The affine the geometry was last imbued with.
+ bool decoupled_mode = false;
+
+ bool solid_background; // Whether the last background set is solid.
+ bool need_outline_store() const {return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY;}
+
+ // Drawing
+ bool on_idle();
+ void paint_rect_internal(Geom::IntRect const &rect);
+ void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr<Cairo::ImageSurface> const &store, bool is_backing_store, bool outline_overlay_pass);
+ std::optional<Geom::Dim2> old_bisector(const Geom::IntRect &rect);
+ std::optional<Geom::Dim2> new_bisector(const Geom::IntRect &rect);
+
+ // Trivial overload of GtkWidget function.
+ void queue_draw_area(Geom::IntRect &rect);
+
+ // For tracking the last known mouse position. (The function Gdk::Window::get_device_position cannot be used because of slow X11 round-trips. Remove this workaround when X11 dies.)
+ std::optional<Geom::Point> last_mouse;
+};
+
+/*
+ * Lifecycle
+ */
+
+Canvas::Canvas()
+ : d(std::make_unique<CanvasPrivate>(this))
+{
+ set_name("InkscapeCanvas");
+
+ // Events
+ add_events(Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK |
+ Gdk::ENTER_NOTIFY_MASK |
+ Gdk::LEAVE_NOTIFY_MASK |
+ Gdk::FOCUS_CHANGE_MASK |
+ Gdk::KEY_PRESS_MASK |
+ Gdk::KEY_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK |
+ Gdk::SCROLL_MASK |
+ Gdk::SMOOTH_SCROLL_MASK );
+
+ // Set up EventProcessor
+ d->eventprocessor = std::make_shared<CanvasPrivate::EventProcessor>();
+ d->eventprocessor->canvasprivate = d.get();
+
+ // Updater
+ d->updater = make_updater(d->prefs.update_strategy);
+
+ // Preferences
+ d->prefs.grabsize.action = [=] {_canvas_item_root->update_canvas_item_ctrl_sizes(d->prefs.grabsize);};
+ d->prefs.debug_show_unclean.action = [=] {queue_draw();};
+ d->prefs.debug_show_clean.action = [=] {queue_draw();};
+ d->prefs.debug_disable_redraw.action = [=] {d->add_idle();};
+ d->prefs.debug_sticky_decoupled.action = [=] {d->add_idle();};
+ d->prefs.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, std::move(d->updater->clean_region));};
+ d->prefs.outline_overlay_opacity.action = [=] {queue_draw();};
+
+ // Developer mode master switch
+ d->prefs.devmode.action = [=] {d->prefs.set_devmode(d->prefs.devmode);};
+ d->prefs.devmode.action();
+
+ // Cavas item root
+ _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr);
+ _canvas_item_root->set_name("CanvasItemGroup:Root");
+ _canvas_item_root->set_canvas(this);
+
+ // Background
+ _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0);
+ d->solid_background = true;
+}
+
+void CanvasPrivate::activate()
+{
+ // Event handling/item picking
+ q->_pick_event.type = GDK_LEAVE_NOTIFY;
+ q->_pick_event.crossing.x = 0;
+ q->_pick_event.crossing.y = 0;
+
+ q->_in_repick = false;
+ q->_left_grabbed_item = false;
+ q->_all_enter_events = false;
+ q->_is_dragging = false;
+ q->_state = 0;
+
+ q->_current_canvas_item = nullptr;
+ q->_current_canvas_item_new = nullptr;
+ q->_grabbed_canvas_item = nullptr;
+ q->_grabbed_event_mask = (Gdk::EventMask)0;
+ pre_scroll_grabbed_item = nullptr;
+
+ // Drawing
+ q->_drawing_disabled = false;
+ q->_need_update = true;
+
+ // Split view
+ q->_split_direction = Inkscape::SplitDirection::EAST;
+ q->_split_position = {-1, -1}; // initialize with off-canvas coordinates
+ q->_hover_direction = Inkscape::SplitDirection::NONE;
+ q->_split_dragging = false;
+
+ add_idle();
+}
+
+void CanvasPrivate::deactivate()
+{
+ // Disconnect signals and timeouts. (Note: They will never be rescheduled while inactive.)
+ hipri_idle.disconnect();
+ lopri_idle.disconnect();
+ bucket_emptier.disconnect();
+ if (bucket_emptier_tick_callback) q->remove_tick_callback(*bucket_emptier_tick_callback);
+}
+
+Canvas::~Canvas()
+{
+ // Not necessary as GTK guarantees realization is always followed by unrealization. But just in case that invariant breaks, we deal with it.
+ if (d->active) {
+ std::cerr << "Canvas destructed while realized!" << std::endl;
+ d->deactivate();
+ }
+
+ // Disconnect from EventProcessor.
+ d->eventprocessor->canvasprivate = nullptr;
+
+ // Remove entire CanvasItem tree.
+ delete _canvas_item_root;
+}
+
+void CanvasPrivate::update_active()
+{
+ bool new_active = q->_drawing && q->get_realized();
+ if (new_active != active) {
+ active = new_active;
+ active ? activate() : deactivate();
+ }
+}
+
+void Canvas::set_drawing(Drawing *drawing)
+{
+ _drawing = drawing;
+ d->update_active();
+}
+
+void
+Canvas::on_realize()
+{
+ parent_type::on_realize();
+ assert(get_realized());
+ d->update_active();
+}
+
+void Canvas::on_unrealize()
+{
+ parent_type::on_unrealize();
+ assert(!get_realized());
+ d->update_active();
+}
+
+/*
+ * Events system
+ */
+
+// The following protected functions of Canvas are where all incoming events initially arrive.
+// Those that do not interact with the Canvas are processed instantaneously, while the rest are
+// delayed by placing them into the bucket.
+
+bool
+Canvas::on_scroll_event(GdkEventScroll *scroll_event)
+{
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(scroll_event));
+}
+
+bool
+Canvas::on_button_press_event(GdkEventButton *button_event)
+{
+ return on_button_event(button_event);
+}
+
+bool
+Canvas::on_button_release_event(GdkEventButton *button_event)
+{
+ return on_button_event(button_event);
+}
+
+// Unified handler for press and release events.
+bool
+Canvas::on_button_event(GdkEventButton *button_event)
+{
+ // Sanity-check event type.
+ switch (button_event->type) {
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ break; // Good
+ default:
+ std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl;
+ return false;
+ }
+
+ // Drag the split view controller.
+ switch (button_event->type) {
+ case GDK_BUTTON_PRESS:
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ _split_dragging = true;
+ _split_drag_start = Geom::Point(button_event->x, button_event->y);
+ return true;
+ }
+ break;
+ case GDK_2BUTTON_PRESS:
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ _split_direction = _hover_direction;
+ _split_dragging = false;
+ queue_draw();
+ return true;
+ }
+ break;
+ case GDK_BUTTON_RELEASE:
+ _split_dragging = false;
+ break;
+ }
+
+ // Otherwise, handle as a delayed event.
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(button_event));
+}
+
+bool
+Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event)
+{
+ if (crossing_event->window != get_window()->gobj()) {
+ return false;
+ }
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(crossing_event));
+}
+
+bool
+Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event)
+{
+ if (crossing_event->window != get_window()->gobj()) {
+ return false;
+ }
+ d->last_mouse = {};
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(crossing_event));
+}
+
+bool
+Canvas::on_focus_in_event(GdkEventFocus *focus_event)
+{
+ grab_focus();
+ return false;
+}
+
+bool
+Canvas::on_key_press_event(GdkEventKey *key_event)
+{
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool
+Canvas::on_key_release_event(GdkEventKey *key_event)
+{
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool
+Canvas::on_motion_notify_event(GdkEventMotion *motion_event)
+{
+ // Record the last mouse position.
+ d->last_mouse = Geom::Point(motion_event->x, motion_event->y);
+
+ // Handle interactions with the split view controller.
+ Geom::IntPoint cursor_position = Geom::IntPoint(motion_event->x, motion_event->y);
+
+ // Check if we are near the edge. If so, revert to normal mode.
+ if (_split_mode == Inkscape::SplitMode::SPLIT && _split_dragging) {
+ if (cursor_position.x() < 5 ||
+ cursor_position.y() < 5 ||
+ cursor_position.x() - get_allocation().get_width() > -5 ||
+ cursor_position.y() - get_allocation().get_height() > -5 ) {
+
+ // Reset everything.
+ _split_mode = Inkscape::SplitMode::NORMAL;
+ _split_position = Geom::Point(-1, -1);
+ _hover_direction = Inkscape::SplitDirection::NONE;
+ set_cursor();
+ queue_draw();
+
+ // Update action (turn into utility function?).
+ auto window = dynamic_cast<Gtk::ApplicationWindow *>(get_toplevel());
+ if (!window) {
+ std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl;
+ return true;
+ }
+
+ auto action = window->lookup_action("canvas-split-mode");
+ if (!action) {
+ std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl;
+ return true;
+ }
+
+ auto saction = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(action);
+ if (!saction) {
+ std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl;
+ return true;
+ }
+
+ saction->change_state((int)Inkscape::SplitMode::NORMAL);
+
+ return true;
+ }
+ }
+
+ if (_split_mode == Inkscape::SplitMode::XRAY) {
+ _split_position = cursor_position;
+ queue_draw();
+ }
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ Inkscape::SplitDirection hover_direction = Inkscape::SplitDirection::NONE;
+ Geom::Point difference(cursor_position - _split_position);
+
+ // Move controller
+ if (_split_dragging) {
+ Geom::Point delta = cursor_position - _split_drag_start; // We don't use _split_position
+ if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) {
+ _split_position += Geom::Point(0, delta.y());
+ } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) {
+ _split_position += Geom::Point(delta.x(), 0);
+ } else {
+ _split_position += delta;
+ }
+ _split_drag_start = cursor_position;
+ queue_draw();
+ return true;
+ }
+
+ if (Geom::distance(cursor_position, _split_position) < 20 * d->_device_scale) {
+ // We're hovering over circle, figure out which direction we are in.
+ if (difference.y() - difference.x() > 0) {
+ if (difference.y() + difference.x() > 0) {
+ hover_direction = Inkscape::SplitDirection::SOUTH;
+ } else {
+ hover_direction = Inkscape::SplitDirection::WEST;
+ }
+ } else {
+ if (difference.y() + difference.x() > 0) {
+ hover_direction = Inkscape::SplitDirection::EAST;
+ } else {
+ hover_direction = Inkscape::SplitDirection::NORTH;
+ }
+ }
+ } else if (_split_direction == Inkscape::SplitDirection::NORTH ||
+ _split_direction == Inkscape::SplitDirection::SOUTH) {
+ if (std::abs(difference.y()) < 3 * d->_device_scale) {
+ // We're hovering over horizontal line
+ hover_direction = Inkscape::SplitDirection::HORIZONTAL;
+ }
+ } else {
+ if (std::abs(difference.x()) < 3 * d->_device_scale) {
+ // We're hovering over vertical line
+ hover_direction = Inkscape::SplitDirection::VERTICAL;
+ }
+ }
+
+ if (_hover_direction != hover_direction) {
+ _hover_direction = hover_direction;
+ set_cursor();
+ queue_draw();
+ }
+
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ // We're hovering, don't pick or emit event.
+ return true;
+ }
+ }
+
+ // Otherwise, handle as a delayed event.
+ return d->add_to_bucket(reinterpret_cast<GdkEvent*>(motion_event));
+}
+
+// Most events end up here. We store them in the bucket, and process them as soon as possible after
+// the next 'on_draw'. If 'on_draw' isn't pending, we use the 'tick_callback' signal to process them
+// when 'on_draw' would have run anyway. If 'on_draw' later becomes pending, we remove this signal.
+
+// Add an event to the bucket and ensure it will be emptied in the near future.
+bool
+CanvasPrivate::add_to_bucket(GdkEvent *event)
+{
+ framecheck_whole_function(this)
+
+ if (!active) {
+ std::cerr << "Canvas::add_to_bucket: Called while not active!" << std::endl;
+ return false;
+ }
+
+ // Prevent re-fired events from going through again.
+ if (event == eventprocessor->ignore) {
+ return false;
+ }
+
+ // If this is the first event, ensure event processing will run on the main loop as soon as possible after the next frame has started.
+ if (eventprocessor->events.empty() && !pending_draw) {
+#ifndef NDEBUG
+ if (bucket_emptier_tick_callback) {
+ g_warning("bucket_emptier_tick_callback not empty");
+ }
+#endif
+ bucket_emptier_tick_callback = q->add_tick_callback([this] (const Glib::RefPtr<Gdk::FrameClock>&) {
+ assert(active);
+ bucket_emptier_tick_callback.reset();
+ schedule_bucket_emptier();
+ return false;
+ });
+ }
+
+ // Add a copy to the queue.
+ eventprocessor->events.emplace_back(gdk_event_copy(event));
+
+ // Tell GTK the event was handled.
+ return true;
+}
+
+void CanvasPrivate::schedule_bucket_emptier()
+{
+ if (!active) {
+ std::cerr << "Canvas::schedule_bucket_emptier: Called while not active!" << std::endl;
+ return;
+ }
+
+ if (!bucket_emptier.connected()) {
+ bucket_emptier = Glib::signal_idle().connect([this] {
+ assert(active);
+ eventprocessor->process();
+ return false;
+ }, G_PRIORITY_HIGH_IDLE + 14); // before hipri_idle
+ }
+}
+
+// The following functions run at the start of the next frame on the GTK main loop.
+// (Note: It is crucial that it runs on the main loop and not in any frame clock tick callbacks. GTK does not allow widgets to be deleted in the latter; only the former.)
+
+// Process bucketed events.
+void
+CanvasPrivate::EventProcessor::process()
+{
+ framecheck_whole_function(canvasprivate)
+
+ // Ensure the EventProcessor continues to live even if the Canvas is destroyed during event processing.
+ auto self = canvasprivate->eventprocessor;
+
+ // Check if toplevel or recursive. (Recursive calls happen if processing an event starts its own nested GTK main loop.)
+ bool toplevel = !in_processing;
+ in_processing = true;
+
+ // If toplevel, initialise the iteration index. It may be incremented externally by gobblers or recursive calls.
+ if (toplevel) {
+ pos = 0;
+ }
+
+ while (pos < events.size()) {
+ // Extract next event.
+ auto event = std::move(events[pos]);
+ pos++;
+
+ // Fire the event at the CanvasItems and see if it was handled.
+ bool handled = canvasprivate->process_bucketed_event(*event);
+
+ if (!handled) {
+ // Re-fire the event at the window, and ignore it when it comes back here again.
+ ignore = event.get();
+ canvasprivate->q->get_toplevel()->event(event.get());
+ ignore = nullptr;
+ }
+
+ // If the Canvas was destroyed or deactivated during event processing, exit now.
+ if (!canvasprivate || !canvasprivate->active) return;
+ }
+
+ // Otherwise, clear the list of events that was just processed.
+ events.clear();
+
+ // Reset the variable to track recursive calls.
+ if (toplevel) {
+ in_processing = false;
+ }
+}
+
+// Called during event processing by some tools to batch backlogs of key events that may have built up after a freeze.
+int
+Canvas::gobble_key_events(guint keyval, guint mask)
+{
+ return d->eventprocessor->gobble_key_events(keyval, mask);
+}
+
+int
+CanvasPrivate::EventProcessor::gobble_key_events(guint keyval, guint mask)
+{
+ int count = 0;
+
+ while (pos < events.size()) {
+ auto &event = events[pos];
+ if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) && event->key.keyval == keyval && (!mask || (event->key.state & mask))) {
+ // Discard event and continue.
+ if (event->type == GDK_KEY_PRESS) count++;
+ pos++;
+ }
+ else {
+ // Stop discarding.
+ break;
+ }
+ }
+
+ if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " key press(es)" << std::endl;
+
+ return count;
+}
+
+// Called during event processing by some tools to ignore backlogs of motion events that may have built up after a freeze.
+void
+Canvas::gobble_motion_events(guint mask)
+{
+ d->eventprocessor->gobble_motion_events(mask);
+}
+
+void
+CanvasPrivate::EventProcessor::gobble_motion_events(guint mask)
+{
+ int count = 0;
+
+ while (pos < events.size()) {
+ auto &event = events[pos];
+ if (event->type == GDK_MOTION_NOTIFY && (event->motion.state & mask)) {
+ // Discard event and continue.
+ count++;
+ pos++;
+ }
+ else {
+ // Stop discarding.
+ break;
+ }
+ }
+
+ if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " motion event(s)" << std::endl;
+}
+
+// From now on Inkscape's regular event processing logic takes place. The only thing to remember is that
+// all of this happens at a slight delay after the original GTK events. Therefore, it's important to make
+// sure that stateful variables like '_current_canvas_item' and friends are ONLY read/written within these
+// functions, not during the earlier GTK event handlers. Otherwise state confusion will ensue.
+
+bool
+CanvasPrivate::process_bucketed_event(const GdkEvent &event)
+{
+ auto calc_button_mask = [&] () -> int {
+ switch (event.button.button) {
+ case 1: return GDK_BUTTON1_MASK; break;
+ case 2: return GDK_BUTTON2_MASK; break;
+ case 3: return GDK_BUTTON3_MASK; break;
+ case 4: return GDK_BUTTON4_MASK; break;
+ case 5: return GDK_BUTTON5_MASK; break;
+ default: return 0; // Buttons can range at least to 9 but mask defined only to 5.
+ }
+ };
+
+ // Do event-specific processing.
+ switch (event.type) {
+
+ case GDK_SCROLL:
+ {
+ // Save the current event-receiving item just before scrolling starts. It will continue to receive scroll events until the mouse is moved.
+ if (!pre_scroll_grabbed_item) {
+ pre_scroll_grabbed_item = q->_current_canvas_item;
+ if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) {
+ pre_scroll_grabbed_item = q->_grabbed_canvas_item;
+ }
+ }
+
+ // Process the scroll event...
+ bool retval = emit_event(event);
+
+ // ...then repick.
+ q->_state = event.scroll.state;
+ pick_current_item(event);
+
+ return retval;
+ }
+
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ {
+ pre_scroll_grabbed_item = nullptr;
+
+ // Pick the current item as if the button were not pressed...
+ q->_state = event.button.state;
+ pick_current_item(event);
+
+ // ...then process the event.
+ q->_state ^= calc_button_mask();
+ bool retval = emit_event(event);
+
+ return retval;
+ }
+
+ case GDK_BUTTON_RELEASE:
+ {
+ pre_scroll_grabbed_item = nullptr;
+
+ // Process the event as if the button were pressed...
+ q->_state = event.button.state;
+ bool retval = emit_event(event);
+
+ // ...then repick after the button has been released.
+ auto event_copy = make_unique_copy(event);
+ event_copy->button.state ^= calc_button_mask();
+ q->_state = event_copy->button.state;
+ pick_current_item(*event_copy);
+
+ return retval;
+ }
+
+ case GDK_ENTER_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event.crossing.state;
+ return pick_current_item(event);
+
+ case GDK_LEAVE_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event.crossing.state;
+ // This is needed to remove alignment or distribution snap indicators.
+ if (q->_desktop) {
+ q->_desktop->snapindicator->remove_snaptarget();
+ }
+ return pick_current_item(event);
+
+ case GDK_KEY_PRESS:
+ case GDK_KEY_RELEASE:
+ return emit_event(event);
+
+ case GDK_MOTION_NOTIFY:
+ pre_scroll_grabbed_item = nullptr;
+ q->_state = event.motion.state;
+ pick_current_item(event);
+ return emit_event(event);
+
+ default:
+ return false;
+ }
+}
+
+// This function is called by 'process_bucketed_event' to manipulate the state variables relating
+// to the current object under the mouse, for example, to generate enter and leave events.
+// (A more detailed explanation by Tavmjong follows.)
+// --------
+// This routine reacts to events from the canvas. It's main purpose is to find the canvas item
+// closest to the cursor where the event occurred and then send the event (sometimes modified) to
+// that item. The event then bubbles up the canvas item tree until an object handles it. If the
+// widget is redrawn, this routine may be called again for the same event.
+//
+// Canvas items register their interest by connecting to the "event" signal.
+// Example in desktop.cpp:
+// canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this));
+bool
+CanvasPrivate::pick_current_item(const GdkEvent &event)
+{
+ // Ensure requested geometry updates are performed first.
+ if (q->_need_update) {
+ q->_canvas_item_root->update(geom_affine);
+ q->_need_update = false;
+ }
+
+ int button_down = 0;
+ if (!q->_all_enter_events) {
+ // Only set true in connector-tool.cpp.
+
+ // If a button is down, we'll perform enter and leave events on the
+ // current item, but not enter on any other item. This is more or
+ // less like X pointer grabbing for canvas items.
+ button_down = q->_state & (GDK_BUTTON1_MASK |
+ GDK_BUTTON2_MASK |
+ GDK_BUTTON3_MASK |
+ GDK_BUTTON4_MASK |
+ GDK_BUTTON5_MASK);
+ if (!button_down) q->_left_grabbed_item = false;
+ }
+
+ // Save the event in the canvas. This is used to synthesize enter and
+ // leave events in case the current item changes. It is also used to
+ // re-pick the current item if the current one gets deleted. Also,
+ // synthesize an enter event.
+ if (&event != &q->_pick_event) {
+ if (event.type == GDK_MOTION_NOTIFY || event.type == GDK_SCROLL || event.type == GDK_BUTTON_RELEASE) {
+ // Convert to GDK_ENTER_NOTIFY
+
+ // These fields have the same offsets in all types of events.
+ q->_pick_event.crossing.type = GDK_ENTER_NOTIFY;
+ q->_pick_event.crossing.window = event.motion.window;
+ q->_pick_event.crossing.send_event = event.motion.send_event;
+ q->_pick_event.crossing.subwindow = nullptr;
+ q->_pick_event.crossing.x = event.motion.x;
+ q->_pick_event.crossing.y = event.motion.y;
+ q->_pick_event.crossing.mode = GDK_CROSSING_NORMAL;
+ q->_pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR;
+ q->_pick_event.crossing.focus = false;
+
+ // These fields don't have the same offsets in all types of events.
+ // (Todo: With C++20, can reduce the code repetition here using a templated lambda.)
+ switch (event.type)
+ {
+ case GDK_MOTION_NOTIFY:
+ q->_pick_event.crossing.state = event.motion.state;
+ q->_pick_event.crossing.x_root = event.motion.x_root;
+ q->_pick_event.crossing.y_root = event.motion.y_root;
+ break;
+ case GDK_SCROLL:
+ q->_pick_event.crossing.state = event.scroll.state;
+ q->_pick_event.crossing.x_root = event.scroll.x_root;
+ q->_pick_event.crossing.y_root = event.scroll.y_root;
+ break;
+ case GDK_BUTTON_RELEASE:
+ q->_pick_event.crossing.state = event.button.state;
+ q->_pick_event.crossing.x_root = event.button.x_root;
+ q->_pick_event.crossing.y_root = event.button.y_root;
+ break;
+ default:
+ assert(false);
+ }
+
+ } else {
+ q->_pick_event = event;
+ }
+ }
+
+ if (q->_in_repick) {
+ // Don't do anything else if this is a recursive call.
+ return false;
+ }
+
+ // Find new item
+ q->_current_canvas_item_new = nullptr;
+
+ if (q->_pick_event.type != GDK_LEAVE_NOTIFY && q->_canvas_item_root->is_visible()) {
+ // Leave notify means there is no current item.
+ // Find closest item.
+ double x = 0.0;
+ double y = 0.0;
+
+ if (q->_pick_event.type == GDK_ENTER_NOTIFY) {
+ x = q->_pick_event.crossing.x;
+ y = q->_pick_event.crossing.y;
+ } else {
+ x = q->_pick_event.motion.x;
+ y = q->_pick_event.motion.y;
+ }
+
+ // If in split mode, look at where cursor is to see if one should pick with outline mode.
+ if (q->_split_mode == Inkscape::SplitMode::SPLIT && q->_render_mode != Inkscape::RenderMode::OUTLINE_OVERLAY) {
+ if ((q->_split_direction == Inkscape::SplitDirection::NORTH && y > q->_split_position.y()) ||
+ (q->_split_direction == Inkscape::SplitDirection::SOUTH && y < q->_split_position.y()) ||
+ (q->_split_direction == Inkscape::SplitDirection::WEST && x > q->_split_position.x()) ||
+ (q->_split_direction == Inkscape::SplitDirection::EAST && x < q->_split_position.x()) ) {
+ q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE);
+ }
+ }
+ // Convert to world coordinates.
+ auto p = Geom::Point(x, y) + q->_pos;
+ if (decoupled_mode) {
+ p *= _store_affine * q->_affine.inverse();
+ }
+
+ q->_current_canvas_item_new = q->_canvas_item_root->pick_item(p);
+ // if (q->_current_canvas_item_new) {
+ // std::cout << " PICKING: FOUND ITEM: " << q->_current_canvas_item_new->get_name() << std::endl;
+ // } else {
+ // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl;
+ // }
+
+ // Reset the drawing back to the requested render mode.
+ q->_drawing->setRenderMode(q->_render_mode);
+ }
+
+ if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) {
+ // Current item did not change!
+ return false;
+ }
+
+ // Synthesize events for old and new current items.
+ bool retval = false;
+ if (q->_current_canvas_item_new != q->_current_canvas_item &&
+ q->_current_canvas_item != nullptr &&
+ !q->_left_grabbed_item ) {
+
+ GdkEvent new_event;
+ new_event = q->_pick_event;
+ new_event.type = GDK_LEAVE_NOTIFY;
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = nullptr;
+ q->_in_repick = true;
+ retval = emit_event(new_event);
+ q->_in_repick = false;
+ }
+
+ if (q->_all_enter_events == false) {
+ // new_current_item may have been set to nullptr during the call to emitEvent() above.
+ if (q->_current_canvas_item_new != q->_current_canvas_item && button_down) {
+ q->_left_grabbed_item = true;
+ return retval;
+ }
+ }
+
+ // Handle the rest of cases
+ q->_left_grabbed_item = false;
+ q->_current_canvas_item = q->_current_canvas_item_new;
+
+ if (q->_current_canvas_item != nullptr) {
+ GdkEvent new_event;
+ new_event = q->_pick_event;
+ new_event.type = GDK_ENTER_NOTIFY;
+ new_event.crossing.detail = GDK_NOTIFY_ANCESTOR;
+ new_event.crossing.subwindow = nullptr;
+ retval = emit_event(new_event);
+ }
+
+ return retval;
+}
+
+// Fires an event at the canvas, after a little pre-processing. Returns true if handled.
+bool
+CanvasPrivate::emit_event(const GdkEvent &event)
+{
+ // Handle grabbed items.
+ if (q->_grabbed_canvas_item) {
+ auto mask = (Gdk::EventMask)0;
+
+ switch (event.type) {
+ case GDK_ENTER_NOTIFY:
+ mask = Gdk::ENTER_NOTIFY_MASK;
+ break;
+ case GDK_LEAVE_NOTIFY:
+ mask = Gdk::LEAVE_NOTIFY_MASK;
+ break;
+ case GDK_MOTION_NOTIFY:
+ mask = Gdk::POINTER_MOTION_MASK;
+ break;
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ mask = Gdk::BUTTON_PRESS_MASK;
+ break;
+ case GDK_BUTTON_RELEASE:
+ mask = Gdk::BUTTON_RELEASE_MASK;
+ break;
+ case GDK_KEY_PRESS:
+ mask = Gdk::KEY_PRESS_MASK;
+ break;
+ case GDK_KEY_RELEASE:
+ mask = Gdk::KEY_RELEASE_MASK;
+ break;
+ case GDK_SCROLL:
+ mask = Gdk::SCROLL_MASK;
+ mask |= Gdk::SMOOTH_SCROLL_MASK;
+ break;
+ default:
+ break;
+ }
+
+ if (!(mask & q->_grabbed_event_mask)) {
+ return false;
+ }
+ }
+
+ // Convert to world coordinates. We have two different cases due to different event structures.
+ auto conv = [&, this] (double &x, double &y) {
+ auto p = Geom::Point(x, y) + q->_pos;
+ if (decoupled_mode) {
+ p *= _store_affine * q->_affine.inverse();
+ }
+ x = p.x();
+ y = p.y();
+ };
+
+ auto event_copy = make_unique_copy(event);
+
+ switch (event.type) {
+ case GDK_ENTER_NOTIFY:
+ case GDK_LEAVE_NOTIFY:
+ conv(event_copy->crossing.x, event_copy->crossing.y);
+ break;
+ case GDK_MOTION_NOTIFY:
+ case GDK_BUTTON_PRESS:
+ case GDK_2BUTTON_PRESS:
+ case GDK_3BUTTON_PRESS:
+ case GDK_BUTTON_RELEASE:
+ conv(event_copy->motion.x, event_copy->motion.y);
+ break;
+ default:
+ break;
+ }
+
+ // Block undo/redo while anything is dragged.
+ if (event.type == GDK_BUTTON_PRESS && event.button.button == 1) {
+ q->_is_dragging = true;
+ } else if (event.type == GDK_BUTTON_RELEASE) {
+ q->_is_dragging = false;
+ }
+
+ if (q->_current_canvas_item) {
+ // Choose where to send event.
+ auto item = q->_current_canvas_item;
+
+ if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) {
+ item = q->_grabbed_canvas_item;
+ }
+
+ if (pre_scroll_grabbed_item && event.type == GDK_SCROLL) {
+ item = pre_scroll_grabbed_item;
+ }
+
+ // Propagate the event up the canvas item hierarchy until handled.
+ while (item) {
+ if (item->handle_event(event_copy.get())) return true;
+ item = item->get_parent();
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Protected functions
+ */
+
+Geom::IntPoint
+Canvas::get_dimensions() const
+{
+ Gtk::Allocation allocation = get_allocation();
+ return {allocation.get_width(), allocation.get_height()};
+}
+
+/**
+ * Is world point inside canvas area?
+ */
+bool
+Canvas::world_point_inside_canvas(Geom::Point const &world) const
+{
+ return get_area_world().contains(world.floor());
+}
+
+/**
+ * Translate point in canvas to world coordinates.
+ */
+Geom::Point
+Canvas::canvas_to_world(Geom::Point const &point) const
+{
+ return point + _pos;
+}
+
+/**
+ * Return the area shown in the canvas in world coordinates.
+ */
+Geom::IntRect
+Canvas::get_area_world() const
+{
+ return Geom::IntRect(_pos, _pos + get_dimensions());
+}
+
+/**
+ * Set the affine for the canvas.
+ */
+void
+Canvas::set_affine(Geom::Affine const &affine)
+{
+ if (_affine == affine) {
+ return;
+ }
+
+ _affine = affine;
+
+ d->add_idle();
+ queue_draw();
+}
+
+void CanvasPrivate::queue_draw_area(Geom::IntRect &rect)
+{
+ q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height());
+}
+
+/**
+ * Invalidate drawing and redraw during idle.
+ */
+void
+Canvas::redraw_all()
+{
+ if (!d->active) {
+ // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed.
+ // We need to ignore their requests!
+ return;
+ }
+ d->updater->reset(); // Empty region (i.e. everything is dirty).
+ d->add_idle();
+ if (d->prefs.debug_show_unclean) queue_draw();
+}
+
+/**
+ * Redraw the given area during idle.
+ */
+void
+Canvas::redraw_area(int x0, int y0, int x1, int y1)
+{
+ if (!d->active) {
+ // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed.
+ // We need to ignore their requests!
+ return;
+ }
+
+ // Clamp area to Cairo's technically supported max size (-2^30..+2^30-1).
+ // This ensures that the rectangle dimensions don't overflow and wrap around.
+ constexpr int min_coord = -(1 << 30);
+ constexpr int max_coord = (1 << 30) - 1;
+
+ x0 = std::clamp(x0, min_coord, max_coord);
+ y0 = std::clamp(y0, min_coord, max_coord);
+ x1 = std::clamp(x1, min_coord, max_coord);
+ y1 = std::clamp(y1, min_coord, max_coord);
+
+ if (x0 >= x1 || y0 >= y1) {
+ return;
+ }
+
+ auto rect = Geom::IntRect::from_xywh(x0, y0, x1 - x0, y1 - y0);
+ d->updater->mark_dirty(rect);
+ d->add_idle();
+ if (d->prefs.debug_show_unclean) queue_draw();
+}
+
+void
+Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1)
+{
+ // Handle overflow during conversion gracefully.
+ // Round outward to make sure integral coordinates cover the entire area.
+ constexpr Geom::Coord min_int = std::numeric_limits<int>::min();
+ constexpr Geom::Coord max_int = std::numeric_limits<int>::max();
+
+ redraw_area(
+ (int)std::floor(std::clamp(x0, min_int, max_int)),
+ (int)std::floor(std::clamp(y0, min_int, max_int)),
+ (int)std::ceil (std::clamp(x1, min_int, max_int)),
+ (int)std::ceil (std::clamp(y1, min_int, max_int))
+ );
+}
+
+void
+Canvas::redraw_area(Geom::Rect &area)
+{
+ redraw_area(area.left(), area.top(), area.right(), area.bottom());
+}
+
+/**
+ * Redraw after changing canvas item geometry.
+ */
+void
+Canvas::request_update()
+{
+ // Flag geometry as needing update.
+ _need_update = true;
+
+ // Trigger the idle process to perform the update.
+ d->add_idle();
+}
+
+/**
+ * Scroll window so drawing point 'pos' is at upper left corner of canvas.
+ */
+void
+Canvas::set_pos(Geom::IntPoint const &pos)
+{
+ if (pos == _pos) {
+ return;
+ }
+
+ _pos = pos;
+
+ d->add_idle();
+ queue_draw();
+
+ if (auto grid = dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(get_parent())) {
+ grid->UpdateRulers();
+ }
+}
+
+/**
+ * Set canvas background color (display only).
+ */
+void
+Canvas::set_background_color(guint32 rgba)
+{
+ double r = SP_RGBA32_R_F(rgba);
+ double g = SP_RGBA32_G_F(rgba);
+ double b = SP_RGBA32_B_F(rgba);
+
+ _background = Cairo::SolidPattern::create_rgb(r, g, b);
+ d->solid_background = true;
+
+ redraw_all();
+}
+
+/**
+ * Set canvas background to a checkerboard pattern.
+ */
+void
+Canvas::set_background_checkerboard(guint32 rgba, bool use_alpha)
+{
+ auto pattern = ink_cairo_pattern_create_checkerboard(rgba, use_alpha);
+ _background = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(pattern));
+ d->solid_background = false;
+ redraw_all();
+}
+
+void Canvas::set_drawing_disabled(bool disable)
+{
+ _drawing_disabled = disable;
+ if (!disable) {
+ d->add_idle();
+ }
+}
+
+void
+Canvas::set_render_mode(Inkscape::RenderMode mode)
+{
+ if (_render_mode != mode) {
+ _render_mode = mode;
+ _drawing->setRenderMode(_render_mode);
+ redraw_all();
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void
+Canvas::set_color_mode(Inkscape::ColorMode mode)
+{
+ if (_color_mode != mode) {
+ _color_mode = mode;
+ redraw_all();
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void
+Canvas::set_split_mode(Inkscape::SplitMode mode)
+{
+ if (_split_mode != mode) {
+ _split_mode = mode;
+ redraw_all();
+ }
+}
+
+Cairo::RefPtr<Cairo::ImageSurface>
+Canvas::get_backing_store() const
+{
+ return d->_backing_store;
+}
+
+/**
+ * Clear current and grabbed items.
+ */
+void
+Canvas::canvas_item_destructed(Inkscape::CanvasItem* item)
+{
+ if (item == _current_canvas_item) {
+ _current_canvas_item = nullptr;
+ }
+
+ if (item == _current_canvas_item_new) {
+ _current_canvas_item_new = nullptr;
+ }
+
+ if (item == _grabbed_canvas_item) {
+ _grabbed_canvas_item = nullptr;
+ auto const display = Gdk::Display::get_default();
+ auto const seat = display->get_default_seat();
+ seat->ungrab();
+ }
+
+ if (item == d->pre_scroll_grabbed_item) {
+ d->pre_scroll_grabbed_item = nullptr;
+ }
+}
+
+// Change cursor
+void
+Canvas::set_cursor() {
+
+ if (!_desktop) {
+ return;
+ }
+
+ auto display = Gdk::Display::get_default();
+
+ switch (_hover_direction) {
+
+ case Inkscape::SplitDirection::NONE:
+ _desktop->event_context->use_tool_cursor();
+ break;
+
+ case Inkscape::SplitDirection::NORTH:
+ case Inkscape::SplitDirection::EAST:
+ case Inkscape::SplitDirection::SOUTH:
+ case Inkscape::SplitDirection::WEST:
+ {
+ auto cursor = Gdk::Cursor::create(display, "pointer");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ case Inkscape::SplitDirection::HORIZONTAL:
+ {
+ auto cursor = Gdk::Cursor::create(display, "ns-resize");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ case Inkscape::SplitDirection::VERTICAL:
+ {
+ auto cursor = Gdk::Cursor::create(display, "ew-resize");
+ get_window()->set_cursor(cursor);
+ break;
+ }
+
+ default:
+ // Shouldn't reach.
+ std::cerr << "Canvas::set_cursor: Unknown hover direction!" << std::endl;
+ }
+}
+
+void
+Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const
+{
+ minimum_width = natural_width = 256;
+}
+
+void
+Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const
+{
+ minimum_height = natural_height = 256;
+}
+
+void
+Canvas::on_size_allocate(Gtk::Allocation &allocation)
+{
+ parent_type::on_size_allocate(allocation);
+ assert(allocation == get_allocation());
+ d->add_idle(); // Trigger the size update to be applied to the stores before the next call to on_draw.
+}
+
+/*
+ * Drawing
+ */
+
+/*
+ * The on_draw() function is called whenever Gtk wants to update the window. This function:
+ *
+ * 1. Ensures that if the idle process was started, at least one cycle has run.
+ *
+ * 2. Blits the store(s) onto the canvas, clipping the outline store as required.
+ * (Or composites them with the transformed snapshot store(s) in decoupled mode.)
+ *
+ * 3. Draws the "controller" in the 'split' split mode.
+ */
+bool
+Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr)
+{
+ auto f = FrameCheck::Event();
+
+ if (!d->active) {
+ std::cerr << "Canvas::on_draw: Called while not active!" << std::endl;
+ return true;
+ }
+
+ // sp_canvas_item_recursive_print_tree(0, _root);
+ // canvas_item_print_tree(_canvas_item_root);
+
+ assert(_drawing);
+
+ // Although hipri_idle is scheduled at a priority higher than draw, and should therefore always be called first if asked, there are times when GTK simply decides to call on_draw anyway.
+ // Here we ensure that that call has taken place. This is problematic because if hipri_idle does rendering, enlarging the damage rect, then our drawing will still be clipped to the old
+ // damage rect. It was precisely this problem that lead to the introduction of hipri_idle. Fortunately, the following failsafe only seems to execute once during initialisation, and
+ // once on further resize events. Both these events seem to trigger a full damage, hence we are ok.
+ if (d->hipri_idle.connected()) {
+ d->hipri_idle.disconnect();
+ d->on_hipri_idle();
+ }
+
+ // Blit background if not solid. (If solid, it is baked into the stores.)
+ if (!d->solid_background) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("background");
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(_background);
+ cr->paint();
+ cr->restore();
+ }
+
+ auto draw_store = [&, this] (const Cairo::RefPtr<Cairo::ImageSurface> &store, const Cairo::RefPtr<Cairo::ImageSurface> &snapshot_store, bool is_backing_store) {
+ if (!d->decoupled_mode) {
+ // Blit store to screen.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw");
+ cr->save();
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ cr->set_source(store, d->_store_rect.left() - _pos.x(), d->_store_rect.top() - _pos.y());
+ cr->paint();
+ cr->restore();
+ } else {
+ // Turn off anti-aliasing for huge performance gains. Only applies to this compositing step.
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+
+ // Blit untransformed snapshot store to complement of transformed snapshot clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 2);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height());
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_snapshot_affine.inverse()));
+ region_to_path(cr, d->_snapshot_clean_region);
+ cr->clip();
+ cr->transform(geom_to_cairo(d->_snapshot_affine * _affine.inverse()));
+ cr->translate(_pos.x(), _pos.y());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ cr->set_source(snapshot_store, d->_snapshot_static_offset.x(), d->_snapshot_static_offset.y());
+ cr->paint();
+ cr->restore();
+
+ // Draw transformed snapshot, clipped to its clean region and the complement of the store's clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 1);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height());
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ region_to_path(cr, d->updater->clean_region);
+ cr->clip();
+ cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse()));
+ region_to_path(cr, d->_snapshot_clean_region);
+ cr->clip();
+ cr->set_source(snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ if (d->prefs.debug_show_snapshot) {
+ cr->set_source_rgba(0, 0, 1, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->paint();
+ }
+ cr->restore();
+
+ // Draw transformed store, clipped to clean region.
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 0);
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ region_to_path(cr, d->updater->clean_region);
+ cr->clip();
+ cr->set_source(store, d->_store_rect.left(), d->_store_rect.top());
+ cr->set_operator(is_backing_store && d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ cr->restore();
+ }
+ };
+
+ // Draw the backing store.
+ draw_store(d->_backing_store, d->_snapshot_store, true);
+
+ // Draw overlay if required.
+ if (_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) {
+ assert(d->_outline_store);
+
+ double outline_overlay_opacity = 1.0 - d->prefs.outline_overlay_opacity / 100.0;
+
+ // Partially obscure drawing by painting semi-transparent white.
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->paint_with_alpha(outline_overlay_opacity);
+
+ // Overlay outline.
+ draw_store(d->_outline_store, d->_snapshot_outline_store, false);
+ }
+
+ // Draw split if required.
+ if (_split_mode != Inkscape::SplitMode::NORMAL) {
+ assert(d->_outline_store);
+
+ // Move split position to center if not in canvas.
+ auto const rect = Geom::Rect(Geom::Point(), get_dimensions());
+ if (!rect.contains(_split_position)) {
+ _split_position = rect.midpoint();
+ }
+
+ // Add clipping path and blit background.
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(_background);
+ add_clippath(cr);
+ cr->paint();
+ cr->restore();
+
+ // Add clipping path and draw outline store.
+ cr->save();
+ add_clippath(cr);
+ draw_store(d->_outline_store, d->_snapshot_outline_store, false);
+ cr->restore();
+ }
+
+ // Paint unclean regions in red.
+ if (d->prefs.debug_show_unclean) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_unclean");
+ auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect));
+ reg->subtract(d->updater->clean_region);
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ if (d->decoupled_mode) {
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ }
+ cr->set_source_rgba(1, 0, 0, 0.2);
+ region_to_path(cr, reg);
+ cr->fill();
+ cr->restore();
+ }
+
+ // Paint internal edges of clean region in green.
+ if (d->prefs.debug_show_clean) {
+ if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_clean");
+ cr->save();
+ cr->translate(-_pos.x(), -_pos.y());
+ if (d->decoupled_mode) {
+ cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse()));
+ }
+ cr->set_source_rgba(0, 0.7, 0, 0.4);
+ region_to_path(cr, d->updater->clean_region);
+ cr->stroke();
+ cr->restore();
+ }
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ // Add dividing line.
+ cr->save();
+ cr->set_source_rgb(0, 0, 0);
+ cr->set_line_width(1);
+ if (_split_direction == Inkscape::SplitDirection::EAST ||
+ _split_direction == Inkscape::SplitDirection::WEST) {
+ cr->move_to((int)_split_position.x() + 0.5, 0);
+ cr->line_to((int)_split_position.x() + 0.5, get_dimensions().y());
+ cr->stroke();
+ } else {
+ cr->move_to( 0, (int)_split_position.y() + 0.5);
+ cr->line_to(get_dimensions().x(), (int)_split_position.y() + 0.5);
+ cr->stroke();
+ }
+ cr->restore();
+
+ // Add controller image.
+ double a = _hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0;
+ cr->save();
+ cr->set_source_rgba(0.2, 0.2, 0.2, a);
+ cr->arc(_split_position.x(), _split_position.y(), 20 * d->_device_scale, 0, 2 * M_PI);
+ cr->fill();
+ cr->restore();
+
+ cr->save();
+ for (int i = 0; i < 4; ++i) {
+ // The four direction triangles.
+ cr->save();
+
+ // Position triangle.
+ cr->translate(_split_position.x(), _split_position.y());
+ cr->rotate((i + 2) * M_PI / 2.0);
+
+ // Draw triangle.
+ cr->move_to(-5 * d->_device_scale, 8 * d->_device_scale);
+ cr->line_to( 0, 18 * d->_device_scale);
+ cr->line_to( 5 * d->_device_scale, 8 * d->_device_scale);
+ cr->close_path();
+
+ double b = (int)_hover_direction == (i + 1) ? 0.9 : 0.7;
+ cr->set_source_rgba(b, b, b, a);
+ cr->fill();
+
+ cr->restore();
+ }
+ cr->restore();
+ }
+
+ // Process bucketed events as soon as possible after draw. We cannot process them now, because we have
+ // a frame to get out as soon as possible, and processing events may take a while. Instead, we schedule
+ // it with a signal callback on the main loop that runs as soon as this function is completed.
+ if (!d->eventprocessor->events.empty()) d->schedule_bucket_emptier();
+
+ // Record the fact that a draw is no longer pending.
+ d->pending_draw = false;
+
+ // Notify the update strategy that another frame has passed.
+ d->updater->frame();
+
+ // Just-for-1.2 flicker "prevention": save the last offset the store was drawn at outside of decoupled mode,
+ // so we can continue to draw a static snapshot upon next going into decoupled mode.
+ if (!d->decoupled_mode) {
+ d->_snapshot_static_offset = d->_store_rect.min() - _pos;
+ }
+
+ return true;
+}
+
+// Sets clip path for Split and X-Ray modes.
+void
+Canvas::add_clippath(const Cairo::RefPtr<Cairo::Context>& cr)
+{
+ double width = get_allocation().get_width();
+ double height = get_allocation().get_height();
+ double sx = _split_position.x();
+ double sy = _split_position.y();
+
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ // We're clipping the outline region... so it's backwards.
+ switch (_split_direction) {
+ case Inkscape::SplitDirection::SOUTH:
+ cr->rectangle(0, 0, width, sy);
+ break;
+ case Inkscape::SplitDirection::NORTH:
+ cr->rectangle(0, sy, width, height - sy);
+ break;
+ case Inkscape::SplitDirection::EAST:
+ cr->rectangle(0, 0, sx, height );
+ break;
+ case Inkscape::SplitDirection::WEST:
+ cr->rectangle(sx, 0, width - sx, height );
+ break;
+ default:
+ // no clipping (for NONE, HORIZONTAL, VERTICAL)
+ break;
+ }
+ } else {
+ cr->arc(sx, sy, d->prefs.x_ray_radius, 0, 2 * M_PI);
+ }
+
+ cr->clip();
+}
+
+void
+CanvasPrivate::add_idle()
+{
+ framecheck_whole_function(this)
+
+ if (!active) {
+ // We can safely discard events until active, because we will run add_idle on activation later in initialisation.
+ return;
+ }
+
+ if (!hipri_idle.connected()) {
+ hipri_idle = Glib::signal_idle().connect(sigc::mem_fun(this, &CanvasPrivate::on_hipri_idle), G_PRIORITY_HIGH_IDLE + 15); // after resize, before draw
+ }
+
+ if (!lopri_idle.connected()) {
+ lopri_idle = Glib::signal_idle().connect(sigc::mem_fun(this, &CanvasPrivate::on_lopri_idle), G_PRIORITY_DEFAULT_IDLE);
+ }
+
+ idle_running = true;
+}
+
+auto
+distSq(const Geom::IntPoint pt, const Geom::IntRect &rect)
+{
+ auto v = rect.clamp(pt) - pt;
+ return v.x() * v.x() + v.y() * v.y();
+}
+
+auto
+calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) {
+ auto c = a.inverse() * b;
+ return std::abs(c[0] - 1) + std::abs(c[1]) + std::abs(c[2]) + std::abs(c[3] - 1);
+}
+
+// Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.)
+auto
+coarsen(const Cairo::RefPtr<Cairo::Region> &region, int min_size, int glue_size, double min_fullness)
+{
+ // Sort the rects by minExtent.
+ struct Compare
+ {
+ bool operator()(const Geom::IntRect &a, const Geom::IntRect &b) const {
+ return a.minExtent() < b.minExtent();
+ }
+ };
+ std::multiset<Geom::IntRect, Compare> rects;
+ int nrects = region->get_num_rectangles();
+ for (int i = 0; i < nrects; i++) {
+ rects.emplace(cairo_to_geom(region->get_rectangle(i)));
+ }
+
+ // List of processed rectangles.
+ std::vector<Geom::IntRect> processed;
+ processed.reserve(nrects);
+
+ // Removal lists.
+ std::vector<decltype(rects)::iterator> remove_rects;
+ std::vector<int> remove_processed;
+
+ // Repeatedly expand small rectangles by absorbing their nearby small rectangles.
+ while (!rects.empty() && rects.begin()->minExtent() < min_size) {
+ // Extract the smallest unprocessed rectangle.
+ auto rect = *rects.begin();
+ rects.erase(rects.begin());
+
+ // Initialise the effective glue size.
+ int effective_glue_size = glue_size;
+
+ while (true) {
+ // Find the glue zone.
+ auto glue_zone = rect;
+ glue_zone.expandBy(effective_glue_size);
+
+ // Absorb rectangles in the glue zone. We could do better algorithmically speaking, but in real life it's already plenty fast.
+ auto newrect = rect;
+ int absorbed_area = 0;
+
+ remove_rects.clear();
+ for (auto it = rects.begin(); it != rects.end(); ++it) {
+ if (glue_zone.contains(*it)) {
+ newrect.unionWith(*it);
+ absorbed_area += it->area();
+ remove_rects.emplace_back(it);
+ }
+ }
+
+ remove_processed.clear();
+ for (int i = 0; i < processed.size(); i++) {
+ auto &r = processed[i];
+ if (glue_zone.contains(r)) {
+ newrect.unionWith(r);
+ absorbed_area += r.area();
+ remove_processed.emplace_back(i);
+ }
+ }
+
+ // If the result was too empty, try again with a smaller glue size.
+ double fullness = (double)(rect.area() + absorbed_area) / newrect.area();
+ if (fullness < min_fullness) {
+ effective_glue_size /= 2;
+ continue;
+ }
+
+ // Commit the change.
+ rect = newrect;
+
+ for (auto &it : remove_rects) {
+ rects.erase(it);
+ }
+
+ for (int j = (int)remove_processed.size() - 1; j >= 0; j--) {
+ int i = remove_processed[j];
+ processed[i] = processed.back();
+ processed.pop_back();
+ }
+
+ // Stop growing if not changed or now big enough.
+ bool finished = absorbed_area == 0 || rect.minExtent() >= min_size;
+ if (finished) {
+ break;
+ }
+
+ // Otherwise, continue normally.
+ effective_glue_size = glue_size;
+ }
+
+ // Put the finished rectangle in processed.
+ processed.emplace_back(rect);
+ }
+
+ // Put any remaining rectangles in processed.
+ for (auto &rect : rects) {
+ processed.emplace_back(rect);
+ }
+
+ return processed;
+}
+
+std::optional<Geom::Dim2>
+CanvasPrivate::old_bisector(const Geom::IntRect &rect)
+{
+ int bw = rect.width();
+ int bh = rect.height();
+
+ /*
+ * Determine redraw strategy:
+ *
+ * bw < bh (strips mode): Draw horizontal strips starting from cursor position.
+ * Seems to be faster for drawing many smaller objects zoomed out.
+ *
+ * bw > hb (chunks mode): Splits across the larger dimension of the rectangle, painting
+ * in almost square chunks (from the cursor.
+ * Seems to be faster for drawing a few blurred objects across the entire screen.
+ * Seems to be somewhat psychologically faster.
+ *
+ * Default is for strips mode.
+ */
+
+ int max_pixels;
+ if (q->_render_mode != Inkscape::RenderMode::OUTLINE) {
+ // Can't be too small or large gradient will be rerendered too many times!
+ max_pixels = 65536 * prefs.tile_multiplier;
+ } else {
+ // Paths only. 1M is catched buffer and we need four channels.
+ max_pixels = 262144;
+ }
+
+ if (bw * bh > max_pixels) {
+ if (bw < bh || bh < 2 * prefs.tile_size) {
+ return Geom::X;
+ } else {
+ return Geom::Y;
+ }
+ }
+
+ return {};
+}
+
+std::optional<Geom::Dim2>
+CanvasPrivate::new_bisector(const Geom::IntRect &rect)
+{
+ int bw = rect.width();
+ int bh = rect.height();
+
+ // Chop in half along the bigger dimension if the bigger dimension is too big.
+ if (bw > bh) {
+ if (bw > prefs.new_bisector_size) {
+ return Geom::X;
+ }
+ } else {
+ if (bh > prefs.new_bisector_size) {
+ return Geom::Y;
+ }
+ }
+
+ return {};
+}
+
+bool
+CanvasPrivate::on_hipri_idle()
+{
+ assert(active);
+ if (idle_running) {
+ idle_running = on_idle();
+ }
+ return false;
+}
+
+bool
+CanvasPrivate::on_lopri_idle()
+{
+ assert(active);
+ if (idle_running) {
+ idle_running = on_idle();
+ }
+ return idle_running;
+}
+
+bool
+CanvasPrivate::on_idle()
+{
+ framecheck_whole_function(this)
+
+ assert(q->_canvas_item_root);
+
+ // Quit idle process if not supposed to be drawing.
+ if (!q->_drawing || q->_drawing_disabled) {
+ return false;
+ }
+
+ const Geom::IntPoint pad(prefs.pad, prefs.pad);
+ auto recreate_store = [&, this] {
+ // Recreate the store at the current affine so that it covers the visible region.
+ _store_rect = q->get_area_world();
+ _store_rect.expandBy(pad);
+ Geom::IntRect expanded = _store_rect;
+ Geom::IntPoint expansion(expanded.width()/2, expanded.height()/2);
+ expanded.expandBy(expansion);
+ q->_drawing->setCacheLimit(expanded);
+ _store_affine = q->_affine;
+ int desired_width = _store_rect.width() * _device_scale;
+ int desired_height = _store_rect.height() * _device_scale;
+ if (!_backing_store || _backing_store->get_width() != desired_width || _backing_store->get_height() != desired_height) {
+ _backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height);
+ cairo_surface_set_device_scale(_backing_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ }
+ auto cr = Cairo::Context::create(_backing_store);
+ if (solid_background) {
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(q->_background);
+ } else {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ }
+ cr->paint();
+ if (need_outline_store()) {
+ if (!_outline_store || _outline_store->get_width() != desired_width || _outline_store->get_height() != desired_height) {
+ _outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height);
+ cairo_surface_set_device_scale(_outline_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ }
+ auto cr = Cairo::Context::create(_outline_store);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ updater->reset();
+ if (prefs.debug_show_unclean) q->queue_draw();
+ };
+
+ // Determine whether the rendering parameters have changed, and reset if so.
+ if (!_backing_store || (need_outline_store() && !_outline_store) || _device_scale != q->get_scale_factor() || _store_solid_background != solid_background) {
+ _device_scale = q->get_scale_factor();
+ _store_solid_background = solid_background;
+ recreate_store();
+ decoupled_mode = false;
+ if (prefs.debug_logging) std::cout << "Full reset" << std::endl;
+ }
+
+ // Make sure to clear the outline store when not in use, so we don't accidentally re-use it when it is required again.
+ if (!need_outline_store()) {
+ _outline_store.clear();
+ }
+
+ auto shift_store = [&, this] {
+ // Recreate the store, but keep re-usable content from the old store.
+ auto store_rect = q->get_area_world();
+ store_rect.expandBy(pad);
+ Geom::IntRect expanded = _store_rect;
+ Geom::IntPoint expansion(expanded.width()/2, expanded.height()/2);
+ expanded.expandBy(expansion);
+ q->_drawing->setCacheLimit(expanded);
+ auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale);
+ cairo_surface_set_device_scale(backing_store->cobj(), _device_scale, _device_scale); // No C++ API!
+
+ // Determine the geometry of the shift.
+ auto shift = store_rect.min() - _store_rect.min();
+ auto reuse_rect = store_rect & _store_rect;
+ assert(reuse_rect); // Should not be called if there is no overlap.
+ auto cr = Cairo::Context::create(backing_store);
+
+ // Paint background into region not covered by next operation.
+ if (solid_background) {
+ auto reg = Cairo::Region::create(geom_to_cairo(store_rect));
+ reg->subtract(geom_to_cairo(*reuse_rect));
+ reg->translate(-store_rect.left(), -store_rect.top());
+ cr->save();
+ if (solid_background) {
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(q->_background);
+ }
+ region_to_path(cr, reg);
+ cr->fill();
+ cr->restore();
+ }
+
+ // Copy re-usuable contents of old store into new store, shifted.
+ cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(_backing_store, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+
+ // Set the result as the new backing store.
+ _store_rect = store_rect;
+ assert(_store_affine == q->_affine); // Should not be called if the affine has changed.
+ _backing_store = std::move(backing_store);
+
+ // Do the same for the outline store
+ if (_outline_store) {
+ auto outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale);
+ cairo_surface_set_device_scale(outline_store->cobj(), _device_scale, _device_scale); // No C++ API!
+ auto cr = Cairo::Context::create(outline_store);
+ cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(_outline_store, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+ _outline_store = std::move(outline_store);
+ }
+
+ updater->intersect(_store_rect);
+ if (prefs.debug_show_unclean) q->queue_draw();
+ };
+
+ auto take_snapshot = [&, this] {
+ // Copy the backing store to the snapshot, leaving us temporarily in an invalid state.
+ std::swap(_snapshot_store, _backing_store); // This will re-use the old snapshot store later if possible.
+ _snapshot_rect = _store_rect;
+ _snapshot_affine = _store_affine;
+ _snapshot_clean_region = updater->clean_region->copy();
+
+ // Do the same for the outline store
+ std::swap(_snapshot_outline_store, _outline_store);
+
+ // Recreate the backing store, making the state valid again.
+ recreate_store();
+ };
+
+ // Handle transitions and actions in response to viewport changes.
+ if (!decoupled_mode) {
+ // Enter decoupled mode if the affine has changed from what the backing store was drawn at.
+ if (q->_affine != _store_affine) {
+ // Snapshot and reset the backing store.
+ take_snapshot();
+
+ // Enter decoupled mode.
+ if (prefs.debug_logging) std::cout << "Entering decoupled mode" << std::endl;
+ decoupled_mode = true;
+
+ // Note: If redrawing is fast enough to finish during the frame, then going into decoupled mode, drawing, and leaving
+ // it again performs exactly the same rendering operations as if we had not gone into it at all. Also, no extra copies
+ // or blits are performed, and the drawing operations done on the screen are the same. Hence this feature comes at zero cost.
+ } else {
+ // Get visible rectangle in canvas coordinates.
+ auto const visible = q->get_area_world();
+ if (!_store_rect.intersects(visible)) {
+ // If the store has gone completely off-screen, recreate it.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Recreated store" << std::endl;
+ } else if (!_store_rect.contains(visible)) {
+ // If the store has gone partially off-screen, shift it.
+ shift_store();
+ if (prefs.debug_logging) std::cout << "Shifted store" << std::endl;
+ }
+ // After these operations, the store should now be fully on-screen.
+ assert(_store_rect.contains(visible));
+ }
+ } else { // if (decoupled_mode)
+ // Completely cancel the previous redraw and start again if the viewing parameters have changed too much.
+ if (!prefs.debug_sticky_decoupled) {
+ auto pl = Geom::Parallelogram(q->get_area_world());
+ pl *= _store_affine * q->_affine.inverse();
+ if (!pl.intersects(_store_rect)) {
+ // Store has gone off the screen.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Restarting redraw (store off-screen)" << std::endl;
+ } else {
+ auto diff = calc_affine_diff(q->_affine, _store_affine);
+ if (diff > prefs.max_affine_diff) {
+ // Affine has changed too much.
+ recreate_store();
+ if (prefs.debug_logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl;
+ }
+ }
+ }
+ }
+
+ // Assert that _clean_region is a subregion of _store_rect.
+ #ifndef NDEBUG
+ auto tmp = updater->clean_region->copy();
+ tmp->subtract(geom_to_cairo(_store_rect));
+ assert(tmp->empty());
+ #endif
+
+ // Ensure the geometry is up-to-date and in the right place.
+ auto affine = decoupled_mode ? _store_affine : q->_affine;
+ if (q->_need_update || geom_affine != affine) {
+ q->_canvas_item_root->update(affine);
+ geom_affine = affine;
+ q->_need_update = false;
+ }
+
+ // If asked to, don't paint anything and instead halt the idle process.
+ if (prefs.debug_disable_redraw) {
+ return false;
+ }
+
+ // Get the subrectangle of store that is visible.
+ Geom::OptIntRect visible_rect;
+ if (!decoupled_mode) {
+ // By a previous assertion, this always lies within the store.
+ visible_rect = q->get_area_world();
+ } else {
+ // Get the window rectangle transformed into canvas space.
+ auto pl = Geom::Parallelogram(q->get_area_world());
+ pl *= _store_affine * q->_affine.inverse();
+
+ // Get its bounding box, rounded outwards.
+ auto b = pl.bounds();
+ auto bi = Geom::IntRect(b.min().floor(), b.max().ceil());
+
+ // The visible rect is the intersection of this with the store
+ visible_rect = bi & _store_rect;
+ }
+ // The visible rectangle must be a subrectangle of store.
+ assert(_store_rect.contains(visible_rect));
+
+ // Get the mouse position in screen space.
+ Geom::IntPoint mouse_loc = (last_mouse ? *last_mouse : Geom::Point(q->get_dimensions()) / 2).round();
+
+ // Map the mouse to canvas space.
+ mouse_loc += q->_pos;
+ if (decoupled_mode) {
+ mouse_loc = geom_act(_store_affine * q->_affine.inverse(), mouse_loc);
+ }
+
+ // Begin processing redraws.
+ auto start_time = g_get_monotonic_time();
+ while (true) {
+ // Get the clean region for the next redraw as reported by the updater.
+ auto clean_region = updater->get_next_clean_region();
+
+ // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store).
+ Cairo::RefPtr<Cairo::Region> paint_region;
+ if (visible_rect) {
+ paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect));
+ paint_region->subtract(clean_region);
+ } else {
+ paint_region = Cairo::Region::create();
+ }
+
+ Geom::OptIntRect dragged = Geom::OptIntRect();
+ if (q->_grabbed_canvas_item) {
+ dragged = q->_grabbed_canvas_item->get_bounds().roundOutwards();
+ if (dragged) {
+ (*dragged).expandBy(prefs.pad);
+ dragged = dragged & visible_rect;
+ if (dragged) {
+ paint_region->subtract(geom_to_cairo(*dragged));
+ }
+ }
+ }
+ // Get the list of rectangles to paint, coarsened to avoid fragmentation.
+ auto rects = coarsen(paint_region,
+ std::min<int>(prefs.coarsener_min_size, prefs.new_bisector_size / 2),
+ std::min<int>(prefs.coarsener_glue_size, prefs.new_bisector_size / 2),
+ prefs.coarsener_min_fullness);
+ if (dragged) {
+ // this become the first after look for cursor
+ rects.push_back(*dragged);
+ }
+ // Ensure that all the rectangles lie within the visible rect (and therefore within the store).
+ #ifndef NDEBUG
+ for (auto &rect : rects) {
+ assert(visible_rect.contains(rect));
+ }
+ #endif
+
+ // Put the rectangles into a heap sorted by distance from mouse.
+ auto cmp = [&] (const Geom::IntRect &a, const Geom::IntRect &b) {
+ return distSq(mouse_loc, a) > distSq(mouse_loc, b);
+ };
+ std::make_heap(rects.begin(), rects.end(), cmp);
+
+ // Process rectangles until none left or timed out.
+ bool start = true;
+ while (!rects.empty()) {
+ // Extract the closest rectangle to the mouse.
+ std::pop_heap(rects.begin(), rects.end(), cmp);
+ auto rect = rects.back();
+ rects.pop_back();
+
+ // Cull empty rectangles.
+ if (rect.width() == 0 || rect.height() == 0) {
+ start = false;
+ continue;
+ }
+
+ // Cull rectangles that lie entirely inside the clean region.
+ // (These can be generated by coarsening; they must be discarded to avoid getting stuck re-rendering the same rectangles.)
+ if (clean_region->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) {
+ start = false;
+ continue;
+ }
+
+ // Lambda to add a rectangle to the heap.
+ auto add_rect = [&] (const Geom::IntRect &rect) {
+ rects.emplace_back(rect);
+ std::push_heap(rects.begin(), rects.end(), cmp);
+ };
+
+ // If the rectangle needs bisecting, bisect it and put it back on the heap.
+ auto axis = prefs.use_new_bisector ? new_bisector(rect) : old_bisector(rect);
+ if (axis && !(dragged && start)) {
+ int mid = rect[*axis].middle();
+ auto lo = rect; lo[*axis].setMax(mid); add_rect(lo);
+ auto hi = rect; hi[*axis].setMin(mid); add_rect(hi);
+ start = false;
+ continue;
+ }
+ start = false;
+ // Paint the rectangle.
+ paint_rect_internal(rect);
+
+ // Check for timeout.
+ auto now = g_get_monotonic_time();
+ auto elapsed = now - start_time;
+ bool fixchoping = false;
+ #ifdef _WIN32
+ fixchoping = true;
+ #elif defined(__APPLE__)
+ fixchoping = true;
+ #endif
+ if (elapsed > ((fixchoping && prefs.render_time_limit == 1000) ? 80000 : prefs.render_time_limit)) {
+ // Timed out. Temporarily return to GTK main loop, and come back here when next idle.
+ if (prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - start_time << " us" << std::endl;
+ framecheckobj.subtype = 1;
+ return true;
+ }
+ }
+
+ // Report the redraw as finished. Exit if there's no more redraws to process.
+ bool keep_going = updater->report_finished();
+ if (!keep_going) break;
+ }
+
+ // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine.
+ if (decoupled_mode) {
+ if (prefs.debug_sticky_decoupled) {
+ // Debug feature: quit idle process, but stay in decoupled mode.
+ return false;
+ } else if (_store_affine == q->_affine) {
+ // Content is rendered at the correct affine - exit decoupled mode and quit idle process.
+ if (prefs.debug_logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl;
+ // Exit decoupled mode.
+ decoupled_mode = false;
+ // Quit idle process.
+ return false;
+ } else {
+ // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine.
+ if (prefs.debug_logging) std::cout << "Scheduling final redraw" << std::endl;
+ // Snapshot and reset the backing store.
+ take_snapshot();
+ // Continue idle process.
+ return true;
+ }
+ } else {
+ // All done, quit the idle process.
+ framecheckobj.subtype = 3;
+ return false;
+ }
+}
+
+void
+CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect)
+{
+ // Paint the rectangle.
+ q->_drawing->setColorMode(q->_color_mode);
+ paint_single_buffer(rect, _backing_store, true, false);
+
+ if (_outline_store) {
+ q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE);
+ paint_single_buffer(rect, _outline_store, false, q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY);
+ q->_drawing->setRenderMode(q->_render_mode); // Leave the drawing in the requested render mode.
+ }
+
+ // Introduce an artificial delay for each rectangle.
+ if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time);
+
+ // Mark the rectangle as clean.
+ updater->mark_clean(rect);
+
+ // Mark the screen dirty.
+ if (!decoupled_mode) {
+ // Get rectangle needing repaint
+ auto repaint_rect = rect - q->_pos;
+
+ // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles)
+ auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height());
+ assert(repaint_rect & screen_rect);
+
+ // Schedule repaint
+ queue_draw_area(repaint_rect); // Guarantees on_draw will be called in the future.
+ if (bucket_emptier_tick_callback) {q->remove_tick_callback(*bucket_emptier_tick_callback); bucket_emptier_tick_callback.reset();}
+ pending_draw = true;
+ } else {
+ // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards)
+ auto pl = Geom::Parallelogram(rect);
+ pl *= q->_affine * _store_affine.inverse();
+ pl *= Geom::Translate(-q->_pos);
+ auto b = pl.bounds();
+ auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil());
+
+ // Check if repaint is necessary - some rectangles could be entirely off-screen.
+ auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height());
+ if (repaint_rect & screen_rect) {
+ // Schedule repaint
+ queue_draw_area(repaint_rect);
+ if (bucket_emptier_tick_callback) {q->remove_tick_callback(*bucket_emptier_tick_callback); bucket_emptier_tick_callback.reset();}
+ pending_draw = true;
+ }
+ }
+}
+
+void
+CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo::RefPtr<Cairo::ImageSurface> &store, bool is_backing_store, bool outline_overlay_pass)
+{
+ // Make sure the following code does not go outside of store's data.
+ assert(store);
+ assert(store->get_format() == Cairo::FORMAT_ARGB32);
+ assert(_store_rect.contains(paint_rect)); // FIXME: Observed to fail once when hitting Ctrl+O while Canvas was busy. Haven't managed to reproduce it. Doesn't mean it's fixed.
+
+ // Create temporary surface that draws directly to store.
+ store->flush();
+ unsigned char *data = store->get_data();
+ int stride = store->get_stride();
+
+ // Check we are using the correct device scale.
+ double x_scale = 1.0;
+ double y_scale = 1.0;
+ cairo_surface_get_device_scale(store->cobj(), &x_scale, &y_scale); // No C++ API!
+ assert (_device_scale == (int) x_scale);
+ assert (_device_scale == (int) y_scale);
+
+ // Move to the correct row.
+ data += stride * (paint_rect.top() - _store_rect.top()) * (int)y_scale;
+ // Move to the correct column.
+ data += 4 * (paint_rect.left() - _store_rect.left()) * (int)x_scale;
+ auto imgs = Cairo::ImageSurface::create(data, Cairo::FORMAT_ARGB32,
+ paint_rect.width() * _device_scale,
+ paint_rect.height() * _device_scale,
+ stride);
+
+ cairo_surface_set_device_scale(imgs->cobj(), _device_scale, _device_scale); // No C++ API!
+
+ auto cr = Cairo::Context::create(imgs);
+
+ // Clear background
+ cr->save();
+ if (is_backing_store && solid_background) {
+ cr->set_source(q->_background);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ } else {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ }
+ cr->paint();
+ cr->restore();
+
+ // Render drawing on top of background.
+ if (q->_canvas_item_root->is_visible()) {
+ auto buf = Inkscape::CanvasItemBuffer{ paint_rect, _device_scale, outline_overlay_pass, cr };
+ q->_canvas_item_root->render(&buf);
+ }
+
+ // Paint over newly drawn content with a translucent random colour.
+ if (prefs.debug_show_redraw) {
+ cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->rectangle(0, 0, imgs->get_width(), imgs->get_height());
+ cr->fill();
+ }
+
+ if (q->_cms_active) {
+ auto transf = prefs.from_display
+ ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key)
+ : Inkscape::CMSSystem::getDisplayTransform();
+
+ if (transf) {
+ imgs->flush();
+ auto px = imgs->get_data();
+ int stride = imgs->get_stride();
+ for (int i = 0; i < paint_rect.height(); i++) {
+ auto row = px + i * stride;
+ Inkscape::CMSSystem::doTransform(transf, row, row, paint_rect.width());
+ }
+ imgs->mark_dirty();
+ }
+ }
+
+ store->mark_dirty();
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/canvas.h b/src/ui/widget/canvas.h
new file mode 100644
index 0000000..f2a434a
--- /dev/null
+++ b/src/ui/widget/canvas.h
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_H
+/*
+ * Authors:
+ * Tavmjong Bah
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <memory>
+#include <gtkmm.h>
+#include <2geom/rect.h>
+#include <2geom/int-rect.h>
+#include "display/rendermode.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItem;
+class CanvasItemGroup;
+class Drawing;
+
+namespace UI {
+namespace Widget {
+
+class CanvasPrivate;
+
+/**
+ * A Gtk::DrawingArea widget for Inkscape's canvas.
+ */
+class Canvas : public Gtk::DrawingArea
+{
+ using parent_type = Gtk::DrawingArea;
+
+public:
+
+ Canvas();
+ ~Canvas() override;
+
+ /* Configuration */
+
+ // Desktop (Todo: Remove.)
+ void set_desktop(SPDesktop *desktop) { _desktop = desktop; }
+ SPDesktop *get_desktop() const { return _desktop; }
+
+ // Drawing
+ void set_drawing(Inkscape::Drawing *drawing);
+
+ // Canvas item root
+ CanvasItemGroup *get_canvas_item_root() const { return _canvas_item_root; }
+
+ // Geometry
+ void set_pos (const Geom::IntPoint &pos);
+ void set_pos (const Geom::Point &fpos) { set_pos(fpos.round()); }
+ void set_affine(const Geom::Affine &affine);
+ const Geom::IntPoint& get_pos () const {return _pos;}
+ const Geom::Affine& get_affine() const {return _affine;}
+
+ // Background
+ void set_background_color(guint32 rgba);
+ void set_background_checkerboard(guint32 rgba = 0xC4C4C4FF, bool use_alpha = false);
+
+ // Rendering modes
+ void set_render_mode(Inkscape::RenderMode mode);
+ void set_color_mode (Inkscape::ColorMode mode);
+ void set_split_mode (Inkscape::SplitMode mode);
+ Inkscape::RenderMode get_render_mode() const { return _render_mode; }
+ Inkscape::ColorMode get_color_mode() const { return _color_mode; }
+ Inkscape::SplitMode get_split_mode() const { return _split_mode; }
+
+ // CMS
+ void set_cms_key(std::string key) {
+ _cms_key = std::move(key);
+ _cms_active = !_cms_key.empty();
+ }
+ const std::string& get_cms_key() const { return _cms_key; }
+ void set_cms_active(bool active) { _cms_active = active; }
+ bool get_cms_active() const { return _cms_active; }
+
+ /* Observers */
+
+ // Geometry
+ Geom::IntPoint get_dimensions() const;
+ bool world_point_inside_canvas(Geom::Point const &world) const; // desktop-events.cpp
+ Geom::Point canvas_to_world(Geom::Point const &window) const;
+ Geom::IntRect get_area_world() const;
+
+ // State
+ bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp
+
+ // Encapsulation leaks
+ Cairo::RefPtr<Cairo::ImageSurface> get_backing_store() const; // canvas-item-rotate.cpp
+ Cairo::RefPtr<Cairo::Pattern> get_background_pattern() const { return _background; } // canvas-item-rect.cpp, canvas-item-ctrl.cpp
+
+ /* Methods */
+
+ // Invalidation
+ void redraw_all(); // Mark everything as having changed.
+ void redraw_area(Geom::Rect& area); // Mark a rectangle of world space as having changed.
+ void redraw_area(int x0, int y0, int x1, int y1);
+ void redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1);
+ void request_update(); // Mark geometry as needing recalculation.
+
+ // Gobblers (tool-base.cpp)
+ int gobble_key_events(guint keyval, guint mask);
+ void gobble_motion_events(guint mask);
+
+ // Callback run on destructor of any canvas item
+ void canvas_item_destructed(Inkscape::CanvasItem *item);
+
+ // State
+ Inkscape::CanvasItem *get_current_canvas_item() const { return _current_canvas_item; }
+ void set_current_canvas_item(Inkscape::CanvasItem *item) {
+ _current_canvas_item = item;
+ }
+ Inkscape::CanvasItem *get_grabbed_canvas_item() const { return _grabbed_canvas_item; }
+ void set_grabbed_canvas_item(Inkscape::CanvasItem *item, Gdk::EventMask mask) {
+ _grabbed_canvas_item = item;
+ _grabbed_event_mask = mask;
+ }
+ void set_drawing_disabled(bool disable); // Disable during path ops, etc.
+ void set_all_enter_events(bool on) { _all_enter_events = on; }
+
+protected:
+
+ void get_preferred_width_vfunc (int &minimum_width, int &natural_width ) const override;
+ void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override;
+
+ // Event handlers
+ bool on_scroll_event (GdkEventScroll* ) override;
+ bool on_button_event (GdkEventButton* );
+ bool on_button_press_event (GdkEventButton* ) override;
+ bool on_button_release_event(GdkEventButton* ) override;
+ bool on_enter_notify_event (GdkEventCrossing*) override;
+ bool on_leave_notify_event (GdkEventCrossing*) override;
+ bool on_focus_in_event (GdkEventFocus* ) override;
+ bool on_key_press_event (GdkEventKey* ) override;
+ bool on_key_release_event (GdkEventKey* ) override;
+ bool on_motion_notify_event (GdkEventMotion* ) override;
+
+ void on_realize() override;
+ void on_unrealize() override;
+ void on_size_allocate(Gtk::Allocation&) override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>&) override;
+
+private:
+
+ /* Configuration */
+
+ // Desktop
+ SPDesktop *_desktop = nullptr;
+
+ // Drawing
+ Inkscape::Drawing *_drawing = nullptr;
+
+ // Canvas item root
+ CanvasItemGroup *_canvas_item_root = nullptr;
+
+ // Geometry
+ Geom::IntPoint _pos; ///< Coordinates of top-left pixel of canvas view within canvas.
+ Geom::Affine _affine; ///< The affine that we have been requested to draw at.
+
+ // Background
+ Cairo::RefPtr<Cairo::Pattern> _background; ///< The background of the widget.
+
+ // Rendering modes
+ Inkscape::RenderMode _render_mode = Inkscape::RenderMode::NORMAL;
+ Inkscape::SplitMode _split_mode = Inkscape::SplitMode::NORMAL;
+ Inkscape::ColorMode _color_mode = Inkscape::ColorMode::NORMAL;
+
+ // CMS
+ std::string _cms_key;
+ bool _cms_active = false;
+
+ /* Internal state */
+
+ // Event handling/item picking
+ GdkEvent _pick_event; ///< Event used to find currently selected item.
+ bool _in_repick; ///< For tracking recursion of pick_current_item().
+ bool _left_grabbed_item; ///< ?
+ bool _all_enter_events; ///< Keep all enter events. Only set true in connector-tool.cpp.
+ bool _is_dragging; ///< Used in selection-chemistry to block undo/redo.
+ int _state; ///< Last known modifier state (SHIFT, CTRL, etc.).
+
+ Inkscape::CanvasItem *_current_canvas_item; ///< Item containing cursor, nullptr if none.
+ Inkscape::CanvasItem *_current_canvas_item_new; ///< Item to become _current_item, nullptr if none.
+ Inkscape::CanvasItem *_grabbed_canvas_item; ///< Item that holds a pointer grab; nullptr if none.
+ Gdk::EventMask _grabbed_event_mask;
+
+ // Drawing
+ bool _drawing_disabled = false; ///< Disable drawing during critical operations
+ bool _need_update = true; // Set true so setting CanvasItem bounds are calculated at least once.
+
+ // Split view
+ Inkscape::SplitDirection _split_direction;
+ Geom::Point _split_position;
+ Inkscape::SplitDirection _hover_direction;
+ bool _split_dragging;
+ Geom::Point _split_drag_start;
+
+ void add_clippath(const Cairo::RefPtr<Cairo::Context>&);
+ void set_cursor();
+
+ // Opaque pointer to implementation
+ friend class CanvasPrivate;
+ std::unique_ptr<CanvasPrivate> d;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_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/ui/widget/color-entry.cpp b/src/ui/widget/color-entry.cpp
new file mode 100644
index 0000000..804350c
--- /dev/null
+++ b/src/ui/widget/color-entry.cpp
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Entry widget for typing color value in css form
+ *//*
+ * Authors:
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <glibmm.h>
+#include <glibmm/i18n.h>
+#include <iomanip>
+
+#include "color-entry.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorEntry::ColorEntry(SelectedColor &color)
+ : _color(color)
+ , _updating(false)
+ , _updatingrgba(false)
+ , _prevpos(0)
+ , _lastcolor(0)
+{
+ _color_changed_connection = color.signal_changed.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ _color_dragged_connection = color.signal_dragged.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ signal_activate().connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged));
+ get_buffer()->signal_inserted_text().connect(sigc::mem_fun(this, &ColorEntry::_inputCheck));
+ _onColorChanged();
+
+ // add extra character for pasting a hash, '#11223344'
+ set_max_length(9);
+ set_width_chars(8);
+ set_tooltip_text(_("Hexadecimal RGBA value of the color"));
+}
+
+ColorEntry::~ColorEntry()
+{
+ _color_changed_connection.disconnect();
+ _color_dragged_connection.disconnect();
+}
+
+void ColorEntry::_inputCheck(guint pos, const gchar * /*chars*/, guint n_chars)
+{
+ // remember position of last character, so we can remove it.
+ // we only overflow by 1 character at most.
+ _prevpos = pos + n_chars - 1;
+}
+
+void ColorEntry::on_changed()
+{
+ if (_updating) {
+ return;
+ }
+ if (_updatingrgba) {
+ return; // Typing text into entry box
+ }
+
+ Glib::ustring text = get_text();
+ bool changed = false;
+
+ // Coerce the value format to hexadecimal
+ for (auto it = text.begin(); it != text.end(); /*++it*/) {
+ if (!g_ascii_isxdigit(*it)) {
+ text.erase(it);
+ changed = true;
+ } else {
+ ++it;
+ }
+ }
+
+ if (text.size() > 8) {
+ text.erase(_prevpos, 1);
+ changed = true;
+ }
+
+ // autofill rules
+ gchar *str = g_strdup(text.c_str());
+ gchar *end = nullptr;
+ guint64 rgba = g_ascii_strtoull(str, &end, 16);
+ ptrdiff_t len = end - str;
+ if (len < 8) {
+ if (len == 0) {
+ rgba = _lastcolor;
+ } else if (len <= 2) {
+ if (len == 1) {
+ rgba *= 17;
+ }
+ rgba = (rgba << 24) + (rgba << 16) + (rgba << 8);
+ } else if (len <= 4) {
+ // display as rrggbbaa
+ rgba = rgba << (4 * (4 - len));
+ guint64 r = rgba & 0xf000;
+ guint64 g = rgba & 0x0f00;
+ guint64 b = rgba & 0x00f0;
+ guint64 a = rgba & 0x000f;
+ rgba = 17 * ((r << 12) + (g << 8) + (b << 4) + a);
+ } else {
+ rgba = rgba << (4 * (8 - len));
+ }
+
+ if (len == 7) {
+ rgba = (rgba & 0xfffffff0) + (_lastcolor & 0x00f);
+ } else if (len == 5) {
+ rgba = (rgba & 0xfffff000) + (_lastcolor & 0xfff);
+ } else if (len != 4 && len != 8) {
+ rgba = (rgba & 0xffffff00) + (_lastcolor & 0x0ff);
+ }
+ }
+
+ _updatingrgba = true;
+ if (changed) {
+ set_text(str);
+ }
+ SPColor color(rgba);
+ _color.setColorAlpha(color, SP_RGBA32_A_F(rgba));
+ _updatingrgba = false;
+
+ g_free(str);
+}
+
+
+void ColorEntry::_onColorChanged()
+{
+ if (_updatingrgba) {
+ return;
+ }
+
+ SPColor color = _color.color();
+ gdouble alpha = _color.alpha();
+
+ _lastcolor = color.toRGBA32(alpha);
+ Glib::ustring text = Glib::ustring::format(std::hex, std::setw(8), std::setfill(L'0'), _lastcolor);
+
+ Glib::ustring old_text = get_text();
+ if (old_text != text) {
+ _updating = true;
+ set_text(text);
+ _updating = 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/ui/widget/color-entry.h b/src/ui/widget/color-entry.h
new file mode 100644
index 0000000..4df80de
--- /dev/null
+++ b/src/ui/widget/color-entry.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Entry widget for typing color value in css form
+ *//*
+ * Authors:
+ * Tomasz Boczkowski <penginsbacon@gmail.com>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_COLOR_ENTRY_H
+#define SEEN_COLOR_ENTRY_H
+
+#include <gtkmm/entry.h>
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorEntry : public Gtk::Entry
+{
+public:
+ ColorEntry(SelectedColor &color);
+ ~ColorEntry() override;
+
+protected:
+ void on_changed() override;
+
+private:
+ void _onColorChanged();
+ void _inputCheck(guint pos, const gchar * /*chars*/, guint /*n_chars*/);
+
+ SelectedColor &_color;
+ sigc::connection _color_changed_connection;
+ sigc::connection _color_dragged_connection;
+ bool _updating;
+ bool _updatingrgba;
+ guint32 _lastcolor;
+ int _prevpos;
+};
+
+}
+}
+}
+
+#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/ui/widget/color-icc-selector.cpp b/src/ui/widget/color-icc-selector.cpp
new file mode 100644
index 0000000..8c8456a
--- /dev/null
+++ b/src/ui/widget/color-icc-selector.cpp
@@ -0,0 +1,1025 @@
+// 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.
+ */
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <set>
+#include <utility>
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/spinbutton.h>
+#include <glibmm/i18n.h>
+
+#include "colorspace.h"
+#include "document.h"
+#include "inkscape.h"
+#include "profile-manager.h"
+
+#include "svg/svg-icc-color.h"
+
+#include "ui/dialog-events.h"
+#include "ui/util.h"
+#include "ui/widget/color-icc-selector.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#include "ui/widget/scrollprotected.h"
+
+#define noDEBUG_LCMS
+
+#include "object/color-profile.h"
+#include "cms-system.h"
+#include "color-profile-cms-fns.h"
+
+#ifdef DEBUG_LCMS
+#include "preferences.h"
+#endif // DEBUG_LCMS
+
+#ifdef DEBUG_LCMS
+extern guint update_in_progress;
+#define DEBUG_MESSAGE(key, ...) \
+ { \
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get(); \
+ bool dump = prefs->getBool("/options/scislac/" #key); \
+ bool dumpD = prefs->getBool("/options/scislac/" #key "D"); \
+ bool dumpD2 = prefs->getBool("/options/scislac/" #key "D2"); \
+ dumpD && = ((update_in_progress == 0) || dumpD2); \
+ if (dump) { \
+ g_message(__VA_ARGS__); \
+ } \
+ if (dumpD) { \
+ GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, \
+ GTK_BUTTONS_OK, __VA_ARGS__); \
+ g_signal_connect_swapped(dialog, "response", G_CALLBACK(gtk_widget_destroy), dialog); \
+ gtk_widget_show_all(dialog); \
+ } \
+ }
+#endif // DEBUG_LCMS
+
+
+#define XPAD 4
+#define YPAD 1
+
+namespace {
+
+GtkWidget *_scrollprotected_combo_box_new_with_model(GtkTreeModel *model)
+{
+ auto combobox = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBox>());
+ gtk_combo_box_set_model(combobox->gobj(), model);
+ return GTK_WIDGET(combobox->gobj());
+}
+
+size_t maxColorspaceComponentCount = 0;
+
+
+/**
+ * Internal variable to track all known colorspaces.
+ */
+std::set<cmsUInt32Number> knownColorspaces;
+
+/**
+ * Helper function to handle GTK2/GTK3 attachment #ifdef code.
+ */
+void attachToGridOrTable(GtkWidget *parent, GtkWidget *child, guint left, guint top, guint width, guint height,
+ bool hexpand = false, bool centered = false, guint xpadding = XPAD, guint ypadding = YPAD)
+{
+ gtk_widget_set_margin_start(child, xpadding);
+ gtk_widget_set_margin_end(child, xpadding);
+ gtk_widget_set_margin_top(child, ypadding);
+ gtk_widget_set_margin_bottom(child, ypadding);
+
+ if (hexpand) {
+ gtk_widget_set_hexpand(child, TRUE);
+ }
+
+ if (centered) {
+ gtk_widget_set_halign(child, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(child, GTK_ALIGN_CENTER);
+ }
+
+ gtk_grid_attach(GTK_GRID(parent), child, left, top, width, height);
+}
+
+} // namespace
+
+/*
+icSigRgbData
+icSigCmykData
+icSigCmyData
+*/
+#define SPACE_ID_RGB 0
+#define SPACE_ID_CMY 1
+#define SPACE_ID_CMYK 2
+
+
+colorspace::Component::Component()
+ : name()
+ , tip()
+ , scale(1)
+{
+}
+
+colorspace::Component::Component(std::string name, std::string tip, guint scale)
+ : name(std::move(name))
+ , tip(std::move(tip))
+ , scale(scale)
+{
+}
+
+static cmsUInt16Number *getScratch()
+{
+ // bytes per pixel * input channels * width
+ static cmsUInt16Number *scritch = static_cast<cmsUInt16Number *>(g_new(cmsUInt16Number, 4 * 1024));
+
+ return scritch;
+}
+
+std::vector<colorspace::Component> colorspace::getColorSpaceInfo(uint32_t space)
+{
+ static std::map<cmsUInt32Number, std::vector<Component> > sets;
+ if (sets.empty()) {
+ sets[cmsSigXYZData].push_back(Component("_X", "X", 2)); // TYPE_XYZ_16
+ sets[cmsSigXYZData].push_back(Component("_Y", "Y", 1));
+ sets[cmsSigXYZData].push_back(Component("_Z", "Z", 2));
+
+ sets[cmsSigLabData].push_back(Component("_L", "L", 100)); // TYPE_Lab_16
+ sets[cmsSigLabData].push_back(Component("_a", "a", 256));
+ sets[cmsSigLabData].push_back(Component("_b", "b", 256));
+
+ // cmsSigLuvData
+
+ sets[cmsSigYCbCrData].push_back(Component("_Y", "Y", 1)); // TYPE_YCbCr_16
+ sets[cmsSigYCbCrData].push_back(Component("C_b", "Cb", 1));
+ sets[cmsSigYCbCrData].push_back(Component("C_r", "Cr", 1));
+
+ sets[cmsSigYxyData].push_back(Component("_Y", "Y", 1)); // TYPE_Yxy_16
+ sets[cmsSigYxyData].push_back(Component("_x", "x", 1));
+ sets[cmsSigYxyData].push_back(Component("y", "y", 1));
+
+ sets[cmsSigRgbData].push_back(Component(_("_R:"), _("Red"), 1)); // TYPE_RGB_16
+ sets[cmsSigRgbData].push_back(Component(_("_G:"), _("Green"), 1));
+ sets[cmsSigRgbData].push_back(Component(_("_B:"), _("Blue"), 1));
+
+ sets[cmsSigGrayData].push_back(Component(_("G:"), _("Gray"), 1)); // TYPE_GRAY_16
+
+ sets[cmsSigHsvData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HSV_16
+ sets[cmsSigHsvData].push_back(Component(_("_S:"), _("Saturation"), 1));
+ sets[cmsSigHsvData].push_back(Component("_V:", "Value", 1));
+
+ sets[cmsSigHlsData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HLS_16
+ sets[cmsSigHlsData].push_back(Component(_("_L:"), _("Lightness"), 1));
+ sets[cmsSigHlsData].push_back(Component(_("_S:"), _("Saturation"), 1));
+
+ sets[cmsSigCmykData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMYK_16
+ sets[cmsSigCmykData].push_back(Component(_("_M:"), _("Magenta"), 1));
+ sets[cmsSigCmykData].push_back(Component(_("_Y:"), _("Yellow"), 1));
+ sets[cmsSigCmykData].push_back(Component(_("_K:"), _("Black"), 1));
+
+ sets[cmsSigCmyData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMY_16
+ sets[cmsSigCmyData].push_back(Component(_("_M:"), _("Magenta"), 1));
+ sets[cmsSigCmyData].push_back(Component(_("_Y:"), _("Yellow"), 1));
+
+ for (auto & set : sets) {
+ knownColorspaces.insert(set.first);
+ maxColorspaceComponentCount = std::max(maxColorspaceComponentCount, set.second.size());
+ }
+ }
+
+ std::vector<Component> target;
+
+ if (sets.find(space) != sets.end()) {
+ target = sets[space];
+ }
+ return target;
+}
+
+
+std::vector<colorspace::Component> colorspace::getColorSpaceInfo(Inkscape::ColorProfile *prof)
+{
+ return getColorSpaceInfo(asICColorSpaceSig(prof->getColorSpace()));
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Class containing the parts for a single color component's UI presence.
+ */
+class ComponentUI {
+ public:
+ ComponentUI()
+ : _component()
+ , _adj(nullptr)
+ , _slider(nullptr)
+ , _btn(nullptr)
+ , _label(nullptr)
+ , _map(nullptr)
+ {
+ }
+
+ ComponentUI(colorspace::Component component)
+ : _component(std::move(component))
+ , _adj(nullptr)
+ , _slider(nullptr)
+ , _btn(nullptr)
+ , _label(nullptr)
+ , _map(nullptr)
+ {
+ }
+
+ colorspace::Component _component;
+ Glib::RefPtr<Gtk::Adjustment> _adj; // Component adjustment
+ Inkscape::UI::Widget::ColorSlider *_slider;
+ GtkWidget *_btn; // spinbutton
+ GtkWidget *_label; // Label
+ guchar *_map;
+};
+
+/**
+ * Class that implements the internals of the selector.
+ */
+class ColorICCSelectorImpl {
+ public:
+ ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color);
+
+ ~ColorICCSelectorImpl();
+
+ void _adjustmentChanged(Glib::RefPtr<Gtk::Adjustment> &adjustment);
+
+ void _sliderGrabbed();
+ void _sliderReleased();
+ void _sliderChanged();
+
+ static void _profileSelected(GtkWidget *src, gpointer data);
+ static void _fixupHit(GtkWidget *src, gpointer data);
+
+ void _setProfile(SVGICCColor *profile);
+ void _switchToProfile(gchar const *name);
+
+ void _updateSliders(gint ignore);
+ void _profilesChanged(std::string const &name);
+
+ ColorICCSelector *_owner;
+ SelectedColor &_color;
+
+ gboolean _updating : 1;
+ gboolean _dragging : 1;
+
+ guint32 _fixupNeeded;
+ GtkWidget *_fixupBtn;
+ GtkWidget *_profileSel;
+
+ std::vector<ComponentUI> _compUI;
+
+ Glib::RefPtr<Gtk::Adjustment> _adj; // Channel adjustment
+ Inkscape::UI::Widget::ColorSlider *_slider;
+ GtkWidget *_sbtn; // Spinbutton
+ GtkWidget *_label; // Label
+
+ std::string _profileName;
+ Inkscape::ColorProfile *_prof;
+ guint _profChannelCount;
+ gulong _profChangedID;
+};
+
+
+
+const gchar *ColorICCSelector::MODE_NAME = N_("CMS");
+
+ColorICCSelector::ColorICCSelector(SelectedColor &color)
+ : _impl(nullptr)
+{
+ _impl = new ColorICCSelectorImpl(this, color);
+ init();
+ color.signal_changed.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged));
+ // color.signal_dragged.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged));
+}
+
+ColorICCSelector::~ColorICCSelector()
+{
+ if (_impl) {
+ delete _impl;
+ _impl = nullptr;
+ }
+}
+
+
+
+ColorICCSelectorImpl::ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color)
+ : _owner(owner)
+ , _color(color)
+ , _updating(FALSE)
+ , _dragging(FALSE)
+ , _fixupNeeded(0)
+ , _fixupBtn(nullptr)
+ , _profileSel(nullptr)
+ , _compUI()
+ , _adj(nullptr)
+ , _slider(nullptr)
+ , _sbtn(nullptr)
+ , _label(nullptr)
+ , _profileName()
+ , _prof(nullptr)
+ , _profChannelCount(0)
+ , _profChangedID(0)
+{
+}
+
+ColorICCSelectorImpl::~ColorICCSelectorImpl()
+{
+ _sbtn = nullptr;
+ _label = nullptr;
+}
+
+void ColorICCSelector::init()
+{
+ gint row = 0;
+
+ _impl->_updating = FALSE;
+ _impl->_dragging = FALSE;
+
+ GtkWidget *t = GTK_WIDGET(gobj());
+
+ _impl->_compUI.clear();
+
+ // Create components
+ row = 0;
+
+
+ _impl->_fixupBtn = gtk_button_new_with_label(_("Fix"));
+ g_signal_connect(G_OBJECT(_impl->_fixupBtn), "clicked", G_CALLBACK(ColorICCSelectorImpl::_fixupHit),
+ (gpointer)_impl);
+ gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE);
+ gtk_widget_set_tooltip_text(_impl->_fixupBtn, _("Fix RGB fallback to match icc-color() value."));
+ gtk_widget_show(_impl->_fixupBtn);
+
+ attachToGridOrTable(t, _impl->_fixupBtn, 0, row, 1, 1);
+
+ // Combobox and store with 2 columns : label (0) and full name (1)
+ GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
+ _impl->_profileSel = _scrollprotected_combo_box_new_with_model(GTK_TREE_MODEL(store));
+
+ GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, TRUE);
+ gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, "text", 0, nullptr);
+
+ GtkTreeIter iter;
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1);
+
+ gtk_widget_show(_impl->_profileSel);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(_impl->_profileSel), 0);
+
+ attachToGridOrTable(t, _impl->_profileSel, 1, row, 1, 1);
+
+ _impl->_profChangedID = g_signal_connect(G_OBJECT(_impl->_profileSel), "changed",
+ G_CALLBACK(ColorICCSelectorImpl::_profileSelected), (gpointer)_impl);
+
+ row++;
+
+// populate the data for colorspaces and channels:
+ std::vector<colorspace::Component> things = colorspace::getColorSpaceInfo(cmsSigRgbData);
+
+ for (size_t i = 0; i < maxColorspaceComponentCount; i++) {
+ if (i < things.size()) {
+ _impl->_compUI.emplace_back(things[i]);
+ }
+ else {
+ _impl->_compUI.emplace_back();
+ }
+
+ std::string labelStr = (i < things.size()) ? things[i].name.c_str() : "";
+
+ _impl->_compUI[i]._label = gtk_label_new_with_mnemonic(labelStr.c_str());
+
+ gtk_widget_set_halign(_impl->_compUI[i]._label, GTK_ALIGN_END);
+ gtk_widget_show(_impl->_compUI[i]._label);
+ gtk_widget_set_no_show_all(_impl->_compUI[i]._label, TRUE);
+
+ attachToGridOrTable(t, _impl->_compUI[i]._label, 0, row, 1, 1);
+
+ // Adjustment
+ guint scaleValue = _impl->_compUI[i]._component.scale;
+ gdouble step = static_cast<gdouble>(scaleValue) / 100.0;
+ gdouble page = static_cast<gdouble>(scaleValue) / 10.0;
+ gint digits = (step > 0.9) ? 0 : 2;
+ _impl->_compUI[i]._adj = Gtk::Adjustment::create(0.0, 0.0, scaleValue, step, page, page);
+
+ // Slider
+ _impl->_compUI[i]._slider =
+ Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_impl->_compUI[i]._adj));
+ _impl->_compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : "");
+ _impl->_compUI[i]._slider->show();
+ _impl->_compUI[i]._slider->set_no_show_all();
+
+ attachToGridOrTable(t, _impl->_compUI[i]._slider->gobj(), 1, row, 1, 1, true);
+
+ auto spinbutton = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_impl->_compUI[i]._adj, step, digits));
+ _impl->_compUI[i]._btn = GTK_WIDGET(spinbutton->gobj());
+ gtk_widget_set_tooltip_text(_impl->_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : "");
+ sp_dialog_defocus_on_enter(_impl->_compUI[i]._btn);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_compUI[i]._label), _impl->_compUI[i]._btn);
+ gtk_widget_show(_impl->_compUI[i]._btn);
+ gtk_widget_set_no_show_all(_impl->_compUI[i]._btn, TRUE);
+
+ attachToGridOrTable(t, _impl->_compUI[i]._btn, 2, row, 1, 1, false, true);
+
+ _impl->_compUI[i]._map = g_new(guchar, 4 * 1024);
+ memset(_impl->_compUI[i]._map, 0x0ff, 1024 * 4);
+
+
+ // Signals
+ _impl->_compUI[i]._adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_adjustmentChanged), _impl->_compUI[i]._adj));
+
+ _impl->_compUI[i]._slider->signal_grabbed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderGrabbed));
+ _impl->_compUI[i]._slider->signal_released.connect(
+ sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderReleased));
+ _impl->_compUI[i]._slider->signal_value_changed.connect(
+ sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderChanged));
+
+ row++;
+ }
+
+ // Label
+ _impl->_label = gtk_label_new_with_mnemonic(_("_A:"));
+
+ gtk_widget_set_halign(_impl->_label, GTK_ALIGN_END);
+ gtk_widget_show(_impl->_label);
+
+ attachToGridOrTable(t, _impl->_label, 0, row, 1, 1);
+
+ // Adjustment
+ _impl->_adj = Gtk::Adjustment::create(0.0, 0.0, 100.0, 1.0, 10.0, 10.0);
+
+ // Slider
+ _impl->_slider = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_impl->_adj));
+ _impl->_slider->set_tooltip_text(_("Alpha (opacity)"));
+ _impl->_slider->show();
+
+ attachToGridOrTable(t, _impl->_slider->gobj(), 1, row, 1, 1, true);
+
+ _impl->_slider->setColors(SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.0), SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.5),
+ SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 1.0));
+
+
+ // Spinbutton
+ auto spinbuttonalpha = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_impl->_adj, 1.0));
+ _impl->_sbtn = GTK_WIDGET(spinbuttonalpha->gobj());
+ gtk_widget_set_tooltip_text(_impl->_sbtn, _("Alpha (opacity)"));
+ sp_dialog_defocus_on_enter(_impl->_sbtn);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_label), _impl->_sbtn);
+ gtk_widget_show(_impl->_sbtn);
+
+ attachToGridOrTable(t, _impl->_sbtn, 2, row, 1, 1, false, true);
+
+ // Signals
+ _impl->_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_adjustmentChanged), _impl->_adj));
+
+ _impl->_slider->signal_grabbed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderGrabbed));
+ _impl->_slider->signal_released.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderReleased));
+ _impl->_slider->signal_value_changed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderChanged));
+
+ gtk_widget_show(t);
+}
+
+void ColorICCSelectorImpl::_fixupHit(GtkWidget * /*src*/, gpointer data)
+{
+ ColorICCSelectorImpl *self = reinterpret_cast<ColorICCSelectorImpl *>(data);
+ gtk_widget_set_sensitive(self->_fixupBtn, FALSE);
+ self->_adjustmentChanged(self->_compUI[0]._adj);
+}
+
+void ColorICCSelectorImpl::_profileSelected(GtkWidget * /*src*/, gpointer data)
+{
+ ColorICCSelectorImpl *self = reinterpret_cast<ColorICCSelectorImpl *>(data);
+
+ GtkTreeIter iter;
+ if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(self->_profileSel), &iter)) {
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(self->_profileSel));
+ gchar *name = nullptr;
+
+ gtk_tree_model_get(store, &iter, 1, &name, -1);
+ self->_switchToProfile(name);
+ gtk_widget_set_tooltip_text(self->_profileSel, name);
+
+ g_free(name);
+ }
+}
+
+void ColorICCSelectorImpl::_switchToProfile(gchar const *name)
+{
+ bool dirty = false;
+ SPColor tmp(_color.color());
+
+ if (name) {
+ if (tmp.icc && tmp.icc->colorProfile == name) {
+#ifdef DEBUG_LCMS
+ g_message("Already at name [%s]", name);
+#endif // DEBUG_LCMS
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("Need to switch to profile [%s]", name);
+#endif // DEBUG_LCMS
+ if (tmp.icc) {
+ tmp.icc->colors.clear();
+ }
+ else {
+ tmp.icc = new SVGICCColor();
+ }
+ tmp.icc->colorProfile = name;
+ Inkscape::ColorProfile *newProf = SP_ACTIVE_DOCUMENT->getProfileManager()->find(name);
+ if (newProf) {
+ cmsHTRANSFORM trans = newProf->getTransfFromSRGB8();
+ if (trans) {
+ guint32 val = _color.color().toRGBA32(0);
+ guchar pre[4] = {
+ static_cast<guchar>(SP_RGBA32_R_U(val)),
+ static_cast<guchar>(SP_RGBA32_G_U(val)),
+ static_cast<guchar>(SP_RGBA32_B_U(val)),
+ 255};
+#ifdef DEBUG_LCMS
+ g_message("Shoving in [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]);
+#endif // DEBUG_LCMS
+ cmsUInt16Number post[4] = { 0, 0, 0, 0 };
+ cmsDoTransform(trans, pre, post, 1);
+#ifdef DEBUG_LCMS
+ g_message("got on out [%04x] [%04x] [%04x] [%04x]", post[0], post[1], post[2], post[3]);
+#endif // DEBUG_LCMS
+ guint count = cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace()));
+
+ std::vector<colorspace::Component> things =
+ colorspace::getColorSpaceInfo(asICColorSpaceSig(newProf->getColorSpace()));
+
+ for (guint i = 0; i < count; i++) {
+ gdouble val =
+ (((gdouble)post[i]) / 65535.0) * (gdouble)((i < things.size()) ? things[i].scale : 1);
+#ifdef DEBUG_LCMS
+ g_message(" scaled %d by %d to be %f", i, ((i < things.size()) ? things[i].scale : 1), val);
+#endif // DEBUG_LCMS
+ tmp.icc->colors.push_back(val);
+ }
+ cmsHTRANSFORM retrans = newProf->getTransfToSRGB8();
+ if (retrans) {
+ cmsDoTransform(retrans, post, pre, 1);
+#ifdef DEBUG_LCMS
+ g_message(" back out [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]);
+#endif // DEBUG_LCMS
+ tmp.set(SP_RGBA32_U_COMPOSE(pre[0], pre[1], pre[2], 0xff));
+ }
+
+ dirty = true;
+ }
+ }
+ }
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("NUKE THE ICC");
+#endif // DEBUG_LCMS
+ if (tmp.icc) {
+ delete tmp.icc;
+ tmp.icc = nullptr;
+ dirty = true;
+ _fixupHit(nullptr, this);
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("No icc to nuke");
+#endif // DEBUG_LCMS
+ }
+ }
+
+ if (dirty) {
+#ifdef DEBUG_LCMS
+ g_message("+----------------");
+ g_message("+ new color is [%s]", tmp.toString().c_str());
+#endif // DEBUG_LCMS
+ _setProfile(tmp.icc);
+ //_adjustmentChanged( _compUI[0]._adj, SP_COLOR_ICC_SELECTOR(_csel) );
+ _color.setColor(tmp);
+#ifdef DEBUG_LCMS
+ g_message("+_________________");
+#endif // DEBUG_LCMS
+ }
+}
+
+struct _cmp {
+ bool operator()(const SPObject * const & a, const SPObject * const & b)
+ {
+ const Inkscape::ColorProfile &a_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*a);
+ const Inkscape::ColorProfile &b_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*b);
+ gchar *a_name_casefold = g_utf8_casefold(a_prof.name, -1 );
+ gchar *b_name_casefold = g_utf8_casefold(b_prof.name, -1 );
+ int result = g_strcmp0(a_name_casefold, b_name_casefold);
+ g_free(a_name_casefold);
+ g_free(b_name_casefold);
+ return result < 0;
+ }
+};
+
+template <typename From, typename To>
+struct static_caster { To * operator () (From * value) const { return static_cast<To *>(value); } };
+
+void ColorICCSelectorImpl::_profilesChanged(std::string const &name)
+{
+ GtkComboBox *combo = GTK_COMBO_BOX(_profileSel);
+
+ g_signal_handler_block(G_OBJECT(_profileSel), _profChangedID);
+
+ GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo));
+ gtk_list_store_clear(store);
+
+ GtkTreeIter iter;
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1);
+
+ gtk_combo_box_set_active(combo, 0);
+
+ int index = 1;
+ std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList("iccprofile");
+
+ std::set<Inkscape::ColorProfile *> _current;
+ std::transform(current.begin(),
+ current.end(),
+ std::inserter(_current, _current.begin()),
+ static_caster<SPObject, Inkscape::ColorProfile>());
+
+ for (auto &it: _current) {
+ Inkscape::ColorProfile *prof = it;
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, ink_ellipsize_text(prof->name, 25).c_str(), 1, prof->name, -1);
+
+ if (name == prof->name) {
+ gtk_combo_box_set_active(combo, index);
+ gtk_widget_set_tooltip_text(_profileSel, prof->name);
+ }
+
+ index++;
+ }
+
+ g_signal_handler_unblock(G_OBJECT(_profileSel), _profChangedID);
+}
+
+void ColorICCSelector::on_show()
+{
+ Gtk::Grid::on_show();
+ _colorChanged();
+}
+
+// Helpers for setting color value
+
+void ColorICCSelector::_colorChanged()
+{
+ _impl->_updating = TRUE;
+// sp_color_icc_set_color( SP_COLOR_ICC( _icc ), &color );
+
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_colorChanged(%08x:%s)", this, _impl->_color.color().toRGBA32(_impl->_color.alpha()),
+ ((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+
+#ifdef DEBUG_LCMS
+ g_message("FLIPPIES!!!! %p '%s'", _impl->_color.color().icc,
+ (_impl->_color.color().icc ? _impl->_color.color().icc->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+
+ _impl->_profilesChanged((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile : std::string(""));
+ ColorScales<>::setScaled(_impl->_adj, _impl->_color.alpha());
+
+ _impl->_setProfile(_impl->_color.color().icc);
+ _impl->_fixupNeeded = 0;
+ gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE);
+
+ if (_impl->_prof) {
+ if (_impl->_prof->getTransfToSRGB8()) {
+ cmsUInt16Number tmp[4];
+ for (guint i = 0; i < _impl->_profChannelCount; i++) {
+ gdouble val = 0.0;
+ if (_impl->_color.color().icc->colors.size() > i) {
+ if (_impl->_compUI[i]._component.scale == 256) {
+ val = (_impl->_color.color().icc->colors[i] + 128.0) /
+ static_cast<gdouble>(_impl->_compUI[i]._component.scale);
+ }
+ else {
+ val = _impl->_color.color().icc->colors[i] /
+ static_cast<gdouble>(_impl->_compUI[i]._component.scale);
+ }
+ }
+ tmp[i] = val * 0x0ffff;
+ }
+ guchar post[4] = { 0, 0, 0, 0 };
+ cmsHTRANSFORM trans = _impl->_prof->getTransfToSRGB8();
+ if (trans) {
+ cmsDoTransform(trans, tmp, post, 1);
+ guint32 other = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255);
+ if (other != _impl->_color.color().toRGBA32(255)) {
+ _impl->_fixupNeeded = other;
+ gtk_widget_set_sensitive(_impl->_fixupBtn, TRUE);
+#ifdef DEBUG_LCMS
+ g_message("Color needs to change 0x%06x to 0x%06x", _color.toRGBA32(255) >> 8, other >> 8);
+#endif // DEBUG_LCMS
+ }
+ }
+ }
+ }
+ _impl->_updateSliders(-1);
+
+
+ _impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_colorChanged()", this);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_setProfile(SVGICCColor *profile)
+{
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_setProfile(%s)", this, ((profile) ? profile->colorProfile.c_str() : "<null>"));
+#endif // DEBUG_LCMS
+ bool profChanged = false;
+ if (_prof && (!profile || (_profileName != profile->colorProfile))) {
+ // Need to clear out the prior one
+ profChanged = true;
+ _profileName.clear();
+ _prof = nullptr;
+ _profChannelCount = 0;
+ }
+ else if (profile && !_prof) {
+ profChanged = true;
+ }
+
+ for (auto & i : _compUI) {
+ gtk_widget_hide(i._label);
+ i._slider->hide();
+ gtk_widget_hide(i._btn);
+ }
+
+ if (profile) {
+ _prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(profile->colorProfile.c_str());
+ if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) {
+ _profChannelCount = cmsChannelsOf(asICColorSpaceSig(_prof->getColorSpace()));
+
+ if (profChanged) {
+ std::vector<colorspace::Component> things =
+ colorspace::getColorSpaceInfo(asICColorSpaceSig(_prof->getColorSpace()));
+ for (size_t i = 0; (i < things.size()) && (i < _profChannelCount); ++i) {
+ _compUI[i]._component = things[i];
+ }
+
+ for (guint i = 0; i < _profChannelCount; i++) {
+ gtk_label_set_text_with_mnemonic(GTK_LABEL(_compUI[i]._label),
+ (i < things.size()) ? things[i].name.c_str() : "");
+
+ _compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : "");
+ gtk_widget_set_tooltip_text(_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : "");
+
+ _compUI[i]._slider->setColors(SPColor(0.0, 0.0, 0.0).toRGBA32(0xff),
+ SPColor(0.5, 0.5, 0.5).toRGBA32(0xff),
+ SPColor(1.0, 1.0, 1.0).toRGBA32(0xff));
+ /*
+ _compUI[i]._adj = GTK_ADJUSTMENT( gtk_adjustment_new( val, 0.0, _fooScales[i],
+ step, page, page ) );
+ g_signal_connect( G_OBJECT( _compUI[i]._adj ), "value_changed", G_CALLBACK(
+ _adjustmentChanged ), _csel );
+
+ sp_color_slider_set_adjustment( SP_COLOR_SLIDER(_compUI[i]._slider),
+ _compUI[i]._adj );
+ gtk_spin_button_set_adjustment( GTK_SPIN_BUTTON(_compUI[i]._btn),
+ _compUI[i]._adj );
+ gtk_spin_button_set_digits( GTK_SPIN_BUTTON(_compUI[i]._btn), digits );
+ */
+ gtk_widget_show(_compUI[i]._label);
+ _compUI[i]._slider->show();
+ gtk_widget_show(_compUI[i]._btn);
+ // gtk_adjustment_set_value( _compUI[i]._adj, 0.0 );
+ // gtk_adjustment_set_value( _compUI[i]._adj, val );
+ }
+ for (size_t i = _profChannelCount; i < _compUI.size(); i++) {
+ gtk_widget_hide(_compUI[i]._label);
+ _compUI[i]._slider->hide();
+ gtk_widget_hide(_compUI[i]._btn);
+ }
+ }
+ }
+ else {
+ // Give up for now on named colors
+ _prof = nullptr;
+ }
+ }
+
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_setProfile()", this);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_updateSliders(gint ignore)
+{
+ if (_color.color().icc) {
+ for (guint i = 0; i < _profChannelCount; i++) {
+ gdouble val = 0.0;
+ if (_color.color().icc->colors.size() > i) {
+ if (_compUI[i]._component.scale == 256) {
+ val = (_color.color().icc->colors[i] + 128.0) / static_cast<gdouble>(_compUI[i]._component.scale);
+ }
+ else {
+ val = _color.color().icc->colors[i] / static_cast<gdouble>(_compUI[i]._component.scale);
+ }
+ }
+ _compUI[i]._adj->set_value(val);
+ }
+
+ if (_prof) {
+ if (_prof->getTransfToSRGB8()) {
+ for (guint i = 0; i < _profChannelCount; i++) {
+ if (static_cast<gint>(i) != ignore) {
+ cmsUInt16Number *scratch = getScratch();
+ cmsUInt16Number filler[4] = { 0, 0, 0, 0 };
+ for (guint j = 0; j < _profChannelCount; j++) {
+ filler[j] = 0x0ffff * ColorScales<>::getScaled(_compUI[j]._adj);
+ }
+
+ cmsUInt16Number *p = scratch;
+ for (guint x = 0; x < 1024; x++) {
+ for (guint j = 0; j < _profChannelCount; j++) {
+ if (j == i) {
+ *p++ = x * 0x0ffff / 1024;
+ }
+ else {
+ *p++ = filler[j];
+ }
+ }
+ }
+
+ cmsHTRANSFORM trans = _prof->getTransfToSRGB8();
+ if (trans) {
+ cmsDoTransform(trans, scratch, _compUI[i]._map, 1024);
+ if (_compUI[i]._slider)
+ {
+ _compUI[i]._slider->setMap(_compUI[i]._map);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ guint32 start = _color.color().toRGBA32(0x00);
+ guint32 mid = _color.color().toRGBA32(0x7f);
+ guint32 end = _color.color().toRGBA32(0xff);
+
+ _slider->setColors(start, mid, end);
+}
+
+
+void ColorICCSelectorImpl::_adjustmentChanged(Glib::RefPtr<Gtk::Adjustment> &adjustment)
+{
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_adjustmentChanged()", this);
+#endif // DEBUG_LCMS
+
+ ColorICCSelector *iccSelector = _owner;
+ if (iccSelector->_impl->_updating) {
+ return;
+ }
+
+ iccSelector->_impl->_updating = TRUE;
+
+ gint match = -1;
+
+ SPColor newColor(iccSelector->_impl->_color.color());
+ gfloat scaled = ColorScales<>::getScaled(iccSelector->_impl->_adj);
+ if (iccSelector->_impl->_adj == adjustment) {
+#ifdef DEBUG_LCMS
+ g_message("ALPHA");
+#endif // DEBUG_LCMS
+ }
+ else {
+ for (size_t i = 0; i < iccSelector->_impl->_compUI.size(); i++) {
+ if (iccSelector->_impl->_compUI[i]._adj == adjustment) {
+ match = i;
+ break;
+ }
+ }
+ if (match >= 0) {
+#ifdef DEBUG_LCMS
+ g_message(" channel %d", match);
+#endif // DEBUG_LCMS
+ }
+
+
+ cmsUInt16Number tmp[4];
+ for (guint i = 0; i < 4; i++) {
+ tmp[i] = ColorScales<>::getScaled(iccSelector->_impl->_compUI[i]._adj) * 0x0ffff;
+ }
+ guchar post[4] = { 0, 0, 0, 0 };
+
+ cmsHTRANSFORM trans = iccSelector->_impl->_prof->getTransfToSRGB8();
+ if (trans) {
+ cmsDoTransform(trans, tmp, post, 1);
+ }
+
+ SPColor other(SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255));
+ other.icc = new SVGICCColor();
+ if (iccSelector->_impl->_color.color().icc) {
+ other.icc->colorProfile = iccSelector->_impl->_color.color().icc->colorProfile;
+ }
+
+ guint32 prior = iccSelector->_impl->_color.color().toRGBA32(255);
+ guint32 newer = other.toRGBA32(255);
+
+ if (prior != newer) {
+#ifdef DEBUG_LCMS
+ g_message("Transformed color from 0x%08x to 0x%08x", prior, newer);
+ g_message(" ~~~~ FLIP");
+#endif // DEBUG_LCMS
+ newColor = other;
+ newColor.icc->colors.clear();
+ for (guint i = 0; i < iccSelector->_impl->_profChannelCount; i++) {
+ gdouble val = ColorScales<>::getScaled(iccSelector->_impl->_compUI[i]._adj);
+ val *= iccSelector->_impl->_compUI[i]._component.scale;
+ if (iccSelector->_impl->_compUI[i]._component.scale == 256) {
+ val -= 128;
+ }
+ newColor.icc->colors.push_back(val);
+ }
+ }
+ }
+ iccSelector->_impl->_color.setColorAlpha(newColor, scaled);
+ // iccSelector->_updateInternals( newColor, scaled, iccSelector->_impl->_dragging );
+ iccSelector->_impl->_updateSliders(match);
+
+ iccSelector->_impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_adjustmentChanged()", cs);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_sliderGrabbed()
+{
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+ // if (!iccSelector->_dragging) {
+ // iccSelector->_dragging = TRUE;
+ // iccSelector->_grabbed();
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_impl->_adj ),
+ // iccSelector->_dragging );
+ // }
+}
+
+void ColorICCSelectorImpl::_sliderReleased()
+{
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+ // if (iccSelector->_dragging) {
+ // iccSelector->_dragging = FALSE;
+ // iccSelector->_released();
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_adj ),
+ // iccSelector->_dragging );
+ // }
+}
+
+#ifdef DEBUG_LCMS
+void ColorICCSelectorImpl::_sliderChanged(SPColorSlider *slider, SPColorICCSelector *cs)
+#else
+void ColorICCSelectorImpl::_sliderChanged()
+#endif // DEBUG_LCMS
+{
+#ifdef DEBUG_LCMS
+ g_message("Changed %p and %p", slider, cs);
+#endif // DEBUG_LCMS
+ // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base);
+
+ // iccSelector->_updateInternals( iccSelector->_color, ColorScales<>::getScaled( iccSelector->_adj ),
+ // iccSelector->_dragging );
+}
+
+Gtk::Widget *ColorICCSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorICCSelector(color));
+ return w;
+}
+
+Glib::ustring ColorICCSelectorFactory::modeName() const { return gettext(ColorICCSelector::MODE_NAME); }
+}
+}
+}
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/color-icc-selector.h b/src/ui/widget/color-icc-selector.h
new file mode 100644
index 0000000..2c5ec41
--- /dev/null
+++ b/src/ui/widget/color-icc-selector.h
@@ -0,0 +1,75 @@
+// 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_ICC_SELECTOR_H
+#define SEEN_SP_COLOR_ICC_SELECTOR_H
+
+#include <gtkmm/widget.h>
+#include <gtkmm/grid.h>
+
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+
+class ColorProfile;
+
+namespace UI {
+namespace Widget {
+
+class ColorICCSelectorImpl;
+
+class ColorICCSelector
+ : public Gtk::Grid
+ {
+ public:
+ static const gchar *MODE_NAME;
+
+ ColorICCSelector(SelectedColor &color);
+ ~ColorICCSelector() override;
+
+ virtual void init();
+
+ protected:
+ void on_show() override;
+
+ virtual void _colorChanged();
+
+ void _recalcColor(gboolean changing);
+
+ private:
+ friend class ColorICCSelectorImpl;
+
+ // By default, disallow copy constructor and assignment operator
+ ColorICCSelector(const ColorICCSelector &obj);
+ ColorICCSelector &operator=(const ColorICCSelector &obj);
+
+ ColorICCSelectorImpl *_impl;
+};
+
+
+class ColorICCSelectorFactory : public ColorSelectorFactory {
+ public:
+ Gtk::Widget *createWidget(SelectedColor &color) const override;
+ Glib::ustring modeName() const override;
+};
+}
+}
+}
+#endif // SEEN_SP_COLOR_ICC_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/ui/widget/color-notebook.cpp b/src/ui/widget/color-notebook.cpp
new file mode 100644
index 0000000..e0dcd16
--- /dev/null
+++ b/src/ui/widget/color-notebook.cpp
@@ -0,0 +1,347 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages
+ *//*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification)
+ *
+ * Copyright (C) 2001-2014 Authors
+ * 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
+
+#undef SPCS_PREVIEW
+#define noDUMP_CHANGE_INFO
+
+#include <glibmm/i18n.h>
+#include <gtkmm/label.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+
+#include "cms-system.h"
+#include "document.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "profile-manager.h"
+
+#include "object/color-profile.h"
+#include "ui/icon-loader.h"
+
+#include "svg/svg-icc-color.h"
+
+#include "ui/dialog-events.h"
+#include "ui/tools/dropper-tool.h"
+#include "ui/widget/color-entry.h"
+#include "ui/widget/color-icc-selector.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/color-scales.h"
+//#include "ui/widget/color-wheel-selector.h"
+//#include "ui/widget/color-wheel-hsluv-selector.h"
+
+#include "widgets/spw-utilities.h"
+
+using Inkscape::CMSSystem;
+
+#define XPAD 2
+#define YPAD 1
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+ColorNotebook::ColorNotebook(SelectedColor &color)
+ : Gtk::Grid()
+ , _selected_color(color)
+{
+ set_name("ColorNotebook");
+
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSL>, "color-selector-hsx"));
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSV>, "color-selector-hsx"));
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::RGB>, "color-selector-rgb"));
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::CMYK>, "color-selector-cmyk"));
+ _available_pages.push_back(new Page(new ColorScalesFactory<SPColorScalesMode::HSLUV>, "color-selector-hsluv"));
+ //_available_pages.push_back(new Page(new ColorWheelSelectorFactory, "color-selector-wheel"));
+ _available_pages.push_back(new Page(new ColorICCSelectorFactory, "color-selector-cms"));
+
+ _initUI();
+
+ _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged));
+ _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged));
+}
+
+ColorNotebook::~ColorNotebook()
+{
+ if (_onetimepick)
+ _onetimepick.disconnect();
+}
+
+ColorNotebook::Page::Page(Inkscape::UI::ColorSelectorFactory *selector_factory, const char* icon)
+ : selector_factory(selector_factory), icon_name(icon)
+{
+}
+
+void ColorNotebook::set_label(const Glib::ustring& label) {
+ _label->set_markup(label);
+}
+
+void ColorNotebook::_initUI()
+{
+ guint row = 0;
+
+ _book = Gtk::make_managed<Gtk::Stack>();
+ _book->show();
+ _book->set_transition_type(Gtk::STACK_TRANSITION_TYPE_CROSSFADE);
+ _book->set_transition_duration(130);
+
+ // mode selection switcher widget shows all buttons for color mode selection, side by side
+ _switcher = Gtk::make_managed<Gtk::StackSwitcher>();
+ _switcher->set_stack(*_book);
+ // cannot leave it homogeneous - in some themes switcher gets very wide
+ _switcher->set_homogeneous(false);
+ _switcher->set_halign(Gtk::ALIGN_CENTER);
+ _switcher->show();
+ attach(*_switcher, 0, row++, 2);
+
+ _buttonbox = Gtk::make_managed<Gtk::Box>();
+ _buttonbox->show();
+
+ // combo mode selection is compact and only shows one entry (active)
+ _combo = Gtk::manage(new IconComboBox());
+ _combo->set_can_focus(false);
+ _combo->set_visible();
+ _combo->set_tooltip_text(_("Choose style of color selection"));
+
+ for (auto&& page : _available_pages) {
+ _addPage(page);
+ }
+
+ _label = Gtk::make_managed<Gtk::Label>();
+ _label->set_visible();
+ _buttonbox->pack_start(*_label, false, true);
+ _buttonbox->pack_end(*_combo, false, false);
+ _combo->signal_changed().connect([=](){ _setCurrentPage(_combo->get_active_row_id(), false); });
+
+ _buttonbox->set_margin_start(XPAD);
+ _buttonbox->set_margin_end(XPAD);
+ _buttonbox->set_margin_top(YPAD);
+ _buttonbox->set_margin_bottom(YPAD);
+ _buttonbox->set_hexpand();
+ _buttonbox->set_valign(Gtk::ALIGN_START);
+ attach(*_buttonbox, 0, row, 2);
+
+ row++;
+
+ _book->set_margin_start(XPAD);
+ _book->set_margin_end(XPAD);
+ _book->set_margin_top(YPAD);
+ _book->set_margin_bottom(YPAD);
+ _book->set_hexpand();
+ _book->set_vexpand();
+ attach(*_book, 0, row, 2, 1);
+
+ // restore the last active page
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _setCurrentPage(prefs->getInt("/colorselector/page", 0), true);
+ row++;
+
+ auto switcher_path = Glib::ustring("/colorselector/switcher");
+ auto choose_switch = [=](bool compact) {
+ if (compact) {
+ _switcher->hide();
+ _buttonbox->show();
+ }
+ else {
+ _buttonbox->hide();
+ _switcher->show();
+ }
+ };
+
+ _observer = prefs->createObserver(switcher_path, [=](const Preferences::Entry& new_value) {
+ choose_switch(new_value.getBool());
+ });
+
+ choose_switch(prefs->getBool(switcher_path));
+
+ GtkWidget *rgbabox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+ /* Create color management icons */
+ _box_colormanaged = gtk_event_box_new();
+ GtkWidget *colormanaged = sp_get_icon_image("color-management", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_container_add(GTK_CONTAINER(_box_colormanaged), colormanaged);
+ gtk_widget_set_tooltip_text(_box_colormanaged, _("Color Managed"));
+ gtk_widget_set_sensitive(_box_colormanaged, false);
+ gtk_box_pack_start(GTK_BOX(rgbabox), _box_colormanaged, FALSE, FALSE, 2);
+
+ _box_outofgamut = gtk_event_box_new();
+ GtkWidget *outofgamut = sp_get_icon_image("out-of-gamut-icon", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_container_add(GTK_CONTAINER(_box_outofgamut), outofgamut);
+ gtk_widget_set_tooltip_text(_box_outofgamut, _("Out of gamut!"));
+ gtk_widget_set_sensitive(_box_outofgamut, false);
+ gtk_box_pack_start(GTK_BOX(rgbabox), _box_outofgamut, FALSE, FALSE, 2);
+
+ _box_toomuchink = gtk_event_box_new();
+ GtkWidget *toomuchink = sp_get_icon_image("too-much-ink-icon", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_container_add(GTK_CONTAINER(_box_toomuchink), toomuchink);
+ gtk_widget_set_tooltip_text(_box_toomuchink, _("Too much ink!"));
+ gtk_widget_set_sensitive(_box_toomuchink, false);
+ gtk_box_pack_start(GTK_BOX(rgbabox), _box_toomuchink, FALSE, FALSE, 2);
+
+
+ /* Color picker */
+ GtkWidget *picker = sp_get_icon_image("color-picker", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ _btn_picker = gtk_button_new();
+ gtk_button_set_relief(GTK_BUTTON(_btn_picker), GTK_RELIEF_NONE);
+ gtk_container_add(GTK_CONTAINER(_btn_picker), picker);
+ gtk_widget_set_tooltip_text(_btn_picker, _("Pick colors from image"));
+ gtk_box_pack_start(GTK_BOX(rgbabox), _btn_picker, FALSE, FALSE, 2);
+ g_signal_connect(G_OBJECT(_btn_picker), "clicked", G_CALLBACK(ColorNotebook::_onPickerClicked), this);
+
+ /* Create RGBA entry and color preview */
+ _rgbal = gtk_label_new_with_mnemonic(_("RGBA_:"));
+ gtk_widget_set_halign(_rgbal, GTK_ALIGN_END);
+ gtk_box_pack_start(GTK_BOX(rgbabox), _rgbal, TRUE, TRUE, 2);
+
+ ColorEntry *rgba_entry = Gtk::manage(new ColorEntry(_selected_color));
+ sp_dialog_defocus_on_enter(GTK_WIDGET(rgba_entry->gobj()));
+ gtk_box_pack_start(GTK_BOX(rgbabox), GTK_WIDGET(rgba_entry->gobj()), FALSE, FALSE, 0);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(_rgbal), GTK_WIDGET(rgba_entry->gobj()));
+
+ gtk_widget_show_all(rgbabox);
+
+ // the "too much ink" icon is initially hidden
+ gtk_widget_hide(GTK_WIDGET(_box_toomuchink));
+
+ gtk_widget_set_margin_start(rgbabox, XPAD);
+ gtk_widget_set_margin_end(rgbabox, XPAD);
+ gtk_widget_set_margin_top(rgbabox, YPAD);
+ gtk_widget_set_margin_bottom(rgbabox, YPAD);
+ attach(*Glib::wrap(rgbabox), 0, row, 2, 1);
+
+#ifdef SPCS_PREVIEW
+ _p = sp_color_preview_new(0xffffffff);
+ gtk_widget_show(_p);
+ attach(*Glib::wrap(_p), 2, 3, row, row + 1, Gtk::FILL, Gtk::FILL, XPAD, YPAD);
+#endif
+}
+
+void ColorNotebook::_onPickerClicked(GtkWidget * /*widget*/, ColorNotebook *colorbook)
+{
+ // Set the dropper into a "one click" mode, so it reverts to the previous tool after a click
+ if (colorbook->_onetimepick) {
+ colorbook->_onetimepick.disconnect();
+ }
+ else {
+ Inkscape::UI::Tools::sp_toggle_dropper(SP_ACTIVE_DESKTOP);
+ auto tool = dynamic_cast<Inkscape::UI::Tools::DropperTool *>(SP_ACTIVE_DESKTOP->event_context);
+ if (tool) {
+ colorbook->_onetimepick = tool->onetimepick_signal.connect(sigc::mem_fun(*colorbook, &ColorNotebook::_pickColor));
+ }
+ }
+}
+
+void ColorNotebook::_pickColor(ColorRGBA *color) {
+ // Set color to color notebook here.
+ _selected_color.setValue(color->getIntValue());
+ _onSelectedColorChanged();
+}
+
+void ColorNotebook::_onSelectedColorChanged() { _updateICCButtons(); }
+
+void ColorNotebook::_onPageSwitched(int page_num)
+{
+ if (get_visible()) {
+ // remember the page we switched to
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/colorselector/page", page_num);
+ }
+}
+
+
+// TODO pass in param so as to avoid the need for SP_ACTIVE_DOCUMENT
+void ColorNotebook::_updateICCButtons()
+{
+ SPColor color = _selected_color.color();
+ gfloat alpha = _selected_color.alpha();
+
+ g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0));
+
+ /* update color management icon*/
+ gtk_widget_set_sensitive(_box_colormanaged, color.icc != nullptr);
+
+ /* update out-of-gamut icon */
+ gtk_widget_set_sensitive(_box_outofgamut, false);
+ if (color.icc) {
+ Inkscape::ColorProfile *target_profile =
+ SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str());
+ if (target_profile)
+ gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color));
+ }
+
+ /* update too-much-ink icon */
+ gtk_widget_set_sensitive(_box_toomuchink, false);
+ if (color.icc) {
+ Inkscape::ColorProfile *prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str());
+ if (prof && CMSSystem::isPrintColorSpace(prof)) {
+ gtk_widget_show(GTK_WIDGET(_box_toomuchink));
+ double ink_sum = 0;
+ for (double i : color.icc->colors) {
+ ink_sum += i;
+ }
+
+ /* Some literature states that when the sum of paint values exceed 320%, it is considered to be a satured color,
+ which means the paper can get too wet due to an excessive amount of ink. This may lead to several issues
+ such as misalignment and poor quality of printing in general.*/
+ if (ink_sum > 3.2)
+ gtk_widget_set_sensitive(_box_toomuchink, true);
+ }
+ else {
+ gtk_widget_hide(GTK_WIDGET(_box_toomuchink));
+ }
+ }
+}
+
+void ColorNotebook::_setCurrentPage(int i, bool sync_combo)
+{
+ const auto pages = _book->get_children();
+ if (i >= 0 && i < pages.size()) {
+ _book->set_visible_child(*pages[i]);
+ if (sync_combo) {
+ _combo->set_active_by_id(i);
+ }
+ _onPageSwitched(i);
+ }
+}
+
+void ColorNotebook::_addPage(Page &page)
+{
+ if (auto selector_widget = page.selector_factory->createWidget(_selected_color)) {
+ selector_widget->show();
+
+ Glib::ustring mode_name = page.selector_factory->modeName();
+ _book->add(*selector_widget, mode_name, mode_name);
+ int page_num = _book->get_children().size() - 1;
+
+ _combo->add_row(page.icon_name, mode_name, page_num);
+ }
+}
+
+}
+}
+}
+
+/*
+ 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/ui/widget/color-notebook.h b/src/ui/widget/color-notebook.h
new file mode 100644
index 0000000..938ab87
--- /dev/null
+++ b/src/ui/widget/color-notebook.h
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages
+ *//*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification)
+ *
+ * Copyright (C) 2001-2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_SP_COLOR_NOTEBOOK_H
+#define SEEN_SP_COLOR_NOTEBOOK_H
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <boost/ptr_container/ptr_vector.hpp>
+#include <gtkmm/grid.h>
+#include <gtkmm/stack.h>
+#include <gtkmm/stackswitcher.h>
+#include <glib.h>
+
+#include "color.h"
+#include "color-rgba.h"
+#include "preferences.h"
+#include "ui/selected-color.h"
+#include "ui/widget/icon-combobox.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorNotebook
+ : public Gtk::Grid
+{
+public:
+ ColorNotebook(SelectedColor &color);
+ ~ColorNotebook() override;
+
+ void set_label(const Glib::ustring& label);
+
+protected:
+ struct Page {
+ Page(Inkscape::UI::ColorSelectorFactory *selector_factory, const char* icon);
+
+ std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory;
+ Glib::ustring icon_name;
+ };
+
+ virtual void _initUI();
+ void _addPage(Page &page);
+
+ void _pickColor(ColorRGBA *color);
+ static void _onPickerClicked(GtkWidget *widget, ColorNotebook *colorbook);
+ void _onPageSwitched(int page_num);
+ virtual void _onSelectedColorChanged();
+
+ void _updateICCButtons();
+ void _setCurrentPage(int i, bool sync_combo);
+
+ Inkscape::UI::SelectedColor &_selected_color;
+ gulong _entryId;
+ Gtk::Stack* _book;
+ Gtk::StackSwitcher* _switcher;
+ Gtk::Box* _buttonbox;
+ Gtk::Label* _label;
+ GtkWidget *_rgbal; /* RGBA entry */
+ GtkWidget *_box_outofgamut, *_box_colormanaged, *_box_toomuchink;
+ GtkWidget *_btn_picker;
+ GtkWidget *_p; /* Color preview */
+ boost::ptr_vector<Page> _available_pages;
+ sigc::connection _onetimepick;
+ IconComboBox* _combo = nullptr;
+
+private:
+ // By default, disallow copy constructor and assignment operator
+ ColorNotebook(const ColorNotebook &obj) = delete;
+ ColorNotebook &operator=(const ColorNotebook &obj) = delete;
+
+ PrefObserver _observer;
+};
+
+}
+}
+}
+#endif // SEEN_SP_COLOR_NOTEBOOK_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/ui/widget/color-palette.cpp b/src/ui/widget/color-palette.cpp
new file mode 100644
index 0000000..a760f71
--- /dev/null
+++ b/src/ui/widget/color-palette.cpp
@@ -0,0 +1,618 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cssprovider.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/scrollbar.h>
+
+#include "color-palette.h"
+#include "ui/builder-utils.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorPalette::ColorPalette():
+ _builder(create_builder("color-palette.glade")),
+ _flowbox(get_widget<Gtk::FlowBox>(_builder, "flow-box")),
+ _menu(get_widget<Gtk::Menu>(_builder, "menu")),
+ _scroll_btn(get_widget<Gtk::FlowBox>(_builder, "scroll-buttons")),
+ _scroll_left(get_widget<Gtk::Button>(_builder, "btn-left")),
+ _scroll_right(get_widget<Gtk::Button>(_builder, "btn-right")),
+ _scroll_up(get_widget<Gtk::Button>(_builder, "btn-up")),
+ _scroll_down(get_widget<Gtk::Button>(_builder, "btn-down")),
+ _scroll(get_widget<Gtk::ScrolledWindow>(_builder, "scroll-wnd"))
+ {
+
+ auto& box = get_widget<Gtk::Box>(_builder, "palette-box");
+ this->add(box);
+
+ auto& config = get_widget<Gtk::MenuItem>(_builder, "config");
+ auto& dlg = get_widget<Gtk::Popover>(_builder, "config-popup");
+ config.signal_activate().connect([=,&dlg](){
+ dlg.popup();
+ });
+
+ auto& size = get_widget<Gtk::Scale>(_builder, "size-slider");
+ size.signal_change_value().connect([=,&size](Gtk::ScrollType, double val) {
+ _set_tile_size(static_cast<int>(size.get_value()));
+ _signal_settings_changed.emit();
+ return true;
+ });
+
+ auto& aspect = get_widget<Gtk::Scale>(_builder, "aspect-slider");
+ aspect.signal_change_value().connect([=,&aspect](Gtk::ScrollType, double val) {
+ _set_aspect(aspect.get_value());
+ _signal_settings_changed.emit();
+ return true;
+ });
+
+ auto& border = get_widget<Gtk::Scale>(_builder, "border-slider");
+ border.signal_change_value().connect([=,&border](Gtk::ScrollType, double val) {
+ _set_tile_border(static_cast<int>(border.get_value()));
+ _signal_settings_changed.emit();
+ return true;
+ });
+
+ auto& rows = get_widget<Gtk::Scale>(_builder, "row-slider");
+ rows.signal_change_value().connect([=,&rows](Gtk::ScrollType, double val) {
+ _set_rows(static_cast<int>(rows.get_value()));
+ _signal_settings_changed.emit();
+ return true;
+ });
+
+ auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb");
+ sb.set_active(_force_scrollbar);
+ sb.signal_toggled().connect([=,&sb](){
+ _enable_scrollbar(sb.get_active());
+ _signal_settings_changed.emit();
+ });
+ update_checkbox();
+
+ auto& stretch = get_widget<Gtk::CheckButton>(_builder, "stretch");
+ stretch.set_active(_force_scrollbar);
+ stretch.signal_toggled().connect([=,&stretch](){
+ _enable_stretch(stretch.get_active());
+ _signal_settings_changed.emit();
+ });
+ update_stretch();
+
+ _scroll.set_min_content_height(1);
+
+ // set style for small buttons; we need them reasonably small, since they impact min height of color palette strip
+ {
+ auto css_provider = Gtk::CssProvider::create();
+ css_provider->load_from_data(
+ ".small {"
+ " padding: 1px;"
+ " margin: 0;"
+ "}"
+ );
+
+ auto& btn_menu = get_widget<Gtk::MenuButton>(_builder, "btn-menu");
+ Gtk::Widget* small_buttons[5] = {&_scroll_up, &_scroll_down, &_scroll_left, &_scroll_right, &btn_menu};
+ for (auto button : small_buttons) {
+ button->get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+ }
+
+ _scroll_down.signal_clicked().connect([=](){ scroll(0, get_palette_height(), get_tile_height() + _border, true); });
+ _scroll_up.signal_clicked().connect([=](){ scroll(0, -get_palette_height(), get_tile_height() + _border, true); });
+ _scroll_left.signal_clicked().connect([=](){ scroll(-10 * (get_tile_width() + _border), 0, 0.0, false); });
+ _scroll_right.signal_clicked().connect([=](){ scroll(10 * (get_tile_width() + _border), 0, 0.0, false); });
+
+ {
+ auto css_provider = Gtk::CssProvider::create();
+ css_provider->load_from_data(
+ "flowbox, scrolledwindow {"
+ " padding: 0;"
+ " border: 0;"
+ " margin: 0;"
+ " min-width: 1px;"
+ " min-height: 1px;"
+ "}");
+ _scroll.get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ _flowbox.get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+
+ // remove padding/margins from FlowBoxChild widgets, so previews can be adjacent to each other
+ {
+ auto css_provider = Gtk::CssProvider::create();
+ css_provider->load_from_data(
+ ".color-palette-main-box flowboxchild {"
+ " padding: 0;"
+ " border: 0;"
+ " margin: 0;"
+ " min-width: 1px;"
+ " min-height: 1px;"
+ "}");
+ get_style_context()->add_provider_for_screen(this->get_screen(), css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+
+ set_vexpand_set(true);
+ set_up_scrolling();
+
+ signal_size_allocate().connect([=](Gtk::Allocation& a){ set_up_scrolling(); });
+}
+
+ColorPalette::~ColorPalette() {
+ if (_active_timeout) {
+ g_source_remove(_active_timeout);
+ }
+}
+
+void ColorPalette::do_scroll(int dx, int dy) {
+ if (auto vert = _scroll.get_vscrollbar()) {
+ vert->set_value(vert->get_value() + dy);
+ }
+ if (auto horz = _scroll.get_hscrollbar()) {
+ horz->set_value(horz->get_value() + dx);
+ }
+}
+
+std::pair<double, double> get_range(Gtk::Scrollbar& sb) {
+ auto adj = sb.get_adjustment();
+ return std::make_pair(adj->get_lower(), adj->get_upper() - adj->get_page_size());
+}
+
+gboolean ColorPalette::scroll_cb(gpointer self) {
+ auto ptr = static_cast<ColorPalette*>(self);
+ bool fire_again = false;
+
+ if (auto vert = ptr->_scroll.get_vscrollbar()) {
+ auto value = vert->get_value();
+ // is this the final adjustment step?
+ if (fabs(ptr->_scroll_final - value) < fabs(ptr->_scroll_step)) {
+ vert->set_value(ptr->_scroll_final);
+ fire_again = false; // cancel timer
+ }
+ else {
+ auto pos = value + ptr->_scroll_step;
+ vert->set_value(pos);
+ auto range = get_range(*vert);
+ if (pos > range.first && pos < range.second) {
+ // not yet done
+ fire_again = true; // fire this callback again
+ }
+ }
+ }
+
+ if (!fire_again) {
+ ptr->_active_timeout = 0;
+ }
+
+ return fire_again;
+}
+
+void ColorPalette::scroll(int dx, int dy, double snap, bool smooth) {
+ if (auto vert = _scroll.get_vscrollbar()) {
+ if (smooth && dy != 0.0) {
+ _scroll_final = vert->get_value() + dy;
+ if (snap > 0) {
+ // round it to whole 'dy' increments
+ _scroll_final -= fmod(_scroll_final, snap);
+ }
+ auto range = get_range(*vert);
+ if (_scroll_final < range.first) {
+ _scroll_final = range.first;
+ }
+ else if (_scroll_final > range.second) {
+ _scroll_final = range.second;
+ }
+ _scroll_step = dy / 4.0;
+ if (!_active_timeout && vert->get_value() != _scroll_final) {
+ // limit refresh to 60 fps, in practice it will be slower
+ _active_timeout = g_timeout_add(1000 / 60, &ColorPalette::scroll_cb, this);
+ }
+ }
+ else {
+ vert->set_value(vert->get_value() + dy);
+ }
+ }
+ if (auto horz = _scroll.get_hscrollbar()) {
+ horz->set_value(horz->get_value() + dx);
+ }
+}
+
+int ColorPalette::get_tile_size() const {
+ return _size;
+}
+
+int ColorPalette::get_tile_border() const {
+ return _border;
+}
+
+int ColorPalette::get_rows() const {
+ return _rows;
+}
+
+double ColorPalette::get_aspect() const {
+ return _aspect;
+}
+
+void ColorPalette::set_tile_border(int border) {
+ _set_tile_border(border);
+ auto& slider = get_widget<Gtk::Scale>(_builder, "border-slider");
+ slider.set_value(border);
+}
+
+void ColorPalette::_set_tile_border(int border) {
+ if (border == _border) return;
+
+ if (border < 0 || border > 100) {
+ g_warning("Unexpected tile border size of color palette: %d", border);
+ return;
+ }
+
+ _border = border;
+ set_up_scrolling();
+}
+
+void ColorPalette::set_tile_size(int size) {
+ _set_tile_size(size);
+ auto& slider = get_widget<Gtk::Scale>(_builder, "size-slider");
+ slider.set_value(size);
+}
+
+void ColorPalette::_set_tile_size(int size) {
+ if (size == _size) return;
+
+ if (size < 1 || size > 1000) {
+ g_warning("Unexpected tile size for color palette: %d", size);
+ return;
+ }
+
+ _size = size;
+ set_up_scrolling();
+}
+
+void ColorPalette::set_aspect(double aspect) {
+ _set_aspect(aspect);
+ auto& slider = get_widget<Gtk::Scale>(_builder, "aspect-slider");
+ slider.set_value(aspect);
+}
+
+void ColorPalette::_set_aspect(double aspect) {
+ if (aspect == _aspect) return;
+
+ if (aspect < -2.0 || aspect > 2.0) {
+ g_warning("Unexpected aspect ratio for color palette: %f", aspect);
+ return;
+ }
+
+ _aspect = aspect;
+ set_up_scrolling();
+}
+
+void ColorPalette::set_rows(int rows) {
+ _set_rows(rows);
+ auto& slider = get_widget<Gtk::Scale>(_builder, "row-slider");
+ slider.set_value(rows);
+}
+
+void ColorPalette::_set_rows(int rows) {
+ if (rows == _rows) return;
+
+ if (rows < 1 || rows > 1000) {
+ g_warning("Unexpected number of rows for color palette: %d", rows);
+ return;
+ }
+
+ _rows = rows;
+ update_checkbox();
+ set_up_scrolling();
+}
+
+void ColorPalette::update_checkbox() {
+ auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb");
+ // scrollbar can only be applied to single-row layouts
+ sb.set_sensitive(_rows == 1);
+}
+
+void ColorPalette::set_compact(bool compact) {
+ if (_compact != compact) {
+ _compact = compact;
+ set_up_scrolling();
+
+ get_widget<Gtk::Scale>(_builder, "row-slider").set_visible(compact);
+ get_widget<Gtk::Label>(_builder, "row-label").set_visible(compact);
+ }
+}
+
+bool ColorPalette::is_scrollbar_enabled() const {
+ return _force_scrollbar;
+}
+
+bool ColorPalette::is_stretch_enabled() const {
+ return _stretch_tiles;
+}
+
+void ColorPalette::enable_stretch(bool enable) {
+ auto& stretch = get_widget<Gtk::CheckButton>(_builder, "stretch");
+ stretch.set_active(enable);
+ _enable_stretch(enable);
+}
+
+void ColorPalette::_enable_stretch(bool enable) {
+ if (_stretch_tiles == enable) return;
+
+ _stretch_tiles = enable;
+ _flowbox.set_halign(enable ? Gtk::ALIGN_FILL : Gtk::ALIGN_START);
+ update_stretch();
+ set_up_scrolling();
+}
+
+void ColorPalette::update_stretch() {
+ auto& aspect = get_widget<Gtk::Scale>(_builder, "aspect-slider");
+ aspect.set_sensitive(!_stretch_tiles);
+ auto& label = get_widget<Gtk::Label>(_builder, "aspect-label");
+ label.set_sensitive(!_stretch_tiles);
+}
+
+void ColorPalette::enable_scrollbar(bool show) {
+ auto& sb = get_widget<Gtk::CheckButton>(_builder, "use-sb");
+ sb.set_active(show);
+ _enable_scrollbar(show);
+}
+
+void ColorPalette::_enable_scrollbar(bool show) {
+ if (_force_scrollbar == show) return;
+
+ _force_scrollbar = show;
+ set_up_scrolling();
+}
+
+void ColorPalette::set_up_scrolling() {
+ auto& box = get_widget<Gtk::Box>(_builder, "palette-box");
+ auto& btn_menu = get_widget<Gtk::MenuButton>(_builder, "btn-menu");
+
+ if (_compact) {
+ box.set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ btn_menu.set_margin_bottom(0);
+ btn_menu.set_margin_end(0);
+ // in compact mode scrollbars are hidden; they take up too much space
+ set_valign(Gtk::ALIGN_START);
+ set_vexpand(false);
+
+ _scroll.set_valign(Gtk::ALIGN_END);
+ _flowbox.set_valign(Gtk::ALIGN_END);
+
+ if (_rows == 1 && _force_scrollbar) {
+ // horizontal scrolling with single row
+ _flowbox.set_max_children_per_line(_count);
+ _flowbox.set_min_children_per_line(_count);
+
+ _scroll_btn.hide();
+
+ if (_force_scrollbar) {
+ _scroll_left.hide();
+ _scroll_right.hide();
+ }
+ else {
+ _scroll_left.show();
+ _scroll_right.show();
+ }
+
+ // ideally we should be able to use POLICY_AUTOMATIC, but on some themes this leads to a scrollbar
+ // that obscures color tiles (it overlaps them); thus resorting to manual scrollbar selection
+ _scroll.set_policy(_force_scrollbar ? Gtk::POLICY_ALWAYS : Gtk::POLICY_EXTERNAL, Gtk::POLICY_NEVER);
+ }
+ else {
+ // vertical scrolling with multiple rows
+ // 'external' allows scrollbar to shrink vertically
+ _scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_EXTERNAL);
+ _flowbox.set_min_children_per_line(1);
+ _flowbox.set_max_children_per_line(_count);
+ _scroll_left.hide();
+ _scroll_right.hide();
+ _scroll_btn.show();
+ }
+ }
+ else {
+ box.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ btn_menu.set_margin_bottom(2);
+ btn_menu.set_margin_end(2);
+ // in normal mode use regular full-size scrollbars
+ set_valign(Gtk::ALIGN_FILL);
+ set_vexpand(true);
+
+ _scroll_left.hide();
+ _scroll_right.hide();
+ _scroll_btn.hide();
+
+ _flowbox.set_valign(Gtk::ALIGN_START);
+ _flowbox.set_min_children_per_line(1);
+ _flowbox.set_max_children_per_line(_count);
+
+ _scroll.set_valign(Gtk::ALIGN_FILL);
+ // 'always' allocates space for scrollbar
+ _scroll.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ }
+
+ resize();
+}
+
+int ColorPalette::get_tile_size(bool horz) const {
+ if (_stretch_tiles) return _size;
+
+ double aspect = horz ? _aspect : -_aspect;
+
+ if (aspect > 0) {
+ return static_cast<int>(round((1.0 + aspect) * _size));
+ }
+ else if (aspect < 0) {
+ return static_cast<int>(round((1.0 / (1.0 - aspect)) * _size));
+ }
+ else {
+ return _size;
+ }
+}
+
+int ColorPalette::get_tile_width() const {
+ return get_tile_size(true);
+}
+
+int ColorPalette::get_tile_height() const {
+ return get_tile_size(false);
+}
+
+int ColorPalette::get_palette_height() const {
+ return (get_tile_height() + _border) * _rows;
+}
+
+void ColorPalette::resize() {
+ if (_rows == 1 && _force_scrollbar || !_compact) {
+ // auto size for single row to allocate space for scrollbar
+ _scroll.set_size_request(-1, -1);
+ }
+ else {
+ // exact size for multiple rows
+ int height = get_palette_height() - _border;
+ _scroll.set_size_request(1, height);
+ }
+
+ _flowbox.set_column_spacing(_border);
+ _flowbox.set_row_spacing(_border);
+
+ int width = get_tile_width();
+ int height = get_tile_height();
+ _flowbox.foreach([=](Gtk::Widget& w){
+ w.set_size_request(width, height);
+ });
+}
+
+void ColorPalette::free() {
+ for (auto widget : _flowbox.get_children()) {
+ if (widget) {
+ _flowbox.remove(*widget);
+ delete widget;
+ }
+ }
+}
+
+void ColorPalette::set_colors(const std::vector<Gtk::Widget*>& swatches) {
+ _flowbox.freeze_notify();
+ _flowbox.freeze_child_notify();
+
+ free();
+
+ int count = 0;
+ for (auto widget : swatches) {
+ if (widget) {
+ _flowbox.add(*widget);
+ ++count;
+ }
+ }
+
+ _flowbox.show_all();
+ _count = std::max(1, count);
+ _flowbox.set_max_children_per_line(_count);
+
+ // resize();
+ set_up_scrolling();
+
+ _flowbox.thaw_child_notify();
+ _flowbox.thaw_notify();
+}
+
+class CustomMenuItem : public Gtk::RadioMenuItem {
+public:
+ CustomMenuItem(Gtk::RadioMenuItem::Group& group, const Glib::ustring& label, std::vector<ColorPalette::rgb_t> colors):
+ Gtk::RadioMenuItem(group, label), _colors(std::move(colors)) {
+
+ set_margin_bottom(2);
+ }
+private:
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+ std::vector<ColorPalette::rgb_t> _colors;
+};
+
+bool CustomMenuItem::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
+ RadioMenuItem::on_draw(cr);
+ if (_colors.empty()) return false;
+
+ auto allocation = get_allocation();
+ auto x = 0;
+ auto y = 0;
+ auto width = allocation.get_width();
+ auto height = allocation.get_height();
+ auto left = x + height;
+ auto right = x + width - height;
+ auto dx = 1;
+ auto dy = 2;
+ auto px = left;
+ auto py = y + height - dy;
+ auto w = right - left;
+ if (w <= 0) return false;
+
+ for (int i = 0; i < w; ++i) {
+ if (px >= right) break;
+
+ int index = i * _colors.size() / w;
+ auto& color = _colors.at(index);
+
+ cr->set_source_rgb(color.r, color.g, color.b);
+ cr->rectangle(px, py, dx, dy);
+ cr->fill();
+
+ px += dx;
+ }
+
+ return false;
+}
+
+void ColorPalette::set_palettes(const std::vector<ColorPalette::palette_t>& palettes) {
+ auto items = _menu.get_children();
+ auto count = items.size();
+
+ int index = 0;
+ while (count > 2) {
+ if (auto item = items[index++]) {
+ _menu.remove(*item);
+ delete item;
+ }
+ count--;
+ }
+
+ Gtk::RadioMenuItem::Group group;
+ for (auto it = palettes.rbegin(); it != palettes.rend(); ++it) {
+ auto& name = it->name;
+ auto item = Gtk::manage(new CustomMenuItem(group, name, it->colors));
+ item->signal_activate().connect([=](){
+ if (!_in_update) {
+ _in_update = true;
+ _signal_palette_selected.emit(name);
+ _in_update = false;
+ }
+ });
+ item->show();
+ _menu.prepend(*item);
+ }
+}
+
+sigc::signal<void, Glib::ustring>& ColorPalette::get_palette_selected_signal() {
+ return _signal_palette_selected;
+}
+
+sigc::signal<void>& ColorPalette::get_settings_changed_signal() {
+ return _signal_settings_changed;
+}
+
+void ColorPalette::set_selected(const Glib::ustring& name) {
+ auto items = _menu.get_children();
+ _in_update = true;
+ for (auto item : items) {
+ if (auto radio = dynamic_cast<Gtk::RadioMenuItem*>(item)) {
+ radio->set_active(radio->get_label() == name);
+ }
+ }
+ _in_update = false;
+}
+
+}}} // namespace
diff --git a/src/ui/widget/color-palette.h b/src/ui/widget/color-palette.h
new file mode 100644
index 0000000..bc4c47f
--- /dev/null
+++ b/src/ui/widget/color-palette.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color palette widget
+ */
+/* Authors:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2021 Michael Kowalski
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_COLOR_PALETTE_H
+#define SEEN_COLOR_PALETTE_H
+
+#include <gtkmm/bin.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/menu.h>
+#include <vector>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorPalette : public Gtk::Bin {
+public:
+ ColorPalette();
+ ~ColorPalette() override;
+
+ struct rgb_t { double r; double g; double b; };
+ struct palette_t { Glib::ustring name; std::vector<rgb_t> colors; };
+
+ // set colors presented in a palette
+ void set_colors(const std::vector<Gtk::Widget*>& swatches);
+ // list of palettes to present in the menu
+ void set_palettes(const std::vector<palette_t>& palettes);
+ // enable compact mode (true) with mini-scroll buttons, or normal mode (false) with regular scrollbars
+ void set_compact(bool compact);
+
+ void set_tile_size(int size_px);
+ void set_tile_border(int border_px);
+ void set_rows(int rows);
+ void set_aspect(double aspect);
+ // show horizontal scrollbar when only 1 row is set
+ void enable_scrollbar(bool show);
+ // allow tile stretching (horizontally)
+ void enable_stretch(bool enable);
+
+ int get_tile_size() const;
+ int get_tile_border() const;
+ int get_rows() const;
+ double get_aspect() const;
+ bool is_scrollbar_enabled() const;
+ bool is_stretch_enabled() const;
+
+ void set_selected(const Glib::ustring& name);
+
+ sigc::signal<void, Glib::ustring>& get_palette_selected_signal();
+ sigc::signal<void>& get_settings_changed_signal();
+
+private:
+ void resize();
+ void set_up_scrolling();
+ void free();
+ void scroll(int dx, int dy, double snap, bool smooth);
+ void do_scroll(int dx, int dy);
+ static gboolean scroll_cb(gpointer self);
+ void _set_tile_size(int size_px);
+ void _set_tile_border(int border_px);
+ void _set_rows(int rows);
+ void _set_aspect(double aspect);
+ void _enable_scrollbar(bool show);
+ void _enable_stretch(bool enable);
+ static gboolean check_scrollbar(gpointer self);
+ void update_checkbox();
+ void update_stretch();
+ int get_tile_size(bool horz) const;
+ int get_tile_width() const;
+ int get_tile_height() const;
+ int get_palette_height() const;
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::FlowBox& _flowbox;
+ Gtk::ScrolledWindow& _scroll;
+ Gtk::FlowBox& _scroll_btn;
+ Gtk::Button& _scroll_up;
+ Gtk::Button& _scroll_down;
+ Gtk::Button& _scroll_left;
+ Gtk::Button& _scroll_right;
+ Gtk::Menu& _menu;
+ int _size = 10;
+ int _border = 0;
+ int _rows = 1;
+ double _aspect = 0.0;
+ int _count = 1;
+ bool _compact = true;
+ sigc::signal<void, Glib::ustring> _signal_palette_selected;
+ sigc::signal<void> _signal_settings_changed;
+ bool _in_update = false;
+ guint _active_timeout = 0;
+ bool _force_scrollbar = false;
+ bool _stretch_tiles = false;
+ double _scroll_step = 0.0; // smooth scrolling step
+ double _scroll_final = 0.0; // smooth scroll final value
+};
+
+}}} // namespace
+
+#endif // SEEN_COLOR_PALETTE_H
diff --git a/src/ui/widget/color-picker.cpp b/src/ui/widget/color-picker.cpp
new file mode 100644
index 0000000..009ec8e
--- /dev/null
+++ b/src/ui/widget/color-picker.cpp
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Abhishek Sharma
+ *
+ * Copyright (C) Authors 2000-2005
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "color-picker.h"
+
+#include "inkscape.h"
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+
+#include "ui/dialog-events.h"
+#include "ui/widget/color-notebook.h"
+
+
+static bool _in_use = false;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorPicker::ColorPicker (const Glib::ustring& title, const Glib::ustring& tip,
+ guint32 rgba, bool undo, Gtk::Button* external_button)
+ : _preview(new ColorPreview(rgba))
+ , _title(title)
+ , _rgba(rgba)
+ , _undo(undo)
+ , _colorSelectorDialog("dialogs.colorpickerwindow")
+{
+ Gtk::Button* button = external_button ? external_button : this;
+ _color_selector = nullptr;
+ setupDialog(title);
+ _preview->show();
+ button->add(*Gtk::manage(_preview));
+ // set tooltip if given, otherwise leave original tooltip in place (from external button)
+ if (!tip.empty()) {
+ button->set_tooltip_text(tip);
+ }
+ _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged));
+ _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged));
+ _selected_color.signal_released.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged));
+
+ if (external_button) {
+ external_button->signal_clicked().connect([=](){ on_clicked(); });
+ }
+}
+
+ColorPicker::~ColorPicker()
+{
+ closeWindow();
+}
+
+void ColorPicker::setupDialog(const Glib::ustring &title)
+{
+ GtkWidget *dlg = GTK_WIDGET(_colorSelectorDialog.gobj());
+ sp_transientize(dlg);
+
+ _colorSelectorDialog.hide();
+ _colorSelectorDialog.set_title (title);
+ _colorSelectorDialog.set_border_width (4);
+}
+
+void ColorPicker::setSensitive(bool sensitive) { set_sensitive(sensitive); }
+
+void ColorPicker::setRgba32 (guint32 rgba)
+{
+ if (_in_use) return;
+
+ set_preview(rgba);
+ _rgba = rgba;
+ if (_color_selector)
+ {
+ _updating = true;
+ _selected_color.setValue(rgba);
+ _updating = false;
+ }
+}
+
+void ColorPicker::closeWindow()
+{
+ _colorSelectorDialog.hide();
+}
+
+void ColorPicker::open() {
+ on_clicked();
+}
+
+void ColorPicker::on_clicked()
+{
+ if (!_color_selector) {
+ auto selector = Gtk::manage(new ColorNotebook(_selected_color));
+ selector->set_label(_title);
+ _color_selector = selector;
+ _colorSelectorDialog.get_content_area()->pack_start(*_color_selector, true, true, 0);
+ _color_selector->show();
+ }
+
+ _updating = true;
+ _selected_color.setValue(_rgba);
+ _updating = false;
+
+ _colorSelectorDialog.show();
+ Glib::RefPtr<Gdk::Window> window = _colorSelectorDialog.get_parent_window();
+ if (window) {
+ window->focus(1);
+ }
+}
+
+void ColorPicker::on_changed (guint32)
+{
+}
+
+void ColorPicker::_onSelectedColorChanged() {
+ if (_updating) {
+ return;
+ }
+
+ if (_in_use) {
+ return;
+ } else {
+ _in_use = true;
+ }
+
+ guint32 rgba = _selected_color.value();
+ set_preview(rgba);
+
+ if (_undo && SP_ACTIVE_DESKTOP) {
+ DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), /* TODO: annotate */ "color-picker.cpp:129", "");
+ }
+
+ on_changed(rgba);
+ _in_use = false;
+ _changed_signal.emit(rgba);
+ _rgba = rgba;
+}
+
+void ColorPicker::set_preview(guint32 rgba) {
+ _preview->setRgba32(_ignore_transparency ? rgba | 0xff : rgba);
+}
+
+void ColorPicker::use_transparency(bool enable) {
+ _ignore_transparency = !enable;
+ set_preview(_rgba);
+}
+
+}//namespace Widget
+}//namespace UI
+}//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/ui/widget/color-picker.h b/src/ui/widget/color-picker.h
new file mode 100644
index 0000000..c7147ae
--- /dev/null
+++ b/src/ui/widget/color-picker.h
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color picker button and window.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) Authors 2000-2005
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __COLOR_PICKER_H__
+#define __COLOR_PICKER_H__
+
+#include "labelled.h"
+
+#include <cstddef>
+
+#include "ui/selected-color.h"
+#include "ui/widget/color-preview.h"
+#include <gtkmm/button.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/window.h>
+#include <sigc++/sigc++.h>
+
+struct SPColorSelector;
+
+namespace Inkscape
+{
+namespace UI
+{
+namespace Widget
+{
+
+
+class ColorPicker : public Gtk::Button {
+public:
+
+ ColorPicker (const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const guint32 rgba,
+ bool undo,
+ Gtk::Button* external_button = nullptr);
+
+ ~ColorPicker() override;
+
+ void setRgba32 (guint32 rgba);
+ void setSensitive(bool sensitive);
+ void open();
+ void closeWindow();
+ sigc::connection connectChanged (const sigc::slot<void,guint>& slot)
+ { return _changed_signal.connect (slot); }
+ void use_transparency(bool enable);
+
+protected:
+
+ void _onSelectedColorChanged();
+ void on_clicked() override;
+ virtual void on_changed (guint32);
+
+ ColorPreview *_preview;
+
+ /*const*/ Glib::ustring _title;
+ sigc::signal<void,guint32> _changed_signal;
+ guint32 _rgba;
+ bool _undo;
+ bool _updating;
+
+ //Dialog
+ void setupDialog(const Glib::ustring &title);
+ //Inkscape::UI::Dialog::Dialog _colorSelectorDialog;
+ Gtk::Dialog _colorSelectorDialog;
+ SelectedColor _selected_color;
+
+private:
+ void set_preview(guint32 rgba);
+
+ Gtk::Widget *_color_selector;
+ bool _ignore_transparency = false;
+};
+
+
+class LabelledColorPicker : public Labelled {
+public:
+
+ LabelledColorPicker (const Glib::ustring& label,
+ const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const guint32 rgba,
+ bool undo) : Labelled(label, tip, new ColorPicker(title, tip, rgba, undo)) {}
+
+ void setRgba32 (guint32 rgba)
+ { static_cast<ColorPicker*>(_widget)->setRgba32 (rgba); }
+
+ void closeWindow()
+ { static_cast<ColorPicker*>(_widget)->closeWindow (); }
+
+ sigc::connection connectChanged (const sigc::slot<void,guint>& slot)
+ { return static_cast<ColorPicker*>(_widget)->connectChanged(slot); }
+};
+
+}//namespace Widget
+}//namespace UI
+}//namespace Inkscape
+
+#endif /* !__COLOR_PICKER_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/ui/widget/color-preview.cpp b/src/ui/widget/color-preview.cpp
new file mode 100644
index 0000000..ab81c60
--- /dev/null
+++ b/src/ui/widget/color-preview.cpp
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2001-2005 Authors
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/color-preview.h"
+#include "display/cairo-utils.h"
+#include <cairo.h>
+
+#define SPCP_DEFAULT_WIDTH 32
+#define SPCP_DEFAULT_HEIGHT 12
+
+namespace Inkscape {
+ namespace UI {
+ namespace Widget {
+
+ColorPreview::ColorPreview (guint32 rgba)
+{
+ _rgba = rgba;
+ set_has_window(false);
+ set_name("ColorPreview");
+}
+
+void
+ColorPreview::on_size_allocate (Gtk::Allocation &all)
+{
+ set_allocation (all);
+ if (get_is_drawable())
+ queue_draw();
+}
+
+void
+ColorPreview::get_preferred_height_vfunc(int& minimum_height, int& natural_height) const
+{
+ minimum_height = natural_height = SPCP_DEFAULT_HEIGHT;
+}
+
+void
+ColorPreview::get_preferred_height_for_width_vfunc(int /* width */, int& minimum_height, int& natural_height) const
+{
+ minimum_height = natural_height = SPCP_DEFAULT_HEIGHT;
+}
+
+void
+ColorPreview::get_preferred_width_vfunc(int& minimum_width, int& natural_width) const
+{
+ minimum_width = natural_width = SPCP_DEFAULT_WIDTH;
+}
+
+void
+ColorPreview::get_preferred_width_for_height_vfunc(int /* height */, int& minimum_width, int& natural_width) const
+{
+ minimum_width = natural_width = SPCP_DEFAULT_WIDTH;
+}
+
+void
+ColorPreview::setRgba32 (guint32 rgba)
+{
+ _rgba = rgba;
+
+ if (get_is_drawable())
+ queue_draw();
+}
+
+bool
+ColorPreview::on_draw(const Cairo::RefPtr<Cairo::Context>& cr)
+{
+ double x, y, width, height;
+ const Gtk::Allocation& allocation = get_allocation();
+ x = 0;
+ y = 0;
+ width = allocation.get_width()/2.0;
+ height = allocation.get_height() - 1;
+
+ double radius = height / 7.5;
+ double degrees = M_PI / 180.0;
+ cairo_new_sub_path (cr->cobj());
+ cairo_line_to(cr->cobj(), width, 0);
+ cairo_line_to(cr->cobj(), width, height);
+ cairo_arc (cr->cobj(), x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees);
+ cairo_arc (cr->cobj(), x + radius, y + radius, radius, 180 * degrees, 270 * degrees);
+ cairo_close_path (cr->cobj());
+
+ /* Transparent area */
+
+ cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard();
+
+ cairo_set_source(cr->cobj(), checkers);
+ cr->fill_preserve();
+ ink_cairo_set_source_rgba32(cr->cobj(), _rgba);
+ cr->fill();
+ cairo_pattern_destroy(checkers);
+
+ /* Solid area */
+
+ x = width;
+
+ cairo_new_sub_path (cr->cobj());
+ cairo_arc (cr->cobj(), x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees);
+ cairo_arc (cr->cobj(), x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees);
+ cairo_line_to(cr->cobj(), x, height);
+ cairo_line_to(cr->cobj(), x, y);
+ cairo_close_path (cr->cobj());
+ ink_cairo_set_source_rgba32(cr->cobj(), _rgba | 0xff);
+ cr->fill();
+
+ return true;
+}
+
+GdkPixbuf*
+ColorPreview::toPixbuf (int width, int height)
+{
+ GdkRectangle carea;
+ gint w2;
+ w2 = width / 2;
+
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ /* Transparent area */
+ carea.x = 0;
+ carea.y = 0;
+ carea.width = w2;
+ carea.height = height;
+
+ cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard();
+
+ // cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height);
+ cairo_arc(ct, carea.x + carea.width / 2, carea.y + carea.height / 2, carea.width / 2, 0, 2 * M_PI);
+ cairo_set_source(ct, checkers);
+ cairo_fill_preserve(ct);
+ ink_cairo_set_source_rgba32(ct, _rgba);
+ cairo_fill(ct);
+
+ cairo_pattern_destroy(checkers);
+
+ /* Solid area */
+ carea.x = w2;
+ carea.y = 0;
+ carea.width = width - w2;
+ carea.height = height;
+
+ cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height);
+ ink_cairo_set_source_rgba32(ct, _rgba | 0xff);
+ cairo_fill(ct);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s);
+ return pixbuf;
+}
+
+}}}
+
+/*
+ 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/ui/widget/color-preview.h b/src/ui/widget/color-preview.h
new file mode 100644
index 0000000..b789579
--- /dev/null
+++ b/src/ui/widget/color-preview.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_COLOR_PREVIEW_H
+#define SEEN_COLOR_PREVIEW_H
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2001-2005 Authors
+ * Copyright (C) 2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/widget.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A simple color preview widget, mainly used within a picker button.
+ */
+class ColorPreview : public Gtk::Widget {
+public:
+ ColorPreview (guint32 rgba);
+ void setRgba32 (guint32 rgba);
+ GdkPixbuf* toPixbuf (int width, int height);
+
+protected:
+ void on_size_allocate (Gtk::Allocation &all) override;
+
+ void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override;
+ void get_preferred_height_for_width_vfunc(int width, int& minimum_height, int& natural_height) const override;
+ void get_preferred_width_vfunc(int& minimum_width, int& natural_width) const override;
+ void get_preferred_width_for_height_vfunc(int height, int& minimum_width, int& natural_width) const override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+
+ guint32 _rgba;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_COLOR_PREVIEW_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/ui/widget/color-scales.cpp b/src/ui/widget/color-scales.cpp
new file mode 100644
index 0000000..5662656
--- /dev/null
+++ b/src/ui/widget/color-scales.cpp
@@ -0,0 +1,1062 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Color selector using sliders for each components, for multiple color modes
+ *//*
+ * Authors:
+ * see git history
+ * bulia byak <buliabyak@users.sf.net>
+ * Massinissa Derriche <massinissa.derriche@gmail.com>
+ *
+ * Copyright (C) 2018, 2021 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/grid.h>
+#include <glibmm/i18n.h>
+#include <functional>
+
+#include "ui/dialog-events.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/icon-loader.h"
+#include "preferences.h"
+
+#include "ui/widget/ink-color-wheel.h"
+
+static int const CSC_CHANNEL_R = (1 << 0);
+static int const CSC_CHANNEL_G = (1 << 1);
+static int const CSC_CHANNEL_B = (1 << 2);
+static int const CSC_CHANNEL_A = (1 << 3);
+static int const CSC_CHANNEL_H = (1 << 0);
+static int const CSC_CHANNEL_S = (1 << 1);
+static int const CSC_CHANNEL_V = (1 << 2);
+static int const CSC_CHANNEL_C = (1 << 0);
+static int const CSC_CHANNEL_M = (1 << 1);
+static int const CSC_CHANNEL_Y = (1 << 2);
+static int const CSC_CHANNEL_K = (1 << 3);
+static int const CSC_CHANNEL_CMYKA = (1 << 4);
+
+static int const CSC_CHANNELS_ALL = 0;
+
+static int const XPAD = 2;
+static int const YPAD = 2;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+static guchar const *sp_color_scales_hue_map();
+static guchar const *sp_color_scales_hsluv_map(guchar *map,
+ std::function<void(float*, float)> callback);
+
+
+template <SPColorScalesMode MODE>
+gchar const *ColorScales<MODE>::SUBMODE_NAMES[] = { N_("None"), N_("RGB"), N_("HSL"),
+ N_("CMYK"), N_("HSV"), N_("HSLuv") };
+
+
+// Preference name for the saved state of toggle-able color wheel
+template <>
+gchar const * const ColorScales<SPColorScalesMode::HSL>::_pref_wheel_visibility =
+ "/wheel_vis_hsl";
+
+template <>
+gchar const * const ColorScales<SPColorScalesMode::HSV>::_pref_wheel_visibility =
+ "/wheel_vis_hsv";
+
+template <>
+gchar const * const ColorScales<SPColorScalesMode::HSLUV>::_pref_wheel_visibility =
+ "/wheel_vis_hsluv";
+
+
+template <SPColorScalesMode MODE>
+ColorScales<MODE>::ColorScales(SelectedColor &color)
+ : Gtk::Box()
+ , _color(color)
+ , _range_limit(255.0)
+ , _updating(false)
+ , _dragging(false)
+ , _wheel(nullptr)
+{
+ for (gint i = 0; i < 5; i++) {
+ _l[i] = nullptr;
+ _s[i] = nullptr;
+ _b[i] = nullptr;
+ }
+
+ _initUI();
+
+ _color_changed = _color.signal_changed.connect([this](){ _onColorChanged(); });
+ _color_dragged = _color.signal_dragged.connect([this](){ _onColorChanged(); });
+}
+
+template <SPColorScalesMode MODE>
+ColorScales<MODE>::~ColorScales()
+{
+ _color_changed.disconnect();
+ _color_dragged.disconnect();
+
+ for (gint i = 0; i < 5; i++) {
+ _l[i] = nullptr;
+ _s[i] = nullptr;
+ _b[i] = nullptr;
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_initUI()
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ Gtk::Expander *wheel_frame = nullptr;
+
+ if constexpr (
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV)
+ {
+ /* Create wheel */
+ if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheelHSLuv());
+ } else {
+ _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheelHSL());
+ }
+
+ _wheel->show();
+ _wheel->set_halign(Gtk::ALIGN_FILL);
+ _wheel->set_valign(Gtk::ALIGN_FILL);
+ _wheel->set_hexpand(true);
+ _wheel->set_vexpand(true);
+ _wheel->set_name("ColorWheel");
+ _wheel->set_size_request(-1, 130); // minimal size
+
+ /* Signal */
+ _wheel->signal_color_changed().connect([this](){ _wheelChanged(); });
+
+ /* Expander */
+ // Label icon
+ Gtk::Image *expander_icon = Gtk::manage(
+ sp_get_icon_image("color-wheel", Gtk::ICON_SIZE_BUTTON)
+ );
+ expander_icon->show();
+ expander_icon->set_margin_start(2 * XPAD);
+ expander_icon->set_margin_end(3 * XPAD);
+ // Label
+ Gtk::Label *expander_label = Gtk::manage(new Gtk::Label(_("Color Wheel")));
+ expander_label->show();
+ // Content
+ Gtk::Box *expander_box = Gtk::manage(new Gtk::Box());
+ expander_box->show();
+ expander_box->pack_start(*expander_icon);
+ expander_box->pack_start(*expander_label);
+ expander_box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ // Expander
+ wheel_frame = Gtk::manage(new Gtk::Expander());
+ wheel_frame->show();
+ wheel_frame->set_margin_start(2 * XPAD);
+ wheel_frame->set_margin_end(XPAD);
+ wheel_frame->set_margin_top(2 * YPAD);
+ wheel_frame->set_margin_bottom(2 * YPAD);
+ wheel_frame->set_halign(Gtk::ALIGN_FILL);
+ wheel_frame->set_valign(Gtk::ALIGN_FILL);
+ wheel_frame->set_hexpand(true);
+ wheel_frame->set_vexpand(false);
+ wheel_frame->set_label_widget(*expander_box);
+
+ // Signal
+ wheel_frame->property_expanded().signal_changed().connect([=](){
+ bool visible = wheel_frame->get_expanded();
+ wheel_frame->set_vexpand(visible);
+
+ // Save wheel visibility
+ Inkscape::Preferences::get()->setBool(_prefs + _pref_wheel_visibility, visible);
+ });
+
+ wheel_frame->add(*_wheel);
+ add(*wheel_frame);
+ }
+
+ /* Create sliders */
+ Gtk::Grid *grid = Gtk::manage(new Gtk::Grid());
+ grid->show();
+ add(*grid);
+
+ for (gint i = 0; i < 5; i++) {
+ /* Label */
+ _l[i] = Gtk::manage(new Gtk::Label("", true));
+
+ _l[i]->set_halign(Gtk::ALIGN_START);
+ _l[i]->show();
+
+ _l[i]->set_margin_start(2 * XPAD);
+ _l[i]->set_margin_end(XPAD);
+ _l[i]->set_margin_top(YPAD);
+ _l[i]->set_margin_bottom(YPAD);
+ grid->attach(*_l[i], 0, i, 1, 1);
+
+ /* Adjustment */
+ _a.push_back(Gtk::Adjustment::create(0.0, 0.0, _range_limit, 1.0, 10.0, 10.0));
+ /* Slider */
+ _s[i] = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_a[i]));
+ _s[i]->show();
+
+ _s[i]->set_margin_start(XPAD);
+ _s[i]->set_margin_end(XPAD);
+ _s[i]->set_margin_top(YPAD);
+ _s[i]->set_margin_bottom(YPAD);
+ _s[i]->set_hexpand(true);
+ grid->attach(*_s[i], 1, i, 1, 1);
+
+ /* Spinbutton */
+ _b[i] = Gtk::manage(new ScrollProtected<Gtk::SpinButton>(_a[i], 1.0));
+ sp_dialog_defocus_on_enter(_b[i]->gobj());
+ _l[i]->set_mnemonic_widget(*_b[i]);
+ _b[i]->show();
+
+ _b[i]->set_margin_start(XPAD);
+ _b[i]->set_margin_end(XPAD);
+ _b[i]->set_margin_top(YPAD);
+ _b[i]->set_margin_bottom(YPAD);
+ _b[i]->set_halign(Gtk::ALIGN_END);
+ _b[i]->set_valign(Gtk::ALIGN_CENTER);
+ grid->attach(*_b[i], 2, i, 1, 1);
+
+ /* Signals */
+ _a[i]->signal_value_changed().connect([this, i](){ _adjustmentChanged(i); });
+ _s[i]->signal_grabbed.connect([this](){ _sliderAnyGrabbed(); });
+ _s[i]->signal_released.connect([this](){ _sliderAnyReleased(); });
+ _s[i]->signal_value_changed.connect([this](){ _sliderAnyChanged(); });
+ }
+
+ // Prevent 5th bar from being shown by PanelDialog::show_all_children
+ _l[4]->set_no_show_all(true);
+ _s[4]->set_no_show_all(true);
+ _b[4]->set_no_show_all(true);
+
+ setupMode();
+
+ if constexpr (
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV)
+ {
+ // Restore the visibility of the wheel
+ bool visible = Inkscape::Preferences::get()->getBool(_prefs + _pref_wheel_visibility,
+ false);
+ wheel_frame->set_expanded(visible);
+ wheel_frame->set_vexpand(visible);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_recalcColor()
+{
+ SPColor color;
+ gfloat alpha = 1.0;
+ gfloat c[5];
+
+ if constexpr (
+ MODE == SPColorScalesMode::RGB ||
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV)
+ {
+ _getRgbaFloatv(c);
+ color.set(c[0], c[1], c[2]);
+ alpha = c[3];
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ _getCmykaFloatv(c);
+
+ float rgb[3];
+ SPColor::cmyk_to_rgb_floatv(rgb, c[0], c[1], c[2], c[3]);
+ color.set(rgb[0], rgb[1], rgb[2]);
+ alpha = c[4];
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode NONE", __FILE__, __LINE__);
+ }
+
+ _color.preserveICC();
+ _color.setColorAlpha(color, alpha);
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_updateDisplay(bool update_wheel)
+{
+#ifdef DUMP_CHANGE_INFO
+ g_message("ColorScales::_onColorChanged( this=%p, %f, %f, %f, %f) %d", this,
+ _color.color().v.c[0],
+ _color.color().v.c[1], _color.color().v.c[2], _color.alpha(), int(update_wheel);
+#endif
+
+ gfloat tmp[3];
+ gfloat c[5] = { 0.0, 0.0, 0.0, 0.0 };
+
+ SPColor color = _color.color();
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ color.get_rgb_floatv(c);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ color.get_rgb_floatv(tmp);
+ SPColor::rgb_to_hsl_floatv(c, tmp[0], tmp[1], tmp[2]);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); }
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ color.get_rgb_floatv(tmp);
+ SPColor::rgb_to_hsv_floatv(c, tmp[0], tmp[1], tmp[2]);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); }
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ color.get_cmyk_floatv(c);
+ c[4] = _color.alpha();
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ color.get_rgb_floatv(tmp);
+ SPColor::rgb_to_hsluv_floatv(c, tmp[0], tmp[1], tmp[2]);
+ c[3] = _color.alpha();
+ c[4] = 0.0;
+ if (update_wheel) { _wheel->setRgb(tmp[0], tmp[1], tmp[2]); }
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode NONE", __FILE__, __LINE__);
+ }
+
+ _updating = true;
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], c[3]);
+ setScaled(_a[4], c[4]);
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+}
+
+/* Helpers for setting color value */
+template <SPColorScalesMode MODE>
+gfloat ColorScales<MODE>::getScaled(Glib::RefPtr<Gtk::Adjustment> const &a)
+{
+ gfloat val = a->get_value() / a->get_upper();
+ return val;
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::setScaled(Glib::RefPtr<Gtk::Adjustment> &a, gfloat v, bool constrained)
+{
+ auto upper = a->get_upper();
+ gfloat val = v * upper;
+ if (constrained) {
+ // TODO: do we want preferences for these?
+ if (upper == 255) {
+ val = round(val/16) * 16;
+ } else {
+ val = round(val/10) * 10;
+ }
+ }
+ a->set_value(val);
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_setRangeLimit(gdouble upper)
+{
+ _range_limit = upper;
+ for (auto & i : _a) {
+ i->set_upper(upper);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_onColorChanged()
+{
+ if (!get_visible()) { return; }
+
+ _updateDisplay();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::on_show()
+{
+ Gtk::Box::on_show();
+
+ _updateDisplay();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_getRgbaFloatv(gfloat *rgba)
+{
+ g_return_if_fail(rgba != nullptr);
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ rgba[0] = getScaled(_a[0]);
+ rgba[1] = getScaled(_a[1]);
+ rgba[2] = getScaled(_a[2]);
+ rgba[3] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ SPColor::hsl_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ rgba[3] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ SPColor::hsv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]));
+ rgba[3] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ SPColor::cmyk_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]),
+ getScaled(_a[2]), getScaled(_a[3]));
+ rgba[3] = getScaled(_a[4]);
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ SPColor::hsluv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]),
+ getScaled(_a[2]));
+ rgba[3] = getScaled(_a[3]);
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_getCmykaFloatv(gfloat *cmyka)
+{
+ gfloat rgb[3];
+
+ g_return_if_fail(cmyka != nullptr);
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(_a[0]), getScaled(_a[1]),
+ getScaled(_a[2]));
+ cmyka[4] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ SPColor::hsl_to_rgb_floatv(rgb, getScaled(_a[0]), getScaled(_a[1]),
+ getScaled(_a[2]));
+ SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]);
+ cmyka[4] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ SPColor::hsluv_to_rgb_floatv(rgb, getScaled(_a[0]), getScaled(_a[1]),
+ getScaled(_a[2]));
+ SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]);
+ cmyka[4] = getScaled(_a[3]);
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ cmyka[0] = getScaled(_a[0]);
+ cmyka[1] = getScaled(_a[1]);
+ cmyka[2] = getScaled(_a[2]);
+ cmyka[3] = getScaled(_a[3]);
+ cmyka[4] = getScaled(_a[4]);
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+}
+
+template <SPColorScalesMode MODE>
+guint32 ColorScales<MODE>::_getRgba32()
+{
+ gfloat c[4];
+ guint32 rgba;
+
+ _getRgbaFloatv(c);
+
+ rgba = SP_RGBA32_F_COMPOSE(c[0], c[1], c[2], c[3]);
+
+ return rgba;
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::setupMode()
+{
+ gfloat rgba[4];
+ gfloat c[4];
+
+ if constexpr (MODE == SPColorScalesMode::NONE) {
+ rgba[0] = rgba[1] = rgba[2] = rgba[3] = 1.0;
+ } else {
+ _getRgbaFloatv(rgba);
+ }
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ _setRangeLimit(255.0);
+ _a[3]->set_upper(100.0);
+ _l[0]->set_markup_with_mnemonic(_("_R:"));
+ _s[0]->set_tooltip_text(_("Red"));
+ _b[0]->set_tooltip_text(_("Red"));
+ _l[1]->set_markup_with_mnemonic(_("_G:"));
+ _s[1]->set_tooltip_text(_("Green"));
+ _b[1]->set_tooltip_text(_("Green"));
+ _l[2]->set_markup_with_mnemonic(_("_B:"));
+ _s[2]->set_tooltip_text(_("Blue"));
+ _b[2]->set_tooltip_text(_("Blue"));
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _s[0]->setMap(nullptr);
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _updating = true;
+ setScaled(_a[0], rgba[0]);
+ setScaled(_a[1], rgba[1]);
+ setScaled(_a[2], rgba[2]);
+ setScaled(_a[3], rgba[3]);
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ _setRangeLimit(100.0);
+
+ _l[0]->set_markup_with_mnemonic(_("_H:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ _b[0]->set_tooltip_text(_("Hue"));
+ _a[0]->set_upper(360.0);
+
+ _l[1]->set_markup_with_mnemonic(_("_S:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ _b[1]->set_tooltip_text(_("Saturation"));
+
+ _l[2]->set_markup_with_mnemonic(_("_L:"));
+ _s[2]->set_tooltip_text(_("Lightness"));
+ _b[2]->set_tooltip_text(_("Lightness"));
+
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _s[0]->setMap(sp_color_scales_hue_map());
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _updating = true;
+ c[0] = 0.0;
+
+ SPColor::rgb_to_hsl_floatv(c, rgba[0], rgba[1], rgba[2]);
+
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], rgba[3]);
+
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ _setRangeLimit(100.0);
+
+ _l[0]->set_markup_with_mnemonic(_("_H:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ _b[0]->set_tooltip_text(_("Hue"));
+ _a[0]->set_upper(360.0);
+
+ _l[1]->set_markup_with_mnemonic(_("_S:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ _b[1]->set_tooltip_text(_("Saturation"));
+
+ _l[2]->set_markup_with_mnemonic(_("_V:"));
+ _s[2]->set_tooltip_text(_("Value"));
+ _b[2]->set_tooltip_text(_("Value"));
+
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _s[0]->setMap(sp_color_scales_hue_map());
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _updating = true;
+ c[0] = 0.0;
+
+ SPColor::rgb_to_hsv_floatv(c, rgba[0], rgba[1], rgba[2]);
+
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], rgba[3]);
+
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ _setRangeLimit(100.0);
+ _l[0]->set_markup_with_mnemonic(_("_C:"));
+ _s[0]->set_tooltip_text(_("Cyan"));
+ _b[0]->set_tooltip_text(_("Cyan"));
+
+ _l[1]->set_markup_with_mnemonic(_("_M:"));
+ _s[1]->set_tooltip_text(_("Magenta"));
+ _b[1]->set_tooltip_text(_("Magenta"));
+
+ _l[2]->set_markup_with_mnemonic(_("_Y:"));
+ _s[2]->set_tooltip_text(_("Yellow"));
+ _b[2]->set_tooltip_text(_("Yellow"));
+
+ _l[3]->set_markup_with_mnemonic(_("_K:"));
+ _s[3]->set_tooltip_text(_("Black"));
+ _b[3]->set_tooltip_text(_("Black"));
+
+ _l[4]->set_markup_with_mnemonic(_("_A:"));
+ _s[4]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[4]->set_tooltip_text(_("Alpha (opacity)"));
+
+ _s[0]->setMap(nullptr);
+ _l[4]->show();
+ _s[4]->show();
+ _b[4]->show();
+ _updating = true;
+
+ SPColor::rgb_to_cmyk_floatv(c, rgba[0], rgba[1], rgba[2]);
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], c[3]);
+
+ setScaled(_a[4], rgba[3]);
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ _setRangeLimit(100.0);
+
+ _l[0]->set_markup_with_mnemonic(_("_H*:"));
+ _s[0]->set_tooltip_text(_("Hue"));
+ _b[0]->set_tooltip_text(_("Hue"));
+ _a[0]->set_upper(360.0);
+
+ _l[1]->set_markup_with_mnemonic(_("_S*:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ _b[1]->set_tooltip_text(_("Saturation"));
+
+ _l[2]->set_markup_with_mnemonic(_("_L*:"));
+ _s[2]->set_tooltip_text(_("Lightness"));
+ _b[2]->set_tooltip_text(_("Lightness"));
+
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+
+ _s[0]->setMap(hsluvHueMap(0.0f, 0.0f, &_sliders_maps[0]));
+ _s[1]->setMap(hsluvSaturationMap(0.0f, 0.0f, &_sliders_maps[1]));
+ _s[2]->setMap(hsluvLightnessMap(0.0f, 0.0f, &_sliders_maps[2]));
+
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _updating = true;
+ c[0] = 0.0;
+
+ SPColor::rgb_to_hsluv_floatv(c, rgba[0], rgba[1], rgba[2]);
+
+ setScaled(_a[0], c[0]);
+ setScaled(_a[1], c[1]);
+ setScaled(_a[2], c[2]);
+ setScaled(_a[3], rgba[3]);
+
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+}
+
+template <SPColorScalesMode MODE>
+SPColorScalesMode ColorScales<MODE>::getMode() const { return MODE; }
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_sliderAnyGrabbed()
+{
+ if (_updating) { return; }
+
+ if (!_dragging) {
+ _dragging = true;
+ _color.setHeld(true);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_sliderAnyReleased()
+{
+ if (_updating) { return; }
+
+ if (_dragging) {
+ _dragging = false;
+ _color.setHeld(false);
+ }
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_sliderAnyChanged()
+{
+ if (_updating) { return; }
+
+ _recalcColor();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_adjustmentChanged(int channel)
+{
+ if (_updating) { return; }
+
+ _updateSliders((1 << channel));
+ _recalcColor();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_wheelChanged()
+{
+ if constexpr (
+ MODE == SPColorScalesMode::NONE ||
+ MODE == SPColorScalesMode::RGB ||
+ MODE == SPColorScalesMode::CMYK)
+ {
+ return;
+ }
+
+ if (_updating) { return; }
+
+ _updating = true;
+
+ double rgb[3];
+ _wheel->getRgbV(rgb);
+ SPColor color(rgb[0], rgb[1], rgb[2]);
+
+ _color_changed.block();
+ _color_dragged.block();
+
+ // Color
+ _color.preserveICC();
+ _color.setHeld(_wheel->isAdjusting());
+ _color.setColor(color);
+
+ // Sliders
+ _updateDisplay(false);
+
+ _color_changed.unblock();
+ _color_dragged.unblock();
+
+ _updating = false;
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::_updateSliders(guint channels)
+{
+ gfloat rgb0[3], rgbm[3], rgb1[3];
+
+#ifdef SPCS_PREVIEW
+ guint32 rgba;
+#endif
+
+ std::array<gfloat, 4> const adj = [this]() -> std::array<gfloat, 4> {
+ if constexpr (MODE == SPColorScalesMode::CMYK) {
+ return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3]) };
+ } else {
+ return { getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0 };
+ }
+ }();
+
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ if ((channels != CSC_CHANNEL_R) && (channels != CSC_CHANNEL_A)) {
+ /* Update red */
+ _s[0]->setColors(SP_RGBA32_F_COMPOSE(0.0, adj[1], adj[2], 1.0),
+ SP_RGBA32_F_COMPOSE(0.5, adj[1], adj[2], 1.0),
+ SP_RGBA32_F_COMPOSE(1.0, adj[1], adj[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_G) && (channels != CSC_CHANNEL_A)) {
+ /* Update green */
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(adj[0], 0.0, adj[2], 1.0),
+ SP_RGBA32_F_COMPOSE(adj[0], 0.5, adj[2], 1.0),
+ SP_RGBA32_F_COMPOSE(adj[0], 1.0, adj[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_B) && (channels != CSC_CHANNEL_A)) {
+ /* Update blue */
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.0, 1.0),
+ SP_RGBA32_F_COMPOSE(adj[0], adj[1], 0.5, 1.0),
+ SP_RGBA32_F_COMPOSE(adj[0], adj[1], 1.0, 1.0));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 0.0),
+ SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 0.5),
+ SP_RGBA32_F_COMPOSE(adj[0], adj[1], adj[2], 1.0));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ /* Hue is never updated */
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ /* Update saturation */
+ SPColor::hsl_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2]);
+ SPColor::hsl_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2]);
+ SPColor::hsl_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2]);
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) {
+ /* Update value */
+ SPColor::hsl_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0);
+ SPColor::hsl_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5);
+ SPColor::hsl_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0);
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ SPColor::hsl_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]);
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ /* Hue is never updated */
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ /* Update saturation */
+ SPColor::hsv_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2]);
+ SPColor::hsv_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2]);
+ SPColor::hsv_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2]);
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) {
+ /* Update value */
+ SPColor::hsv_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0);
+ SPColor::hsv_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5);
+ SPColor::hsv_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0);
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ SPColor::hsv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]);
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ if ((channels != CSC_CHANNEL_C) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update C */
+ SPColor::cmyk_to_rgb_floatv(rgb0, 0.0, adj[1], adj[2], adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgbm, 0.5, adj[1], adj[2], adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgb1, 1.0, adj[1], adj[2], adj[3]);
+ _s[0]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_M) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update M */
+ SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], 0.0, adj[2], adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], 0.5, adj[2], adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], 1.0, adj[2], adj[3]);
+ _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_Y) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update Y */
+ SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], 0.0, adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], adj[1], 0.5, adj[3]);
+ SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], adj[1], 1.0, adj[3]);
+ _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if ((channels != CSC_CHANNEL_K) && (channels != CSC_CHANNEL_CMYKA)) {
+ /* Update K */
+ SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2], 0.0);
+ SPColor::cmyk_to_rgb_floatv(rgbm, adj[0], adj[1], adj[2], 0.5);
+ SPColor::cmyk_to_rgb_floatv(rgb1, adj[0], adj[1], adj[2], 1.0);
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0),
+ SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0));
+ }
+ if (channels != CSC_CHANNEL_CMYKA) {
+ /* Update alpha */
+ SPColor::cmyk_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2], adj[3]);
+ _s[4]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0));
+ }
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ if ((channels != CSC_CHANNEL_H) && (channels != CSC_CHANNEL_A)) {
+ /* Update hue */
+ _s[0]->setMap(hsluvHueMap(adj[1], adj[2], &_sliders_maps[0]));
+ }
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ /* Update saturation (scaled chroma) */
+ _s[1]->setMap(hsluvSaturationMap(adj[0], adj[2], &_sliders_maps[1]));
+ }
+ if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) {
+ /* Update lightness */
+ _s[2]->setMap(hsluvLightnessMap(adj[0], adj[1], &_sliders_maps[2]));
+ }
+ if (channels != CSC_CHANNEL_A) {
+ /* Update alpha */
+ SPColor::hsluv_to_rgb_floatv(rgb0, adj[0], adj[1], adj[2]);
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0));
+ }
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+
+#ifdef SPCS_PREVIEW
+ rgba = sp_color_scales_get_rgba32(cs);
+ sp_color_preview_set_rgba32(SP_COLOR_PREVIEW(_p), rgba);
+#endif
+}
+
+static guchar const *sp_color_scales_hue_map()
+{
+ static std::array<guchar, 4 * 1024> const map = []() {
+ std::array<guchar, 4 * 1024> m;
+
+ guchar *p;
+ p = m.data();
+ for (gint h = 0; h < 1024; h++) {
+ gfloat rgb[3];
+ SPColor::hsl_to_rgb_floatv(rgb, h / 1024.0, 1.0, 0.5);
+ *p++ = SP_COLOR_F_TO_U(rgb[0]);
+ *p++ = SP_COLOR_F_TO_U(rgb[1]);
+ *p++ = SP_COLOR_F_TO_U(rgb[2]);
+ *p++ = 0xFF;
+ }
+
+ return m;
+ }();
+
+ return map.data();
+}
+
+static void sp_color_interp(guchar *out, gint steps, gfloat *start, gfloat *end)
+{
+ gfloat s[3] = {
+ (end[0] - start[0]) / steps,
+ (end[1] - start[1]) / steps,
+ (end[2] - start[2]) / steps
+ };
+
+ guchar *p = out;
+ for (int i = 0; i < steps; i++) {
+ *p++ = SP_COLOR_F_TO_U(start[0] + s[0] * i);
+ *p++ = SP_COLOR_F_TO_U(start[1] + s[1] * i);
+ *p++ = SP_COLOR_F_TO_U(start[2] + s[2] * i);
+ *p++ = 0xFF;
+ }
+}
+
+template <typename T>
+static std::vector<T> range (int const steps, T start, T end)
+{
+ T step = (end - start) / (steps - 1);
+
+ std::vector<T> out;
+ out.reserve(steps);
+
+ for (int i = 0; i < steps-1; i++) {
+ out.emplace_back(start + step * i);
+ }
+ out.emplace_back(end);
+
+ return out;
+}
+
+static guchar const *sp_color_scales_hsluv_map(guchar *map,
+ std::function<void(float*, float)> callback)
+{
+ // Only generate 21 colors and interpolate between them to get 1024
+ static int const STEPS = 21;
+ static int const COLORS = (STEPS+1) * 3;
+
+ std::vector<float> steps = range<float>(STEPS+1, 0.f, 1.f);
+
+ // Generate color steps
+ gfloat colors[COLORS];
+ for (int i = 0; i < STEPS+1; i++) {
+ callback(colors+(i*3), steps[i]);
+ }
+
+ for (int i = 0; i < STEPS; i++) {
+ int a = steps[i] * 1023,
+ b = steps[i+1] * 1023;
+ sp_color_interp(map+(a * 4), b-a, colors+(i*3), colors+((i+1)*3));
+ }
+
+ return map;
+}
+
+template <SPColorScalesMode MODE>
+guchar const *ColorScales<MODE>::hsluvHueMap(gfloat s, gfloat l,
+ std::array<guchar, 4 * 1024> *map)
+{
+ return sp_color_scales_hsluv_map(map->data(), [s, l] (float *colors, float h) {
+ SPColor::hsluv_to_rgb_floatv(colors, h, s, l);
+ });
+}
+
+template <SPColorScalesMode MODE>
+guchar const *ColorScales<MODE>::hsluvSaturationMap(gfloat h, gfloat l,
+ std::array<guchar, 4 * 1024> *map)
+{
+ return sp_color_scales_hsluv_map(map->data(), [h, l] (float *colors, float s) {
+ SPColor::hsluv_to_rgb_floatv(colors, h, s, l);
+ });
+}
+
+template <SPColorScalesMode MODE>
+guchar const *ColorScales<MODE>::hsluvLightnessMap(gfloat h, gfloat s,
+ std::array<guchar, 4 * 1024> *map)
+{
+ return sp_color_scales_hsluv_map(map->data(), [h, s] (float *colors, float l) {
+ SPColor::hsluv_to_rgb_floatv(colors, h, s, l);
+ });
+}
+
+template <SPColorScalesMode MODE>
+ColorScalesFactory<MODE>::ColorScalesFactory()
+{}
+
+template <SPColorScalesMode MODE>
+Gtk::Widget *ColorScalesFactory<MODE>::createWidget(Inkscape::UI::SelectedColor &color) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorScales<MODE>(color));
+ return w;
+}
+
+template <SPColorScalesMode MODE>
+Glib::ustring ColorScalesFactory<MODE>::modeName() const
+{
+ if constexpr (MODE == SPColorScalesMode::RGB) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[1]);
+ } else if constexpr (MODE == SPColorScalesMode::HSL) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[2]);
+ } else if constexpr (MODE == SPColorScalesMode::CMYK) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[3]);
+ } else if constexpr (MODE == SPColorScalesMode::HSV) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[4]);
+ } else if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[5]);
+ } else {
+ return gettext(ColorScales<>::SUBMODE_NAMES[0]);
+ }
+}
+
+// Explicit instantiations
+template class ColorScales<SPColorScalesMode::NONE>;
+template class ColorScales<SPColorScalesMode::RGB>;
+template class ColorScales<SPColorScalesMode::HSL>;
+template class ColorScales<SPColorScalesMode::CMYK>;
+template class ColorScales<SPColorScalesMode::HSV>;
+template class ColorScales<SPColorScalesMode::HSLUV>;
+
+template class ColorScalesFactory<SPColorScalesMode::NONE>;
+template class ColorScalesFactory<SPColorScalesMode::RGB>;
+template class ColorScalesFactory<SPColorScalesMode::HSL>;
+template class ColorScalesFactory<SPColorScalesMode::CMYK>;
+template class ColorScalesFactory<SPColorScalesMode::HSV>;
+template class ColorScalesFactory<SPColorScalesMode::HSLUV>;
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/color-scales.h b/src/ui/widget/color-scales.h
new file mode 100644
index 0000000..6e5b9a2
--- /dev/null
+++ b/src/ui/widget/color-scales.h
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Color selector using sliders for each components, for multiple color modes
+ *//*
+ * Authors:
+ * see git history
+ *
+ * Copyright (C) 2018, 2021 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_SP_COLOR_SCALES_H
+#define SEEN_SP_COLOR_SCALES_H
+
+#include <gtkmm/box.h>
+#include <array>
+
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorSlider;
+class ColorWheel;
+
+enum class SPColorScalesMode {
+ NONE,
+ RGB,
+ HSL,
+ CMYK,
+ HSV,
+ HSLUV
+};
+
+template <SPColorScalesMode MODE = SPColorScalesMode::NONE>
+class ColorScales
+ : public Gtk::Box
+{
+public:
+ static gchar const *SUBMODE_NAMES[];
+
+ static gfloat getScaled(Glib::RefPtr<Gtk::Adjustment> const &a);
+ static void setScaled(Glib::RefPtr<Gtk::Adjustment> &a, gfloat v, bool constrained = false);
+
+ ColorScales(SelectedColor &color);
+ ~ColorScales() override;
+
+ void setupMode();
+ SPColorScalesMode getMode() const;
+
+ static guchar const *hsluvHueMap(gfloat s, gfloat l,
+ std::array<guchar, 4 * 1024> *map);
+ static guchar const *hsluvSaturationMap(gfloat h, gfloat l,
+ std::array<guchar, 4 * 1024> *map);
+ static guchar const *hsluvLightnessMap(gfloat h, gfloat s,
+ std::array<guchar, 4 * 1024> *map);
+
+protected:
+ void _onColorChanged();
+ void on_show() override;
+
+ virtual void _initUI();
+
+ void _sliderAnyGrabbed();
+ void _sliderAnyReleased();
+ void _sliderAnyChanged();
+ void _adjustmentChanged(int channel);
+ void _wheelChanged();
+
+ void _getRgbaFloatv(gfloat *rgba);
+ void _getCmykaFloatv(gfloat *cmyka);
+ guint32 _getRgba32();
+ void _updateSliders(guint channels);
+ void _recalcColor();
+ void _updateDisplay(bool update_wheel = true);
+
+ void _setRangeLimit(gdouble upper);
+
+ SelectedColor &_color;
+ gdouble _range_limit;
+ gboolean _updating : 1;
+ gboolean _dragging : 1;
+ std::vector<Glib::RefPtr<Gtk::Adjustment>> _a; /* Channel adjustments */
+ Inkscape::UI::Widget::ColorSlider *_s[5]; /* Channel sliders */
+ Gtk::Widget *_b[5]; /* Spinbuttons */
+ Gtk::Label *_l[5]; /* Labels */
+ std::array<guchar, 4 * 1024> _sliders_maps[4];
+ Inkscape::UI::Widget::ColorWheel *_wheel;
+
+ const Glib::ustring _prefs = "/color_scales";
+ static gchar const * const _pref_wheel_visibility;
+
+ sigc::connection _color_changed;
+ sigc::connection _color_dragged;
+
+private:
+ // By default, disallow copy constructor and assignment operator
+ ColorScales(ColorScales const &obj) = delete;
+ ColorScales &operator=(ColorScales const &obj) = delete;
+};
+
+template <SPColorScalesMode MODE>
+class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory
+{
+public:
+ ColorScalesFactory();
+
+ Gtk::Widget *createWidget(Inkscape::UI::SelectedColor &color) const override;
+ Glib::ustring modeName() const override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* !SEEN_SP_COLOR_SCALES_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/ui/widget/color-slider.cpp b/src/ui/widget/color-slider.cpp
new file mode 100644
index 0000000..6ffd266
--- /dev/null
+++ b/src/ui/widget/color-slider.cpp
@@ -0,0 +1,546 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A slider with colored background - implementation.
+ *//*
+ * Authors:
+ * see git history
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdkmm/cursor.h>
+#include <gdkmm/general.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/stylecontext.h>
+
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#include "preferences.h"
+
+static const gint SLIDER_WIDTH = 96;
+static const gint SLIDER_HEIGHT = 8;
+static const gint ARROW_SIZE = 8;
+
+static const guchar *sp_color_slider_render_gradient(gint x0, gint y0, gint width, gint height, gint c[], gint dc[],
+ guint b0, guint b1, guint mask);
+static const guchar *sp_color_slider_render_map(gint x0, gint y0, gint width, gint height, guchar *map, gint start,
+ gint step, guint b0, guint b1, guint mask);
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorSlider::ColorSlider(Glib::RefPtr<Gtk::Adjustment> adjustment)
+ : _dragging(false)
+ , _value(0.0)
+ , _oldvalue(0.0)
+ , _map(nullptr)
+{
+ _c0[0] = 0x00;
+ _c0[1] = 0x00;
+ _c0[2] = 0x00;
+ _c0[3] = 0xff;
+
+ _cm[0] = 0xff;
+ _cm[1] = 0x00;
+ _cm[2] = 0x00;
+ _cm[3] = 0xff;
+
+ _c0[0] = 0xff;
+ _c0[1] = 0xff;
+ _c0[2] = 0xff;
+ _c0[3] = 0xff;
+
+ _b0 = 0x5f;
+ _b1 = 0xa0;
+ _bmask = 0x08;
+
+ setAdjustment(adjustment);
+}
+
+ColorSlider::~ColorSlider()
+{
+ if (_adjustment) {
+ _adjustment_changed_connection.disconnect();
+ _adjustment_value_changed_connection.disconnect();
+ _adjustment.reset();
+ }
+}
+
+void ColorSlider::on_realize()
+{
+ set_realized();
+
+ if (!_gdk_window) {
+ GdkWindowAttr attributes;
+ gint attributes_mask;
+ Gtk::Allocation allocation = get_allocation();
+
+ memset(&attributes, 0, sizeof(attributes));
+ attributes.x = allocation.get_x();
+ attributes.y = allocation.get_y();
+ attributes.width = allocation.get_width();
+ attributes.height = allocation.get_height();
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gdk_screen_get_system_visual(gdk_screen_get_default());
+ attributes.event_mask = get_events();
+ attributes.event_mask |= (Gdk::EXPOSURE_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK |
+ Gdk::POINTER_MOTION_MASK | Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
+
+ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ _gdk_window = Gdk::Window::create(get_parent_window(), &attributes, attributes_mask);
+ set_window(_gdk_window);
+ _gdk_window->set_user_data(gobj());
+ }
+}
+
+void ColorSlider::on_unrealize()
+{
+ _gdk_window.reset();
+
+ Gtk::Widget::on_unrealize();
+}
+
+void ColorSlider::on_size_allocate(Gtk::Allocation &allocation)
+{
+ set_allocation(allocation);
+
+ if (get_realized()) {
+ _gdk_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(),
+ allocation.get_height());
+ }
+}
+
+void ColorSlider::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const
+{
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border padding = style_context->get_padding(get_state_flags());
+ int width = SLIDER_WIDTH + padding.get_left() + padding.get_right();
+ minimum_width = natural_width = width;
+}
+
+void ColorSlider::get_preferred_width_for_height_vfunc(int /*height*/, int &minimum_width, int &natural_width) const
+{
+ get_preferred_width(minimum_width, natural_width);
+}
+
+void ColorSlider::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const
+{
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border padding = style_context->get_padding(get_state_flags());
+ int height = SLIDER_HEIGHT + padding.get_top() + padding.get_bottom();
+ minimum_height = natural_height = height;
+}
+
+void ColorSlider::get_preferred_height_for_width_vfunc(int /*width*/, int &minimum_height, int &natural_height) const
+{
+ get_preferred_height(minimum_height, natural_height);
+}
+
+bool ColorSlider::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 1) {
+ Gtk::Allocation allocation = get_allocation();
+ gint cx, cw;
+ cx = get_style_context()->get_padding(get_state_flags()).get_left();
+ cw = allocation.get_width() - 2 * cx;
+ signal_grabbed.emit();
+ _dragging = true;
+ _oldvalue = _value;
+ gfloat value = CLAMP((gfloat)(event->x - cx) / cw, 0.0, 1.0);
+ bool constrained = event->state & GDK_CONTROL_MASK;
+ ColorScales<>::setScaled(_adjustment, value, constrained);
+ signal_dragged.emit();
+
+ auto window = _gdk_window->gobj();
+
+ auto seat = gdk_event_get_seat(reinterpret_cast<GdkEvent *>(event));
+ gdk_seat_grab(seat,
+ window,
+ GDK_SEAT_CAPABILITY_ALL_POINTING,
+ FALSE,
+ nullptr,
+ reinterpret_cast<GdkEvent *>(event),
+ nullptr,
+ nullptr);
+ }
+
+ return false;
+}
+
+bool ColorSlider::on_button_release_event(GdkEventButton *event)
+{
+ if (event->button == 1) {
+ gdk_seat_ungrab(gdk_event_get_seat(reinterpret_cast<GdkEvent *>(event)));
+ _dragging = false;
+ signal_released.emit();
+ if (_value != _oldvalue) {
+ signal_value_changed.emit();
+ }
+ }
+
+ return false;
+}
+
+bool ColorSlider::on_motion_notify_event(GdkEventMotion *event)
+{
+ if (_dragging) {
+ gint cx, cw;
+ Gtk::Allocation allocation = get_allocation();
+ cx = get_style_context()->get_padding(get_state_flags()).get_left();
+ cw = allocation.get_width() - 2 * cx;
+ gfloat value = CLAMP((gfloat)(event->x - cx) / cw, 0.0, 1.0);
+ bool constrained = event->state & GDK_CONTROL_MASK;
+ ColorScales<>::setScaled(_adjustment, value, constrained);
+ signal_dragged.emit();
+ }
+
+ return false;
+}
+
+void ColorSlider::setAdjustment(Glib::RefPtr<Gtk::Adjustment> adjustment)
+{
+ if (!adjustment) {
+ _adjustment = Gtk::Adjustment::create(0.0, 0.0, 1.0, 0.01, 0.0, 0.0);
+ }
+ else {
+ adjustment->set_page_increment(0.0);
+ adjustment->set_page_size(0.0);
+ }
+
+ if (_adjustment != adjustment) {
+ if (_adjustment) {
+ _adjustment_changed_connection.disconnect();
+ _adjustment_value_changed_connection.disconnect();
+ }
+
+ _adjustment = adjustment;
+ _adjustment_changed_connection =
+ _adjustment->signal_changed().connect(sigc::mem_fun(this, &ColorSlider::_onAdjustmentChanged));
+ _adjustment_value_changed_connection =
+ _adjustment->signal_value_changed().connect(sigc::mem_fun(this, &ColorSlider::_onAdjustmentValueChanged));
+
+ _value = ColorScales<>::getScaled(_adjustment);
+
+ _onAdjustmentChanged();
+ }
+}
+
+void ColorSlider::_onAdjustmentChanged() { queue_draw(); }
+
+void ColorSlider::_onAdjustmentValueChanged()
+{
+ if (_value != ColorScales<>::getScaled(_adjustment)) {
+ gint cx, cy, cw, ch;
+ auto style_context = get_style_context();
+ auto allocation = get_allocation();
+ auto padding = style_context->get_padding(get_state_flags());
+ cx = padding.get_left();
+ cy = padding.get_top();
+ cw = allocation.get_width() - 2 * cx;
+ ch = allocation.get_height() - 2 * cy;
+ if ((gint)(ColorScales<>::getScaled(_adjustment) * cw) != (gint)(_value * cw)) {
+ gint ax, ay;
+ gfloat value;
+ value = _value;
+ _value = ColorScales<>::getScaled(_adjustment);
+ ax = (int)(cx + value * cw - ARROW_SIZE / 2 - 2);
+ ay = cy;
+ queue_draw_area(ax, ay, ARROW_SIZE + 4, ch);
+ ax = (int)(cx + _value * cw - ARROW_SIZE / 2 - 2);
+ ay = cy;
+ queue_draw_area(ax, ay, ARROW_SIZE + 4, ch);
+ }
+ else {
+ _value = ColorScales<>::getScaled(_adjustment);
+ }
+ }
+}
+
+void ColorSlider::setColors(guint32 start, guint32 mid, guint32 end)
+{
+ // Remove any map, if set
+ _map = nullptr;
+
+ _c0[0] = start >> 24;
+ _c0[1] = (start >> 16) & 0xff;
+ _c0[2] = (start >> 8) & 0xff;
+ _c0[3] = start & 0xff;
+
+ _cm[0] = mid >> 24;
+ _cm[1] = (mid >> 16) & 0xff;
+ _cm[2] = (mid >> 8) & 0xff;
+ _cm[3] = mid & 0xff;
+
+ _c1[0] = end >> 24;
+ _c1[1] = (end >> 16) & 0xff;
+ _c1[2] = (end >> 8) & 0xff;
+ _c1[3] = end & 0xff;
+
+ queue_draw();
+}
+
+void ColorSlider::setMap(const guchar *map)
+{
+ _map = const_cast<guchar *>(map);
+
+ queue_draw();
+}
+
+void ColorSlider::setBackground(guint dark, guint light, guint size)
+{
+ _b0 = dark;
+ _b1 = light;
+ _bmask = size;
+
+ queue_draw();
+}
+
+bool ColorSlider::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ gboolean colorsOnTop = Inkscape::Preferences::get()->getBool("/options/workarounds/colorsontop", false);
+
+ auto allocation = get_allocation();
+ auto style_context = get_style_context();
+
+ // Draw shadow
+ if (colorsOnTop) {
+ style_context->render_frame(cr, 0, 0, allocation.get_width(), allocation.get_height());
+ }
+
+ /* Paintable part of color gradient area */
+ Gdk::Rectangle carea;
+ Gtk::Border padding;
+
+ padding = style_context->get_padding(get_state_flags());
+
+ int scale = style_context->get_scale();
+ carea.set_x(padding.get_left() * scale);
+ carea.set_y(padding.get_top() * scale);
+
+ carea.set_width(allocation.get_width() * scale - 2 * carea.get_x());
+ carea.set_height(allocation.get_height() * scale - 2 * carea.get_y());
+
+ cr->save();
+ // changing scale to draw pixmap at display resolution
+ cr->scale(1.0 / scale, 1.0 / scale);
+
+ if (_map) {
+ /* Render map pixelstore */
+ gint d = (1024 << 16) / carea.get_width();
+ gint s = 0;
+
+ const guchar *b =
+ sp_color_slider_render_map(0, 0, carea.get_width(), carea.get_height(), _map, s, d, _b0, _b1, _bmask * scale);
+
+ if (b != nullptr && carea.get_width() > 0) {
+ Glib::RefPtr<Gdk::Pixbuf> pb = Gdk::Pixbuf::create_from_data(
+ b, Gdk::COLORSPACE_RGB, false, 8, carea.get_width(), carea.get_height(), carea.get_width() * 3);
+
+ Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_x(), carea.get_y());
+ cr->paint();
+ }
+ }
+ else {
+ gint c[4], dc[4];
+
+ /* Render gradient */
+
+ // part 1: from c0 to cm
+ if (carea.get_width() > 0) {
+ for (gint i = 0; i < 4; i++) {
+ c[i] = _c0[i] << 16;
+ dc[i] = ((_cm[i] << 16) - c[i]) / (carea.get_width() / 2);
+ }
+ guint wi = carea.get_width() / 2;
+ const guchar *b = sp_color_slider_render_gradient(0, 0, wi, carea.get_height(), c, dc, _b0, _b1, _bmask * scale);
+
+ /* Draw pixelstore 1 */
+ if (b != nullptr && wi > 0) {
+ Glib::RefPtr<Gdk::Pixbuf> pb =
+ Gdk::Pixbuf::create_from_data(b, Gdk::COLORSPACE_RGB, false, 8, wi, carea.get_height(), wi * 3);
+
+ Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_x(), carea.get_y());
+ cr->paint();
+ }
+ }
+
+ // part 2: from cm to c1
+ if (carea.get_width() > 0) {
+ for (gint i = 0; i < 4; i++) {
+ c[i] = _cm[i] << 16;
+ dc[i] = ((_c1[i] << 16) - c[i]) / (carea.get_width() / 2);
+ }
+ guint wi = carea.get_width() / 2;
+ const guchar *b = sp_color_slider_render_gradient(carea.get_width() / 2, 0, wi, carea.get_height(), c, dc,
+ _b0, _b1, _bmask * scale);
+
+ /* Draw pixelstore 2 */
+ if (b != nullptr && wi > 0) {
+ Glib::RefPtr<Gdk::Pixbuf> pb =
+ Gdk::Pixbuf::create_from_data(b, Gdk::COLORSPACE_RGB, false, 8, wi, carea.get_height(), wi * 3);
+
+ Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_width() / 2 + carea.get_x(), carea.get_y());
+ cr->paint();
+ }
+ }
+ }
+
+ cr->restore();
+
+ /* Draw shadow */
+ if (!colorsOnTop) {
+ style_context->render_frame(cr, 0, 0, allocation.get_width(), allocation.get_height());
+ }
+
+ /* Draw arrow */
+ gint x = (int)(_value * (carea.get_width() / scale) - ARROW_SIZE / 2 + carea.get_x() / scale);
+ gint y1 = carea.get_y() / scale;
+ gint y2 = carea.get_y() / scale + carea.get_height() / scale - 1;
+ cr->set_line_width(2.0);
+
+ // Define top arrow
+ cr->move_to(x - 0.5, y1 + 0.5);
+ cr->line_to(x + ARROW_SIZE - 0.5, y1 + 0.5);
+ cr->line_to(x + (ARROW_SIZE - 1) / 2.0, y1 + ARROW_SIZE / 2.0 + 0.5);
+ cr->close_path();
+
+ // Define bottom arrow
+ cr->move_to(x - 0.5, y2 + 0.5);
+ cr->line_to(x + ARROW_SIZE - 0.5, y2 + 0.5);
+ cr->line_to(x + (ARROW_SIZE - 1) / 2.0, y2 - ARROW_SIZE / 2.0 + 0.5);
+ cr->close_path();
+
+ // Render both arrows
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->stroke_preserve();
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->fill();
+
+ return false;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/* Colors are << 16 */
+
+inline bool checkerboard(gint x, gint y, guint size) {
+ return ((x / size) & 1) != ((y / size) & 1);
+}
+
+static const guchar *sp_color_slider_render_gradient(gint x0, gint y0, gint width, gint height, gint c[], gint dc[],
+ guint b0, guint b1, guint mask)
+{
+ static guchar *buf = nullptr;
+ static gint bs = 0;
+ guchar *dp;
+ gint x, y;
+ guint r, g, b, a;
+
+ if (buf && (bs < width * height)) {
+ g_free(buf);
+ buf = nullptr;
+ }
+ if (!buf) {
+ buf = g_new(guchar, width * height * 3);
+ bs = width * height;
+ }
+
+ dp = buf;
+ r = c[0];
+ g = c[1];
+ b = c[2];
+ a = c[3];
+ for (x = x0; x < x0 + width; x++) {
+ gint cr, cg, cb, ca;
+ guchar *d;
+ cr = r >> 16;
+ cg = g >> 16;
+ cb = b >> 16;
+ ca = a >> 16;
+ d = dp;
+ for (y = y0; y < y0 + height; y++) {
+ guint bg, fc;
+ /* Background value */
+ bg = checkerboard(x, y, mask) ? b0 : b1;
+ fc = (cr - bg) * ca;
+ d[0] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ fc = (cg - bg) * ca;
+ d[1] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ fc = (cb - bg) * ca;
+ d[2] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ d += 3 * width;
+ }
+ r += dc[0];
+ g += dc[1];
+ b += dc[2];
+ a += dc[3];
+ dp += 3;
+ }
+
+ return buf;
+}
+
+/* Positions are << 16 */
+
+static const guchar *sp_color_slider_render_map(gint x0, gint y0, gint width, gint height, guchar *map, gint start,
+ gint step, guint b0, guint b1, guint mask)
+{
+ static guchar *buf = nullptr;
+ static gint bs = 0;
+ guchar *dp;
+ gint x, y;
+
+ if (buf && (bs < width * height)) {
+ g_free(buf);
+ buf = nullptr;
+ }
+ if (!buf) {
+ buf = g_new(guchar, width * height * 3);
+ bs = width * height;
+ }
+
+ dp = buf;
+ for (x = x0; x < x0 + width; x++) {
+ gint cr, cg, cb, ca;
+ guchar *d = dp;
+ guchar *sp = map + 4 * (start >> 16);
+ cr = *sp++;
+ cg = *sp++;
+ cb = *sp++;
+ ca = *sp++;
+ for (y = y0; y < y0 + height; y++) {
+ guint bg, fc;
+ /* Background value */
+ bg = checkerboard(x, y, mask) ? b0 : b1;
+ fc = (cr - bg) * ca;
+ d[0] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ fc = (cg - bg) * ca;
+ d[1] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ fc = (cb - bg) * ca;
+ d[2] = bg + ((fc + (fc >> 8) + 0x80) >> 8);
+ d += 3 * width;
+ }
+ dp += 3;
+ start += step;
+ }
+
+ return buf;
+}
+/*
+ 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/ui/widget/color-slider.h b/src/ui/widget/color-slider.h
new file mode 100644
index 0000000..963e107
--- /dev/null
+++ b/src/ui/widget/color-slider.h
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors:
+ * see git history
+* Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_COLOR_SLIDER_H
+#define SEEN_COLOR_SLIDER_H
+
+#include <gtkmm/widget.h>
+#include <sigc++/signal.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*
+ * A slider with colored background
+ */
+class ColorSlider : public Gtk::Widget {
+public:
+ ColorSlider(Glib::RefPtr<Gtk::Adjustment> adjustment);
+ ~ColorSlider() override;
+
+ void setAdjustment(Glib::RefPtr<Gtk::Adjustment> adjustment);
+
+ void setColors(guint32 start, guint32 mid, guint32 end);
+
+ void setMap(const guchar *map);
+
+ void setBackground(guint dark, guint light, guint size);
+
+ sigc::signal<void> signal_grabbed;
+ sigc::signal<void> signal_dragged;
+ sigc::signal<void> signal_released;
+ sigc::signal<void> signal_value_changed;
+
+protected:
+ void on_size_allocate(Gtk::Allocation &allocation) override;
+ void on_realize() override;
+ void on_unrealize() override;
+ bool on_button_press_event(GdkEventButton *event) override;
+ bool on_button_release_event(GdkEventButton *event) override;
+ bool on_motion_notify_event(GdkEventMotion *event) override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+ void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override;
+ void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override;
+ void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override;
+ void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override;
+
+private:
+ void _onAdjustmentChanged();
+ void _onAdjustmentValueChanged();
+
+ bool _dragging;
+
+ Glib::RefPtr<Gtk::Adjustment> _adjustment;
+ sigc::connection _adjustment_changed_connection;
+ sigc::connection _adjustment_value_changed_connection;
+
+ gfloat _value;
+ gfloat _oldvalue;
+ guchar _c0[4], _cm[4], _c1[4];
+ guchar _b0, _b1;
+ guchar _bmask;
+
+ guchar *_map;
+
+ Glib::RefPtr<Gdk::Window> _gdk_window;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#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/ui/widget/combo-box-entry-tool-item.cpp b/src/ui/widget/combo-box-entry-tool-item.cpp
new file mode 100644
index 0000000..865d827
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.cpp
@@ -0,0 +1,707 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A class derived from Gtk::ToolItem that wraps a GtkComboBoxEntry.
+ * Features:
+ * Setting GtkEntryBox width in characters.
+ * Passing a function for formatting cells.
+ * Displaying a warning if entry text isn't in list.
+ * Check comma separated values in text against list. (Useful for font-family fallbacks.)
+ * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry)
+ * to allow setting resources.
+ *
+ * Author(s):
+ * Tavmjong Bah
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/*
+ * We must provide for both a toolbar item and a menu item.
+ * As we don't know which widgets are used (or even constructed),
+ * we must keep track of things like active entry ourselves.
+ */
+
+#include "combo-box-entry-tool-item.h"
+
+#include <cassert>
+#include <iostream>
+#include <cstring>
+#include <glibmm/ustring.h>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <gdkmm/display.h>
+
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ComboBoxEntryToolItem::ComboBoxEntryToolItem(Glib::ustring name,
+ Glib::ustring label,
+ Glib::ustring tooltip,
+ GtkTreeModel *model,
+ gint entry_width,
+ gint extra_width,
+ void *cell_data_func,
+ void *separator_func,
+ GtkWidget *focusWidget)
+ : _label(std::move(label)),
+ _tooltip(std::move(tooltip)),
+ _model(model),
+ _entry_width(entry_width),
+ _extra_width(extra_width),
+ _cell_data_func(cell_data_func),
+ _separator_func(separator_func),
+ _focusWidget(focusWidget),
+ _active(-1),
+ _text(strdup("")),
+ _entry_completion(nullptr),
+ _popup(false),
+ _info(nullptr),
+ _info_cb(nullptr),
+ _info_cb_id(0),
+ _info_cb_blocked(false),
+ _warning(nullptr),
+ _warning_cb(nullptr),
+ _warning_cb_id(0),
+ _warning_cb_blocked(false),
+ _isload(true),
+ _markup(false)
+
+{
+ set_name(name);
+
+ gchar *action_name = g_strdup( get_name().c_str() );
+ gchar *combobox_name = g_strjoin( nullptr, action_name, "_combobox", nullptr );
+ gchar *entry_name = g_strjoin( nullptr, action_name, "_entry", nullptr );
+ g_free( action_name );
+
+ GtkWidget* comboBoxEntry = gtk_combo_box_new_with_model_and_entry (_model);
+ gtk_combo_box_set_entry_text_column (GTK_COMBO_BOX (comboBoxEntry), 0);
+
+ // Name it so we can muck with it using an RC file
+ gtk_widget_set_name( comboBoxEntry, combobox_name );
+ g_free( combobox_name );
+
+ {
+ gtk_widget_set_halign(comboBoxEntry, GTK_ALIGN_START);
+ gtk_widget_set_hexpand(comboBoxEntry, FALSE);
+ gtk_widget_set_vexpand(comboBoxEntry, FALSE);
+ add(*Glib::wrap(comboBoxEntry));
+ }
+
+ _combobox = GTK_COMBO_BOX (comboBoxEntry);
+
+ //gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), ink_comboboxentry_action->active );
+ gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), 0 );
+
+ g_signal_connect( G_OBJECT(comboBoxEntry), "changed", G_CALLBACK(combo_box_changed_cb), this );
+
+ // Optionally add separator function...
+ if( _separator_func != nullptr ) {
+ gtk_combo_box_set_row_separator_func( _combobox,
+ GtkTreeViewRowSeparatorFunc (_separator_func),
+ nullptr, nullptr );
+ }
+
+ // Optionally add formatting...
+ if( _cell_data_func != nullptr ) {
+ gtk_combo_box_set_popup_fixed_width (GTK_COMBO_BOX(comboBoxEntry), false);
+ this->_cell = gtk_cell_renderer_text_new();
+ int total = gtk_tree_model_iter_n_children (model, nullptr);
+ int height = 30;
+ if (total > 1000) {
+ height = 30000/total;
+ g_warning("You have a huge number of font families (%d), "
+ "and Cairo is limiting the size of widgets you can draw.\n"
+ "Your preview cell height is capped to %d.",
+ total, height);
+ }
+ gtk_cell_renderer_set_fixed_size(_cell, -1, height);
+ g_signal_connect(G_OBJECT(comboBoxEntry), "popup", G_CALLBACK(combo_box_popup_cb), this);
+ gtk_cell_layout_clear( GTK_CELL_LAYOUT( comboBoxEntry ) );
+ gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( comboBoxEntry ), _cell, true );
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT( comboBoxEntry ), _cell,
+ GtkCellLayoutDataFunc (_cell_data_func), 0, nullptr );
+ }
+
+ // Optionally widen the combobox width... which widens the drop-down list in list mode.
+ if( _extra_width > 0 ) {
+ GtkRequisition req;
+ gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr);
+ gtk_widget_set_size_request( GTK_WIDGET( _combobox ),
+ req.width + _extra_width, -1 );
+ }
+
+ // Get reference to GtkEntry and fiddle a bit with it.
+ GtkWidget *child = gtk_bin_get_child( GTK_BIN(comboBoxEntry) );
+
+ // Name it so we can muck with it using an RC file
+ gtk_widget_set_name( child, entry_name );
+ g_free( entry_name );
+
+ if( child && GTK_IS_ENTRY( child ) ) {
+
+ _entry = GTK_ENTRY(child);
+
+ // Change width
+ if( _entry_width > 0 ) {
+ gtk_entry_set_width_chars (GTK_ENTRY (child), _entry_width );
+ }
+
+ // Add pop-up entry completion if required
+ if( _popup ) {
+ popup_enable();
+ }
+
+ // Add signal for GtkEntry to check if finished typing.
+ g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(entry_activate_cb), this );
+ g_signal_connect( G_OBJECT(child), "key-press-event", G_CALLBACK(keypress_cb), this );
+ }
+
+ set_tooltip(_tooltip.c_str());
+
+ show_all();
+}
+
+// Setters/Getters ---------------------------------------------------
+
+Glib::ustring
+ComboBoxEntryToolItem::get_active_text()
+{
+ assert(_text);
+ return _text;
+}
+
+/*
+ * For the font-family list we need to handle two cases:
+ * Text is in list store:
+ * In this case we use row number as the font-family list can have duplicate
+ * entries, one in the document font part and one in the system font part. In
+ * order that scrolling through the list works properly we must distinguish
+ * between the two.
+ * Text is not in the list store (i.e. default font-family is not on system):
+ * In this case we have a row number of -1, and the text must be set by hand.
+ */
+gboolean
+ComboBoxEntryToolItem::set_active_text(const gchar* text, int row)
+{
+ if( strcmp( _text, text ) != 0 ) {
+ g_free( _text );
+ _text = g_strdup( text );
+ }
+
+ // Get active row or -1 if none
+ if( row < 0 ) {
+ row = get_active_row_from_text(this, _text);
+ }
+ _active = row;
+
+ // Set active row, check that combobox has been created.
+ if( _combobox ) {
+ gtk_combo_box_set_active( GTK_COMBO_BOX( _combobox ), _active );
+ }
+
+ // Fiddle with entry
+ if( _entry ) {
+
+ // Explicitly set text in GtkEntry box (won't be set if text not in list).
+ gtk_entry_set_text( _entry, text );
+
+ // Show or hide warning -- this might be better moved to text-toolbox.cpp
+ if( _info_cb_id != 0 &&
+ !_info_cb_blocked ) {
+ g_signal_handler_block (G_OBJECT(_entry),
+ _info_cb_id );
+ _info_cb_blocked = true;
+ }
+ if( _warning_cb_id != 0 &&
+ !_warning_cb_blocked ) {
+ g_signal_handler_block (G_OBJECT(_entry),
+ _warning_cb_id );
+ _warning_cb_blocked = true;
+ }
+
+ bool set = false;
+ if( _warning != nullptr ) {
+ Glib::ustring missing = check_comma_separated_text();
+ if( !missing.empty() ) {
+ gtk_entry_set_icon_from_icon_name( _entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ INKSCAPE_ICON("dialog-warning") );
+ // Can't add tooltip until icon set
+ Glib::ustring warning = _warning;
+ warning += ": ";
+ warning += missing;
+ gtk_entry_set_icon_tooltip_text( _entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ warning.c_str() );
+
+ if( _warning_cb ) {
+
+ // Add callback if we haven't already
+ if( _warning_cb_id == 0 ) {
+ _warning_cb_id =
+ g_signal_connect( G_OBJECT(_entry),
+ "icon-press",
+ G_CALLBACK(_warning_cb),
+ this);
+ }
+ // Unblock signal
+ if( _warning_cb_blocked ) {
+ g_signal_handler_unblock (G_OBJECT(_entry),
+ _warning_cb_id );
+ _warning_cb_blocked = false;
+ }
+ }
+ set = true;
+ }
+ }
+
+ if( !set && _info != nullptr ) {
+ gtk_entry_set_icon_from_icon_name( GTK_ENTRY(_entry),
+ GTK_ENTRY_ICON_SECONDARY,
+ INKSCAPE_ICON("edit-select-all") );
+ gtk_entry_set_icon_tooltip_text( _entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ _info );
+
+ if( _info_cb ) {
+ // Add callback if we haven't already
+ if( _info_cb_id == 0 ) {
+ _info_cb_id =
+ g_signal_connect( G_OBJECT(_entry),
+ "icon-press",
+ G_CALLBACK(_info_cb),
+ this);
+ }
+ // Unblock signal
+ if( _info_cb_blocked ) {
+ g_signal_handler_unblock (G_OBJECT(_entry),
+ _info_cb_id );
+ _info_cb_blocked = false;
+ }
+ }
+ set = true;
+ }
+
+ if( !set ) {
+ gtk_entry_set_icon_from_icon_name( GTK_ENTRY(_entry),
+ GTK_ENTRY_ICON_SECONDARY,
+ nullptr );
+ }
+ }
+
+ // Return if active text in list
+ gboolean found = ( _active != -1 );
+ return found;
+}
+
+void
+ComboBoxEntryToolItem::set_entry_width(gint entry_width)
+{
+ _entry_width = entry_width;
+
+ // Clamp to limits
+ if(entry_width < -1) entry_width = -1;
+ if(entry_width > 100) entry_width = 100;
+
+ // Widget may not have been created....
+ if( _entry ) {
+ gtk_entry_set_width_chars( GTK_ENTRY(_entry), entry_width );
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_extra_width( gint extra_width )
+{
+ _extra_width = extra_width;
+
+ // Clamp to limits
+ if(extra_width < -1) extra_width = -1;
+ if(extra_width > 500) extra_width = 500;
+
+ // Widget may not have been created....
+ if( _combobox ) {
+ GtkRequisition req;
+ gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr);
+ gtk_widget_set_size_request( GTK_WIDGET( _combobox ), req.width + _extra_width, -1 );
+ }
+}
+
+void
+ComboBoxEntryToolItem::focus_on_click( bool focus_on_click )
+{
+ if (_combobox) {
+ gtk_widget_set_focus_on_click(GTK_WIDGET(_combobox), focus_on_click);
+ }
+}
+
+void
+ComboBoxEntryToolItem::popup_enable()
+{
+ _popup = true;
+
+ // Widget may not have been created....
+ if( _entry ) {
+
+ // Check we don't already have a GtkEntryCompletion
+ if( _entry_completion ) return;
+
+ _entry_completion = gtk_entry_completion_new();
+
+ gtk_entry_set_completion( _entry, _entry_completion );
+ gtk_entry_completion_set_model( _entry_completion, _model );
+ gtk_entry_completion_set_text_column( _entry_completion, 0 );
+ gtk_entry_completion_set_popup_completion( _entry_completion, true );
+ gtk_entry_completion_set_inline_completion( _entry_completion, false );
+ gtk_entry_completion_set_inline_selection( _entry_completion, true );
+
+ g_signal_connect (G_OBJECT (_entry_completion), "match-selected", G_CALLBACK (match_selected_cb), this);
+ }
+}
+
+void
+ComboBoxEntryToolItem::popup_disable()
+{
+ _popup = false;
+
+ if( _entry_completion ) {
+ gtk_widget_destroy(GTK_WIDGET(_entry_completion));
+ _entry_completion = nullptr;
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_tooltip(const gchar* tooltip)
+{
+ set_tooltip_text(tooltip);
+ gtk_widget_set_tooltip_text ( GTK_WIDGET(_combobox), tooltip);
+
+ // Widget may not have been created....
+ if( _entry ) {
+ gtk_widget_set_tooltip_text ( GTK_WIDGET(_entry), tooltip);
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_info(const gchar* info)
+{
+ g_free( _info );
+ _info = g_strdup( info );
+
+ // Widget may not have been created....
+ if( _entry ) {
+ gtk_entry_set_icon_tooltip_text( GTK_ENTRY(_entry),
+ GTK_ENTRY_ICON_SECONDARY,
+ _info );
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_info_cb(gpointer info_cb)
+{
+ _info_cb = info_cb;
+}
+
+void
+ComboBoxEntryToolItem::set_warning(const gchar* warning)
+{
+ g_free( _warning );
+ _warning = g_strdup( warning );
+
+ // Widget may not have been created....
+ if( _entry ) {
+ gtk_entry_set_icon_tooltip_text( GTK_ENTRY(_entry),
+ GTK_ENTRY_ICON_SECONDARY,
+ _warning );
+ }
+}
+
+void
+ComboBoxEntryToolItem::set_warning_cb(gpointer warning_cb)
+{
+ _warning_cb = warning_cb;
+}
+
+// Internal ---------------------------------------------------
+
+// Return row of active text or -1 if not found. If exclude is true,
+// use 3d column if available to exclude row from checking (useful to
+// skip rows added for font-families included in doc and not on
+// system)
+gint
+ComboBoxEntryToolItem::get_active_row_from_text(ComboBoxEntryToolItem *action,
+ const gchar *target_text,
+ gboolean exclude,
+ gboolean ignore_case )
+{
+ // Check if text in list
+ gint row = 0;
+ gboolean found = false;
+ GtkTreeIter iter;
+ gboolean valid = gtk_tree_model_get_iter_first( action->_model, &iter );
+ while ( valid ) {
+
+ // See if we should exclude a row
+ gboolean check = true; // If true, font-family is on system.
+ if( exclude && gtk_tree_model_get_n_columns( action->_model ) > 2 ) {
+ gtk_tree_model_get( action->_model, &iter, 2, &check, -1 );
+ }
+
+ if( check ) {
+ // Get text from list entry
+ gchar* text = nullptr;
+ gtk_tree_model_get( action->_model, &iter, 0, &text, -1 ); // Column 0
+
+ if( !ignore_case ) {
+ // Case sensitive compare
+ if( strcmp( target_text, text ) == 0 ){
+ found = true;
+ g_free(text);
+ break;
+ }
+ } else {
+ // Case insensitive compare
+ gchar* target_text_casefolded = g_utf8_casefold( target_text, -1 );
+ gchar* text_casefolded = g_utf8_casefold( text, -1 );
+ gboolean equal = (strcmp( target_text_casefolded, text_casefolded ) == 0 );
+ g_free( text_casefolded );
+ g_free( target_text_casefolded );
+ if( equal ) {
+ found = true;
+ g_free(text);
+ break;
+ }
+ }
+ g_free(text);
+ }
+
+ ++row;
+ valid = gtk_tree_model_iter_next( action->_model, &iter );
+ }
+
+ if( !found ) row = -1;
+
+ return row;
+}
+
+// Checks if all comma separated text fragments are in the list and
+// returns a ustring with a list of missing fragments.
+// This is useful for checking if all fonts in a font-family fallback
+// list are available on the system.
+//
+// This routine could also create a Pango Markup string to show which
+// fragments are invalid in the entry box itself. See:
+// http://developer.gnome.org/pango/stable/PangoMarkupFormat.html
+// However... it appears that while one can retrieve the PangoLayout
+// for a GtkEntry box, it is only a copy and changing it has no effect.
+// PangoLayout * pl = gtk_entry_get_layout( entry );
+// pango_layout_set_markup( pl, "NEW STRING", -1 ); // DOESN'T WORK
+Glib::ustring
+ComboBoxEntryToolItem::check_comma_separated_text()
+{
+ Glib::ustring missing;
+
+ // Parse fallback_list using a comma as deliminator
+ gchar** tokens = g_strsplit( _text, ",", 0 );
+
+ gint i = 0;
+ while( tokens[i] != nullptr ) {
+
+ // Remove any surrounding white space.
+ g_strstrip( tokens[i] );
+
+ if( get_active_row_from_text( this, tokens[i], true, true ) == -1 ) {
+ missing += tokens[i];
+ missing += ", ";
+ }
+ ++i;
+ }
+ g_strfreev( tokens );
+
+ // Remove extra comma and space from end.
+ if( missing.size() >= 2 ) {
+ missing.resize( missing.size()-2 );
+ }
+ return missing;
+}
+
+// Callbacks ---------------------------------------------------
+
+void
+ComboBoxEntryToolItem::combo_box_changed_cb( GtkComboBox* widget, gpointer data )
+{
+ // Two things can happen to get here:
+ // An item is selected in the drop-down menu.
+ // Text is typed.
+ // We only react here if an item is selected.
+
+ // Get action
+ auto action = reinterpret_cast<ComboBoxEntryToolItem *>( data );
+
+ // Check if item selected:
+ gint newActive = gtk_combo_box_get_active(widget);
+ if( newActive >= 0 && newActive != action->_active ) {
+
+ action->_active = newActive;
+
+ GtkTreeIter iter;
+ if( gtk_combo_box_get_active_iter( GTK_COMBO_BOX( action->_combobox ), &iter ) ) {
+
+ gchar* text = nullptr;
+ gtk_tree_model_get( action->_model, &iter, 0, &text, -1 );
+ gtk_entry_set_text( action->_entry, text );
+
+ g_free( action->_text );
+ action->_text = text;
+ }
+
+ // Now let the world know
+ action->_signal_changed.emit();
+ }
+}
+
+gboolean ComboBoxEntryToolItem::combo_box_popup_cb(ComboBoxEntryToolItem *widget, gpointer data)
+{
+ auto w = reinterpret_cast<ComboBoxEntryToolItem *>(data);
+ GtkComboBox *comboBoxEntry = GTK_COMBO_BOX(w->_combobox);
+ if (!w->_isload && !w->_markup && w->_cell_data_func) {
+ // first click is always displaying something wrong.
+ // Second loading of the screen should have preallocated space, and only has to render the text now
+ gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(comboBoxEntry), w->_cell,
+ GtkCellLayoutDataFunc(w->_cell_data_func), widget, nullptr);
+ w->_markup = true;
+ }
+ w->_isload = false;
+ return true;
+}
+
+void
+ComboBoxEntryToolItem::entry_activate_cb( GtkEntry *widget,
+ gpointer data )
+{
+ // Get text from entry box.. check if it matches a menu entry.
+
+ // Get action
+ auto action = reinterpret_cast<ComboBoxEntryToolItem*>( data );
+
+ // Get text
+ g_free( action->_text );
+ action->_text = g_strdup( gtk_entry_get_text( widget ) );
+
+ // Get row
+ action->_active =
+ get_active_row_from_text( action, action->_text );
+
+ // Set active row
+ gtk_combo_box_set_active( GTK_COMBO_BOX( action->_combobox), action->_active );
+
+ // Now let the world know
+ action->_signal_changed.emit();
+}
+
+gboolean
+ComboBoxEntryToolItem::match_selected_cb( GtkEntryCompletion* /*widget*/, GtkTreeModel* model, GtkTreeIter* iter, gpointer data )
+{
+ // Get action
+ auto action = reinterpret_cast<ComboBoxEntryToolItem*>(data);
+ GtkEntry *entry = action->_entry;
+
+ if( entry) {
+ gchar *family = nullptr;
+ gtk_tree_model_get(model, iter, 0, &family, -1);
+
+ // Set text in GtkEntry
+ gtk_entry_set_text (GTK_ENTRY (entry), family );
+
+ // Set text in ToolItem
+ g_free( action->_text );
+ action->_text = family;
+
+ // Get row
+ action->_active =
+ get_active_row_from_text( action, action->_text );
+
+ // Set active row
+ gtk_combo_box_set_active( GTK_COMBO_BOX( action->_combobox), action->_active );
+
+ // Now let the world know
+ action->_signal_changed.emit();
+
+ return true;
+ }
+ return false;
+}
+
+void
+ComboBoxEntryToolItem::defocus()
+{
+ if ( _focusWidget ) {
+ gtk_widget_grab_focus( _focusWidget );
+ }
+}
+
+gboolean
+ComboBoxEntryToolItem::keypress_cb( GtkWidget *entry, GdkEventKey *event, gpointer data )
+{
+ gboolean wasConsumed = FALSE; /* default to report event not consumed */
+ guint key = 0;
+ auto action = reinterpret_cast<ComboBoxEntryToolItem*>(data);
+ gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(),
+ event->hardware_keycode, (GdkModifierType)event->state,
+ 0, &key, nullptr, nullptr, nullptr );
+
+ switch ( key ) {
+
+ case GDK_KEY_Escape:
+ {
+ //gtk_spin_button_set_value( GTK_SPIN_BUTTON(widget), action->private_data->lastVal );
+ action->defocus();
+ wasConsumed = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Tab:
+ {
+ // Fire activation similar to how Return does, but also return focus to text object
+ // itself
+ entry_activate_cb( GTK_ENTRY (entry), data );
+ action->defocus();
+ wasConsumed = TRUE;
+ }
+ break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ action->defocus();
+ //wasConsumed = TRUE;
+ }
+ break;
+
+
+ }
+
+ return wasConsumed;
+}
+
+}
+}
+}
+
+/*
+ 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/ui/widget/combo-box-entry-tool-item.h b/src/ui/widget/combo-box-entry-tool-item.h
new file mode 100644
index 0000000..978f2ef
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.h
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A class derived from Gtk::ToolItem that wraps a GtkComboBoxEntry.
+ * Features:
+ * Setting GtkEntryBox width in characters.
+ * Passing a function for formatting cells.
+ * Displaying a warning if entry text isn't in list.
+ * Check comma separated values in text against list. (Useful for font-family fallbacks.)
+ * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry)
+ * to allow setting resources.
+ *
+ * Author(s):
+ * Tavmjong Bah
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2010 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INK_COMBOBOXENTRY_ACTION
+#define SEEN_INK_COMBOBOXENTRY_ACTION
+
+#include <gtkmm/toolitem.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Creates a Gtk::ToolItem subclass that wraps a Gtk::ComboBox object.
+ */
+class ComboBoxEntryToolItem : public Gtk::ToolItem {
+private:
+ Glib::ustring _tooltip;
+ Glib::ustring _label;
+ GtkTreeModel *_model; ///< Tree Model
+ GtkComboBox *_combobox;
+ GtkEntry *_entry;
+ gint _entry_width;// Width of GtkEntry in characters.
+ gint _extra_width;// Extra Width of GtkComboBox.. to widen drop-down list in list mode.
+ gpointer _cell_data_func; // drop-down menu format
+ gpointer _separator_func;
+ gboolean _popup; // Do we pop-up an entry-completion dialog?
+ GtkEntryCompletion *_entry_completion;
+ GtkWidget *_focusWidget; ///< The widget to return focus to
+ GtkCellRenderer *_cell;
+
+ gint _active; // Index of active menu item (-1 if not in list).
+ gchar *_text; // Text of active menu item or entry box.
+ gchar *_info; // Text for tooltip info about entry.
+ gpointer _info_cb; // Callback for clicking info icon.
+ gint _info_cb_id;
+ gboolean _info_cb_blocked;
+ gchar *_warning; // Text for tooltip warning that entry isn't in list.
+ gpointer _warning_cb; // Callback for clicking warning icon.
+ gint _warning_cb_id;
+ gboolean _warning_cb_blocked;
+ gboolean _isload;
+ gboolean _markup;
+
+ // Signals
+ sigc::signal<void> _signal_changed;
+
+ static gint get_active_row_from_text(ComboBoxEntryToolItem *action,
+ const gchar *target_text,
+ gboolean exclude = false,
+ gboolean ignore_case = false);
+ void defocus();
+
+ static void combo_box_changed_cb( GtkComboBox* widget, gpointer data );
+ static gboolean combo_box_popup_cb( ComboBoxEntryToolItem* widget, gpointer data );
+ static void entry_activate_cb( GtkEntry *widget,
+ gpointer data );
+ static gboolean match_selected_cb( GtkEntryCompletion *widget,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer data);
+ static gboolean keypress_cb( GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer data );
+
+ Glib::ustring check_comma_separated_text();
+
+public:
+ ComboBoxEntryToolItem(const Glib::ustring name,
+ const Glib::ustring label,
+ const Glib::ustring tooltip,
+ GtkTreeModel *model,
+ gint entry_width = -1,
+ gint extra_width = -1,
+ gpointer cell_data_func = nullptr,
+ gpointer separator_func = nullptr,
+ GtkWidget* focusWidget = nullptr);
+
+ Glib::ustring get_active_text();
+ gboolean set_active_text(const gchar* text, int row=-1);
+
+ void set_entry_width(gint entry_width);
+ void set_extra_width(gint extra_width);
+
+ void popup_enable();
+ void popup_disable();
+ void focus_on_click( bool focus_on_click );
+
+ void set_info( const gchar* info );
+ void set_info_cb( gpointer info_cb );
+ void set_warning( const gchar* warning_cb );
+ void set_warning_cb(gpointer warning );
+ void set_tooltip( const gchar* tooltip );
+
+ // Accessor methods
+ decltype(_model) get_model() const {return _model;}
+ decltype(_combobox) get_combobox() const {return _combobox;}
+ decltype(_entry) get_entry() const {return _entry;}
+ decltype(_entry_width) get_entry_width() const {return _entry_width;}
+ decltype(_extra_width) get_extra_width() const {return _extra_width;}
+ decltype(_cell_data_func) get_cell_data_func() const {return _cell_data_func;}
+ decltype(_separator_func) get_separator_func() const {return _separator_func;}
+ decltype(_popup) get_popup() const {return _popup;}
+ decltype(_focusWidget) get_focus_widget() const {return _focusWidget;}
+
+ decltype(_active) get_active() const {return _active;}
+
+ decltype(_signal_changed) signal_changed() {return _signal_changed;}
+
+ // Mutator methods
+ void set_model (decltype(_model) model) {_model = model;}
+ void set_combobox (decltype(_combobox) combobox) {_combobox = combobox;}
+ void set_entry (decltype(_entry) entry) {_entry = entry;}
+ void set_cell_data_func(decltype(_cell_data_func) cell_data_func) {_cell_data_func = cell_data_func;}
+ void set_separator_func(decltype(_separator_func) separator_func) {_separator_func = separator_func;}
+ void set_popup (decltype(_popup) popup) {_popup = popup;}
+ void set_focus_widget (decltype(_focusWidget) focus_widget) {_focusWidget = focus_widget;}
+
+ // This doesn't seem right... surely we should set the active row in the Combobox too?
+ void set_active (decltype(_active) active) {_active = active;}
+};
+
+}
+}
+}
+#endif /* SEEN_INK_COMBOBOXENTRY_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/ui/widget/combo-enums.h b/src/ui/widget/combo-enums.h
new file mode 100644
index 0000000..4647a00
--- /dev/null
+++ b/src/ui/widget/combo-enums.h
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_COMBO_ENUMS_H
+#define INKSCAPE_UI_WIDGET_COMBO_ENUMS_H
+
+#include "ui/widget/labelled.h"
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include "attr-widget.h"
+#include "util/enums.h"
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class ComboBoxEnum : public Gtk::ComboBox, public AttrWidget
+{
+private:
+ int on_sort_compare( const Gtk::TreeModel::iterator & a, const Gtk::TreeModel::iterator & b)
+ {
+ Glib::ustring an=(*a)[_columns.label];
+ Glib::ustring bn=(*b)[_columns.label];
+ return an.compare(bn);
+ }
+
+ bool _sort;
+
+public:
+ ComboBoxEnum(E default_value, const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true)
+ : AttrWidget(a, (unsigned int)default_value), setProgrammatically(false), _converter(c)
+ {
+ _sort = sort;
+
+ signal_changed().connect(signal_attr_changed().make_slot());
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event));
+ _model = Gtk::ListStore::create(_columns);
+ set_model(_model);
+
+ pack_start(_columns.label);
+
+ // Initialize list
+ for(int i = 0; i < static_cast<int>(_converter._length); ++i) {
+ Gtk::TreeModel::Row row = *_model->append();
+ const Util::EnumData<E>* data = &_converter.data(i);
+ row[_columns.data] = data;
+ row[_columns.label] = _( _converter.get_label(data->id).c_str() );
+ }
+ set_active_by_id(default_value);
+
+ // Sort the list
+ if (sort) {
+ _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare));
+ _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING);
+ }
+ }
+
+ ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true)
+ : AttrWidget(a, (unsigned int) 0), setProgrammatically(false), _converter(c)
+ {
+ _sort = sort;
+
+ signal_changed().connect(signal_attr_changed().make_slot());
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event));
+
+ _model = Gtk::ListStore::create(_columns);
+ set_model(_model);
+
+ pack_start(_columns.label);
+
+ // Initialize list
+ for(unsigned int i = 0; i < _converter._length; ++i) {
+ Gtk::TreeModel::Row row = *_model->append();
+ const Util::EnumData<E>* data = &_converter.data(i);
+ row[_columns.data] = data;
+ row[_columns.label] = _( _converter.get_label(data->id).c_str() );
+ }
+ set_active(0);
+
+ // Sort the list
+ if (_sort) {
+ _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare));
+ _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING);
+ }
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ return get_active_data()->key;
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ setProgrammatically = true;
+ const gchar* val = attribute_value(o);
+ if(val)
+ set_active_by_id(_converter.get_id_from_key(val));
+ else
+ set_active(get_default()->as_uint());
+ }
+
+ const Util::EnumData<E>* get_active_data() const
+ {
+ Gtk::TreeModel::iterator i = this->get_active();
+ if(i)
+ return (*i)[_columns.data];
+ return nullptr;
+ }
+
+ void add_row(const Glib::ustring& s)
+ {
+ Gtk::TreeModel::Row row = *_model->append();
+ row[_columns.data] = 0;
+ row[_columns.label] = s;
+ }
+
+ void remove_row(E id) {
+ Gtk::TreeModel::iterator i;
+
+ for(i = _model->children().begin(); i != _model->children().end(); ++i) {
+ const Util::EnumData<E>* data = (*i)[_columns.data];
+
+ if(data->id == id)
+ break;
+ }
+
+ if(i != _model->children().end())
+ _model->erase(i);
+ }
+
+ void set_active_by_id(E id) {
+ setProgrammatically = true;
+ for(Gtk::TreeModel::iterator i = _model->children().begin();
+ i != _model->children().end(); ++i)
+ {
+ const Util::EnumData<E>* data = (*i)[_columns.data];
+ if(data->id == id) {
+ set_active(i);
+ break;
+ }
+ }
+ };
+
+ bool on_scroll_event(GdkEventScroll *event) override { return false; }
+
+ void set_active_by_key(const Glib::ustring& key) {
+ setProgrammatically = true;
+ set_active_by_id( _converter.get_id_from_key(key) );
+ };
+
+ bool setProgrammatically;
+
+private:
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns()
+ {
+ add(data);
+ add(label);
+ }
+
+ Gtk::TreeModelColumn<const Util::EnumData<E>*> data;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ };
+
+ Columns _columns;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ const Util::EnumDataConverter<E>& _converter;
+};
+
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class LabelledComboBoxEnum : public Labelled
+{
+public:
+ LabelledComboBoxEnum( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ const Util::EnumDataConverter<E>& c,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true,
+ bool sorted = true)
+ : Labelled(label, tooltip, new ComboBoxEnum<E>(c, SPAttr::INVALID, sorted), suffix, icon, mnemonic)
+ {
+ }
+
+ ComboBoxEnum<E>* getCombobox() {
+ return static_cast< ComboBoxEnum<E>* > (_widget);
+ }
+};
+
+}
+}
+}
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/combo-tool-item.cpp b/src/ui/widget/combo-tool-item.cpp
new file mode 100644
index 0000000..ffc7e75
--- /dev/null
+++ b/src/ui/widget/combo-tool-item.cpp
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+/** \file
+ A combobox that can be displayed in a toolbar.
+*/
+
+#include "combo-tool-item.h"
+#include "preferences.h"
+#include <iostream>
+#include <utility>
+#include <gtkmm/toolitem.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ComboToolItem*
+ComboToolItem::create(const Glib::ustring &group_label,
+ const Glib::ustring &tooltip,
+ const Glib::ustring &stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry)
+{
+ return new ComboToolItem(group_label, tooltip, stock_id, store, has_entry);
+}
+
+ComboToolItem::ComboToolItem(Glib::ustring group_label,
+ Glib::ustring tooltip,
+ Glib::ustring stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry) :
+ _active(-1),
+ _group_label(std::move( group_label )),
+ _tooltip(std::move( tooltip )),
+ _stock_id(std::move( stock_id )),
+ _store (std::move(store)),
+ _use_label (true),
+ _use_icon (false),
+ _use_pixbuf (true),
+ _icon_size ( Gtk::ICON_SIZE_LARGE_TOOLBAR ),
+ _combobox (nullptr),
+ _group_label_widget(nullptr),
+ _container(Gtk::manage(new Gtk::Box())),
+ _menuitem (nullptr)
+{
+ add(*_container);
+ _container->set_spacing(3);
+
+ // ": " is added to the group label later
+ if (!_group_label.empty()) {
+ // we don't expect trailing spaces
+ // g_assert(_group_label.raw()[_group_label.raw().size() - 1] != ' ');
+
+ // strip space (note: raw() indexing is much cheaper on Glib::ustring)
+ if (_group_label.raw()[_group_label.raw().size() - 1] == ' ') {
+ _group_label.resize(_group_label.size() - 1);
+ }
+ }
+ if (!_group_label.empty()) {
+ // we don't expect a trailing colon
+ // g_assert(_group_label.raw()[_group_label.raw().size() - 1] != ':');
+
+ // strip colon (note: raw() indexing is much cheaper on Glib::ustring)
+ if (_group_label.raw()[_group_label.raw().size() - 1] == ':') {
+ _group_label.resize(_group_label.size() - 1);
+ }
+ }
+
+
+ // Create combobox
+ _combobox = Gtk::manage (new Gtk::ComboBox(has_entry));
+ _combobox->set_model(_store);
+
+ populate_combobox();
+
+ _combobox->signal_changed().connect(
+ sigc::mem_fun(*this, &ComboToolItem::on_changed_combobox));
+ _container->pack_start(*_combobox);
+
+ show_all();
+}
+
+void
+ComboToolItem::focus_on_click( bool focus_on_click )
+{
+ _combobox->set_focus_on_click(focus_on_click);
+}
+
+
+void
+ComboToolItem::use_label(bool use_label)
+{
+ _use_label = use_label;
+ populate_combobox();
+}
+
+void
+ComboToolItem::use_icon(bool use_icon)
+{
+ _use_icon = use_icon;
+ populate_combobox();
+}
+
+void
+ComboToolItem::use_pixbuf(bool use_pixbuf)
+{
+ _use_pixbuf = use_pixbuf;
+ populate_combobox();
+}
+
+void
+ComboToolItem::use_group_label(bool use_group_label)
+{
+ if (use_group_label == (_group_label_widget != nullptr)) {
+ return;
+ }
+ if (use_group_label) {
+ _container->remove(*_combobox);
+ _group_label_widget = Gtk::manage(new Gtk::Label(_group_label + ": "));
+ _container->pack_start(*_group_label_widget);
+ _container->pack_start(*_combobox);
+ } else {
+ _container->remove(*_group_label_widget);
+ delete _group_label_widget;
+ _group_label_widget = nullptr;
+ }
+}
+
+void
+ComboToolItem::populate_combobox()
+{
+ _combobox->clear();
+
+ ComboToolItemColumns columns;
+ if (_use_icon) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ auto children = _store->children();
+ for (auto row : children) {
+ Glib::ustring icon = row[columns.col_icon];
+ gint pos = icon.find("-symbolic");
+ if (pos == std::string::npos) {
+ icon += "-symbolic";
+ }
+ row[columns.col_icon] = icon;
+ }
+ }
+ Gtk::CellRendererPixbuf *renderer = new Gtk::CellRendererPixbuf;
+ renderer->set_property ("stock_size", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ _combobox->pack_start (*Gtk::manage(renderer), false);
+ _combobox->add_attribute (*renderer, "icon_name", columns.col_icon );
+ } else if (_use_pixbuf) {
+ Gtk::CellRendererPixbuf *renderer = new Gtk::CellRendererPixbuf;
+ //renderer->set_property ("stock_size", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ _combobox->pack_start (*Gtk::manage(renderer), false);
+ _combobox->add_attribute (*renderer, "pixbuf", columns.col_pixbuf );
+ }
+
+ if (_use_label) {
+ _combobox->pack_start(columns.col_label);
+ }
+
+ std::vector<Gtk::CellRenderer*> cells = _combobox->get_cells();
+ for (auto & cell : cells) {
+ _combobox->add_attribute (*cell, "sensitive", columns.col_sensitive);
+ }
+
+ set_tooltip_text(_tooltip);
+ _combobox->set_tooltip_text(_tooltip);
+ _combobox->set_active (_active);
+}
+
+void
+ComboToolItem::set_active (gint active) {
+ if (_active != active) {
+
+ _active = active;
+
+ if (_combobox) {
+ _combobox->set_active (active);
+ }
+
+ if (active < _radiomenuitems.size()) {
+ _radiomenuitems[ active ]->set_active();
+ }
+ }
+}
+
+Glib::ustring
+ComboToolItem::get_active_text () {
+ Gtk::TreeModel::Row row = _store->children()[_active];
+ ComboToolItemColumns columns;
+ Glib::ustring label = row[columns.col_label];
+ return label;
+}
+
+bool
+ComboToolItem::on_create_menu_proxy()
+{
+ if (_menuitem == nullptr) {
+
+ _menuitem = Gtk::manage (new Gtk::MenuItem(_group_label));
+ Gtk::Menu *menu = Gtk::manage (new Gtk::Menu);
+
+ Gtk::RadioButton::Group group;
+ int index = 0;
+ auto children = _store->children();
+ for (auto row : children) {
+ ComboToolItemColumns columns;
+ Glib::ustring label = row[columns.col_label ];
+ Glib::ustring icon = row[columns.col_icon ];
+ Glib::ustring tooltip = row[columns.col_tooltip ];
+ bool sensitive = row[columns.col_sensitive ];
+
+ Gtk::RadioMenuItem* button = Gtk::manage(new Gtk::RadioMenuItem(group));
+ button->set_label (label);
+ button->set_tooltip_text( tooltip );
+ button->set_sensitive( sensitive );
+
+ button->signal_toggled().connect( sigc::bind<0>(
+ sigc::mem_fun(*this, &ComboToolItem::on_toggled_radiomenu), index++)
+ );
+
+ menu->add (*button);
+
+ _radiomenuitems.push_back( button );
+ }
+
+ if ( _active < _radiomenuitems.size()) {
+ _radiomenuitems[ _active ]->set_active();
+ }
+
+ _menuitem->set_submenu (*menu);
+ _menuitem->show_all();
+ }
+
+ set_proxy_menu_item(_group_label, *_menuitem);
+ return true;
+}
+
+void
+ComboToolItem::on_changed_combobox() {
+
+ int row = _combobox->get_active_row_number();
+ set_active( row );
+ _changed.emit (_active);
+ _changed_after.emit (_active);
+}
+
+void
+ComboToolItem::on_toggled_radiomenu(int n) {
+
+ // toggled emitted twice, first for button toggled off, second for button toggled on.
+ // We want to react only to the button turned on.
+ if ( n < _radiomenuitems.size() &&_radiomenuitems[ n ]->get_active()) {
+ set_active ( n );
+ _changed.emit (_active);
+ _changed_after.emit (_active);
+ }
+}
+
+}
+}
+}
+/*
+ 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/ui/widget/combo-tool-item.h b/src/ui/widget/combo-tool-item.h
new file mode 100644
index 0000000..1fc8b00
--- /dev/null
+++ b/src/ui/widget/combo-tool-item.h
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_COMBO_TOOL_ITEM
+#define SEEN_COMBO_TOOL_ITEM
+
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/**
+ A combobox that can be displayed in a toolbar
+*/
+
+#include <gtkmm/toolitem.h>
+#include <gtkmm/liststore.h>
+#include <sigc++/sigc++.h>
+#include <vector>
+
+namespace Gtk {
+class Box;
+class ComboBox;
+class Label;
+class MenuItem;
+class RadioMenuItem;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class ComboToolItemColumns : public Gtk::TreeModel::ColumnRecord {
+public:
+ ComboToolItemColumns() {
+ add (col_label);
+ add (col_value);
+ add (col_icon);
+ add (col_pixbuf);
+ add (col_data); // Used to store a pointer
+ add (col_tooltip);
+ add (col_sensitive);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> col_label;
+ Gtk::TreeModelColumn<Glib::ustring> col_value;
+ Gtk::TreeModelColumn<Glib::ustring> col_icon;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > col_pixbuf;
+ Gtk::TreeModelColumn<void *> col_data;
+ Gtk::TreeModelColumn<Glib::ustring> col_tooltip;
+ Gtk::TreeModelColumn<bool> col_sensitive;
+};
+
+
+class ComboToolItem : public Gtk::ToolItem {
+
+public:
+ static ComboToolItem* create(const Glib::ustring &label,
+ const Glib::ustring &tooltip,
+ const Glib::ustring &stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry = false);
+
+ /* Style of combobox */
+ void use_label( bool use_label );
+ void use_icon( bool use_icon );
+ void focus_on_click( bool focus_on_click );
+ void use_pixbuf( bool use_pixbuf );
+ void use_group_label( bool use_group_label ); // Applies to tool item only
+
+ gint get_active() { return _active; }
+ Glib::ustring get_active_text();
+ void set_active( gint active );
+ void set_icon_size( Gtk::BuiltinIconSize size ) { _icon_size = size; }
+
+ Glib::RefPtr<Gtk::ListStore> get_store() { return _store; }
+
+ sigc::signal<void, int> signal_changed() { return _changed; }
+ sigc::signal<void, int> signal_changed_after() { return _changed_after; }
+
+protected:
+ bool on_create_menu_proxy() override;
+ void populate_combobox();
+
+ /* Signals */
+ sigc::signal<void, int> _changed;
+ sigc::signal<void, int> _changed_after; // Needed for unit tracker which eats _changed.
+
+private:
+
+ Glib::ustring _group_label;
+ Glib::ustring _tooltip;
+ Glib::ustring _stock_id;
+ Glib::RefPtr<Gtk::ListStore> _store;
+
+ gint _active; /* Active menu item/button */
+
+ /* Style */
+ bool _use_label;
+ bool _use_icon; // Applies to menu item only
+ bool _use_pixbuf;
+ Gtk::BuiltinIconSize _icon_size;
+
+ /* Combobox in tool */
+ Gtk::ComboBox* _combobox;
+ Gtk::Label* _group_label_widget;
+ Gtk::Box* _container;
+
+ Gtk::MenuItem* _menuitem;
+ std::vector<Gtk::RadioMenuItem*> _radiomenuitems;
+
+ /* Internal Callbacks */
+ void on_changed_combobox();
+ void on_toggled_radiomenu(int n);
+
+ ComboToolItem(Glib::ustring group_label,
+ Glib::ustring tooltip,
+ Glib::ustring stock_id,
+ Glib::RefPtr<Gtk::ListStore> store,
+ bool has_entry = false);
+};
+}
+}
+}
+#endif /* SEEN_COMBO_TOOL_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/ui/widget/dash-selector.cpp b/src/ui/widget/dash-selector.cpp
new file mode 100644
index 0000000..c3f40ab
--- /dev/null
+++ b/src/ui/widget/dash-selector.cpp
@@ -0,0 +1,259 @@
+// 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 "dash-selector.h"
+
+#include <cstring>
+#include <glibmm/i18n.h>
+#include <2geom/coord.h>
+#include <numeric>
+
+#include "preferences.h"
+#include "display/cairo-utils.h"
+#include "style.h"
+
+#include "ui/dialog-events.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+gchar const *const DashSelector::_prefs_path = "/palette/dashes";
+
+static std::vector<std::vector<double>> s_dashes;
+
+DashSelector::DashSelector()
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4),
+ _preview_width(100),
+ _preview_height(16),
+ _preview_lineheight(2)
+{
+ // TODO: find something more sensible here!!
+ init_dashes();
+
+ _dash_store = Gtk::ListStore::create(dash_columns);
+ _dash_combo.set_model(_dash_store);
+ _dash_combo.pack_start(_image_renderer);
+ _dash_combo.set_cell_data_func(_image_renderer, sigc::mem_fun(*this, &DashSelector::prepareImageRenderer));
+ _dash_combo.set_tooltip_text(_("Dash pattern"));
+ _dash_combo.show();
+ _dash_combo.signal_changed().connect( sigc::mem_fun(*this, &DashSelector::on_selection) );
+ // show dashes in two columns to eliminate or minimize scrolling
+ _dash_combo.set_wrap_width(2);
+
+ this->pack_start(_dash_combo, true, true, 0);
+
+ _offset = Gtk::Adjustment::create(0.0, 0.0, 1000.0, 0.1, 1.0, 0.0);
+ _offset->signal_value_changed().connect(sigc::mem_fun(*this, &DashSelector::offset_value_changed));
+ _sb = new Inkscape::UI::Widget::SpinButton(_offset, 0.1, 2);
+ _sb->set_tooltip_text(_("Pattern offset"));
+ sp_dialog_defocus_on_enter_cpp(_sb);
+ _sb->set_width_chars(4);
+ _sb->show();
+
+ this->pack_start(*_sb, false, false, 0);
+
+ for (std::size_t i = 0; i < s_dashes.size(); ++i) {
+ Gtk::TreeModel::Row row = *(_dash_store->append());
+ row[dash_columns.dash] = i;
+ }
+
+ _pattern = &s_dashes.front();
+}
+
+DashSelector::~DashSelector() {
+ // FIXME: for some reason this doesn't get called; does the call to manage() in
+ // sp_stroke_style_line_widget_new() not processed correctly?
+}
+
+void DashSelector::prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ) {
+ // dashes are rendered on the fly to adapt to current theme colors
+ std::size_t index = (*row)[dash_columns.dash];
+ Cairo::RefPtr<Cairo::Surface> surface;
+ if (index == 1) {
+ // add the custom one as a second option; it'll show up at the top of second column
+ surface = sp_text_to_pixbuf((char *)"Custom");
+ }
+ else if (index < s_dashes.size()) {
+ // add the dash to the combobox
+ surface = sp_dash_to_pixbuf(s_dashes[index]);
+ }
+ else {
+ surface = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1)));
+ g_warning("No surface in prepareImageRenderer.");
+ }
+ _image_renderer.property_surface() = surface;
+}
+
+static std::vector<double> map_values(const std::vector<SPILength>& values) {
+ std::vector<double> out;
+ out.reserve(values.size());
+ for (auto&& v : values) {
+ out.push_back(v.value);
+ }
+ return out;
+}
+
+void DashSelector::init_dashes() {
+ if (s_dashes.empty()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ std::vector<Glib::ustring> dash_prefs = prefs->getAllDirs(_prefs_path);
+
+ if (!dash_prefs.empty()) {
+ SPStyle style;
+ s_dashes.reserve(dash_prefs.size() + 1);
+
+ for (auto & dash_pref : dash_prefs) {
+ style.readFromPrefs( dash_pref );
+
+ if (!style.stroke_dasharray.values.empty()) {
+ s_dashes.emplace_back(map_values(style.stroke_dasharray.values));
+ } else {
+ s_dashes.emplace_back(std::vector<double>());
+ }
+ }
+ } else {
+ g_warning("Missing stock dash definitions. DashSelector::init_dashes.");
+ // This code may never execute - a new preferences.xml is created for a new user. Maybe if the user deletes dashes from preferences.xml?
+ s_dashes.emplace_back(std::vector<double>());
+ }
+
+ std::vector<double> custom {1, 2, 1, 4}; // 'custom' dashes second on the list, so they are at the top of the second column in a combo box
+ s_dashes.insert(s_dashes.begin() + 1, custom);
+ }
+}
+
+void DashSelector::set_dash(const std::vector<double>& dash, double offset) {
+ int pos = -1; // Allows custom patterns to remain unscathed by this.
+
+ double delta = std::accumulate(dash.begin(), dash.end(), 0.0) / (10000.0 * (dash.empty() ? 1 : dash.size()));
+
+ int index = 0;
+ for (auto&& pattern : s_dashes) {
+ if (dash.size() == pattern.size() &&
+ std::equal(dash.begin(), dash.end(), pattern.begin(),
+ [=](double a, double b) { return Geom::are_near(a, b, delta); })) {
+ pos = index;
+ break;
+ }
+ ++index;
+ }
+
+ if (pos >= 0) {
+ _pattern = &s_dashes.at(pos);
+ _dash_combo.set_active(pos);
+ _offset->set_value(offset);
+ }
+ else { // Hit a custom pattern in the SVG, write it into the combobox.
+ pos = 1; // the one slot for custom patterns
+ _pattern = &s_dashes[pos];
+ _pattern->assign(dash.begin(), dash.end());
+ _dash_combo.set_active(pos);
+ _offset->set_value(offset);
+ }
+}
+
+const std::vector<double>& DashSelector::get_dash(double* offset) const {
+ if (offset) *offset = _offset->get_value();
+ return *_pattern;
+}
+
+double DashSelector::get_offset() {
+ return _offset ? _offset->get_value() : 0.0;
+}
+
+/**
+ * Fill a pixbuf with the dash pattern using standard cairo drawing
+ */
+Cairo::RefPtr<Cairo::Surface> DashSelector::sp_dash_to_pixbuf(const std::vector<double>& pattern) {
+ auto device_scale = get_scale_factor();
+
+ auto height = _preview_height * device_scale;
+ auto width = _preview_width * device_scale;
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t *ct = cairo_create(s);
+
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+
+ cairo_set_line_width (ct, _preview_lineheight * device_scale);
+ cairo_scale (ct, _preview_lineheight * device_scale, 1);
+ cairo_move_to (ct, 0, height/2);
+ cairo_line_to (ct, width, height/2);
+ cairo_set_dash(ct, pattern.data(), pattern.size(), 0);
+ cairo_set_source_rgb(ct, fg.get_red(), fg.get_green(), fg.get_blue());
+ cairo_stroke (ct);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ cairo_surface_set_device_scale(s, device_scale, device_scale);
+ return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s));
+}
+
+/**
+ * Fill a pixbuf with a text label using standard cairo drawing
+ */
+Cairo::RefPtr<Cairo::Surface> DashSelector::sp_text_to_pixbuf(const char* text) {
+ auto device_scale = get_scale_factor();
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _preview_width * device_scale, _preview_height * device_scale);
+ cairo_t *ct = cairo_create(s);
+
+ cairo_select_font_face (ct, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
+ // todo: how to find default font face and size?
+ cairo_set_font_size (ct, 12 * device_scale);
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ cairo_set_source_rgb(ct, fg.get_red(), fg.get_green(), fg.get_blue());
+ cairo_move_to (ct, 16.0 * device_scale, 13.0 * device_scale);
+ cairo_show_text (ct, text);
+
+ cairo_destroy(ct);
+ cairo_surface_flush(s);
+
+ cairo_surface_set_device_scale(s, device_scale, device_scale);
+ return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s));
+}
+
+void DashSelector::on_selection()
+{
+ _pattern = &s_dashes.at(_dash_combo.get_active()->get_value(dash_columns.dash));
+ changed_signal.emit();
+}
+
+void DashSelector::offset_value_changed()
+{
+ Glib::ustring offset = _("Pattern offset");
+ offset += " (";
+ offset += Glib::ustring::format(_sb->get_value());
+ offset += ")";
+ _sb->set_tooltip_text(offset.c_str());
+ changed_signal.emit();
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/dash-selector.h b/src/ui/widget/dash-selector.h
new file mode 100644
index 0000000..f98bb50
--- /dev/null
+++ b/src/ui/widget/dash-selector.h
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SP_DASH_SELECTOR_NEW_H
+#define SEEN_SP_DASH_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 "ui/widget/spinbutton.h"
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+
+#include <sigc++/signal.h>
+
+#include "scrollprotected.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Class that wraps a combobox and spinbutton for selecting dash patterns.
+ */
+class DashSelector : public Gtk::Box {
+public:
+ DashSelector();
+ ~DashSelector() override;
+
+ /**
+ * Get and set methods for dashes
+ */
+ void set_dash(const std::vector<double>& dash, double offset);
+
+ const std::vector<double>& get_dash(double* offset) const;
+
+ sigc::signal<void> changed_signal;
+
+ double get_offset();
+
+private:
+
+ /**
+ * Initialize dashes list from preferences
+ */
+ static void init_dashes();
+
+ /**
+ * Fill a pixbuf with the dash pattern using standard cairo drawing
+ */
+ Cairo::RefPtr<Cairo::Surface> sp_dash_to_pixbuf(const std::vector<double>& pattern);
+
+ /**
+ * Fill a pixbuf with text standard cairo drawing
+ */
+ Cairo::RefPtr<Cairo::Surface> sp_text_to_pixbuf(const char* text);
+
+ /**
+ * Callback for combobox image renderer
+ */
+ void prepareImageRenderer( Gtk::TreeModel::const_iterator const &row );
+
+ /**
+ * Callback for offset adjustment changing
+ */
+ void offset_value_changed();
+
+ /**
+ * Callback for combobox selection changing
+ */
+ void on_selection();
+
+ /**
+ * Combobox columns
+ */
+ class DashColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<std::size_t> dash;
+ DashColumns() {
+ add(dash);
+ }
+ };
+ DashColumns dash_columns;
+ Glib::RefPtr<Gtk::ListStore> _dash_store;
+ ScrollProtected<Gtk::ComboBox> _dash_combo;
+ Gtk::CellRendererPixbuf _image_renderer;
+ Glib::RefPtr<Gtk::Adjustment> _offset;
+ Inkscape::UI::Widget::SpinButton *_sb;
+ static gchar const *const _prefs_path;
+ int _preview_width;
+ int _preview_height;
+ int _preview_lineheight;
+ std::vector<double>* _pattern = nullptr;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_SP_DASH_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/ui/widget/entity-entry.cpp b/src/ui/widget/entity-entry.cpp
new file mode 100644
index 0000000..2e5c40b
--- /dev/null
+++ b/src/ui/widget/entity-entry.cpp
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2000 - 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "entity-entry.h"
+
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/entry.h>
+
+#include "document-undo.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "rdf.h"
+
+#include "object/sp-root.h"
+
+#include "ui/widget/registry.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+//===================================================
+
+//---------------------------------------------------
+
+EntityEntry*
+EntityEntry::create (rdf_work_entity_t* ent, Registry& wr)
+{
+ g_assert (ent);
+ EntityEntry* obj = nullptr;
+ switch (ent->format)
+ {
+ case RDF_FORMAT_LINE:
+ obj = new EntityLineEntry (ent, wr);
+ break;
+ case RDF_FORMAT_MULTILINE:
+ obj = new EntityMultiLineEntry (ent, wr);
+ break;
+ default:
+ g_warning ("An unknown RDF format was requested.");
+ }
+
+ g_assert (obj);
+ obj->_label.show();
+ return obj;
+}
+
+EntityEntry::EntityEntry (rdf_work_entity_t* ent, Registry& wr)
+ : _label(Glib::ustring(_(ent->title)), Gtk::ALIGN_END),
+ _packable(nullptr),
+ _entity(ent), _wr(&wr)
+{
+}
+
+EntityEntry::~EntityEntry()
+{
+ _changed_connection.disconnect();
+}
+
+void EntityEntry::save_to_preferences(SPDocument *doc)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ const gchar *text = rdf_get_work_entity (doc, _entity);
+ prefs->setString(PREFS_METADATA + Glib::ustring(_entity->name), Glib::ustring(text ? text : ""));
+}
+
+EntityLineEntry::EntityLineEntry (rdf_work_entity_t* ent, Registry& wr)
+: EntityEntry (ent, wr)
+{
+ Gtk::Entry *e = new Gtk::Entry;
+ e->set_tooltip_text (_(ent->tip));
+ _packable = e;
+ _changed_connection = e->signal_changed().connect (sigc::mem_fun (*this, &EntityLineEntry::on_changed));
+}
+
+EntityLineEntry::~EntityLineEntry()
+{
+ delete static_cast<Gtk::Entry*>(_packable);
+}
+
+void EntityLineEntry::update(SPDocument *doc)
+{
+ const char *text = rdf_get_work_entity (doc, _entity);
+ // If RDF title is not set, get the document's <title> and set the RDF:
+ if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) {
+ text = doc->getRoot()->title();
+ rdf_set_work_entity(doc, _entity, text);
+ }
+ static_cast<Gtk::Entry*>(_packable)->set_text (text ? text : "");
+}
+
+
+void EntityLineEntry::load_from_preferences()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name));
+ if (text.length() > 0) {
+ static_cast<Gtk::Entry*>(_packable)->set_text (text.c_str());
+ }
+}
+
+void
+EntityLineEntry::on_changed()
+{
+ if (_wr->isUpdating() || !_wr->desktop())
+ return;
+
+ _wr->setUpdating (true);
+ SPDocument *doc = _wr->desktop()->getDocument();
+ Glib::ustring text = static_cast<Gtk::Entry*>(_packable)->get_text();
+ if (rdf_set_work_entity (doc, _entity, text.c_str())) {
+ if (doc->isSensitive()) {
+ DocumentUndo::done(doc, "Document metadata updated", "");
+ }
+ }
+ _wr->setUpdating (false);
+}
+
+EntityMultiLineEntry::EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr)
+: EntityEntry (ent, wr)
+{
+ Gtk::ScrolledWindow *s = new Gtk::ScrolledWindow;
+ s->set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ s->set_shadow_type (Gtk::SHADOW_IN);
+ _packable = s;
+ _v.set_size_request (-1, 35);
+ _v.set_wrap_mode (Gtk::WRAP_WORD);
+ _v.set_accepts_tab (false);
+ s->add (_v);
+ _v.set_tooltip_text (_(ent->tip));
+ _changed_connection = _v.get_buffer()->signal_changed().connect (sigc::mem_fun (*this, &EntityMultiLineEntry::on_changed));
+}
+
+EntityMultiLineEntry::~EntityMultiLineEntry()
+{
+ delete static_cast<Gtk::ScrolledWindow*>(_packable);
+}
+
+void EntityMultiLineEntry::update(SPDocument *doc)
+{
+ const char *text = rdf_get_work_entity (doc, _entity);
+ // If RDF title is not set, get the document's <title> and set the RDF:
+ if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) {
+ text = doc->getRoot()->title();
+ rdf_set_work_entity(doc, _entity, text);
+ }
+ Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable);
+ Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child());
+ tv->get_buffer()->set_text (text ? text : "");
+}
+
+
+void EntityMultiLineEntry::load_from_preferences()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name));
+ if (text.length() > 0) {
+ Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable);
+ Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child());
+ tv->get_buffer()->set_text (text.c_str());
+ }
+}
+
+
+void
+EntityMultiLineEntry::on_changed()
+{
+ if (_wr->isUpdating() || !_wr->desktop())
+ return;
+
+ _wr->setUpdating (true);
+ SPDocument *doc = _wr->desktop()->getDocument();
+ Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable);
+ Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child());
+ Glib::ustring text = tv->get_buffer()->get_text();
+ if (rdf_set_work_entity (doc, _entity, text.c_str())) {
+ DocumentUndo::done(doc, "Document metadata updated", "");
+ }
+ _wr->setUpdating (false);
+}
+
+} // namespace Dialog
+} // namespace UI
+} // 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/ui/widget/entity-entry.h b/src/ui/widget/entity-entry.h
new file mode 100644
index 0000000..3168e4c
--- /dev/null
+++ b/src/ui/widget/entity-entry.h
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H
+#define INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H
+
+#include <gtkmm/textview.h>
+
+struct rdf_work_entity_t;
+class SPDocument;
+
+namespace Gtk {
+class TextBuffer;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry;
+
+class EntityEntry {
+public:
+ static EntityEntry* create (rdf_work_entity_t* ent, Registry& wr);
+ virtual ~EntityEntry() = 0;
+ virtual void update (SPDocument *doc) = 0;
+ virtual void on_changed() = 0;
+ virtual void load_from_preferences() = 0;
+ void save_to_preferences(SPDocument *doc);
+ Gtk::Label _label;
+ Gtk::Widget *_packable;
+
+protected:
+ EntityEntry (rdf_work_entity_t* ent, Registry& wr);
+ sigc::connection _changed_connection;
+ rdf_work_entity_t *_entity;
+ Registry *_wr;
+};
+
+class EntityLineEntry : public EntityEntry {
+public:
+ EntityLineEntry (rdf_work_entity_t* ent, Registry& wr);
+ ~EntityLineEntry() override;
+ void update (SPDocument *doc) override;
+ void load_from_preferences() override;
+
+protected:
+ void on_changed() override;
+};
+
+class EntityMultiLineEntry : public EntityEntry {
+public:
+ EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr);
+ ~EntityMultiLineEntry() override;
+ void update (SPDocument *doc) override;
+ void load_from_preferences() override;
+
+protected:
+ void on_changed() override;
+ Gtk::TextView _v;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_ENTITY_ENTRY__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/ui/widget/entry.cpp b/src/ui/widget/entry.cpp
new file mode 100644
index 0000000..e9a63c5
--- /dev/null
+++ b/src/ui/widget/entry.cpp
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "entry.h"
+
+#include <gtkmm/entry.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Entry::Entry( Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic)
+{
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
diff --git a/src/ui/widget/entry.h b/src/ui/widget/entry.h
new file mode 100644
index 0000000..3674d51
--- /dev/null
+++ b/src/ui/widget/entry.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ENTRY__H
+#define INKSCAPE_UI_WIDGET_ENTRY__H
+
+#include "labelled.h"
+
+namespace Gtk {
+class Entry;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Helperclass for Gtk::Entry widgets.
+ */
+class Entry : public Labelled
+{
+public:
+ Entry( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ // TO DO: add methods to access Gtk::Entry widget
+
+ Gtk::Entry* getEntry() {return (Gtk::Entry*)(_widget);};
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_ENTRY__H
diff --git a/src/ui/widget/export-lists.cpp b/src/ui/widget/export-lists.cpp
new file mode 100644
index 0000000..ba1da1a
--- /dev/null
+++ b/src/ui/widget/export-lists.cpp
@@ -0,0 +1,287 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "export-lists.h"
+
+#include <glibmm/convert.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <gtkmm.h>
+#include <png.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "extension/db.h"
+#include "extension/output.h"
+#include "file.h"
+#include "helper/png-write.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "message-stack.h"
+#include "object/object-set.h"
+#include "object/sp-namedview.h"
+#include "object/sp-page.h"
+#include "object/sp-root.h"
+#include "page-manager.h"
+#include "preferences.h"
+#include "selection-chemistry.h"
+#include "ui/dialog-events.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-loader.h"
+#include "ui/interface.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/unit-menu.h"
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ExtensionList::ExtensionList()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _watch_pref = prefs->createObserver("/dialogs/export/show_all_extensions", [=]() { setup(); });
+}
+
+ExtensionList::ExtensionList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>(cobject, refGlade)
+{
+ // This duplication is silly, but needed because C++ can't
+ // both deligate the constructor, and construct for glade
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _watch_pref = prefs->createObserver("/dialogs/export/show_all_extensions", [=]() { setup(); });
+}
+
+void ExtensionList::setup()
+{
+ this->remove_all();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool export_all = prefs->getBool("/dialogs/export/show_all_extensions", false);
+
+ Inkscape::Extension::DB::OutputList extensions;
+ Inkscape::Extension::db.get_output_list(extensions);
+ for (auto omod : extensions) {
+ auto oid = Glib::ustring(omod->get_id());
+ if (!export_all && !omod->is_raster() && !omod->is_exported())
+ continue;
+ // Comboboxes don't have a disabled row property
+ if (omod->deactivated())
+ continue;
+ this->append(oid, omod->get_filetypename());
+ // Record extensions map for filename-to-combo selections
+ auto ext = omod->get_extension();
+ if (!ext_to_mod[ext]) {
+ // Some extensions have multiple of the same extension (for example PNG)
+ // we're going to pick the first in the found list to back-link to.
+ ext_to_mod[ext] = omod;
+ }
+ }
+ this->set_active_id(SP_MODULE_KEY_RASTER_PNG);
+}
+
+/**
+ * Returns the Output extension currently selected in this dropdown.
+ */
+Inkscape::Extension::Output *ExtensionList::getExtension()
+{
+ return dynamic_cast<Inkscape::Extension::Output *>(Inkscape::Extension::db.get(this->get_active_id().c_str()));
+}
+
+/**
+ * Returns the file extension (file ending) of the currently selected extension.
+ */
+Glib::ustring ExtensionList::getFileExtension()
+{
+ if (auto ext = getExtension()) {
+ return ext->get_extension();
+ }
+ return "";
+}
+
+/**
+ * Removes the file extension, *if* it's one of the extensions in the list.
+ */
+void ExtensionList::removeExtension(Glib::ustring &filename)
+{
+ auto ext = Inkscape::IO::get_file_extension(filename);
+ if (ext_to_mod[ext]) {
+ filename.erase(filename.size()-ext.size());
+ }
+}
+
+void ExtensionList::setExtensionFromFilename(Glib::ustring const &filename)
+{
+ auto ext = Inkscape::IO::get_file_extension(filename);
+ if (auto omod = ext_to_mod[ext]) {
+ this->set_active_id(omod->get_id());
+ }
+}
+
+void ExportList::setup()
+{
+ if (_initialised) {
+ return;
+ }
+ _initialised = true;
+ prefs = Inkscape::Preferences::get();
+ default_dpi = prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE);
+
+ Gtk::Button *add_button = Gtk::manage(new Gtk::Button());
+ Glib::ustring label = _("Add Export");
+ add_button->set_label(label);
+ this->attach(*add_button, 0, 0, 4, 1);
+
+ this->insert_row(0);
+
+ Gtk::Label *suffix_label = Gtk::manage(new Gtk::Label(_("Suffix")));
+ this->attach(*suffix_label, _suffix_col, 0, 1, 1);
+ suffix_label->show();
+
+ Gtk::Label *extension_label = Gtk::manage(new Gtk::Label(_("Format")));
+ this->attach(*extension_label, _extension_col, 0, 1, 1);
+ extension_label->show();
+
+ Gtk::Label *dpi_label = Gtk::manage(new Gtk::Label(_("DPI")));
+ this->attach(*dpi_label, _dpi_col, 0, 1, 1);
+ dpi_label->show();
+
+ append_row();
+
+ add_button->signal_clicked().connect(sigc::mem_fun(*this, &ExportList::append_row));
+ add_button->set_hexpand(true);
+ add_button->show();
+
+ this->set_row_spacing(5);
+ this->set_column_spacing(2);
+}
+
+void ExportList::removeExtension(Glib::ustring &filename)
+{
+ ExtensionList *extension_cb = dynamic_cast<ExtensionList *>(this->get_child_at(_extension_col, 1));
+ if (extension_cb) {
+ extension_cb->removeExtension(filename);
+ return;
+ }
+}
+
+void ExportList::append_row()
+{
+ int current_row = _num_rows + 1; // because we have label row at top
+ this->insert_row(current_row);
+
+ Gtk::Entry *suffix = Gtk::manage(new Gtk::Entry());
+ this->attach(*suffix, _suffix_col, current_row, 1, 1);
+ suffix->set_width_chars(2);
+ suffix->set_hexpand(true);
+ suffix->set_placeholder_text(_("Suffix"));
+ suffix->show();
+
+ ExtensionList *extension = Gtk::manage(new ExtensionList());
+ SpinButton *dpi_sb = Gtk::manage(new SpinButton());
+
+ extension->setup();
+ extension->show();
+ this->attach(*extension, _extension_col, current_row, 1, 1);
+
+ // Disable DPI when not using a raster image output
+ extension->signal_changed().connect([=]() {
+ if (auto ext = extension->getExtension()) {
+ dpi_sb->set_sensitive(ext->is_raster());
+ }
+ });
+
+ dpi_sb->set_digits(2);
+ dpi_sb->set_increments(0.1, 1.0);
+ dpi_sb->set_range(1.0, 100000.0);
+ dpi_sb->set_value(default_dpi);
+ dpi_sb->set_sensitive(true);
+ dpi_sb->set_width_chars(6);
+ dpi_sb->set_max_width_chars(6);
+ dpi_sb->show();
+ this->attach(*dpi_sb, _dpi_col, current_row, 1, 1);
+
+ Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("window-close", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ Gtk::Button *delete_btn = Gtk::manage(new Gtk::Button());
+ delete_btn->set_relief(Gtk::RELIEF_NONE);
+ delete_btn->add(*pIcon);
+ delete_btn->show_all();
+ delete_btn->set_no_show_all(true);
+ this->attach(*delete_btn, _delete_col, current_row, 1, 1);
+ delete_btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &ExportList::delete_row), delete_btn));
+
+ _num_rows++;
+}
+
+void ExportList::delete_row(Gtk::Widget *widget)
+{
+ if (widget == nullptr) {
+ return;
+ }
+ if (_num_rows <= 1) {
+ return;
+ }
+ int row = this->child_property_top_attach(*widget);
+ this->remove_row(row);
+ _num_rows--;
+ if (_num_rows <= 1) {
+ Gtk::Widget *d_button_0 = dynamic_cast<Gtk::Widget *>(this->get_child_at(_delete_col, 1));
+ if (d_button_0) {
+ d_button_0->hide();
+ }
+ }
+}
+
+Glib::ustring ExportList::get_suffix(int row)
+{
+ Glib::ustring suffix = "";
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(this->get_child_at(_suffix_col, row + 1));
+ if (entry == nullptr) {
+ return suffix;
+ }
+ suffix = entry->get_text();
+ return suffix;
+}
+Inkscape::Extension::Output *ExportList::getExtension(int row)
+{
+ ExtensionList *extension_cb = dynamic_cast<ExtensionList *>(this->get_child_at(_extension_col, row + 1));
+ return extension_cb ? extension_cb->getExtension() : nullptr;
+}
+double ExportList::get_dpi(int row)
+{
+ double dpi = default_dpi;
+ SpinButton *spin_sb = dynamic_cast<SpinButton *>(this->get_child_at(_dpi_col, row + 1));
+ if (spin_sb == nullptr) {
+ return dpi;
+ }
+ dpi = spin_sb->get_value();
+ return dpi;
+}
+
+
+
+} // namespace Dialog
+} // namespace UI
+} // 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/ui/widget/export-lists.h b/src/ui/widget/export-lists.h
new file mode 100644
index 0000000..5bea6b3
--- /dev/null
+++ b/src/ui/widget/export-lists.h
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SP_EXPORT_HELPER_H
+#define SP_EXPORT_HELPER_H
+
+#include "2geom/rect.h"
+#include "preferences.h"
+#include "ui/widget/scrollprotected.h"
+
+class SPDocument;
+class SPItem;
+class SPPage;
+
+namespace Inkscape {
+ namespace Util {
+ class Unit;
+ }
+ namespace Extension {
+ class Output;
+ }
+namespace UI {
+namespace Dialog {
+
+#define EXPORT_COORD_PRECISION 3
+#define SP_EXPORT_MIN_SIZE 1.0
+#define DPI_BASE Inkscape::Util::Quantity::convert(1, "in", "px")
+
+// Class for storing and manipulating extensions
+class ExtensionList : public Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>
+{
+public:
+ ExtensionList();
+ ExtensionList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade);
+ ~ExtensionList() override {};
+
+public:
+ void setup();
+ Glib::ustring getFileExtension();
+ void setExtensionFromFilename(Glib::ustring const &filename);
+ void removeExtension(Glib::ustring &filename);
+ void createList();
+ Inkscape::Extension::Output *getExtension();
+
+private:
+ PrefObserver _watch_pref;
+ std::map<std::string, Inkscape::Extension::Output *> ext_to_mod;
+};
+
+class ExportList : public Gtk::Grid
+{
+public:
+ ExportList(){};
+ ExportList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Gtk::Grid(cobject){};
+ ~ExportList() override = default;
+
+public:
+ void setup();
+ void append_row();
+ void delete_row(Gtk::Widget *widget);
+ Glib::ustring get_suffix(int row);
+ Inkscape::Extension::Output *getExtension(int row);
+ void removeExtension(Glib::ustring &filename);
+ double get_dpi(int row);
+ int get_rows() { return _num_rows; }
+
+private:
+ typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton;
+ Inkscape::Preferences *prefs = nullptr;
+ double default_dpi = 96.00;
+
+private:
+ bool _initialised = false;
+ int _num_rows = 0;
+ int _suffix_col = 0;
+ int _extension_col = 1;
+ int _dpi_col = 2;
+ int _delete_col = 3;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+#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/ui/widget/export-preview.cpp b/src/ui/widget/export-preview.cpp
new file mode 100644
index 0000000..008caff
--- /dev/null
+++ b/src/ui/widget/export-preview.cpp
@@ -0,0 +1,235 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "export-preview.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/timer.h>
+#include <gtkmm.h>
+
+#include "display/cairo-utils.h"
+#include "inkscape.h"
+#include "object/sp-defs.h"
+#include "object/sp-item.h"
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+#include "util/preview.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+void ExportPreview::resetPixels()
+{
+ clear();
+ show();
+}
+
+ExportPreview::~ExportPreview()
+{
+ if (drawing) {
+ if (_document) {
+ _document->getRoot()->invoke_hide(visionkey);
+ }
+ delete drawing;
+ drawing = nullptr;
+ }
+ if (timer) {
+ timer->stop();
+ delete timer;
+ timer = nullptr;
+ }
+ if (renderTimer) {
+ renderTimer->stop();
+ delete renderTimer;
+ renderTimer = nullptr;
+ }
+ _item = nullptr;
+ _document = nullptr;
+}
+
+void ExportPreview::setItem(SPItem *item)
+{
+ _item = item;
+ _dbox = Geom::OptRect();
+}
+void ExportPreview::setDbox(double x0, double x1, double y0, double y1)
+{
+ if (!_document) {
+ return;
+ }
+ if ((x1 - x0 == 0) || (y1 - y0) == 0) {
+ return;
+ }
+ _item = nullptr;
+ _dbox = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1)) * _document->dt2doc();
+}
+
+void ExportPreview::setDocument(SPDocument *document)
+{
+ if (drawing) {
+ if (_document) {
+ _document->getRoot()->invoke_hide(visionkey);
+ }
+ delete drawing;
+ drawing = nullptr;
+ }
+ _document = document;
+ if (_document) {
+ drawing = new Inkscape::Drawing();
+ visionkey = SPItem::display_key_new(1);
+ DrawingItem *ai = _document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY);
+ if (ai) {
+ drawing->setRoot(ai);
+ }
+ }
+}
+
+void ExportPreview::refreshHide(const std::vector<SPItem *> &list)
+{
+ _hidden_excluded = std::vector<SPItem *>(list.begin(), list.end());
+ _hidden_requested = true;
+}
+
+void ExportPreview::performHide(const std::vector<SPItem *> *list)
+{
+ if (_document) {
+ if (isLastHide) {
+ if (drawing) {
+ if (_document) {
+ _document->getRoot()->invoke_hide(visionkey);
+ }
+ delete drawing;
+ drawing = nullptr;
+ }
+ drawing = new Inkscape::Drawing();
+ visionkey = SPItem::display_key_new(1);
+ DrawingItem *ai = _document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY);
+ if (ai) {
+ drawing->setRoot(ai);
+ }
+ isLastHide = false;
+ }
+ if (list && !list->empty()) {
+ hide_other_items_recursively(_document->getRoot(), *list);
+ isLastHide = true;
+ }
+ }
+}
+
+void ExportPreview::hide_other_items_recursively(SPObject *o, const std::vector<SPItem *> &list)
+{
+ if (SP_IS_ITEM(o) && !SP_IS_DEFS(o) && !SP_IS_ROOT(o) && !SP_IS_GROUP(o) &&
+ list.end() == find(list.begin(), list.end(), o)) {
+ SP_ITEM(o)->invoke_hide(visionkey);
+ }
+
+ // recurse
+ if (list.end() == find(list.begin(), list.end(), o)) {
+ for (auto &child : o->children) {
+ hide_other_items_recursively(&child, list);
+ }
+ }
+}
+
+void ExportPreview::queueRefresh()
+{
+ if (drawing == nullptr) {
+ return;
+ }
+ if (!pending) {
+ pending = true;
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ Glib::signal_idle().connect(sigc::mem_fun(this, &ExportPreview::refreshCB), Glib::PRIORITY_DEFAULT_IDLE);
+ }
+}
+
+bool ExportPreview::refreshCB()
+{
+ bool callAgain = true;
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ if (timer->elapsed() > minDelay) {
+ callAgain = false;
+ refreshPreview();
+ pending = false;
+ }
+ return callAgain;
+}
+
+void ExportPreview::refreshPreview()
+{
+ auto document = _document;
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ if (timer->elapsed() < minDelay) {
+ // Do not refresh too quickly
+ queueRefresh();
+ } else if (document) {
+ renderPreview();
+ timer->reset();
+ }
+}
+
+/*
+This is main function which finally render preview. Call this after setting document, item and dbox.
+If dbox is given it will use it.
+if item is given and not dbox then item is used
+If both are not given then simply we do nothing.
+*/
+void ExportPreview::renderPreview()
+{
+ if (!renderTimer) {
+ renderTimer = new Glib::Timer();
+ }
+ renderTimer->reset();
+ if (drawing == nullptr) {
+ return;
+ }
+
+ if (_hidden_requested) {
+ this->performHide(&_hidden_excluded);
+ _hidden_requested = false;
+ }
+ if (_document) {
+ GdkPixbuf *pb = nullptr;
+ if (_item) {
+ pb = Inkscape::UI::PREVIEW::render_preview(_document, *drawing, _item, size, size);
+ } else if (_dbox) {
+ pb = Inkscape::UI::PREVIEW::render_preview(_document, *drawing, nullptr, size, size, &_dbox);
+ }
+ if (pb) {
+ set(Glib::wrap(pb));
+ show();
+ }
+ }
+
+ renderTimer->stop();
+ minDelay = std::max(0.1, renderTimer->elapsed() * 3.0);
+}
+
+} // namespace Dialog
+} // namespace UI
+} // 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/ui/widget/export-preview.h b/src/ui/widget/export-preview.h
new file mode 100644
index 0000000..ad7f49c
--- /dev/null
+++ b/src/ui/widget/export-preview.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SP_EXPORT_PREVIEW_H
+#define SP_EXPORT_PREVIEW_H
+
+#include <gtkmm.h>
+
+#include "desktop.h"
+#include "document.h"
+
+class SPObject;
+class SPItem;
+
+namespace Glib {
+class Timer;
+}
+
+namespace Inkscape {
+class Drawing;
+namespace UI {
+namespace Dialog {
+
+class ExportPreview : public Gtk::Image
+{
+public:
+ ExportPreview() {};
+ ~ExportPreview() override;
+
+ ExportPreview(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade):Gtk::Image(cobject){};
+private:
+ int size = 128; // size of preview image
+ bool isLastHide = false;
+ SPDocument *_document = nullptr;
+ SPItem *_item = nullptr;
+ Geom::OptRect _dbox;
+
+ Drawing *drawing = nullptr;
+ unsigned int visionkey = 0;
+ Glib::Timer *timer = nullptr;
+ Glib::Timer *renderTimer = nullptr;
+ bool pending = false;
+ gdouble minDelay = 0.1;
+
+ std::vector<SPItem *> _hidden_excluded;
+ bool _hidden_requested = false;
+public:
+ void setDocument(SPDocument *document);
+ void refreshHide(const std::vector<SPItem *> &list = {});
+ void hide_other_items_recursively(SPObject *o, const std::vector<SPItem *> &list);
+ void setItem(SPItem *item);
+ void setDbox(double x0, double x1, double y0, double y1);
+ void queueRefresh();
+ void resetPixels();
+
+ void setSize(int newSize)
+ {
+ size = newSize;
+ resetPixels();
+ }
+private:
+ void refreshPreview();
+ void renderPreview();
+ bool refreshCB();
+ void performHide(const std::vector<SPItem *> *list);
+};
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+#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/ui/widget/fill-style.cpp b/src/ui/widget/fill-style.cpp
new file mode 100644
index 0000000..b5b9d9a
--- /dev/null
+++ b/src/ui/widget/fill-style.cpp
@@ -0,0 +1,714 @@
+// 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 <glibmm/i18n.h>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "fill-style.h"
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "selection.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 "object/sp-stop.h"
+#include "ui/dialog/dialog-base.h"
+#include "style.h"
+
+#include "ui/icon-names.h"
+
+// These can be deleted once we sort out the libart dependence.
+
+#define ART_WIND_RULE_NONZERO 0
+
+/* Fill */
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FillNStroke::FillNStroke(FillOrStroke k)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , kind(k)
+ , subselChangedConn()
+ , eventContextConn()
+{
+ // Add and connect up the paint selector widget:
+ _psel = Gtk::manage(new UI::Widget::PaintSelector(kind));
+ _psel->show();
+ add(*_psel);
+ _psel->signal_mode_changed().connect(sigc::mem_fun(*this, &FillNStroke::paintModeChangeCB));
+ _psel->signal_dragged().connect(sigc::mem_fun(*this, &FillNStroke::dragFromPaint));
+ _psel->signal_changed().connect(sigc::mem_fun(*this, &FillNStroke::paintChangedCB));
+ _psel->signal_stop_selected().connect([=](SPStop* stop) {
+ if (_desktop) { _desktop->emit_gradient_stop_selected(this, stop); }
+ });
+
+ if (kind == FILL) {
+ _psel->signal_fillrule_changed().connect(sigc::mem_fun(*this, &FillNStroke::setFillrule));
+ }
+
+ performUpdate();
+}
+
+FillNStroke::~FillNStroke()
+{
+ if (_drag_id) {
+ g_source_remove(_drag_id);
+ _drag_id = 0;
+ }
+
+ _psel = nullptr;
+ subselChangedConn.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 (_desktop != desktop) {
+ if (_drag_id) {
+ g_source_remove(_drag_id);
+ _drag_id = 0;
+ }
+ if (_desktop) {
+ subselChangedConn.disconnect();
+ eventContextConn.disconnect();
+ stop_selected_connection.disconnect();
+ }
+ _desktop = desktop;
+ if (desktop && desktop->selection) {
+ // 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)));
+
+ stop_selected_connection = desktop->connect_gradient_stop_selected([=](void* sender, SPStop* stop){
+ if (sender != this) {
+ performUpdate();
+ }
+ });
+ }
+ 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;
+ }
+ auto *widg = get_parent()->get_parent()->get_parent()->get_parent();
+ auto dialogbase = dynamic_cast<Inkscape::UI::Dialog::DialogBase*>(widg);
+ if (dialogbase && !dialogbase->getShowing()) {
+ return;
+ }
+ if (_drag_id) {
+ // local change; do nothing, but reset the flag
+ g_source_remove(_drag_id);
+ _drag_id = 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
+ const int property = kind == FILL ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE;
+ int result = sp_desktop_query_style(_desktop, &query, property);
+ SPIPaint& paint = *query.getFillOrStroke(kind == FILL);
+ auto stop = dynamic_cast<SPStop*>(paint.getTag());
+ if (stop) {
+ // there's a stop selected, which is part of subselection, now query selection only to find selected gradient
+ if (_desktop->selection != nullptr) {
+ std::vector<SPItem*> vec(_desktop->selection->items().begin(), _desktop->selection->items().end());
+ result = sp_desktop_query_style_from_list(vec, &query, property);
+ }
+ }
+ 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(UI::Widget::PaintSelector::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: {
+ auto pselmode = UI::Widget::PaintSelector::getModeForStyle(query, kind);
+ _psel->setMode(pselmode);
+
+ if (kind == FILL) {
+ _psel->setFillrule(query.fill_rule.computed == ART_WIND_RULE_NONZERO
+ ? UI::Widget::PaintSelector::FILLRULE_NONZERO
+ : UI::Widget::PaintSelector::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();
+ SPLinearGradient *lg = SP_LINEARGRADIENT(server);
+ _psel->setGradientLinear(vector, lg, stop);
+
+ _psel->setGradientProperties(lg->getUnits(), lg->getSpread());
+ } else if (SP_IS_RADIALGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ SPRadialGradient *rg = SP_RADIALGRADIENT(server);
+ _psel->setGradientRadial(vector, rg, stop);
+
+ _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));
+ _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(UI::Widget::PaintSelector::MODE_MULTIPLE);
+ break;
+ }
+ }
+
+ _update = false;
+}
+
+/**
+ * When the mode is changed, invoke a regular changed handler.
+ */
+void FillNStroke::paintModeChangeCB(UI::Widget::PaintSelector::Mode /*mode*/, bool switch_style)
+{
+#ifdef SP_FS_VERBOSE
+ g_message("paintModeChangeCB()");
+#endif
+ if (!_update) {
+ updateFromPaint(switch_style);
+ }
+}
+
+void FillNStroke::setFillrule(UI::Widget::PaintSelector::FillRule mode)
+{
+ if (!_update && _desktop) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-rule",
+ (mode == UI::Widget::PaintSelector::FILLRULE_EVENODD) ? "evenodd" : "nonzero");
+
+ sp_desktop_set_style(_desktop, css);
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(_desktop->doc(), _("Change fill rule"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+}
+
+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;
+
+gboolean FillNStroke::dragDelayCB(gpointer data)
+{
+ gboolean keepGoing = TRUE;
+ if (data) {
+ FillNStroke *self = reinterpret_cast<FillNStroke *>(data);
+ if (!self->_update) {
+ if (self->_drag_id) {
+ g_source_remove(self->_drag_id);
+ self->_drag_id = 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 (!_drag_id && _last_drag && when && ((when - _last_drag) < 32)) {
+ // local change, do not update from selection
+ _drag_id = g_timeout_add_full(G_PRIORITY_DEFAULT, 33, dragDelayCB, this, nullptr);
+ }
+
+ if (_drag_id) {
+ // 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;
+ }
+ _last_drag = when;
+
+ _update = true;
+
+ switch (_psel->get_mode()) {
+ case UI::Widget::PaintSelector::MODE_SOLID_COLOR: {
+ // local change, do not update from selection
+ _drag_id = 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,
+ (kind == FILL) ? _("Set fill color") : _("Set stroke color"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+ break;
+ }
+
+ default:
+ g_warning("file %s: line %d: Paint %d should not emit 'dragged'", __FILE__, __LINE__, _psel->get_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()
+{
+#ifdef SP_FS_VERBOSE
+ g_message("paintChangedCB()");
+#endif
+ if (!_update) {
+ updateFromPaint();
+ }
+}
+
+void FillNStroke::updateFromPaint(bool switch_style)
+{
+ if (!_desktop) {
+ return;
+ }
+ _update = true;
+
+ auto document = _desktop->getDocument();
+ auto selection = _desktop->getSelection();
+
+ std::vector<SPItem *> const items(selection->items().begin(), selection->items().end());
+
+ switch (_psel->get_mode()) {
+ case UI::Widget::PaintSelector::MODE_EMPTY:
+ // This should not happen.
+ g_warning("file %s: line %d: Paint %d should not emit 'changed'", __FILE__, __LINE__, _psel->get_mode());
+ break;
+ case UI::Widget::PaintSelector::MODE_MULTIPLE:
+ // This happens when you switch multiple objects with different gradients to flat color;
+ // nothing to do here.
+ break;
+
+ case UI::Widget::PaintSelector::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, true, true, switch_style);
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ DocumentUndo::done(document, (kind == FILL) ? _("Remove fill") : _("Remove stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ break;
+ }
+
+ case UI::Widget::PaintSelector::MODE_SOLID_COLOR: {
+ _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,
+ (kind == FILL) ? _("Set fill color") : _("Set stroke color"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ // 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 UI::Widget::PaintSelector::MODE_GRADIENT_LINEAR:
+ case UI::Widget::PaintSelector::MODE_GRADIENT_RADIAL:
+ case UI::Widget::PaintSelector::MODE_SWATCH:
+ if (!items.empty()) {
+ SPGradientType const gradient_type =
+ (_psel->get_mode() != UI::Widget::PaintSelector::MODE_GRADIENT_RADIAL ? SP_GRADIENT_TYPE_LINEAR
+ : SP_GRADIENT_TYPE_RADIAL);
+ bool createSwatch = (_psel->get_mode() == UI::Widget::PaintSelector::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");
+ }
+
+ auto 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) {
+ auto gr = sp_gradient_vector_for_object(
+ document, _desktop, 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, (kind == FILL) ? _("Set gradient on fill") : _("Set gradient on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+ break;
+
+#ifdef WITH_MESH
+ case UI::Widget::PaintSelector::MODE_GRADIENT_MESH:
+
+ if (!items.empty()) {
+ 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();
+
+ auto 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, (kind == FILL) ? _("Set mesh on fill") : _("Set mesh on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+ break;
+#endif
+
+ case UI::Widget::PaintSelector::MODE_PATTERN:
+
+ if (!items.empty()) {
+
+ auto 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, (kind == FILL) ? _("Set pattern on fill") : _("Set pattern on stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ } // end if
+
+ break;
+
+ case UI::Widget::PaintSelector::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, (kind == FILL) ? _("Unset fill") : _("Unset stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+ break;
+
+ default:
+ g_warning("file %s: line %d: Paint selector should not be in "
+ "mode %d",
+ __FILE__, __LINE__, _psel->get_mode());
+ break;
+ }
+
+ _update = false;
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/fill-style.h b/src/ui/widget/fill-style.h
new file mode 100644
index 0000000..165aec7
--- /dev/null
+++ b/src/ui/widget/fill-style.h
@@ -0,0 +1,85 @@
+// 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
+
+#include "ui/widget/paint-selector.h"
+
+#include <gtkmm/box.h>
+
+namespace Gtk {
+class Widget;
+}
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+class ToolBase;
+}
+
+namespace Widget {
+
+class FillNStroke : public Gtk::Box {
+ private:
+ FillOrStroke kind;
+ SPDesktop *_desktop = nullptr;
+ PaintSelector *_psel = nullptr;
+ guint32 _last_drag = 0;
+ guint _drag_id = 0;
+ bool _update = false;
+
+ sigc::connection subselChangedConn;
+ sigc::connection eventContextConn;
+ sigc::connection stop_selected_connection;
+
+ void paintModeChangeCB(UI::Widget::PaintSelector::Mode mode, bool switch_style);
+ void paintChangedCB();
+ static gboolean dragDelayCB(gpointer data);
+
+
+ void eventContextCB(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext);
+
+ void dragFromPaint();
+ void updateFromPaint(bool switch_style = false);
+
+ public:
+ FillNStroke(FillOrStroke k);
+ ~FillNStroke() override;
+
+ void selectionModifiedCB(guint flags);
+ void performUpdate();
+
+ void setFillrule(PaintSelector::FillRule mode);
+ void setDesktop(SPDesktop *desktop);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#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/ui/widget/filter-effect-chooser.cpp b/src/ui/widget/filter-effect-chooser.cpp
new file mode 100644
index 0000000..16c02a3
--- /dev/null
+++ b/src/ui/widget/filter-effect-chooser.cpp
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Filter effect selection selection widget
+ *
+ * Author:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2007, 2017 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "filter-effect-chooser.h"
+
+#include "document.h"
+
+namespace Inkscape {
+
+const EnumData<SPBlendMode> SPBlendModeData[SP_CSS_BLEND_ENDMODE] = {
+ { SP_CSS_BLEND_NORMAL, _("Normal"), "normal" },
+ { SP_CSS_BLEND_MULTIPLY, _("Multiply"), "multiply" },
+ { SP_CSS_BLEND_SCREEN, _("Screen"), "screen" },
+ { SP_CSS_BLEND_DARKEN, _("Darken"), "darken" },
+ { SP_CSS_BLEND_LIGHTEN, _("Lighten"), "lighten" },
+ // New in Compositing and Blending Level 1
+ { SP_CSS_BLEND_OVERLAY, _("Overlay"), "overlay" },
+ { SP_CSS_BLEND_COLORDODGE, _("Color Dodge"), "color-dodge" },
+ { SP_CSS_BLEND_COLORBURN, _("Color Burn"), "color-burn" },
+ { SP_CSS_BLEND_HARDLIGHT, _("Hard Light"), "hard-light" },
+ { SP_CSS_BLEND_SOFTLIGHT, _("Soft Light"), "soft-light" },
+ { SP_CSS_BLEND_DIFFERENCE, _("Difference"), "difference" },
+ { SP_CSS_BLEND_EXCLUSION, _("Exclusion"), "exclusion" },
+ { SP_CSS_BLEND_HUE, _("Hue"), "hue" },
+ { SP_CSS_BLEND_SATURATION, _("Saturation"), "saturation" },
+ { SP_CSS_BLEND_COLOR, _("Color"), "color" },
+ { SP_CSS_BLEND_LUMINOSITY, _("Luminosity"), "luminosity" }
+};
+const EnumDataConverter<SPBlendMode> SPBlendModeConverter(SPBlendModeData, SP_CSS_BLEND_ENDMODE);
+
+
+namespace UI {
+namespace Widget {
+
+SimpleFilterModifier::SimpleFilterModifier(int flags)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , _flags(flags)
+ , _lb_blend(_("Blend mode:"))
+ , _lb_isolation("Isolate") // Translate for 1.1
+ , _blend(SPBlendModeConverter, SPAttr::INVALID, false)
+ , _blur(_("Blur (%)"), 0, 0, 100, 1, 0.1, 1)
+ , _opacity(_("Opacity (%)"), 0, 0, 100, 1, 0.1, 1)
+ , _notify(true)
+ , _hb_blend(Gtk::ORIENTATION_HORIZONTAL)
+{
+ set_name("SimpleFilterModifier");
+
+ /* "More options" expander --------
+ _extras.set_visible();
+ _extras.set_label(_("More options"));
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL);
+ _extras.add(*box);
+ if (flags & (BLEND | BLUR)) {
+ add(_extras);
+ }
+ */
+
+ _flags = flags;
+
+ if (flags & BLEND) {
+ add(_hb_blend);
+ _lb_blend.set_use_underline();
+ _hb_blend.set_halign(Gtk::ALIGN_END);
+ _hb_blend.set_valign(Gtk::ALIGN_CENTER);
+ _hb_blend.set_margin_top(0);
+ _hb_blend.set_margin_bottom(1);
+ _hb_blend.set_margin_end(2);
+ _lb_blend.set_mnemonic_widget(_blend);
+ _hb_blend.pack_start(_lb_blend, false, false, 0);
+ _hb_blend.pack_start(_blend, false, false, 0);
+ /*
+ * For best fit inkscape-browsers with no GUI to isolation we need all groups,
+ * clones, and symbols with isolation == isolate to not show to the Inkscape
+ * user "strange" behaviour from the designer point of view.
+ * It's strange because it only happens when object doesn't have: clip, mask,
+ * filter, blending, or opacity.
+ * Anyway the feature is a no-gui feature and renders as expected.
+ */
+ /* if (flags & ISOLATION) {
+ _isolation.property_active() = false;
+ _hb_blend.pack_start(_isolation, false, false, 5);
+ _hb_blend.pack_start(_lb_isolation, false, false, 5);
+ _isolation.set_tooltip_text("Don't blend childrens with objects behind");
+ _lb_isolation.set_tooltip_text("Don't blend childrens with objects behind");
+ } */
+ }
+
+ if (flags & BLUR) {
+ add(_blur);
+ }
+
+ if (flags & OPACITY) {
+ add(_opacity);
+ }
+ show_all_children();
+
+ _blend.signal_changed().connect(signal_blend_changed());
+ _blur.signal_value_changed().connect(signal_blur_changed());
+ _opacity.signal_value_changed().connect(signal_opacity_changed());
+ _isolation.signal_toggled().connect(signal_isolation_changed());
+}
+
+sigc::signal<void> &SimpleFilterModifier::signal_isolation_changed()
+{
+ if (_notify) {
+ return _signal_isolation_changed;
+ }
+ _notify = true;
+ return _signal_null;
+}
+
+sigc::signal<void>& SimpleFilterModifier::signal_blend_changed()
+{
+ if (_notify) {
+ return _signal_blend_changed;
+ }
+ _notify = true;
+ return _signal_null;
+}
+
+sigc::signal<void>& SimpleFilterModifier::signal_blur_changed()
+{
+ // we dont use notifi to block use aberaje for multiple
+ return _signal_blur_changed;
+}
+
+sigc::signal<void>& SimpleFilterModifier::signal_opacity_changed()
+{
+ // we dont use notifi to block use averaje for multiple
+ return _signal_opacity_changed;
+}
+
+SPIsolation SimpleFilterModifier::get_isolation_mode()
+{
+ return _isolation.get_active() ? SP_CSS_ISOLATION_ISOLATE : SP_CSS_ISOLATION_AUTO;
+}
+
+void SimpleFilterModifier::set_isolation_mode(const SPIsolation val, bool notify)
+{
+ _notify = notify;
+ _isolation.set_active(val == SP_CSS_ISOLATION_ISOLATE);
+}
+
+SPBlendMode SimpleFilterModifier::get_blend_mode()
+{
+ const Util::EnumData<SPBlendMode> *d = _blend.get_active_data();
+ if (d) {
+ return _blend.get_active_data()->id;
+ } else {
+ return SP_CSS_BLEND_NORMAL;
+ }
+}
+
+void SimpleFilterModifier::set_blend_mode(const SPBlendMode val, bool notify)
+{
+ _notify = notify;
+ _blend.set_active(val);
+}
+
+double SimpleFilterModifier::get_blur_value() const
+{
+ return _blur.get_value();
+}
+
+void SimpleFilterModifier::set_blur_value(const double val)
+{
+ _blur.set_value(val);
+}
+
+double SimpleFilterModifier::get_opacity_value() const
+{
+ return _opacity.get_value();
+}
+
+void SimpleFilterModifier::set_opacity_value(const double val)
+{
+ _opacity.set_value(val);
+}
+
+}
+}
+}
+
+/*
+ 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/ui/widget/filter-effect-chooser.h b/src/ui/widget/filter-effect-chooser.h
new file mode 100644
index 0000000..f0b07b3
--- /dev/null
+++ b/src/ui/widget/filter-effect-chooser.h
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __FILTER_EFFECT_CHOOSER_H__
+#define __FILTER_EFFECT_CHOOSER_H__
+
+/*
+ * Filter effect selection selection widget
+ *
+ * Author:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2007, 2017 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/separator.h>
+#include <gtkmm/expander.h>
+
+#include "combo-enums.h"
+#include "spin-scale.h"
+#include "style-enums.h"
+
+using Inkscape::Util::EnumData;
+using Inkscape::Util::EnumDataConverter;
+
+namespace Inkscape {
+extern const Util::EnumDataConverter<SPBlendMode> SPBlendModeConverter;
+namespace UI {
+namespace Widget {
+
+/* Allows basic control over feBlend and feGaussianBlur effects as well as opacity.
+ * Common for Object, Layers, and Fill and Stroke dialogs.
+*/
+class SimpleFilterModifier : public Gtk::Box
+{
+public:
+ enum Flags { NONE = 0, BLUR = 1, OPACITY = 2, BLEND = 4, ISOLATION = 16 };
+
+ SimpleFilterModifier(int flags);
+
+ sigc::signal<void> &signal_blend_changed();
+ sigc::signal<void> &signal_blur_changed();
+ sigc::signal<void> &signal_opacity_changed();
+ sigc::signal<void> &signal_isolation_changed();
+
+ SPIsolation get_isolation_mode();
+ void set_isolation_mode(const SPIsolation, bool notify);
+
+ SPBlendMode get_blend_mode();
+ void set_blend_mode(const SPBlendMode, bool notify);
+
+ double get_blur_value() const;
+ void set_blur_value(const double);
+
+ double get_opacity_value() const;
+ void set_opacity_value(const double);
+
+private:
+ int _flags;
+ bool _notify;
+
+ Gtk::Expander _extras;
+ Gtk::Box _hb_blend;
+ Gtk::Label _lb_blend;
+ Gtk::Label _lb_isolation;
+ ComboBoxEnum<SPBlendMode> _blend;
+ SpinScale _blur;
+ SpinScale _opacity;
+ Gtk::CheckButton _isolation;
+
+ sigc::signal<void> _signal_null;
+ sigc::signal<void> _signal_blend_changed;
+ sigc::signal<void> _signal_blur_changed;
+ sigc::signal<void> _signal_opacity_changed;
+ sigc::signal<void> _signal_isolation_changed;
+};
+
+}
+}
+}
+
+#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/ui/widget/font-button.cpp b/src/ui/widget/font-button.cpp
new file mode 100644
index 0000000..e0a140a
--- /dev/null
+++ b/src/ui/widget/font-button.cpp
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "font-button.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/fontbutton.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontButton::FontButton(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::FontButton("Sans 10"), suffix, icon, mnemonic)
+{
+}
+
+Glib::ustring FontButton::getValue() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<Gtk::FontButton*>(_widget)->get_font_name();
+}
+
+
+void FontButton::setValue (Glib::ustring fontspec)
+{
+ g_assert(_widget != nullptr);
+ static_cast<Gtk::FontButton*>(_widget)->set_font_name(fontspec);
+}
+
+Glib::SignalProxy0<void> FontButton::signal_font_value_changed()
+{
+ g_assert(_widget != nullptr);
+ return static_cast<Gtk::FontButton*>(_widget)->signal_font_set();
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/font-button.h b/src/ui/widget/font-button.h
new file mode 100644
index 0000000..a53b7d6
--- /dev/null
+++ b/src/ui/widget/font-button.h
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *
+ * Copyright (C) 2007 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_BUTTON_H
+#define INKSCAPE_UI_WIDGET_FONT_BUTTON_H
+
+#include "labelled.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled font button for entering font values
+ */
+class FontButton : public Labelled
+{
+public:
+ /**
+ * Construct a FontButton Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ FontButton( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ Glib::ustring getValue() const;
+ void setValue (Glib::ustring fontspec);
+ /**
+ * Signal raised when the font button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_font_value_changed();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_RANDOM_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/ui/widget/font-selector-toolbar.cpp b/src/ui/widget/font-selector-toolbar.cpp
new file mode 100644
index 0000000..68c5e79
--- /dev/null
+++ b/src/ui/widget/font-selector-toolbar.cpp
@@ -0,0 +1,302 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+#include <gdkmm/display.h>
+
+#include "font-selector-toolbar.h"
+
+#include "libnrtype/font-lister.h"
+#include "libnrtype/font-instance.h"
+
+#include "ui/icon-names.h"
+
+// For updating from selection
+#include "inkscape.h"
+#include "desktop.h"
+#include "object/sp-text.h"
+
+// TEMP TEMP TEMP
+#include "ui/toolbar/text-toolbar.h"
+
+/* To do:
+ * Fix altx. The setToolboxFocusTo method now just searches for a named widget.
+ * We just need to do the following:
+ * * Set the name of the family_combo child widget
+ * * Change the setToolboxFocusTo() argument in tools/text-tool to point to that widget name
+ */
+
+void family_cell_data_func(const Gtk::TreeModel::const_iterator iter, Gtk::CellRendererText* cell ) {
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Glib::ustring markup = font_lister->get_font_family_markup(iter);
+ // std::cout << "Markup: " << markup << std::endl;
+
+ cell->set_property ("markup", markup);
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontSelectorToolbar::FontSelectorToolbar ()
+ : Gtk::Grid ()
+ , family_combo (true) // true => with text entry.
+ , style_combo (true)
+ , signal_block (false)
+{
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ // Font family
+ family_combo.set_model (font_lister->get_font_list());
+ family_combo.set_entry_text_column (0);
+ family_combo.set_name ("FontSelectorToolBar: Family");
+ family_combo.set_row_separator_func (&font_lister_separator_func);
+
+ family_combo.clear(); // Clears all CellRenderer mappings.
+ family_combo.set_cell_data_func (family_cell,
+ sigc::bind(sigc::ptr_fun(family_cell_data_func), &family_cell));
+ family_combo.pack_start (family_cell);
+
+
+ Gtk::Entry* entry = family_combo.get_entry();
+ entry->signal_icon_press().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_icon_pressed));
+ entry->signal_key_press_event().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_key_press_event), false); // false => connect first
+
+ Glib::RefPtr<Gtk::EntryCompletion> completion = Gtk::EntryCompletion::create();
+ completion->set_model (font_lister->get_font_list());
+ completion->set_text_column (0);
+ completion->set_popup_completion ();
+ completion->set_inline_completion (false);
+ completion->set_inline_selection ();
+ // completion->signal_match_selected().connect(sigc::mem_fun(*this, &FontSelectorToolbar::on_match_selected), false); // false => connect before default handler.
+ entry->set_completion (completion);
+
+ // Style
+ style_combo.set_model (font_lister->get_style_list());
+ style_combo.set_name ("FontSelectorToolbar: Style");
+
+ // Grid
+ set_name ("FontSelectorToolbar: Grid");
+ attach (family_combo, 0, 0, 1, 1);
+ attach (style_combo, 1, 0, 1, 1);
+
+ // Add signals
+ family_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_family_changed));
+ style_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_style_changed));
+
+ show_all_children();
+
+ // Initialize font family lists. (May already be done.) Should be done on document change.
+ font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument());
+
+ // When FontLister is changed, update family and style shown in GUI.
+ font_lister->connectUpdate(sigc::mem_fun(*this, &FontSelectorToolbar::update_font));
+}
+
+
+// Update GUI based on font-selector values.
+void
+FontSelectorToolbar::update_font ()
+{
+ if (signal_block) return;
+
+ signal_block = true;
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Gtk::TreeModel::Row row;
+
+ // Set font family.
+ try {
+ row = font_lister->get_row_for_font ();
+ family_combo.set_active (row);
+ } catch (...) {
+ std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for family: "
+ << font_lister->get_font_family() << std::endl;
+ }
+
+ // Set style.
+ try {
+ row = font_lister->get_row_for_style ();
+ style_combo.set_active (row);
+ } catch (...) {
+ std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for style: "
+ << font_lister->get_font_style() << std::endl;
+ }
+
+ // Check for missing fonts.
+ Glib::ustring missing_fonts = get_missing_fonts();
+
+ // Add an icon to end of entry.
+ Gtk::Entry* entry = family_combo.get_entry();
+ if (missing_fonts.empty()) {
+ // If no missing fonts, add icon for selecting all objects with this font-family.
+ entry->set_icon_from_icon_name (INKSCAPE_ICON("edit-select-all"), Gtk::ENTRY_ICON_SECONDARY);
+ entry->set_icon_tooltip_text (_("Select all text with this text family"), Gtk::ENTRY_ICON_SECONDARY);
+ } else {
+ // If missing fonts, add warning icon.
+ Glib::ustring warning = _("Font not found on system: ") + missing_fonts;
+ entry->set_icon_from_icon_name (INKSCAPE_ICON("dialog-warning"), Gtk::ENTRY_ICON_SECONDARY);
+ entry->set_icon_tooltip_text (warning, Gtk::ENTRY_ICON_SECONDARY);
+ }
+
+ signal_block = false;
+}
+
+// Get comma separated list of fonts in font-family that are not on system.
+// To do, move to font-lister.
+Glib::ustring
+FontSelectorToolbar::get_missing_fonts ()
+{
+ // Get font list in text entry which may be a font stack (with fallbacks).
+ Glib::ustring font_list = family_combo.get_entry_text();
+ Glib::ustring missing_font_list;
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", font_list);
+
+ for (auto token: tokens) {
+ bool found = false;
+ Gtk::TreeModel::Children children = font_lister->get_font_list()->children();
+ for (auto iter2: children) {
+ Gtk::TreeModel::Row row2 = *iter2;
+ Glib::ustring family2 = row2[font_lister->FontList.family];
+ bool onSystem2 = row2[font_lister->FontList.onSystem];
+ // CSS dictates that font family names are case insensitive.
+ // This should really implement full Unicode case unfolding.
+ if (onSystem2 && token.casefold().compare(family2.casefold()) == 0) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ missing_font_list += token;
+ missing_font_list += ", ";
+ }
+ }
+
+ // Remove extra comma and space from end.
+ if (missing_font_list.size() >= 2) {
+ missing_font_list.resize(missing_font_list.size() - 2);
+ }
+
+ return missing_font_list;
+}
+
+
+// Callbacks
+
+// Need to update style list
+void
+FontSelectorToolbar::on_family_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::ustring family = family_combo.get_entry_text();
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_font_family (family);
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelectorToolbar::on_style_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::ustring style = style_combo.get_entry_text();
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_font_style (style);
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelectorToolbar::on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event) {
+ std::cout << "FontSelectorToolbar::on_entry_icon_pressed" << std::endl;
+ std::cout << " .... Should select all items with same font-family. FIXME" << std::endl;
+ // Call equivalent of sp_text_toolbox_select_cb() in text-toolbar.cpp
+ // Should be action! (Maybe: select_all_fontfamily( Glib::ustring font_family );).
+ // Check how Find dialog works.
+}
+
+// bool
+// FontSelectorToolbar::on_match_selected (const Gtk::TreeModel::iterator& iter)
+// {
+// std::cout << "on_match_selected" << std::endl;
+// std::cout << " FIXME" << std::endl;
+// Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+// Glib::ustring family = (*iter)[font_lister->FontList.family];
+// std::cout << " family: " << family << std::endl;
+// return false; // Leave it to default handler to set entry text.
+// }
+
+// Return focus to canvas.
+bool
+FontSelectorToolbar::on_key_press_event (GdkEventKey* key_event)
+{
+ bool consumed = false;
+
+ unsigned int key = 0;
+ gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode,
+ (GdkModifierType)key_event->state,
+ 0, &key, nullptr, nullptr, nullptr );
+
+ switch ( key ) {
+
+ case GDK_KEY_Escape:
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ // Defocus
+ std::cerr << "FontSelectorToolbar::on_key_press_event: Defocus: FIXME" << std::endl;
+ consumed = true;
+ }
+ break;
+ }
+
+ return consumed; // Leave it to default handler if false.
+}
+
+void
+FontSelectorToolbar::changed_emit() {
+ signal_block = true;
+ changed_signal.emit ();
+ signal_block = false;
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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 :
diff --git a/src/ui/widget/font-selector-toolbar.h b/src/ui/widget/font-selector-toolbar.h
new file mode 100644
index 0000000..53cdcea
--- /dev/null
+++ b/src/ui/widget/font-selector-toolbar.h
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ *
+ * The routines here create and manage a font selector widget with two parts,
+ * one each for font-family and font-style.
+ *
+ * This is essentially a toolbar version of the 'FontSelector' widget. Someday
+ * this may be merged with it.
+ *
+ * The main functions are:
+ * Create the font-selector toolbar widget.
+ * Update the lists when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Update the on-screen text.
+ * Provide the currently selected values.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H
+#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H
+
+#include <gtkmm/grid.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/comboboxtext.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container of widgets for selecting font faces.
+ *
+ * It is used by Text tool toolbar. The FontSelectorToolbar class utilizes the
+ * FontLister class to obtain a list of font-families and their associated styles for fonts either
+ * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts
+ * are kept track of by their "fontspecs" which are the same as the strings that Pango generates.
+ *
+ * The main functions are:
+ * Create the font-selector widget.
+ * Update the child widgets when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Emit a signal when any change is made to a child widget.
+ */
+class FontSelectorToolbar : public Gtk::Grid
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontSelectorToolbar ();
+
+protected:
+
+ // Font family
+ Gtk::ComboBox family_combo;
+ Gtk::CellRendererText family_cell;
+
+ // Font style
+ Gtk::ComboBoxText style_combo;
+ Gtk::CellRendererText style_cell;
+
+private:
+
+ // Make a list of missing fonts for tooltip and for warning icon.
+ Glib::ustring get_missing_fonts ();
+
+ // Signal handlers
+ void on_family_changed();
+ void on_style_changed();
+ void on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event);
+ // bool on_match_selected (const Gtk::TreeModel::iterator& iter);
+ bool on_key_press_event (GdkEventKey* key_event) override;
+
+ // Signals
+ sigc::signal<void> changed_signal;
+ void changed_emit();
+ bool signal_block;
+
+public:
+
+ /**
+ * Update GUI based on font-selector values.
+ */
+ void update_font ();
+
+ /**
+ * Let others know that user has changed GUI settings.
+ */
+ sigc::connection connectChanged(sigc::slot<void> slot) {
+ return changed_signal.connect(slot);
+ }
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_TOOLBAR_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/ui/widget/font-selector.cpp b/src/ui/widget/font-selector.cpp
new file mode 100644
index 0000000..0617e87
--- /dev/null
+++ b/src/ui/widget/font-selector.cpp
@@ -0,0 +1,471 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+
+#include "font-selector.h"
+
+#include "libnrtype/font-lister.h"
+#include "libnrtype/font-instance.h"
+
+// For updating from selection
+#include "inkscape.h"
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontSelector::FontSelector (bool with_size, bool with_variations)
+ : Gtk::Grid ()
+ , family_frame (_("Font family"))
+ , style_frame (C_("Font selector", "Style"))
+ , size_label (_("Font size"))
+ , size_combobox (true) // With entry
+ , signal_block (false)
+ , font_size (18)
+{
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Glib::RefPtr<Gtk::TreeModel> model = font_lister->get_font_list();
+ int total = model->children().size();
+ int height = 30;
+ if (total > 1000) {
+ height = 30000/total;
+ g_warning("You have a huge number of font families (%d), "
+ "and Cairo is limiting the size of widgets you can draw.\n"
+ "Your preview cell height is capped to %d.",
+ total, height);
+ // hope we dont need a forced height because now pango line height
+ // not add data outside parent rendered expanding it so no naturall cells become over 30 height
+ }
+ family_cell.set_fixed_size(-1, height);
+ // Font family
+ family_treecolumn.pack_start (family_cell, false);
+ family_treecolumn.set_fixed_width (120); // limit minimal width to keep entire dialog narrow; column can still grow
+ family_treecolumn.add_attribute (family_cell, "text", 0);
+ family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func);
+
+ family_treeview.set_row_separator_func (&font_lister_separator_func);
+ family_treeview.set_model (model);
+ family_treeview.set_name ("FontSelector: Family");
+ family_treeview.set_headers_visible (false);
+ family_treeview.append_column (family_treecolumn);
+
+ family_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ family_scroll.add (family_treeview);
+
+ family_frame.set_hexpand (true);
+ family_frame.set_vexpand (true);
+ family_frame.add (family_scroll);
+
+ // Style
+ style_treecolumn.pack_start (style_cell, false);
+ style_treecolumn.add_attribute (style_cell, "text", 0);
+ style_treecolumn.set_cell_data_func (style_cell, sigc::mem_fun(*this, &FontSelector::style_cell_data_func));
+ style_treecolumn.set_title ("Face");
+ style_treecolumn.set_resizable (true);
+
+ style_treeview.set_model (font_lister->get_style_list());
+ style_treeview.set_name ("FontSelectorStyle");
+ style_treeview.append_column ("CSS", font_lister->FontStyleList.cssStyle);
+ style_treeview.append_column (style_treecolumn);
+
+ style_treeview.get_column(0)->set_resizable (true);
+
+ style_scroll.set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ style_scroll.add (style_treeview);
+
+ style_frame.set_hexpand (true);
+ style_frame.set_vexpand (true);
+ style_frame.add (style_scroll);
+
+ // Size
+ size_combobox.set_name ("FontSelectorSize");
+ if (auto entry = size_combobox.get_entry()) {
+ // limit min size of the entry box to 6 chars, so it doesn't inflate entire dialog!
+ entry->set_width_chars(6);
+ }
+ set_sizes();
+ size_combobox.set_active_text( "18" );
+
+ // Font Variations
+ font_variations.set_vexpand (true);
+ font_variations_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ font_variations_scroll.add (font_variations);
+
+ // Grid
+ set_name ("FontSelectorGrid");
+ set_row_spacing(4);
+ set_column_spacing(4);
+ // Add extra columns to the "family frame" to change space distribution
+ // by prioritizing font family over styles
+ const int extra = 4;
+ attach (family_frame, 0, 0, 1 + extra, 2);
+ attach (style_frame, 1 + extra, 0, 2, 1);
+ if (with_size) { // Glyph panel does not use size.
+ attach (size_label, 1 + extra, 1, 1, 1);
+ attach (size_combobox, 2 + extra, 1, 1, 1);
+ }
+ if (with_variations) { // Glyphs panel does not use variations.
+ attach (font_variations_scroll, 0, 2, 3 + extra, 1);
+ }
+
+ // Add signals
+ family_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_family_changed));
+ style_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_style_changed));
+ size_combobox.signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_size_changed));
+ font_variations.connectChanged(sigc::mem_fun(*this, &FontSelector::on_variations_changed));
+
+ show_all_children();
+
+ // Initialize font family lists. (May already be done.) Should be done on document change.
+ font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument());
+}
+
+void
+FontSelector::set_sizes ()
+{
+ size_combobox.remove_all();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+
+ int sizes[] = {
+ 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28,
+ 32, 36, 40, 48, 56, 64, 72, 144
+ };
+
+ // Array must be same length as SPCSSUnit in style-internal.h
+ // PX PT PC MM CM IN EM EX %
+ double ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16};
+
+ for (int i : sizes)
+ {
+ double size = i/ratios[unit];
+ size_combobox.append( Glib::ustring::format(size) );
+ }
+}
+
+void
+FontSelector::set_fontsize_tooltip()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", sp_style_get_css_unit_string(unit), ")");
+ size_combobox.set_tooltip_text (tooltip);
+}
+
+// Update GUI.
+// We keep a private copy of the style list as the font-family in widget is only temporary
+// until the "Apply" button is set so the style list can be different from that in
+// FontLister.
+void
+FontSelector::update_font ()
+{
+ signal_block = true;
+
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ Gtk::TreePath path;
+ Glib::ustring family = font_lister->get_font_family();
+ Glib::ustring style = font_lister->get_font_style();
+
+ // Set font family
+ try {
+ path = font_lister->get_row_for_font (family);
+ } catch (...) {
+ std::cerr << "FontSelector::update_font: Couldn't find row for font-family: "
+ << family << std::endl;
+ path.clear();
+ path.push_back(0);
+ }
+
+ Gtk::TreePath currentPath;
+ Gtk::TreeViewColumn *currentColumn;
+ family_treeview.get_cursor(currentPath, currentColumn);
+ if (currentPath.empty() || !font_lister->is_path_for_font(currentPath, family)) {
+ family_treeview.set_cursor (path);
+ family_treeview.scroll_to_row (path);
+ }
+
+ // Get font-lister style list for selected family
+ Gtk::TreeModel::Row row = *(family_treeview.get_model()->get_iter (path));
+ GList *styles;
+ row.get_value(1, styles);
+
+ // Copy font-lister style list to private list store, searching for match.
+ Gtk::TreeModel::iterator match;
+ FontLister::FontStyleListClass FontStyleList;
+ Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList);
+ for ( ; styles; styles = styles->next ) {
+ Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append();
+ (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName;
+ (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName;
+ if (style == ((StyleNames*)styles->data)->CssName) {
+ match = treeModelIter;
+ }
+ }
+
+ // Attach store to tree view and select row.
+ style_treeview.set_model (local_style_list_store);
+ if (match) {
+ style_treeview.get_selection()->select (match);
+ }
+
+ Glib::ustring fontspec = font_lister->get_fontspec();
+ update_variations(fontspec);
+
+ signal_block = false;
+}
+
+void
+FontSelector::update_size (double size)
+{
+ signal_block = true;
+
+ // Set font size
+ std::stringstream ss;
+ ss << size;
+ size_combobox.get_entry()->set_text( ss.str() );
+ font_size = size; // Store value
+ set_fontsize_tooltip();
+
+ signal_block = false;
+}
+
+
+// If use_variations is true (default), we get variation values from variations widget otherwise we
+// get values from CSS widget (we need to be able to keep the two widgets synchronized both ways).
+Glib::ustring
+FontSelector::get_fontspec(bool use_variations) {
+
+ // Build new fontspec from GUI settings
+ Glib::ustring family = "Sans"; // Default...family list may not have been constructed.
+ Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected();
+ if (iter) {
+ (*iter).get_value(0, family);
+ }
+
+ Glib::ustring style = "Normal";
+ iter = style_treeview.get_selection()->get_selected();
+ if (iter) {
+ (*iter).get_value(0, style);
+ }
+
+ if (family.empty()) {
+ std::cerr << "FontSelector::get_fontspec: empty family!" << std::endl;
+ }
+
+ if (style.empty()) {
+ std::cerr << "FontSelector::get_fontspec: empty style!" << std::endl;
+ }
+
+ Glib::ustring fontspec = family + ", ";
+
+ if (use_variations) {
+ // Clip any font_variation data in 'style' as we'll replace it.
+ auto pos = style.find('@');
+ if (pos != Glib::ustring::npos) {
+ style.erase (pos, style.length()-1);
+ }
+
+ Glib::ustring variations = font_variations.get_pango_string();
+
+ if (variations.empty()) {
+ fontspec += style;
+ } else {
+ fontspec += variations;
+ }
+ } else {
+ fontspec += style;
+ }
+
+ return fontspec;
+}
+
+void
+FontSelector::style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ Glib::ustring family = "Sans"; // Default...family list may not have been constructed.
+ Gtk::TreeModel::iterator iter_family = family_treeview.get_selection()->get_selected();
+ if (iter_family) {
+ (*iter_family).get_value(0, family);
+ }
+
+ Glib::ustring style = "Normal";
+ (*iter).get_value(1, style);
+
+ Glib::ustring style_escaped = Glib::Markup::escape_text( style );
+ Glib::ustring font_desc = Glib::Markup::escape_text( family + ", " + style );
+ Glib::ustring markup;
+
+ markup = "<span font='" + font_desc + "'>" + style_escaped + "</span>";
+
+ // std::cout << " markup: " << markup << " (" << name << ")" << std::endl;
+
+ renderer->set_property("markup", markup);
+}
+
+
+// Callbacks
+
+// Need to update style list
+void
+FontSelector::on_family_changed() {
+
+ if (signal_block) return;
+ signal_block = true;
+
+ Glib::RefPtr<Gtk::TreeModel> model;
+ Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected(model);
+
+ if (!iter) {
+ // This can happen just after the family list is recreated.
+ signal_block = false;
+ return;
+ }
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->ensureRowStyles(model, iter);
+
+ Gtk::TreeModel::Row row = *iter;
+
+ // Get family name
+ Glib::ustring family;
+ row.get_value(0, family);
+
+ // Get style list (TO DO: Get rid of GList)
+ GList *styles;
+ row.get_value(1, styles);
+
+ // Find best style match for selected family with current style (e.g. of selected text).
+ Glib::ustring style = fontlister->get_font_style();
+ Glib::ustring best = fontlister->get_best_style_match (family, style);
+
+ // Create are own store of styles for selected font-family (the font-family selected
+ // in the dialog may not be the same as stored in the font-lister class until the
+ // "Apply" button is triggered).
+ Gtk::TreeModel::iterator it_best;
+ FontLister::FontStyleListClass FontStyleList;
+ Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList);
+
+ // Build list and find best match.
+ for ( ; styles; styles = styles->next ) {
+ Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append();
+ (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName;
+ (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName;
+ if (best == ((StyleNames*)styles->data)->CssName) {
+ it_best = treeModelIter;
+ }
+ }
+
+ // Attach store to tree view and select row.
+ style_treeview.set_model (local_style_list_store);
+ if (it_best) {
+ style_treeview.get_selection()->select (it_best);
+ }
+
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::on_style_changed() {
+ if (signal_block) return;
+
+ // Update variations widget if new style selected from style widget.
+ signal_block = true;
+ Glib::ustring fontspec = get_fontspec( false );
+ update_variations(fontspec);
+ signal_block = false;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::on_size_changed() {
+
+ if (signal_block) return;
+
+ double size;
+ Glib::ustring input = size_combobox.get_active_text();
+ try {
+ size = std::stod (input);
+ }
+ catch (std::invalid_argument) {
+ std::cerr << "FontSelector::on_size_changed: Invalid input: " << input << std::endl;
+ size = -1;
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // Arbitrary: Text and Font preview freezes with huge font sizes.
+ int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000);
+
+ if (size <= 0) {
+ return;
+ }
+ if (size > max_size)
+ size = max_size;
+
+ if (fabs(font_size - size) > 0.001) {
+ font_size = size;
+ // Let world know
+ changed_emit();
+ }
+}
+
+void
+FontSelector::on_variations_changed() {
+
+ if (signal_block) return;
+
+ // Let world know
+ changed_emit();
+}
+
+void
+FontSelector::changed_emit() {
+ signal_block = true;
+ signal_changed.emit (get_fontspec());
+ if (initial) {
+ initial = false;
+ family_treecolumn.unset_cell_data_func (family_cell);
+ family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func_markup);
+ }
+ signal_block = false;
+}
+
+void FontSelector::update_variations(const Glib::ustring& fontspec) {
+ font_variations.update(fontspec);
+
+ // Check if there are any variations available; if not, don't expand font_variations_scroll
+ bool hasContent = font_variations.variations_present();
+ font_variations_scroll.set_vexpand(hasContent);
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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 :
diff --git a/src/ui/widget/font-selector.h b/src/ui/widget/font-selector.h
new file mode 100644
index 0000000..e1d358e
--- /dev/null
+++ b/src/ui/widget/font-selector.h
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ *
+ * The routines here create and manage a font selector widget with three parts,
+ * one each for font-family, font-style, and font-size.
+ *
+ * It is used by the TextEdit and Glyphs panel dialogs. The FontLister class is used
+ * to access the list of font-families and their associated styles for fonts either
+ * on the system or in the document. The FontLister class is also used by the Text
+ * toolbar. Fonts are kept track of by their "fontspecs" which are the same as the
+ * strings that Pango generates.
+ *
+ * The main functions are:
+ * Create the font-seletor widget.
+ * Update the lists when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Emit a signal when any change is made so that the Text Preview can be updated.
+ * Provide the currently selected values.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_H
+#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_H
+
+#include <gtkmm/grid.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/label.h>
+#include <gtkmm/comboboxtext.h>
+
+#include "ui/widget/font-variations.h"
+#include "ui/widget/scrollprotected.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container of widgets for selecting font faces.
+ *
+ * It is used by the TextEdit and Glyphs panel dialogs. The FontSelector class utilizes the
+ * FontLister class to obtain a list of font-families and their associated styles for fonts either
+ * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts
+ * are kept track of by their "fontspecs" which are the same as the strings that Pango generates.
+ *
+ * The main functions are:
+ * Create the font-selector widget.
+ * Update the child widgets when a new text selection is made.
+ * Update the Style list when a new font-family is selected, highlighting the
+ * best match to the original font style (as not all fonts have the same style options).
+ * Emit a signal when any change is made to a child widget.
+ */
+class FontSelector : public Gtk::Grid
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontSelector (bool with_size = true, bool with_variations = true);
+
+protected:
+
+ // Font family
+ Gtk::Frame family_frame;
+ Gtk::ScrolledWindow family_scroll;
+ Gtk::TreeView family_treeview;
+ Gtk::TreeViewColumn family_treecolumn;
+ Gtk::CellRendererText family_cell;
+
+ // Font style
+ Gtk::Frame style_frame;
+ Gtk::ScrolledWindow style_scroll;
+ Gtk::TreeView style_treeview;
+ Gtk::TreeViewColumn style_treecolumn;
+ Gtk::CellRendererText style_cell;
+
+ // Font size
+ Gtk::Label size_label;
+ ScrollProtected<Gtk::ComboBoxText> size_combobox;
+
+ // Font variations
+ Gtk::ScrolledWindow font_variations_scroll;
+ FontVariations font_variations;
+
+private:
+
+ // Set sizes in font size combobox.
+ void set_sizes();
+ void set_fontsize_tooltip();
+
+ // Use font style when listing style names.
+ void style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter);
+
+ // Signal handlers
+ void on_family_changed();
+ void on_style_changed();
+ void on_size_changed();
+ void on_variations_changed();
+
+ // Signals
+ sigc::signal<void, Glib::ustring> signal_changed;
+ void changed_emit();
+ bool signal_block;
+
+ // Variables
+ double font_size;
+ bool initial = true;
+
+ // control font variations update and UI element size
+ void update_variations(const Glib::ustring& fontspec);
+
+public:
+
+ /**
+ * Update GUI based on fontspec
+ */
+ void update_font ();
+ void update_size (double size);
+
+ /**
+ * Get fontspec based on current settings. (Does not handle size, yet.)
+ */
+ Glib::ustring get_fontspec(bool use_variations = true);
+
+ /**
+ * Get font size. Could be merged with fontspec.
+ */
+ double get_fontsize() { return font_size; };
+
+ /**
+ * Let others know that user has changed GUI settings.
+ * (Used to enable 'Apply' and 'Default' buttons.)
+ */
+ sigc::connection connectChanged(sigc::slot<void, Glib::ustring> slot) {
+ return signal_changed.connect(slot);
+ }
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_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/ui/widget/font-variants.cpp b/src/ui/widget/font-variants.cpp
new file mode 100644
index 0000000..3ec77c8
--- /dev/null
+++ b/src/ui/widget/font-variants.cpp
@@ -0,0 +1,1459 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2015, 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include <libnrtype/font-instance.h>
+
+#include "font-variants.h"
+
+// For updating from selection
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ // A simple class to handle UI for one feature. We could of derived this from Gtk::HBox but by
+ // attaching widgets directly to Gtk::Grid, we keep columns lined up (which may or may not be a
+ // good thing).
+ class Feature
+ {
+ public:
+ Feature( const Glib::ustring& name, OTSubstitution& glyphs, int options, Glib::ustring family, Gtk::Grid& grid, int &row, FontVariants* parent)
+ : _name (name)
+ {
+ Gtk::Label* table_name = Gtk::manage (new Gtk::Label());
+ table_name->set_markup ("\"" + name + "\" ");
+
+ grid.attach (*table_name, 0, row, 1, 1);
+
+ Gtk::FlowBox* flow_box = nullptr;
+ Gtk::ScrolledWindow* scrolled_window = nullptr;
+ if (options > 2) {
+ // If there are more than 2 option, pack them into a flowbox instead of directly putting them in the grid.
+ // Some fonts might have a table with many options (Bungee Hairline table 'ornm' has 113 entries).
+ flow_box = Gtk::manage (new Gtk::FlowBox());
+ flow_box->set_selection_mode(); // Turn off selection
+ flow_box->set_homogeneous();
+ flow_box->set_max_children_per_line (100); // Override default value
+ flow_box->set_min_children_per_line (10); // Override default value
+
+ // We pack this into a scrollbar... otherwise the minimum height is set to what is required to fit all
+ // flow box children into the flow box when the flow box has minimum width. (Crazy if you ask me!)
+ scrolled_window = Gtk::manage (new Gtk::ScrolledWindow());
+ scrolled_window->set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ scrolled_window->add(*flow_box);
+ }
+
+ Gtk::RadioButton::Group group;
+ for (int i = 0; i < options; ++i) {
+
+ // Create radio button and create or add to button group.
+ Gtk::RadioButton* button = Gtk::manage (new Gtk::RadioButton());
+ if (i == 0) {
+ group = button->get_group();
+ } else {
+ button->set_group (group);
+ }
+ button->signal_clicked().connect ( sigc::mem_fun(*parent, &FontVariants::feature_callback) );
+ buttons.push_back (button);
+
+ // Create label.
+ Gtk::Label* label = Gtk::manage (new Gtk::Label());
+
+ // Restrict label width (some fonts have lots of alternatives).
+ label->set_line_wrap( true );
+ label->set_line_wrap_mode( Pango::WRAP_WORD_CHAR );
+ label->set_ellipsize( Pango::ELLIPSIZE_END );
+ label->set_lines(3);
+ label->set_hexpand();
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += family;
+ markup += "' font_features='";
+ markup += name;
+ markup += " ";
+ markup += std::to_string (i);
+ markup += "'>";
+ markup += Glib::Markup::escape_text (glyphs.input);
+ markup += "</span>";
+ label->set_markup (markup);
+
+ // Add button and label to widget
+ if (!flow_box) {
+ // Attach directly to grid (keeps things aligned row-to-row).
+ grid.attach (*button, 2*i+1, row, 1, 1);
+ grid.attach (*label, 2*i+2, row, 1, 1);
+ } else {
+ // Pack into FlowBox
+
+ // Pack button and label into a box so they stay together.
+ Gtk::Box* box = Gtk::manage (new Gtk::Box());
+ box->add(*button);
+ box->add(*label);
+
+ flow_box->add(*box);
+ }
+ }
+
+ if (scrolled_window) {
+ grid.attach (*scrolled_window, 1, row, 4, 1);
+ }
+ }
+
+ Glib::ustring
+ get_css()
+ {
+ int i = 0;
+ for (auto b: buttons) {
+ if (b->get_active()) {
+ if (i == 0) {
+ // Features are always off by default (for those handled here).
+ return "";
+ } else if (i == 1) {
+ // Feature without value has implied value of 1.
+ return ("\"" + _name + "\", ");
+ } else {
+ // Feature with value greater than 1 must be explicitly set.
+ return ("\"" + _name + "\" " + std::to_string (i) + ", ");
+ }
+ }
+ ++i;
+ }
+ return "";
+ }
+
+ void
+ set_active(int i)
+ {
+ if (i < buttons.size()) {
+ buttons[i]->set_active();
+ }
+ }
+
+ private:
+ Glib::ustring _name;
+ std::vector <Gtk::RadioButton*> buttons;
+ };
+
+ FontVariants::FontVariants () :
+ Gtk::Box (Gtk::ORIENTATION_VERTICAL),
+ _ligatures_frame ( Glib::ustring(C_("Font feature", "Ligatures" )) ),
+ _ligatures_common ( Glib::ustring(C_("Font feature", "Common" )) ),
+ _ligatures_discretionary ( Glib::ustring(C_("Font feature", "Discretionary")) ),
+ _ligatures_historical ( Glib::ustring(C_("Font feature", "Historical" )) ),
+ _ligatures_contextual ( Glib::ustring(C_("Font feature", "Contextual" )) ),
+
+ _position_frame ( Glib::ustring(C_("Font feature", "Position" )) ),
+ _position_normal ( Glib::ustring(C_("Font feature", "Normal" )) ),
+ _position_sub ( Glib::ustring(C_("Font feature", "Subscript" )) ),
+ _position_super ( Glib::ustring(C_("Font feature", "Superscript" )) ),
+
+ _caps_frame ( Glib::ustring(C_("Font feature", "Capitals" )) ),
+ _caps_normal ( Glib::ustring(C_("Font feature", "Normal" )) ),
+ _caps_small ( Glib::ustring(C_("Font feature", "Small" )) ),
+ _caps_all_small ( Glib::ustring(C_("Font feature", "All small" )) ),
+ _caps_petite ( Glib::ustring(C_("Font feature", "Petite" )) ),
+ _caps_all_petite ( Glib::ustring(C_("Font feature", "All petite" )) ),
+ _caps_unicase ( Glib::ustring(C_("Font feature", "Unicase" )) ),
+ _caps_titling ( Glib::ustring(C_("Font feature", "Titling" )) ),
+
+ _numeric_frame ( Glib::ustring(C_("Font feature", "Numeric" )) ),
+ _numeric_lining ( Glib::ustring(C_("Font feature", "Lining" )) ),
+ _numeric_old_style ( Glib::ustring(C_("Font feature", "Old Style" )) ),
+ _numeric_default_style ( Glib::ustring(C_("Font feature", "Default Style")) ),
+ _numeric_proportional ( Glib::ustring(C_("Font feature", "Proportional" )) ),
+ _numeric_tabular ( Glib::ustring(C_("Font feature", "Tabular" )) ),
+ _numeric_default_width ( Glib::ustring(C_("Font feature", "Default Width")) ),
+ _numeric_diagonal ( Glib::ustring(C_("Font feature", "Diagonal" )) ),
+ _numeric_stacked ( Glib::ustring(C_("Font feature", "Stacked" )) ),
+ _numeric_default_fractions( Glib::ustring(C_("Font feature", "Default Fractions")) ),
+ _numeric_ordinal ( Glib::ustring(C_("Font feature", "Ordinal" )) ),
+ _numeric_slashed_zero ( Glib::ustring(C_("Font feature", "Slashed Zero" )) ),
+
+ _asian_frame ( Glib::ustring(C_("Font feature", "East Asian" )) ),
+ _asian_default_variant ( Glib::ustring(C_("Font feature", "Default" )) ),
+ _asian_jis78 ( Glib::ustring(C_("Font feature", "JIS78" )) ),
+ _asian_jis83 ( Glib::ustring(C_("Font feature", "JIS83" )) ),
+ _asian_jis90 ( Glib::ustring(C_("Font feature", "JIS90" )) ),
+ _asian_jis04 ( Glib::ustring(C_("Font feature", "JIS04" )) ),
+ _asian_simplified ( Glib::ustring(C_("Font feature", "Simplified" )) ),
+ _asian_traditional ( Glib::ustring(C_("Font feature", "Traditional" )) ),
+ _asian_default_width ( Glib::ustring(C_("Font feature", "Default" )) ),
+ _asian_full_width ( Glib::ustring(C_("Font feature", "Full Width" )) ),
+ _asian_proportional_width ( Glib::ustring(C_("Font feature", "Proportional" )) ),
+ _asian_ruby ( Glib::ustring(C_("Font feature", "Ruby" )) ),
+
+ _feature_frame ( Glib::ustring(C_("Font feature", "Feature Settings")) ),
+ _feature_label ( Glib::ustring(C_("Font feature", "Selection has different Feature Settings!")) ),
+
+ _ligatures_changed( false ),
+ _position_changed( false ),
+ _caps_changed( false ),
+ _numeric_changed( false ),
+ _asian_changed( false ),
+ _feature_vbox(Gtk::ORIENTATION_VERTICAL)
+
+ {
+
+ set_name ( "FontVariants" );
+
+ // Ligatures --------------------------
+
+ // Add tooltips
+ _ligatures_common.set_tooltip_text(
+ _("Common ligatures. On by default. OpenType tables: 'liga', 'clig'"));
+ _ligatures_discretionary.set_tooltip_text(
+ _("Discretionary ligatures. Off by default. OpenType table: 'dlig'"));
+ _ligatures_historical.set_tooltip_text(
+ _("Historical ligatures. Off by default. OpenType table: 'hlig'"));
+ _ligatures_contextual.set_tooltip_text(
+ _("Contextual forms. On by default. OpenType table: 'calt'"));
+
+ // Add signals
+ _ligatures_common.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) );
+ _ligatures_discretionary.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) );
+ _ligatures_historical.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) );
+ _ligatures_contextual.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) );
+
+ // Restrict label widths (some fonts have lots of ligatures). Must also set ellipsize mode.
+ Gtk::Label* labels[] = {
+ &_ligatures_label_common,
+ &_ligatures_label_discretionary,
+ &_ligatures_label_historical,
+ &_ligatures_label_contextual
+ };
+ for (auto label : labels) {
+ // char limit - not really needed, since number of lines is restricted
+ label->set_max_width_chars(999);
+ // show ellipsis when text overflows
+ label->set_ellipsize(Pango::ELLIPSIZE_END);
+ // up to 5 lines
+ label->set_lines(5);
+ // multiline
+ label->set_line_wrap();
+ // break it as needed
+ label->set_line_wrap_mode(Pango::WRAP_WORD_CHAR);
+ }
+
+ // Allow user to select characters. Not useful as this selects the ligatures.
+ // _ligatures_label_common.set_selectable( true );
+ // _ligatures_label_discretionary.set_selectable( true );
+ // _ligatures_label_historical.set_selectable( true );
+ // _ligatures_label_contextual.set_selectable( true );
+
+ // Add to frame
+ _ligatures_grid.attach( _ligatures_common, 0, 0, 1, 1);
+ _ligatures_grid.attach( _ligatures_discretionary, 0, 1, 1, 1);
+ _ligatures_grid.attach( _ligatures_historical, 0, 2, 1, 1);
+ _ligatures_grid.attach( _ligatures_contextual, 0, 3, 1, 1);
+ _ligatures_grid.attach( _ligatures_label_common, 1, 0, 1, 1);
+ _ligatures_grid.attach( _ligatures_label_discretionary, 1, 1, 1, 1);
+ _ligatures_grid.attach( _ligatures_label_historical, 1, 2, 1, 1);
+ _ligatures_grid.attach( _ligatures_label_contextual, 1, 3, 1, 1);
+
+ _ligatures_grid.set_margin_start(15);
+ _ligatures_grid.set_margin_end(15);
+
+ _ligatures_frame.add( _ligatures_grid );
+ pack_start( _ligatures_frame, Gtk::PACK_SHRINK );
+
+ ligatures_init();
+
+ // Position ----------------------------------
+
+ // Add tooltips
+ _position_normal.set_tooltip_text( _("Normal position."));
+ _position_sub.set_tooltip_text( _("Subscript. OpenType table: 'subs'") );
+ _position_super.set_tooltip_text( _("Superscript. OpenType table: 'sups'") );
+
+ // Group buttons
+ Gtk::RadioButton::Group position_group = _position_normal.get_group();
+ _position_sub.set_group(position_group);
+ _position_super.set_group(position_group);
+
+ // Add signals
+ _position_normal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) );
+ _position_sub.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) );
+ _position_super.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) );
+
+ // Add to frame
+ _position_grid.attach( _position_normal, 0, 0, 1, 1);
+ _position_grid.attach( _position_sub, 1, 0, 1, 1);
+ _position_grid.attach( _position_super, 2, 0, 1, 1);
+
+ _position_grid.set_margin_start(15);
+ _position_grid.set_margin_end(15);
+
+ _position_frame.add( _position_grid );
+ pack_start( _position_frame, Gtk::PACK_SHRINK );
+
+ position_init();
+
+ // Caps ----------------------------------
+
+ // Add tooltips
+ _caps_normal.set_tooltip_text( _("Normal capitalization."));
+ _caps_small.set_tooltip_text( _("Small-caps (lowercase). OpenType table: 'smcp'"));
+ _caps_all_small.set_tooltip_text( _("All small-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'smcp'"));
+ _caps_petite.set_tooltip_text( _("Petite-caps (lowercase). OpenType table: 'pcap'"));
+ _caps_all_petite.set_tooltip_text( _("All petite-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'pcap'"));
+ _caps_unicase.set_tooltip_text( _("Unicase (small caps for uppercase, normal for lowercase). OpenType table: 'unic'"));
+ _caps_titling.set_tooltip_text( _("Titling caps (lighter-weight uppercase for use in titles). OpenType table: 'titl'"));
+
+ // Group buttons
+ Gtk::RadioButton::Group caps_group = _caps_normal.get_group();
+ _caps_small.set_group(caps_group);
+ _caps_all_small.set_group(caps_group);
+ _caps_petite.set_group(caps_group);
+ _caps_all_petite.set_group(caps_group);
+ _caps_unicase.set_group(caps_group);
+ _caps_titling.set_group(caps_group);
+
+ // Add signals
+ _caps_normal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_small.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_all_small.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_petite.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_all_petite.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_unicase.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+ _caps_titling.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) );
+
+ // Add to frame
+ _caps_grid.attach( _caps_normal, 0, 0, 1, 1);
+ _caps_grid.attach( _caps_unicase, 1, 0, 1, 1);
+ _caps_grid.attach( _caps_titling, 2, 0, 1, 1);
+ _caps_grid.attach( _caps_small, 0, 1, 1, 1);
+ _caps_grid.attach( _caps_all_small, 1, 1, 1, 1);
+ _caps_grid.attach( _caps_petite, 2, 1, 1, 1);
+ _caps_grid.attach( _caps_all_petite, 3, 1, 1, 1);
+
+ _caps_grid.set_margin_start(15);
+ _caps_grid.set_margin_end(15);
+
+ _caps_frame.add( _caps_grid );
+ pack_start( _caps_frame, Gtk::PACK_SHRINK );
+
+ caps_init();
+
+ // Numeric ------------------------------
+
+ // Add tooltips
+ _numeric_default_style.set_tooltip_text( _("Normal style."));
+ _numeric_lining.set_tooltip_text( _("Lining numerals. OpenType table: 'lnum'"));
+ _numeric_old_style.set_tooltip_text( _("Old style numerals. OpenType table: 'onum'"));
+ _numeric_default_width.set_tooltip_text( _("Normal widths."));
+ _numeric_proportional.set_tooltip_text( _("Proportional width numerals. OpenType table: 'pnum'"));
+ _numeric_tabular.set_tooltip_text( _("Same width numerals. OpenType table: 'tnum'"));
+ _numeric_default_fractions.set_tooltip_text( _("Normal fractions."));
+ _numeric_diagonal.set_tooltip_text( _("Diagonal fractions. OpenType table: 'frac'"));
+ _numeric_stacked.set_tooltip_text( _("Stacked fractions. OpenType table: 'afrc'"));
+ _numeric_ordinal.set_tooltip_text( _("Ordinals (raised 'th', etc.). OpenType table: 'ordn'"));
+ _numeric_slashed_zero.set_tooltip_text( _("Slashed zeros. OpenType table: 'zero'"));
+
+ // Group buttons
+ Gtk::RadioButton::Group style_group = _numeric_default_style.get_group();
+ _numeric_lining.set_group(style_group);
+ _numeric_old_style.set_group(style_group);
+
+ Gtk::RadioButton::Group width_group = _numeric_default_width.get_group();
+ _numeric_proportional.set_group(width_group);
+ _numeric_tabular.set_group(width_group);
+
+ Gtk::RadioButton::Group fraction_group = _numeric_default_fractions.get_group();
+ _numeric_diagonal.set_group(fraction_group);
+ _numeric_stacked.set_group(fraction_group);
+
+ // Add signals
+ _numeric_default_style.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_lining.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_old_style.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_default_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_proportional.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_tabular.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_default_fractions.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_diagonal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_stacked.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_ordinal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+ _numeric_slashed_zero.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) );
+
+ // Add to frame
+ _numeric_grid.attach (_numeric_default_style, 0, 0, 1, 1);
+ _numeric_grid.attach (_numeric_lining, 1, 0, 1, 1);
+ _numeric_grid.attach (_numeric_lining_label, 2, 0, 1, 1);
+ _numeric_grid.attach (_numeric_old_style, 3, 0, 1, 1);
+ _numeric_grid.attach (_numeric_old_style_label, 4, 0, 1, 1);
+
+ _numeric_grid.attach (_numeric_default_width, 0, 1, 1, 1);
+ _numeric_grid.attach (_numeric_proportional, 1, 1, 1, 1);
+ _numeric_grid.attach (_numeric_proportional_label, 2, 1, 1, 1);
+ _numeric_grid.attach (_numeric_tabular, 3, 1, 1, 1);
+ _numeric_grid.attach (_numeric_tabular_label, 4, 1, 1, 1);
+
+ _numeric_grid.attach (_numeric_default_fractions, 0, 2, 1, 1);
+ _numeric_grid.attach (_numeric_diagonal, 1, 2, 1, 1);
+ _numeric_grid.attach (_numeric_diagonal_label, 2, 2, 1, 1);
+ _numeric_grid.attach (_numeric_stacked, 3, 2, 1, 1);
+ _numeric_grid.attach (_numeric_stacked_label, 4, 2, 1, 1);
+
+ _numeric_grid.attach (_numeric_ordinal, 0, 3, 1, 1);
+ _numeric_grid.attach (_numeric_ordinal_label, 1, 3, 4, 1);
+
+ _numeric_grid.attach (_numeric_slashed_zero, 0, 4, 1, 1);
+ _numeric_grid.attach (_numeric_slashed_zero_label, 1, 4, 1, 1);
+
+ _numeric_grid.set_margin_start(15);
+ _numeric_grid.set_margin_end(15);
+
+ _numeric_frame.add( _numeric_grid );
+ pack_start( _numeric_frame, Gtk::PACK_SHRINK );
+
+ // East Asian
+
+ // Add tooltips
+ _asian_default_variant.set_tooltip_text ( _("Default variant."));
+ _asian_jis78.set_tooltip_text( _("JIS78 forms. OpenType table: 'jp78'."));
+ _asian_jis83.set_tooltip_text( _("JIS83 forms. OpenType table: 'jp83'."));
+ _asian_jis90.set_tooltip_text( _("JIS90 forms. OpenType table: 'jp90'."));
+ _asian_jis04.set_tooltip_text( _("JIS2004 forms. OpenType table: 'jp04'."));
+ _asian_simplified.set_tooltip_text( _("Simplified forms. OpenType table: 'smpl'."));
+ _asian_traditional.set_tooltip_text( _("Traditional forms. OpenType table: 'trad'."));
+ _asian_default_width.set_tooltip_text ( _("Default width."));
+ _asian_full_width.set_tooltip_text( _("Full width variants. OpenType table: 'fwid'."));
+ _asian_proportional_width.set_tooltip_text(_("Proportional width variants. OpenType table: 'pwid'."));
+ _asian_ruby.set_tooltip_text( _("Ruby variants. OpenType table: 'ruby'."));
+
+ // Add signals
+ _asian_default_variant.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_jis78.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_jis83.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_jis90.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_jis04.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_simplified.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_traditional.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_default_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_full_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_proportional_width.signal_clicked().connect (sigc::mem_fun(*this, &FontVariants::asian_callback) );
+ _asian_ruby.signal_clicked().connect( sigc::mem_fun(*this, &FontVariants::asian_callback) );
+
+ // Add to frame
+ _asian_grid.attach (_asian_default_variant, 0, 0, 1, 1);
+ _asian_grid.attach (_asian_jis78, 1, 0, 1, 1);
+ _asian_grid.attach (_asian_jis83, 2, 0, 1, 1);
+ _asian_grid.attach (_asian_jis90, 1, 1, 1, 1);
+ _asian_grid.attach (_asian_jis04, 2, 1, 1, 1);
+ _asian_grid.attach (_asian_simplified, 1, 2, 1, 1);
+ _asian_grid.attach (_asian_traditional, 2, 2, 1, 1);
+ _asian_grid.attach (_asian_default_width, 0, 3, 1, 1);
+ _asian_grid.attach (_asian_full_width, 1, 3, 1, 1);
+ _asian_grid.attach (_asian_proportional_width, 2, 3, 1, 1);
+ _asian_grid.attach (_asian_ruby, 0, 4, 1, 1);
+
+ _asian_grid.set_margin_start(15);
+ _asian_grid.set_margin_end(15);
+
+ _asian_frame.add( _asian_grid );
+ pack_start( _asian_frame, Gtk::PACK_SHRINK );
+
+ // Group Buttons
+ Gtk::RadioButton::Group asian_variant_group = _asian_default_variant.get_group();
+ _asian_jis78.set_group(asian_variant_group);
+ _asian_jis83.set_group(asian_variant_group);
+ _asian_jis90.set_group(asian_variant_group);
+ _asian_jis04.set_group(asian_variant_group);
+ _asian_simplified.set_group(asian_variant_group);
+ _asian_traditional.set_group(asian_variant_group);
+
+ Gtk::RadioButton::Group asian_width_group = _asian_default_width.get_group();
+ _asian_full_width.set_group (asian_width_group);
+ _asian_proportional_width.set_group (asian_width_group);
+
+ // Feature settings ---------------------
+
+ // Add tooltips
+ _feature_entry.set_tooltip_text( _("Feature settings in CSS form (e.g. \"wxyz\" or \"wxyz\" 3)."));
+
+ _feature_substitutions.set_justify( Gtk::JUSTIFY_LEFT );
+ _feature_substitutions.set_line_wrap( true );
+ _feature_substitutions.set_line_wrap_mode( Pango::WRAP_WORD_CHAR );
+
+ _feature_list.set_justify( Gtk::JUSTIFY_LEFT );
+ _feature_list.set_line_wrap( true );
+
+ // Add to frame
+ _feature_vbox.pack_start( _feature_grid, Gtk::PACK_SHRINK );
+ _feature_vbox.pack_start( _feature_entry, Gtk::PACK_SHRINK );
+ _feature_vbox.pack_start( _feature_label, Gtk::PACK_SHRINK );
+ _feature_vbox.pack_start( _feature_substitutions, Gtk::PACK_SHRINK );
+ _feature_vbox.pack_start( _feature_list, Gtk::PACK_SHRINK );
+
+ _feature_vbox.set_margin_start(15);
+ _feature_vbox.set_margin_end(15);
+
+ _feature_frame.add( _feature_vbox );
+ pack_start( _feature_frame, Gtk::PACK_SHRINK );
+
+ // Add signals
+ //_feature_entry.signal_key_press_event().connect ( sigc::mem_fun(*this, &FontVariants::feature_callback) );
+ _feature_entry.signal_changed().connect( sigc::mem_fun(*this, &FontVariants::feature_callback) );
+
+ show_all_children();
+
+ }
+
+ void
+ FontVariants::ligatures_init() {
+ // std::cout << "FontVariants::ligatures_init()" << std::endl;
+ }
+
+ void
+ FontVariants::ligatures_callback() {
+ // std::cout << "FontVariants::ligatures_callback()" << std::endl;
+ _ligatures_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::position_init() {
+ // std::cout << "FontVariants::position_init()" << std::endl;
+ }
+
+ void
+ FontVariants::position_callback() {
+ // std::cout << "FontVariants::position_callback()" << std::endl;
+ _position_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::caps_init() {
+ // std::cout << "FontVariants::caps_init()" << std::endl;
+ }
+
+ void
+ FontVariants::caps_callback() {
+ // std::cout << "FontVariants::caps_callback()" << std::endl;
+ _caps_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::numeric_init() {
+ // std::cout << "FontVariants::numeric_init()" << std::endl;
+ }
+
+ void
+ FontVariants::numeric_callback() {
+ // std::cout << "FontVariants::numeric_callback()" << std::endl;
+ _numeric_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::asian_init() {
+ // std::cout << "FontVariants::asian_init()" << std::endl;
+ }
+
+ void
+ FontVariants::asian_callback() {
+ // std::cout << "FontVariants::asian_callback()" << std::endl;
+ _asian_changed = true;
+ _changed_signal.emit();
+ }
+
+ void
+ FontVariants::feature_init() {
+ // std::cout << "FontVariants::feature_init()" << std::endl;
+ }
+
+ void
+ FontVariants::feature_callback() {
+ // std::cout << "FontVariants::feature_callback()" << std::endl;
+ _feature_changed = true;
+ _changed_signal.emit();
+ }
+
+ // Update GUI based on query.
+ void
+ FontVariants::update( SPStyle const *query, bool different_features, Glib::ustring& font_spec ) {
+
+ update_opentype( font_spec );
+
+ _ligatures_all = query->font_variant_ligatures.computed;
+ _ligatures_mix = query->font_variant_ligatures.value;
+
+ _ligatures_common.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_COMMON );
+ _ligatures_discretionary.set_active(_ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY );
+ _ligatures_historical.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL );
+ _ligatures_contextual.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL );
+
+ _ligatures_common.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_COMMON );
+ _ligatures_discretionary.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY );
+ _ligatures_historical.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL );
+ _ligatures_contextual.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL );
+
+ _position_all = query->font_variant_position.computed;
+ _position_mix = query->font_variant_position.value;
+
+ _position_normal.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_NORMAL );
+ _position_sub.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_SUB );
+ _position_super.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_SUPER );
+
+ _position_normal.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_NORMAL );
+ _position_sub.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_SUB );
+ _position_super.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_SUPER );
+
+ _caps_all = query->font_variant_caps.computed;
+ _caps_mix = query->font_variant_caps.value;
+
+ _caps_normal.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_NORMAL );
+ _caps_small.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_SMALL );
+ _caps_all_small.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL );
+ _caps_petite.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_PETITE );
+ _caps_all_petite.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE );
+ _caps_unicase.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_UNICASE );
+ _caps_titling.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_TITLING );
+
+ _caps_normal.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_NORMAL );
+ _caps_small.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_SMALL );
+ _caps_all_small.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL );
+ _caps_petite.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_PETITE );
+ _caps_all_petite.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE );
+ _caps_unicase.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_UNICASE );
+ _caps_titling.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_TITLING );
+
+ _numeric_all = query->font_variant_numeric.computed;
+ _numeric_mix = query->font_variant_numeric.value;
+
+ if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS) {
+ _numeric_lining.set_active();
+ } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS) {
+ _numeric_old_style.set_active();
+ } else {
+ _numeric_default_style.set_active();
+ }
+
+ if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS) {
+ _numeric_proportional.set_active();
+ } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS) {
+ _numeric_tabular.set_active();
+ } else {
+ _numeric_default_width.set_active();
+ }
+
+ if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS) {
+ _numeric_diagonal.set_active();
+ } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS) {
+ _numeric_stacked.set_active();
+ } else {
+ _numeric_default_fractions.set_active();
+ }
+
+ _numeric_ordinal.set_active( _numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL );
+ _numeric_slashed_zero.set_active( _numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO );
+
+
+ _numeric_lining.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS );
+ _numeric_old_style.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS );
+ _numeric_proportional.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS );
+ _numeric_tabular.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS );
+ _numeric_diagonal.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS );
+ _numeric_stacked.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS );
+ _numeric_ordinal.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL );
+ _numeric_slashed_zero.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO );
+
+ _asian_all = query->font_variant_east_asian.computed;
+ _asian_mix = query->font_variant_east_asian.value;
+
+ if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78) {
+ _asian_jis78.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83) {
+ _asian_jis83.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90) {
+ _asian_jis90.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04) {
+ _asian_jis04.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED) {
+ _asian_simplified.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL) {
+ _asian_traditional.set_active();
+ } else {
+ _asian_default_variant.set_active();
+ }
+
+ if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH) {
+ _asian_full_width.set_active();
+ } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH) {
+ _asian_proportional_width.set_active();
+ } else {
+ _asian_default_width.set_active();
+ }
+
+ _asian_ruby.set_active ( _asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY );
+
+ _asian_jis78.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78);
+ _asian_jis83.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83);
+ _asian_jis90.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90);
+ _asian_jis04.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04);
+ _asian_simplified.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED);
+ _asian_traditional.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL);
+ _asian_full_width.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH);
+ _asian_proportional_width.set_inconsistent(_asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH);
+ _asian_ruby.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY);
+
+ // Fix me: Should match a space if second part matches. ---,
+ // : Add boundary to 'on' and 'off'. v
+ Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("\"(\\w{4})\"\\s*([0-9]+|on|off|)");
+ Glib::MatchInfo matchInfo;
+ std::string setting;
+
+ // Set feature radiobutton (if it exists) or add to _feature_entry string.
+ char const *val = query->font_feature_settings.value();
+ if (val) {
+
+ std::vector<Glib::ustring> tokens =
+ Glib::Regex::split_simple("\\s*,\\s*", val);
+
+ for (auto token: tokens) {
+ regex->match(token, matchInfo);
+ if (matchInfo.matches()) {
+ Glib::ustring table = matchInfo.fetch(1);
+ Glib::ustring value = matchInfo.fetch(2);
+
+ if (_features.find(table) != _features.end()) {
+ int v = 0;
+ if (value == "0" || value == "off") v = 0;
+ else if (value == "1" || value == "on" || value.empty() ) v = 1;
+ else v = std::stoi(value);
+ _features[table]->set_active(v);
+ } else {
+ setting += token + ", ";
+ }
+ }
+ }
+ }
+
+ // Remove final ", "
+ if (setting.length() > 1) {
+ setting.pop_back();
+ setting.pop_back();
+ }
+
+ // Tables without radiobuttons.
+ _feature_entry.set_text( setting );
+
+ if( different_features ) {
+ _feature_label.show();
+ } else {
+ _feature_label.hide();
+ }
+ }
+
+ // Update GUI based on OpenType tables of selected font (which may be changed in font selector tab).
+ void
+ FontVariants::update_opentype (Glib::ustring& font_spec) {
+
+ // Disable/Enable based on available OpenType tables.
+ font_instance* res = font_factory::Default()->FaceFromFontSpecification( font_spec.c_str() );
+ if( res ) {
+
+ std::map<Glib::ustring, OTSubstitution>::iterator it;
+
+ if((it = res->openTypeTables.find("liga"))!= res->openTypeTables.end() ||
+ (it = res->openTypeTables.find("clig"))!= res->openTypeTables.end()) {
+ _ligatures_common.set_sensitive();
+ } else {
+ _ligatures_common.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("dlig"))!= res->openTypeTables.end()) {
+ _ligatures_discretionary.set_sensitive();
+ } else {
+ _ligatures_discretionary.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("hlig"))!= res->openTypeTables.end()) {
+ _ligatures_historical.set_sensitive();
+ } else {
+ _ligatures_historical.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("calt"))!= res->openTypeTables.end()) {
+ _ligatures_contextual.set_sensitive();
+ } else {
+ _ligatures_contextual.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("subs"))!= res->openTypeTables.end()) {
+ _position_sub.set_sensitive();
+ } else {
+ _position_sub.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("sups"))!= res->openTypeTables.end()) {
+ _position_super.set_sensitive();
+ } else {
+ _position_super.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) {
+ _caps_small.set_sensitive();
+ } else {
+ _caps_small.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() &&
+ (it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) {
+ _caps_all_small.set_sensitive();
+ } else {
+ _caps_all_small.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) {
+ _caps_petite.set_sensitive();
+ } else {
+ _caps_petite.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() &&
+ (it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) {
+ _caps_all_petite.set_sensitive();
+ } else {
+ _caps_all_petite.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("unic"))!= res->openTypeTables.end()) {
+ _caps_unicase.set_sensitive();
+ } else {
+ _caps_unicase.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("titl"))!= res->openTypeTables.end()) {
+ _caps_titling.set_sensitive();
+ } else {
+ _caps_titling.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("lnum"))!= res->openTypeTables.end()) {
+ _numeric_lining.set_sensitive();
+ } else {
+ _numeric_lining.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("onum"))!= res->openTypeTables.end()) {
+ _numeric_old_style.set_sensitive();
+ } else {
+ _numeric_old_style.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pnum"))!= res->openTypeTables.end()) {
+ _numeric_proportional.set_sensitive();
+ } else {
+ _numeric_proportional.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("tnum"))!= res->openTypeTables.end()) {
+ _numeric_tabular.set_sensitive();
+ } else {
+ _numeric_tabular.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("frac"))!= res->openTypeTables.end()) {
+ _numeric_diagonal.set_sensitive();
+ } else {
+ _numeric_diagonal.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("afrac"))!= res->openTypeTables.end()) {
+ _numeric_stacked.set_sensitive();
+ } else {
+ _numeric_stacked.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("ordn"))!= res->openTypeTables.end()) {
+ _numeric_ordinal.set_sensitive();
+ } else {
+ _numeric_ordinal.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("zero"))!= res->openTypeTables.end()) {
+ _numeric_slashed_zero.set_sensitive();
+ } else {
+ _numeric_slashed_zero.set_sensitive( false );
+ }
+
+ // East-Asian
+ if((it = res->openTypeTables.find("jp78"))!= res->openTypeTables.end()) {
+ _asian_jis78.set_sensitive();
+ } else {
+ _asian_jis78.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp83"))!= res->openTypeTables.end()) {
+ _asian_jis83.set_sensitive();
+ } else {
+ _asian_jis83.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp90"))!= res->openTypeTables.end()) {
+ _asian_jis90.set_sensitive();
+ } else {
+ _asian_jis90.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("jp04"))!= res->openTypeTables.end()) {
+ _asian_jis04.set_sensitive();
+ } else {
+ _asian_jis04.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("smpl"))!= res->openTypeTables.end()) {
+ _asian_simplified.set_sensitive();
+ } else {
+ _asian_simplified.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("trad"))!= res->openTypeTables.end()) {
+ _asian_traditional.set_sensitive();
+ } else {
+ _asian_traditional.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("fwid"))!= res->openTypeTables.end()) {
+ _asian_full_width.set_sensitive();
+ } else {
+ _asian_full_width.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("pwid"))!= res->openTypeTables.end()) {
+ _asian_proportional_width.set_sensitive();
+ } else {
+ _asian_proportional_width.set_sensitive( false );
+ }
+
+ if((it = res->openTypeTables.find("ruby"))!= res->openTypeTables.end()) {
+ _asian_ruby.set_sensitive();
+ } else {
+ _asian_ruby.set_sensitive( false );
+ }
+
+ // List available ligatures
+ Glib::ustring markup_liga;
+ Glib::ustring markup_dlig;
+ Glib::ustring markup_hlig;
+ Glib::ustring markup_calt;
+
+ for (auto table: res->openTypeTables) {
+
+ if (table.first == "liga" ||
+ table.first == "clig" ||
+ table.first == "dlig" ||
+ table.first == "hgli" ||
+ table.first == "calt") {
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += sp_font_description_get_family(res->descr);
+ markup += "'>";
+ markup += Glib::Markup::escape_text(table.second.output);
+ markup += "</span>";
+
+ if (table.first == "liga") markup_liga += markup;
+ if (table.first == "clig") markup_liga += markup;
+ if (table.first == "dlig") markup_dlig += markup;
+ if (table.first == "hlig") markup_hlig += markup;
+ if (table.first == "calt") markup_calt += markup;
+ }
+ }
+
+ _ligatures_label_common.set_markup ( markup_liga.c_str() );
+ _ligatures_label_discretionary.set_markup ( markup_dlig.c_str() );
+ _ligatures_label_historical.set_markup ( markup_hlig.c_str() );
+ _ligatures_label_contextual.set_markup ( markup_calt.c_str() );
+
+ // List available numeric variants
+ Glib::ustring markup_lnum;
+ Glib::ustring markup_onum;
+ Glib::ustring markup_pnum;
+ Glib::ustring markup_tnum;
+ Glib::ustring markup_frac;
+ Glib::ustring markup_afrc;
+ Glib::ustring markup_ordn;
+ Glib::ustring markup_zero;
+
+ for (auto table: res->openTypeTables) {
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += sp_font_description_get_family(res->descr);
+ markup += "' font_features='";
+ markup += table.first;
+ markup += "'>";
+ if (table.first == "lnum" ||
+ table.first == "onum" ||
+ table.first == "pnum" ||
+ table.first == "tnum") markup += "0123456789";
+ if (table.first == "zero") markup += "0";
+ if (table.first == "ordn") markup += "[" + table.second.before + "]" + table.second.output;
+ if (table.first == "frac" ||
+ table.first == "afrc" ) markup += "1/2 2/3 3/4 4/5 5/6"; // Can we do better?
+ markup += "</span>";
+
+ if (table.first == "lnum") markup_lnum += markup;
+ if (table.first == "onum") markup_onum += markup;
+ if (table.first == "pnum") markup_pnum += markup;
+ if (table.first == "tnum") markup_tnum += markup;
+ if (table.first == "frac") markup_frac += markup;
+ if (table.first == "afrc") markup_afrc += markup;
+ if (table.first == "ordn") markup_ordn += markup;
+ if (table.first == "zero") markup_zero += markup;
+ }
+
+ _numeric_lining_label.set_markup ( markup_lnum.c_str() );
+ _numeric_old_style_label.set_markup ( markup_onum.c_str() );
+ _numeric_proportional_label.set_markup ( markup_pnum.c_str() );
+ _numeric_tabular_label.set_markup ( markup_tnum.c_str() );
+ _numeric_diagonal_label.set_markup ( markup_frac.c_str() );
+ _numeric_stacked_label.set_markup ( markup_afrc.c_str() );
+ _numeric_ordinal_label.set_markup ( markup_ordn.c_str() );
+ _numeric_slashed_zero_label.set_markup ( markup_zero.c_str() );
+
+ // Make list of tables not handled above.
+ std::map<Glib::ustring, OTSubstitution> table_copy = res->openTypeTables;
+ if( (it = table_copy.find("liga")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("clig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("hlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("calt")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("subs")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("sups")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("smcp")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("c2sc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pcap")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("c2pc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("unic")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("titl")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("lnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("onum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("tnum")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("frac")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("afrc")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ordn")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("zero")) != table_copy.end() ) table_copy.erase( it );
+
+ if( (it = table_copy.find("jp78")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp83")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp90")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("jp04")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("smpl")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("trad")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("fwid")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pwid")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ruby")) != table_copy.end() ) table_copy.erase( it );
+
+ // An incomplete list of tables that should not be exposed to the user:
+ if( (it = table_copy.find("abvf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("abvs")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("akhn")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("blwf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("blws")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ccmp")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("cjct")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dnom")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("dtls")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("fina")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("half")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("haln")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("init")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("isol")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("locl")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("medi")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("nukt")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("numr")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pref")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pres")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("pstf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("psts")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rlig")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rkrf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rphf")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("rtlm")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("ssty")) != table_copy.end() ) table_copy.erase( it );
+ if( (it = table_copy.find("vatu")) != table_copy.end() ) table_copy.erase( it );
+
+ // Clear out old features
+ auto children = _feature_grid.get_children();
+ for (auto child: children) {
+ _feature_grid.remove (*child);
+ }
+ _features.clear();
+
+ std::string markup;
+ int grid_row = 0;
+
+ // GSUB lookup type 1 (1 to 1 mapping).
+ for (auto table: res->openTypeTables) {
+ if (table.first == "case" ||
+ table.first == "hist" ||
+ (table.first[0] == 's' && table.first[1] == 's' && !(table.first[2] == 't'))) {
+
+ if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it );
+
+ _features[table.first] = new Feature (table.first, table.second, 2,
+ sp_font_description_get_family(res->descr),
+ _feature_grid, grid_row, this);
+ grid_row++;
+ }
+ }
+
+ // GSUB lookup type 3 (1 to many mapping). Optionally type 1.
+ for (auto table: res->openTypeTables) {
+ if (table.first == "salt" ||
+ table.first == "swsh" ||
+ table.first == "cwsh" ||
+ table.first == "ornm" ||
+ table.first == "nalt" ||
+ (table.first[0] == 'c' && table.first[1] == 'v')) {
+
+ if (table.second.input.length() == 0) {
+ // This can happen if a table is not in the 'DFLT' script and 'dflt' language.
+ // We should be using the 'lang' attribute to find the correct tables.
+ // std::cerr << "FontVariants::open_type_update: "
+ // << table.first << " has no entries!" << std::endl;
+ continue;
+ }
+
+ if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it );
+
+ // Our lame attempt at determining number of alternative glyphs for one glyph:
+ int number = table.second.output.length() / table.second.input.length();
+ if (number < 1) {
+ number = 1; // Must have at least on/off, see comment above about 'lang' attribute.
+ // std::cout << table.first << " "
+ // << table.second.output.length() << "/"
+ // << table.second.input.length() << "="
+ // << number << std::endl;
+ }
+
+ _features[table.first] = new Feature (table.first, table.second, number+1,
+ sp_font_description_get_family(res->descr),
+ _feature_grid, grid_row, this);
+ grid_row++;
+ }
+ }
+
+ _feature_grid.show_all();
+
+ _feature_substitutions.set_markup ( markup.c_str() );
+
+ std::string ott_list = "OpenType tables not included above: ";
+ for(it = table_copy.begin(); it != table_copy.end(); ++it) {
+ ott_list += it->first;
+ ott_list += ", ";
+ }
+
+ if (table_copy.size() > 0) {
+ ott_list.pop_back();
+ ott_list.pop_back();
+ _feature_list.set_text( ott_list.c_str() );
+ } else {
+ _feature_list.set_text( "" );
+ }
+
+ } else {
+ std::cerr << "FontVariants::update(): Couldn't find font_instance for: "
+ << font_spec << std::endl;
+ }
+
+ _ligatures_changed = false;
+ _position_changed = false;
+ _caps_changed = false;
+ _numeric_changed = false;
+ _feature_changed = false;
+ }
+
+ void
+ FontVariants::fill_css( SPCSSAttr *css ) {
+
+ // Ligatures
+ bool common = _ligatures_common.get_active();
+ bool discretionary = _ligatures_discretionary.get_active();
+ bool historical = _ligatures_historical.get_active();
+ bool contextual = _ligatures_contextual.get_active();
+
+ if( !common && !discretionary && !historical && !contextual ) {
+ sp_repr_css_set_property(css, "font-variant-ligatures", "none" );
+ } else if ( common && !discretionary && !historical && contextual ) {
+ sp_repr_css_set_property(css, "font-variant-ligatures", "normal" );
+ } else {
+ Glib::ustring css_string;
+ if ( !common )
+ css_string += "no-common-ligatures ";
+ if ( discretionary )
+ css_string += "discretionary-ligatures ";
+ if ( historical )
+ css_string += "historical-ligatures ";
+ if ( !contextual )
+ css_string += "no-contextual ";
+ sp_repr_css_set_property(css, "font-variant-ligatures", css_string.c_str() );
+ }
+
+ // Position
+ {
+ unsigned position_new = SP_CSS_FONT_VARIANT_POSITION_NORMAL;
+ Glib::ustring css_string;
+ if( _position_normal.get_active() ) {
+ css_string = "normal";
+ } else if( _position_sub.get_active() ) {
+ css_string = "sub";
+ position_new = SP_CSS_FONT_VARIANT_POSITION_SUB;
+ } else if( _position_super.get_active() ) {
+ css_string = "super";
+ position_new = SP_CSS_FONT_VARIANT_POSITION_SUPER;
+ }
+
+ // 'if' may not be necessary... need to test.
+ if( (_position_all != position_new) || ((_position_mix != 0) && _position_changed) ) {
+ sp_repr_css_set_property(css, "font-variant-position", css_string.c_str() );
+ }
+ }
+
+ // Caps
+ {
+ //unsigned caps_new;
+ Glib::ustring css_string;
+ if( _caps_normal.get_active() ) {
+ css_string = "normal";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL;
+ } else if( _caps_small.get_active() ) {
+ css_string = "small-caps";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_SMALL;
+ } else if( _caps_all_small.get_active() ) {
+ css_string = "all-small-caps";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL;
+ } else if( _caps_petite.get_active() ) {
+ css_string = "petite";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_PETITE;
+ } else if( _caps_all_petite.get_active() ) {
+ css_string = "all-petite";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE;
+ } else if( _caps_unicase.get_active() ) {
+ css_string = "unicase";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_UNICASE;
+ } else if( _caps_titling.get_active() ) {
+ css_string = "titling";
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_TITLING;
+ //} else {
+ // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL;
+ }
+
+ // May not be necessary... need to test.
+ //if( (_caps_all != caps_new) || ((_caps_mix != 0) && _caps_changed) ) {
+ sp_repr_css_set_property(css, "font-variant-caps", css_string.c_str() );
+ //}
+ }
+
+ // Numeric
+ bool default_style = _numeric_default_style.get_active();
+ bool lining = _numeric_lining.get_active();
+ bool old_style = _numeric_old_style.get_active();
+
+ bool default_width = _numeric_default_width.get_active();
+ bool proportional = _numeric_proportional.get_active();
+ bool tabular = _numeric_tabular.get_active();
+
+ bool default_fractions = _numeric_default_fractions.get_active();
+ bool diagonal = _numeric_diagonal.get_active();
+ bool stacked = _numeric_stacked.get_active();
+
+ bool ordinal = _numeric_ordinal.get_active();
+ bool slashed_zero = _numeric_slashed_zero.get_active();
+
+ if (default_style & default_width & default_fractions & !ordinal & !slashed_zero) {
+ sp_repr_css_set_property(css, "font-variant-numeric", "normal");
+ } else {
+ Glib::ustring css_string;
+ if ( lining )
+ css_string += "lining-nums ";
+ if ( old_style )
+ css_string += "oldstyle-nums ";
+ if ( proportional )
+ css_string += "proportional-nums ";
+ if ( tabular )
+ css_string += "tabular-nums ";
+ if ( diagonal )
+ css_string += "diagonal-fractions ";
+ if ( stacked )
+ css_string += "stacked-fractions ";
+ if ( ordinal )
+ css_string += "ordinal ";
+ if ( slashed_zero )
+ css_string += "slashed-zero ";
+ sp_repr_css_set_property(css, "font-variant-numeric", css_string.c_str() );
+ }
+
+ // East Asian
+ bool jis78 = _asian_jis78.get_active();
+ bool jis83 = _asian_jis83.get_active();
+ bool jis90 = _asian_jis90.get_active();
+ bool jis04 = _asian_jis04.get_active();
+ bool simplified = _asian_simplified.get_active();
+ bool traditional = _asian_traditional.get_active();
+ bool asian_width = _asian_default_width.get_active();
+ bool fwid = _asian_full_width.get_active();
+ bool pwid = _asian_proportional_width.get_active();
+ bool ruby = _asian_ruby.get_active();
+
+ if (default_style & asian_width & !ruby) {
+ sp_repr_css_set_property(css, "font-variant-east-asian", "normal");
+ } else {
+ Glib::ustring css_string;
+ if (jis78) css_string += "jis78 ";
+ if (jis83) css_string += "jis83 ";
+ if (jis90) css_string += "jis90 ";
+ if (jis04) css_string += "jis04 ";
+ if (simplified) css_string += "simplfied ";
+ if (traditional) css_string += "traditional ";
+
+ if (fwid) css_string += "fwid ";
+ if (pwid) css_string += "pwid ";
+
+ if (ruby) css_string += "ruby ";
+
+ sp_repr_css_set_property(css, "font-variant-east-asian", css_string.c_str() );
+ }
+
+ // Feature settings
+ Glib::ustring feature_string;
+ for (auto i: _features) {
+ feature_string += i.second->get_css();
+ }
+
+ feature_string += _feature_entry.get_text();
+ // std::cout << "feature_string: " << feature_string << std::endl;
+
+ if (!feature_string.empty()) {
+ sp_repr_css_set_property(css, "font-feature-settings", feature_string.c_str());
+ } else {
+ sp_repr_css_unset_property(css, "font-feature-settings");
+ }
+ }
+
+ Glib::ustring
+ FontVariants::get_markup() {
+
+ Glib::ustring markup;
+
+ // Ligatures
+ bool common = _ligatures_common.get_active();
+ bool discretionary = _ligatures_discretionary.get_active();
+ bool historical = _ligatures_historical.get_active();
+ bool contextual = _ligatures_contextual.get_active();
+
+ if (!common) markup += "liga=0,clig=0,"; // On by default.
+ if (discretionary) markup += "dlig=1,";
+ if (historical) markup += "hlig=1,";
+ if (contextual) markup += "calt=1,";
+
+ // Position
+ if ( _position_sub.get_active() ) markup += "subs=1,";
+ else if ( _position_super.get_active() ) markup += "sups=1,";
+
+ // Caps
+ if ( _caps_small.get_active() ) markup += "smcp=1,";
+ else if ( _caps_all_small.get_active() ) markup += "c2sc=1,smcp=1,";
+ else if ( _caps_petite.get_active() ) markup += "pcap=1,";
+ else if ( _caps_all_petite.get_active() ) markup += "c2pc=1,pcap=1,";
+ else if ( _caps_unicase.get_active() ) markup += "unic=1,";
+ else if ( _caps_titling.get_active() ) markup += "titl=1,";
+
+ // Numeric
+ bool lining = _numeric_lining.get_active();
+ bool old_style = _numeric_old_style.get_active();
+
+ bool proportional = _numeric_proportional.get_active();
+ bool tabular = _numeric_tabular.get_active();
+
+ bool diagonal = _numeric_diagonal.get_active();
+ bool stacked = _numeric_stacked.get_active();
+
+ bool ordinal = _numeric_ordinal.get_active();
+ bool slashed_zero = _numeric_slashed_zero.get_active();
+
+ if (lining) markup += "lnum=1,";
+ if (old_style) markup += "onum=1,";
+ if (proportional) markup += "pnum=1,";
+ if (tabular) markup += "tnum=1,";
+ if (diagonal) markup += "frac=1,";
+ if (stacked) markup += "afrc=1,";
+ if (ordinal) markup += "ordn=1,";
+ if (slashed_zero) markup += "zero=1,";
+
+ // East Asian
+ bool jis78 = _asian_jis78.get_active();
+ bool jis83 = _asian_jis83.get_active();
+ bool jis90 = _asian_jis90.get_active();
+ bool jis04 = _asian_jis04.get_active();
+ bool simplified = _asian_simplified.get_active();
+ bool traditional = _asian_traditional.get_active();
+ //bool asian_width = _asian_default_width.get_active();
+ bool fwid = _asian_full_width.get_active();
+ bool pwid = _asian_proportional_width.get_active();
+ bool ruby = _asian_ruby.get_active();
+
+ if (jis78 ) markup += "jp78=1,";
+ if (jis83 ) markup += "jp83=1,";
+ if (jis90 ) markup += "jp90=1,";
+ if (jis04 ) markup += "jp04=1,";
+ if (simplified ) markup += "smpl=1,";
+ if (traditional ) markup += "trad=1,";
+
+ if (fwid ) markup += "fwid=1,";
+ if (pwid ) markup += "pwid=1,";
+
+ if (ruby ) markup += "ruby=1,";
+
+ // Feature settings
+ Glib::ustring feature_string;
+ for (auto i: _features) {
+ feature_string += i.second->get_css();
+ }
+
+ feature_string += _feature_entry.get_text();
+ if (!feature_string.empty()) {
+ markup += feature_string;
+ }
+
+ // std::cout << "|" << markup << "|" << std::endl;
+ return markup;
+ }
+
+} // namespace Widget
+} // namespace UI
+} // 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 :
diff --git a/src/ui/widget/font-variants.h b/src/ui/widget/font-variants.h
new file mode 100644
index 0000000..b581aa5
--- /dev/null
+++ b/src/ui/widget/font-variants.h
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2015, 2018 Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_VARIANT_H
+#define INKSCAPE_UI_WIDGET_FONT_VARIANT_H
+
+#include <gtkmm/expander.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/hvbox.h>
+
+class SPDesktop;
+class SPObject;
+class SPStyle;
+class SPCSSAttr;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Feature;
+
+/**
+ * A container for selecting font variants (OpenType Features).
+ */
+class FontVariants : public Gtk::Box
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontVariants();
+
+protected:
+ // Ligatures: To start, use four check buttons.
+ Gtk::Expander _ligatures_frame;
+ Gtk::Grid _ligatures_grid;
+ Gtk::CheckButton _ligatures_common;
+ Gtk::CheckButton _ligatures_discretionary;
+ Gtk::CheckButton _ligatures_historical;
+ Gtk::CheckButton _ligatures_contextual;
+ Gtk::Label _ligatures_label_common;
+ Gtk::Label _ligatures_label_discretionary;
+ Gtk::Label _ligatures_label_historical;
+ Gtk::Label _ligatures_label_contextual;
+
+ // Position: Exclusive options
+ Gtk::Expander _position_frame;
+ Gtk::Grid _position_grid;
+ Gtk::RadioButton _position_normal;
+ Gtk::RadioButton _position_sub;
+ Gtk::RadioButton _position_super;
+
+ // Caps: Exclusive options (maybe a dropdown menu to save space?)
+ Gtk::Expander _caps_frame;
+ Gtk::Grid _caps_grid;
+ Gtk::RadioButton _caps_normal;
+ Gtk::RadioButton _caps_small;
+ Gtk::RadioButton _caps_all_small;
+ Gtk::RadioButton _caps_petite;
+ Gtk::RadioButton _caps_all_petite;
+ Gtk::RadioButton _caps_unicase;
+ Gtk::RadioButton _caps_titling;
+
+ // Numeric: Complicated!
+ Gtk::Expander _numeric_frame;
+ Gtk::Grid _numeric_grid;
+
+ Gtk::RadioButton _numeric_default_style;
+ Gtk::RadioButton _numeric_lining;
+ Gtk::Label _numeric_lining_label;
+ Gtk::RadioButton _numeric_old_style;
+ Gtk::Label _numeric_old_style_label;
+
+ Gtk::RadioButton _numeric_default_width;
+ Gtk::RadioButton _numeric_proportional;
+ Gtk::Label _numeric_proportional_label;
+ Gtk::RadioButton _numeric_tabular;
+ Gtk::Label _numeric_tabular_label;
+
+ Gtk::RadioButton _numeric_default_fractions;
+ Gtk::RadioButton _numeric_diagonal;
+ Gtk::Label _numeric_diagonal_label;
+ Gtk::RadioButton _numeric_stacked;
+ Gtk::Label _numeric_stacked_label;
+
+ Gtk::CheckButton _numeric_ordinal;
+ Gtk::Label _numeric_ordinal_label;
+
+ Gtk::CheckButton _numeric_slashed_zero;
+ Gtk::Label _numeric_slashed_zero_label;
+
+ // East Asian: Complicated!
+ Gtk::Expander _asian_frame;
+ Gtk::Grid _asian_grid;
+
+ Gtk::RadioButton _asian_default_variant;
+ Gtk::RadioButton _asian_jis78;
+ Gtk::RadioButton _asian_jis83;
+ Gtk::RadioButton _asian_jis90;
+ Gtk::RadioButton _asian_jis04;
+ Gtk::RadioButton _asian_simplified;
+ Gtk::RadioButton _asian_traditional;
+
+ Gtk::RadioButton _asian_default_width;
+ Gtk::RadioButton _asian_full_width;
+ Gtk::RadioButton _asian_proportional_width;
+
+ Gtk::CheckButton _asian_ruby;
+
+ // -----
+ Gtk::Expander _feature_frame;
+ Gtk::Grid _feature_grid;
+ Gtk::Box _feature_vbox;
+ Gtk::Entry _feature_entry;
+ Gtk::Label _feature_label;
+ Gtk::Label _feature_list;
+ Gtk::Label _feature_substitutions;
+
+private:
+ void ligatures_init();
+ void ligatures_callback();
+
+ void position_init();
+ void position_callback();
+
+ void caps_init();
+ void caps_callback();
+
+ void numeric_init();
+ void numeric_callback();
+
+ void asian_init();
+ void asian_callback();
+
+ void feature_init();
+public:
+ void feature_callback();
+
+private:
+ // To determine if we need to write out property (may not be necessary)
+ unsigned _ligatures_all;
+ unsigned _position_all;
+ unsigned _caps_all;
+ unsigned _numeric_all;
+ unsigned _asian_all;
+
+ unsigned _ligatures_mix;
+ unsigned _position_mix;
+ unsigned _caps_mix;
+ unsigned _numeric_mix;
+ unsigned _asian_mix;
+
+ bool _ligatures_changed;
+ bool _position_changed;
+ bool _caps_changed;
+ bool _numeric_changed;
+ bool _feature_changed;
+ bool _asian_changed;
+
+ std::map<std::string, Feature*> _features;
+
+ sigc::signal<void> _changed_signal;
+
+public:
+
+ /**
+ * Update GUI based on query results.
+ */
+ void update( SPStyle const *query, bool different_features, Glib::ustring& font_spec );
+
+ /**
+ * Update GUI based on OpenType features of selected font.
+ */
+ void update_opentype( Glib::ustring& font_spec );
+
+ /**
+ * Fill SPCSSAttr based on settings of buttons.
+ */
+ void fill_css( SPCSSAttr* css );
+
+ /**
+ * Get CSS string for markup.
+ */
+ Glib::ustring get_markup();
+
+ /**
+ * Let others know that user has changed GUI settings.
+ * (Used to enable 'Apply' and 'Default' buttons.)
+ */
+ sigc::connection connectChanged(sigc::slot<void> slot) {
+ return _changed_signal.connect(slot);
+ }
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_VARIANT_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/ui/widget/font-variations.cpp b/src/ui/widget/font-variations.cpp
new file mode 100644
index 0000000..66af8e7
--- /dev/null
+++ b/src/ui/widget/font-variations.cpp
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Felipe Corrêa da Silva Sanches <juca@members.fsf.org>
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Felipe Corrêa da Silva Sanches, Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream>
+#include <iomanip>
+
+#include <gtkmm.h>
+#include <glibmm/i18n.h>
+
+#include <libnrtype/font-instance.h>
+
+#include "font-variations.h"
+
+// For updating from selection
+#include "desktop.h"
+#include "object/sp-text.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontVariationAxis::FontVariationAxis (Glib::ustring name, OTVarAxis& axis)
+ : name (name)
+{
+
+ // std::cout << "FontVariationAxis::FontVariationAxis:: "
+ // << " name: " << name
+ // << " min: " << axis.minimum
+ // << " def: " << axis.def
+ // << " max: " << axis.maximum
+ // << " val: " << axis.set_val << std::endl;
+
+ label = Gtk::manage( new Gtk::Label( name ) );
+ add( *label );
+
+ precision = 2 - int( log10(axis.maximum - axis.minimum));
+ if (precision < 0) precision = 0;
+
+ scale = Gtk::manage( new Gtk::Scale() );
+ scale->set_range (axis.minimum, axis.maximum);
+ scale->set_value (axis.set_val);
+ scale->set_digits (precision);
+ scale->set_hexpand(true);
+ add( *scale );
+
+ def = axis.def; // Default value
+}
+
+
+// ------------------------------------------------------------- //
+
+FontVariations::FontVariations () :
+ Gtk::Grid ()
+{
+ // std::cout << "FontVariations::FontVariations" << std::endl;
+ set_orientation( Gtk::ORIENTATION_VERTICAL );
+ set_name ("FontVariations");
+ size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+ show_all_children();
+}
+
+
+// Update GUI based on query.
+void
+FontVariations::update (const Glib::ustring& font_spec) {
+
+ font_instance* res = font_factory::Default()->FaceFromFontSpecification (font_spec.c_str());
+
+ auto children = get_children();
+ for (auto child: children) {
+ remove ( *child );
+ }
+ axes.clear();
+
+ for (auto a: res->openTypeVarAxes) {
+ // std::cout << "Creating axis: " << a.first << std::endl;
+ FontVariationAxis* axis = Gtk::manage( new FontVariationAxis( a.first, a.second ));
+ axes.push_back( axis );
+ add( *axis );
+ size_group->add_widget( *(axis->get_label()) ); // Keep labels the same width
+ axis->get_scale()->signal_value_changed().connect(
+ sigc::mem_fun(*this, &FontVariations::on_variations_change)
+ );
+ }
+
+ show_all_children();
+}
+
+void
+FontVariations::fill_css( SPCSSAttr *css ) {
+
+ // Eventually will want to favor using 'font-weight', etc. but at the moment these
+ // can't handle "fractional" values. See CSS Fonts Module Level 4.
+ sp_repr_css_set_property(css, "font-variation-settings", get_css_string().c_str());
+}
+
+Glib::ustring
+FontVariations::get_css_string() {
+
+ Glib::ustring css_string;
+
+ for (auto axis: axes) {
+ Glib::ustring name = axis->get_name();
+
+ // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.)
+ if (name == "Width") name = "wdth"; // 'font-stretch'
+ if (name == "Weight") name = "wght"; // 'font-weight'
+ if (name == "OpticalSize") name = "opsz"; // 'font-optical-sizing' Can trigger glyph substitution.
+ if (name == "Slant") name = "slnt"; // 'font-style'
+ if (name == "Italic") name = "ital"; // 'font-style' Toggles from Roman to Italic.
+
+ std::stringstream value;
+ value << std::fixed << std::setprecision(axis->get_precision()) << axis->get_value();
+ css_string += "'" + name + "' " + value.str() + "', ";
+ }
+
+ return css_string;
+}
+
+Glib::ustring
+FontVariations::get_pango_string() {
+
+ Glib::ustring pango_string;
+
+ if (!axes.empty()) {
+
+ pango_string += "@";
+
+ for (auto axis: axes) {
+ if (axis->get_value() == axis->get_def()) continue;
+ Glib::ustring name = axis->get_name();
+
+ // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.)
+ if (name == "Width") name = "wdth"; // 'font-stretch'
+ if (name == "Weight") name = "wght"; // 'font-weight'
+ if (name == "OpticalSize") name = "opsz"; // 'font-optical-sizing' Can trigger glyph substitution.
+ if (name == "Slant") name = "slnt"; // 'font-style'
+ if (name == "Italic") name = "ital"; // 'font-style' Toggles from Roman to Italic.
+
+ std::stringstream value;
+ value << std::fixed << std::setprecision(axis->get_precision()) << axis->get_value();
+ pango_string += name + "=" + value.str() + ",";
+ }
+
+ pango_string.erase (pango_string.size() - 1); // Erase last ',' or '@'
+ }
+
+ return pango_string;
+}
+
+void
+FontVariations::on_variations_change() {
+ // std::cout << "FontVariations::on_variations_change: " << get_css_string() << std::endl;;
+ signal_changed.emit ();
+}
+
+bool FontVariations::variations_present() const {
+ return !axes.empty();
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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 :
diff --git a/src/ui/widget/font-variations.h b/src/ui/widget/font-variations.h
new file mode 100644
index 0000000..e3c09aa
--- /dev/null
+++ b/src/ui/widget/font-variations.h
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Felipe Corrêa da Silva Sanches <juca@members.fsf.org>
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2018 Felipe Corrêa da Silva Sanches, Tavmong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H
+#define INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H
+
+#include <gtkmm/grid.h>
+#include <gtkmm/sizegroup.h>
+#include <gtkmm/label.h>
+#include <gtkmm/scale.h>
+
+#include "libnrtype/OpenTypeUtil.h"
+
+#include "style.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+/**
+ * A widget for a single axis: Label and Slider
+ */
+class FontVariationAxis : public Gtk::Grid
+{
+public:
+ FontVariationAxis(Glib::ustring name, OTVarAxis& axis);
+ Glib::ustring get_name() { return name; }
+ Gtk::Label* get_label() { return label; }
+ double get_value() { return scale->get_value(); }
+ int get_precision() { return precision; }
+ Gtk::Scale* get_scale() { return scale; }
+ double get_def() { return def; }
+
+private:
+
+ // Widgets
+ Glib::ustring name;
+ Gtk::Label* label;
+ Gtk::Scale* scale;
+
+ int precision;
+ double def = 0.0; // Default value
+
+ // Signals
+ sigc::signal<void> signal_changed;
+};
+
+/**
+ * A widget for selecting font variations (OpenType Variations).
+ */
+class FontVariations : public Gtk::Grid
+{
+
+public:
+
+ /**
+ * Constructor
+ */
+ FontVariations();
+
+protected:
+
+public:
+
+ /**
+ * Update GUI.
+ */
+ void update(const Glib::ustring& font_spec);
+
+ /**
+ * Fill SPCSSAttr based on settings of buttons.
+ */
+ void fill_css( SPCSSAttr* css );
+
+ /**
+ * Get CSS String
+ */
+ Glib::ustring get_css_string();
+
+ Glib::ustring get_pango_string();
+
+ void on_variations_change();
+
+ /**
+ * Let others know that user has changed GUI settings.
+ * (Used to enable 'Apply' and 'Default' buttons.)
+ */
+ sigc::connection connectChanged(sigc::slot<void> slot) {
+ return signal_changed.connect(slot);
+ }
+
+ // return true if there are some variations present
+ bool variations_present() const;
+
+private:
+
+ std::vector<FontVariationAxis*> axes;
+ Glib::RefPtr<Gtk::SizeGroup> size_group;
+
+ sigc::signal<void> signal_changed;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_VARIATIONS_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/ui/widget/frame.cpp b/src/ui/widget/frame.cpp
new file mode 100644
index 0000000..eac4e22
--- /dev/null
+++ b/src/ui/widget/frame.cpp
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Murray C
+ *
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "frame.h"
+
+
+// Inkscape::UI::Widget::Frame
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Frame::Frame(Glib::ustring const &label_text /*= ""*/, gboolean label_bold /*= TRUE*/ )
+ : _label(label_text, Gtk::ALIGN_END, Gtk::ALIGN_CENTER, true)
+{
+ set_shadow_type(Gtk::SHADOW_NONE);
+
+ set_label_widget(_label);
+ set_label(label_text, label_bold);
+}
+
+void
+Frame::add(Widget& widget)
+{
+ Gtk::Frame::add(widget);
+ set_padding(4, 0, 8, 0);
+ show_all_children();
+}
+
+void
+Frame::set_label(const Glib::ustring &label_text, gboolean label_bold /*= TRUE*/)
+{
+ if (label_bold) {
+ _label.set_markup(Glib::ustring("<b>") + label_text + "</b>");
+ } else {
+ _label.set_text(label_text);
+ }
+}
+
+void
+Frame::set_padding (guint padding_top, guint padding_bottom, guint padding_left, guint padding_right)
+{
+ auto child = get_child();
+
+ if(child)
+ {
+ child->set_margin_top(padding_top);
+ child->set_margin_bottom(padding_bottom);
+ child->set_margin_start(padding_left);
+ child->set_margin_end(padding_right);
+ }
+}
+
+Gtk::Label const *
+Frame::get_label_widget() const
+{
+ return &_label;
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/frame.h b/src/ui/widget/frame.h
new file mode 100644
index 0000000..b2934b6
--- /dev/null
+++ b/src/ui/widget/frame.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Murray C
+ *
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FRAME_H
+#define INKSCAPE_UI_WIDGET_FRAME_H
+
+#include <gtkmm/frame.h>
+#include <gtkmm/label.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Creates a Gnome HIG style indented frame with bold label
+ * See http://developer.gnome.org/hig-book/stable/controls-frames.html.en
+ */
+class Frame : public Gtk::Frame
+{
+public:
+
+ /**
+ * Construct a Frame Widget.
+ *
+ * @param label The frame text.
+ */
+ Frame(Glib::ustring const &label = "", gboolean label_bold = TRUE);
+
+ /**
+ * Return the label widget
+ */
+ Gtk::Label const *get_label_widget() const;
+
+ /**
+ * Add a widget to this frame
+ */
+ void add(Widget& widget) override;
+
+ /**
+ * Set the frame label text and if bold or not
+ */
+ void set_label(const Glib::ustring &label, gboolean label_bold = TRUE);
+
+ /**
+ * Set the frame padding
+ */
+ void set_padding (guint padding_top, guint padding_bottom, guint padding_left, guint padding_right);
+
+protected:
+ Gtk::Label _label;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FRAME_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/ui/widget/framecheck.cpp b/src/ui/widget/framecheck.cpp
new file mode 100644
index 0000000..27b3d5b
--- /dev/null
+++ b/src/ui/widget/framecheck.cpp
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <fstream>
+#include <iostream>
+#include <boost/filesystem.hpp> // Using boost::filesystem instead of std::filesystem due to broken C++17 on MacOS.
+#include "framecheck.h"
+namespace fs = boost::filesystem;
+
+namespace Inkscape {
+namespace FrameCheck {
+
+std::ostream &logfile()
+{
+ static std::ofstream f;
+
+ if (!f.is_open()) {
+ try {
+ auto path = fs::temp_directory_path() / "framecheck.txt";
+ auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary;
+ f.open(path.string(), mode);
+ } catch (...) {
+ std::cerr << "failed to create framecheck logfile" << std::endl;
+ }
+ }
+
+ return f;
+}
+
+} // namespace FrameCheck
+} // namespace Inkscape
diff --git a/src/ui/widget/framecheck.h b/src/ui/widget/framecheck.h
new file mode 100644
index 0000000..36eeea1
--- /dev/null
+++ b/src/ui/widget/framecheck.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Functions for logging timing events.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef FRAMECHECK_H
+#define FRAMECHECK_H
+
+#include <ostream>
+#include <glib.h>
+
+namespace Inkscape {
+namespace FrameCheck {
+
+extern std::ostream &logfile();
+
+// RAII object that logs a timing event for the duration of its lifetime.
+struct Event
+{
+ gint64 start;
+ const char *name;
+ int subtype;
+
+ Event() : start(-1) {}
+
+ Event(const char *name, int subtype = 0) : start(g_get_monotonic_time()), name(name), subtype(subtype) {}
+
+ Event(Event &&p)
+ {
+ movefrom(p);
+ }
+
+ ~Event()
+ {
+ finish();
+ }
+
+ Event &operator=(Event &&p)
+ {
+ finish();
+ movefrom(p);
+ return *this;
+ }
+
+ void movefrom(Event &p)
+ {
+ start = p.start;
+ name = p.name;
+ subtype = p.subtype;
+ p.start = -1;
+ }
+
+ void finish()
+ {
+ if (start != -1) {
+ logfile() << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << '\n';
+ }
+ }
+};
+
+} // namespace FrameCheck
+} // namespace Inkscape
+
+#endif // FRAMECHECK_H
diff --git a/src/ui/widget/gradient-editor.cpp b/src/ui/widget/gradient-editor.cpp
new file mode 100644
index 0000000..df1b51b
--- /dev/null
+++ b/src/ui/widget/gradient-editor.cpp
@@ -0,0 +1,653 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Gradient editor widget for "Fill and Stroke" dialog
+ *
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2020-2021 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "gradient-editor.h"
+
+#include <gtkmm/builder.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/button.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treemodelcolumn.h>
+#include <glibmm/i18n.h>
+#include <cairo.h>
+
+#include "document-undo.h"
+#include "gradient-chemistry.h"
+#include "gradient-selector.h"
+#include "preferences.h"
+
+#include "display/cairo-utils.h"
+
+#include "io/resource.h"
+
+#include "object/sp-gradient-vector.h"
+#include "object/sp-linear-gradient.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/color-preview.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+using namespace Inkscape::IO;
+using Inkscape::UI::Widget::ColorNotebook;
+
+class scope {
+public:
+ scope(bool& flag): _flag(flag) {
+ flag = true;
+ }
+
+ ~scope() {
+ _flag = false;
+ }
+
+private:
+ bool& _flag;
+};
+
+void set_icon(Gtk::Button& btn, gchar const* pixmap) {
+ if (Gtk::Image* img = sp_get_icon_image(pixmap, Gtk::ICON_SIZE_BUTTON)) {
+ btn.set_image(*img);
+ }
+}
+
+// draw solid color circle with black outline; right side is to show checkerboard if color's alpha is > 0
+Glib::RefPtr<Gdk::Pixbuf> draw_circle(int size, guint32 rgba) {
+ int width = size;
+ int height = size;
+ gint w2 = width / 2;
+
+ cairo_surface_t* s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t* cr = cairo_create(s);
+
+ int x = 0, y = 0;
+ double radius = size / 2;
+ double degrees = M_PI / 180.0;
+ cairo_new_sub_path(cr);
+ cairo_arc(cr, x + radius, y + radius, radius, 0, 2 * M_PI);
+ cairo_close_path(cr);
+ // semi-transparent black outline
+ cairo_set_source_rgba(cr, 0, 0, 0, 0.2);
+ cairo_fill(cr);
+
+ radius--;
+
+ cairo_new_sub_path(cr);
+ cairo_line_to(cr, x + w2, 0);
+ cairo_line_to(cr, x + w2, height);
+ cairo_arc(cr, x + w2, y + w2, radius, 90 * degrees, 270 * degrees);
+ cairo_close_path(cr);
+
+ // solid part
+ ink_cairo_set_source_rgba32(cr, rgba | 0xff);
+ cairo_fill(cr);
+
+ x = w2;
+
+ cairo_new_sub_path(cr);
+ cairo_arc(cr, x, y + w2, radius, -90 * degrees, 90 * degrees);
+ cairo_line_to(cr, x, y);
+ cairo_close_path(cr);
+
+ // (semi)transparent part
+ if ((rgba & 0xff) != 0xff) {
+ cairo_pattern_t* checkers = ink_cairo_pattern_create_checkerboard();
+ cairo_set_source(cr, checkers);
+ cairo_fill_preserve(cr);
+ cairo_pattern_destroy(checkers);
+ }
+ ink_cairo_set_source_rgba32(cr, rgba);
+ cairo_fill(cr);
+
+ cairo_destroy(cr);
+ cairo_surface_flush(s);
+
+ GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s);
+ return Glib::wrap(pixbuf);
+}
+
+
+Glib::RefPtr<Gdk::Pixbuf> get_stop_pixmap(SPStop* stop) {
+ const int size = 30;
+ return draw_circle(size, stop->getColor().toRGBA32(stop->getOpacity()));
+}
+
+// get widget from builder or throw
+template<class W> W& get_widget(Glib::RefPtr<Gtk::Builder>& builder, const char* id) {
+ W* widget;
+ builder->get_widget(id, widget);
+ if (!widget) {
+ throw std::runtime_error("Missing widget in a glade resource file");
+ }
+ return *widget;
+}
+
+Glib::RefPtr<Gtk::Builder> create_builder() {
+ auto glade = Resource::get_filename(Resource::UIS, "gradient-edit.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ return Gtk::Builder::create_from_file(glade);
+ }
+ catch (Glib::Error& ex) {
+ g_error("Cannot load glade file for gradient editor: %s", + ex.what().c_str());
+ throw;
+ }
+}
+
+Glib::ustring get_repeat_icon(SPGradientSpread mode) {
+ const char* ico = "";
+ switch (mode) {
+ case SP_GRADIENT_SPREAD_PAD:
+ ico = "gradient-spread-pad";
+ break;
+ case SP_GRADIENT_SPREAD_REPEAT:
+ ico = "gradient-spread-repeat";
+ break;
+ case SP_GRADIENT_SPREAD_REFLECT:
+ ico = "gradient-spread-reflect";
+ break;
+ default:
+ g_warning("Missing case in %s\n", __func__);
+ break;
+ }
+ return ico;
+}
+
+GradientEditor::GradientEditor(const char* prefs) :
+ _builder(create_builder()),
+ _selector(Gtk::manage(new GradientSelector())),
+ _repeat_icon(get_widget<Gtk::Image>(_builder, "repeatIco")),
+ _popover(get_widget<Gtk::Popover>(_builder, "libraryPopover")),
+ _stop_tree(get_widget<Gtk::TreeView>(_builder, "stopList")),
+ _offset_btn(get_widget<Gtk::SpinButton>(_builder, "offsetSpin")),
+ _show_stops_list(get_widget<Gtk::Expander>(_builder, "stopsBtn")),
+ _add_stop(get_widget<Gtk::Button>(_builder, "stopAdd")),
+ _delete_stop(get_widget<Gtk::Button>(_builder, "stopDelete")),
+ _stops_gallery(get_widget<Gtk::Box>(_builder, "stopsGallery")),
+ _colors_box(get_widget<Gtk::Box>(_builder, "colorsBox")),
+ _linear_btn(get_widget<Gtk::ToggleButton>(_builder, "linearBtn")),
+ _radial_btn(get_widget<Gtk::ToggleButton>(_builder, "radialBtn")),
+ _main_grid(get_widget<Gtk::Grid>(_builder, "mainGrid")),
+ _prefs(prefs)
+{
+ // gradient type buttons; not currently used, hidden, WIP
+ set_icon(_linear_btn, INKSCAPE_ICON("paint-gradient-linear"));
+ set_icon(_radial_btn, INKSCAPE_ICON("paint-gradient-radial"));
+
+ auto& reverse = get_widget<Gtk::Button>(_builder, "reverseBtn");
+ set_icon(reverse, INKSCAPE_ICON("object-flip-horizontal"));
+ reverse.signal_clicked().connect([=](){ reverse_gradient(); });
+
+ auto& gradBox = get_widget<Gtk::Box>(_builder, "gradientBox");
+ const int dot_size = 8;
+ _gradient_image.show();
+ _gradient_image.set_margin_start(dot_size / 2);
+ _gradient_image.set_margin_end(dot_size / 2);
+ // gradient stop selected in a gradient widget; sync list selection
+ _gradient_image.signal_stop_selected().connect([=](size_t index) {
+ select_stop(index);
+ fire_stop_selected(get_current_stop());
+ });
+ _gradient_image.signal_stop_offset_changed().connect([=](size_t index, double offset) {
+ set_stop_offset(index, offset);
+ });
+ _gradient_image.signal_add_stop_at().connect([=](double offset) {
+ insert_stop_at(offset);
+ });
+ _gradient_image.signal_delete_stop().connect([=](size_t index) {
+ delete_stop(index);
+ });
+
+ gradBox.pack_start(_gradient_image, true, true, 0);
+
+ // add color selector
+ auto color_selector = Gtk::manage(new ColorNotebook(_selected_color));
+ color_selector->set_label(_("Stop color"));
+ color_selector->show();
+ _colors_box.pack_start(*color_selector, true, true, 0);
+
+ // gradient library in a popup
+ _popover.add(*_selector);
+ const int h = 5;
+ const int v = 3;
+ _selector->set_margin_start(h);
+ _selector->set_margin_end(h);
+ _selector->set_margin_top(v);
+ _selector->set_margin_bottom(v);
+ _selector->show();
+ _selector->show_edit_button(false);
+ _selector->set_gradient_size(160, 20);
+ _selector->set_name_col_size(120);
+ // gradient changed is currently the only signal that GradientSelector can emit:
+ _selector->signal_changed().connect([=](SPGradient* gradient) {
+ // new gradient selected from the library
+ _signal_changed.emit(gradient);
+ });
+
+ // construct store for a list of stops
+ _stop_columns.add(_stopObj);
+ _stop_columns.add(_stopIdx);
+ _stop_columns.add(_stopID);
+ _stop_columns.add(_stop_color);
+ _stop_list_store = Gtk::ListStore::create(_stop_columns);
+ _stop_tree.set_model(_stop_list_store);
+ // indices in the stop list view; currently hidden
+ // _stop_tree.append_column("n", _stopID); // 1-based stop index
+ _stop_tree.append_column("c", _stop_color); // and its color
+
+ auto selection = _stop_tree.get_selection();
+ selection->signal_changed().connect([=]() {
+ if (!_update.pending()) {
+ stop_selected();
+ fire_stop_selected(get_current_stop());
+ }
+ });
+
+ _show_stops_list.property_expanded().signal_changed().connect(
+ [&](){ show_stops(_show_stops_list.get_expanded()); }
+ );
+
+ set_icon(_add_stop, "list-add");
+ _add_stop.signal_clicked().connect([=](){
+ if (auto row = current_stop()) {
+ auto index = row->get_value(_stopIdx);
+ add_stop(static_cast<int>(index));
+ }
+ });
+
+ set_icon(_delete_stop, "list-remove");
+ _delete_stop.signal_clicked().connect([=]() {
+ if (auto row = current_stop()) {
+ auto index = row->get_value(_stopIdx);
+ delete_stop(static_cast<int>(index));
+ }
+ });
+
+ // connect gradient repeat modes menu
+ std::tuple<const char*, SPGradientSpread> repeats[3] = {
+ {"repeatNone", SP_GRADIENT_SPREAD_PAD},
+ {"repeatDirect", SP_GRADIENT_SPREAD_REPEAT},
+ {"repeatReflected", SP_GRADIENT_SPREAD_REFLECT}
+ };
+ for (auto& el : repeats) {
+ auto& item = get_widget<Gtk::MenuItem>(_builder, std::get<0>(el));
+ auto mode = std::get<1>(el);
+ item.signal_activate().connect([=](){ set_repeat_mode(mode); });
+ // pack icon and text into MenuItem, since MenuImageItem is deprecated
+ auto text = item.get_label();
+ auto hbox = Gtk::manage(new Gtk::Box);
+ Gtk::Image* img = sp_get_icon_image(get_repeat_icon(mode), Gtk::ICON_SIZE_BUTTON);
+ hbox->pack_start(*img, false, true, 8);
+ auto label = Gtk::manage(new Gtk::Label);
+ label->set_label(text);
+ hbox->pack_start(*label, false, true, 8);
+ hbox->show_all();
+ item.remove();
+ item.add(*hbox);
+ }
+
+ set_repeat_icon(SP_GRADIENT_SPREAD_PAD);
+
+ _selected_color.signal_changed.connect([=]() {
+ set_stop_color(_selected_color.color(), _selected_color.alpha());
+ });
+ _selected_color.signal_dragged.connect([=]() {
+ set_stop_color(_selected_color.color(), _selected_color.alpha());
+ });
+
+ _offset_btn.signal_changed().connect([=]() {
+ if (auto row = current_stop()) {
+ auto index = row->get_value(_stopIdx);
+ double offset = _offset_btn.get_value();
+ set_stop_offset(index, offset);
+ }
+ });
+
+ pack_start(_main_grid);
+
+ // restore visibility of the stop list view
+ _stops_list_visible = Inkscape::Preferences::get()->getBool(_prefs + "/stoplist", true);
+ _show_stops_list.set_expanded(_stops_list_visible);
+ update_stops_layout();
+}
+
+GradientEditor::~GradientEditor() noexcept {
+}
+
+void GradientEditor::set_stop_color(SPColor color, float opacity) {
+ if (_update.pending()) return;
+
+ SPGradient* vector = get_gradient_vector();
+ if (!vector) return;
+
+ if (auto row = current_stop()) {
+ auto index = row->get_value(_stopIdx);
+ SPStop* stop = sp_get_nth_stop(vector, index);
+ if (stop && _document) {
+ auto scoped(_update.block());
+
+ // update list view too
+ row->set_value(_stop_color, get_stop_pixmap(stop));
+
+ sp_set_gradient_stop_color(_document, stop, color, opacity);
+ }
+ }
+}
+
+std::optional<Gtk::TreeRow> GradientEditor::current_stop() {
+ auto sel = _stop_tree.get_selection();
+ auto it = sel->get_selected();
+ if (!it) {
+ return std::nullopt;
+ }
+ else {
+ return *it;
+ }
+}
+
+SPStop* GradientEditor::get_nth_stop(size_t index) {
+ if (SPGradient* vector = get_gradient_vector()) {
+ return sp_get_nth_stop(vector, index);
+ }
+ return nullptr;
+}
+
+// stop has been selected in a list view
+void GradientEditor::stop_selected() {
+ if (auto row = current_stop()) {
+ SPStop* stop = row->get_value(_stopObj);
+ if (stop) {
+ auto scoped(_update.block());
+
+ _selected_color.setColor(stop->getColor());
+ _selected_color.setAlpha(stop->getOpacity());
+
+ auto stops = sp_get_before_after_stops(stop);
+ if (stops.first && stops.second) {
+ _offset_btn.set_range(stops.first->offset, stops.second->offset);
+ }
+ else {
+ _offset_btn.set_range(stops.first ? stops.first->offset : 0, stops.second ? stops.second->offset : 1);
+ }
+ _offset_btn.set_sensitive();
+ _offset_btn.set_value(stop->offset);
+
+ int index = row->get_value(_stopIdx);
+ _gradient_image.set_focused_stop(index);
+ }
+ }
+ else {
+ // no selection
+ auto scoped(_update.block());
+
+ _selected_color.setColor(SPColor());
+
+ _offset_btn.set_range(0, 0);
+ _offset_btn.set_value(0);
+ _offset_btn.set_sensitive(false);
+ }
+}
+
+void GradientEditor::insert_stop_at(double offset) {
+ if (SPGradient* vector = get_gradient_vector()) {
+ // only insert new stop if there are some stops present
+ if (vector->hasStops()) {
+ SPStop* stop = sp_gradient_add_stop_at(vector, offset);
+ // just select next stop; newly added stop will be in a list view after selection refresh (on idle)
+ auto pos = sp_number_of_stops_before_stop(vector, stop);
+ auto selected = select_stop(pos);
+ fire_stop_selected(stop);
+ if (!selected) {
+ select_stop(pos);
+ }
+ }
+ }
+}
+
+void GradientEditor::add_stop(int index) {
+ if (SPGradient* vector = get_gradient_vector()) {
+ if (SPStop* current = sp_get_nth_stop(vector, index)) {
+ SPStop* stop = sp_gradient_add_stop(vector, current);
+ // just select next stop; newly added stop will be in a list view after selection refresh (on idle)
+ select_stop(sp_number_of_stops_before_stop(vector, stop));
+ fire_stop_selected(stop);
+ }
+ }
+}
+
+void GradientEditor::delete_stop(int index) {
+ if (SPGradient* vector = get_gradient_vector()) {
+ if (SPStop* stop = sp_get_nth_stop(vector, index)) {
+ // try deleting a stop, if it can be
+ sp_gradient_delete_stop(vector, stop);
+ }
+ }
+}
+
+// collapse/expand list of stops in the UI
+void GradientEditor::show_stops(bool visible) {
+ _stops_list_visible = visible;
+ update_stops_layout();
+ Inkscape::Preferences::get()->setBool(_prefs + "/stoplist", _stops_list_visible);
+}
+
+void GradientEditor::update_stops_layout() {
+ if (_stops_list_visible) {
+ _stops_gallery.show();
+ }
+ else {
+ _stops_gallery.hide();
+ }
+}
+
+void GradientEditor::reverse_gradient() {
+ if (_document && _gradient) {
+ // reverse works on a gradient definition, the one with stops:
+ SPGradient* vector = get_gradient_vector();
+
+ if (vector) {
+ sp_gradient_reverse_vector(vector);
+ DocumentUndo::done(_document, _("Reverse gradient"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+}
+
+void GradientEditor::set_repeat_mode(SPGradientSpread mode) {
+ if (_update.pending()) return;
+
+ if (_document && _gradient) {
+ auto scoped(_update.block());
+
+ // spread is set on a gradient reference, which is _gradient object
+ _gradient->setSpread(mode);
+ _gradient->updateRepr();
+
+ DocumentUndo::done(_document, _("Set gradient repeat"), INKSCAPE_ICON("color-gradient"));
+
+ set_repeat_icon(mode);
+ }
+}
+
+void GradientEditor::set_repeat_icon(SPGradientSpread mode) {
+ auto ico = get_repeat_icon(mode);
+ if (!ico.empty()) {
+ _repeat_icon.set_from_icon_name(ico, Gtk::ICON_SIZE_BUTTON);
+ }
+}
+
+void GradientEditor::setGradient(SPGradient* gradient) {
+ auto scoped(_update.block());
+ auto scoped2(_notification.block());
+ _gradient = gradient;
+ _document = gradient ? gradient->document : nullptr;
+ set_gradient(gradient);
+}
+
+SPGradient* GradientEditor::getVector() {
+ return _selector->getVector();
+}
+
+void GradientEditor::setVector(SPDocument* doc, SPGradient* vector) {
+ auto scoped(_update.block());
+ _selector->setVector(doc, vector);
+}
+
+void GradientEditor::setMode(SelectorMode mode) {
+ _selector->setMode(mode);
+}
+
+void GradientEditor::setUnits(SPGradientUnits units) {
+ _selector->setUnits(units);
+}
+
+SPGradientUnits GradientEditor::getUnits() {
+ return _selector->getUnits();
+}
+
+void GradientEditor::setSpread(SPGradientSpread spread) {
+ _selector->setSpread(spread);
+}
+
+SPGradientSpread GradientEditor::getSpread() {
+ return _selector->getSpread();
+}
+
+void GradientEditor::selectStop(SPStop* selected) {
+ if (_notification.pending()) return;
+
+ auto scoped(_notification.block());
+ // request from the outside to sync stop selection
+ const auto& items = _stop_tree.get_model()->children();
+ auto it = std::find_if(items.begin(), items.end(), [=](const auto& row) {
+ SPStop* stop = row->get_value(_stopObj);
+ return stop == selected;
+ });
+ if (it != items.end()) {
+ select_stop(std::distance(items.begin(), it));
+ }
+}
+
+SPGradient* GradientEditor::get_gradient_vector() {
+ if (!_gradient) return nullptr;
+ return sp_gradient_get_forked_vector_if_necessary(_gradient, false);
+}
+
+void GradientEditor::set_gradient(SPGradient* gradient) {
+ auto scoped(_update.block());
+
+ // remember which stop is selected, so we can restore it
+ size_t selected_stop_index = 0;
+ if (auto it = _stop_tree.get_selection()->get_selected()) {
+ selected_stop_index = it->get_value(_stopIdx);
+ }
+
+ _stop_list_store->clear();
+
+ SPGradient* vector = gradient ? gradient->getVector() : nullptr;
+
+ if (vector) {
+ vector->ensureVector();
+ }
+
+ _gradient_image.set_gradient(vector);
+
+ if (!vector || !vector->hasStops()) return;
+
+ size_t index = 0;
+ for (auto& child : vector->children) {
+ if (SP_IS_STOP(&child)) {
+ auto stop = SP_STOP(&child);
+ auto it = _stop_list_store->append();
+ it->set_value(_stopObj, stop);
+ it->set_value(_stopIdx, index);
+ it->set_value(_stopID, Glib::ustring::compose("%1.", index + 1));
+ it->set_value(_stop_color, get_stop_pixmap(stop));
+
+ ++index;
+ }
+ }
+
+ auto mode = gradient->isSpreadSet() ? gradient->getSpread() : SP_GRADIENT_SPREAD_PAD;
+ set_repeat_icon(mode);
+
+ // list not empty?
+ if (index > 0) {
+ select_stop(std::min(selected_stop_index, index - 1));
+ // update related widgets
+ stop_selected();
+ //
+ // emit_stop_selected(get_current_stop());
+ }
+}
+
+void GradientEditor::set_stop_offset(size_t index, double offset) {
+ if (_update.pending()) return;
+
+ // adjust stop's offset after user edits it in offset spin button or drags stop handle
+ SPStop* stop = get_nth_stop(index);
+ if (stop) {
+ auto scoped(_update.block());
+
+ stop->offset = offset;
+ if (auto repr = stop->getRepr()) {
+ repr->setAttributeCssDouble("offset", stop->offset);
+ }
+
+ DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", _("Change gradient stop offset"), INKSCAPE_ICON("color-gradient"));
+ }
+}
+
+// select requested stop in a list view
+bool GradientEditor::select_stop(size_t index) {
+ if (!_gradient) return false;
+
+ bool selected = false;
+ const auto& items = _stop_tree.get_model()->children();
+ if (index < items.size()) {
+ auto it = items.begin();
+ std::advance(it, index);
+ auto path = _stop_tree.get_model()->get_path(it);
+ _stop_tree.get_selection()->select(it);
+ _stop_tree.scroll_to_cell(path, *_stop_tree.get_column(0));
+ selected = true;
+ }
+
+ return selected;
+}
+
+SPStop* GradientEditor::get_current_stop() {
+ if (auto row = current_stop()) {
+ SPStop* stop = row->get_value(_stopObj);
+ return stop;
+ }
+ return nullptr;
+}
+
+void GradientEditor::fire_stop_selected(SPStop* stop) {
+ if (!_notification.pending()) {
+ auto scoped(_notification.block());
+ emit_stop_selected(stop);
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/gradient-editor.h b/src/ui/widget/gradient-editor.h
new file mode 100644
index 0000000..6b62164
--- /dev/null
+++ b/src/ui/widget/gradient-editor.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_EDITOR_H
+#define SEEN_GRADIENT_EDITOR_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/button.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/image.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treemodelcolumn.h>
+#include <gtkmm/builder.h>
+#include <optional>
+
+#include "object/sp-gradient.h"
+#include "object/sp-stop.h"
+#include "ui/selected-color.h"
+#include "spin-scale.h"
+#include "gradient-with-stops.h"
+#include "gradient-selector-interface.h"
+#include "ui/operation-blocker.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class GradientSelector;
+
+class GradientEditor : public Gtk::Box, public GradientSelectorInterface {
+public:
+ GradientEditor(const char* prefs);
+ ~GradientEditor() noexcept override;
+
+private:
+ sigc::signal<void> _signal_grabbed;
+ sigc::signal<void> _signal_dragged;
+ sigc::signal<void> _signal_released;
+ sigc::signal<void, SPGradient*> _signal_changed;
+
+public:
+ decltype(_signal_changed) signal_changed() const { return _signal_changed; }
+ decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; }
+ decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; }
+ decltype(_signal_released) signal_released() const { return _signal_released; }
+
+ void setGradient(SPGradient* gradient) override;
+ SPGradient* getVector() override;
+ void setVector(SPDocument* doc, SPGradient* vector) override;
+ void setMode(SelectorMode mode) override;
+ void setUnits(SPGradientUnits units) override;
+ SPGradientUnits getUnits() override;
+ void setSpread(SPGradientSpread spread) override;
+ SPGradientSpread getSpread() override;
+ void selectStop(SPStop* selected) override;
+
+private:
+ void set_gradient(SPGradient* gradient);
+ void stop_selected();
+ void insert_stop_at(double offset);
+ void add_stop(int index);
+ void duplicate_stop();
+ void delete_stop(int index);
+ void show_stops(bool visible);
+ void update_stops_layout();
+ void set_repeat_mode(SPGradientSpread mode);
+ void set_repeat_icon(SPGradientSpread mode);
+ void reverse_gradient();
+ void set_stop_color(SPColor color, float opacity);
+ std::optional<Gtk::TreeRow> current_stop();
+ SPStop* get_nth_stop(size_t index);
+ SPStop* get_current_stop();
+ bool select_stop(size_t index);
+ void set_stop_offset(size_t index, double offset);
+ SPGradient* get_gradient_vector();
+ void fire_stop_selected(SPStop* stop);
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ GradientSelector* _selector;
+ Inkscape::UI::SelectedColor _selected_color;
+ Gtk::Popover& _popover;
+ Gtk::Image& _repeat_icon;
+ GradientWithStops _gradient_image;
+ Glib::RefPtr<Gtk::ListStore> _stop_list_store;
+ Gtk::TreeModelColumnRecord _stop_columns;
+ Gtk::TreeModelColumn<SPStop*> _stopObj;
+ Gtk::TreeModelColumn<size_t> _stopIdx;
+ Gtk::TreeModelColumn<Glib::ustring> _stopID;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> _stop_color;
+ Gtk::TreeView& _stop_tree;
+ Gtk::SpinButton& _offset_btn;
+ Gtk::Button& _add_stop;
+ Gtk::Button& _delete_stop;
+ Gtk::Expander& _show_stops_list;
+ bool _stops_list_visible = true;
+ Gtk::Box& _stops_gallery;
+ Gtk::Box& _colors_box;
+ Gtk::ToggleButton& _linear_btn;
+ Gtk::ToggleButton& _radial_btn;
+ Gtk::Grid& _main_grid;
+ SPGradient* _gradient = nullptr;
+ SPDocument* _document = nullptr;
+ OperationBlocker _update;
+ OperationBlocker _notification;
+ Glib::ustring _prefs;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
diff --git a/src/ui/widget/gradient-image.cpp b/src/ui/widget/gradient-image.cpp
new file mode 100644
index 0000000..662082b
--- /dev/null
+++ b/src/ui/widget/gradient-image.cpp
@@ -0,0 +1,247 @@
+// 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"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+GradientImage::GradientImage(SPGradient *gradient)
+ : _gradient(nullptr)
+{
+ set_has_window(false);
+ set_gradient(gradient);
+}
+
+GradientImage::~GradientImage()
+{
+ if (_gradient) {
+ _release_connection.disconnect();
+ _modified_connection.disconnect();
+ _gradient = nullptr;
+ }
+}
+
+void
+GradientImage::size_request(GtkRequisition *requisition) const
+{
+ requisition->width = 54;
+ requisition->height = 12;
+}
+
+void
+GradientImage::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_width = natural_width = requisition.width;
+}
+
+void
+GradientImage::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_height = natural_height = requisition.height;
+}
+
+bool
+GradientImage::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ auto allocation = get_allocation();
+
+ cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard();
+ auto ct = cr->cobj();
+
+ cairo_set_source(ct, check);
+ cairo_paint(ct);
+ cairo_pattern_destroy(check);
+
+ if (_gradient) {
+ auto p = _gradient->create_preview_pattern(allocation.get_width());
+ cairo_set_source(ct, p);
+ cairo_paint(ct);
+ cairo_pattern_destroy(p);
+ }
+
+ return true;
+}
+
+void
+GradientImage::set_gradient(SPGradient *gradient)
+{
+ if (_gradient) {
+ _release_connection.disconnect();
+ _modified_connection.disconnect();
+ }
+
+ _gradient = gradient;
+
+ if (gradient) {
+ _release_connection = gradient->connectRelease(sigc::mem_fun(this, &GradientImage::gradient_release));
+ _modified_connection = gradient->connectModified(sigc::mem_fun(this, &GradientImage::gradient_modified));
+ }
+
+ update();
+}
+
+void
+GradientImage::gradient_release(SPObject *)
+{
+ if (_gradient) {
+ _release_connection.disconnect();
+ _modified_connection.disconnect();
+ }
+
+ _gradient = nullptr;
+
+ update();
+}
+
+void
+GradientImage::gradient_modified(SPObject *, guint /*flags*/)
+{
+ update();
+}
+
+void
+GradientImage::update()
+{
+ if (get_is_drawable()) {
+ queue_draw();
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+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;
+}
+
+
+
+/*
+ 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/ui/widget/gradient-image.h b/src/ui/widget/gradient-image.h
new file mode 100644
index 0000000..d583dbe
--- /dev/null
+++ b/src/ui/widget/gradient-image.h
@@ -0,0 +1,76 @@
+// 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 <glibmm/refptr.h>
+#include <gtkmm/widget.h>
+
+class SPGradient;
+class SPObject;
+class SPStop;
+
+namespace Gdk {
+ class Pixbuf;
+}
+
+#include <sigc++/connection.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class GradientImage : public Gtk::Widget {
+ private:
+ SPGradient *_gradient;
+
+ sigc::connection _release_connection;
+ sigc::connection _modified_connection;
+
+ void gradient_release(SPObject *obj);
+ void gradient_modified(SPObject *obj, guint flags);
+ void update();
+ void size_request(GtkRequisition *requisition) const;
+
+ protected:
+ void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override;
+ void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+
+ public:
+ GradientImage(SPGradient *gradient);
+ ~GradientImage() override;
+
+ void set_gradient(SPGradient *gr);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+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);
+
+#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/ui/widget/gradient-selector-interface.h b/src/ui/widget/gradient-selector-interface.h
new file mode 100644
index 0000000..b6833cf
--- /dev/null
+++ b/src/ui/widget/gradient-selector-interface.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_SELECTOR_INTERFACE_H
+#define SEEN_GRADIENT_SELECTOR_INTERFACE_H
+
+#include "object/sp-gradient.h"
+#include "object/sp-gradient-spread.h"
+#include "object/sp-gradient-units.h"
+
+class GradientSelectorInterface {
+public:
+ enum SelectorMode { MODE_LINEAR, MODE_RADIAL, MODE_SWATCH };
+
+ // pass gradient object (SPLinearGradient or SPRadialGradient)
+ virtual void setGradient(SPGradient* gradient) = 0;
+
+ virtual SPGradient* getVector() = 0;
+ virtual void setVector(SPDocument* doc, SPGradient* vector) = 0;
+ virtual void setMode(SelectorMode mode) = 0;
+ virtual void setUnits(SPGradientUnits units) = 0;
+ virtual SPGradientUnits getUnits() = 0;
+ virtual void setSpread(SPGradientSpread spread) = 0;
+ virtual SPGradientSpread getSpread() = 0;
+ virtual void selectStop(SPStop* selected) {};
+
+ sigc::signal<void (SPStop*)>& signal_stop_selected() { return _signal_stop_selected; }
+ void emit_stop_selected(SPStop* stop) { _signal_stop_selected.emit(stop); }
+
+private:
+ sigc::signal<void (SPStop*)> _signal_stop_selected;
+};
+
+#endif
diff --git a/src/ui/widget/gradient-selector.cpp b/src/ui/widget/gradient-selector.cpp
new file mode 100644
index 0000000..e6bfa7e
--- /dev/null
+++ b/src/ui/widget/gradient-selector.cpp
@@ -0,0 +1,612 @@
+// 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 "id-clash.h"
+#include "inkscape.h"
+#include "preferences.h"
+
+#include "object/sp-defs.h"
+#include "style.h"
+
+#include "actions/actions-tools.h" // Invoke gradient tool
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/widget/gradient-vector-selector.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void GradientSelector::style_button(Gtk::Button *btn, char const *iconName)
+{
+ 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);
+}
+
+GradientSelector::GradientSelector()
+ : _blocked(false)
+ , _mode(MODE_LINEAR)
+ , _gradientUnits(SP_GRADIENT_UNITS_USERSPACEONUSE)
+ , _gradientSpread(SP_GRADIENT_SPREAD_PAD)
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ /* Vectors */
+ _vectors = Gtk::manage(new GradientVectorSelector(nullptr, nullptr));
+ _store = _vectors->get_store();
+ _columns = _vectors->get_columns();
+
+ _treeview = Gtk::manage(new Gtk::TreeView());
+ _treeview->set_model(_store);
+ _treeview->set_headers_clickable(true);
+ _treeview->set_search_column(1);
+ _treeview->set_vexpand();
+ _icon_renderer = Gtk::manage(new Gtk::CellRendererPixbuf());
+ _text_renderer = Gtk::manage(new Gtk::CellRendererText());
+
+ _treeview->append_column(_("Gradient"), *_icon_renderer);
+ auto icon_column = _treeview->get_column(0);
+ icon_column->add_attribute(_icon_renderer->property_pixbuf(), _columns->pixbuf);
+ icon_column->set_sort_column(_columns->color);
+ icon_column->set_clickable(true);
+
+ _treeview->append_column(_("Name"), *_text_renderer);
+ auto name_column = _treeview->get_column(1);
+ _text_renderer->property_editable() = true;
+ name_column->add_attribute(_text_renderer->property_text(), _columns->name);
+ name_column->set_min_width(180);
+ name_column->set_clickable(true);
+ name_column->set_resizable(true);
+
+ _treeview->append_column("#", _columns->refcount);
+ auto count_column = _treeview->get_column(2);
+ count_column->set_clickable(true);
+ count_column->set_resizable(true);
+
+ _treeview->signal_key_press_event().connect(sigc::mem_fun(this, &GradientSelector::onKeyPressEvent), false);
+
+ _treeview->show();
+
+ icon_column->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::onTreeColorColClick));
+ name_column->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::onTreeNameColClick));
+ count_column->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::onTreeCountColClick));
+
+ auto tree_select_connection = _treeview->get_selection()->signal_changed().connect(sigc::mem_fun(this, &GradientSelector::onTreeSelection));
+ _vectors->set_tree_select_connection(tree_select_connection);
+ _text_renderer->signal_edited().connect(sigc::mem_fun(this, &GradientSelector::onGradientRename));
+
+ _scrolled_window = Gtk::manage(new Gtk::ScrolledWindow());
+ _scrolled_window->add(*_treeview);
+ _scrolled_window->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _scrolled_window->set_shadow_type(Gtk::SHADOW_IN);
+ _scrolled_window->set_size_request(0, 180);
+ _scrolled_window->set_hexpand();
+ _scrolled_window->show();
+
+ pack_start(*_scrolled_window, true, true, 4);
+
+
+ /* Create box for buttons */
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb->set_homogeneous(false);
+ pack_start(*hb, false, false, 0);
+
+ _add = Gtk::manage(new Gtk::Button());
+ style_button(_add, INKSCAPE_ICON("list-add"));
+
+ _nonsolid.push_back(_add);
+ hb->pack_start(*_add, false, false, 0);
+
+ _add->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::add_vector_clicked));
+ _add->set_sensitive(false);
+ _add->set_relief(Gtk::RELIEF_NONE);
+ _add->set_tooltip_text(_("Create a duplicate gradient"));
+
+ _del2 = Gtk::manage(new Gtk::Button());
+ style_button(_del2, INKSCAPE_ICON("list-remove"));
+
+ _nonsolid.push_back(_del2);
+ hb->pack_start(*_del2, false, false, 0);
+ _del2->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::delete_vector_clicked_2));
+ _del2->set_sensitive(false);
+ _del2->set_relief(Gtk::RELIEF_NONE);
+ _del2->set_tooltip_text(_("Delete unused gradient"));
+
+ // The only use of this button is hidden!
+ _edit = Gtk::manage(new Gtk::Button());
+ style_button(_edit, INKSCAPE_ICON("edit"));
+
+ _nonsolid.push_back(_edit);
+ hb->pack_start(*_edit, false, false, 0);
+ _edit->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::edit_vector_clicked));
+ _edit->set_sensitive(false);
+ _edit->set_relief(Gtk::RELIEF_NONE);
+ _edit->set_tooltip_text(_("Edit gradient"));
+ _edit->set_no_show_all();
+
+ _del = Gtk::manage(new Gtk::Button());
+ style_button(_del, INKSCAPE_ICON("list-remove"));
+
+ _swatch_widgets.push_back(_del);
+ hb->pack_start(*_del, false, false, 0);
+ _del->signal_clicked().connect(sigc::mem_fun(this, &GradientSelector::delete_vector_clicked));
+ _del->set_sensitive(false);
+ _del->set_relief(Gtk::RELIEF_NONE);
+ _del->set_tooltip_text(_("Delete swatch"));
+
+ hb->show_all();
+}
+
+void GradientSelector::setSpread(SPGradientSpread spread)
+{
+ _gradientSpread = spread;
+ // gtk_combo_box_set_active (GTK_COMBO_BOX(this->spread), gradientSpread);
+}
+
+void GradientSelector::setMode(SelectorMode mode)
+{
+ if (mode != _mode) {
+ _mode = mode;
+ if (mode == MODE_SWATCH) {
+ for (auto &it : _nonsolid) {
+ it->hide();
+ }
+ for (auto &swatch_widget : _swatch_widgets) {
+ swatch_widget->show_all();
+ }
+
+ auto icon_column = _treeview->get_column(0);
+ icon_column->set_title(_("Swatch"));
+
+ _vectors->setSwatched();
+ } else {
+ for (auto &it : _nonsolid) {
+ it->show_all();
+ }
+ for (auto &swatch_widget : _swatch_widgets) {
+ swatch_widget->hide();
+ }
+ auto icon_column = _treeview->get_column(0);
+ icon_column->set_title(_("Gradient"));
+ }
+ }
+}
+
+void GradientSelector::setUnits(SPGradientUnits units) { _gradientUnits = units; }
+
+SPGradientUnits GradientSelector::getUnits() { return _gradientUnits; }
+
+SPGradientSpread GradientSelector::getSpread() { return _gradientSpread; }
+
+void GradientSelector::onGradientRename(const Glib::ustring &path_string, const Glib::ustring &new_text)
+{
+ Gtk::TreePath path(path_string);
+ auto 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, _("Rename gradient"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+ }
+ }
+}
+
+void GradientSelector::onTreeColorColClick()
+{
+ auto column = _treeview->get_column(0);
+ column->set_sort_column(_columns->color);
+}
+
+void GradientSelector::onTreeNameColClick()
+{
+ auto column = _treeview->get_column(1);
+ column->set_sort_column(_columns->name);
+}
+
+
+void GradientSelector::onTreeCountColClick()
+{
+ auto column = _treeview->get_column(2);
+ column->set_sort_column(_columns->refcount);
+}
+
+void GradientSelector::moveSelection(int amount, bool down, bool toEnd)
+{
+ auto 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 GradientSelector::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 GradientSelector::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 auto sel = _treeview->get_selection();
+ if (!sel) {
+ return;
+ }
+
+ SPGradient *obj = nullptr;
+ /* Single selection */
+ auto iter = sel->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[_columns->data];
+ }
+
+ if (obj) {
+ vector_set(obj);
+ }
+
+ check_del_button();
+}
+
+void GradientSelector::check_del_button() {
+ const auto sel = _treeview->get_selection();
+ if (!sel) {
+ return;
+ }
+
+ SPGradient *obj = nullptr;
+ /* Single selection */
+ auto iter = sel->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[_columns->data];
+ }
+ if (_del2) {
+ _del2->set_sensitive(obj && sp_get_gradient_refcount(obj->document, obj) < 2 && _store->children().size() > 1);
+ }
+}
+
+bool GradientSelector::_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);
+ auto select = _treeview->get_selection();
+ bool wasBlocked = _blocked;
+ _blocked = true;
+ select->select(iter);
+ _blocked = wasBlocked;
+ found = true;
+ }
+
+ return found;
+}
+
+void GradientSelector::selectGradientInTree(SPGradient *vector)
+{
+ _store->foreach (sigc::bind<SPGradient *>(sigc::mem_fun(*this, &GradientSelector::_checkForSelected), vector));
+}
+
+void GradientSelector::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;
+ }
+
+ _vectors->set_gradient(doc, vector);
+
+ selectGradientInTree(vector);
+
+ if (vector) {
+ if ((_mode == MODE_SWATCH) && vector->isSwatch()) {
+ if (vector->isSolid()) {
+ for (auto &it : _nonsolid) {
+ it->hide();
+ }
+ } else {
+ for (auto &it : _nonsolid) {
+ it->show_all();
+ }
+ }
+ } else if (_mode != MODE_SWATCH) {
+
+ for (auto &swatch_widget : _swatch_widgets) {
+ swatch_widget->hide();
+ }
+ for (auto &it : _nonsolid) {
+ it->show_all();
+ }
+ }
+
+ if (_edit) {
+ _edit->set_sensitive(true);
+ }
+ if (_add) {
+ _add->set_sensitive(true);
+ }
+ if (_del) {
+ _del->set_sensitive(true);
+ }
+ check_del_button();
+ } else {
+ if (_edit) {
+ _edit->set_sensitive(false);
+ }
+ if (_add) {
+ _add->set_sensitive(doc != nullptr);
+ }
+ if (_del) {
+ _del->set_sensitive(false);
+ }
+ if (_del2) {
+ _del2->set_sensitive(false);
+ }
+ }
+}
+
+SPGradient *GradientSelector::getVector()
+{
+ return _vectors->get_gradient();
+}
+
+
+void GradientSelector::vector_set(SPGradient *gr)
+{
+ if (!_blocked) {
+ _blocked = true;
+ gr = sp_gradient_ensure_vector_normalized(gr);
+ setVector((gr) ? gr->document : nullptr, gr);
+ _signal_changed.emit(gr);
+ _blocked = false;
+ }
+}
+
+void GradientSelector::delete_vector_clicked_2() {
+ const auto selection = _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[_columns->data];
+ }
+
+ if (obj) {
+ if (auto repr = obj->getRepr()) {
+ repr->setAttribute("inkscape:collect", "always");
+
+ auto move = iter;
+ --move;
+ if (!move) {
+ move = iter;
+ ++move;
+ }
+ if (move) {
+ selection->select(move);
+ _treeview->scroll_to_row(_store->get_path(move), 0.5);
+ }
+ }
+ }
+}
+
+void GradientSelector::delete_vector_clicked()
+{
+ const auto selection = _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[_columns->data];
+ }
+
+ if (obj) {
+ std::string id = obj->getId();
+ sp_gradient_unset_swatch(SP_ACTIVE_DESKTOP, id);
+ }
+}
+
+void GradientSelector::edit_vector_clicked()
+{
+ // Invoke the gradient tool.... never actually called as button is hidden in only use!
+ set_active_tool(SP_ACTIVE_DESKTOP, "Gradient");
+}
+
+void GradientSelector::add_vector_clicked()
+{
+ auto doc = _vectors->get_document();
+
+ if (!doc)
+ return;
+
+ auto gr = _vectors->get_gradient();
+ Inkscape::XML::Document *xml_doc = doc->getReprDoc();
+
+ Inkscape::XML::Node *repr = nullptr;
+
+ if (gr) {
+ gr->getRepr()->removeAttribute("inkscape:collect");
+ repr = gr->getRepr()->duplicate(xml_doc);
+ // Rename the new gradients id to be similar to the cloned gradients
+ auto new_id = generate_unique_id(doc, gr->getId());
+ gr->setAttribute("id", new_id.c_str());
+ 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));
+ }
+
+ _vectors->set_gradient(doc, gr);
+
+ selectGradientInTree(gr);
+
+ // assign gradient to selection
+ vector_set(gr);
+
+ Inkscape::GC::release(repr);
+}
+
+void GradientSelector::show_edit_button(bool show) {
+ if (show) _edit->show(); else _edit->hide();
+}
+
+void GradientSelector::set_name_col_size(int min_width) {
+ auto name_column = _treeview->get_column(1);
+ name_column->set_min_width(min_width);
+}
+
+void GradientSelector::set_gradient_size(int width, int height) {
+ _vectors->set_pixmap_size(width, height);
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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 :
diff --git a/src/ui/widget/gradient-selector.h b/src/ui/widget/gradient-selector.h
new file mode 100644
index 0000000..f76949d
--- /dev/null
+++ b/src/ui/widget/gradient-selector.h
@@ -0,0 +1,160 @@
+// 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/box.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+
+#include "object/sp-gradient-spread.h"
+#include "object/sp-gradient-units.h"
+#include <vector>
+#include "gradient-selector-interface.h"
+
+class SPDocument;
+class SPGradient;
+
+namespace Gtk {
+class Button;
+class CellRendererPixbuf;
+class CellRendererText;
+class ScrolledWindow;
+class TreeView;
+} // namespace Gtk
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class GradientVectorSelector;
+
+class GradientSelector : public Gtk::Box, public GradientSelectorInterface {
+ public:
+ // enum SelectorMode { MODE_LINEAR, MODE_RADIAL, MODE_SWATCH };
+
+ 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;
+ };
+
+
+ private:
+ sigc::signal<void> _signal_grabbed;
+ sigc::signal<void> _signal_dragged;
+ sigc::signal<void> _signal_released;
+ sigc::signal<void, SPGradient *> _signal_changed;
+ SelectorMode _mode;
+
+ SPGradientUnits _gradientUnits;
+ SPGradientSpread _gradientSpread;
+
+ /* Vector selector */
+ GradientVectorSelector *_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;
+ ModelColumns *_columns;
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Gtk::CellRendererPixbuf *_icon_renderer;
+ Gtk::CellRendererText *_text_renderer;
+
+ /* Editing buttons */
+ Gtk::Button *_edit;
+ Gtk::Button *_add;
+ Gtk::Button *_del;
+ Gtk::Button *_del2;
+
+ bool _blocked;
+
+ std::vector<Gtk::Widget *> _nonsolid;
+ std::vector<Gtk::Widget *> _swatch_widgets;
+
+ void selectGradientInTree(SPGradient *vector);
+ void moveSelection(int amount, bool down = true, bool toEnd = false);
+
+ void style_button(Gtk::Button *btn, char const *iconName);
+ void check_del_button();
+
+ // Signal handlers
+ void add_vector_clicked();
+ void edit_vector_clicked();
+ void delete_vector_clicked();
+ void delete_vector_clicked_2();
+ void vector_set(SPGradient *gr);
+
+ public:
+ GradientSelector();
+
+ void show_edit_button(bool show);
+ void set_name_col_size(int min_width);
+ void set_gradient_size(int width, int height);
+
+ inline decltype(_signal_changed) signal_changed() const { return _signal_changed; }
+ inline decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; }
+ inline decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; }
+ inline decltype(_signal_released) signal_released() const { return _signal_released; }
+
+ void setGradient(SPGradient* gradient) override { /* no op */ }
+ SPGradient *getVector() override;
+ void setVector(SPDocument *doc, SPGradient *vector) override;
+ void setMode(SelectorMode mode) override;
+ void setUnits(SPGradientUnits units) override;
+ SPGradientUnits getUnits() override;
+ void setSpread(SPGradientSpread spread) override;
+ SPGradientSpread getSpread() override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#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/ui/widget/gradient-vector-selector.cpp b/src/ui/widget/gradient-vector-selector.cpp
new file mode 100644
index 0000000..ac6624c
--- /dev/null
+++ b/src/ui/widget/gradient-vector-selector.cpp
@@ -0,0 +1,327 @@
+// 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 "ui/widget/gradient-vector-selector.h"
+
+#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 "layer-manager.h"
+#include "include/macros.h"
+#include "selection-chemistry.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 "ui/widget/gradient-image.h"
+
+#include "xml/repr.h"
+
+using Inkscape::UI::SelectedColor;
+
+void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount );
+unsigned long sp_gradient_to_hhssll(SPGradient *gr);
+
+// TODO FIXME kill these globals!!!
+static Glib::ustring const prefs_path = "/dialogs/gradienteditor/";
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+GradientVectorSelector::GradientVectorSelector(SPDocument *doc, SPGradient *gr)
+{
+ _columns = new GradientSelector::ModelColumns();
+ _store = Gtk::ListStore::create(*_columns);
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ if (doc) {
+ set_gradient(doc, gr);
+ } else {
+ rebuild_gui_full();
+ }
+}
+
+GradientVectorSelector::~GradientVectorSelector()
+{
+ if (_gr) {
+ _gradient_release_connection.disconnect();
+ _tree_select_connection.disconnect();
+ _gr = nullptr;
+ }
+
+ if (_doc) {
+ _defs_release_connection.disconnect();
+ _defs_modified_connection.disconnect();
+ _doc = nullptr;
+ }
+}
+
+void GradientVectorSelector::set_gradient(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(!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 != _doc) {
+ /* Disconnect signals */
+ if (_gr) {
+ _gradient_release_connection.disconnect();
+ _gr = nullptr;
+ }
+ if (_doc) {
+ _defs_release_connection.disconnect();
+ _defs_modified_connection.disconnect();
+ _doc = nullptr;
+ }
+
+ // Connect signals
+ if (doc) {
+ _defs_release_connection = doc->getDefs()->connectRelease(sigc::mem_fun(this, &GradientVectorSelector::defs_release));
+ _defs_modified_connection = doc->getDefs()->connectModified(sigc::mem_fun(this, &GradientVectorSelector::defs_modified));
+ }
+ if (gr) {
+ _gradient_release_connection = gr->connectRelease(sigc::mem_fun(this, &GradientVectorSelector::gradient_release));
+ }
+ _doc = doc;
+ _gr = gr;
+ rebuild_gui_full();
+ if (!suppress) _signal_vector_set.emit(gr);
+ } else if (gr != _gr) {
+ // Harder case - keep document, rebuild list and stuff
+ // fixme: (Lauris)
+ suppress = TRUE;
+ set_gradient(nullptr, nullptr);
+ set_gradient(doc, gr);
+ suppress = FALSE;
+ _signal_vector_set.emit(gr);
+ }
+ /* The case of setting NULL -> NULL is not very interesting */
+}
+
+void
+GradientVectorSelector::gradient_release(SPObject * /*obj*/)
+{
+ /* Disconnect gradient */
+ if (_gr) {
+ _gradient_release_connection.disconnect();
+ _gr = nullptr;
+ }
+
+ /* Rebuild GUI */
+ rebuild_gui_full();
+}
+
+void
+GradientVectorSelector::defs_release(SPObject * /*defs*/)
+{
+ _doc = nullptr;
+
+ _defs_release_connection.disconnect();
+ _defs_modified_connection.disconnect();
+
+ /* Disconnect gradient as well */
+ if (_gr) {
+ _gradient_release_connection.disconnect();
+ _gr = nullptr;
+ }
+
+ /* Rebuild GUI */
+ rebuild_gui_full();
+}
+
+void
+GradientVectorSelector::defs_modified(SPObject *defs, guint flags)
+{
+ /* fixme: We probably have to check some flags here (Lauris) */
+ rebuild_gui_full();
+}
+
+void
+GradientVectorSelector::rebuild_gui_full()
+{
+ _tree_select_connection.block();
+
+ /* Clear old list, if there is any */
+ _store->clear();
+
+ /* Pick up all gradients with vectors */
+ std::vector<SPGradient *> gl;
+ if (_gr) {
+ auto gradients = _gr->document->getResourceList("gradient");
+ for (auto gradient : gradients) {
+ SPGradient* grad = SP_GRADIENT(gradient);
+ if ( grad->hasStops() && (grad->isSwatch() == _swatched) ) {
+ gl.push_back(SP_GRADIENT(gradient));
+ }
+ }
+ }
+
+ /* Get usage count of all the gradients */
+ std::map<SPGradient *, gint> usageCount;
+ gr_get_usage_counts(_doc, &usageCount);
+
+ if (!_doc) {
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_columns->name] = _("No document selected");
+
+ } else if (gl.empty()) {
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_columns->name] = _("No gradients in document");
+
+ } else if (!_gr) {
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_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, _pix_width, _pix_height);
+ Glib::ustring label = gr_prepare_label(gr);
+
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_columns->name] = label.c_str();
+ row[_columns->color] = hhssll;
+ row[_columns->refcount] = usageCount[gr];
+ row[_columns->data] = gr;
+ row[_columns->pixbuf] = Glib::wrap(pixb);
+ }
+ }
+
+ _tree_select_connection.unblock();
+}
+
+void
+GradientVectorSelector::setSwatched()
+{
+ _swatched = true;
+ rebuild_gui_full();
+}
+
+void GradientVectorSelector::set_pixmap_size(int width, int height) {
+ _pix_width = width;
+ _pix_height = height;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+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(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;
+}
+
+
+/*
+ * 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));
+}
+
+/*
+ * 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;
+
+ for (auto item : sp_get_all_document_items(doc)) {
+ if (!item->getId())
+ continue;
+ SPGradient *gr = nullptr;
+ gr = sp_item_get_gradient(item, true); // fill
+ if (gr) {
+ mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1;
+ }
+ gr = sp_item_get_gradient(item, false); // stroke
+ if (gr) {
+ mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 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/ui/widget/gradient-vector-selector.h b/src/ui/widget/gradient-vector-selector.h
new file mode 100644
index 0000000..6ce8edb
--- /dev/null
+++ b/src/ui/widget/gradient-vector-selector.h
@@ -0,0 +1,97 @@
+// 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 "ui/widget/gradient-selector.h"
+
+#include <gtkmm/liststore.h>
+#include <sigc++/connection.h>
+
+class SPDocument;
+class SPObject;
+class SPGradient;
+class SPStop;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class GradientVectorSelector : public Gtk::Box {
+ private:
+ bool _swatched = false;
+
+ SPDocument *_doc = nullptr;
+ SPGradient *_gr = nullptr;
+
+ /* Gradient vectors store */
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Inkscape::UI::Widget::GradientSelector::ModelColumns *_columns;
+
+ sigc::connection _gradient_release_connection;
+ sigc::connection _defs_release_connection;
+ sigc::connection _defs_modified_connection;
+ sigc::connection _tree_select_connection;
+
+ sigc::signal<void, SPGradient *> _signal_vector_set;
+
+ void gradient_release(SPObject *obj);
+ void defs_release(SPObject *defs);
+ void defs_modified(SPObject *defs, guint flags);
+ void rebuild_gui_full();
+
+ public:
+ GradientVectorSelector(SPDocument *doc, SPGradient *gradient);
+ ~GradientVectorSelector() override;
+
+ void setSwatched();
+ void set_gradient(SPDocument *doc, SPGradient *gr);
+ // width and height of gradient preview pixmap
+ void set_pixmap_size(int width, int height);
+
+ inline decltype(_columns) get_columns() const { return _columns; }
+ inline decltype(_doc) get_document() const { return _doc; }
+ inline decltype(_gr) get_gradient() const { return _gr; }
+ inline decltype(_store) get_store() const { return _store; }
+
+ inline decltype(_signal_vector_set) signal_vector_set() const { return _signal_vector_set; }
+
+ inline void set_tree_select_connection(sigc::connection &connection) { _tree_select_connection = connection; }
+
+private:
+ int _pix_width = 64;
+ int _pix_height = 18;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+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/ui/widget/gradient-with-stops.cpp b/src/ui/widget/gradient-with-stops.cpp
new file mode 100644
index 0000000..1d413dd
--- /dev/null
+++ b/src/ui/widget/gradient-with-stops.cpp
@@ -0,0 +1,552 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Gradient image widget with stop handles
+ *
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2020-2021 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <string>
+
+#include "gradient-with-stops.h"
+#include "object/sp-gradient.h"
+#include "object/sp-stop.h"
+#include "display/cairo-utils.h"
+#include "io/resource.h"
+#include "ui/cursor-utils.h"
+#include "ui/util.h"
+
+// widget's height; it should take stop template's height into account
+// current value is fine-tuned to make stop handles overlap gradient image just the right amount
+const int GRADIENT_WIDGET_HEIGHT = 33;
+// gradient's image height (multiple of checkerboard tiles, they are 6x6)
+const int GRADIENT_IMAGE_HEIGHT = 3 * 6;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+using namespace Inkscape::IO;
+
+std::string get_stop_template_path(const char* filename) {
+ // "stop handle" template files path
+ return Resource::get_filename(Resource::UIS, filename);
+}
+
+GradientWithStops::GradientWithStops() :
+ _template(get_stop_template_path("gradient-stop.svg").c_str()),
+ _tip_template(get_stop_template_path("gradient-tip.svg").c_str())
+ {
+ // default color, it will be updated
+ _background_color.set_grey(0.5);
+ // for theming, but not used
+ set_name("GradientEdit");
+ // we need some events
+ add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK |
+ Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK);
+ set_can_focus();
+}
+
+void GradientWithStops::set_gradient(SPGradient* gradient) {
+ _gradient = gradient;
+
+ // listen to release & changes
+ _release = gradient ? gradient->connectRelease([=](SPObject*){ set_gradient(nullptr); }) : sigc::connection();
+ _modified = gradient ? gradient->connectModified([=](SPObject*, guint){ modified(); }) : sigc::connection();
+
+ // TODO: check selected/focused stop index
+
+ modified();
+
+ set_sensitive(gradient != nullptr);
+}
+
+void GradientWithStops::modified() {
+ // gradient has been modified
+
+ // read all stops
+ _stops.clear();
+
+ if (_gradient) {
+ SPStop* stop = _gradient->getFirstStop();
+ while (stop) {
+ _stops.push_back(stop_t {
+ .offset = stop->offset, .color = stop->getColor(), .opacity = stop->getOpacity()
+ });
+ stop = stop->getNextStop();
+ }
+ }
+
+ update();
+}
+
+void GradientWithStops::size_request(GtkRequisition* requisition) const {
+ requisition->width = 60;
+ requisition->height = GRADIENT_WIDGET_HEIGHT;
+}
+
+void GradientWithStops::get_preferred_width_vfunc(int& minimal_width, int& natural_width) const {
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_width = natural_width = requisition.width;
+}
+
+void GradientWithStops::get_preferred_height_vfunc(int& minimal_height, int& natural_height) const {
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_height = natural_height = requisition.height;
+}
+
+void GradientWithStops::update() {
+ if (get_is_drawable()) {
+ queue_draw();
+ }
+}
+
+// capture background color when styles change
+void GradientWithStops::on_style_updated() {
+ if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) {
+ auto sc = wnd->get_style_context();
+ _background_color = get_background_color(sc);
+ }
+
+ // load and cache cursors
+ auto wnd = get_window();
+ if (wnd && !_cursor_mouseover) {
+ // use standard cursors:
+ _cursor_mouseover = Gdk::Cursor::create(get_display(), "grab");
+ _cursor_dragging = Gdk::Cursor::create(get_display(), "grabbing");
+ _cursor_insert = Gdk::Cursor::create(get_display(), "crosshair");
+ // or custom cursors:
+ // _cursor_mouseover = load_svg_cursor(get_display(), wnd, "gradient-over-stop.svg");
+ // _cursor_dragging = load_svg_cursor(get_display(), wnd, "gradient-drag-stop.svg");
+ // _cursor_insert = load_svg_cursor(get_display(), wnd, "gradient-add-stop.svg");
+ wnd->set_cursor();
+ }
+}
+
+void draw_gradient(const Cairo::RefPtr<Cairo::Context>& cr, SPGradient* gradient, int x, int width) {
+ cairo_pattern_t* check = ink_cairo_pattern_create_checkerboard();
+
+ cairo_set_source(cr->cobj(), check);
+ cr->fill_preserve();
+ cairo_pattern_destroy(check);
+
+ if (gradient) {
+ auto p = gradient->create_preview_pattern(width);
+ cairo_matrix_t m;
+ cairo_matrix_init_translate(&m, -x, 0);
+ cairo_pattern_set_matrix(p, &m);
+ cairo_set_source(cr->cobj(), p);
+ cr->fill();
+ cairo_pattern_destroy(p);
+ }
+}
+
+// return on-screen position of the UI stop corresponding to the gradient's color stop at 'index'
+GradientWithStops::stop_pos_t GradientWithStops::get_stop_position(size_t index, const layout_t& layout) const {
+ if (!_gradient || index >= _stops.size()) {
+ return stop_pos_t {};
+ }
+
+ // half of the stop template width; round it to avoid half-pixel coordinates
+ const auto dx = round((_template.get_width_px() + 1) / 2);
+
+ auto pos = [&](double offset) { return round(layout.x + layout.width * CLAMP(offset, 0, 1)); };
+ const auto& v = _stops;
+
+ auto offset = pos(v[index].offset);
+ auto left = offset - dx;
+ if (index > 0) {
+ // check previous stop; it may overlap
+ auto prev = pos(v[index - 1].offset) + dx;
+ if (prev > left) {
+ // overlap
+ left = round((left + prev) / 2);
+ }
+ }
+
+ auto right = offset + dx;
+ if (index + 1 < v.size()) {
+ // check next stop for overlap
+ auto next = pos(v[index + 1].offset) - dx;
+ if (right > next) {
+ // overlap
+ right = round((right + next) / 2);
+ }
+ }
+
+ return stop_pos_t {
+ .left = left,
+ .tip = offset,
+ .right = right,
+ .top = layout.height - _template.get_height_px(),
+ .bottom = layout.height
+ };
+}
+
+// widget's layout; mainly location of the gradient's image and stop handles
+GradientWithStops::layout_t GradientWithStops::get_layout() const {
+ auto allocation = get_allocation();
+
+ const auto stop_width = _template.get_width_px();
+ const auto half_stop = round((stop_width + 1) / 2);
+ const auto x = half_stop;
+ const double width = allocation.get_width() - stop_width;
+ const double height = allocation.get_height();
+
+ return layout_t {
+ .x = x,
+ .y = 0,
+ .width = width,
+ .height = height
+ };
+}
+
+// check if stop handle is under (x, y) location, return its index or -1 if not hit
+int GradientWithStops::find_stop_at(double x, double y) const {
+ if (!_gradient) return -1;
+
+ const auto& v = _stops;
+ const auto& layout = get_layout();
+
+ // find stop handle at (x, y) position; note: stops may not be ordered by offsets
+ for (size_t i = 0; i < v.size(); ++i) {
+ auto pos = get_stop_position(i, layout);
+ if (x >= pos.left && x <= pos.right && y >= pos.top && y <= pos.bottom) {
+ return static_cast<int>(i);
+ }
+ }
+
+ return -1;
+}
+
+// this is range of offset adjustment for a given stop
+GradientWithStops::limits_t GradientWithStops::get_stop_limits(int maybe_index) const {
+ if (!_gradient) return limits_t {};
+
+ // let negative index turn into a large out-of-range number
+ auto index = static_cast<size_t>(maybe_index);
+
+ const auto& v = _stops;
+
+ if (index < v.size()) {
+ double min = 0;
+ double max = 1;
+
+ if (v.size() > 1) {
+ std::vector<double> offsets;
+ offsets.reserve(v.size());
+ for (auto& s : _stops) {
+ offsets.push_back(s.offset);
+ }
+ std::sort(offsets.begin(), offsets.end());
+
+ // special cases:
+ if (index == 0) { // first stop
+ max = offsets[index + 1];
+ }
+ else if (index + 1 == v.size()) { // last stop
+ min = offsets[index - 1];
+ }
+ else {
+ // stops "inside" gradient
+ min = offsets[index - 1];
+ max = offsets[index + 1];
+ }
+ }
+ return limits_t { .min_offset = min, .max_offset = max, .offset = v[index].offset };
+ }
+ else {
+ return limits_t {};
+ }
+}
+
+bool GradientWithStops::on_focus_out_event(GdkEventFocus* event) {
+ update();
+ return false;
+}
+
+bool GradientWithStops::on_focus_in_event(GdkEventFocus* event) {
+ update();
+ return false;
+}
+
+bool GradientWithStops::on_focus(Gtk::DirectionType direction) {
+ if (has_focus()) {
+ return false; // let focus go
+ }
+
+ grab_focus();
+ // TODO - add focus indicator frame or some focus indicator
+ return true;
+}
+
+bool GradientWithStops::on_key_press_event(GdkEventKey* key_event) {
+ bool consumed = false;
+ // currently all keyboard activity involves acting on focused stop handle; bail if nothing's selected
+ if (_focused_stop < 0) return consumed;
+
+ unsigned int key = 0;
+ auto modifier = static_cast<GdkModifierType>(key_event->state);
+ gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode, modifier, 0, &key, nullptr, nullptr, nullptr);
+
+ auto delta = _stop_move_increment;
+ if (modifier & GDK_SHIFT_MASK) {
+ delta *= 10;
+ }
+
+ consumed = true;
+
+ switch (key) {
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ move_stop(_focused_stop, -delta);
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ move_stop(_focused_stop, delta);
+ break;
+ case GDK_KEY_BackSpace:
+ case GDK_KEY_Delete:
+ _signal_delete_stop.emit(_focused_stop);
+ break;
+ default:
+ consumed = false;
+ break;
+ }
+
+ return consumed;
+}
+
+bool GradientWithStops::on_button_press_event(GdkEventButton* event) {
+ // single button press selects stop and can start dragging it
+ constexpr auto LEFT_BTN = 1;
+ if (event->button == LEFT_BTN && _gradient && event->type == GDK_BUTTON_PRESS) {
+ _focused_stop = -1;
+
+ if (!has_focus()) {
+ // grab focus, so we can show selection indicator and move selected stop with left/right keys
+ grab_focus();
+ }
+ update();
+
+ // find stop handle
+ auto index = find_stop_at(event->x, event->y);
+
+ if (index >= 0) {
+ _focused_stop = index;
+ // fire stop selection, whether stop can be moved or not
+ _signal_stop_selected.emit(index);
+
+ auto limits = get_stop_limits(index);
+
+ // check if clicked stop can be moved
+ if (limits.min_offset < limits.max_offset) {
+ // TODO: to facilitate selecting stops without accidentally moving them,
+ // delay dragging mode until mouse cursor moves certain distance...
+ _dragging = true;
+ _pointer_x = event->x;
+ _stop_offset = _stops.at(index).offset;
+
+ if (_cursor_dragging) {
+ gdk_window_set_cursor(event->window, _cursor_dragging->gobj());
+ }
+ }
+ }
+ }
+ else if (event->button == LEFT_BTN && _gradient && event->type == GDK_2BUTTON_PRESS) {
+ // double-click may insert a new stop
+ auto index = find_stop_at(event->x, event->y);
+ if (index < 0) {
+ auto layout = get_layout();
+ if (layout.width > 0 && event->x > layout.x && event->x < layout.x + layout.width) {
+ double position = (event->x - layout.x) / layout.width;
+ // request new stop
+ _signal_add_stop_at.emit(position);
+ }
+ }
+ }
+
+ return false;
+}
+
+bool GradientWithStops::on_button_release_event(GdkEventButton* event) {
+ GdkCursor* cursor = get_cursor(event->x, event->y);
+ gdk_window_set_cursor(event->window, cursor);
+
+ _dragging = false;
+ return false;
+}
+
+// move stop by a given amount (delta)
+void GradientWithStops::move_stop(int stop_index, double offset_shift) {
+ auto layout = get_layout();
+ if (layout.width > 0) {
+ auto limits = get_stop_limits(stop_index);
+ if (limits.min_offset < limits.max_offset) {
+ auto new_offset = CLAMP(limits.offset + offset_shift, limits.min_offset, limits.max_offset);
+ if (new_offset != limits.offset) {
+ _signal_stop_offset_changed.emit(stop_index, new_offset);
+ }
+ }
+ }
+}
+
+bool GradientWithStops::on_motion_notify_event(GdkEventMotion* event) {
+ if (_dragging && _gradient) {
+ // move stop to a new position (adjust offset)
+ auto dx = event->x - _pointer_x;
+ auto layout = get_layout();
+ if (layout.width > 0) {
+ auto delta = dx / layout.width;
+ auto limits = get_stop_limits(_focused_stop);
+ if (limits.min_offset < limits.max_offset) {
+ auto new_offset = CLAMP(_stop_offset + delta, limits.min_offset, limits.max_offset);
+ _signal_stop_offset_changed.emit(_focused_stop, new_offset);
+ }
+ }
+ }
+ else if (!_dragging && _gradient) {
+ GdkCursor* cursor = get_cursor(event->x, event->y);
+ gdk_window_set_cursor(event->window, cursor);
+ }
+
+ return false;
+}
+
+GdkCursor* GradientWithStops::get_cursor(double x, double y) const {
+ GdkCursor* cursor = nullptr;
+ if (_gradient) {
+ // check if mouse if over stop handle that we can adjust
+ auto index = find_stop_at(x, y);
+ if (index >= 0) {
+ auto limits = get_stop_limits(index);
+ if (limits.min_offset < limits.max_offset && _cursor_mouseover) {
+ cursor = _cursor_mouseover->gobj();
+ }
+ }
+ else {
+ if (_cursor_insert) {
+ cursor = _cursor_insert->gobj();
+ }
+ }
+ }
+ return cursor;
+}
+
+bool GradientWithStops::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
+ auto allocation = get_allocation();
+ auto context = get_style_context();
+ const double scale = get_scale_factor();
+ const auto layout = get_layout();
+
+ if (layout.width <= 0) return true;
+
+ context->render_background(cr, 0, 0, allocation.get_width(), allocation.get_height());
+
+ // empty gradient checkboard or gradient itself
+ cr->rectangle(layout.x, layout.y, layout.width, GRADIENT_IMAGE_HEIGHT);
+ draw_gradient(cr, _gradient, layout.x, layout.width);
+
+ if (!_gradient) return true;
+
+ // draw stop handles
+
+ cr->begin_new_path();
+
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ Gdk::RGBA bg = _background_color;
+
+ // stop handle outlines and selection indicator use theme colors:
+ _template.set_style(".outer", "fill", rgba_to_css_color(fg));
+ _template.set_style(".inner", "stroke", rgba_to_css_color(bg));
+ _template.set_style(".hole", "fill", rgba_to_css_color(bg));
+
+ auto tip = _tip_template.render(scale);
+
+ for (size_t i = 0; i < _stops.size(); ++i) {
+ const auto& stop = _stops[i];
+
+ // stop handle shows stop color and opacity:
+ _template.set_style(".color", "fill", rgba_to_css_color(stop.color));
+ _template.set_style(".opacity", "opacity", double_to_css_value(stop.opacity));
+
+ // show/hide selection indicator
+ const auto is_selected = _focused_stop == static_cast<int>(i);
+ _template.set_style(".selected", "opacity", double_to_css_value(is_selected ? 1 : 0));
+
+ // render stop handle
+ auto pix = _template.render(scale);
+
+ if (!pix) {
+ g_warning("Rendering gradient stop failed.");
+ break;
+ }
+
+ auto pos = get_stop_position(i, layout);
+
+ // selected handle sports a 'tip' to make it easily noticeable
+ if (is_selected && tip) {
+ if (auto surface = Gdk::Cairo::create_surface_from_pixbuf(tip, 1)) {
+ cr->save();
+ // scale back to physical pixels
+ cr->scale(1 / scale, 1 / scale);
+ // paint tip bitmap
+ cr->set_source(surface, round(pos.tip * scale - tip->get_width() / 2), layout.y * scale);
+ cr->paint();
+ cr->restore();
+ }
+ }
+
+ // surface from pixbuf *without* scaling (scale = 1)
+ auto surface = Gdk::Cairo::create_surface_from_pixbuf(pix, 1);
+ if (!surface) continue;
+
+ // calc space available for stop marker
+ cr->save();
+ cr->rectangle(pos.left, layout.y, pos.right - pos.left, layout.height);
+ cr->clip();
+ // scale back to physical pixels
+ cr->scale(1 / scale, 1 / scale);
+ // paint bitmap
+ cr->set_source(surface, round(pos.tip * scale - pix->get_width() / 2), pos.top * scale);
+ cr->paint();
+ cr->restore();
+ cr->reset_clip();
+ }
+
+ return true;
+}
+
+// focused/selected stop indicator
+void GradientWithStops::set_focused_stop(int index) {
+ if (_focused_stop != index) {
+ _focused_stop = index;
+
+ if (has_focus()) {
+ update();
+ }
+ }
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/ui/widget/gradient-with-stops.h b/src/ui/widget/gradient-with-stops.h
new file mode 100644
index 0000000..0276fa2
--- /dev/null
+++ b/src/ui/widget/gradient-with-stops.h
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_GRADIENT_WITH_STOPS_H
+#define SEEN_GRADIENT_WITH_STOPS_H
+
+#include <gtkmm/widget.h>
+#include <gdkmm/color.h>
+#include "ui/svg-renderer.h"
+#include "helper/auto-connection.h"
+
+class SPGradient;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class GradientWithStops : public Gtk::DrawingArea {
+public:
+ GradientWithStops();
+
+ // gradient to draw or nullptr
+ void set_gradient(SPGradient* gradient);
+
+ // set selected stop handle (or pass -1 to deselect)
+ void set_focused_stop(int index);
+
+ // stop has been selected
+ sigc::signal<void (size_t)>& signal_stop_selected() {
+ return _signal_stop_selected;
+ }
+
+ // request to change stop's offset
+ sigc::signal<void (size_t, double)>& signal_stop_offset_changed() {
+ return _signal_stop_offset_changed;
+ }
+
+ sigc::signal<void (double)>& signal_add_stop_at() {
+ return _signal_add_stop_at;
+ }
+
+ sigc::signal<void (size_t)>& signal_delete_stop() {
+ return _signal_delete_stop;
+ }
+
+private:
+ void get_preferred_width_vfunc(int& minimum_width, int& natural_width) const override;
+ void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+ void on_style_updated() override;
+ bool on_button_press_event(GdkEventButton* event) override;
+ bool on_button_release_event(GdkEventButton* event) override;
+ bool on_motion_notify_event(GdkEventMotion* event) override;
+ bool on_key_press_event(GdkEventKey* key_event) override;
+ bool on_focus_in_event(GdkEventFocus* event) override;
+ bool on_focus_out_event(GdkEventFocus* event) override;
+ bool on_focus(Gtk::DirectionType direction) override;
+ void size_request(GtkRequisition* requisition) const;
+ void modified();
+ // repaint widget
+ void update();
+ // index of gradient stop handle under (x, y) or -1
+ int find_stop_at(double x, double y) const;
+ // request stop move
+ void move_stop(int stop_index, double offset_shift);
+
+ // layout of gradient image/editor
+ struct layout_t {
+ double x, y, width, height;
+ };
+ layout_t get_layout() const;
+
+ // position of single gradient stop handle
+ struct stop_pos_t {
+ double left, tip, right, top, bottom;
+ };
+ stop_pos_t get_stop_position(size_t index, const layout_t& layout) const;
+
+ struct limits_t {
+ double min_offset, max_offset, offset;
+ };
+ limits_t get_stop_limits(int index) const;
+ GdkCursor* get_cursor(double x, double y) const;
+
+ SPGradient* _gradient = nullptr;
+ struct stop_t {
+ double offset;
+ SPColor color;
+ double opacity;
+ };
+ std::vector<stop_t> _stops;
+ // handle stop SVG template
+ svg_renderer _template;
+ // selected handle indicator
+ svg_renderer _tip_template;
+ auto_connection _release;
+ auto_connection _modified;
+ Gdk::RGBA _background_color;
+ sigc::signal<void (size_t)> _signal_stop_selected;
+ sigc::signal<void (size_t, double)> _signal_stop_offset_changed;
+ sigc::signal<void (double)> _signal_add_stop_at;
+ sigc::signal<void (size_t)> _signal_delete_stop;
+ bool _dragging = false;
+ // index of handle stop that user clicked; may be out of range
+ int _focused_stop = -1;
+ double _pointer_x = 0;
+ double _stop_offset = 0;
+ Glib::RefPtr<Gdk::Cursor> _cursor_mouseover;
+ Glib::RefPtr<Gdk::Cursor> _cursor_dragging;
+ Glib::RefPtr<Gdk::Cursor> _cursor_insert;
+ // TODO: customize this amount or read prefs
+ double _stop_move_increment = 0.01;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
diff --git a/src/ui/widget/icon-combobox.h b/src/ui/widget/icon-combobox.h
new file mode 100644
index 0000000..6c1770c
--- /dev/null
+++ b/src/ui/widget/icon-combobox.h
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef ICON_COMBO_BOX_SEEN_
+#define ICON_COMBO_BOX_SEEN_
+
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class IconComboBox : public Gtk::ComboBox {
+public:
+ IconComboBox() {
+ _model = Gtk::ListStore::create(_columns);
+ set_model(_model);
+
+ pack_start(_renderer, false);
+ _renderer.set_property("stock_size", Gtk::ICON_SIZE_BUTTON);
+ _renderer.set_padding(2, 0);
+ add_attribute(_renderer, "icon_name", _columns.icon_name);
+
+ pack_start(_columns.label);
+ }
+
+ void add_row(const Glib::ustring& icon_name, const Glib::ustring& label, int id) {
+ Gtk::TreeModel::Row row = *_model->append();
+ row[_columns.id] = id;
+ row[_columns.icon_name] = icon_name;
+ row[_columns.label] = ' ' + label;
+ }
+
+ void set_active_by_id(int id) {
+ for (auto i = _model->children().begin(); i != _model->children().end(); ++i) {
+ const int data = (*i)[_columns.id];
+ if (data == id) {
+ set_active(i);
+ break;
+ }
+ }
+ };
+
+ int get_active_row_id() const {
+ if (auto it = get_active()) {
+ return (*it)[_columns.id];
+ }
+ return -1;
+ }
+
+private:
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns() {
+ add(icon_name);
+ add(label);
+ add(id);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> icon_name;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<int> id;
+ };
+
+ Columns _columns;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ Gtk::CellRendererPixbuf _renderer;
+};
+
+}}}
+
+#endif
diff --git a/src/ui/widget/iconrenderer.cpp b/src/ui/widget/iconrenderer.cpp
new file mode 100644
index 0000000..f761316
--- /dev/null
+++ b/src/ui/widget/iconrenderer.cpp
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ * Martin Owens 2018 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/iconrenderer.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+IconRenderer::IconRenderer() :
+ Glib::ObjectBase(typeid(IconRenderer)),
+ Gtk::CellRendererPixbuf(),
+ _property_icon(*this, "icon", 0)
+{
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+ set_pixbuf();
+}
+
+/*
+ * Called when an icon is clicked.
+ */
+IconRenderer::type_signal_activated IconRenderer::signal_activated()
+{
+ return m_signal_activated;
+}
+
+void IconRenderer::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h);
+
+ if (min_h) {
+ min_h += (min_h) >> 1;
+ }
+
+ if (nat_h) {
+ nat_h += (nat_h) >> 1;
+ }
+}
+
+void IconRenderer::get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const
+{
+ Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w);
+
+ if (min_w) {
+ min_w += (min_w) >> 1;
+ }
+
+ if (nat_w) {
+ nat_w += (nat_w) >> 1;
+ }
+}
+
+void IconRenderer::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags )
+{
+ set_pixbuf();
+
+ Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags );
+}
+
+bool IconRenderer::activate_vfunc(GdkEvent* /*event*/,
+ Gtk::Widget& /*widget*/,
+ const Glib::ustring& path,
+ const Gdk::Rectangle& /*background_area*/,
+ const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/)
+{
+ m_signal_activated.emit(path);
+ return true;
+}
+
+void IconRenderer::add_icon(Glib::ustring name)
+{
+ _icons.push_back(sp_get_icon_pixbuf(name.c_str(), GTK_ICON_SIZE_BUTTON));
+}
+
+void IconRenderer::set_pixbuf()
+{
+ int icon_index = property_icon().get_value();
+ if(icon_index >= 0 && icon_index < _icons.size()) {
+ property_pixbuf() = _icons[icon_index];
+ } else {
+ property_pixbuf() = sp_get_icon_pixbuf("image-missing", GTK_ICON_SIZE_BUTTON);
+ }
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/iconrenderer.h b/src/ui/widget/iconrenderer.h
new file mode 100644
index 0000000..27c389b
--- /dev/null
+++ b/src/ui/widget/iconrenderer.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_WIDGET_ICONRENDERER_H__
+#define __UI_WIDGET_ICONRENDERER_H__
+/*
+ * Authors:
+ * Theodore Janeczko
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ * Martin Owens 2018 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class IconRenderer : public Gtk::CellRendererPixbuf {
+public:
+ IconRenderer();
+ ~IconRenderer() override = default;;
+
+ Glib::PropertyProxy<int> property_icon() { return _property_icon.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+ void add_icon(Glib::ustring name);
+
+ typedef sigc::signal<void, Glib::ustring> type_signal_activated;
+ type_signal_activated signal_activated();
+protected:
+ type_signal_activated m_signal_activated;
+
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+private:
+
+ Glib::Property<int> _property_icon;
+ std::vector<Glib::RefPtr<Gdk::Pixbuf>> _icons;
+ void set_pixbuf();
+};
+
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_WIDGET_ICONRENDERER_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/ui/widget/imagetoggler.cpp b/src/ui/widget/imagetoggler.cpp
new file mode 100644
index 0000000..7a0c295
--- /dev/null
+++ b/src/ui/widget/imagetoggler.cpp
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Jon A. Cruz
+ * Johan B. C. Engelen
+ *
+ * Copyright (C) 2006-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/iconinfo.h>
+
+#include "ui/widget/imagetoggler.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ImageToggler::ImageToggler( char const* on, char const* off) :
+ Glib::ObjectBase(typeid(ImageToggler)),
+ Gtk::CellRenderer(),
+ _pixOnName(on),
+ _pixOffName(off),
+ _property_active(*this, "active", false),
+ _property_activatable(*this, "activatable", true),
+ _property_gossamer(*this, "gossamer", false),
+ _property_pixbuf_on(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)),
+ _property_pixbuf_off(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr))
+{
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+ Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, _size, _size);
+}
+
+void ImageToggler::get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const
+{
+ min_h = _size + 6;
+ nat_h = _size + 8;
+}
+
+void ImageToggler::get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const
+{
+ min_w = _size + 12;
+ nat_w = _size + 16;
+}
+
+void ImageToggler::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags )
+{
+ // Lazy/late pixbuf rendering to get access to scale factor from widget.
+ if(!_property_pixbuf_on.get_value()) {
+ int scale = widget.get_scale_factor();
+ _property_pixbuf_on = sp_get_icon_pixbuf(_pixOnName, _size * scale);
+ _property_pixbuf_off = sp_get_icon_pixbuf(_pixOffName, _size * scale);
+ }
+
+ // Hide when not being used.
+ double alpha = 1.0;
+ bool visible = _property_activatable.get_value()
+ || _property_active.get_value();
+ if (!visible) {
+ // XXX There is conflict about this value, some users want 0.2, others want 0.0
+ alpha = 0.0;
+ }
+ if (_property_gossamer.get_value()) {
+ alpha += 0.2;
+ }
+ if (alpha <= 0.0) {
+ return;
+ }
+
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf;
+ if(_property_active.get_value()) {
+ pixbuf = _property_pixbuf_on.get_value();
+ } else {
+ pixbuf = _property_pixbuf_off.get_value();
+ }
+
+ cairo_surface_t *surface = gdk_cairo_surface_create_from_pixbuf(
+ pixbuf->gobj(), 0, widget.get_window()->gobj());
+ g_return_if_fail(surface);
+
+ // Center the icon in the cell area
+ int x = cell_area.get_x() + int((cell_area.get_width() - _size) * 0.5);
+ int y = cell_area.get_y() + int((cell_area.get_height() - _size) * 0.5);
+
+ cairo_set_source_surface(cr->cobj(), surface, x, y);
+ cr->set_operator(Cairo::OPERATOR_ATOP);
+ cr->rectangle(x, y, _size, _size);
+ if (alpha < 1.0) {
+ cr->clip();
+ cr->paint_with_alpha(alpha);
+ } else {
+ cr->fill();
+ }
+ cairo_surface_destroy(surface); // free!
+}
+
+bool
+ImageToggler::activate_vfunc(GdkEvent* event,
+ Gtk::Widget& /*widget*/,
+ const Glib::ustring& path,
+ const Gdk::Rectangle& /*background_area*/,
+ const Gdk::Rectangle& /*cell_area*/,
+ Gtk::CellRendererState /*flags*/)
+{
+ _signal_pre_toggle.emit(event);
+ _signal_toggled.emit(path);
+
+ return false;
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/imagetoggler.h b/src/ui/widget/imagetoggler.h
new file mode 100644
index 0000000..8ec406b
--- /dev/null
+++ b/src/ui/widget/imagetoggler.h
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_IMAGETOGGLER_H__
+#define __UI_DIALOG_IMAGETOGGLER_H__
+/*
+ * Authors:
+ * Jon A. Cruz
+ * Johan B. C. Engelen
+ *
+ * Copyright (C) 2006-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ImageToggler : public Gtk::CellRenderer {
+public:
+ ImageToggler( char const *on, char const *off);
+ ~ImageToggler() override = default;;
+
+ sigc::signal<void, const Glib::ustring&> signal_toggled() { return _signal_toggled;}
+ sigc::signal<void, GdkEvent const *> signal_pre_toggle() { return _signal_pre_toggle; }
+
+ Glib::PropertyProxy<bool> property_active() { return _property_active.get_proxy(); }
+ Glib::PropertyProxy<bool> property_activatable() { return _property_activatable.get_proxy(); }
+ Glib::PropertyProxy<bool> property_gossamer() { return _property_gossamer.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+protected:
+ void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags ) override;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& min_w,
+ int& nat_w) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& min_h,
+ int& nat_h) const override;
+
+ bool activate_vfunc(GdkEvent *event,
+ Gtk::Widget &widget,
+ const Glib::ustring &path,
+ const Gdk::Rectangle &background_area,
+ const Gdk::Rectangle &cell_area,
+ Gtk::CellRendererState flags) override;
+
+
+private:
+ int _size;
+ Glib::ustring _pixOnName;
+ Glib::ustring _pixOffName;
+
+ Glib::Property<bool> _property_active;
+ Glib::Property<bool> _property_activatable;
+ Glib::Property<bool> _property_gossamer;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_on;
+ Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_off;
+
+ sigc::signal<void, const Glib::ustring&> _signal_toggled;
+ sigc::signal<void, GdkEvent const *> _signal_pre_toggle;
+};
+
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_IMAGETOGGLER_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/ui/widget/ink-color-wheel.cpp b/src/ui/widget/ink-color-wheel.cpp
new file mode 100644
index 0000000..4d6cc8c
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.cpp
@@ -0,0 +1,1507 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * HSLuv color wheel widget, based on the web implementation at
+ * https://www.hsluv.org
+ *//*
+ * Authors:
+ * Tavmjong Bah
+ * Massinissa Derriche <massinissa.derriche@gmail.com>
+ *
+ * Copyright (C) 2018, 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ink-color-wheel.h"
+
+#include <cstring>
+#include <algorithm>
+
+#include "hsluv.h"
+
+// Sizes in pixels
+static int const SIZE = 400;
+static int const OUTER_CIRCLE_RADIUS = 190;
+
+static double const MAX_HUE = 360.0;
+static double const MAX_SATURATION = 100.0;
+static double const MAX_LIGHTNESS = 100.0;
+static double const MIN_HUE = 0.0;
+static double const MIN_SATURATION = 0.0;
+static double const MIN_LIGHTNESS = 0.0;
+static double const OUTER_CIRCLE_DASH_SIZE = 10.0;
+static double const VERTEX_EPSILON = 0.01;
+
+using Hsluv::Line;
+
+class ColorPoint {
+public:
+ ColorPoint();
+ ColorPoint(double x, double y, double r, double g, double b);
+ ColorPoint(double x, double y, guint color);
+
+ guint32 get_color();
+ void set_color(double red, double green, double blue);
+
+ double x;
+ double y;
+ double r;
+ double g;
+ double b;
+};
+
+/* FIXME: replace with Geom::Point */
+class Point {
+public:
+ Point();
+ Point(double x, double y);
+
+ double x;
+ double y;
+};
+
+class Intersection {
+public:
+ Intersection();
+ Intersection (int line1, int line2, Point intersectionPoint,
+ double intersectionPointAngle, double relativeAngle);
+
+ int line1;
+ int line2;
+ Point intersectionPoint;
+ double intersectionPointAngle;
+ double relativeAngle;
+};
+
+static Point intersect_line_line(Line a, Line b);
+static double distance_from_origin(Point point);
+static double distance_line_from_origin(Line line);
+static double angle_from_origin(Point point);
+static double normalize_angle(double angle);
+static double lerp(double v0, double v1, double t0, double t1, double t);
+static ColorPoint lerp(ColorPoint const &v0, ColorPoint const &v1, double t0, double t1, double t);
+static guint32 hsv_to_rgb(double h, double s, double v);
+static double luminance(guint32 color);
+static Point to_pixel_coordinate(Point const &point, double scale, double resize);
+static Point from_pixel_coordinate(Point const &point, double scale, double resize);
+static std::vector<Point> to_pixel_coordinate( std::vector<Point> const &points, double scale,
+ double resize);
+static void draw_vertical_padding(ColorPoint p0, ColorPoint p1, int padding,
+ bool pad_upwards, guint32 *buffer, int height, int stride);
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Used to represent the in RGB gamut colors polygon of the color wheel.
+ *
+ * @struct
+ */
+struct PickerGeometry {
+ std::vector<Line> lines;
+ /** Ordered such that 1st vertex is intersection between first and second
+ * line, 2nd vertex between second and third line etc. */
+ std::vector<Point> vertices;
+ /** Angles from origin to corresponding vertex, in radians */
+ std::vector<double> angles;
+ /** Smallest circle with center at origin such that polygon fits inside */
+ double outerCircleRadius;
+ /** Largest circle with center at origin such that it fits inside polygon */
+ double innerCircleRadius;
+};
+
+/**
+ * Update the passed in PickerGeometry structure to the given lightness value.
+ *
+ * @param[out] pickerGeometry The PickerGeometry instance to update.
+ * @param lightness The lightness value.
+ */
+static void get_picker_geometry(PickerGeometry *pickerGeometry, double lightness);
+
+/* Base Color Wheel */
+ColorWheel::ColorWheel()
+ : _adjusting(false)
+{
+ set_name("ColorWheel");
+
+ add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::BUTTON_MOTION_MASK | Gdk::KEY_PRESS_MASK);
+ set_can_focus();
+}
+
+void ColorWheel::setRgb(double /*r*/, double /*g*/, double /*b*/, bool /*overrideHue*/)
+{}
+
+void ColorWheel::getRgb(double */*r*/, double */*g*/, double */*b*/) const
+{}
+
+void ColorWheel::getRgbV(double *rgb) const {}
+
+guint32 ColorWheel::getRgb() const { return 0; }
+
+void ColorWheel::setHue(double h)
+{
+ _values[0] = std::clamp(h, MIN_HUE, MAX_HUE);
+}
+
+void ColorWheel::setSaturation(double s)
+{
+ _values[1] = std::clamp(s, MIN_SATURATION, MAX_SATURATION);
+}
+
+void ColorWheel::setLightness(double l)
+{
+ _values[2] = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS);
+}
+
+void ColorWheel::getValues(double *a, double *b, double *c) const
+{
+ if (a) *a = _values[0];
+ if (b) *b = _values[1];
+ if (c) *c = _values[2];
+}
+
+void ColorWheel::_set_from_xy(double const x, double const y)
+{}
+
+bool ColorWheel::on_key_release_event(GdkEventKey* key_event)
+{
+ unsigned int key = 0;
+ gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode,
+ (GdkModifierType)key_event->state,
+ 0, &key, nullptr, nullptr, nullptr);
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ _adjusting = false;
+ return true;
+ }
+
+ return false;
+}
+
+sigc::signal<void> ColorWheel::signal_color_changed()
+{
+ return _signal_color_changed;
+}
+
+/* HSL Color Wheel */
+void ColorWheelHSL::setRgb(double r, double g, double b, bool overrideHue)
+{
+ double min = std::min({r, g, b});
+ double max = std::max({r, g, b});
+
+ _values[2] = max;
+
+ if (min == max) {
+ if (overrideHue) {
+ _values[0] = 0.0;
+ }
+ } else {
+ if (max == r) {
+ _values[0] = ((g - b) / (max - min) ) / 6.0;
+ } else if (max == g) {
+ _values[0] = ((b - r) / (max - min) + 2) / 6.0;
+ } else {
+ _values[0] = ((r - g) / (max - min) + 4) / 6.0;
+ }
+
+ if (_values[0] < 0.0) {
+ _values[0] += 1.0;
+ }
+ }
+
+ if (max == 0) {
+ _values[1] = 0;
+ } else {
+ _values[1] = (max - min) / max;
+ }
+}
+
+void ColorWheelHSL::getRgb(double *r, double *g, double *b) const
+{
+ guint32 color = getRgb();
+ *r = ((color & 0x00ff0000) >> 16) / 255.0;
+ *g = ((color & 0x0000ff00) >> 8) / 255.0;
+ *b = ((color & 0x000000ff) ) / 255.0;
+}
+
+void ColorWheelHSL::getRgbV(double *rgb) const
+{
+ guint32 color = getRgb();
+ rgb[0] = ((color & 0x00ff0000) >> 16) / 255.0;
+ rgb[1] = ((color & 0x0000ff00) >> 8) / 255.0;
+ rgb[2] = ((color & 0x000000ff) ) / 255.0;
+}
+
+guint32 ColorWheelHSL::getRgb() const
+{
+ return hsv_to_rgb(_values[0], _values[1], _values[2]);
+}
+
+void ColorWheelHSL::getHsl(double *h, double *s, double *l) const
+{
+ getValues(h, s, l);
+}
+
+bool ColorWheelHSL::on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const cx = width/2;
+ int const cy = height/2;
+
+ int const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, width);
+
+ int focus_line_width;
+ int focus_padding;
+ get_style_property("focus-line-width", focus_line_width);
+ get_style_property("focus-padding", focus_padding);
+
+ // Paint ring
+ guint32* buffer_ring = g_new (guint32, height * stride / 4);
+ double r_max = std::min(width, height)/2.0 - 2 * (focus_line_width + focus_padding);
+ double r_min = r_max * (1.0 - _ring_width);
+ double r2_max = (r_max+2) * (r_max+2); // Must expand a bit to avoid edge effects.
+ double r2_min = (r_min-2) * (r_min-2); // Must shrink a bit to avoid edge effects.
+
+ for (int i = 0; i < height; ++i) {
+ guint32* p = buffer_ring + i * width;
+ double dy = (cy - i);
+ for (int j = 0; j < width; ++j) {
+ double dx = (j - cx);
+ double r2 = dx * dx + dy * dy;
+ if (r2 < r2_min || r2 > r2_max) {
+ *p++ = 0; // Save calculation time.
+ } else {
+ double angle = atan2 (dy, dx);
+ if (angle < 0.0) {
+ angle += 2.0 * M_PI;
+ }
+ double hue = angle/(2.0 * M_PI);
+
+ *p++ = hsv_to_rgb(hue, 1.0, 1.0);
+ }
+ }
+ }
+
+ Cairo::RefPtr<::Cairo::ImageSurface> source_ring =
+ ::Cairo::ImageSurface::create((unsigned char *)buffer_ring,
+ Cairo::FORMAT_RGB24,
+ width, height, stride);
+
+ cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+
+ // Paint line on ring in source (so it gets clipped by stroke).
+ double l = 0.0;
+ guint32 color_on_ring = hsv_to_rgb(_values[0], 1.0, 1.0);
+ if (luminance(color_on_ring) < 0.5) l = 1.0;
+
+ Cairo::RefPtr<::Cairo::Context> cr_source_ring = ::Cairo::Context::create(source_ring);
+ cr_source_ring->set_source_rgb(l, l, l);
+
+ cr_source_ring->move_to (cx, cy);
+ cr_source_ring->line_to (cx + cos(_values[0] * M_PI * 2.0) * r_max+1,
+ cy - sin(_values[0] * M_PI * 2.0) * r_max+1);
+ cr_source_ring->stroke();
+
+ // Paint with ring surface, clipping to ring.
+ cr->save();
+ cr->set_source(source_ring, 0, 0);
+ cr->set_line_width (r_max - r_min);
+ cr->begin_new_path();
+ cr->arc(cx, cy, (r_max + r_min)/2.0, 0, 2.0 * M_PI);
+ cr->stroke();
+ cr->restore();
+
+ g_free(buffer_ring);
+
+ // Draw focus
+ if (has_focus() && _focus_on_ring) {
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->render_focus(cr, 0, 0, width, height);
+ }
+
+ // Paint triangle.
+ /* The triangle is painted by first finding color points on the
+ * edges of the triangle at the same y value via linearly
+ * interpolating between corner values, and then interpolating along
+ * x between the those edge points. The interpolation is in sRGB
+ * space which leads to a complicated mapping between x/y and
+ * saturation/value. This was probably done to remove the need to
+ * convert between HSV and RGB for each pixel.
+ * Black corner: v = 0, s = 1
+ * White corner: v = 1, s = 0
+ * Color corner; v = 1, s = 1
+ */
+ const int padding = 3; // Avoid edge artifacts.
+ double x0, y0, x1, y1, x2, y2;
+ _triangle_corners(x0, y0, x1, y1, x2, y2);
+ guint32 color0 = hsv_to_rgb(_values[0], 1.0, 1.0);
+ guint32 color1 = hsv_to_rgb(_values[0], 1.0, 0.0);
+ guint32 color2 = hsv_to_rgb(_values[0], 0.0, 1.0);
+
+ ColorPoint p0 (x0, y0, color0);
+ ColorPoint p1 (x1, y1, color1);
+ ColorPoint p2 (x2, y2, color2);
+
+ // Reorder so we paint from top down.
+ if (p1.y > p2.y) {
+ std::swap(p1, p2);
+ }
+
+ if (p0.y > p2.y) {
+ std::swap(p0, p2);
+ }
+
+ if (p0.y > p1.y) {
+ std::swap(p0, p1);
+ }
+
+ guint32* buffer_triangle = g_new(guint32, height * stride / 4);
+
+ for (int y = 0; y < height; ++y) {
+ guint32 *p = buffer_triangle + y * (stride / 4);
+
+ if (p0.y <= y+padding && y-padding < p2.y) {
+
+ // Get values on side at position y.
+ ColorPoint side0;
+ double y_inter = std::clamp(static_cast<double>(y), p0.y, p2.y);
+ if (y < p1.y) {
+ side0 = lerp(p0, p1, p0.y, p1.y, y_inter);
+ } else {
+ side0 = lerp(p1, p2, p1.y, p2.y, y_inter);
+ }
+ ColorPoint side1 = lerp(p0, p2, p0.y, p2.y, y_inter);
+
+ // side0 should be on left
+ if (side0.x > side1.x) {
+ std::swap (side0, side1);
+ }
+
+ int x_start = std::max(0, int(side0.x));
+ int x_end = std::min(int(side1.x), width);
+
+ for (int x = 0; x < width; ++x) {
+ if (x <= x_start) {
+ *p++ = side0.get_color();
+ } else if (x < x_end) {
+ *p++ = lerp(side0, side1, side0.x, side1.x, x).get_color();
+ } else {
+ *p++ = side1.get_color();
+ }
+ }
+ }
+ }
+
+ // add vertical padding to each side separately
+ ColorPoint temp_point = lerp(p0, p1, p0.x, p1.x, (p0.x + p1.x) / 2.0);
+ bool pad_upwards = _is_in_triangle(temp_point.x, temp_point.y + 1);
+ draw_vertical_padding(p0, p1, padding, pad_upwards, buffer_triangle, height, stride / 4);
+
+ temp_point = lerp(p0, p2, p0.x, p2.x, (p0.x + p2.x) / 2.0);
+ pad_upwards = _is_in_triangle(temp_point.x, temp_point.y + 1);
+ draw_vertical_padding(p0, p2, padding, pad_upwards, buffer_triangle, height, stride / 4);
+
+ temp_point = lerp(p1, p2, p1.x, p2.x, (p1.x + p2.x) / 2.0);
+ pad_upwards = _is_in_triangle(temp_point.x, temp_point.y + 1);
+ draw_vertical_padding(p1, p2, padding, pad_upwards, buffer_triangle, height, stride / 4);
+
+ Cairo::RefPtr<::Cairo::ImageSurface> source_triangle =
+ ::Cairo::ImageSurface::create((unsigned char *)buffer_triangle,
+ Cairo::FORMAT_RGB24,
+ width, height, stride);
+
+ // Paint with triangle surface, clipping to triangle.
+ cr->save();
+ cr->set_source(source_triangle, 0, 0);
+ cr->move_to(p0.x, p0.y);
+ cr->line_to(p1.x, p1.y);
+ cr->line_to(p2.x, p2.y);
+ cr->close_path();
+ cr->fill();
+ cr->restore();
+
+ g_free(buffer_triangle);
+
+ // Draw marker
+ double mx = x1 + (x2-x1) * _values[2] + (x0-x2) * _values[1] * _values[2];
+ double my = y1 + (y2-y1) * _values[2] + (y0-y2) * _values[1] * _values[2];
+
+ double a = 0.0;
+ guint32 color_at_marker = getRgb();
+ if (luminance(color_at_marker) < 0.5) a = 1.0;
+
+ cr->set_source_rgb(a, a, a);
+ cr->begin_new_path();
+ cr->arc(mx, my, 4, 0, 2 * M_PI);
+ cr->stroke();
+
+ // Draw focus
+ if (has_focus() && !_focus_on_ring) {
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->render_focus(cr, mx-4, my-4, 8, 8); // This doesn't seem to work.
+ cr->set_line_width(0.5);
+ cr->set_source_rgb(1-a, 1-a, 1-a);
+ cr->begin_new_path();
+ cr->arc(mx, my, 7, 0, 2 * M_PI);
+ cr->stroke();
+ }
+
+ return true;
+}
+
+bool ColorWheelHSL::on_focus(Gtk::DirectionType direction)
+{
+ // In forward direction, focus passes from no focus to ring focus to triangle
+ // focus to no focus.
+ if (!has_focus()) {
+ _focus_on_ring = (direction == Gtk::DIR_TAB_FORWARD);
+ grab_focus();
+ return true;
+ }
+
+ // Already have focus
+ bool keep_focus = false;
+
+ switch (direction) {
+ case Gtk::DIR_UP:
+ case Gtk::DIR_LEFT:
+ case Gtk::DIR_TAB_BACKWARD:
+ if (!_focus_on_ring) {
+ _focus_on_ring = true;
+ keep_focus = true;
+ }
+ break;
+
+ case Gtk::DIR_DOWN:
+ case Gtk::DIR_RIGHT:
+ case Gtk::DIR_TAB_FORWARD:
+ if (_focus_on_ring) {
+ _focus_on_ring = false;
+ keep_focus = true;
+ }
+ break;
+ }
+
+ queue_draw(); // Update focus indicators.
+
+ return keep_focus;
+}
+
+void ColorWheelHSL::_set_from_xy(double const x, double const y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ double const cx = width/2.0;
+ double const cy = height/2.0;
+ double const r = std::min(cx, cy) * (1 - _ring_width);
+
+ // We calculate RGB value under the cursor by rotating the cursor
+ // and triangle by the hue value and looking at position in the
+ // now right pointing triangle.
+ double angle = _values[0] * 2 * M_PI;
+ double sin = std::sin(angle);
+ double cos = std::cos(angle);
+ double xp = ((x - cx) * cos - (y - cy) * sin) / r;
+ double yp = ((x - cx) * sin + (y - cy) * cos) / r;
+
+ double xt = lerp(0.0, 1.0, -0.5, 1.0, xp);
+ xt = std::clamp(xt, 0.0, 1.0);
+
+ double dy = (1-xt) * std::cos(M_PI / 6.0);
+ double yt = lerp(0.0, 1.0, -dy, dy, yp);
+ yt = std::clamp(yt, 0.0, 1.0);
+
+ ColorPoint c0(0, 0, yt, yt, yt); // Grey point along base.
+ ColorPoint c1(0, 0, hsv_to_rgb(_values[0], 1, 1)); // Hue point at apex
+ ColorPoint c = lerp(c0, c1, 0, 1, xt);
+
+ setRgb(c.r, c.g, c.b, false); // Don't override previous hue.
+}
+
+bool ColorWheelHSL::_is_in_ring(double x, double y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const cx = width/2;
+ int const cy = height/2;
+
+ int focus_line_width;
+ int focus_padding;
+ get_style_property("focus-line-width", focus_line_width);
+ get_style_property("focus-padding", focus_padding);
+
+ double r_max = std::min( width, height)/2.0 - 2 * (focus_line_width + focus_padding);
+ double r_min = r_max * (1.0 - _ring_width);
+ double r2_max = r_max * r_max;
+ double r2_min = r_min * r_min;
+
+ double dx = x - cx;
+ double dy = y - cy;
+ double r2 = dx * dx + dy * dy;
+
+ return (r2_min < r2 && r2 < r2_max);
+}
+
+bool ColorWheelHSL::_is_in_triangle(double x, double y)
+{
+ double x0, y0, x1, y1, x2, y2;
+ _triangle_corners(x0, y0, x1, y1, x2, y2);
+
+ double det = (x2 - x1) * (y0 - y1) - (y2 - y1) * (x0 - x1);
+ double s = ((x - x1) * (y0 - y1) - (y - y1) * (x0 - x1)) / det;
+ double t = ((x2 - x1) * (y - y1) - (y2 - y1) * (x - x1)) / det;
+
+ return (s >= 0.0 && t >= 0.0 && s + t <= 1.0);
+}
+
+void ColorWheelHSL::_update_triangle_color(double x, double y)
+{
+ _set_from_xy(x, y);
+ _signal_color_changed.emit();
+ queue_draw();
+}
+
+void ColorWheelHSL::_triangle_corners(double &x0, double &y0, double &x1, double &y1,
+ double &x2, double &y2)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const cx = width / 2;
+ int const cy = height / 2;
+
+ int focus_line_width;
+ int focus_padding;
+ get_style_property("focus-line-width", focus_line_width);
+ get_style_property("focus-padding", focus_padding);
+
+ double r_max = std::min(width, height) / 2.0 - 2 * (focus_line_width + focus_padding);
+ double r_min = r_max * (1.0 - _ring_width);
+
+ double angle = _values[0] * 2.0 * M_PI;
+
+ x0 = cx + std::cos(angle) * r_min;
+ y0 = cy - std::sin(angle) * r_min;
+ x1 = cx + std::cos(angle + 2.0 * M_PI / 3.0) * r_min;
+ y1 = cy - std::sin(angle + 2.0 * M_PI / 3.0) * r_min;
+ x2 = cx + std::cos(angle + 4.0 * M_PI / 3.0) * r_min;
+ y2 = cy - std::sin(angle + 4.0 * M_PI / 3.0) * r_min;
+}
+
+void ColorWheelHSL::_update_ring_color(double x, double y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ double cx = width / 2.0;
+ double cy = height / 2.0;
+ double angle = -atan2(y - cy, x - cx);
+
+ if (angle < 0) {
+ angle += 2.0 * M_PI;
+ }
+ _values[0] = angle / (2.0 * M_PI);
+
+ queue_draw();
+ _signal_color_changed.emit();
+}
+
+bool ColorWheelHSL::on_button_press_event(GdkEventButton* event)
+{
+ // Seat is automatically grabbed.
+ double x = event->x;
+ double y = event->y;
+
+ if (_is_in_ring(x, y) ) {
+ _adjusting = true;
+ _mode = DragMode::HUE;
+ grab_focus();
+ _focus_on_ring = true;
+ _update_ring_color(x, y);
+ return true;
+ } else if (_is_in_triangle(x, y)) {
+ _adjusting = true;
+ _mode = DragMode::SATURATION_VALUE;
+ grab_focus();
+ _focus_on_ring = false;
+ _update_triangle_color(x, y);
+ return true;
+ }
+
+ return false;
+}
+
+bool ColorWheelHSL::on_button_release_event(GdkEventButton */*event*/)
+{
+ _mode = DragMode::NONE;
+
+ _adjusting = false;
+ return true;
+}
+
+bool ColorWheelHSL::on_motion_notify_event(GdkEventMotion* event)
+{
+ if (!_adjusting) { return false; }
+
+ double x = event->x;
+ double y = event->y;
+
+ if (_mode == DragMode::HUE) {
+ _update_ring_color(x, y);
+ return true;
+ } else if (_mode == DragMode::SATURATION_VALUE) {
+ _update_triangle_color(x, y);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool ColorWheelHSL::on_key_press_event(GdkEventKey* key_event)
+{
+ bool consumed = false;
+
+ unsigned int key = 0;
+ gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode,
+ (GdkModifierType)key_event->state,
+ 0, &key, nullptr, nullptr, nullptr);
+
+ double x0, y0, x1, y1, x2, y2;
+ _triangle_corners(x0, y0, x1, y1, x2, y2);
+
+ // Marker position
+ double mx = x1 + (x2-x1) * _values[2] + (x0-x2) * _values[1] * _values[2];
+ double my = y1 + (y2-y1) * _values[2] + (y0-y2) * _values[1] * _values[2];
+
+ double const delta_hue = 2.0 / 360.0;
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (_focus_on_ring) {
+ _values[0] += delta_hue;
+ } else {
+ my -= 1.0;
+ _set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ if (_focus_on_ring) {
+ _values[0] -= delta_hue;
+ } else {
+ my += 1.0;
+ _set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (_focus_on_ring) {
+ _values[0] += delta_hue;
+ } else {
+ mx -= 1.0;
+ _set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (_focus_on_ring) {
+ _values[0] -= delta_hue;
+ } else {
+ mx += 1.0;
+ _set_from_xy(mx, my);
+ }
+ consumed = true;
+ break;
+ }
+
+ if (consumed) {
+ if (_values[0] >= 1.0) {
+ _values[0] -= 1.0;
+ } else if (_values[0] < 0.0) {
+ _values[0] += 1.0;
+ }
+
+ _signal_color_changed.emit();
+ queue_draw();
+ }
+
+ return consumed;
+}
+
+/* HSLuv Color Wheel */
+ColorWheelHSLuv::ColorWheelHSLuv()
+ : _scale(1.0)
+ , _cache_width(0)
+ , _cache_height(0)
+ , _square_size(1)
+{
+ _picker_geometry = new PickerGeometry;
+ setHsluv(0.0, 100.0, 50.0);
+}
+
+ColorWheelHSLuv::~ColorWheelHSLuv()
+{
+ delete _picker_geometry;
+}
+
+void ColorWheelHSLuv::setRgb(double r, double g, double b, bool /*overrideHue*/)
+{
+ double h, s ,l;
+ Hsluv::rgb_to_hsluv(r, g, b, &h, &s, &l);
+
+ setHue(h);
+ setSaturation(s);
+ setLightness(l);
+}
+
+void ColorWheelHSLuv::getRgb(double *r, double *g, double *b) const
+{
+ Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], r, g, b);
+}
+
+void ColorWheelHSLuv::getRgbV(double *rgb) const
+{
+ Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], &rgb[0], &rgb[1], &rgb[2]);
+}
+
+guint32 ColorWheelHSLuv::getRgb() const
+{
+ double r, g, b;
+ Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2], &r, &g, &b);
+
+ return (
+ (static_cast<guint32>(r * 255.0) << 16) |
+ (static_cast<guint32>(g * 255.0) << 8) |
+ (static_cast<guint32>(b * 255.0) )
+ );
+}
+
+void ColorWheelHSLuv::setHsluv(double h, double s, double l)
+{
+ setHue(h);
+ setSaturation(s);
+ setLightness(l);
+}
+
+void ColorWheelHSLuv::setLightness(double l)
+{
+ _values[2] = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS);
+
+ // Update polygon
+ get_picker_geometry(_picker_geometry, _values[2]);
+ _scale = OUTER_CIRCLE_RADIUS / _picker_geometry->outerCircleRadius;
+ _update_polygon();
+
+ queue_draw();
+}
+
+void ColorWheelHSLuv::getHsluv(double *h, double *s, double *l) const
+{
+ getValues(h, s, l);
+}
+
+bool ColorWheelHSLuv::on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const cx = width/2;
+ int const cy = height/2;
+
+ double const resize = std::min(width, height) / static_cast<double>(SIZE);
+
+ int const marginX = std::max(0.0, (width - height) / 2.0);
+ int const marginY = std::max(0.0, (height - width) / 2.0);
+
+ std::vector<Point> shapePointsPixel =
+ to_pixel_coordinate(_picker_geometry->vertices, _scale, resize);
+ for (Point &point : shapePointsPixel) {
+ point.x += marginX;
+ point.y += marginY;
+ }
+
+ // Detect if we're at the top or bottom vertex of the color space
+ bool is_vertex = (_values[2] < VERTEX_EPSILON || _values[2] > 100.0 - VERTEX_EPSILON);
+
+ cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+
+ if (width > _square_size && height > _square_size) {
+ if (_cache_width != width || _cache_height != height) {
+ _update_polygon();
+ }
+ if (!is_vertex) {
+ // Paint with surface, clipping to polygon
+ cr->save();
+ cr->set_source(_surface_polygon, 0, 0);
+ cr->move_to(shapePointsPixel[0].x, shapePointsPixel[0].y);
+ for (size_t i = 1; i < shapePointsPixel.size(); i++) {
+ Point const &point = shapePointsPixel[i];
+ cr->line_to(point.x, point.y);
+ }
+ cr->close_path();
+ cr->fill();
+ cr->restore();
+ }
+ }
+
+ // Draw foreground
+
+ // Outer circle
+ std::vector<double> dashes{OUTER_CIRCLE_DASH_SIZE};
+ cr->set_line_width(1);
+ // White dashes
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->set_dash(dashes, 0.0);
+ cr->begin_new_path();
+ cr->arc(cx, cy, _scale * resize * _picker_geometry->outerCircleRadius, 0, 2 * M_PI);
+ cr->stroke();
+ // Black dashes
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->set_dash(dashes, OUTER_CIRCLE_DASH_SIZE);
+ cr->begin_new_path();
+ cr->arc(cx, cy, _scale * resize * _picker_geometry->outerCircleRadius, 0, 2 * M_PI);
+ cr->stroke();
+ cr->unset_dash();
+
+ // Contrast
+ double a = (_values[2] > 70.0) ? 0.0 : 1.0;
+ cr->set_source_rgb(a, a, a);
+
+ // Pastel circle
+ double const innerRadius = is_vertex ? 0.01 : _picker_geometry->innerCircleRadius;
+ cr->set_line_width(2);
+ cr->begin_new_path();
+ cr->arc(cx, cy, _scale * resize * innerRadius, 0, 2 * M_PI);
+ cr->stroke();
+
+ // Center
+ cr->begin_new_path();
+ cr->arc(cx, cy, 2, 0, 2 * M_PI);
+ cr->fill();
+
+ // Draw marker
+ double l, u, v;
+ Hsluv::hsluv_to_luv(_values[0], _values[1], _values[2], &l, &u, &v);
+ Point mp = to_pixel_coordinate(Point(u, v), _scale, resize);
+ mp.x += marginX;
+ mp.y += marginY;
+
+ cr->set_line_width(2);
+ cr->begin_new_path();
+ cr->arc(mp.x, mp.y, 4, 0, 2 * M_PI);
+ cr->stroke();
+
+ // Focus
+ if (has_focus()) {
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->render_focus(cr, mp.x-4, mp.y-4, 8, 8);
+
+ cr->set_line_width(0.5);
+ cr->set_source_rgb(1-a, 1-a, 1-a);
+ cr->begin_new_path();
+ cr->arc(mp.x, mp.y, 7, 0, 2 * M_PI);
+ cr->stroke();
+ }
+
+ return true;
+}
+
+void ColorWheelHSLuv::_set_from_xy(double const x, double const y)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ double const resize = std::min(width, height) / static_cast<double>(SIZE);
+
+ int const margin_x = std::max(0.0, (width - height) / 2.0);
+ int const margin_y = std::max(0.0, (height - width) / 2.0);
+
+ Point const p = from_pixel_coordinate(Point(
+ x - margin_x,
+ y - margin_y
+ ), _scale, resize);
+
+ double h, s, l;
+ Hsluv::luv_to_hsluv(_values[2], p.x, p.y, &h, &s, &l);
+
+ setHue(h);
+ setSaturation(s);
+
+ _signal_color_changed.emit();
+ queue_draw();
+}
+
+void ColorWheelHSLuv::_update_polygon()
+{
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+ int const size = std::min(width, height);
+
+ // Update square size
+ _square_size = std::max(1, static_cast<int>(size/50));
+
+ if (width < _square_size || height < _square_size) {
+ return;
+ }
+
+ _cache_width = width;
+ _cache_height = height;
+
+ double const resize = size / static_cast<double>(SIZE);
+
+ int const marginX = std::max(0.0, (width - height) / 2.0);
+ int const marginY = std::max(0.0, (height - width) / 2.0);
+
+ std::vector<Point> shapePointsPixel =
+ to_pixel_coordinate(_picker_geometry->vertices, _scale, resize);
+
+ for (Point &point : shapePointsPixel) {
+ point.x += marginX;
+ point.y += marginY;
+ }
+
+ std::vector<double> xs;
+ std::vector<double> ys;
+
+ for (Point const &point : shapePointsPixel) {
+ xs.emplace_back(point.x);
+ ys.emplace_back(point.y);
+ }
+
+ int const xmin = std::floor(*std::min_element(xs.begin(), xs.end()) / _square_size);
+ int const ymin = std::floor(*std::min_element(ys.begin(), ys.end()) / _square_size);
+ int const xmax = std::ceil(*std::max_element(xs.begin(), xs.end()) / _square_size);
+ int const ymax = std::ceil(*std::max_element(ys.begin(), ys.end()) / _square_size);
+
+ int const stride =
+ Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, width);
+
+ _buffer_polygon.resize(height * stride / 4);
+ std::vector<guint32> buffer_line;
+ buffer_line.resize(stride / 4);
+
+ ColorPoint clr;
+
+ // Set the color of each pixel/square
+ for (int y = ymin; y < ymax; y++) {
+ for (int x = xmin; x < xmax; x++) {
+ double px = x * _square_size;
+ double py = y * _square_size;
+ Point point = from_pixel_coordinate(Point(
+ px + (_square_size / 2) - marginX,
+ py + (_square_size / 2) - marginY
+ ), _scale, resize);
+
+ double r, g ,b;
+ Hsluv::luv_to_rgb(_values[2], point.x, point.y, &r, &g, &b); // safe with _values[2] == 0
+
+ r = std::clamp(r, 0.0, 1.0);
+ g = std::clamp(g, 0.0, 1.0);
+ b = std::clamp(b, 0.0, 1.0);
+
+ clr.set_color(r, g, b);
+
+ guint32 *p = buffer_line.data() + (x * _square_size);
+ for (int i = 0; i < _square_size; i++) {
+ p[i] = clr.get_color();
+ }
+ }
+
+ // Copy the line buffer to the surface buffer
+ int Y = y * _square_size;
+ for (int i = 0; i < _square_size; i++) {
+ guint32 *t = _buffer_polygon.data() + (Y + i) * (stride / 4);
+ std::memcpy(t, buffer_line.data(), stride);
+ }
+ }
+
+ _surface_polygon = ::Cairo::ImageSurface::create(
+ reinterpret_cast<unsigned char *>(_buffer_polygon.data()),
+ Cairo::FORMAT_RGB24, width, height, stride
+ );
+}
+
+bool ColorWheelHSLuv::on_button_press_event(GdkEventButton* event)
+{
+ double const x = event->x;
+ double const y = event->y;
+
+ Gtk::Allocation allocation = get_allocation();
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ int const margin_x = std::max(0.0, (width - height) / 2.0);
+ int const margin_y = std::max(0.0, (height - width) / 2.0);
+ int const size = std::min(width, height);
+
+ if (x > margin_x && x < (margin_x+size) && y > margin_y && y < (margin_y+size)) {
+ _adjusting = true;
+ grab_focus();
+ _set_from_xy(x, y);
+ return true;
+ }
+
+ return false;
+}
+
+bool ColorWheelHSLuv::on_button_release_event(GdkEventButton */*event*/)
+{
+ _adjusting = false;
+ return true;
+}
+
+bool ColorWheelHSLuv::on_motion_notify_event(GdkEventMotion* event)
+{
+ if (!_adjusting) { return false; }
+
+ double x = event->x;
+ double y = event->y;
+
+ _set_from_xy(x, y);
+
+ return true;
+}
+
+bool ColorWheelHSLuv::on_key_press_event(GdkEventKey* key_event)
+{
+ bool consumed = false;
+
+ unsigned int key = 0;
+ gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(),
+ key_event->hardware_keycode,
+ (GdkModifierType)key_event->state,
+ 0, &key, nullptr, nullptr, nullptr);
+
+ // Get current point
+ double l, u, v;
+ Hsluv::hsluv_to_luv(_values[0], _values[1], _values[2], &l, &u, &v);
+
+ double const marker_move = 1.0 / _scale;
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ v += marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ v -= marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ u -= marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ u += marker_move;
+ consumed = true;
+ break;
+ }
+
+ if (consumed) {
+ double h, s, l;
+ Hsluv::luv_to_hsluv(_values[2], u, v, &h, &s, &l);
+
+ setHue(h);
+ setSaturation(s);
+
+ _adjusting = true;
+ _signal_color_changed.emit();
+ queue_draw();
+ }
+
+ return consumed;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/* ColorPoint */
+ColorPoint::ColorPoint()
+ : x(0), y(0), r(0), g(0), b(0)
+{}
+
+ColorPoint::ColorPoint(double x, double y, double r, double g, double b)
+ : x(x), y(y), r(r), g(g), b(b)
+{}
+
+ColorPoint::ColorPoint(double x, double y, guint color)
+ : x(x)
+ , y(y)
+ , r(((color & 0xff0000) >> 16) / 255.0)
+ , g(((color & 0x00ff00) >> 8) / 255.0)
+ , b(((color & 0x0000ff) ) / 255.0)
+{}
+
+guint32 ColorPoint::get_color()
+{
+ return (static_cast<int>(r * 255) << 16 |
+ static_cast<int>(g * 255) << 8 |
+ static_cast<int>(b * 255)
+ );
+};
+
+void ColorPoint::set_color(double red, double green, double blue)
+{
+ r = red;
+ g = green;
+ b = blue;
+};
+
+/* Point */
+Point::Point() : x(0), y(0)
+{}
+
+Point::Point(double x, double y) : x(x), y(y)
+{}
+
+/* Intersection */
+Intersection::Intersection() : line1(0), line2(0)
+{}
+
+Intersection::Intersection(int line1, int line2, Point intersectionPoint,
+ double intersectionPointAngle, double relativeAngle)
+ : line1(line1)
+ , line2(line2)
+ , intersectionPoint(intersectionPoint)
+ , intersectionPointAngle(intersectionPointAngle)
+ , relativeAngle(relativeAngle)
+{}
+
+/* FIXME: replace these utility functions with calls into lib2geom */
+/* Utility functions */
+static Point intersect_line_line(Line a, Line b)
+{
+ double x = (a.intercept - b.intercept) / (b.slope - a.slope);
+ double y = a.slope * x + a.intercept;
+ return {x, y};
+}
+
+static double distance_from_origin(Point point)
+{
+ return std::sqrt(std::pow(point.x, 2) + std::pow(point.y, 2));
+}
+
+static double distance_line_from_origin(Line line)
+{
+ // https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
+ return std::abs(line.intercept) / std::sqrt(std::pow(line.slope, 2) + 1);
+}
+
+static double angle_from_origin(Point point)
+{
+ return std::atan2(point.y, point.x);
+}
+
+static double normalize_angle(double angle)
+{
+ double m = 2 * M_PI;
+ return std::fmod(std::fmod(angle, m) + m, m);
+}
+
+static double lerp(double v0, double v1, double t0, double t1, double t)
+{
+ double s = 0;
+
+ if (t0 != t1) {
+ s = (t - t0) / (t1 - t0);
+ }
+
+ return (1.0 - s) * v0 + s * v1;
+}
+
+static ColorPoint lerp(ColorPoint const &v0, ColorPoint const &v1, double t0, double t1,
+ double t)
+{
+ double x = lerp(v0.x, v1.x, t0, t1, t);
+ double y = lerp(v0.y, v1.y, t0, t1, t);
+ double r = lerp(v0.r, v1.r, t0, t1, t);
+ double g = lerp(v0.g, v1.g, t0, t1, t);
+ double b = lerp(v0.b, v1.b, t0, t1, t);
+
+ return ColorPoint(x, y, r, g, b);
+}
+
+/**
+ * @param h Hue. Between 0 and 1.
+ * @param s Saturation. Between 0 and 1.
+ * @param v Value. Between 0 and 1.
+ */
+static guint32 hsv_to_rgb(double h, double s, double v)
+{
+ h = std::clamp(h, 0.0, 1.0);
+ s = std::clamp(s, 0.0, 1.0);
+ v = std::clamp(v, 0.0, 1.0);
+
+ double r = v;
+ double g = v;
+ double b = v;
+
+ if (s != 0.0) {
+ if (h == 1.0) h = 0.0;
+ h *= 6.0;
+
+ double f = h - (int)h;
+ double p = v * (1.0 - s);
+ double q = v * (1.0 - s * f);
+ double t = v * (1.0 - s * (1.0 - f));
+
+ switch (static_cast<int>(h)) {
+ case 0: r = v; g = t; b = p; break;
+ case 1: r = q; g = v; b = p; break;
+ case 2: r = p; g = v; b = t; break;
+ case 3: r = p; g = q; b = v; break;
+ case 4: r = t; g = p; b = v; break;
+ case 5: r = v; g = p; b = q; break;
+ default: g_assert_not_reached();
+ }
+ }
+
+ guint32 rgb = (static_cast<int>(floor(r * 255 + 0.5)) << 16) |
+ (static_cast<int>(floor(g * 255 + 0.5)) << 8) |
+ (static_cast<int>(floor(b * 255 + 0.5)) );
+ return rgb;
+}
+
+double luminance(guint32 color)
+{
+ double r = ((color & 0xff0000) >> 16) / 255.0;
+ double g = ((color & 0xff00) >> 8) / 255.0;
+ double b = ((color & 0xff) ) / 255.0;
+ return (r * 0.2125 + g * 0.7154 + b * 0.0721);
+}
+
+/**
+ * Convert the vertice of the in gamut color polygon (Luv) to pixel coordinates.
+ *
+ * @param point The point in Luv coordinates.
+ * @param scale Zoom amount to fit polygon to outer circle.
+ * @param resize Zoom amount to fit wheel in widget.
+ */
+static Point to_pixel_coordinate(Point const &point, double scale, double resize)
+{
+ return Point(
+ point.x * scale * resize + (SIZE * resize / 2.0),
+ (SIZE * resize / 2.0) - point.y * scale * resize
+ );
+}
+
+/**
+ * Convert a point in pixels on the widget to Luv coordinates.
+ *
+ * @param point The point in pixel coordinates.
+ * @param scale Zoom amount to fit polygon to outer circle.
+ * @param resize Zoom amount to fit wheel in widget.
+ */
+static Point from_pixel_coordinate(Point const &point, double scale, double resize)
+{
+ return Point(
+ (point.x - (SIZE * resize / 2.0)) / (scale * resize),
+ ((SIZE * resize / 2.0) - point.y) / (scale * resize)
+ );
+}
+
+/**
+ * @overload
+ * @param point A vector of points in Luv coordinates.
+ * @param scale Zoom amount to fit polygon to outer circle.
+ * @param resize Zoom amount to fit wheel in widget.
+ */
+static std::vector<Point> to_pixel_coordinate(std::vector<Point> const &points,
+ double scale, double resize)
+{
+ std::vector<Point> result;
+
+ for (Point const &p : points) {
+ result.emplace_back(to_pixel_coordinate(p, scale, resize));
+ }
+
+ return result;
+}
+
+/**
+ * Paints padding for an edge of the triangle,
+ * using the (vertically) closest point.
+ *
+ * @param p0 A corner of the triangle. Not the same corner as p1
+ * @param p1 A corner of the triangle. Not the same corner as p0
+ * @param padding The height of the padding
+ * @param pad_upwards True if padding is above the line
+ * @param buffer Array that the triangle is painted to
+ * @param height Height of buffer
+ * @param stride Stride of buffer
+*/
+void draw_vertical_padding(ColorPoint p0, ColorPoint p1, int padding, bool pad_upwards,
+ guint32 *buffer, int height, int stride)
+{
+ // skip if horizontal padding is more accurate, e.g. if the edge is vertical
+ double gradient = (p1.y - p0.y) / (p1.x - p0.x);
+ if (std::abs(gradient) > 1.0) {
+ return;
+ }
+
+ double min_y = std::min(p0.y, p1.y);
+ double max_y = std::max(p0.y, p1.y);
+
+ double min_x = std::min(p0.x, p1.x);
+ double max_x = std::max(p0.x, p1.x);
+
+ // go through every point on the line
+ for (int y = min_y; y <= max_y; ++y) {
+ double start_x = lerp(p0, p1, p0.y, p1.y, std::clamp(static_cast<double>(y), min_y,
+ max_y)).x;
+ double end_x = lerp(p0, p1, p0.y, p1.y, std::clamp(static_cast<double>(y) + 1, min_y,
+ max_y)).x;
+ if (start_x > end_x) {
+ std::swap(start_x, end_x);
+ }
+
+ guint32 *p = buffer + y * stride;
+ p += static_cast<int>(start_x);
+ for (int x = start_x; x <= end_x; ++x) {
+ // get the color at this point on the line
+ ColorPoint point = lerp(p0, p1, p0.x, p1.x, std::clamp(static_cast<double>(x),
+ min_x, max_x));
+ // paint the padding vertically above or below this point
+ for (int offset = 0; offset <= padding; ++offset) {
+ if (pad_upwards && (point.y - offset) >= 0) {
+ *(p - (offset * stride)) = point.get_color();
+ } else if (!pad_upwards && (point.y + offset) < height) {
+ *(p + (offset * stride)) = point.get_color();
+ }
+ }
+ ++p;
+ }
+ }
+}
+
+static void Inkscape::UI::Widget::get_picker_geometry(PickerGeometry *pickerGeometry, double lightness)
+{
+ // Add a lambda to avoid overlapping intersections
+ lightness = std::clamp(lightness + 0.01, 0.1, 99.9);
+
+ // Array of lines
+ std::array<Line, 6> const lines = Hsluv::getBounds(lightness);
+ int numLines = lines.size();
+ double outerCircleRadius = 0.0;
+
+ // Find the line closest to origin
+ int closestIndex2 = -1;
+ double closestLineDistance = -1;
+
+ for (int i = 0; i < numLines; i++) {
+ double d = distance_line_from_origin(lines[i]);
+ if (closestLineDistance < 0 || d < closestLineDistance) {
+ closestLineDistance = d;
+ closestIndex2 = i;
+ }
+ }
+
+ Line closestLine = lines[closestIndex2];
+ Line perpendicularLine (0 - (1 / closestLine.slope), 0.0);
+
+ Point intersectionPoint = intersect_line_line(closestLine,
+ perpendicularLine);
+ double startingAngle = angle_from_origin(intersectionPoint);
+
+ std::vector<Intersection> intersections;
+ double intersectionPointAngle;
+ double relativeAngle;
+
+ for (int i = 0; i < numLines - 1; i++) {
+ for (int j = i + 1; j < numLines; j++) {
+ intersectionPoint = intersect_line_line(lines[i], lines[j]);
+ intersectionPointAngle = angle_from_origin(intersectionPoint);
+ relativeAngle = normalize_angle(
+ intersectionPointAngle - startingAngle);
+ intersections.emplace_back(i, j, intersectionPoint,
+ intersectionPointAngle, relativeAngle);
+ }
+ }
+
+ std::sort(intersections.begin(), intersections.end(),
+ [] (Intersection const &lhs, Intersection const &rhs)
+ {
+ return lhs.relativeAngle >= rhs.relativeAngle;
+ });
+
+ std::vector<Line> orderedLines;
+ std::vector<Point> orderedVertices;
+ std::vector<double> orderedAngles;
+
+ int nextIndex;
+ double intersectionPointDistance;
+ int currentIndex = closestIndex2;
+
+ for (Intersection intersection : intersections) {
+ nextIndex = -1;
+
+ if (intersection.line1 == currentIndex) {
+ nextIndex = intersection.line2;
+ }
+ else if (intersection.line2 == currentIndex) {
+ nextIndex = intersection.line1;
+ }
+
+ if (nextIndex > -1) {
+ currentIndex = nextIndex;
+
+ orderedLines.emplace_back(lines[nextIndex]);
+ orderedVertices.emplace_back(intersection.intersectionPoint);
+ orderedAngles.emplace_back(intersection.intersectionPointAngle);
+
+ intersectionPointDistance = distance_from_origin(intersection.intersectionPoint);
+ if (intersectionPointDistance > outerCircleRadius) {
+ outerCircleRadius = intersectionPointDistance;
+ }
+ }
+ }
+
+ pickerGeometry->lines = orderedLines;
+ pickerGeometry->vertices = orderedVertices;
+ pickerGeometry->angles = orderedAngles;
+ pickerGeometry->outerCircleRadius = outerCircleRadius;
+ pickerGeometry->innerCircleRadius = closestLineDistance;
+}
+/*
+ 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/ui/widget/ink-color-wheel.h b/src/ui/widget/ink-color-wheel.h
new file mode 100644
index 0000000..57af896
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.h
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * @file
+ * HSLuv color wheel widget, based on the web implementation at
+ * https://www.hsluv.org
+ *
+ * Authors:
+ * Tavmjong Bah
+ * Massinissa Derriche <massinissa.derriche@gmail.com>
+ *
+ * Copyright (C) 2018, 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INK_COLORWHEEL_H
+#define INK_COLORWHEEL_H
+
+#include <gtkmm.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+struct PickerGeometry;
+
+/**
+ * @class ColorWheel
+ */
+class ColorWheel : public Gtk::DrawingArea
+{
+public:
+ ColorWheel();
+
+ virtual void setRgb(double r, double g, double b, bool overrideHue = true);
+ virtual void getRgb(double *r, double *g, double *b) const;
+ virtual void getRgbV(double *rgb) const;
+ virtual guint32 getRgb() const;
+
+ void setHue(double h);
+ void setSaturation(double s);
+ virtual void setLightness(double l);
+ void getValues(double *a, double *b, double *c) const;
+
+ bool isAdjusting() const { return _adjusting; }
+
+protected:
+ virtual void _set_from_xy(double const x, double const y);
+
+ double _values[3];
+ bool _adjusting;
+
+private:
+ // Callbacks
+ bool on_key_release_event(GdkEventKey* key_event) override;
+
+ // Signals
+public:
+ sigc::signal<void> signal_color_changed();
+
+protected:
+ sigc::signal<void> _signal_color_changed;
+};
+
+/**
+ * @class ColorWheelHSL
+ */
+class ColorWheelHSL : public ColorWheel
+{
+public:
+ void setRgb(double r, double g, double b, bool overrideHue = true) override;
+ void getRgb(double *r, double *g, double *b) const override;
+ void getRgbV(double *rgb) const override;
+ guint32 getRgb() const override;
+
+ void getHsl(double *h, double *s, double *l) const;
+
+protected:
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+ bool on_focus(Gtk::DirectionType direction) override;
+
+private:
+ void _set_from_xy(double const x, double const y) override;
+ bool _is_in_ring(double x, double y);
+ bool _is_in_triangle(double x, double y);
+ void _update_triangle_color(double x, double y);
+ void _update_ring_color(double x, double y);
+ void _triangle_corners(double& x0, double& y0, double& x1, double& y1, double& x2,
+ double& y2);
+
+ enum class DragMode {
+ NONE,
+ HUE,
+ SATURATION_VALUE
+ };
+
+ double _ring_width = 0.2;
+ DragMode _mode = DragMode::NONE;
+ bool _focus_on_ring = true;
+
+ // Callbacks
+ bool on_button_press_event(GdkEventButton* event) override;
+ bool on_button_release_event(GdkEventButton* event) override;
+ bool on_motion_notify_event(GdkEventMotion* event) override;
+ bool on_key_press_event(GdkEventKey* key_event) override;
+};
+
+/**
+ * @class ColorWheelHSLuv
+ */
+class ColorWheelHSLuv : public ColorWheel
+{
+public:
+ ColorWheelHSLuv();
+ ~ColorWheelHSLuv() override;
+
+ void setRgb(double r, double g, double b, bool overrideHue = true) override;
+ void getRgb(double *r, double *g, double *b) const override;
+ void getRgbV(double *rgb) const override;
+ guint32 getRgb() const override;
+
+ void setHsluv(double h, double s, double l);
+ void setLightness(double l) override;
+
+ void getHsluv(double *h, double *s, double *l) const;
+
+protected:
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+
+private:
+ void _set_from_xy(double const x, double const y) override;
+ void _update_polygon();
+
+ // Callbacks
+ bool on_button_press_event(GdkEventButton* event) override;
+ bool on_button_release_event(GdkEventButton* event) override;
+ bool on_motion_notify_event(GdkEventMotion* event) override;
+ bool on_key_press_event(GdkEventKey* key_event) override;
+
+ double _scale;
+ PickerGeometry *_picker_geometry;
+ std::vector<guint32> _buffer_polygon;
+ Cairo::RefPtr<::Cairo::ImageSurface> _surface_polygon;
+ int _cache_width, _cache_height;
+ int _square_size;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INK_COLORWHEEL_HSLUV_H
diff --git a/src/ui/widget/ink-flow-box.cpp b/src/ui/widget/ink-flow-box.cpp
new file mode 100644
index 0000000..86eb8a0
--- /dev/null
+++ b/src/ui/widget/ink-flow-box.cpp
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkflow-box widget.
+ * This widget allow pack widgets in a flowbox with a controller to show-hide
+ *
+ * Author:
+ * Jabier Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2018 Jabier Arraiza
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "preferences.h"
+#include "ui/icon-loader.h"
+#include "ui/widget/ink-flow-box.h"
+#include <gtkmm/adjustment.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+InkFlowBox::InkFlowBox(const gchar *name)
+: Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+{
+ set_name(name);
+ this->pack_start(_controller, false, false, 0);
+ this->pack_start(_flowbox, true, true, 0);
+ _flowbox.set_activate_on_single_click(true);
+ Gtk::ToggleButton *tbutton = new Gtk::ToggleButton("", false);
+ tbutton->set_always_show_image(true);
+ _flowbox.set_selection_mode(Gtk::SelectionMode::SELECTION_NONE);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), false);
+ tbutton->set_active(prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true));
+ Glib::ustring iconname = "object-unlocked";
+ if (tbutton->get_active()) {
+ iconname = "object-locked";
+ }
+ tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU));
+ tbutton->signal_toggled().connect(
+ sigc::bind<Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_global_toggle), tbutton));
+ _controller.pack_start(*tbutton);
+ tbutton->hide();
+ tbutton->set_no_show_all(true);
+ showing = 0;
+ sensitive = true;
+}
+
+InkFlowBox::~InkFlowBox() = default;
+
+Glib::ustring InkFlowBox::getPrefsPath(gint pos)
+{
+ return Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/index_") + std::to_string(pos);
+}
+
+bool InkFlowBox::on_filter(Gtk::FlowBoxChild *child)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool(getPrefsPath(child->get_index()), true)) {
+ showing++;
+ return true;
+ }
+ return false;
+}
+
+void InkFlowBox::on_toggle(gint pos, Gtk::ToggleButton *tbutton)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool global = prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true);
+ if (global && sensitive) {
+ sensitive = false;
+ bool active = true;
+ for (auto child : tbutton->get_parent()->get_children()) {
+ if (tbutton != child) {
+ static_cast<Gtk::ToggleButton *>(child)->set_active(active);
+ active = false;
+ }
+ }
+ prefs->setBool(getPrefsPath(pos), true);
+ tbutton->set_active(true);
+ sensitive = true;
+ } else {
+ prefs->setBool(getPrefsPath(pos), tbutton->get_active());
+ }
+ showing = 0;
+ _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter));
+ _flowbox.set_max_children_per_line(showing);
+}
+
+void InkFlowBox::on_global_toggle(Gtk::ToggleButton *tbutton)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), tbutton->get_active());
+ sensitive = true;
+ if (tbutton->get_active()) {
+ sensitive = false;
+ bool active = true;
+ for (auto child : tbutton->get_parent()->get_children()) {
+ if (tbutton != child) {
+ static_cast<Gtk::ToggleButton *>(child)->set_active(active);
+ active = false;
+ }
+ }
+ }
+ Glib::ustring iconname = "object-unlocked";
+ if (tbutton->get_active()) {
+ iconname = "object-locked";
+ }
+ tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU));
+ sensitive = true;
+}
+
+void InkFlowBox::insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Gtk::ToggleButton *tbutton = new Gtk::ToggleButton(label, true);
+ tbutton->set_active(prefs->getBool(getPrefsPath(pos), active));
+ tbutton->signal_toggled().connect(
+ sigc::bind<gint, Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_toggle), pos, tbutton));
+ _controller.pack_start(*tbutton);
+ tbutton->show();
+ prefs->setBool(getPrefsPath(pos), prefs->getBool(getPrefsPath(pos), active));
+ widget->set_size_request(minwidth, -1);
+ _flowbox.insert(*widget, pos);
+ showing = 0;
+ _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter));
+ _flowbox.set_max_children_per_line(showing);
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/ink-flow-box.h b/src/ui/widget/ink-flow-box.h
new file mode 100644
index 0000000..9db1d21
--- /dev/null
+++ b/src/ui/widget/ink-flow-box.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkflow-box widget.
+ * This widget allow pack widgets in a flowbox with a controller to show-hide
+ *
+ * Author:
+ * Jabier Arraiza <jabier.arraiza@marker.es>
+ *
+ * Copyright (C) 2018 Jabier Arraiza
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_INK_FLOW_BOX_H
+#define INKSCAPE_INK_FLOW_BOX_H
+
+#include <gtkmm/actionbar.h>
+#include <gtkmm/box.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/flowboxchild.h>
+#include <gtkmm/togglebutton.h>
+#include <sigc++/signal.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A flowbox widget with filter controller for dialogs.
+ */
+
+class InkFlowBox : public Gtk::Box {
+ public:
+ InkFlowBox(const gchar *name);
+ ~InkFlowBox() override;
+ void insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth);
+ void on_toggle(gint pos, Gtk::ToggleButton *tbutton);
+ void on_global_toggle(Gtk::ToggleButton *tbutton);
+ void set_visible(gint pos, bool visible);
+ bool on_filter(Gtk::FlowBoxChild *child);
+ Glib::ustring getPrefsPath(gint pos);
+ /**
+ * Construct a InkFlowBox.
+ */
+
+ private:
+ Gtk::FlowBox _flowbox;
+ Gtk::ActionBar _controller;
+ gint showing;
+ bool sensitive;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_INK_FLOW_BOX_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/ui/widget/ink-ruler.cpp b/src/ui/widget/ink-ruler.cpp
new file mode 100644
index 0000000..535eddf
--- /dev/null
+++ b/src/ui/widget/ink-ruler.cpp
@@ -0,0 +1,476 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget.
+ *
+ * Copyright (C) 2019 Tavmjong Bah
+ *
+ * Rewrite of the 'C' ruler code which came originally from Gimp.
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#include "ink-ruler.h"
+
+#include <iostream>
+#include <cmath>
+
+#include "util/units.h"
+
+struct SPRulerMetric
+{
+ gdouble ruler_scale[16];
+ gint subdivide[5];
+};
+
+// Ruler metric for general use.
+static SPRulerMetric const ruler_metric_general = {
+ { 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000 },
+ { 1, 5, 10, 50, 100 }
+};
+
+// Ruler metric for inch scales.
+static SPRulerMetric const ruler_metric_inches = {
+ { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 },
+ { 1, 2, 4, 8, 16 }
+};
+
+// Half width of pointer triangle.
+static double half_width = 5.0;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Ruler::Ruler(Gtk::Orientation orientation)
+ : _orientation(orientation)
+ , _backing_store(nullptr)
+ , _lower(0)
+ , _upper(1000)
+ , _max_size(1000)
+ , _unit(nullptr)
+ , _backing_store_valid(false)
+ , _rect()
+ , _position(0)
+{
+ set_name("InkRuler");
+
+ set_events(Gdk::POINTER_MOTION_MASK |
+ Gdk::BUTTON_PRESS_MASK | // For guide creation
+ Gdk::BUTTON_RELEASE_MASK );
+
+ signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback));
+ set_no_show_all();
+}
+
+// Set display unit for ruler.
+void
+Ruler::set_unit(Inkscape::Util::Unit const *unit)
+{
+ if (_unit != unit) {
+ _unit = unit;
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+// Set range for ruler, update ticks.
+void
+Ruler::set_range(const double& lower, const double& upper)
+{
+ if (_lower != lower || _upper != upper) {
+
+ _lower = lower;
+ _upper = upper;
+ _max_size = _upper - _lower;
+ if (_max_size == 0) {
+ _max_size = 1;
+ }
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+// Add a widget (i.e. canvas) to monitor. Note, we don't worry about removing this signal as
+// our ruler is tied tightly to the canvas, if one is destroyed, so is the other.
+void
+Ruler::add_track_widget(Gtk::Widget& widget)
+{
+ widget.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback), false); // false => connect first
+}
+
+
+// Draws marker in response to motion events from canvas. Position is defined in ruler pixel
+// coordinates. The routine assumes that the ruler is the same width (height) as the canvas. If
+// not, one could use Gtk::Widget::translate_coordinates() to convert the coordinates.
+bool
+Ruler::draw_marker_callback(GdkEventMotion* motion_event)
+{
+ double position = 0;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ position = motion_event->x;
+ } else {
+ position = motion_event->y;
+ }
+
+ if (position != _position) {
+
+ _position = position;
+
+ // Find region to repaint (old and new marker positions).
+ Cairo::RectangleInt new_rect = marker_rect();
+ Cairo::RefPtr<Cairo::Region> region = Cairo::Region::create(new_rect);
+ region->do_union(_rect);
+
+ // Queue repaint
+ queue_draw_region(region);
+
+ _rect = new_rect;
+ }
+
+ return false;
+}
+
+
+// Find smallest dimension of ruler based on font size.
+void
+Ruler::size_request (Gtk::Requisition& requisition) const
+{
+ // Get border size
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+
+ // Get font size
+ Pango::FontDescription font = style_context->get_font(get_state_flags());
+ int font_size = font.get_size();
+ if (!font.get_size_is_absolute()) {
+ font_size /= Pango::SCALE;
+ }
+
+ int size = 2 + font_size * 2.0; // Room for labels and ticks
+
+ int width = border.get_left() + border.get_right();
+ int height = border.get_top() + border.get_bottom();
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ width += 1;
+ height += size;
+ } else {
+ width += size;
+ height += 1;
+ }
+
+ // Only valid for orientation in question (smallest dimension)!
+ requisition.width = width;
+ requisition.height = height;
+}
+
+void
+Ruler::get_preferred_width_vfunc (int& minimum_width, int& natural_width) const
+{
+ Gtk::Requisition requisition;
+ size_request(requisition);
+ minimum_width = natural_width = requisition.width;
+}
+
+void
+Ruler::get_preferred_height_vfunc (int& minimum_height, int& natural_height) const
+{
+ Gtk::Requisition requisition;
+ size_request(requisition);
+ minimum_height = natural_height = requisition.height;
+}
+
+// Update backing store when scale changes.
+// Note: in principle, there should not be a border (ruler ends should match canvas ends). If there
+// is a border, we calculate tick position ignoring border width at ends of ruler but move the
+// ticks and position marker inside the border.
+bool
+Ruler::draw_scale(const::Cairo::RefPtr<::Cairo::Context>& cr_in)
+{
+
+ // Get style information
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+ Gdk::RGBA foreground = style_context->get_color(get_state_flags());
+
+ Pango::FontDescription font = style_context->get_font(get_state_flags());
+ int font_size = font.get_size();
+ if (!font.get_size_is_absolute()) {
+ font_size /= Pango::SCALE;
+ }
+
+ Gtk::Allocation allocation = get_allocation();
+ int awidth = allocation.get_width();
+ int aheight = allocation.get_height();
+
+ // if (allocation.get_x() != 0 || allocation.get_y() != 0) {
+ // std::cerr << "Ruler::draw_scale: maybe we do have to handle allocation x and y! "
+ // << " x: " << allocation.get_x() << " y: " << allocation.get_y() << std::endl;
+ // }
+
+ // Create backing store (need surface_in to get scale factor correct).
+ Cairo::RefPtr<Cairo::Surface> surface_in = cr_in->get_target();
+ _backing_store = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, awidth, aheight);
+
+ // Get context
+ Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(_backing_store);
+ style_context->render_background(cr, 0, 0, awidth, aheight);
+
+ cr->set_line_width(1.0);
+ Gdk::Cairo::set_source_rgba(cr, foreground);
+
+ // Ruler size (only smallest dimension used later).
+ int rwidth = awidth - (border.get_left() + border.get_right());
+ int rheight = aheight - (border.get_top() + border.get_bottom());
+
+ // Draw bottom/right line of ruler
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->rectangle( 0, aheight - border.get_bottom() - 1, awidth, 1);
+ } else {
+ cr->rectangle( awidth - border.get_left() - 1, 0, 1, aheight);
+ std::swap(awidth, aheight);
+ std::swap(rwidth, rheight);
+ }
+ cr->fill();
+
+ // From here on, awidth is the longest dimension of the ruler, rheight is the shortest.
+
+ // Figure out scale. Largest ticks must be far enough apart to fit largest text in vertical ruler.
+ // We actually require twice the distance.
+ unsigned int scale = std::ceil (_max_size); // Largest number
+ Glib::ustring scale_text = std::to_string(scale);
+ unsigned int digits = scale_text.length() + 1; // Add one for negative sign.
+ unsigned int minimum = digits * font_size * 2;
+
+ double pixels_per_unit = awidth/_max_size; // pixel per distance
+
+ SPRulerMetric ruler_metric = ruler_metric_general;
+ if (_unit == Inkscape::Util::unit_table.getUnit("in")) {
+ ruler_metric = ruler_metric_inches;
+ }
+
+ unsigned scale_index;
+ for (scale_index = 0; scale_index < G_N_ELEMENTS (ruler_metric.ruler_scale)-1; ++scale_index) {
+ if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) > minimum) break;
+ }
+
+ // Now we find out what is the subdivide index for the closest ticks we can draw
+ unsigned divide_index;
+ for (divide_index = 0; divide_index < G_N_ELEMENTS (ruler_metric.subdivide)-1; ++divide_index) {
+ if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) < 5 * ruler_metric.subdivide[divide_index+1]) break;
+ }
+
+ // We'll loop over all ticks.
+ double pixels_per_tick = pixels_per_unit *
+ ruler_metric.ruler_scale[scale_index] / ruler_metric.subdivide[divide_index];
+
+ double units_per_tick = pixels_per_tick/pixels_per_unit;
+ double ticks_per_unit = 1.0/units_per_tick;
+
+ // Find first and last ticks
+ int start = 0;
+ int end = 0;
+ if (_lower < _upper) {
+ start = std::floor (_lower * ticks_per_unit);
+ end = std::ceil (_upper * ticks_per_unit);
+ } else {
+ start = std::floor (_upper * ticks_per_unit);
+ end = std::ceil (_lower * ticks_per_unit);
+ }
+
+ // std::cout << " start: " << start
+ // << " end: " << end
+ // << " pixels_per_unit: " << pixels_per_unit
+ // << " pixels_per_tick: " << pixels_per_tick
+ // << std::endl;
+
+ // Loop over all ticks
+ for (int i = start; i < end+1; ++i) {
+
+ // Position of tick (add 0.5 to center tick on pixel).
+ double position = std::floor(i*pixels_per_tick - _lower*pixels_per_unit) + 0.5;
+
+ // Height of tick
+ int height = rheight;
+ for (int j = divide_index; j > 0; --j) {
+ if (i%ruler_metric.subdivide[j] == 0) break;
+ height = height/2 + 1;
+ }
+
+ // Draw text for major ticks.
+ if (i%ruler_metric.subdivide[divide_index] == 0) {
+
+ int label_value = std::round(i*units_per_tick);
+ Glib::ustring label = std::to_string(label_value);
+
+ Glib::RefPtr<Pango::Layout> layout = create_pango_layout("");
+ layout->set_font_description(font);
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ layout->set_text(label);
+ cr->move_to (position+4, border.get_top()); // Magic number offset lables
+ layout->show_in_cairo_context(cr);
+ } else {
+ cr->move_to (border.get_left(), position);
+ int n = 0;
+ for (char const &c : label) {
+ std::string s(1, c);
+ layout->set_text(s);
+ int text_width;
+ int text_height;
+ layout->get_pixel_size(text_width, text_height);
+ cr->move_to(border.get_left() + (aheight-text_width)/2.0 - 1,
+ position + n*0.8*text_height + 1);
+ layout->show_in_cairo_context(cr);
+ ++n;
+ }
+ // Glyphs are not centered in vertical text... should specify fixed width numbers.
+ // Glib::RefPtr<Pango::Context> context = layout->get_context();
+ // if (_orientation == Gtk::ORIENTATION_VERTICAL) {
+ // context->set_base_gravity(Pango::GRAVITY_EAST);
+ // context->set_gravity_hint(Pango::GRAVITY_HINT_STRONG);
+ // cr->move_to(...)
+ // cr->save();
+ // cr->rotate(M_PI_2);
+ // layout->show_in_cairo_context(cr);
+ // cr->restore();
+ // }
+ }
+ }
+
+ // Draw ticks
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(position, rheight + border.get_top() - height);
+ cr->line_to(position, rheight + border.get_top());
+ } else {
+ cr->move_to(rheight + border.get_left() - height, position);
+ cr->line_to(rheight + border.get_left(), position);
+ }
+ cr->stroke();
+ }
+
+ _backing_store_valid = true;
+
+ return true;
+}
+
+// Draw position marker, we use doubles here.
+void
+Ruler::draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr)
+{
+
+ // Get style information
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+ Gdk::RGBA foreground = style_context->get_color(get_state_flags());
+
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ // Temp (to verify our redraw rectangle encloses position marker).
+ // Cairo::RectangleInt rect = marker_rect();
+ // cr->set_source_rgb(0, 1.0, 0);
+ // cr->rectangle (rect.x, rect.y, rect.width, rect.height);
+ // cr->fill();
+
+ Gdk::Cairo::set_source_rgba(cr, foreground);
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ double offset = aheight - border.get_bottom();
+ cr->move_to(_position, offset);
+ cr->line_to(_position - half_width, offset - half_width);
+ cr->line_to(_position + half_width, offset - half_width);
+ cr->close_path();
+ } else {
+ double offset = awidth - border.get_right();
+ cr->move_to(offset, _position);
+ cr->line_to(offset - half_width, _position - half_width);
+ cr->line_to(offset - half_width, _position + half_width);
+ cr->close_path();
+ }
+ cr->fill();
+}
+
+// This is a pixel aligned integer rectangle that encloses the position marker. Used to define the
+// redraw area.
+Cairo::RectangleInt
+Ruler::marker_rect()
+{
+ // Get border size
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gtk::Border border = style_context->get_border(get_state_flags());
+
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ int rwidth = awidth - border.get_left() - border.get_right();
+ int rheight = aheight - border.get_top() - border.get_bottom();
+
+ Cairo::RectangleInt rect;
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = 0;
+ rect.height = 0;
+
+ // Find size of rectangle to enclose triangle.
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ rect.x = std::floor(_position - half_width);
+ rect.y = std::floor(border.get_top() + rheight - half_width);
+ rect.width = std::ceil(half_width * 2.0 + 1);
+ rect.height = std::ceil(half_width);
+ } else {
+ rect.x = std::floor(border.get_left() + rwidth - half_width);
+ rect.y = std::floor(_position - half_width);
+ rect.width = std::ceil(half_width);
+ rect.height = std::ceil(half_width * 2.0 + 1);
+ }
+
+ return rect;
+}
+
+// Draw the ruler using the tick backing store.
+bool
+Ruler::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) {
+
+ if (!_backing_store_valid) {
+ draw_scale (cr);
+ }
+
+ cr->set_source (_backing_store, 0, 0);
+ cr->paint();
+
+ draw_marker (cr);
+
+ return true;
+}
+
+// Update ruler on style change (font-size, etc.)
+void
+Ruler::on_style_updated() {
+
+ Gtk::DrawingArea::on_style_updated();
+
+ _backing_store_valid = false; // If font-size changed we need to regenerate store.
+
+ queue_resize();
+ queue_draw();
+}
+
+} // 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/ui/widget/ink-ruler.h b/src/ui/widget/ink-ruler.h
new file mode 100644
index 0000000..f34960b
--- /dev/null
+++ b/src/ui/widget/ink-ruler.h
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget.
+ *
+ * Copyright (C) 2019 Tavmjong Bah
+ *
+ * The contents of this file may be used under the GNU General Public License Version 2 or later.
+ *
+ */
+
+#ifndef INK_RULER_H
+#define INK_RULER_H
+
+/* Rewrite of the C Ruler. */
+
+#include <gtkmm.h>
+
+namespace Inkscape {
+namespace Util {
+class Unit;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Ruler : public Gtk::DrawingArea
+{
+public:
+ Ruler(Gtk::Orientation orientation);
+
+ void set_unit(Inkscape::Util::Unit const *unit);
+ void set_range(const double& lower, const double& upper);
+
+ void add_track_widget(Gtk::Widget& widget);
+ bool draw_marker_callback(GdkEventMotion* motion_event);
+
+ void size_request(Gtk::Requisition& requisition) const;
+ void get_preferred_width_vfunc( int& minimum_width, int& natural_width ) const override;
+ void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override;
+
+protected:
+ bool draw_scale(const Cairo::RefPtr<::Cairo::Context>& cr);
+ void draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr);
+ Cairo::RectangleInt marker_rect();
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+ void on_style_updated() override;
+
+private:
+ Gtk::Orientation _orientation;
+
+ Inkscape::Util::Unit const* _unit;
+ double _lower;
+ double _upper;
+ double _position;
+ double _max_size;
+
+ bool _backing_store_valid;
+
+ Cairo::RefPtr<::Cairo::Surface> _backing_store;
+ Cairo::RectangleInt _rect;
+};
+
+} // Namespace Inkscape
+}
+}
+#endif // INK_RULER_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/ui/widget/ink-spinscale.cpp b/src/ui/widget/ink-spinscale.cpp
new file mode 100644
index 0000000..b977d21
--- /dev/null
+++ b/src/ui/widget/ink-spinscale.cpp
@@ -0,0 +1,288 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/** \file
+ A widget that allows entering a numerical value either by
+ clicking/dragging on a custom Gtk::Scale or by using a
+ Gtk::SpinButton. The custom Gtk::Scale differs from the stock
+ Gtk::Scale in that it includes a label to save space and has a
+ "slow dragging" mode triggered by the Alt key.
+*/
+
+#include "ink-spinscale.h"
+#include <gdkmm/general.h>
+#include <gdkmm/cursor.h>
+#include <gdkmm/event.h>
+
+#include <gtkmm/spinbutton.h>
+
+#include <gdk/gdk.h>
+
+#include <iostream>
+#include <utility>
+
+InkScale::InkScale(Glib::RefPtr<Gtk::Adjustment> adjustment, Gtk::SpinButton* spinbutton)
+ : Glib::ObjectBase("InkScale")
+ , parent_type(adjustment)
+ , _spinbutton(spinbutton)
+ , _dragging(false)
+ , _drag_start(0)
+ , _drag_offset(0)
+{
+ set_name("InkScale");
+ // std::cout << "GType name: " << G_OBJECT_TYPE_NAME(gobj()) << std::endl;
+}
+
+void
+InkScale::set_label(Glib::ustring label) {
+ _label = label;
+}
+
+bool
+InkScale::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) {
+
+ Gtk::Range::on_draw(cr);
+
+ // Get SpinButton style info...
+ auto style_spin = _spinbutton->get_style_context();
+ auto state_spin = style_spin->get_state();
+ Gdk::RGBA text_color = style_spin->get_color( state_spin );
+
+ // Create Pango layout.
+ auto layout_label = create_pango_layout(_label);
+ layout_label->set_ellipsize( Pango::ELLIPSIZE_END );
+ layout_label->set_width(PANGO_SCALE * get_width());
+
+ // Get y location of SpinButton text (to match vertical position of SpinButton text).
+ int x, y;
+ _spinbutton->get_layout_offsets(x, y);
+ auto btn_alloc = _spinbutton->get_allocation();
+ auto alloc = get_allocation();
+ y += btn_alloc.get_y() - alloc.get_y();
+
+ // Fill widget proportional to value.
+ double fraction = get_fraction();
+
+ // Get through rectangle and clipping point for text.
+ Gdk::Rectangle slider_area = get_range_rect();
+ double clip_text_x = slider_area.get_x() + slider_area.get_width() * fraction;
+
+ // Render text in normal text color.
+ cr->save();
+ cr->rectangle(clip_text_x, 0, get_width(), get_height());
+ cr->clip();
+ Gdk::Cairo::set_source_rgba(cr, text_color);
+ //cr->set_source_rgba(0, 0, 0, 1);
+ cr->move_to(5, y );
+ layout_label->show_in_cairo_context(cr);
+ cr->restore();
+
+ // Render text, clipped, in white over bar (TODO: use same color as SpinButton progress bar).
+ cr->save();
+ cr->rectangle(0, 0, clip_text_x, get_height());
+ cr->clip();
+ cr->set_source_rgba(1, 1, 1, 1);
+ cr->move_to(5, y);
+ layout_label->show_in_cairo_context(cr);
+ cr->restore();
+
+ return true;
+}
+
+bool
+InkScale::on_button_press_event(GdkEventButton* button_event) {
+
+ if (! (button_event->state & GDK_MOD1_MASK) ) {
+ bool constrained = button_event->state & GDK_CONTROL_MASK;
+ set_adjustment_value(button_event->x, constrained);
+ }
+
+ // Dragging must be initialized after any adjustment due to button press.
+ _dragging = true;
+ _drag_start = button_event->x;
+ _drag_offset = get_width() * get_fraction();
+
+ return true;
+}
+
+bool
+InkScale::on_button_release_event(GdkEventButton* button_event) {
+
+ _dragging = false;
+ return true;
+}
+
+bool
+InkScale::on_motion_notify_event(GdkEventMotion* motion_event) {
+
+ double x = motion_event->x;
+
+ if (_dragging) {
+
+ if (! (motion_event->state & GDK_MOD1_MASK) ) {
+ // Absolute change
+ bool constrained = motion_event->state & GDK_CONTROL_MASK;
+ set_adjustment_value(x, constrained);
+ } else {
+ // Relative change
+ double xx = (_drag_offset + (x - _drag_start) * 0.1);
+ set_adjustment_value(xx);
+ }
+ return true;
+ }
+
+ if (! (motion_event->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) {
+
+ auto display = get_display();
+ auto cursor = Gdk::Cursor::create(display, Gdk::SB_UP_ARROW);
+ // Get Gdk::window (not Gtk::window).. set cursor for entire window.
+ // Would need to unset with leave event.
+ // get_window()->set_cursor( cursor );
+
+ // Can't see how to do this the C++ way since GdkEventMotion
+ // is a structure with a C window member. There is a gdkmm
+ // wrapping function for Gdk::EventMotion but only in unstable.
+
+ // If the cursor theme doesn't have the `sb_up_arrow` cursor then the pointer will be NULL
+ if (cursor)
+ gdk_window_set_cursor( motion_event->window, cursor->gobj() );
+ }
+
+ return false;
+}
+
+double
+InkScale::get_fraction() {
+
+ Glib::RefPtr<Gtk::Adjustment> adjustment = get_adjustment();
+ double upper = adjustment->get_upper();
+ double lower = adjustment->get_lower();
+ double value = adjustment->get_value();
+ double fraction = (value - lower)/(upper - lower);
+
+ return fraction;
+}
+
+void
+InkScale::set_adjustment_value(double x, bool constrained) {
+
+ Glib::RefPtr<Gtk::Adjustment> adjustment = get_adjustment();
+ double upper = adjustment->get_upper();
+ double lower = adjustment->get_lower();
+ double range = upper-lower;
+
+ Gdk::Rectangle slider_area = get_range_rect();
+ double fraction = (x - slider_area.get_x()) / (double)slider_area.get_width();
+ double value = fraction * range + lower;
+
+ if (constrained) {
+ // TODO: do we want preferences for (any of) these?
+ if (fmod(range+1,16) == 0) {
+ value = round(value/16) * 16;
+ } else if (range >= 1000 && fmod(upper,100) == 0) {
+ value = round(value/100) * 100;
+ } else if (range >= 100 && fmod(upper,10) == 0) {
+ value = round(value/10) * 10;
+ } else if (range > 20 && fmod(upper,5) == 0) {
+ value = round(value/5) * 5;
+ } else if (range > 2) {
+ value = round(value);
+ } else if (range <= 2) {
+ value = round(value*10) / 10;
+ }
+ }
+
+ adjustment->set_value( value );
+}
+
+/*******************************************************************/
+
+InkSpinScale::InkSpinScale(double value, double lower,
+ double upper, double step_increment,
+ double page_increment, double page_size)
+{
+ set_name("InkSpinScale");
+
+ g_assert (upper - lower > 0);
+
+ _adjustment = Gtk::Adjustment::create(value,
+ lower,
+ upper,
+ step_increment,
+ page_increment,
+ page_size);
+
+ _spinbutton = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton>(_adjustment));
+ _spinbutton->set_valign(Gtk::ALIGN_CENTER);
+ _spinbutton->set_numeric();
+ _spinbutton->signal_key_release_event().connect(sigc::mem_fun(*this,&InkSpinScale::on_key_release_event),false);
+
+ _scale = Gtk::manage(new InkScale(_adjustment, _spinbutton));
+ _scale->set_draw_value(false);
+
+ pack_end( *_spinbutton, Gtk::PACK_SHRINK );
+ pack_end( *_scale, Gtk::PACK_EXPAND_WIDGET );
+}
+
+InkSpinScale::InkSpinScale(Glib::RefPtr<Gtk::Adjustment> adjustment)
+ : _adjustment(std::move(adjustment))
+{
+ set_name("InkSpinScale");
+
+ g_assert (_adjustment->get_upper() - _adjustment->get_lower() > 0);
+
+ _spinbutton = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton>(_adjustment));
+ _spinbutton->set_numeric();
+
+ _scale = Gtk::manage(new InkScale(_adjustment, _spinbutton));
+ _scale->set_draw_value(false);
+
+ pack_end( *_spinbutton, Gtk::PACK_SHRINK );
+ pack_end( *_scale, Gtk::PACK_EXPAND_WIDGET );
+}
+
+void
+InkSpinScale::set_label(Glib::ustring label) {
+ _scale->set_label(label);
+}
+
+void
+InkSpinScale::set_digits(int digits) {
+ _spinbutton->set_digits(digits);
+}
+
+int
+InkSpinScale::get_digits() const {
+ return _spinbutton->get_digits();
+}
+
+void
+InkSpinScale::set_focus_widget(GtkWidget * focus_widget) {
+ _focus_widget = focus_widget;
+}
+
+// Return focus to canvas.
+bool
+InkSpinScale::on_key_release_event(GdkEventKey* key_event) {
+
+ switch (key_event->keyval) {
+ case GDK_KEY_Escape:
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ if (_focus_widget) {
+ gtk_widget_grab_focus( _focus_widget );
+ }
+ }
+ break;
+ }
+
+ return false;
+}
diff --git a/src/ui/widget/ink-spinscale.h b/src/ui/widget/ink-spinscale.h
new file mode 100644
index 0000000..4f07b27
--- /dev/null
+++ b/src/ui/widget/ink-spinscale.h
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INK_SPINSCALE_H
+#define INK_SPINSCALE_H
+
+/*
+ * Authors:
+ * Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Copyright (C) 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/**
+ A widget that allows entering a numerical value either by
+ clicking/dragging on a custom Gtk::Scale or by using a
+ Gtk::SpinButton. The custom Gtk::Scale differs from the stock
+ Gtk::Scale in that it includes a label to save space and has a
+ "slow-dragging" mode triggered by the Alt key.
+*/
+
+#include <glibmm/ustring.h>
+
+#include <gtkmm/box.h>
+#include <gtkmm/scale.h>
+
+#include "scrollprotected.h"
+
+namespace Gtk {
+ class SpinButton;
+}
+
+class InkScale : public Inkscape::UI::Widget::ScrollProtected<Gtk::Scale>
+{
+ using parent_type = ScrollProtected<Gtk::Scale>;
+
+ public:
+ InkScale(Glib::RefPtr<Gtk::Adjustment>, Gtk::SpinButton* spinbutton);
+ ~InkScale() override = default;;
+
+ void set_label(Glib::ustring label);
+
+ bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override;
+
+ protected:
+
+ bool on_button_press_event(GdkEventButton* button_event) override;
+ bool on_button_release_event(GdkEventButton* button_event) override;
+ bool on_motion_notify_event(GdkEventMotion* motion_event) override;
+
+ private:
+
+ double get_fraction();
+ void set_adjustment_value(double x, bool constrained = false);
+
+ Gtk::SpinButton * _spinbutton; // Needed to get placement/text color.
+ Glib::ustring _label;
+
+ bool _dragging;
+ double _drag_start;
+ double _drag_offset;
+};
+
+class InkSpinScale : public Gtk::Box
+{
+ public:
+
+ // Create an InkSpinScale with a new adjustment.
+ InkSpinScale(double value,
+ double lower,
+ double upper,
+ double step_increment = 1,
+ double page_increment = 10,
+ double page_size = 0);
+
+ // Create an InkSpinScale with a preexisting adjustment.
+ InkSpinScale(Glib::RefPtr<Gtk::Adjustment>);
+
+ ~InkSpinScale() override = default;;
+
+ void set_label(Glib::ustring label);
+ void set_digits(int digits);
+ int get_digits() const;
+ void set_focus_widget(GtkWidget *focus_widget);
+ Glib::RefPtr<Gtk::Adjustment> get_adjustment() { return _adjustment; };
+
+ protected:
+
+ InkScale* _scale;
+ Gtk::SpinButton* _spinbutton;
+ Glib::RefPtr<Gtk::Adjustment> _adjustment;
+ GtkWidget* _focus_widget = nullptr;
+
+ bool on_key_release_event(GdkEventKey* key_event) override;
+
+ private:
+
+};
+
+#endif // INK_SPINSCALE_H
diff --git a/src/ui/widget/label-tool-item.cpp b/src/ui/widget/label-tool-item.cpp
new file mode 100644
index 0000000..979cfa2
--- /dev/null
+++ b/src/ui/widget/label-tool-item.cpp
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A label that can be added to a toolbar
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "label-tool-item.h"
+
+#include <gtkmm/label.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * \brief Create a tool-item containing a label
+ *
+ * \param[in] label The text to display in the label
+ * \param[in] mnemonic True if text should use a mnemonic
+ */
+LabelToolItem::LabelToolItem(const Glib::ustring& label, bool mnemonic)
+ : _label(Gtk::manage(new Gtk::Label(label, mnemonic)))
+{
+ add(*_label);
+ show_all();
+}
+
+/**
+ * \brief Set the markup text in the label
+ *
+ * \param[in] str The markup text
+ */
+void
+LabelToolItem::set_markup(const Glib::ustring& str)
+{
+ _label->set_markup(str);
+}
+
+/**
+ * \brief Sets whether label uses Pango markup
+ *
+ * \param[in] setting true if the label text should be parsed for markup
+ */
+void
+LabelToolItem::set_use_markup(bool setting)
+{
+ _label->set_use_markup(setting);
+}
+
+}
+}
+}
+/*
+ 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/ui/widget/label-tool-item.h b/src/ui/widget/label-tool-item.h
new file mode 100644
index 0000000..1fe6892
--- /dev/null
+++ b/src/ui/widget/label-tool-item.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * A label that can be added to a toolbar
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_LABEL_TOOL_ITEM_H
+#define SEEN_LABEL_TOOL_ITEM_H
+
+#include <gtkmm/toolitem.h>
+
+namespace Gtk {
+class Label;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * \brief A label that can be added to a toolbar
+ */
+class LabelToolItem : public Gtk::ToolItem {
+private:
+ Gtk::Label *_label;
+
+public:
+ LabelToolItem(const Glib::ustring& label, bool mnemonic = false);
+
+ void set_markup(const Glib::ustring& str);
+ void set_use_markup(bool setting = true);
+};
+}
+}
+}
+
+#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/ui/widget/labelled.cpp b/src/ui/widget/labelled.cpp
new file mode 100644
index 0000000..3b47e84
--- /dev/null
+++ b/src/ui/widget/labelled.cpp
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "labelled.h"
+#include "ui/icon-loader.h"
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Labelled::Labelled(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Gtk::Widget *widget,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL),
+ _widget(widget),
+ _label(new Gtk::Label(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, mnemonic)),
+ _suffix(nullptr)
+{
+ g_assert(g_utf8_validate(icon.c_str(), -1, nullptr));
+ if (icon != "") {
+ _icon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR));
+ pack_start(*_icon, Gtk::PACK_SHRINK);
+ }
+
+ set_spacing(6);
+ // Setting margins separately allows for more control over them
+ set_margin_start(6);
+ set_margin_end(6);
+ pack_start(*Gtk::manage(_label), Gtk::PACK_SHRINK);
+ pack_start(*Gtk::manage(_widget), Gtk::PACK_SHRINK);
+ if (mnemonic) {
+ _label->set_mnemonic_widget(*_widget);
+ }
+ widget->set_tooltip_text(tooltip);
+}
+
+
+void Labelled::setWidgetSizeRequest(int width, int height)
+{
+ if (_widget)
+ _widget->set_size_request(width, height);
+
+
+}
+
+Gtk::Label const *
+Labelled::getLabel() const
+{
+ return _label;
+}
+
+void
+Labelled::setLabelText(const Glib::ustring &str)
+{
+ _label->set_text(str);
+}
+
+void
+Labelled::setTooltipText(const Glib::ustring &tooltip)
+{
+ _label->set_tooltip_text(tooltip);
+ _widget->set_tooltip_text(tooltip);
+}
+
+bool Labelled::on_mnemonic_activate ( bool group_cycling )
+{
+ return _widget->mnemonic_activate ( group_cycling );
+}
+
+void
+Labelled::set_hexpand(bool expand)
+{
+ // should only have 2 children, but second child may not be _widget
+ child_property_pack_type(*get_children().back()) = expand ? Gtk::PACK_END
+ : Gtk::PACK_START;
+
+ Gtk::Box::set_hexpand(expand);
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/labelled.h b/src/ui/widget/labelled.h
new file mode 100644
index 0000000..bd82090
--- /dev/null
+++ b/src/ui/widget/labelled.h
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_LABELLED_H
+#define INKSCAPE_UI_WIDGET_LABELLED_H
+
+#include <gtkmm/box.h>
+
+namespace Gtk {
+class Image;
+class Label;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Adds a label with optional icon or suffix to another widget.
+ */
+class Labelled : public Gtk::Box
+{
+protected:
+ Gtk::Widget *_widget;
+ Gtk::Label *_label;
+ Gtk::Label *_suffix;
+ Gtk::Image *_icon;
+
+public:
+ /**
+ * Construct a Labelled Widget.
+ *
+ * @param label Label.
+ * @param widget Widget to label; should be allocated with new, as it will
+ * be passed to Gtk::manage().
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the text
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Labelled(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Gtk::Widget *widget,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Allow the setting of the width of the labelled widget
+ */
+ void setWidgetSizeRequest(int width, int height);
+
+ inline decltype(_widget) getWidget() const { return _widget; }
+ Gtk::Label const *getLabel() const;
+
+ void setLabelText(const Glib::ustring &str);
+ void setTooltipText(const Glib::ustring &tooltip);
+
+ void set_hexpand(bool expand = true);
+
+private:
+ bool on_mnemonic_activate( bool group_cycling ) override;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_LABELLED_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/ui/widget/layer-selector.cpp b/src/ui/widget/layer-selector.cpp
new file mode 100644
index 0000000..a96c120
--- /dev/null
+++ b/src/ui/widget/layer-selector.cpp
@@ -0,0 +1,219 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::Widgets::LayerSelector - layer selector widget
+ *
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+
+#include <boost/range/adaptor/filtered.hpp>
+#include <boost/range/adaptor/reversed.hpp>
+
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "layer-manager.h"
+
+#include "ui/widget/layer-selector.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/objects.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/util.h"
+
+#include "object/sp-root.h"
+#include "object/sp-item-group.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AlternateIcons : public Gtk::Box {
+public:
+ AlternateIcons(Gtk::BuiltinIconSize size, Glib::ustring const &a, Glib::ustring const &b)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , _a(nullptr)
+ , _b(nullptr)
+ {
+ set_name("AlternateIcons");
+ if (!a.empty()) {
+ _a = Gtk::manage(sp_get_icon_image(a, size));
+ _a->set_no_show_all(true);
+ add(*_a);
+ }
+ if (!b.empty()) {
+ _b = Gtk::manage(sp_get_icon_image(b, size));
+ _b->set_no_show_all(true);
+ add(*_b);
+ }
+ setState(false);
+ }
+
+ bool state() const { return _state; }
+ void setState(bool state) {
+ _state = state;
+ if (_state) {
+ if (_a) _a->hide();
+ if (_b) _b->show();
+ } else {
+ if (_a) _a->show();
+ if (_b) _b->hide();
+ }
+ }
+private:
+ Gtk::Image *_a;
+ Gtk::Image *_b;
+ bool _state;
+};
+
+LayerSelector::LayerSelector(SPDesktop *desktop)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , _desktop(nullptr)
+ , _observer(new Inkscape::XML::SignalObserver)
+{
+ set_name("LayerSelector");
+
+ _layer_name.signal_clicked().connect(sigc::mem_fun(*this, &LayerSelector::_layerChoose));
+ _layer_name.set_relief(Gtk::RELIEF_NONE);
+ _layer_name.set_tooltip_text(_("Current layer"));
+ pack_start(_layer_name, Gtk::PACK_EXPAND_WIDGET);
+
+ _eye_label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU,
+ INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")));
+ _eye_toggle.add(*_eye_label);
+ _hide_layer_connection = _eye_toggle.signal_toggled().connect(sigc::mem_fun(*this, &LayerSelector::_hideLayer));
+
+ _eye_toggle.set_relief(Gtk::RELIEF_NONE);
+ _eye_toggle.set_tooltip_text(_("Toggle current layer visibility"));
+ pack_start(_eye_toggle, Gtk::PACK_EXPAND_PADDING);
+
+ _lock_label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU,
+ INKSCAPE_ICON("object-unlocked"), INKSCAPE_ICON("object-locked")));
+ _lock_toggle.add(*_lock_label);
+ _lock_layer_connection = _lock_toggle.signal_toggled().connect(sigc::mem_fun(*this, &LayerSelector::_lockLayer));
+
+ _lock_toggle.set_relief(Gtk::RELIEF_NONE);
+ _lock_toggle.set_tooltip_text(_("Lock or unlock current layer"));
+ pack_start(_lock_toggle, Gtk::PACK_EXPAND_PADDING);
+
+ _layer_name.add(_layer_label);
+ _layer_label.set_max_width_chars(16);
+ _layer_label.set_ellipsize(Pango::ELLIPSIZE_END);
+ _layer_label.set_markup("<i>Unset</i>");
+ _layer_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _observer->signal_changed().connect(sigc::mem_fun(*this, &LayerSelector::_layerModified));
+ setDesktop(desktop);
+}
+
+LayerSelector::~LayerSelector() {
+ setDesktop(nullptr);
+}
+
+void LayerSelector::setDesktop(SPDesktop *desktop) {
+ if ( desktop == _desktop )
+ return;
+
+ _layer_changed.disconnect();
+ _desktop = desktop;
+
+ if (_desktop) {
+ _layer_changed = _desktop->layerManager().connectCurrentLayerChanged(sigc::mem_fun(*this, &LayerSelector::_layerChanged));
+ _layerChanged(_desktop->layerManager().currentLayer());
+ }
+}
+
+/**
+ * Selects the given layer in the widget.
+ */
+void LayerSelector::_layerChanged(SPGroup *layer)
+{
+ _layer = layer;
+ _observer->set(layer);
+ _layerModified();
+}
+
+/**
+ * If anything happens to the layer, refresh it.
+ */
+void LayerSelector::_layerModified()
+{
+ auto root = _desktop->layerManager().currentRoot();
+ bool active = _layer && _layer != root;
+
+ if (_label_style) {
+ _layer_label.get_style_context()->remove_provider(_label_style);
+ }
+ auto color_str = std::string("white");
+
+ if (active) {
+ _layer_label.set_text(_layer->defaultLabel());
+ color_str = SPColor(_layer->highlight_color()).toString();
+ } else {
+ _layer_label.set_markup(_layer ? "<i>[root]</i>" : "<i>nothing</i>");
+ }
+
+ Glib::RefPtr<Gtk::StyleContext> style_context = _layer_label.get_style_context();
+ _label_style = Gtk::CssProvider::create();
+ _label_style->load_from_data("#LayerSelector label {border-color:" + color_str + ";}");
+ _layer_label.get_style_context()->add_provider(_label_style, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ _hide_layer_connection.block();
+ _lock_layer_connection.block();
+ _eye_toggle.set_sensitive(active);
+ _lock_toggle.set_sensitive(active);
+ _eye_label->setState(active && _layer->isHidden());
+ _eye_toggle.set_active(active && _layer->isHidden());
+ _lock_label->setState(active && _layer->isLocked());
+ _lock_toggle.set_active(active && _layer->isLocked());
+ _hide_layer_connection.unblock();
+ _lock_layer_connection.unblock();
+}
+
+void LayerSelector::_lockLayer()
+{
+ bool lock = _lock_toggle.get_active();
+ if (auto layer = _desktop->layerManager().currentLayer()) {
+ layer->setLocked(lock);
+ DocumentUndo::done(_desktop->getDocument(), lock ? _("Lock layer") : _("Unlock layer"), "");
+ }
+}
+
+void LayerSelector::_hideLayer()
+{
+ bool hide = _eye_toggle.get_active();
+ if (auto layer = _desktop->layerManager().currentLayer()) {
+ layer->setHidden(hide);
+ DocumentUndo::done(_desktop->getDocument(), hide ? _("Hide layer") : _("Unhide layer"), "");
+ }
+}
+
+void LayerSelector::_layerChoose()
+{
+ _desktop->getContainer()->new_dialog("Objects");
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/layer-selector.h b/src/ui/widget/layer-selector.h
new file mode 100644
index 0000000..6300cba
--- /dev/null
+++ b/src/ui/widget/layer-selector.h
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::UI::Widget::LayerSelector - layer selector widget
+ *
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 2004 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_WIDGETS_LAYER_SELECTOR
+#define SEEN_INKSCAPE_WIDGETS_LAYER_SELECTOR
+
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/cssprovider.h>
+#include <sigc++/slot.h>
+
+#include "xml/helper-observer.h"
+
+class SPDesktop;
+class SPDocument;
+class SPGroup;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class AlternateIcons;
+
+class LayerSelector : public Gtk::Box {
+public:
+ LayerSelector(SPDesktop *desktop = nullptr);
+ ~LayerSelector() override;
+
+ void setDesktop(SPDesktop *desktop);
+private:
+ SPDesktop *_desktop;
+ SPGroup *_layer;
+
+ Gtk::ToggleButton _eye_toggle;
+ Gtk::ToggleButton _lock_toggle;
+ Gtk::Button _layer_name;
+ Gtk::Label _layer_label;
+ Glib::RefPtr<Gtk::CssProvider> _label_style;
+ AlternateIcons * _eye_label;
+ AlternateIcons * _lock_label;
+
+ sigc::connection _layer_changed;
+ sigc::connection _hide_layer_connection;
+ sigc::connection _lock_layer_connection;
+ std::unique_ptr<Inkscape::XML::SignalObserver> _observer;
+
+ void _layerChanged(SPGroup *layer);
+ void _layerModified();
+ void _selectLayer();
+ void _hideLayer();
+ void _lockLayer();
+ void _layerChoose();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#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/ui/widget/licensor.cpp b/src/ui/widget/licensor.cpp
new file mode 100644
index 0000000..d6ed8ff
--- /dev/null
+++ b/src/ui/widget/licensor.cpp
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2000 - 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "licensor.h"
+
+#include <gtkmm/entry.h>
+#include <gtkmm/radiobutton.h>
+
+#include "rdf.h"
+#include "inkscape.h"
+#include "document-undo.h"
+
+#include "ui/widget/entity-entry.h"
+#include "ui/widget/registry.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+//===================================================
+
+const struct rdf_license_t _proprietary_license =
+ {_("Proprietary"), "", nullptr};
+
+const struct rdf_license_t _other_license =
+ {Q_("MetadataLicence|Other"), "", nullptr};
+
+class LicenseItem : public Gtk::RadioButton {
+public:
+ LicenseItem (struct rdf_license_t const* license, EntityEntry* entity, Registry &wr, Gtk::RadioButtonGroup *group);
+protected:
+ void on_toggled() override;
+ struct rdf_license_t const *_lic;
+ EntityEntry *_eep;
+ Registry &_wr;
+};
+
+LicenseItem::LicenseItem (struct rdf_license_t const* license, EntityEntry* entity, Registry &wr, Gtk::RadioButtonGroup *group)
+: Gtk::RadioButton(_(license->name)), _lic(license), _eep(entity), _wr(wr)
+{
+ if (group) {
+ set_group (*group);
+ }
+}
+
+/// \pre it is assumed that the license URI entry is a Gtk::Entry
+void LicenseItem::on_toggled()
+{
+ if (_wr.isUpdating() || !_wr.desktop())
+ return;
+
+ _wr.setUpdating (true);
+ SPDocument *doc = _wr.desktop()->getDocument();
+ rdf_set_license (doc, _lic->details ? _lic : nullptr);
+ if (doc->isSensitive()) {
+ DocumentUndo::done(doc, _("Document license updated"), "");
+ }
+ _wr.setUpdating (false);
+ static_cast<Gtk::Entry*>(_eep->_packable)->set_text (_lic->uri);
+ _eep->on_changed();
+}
+
+//---------------------------------------------------
+
+Licensor::Licensor()
+: Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4),
+ _eentry (nullptr)
+{
+}
+
+Licensor::~Licensor()
+{
+ if (_eentry) delete _eentry;
+}
+
+void Licensor::init (Registry& wr)
+{
+ /* add license-specific metadata entry areas */
+ rdf_work_entity_t* entity = rdf_find_entity ( "license_uri" );
+ _eentry = EntityEntry::create (entity, wr);
+
+ LicenseItem *i;
+ wr.setUpdating (true);
+ i = Gtk::manage (new LicenseItem (&_proprietary_license, _eentry, wr, nullptr));
+ Gtk::RadioButtonGroup group = i->get_group();
+ add (*i);
+ LicenseItem *pd = i;
+
+ for (struct rdf_license_t * license = rdf_licenses;
+ license && license->name;
+ license++) {
+ i = Gtk::manage (new LicenseItem (license, _eentry, wr, &group));
+ add(*i);
+ }
+ // add Other at the end before the URI field for the confused ppl.
+ LicenseItem *io = Gtk::manage (new LicenseItem (&_other_license, _eentry, wr, &group));
+ add (*io);
+
+ pd->set_active();
+ wr.setUpdating (false);
+
+ Gtk::Box *box = Gtk::manage (new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ pack_start (*box, true, true, 0);
+
+ box->pack_start (_eentry->_label, false, false, 5);
+ box->pack_start (*_eentry->_packable, true, true, 0);
+
+ show_all_children();
+}
+
+void Licensor::update (SPDocument *doc)
+{
+ /* identify the license info */
+ struct rdf_license_t * license = rdf_get_license (doc);
+
+ if (license) {
+ int i;
+ for (i=0; rdf_licenses[i].name; i++)
+ if (license == &rdf_licenses[i])
+ break;
+ static_cast<LicenseItem*>(get_children()[i+1])->set_active();
+ }
+ else {
+ static_cast<LicenseItem*>(get_children()[0])->set_active();
+ }
+
+ /* update the URI */
+ _eentry->update (doc);
+}
+
+} // namespace Dialog
+} // namespace UI
+} // 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/ui/widget/licensor.h b/src/ui/widget/licensor.h
new file mode 100644
index 0000000..214ffca
--- /dev/null
+++ b/src/ui/widget/licensor.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_LICENSOR_H
+#define INKSCAPE_UI_WIDGET_LICENSOR_H
+
+#include <gtkmm/box.h>
+
+class SPDocument;
+
+namespace Inkscape {
+ namespace UI {
+ namespace Widget {
+
+class EntityEntry;
+class Registry;
+
+
+/**
+ * Widget for specifying a document's license; part of document
+ * preferences dialog.
+ */
+class Licensor : public Gtk::Box {
+public:
+ Licensor();
+ ~Licensor() override;
+ void init (Registry&);
+ void update (SPDocument *doc);
+
+protected:
+ EntityEntry *_eentry;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_LICENSOR_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/ui/widget/marker-combo-box.cpp b/src/ui/widget/marker-combo-box.cpp
new file mode 100644
index 0000000..a8ea110
--- /dev/null
+++ b/src/ui/widget/marker-combo-box.cpp
@@ -0,0 +1,1033 @@
+// 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 "marker-combo-box.h"
+
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <gtkmm/icontheme.h>
+#include <gtkmm/menubutton.h>
+
+#include "desktop-style.h"
+#include "helper/stock-items.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "object/sp-defs.h"
+#include "object/sp-marker.h"
+#include "object/sp-root.h"
+#include "path-prefix.h"
+#include "style.h"
+#include "ui/builder-utils.h"
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/svg-renderer.h"
+#include "ui/util.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/stroke-style.h"
+
+#define noTIMING_INFO 1;
+
+using Inkscape::UI::get_widget;
+using Inkscape::UI::create_builder;
+
+// size of marker image in a list
+static const int ITEM_WIDTH = 40;
+static const int ITEM_HEIGHT = 32;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// separator for FlowBox widget
+static cairo_surface_t* create_separator(double alpha, int width, int height, int device_scale) {
+ width *= device_scale;
+ height *= device_scale;
+ cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ cairo_t* ctx = cairo_create(surface);
+ cairo_set_source_rgba(ctx, 0.5, 0.5, 0.5, alpha);
+ cairo_move_to(ctx, 0.5, height / 2 + 0.5);
+ cairo_line_to(ctx, width + 0.5, height / 2 + 0.5);
+ cairo_set_line_width(ctx, 1.0 * device_scale);
+ cairo_stroke(ctx);
+ cairo_surface_flush(surface);
+ cairo_surface_set_device_scale(surface, device_scale, device_scale);
+ return surface;
+}
+
+// empty image; "no marker"
+static Cairo::RefPtr<Cairo::Surface> g_image_none;
+// error extracting/rendering marker; "bad marker"
+static Cairo::RefPtr<Cairo::Surface> g_bad_marker;
+
+Glib::ustring get_attrib(SPMarker* marker, const char* attrib) {
+ auto value = marker->getAttribute(attrib);
+ return value ? value : "";
+}
+
+double get_attrib_num(SPMarker* marker, const char* attrib) {
+ auto val = get_attrib(marker, attrib);
+ return strtod(val.c_str(), nullptr);
+}
+
+MarkerComboBox::MarkerComboBox(Glib::ustring id, int l) :
+ _combo_id(std::move(id)),
+ _loc(l),
+ _builder(create_builder("marker-popup.glade")),
+ _marker_list(get_widget<Gtk::FlowBox>(_builder, "flowbox")),
+ _preview(get_widget<Gtk::Image>(_builder, "preview")),
+ _marker_name(get_widget<Gtk::Label>(_builder, "marker-id")),
+ _link_scale(get_widget<Gtk::Button>(_builder, "link-scale")),
+ _scale_x(get_widget<Gtk::SpinButton>(_builder, "scale-x")),
+ _scale_y(get_widget<Gtk::SpinButton>(_builder, "scale-y")),
+ _scale_with_stroke(get_widget<Gtk::CheckButton>(_builder, "scale-with-stroke")),
+ _menu_btn(get_widget<Gtk::MenuButton>(_builder, "menu-btn")),
+ _angle_btn(get_widget<Gtk::SpinButton>(_builder, "angle")),
+ _offset_x(get_widget<Gtk::SpinButton>(_builder, "offset-x")),
+ _offset_y(get_widget<Gtk::SpinButton>(_builder, "offset-y")),
+ _input_grid(get_widget<Gtk::Grid>(_builder, "input-grid")),
+ _orient_auto_rev(get_widget<Gtk::RadioButton>(_builder, "orient-auto-rev")),
+ _orient_auto(get_widget<Gtk::RadioButton>(_builder, "orient-auto")),
+ _orient_angle(get_widget<Gtk::RadioButton>(_builder, "orient-angle")),
+ _orient_flip_horz(get_widget<Gtk::Button>(_builder, "btn-horz-flip")),
+ _current_img(get_widget<Gtk::Image>(_builder, "current-img")),
+ _edit_marker(get_widget<Gtk::Button>(_builder, "edit-marker"))
+{
+ _background_color = 0x808080ff;
+ _foreground_color = 0x808080ff;
+
+ if (!g_image_none) {
+ auto device_scale = get_scale_factor();
+ g_image_none = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(create_separator(1, ITEM_WIDTH, ITEM_HEIGHT, device_scale)));
+ }
+
+ if (!g_bad_marker) {
+ auto path = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "bad-marker.svg");
+ Inkscape::svg_renderer renderer(path.c_str());
+ g_bad_marker = renderer.render_surface(1.0);
+ }
+
+ add(_menu_btn);
+
+ _preview.signal_size_allocate().connect([=](Gtk::Allocation& a){
+ // refresh after preview widget has been finally resized/expanded
+ if (_preview_no_alloc) update_preview(find_marker_item(get_current()));
+ });
+
+ _marker_store = Gio::ListStore<MarkerItem>::create();
+ _marker_list.bind_list_store(_marker_store, [=](const Glib::RefPtr<MarkerItem>& item){
+ auto image = Gtk::make_managed<Gtk::Image>(item->pix);
+ image->show();
+ auto box = Gtk::make_managed<Gtk::FlowBoxChild>();
+ box->add(*image);
+ if (item->separator) {
+ image->set_sensitive(false);
+ image->set_can_focus(false);
+ image->set_size_request(-1, 10);
+ box->set_sensitive(false);
+ box->set_can_focus(false);
+ box->get_style_context()->add_class("marker-separator");
+ }
+ else {
+ box->get_style_context()->add_class("marker-item-box");
+ }
+ _widgets_to_markers[image] = item;
+ box->set_size_request(item->width, item->height);
+ return box;
+ });
+
+ _sandbox = ink_markers_preview_doc(_combo_id);
+
+ set_sensitive(true);
+
+ _marker_list.signal_selected_children_changed().connect([=](){
+ auto item = get_active();
+ if (!item && !_marker_list.get_selected_children().empty()) {
+ _marker_list.unselect_all();
+ }
+ });
+
+ _marker_list.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){
+ if (box->get_sensitive()) _signal_changed.emit();
+ });
+
+ auto set_orient = [=](bool enable_angle, const char* value) {
+ if (_update.pending()) return;
+ _angle_btn.set_sensitive(enable_angle);
+ sp_marker_set_orient(get_current(), value);
+ };
+ _orient_auto_rev.signal_toggled().connect([=](){ set_orient(false, "auto-start-reverse"); });
+ _orient_auto.signal_toggled().connect([=]() { set_orient(false, "auto"); });
+ _orient_angle.signal_toggled().connect([=]() { set_orient(true, _angle_btn.get_text().c_str()); });
+ _orient_flip_horz.signal_clicked().connect([=]() { sp_marker_flip_horizontally(get_current()); });
+
+ _angle_btn.signal_value_changed().connect([=]() {
+ if (_update.pending() || !_angle_btn.is_sensitive()) return;
+ sp_marker_set_orient(get_current(), _angle_btn.get_text().c_str());
+ });
+
+ auto set_scale = [=](bool changeWidth) {
+ if (_update.pending()) return;
+ if (auto marker = get_current()) {
+ auto sx = _scale_x.get_value();
+ auto sy = _scale_y.get_value();
+ auto width = get_attrib_num(marker, "markerWidth");
+ auto height = get_attrib_num(marker, "markerHeight");
+ if (_scale_linked && width > 0.0 && height > 0.0) {
+ auto scoped(_update.block());
+ if (changeWidth) {
+ // scale height proportionally
+ sy = height * (sx / width);
+ _scale_y.set_value(sy);
+ }
+ else {
+ // scale width proportionally
+ sx = width * (sy / height);
+ _scale_x.set_value(sx);
+ }
+ }
+ sp_marker_set_size(marker, sx, sy);
+ }
+ };
+
+ _link_scale.signal_clicked().connect([=](){
+ if (_update.pending()) return;
+ _scale_linked = !_scale_linked;
+ sp_marker_set_uniform_scale(get_current(), _scale_linked);
+ update_scale_link();
+ });
+
+ _scale_x.signal_value_changed().connect([=]() { set_scale(true); });
+ _scale_y.signal_value_changed().connect([=]() { set_scale(false); });
+
+ _scale_with_stroke.signal_toggled().connect([=](){
+ if (_update.pending()) return;
+ sp_marker_scale_with_stroke(get_current(), _scale_with_stroke.get_active());
+ });
+
+ auto set_offset = [=](){
+ if (_update.pending()) return;
+ sp_marker_set_offset(get_current(), _offset_x.get_value(), _offset_y.get_value());
+ };
+ _offset_x.signal_value_changed().connect([=]() { set_offset(); });
+ _offset_y.signal_value_changed().connect([=]() { set_offset(); });
+
+ // request to edit marker on canvas; close popup to get it out of the way and call marker edit tool
+ _edit_marker.signal_clicked().connect([=]() { _menu_btn.get_popover()->popdown(); edit_signal(); });
+
+ // before showing popover refresh marker attributes
+ _menu_btn.get_popover()->signal_show().connect([=](){ update_ui(get_current(), false); }, false);
+
+ update_scale_link();
+ _current_img.set(g_image_none);
+ show();
+}
+
+MarkerComboBox::~MarkerComboBox() {
+ if (_document) {
+ modified_connection.disconnect();
+ }
+}
+
+void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) {
+ _input_grid.set_sensitive(marker != nullptr);
+
+ if (marker) {
+ marker->updateRepr();
+
+ _scale_x.set_value(get_attrib_num(marker, "markerWidth"));
+ _scale_y.set_value(get_attrib_num(marker, "markerHeight"));
+ auto units = get_attrib(marker, "markerUnits");
+ _scale_with_stroke.set_active(units == "strokeWidth" || units == "");
+ auto aspect = get_attrib(marker, "preserveAspectRatio");
+ _scale_linked = aspect != "none";
+ update_scale_link();
+ // marker->setAttribute("markerUnits", scale_with_stroke ? "strokeWidth" : "userSpaceOnUse");
+ _offset_x.set_value(get_attrib_num(marker, "refX"));
+ _offset_y.set_value(get_attrib_num(marker, "refY"));
+ auto orient = get_attrib(marker, "orient");
+
+ // try parsing as number
+ _angle_btn.set_value(strtod(orient.c_str(), nullptr));
+ if (orient == "auto-start-reverse") {
+ _orient_auto_rev.set_active();
+ _angle_btn.set_sensitive(false);
+ }
+ else if (orient == "auto") {
+ _orient_auto.set_active();
+ _angle_btn.set_sensitive(false);
+ }
+ else {
+ _orient_angle.set_active();
+ _angle_btn.set_sensitive(true);
+ }
+ }
+}
+
+void MarkerComboBox::update_scale_link() {
+ _link_scale.remove();
+ _link_scale.add(get_widget<Gtk::Image>(_builder, _scale_linked ? "image-linked" : "image-unlinked"));
+}
+
+// update marker image inside the menu button
+void MarkerComboBox::update_menu_btn(Glib::RefPtr<MarkerItem> marker) {
+ _current_img.set(marker ? marker->pix : g_image_none);
+}
+
+// update marker preview image in the popover panel
+void MarkerComboBox::update_preview(Glib::RefPtr<MarkerItem> item) {
+ Cairo::RefPtr<Cairo::Surface> surface;
+ Glib::ustring label;
+
+ if (!item) {
+ // TRANSLATORS: None - no marker selected for a path
+ label = _("None");
+ }
+
+ if (item && item->source && !item->id.empty()) {
+ Inkscape::Drawing drawing;
+ unsigned const visionkey = SPItem::display_key_new(1);
+ drawing.setRoot(_sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
+ // generate preview
+ auto alloc = _preview.get_allocation();
+ auto size = Geom::IntPoint(alloc.get_width() - 10, alloc.get_height() - 10);
+ if (size.x() > 0 && size.y() > 0) {
+ surface = create_marker_image(size, item->id.c_str(), item->source, drawing, visionkey, true, true, 2.60);
+ }
+ else {
+ // too early, preview hasn't been expanded/resized yet
+ _preview_no_alloc = true;
+ }
+ _sandbox->getRoot()->invoke_hide(visionkey);
+ label = item->label;
+ }
+
+ _preview.set(surface);
+ std::ostringstream ost;
+ ost << "<small>" << label.raw() << "</small>";
+ _marker_name.set_markup(ost.str().c_str());
+}
+
+bool MarkerComboBox::MarkerItem::operator == (const MarkerItem& item) const {
+ return
+ id == item.id &&
+ label == item.label &&
+ separator == item.separator &&
+ stock == item.stock &&
+ history == item.history &&
+ source == item.source &&
+ width == item.width &&
+ height == item.height;
+}
+
+// find marker object by ID in a document
+SPMarker* find_marker(SPDocument* document, const Glib::ustring& marker_id) {
+ if (!document) return nullptr;
+
+ SPDefs* defs = document->getDefs();
+ if (!defs) return nullptr;
+
+ for (auto& child : defs->children) {
+ if (SP_IS_MARKER(&child)) {
+ auto marker = SP_MARKER(&child);
+ auto id = marker->getId();
+ if (id && marker_id == id) {
+ // found it
+ return marker;
+ }
+ }
+ }
+
+ // not found
+ return nullptr;
+}
+
+SPMarker* MarkerComboBox::get_current() const {
+ // find current marker
+ return find_marker(_document, _current_marker_id);
+}
+
+void MarkerComboBox::set_active(Glib::RefPtr<MarkerItem> item) {
+ bool selected = false;
+ if (item) {
+ _marker_list.foreach([=,&selected](Gtk::Widget& widget){
+ if (auto box = dynamic_cast<Gtk::FlowBoxChild*>(&widget)) {
+ if (auto marker = _widgets_to_markers[box->get_child()]) {
+ if (*marker.get() == *item.get()) {
+ _marker_list.select_child(*box);
+ selected = true;
+ }
+ }
+ }
+ });
+ }
+
+ if (!selected) {
+ _marker_list.unselect_all();
+ }
+}
+
+Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::find_marker_item(SPMarker* marker) {
+ std::string id;
+ if (marker != nullptr) {
+ if (auto markname = marker->getRepr()->attribute("id")) {
+ id = markname;
+ }
+ }
+
+ Glib::RefPtr<MarkerItem> marker_item;
+ if (!id.empty()) {
+ for (auto&& item : _history_items) {
+ if (item->id == id) {
+ marker_item = item;
+ break;
+ }
+ }
+ }
+
+ return marker_item;
+}
+
+Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::get_active() {
+ auto empty = Glib::RefPtr<MarkerItem>();
+ auto sel = _marker_list.get_selected_children();
+ if (sel.size() == 1) {
+ auto item = _widgets_to_markers[sel.front()->get_child()];
+ if (item && item->separator) {
+ return empty;
+ }
+ return item;
+ }
+ else {
+ return empty;
+ }
+}
+
+void MarkerComboBox::setDocument(SPDocument *document)
+{
+ if (_document != document) {
+
+ if (_document) {
+ modified_connection.disconnect();
+ }
+
+ _document = document;
+
+ if (_document) {
+ modified_connection = _document->getDefs()->connectModified([=](SPObject*, unsigned int){
+ refresh_after_markers_modified();
+ });
+ }
+
+ _current_marker_id = "";
+
+ refresh_after_markers_modified();
+ }
+}
+
+/**
+ * This function is invoked after document "defs" section changes.
+ * It will change when current marker's attributes are modified in this popup
+ * and this function will refresh the recent list and a preview to reflect the changes.
+ * It would be more efficient if there was a way to determine what has changed
+ * and perform only more targeted update.
+ */
+void MarkerComboBox::refresh_after_markers_modified() {
+ if (_update.pending()) return;
+
+ auto scoped(_update.block());
+
+ // collect orphaned markers, so they are not listed; if they are listed then
+ // they disappear upon selection leaving dangling URLs
+ if (_document) _document->collectOrphans();
+
+ /*
+ * 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
+ */
+ // TODO: detect changes to markers; ignore changes to everything else;
+ // simple count check doesn't cut it, so just do it unconditionally for now
+ marker_list_from_doc(_document, true);
+
+ auto marker = find_marker_item(get_current());
+ update_menu_btn(marker);
+ update_preview(marker);
+}
+
+Glib::RefPtr<MarkerComboBox::MarkerItem> MarkerComboBox::add_separator(bool filler) {
+ auto item = Glib::RefPtr<MarkerItem>(new MarkerItem);
+ item->history = false;
+ item->separator = true;
+ item->id = "None";
+ item->label = filler ? "filler" : "Separator";
+ item->stock = false;
+ if (!filler) {
+ auto device_scale = get_scale_factor();
+ static Cairo::RefPtr<Cairo::Surface> separator(new Cairo::Surface(create_separator(0.7, ITEM_WIDTH, 10, device_scale)));
+ item->pix = separator;
+ }
+ item->height = 10;
+ item->width = -1;
+ return item;
+}
+
+/**
+ * Init the combobox widget to display markers from markers.svg
+ */
+void
+MarkerComboBox::init_combo()
+{
+ if (_update.pending()) return;
+
+ static SPDocument *markers_doc = nullptr;
+
+ // find and load markers.svg
+ if (markers_doc == nullptr) {
+ using namespace Inkscape::IO::Resource;
+ auto markers_source = get_path_string(SYSTEM, MARKERS, "markers.svg");
+ if (Glib::file_test(markers_source, Glib::FILE_TEST_IS_REGULAR)) {
+ markers_doc = SPDocument::createNewDoc(markers_source.c_str(), false);
+ }
+ }
+
+ // load markers from markers.svg
+ if (markers_doc) {
+ marker_list_from_doc(markers_doc, false);
+ }
+
+ refresh_after_markers_modified();
+}
+
+/**
+ * Sets the current marker in the marker combobox.
+ */
+void MarkerComboBox::set_current(SPObject *marker)
+{
+ auto sp_marker = dynamic_cast<SPMarker*>(marker);
+
+ bool reselect = sp_marker != get_current();
+
+ update_ui(sp_marker, reselect);
+}
+
+void MarkerComboBox::update_ui(SPMarker* marker, bool select) {
+ auto scoped(_update.block());
+
+ auto id = marker ? marker->getId() : nullptr;
+ _current_marker_id = id ? id : "";
+
+ auto marker_item = find_marker_item(marker);
+
+ if (select) {
+ set_active(marker_item);
+ }
+
+ update_widgets_from_marker(marker);
+ update_menu_btn(marker_item);
+ update_preview(marker_item);
+}
+
+/**
+ * Return a uri string representing the current selected marker used for setting the marker style in the document
+ */
+std::string MarkerComboBox::get_active_marker_uri()
+{
+ /* Get Marker */
+ auto item = get_active();
+ if (!item) {
+ return std::string();
+ }
+
+ std::string marker;
+
+ if (item->id != "none") {
+ bool stockid = item->stock;
+
+ std::string markurn = stockid ? "urn:inkscape:marker:" + item->id : item->id;
+ auto mark = dynamic_cast<SPMarker*>(get_stock_item(markurn.c_str(), stockid));
+
+ if (mark) {
+ Inkscape::XML::Node* repr = mark->getRepr();
+ auto id = repr->attribute("id");
+ if (id) {
+ std::ostringstream ost;
+ ost << "url(#" << id << ")";
+ marker = ost.str();
+ }
+ if (stockid) {
+ mark->getRepr()->setAttribute("inkscape:collect", "always");
+ }
+ // adjust marker's attributes (or add missing ones) to stay in sync with marker tool
+ sp_validate_marker(mark, _document);
+ }
+ } else {
+ marker = item->id;
+ }
+
+ return marker;
+}
+
+/**
+ * Pick up all markers from source and add items to the list/store.
+ * If 'history' is true, then update recently used in-document portion of the list;
+ * otherwise update list of stock markers, which is displayed after recent ones
+ */
+void MarkerComboBox::marker_list_from_doc(SPDocument* source, bool history) {
+ std::vector<SPMarker*> markers = get_marker_list(source);
+ remove_markers(history);
+ add_markers(markers, source, history);
+ update_store();
+}
+
+void MarkerComboBox::update_store() {
+ _marker_store->freeze_notify();
+
+ auto selected = get_active();
+
+ _marker_store->remove_all();
+ _widgets_to_markers.clear();
+
+ // recent and user-defined markers come first
+ for (auto&& item : _history_items) {
+ _marker_store->append(item);
+ }
+
+ // separator
+ if (!_history_items.empty()) {
+ // add empty boxes to fill up the row to 'max' elements and then
+ // extra ones to create entire new empty row (a separator of sorts)
+ auto max = _marker_list.get_max_children_per_line();
+ auto fillup = max - _history_items.size() % max;
+
+ for (int i = 0; i < fillup; ++i) {
+ _marker_store->append(add_separator(true));
+ }
+ for (int i = 0; i < max; ++i) {
+ _marker_store->append(add_separator(false));
+ }
+ }
+
+ // stock markers
+ for (auto&& item : _stock_items) {
+ _marker_store->append(item);
+ }
+
+ _marker_store->thaw_notify();
+
+ // reselect current
+ set_active(selected);
+}
+/**
+ * Returns a vector of markers in the defs of the given source document as a vector.
+ * Returns empty vector if there are no markers in the document.
+ * If validate is true then it runs each marker through the validation routine that alters some attributes.
+ */
+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)) {
+ auto marker = SP_MARKER(&child);
+ ml.push_back(marker);
+ }
+ }
+ return ml;
+}
+
+/**
+ * Remove history or non-history markers from the combo
+ */
+void MarkerComboBox::remove_markers (gboolean history)
+{
+ if (history) {
+ _history_items.clear();
+ }
+ else {
+ _stock_items.clear();
+ }
+}
+
+/**
+ * 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));
+
+ if (history) {
+ // add "None"
+ auto item = Glib::RefPtr<MarkerItem>(new MarkerItem);
+ item->pix = g_image_none;
+ item->history = true;
+ item->separator = false;
+ item->id = "None";
+ item->label = "None";
+ item->stock = false;
+ item->width = ITEM_WIDTH;
+ item->height = ITEM_HEIGHT;
+ _history_items.push_back(item);
+ }
+
+#if TIMING_INFO
+auto old_time = std::chrono::high_resolution_clock::now();
+#endif
+
+ 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
+ auto pixbuf = create_marker_image(Geom::IntPoint(ITEM_WIDTH, ITEM_HEIGHT), repr->attribute("id"), source, drawing, visionkey, false, true, 1.50);
+
+ auto item = Glib::RefPtr<MarkerItem>(new MarkerItem);
+ item->source = source;
+ item->pix = pixbuf;
+ if (auto id = repr->attribute("id")) {
+ item->id = id;
+ }
+ item->label = markid ? markid : "";
+ item->stock = !history;
+ item->history = history;
+ item->width = ITEM_WIDTH;
+ item->height = ITEM_HEIGHT;
+
+ if (history) {
+ _history_items.push_back(item);
+ }
+ else {
+ _stock_items.push_back(item);
+ }
+ }
+
+ _sandbox->getRoot()->invoke_hide(visionkey);
+
+#if TIMING_INFO
+auto current_time = std::chrono::high_resolution_clock::now();
+auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - old_time);
+g_warning("%s render time for %d markers: %d ms", combo_id, (int)marker_list.size(), static_cast<int>(elapsed.count()));
+#endif
+}
+
+/**
+ * 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.
+ */
+Cairo::RefPtr<Cairo::Surface>
+MarkerComboBox::create_marker_image(Geom::IntPoint pixel_size, gchar const *mname,
+ SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/, bool checkerboard, bool no_clip, double scale)
+{
+ // Retrieve the marker named 'mname' from the source SVG document
+ SPObject const *marker = source->getObjectById(mname);
+ if (marker == nullptr) {
+ g_warning("bad mname: %s", mname);
+ return g_bad_marker;
+ }
+
+ SPObject *oldmarker = _sandbox->getObjectById("sample");
+ if (oldmarker) {
+ oldmarker->deleteObject(false);
+ }
+
+ // 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();
+
+ // 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", nullptr), "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);
+
+ if (object == nullptr || !SP_IS_ITEM(object)) {
+ g_warning("no obj: %s", _combo_id.c_str());
+ return g_bad_marker;
+ }
+
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ auto fgcolor = rgba_to_css_color(fg);
+ fg.set_red(1 - fg.get_red());
+ fg.set_green(1 - fg.get_green());
+ fg.set_blue(1 - fg.get_blue());
+ auto bgcolor = rgba_to_css_color(fg);
+ auto objects = _sandbox->getObjectsBySelector(".colors");
+ for (auto el : objects) {
+ if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) {
+ sp_repr_css_set_property(css, "fill", bgcolor.c_str());
+ sp_repr_css_set_property(css, "stroke", fgcolor.c_str());
+ el->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ }
+ }
+
+ auto cross = _sandbox->getObjectsBySelector(".cross");
+ double stroke = 0.5;
+ for (auto el : cross) {
+ if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) {
+ sp_repr_css_set_property(css, "display", checkerboard ? "block" : "none");
+ sp_repr_css_set_property_double(css, "stroke-width", stroke);
+ el->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ }
+ }
+
+ SPDocument::install_reference_document scoped(_sandbox.get(), marker->document);
+
+ _sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ _sandbox->ensureUpToDate();
+
+ SPItem *item = SP_ITEM(object);
+ // Find object's bbox in document
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if (!dbox) {
+ g_warning("no dbox");
+ return g_bad_marker;
+ }
+
+ if (auto measure = dynamic_cast<SPItem*>(_sandbox->getObjectById("measure-marker"))) {
+ if (auto box = measure->documentVisualBounds()) {
+ // check size of the marker applied to a path with stroke of 1px
+ auto size = std::max(box->width(), box->height());
+ const double small = 5.0;
+ // if too small, then scale up; clip needs to be enabled for scale to work
+ if (size > 0 && size < small) {
+ auto factor = 1 + small - size;
+ scale *= factor;
+ no_clip = false;
+
+ // adjust cross stroke
+ stroke /= factor;
+ for (auto el : cross) {
+ if (SPCSSAttr* css = sp_repr_css_attr(el->getRepr(), "style")) {
+ sp_repr_css_set_property_double(css, "stroke-width", stroke);
+ el->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ }
+ }
+
+ _sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ _sandbox->ensureUpToDate();
+ }
+ }
+ }
+
+ /* Update to renderable state */
+ const double device_scale = get_scale_factor();
+ auto surface = render_surface(drawing, scale, *dbox, pixel_size, device_scale, checkerboard ? &_background_color : nullptr, no_clip);
+ cairo_surface_set_device_scale(surface, device_scale, device_scale);
+ return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(surface, false));
+}
+
+// capture background color when styles change
+void MarkerComboBox::on_style_updated() {
+ auto background = _background_color;
+ if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) {
+ auto sc = wnd->get_style_context();
+ auto color = get_background_color(sc);
+ background =
+ gint32(0xff * color.get_red()) << 24 |
+ gint32(0xff * color.get_green()) << 16 |
+ gint32(0xff * color.get_blue()) << 8 |
+ 0xff;
+ }
+
+ auto context = get_style_context();
+ Gdk::RGBA color = context->get_color(get_state_flags());
+ auto foreground =
+ gint32(0xff * color.get_red()) << 24 |
+ gint32(0xff * color.get_green()) << 16 |
+ gint32(0xff * color.get_blue()) << 8 |
+ 0xff;
+ if (foreground != _foreground_color || background != _background_color) {
+ _foreground_color = foreground;
+ _background_color = background;
+ // theme changed?
+ init_combo();
+ }
+}
+
+/*TODO: background for custom markers
+ <filter id="softGlow" height="1.2" width="1.2" x="0.0" y="0.0">
+ <!-- <feMorphology operator="dilate" radius="1" in="SourceAlpha" result="thicken" id="feMorphology2" /> -->
+ <!-- Use a gaussian blur to create the soft blurriness of the glow -->
+ <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred" id="feGaussianBlur4" />
+ <!-- Change the color -->
+ <feFlood flood-color="rgb(255,255,255)" result="glowColor" id="feFlood6" flood-opacity="0.70" />
+ <!-- Color in the glows -->
+ <feComposite in="glowColor" in2="blurred" operator="in" result="softGlow_colored" id="feComposite8" />
+ <!-- Layer the effects together -->
+ <feMerge id="feMerge14">
+ <feMergeNode in="softGlow_colored" id="feMergeNode10" />
+ <feMergeNode in="SourceGraphic" id="feMergeNode12" />
+ </feMerge>
+ </filter>
+*/
+
+/**
+ * Returns a new document containing default start, mid, and end markers.
+ * Note 1: group IDs are matched against "_combo_id" to render correct preview object.
+ * Note 2: paths/lines are kept outside of groups, so they don't inflate visible bounds
+ * Note 3: invisible rects inside groups keep visual bounds from getting too small, so we can see relative marker sizes
+ */
+std::unique_ptr<SPDocument> MarkerComboBox::ink_markers_preview_doc(const Glib::ustring& group_id)
+{
+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">
+ <filter id="softGlow" height="1.2" width="1.2" x="0.0" y="0.0">
+ <!-- <feMorphology operator="dilate" radius="1" in="SourceAlpha" result="thicken" id="feMorphology2" /> -->
+ <!-- Use a gaussian blur to create the soft blurriness of the glow -->
+ <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred" id="feGaussianBlur4" />
+ <!-- Change the color -->
+ <feFlood flood-color="rgb(255,255,255)" result="glowColor" id="feFlood6" flood-opacity="0.70" />
+ <!-- Color in the glows -->
+ <feComposite in="glowColor" in2="blurred" operator="in" result="softGlow_colored" id="feComposite8" />
+ <!-- Layer the effects together -->
+ <feMerge id="feMerge14">
+ <feMergeNode in="softGlow_colored" id="feMergeNode10" />
+ <feMergeNode in="SourceGraphic" id="feMergeNode12" />
+ </feMerge>
+ </filter>
+ </defs>
+
+ <!-- cross at the end of the line to help position marker -->
+ <symbol id="cross" width="25" height="25" viewBox="0 0 25 25">
+ <path class="cross" style="mix-blend-mode:difference;stroke:#7ff;stroke-opacity:1;fill:none;display:block" d="M 0,0 M 25,25 M 10,10 15,15 M 10,15 15,10" />
+ <!-- <path class="cross" style="mix-blend-mode:difference;stroke:#7ff;stroke-width:1;stroke-opacity:1;fill:none;display:block;-inkscape-stroke:hairline" d="M 0,0 M 25,25 M 10,10 15,15 M 10,15 15,10" /> -->
+ </symbol>
+
+ <!-- very short path with 1px stroke used to measure size of marker -->
+ <path id="measure-marker" style="stroke-width:1.0;stroke-opacity:0.01;marker-start:url(#sample)" d="M 0,9999 m 0,0.1" />
+
+ <path id="line-marker-start" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M 12.5,12.5 l 1000,0" />
+ <!-- <g id="marker-start" class="group" style="filter:url(#softGlow)"> -->
+ <g id="marker-start" class="group">
+ <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-start:url(#sample)"
+ d="M 12.5,12.5 L 25,12.5"/>
+ <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/>
+ <use xlink:href="#cross" width="25" height="25" />
+ </g>
+
+ <path id="line-marker-mid" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M -1000,12.5 L 1000,12.5" />
+ <g id="marker-mid" class="group">
+ <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-mid:url(#sample)"
+ d="M 0,12.5 L 12.5,12.5 L 25,12.5"/>
+ <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/>
+ <use xlink:href="#cross" width="25" height="25" />
+ </g>
+
+ <path id="line-marker-end" class="line colors" style="stroke-width:2;stroke-opacity:0.2" d="M -1000,12.5 L 12.5,12.5" />
+ <g id="marker-end" class="group">
+ <path class="colors" style="stroke-width:2;stroke-opacity:0;marker-end:url(#sample)"
+ d="M 0,12.5 L 12.5,12.5"/>
+ <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/>
+ <use xlink:href="#cross" width="25" height="25" />
+ </g>
+
+ </svg>
+)A";
+
+ auto document = std::unique_ptr<SPDocument>(SPDocument::createNewDocFromMem(buffer, strlen(buffer), false));
+ // only leave requested group, so nothing else gets rendered
+ for (auto&& group : document->getObjectsByClass("group")) {
+ assert(group->getId());
+ if (group->getId() != group_id) {
+ group->deleteObject();
+ }
+ }
+ auto id = "line-" + group_id;
+ for (auto&& line : document->getObjectsByClass("line")) {
+ assert(line->getId());
+ if (line->getId() != id) {
+ line->deleteObject();
+ }
+ }
+ return document;
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/marker-combo-box.h b/src/ui/widget/marker-combo-box.h
new file mode 100644
index 0000000..46391a4
--- /dev/null
+++ b/src/ui/widget/marker-combo-box.h
@@ -0,0 +1,175 @@
+// 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/bin.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/image.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gio/gliststore.h>
+
+#include <sigc++/signal.h>
+
+#include "document.h"
+#include "inkscape.h"
+#include "scrollprotected.h"
+#include "display/drawing.h"
+#include "ui/operation-blocker.h"
+
+class SPMarker;
+
+namespace Gtk {
+
+class Container;
+class Adjustment;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * ComboBox-like class for selecting stroke markers.
+ */
+class MarkerComboBox : public Gtk::Bin {
+ using parent_type = Gtk::Bin;
+
+public:
+ MarkerComboBox(Glib::ustring id, int loc);
+ ~MarkerComboBox() override;
+
+ void setDocument(SPDocument *);
+
+ sigc::signal<void> changed_signal;
+ sigc::signal<void> edit_signal;
+
+ void set_current(SPObject *marker);
+ std::string get_active_marker_uri();
+ bool in_update() { return _update.pending(); };
+ const char* get_id() { return _combo_id.c_str(); };
+ int get_loc() { return _loc; };
+
+ sigc::signal<void()> signal_changed() { return _signal_changed; }
+
+private:
+ struct MarkerItem : Glib::Object {
+ Cairo::RefPtr<Cairo::Surface> pix;
+ SPDocument* source = nullptr;
+ std::string id;
+ std::string label;
+ bool stock = false;
+ bool history = false;
+ bool separator = false;
+ int width = 0;
+ int height = 0;
+
+ bool operator == (const MarkerItem& item) const;
+ };
+
+ SPMarker* get_current() const;
+ Glib::ustring _current_marker_id;
+ // SPMarker* _current_marker = nullptr;
+ sigc::signal<void()> _signal_changed;
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::FlowBox& _marker_list;
+ Gtk::Label& _marker_name;
+ Glib::RefPtr<Gio::ListStore<MarkerItem>> _marker_store;
+ std::vector<Glib::RefPtr<MarkerItem>> _stock_items;
+ std::vector<Glib::RefPtr<MarkerItem>> _history_items;
+ std::map<Gtk::Widget*, Glib::RefPtr<MarkerItem>> _widgets_to_markers;
+ Gtk::Image& _preview;
+ bool _preview_no_alloc = true;
+ Gtk::Button& _link_scale;
+ Gtk::SpinButton& _angle_btn;
+ Gtk::MenuButton& _menu_btn;
+ Gtk::SpinButton& _scale_x;
+ Gtk::SpinButton& _scale_y;
+ Gtk::CheckButton& _scale_with_stroke;
+ Gtk::SpinButton& _offset_x;
+ Gtk::SpinButton& _offset_y;
+ Gtk::Widget& _input_grid;
+ Gtk::RadioButton& _orient_auto_rev;
+ Gtk::RadioButton& _orient_auto;
+ Gtk::RadioButton& _orient_angle;
+ Gtk::Button& _orient_flip_horz;
+ Gtk::Image& _current_img;
+ Gtk::Button& _edit_marker;
+ bool _scale_linked = true;
+ guint32 _background_color;
+ guint32 _foreground_color;
+ Glib::ustring _combo_id;
+ int _loc;
+ OperationBlocker _update;
+ SPDocument *_document = nullptr;
+ std::unique_ptr<SPDocument> _sandbox;
+ 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<Glib::RefPtr<Gdk::Pixbuf>> pixbuf;
+ Gtk::TreeModelColumn<gboolean> history;
+ Gtk::TreeModelColumn<gboolean> separator;
+
+ MarkerColumns() {
+ add(label); add(stock); add(marker); add(history); add(separator); add(pixbuf);
+ }
+ };
+ MarkerColumns marker_columns;
+
+ void update_ui(SPMarker* marker, bool select);
+ void update_widgets_from_marker(SPMarker* marker);
+ void update_store();
+ Glib::RefPtr<MarkerItem> add_separator(bool filler);
+ void update_scale_link();
+ Glib::RefPtr<MarkerItem> get_active();
+ Glib::RefPtr<MarkerItem> find_marker_item(SPMarker* marker);
+ void on_style_updated() override;
+ void update_preview(Glib::RefPtr<MarkerItem> marker_item);
+ void update_menu_btn(Glib::RefPtr<MarkerItem> marker_item);
+ void set_active(Glib::RefPtr<MarkerItem> item);
+ void init_combo();
+ void set_history(Gtk::TreeModel::Row match_row);
+ void marker_list_from_doc(SPDocument* source, bool 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);
+ std::unique_ptr<SPDocument> ink_markers_preview_doc(const Glib::ustring& group_id);
+ Cairo::RefPtr<Cairo::Surface> create_marker_image(Geom::IntPoint pixel_size, gchar const *mname,
+ SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/, bool checkerboard, bool no_clip, double scale);
+ void refresh_after_markers_modified();
+ sigc::connection modified_connection;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+#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/ui/widget/notebook-page.cpp b/src/ui/widget/notebook-page.cpp
new file mode 100644
index 0000000..876edb6
--- /dev/null
+++ b/src/ui/widget/notebook-page.cpp
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Notebook page widget.
+ *
+ * Author:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "notebook-page.h"
+
+# include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+NotebookPage::NotebookPage(int n_rows, int n_columns, bool expand, bool fill, guint padding)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , _table(Gtk::manage(new Gtk::Grid()))
+{
+ set_name("NotebookPage");
+ set_border_width(4);
+ set_spacing(4);
+
+ _table->set_row_spacing(4);
+ _table->set_column_spacing(4);
+
+ pack_start(*_table, expand, fill, padding);
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/notebook-page.h b/src/ui/widget/notebook-page.h
new file mode 100644
index 0000000..9c9bd06
--- /dev/null
+++ b/src/ui/widget/notebook-page.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H
+#define INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H
+
+#include <gtkmm/box.h>
+
+namespace Gtk {
+class Grid;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A tabbed notebook page for dialogs.
+ */
+class NotebookPage : public Gtk::Box
+{
+public:
+
+ /**
+ * Construct a NotebookPage.
+ */
+ NotebookPage(int n_rows, int n_columns, bool expand=false, bool fill=false, guint padding=0);
+
+ Gtk::Grid& table() { return *_table; }
+
+protected:
+ Gtk::Grid *_table;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_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/ui/widget/object-composite-settings.cpp b/src/ui/widget/object-composite-settings.cpp
new file mode 100644
index 0000000..9331e71
--- /dev/null
+++ b/src/ui/widget/object-composite-settings.cpp
@@ -0,0 +1,312 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A widget for controlling object compositing (filter, opacity, etc.)
+ *
+ * Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ * Niko Kiirala <niko@kiirala.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004--2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "object-composite-settings.h"
+
+#include <utility>
+
+#include "desktop.h"
+#include "desktop-style.h"
+#include "document.h"
+#include "document-undo.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "style.h"
+
+#include "object/filters/blend.h"
+#include "svg/css-ostringstream.h"
+#include "ui/widget/style-subject.h"
+
+constexpr double BLUR_MULTIPLIER = 4.0;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ObjectCompositeSettings::ObjectCompositeSettings(Glib::ustring icon_name, char const *history_prefix, int flags)
+: Gtk::Box(Gtk::ORIENTATION_VERTICAL),
+ _icon_name(std::move(icon_name)),
+ _blend_tag(Glib::ustring(history_prefix) + ":blend"),
+ _blur_tag(Glib::ustring(history_prefix) + ":blur"),
+ _opacity_tag(Glib::ustring(history_prefix) + ":opacity"),
+ _isolation_tag(Glib::ustring(history_prefix) + ":isolation"),
+ _filter_modifier(flags),
+ _blocked(false)
+{
+ set_name( "ObjectCompositeSettings");
+
+ // Filter Effects
+ pack_start(_filter_modifier, false, false, 2);
+
+ _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_blendBlurValueChanged));
+ _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_blendBlurValueChanged));
+ _filter_modifier.signal_opacity_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_opacityValueChanged));
+ _filter_modifier.signal_isolation_changed().connect(
+ sigc::mem_fun(*this, &ObjectCompositeSettings::_isolationValueChanged));
+
+ show_all_children();
+}
+
+ObjectCompositeSettings::~ObjectCompositeSettings() {
+ setSubject(nullptr);
+}
+
+void ObjectCompositeSettings::setSubject(StyleSubject *subject) {
+ _subject_changed.disconnect();
+ if (subject) {
+ _subject = subject;
+ _subject_changed = _subject->connectChanged(sigc::mem_fun(*this, &ObjectCompositeSettings::_subjectChanged));
+ }
+}
+
+// We get away with sharing one callback for blend and blur as this is used by
+// * the Layers dialog where only one layer can be selected at a time,
+// * the Fill and Stroke dialog where only blur is used.
+// If both blend and blur are used in a dialog where more than one object can
+// be selected then this should be split into separate functions for blend and
+// blur (like in the Objects dialog).
+void
+ObjectCompositeSettings::_blendBlurValueChanged()
+{
+ if (!_subject) {
+ return;
+ }
+
+ SPDesktop *desktop = _subject->getDesktop();
+ if (!desktop) {
+ return;
+ }
+ SPDocument *document = desktop->getDocument();
+
+ if (_blocked)
+ return;
+ _blocked = true;
+
+ Geom::OptRect bbox = _subject->getBounds(SPItem::GEOMETRIC_BBOX);
+ double radius;
+ if (bbox) {
+ double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct?
+ double blur_value = _filter_modifier.get_blur_value() / 100.0;
+ radius = blur_value * blur_value * perimeter / BLUR_MULTIPLIER;
+ } else {
+ radius = 0;
+ }
+
+ //apply created filter to every selected item
+ std::vector<SPObject*> sel = _subject->list();
+ for (auto i : sel) {
+ if (!SP_IS_ITEM(i)) {
+ continue;
+ }
+ SPItem * item = SP_ITEM(i);
+ SPStyle *style = item->style;
+ g_assert(style != nullptr);
+ bool change_blend = (item->style->mix_blend_mode.set ? item->style->mix_blend_mode.value : SP_CSS_BLEND_NORMAL) != _filter_modifier.get_blend_mode();
+ // < 1.0 filter based blend removal
+ if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) {
+ remove_filter_legacy_blend(item);
+ }
+ item->style->mix_blend_mode.set = TRUE;
+ if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) {
+ item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL;
+ } else {
+ item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode();
+ }
+
+ if (radius == 0 && item->style->filter.set && item->style->getFilter()
+ && filter_is_single_gaussian_blur(item->style->getFilter())) {
+ remove_filter(item, false);
+ } else if (radius != 0) {
+ SPFilter *filter = modify_filter_gaussian_blur_from_item(document, item, radius);
+ filter->update_filter_region(item);
+ sp_style_set_property_url(item, "filter", filter, false);
+ }
+ if (change_blend) { //we do blend so we need update display style
+ item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
+ } else {
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ }
+
+ DocumentUndo::maybeDone(document, _blur_tag.c_str(), _("Change blur/blend filter"), _icon_name);
+
+ _blocked = false;
+}
+
+void
+ObjectCompositeSettings::_opacityValueChanged()
+{
+ if (!_subject) {
+ return;
+ }
+
+ SPDesktop *desktop = _subject->getDesktop();
+ if (!desktop) {
+ return;
+ }
+
+ if (_blocked)
+ return;
+ _blocked = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+
+ Inkscape::CSSOStringStream os;
+ os << CLAMP (_filter_modifier.get_opacity_value() / 100, 0.0, 1.0);
+ sp_repr_css_set_property (css, "opacity", os.str().c_str());
+
+ _subject->setCSS(css);
+
+ sp_repr_css_attr_unref (css);
+
+ DocumentUndo::maybeDone(desktop->getDocument(), _opacity_tag.c_str(), _("Change opacity"), _icon_name);
+
+ _blocked = false;
+}
+
+void ObjectCompositeSettings::_isolationValueChanged()
+{
+ if (!_subject) {
+ return;
+ }
+
+ SPDesktop *desktop = _subject->getDesktop();
+ if (!desktop) {
+ return;
+ }
+
+ if (_blocked)
+ return;
+ _blocked = true;
+
+ for (auto item : _subject->list()) {
+ item->style->isolation.set = TRUE;
+ item->style->isolation.value = _filter_modifier.get_isolation_mode();
+ if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) {
+ item->style->mix_blend_mode.set = TRUE;
+ item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL;
+ }
+ item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT);
+ }
+
+ DocumentUndo::maybeDone(desktop->getDocument(), _isolation_tag.c_str(), _("Change isolation"), _icon_name);
+
+ _blocked = false;
+}
+
+void
+ObjectCompositeSettings::_subjectChanged() {
+ if (!_subject) {
+ return;
+ }
+
+ SPDesktop *desktop = _subject->getDesktop();
+ if (!desktop) {
+ return;
+ }
+
+ if (_blocked)
+ return;
+ _blocked = true;
+ SPStyle query(desktop->getDocument());
+ int result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_MASTEROPACITY);
+
+ switch (result) {
+ case QUERY_STYLE_NOTHING:
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _filter_modifier.set_opacity_value(100 * SP_SCALE24_TO_FLOAT(query.opacity.value));
+ break;
+ }
+
+ //query now for current filter mode and average blurring of selection
+ const int isolation_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_ISOLATION);
+ switch (isolation_result) {
+ case QUERY_STYLE_NOTHING:
+ _filter_modifier.set_isolation_mode(SP_CSS_ISOLATION_AUTO, false);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _filter_modifier.set_isolation_mode(query.isolation.value, true); // here dont work mix_blend_mode.set
+ break;
+ case QUERY_STYLE_MULTIPLE_DIFFERENT:
+ _filter_modifier.set_isolation_mode(SP_CSS_ISOLATION_AUTO, false);
+ // TODO: set text
+ break;
+ }
+
+ // query now for current filter mode and average blurring of selection
+ const int blend_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_BLEND);
+ switch(blend_result) {
+ case QUERY_STYLE_NOTHING:
+ _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _filter_modifier.set_blend_mode(query.mix_blend_mode.value, true); // here dont work mix_blend_mode.set
+ break;
+ case QUERY_STYLE_MULTIPLE_DIFFERENT:
+ _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false);
+ break;
+ }
+
+ int blur_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_BLUR);
+ switch (blur_result) {
+ case QUERY_STYLE_NOTHING: // no blurring
+ _filter_modifier.set_blur_value(0);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ Geom::OptRect bbox = _subject->getBounds(SPItem::GEOMETRIC_BBOX);
+ if (bbox) {
+ double perimeter =
+ bbox->dimensions()[Geom::X] +
+ bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct?
+ // update blur widget value
+ float radius = query.filter_gaussianBlur_deviation.value;
+ float percent = std::sqrt(radius * BLUR_MULTIPLIER / perimeter) * 100;
+ _filter_modifier.set_blur_value(percent);
+ }
+ break;
+ }
+
+ // If we have nothing selected, disable dialog.
+ if (result == QUERY_STYLE_NOTHING &&
+ blend_result == QUERY_STYLE_NOTHING ) {
+ _filter_modifier.set_sensitive( false );
+ } else {
+ _filter_modifier.set_sensitive( true );
+ }
+
+ _blocked = 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/ui/widget/object-composite-settings.h b/src/ui/widget/object-composite-settings.h
new file mode 100644
index 0000000..bb2ef85
--- /dev/null
+++ b/src/ui/widget/object-composite-settings.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_UI_WIDGET_OBJECT_COMPOSITE_SETTINGS_H
+#define SEEN_UI_WIDGET_OBJECT_COMPOSITE_SETTINGS_H
+
+/*
+ * Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ *
+ * Copyright (C) 2004--2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+
+#include "ui/widget/filter-effect-chooser.h"
+
+class SPDesktop;
+struct InkscapeApplication;
+
+namespace Inkscape {
+
+namespace UI {
+namespace Widget {
+
+class StyleSubject;
+
+/*
+ * A widget for controlling object compositing (filter, opacity, etc.)
+ */
+class ObjectCompositeSettings : public Gtk::Box {
+public:
+ ObjectCompositeSettings(Glib::ustring icon_name, char const *history_prefix, int flags);
+ ~ObjectCompositeSettings() override;
+
+ void setSubject(StyleSubject *subject);
+
+private:
+ Glib::ustring _icon_name; // Used by History dialog.
+
+ Glib::ustring _blend_tag;
+ Glib::ustring _blur_tag;
+ Glib::ustring _opacity_tag;
+ Glib::ustring _isolation_tag;
+
+ StyleSubject *_subject = nullptr;
+
+ SimpleFilterModifier _filter_modifier;
+
+ bool _blocked;
+ gulong _desktop_activated;
+ sigc::connection _subject_changed;
+
+ static void _on_desktop_activate(SPDesktop *desktop, ObjectCompositeSettings *w);
+ static void _on_desktop_deactivate(SPDesktop *desktop, ObjectCompositeSettings *w);
+ void _subjectChanged();
+ void _blendBlurValueChanged();
+ void _opacityValueChanged();
+ void _isolationValueChanged();
+};
+
+}
+}
+}
+
+#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/ui/widget/page-properties.cpp b/src/ui/widget/page-properties.cpp
new file mode 100644
index 0000000..effc6b2
--- /dev/null
+++ b/src/ui/widget/page-properties.cpp
@@ -0,0 +1,515 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ *
+ * Document properties widget: viewbox, document size, colors
+ */
+/*
+ * Authors:
+ * Mike Kowalski
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/togglebutton.h>
+
+#include <type_traits>
+
+#include "page-properties.h"
+#include "page-size-preview.h"
+#include "util/paper.h"
+#include "ui/widget/registry.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/builder-utils.h"
+#include "ui/operation-blocker.h"
+
+using Inkscape::UI::create_builder;
+using Inkscape::UI::get_widget;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void show_widget(Gtk::Widget& widget, bool show) {
+ if (show) {
+ widget.show();
+ }
+ else {
+ widget.hide();
+ }
+};
+
+const char* g_linked = "entries-linked-symbolic";
+const char* g_unlinked = "entries-unlinked-symbolic";
+
+#define GET(prop, id) prop(get_widget<std::remove_reference_t<decltype(prop)>>(_builder, id))
+
+class PagePropertiesBox : public PageProperties {
+public:
+ PagePropertiesBox() :
+ _builder(create_builder("page-properties.glade")),
+ GET(_main_grid, "main-grid"),
+ GET(_left_grid, "left-grid"),
+ GET(_page_width, "page-width"),
+ GET(_page_height, "page-height"),
+ GET(_portrait, "page-portrait"),
+ GET(_landscape, "page-landscape"),
+ GET(_scale_x, "scale-x"),
+ GET(_doc_units, "user-units"),
+ GET(_unsupported_size, "unsupported"),
+ GET(_nonuniform_scale, "nonuniform-scale"),
+ GET(_viewbox_x, "viewbox-x"),
+ GET(_viewbox_y, "viewbox-y"),
+ GET(_viewbox_width, "viewbox-width"),
+ GET(_viewbox_height, "viewbox-height"),
+ GET(_page_templates_menu, "page-templates-menu"),
+ GET(_template_name, "page-template-name"),
+ GET(_preview_box, "preview-box"),
+ GET(_checkerboard, "checkerboard"),
+ GET(_antialias, "use-antialias"),
+ GET(_border, "border"),
+ GET(_border_on_top, "border-top"),
+ GET(_shadow, "shadow"),
+ GET(_link_width_height, "link-width-height"),
+ GET(_viewbox_expander, "viewbox-expander"),
+ GET(_linked_viewbox_scale, "linked-scale-img")
+ {
+#undef GET
+
+ _backgnd_color_picker = std::make_unique<ColorPicker>(
+ _("Background color"), "", 0xffffff00, true,
+ &get_widget<Gtk::Button>(_builder, "background-color"));
+
+ _border_color_picker = std::make_unique<ColorPicker>(
+ _("Border and shadow color"), "", 0x0000001f, true,
+ &get_widget<Gtk::Button>(_builder, "border-color"));
+
+ _desk_color_picker = std::make_unique<ColorPicker>(
+ _("Desk color"), "", 0xd0d0d0ff, true,
+ &get_widget<Gtk::Button>(_builder, "desk-color"));
+ _desk_color_picker->use_transparency(false);
+
+ for (auto element : {Color::Background, Color::Border, Color::Desk}) {
+ get_color_picker(element).connectChanged([=](guint rgba) {
+ update_preview_color(element, rgba);
+ if (_update.pending()) return;
+ _signal_color_changed.emit(rgba, element);
+ });
+ }
+
+ _builder->get_widget_derived("display-units", _display_units);
+ _display_units->setUnitType(UNIT_TYPE_LINEAR);
+ _display_units->signal_changed().connect([=](){ set_display_unit(); });
+
+ _builder->get_widget_derived("page-units", _page_units);
+ _page_units->setUnitType(UNIT_TYPE_LINEAR);
+ _current_page_unit = _page_units->getUnit();
+ _page_units->signal_changed().connect([=](){ set_page_unit(); });
+
+ for (auto&& page : PaperSize::getPageSizes()) {
+ auto item = Gtk::manage(new Gtk::MenuItem(page.getDescription(false)));
+ item->show();
+ _page_templates_menu.append(*item);
+ item->signal_activate().connect([=](){ set_page_template(page); });
+ }
+
+ _preview->set_hexpand();
+ _preview->set_vexpand();
+ _preview_box.add(*_preview);
+
+ for (auto check : {Check::Border, Check::Shadow, Check::Checkerboard, Check::BorderOnTop, Check::AntiAlias}) {
+ auto checkbutton = &get_checkbutton(check);
+ checkbutton->signal_toggled().connect([=](){ fire_checkbox_toggled(*checkbutton, check); });
+ }
+ _border.signal_toggled().connect([=](){
+ _preview->draw_border(_border.get_active());
+ });
+ _shadow.signal_toggled().connect([=](){
+ //
+ _preview->enable_drop_shadow(_shadow.get_active());
+ });
+ _checkerboard.signal_toggled().connect([=](){
+ _preview->enable_checkerboard(_checkerboard.get_active());
+ });
+
+ _viewbox_expander.property_expanded().signal_changed().connect([=](){
+ // hide/show viewbox controls
+ show_viewbox(_viewbox_expander.get_expanded());
+ });
+ show_viewbox(_viewbox_expander.get_expanded());
+
+ _link_width_height.signal_clicked().connect([=](){
+ // toggle size link
+ _locked_size_ratio = !_locked_size_ratio;
+ // set image
+ _link_width_height.set_image_from_icon_name(_locked_size_ratio && _size_ratio > 0 ? g_linked : g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ });
+ _link_width_height.set_image_from_icon_name(g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ // set image for linked scale
+ _linked_viewbox_scale.set_from_icon_name(g_linked, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+
+ // report page size changes
+ _page_width .signal_value_changed().connect([=](){ set_page_size_linked(true); });
+ _page_height.signal_value_changed().connect([=](){ set_page_size_linked(false); });
+ // enforce uniform scale thru viewbox
+ _viewbox_width. signal_value_changed().connect([=](){ set_viewbox_size_linked(true); });
+ _viewbox_height.signal_value_changed().connect([=](){ set_viewbox_size_linked(false); });
+
+ _landscape.signal_toggled().connect([=](){ if (_landscape.get_active()) swap_width_height(); });
+ _portrait .signal_toggled().connect([=](){ if (_portrait .get_active()) swap_width_height(); });
+
+ for (auto dim : {Dimension::Scale, Dimension::ViewboxPosition}) {
+ auto pair = get_dimension(dim);
+ auto b1 = &pair.first;
+ auto b2 = &pair.second;
+ if (dim == Dimension::Scale) {
+ // uniform scale: report the same x and y
+ b1->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b1, nullptr, dim); });
+ }
+ else {
+ b1->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b2, nullptr, dim); });
+ b2->signal_value_changed().connect([=](){ fire_value_changed(*b1, *b2, nullptr, dim); });
+ }
+ }
+
+ auto& page_resize = get_widget<Gtk::Button>(_builder, "page-resize");
+ page_resize.signal_clicked().connect([=](){ _signal_resize_to_fit.emit(); });
+
+ add(_main_grid);
+ show();
+ }
+
+private:
+
+ void show_viewbox(bool show_widgets) {
+ auto show = [=](Gtk::Widget* w) { show_widget(*w, show_widgets); };
+
+ for (auto&& widget : _left_grid.get_children()) {
+ if (widget->get_style_context()->has_class("viewbox")) {
+ show(widget);
+ }
+ }
+ }
+
+ void update_preview_color(Color element, guint rgba) {
+ switch (element) {
+ case Color::Desk: _preview->set_desk_color(rgba); break;
+ case Color::Border: _preview->set_border_color(rgba); break;
+ case Color::Background: _preview->set_page_color(rgba); break;
+ }
+ }
+
+ void set_page_template(const PaperSize& page) {
+ if (_update.pending()) return;
+
+ {
+ auto scoped(_update.block());
+ auto width = page.width;
+ auto height = page.height;
+ if (_landscape.get_active() != (width > height)) {
+ std::swap(width, height);
+ }
+ _page_width.set_value(width);
+ _page_height.set_value(height);
+ _page_units->setUnit(page.unit->abbr);
+ _doc_units.set_text(page.unit->abbr);
+ _current_page_unit = _page_units->getUnit();
+ if (width > 0 && height > 0) {
+ _size_ratio = width / height;
+ }
+ }
+ set_page_size(true);
+ }
+
+ void changed_linked_value(bool width_changing, Gtk::SpinButton& wedit, Gtk::SpinButton& hedit) {
+ if (_size_ratio > 0) {
+ auto scoped(_update.block());
+ if (width_changing) {
+ auto width = wedit.get_value();
+ hedit.set_value(width / _size_ratio);
+ }
+ else {
+ auto height = hedit.get_value();
+ wedit.set_value(height * _size_ratio);
+ }
+ }
+ }
+
+ void set_viewbox_size_linked(bool width_changing) {
+ if (_update.pending()) return;
+
+ if (_scale_is_uniform) {
+ // viewbox size - width and height always linked to make scaling uniform
+ changed_linked_value(width_changing, _viewbox_width, _viewbox_height);
+ }
+
+ auto width = _viewbox_width.get_value();
+ auto height = _viewbox_height.get_value();
+ _signal_dimmension_changed.emit(width, height, nullptr, Dimension::ViewboxSize);
+ }
+
+ void set_page_size_linked(bool width_changing) {
+ if (_update.pending()) return;
+
+ // if size ratio is locked change the other dimension too
+ if (_locked_size_ratio) {
+ changed_linked_value(width_changing, _page_width, _page_height);
+ }
+ set_page_size();
+ }
+
+ void set_page_size(bool template_selected = false) {
+ auto pending = _update.pending();
+
+ auto scoped(_update.block());
+
+ auto unit = _page_units->getUnit();
+ auto width = _page_width.get_value();
+ auto height = _page_height.get_value();
+ _preview->set_page_size(width, height);
+ if (width != height) {
+ (width > height ? _landscape : _portrait).set_active();
+ _portrait.set_sensitive();
+ _landscape.set_sensitive();
+ }
+ else {
+ _portrait.set_sensitive(false);
+ _landscape.set_sensitive(false);
+ }
+ if (width > 0 && height > 0) {
+ _size_ratio = width / height;
+ }
+
+ auto templ = find_page_template(width, height, *unit);
+ _template_name.set_label(templ ? templ->name : _("Custom"));
+
+ if (!pending) {
+ _signal_dimmension_changed.emit(width, height, unit, template_selected ? Dimension::PageTemplate : Dimension::PageSize);
+ }
+ }
+
+ void swap_width_height() {
+ if (_update.pending()) return;
+
+ {
+ auto scoped(_update.block());
+ auto width = _page_width.get_value();
+ auto height = _page_height.get_value();
+ _page_width.set_value(height);
+ _page_height.set_value(width);
+ }
+ set_page_size();
+ };
+
+ void set_display_unit() {
+ if (_update.pending()) return;
+
+ const auto unit = _display_units->getUnit();
+ _signal_unit_changed.emit(unit, Units::Display);
+ }
+
+ void set_page_unit() {
+ if (_update.pending()) return;
+
+ const auto old_unit = _current_page_unit;
+ _current_page_unit = _page_units->getUnit();
+ const auto new_unit = _current_page_unit;
+
+ {
+ auto width = _page_width.get_value();
+ auto height = _page_height.get_value();
+ Quantity w(width, old_unit->abbr);
+ Quantity h(height, old_unit->abbr);
+ auto scoped(_update.block());
+ _page_width.set_value(w.value(new_unit));
+ _page_height.set_value(h.value(new_unit));
+ }
+ _doc_units.set_text(new_unit->abbr);
+ set_page_size();
+ _signal_unit_changed.emit(new_unit, Units::Document);
+ }
+
+ void set_color(Color element, unsigned int color) override {
+ auto scoped(_update.block());
+
+ get_color_picker(element).setRgba32(color);
+ update_preview_color(element, color);
+ }
+
+ void set_check(Check element, bool checked) override {
+ auto scoped(_update.block());
+
+ if (element == Check::NonuniformScale) {
+ show_widget(_nonuniform_scale, checked);
+ _scale_is_uniform = !checked;
+ _scale_x.set_sensitive(_scale_is_uniform);
+ _linked_viewbox_scale.set_from_icon_name(_scale_is_uniform ? g_linked : g_unlinked, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+ }
+ else if (element == Check::DisabledScale) {
+ _scale_x.set_sensitive(!checked);
+ }
+ else if (element == Check::UnsupportedSize) {
+ show_widget(_unsupported_size, checked);
+ }
+ else {
+ get_checkbutton(element).set_active(checked);
+
+ // special cases
+ if (element == Check::Checkerboard) _preview->enable_checkerboard(checked);
+ if (element == Check::Shadow) _preview->enable_drop_shadow(checked);
+ if (element == Check::Border) _preview->draw_border(checked);
+ }
+ }
+
+ void set_dimension(Dimension dimension, double x, double y) override {
+ auto scoped(_update.block());
+
+ auto dim = get_dimension(dimension);
+ dim.first.set_value(x);
+ dim.second.set_value(y);
+
+ set_page_size();
+ }
+
+ void set_unit(Units unit, const Glib::ustring& abbr) override {
+ auto scoped(_update.block());
+
+ if (unit == Units::Display) {
+ _display_units->setUnit(abbr);
+ }
+ else if (unit == Units::Document) {
+ _doc_units.set_text(abbr);
+ _page_units->setUnit(abbr);
+ _current_page_unit = _page_units->getUnit();
+ set_page_size();
+ }
+ }
+
+ ColorPicker& get_color_picker(Color element) {
+ switch (element) {
+ case Color::Background: return *_backgnd_color_picker;
+ case Color::Desk: return *_desk_color_picker;
+ case Color::Border: return *_border_color_picker;
+
+ default:
+ throw std::runtime_error("missing case in get_color_picker");
+ }
+ }
+
+ void fire_value_changed(Gtk::SpinButton& b1, Gtk::SpinButton& b2, const Util::Unit* unit, Dimension dim) {
+ if (!_update.pending()) {
+ _signal_dimmension_changed.emit(b1.get_value(), b2.get_value(), unit, dim);
+ }
+ }
+
+ void fire_checkbox_toggled(Gtk::CheckButton& checkbox, Check check) {
+ if (!_update.pending()) {
+ _signal_check_toggled.emit(checkbox.get_active(), check);
+ }
+ }
+
+ const PaperSize* find_page_template(double width, double height, const Unit& unit) {
+ Quantity w(std::min(width, height), &unit);
+ Quantity h(std::max(width, height), &unit);
+
+ const double eps = 1e-6;
+ for (auto&& page : PaperSize::getPageSizes()) {
+ Quantity pw(std::min(page.width, page.height), page.unit);
+ Quantity ph(std::max(page.width, page.height), page.unit);
+
+ if (are_near(w, pw, eps) && are_near(h, ph, eps)) {
+ return &page;
+ }
+ }
+
+ return nullptr;
+ }
+
+ Gtk::CheckButton& get_checkbutton(Check check) {
+ switch (check) {
+ case Check::AntiAlias: return _antialias;
+ case Check::Border: return _border;
+ case Check::Shadow: return _shadow;
+ case Check::BorderOnTop: return _border_on_top;
+ case Check::Checkerboard: return _checkerboard;
+
+ default:
+ throw std::runtime_error("missing case in get_checkbutton");
+ }
+ }
+
+ typedef std::pair<Gtk::SpinButton&, Gtk::SpinButton&> spin_pair;
+ spin_pair get_dimension(Dimension dimension) {
+ switch (dimension) {
+ case Dimension::PageSize: return spin_pair(_page_width, _page_height);
+ case Dimension::PageTemplate: return spin_pair(_page_width, _page_height);
+ case Dimension::Scale: return spin_pair(_scale_x, _scale_x);
+ case Dimension::ViewboxPosition: return spin_pair(_viewbox_x, _viewbox_y);
+ case Dimension::ViewboxSize: return spin_pair(_viewbox_width, _viewbox_height);
+
+ default:
+ throw std::runtime_error("missing case in get_dimension");
+ }
+ }
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::Grid& _main_grid;
+ Gtk::Grid& _left_grid;
+ Gtk::SpinButton& _page_width;
+ Gtk::SpinButton& _page_height;
+ Gtk::RadioButton& _portrait;
+ Gtk::RadioButton& _landscape;
+ Gtk::SpinButton& _scale_x;
+ Gtk::Label& _unsupported_size;
+ Gtk::Label& _nonuniform_scale;
+ Gtk::Label& _doc_units;
+ Gtk::SpinButton& _viewbox_x;
+ Gtk::SpinButton& _viewbox_y;
+ Gtk::SpinButton& _viewbox_width;
+ Gtk::SpinButton& _viewbox_height;
+ std::unique_ptr<ColorPicker> _backgnd_color_picker;
+ std::unique_ptr<ColorPicker> _border_color_picker;
+ std::unique_ptr<ColorPicker> _desk_color_picker;
+ Gtk::Menu& _page_templates_menu;
+ Gtk::Label& _template_name;
+ Gtk::Box& _preview_box;
+ std::unique_ptr<PageSizePreview> _preview = std::make_unique<PageSizePreview>();
+ Gtk::CheckButton& _border;
+ Gtk::CheckButton& _border_on_top;
+ Gtk::CheckButton& _shadow;
+ Gtk::CheckButton& _checkerboard;
+ Gtk::CheckButton& _antialias;
+ Gtk::Button& _link_width_height;
+ UnitMenu *_display_units;
+ UnitMenu *_page_units;
+ const Unit* _current_page_unit = nullptr;
+ OperationBlocker _update;
+ double _size_ratio = 1; // width to height ratio
+ bool _locked_size_ratio = false;
+ bool _scale_is_uniform = true;
+ Gtk::Expander& _viewbox_expander;
+ Gtk::Image& _linked_viewbox_scale;
+};
+
+PageProperties* PageProperties::create() {
+ return new PagePropertiesBox();
+}
+
+
+} } } // namespace Inkscape/Widget/UI
diff --git a/src/ui/widget/page-properties.h b/src/ui/widget/page-properties.h
new file mode 100644
index 0000000..37779e5
--- /dev/null
+++ b/src/ui/widget/page-properties.h
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H
+#define INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H
+
+#include <gtkmm/box.h>
+
+namespace Inkscape {
+ namespace Util { class Unit; }
+namespace UI {
+namespace Widget {
+
+class PageProperties : public Gtk::Box {
+public:
+ static PageProperties* create();
+
+ ~PageProperties() override = default;
+
+ enum class Color { Background, Desk, Border };
+ virtual void set_color(Color element, unsigned int rgba) = 0;
+
+ sigc::signal<void (unsigned int, Color)>& signal_color_changed() { return _signal_color_changed; }
+
+ enum class Check { Checkerboard, Border, Shadow, BorderOnTop, AntiAlias, NonuniformScale, DisabledScale, UnsupportedSize };
+ virtual void set_check(Check element, bool checked) = 0;
+
+ sigc::signal<void (bool, Check)>& signal_check_toggled() { return _signal_check_toggled; }
+
+ enum class Dimension { PageSize, ViewboxSize, ViewboxPosition, Scale, PageTemplate };
+ virtual void set_dimension(Dimension dim, double x, double y) = 0;
+
+ sigc::signal<void (double, double, const Util::Unit*, Dimension)>& signal_dimmension_changed() { return _signal_dimmension_changed; }
+
+ enum class Units { Display, Document };
+ virtual void set_unit(Units unit, const Glib::ustring& abbr) = 0;
+
+ sigc::signal<void (const Util::Unit*, Units)> signal_unit_changed() { return _signal_unit_changed; }
+
+ sigc::signal<void ()> signal_resize_to_fit() { return _signal_resize_to_fit; }
+
+protected:
+ sigc::signal<void (unsigned int, Color)> _signal_color_changed;
+ sigc::signal<void (bool, Check)> _signal_check_toggled;
+ sigc::signal<void (double, double, const Util::Unit*, Dimension)> _signal_dimmension_changed;
+ sigc::signal<void (const Util::Unit*, Units)> _signal_unit_changed;
+ sigc::signal<void ()> _signal_resize_to_fit;
+};
+
+} } } // namespace Inkscape/Widget/UI
+
+#endif // INKSCAPE_UI_WIDGET_PAGE_PROPERTIES_H
diff --git a/src/ui/widget/page-selector.cpp b/src/ui/widget/page-selector.cpp
new file mode 100644
index 0000000..baa258b
--- /dev/null
+++ b/src/ui/widget/page-selector.cpp
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::Widgets::PageSelector - select and move to pages
+ *
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2021 Martin Owens
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "page-selector.h"
+
+#include <cstring>
+#include <glibmm/i18n.h>
+#include <string>
+
+#include "desktop.h"
+#include "document.h"
+#include "object/sp-namedview.h"
+#include "object/sp-page.h"
+#include "page-manager.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+PageSelector::PageSelector(SPDesktop *desktop)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , _desktop(desktop)
+{
+ set_name("PageSelector");
+
+ _prev_button.add(*Gtk::manage(sp_get_icon_image(INKSCAPE_ICON("pan-start"), Gtk::ICON_SIZE_MENU)));
+ _prev_button.set_relief(Gtk::RELIEF_NONE);
+ _prev_button.set_tooltip_text(_("Move to previous page"));
+ _prev_button.signal_clicked().connect(sigc::mem_fun(*this, &PageSelector::prevPage));
+
+ _next_button.add(*Gtk::manage(sp_get_icon_image(INKSCAPE_ICON("pan-end"), Gtk::ICON_SIZE_MENU)));
+ _next_button.set_relief(Gtk::RELIEF_NONE);
+ _next_button.set_tooltip_text(_("Move to next page"));
+ _next_button.signal_clicked().connect(sigc::mem_fun(*this, &PageSelector::nextPage));
+
+ _selector.set_tooltip_text(_("Current page"));
+
+ _page_model = Gtk::ListStore::create(_model_columns);
+ _selector.set_model(_page_model);
+ _selector.pack_start(_label_renderer);
+ _selector.set_cell_data_func(_label_renderer, sigc::mem_fun(*this, &PageSelector::renderPageLabel));
+
+ _selector_changed_connection =
+ _selector.signal_changed().connect(sigc::mem_fun(*this, &PageSelector::setSelectedPage));
+
+ pack_start(_prev_button, Gtk::PACK_EXPAND_PADDING);
+ pack_start(_selector, Gtk::PACK_EXPAND_WIDGET);
+ pack_start(_next_button, Gtk::PACK_EXPAND_PADDING);
+
+ _doc_replaced_connection =
+ _desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &PageSelector::setDocument)));
+
+ this->show_all();
+ this->set_no_show_all();
+ setDocument(desktop->getDocument());
+}
+
+PageSelector::~PageSelector()
+{
+ _doc_replaced_connection.disconnect();
+ _selector_changed_connection.disconnect();
+ setDocument(nullptr);
+}
+
+void PageSelector::setDocument(SPDocument *document)
+{
+ _document = document;
+ _pages_changed_connection.disconnect();
+ _page_selected_connection.disconnect();
+ if (document) {
+ auto &page_manager = document->getPageManager();
+ _pages_changed_connection =
+ page_manager.connectPagesChanged(sigc::mem_fun(*this, &PageSelector::pagesChanged));
+ _page_selected_connection =
+ page_manager.connectPageSelected(sigc::mem_fun(*this, &PageSelector::selectonChanged));
+ pagesChanged();
+ }
+}
+
+void PageSelector::pagesChanged()
+{
+ _selector_changed_connection.block();
+ auto &page_manager = _document->getPageManager();
+
+ // Destroy all existing pages in the model.
+ while (!_page_model->children().empty()) {
+ Gtk::ListStore::iterator row(_page_model->children().begin());
+ // Put cleanup here if any
+ _page_model->erase(row);
+ }
+
+ // Hide myself when there's no pages (single page document)
+ this->set_visible(page_manager.hasPages());
+
+ // Add in pages, do not use getResourcelist("page") because the items
+ // are not guaranteed to be in node order, they are in first-seen order.
+ for (auto &page : page_manager.getPages()) {
+ Gtk::ListStore::iterator row(_page_model->append());
+ row->set_value(_model_columns.object, page);
+ }
+
+ selectonChanged(page_manager.getSelected());
+
+ _selector_changed_connection.unblock();
+}
+
+void PageSelector::selectonChanged(SPPage *page)
+{
+ _next_button.set_sensitive(_document->getPageManager().hasNextPage());
+ _prev_button.set_sensitive(_document->getPageManager().hasPrevPage());
+
+ auto active = _selector.get_active();
+
+ if (!active || active->get_value(_model_columns.object) != page) {
+ for (auto row : _page_model->children()) {
+ if (page == row->get_value(_model_columns.object)) {
+ _selector.set_active(row);
+ return;
+ }
+ }
+ }
+}
+
+/**
+ * Render the page icon into a suitable label.
+ */
+void PageSelector::renderPageLabel(Gtk::TreeModel::const_iterator const &row)
+{
+ SPPage *page = (*row)[_model_columns.object];
+
+ if (page && page->getRepr()) {
+ int page_num = page->getPagePosition();
+
+ gchar *format;
+ if (auto label = page->label()) {
+ format = g_strdup_printf("<span size=\"smaller\"><tt>%d.</tt>%s</span>", page_num, label);
+ } else {
+ format = g_strdup_printf("<span size=\"smaller\"><i>%s</i></span>", page->getDefaultLabel().c_str());
+ }
+
+ _label_renderer.property_markup() = format;
+ g_free(format);
+ } else {
+ _label_renderer.property_markup() = "⚠️";
+ }
+
+ _label_renderer.property_ypad() = 1;
+}
+
+void PageSelector::setSelectedPage()
+{
+ SPPage *page = _selector.get_active()->get_value(_model_columns.object);
+ if (page && _document->getPageManager().selectPage(page)) {
+ _document->getPageManager().zoomToSelectedPage(_desktop);
+ }
+}
+
+void PageSelector::nextPage()
+{
+ if (_document->getPageManager().selectNextPage()) {
+ _document->getPageManager().zoomToSelectedPage(_desktop);
+ }
+}
+
+void PageSelector::prevPage()
+{
+ if (_document->getPageManager().selectPrevPage()) {
+ _document->getPageManager().zoomToSelectedPage(_desktop);
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/page-selector.h b/src/ui/widget/page-selector.h
new file mode 100644
index 0000000..a439386
--- /dev/null
+++ b/src/ui/widget/page-selector.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::UI::Widget::PageSelector - page selector widget
+ *
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 2004 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_WIDGETS_PAGE_SELECTOR
+#define SEEN_INKSCAPE_WIDGETS_PAGE_SELECTOR
+
+#include <gtkmm/box.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/treemodel.h>
+#include <sigc++/slot.h>
+
+#include "object/sp-page.h"
+
+class SPDesktop;
+class SPDocument;
+class SPPage;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// class DocumentTreeModel;
+
+class PageSelector : public Gtk::Box
+{
+public:
+ PageSelector(SPDesktop *desktop = nullptr);
+ ~PageSelector() override;
+
+private:
+ class PageModelColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Gtk::TreeModelColumn<SPPage *> object;
+
+ PageModelColumns() { add(object); }
+ };
+
+ SPDesktop *_desktop;
+ SPDocument *_document;
+
+ Gtk::ComboBox _selector;
+ Gtk::Button _prev_button;
+ Gtk::Button _next_button;
+
+ PageModelColumns _model_columns;
+ Gtk::CellRendererText _label_renderer;
+ Glib::RefPtr<Gtk::ListStore> _page_model;
+
+ sigc::connection _selector_changed_connection;
+ sigc::connection _pages_changed_connection;
+ sigc::connection _page_selected_connection;
+ sigc::connection _doc_replaced_connection;
+
+ void setDocument(SPDocument *document);
+ void pagesChanged();
+ void selectonChanged(SPPage *page);
+
+ void renderPageLabel(Gtk::TreeModel::const_iterator const &row);
+ void setSelectedPage();
+ void nextPage();
+ void prevPage();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#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/ui/widget/page-size-preview.cpp b/src/ui/widget/page-size-preview.cpp
new file mode 100644
index 0000000..40e2ea5
--- /dev/null
+++ b/src/ui/widget/page-size-preview.cpp
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ *
+ * Page size preview widget
+ */
+/*
+ * Authors:
+ * Mike Kowalski
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "page-size-preview.h"
+#include "display/cairo-utils.h"
+#include "2geom/rect.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+PageSizePreview::PageSizePreview() {
+ show();
+}
+
+void rounded_rectangle(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double w, double h, double r) {
+ cr->begin_new_sub_path();
+ cr->arc(x + r, y + r, r, M_PI, 3 * M_PI / 2);
+ cr->arc(x + w - r, y + r, r, 3 * M_PI / 2, 2 * M_PI);
+ cr->arc(x + w - r, y + h - r, r, 0, M_PI / 2);
+ cr->arc(x + r, y + h - r, r, M_PI / 2, M_PI);
+ cr->close_path();
+}
+
+void set_source_rgba(const Cairo::RefPtr<Cairo::Context>& ctx, unsigned int rgba) {
+ ctx->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba));
+}
+
+bool PageSizePreview::on_draw(const Cairo::RefPtr<Cairo::Context>& ctx) {
+ auto alloc = get_allocation();
+ double width = alloc.get_width();
+ double height = alloc.get_height();
+ // too small to fit anything?
+ if (width <= 2 || height <= 2) return false;
+
+ double x = 0;//alloc.get_x();
+ double y = 0;//alloc.get_y();
+
+ if (_draw_checkerboard) {
+ // auto device_scale = get_scale_factor();
+ Cairo::RefPtr<Cairo::Pattern> pattern(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(_desk_color)));
+ ctx->save();
+ ctx->set_operator(Cairo::OPERATOR_SOURCE);
+ ctx->set_source(pattern);
+ rounded_rectangle(ctx, x, y, width, height, 2.0);
+ ctx->fill();
+ ctx->restore();
+ }
+ else {
+ rounded_rectangle(ctx, x, y, width, height, 2.0);
+ set_source_rgba(ctx, _desk_color);
+ ctx->fill();
+ }
+
+ // use lesser dimension to prevent page from changing size when
+ // switching from portrait to landscape or vice versa
+ auto size = std::round(std::min(width, height) * 0.90); // 90% to leave margins
+ double w, h;
+ if (_width > _height) {
+ w = size;
+ h = std::round(size * _height / _width);
+ }
+ else {
+ h = size;
+ w = std::round(size * _width / _height);
+ }
+ if (w < 2) w = 2;
+ if (h < 2) h = 2;
+
+ // center page
+ double ox = std::round(x + (width - w) / 2);
+ double oy = std::round(y + (height - h) / 2);
+ Geom::Rect rect(ox, oy, ox + w, oy + h);
+
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+
+ if (_draw_checkerboard) {
+ Cairo::RefPtr<Cairo::Pattern> pattern(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(_page_color)));
+ ctx->save();
+ ctx->set_operator(Cairo::OPERATOR_SOURCE);
+ ctx->set_source(pattern);
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ ctx->fill();
+ ctx->restore();
+ }
+ else {
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ set_source_rgba(ctx, _page_color | 0xff);
+ ctx->fill();
+ }
+
+ // draw cross
+ {
+ double gradient_size = 4;
+ double cx = std::round(x + (width - gradient_size) / 2);
+ double cy = std::round(y + (height - gradient_size) / 2);
+ auto horz = Cairo::LinearGradient::create(x, cy, x, cy + gradient_size);
+ auto vert = Cairo::LinearGradient::create(cx, y, cx + gradient_size, y);
+
+ horz->add_color_stop_rgba(0.0, 0, 0, 0, 0.0);
+ horz->add_color_stop_rgba(0.5, 0, 0, 0, 0.2);
+ horz->add_color_stop_rgba(0.5, 1, 1, 1, 0.8);
+ horz->add_color_stop_rgba(1.0, 1, 1, 1, 0.0);
+
+ vert->add_color_stop_rgba(0.0, 0, 0, 0, 0.0);
+ vert->add_color_stop_rgba(0.5, 0, 0, 0, 0.2);
+ vert->add_color_stop_rgba(0.5, 1, 1, 1, 0.8);
+ vert->add_color_stop_rgba(1.0, 1, 1, 1, 0.0);
+
+ ctx->rectangle(x, cy, width, gradient_size);
+ ctx->set_source(horz);
+ ctx->fill();
+
+ ctx->rectangle(cx, y, gradient_size, height);
+ ctx->set_source(vert);
+ ctx->fill();
+ }
+
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ set_source_rgba(ctx, _page_color);
+ ctx->fill();
+
+ if (_draw_border) {
+ // stoke; not pixel aligned, just like page on canvas
+ ctx->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ set_source_rgba(ctx, _border_color);
+ ctx->set_line_width(1);
+ ctx->stroke();
+
+ if (_draw_shadow) {
+ const auto a = (exp(-3 * SP_RGBA32_A_F(_border_color)) - 1) / (exp(-3) - 1);
+ ink_cairo_draw_drop_shadow(ctx, rect, 12, _border_color, a);
+ }
+ }
+
+ return true;
+}
+
+void PageSizePreview::draw_border(bool border) {
+ _draw_border = border;
+ queue_draw();
+}
+
+void PageSizePreview::set_desk_color(unsigned int rgba) {
+ _desk_color = rgba | 0xff; // desk always opaque
+ queue_draw();
+}
+void PageSizePreview::set_page_color(unsigned int rgba) {
+ _page_color = rgba;
+ queue_draw();
+}
+void PageSizePreview::set_border_color(unsigned int rgba) {
+ _border_color = rgba;
+ queue_draw();
+}
+
+void PageSizePreview::enable_drop_shadow(bool shadow) {
+ _draw_shadow = shadow;
+ queue_draw();
+}
+
+void PageSizePreview::enable_checkerboard(bool checkerboard) {
+ _draw_checkerboard = checkerboard;
+ queue_draw();
+}
+
+void PageSizePreview::set_page_size(double width, double height) {
+ _width = width;
+ _height = height;
+ queue_draw();
+}
+
+} } } // namespace Inkscape/Widget/UI
diff --git a/src/ui/widget/page-size-preview.h b/src/ui/widget/page-size-preview.h
new file mode 100644
index 0000000..093e79b
--- /dev/null
+++ b/src/ui/widget/page-size-preview.h
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Michael Kowalski
+ *
+ * Copyright (C) 2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H
+#define INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H
+
+#include <gtkmm/drawingarea.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class PageSizePreview : public Gtk::DrawingArea {
+public:
+ PageSizePreview();
+ // static PageSizePreview* create();
+
+ void set_desk_color(unsigned int rgba);
+ void set_page_color(unsigned int rgba);
+ void set_border_color(unsigned int rgba);
+ void draw_border(bool border);
+ void enable_drop_shadow(bool shadow);
+ void set_page_size(double width, double height);
+ void enable_checkerboard(bool checkerboard);
+
+ ~PageSizePreview() override = default;
+
+private:
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& ctx) override;
+ unsigned int _border_color = 0x0000001f;
+ unsigned int _page_color = 0xffffff00;
+ unsigned int _desk_color = 0xc8c8c8ff;
+ bool _draw_border = true;
+ bool _draw_shadow = true;
+ bool _draw_checkerboard = false;
+ double _width = 10;
+ double _height = 7;
+};
+
+} } } // namespace Inkscape/Widget/UI
+
+#endif // INKSCAPE_UI_WIDGET_PAGE_SIZE_PREVIEW_H
+
diff --git a/src/ui/widget/paint-selector.cpp b/src/ui/widget/paint-selector.cpp
new file mode 100644
index 0000000..ed67d46
--- /dev/null
+++ b/src/ui/widget/paint-selector.cpp
@@ -0,0 +1,1476 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * PaintSelector: 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 <glibmm/fileutils.h>
+
+#include "desktop-style.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 "io/resource.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 "ui/widget/gradient-selector.h"
+#include "ui/widget/gradient-editor.h"
+#include "ui/widget/swatch-selector.h"
+#include "ui/widget/scrollprotected.h"
+
+#include "widgets/widget-sizes.h"
+
+#include "xml/repr.h"
+
+#ifdef SP_PS_VERBOSE
+#include "svg/svg-icc-color.h"
+#endif // SP_PS_VERBOSE
+
+#include <gtkmm/label.h>
+#include <gtkmm/combobox.h>
+
+using Inkscape::UI::SelectedColor;
+
+#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
+
+namespace {
+GtkWidget *_scrollprotected_combo_box_new_with_model(GtkTreeModel *model)
+{
+ auto combobox = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBox>());
+ gtk_combo_box_set_model(combobox->gobj(), model);
+ return GTK_WIDGET(combobox->gobj());
+}
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class FillRuleRadioButton : public Gtk::RadioButton {
+ private:
+ PaintSelector::FillRule _fillrule;
+
+ public:
+ FillRuleRadioButton()
+ : Gtk::RadioButton()
+ {}
+
+ FillRuleRadioButton(Gtk::RadioButton::Group &group)
+ : Gtk::RadioButton(group)
+ {}
+
+ inline void set_fillrule(PaintSelector::FillRule fillrule) { _fillrule = fillrule; }
+ inline PaintSelector::FillRule get_fillrule() const { return _fillrule; }
+};
+
+class StyleToggleButton : public Gtk::ToggleButton {
+ private:
+ PaintSelector::Mode _style;
+
+ public:
+ inline void set_style(PaintSelector::Mode style) { _style = style; }
+ inline PaintSelector::Mode get_style() const { return _style; }
+};
+
+static bool isPaintModeGradient(PaintSelector::Mode mode)
+{
+ bool isGrad = (mode == PaintSelector::MODE_GRADIENT_LINEAR) || (mode == PaintSelector::MODE_GRADIENT_RADIAL) ||
+ (mode == PaintSelector::MODE_SWATCH);
+
+ return isGrad;
+}
+
+GradientSelectorInterface *PaintSelector::getGradientFromData() const
+{
+ if (_mode == PaintSelector::MODE_SWATCH && _selector_swatch) {
+ return _selector_swatch->getGradientSelector();
+ }
+ return _selector_gradient;
+}
+
+#define XPAD 4
+#define YPAD 1
+
+PaintSelector::PaintSelector(FillOrStroke kind)
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ _mode = static_cast<PaintSelector::Mode>(-1); // huh? do you mean 0xff? -- I think this means "not in the enum"
+
+ /* Paint style button box */
+ _style = Gtk::manage(new Gtk::Box());
+ _style->set_homogeneous(false);
+ _style->set_name("PaintSelector");
+ _style->show();
+ _style->set_border_width(0);
+ pack_start(*_style, false, false);
+
+ /* Buttons */
+ _none = style_button_add(INKSCAPE_ICON("paint-none"), PaintSelector::MODE_NONE, _("No paint"));
+ _solid = style_button_add(INKSCAPE_ICON("paint-solid"), PaintSelector::MODE_SOLID_COLOR, _("Flat color"));
+ _gradient = style_button_add(INKSCAPE_ICON("paint-gradient-linear"), PaintSelector::MODE_GRADIENT_LINEAR,
+ _("Linear gradient"));
+ _radial = style_button_add(INKSCAPE_ICON("paint-gradient-radial"), PaintSelector::MODE_GRADIENT_RADIAL,
+ _("Radial gradient"));
+#ifdef WITH_MESH
+ _mesh =
+ style_button_add(INKSCAPE_ICON("paint-gradient-mesh"), PaintSelector::MODE_GRADIENT_MESH, _("Mesh gradient"));
+#endif
+ _pattern = style_button_add(INKSCAPE_ICON("paint-pattern"), PaintSelector::MODE_PATTERN, _("Pattern"));
+ _swatch = style_button_add(INKSCAPE_ICON("paint-swatch"), PaintSelector::MODE_SWATCH, _("Swatch"));
+ _unset = style_button_add(INKSCAPE_ICON("paint-unknown"), PaintSelector::MODE_UNSET,
+ _("Unset paint (make it undefined so it can be inherited)"));
+
+ /* Fillrule */
+ {
+ _fillrulebox = Gtk::manage(new Gtk::Box());
+ _fillrulebox->set_homogeneous(false);
+ _style->pack_end(*_fillrulebox, false, false, 0);
+
+ _evenodd = Gtk::manage(new FillRuleRadioButton());
+ _evenodd->set_relief(Gtk::RELIEF_NONE);
+ _evenodd->set_mode(false);
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty
+ _evenodd->set_tooltip_text(
+ _("Any path self-intersections or subpaths create holes in the fill (fill-rule: evenodd)"));
+ _evenodd->set_fillrule(PaintSelector::FILLRULE_EVENODD);
+ auto w = sp_get_icon_image("fill-rule-even-odd", GTK_ICON_SIZE_MENU);
+ gtk_container_add(GTK_CONTAINER(_evenodd->gobj()), w);
+ _fillrulebox->pack_start(*_evenodd, false, false, 0);
+ _evenodd->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(*this, &PaintSelector::fillrule_toggled), _evenodd));
+
+ auto grp = _evenodd->get_group();
+ _nonzero = Gtk::manage(new FillRuleRadioButton(grp));
+ _nonzero->set_relief(Gtk::RELIEF_NONE);
+ _nonzero->set_mode(false);
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty
+ _nonzero->set_tooltip_text(_("Fill is solid unless a subpath is counterdirectional (fill-rule: nonzero)"));
+ _nonzero->set_fillrule(PaintSelector::FILLRULE_NONZERO);
+ w = sp_get_icon_image("fill-rule-nonzero", GTK_ICON_SIZE_MENU);
+ gtk_container_add(GTK_CONTAINER(_nonzero->gobj()), w);
+ _fillrulebox->pack_start(*_nonzero, false, false, 0);
+ _nonzero->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(*this, &PaintSelector::fillrule_toggled), _nonzero));
+ }
+
+ /* Frame */
+ _label = Gtk::manage(new Gtk::Label(""));
+ auto lbbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ lbbox->set_homogeneous(false);
+ _label->show();
+ lbbox->pack_start(*_label, false, false, 4);
+ pack_start(*lbbox, false, false, 4);
+
+ _frame = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ _frame->set_homogeneous(false);
+ _frame->show();
+ // gtk_container_set_border_width(GTK_CONTAINER(psel->frame), 0);
+ pack_start(*_frame, true, true, 0);
+
+
+ /* Last used color */
+ _selected_color = new SelectedColor;
+ _updating_color = false;
+
+ _selected_color->signal_grabbed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorGrabbed));
+ _selected_color->signal_dragged.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorDragged));
+ _selected_color->signal_released.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorReleased));
+ _selected_color->signal_changed.connect(sigc::mem_fun(*this, &PaintSelector::onSelectedColorChanged));
+
+ // from _new function
+ setMode(PaintSelector::MODE_MULTIPLE);
+
+ if (kind == FILL)
+ _fillrulebox->show_all();
+ else
+ _fillrulebox->hide();
+
+ show_all();
+
+ // don't let docking manager uncover hidden widgets
+ set_no_show_all();
+}
+
+PaintSelector::~PaintSelector()
+{
+ if (_selected_color) {
+ delete _selected_color;
+ _selected_color = nullptr;
+ }
+}
+
+StyleToggleButton *PaintSelector::style_button_add(gchar const *pixmap, PaintSelector::Mode mode, gchar const *tip)
+{
+ GtkWidget *w;
+
+ auto b = Gtk::manage(new StyleToggleButton());
+ b->set_tooltip_text(tip);
+ b->show();
+ b->set_border_width(0);
+ b->set_relief(Gtk::RELIEF_NONE);
+ b->set_mode(false);
+ b->set_style(mode);
+
+ w = sp_get_icon_image(pixmap, GTK_ICON_SIZE_BUTTON);
+ gtk_container_add(GTK_CONTAINER(b->gobj()), w);
+
+ _style->pack_start(*b, false, false);
+ b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &PaintSelector::style_button_toggled), b));
+
+ return b;
+}
+
+void PaintSelector::style_button_toggled(StyleToggleButton *tb)
+{
+ if (!_update && tb->get_active()) {
+ // button toggled: explicit user action where fill/stroke style change is initiated/requested
+ set_mode_ex(tb->get_style(), true);
+ }
+}
+
+void PaintSelector::fillrule_toggled(FillRuleRadioButton *tb)
+{
+ if (!_update && tb->get_active()) {
+ auto fr = tb->get_fillrule();
+ _signal_fillrule_changed.emit(fr);
+ }
+}
+
+void PaintSelector::setMode(Mode mode) {
+ set_mode_ex(mode, false);
+}
+
+void PaintSelector::set_mode_ex(Mode mode, bool switch_style) {
+ if (_mode != mode) {
+ _update = true;
+ _label->show();
+#ifdef SP_PS_VERBOSE
+ g_print("Mode change %d -> %d %s -> %s\n", _mode, mode, modeStrings[_mode], modeStrings[mode]);
+#endif
+ switch (mode) {
+ case MODE_EMPTY:
+ set_mode_empty();
+ break;
+ case MODE_MULTIPLE:
+ set_mode_multiple();
+ break;
+ case MODE_NONE:
+ set_mode_none();
+ break;
+ case MODE_SOLID_COLOR:
+ set_mode_color(mode);
+ break;
+ case MODE_GRADIENT_LINEAR:
+ case MODE_GRADIENT_RADIAL:
+ set_mode_gradient(mode);
+ break;
+#ifdef WITH_MESH
+ case MODE_GRADIENT_MESH:
+ set_mode_mesh(mode);
+ break;
+#endif
+ case MODE_PATTERN:
+ set_mode_pattern(mode);
+ break;
+ case MODE_HATCH:
+ set_mode_hatch(mode);
+ break;
+ case MODE_SWATCH:
+ set_mode_swatch(mode);
+ break;
+ case MODE_UNSET:
+ set_mode_unset();
+ break;
+ default:
+ g_warning("file %s: line %d: Unknown paint mode %d", __FILE__, __LINE__, mode);
+ break;
+ }
+ _mode = mode;
+ _signal_mode_changed.emit(_mode, switch_style);
+ _update = false;
+ }
+}
+
+void PaintSelector::setFillrule(FillRule fillrule)
+{
+ if (_fillrulebox) {
+ // TODO this flips widgets but does not use a member to store state. Revisit
+ _evenodd->set_active(fillrule == FILLRULE_EVENODD);
+ _nonzero->set_active(fillrule == FILLRULE_NONZERO);
+ }
+}
+
+void PaintSelector::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 PaintSelector::setSwatch(SPGradient *vector)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set SWATCH\n");
+#endif
+ setMode(MODE_SWATCH);
+
+ if (_selector_swatch) {
+ _selector_swatch->setVector((vector) ? vector->document : nullptr, vector);
+ }
+}
+
+void PaintSelector::setGradientLinear(SPGradient *vector, SPLinearGradient* gradient, SPStop* selected)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT LINEAR\n");
+#endif
+ setMode(MODE_GRADIENT_LINEAR);
+
+ auto gsel = getGradientFromData();
+
+ gsel->setMode(GradientSelector::MODE_LINEAR);
+ gsel->setGradient(gradient);
+ gsel->setVector((vector) ? vector->document : nullptr, vector);
+ gsel->selectStop(selected);
+}
+
+void PaintSelector::setGradientRadial(SPGradient *vector, SPRadialGradient* gradient, SPStop* selected)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT RADIAL\n");
+#endif
+ setMode(MODE_GRADIENT_RADIAL);
+
+ auto gsel = getGradientFromData();
+
+ gsel->setMode(GradientSelector::MODE_RADIAL);
+ gsel->setGradient(gradient);
+ gsel->setVector((vector) ? vector->document : nullptr, vector);
+ gsel->selectStop(selected);
+}
+
+#ifdef WITH_MESH
+void PaintSelector::setGradientMesh(SPMeshGradient *array)
+{
+#ifdef SP_PS_VERBOSE
+ g_print("PaintSelector set GRADIENT MESH\n");
+#endif
+ setMode(MODE_GRADIENT_MESH);
+
+ // GradientSelector *gsel = getGradientFromData(this);
+
+ // gsel->setMode(GradientSelector::MODE_GRADIENT_MESH);
+ // gsel->setVector((mesh) ? mesh->document : 0, mesh);
+}
+#endif
+
+void PaintSelector::setGradientProperties(SPGradientUnits units, SPGradientSpread spread)
+{
+ g_return_if_fail(isPaintModeGradient(_mode));
+
+ auto gsel = getGradientFromData();
+ gsel->setUnits(units);
+ gsel->setSpread(spread);
+}
+
+void PaintSelector::getGradientProperties(SPGradientUnits &units, SPGradientSpread &spread) const
+{
+ g_return_if_fail(isPaintModeGradient(_mode));
+
+ auto gsel = getGradientFromData();
+ units = gsel->getUnits();
+ spread = gsel->getSpread();
+}
+
+
+/**
+ * \post (alpha == NULL) || (*alpha in [0.0, 1.0]).
+ */
+void PaintSelector::getColorAlpha(SPColor &color, gfloat &alpha) const
+{
+ _selected_color->colorAlpha(color, alpha);
+
+ g_assert((0.0 <= alpha) && (alpha <= 1.0));
+}
+
+SPGradient *PaintSelector::getGradientVector()
+{
+ SPGradient *vect = nullptr;
+
+ if (isPaintModeGradient(_mode)) {
+ auto gsel = getGradientFromData();
+ vect = gsel->getVector();
+ }
+
+ return vect;
+}
+
+
+void PaintSelector::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();
+}
+
+void PaintSelector::clear_frame()
+{
+ if (_selector_solid_color) {
+ _selector_solid_color->hide();
+ }
+ if (_selector_gradient) {
+ _selector_gradient->hide();
+ }
+ if (_selector_mesh) {
+ _selector_mesh->hide();
+ }
+ if (_selector_pattern) {
+ _selector_pattern->hide();
+ }
+ if (_selector_swatch) {
+ _selector_swatch->hide();
+ }
+}
+
+void PaintSelector::set_mode_empty()
+{
+ set_style_buttons(nullptr);
+ _style->set_sensitive(false);
+ clear_frame();
+ _label->set_markup(_("<b>No objects</b>"));
+}
+
+void PaintSelector::set_mode_multiple()
+{
+ set_style_buttons(nullptr);
+ _style->set_sensitive(true);
+ clear_frame();
+ _label->set_markup(_("<b>Multiple styles</b>"));
+}
+
+void PaintSelector::set_mode_unset()
+{
+ set_style_buttons(_unset);
+ _style->set_sensitive(true);
+ clear_frame();
+ _label->set_markup(_("<b>Paint is undefined</b>"));
+}
+
+void PaintSelector::set_mode_none()
+{
+ set_style_buttons(_none);
+ _style->set_sensitive(true);
+ clear_frame();
+ _label->set_markup(_("<b>No paint</b>"));
+}
+
+/* Color paint */
+
+void PaintSelector::onSelectedColorGrabbed() { _signal_grabbed.emit(); }
+
+void PaintSelector::onSelectedColorDragged()
+{
+ if (_updating_color) {
+ return;
+ }
+
+ _signal_dragged.emit();
+}
+
+void PaintSelector::onSelectedColorReleased() { _signal_released.emit(); }
+
+void PaintSelector::onSelectedColorChanged()
+{
+ if (_updating_color) {
+ return;
+ }
+
+ if (_mode == MODE_SOLID_COLOR) {
+ _signal_changed.emit();
+ } else {
+ g_warning("PaintSelector::onSelectedColorChanged(): selected color changed while not in color selection mode");
+ }
+}
+
+void PaintSelector::set_mode_color(PaintSelector::Mode /*mode*/)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ if (_mode == PaintSelector::MODE_SWATCH) {
+ auto gsel = getGradientFromData();
+ 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();
+ _selected_color->setColorAlpha(color, alpha, false);
+ }
+ }
+ }
+
+ set_style_buttons(_solid);
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_SOLID_COLOR) {
+ /* Already have color selector */
+ // Do nothing
+ } else {
+ clear_frame();
+
+ /* Create new color selector */
+ /* Create vbox */
+ if (!_selector_solid_color) {
+ _selector_solid_color = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ _selector_solid_color->set_homogeneous(false);
+
+ /* Color selector */
+ auto color_selector = Gtk::manage(new ColorNotebook(*(_selected_color)));
+ color_selector->show();
+ _selector_solid_color->pack_start(*color_selector, true, true, 0);
+ /* Pack everything to frame */
+ _frame->add(*_selector_solid_color);
+ color_selector->set_label(_("<b>Flat color</b>"));
+ }
+
+ _selector_solid_color->show();
+ }
+
+ _label->set_markup(""); //_("<b>Flat color</b>"));
+ _label->hide();
+
+#ifdef SP_PS_VERBOSE
+ g_print("Color req\n");
+#endif
+}
+
+/* Gradient */
+
+void PaintSelector::gradient_grabbed() { _signal_grabbed.emit(); }
+
+void PaintSelector::gradient_dragged() { _signal_dragged.emit(); }
+
+void PaintSelector::gradient_released() { _signal_released.emit(); }
+
+void PaintSelector::gradient_changed(SPGradient * /* gr */) { _signal_changed.emit(); }
+
+void PaintSelector::set_mode_gradient(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_GRADIENT_LINEAR) {
+ set_style_buttons(_gradient);
+ } else if (mode == PaintSelector::MODE_GRADIENT_RADIAL) {
+ set_style_buttons(_radial);
+ }
+ _style->set_sensitive(true);
+
+ if ((_mode == PaintSelector::MODE_GRADIENT_LINEAR) || (_mode == PaintSelector::MODE_GRADIENT_RADIAL)) {
+ // do nothing - the selector should already be a GradientSelector
+ } else {
+ clear_frame();
+ if (!_selector_gradient) {
+ /* Create new gradient selector */
+ try {
+ _selector_gradient = Gtk::manage(new GradientEditor("/gradient-edit"));
+ _selector_gradient->show();
+ _selector_gradient->signal_grabbed().connect(sigc::mem_fun(this, &PaintSelector::gradient_grabbed));
+ _selector_gradient->signal_dragged().connect(sigc::mem_fun(this, &PaintSelector::gradient_dragged));
+ _selector_gradient->signal_released().connect(sigc::mem_fun(this, &PaintSelector::gradient_released));
+ _selector_gradient->signal_changed().connect(sigc::mem_fun(this, &PaintSelector::gradient_changed));
+ _selector_gradient->signal_stop_selected().connect([=](SPStop* stop) { _signal_stop_selected.emit(stop); });
+ /* Pack everything to frame */
+ _frame->add(*_selector_gradient);
+ }
+ catch (std::exception& ex) {
+ g_error("Creation of GradientEditor widget failed: %s.", ex.what());
+ throw;
+ }
+ } else {
+ // Necessary when creating new gradients via the Fill and Stroke dialog
+ _selector_gradient->setVector(nullptr, nullptr);
+ }
+ _selector_gradient->show();
+ }
+
+ /* Actually we have to set option menu history here */
+ if (mode == PaintSelector::MODE_GRADIENT_LINEAR) {
+ _selector_gradient->setMode(GradientSelector::MODE_LINEAR);
+ // sp_gradient_selector_set_mode(SP_GRADIENT_SELECTOR(gsel), SP_GRADIENT_SELECTOR_MODE_LINEAR);
+ // _label->set_markup(_("<b>Linear gradient</b>"));
+ _label->hide();
+ } else if (mode == PaintSelector::MODE_GRADIENT_RADIAL) {
+ _selector_gradient->setMode(GradientSelector::MODE_RADIAL);
+ // _label->set_markup(_("<b>Radial gradient</b>"));
+ _label->hide();
+ }
+
+#ifdef SP_PS_VERBOSE
+ g_print("Gradient req\n");
+#endif
+}
+
+// ************************* MESH ************************
+#ifdef WITH_MESH
+void PaintSelector::mesh_destroy(GtkWidget *widget, PaintSelector * /*psel*/)
+{
+ // drop our reference to the mesh menu widget
+ g_object_unref(G_OBJECT(widget));
+}
+
+void PaintSelector::mesh_change(GtkWidget * /*widget*/, PaintSelector *psel) { psel->_signal_changed.emit(); }
+
+
+/**
+ * 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 PaintSelector::updateMeshList(SPMeshGradient *mesh)
+{
+ if (_update) {
+ return;
+ }
+
+ g_assert(_meshmenu != nullptr);
+
+ /* Clear existing menu if any */
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(_meshmenu));
+ gtk_list_store_clear(GTK_LIST_STORE(store));
+
+ ink_mesh_menu(_meshmenu);
+
+ /* Set history */
+
+ if (mesh && !_meshmenu_update) {
+ _meshmenu_update = 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(_meshmenu), &iter);
+ }
+
+ _meshmenu_update = false;
+ g_free(meshid);
+ }
+}
+
+#ifdef WITH_MESH
+void PaintSelector::set_mode_mesh(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_GRADIENT_MESH) {
+ set_style_buttons(_mesh);
+ }
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_GRADIENT_MESH) {
+ /* Already have mesh menu */
+ // Do nothing - the Selector is already a Gtk::Box with the required contents
+ } else {
+ clear_frame();
+
+ if (!_selector_mesh) {
+ /* Create vbox */
+ _selector_mesh = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ _selector_mesh->set_homogeneous(false);
+
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 1));
+ hb->set_homogeneous(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 = _scrollprotected_combo_box_new_with_model(GTK_TREE_MODEL(store));
+ gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), PaintSelector::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, nullptr);
+
+ ink_mesh_menu(combo);
+ g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(PaintSelector::mesh_change), this);
+ g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(PaintSelector::mesh_destroy), this);
+ _meshmenu = combo;
+ g_object_ref(G_OBJECT(combo));
+
+ gtk_container_add(GTK_CONTAINER(hb->gobj()), combo);
+ _selector_mesh->pack_start(*hb, false, false, AUX_BETWEEN_BUTTON_GROUPS);
+
+ g_object_unref(G_OBJECT(store));
+
+ auto hb2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb2->set_homogeneous(false);
+
+ auto l = Gtk::manage(new Gtk::Label());
+ l->set_markup(_("Use the <b>Mesh tool</b> to modify the mesh."));
+ l->set_line_wrap(true);
+ l->set_size_request(180, -1);
+ hb2->pack_start(*l, true, true, AUX_BETWEEN_BUTTON_GROUPS);
+ _selector_mesh->pack_start(*hb2, false, false, AUX_BETWEEN_BUTTON_GROUPS);
+ _selector_mesh->show_all();
+
+ _frame->add(*_selector_mesh);
+ }
+
+ _selector_mesh->show();
+ _label->set_markup(_("<b>Mesh fill</b>"));
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Mesh req\n");
+#endif
+}
+#endif // WITH_MESH
+
+SPMeshGradient *PaintSelector::getMeshGradient()
+{
+ g_return_val_if_fail((_mode == MODE_GRADIENT_MESH), NULL);
+
+ /* no mesh menu if we were just selected */
+ if (_meshmenu == nullptr) {
+ return nullptr;
+ }
+ GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(_meshmenu));
+
+ /* Get the selected mesh */
+ GtkTreeIter iter;
+ if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(_meshmenu), &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, nullptr);
+ } 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 << "PaintSelector::getMeshGradient: Unexpected meshid value." << std::endl;
+ }
+
+ g_free(meshid);
+
+ return mesh;
+}
+
+#endif
+// ************************ End Mesh ************************
+
+void PaintSelector::set_style_buttons(Gtk::ToggleButton *active)
+{
+ _none->set_active(active == _none);
+ _solid->set_active(active == _solid);
+ _gradient->set_active(active == _gradient);
+ _radial->set_active(active == _radial);
+#ifdef WITH_MESH
+ _mesh->set_active(active == _mesh);
+#endif
+ _pattern->set_active(active == _pattern);
+ _swatch->set_active(active == _swatch);
+ _unset->set_active(active == _unset);
+}
+
+void PaintSelector::pattern_destroy(GtkWidget *widget, PaintSelector * /*psel*/)
+{
+ // drop our reference to the pattern menu widget
+ g_object_unref(G_OBJECT(widget));
+}
+
+void PaintSelector::pattern_change(GtkWidget * /*widget*/, PaintSelector *psel) { psel->_signal_changed.emit(); }
+
+
+/**
+ * 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) {
+ using namespace Inkscape::IO::Resource;
+ auto patterns_source = get_path_string(SYSTEM, PAINT, "patterns.svg");
+ if (Glib::file_test(patterns_source, Glib::FILE_TEST_IS_REGULAR)) {
+ patterns_doc = SPDocument::createNewDoc(patterns_source.c_str(), false);
+ }
+ }
+
+ // 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 PaintSelector::updatePatternList(SPPattern *pattern)
+{
+ if (_update) {
+ return;
+ }
+ g_assert(_patternmenu != nullptr);
+
+ /* Clear existing menu if any */
+ auto store = gtk_combo_box_get_model(GTK_COMBO_BOX(_patternmenu));
+ gtk_list_store_clear(GTK_LIST_STORE(store));
+
+ ink_pattern_menu(_patternmenu);
+
+ /* Set history */
+
+ if (pattern && !_patternmenu_update) {
+ _patternmenu_update = 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(_patternmenu), &iter);
+ }
+
+ _patternmenu_update = false;
+ }
+}
+
+void PaintSelector::set_mode_pattern(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_PATTERN) {
+ set_style_buttons(_pattern);
+ }
+
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_PATTERN) {
+ /* Already have pattern menu */
+ } else {
+ clear_frame();
+
+ if (!_selector_pattern) {
+ /* Create vbox */
+ _selector_pattern = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ _selector_pattern->set_homogeneous(false);
+
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 1));
+ hb->set_homogeneous(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);
+ _patternmenu = _scrollprotected_combo_box_new_with_model(GTK_TREE_MODEL(store));
+ gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(_patternmenu), PaintSelector::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(_patternmenu), renderer, TRUE);
+ gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(_patternmenu), renderer, "text", COMBO_COL_LABEL, nullptr);
+
+ ink_pattern_menu(_patternmenu);
+ g_signal_connect(G_OBJECT(_patternmenu), "changed", G_CALLBACK(PaintSelector::pattern_change), this);
+ g_signal_connect(G_OBJECT(_patternmenu), "destroy", G_CALLBACK(PaintSelector::pattern_destroy), this);
+ g_object_ref(G_OBJECT(_patternmenu));
+
+ gtk_container_add(GTK_CONTAINER(hb->gobj()), _patternmenu);
+ _selector_pattern->pack_start(*hb, false, false, AUX_BETWEEN_BUTTON_GROUPS);
+
+ g_object_unref(G_OBJECT(store));
+
+ auto hb2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb2->set_homogeneous(false);
+ auto l = Gtk::manage(new Gtk::Label());
+ l->set_markup(
+ _("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."));
+ l->set_line_wrap(true);
+ l->set_size_request(180, -1);
+ hb2->pack_start(*l, true, true, AUX_BETWEEN_BUTTON_GROUPS);
+ _selector_pattern->pack_start(*hb2, false, false, AUX_BETWEEN_BUTTON_GROUPS);
+ _selector_pattern->show_all();
+ _frame->add(*_selector_pattern);
+ }
+
+ _selector_pattern->show();
+ _label->set_markup(_("<b>Pattern fill</b>"));
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Pattern req\n");
+#endif
+}
+
+void PaintSelector::set_mode_hatch(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_HATCH) {
+ set_style_buttons(_unset);
+ }
+
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_HATCH) {
+ /* Already have hatch menu, for the moment unset */
+ } else {
+ clear_frame();
+
+ _label->set_markup(_("<b>Hatch fill</b>"));
+ }
+#ifdef SP_PS_VERBOSE
+ g_print("Hatch req\n");
+#endif
+}
+
+gboolean PaintSelector::isSeparator(GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/)
+{
+
+ gboolean sep = FALSE;
+ gtk_tree_model_get(model, iter, COMBO_COL_SEP, &sep, -1);
+ return sep;
+}
+
+SPPattern *PaintSelector::getPattern()
+{
+ SPPattern *pat = nullptr;
+ g_return_val_if_fail(_mode == MODE_PATTERN, nullptr);
+
+ /* no pattern menu if we were just selected */
+ if (!_patternmenu) {
+ return nullptr;
+ }
+
+ auto store = gtk_combo_box_get_model(GTK_COMBO_BOX(_patternmenu));
+
+ /* Get the selected pattern */
+ GtkTreeIter iter;
+ if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(_patternmenu), &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, nullptr);
+ } 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;
+}
+
+void PaintSelector::set_mode_swatch(PaintSelector::Mode mode)
+{
+ if (mode == PaintSelector::MODE_SWATCH) {
+ set_style_buttons(_swatch);
+ }
+
+ _style->set_sensitive(true);
+
+ if (_mode == PaintSelector::MODE_SWATCH) {
+ // Do nothing. The selector is already a SwatchSelector
+ } else {
+ clear_frame();
+
+ if (!_selector_swatch) {
+ // Create new gradient selector
+ _selector_swatch = Gtk::manage(new SwatchSelector());
+
+ auto gsel = _selector_swatch->getGradientSelector();
+ gsel->signal_grabbed().connect(sigc::mem_fun(this, &PaintSelector::gradient_grabbed));
+ gsel->signal_dragged().connect(sigc::mem_fun(this, &PaintSelector::gradient_dragged));
+ gsel->signal_released().connect(sigc::mem_fun(this, &PaintSelector::gradient_released));
+ gsel->signal_changed().connect(sigc::mem_fun(this, &PaintSelector::gradient_changed));
+
+ // Pack everything to frame
+ _frame->add(*_selector_swatch);
+ } else {
+ // Necessary when creating new swatches via the Fill and Stroke dialog
+ _selector_swatch->setVector(nullptr, nullptr);
+ }
+ _selector_swatch->show();
+ _label->set_markup(_("<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 PaintSelector::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);
+}
+
+PaintSelector::Mode PaintSelector::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("PaintSelector::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;
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/paint-selector.h b/src/ui/widget/paint-selector.h
new file mode 100644
index 0000000..ad069d8
--- /dev/null
+++ b/src/ui/widget/paint-selector.h
@@ -0,0 +1,226 @@
+// 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 "color.h"
+#include "fill-or-stroke.h"
+#include <glib.h>
+#include <gtkmm/box.h>
+
+#include "object/sp-gradient-spread.h"
+#include "object/sp-gradient-units.h"
+#include "gradient-selector-interface.h"
+#include "ui/selected-color.h"
+#include "ui/widget/gradient-selector.h"
+#include "ui/widget/swatch-selector.h"
+
+class SPGradient;
+class SPLinearGradient;
+class SPRadialGradient;
+#ifdef WITH_MESH
+class SPMeshGradient;
+#endif
+class SPDesktop;
+class SPPattern;
+class SPStyle;
+
+namespace Gtk {
+class Label;
+class RadioButton;
+class ToggleButton;
+} // namespace Gtk
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class FillRuleRadioButton;
+class StyleToggleButton;
+class GradientEditor;
+
+/**
+ * Generic paint selector widget.
+ */
+class PaintSelector : public Gtk::Box {
+ public:
+ 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 };
+
+ private:
+ bool _update = false;
+
+ Mode _mode;
+
+ Gtk::Box *_style;
+ StyleToggleButton *_none;
+ StyleToggleButton *_solid;
+ StyleToggleButton *_gradient;
+ StyleToggleButton *_radial;
+#ifdef WITH_MESH
+ StyleToggleButton *_mesh;
+#endif
+ StyleToggleButton *_pattern;
+ StyleToggleButton *_swatch;
+ StyleToggleButton *_unset;
+
+ Gtk::Box *_fillrulebox;
+ FillRuleRadioButton *_evenodd;
+ FillRuleRadioButton *_nonzero;
+
+ Gtk::Box *_frame;
+
+ Gtk::Box *_selector_solid_color = nullptr;
+ GradientEditor *_selector_gradient = nullptr;
+ Gtk::Box *_selector_mesh = nullptr;
+ Gtk::Box *_selector_pattern = nullptr;
+ SwatchSelector *_selector_swatch = nullptr;
+
+ Gtk::Label *_label;
+ GtkWidget *_patternmenu = nullptr;
+ bool _patternmenu_update = false;
+#ifdef WITH_MESH
+ GtkWidget *_meshmenu = nullptr;
+ bool _meshmenu_update = false;
+#endif
+
+ Inkscape::UI::SelectedColor *_selected_color;
+ bool _updating_color;
+
+ void getColorAlpha(SPColor &color, gfloat &alpha) const;
+
+ static gboolean isSeparator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data);
+
+ private:
+ sigc::signal<void, FillRule> _signal_fillrule_changed;
+ sigc::signal<void> _signal_dragged;
+ sigc::signal<void, Mode, bool> _signal_mode_changed;
+ sigc::signal<void> _signal_grabbed;
+ sigc::signal<void> _signal_released;
+ sigc::signal<void> _signal_changed;
+ sigc::signal<void (SPStop*)> _signal_stop_selected;
+
+ StyleToggleButton *style_button_add(gchar const *px, PaintSelector::Mode mode, gchar const *tip);
+ void style_button_toggled(StyleToggleButton *tb);
+ void fillrule_toggled(FillRuleRadioButton *tb);
+ void onSelectedColorGrabbed();
+ void onSelectedColorDragged();
+ void onSelectedColorReleased();
+ void onSelectedColorChanged();
+ void set_mode_empty();
+ void set_style_buttons(Gtk::ToggleButton *active);
+ void set_mode_multiple();
+ void set_mode_none();
+ GradientSelectorInterface *getGradientFromData() const;
+ void clear_frame();
+ void set_mode_unset();
+ void set_mode_color(PaintSelector::Mode mode);
+ void set_mode_gradient(PaintSelector::Mode mode);
+#ifdef WITH_MESH
+ void set_mode_mesh(PaintSelector::Mode mode);
+#endif
+ void set_mode_pattern(PaintSelector::Mode mode);
+ void set_mode_hatch(PaintSelector::Mode mode);
+ void set_mode_swatch(PaintSelector::Mode mode);
+ void set_mode_ex(Mode mode, bool switch_style);
+
+ void gradient_grabbed();
+ void gradient_dragged();
+ void gradient_released();
+ void gradient_changed(SPGradient *gr);
+
+ static void mesh_change(GtkWidget *widget, PaintSelector *psel);
+ static void mesh_destroy(GtkWidget *widget, PaintSelector *psel);
+
+ static void pattern_change(GtkWidget *widget, PaintSelector *psel);
+ static void pattern_destroy(GtkWidget *widget, PaintSelector *psel);
+
+ public:
+ PaintSelector(FillOrStroke kind);
+ ~PaintSelector() override;
+
+ inline decltype(_signal_fillrule_changed) signal_fillrule_changed() const { return _signal_fillrule_changed; }
+ inline decltype(_signal_dragged) signal_dragged() const { return _signal_dragged; }
+ inline decltype(_signal_mode_changed) signal_mode_changed() const { return _signal_mode_changed; }
+ inline decltype(_signal_grabbed) signal_grabbed() const { return _signal_grabbed; }
+ inline decltype(_signal_released) signal_released() const { return _signal_released; }
+ inline decltype(_signal_changed) signal_changed() const { return _signal_changed; }
+ inline decltype(_signal_stop_selected) signal_stop_selected() const { return _signal_stop_selected; }
+
+ void setMode(Mode mode);
+ static Mode getModeForStyle(SPStyle const &style, FillOrStroke kind);
+ void setFillrule(FillRule fillrule);
+ void setColorAlpha(SPColor const &color, float alpha);
+ void setSwatch(SPGradient *vector);
+ void setGradientLinear(SPGradient *vector, SPLinearGradient* gradient, SPStop* selected);
+ void setGradientRadial(SPGradient *vector, SPRadialGradient* gradient, SPStop* selected);
+#ifdef WITH_MESH
+ void setGradientMesh(SPMeshGradient *array);
+#endif
+ void setGradientProperties(SPGradientUnits units, SPGradientSpread spread);
+ void getGradientProperties(SPGradientUnits &units, SPGradientSpread &spread) const;
+
+#ifdef WITH_MESH
+ SPMeshGradient *getMeshGradient();
+ void updateMeshList(SPMeshGradient *pat);
+#endif
+
+ void updatePatternList(SPPattern *pat);
+ inline decltype(_mode) get_mode() const { return _mode; }
+
+ // TODO move this elsewhere:
+ void setFlatColor(SPDesktop *desktop, const gchar *color_property, const gchar *opacity_property);
+
+ SPGradient *getGradientVector();
+ void pushAttrsToGradient(SPGradient *gr) const;
+ SPPattern *getPattern();
+};
+
+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
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+#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/ui/widget/point.cpp b/src/ui/widget/point.cpp
new file mode 100644
index 0000000..5bc50ab
--- /dev/null
+++ b/src/ui/widget/point.cpp
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2007 Authors
+ * Copyright (C) 2004 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/point.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic),
+ xwidget("X:",""),
+ ywidget("Y:","")
+{
+ static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->show_all_children();
+}
+
+Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic),
+ xwidget("X:","", digits),
+ ywidget("Y:","", digits)
+{
+ static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->show_all_children();
+}
+
+Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Box(Gtk::ORIENTATION_VERTICAL), suffix, icon, mnemonic),
+ xwidget("X:","", adjust, digits),
+ ywidget("Y:","", adjust, digits)
+{
+ static_cast<Gtk::Box*>(_widget)->pack_start(xwidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->pack_start(ywidget, true, true);
+ static_cast<Gtk::Box*>(_widget)->show_all_children();
+}
+
+unsigned Point::getDigits() const
+{
+ return xwidget.getDigits();
+}
+
+double Point::getStep() const
+{
+ return xwidget.getStep();
+}
+
+double Point::getPage() const
+{
+ return xwidget.getPage();
+}
+
+double Point::getRangeMin() const
+{
+ return xwidget.getRangeMin();
+}
+
+double Point::getRangeMax() const
+{
+ return xwidget.getRangeMax();
+}
+
+double Point::getXValue() const
+{
+ return xwidget.getValue();
+}
+
+double Point::getYValue() const
+{
+ return ywidget.getValue();
+}
+
+Geom::Point Point::getValue() const
+{
+ return Geom::Point( getXValue() , getYValue() );
+}
+
+int Point::getXValueAsInt() const
+{
+ return xwidget.getValueAsInt();
+}
+
+int Point::getYValueAsInt() const
+{
+ return ywidget.getValueAsInt();
+}
+
+
+void Point::setDigits(unsigned digits)
+{
+ xwidget.setDigits(digits);
+ ywidget.setDigits(digits);
+}
+
+void Point::setIncrements(double step, double page)
+{
+ xwidget.setIncrements(step, page);
+ ywidget.setIncrements(step, page);
+}
+
+void Point::setRange(double min, double max)
+{
+ xwidget.setRange(min, max);
+ ywidget.setRange(min, max);
+}
+
+void Point::setValue(Geom::Point const & p)
+{
+ xwidget.setValue(p[0]);
+ ywidget.setValue(p[1]);
+}
+
+void Point::update()
+{
+ xwidget.update();
+ ywidget.update();
+}
+
+bool Point::setProgrammatically()
+{
+ return (xwidget.setProgrammatically || ywidget.setProgrammatically);
+}
+
+void Point::clearProgrammatically()
+{
+ xwidget.setProgrammatically = false;
+ ywidget.setProgrammatically = false;
+}
+
+
+Glib::SignalProxy0<void> Point::signal_x_value_changed()
+{
+ return xwidget.signal_value_changed();
+}
+
+Glib::SignalProxy0<void> Point::signal_y_value_changed()
+{
+ return ywidget.signal_value_changed();
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/point.h b/src/ui/widget/point.h
new file mode 100644
index 0000000..018be5b
--- /dev/null
+++ b/src/ui/widget/point.h
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2007 Authors
+ * Copyright (C) 2004 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_UI_WIDGET_POINT_H
+#define INKSCAPE_UI_WIDGET_POINT_H
+
+#include "ui/widget/labelled.h"
+#include <2geom/point.h>
+#include "ui/widget/scalar.h"
+
+namespace Gtk {
+class Adjustment;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional icon or suffix, for
+ * entering arbitrary coordinate values.
+ */
+class Point : public Labelled
+{
+public:
+
+
+ /**
+ * Construct a Point Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Point( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Point Widget.
+ *
+ * @param label Label.
+ * @param digits Number of decimal digits to display.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Point( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Point Widget.
+ *
+ * @param label Label.
+ * @param adjust Adjustment to use for the SpinButton.
+ * @param digits Number of decimal digits to display (defaults to 0).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Point( Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits = 0,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Fetches the precision of the spin button.
+ */
+ unsigned getDigits() const;
+
+ /**
+ * Gets the current step increment used by the spin button.
+ */
+ double getStep() const;
+
+ /**
+ * Gets the current page increment used by the spin button.
+ */
+ double getPage() const;
+
+ /**
+ * Gets the minimum range value allowed for the spin button.
+ */
+ double getRangeMin() const;
+
+ /**
+ * Gets the maximum range value allowed for the spin button.
+ */
+ double getRangeMax() const;
+
+ bool getSnapToTicks() const;
+
+ /**
+ * Get the value in the spin_button.
+ */
+ double getXValue() const;
+
+ double getYValue() const;
+
+ Geom::Point getValue() const;
+
+ /**
+ * Get the value spin_button represented as an integer.
+ */
+ int getXValueAsInt() const;
+
+ int getYValueAsInt() const;
+
+ /**
+ * Sets the precision to be displayed by the spin button.
+ */
+ void setDigits(unsigned digits);
+
+ /**
+ * Sets the step and page increments for the spin button.
+ */
+ void setIncrements(double step, double page);
+
+ /**
+ * Sets the minimum and maximum range allowed for the spin button.
+ */
+ void setRange(double min, double max);
+
+ /**
+ * Sets the value of the spin button.
+ */
+ void setValue(Geom::Point const & p);
+
+ /**
+ * Manually forces an update of the spin button.
+ */
+ void update();
+
+ /**
+ * Signal raised when the spin button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_x_value_changed();
+
+ Glib::SignalProxy0<void> signal_y_value_changed();
+
+ /**
+ * Check 'setProgrammatically' of both scalar widgets. False if value is changed by user by clicking the widget.
+ * true if the value was set by setValue, not changed by the user;
+ * if a callback checks it, it must reset it back to false.
+ */
+ bool setProgrammatically();
+
+ void clearProgrammatically();
+
+protected:
+ Scalar xwidget;
+ Scalar ywidget;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_POINT_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/ui/widget/preferences-widget.cpp b/src/ui/widget/preferences-widget.cpp
new file mode 100644
index 0000000..e19ebe3
--- /dev/null
+++ b/src/ui/widget/preferences-widget.cpp
@@ -0,0 +1,1118 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape Preferences dialog.
+ *
+ * Authors:
+ * Marco Scholten
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * Copyright (C) 2004, 2006, 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/convert.h>
+#include <glibmm/regex.h>
+
+#include <gtkmm/box.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/table.h>
+
+
+#include "desktop.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "preferences.h"
+#include "selcue.h"
+#include "selection-chemistry.h"
+
+#include "include/gtkmm_version.h"
+
+#include "io/sys.h"
+
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-loader.h"
+#include "ui/util.h"
+#include "ui/widget/preferences-widget.h"
+
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+using namespace Inkscape::UI::Widget;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+DialogPage::DialogPage()
+{
+ set_border_width(12);
+
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+ set_column_spacing(12);
+ set_row_spacing(6);
+}
+
+/**
+ * Add a widget to the bottom row of the dialog page
+ *
+ * \param[in] indent Whether the widget should be indented by one column
+ * \param[in] label The label text for the widget
+ * \param[in] widget The widget to add to the page
+ * \param[in] suffix Text for an optional label at the right of the widget
+ * \param[in] tip Tooltip text for the widget
+ * \param[in] expand_widget Whether to expand the widget horizontally
+ * \param[in] other_widget An optional additional widget to display at the right of the first one
+ */
+void DialogPage::add_line(bool indent,
+ Glib::ustring const &label,
+ Gtk::Widget &widget,
+ Glib::ustring const &suffix,
+ const Glib::ustring &tip,
+ bool expand_widget,
+ Gtk::Widget *other_widget)
+{
+ if (tip != "")
+ widget.set_tooltip_text (tip);
+
+ auto hb = Gtk::manage(new Gtk::Box());
+ hb->set_spacing(12);
+ hb->set_hexpand(true);
+ hb->pack_start(widget, expand_widget, expand_widget);
+ hb->set_valign(Gtk::ALIGN_CENTER);
+
+ // Add a label in the first column if provided
+ if (label != "")
+ {
+ Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(label, Gtk::ALIGN_START,
+ Gtk::ALIGN_CENTER, true));
+ label_widget->set_mnemonic_widget(widget);
+ label_widget->set_markup(label_widget->get_text());
+
+ if (indent) {
+ label_widget->set_margin_start(12);
+ }
+
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ add(*label_widget);
+ attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1);
+ }
+
+ // Now add the widget to the bottom of the dialog
+ if (label == "")
+ {
+ if (indent) {
+ hb->set_margin_start(12);
+ }
+
+ add(*hb);
+
+ GValue width = G_VALUE_INIT;
+ g_value_init(&width, G_TYPE_INT);
+ g_value_set_int(&width, 2);
+ gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(hb->gobj()), "width", &width);
+ }
+
+ // Add a label on the right of the widget if desired
+ if (suffix != "")
+ {
+ Gtk::Label* suffix_widget = Gtk::manage(new Gtk::Label(suffix , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
+ suffix_widget->set_markup(suffix_widget->get_text());
+ hb->pack_start(*suffix_widget,false,false);
+ }
+
+ // Pack an additional widget into a box with the widget if desired
+ if (other_widget)
+ hb->pack_start(*other_widget, expand_widget, expand_widget);
+}
+
+void DialogPage::add_group_header(Glib::ustring name, int columns)
+{
+ if (name != "")
+ {
+ Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring(/*"<span size='large'>*/"<b>") + name +
+ Glib::ustring("</b>"/*</span>"*/) , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
+
+ label_widget->set_use_markup(true);
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ add(*label_widget);
+ if (columns > 1) {
+ GValue width = G_VALUE_INIT;
+ g_value_init(&width, G_TYPE_INT);
+ g_value_set_int(&width, columns);
+ gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(label_widget->gobj()), "width", &width);
+ }
+ }
+}
+
+void DialogPage::add_group_note(Glib::ustring name)
+{
+ if (name != "")
+ {
+ Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring("<i>") + name +
+ Glib::ustring("</i>") , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
+ label_widget->set_use_markup(true);
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ label_widget->set_line_wrap(true);
+ label_widget->set_line_wrap_mode(Pango::WRAP_WORD);
+
+ add(*label_widget);
+ GValue width = G_VALUE_INIT;
+ g_value_init(&width, G_TYPE_INT);
+ g_value_set_int(&width, 2);
+ gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(label_widget->gobj()), "width", &width);
+ }
+}
+
+void DialogPage::set_tip(Gtk::Widget& widget, Glib::ustring const &tip)
+{
+ widget.set_tooltip_text (tip);
+}
+
+void PrefCheckButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ bool default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->set_label(label);
+ this->set_active( prefs->getBool(_prefs_path, default_value) );
+}
+
+void PrefCheckButton::on_toggled()
+{
+ if (this->get_visible()) //only take action if the user toggled it
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(_prefs_path, this->get_active());
+ }
+ this->changed_signal.emit(this->get_active());
+}
+
+void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member)
+{
+ _prefs_path = prefs_path;
+ _value_type = VAL_STRING;
+ _string_value = string_value;
+ (void)default_value;
+ this->set_label(label);
+ if (group_member)
+ {
+ Gtk::RadioButtonGroup rbg = group_member->get_group();
+ this->set_group(rbg);
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring val = prefs->getString(_prefs_path);
+ if ( !val.empty() )
+ this->set_active(val == _string_value);
+ else
+ this->set_active( false );
+}
+
+void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ int int_value, bool default_value, PrefRadioButton* group_member)
+{
+ _prefs_path = prefs_path;
+ _value_type = VAL_INT;
+ _int_value = int_value;
+ this->set_label(label);
+ if (group_member)
+ {
+ Gtk::RadioButtonGroup rbg = group_member->get_group();
+ this->set_group(rbg);
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (default_value)
+ this->set_active( prefs->getInt(_prefs_path, int_value) == _int_value );
+ else
+ this->set_active( prefs->getInt(_prefs_path, int_value + 1) == _int_value );
+}
+
+void PrefRadioButton::on_toggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (this->get_visible() && this->get_active() ) //only take action if toggled by user (to active)
+ {
+ if ( _value_type == VAL_STRING )
+ prefs->setString(_prefs_path, _string_value);
+ else if ( _value_type == VAL_INT )
+ prefs->setInt(_prefs_path, _int_value);
+ }
+ this->changed_signal.emit(this->get_active());
+}
+
+
+PrefRadioButtons::PrefRadioButtons(const std::vector<PrefItem>& buttons, const Glib::ustring& prefs_path) {
+ set_spacing(2);
+
+ PrefRadioButton* group = nullptr;
+ for (auto&& item : buttons) {
+ auto* btn = Gtk::make_managed<PrefRadioButton>();
+ btn->init(item.label, prefs_path, item.int_value, item.is_default, group);
+ btn->set_tooltip_text(item.tooltip);
+ add(*btn);
+ if (!group) group = btn;
+ }
+}
+
+
+void PrefSpinButton::init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double /*page_increment*/,
+ double default_value, bool is_int, bool is_percent)
+{
+ _prefs_path = prefs_path;
+ _is_int = is_int;
+ _is_percent = is_percent;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value;
+ if (is_int) {
+ if (is_percent) {
+ value = 100 * prefs->getDoubleLimited(prefs_path, default_value, lower/100.0, upper/100.0);
+ } else {
+ value = (double) prefs->getIntLimited(prefs_path, (int) default_value, (int) lower, (int) upper);
+ }
+ } else {
+ value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
+ }
+
+ this->set_range (lower, upper);
+ this->set_increments (step_increment, 0);
+ this->set_value (value);
+ this->set_width_chars(6);
+ if (is_int)
+ this->set_digits(0);
+ else if (step_increment < 0.1)
+ this->set_digits(4);
+ else
+ this->set_digits(2);
+
+}
+
+void PrefSpinButton::on_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (this->get_visible()) //only take action if user changed value
+ {
+ if (_is_int) {
+ if (_is_percent) {
+ prefs->setDouble(_prefs_path, this->get_value()/100.0);
+ } else {
+ prefs->setInt(_prefs_path, (int) this->get_value());
+ }
+ } else {
+ prefs->setDouble(_prefs_path, this->get_value());
+ }
+ }
+ this->changed_signal.emit(this->get_value());
+}
+
+void PrefSpinUnit::init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment,
+ double default_value, UnitType unit_type, Glib::ustring const &default_unit)
+{
+ _prefs_path = prefs_path;
+ _is_percent = (unit_type == UNIT_TYPE_DIMENSIONLESS);
+
+ resetUnitType(unit_type);
+ setUnit(default_unit);
+ setRange (lower, upper); /// @fixme this disregards changes of units
+ setIncrements (step_increment, 0);
+ if (step_increment < 0.1) {
+ setDigits(4);
+ } else {
+ setDigits(2);
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
+ Glib::ustring unitstr = prefs->getUnit(prefs_path);
+ if (unitstr.length() == 0) {
+ unitstr = default_unit;
+ // write the assumed unit to preferences:
+ prefs->setDoubleUnit(_prefs_path, value, unitstr);
+ }
+ setValue(value, unitstr);
+
+ signal_value_changed().connect_notify(sigc::mem_fun(*this, &PrefSpinUnit::on_my_value_changed));
+}
+
+void PrefSpinUnit::on_my_value_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (getWidget()->get_visible()) //only take action if user changed value
+ {
+ prefs->setDoubleUnit(_prefs_path, getValue(getUnit()->abbr), getUnit()->abbr);
+ }
+}
+
+const double ZoomCorrRuler::textsize = 7;
+const double ZoomCorrRuler::textpadding = 5;
+
+ZoomCorrRuler::ZoomCorrRuler(int width, int height) :
+ _unitconv(1.0),
+ _border(5)
+{
+ set_size(width, height);
+}
+
+void ZoomCorrRuler::set_size(int x, int y)
+{
+ _min_width = x;
+ _height = y;
+ set_size_request(x + _border*2, y + _border*2);
+}
+
+// The following two functions are borrowed from 2geom's toy-framework-2; if they are useful in
+// other locations, we should perhaps make them (or adapted versions of them) publicly available
+static void
+draw_text(cairo_t *cr, Geom::Point loc, const char* txt, bool bottom = false,
+ double fontsize = ZoomCorrRuler::textsize, std::string fontdesc = "Sans") {
+ PangoLayout* layout = pango_cairo_create_layout (cr);
+ pango_layout_set_text(layout, txt, -1);
+
+ // set font and size
+ std::ostringstream sizestr;
+ sizestr << fontsize;
+ fontdesc = fontdesc + " " + sizestr.str();
+ PangoFontDescription *font_desc = pango_font_description_from_string(fontdesc.c_str());
+ pango_layout_set_font_description(layout, font_desc);
+ pango_font_description_free (font_desc);
+
+ PangoRectangle logical_extent;
+ pango_layout_get_pixel_extents(layout, nullptr, &logical_extent);
+ cairo_move_to(cr, loc[Geom::X], loc[Geom::Y] - (bottom ? logical_extent.height : 0));
+ pango_cairo_show_layout(cr, layout);
+}
+
+static void
+draw_number(cairo_t *cr, Geom::Point pos, double num) {
+ std::ostringstream number;
+ number << num;
+ draw_text(cr, pos, number.str().c_str(), true);
+}
+
+/*
+ * \arg dist The distance between consecutive minor marks
+ * \arg major_interval Number of marks after which to draw a major mark
+ */
+void
+ZoomCorrRuler::draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ const double zoomcorr = prefs->getDouble("/options/zoomcorrection/value", 1.0);
+ double mark = 0;
+ int i = 0;
+ double step = dist * zoomcorr / _unitconv;
+ bool draw_minor = true;
+ if (step <= 0) {
+ return;
+ }
+ else if (step < 2) {
+ // marks too dense
+ draw_minor = false;
+ }
+ int last_pos = -1;
+ while (mark <= _drawing_width) {
+ cr->move_to(mark, _height);
+ if ((i % major_interval) == 0) {
+ // don't overcrowd the marks
+ if (static_cast<int>(mark) > last_pos) {
+ // major mark
+ cr->line_to(mark, 0);
+ Geom::Point textpos(mark + 3, ZoomCorrRuler::textsize + ZoomCorrRuler::textpadding);
+ draw_number(cr->cobj(), textpos, dist * i);
+
+ last_pos = static_cast<int>(mark) + 1;
+ }
+ } else if (draw_minor) {
+ // minor mark
+ cr->line_to(mark, ZoomCorrRuler::textsize + 2 * ZoomCorrRuler::textpadding);
+ }
+ mark += step;
+ ++i;
+ }
+}
+
+bool
+ZoomCorrRuler::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
+ Glib::RefPtr<Gdk::Window> window = get_window();
+
+ int w = window->get_width();
+ _drawing_width = w - _border * 2;
+
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ Gdk::RGBA bg;
+ bg.set_grey(0.5);
+ if (auto wnd = dynamic_cast<Gtk::Window*>(this->get_toplevel())) {
+ auto sc = wnd->get_style_context();
+ bg = get_background_color(sc);
+ }
+
+ cr->set_source_rgb(bg.get_red(), bg.get_green(), bg.get_blue());
+ cr->set_fill_rule(Cairo::FILL_RULE_WINDING);
+ cr->rectangle(0, 0, w, _height + _border*2);
+ cr->fill();
+
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->set_line_width(0.5);
+
+ cr->translate(_border, _border); // so that we have a small white border around the ruler
+ cr->move_to (0, _height);
+ cr->line_to (_drawing_width, _height);
+
+ cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue());
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring abbr = prefs->getString("/options/zoomcorrection/unit");
+ if (abbr == "cm") {
+ draw_marks(cr, 0.1, 10);
+ } else if (abbr == "in") {
+ draw_marks(cr, 0.25, 4);
+ } else if (abbr == "mm") {
+ draw_marks(cr, 10, 10);
+ } else if (abbr == "pc") {
+ draw_marks(cr, 1, 10);
+ } else if (abbr == "pt") {
+ draw_marks(cr, 10, 10);
+ } else if (abbr == "px") {
+ draw_marks(cr, 10, 10);
+ } else {
+ draw_marks(cr, 1, 1);
+ }
+ cr->stroke();
+
+ return true;
+}
+
+
+void
+ZoomCorrRulerSlider::on_slider_value_changed()
+{
+ if (this->get_visible() || freeze) //only take action if user changed value
+ {
+ freeze = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/options/zoomcorrection/value", _slider->get_value() / 100.0);
+ _sb->set_value(_slider->get_value());
+ _ruler.queue_draw();
+ freeze = false;
+ }
+}
+
+void
+ZoomCorrRulerSlider::on_spinbutton_value_changed()
+{
+ if (this->get_visible() || freeze) //only take action if user changed value
+ {
+ freeze = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/options/zoomcorrection/value", _sb->get_value() / 100.0);
+ _slider->set_value(_sb->get_value());
+ _ruler.queue_draw();
+ freeze = false;
+ }
+}
+
+void
+ZoomCorrRulerSlider::on_unit_changed() {
+ if (!_unit.get_sensitive()) {
+ // when the unit menu is initialized, the unit is set to the default but
+ // it needs to be reset later so we don't perform the change in this case
+ return;
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/options/zoomcorrection/unit", _unit.getUnitAbbr());
+ double conv = _unit.getConversion(_unit.getUnitAbbr(), "px");
+ _ruler.set_unit_conversion(conv);
+ if (_ruler.get_visible()) {
+ _ruler.queue_draw();
+ }
+}
+
+bool ZoomCorrRulerSlider::on_mnemonic_activate ( bool group_cycling )
+{
+ return _sb->mnemonic_activate ( group_cycling );
+}
+
+
+void
+ZoomCorrRulerSlider::init(int ruler_width, int ruler_height, double lower, double upper,
+ double step_increment, double page_increment, double default_value)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited("/options/zoomcorrection/value", default_value, lower, upper) * 100.0;
+
+ freeze = false;
+
+ _ruler.set_size(ruler_width, ruler_height);
+
+ _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+
+ _slider->set_size_request(_ruler.width(), -1);
+ _slider->set_range (lower, upper);
+ _slider->set_increments (step_increment, page_increment);
+ _slider->set_value (value);
+ _slider->set_digits(2);
+
+ _slider->signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_slider_value_changed));
+ _sb = Gtk::manage(new Inkscape::UI::Widget::SpinButton());
+ _sb->signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_spinbutton_value_changed));
+ _unit.signal_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_unit_changed));
+
+ _sb->set_range (lower, upper);
+ _sb->set_increments (step_increment, 0);
+ _sb->set_value (value);
+ _sb->set_digits(2);
+ _sb->set_halign(Gtk::ALIGN_CENTER);
+ _sb->set_valign(Gtk::ALIGN_END);
+
+ _unit.set_sensitive(false);
+ _unit.setUnitType(UNIT_TYPE_LINEAR);
+ _unit.set_sensitive(true);
+ _unit.setUnit(prefs->getString("/options/zoomcorrection/unit"));
+ _unit.set_halign(Gtk::ALIGN_CENTER);
+ _unit.set_valign(Gtk::ALIGN_END);
+
+ _slider->set_hexpand(true);
+ _ruler.set_hexpand(true);
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->attach(*_slider, 0, 0, 1, 1);
+ table->attach(*_sb, 1, 0, 1, 1);
+ table->attach(_ruler, 0, 1, 1, 1);
+ table->attach(_unit, 1, 1, 1, 1);
+
+ pack_start(*table, Gtk::PACK_SHRINK);
+}
+
+void
+PrefSlider::on_slider_value_changed()
+{
+ if (this->get_visible() || freeze) //only take action if user changed value
+ {
+ freeze = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble(_prefs_path, _slider->get_value());
+ if (_sb) _sb->set_value(_slider->get_value());
+ freeze = false;
+ }
+}
+
+void
+PrefSlider::on_spinbutton_value_changed()
+{
+ if (this->get_visible() || freeze) //only take action if user changed value
+ {
+ freeze = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (_sb) {
+ prefs->setDouble(_prefs_path, _sb->get_value());
+ _slider->set_value(_sb->get_value());
+ }
+ freeze = false;
+ }
+}
+
+bool PrefSlider::on_mnemonic_activate ( bool group_cycling )
+{
+ return _sb ? _sb->mnemonic_activate ( group_cycling ) : false;
+}
+
+void
+PrefSlider::init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double page_increment, double default_value, int digits)
+{
+ _prefs_path = prefs_path;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
+
+ freeze = false;
+
+ _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+
+ _slider->set_range (lower, upper);
+ _slider->set_increments (step_increment, page_increment);
+ _slider->set_value (value);
+ _slider->set_digits(digits);
+ _slider->signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_slider_value_changed));
+ if (_spin) {
+ _sb = Gtk::manage(new Inkscape::UI::Widget::SpinButton());
+ _sb->signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_spinbutton_value_changed));
+ _sb->set_range (lower, upper);
+ _sb->set_increments (step_increment, 0);
+ _sb->set_value (value);
+ _sb->set_digits(digits);
+ _sb->set_halign(Gtk::ALIGN_CENTER);
+ _sb->set_valign(Gtk::ALIGN_END);
+ }
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ _slider->set_hexpand();
+ table->attach(*_slider, 0, 0, 1, 1);
+ if (_sb) table->attach(*_sb, 1, 0, 1, 1);
+
+ this->pack_start(*table, Gtk::PACK_EXPAND_WIDGET);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], int values[], int num_items, int default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ int value = prefs->getInt(_prefs_path, default_value);
+
+ for (int i = 0 ; i < num_items; ++i)
+ {
+ this->append(labels[i]);
+ _values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ Glib::ustring value = prefs->getString(_prefs_path);
+ if(value.empty())
+ {
+ value = default_value;
+ }
+
+ for (int i = 0 ; i < num_items; ++i)
+ {
+ this->append(labels[i]);
+ _ustr_values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values,
+ int default_value)
+{
+ size_t labels_size = labels.size();
+ size_t values_size = values.size();
+ if (values_size != labels_size) {
+ std::cout << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path << std::endl;
+ return;
+ }
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ int value = prefs->getInt(_prefs_path, default_value);
+
+ for (int i = 0; i < labels_size; ++i) {
+ this->append(labels[i]);
+ _values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels,
+ std::vector<Glib::ustring> values, Glib::ustring default_value)
+{
+ size_t labels_size = labels.size();
+ size_t values_size = values.size();
+ if (values_size != labels_size) {
+ std::cout << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path << std::endl;
+ return;
+ }
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int row = 0;
+ Glib::ustring value = prefs->getString(_prefs_path);
+ if (value.empty()) {
+ value = default_value;
+ }
+
+ for (int i = 0; i < labels_size; ++i) {
+ this->append(labels[i]);
+ _ustr_values.push_back(values[i]);
+ if (value == values[i])
+ row = i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::init(Glib::ustring const &prefs_path,
+ std::vector<std::pair<Glib::ustring, Glib::ustring>> labels_and_values,
+ Glib::ustring default_value)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring value = prefs->getString(_prefs_path);
+ if (value.empty()) {
+ value = default_value;
+ }
+
+ int row = 0;
+ int i = 0;
+ for (auto entry : labels_and_values) {
+ this->append(entry.first);
+ _ustr_values.push_back(entry.second);
+ if (value == entry.second) {
+ row = i;
+ }
+ ++i;
+ }
+ this->set_active(row);
+}
+
+void PrefCombo::on_changed()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if(!_values.empty())
+ {
+ prefs->setInt(_prefs_path, _values[this->get_active_row_number()]);
+ }
+ else
+ {
+ prefs->setString(_prefs_path, _ustr_values[this->get_active_row_number()]);
+ }
+ }
+}
+
+void PrefEntryButtonHBox::init(Glib::ustring const &prefs_path,
+ bool visibility, Glib::ustring const &default_string)
+{
+ _prefs_path = prefs_path;
+ _default_string = default_string;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ relatedEntry = new Gtk::Entry();
+ relatedButton = new Gtk::Button(_("Reset"));
+ relatedEntry->set_invisible_char('*');
+ relatedEntry->set_visibility(visibility);
+ relatedEntry->set_text(prefs->getString(_prefs_path));
+ this->pack_start(*relatedEntry);
+ this->pack_start(*relatedButton);
+ relatedButton->signal_clicked().connect(
+ sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedButtonClickedCallback));
+ relatedEntry->signal_changed().connect(
+ sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedEntryChangedCallback));
+}
+
+void PrefEntryButtonHBox::onRelatedEntryChangedCallback()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, relatedEntry->get_text());
+ }
+}
+
+void PrefEntryButtonHBox::onRelatedButtonClickedCallback()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, _default_string);
+ relatedEntry->set_text(_default_string);
+ }
+}
+
+bool PrefEntryButtonHBox::on_mnemonic_activate ( bool group_cycling )
+{
+ return relatedEntry->mnemonic_activate ( group_cycling );
+}
+
+void PrefEntryFileButtonHBox::init(Glib::ustring const &prefs_path,
+ bool visibility)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ relatedEntry = new Gtk::Entry();
+ relatedEntry->set_invisible_char('*');
+ relatedEntry->set_visibility(visibility);
+ relatedEntry->set_text(prefs->getString(_prefs_path));
+
+ relatedButton = new Gtk::Button();
+ Gtk::Box* pixlabel = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 3);
+ Gtk::Image *im = sp_get_icon_image("applications-graphics", Gtk::ICON_SIZE_BUTTON);
+ pixlabel->pack_start(*im);
+ Gtk::Label *l = new Gtk::Label();
+ l->set_markup_with_mnemonic(_("_Browse..."));
+ pixlabel->pack_start(*l);
+ relatedButton->add(*pixlabel);
+
+ this->pack_end(*relatedButton, false, false, 4);
+ this->pack_start(*relatedEntry, true, true, 0);
+
+ relatedButton->signal_clicked().connect(
+ sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedButtonClickedCallback));
+ relatedEntry->signal_changed().connect(
+ sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedEntryChangedCallback));
+}
+
+void PrefEntryFileButtonHBox::onRelatedEntryChangedCallback()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, relatedEntry->get_text());
+ }
+}
+
+static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr;
+
+void PrefEntryFileButtonHBox::onRelatedButtonClickedCallback()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ //# Get the current directory for finding files
+ static Glib::ustring open_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+
+ Glib::ustring attr = prefs->getString(_prefs_path);
+ if (!attr.empty()) open_path = attr;
+
+ //# Test if the open_path directory exists
+ if (!Inkscape::IO::file_test(open_path.c_str(),
+ (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)))
+ open_path = "";
+
+#ifdef _WIN32
+ //# If no open path, default to our win32 documents folder
+ if (open_path.empty())
+ {
+ // The path to the My Documents folder is read from the
+ // value "HKEY_CURRENT_USER\Software\Windows\CurrentVersion\Explorer\Shell Folders\Personal"
+ HKEY key = NULL;
+ if(RegOpenKeyExA(HKEY_CURRENT_USER,
+ "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders",
+ 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
+ {
+ WCHAR utf16path[_MAX_PATH];
+ DWORD value_type;
+ DWORD data_size = sizeof(utf16path);
+ if(RegQueryValueExW(key, L"Personal", NULL, &value_type,
+ (BYTE*)utf16path, &data_size) == ERROR_SUCCESS)
+ {
+ g_assert(value_type == REG_SZ);
+ gchar *utf8path = g_utf16_to_utf8(
+ (const gunichar2*)utf16path, -1, NULL, NULL, NULL);
+ if(utf8path)
+ {
+ open_path = Glib::ustring(utf8path);
+ g_free(utf8path);
+ }
+ }
+ }
+ }
+#endif
+
+ //# If no open path, default to our home directory
+ if (open_path.empty())
+ {
+ open_path = g_get_home_dir();
+ open_path.append(G_DIR_SEPARATOR_S);
+ }
+
+ //# Create a dialog
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!selectPrefsFileInstance) {
+ selectPrefsFileInstance =
+ Inkscape::UI::Dialog::FileOpenDialog::create(
+ *desktop->getToplevel(),
+ open_path,
+ Inkscape::UI::Dialog::EXE_TYPES,
+ _("Select a bitmap editor"));
+ }
+
+ //# Show the dialog
+ bool const success = selectPrefsFileInstance->show();
+
+ if (!success) {
+ return;
+ }
+
+ //# User selected something. Get name and type
+ Glib::ustring fileName = selectPrefsFileInstance->getFilename();
+
+ if (!fileName.empty())
+ {
+ Glib::ustring newFileName = Glib::filename_to_utf8(fileName);
+
+ if ( newFileName.size() > 0)
+ open_path = newFileName;
+ else
+ g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" );
+
+ prefs->setString(_prefs_path, open_path);
+ }
+
+ relatedEntry->set_text(fileName);
+ }
+}
+
+bool PrefEntryFileButtonHBox::on_mnemonic_activate ( bool group_cycling )
+{
+ return relatedEntry->mnemonic_activate ( group_cycling );
+}
+
+void PrefOpenFolder::init(Glib::ustring const &entry_string, Glib::ustring const &tooltip)
+{
+ relatedEntry = new Gtk::Entry();
+ relatedButton = new Gtk::Button();
+ Gtk::Box *pixlabel = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 3);
+ Gtk::Image *im = sp_get_icon_image("document-open", Gtk::ICON_SIZE_BUTTON);
+ pixlabel->pack_start(*im);
+ Gtk::Label *l = new Gtk::Label();
+ l->set_markup_with_mnemonic(_("Open"));
+ pixlabel->pack_start(*l);
+ relatedButton->add(*pixlabel);
+ relatedButton->set_tooltip_text(tooltip);
+ relatedEntry->set_text(entry_string);
+ relatedEntry->set_sensitive(false);
+ this->pack_end(*relatedButton, false, false, 4);
+ this->pack_start(*relatedEntry, true, true, 0);
+ relatedButton->signal_clicked().connect(sigc::mem_fun(*this, &PrefOpenFolder::onRelatedButtonClickedCallback));
+}
+
+void PrefOpenFolder::onRelatedButtonClickedCallback()
+{
+ g_mkdir_with_parents(relatedEntry->get_text().c_str(), 0700);
+ // https://stackoverflow.com/questions/42442189/how-to-open-spawn-a-file-with-glib-gtkmm-in-windows
+#ifdef _WIN32
+ ShellExecute(NULL, "open", relatedEntry->get_text().c_str(), NULL, NULL, SW_SHOWDEFAULT);
+#elif defined(__APPLE__)
+ std::vector<std::string> argv = { "open", relatedEntry->get_text().raw() };
+ Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH);
+#else
+ gchar *path = g_filename_to_uri(relatedEntry->get_text().c_str(), NULL, NULL);
+ std::vector<std::string> argv = { "xdg-open", path };
+ Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH);
+ g_free(path);
+#endif
+}
+
+void PrefFileButton::init(Glib::ustring const &prefs_path)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ select_filename(Glib::filename_from_utf8(prefs->getString(_prefs_path)));
+
+ signal_selection_changed().connect(sigc::mem_fun(*this, &PrefFileButton::onFileChanged));
+}
+
+void PrefFileButton::onFileChanged()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, Glib::filename_to_utf8(get_filename()));
+}
+
+void PrefEntry::init(Glib::ustring const &prefs_path, bool visibility)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->set_invisible_char('*');
+ this->set_visibility(visibility);
+ this->set_text(prefs->getString(_prefs_path));
+}
+
+void PrefEntry::on_changed()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, this->get_text());
+ }
+}
+
+void PrefMultiEntry::init(Glib::ustring const &prefs_path, int height)
+{
+ // TODO: Figure out if there's a way to specify height in lines instead of px
+ // and how to obtain a reasonable default width if 'expand_widget' is not used
+ set_size_request(100, height);
+ set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ set_shadow_type(Gtk::SHADOW_IN);
+
+ add(_text);
+
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring value = prefs->getString(_prefs_path);
+ value = Glib::Regex::create("\\|")->replace_literal(value, 0, "\n", (Glib::RegexMatchFlags)0);
+ _text.get_buffer()->set_text(value);
+ _text.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &PrefMultiEntry::on_changed));
+}
+
+void PrefMultiEntry::on_changed()
+{
+ if (get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring value = _text.get_buffer()->get_text();
+ value = Glib::Regex::create("\\n")->replace_literal(value, 0, "|", (Glib::RegexMatchFlags)0);
+ prefs->setString(_prefs_path, value);
+ }
+}
+
+void PrefColorPicker::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ guint32 default_rgba)
+{
+ _prefs_path = prefs_path;
+ _title = label;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->setRgba32( prefs->getInt(_prefs_path, (int)default_rgba) );
+}
+
+void PrefColorPicker::on_changed (guint32 rgba)
+{
+ if (this->get_visible()) //only take action if the user toggled it
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt(_prefs_path, (int) rgba);
+ }
+}
+
+void PrefUnit::init(Glib::ustring const &prefs_path)
+{
+ _prefs_path = prefs_path;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ setUnitType(UNIT_TYPE_LINEAR);
+ setUnit(prefs->getString(_prefs_path));
+}
+
+void PrefUnit::on_changed()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, getUnitAbbr());
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/preferences-widget.h b/src/ui/widget/preferences-widget.h
new file mode 100644
index 0000000..07fee61
--- /dev/null
+++ b/src/ui/widget/preferences-widget.h
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Widgets for Inkscape Preferences dialog.
+ */
+/*
+ * Authors:
+ * Marco Scholten
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * Copyright (C) 2004, 2006, 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H
+#define INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H
+
+#include <iostream>
+#include <vector>
+
+#include <gtkmm/filechooserbutton.h>
+#include "ui/widget/spinbutton.h"
+#include <cstddef>
+#include <sigc++/sigc++.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/grid.h>
+
+#include "ui/widget/color-picker.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/scalar-unit.h"
+
+namespace Gtk {
+class Scale;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class PrefCheckButton : public Gtk::CheckButton
+{
+public:
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ bool default_value);
+ sigc::signal<void, bool> changed_signal;
+protected:
+ Glib::ustring _prefs_path;
+ void on_toggled() override;
+};
+
+class PrefRadioButton : public Gtk::RadioButton
+{
+public:
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ int int_value, bool default_value, PrefRadioButton* group_member);
+ void init(Glib::ustring const &label, Glib::ustring const &prefs_path,
+ Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member);
+ sigc::signal<void, bool> changed_signal;
+protected:
+ Glib::ustring _prefs_path;
+ Glib::ustring _string_value;
+ int _value_type;
+ enum
+ {
+ VAL_INT,
+ VAL_STRING
+ };
+ int _int_value;
+ void on_toggled() override;
+};
+
+struct PrefItem { Glib::ustring label; int int_value; Glib::ustring tooltip; bool is_default = false; };
+
+class PrefRadioButtons : public Gtk::Box {
+public:
+ PrefRadioButtons(const std::vector<PrefItem>& buttons, const Glib::ustring& prefs_path);
+
+private:
+};
+
+class PrefSpinButton : public SpinButton
+{
+public:
+ void init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double page_increment,
+ double default_value, bool is_int, bool is_percent);
+ sigc::signal<void, double> changed_signal;
+protected:
+ Glib::ustring _prefs_path;
+ bool _is_int;
+ bool _is_percent;
+ void on_value_changed() override;
+};
+
+class PrefSpinUnit : public ScalarUnit
+{
+public:
+ PrefSpinUnit() : ScalarUnit("", "") {};
+
+ void init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment,
+ double default_value,
+ UnitType unit_type, Glib::ustring const &default_unit);
+protected:
+ Glib::ustring _prefs_path;
+ bool _is_percent;
+ void on_my_value_changed();
+};
+
+class ZoomCorrRuler : public Gtk::DrawingArea {
+public:
+ ZoomCorrRuler(int width = 100, int height = 20);
+ void set_size(int x, int y);
+ void set_unit_conversion(double conv) { _unitconv = conv; }
+
+ int width() { return _min_width + _border*2; }
+
+ static const double textsize;
+ static const double textpadding;
+
+private:
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+
+ void draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval);
+
+ double _unitconv;
+ int _min_width;
+ int _height;
+ int _border;
+ int _drawing_width;
+};
+
+class ZoomCorrRulerSlider : public Gtk::Box
+{
+public:
+ ZoomCorrRulerSlider() : Gtk::Box(Gtk::ORIENTATION_VERTICAL) {}
+
+ void init(int ruler_width, int ruler_height, double lower, double upper,
+ double step_increment, double page_increment, double default_value);
+
+private:
+ void on_slider_value_changed();
+ void on_spinbutton_value_changed();
+ void on_unit_changed();
+ bool on_mnemonic_activate( bool group_cycling ) override;
+
+ Inkscape::UI::Widget::SpinButton *_sb;
+ UnitMenu _unit;
+ Gtk::Scale* _slider;
+ ZoomCorrRuler _ruler;
+ bool freeze; // used to block recursive updates of slider and spinbutton
+};
+
+class PrefSlider : public Gtk::Box
+{
+public:
+ PrefSlider(bool spin = true) : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) { _spin = spin; }
+
+ void init(Glib::ustring const &prefs_path,
+ double lower, double upper, double step_increment, double page_increment, double default_value, int digits);
+
+ Gtk::Scale* getSlider() {return _slider;};
+ Inkscape::UI::Widget::SpinButton * getSpinButton() {return _sb;};
+private:
+ void on_slider_value_changed();
+ void on_spinbutton_value_changed();
+ bool on_mnemonic_activate( bool group_cycling ) override;
+
+ Glib::ustring _prefs_path;
+ Inkscape::UI::Widget::SpinButton *_sb = nullptr;
+ bool _spin;
+ Gtk::Scale* _slider = nullptr;
+
+ bool freeze; // used to block recursive updates of slider and spinbutton
+};
+
+
+class PrefCombo : public Gtk::ComboBoxText
+{
+public:
+ void init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], int values[], int num_items, int default_value);
+
+ /**
+ * Initialize a combo box.
+ * second form uses strings as key values.
+ */
+ void init(Glib::ustring const &prefs_path,
+ Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value);
+ /**
+ * Initialize a combo box.
+ * with vectors.
+ */
+ void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values,
+ int default_value);
+
+ void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<Glib::ustring> values,
+ Glib::ustring default_value);
+
+ /**
+ * Initialize a combo box with a vector of Glib::ustring pairs.
+ */
+ void init(Glib::ustring const &prefs_path,
+ std::vector<std::pair<Glib::ustring, Glib::ustring>> labels_and_values,
+ Glib::ustring default_value);
+
+ protected:
+ Glib::ustring _prefs_path;
+ std::vector<int> _values;
+ std::vector<Glib::ustring> _ustr_values; ///< string key values used optionally instead of numeric _values
+ void on_changed() override;
+};
+
+class PrefEntry : public Gtk::Entry
+{
+public:
+ void init(Glib::ustring const &prefs_path, bool mask);
+protected:
+ Glib::ustring _prefs_path;
+ void on_changed() override;
+};
+
+class PrefMultiEntry : public Gtk::ScrolledWindow
+{
+public:
+ void init(Glib::ustring const &prefs_path, int height);
+protected:
+ Glib::ustring _prefs_path;
+ Gtk::TextView _text;
+ void on_changed();
+};
+
+class PrefEntryButtonHBox : public Gtk::Box
+{
+public:
+ PrefEntryButtonHBox() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {}
+
+ void init(Glib::ustring const &prefs_path,
+ bool mask, Glib::ustring const &default_string);
+
+protected:
+ Glib::ustring _prefs_path;
+ Glib::ustring _default_string;
+ Gtk::Button *relatedButton;
+ Gtk::Entry *relatedEntry;
+ void onRelatedEntryChangedCallback();
+ void onRelatedButtonClickedCallback();
+ bool on_mnemonic_activate( bool group_cycling ) override;
+};
+
+class PrefEntryFileButtonHBox : public Gtk::Box
+{
+public:
+ PrefEntryFileButtonHBox() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {}
+
+ void init(Glib::ustring const &prefs_path,
+ bool mask);
+protected:
+ Glib::ustring _prefs_path;
+ Gtk::Button *relatedButton;
+ Gtk::Entry *relatedEntry;
+ void onRelatedEntryChangedCallback();
+ void onRelatedButtonClickedCallback();
+ bool on_mnemonic_activate( bool group_cycling ) override;
+};
+
+class PrefOpenFolder : public Gtk::Box {
+ public:
+ PrefOpenFolder() : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {}
+
+ void init(Glib::ustring const &entry_string, Glib::ustring const &tooltip);
+
+ protected:
+ Gtk::Button *relatedButton;
+ Gtk::Entry *relatedEntry;
+ void onRelatedButtonClickedCallback();
+};
+
+class PrefFileButton : public Gtk::FileChooserButton
+{
+public:
+ void init(Glib::ustring const &prefs_path);
+
+protected:
+ Glib::ustring _prefs_path;
+ void onFileChanged();
+};
+
+class PrefColorPicker : public ColorPicker
+{
+public:
+ PrefColorPicker() : ColorPicker("", "", 0, false) {};
+ ~PrefColorPicker() override = default;;
+
+ void init(Glib::ustring const &abel, Glib::ustring const &prefs_path,
+ guint32 default_rgba);
+
+protected:
+ Glib::ustring _prefs_path;
+ void on_changed (guint32 rgba) override;
+};
+
+class PrefUnit : public UnitMenu
+{
+public:
+ void init(Glib::ustring const &prefs_path);
+protected:
+ Glib::ustring _prefs_path;
+ void on_changed() override;
+};
+
+class DialogPage : public Gtk::Grid
+{
+public:
+ DialogPage();
+ void add_line(bool indent, Glib::ustring const &label, Gtk::Widget& widget, Glib::ustring const &suffix, Glib::ustring const &tip, bool expand = true, Gtk::Widget *other_widget = nullptr);
+ void add_group_header(Glib::ustring name, int columns = 1);
+ void add_group_note(Glib::ustring name);
+ void set_tip(Gtk::Widget &widget, Glib::ustring const &tip);
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif //INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_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/ui/widget/preview.cpp b/src/ui/widget/preview.cpp
new file mode 100644
index 0000000..663d4b8
--- /dev/null
+++ b/src/ui/widget/preview.cpp
@@ -0,0 +1,511 @@
+// 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 Preview Stuffs.
+ *
+ * The Initial Developer of the Original Code is
+ * Jon A. Cruz.
+ * Portions created by the Initial Developer are Copyright (C) 2005
+ * 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 <algorithm>
+#include <gdkmm/general.h>
+#include "preview.h"
+#include "preferences.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+#define PRIME_BUTTON_MAGIC_NUMBER 1
+
+/* Keep in sync with last value in eek-preview.h */
+#define PREVIEW_SIZE_LAST PREVIEW_SIZE_HUGE
+#define PREVIEW_SIZE_NEXTFREE (PREVIEW_SIZE_HUGE + 1)
+
+#define PREVIEW_MAX_RATIO 500
+
+void
+Preview::set_color(int r, int g, int b )
+{
+ _r = r;
+ _g = g;
+ _b = b;
+
+ queue_draw();
+}
+
+
+void
+Preview::set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf)
+{
+ _previewPixbuf = pixbuf;
+
+ queue_draw();
+
+ if (_scaled)
+ {
+ _scaled.reset();
+ }
+
+ _scaledW = _previewPixbuf->get_width();
+ _scaledH = _previewPixbuf->get_height();
+}
+
+static gboolean setupDone = FALSE;
+static GtkRequisition sizeThings[PREVIEW_SIZE_NEXTFREE];
+
+void
+Preview::set_size_mappings( guint count, GtkIconSize const* sizes )
+{
+ gint width = 0;
+ gint height = 0;
+ gint smallest = 512;
+ gint largest = 0;
+ guint i = 0;
+ guint delta = 0;
+
+ for ( i = 0; i < count; ++i ) {
+ gboolean worked = gtk_icon_size_lookup( sizes[i], &width, &height );
+ if ( worked ) {
+ if ( width < smallest ) {
+ smallest = width;
+ }
+ if ( width > largest ) {
+ largest = width;
+ }
+ }
+ }
+
+ smallest = (smallest * 3) / 4;
+
+ delta = largest - smallest;
+
+ for ( i = 0; i < G_N_ELEMENTS(sizeThings); ++i ) {
+ guint val = smallest + ( (i * delta) / (G_N_ELEMENTS(sizeThings) - 1) );
+ sizeThings[i].width = val;
+ sizeThings[i].height = val;
+ }
+
+ setupDone = TRUE;
+}
+
+void Preview::set_freesize(bool en) {
+ _freesize = en;
+}
+
+void
+Preview::size_request(GtkRequisition* req) const
+{
+ if (_freesize) {
+ req->width = req->height = 1;
+ return;
+ }
+
+ int width = 0;
+ int height = 0;
+
+ if ( !setupDone ) {
+ GtkIconSize sizes[] = {
+ GTK_ICON_SIZE_MENU,
+ GTK_ICON_SIZE_SMALL_TOOLBAR,
+ GTK_ICON_SIZE_LARGE_TOOLBAR,
+ GTK_ICON_SIZE_BUTTON,
+ GTK_ICON_SIZE_DIALOG
+ };
+ set_size_mappings( G_N_ELEMENTS(sizes), sizes );
+ }
+
+ width = sizeThings[_size].width;
+ height = sizeThings[_size].height;
+
+ if ( _view == VIEW_TYPE_LIST ) {
+ width *= 3;
+ }
+
+ if ( _ratio != 100 ) {
+ width = (width * _ratio) / 100;
+ if ( width < 0 ) {
+ width = 1;
+ }
+ }
+
+ req->width = width;
+ req->height = height;
+}
+
+void
+Preview::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_width = natural_width = requisition.width;
+}
+
+void
+Preview::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const
+{
+ GtkRequisition requisition;
+ size_request(&requisition);
+ minimal_height = natural_height = requisition.height;
+}
+
+bool
+Preview::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ auto allocation = get_allocation();
+
+ gint insetTop = 0, insetBottom = 0;
+ gint insetLeft = 0, insetRight = 0;
+
+ if (_border == BORDER_SOLID) {
+ insetTop = 1;
+ insetLeft = 1;
+ }
+ if (_border == BORDER_SOLID_LAST_ROW) {
+ insetTop = insetBottom = 1;
+ insetLeft = 1;
+ }
+ if (_border == BORDER_WIDE) {
+ insetTop = insetBottom = 1;
+ insetLeft = insetRight = 1;
+ }
+
+ auto context = get_style_context();
+
+ context->render_frame(cr,
+ 0, 0,
+ allocation.get_width(), allocation.get_height());
+
+ context->render_background(cr,
+ 0, 0,
+ allocation.get_width(), allocation.get_height());
+
+ // Border
+ if (_border != BORDER_NONE) {
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->rectangle(0, 0, allocation.get_width(), allocation.get_height());
+ cr->fill();
+ }
+
+ cr->set_source_rgb(_r/65535.0, _g/65535.0, _b/65535.0 );
+ cr->rectangle(insetLeft, insetTop, allocation.get_width() - (insetLeft + insetRight), allocation.get_height() - (insetTop + insetBottom));
+ cr->fill();
+
+ if (_previewPixbuf )
+ {
+ if ((allocation.get_width() != _scaledW) || (allocation.get_height() != _scaledH)) {
+ if (_scaled)
+ {
+ _scaled.reset();
+ }
+
+ _scaledW = allocation.get_width() - (insetLeft + insetRight);
+ _scaledH = allocation.get_height() - (insetTop + insetBottom);
+
+ _scaled = _previewPixbuf->scale_simple(_scaledW,
+ _scaledH,
+ Gdk::INTERP_BILINEAR);
+ }
+
+ Glib::RefPtr<Gdk::Pixbuf> pix = (_scaled) ? _scaled : _previewPixbuf;
+
+ // Border
+ if (_border != BORDER_NONE) {
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->rectangle(0, 0, allocation.get_width(), allocation.get_height());
+ cr->fill();
+ }
+
+ Gdk::Cairo::set_source_pixbuf(cr, pix, insetLeft, insetTop);
+ cr->paint();
+ }
+
+ if (_linked)
+ {
+ /* Draw arrow */
+ GdkRectangle possible = {insetLeft,
+ insetTop,
+ (allocation.get_width() - (insetLeft + insetRight)),
+ (allocation.get_height() - (insetTop + insetBottom))
+ };
+
+ GdkRectangle area = {possible.x,
+ possible.y,
+ possible.width / 2,
+ possible.height / 2 };
+
+ /* Make it square */
+ if ( area.width > area.height )
+ area.width = area.height;
+ if ( area.height > area.width )
+ area.height = area.width;
+
+ /* Center it horizontally */
+ if ( area.width < possible.width ) {
+ int diff = (possible.width - area.width) / 2;
+ area.x += diff;
+ }
+
+ if (_linked & PREVIEW_LINK_IN)
+ {
+ context->render_arrow(cr,
+ G_PI, // Down-pointing arrow
+ area.x, area.y,
+ std::min(area.width, area.height)
+ );
+ }
+
+ if (_linked & PREVIEW_LINK_OUT)
+ {
+ GdkRectangle otherArea = {area.x, area.y, area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height);
+ }
+
+ context->render_arrow(cr,
+ G_PI, // Down-pointing arrow
+ otherArea.x, otherArea.y,
+ std::min(otherArea.width, otherArea.height)
+ );
+ }
+
+ if (_linked & PREVIEW_LINK_OTHER)
+ {
+ GdkRectangle otherArea = {insetLeft, area.y, area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+
+ context->render_arrow(cr,
+ 1.5*G_PI, // Left-pointing arrow
+ otherArea.x, otherArea.y,
+ std::min(otherArea.width, otherArea.height)
+ );
+ }
+
+
+ if (_linked & PREVIEW_FILL)
+ {
+ GdkRectangle otherArea = {possible.x + ((possible.width / 4) - (area.width / 2)),
+ area.y,
+ area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+ context->render_check(cr,
+ otherArea.x, otherArea.y,
+ otherArea.width, otherArea.height );
+ }
+
+ if (_linked & PREVIEW_STROKE)
+ {
+ GdkRectangle otherArea = {possible.x + (((possible.width * 3) / 4) - (area.width / 2)),
+ area.y,
+ area.width, area.height};
+ if ( otherArea.height < possible.height ) {
+ otherArea.y = possible.y + (possible.height - otherArea.height) / 2;
+ }
+ // This should be a diamond too?
+ context->render_check(cr,
+ otherArea.x, otherArea.y,
+ otherArea.width, otherArea.height );
+ }
+ }
+
+
+ if ( has_focus() ) {
+ allocation = get_allocation();
+
+ context->render_focus(cr,
+ 0 + 1, 0 + 1,
+ allocation.get_width() - 2, allocation.get_height() - 2 );
+ }
+
+ return false;
+}
+
+
+bool
+Preview::on_enter_notify_event(GdkEventCrossing* event )
+{
+ _within = true;
+ set_state_flags(_hot ? Gtk::STATE_FLAG_ACTIVE : Gtk::STATE_FLAG_PRELIGHT, false);
+
+ return false;
+}
+
+bool
+Preview::on_leave_notify_event(GdkEventCrossing* event)
+{
+ _within = false;
+ set_state_flags(Gtk::STATE_FLAG_NORMAL, false);
+
+ return false;
+}
+
+bool
+Preview::on_button_press_event(GdkEventButton *event)
+{
+ if (_takesFocus && !has_focus() )
+ {
+ grab_focus();
+ }
+
+ if ( event->button == PRIME_BUTTON_MAGIC_NUMBER ||
+ event->button == 2 )
+ {
+ _hot = true;
+
+ if ( _within )
+ {
+ set_state_flags(Gtk::STATE_FLAG_ACTIVE, false);
+ }
+ }
+
+ return false;
+}
+
+bool
+Preview::on_button_release_event(GdkEventButton* event)
+{
+ _hot = false;
+ set_state_flags(Gtk::STATE_FLAG_NORMAL, false);
+
+ if (_within &&
+ (event->button == PRIME_BUTTON_MAGIC_NUMBER ||
+ event->button == 2))
+ {
+ gboolean isAlt = ( ((event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) ||
+ (event->button == 2));
+
+ if ( isAlt )
+ {
+ _signal_alt_clicked(2);
+ }
+ else
+ {
+ _signal_clicked.emit();
+ }
+ }
+
+ return false;
+}
+
+void
+Preview::set_linked(LinkType link)
+{
+ link = (LinkType)(link & PREVIEW_LINK_ALL);
+
+ if (link != _linked)
+ {
+ _linked = link;
+
+ queue_draw();
+ }
+}
+
+LinkType
+Preview::get_linked() const
+{
+ return (LinkType)_linked;
+}
+
+void
+Preview::set_details(ViewType view,
+ PreviewSize size,
+ guint ratio,
+ guint border)
+{
+ _view = view;
+
+ if ( size > PREVIEW_SIZE_LAST )
+ {
+ size = PREVIEW_SIZE_LAST;
+ }
+
+ _size = size;
+
+ if ( ratio > PREVIEW_MAX_RATIO )
+ {
+ ratio = PREVIEW_MAX_RATIO;
+ }
+
+ _ratio = ratio;
+ _border = border;
+
+ queue_draw();
+}
+
+Preview::Preview()
+ : _r(0x80),
+ _g(0x80),
+ _b(0xcc),
+ _scaledW(0),
+ _scaledH(0),
+ _hot(false),
+ _within(false),
+ _takesFocus(false),
+ _view(VIEW_TYPE_LIST),
+ _size(PREVIEW_SIZE_SMALL),
+ _ratio(100),
+ _border(BORDER_NONE),
+ _previewPixbuf(nullptr),
+ _scaled(nullptr),
+ _linked(PREVIEW_LINK_NONE)
+{
+ set_can_focus(true);
+ set_receives_default(true);
+
+ set_sensitive(true);
+
+ add_events(Gdk::BUTTON_PRESS_MASK
+ |Gdk::BUTTON_RELEASE_MASK
+ |Gdk::KEY_PRESS_MASK
+ |Gdk::KEY_RELEASE_MASK
+ |Gdk::FOCUS_CHANGE_MASK
+ |Gdk::ENTER_NOTIFY_MASK
+ |Gdk::LEAVE_NOTIFY_MASK );
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/preview.h b/src/ui/widget/preview.h
new file mode 100644
index 0000000..d352a0d
--- /dev/null
+++ b/src/ui/widget/preview.h
@@ -0,0 +1,165 @@
+// 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 Preview Stuffs.
+ *
+ * The Initial Developer of the Original Code is
+ * Jon A. Cruz.
+ * Portions created by the Initial Developer are Copyright (C) 2005-2008
+ * 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_EEK_PREVIEW_H
+#define SEEN_EEK_PREVIEW_H
+
+#include <gtkmm/drawingarea.h>
+
+/**
+ * @file
+ * Generic implementation of an object that can be shown by a preview.
+ */
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+enum PreviewStyle {
+ PREVIEW_STYLE_ICON = 0,
+ PREVIEW_STYLE_PREVIEW,
+ PREVIEW_STYLE_NAME,
+ PREVIEW_STYLE_BLURB,
+ PREVIEW_STYLE_ICON_NAME,
+ PREVIEW_STYLE_ICON_BLURB,
+ PREVIEW_STYLE_PREVIEW_NAME,
+ PREVIEW_STYLE_PREVIEW_BLURB
+};
+
+enum ViewType {
+ VIEW_TYPE_LIST = 0,
+ VIEW_TYPE_GRID
+};
+
+enum PreviewSize {
+ PREVIEW_SIZE_TINY = 0,
+ PREVIEW_SIZE_SMALL,
+ PREVIEW_SIZE_MEDIUM,
+ PREVIEW_SIZE_BIG,
+ PREVIEW_SIZE_BIGGER,
+ PREVIEW_SIZE_HUGE
+};
+
+enum LinkType {
+ PREVIEW_LINK_NONE = 0,
+ PREVIEW_LINK_IN = 1,
+ PREVIEW_LINK_OUT = 2,
+ PREVIEW_LINK_OTHER = 4,
+ PREVIEW_FILL = 8,
+ PREVIEW_STROKE = 16,
+ PREVIEW_LINK_ALL = 31
+};
+
+enum BorderStyle {
+ BORDER_NONE = 0,
+ BORDER_SOLID,
+ BORDER_WIDE,
+ BORDER_SOLID_LAST_ROW,
+};
+
+class Preview : public Gtk::DrawingArea {
+private:
+ int _scaledW;
+ int _scaledH;
+
+ int _r;
+ int _g;
+ int _b;
+
+ bool _hot;
+ bool _within;
+ bool _takesFocus; ///< flag to grab focus when clicked
+ ViewType _view;
+ PreviewSize _size;
+ unsigned int _ratio;
+ LinkType _linked;
+ unsigned int _border;
+ bool _freesize = false;
+
+ Glib::RefPtr<Gdk::Pixbuf> _previewPixbuf;
+ Glib::RefPtr<Gdk::Pixbuf> _scaled;
+
+ // signals
+ sigc::signal<void> _signal_clicked;
+ sigc::signal<void, int> _signal_alt_clicked;
+
+ void size_request(GtkRequisition *req) const;
+
+protected:
+ void get_preferred_width_vfunc(int &minimal_width, int &natural_width) const override;
+ void get_preferred_height_vfunc(int &minimal_height, int &natural_height) const override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+ bool on_button_press_event(GdkEventButton *button_event) override;
+ bool on_button_release_event(GdkEventButton *button_event) override;
+ bool on_enter_notify_event(GdkEventCrossing* event ) override;
+ bool on_leave_notify_event(GdkEventCrossing* event ) override;
+
+public:
+ Preview();
+ bool get_focus_on_click() const {return _takesFocus;}
+ void set_focus_on_click(bool focus_on_click) {_takesFocus = focus_on_click;}
+ LinkType get_linked() const;
+ void set_linked(LinkType link);
+ void set_details(ViewType view,
+ PreviewSize size,
+ guint ratio,
+ guint border);
+ void set_color(int r, int g, int b);
+ void set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf);
+ void set_freesize(bool enable);
+ static void set_size_mappings(guint count, GtkIconSize const* sizes);
+
+ decltype(_signal_clicked) signal_clicked() {return _signal_clicked;}
+ decltype(_signal_alt_clicked) signal_alt_clicked() {return _signal_alt_clicked;}
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* SEEN_EEK_PREVIEW_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/ui/widget/random.cpp b/src/ui/widget/random.cpp
new file mode 100644
index 0000000..495a778
--- /dev/null
+++ b/src/ui/widget/random.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "random.h"
+#include "ui/icon-loader.h"
+#include <glibmm/i18n.h>
+
+#include <gtkmm/button.h>
+#include <gtkmm/image.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, digits, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, adjust, digits, suffix, icon, mnemonic)
+{
+ startseed = 0;
+ addReseedButton();
+}
+
+long Random::getStartSeed() const
+{
+ return startseed;
+}
+
+void Random::setStartSeed(long newseed)
+{
+ startseed = newseed;
+}
+
+void Random::addReseedButton()
+{
+ Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("randomize", Gtk::ICON_SIZE_BUTTON));
+ Gtk::Button * pButton = Gtk::manage(new Gtk::Button());
+ pButton->set_relief(Gtk::RELIEF_NONE);
+ pIcon->show();
+ pButton->add(*pIcon);
+ pButton->show();
+ pButton->signal_clicked().connect(sigc::mem_fun(*this, &Random::onReseedButtonClick));
+ pButton->set_tooltip_text(_("Reseed the random number generator; this creates a different sequence of random numbers."));
+
+ pack_start(*pButton, Gtk::PACK_SHRINK, 0);
+}
+
+void
+Random::onReseedButtonClick()
+{
+ startseed = g_random_int();
+ signal_reseeded.emit();
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/random.h b/src/ui/widget/random.h
new file mode 100644
index 0000000..2648cb2
--- /dev/null
+++ b/src/ui/widget/random.h
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_RANDOM_H
+#define INKSCAPE_UI_WIDGET_RANDOM_H
+
+#include "scalar.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional
+ * icon or suffix, for entering arbitrary number values. It adds an extra
+ * number called "startseed", that is not UI edittable, but should be put in SVG.
+ * This does NOT generate a random number, but provides merely the saving of
+ * the startseed value.
+ */
+class Random : public Scalar
+{
+public:
+
+ /**
+ * Construct a Random scalar Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Random Scalar Widget.
+ *
+ * @param label Label.
+ * @param digits Number of decimal digits to display.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Random Scalar Widget.
+ *
+ * @param label Label.
+ * @param adjust Adjustment to use for the SpinButton.
+ * @param digits Number of decimal digits to display (defaults to 0).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Random(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits = 0,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Gets the startseed.
+ */
+ long getStartSeed() const;
+
+ /**
+ * Sets the startseed number.
+ */
+ void setStartSeed(long newseed);
+
+ sigc::signal <void> signal_reseeded;
+
+protected:
+ long startseed;
+
+private:
+
+ /**
+ * Add reseed button to the widget.
+ */
+ void addReseedButton();
+
+ void onReseedButtonClick();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_RANDOM_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/ui/widget/registered-enums.h b/src/ui/widget/registered-enums.h
new file mode 100644
index 0000000..b0cc199
--- /dev/null
+++ b/src/ui/widget/registered-enums.h
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H
+#define INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H
+
+#include "ui/widget/combo-enums.h"
+#include "ui/widget/registered-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Simplified management of enumerations in the UI as combobox.
+ */
+template<typename E> class RegisteredEnum : public RegisteredWidget< LabelledComboBoxEnum<E> >
+{
+public:
+ ~RegisteredEnum() override {
+ _changed_connection.disconnect();
+ }
+
+ RegisteredEnum ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ const Util::EnumDataConverter<E>& c,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr,
+ bool sorted = true )
+ : RegisteredWidget< LabelledComboBoxEnum<E> >(label, tip, c, (const Glib::ustring &)"", (const Glib::ustring &)"", true, sorted)
+ {
+ RegisteredWidget< LabelledComboBoxEnum<E> >::init_parent(key, wr, repr_in, doc_in);
+ _changed_connection = combobox()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredEnum::on_changed));
+ }
+
+ void set_active_by_id (E id) {
+ combobox()->set_active_by_id(id);
+ };
+
+ void set_active_by_key (const Glib::ustring& key) {
+ combobox()->set_active_by_key(key);
+ }
+
+ inline const Util::EnumData<E>* get_active_data() {
+ combobox()->get_active_data();
+ }
+
+ ComboBoxEnum<E> * combobox() {
+ return LabelledComboBoxEnum<E>::getCombobox();
+ }
+
+ sigc::connection _changed_connection;
+
+protected:
+ void on_changed() {
+ if (combobox()->setProgrammatically) {
+ combobox()->setProgrammatically = false;
+ return;
+ }
+
+ if (RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->isUpdating())
+ return;
+
+ RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (true);
+
+ const Util::EnumData<E>* data = combobox()->get_active_data();
+ if (data) {
+ RegisteredWidget< LabelledComboBoxEnum<E> >::write_to_xml(data->key.c_str());
+ }
+
+ RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (false);
+ }
+};
+
+}
+}
+}
+
+#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/ui/widget/registered-widget.cpp b/src/ui/widget/registered-widget.cpp
new file mode 100644
index 0000000..aaea668
--- /dev/null
+++ b/src/ui/widget/registered-widget.cpp
@@ -0,0 +1,843 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * bulia byak <buliabyak@users.sf.net>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon Phillips <jon@rejon.org>
+ * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm)
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2000 - 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "registered-widget.h"
+
+#include <gtkmm/radiobutton.h>
+
+#include "object/sp-root.h"
+
+#include "svg/svg-color.h"
+#include "svg/stringstream.h"
+
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*#########################################
+ * Registered CHECKBUTTON
+ */
+
+RegisteredCheckButton::~RegisteredCheckButton()
+{
+ _toggled_connection.disconnect();
+}
+
+RegisteredCheckButton::RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *active_str, char const *inactive_str)
+ : RegisteredWidget<Gtk::CheckButton>()
+ , _active_str(active_str)
+ , _inactive_str(inactive_str)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ set_tooltip_text (tip);
+ Gtk::Label *l = new Gtk::Label();
+ l->set_markup(label);
+ l->set_use_underline (true);
+ add (*manage (l));
+
+ if(right) set_halign(Gtk::ALIGN_END);
+ else set_halign(Gtk::ALIGN_START);
+
+ set_valign(Gtk::ALIGN_CENTER);
+ _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredCheckButton::on_toggled));
+}
+
+void
+RegisteredCheckButton::setActive (bool b)
+{
+ setProgrammatically = true;
+ set_active (b);
+ //The slave button is greyed out if the master button is unchecked
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(b);
+ }
+ setProgrammatically = false;
+}
+
+void
+RegisteredCheckButton::on_toggled()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+ _wr->setUpdating (true);
+
+ write_to_xml(get_active() ? _active_str : _inactive_str);
+ //The slave button is greyed out if the master button is unchecked
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(get_active());
+ }
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered TOGGLEBUTTON
+ */
+
+RegisteredToggleButton::~RegisteredToggleButton()
+{
+ _toggled_connection.disconnect();
+}
+
+RegisteredToggleButton::RegisteredToggleButton (const Glib::ustring& /*label*/, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *icon_active, char const *icon_inactive)
+ : RegisteredWidget<Gtk::ToggleButton>()
+{
+ init_parent(key, wr, repr_in, doc_in);
+ setProgrammatically = false;
+ set_tooltip_text (tip);
+
+ if(right) set_halign(Gtk::ALIGN_END);
+ else set_halign(Gtk::ALIGN_START);
+
+ set_valign(Gtk::ALIGN_CENTER);
+ _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredToggleButton::on_toggled));
+}
+
+void
+RegisteredToggleButton::setActive (bool b)
+{
+ setProgrammatically = true;
+ set_active (b);
+ //The slave button is greyed out if the master button is untoggled
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(b);
+ }
+ setProgrammatically = false;
+}
+
+void
+RegisteredToggleButton::on_toggled()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+ _wr->setUpdating (true);
+
+ write_to_xml(get_active() ? "true" : "false");
+ //The slave button is greyed out if the master button is untoggled
+ for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) {
+ (*i)->set_sensitive(get_active());
+ }
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered UNITMENU
+ */
+
+RegisteredUnitMenu::~RegisteredUnitMenu()
+{
+ _changed_connection.disconnect();
+}
+
+RegisteredUnitMenu::RegisteredUnitMenu (const Glib::ustring& label, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ : RegisteredWidget<Labelled> (label, "" /*tooltip*/, new UnitMenu())
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ getUnitMenu()->setUnitType (UNIT_TYPE_LINEAR);
+ _changed_connection = getUnitMenu()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredUnitMenu::on_changed));
+}
+
+void
+RegisteredUnitMenu::setUnit (Glib::ustring unit)
+{
+ getUnitMenu()->setUnit(unit);
+}
+
+void
+RegisteredUnitMenu::on_changed()
+{
+ if (_wr->isUpdating())
+ return;
+
+ Inkscape::SVGOStringStream os;
+ os << getUnitMenu()->getUnitAbbr();
+
+ _wr->setUpdating (true);
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered SCALARUNIT
+ */
+
+RegisteredScalarUnit::~RegisteredScalarUnit()
+{
+ _value_changed_connection.disconnect();
+}
+
+RegisteredScalarUnit::RegisteredScalarUnit (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, const RegisteredUnitMenu &rum, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in, RSU_UserUnits user_units)
+ : RegisteredWidget<ScalarUnit>(label, tip, UNIT_TYPE_LINEAR, "", "", rum.getUnitMenu()),
+ _um(nullptr)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ initScalar (-1e6, 1e6);
+ setUnit (rum.getUnitMenu()->getUnitAbbr());
+ setDigits (2);
+ _um = rum.getUnitMenu();
+ _user_units = user_units;
+ _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalarUnit::on_value_changed));
+}
+
+
+void
+RegisteredScalarUnit::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ if (_user_units != RSU_none) {
+ // Output length in 'user units', taking into account scale in 'x' or 'y'.
+ double scale = 1.0;
+ if (doc) {
+ SPRoot *root = doc->getRoot();
+ if (root->viewBox_set) {
+ // check to see if scaling is uniform
+ if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) {
+ scale = (root->viewBox.width() / root->width.computed + root->viewBox.height() / root->height.computed)/2.0;
+ } else if (_user_units == RSU_x) {
+ scale = root->viewBox.width() / root->width.computed;
+ } else {
+ scale = root->viewBox.height() / root->height.computed;
+ }
+ }
+ }
+ os << getValue("px") * scale;
+ } else {
+ // Output using unit identifiers.
+ os << getValue("");
+ if (_um)
+ os << _um->getUnitAbbr();
+ }
+
+ write_to_xml(os.str().c_str());
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered SCALAR
+ */
+
+RegisteredScalar::~RegisteredScalar()
+{
+ _value_changed_connection.disconnect();
+}
+
+RegisteredScalar::RegisteredScalar ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument * doc_in )
+ : RegisteredWidget<Scalar>(label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalar::on_value_changed));
+}
+
+void
+RegisteredScalar::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+ if (_wr->isUpdating()) {
+ return;
+ }
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ //Force exact 0 if decimals over to 6
+ double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue();
+ os << val;
+ //TODO: Test is ok remove this sensitives
+ //also removed in registered text and in registered random
+ //set_sensitive(false);
+ write_to_xml(os.str().c_str());
+ //set_sensitive(true);
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered TEXT
+ */
+
+RegisteredText::~RegisteredText()
+{
+ _activate_connection.disconnect();
+}
+
+RegisteredText::RegisteredText ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument * doc_in )
+ : RegisteredWidget<Text>(label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+ _activate_connection = signal_activate().connect (sigc::mem_fun (*this, &RegisteredText::on_activate));
+}
+
+void
+RegisteredText::on_activate()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating()) {
+ return;
+ }
+ _wr->setUpdating (true);
+ Glib::ustring str(getText());
+ Inkscape::SVGOStringStream os;
+ os << str;
+ write_to_xml(os.str().c_str());
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered COLORPICKER
+ */
+
+RegisteredColorPicker::RegisteredColorPicker(const Glib::ustring& label,
+ const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const Glib::ustring& ckey,
+ const Glib::ustring& akey,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in,
+ SPDocument *doc_in)
+ : RegisteredWidget<LabelledColorPicker> (label, title, tip, 0, true)
+{
+ init_parent("", wr, repr_in, doc_in);
+
+ _ckey = ckey;
+ _akey = akey;
+ _changed_connection = connectChanged (sigc::mem_fun (*this, &RegisteredColorPicker::on_changed));
+}
+
+RegisteredColorPicker::~RegisteredColorPicker()
+{
+ _changed_connection.disconnect();
+}
+
+void
+RegisteredColorPicker::setRgba32 (guint32 rgba)
+{
+ LabelledColorPicker::setRgba32 (rgba);
+}
+
+void
+RegisteredColorPicker::closeWindow()
+{
+ LabelledColorPicker::closeWindow();
+}
+
+void
+RegisteredColorPicker::on_changed (guint32 rgba)
+{
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ // Use local repr here. When repr is specified, use that one, but
+ // if repr==NULL, get the repr of namedview of active desktop.
+ Inkscape::XML::Node *local_repr = repr;
+ SPDocument *local_doc = doc;
+ if (!local_repr) {
+ SPDesktop *dt = _wr->desktop();
+ if (!dt) {
+ _wr->setUpdating(false);
+ return;
+ }
+ local_repr = dt->getNamedView()->getRepr();
+ local_doc = dt->getDocument();
+ }
+ gchar c[32];
+ if (_akey == _ckey + "_opacity_LPE") { //For LPE parameter we want stored with alpha
+ sprintf(c, "#%08x", rgba);
+ } else {
+ sp_svg_write_color(c, sizeof(c), rgba);
+ }
+ bool saved = DocumentUndo::getUndoSensitive(local_doc);
+ DocumentUndo::setUndoSensitive(local_doc, false);
+ local_repr->setAttribute(_ckey, c);
+ local_repr->setAttributeCssDouble(_akey.c_str(), (rgba & 0xff) / 255.0);
+ DocumentUndo::setUndoSensitive(local_doc, saved);
+
+ local_doc->setModifiedSinceSave();
+ DocumentUndo::done(local_doc, "registered-widget.cpp: RegisteredColorPicker::on_changed", ""); // TODO Fix description.
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered SUFFIXEDINTEGER
+ */
+
+RegisteredSuffixedInteger::~RegisteredSuffixedInteger()
+{
+ _changed_connection.disconnect();
+}
+
+RegisteredSuffixedInteger::RegisteredSuffixedInteger (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& suffix, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ : RegisteredWidget<Scalar>(label, tip, 0, suffix),
+ setProgrammatically(false)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setRange (0, 1e6);
+ setDigits (0);
+ setIncrements(1, 10);
+
+ _changed_connection = signal_value_changed().connect (sigc::mem_fun(*this, &RegisteredSuffixedInteger::on_value_changed));
+}
+
+void
+RegisteredSuffixedInteger::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ os << getValue();
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered RADIOBUTTONPAIR
+ */
+
+RegisteredRadioButtonPair::~RegisteredRadioButtonPair()
+{
+ _changed_connection.disconnect();
+}
+
+RegisteredRadioButtonPair::RegisteredRadioButtonPair (const Glib::ustring& label,
+ const Glib::ustring& label1, const Glib::ustring& label2,
+ const Glib::ustring& tip1, const Glib::ustring& tip2,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ : RegisteredWidget<Gtk::Box>(),
+ _rb1(nullptr),
+ _rb2(nullptr)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+
+ set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ add(*Gtk::manage(new Gtk::Label(label)));
+ _rb1 = Gtk::manage(new Gtk::RadioButton(label1, true));
+ add (*_rb1);
+ Gtk::RadioButtonGroup group = _rb1->get_group();
+ _rb2 = Gtk::manage(new Gtk::RadioButton(group, label2, true));
+ add (*_rb2);
+ _rb2->set_active();
+ _rb1->set_tooltip_text(tip1);
+ _rb2->set_tooltip_text(tip2);
+ _changed_connection = _rb1->signal_toggled().connect (sigc::mem_fun (*this, &RegisteredRadioButtonPair::on_value_changed));
+}
+
+void
+RegisteredRadioButtonPair::setValue (bool second)
+{
+ if (!_rb1 || !_rb2)
+ return;
+
+ setProgrammatically = true;
+ if (second) {
+ _rb2->set_active();
+ } else {
+ _rb1->set_active();
+ }
+}
+
+void
+RegisteredRadioButtonPair::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ bool second = _rb2->get_active();
+ write_to_xml(second ? "true" : "false");
+
+ _wr->setUpdating (false);
+}
+
+
+/*#########################################
+ * Registered POINT
+ */
+
+RegisteredPoint::~RegisteredPoint()
+{
+ _value_x_changed_connection.disconnect();
+ _value_y_changed_connection.disconnect();
+}
+
+RegisteredPoint::RegisteredPoint ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument* doc_in )
+ : RegisteredWidget<Point> (label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredPoint::on_value_changed));
+ _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredPoint::on_value_changed));
+}
+
+void
+RegisteredPoint::on_value_changed()
+{
+ if (setProgrammatically()) {
+ clearProgrammatically();
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ os << getXValue() << "," << getYValue();
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered TRANSFORMEDPOINT
+ */
+
+RegisteredTransformedPoint::~RegisteredTransformedPoint()
+{
+ _value_x_changed_connection.disconnect();
+ _value_y_changed_connection.disconnect();
+}
+
+RegisteredTransformedPoint::RegisteredTransformedPoint ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument* doc_in )
+ : RegisteredWidget<Point> (label, tip),
+ to_svg(Geom::identity())
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredTransformedPoint::on_value_changed));
+ _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredTransformedPoint::on_value_changed));
+}
+
+void
+RegisteredTransformedPoint::setValue(Geom::Point const & p)
+{
+ Geom::Point new_p = p * to_svg.inverse();
+ Point::setValue(new_p); // the Point widget should display things in canvas coordinates
+}
+
+void
+RegisteredTransformedPoint::setTransform(Geom::Affine const & canvas_to_svg)
+{
+ // check if matrix is singular / has inverse
+ if ( ! canvas_to_svg.isSingular() ) {
+ to_svg = canvas_to_svg;
+ } else {
+ // set back to default
+ to_svg = Geom::identity();
+ }
+}
+
+void
+RegisteredTransformedPoint::on_value_changed()
+{
+ if (setProgrammatically()) {
+ clearProgrammatically();
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Geom::Point pos = getValue() * to_svg;
+
+ Inkscape::SVGOStringStream os;
+ os << pos;
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered TRANSFORMEDPOINT
+ */
+
+RegisteredVector::~RegisteredVector()
+{
+ _value_x_changed_connection.disconnect();
+ _value_y_changed_connection.disconnect();
+}
+
+RegisteredVector::RegisteredVector ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument* doc_in )
+ : RegisteredWidget<Point> (label, tip),
+ _polar_coords(false)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed));
+ _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed));
+}
+
+void
+RegisteredVector::setValue(Geom::Point const & p)
+{
+ if (!_polar_coords) {
+ Point::setValue(p);
+ } else {
+ Geom::Point polar;
+ polar[Geom::X] = atan2(p) *180/M_PI;
+ polar[Geom::Y] = p.length();
+ Point::setValue(polar);
+ }
+}
+
+void
+RegisteredVector::setValue(Geom::Point const & p, Geom::Point const & origin)
+{
+ RegisteredVector::setValue(p);
+ _origin = origin;
+}
+
+void RegisteredVector::setPolarCoords(bool polar_coords)
+{
+ _polar_coords = polar_coords;
+ if (polar_coords) {
+ xwidget.setLabelText(_("Angle:"));
+ ywidget.setLabelText(_("Distance:"));
+ } else {
+ xwidget.setLabelText(_("X:"));
+ ywidget.setLabelText(_("Y:"));
+ }
+}
+
+void
+RegisteredVector::on_value_changed()
+{
+ if (setProgrammatically()) {
+ clearProgrammatically();
+ return;
+ }
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Geom::Point origin = _origin;
+ Geom::Point vector = getValue();
+ if (_polar_coords) {
+ vector = Geom::Point::polar(vector[Geom::X]*M_PI/180, vector[Geom::Y]);
+ }
+
+ Inkscape::SVGOStringStream os;
+ os << origin << " , " << vector;
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered RANDOM
+ */
+
+RegisteredRandom::~RegisteredRandom()
+{
+ _value_changed_connection.disconnect();
+ _reseeded_connection.disconnect();
+}
+
+RegisteredRandom::RegisteredRandom ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument * doc_in )
+ : RegisteredWidget<Random> (label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+
+ setProgrammatically = false;
+ setRange (-1e6, 1e6);
+ setDigits (2);
+ setIncrements(0.1, 1.0);
+ _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredRandom::on_value_changed));
+ _reseeded_connection = signal_reseeded.connect(sigc::mem_fun(*this, &RegisteredRandom::on_value_changed));
+}
+
+void
+RegisteredRandom::setValue (double val, long startseed)
+{
+ Scalar::setValue (val);
+ setStartSeed(startseed);
+}
+
+void
+RegisteredRandom::on_value_changed()
+{
+ if (setProgrammatically) {
+ setProgrammatically = false;
+ return;
+ }
+
+ if (_wr->isUpdating()) {
+ return;
+ }
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ //Force exact 0 if decimals over to 6
+ double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue();
+ os << val << ';' << getStartSeed();
+ write_to_xml(os.str().c_str());
+ _wr->setUpdating (false);
+}
+
+/*#########################################
+ * Registered FONT-BUTTON
+ */
+
+RegisteredFontButton::~RegisteredFontButton()
+{
+ _signal_font_set.disconnect();
+}
+
+RegisteredFontButton::RegisteredFontButton ( const Glib::ustring& label, const Glib::ustring& tip,
+ const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in,
+ SPDocument* doc_in )
+ : RegisteredWidget<FontButton>(label, tip)
+{
+ init_parent(key, wr, repr_in, doc_in);
+ _signal_font_set = signal_font_value_changed().connect (sigc::mem_fun (*this, &RegisteredFontButton::on_value_changed));
+}
+
+void
+RegisteredFontButton::setValue (Glib::ustring fontspec)
+{
+ FontButton::setValue(fontspec);
+}
+
+void
+RegisteredFontButton::on_value_changed()
+{
+
+ if (_wr->isUpdating())
+ return;
+
+ _wr->setUpdating (true);
+
+ Inkscape::SVGOStringStream os;
+ os << getValue();
+
+ write_to_xml(os.str().c_str());
+
+ _wr->setUpdating (false);
+}
+
+} // namespace Dialog
+} // namespace UI
+} // 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/ui/widget/registered-widget.h b/src/ui/widget/registered-widget.h
new file mode 100644
index 0000000..f843540
--- /dev/null
+++ b/src/ui/widget/registered-widget.h
@@ -0,0 +1,455 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Johan Engelen <j.b.c.engelen@utwente.nl>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2005-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ *
+ * Used by Live Path Effects (see src/live_effects/parameter/) and Document Properties dialog.
+ *
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_
+#define INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_
+
+#include <2geom/affine.h>
+#include "xml/node.h"
+#include "registry.h"
+
+#include "ui/widget/scalar.h"
+#include "ui/widget/scalar-unit.h"
+#include "ui/widget/point.h"
+#include "ui/widget/text.h"
+#include "ui/widget/random.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/widget/font-button.h"
+#include "ui/widget/color-picker.h"
+#include "inkscape.h"
+
+#include "document.h"
+#include "document-undo.h"
+#include "desktop.h"
+#include "object/sp-namedview.h"
+
+#include <gtkmm/checkbutton.h>
+
+class SPDocument;
+
+namespace Gtk {
+ class HScale;
+ class RadioButton;
+ class SpinButton;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry;
+
+template <class W>
+class RegisteredWidget : public W {
+public:
+ void set_undo_parameters(Glib::ustring _event_description, Glib::ustring _icon_name)
+ {
+ icon_name = _icon_name;
+ event_description = _event_description;
+ write_undo = true;
+ }
+ void set_xml_target(Inkscape::XML::Node *xml_node, SPDocument *document)
+ {
+ repr = xml_node;
+ doc = document;
+ }
+
+ bool is_updating() {if (_wr) return _wr->isUpdating(); else return false;}
+
+protected:
+ RegisteredWidget() : W() {}
+ template< typename A >
+ explicit RegisteredWidget( A& a ): W( a ) {}
+ template< typename A, typename B >
+ RegisteredWidget( A& a, B& b ): W( a, b ) {}
+ template< typename A, typename B, typename C >
+ RegisteredWidget( A& a, B& b, C* c ): W( a, b, c ) {}
+ template< typename A, typename B, typename C >
+ RegisteredWidget( A& a, B& b, C& c ): W( a, b, c ) {}
+ template< typename A, typename B, typename C, typename D >
+ RegisteredWidget( A& a, B& b, C c, D d ): W( a, b, c, d ) {}
+ template< typename A, typename B, typename C, typename D, typename E >
+ RegisteredWidget( A& a, B& b, C& c, D d, E e ): W( a, b, c, d, e ) {}
+ template< typename A, typename B, typename C, typename D, typename E , typename F>
+ RegisteredWidget( A& a, B& b, C c, D& d, E& e, F* f): W( a, b, c, d, e, f) {}
+ template< typename A, typename B, typename C, typename D, typename E , typename F, typename G>
+ RegisteredWidget( A& a, B& b, C& c, D& d, E& e, F f, G& g): W( a, b, c, d, e, f, g) {}
+
+ ~RegisteredWidget() override = default;;
+
+ void init_parent(const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in)
+ {
+ _wr = &wr;
+ _key = key;
+ repr = repr_in;
+ doc = doc_in;
+ if (repr && !doc) // doc cannot be NULL when repr is not NULL
+ g_warning("Initialization of registered widget using defined repr but with doc==NULL");
+ }
+
+ void write_to_xml(const char * svgstr)
+ {
+ // Use local repr here. When repr is specified, use that one, but
+ // if repr==NULL, get the repr of namedview of active desktop.
+ Inkscape::XML::Node *local_repr = repr;
+ SPDocument *local_doc = doc;
+ if (!local_repr) {
+ SPDesktop* dt = _wr->desktop();
+ if (!dt) {
+ return;
+ }
+ local_repr = reinterpret_cast<SPObject *>(dt->getNamedView())->getRepr();
+ local_doc = dt->getDocument();
+ }
+
+ bool saved = DocumentUndo::getUndoSensitive(local_doc);
+ DocumentUndo::setUndoSensitive(local_doc, false);
+ const char * svgstr_old = local_repr->attribute(_key.c_str());
+ if (!write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ }
+ DocumentUndo::setUndoSensitive(local_doc, saved);
+ if (svgstr_old && svgstr && strcmp(svgstr_old,svgstr)) {
+ local_doc->setModifiedSinceSave();
+ }
+
+ if (write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ DocumentUndo::done(local_doc, event_description, icon_name);
+ }
+ }
+
+ Registry * _wr = nullptr;
+ Glib::ustring _key;
+ Inkscape::XML::Node * repr = nullptr;
+ SPDocument * doc = nullptr;
+ Glib::ustring event_description;
+ Glib::ustring icon_name; // Used by History dialog.
+ bool write_undo = false;
+};
+
+//#######################################################
+
+class RegisteredCheckButton : public RegisteredWidget<Gtk::CheckButton> {
+public:
+ ~RegisteredCheckButton() override;
+ RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=false, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *active_str = "true", char const *inactive_str = "false");
+
+ void setActive (bool);
+
+ std::list<Gtk::Widget*> _slavewidgets;
+
+ // a slave button is only sensitive when the master button is active
+ // i.e. a slave button is greyed-out when the master button is not checked
+
+ void setSlaveWidgets(std::list<Gtk::Widget*> const &btns) {
+ _slavewidgets = btns;
+ }
+
+ bool setProgrammatically; // true if the value was set by setActive, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+
+protected:
+ char const *_active_str, *_inactive_str;
+ sigc::connection _toggled_connection;
+ void on_toggled() override;
+};
+
+class RegisteredToggleButton : public RegisteredWidget<Gtk::ToggleButton> {
+public:
+ ~RegisteredToggleButton() override;
+ RegisteredToggleButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=true, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *icon_active = "true", char const *icon_inactive = "false");
+
+ void setActive (bool);
+
+ std::list<Gtk::Widget*> _slavewidgets;
+
+ // a slave button is only sensitive when the master button is active
+ // i.e. a slave button is greyed-out when the master button is not checked
+
+ void setSlaveWidgets(std::list<Gtk::Widget*> const &btns) {
+ _slavewidgets = btns;
+ }
+
+ bool setProgrammatically; // true if the value was set by setActive, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+
+protected:
+ sigc::connection _toggled_connection;
+ void on_toggled() override;
+};
+
+class RegisteredUnitMenu : public RegisteredWidget<Labelled> {
+public:
+ ~RegisteredUnitMenu() override;
+ RegisteredUnitMenu ( const Glib::ustring& label,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ void setUnit (const Glib::ustring);
+ Unit const * getUnit() const { return static_cast<UnitMenu*>(_widget)->getUnit(); };
+ UnitMenu* getUnitMenu() const { return static_cast<UnitMenu*>(_widget); };
+ sigc::connection _changed_connection;
+
+protected:
+ void on_changed();
+};
+
+// Allow RegisteredScalarUnit to output lengths in 'user units' (which may have direction dependent
+// scale factors).
+enum RSU_UserUnits {
+ RSU_none,
+ RSU_x,
+ RSU_y
+};
+
+class RegisteredScalarUnit : public RegisteredWidget<ScalarUnit> {
+public:
+ ~RegisteredScalarUnit() override;
+ RegisteredScalarUnit ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ const RegisteredUnitMenu &rum,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr,
+ RSU_UserUnits _user_units = RSU_none );
+
+protected:
+ sigc::connection _value_changed_connection;
+ UnitMenu *_um;
+ void on_value_changed();
+ RSU_UserUnits _user_units;
+};
+
+class RegisteredScalar : public RegisteredWidget<Scalar> {
+public:
+ ~RegisteredScalar() override;
+ RegisteredScalar (const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+protected:
+ sigc::connection _value_changed_connection;
+ void on_value_changed();
+};
+
+class RegisteredText : public RegisteredWidget<Text> {
+public:
+ ~RegisteredText() override;
+ RegisteredText (const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+protected:
+ sigc::connection _activate_connection;
+ void on_activate();
+};
+
+class RegisteredColorPicker : public RegisteredWidget<LabelledColorPicker> {
+public:
+ ~RegisteredColorPicker() override;
+
+ RegisteredColorPicker (const Glib::ustring& label,
+ const Glib::ustring& title,
+ const Glib::ustring& tip,
+ const Glib::ustring& ckey,
+ const Glib::ustring& akey,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr);
+
+ void setRgba32 (guint32);
+ void closeWindow();
+
+protected:
+ Glib::ustring _ckey, _akey;
+ void on_changed (guint32);
+ sigc::connection _changed_connection;
+};
+
+class RegisteredSuffixedInteger : public RegisteredWidget<Scalar> {
+public:
+ ~RegisteredSuffixedInteger() override;
+ RegisteredSuffixedInteger ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& suffix,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ bool setProgrammatically; // true if the value was set by setValue, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+
+protected:
+ sigc::connection _changed_connection;
+ void on_value_changed();
+};
+
+class RegisteredRadioButtonPair : public RegisteredWidget<Gtk::Box> {
+public:
+ ~RegisteredRadioButtonPair() override;
+ RegisteredRadioButtonPair ( const Glib::ustring& label,
+ const Glib::ustring& label1,
+ const Glib::ustring& label2,
+ const Glib::ustring& tip1,
+ const Glib::ustring& tip2,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ void setValue (bool second);
+
+ bool setProgrammatically; // true if the value was set by setValue, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+protected:
+ Gtk::RadioButton *_rb1, *_rb2;
+ sigc::connection _changed_connection;
+ void on_value_changed();
+};
+
+class RegisteredPoint : public RegisteredWidget<Point> {
+public:
+ ~RegisteredPoint() override;
+ RegisteredPoint ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+protected:
+ sigc::connection _value_x_changed_connection;
+ sigc::connection _value_y_changed_connection;
+ void on_value_changed();
+};
+
+
+class RegisteredTransformedPoint : public RegisteredWidget<Point> {
+public:
+ ~RegisteredTransformedPoint() override;
+ RegisteredTransformedPoint ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ // redefine setValue, because transform must be applied
+ void setValue(Geom::Point const & p);
+
+ void setTransform(Geom::Affine const & canvas_to_svg);
+
+protected:
+ sigc::connection _value_x_changed_connection;
+ sigc::connection _value_y_changed_connection;
+ void on_value_changed();
+
+ Geom::Affine to_svg;
+};
+
+
+class RegisteredVector : public RegisteredWidget<Point> {
+public:
+ ~RegisteredVector() override;
+ RegisteredVector (const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr );
+
+ // redefine setValue, because transform must be applied
+ void setValue(Geom::Point const & p);
+ void setValue(Geom::Point const & p, Geom::Point const & origin);
+
+ /**
+ * Changes the widgets text to polar coordinates. The SVG output will still be a normal cartesian vector.
+ * Careful: when calling getValue(), the return value's X-coord will be the angle, Y-value will be the distance/length.
+ * After changing the coords type (polar/non-polar), the value has to be reset (setValue).
+ */
+ void setPolarCoords(bool polar_coords = true);
+
+protected:
+ sigc::connection _value_x_changed_connection;
+ sigc::connection _value_y_changed_connection;
+ void on_value_changed();
+
+ Geom::Point _origin;
+ bool _polar_coords;
+};
+
+
+class RegisteredRandom : public RegisteredWidget<Random> {
+public:
+ ~RegisteredRandom() override;
+ RegisteredRandom ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr);
+
+ void setValue (double val, long startseed);
+
+protected:
+ sigc::connection _value_changed_connection;
+ sigc::connection _reseeded_connection;
+ void on_value_changed();
+};
+
+class RegisteredFontButton : public RegisteredWidget<FontButton> {
+public:
+ ~RegisteredFontButton() override;
+ RegisteredFontButton ( const Glib::ustring& label,
+ const Glib::ustring& tip,
+ const Glib::ustring& key,
+ Registry& wr,
+ Inkscape::XML::Node* repr_in = nullptr,
+ SPDocument *doc_in = nullptr);
+
+ void setValue (Glib::ustring fontspec);
+
+protected:
+ sigc::connection _signal_font_set;
+ void on_value_changed();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_REGISTERED_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:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/registry.cpp b/src/ui/widget/registry.cpp
new file mode 100644
index 0000000..4bc8e00
--- /dev/null
+++ b/src/ui/widget/registry.cpp
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "registry.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+//===================================================
+
+//---------------------------------------------------
+
+Registry::Registry() : _updating(false) {}
+
+Registry::~Registry() = default;
+
+bool
+Registry::isUpdating()
+{
+ return _updating;
+}
+
+void
+Registry::setUpdating (bool upd)
+{
+ _updating = upd;
+}
+
+void Registry::setDesktop(SPDesktop *desktop)
+{ //
+ _desktop = desktop;
+}
+
+//====================================================
+
+
+} // namespace Dialog
+} // namespace UI
+} // 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/ui/widget/registry.h b/src/ui/widget/registry.h
new file mode 100644
index 0000000..e6a190d
--- /dev/null
+++ b/src/ui/widget/registry.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_UI_WIDGET_REGISTRY__H
+#define INKSCAPE_UI_WIDGET_REGISTRY__H
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry {
+public:
+ Registry();
+ ~Registry();
+
+ bool isUpdating();
+ void setUpdating (bool);
+
+ SPDesktop *desktop() const { return _desktop; }
+ void setDesktop(SPDesktop *desktop);
+
+protected:
+ bool _updating;
+
+ SPDesktop *_desktop = nullptr;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Widget
+
+#endif // INKSCAPE_UI_WIDGET_REGISTRY__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/ui/widget/rendering-options.cpp b/src/ui/widget/rendering-options.cpp
new file mode 100644
index 0000000..4640b1e
--- /dev/null
+++ b/src/ui/widget/rendering-options.cpp
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Kees Cook <kees@outflux.net>
+ *
+ * Copyright (C) 2007 Kees Cook
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+
+#include "preferences.h"
+#include "rendering-options.h"
+#include "util/units.h"
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void RenderingOptions::_toggled()
+{
+ _frame_bitmap.set_sensitive(as_bitmap());
+}
+
+RenderingOptions::RenderingOptions () :
+ Gtk::Box (Gtk::ORIENTATION_VERTICAL),
+ _frame_backends ( Glib::ustring(_("Backend")) ),
+ _radio_vector ( Glib::ustring(_("Vector")) ),
+ _radio_bitmap ( Glib::ustring(_("Bitmap")) ),
+ _frame_bitmap ( Glib::ustring(_("Bitmap options")) ),
+ _dpi( _("DPI"),
+ Glib::ustring(_("Preferred resolution of rendering, "
+ "in dots per inch.")),
+ 1,
+ Glib::ustring(""), Glib::ustring(""),
+ false)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ // set up tooltips
+ _radio_vector.set_tooltip_text(
+ _("Render using Cairo vector operations. "
+ "The resulting image is usually smaller in file "
+ "size and can be arbitrarily scaled, but some "
+ "filter effects will not be correctly rendered."));
+ _radio_bitmap.set_tooltip_text(
+ _("Render everything as bitmap. The resulting image "
+ "is usually larger in file size and cannot be "
+ "arbitrarily scaled without quality loss, but all "
+ "objects will be rendered exactly as displayed."));
+
+ set_border_width(2);
+
+ Gtk::RadioButtonGroup group = _radio_vector.get_group ();
+ _radio_bitmap.set_group (group);
+ _radio_bitmap.signal_toggled().connect(sigc::mem_fun(*this, &RenderingOptions::_toggled));
+
+ // default to vector operations
+ if (prefs->getBool("/dialogs/printing/asbitmap", false)) {
+ _radio_bitmap.set_active();
+ } else {
+ _radio_vector.set_active();
+ }
+
+ // configure default DPI
+ _dpi.setRange(Inkscape::Util::Quantity::convert(1, "in", "pt"),2400.0);
+ _dpi.setValue(prefs->getDouble("/dialogs/printing/dpi",
+ Inkscape::Util::Quantity::convert(1, "in", "pt")));
+ _dpi.setIncrements(1.0,10.0);
+ _dpi.setDigits(0);
+ _dpi.update();
+
+ // fill frames
+ Gtk::Box *box_vector = Gtk::manage( new Gtk::Box (Gtk::ORIENTATION_VERTICAL) );
+ box_vector->set_border_width (2);
+ box_vector->add (_radio_vector);
+ box_vector->add (_radio_bitmap);
+ _frame_backends.add (*box_vector);
+
+ Gtk::Box *box_bitmap = Gtk::manage( new Gtk::Box (Gtk::ORIENTATION_HORIZONTAL) );
+ box_bitmap->set_border_width (2);
+ box_bitmap->add (_dpi);
+ _frame_bitmap.add (*box_bitmap);
+
+ // fill up container
+ add (_frame_backends);
+ add (_frame_bitmap);
+
+ // initialize states
+ _toggled();
+
+ show_all_children ();
+}
+
+bool
+RenderingOptions::as_bitmap ()
+{
+ return _radio_bitmap.get_active();
+}
+
+double
+RenderingOptions::bitmap_dpi ()
+{
+ return _dpi.getValue();
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ 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/ui/widget/rendering-options.h b/src/ui/widget/rendering-options.h
new file mode 100644
index 0000000..65d96f4
--- /dev/null
+++ b/src/ui/widget/rendering-options.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Kees Cook <kees@outflux.net>
+ *
+ * Copyright (C) 2007 Kees Cook
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H
+#define INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H
+
+#include "scalar.h"
+
+#include <gtkmm/frame.h>
+#include <gtkmm/radiobutton.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container for selecting rendering options.
+ */
+class RenderingOptions : public Gtk::Box
+{
+public:
+
+ /**
+ * Construct a Rendering Options widget.
+ */
+ RenderingOptions();
+
+ bool as_bitmap(); // should we render as a bitmap?
+ double bitmap_dpi(); // at what DPI should we render the bitmap?
+
+protected:
+ // Radio buttons to select desired rendering
+ Gtk::Frame _frame_backends;
+ Gtk::RadioButton _radio_vector;
+ Gtk::RadioButton _radio_bitmap;
+
+ // Bitmap options
+ Gtk::Frame _frame_bitmap;
+ Scalar _dpi; // DPI of bitmap to render
+
+ // callback for bitmap button
+ void _toggled();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0))
+ 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/ui/widget/rotateable.cpp b/src/ui/widget/rotateable.cpp
new file mode 100644
index 0000000..7f32823
--- /dev/null
+++ b/src/ui/widget/rotateable.cpp
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * buliabyak@gmail.com
+ *
+ * Copyright (C) 2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/eventbox.h>
+#include <2geom/point.h>
+#include "ui/tools/tool-base.h"
+#include "rotateable.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Rotateable::Rotateable():
+ axis(-M_PI/4),
+ maxdecl(M_PI/4)
+{
+ dragging = false;
+ working = false;
+ scrolling = false;
+ modifier = 0;
+ current_axis = axis;
+
+ signal_button_press_event().connect(sigc::mem_fun(*this, &Rotateable::on_click));
+ signal_motion_notify_event().connect(sigc::mem_fun(*this, &Rotateable::on_motion));
+ signal_button_release_event().connect(sigc::mem_fun(*this, &Rotateable::on_release));
+ gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect(sigc::mem_fun(*this, &Rotateable::on_scroll));
+
+}
+
+bool Rotateable::on_click(GdkEventButton *event) {
+ if (event->button == 1) {
+ drag_started_x = event->x;
+ drag_started_y = event->y;
+ modifier = get_single_modifier(modifier, event->state);
+ dragging = true;
+ working = false;
+ current_axis = axis;
+ return true;
+ }
+ return false;
+}
+
+guint Rotateable::get_single_modifier(guint old, guint state) {
+
+ if (old == 0 || old == 3) {
+ if (state & GDK_CONTROL_MASK)
+ return 1; // ctrl
+ if (state & GDK_SHIFT_MASK)
+ return 2; // shift
+ if (state & GDK_MOD1_MASK)
+ return 3; // alt
+ return 0;
+ } else {
+ if (!(state & GDK_CONTROL_MASK) && !(state & GDK_SHIFT_MASK)) {
+ if (state & GDK_MOD1_MASK)
+ return 3; // alt
+ else
+ return 0; // none
+ }
+ if (old == 1) {
+ if (state & GDK_SHIFT_MASK && !(state & GDK_CONTROL_MASK))
+ return 2; // shift
+ if (state & GDK_MOD1_MASK && !(state & GDK_CONTROL_MASK))
+ return 3; // alt
+ return 1;
+ }
+ if (old == 2) {
+ if (state & GDK_CONTROL_MASK && !(state & GDK_SHIFT_MASK))
+ return 1; // ctrl
+ if (state & GDK_MOD1_MASK && !(state & GDK_SHIFT_MASK))
+ return 3; // alt
+ return 2;
+ }
+ return old;
+ }
+}
+
+
+bool Rotateable::on_motion(GdkEventMotion *event) {
+ if (dragging) {
+ double dist = Geom::L2(Geom::Point(event->x, event->y) - Geom::Point(drag_started_x, drag_started_y));
+ double angle = atan2(event->y - drag_started_y, event->x - drag_started_x);
+ if (dist > 20) {
+ working = true;
+ double force = CLAMP (-(angle - current_axis)/maxdecl, -1, 1);
+ if (fabs(force) < 0.002)
+ force = 0; // snap to zero
+ if (modifier != get_single_modifier(modifier, event->state)) {
+ // user has switched modifiers in mid drag, close past drag and start a new
+ // one, redefining axis temporarily
+ do_release(force, modifier);
+ current_axis = angle;
+ modifier = get_single_modifier(modifier, event->state);
+ } else {
+ do_motion(force, modifier);
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+
+bool Rotateable::on_release(GdkEventButton *event) {
+ if (dragging && working) {
+ double angle = atan2(event->y - drag_started_y, event->x - drag_started_x);
+ double force = CLAMP(-(angle - current_axis) / maxdecl, -1, 1);
+ if (fabs(force) < 0.002)
+ force = 0; // snap to zero
+ do_release(force, modifier);
+ current_axis = axis;
+ dragging = false;
+ working = false;
+ return true;
+ }
+ dragging = false;
+ working = false;
+ return false;
+}
+
+bool Rotateable::on_scroll(GdkEventScroll* event)
+{
+ double change = 0.0;
+
+ if (event->direction == GDK_SCROLL_UP) {
+ change = 1.0;
+ } else if (event->direction == GDK_SCROLL_DOWN) {
+ change = -1.0;
+ } else if (event->direction == GDK_SCROLL_SMOOTH) {
+ double delta_y_clamped = CLAMP(event->delta_y, -1.0, 1.0); // values > 1 result in excessive changes
+ change = 1.0 * -delta_y_clamped;
+ } else {
+ return FALSE;
+ }
+
+ drag_started_x = event->x;
+ drag_started_y = event->y;
+ modifier = get_single_modifier(modifier, event->state);
+ dragging = false;
+ working = false;
+ scrolling = true;
+ current_axis = axis;
+
+ do_scroll(change, modifier);
+
+ dragging = false;
+ working = false;
+ scrolling = false;
+
+ return TRUE;
+}
+
+Rotateable::~Rotateable() = default;
+
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/rotateable.h b/src/ui/widget/rotateable.h
new file mode 100644
index 0000000..c174a09
--- /dev/null
+++ b/src/ui/widget/rotateable.h
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * buliabyak@gmail.com
+ *
+ * Copyright (C) 2007 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_ROTATEABLE_H
+#define INKSCAPE_UI_ROTATEABLE_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/eventbox.h>
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Widget adjustable by dragging it to rotate away from a zero-change axis.
+ */
+class Rotateable: public Gtk::EventBox
+{
+public:
+ Rotateable();
+
+ ~Rotateable() override;
+
+ bool on_click(GdkEventButton *event);
+ bool on_motion(GdkEventMotion *event);
+ bool on_release(GdkEventButton *event);
+ bool on_scroll(GdkEventScroll* event);
+
+ double axis;
+ double current_axis;
+ double maxdecl;
+ bool scrolling;
+
+private:
+ double drag_started_x;
+ double drag_started_y;
+ guint modifier;
+ bool dragging;
+ bool working;
+
+ guint get_single_modifier(guint old, guint state);
+
+ virtual void do_motion (double /*by*/, guint /*state*/) {}
+ virtual void do_release (double /*by*/, guint /*state*/) {}
+ virtual void do_scroll (double /*by*/, guint /*state*/) {}
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_ROTATEABLE_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/ui/widget/scalar-unit.cpp b/src/ui/widget/scalar-unit.cpp
new file mode 100644
index 0000000..9db6b79
--- /dev/null
+++ b/src/ui/widget/scalar-unit.cpp
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Derek P. Moore <derekm@hackunix.org>
+ * buliabyak@gmail.com
+ *
+ * Copyright (C) 2004-2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "scalar-unit.h"
+#include "spinbutton.h"
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ScalarUnit::ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip,
+ UnitType unit_type,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ UnitMenu *unit_menu,
+ bool mnemonic)
+ : Scalar(label, tooltip, suffix, icon, mnemonic),
+ _unit_menu(unit_menu),
+ _hundred_percent(0),
+ _absolute_is_increment(false),
+ _percentage_is_increment(false)
+{
+ if (_unit_menu == nullptr) {
+ _unit_menu = new UnitMenu();
+ g_assert(_unit_menu);
+ _unit_menu->setUnitType(unit_type);
+
+ remove(*_widget);
+ Gtk::Box *widget_holder = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 6);
+ widget_holder->pack_start(*_widget, Gtk::PACK_SHRINK);
+ widget_holder->pack_start(*Gtk::manage(_unit_menu), Gtk::PACK_SHRINK);
+ pack_start(*Gtk::manage(widget_holder), Gtk::PACK_SHRINK);
+ }
+ _unit_menu->signal_changed()
+ .connect_notify(sigc::mem_fun(*this, &ScalarUnit::on_unit_changed));
+
+ static_cast<SpinButton*>(_widget)->setUnitMenu(_unit_menu);
+
+ lastUnits = _unit_menu->getUnitAbbr();
+}
+
+ScalarUnit::ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip,
+ ScalarUnit &take_unitmenu,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Scalar(label, tooltip, suffix, icon, mnemonic),
+ _unit_menu(take_unitmenu._unit_menu),
+ _hundred_percent(0),
+ _absolute_is_increment(false),
+ _percentage_is_increment(false)
+{
+ _unit_menu->signal_changed()
+ .connect_notify(sigc::mem_fun(*this, &ScalarUnit::on_unit_changed));
+
+ static_cast<SpinButton*>(_widget)->setUnitMenu(_unit_menu);
+
+ lastUnits = _unit_menu->getUnitAbbr();
+}
+
+
+void ScalarUnit::initScalar(double min_value, double max_value)
+{
+ g_assert(_unit_menu != nullptr);
+ Scalar::setDigits(_unit_menu->getDefaultDigits());
+ Scalar::setIncrements(_unit_menu->getDefaultStep(),
+ _unit_menu->getDefaultPage());
+ Scalar::setRange(min_value, max_value);
+}
+
+bool ScalarUnit::setUnit(Glib::ustring const &unit)
+{
+ g_assert(_unit_menu != nullptr);
+ // First set the unit
+ if (!_unit_menu->setUnit(unit)) {
+ return false;
+ }
+ lastUnits = unit;
+ return true;
+}
+
+void ScalarUnit::setUnitType(UnitType unit_type)
+{
+ g_assert(_unit_menu != nullptr);
+ _unit_menu->setUnitType(unit_type);
+ lastUnits = _unit_menu->getUnitAbbr();
+}
+
+void ScalarUnit::resetUnitType(UnitType unit_type)
+{
+ g_assert(_unit_menu != nullptr);
+ _unit_menu->resetUnitType(unit_type);
+ lastUnits = _unit_menu->getUnitAbbr();
+}
+
+Unit const * ScalarUnit::getUnit() const
+{
+ g_assert(_unit_menu != nullptr);
+ return _unit_menu->getUnit();
+}
+
+UnitType ScalarUnit::getUnitType() const
+{
+ g_assert(_unit_menu);
+ return _unit_menu->getUnitType();
+}
+
+void ScalarUnit::setValue(double number, Glib::ustring const &units)
+{
+ g_assert(_unit_menu != nullptr);
+ _unit_menu->setUnit(units);
+ Scalar::setValue(number);
+}
+
+void ScalarUnit::setValueKeepUnit(double number, Glib::ustring const &units)
+{
+ g_assert(_unit_menu != nullptr);
+ if (units == "") {
+ // set the value in the default units
+ Scalar::setValue(number);
+ } else {
+ double conversion = _unit_menu->getConversion(units);
+ Scalar::setValue(number / conversion);
+ }
+}
+
+void ScalarUnit::setValue(double number)
+{
+ Scalar::setValue(number);
+}
+
+double ScalarUnit::getValue(Glib::ustring const &unit_name) const
+{
+ g_assert(_unit_menu != nullptr);
+ if (unit_name == "") {
+ // Return the value in the default units
+ return Scalar::getValue();
+ } else {
+ double conversion = _unit_menu->getConversion(unit_name);
+ return conversion * Scalar::getValue();
+ }
+}
+
+void ScalarUnit::grabFocusAndSelectEntry()
+{
+ _widget->grab_focus();
+ static_cast<SpinButton*>(_widget)->select_region(0, 20);
+}
+
+void ScalarUnit::setAlignment(double xalign)
+{
+ xalign = std::clamp(xalign,0.0,1.0);
+ static_cast<Gtk::Entry*>(_widget)->set_alignment(xalign);
+}
+
+void ScalarUnit::setHundredPercent(double number)
+{
+ _hundred_percent = number;
+}
+
+void ScalarUnit::setAbsoluteIsIncrement(bool value)
+{
+ _absolute_is_increment = value;
+}
+
+void ScalarUnit::setPercentageIsIncrement(bool value)
+{
+ _percentage_is_increment = value;
+}
+
+double ScalarUnit::PercentageToAbsolute(double value)
+{
+ // convert from percent to absolute
+ double convertedVal = 0;
+ double hundred_converted = _hundred_percent / _unit_menu->getConversion("px"); // _hundred_percent is in px
+ if (_percentage_is_increment)
+ value += 100;
+ convertedVal = 0.01 * hundred_converted * value;
+ if (_absolute_is_increment)
+ convertedVal -= hundred_converted;
+
+ return convertedVal;
+}
+
+double ScalarUnit::AbsoluteToPercentage(double value)
+{
+ double convertedVal = 0;
+ // convert from absolute to percent
+ if (_hundred_percent == 0) {
+ if (_percentage_is_increment)
+ convertedVal = 0;
+ else
+ convertedVal = 100;
+ } else {
+ double hundred_converted = _hundred_percent / _unit_menu->getConversion("px", lastUnits); // _hundred_percent is in px
+ if (_absolute_is_increment)
+ value += hundred_converted;
+ convertedVal = 100 * value / hundred_converted;
+ if (_percentage_is_increment)
+ convertedVal -= 100;
+ }
+
+ return convertedVal;
+}
+
+double ScalarUnit::getAsPercentage()
+{
+ double convertedVal = AbsoluteToPercentage(Scalar::getValue());
+ return convertedVal;
+}
+
+
+void ScalarUnit::setFromPercentage(double value)
+{
+ double absolute = PercentageToAbsolute(value);
+ Scalar::setValue(absolute);
+}
+
+
+void ScalarUnit::on_unit_changed()
+{
+ g_assert(_unit_menu != nullptr);
+
+ Glib::ustring abbr = _unit_menu->getUnitAbbr();
+
+ if (_suffix) {
+ _suffix->set_label(abbr);
+ }
+
+ Inkscape::Util::Unit const *new_unit = unit_table.getUnit(abbr);
+ Inkscape::Util::Unit const *old_unit = unit_table.getUnit(lastUnits);
+
+ double convertedVal = 0;
+ if (old_unit->type == UNIT_TYPE_DIMENSIONLESS && new_unit->type == UNIT_TYPE_LINEAR) {
+ convertedVal = PercentageToAbsolute(Scalar::getValue());
+ } else if (old_unit->type == UNIT_TYPE_LINEAR && new_unit->type == UNIT_TYPE_DIMENSIONLESS) {
+ convertedVal = AbsoluteToPercentage(Scalar::getValue());
+ } else {
+ double conversion = _unit_menu->getConversion(lastUnits);
+ convertedVal = Scalar::getValue() / conversion;
+ }
+ Scalar::setValue(convertedVal);
+
+ lastUnits = abbr;
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/scalar-unit.h b/src/ui/widget/scalar-unit.h
new file mode 100644
index 0000000..3d7b77d
--- /dev/null
+++ b/src/ui/widget/scalar-unit.h
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Derek P. Moore <derekm@hackunix.org>
+ * buliabyak@gmail.com
+ *
+ * Copyright (C) 2004-2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SCALAR_UNIT_H
+#define INKSCAPE_UI_WIDGET_SCALAR_UNIT_H
+
+#include "scalar.h"
+#include "unit-menu.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional icon or suffix, for
+ * entering the values of various unit types.
+ *
+ * A ScalarUnit is a control for entering, viewing, or manipulating
+ * numbers with units. This differs from ordinary numbers like 2 or
+ * 3.14 because the number portion of a scalar *only* has meaning
+ * when considered with its unit type. For instance, 12 m and 12 in
+ * have very different actual values, but 1 m and 100 cm have the same
+ * value. The ScalarUnit allows us to abstract the presentation of
+ * the scalar to the user from the internal representations used by
+ * the program.
+ */
+class ScalarUnit : public Scalar
+{
+public:
+ /**
+ * Construct a ScalarUnit.
+ *
+ * @param label Label.
+ * @param unit_type Unit type (defaults to UNIT_TYPE_LINEAR).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param unit_menu UnitMenu drop down; if not specified, one will be created
+ * and displayed after the widget (defaults to NULL).
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip,
+ UnitType unit_type = UNIT_TYPE_LINEAR,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ UnitMenu *unit_menu = nullptr,
+ bool mnemonic = true);
+
+ /**
+ * Construct a ScalarUnit.
+ *
+ * @param label Label.
+ * @param tooltip Tooltip text.
+ * @param take_unitmenu Use the unitmenu from this parameter.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip,
+ ScalarUnit &take_unitmenu,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Initializes the scalar based on the settings in _unit_menu.
+ * Requires that _unit_menu has already been initialized.
+ */
+ void initScalar(double min_value, double max_value);
+
+ /**
+ * Gets the object for the currently selected unit.
+ */
+ Unit const * getUnit() const;
+
+ /**
+ * Gets the UnitType ID for the unit.
+ */
+ UnitType getUnitType() const;
+
+ /**
+ * Returns the value in the given unit system.
+ */
+ double getValue(Glib::ustring const &units) const;
+
+ /**
+ * Sets the unit for the ScalarUnit widget.
+ */
+ bool setUnit(Glib::ustring const &units);
+
+ /**
+ * Adds the unit type to the ScalarUnit widget.
+ */
+ void setUnitType(UnitType unit_type);
+
+ /**
+ * Resets the unit type for the ScalarUnit widget.
+ */
+ void resetUnitType(UnitType unit_type);
+
+ /**
+ * allow align text in entry.
+ */
+ void setAlignment(double xalign);
+
+ /**
+ * Sets the number and unit system.
+ */
+ void setValue(double number, Glib::ustring const &units);
+
+ /**
+ * Convert and sets the number only and keeps the current unit.
+ */
+ void setValueKeepUnit(double number, Glib::ustring const &units);
+
+ /**
+ * Sets the number only.
+ */
+ void setValue(double number);
+
+ /**
+ * Grab focus, and select the text that is in the entry field.
+ */
+ void grabFocusAndSelectEntry();
+
+ void setHundredPercent(double number);
+
+ void setAbsoluteIsIncrement(bool value);
+
+ void setPercentageIsIncrement(bool value);
+
+ /**
+ * Convert value from % to absolute, using _hundred_percent and *_is_increment flags.
+ */
+ double PercentageToAbsolute(double value);
+
+ /**
+ * Convert value from absolute to %, using _hundred_percent and *_is_increment flags.
+ */
+ double AbsoluteToPercentage(double value);
+
+ /**
+ * Assuming the current unit is absolute, get the corresponding % value.
+ */
+ double getAsPercentage();
+
+ /**
+ * Assuming the current unit is absolute, set the value corresponding to a given %.
+ */
+ void setFromPercentage(double value);
+
+ /**
+ * Signal handler for updating the value and suffix label when unit is changed.
+ */
+ void on_unit_changed();
+
+protected:
+ UnitMenu *_unit_menu;
+
+ double _hundred_percent; // the length that corresponds to 100%, in px, for %-to/from-absolute conversions
+
+ bool _absolute_is_increment; // if true, 120% with _hundred_percent=100px gets converted to/from 20px; otherwise, to/from 120px
+ bool _percentage_is_increment; // if true, 120px with _hundred_percent=100px gets converted to/from 20%; otherwise, to/from 120%
+ // if both are true, 20px is converted to/from 20% if _hundred_percent=100px
+
+ Glib::ustring lastUnits; // previously selected unit, for conversions
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SCALAR_UNIT_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/ui/widget/scalar.cpp b/src/ui/widget/scalar.cpp
new file mode 100644
index 0000000..d6766fe
--- /dev/null
+++ b/src/ui/widget/scalar.cpp
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl>
+ *
+ * Copyright (C) 2004-2011 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "scalar.h"
+#include "spinbutton.h"
+#include <gtkmm/scale.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(0.0, digits), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new SpinButton(adjust, 0.0, digits), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+unsigned Scalar::getDigits() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_digits();
+}
+
+double Scalar::getStep() const
+{
+ g_assert(_widget != nullptr);
+ double step, page;
+ static_cast<SpinButton*>(_widget)->get_increments(step, page);
+ return step;
+}
+
+double Scalar::getPage() const
+{
+ g_assert(_widget != nullptr);
+ double step, page;
+ static_cast<SpinButton*>(_widget)->get_increments(step, page);
+ return page;
+}
+
+double Scalar::getRangeMin() const
+{
+ g_assert(_widget != nullptr);
+ double min, max;
+ static_cast<SpinButton*>(_widget)->get_range(min, max);
+ return min;
+}
+
+double Scalar::getRangeMax() const
+{
+ g_assert(_widget != nullptr);
+ double min, max;
+ static_cast<SpinButton*>(_widget)->get_range(min, max);
+ return max;
+}
+
+double Scalar::getValue() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_value();
+}
+
+int Scalar::getValueAsInt() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<SpinButton*>(_widget)->get_value_as_int();
+}
+
+
+void Scalar::setDigits(unsigned digits)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_digits(digits);
+}
+
+void Scalar::setIncrements(double step, double /*page*/)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_increments(step, 0);
+}
+
+void Scalar::setRange(double min, double max)
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->set_range(min, max);
+}
+
+void Scalar::setValue(double value, bool setProg)
+{
+ g_assert(_widget != nullptr);
+ if (setProg) {
+ setProgrammatically = true; // callback is supposed to reset back, if it cares
+ }
+ static_cast<SpinButton*>(_widget)->set_value(value);
+}
+
+void Scalar::setWidthChars(unsigned chars)
+{
+ g_assert(_widget != NULL);
+ static_cast<SpinButton*>(_widget)->set_width_chars(chars);
+}
+
+void Scalar::update()
+{
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->update();
+}
+
+void Scalar::addSlider()
+{
+ auto scale = new Gtk::Scale(static_cast<SpinButton*>(_widget)->get_adjustment());
+ scale->set_draw_value(false);
+ pack_start(*manage (scale));
+}
+
+Glib::SignalProxy0<void> Scalar::signal_value_changed()
+{
+ return static_cast<SpinButton*>(_widget)->signal_value_changed();
+}
+
+Glib::SignalProxy1<bool, GdkEventButton*> Scalar::signal_button_release_event()
+{
+ return static_cast<SpinButton*>(_widget)->signal_button_release_event();
+}
+
+void Scalar::hide_label() {
+ if (auto label = const_cast<Gtk::Label*>(getLabel())) {
+ label->hide();
+ label->set_no_show_all();
+ label->set_hexpand(true);
+ }
+ if (_widget) {
+ remove(*_widget);
+ _widget->set_hexpand();
+ this->pack_end(*_widget);
+ }
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/scalar.h b/src/ui/widget/scalar.h
new file mode 100644
index 0000000..0ef8d35
--- /dev/null
+++ b/src/ui/widget/scalar.h
@@ -0,0 +1,191 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Derek P. Moore <derekm@hackunix.org>
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SCALAR_H
+#define INKSCAPE_UI_WIDGET_SCALAR_H
+
+#include "labelled.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with spin buttons and optional
+ * icon or suffix, for entering arbitrary number values.
+ */
+class Scalar : public Labelled
+{
+public:
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param digits Number of decimal digits to display.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ unsigned digits,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Construct a Scalar Widget.
+ *
+ * @param label Label.
+ * @param adjust Adjustment to use for the SpinButton.
+ * @param digits Number of decimal digits to display (defaults to 0).
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to true).
+ */
+ Scalar(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::RefPtr<Gtk::Adjustment> &adjust,
+ unsigned digits = 0,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Fetches the precision of the spin button.
+ */
+ unsigned getDigits() const;
+
+ /**
+ * Gets the current step increment used by the spin button.
+ */
+ double getStep() const;
+
+ /**
+ * Gets the current page increment used by the spin button.
+ */
+ double getPage() const;
+
+ /**
+ * Gets the minimum range value allowed for the spin button.
+ */
+ double getRangeMin() const;
+
+ /**
+ * Gets the maximum range value allowed for the spin button.
+ */
+ double getRangeMax() const;
+
+ bool getSnapToTicks() const;
+
+ /**
+ * Get the value in the spin_button.
+ */
+ double getValue() const;
+
+ /**
+ * Get the value spin_button represented as an integer.
+ */
+ int getValueAsInt() const;
+
+ /**
+ * Sets the precision to be displayed by the spin button.
+ */
+ void setDigits(unsigned digits);
+
+ /**
+ * Sets the step and page increments for the spin button.
+ * @todo Remove the second parameter - deprecated
+ */
+ void setIncrements(double step, double page);
+
+ /**
+ * Sets the minimum and maximum range allowed for the spin button.
+ */
+ void setRange(double min, double max);
+
+ /**
+ * Sets the value of the spin button.
+ */
+ void setValue(double value, bool setProg = true);
+
+ /**
+ * Sets the width of the spin button by number of characters.
+ */
+ void setWidthChars(unsigned chars);
+
+ /**
+ * Manually forces an update of the spin button.
+ */
+ void update();
+
+ /**
+ * Adds a slider (HScale) to the left of the spinbox.
+ */
+ void addSlider();
+
+ /**
+ * Signal raised when the spin button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_value_changed();
+
+ /**
+ * Signal raised when the spin button's pressed.
+ */
+ Glib::SignalProxy1<bool, GdkEventButton*> signal_button_release_event();
+
+ /**
+ * true if the value was set by setValue, not changed by the user;
+ * if a callback checks it, it must reset it back to false.
+ */
+ bool setProgrammatically;
+
+ // permanently hide label part
+ void hide_label();
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SCALAR_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/ui/widget/scroll-utils.cpp b/src/ui/widget/scroll-utils.cpp
new file mode 100644
index 0000000..5822a15
--- /dev/null
+++ b/src/ui/widget/scroll-utils.cpp
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Thomas Holder
+ *
+ * Copyright (C) 2020 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "scroll-utils.h"
+
+#include <gtkmm/scrolledwindow.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Get the first ancestor which is scrollable.
+ */
+Gtk::Widget *get_scrollable_ancestor(Gtk::Widget *widget)
+{
+ auto parent = widget->get_parent();
+ if (!parent) {
+ return nullptr;
+ }
+ if (auto scrollable = dynamic_cast<Gtk::ScrolledWindow *>(parent)) {
+ return scrollable;
+ }
+ return get_scrollable_ancestor(parent);
+}
+
+/**
+ * Return true if scrolling is allowed.
+ *
+ * Scrolling is allowed for any of:
+ * - Shift modifier is pressed
+ * - Widget has focus
+ * - Widget has no scrollable ancestor
+ */
+bool scrolling_allowed(Gtk::Widget *widget, GdkEventScroll *event)
+{
+ bool const shift = event && (event->state & GDK_SHIFT_MASK);
+ return shift || //
+ widget->has_focus() || //
+ get_scrollable_ancestor(widget) == nullptr;
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/scroll-utils.h b/src/ui/widget/scroll-utils.h
new file mode 100644
index 0000000..14b45de
--- /dev/null
+++ b/src/ui/widget/scroll-utils.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_INKSCAPE_UI_WIDGET_SCROLL_UTILS_H
+#define SEEN_INKSCAPE_UI_WIDGET_SCROLL_UTILS_H
+
+/* Authors:
+ * Thomas Holder
+ *
+ * Copyright (C) 2020 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdk/gdk.h>
+
+namespace Gtk {
+class Widget;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Gtk::Widget *get_scrollable_ancestor(Gtk::Widget *widget);
+
+bool scrolling_allowed(Gtk::Widget *widget, GdkEventScroll *event = nullptr);
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
+// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/scrollprotected.h b/src/ui/widget/scrollprotected.h
new file mode 100644
index 0000000..c060398
--- /dev/null
+++ b/src/ui/widget/scrollprotected.h
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_INKSCAPE_UI_WIDGET_SCROLLPROTECTED_H
+#define SEEN_INKSCAPE_UI_WIDGET_SCROLLPROTECTED_H
+
+/* Authors:
+ * Thomas Holder
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 2020-2021 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+
+#include "scroll-utils.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A class decorator which blocks the scroll event if the widget does not have
+ * focus and any ancestor is a scrollable window, and SHIFT is not pressed.
+ *
+ * For custom scroll event handlers, derived classes must implement
+ * on_safe_scroll_event instead of on_scroll_event. Directly connecting to
+ * signal_scroll_event() will bypass the scroll protection.
+ *
+ * @tparam Base A subclass of Gtk::Widget
+ */
+template <typename Base>
+class ScrollProtected : public Base
+{
+public:
+ using Base::Base;
+ using typename Base::BaseObjectType;
+ ScrollProtected()
+ : Base()
+ {}
+ ScrollProtected(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Base(cobject){};
+ ~ScrollProtected() override{};
+
+protected:
+ /**
+ * Event handler for "safe" scroll events which are only triggered if:
+ * - the widget has focus
+ * - or the widget has no scrolled window ancestor
+ * - or the Shift key is pressed
+ */
+ virtual bool on_safe_scroll_event(GdkEventScroll *event)
+ { //
+ return Base::on_scroll_event(event);
+ }
+
+ bool on_scroll_event(GdkEventScroll *event) final
+ {
+ if (!scrolling_allowed(this, event)) {
+ return false;
+ }
+ return on_safe_scroll_event(event);
+ }
+};
+
+/**
+ * A class decorator for scroll widgets like scrolled window to transfer scroll to
+ * any ancestor which is is a scrollable window when scroll reached end.
+ *
+ * For custom scroll event handlers, derived classes must implement
+ * on_safe_scroll_event instead of on_scroll_event. Directly connecting to
+ * signal_scroll_event() will bypass the scroll protection.
+ *
+ * @tparam Base A subclass of Gtk::Widget
+ */
+template <typename Base>
+class ScrollTransfer : public Base
+{
+public:
+ using Base::Base;
+ using typename Base::BaseObjectType;
+ ScrollTransfer()
+ : Base()
+ {}
+ ScrollTransfer(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Base(cobject){};
+ ~ScrollTransfer() override{};
+protected:
+ /**
+ * Event handler for "safe" scroll events
+ */
+ virtual bool on_safe_scroll_event(GdkEventScroll *event)
+ { //
+ return Base::on_scroll_event(event);
+ }
+
+ bool on_scroll_event(GdkEventScroll *event) final
+ {
+ auto scrollable = dynamic_cast<Gtk::Widget *>(Inkscape::UI::Widget::get_scrollable_ancestor(this));
+ auto adj = this->get_vadjustment();
+ auto before = adj->get_value();
+ bool result = on_safe_scroll_event(event);
+ auto after = adj->get_value();
+ if (scrollable && before == after) {
+ return false;
+ }
+
+ return result;
+ }
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
+// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp
new file mode 100644
index 0000000..433112c
--- /dev/null
+++ b/src/ui/widget/selected-style.cpp
@@ -0,0 +1,1435 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * buliabyak@gmail.com
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2005 author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "selected-style.h"
+
+#include <vector>
+
+#include <gtkmm/separatormenuitem.h>
+
+
+#include "desktop-style.h"
+#include "document-undo.h"
+#include "gradient-chemistry.h"
+#include "message-context.h"
+#include "selection.h"
+
+#include "include/gtkmm_version.h"
+
+#include "object/sp-hatch.h"
+#include "object/sp-linear-gradient.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-namedview.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+#include "style.h"
+
+#include "svg/css-ostringstream.h"
+#include "svg/svg-color.h"
+
+#include "ui/cursor-utils.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/fill-and-stroke.h"
+#include "ui/icon-names.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/canvas.h" // Forced redraws.
+#include "ui/widget/color-preview.h"
+#include "ui/widget/gradient-image.h"
+
+#include "widgets/ege-paint-def.h"
+#include "widgets/spw-utilities.h"
+
+using Inkscape::Util::unit_table;
+
+static gdouble const _sw_presets[] = { 32 , 16 , 10 , 8 , 6 , 4 , 3 , 2 , 1.5 , 1 , 0.75 , 0.5 , 0.25 , 0.1 };
+static gchar const *const _sw_presets_str[] = {"32", "16", "10", "8", "6", "4", "3", "2", "1.5", "1", "0.75", "0.5", "0.25", "0.1"};
+
+static void
+ss_selection_changed (Inkscape::Selection *, gpointer data)
+{
+ Inkscape::UI::Widget::SelectedStyle *ss = (Inkscape::UI::Widget::SelectedStyle *) data;
+ ss->update();
+}
+
+static void
+ss_selection_modified( Inkscape::Selection *selection, guint flags, gpointer data )
+{
+ // Don't update the style when dragging or doing non-style related changes
+ if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+ ss_selection_changed (selection, data);
+ }
+}
+
+static void
+ss_subselection_changed( gpointer /*dragger*/, gpointer data )
+{
+ ss_selection_changed (nullptr, data);
+}
+
+namespace {
+
+void clearTooltip( Gtk::Widget &widget )
+{
+ widget.set_tooltip_text("");
+ widget.set_has_tooltip(false);
+}
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+struct DropTracker {
+ SelectedStyle* parent;
+ int item;
+};
+
+/* Drag and Drop */
+enum ui_drop_target_info {
+ APP_OSWB_COLOR
+};
+
+//TODO: warning: deprecated conversion from string constant to ‘gchar*’
+//
+//Turn out to be warnings that we should probably leave in place. The
+// pointers/types used need to be read-only. So until we correct the using
+// code, those warnings are actually desired. They say "Hey! Fix this". We
+// definitely don't want to hide/ignore them. --JonCruz
+static const GtkTargetEntry ui_drop_target_entries [] = {
+ {g_strdup("application/x-oswb-color"), 0, APP_OSWB_COLOR}
+};
+
+static guint nui_drop_target_entries = G_N_ELEMENTS(ui_drop_target_entries);
+
+/* convenience function */
+static Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop);
+
+SelectedStyle::SelectedStyle(bool /*layout*/)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ , current_stroke_width(0)
+ , _sw_unit(nullptr)
+ , _desktop(nullptr)
+ , _table()
+ , _fill_label(_("Fill:"))
+ , _stroke_label(_("Stroke:"))
+ , _opacity_label(_("O:"))
+ , _fill_place(this, SS_FILL)
+ , _stroke_place(this, SS_STROKE)
+ , _fill_flag_place()
+ , _stroke_flag_place()
+ , _opacity_place()
+ , _opacity_adjustment(Gtk::Adjustment::create(100, 0.0, 100, 1.0, 10.0))
+ , _opacity_sb(0.02, 0)
+ , _fill(Gtk::ORIENTATION_HORIZONTAL, 1)
+ , _stroke(Gtk::ORIENTATION_HORIZONTAL)
+ , _stroke_width_place(this)
+ , _stroke_width("")
+ , _fill_empty_space("")
+ , _opacity_blocked(false)
+{
+ set_name("SelectedStyle");
+ _drop[0] = _drop[1] = nullptr;
+ _dropEnabled[0] = _dropEnabled[1] = false;
+
+ _fill_label.set_halign(Gtk::ALIGN_END);
+ _fill_label.set_valign(Gtk::ALIGN_CENTER);
+ _fill_label.set_margin_top(0);
+ _fill_label.set_margin_bottom(0);
+ _stroke_label.set_halign(Gtk::ALIGN_END);
+ _stroke_label.set_valign(Gtk::ALIGN_CENTER);
+ _stroke_label.set_margin_top(0);
+ _stroke_label.set_margin_bottom(0);
+ _opacity_label.set_halign(Gtk::ALIGN_START);
+ _opacity_label.set_valign(Gtk::ALIGN_CENTER);
+ _opacity_label.set_margin_top(0);
+ _opacity_label.set_margin_bottom(0);
+ _stroke_width.set_name("monoStrokeWidth");
+ _fill_empty_space.set_name("fillEmptySpace");
+
+ _fill_label.set_margin_start(0);
+ _fill_label.set_margin_end(0);
+ _stroke_label.set_margin_start(0);
+ _stroke_label.set_margin_end(0);
+ _opacity_label.set_margin_start(0);
+ _opacity_label.set_margin_end(0);
+
+ _table.set_column_spacing(2);
+ _table.set_row_spacing(0);
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+
+ _na[i].set_markup (_("N/A"));
+ _na[i].show_all();
+ __na[i] = (_("Nothing selected"));
+
+ if (i == SS_FILL) {
+ _none[i].set_markup (C_("Fill", "<i>None</i>"));
+ } else {
+ _none[i].set_markup (C_("Stroke", "<i>None</i>"));
+ }
+ _none[i].show_all();
+ __none[i] = (i == SS_FILL)? (C_("Fill and stroke", "No fill, middle-click for black fill")) : (C_("Fill and stroke", "No stroke, middle-click for black stroke"));
+
+ _pattern[i].set_markup (_("Pattern"));
+ _pattern[i].show_all();
+ __pattern[i] = (i == SS_FILL)? (_("Pattern (fill)")) : (_("Pattern (stroke)"));
+
+ _hatch[i].set_markup(_("Hatch"));
+ _hatch[i].show_all();
+ __hatch[i] = (i == SS_FILL) ? (_("Hatch (fill)")) : (_("Hatch (stroke)"));
+
+ _lgradient[i].set_markup (_("<b>L</b>"));
+ _lgradient[i].show_all();
+ __lgradient[i] = (i == SS_FILL)? (_("Linear gradient (fill)")) : (_("Linear gradient (stroke)"));
+
+ _gradient_preview_l[i] = Gtk::manage(new GradientImage(nullptr));
+ _gradient_box_l[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ _gradient_box_l[i].pack_start(_lgradient[i]);
+ _gradient_box_l[i].pack_start(*_gradient_preview_l[i]);
+ _gradient_box_l[i].show_all();
+
+ _rgradient[i].set_markup (_("<b>R</b>"));
+ _rgradient[i].show_all();
+ __rgradient[i] = (i == SS_FILL)? (_("Radial gradient (fill)")) : (_("Radial gradient (stroke)"));
+
+ _gradient_preview_r[i] = Gtk::manage(new GradientImage(nullptr));
+ _gradient_box_r[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ _gradient_box_r[i].pack_start(_rgradient[i]);
+ _gradient_box_r[i].pack_start(*_gradient_preview_r[i]);
+ _gradient_box_r[i].show_all();
+
+#ifdef WITH_MESH
+ _mgradient[i].set_markup (_("<b>M</b>"));
+ _mgradient[i].show_all();
+ __mgradient[i] = (i == SS_FILL)? (_("Mesh gradient (fill)")) : (_("Mesh gradient (stroke)"));
+
+ _gradient_preview_m[i] = Gtk::manage(new GradientImage(nullptr));
+ _gradient_box_m[i].set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ _gradient_box_m[i].pack_start(_mgradient[i]);
+ _gradient_box_m[i].pack_start(*_gradient_preview_m[i]);
+ _gradient_box_m[i].show_all();
+#endif
+
+ _many[i].set_markup (_("≠"));
+ _many[i].show_all();
+ __many[i] = (i == SS_FILL)? (_("Different fills")) : (_("Different strokes"));
+
+ _unset[i].set_markup (_("<b>Unset</b>"));
+ _unset[i].show_all();
+ __unset[i] = (i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke"));
+
+ _color_preview[i] = new Inkscape::UI::Widget::ColorPreview (0);
+ __color[i] = (i == SS_FILL)? (_("Flat color (fill)")) : (_("Flat color (stroke)"));
+
+ // TRANSLATORS: A means "Averaged"
+ _averaged[i].set_markup (_("<b>a</b>"));
+ _averaged[i].show_all();
+ __averaged[i] = (i == SS_FILL)? (_("Fill is averaged over selected objects")) : (_("Stroke is averaged over selected objects"));
+
+ // TRANSLATORS: M means "Multiple"
+ _multiple[i].set_markup (_("<b>m</b>"));
+ _multiple[i].show_all();
+ __multiple[i] = (i == SS_FILL)? (_("Multiple selected objects have the same fill")) : (_("Multiple selected objects have the same stroke"));
+
+ _popup_edit[i].add(*(new Gtk::Label((i == SS_FILL)? _("Edit fill...") : _("Edit stroke..."), Gtk::ALIGN_START)));
+ _popup_edit[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_edit : &SelectedStyle::on_stroke_edit ));
+
+ _popup_lastused[i].add(*(new Gtk::Label(_("Last set color"), Gtk::ALIGN_START)));
+ _popup_lastused[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_lastused : &SelectedStyle::on_stroke_lastused ));
+
+ _popup_lastselected[i].add(*(new Gtk::Label(_("Last selected color"), Gtk::ALIGN_START)));
+ _popup_lastselected[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_lastselected : &SelectedStyle::on_stroke_lastselected ));
+
+ _popup_invert[i].add(*(new Gtk::Label(_("Invert"), Gtk::ALIGN_START)));
+ _popup_invert[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_invert : &SelectedStyle::on_stroke_invert ));
+
+ _popup_white[i].add(*(new Gtk::Label(_("White"), Gtk::ALIGN_START)));
+ _popup_white[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_white : &SelectedStyle::on_stroke_white ));
+
+ _popup_black[i].add(*(new Gtk::Label(_("Black"), Gtk::ALIGN_START)));
+ _popup_black[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_black : &SelectedStyle::on_stroke_black ));
+
+ _popup_copy[i].add(*(new Gtk::Label(_("Copy color"), Gtk::ALIGN_START)));
+ _popup_copy[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_copy : &SelectedStyle::on_stroke_copy ));
+
+ _popup_paste[i].add(*(new Gtk::Label(_("Paste color"), Gtk::ALIGN_START)));
+ _popup_paste[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_paste : &SelectedStyle::on_stroke_paste ));
+
+ _popup_swap[i].add(*(new Gtk::Label(_("Swap fill and stroke"), Gtk::ALIGN_START)));
+ _popup_swap[i].signal_activate().connect(sigc::mem_fun(*this,
+ &SelectedStyle::on_fillstroke_swap));
+
+ _popup_opaque[i].add(*(new Gtk::Label((i == SS_FILL)? _("Make fill opaque") : _("Make stroke opaque"), Gtk::ALIGN_START)));
+ _popup_opaque[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_opaque : &SelectedStyle::on_stroke_opaque ));
+
+ //TRANSLATORS COMMENT: unset is a verb here
+ _popup_unset[i].add(*(new Gtk::Label((i == SS_FILL)? _("Unset fill") : _("Unset stroke"), Gtk::ALIGN_START)));
+ _popup_unset[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_unset : &SelectedStyle::on_stroke_unset ));
+
+ _popup_remove[i].add(*(new Gtk::Label((i == SS_FILL)? _("Remove fill") : _("Remove stroke"), Gtk::ALIGN_START)));
+ _popup_remove[i].signal_activate().connect(sigc::mem_fun(*this,
+ (i == SS_FILL)? &SelectedStyle::on_fill_remove : &SelectedStyle::on_stroke_remove ));
+
+ _popup[i].attach(_popup_edit[i], 0,1, 0,1);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 1,2);
+ _popup[i].attach(_popup_lastused[i], 0,1, 2,3);
+ _popup[i].attach(_popup_lastselected[i], 0,1, 3,4);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 4,5);
+ _popup[i].attach(_popup_invert[i], 0,1, 5,6);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 6,7);
+ _popup[i].attach(_popup_white[i], 0,1, 7,8);
+ _popup[i].attach(_popup_black[i], 0,1, 8,9);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 9,10);
+ _popup[i].attach(_popup_copy[i], 0,1, 10,11);
+ _popup_copy[i].set_sensitive(false);
+ _popup[i].attach(_popup_paste[i], 0,1, 11,12);
+ _popup[i].attach(_popup_swap[i], 0,1, 12,13);
+ _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 13,14);
+ _popup[i].attach(_popup_opaque[i], 0,1, 14,15);
+ _popup[i].attach(_popup_unset[i], 0,1, 15,16);
+ _popup[i].attach(_popup_remove[i], 0,1, 16,17);
+ _popup[i].show_all();
+
+ _mode[i] = SS_NA;
+ }
+
+ {
+ int row = 0;
+
+ Inkscape::Util::UnitTable::UnitMap m = unit_table.units(Inkscape::Util::UNIT_TYPE_LINEAR);
+ Inkscape::Util::UnitTable::UnitMap::iterator iter = m.begin();
+ while(iter != m.end()) {
+ Gtk::RadioMenuItem *mi = Gtk::manage(new Gtk::RadioMenuItem(_sw_group));
+ mi->add(*(new Gtk::Label(iter->first, Gtk::ALIGN_START)));
+ _unit_mis.push_back(mi);
+ Inkscape::Util::Unit const *u = unit_table.getUnit(iter->first);
+ mi->signal_activate().connect(sigc::bind<Inkscape::Util::Unit const *>(sigc::mem_fun(*this, &SelectedStyle::on_popup_units), u));
+ _popup_sw.attach(*mi, 0,1, row, row+1);
+ row++;
+ ++iter;
+ }
+
+ _popup_sw.attach(*(new Gtk::SeparatorMenuItem()), 0,1, row, row+1);
+ row++;
+
+ for (guint i = 0; i < G_N_ELEMENTS(_sw_presets_str); ++i) {
+ Gtk::MenuItem *mi = Gtk::manage(new Gtk::MenuItem());
+ mi->add(*(new Gtk::Label(_sw_presets_str[i], Gtk::ALIGN_START)));
+ mi->signal_activate().connect(sigc::bind<int>(sigc::mem_fun(*this, &SelectedStyle::on_popup_preset), i));
+ _popup_sw.attach(*mi, 0,1, row, row+1);
+ row++;
+ }
+
+ _popup_sw.attach(*(new Gtk::SeparatorMenuItem()), 0,1, row, row+1);
+ row++;
+
+ _popup_sw_remove.add(*(new Gtk::Label(_("Remove"), Gtk::ALIGN_START)));
+ _popup_sw_remove.signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::on_stroke_remove));
+ _popup_sw.attach(_popup_sw_remove, 0,1, row, row+1);
+ row++;
+
+ _popup_sw.show_all();
+ }
+ // fill row
+ _fill_flag_place.set_size_request(SELECTED_STYLE_FLAG_WIDTH , -1);
+
+ _fill_place.add(_na[SS_FILL]);
+ _fill_place.set_tooltip_text(__na[SS_FILL]);
+ _fill.set_size_request(SELECTED_STYLE_PLACE_WIDTH, -1);
+ _fill.pack_start(_fill_place, Gtk::PACK_EXPAND_WIDGET);
+
+ _fill_empty_space.set_size_request(SELECTED_STYLE_STROKE_WIDTH);
+
+ // stroke row
+ _stroke_flag_place.set_size_request(SELECTED_STYLE_FLAG_WIDTH, -1);
+
+ _stroke_place.add(_na[SS_STROKE]);
+ _stroke_place.set_tooltip_text(__na[SS_STROKE]);
+ _stroke.set_size_request(SELECTED_STYLE_PLACE_WIDTH, -1);
+ _stroke.pack_start(_stroke_place, Gtk::PACK_EXPAND_WIDGET);
+
+ _stroke_width_place.add(_stroke_width);
+ _stroke_width_place.set_size_request(SELECTED_STYLE_STROKE_WIDTH);
+
+ // opacity selector
+ _opacity_place.add(_opacity_label);
+
+ _opacity_sb.set_adjustment(_opacity_adjustment);
+ _opacity_sb.set_size_request (SELECTED_STYLE_SB_WIDTH, -1);
+ _opacity_sb.set_sensitive (false);
+
+ // arrange in table
+ _table.attach(_fill_label, 0, 0, 1, 1);
+ _table.attach(_stroke_label, 0, 1, 1, 1);
+
+ _table.attach(_fill_flag_place, 1, 0, 1, 1);
+ _table.attach(_stroke_flag_place, 1, 1, 1, 1);
+
+ _table.attach(_fill, 2, 0, 1, 1);
+ _table.attach(_stroke, 2, 1, 1, 1);
+
+ _table.attach(_fill_empty_space, 3, 0, 1, 1);
+ _table.attach(_stroke_width_place, 3, 1, 1, 1);
+
+ _table.attach(_opacity_place, 4, 0, 1, 2);
+ _table.attach(_opacity_sb, 5, 0, 1, 2);
+
+ pack_start(_table, true, true, 2);
+
+ set_size_request (SELECTED_STYLE_WIDTH, -1);
+
+ _drop[SS_FILL] = new DropTracker();
+ ((DropTracker*)_drop[SS_FILL])->parent = this;
+ ((DropTracker*)_drop[SS_FILL])->item = SS_FILL;
+
+ _drop[SS_STROKE] = new DropTracker();
+ ((DropTracker*)_drop[SS_STROKE])->parent = this;
+ ((DropTracker*)_drop[SS_STROKE])->item = SS_STROKE;
+
+ g_signal_connect(_stroke_place.gobj(),
+ "drag_data_received",
+ G_CALLBACK(dragDataReceived),
+ _drop[SS_STROKE]);
+
+ g_signal_connect(_fill_place.gobj(),
+ "drag_data_received",
+ G_CALLBACK(dragDataReceived),
+ _drop[SS_FILL]);
+
+ _fill_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_fill_click));
+ _stroke_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_stroke_click));
+ _opacity_place.signal_button_press_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_click));
+ _stroke_width_place.signal_button_press_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_sw_click));
+ _stroke_width_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_sw_click));
+ _opacity_sb.signal_populate_popup().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_menu));
+ _opacity_sb.signal_value_changed().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_changed));
+}
+
+SelectedStyle::~SelectedStyle()
+{
+ selection_changed_connection->disconnect();
+ delete selection_changed_connection;
+ selection_modified_connection->disconnect();
+ delete selection_modified_connection;
+ subselection_changed_connection->disconnect();
+ delete subselection_changed_connection;
+ _unit_mis.clear();
+
+ _fill_place.remove();
+ _stroke_place.remove();
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ delete _color_preview[i];
+ }
+
+ delete (DropTracker*)_drop[SS_FILL];
+ delete (DropTracker*)_drop[SS_STROKE];
+}
+
+void
+SelectedStyle::setDesktop(SPDesktop *desktop)
+{
+ _desktop = desktop;
+
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ selection_changed_connection = new sigc::connection (selection->connectChanged(
+ sigc::bind (
+ sigc::ptr_fun(&ss_selection_changed),
+ this )
+ ));
+ selection_modified_connection = new sigc::connection (selection->connectModified(
+ sigc::bind (
+ sigc::ptr_fun(&ss_selection_modified),
+ this )
+ ));
+ subselection_changed_connection = new sigc::connection (desktop->connectToolSubselectionChanged(
+ sigc::bind (
+ sigc::ptr_fun(&ss_subselection_changed),
+ this )
+ ));
+
+ _sw_unit = desktop->getNamedView()->display_units;
+
+ // Set the doc default unit active in the units list
+ for ( auto mi:_unit_mis ) {
+ if (mi && mi->get_label() == _sw_unit->abbr) {
+ mi->set_active();
+ break;
+ }
+ }
+}
+
+void SelectedStyle::dragDataReceived( GtkWidget */*widget*/,
+ GdkDragContext */*drag_context*/,
+ gint /*x*/, gint /*y*/,
+ GtkSelectionData *data,
+ guint /*info*/,
+ guint /*event_time*/,
+ gpointer user_data )
+{
+ DropTracker* tracker = (DropTracker*)user_data;
+
+ // copied from drag-and-drop.cpp, case APP_OSWB_COLOR
+ bool worked = false;
+ Glib::ustring colorspec;
+ if (gtk_selection_data_get_format(data) == 8) {
+ ege::PaintDef color;
+ worked = color.fromMIMEData("application/x-oswb-color",
+ reinterpret_cast<char const *>(gtk_selection_data_get_data(data)),
+ gtk_selection_data_get_length(data),
+ gtk_selection_data_get_format(data));
+ if (worked) {
+ if (color.getType() == ege::PaintDef::CLEAR) {
+ colorspec = ""; // TODO check if this is sufficient
+ } else if (color.getType() == ege::PaintDef::NONE) {
+ colorspec = "none";
+ } else {
+ unsigned int r = color.getR();
+ unsigned int g = color.getG();
+ unsigned int b = color.getB();
+
+ gchar* tmp = g_strdup_printf("#%02x%02x%02x", r, g, b);
+ colorspec = tmp;
+ g_free(tmp);
+ }
+ }
+ }
+ if (worked) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, (tracker->item == SS_FILL) ? "fill":"stroke", colorspec.c_str());
+
+ sp_desktop_set_style(tracker->parent->_desktop, css);
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(tracker->parent->_desktop->getDocument(), _("Drop color"), "");
+ }
+}
+
+void SelectedStyle::on_fill_remove() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "fill", "none");
+ sp_desktop_set_style (_desktop, css, true, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Remove fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_stroke_remove() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "stroke", "none");
+ sp_desktop_set_style (_desktop, css, true, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Remove stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_fill_unset() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_unset_property (css, "fill");
+ sp_desktop_set_style (_desktop, css, true, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Unset fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+}
+
+void SelectedStyle::on_stroke_unset() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ 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, true, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Unset stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_fill_opaque() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "fill-opacity", "1");
+ sp_desktop_set_style (_desktop, css, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Make fill opaque"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_stroke_opaque() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "stroke-opacity", "1");
+ sp_desktop_set_style (_desktop, css, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Make fill opaque"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_fill_lastused() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ guint32 color = sp_desktop_get_color(_desktop, true);
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), color);
+ sp_repr_css_set_property (css, "fill", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Apply last set color to fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_stroke_lastused() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ guint32 color = sp_desktop_get_color(_desktop, false);
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), color);
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Apply last set color to stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_fill_lastselected() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), _lastselected[SS_FILL]);
+ sp_repr_css_set_property (css, "fill", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Apply last selected color to fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_stroke_lastselected() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), _lastselected[SS_STROKE]);
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Apply last selected color to stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_fill_invert() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ guint32 color = _thisselected[SS_FILL];
+ gchar c[64];
+ if (_mode[SS_FILL] == SS_LGRADIENT || _mode[SS_FILL] == SS_RGRADIENT) {
+ sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_FILL);
+ return;
+
+ }
+
+ if (_mode[SS_FILL] != SS_COLOR) return;
+ sp_svg_write_color (c, sizeof(c),
+ SP_RGBA32_U_COMPOSE(
+ (255 - SP_RGBA32_R_U(color)),
+ (255 - SP_RGBA32_G_U(color)),
+ (255 - SP_RGBA32_B_U(color)),
+ SP_RGBA32_A_U(color)
+ )
+ );
+ sp_repr_css_set_property (css, "fill", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Invert fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_stroke_invert() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ guint32 color = _thisselected[SS_STROKE];
+ gchar c[64];
+ if (_mode[SS_STROKE] == SS_LGRADIENT || _mode[SS_STROKE] == SS_RGRADIENT) {
+ sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_STROKE);
+ return;
+ }
+ if (_mode[SS_STROKE] != SS_COLOR) return;
+ sp_svg_write_color (c, sizeof(c),
+ SP_RGBA32_U_COMPOSE(
+ (255 - SP_RGBA32_R_U(color)),
+ (255 - SP_RGBA32_G_U(color)),
+ (255 - SP_RGBA32_B_U(color)),
+ SP_RGBA32_A_U(color)
+ )
+ );
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Invert stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_fill_white() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), 0xffffffff);
+ sp_repr_css_set_property (css, "fill", c);
+ sp_repr_css_set_property (css, "fill-opacity", "1");
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("White fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_stroke_white() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), 0xffffffff);
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_repr_css_set_property (css, "stroke-opacity", "1");
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("White stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_fill_black() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), 0x000000ff);
+ sp_repr_css_set_property (css, "fill", c);
+ sp_repr_css_set_property (css, "fill-opacity", "1.0");
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Black fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_stroke_black() {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), 0x000000ff);
+ sp_repr_css_set_property (css, "stroke", c);
+ sp_repr_css_set_property (css, "stroke-opacity", "1.0");
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Black stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+}
+
+void SelectedStyle::on_fill_copy() {
+ if (_mode[SS_FILL] == SS_COLOR) {
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), _thisselected[SS_FILL]);
+ Glib::ustring text;
+ text += c;
+ if (!text.empty()) {
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ refClipboard->set_text(text);
+ }
+ }
+}
+
+void SelectedStyle::on_stroke_copy() {
+ if (_mode[SS_STROKE] == SS_COLOR) {
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c), _thisselected[SS_STROKE]);
+ Glib::ustring text;
+ text += c;
+ if (!text.empty()) {
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ refClipboard->set_text(text);
+ }
+ }
+}
+
+void SelectedStyle::on_fill_paste() {
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ Glib::ustring const text = refClipboard->wait_for_text();
+
+ if (!text.empty()) {
+ guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity
+ if (color == 0x000000ff) // failed to parse color string
+ return;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "fill", text.c_str());
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Paste fill"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+}
+
+void SelectedStyle::on_stroke_paste() {
+ Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get();
+ Glib::ustring const text = refClipboard->wait_for_text();
+
+ if (!text.empty()) {
+ guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity
+ if (color == 0x000000ff) // failed to parse color string
+ return;
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "stroke", text.c_str());
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Paste stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+}
+
+void SelectedStyle::on_fillstroke_swap() {
+ _desktop->getSelection()->swapFillStroke();
+}
+
+void SelectedStyle::on_fill_edit() {
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageFill();
+}
+
+void SelectedStyle::on_stroke_edit() {
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageStrokePaint();
+}
+
+bool
+SelectedStyle::on_fill_click(GdkEventButton *event)
+{
+ if (event->button == 1) { // click, open fill&stroke
+
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageFill();
+
+ } else if (event->button == 3) { // right-click, popup menu
+ _popup[SS_FILL].popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ } else if (event->button == 2) { // middle click, toggle none/lastcolor
+ if (_mode[SS_FILL] == SS_NONE) {
+ on_fill_lastused();
+ } else {
+ on_fill_remove();
+ }
+ }
+ return true;
+}
+
+bool
+SelectedStyle::on_stroke_click(GdkEventButton *event)
+{
+ if (event->button == 1) { // click, open fill&stroke
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageStrokePaint();
+ } else if (event->button == 3) { // right-click, popup menu
+ _popup[SS_STROKE].popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ } else if (event->button == 2) { // middle click, toggle none/lastcolor
+ if (_mode[SS_STROKE] == SS_NONE) {
+ on_stroke_lastused();
+ } else {
+ on_stroke_remove();
+ }
+ }
+ return true;
+}
+
+bool
+SelectedStyle::on_sw_click(GdkEventButton *event)
+{
+ if (event->button == 1) { // click, open fill&stroke
+ if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop))
+ fs->showPageStrokeStyle();
+ } else if (event->button == 3) { // right-click, popup menu
+ _popup_sw.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ } else if (event->button == 2) { // middle click, toggle none/lastwidth?
+ //
+ }
+ return true;
+}
+
+bool
+SelectedStyle::on_opacity_click(GdkEventButton *event)
+{
+ if (event->button == 2) { // middle click
+ const char* opacity = _opacity_sb.get_value() < 50? "0.5" : (_opacity_sb.get_value() == 100? "0" : "1");
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ sp_repr_css_set_property (css, "opacity", opacity);
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Change opacity"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ return true;
+ }
+
+ return false;
+}
+
+void SelectedStyle::on_popup_units(Inkscape::Util::Unit const *unit) {
+ _sw_unit = unit;
+ update();
+}
+
+void SelectedStyle::on_popup_preset(int i) {
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ gdouble w;
+ if (_sw_unit) {
+ w = Inkscape::Util::Quantity::convert(_sw_presets[i], _sw_unit, "px");
+ } else {
+ w = _sw_presets[i];
+ }
+ Inkscape::CSSOStringStream os;
+ os << w;
+ sp_repr_css_set_property (css, "stroke-width", os.str().c_str());
+ // FIXME: update dash patterns!
+ sp_desktop_set_style (_desktop, css, true);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::done(_desktop->getDocument(), _("Change stroke width"), INKSCAPE_ICON("swatches"));
+}
+
+void
+SelectedStyle::update()
+{
+ if (_desktop == nullptr)
+ return;
+
+ // create temporary style
+ SPStyle query(_desktop->getDocument());
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ Gtk::EventBox *place = (i == SS_FILL)? &_fill_place : &_stroke_place;
+ Gtk::EventBox *flag_place = (i == SS_FILL)? &_fill_flag_place : &_stroke_flag_place;
+
+ place->remove();
+ flag_place->remove();
+
+ clearTooltip(*place);
+ clearTooltip(*flag_place);
+
+ _mode[i] = SS_NA;
+ _paintserver_id[i].clear();
+
+ _popup_copy[i].set_sensitive(false);
+
+ // query style from desktop. 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,
+ (i == SS_FILL)? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE);
+ switch (result) {
+ case QUERY_STYLE_NOTHING:
+ place->add(_na[i]);
+ place->set_tooltip_text(__na[i]);
+ _mode[i] = SS_NA;
+ if ( _dropEnabled[i] ) {
+ gtk_drag_dest_unset( GTK_WIDGET((i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()) );
+ _dropEnabled[i] = false;
+ }
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ if ( !_dropEnabled[i] ) {
+ gtk_drag_dest_set( GTK_WIDGET( (i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()),
+ GTK_DEST_DEFAULT_ALL,
+ ui_drop_target_entries,
+ nui_drop_target_entries,
+ GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE) );
+ _dropEnabled[i] = true;
+ }
+ SPIPaint *paint;
+ if (i == SS_FILL) {
+ paint = &(query.fill);
+ } else {
+ paint = &(query.stroke);
+ }
+ if (paint->set && paint->isPaintserver()) {
+ SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (&query) : SP_STYLE_STROKE_SERVER (&query);
+ if ( server ) {
+ Inkscape::XML::Node *srepr = server->getRepr();
+ _paintserver_id[i] += "url(#";
+ _paintserver_id[i] += srepr->attribute("id");
+ _paintserver_id[i] += ")";
+
+ if (SP_IS_LINEARGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ _gradient_preview_l[i]->set_gradient(vector);
+ place->add(_gradient_box_l[i]);
+ place->set_tooltip_text(__lgradient[i]);
+ _mode[i] = SS_LGRADIENT;
+ } else if (SP_IS_RADIALGRADIENT(server)) {
+ SPGradient *vector = SP_GRADIENT(server)->getVector();
+ _gradient_preview_r[i]->set_gradient(vector);
+ place->add(_gradient_box_r[i]);
+ place->set_tooltip_text(__rgradient[i]);
+ _mode[i] = SS_RGRADIENT;
+#ifdef WITH_MESH
+ } else if (SP_IS_MESHGRADIENT(server)) {
+ SPGradient *array = SP_GRADIENT(server)->getArray();
+ _gradient_preview_m[i]->set_gradient(array);
+ place->add(_gradient_box_m[i]);
+ place->set_tooltip_text(__mgradient[i]);
+ _mode[i] = SS_MGRADIENT;
+#endif
+ } else if (SP_IS_PATTERN(server)) {
+ place->add(_pattern[i]);
+ place->set_tooltip_text(__pattern[i]);
+ _mode[i] = SS_PATTERN;
+ } else if (SP_IS_HATCH(server)) {
+ place->add(_hatch[i]);
+ place->set_tooltip_text(__hatch[i]);
+ _mode[i] = SS_HATCH;
+ }
+ } else {
+ g_warning ("file %s: line %d: Unknown paint server", __FILE__, __LINE__);
+ }
+ } else if (paint->set && paint->isColor()) {
+ guint32 color = paint->value.color.toRGBA32(
+ SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query.fill_opacity.value : query.stroke_opacity.value));
+ _lastselected[i] = _thisselected[i];
+ _thisselected[i] = color; // include opacity
+ ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color);
+ _color_preview[i]->show_all();
+ place->add(*_color_preview[i]);
+ gchar c_string[64];
+ g_snprintf (c_string, 64, "%06x/%.3g", color >> 8, SP_RGBA32_A_F(color));
+ place->set_tooltip_text(__color[i] + ": " + c_string + _(", drag to adjust, middle-click to remove"));
+ _mode[i] = SS_COLOR;
+ _popup_copy[i].set_sensitive(true);
+
+ } else if (paint->set && paint->isNone()) {
+ place->add(_none[i]);
+ place->set_tooltip_text(__none[i]);
+ _mode[i] = SS_NONE;
+ } else if (!paint->set) {
+ place->add(_unset[i]);
+ place->set_tooltip_text(__unset[i]);
+ _mode[i] = SS_UNSET;
+ }
+ if (result == QUERY_STYLE_MULTIPLE_AVERAGED) {
+ flag_place->add(_averaged[i]);
+ flag_place->set_tooltip_text(__averaged[i]);
+ } else if (result == QUERY_STYLE_MULTIPLE_SAME) {
+ flag_place->add(_multiple[i]);
+ flag_place->set_tooltip_text(__multiple[i]);
+ }
+ break;
+ case QUERY_STYLE_MULTIPLE_DIFFERENT:
+ place->add(_many[i]);
+ place->set_tooltip_text(__many[i]);
+ _mode[i] = SS_MANY;
+ break;
+ default:
+ break;
+ }
+ }
+
+// Now query opacity
+ clearTooltip(_opacity_place);
+ clearTooltip(_opacity_sb);
+
+ int result = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_MASTEROPACITY);
+
+ switch (result) {
+ case QUERY_STYLE_NOTHING:
+ _opacity_place.set_tooltip_text(_("Nothing selected"));
+ _opacity_sb.set_tooltip_text(_("Nothing selected"));
+ _opacity_sb.set_sensitive(false);
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ _opacity_place.set_tooltip_text(_("Opacity (%)"));
+ _opacity_sb.set_tooltip_text(_("Opacity (%)"));
+ if (_opacity_blocked) break;
+ _opacity_blocked = true;
+ _opacity_sb.set_sensitive(true);
+ _opacity_adjustment->set_value(SP_SCALE24_TO_FLOAT(query.opacity.value) * 100);
+ _opacity_blocked = false;
+ break;
+ }
+
+// Now query stroke_width
+ int result_sw = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH);
+ switch (result_sw) {
+ case QUERY_STYLE_NOTHING:
+ _stroke_width.set_markup("");
+ current_stroke_width = 0;
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ {
+ if (query.stroke_extensions.hairline) {
+ _stroke_width.set_markup(_("Hairline"));
+ auto str = Glib::ustring::compose(_("Stroke width: %1"), _("Hairline"));
+ _stroke_width_place.set_tooltip_text(str);
+ } else {
+ double w;
+ if (_sw_unit) {
+ w = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", _sw_unit);
+ } else {
+ w = query.stroke_width.computed;
+ }
+ current_stroke_width = w;
+
+ {
+ gchar *str = g_strdup_printf(" %#.3g", w);
+ if (str[strlen(str) - 1] == ',' || str[strlen(str) - 1] == '.') {
+ str[strlen(str)-1] = '\0';
+ }
+ _stroke_width.set_markup(str);
+ g_free (str);
+ }
+ {
+ gchar *str = g_strdup_printf(_("Stroke width: %.5g%s%s"),
+ w,
+ _sw_unit? _sw_unit->abbr.c_str() : "px",
+ (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED)?
+ _(" (averaged)") : "");
+ _stroke_width_place.set_tooltip_text(str);
+ g_free (str);
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+void SelectedStyle::opacity_0() {_opacity_sb.set_value(0);}
+void SelectedStyle::opacity_025() {_opacity_sb.set_value(25);}
+void SelectedStyle::opacity_05() {_opacity_sb.set_value(50);}
+void SelectedStyle::opacity_075() {_opacity_sb.set_value(75);}
+void SelectedStyle::opacity_1() {_opacity_sb.set_value(100);}
+
+void SelectedStyle::on_opacity_menu (Gtk::Menu *menu) {
+
+ std::vector<Gtk::Widget *> children = menu->get_children();
+ for (auto iter : children) {
+ menu->remove(*iter);
+ }
+
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label(_("0 (transparent)"), Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_0 ));
+ menu->add(*item);
+ }
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label("25%", Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_025 ));
+ menu->add(*item);
+ }
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label("50%", Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_05 ));
+ menu->add(*item);
+ }
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label("75%", Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_075 ));
+ menu->add(*item);
+ }
+ {
+ Gtk::MenuItem *item = new Gtk::MenuItem;
+ item->add(*(new Gtk::Label(_("100% (opaque)"), Gtk::ALIGN_START, Gtk::ALIGN_START)));
+ item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_1 ));
+ menu->add(*item);
+ }
+
+ menu->show_all();
+}
+
+void SelectedStyle::on_opacity_changed ()
+{
+ g_return_if_fail(_desktop); // TODO this shouldn't happen!
+ if (_opacity_blocked)
+ return;
+ _opacity_blocked = true;
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ Inkscape::CSSOStringStream os;
+ os << CLAMP ((_opacity_adjustment->get_value() / 100), 0.0, 1.0);
+ sp_repr_css_set_property (css, "opacity", os.str().c_str());
+ sp_desktop_set_style (_desktop, css);
+ sp_repr_css_attr_unref (css);
+ DocumentUndo::maybeDone(_desktop->getDocument(), "fillstroke:opacity", _("Change opacity"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ _opacity_blocked = false;
+}
+
+/* ============================================= RotateableSwatch */
+
+RotateableSwatch::RotateableSwatch(SelectedStyle *parent, guint mode)
+ : fillstroke(mode)
+ , parent(parent)
+{
+}
+
+RotateableSwatch::~RotateableSwatch() = default;
+
+double
+RotateableSwatch::color_adjust(float *hsla, double by, guint32 cc, guint modifier)
+{
+ SPColor::rgb_to_hsl_floatv (hsla, SP_RGBA32_R_F(cc), SP_RGBA32_G_F(cc), SP_RGBA32_B_F(cc));
+ hsla[3] = SP_RGBA32_A_F(cc);
+ double diff = 0;
+ if (modifier == 2) { // saturation
+ double old = hsla[1];
+ if (by > 0) {
+ hsla[1] += by * (1 - hsla[1]);
+ } else {
+ hsla[1] += by * (hsla[1]);
+ }
+ diff = hsla[1] - old;
+ } else if (modifier == 1) { // lightness
+ double old = hsla[2];
+ if (by > 0) {
+ hsla[2] += by * (1 - hsla[2]);
+ } else {
+ hsla[2] += by * (hsla[2]);
+ }
+ diff = hsla[2] - old;
+ } else if (modifier == 3) { // alpha
+ double old = hsla[3];
+ hsla[3] += by/2;
+ if (hsla[3] < 0) {
+ hsla[3] = 0;
+ } else if (hsla[3] > 1) {
+ hsla[3] = 1;
+ }
+ diff = hsla[3] - old;
+ } else { // hue
+ double old = hsla[0];
+ hsla[0] += by/2;
+ while (hsla[0] < 0)
+ hsla[0] += 1;
+ while (hsla[0] > 1)
+ hsla[0] -= 1;
+ diff = hsla[0] - old;
+ }
+
+ float rgb[3];
+ SPColor::hsl_to_rgb_floatv (rgb, hsla[0], hsla[1], hsla[2]);
+
+ gchar c[64];
+ sp_svg_write_color (c, sizeof(c),
+ SP_RGBA32_U_COMPOSE(
+ (SP_COLOR_F_TO_U(rgb[0])),
+ (SP_COLOR_F_TO_U(rgb[1])),
+ (SP_COLOR_F_TO_U(rgb[2])),
+ 0xff
+ )
+ );
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+
+ if (modifier == 3) { // alpha
+ Inkscape::CSSOStringStream osalpha;
+ osalpha << hsla[3];
+ sp_repr_css_set_property(css, (fillstroke == SS_FILL) ? "fill-opacity" : "stroke-opacity", osalpha.str().c_str());
+ } else {
+ sp_repr_css_set_property (css, (fillstroke == SS_FILL) ? "fill" : "stroke", c);
+ }
+ sp_desktop_set_style (parent->getDesktop(), css);
+ sp_repr_css_attr_unref (css);
+ return diff;
+}
+
+void
+RotateableSwatch::do_motion(double by, guint modifier) {
+ if (parent->_mode[fillstroke] != SS_COLOR)
+ return;
+
+ if (!scrolling && !cr_set) {
+
+ std::string cursor_filename = "adjust_hue.svg";
+ if (modifier == 2) {
+ cursor_filename = "adjust_saturation.svg";
+ } else if (modifier == 1) {
+ cursor_filename = "adjust_lightness.svg";
+ } else if (modifier == 3) {
+ cursor_filename = "adjust_alpha.svg";
+ }
+
+ auto window = get_window();
+ auto cursor = load_svg_cursor(get_display(), window, cursor_filename);
+ get_window()->set_cursor(cursor);
+ }
+
+ guint32 cc;
+ if (!startcolor_set) {
+ cc = startcolor = parent->_thisselected[fillstroke];
+ startcolor_set = true;
+ } else {
+ cc = startcolor;
+ }
+
+ float hsla[4];
+ double diff = 0;
+
+ diff = color_adjust(hsla, by, cc, modifier);
+
+ if (modifier == 3) { // alpha
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust alpha")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ double ch = hsla[3];
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>alpha</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Ctrl</b> to adjust lightness, with <b>Shift</b> to adjust saturation, without modifiers to adjust hue"), ch - diff, ch, diff);
+
+ } else if (modifier == 2) { // saturation
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust saturation")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ double ch = hsla[1];
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>saturation</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Ctrl</b> to adjust lightness, with <b>Alt</b> to adjust alpha, without modifiers to adjust hue"), ch - diff, ch, diff);
+
+ } else if (modifier == 1) { // lightness
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust lightness")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ double ch = hsla[2];
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>lightness</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Shift</b> to adjust saturation, with <b>Alt</b> to adjust alpha, without modifiers to adjust hue"), ch - diff, ch, diff);
+
+ } else { // hue
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust hue")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ double ch = hsla[0];
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>hue</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Shift</b> to adjust saturation, with <b>Alt</b> to adjust alpha, with <b>Ctrl</b> to adjust lightness"), ch - diff, ch, diff);
+ }
+}
+
+
+void
+RotateableSwatch::do_scroll(double by, guint modifier) {
+ do_motion(by/30.0, modifier);
+ do_release(by/30.0, modifier);
+}
+
+void
+RotateableSwatch::do_release(double by, guint modifier) {
+ if (parent->_mode[fillstroke] != SS_COLOR)
+ return;
+
+ float hsla[4];
+ color_adjust(hsla, by, startcolor, modifier);
+
+ if (cr_set) {
+ get_window()->set_cursor(); // Use parent window cursor.
+ cr_set = false;
+ }
+
+ if (modifier == 3) { // alpha
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust alpha"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ } else if (modifier == 2) { // saturation
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust saturation"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ } else if (modifier == 1) { // lightness
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust lightness"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ } else { // hue
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, ("Adjust hue"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+
+ if (!strcmp(undokey, "ssrot1")) {
+ undokey = "ssrot2";
+ } else {
+ undokey = "ssrot1";
+ }
+
+ parent->getDesktop()->event_context->message_context->clear();
+ startcolor_set = false;
+}
+
+/* ============================================= RotateableStrokeWidth */
+
+RotateableStrokeWidth::RotateableStrokeWidth(SelectedStyle *parent) :
+ parent(parent),
+ startvalue(0),
+ startvalue_set(false),
+ undokey("swrot1")
+{
+}
+
+RotateableStrokeWidth::~RotateableStrokeWidth() = default;
+
+double
+RotateableStrokeWidth::value_adjust(double current, double by, guint /*modifier*/, bool final)
+{
+ double newval;
+ // by is -1..1
+ double max_f = 50; // maximum width is (current * max_f), minimum - zero
+ newval = current * (std::exp(std::log(max_f-1) * (by+1)) - 1) / (max_f-2);
+
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+ if (final && newval < 1e-6) {
+ // if dragged into zero and this is the final adjust on mouse release, delete stroke;
+ // if it's not final, leave it a chance to increase again (which is not possible with "none")
+ sp_repr_css_set_property (css, "stroke", "none");
+ } else {
+ newval = Inkscape::Util::Quantity::convert(newval, parent->_sw_unit, "px");
+ Inkscape::CSSOStringStream os;
+ os << newval;
+ sp_repr_css_set_property (css, "stroke-width", os.str().c_str());
+ }
+
+ sp_desktop_set_style (parent->getDesktop(), css);
+ sp_repr_css_attr_unref (css);
+ return newval - current;
+}
+
+void
+RotateableStrokeWidth::do_motion(double by, guint modifier) {
+
+ // if this is the first motion after a mouse grab, remember the current width
+ if (!startvalue_set) {
+ startvalue = parent->current_stroke_width;
+ // if it's 0, adjusting (which uses multiplication) will not be able to change it, so we
+ // cheat and provide a non-zero value
+ if (startvalue == 0)
+ startvalue = 1;
+ startvalue_set = true;
+ }
+
+ if (modifier == 3) { // Alt, do nothing
+ } else {
+ double diff = value_adjust(startvalue, by, modifier, false);
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust stroke width")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>stroke width</b>: was %.3g, now <b>%.3g</b> (diff %.3g)"), startvalue, startvalue + diff, diff);
+ }
+}
+
+void
+RotateableStrokeWidth::do_release(double by, guint modifier) {
+
+ if (modifier == 3) { // do nothing
+
+ } else {
+ value_adjust(startvalue, by, modifier, true);
+ startvalue_set = false;
+ DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, (_("Adjust stroke width")), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+
+ if (!strcmp(undokey, "swrot1")) {
+ undokey = "swrot2";
+ } else {
+ undokey = "swrot1";
+ }
+ parent->getDesktop()->event_context->message_context->clear();
+}
+
+void
+RotateableStrokeWidth::do_scroll(double by, guint modifier) {
+ do_motion(by/10.0, modifier);
+ startvalue_set = false;
+}
+
+Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop)
+{
+ desktop->getContainer()->new_dialog("FillStroke");
+ return dynamic_cast<Dialog::FillAndStroke *>(desktop->getContainer()->get_dialog("FillStroke"));
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/selected-style.h b/src/ui/widget/selected-style.h
new file mode 100644
index 0000000..0ad002b
--- /dev/null
+++ b/src/ui/widget/selected-style.h
@@ -0,0 +1,302 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * buliabyak@gmail.com
+ * scislac@users.sf.net
+ *
+ * Copyright (C) 2005 authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_CURRENT_STYLE_H
+#define INKSCAPE_UI_CURRENT_STYLE_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/grid.h>
+
+#include <gtkmm/label.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/radiobuttongroup.h>
+#include <gtkmm/radiomenuitem.h>
+#include "ui/widget/spinbutton.h"
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+
+#include "rotateable.h"
+
+constexpr int SELECTED_STYLE_SB_WIDTH = 48;
+constexpr int SELECTED_STYLE_PLACE_WIDTH = 50;
+constexpr int SELECTED_STYLE_STROKE_WIDTH = 40;
+constexpr int SELECTED_STYLE_FLAG_WIDTH = 12;
+constexpr int SELECTED_STYLE_WIDTH = 250;
+
+class SPDesktop;
+
+namespace Inkscape {
+
+namespace Util {
+ class Unit;
+}
+
+namespace UI {
+namespace Widget {
+
+enum {
+ SS_NA,
+ SS_NONE,
+ SS_UNSET,
+ SS_PATTERN,
+ SS_LGRADIENT,
+ SS_RGRADIENT,
+#ifdef WITH_MESH
+ SS_MGRADIENT,
+#endif
+ SS_MANY,
+ SS_COLOR,
+ SS_HATCH
+};
+
+enum {
+ SS_FILL,
+ SS_STROKE
+};
+
+class GradientImage;
+class SelectedStyle;
+
+class RotateableSwatch : public Rotateable {
+ public:
+ RotateableSwatch(SelectedStyle *parent, guint mode);
+ ~RotateableSwatch() override;
+
+ double color_adjust (float *hsl, double by, guint32 cc, guint state);
+
+ void do_motion (double by, guint state) override;
+ void do_release (double by, guint state) override;
+ void do_scroll (double by, guint state) override;
+
+private:
+ guint fillstroke;
+
+ SelectedStyle *parent;
+
+ guint32 startcolor = 0;
+ bool startcolor_set = false;
+
+ gchar const *undokey = "ssrot1";
+
+ bool cr_set = false;
+};
+
+class RotateableStrokeWidth : public Rotateable {
+ public:
+ RotateableStrokeWidth(SelectedStyle *parent);
+ ~RotateableStrokeWidth() override;
+
+ double value_adjust(double current, double by, guint modifier, bool final);
+ void do_motion (double by, guint state) override;
+ void do_release (double by, guint state) override;
+ void do_scroll (double by, guint state) override;
+
+private:
+ SelectedStyle *parent;
+
+ double startvalue;
+ bool startvalue_set;
+
+ gchar const *undokey;
+};
+
+/**
+ * Selected style indicator (fill, stroke, opacity).
+ */
+class SelectedStyle : public Gtk::Box
+{
+public:
+ SelectedStyle(bool layout = true);
+
+ ~SelectedStyle() override;
+
+ void setDesktop(SPDesktop *desktop);
+ SPDesktop *getDesktop() {return _desktop;}
+ void update();
+
+ guint32 _lastselected[2];
+ guint32 _thisselected[2];
+
+ guint _mode[2];
+
+ double current_stroke_width;
+ Inkscape::Util::Unit const *_sw_unit; // points to object in UnitTable, do not delete
+
+protected:
+ SPDesktop *_desktop;
+
+ Gtk::Grid _table;
+
+ Gtk::Label _fill_label;
+ Gtk::Label _stroke_label;
+ Gtk::Label _opacity_label;
+
+ RotateableSwatch _fill_place;
+ RotateableSwatch _stroke_place;
+
+ Gtk::EventBox _fill_flag_place;
+ Gtk::EventBox _stroke_flag_place;
+
+ Gtk::EventBox _opacity_place;
+ Glib::RefPtr<Gtk::Adjustment> _opacity_adjustment;
+ Inkscape::UI::Widget::SpinButton _opacity_sb;
+
+ Gtk::Label _na[2];
+ Glib::ustring __na[2];
+
+ Gtk::Label _none[2];
+ Glib::ustring __none[2];
+
+ Gtk::Label _pattern[2];
+ Glib::ustring __pattern[2];
+
+ Gtk::Label _hatch[2];
+ Glib::ustring __hatch[2];
+
+ Gtk::Label _lgradient[2];
+ Glib::ustring __lgradient[2];
+
+ GradientImage *_gradient_preview_l[2];
+ Gtk::Box _gradient_box_l[2];
+
+ Gtk::Label _rgradient[2];
+ Glib::ustring __rgradient[2];
+
+ GradientImage *_gradient_preview_r[2];
+ Gtk::Box _gradient_box_r[2];
+
+#ifdef WITH_MESH
+ Gtk::Label _mgradient[2];
+ Glib::ustring __mgradient[2];
+
+ GradientImage *_gradient_preview_m[2];
+ Gtk::Box _gradient_box_m[2];
+#endif
+
+ Gtk::Label _many[2];
+ Glib::ustring __many[2];
+
+ Gtk::Label _unset[2];
+ Glib::ustring __unset[2];
+
+ Gtk::Widget *_color_preview[2];
+ Glib::ustring __color[2];
+
+ Gtk::Label _averaged[2];
+ Glib::ustring __averaged[2];
+ Gtk::Label _multiple[2];
+ Glib::ustring __multiple[2];
+
+ Gtk::Box _fill;
+ Gtk::Box _stroke;
+ RotateableStrokeWidth _stroke_width_place;
+ Gtk::Label _stroke_width;
+ Gtk::Label _fill_empty_space;
+
+ Glib::ustring _paintserver_id[2];
+
+ sigc::connection *selection_changed_connection;
+ sigc::connection *selection_modified_connection;
+ sigc::connection *subselection_changed_connection;
+
+ static void dragDataReceived( GtkWidget *widget,
+ GdkDragContext *drag_context,
+ gint x, gint y,
+ GtkSelectionData *data,
+ guint info,
+ guint event_time,
+ gpointer user_data );
+
+ bool on_fill_click(GdkEventButton *event);
+ bool on_stroke_click(GdkEventButton *event);
+ bool on_opacity_click(GdkEventButton *event);
+ bool on_sw_click(GdkEventButton *event);
+
+ bool _opacity_blocked;
+ void on_opacity_changed();
+ void on_opacity_menu(Gtk::Menu *menu);
+ void opacity_0();
+ void opacity_025();
+ void opacity_05();
+ void opacity_075();
+ void opacity_1();
+
+ void on_fill_remove();
+ void on_stroke_remove();
+ void on_fill_lastused();
+ void on_stroke_lastused();
+ void on_fill_lastselected();
+ void on_stroke_lastselected();
+ void on_fill_unset();
+ void on_stroke_unset();
+ void on_fill_edit();
+ void on_stroke_edit();
+ void on_fillstroke_swap();
+ void on_fill_invert();
+ void on_stroke_invert();
+ void on_fill_white();
+ void on_stroke_white();
+ void on_fill_black();
+ void on_stroke_black();
+ void on_fill_copy();
+ void on_stroke_copy();
+ void on_fill_paste();
+ void on_stroke_paste();
+ void on_fill_opaque();
+ void on_stroke_opaque();
+
+ Gtk::Menu _popup[2];
+ Gtk::MenuItem _popup_edit[2];
+ Gtk::MenuItem _popup_lastused[2];
+ Gtk::MenuItem _popup_lastselected[2];
+ Gtk::MenuItem _popup_invert[2];
+ Gtk::MenuItem _popup_white[2];
+ Gtk::MenuItem _popup_black[2];
+ Gtk::MenuItem _popup_copy[2];
+ Gtk::MenuItem _popup_paste[2];
+ Gtk::MenuItem _popup_swap[2];
+ Gtk::MenuItem _popup_opaque[2];
+ Gtk::MenuItem _popup_unset[2];
+ Gtk::MenuItem _popup_remove[2];
+
+ Gtk::Menu _popup_sw;
+ Gtk::RadioButtonGroup _sw_group;
+ std::vector<Gtk::RadioMenuItem*> _unit_mis;
+ void on_popup_units(Inkscape::Util::Unit const *u);
+ void on_popup_preset(int i);
+ Gtk::MenuItem _popup_sw_remove;
+
+ void *_drop[2];
+ bool _dropEnabled[2];
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_BUTTON_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/ui/widget/shapeicon.cpp b/src/ui/widget/shapeicon.cpp
new file mode 100644
index 0000000..6b7a9d9
--- /dev/null
+++ b/src/ui/widget/shapeicon.cpp
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2020 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include "color.h"
+#include "ui/widget/shapeicon.h"
+#include "ui/icon-loader.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/*
+ * This is a type of CellRenderer which you might expect to inherit from the
+ * pixbuf CellRenderer, but we actually need to write a Cairo surface directly
+ * in order to maintain HiDPI sharpness in icons. Upstream Gtk have made it clear
+ * that CellRenderers are going away in Gtk4 so they aren't interested in fixing
+ * rendering problems like the one in CellRendererPixbuf.
+ *
+ * See: https://gitlab.gnome.org/GNOME/gtk/-/issues/613
+ */
+
+void CellRendererItemIcon::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags)
+{
+ std::string shape_type = _property_shape_type.get_value();
+ std::string highlight = SPColor(_property_color.get_value()).toString();
+ std::string cache_id = shape_type + "-" + highlight;
+
+ // if the icon isn't cached, render it to a pixbuf
+ int scale = widget.get_scale_factor();
+ if ( !_icon_cache[cache_id] ) {
+ _icon_cache[cache_id] = sp_get_shape_icon(shape_type, Gdk::RGBA(highlight), _size, scale);
+ }
+ g_return_if_fail(_icon_cache[cache_id]);
+
+ // Center the icon in the cell area
+ int x = cell_area.get_x() + int((cell_area.get_width() - _size) * 0.5);
+ int y = cell_area.get_y() + int((cell_area.get_height() - _size) * 0.5);
+
+ // Paint the pixbuf to a cairo surface to get HiDPI support
+ paint_icon(cr, widget, _icon_cache[cache_id], x, y);
+
+ // Create an overlay icon
+ int clipmask = _property_clipmask.get_value();
+ if (clipmask > 0) {
+ if (!_clip_overlay) {
+ _clip_overlay = sp_get_icon_pixbuf("overlay-clip", Gtk::ICON_SIZE_MENU, scale);
+ }
+ if (!_mask_overlay) {
+ _mask_overlay = sp_get_icon_pixbuf("overlay-mask", Gtk::ICON_SIZE_MENU, scale);
+ }
+
+ if (clipmask == OVERLAY_CLIP && _clip_overlay) {
+ paint_icon(cr, widget, _clip_overlay, x, y);
+ }
+ if (clipmask == OVERLAY_MASK && _mask_overlay) {
+ paint_icon(cr, widget, _mask_overlay, x, y);
+ }
+ }
+
+}
+
+void CellRendererItemIcon::paint_icon(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ int x, int y)
+{
+ cairo_surface_t *surface = gdk_cairo_surface_create_from_pixbuf(
+ pixbuf->gobj(), 0, widget.get_window()->gobj());
+ if (!surface) return;
+ cairo_set_source_surface(cr->cobj(), surface, x, y);
+ cr->set_operator(Cairo::OPERATOR_ATOP);
+ cr->rectangle(x, y, _size, _size);
+ cr->fill();
+ cairo_surface_destroy(surface); // free!
+}
+
+void CellRendererItemIcon::get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const
+{
+ min_h = _size;
+ nat_h = _size + 4;
+}
+
+void CellRendererItemIcon::get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const
+{
+ min_w = _size;
+ nat_w = _size + 4;
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/shapeicon.h b/src/ui/widget/shapeicon.h
new file mode 100644
index 0000000..b5ca033
--- /dev/null
+++ b/src/ui/widget/shapeicon.h
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef __UI_DIALOG_SHAPEICON_H__
+#define __UI_DIALOG_SHAPEICON_H__
+/*
+ * Authors:
+ * Martin Owens
+ *
+ * Copyright (C) 2020 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/iconinfo.h>
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// Object overlay states usually modify the icon and indicate
+// That there may be non-item children under this item (e.g. clip)
+using OverlayState = int;
+enum OverlayStates : OverlayState {
+ OVERLAY_NONE = 0, // Nothing special about the object.
+ OVERLAY_CLIP = 1, // Object has a clip
+ OVERLAY_MASK = 2 // Object has a mask
+};
+
+/* Custom cell renderer for type icon */
+class CellRendererItemIcon : public Gtk::CellRenderer {
+public:
+
+ CellRendererItemIcon() :
+ Glib::ObjectBase(typeid(CellRenderer)),
+ Gtk::CellRenderer(),
+ _property_shape_type(*this, "shape_type", "unknown"),
+ _property_color(*this, "color", 0),
+ _property_clipmask(*this, "clipmask", 0),
+ _clip_overlay(nullptr),
+ _mask_overlay(nullptr)
+ {
+ Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, _size, _size);
+ }
+ ~CellRendererItemIcon() override = default;
+
+ Glib::PropertyProxy<std::string> property_shape_type() {
+ return _property_shape_type.get_proxy();
+ }
+ Glib::PropertyProxy<unsigned int> property_color() {
+ return _property_color.get_proxy();
+ }
+ Glib::PropertyProxy<unsigned int> property_clipmask() {
+ return _property_clipmask.get_proxy();
+ }
+
+protected:
+ void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) override;
+ void paint_icon(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf,
+ int x, int y);
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const override;
+ void get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const override;
+
+private:
+
+ int _size;
+ Glib::Property<std::string> _property_shape_type;
+ Glib::Property<unsigned int> _property_color;
+ Glib::Property<unsigned int> _property_clipmask;
+ std::map<const std::string, Glib::RefPtr<Gdk::Pixbuf> > _icon_cache;
+
+ // Overlay indicators
+ Glib::RefPtr<Gdk::Pixbuf> _mask_overlay;
+ Glib::RefPtr<Gdk::Pixbuf> _clip_overlay;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif /* __UI_DIALOG_SHAPEICON_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/ui/widget/spin-button-tool-item.cpp b/src/ui/widget/spin-button-tool-item.cpp
new file mode 100644
index 0000000..08ba38b
--- /dev/null
+++ b/src/ui/widget/spin-button-tool-item.cpp
@@ -0,0 +1,607 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "spin-button-tool-item.h"
+
+#include <algorithm>
+#include <gtkmm/box.h>
+#include <gtkmm/image.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/toolbar.h>
+
+#include <cmath>
+#include <utility>
+
+#include "spinbutton.h"
+#include "ui/icon-loader.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * \brief Handler for the button's "focus-in-event" signal
+ *
+ * \param focus_event The event that triggered the signal
+ *
+ * \detail This just logs the current value of the spin-button
+ * and sets the _transfer_focus flag
+ */
+bool
+SpinButtonToolItem::on_btn_focus_in_event(GdkEventFocus * /* focus_event */)
+{
+ _last_val = _btn->get_value();
+ _transfer_focus = true;
+
+ return false; // Event not consumed
+}
+
+/**
+ * \brief Handler for the button's "focus-out-event" signal
+ *
+ * \param focus_event The event that triggered the signal
+ *
+ * \detail This just unsets the _transfer_focus flag
+ */
+bool
+SpinButtonToolItem::on_btn_focus_out_event(GdkEventFocus * /* focus_event */)
+{
+ _transfer_focus = false;
+
+ return false; // Event not consumed
+}
+
+/**
+ * \brief Handler for the button's "key-press-event" signal
+ *
+ * \param key_event The event that triggered the signal
+ *
+ * \detail If the ESC key was pressed, restore the last value and defocus.
+ * If the Enter key was pressed, just defocus.
+ */
+bool
+SpinButtonToolItem::on_btn_key_press_event(GdkEventKey *key_event)
+{
+ bool was_consumed = false; // Whether event has been consumed or not
+ auto display = Gdk::Display::get_default();
+ auto keymap = display->get_keymap();
+ guint key = 0;
+ gdk_keymap_translate_keyboard_state(keymap, key_event->hardware_keycode,
+ static_cast<GdkModifierType>(key_event->state),
+ 0, &key, 0, 0, 0);
+
+ auto val = _btn->get_value();
+
+ switch(key) {
+ case GDK_KEY_Escape:
+ {
+ _transfer_focus = true;
+ _btn->set_value(_last_val);
+ defocus();
+ was_consumed = true;
+ }
+ break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ {
+ _transfer_focus = true;
+ defocus();
+ was_consumed = true;
+ }
+ break;
+
+ case GDK_KEY_Tab:
+ {
+ _transfer_focus = false;
+ was_consumed = process_tab(1);
+ }
+ break;
+
+ case GDK_KEY_ISO_Left_Tab:
+ {
+ _transfer_focus = false;
+ was_consumed = process_tab(-1);
+ }
+ break;
+
+ // TODO: Enable variable step-size if this is ever used
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ {
+ _transfer_focus = false;
+ _btn->set_value(val+1);
+ was_consumed=true;
+ }
+ break;
+
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ {
+ _transfer_focus = false;
+ _btn->set_value(val-1);
+ was_consumed=true;
+ }
+ break;
+
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up:
+ {
+ _transfer_focus = false;
+ _btn->set_value(val+10);
+ was_consumed=true;
+ }
+ break;
+
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down:
+ {
+ _transfer_focus = false;
+ _btn->set_value(val-10);
+ was_consumed=true;
+ }
+ break;
+
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ {
+ _transfer_focus = false;
+ _btn->set_value(_last_val);
+ was_consumed = true;
+ }
+ break;
+ }
+
+ return was_consumed;
+}
+
+/**
+ * \brief Shift focus to a different widget
+ *
+ * \details This only has an effect if the _transfer_focus flag and the _focus_widget are set
+ */
+void
+SpinButtonToolItem::defocus()
+{
+ if(_transfer_focus && _focus_widget) {
+ _focus_widget->grab_focus();
+ }
+}
+
+/**
+ * \brief Move focus to another spinbutton in the toolbar
+ *
+ * \param increment[in] The number of places to shift within the toolbar
+ */
+bool
+SpinButtonToolItem::process_tab(int increment)
+{
+ // If the increment is zero, do nothing
+ if(increment == 0) return true;
+
+ // Here, we're working through the widget hierarchy:
+ // Toolbar
+ // |- ToolItem (*this)
+ // |-> Box
+ // |-> SpinButton (*_btn)
+ //
+ // Our aim is to find the next/previous spin-button within a toolitem in our toolbar
+
+ bool handled = false;
+
+ // We only bother doing this if the current item is actually in a toolbar!
+ auto toolbar = dynamic_cast<Gtk::Toolbar *>(get_parent());
+
+ if (toolbar) {
+ // Get the index of the current item within the toolbar and the total number of items
+ auto my_index = toolbar->get_item_index(*this);
+ auto n_items = toolbar->get_n_items();
+
+ auto test_index = my_index + increment; // The index of the item we want to check
+
+ // Loop through tool items as long as we're within the limits of the toolbar and
+ // we haven't yet found our new item to focus on
+ while(test_index > 0 && test_index <= n_items && !handled) {
+
+ auto tool_item = toolbar->get_nth_item(test_index);
+
+ if(tool_item) {
+ // There are now two options that we support:
+ if (auto sb_tool_item = dynamic_cast<SpinButtonToolItem *>(tool_item)) {
+ // (1) The tool item is a SpinButtonToolItem, in which case, we just pass
+ // focus to its spin-button
+ sb_tool_item->grab_button_focus();
+ handled = true;
+ }
+ else if(dynamic_cast<Gtk::SpinButton *>(tool_item->get_child())) {
+ // (2) The tool item contains a plain Gtk::SpinButton, in which case we
+ // pass focus directly to it
+ tool_item->get_child()->grab_focus();
+ }
+ }
+
+ test_index += increment;
+ }
+ }
+
+ return handled;
+}
+
+/**
+ * \brief Handler for toggle events on numeric menu items
+ *
+ * \details Sets the adjustment to the desired value
+ */
+void
+SpinButtonToolItem::on_numeric_menu_item_toggled(double value, Gtk::RadioMenuItem* button)
+{
+ // Called both when Radio button is deactivated and activated. Only set when activated.
+ if (button->get_active()) {
+ auto adj = _btn->get_adjustment();
+ adj->set_value(value);
+ }
+}
+
+Gtk::RadioMenuItem *
+SpinButtonToolItem::create_numeric_menu_item(Gtk::RadioButtonGroup *group,
+ double value,
+ const Glib::ustring& label,
+ bool enable)
+{
+ // Represent the value as a string
+ std::ostringstream ss;
+ ss << value;
+
+ Glib::ustring item_label = ss.str();
+
+ // Append the label if specified
+ if (!label.empty()) {
+ item_label += ": " + label;
+ }
+
+ auto numeric_option = Gtk::manage(new Gtk::RadioMenuItem(*group, item_label));
+ if (enable) {
+ numeric_option->set_active(); // Do before connecting toggled_handler.
+ }
+
+ // Set the adjustment value in response to changes in the selected item
+ auto toggled_handler = sigc::bind(sigc::mem_fun(*this, &SpinButtonToolItem::on_numeric_menu_item_toggled), value, numeric_option);
+ numeric_option->signal_toggled().connect(toggled_handler);
+
+ return numeric_option;
+}
+
+/**
+ * \brief Create a menu containing fixed numeric options for the adjustment
+ *
+ * \details Each of these values represents a snap-point for the adjustment's value
+ */
+Gtk::Menu *
+SpinButtonToolItem::create_numeric_menu()
+{
+ auto numeric_menu = Gtk::manage(new Gtk::Menu());
+
+ Gtk::RadioMenuItem::Group group;
+
+ // Get values for the adjustment
+ auto adj = _btn->get_adjustment();
+ auto adj_value = round_to_precision(adj->get_value());
+ auto lower = round_to_precision(adj->get_lower());
+ auto upper = round_to_precision(adj->get_upper());
+ auto page = adj->get_page_increment();
+
+ // Start by setting some fixed values based on the adjustment's
+ // parameters.
+ NumericMenuData values;
+
+ // first add all custom items (necessary)
+ for (auto custom_data : _custom_menu_data) {
+ if (custom_data.first >= lower && custom_data.first <= upper) {
+ values.emplace(custom_data);
+ }
+ }
+
+ values.emplace(adj_value, "");
+
+ // for quick page changes using mouse, step can changes can be done with +/- buttons on
+ // SpinButton
+ values.emplace(::fmin(adj_value + page, upper), "");
+ values.emplace(::fmax(adj_value - page, lower), "");
+
+ // add upper/lower limits to options
+ if (_show_upper_limit) {
+ values.emplace(upper, "");
+ }
+ if (_show_lower_limit) {
+ values.emplace(lower, "");
+ }
+
+ auto add_item = [&numeric_menu, this, &group, adj_value](ValueLabel value){
+ bool enable = (adj_value == value.first);
+ auto numeric_menu_item = create_numeric_menu_item(&group, value.first, value.second, enable);
+ numeric_menu->append(*numeric_menu_item);
+ };
+
+ if (_sort_decreasing) {
+ std::for_each(values.crbegin(), values.crend(), add_item);
+ } else {
+ std::for_each(values.cbegin(), values.cend(), add_item);
+ }
+
+ return numeric_menu;
+}
+
+/**
+ * \brief Create a menu-item in response to the "create-menu-proxy" signal
+ *
+ * \detail This is an override for the default Gtk::ToolItem handler so
+ * we don't need to explicitly connect this to the signal. It
+ * runs if the toolitem is unable to fit on the toolbar, and
+ * must be represented by a menu item instead.
+ */
+bool
+SpinButtonToolItem::on_create_menu_proxy()
+{
+ // The main menu-item. It just contains the label that normally appears
+ // next to the spin-button, and an indicator for a sub-menu.
+ auto menu_item = Gtk::manage(new Gtk::MenuItem(_label_text));
+ auto numeric_menu = create_numeric_menu();
+ menu_item->set_submenu(*numeric_menu);
+
+ set_proxy_menu_item(_name, *menu_item);
+
+ return true; // Finished handling the event
+}
+
+/**
+ * \brief Create a new SpinButtonToolItem
+ *
+ * \param[in] name A unique ID for this tool-item (not translatable)
+ * \param[in] label_text The text to display in the toolbar
+ * \param[in] adjustment The Gtk::Adjustment to attach to the spinbutton
+ * \param[in] climb_rate The climb rate for the spin button (default = 0)
+ * \param[in] digits Number of decimal places to display
+ */
+SpinButtonToolItem::SpinButtonToolItem(const Glib::ustring name,
+ const Glib::ustring& label_text,
+ Glib::RefPtr<Gtk::Adjustment>& adjustment,
+ double climb_rate,
+ int digits)
+ : _btn(Gtk::manage(new SpinButton(adjustment, climb_rate, digits))),
+ _name(std::move(name)),
+ _label_text(label_text),
+ _digits(digits)
+{
+ set_margin_start(3);
+ set_margin_end(3);
+ set_name(_name);
+
+ // Handle popup menu
+ _btn->signal_popup_menu().connect(sigc::mem_fun(*this, &SpinButtonToolItem::on_popup_menu), false);
+
+ // Handle button events
+ auto btn_focus_in_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_in_event);
+ _btn->signal_focus_in_event().connect(btn_focus_in_event_cb, false);
+
+ auto btn_focus_out_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_out_event);
+ _btn->signal_focus_out_event().connect(btn_focus_out_event_cb, false);
+
+ auto btn_key_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_key_press_event);
+ _btn->signal_key_press_event().connect(btn_key_press_event_cb, false);
+
+ auto btn_button_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_button_press_event);
+ _btn->signal_button_press_event().connect(btn_button_press_event_cb, false);
+
+ _btn->add_events(Gdk::KEY_PRESS_MASK);
+
+ // Create a label
+ _label = Gtk::manage(new Gtk::Label(label_text));
+
+ // Arrange the widgets in a horizontal box
+ _hbox = Gtk::manage(new Gtk::Box());
+ _hbox->set_spacing(3);
+ _hbox->pack_start(*_label);
+ _hbox->pack_start(*_btn);
+ add(*_hbox);
+ show_all();
+}
+
+void
+SpinButtonToolItem::set_icon(const Glib::ustring& icon_name)
+{
+ _hbox->remove(*_label);
+ _icon = Gtk::manage(sp_get_icon_image(icon_name, Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ if(_icon) {
+ _hbox->pack_start(*_icon);
+ _hbox->reorder_child(*_icon, 0);
+ }
+
+ show_all();
+}
+
+bool
+SpinButtonToolItem::on_btn_button_press_event(const GdkEventButton *button_event)
+{
+ if (gdk_event_triggers_context_menu(reinterpret_cast<const GdkEvent *>(button_event)) &&
+ button_event->type == GDK_BUTTON_PRESS) {
+ do_popup_menu(button_event);
+ return true;
+ }
+
+ return false;
+}
+
+void
+SpinButtonToolItem::do_popup_menu(const GdkEventButton *button_event)
+{
+ auto menu = create_numeric_menu();
+ menu->attach_to_widget(*_btn);
+ menu->show_all();
+ menu->popup_at_pointer(reinterpret_cast<const GdkEvent *>(button_event));
+}
+
+/**
+ * \brief Create a popup menu
+ */
+bool
+SpinButtonToolItem::on_popup_menu()
+{
+ do_popup_menu(nullptr);
+ return true;
+}
+
+/**
+ * \brief Transfers focus to the child spinbutton by default
+ */
+void
+SpinButtonToolItem::on_grab_focus()
+{
+ grab_button_focus();
+}
+
+/**
+ * \brief Set the tooltip to display on this (and all child widgets)
+ *
+ * \param[in] text The tooltip to display
+ */
+void
+SpinButtonToolItem::set_all_tooltip_text(const Glib::ustring& text)
+{
+ set_tooltip_text(text);
+ _btn->set_tooltip_text(text);
+}
+
+/**
+ * \brief Set the widget that focus moves to when this one loses focus
+ *
+ * \param widget The widget that will gain focus
+ */
+void
+SpinButtonToolItem::set_focus_widget(Gtk::Widget *widget)
+{
+ _focus_widget = widget;
+}
+
+/**
+ * \brief Grab focus on the spin-button widget
+ */
+void
+SpinButtonToolItem::grab_button_focus()
+{
+ _btn->grab_focus();
+}
+
+/**
+ * \brief A wrapper of Geom::decimal_round to remember precision
+ */
+double
+SpinButtonToolItem::round_to_precision(double value) {
+ return Geom::decimal_round(value, _digits);
+}
+
+/**
+ * \brief [discouraged] Set numeric data option in Radio menu.
+ *
+ * \param[in] values values to provide as options
+ * \param[in] labels label to show for the value at same index in values.
+ *
+ * \detail Use is advised only when there are no labels.
+ * This is discouraged in favor of other overloads of the function, due to error prone
+ * usage. Using two vectors for related data, undermining encapsulation.
+ */
+void
+SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<double>& values,
+ const std::vector<Glib::ustring>& labels)
+{
+
+ if (values.size() != labels.size() && !labels.empty()) {
+ g_warning("Cannot add custom menu items. Value and label arrays are different sizes");
+ return;
+ }
+
+ _custom_menu_data.clear();
+
+ if (labels.empty()) {
+ for (const auto &value : values) {
+ _custom_menu_data.emplace(round_to_precision(value), "");
+ }
+ return;
+ }
+
+ int i = 0;
+ for (const auto &value : values) {
+ _custom_menu_data.emplace(round_to_precision(value), labels[i++]);
+ }
+}
+
+/**
+ * \brief Set numeric data options for Radio menu (densely labeled data).
+ *
+ * \param[in] value_labels value and labels to provide as options
+ *
+ * \detail Should be used when most of the values have an associated label (densely labeled data)
+ *
+ */
+void
+SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<ValueLabel>& value_labels) {
+ _custom_menu_data.clear();
+ for(const auto& value_label : value_labels) {
+ _custom_menu_data.emplace(round_to_precision(value_label.first), value_label.second);
+ }
+}
+
+
+/**
+ * \brief Set numeric data options for Radio menu (sparsely labeled data).
+ *
+ * \param[in] values values without labels
+ * \param[in] sparse_labels value and labels to provide as options
+ *
+ * \detail Should be used when very few values have an associated label (sparsely labeled data).
+ * Duplicate values in vector and map are acceptable but, values labels in map are
+ * preferred. Avoid using duplicate values intentionally though.
+ *
+ */
+void
+SpinButtonToolItem::set_custom_numeric_menu_data(const std::vector<double> &values,
+ const std::unordered_map<double, Glib::ustring> &sparse_labels)
+{
+ _custom_menu_data.clear();
+
+ for(const auto& value_label : sparse_labels) {
+ _custom_menu_data.emplace(round_to_precision(value_label.first), value_label.second);
+ }
+
+ for(const auto& value : values) {
+ _custom_menu_data.emplace(round_to_precision(value), "");
+ }
+
+}
+
+
+void SpinButtonToolItem::show_upper_limit(bool show) { _show_upper_limit = show; }
+
+void SpinButtonToolItem::show_lower_limit(bool show) { _show_lower_limit = show; }
+
+void SpinButtonToolItem::show_limits(bool show) { _show_upper_limit = _show_lower_limit = show; }
+
+void SpinButtonToolItem::sort_decreasing(bool decreasing) { _sort_decreasing = decreasing; }
+
+Glib::RefPtr<Gtk::Adjustment>
+SpinButtonToolItem::get_adjustment()
+{
+ return _btn->get_adjustment();
+}
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/spin-button-tool-item.h b/src/ui/widget/spin-button-tool-item.h
new file mode 100644
index 0000000..73caf14
--- /dev/null
+++ b/src/ui/widget/spin-button-tool-item.h
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_SPIN_BUTTON_TOOL_ITEM_H
+#define SEEN_SPIN_BUTTON_TOOL_ITEM_H
+
+#include <gtkmm/toolitem.h>
+#include <unordered_map>
+#include <utility>
+
+#include "2geom/math-utils.h"
+
+namespace Gtk {
+class Box;
+class RadioButtonGroup;
+class RadioMenuItem;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class SpinButton;
+
+/**
+ * \brief A spin-button with a label that can be added to a toolbar
+ */
+class SpinButtonToolItem : public Gtk::ToolItem
+{
+private:
+ using ValueLabel = std::pair<double, Glib::ustring>;
+ using NumericMenuData = std::map<double, Glib::ustring>;
+
+ Glib::ustring _name; ///< A unique ID for the widget (NOT translatable)
+ SpinButton *_btn; ///< The spin-button within the widget
+ Glib::ustring _label_text; ///< A string to use in labels for the widget (translatable)
+ double _last_val = 0.0; ///< The last value of the adjustment
+ bool _transfer_focus = false; ///< Whether or not to transfer focus
+
+ Gtk::Box *_hbox; ///< Horizontal box, to store widgets
+ Gtk::Widget *_label; ///< A text label to describe the setting
+ Gtk::Widget *_icon; ///< An icon to describe the setting
+
+ /** A widget that grabs focus when this one loses it */
+ Gtk::Widget * _focus_widget = nullptr;
+
+ // Custom values and labels to add to the numeric popup-menu
+ NumericMenuData _custom_menu_data;
+
+ // To show or not to show upper/lower limit of the adjustment
+ bool _show_upper_limit = false;
+ bool _show_lower_limit = false;
+
+ // sort in decreasing order
+ bool _sort_decreasing = false;
+
+ // digits of adjustment
+ int _digits;
+
+ // just a wrapper for Geom::decimal_round to simplify calls
+ double round_to_precision(double value);
+
+ // Event handlers
+ bool on_btn_focus_in_event(GdkEventFocus *focus_event);
+ bool on_btn_focus_out_event(GdkEventFocus *focus_event);
+ bool on_btn_key_press_event(GdkEventKey *key_event);
+ bool on_btn_button_press_event(const GdkEventButton *button_event);
+ bool on_popup_menu();
+ void do_popup_menu(const GdkEventButton *button_event);
+
+ void defocus();
+ bool process_tab(int direction);
+
+ void on_numeric_menu_item_toggled(double value, Gtk::RadioMenuItem* button);
+
+ Gtk::Menu * create_numeric_menu();
+
+ Gtk::RadioMenuItem * create_numeric_menu_item(Gtk::RadioButtonGroup *group,
+ double value,
+ const Glib::ustring& label = "",
+ bool enable = false);
+
+protected:
+ bool on_create_menu_proxy() override;
+ void on_grab_focus() override;
+
+public:
+ SpinButtonToolItem(const Glib::ustring name,
+ const Glib::ustring& label_text,
+ Glib::RefPtr<Gtk::Adjustment>& adjustment,
+ double climb_rate = 0.1,
+ int digits = 3);
+
+ void set_all_tooltip_text(const Glib::ustring& text);
+ void set_focus_widget(Gtk::Widget *widget);
+ void grab_button_focus();
+
+ void set_custom_numeric_menu_data(const std::vector<double>& values,
+ const std::vector<Glib::ustring>& labels = std::vector<Glib::ustring>());
+
+ void set_custom_numeric_menu_data(const std::vector<ValueLabel> &value_labels);
+
+ void set_custom_numeric_menu_data(const std::vector<double>& values,
+ const std::unordered_map<double, Glib::ustring>& sparse_labels);
+
+ Glib::RefPtr<Gtk::Adjustment> get_adjustment();
+ void set_icon(const Glib::ustring& icon_name);
+
+ // display limits
+ void show_upper_limit(bool show = true);
+ void show_lower_limit(bool show = true);
+ void show_limits (bool show = true);
+
+ // sorting order
+ void sort_decreasing(bool decreasing = true);
+
+ SpinButton *get_spin_button() { return _btn; };
+};
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+#endif // SEEN_SPIN_BUTTON_TOOL_ITEM_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/ui/widget/spin-scale.cpp b/src/ui/widget/spin-scale.cpp
new file mode 100644
index 0000000..21aa525
--- /dev/null
+++ b/src/ui/widget/spin-scale.cpp
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ *
+ * Copyright (C) 2007 Nicholas Bishop <nicholasbishop@gmail.com>
+ * 2008 Felipe C. da S. Sanches <juca@members.fsf.org>
+ * 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/*
+ * Derived from and replaces SpinSlider
+ */
+
+#include "spin-scale.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+SpinScale::SpinScale(const Glib::ustring label, double value,
+ double lower, double upper,
+ double step_increment, double page_increment, int digits,
+ const SPAttr a, const Glib::ustring tip_text)
+ : AttrWidget(a, value)
+ , _inkspinscale(value, lower, upper, step_increment, page_increment, 0)
+{
+ set_name("SpinScale");
+
+ _inkspinscale.set_label (label);
+ _inkspinscale.set_digits (digits);
+ _inkspinscale.set_tooltip_text (tip_text);
+
+ _adjustment = _inkspinscale.get_adjustment();
+
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ pack_start(_inkspinscale);
+
+ show_all_children();
+}
+
+SpinScale::SpinScale(const Glib::ustring label,
+ Glib::RefPtr<Gtk::Adjustment> adjustment, int digits,
+ const SPAttr a, const Glib::ustring tip_text)
+ : AttrWidget(a, 0.0)
+ , _inkspinscale(adjustment)
+{
+ set_name("SpinScale");
+
+ _inkspinscale.set_label (label);
+ _inkspinscale.set_digits (digits);
+ _inkspinscale.set_tooltip_text (tip_text);
+
+ _adjustment = _inkspinscale.get_adjustment();
+
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ pack_start(_inkspinscale);
+
+ show_all_children();
+}
+
+Glib::ustring SpinScale::get_as_attribute() const
+{
+ const double val = _adjustment->get_value();
+
+ if( _inkspinscale.get_digits() == 0)
+ return Glib::Ascii::dtostr((int)val);
+ else
+ return Glib::Ascii::dtostr(val);
+}
+
+void SpinScale::set_from_attribute(SPObject* o)
+{
+ const gchar* val = attribute_value(o);
+ if (val)
+ _adjustment->set_value(Glib::Ascii::strtod(val));
+ else
+ _adjustment->set_value(get_default()->as_double());
+}
+
+Glib::SignalProxy0<void> SpinScale::signal_value_changed()
+{
+ return _adjustment->signal_value_changed();
+}
+
+double SpinScale::get_value() const
+{
+ return _adjustment->get_value();
+}
+
+void SpinScale::set_value(const double val)
+{
+ _adjustment->set_value(val);
+}
+
+void SpinScale::set_focuswidget(GtkWidget *widget)
+{
+ _inkspinscale.set_focus_widget(widget);
+}
+
+const decltype(SpinScale::_adjustment) SpinScale::get_adjustment() const
+{
+ return _adjustment;
+}
+
+decltype(SpinScale::_adjustment) SpinScale::get_adjustment()
+{
+ return _adjustment;
+}
+
+
+DualSpinScale::DualSpinScale(const Glib::ustring label1, const Glib::ustring label2,
+ double value, double lower, double upper,
+ double step_increment, double page_increment, int digits,
+ const SPAttr a,
+ const Glib::ustring tip_text1, const Glib::ustring tip_text2)
+ : AttrWidget(a),
+ _s1(label1, value, lower, upper, step_increment, page_increment, digits, SPAttr::INVALID, tip_text1),
+ _s2(label2, value, lower, upper, step_increment, page_increment, digits, SPAttr::INVALID, tip_text2),
+ //TRANSLATORS: "Link" means to _link_ two sliders together
+ _link(C_("Sliders", "Link"))
+{
+ set_name("DualSpinScale");
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ _s1.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot());
+ _s2.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot());
+ _s1.get_adjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &DualSpinScale::update_linked));
+
+ _link.signal_toggled().connect(sigc::mem_fun(*this, &DualSpinScale::link_toggled));
+
+ Gtk::Box* vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ vb->add(_s1);
+ vb->add(_s2);
+ pack_start(*vb);
+ pack_start(_link, false, false);
+ _link.set_active(true);
+
+ show_all();
+}
+
+Glib::ustring DualSpinScale::get_as_attribute() const
+{
+ if(_link.get_active())
+ return _s1.get_as_attribute();
+ else
+ return _s1.get_as_attribute() + " " + _s2.get_as_attribute();
+}
+
+void DualSpinScale::set_from_attribute(SPObject* o)
+{
+ const gchar* val = attribute_value(o);
+ if(val) {
+ // Split val into parts
+ gchar** toks = g_strsplit(val, " ", 2);
+
+ if(toks) {
+ double v1 = 0.0, v2 = 0.0;
+ if(toks[0])
+ v1 = v2 = Glib::Ascii::strtod(toks[0]);
+ if(toks[1])
+ v2 = Glib::Ascii::strtod(toks[1]);
+
+ _link.set_active(toks[1] == nullptr);
+
+ _s1.get_adjustment()->set_value(v1);
+ _s2.get_adjustment()->set_value(v2);
+
+ g_strfreev(toks);
+ }
+ }
+}
+
+sigc::signal<void>& DualSpinScale::signal_value_changed()
+{
+ return _signal_value_changed;
+}
+
+const SpinScale& DualSpinScale::get_SpinScale1() const
+{
+ return _s1;
+}
+
+SpinScale& DualSpinScale::get_SpinScale1()
+{
+ return _s1;
+}
+
+const SpinScale& DualSpinScale::get_SpinScale2() const
+{
+ return _s2;
+}
+
+SpinScale& DualSpinScale::get_SpinScale2()
+{
+ return _s2;
+}
+
+void DualSpinScale::link_toggled()
+{
+ _s2.set_sensitive(!_link.get_active());
+ update_linked();
+}
+
+void DualSpinScale::update_linked()
+{
+ if(_link.get_active())
+ _s2.set_value(_s1.get_value());
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/spin-scale.h b/src/ui/widget/spin-scale.h
new file mode 100644
index 0000000..b2e478d
--- /dev/null
+++ b/src/ui/widget/spin-scale.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ *
+ * Copyright (C) 2007 Nicholas Bishop <nicholasbishop@gmail.com>
+ * 2017 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+/*
+ * Derived from and replaces SpinSlider
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SPIN_SCALE_H
+#define INKSCAPE_UI_WIDGET_SPIN_SCALE_H
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/togglebutton.h>
+#include "attr-widget.h"
+#include "ink-spinscale.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Wrap the InkSpinScale class and attach an attribute.
+ * A combo widget with label, scale slider, spinbutton, and adjustment;
+ */
+class SpinScale : public Gtk::Box, public AttrWidget
+{
+
+public:
+ SpinScale(const Glib::ustring label, double value,
+ double lower, double upper,
+ double step_increment, double page_increment, int digits,
+ const SPAttr a = SPAttr::INVALID, const Glib::ustring tip_text = "");
+
+ // Used by extensions
+ SpinScale(const Glib::ustring label,
+ Glib::RefPtr<Gtk::Adjustment> adjustment, int digits,
+ const SPAttr a = SPAttr::INVALID, const Glib::ustring tip_text = "");
+
+ Glib::ustring get_as_attribute() const override;
+ void set_from_attribute(SPObject*) override;
+
+ // Shortcuts to _adjustment
+ Glib::SignalProxy0<void> signal_value_changed();
+ double get_value() const;
+ void set_value(const double);
+ void set_focuswidget(GtkWidget *widget);
+
+private:
+ Glib::RefPtr<Gtk::Adjustment> _adjustment;
+ InkSpinScale _inkspinscale;
+
+public:
+ const decltype(_adjustment) get_adjustment() const;
+ decltype(_adjustment) get_adjustment();
+};
+
+
+/**
+ * Contains two SpinScales for controlling number-opt-number attributes.
+ *
+ * @see SpinScale
+ */
+class DualSpinScale : public Gtk::Box, public AttrWidget
+{
+public:
+ DualSpinScale(const Glib::ustring label1, const Glib::ustring label2,
+ double value, double lower, double upper,
+ double step_increment, double page_increment, int digits,
+ const SPAttr a,
+ const Glib::ustring tip_text1, const Glib::ustring tip_text2);
+
+ Glib::ustring get_as_attribute() const override;
+ void set_from_attribute(SPObject*) override;
+
+ sigc::signal<void>& signal_value_changed();
+
+ const SpinScale& get_SpinScale1() const;
+ SpinScale& get_SpinScale1();
+
+ const SpinScale& get_SpinScale2() const;
+ SpinScale& get_SpinScale2();
+
+ //void remove_scale();
+private:
+ void link_toggled();
+ void update_linked();
+ sigc::signal<void> _signal_value_changed;
+ SpinScale _s1, _s2;
+ Gtk::ToggleButton _link;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SPIN_SCALE_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/ui/widget/spinbutton.cpp b/src/ui/widget/spinbutton.cpp
new file mode 100644
index 0000000..423ffe5
--- /dev/null
+++ b/src/ui/widget/spinbutton.cpp
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Johan B. C. Engelen
+ *
+ * Copyright (C) 2011 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "spinbutton.h"
+
+#include "scroll-utils.h"
+#include "unit-menu.h"
+#include "unit-tracker.h"
+#include "util/expression-evaluator.h"
+#include "ui/tools/tool-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+int SpinButton::on_input(double* newvalue)
+{
+ if (_dont_evaluate) return false;
+
+ try {
+ Inkscape::Util::EvaluatorQuantity result;
+ if (_unit_menu || _unit_tracker) {
+ Unit const *unit = nullptr;
+ if (_unit_menu) {
+ unit = _unit_menu->getUnit();
+ } else {
+ unit = _unit_tracker->getActiveUnit();
+ }
+ Inkscape::Util::ExpressionEvaluator eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), unit);
+ result = eval.evaluate();
+ // check if output dimension corresponds to input unit
+ if (result.dimension != (unit->isAbsolute() ? 1 : 0) ) {
+ throw Inkscape::Util::EvaluatorException("Input dimensions do not match with parameter dimensions.","");
+ }
+ } else {
+ Inkscape::Util::ExpressionEvaluator eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), nullptr);
+ result = eval.evaluate();
+ }
+ *newvalue = result.value;
+ }
+ catch(Inkscape::Util::EvaluatorException &e) {
+ g_message ("%s", e.what());
+
+ return false;
+ }
+
+ return true;
+}
+
+bool SpinButton::on_focus_in_event(GdkEventFocus *event)
+{
+ _on_focus_in_value = get_value();
+ return parent_type::on_focus_in_event(event);
+}
+
+bool SpinButton::on_key_press_event(GdkEventKey* event)
+{
+ switch (Inkscape::UI::Tools::get_latin_keyval (event)) {
+ case GDK_KEY_Escape: // defocus
+ undo();
+ defocus();
+ return true; // I consumed the event
+ break;
+ case GDK_KEY_Return: // defocus
+ case GDK_KEY_KP_Enter:
+ defocus();
+ break;
+ case GDK_KEY_Tab:
+ case GDK_KEY_ISO_Left_Tab:
+ // set the flag meaning "do not leave toolbar when changing value"
+ _stay = true;
+ break;
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ _stay = true;
+ if (event->state & GDK_CONTROL_MASK) {
+ undo();
+ return true; // I consumed the event
+ }
+ break;
+ default:
+ break;
+ }
+
+ return parent_type::on_key_press_event(event);
+}
+
+void SpinButton::undo()
+{
+ set_value(_on_focus_in_value);
+}
+
+void SpinButton::defocus()
+{
+ // defocus spinbutton by moving focus to the canvas, unless "stay" is on
+ if (_stay) {
+ _stay = false;
+ } else {
+ Gtk::Widget *widget = _defocus_widget ? _defocus_widget : get_scrollable_ancestor(this);
+ if (widget) {
+ widget->grab_focus();
+ }
+ }
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/spinbutton.h b/src/ui/widget/spinbutton.h
new file mode 100644
index 0000000..e8e5628
--- /dev/null
+++ b/src/ui/widget/spinbutton.h
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Johan B. C. Engelen
+ *
+ * Copyright (C) 2011 Author
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_SPINBUTTON_H
+#define INKSCAPE_UI_WIDGET_SPINBUTTON_H
+
+#include <gtkmm/spinbutton.h>
+
+#include "scrollprotected.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class UnitMenu;
+class UnitTracker;
+
+/**
+ * SpinButton widget, that allows entry of simple math expressions (also units, when linked with UnitMenu),
+ * and allows entry of both '.' and ',' for the decimal, even when in numeric mode.
+ *
+ * Calling "set_numeric()" effectively disables the expression parsing. If no unit menu is linked, all unitlike characters are ignored.
+ */
+class SpinButton : public ScrollProtected<Gtk::SpinButton>
+{
+ using parent_type = ScrollProtected<Gtk::SpinButton>;
+
+public:
+ using parent_type::parent_type;
+
+ void setUnitMenu(UnitMenu* unit_menu) { _unit_menu = unit_menu; };
+
+ void addUnitTracker(UnitTracker* ut) { _unit_tracker = ut; };
+
+ // TODO: Might be better to just have a default value and a reset() method?
+ inline void set_zeroable(const bool zeroable = true) { _zeroable = zeroable; }
+ inline void set_oneable(const bool oneable = true) { _oneable = oneable; }
+
+ inline bool get_zeroable() const { return _zeroable; }
+ inline bool get_oneable() const { return _oneable; }
+
+ void defocus();
+
+protected:
+ UnitMenu *_unit_menu = nullptr; ///< Linked unit menu for unit conversion in entered expressions.
+ UnitTracker *_unit_tracker = nullptr; ///< Linked unit tracker for unit conversion in entered expressions.
+ double _on_focus_in_value = 0.;
+ Gtk::Widget *_defocus_widget = nullptr; ///< Widget that should grab focus when the spinbutton defocuses
+
+ bool _zeroable = false; ///< Reset-value should be zero
+ bool _oneable = false; ///< Reset-value should be one
+
+ bool _stay = false; ///< Whether to ignore defocusing
+ bool _dont_evaluate = false; ///< Don't attempt to evaluate expressions
+
+ /**
+ * This callback function should try to convert the entered text to a number and write it to newvalue.
+ * It calls a method to evaluate the (potential) mathematical expression.
+ *
+ * @retval false No conversion done, continue with default handler.
+ * @retval true Conversion successful, don't call default handler.
+ */
+ int on_input(double* newvalue) override;
+
+ /**
+ * When focus is obtained, save the value to enable undo later.
+ * @retval false continue with default handler.
+ * @retval true don't call default handler.
+ */
+ bool on_focus_in_event(GdkEventFocus *) override;
+
+ /**
+ * Handle specific keypress events, like Ctrl+Z.
+ *
+ * @retval false continue with default handler.
+ * @retval true don't call default handler.
+ */
+ bool on_key_press_event(GdkEventKey *) override;
+
+ /**
+ * Undo the editing, by resetting the value upon when the spinbutton got focus.
+ */
+ void undo();
+
+ public:
+ inline void set_defocus_widget(const decltype(_defocus_widget) widget) { _defocus_widget = widget; }
+ inline void set_dont_evaluate(bool flag) { _dont_evaluate = flag; }
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_SPINBUTTON_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/ui/widget/stroke-style.cpp b/src/ui/widget/stroke-style.cpp
new file mode 100644
index 0000000..53b50dd
--- /dev/null
+++ b/src/ui/widget/stroke-style.cpp
@@ -0,0 +1,1232 @@
+// 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 "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/marker-combo-box.h"
+#include "ui/widget/unit-menu.h"
+#include "ui/tools/marker-tool.h"
+#include "ui/dialog/dialog-base.h"
+
+#include "actions/actions-tools.h"
+
+#include "widgets/style-utils.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+
+/**
+ * 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 {
+namespace UI {
+namespace Widget {
+
+/**
+ * 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);
+}
+
+std::vector<double> parse_pattern(const Glib::ustring& input) {
+ std::vector<double> output;
+ if (input.empty()) return output;
+
+ std::istringstream stream(input.c_str());
+ while (stream) {
+ double val;
+ stream >> val;
+ if (stream) {
+ output.push_back(val);
+ }
+ }
+
+ return output;
+}
+
+StrokeStyle::StrokeStyle() :
+ Gtk::Box(),
+ miterLimitSpin(),
+ widthSpin(),
+ unitSelector(),
+ joinMiter(),
+ joinRound(),
+ joinBevel(),
+ capButt(),
+ capRound(),
+ capSquare(),
+ dashSelector(),
+ update(false),
+ desktop(nullptr),
+ startMarkerConn(),
+ midMarkerConn(),
+ endMarkerConn(),
+ _old_unit(nullptr)
+{
+ set_name("StrokeSelector");
+ table = Gtk::manage(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::Box *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)
+ 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 = Gtk::manage(new Inkscape::UI::Widget::UnitMenu());
+ unitSelector->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR);
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ unitSelector->addUnit(*unit_table.getUnit("%"));
+ unitSelector->append("hairline", _("Hairline"));
+ _old_unit = unitSelector->getUnit();
+ if (desktop) {
+ unitSelector->setUnit(desktop->getNamedView()->display_units->abbr);
+ _old_unit = desktop->getNamedView()->display_units;
+ }
+ widthSpin->setUnitMenu(unitSelector);
+ unitSelector->signal_changed().connect(sigc::mem_fun(*this, &StrokeStyle::unitChangedCB));
+ unitSelector->show();
+
+ hb->pack_start(*unitSelector, FALSE, FALSE, 0);
+ (*widthAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::setStrokeWidth));
+
+ 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);
+ _pattern = Gtk::make_managed<Gtk::Entry>();
+
+ 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::setStrokeDash));
+
+ i++;
+
+ table->attach(*_pattern, 1, i, 4, 1);
+ _pattern_label = spw_label(table, _("_Pattern:"), 0, i, _pattern);
+ _pattern_label->set_tooltip_text(_("Repeating \"dash gap ...\" pattern"));
+ _pattern->set_no_show_all();
+ _pattern_label->set_no_show_all();
+ _pattern->signal_changed().connect([=](){
+ if (update || _editing_pattern) return;
+
+ auto pat = parse_pattern(_pattern->get_text());
+ _editing_pattern = true;
+ update = true;
+ dashSelector->set_dash(pat, dashSelector->get_offset());
+ update = false;
+ setStrokeDash();
+ _editing_pattern = false;
+ });
+ update_pattern(0, nullptr);
+
+ 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([=]() { markerSelectCB(startMarkerCombo, SP_MARKER_LOC_START); });
+ startMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(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([=]() { markerSelectCB(midMarkerCombo, SP_MARKER_LOC_MID); });
+ midMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(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([=]() { markerSelectCB(endMarkerCombo, SP_MARKER_LOC_END); });
+ endMarkerCombo->edit_signal.connect([=] { enterEditMarkerMode(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;
+
+ 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"));
+
+ 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"));
+
+ 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, 100000.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->set_width_chars(6);
+ 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::setStrokeMiter));
+ 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()
+{
+}
+
+void StrokeStyle::setDesktop(SPDesktop *desktop)
+{
+ if (this->desktop != desktop) {
+
+ if (this->desktop) {
+ _document_replaced_connection.disconnect();
+ }
+ this->desktop = desktop;
+
+ if (!desktop) {
+ return;
+ }
+
+ _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::Box *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);
+
+ tb->signal_toggled().connect(sigc::bind<StrokeStyleButton *, StrokeStyle *>(
+ sigc::ptr_fun(&StrokeStyle::buttonToggledCB), tb, this));
+
+ return tb;
+}
+
+void StrokeStyle::enterEditMarkerMode(SPMarkerLoc _editMarkerMode)
+{
+ SPDesktop *desktop = this->desktop;
+
+ if (desktop) {
+ set_active_tool(desktop, "Marker");
+ Inkscape::UI::Tools::MarkerTool *mt = dynamic_cast<Inkscape::UI::Tools::MarkerTool*>(desktop->event_context);
+
+ if(mt) {
+ mt->editMarkerMode = _editMarkerMode;
+ mt->selection_changed(desktop->getSelection());
+ }
+ }
+}
+
+
+bool StrokeStyle::areMarkersBeingUpdated()
+{
+ return startMarkerCombo->in_update() || midMarkerCombo->in_update() || endMarkerCombo->in_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, SPMarkerLoc const which)
+{
+ if (update || areMarkersBeingUpdated()) {
+ return;
+ }
+
+ SPDocument *document = desktop->getDocument();
+ if (!document) {
+ return;
+ }
+
+ // Get marker ID; could be empty (to remove marker)
+ std::string marker = marker_combo->get_active_marker_uri();
+
+ update = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ gchar const *combo_id = marker_combo->get_id();
+ sp_repr_css_set_property(css, combo_id, marker.c_str());
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ auto itemlist= selection->items();
+ for(auto i=itemlist.begin();i!=itemlist.end();++i){
+ SPItem *item = *i;
+ if (!SP_IS_SHAPE(item)) {
+ continue;
+ }
+ Inkscape::XML::Node *selrepr = item->getRepr();
+ if (selrepr) {
+ sp_repr_css_change_recursive(selrepr, css, "style");
+ }
+
+ item->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+
+ DocumentUndo::done(document, _("Set markers"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+
+ /* edit marker mode - update */
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+
+ if (desktop) {
+ Inkscape::UI::Tools::MarkerTool *mt = dynamic_cast<Inkscape::UI::Tools::MarkerTool*>(desktop->event_context);
+
+ if(mt) {
+ mt->editMarkerMode = which;
+ mt->selection_changed(desktop->getSelection());
+ }
+ }
+
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+
+ update = false;
+};
+
+/**
+ * Callback for when UnitMenu widget is modified.
+ * Triggers update action.
+ */
+void StrokeStyle::unitChangedCB()
+{
+ Inkscape::Util::Unit const *new_unit = unitSelector->getUnit();
+
+ if (_old_unit == new_unit)
+ return;
+
+ // If the unit selector is set to hairline, don't do the normal conversion.
+ if (isHairlineSelected()) {
+ // Force update in setStrokeWidth
+ _old_unit = new_unit;
+ _last_width = -1;
+ setStrokeWidth();
+ return;
+ }
+
+ if (new_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) {
+ // Prevent update in setStrokeWidth
+ _last_width = 100.0;
+ widthSpin->set_value(100);
+ } else {
+ // Remove the non-scaling-stroke effect and the hairline extensions
+ if (!update) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_unset_property(css, "vector-effect");
+ sp_repr_css_unset_property(css, "-inkscape-stroke");
+ sp_desktop_set_style(desktop, css);
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ DocumentUndo::done(desktop->getDocument(), _("Remove hairline stroke"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+ if (_old_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) {
+ // Prevent update of unit (inf-loop) in updateLine
+ _old_unit = new_unit;
+ // Going from % to any other unit means our widthSpin is completely invalid.
+ updateLine();
+ } else {
+ // Scale the value and record the old_unit
+ 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();
+}
+
+/**
+ * Get a dash array and offset from the style.
+ *
+ * Both values are de-scaled by the style's width if needed.
+ */
+std::vector<double>
+StrokeStyle::getDashFromStyle(SPStyle *style, double &offset)
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ std::vector<double> ret;
+ size_t len = style->stroke_dasharray.values.size();
+
+ double scaledash = 1.0;
+ if (prefs->getBool("/options/dash/scale", true) && style->stroke_width.computed) {
+ scaledash = style->stroke_width.computed;
+ }
+
+ offset = style->stroke_dashoffset.value / scaledash;
+ for (unsigned i = 0; i < len; i++) {
+ ret.push_back(style->stroke_dasharray.values[i].value / scaledash);
+ }
+ return ret;
+}
+
+/**
+ * Sets selector widgets' dash style from an SPStyle object.
+ */
+void
+StrokeStyle::setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style)
+{
+ double offset = 0;
+ auto d = getDashFromStyle(style, offset);
+ if (!d.empty()) {
+ dsel->set_dash(d, offset);
+ update_pattern(d.size(), d.data());
+ } else {
+ dsel->set_dash(std::vector<double>(), 0.0);
+ update_pattern(0, nullptr);
+ }
+}
+
+void StrokeStyle::update_pattern(int ndash, const double* pattern) {
+ if (_editing_pattern || _pattern->has_focus()) return;
+
+ std::ostringstream ost;
+ for (int i = 0; i < ndash; ++i) {
+ ost << pattern[i] << ' ';
+ }
+ _pattern->set_text(ost.str().c_str());
+ if (ndash > 0) {
+ _pattern_label->show();
+ _pattern->show();
+ }
+ else {
+ _pattern_label->hide();
+ _pattern->hide();
+ }
+}
+
+/**
+ * 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;
+ }
+
+ auto *widg = get_parent()->get_parent()->get_parent()->get_parent();
+ auto dialogbase = dynamic_cast<Inkscape::UI::Dialog::DialogBase*>(widg);
+ if (dialogbase && !dialogbase->getShowing()) {
+ return;
+ }
+
+ update = true;
+
+ Inkscape::Selection *sel = desktop ? desktop->getSelection() : nullptr;
+
+ if (!sel || sel->isEmpty()) {
+ // Nothing selected, grey-out all controls in the stroke-style dialog
+ table->set_sensitive(false);
+
+ update = false;
+
+ return;
+ }
+
+ FillOrStroke kind = STROKE;
+
+ // create temporary style
+ SPStyle query(SP_ACTIVE_DOCUMENT);
+ // query into it
+ int result_sw = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH);
+ int result_ml = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEMITERLIMIT);
+ int result_cap = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKECAP);
+ int result_join = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEJOIN);
+ int result_order = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_PAINTORDER);
+
+ SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL);
+
+ {
+ table->set_sensitive(true);
+ widthSpin->set_sensitive(true);
+
+ if (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED) {
+ unitSelector->setUnit("%");
+ } else if (query.stroke_extensions.hairline) {
+ unitSelector->set_active_id("hairline");
+ } 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(desktop->getNamedView()->display_units->abbr);
+ }
+ }
+
+ Inkscape::Util::Unit const *unit = unitSelector->getUnit();
+
+ if (query.stroke_extensions.hairline) {
+ widthSpin->set_sensitive(false);
+ (*widthAdj)->set_value(1);
+ } else 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
+ // These options should also be disabled for hairlines, since they don't make sense for
+ // 0-width lines.
+ // The markers might still be shown though, so marker and stroke-width widgets stay enabled
+ bool is_enabled = (result_sw != QUERY_STYLE_NOTHING) && !targPaint.isNoneSet()
+ && !query.stroke_extensions.hairline;
+ joinMiter->set_sensitive(is_enabled);
+ joinRound->set_sensitive(is_enabled);
+ joinBevel->set_sensitive(is_enabled);
+
+ miterLimitSpin->set_sensitive(is_enabled);
+
+ capButt->set_sensitive(is_enabled);
+ capRound->set_sensitive(is_enabled);
+ capSquare->set_sensitive(is_enabled);
+
+ dashSelector->set_sensitive(is_enabled);
+ _pattern->set_sensitive(is_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, const 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->abbr == "%") {
+ auto scale = item->i2doc_affine().descrim();;
+ const gdouble old_w = item->style->stroke_width.computed;
+ return (old_w * width_typed / 100) * scale;
+ } else if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
+ return Inkscape::Util::Quantity::convert(width_typed, unit, "px");
+ }
+ return width_typed;
+}
+
+/**
+ * Set the stroke width and adjust the dash pattern if needed.
+ */
+void StrokeStyle::setStrokeWidth()
+{
+ double width_typed = (*widthAdj)->get_value();
+
+ // Don't change the selection if an update is happening,
+ // but also store the value for later comparison.
+ if (update || fabs(_last_width - width_typed) < 1E-6) {
+ _last_width = width_typed;
+ return;
+ }
+ update = true;
+
+ auto prefs = Inkscape::Preferences::get();
+ auto unit = unitSelector->getUnit();
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ if (isHairlineSelected()) {
+ /* For renderers that don't understand -inkscape-stroke:hairline, fall back to 1px non-scaling */
+ width_typed = 1;
+ sp_repr_css_set_property(css, "vector-effect", "non-scaling-stroke");
+ sp_repr_css_set_property(css, "-inkscape-stroke", "hairline");
+ } else {
+ sp_repr_css_unset_property(css, "vector-effect");
+ sp_repr_css_unset_property(css, "-inkscape-stroke");
+ }
+
+ for (auto item : desktop->getSelection()->items()) {
+ const double width = calcScaleLineWidth(width_typed, item, unit);
+ sp_repr_css_set_property_double(css, "stroke-width", width);
+
+ if (prefs->getBool("/options/dash/scale", true)) {
+ // This will read the old stroke-width to un-scale the pattern.
+ double offset = 0;
+ auto dash = getDashFromStyle(item->style, offset);
+ setScaledDash(css, dash.size(), dash.data(), offset, width);
+ }
+ sp_desktop_apply_css_recursive (item, css, true);
+ }
+ sp_desktop_set_style (desktop, css, false);
+
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(desktop->getDocument(), _("Set stroke width"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+
+ if (unit->abbr == "%") {
+ // reset to 100 percent
+ _last_width = 100.0;
+ (*widthAdj)->set_value(100.0);
+ } else {
+ _last_width = width_typed;
+ }
+ update = false;
+}
+
+/**
+ * Set the stroke dash pattern, scale to the existing width if needed
+ */
+void StrokeStyle::setStrokeDash()
+{
+ if (update) return;
+ update = true;
+
+ auto document = desktop->getDocument();
+ auto prefs = Inkscape::Preferences::get();
+
+ double offset = 0;
+ const auto& dash = dashSelector->get_dash(&offset);
+ update_pattern(dash.size(), dash.data());
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ for (auto item : desktop->getSelection()->items()) {
+ double scale = item->i2doc_affine().descrim();
+ if(prefs->getBool("/options/dash/scale", true)) {
+ scale = item->style->stroke_width.computed * scale;
+ }
+
+ setScaledDash(css, dash.size(), dash.data(), offset, scale);
+ sp_desktop_apply_css_recursive (item, css, true);
+ }
+ sp_desktop_set_style (desktop, css, false);
+
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(document, _("Set stroke dash"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+ update = false;
+}
+
+/**
+ * Set the Miter Limit value only.
+ */
+void StrokeStyle::setStrokeMiter()
+{
+ if (update) return;
+ update = true;
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ auto value = (*miterLimitAdj)->get_value();
+ sp_repr_css_set_property_double(css, "stroke-miterlimit", value);
+
+ for (auto item : desktop->getSelection()->items()) {
+ sp_desktop_apply_css_recursive(item, css, true);
+ }
+ sp_desktop_set_style (desktop, css, false);
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::done(desktop->getDocument(), _("Set stroke miter"),
+ INKSCAPE_ICON("dialog-fill-and-stroke"));
+ update = false;
+}
+
+/**
+ * Returns whether the currently selected stroke width is "hairline"
+ *
+ */
+bool
+StrokeStyle::isHairlineSelected() const
+{
+ return unitSelector->get_active_id() == "hairline";
+}
+
+
+/**
+ * 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(), _("Set stroke style"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+}
+
+/**
+ * Updates the join style toggle buttons
+ */
+void
+StrokeStyle::setJoinButtons(Gtk::ToggleButton *active)
+{
+ joinMiter->set_active(active == joinMiter);
+ miterLimitSpin->set_sensitive(active == joinMiter && !isHairlineSelected());
+ 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
+
+ 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->in_update()) {
+ return;
+ }
+
+ // Per SVG spec, text objects cannot have markers; disable combobox if only texts are selected
+ // They should also be disabled for hairlines, since scaling against a 0-width line doesn't
+ // make sense.
+ combo->set_sensitive(!all_texts && !isHairlineSelected());
+
+ SPObject *marker = nullptr;
+
+ if (!all_texts && !isHairlineSelected()) {
+ 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);
+ }
+ }
+
+ // Scroll the combobox to that marker
+ combo->set_current(marker);
+ }
+
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/stroke-style.h b/src/ui/widget/stroke-style.h
new file mode 100644
index 0000000..0cc29d5
--- /dev/null
+++ b/src/ui/widget/stroke-style.h
@@ -0,0 +1,213 @@
+// 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-style.h" // to get sp_fill_style_widget_set_desktop
+#include "gradient-chemistry.h"
+#include "inkscape.h"
+#include "path-prefix.h"
+#include "preferences.h"
+#include "selection.h"
+#include "style.h"
+
+#include "display/drawing.h"
+
+#include "helper/stock-items.h"
+
+#include "io/sys.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/spw-utilities.h"
+
+
+namespace Gtk {
+class Widget;
+class Container;
+}
+
+namespace Inkscape {
+ namespace Util {
+ class Unit;
+ }
+ namespace UI {
+ namespace Widget {
+ class DashSelector;
+ class MarkerComboBox;
+ 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}
+};
+
+
+SPObject *getMarkerObj(gchar const *n, SPDocument *doc);
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class StrokeStyleButton;
+
+class StrokeStyle : public Gtk::Box
+{
+public:
+ StrokeStyle();
+ ~StrokeStyle() override;
+ void setDesktop(SPDesktop *desktop);
+ void updateLine();
+ void selectionModifiedCB(guint flags);
+ void selectionChangedCB();
+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
+ };
+
+ std::vector<double> getDashFromStyle(SPStyle *style, double &offset);
+
+ void updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo = false);
+ 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 setStrokeWidth();
+ void setStrokeDash();
+ void setStrokeMiter();
+ void setScaledDash(SPCSSAttr *css, int ndash, const double *dash, double offset, double scale);
+ bool isHairlineSelected() const;
+
+ StrokeStyleButton * makeRadioButton(Gtk::RadioButtonGroup &grp,
+ char const *icon,
+ Gtk::Box *hb,
+ StrokeStyleButtonType button_type,
+ gchar const *stroke_style);
+
+ // Callback functions
+ void unitChangedCB();
+ bool areMarkersBeingUpdated();
+ void markerSelectCB(MarkerComboBox *marker_combo, 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;
+ //Gtk::ToggleButton *hairline;
+ 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;
+ Gtk::Entry* _pattern = nullptr;
+ Gtk::Label* _pattern_label = nullptr;
+ void update_pattern(int ndash, const double* pattern);
+ bool _editing_pattern = false;
+
+ gboolean update;
+ double _last_width = 0.0;
+ SPDesktop *desktop;
+ sigc::connection startMarkerConn;
+ sigc::connection midMarkerConn;
+ sigc::connection endMarkerConn;
+
+ Inkscape::Util::Unit const *_old_unit;
+
+ void _handleDocumentReplaced(SPDesktop *, SPDocument *);
+ void enterEditMarkerMode(SPMarkerLoc editMarkerMode);
+ sigc::connection _document_replaced_connection;
+};
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/style-subject.cpp b/src/ui/widget/style-subject.cpp
new file mode 100644
index 0000000..110f6ff
--- /dev/null
+++ b/src/ui/widget/style-subject.cpp
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2007 MenTaLguY <mental@rydia.net>
+ * Abhishek Sharma
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "style-subject.h"
+
+#include "desktop.h"
+#include "desktop-style.h"
+#include "layer-manager.h"
+#include "selection.h"
+
+#include "xml/sp-css-attr.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+StyleSubject::StyleSubject() {
+}
+
+StyleSubject::~StyleSubject() {
+ setDesktop(nullptr);
+}
+
+void StyleSubject::setDesktop(SPDesktop *desktop) {
+ if (desktop != _desktop) {
+ _desktop = desktop;
+ _afterDesktopSwitch(desktop);
+ if (_desktop) {
+ _emitChanged(); // This updates the widgets.
+ }
+ }
+}
+
+StyleSubject::Selection::Selection() = default;
+
+StyleSubject::Selection::~Selection() = default;
+
+Inkscape::Selection *StyleSubject::Selection::_getSelection() const {
+ SPDesktop *desktop = getDesktop();
+ if (desktop) {
+ return desktop->getSelection();
+ } else {
+ return nullptr;
+ }
+}
+
+std::vector<SPObject*> StyleSubject::Selection::list() {
+ Inkscape::Selection *selection = _getSelection();
+ if(selection) {
+ return std::vector<SPObject *>(selection->objects().begin(), selection->objects().end());
+ }
+
+ return std::vector<SPObject*>();
+}
+
+Geom::OptRect StyleSubject::Selection::getBounds(SPItem::BBoxType type) {
+ Inkscape::Selection *selection = _getSelection();
+ if (selection) {
+ return selection->bounds(type);
+ } else {
+ return Geom::OptRect();
+ }
+}
+
+int StyleSubject::Selection::queryStyle(SPStyle *query, int property) {
+ SPDesktop *desktop = getDesktop();
+ if (desktop) {
+ return sp_desktop_query_style(desktop, query, property);
+ } else {
+ return QUERY_STYLE_NOTHING;
+ }
+}
+
+void StyleSubject::Selection::_afterDesktopSwitch(SPDesktop *desktop) {
+ _sel_changed.disconnect();
+ _subsel_changed.disconnect();
+ _sel_modified.disconnect();
+ if (desktop) {
+ _subsel_changed = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &Selection::_emitChanged)));
+ Inkscape::Selection *selection = desktop->getSelection();
+ if (selection) {
+ _sel_changed = selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Selection::_emitChanged)));
+ _sel_modified = selection->connectModified(sigc::mem_fun(*this, &Selection::_emitModified));
+ }
+ }
+}
+
+void StyleSubject::Selection::setCSS(SPCSSAttr *css) {
+ SPDesktop *desktop = getDesktop();
+ if (desktop) {
+ sp_desktop_set_style(desktop, css);
+ }
+}
+
+}
+}
+}
+
+/*
+ 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/ui/widget/style-subject.h b/src/ui/widget/style-subject.h
new file mode 100644
index 0000000..d88c6da
--- /dev/null
+++ b/src/ui/widget/style-subject.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Abstraction for different style widget operands. Used by ObjectCompositeSettings in Layers and
+ * Fill and Stroke dialogs. Dialog is responsible for keeping desktop pointer valid.
+ *
+ * This class is due to the need to differentiate between layers and objects but a layer is just a
+ * a group object with an extra tag. There should be no need to differentiate between the two.
+ * To do: remove this class and intergrate the functionality into ObjectCompositeSettings.
+ */
+/*
+ * Copyright (C) 2007 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H
+#define SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H
+
+#include <optional>
+#include <2geom/rect.h>
+#include <cstddef>
+#include <sigc++/sigc++.h>
+
+#include "object/sp-item.h"
+#include "object/sp-tag.h"
+#include "object/sp-tag-use.h"
+#include "object/sp-tag-use-reference.h"
+
+class SPDesktop;
+class SPObject;
+class SPCSSAttr;
+class SPStyle;
+
+namespace Inkscape {
+class Selection;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class StyleSubject {
+public:
+ class Selection;
+ class CurrentLayer;
+
+
+ StyleSubject();
+ virtual ~StyleSubject();
+
+ void setDesktop(SPDesktop *desktop);
+ SPDesktop *getDesktop() const { return _desktop; }
+
+ virtual Geom::OptRect getBounds(SPItem::BBoxType type) = 0;
+ virtual int queryStyle(SPStyle *query, int property) = 0;
+ virtual void setCSS(SPCSSAttr *css) = 0;
+ virtual std::vector<SPObject*> list(){return std::vector<SPObject*>();};
+
+ sigc::connection connectChanged(sigc::signal<void>::slot_type slot) {
+ return _changed_signal.connect(slot);
+ }
+
+protected:
+ virtual void _afterDesktopSwitch(SPDesktop */*desktop*/) {}
+ void _emitChanged() { _changed_signal.emit(); }
+ void _emitModified(Inkscape::Selection* selection, guint flags) {
+ // Do not say this object has styles unless it's style has been modified
+ if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+ _emitChanged();
+ }
+ }
+
+private:
+ sigc::signal<void> _changed_signal;
+ SPDesktop *_desktop = nullptr;
+};
+
+class StyleSubject::Selection : public StyleSubject {
+public:
+ Selection();
+ ~Selection() override;
+
+ Geom::OptRect getBounds(SPItem::BBoxType type) override;
+ int queryStyle(SPStyle *query, int property) override;
+ void setCSS(SPCSSAttr *css) override;
+ std::vector<SPObject*> list() override;
+
+protected:
+ void _afterDesktopSwitch(SPDesktop *desktop) override;
+
+private:
+ Inkscape::Selection *_getSelection() const;
+
+ sigc::connection _sel_changed;
+ sigc::connection _subsel_changed;
+ sigc::connection _sel_modified;
+};
+
+}
+}
+}
+
+#endif // SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_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/ui/widget/style-swatch.cpp b/src/ui/widget/style-swatch.cpp
new file mode 100644
index 0000000..a79f9d9
--- /dev/null
+++ b/src/ui/widget/style-swatch.cpp
@@ -0,0 +1,386 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Static style swatch (fill, stroke, opacity).
+ */
+/* Authors:
+ * buliabyak@gmail.com
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2005-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "style-swatch.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/grid.h>
+
+#include "inkscape.h"
+#include "style.h"
+
+#include "actions/actions-tools.h" // Open tool preferences.
+
+#include "object/sp-linear-gradient.h"
+#include "object/sp-pattern.h"
+#include "object/sp-radial-gradient.h"
+
+#include "ui/widget/color-preview.h"
+#include "util/units.h"
+
+#include "widgets/spw-utilities.h"
+
+#include "xml/sp-css-attr.h"
+#include "xml/attribute-record.h"
+
+enum {
+ SS_FILL,
+ SS_STROKE
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * Watches whether the tool uses the current style.
+ */
+class StyleSwatch::ToolObserver : public Inkscape::Preferences::Observer {
+public:
+ ToolObserver(Glib::ustring const &path, StyleSwatch &ss) :
+ Observer(path),
+ _style_swatch(ss)
+ {}
+ void notify(Inkscape::Preferences::Entry const &val) override;
+private:
+ StyleSwatch &_style_swatch;
+};
+
+/**
+ * Watches for changes in the observed style pref.
+ */
+class StyleSwatch::StyleObserver : public Inkscape::Preferences::Observer {
+public:
+ StyleObserver(Glib::ustring const &path, StyleSwatch &ss) :
+ Observer(path),
+ _style_swatch(ss)
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ this->notify(prefs->getEntry(path));
+ }
+ void notify(Inkscape::Preferences::Entry const &val) override {
+ SPCSSAttr *css = val.getInheritedStyle();
+ _style_swatch.setStyle(css);
+ sp_repr_css_attr_unref(css);
+ }
+private:
+ StyleSwatch &_style_swatch;
+};
+
+void StyleSwatch::ToolObserver::notify(Inkscape::Preferences::Entry const &val)
+{
+ bool usecurrent = val.getBool();
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (_style_swatch._style_obs) delete _style_swatch._style_obs;
+
+ if (usecurrent) {
+ _style_swatch._style_obs = new StyleObserver("/desktop/style", _style_swatch);
+
+ // If desktop's last-set style is empty, a tool uses its own fixed style even if set to use
+ // last-set (so long as it's empty). To correctly show this, we get the tool's style
+ // if the desktop's style is empty.
+ SPCSSAttr *css = prefs->getStyle("/desktop/style");
+ const auto & al = css->attributeList();
+ if (al.empty()) {
+ SPCSSAttr *css2 = prefs->getInheritedStyle(_style_swatch._tool_path + "/style");
+ _style_swatch.setStyle(css2);
+ sp_repr_css_attr_unref(css2);
+ }
+ sp_repr_css_attr_unref(css);
+ } else {
+ _style_swatch._style_obs = new StyleObserver(_style_swatch._tool_path + "/style", _style_swatch);
+ }
+ prefs->addObserver(*_style_swatch._style_obs);
+}
+
+StyleSwatch::StyleSwatch(SPCSSAttr *css, gchar const *main_tip)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL),
+ _desktop(nullptr),
+ _css(nullptr),
+ _tool_obs(nullptr),
+ _style_obs(nullptr),
+ _table(Gtk::manage(new Gtk::Grid())),
+ _sw_unit(nullptr),
+ _stroke(Gtk::ORIENTATION_HORIZONTAL)
+{
+ set_name("StyleSwatch");
+ _label[SS_FILL].set_markup(_("Fill:"));
+ _label[SS_STROKE].set_markup(_("Stroke:"));
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ _label[i].set_halign(Gtk::ALIGN_START);
+ _label[i].set_valign(Gtk::ALIGN_CENTER);
+ _label[i].set_margin_top(0);
+ _label[i].set_margin_bottom(0);
+ _label[i].set_margin_start(0);
+ _label[i].set_margin_end(0);
+
+ _color_preview[i] = new Inkscape::UI::Widget::ColorPreview (0);
+ }
+
+ _opacity_value.set_halign(Gtk::ALIGN_START);
+ _opacity_value.set_valign(Gtk::ALIGN_CENTER);
+ _opacity_value.set_margin_top(0);
+ _opacity_value.set_margin_bottom(0);
+ _opacity_value.set_margin_start(0);
+ _opacity_value.set_margin_end(0);
+
+ _table->set_column_spacing(2);
+ _table->set_row_spacing(0);
+
+ _stroke.pack_start(_place[SS_STROKE]);
+ _stroke_width_place.add(_stroke_width);
+ _stroke.pack_start(_stroke_width_place, Gtk::PACK_SHRINK);
+
+ _opacity_place.add(_opacity_value);
+
+ _table->attach(_label[SS_FILL], 0, 0, 1, 1);
+ _table->attach(_label[SS_STROKE], 0, 1, 1, 1);
+ _table->attach(_place[SS_FILL], 1, 0, 1, 1);
+ _table->attach(_stroke, 1, 1, 1, 1);
+ _table->attach(_empty_space, 2, 0, 1, 2);
+ _table->attach(_opacity_place, 2, 0, 1, 2);
+ _swatch.add(*_table);
+ pack_start(_swatch, true, true, 0);
+
+ set_size_request (STYLE_SWATCH_WIDTH, -1);
+
+ setStyle (css);
+
+ _swatch.signal_button_press_event().connect(sigc::mem_fun(*this, &StyleSwatch::on_click));
+
+ if (main_tip)
+ {
+ _swatch.set_tooltip_text(main_tip);
+ }
+}
+
+void StyleSwatch::setToolName(const Glib::ustring& tool_name) {
+ _tool_name = tool_name;
+}
+
+void StyleSwatch::setDesktop(SPDesktop *desktop) {
+ _desktop = desktop;
+}
+
+bool
+StyleSwatch::on_click(GdkEventButton */*event*/)
+{
+ if (_desktop && !_tool_name.empty()) {
+ auto win = _desktop->getInkscapeWindow();
+ open_tool_preferences(win, _tool_name);
+ return true;
+ }
+ return false;
+}
+
+StyleSwatch::~StyleSwatch()
+{
+ if (_css)
+ sp_repr_css_attr_unref (_css);
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ delete _color_preview[i];
+ }
+
+ if (_style_obs) delete _style_obs;
+ if (_tool_obs) delete _tool_obs;
+}
+
+void
+StyleSwatch::setWatchedTool(const char *path, bool synthesize)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (_tool_obs) {
+ delete _tool_obs;
+ _tool_obs = nullptr;
+ }
+
+ if (path) {
+ _tool_path = path;
+ _tool_obs = new ToolObserver(_tool_path + "/usecurrent", *this);
+ prefs->addObserver(*_tool_obs);
+ } else {
+ _tool_path = "";
+ }
+
+ // hack until there is a real synthesize events function for prefs,
+ // which shouldn't be hard to write once there is sufficient need for it
+ if (synthesize && _tool_obs) {
+ _tool_obs->notify(prefs->getEntry(_tool_path + "/usecurrent"));
+ }
+}
+
+
+void StyleSwatch::setStyle(SPCSSAttr *css)
+{
+ if (_css)
+ sp_repr_css_attr_unref (_css);
+
+ if (!css)
+ return;
+
+ _css = sp_repr_css_attr_new();
+ sp_repr_css_merge(_css, css);
+
+ Glib::ustring css_string;
+ sp_repr_css_write_string (_css, css_string);
+
+ SPStyle style(_desktop ? _desktop->getDocument() : nullptr);
+ if (!css_string.empty()) {
+ style.mergeString(css_string.c_str());
+ }
+ setStyle (&style);
+}
+
+void StyleSwatch::setStyle(SPStyle *query)
+{
+ _place[SS_FILL].remove();
+ _place[SS_STROKE].remove();
+
+ bool has_stroke = true;
+
+ for (int i = SS_FILL; i <= SS_STROKE; i++) {
+ Gtk::EventBox *place = &(_place[i]);
+
+ SPIPaint *paint;
+ if (i == SS_FILL) {
+ paint = &(query->fill);
+ } else {
+ paint = &(query->stroke);
+ }
+
+ if (paint->set && paint->isPaintserver()) {
+ SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (query) : SP_STYLE_STROKE_SERVER (query);
+
+ if (SP_IS_LINEARGRADIENT (server)) {
+ _value[i].set_markup(_("L Gradient"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Linear gradient (fill)")) : (_("Linear gradient (stroke)")));
+ } else if (SP_IS_RADIALGRADIENT (server)) {
+ _value[i].set_markup(_("R Gradient"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Radial gradient (fill)")) : (_("Radial gradient (stroke)")));
+ } else if (SP_IS_PATTERN (server)) {
+ _value[i].set_markup(_("Pattern"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Pattern (fill)")) : (_("Pattern (stroke)")));
+ }
+
+ } else if (paint->set && paint->isColor()) {
+ guint32 color = paint->value.color.toRGBA32( SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query->fill_opacity.value : query->stroke_opacity.value) );
+ ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color);
+ _color_preview[i]->show_all();
+ place->add(*_color_preview[i]);
+ gchar *tip;
+ if (i == SS_FILL) {
+ tip = g_strdup_printf (_("Fill: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color));
+ } else {
+ tip = g_strdup_printf (_("Stroke: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color));
+ }
+ place->set_tooltip_text(tip);
+ g_free (tip);
+ } else if (paint->set && paint->isNone()) {
+ _value[i].set_markup(C_("Fill and stroke", "<i>None</i>"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (C_("Fill and stroke", "No fill")) : (C_("Fill and stroke", "No stroke")));
+ if (i == SS_STROKE) has_stroke = false;
+ } else if (!paint->set) {
+ _value[i].set_markup(_("<b>Unset</b>"));
+ place->add(_value[i]);
+ place->set_tooltip_text((i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke")));
+ if (i == SS_STROKE) has_stroke = false;
+ }
+ }
+
+// Now query stroke_width
+ if (has_stroke) {
+ if (query->stroke_extensions.hairline) {
+ Glib::ustring swidth = "<small>";
+ swidth += _("Hairline");
+ swidth += "</small>";
+ _stroke_width.set_markup(swidth.c_str());
+ auto str = Glib::ustring::compose(_("Stroke width: %1"), _("Hairline"));
+ _stroke_width_place.set_tooltip_text(str);
+ } else {
+ double w;
+ if (_sw_unit) {
+ w = Inkscape::Util::Quantity::convert(query->stroke_width.computed, "px", _sw_unit);
+ } else {
+ w = query->stroke_width.computed;
+ }
+
+ {
+ gchar *str = g_strdup_printf(" %.3g", w);
+ Glib::ustring swidth = "<small>";
+ swidth += str;
+ swidth += "</small>";
+ _stroke_width.set_markup(swidth.c_str());
+ g_free (str);
+ }
+ {
+ gchar *str = g_strdup_printf(_("Stroke width: %.5g%s"),
+ w,
+ _sw_unit? _sw_unit->abbr.c_str() : "px");
+ _stroke_width_place.set_tooltip_text(str);
+ g_free (str);
+ }
+ }
+ } else {
+ _stroke_width_place.set_tooltip_text("");
+ _stroke_width.set_markup("");
+ _stroke_width.set_has_tooltip(false);
+ }
+
+ gdouble op = SP_SCALE24_TO_FLOAT(query->opacity.value);
+ if (op != 1) {
+ {
+ gchar *str;
+ str = g_strdup_printf(_("O: %2.0f"), (op*100.0));
+ Glib::ustring opacity = "<small>";
+ opacity += str;
+ opacity += "</small>";
+ _opacity_value.set_markup (opacity.c_str());
+ g_free (str);
+ }
+ {
+ gchar *str = g_strdup_printf(_("Opacity: %2.1f %%"), (op*100.0));
+ _opacity_place.set_tooltip_text(str);
+ g_free (str);
+ }
+ } else {
+ _opacity_place.set_tooltip_text("");
+ _opacity_value.set_markup("");
+ _opacity_value.set_has_tooltip(false);
+ }
+
+ show_all();
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/style-swatch.h b/src/ui/widget/style-swatch.h
new file mode 100644
index 0000000..31165f5
--- /dev/null
+++ b/src/ui/widget/style-swatch.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Static style swatch (fill, stroke, opacity)
+ */
+/* Authors:
+ * buliabyak@gmail.com
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2005-2008 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_CURRENT_STYLE_H
+#define INKSCAPE_UI_CURRENT_STYLE_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/enums.h>
+
+#include "desktop.h"
+#include "preferences.h"
+
+constexpr int STYLE_SWATCH_WIDTH = 135;
+
+class SPStyle;
+class SPCSSAttr;
+
+namespace Gtk {
+class Grid;
+}
+
+namespace Inkscape {
+
+namespace Util {
+ class Unit;
+}
+
+namespace UI {
+namespace Widget {
+
+class StyleSwatch : public Gtk::Box
+{
+public:
+ StyleSwatch (SPCSSAttr *attr, gchar const *main_tip);
+
+ ~StyleSwatch() override;
+
+ void setStyle(SPStyle *style);
+ void setStyle(SPCSSAttr *attr);
+ SPCSSAttr *getStyle();
+
+ void setWatchedTool (const char *path, bool synthesize);
+ void setToolName(const Glib::ustring& tool_name);
+ void setDesktop(SPDesktop *desktop);
+ bool on_click(GdkEventButton *event);
+
+private:
+ class ToolObserver;
+ class StyleObserver;
+
+ SPDesktop *_desktop;
+ Glib::ustring _tool_name;
+ SPCSSAttr *_css;
+ ToolObserver *_tool_obs;
+ StyleObserver *_style_obs;
+ Glib::ustring _tool_path;
+
+ Gtk::EventBox _swatch;
+
+ Gtk::Grid *_table;
+
+ Gtk::Label _label[2];
+ Gtk::Box _empty_space;
+ Gtk::EventBox _place[2];
+ Gtk::EventBox _opacity_place;
+ Gtk::Label _value[2];
+ Gtk::Label _opacity_value;
+ Gtk::Widget *_color_preview[2];
+ Glib::ustring __color[2];
+ Gtk::Box _stroke;
+ Gtk::EventBox _stroke_width_place;
+ Gtk::Label _stroke_width;
+
+ Inkscape::Util::Unit *_sw_unit;
+
+friend class ToolObserver;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_BUTTON_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/ui/widget/swatch-selector.cpp b/src/ui/widget/swatch-selector.cpp
new file mode 100644
index 0000000..edff1d5
--- /dev/null
+++ b/src/ui/widget/swatch-selector.cpp
@@ -0,0 +1,148 @@
+// 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 "object/sp-stop.h"
+
+#include "svg/css-ostringstream.h"
+#include "svg/svg-color.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/gradient-selector.h"
+
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+SwatchSelector::SwatchSelector() :
+ Gtk::Box(Gtk::ORIENTATION_VERTICAL),
+ _gsel(nullptr),
+ _updating_color(false)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ _gsel = Gtk::manage(new GradientSelector());
+ _gsel->setMode(GradientSelector::MODE_SWATCH);
+
+ _gsel->show();
+
+ pack_start(*_gsel);
+
+ auto color_selector = Gtk::manage(new ColorNotebook(_selected_color));
+ color_selector->set_label(_("Swatch 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;
+}
+
+GradientSelector *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, _("Change swatch color"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+}
+
+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 Widget
+} // namespace UI
+} // 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/ui/widget/swatch-selector.h b/src/ui/widget/swatch-selector.h
new file mode 100644
index 0000000..67486a5
--- /dev/null
+++ b/src/ui/widget/swatch-selector.h
@@ -0,0 +1,65 @@
+// 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;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class GradientSelector;
+
+class SwatchSelector : public Gtk::Box
+{
+public:
+ SwatchSelector();
+ ~SwatchSelector() override;
+
+ void connectchangedHandler( GCallback handler, void *data );
+
+ void setVector(SPDocument *doc, SPGradient *vector);
+
+ GradientSelector *getGradientSelector();
+
+private:
+ void _grabbedCb();
+ void _draggedCb();
+ void _releasedCb();
+ void _changedCb();
+
+ GradientSelector *_gsel;
+ Inkscape::UI::SelectedColor _selected_color;
+ bool _updating_color;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/text.cpp b/src/ui/widget/text.cpp
new file mode 100644
index 0000000..656ec45
--- /dev/null
+++ b/src/ui/widget/text.cpp
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "text.h"
+#include <gtkmm/entry.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Text::Text(Glib::ustring const &label, Glib::ustring const &tooltip,
+ Glib::ustring const &suffix,
+ Glib::ustring const &icon,
+ bool mnemonic)
+ : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic),
+ setProgrammatically(false)
+{
+}
+
+Glib::ustring const Text::getText() const
+{
+ g_assert(_widget != nullptr);
+ return static_cast<Gtk::Entry*>(_widget)->get_text();
+}
+
+void Text::setText(Glib::ustring const text)
+{
+ g_assert(_widget != nullptr);
+ setProgrammatically = true; // callback is supposed to reset back, if it cares
+ static_cast<Gtk::Entry*>(_widget)->set_text(text); // FIXME: set correctly
+}
+
+Glib::SignalProxy0<void> Text::signal_activate()
+{
+ return static_cast<Gtk::Entry*>(_widget)->signal_activate();
+}
+
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/text.h b/src/ui/widget/text.h
new file mode 100644
index 0000000..87c9357
--- /dev/null
+++ b/src/ui/widget/text.h
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Carl Hetherington <inkscape@carlh.net>
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ *
+ * Copyright (C) 2004 Carl Hetherington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_TEXT_H
+#define INKSCAPE_UI_WIDGET_TEXT_H
+
+#include "labelled.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A labelled text box, with optional icon or suffix, for entering arbitrary number values.
+ */
+class Text : public Labelled
+{
+public:
+
+ /**
+ * Construct a Text Widget.
+ *
+ * @param label Label.
+ * @param suffix Suffix, placed after the widget (defaults to "").
+ * @param icon Icon filename, placed before the label (defaults to "").
+ * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label
+ * indicates the next character should be used for the
+ * mnemonic accelerator key (defaults to false).
+ */
+ Text(Glib::ustring const &label,
+ Glib::ustring const &tooltip,
+ Glib::ustring const &suffix = "",
+ Glib::ustring const &icon = "",
+ bool mnemonic = true);
+
+ /**
+ * Get the text in the entry.
+ */
+ Glib::ustring const getText() const;
+
+ /**
+ * Sets the text of the text entry.
+ */
+ void setText(Glib::ustring const text);
+
+ void update();
+
+ /**
+ * Signal raised when the spin button's value changes.
+ */
+ Glib::SignalProxy0<void> signal_activate();
+
+ bool setProgrammatically; // true if the value was set by setValue, not changed by the user;
+ // if a callback checks it, it must reset it back to false
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_TEXT_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/ui/widget/tolerance-slider.cpp b/src/ui/widget/tolerance-slider.cpp
new file mode 100644
index 0000000..817e783
--- /dev/null
+++ b/src/ui/widget/tolerance-slider.cpp
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tolerance-slider.h"
+
+#include "registry.h"
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scale.h>
+
+#include "inkscape.h"
+#include "document.h"
+#include "document-undo.h"
+#include "desktop.h"
+
+#include "object/sp-namedview.h"
+
+#include "svg/stringstream.h"
+
+#include "xml/repr.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+//===================================================
+
+//---------------------------------------------------
+
+
+
+//====================================================
+
+ToleranceSlider::ToleranceSlider(const Glib::ustring& label1, const Glib::ustring& label2, const Glib::ustring& label3, const Glib::ustring& tip1, const Glib::ustring& tip2, const Glib::ustring& tip3, const Glib::ustring& key, Registry& wr)
+: _vbox(nullptr)
+{
+ init(label1, label2, label3, tip1, tip2, tip3, key, wr);
+}
+
+ToleranceSlider::~ToleranceSlider()
+{
+ if (_vbox) delete _vbox;
+ _scale_changed_connection.disconnect();
+}
+
+void ToleranceSlider::init (const Glib::ustring& label1, const Glib::ustring& label2, const Glib::ustring& label3, const Glib::ustring& tip1, const Glib::ustring& tip2, const Glib::ustring& tip3, const Glib::ustring& key, Registry& wr)
+{
+ // hbox = label + slider
+ //
+ // e.g.
+ //
+ // snap distance |-------X---| 37
+
+ // vbox = checkbutton
+ // +
+ // hbox
+
+ _vbox = new Gtk::Box(Gtk::ORIENTATION_VERTICAL);
+ _hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+
+ Gtk::Label *theLabel1 = Gtk::manage(new Gtk::Label(label1));
+ theLabel1->set_use_underline();
+ theLabel1->set_halign(Gtk::ALIGN_START);
+ theLabel1->set_valign(Gtk::ALIGN_CENTER);
+ // align the label with the checkbox text above by indenting 22 px.
+ _hbox->pack_start(*theLabel1, Gtk::PACK_EXPAND_WIDGET, 22);
+
+ _hscale = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+ _hscale->set_range(1.0, 51.0);
+
+ theLabel1->set_mnemonic_widget (*_hscale);
+ _hscale->set_draw_value (true);
+ _hscale->set_value_pos (Gtk::POS_RIGHT);
+ _hscale->set_size_request (100, -1);
+ _old_val = 10;
+ _hscale->set_value (_old_val);
+ _hscale->set_tooltip_text (tip1);
+ _hbox->add (*_hscale);
+
+
+ Gtk::Label *theLabel2 = Gtk::manage(new Gtk::Label(label2));
+ theLabel2->set_use_underline();
+ Gtk::Label *theLabel3 = Gtk::manage(new Gtk::Label(label3));
+ theLabel3->set_use_underline();
+ _button1 = Gtk::manage(new Gtk::RadioButton);
+ _radio_button_group = _button1->get_group();
+ _button2 = Gtk::manage(new Gtk::RadioButton);
+ _button2->set_group(_radio_button_group);
+ _button1->set_tooltip_text (tip2);
+ _button2->set_tooltip_text (tip3);
+ _button1->add (*theLabel3);
+ _button1->set_halign(Gtk::ALIGN_START);
+ _button1->set_valign(Gtk::ALIGN_CENTER);
+ _button2->add (*theLabel2);
+ _button2->set_halign(Gtk::ALIGN_START);
+ _button2->set_valign(Gtk::ALIGN_CENTER);
+
+ _vbox->add (*_button1);
+ _vbox->add (*_button2);
+ // Here we need some extra pixels to get the vertical spacing right. Why?
+ _vbox->pack_end(*_hbox, true, true, 3); // add 3 px.
+ _key = key;
+ _scale_changed_connection = _hscale->signal_value_changed().connect (sigc::mem_fun (*this, &ToleranceSlider::on_scale_changed));
+ _btn_toggled_connection = _button2->signal_toggled().connect (sigc::mem_fun (*this, &ToleranceSlider::on_toggled));
+ _wr = &wr;
+ _vbox->show_all_children();
+}
+
+void ToleranceSlider::setValue (double val)
+{
+ auto adj = _hscale->get_adjustment();
+
+ adj->set_lower (1.0);
+ adj->set_upper (51.0);
+ adj->set_step_increment (1.0);
+
+ if (val > 9999.9) // magic value 10000.0
+ {
+ _button1->set_active (true);
+ _button2->set_active (false);
+ _hbox->set_sensitive (false);
+ val = 50.0;
+ }
+ else
+ {
+ _button1->set_active (false);
+ _button2->set_active (true);
+ _hbox->set_sensitive (true);
+ }
+ _hscale->set_value (val);
+ _hbox->show_all();
+}
+
+void ToleranceSlider::setLimits (double theMin, double theMax)
+{
+ _hscale->set_range (theMin, theMax);
+ _hscale->get_adjustment()->set_step_increment (1);
+}
+
+void ToleranceSlider::on_scale_changed()
+{
+ update (_hscale->get_value());
+}
+
+void ToleranceSlider::on_toggled()
+{
+ if (!_button2->get_active())
+ {
+ _old_val = _hscale->get_value();
+ _hbox->set_sensitive (false);
+ _hbox->show_all();
+ setValue (10000.0);
+ update (10000.0);
+ }
+ else
+ {
+ _hbox->set_sensitive (true);
+ _hbox->show_all();
+ setValue (_old_val);
+ update (_old_val);
+ }
+}
+
+void ToleranceSlider::update (double val)
+{
+ if (_wr->isUpdating())
+ return;
+
+ SPDesktop *dt = _wr->desktop();
+ if (!dt)
+ return;
+
+ Inkscape::SVGOStringStream os;
+ os << val;
+
+ _wr->setUpdating (true);
+
+ SPDocument *doc = dt->getDocument();
+ bool saved = DocumentUndo::getUndoSensitive(doc);
+ DocumentUndo::setUndoSensitive(doc, false);
+ Inkscape::XML::Node *repr = dt->getNamedView()->getRepr();
+ repr->setAttribute(_key, os.str());
+ DocumentUndo::setUndoSensitive(doc, saved);
+
+ doc->setModifiedSinceSave();
+
+ _wr->setUpdating (false);
+}
+
+
+} // namespace Dialog
+} // namespace UI
+} // 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/ui/widget/tolerance-slider.h b/src/ui/widget/tolerance-slider.h
new file mode 100644
index 0000000..cb12116
--- /dev/null
+++ b/src/ui/widget/tolerance-slider.h
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ *
+ * Copyright (C) 2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_
+#define INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_
+
+#include <gtkmm/radiobuttongroup.h>
+
+namespace Gtk {
+class RadioButton;
+class Scale;
+class Box;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Registry;
+
+/**
+ * Implementation of tolerance slider widget.
+ * This widget is part of the Document properties dialog.
+ */
+class ToleranceSlider {
+public:
+ ToleranceSlider(const Glib::ustring& label1,
+ const Glib::ustring& label2,
+ const Glib::ustring& label3,
+ const Glib::ustring& tip1,
+ const Glib::ustring& tip2,
+ const Glib::ustring& tip3,
+ const Glib::ustring& key,
+ Registry& wr);
+ ~ToleranceSlider();
+ void setValue (double);
+ void setLimits (double, double);
+ Gtk::Box* _vbox;
+private:
+ void init (const Glib::ustring& label1,
+ const Glib::ustring& label2,
+ const Glib::ustring& label3,
+ const Glib::ustring& tip1,
+ const Glib::ustring& tip2,
+ const Glib::ustring& tip3,
+ const Glib::ustring& key,
+ Registry& wr);
+
+protected:
+ void on_scale_changed();
+ void on_toggled();
+ void update (double val);
+ Gtk::Box *_hbox;
+ Gtk::Scale *_hscale;
+ Gtk::RadioButtonGroup _radio_button_group;
+ Gtk::RadioButton *_button1;
+ Gtk::RadioButton *_button2;
+ Registry *_wr;
+ Glib::ustring _key;
+ sigc::connection _scale_changed_connection;
+ sigc::connection _btn_toggled_connection;
+ double _old_val;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__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/ui/widget/unit-menu.cpp b/src/ui/widget/unit-menu.cpp
new file mode 100644
index 0000000..a0ae163
--- /dev/null
+++ b/src/ui/widget/unit-menu.cpp
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cmath>
+
+#include "unit-menu.h"
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+UnitMenu::UnitMenu() : _type(UNIT_TYPE_NONE)
+{
+ set_active(0);
+ add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
+ signal_scroll_event().connect([](GdkEventScroll*){ return false; });
+}
+
+UnitMenu::~UnitMenu() = default;
+
+bool UnitMenu::setUnitType(UnitType unit_type)
+{
+ // Expand the unit widget with unit entries from the unit table
+ UnitTable::UnitMap m = unit_table.units(unit_type);
+
+ for (auto & i : m) {
+ append(i.first);
+ }
+ _type = unit_type;
+ set_active_text(unit_table.primary(unit_type));
+
+ return true;
+}
+
+bool UnitMenu::resetUnitType(UnitType unit_type)
+{
+ remove_all();
+
+ return setUnitType(unit_type);
+}
+
+void UnitMenu::addUnit(Unit const& u)
+{
+ unit_table.addUnit(u, false);
+ append(u.abbr);
+}
+
+Unit const * UnitMenu::getUnit() const
+{
+ if (get_active_text() == "") {
+ g_assert(_type != UNIT_TYPE_NONE);
+ return unit_table.getUnit(unit_table.primary(_type));
+ }
+ return unit_table.getUnit(get_active_text());
+}
+
+bool UnitMenu::setUnit(Glib::ustring const & unit)
+{
+ // TODO: Determine if 'unit' is available in the dropdown.
+ // If not, return false
+
+ set_active_text(unit);
+ return true;
+}
+
+Glib::ustring UnitMenu::getUnitAbbr() const
+{
+ if (get_active_text() == "") {
+ return "";
+ }
+ return getUnit()->abbr;
+}
+
+UnitType UnitMenu::getUnitType() const
+{
+ return getUnit()->type;
+}
+
+double UnitMenu::getUnitFactor() const
+{
+ return getUnit()->factor;
+}
+
+int UnitMenu::getDefaultDigits() const
+{
+ return getUnit()->defaultDigits();
+}
+
+double UnitMenu::getDefaultStep() const
+{
+ int factor_digits = -1*int(log10(getUnit()->factor));
+ return pow(10.0, factor_digits);
+}
+
+double UnitMenu::getDefaultPage() const
+{
+ return 10 * getDefaultStep();
+}
+
+double UnitMenu::getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr) const
+{
+ double old_factor = getUnit()->factor;
+ if (old_unit_abbr != "no_unit") {
+ old_factor = unit_table.getUnit(old_unit_abbr)->factor;
+ }
+ Unit const * new_unit = unit_table.getUnit(new_unit_abbr);
+
+ // Catch the case of zero or negative unit factors (error!)
+ if (old_factor < 0.0000001 ||
+ new_unit->factor < 0.0000001) {
+ // TODO: Should we assert here?
+ return 0.00;
+ }
+
+ return old_factor / new_unit->factor;
+}
+
+bool UnitMenu::isAbsolute() const
+{
+ return getUnitType() != UNIT_TYPE_DIMENSIONLESS;
+}
+
+bool UnitMenu::isRadial() const
+{
+ return getUnitType() == UNIT_TYPE_RADIAL;
+}
+
+bool UnitMenu::on_scroll_event(GdkEventScroll *event) { return false; }
+
+} // namespace Widget
+} // namespace UI
+} // 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/ui/widget/unit-menu.h b/src/ui/widget/unit-menu.h
new file mode 100644
index 0000000..1f10cc2
--- /dev/null
+++ b/src/ui/widget/unit-menu.h
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Bryce Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_UNIT_H
+#define INKSCAPE_UI_WIDGET_UNIT_H
+
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm.h>
+#include "util/units.h"
+
+using namespace Inkscape::Util;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A drop down menu for choosing unit types.
+ */
+class UnitMenu : public Gtk::ComboBoxText
+{
+public:
+
+ /**
+ * Construct a UnitMenu
+ */
+ UnitMenu();
+
+ /* GtkBuilder constructor */
+ UnitMenu(BaseObjectType* cobject, const Glib::RefPtr<Gtk::Builder>& refGlade):Gtk::ComboBoxText(cobject){
+ UnitMenu();
+ };
+
+ ~UnitMenu() override;
+
+ /**
+ * Adds the unit type to the widget. This extracts the corresponding
+ * units from the unit map matching the given type, and appends them
+ * to the dropdown widget. It causes the primary unit for the given
+ * unit_type to be selected.
+ */
+ bool setUnitType(UnitType unit_type);
+
+ /**
+ * Removes all unit entries, then adds the unit type to the widget.
+ * This extracts the corresponding
+ * units from the unit map matching the given type, and appends them
+ * to the dropdown widget. It causes the primary unit for the given
+ * unit_type to be selected.
+ */
+ bool resetUnitType(UnitType unit_type);
+
+ /**
+ * Adds a unit, possibly user-defined, to the menu.
+ */
+ void addUnit(Unit const& u);
+
+ /**
+ * Sets the dropdown widget to the given unit abbreviation.
+ * Returns true if the unit was selectable, false if not
+ * (i.e., if the unit was not present in the widget).
+ */
+ bool setUnit(Glib::ustring const &unit);
+
+ /**
+ * Returns the Unit object corresponding to the current selection
+ * in the dropdown widget.
+ */
+ Unit const * getUnit() const;
+
+ /**
+ * Returns the abbreviated unit name of the selected unit.
+ */
+ Glib::ustring getUnitAbbr() const;
+
+ /**
+ * Returns the UnitType of the selected unit.
+ */
+ UnitType getUnitType() const;
+
+ /**
+ * Returns the unit factor for the selected unit.
+ */
+ double getUnitFactor() const;
+
+ /**
+ * Returns the recommended number of digits for displaying
+ * numbers of this unit type.
+ */
+ int getDefaultDigits() const;
+
+ /**
+ * Returns the recommended step size in spin buttons
+ * displaying units of this type.
+ */
+ double getDefaultStep() const;
+
+ /**
+ * Returns the recommended page size (when hitting pgup/pgdn)
+ * in spin buttons displaying units of this type.
+ */
+ double getDefaultPage() const;
+
+ /**
+ * Returns the conversion factor required to convert values
+ * of the currently selected unit into units of type
+ * new_unit_abbr.
+ */
+ double getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr = "no_unit") const;
+
+ /**
+ * Returns true if the selected unit is not dimensionless
+ * (false for %, true for px, pt, cm, etc).
+ */
+ bool isAbsolute() const;
+
+ /**
+ * Returns true if the selected unit is radial (deg or rad).
+ */
+ bool isRadial() const;
+
+protected:
+ UnitType _type;
+ /**
+ * block scroll from widget if is inside a scrolled window.
+ */
+ bool on_scroll_event(GdkEventScroll *event) override;
+
+ Gtk::ComboBoxText* _combo;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_UNIT_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/ui/widget/unit-tracker.cpp b/src/ui/widget/unit-tracker.cpp
new file mode 100644
index 0000000..7c52b3b
--- /dev/null
+++ b/src/ui/widget/unit-tracker.cpp
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::UI::Widget::UnitTracker
+ * Simple mediator to synchronize changes to unit menus
+ *
+ * Authors:
+ * Jon A. Cruz <jon@joncruz.org>
+ * Matthew Petroff <matthew@mpetroff.net>
+ *
+ * Copyright (C) 2007 Jon A. Cruz
+ * Copyright (C) 2013 Matthew Petroff
+ * Copyright (C) 2018 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <algorithm>
+#include <iostream>
+
+#include "unit-tracker.h"
+
+#include "combo-tool-item.h"
+
+#define COLUMN_STRING 0
+
+using Inkscape::Util::UnitTable;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+UnitTracker::UnitTracker(UnitType unit_type) :
+ _active(0),
+ _isUpdating(false),
+ _activeUnit(nullptr),
+ _activeUnitInitialized(false),
+ _store(nullptr),
+ _priorValues()
+{
+ UnitTable::UnitMap m = unit_table.units(unit_type);
+
+ ComboToolItemColumns columns;
+ _store = Gtk::ListStore::create(columns);
+ Gtk::TreeModel::Row row;
+
+ for (auto & m_iter : m) {
+
+ Glib::ustring unit = m_iter.first;
+
+ row = *(_store->append());
+ row[columns.col_label ] = unit;
+ row[columns.col_value ] = unit;
+ row[columns.col_tooltip ] = ("");
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+ }
+
+ // Why?
+ gint count = _store->children().size();
+ if ((count > 0) && (_active > count)) {
+ _setActive(--count);
+ } else {
+ _setActive(_active);
+ }
+}
+
+UnitTracker::~UnitTracker()
+{
+ _combo_list.clear();
+
+ // Unhook weak references to GtkAdjustments
+ for (auto i : _adjList) {
+ g_object_weak_unref(G_OBJECT(i), _adjustmentFinalizedCB, this);
+ }
+ _adjList.clear();
+}
+
+bool UnitTracker::isUpdating() const
+{
+ return _isUpdating;
+}
+
+Inkscape::Util::Unit const * UnitTracker::getActiveUnit() const
+{
+ return _activeUnit;
+}
+
+Glib::ustring UnitTracker::getCurrentLabel()
+{
+ ComboToolItemColumns columns;
+ return _store->children()[_active][columns.col_label];
+}
+
+void UnitTracker::changeLabel(Glib::ustring new_label, gint pos, bool onlylabel)
+{
+ ComboToolItemColumns columns;
+ _store->children()[pos][columns.col_label] = new_label;
+ if (!onlylabel) {
+ _store->children()[pos][columns.col_value] = new_label;
+ }
+}
+
+void UnitTracker::setActiveUnit(Inkscape::Util::Unit const *unit)
+{
+ if (unit) {
+
+ ComboToolItemColumns columns;
+ int index = 0;
+ for (auto& row: _store->children() ) {
+ Glib::ustring storedUnit = row[columns.col_value];
+ if (!unit->abbr.compare (storedUnit)) {
+ _setActive (index);
+ break;
+ }
+ index++;
+ }
+ }
+}
+
+void UnitTracker::setActiveUnitByLabel(Glib::ustring label)
+{
+ ComboToolItemColumns columns;
+ int index = 0;
+ for (auto &row : _store->children()) {
+ Glib::ustring storedUnit = row[columns.col_label];
+ if (!label.compare(storedUnit)) {
+ _setActive(index);
+ break;
+ }
+ index++;
+ }
+}
+
+void UnitTracker::setActiveUnitByAbbr(gchar const *abbr)
+{
+ Inkscape::Util::Unit const *u = unit_table.getUnit(abbr);
+ setActiveUnit(u);
+}
+
+void UnitTracker::addAdjustment(GtkAdjustment *adj)
+{
+ if (std::find(_adjList.begin(),_adjList.end(),adj) == _adjList.end()) {
+ g_object_weak_ref(G_OBJECT(adj), _adjustmentFinalizedCB, this);
+ _adjList.push_back(adj);
+ } else {
+ std::cerr << "UnitTracker::addAjustment: Adjustment already added!" << std::endl;
+ }
+}
+
+void UnitTracker::addUnit(Inkscape::Util::Unit const *u)
+{
+ ComboToolItemColumns columns;
+
+ Gtk::TreeModel::Row row;
+ row = *(_store->append());
+ row[columns.col_label ] = u ? u->abbr.c_str() : "";
+ row[columns.col_value ] = u ? u->abbr.c_str() : "";
+ row[columns.col_tooltip ] = ("");
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+}
+
+void UnitTracker::prependUnit(Inkscape::Util::Unit const *u)
+{
+ ComboToolItemColumns columns;
+
+ Gtk::TreeModel::Row row;
+ row = *(_store->prepend());
+ row[columns.col_label ] = u ? u->abbr.c_str() : "";
+ row[columns.col_value ] = u ? u->abbr.c_str() : "";
+ row[columns.col_tooltip ] = ("");
+ row[columns.col_icon ] = "NotUsed";
+ row[columns.col_sensitive] = true;
+
+ /* Re-shuffle our default selection here (_active gets out of sync) */
+ setActiveUnit(_activeUnit);
+
+}
+
+void UnitTracker::setFullVal(GtkAdjustment *adj, gdouble val)
+{
+ _priorValues[adj] = val;
+}
+
+ComboToolItem *
+UnitTracker::create_tool_item(Glib::ustring const &label,
+ Glib::ustring const &tooltip)
+{
+ auto combo = ComboToolItem::create(label, tooltip, "NotUsed", _store);
+ combo->set_active(_active);
+ combo->signal_changed().connect(sigc::mem_fun(*this, &UnitTracker::_unitChangedCB));
+ combo->set_name("unit-tracker");
+ combo->set_data(Glib::Quark("unit-tracker"), this);
+ _combo_list.push_back(combo);
+ return combo;
+}
+
+void UnitTracker::_unitChangedCB(int active)
+{
+ _setActive(active);
+}
+
+void UnitTracker::_adjustmentFinalizedCB(gpointer data, GObject *where_the_object_was)
+{
+ if (data && where_the_object_was) {
+ UnitTracker *self = reinterpret_cast<UnitTracker *>(data);
+ self->_adjustmentFinalized(where_the_object_was);
+ }
+}
+
+void UnitTracker::_adjustmentFinalized(GObject *where_the_object_was)
+{
+ GtkAdjustment* adj = (GtkAdjustment*)(where_the_object_was);
+ auto it = std::find(_adjList.begin(),_adjList.end(), adj);
+ if (it != _adjList.end()) {
+ _adjList.erase(it);
+ } else {
+ g_warning("Received a finalization callback for unknown object %p", where_the_object_was);
+ }
+}
+
+void UnitTracker::_setActive(gint active)
+{
+ if ( active != _active || !_activeUnitInitialized ) {
+ gint oldActive = _active;
+
+ if (_store) {
+
+ // Find old and new units
+ ComboToolItemColumns columns;
+ int index = 0;
+ Glib::ustring oldAbbr( "NotFound" );
+ Glib::ustring newAbbr( "NotFound" );
+ for (auto& row: _store->children() ) {
+ if (index == _active) {
+ oldAbbr = row[columns.col_value];
+ }
+ if (index == active) {
+ newAbbr = row[columns.col_value];
+ }
+ if (newAbbr != "NotFound" && oldAbbr != "NotFound") break;
+ ++index;
+ }
+
+ if (oldAbbr != "NotFound") {
+
+ if (newAbbr != "NotFound") {
+ Inkscape::Util::Unit const *oldUnit = unit_table.getUnit(oldAbbr);
+ Inkscape::Util::Unit const *newUnit = unit_table.getUnit(newAbbr);
+ _activeUnit = newUnit;
+
+ if (!_adjList.empty()) {
+ _fixupAdjustments(oldUnit, newUnit);
+ }
+ } else {
+ std::cerr << "UnitTracker::_setActive: Did not find new unit: " << active << std::endl;
+ }
+
+ } else {
+ std::cerr << "UnitTracker::_setActive: Did not find old unit: " << oldActive
+ << " new: " << active << std::endl;
+ }
+ }
+ _active = active;
+
+ for (auto combo : _combo_list) {
+ if(combo) combo->set_active(active);
+ }
+
+ _activeUnitInitialized = true;
+ }
+}
+
+void UnitTracker::_fixupAdjustments(Inkscape::Util::Unit const *oldUnit, Inkscape::Util::Unit const *newUnit)
+{
+ _isUpdating = true;
+ for ( auto adj : _adjList ) {
+ gdouble oldVal = gtk_adjustment_get_value(adj);
+ gdouble val = oldVal;
+
+ if ( (oldUnit->type != Inkscape::Util::UNIT_TYPE_DIMENSIONLESS)
+ && (newUnit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) )
+ {
+ val = newUnit->factor * 100;
+ _priorValues[adj] = Inkscape::Util::Quantity::convert(oldVal, oldUnit, "px");
+ } else if ( (oldUnit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS)
+ && (newUnit->type != Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) )
+ {
+ if (_priorValues.find(adj) != _priorValues.end()) {
+ val = Inkscape::Util::Quantity::convert(_priorValues[adj], "px", newUnit);
+ }
+ } else {
+ val = Inkscape::Util::Quantity::convert(oldVal, oldUnit, newUnit);
+ }
+
+ gtk_adjustment_set_value(adj, val);
+ }
+ _isUpdating = false;
+}
+
+} // namespace Widget
+} // namespace UI
+} // 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 :
diff --git a/src/ui/widget/unit-tracker.h b/src/ui/widget/unit-tracker.h
new file mode 100644
index 0000000..243b19f
--- /dev/null
+++ b/src/ui/widget/unit-tracker.h
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Inkscape::UI::Widget::UnitTracker
+ * Simple mediator to synchronize changes to unit menus
+ *
+ * Authors:
+ * Jon A. Cruz <jon@joncruz.org>
+ * Matthew Petroff <matthew@mpetroff.net>
+ *
+ * Copyright (C) 2007 Jon A. Cruz
+ * Copyright (C) 2013 Matthew Petroff
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_UNIT_TRACKER_H
+#define INKSCAPE_UI_WIDGET_UNIT_TRACKER_H
+
+#include <map>
+#include <vector>
+
+#include <gtkmm/liststore.h>
+
+#include "util/units.h"
+
+using Inkscape::Util::Unit;
+using Inkscape::Util::UnitType;
+
+typedef struct _GObject GObject;
+typedef struct _GtkAdjustment GtkAdjustment;
+typedef struct _GtkListStore GtkListStore;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class ComboToolItem;
+
+class UnitTracker {
+public:
+ UnitTracker(UnitType unit_type);
+ virtual ~UnitTracker();
+
+ bool isUpdating() const;
+
+ void setActiveUnit(Inkscape::Util::Unit const *unit);
+ void setActiveUnitByAbbr(gchar const *abbr);
+ void setActiveUnitByLabel(Glib::ustring label);
+ Inkscape::Util::Unit const * getActiveUnit() const;
+
+ void addUnit(Inkscape::Util::Unit const *u);
+ void addAdjustment(GtkAdjustment *adj);
+ void prependUnit(Inkscape::Util::Unit const *u);
+ void setFullVal(GtkAdjustment *adj, gdouble val);
+ Glib::ustring getCurrentLabel();
+ void changeLabel(Glib::ustring new_label, gint pos, bool onlylabel = false);
+
+ ComboToolItem *create_tool_item(Glib::ustring const &label,
+ Glib::ustring const &tooltip);
+
+protected:
+ UnitType _type;
+
+private:
+ // Callbacks
+ void _unitChangedCB(int active);
+ static void _adjustmentFinalizedCB(gpointer data, GObject *where_the_object_was);
+
+ void _setActive(gint index);
+ void _fixupAdjustments(Inkscape::Util::Unit const *oldUnit, Inkscape::Util::Unit const *newUnit);
+
+ // Cleanup
+ void _adjustmentFinalized(GObject *where_the_object_was);
+
+ gint _active;
+ bool _isUpdating;
+ Inkscape::Util::Unit const *_activeUnit;
+ bool _activeUnitInitialized;
+
+ Glib::RefPtr<Gtk::ListStore> _store;
+ std::vector<ComboToolItem *> _combo_list;
+ std::vector<GtkAdjustment*> _adjList;
+ std::map <GtkAdjustment *, gdouble> _priorValues;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_UNIT_TRACKER_H