summaryrefslogtreecommitdiffstats
path: root/src/ui/widget
diff options
context:
space:
mode:
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.cpp419
-rw-r--r--src/ui/widget/canvas-grid.h131
-rw-r--r--src/ui/widget/canvas-notice.cpp52
-rw-r--r--src/ui/widget/canvas-notice.h39
-rw-r--r--src/ui/widget/canvas.cpp2426
-rw-r--r--src/ui/widget/canvas.h224
-rw-r--r--src/ui/widget/canvas/cairographics.cpp423
-rw-r--r--src/ui/widget/canvas/cairographics.h80
-rw-r--r--src/ui/widget/canvas/fragment.h33
-rw-r--r--src/ui/widget/canvas/framecheck.cpp24
-rw-r--r--src/ui/widget/canvas/framecheck.h58
-rw-r--r--src/ui/widget/canvas/glgraphics.cpp873
-rw-r--r--src/ui/widget/canvas/glgraphics.h144
-rw-r--r--src/ui/widget/canvas/graphics.cpp166
-rw-r--r--src/ui/widget/canvas/graphics.h91
-rw-r--r--src/ui/widget/canvas/pixelstreamer.cpp501
-rw-r--r--src/ui/widget/canvas/pixelstreamer.h75
-rw-r--r--src/ui/widget/canvas/prefs.h102
-rw-r--r--src/ui/widget/canvas/stores.cpp371
-rw-r--r--src/ui/widget/canvas/stores.h106
-rw-r--r--src/ui/widget/canvas/synchronizer.cpp103
-rw-r--r--src/ui/widget/canvas/synchronizer.h72
-rw-r--r--src/ui/widget/canvas/texture.cpp66
-rw-r--r--src/ui/widget/canvas/texture.h62
-rw-r--r--src/ui/widget/canvas/texturecache.cpp115
-rw-r--r--src/ui/widget/canvas/texturecache.h52
-rw-r--r--src/ui/widget/canvas/updaters.cpp235
-rw-r--r--src/ui/widget/canvas/updaters.h78
-rw-r--r--src/ui/widget/canvas/util.cpp70
-rw-r--r--src/ui/widget/canvas/util.h75
-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.cpp987
-rw-r--r--src/ui/widget/color-icc-selector.h75
-rw-r--r--src/ui/widget/color-notebook.cpp382
-rw-r--r--src/ui/widget/color-notebook.h108
-rw-r--r--src/ui/widget/color-palette.cpp724
-rw-r--r--src/ui/widget/color-palette.h130
-rw-r--r--src/ui/widget/color-picker.cpp173
-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.cpp1247
-rw-r--r--src/ui/widget/color-scales.h141
-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.cpp725
-rw-r--r--src/ui/widget/combo-box-entry-tool-item.h153
-rw-r--r--src/ui/widget/combo-enums.h232
-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/completion-popup.cpp108
-rw-r--r--src/ui/widget/completion-popup.h51
-rw-r--r--src/ui/widget/custom-tooltip.cpp61
-rw-r--r--src/ui/widget/custom-tooltip.h22
-rw-r--r--src/ui/widget/dash-selector.cpp260
-rw-r--r--src/ui/widget/dash-selector.h116
-rw-r--r--src/ui/widget/entity-entry.cpp220
-rw-r--r--src/ui/widget/entity-entry.h89
-rw-r--r--src/ui/widget/entry.cpp30
-rw-r--r--src/ui/widget/entry.h45
-rw-r--r--src/ui/widget/export-lists.cpp321
-rw-r--r--src/ui/widget/export-lists.h114
-rw-r--r--src/ui/widget/export-preview.cpp208
-rw-r--r--src/ui/widget/export-preview.h99
-rw-r--r--src/ui/widget/fill-style.cpp738
-rw-r--r--src/ui/widget/fill-style.h85
-rw-r--r--src/ui/widget/filter-effect-chooser.cpp211
-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-collection-selector.cpp674
-rw-r--r--src/ui/widget/font-collection-selector.h139
-rw-r--r--src/ui/widget/font-selector-toolbar.cpp301
-rw-r--r--src/ui/widget/font-selector-toolbar.h120
-rw-r--r--src/ui/widget/font-selector.cpp562
-rw-r--r--src/ui/widget/font-selector.h177
-rw-r--r--src/ui/widget/font-variants.cpp1461
-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/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.cpp611
-rw-r--r--src/ui/widget/gradient-selector.h160
-rw-r--r--src/ui/widget/gradient-vector-selector.cpp326
-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.h101
-rw-r--r--src/ui/widget/iconrenderer.cpp119
-rw-r--r--src/ui/widget/iconrenderer.h84
-rw-r--r--src/ui/widget/image-properties.cpp295
-rw-r--r--src/ui/widget/image-properties.h47
-rw-r--r--src/ui/widget/imagetoggler.cpp148
-rw-r--r--src/ui/widget/imagetoggler.h97
-rw-r--r--src/ui/widget/ink-color-wheel.cpp1356
-rw-r--r--src/ui/widget/ink-color-wheel.h167
-rw-r--r--src/ui/widget/ink-ruler.cpp645
-rw-r--r--src/ui/widget/ink-ruler.h112
-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.cpp106
-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.cpp157
-rw-r--r--src/ui/widget/licensor.h57
-rw-r--r--src/ui/widget/marker-combo-box.cpp814
-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.cpp302
-rw-r--r--src/ui/widget/object-composite-settings.h78
-rw-r--r--src/ui/widget/objects-dialog-cells.cpp90
-rw-r--r--src/ui/widget/objects-dialog-cells.h75
-rw-r--r--src/ui/widget/oklab-color-wheel.cpp309
-rw-r--r--src/ui/widget/oklab-color-wheel.h83
-rw-r--r--src/ui/widget/optglarea.cpp127
-rw-r--r--src/ui/widget/optglarea.h80
-rw-r--r--src/ui/widget/page-properties.cpp525
-rw-r--r--src/ui/widget/page-properties.h59
-rw-r--r--src/ui/widget/page-selector.cpp198
-rw-r--r--src/ui/widget/page-selector.h91
-rw-r--r--src/ui/widget/page-size-preview.cpp186
-rw-r--r--src/ui/widget/page-size-preview.h50
-rw-r--r--src/ui/widget/paint-selector.cpp1267
-rw-r--r--src/ui/widget/paint-selector.h236
-rw-r--r--src/ui/widget/pattern-editor.cpp685
-rw-r--r--src/ui/widget/pattern-editor.h134
-rw-r--r--src/ui/widget/pattern-store.h61
-rw-r--r--src/ui/widget/point.cpp186
-rw-r--r--src/ui/widget/point.h196
-rw-r--r--src/ui/widget/preferences-widget.cpp1117
-rw-r--r--src/ui/widget/preferences-widget.h357
-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.cpp830
-rw-r--r--src/ui/widget/registered-widget.h452
-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.cpp180
-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.cpp212
-rw-r--r--src/ui/widget/scalar.h202
-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.cpp1416
-rw-r--r--src/ui/widget/selected-style.h302
-rw-r--r--src/ui/widget/shapeicon.cpp148
-rw-r--r--src/ui/widget/shapeicon.h115
-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.cpp247
-rw-r--r--src/ui/widget/spin-scale.h116
-rw-r--r--src/ui/widget/spinbutton.cpp144
-rw-r--r--src/ui/widget/spinbutton.h124
-rw-r--r--src/ui/widget/stroke-style.cpp1223
-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.cpp405
-rw-r--r--src/ui/widget/style-swatch.h107
-rw-r--r--src/ui/widget/swatch-selector.cpp101
-rw-r--r--src/ui/widget/swatch-selector.h58
-rw-r--r--src/ui/widget/template-list.cpp224
-rw-r--r--src/ui/widget/template-list.h62
-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
193 files changed, 46884 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..58f13c8
--- /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..0be3513
--- /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..4815d74
--- /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..460b606
--- /dev/null
+++ b/src/ui/widget/canvas-grid.cpp
@@ -0,0 +1,419 @@
+// 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 "page-manager.h"
+
+#include "ui/dialog/command-palette.h"
+#include "ui/icon-loader.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/canvas-notice.h"
+#include "ui/widget/ink-ruler.h"
+#include "io/resource.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 = std::make_unique<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
+
+ // Command palette
+ _command_palette = std::make_unique<Inkscape::UI::Dialog::CommandPalette>();
+
+ // Notice overlay, note using unique_ptr will cause destruction race conditions
+ _notice = CanvasNotice::create();
+
+ // Canvas overlay
+ _canvas_overlay.add(*_canvas);
+ _canvas_overlay.add_overlay(*_command_palette->get_base_widget());
+ _canvas_overlay.add_overlay(*_notice);
+
+ // Horizontal Ruler
+ _hruler = std::make_unique<Inkscape::UI::Widget::Ruler>(Gtk::ORIENTATION_HORIZONTAL);
+ _hruler->add_track_widget(*_canvas);
+ _hruler->set_hexpand(true);
+ _hruler->show();
+ // Tooltip/Unit set elsewhere
+
+ // Vertical Ruler
+ _vruler = std::make_unique<Inkscape::UI::Widget::Ruler>(Gtk::ORIENTATION_VERTICAL);
+ _vruler->add_track_widget(*_canvas);
+ _vruler->set_vexpand(true);
+ _vruler->show();
+ // Tooltip/Unit set elsewhere.
+
+ // Guide Lock
+ _guide_lock.set_name("LockGuides");
+ _guide_lock.add(*Gtk::make_managed<Gtk::Image>("object-locked", Gtk::ICON_SIZE_MENU));
+ // 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"));
+ // Subgrid
+ _subgrid.attach(_guide_lock, 0, 0, 1, 1);
+ _subgrid.attach(*_vruler, 0, 1, 1, 1);
+ _subgrid.attach(*_hruler, 1, 0, 1, 1);
+ _subgrid.attach(_canvas_overlay, 1, 1, 1, 1);
+
+ // 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::Scrollbar(_hadj, Gtk::ORIENTATION_HORIZONTAL);
+ _hscrollbar.set_name("CanvasScrollbar");
+ _hscrollbar.set_hexpand(true);
+
+ // 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::Scrollbar(_vadj, Gtk::ORIENTATION_VERTICAL);
+ _vscrollbar.set_name("CanvasScrollbar");
+ _vscrollbar.set_vexpand(true);
+
+ // CMS Adjust (To be replaced by Gio::Action)
+ _cms_adjust.set_name("CMS_Adjust");
+ _cms_adjust.add(*Gtk::make_managed<Gtk::Image>("color-management", Gtk::ICON_SIZE_MENU));
+ // 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"));
+
+ // popover with some common display mode related options
+ auto builder = Gtk::Builder::create_from_file(Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "display-popup.glade"));
+ _display_popup = builder;
+ Gtk::Popover* popover;
+ _display_popup->get_widget("popover", popover);
+ Gtk::CheckButton* sticky_zoom;
+ _display_popup->get_widget("zoom-resize", sticky_zoom);
+ // To be replaced by Gio::Action:
+ sticky_zoom->signal_toggled().connect([=](){ _dtw->sticky_zoom_toggled(); });
+ _quick_actions.set_name("QuickActions");
+ _quick_actions.set_popover(*popover);
+ _quick_actions.set_image_from_icon_name("display-symbolic");
+ _quick_actions.set_direction(Gtk::ARROW_LEFT);
+ _quick_actions.set_tooltip_text(_("Display options"));
+
+ // Main grid
+ attach(_subgrid, 0, 0, 1, 2);
+ attach(_hscrollbar, 0, 2, 1, 1);
+ attach(_cms_adjust, 1, 2, 1, 1);
+ attach(_quick_actions, 1, 0, 1, 1);
+ attach(_vscrollbar, 1, 1, 1, 1);
+
+ // For creating guides, etc.
+ _hruler->signal_button_press_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _hruler.get(), true));
+ _hruler->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _hruler.get(), true));
+ _hruler->signal_motion_notify_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _hruler.get(), true));
+
+ // For creating guides, etc.
+ _vruler->signal_button_press_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_press_event), _vruler.get(), false));
+ _vruler->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_button_release_event), _vruler.get(), false));
+ _vruler->signal_motion_notify_event().connect(
+ sigc::bind(sigc::mem_fun(*_dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), _vruler.get(), false));
+
+ show_all();
+}
+
+CanvasGrid::~CanvasGrid()
+{
+ _page_modified_connection.disconnect();
+ _page_selected_connection.disconnect();
+ _sel_modified_connection.disconnect();
+ _sel_changed_connection.disconnect();
+ _document = nullptr;
+ _notice = nullptr;
+}
+
+void CanvasGrid::on_realize() {
+ // actions should be available now
+
+ if (auto map = _dtw->get_action_map()) {
+ auto set_display_icon = [=]() {
+ Glib::ustring id;
+ auto mode = _canvas->get_render_mode();
+ switch (mode) {
+ case RenderMode::NORMAL: id = "display";
+ break;
+ case RenderMode::OUTLINE: id = "display-outline";
+ break;
+ case RenderMode::OUTLINE_OVERLAY: id = "display-outline-overlay";
+ break;
+ case RenderMode::VISIBLE_HAIRLINES: id = "display-enhance-stroke";
+ break;
+ case RenderMode::NO_FILTERS: id = "display-no-filter";
+ break;
+ default:
+ g_warning("Unknown display mode in canvas-grid");
+ break;
+ }
+
+ if (!id.empty()) {
+ // if CMS is ON show alternative icons
+ if (_canvas->get_cms_active()) {
+ id += "-alt";
+ }
+ _quick_actions.set_image_from_icon_name(id + "-symbolic");
+ }
+ };
+
+ set_display_icon();
+
+ // when display mode state changes, update icon
+ auto cms_action = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(map->lookup_action("canvas-color-manage"));
+ auto disp_action = Glib::RefPtr<Gio::SimpleAction>::cast_dynamic(map->lookup_action("canvas-display-mode"));
+
+ if (cms_action && disp_action) {
+ disp_action->signal_activate().connect([=](const Glib::VariantBase& state){ set_display_icon(); });
+ cms_action-> signal_activate().connect([=](const Glib::VariantBase& state){ set_display_icon(); });
+ }
+ else {
+ g_warning("No canvas-display-mode and/or canvas-color-manage action available to canvas-grid");
+ }
+ }
+ else {
+ g_warning("No action map available to canvas-grid");
+ }
+
+ parent_type::on_realize();
+}
+
+// TODO: remove when sticky zoom gets replaced by Gio::Action:
+Gtk::ToggleButton* CanvasGrid::GetStickyZoom() {
+ Gtk::CheckButton* sticky_zoom;
+ _display_popup->get_widget("zoom-resize", sticky_zoom);
+ return sticky_zoom;
+}
+
+// _dt2r should be a member of _canvas.
+// get_display_area should be a member of _canvas.
+void
+CanvasGrid::UpdateRulers()
+{
+ auto prefs = Inkscape::Preferences::get();
+ auto desktop = _dtw->desktop;
+ auto document = desktop->getDocument();
+ auto &pm = document->getPageManager();
+ auto sel = desktop->getSelection();
+
+ // Our connections to the document are handled with a lazy pattern to avoid
+ // having to refactor the SPDesktopWidget class. We know UpdateRulers is
+ // called in all situations when documents are loaded and replaced.
+ if (document != _document) {
+ _document = document;
+ _page_selected_connection = pm.connectPageSelected([=](SPPage *) { UpdateRulers(); });
+ _page_modified_connection = pm.connectPageModified([=](SPPage *) { UpdateRulers(); });
+ _sel_modified_connection = sel->connectModified([=](Inkscape::Selection *, int) { UpdateRulers(); });
+ _sel_changed_connection = sel->connectChanged([=](Inkscape::Selection *) { UpdateRulers(); });
+ }
+
+ Geom::Rect viewbox = desktop->get_display_area().bounds();
+ Geom::Rect startbox = viewbox;
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ // Move viewbox according to the selected page's position (if any)
+ startbox *= pm.getSelectedPageAffine().inverse();
+ }
+
+ // Scale and offset the ruler coordinates
+ // Use an integer box to align the ruler to the grid and page.
+ auto rulerbox = (startbox * Geom::Scale(_dtw->_dt2r));
+ _hruler->set_range(rulerbox.left(), rulerbox.right());
+ if (_dtw->desktop->is_yaxisdown()) {
+ _vruler->set_range(rulerbox.top(), rulerbox.bottom());
+ } else {
+ _vruler->set_range(rulerbox.bottom(), rulerbox.top());
+ }
+
+ Geom::Point pos(_canvas->get_pos());
+ auto scale = _canvas->get_affine();
+ auto d2c = Geom::Translate(pos * scale.inverse()).inverse() * scale;
+ auto pagebox = (pm.getSelectedPageRect() * d2c).roundOutwards();
+ _hruler->set_page(pagebox.left(), pagebox.right());
+ _vruler->set_page(pagebox.top(), pagebox.bottom());
+
+ Geom::Rect selbox = Geom::IntRect(0, 0, 0, 0);
+ if (auto bbox = sel->preferredBounds())
+ selbox = (*bbox * d2c).roundOutwards();
+ _hruler->set_selection(selbox.left(), selbox.right());
+ _vruler->set_selection(selbox.top(), selbox.bottom());
+}
+
+void
+CanvasGrid::ShowScrollbars(bool state)
+{
+ if (_show_scrollbars == state) return;
+ _show_scrollbars = state;
+
+ if (_show_scrollbars) {
+ // Show scrollbars
+ _hscrollbar.show();
+ _vscrollbar.show();
+ _cms_adjust.show();
+ _cms_adjust.show_all_children();
+ _quick_actions.show();
+ } else {
+ // Hide scrollbars
+ _hscrollbar.hide();
+ _vscrollbar.hide();
+ _cms_adjust.hide();
+ _quick_actions.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)
+{
+ if (_show_rulers == state) return;
+ _show_rulers = state;
+
+ if (_show_rulers) {
+ // Show rulers
+ _hruler->show();
+ _vruler->show();
+ _guide_lock.show();
+ _guide_lock.show_all_children();
+ } else {
+ // Hide rulers
+ _hruler->hide();
+ _vruler->hide();
+ _guide_lock.hide();
+ }
+}
+
+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::showNotice(Glib::ustring const &msg, unsigned timeout)
+{
+ _notice->show(msg, timeout);
+}
+
+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::on_size_allocate(Gtk::Allocation& allocation)
+{
+ Gtk::Grid::on_size_allocate(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) {
+ _dtw->desktop->getCanvasDrawing()->set_sticky(event->button.state & GDK_SHIFT_MASK);
+ }
+
+ // 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..5cddbbe
--- /dev/null
+++ b/src/ui/widget/canvas-grid.h
@@ -0,0 +1,131 @@
+// 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 <gtkmm/menubutton.h>
+#include <gtkmm/builder.h>
+
+class SPPage;
+class SPDocument;
+class SPCanvas;
+class SPDesktopWidget;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Dialog {
+class CommandPalette;
+}
+
+namespace Widget {
+
+class Canvas;
+class CanvasNotice;
+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
+{
+ using parent_type = 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();
+
+ void showNotice(Glib::ustring const &msg, unsigned timeout = 0);
+
+ Inkscape::UI::Widget::Canvas *GetCanvas() { return _canvas.get(); };
+
+ // Hopefully temp.
+ Inkscape::UI::Widget::Ruler *GetHRuler() { return _vruler.get(); };
+ Inkscape::UI::Widget::Ruler *GetVRuler() { return _hruler.get(); };
+ Gtk::Adjustment *GetHAdj() { return _hadj.get(); };
+ Gtk::Adjustment *GetVAdj() { return _vadj.get(); };
+ Gtk::ToggleButton *GetGuideLock() { return &_guide_lock; }
+ Gtk::ToggleButton *GetCmsAdjust() { return &_cms_adjust; }
+ Gtk::ToggleButton *GetStickyZoom();
+
+private:
+ // Signal callbacks
+ void on_size_allocate(Gtk::Allocation& allocation) override;
+ bool SignalEvent(GdkEvent *event);
+ void on_realize() override;
+
+ // The widgets
+ std::unique_ptr<Inkscape::UI::Widget::Canvas> _canvas;
+ std::unique_ptr<Dialog::CommandPalette> _command_palette;
+ CanvasNotice *_notice;
+ Gtk::Overlay _canvas_overlay;
+ Gtk::Grid _subgrid;
+
+ Glib::RefPtr<Gtk::Adjustment> _hadj;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+ Gtk::Scrollbar _hscrollbar;
+ Gtk::Scrollbar _vscrollbar;
+
+ std::unique_ptr<Inkscape::UI::Widget::Ruler> _hruler;
+ std::unique_ptr<Inkscape::UI::Widget::Ruler> _vruler;
+
+ Gtk::ToggleButton _guide_lock;
+ Gtk::ToggleButton _cms_adjust;
+ Gtk::MenuButton _quick_actions;
+ Glib::RefPtr<Gtk::Builder> _display_popup;
+
+ // To be replaced by stateful Gio::Actions
+ bool _show_scrollbars = true;
+ bool _show_rulers = true;
+
+ // Hopefully temp
+ SPDesktopWidget *_dtw;
+ SPDocument *_document = nullptr;
+
+ // Store allocation so we don't redraw too often.
+ Gtk::Allocation _allocation;
+
+ // Connections for page and selection tracking
+ sigc::connection _page_selected_connection;
+ sigc::connection _page_modified_connection;
+ sigc::connection _sel_changed_connection;
+ sigc::connection _sel_modified_connection;
+};
+
+} // 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-notice.cpp b/src/ui/widget/canvas-notice.cpp
new file mode 100644
index 0000000..0337bf9
--- /dev/null
+++ b/src/ui/widget/canvas-notice.cpp
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "canvas-notice.h"
+
+#include <utility>
+#include <glibmm/main.h>
+
+#include "ui/builder-utils.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+CanvasNotice::CanvasNotice(BaseObjectType *cobject, Glib::RefPtr<Gtk::Builder> builder)
+ : Gtk::Revealer(cobject)
+ , _builder(std::move(builder))
+ , _icon(get_widget<Gtk::Image>(_builder, "notice-icon"))
+ , _label(get_widget<Gtk::Label>(_builder, "notice-label"))
+{
+ auto &close = get_widget<Gtk::Button>(_builder, "notice-close");
+ close.signal_clicked().connect([=]() {
+ hide();
+ });
+}
+
+void CanvasNotice::show(Glib::ustring const &msg, unsigned timeout)
+{
+ _label.set_text(msg);
+ set_reveal_child(true);
+ if (timeout != 0) {
+ _timeout = Glib::signal_timeout().connect([=]() {
+ hide();
+ return false;
+ }, timeout);
+ }
+}
+
+void CanvasNotice::hide()
+{
+ set_reveal_child(false);
+}
+
+CanvasNotice *CanvasNotice::create()
+{
+ CanvasNotice *widget = nullptr;
+ auto builder = create_builder("canvas-notice.glade");
+ builder->get_widget_derived("canvas-notice", widget);
+ return widget;
+}
+
+}}} // namespaces
diff --git a/src/ui/widget/canvas-notice.h b/src/ui/widget/canvas-notice.h
new file mode 100644
index 0000000..88c7bed
--- /dev/null
+++ b/src/ui/widget/canvas-notice.h
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H
+#define INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H
+
+#include <glibmm/refptr.h>
+#include <gtkmm/builder.h>
+
+#include <gtkmm/revealer.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/button.h>
+
+#include "helper/auto-connection.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class CanvasNotice : public Gtk::Revealer {
+public:
+ static CanvasNotice *create();
+
+ CanvasNotice(BaseObjectType *cobject, Glib::RefPtr<Gtk::Builder> refGlade);
+ void show(Glib::ustring const &msg, unsigned timeout = 0);
+ void hide();
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ Gtk::Image& _icon;
+ Gtk::Label& _label;
+
+ Inkscape::auto_connection _timeout;
+};
+
+}}} // namespaces
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_NOTICE_H
diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp
new file mode 100644
index 0000000..cfdb966
--- /dev/null
+++ b/src/ui/widget/canvas.cpp
@@ -0,0 +1,2426 @@
+// 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 <thread>
+#include <mutex>
+#include <array>
+#include <cassert>
+#include <boost/asio/thread_pool.hpp>
+#include <boost/asio/post.hpp>
+#include <2geom/convex-hull.h>
+
+#include "canvas.h"
+#include "canvas-grid.h"
+
+#include "color.h" // Background color
+#include "cms-system.h" // Color correction
+#include "desktop.h"
+#include "document.h"
+#include "preferences.h"
+#include "ui/util.h"
+#include "helper/geom.h"
+
+#include "canvas/prefs.h"
+#include "canvas/fragment.h"
+#include "canvas/util.h"
+#include "canvas/stores.h"
+#include "canvas/graphics.h"
+#include "canvas/synchronizer.h"
+#include "display/drawing.h"
+#include "display/control/canvas-item-drawing.h"
+#include "display/control/canvas-item-group.h"
+#include "display/control/snap-indicator.h"
+
+#include "ui/tools/tool-base.h" // Default cursor
+
+#include "canvas/updaters.h" // Update strategies
+#include "canvas/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::UI::Widget {
+namespace {
+
+/*
+ * Utilities
+ */
+
+// GdkEvents can only be safely copied using gdk_event_copy. Since this function allocates, 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(GdkEvent const *ev) { return GdkEventUniqPtr(gdk_event_copy(ev)); }
+
+// Convert an integer received from preferences into an Updater enum.
+auto pref_to_updater(int index)
+{
+ constexpr auto arr = std::array{Updater::Strategy::Responsive,
+ Updater::Strategy::FullRedraw,
+ Updater::Strategy::Multiscale};
+ assert(1 <= index && index <= arr.size());
+ return arr[index - 1];
+}
+
+// Represents the raster data and location of an in-flight tile (one that is drawn, but not yet pasted into the stores).
+struct Tile
+{
+ Fragment fragment;
+ Cairo::RefPtr<Cairo::ImageSurface> surface;
+ Cairo::RefPtr<Cairo::ImageSurface> outline_surface;
+};
+
+// The urgency with which the async redraw process should exit.
+enum class AbortFlags : int
+{
+ None = 0,
+ Soft = 1, // exit if reached prerender phase
+ Hard = 2 // exit in any phase
+};
+
+// A copy of all the data the async redraw process needs access to, along with its internal state.
+struct RedrawData
+{
+ // Data on what/how to draw.
+ Geom::IntPoint mouse_loc;
+ Geom::IntRect visible;
+ Fragment store;
+ bool decoupled_mode;
+ Cairo::RefPtr<Cairo::Region> snapshot_drawn;
+ Geom::OptIntRect grabbed;
+
+ // Saved prefs
+ int coarsener_min_size;
+ int coarsener_glue_size;
+ double coarsener_min_fullness;
+ int tile_size;
+ int preempt;
+ int margin;
+ std::optional<int> redraw_delay;
+ int render_time_limit;
+ int numthreads;
+ bool background_in_stores_required;
+ uint64_t page, desk;
+ bool debug_framecheck;
+ bool debug_show_redraw;
+
+ // State
+ std::mutex mutex;
+ gint64 start_time;
+ int numactive;
+ int phase;
+ Geom::OptIntRect vis_store;
+
+ Geom::IntRect bounds;
+ Cairo::RefPtr<Cairo::Region> clean;
+ bool interruptible;
+ bool preemptible;
+ std::vector<Geom::IntRect> rects;
+ int effective_tile_size;
+
+ // Results
+ std::mutex tiles_mutex;
+ std::vector<Tile> tiles;
+ bool timeoutflag;
+
+ // Return comparison object for sorting rectangles by distance from mouse point.
+ auto getcmp() const
+ {
+ return [mouse_loc = mouse_loc] (Geom::IntRect const &a, Geom::IntRect const &b) {
+ return a.distanceSq(mouse_loc) > b.distanceSq(mouse_loc);
+ };
+ }
+};
+
+} // namespace
+
+/*
+ * Implementation class
+ */
+
+class CanvasPrivate
+{
+public:
+ friend class Canvas;
+ Canvas *q;
+ CanvasPrivate(Canvas *q)
+ : q(q)
+ , stores(prefs) {}
+
+ // Lifecycle
+ bool active = false;
+ void activate();
+ void deactivate();
+
+ // CanvasItem tree
+ std::optional<CanvasItemContext> canvasitem_ctx;
+
+ // Preferences
+ Prefs prefs;
+
+ // Stores
+ Stores stores;
+ void handle_stores_action(Stores::Action action);
+
+ // Invalidation
+ std::unique_ptr<Updater> updater; // Tracks the unclean region and decides how to redraw it.
+ Cairo::RefPtr<Cairo::Region> invalidated; // Buffers invalidations while the updater is in use by the background process.
+
+ // Graphics state; holds all the graphics resources, including the drawn content.
+ std::unique_ptr<Graphics> graphics;
+ void activate_graphics();
+ void deactivate_graphics();
+
+ // Redraw process management.
+ bool redraw_active = false;
+ bool redraw_requested = false;
+ sigc::connection schedule_redraw_conn;
+ void schedule_redraw();
+ void launch_redraw();
+ void after_redraw();
+ void commit_tiles();
+
+ // Event handling.
+ bool process_event(const GdkEvent*);
+ bool pick_current_item(const GdkEvent*);
+ bool emit_event(const GdkEvent*);
+ Inkscape::CanvasItem *pre_scroll_grabbed_item;
+
+ // Various state affecting what is drawn.
+ uint32_t desk = 0xffffffff; // The background colour, with the alpha channel used to control checkerboard.
+ uint32_t border = 0x000000ff; // The border colour, used only to control shadow colour.
+ uint32_t page = 0xffffffff; // The page colour, also with alpha channel used to control checkerboard.
+
+ bool clip_to_page = false; // Whether to enable clip-to-page mode.
+ PageInfo pi; // The list of page rectangles.
+ std::optional<Geom::PathVector> calc_page_clip() const; // Union of the page rectangles if in clip-to-page mode, otherwise no clip.
+
+ int scale_factor = 1; // The device scale the stores are drawn at.
+
+ bool outlines_enabled = false; // Whether to enable the outline layer.
+ bool outlines_required() const { return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY; }
+
+ bool background_in_stores_enabled = false; // Whether the page and desk should be drawn into the stores/tiles; if not then transparency is used instead.
+ bool background_in_stores_required() const { return !q->get_opengl_enabled() && SP_RGBA32_A_U(page) == 255 && SP_RGBA32_A_U(desk) == 255; } // Enable solid colour optimisation if both page and desk are solid (as opposed to checkerboard).
+
+ // Async redraw process.
+ std::optional<boost::asio::thread_pool> pool;
+ int numthreads;
+ int get_numthreads() const;
+
+ Synchronizer sync;
+ RedrawData rd;
+ std::atomic<int> abort_flags;
+
+ void init_tiler();
+ bool init_redraw();
+ bool end_redraw(); // returns true to indicate further redraw cycles required
+ void process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr<Cairo::Region> clean, bool interruptible = true, bool preemptible = true);
+ void render_tile(int debug_id);
+ void paint_rect(Geom::IntRect const &rect);
+ void paint_single_buffer(const Cairo::RefPtr<Cairo::ImageSurface> &surface, const Geom::IntRect &rect, bool need_background, bool outline_pass);
+ void paint_error_buffer(const Cairo::RefPtr<Cairo::ImageSurface> &surface);
+
+ // Trivial overload of GtkWidget function.
+ void queue_draw_area(Geom::IntRect const &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::IntPoint> last_mouse;
+
+ // Auto-scrolling.
+ std::optional<guint> tick_callback;
+ std::optional<gint64> last_time;
+ Geom::IntPoint strain;
+ Geom::Point displacement, velocity;
+ void autoscroll_begin(Geom::IntPoint const &to);
+ void autoscroll_end();
+};
+
+/*
+ * 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 );
+
+ // Updater
+ d->updater = Updater::create(pref_to_updater(d->prefs.update_strategy));
+ d->updater->reset();
+ d->invalidated = Cairo::Region::create();
+
+ // Preferences
+ d->prefs.grabsize.action = [=] { d->canvasitem_ctx->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->schedule_redraw(); };
+ d->prefs.debug_sticky_decoupled.action = [=] { d->schedule_redraw(); };
+ d->prefs.debug_animate.action = [=] { queue_draw(); };
+ d->prefs.outline_overlay_opacity.action = [=] { queue_draw(); };
+ d->prefs.softproof.action = [=] { redraw_all(); };
+ d->prefs.displayprofile.action = [=] { redraw_all(); };
+ d->prefs.request_opengl.action = [=] {
+ if (get_realized()) {
+ d->deactivate();
+ d->deactivate_graphics();
+ set_opengl_enabled(d->prefs.request_opengl);
+ d->updater->reset();
+ d->activate_graphics();
+ d->activate();
+ }
+ };
+ d->prefs.pixelstreamer_method.action = [=] {
+ if (get_realized() && get_opengl_enabled()) {
+ d->deactivate();
+ d->deactivate_graphics();
+ d->activate_graphics();
+ d->activate();
+ }
+ };
+ d->prefs.numthreads.action = [=] {
+ if (!d->active) return;
+ int const new_numthreads = d->get_numthreads();
+ if (d->numthreads == new_numthreads) return;
+ d->numthreads = new_numthreads;
+ d->deactivate();
+ d->deactivate_graphics();
+ d->pool.emplace(d->numthreads);
+ d->activate_graphics();
+ d->activate();
+ };
+
+ // Canvas item tree
+ d->canvasitem_ctx.emplace(this);
+
+ // Split view.
+ _split_direction = Inkscape::SplitDirection::EAST;
+ _split_frac = {0.5, 0.5};
+
+ // Recreate stores on HiDPI change.
+ property_scale_factor().signal_changed().connect([this] { d->schedule_redraw(); });
+
+ // OpenGL switch.
+ set_opengl_enabled(d->prefs.request_opengl);
+
+ // Async redraw process.
+ d->numthreads = d->get_numthreads();
+ d->pool.emplace(d->numthreads);
+
+ d->sync.connectExit([this] { d->after_redraw(); });
+}
+
+int CanvasPrivate::get_numthreads() const
+{
+ if (int n = prefs.numthreads; n > 0) {
+ // First choice is the value set in preferences.
+ return n;
+ } else if (int n = std::thread::hardware_concurrency(); n > 0) {
+ // If not set, use the number of processors minus one. (Using all of them causes stuttering.)
+ return n == 1 ? 1 : n - 1;
+ } else {
+ // If not reported, use a sensible fallback.
+ return 4;
+ }
+}
+
+// Graphics becomes active when the widget is realized.
+void CanvasPrivate::activate_graphics()
+{
+ if (q->get_opengl_enabled()) {
+ q->make_current();
+ graphics = Graphics::create_gl(prefs, stores, pi);
+ } else {
+ graphics = Graphics::create_cairo(prefs, stores, pi);
+ }
+ stores.set_graphics(graphics.get());
+ stores.reset();
+}
+
+// After graphics becomes active, the canvas becomes active when additionally a drawing is set.
+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->_need_update = true;
+
+ // Split view
+ q->_split_dragging = false;
+
+ // Todo: Disable GTK event compression again when doing so is no longer buggy.
+ // Note: ToolBase::set_high_motion_precision() will keep turning it back on.
+ // q->get_window()->set_event_compression(false);
+
+ active = true;
+
+ schedule_redraw();
+}
+
+void CanvasPrivate::deactivate()
+{
+ active = false;
+
+ if (redraw_active) {
+ if (schedule_redraw_conn.connected()) {
+ // In first link in chain, from schedule_redraw() to launch_redraw(). Break the link and exit.
+ schedule_redraw_conn.disconnect();
+ } else {
+ // Otherwise, the background process is running. Interrupt the signal chain at exit.
+ abort_flags.store((int)AbortFlags::Hard, std::memory_order_relaxed);
+ if (prefs.debug_logging) std::cout << "Hard exit request" << std::endl;
+ sync.waitForExit();
+
+ // Unsnapshot the CanvasItems and DrawingItems.
+ canvasitem_ctx->unsnapshot();
+ q->_drawing->unsnapshot();
+ }
+
+ redraw_active = false;
+ redraw_requested = false;
+ assert(!schedule_redraw_conn.connected());
+ }
+}
+
+void CanvasPrivate::deactivate_graphics()
+{
+ if (q->get_opengl_enabled()) q->make_current();
+ commit_tiles();
+ stores.set_graphics(nullptr);
+ graphics.reset();
+}
+
+Canvas::~Canvas()
+{
+ // Remove entire CanvasItem tree.
+ d->canvasitem_ctx.reset();
+}
+
+void Canvas::set_drawing(Drawing *drawing)
+{
+ if (d->active && !drawing) d->deactivate();
+ _drawing = drawing;
+ if (_drawing) {
+ _drawing->setRenderMode(_render_mode == RenderMode::OUTLINE_OVERLAY ? RenderMode::NORMAL : _render_mode);
+ _drawing->setColorMode(_color_mode);
+ _drawing->setOutlineOverlay(d->outlines_required());
+ }
+ if (!d->active && get_realized() && drawing) d->activate();
+}
+
+CanvasItemGroup *Canvas::get_canvas_item_root() const
+{
+ return d->canvasitem_ctx->root();
+}
+
+void Canvas::on_realize()
+{
+ parent_type::on_realize();
+ d->activate_graphics();
+ if (_drawing) d->activate();
+}
+
+void Canvas::on_unrealize()
+{
+ if (_drawing) d->deactivate();
+ d->deactivate_graphics();
+ parent_type::on_unrealize();
+}
+
+/*
+ * Redraw process managment
+ */
+
+// Schedule another redraw iteration to take place, waiting for the current one to finish if necessary.
+void CanvasPrivate::schedule_redraw()
+{
+ if (!active) {
+ // We can safely discard calls until active, because we will run an iteration on activation later in initialisation.
+ return;
+ }
+
+ // Ensure another iteration is performed if one is in progress.
+ redraw_requested = true;
+
+ if (redraw_active) {
+ return;
+ }
+
+ redraw_active = true;
+
+ // Call run_redraw() as soon as possible on the main loop. (Cannot run now since CanvasItem tree could be in an invalid intermediate state.)
+ assert(!schedule_redraw_conn.connected());
+ schedule_redraw_conn = Glib::signal_idle().connect([this] {
+ if (q->get_opengl_enabled()) {
+ q->make_current();
+ }
+ if (prefs.debug_logging) std::cout << "Redraw start" << std::endl;
+ launch_redraw();
+ return false;
+ }, Glib::PRIORITY_HIGH);
+}
+
+// Update state and launch redraw process in background. Requires a current OpenGL context.
+void CanvasPrivate::launch_redraw()
+{
+ assert(redraw_active);
+
+ // Determine whether the rendering parameters have changed, and trigger full store recreation if so.
+ if ((outlines_required() && !outlines_enabled) || scale_factor != q->get_scale_factor()) {
+ stores.reset();
+ }
+
+ outlines_enabled = outlines_required();
+ scale_factor = q->get_scale_factor();
+
+ graphics->set_outlines_enabled(outlines_enabled);
+ graphics->set_scale_factor(scale_factor);
+
+ /*
+ * Update state.
+ */
+
+ // Page information.
+ pi.pages.clear();
+ canvasitem_ctx->root()->visit_page_rects([this] (auto &rect) {
+ pi.pages.emplace_back(rect);
+ });
+
+ graphics->set_colours(page, desk, border);
+ graphics->set_background_in_stores(background_in_stores_required());
+
+ q->_drawing->setClip(calc_page_clip());
+
+ // Stores.
+ handle_stores_action(stores.update(Fragment{ q->_affine, q->get_area_world() }));
+
+ // Geometry.
+ bool const affine_changed = canvasitem_ctx->affine() != stores.store().affine;
+ if (q->_need_update || affine_changed) {
+ FrameCheck::Event fc;
+ if (prefs.debug_framecheck) fc = FrameCheck::Event("update");
+ q->_need_update = false;
+ canvasitem_ctx->setAffine(stores.store().affine);
+ canvasitem_ctx->root()->update(affine_changed);
+ }
+
+ // Update strategy.
+ auto const strategy = pref_to_updater(prefs.update_strategy);
+ if (updater->get_strategy() != strategy) {
+ auto new_updater = Updater::create(strategy);
+ new_updater->clean_region = std::move(updater->clean_region);
+ updater = std::move(new_updater);
+ }
+
+ updater->mark_dirty(invalidated);
+ invalidated = Cairo::Region::create();
+
+ updater->next_frame();
+
+ /*
+ * Launch redraw process in background.
+ */
+
+ // If asked to, don't paint anything and instead halt the redraw process.
+ if (prefs.debug_disable_redraw) {
+ redraw_active = false;
+ return;
+ }
+
+ // Snapshot the CanvasItems and DrawingItems.
+ canvasitem_ctx->snapshot();
+ q->_drawing->snapshot();
+
+ // Get the mouse position in screen space.
+ rd.mouse_loc = last_mouse.value_or((Geom::Point(q->get_dimensions()) / 2).round());
+
+ // Map the mouse to canvas space.
+ rd.mouse_loc += q->_pos;
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ rd.mouse_loc = (Geom::Point(rd.mouse_loc) * q->_affine.inverse() * stores.store().affine).round();
+ }
+
+ // Get the visible rect.
+ rd.visible = q->get_area_world();
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ rd.visible = (Geom::Parallelogram(rd.visible) * q->_affine.inverse() * stores.store().affine).bounds().roundOutwards();
+ }
+
+ // Get other misc data.
+ rd.store = Fragment{ stores.store().affine, stores.store().rect };
+ rd.decoupled_mode = stores.mode() == Stores::Mode::Decoupled;
+ rd.coarsener_min_size = prefs.coarsener_min_size;
+ rd.coarsener_glue_size = prefs.coarsener_glue_size;
+ rd.coarsener_min_fullness = prefs.coarsener_min_fullness;
+ rd.tile_size = prefs.tile_size;
+ rd.preempt = prefs.preempt;
+ rd.margin = prefs.prerender;
+ rd.redraw_delay = prefs.debug_delay_redraw ? std::make_optional<int>(prefs.debug_delay_redraw_time) : std::nullopt;
+ rd.render_time_limit = prefs.render_time_limit;
+ rd.numthreads = get_numthreads();
+ rd.background_in_stores_required = background_in_stores_required();
+ rd.page = page;
+ rd.desk = desk;
+ rd.debug_framecheck = prefs.debug_framecheck;
+ rd.debug_show_redraw = prefs.debug_show_redraw;
+
+ rd.snapshot_drawn = stores.snapshot().drawn ? stores.snapshot().drawn->copy() : Cairo::RefPtr<Cairo::Region>();
+ rd.grabbed = q->_grabbed_canvas_item && prefs.block_updates ? (roundedOutwards(q->_grabbed_canvas_item->get_bounds()) & rd.visible & rd.store.rect).regularized() : Geom::OptIntRect();
+
+ abort_flags.store((int)AbortFlags::None, std::memory_order_relaxed);
+
+ boost::asio::post(*pool, [this] { init_tiler(); });
+}
+
+void CanvasPrivate::after_redraw()
+{
+ assert(redraw_active);
+
+ // Unsnapshot the CanvasItems and DrawingItems.
+ canvasitem_ctx->unsnapshot();
+ q->_drawing->unsnapshot();
+
+ // OpenGL context needed for commit_tiles(), stores.finished_draw(), and launch_redraw().
+ if (q->get_opengl_enabled()) {
+ q->make_current();
+ }
+
+ // Commit tiles before stores.finished_draw() to avoid changing stores while tiles are still pending.
+ commit_tiles();
+
+ // Handle any pending stores action.
+ bool stores_changed = false;
+ if (!rd.timeoutflag) {
+ auto const ret = stores.finished_draw(Fragment{ q->_affine, q->get_area_world() });
+ handle_stores_action(ret);
+ if (ret != Stores::Action::None) {
+ stores_changed = true;
+ }
+ }
+
+ // Relaunch or stop as necessary.
+ if (rd.timeoutflag || redraw_requested || stores_changed) {
+ if (prefs.debug_logging) std::cout << "Continuing redrawing" << std::endl;
+ redraw_requested = false;
+ launch_redraw();
+ } else {
+ if (prefs.debug_logging) std::cout << "Redraw exit" << std::endl;
+ redraw_active = false;
+ }
+}
+
+void CanvasPrivate::handle_stores_action(Stores::Action action)
+{
+ switch (action) {
+ case Stores::Action::Recreated:
+ // Set everything as needing redraw.
+ invalidated->do_union(geom_to_cairo(stores.store().rect));
+ updater->reset();
+
+ if (prefs.debug_show_unclean) q->queue_draw();
+ break;
+
+ case Stores::Action::Shifted:
+ invalidated->intersect(geom_to_cairo(stores.store().rect));
+ updater->intersect(stores.store().rect);
+
+ if (prefs.debug_show_unclean) q->queue_draw();
+ break;
+
+ default:
+ break;
+ }
+
+ if (action != Stores::Action::None) {
+ q->_drawing->setCacheLimit(stores.store().rect);
+ }
+}
+
+// Commit all in-flight tiles to the stores. Requires a current OpenGL context (for graphics->draw_tile).
+void CanvasPrivate::commit_tiles()
+{
+ framecheck_whole_function(this)
+
+ decltype(rd.tiles) tiles;
+
+ {
+ auto lock = std::lock_guard(rd.tiles_mutex);
+ tiles = std::move(rd.tiles);
+ }
+
+ for (auto &tile : tiles) {
+ // Todo: Make CMS system thread-safe, then move this to render thread too.
+ if (q->_cms_active) {
+ auto transf = prefs.from_display
+ ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key)
+ : Inkscape::CMSSystem::getDisplayTransform();
+ if (transf) {
+ tile.surface->flush();
+ auto px = tile.surface->get_data();
+ int stride = tile.surface->get_stride();
+ for (int i = 0; i < tile.surface->get_height(); i++) {
+ auto row = px + i * stride;
+ Inkscape::CMSSystem::doTransform(transf, row, row, tile.surface->get_width());
+ }
+ tile.surface->mark_dirty();
+ }
+ }
+
+ // Paste tile content onto stores.
+ graphics->draw_tile(tile.fragment, std::move(tile.surface), std::move(tile.outline_surface));
+
+ // Add to drawn region.
+ assert(stores.store().rect.contains(tile.fragment.rect));
+ stores.mark_drawn(tile.fragment.rect);
+
+ // Get the rectangle of screen-space needing repaint.
+ Geom::IntRect repaint_rect;
+ if (stores.mode() == Stores::Mode::Normal) {
+ // Simply translate to get back to screen space.
+ repaint_rect = tile.fragment.rect - q->_pos;
+ } else {
+ // Transform into screen space, take bounding box, and round outwards.
+ auto pl = Geom::Parallelogram(tile.fragment.rect);
+ pl *= stores.store().affine.inverse() * q->_affine;
+ pl *= Geom::Translate(-q->_pos);
+ repaint_rect = pl.bounds().roundOutwards();
+ }
+
+ // Check if repaint is necessary - some rectangles could be entirely off-screen.
+ auto screen_rect = Geom::IntRect({0, 0}, q->get_dimensions());
+ if ((repaint_rect & screen_rect).regularized()) {
+ // Schedule repaint.
+ queue_draw_area(repaint_rect);
+ }
+ }
+}
+
+/*
+ * Auto-scrolling
+ */
+
+static Geom::Point cap_length(Geom::Point const &pt, double max)
+{
+ auto const r = pt.length();
+ return r <= max ? pt : pt * max / r;
+}
+
+static double profile(double r)
+{
+ constexpr double max_speed = 30.0;
+ constexpr double max_distance = 25.0;
+ return std::clamp(Geom::sqr(r / max_distance) * max_speed, 1.0, max_speed);
+}
+
+static Geom::Point apply_profile(Geom::Point const &pt)
+{
+ auto const r = pt.length();
+ if (r <= Geom::EPSILON) return {};
+ return pt * profile(r) / r;
+}
+
+void CanvasPrivate::autoscroll_begin(Geom::IntPoint const &to)
+{
+ if (!q->_desktop) {
+ return;
+ }
+
+ auto const rect = expandedBy(Geom::IntRect({}, q->get_dimensions()), -(int)prefs.autoscrolldistance);
+ strain = to - rect.clamp(to);
+
+ if (strain == Geom::IntPoint(0, 0) || tick_callback) {
+ return;
+ }
+
+ tick_callback = q->add_tick_callback([this] (Glib::RefPtr<Gdk::FrameClock> const &clock) {
+ auto timings = clock->get_current_timings();
+ auto const t = timings->get_frame_time();
+ double dt;
+ if (last_time) {
+ dt = t - *last_time;
+ } else {
+ dt = timings->get_refresh_interval();
+ }
+ last_time = t;
+ dt *= 60.0 / 1e6 * prefs.autoscrollspeed;
+
+ bool const strain_zero = strain == Geom::IntPoint(0, 0);
+
+ if (strain.x() * velocity.x() < 0) velocity.x() = 0;
+ if (strain.y() * velocity.y() < 0) velocity.y() = 0;
+ auto const tgtvel = apply_profile(strain);
+ auto const max_accel = strain_zero ? 3 : 2;
+ velocity += cap_length(tgtvel - velocity, max_accel * dt);
+ displacement += velocity * dt;
+ auto const dpos = displacement.round();
+ q->_desktop->scroll_relative(-dpos);
+ displacement -= dpos;
+
+ if (last_mouse) {
+ GdkEventMotion event;
+ memset(&event, 0, sizeof(GdkEventMotion));
+ event.type = GDK_MOTION_NOTIFY;
+ event.x = last_mouse->x();
+ event.y = last_mouse->y();
+ event.state = q->_state;
+ emit_event(reinterpret_cast<GdkEvent*>(&event));
+ }
+
+ if (strain_zero && velocity.length() <= 0.1) {
+ tick_callback = {};
+ last_time = {};
+ displacement = velocity = {};
+ return false;
+ }
+
+ q->queue_draw();
+
+ return true;
+ });
+}
+
+void CanvasPrivate::autoscroll_end()
+{
+ strain = {};
+}
+
+// Allow auto-scrolling to take place if the mouse reaches the edge.
+// The effect wears off when the mouse is next released.
+void Canvas::enable_autoscroll()
+{
+ if (d->last_mouse) {
+ d->autoscroll_begin(*d->last_mouse);
+ } else {
+ d->autoscroll_end();
+ }
+}
+
+/*
+ * Event handling
+ */
+
+bool Canvas::on_scroll_event(GdkEventScroll *scroll_event)
+{
+ return d->process_event(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)
+{
+ if (button_event->button == 1) {
+ d->autoscroll_end();
+ }
+
+ 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.
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ auto cursor_position = Geom::IntPoint(button_event->x, button_event->y);
+ switch (button_event->type) {
+ case GDK_BUTTON_PRESS:
+ if (_hover_direction != Inkscape::SplitDirection::NONE) {
+ _split_dragging = true;
+ _split_drag_start = cursor_position;
+ 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:
+ if (!_split_dragging) break;
+ _split_dragging = false;
+
+ // Check if we are near the edge. If so, revert to normal mode.
+ 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_frac = {0.5, 0.5};
+ set_cursor();
+ set_split_mode(Inkscape::SplitMode::NORMAL);
+
+ // 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(static_cast<int>(Inkscape::SplitMode::NORMAL));
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return d->process_event(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->process_event(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->process_event(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->process_event(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool Canvas::on_key_release_event(GdkEventKey *key_event)
+{
+ return d->process_event(reinterpret_cast<GdkEvent*>(key_event));
+}
+
+bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event)
+{
+ // Record the last mouse position.
+ d->last_mouse = Geom::IntPoint(motion_event->x, motion_event->y);
+
+ // Handle interactions with the split view controller.
+ if (_split_mode == Inkscape::SplitMode::XRAY) {
+ queue_draw();
+ } else if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ auto cursor_position = Geom::IntPoint(motion_event->x, motion_event->y);
+
+ // Move controller.
+ if (_split_dragging) {
+ auto delta = cursor_position - _split_drag_start;
+ if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) {
+ delta.x() = 0;
+ } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) {
+ delta.y() = 0;
+ }
+ _split_frac += Geom::Point(delta) / get_dimensions();
+ _split_drag_start = cursor_position;
+ queue_draw();
+ return true;
+ }
+
+ auto split_position = (_split_frac * get_dimensions()).round();
+ auto diff = cursor_position - split_position;
+ auto hover_direction = Inkscape::SplitDirection::NONE;
+ if (Geom::Point(diff).length() < 20.0) {
+ // We're hovering over circle, figure out which direction we are in.
+ if (diff.y() - diff.x() > 0) {
+ if (diff.y() + diff.x() > 0) {
+ hover_direction = Inkscape::SplitDirection::SOUTH;
+ } else {
+ hover_direction = Inkscape::SplitDirection::WEST;
+ }
+ } else {
+ if (diff.y() + diff.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(diff.y()) < 3) {
+ // We're hovering over the horizontal line.
+ hover_direction = Inkscape::SplitDirection::HORIZONTAL;
+ }
+ } else {
+ if (std::abs(diff.x()) < 3) {
+ // We're hovering over the 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;
+ }
+ }
+
+ // Avoid embarrassing neverending autoscroll in case the button-released handler somehow doesn't fire.
+ if (!(motion_event->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) {
+ d->autoscroll_end();
+ }
+
+ return d->process_event(reinterpret_cast<GdkEvent*>(motion_event));
+}
+
+// Unified handler for all events.
+bool CanvasPrivate::process_event(const GdkEvent *event)
+{
+ framecheck_whole_function(this)
+
+ if (!active) {
+ std::cerr << "Canvas::process_event: Called while not active!" << std::endl;
+ return false;
+ }
+
+ 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();
+ return emit_event(event);
+ }
+
+ 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.get());
+
+ 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_event' to manipulate the state variables relating
+// to the current object under the mouse, for example, to generate enter and leave events.
+//
+// This routine reacts to events from the canvas. Its 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->_drawing->snapshotted() && !canvasitem_ctx->snapshotted()) {
+ FrameCheck::Event fc;
+ if (prefs.debug_framecheck) fc = FrameCheck::Event("update", 1);
+ q->_need_update = false;
+ canvasitem_ctx->root()->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.
+ 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 && canvasitem_ctx->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;
+ }
+
+ // Look at where the cursor is to see if one should pick with outline mode.
+ bool outline = q->canvas_point_in_outline_zone({ x, y });
+
+ // Convert to world coordinates.
+ auto p = Geom::Point(x, y) + q->_pos;
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ p *= q->_affine.inverse() * canvasitem_ctx->affine();
+ }
+
+ q->_drawing->getCanvasItemDrawing()->set_pick_outline(outline);
+ q->_current_canvas_item_new = canvasitem_ctx->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;
+ // }
+ }
+
+ 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 (stores.mode() == Stores::Mode::Decoupled) {
+ p *= q->_affine.inverse() * canvasitem_ctx->affine();
+ }
+ 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
+{
+ return dimensions(get_allocation());
+}
+
+/**
+ * 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());
+}
+
+/**
+ * Return whether a point in screen space / canvas coordinates is inside the region
+ * of the canvas where things respond to mouse clicks as if they are in outline mode.
+ */
+bool Canvas::canvas_point_in_outline_zone(Geom::Point const &p) const
+{
+ if (_render_mode == RenderMode::OUTLINE || _render_mode == RenderMode::OUTLINE_OVERLAY) {
+ return true;
+ } else if (_split_mode == SplitMode::SPLIT) {
+ auto split_position = _split_frac * get_dimensions();
+ switch (_split_direction) {
+ case SplitDirection::NORTH: return p.y() > split_position.y();
+ case SplitDirection::SOUTH: return p.y() < split_position.y();
+ case SplitDirection::WEST: return p.x() > split_position.x();
+ case SplitDirection::EAST: return p.x() < split_position.x();
+ default: return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Return the last known mouse position of center if off-canvas.
+ */
+std::optional<Geom::Point> Canvas::get_last_mouse() const
+{
+ return d->last_mouse;
+}
+
+const Geom::Affine &Canvas::get_geom_affine() const
+{
+ return d->canvasitem_ctx->affine();
+}
+
+void CanvasPrivate::queue_draw_area(const Geom::IntRect &rect)
+{
+ if (q->get_opengl_enabled()) {
+ // Note: GTK glitches out when you use queue_draw_area in OpenGL mode.
+ // It's also pointless, because it seems to just call queue_draw anyway.
+ q->queue_draw();
+ } else {
+ 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->invalidated->do_union(geom_to_cairo(d->stores.store().rect));
+ d->schedule_redraw();
+ 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;
+ }
+
+ if (d->redraw_active && d->invalidated->empty()) {
+ d->abort_flags.store((int)AbortFlags::Soft, std::memory_order_relaxed); // responding to partial invalidations takes priority over prerendering
+ if (d->prefs.debug_logging) std::cout << "Soft exit request" << std::endl;
+ }
+
+ auto const rect = Geom::IntRect(x0, y0, x1, y1);
+ d->invalidated->do_union(geom_to_cairo(rect));
+ d->schedule_redraw();
+ 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 const &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 redraw process to perform the update.
+ d->schedule_redraw();
+}
+
+/**
+ * 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->schedule_redraw();
+ queue_draw();
+}
+
+/**
+ * Set the affine for the canvas.
+ */
+void Canvas::set_affine(Geom::Affine const &affine)
+{
+ if (_affine == affine) {
+ return;
+ }
+
+ _affine = affine;
+
+ d->schedule_redraw();
+ queue_draw();
+}
+
+/**
+ * Set the desk colour. Transparency is interpreted as amount of checkerboard.
+ */
+void Canvas::set_desk(uint32_t rgba)
+{
+ if (d->desk == rgba) return;
+ bool invalidated = d->background_in_stores_enabled;
+ d->desk = rgba;
+ invalidated |= d->background_in_stores_enabled = d->background_in_stores_required();
+ if (get_realized() && invalidated) redraw_all();
+ queue_draw();
+}
+
+/**
+ * Set the page border colour. Although we don't draw the borders, this colour affects the shadows which we do draw (in OpenGL mode).
+ */
+void Canvas::set_border(uint32_t rgba)
+{
+ if (d->border == rgba) return;
+ d->border = rgba;
+ if (get_realized() && get_opengl_enabled()) queue_draw();
+}
+
+/**
+ * Set the page colour. Like the desk colour, transparency is interpreted as checkerboard.
+ */
+void Canvas::set_page(uint32_t rgba)
+{
+ if (d->page == rgba) return;
+ bool invalidated = d->background_in_stores_enabled;
+ d->page = rgba;
+ invalidated |= d->background_in_stores_enabled = d->background_in_stores_required();
+ if (get_realized() && invalidated) redraw_all();
+ queue_draw();
+}
+
+uint32_t Canvas::get_effective_background() const
+{
+ auto arr = checkerboard_darken(rgb_to_array(d->desk), 1.0f - 0.5f * SP_RGBA32_A_U(d->desk) / 255.0f);
+ return SP_RGBA32_F_COMPOSE(arr[0], arr[1], arr[2], 1.0);
+}
+
+void Canvas::set_render_mode(Inkscape::RenderMode mode)
+{
+ if ((_render_mode == RenderMode::OUTLINE_OVERLAY) != (mode == RenderMode::OUTLINE_OVERLAY) && !get_opengl_enabled()) {
+ queue_draw();
+ }
+ _render_mode = mode;
+ if (_drawing) {
+ _drawing->setRenderMode(_render_mode == RenderMode::OUTLINE_OVERLAY ? RenderMode::NORMAL : _render_mode);
+ _drawing->setOutlineOverlay(d->outlines_required());
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void Canvas::set_color_mode(Inkscape::ColorMode mode)
+{
+ _color_mode = mode;
+ if (_drawing) {
+ _drawing->setColorMode(_color_mode);
+ }
+ if (_desktop) {
+ _desktop->setWindowTitle(); // Mode is listed in title.
+ }
+}
+
+void Canvas::set_split_mode(Inkscape::SplitMode mode)
+{
+ if (_split_mode != mode) {
+ _split_mode = mode;
+ if (_split_mode == Inkscape::SplitMode::SPLIT) {
+ _hover_direction = Inkscape::SplitDirection::NONE;
+ }
+ if (_drawing) {
+ _drawing->setOutlineOverlay(d->outlines_required());
+ }
+ redraw_all();
+ }
+}
+
+void Canvas::set_clip_to_page_mode(bool clip)
+{
+ if (clip != d->clip_to_page) {
+ d->clip_to_page = clip;
+ d->schedule_redraw();
+ }
+}
+
+void Canvas::set_cms_key(std::string key)
+{
+ _cms_key = std::move(key);
+ _cms_active = !_cms_key.empty();
+ redraw_all();
+}
+
+/**
+ * Clear current and grabbed items.
+ */
+void Canvas::canvas_item_destructed(Inkscape::CanvasItem *item)
+{
+ if (!d->active) {
+ return;
+ }
+
+ 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;
+ }
+}
+
+std::optional<Geom::PathVector> CanvasPrivate::calc_page_clip() const
+{
+ if (!clip_to_page) {
+ return {};
+ }
+
+ Geom::PathVector pv;
+ for (auto &rect : pi.pages) {
+ pv.push_back(Geom::Path(rect));
+ }
+ return pv;
+}
+
+// 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)
+{
+ auto const old_dimensions = get_dimensions();
+ parent_type::on_size_allocate(allocation);
+ auto const new_dimensions = get_dimensions();
+
+ // Necessary as GTK seems to somehow invalidate the current pipeline state upon resize.
+ if (d->active) {
+ d->graphics->invalidated_glstate();
+ }
+
+ // Trigger the size update to be applied to the stores before the next redraw of the window.
+ d->schedule_redraw();
+
+ // Keep canvas centered and optionally zoomed in.
+ if (_desktop && new_dimensions != old_dimensions) {
+ auto const midpoint = _desktop->w2d(_pos + Geom::Point(old_dimensions) * 0.5);
+ double zoom = _desktop->current_zoom();
+
+ auto prefs = Preferences::get();
+ if (prefs->getBool("/options/stickyzoom/value", false)) {
+ // Calculate adjusted zoom.
+ auto const old_minextent = min(old_dimensions);
+ auto const new_minextent = min(new_dimensions);
+ if (old_minextent != 0) {
+ zoom *= (double)new_minextent / old_minextent;
+ }
+ }
+
+ _desktop->zoom_absolute(midpoint, zoom, false);
+ }
+}
+
+Glib::RefPtr<Gdk::GLContext> Canvas::create_context()
+{
+ Glib::RefPtr<Gdk::GLContext> result;
+
+ try {
+ result = get_window()->create_gl_context();
+ } catch (const Gdk::GLError &e) {
+ std::cerr << "Failed to create OpenGL context: " << e.what().raw() << std::endl;
+ return {};
+ }
+
+ try {
+ result->realize();
+ } catch (const Glib::Error &e) {
+ std::cerr << "Failed to realize OpenGL context: " << e.what().raw() << std::endl;
+ return {};
+ }
+
+ return result;
+}
+
+void Canvas::paint_widget(Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ framecheck_whole_function(d)
+
+ if (!d->active) {
+ std::cerr << "Canvas::paint_widget: Called while not active!" << std::endl;
+ return;
+ }
+
+ if constexpr (false) d->canvasitem_ctx->root()->canvas_item_print_tree();
+
+ // Although launch_redraw() 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. Since launch_redraw() is required to have been
+ // called at least once to perform vital initalisation, if it has not been called, we have to exit.
+ if (d->stores.mode() == Stores::Mode::None) {
+ return;
+ }
+
+ // Commit pending tiles in case GTK called on_draw even though after_redraw() is scheduled at higher priority.
+ if (!d->redraw_active) {
+ d->commit_tiles();
+ }
+
+ if (get_opengl_enabled()) {
+ bind_framebuffer();
+ }
+
+ Graphics::PaintArgs args;
+ args.mouse = d->last_mouse;
+ args.render_mode = _render_mode;
+ args.splitmode = _split_mode;
+ args.splitfrac = _split_frac;
+ args.splitdir = _split_direction;
+ args.hoverdir = _hover_direction;
+ args.yaxisdir = _desktop ? _desktop->yaxisdir() : 1.0;
+
+ d->graphics->paint_widget(Fragment{ _affine, get_area_world() }, args, cr);
+
+ // If asked, run an animation loop.
+ if (d->prefs.debug_animate) {
+ auto t = g_get_monotonic_time() / 1700000.0;
+ auto affine = Geom::Rotate(t * 5) * Geom::Scale(1.0 + 0.6 * cos(t * 2));
+ set_affine(affine);
+ auto dim = _desktop && _desktop->doc() ? _desktop->doc()->getDimensions() : Geom::Point();
+ set_pos(Geom::Point((0.5 + 0.3 * cos(t * 2)) * dim.x(), (0.5 + 0.3 * sin(t * 3)) * dim.y()) * affine - Geom::Point(get_dimensions()) * 0.5);
+ }
+}
+
+/*
+ * Async redrawing process
+ */
+
+// 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;
+}
+
+static std::optional<Geom::Dim2> bisect(Geom::IntRect const &rect, int tile_size)
+{
+ 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 > tile_size) {
+ return Geom::X;
+ }
+ } else {
+ if (bh > tile_size) {
+ return Geom::Y;
+ }
+ }
+
+ return {};
+}
+
+void CanvasPrivate::init_tiler()
+{
+ // Begin processing redraws.
+ rd.start_time = g_get_monotonic_time();
+ rd.phase = 0;
+ rd.vis_store = (rd.visible & rd.store.rect).regularized();
+
+ if (!init_redraw()) {
+ sync.signalExit();
+ return;
+ }
+
+ // Launch render threads to process tiles.
+ rd.timeoutflag = false;
+
+ rd.numactive = rd.numthreads;
+
+ for (int i = 0; i < rd.numthreads - 1; i++) {
+ boost::asio::post(*pool, [=] { render_tile(i); });
+ }
+
+ render_tile(rd.numthreads - 1);
+}
+
+bool CanvasPrivate::init_redraw()
+{
+ assert(rd.rects.empty());
+
+ switch (rd.phase) {
+ case 0:
+ if (rd.vis_store && rd.decoupled_mode) {
+ // The highest priority to redraw is the region that is visible but not covered by either clean or snapshot content, if in decoupled mode.
+ // If this is not rendered immediately, it will be perceived as edge flicker, most noticeably on zooming out, but also on rotation too.
+ process_redraw(*rd.vis_store, unioned(updater->clean_region->copy(), rd.snapshot_drawn));
+ return true;
+ } else {
+ rd.phase++;
+ // fallthrough
+ }
+
+ case 1:
+ // Another high priority to redraw is the grabbed canvas item, if the user has requested block updates.
+ if (rd.grabbed) {
+ process_redraw(*rd.grabbed, updater->clean_region, false, false); // non-interruptible, non-preemptible
+ return true;
+ } else {
+ rd.phase++;
+ // fallthrough
+ }
+
+ case 2:
+ if (rd.vis_store) {
+ // The main priority to redraw, and the bread and butter of Inkscape's painting, is the visible content that is not clean.
+ // This may be done over several cycles, at the direction of the Updater, each outwards from the mouse.
+ process_redraw(*rd.vis_store, updater->get_next_clean_region());
+ return true;
+ } else {
+ rd.phase++;
+ // fallthrough
+ }
+
+ case 3: {
+ // The lowest priority to redraw is the prerender margin around the visible rectangle.
+ // (This is in addition to any opportunistic prerendering that may have already occurred in the above steps.)
+ auto prerender = expandedBy(rd.visible, rd.margin);
+ auto prerender_store = (prerender & rd.store.rect).regularized();
+ if (prerender_store) {
+ process_redraw(*prerender_store, updater->clean_region);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ default:
+ assert(false);
+ return false;
+ }
+}
+
+// Paint a given subrectangle of the store given by 'bounds', but avoid painting the part of it within 'clean' if possible.
+// Some parts both outside the bounds and inside the clean region may also be painted if it helps reduce fragmentation.
+void CanvasPrivate::process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr<Cairo::Region> clean, bool interruptible, bool preemptible)
+{
+ rd.bounds = bounds;
+ rd.clean = std::move(clean);
+ rd.interruptible = interruptible;
+ rd.preemptible = preemptible;
+
+ // Assert that we do not render outside of store.
+ assert(rd.store.rect.contains(rd.bounds));
+
+ // Get the region we are asked to paint.
+ auto region = Cairo::Region::create(geom_to_cairo(rd.bounds));
+ region->subtract(rd.clean);
+
+ // Get the list of rectangles to paint, coarsened to avoid fragmentation.
+ rd.rects = coarsen(region,
+ std::min<int>(rd.coarsener_min_size, rd.tile_size / 2),
+ std::min<int>(rd.coarsener_glue_size, rd.tile_size / 2),
+ rd.coarsener_min_fullness);
+
+ // Put the rectangles into a heap sorted by distance from mouse.
+ std::make_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
+
+ // Adjust the effective tile size proportional to the painting area.
+ double adjust = (double)cairo_to_geom(region->get_extents()).maxExtent() / rd.visible.maxExtent();
+ adjust = std::clamp(adjust, 0.3, 1.0);
+ rd.effective_tile_size = rd.tile_size * adjust;
+}
+
+// Process rectangles until none left or timed out.
+void CanvasPrivate::render_tile(int debug_id)
+{
+ rd.mutex.lock();
+
+ std::string fc_str;
+ FrameCheck::Event fc;
+ if (rd.debug_framecheck) {
+ fc_str = "render_thread_" + std::to_string(debug_id + 1);
+ fc = FrameCheck::Event(fc_str.c_str());
+ }
+
+ while (true) {
+ // If we've run out of rects, try to start a new redraw cycle.
+ if (rd.rects.empty()) {
+ if (end_redraw()) {
+ // More redraw cycles to do.
+ continue;
+ } else {
+ // All finished.
+ break;
+ }
+ }
+
+ // Check for cancellation.
+ auto const flags = abort_flags.load(std::memory_order_relaxed);
+ bool const soft = flags & (int)AbortFlags::Soft;
+ bool const hard = flags & (int)AbortFlags::Hard;
+ if (hard || (rd.phase == 3 && soft)) {
+ break;
+ }
+
+ // Extract the closest rectangle to the mouse.
+ std::pop_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
+ auto rect = rd.rects.back();
+ rd.rects.pop_back();
+
+ // Cull empty rectangles.
+ if (rect.hasZeroArea()) {
+ 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 (rd.clean->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) {
+ continue;
+ }
+
+ // Lambda to add a rectangle to the heap.
+ auto add_rect = [&] (Geom::IntRect const &rect) {
+ rd.rects.emplace_back(rect);
+ std::push_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp());
+ };
+
+ // If the rectangle needs bisecting, bisect it and put it back on the heap.
+ if (auto axis = bisect(rect, rd.effective_tile_size)) {
+ 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);
+ continue;
+ }
+
+ // Extend thin rectangles at the edge of the bounds rect to at least some minimum size, being sure to keep them within the store.
+ // (This ensures we don't end up rendering one thin rectangle at the edge every frame while the view is moved continuously.)
+ if (rd.preemptible) {
+ if (rect.width() < rd.preempt) {
+ if (rect.left() == rd.bounds.left() ) rect.setLeft (std::max(rect.right() - rd.preempt, rd.store.rect.left() ));
+ if (rect.right() == rd.bounds.right()) rect.setRight(std::min(rect.left() + rd.preempt, rd.store.rect.right()));
+ }
+ if (rect.height() < rd.preempt) {
+ if (rect.top() == rd.bounds.top() ) rect.setTop (std::max(rect.bottom() - rd.preempt, rd.store.rect.top() ));
+ if (rect.bottom() == rd.bounds.bottom()) rect.setBottom(std::min(rect.top() + rd.preempt, rd.store.rect.bottom()));
+ }
+ }
+
+ // Mark the rectangle as clean.
+ updater->mark_clean(rect);
+
+ rd.mutex.unlock();
+
+ // Paint the rectangle.
+ paint_rect(rect);
+
+ rd.mutex.lock();
+
+ // Check for timeout.
+ if (rd.interruptible) {
+ auto now = g_get_monotonic_time();
+ auto elapsed = now - rd.start_time;
+ if (elapsed > rd.render_time_limit * 1000) {
+ // Timed out. Temporarily return to GTK main loop, and come back here when next idle.
+ rd.timeoutflag = true;
+ break;
+ }
+ }
+ }
+
+ if (rd.debug_framecheck && rd.timeoutflag) {
+ fc.subtype = 1;
+ }
+
+ rd.numactive--;
+ bool const done = rd.numactive == 0;
+
+ rd.mutex.unlock();
+
+ if (done) {
+ rd.rects.clear();
+ sync.signalExit();
+ }
+}
+
+bool CanvasPrivate::end_redraw()
+{
+ switch (rd.phase) {
+ case 0:
+ rd.phase++;
+ return init_redraw();
+
+ case 1:
+ rd.phase++;
+ // Reset timeout to leave the normal amount of time for clearing up artifacts.
+ rd.start_time = g_get_monotonic_time();
+ return init_redraw();
+
+ case 2:
+ if (!updater->report_finished()) {
+ rd.phase++;
+ }
+ return init_redraw();
+
+ case 3:
+ return false;
+
+ default:
+ assert(false);
+ return false;
+ }
+}
+
+void CanvasPrivate::paint_rect(Geom::IntRect const &rect)
+{
+ // Make sure the paint rectangle lies within the store.
+ assert(rd.store.rect.contains(rect));
+
+ auto paint = [&, this] (bool need_background, bool outline_pass) {
+
+ auto surface = graphics->request_tile_surface(rect, true);
+ if (!surface) {
+ sync.runInMain([&] {
+ if (prefs.debug_logging) std::cout << "Blocked - buffer mapping" << std::endl;
+ if (q->get_opengl_enabled()) q->make_current();
+ surface = graphics->request_tile_surface(rect, false);
+ });
+ }
+
+ try {
+
+ paint_single_buffer(surface, rect, need_background, outline_pass);
+
+ } catch (std::bad_alloc const &) {
+ // Note: std::bad_alloc actually indicates a Cairo error that occurs regularly at high zoom, and we must handle it.
+ // See https://gitlab.com/inkscape/inkscape/-/issues/3975
+ sync.runInMain([&] {
+ std::cerr << "Rendering failure. You probably need to zoom out!" << std::endl;
+ if (q->get_opengl_enabled()) q->make_current();
+ graphics->junk_tile_surface(std::move(surface));
+ surface = graphics->request_tile_surface(rect, false);
+ paint_error_buffer(surface);
+ });
+ }
+
+ return surface;
+ };
+
+ // Create and render the tile.
+ Tile tile;
+ tile.fragment.affine = rd.store.affine;
+ tile.fragment.rect = rect;
+ tile.surface = paint(background_in_stores_required(), false);
+ if (outlines_enabled) {
+ tile.outline_surface = paint(false, true);
+ }
+
+ // Introduce an artificial delay for each rectangle.
+ if (rd.redraw_delay) g_usleep(*rd.redraw_delay);
+
+ // Stick the tile on the list of tiles to reap.
+ {
+ auto g = std::lock_guard(rd.tiles_mutex);
+ rd.tiles.emplace_back(std::move(tile));
+ }
+}
+
+void CanvasPrivate::paint_single_buffer(Cairo::RefPtr<Cairo::ImageSurface> const &surface, Geom::IntRect const &rect, bool need_background, bool outline_pass)
+{
+ // Create Cairo context.
+ auto cr = Cairo::Context::create(surface);
+
+ // Clear background.
+ cr->save();
+ if (need_background) {
+ Graphics::paint_background(Fragment{ rd.store.affine, rect }, pi, rd.page, rd.desk, cr);
+ } else {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ cr->restore();
+
+ // Render drawing on top of background.
+ auto buf = Inkscape::CanvasItemBuffer{ rect, scale_factor, cr, outline_pass };
+ canvasitem_ctx->root()->render(buf);
+
+ // Paint over newly drawn content with a translucent random colour.
+ if (rd.debug_show_redraw) {
+ cr->set_source_rgba((rand() % 256) / 255.0, (rand() % 256) / 255.0, (rand() % 256) / 255.0, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->paint();
+ }
+}
+
+void CanvasPrivate::paint_error_buffer(Cairo::RefPtr<Cairo::ImageSurface> const &surface)
+{
+ // Paint something into surface to represent an "error" state for that tile.
+ // Currently just paints solid black.
+ auto cr = Cairo::Context::create(surface);
+ cr->set_source_rgb(0, 0, 0);
+ cr->paint();
+}
+
+} // namespace Inkscape::UI::Widget
+
+/*
+ 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..970488e
--- /dev/null
+++ b/src/ui/widget/canvas.h
@@ -0,0 +1,224 @@
+// 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"
+#include "optglarea.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+
+class CanvasItem;
+class CanvasItemGroup;
+class Drawing;
+
+namespace UI {
+namespace Widget {
+
+class CanvasPrivate;
+
+/**
+ * A widget for Inkscape's canvas.
+ */
+class Canvas : public OptGLArea
+{
+ using parent_type = OptGLArea;
+
+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;
+
+ // 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; }
+ const Geom::Affine &get_geom_affine() const; // tool-base.cpp (todo: remove this dependency)
+
+ // Background
+ void set_desk (uint32_t rgba);
+ void set_border(uint32_t rgba);
+ void set_page (uint32_t rgba);
+ uint32_t get_effective_background() const; // This function is now wrong.
+
+ // 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; }
+ void set_clip_to_page_mode(bool clip);
+
+ // CMS
+ void set_cms_key(std::string key);
+ 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;
+ bool canvas_point_in_outline_zone(Geom::Point const &world) const;
+
+ // State
+ bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp
+
+ // Mouse
+ std::optional<Geom::Point> get_last_mouse() const; // desktop-widget.cpp
+
+ /* Methods */
+
+ // Invalidation
+ void redraw_all(); // Mark everything as having changed.
+ void redraw_area(Geom::Rect const &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.
+
+ // 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_all_enter_events(bool on) { _all_enter_events = on; }
+
+ void enable_autoscroll();
+
+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;
+
+ Glib::RefPtr<Gdk::GLContext> create_context() override;
+ void paint_widget(const Cairo::RefPtr<Cairo::Context>&) override;
+
+private:
+ /* Configuration */
+
+ // Desktop
+ SPDesktop *_desktop = nullptr;
+
+ // Drawing
+ Inkscape::Drawing *_drawing = nullptr;
+
+ // Geometry
+ Geom::IntPoint _pos = {0, 0}; ///< Coordinates of top-left pixel of canvas view within canvas.
+ Geom::Affine _affine; ///< The affine that we have been requested to draw at.
+
+ // 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 _need_update = true; // Set true so setting CanvasItem bounds are calculated at least once.
+
+ // Split view
+ Inkscape::SplitDirection _split_direction;
+ Geom::Point _split_frac;
+ Inkscape::SplitDirection _hover_direction;
+ bool _split_dragging;
+ Geom::IntPoint _split_drag_start;
+
+ 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/canvas/cairographics.cpp b/src/ui/widget/canvas/cairographics.cpp
new file mode 100644
index 0000000..42b3353
--- /dev/null
+++ b/src/ui/widget/canvas/cairographics.cpp
@@ -0,0 +1,423 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/parallelogram.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "cairographics.h"
+#include "stores.h"
+#include "prefs.h"
+#include "util.h"
+#include "framecheck.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+CairoGraphics::CairoGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+ : prefs(prefs)
+ , stores(stores)
+ , pi(pi) {}
+
+std::unique_ptr<Graphics> Graphics::create_cairo(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+{
+ return std::make_unique<CairoGraphics>(prefs, stores, pi);
+}
+
+void CairoGraphics::set_outlines_enabled(bool enabled)
+{
+ outlines_enabled = enabled;
+ if (!enabled) {
+ store.outline_surface.clear();
+ snapshot.outline_surface.clear();
+ }
+}
+
+void CairoGraphics::recreate_store(Geom::IntPoint const &dims)
+{
+ auto surface_size = dims * scale_factor;
+
+ auto make_surface = [&, this] {
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, surface_size.x(), surface_size.y());
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API!
+ return surface;
+ };
+
+ // Recreate the store surface.
+ bool reuse_surface = store.surface && dimensions(store.surface) == surface_size;
+ if (!reuse_surface) {
+ store.surface = make_surface();
+ }
+
+ // Ensure the store surface is filled with the correct default background.
+ if (background_in_stores) {
+ auto cr = Cairo::Context::create(store.surface);
+ paint_background(stores.store(), pi, page, desk, cr);
+ } else if (reuse_surface) {
+ auto cr = Cairo::Context::create(store.surface);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+
+ // Do the same for the outline surface (except always clearing it to transparent).
+ if (outlines_enabled) {
+ bool reuse_outline_surface = store.outline_surface && dimensions(store.outline_surface) == surface_size;
+ if (!reuse_outline_surface) {
+ store.outline_surface = make_surface();
+ } else {
+ auto cr = Cairo::Context::create(store.outline_surface);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ }
+}
+
+void CairoGraphics::shift_store(Fragment const &dest)
+{
+ auto surface_size = dest.rect.dimensions() * scale_factor;
+
+ // Determine the geometry of the shift.
+ auto shift = dest.rect.min() - stores.store().rect.min();
+ auto reuse_rect = (dest.rect & cairo_to_geom(stores.store().drawn->get_extents())).regularized();
+ assert(reuse_rect); // Should not be called if there is no overlap.
+
+ auto make_surface = [&, this] {
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, surface_size.x(), surface_size.y());
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API!
+ return surface;
+ };
+
+ // Create the new store surface.
+ bool reuse_surface = snapshot.surface && dimensions(snapshot.surface) == surface_size;
+ auto new_surface = reuse_surface ? std::move(snapshot.surface) : make_surface();
+
+ // Paint background into region of store not covered by next operation.
+ auto cr = Cairo::Context::create(new_surface);
+ if (background_in_stores || reuse_surface) {
+ auto reg = Cairo::Region::create(geom_to_cairo(dest.rect));
+ reg->subtract(geom_to_cairo(*reuse_rect));
+ reg->translate(-dest.rect.left(), -dest.rect.top());
+ cr->save();
+ region_to_path(cr, reg);
+ cr->clip();
+ if (background_in_stores) {
+ paint_background(dest, pi, page, desk, cr);
+ } else { // otherwise, reuse_surface is true
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ cr->restore();
+ }
+
+ // Copy re-usuable contents of old store into new store, shifted.
+ cr->rectangle(reuse_rect->left() - dest.rect.left(), reuse_rect->top() - dest.rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(store.surface, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+
+ // Set the result as the new store surface.
+ snapshot.surface = std::move(store.surface);
+ store.surface = std::move(new_surface);
+
+ // Do the same for the outline store
+ if (outlines_enabled) {
+ // Create.
+ bool reuse_outline_surface = snapshot.outline_surface && dimensions(snapshot.outline_surface) == surface_size;
+ auto new_outline_surface = reuse_outline_surface ? std::move(snapshot.outline_surface) : make_surface();
+ // Background.
+ auto cr = Cairo::Context::create(new_outline_surface);
+ if (reuse_outline_surface) {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ // Copy.
+ cr->rectangle(reuse_rect->left() - dest.rect.left(), reuse_rect->top() - dest.rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(store.outline_surface, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+ // Set.
+ snapshot.outline_surface = std::move(store.outline_surface);
+ store.outline_surface = std::move(new_outline_surface);
+ }
+}
+
+void CairoGraphics::swap_stores()
+{
+ std::swap(store, snapshot);
+}
+
+void CairoGraphics::fast_snapshot_combine()
+{
+ auto copy = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &to) {
+ auto cr = Cairo::Context::create(to);
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->translate(-stores.snapshot().rect.left(), -stores.snapshot().rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * stores.snapshot().affine));
+ cr->translate(-1.0, -1.0);
+ region_to_path(cr, shrink_region(stores.store().drawn, 2));
+ cr->translate(1.0, 1.0);
+ cr->clip();
+ cr->set_source(from, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ };
+
+ copy(store.surface, snapshot.surface);
+ if (outlines_enabled) copy(store.outline_surface, snapshot.outline_surface);
+}
+
+void CairoGraphics::snapshot_combine(Fragment const &dest)
+{
+ // Create the new fragment.
+ auto content_size = dest.rect.dimensions() * scale_factor;
+
+ auto make_surface = [&] {
+ auto result = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, content_size.x(), content_size.y());
+ cairo_surface_set_device_scale(result->cobj(), scale_factor, scale_factor); // No C++ API!
+ return result;
+ };
+
+ CairoFragment fragment;
+ fragment.surface = make_surface();
+ if (outlines_enabled) fragment.outline_surface = make_surface();
+
+ auto copy = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &store_from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &snapshot_from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &to, bool background) {
+ auto cr = Cairo::Context::create(to);
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ if (background) paint_background(dest, pi, page, desk, cr);
+ cr->translate(-dest.rect.left(), -dest.rect.top());
+ cr->transform(geom_to_cairo(stores.snapshot().affine.inverse() * dest.affine));
+ cr->rectangle(stores.snapshot().rect.left(), stores.snapshot().rect.top(), stores.snapshot().rect.width(), stores.snapshot().rect.height());
+ cr->set_source(snapshot_from, stores.snapshot().rect.left(), stores.snapshot().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->fill();
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * stores.snapshot().affine));
+ cr->translate(-1.0, -1.0);
+ region_to_path(cr, shrink_region(stores.store().drawn, 2));
+ cr->translate(1.0, 1.0);
+ cr->clip();
+ cr->set_source(store_from, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ };
+
+ copy(store.surface, snapshot.surface, fragment.surface, background_in_stores);
+ if (outlines_enabled) copy(store.outline_surface, snapshot.outline_surface, fragment.outline_surface, false);
+
+ snapshot = std::move(fragment);
+}
+
+Cairo::RefPtr<Cairo::ImageSurface> CairoGraphics::request_tile_surface(Geom::IntRect const &rect, bool /*nogl*/)
+{
+ // Create temporary surface, isolated from store.
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, rect.width() * scale_factor, rect.height() * scale_factor);
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+ return surface;
+}
+
+void CairoGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface)
+{
+ // Blit from the temporary surface to the store.
+ auto diff = fragment.rect.min() - stores.store().rect.min();
+
+ auto cr = Cairo::Context::create(store.surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(surface, diff.x(), diff.y());
+ cr->rectangle(diff.x(), diff.y(), fragment.rect.width(), fragment.rect.height());
+ cr->fill();
+
+ if (outlines_enabled) {
+ auto cr = Cairo::Context::create(store.outline_surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(outline_surface, diff.x(), diff.y());
+ cr->rectangle(diff.x(), diff.y(), fragment.rect.width(), fragment.rect.height());
+ cr->fill();
+ }
+}
+
+void CairoGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ auto f = FrameCheck::Event();
+
+ // Turn off anti-aliasing while compositing the widget for large performance gains. (We can usually
+ // get away with it without any negative visual impact; when we can't, we turn it back on.)
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+
+ // Due to a Cairo bug, Cairo sometimes draws outside of its clip region. This results in flickering as Canvas content is drawn
+ // over the bottom scrollbar. This cannot be fixed by setting the correct clip region, as Cairo detects that and turns it into
+ // a no-op. Hence the following workaround, which recreates the clip region from scratch, is required.
+ auto rlist = cairo_copy_clip_rectangle_list(cr->cobj());
+ cr->reset_clip();
+ for (int i = 0; i < rlist->num_rectangles; i++) {
+ cr->rectangle(rlist->rectangles[i].x, rlist->rectangles[i].y, rlist->rectangles[i].width, rlist->rectangles[i].height);
+ }
+ cr->clip();
+ cairo_rectangle_list_destroy(rlist);
+
+ // Draw background if solid colour optimisation is not enabled. (If enabled, it is baked into the stores.)
+ if (!background_in_stores) {
+ if (prefs.debug_framecheck) f = FrameCheck::Event("background");
+ paint_background(view, pi, page, desk, cr);
+ }
+
+ // Even if in solid colour mode, draw the part of background that is not going to be rendered.
+ if (background_in_stores) {
+ auto const &s = stores.mode() == Stores::Mode::Decoupled ? stores.snapshot() : stores.store();
+ if (!(Geom::Parallelogram(s.rect) * s.affine.inverse() * view.affine).contains(view.rect)) {
+ if (prefs.debug_framecheck) f = FrameCheck::Event("background", 2);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, view.rect.width(), view.rect.height());
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(s.affine.inverse() * view.affine));
+ cr->rectangle(s.rect.left(), s.rect.top(), s.rect.width(), s.rect.height());
+ cr->clip();
+ cr->transform(geom_to_cairo(view.affine.inverse() * s.affine));
+ cr->translate(view.rect.left(), view.rect.top());
+ paint_background(view, pi, page, desk, cr);
+ cr->restore();
+ }
+ }
+
+ auto draw_store = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &store, Cairo::RefPtr<Cairo::ImageSurface> const &snapshot_store) {
+ if (stores.mode() == Stores::Mode::Normal) {
+ // Blit store to view.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("draw");
+ cr->save();
+ auto const &r = stores.store().rect;
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine)); // Almost always the identity.
+ cr->rectangle(r.left(), r.top(), r.width(), r.height());
+ cr->set_source(store, r.left(), r.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->fill();
+ cr->restore();
+ } else {
+ // Draw transformed snapshot, clipped to the complement of the store's clean region.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("composite", 1);
+
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, view.rect.width(), view.rect.height());
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine));
+ region_to_path(cr, stores.store().drawn);
+ cr->transform(geom_to_cairo(stores.snapshot().affine.inverse() * stores.store().affine));
+ cr->clip();
+ auto const &r = stores.snapshot().rect;
+ cr->rectangle(r.left(), r.top(), r.width(), r.height());
+ cr->clip();
+ cr->set_source(snapshot_store, r.left(), r.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ if (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 drawn region.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("composite", 0);
+ cr->save();
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine));
+ cr->set_source(store, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ region_to_path(cr, stores.store().drawn);
+ cr->fill();
+ cr->restore();
+ }
+ };
+
+ auto draw_overlay = [&, this] {
+ // Get whitewash opacity.
+ double outline_overlay_opacity = prefs.outline_overlay_opacity / 100.0;
+
+ // Partially obscure drawing by painting semi-transparent white, then paint outline content.
+ // Note: Unfortunately this also paints over the background, but this is unavoidable.
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->paint_with_alpha(outline_overlay_opacity);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ cr->restore();
+ };
+
+ if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+
+ // Calculate the clipping rectangles for split view.
+ auto [store_clip, outline_clip] = calc_splitview_cliprects(view.rect.dimensions(), a.splitfrac, a.splitdir);
+
+ // Draw normal content.
+ cr->save();
+ cr->rectangle(store_clip.left(), store_clip.top(), store_clip.width(), store_clip.height());
+ cr->clip();
+ cr->set_operator(background_in_stores ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ draw_store(store.surface, snapshot.surface);
+ if (a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) draw_overlay();
+ cr->restore();
+
+ // Draw outline.
+ if (background_in_stores) {
+ cr->save();
+ cr->translate(outline_clip.left(), outline_clip.top());
+ paint_background(Fragment{view.affine, view.rect.min() + outline_clip}, pi, page, desk, cr);
+ cr->restore();
+ }
+ cr->save();
+ cr->rectangle(outline_clip.left(), outline_clip.top(), outline_clip.width(), outline_clip.height());
+ cr->clip();
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ cr->restore();
+
+ } else {
+
+ // Draw the normal content over the whole view.
+ cr->set_operator(background_in_stores ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ draw_store(store.surface, snapshot.surface);
+ if (a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) draw_overlay();
+
+ // Draw outline if in X-ray mode.
+ if (a.splitmode == Inkscape::SplitMode::XRAY && a.mouse) {
+ // Clip to circle
+ cr->set_antialias(Cairo::ANTIALIAS_DEFAULT);
+ cr->arc(a.mouse->x(), a.mouse->y(), prefs.xray_radius, 0, 2 * M_PI);
+ cr->clip();
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ // Draw background.
+ paint_background(view, pi, page, desk, cr);
+ // Draw outline.
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ }
+ }
+
+ // The rest can be done with antialiasing.
+ cr->set_antialias(Cairo::ANTIALIAS_DEFAULT);
+
+ if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+ paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr);
+ }
+}
+
+} // 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/canvas/cairographics.h b/src/ui/widget/canvas/cairographics.h
new file mode 100644
index 0000000..c29eff1
--- /dev/null
+++ b/src/ui/widget/canvas/cairographics.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Cairo display backend.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H
+
+#include "graphics.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+struct CairoFragment
+{
+ Cairo::RefPtr<Cairo::ImageSurface> surface;
+ Cairo::RefPtr<Cairo::ImageSurface> outline_surface;
+};
+
+class CairoGraphics : public Graphics
+{
+public:
+ CairoGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+
+ void set_scale_factor(int scale) override { scale_factor = scale; }
+ void set_outlines_enabled(bool) override;
+ void set_background_in_stores(bool enabled) override { background_in_stores = enabled; }
+ void set_colours(uint32_t p, uint32_t d, uint32_t b) override { page = p; desk = d; border = b; }
+
+ void recreate_store(Geom::IntPoint const &dimensions) override;
+ void shift_store(Fragment const &dest) override;
+ void swap_stores() override;
+ void fast_snapshot_combine() override;
+ void snapshot_combine(Fragment const &dest) override;
+ void invalidate_snapshot() override {}
+
+ bool is_opengl() const override { return false; }
+ void invalidated_glstate() override {}
+
+ Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) override;
+ void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) override;
+ void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) override {}
+
+ void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) override;
+
+private:
+ // Drawn content.
+ CairoFragment store, snapshot;
+
+ // Dependency objects in canvas.
+ Prefs const &prefs;
+ Stores const &stores;
+ PageInfo const &pi;
+
+ // Backend-agnostic state.
+ int scale_factor = 1;
+ bool outlines_enabled = false;
+ bool background_in_stores = false;
+ uint32_t page, desk, border;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_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/canvas/fragment.h b/src/ui/widget/canvas/fragment.h
new file mode 100644
index 0000000..d3edc74
--- /dev/null
+++ b/src/ui/widget/canvas/fragment.h
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H
+#define INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H
+
+#include <2geom/int-rect.h>
+#include <2geom/affine.h>
+
+namespace Inkscape::UI::Widget {
+
+/// A "fragment" is a rectangle of drawn content at a specfic place.
+struct Fragment
+{
+ // The matrix the geometry was transformed with when the content was drawn.
+ Geom::Affine affine;
+
+ // The rectangle of world space where the fragment was drawn.
+ Geom::IntRect rect;
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_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/canvas/framecheck.cpp b/src/ui/widget/canvas/framecheck.cpp
new file mode 100644
index 0000000..c127c8e
--- /dev/null
+++ b/src/ui/widget/canvas/framecheck.cpp
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <fstream>
+#include <iostream>
+#include <mutex>
+#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::FrameCheck {
+
+void Event::write()
+{
+ static std::mutex mutex;
+ static auto logfile = [] {
+ auto path = fs::temp_directory_path() / "framecheck.txt";
+ auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary;
+ return std::ofstream(path.string(), mode);
+ }();
+
+ auto lock = std::lock_guard(mutex);
+ logfile << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << std::endl;
+}
+
+} // namespace Inkscape::FrameCheck
diff --git a/src/ui/widget/canvas/framecheck.h b/src/ui/widget/canvas/framecheck.h
new file mode 100644
index 0000000..8964561
--- /dev/null
+++ b/src/ui/widget/canvas/framecheck.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_FRAMECHECK_H
+#define INKSCAPE_FRAMECHECK_H
+
+#include <glib.h>
+
+namespace Inkscape::FrameCheck {
+
+/// RAII object that logs a timing event for the duration of its lifetime.
+struct Event
+{
+ gint64 start;
+ char const *name;
+ int subtype;
+
+ Event() : start(-1) {}
+
+ Event(char const *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;
+ }
+
+private:
+ void movefrom(Event &p)
+ {
+ start = p.start;
+ name = p.name;
+ subtype = p.subtype;
+ p.start = -1;
+ }
+
+ void finish() { if (start != -1) write(); }
+
+ void write();
+};
+
+} // namespace Inkscape::FrameCheck
+
+#endif // INKSCAPE_FRAMECHECK_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/canvas/glgraphics.cpp b/src/ui/widget/canvas/glgraphics.cpp
new file mode 100644
index 0000000..b00503c
--- /dev/null
+++ b/src/ui/widget/canvas/glgraphics.cpp
@@ -0,0 +1,873 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/transforms.h>
+#include <2geom/rect.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "glgraphics.h"
+#include "stores.h"
+#include "prefs.h"
+#include "pixelstreamer.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+// 2Geom <-> OpenGL
+
+void geom_to_uniform_mat(Geom::Affine const &affine, GLuint location)
+{
+ glUniformMatrix2fv(location, 1, GL_FALSE, std::begin({(GLfloat)affine[0], (GLfloat)affine[1], (GLfloat)affine[2], (GLfloat)affine[3]}));
+}
+
+void geom_to_uniform_trans(Geom::Affine const &affine, GLuint location)
+{
+ glUniform2fv(location, 1, std::begin({(GLfloat)affine[4], (GLfloat)affine[5]}));
+}
+
+void geom_to_uniform(Geom::Affine const &affine, GLuint mat_location, GLuint trans_location)
+{
+ geom_to_uniform_mat(affine, mat_location);
+ geom_to_uniform_trans(affine, trans_location);
+}
+
+void geom_to_uniform(Geom::Point const &vec, GLuint location)
+{
+ glUniform2fv(location, 1, std::begin({(GLfloat)vec.x(), (GLfloat)vec.y()}));
+}
+
+// Get the affine transformation required to paste fragment A onto fragment B, assuming
+// coordinates such that A is a texture (0 to 1) and B is a framebuffer (-1 to 1).
+static auto calc_paste_transform(Fragment const &a, Fragment const &b)
+{
+ Geom::Affine result = Geom::Scale(a.rect.dimensions());
+
+ if (a.affine == b.affine) {
+ result *= Geom::Translate(a.rect.min() - b.rect.min());
+ } else {
+ result *= Geom::Translate(a.rect.min()) * a.affine.inverse() * b.affine * Geom::Translate(-b.rect.min());
+ }
+
+ return result * Geom::Scale(2.0 / b.rect.dimensions()) * Geom::Translate(-1.0, -1.0);
+}
+
+// Given a region, shrink it by 0.5px, and convert the result to a VAO of triangles.
+static auto region_shrink_vao(Cairo::RefPtr<Cairo::Region> const &reg, Geom::IntRect const &rel)
+{
+ // Shrink the region by 0.5 (translating it by (0.5, 0.5) in the process).
+ auto reg2 = shrink_region(reg, 1);
+
+ // Preallocate the vertex buffer.
+ int nrects = reg2->get_num_rectangles();
+ std::vector<GLfloat> verts;
+ verts.reserve(nrects * 12);
+
+ // Add a vertex to the buffer, transformed to a coordinate system in which the enclosing rectangle 'rel' goes from 0 to 1.
+ // Also shift them up/left by 0.5px; combined with the width/height increase from earlier, this shrinks the region by 0.5px.
+ auto emit_vertex = [&] (Geom::IntPoint const &pt) {
+ verts.emplace_back((pt.x() - 0.5f - rel.left()) / rel.width());
+ verts.emplace_back((pt.y() - 0.5f - rel.top() ) / rel.height());
+ };
+
+ // Todo: Use a better triangulation algorithm here that results in 1) less triangles, and 2) no seaming.
+ for (int i = 0; i < nrects; i++) {
+ auto rect = cairo_to_geom(reg2->get_rectangle(i));
+ for (int j = 0; j < 6; j++) {
+ int constexpr indices[] = {0, 1, 2, 0, 2, 3};
+ emit_vertex(rect.corner(indices[j]));
+ }
+ }
+
+ // Package the data in a VAO.
+ VAO result;
+ glGenBuffers(1, &result.vbuf);
+ glBindBuffer(GL_ARRAY_BUFFER, result.vbuf);
+ glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(GLfloat), verts.data(), GL_STREAM_DRAW);
+ glGenVertexArrays(1, &result.vao);
+ glBindVertexArray(result.vao);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, 0);
+
+ // Return the VAO and the number of rectangles.
+ return std::make_pair(std::move(result), nrects);
+}
+
+auto pref_to_pixelstreamer(int index)
+{
+ auto constexpr arr = std::array{PixelStreamer::Method::Auto,
+ PixelStreamer::Method::Persistent,
+ PixelStreamer::Method::Asynchronous,
+ PixelStreamer::Method::Synchronous};
+ assert(1 <= index && index <= arr.size());
+ return arr[index - 1];
+}
+
+} // namespace
+
+GLGraphics::GLGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+ : prefs(prefs)
+ , stores(stores)
+ , pi(pi)
+{
+ // Create rectangle geometry.
+ GLfloat constexpr verts[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
+ glGenBuffers(1, &rect.vbuf);
+ glBindBuffer(GL_ARRAY_BUFFER, rect.vbuf);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
+ glGenVertexArrays(1, &rect.vao);
+ glBindVertexArray(rect.vao);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, 0);
+
+ // Create shader programs.
+ auto vs = VShader(R"(
+ #version 330 core
+
+ uniform mat2 mat;
+ uniform vec2 trans;
+ uniform vec2 subrect;
+ layout(location = 0) in vec2 pos;
+ smooth out vec2 uv;
+
+ void main()
+ {
+ uv = pos * subrect;
+ vec2 pos2 = mat * pos + trans;
+ gl_Position = vec4(pos2.x, pos2.y, 0.0, 1.0);
+ }
+ )");
+
+ auto texcopy_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ outColour = texture(tex, uv);
+ }
+ )");
+
+ auto texcopydouble_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ smooth in vec2 uv;
+ layout(location = 0) out vec4 outColour;
+ layout(location = 1) out vec4 outColour_outline;
+
+ void main()
+ {
+ outColour = texture(tex, uv);
+ outColour_outline = texture(tex_outline, uv);
+ }
+ )");
+
+ auto outlineoverlay_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform float opacity;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+ vec4 c1w = vec4(mix(c1.rgb, vec3(1.0, 1.0, 1.0) * c1.a, opacity), c1.a);
+ outColour = c1w * (1.0 - c2.a) + c2;
+ }
+ )");
+
+ auto xray_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform vec2 pos;
+ uniform float radius;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+
+ float r = length(gl_FragCoord.xy - pos);
+ r = clamp((radius - r) / 2.0, 0.0, 1.0);
+
+ outColour = mix(c1, c2, r);
+ }
+ )");
+
+ auto outlineoverlayxray_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform float opacity;
+ uniform vec2 pos;
+ uniform float radius;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+ vec4 c1w = vec4(mix(c1.rgb, vec3(1.0, 1.0, 1.0) * c1.a, opacity), c1.a);
+ outColour = c1w * (1.0 - c2.a) + c2;
+
+ float r = length(gl_FragCoord.xy - pos);
+ r = clamp((radius - r) / 2.0, 0.0, 1.0);
+
+ outColour = mix(outColour, c2, r);
+ }
+ )");
+
+ auto checker_fs = FShader(R"(
+ #version 330 core
+
+ uniform float size;
+ uniform vec3 col1, col2;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec2 a = floor(fract(gl_FragCoord.xy / size) * 2.0);
+ float b = abs(a.x - a.y);
+ outColour = vec4((1.0 - b) * col1 + b * col2, 1.0);
+ }
+ )");
+
+ auto shadow_gs = GShader(R"(
+ #version 330 core
+
+ layout(triangles) in;
+ layout(triangle_strip, max_vertices = 10) out;
+
+ uniform vec2 wh;
+ uniform float size;
+ uniform vec2 dir;
+
+ smooth out vec2 uv;
+ flat out vec2 maxuv;
+
+ void f(vec4 p, vec4 v0, mat2 m)
+ {
+ gl_Position = p;
+ uv = m * (p.xy - v0.xy);
+ EmitVertex();
+ }
+
+ float push(float x)
+ {
+ return 0.15 * (1.0 + clamp(x / 0.707, -1.0, 1.0));
+ }
+
+ void main()
+ {
+ vec4 v0 = gl_in[0].gl_Position;
+ vec4 v1 = gl_in[1].gl_Position;
+ vec4 v2 = gl_in[2].gl_Position;
+ vec4 v3 = gl_in[2].gl_Position - gl_in[1].gl_Position + gl_in[0].gl_Position;
+
+ vec2 a = normalize((v1 - v0).xy * wh);
+ vec2 b = normalize((v3 - v0).xy * wh);
+ float det = a.x * b.y - a.y * b.x;
+ float s = -sign(det);
+ vec2 c = size / abs(det) / wh;
+ vec4 d = vec4(a * c, 0.0, 0.0);
+ vec4 e = vec4(b * c, 0.0, 0.0);
+ mat2 m = s * mat2(a.y, -b.y, -a.x, b.x) * mat2(wh.x, 0.0, 0.0, wh.y) / size;
+
+ float ap = s * dot(vec2(a.y, -a.x), dir);
+ float bp = s * dot(vec2(-b.y, b.x), dir);
+ v0.xy += (b * push( ap) + a * push( bp)) * size / wh;
+ v1.xy += (b * push( ap) + a * -push(-bp)) * size / wh;
+ v2.xy += (b * -push(-ap) + a * -push(-bp)) * size / wh;
+ v3.xy += (b * -push(-ap) + a * push( bp)) * size / wh;
+
+ maxuv = m * (v2.xy - v0.xy);
+ f(v0, v0, m);
+ f(v0 - d - e, v0, m);
+ f(v1, v0, m);
+ f(v1 + d - e, v0, m);
+ f(v2, v0, m);
+ f(v2 + d + e, v0, m);
+ f(v3, v0, m);
+ f(v3 - d + e, v0, m);
+ f(v0, v0, m);
+ f(v0 - d - e, v0, m);
+ EndPrimitive();
+ }
+ )");
+
+ auto shadow_fs = FShader(R"(
+ #version 330 core
+
+ uniform vec4 shadow_col;
+
+ smooth in vec2 uv;
+ flat in vec2 maxuv;
+
+ out vec4 outColour;
+
+ void main()
+ {
+ float x = max(uv.x - maxuv.x, 0.0) - max(-uv.x, 0.0);
+ float y = max(uv.y - maxuv.y, 0.0) - max(-uv.y, 0.0);
+ float s = min(length(vec2(x, y)), 1.0);
+
+ float A = 4.0; // This coefficient changes how steep the curve is and controls shadow drop-off.
+ s = (exp(A * (1.0 - s)) - 1.0) / (exp(A) - 1.0); // Exponential decay for drop shadow - long tail.
+
+ outColour = shadow_col * s;
+ }
+ )");
+
+ texcopy.create(vs, texcopy_fs);
+ texcopydouble.create(vs, texcopydouble_fs);
+ outlineoverlay.create(vs, outlineoverlay_fs);
+ xray.create(vs, xray_fs);
+ outlineoverlayxray.create(vs, outlineoverlayxray_fs);
+ checker.create(vs, checker_fs);
+ shadow.create(vs, shadow_gs, shadow_fs);
+
+ // Create the framebuffer object for rendering to off-view fragments.
+ glGenFramebuffers(1, &fbo);
+
+ // Create the texture cache.
+ texturecache = TextureCache::create();
+
+ // Create the PixelStreamer.
+ pixelstreamer = PixelStreamer::create_supported(pref_to_pixelstreamer(prefs.pixelstreamer_method));
+
+ // Set the last known state as unspecified, forcing a pipeline recreation whatever the next operation is.
+ state = State::None;
+}
+
+GLGraphics::~GLGraphics()
+{
+ glDeleteFramebuffers(1, &fbo);
+}
+
+std::unique_ptr<Graphics> Graphics::create_gl(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+{
+ return std::make_unique<GLGraphics>(prefs, stores, pi);
+}
+
+void GLGraphics::set_outlines_enabled(bool enabled)
+{
+ outlines_enabled = enabled;
+ if (!enabled) {
+ store.outline_texture.clear();
+ snapshot.outline_texture.clear();
+ }
+}
+
+void GLGraphics::setup_stores_pipeline()
+{
+ if (state == State::Stores) return;
+ state = State::Stores;
+
+ glDisable(GL_BLEND);
+
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+ GLuint constexpr attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
+ glDrawBuffers(outlines_enabled ? 2 : 1, attachments);
+
+ auto const &shader = outlines_enabled ? texcopydouble : texcopy;
+ glUseProgram(shader.id);
+ mat_loc = shader.loc("mat");
+ trans_loc = shader.loc("trans");
+ geom_to_uniform({1.0, 1.0}, shader.loc("subrect"));
+ tex_loc = shader.loc("tex");
+ if (outlines_enabled) texoutline_loc = shader.loc("tex_outline");
+}
+
+void GLGraphics::recreate_store(Geom::IntPoint const &dims)
+{
+ auto tex_size = dims * scale_factor;
+
+ // Setup the base pipeline.
+ setup_stores_pipeline();
+
+ // Recreate the store textures.
+ auto recreate = [&] (Texture &tex) {
+ if (tex && tex.size() == tex_size) {
+ tex.invalidate();
+ } else {
+ tex = Texture(tex_size);
+ }
+ };
+
+ recreate(store.texture);
+ if (outlines_enabled) {
+ recreate(store.outline_texture);
+ }
+
+ // Bind the store to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, store.outline_texture.id(), 0);
+ glViewport(0, 0, store.texture.size().x(), store.texture.size().y());
+
+ // Clear the store to transparent.
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+}
+
+void GLGraphics::shift_store(Fragment const &dest)
+{
+ auto tex_size = dest.rect.dimensions() * scale_factor;
+
+ // Setup the base pipeline.
+ setup_stores_pipeline();
+
+ // Create the new fragment.
+ auto create_or_reuse = [&] (Texture &tex, Texture &from) {
+ if (from && from.size() == tex_size) {
+ from.invalidate();
+ tex = std::move(from);
+ } else {
+ tex = Texture(tex_size);
+ }
+ };
+
+ GLFragment fragment;
+ create_or_reuse(fragment.texture, snapshot.texture);
+ if (outlines_enabled) {
+ create_or_reuse(fragment.outline_texture, snapshot.outline_texture);
+ }
+
+ // Bind new store to the framebuffer to writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fragment.texture .id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fragment.outline_texture.id(), 0);
+ glViewport(0, 0, fragment.texture.size().x(), fragment.texture.size().y());
+
+ // Clear new store to transparent.
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Bind the old store to texture units 0 and 1 for reading from.
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glUniform1i(texoutline_loc, 1);
+ }
+ glBindVertexArray(rect.vao);
+
+ // Copy re-usuable contents of the old store into the new store.
+ geom_to_uniform(calc_paste_transform(stores.store(), dest), mat_loc, trans_loc);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Set the result as the new store.
+ snapshot = std::move(store);
+ store = std::move(fragment);
+}
+
+void GLGraphics::swap_stores()
+{
+ std::swap(store, snapshot);
+}
+
+void GLGraphics::fast_snapshot_combine()
+{
+ // Ensure the base pipeline is correctly set up.
+ setup_stores_pipeline();
+
+ // Compute the vertex data for the drawn region.
+ auto [clean_vao, clean_numrects] = region_shrink_vao(stores.store().drawn, stores.store().rect);
+
+ // Bind the snapshot to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, snapshot.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, snapshot.outline_texture.id(), 0);
+ glViewport(0, 0, snapshot.texture.size().x(), snapshot.texture.size().y());
+
+ // Bind the store to texture unit 0 (and its outline to 1, if necessary).
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glUniform1i(texoutline_loc, 1);
+ }
+
+ // Copy the clean region of the store to the snapshot.
+ geom_to_uniform(calc_paste_transform(stores.store(), stores.snapshot()), mat_loc, trans_loc);
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+}
+
+void GLGraphics::snapshot_combine(Fragment const &dest)
+{
+ // Create the new fragment.
+ auto content_size = dest.rect.dimensions() * scale_factor;
+
+ // Ensure the base pipeline is correctly set up.
+ setup_stores_pipeline();
+
+ // Compute the vertex data for the clean region.
+ auto [clean_vao, clean_numrects] = region_shrink_vao(stores.store().drawn, stores.store().rect);
+
+ GLFragment fragment;
+ fragment.texture = Texture(content_size);
+ if (outlines_enabled) fragment.outline_texture = Texture(content_size);
+
+ // Bind the new fragment to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fragment.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fragment.outline_texture.id(), 0);
+
+ // Clear the new fragment to transparent.
+ glViewport(0, 0, fragment.texture.size().x(), fragment.texture.size().y());
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Bind the store and snapshot to texture units 0 and 1 (and their outlines to 2 and 3, if necessary).
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, snapshot.texture.id());
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, snapshot.outline_texture.id());
+ glActiveTexture(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ }
+
+ // Paste the snapshot store onto the new fragment.
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) glUniform1i(texoutline_loc, 2);
+ geom_to_uniform(calc_paste_transform(stores.snapshot(), dest), mat_loc, trans_loc);
+ glBindVertexArray(rect.vao);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Paste the backing store onto the new fragment.
+ glUniform1i(tex_loc, 1);
+ if (outlines_enabled) glUniform1i(texoutline_loc, 3);
+ geom_to_uniform(calc_paste_transform(stores.store(), dest), mat_loc, trans_loc);
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+
+ // Set the result as the new snapshot.
+ snapshot = std::move(fragment);
+}
+
+void GLGraphics::invalidate_snapshot()
+{
+ if (snapshot.texture) snapshot.texture.invalidate();
+ if (snapshot.outline_texture) snapshot.outline_texture.invalidate();
+}
+
+void GLGraphics::setup_tiles_pipeline()
+{
+ if (state == State::Tiles) return;
+ state = State::Tiles;
+
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+ GLuint constexpr attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
+ glDrawBuffers(outlines_enabled ? 2 : 1, attachments);
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, store.outline_texture.id(), 0);
+ glViewport(0, 0, store.texture.size().x(), store.texture.size().y());
+
+ auto const &shader = outlines_enabled ? texcopydouble : texcopy;
+ glUseProgram(shader.id);
+ mat_loc = shader.loc("mat");
+ trans_loc = shader.loc("trans");
+ subrect_loc = shader.loc("subrect");
+ glUniform1i(shader.loc("tex"), 0);
+ if (outlines_enabled) glUniform1i(shader.loc("tex_outline"), 1);
+
+ glBindVertexArray(rect.vao);
+ glDisable(GL_BLEND);
+};
+
+Cairo::RefPtr<Cairo::ImageSurface> GLGraphics::request_tile_surface(Geom::IntRect const &rect, bool nogl)
+{
+ Cairo::RefPtr<Cairo::ImageSurface> surface;
+
+ {
+ auto g = std::lock_guard(ps_mutex);
+ surface = pixelstreamer->request(rect.dimensions() * scale_factor, nogl);
+ }
+
+ if (surface) {
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+ }
+
+ return surface;
+}
+
+void GLGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface)
+{
+ auto g = std::lock_guard(ps_mutex);
+ auto surface_size = dimensions(surface);
+
+ Texture texture, outline_texture;
+
+ glActiveTexture(GL_TEXTURE0);
+ texture = texturecache->request(surface_size); // binds
+ pixelstreamer->finish(std::move(surface)); // uploads content
+
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ outline_texture = texturecache->request(surface_size);
+ pixelstreamer->finish(std::move(outline_surface));
+ }
+
+ setup_tiles_pipeline();
+
+ geom_to_uniform(calc_paste_transform(fragment, stores.store()), mat_loc, trans_loc);
+ geom_to_uniform(Geom::Point(surface_size) / texture.size(), subrect_loc);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ texturecache->finish(std::move(texture));
+ if (outlines_enabled) {
+ texturecache->finish(std::move(outline_texture));
+ }
+}
+
+void GLGraphics::junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface)
+{
+ auto g = std::lock_guard(ps_mutex);
+ pixelstreamer->finish(std::move(surface), true);
+}
+
+void GLGraphics::setup_widget_pipeline(Fragment const &view)
+{
+ state = State::Widget;
+
+ glDrawBuffer(GL_COLOR_ATTACHMENT0);
+ glViewport(0, 0, view.rect.width() * scale_factor, view.rect.height() * scale_factor);
+ glEnable(GL_STENCIL_TEST);
+ glStencilFunc(GL_NOTEQUAL, 1, 1);
+ glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, snapshot.texture.id());
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glActiveTexture(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_2D, snapshot.outline_texture.id());
+ }
+ glBindVertexArray(rect.vao);
+};
+
+void GLGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::RefPtr<Cairo::Context> const&)
+{
+ // If in decoupled mode, create the vertex data describing the drawn region of the store.
+ VAO clean_vao;
+ int clean_numrects;
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ std::tie(clean_vao, clean_numrects) = region_shrink_vao(stores.store().drawn, stores.store().rect);
+ }
+
+ setup_widget_pipeline(view);
+
+ // Clear the buffers. Since we have to pick a clear colour, we choose the page colour, enabling the single-page optimisation later.
+ glClearColor(SP_RGBA32_R_U(page) / 255.0f, SP_RGBA32_G_U(page) / 255.0f, SP_RGBA32_B_U(page) / 255.0f, 1.0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+ if (check_single_page(view, pi)) {
+ // A single page occupies the whole view.
+ if (SP_RGBA32_A_U(page) == 255) {
+ // Page is solid - nothing to do, since already cleared to this colour.
+ } else {
+ // Page is checkerboard - fill view with page pattern.
+ glDisable(GL_BLEND);
+ glUseProgram(checker.id);
+ glUniform1f(checker.loc("size"), 12.0 * scale_factor);
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(page)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(page)));
+ geom_to_uniform(Geom::Scale(2.0, -2.0) * Geom::Translate(-1.0, 1.0), checker.loc("mat"), checker.loc("trans"));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+ } else {
+ glDisable(GL_BLEND);
+
+ auto set_page_transform = [&] (Geom::Rect const &rect, Program const &prog) {
+ geom_to_uniform(Geom::Scale(rect.dimensions()) * Geom::Translate(rect.min()) * calc_paste_transform({{}, Geom::IntRect::from_xywh(0, 0, 1, 1)}, view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ };
+
+ // Pages
+ glUseProgram(checker.id);
+ glUniform1f(checker.loc("size"), 12.0 * scale_factor);
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(page)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(page)));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ for (auto &rect : pi.pages) {
+ set_page_transform(rect, checker);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+
+ glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+
+ // Desk
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(desk)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(desk)));
+ geom_to_uniform(Geom::Scale(2.0, -2.0) * Geom::Translate(-1.0, 1.0), checker.loc("mat"), checker.loc("trans"));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
+ // Shadows
+ if (SP_RGBA32_A_U(border) != 0) {
+ auto dir = (Geom::Point(1.0, a.yaxisdir) * view.affine * Geom::Scale(1.0, -1.0)).normalized(); // Shadow direction rotates with view.
+ glUseProgram(shadow.id);
+ geom_to_uniform({1.0, 1.0}, shadow.loc("subrect"));
+ glUniform2fv(shadow.loc("wh"), 1, std::begin({(GLfloat)view.rect.width(), (GLfloat)view.rect.height()}));
+ glUniform1f(shadow.loc("size"), 40.0 * std::pow(std::abs(view.affine.det()), 0.25));
+ glUniform2fv(shadow.loc("dir"), 1, std::begin({(GLfloat)dir.x(), (GLfloat)dir.y()}));
+ glUniform4fv(shadow.loc("shadow_col"), 1, std::begin(premultiplied(rgba_to_array(border))));
+ for (auto &rect : pi.pages) {
+ set_page_transform(rect, shadow);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ }
+ }
+
+ glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
+ }
+
+ glStencilFunc(GL_NOTEQUAL, 2, 2);
+
+ enum class DrawMode
+ {
+ Store,
+ Outline,
+ Combine
+ };
+
+ auto draw_store = [&, this] (Program const &prog, DrawMode drawmode) {
+ glUseProgram(prog.id);
+ geom_to_uniform({1.0, 1.0}, prog.loc("subrect"));
+ glUniform1i(prog.loc("tex"), drawmode == DrawMode::Outline ? 2 : 0);
+ if (drawmode == DrawMode::Combine) {
+ glUniform1i(prog.loc("tex_outline"), 2);
+ glUniform1f(prog.loc("opacity"), prefs.outline_overlay_opacity / 100.0);
+ }
+
+ if (stores.mode() == Stores::Mode::Normal) {
+ // Backing store fragment.
+ geom_to_uniform(calc_paste_transform(stores.store(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ } else {
+ // Backing store fragment, clipped to its clean region.
+ geom_to_uniform(calc_paste_transform(stores.store(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+
+ // Snapshot fragment.
+ glUniform1i(prog.loc("tex"), drawmode == DrawMode::Outline ? 3 : 1);
+ if (drawmode == DrawMode::Combine) glUniform1i(prog.loc("tex_outline"), 3);
+ geom_to_uniform(calc_paste_transform(stores.snapshot(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glBindVertexArray(rect.vao);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+ };
+
+ if (a.splitmode == Inkscape::SplitMode::NORMAL || (a.splitmode == Inkscape::SplitMode::XRAY && !a.mouse)) {
+
+ // Drawing the backing store over the whole view.
+ a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY
+ ? draw_store(outlineoverlay, DrawMode::Combine)
+ : draw_store(texcopy, DrawMode::Store);
+
+ } else if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+
+ // Calculate the clipping rectangles for split view.
+ auto [store_clip, outline_clip] = calc_splitview_cliprects(view.rect.dimensions(), a.splitfrac, a.splitdir);
+
+ glEnable(GL_SCISSOR_TEST);
+
+ // Draw the backing store.
+ glScissor(store_clip.left() * scale_factor, (view.rect.height() - store_clip.bottom()) * scale_factor, store_clip.width() * scale_factor, store_clip.height() * scale_factor);
+ a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY
+ ? draw_store(outlineoverlay, DrawMode::Combine)
+ : draw_store(texcopy, DrawMode::Store);
+
+ // Draw the outline store.
+ glScissor(outline_clip.left() * scale_factor, (view.rect.height() - outline_clip.bottom()) * scale_factor, outline_clip.width() * scale_factor, outline_clip.height() * scale_factor);
+ draw_store(texcopy, DrawMode::Outline);
+
+ glDisable(GL_SCISSOR_TEST);
+ glDisable(GL_STENCIL_TEST);
+
+ // Calculate the bounding rectangle of the split view controller.
+ auto rect = Geom::IntRect({0, 0}, view.rect.dimensions());
+ auto dim = a.splitdir == Inkscape::SplitDirection::EAST || a.splitdir == Inkscape::SplitDirection::WEST ? Geom::X : Geom::Y;
+ rect[dim] = Geom::IntInterval(-21, 21) + std::round(a.splitfrac[dim] * view.rect.dimensions()[dim]);
+
+ // Lease out a PixelStreamer mapping to draw on.
+ auto surface_size = rect.dimensions() * scale_factor;
+ auto surface = pixelstreamer->request(surface_size);
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+
+ // Actually draw the content with Cairo.
+ auto cr = Cairo::Context::create(surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source_rgba(0.0, 0.0, 0.0, 0.0);
+ cr->paint();
+ cr->translate(-rect.left(), -rect.top());
+ paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr);
+
+ // Convert the surface to a texture.
+ glActiveTexture(GL_TEXTURE0);
+ auto texture = texturecache->request(surface_size);
+ pixelstreamer->finish(std::move(surface));
+
+ // Paint the texture onto the view.
+ glUseProgram(texcopy.id);
+ glUniform1i(texcopy.loc("tex"), 0);
+ geom_to_uniform(Geom::Scale(rect.dimensions()) * Geom::Translate(rect.min()) * Geom::Scale(2.0 / view.rect.width(), -2.0 / view.rect.height()) * Geom::Translate(-1.0, 1.0), texcopy.loc("mat"), texcopy.loc("trans"));
+ geom_to_uniform(Geom::Point(surface_size) / texture.size(), texcopy.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Return the texture back to the texture cache.
+ texturecache->finish(std::move(texture));
+
+ } else { // if (_split_mode == Inkscape::SplitMode::XRAY && a.mouse)
+
+ // Draw the backing store over the whole view.
+ auto const &shader = a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY ? outlineoverlayxray : xray;
+ glUseProgram(shader.id);
+ glUniform1f(shader.loc("radius"), prefs.xray_radius * scale_factor);
+ glUniform2fv(shader.loc("pos"), 1, std::begin({(GLfloat)(a.mouse->x() * scale_factor), (GLfloat)((view.rect.height() - a.mouse->y()) * scale_factor)}));
+ draw_store(shader, DrawMode::Combine);
+ }
+}
+
+} // 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/canvas/glgraphics.h b/src/ui/widget/canvas/glgraphics.h
new file mode 100644
index 0000000..7cb6ecf
--- /dev/null
+++ b/src/ui/widget/canvas/glgraphics.h
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * OpenGL display backend.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H
+
+#include <mutex>
+#include <epoxy/gl.h>
+#include "graphics.h"
+#include "texturecache.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class Stores;
+class Prefs;
+class PixelStreamer;
+
+template <GLuint type>
+struct Shader : boost::noncopyable
+{
+ GLuint id;
+ Shader(char const *src) { id = glCreateShader(type); glShaderSource(id, 1, &src, nullptr); glCompileShader(id); }
+ ~Shader() { glDeleteShader(id); }
+};
+using GShader = Shader<GL_GEOMETRY_SHADER>;
+using VShader = Shader<GL_VERTEX_SHADER>;
+using FShader = Shader<GL_FRAGMENT_SHADER>;
+
+struct Program : boost::noncopyable
+{
+ GLuint id = 0;
+ void create(VShader const &v, FShader const &f) { id = glCreateProgram(); glAttachShader(id, v.id); glAttachShader(id, f.id); glLinkProgram(id); }
+ void create(VShader const &v, const GShader &g, FShader const &f) { id = glCreateProgram(); glAttachShader(id, v.id); glAttachShader(id, g.id); glAttachShader(id, f.id); glLinkProgram(id); }
+ auto loc(char const *str) const { return glGetUniformLocation(id, str); }
+ ~Program() { glDeleteProgram(id); }
+};
+
+class VAO
+{
+public:
+ GLuint vao = 0;
+ GLuint vbuf;
+
+ VAO() = default;
+ VAO(GLuint vao, GLuint vbuf) : vao(vao), vbuf(vbuf) {}
+ VAO(VAO &&other) noexcept { movefrom(other); }
+ VAO &operator=(VAO &&other) noexcept { reset(); movefrom(other); return *this; }
+ ~VAO() { reset(); }
+
+private:
+ void reset() noexcept { if (vao) { glDeleteVertexArrays(1, &vao); glDeleteBuffers(1, &vbuf); } }
+ void movefrom(VAO &other) noexcept { vao = other.vao; vbuf = other.vbuf; other.vao = 0; }
+};
+
+struct GLFragment
+{
+ Texture texture;
+ Texture outline_texture;
+};
+
+class GLGraphics : public Graphics
+{
+public:
+ GLGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ ~GLGraphics() override;
+
+ void set_scale_factor(int scale) override { scale_factor = scale; }
+ void set_outlines_enabled(bool) override;
+ void set_background_in_stores(bool enabled) override { background_in_stores = enabled; }
+ void set_colours(uint32_t p, uint32_t d, uint32_t b) override { page = p; desk = d; border = b; }
+
+ void recreate_store(Geom::IntPoint const &dimensions) override;
+ void shift_store(Fragment const &dest) override;
+ void swap_stores() override;
+ void fast_snapshot_combine() override;
+ void snapshot_combine(Fragment const &dest) override;
+ void invalidate_snapshot() override;
+
+ bool is_opengl() const override { return true; }
+ void invalidated_glstate() override { state = State::None; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) override;
+ void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) override;
+ void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) override;
+
+ void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) override;
+
+private:
+ // Drawn content.
+ GLFragment store, snapshot;
+
+ // OpenGL objects.
+ VAO rect; // Rectangle vertex data.
+ Program checker, shadow, texcopy, texcopydouble, outlineoverlay, xray, outlineoverlayxray; // Shaders
+ GLuint fbo; // Framebuffer object for rendering to stores.
+
+ // Pixel streamer and texture cache for uploading pixel data to GPU.
+ std::unique_ptr<PixelStreamer> pixelstreamer;
+ std::unique_ptr<TextureCache> texturecache;
+ std::mutex ps_mutex;
+
+ // For preventing unnecessary pipeline recreation.
+ enum class State { None, Widget, Stores, Tiles };
+ State state;
+ void setup_stores_pipeline();
+ void setup_tiles_pipeline();
+ void setup_widget_pipeline(Fragment const &view);
+
+ // For caching frequently-used uniforms.
+ GLuint mat_loc, trans_loc, subrect_loc, tex_loc, texoutline_loc;
+
+ // Dependency objects in canvas.
+ Prefs const &prefs;
+ Stores const &stores;
+ PageInfo const &pi;
+
+ // Backend-agnostic state.
+ int scale_factor = 1;
+ bool outlines_enabled = false;
+ bool background_in_stores = false;
+ uint32_t page, desk, border;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_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/canvas/graphics.cpp b/src/ui/widget/canvas/graphics.cpp
new file mode 100644
index 0000000..28972e2
--- /dev/null
+++ b/src/ui/widget/canvas/graphics.cpp
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/parallelogram.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "graphics.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+// Convert an rgba into a pattern, turning transparency into checkerboard-ness.
+Cairo::RefPtr<Cairo::Pattern> rgba_to_pattern(uint32_t rgba)
+{
+ if (SP_RGBA32_A_U(rgba) == 255) {
+ return Cairo::SolidPattern::create_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+ } else {
+ int constexpr w = 6;
+ int constexpr h = 6;
+
+ auto dark = checkerboard_darken(rgba);
+
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 2 * w, 2 * h);
+
+ auto cr = Cairo::Context::create(surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+ cr->paint();
+ cr->set_source_rgb(dark[0], dark[1], dark[2]);
+ cr->rectangle(0, 0, w, h);
+ cr->rectangle(w, h, w, h);
+ cr->fill();
+
+ auto pattern = Cairo::SurfacePattern::create(surface);
+ pattern->set_extend(Cairo::EXTEND_REPEAT);
+ pattern->set_filter(Cairo::FILTER_NEAREST);
+
+ return pattern;
+ }
+}
+
+} // namespace
+
+// Paint the background and pages using Cairo into the given fragment.
+void Graphics::paint_background(Fragment const &fragment, PageInfo const &pi, uint32_t page, uint32_t desk, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->rectangle(0, 0, fragment.rect.width(), fragment.rect.height());
+ cr->clip();
+
+ if (desk == page || check_single_page(fragment, pi)) {
+ // Desk and page are the same, or a single page fills the whole screen; just clear the fragment to page.
+ cr->set_source(rgba_to_pattern(page));
+ cr->paint();
+ } else {
+ // Paint the background to the complement of the pages. (Slightly overpaints when pages overlap.)
+ cr->save();
+ cr->set_source(rgba_to_pattern(desk));
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, fragment.rect.width(), fragment.rect.height());
+ cr->translate(-fragment.rect.left(), -fragment.rect.top());
+ cr->transform(geom_to_cairo(fragment.affine));
+ for (auto &rect : pi.pages) {
+ cr->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ }
+ cr->fill();
+ cr->restore();
+
+ // Paint the pages.
+ cr->save();
+ cr->set_source(rgba_to_pattern(page));
+ cr->translate(-fragment.rect.left(), -fragment.rect.top());
+ cr->transform(geom_to_cairo(fragment.affine));
+ for (auto &rect : pi.pages) {
+ cr->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ }
+ cr->fill();
+ cr->restore();
+ }
+
+ cr->restore();
+}
+
+std::pair<Geom::IntRect, Geom::IntRect> Graphics::calc_splitview_cliprects(Geom::IntPoint const &size, Geom::Point const &split_frac, SplitDirection split_direction)
+{
+ auto window = Geom::IntRect({0, 0}, size);
+
+ auto content = window;
+ auto outline = window;
+ auto split = [&] (Geom::Dim2 dim, Geom::IntRect &lo, Geom::IntRect &hi) {
+ int s = std::round(split_frac[dim] * size[dim]);
+ lo[dim].setMax(s);
+ hi[dim].setMin(s);
+ };
+
+ switch (split_direction) {
+ case Inkscape::SplitDirection::NORTH: split(Geom::Y, content, outline); break;
+ case Inkscape::SplitDirection::EAST: split(Geom::X, outline, content); break;
+ case Inkscape::SplitDirection::SOUTH: split(Geom::Y, outline, content); break;
+ case Inkscape::SplitDirection::WEST: split(Geom::X, content, outline); break;
+ default: assert(false); break;
+ }
+
+ return std::make_pair(content, outline);
+}
+
+void Graphics::paint_splitview_controller(Geom::IntPoint const &size, Geom::Point const &split_frac, SplitDirection split_direction, SplitDirection hover_direction, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ auto split_position = (split_frac * size).round();
+
+ // Add dividing line.
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->set_line_width(1.0);
+ if (split_direction == Inkscape::SplitDirection::EAST ||
+ split_direction == Inkscape::SplitDirection::WEST) {
+ cr->move_to(split_position.x() + 0.5, 0.0 );
+ cr->line_to(split_position.x() + 0.5, size.y());
+ cr->stroke();
+ } else {
+ cr->move_to(0.0 , split_position.y() + 0.5);
+ cr->line_to(size.x(), split_position.y() + 0.5);
+ cr->stroke();
+ }
+
+ // Add controller image.
+ double a = hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0;
+ cr->set_source_rgba(0.2, 0.2, 0.2, a);
+ cr->arc(split_position.x(), split_position.y(), 20, 0, 2 * M_PI);
+ cr->fill();
+
+ 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);
+
+ // Draw triangle.
+ cr->move_to(-5, 8);
+ cr->line_to( 0, 18);
+ cr->line_to( 5, 8);
+ 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();
+ }
+}
+
+bool Graphics::check_single_page(Fragment const &view, PageInfo const &pi)
+{
+ auto pl = Geom::Parallelogram(view.rect) * view.affine.inverse();
+ return std::any_of(pi.pages.begin(), pi.pages.end(), [&] (auto &rect) {
+ return Geom::Parallelogram(rect).contains(pl);
+ });
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/canvas/graphics.h b/src/ui/widget/canvas/graphics.h
new file mode 100644
index 0000000..0e7767d
--- /dev/null
+++ b/src/ui/widget/canvas/graphics.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Display backend interface.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
+
+#include <memory>
+#include <cstdint>
+#include <boost/noncopyable.hpp>
+#include <2geom/rect.h>
+#include <cairomm/cairomm.h>
+#include "display/rendermode.h"
+#include "fragment.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class Stores;
+class Prefs;
+
+struct PageInfo
+{
+ std::vector<Geom::Rect> pages;
+};
+
+class Graphics
+{
+public:
+ // Creation/destruction.
+ static std::unique_ptr<Graphics> create_gl (Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ static std::unique_ptr<Graphics> create_cairo(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ virtual ~Graphics() = default;
+
+ // State updating.
+ virtual void set_scale_factor(int) = 0; ///< Set the HiDPI scale factor.
+ virtual void set_outlines_enabled(bool) = 0; ///< Whether to maintain a second layer of outline content.
+ virtual void set_background_in_stores(bool) = 0; ///< Whether to assume the first layer is drawn on top of background or transparency.
+ virtual void set_colours(uint32_t page, uint32_t desk, uint32_t border) = 0; ///< Set colours for background/page shadow drawing.
+
+ // Store manipulation.
+ virtual void recreate_store(Geom::IntPoint const &dims) = 0; ///< Set the store to a surface of the given size, of unspecified contents.
+ virtual void shift_store(Fragment const &dest) = 0; ///< Called when the store fragment shifts position to \a dest.
+ virtual void swap_stores() = 0; ///< Exchange the store and snapshot surfaces.
+ virtual void fast_snapshot_combine() = 0; ///< Paste the store onto the snapshot.
+ virtual void snapshot_combine(Fragment const &dest) = 0; ///< Paste the snapshot followed by the store onto a new snapshot at \a dest.
+ virtual void invalidate_snapshot() = 0; ///< Indicate that the content in the snapshot store is not going to be used again.
+
+ // Misc.
+ virtual bool is_opengl() const = 0; ///< Whether this is an OpenGL backend.
+ virtual void invalidated_glstate() = 0; ///< Tells the Graphics to no longer rely on any OpenGL state it had set up.
+
+ // Tile drawing.
+ /// Return a surface for drawing on. If nogl is true, no GL commands are issued, as is a requirement off-main-thread. All such surfaces must be
+ /// returned by passing them either to draw_tile() or junk_tile_surface().
+ virtual Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) = 0;
+ /// Commit the contents of a surface previously issued by request_tile_surface() to the canvas. In outline mode, a second surface must be passed
+ /// containing the outline content, otherwise it should be null.
+ virtual void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) = 0;
+ /// Get rid of a surface previously issued by request_tile_surface() without committing it to the canvas. Usually useful only to dispose of
+ /// surfaces which have gone into an error state while rendering, which is irreversible, and therefore we can't do anything useful with them.
+ virtual void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) = 0;
+
+ // Widget painting.
+ struct PaintArgs
+ {
+ std::optional<Geom::IntPoint> mouse;
+ RenderMode render_mode;
+ SplitMode splitmode;
+ Geom::Point splitfrac;
+ SplitDirection splitdir;
+ SplitDirection hoverdir;
+ double yaxisdir;
+ };
+ virtual void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) = 0;
+
+ // Static functions providing common functionality.
+ static bool check_single_page(Fragment const &view, PageInfo const &pi);
+ static std::pair<Geom::IntRect, Geom::IntRect> calc_splitview_cliprects(Geom::IntPoint const &size, Geom::Point const &splitfrac, SplitDirection splitdir);
+ static void paint_splitview_controller(Geom::IntPoint const &size, Geom::Point const &splitfrac, SplitDirection splitdir, SplitDirection hoverdir, Cairo::RefPtr<Cairo::Context> const &cr);
+ static void paint_background(Fragment const &fragment, PageInfo const &pi, uint32_t page, uint32_t desk, Cairo::RefPtr<Cairo::Context> const &cr);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
diff --git a/src/ui/widget/canvas/pixelstreamer.cpp b/src/ui/widget/canvas/pixelstreamer.cpp
new file mode 100644
index 0000000..74d557b
--- /dev/null
+++ b/src/ui/widget/canvas/pixelstreamer.cpp
@@ -0,0 +1,501 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <cassert>
+#include <cmath>
+#include <vector>
+#include <epoxy/gl.h>
+#include "pixelstreamer.h"
+#include "helper/mathfns.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+namespace {
+
+cairo_user_data_key_t constexpr key{};
+
+class PersistentPixelStreamer : public PixelStreamer
+{
+ static int constexpr bufsize = 0x1000000; // 16 MiB
+
+ struct Buffer
+ {
+ GLuint pbo; // Pixel buffer object.
+ unsigned char *data; // The pointer to the mapped region.
+ int off; // Offset of the unused region, in bytes. Always a multiple of 64.
+ int refs; // How many mappings are currently using this buffer.
+ GLsync sync; // Sync object for telling us when the GPU has finished reading from this buffer.
+ bool ready; // Whether this buffer is ready for re-use.
+
+ void create()
+ {
+ glGenBuffers(1, &pbo);
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glBufferStorage(GL_PIXEL_UNPACK_BUFFER, bufsize, nullptr, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT);
+ data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, bufsize, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_FLUSH_EXPLICIT_BIT);
+ off = 0;
+ refs = 0;
+ }
+
+ void destroy()
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ glDeleteBuffers(1, &pbo);
+ }
+
+ // Advance a buffer in state 3 or 4 as far as possible towards state 5.
+ void advance()
+ {
+ if (!sync) {
+ sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ } else {
+ auto ret = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
+ if (ret == GL_CONDITION_SATISFIED || ret == GL_ALREADY_SIGNALED) {
+ glDeleteSync(sync);
+ ready = true;
+ }
+ }
+ }
+ };
+ std::vector<Buffer> buffers;
+
+ int current_buffer;
+
+ struct Mapping
+ {
+ bool used; // Whether the mapping is in use, or on the freelist.
+ int buf; // The buffer the mapping is using.
+ int off; // Offset of the mapped region.
+ int size; // Size of the mapped region.
+ int width, height, stride; // Image properties.
+ };
+ std::vector<Mapping> mappings;
+
+ /*
+ * A Buffer cycles through the following five states:
+ *
+ * 1. Current --> We are currently filling this buffer up with allocations.
+ * 2. Not current, refs > 0 --> Finished the above, but may still be writing into it and issuing GL commands from it.
+ * 3. Not current, refs == 0, !ready, !sync --> Finished the above, but GL may be reading from it. We have yet to create its sync object.
+ * 4. Not current, refs == 0, !ready, sync --> We have now created its sync object, but it has not been signalled yet.
+ * 5. Not current, refs == 0, ready --> The sync object has been signalled and deleted.
+ *
+ * Only one Buffer is Current at any given time, and is marked by the current_buffer variable.
+ */
+
+public:
+ PersistentPixelStreamer()
+ {
+ // Create a single initial buffer and make it the current buffer.
+ buffers.emplace_back();
+ buffers.back().create();
+ current_buffer = 0;
+ }
+
+ Method get_method() const override { return Method::Persistent; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override
+ {
+ // Calculate image properties required by cairo.
+ int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x());
+ int size = stride * dimensions.y();
+ int sizeup = Util::roundup(size, 64);
+ assert(sizeup < bufsize);
+
+ // Attempt to advance buffers in states 3 or 4 towards 5, if allowed.
+ if (!nogl) {
+ for (int i = 0; i < buffers.size(); i++) {
+ if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready) {
+ buffers[i].advance();
+ }
+ }
+ }
+ // Continue using the current buffer if possible.
+ if (buffers[current_buffer].off + sizeup <= bufsize) {
+ goto chosen_buffer;
+ }
+ // Otherwise, the current buffer has filled up. After this point, the current buffer will change.
+ // Therefore, handle the state change of the current buffer out of the Current state. Usually that
+ // means doing nothing because the transition to state 2 is automatic. But if refs == 0 already,
+ // then we need to transition into state 3 by setting ready = false. If we're allowed to use GL,
+ // then we can additionally transition into state 4 by creating the sync object.
+ if (buffers[current_buffer].refs == 0) {
+ buffers[current_buffer].ready = false;
+ buffers[current_buffer].sync = nogl ? nullptr : glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ }
+ // Attempt to re-use a old buffer that has reached state 5.
+ for (int i = 0; i < buffers.size(); i++) {
+ if (i != current_buffer && buffers[i].refs == 0 && buffers[i].ready) {
+ // Found an unused buffer. Re-use it. (Move to state 1.)
+ buffers[i].off = 0;
+ current_buffer = i;
+ goto chosen_buffer;
+ }
+ }
+ // Otherwise, there are no available buffers. Create and use a new one. That requires GL, so fail if not allowed.
+ if (nogl) {
+ return {};
+ }
+ buffers.emplace_back();
+ buffers.back().create();
+ current_buffer = buffers.size() - 1;
+ chosen_buffer:
+ // Finished changing the current buffer.
+ auto &b = buffers[current_buffer];
+
+ // Choose/create the mapping to use.
+ auto choose_mapping = [&, this] {
+ for (int i = 0; i < mappings.size(); i++) {
+ if (!mappings[i].used) {
+ // Found unused mapping.
+ return i;
+ }
+ }
+ // No free mapping; create one.
+ mappings.emplace_back();
+ return (int)mappings.size() - 1;
+ };
+
+ auto mapping = choose_mapping();
+ auto &m = mappings[mapping];
+
+ // Set up the mapping bookkeeping.
+ m = {true, current_buffer, b.off, size, dimensions.x(), dimensions.y(), stride};
+ b.off += sizeup;
+ b.refs++;
+
+ // Create the image surface.
+ auto surface = Cairo::ImageSurface::create(b.data + m.off, Cairo::FORMAT_ARGB32, dimensions.x(), dimensions.y(), stride);
+
+ // Attach the mapping handle as user data.
+ cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
+
+ return surface;
+ }
+
+ void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
+ {
+ // Extract the mapping handle from the surface's user data.
+ auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
+
+ // Flush all changes from the image surface to the buffer, and delete it.
+ surface.clear();
+
+ auto &m = mappings[mapping];
+ auto &b = buffers[m.buf];
+
+ // Flush the mapped subregion.
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, b.pbo);
+ glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, m.off, m.size);
+
+ // Tear down the mapping bookkeeping. (if this causes transition 2 --> 3, it is handled below.)
+ m.used = false;
+ b.refs--;
+
+ // Upload to the texture from the mapped subregion.
+ if (!junk) {
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, (void*)(uintptr_t)m.off);
+ }
+
+ // If the buffer is due for recycling, issue a sync command so that we can recycle it when it's ready. (Handle transition 2 --> 4.)
+ if (m.buf != current_buffer && b.refs == 0) {
+ b.ready = false;
+ b.sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ }
+
+ // Check other buffers to see if they're ready for recycling. (Advance from 3/4 towards 5.)
+ for (int i = 0; i < buffers.size(); i++) {
+ if (i != current_buffer && i != m.buf && buffers[i].refs == 0 && !buffers[i].ready) {
+ buffers[i].advance();
+ }
+ }
+ }
+
+ ~PersistentPixelStreamer() override
+ {
+ // Delete any sync objects. (For buffers in state 4.)
+ for (int i = 0; i < buffers.size(); i++) {
+ if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready && buffers[i].sync) {
+ glDeleteSync(buffers[i].sync);
+ }
+ }
+
+ // Wait for GL to finish reading out of all the buffers.
+ glFinish();
+
+ // Deallocate the buffers on the GL side.
+ for (auto &b : buffers) {
+ b.destroy();
+ }
+ }
+};
+
+class AsynchronousPixelStreamer : public PixelStreamer
+{
+ static int constexpr minbufsize = 0x4000; // 16 KiB
+ static int constexpr expire_timeout = 10000;
+
+ static int constexpr size_to_bucket(int size) { return Util::floorlog2((size - 1) / minbufsize) + 1; }
+ static int constexpr bucket_maxsize(int b) { return minbufsize * (1 << b); }
+
+ struct Buffer
+ {
+ GLuint pbo;
+ unsigned char *data;
+
+ void create(int size)
+ {
+ glGenBuffers(1, &pbo);
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW);
+ data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
+ }
+
+ void destroy()
+ {
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+ glDeleteBuffers(1, &pbo);
+ }
+ };
+
+ struct Bucket
+ {
+ std::vector<Buffer> spares;
+ int used = 0;
+ int high_use_count = 0;
+ };
+ std::vector<Bucket> buckets;
+
+ struct Mapping
+ {
+ bool used;
+ Buffer buf;
+ int bucket;
+ int width, height, stride;
+ };
+ std::vector<Mapping> mappings;
+
+ int expire_timer = 0;
+
+public:
+ Method get_method() const override { return Method::Asynchronous; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override
+ {
+ // Calculate image properties required by cairo.
+ int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x());
+ int size = stride * dimensions.y();
+
+ // Find the bucket that size falls into.
+ int bucket = size_to_bucket(size);
+ if (bucket >= buckets.size()) {
+ buckets.resize(bucket + 1);
+ }
+ auto &b = buckets[bucket];
+
+ // Find/create a buffer of the appropriate size.
+ Buffer buf;
+ if (!b.spares.empty()) {
+ // If the bucket has any spare mapped buffers, then use one of them.
+ buf = std::move(b.spares.back());
+ b.spares.pop_back();
+ } else if (!nogl) {
+ // Otherwise, we have to use OpenGL to create and map a new buffer.
+ buf.create(bucket_maxsize(bucket));
+ } else {
+ // If we're not allowed to issue GL commands, then that is a failure.
+ return {};
+ }
+
+ // Record the new use count of the bucket.
+ b.used++;
+ if (b.used > b.high_use_count) {
+ // If the use count has gone above the high-water mark, record it and reset the timer for when to clean up excess spares.
+ b.high_use_count = b.used;
+ expire_timer = 0;
+ }
+
+ auto choose_mapping = [&, this] {
+ for (int i = 0; i < mappings.size(); i++) {
+ if (!mappings[i].used) {
+ return i;
+ }
+ }
+ mappings.emplace_back();
+ return (int)mappings.size() - 1;
+ };
+
+ auto mapping = choose_mapping();
+ auto &m = mappings[mapping];
+
+ m.used = true;
+ m.buf = std::move(buf);
+ m.bucket = bucket;
+ m.width = dimensions.x();
+ m.height = dimensions.y();
+ m.stride = stride;
+
+ auto surface = Cairo::ImageSurface::create(m.buf.data, Cairo::FORMAT_ARGB32, m.width, m.height, m.stride);
+ cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
+ return surface;
+ }
+
+ void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
+ {
+ auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
+ surface.clear();
+
+ auto &m = mappings[mapping];
+ auto &b = buckets[m.bucket];
+
+ // Unmap the buffer.
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m.buf.pbo);
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
+
+ // Upload the buffer to the texture.
+ if (!junk) {
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
+ }
+
+ // Mark the mapping slot as unused.
+ m.used = false;
+
+ // Orphan and re-map the buffer.
+ auto size = bucket_maxsize(m.bucket);
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW);
+ m.buf.data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
+
+ // Put the buffer back in its corresponding bucket's pile of spares.
+ b.spares.emplace_back(std::move(m.buf));
+ b.used--;
+
+ // If the expiration timeout has been reached, get rid of excess spares from all buckets, and reset the high use counts.
+ expire_timer++;
+ if (expire_timer >= expire_timeout) {
+ expire_timer = 0;
+
+ for (auto &b : buckets) {
+ int max_spares = b.high_use_count - b.used;
+ assert(max_spares >= 0);
+ if (b.spares.size() > max_spares) {
+ for (int i = max_spares; i < b.spares.size(); i++) {
+ b.spares[i].destroy();
+ }
+ b.spares.resize(max_spares);
+ }
+ b.high_use_count = b.used;
+ }
+ }
+ }
+
+ ~AsynchronousPixelStreamer() override
+ {
+ // Unmap and delete all spare buffers. (They are not being used.)
+ for (auto &b : buckets) {
+ for (auto &buf : b.spares) {
+ buf.destroy();
+ }
+ }
+ }
+};
+
+class SynchronousPixelStreamer : public PixelStreamer
+{
+ struct Mapping
+ {
+ bool used;
+ std::vector<unsigned char> data;
+ int size, width, height, stride;
+ };
+ std::vector<Mapping> mappings;
+
+public:
+ Method get_method() const override { return Method::Synchronous; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool) override
+ {
+ auto choose_mapping = [&, this] {
+ for (int i = 0; i < mappings.size(); i++) {
+ if (!mappings[i].used) {
+ return i;
+ }
+ }
+ mappings.emplace_back();
+ return (int)mappings.size() - 1;
+ };
+
+ auto mapping = choose_mapping();
+ auto &m = mappings[mapping];
+
+ m.used = true;
+ m.width = dimensions.x();
+ m.height = dimensions.y();
+ m.stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, m.width);
+ m.size = m.stride * m.height;
+ m.data.resize(m.size);
+
+ auto surface = Cairo::ImageSurface::create(&m.data[0], Cairo::FORMAT_ARGB32, m.width, m.height, m.stride);
+ cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
+ return surface;
+ }
+
+ void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
+ {
+ auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
+ surface.clear();
+
+ auto &m = mappings[mapping];
+
+ if (!junk) {
+ glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, &m.data[0]);
+ }
+
+ m.used = false;
+ m.data.clear();
+ }
+};
+
+} // namespace
+
+std::unique_ptr<PixelStreamer> PixelStreamer::create_supported(Method method)
+{
+ int ver = epoxy_gl_version();
+
+ if (method <= Method::Asynchronous) {
+ if (ver >= 30 || epoxy_has_gl_extension("GL_ARB_map_buffer_range")) {
+ if (method <= Method::Persistent) {
+ if (ver >= 44 || (epoxy_has_gl_extension("GL_ARB_buffer_storage") &&
+ epoxy_has_gl_extension("GL_ARB_texture_storage") &&
+ epoxy_has_gl_extension("GL_ARB_SYNC")))
+ {
+ return std::make_unique<PersistentPixelStreamer>();
+ } else if (method != Method::Auto) {
+ std::cerr << "Persistent PixelStreamer not available" << std::endl;
+ }
+ }
+ return std::make_unique<AsynchronousPixelStreamer>();
+ } else if (method != Method::Auto) {
+ std::cerr << "Asynchronous PixelStreamer not available" << std::endl;
+ }
+ }
+ return std::make_unique<SynchronousPixelStreamer>();
+}
+
+} // 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/canvas/pixelstreamer.h b/src/ui/widget/canvas/pixelstreamer.h
new file mode 100644
index 0000000..bcd3684
--- /dev/null
+++ b/src/ui/widget/canvas/pixelstreamer.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A class hierarchy implementing various ways of streaming pixel buffers to the GPU.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H
+#define INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H
+
+#include <memory>
+#include <2geom/int-point.h>
+#include <cairomm/refptr.h>
+#include <cairomm/surface.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// A class for turning Cairo image surfaces into OpenGL textures.
+class PixelStreamer
+{
+public:
+ virtual ~PixelStreamer() = default;
+
+ // Method for streaming pixels to the GPU.
+ enum class Method
+ {
+ Auto, // Use the best option available at runtime.
+ Persistent, // Persistent buffer mapping. (Best, requires OpenGL 4.4.)
+ Asynchronous, // Ordinary buffer mapping. (Almost as good, requires OpenGL 3.0.)
+ Synchronous // Synchronous texture uploads. (Worst but still tolerable, requires OpenGL 1.1.)
+ };
+
+ // Create a PixelStreamer using a choice of method specified at runtime, falling back if unsupported.
+ static std::unique_ptr<PixelStreamer> create_supported(Method method);
+
+ // Return the method in use.
+ virtual Method get_method() const = 0;
+
+ /**
+ * Request a drawing surface of the given dimensions. If nogl is true, no GL commands will be issued,
+ * but the request may fail. An effort is made to keep such failures to a minimum.
+ *
+ * The surface must be returned to the PixelStreamer by calling finish(), in order to deallocate
+ * GL resourecs.
+ */
+ virtual Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl = false) = 0;
+
+ /**
+ * Give back a drawing surface produced by request(), uploading the contents to the currently bound texture.
+ * The texture must be at least as big as the surface.
+ *
+ * If junk is true, then the surface will be junked instead, meaning nothing will be done with the contents,
+ * and its GL resources will simply be deallocated.
+ */
+ virtual void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk = false) = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_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/canvas/prefs.h b/src/ui/widget/canvas/prefs.h
new file mode 100644
index 0000000..363fb6d
--- /dev/null
+++ b/src/ui/widget/canvas/prefs.h
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_PREFS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_PREFS_H
+
+#include "preferences.h"
+
+namespace Inkscape::UI::Widget {
+
+class Prefs
+{
+public:
+ Prefs()
+ {
+ devmode.action = [this] { set_devmode(devmode); };
+ devmode.action();
+ }
+
+ // Main preferences
+ Pref<int> xray_radius = { "/options/rendering/xray-radius", 100, 1, 1500 };
+ Pref<int> outline_overlay_opacity = { "/options/rendering/outline-overlay-opacity", 50, 0, 100 };
+ Pref<int> update_strategy = { "/options/rendering/update_strategy", 3, 1, 3 };
+ Pref<bool> request_opengl = { "/options/rendering/request_opengl" };
+ Pref<int> grabsize = { "/options/grabsize/value", 3, 1, 15 };
+ Pref<int> numthreads = { "/options/threading/numthreads", 0, 1, 256 };
+
+ // Colour management
+ Pref<bool> from_display = { "/options/displayprofile/from_display" };
+ Pref<void> displayprofile = { "/options/displayprofile" };
+ Pref<void> softproof = { "/options/softproof" };
+
+ // Auto-scrolling
+ Pref<int> autoscrolldistance = { "/options/autoscrolldistance/value", 0, -1000, 10000 };
+ Pref<double> autoscrollspeed = { "/options/autoscrollspeed/value", 1.0, 0.0, 10.0 };
+
+ // Devmode preferences
+ Pref<int> tile_size = { "/options/rendering/tile_size", 300, 1, 10000 };
+ Pref<int> render_time_limit = { "/options/rendering/render_time_limit", 80, 1, 5000 };
+ Pref<bool> block_updates = { "/options/rendering/block_updates", true };
+ Pref<int> pixelstreamer_method = { "/options/rendering/pixelstreamer_method", 1, 1, 4 };
+ Pref<int> padding = { "/options/rendering/padding", 350, 0, 1000 };
+ Pref<int> prerender = { "/options/rendering/prerender", 100, 0, 1000 };
+ Pref<int> preempt = { "/options/rendering/preempt", 250, 0, 1000 };
+ Pref<int> coarsener_min_size = { "/options/rendering/coarsener_min_size", 200, 0, 1000 };
+ Pref<int> coarsener_glue_size = { "/options/rendering/coarsener_glue_size", 80, 0, 1000 };
+ Pref<double> coarsener_min_fullness = { "/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0 };
+
+ // Debug switches
+ Pref<bool> debug_framecheck = { "/options/rendering/debug_framecheck" };
+ Pref<bool> debug_logging = { "/options/rendering/debug_logging" };
+ Pref<bool> debug_delay_redraw = { "/options/rendering/debug_delay_redraw" };
+ Pref<int> debug_delay_redraw_time = { "/options/rendering/debug_delay_redraw_time", 50, 0, 1000000 };
+ Pref<bool> debug_show_redraw = { "/options/rendering/debug_show_redraw" };
+ Pref<bool> debug_show_unclean = { "/options/rendering/debug_show_unclean" }; // no longer implemented
+ Pref<bool> debug_show_snapshot = { "/options/rendering/debug_show_snapshot" };
+ Pref<bool> debug_show_clean = { "/options/rendering/debug_show_clean" }; // no longer implemented
+ Pref<bool> debug_disable_redraw = { "/options/rendering/debug_disable_redraw" };
+ Pref<bool> debug_sticky_decoupled = { "/options/rendering/debug_sticky_decoupled" };
+ Pref<bool> debug_animate = { "/options/rendering/debug_animate" };
+
+private:
+ // Developer mode
+ Pref<bool> devmode = { "/options/rendering/devmode" };
+
+ void set_devmode(bool on)
+ {
+ tile_size.set_enabled(on);
+ render_time_limit.set_enabled(on);
+ pixelstreamer_method.set_enabled(on);
+ padding.set_enabled(on);
+ prerender.set_enabled(on);
+ preempt.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_delay_redraw.set_enabled(on);
+ debug_delay_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);
+ debug_animate.set_enabled(on);
+ }
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_PREFS_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/canvas/stores.cpp b/src/ui/widget/canvas/stores.cpp
new file mode 100644
index 0000000..70327f5
--- /dev/null
+++ b/src/ui/widget/canvas/stores.cpp
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <array>
+#include <cmath>
+#include <2geom/transforms.h>
+#include <2geom/parallelogram.h>
+#include <2geom/point.h>
+#include "helper/geom.h"
+#include "ui/util.h"
+#include "stores.h"
+#include "prefs.h"
+#include "fragment.h"
+#include "graphics.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+namespace {
+
+// Determine whether an affine transformation approximately maps the unit square [0, 1]^2 to itself.
+bool preserves_unitsquare(Geom::Affine const &affine)
+{
+ return approx_dihedral(Geom::Translate(0.5, 0.5) * affine * Geom::Translate(-0.5, -0.5));
+}
+
+// Apply an affine transformation to a region, then return a strictly smaller region approximating it, made from chunks of size roughly d.
+// To reduce computation, only the intersection of the result with bounds will be valid.
+auto region_affine_approxinwards(Cairo::RefPtr<Cairo::Region> const &reg, Geom::Affine const &affine, Geom::IntRect const &bounds, int d = 200)
+{
+ // Trivial empty case.
+ if (reg->empty()) return Cairo::Region::create();
+
+ // Trivial identity case.
+ if (affine.isIdentity(0.001)) return reg->copy();
+
+ // Fast-path for rectilinear transformations.
+ if (affine.withoutTranslation().isScale(0.001)) {
+ auto regdst = Cairo::Region::create();
+
+ auto transform = [&] (const Geom::IntPoint &p) {
+ return (Geom::Point(p) * affine).round();
+ };
+
+ for (int i = 0; i < reg->get_num_rectangles(); i++) {
+ auto rect = cairo_to_geom(reg->get_rectangle(i));
+ regdst->do_union(geom_to_cairo(Geom::IntRect(transform(rect.min()), transform(rect.max()))));
+ }
+
+ return regdst;
+ }
+
+ // General case.
+ auto ext = cairo_to_geom(reg->get_extents());
+ auto rectdst = ((Geom::Parallelogram(ext) * affine).bounds().roundOutwards() & bounds).regularized();
+ if (!rectdst) return Cairo::Region::create();
+ auto rectsrc = (Geom::Parallelogram(*rectdst) * affine.inverse()).bounds().roundOutwards();
+
+ auto regdst = Cairo::Region::create(geom_to_cairo(*rectdst));
+ auto regsrc = Cairo::Region::create(geom_to_cairo(rectsrc));
+ regsrc->subtract(reg);
+
+ double fx = min(absolute(Geom::Point(1.0, 0.0) * affine.withoutTranslation()));
+ double fy = min(absolute(Geom::Point(0.0, 1.0) * affine.withoutTranslation()));
+
+ for (int i = 0; i < regsrc->get_num_rectangles(); i++)
+ {
+ auto rect = cairo_to_geom(regsrc->get_rectangle(i));
+ int nx = std::ceil(rect.width() * fx / d);
+ int ny = std::ceil(rect.height() * fy / d);
+ auto pt = [&] (int x, int y) {
+ return rect.min() + (rect.dimensions() * Geom::IntPoint(x, y)) / Geom::IntPoint(nx, ny);
+ };
+ for (int x = 0; x < nx; x++) {
+ for (int y = 0; y < ny; y++) {
+ auto r = Geom::IntRect(pt(x, y), pt(x + 1, y + 1));
+ auto r2 = (Geom::Parallelogram(r) * affine).bounds().roundOutwards();
+ regdst->subtract(geom_to_cairo(r2));
+ }
+ }
+ }
+
+ return regdst;
+}
+
+} // namespace
+
+Geom::IntRect Stores::centered(Fragment const &view) const
+{
+ // Return the visible region of the view, plus the prerender and padding margins.
+ return expandedBy(view.rect, _prefs.prerender + _prefs.padding);
+}
+
+void Stores::recreate_store(Fragment const &view)
+{
+ // Recreate the store at the view's affine.
+ _store.affine = view.affine;
+ _store.rect = centered(view);
+ _store.drawn = Cairo::Region::create();
+ // Tell the graphics to create a blank new store.
+ _graphics->recreate_store(_store.rect.dimensions());
+}
+
+void Stores::shift_store(Fragment const &view)
+{
+ // Create a new fragment centred on the viewport.
+ auto rect = centered(view);
+ // Tell the graphics to copy the drawn part of the old store to the new store.
+ _graphics->shift_store(Fragment{ _store.affine, rect });
+ // Set the shifted store as the new store.
+ _store.rect = rect;
+ // Clip the drawn region to the new store.
+ _store.drawn->intersect(geom_to_cairo(_store.rect));
+};
+
+void Stores::take_snapshot(Fragment const &view)
+{
+ // Copy the store to the snapshot, leaving us temporarily in an invalid state.
+ _snapshot = std::move(_store);
+ // Tell the graphics to do the same, except swapping them so we can re-use the old snapshot store.
+ _graphics->swap_stores();
+ // Reset the store.
+ recreate_store(view);
+ // Transform the snapshot's drawn region to the new store's affine.
+ _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, _snapshot.affine.inverse() * _store.affine, _store.rect), 4, -2);
+}
+
+void Stores::snapshot_combine(Fragment const &view)
+{
+ // Add the drawn region to the snapshot drawn region (they both exist in store space, so this is valid), and save its affine.
+ _snapshot.drawn->do_union(_store.drawn);
+ auto old_store_affine = _store.affine;
+
+ // Get the list of corner points in the store's drawn region and the snapshot bounds rect, all at the view's affine.
+ std::vector<Geom::Point> pts;
+ auto add_rect = [&, this] (Geom::Parallelogram const &pl) {
+ for (int i = 0; i < 4; i++) {
+ pts.emplace_back(Geom::Point(pl.corner(i)));
+ }
+ };
+ auto add_store = [&, this] (Store const &s) {
+ int nrects = s.drawn->get_num_rectangles();
+ auto affine = s.affine.inverse() * view.affine;
+ for (int i = 0; i < nrects; i++) {
+ add_rect(Geom::Parallelogram(cairo_to_geom(s.drawn->get_rectangle(i))) * affine);
+ }
+ };
+ add_store(_store);
+ add_rect(Geom::Parallelogram(_snapshot.rect) * _snapshot.affine.inverse() * view.affine);
+
+ // Compute their minimum-area bounding box as a fragment - an (affine, rect) pair.
+ auto [affine, rect] = min_bounding_box(pts);
+ affine = view.affine * affine;
+
+ // Check if the paste transform takes the snapshot store exactly onto the new fragment, possibly with a dihedral transformation.
+ auto paste = Geom::Scale(_snapshot.rect.dimensions())
+ * Geom::Translate(_snapshot.rect.min())
+ * _snapshot.affine.inverse()
+ * affine
+ * Geom::Translate(-rect.min())
+ * Geom::Scale(rect.dimensions()).inverse();
+ if (preserves_unitsquare(paste)) {
+ // If so, simply take the new fragment to be exactly the same as the snapshot store.
+ rect = _snapshot.rect;
+ affine = _snapshot.affine;
+ }
+
+ // Compute the scale difference between the backing store and the new fragment, giving the amount of detail that would be lost by pasting.
+ if ( double scale_ratio = std::sqrt(std::abs(_store.affine.det() / affine.det()));
+ scale_ratio > 4.0 )
+ {
+ // Zoom the new fragment in to increase its quality.
+ double grow = scale_ratio / 2.0;
+ rect *= Geom::Scale(grow);
+ affine *= Geom::Scale(grow);
+ }
+
+ // Do not allow the fragment to become more detailed than the window.
+ if ( double scale_ratio = std::sqrt(std::abs(affine.det() / view.affine.det()));
+ scale_ratio > 1.0 )
+ {
+ // Zoom the new fragment out to reduce its quality.
+ double shrink = 1.0 / scale_ratio;
+ rect *= Geom::Scale(shrink);
+ affine *= Geom::Scale(shrink);
+ }
+
+ // Find the bounding rect of the visible region + prerender margin within the new fragment. We do not want to discard this content in the next clipping step.
+ auto renderable = (Geom::Parallelogram(expandedBy(view.rect, _prefs.prerender)) * view.affine.inverse() * affine).bounds() & rect;
+
+ // Cap the dimensions of the new fragment to slightly larger than the maximum dimension of the window by clipping it towards the screen centre. (Lower in Cairo mode since otherwise too slow to cope.)
+ double max_dimension = max(view.rect.dimensions()) * (_graphics->is_opengl() ? 1.7 : 0.8);
+ auto dimens = rect.dimensions();
+ dimens.x() = std::min(dimens.x(), max_dimension);
+ dimens.y() = std::min(dimens.y(), max_dimension);
+ auto center = Geom::Rect(view.rect).midpoint() * view.affine.inverse() * affine;
+ center.x() = Util::safeclamp(center.x(), rect.left() + dimens.x() * 0.5, rect.right() - dimens.x() * 0.5);
+ center.y() = Util::safeclamp(center.y(), rect.top() + dimens.y() * 0.5, rect.bottom() - dimens.y() * 0.5);
+ rect = Geom::Rect(center - dimens * 0.5, center + dimens * 0.5);
+
+ // Ensure the new fragment contains the renderable rect from earlier, enlarging it and reducing resolution if necessary.
+ if (!rect.contains(renderable)) {
+ auto oldrect = rect;
+ rect.unionWith(renderable);
+ double shrink = 1.0 / std::max(rect.width() / oldrect.width(), rect.height() / oldrect.height());
+ rect *= Geom::Scale(shrink);
+ affine *= Geom::Scale(shrink);
+ }
+
+ // Calculate the paste transform from the snapshot store to the new fragment (again).
+ paste = Geom::Scale(_snapshot.rect.dimensions())
+ * Geom::Translate(_snapshot.rect.min())
+ * _snapshot.affine.inverse()
+ * affine
+ * Geom::Translate(-rect.min())
+ * Geom::Scale(rect.dimensions()).inverse();
+
+ if (_prefs.debug_logging) std::cout << "New fragment dimensions " << rect.width() << ' ' << rect.height() << std::endl;
+
+ if (paste.isIdentity(0.001) && rect.dimensions().round() == _snapshot.rect.dimensions()) {
+ // Fast path: simply paste the backing store onto the snapshot store.
+ if (_prefs.debug_logging) std::cout << "Fast snapshot combine" << std::endl;
+ _graphics->fast_snapshot_combine();
+ } else {
+ // General path: paste the snapshot store and then the backing store onto a new fragment, then set that as the snapshot store.
+ auto frag_rect = rect.roundOutwards();
+ _graphics->snapshot_combine(Fragment{ affine, frag_rect });
+ _snapshot.rect = frag_rect;
+ _snapshot.affine = affine;
+ }
+
+ // Start drawing again on a new blank store aligned to the screen.
+ recreate_store(view);
+ // Transform the snapshot clean region to the new store.
+ // Todo: Should really clip this to the new snapshot rect, only we can't because it's generally not aligned with the store's affine.
+ _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, old_store_affine.inverse() * _store.affine, _store.rect), 4, -2);
+};
+
+void Stores::reset()
+{
+ _mode = Mode::None;
+ _store.drawn.clear();
+ _snapshot.drawn.clear();
+}
+
+// Handle transitions and actions in response to viewport changes.
+auto Stores::update(Fragment const &view) -> Action
+{
+ switch (_mode) {
+
+ case Mode::None: {
+ // Not yet initialised or just reset - create store for first time.
+ recreate_store(view);
+ _mode = Mode::Normal;
+ if (_prefs.debug_logging) std::cout << "Full reset" << std::endl;
+ return Action::Recreated;
+ }
+
+ case Mode::Normal: {
+ auto result = Action::None;
+ // Enter decoupled mode if the affine has changed from what the store was drawn at.
+ if (view.affine != _store.affine) {
+ // Snapshot and reset the store.
+ take_snapshot(view);
+ // Enter decoupled mode.
+ _mode = Mode::Decoupled;
+ if (_prefs.debug_logging) std::cout << "Enter decoupled mode" << std::endl;
+ result = Action::Recreated;
+ } else {
+ // Determine whether the view has moved sufficiently far that we need to shift the store.
+ if (!_store.rect.contains(expandedBy(view.rect, _prefs.prerender))) {
+ // The visible region + prerender margin has reached the edge of the store.
+ if (!(cairo_to_geom(_store.drawn->get_extents()) & expandedBy(view.rect, _prefs.prerender + _prefs.padding)).regularized()) {
+ // If the store contains no reusable content at all, recreate it.
+ recreate_store(view);
+ if (_prefs.debug_logging) std::cout << "Recreate store" << std::endl;
+ result = Action::Recreated;
+ } else {
+ // Otherwise shift it.
+ shift_store(view);
+ if (_prefs.debug_logging) std::cout << "Shift store" << std::endl;
+ result = Action::Shifted;
+ }
+ }
+ }
+ // After these operations, the store should now contain the visible region + prerender margin.
+ assert(_store.rect.contains(expandedBy(view.rect, _prefs.prerender)));
+ return result;
+ }
+
+ case Mode::Decoupled: {
+ // Completely cancel the previous redraw and start again if the viewing parameters have changed too much.
+ auto check_restart_redraw = [&, this] {
+ // With this debug feature on, redraws should never be restarted.
+ if (_prefs.debug_sticky_decoupled) return false;
+
+ // Restart if the store is no longer covering the middle 50% of the screen. (Usually triggered by rotating or zooming out.)
+ auto pl = Geom::Parallelogram(view.rect);
+ pl *= Geom::Translate(-pl.midpoint()) * Geom::Scale(0.5) * Geom::Translate(pl.midpoint());
+ pl *= view.affine.inverse() * _store.affine;
+ if (!Geom::Parallelogram(_store.rect).contains(pl)) {
+ if (_prefs.debug_logging) std::cout << "Restart redraw (store not fully covering screen)" << std::endl;
+ return true;
+ }
+
+ // Also restart if zoomed in or out too much.
+ auto scale_ratio = std::abs(view.affine.det() / _store.affine.det());
+ if (scale_ratio > 3.0 || scale_ratio < 0.7) {
+ // Todo: Un-hard-code these thresholds.
+ // * The threshold 3.0 is for zooming in. It says that if the quality of what is being redrawn is more than 3x worse than that of the screen, restart. This is necessary to ensure acceptably high resolution is kept as you zoom in.
+ // * The threshold 0.7 is for zooming out. It says that if the quality of what is being redrawn is too high compared to the screen, restart. This prevents wasting time redrawing the screen slowly, at too high a quality that will probably not ever be seen.
+ if (_prefs.debug_logging) std::cout << "Restart redraw (zoomed changed too much)" << std::endl;
+ return true;
+ }
+
+ // Don't restart.
+ return false;
+ };
+
+ if (check_restart_redraw()) {
+ // Re-use as much content as possible from the store and the snapshot, and set as the new snapshot.
+ snapshot_combine(view);
+ return Action::Recreated;
+ }
+
+ return Action::None;
+ }
+
+ default: {
+ assert(false);
+ return Action::None;
+ }
+ }
+}
+
+auto Stores::finished_draw(Fragment const &view) -> Action
+{
+ // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to reset the store to the correct affine.
+ if (_mode == Mode::Decoupled) {
+ if (_prefs.debug_sticky_decoupled) {
+ // Debug feature: stop redrawing, but stay in decoupled mode.
+ } else if (_store.affine == view.affine) {
+ // Store is at the correct affine - exit decoupled mode.
+ if (_prefs.debug_logging) std::cout << "Exit decoupled mode" << std::endl;
+ // Exit decoupled mode.
+ _mode = Mode::Normal;
+ _graphics->invalidate_snapshot();
+ } else {
+ // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine.
+ // Snapshot and reset the backing store.
+ take_snapshot(view);
+ if (_prefs.debug_logging) std::cout << "Remain in decoupled mode" << std::endl;
+ return Action::Recreated;
+ }
+ }
+
+ return Action::None;
+}
+
+} // 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/canvas/stores.h b/src/ui/widget/canvas/stores.h
new file mode 100644
index 0000000..70b10cc
--- /dev/null
+++ b/src/ui/widget/canvas/stores.h
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Abstraction of the store/snapshot mechanism.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_STORES_H
+#define INKSCAPE_UI_WIDGET_CANVAS_STORES_H
+
+#include "fragment.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+struct Fragment;
+class Prefs;
+class Graphics;
+
+class Stores
+{
+public:
+ enum class Mode
+ {
+ None, /// Not initialised or just reset; no stores exist yet.
+ Normal, /// Normal mode consisting of just a backing store.
+ Decoupled /// Decoupled mode consisting of both a backing store and a snapshot store.
+ };
+
+ enum class Action
+ {
+ None, /// The backing store was not changed.
+ Recreated, /// The backing store was completely recreated.
+ Shifted /// The backing store was shifted into a new rectangle.
+ };
+
+ struct Store : Fragment
+ {
+ /**
+ * The region of space containing drawn content.
+ * For the snapshot, this region is transformed to store space and approximated inwards.
+ */
+ Cairo::RefPtr<Cairo::Region> drawn;
+ };
+
+ /// Construct a blank object with no stores.
+ Stores(Prefs const &prefs)
+ : _mode(Mode::None)
+ , _graphics(nullptr)
+ , _prefs(prefs) {}
+
+ /// Set the pointer to the graphics object.
+ void set_graphics(Graphics *g) { _graphics = g; }
+
+ /// Discards all stores. (The actual operation on the graphics is performed on the next update().)
+ void reset();
+
+ /// Respond to a viewport change. (Requires a valid graphics.)
+ Action update(Fragment const &view);
+
+ /// Respond to drawing of the backing store having finished. (Requires a valid graphics.)
+ Action finished_draw(Fragment const &view);
+
+ /// Record a rectangle as being drawn to the store.
+ void mark_drawn(Geom::IntRect const &rect) { _store.drawn->do_union(geom_to_cairo(rect)); }
+
+ // Getters.
+ Store const &store() const { return _store; }
+ Store const &snapshot() const { return _snapshot; }
+ Mode mode() const { return _mode; }
+
+private:
+ // Internal state.
+ Mode _mode;
+ Store _store, _snapshot;
+
+ // The graphics object that executes the operations on the stores.
+ Graphics *_graphics;
+
+ // The preferences object we read preferences from.
+ Prefs const &_prefs;
+
+ // Internal actions.
+ Geom::IntRect centered(Fragment const &view) const;
+ void recreate_store(Fragment const &view);
+ void shift_store(Fragment const &view);
+ void take_snapshot(Fragment const &view);
+ void snapshot_combine(Fragment const &view);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_STORES_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/canvas/synchronizer.cpp b/src/ui/widget/canvas/synchronizer.cpp
new file mode 100644
index 0000000..331057b
--- /dev/null
+++ b/src/ui/widget/canvas/synchronizer.cpp
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "synchronizer.h"
+#include <cassert>
+
+namespace Inkscape::UI::Widget {
+
+Synchronizer::Synchronizer()
+{
+ dispatcher.connect([this] { on_dispatcher(); });
+}
+
+void Synchronizer::signalExit() const
+{
+ auto lock = std::unique_lock(mutables);
+ awaken();
+ assert(slots.empty());
+ exitposted = true;
+}
+
+void Synchronizer::runInMain(std::function<void()> const &f) const
+{
+ auto lock = std::unique_lock(mutables);
+ awaken();
+ auto s = Slot{ &f };
+ slots.emplace_back(&s);
+ assert(!exitposted);
+ slots_cond.wait(lock, [&] { return !s.func; });
+}
+
+void Synchronizer::waitForExit() const
+{
+ auto lock = std::unique_lock(mutables);
+ main_blocked = true;
+ while (true) {
+ if (!slots.empty()) {
+ process_slots(lock);
+ } else if (exitposted) {
+ exitposted = false;
+ break;
+ }
+ main_cond.wait(lock);
+ }
+ main_blocked = false;
+}
+
+sigc::connection Synchronizer::connectExit(sigc::slot<void()> const &slot)
+{
+ return signal_exit.connect(slot);
+}
+
+void Synchronizer::awaken() const
+{
+ if (exitposted || !slots.empty()) {
+ return;
+ }
+
+ if (main_blocked) {
+ main_cond.notify_all();
+ } else {
+ const_cast<Glib::Dispatcher&>(dispatcher).emit(); // Glib::Dispatcher is const-incorrect.
+ }
+}
+
+void Synchronizer::on_dispatcher() const
+{
+ auto lock = std::unique_lock(mutables);
+ if (!slots.empty()) {
+ process_slots(lock);
+ } else if (exitposted) {
+ exitposted = false;
+ lock.unlock();
+ signal_exit.emit();
+ }
+}
+
+void Synchronizer::process_slots(std::unique_lock<std::mutex> &lock) const
+{
+ while (!slots.empty()) {
+ auto slots_grabbed = std::move(slots);
+ lock.unlock();
+ for (auto &s : slots_grabbed) {
+ (*s->func)();
+ }
+ lock.lock();
+ for (auto &s : slots_grabbed) {
+ s->func = nullptr;
+ }
+ slots_cond.notify_all();
+ }
+}
+
+} // namespace Inkscape::UI::Widget
+
+/*
+ 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/canvas/synchronizer.h b/src/ui/widget/canvas/synchronizer.h
new file mode 100644
index 0000000..45c88d2
--- /dev/null
+++ b/src/ui/widget/canvas/synchronizer.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H
+#define INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H
+
+#include <functional>
+#include <vector>
+#include <mutex>
+#include <condition_variable>
+
+#include <sigc++/sigc++.h>
+#include <glibmm/dispatcher.h>
+
+namespace Inkscape::UI::Widget {
+
+// Synchronisation primitive suiting the canvas's needs. All synchronisation between the main/render threads goes through here.
+class Synchronizer
+{
+public:
+ Synchronizer();
+
+ // Background side:
+
+ // Indicate that the background process has exited, causing EITHER signal_exit to be emitted OR waitforexit() to unblock.
+ void signalExit() const;
+
+ // Block until the given function has executed in the main thread, possibly waking it up if it is itself blocked.
+ // (Note: This is necessary for servicing occasional buffer mapping requests where one can't be pulled from a pool.)
+ void runInMain(std::function<void()> const &f) const;
+
+ // Main-thread side:
+
+ // Block until the background process has exited, gobbling the emission of signal_exit in the process.
+ void waitForExit() const;
+
+ // Connect to signal_exit.
+ sigc::connection connectExit(sigc::slot<void()> const &slot);
+
+private:
+ struct Slot
+ {
+ std::function<void()> const *func;
+ };
+
+ Glib::Dispatcher dispatcher; // Used to wake up main thread if idle in GTK main loop.
+ sigc::signal<void()> signal_exit;
+
+ mutable std::mutex mutables;
+ mutable bool exitposted = false;
+ mutable bool main_blocked = false; // Whether main thread is blocked in waitForExit().
+ mutable std::condition_variable main_cond; // Used to wake up main thread if blocked.
+ mutable std::vector<Slot*> slots; // List of functions from runInMain() waiting to be run.
+ mutable std::condition_variable slots_cond; // Used to wake up render threads blocked in runInMain().
+
+ void awaken() const;
+ void on_dispatcher() const;
+ void process_slots(std::unique_lock<std::mutex> &lock) const;
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_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/canvas/texture.cpp b/src/ui/widget/canvas/texture.cpp
new file mode 100644
index 0000000..420937a
--- /dev/null
+++ b/src/ui/widget/canvas/texture.cpp
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "texture.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+static bool have_gltexstorage()
+{
+ static bool result = [] {
+ return epoxy_gl_version() >= 42 || epoxy_has_gl_extension("GL_ARB_texture_storage");
+ }();
+ return result;
+}
+
+static bool have_glinvalidateteximage()
+{
+ static bool result = [] {
+ return epoxy_gl_version() >= 43 || epoxy_has_gl_extension("ARB_invalidate_subdata");
+ }();
+ return result;
+}
+
+Texture::Texture(Geom::IntPoint const &size)
+ : _size(size)
+{
+ glGenTextures(1, &_id);
+ glBindTexture(GL_TEXTURE_2D, _id);
+
+ // Common flags for all textures used at the moment.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ if (have_gltexstorage()) {
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, size.x(), size.y());
+ } else {
+ // Note: This fallback path is always chosen on the Mac due to Apple's crippling of OpenGL.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size.x(), size.y(), 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
+ }
+}
+
+void Texture::invalidate()
+{
+ if (have_glinvalidateteximage()) {
+ glInvalidateTexImage(_id, 0);
+ }
+}
+
+} // 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/canvas/texture.h b/src/ui/widget/canvas/texture.h
new file mode 100644
index 0000000..98aeba2
--- /dev/null
+++ b/src/ui/widget/canvas/texture.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H
+#define INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H
+
+#include <boost/noncopyable.hpp>
+#include <2geom/point.h>
+#include <epoxy/gl.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Texture
+{
+public:
+ // Create null texture owning no resources.
+ Texture() = default;
+
+ // Allocate a blank texture of a given size. The texture is bound to GL_TEXTURE_2D.
+ Texture(Geom::IntPoint const &size);
+
+ // Wrap an existing texture.
+ Texture(GLuint id, Geom::IntPoint const &size) : _id(id), _size(size) {}
+
+ // Boilerplate constructors/operators
+ Texture(Texture &&other) noexcept { _movefrom(other); }
+ Texture &operator=(Texture &&other) noexcept { _reset(); _movefrom(other); return *this; }
+ ~Texture() { _reset(); }
+
+ // Observers
+ GLuint id() const { return _id; }
+ Geom::IntPoint const &size() const { return _size; }
+ explicit operator bool() const { return _id; }
+
+ // Methods
+ void clear() { _reset(); _id = 0; }
+ void invalidate();
+
+private:
+ GLuint _id = 0;
+ Geom::IntPoint _size;
+
+ void _reset() noexcept { if (_id) glDeleteTextures(1, &_id); }
+ void _movefrom(Texture &other) noexcept { _id = other._id; _size = other._size; other._id = 0; }
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_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/canvas/texturecache.cpp b/src/ui/widget/canvas/texturecache.cpp
new file mode 100644
index 0000000..6215849
--- /dev/null
+++ b/src/ui/widget/canvas/texturecache.cpp
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <unordered_map>
+#include <vector>
+#include <cassert>
+#include <boost/unordered_map.hpp> // For hash of pair
+#include "helper/mathfns.h"
+#include "texturecache.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+class BasicTextureCache : public TextureCache
+{
+ static int constexpr min_dimension = 16;
+ static int constexpr expiration_timeout = 10000;
+
+ static int constexpr dim_to_ind(int dim) { return Util::floorlog2((dim - 1) / min_dimension) + 1; }
+ static int constexpr ind_to_maxdim(int index) { return min_dimension * (1 << index); }
+
+ static std::pair<int, int> dims_to_inds(Geom::IntPoint const &dims) { return { dim_to_ind(dims.x()), dim_to_ind(dims.y()) }; }
+ static Geom::IntPoint inds_to_maxdims(std::pair<int, int> const &inds) { return { ind_to_maxdim(inds.first), ind_to_maxdim(inds.second) }; }
+
+ // A cache of POT textures.
+ struct Bucket
+ {
+ std::vector<Texture> unused;
+ int used = 0;
+ int high_use_count = 0;
+ };
+ boost::unordered_map<std::pair<int, int>, Bucket> buckets;
+
+ // Used to periodicially discard excess cached textures.
+ int expiration_timer = 0;
+
+public:
+ Texture request(Geom::IntPoint const &dimensions) override
+ {
+ // Find the bucket that the dimensions fall into.
+ auto indexes = dims_to_inds(dimensions);
+ auto &b = buckets[indexes];
+
+ // Reuse or create a texture of the appropriate dimensions.
+ Texture tex;
+ if (!b.unused.empty()) {
+ tex = std::move(b.unused.back());
+ b.unused.pop_back();
+ glBindTexture(GL_TEXTURE_2D, tex.id());
+ } else {
+ tex = Texture(inds_to_maxdims(indexes)); // binds
+ }
+
+ // Record the new use count of the bucket.
+ b.used++;
+ if (b.used > b.high_use_count) {
+ // If the use count has gone above the high-water mark, record this, and reset the timer for when to clean up excess unused textures.
+ b.high_use_count = b.used;
+ expiration_timer = 0;
+ }
+
+ return tex;
+ }
+
+ void finish(Texture tex) override
+ {
+ auto indexes = dims_to_inds(tex.size());
+ auto &b = buckets[indexes];
+
+ // Orphan the texture, if possible.
+ tex.invalidate();
+
+ // Put the texture back in its corresponding bucket's cache of unused textures.
+ b.unused.emplace_back(std::move(tex));
+ b.used--;
+
+ // If the expiration timeout has been reached, prune the cache of textures down to what was actually used in the last cycle.
+ expiration_timer++;
+ if (expiration_timer >= expiration_timeout) {
+ expiration_timer = 0;
+
+ for (auto &[k, b] : buckets) {
+ int max_unused = b.high_use_count - b.used;
+ assert(max_unused >= 0);
+ if (b.unused.size() > max_unused) {
+ b.unused.resize(max_unused);
+ }
+ b.high_use_count = b.used;
+ }
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr<TextureCache> TextureCache::create()
+{
+ return std::make_unique<BasicTextureCache>();
+}
+
+} // 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/canvas/texturecache.h b/src/ui/widget/canvas/texturecache.h
new file mode 100644
index 0000000..ea78a67
--- /dev/null
+++ b/src/ui/widget/canvas/texturecache.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Extremely basic gadget for re-using textures, since texture creation turns out to be quite expensive.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H
+#define INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H
+
+#include <memory>
+#include "texture.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class TextureCache
+{
+public:
+ virtual ~TextureCache() = default;
+
+ static std::unique_ptr<TextureCache> create();
+
+ /**
+ * Request a texture of at least the given dimensions.
+ * The texture is bound to GL_TEXTURE_2D.
+ */
+ virtual Texture request(Geom::IntPoint const &dimensions) = 0;
+
+ /**
+ * Return a no-longer used texture to the pool.
+ */
+ virtual void finish(Texture tex) = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_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/canvas/updaters.cpp b/src/ui/widget/canvas/updaters.cpp
new file mode 100644
index 0000000..8441be0
--- /dev/null
+++ b/src/ui/widget/canvas/updaters.cpp
@@ -0,0 +1,235 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "updaters.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ResponsiveUpdater : public Updater
+{
+public:
+ Strategy get_strategy() const override { return Strategy::Responsive; }
+
+ void reset() override { clean_region = Cairo::Region::create(); }
+ void intersect (Geom::IntRect const &rect) override { clean_region->intersect(geom_to_cairo(rect)); }
+ void mark_dirty(Geom::IntRect const &rect) override { clean_region->subtract(geom_to_cairo(rect)); }
+ void mark_dirty(Cairo::RefPtr<Cairo::Region> const &reg) override { clean_region->subtract(reg); }
+ void mark_clean(Geom::IntRect const &rect) override { clean_region->do_union(geom_to_cairo(rect)); }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override { return clean_region; }
+ bool report_finished () override { return false; }
+ void next_frame () override {}
+};
+
+class FullRedrawUpdater : public ResponsiveUpdater
+{
+ // 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:
+ Strategy get_strategy() const override { return Strategy::FullRedraw; }
+
+ void reset() override
+ {
+ ResponsiveUpdater::reset();
+ inprogress = false;
+ old_clean_region.clear();
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::intersect(rect);
+ if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect));
+ }
+
+ void mark_dirty(Geom::IntRect const &rect) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ ResponsiveUpdater::mark_dirty(rect);
+ }
+
+ void mark_dirty(const Cairo::RefPtr<Cairo::Region> &reg) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ ResponsiveUpdater::mark_dirty(reg);
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::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;
+ }
+ }
+};
+
+class MultiscaleUpdater : public ResponsiveUpdater
+{
+ // 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:
+ Strategy get_strategy() const override { return Strategy::Multiscale; }
+
+ void reset() override
+ {
+ ResponsiveUpdater::reset();
+ inprogress = activated = false;
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::intersect(rect);
+ if (activated) {
+ for (auto &reg : blocked) {
+ reg->intersect(geom_to_cairo(rect));
+ }
+ }
+ }
+
+ void mark_dirty(Geom::IntRect const &rect) override
+ {
+ ResponsiveUpdater::mark_dirty(rect);
+ post_mark_dirty();
+ }
+
+ void mark_dirty(const Cairo::RefPtr<Cairo::Region> &reg) override
+ {
+ ResponsiveUpdater::mark_dirty(reg);
+ post_mark_dirty();
+ }
+
+ void post_mark_dirty()
+ {
+ if (inprogress && !activated) {
+ counter = scale = elapsed = 0;
+ blocked = { Cairo::Region::create() };
+ activated = true;
+ }
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::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 next_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]);
+ }
+ }
+};
+
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::Responsive>() {return std::make_unique<ResponsiveUpdater>();}
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::FullRedraw>() {return std::make_unique<FullRedrawUpdater>();}
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::Multiscale>() {return std::make_unique<MultiscaleUpdater>();}
+
+std::unique_ptr<Updater> Updater::create(Strategy strategy)
+{
+ switch (strategy)
+ {
+ case Strategy::Responsive: return create<Strategy::Responsive>();
+ case Strategy::FullRedraw: return create<Strategy::FullRedraw>();
+ case Strategy::Multiscale: return create<Strategy::Multiscale>();
+ default: return nullptr; // Never triggered, but GCC errors out on build without.
+ }
+}
+
+} // 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/canvas/updaters.h b/src/ui/widget/canvas/updaters.h
new file mode 100644
index 0000000..d36685a
--- /dev/null
+++ b/src/ui/widget/canvas/updaters.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Controls the order to update invalidated regions.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H
+
+#include <vector>
+#include <memory>
+#include <2geom/int-rect.h>
+#include <cairomm/refptr.h>
+#include <cairomm/region.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// A class for tracking invalidation events and producing redraw regions.
+class Updater
+{
+public:
+ virtual ~Updater() = default;
+
+ // The subregion of the store with up-to-date content.
+ Cairo::RefPtr<Cairo::Region> clean_region;
+
+ enum class Strategy
+ {
+ Responsive, // As soon as a region is invalidated, redraw it.
+ FullRedraw, // When a region is invalidated, delay redraw until after the current redraw is completed.
+ Multiscale, // Updates tiles near the mouse faster. Gives the best of both.
+ };
+
+ // Create an Updater using the given strategy.
+ template <Strategy strategy>
+ static std::unique_ptr<Updater> create();
+
+ // Create an Updater using a choice of strategy specified at runtime.
+ static std::unique_ptr<Updater> create(Strategy strategy);
+
+ // Return the strategy in use.
+ virtual Strategy get_strategy() const = 0;
+
+ virtual void reset() = 0; // Reset the clean region to empty.
+ virtual void intersect (Geom::IntRect const &) = 0; // Called when the store changes position; clip everything to the new store rectangle.
+ virtual void mark_dirty(Geom::IntRect const &) = 0; // Called on every invalidate event.
+ virtual void mark_dirty(Cairo::RefPtr<Cairo::Region> const &) = 0; // Called on every invalidate event.
+ virtual void mark_clean(Geom::IntRect const &) = 0; // Called on every rectangle redrawn.
+
+ // Called at the start of a redraw to determine what region to consider clean (i.e. will not be drawn).
+ virtual Cairo::RefPtr<Cairo::Region> get_next_clean_region() = 0;
+
+ // Called after a redraw has finished. Returns true to indicate that further redraws are required with different clean regions.
+ virtual bool report_finished() = 0;
+
+ // Called at the start of each frame. Some updaters (Multiscale) require this information.
+ virtual void next_frame() = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_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/canvas/util.cpp b/src/ui/widget/canvas/util.cpp
new file mode 100644
index 0000000..3d9d59b
--- /dev/null
+++ b/src/ui/widget/canvas/util.cpp
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void region_to_path(Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const &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);
+ }
+}
+
+Cairo::RefPtr<Cairo::Region> shrink_region(Cairo::RefPtr<Cairo::Region> const &reg, int d, int t)
+{
+ // Find the bounding rect, expanded by 1 in all directions.
+ auto rect = geom_to_cairo(expandedBy(cairo_to_geom(reg->get_extents()), 1));
+
+ // Take the complement of the region within the rect.
+ auto reg2 = Cairo::Region::create(rect);
+ reg2->subtract(reg);
+
+ // Increase the width and height of every rectangle by d.
+ auto reg3 = Cairo::Region::create();
+ for (int i = 0; i < reg2->get_num_rectangles(); i++) {
+ auto rect = reg2->get_rectangle(i);
+ rect.x += t;
+ rect.y += t;
+ rect.width += d;
+ rect.height += d;
+ reg3->do_union(rect);
+ }
+
+ // Take the complement of the region within the rect.
+ reg2 = Cairo::Region::create(rect);
+ reg2->subtract(reg3);
+
+ return reg2;
+}
+
+std::array<float, 3> checkerboard_darken(std::array<float, 3> const &rgb, float amount)
+{
+ std::array<float, 3> hsl;
+ SPColor::rgb_to_hsl_floatv(&hsl[0], rgb[0], rgb[1], rgb[2]);
+ hsl[2] += (hsl[2] < 0.08 ? 0.08 : -0.08) * amount;
+
+ std::array<float, 3> rgb2;
+ SPColor::hsl_to_rgb_floatv(&rgb2[0], hsl[0], hsl[1], hsl[2]);
+
+ return rgb2;
+}
+
+} // 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/canvas/util.h b/src/ui/widget/canvas/util.h
new file mode 100644
index 0000000..c2c1ad3
--- /dev/null
+++ b/src/ui/widget/canvas/util.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_UTIL_H
+#define INKSCAPE_UI_WIDGET_CANVAS_UTIL_H
+
+#include <array>
+#include <2geom/int-rect.h>
+#include <2geom/affine.h>
+#include <cairomm/cairomm.h>
+#include "color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// Cairo additions
+
+/**
+ * Turn a Cairo region into a path on a given Cairo context.
+ */
+void region_to_path(Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const &reg);
+
+/**
+ * Shrink a region by d/2 in all directions, while also translating it by (d/2 + t, d/2 + t).
+ */
+Cairo::RefPtr<Cairo::Region> shrink_region(Cairo::RefPtr<Cairo::Region> const &reg, int d, int t = 0);
+
+inline auto unioned(Cairo::RefPtr<Cairo::Region> a, Cairo::RefPtr<Cairo::Region> const &b)
+{
+ a->do_union(b);
+ return a;
+}
+
+// Colour operations
+
+inline auto rgb_to_array(uint32_t rgb)
+{
+ return std::array{SP_RGBA32_R_U(rgb) / 255.0f, SP_RGBA32_G_U(rgb) / 255.0f, SP_RGBA32_B_U(rgb) / 255.0f};
+}
+
+inline auto rgba_to_array(uint32_t rgba)
+{
+ return std::array{SP_RGBA32_R_U(rgba) / 255.0f, SP_RGBA32_G_U(rgba) / 255.0f, SP_RGBA32_B_U(rgba) / 255.0f, SP_RGBA32_A_U(rgba) / 255.0f};
+}
+
+inline auto premultiplied(std::array<float, 4> arr)
+{
+ arr[0] *= arr[3];
+ arr[1] *= arr[3];
+ arr[2] *= arr[3];
+ return arr;
+}
+
+std::array<float, 3> checkerboard_darken(std::array<float, 3> const &rgb, float amount = 1.0f);
+
+inline auto checkerboard_darken(uint32_t rgba)
+{
+ return checkerboard_darken(rgb_to_array(rgba), 1.0f - SP_RGBA32_A_U(rgba) / 255.0f);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_UTIL_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-entry.cpp b/src/ui/widget/color-entry.cpp
new file mode 100644
index 0000000..c3d8ec3
--- /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..184427f
--- /dev/null
+++ b/src/ui/widget/color-icc-selector.cpp
@@ -0,0 +1,987 @@
+// 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 "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(const std::string &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, bool no_alpha)
+ : _impl(nullptr)
+{
+ _impl = new ColorICCSelectorImpl(this, color);
+ init(no_alpha);
+ color.signal_changed.connect(sigc::mem_fun(*this, &ColorICCSelector::_colorChanged));
+ color.signal_icc_changed.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(bool no_alpha)
+{
+ 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, "null", -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);
+
+ if (no_alpha) {
+ _impl->_slider->hide();
+ gtk_widget_hide(_impl->_label);
+ gtk_widget_hide(_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 && std::string(name) != "null") {
+ if (tmp.getColorProfile() == 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 (auto newProf = SP_ACTIVE_DOCUMENT->getProfileManager().find(name)) {
+ 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()));
+
+ std::vector<double> colors;
+ 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
+ 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));
+ tmp.setColorProfile(newProf);
+ tmp.setColors(std::move(colors));
+ } else {
+ g_warning("Couldn't get sRGB from color profile.");
+ }
+
+ dirty = true;
+ }
+ }
+ }
+ }
+ else {
+#ifdef DEBUG_LCMS
+ g_message("NUKE THE ICC");
+#endif // DEBUG_LCMS
+ if (tmp.hasColorProfile()) {
+ tmp.unsetColorProfile();
+ 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.getColorProfile());
+ _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, "null", -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;
+ auto color = _impl->_color.color();
+ auto name = color.getColorProfile();
+
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_colorChanged(%08x:%s)", this, color.toRGBA32(_impl->_color.alpha()), name.c_str());
+#endif // DEBUG_LCMS
+
+ _impl->_profilesChanged(name);
+ ColorScales<>::setScaled(_impl->_adj, _impl->_color.alpha());
+
+ _impl->_setProfile(name);
+ _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++) {
+ auto colors = color.getColors();
+ gdouble val = 0.0;
+ if (colors.size() > i) {
+ auto scale = static_cast<double>(_impl->_compUI[i]._component.scale);
+ if (_impl->_compUI[i]._component.scale == 256) {
+ val = (colors[i] + 128.0) / scale;
+ }
+ else {
+ val = colors[i] / 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 != 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(const std::string &profile)
+{
+#ifdef DEBUG_LCMS
+ g_message("/^^^^^^^^^ %p::_setProfile(%s)", this, profile.c_str());
+#endif // DEBUG_LCMS
+ bool profChanged = false;
+ if (_prof && _profileName != profile) {
+ // Need to clear out the prior one
+ profChanged = true;
+ _profileName.clear();
+ _prof = nullptr;
+ _profChannelCount = 0;
+ } else if (!_prof && !profile.empty()) {
+ profChanged = true;
+ }
+
+ for (auto & i : _compUI) {
+ gtk_widget_hide(i._label);
+ i._slider->hide();
+ gtk_widget_hide(i._btn);
+ }
+
+ if (!profile.empty()) {
+ _prof = SP_ACTIVE_DOCUMENT->getProfileManager().find(profile.c_str());
+ if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) {
+ _profChannelCount = _prof->getChannelCount();
+
+ 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));
+ gtk_widget_show(_compUI[i]._label);
+ _compUI[i]._slider->show();
+ gtk_widget_show(_compUI[i]._btn);
+ }
+ 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)
+{
+ _slider->set_sensitive(false);
+
+ if (_color.color().hasColorProfile()) {
+ auto colors = _color.color().getColors();
+ if (colors.size() != _profChannelCount) {
+ g_warning("Can't set profile with %d colors to %d channels", (int)colors.size(), _profChannelCount);
+ }
+ for (guint i = 0; i < _profChannelCount; i++) {
+ double val = 0.0;
+ auto scale = static_cast<double>(_compUI[i]._component.scale);
+ if (_compUI[i]._component.scale == 256) {
+ val = (colors[i] + 128.0) / scale;
+ } else {
+ val = colors[i] / scale;
+ }
+ _compUI[i]._adj->set_value(val);
+ }
+
+ if (_prof) {
+ _slider->set_sensitive(true);
+
+ 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);
+ }
+
+ // Set the sRGB version of the color first.
+ guint32 prior = iccSelector->_impl->_color.color().toRGBA32(255);
+ guint32 newer = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 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
+
+ // Be careful to always set() and then setColors() to retain ICC data.
+ newColor.set(newer);
+ if (iccSelector->_impl->_color.color().hasColorProfile()) {
+ std::vector<double> colors;
+ for (guint i = 0; i < iccSelector->_impl->_profChannelCount; i++) {
+ double 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;
+ }
+ colors.push_back(val);
+ }
+ newColor.setColors(std::move(colors));
+ }
+ }
+ }
+ iccSelector->_impl->_color.setColorAlpha(newColor, scaled);
+ iccSelector->_impl->_updateSliders(match);
+
+ iccSelector->_impl->_updating = FALSE;
+#ifdef DEBUG_LCMS
+ g_message("\\_________ %p::_adjustmentChanged()", this);
+#endif // DEBUG_LCMS
+}
+
+void ColorICCSelectorImpl::_sliderGrabbed()
+{
+}
+
+void ColorICCSelectorImpl::_sliderReleased()
+{
+}
+
+void ColorICCSelectorImpl::_sliderChanged()
+{
+}
+
+Gtk::Widget *ColorICCSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color, bool no_alpha) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorICCSelector(color, no_alpha));
+ 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..444fbe2
--- /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, bool no_alpha);
+ ~ColorICCSelector() override;
+
+ void init(bool no_alpha);
+
+ 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, bool no_alpha) 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..f28ba25
--- /dev/null
+++ b/src/ui/widget/color-notebook.cpp
@@ -0,0 +1,382 @@
+// 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 "widgets/spw-utilities.h"
+
+using Inkscape::CMSSystem;
+
+#define XPAD 2
+#define YPAD 1
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+
+ColorNotebook::ColorNotebook(SelectedColor &color, bool no_alpha)
+ : Gtk::Grid()
+ , _selected_color(color)
+{
+ set_name("ColorNotebook");
+
+ _initUI(no_alpha);
+
+ _selected_color.signal_changed.connect(sigc::mem_fun(*this, &ColorNotebook::_onSelectedColorChanged));
+ _selected_color.signal_dragged.connect(sigc::mem_fun(*this, &ColorNotebook::_onSelectedColorChanged));
+
+ auto desktop = SP_ACTIVE_DESKTOP;
+ _doc_replaced_connection = desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &ColorNotebook::setDocument)));
+ setDocument(desktop->getDocument());
+}
+
+ColorNotebook::~ColorNotebook()
+{
+ if (_onetimepick)
+ _onetimepick.disconnect();
+ _doc_replaced_connection.disconnect();
+ setDocument(nullptr);
+}
+
+ColorNotebook::Page::Page(std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory, const char* icon)
+ : selector_factory(std::move(selector_factory)), icon_name(icon)
+{
+}
+
+void ColorNotebook::setDocument(SPDocument *document)
+{
+ _document = document;
+ _icc_changed_connection.disconnect();
+ if (document) {
+ _icc_changed_connection = document->connectResourcesChanged("iccprofile", [this]() {
+ _selected_color.emitIccChanged();
+ });
+ }
+}
+
+void ColorNotebook::set_label(const Glib::ustring& label) {
+ _label->set_markup(label);
+}
+
+void ColorNotebook::_initUI(bool no_alpha)
+{
+ 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&& picker : get_color_pickers()) {
+ auto page = Page(std::move(picker.factory), picker.icon);
+ _addPage(page, no_alpha, picker.visibility_path);
+ }
+
+ _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();
+ Glib::ustring page_name = prefs->getString("/colorselector/page", "");
+ _setCurrentPage(getPageIndex(page_name), true);
+ row++;
+
+ _observer = prefs->createObserver("/colorselector/switcher", [=](const Preferences::Entry& new_value) {
+ _switcher->set_visible(!new_value.getBool());
+ _buttonbox->set_visible(new_value.getBool());
+ });
+ _observer->call();
+
+ 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);
+
+ // remember the page we switched to
+ _book->property_visible_child_name().signal_changed().connect([=]() {
+ // We don't want to remember auto cms selection
+ Glib::ustring name = _book->get_visible_child_name();
+ if (get_visible() && !name.empty() && name != "CMS") {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/colorselector/page", name);
+ }
+ });
+
+#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::_updateICCButtons()
+{
+ if (!_document) {
+ return;
+ }
+
+ 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.hasColorProfile());
+ gtk_widget_set_sensitive(_box_toomuchink, false);
+ gtk_widget_set_sensitive(_box_outofgamut, false);
+
+ if (color.hasColors()) {
+ auto name = color.getColorProfile();
+
+ // Set notebook page to cms if icc profile being used.
+ _setCurrentPage(getPageIndex("CMS"), true);
+
+ /* update out-of-gamut icon */
+ Inkscape::ColorProfile *target_profile =
+ _document->getProfileManager().find(name.c_str());
+ if (target_profile)
+ gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color));
+
+ /* update too-much-ink icon */
+ Inkscape::ColorProfile *prof = _document->getProfileManager().find(name.c_str());
+ if (prof && CMSSystem::isPrintColorSpace(prof)) {
+ gtk_widget_show(GTK_WIDGET(_box_toomuchink));
+ double ink_sum = 0;
+ for (double i : color.getColors()) {
+ 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));
+ }
+ } else {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ auto page = prefs->getString("/colorselector/page");
+ _setCurrentPage(getPageIndex(page), true);
+ }
+}
+
+int ColorNotebook::getPageIndex(const Glib::ustring &name)
+{
+ return getPageIndex(_book->get_child_by_name(name));
+}
+
+int ColorNotebook::getPageIndex(Gtk::Widget *widget)
+{
+ const auto pages = _book->get_children();
+ for (int i = 0; i < pages.size(); i++) {
+ if (pages[i] == widget) {
+ return i;
+ }
+ }
+ return 0;
+}
+
+void ColorNotebook::_setCurrentPage(int i, bool sync_combo)
+{
+ const auto pages = _book->get_children();
+
+ if (i >= pages.size()) {
+ // page index could be outside the valid range if we manipulate visible color pickers;
+ // default to the first page, so we show something
+ i = 0;
+ }
+
+ if (i >= 0 && i < pages.size()) {
+ _book->set_visible_child(*pages[i]);
+ if (sync_combo) {
+ _combo->set_active_by_id(i);
+ }
+ }
+}
+
+void ColorNotebook::_addPage(Page &page, bool no_alpha, const Glib::ustring vpath)
+{
+ if (auto selector_widget = page.selector_factory->createWidget(_selected_color, no_alpha)) {
+ 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);
+
+ auto prefs = Inkscape::Preferences::get();
+ auto obs = prefs->createObserver(vpath, [=](const Preferences::Entry& value) {
+ _combo->set_row_visible(page_num, value.getBool());
+ selector_widget->set_visible(value.getBool());
+ });
+ obs->call();
+ _visibility_observers.emplace_back(std::move(obs));
+ }
+}
+
+}
+}
+}
+
+/*
+ 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..6e805eb
--- /dev/null
+++ b/src/ui/widget/color-notebook.h
@@ -0,0 +1,108 @@
+// 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
+
+#include <memory>
+#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, bool no_alpha = false);
+ ~ColorNotebook() override;
+
+ void set_label(const Glib::ustring& label);
+
+protected:
+ struct Page {
+ Page(std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory, const char* icon);
+
+ std::unique_ptr<Inkscape::UI::ColorSelectorFactory> selector_factory;
+ Glib::ustring icon_name;
+ };
+
+ void _initUI(bool no_alpha);
+ void _addPage(Page &page, bool no_alpha, const Glib::ustring vpath);
+ void setDocument(SPDocument *document);
+
+ void _pickColor(ColorRGBA *color);
+ static void _onPickerClicked(GtkWidget *widget, ColorNotebook *colorbook);
+ virtual void _onSelectedColorChanged();
+ int getPageIndex(const Glib::ustring &name);
+ int getPageIndex(Gtk::Widget *widget);
+
+ 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 */
+ sigc::connection _onetimepick;
+ IconComboBox* _combo = nullptr;
+
+public:
+ // By default, disallow copy constructor and assignment operator
+ ColorNotebook(const ColorNotebook &obj) = delete;
+ ColorNotebook &operator=(const ColorNotebook &obj) = delete;
+
+ PrefObserver _observer;
+ std::vector<PrefObserver> _visibility_observers;
+
+ SPDocument *_document = nullptr;
+ sigc::connection _doc_replaced_connection;
+ sigc::connection _selection_connection;
+ sigc::connection _icc_changed_connection;
+};
+
+}
+}
+}
+#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..e85ac01
--- /dev/null
+++ b/src/ui/widget/color-palette.cpp
@@ -0,0 +1,724 @@
+// 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"
+#include "ui/dialog/color-item.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+ColorPalette::ColorPalette():
+ _builder(create_builder("color-palette.glade")),
+ _normal_box(get_widget<Gtk::FlowBox>(_builder, "flow-box")),
+ _pinned_box(get_widget<Gtk::FlowBox>(_builder, "pinned")),
+ _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();
+ });
+
+ 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();
+
+ auto& large = get_widget<Gtk::CheckButton>(_builder, "enlarge");
+ large.set_active(_large_pinned_panel);
+ large.signal_toggled().connect([=,&large](){
+ _set_large_pinned_panel(large.get_active());
+ _signal_settings_changed.emit();
+ });
+ update_checkbox();
+
+ auto& sl = get_widget<Gtk::CheckButton>(_builder, "show-labels");
+ sl.set_visible(false);
+ sl.set_active(_show_labels);
+ sl.signal_toggled().connect([=,&sl](){
+ _show_labels = sl.get_active();
+ _signal_settings_changed.emit();
+ rebuild_widgets();
+ });
+
+ _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);
+ _normal_box.get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ _pinned_box.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);
+ get_widget<Gtk::CheckButton>(_builder, "enlarge").set_visible(compact);
+ get_widget<Gtk::CheckButton>(_builder, "show-labels").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;
+ _normal_box.set_halign(enable ? Gtk::ALIGN_FILL : Gtk::ALIGN_START);
+ update_stretch();
+ set_up_scrolling();
+}
+
+void ColorPalette::enable_labels(bool labels) {
+ auto& sl = get_widget<Gtk::CheckButton>(_builder, "show-labels");
+ sl.set_active(labels);
+ _show_labels = labels;
+}
+
+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");
+ auto normal_count = std::max(1, static_cast<int>(_normal_box.get_children().size()));
+ auto pinned_count = std::max(1, static_cast<int>(_pinned_box.get_children().size()));
+
+ _normal_box.set_max_children_per_line(_show_labels ? 1 : normal_count);
+ _normal_box.set_min_children_per_line(1);
+ _pinned_box.set_max_children_per_line(_show_labels ? 1 : pinned_count);
+ _pinned_box.set_min_children_per_line(1);
+
+ 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);
+ _normal_box.set_valign(Gtk::ALIGN_END);
+
+ if (_rows == 1 && _force_scrollbar) {
+ // horizontal scrolling with single row
+ _normal_box.set_min_children_per_line(normal_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);
+ _scroll_left.hide();
+ _scroll_right.hide();
+ _scroll_btn.show();
+ }
+
+ int div = _large_pinned_panel ? (_rows > 2 ? 2 : 1) : _rows;
+ _pinned_box.set_max_children_per_line(std::max((pinned_count + div - 1) / div, 1));
+ _pinned_box.set_margin_end(_border);
+ }
+ 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();
+
+ _normal_box.set_valign(Gtk::ALIGN_START);
+ _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::set_large_pinned_panel(bool large) {
+ auto& checkbox = get_widget<Gtk::CheckButton>(_builder, "enlarge");
+ checkbox.set_active(large);
+ _set_large_pinned_panel(large);
+}
+
+void ColorPalette::_set_large_pinned_panel(bool large) {
+ if (_large_pinned_panel == large) return;
+
+ _large_pinned_panel = large;
+ set_up_scrolling();
+}
+
+bool ColorPalette::is_pinned_panel_large() const {
+ return _large_pinned_panel;
+}
+
+bool ColorPalette::are_labels_enabled() const {
+ return _show_labels;
+}
+
+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);
+ }
+
+ _normal_box.set_column_spacing(_border);
+ _normal_box.set_row_spacing(_border);
+ _pinned_box.set_column_spacing(_border);
+ _pinned_box.set_row_spacing(_border);
+
+ double scale = _show_labels ? 2.0 : 1.0;
+
+ int width = get_tile_width() * scale;
+ int height = get_tile_height() * scale;
+ for (auto item : _normal_items) {
+ item->set_size_request(width, height);
+ }
+
+ int pinned_width = width;
+ int pinned_height = height;
+ if (_large_pinned_panel) {
+ double mult = _rows > 2 ? _rows / 2.0 : 2.0;
+ pinned_width = pinned_height = static_cast<int>((height + _border) * mult - _border);
+ }
+ for (auto item : _pinned_items) {
+ item->set_size_request(pinned_width, pinned_height);
+ }
+}
+
+void free_colors(Gtk::FlowBox& flowbox) {
+ for (auto widget : flowbox.get_children()) {
+ if (widget) {
+ flowbox.remove(*widget);
+ }
+ }
+}
+
+void ColorPalette::set_colors(std::vector<Dialog::ColorItem*> const &swatches)
+{
+ _normal_items.clear();
+ _pinned_items.clear();
+
+ for (auto item : swatches) {
+ if (item->is_pinned()) {
+ _pinned_items.emplace_back(item);
+ } else {
+ _normal_items.emplace_back(item);
+ }
+ item->signal_modified().connect([=] {
+ item->get_parent()->foreach([=](Gtk::Widget& w) {
+ if (auto label = dynamic_cast<Gtk::Label *>(&w)) {
+ label->set_text(item->get_description());
+ }
+ });
+ });
+ }
+ rebuild_widgets();
+}
+
+Gtk::Widget *ColorPalette::_get_widget(Dialog::ColorItem *item) {
+ if (auto parent = item->get_parent()) {
+ parent->remove(*item);
+ }
+ if (_show_labels) {
+ item->set_valign(Gtk::ALIGN_CENTER);
+ auto box = Gtk::make_managed<Gtk::Box>();
+ auto label = Gtk::make_managed<Gtk::Label>(item->get_description());
+ box->add(*item);
+ box->add(*label);
+ return box;
+ }
+ return Gtk::manage(item);
+}
+
+void ColorPalette::rebuild_widgets()
+{
+ _normal_box.freeze_notify();
+ _normal_box.freeze_child_notify();
+ _pinned_box.freeze_notify();
+ _pinned_box.freeze_child_notify();
+
+ free_colors(_normal_box);
+ free_colors(_pinned_box);
+
+ for (auto item : _normal_items) {
+ _normal_box.add(*_get_widget(item));
+ }
+ for (auto item : _pinned_items) {
+ _pinned_box.add(*_get_widget(item));
+ }
+
+ _normal_box.show_all();
+ _pinned_box.show_all();
+
+ set_up_scrolling();
+
+ _normal_box.thaw_child_notify();
+ _normal_box.thaw_notify();
+ _pinned_box.thaw_child_notify();
+ _pinned_box.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..1f20d24
--- /dev/null
+++ b/src/ui/widget/color-palette.h
@@ -0,0 +1,130 @@
+// 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 Dialog {
+ class ColorItem;
+ };
+
+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(std::vector<Dialog::ColorItem*> const &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);
+ // enlarge color tiles in a pinned panel
+ void set_large_pinned_panel(bool large);
+
+ 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);
+ // Show labels in swatches dialog
+ void enable_labels(bool labels);
+
+ 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;
+ bool is_pinned_panel_large() const;
+ bool are_labels_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(Gtk::FlowBox& box);
+ 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);
+ void _set_large_pinned_panel(bool large);
+ 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;
+
+ Gtk::Widget *_get_widget(Dialog::ColorItem *item);
+ void rebuild_widgets();
+
+ std::vector<Dialog::ColorItem *> _normal_items;
+ std::vector<Dialog::ColorItem *> _pinned_items;
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::FlowBox& _normal_box;
+ Gtk::FlowBox& _pinned_box;
+ 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;
+ 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
+ bool _large_pinned_panel = false;
+ bool _show_labels = false;
+};
+
+}}} // 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..5794065
--- /dev/null
+++ b/src/ui/widget/color-picker.cpp
@@ -0,0 +1,173 @@
+// 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, _ignore_transparency));
+ 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;
+ _rgba = rgba;
+ _changed_signal.emit(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);
+}
+
+guint32 ColorPicker::get_current_color() const {
+ return _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..61b3834
--- /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);
+ guint32 get_current_color() const;
+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..5a2fe42
--- /dev/null
+++ b/src/ui/widget/color-scales.cpp
@@ -0,0 +1,1247 @@
+// 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 <glibmm/ustring.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/grid.h>
+#include <glibmm/i18n.h>
+#include <functional>
+#include <memory>
+#include <stdexcept>
+#include <vector>
+
+#include "ui/dialog-events.h"
+#include "ui/selected-color.h"
+#include "ui/widget/color-scales.h"
+#include "ui/widget/color-slider.h"
+#include "ui/widget/color-icc-selector.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/icon-loader.h"
+#include "oklab.h"
+#include "preferences.h"
+
+#include "ui/widget/ink-color-wheel.h"
+#include "ui/widget/oklab-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);
+
+static const char* color_mode_icons[] = {
+ nullptr,
+ "color-selector-rgb",
+ "color-selector-hsx",
+ "color-selector-cmyk",
+ "color-selector-hsx",
+ "color-selector-hsluv",
+ "color-selector-okhsl",
+ "color-selector-cms",
+ nullptr
+};
+
+const char* color_mode_name[] = {
+ N_("None"), N_("RGB"), N_("HSL"), N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL"), N_("CMS"), nullptr
+};
+
+const char* get_color_mode_icon(SPColorScalesMode mode) {
+ auto index = static_cast<size_t>(mode);
+ assert(index > 0 && index < (sizeof(color_mode_icons) / sizeof(color_mode_icons[0])));
+ return color_mode_icons[index];
+}
+
+const char* get_color_mode_label(SPColorScalesMode mode) {
+ auto index = static_cast<size_t>(mode);
+ assert(index > 0 && index < (sizeof(color_mode_name) / sizeof(color_mode_name[0])));
+ return color_mode_name[index];
+}
+
+std::unique_ptr<Inkscape::UI::ColorSelectorFactory> get_factory(SPColorScalesMode mode) {
+ switch (mode) {
+ case SPColorScalesMode::RGB: return std::make_unique<ColorScalesFactory<SPColorScalesMode::RGB>>();
+ case SPColorScalesMode::HSL: return std::make_unique<ColorScalesFactory<SPColorScalesMode::HSL>>();
+ case SPColorScalesMode::HSV: return std::make_unique<ColorScalesFactory<SPColorScalesMode::HSV>>();
+ case SPColorScalesMode::CMYK: return std::make_unique<ColorScalesFactory<SPColorScalesMode::CMYK>>();
+ case SPColorScalesMode::HSLUV: return std::make_unique<ColorScalesFactory<SPColorScalesMode::HSLUV>>();
+ case SPColorScalesMode::OKLAB: return std::make_unique<ColorScalesFactory<SPColorScalesMode::OKLAB>>();
+ case SPColorScalesMode::CMS: return std::make_unique<ColorICCSelectorFactory>();
+ default:
+ throw std::invalid_argument("There's no factory for the requested color mode");
+ }
+}
+
+std::vector<ColorPickerDescription> get_color_pickers() {
+ std::vector<ColorPickerDescription> pickers;
+
+ for (auto mode : {
+ SPColorScalesMode::HSL,
+ SPColorScalesMode::HSV,
+ SPColorScalesMode::RGB,
+ SPColorScalesMode::CMYK,
+ SPColorScalesMode::OKLAB,
+ SPColorScalesMode::HSLUV,
+ SPColorScalesMode::CMS
+ }) {
+ auto label = get_color_mode_label(mode);
+
+ pickers.emplace_back(ColorPickerDescription {
+ mode,
+ get_color_mode_icon(mode),
+ label,
+ Glib::ustring::format("/colorselector/", label, "/visible"),
+ get_factory(mode)
+ });
+ }
+
+ return pickers;
+}
+
+
+template <SPColorScalesMode MODE>
+gchar const *ColorScales<MODE>::SUBMODE_NAMES[] = { N_("None"), N_("RGB"), N_("HSL"),
+ N_("CMYK"), N_("HSV"), N_("HSLuv"), N_("OKHSL") };
+
+// 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 <>
+gchar const * const ColorScales<SPColorScalesMode::OKLAB>::_pref_wheel_visibility =
+ "/wheel_vis_okhsl";
+
+template <SPColorScalesMode MODE>
+ColorScales<MODE>::ColorScales(SelectedColor &color, bool no_alpha)
+ : 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(no_alpha);
+
+ _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(bool no_alpha)
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ Gtk::Expander *wheel_frame = nullptr;
+
+ if constexpr (
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV ||
+ MODE == SPColorScalesMode::OKLAB)
+ {
+ /* Create wheel */
+ if constexpr (MODE == SPColorScalesMode::HSLUV) {
+ _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheelHSLuv());
+ } else if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ _wheel = Gtk::make_managed<OKWheel>();
+ } 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(no_alpha);
+
+ if constexpr (
+ MODE == SPColorScalesMode::HSL ||
+ MODE == SPColorScalesMode::HSV ||
+ MODE == SPColorScalesMode::HSLUV ||
+ MODE == SPColorScalesMode::OKLAB)
+ {
+ // 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 ||
+ MODE == SPColorScalesMode::OKLAB)
+ {
+ _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.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 if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ color.get_rgb_floatv(tmp);
+ // OKLab color space is more sensitive to numerical errors; use doubles.
+ auto const hsl = Oklab::oklab_to_okhsl(Oklab::rgb_to_oklab({tmp[0], tmp[1], tmp[2]}));
+ _updating = true;
+ for (size_t i : {0, 1, 2}) {
+ setScaled(_a[i], hsl[i]);
+ }
+ setScaled(_a[3], _color.alpha());
+ setScaled(_a[4], 0.0);
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ if (update_wheel) {
+ _wheel->setRgb(tmp[0], tmp[1], tmp[2]);
+ }
+ return;
+ } 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>
+double ColorScales<MODE>::getScaled(Glib::RefPtr<Gtk::Adjustment> const &a)
+{
+ return a->get_value() / a->get_upper();
+}
+
+template <SPColorScalesMode MODE>
+void ColorScales<MODE>::setScaled(Glib::RefPtr<Gtk::Adjustment> &a, double v, bool constrained)
+{
+ auto upper = a->get_upper();
+ double 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 if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ auto const tmp = Oklab::oklab_to_rgb(
+ Oklab::okhsl_to_oklab({ getScaled(_a[0]),
+ getScaled(_a[1]),
+ getScaled(_a[2]) }));
+ for (size_t i : {0, 1, 2}) {
+ rgba[i] = static_cast<float>(tmp[i]);
+ }
+ 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::OKLAB) {
+ auto const tmp = Oklab::oklab_to_rgb(
+ Oklab::okhsl_to_oklab({ getScaled(_a[0]),
+ getScaled(_a[1]),
+ getScaled(_a[2]) }));
+ SPColor::rgb_to_cmyk_floatv(cmyka, (float)tmp[0], (float)tmp[1], (float)tmp[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(bool no_alpha)
+{
+ gfloat rgba[4];
+ gfloat c[4];
+ int alpha_index = 0;
+
+ 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"));
+ alpha_index = 3;
+ _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"));
+
+ alpha_index = 3;
+ _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"));
+
+ alpha_index = 3;
+ _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"));
+
+ alpha_index = 4;
+ _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"));
+
+ alpha_index = 3;
+ _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 if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ _setRangeLimit(100.0);
+
+ _l[0]->set_markup_with_mnemonic(_("_H<sub>OK</sub>:"));
+ _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<sub>OK</sub>:"));
+ _s[1]->set_tooltip_text(_("Saturation"));
+ _b[1]->set_tooltip_text(_("Saturation"));
+
+ _l[2]->set_markup_with_mnemonic(_("_L<sub>OK</sub>:"));
+ _s[2]->set_tooltip_text(_("Lightness"));
+ _b[2]->set_tooltip_text(_("Lightness"));
+
+ alpha_index = 3;
+ _l[3]->set_markup_with_mnemonic(_("_A:"));
+ _s[3]->set_tooltip_text(_("Alpha (opacity)"));
+ _b[3]->set_tooltip_text(_("Alpha (opacity)"));
+
+ _l[4]->hide();
+ _s[4]->hide();
+ _b[4]->hide();
+ _updating = true;
+
+ auto const tmp = Oklab::oklab_to_okhsl(Oklab::rgb_to_oklab({rgba[0], rgba[1], rgba[2]}));
+ for (size_t i : {0, 1, 2}) {
+ setScaled(_a[i], tmp[i]);
+ }
+ setScaled(_a[3], rgba[3]);
+
+ _updateSliders(CSC_CHANNELS_ALL);
+ _updating = false;
+ } else {
+ g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__);
+ }
+
+ if (no_alpha && alpha_index > 0) {
+ _l[alpha_index]->hide();
+ _s[alpha_index]->hide();
+ _b[alpha_index]->hide();
+ _l[alpha_index]->set_no_show_all(true);
+ _s[alpha_index]->set_no_show_all(true);
+ _b[alpha_index]->set_no_show_all(true);
+ }
+}
+
+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.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<double, 4> const adj = [this]() -> std::array<double, 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 if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ if (channels != CSC_CHANNEL_H && channels != CSC_CHANNEL_A) {
+ _s[0]->setMap(Oklab::render_hue_scale(adj[1], adj[2], &_sliders_maps[0]));
+ }
+ if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) {
+ _s[1]->setMap(Oklab::render_saturation_scale(360.0 * adj[0], adj[2], &_sliders_maps[1]));
+ }
+ if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) {
+ _s[2]->setMap(Oklab::render_lightness_scale(360.0 * adj[0], adj[1], &_sliders_maps[2]));
+ }
+ if (channels != CSC_CHANNEL_A) { // Update the alpha gradient.
+ auto const rgb = Oklab::oklab_to_rgb(
+ Oklab::okhsl_to_oklab({ getScaled(_a[0]),
+ getScaled(_a[1]),
+ getScaled(_a[2]) }));
+ _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 0.0),
+ SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 0.5),
+ SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[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;
+ }
+}
+
+// TODO: consider turning this into a generator (without memory allocation).
+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, bool no_alpha) const
+{
+ Gtk::Widget *w = Gtk::manage(new ColorScales<MODE>(color, no_alpha));
+ 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 if constexpr (MODE == SPColorScalesMode::OKLAB) {
+ return gettext(ColorScales<>::SUBMODE_NAMES[6]);
+ } 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 ColorScales<SPColorScalesMode::OKLAB>;
+
+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>;
+template class ColorScalesFactory<SPColorScalesMode::OKLAB>;
+
+} // 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..0ded543
--- /dev/null
+++ b/src/ui/widget/color-scales.h
@@ -0,0 +1,141 @@
+// 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 <vector>
+
+#include "ui/selected-color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorSlider;
+class ColorWheel;
+
+enum class SPColorScalesMode {
+ NONE,
+ RGB,
+ HSL,
+ CMYK,
+ HSV,
+ HSLUV,
+ OKLAB,
+ CMS
+};
+
+template <SPColorScalesMode MODE = SPColorScalesMode::NONE>
+class ColorScales
+ : public Gtk::Box
+{
+public:
+ static gchar const *SUBMODE_NAMES[];
+
+ static double getScaled(Glib::RefPtr<Gtk::Adjustment> const &a);
+ static void setScaled(Glib::RefPtr<Gtk::Adjustment> &a, double v, bool constrained = false);
+
+ ColorScales(SelectedColor &color, bool no_alpha);
+ ~ColorScales() override;
+
+ void setupMode(bool no_alpha);
+ 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;
+
+ void _initUI(bool no_alpha);
+
+ 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;
+
+public:
+ // 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, bool no_alpha) const override;
+ Glib::ustring modeName() const override;
+};
+
+struct ColorPickerDescription
+{
+ SPColorScalesMode mode;
+ const char* icon;
+ const char* label;
+ Glib::ustring visibility_path;
+ std::unique_ptr<Inkscape::UI::ColorSelectorFactory> factory;
+};
+
+std::vector<ColorPickerDescription> get_color_pickers();
+
+} // 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..2b71e6c
--- /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..8257409
--- /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..94efec8
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.cpp
@@ -0,0 +1,725 @@
+// 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 "libnrtype/font-lister.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)
+{
+ 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);
+ // 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
+ gtk_cell_renderer_set_fixed_size(_cell, -1, height);
+ } else {
+#if !PANGO_VERSION_CHECK(1,50,0)
+ gtk_cell_renderer_set_fixed_size(_cell, -1, height);
+#endif
+ }
+ 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(_combobox ), _cell,
+ GtkCellLayoutDataFunc (_cell_data_func), nullptr, nullptr );
+ g_signal_connect(G_OBJECT(comboBoxEntry), "popup", G_CALLBACK(combo_box_popup_cb), this);
+ }
+
+ // 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();
+ }
+}
+
+static gboolean add_more_font_families_idle(gpointer user_data)
+{
+ FontLister* fl = FontLister::get_instance();
+ static int q = 1;
+ static unsigned recurse_times = fl->get_font_families_size() / FONT_FAMILIES_GROUP_SIZE;
+
+ fl->init_font_families(q, FONT_FAMILIES_GROUP_SIZE);
+ if (q < recurse_times)
+ gdk_threads_add_idle (add_more_font_families_idle, NULL);
+ q++;
+ return false;
+}
+
+gboolean ComboBoxEntryToolItem::combo_box_popup_cb(ComboBoxEntryToolItem *widget, gpointer data)
+{
+ auto action = reinterpret_cast<ComboBoxEntryToolItem *>( data );
+ g_idle_add(ComboBoxEntryToolItem::set_cell_markup, action);
+ return true;
+}
+
+gboolean ComboBoxEntryToolItem::set_cell_markup(gpointer data)
+{
+ ComboBoxEntryToolItem *self = static_cast<ComboBoxEntryToolItem *>(data);
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT( self->_combobox ), self->_cell,
+ GtkCellLayoutDataFunc (self->_cell_data_func), self, nullptr );
+ return false;
+}
+
+
+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..61ecb9f
--- /dev/null
+++ b/src/ui/widget/combo-box-entry-tool-item.h
@@ -0,0 +1,153 @@
+// 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;
+
+ // 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 gboolean set_cell_markup(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..1b0112a
--- /dev/null
+++ b/src/ui/widget/combo-enums.h
@@ -0,0 +1,232 @@
+// 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
+{
+public:
+ ComboBoxEnum(E default_value, const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true, const char* translation_context = nullptr) :
+ ComboBoxEnum(c, a, sort, translation_context, static_cast<unsigned int>(default_value))
+ {
+ set_active_by_id(default_value);
+ sort_items();
+ }
+
+ ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttr a = SPAttr::INVALID, bool sort = true, const char* translation_context = nullptr) :
+ ComboBoxEnum(c, a, sort, translation_context, 0)
+ {
+ set_active(0);
+ sort_items();
+ }
+
+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;
+
+ ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttr a, bool sort, const char* translation_context, unsigned int default_value)
+ : AttrWidget(a, 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;
+ auto label = _converter.get_label(data->id);
+ auto trans = translation_context ?
+ g_dpgettext2(nullptr, translation_context, label.c_str()) :
+ gettext(label.c_str());
+ row[_columns.label] = trans;
+ row[_columns.is_separator] = _converter.get_key(data->id) == "-";
+ }
+ set_row_separator_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::combo_separator_func));
+ }
+
+ void sort_items() {
+ // 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);
+ }
+ }
+
+public:
+ 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;
+ auto index = get_active_by_id(id);
+ if (index >= 0) {
+ set_active(index);
+ }
+ };
+
+ 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 combo_separator_func(const Glib::RefPtr<Gtk::TreeModel>& model,
+ const Gtk::TreeModel::iterator& iter) {
+ return (*iter)[_columns.is_separator];
+ };
+
+ bool setProgrammatically;
+
+private:
+ int get_active_by_id(E id) const {
+ int index = 0;
+ for (auto&& child : _model->children()) {
+ const Util::EnumData<E>* data = child[_columns.data];
+ if (data->id == id) {
+ return index;
+ }
+ ++index;
+ }
+ return -1;
+ };
+
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns()
+ {
+ add(data);
+ add(label);
+ add(is_separator);
+ }
+
+ Gtk::TreeModelColumn<const Util::EnumData<E>*> data;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<bool> is_separator;
+ };
+
+ 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..74a38ee
--- /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/completion-popup.cpp b/src/ui/widget/completion-popup.cpp
new file mode 100644
index 0000000..1522d4f
--- /dev/null
+++ b/src/ui/widget/completion-popup.cpp
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "completion-popup.h"
+#include <cassert>
+#include <glibmm/ustring.h>
+#include <gtkmm/entrycompletion.h>
+#include <gtkmm/searchentry.h>
+#include "ui/builder-utils.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+enum Columns {
+ ColID = 0,
+ ColName,
+ ColIcon,
+ ColSearch
+};
+
+CompletionPopup::CompletionPopup() :
+ _builder(create_builder("completion-box.glade")),
+ _search(get_widget<Gtk::SearchEntry>(_builder, "search")),
+ _button(get_widget<Gtk::MenuButton>(_builder, "menu-btn")),
+ _completion(get_object<Gtk::EntryCompletion>(_builder, "completion")),
+ _popup(get_widget<Gtk::Menu>(_builder, "popup"))
+{
+ _list = Glib::RefPtr<Gtk::ListStore>::cast_dynamic(_builder->get_object("list"));
+ assert(_list);
+
+ add(get_widget<Gtk::Box>(_builder, "main-box"));
+
+ _completion->set_match_func([=](const Glib::ustring& text, const Gtk::TreeModel::const_iterator& it){
+ Glib::ustring str;
+ it->get_value(ColSearch, str);
+ if (str.empty()) {
+ return false;
+ }
+ return str.lowercase().find(text.lowercase()) != Glib::ustring::npos;
+ });
+
+ // clear search box without triggering completion popup menu
+ auto clear = [=]() { _search.get_buffer()->set_text(Glib::ustring()); };
+
+ _completion->signal_match_selected().connect([=](const Gtk::TreeModel::iterator& it){
+ int id;
+ it->get_value(ColID, id);
+ _match_selected.emit(id);
+ clear();
+ return true;
+ }, false);
+
+ _search.signal_focus_in_event().connect([=](GdkEventFocus*){
+ _on_focus.emit();
+ clear();
+ return false;
+ });
+ _button.signal_button_press_event().connect([=](GdkEventButton*){
+ _button_press.emit();
+ clear();
+ return false;
+ }, false);
+ _search.signal_focus_out_event().connect([=](GdkEventFocus*){
+ clear();
+ return false;
+ });
+
+ _search.signal_stop_search().connect([=](){
+ clear();
+ });
+
+ show();
+}
+
+void CompletionPopup::clear_completion_list() {
+ _list->clear();
+}
+
+void CompletionPopup::add_to_completion_list(int id, Glib::ustring name, Glib::ustring icon_name, Glib::ustring search_text) {
+ auto row = *_list->append();
+ row.set_value(ColID, id);
+ row.set_value(ColName, name);
+ row.set_value(ColIcon, icon_name);
+ row.set_value(ColSearch, search_text.empty() ? name : search_text);
+}
+
+Gtk::Menu& CompletionPopup::get_menu() {
+ return _popup;
+}
+
+Gtk::SearchEntry& CompletionPopup::get_entry() {
+ return _search;
+}
+
+sigc::signal<void (int)>& CompletionPopup::on_match_selected() {
+ return _match_selected;
+}
+
+sigc::signal<void ()>& CompletionPopup::on_button_press() {
+ return _button_press;
+}
+
+sigc::signal<bool ()>& CompletionPopup::on_focus() {
+ return _on_focus;
+}
+
+
+}}} // namespaces
diff --git a/src/ui/widget/completion-popup.h b/src/ui/widget/completion-popup.h
new file mode 100644
index 0000000..b7b77a1
--- /dev/null
+++ b/src/ui/widget/completion-popup.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+
+#ifndef INKSCAPE_UI_WIDGET_COMPLETION_POPUP_H
+#define INKSCAPE_UI_WIDGET_COMPLETION_POPUP_H
+
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/entrycompletion.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/searchentry.h>
+#include <sigc++/connection.h>
+#include "labelled.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class CompletionPopup : public Gtk::Box {
+public:
+ CompletionPopup();
+
+ Gtk::Menu& get_menu();
+ Gtk::SearchEntry& get_entry();
+ Glib::RefPtr<Gtk::ListStore> get_list();
+
+ void clear_completion_list();
+ void add_to_completion_list(int id, Glib::ustring name, Glib::ustring icon_name, Glib::ustring search_text = Glib::ustring());
+
+ sigc::signal<void (int)>& on_match_selected();
+ sigc::signal<void ()>& on_button_press();
+ sigc::signal<bool ()>& on_focus();
+
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Glib::RefPtr<Gtk::ListStore> _list;
+ Gtk::SearchEntry& _search;
+ Gtk::MenuButton& _button;
+ Gtk::Menu& _popup;
+ Glib::RefPtr<Gtk::EntryCompletion> _completion;
+ sigc::signal<void (int)> _match_selected;
+ sigc::signal<void ()> _button_press;
+ sigc::signal<bool ()> _on_focus;
+};
+
+}}} // namespaces
+
+#endif // INKSCAPE_UI_WIDGET_COMPLETION_POPUP_H
diff --git a/src/ui/widget/custom-tooltip.cpp b/src/ui/widget/custom-tooltip.cpp
new file mode 100644
index 0000000..c77082e
--- /dev/null
+++ b/src/ui/widget/custom-tooltip.cpp
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "custom-tooltip.h"
+#include "gtkmm/box.h"
+#include "gtkmm/label.h"
+#include "gtkmm/image.h"
+#include <ctime>
+#include <chrono>
+#include <gdk/gdk.h>
+
+static gint timeoutid = -1;
+
+static
+gboolean
+delaytooltip (gpointer data)
+{
+ GdkDisplay *display = reinterpret_cast<GdkDisplay *>(data);
+ gtk_tooltip_trigger_tooltip_query(display);
+ return true;
+}
+
+void sp_clear_custom_tooltip()
+{
+ if (timeoutid != -1) {
+ g_source_remove(timeoutid);
+ timeoutid = -1;
+ }
+}
+
+bool
+sp_query_custom_tooltip(int x, int y, bool keyboard_tooltip, const Glib::RefPtr<Gtk::Tooltip>& tooltipw, gint id, Glib::ustring tooltip, Glib::ustring icon, Gtk::IconSize iconsize, int delaytime)
+{
+ sp_clear_custom_tooltip();
+
+ static gint last = -1;
+ static auto start = std::chrono::steady_clock::now();
+ auto end = std::chrono::steady_clock::now();
+ if (last != id) {
+ start = std::chrono::steady_clock::now();
+ last = id;
+ }
+ Gtk::Box *box = Gtk::make_managed<Gtk::Box>();
+ Gtk::Label *label = Gtk::make_managed<Gtk::Label>();
+ label->set_line_wrap(true);
+ label->set_markup(tooltip);
+ label->set_max_width_chars(40);
+ if (icon != "") {
+ box->pack_start(*Gtk::make_managed<Gtk::Image>(icon, iconsize), true, true, 2);
+ }
+ box->pack_start(*label, true, true, 2);
+ tooltipw->set_custom(*box);
+ box->get_style_context()->add_class("symbolic");
+ box->show_all_children();
+ auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
+ if (elapsed.count() / delaytime < 0.5) {
+ GdkDisplay *display = gdk_display_get_default();
+ if (display) {
+ timeoutid = g_timeout_add(501-elapsed.count(), delaytooltip, display);
+ }
+ }
+ return elapsed.count() / delaytime > 0.5;
+}
diff --git a/src/ui/widget/custom-tooltip.h b/src/ui/widget/custom-tooltip.h
new file mode 100644
index 0000000..32647bc
--- /dev/null
+++ b/src/ui/widget/custom-tooltip.h
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_WIDGET_CUSTOM_TOOLTIP_H
+#define INKSCAPE_UI_WIDGET_CUSTOM_TOOLTIP_H
+
+#include <gtkmm/tooltip.h>
+
+void sp_clear_custom_tooltip();
+
+bool
+sp_query_custom_tooltip(
+ int x,
+ int y,
+ bool keyboard_tooltip,
+ const Glib::RefPtr<Gtk::Tooltip>& tooltipw,
+ gint id,
+ Glib::ustring tooltip,
+ Glib::ustring icon = "",
+ Gtk::IconSize iconsize = Gtk::ICON_SIZE_DIALOG,
+ int delaytime = 1000.0);
+
+#endif // INKSCAPE_UI_WIDGET_CUSTOM_TOOLTIP_H
diff --git a/src/ui/widget/dash-selector.cpp b/src/ui/widget/dash-selector.cpp
new file mode 100644
index 0000000..016025f
--- /dev/null
+++ b/src/ui/widget/dash-selector.cpp
@@ -0,0 +1,260 @@
+// 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
+ // TRANSLATORS: 'Custom' here means, that user-defined dash pattern is specified in an entry box
+ surface = sp_text_to_pixbuf(_("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..21b370a
--- /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..cfff80f
--- /dev/null
+++ b/src/ui/widget/entity-entry.cpp
@@ -0,0 +1,220 @@
+// 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, bool read_only)
+{
+ 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();
+ if (!read_only) {
+ 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);
+}
+
+Glib::ustring EntityLineEntry::content() const {
+ return static_cast<Gtk::Entry*>(_packable)->get_text();
+}
+
+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));
+}
+
+Glib::ustring EntityMultiLineEntry::content() const {
+ return _v.get_buffer()->get_text();
+}
+
+EntityMultiLineEntry::~EntityMultiLineEntry()
+{
+ delete static_cast<Gtk::ScrolledWindow*>(_packable);
+}
+
+void EntityMultiLineEntry::update(SPDocument* doc, bool read_only)
+{
+ 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();
+ if (!read_only) {
+ 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..7fd243b
--- /dev/null
+++ b/src/ui/widget/entity-entry.h
@@ -0,0 +1,89 @@
+// 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 <glibmm/ustring.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, bool read_only) = 0;
+ virtual void on_changed() = 0;
+ virtual void load_from_preferences() = 0;
+ virtual Glib::ustring content() const = 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, bool read_only) override;
+ void load_from_preferences() override;
+ Glib::ustring content() const 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, bool read_only) override;
+ void load_from_preferences() override;
+ Glib::ustring content() const 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..857bc00
--- /dev/null
+++ b/src/ui/widget/export-lists.cpp
@@ -0,0 +1,321 @@
+// 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"
+#include "ui/builder-utils.h"
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ExtensionList::ExtensionList()
+{
+ init();
+}
+
+ExtensionList::ExtensionList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>(cobject, refGlade)
+{
+ init();
+}
+
+ExtensionList::~ExtensionList()
+{
+ _popover_signal.disconnect();
+}
+
+void ExtensionList::init()
+{
+ _builder = create_builder("dialog-export-prefs.glade");
+ _builder->get_widget("pref_button", _pref_button);
+ _builder->get_widget("pref_popover", _pref_popover);
+ _builder->get_widget("pref_holder", _pref_holder);
+
+ _popover_signal = _pref_popover->signal_show().connect([=]() {
+ _pref_holder->remove();
+ if (auto ext = getExtension()) {
+ if (auto gui = ext->autogui(nullptr, nullptr)) {
+ _pref_holder->add(*gui);
+ _pref_popover->grab_focus();
+ }
+ }
+ });
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _watch_pref = prefs->createObserver("/dialogs/export/show_all_extensions", [=]() { setup(); });
+}
+
+void ExtensionList::on_changed()
+{
+ bool has_prefs = false;
+ if (auto ext = getExtension()) {
+ has_prefs = (ext->widget_visible_count() > 0);
+ }
+ _pref_button->set_sensitive(has_prefs);
+}
+
+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, 2, 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);
+ this->attach(*extension->getPrefButton(), _prefs_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->getExtension();
+}
+
+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..67108f3
--- /dev/null
+++ b/src/ui/widget/export-lists.h
@@ -0,0 +1,114 @@
+// 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;
+
+ void setup();
+ Glib::ustring getFileExtension();
+ void setExtensionFromFilename(Glib::ustring const &filename);
+ void removeExtension(Glib::ustring &filename);
+ void createList();
+ Gtk::MenuButton *getPrefButton() const { return _pref_button; }
+ Inkscape::Extension::Output *getExtension();
+
+private:
+ void init();
+ void on_changed() override;
+
+ PrefObserver _watch_pref;
+ std::map<std::string, Inkscape::Extension::Output *> ext_to_mod;
+
+ sigc::connection _popover_signal;
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::MenuButton *_pref_button = nullptr;
+ Gtk::Popover *_pref_popover = nullptr;
+ Gtk::Viewport *_pref_holder = nullptr;
+};
+
+class ExportList : public Gtk::Grid
+{
+public:
+ ExportList() = default;
+ ExportList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &)
+ : 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 _prefs_col = 2;
+ int _dpi_col = 3;
+ int _delete_col = 4;
+};
+
+} // 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..cf99258
--- /dev/null
+++ b/src/ui/widget/export-preview.cpp
@@ -0,0 +1,208 @@
+// 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 "document.h"
+#include "display/cairo-utils.h"
+#include "object/sp-item.h"
+#include "object/sp-root.h"
+#include "util/preview.h"
+#include "io/resource.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * A preview drawing object is responsible for constructing a drawing and showing it's contents
+ *
+ * On destruction it will gracefully invoke hide itself. You should destroy this object when
+ * you need to change the document object being used for the preview.
+ */
+PreviewDrawing::PreviewDrawing(SPDocument *doc)
+{
+ _document = doc;
+}
+
+PreviewDrawing::~PreviewDrawing()
+{
+ destruct();
+ _document = nullptr;
+}
+
+void PreviewDrawing::destruct()
+{
+ if (!_visionkey)
+ return;
+
+ // On exiting the document root might have gone already.
+ if (auto root = _document->getRoot()) {
+ root->invoke_hide(_visionkey);
+ }
+ _drawing.reset();
+ _visionkey = 0;
+}
+
+/**
+ * Construct the drawing, when needed
+ */
+void PreviewDrawing::construct()
+{
+ auto drawing = std::make_shared<Inkscape::Drawing>();
+ _visionkey = SPItem::display_key_new(1);
+ if (auto di = _document->getRoot()->invoke_show(*drawing, _visionkey, SP_ITEM_SHOW_DISPLAY)) {
+ drawing->setRoot(di);
+ } else {
+ drawing.reset();
+ }
+
+ if (!_shown_items.empty()) {
+ _document->getRoot()->invoke_hide_except(_visionkey, _shown_items);
+ }
+ _drawing = drawing;
+}
+
+/**
+ * Render the drawing into a cairo image surface.
+ */
+bool PreviewDrawing::render(ExportPreview *widget, uint32_t bg, SPItem *item, unsigned size, Geom::OptRect const &dbox)
+{
+ if (!_drawing || _to_destruct) {
+ if (!_construct_idle.connected()) {
+ _construct_idle = Glib::signal_timeout().connect([=]() {
+ _to_destruct = false;
+ destruct();
+ construct();
+ return false;
+ }, 100);
+ }
+ return false;
+ }
+
+ Geom::OptRect bbox = dbox;
+ DrawingItem *di = nullptr;
+
+ if (item) {
+ bbox = item->documentVisualBounds();
+ di = item->get_arenaitem(_visionkey);
+ } else if (!dbox)
+ bbox = _document->getRoot()->documentVisualBounds();
+
+ if (!bbox)
+ return true; // Force quit
+
+ // Use a callback to set the preview rendering;
+ widget->setPreview(UI::Preview::render_preview(_document, _drawing, bg, di, size, size, *bbox));
+ return true;
+}
+
+/**
+ * Limit the preview to just these items.
+ *
+ * You must call refresh after this for the change to take effect.
+ */
+void PreviewDrawing::set_shown_items(std::vector<SPItem*> &&list)
+{
+ _shown_items = std::move(list);
+ _to_destruct = true;
+}
+
+void ExportPreview::resetPixels(bool new_size)
+{
+ clear();
+ // An icon to use when the preview hasn't loaded yet
+ static Glib::RefPtr<Gdk::Pixbuf> preview_loading;
+ if (!preview_loading || new_size) {
+ using namespace Inkscape::IO::Resource;
+ preview_loading = Gdk::Pixbuf::create_from_file(get_filename(PIXMAPS, "preview_loading.svg"), size, size);
+ }
+ if (preview_loading) {
+ set(preview_loading);
+ }
+ show();
+}
+
+void ExportPreview::setSize(int newSize)
+{
+ size = newSize;
+ resetPixels(true);
+}
+
+ExportPreview::~ExportPreview()
+{
+ refresh_conn.disconnect();
+}
+
+void ExportPreview::setItem(SPItem *item)
+{
+ _item = item;
+ _dbox = {};
+}
+
+void ExportPreview::setBox(Geom::Rect const &bbox)
+{
+ if (bbox.hasZeroArea())
+ return;
+
+ _item = nullptr;
+ _dbox = bbox;
+}
+
+void ExportPreview::setDrawing(std::shared_ptr<PreviewDrawing> drawing)
+{
+ _drawing = drawing;
+}
+
+/*
+ * This is the main function which finally renders the preview.
+ * 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 we simply do nothing.
+ */
+void ExportPreview::queueRefresh()
+{
+ if (!_drawing || _render_idle.connected())
+ return;
+
+ _render_idle = Glib::signal_timeout().connect([=]() {
+ return !_drawing->render(this, _bg_color, _item, size, _dbox);
+ }, 100);
+}
+
+/**
+ * Callback when the rendering is complete.
+ */
+void ExportPreview::setPreview(Cairo::RefPtr<Cairo::ImageSurface> surface)
+{
+ if (surface) {
+ set(Gdk::Pixbuf::create(surface, 0, 0, surface->get_width(), surface->get_height()));
+ show();
+ }
+}
+
+void ExportPreview::setBackgroundColor(uint32_t bg_color)
+{
+ _bg_color = bg_color;
+}
+
+} // 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..e0be123
--- /dev/null
+++ b/src/ui/widget/export-preview.h
@@ -0,0 +1,99 @@
+// 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 INKSCAPE_UI_WIDGET_EXPORT_PREVIEW_H
+#define INKSCAPE_UI_WIDGET_EXPORT_PREVIEW_H
+
+#include <cstdint>
+#include <2geom/rect.h>
+#include <gtkmm.h>
+#include "display/drawing.h"
+#include "helper/auto-connection.h"
+#include "async/channel.h"
+
+class SPDocument;
+class SPObject;
+class SPItem;
+
+namespace Inkscape {
+class Drawing;
+
+namespace UI {
+namespace Dialog {
+class ExportPreview;
+
+class PreviewDrawing
+{
+public:
+ PreviewDrawing(SPDocument *document);
+ ~PreviewDrawing();
+
+ bool render(ExportPreview *widget, uint32_t bg, SPItem *item, unsigned size, Geom::OptRect const &dboxIn);
+ void set_shown_items(std::vector<SPItem*> &&list = {});
+
+private:
+ void destruct();
+ void construct();
+
+ SPDocument *_document = nullptr;
+ std::shared_ptr<Inkscape::Drawing> _drawing;
+ unsigned _visionkey = 0;
+ bool _to_destruct = false;
+
+ std::vector<SPItem*> _shown_items;
+ Inkscape::auto_connection _construct_idle;
+};
+
+class ExportPreview final : public Gtk::Image
+{
+public:
+ ExportPreview() = default;
+ ExportPreview(BaseObjectType *cobj, Glib::RefPtr<Gtk::Builder> const &) : Gtk::Image(cobj) {}
+ ~ExportPreview() override;
+
+ void setDrawing(std::shared_ptr<PreviewDrawing> drawing);
+ void setItem(SPItem *item);
+ void setBox(Geom::Rect const &bbox);
+ void queueRefresh();
+ void resetPixels(bool new_size = false);
+ void setSize(int newSize);
+ void setPreview(Cairo::RefPtr<Cairo::ImageSurface>);
+ void setBackgroundColor(uint32_t bg_color);
+
+ static std::shared_ptr<Inkscape::Drawing> makeDrawing(SPDocument *doc);
+
+private:
+ int size = 128; // size of preview image
+ sigc::connection refresh_conn;
+
+ SPItem *_item = nullptr;
+ Geom::OptRect _dbox;
+
+ std::shared_ptr<PreviewDrawing> _drawing;
+ uint32_t _bg_color = 0;
+
+ Inkscape::auto_connection _render_idle;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_EXPORT_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/fill-style.cpp b/src/ui/widget/fill-style.cpp
new file mode 100644
index 0000000..124d6d8
--- /dev/null
+++ b/src/ui/widget/fill-style.cpp
@@ -0,0 +1,738 @@
+// 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 "actions/actions-tools.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 "object/sp-object.h"
+#include "ui/dialog/dialog-base.h"
+#include "style.h"
+#include "object/sp-use.h"
+#include "pattern-manipulation.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); }
+ });
+ _psel->signal_edit_pattern().connect([=](){
+ if (_desktop) set_active_tool(_desktop, "Node");
+ });
+
+ 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->getSelection()) {
+ subselChangedConn = desktop->connect_text_cursor_moved([=](void* sender, Inkscape::UI::Tools::TextTool* tool) {
+ 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 = 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 (auto selection = _desktop->getSelection()) {
+ std::vector<SPItem*> vec(selection->items().begin(), 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 (is<SPGradient>(server) && cast<SPGradient>(server)->getVector()->isSwatch()) {
+ auto vector = cast<SPGradient>(server)->getVector();
+ _psel->setSwatch(vector);
+ } else if (is<SPLinearGradient>(server)) {
+ auto vector = cast<SPGradient>(server)->getVector();
+ auto lg = cast<SPLinearGradient>(server);
+ _psel->setGradientLinear(vector, lg, stop);
+
+ _psel->setGradientProperties(lg->getUnits(), lg->getSpread());
+ } else if (is<SPRadialGradient>(server)) {
+ auto vector = cast<SPGradient>(server)->getVector();
+ auto rg = cast<SPRadialGradient>(server);
+ _psel->setGradientRadial(vector, rg, stop);
+
+ _psel->setGradientProperties(rg->getUnits(), rg->getSpread());
+#ifdef WITH_MESH
+ } else if (is<SPMeshGradient>(server)) {
+ auto array = cast<SPGradient>(server)->getArray();
+ _psel->setGradientMesh(cast<SPMeshGradient>(array));
+ _psel->updateMeshList(cast<SPMeshGradient>(array));
+#endif
+ } else if (is<SPPattern>(server)) {
+ _psel->updatePatternList(cast<SPPattern>(server));
+ }
+ }
+ }
+ 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;
+}
+
+void unset_recursive(const char *attribute, SPObject* object)
+{
+ object->removeAttribute(attribute);
+
+ for (auto& child: object->children)
+ {
+ if (is<SPUse> (object)) return;
+ unset_recursive(attribute, &child);
+ }
+}
+/**
+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);
+
+ 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, 1.0, createSwatch);
+ }
+ if (vector)
+ vector->setSwatch(createSwatch);
+
+ for (auto item : items) {
+ if (!vector) {
+ auto gr = sp_gradient_vector_for_object(
+ document, _desktop, item,
+ (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE, createSwatch);
+ if (gr) {
+ gr->setSwatch(createSwatch);
+ }
+ 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) {
+ SPGradient *gr = sp_item_set_gradient(
+ item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE);
+ _psel->pushAttrsToGradient(gr);
+ }
+ }
+
+ for (auto item : items) {
+ // fill and stroke opacity should never be set on gradients since in our user interface
+ // these are controlled by the gradient stops themselves.
+ item->style->clear(kind == FILL ? SPAttr::FILL_OPACITY : SPAttr::STROKE_OPACITY);
+ }
+ 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 && is<SPMeshGradient>(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 = is<SPText>(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 = is<SPText>(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 {
+ auto link_pattern = pattern;
+ auto root_pattern = pattern->rootPattern();
+ if (auto color = _psel->get_pattern_color()) {
+ sp_pattern_set_color(root_pattern, color.value());
+ }
+ // pattern name is applied to the root
+ root_pattern->setAttribute("inkscape:label", _psel->get_pattern_label().c_str());
+ // remaining settings apply to link pattern
+ if (link_pattern != root_pattern) {
+ auto transform = _psel->get_pattern_transform();
+ sp_pattern_set_transform(link_pattern, transform);
+ auto offset = _psel->get_pattern_offset();
+ sp_pattern_set_offset(link_pattern, offset);
+ auto uniform = _psel->is_pattern_scale_uniform();
+ sp_pattern_set_uniform_scale(link_pattern, uniform);
+ // gap requires both patterns, but they are only created later by calling "adjust_pattern" below
+ // it is OK to ignore it for now, during initial creation gap is 0,0
+ auto gap = _psel->get_pattern_gap();
+ sp_pattern_set_gap(link_pattern, gap);
+ }
+
+ Inkscape::XML::Node *patrepr = root_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 (is<SPPattern>(server) && cast<SPPattern>(server)->rootPattern() == root_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");
+ }
+
+ // create link to pattern right away, without waiting for object to be moved;
+ // otherwise pattern editor may end up modifying pattern shared by different objects
+ item->adjust_pattern(Geom::Affine());
+ }
+
+ 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()) {
+ for (auto item: items) {
+ if (item) {
+ unset_recursive((kind == FILL) ? "fill" : "stroke", item);
+ }
+ }
+ 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..2d26d9a
--- /dev/null
+++ b/src/ui/widget/filter-effect-chooser.cpp
@@ -0,0 +1,211 @@
+// 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 {
+
+// Blend modes are in six groups according to the types of changes they make to luminosity
+// See: https://typefully.com/DanHollick/blending-modes-KrBa0JP
+// Add 5 to ENDMODE for the five additional separators in the list
+const int SP_CSS_BLEND_COUNT = SP_CSS_BLEND_ENDMODE + 5;
+const EnumData<SPBlendMode> SPBlendModeData[SP_CSS_BLEND_COUNT] = {
+ { SP_CSS_BLEND_NORMAL, NC_("BlendMode", "Normal"), "normal" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_DARKEN, NC_("BlendMode", "Darken"), "darken" },
+ { SP_CSS_BLEND_MULTIPLY, NC_("BlendMode", "Multiply"), "multiply" },
+ { SP_CSS_BLEND_COLORBURN, NC_("BlendMode", "Color Burn"), "color-burn" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_LIGHTEN, NC_("BlendMode", "Lighten"), "lighten" },
+ { SP_CSS_BLEND_SCREEN, NC_("BlendMode", "Screen"), "screen" },
+ { SP_CSS_BLEND_COLORDODGE, NC_("BlendMode", "Color Dodge"), "color-dodge" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_OVERLAY, NC_("BlendMode", "Overlay"), "overlay" },
+ { SP_CSS_BLEND_SOFTLIGHT, NC_("BlendMode", "Soft Light"), "soft-light" },
+ { SP_CSS_BLEND_HARDLIGHT, NC_("BlendMode", "Hard Light"), "hard-light" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_DIFFERENCE, NC_("BlendMode", "Difference"), "difference" },
+ { SP_CSS_BLEND_EXCLUSION, NC_("BlendMode", "Exclusion"), "exclusion" },
+ { SP_CSS_BLEND_ENDMODE, "-", "-" },
+ { SP_CSS_BLEND_HUE, NC_("BlendMode", "Hue"), "hue" },
+ { SP_CSS_BLEND_SATURATION, NC_("BlendMode", "Saturation"), "saturation" },
+ { SP_CSS_BLEND_COLOR, NC_("BlendMode", "Color"), "color" },
+ { SP_CSS_BLEND_LUMINOSITY, NC_("BlendMode", "Luminosity"), "luminosity" }
+};
+const EnumDataConverter<SPBlendMode> SPBlendModeConverter(SPBlendModeData, SP_CSS_BLEND_COUNT);
+
+
+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, "BlendMode")
+ , _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_by_id(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..5001e5b
--- /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-collection-selector.cpp b/src/ui/widget/font-collection-selector.cpp
new file mode 100644
index 0000000..70d1e11
--- /dev/null
+++ b/src/ui/widget/font-collection-selector.cpp
@@ -0,0 +1,674 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Vaibhav Malik <vaibhavmalik2018@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+
+#include "font-collection-selector.h"
+
+#include "libnrtype/font-lister.h"
+
+// For updating from selection
+#include "util/document-fonts.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+FontCollectionSelector::FontCollectionSelector()
+{
+ // Step 1: Initialize the treeview.
+ treeview = Gtk::manage(new Gtk::TreeView());
+
+ // Step 2: Setup the treeview.
+ setup_tree_view(treeview);
+
+ // Step 3: Intialize the model.
+ store = Gtk::TreeStore::create(FontCollection);
+ // Step 4: Populate the ListStore.
+ treeview->set_model(store);
+
+ // Signals.
+ setup_signals();
+
+ show_all_children();
+}
+
+// Setup the treeview of the widget.
+void FontCollectionSelector::setup_tree_view(Gtk::TreeView *tv)
+{
+ cell_text = new Gtk::CellRendererText();
+ del_icon_renderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ del_icon_renderer->add_icon("edit-delete");
+
+ text_column.pack_start (*cell_text, true);
+ text_column.add_attribute (*cell_text, "text", TEXT_COLUMN);
+ text_column.set_expand(true);
+
+ del_icon_column.pack_start (*del_icon_renderer, false);
+
+ // Attach the cell data functions.
+ text_column.set_cell_data_func(*cell_text, sigc::mem_fun(*this, &FontCollectionSelector::text_cell_data_func));
+
+ treeview->enable_model_drag_dest (Gdk::ACTION_MOVE);
+ treeview->set_headers_visible (false);
+
+ // Target entries for Drag and Drop.
+ target_entries.emplace_back(Gtk::TargetEntry("STRING", (Gtk::TargetFlags)0, 0));
+ target_entries.emplace_back(Gtk::TargetEntry("text/plain", (Gtk::TargetFlags)0, 0));
+
+ treeview->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY);
+
+ // Append the columns to the treeview.
+ treeview->append_column(text_column);
+ treeview->append_column(del_icon_column);
+
+ scroll.set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scroll.set_overlay_scrolling(false);
+ scroll.add (*treeview);
+
+ frame.set_hexpand (true);
+ frame.set_vexpand (true);
+ frame.add (scroll);
+
+ // Grid
+ set_name("FontCollection");
+ set_row_spacing(4);
+ set_column_spacing(1);
+
+ // Add extra columns to the "frame" to change space distribution
+ attach (frame, 0, 0, 1, 2);
+}
+
+void FontCollectionSelector::change_frame_name(const Glib::ustring& name)
+{
+ frame.set_label(name);
+}
+
+void FontCollectionSelector::setup_signals()
+{
+ cell_text->signal_edited().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_rename_collection));
+ del_icon_renderer->signal_activated().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_delete_icon_clicked));
+ treeview->signal_key_press_event().connect([=](GdkEventKey *ev){ return on_key_pressed(ev); });
+ treeview->set_row_separator_func(sigc::mem_fun(*this, &FontCollectionSelector::row_separator_func));
+ treeview->get_column(ICON_COLUMN)->set_cell_data_func(*del_icon_renderer, sigc::mem_fun(*this, &FontCollectionSelector::icon_cell_data_func));
+
+ // Signals for drag and drop.
+ treeview->signal_drag_motion().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_motion), false);
+ treeview->signal_drag_data_received().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_data_received), false);
+ treeview->signal_drag_drop().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_drop), false);
+ // treeview->signal_drag_failed().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_failed), false);
+ treeview->signal_drag_leave().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_leave), false);
+ treeview->signal_drag_end().connect(sigc::mem_fun(*this, &FontCollectionSelector::on_drag_end), false);
+ treeview->get_selection()->signal_changed().connect([=](){ on_selection_changed(); });
+ Inkscape::RecentlyUsedFonts::get()->connectUpdate(sigc::mem_fun(*this, &FontCollectionSelector::populate_system_collections));
+}
+
+// To distinguish the collection name and the font name.
+Glib::ustring FontCollectionSelector::get_text_cell_markup(Gtk::TreeIter const &iter)
+{
+ Glib::ustring markup;
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ // It is a font.
+ markup = "<span alpha='50%'>";
+ markup += (*iter)[FontCollection.name];
+ markup += "</span>";
+ }
+ else {
+ // It is a collection.
+ markup = "<span>";
+ markup += (*iter)[FontCollection.name];
+ markup += "</span>";
+ }
+
+ return markup;
+}
+
+// This function will TURN OFF the visibility of the delete icon for system collections.
+void FontCollectionSelector::text_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ // Add the delete icon only if the collection is editable(user-collection).
+ Glib::ustring markup = get_text_cell_markup(iter);
+ renderer->set_property("markup", markup);
+}
+
+// This function will TURN OFF the visibility of the delete icon for system collections.
+void FontCollectionSelector::icon_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ // Add the delete icon only if the collection is editable(user-collection).
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ // Case: It is a font.
+ bool is_user = (*parent)[FontCollection.is_editable];
+ del_icon_renderer->set_visible(is_user);
+ cell_text->property_editable() = false;
+ } else if((*iter)[FontCollection.is_editable]) {
+ // Case: User font collection.
+ del_icon_renderer->set_visible(true);
+ cell_text->property_editable() = true;
+ } else {
+ // Case: System font collection.
+ del_icon_renderer->set_visible(false);
+ cell_text->property_editable() = false;
+ }
+}
+
+// This function will TURN OFF the visibility of checkbuttons for children in the TreeStore.
+void FontCollectionSelector::check_button_cell_data_func(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
+{
+ renderer->set_visible(false);
+ /*
+ // Append the checkbutton column only if the iterator have some children.
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+
+ if(parent) {
+ renderer->set_visible(false);
+ }
+ else {
+ renderer->set_visible(true);
+ }
+ */
+}
+
+bool FontCollectionSelector::row_separator_func(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter)
+{
+ return (*iter)[FontCollection.name] == "#";
+}
+
+void FontCollectionSelector::populate_collections()
+{
+ store->clear();
+ populate_system_collections();
+ populate_user_collections();
+}
+
+// This function will keep the populate the system collections and their fonts.
+void FontCollectionSelector::populate_system_collections()
+{
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ std::vector <Glib::ustring> system_collections = font_collections->get_collections(true);
+
+ // Erase the previous collections.
+ store->freeze_notify();
+ Gtk::TreePath path;
+ path.push_back(0);
+ Gtk::TreeModel::iterator iter;
+ bool row_0 = false, row_1 = false;
+
+ for(int i = 0; i < 3; i++) {
+ iter = store->get_iter(path);
+ if(iter) {
+ if(treeview->row_expanded(path)) {
+ if(i == 0) {
+ row_0 = true;
+ } else if(i == 1) {
+ row_1 = true;
+ }
+ }
+ store->erase(iter);
+ }
+ }
+
+ // Insert a separator.
+ iter = store->prepend();
+ (*iter)[FontCollection.name] = "#";
+ (*iter)[FontCollection.is_editable] = false;
+ iter = store->children();
+
+ for(auto const &col: system_collections) {
+ iter = store->prepend();
+ (*iter)[FontCollection.name] = col;
+ (*iter)[FontCollection.is_editable] = false;
+ }
+
+ populate_document_fonts();
+ populate_recently_used_fonts();
+ store->thaw_notify();
+
+ if(row_0) {
+ treeview->expand_row(Gtk::TreePath("0"), true);
+ }
+ if(row_1) {
+ treeview->expand_row(Gtk::TreePath("1"), true);
+ }
+}
+
+void FontCollectionSelector::populate_document_fonts()
+{
+ // The position of the recently used collection is hardcoded for now.
+ Gtk::TreePath path;
+ path.push_back(1);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ for(auto const& font: Inkscape::DocumentFonts::get()->get_fonts()) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+}
+
+void FontCollectionSelector::populate_recently_used_fonts()
+{
+ // The position of the recently used collection is hardcoded for now.
+ Gtk::TreePath path;
+ path.push_back(0);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ for(auto const& font: Inkscape::RecentlyUsedFonts::get()->get_fonts()) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+}
+
+// This function will keep the collections_list updated after any event.
+void FontCollectionSelector::populate_user_collections()
+{
+ // Get the list of all the user collections.
+ auto collections = Inkscape::FontCollections::get()->get_collections();
+
+ // Now insert these collections one by one into the treeview.
+ store->freeze_notify();
+ Gtk::TreeModel::iterator iter;
+
+ for(const auto &col: collections) {
+ iter = store->append();
+ (*iter)[FontCollection.name] = col;
+
+ // User collections are editable.
+ (*iter)[FontCollection.is_editable] = true;
+
+ // Alright, now populate the fonts of this collection.
+ populate_fonts(col);
+ }
+ store->thaw_notify();
+}
+
+void FontCollectionSelector::populate_fonts(const Glib::ustring& collection_name)
+{
+ // Get the FontLister instance to get the list of all the collections.
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ std::set <Glib::ustring> fonts = font_collections->get_fonts(collection_name);
+
+ // First find the location of this collection_name in the map.
+ // +1 for the separator.
+ int index = font_collections->get_user_collection_location(collection_name) + 1;
+
+ store->freeze_notify();
+
+ // Generate the iterator path.
+ Gtk::TreePath path;
+ path.push_back(index);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ // auto child_iter = iter->children();
+ auto size = iter->children().size();
+
+ // Clear the previously stored fonts at this path.
+ while(size--) {
+ Gtk::TreeModel::iterator child = iter->children().begin();
+ store->erase(child);
+ }
+
+ for(auto const &font: fonts) {
+ Gtk::TreeModel::iterator child = store->append((*iter).children());
+ (*child)[FontCollection.name] = font;
+ (*child)[FontCollection.is_editable] = false;
+ }
+
+ store->thaw_notify();
+}
+
+void FontCollectionSelector::on_delete_icon_clicked(Glib::ustring const &path)
+{
+ FontCollections *collections = Inkscape::FontCollections::get();
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+ auto parent = (*iter)->parent();
+ if(!parent) {
+ // It is a collection.
+ // No need to confirm in case of empty collections.
+ if (!collections->get_fonts((*iter)[FontCollection.name]).empty()) {
+ // Warn the user and then proceed.
+ int response = deleltion_warning_message_dialog((*iter)[FontCollection.name]);
+ if (response != Gtk::RESPONSE_YES) {
+ return;
+ }
+ }
+ collections->remove_collection((*iter)[FontCollection.name]);
+ }
+ else {
+ // It is a font.
+ collections->remove_font((*parent)[FontCollection.name], (*iter)[FontCollection.name]);
+ }
+ store->erase(iter);
+}
+
+void FontCollectionSelector::on_create_collection()
+{
+ Gtk::TreeModel::iterator iter = store->append();
+ (*iter)[FontCollection.is_editable] = true;
+
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ treeview->set_cursor(path, text_column, true);
+ grab_focus();
+}
+
+void FontCollectionSelector::on_rename_collection(const Glib::ustring& path, const Glib::ustring& new_text)
+{
+ // Fetch the collections.
+ FontCollections *collections = Inkscape::FontCollections::get();
+
+ // Check if the same collection is already present.
+ bool is_system = collections->find_collection(new_text, true);
+ bool is_user = collections->find_collection(new_text, false);
+
+ // Return if the new name is empty.
+ // Do not allow user collections to be named as system collections.
+ if (new_text == "" || is_system || is_user) {
+ return;
+ }
+
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+
+ // Return if it is not a valid iter.
+ if(!iter) {
+ return;
+ }
+
+ // To check if it's a font-collection or a font.
+ auto parent = (*iter)->parent();
+
+ if(!parent) {
+ // Call the rename_collection function
+ collections->rename_collection((*iter)[FontCollection.name], new_text);
+ }
+ else {
+ collections->rename_font((*parent)[FontCollection.name], (*iter)[FontCollection.name], new_text);
+ }
+
+ (*iter)[FontCollection.name] = new_text;
+ populate_collections();
+}
+
+void FontCollectionSelector::on_delete_button_pressed()
+{
+ // Get the current collection.
+ Glib::RefPtr<Gtk::TreeSelection> selection = treeview->get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+
+ FontCollections *collections = Inkscape::FontCollections::get();
+
+ if(!parent) {
+ // It is a collection.
+ // Check if it is a system collection.
+ bool is_system = collections->find_collection((*iter)[FontCollection.name], true);
+ if(is_system) {
+ return;
+ }
+
+ // Warn the user and then proceed.
+ int response = deleltion_warning_message_dialog((*iter)[FontCollection.name]);
+
+ if (response != Gtk::RESPONSE_YES) {
+ return;
+ }
+
+ collections->remove_collection((*iter)[FontCollection.name]);
+ }
+ else {
+ // It is a font.
+ // Check if it belongs to a system collection.
+ bool is_system = collections->find_collection((*parent)[FontCollection.name], true);
+
+ if(is_system) {
+ return;
+ }
+
+ collections->remove_font((*parent)[FontCollection.name], row[FontCollection.name]);
+ }
+ store->erase(iter);
+}
+
+// Function to edit the name of the collection or font.
+void FontCollectionSelector::on_edit_button_pressed()
+{
+ Glib::RefPtr<Gtk::TreeSelection> selection = treeview->get_selection();
+
+ if(selection) {
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if(!iter) {
+ return;
+ }
+
+ Gtk::TreeModel::Row row = *iter;
+ auto parent = row->parent();
+ bool is_system = Inkscape::FontCollections::get()->find_collection((*iter)[FontCollection.name], true);
+
+ if(!parent && !is_system) {
+ // It is a collection.
+ treeview->set_cursor(Gtk::TreePath(iter), text_column, true);
+ }
+ }
+}
+
+int FontCollectionSelector::deleltion_warning_message_dialog(const Glib::ustring &collection_name)
+{
+ Glib::ustring message =
+ Glib::ustring::compose(_("Are you sure want to delete the \"%1\" font collection?\n"), collection_name);
+ Gtk::MessageDialog dialog(message, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true);
+ dialog.set_transient_for(*dynamic_cast<Gtk::Window *>(get_toplevel()));
+ return dialog.run();
+}
+
+bool FontCollectionSelector::on_key_pressed(GdkEventKey *event)
+{
+ if (event->type == GDK_KEY_PRESS && frame.get_label() == "Collections")
+ {
+ // std::cout << "Key pressed" << std::endl;
+ switch (Inkscape::UI::Tools::get_latin_keyval (event)) {
+ case GDK_KEY_Delete:
+ on_delete_button_pressed();
+ break;
+ }
+ // We handled this event.
+ return true;
+ }
+ // We did not handle this event.
+ return false;
+}
+
+bool FontCollectionSelector::on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context,
+ int x,
+ int y,
+ guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+
+ treeview->get_dest_row_at_pos(x, y, path, pos);
+ treeview->drag_unhighlight();
+
+ if (path) {
+ context->drag_status(Gdk::ACTION_COPY, time);
+ return false;
+ }
+
+ // remove drop highlight
+ context->drag_refuse(time);
+ return true;
+}
+
+void FontCollectionSelector::on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> context,
+ int x,
+ int y,
+ const Gtk::SelectionData &selection_data,
+ guint info, guint time)
+{
+ // std::cout << "FontCollectionSelector::on_drag_data_received()" << std::endl;
+ // 1. Get the row at which the data is dropped.
+ Gtk::TreePath path;
+ treeview->get_path_at_pos(x, y, path);
+ Gtk::TreeModel::iterator iter = store->get_iter(path);
+ bool is_expanded = false;
+
+ // Case when the font is dragged in the empty space.
+ if(!iter) {
+ return;
+ }
+
+ Glib::ustring collection_name = (*iter)[FontCollection.name];
+ auto font_name = Inkscape::FontLister::get_instance()->get_dragging_family();
+
+ FontCollections *collections = Inkscape::FontCollections::get();
+ std::vector <Glib::ustring> system_collections = collections->get_collections(true);
+ auto parent = (*iter)->parent();
+
+ if(parent) {
+ is_expanded = true;
+ collection_name = (*parent)[FontCollection.name];
+ bool is_system = collections->find_collection(collection_name, true);
+
+ if(is_system) {
+ // The font is dropped in a system collection.
+ return;
+ }
+ } else {
+ if (treeview->row_expanded(path)) {
+ is_expanded = true;
+ }
+
+ bool is_system = collections->find_collection(collection_name, true);
+
+ if(is_system) {
+ // The font is dropped in a system collection.
+ return;
+ }
+ }
+
+ // 2. Get the data that is sent by the source.
+ // std::cout << "Received: " << selection_data.get_data() << std::endl;
+ // std::cout << (*iter)[FontCollection.name] << std::endl;
+ // Add the font into the collection.
+ collections->add_font(collection_name, font_name);
+
+ // Re-populate the collection.
+ populate_fonts(collection_name);
+
+ // Re-expand this row after re-population.
+ if(is_expanded) {
+ treeview->expand_to_path(path);
+ }
+
+ // Call gtk_drag_finish(context, success, del = false, time)
+ gtk_drag_finish(context->gobj(), TRUE, FALSE, time);
+}
+
+bool FontCollectionSelector::on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &context,
+ int x,
+ int y,
+ guint time)
+{
+ // std::cout << "FontCollectionSelector::on_drag_drop()" << std::endl;
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+ treeview->get_dest_row_at_pos(x, y, path, pos);
+
+ if (!path) {
+ // std::cout << "Not on target\n";
+ return false;
+ }
+
+ on_drag_end(context);
+ return true;
+}
+
+/*
+bool FontCollectionSelector::on_drag_failed(const Glib::RefPtr<Gdk::DragContext> &context,
+ const Gtk::DragResult result)
+{
+ std::cout << "Drag Failed\n";
+ return true;
+}
+*/
+
+void FontCollectionSelector::on_drag_leave(const Glib::RefPtr<Gdk::DragContext> &context,
+ guint time)
+{
+ // std::cout << "Drag Leave\n";
+}
+
+/*
+void FontCollectionSelector::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // std::cout << "FontCollectionSelector::on_drag_start()" << std::endl;
+}
+*/
+
+void FontCollectionSelector::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // std::cout << "FontCollection::on_drag_end()" << std::endl;
+ treeview->drag_unhighlight();
+}
+
+void FontCollectionSelector::on_selection_changed()
+{
+ Glib::RefPtr <Gtk::TreeSelection> selection = treeview->get_selection();
+ if(selection) {
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ auto parent = iter->parent();
+
+ // We use 3 states to adjust the sensitivity of the edit and
+ // delete buttons in the font collections manager dialog.
+ int state = 0;
+
+ // State -1: Selection is a system collection or a system
+ // collection font.(Neither edit nor delete)
+
+ // State 0: It's not a system collection or it's font. But it is
+ // a user collection.(Both edit and delete).
+
+ // State 1: It is a font that belongs to a user collection.
+ // (Only delete)
+
+ if(parent) {
+ // It is a font, and thus it is not editable.
+ // Now check if it's parent is a system collection.
+ bool is_system = font_collections->find_collection((*parent)[FontCollection.name], true);
+ state = (is_system) ? SYSTEM_COLLECTION: USER_COLLECTION_FONT;
+ } else {
+ // Check if it is a system collection.
+ bool is_system = font_collections->find_collection((*iter)[FontCollection.name], true);
+ state = (is_system) ? SYSTEM_COLLECTION: USER_COLLECTION;
+ }
+
+ signal_changed.emit(state);
+ }
+}
+
+} // 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-collection-selector.h b/src/ui/widget/font-collection-selector.h
new file mode 100644
index 0000000..cf21ec4
--- /dev/null
+++ b/src/ui/widget/font-collection-selector.h
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * This file contains the definition of the FontCollectionSelector class. This widget
+ * defines a treeview to provide the interface to create, read, update and delete font
+ * collections and their respective fonts. This class contains all the code related to
+ * population of collections and their fonts in the TreeStore.
+ *
+ * Author:
+ * Vaibhav Malik <vaibhavmalik2018@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_FONT_COLLECTION_SELECTOR_H
+#define INKSCAPE_UI_WIDGET_FONT_COLLECTION_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/tools/tool-base.h"
+#include "ui/widget/iconrenderer.h"
+#include "ui/widget/scrollprotected.h"
+#include "util/font-collections.h"
+#include "util/document-fonts.h"
+#include "util/recently-used-fonts.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A container of widgets for selecting font faces.
+ */
+class FontCollectionSelector : public Gtk::Grid
+{
+public:
+
+ enum {TEXT_COLUMN, ICON_COLUMN, N_COLUMNS};
+ enum SelectionStates {SYSTEM_COLLECTION = -1, USER_COLLECTION, USER_COLLECTION_FONT};
+
+ FontCollectionSelector();
+
+ // Basic setup.
+ void setup_tree_view(Gtk::TreeView*);
+ void change_frame_name(const Glib::ustring&);
+ void setup_signals();
+
+ Glib::ustring get_text_cell_markup(Gtk::TreeIter const &iter);
+
+ // Custom renderers.
+ void text_cell_data_func(Gtk::CellRenderer*, Gtk::TreeIter const&);
+ void icon_cell_data_func(Gtk::CellRenderer*, Gtk::TreeIter const&);
+ void check_button_cell_data_func(Gtk::CellRenderer*, Gtk::TreeIter const&);
+ bool row_separator_func(const Glib::RefPtr<Gtk::TreeModel>&, const Gtk::TreeModel::iterator&);
+
+ void populate_collections();
+
+ void populate_system_collections();
+ void populate_document_fonts();
+ void populate_recently_used_fonts();
+
+ void populate_user_collections();
+ void populate_fonts(const Glib::ustring&);
+
+ // Signal handlers
+ void on_delete_icon_clicked(Glib::ustring const&);
+ void on_create_collection();
+ void on_rename_collection(const Glib::ustring&, const Glib::ustring&);
+ void on_delete_button_pressed();
+ void on_edit_button_pressed();
+
+ int deleltion_warning_message_dialog(const Glib::ustring&);
+ bool on_key_pressed(GdkEventKey*);
+
+ bool on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override;
+ void on_drag_data_received(const Glib::RefPtr<Gdk::DragContext>, int, int, const Gtk::SelectionData&, guint, guint);
+ bool on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override;
+ // bool on_drag_failed(const Glib::RefPtr<Gdk::DragContext> &, const Gtk::DragResult);
+ void on_drag_leave(const Glib::RefPtr<Gdk::DragContext> &, guint) override;
+ // void on_drag_start(const Glib::RefPtr<Gdk::DragContext> &);
+ void on_drag_end(const Glib::RefPtr<Gdk::DragContext> &) override;
+ void on_selection_changed();
+
+ sigc::connection connect_signal_changed(sigc::slot <void (int)> slot) {
+ return signal_changed.connect(slot);
+ }
+
+protected:
+
+ class FontCollectionClass : public Gtk::TreeModelColumnRecord
+ {
+ public:
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<bool> is_editable;
+
+ FontCollectionClass()
+ {
+ add(name);
+ add(is_editable);
+ }
+ };
+ FontCollectionClass FontCollection;
+
+ Gtk::TreeView *treeview;
+ Gtk::Frame frame;
+ Gtk::ScrolledWindow scroll;
+ Gtk::TreeViewColumn text_column;
+ Gtk::TreeViewColumn del_icon_column;
+ Gtk::CellRendererText *cell_text;
+ Inkscape::UI::Widget::IconRenderer *del_icon_renderer;
+
+ Glib::RefPtr<Gtk::TreeStore> store;
+
+ // What type of object can be dropped.
+ std::vector<Gtk::TargetEntry> target_entries;
+ sigc::signal <void (int)> signal_changed;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_FONT_COLLECTION_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 :
diff --git a/src/ui/widget/font-selector-toolbar.cpp b/src/ui/widget/font-selector-toolbar.cpp
new file mode 100644
index 0000000..830a54b
--- /dev/null
+++ b/src/ui/widget/font-selector-toolbar.cpp
@@ -0,0 +1,301 @@
+// 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 ([=](){ on_family_changed(); });
+ style_combo.signal_changed().connect ([=](){ 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([=](){ 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().raw() << 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().raw() << 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::cerr << "FontSelectorToolbar::on_entry_icon_pressed" << std::endl;
+ std::cerr << " .... 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..510f306
--- /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..ea7d53b
--- /dev/null
+++ b/src/ui/widget/font-selector.cpp
@@ -0,0 +1,562 @@
+// 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"
+#include "libnrtype/font-factory.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();
+ // Font family
+ family_treecolumn.pack_start (family_cell, false);
+ 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);
+ } else {
+#if !PANGO_VERSION_CHECK(1,50,0)
+ family_cell.set_fixed_size(-1, height);
+#endif
+ }
+ 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_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func_markup);
+ 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);
+ }
+
+ // For drag and drop.
+ // Target entries for Drag and Drop.
+ // target_entries.emplace_back(Gtk::TargetEntry("text/uri-list", (Gtk::TargetFlags)0, 0));
+ target_entries.emplace_back(Gtk::TargetEntry("STRING", (Gtk::TargetFlags)0, 0));
+ target_entries.emplace_back(Gtk::TargetEntry("text/plain", (Gtk::TargetFlags)0, 0));
+
+ family_treeview.drag_source_set(target_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_COPY | Gdk::ACTION_DEFAULT);
+ family_treeview.signal_drag_data_get().connect(sigc::mem_fun(*this, &FontSelector::on_drag_data_get));
+ family_treeview.signal_drag_begin().connect(sigc::mem_fun(*this, &FontSelector::on_drag_start), false);
+
+ // 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));
+ family_treeview.signal_realize().connect(sigc::mem_fun(*this, &FontSelector::on_realize_list));
+ show_all_children();
+ font_variations_scroll.set_vexpand(false);
+
+ // 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::on_realize_list() {
+ family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func);
+ g_idle_add(FontSelector::set_cell_markup, this);
+}
+
+gboolean FontSelector::set_cell_markup(gpointer data)
+{
+ FontSelector *self = static_cast<FontSelector *>(data);
+ self->family_treeview.hide();
+ self->family_treecolumn.set_cell_data_func (self->family_cell, &font_lister_cell_data_func_markup);
+ self->family_treeview.show();
+ return false;
+}
+
+void FontSelector::hide_others()
+{
+ style_frame.set_no_show_all();
+ style_frame.hide();
+ size_label.set_no_show_all();
+ size_label.hide();
+ size_combobox.set_no_show_all();
+ size_combobox.hide();
+ font_variations.set_no_show_all();
+ font_variations_scroll.hide();
+ font_variations_scroll.set_vexpand(false);
+}
+
+// TODO:
+void FontSelector::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // Get the current collection.
+ Glib::RefPtr<Gtk::TreeSelection> selection = family_treeview.get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ Gtk::TreePath path(iter);
+ auto surface = family_treeview.create_row_drag_icon(path);
+
+ context->set_icon(surface);
+ /*
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Glib::ustring family_name = font_lister->get_treeview_drag_selection();
+ // std::cout << "FontSelector::on_drag_start()" << std::endl;
+ auto drag_label = Gtk::manage(new Gtk::Label(family_name));
+
+ gtk_drag_set_icon_widget(context, drag_label, 0, 0);
+ */
+}
+
+void FontSelector::on_drag_data_get(Glib::RefPtr<Gdk::DragContext> const &context, Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ // std::cout << "FontSelector::on_drag_data_get()" << std::endl;
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ // std::cout << font_lister->get_font_family_row() << ", " << font_lister->get_treeview_selection() << std::endl;
+
+ Glib::ustring family_name = font_lister->get_dragging_family();
+ // std::cout << "Family: " << family_name << std::endl;
+
+ selection_data.set_text(family_name);
+}
+
+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.raw() << 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;
+}
+
+void FontSelector::unset_model()
+{
+ family_treeview.unset_model();
+}
+
+void FontSelector::set_model()
+{
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+ Glib::RefPtr<Gtk::TreeModel> model = font_lister->get_font_list();
+ family_treeview.set_model(model);
+}
+
+// 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);
+
+ fontlister->set_dragging_family(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.raw() << 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);
+ g_idle_add(FontSelector::set_cell_markup, this);
+ }
+ 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..ca3cfea
--- /dev/null
+++ b/src/ui/widget/font-selector.h
@@ -0,0 +1,177 @@
+// 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);
+ void hide_others();
+
+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);
+
+ // What type of object can be dropped.
+ std::vector<Gtk::TargetEntry> target_entries;
+ static gboolean set_cell_markup(gpointer);
+ void on_realize_list();
+ // For drag and drop.
+ void on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context);
+ void on_drag_data_get(Glib::RefPtr<Gdk::DragContext> const &context, Gtk::SelectionData &selection_data, guint info, guint time) override;
+
+public:
+
+ /**
+ * Update GUI based on fontspec
+ */
+ void update_font ();
+ void update_size (double size);
+ void unset_model();
+ void set_model();
+
+ /**
+ * 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..504eda5
--- /dev/null
+++ b/src/ui/widget/font-variants.cpp
@@ -0,0 +1,1461 @@
+// 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 "libnrtype/font-factory.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(Glib::ustring const &name, OTSubstitution const &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.
+ auto res = FontFactory::get().FaceFromFontSpecification(font_spec.c_str());
+ if (res) {
+
+ auto const &tab = res->get_opentype_tables();
+ std::remove_reference<decltype(tab)>::type::const_iterator it;
+
+ if((it = tab.find("liga"))!= tab.end() ||
+ (it = tab.find("clig"))!= tab.end()) {
+ _ligatures_common.set_sensitive();
+ } else {
+ _ligatures_common.set_sensitive( false );
+ }
+
+ if((it = tab.find("dlig"))!= tab.end()) {
+ _ligatures_discretionary.set_sensitive();
+ } else {
+ _ligatures_discretionary.set_sensitive( false );
+ }
+
+ if((it = tab.find("hlig"))!= tab.end()) {
+ _ligatures_historical.set_sensitive();
+ } else {
+ _ligatures_historical.set_sensitive( false );
+ }
+
+ if((it = tab.find("calt"))!= tab.end()) {
+ _ligatures_contextual.set_sensitive();
+ } else {
+ _ligatures_contextual.set_sensitive( false );
+ }
+
+ if((it = tab.find("subs"))!= tab.end()) {
+ _position_sub.set_sensitive();
+ } else {
+ _position_sub.set_sensitive( false );
+ }
+
+ if((it = tab.find("sups"))!= tab.end()) {
+ _position_super.set_sensitive();
+ } else {
+ _position_super.set_sensitive( false );
+ }
+
+ if((it = tab.find("smcp"))!= tab.end()) {
+ _caps_small.set_sensitive();
+ } else {
+ _caps_small.set_sensitive( false );
+ }
+
+ if((it = tab.find("c2sc"))!= tab.end() &&
+ (it = tab.find("smcp"))!= tab.end()) {
+ _caps_all_small.set_sensitive();
+ } else {
+ _caps_all_small.set_sensitive( false );
+ }
+
+ if((it = tab.find("pcap"))!= tab.end()) {
+ _caps_petite.set_sensitive();
+ } else {
+ _caps_petite.set_sensitive( false );
+ }
+
+ if((it = tab.find("c2sc"))!= tab.end() &&
+ (it = tab.find("pcap"))!= tab.end()) {
+ _caps_all_petite.set_sensitive();
+ } else {
+ _caps_all_petite.set_sensitive( false );
+ }
+
+ if((it = tab.find("unic"))!= tab.end()) {
+ _caps_unicase.set_sensitive();
+ } else {
+ _caps_unicase.set_sensitive( false );
+ }
+
+ if((it = tab.find("titl"))!= tab.end()) {
+ _caps_titling.set_sensitive();
+ } else {
+ _caps_titling.set_sensitive( false );
+ }
+
+ if((it = tab.find("lnum"))!= tab.end()) {
+ _numeric_lining.set_sensitive();
+ } else {
+ _numeric_lining.set_sensitive( false );
+ }
+
+ if((it = tab.find("onum"))!= tab.end()) {
+ _numeric_old_style.set_sensitive();
+ } else {
+ _numeric_old_style.set_sensitive( false );
+ }
+
+ if((it = tab.find("pnum"))!= tab.end()) {
+ _numeric_proportional.set_sensitive();
+ } else {
+ _numeric_proportional.set_sensitive( false );
+ }
+
+ if((it = tab.find("tnum"))!= tab.end()) {
+ _numeric_tabular.set_sensitive();
+ } else {
+ _numeric_tabular.set_sensitive( false );
+ }
+
+ if((it = tab.find("frac"))!= tab.end()) {
+ _numeric_diagonal.set_sensitive();
+ } else {
+ _numeric_diagonal.set_sensitive( false );
+ }
+
+ if((it = tab.find("afrac"))!= tab.end()) {
+ _numeric_stacked.set_sensitive();
+ } else {
+ _numeric_stacked.set_sensitive( false );
+ }
+
+ if((it = tab.find("ordn"))!= tab.end()) {
+ _numeric_ordinal.set_sensitive();
+ } else {
+ _numeric_ordinal.set_sensitive( false );
+ }
+
+ if((it = tab.find("zero"))!= tab.end()) {
+ _numeric_slashed_zero.set_sensitive();
+ } else {
+ _numeric_slashed_zero.set_sensitive( false );
+ }
+
+ // East-Asian
+ if((it = tab.find("jp78"))!= tab.end()) {
+ _asian_jis78.set_sensitive();
+ } else {
+ _asian_jis78.set_sensitive( false );
+ }
+
+ if((it = tab.find("jp83"))!= tab.end()) {
+ _asian_jis83.set_sensitive();
+ } else {
+ _asian_jis83.set_sensitive( false );
+ }
+
+ if((it = tab.find("jp90"))!= tab.end()) {
+ _asian_jis90.set_sensitive();
+ } else {
+ _asian_jis90.set_sensitive( false );
+ }
+
+ if((it = tab.find("jp04"))!= tab.end()) {
+ _asian_jis04.set_sensitive();
+ } else {
+ _asian_jis04.set_sensitive( false );
+ }
+
+ if((it = tab.find("smpl"))!= tab.end()) {
+ _asian_simplified.set_sensitive();
+ } else {
+ _asian_simplified.set_sensitive( false );
+ }
+
+ if((it = tab.find("trad"))!= tab.end()) {
+ _asian_traditional.set_sensitive();
+ } else {
+ _asian_traditional.set_sensitive( false );
+ }
+
+ if((it = tab.find("fwid"))!= tab.end()) {
+ _asian_full_width.set_sensitive();
+ } else {
+ _asian_full_width.set_sensitive( false );
+ }
+
+ if((it = tab.find("pwid"))!= tab.end()) {
+ _asian_proportional_width.set_sensitive();
+ } else {
+ _asian_proportional_width.set_sensitive( false );
+ }
+
+ if((it = tab.find("ruby"))!= tab.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 : tab) {
+
+ 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->get_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->get_opentype_tables()) {
+
+ Glib::ustring markup;
+ markup += "<span font_family='";
+ markup += sp_font_description_get_family(res->get_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.
+ auto table_copy = res->get_opentype_tables();
+ 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->get_opentype_tables()) {
+ 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->get_descr()),
+ _feature_grid, grid_row, this);
+ grid_row++;
+ }
+ }
+
+ // GSUB lookup type 3 (1 to many mapping). Optionally type 1.
+ for (auto &table : res->get_opentype_tables()) {
+ 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->get_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 FontInstance for: "
+ << font_spec.raw() << 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..8af2e23
--- /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..d0e7e9b
--- /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 "libnrtype/font-factory.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 const &axis)
+ : name(std::move(name_))
+{
+
+ // std::cout << "FontVariationAxis::FontVariationAxis:: "
+ // << " name: " << name
+ // << " min: " << axis.minimum
+ // << " def: " << axis.def
+ // << " max: " << axis.maximum
+ // << " val: " << axis.set_val << std::endl;
+
+ label = Gtk::make_managed<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(Glib::ustring const &font_spec)
+{
+ auto res = FontFactory::get().FaceFromFontSpecification(font_spec.c_str());
+
+ auto children = get_children();
+ for (auto child : children) {
+ remove(*child);
+ }
+ axes.clear();
+
+ for (auto &a : res->get_opentype_varaxes()) {
+ // std::cout << "Creating axis: " << a.first << std::endl;
+ auto axis = Gtk::make_managed<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..dbc5eb8
--- /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 const &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/gradient-editor.cpp b/src/ui/widget/gradient-editor.cpp
new file mode 100644
index 0000000..2edb6c5
--- /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 (is<SPStop>(&child)) {
+ auto stop = cast<SPStop>(&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..792c042
--- /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..0322157
--- /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..3a5380d
--- /dev/null
+++ b/src/ui/widget/gradient-selector.cpp
@@ -0,0 +1,611 @@
+// 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) {
+ if (!new_text.empty() && new_text != gr_prepare_label(obj)) {
+ obj->setLabel(new_text.c_str());
+ Inkscape::DocumentUndo::done(obj->document, _("Rename gradient"), INKSCAPE_ICON("color-gradient"));
+ }
+ row[_columns->name] = gr_prepare_label(obj);
+ }
+ }
+ }
+}
+
+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 || (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_similar_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 = cast<SPGradient>(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..aae4369
--- /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..7626b50
--- /dev/null
+++ b/src/ui/widget/gradient-vector-selector.cpp
@@ -0,0 +1,326 @@
+// 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 || (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) {
+ auto grad = cast<SPGradient>(gradient);
+ if ( grad->hasStops() && (grad->isSwatch() == _swatched) ) {
+ gl.push_back(cast<SPGradient>(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..ac5460e
--- /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..e5fd3e6
--- /dev/null
+++ b/src/ui/widget/icon-combobox.h
@@ -0,0 +1,101 @@
+// 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>
+#include <gtkmm/treemodelfilter.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class IconComboBox : public Gtk::ComboBox {
+public:
+ IconComboBox() {
+ _model = Gtk::ListStore::create(_columns);
+
+ 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);
+
+ _filter = Gtk::TreeModelFilter::create(_model);
+ _filter->set_visible_column(_columns.is_visible);
+ set_model(_filter);
+ }
+
+ 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;
+ row[_columns.is_visible] = true;
+ }
+
+ void set_active_by_id(int id) {
+ for (auto i = _filter->children().begin(); i != _filter->children().end(); ++i) {
+ const int data = (*i)[_columns.id];
+ if (data == id) {
+ set_active(i);
+ break;
+ }
+ }
+ };
+
+ void set_row_visible(int id, bool visible = true) {
+ auto active_id = get_active_row_id();
+ for (const auto & i : _model->children()) {
+ const int data = i[_columns.id];
+ if (data == id) {
+ i[_columns.is_visible] = visible;
+ }
+ }
+ _filter->refilter();
+
+ // Reset the selected row if needed
+ if (active_id == id) {
+ for (const auto & i : _filter->children()) {
+ const int data = i[_columns.id];
+ set_active_by_id(data);
+ 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);
+ add(is_visible);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> icon_name;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<int> id;
+ Gtk::TreeModelColumn<bool> is_visible;
+ };
+
+ Columns _columns;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ Glib::RefPtr<Gtk::TreeModelFilter> _filter;
+ 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..01d6277
--- /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/image-properties.cpp b/src/ui/widget/image-properties.cpp
new file mode 100644
index 0000000..e661dde
--- /dev/null
+++ b/src/ui/widget/image-properties.cpp
@@ -0,0 +1,295 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Image properties widget for "Fill and Stroke" dialog
+ *
+ * Copyright (C) 2023 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "image-properties.h"
+#include <array>
+#include <glib/gi18n.h>
+#include <glibmm/convert.h>
+#include <glibmm/markup.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/button.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/window.h>
+#include <sstream>
+#include <string>
+#include "display/cairo-utils.h"
+#include "document-undo.h"
+#include "enums.h"
+#include "helper/choose-file.h"
+#include "helper/save-image.h"
+#include "object/sp-image.h"
+#include "ui/builder-utils.h"
+#include "ui/icon-names.h"
+#include "ui/util.h"
+#include "util/format_size.h"
+#include "util/object-renderer.h"
+#include "xml/href-attribute-helper.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+Cairo::RefPtr<Cairo::Surface> draw_preview(SPImage* image, double width, double height, int device_scale, uint32_t frame_color, uint32_t background) {
+ if (!image || !image->pixbuf) return Cairo::RefPtr<Cairo::Surface>();
+
+ object_renderer r;
+ object_renderer::options opt;
+ opt.frame(frame_color);
+ auto s = image->style;
+ // here for preview purposes using image's own opacity only
+ double alpha = s && s->opacity.set && !s->opacity.inherit ? SP_SCALE24_TO_FLOAT(s->opacity.value) : 1.0;
+ opt.image_opacity(alpha);
+ opt.checkerboard(background);
+ return r.render(*image, width, height, device_scale, opt);
+}
+
+void link_image(Gtk::Window* window, SPImage* image) {
+ if (!window || !image) return;
+
+ static std::string current_folder;
+ std::vector<Glib::ustring> mime_types = {
+ "image/png", "image/jpeg", "image/gif", "image/bmp", "image/tiff"
+ };
+ auto file = choose_file_open(_("Change Image"), window, mime_types, current_folder);
+ if (file.empty()) return;
+
+ // link image now
+ // todo: set/calc dpi?
+ // todo: set color profile?
+ try {
+ // convert filename to uri
+ auto uri = Glib::filename_to_uri(file);
+ setHrefAttribute(*image->getRepr(), uri);
+ }
+ catch (Glib::ConvertError const &e) {
+ g_warning("Error converting path to URI: %s", e.what().c_str());
+ setHrefAttribute(*image->getRepr(), file);
+ }
+ // SPImage modifies size when href changes; trigger it now before undo concludes
+ // TODO: this needs to be fixed in SPImage
+ image->document->_updateDocument(0);
+ DocumentUndo::done(image->document, _("Change image"), INKSCAPE_ICON("shape-image"));
+}
+
+void set_rendering_mode(SPImage* image, int index) {
+ static const std::array<const char*, 5> render = {
+ "auto", "optimizeSpeed", "optimizeQuality", "crisp-edges", "pixelated"
+ }; // SPImageRendering values
+
+ if (!image || index < 0 || index >= render.size()) return;
+
+ auto css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "image-rendering", render[index]);
+ if (auto image_node = image->getRepr()) {
+ sp_repr_css_change(image_node, css, "style");
+ DocumentUndo::done(image->document, _("Set image rendering option"), INKSCAPE_ICON("shape-image"));
+ }
+ sp_repr_css_attr_unref(css);
+}
+
+void set_aspect_ratio(SPImage* image, bool preserve_aspect_ratio) {
+ if (!image) return;
+ image->setAttribute("preserveAspectRatio", preserve_aspect_ratio ? "xMidYMid" : "none");
+ DocumentUndo::done(image->document, _("Preserve image aspect ratio"), INKSCAPE_ICON("shape-image"));
+}
+
+ImageProperties::ImageProperties() :
+ Gtk::Box(Gtk::ORIENTATION_HORIZONTAL),
+ _builder(create_builder("image-properties.glade")),
+ _preview(get_widget<Gtk::DrawingArea>(_builder, "preview")),
+ _aspect(get_widget<Gtk::RadioButton>(_builder, "preserve")),
+ _stretch(get_widget<Gtk::RadioButton>(_builder, "stretch")),
+ _rendering(get_widget<Gtk::ComboBoxText>(_builder, "rendering")),
+ _embed(get_widget<Gtk::Button>(_builder, "embed"))
+{
+
+ auto& main = get_widget<Gtk::Grid>(_builder, "main");
+ pack_start(main, true, true);
+
+ // arbitrarily selected max preview size for image content:
+ _preview_max_width = 120;
+ _preview_max_height = 90;
+
+ _preview.signal_draw().connect([=](const Cairo::RefPtr<Cairo::Context>& ctx){
+ if (_preview_image) {
+ ctx->set_source(_preview_image, 0, 0);
+ ctx->paint();
+ }
+ return true;
+ });
+
+ auto& change = get_widget<Gtk::Button>(_builder, "change-img");
+ change.signal_clicked().connect([=](){
+ if (_update.pending()) return;
+ auto window = dynamic_cast<Gtk::Window*>(get_toplevel());
+ link_image(window, _image);
+ });
+
+ auto& extract = get_widget<Gtk::Button>(_builder, "export");
+ extract.signal_clicked().connect([=](){
+ if (_update.pending()) return;
+ auto window = dynamic_cast<Gtk::Window*>(get_toplevel());
+ extract_image(window, _image);
+ });
+
+ _embed.signal_clicked().connect([=](){
+ if (_update.pending() || !_image) return;
+ // embed image in the current document
+ Inkscape::Pixbuf copy(*_image->pixbuf);
+ sp_embed_image(_image->getRepr(), &copy);
+ DocumentUndo::done(_image->document, _("Embed image"), INKSCAPE_ICON("selection-make-bitmap-copy"));
+ });
+
+ _rendering.signal_changed().connect([=](){
+ if (_update.pending()) return;
+ auto index = _rendering.get_active_row_number();
+ set_rendering_mode(_image, index);
+ });
+
+ _aspect.signal_toggled().connect([=](){
+ if (_update.pending()) return;
+ set_aspect_ratio(_image, _aspect.get_active());
+ });
+ _stretch.signal_toggled().connect([=](){
+ if (_update.pending()) return;
+ set_aspect_ratio(_image, !_stretch.get_active());
+ });
+}
+
+void ImageProperties::update(SPImage* image) {
+ if (!image && !_image) return; // nothing to do
+
+ _image = image;
+
+ auto scoped(_update.block());
+
+ auto small = [](const char* str) { return "<small>" + Glib::Markup::escape_text(str ? str : "") + "</small>"; };
+ auto& name = get_widget<Gtk::Label>(_builder, "name");
+ auto& info = get_widget<Gtk::Label>(_builder, "info");
+ auto& url = get_widget<Gtk::Entry>(_builder, "href");
+
+ if (!image) {
+ name.set_markup(small("-"));
+ info.set_markup(small("-"));
+ }
+ else {
+ Glib::ustring id(image->getId() ? image->getId() : "");
+ name.set_markup(small(id.empty() ? "-" : ("#" + id).c_str()));
+
+ bool embedded = false;
+ bool linked = false;
+ auto href = Inkscape::getHrefAttribute(*image->getRepr()).second;
+ if (href && std::strncmp(href, "data:", 5) == 0) {
+ embedded = true;
+ }
+ else if (href && *href) {
+ linked = true;
+ }
+
+ if (image->pixbuf) {
+ std::ostringstream ost;
+ if (!image->missing) {
+ auto times = "\u00d7"; // multiplication sign
+ // dimensions
+ ost << image->pixbuf->width() << times << image->pixbuf->height() << " px\n";
+
+ if (embedded) {
+ ost << _("Embedded");
+ ost << " (" << Util::format_file_size(std::strlen(href)) << ")\n";
+ }
+ if (linked) {
+ ost << _("Linked");
+ ost << '\n';
+ }
+ // color space
+ if (image->color_profile && *image->color_profile) {
+ ost << _("Color profile:") << ' ' << image->color_profile << '\n';
+ }
+ }
+ else {
+ ost << _("Missing image") << '\n';
+ }
+ info.set_markup(small(ost.str().c_str()));
+ }
+ else {
+ info.set_markup(small("-"));
+ }
+
+ url.set_text(linked ? href : "");
+ url.set_sensitive(linked);
+ _embed.set_sensitive(linked && image->pixbuf);
+
+ // aspect ratio
+ bool aspect_none = false;
+ if (image->aspect_set) {
+ aspect_none = image->aspect_align == SP_ASPECT_NONE;
+ }
+ if (aspect_none) {
+ _stretch.set_active();
+ }
+ else {
+ _aspect.set_active();
+ }
+
+ // rendering
+ _rendering.set_active(image->style ? image->style->image_rendering.value : -1);
+ }
+
+ int width = _preview_max_width;
+ int height = _preview_max_height;
+ if (image && image->pixbuf) {
+ double sw = image->pixbuf->width();
+ double sh = image->pixbuf->height();
+ double sx = sw / width;
+ double sy = sh / height;
+ auto scale = 1.0 / std::max(sx, sy);
+ width = std::max(1, int(sw * scale + 0.5));
+ height = std::max(1, int(sh * scale + 0.5));
+ }
+ // expand size to account for a frame around the image
+ int frame = 2;
+ width += frame;
+ height += frame;
+ _preview.set_size_request(width, height);
+ _preview.queue_draw();
+
+ // prepare preview
+ auto device_scale = get_scale_factor();
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(Gtk::STATE_FLAG_NORMAL);
+ auto foreground = conv_gdk_color_to_rgba(fg, 0.30);
+ if (!_background_color) {
+ update_bg_color();
+ }
+ _preview_image = draw_preview(_image, width, height, device_scale, foreground, _background_color);
+}
+
+void ImageProperties::update_bg_color() {
+ if (auto wnd = dynamic_cast<Gtk::Window*>(get_toplevel())) {
+ auto sc = wnd->get_style_context();
+ auto color = get_background_color(sc);
+ _background_color = conv_gdk_color_to_rgba(color);
+ }
+ else {
+ _background_color = 0x808080ff;
+ }
+}
+
+void ImageProperties::on_style_updated() {
+ update_bg_color();
+ update(_image);
+}
+
+}}} // namespaces
diff --git a/src/ui/widget/image-properties.h b/src/ui/widget/image-properties.h
new file mode 100644
index 0000000..24678d2
--- /dev/null
+++ b/src/ui/widget/image-properties.h
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_IMAGE_PROPERTIES_H
+#define SEEN_IMAGE_PROPERTIES_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/radiobutton.h>
+#include "helper/auto-connection.h"
+#include "object/sp-image.h"
+#include "ui/operation-blocker.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ImageProperties : public Gtk::Box {
+public:
+ ImageProperties();
+ ~ImageProperties() override = default;
+
+ void update(SPImage* image);
+
+private:
+ void on_style_updated() override;
+ void update_bg_color();
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ Gtk::DrawingArea& _preview;
+ Gtk::RadioButton& _aspect;
+ Gtk::RadioButton& _stretch;
+ Gtk::ComboBoxText& _rendering;
+ Gtk::Button& _embed;
+ int _preview_max_height;
+ int _preview_max_width;
+ SPImage* _image = nullptr;
+ OperationBlocker _update;
+ Cairo::RefPtr<Cairo::Surface> _preview_image;
+ uint32_t _background_color = 0;
+};
+
+}}} // namespaces
+
+#endif // SEEN_IMAGE_PROPERTIES_H
diff --git a/src/ui/widget/imagetoggler.cpp b/src/ui/widget/imagetoggler.cpp
new file mode 100644
index 0000000..829c470
--- /dev/null
+++ b/src/ui/widget/imagetoggler.cpp
@@ -0,0 +1,148 @@
+// 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_active_icon(*this, "active_icon", ""),
+ _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::set_active(bool active) {
+ _active = active;
+}
+
+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);
+ }
+
+ std::string icon_name = _property_active_icon.get_value();
+ // if the icon isn't cached, render it to a pixbuf
+ if (!icon_name.empty() && !_icon_cache[icon_name]) {
+ int scale = widget.get_scale_factor();
+ _icon_cache[icon_name] = sp_get_icon_pixbuf(icon_name, _size * scale);
+ }
+
+ // Hide when not being used.
+ double alpha = 1.0;
+ bool visible = _property_activatable.get_value()
+ || _property_active.get_value()
+ || _active;
+ 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 = icon_name.empty() ? _property_pixbuf_on.get_value() : _icon_cache[icon_name];
+ } 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..579f6b9
--- /dev/null
+++ b/src/ui/widget/imagetoggler.h
@@ -0,0 +1,97 @@
+// 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<std::string> property_active_icon() { return _property_active_icon.get_proxy(); }
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on();
+ Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off();
+
+ void set_active(bool active = true);
+
+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;
+ bool _active = false;
+ 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;
+ Glib::Property<std::string> _property_active_icon;
+ std::map<const std::string, Glib::RefPtr<Gdk::Pixbuf>> _icon_cache;
+
+ 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..3c35779
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.cpp
@@ -0,0 +1,1356 @@
+// 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 <cstring>
+#include <algorithm>
+#include <2geom/angle.h>
+#include <2geom/coord.h>
+#include <2geom/point.h>
+#include <2geom/line.h>
+
+#include "ui/dialog/color-item.h"
+#include "hsluv.h"
+#include "ui/widget/ink-color-wheel.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;
+
+struct ColorPoint
+{
+ 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(Hsluv::Triplet const &rgb)
+ {
+ r = rgb[0];
+ g = rgb[1];
+ b = rgb[2];
+ }
+
+ double x;
+ double y;
+ double r;
+ double g;
+ double b;
+};
+
+/** Represents a vertex of the Luv color polygon (intersection of bounding lines). */
+struct Intersection
+{
+ Intersection();
+ Intersection(int line_1, int line_2, Geom::Point &&intersection_point, Geom::Angle start_angle)
+ : line1{line_1}
+ , line2{line_2}
+ , point{intersection_point}
+ , polar_angle{point}
+ , relative_angle{polar_angle - start_angle}
+ {
+ }
+
+ int line1 = 0; ///< Index of the first of the intersecting lines.
+ int line2 = 0; ///< Index of the second of the intersecting lines.
+ Geom::Point point; ///< The geometric position of the intersection.
+ Geom::Angle polar_angle = 0.0; ///< Polar angle of the point (in radians).
+ /** Angle relative to the polar angle of the point at which the boundary of the polygon
+ * passes the origin at the minimum distance (i.e., where an expanding origin-centered
+ * circle inside the polygon starts touching an edge of the polygon.)
+ */
+ Geom::Angle relative_angle = 0.0;
+};
+
+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 Geom::Point to_pixel_coordinate(Geom::Point const &point, double scale, double resize);
+static Geom::Point from_pixel_coordinate(Geom::Point const &point, double scale, double resize);
+static std::vector<Geom::Point> to_pixel_coordinate(std::vector<Geom::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 {
+
+
+/* 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 / MAX_HUE;
+
+ 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()
+{
+ _picker_geometry = std::make_unique<Hsluv::PickerGeometry>();
+ setHsluv(MIN_HUE, MAX_SATURATION, 0.5 * MAX_LIGHTNESS);
+}
+
+void ColorWheelHSLuv::setRgb(double r, double g, double b, bool /*overrideHue*/)
+{
+ auto hsl = Hsluv::rgb_to_hsluv(r, g, b);
+ setHue(hsl[0]);
+ setSaturation(hsl[1]);
+ setLightness(hsl[2]);
+}
+
+void ColorWheelHSLuv::getRgb(double *r, double *g, double *b) const
+{
+ auto rgb = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]);
+ *r = rgb[0];
+ *g = rgb[1];
+ *b = rgb[2];
+}
+
+void ColorWheelHSLuv::getRgbV(double *rgb) const
+{
+ auto converted = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]);
+ for (size_t i : {0, 1, 2}) {
+ rgb[i] = converted[i];
+ }
+}
+
+guint32 ColorWheelHSLuv::getRgb() const
+{
+ auto rgb = Hsluv::hsluv_to_rgb(_values[0], _values[1], _values[2]);
+ return (
+ (static_cast<guint32>(rgb[0] * 255.0) << 16) |
+ (static_cast<guint32>(rgb[1] * 255.0) << 8) |
+ (static_cast<guint32>(rgb[2] * 255.0) )
+ );
+}
+
+void ColorWheelHSLuv::setHsluv(double h, double s, double l)
+{
+ setHue(h);
+ setSaturation(s);
+ setLightness(l);
+}
+
+/**
+ * Update the PickerGeometry structure owned by the instance.
+ */
+void ColorWheelHSLuv::updateGeometry()
+{
+ // Separate from the extremes to avoid overlapping intersections
+ double lightness = std::clamp(_values[2] + 0.01, 0.1, 99.9);
+
+ // Find the lines bounding the gamut polygon
+ auto const lines = Hsluv::get_bounds(lightness);
+
+ // Find the line closest to origin
+ Geom::Line const *closest_line = nullptr;
+ double closest_distance = -1;
+
+ for (auto const &line : lines) {
+ double d = Geom::distance(Geom::Point(0, 0), line);
+ if (closest_distance < 0 || d < closest_distance) {
+ closest_distance = d;
+ closest_line = &line;
+ }
+ }
+
+ g_assert(closest_line);
+ auto const nearest_time = closest_line->nearestTime(Geom::Point(0, 0));
+ Geom::Angle start_angle{closest_line->pointAt(nearest_time)};
+
+ std::vector<Intersection> intersections;
+ unsigned const num_lines = 6;
+ unsigned const max_intersections = num_lines * (num_lines - 1) / 2;
+ intersections.reserve(max_intersections);
+
+ for (int i = 0; i < num_lines - 1; i++) {
+ for (int j = i + 1; j < num_lines; j++) {
+ auto xings = lines[i].intersect(lines[j]);
+ if (xings.empty()) {
+ continue;
+ }
+ intersections.emplace_back(i, j, xings.front().point(), start_angle);
+ }
+ }
+
+ std::sort(intersections.begin(), intersections.end(), [](Intersection const &lhs, Intersection const &rhs) {
+ return lhs.relative_angle.radians0() >= rhs.relative_angle.radians0();
+ });
+
+ // Find the relevant vertices of the polygon, in the counter-clockwise order.
+ std::vector<Geom::Point> ordered_vertices;
+ double circumradius = 0.0;
+ unsigned current_index = closest_line - &lines[0];
+
+ for (auto const &intersection : intersections) {
+ if (intersection.line1 == current_index) {
+ current_index = intersection.line2;
+ } else if (intersection.line2 == current_index) {
+ current_index = intersection.line1;
+ } else {
+ continue;
+ }
+ ordered_vertices.emplace_back(intersection.point);
+ circumradius = std::max(circumradius, intersection.point.length());
+ }
+
+ _picker_geometry->vertices = std::move(ordered_vertices);
+ _picker_geometry->outer_circle_radius = circumradius;
+ _picker_geometry->inner_circle_radius = closest_distance;
+}
+
+void ColorWheelHSLuv::setLightness(double l)
+{
+ _values[2] = std::clamp(l, MIN_LIGHTNESS, MAX_LIGHTNESS);
+
+ // Update polygon
+ updateGeometry();
+ _scale = OUTER_CIRCLE_RADIUS / _picker_geometry->outer_circle_radius;
+ _updatePolygon();
+
+ queue_draw();
+}
+
+void ColorWheelHSLuv::getHsluv(double *h, double *s, double *l) const
+{
+ getValues(h, s, l);
+}
+
+Geom::IntPoint ColorWheelHSLuv::_getMargin(Gtk::Allocation const &allocation)
+{
+ int const width = allocation.get_width();
+ int const height = allocation.get_height();
+
+ return {std::max(0, (width - height) / 2),
+ std::max(0, (height - width) / 2)};
+}
+
+/// Detect whether we're at the top or bottom vertex of the color space.
+bool ColorWheelHSLuv::_vertex() const
+{
+ return _values[2] < VERTEX_EPSILON || _values[2] > MAX_LIGHTNESS - VERTEX_EPSILON;
+}
+
+bool ColorWheelHSLuv::on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr)
+{
+ Gtk::Allocation allocation = get_allocation();
+ auto dimensions = _getAllocationDimensions(allocation);
+ auto center = (0.5 * (Geom::Point)dimensions).floor();
+
+ auto size = _getAllocationSize(allocation);
+ double const resize = size / static_cast<double>(SIZE);
+
+ auto const margin = _getMargin(allocation);
+ auto polygon_vertices_px = to_pixel_coordinate(_picker_geometry->vertices, _scale, resize);
+ for (auto &point : polygon_vertices_px) {
+ point += margin;
+ }
+
+ bool const is_vertex = _vertex();
+ cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+
+ if (size > _square_size) {
+ if (_cache_width != dimensions[Geom::X] || _cache_height != dimensions[Geom::Y]) {
+ _updatePolygon();
+ }
+ if (!is_vertex) {
+ // Paint with surface, clipping to polygon
+ cr->save();
+ cr->set_source(_surface_polygon, 0, 0);
+ auto it = polygon_vertices_px.begin();
+ cr->move_to((*it)[Geom::X], (*it)[Geom::Y]);
+ for (++it; it != polygon_vertices_px.end(); ++it) {
+ cr->line_to((*it)[Geom::X], (*it)[Geom::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(center[Geom::X], center[Geom::Y], _scale * resize * _picker_geometry->outer_circle_radius, 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(center[Geom::X], center[Geom::Y], _scale * resize * _picker_geometry->outer_circle_radius, 0, 2 * M_PI);
+ cr->stroke();
+ cr->unset_dash();
+
+ // Contrast
+ auto [gray, alpha] = Hsluv::get_contrasting_color(Hsluv::perceptual_lightness(_values[2]));
+ cr->set_source_rgba(gray, gray, gray, alpha);
+
+ // Draw inscribed circle
+ double const inner_stroke_width = 2.0;
+ double inner_radius = is_vertex ? 0.01 : _picker_geometry->inner_circle_radius;
+ cr->set_line_width(inner_stroke_width);
+ cr->begin_new_path();
+ cr->arc(center[Geom::X], center[Geom::Y], _scale * resize * inner_radius, 0, 2 * M_PI);
+ cr->stroke();
+
+ // Center
+ cr->begin_new_path();
+ cr->arc(center[Geom::X], center[Geom::Y], 2, 0, 2 * M_PI);
+ cr->fill();
+
+ // Draw marker
+ auto luv = Hsluv::hsluv_to_luv(_values);
+ auto mp = to_pixel_coordinate({luv[1], luv[2]}, _scale, resize) + margin;
+
+ cr->set_line_width(inner_stroke_width);
+ cr->begin_new_path();
+ cr->arc(mp[Geom::X], mp[Geom::Y], 2 * inner_stroke_width, 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[Geom::X] - 4, mp[Geom::Y] - 4, 8, 8);
+
+ cr->set_line_width(0.25 * inner_stroke_width);
+ cr->set_source_rgb(1 - gray, 1 - gray, 1 - gray);
+ cr->begin_new_path();
+ cr->arc(mp[Geom::X], mp[Geom::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);
+ auto const p = from_pixel_coordinate(Geom::Point(x, y) - _getMargin(allocation), _scale, resize);
+
+ auto hsluv = Hsluv::luv_to_hsluv(_values[2], p[Geom::X], p[Geom::Y]);
+ setHue(hsluv[0]);
+ setSaturation(hsluv[1]);
+
+ _signal_color_changed.emit();
+ queue_draw();
+}
+
+void ColorWheelHSLuv::_updatePolygon()
+{
+ Gtk::Allocation allocation = get_allocation();
+ auto allocation_size = _getAllocationDimensions(allocation);
+ int const size = std::min(allocation_size[Geom::X], allocation_size[Geom::Y]);
+
+ // Update square size
+ _square_size = std::max(1, static_cast<int>(size / 50));
+ if (size < _square_size) {
+ return;
+ }
+
+ _cache_width = allocation_size[Geom::X];
+ _cache_height = allocation_size[Geom::Y];
+
+ double const resize = size / static_cast<double>(SIZE);
+
+ auto const margin = _getMargin(allocation);
+ auto polygon_vertices_px = to_pixel_coordinate(_picker_geometry->vertices, _scale, resize);
+
+ // Find the bounding rectangle containing all points (adjusted by the margin).
+ Geom::Rect bounding_rect;
+ for (auto const &point : polygon_vertices_px) {
+ bounding_rect.expandTo(point + margin);
+ }
+ bounding_rect *= Geom::Scale(1.0 / _square_size);
+
+ // Round to integer pixel coords
+ auto const bounding_max = bounding_rect.max().ceil();
+ auto const bounding_min = bounding_rect.min().floor();
+
+ int const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, _cache_width);
+
+ _buffer_polygon.resize(_cache_height * stride / 4);
+ std::vector<guint32> buffer_line;
+ buffer_line.resize(stride / 4);
+
+ ColorPoint clr;
+ auto const square_center = Geom::IntPoint(_square_size / 2, _square_size / 2);
+
+ // Set the color of each pixel/square
+ for (int y = bounding_min[Geom::Y]; y < bounding_max[Geom::Y]; y++) {
+ for (int x = bounding_min[Geom::X]; x < bounding_max[Geom::X]; x++) {
+ auto pos = Geom::IntPoint(x * _square_size, y * _square_size);
+ auto point = from_pixel_coordinate(pos + square_center - margin, _scale, resize);
+
+ auto rgb = Hsluv::luv_to_rgb(_values[2], point[Geom::X], point[Geom::Y]); // safe with _values[2] == 0
+ clr.set_color(rgb);
+
+ 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 const scaled_y = y * _square_size;
+ for (int i = 0; i < _square_size; i++) {
+ guint32 *t = _buffer_polygon.data() + (scaled_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, _cache_width, _cache_height, stride);
+}
+
+bool ColorWheelHSLuv::on_button_press_event(GdkEventButton* event)
+{
+ auto event_pt = Geom::Point(event->x, event->y);
+ Gtk::Allocation allocation = get_allocation();
+ int const size = _getAllocationSize(allocation);
+ auto const region = Geom::IntRect::from_xywh(_getMargin(allocation), {size, size});
+
+ if (region.contains(event_pt.round())) {
+ _adjusting = true;
+ grab_focus();
+ _setFromPoint(event_pt);
+ 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;
+ }
+ _set_from_xy(event->x, event->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
+ auto luv = Hsluv::hsluv_to_luv(_values);
+
+ double const marker_move = 1.0 / _scale;
+
+ switch (key) {
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ luv[2] += marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ luv[2] -= marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ luv[1] -= marker_move;
+ consumed = true;
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ luv[1] += marker_move;
+ consumed = true;
+ break;
+ }
+
+ if (consumed) {
+ auto hsluv = Hsluv::luv_to_hsluv(luv[0], luv[1], luv[1]);
+ setHue(hsluv[0]);
+ setSaturation(hsluv[1]);
+
+ _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)
+ );
+};
+
+static double lerp(double v0, double v1, double t0, double t1, double t)
+{
+ double const s = (t0 != t1) ? (t - t0) / (t1 - t0) : 0.0;
+ return Geom::lerp(s, v0, 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 a point of the 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 Geom::Point to_pixel_coordinate(Geom::Point const &point, double scale, double resize)
+{
+ return Geom::Point(
+ point[Geom::X] * scale * resize + (SIZE * resize / 2.0),
+ (SIZE * resize / 2.0) - point[Geom::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 Geom::Point from_pixel_coordinate(Geom::Point const &point, double scale, double resize)
+{
+ return Geom::Point(
+ (point[Geom::X] - (SIZE * resize / 2.0)) / (scale * resize),
+ ((SIZE * resize / 2.0) - point[Geom::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<Geom::Point> to_pixel_coordinate(std::vector<Geom::Point> const &points,
+ double scale, double resize)
+{
+ std::vector<Geom::Point> result;
+
+ for (auto 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;
+ }
+ }
+}
+
+/*
+ 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..5fbd05c
--- /dev/null
+++ b/src/ui/widget/ink-color-wheel.h
@@ -0,0 +1,167 @@
+// 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>
+#include <2geom/point.h>
+#include <2geom/line.h>
+
+#include "hsluv.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * @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 = default;
+
+ 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;
+ void updateGeometry();
+
+protected:
+ bool on_draw(::Cairo::RefPtr<::Cairo::Context> const &cr) override;
+
+private:
+ void _set_from_xy(double const x, double const y) override;
+ void _setFromPoint(Geom::Point const &pt) { _set_from_xy(pt[Geom::X], pt[Geom::Y]); }
+ void _updatePolygon();
+
+ static Geom::IntPoint _getMargin(Gtk::Allocation const &allocation);
+ inline static Geom::IntPoint _getAllocationDimensions(Gtk::Allocation const &allocation)
+ {
+ return {allocation.get_width(), allocation.get_height()};
+ }
+ inline static int _getAllocationSize(Gtk::Allocation const &allocation)
+ {
+ return std::min(allocation.get_width(), allocation.get_height());
+ }
+ bool _vertex() const;
+
+ // 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 = 1.0;
+ std::unique_ptr<Hsluv::PickerGeometry> _picker_geometry;
+ std::vector<guint32> _buffer_polygon;
+ Cairo::RefPtr<::Cairo::ImageSurface> _surface_polygon;
+ int _cache_width = 0, _cache_height = 0;
+ int _square_size = 1;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INK_COLORWHEEL_HSLUV_H
diff --git a/src/ui/widget/ink-ruler.cpp b/src/ui/widget/ink-ruler.cpp
new file mode 100644
index 0000000..1705978
--- /dev/null
+++ b/src/ui/widget/ink-ruler.cpp
@@ -0,0 +1,645 @@
+// 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
+ * 2022 Martin Owens
+ *
+ * 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 <gdkmm/rgba.h>
+#include <glibmm/ustring.h>
+#include <iostream>
+#include <cmath>
+
+#include "inkscape.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+#include "util/units.h"
+
+using Inkscape::Util::unit_table;
+
+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 );
+
+ set_no_show_all();
+
+ auto prefs = Inkscape::Preferences::get();
+ _watch_prefs = prefs->createObserver("/options/ruler/show_bbox", sigc::mem_fun(*this, &Ruler::on_prefs_changed));
+ on_prefs_changed();
+
+ INKSCAPE.themecontext->getChangeThemeSignal().connect(sigc::mem_fun(*this, &Ruler::on_style_updated));
+}
+
+void Ruler::on_prefs_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+ _sel_visible = prefs->getBool("/options/ruler/show_bbox", true);
+
+ _backing_store_valid = false;
+ queue_draw();
+}
+
+// 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(double lower, 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();
+ }
+}
+
+/**
+ * Set the location of the currently selected page.
+ */
+void Ruler::set_page(double lower, double upper)
+{
+ if (_page_lower != lower || _page_upper != upper) {
+ _page_lower = lower;
+ _page_upper = upper;
+
+ _backing_store_valid = false;
+ queue_draw();
+ }
+}
+
+/**
+ * Set the location of the currently selected page.
+ */
+void Ruler::set_selection(double lower, double upper)
+{
+ if (_sel_lower != lower || _sel_upper != upper) {
+ _sel_lower = lower;
+ _sel_upper = upper;
+
+ _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::on_motion_notify_event), 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::on_motion_notify_event(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;
+}
+
+bool Ruler::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 3) {
+ auto menu = getContextMenu();
+ menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ // Question to Reviewer: Does this leak?
+ return true;
+ }
+ 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 ruler's size from CSS style
+ GValue minimum_height = G_VALUE_INIT;
+ gtk_style_context_get_property(style_context->gobj(), "min-height", GTK_STATE_FLAG_NORMAL, &minimum_height);
+ auto size = g_value_get_int(&minimum_height);
+ g_value_unset(&minimum_height);
+
+ 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)
+{
+ Gtk::Allocation allocation = get_allocation();
+ int awidth = allocation.get_width();
+ int aheight = allocation.get_height();
+
+ // 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);
+
+ // background
+ auto context = get_style_context();
+ context->render_background(cr, 0, 0, awidth, aheight);
+
+ // Color in page indication box
+ if (double psize = std::abs(_page_upper - _page_lower)) {
+ Gdk::Cairo::set_source_rgba(cr, _page_fill);
+ cr->begin_new_path();
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->rectangle(_page_lower, 0, psize, aheight);
+ } else {
+ cr->rectangle(0, _page_lower, awidth, psize);
+ }
+ cr->fill();
+ } else {
+ g_warning("No size?");
+ }
+ cr->set_line_width(1.0);
+
+ // 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());
+
+ auto paint_line = [=](Gdk::RGBA color, int offset) {
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(0, offset - 0.5);
+ cr->line_to(allocation.get_width(), offset - 0.5);
+ } else {
+ cr->move_to(offset - 0.5, 0);
+ cr->line_to(offset - 0.5, allocation.get_height());
+ }
+ Gdk::Cairo::set_source_rgba(cr, color);
+ cr->stroke();
+ };
+
+ if (_orientation != Gtk::ORIENTATION_HORIZONTAL) {
+ // From here on, awidth is the longest dimension of the ruler, rheight is the shortest.
+ std::swap(awidth, aheight);
+ std::swap(rwidth, rheight);
+ }
+ // Draw bottom/right line of ruler
+ paint_line(_foreground, aheight);
+
+ // Draw a shadow which overlaps any previously painted object.
+ auto paint_shadow = [=](double size_x, double size_y, double width, double height) {
+ auto trans = change_alpha(_shadow, 0.0);
+ auto gr = create_cubic_gradient(Geom::Rect(0, 0, size_x, size_y), _shadow, trans, Geom::Point(0, 0.5), Geom::Point(0.5, 1));
+ cr->rectangle(0, 0, width, height);
+ cr->set_source(gr);
+ cr->fill();
+ };
+ int gradient_size = 4;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ paint_shadow(0, gradient_size, allocation.get_width(), gradient_size);
+ } else {
+ paint_shadow(gradient_size, 0, gradient_size, allocation.get_height());
+ }
+
+ // 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);
+ }
+
+ // Loop over all ticks
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ 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 - 7;
+ 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) {
+ cr->save();
+
+ int label_value = std::round(i * units_per_tick);
+
+ auto &label = _label_cache[label_value];
+ if (!label) {
+ label = draw_label(surface_in, label_value);
+ }
+
+ // Align text to pixel
+ int x = _border.get_left() + 3;
+ int y = position + 2.5;
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ x = position + 2.5;
+ y = _border.get_top() + 3;
+ }
+
+ // We don't know the surface height/width, damn you cairo.
+ cr->rectangle(x, y, 100, 100);
+ cr->clip();
+ cr->set_source(label, x, y);
+ cr->paint();
+ cr->restore();
+ }
+
+ // Draw ticks
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ 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();
+ }
+
+ // Draw a selection bar
+ if (_sel_lower != _sel_upper && _sel_visible) {
+
+ const auto radius = 3.0;
+ const auto delta = _sel_upper - _sel_lower;
+ const auto dxy = delta > 0 ? radius : -radius;
+ double sy0 = _sel_lower;
+ double sy1 = _sel_upper;
+ double sx0 = floor(aheight * 0.7);
+ double sx1 = sx0;
+
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ std::swap(sy0, sx0);
+ std::swap(sy1, sx1);
+ }
+
+ cr->set_line_width(2.0);
+
+ if (fabs(delta) > 2 * radius) {
+ Gdk::Cairo::set_source_rgba(cr, _select_stroke);
+ if (_orientation == Gtk::ORIENTATION_HORIZONTAL) {
+ cr->move_to(sx0 + dxy, sy0);
+ cr->line_to(sx1 - dxy, sy1);
+ }
+ else {
+ cr->move_to(sx0, sy0 + dxy);
+ cr->line_to(sx1, sy1 - dxy);
+ }
+ cr->stroke();
+ }
+
+ // Markers
+ Gdk::Cairo::set_source_rgba(cr, _select_fill);
+ cr->begin_new_path();
+ cr->arc(sx0, sy0, radius, 0, 2 * M_PI);
+ cr->arc(sx1, sy1, radius, 0, 2 * M_PI);
+ cr->fill();
+
+ Gdk::Cairo::set_source_rgba(cr, _select_stroke);
+ cr->begin_new_path();
+ cr->arc(sx0, sy0, radius, 0, 2 * M_PI);
+ cr->stroke();
+ cr->begin_new_path();
+ cr->arc(sx1, sy1, radius, 0, 2 * M_PI);
+ cr->stroke();
+ }
+
+ _backing_store_valid = true;
+ return true;
+}
+
+/**
+ * Generate the label as it's only small surface for caching.
+ */
+Cairo::RefPtr<Cairo::Surface> Ruler::draw_label(Cairo::RefPtr<Cairo::Surface> const &surface_in, int label_value)
+{
+ bool rotate = _orientation != Gtk::ORIENTATION_HORIZONTAL;
+
+ Glib::RefPtr<Pango::Layout> layout = create_pango_layout(std::to_string(label_value));
+ layout->set_font_description(_font);
+
+ int text_width;
+ int text_height;
+ layout->get_pixel_size(text_width, text_height);
+ if (rotate) {
+ std::swap(text_width, text_height);
+ }
+
+ auto surface = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, text_width, text_height);
+ Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(surface);
+
+ cr->save();
+ Gdk::Cairo::set_source_rgba(cr, _foreground);
+ if (rotate) {
+ cr->translate(text_width / 2, text_height / 2);
+ cr->rotate(-M_PI_2);
+ cr->translate(-text_height / 2, -text_width / 2);
+ }
+ layout->show_in_cairo_context(cr);
+ cr->restore();
+
+ return surface;
+}
+
+// Draw position marker, we use doubles here.
+void
+Ruler::draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr)
+{
+ Gtk::Allocation allocation = get_allocation();
+ const int awidth = allocation.get_width();
+ const int aheight = allocation.get_height();
+
+ 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()
+{
+ 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();
+
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ style_context->add_class(_orientation == Gtk::ORIENTATION_HORIZONTAL ? "horz" : "vert");
+
+ // Cache all our colors to speed up rendering.
+ _border = style_context->get_border();
+ _foreground = get_context_color(style_context, "color");
+ _font = style_context->get_font();
+ _font_size = _font.get_size();
+ if (!_font.get_size_is_absolute())
+ _font_size /= Pango::SCALE;
+
+ style_context->add_class("shadow");
+ _shadow = get_context_color(style_context, "border-color");
+ style_context->remove_class("shadow");
+
+ style_context->add_class("page");
+ _page_fill = get_background_color(style_context);
+ style_context->remove_class("page");
+
+ style_context->add_class("selection");
+ _select_fill = get_background_color(style_context);
+ _select_stroke = get_context_color(style_context, "border-color");
+ style_context->remove_class("selection");
+ _label_cache.clear();
+ _backing_store_valid = false;
+ queue_resize();
+ queue_draw();
+}
+
+/**
+ * Return a contextmenu for the ruler
+ */
+Gtk::Menu *Ruler::getContextMenu()
+{
+ auto gtk_menu = new Gtk::Menu();
+ auto gio_menu = Gio::Menu::create();
+ auto unit_menu = Gio::Menu::create();
+
+ for (auto &pair : unit_table.units(Inkscape::Util::UNIT_TYPE_LINEAR)) {
+ auto unit = pair.second.abbr;
+ Glib::ustring action_name = "doc.set-display-unit('" + unit + "')";
+ auto item = Gio::MenuItem::create(unit, action_name);
+ unit_menu->append_item(item);
+ }
+
+ gio_menu->append_section(unit_menu);
+ gtk_menu->bind_model(gio_menu, true);
+ gtk_menu->attach_to_widget(*this); // Might need canvas here
+ gtk_menu->show();
+ return gtk_menu;
+}
+
+} // 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..987519e
--- /dev/null
+++ b/src/ui/widget/ink-ruler.h
@@ -0,0 +1,112 @@
+// 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 "preferences.h"
+#include <gtkmm.h>
+#include <unordered_map>
+
+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(double lower, double upper);
+ void set_page(double lower, double upper);
+ void set_selection(double lower, double upper);
+
+ void add_track_widget(Gtk::Widget& widget);
+
+ 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;
+ void on_prefs_changed();
+
+ bool on_motion_notify_event(GdkEventMotion *motion_event) override;
+ bool on_button_press_event(GdkEventButton *button_event) override;
+
+private:
+ Inkscape::PrefObserver _watch_prefs;
+
+ Gtk::Menu *getContextMenu();
+ Cairo::RefPtr<Cairo::Surface> draw_label(Cairo::RefPtr<Cairo::Surface> const &surface_in, int label_value);
+
+ Gtk::Orientation _orientation;
+
+ Inkscape::Util::Unit const* _unit;
+ double _lower;
+ double _upper;
+ double _position;
+ double _max_size;
+
+ // Page block
+ double _page_lower = 0.0;
+ double _page_upper = 0.0;
+
+ // Selection block
+ double _sel_lower = 0.0;
+ double _sel_upper = 0.0;
+ double _sel_visible = true;
+
+ bool _backing_store_valid;
+
+ Cairo::RefPtr<::Cairo::Surface> _backing_store;
+ Cairo::RectangleInt _rect;
+
+ std::unordered_map<int, Cairo::RefPtr<::Cairo::Surface>> _label_cache;
+
+ // Cached style properties
+ Gtk::Border _border;
+ Gdk::RGBA _shadow;
+ Gdk::RGBA _foreground;
+ Pango::FontDescription _font;
+ int _font_size;
+ Gdk::RGBA _page_fill;
+ Gdk::RGBA _select_fill;
+ Gdk::RGBA _select_stroke;
+};
+
+} // 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..348092f
--- /dev/null
+++ b/src/ui/widget/labelled.cpp
@@ -0,0 +1,106 @@
+// 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)
+{
+ _widget->drag_dest_unset();
+ 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_markup(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..31ca2a0
--- /dev/null
+++ b/src/ui/widget/licensor.cpp
@@ -0,0 +1,157 @@
+// 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 */
+ constexpr bool read_only = false;
+ struct rdf_license_t * license = rdf_get_license(doc, read_only);
+
+ 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, read_only);
+}
+
+} // 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..0b712b2
--- /dev/null
+++ b/src/ui/widget/marker-combo-box.cpp
@@ -0,0 +1,814 @@
+// 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 "manipulation/copy-resource.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"
+#include "util/object-renderer.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 = Inkscape::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);
+ }
+ };
+
+ // delay setting scale to idle time; if invoked by focus change due to new marker selection
+ // it leads to marker list rebuild and apparent flowbox content corruption
+ auto idle_set_scale = [=](bool changeWidth) {
+ if (_update.pending()) return;
+
+ if (auto orig_marker = get_current()) {
+ _idle = Glib::signal_idle().connect([=](){
+ if (auto marker = get_current()) {
+ if (marker == orig_marker) {
+ set_scale(changeWidth);
+ }
+ }
+ return false; // don't call again
+ });
+ }
+ };
+
+ _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([=]() { idle_set_scale(true); });
+ _scale_y.signal_value_changed().connect([=]() { idle_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 (_idle) {
+ _idle.disconnect();
+ }
+ if (_document) {
+ modified_connection.disconnect();
+ }
+}
+
+void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) {
+ _input_grid.set_sensitive(marker != nullptr);
+
+ if (marker) {
+ _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.c_str());
+ }
+
+ _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 (is<SPMarker>(&child)) {
+ auto marker = cast<SPMarker>(&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());
+
+ /*
+ * 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 = 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 = 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 (is<SPMarker>(&child)) {
+ auto marker = cast<SPMarker>(&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.emplace_back(std::move(item));
+ }
+ else {
+ _stock_items.emplace_back(std::move(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)
+{
+ std::optional<guint32> checkerboard_color;
+ if (checkerboard) {
+ checkerboard_color = _background_color;
+ }
+ int device_scale = get_scale_factor();
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+
+ return Inkscape::create_marker_image(_combo_id, _sandbox.get(), fg, pixel_size, mname, source,
+ drawing, checkerboard_color, no_clip, scale, device_scale);
+}
+
+// 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();
+ }
+}
+
+} // 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..8e3436d
--- /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);
+ 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;
+ sigc::connection _idle;
+};
+
+} // 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..23daa04
--- /dev/null
+++ b/src/ui/widget/object-composite-settings.cpp
@@ -0,0 +1,302 @@
+// 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 (!is<SPItem>(i)) {
+ continue;
+ }
+ auto item = cast<SPItem>(i);
+ SPStyle *style = item->style;
+ g_assert(style != nullptr);
+ bool change_blend = set_blend_mode(item, _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) {
+ ; // update done already
+ } 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/objects-dialog-cells.cpp b/src/ui/widget/objects-dialog-cells.cpp
new file mode 100644
index 0000000..00ee641
--- /dev/null
+++ b/src/ui/widget/objects-dialog-cells.cpp
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Mike Kowalski
+ * Martin Owens
+ *
+ * Copyright (C) 2021-2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/objects-dialog-cells.h"
+#include "color-rgba.h"
+#include "preferences.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A colored tag cell which indicates which layer an object is in.
+ */
+ColorTagRenderer::ColorTagRenderer() :
+ Glib::ObjectBase(typeid(CellRenderer)),
+ Gtk::CellRenderer(),
+ _property_color(*this, "tagcolor", 0),
+ _property_hover(*this, "taghover", false)
+{
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ int dummy_width;
+ // height size is not critical
+ Gtk::IconSize::lookup(Gtk::ICON_SIZE_MENU, dummy_width, _height);
+}
+
+void ColorTagRenderer::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) {
+ cr->rectangle(cell_area.get_x(), cell_area.get_y(), cell_area.get_width(), cell_area.get_height());
+ ColorRGBA color(_property_color.get_value());
+ cr->set_source_rgb(color[0], color[1], color[2]);
+ cr->fill();
+ if (_property_hover.get_value()) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff);
+ double r = ((colorsetbase >> 24) & 0xFF) / 255.0;
+ double g = ((colorsetbase >> 16) & 0xFF) / 255.0;
+ double b = ((colorsetbase >> 8) & 0xFF) / 255.0;
+ cr->set_source_rgba(r, g, b, 0.6);
+ cr->rectangle(background_area.get_x() + 0.5, background_area.get_y() + 0.5, background_area.get_width() - 1.0, background_area.get_height() - 1.0);
+ cr->set_line_width(1.0);
+ cr->stroke();
+ }
+}
+
+void ColorTagRenderer::get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const {
+ min_w = nat_w = _width;
+}
+
+void ColorTagRenderer::get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const {
+ min_h = 1;
+ nat_h = _height;
+}
+
+bool ColorTagRenderer::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_clicked.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/objects-dialog-cells.h b/src/ui/widget/objects-dialog-cells.h
new file mode 100644
index 0000000..448ea1b
--- /dev/null
+++ b/src/ui/widget/objects-dialog-cells.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_UI_WIDGET_OBJECTS_CELLS_H
+#define SEEN_UI_WIDGET_OBJECTS_CELLS_H
+/*
+ * Authors:
+ * Mike Kowalski
+ * Martin Owens
+ *
+ * Copyright (C) 2021-2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/widget.h>
+#include <glibmm/property.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ColorTagRenderer : public Gtk::CellRenderer {
+public:
+ ColorTagRenderer();
+ ~ColorTagRenderer() override = default;
+
+ Glib::PropertyProxy<unsigned int> property_color() {
+ return _property_color.get_proxy();
+ }
+ Glib::PropertyProxy<bool> property_hover() {
+ return _property_hover.get_proxy();
+ }
+ sigc::signal<void (const Glib::ustring&)> signal_clicked() {
+ return _signal_clicked;
+ }
+
+ int get_width() const { return _width; }
+
+private:
+ 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;
+
+ int _width = 8;
+ int _height;
+ Glib::Property<unsigned int> _property_color;
+ Glib::Property<bool> _property_hover;
+ sigc::signal<void (const Glib::ustring&)> _signal_clicked;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_UI_WIDGET_OBJECTS_CELLS_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/oklab-color-wheel.cpp b/src/ui/widget/oklab-color-wheel.cpp
new file mode 100644
index 0000000..4324b4e
--- /dev/null
+++ b/src/ui/widget/oklab-color-wheel.cpp
@@ -0,0 +1,309 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file OKHSL color wheel widget implementation.
+ */
+/*
+ * Authors:
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/widget/oklab-color-wheel.h"
+
+#include <algorithm>
+
+#include "display/cairo-utils.h"
+#include "oklab.h"
+
+namespace Inkscape::UI::Widget {
+
+OKWheel::OKWheel()
+{
+ // Set to black
+ _values[H] = 0;
+ _values[S] = 0;
+ _values[L] = 0;
+}
+
+void OKWheel::setRgb(double r, double g, double b, bool)
+{
+ using namespace Oklab;
+ auto [h, s, l] = oklab_to_okhsl(rgb_to_oklab({ r, g, b }));
+ _values[H] = h * 2.0 * M_PI;
+ _values[S] = s;
+ bool const changed_lightness = _values[L] != l;
+ _values[L] = l;
+ if (changed_lightness) {
+ _updateChromaBounds();
+ _redrawDisc();
+ }
+}
+
+void OKWheel::getRgb(double *red, double *green, double *blue) const
+{
+ using namespace Oklab;
+ auto [r, g, b] = oklab_to_rgb(okhsl_to_oklab({ _values[H] / (2.0 * M_PI), _values[S], _values[L] }));
+ *red = r;
+ *green = g;
+ *blue = b;
+}
+
+guint32 OKWheel::getRgb() const
+{
+ guint32 result = 0x0;
+ double rgb[3];
+ getRgbV(rgb);
+ for (auto component : rgb) {
+ result <<= 8;
+ result |= SP_COLOR_F_TO_U(component);
+ }
+ return result;
+}
+
+/** @brief Compute the chroma bounds around the picker disc.
+ *
+ * Calculates the maximum absolute Lch chroma along rays emanating
+ * from the center of the picker disc. CHROMA_BOUND_SAMPLES evenly
+ * spaced rays will be used. The result is stored in _bounds.
+ */
+void OKWheel::_updateChromaBounds()
+{
+ double const angle_step = 360.0 / CHROMA_BOUND_SAMPLES;
+ double hue_angle_deg = 0.0;
+ for (unsigned i = 0; i < CHROMA_BOUND_SAMPLES; i++) {
+ _bounds[i] = Oklab::max_chroma(_values[L], hue_angle_deg);
+ hue_angle_deg += angle_step;
+ }
+}
+
+/** @brief Update the size of the color disc and margins
+ * depending on the widget's allocation.
+ *
+ * @return Whether the colorful disc background needs to be regenerated.
+ */
+bool OKWheel::_updateDimensions()
+{
+ auto allocation = get_allocation();
+ auto width = allocation.get_width();
+ auto height = allocation.get_height();
+ double new_radius = 0.5 * std::min(width, height);
+ // Allow the halo to fit at coordinate extrema.
+ new_radius -= HALO_RADIUS + 0.5 * HALO_STROKE;
+ bool disc_needs_redraw = (_disc_radius != new_radius);
+ _disc_radius = new_radius;
+ _margin = {std::max(0.0, 0.5 * (width - 2.0 * _disc_radius)),
+ std::max(0.0, 0.5 * (height - 2.0 * _disc_radius))};
+ return disc_needs_redraw;
+}
+
+/** @brief Compute the ARGB32 color for a point inside the picker disc.
+ *
+ * The picker disc is viewed as the unit disc in the xy-plane, with
+ * the y-axis pointing up. If the passed point lies outside of the unit
+ * disc, the returned color is the same as for a point rescaled to the
+ * unit circle (outermost possible color in that direction).
+ *
+ * @param point A point in the normalized disc coordinates.
+ * @return a Cairo-compatible ARGB32 color.
+ */
+uint32_t OKWheel::_discColor(Geom::Point const &point) const
+{
+ using namespace Oklab;
+ using Display::AssembleARGB32;
+
+ double saturation = point.length();
+ if (saturation == 0.0) {
+ auto [r, g, b] = oklab_to_rgb({ _values[L], 0.0, 0.0 });
+ return AssembleARGB32(0xFF, (guint)(r * 255.5), (guint)(g * 255.5), (guint)(b * 255.5));
+ } else if (saturation > 1.0) {
+ saturation = 1.0;
+ }
+
+ double const hue_radians = Geom::Angle(Geom::atan2(point)).radians0();
+
+ // Find the precomputed chroma bounds on both sides of this angle.
+ unsigned previous_sample = std::floor(hue_radians * 0.5 * CHROMA_BOUND_SAMPLES / M_PI);
+ if (previous_sample >= CHROMA_BOUND_SAMPLES) {
+ previous_sample = 0;
+ }
+ unsigned const next_sample = (previous_sample == CHROMA_BOUND_SAMPLES - 1) ? 0 : previous_sample + 1;
+ double const previous_sample_angle = 2.0 * M_PI * previous_sample / CHROMA_BOUND_SAMPLES;
+ double const angle_delta = hue_radians - previous_sample_angle;
+ double const t = angle_delta * 0.5 * CHROMA_BOUND_SAMPLES / M_PI;
+ double const chroma_bound_estimate = Geom::lerp(t, _bounds[previous_sample], _bounds[next_sample]);
+ double const absolute_chroma = chroma_bound_estimate * saturation;
+
+ auto [r, g, b] = oklab_to_rgb(oklch_radians_to_oklab({ _values[L], absolute_chroma, hue_radians }));
+ return AssembleARGB32(0xFF, (guint)(r * 255.5), (guint)(g * 255.5), (guint)(b * 255.5));
+}
+
+/** @brief Returns the position of the current color in the coordinates
+ * of the picker wheel.
+ *
+ * The picker wheel is inscribed in a square with side length 2 * _disc_radius.
+ * The point (0, 0) corresponds to the center of the disc; y-axis points down.
+ */
+Geom::Point OKWheel::_curColorWheelCoords() const
+{
+ Geom::Point result;
+ Geom::sincos(_values[H], result.y(), result.x());
+ result *= _values[S];
+ return result * Geom::Scale(_disc_radius, -_disc_radius);
+}
+
+/** @brief Draw the widget into the Cairo context. */
+bool OKWheel::on_draw(Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ if(_updateDimensions()) {
+ _redrawDisc();
+ }
+
+ cr->save();
+ cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+
+ // Draw the colorful disc background from the cached pixbuf,
+ // clipping to a geometric circle (avoids aliasing).
+ cr->translate(_margin[Geom::X], _margin[Geom::Y]);
+ cr->move_to(2 * _disc_radius, _disc_radius);
+ cr->arc(_disc_radius, _disc_radius, _disc_radius, 0.0, 2.0 * M_PI);
+ cr->close_path();
+ cr->set_source(_disc, 0, 0);
+ cr->fill();
+
+ // Draw the halo around the current color.
+ {
+ auto const where = _curColorWheelCoords();
+ cr->translate(_disc_radius, _disc_radius);
+ cr->move_to(where.x() + HALO_RADIUS, where.y());
+ cr->arc(where.x(), where.y(), HALO_RADIUS, 0.0, 2.0 * M_PI);
+ cr->close_path();
+ // Fill the halo with the current color.
+ {
+ double r, g, b;
+ getRgb(&r, &g, &b);
+ cr->set_source_rgba(r, g, b, 1.0);
+ }
+ cr->fill_preserve();
+
+ // Stroke the border of the halo.
+ {
+ auto [gray, alpha] = Hsluv::get_contrasting_color(_values[L]);
+ cr->set_source_rgba(gray, gray, gray, alpha);
+ }
+ cr->set_line_width(HALO_STROKE);
+ cr->stroke();
+ }
+ cr->restore();
+ return true;
+}
+
+/** @brief Recreate the pixel buffer containing the colourful disc. */
+void OKWheel::_redrawDisc()
+{
+ int const size = std::ceil(2.0 * _disc_radius);
+ _pixbuf.resize(4 * size * size);
+
+ double const radius = 0.5 * size;
+ double const inverse_radius = 1.0 / radius;
+
+ // Fill buffer with (<don't care>, R, G, B) values.
+ uint32_t *pos = (uint32_t *)(_pixbuf.data());
+ for (int y = 0; y < size; y++) {
+ // Convert (x, y) to a coordinate system where the
+ // disc is the unit disc and the y-axis points up.
+ double const normalized_y = inverse_radius * (radius - y);
+ for (int x = 0; x < size; x++) {
+ auto const pt = Geom::Point(inverse_radius * (x - radius), normalized_y);
+ *pos++ = _discColor(pt);
+ }
+ }
+
+ int const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, size);
+ _disc = Cairo::ImageSurface::create(_pixbuf.data(), Cairo::FORMAT_RGB24, size, size, stride);
+}
+
+/** @brief Convert widget (event) coordinates to an abstract coordinate system
+ * in which the picker disc is the unit disc and the y-axis points up.
+ */
+Geom::Point OKWheel::_event2abstract(Geom::Point const &event_pt) const
+{
+ auto result = event_pt - _margin - Geom::Point(_disc_radius, _disc_radius);
+ double const scale = 1.0 / _disc_radius;
+ return result * Geom::Scale(scale, -scale);
+}
+
+/** @brief Set the current color based on a point on the wheel.
+ *
+ * @param pt A point in the abstract coordinate system in which the picker
+ * disc is the unit disc and the y-axis points up.
+ */
+void OKWheel::_setColor(Geom::Point const &pt)
+{
+ _values[S] = std::clamp(pt.length(), 0.0, 1.0);
+ Geom::Angle clicked_hue = _values[S] ? Geom::atan2(pt) : 0.0;
+ _values[H] = clicked_hue.radians0();
+ _signal_color_changed.emit();
+ queue_draw();
+}
+
+/** @brief Handle a left mouse click on the widget.
+ *
+ * @param pt The clicked point expressed in the coordinate system in which
+ * the picker disc is the unit disc and the y-axis points up.
+ * @return Whether the click has been handled.
+ */
+bool OKWheel::_onClick(Geom::Point const &pt)
+{
+ auto r = pt.length();
+ if (r > 1.0) { // Clicked outside the disc, no cookie.
+ return false;
+ }
+ _adjusting = true;
+ _setColor(pt);
+ return true;
+}
+
+/** @brief Handle a button press event. */
+bool OKWheel::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 1) {
+ // Convert the click coordinates to the abstract coords in which
+ // the picker disc is the unit disc in the xy-plane.
+ return _onClick(_event2abstract({event->x, event->y}));
+ }
+ // TODO: add a context menu to copy out the CSS4 color values.
+ return false;
+}
+
+/** @brief Handle a button release event. */
+bool OKWheel::on_button_release_event(GdkEventButton *event)
+{
+ _adjusting = false;
+ return true;
+}
+
+/** @brief Handle a drag (motion notify event). */
+bool OKWheel::on_motion_notify_event(GdkEventMotion *event)
+{
+ if (!_adjusting) {
+ return false;
+ }
+ _setColor(_event2abstract({event->x, event->y}));
+ return true;
+}
+
+} // namespace Inkscape::UI::Widget
+
+/*
+ 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 : \ No newline at end of file
diff --git a/src/ui/widget/oklab-color-wheel.h b/src/ui/widget/oklab-color-wheel.h
new file mode 100644
index 0000000..f1a55fd
--- /dev/null
+++ b/src/ui/widget/oklab-color-wheel.h
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file OKHSL color wheel widget, based on the OKLab/OKLch color space.
+ */
+/*
+ * Authors:
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_OKLAB_COLOR_WHEEL_H
+#define SEEN_OKLAB_COLOR_WHEEL_H
+
+#include "ui/widget/ink-color-wheel.h"
+
+namespace Inkscape::UI::Widget {
+
+/** @brief The color wheel used in the OKHSL picker. */
+class OKWheel : public ColorWheel
+{
+public:
+ OKWheel();
+ ~OKWheel() override = default;
+
+ /** @brief Set the displayed color to the specified gamma-compressed sRGB color. */
+ void setRgb(double r, double g, double b, bool overrideHue = true) override;
+
+ /** @brief Get the gamma-compressed sRGB color from the picker wheel. */
+ void getRgb(double *r, double *g, double *b) const override;
+ void getRgbV(double *rgb) const override { getRgb(rgb, rgb + 1, rgb + 2); }
+ guint32 getRgb() const override;
+
+protected:
+ bool on_draw(Cairo::RefPtr<Cairo::Context> const &cr) override;
+
+private:
+ static unsigned constexpr H = 0, S = 1, L = 2; ///< Indices into _values
+
+ /** How many samples for the chroma bounds to use for the color disc.
+ * A larger value produces a nicer gradient at the cost of slower performance.
+ */
+ static unsigned constexpr CHROMA_BOUND_SAMPLES = 120;
+ static double constexpr HALO_RADIUS = 4.5; ///< Radius of the halo around the current color.
+ static double constexpr HALO_STROKE = 1.5; ///< Width of the halo's stroke.
+
+ Geom::Point _curColorWheelCoords() const;
+ uint32_t _discColor(Geom::Point const &point) const;
+ Geom::Point _event2abstract(Geom::Point const &point) const;
+ void _redrawDisc();
+ void _setColor(Geom::Point const &pt);
+ void _updateChromaBounds();
+ bool _updateDimensions();
+
+ // Event handlers
+ bool on_button_press_event(GdkEventButton *event) override;
+ bool _onClick(Geom::Point const &unit_pos);
+ bool on_button_release_event(GdkEventButton *event) override;
+ bool on_motion_notify_event(GdkEventMotion *event) override;
+
+ double _disc_radius = 1.0;
+ Geom::Point _margin;
+ Cairo::RefPtr<Cairo::ImageSurface> _disc;
+ std::vector<uint8_t> _pixbuf;
+ std::array<double, CHROMA_BOUND_SAMPLES> _bounds;
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // SEEN_OKLAB_COLOR_WHEEL_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 : \ No newline at end of file
diff --git a/src/ui/widget/optglarea.cpp b/src/ui/widget/optglarea.cpp
new file mode 100644
index 0000000..72ec362
--- /dev/null
+++ b/src/ui/widget/optglarea.cpp
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <cassert>
+#include "optglarea.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+OptGLArea::OptGLArea()
+{
+ set_app_paintable(true); // No problem for GTK4 port since this whole widget will be deleted.
+ opengl_enabled = false;
+}
+
+void OptGLArea::on_realize()
+{
+ Gtk::DrawingArea::on_realize();
+ if (opengl_enabled) init_opengl();
+}
+
+void OptGLArea::on_unrealize()
+{
+ if (context) {
+ if (framebuffer) {
+ context->make_current();
+ delete_framebuffer();
+ }
+ if (context == Gdk::GLContext::get_current()) {
+ Gdk::GLContext::clear_current(); // ?
+ }
+ context.reset();
+ }
+ Gtk::DrawingArea::on_unrealize();
+}
+
+void OptGLArea::on_size_allocate(Gtk::Allocation &allocation)
+{
+ Gtk::DrawingArea::on_size_allocate(allocation);
+ if (get_realized()) need_resize = true;
+}
+
+void OptGLArea::set_opengl_enabled(bool enabled)
+{
+ if (opengl_enabled == enabled) return;
+ opengl_enabled = enabled;
+ if (opengl_enabled && get_realized()) init_opengl();
+}
+
+void OptGLArea::init_opengl()
+{
+ context = create_context();
+ if (!context) opengl_enabled = false;
+ framebuffer = 0;
+ need_resize = true;
+}
+
+void OptGLArea::make_current()
+{
+ assert(context);
+ context->make_current();
+}
+
+bool OptGLArea::on_draw(const Cairo::RefPtr<Cairo::Context> &cr)
+{
+ if (opengl_enabled) {
+ context->make_current();
+
+ if (!framebuffer) {
+ create_framebuffer();
+ }
+
+ if (need_resize) {
+ resize_framebuffer();
+ need_resize = false;
+ }
+
+ paint_widget(cr);
+
+ int s = get_scale_factor();
+ int w = get_allocated_width() * s;
+ int h = get_allocated_height() * s;
+ gdk_cairo_draw_from_gl(cr->cobj(), get_window()->gobj(), renderbuffer, GL_RENDERBUFFER, s, 0, 0, w, h);
+
+ context->make_current(); // ?
+ } else {
+ paint_widget(cr);
+ }
+
+ return true;
+}
+
+void OptGLArea::bind_framebuffer() const
+{
+ assert(context);
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, stencilbuffer);
+}
+
+void OptGLArea::create_framebuffer()
+{
+ glGenFramebuffers (1, &framebuffer);
+ glGenRenderbuffers(1, &renderbuffer);
+ glGenRenderbuffers(1, &stencilbuffer);
+}
+
+void OptGLArea::delete_framebuffer()
+{
+ glDeleteRenderbuffers(1, &renderbuffer);
+ glDeleteRenderbuffers(1, &stencilbuffer);
+ glDeleteFramebuffers (1, &framebuffer);
+}
+
+void OptGLArea::resize_framebuffer() const
+{
+ int s = get_scale_factor();
+ int w = get_allocated_width() * s;
+ int h = get_allocated_height() * s;
+ glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, w, h);
+ glBindRenderbuffer(GL_RENDERBUFFER, stencilbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/optglarea.h b/src/ui/widget/optglarea.h
new file mode 100644
index 0000000..fa71afc
--- /dev/null
+++ b/src/ui/widget/optglarea.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_OPTGLAREA_H
+#define INKSCAPE_UI_WIDGET_OPTGLAREA_H
+
+#include <gtkmm.h>
+#include <epoxy/gl.h>
+
+namespace Cairo {
+class Context;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+/**
+ * A widget that can dynamically switch between a Gtk::DrawingArea and a Gtk::GLArea.
+ * Based on the GTK source code for both widgets.
+ */
+class OptGLArea : public Gtk::DrawingArea
+{
+public:
+ OptGLArea();
+
+ /**
+ * Set whether OpenGL is enabled. Initially it is disabled. Upon enabling it,
+ * create_context will be called as soon as the widget is realized. If
+ * context creation fails, OpenGL will be disabled again.
+ */
+ void set_opengl_enabled(bool);
+ bool get_opengl_enabled() const { return opengl_enabled; }
+
+ /**
+ * Call before doing any OpenGL operations to make the context current.
+ * Automatically done before calling opengl_render.
+ */
+ void make_current();
+
+ /**
+ * Call before rendering to the widget to bind the widget's framebuffer.
+ */
+ void bind_framebuffer() const;
+
+protected:
+ void on_realize() override;
+ void on_unrealize() override;
+ void on_size_allocate(Gtk::Allocation&) override;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>&) final;
+
+ /**
+ * Reimplement to create the desired OpenGL context. Return nullptr on error.
+ */
+ virtual Glib::RefPtr<Gdk::GLContext> create_context() = 0;
+
+ /**
+ * Reimplement to render the widget. The Cairo context is only for when OpenGL is disabled.
+ */
+ virtual void paint_widget(const Cairo::RefPtr<Cairo::Context>&) {}
+
+private:
+ void init_opengl();
+ void create_framebuffer();
+ void delete_framebuffer();
+ void resize_framebuffer() const;
+
+ Glib::RefPtr<Gdk::GLContext> context;
+
+ bool opengl_enabled;
+ bool need_resize;
+
+ GLuint framebuffer;
+ GLuint renderbuffer;
+ GLuint stencilbuffer;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_OPTGLAREA_H
diff --git a/src/ui/widget/page-properties.cpp b/src/ui/widget/page-properties.cpp
new file mode 100644
index 0000000..927544b
--- /dev/null
+++ b/src/ui/widget/page-properties.cpp
@@ -0,0 +1,525 @@
+// 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 "ui/widget/spinbutton.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))
+#define GETD(prop, id) prop(get_derived_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"),
+ GETD(_page_width, "page-width"),
+ GETD(_page_height, "page-height"),
+ GET(_portrait, "page-portrait"),
+ GET(_landscape, "page-landscape"),
+ GETD(_scale_x, "scale-x"),
+ GET(_doc_units, "user-units"),
+ GET(_unsupported_size, "unsupported"),
+ GET(_nonuniform_scale, "nonuniform-scale"),
+ GETD(_viewbox_x, "viewbox-x"),
+ GETD(_viewbox_y, "viewbox-y"),
+ GETD(_viewbox_width, "viewbox-width"),
+ GETD(_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(_clip_to_page, "clip-to-page"),
+ GET(_page_label_style, "page-label-style"),
+ 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
+#undef GETD
+
+ _backgnd_color_picker = std::make_unique<ColorPicker>(
+ _("Background color"), "", 0xffffff00, true,
+ &get_widget<Gtk::Button>(_builder, "background-color"));
+ _backgnd_color_picker->use_transparency(false);
+
+ _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, Check::ClipToPage, Check::PageLabelStyle}) {
+ 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.empty() ? _(templ->name.c_str()) : _("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;
+ case Check::ClipToPage: return _clip_to_page;
+ case Check::PageLabelStyle: return _page_label_style;
+
+ 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;
+ MathSpinButton& _page_width;
+ MathSpinButton& _page_height;
+ Gtk::RadioButton& _portrait;
+ Gtk::RadioButton& _landscape;
+ MathSpinButton& _scale_x;
+ Gtk::Label& _unsupported_size;
+ Gtk::Label& _nonuniform_scale;
+ Gtk::Label& _doc_units;
+ MathSpinButton& _viewbox_x;
+ MathSpinButton& _viewbox_y;
+ MathSpinButton& _viewbox_width;
+ MathSpinButton& _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::CheckButton& _clip_to_page;
+ Gtk::CheckButton& _page_label_style;
+ 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..a2d0e06
--- /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, ClipToPage, PageLabelStyle };
+ 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..38e1410
--- /dev/null
+++ b/src/ui/widget/page-selector.cpp
@@ -0,0 +1,198 @@
+// 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)
+{
+ _selector_changed_connection.block();
+ _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);
+ break;
+ }
+ }
+ }
+ _selector_changed_connection.unblock();
+}
+
+/**
+ * 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..6cf2ed3
--- /dev/null
+++ b/src/ui/widget/page-size-preview.cpp
@@ -0,0 +1,186 @@
+// 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..480bea0
--- /dev/null
+++ b/src/ui/widget/paint-selector.cpp
@@ -0,0 +1,1267 @@
+// 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 "pattern-manipulation.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/pattern-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));
+ {
+#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;
+}
+
+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 (is<SPMeshGradient>(meshe) && cast<SPGradient>(meshe) == cast<SPGradient>(meshe)->getArray()) { // only if this is a
+ // root mesh
+ pl.push_back(cast<SPMeshGradient>(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 && is<SPMeshGradient>(mesh_obj)) {
+ mesh = cast<SPMeshGradient>(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(); }
+
+
+/*update pattern list*/
+void PaintSelector::updatePatternList(SPPattern *pattern)
+{
+ if (_update) return;
+ if (!_selector_pattern) return;
+
+ _selector_pattern->set_selected(pattern);
+}
+
+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) {
+ _selector_pattern = Gtk::manage(new PatternEditor("/pattern-edit", PatternManager::get()));
+ _selector_pattern->signal_changed().connect([=](){ _signal_changed.emit(); });
+ _selector_pattern->signal_color_changed().connect([=](unsigned){ _signal_changed.emit(); });
+ _selector_pattern->signal_edit().connect([=](){ _signal_edit_pattern.emit(); });
+ _selector_pattern->show_all();
+ _frame->add(*_selector_pattern);
+ }
+
+ SPDocument* document = SP_ACTIVE_DOCUMENT;
+ _selector_pattern->set_document(document);
+ _selector_pattern->show();
+ _label->hide();
+ }
+#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;
+}
+
+std::optional<unsigned int> PaintSelector::get_pattern_color() {
+ if (!_selector_pattern) return 0;
+
+ return _selector_pattern->get_selected_color();
+}
+
+Geom::Affine PaintSelector::get_pattern_transform() {
+ Geom::Affine matrix;
+ if (!_selector_pattern) return matrix;
+
+ return _selector_pattern->get_selected_transform();
+}
+
+Geom::Point PaintSelector::get_pattern_offset() {
+ Geom::Point offset;
+ if (!_selector_pattern) return offset;
+
+ return _selector_pattern->get_selected_offset();
+}
+
+Geom::Scale PaintSelector::get_pattern_gap() {
+ Geom::Scale gap(0, 0);
+ if (!_selector_pattern) return gap;
+
+ return _selector_pattern->get_selected_gap();
+}
+
+Glib::ustring PaintSelector::get_pattern_label() {
+ if (!_selector_pattern) return Glib::ustring();
+
+ return _selector_pattern->get_label();
+}
+
+bool PaintSelector::is_pattern_scale_uniform() {
+ if (!_selector_pattern) return false;
+
+ return _selector_pattern->is_selected_scale_uniform();
+}
+
+SPPattern* PaintSelector::getPattern() {
+ g_return_val_if_fail(_mode == MODE_PATTERN, nullptr);
+
+ if (!_selector_pattern) return nullptr;
+
+ auto sel = _selector_pattern->get_selected();
+ auto stock_doc = sel.second;
+
+ if (sel.first.empty()) return nullptr;
+
+ auto patid = sel.first;
+ SPObject* pat_obj = nullptr;
+ if (patid != "none") {
+ if (stock_doc) {
+ patid = "urn:inkscape:pattern:" + patid;
+ }
+ pat_obj = get_stock_item(patid.c_str(), stock_doc != nullptr, stock_doc);
+ } else {
+ SPDocument *doc = SP_ACTIVE_DOCUMENT;
+ pat_obj = doc->getObjectById(patid);
+ }
+
+ return cast<SPPattern>(pat_obj);
+}
+
+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(),
+ (is<SPGradient>(server) ? "Y" : "n"),
+ (is<SPGradient>(server) && cast<SPGradient>(server)->getVector()->isSwatch() ? "Y" : "n"));
+#endif // SP_PS_VERBOSE
+
+
+ if (server && is<SPGradient>(server) && cast<SPGradient>(server)->getVector()->isSwatch()) {
+ mode = MODE_SWATCH;
+ } else if (is<SPLinearGradient>(server)) {
+ mode = MODE_GRADIENT_LINEAR;
+ } else if (is<SPRadialGradient>(server)) {
+ mode = MODE_GRADIENT_RADIAL;
+#ifdef WITH_MESH
+ } else if (is<SPMeshGradient>(server)) {
+ mode = MODE_GRADIENT_MESH;
+#endif
+ } else if (is<SPPattern>(server)) {
+ mode = MODE_PATTERN;
+ } else if (is<SPHatch>(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..34ebc17
--- /dev/null
+++ b/src/ui/widget/paint-selector.h
@@ -0,0 +1,236 @@
+// 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 <optional>
+
+#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;
+class PatternEditor;
+
+/**
+ * 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;
+ SwatchSelector *_selector_swatch = nullptr;
+ PatternEditor* _selector_pattern = 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;
+ sigc::signal<void> _signal_edit_pattern;
+
+ 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; }
+ inline decltype(_signal_edit_pattern) signal_edit_pattern() const { return _signal_edit_pattern; }
+
+ 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();
+ std::optional<unsigned int> get_pattern_color();
+ Geom::Affine get_pattern_transform();
+ Geom::Point get_pattern_offset();
+ Geom::Scale get_pattern_gap();
+ Glib::ustring get_pattern_label();
+ bool is_pattern_scale_uniform();
+};
+
+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/pattern-editor.cpp b/src/ui/widget/pattern-editor.cpp
new file mode 100644
index 0000000..e31cad0
--- /dev/null
+++ b/src/ui/widget/pattern-editor.cpp
@@ -0,0 +1,685 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * Pattern editor widget for "Fill and Stroke" dialog
+ *
+ * Copyright (C) 2022 Michael Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "pattern-editor.h"
+
+#include <gtkmm/widget.h>
+#include <optional>
+#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 <iomanip>
+
+#include "object/sp-defs.h"
+#include "object/sp-root.h"
+#include "style.h"
+#include "ui/builder-utils.h"
+#include "ui/svg-renderer.h"
+#include "io/resource.h"
+#include "manipulation/copy-resource.h"
+#include "pattern-manager.h"
+#include "pattern-manipulation.h"
+#include "preferences.h"
+#include "util/units.h"
+#include "widgets/spw-utilities.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+using namespace Inkscape::IO;
+
+// default size of pattern image in a list
+static const int ITEM_WIDTH = 45;
+
+// get slider position 'index' (linear) and transform that into gap percentage (non-linear)
+static double slider_to_gap(double index, double upper) {
+ auto v = std::tan(index / (upper + 1) * M_PI / 2.0) * 500;
+ return std::round(v / 20) * 20;
+}
+// transform gap percentage value into slider position
+static double gap_to_slider(double gap, double upper) {
+ return std::atan(gap / 500) * (upper + 1) / M_PI * 2;
+}
+
+// tile size slider functions
+static int slider_to_tile(double index) {
+ return 30 + static_cast<int>(index) * 5;
+}
+static double tile_to_slider(int tile) {
+ return (tile - 30) / 5.0;
+}
+
+Glib::ustring get_attrib(SPPattern* pattern, const char* attrib) {
+ auto value = pattern->getAttribute(attrib);
+ return value ? value : "";
+}
+
+double get_attrib_num(SPPattern* pattern, const char* attrib) {
+ auto val = get_attrib(pattern, attrib);
+ return strtod(val.c_str(), nullptr);
+}
+
+const double ANGLE_STEP = 15.0;
+
+PatternEditor::PatternEditor(const char* prefs, Inkscape::PatternManager& manager) :
+ _manager(manager),
+ _builder(create_builder("pattern-edit.glade")),
+ _offset_x(get_widget<Gtk::SpinButton>(_builder, "offset-x")),
+ _offset_y(get_widget<Gtk::SpinButton>(_builder, "offset-y")),
+ _scale_x(get_widget<Gtk::SpinButton>(_builder, "scale-x")),
+ _scale_y(get_widget<Gtk::SpinButton>(_builder, "scale-y")),
+ _angle_btn(get_widget<Gtk::SpinButton>(_builder, "angle")),
+ _orient_slider(get_widget<Gtk::Scale>(_builder, "orient")),
+ _gap_x_slider(get_widget<Gtk::Scale>(_builder, "gap-x")),
+ _gap_y_slider(get_widget<Gtk::Scale>(_builder, "gap-y")),
+ _edit_btn(get_widget<Gtk::Button>(_builder, "edit-pattern")),
+ _preview_img(get_widget<Gtk::Image>(_builder, "preview")),
+ _preview(get_widget<Gtk::Viewport>(_builder, "preview-box")),
+ _color_btn(get_widget<Gtk::Button>(_builder, "color-btn")),
+ _color_label(get_widget<Gtk::Label>(_builder, "color-label")),
+ _paned(get_widget<Gtk::Paned>(_builder, "paned")),
+ _main_grid(get_widget<Gtk::Box>(_builder, "main-box")),
+ _input_grid(get_widget<Gtk::Grid>(_builder, "input-grid")),
+ _stock_gallery(get_widget<Gtk::FlowBox>(_builder, "flowbox")),
+ _doc_gallery(get_widget<Gtk::FlowBox>(_builder, "doc-flowbox")),
+ _link_scale(get_widget<Gtk::Button>(_builder, "link-scale")),
+ _name_box(get_widget<Gtk::Entry>(_builder, "pattern-name")),
+ _combo_set(get_widget<Gtk::ComboBoxText>(_builder, "pattern-combo")),
+ _search_box(get_widget<Gtk::SearchEntry>(_builder, "search")),
+ _tile_slider(get_widget<Gtk::Scale>(_builder, "tile-slider")),
+ _show_names(get_widget<Gtk::CheckButton>(_builder, "show-names")),
+ _prefs(prefs)
+{
+ _color_picker = std::make_unique<ColorPicker>(
+ _("Pattern color"), "", 0x7f7f7f00, true,
+ &get_widget<Gtk::Button>(_builder, "color-btn"));
+ _color_picker->use_transparency(false);
+ _color_picker->connectChanged([=](guint color){
+ if (_update.pending()) return;
+ _signal_color_changed.emit(color);
+ });
+
+ _tile_size = Inkscape::Preferences::get()->getIntLimited(_prefs + "/tileSize", ITEM_WIDTH, 30, 1000);
+ _tile_slider.set_value(tile_to_slider(_tile_size));
+ _tile_slider.signal_change_value().connect([=](Gtk::ScrollType st, double value){
+ if (_update.pending()) return true;
+ auto scoped(_update.block());
+ auto size = slider_to_tile(value);
+ if (size != _tile_size) {
+ _tile_slider.set_value(tile_to_slider(size));
+ // change pattern tile size
+ _tile_size = size;
+ update_pattern_tiles();
+ Inkscape::Preferences::get()->setInt(_prefs + "/tileSize", size);
+ }
+ return true;
+ });
+
+ auto show_labels = Inkscape::Preferences::get()->getBool(_prefs + "/showLabels", false);
+ _show_names.set_active(show_labels);
+ _show_names.signal_toggled().connect([=](){
+ // toggle pattern labels
+ _stock_pattern_store.store.refresh();
+ _doc_pattern_store.store.refresh();
+ Inkscape::Preferences::get()->setBool(_prefs + "/showLabels", _show_names.get_active());
+ });
+
+ const auto max = 180.0 / ANGLE_STEP;
+ _orient_slider.set_range(-max, max);
+ _orient_slider.set_increments(1, 1);
+ _orient_slider.set_digits(0);
+ _orient_slider.set_value(0);
+ _orient_slider.signal_change_value().connect([=](Gtk::ScrollType st, double value){
+ if (_update.pending()) return false;
+ auto scoped(_update.block());
+ // slider works with 15deg discrete steps
+ _angle_btn.set_value(round(CLAMP(value, -max, max)) * ANGLE_STEP);
+ _signal_changed.emit();
+ return true;
+ });
+
+ for (auto slider : {&_gap_x_slider, &_gap_y_slider}) {
+ slider->set_increments(1, 1);
+ slider->set_digits(0);
+ slider->set_value(0);
+ slider->signal_format_value().connect([=](double val){
+ auto upper = slider->get_adjustment()->get_upper();
+ return Glib::ustring::format(std::fixed, std::setprecision(0), slider_to_gap(val, upper)) + "%";
+ });
+ slider->signal_change_value().connect([=](Gtk::ScrollType st, double value){
+ if (_update.pending()) return false;
+ _signal_changed.emit();
+ return true;
+ });
+ }
+
+ _angle_btn.signal_value_changed().connect([=]() {
+ if (_update.pending() || !_angle_btn.is_sensitive()) return;
+ auto scoped(_update.block());
+ auto angle = _angle_btn.get_value();
+ _orient_slider.set_value(round(angle / ANGLE_STEP));
+ _signal_changed.emit();
+ });
+
+ _link_scale.signal_clicked().connect([=](){
+ if (_update.pending()) return;
+ auto scoped(_update.block());
+ _scale_linked = !_scale_linked;
+ if (_scale_linked) {
+ // this is simplistic
+ _scale_x.set_value(_scale_y.get_value());
+ }
+ update_scale_link();
+ _signal_changed.emit();
+ });
+
+ for (auto el : {&_scale_x, &_scale_y, &_offset_x, &_offset_y}) {
+ el->signal_value_changed().connect([=]() {
+ if (_update.pending()) return;
+ if (_scale_linked && (el == &_scale_x || el == &_scale_y)) {
+ auto scoped(_update.block());
+ // enforce uniform scaling
+ (el == &_scale_x) ? _scale_y.set_value(el->get_value()) : _scale_x.set_value(el->get_value());
+ }
+ _signal_changed.emit();
+ });
+ }
+
+ _name_box.signal_changed().connect([=](){
+ if (_update.pending()) return;
+
+ _signal_changed.emit();
+ });
+
+ _search_box.signal_search_changed().connect([=](){
+ if (_update.pending()) return;
+
+ // filter patterns
+ _filter_text = _search_box.get_text();
+ apply_filter(false);
+ apply_filter(true);
+ });
+
+ // populate combo box with all patern categories
+ auto pattern_categories = _manager.get_categories()->children();
+ int cat_count = pattern_categories.size();
+ for (auto row : pattern_categories) {
+ auto name = row.get_value(_manager.columns.name);
+ _combo_set.append(name);
+ }
+
+ get_widget<Gtk::Button>(_builder, "previous").signal_clicked().connect([=](){
+ int previous = _combo_set.get_active_row_number() - 1;
+ if (previous >= 0) _combo_set.set_active(previous);
+ });
+ get_widget<Gtk::Button>(_builder, "next").signal_clicked().connect([=](){
+ auto next = _combo_set.get_active_row_number() + 1;
+ if (next < cat_count) _combo_set.set_active(next);
+ });
+ _combo_set.signal_changed().connect([=](){
+ // select pattern category to show
+ auto index = _combo_set.get_active_row_number();
+ select_pattern_set(index);
+ Inkscape::Preferences::get()->setInt(_prefs + "/currentSet", index);
+ });
+
+ bind_store(_doc_gallery, _doc_pattern_store);
+ bind_store(_stock_gallery, _stock_pattern_store);
+
+ _stock_gallery.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){
+ if (_update.pending()) return;
+ auto scoped(_update.block());
+ auto pat = _stock_pattern_store.widgets_to_pattern[box];
+ update_ui(pat);
+ _doc_gallery.unselect_all();
+ _signal_changed.emit();
+ });
+
+ _doc_gallery.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){
+ if (_update.pending()) return;
+ auto scoped(_update.block());
+ auto pat = _doc_pattern_store.widgets_to_pattern[box];
+ update_ui(pat);
+ _stock_gallery.unselect_all();
+ _signal_changed.emit();
+ });
+
+ _edit_btn.signal_clicked().connect([=](){
+ _signal_edit.emit();
+ });
+
+ _paned.set_position(Inkscape::Preferences::get()->getIntLimited(_prefs + "/handlePos", 50, 10, 9999));
+ _paned.property_position().signal_changed().connect([=](){
+ Inkscape::Preferences::get()->setInt(_prefs + "/handlePos", _paned.get_position());
+ });
+
+ // current pattern category
+ _combo_set.set_active(Inkscape::Preferences::get()->getIntLimited(_prefs + "/currentSet", 0, 0, std::max(cat_count - 1, 0)));
+
+ update_scale_link();
+ pack_start(_main_grid);
+}
+
+PatternEditor::~PatternEditor() noexcept {}
+
+void PatternEditor::bind_store(Gtk::FlowBox& list, PatternStore& pat) {
+ pat.store.set_filter([=](const Glib::RefPtr<PatternItem>& p){
+ if (!p) return false;
+ if (_filter_text.empty()) return true;
+
+ auto name = Glib::ustring(p->label).lowercase();
+ auto expr = _filter_text.lowercase();
+ auto pos = name.find(expr);
+ return pos != Glib::ustring::npos;
+ });
+
+ list.bind_list_store(pat.store.get_store(), [=, &pat](const Glib::RefPtr<PatternItem>& item){
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL);
+ auto image = Gtk::make_managed<Gtk::Image>(item->pix);
+ box->pack_start(*image);
+ auto name = Glib::ustring(item->label.c_str());
+ if (_show_names.get_active()) {
+ auto label = Gtk::make_managed<Gtk::Label>(name);
+ label->get_style_context()->add_class("small-font");
+ // limit label size to tile size
+ label->set_ellipsize(Pango::EllipsizeMode::ELLIPSIZE_END);
+ label->set_max_width_chars(0);
+ label->set_size_request(_tile_size);
+ box->pack_end(*label);
+ }
+ image->set_tooltip_text(name);
+ box->show_all();
+ auto cbox = Gtk::make_managed<Gtk::FlowBoxChild>();
+ cbox->add(*box);
+ cbox->get_style_context()->add_class("pattern-item-box");
+ pat.widgets_to_pattern[cbox] = item;
+ cbox->set_size_request(_tile_size, _tile_size);
+ return cbox;
+ });
+}
+
+void PatternEditor::select_pattern_set(int index) {
+ auto sets = _manager.get_categories()->children();
+ if (index >= 0 && index < sets.size()) {
+ auto row = sets[index];
+ if (auto category = row.get_value(_manager.columns.category)) {
+ set_stock_patterns(category->patterns);
+ }
+ }
+}
+
+void PatternEditor::update_scale_link() {
+ _link_scale.remove();
+ _link_scale.add(get_widget<Gtk::Image>(_builder, _scale_linked ? "image-linked" : "image-unlinked"));
+}
+
+void PatternEditor::update_widgets_from_pattern(Glib::RefPtr<PatternItem>& pattern) {
+ _input_grid.set_sensitive(!!pattern);
+
+ PatternItem empty;
+ const auto& item = pattern ? *pattern.get() : empty;
+
+ _name_box.set_text(item.label.c_str());
+
+ _scale_x.set_value(item.transform.xAxis().length());
+ _scale_y.set_value(item.transform.yAxis().length());
+
+ // TODO if needed
+ // auto units = get_attrib(pattern, "patternUnits");
+
+ _scale_linked = item.uniform_scale;
+ update_scale_link();
+
+ _offset_x.set_value(item.offset.x());
+ _offset_y.set_value(item.offset.y());
+
+ auto degrees = 180.0 / M_PI * Geom::atan2(item.transform.xAxis());
+ _orient_slider.set_value(round(degrees / ANGLE_STEP));
+ _angle_btn.set_value(degrees);
+
+ double x_index = gap_to_slider(item.gap[Geom::X], _gap_x_slider.get_adjustment()->get_upper());
+ _gap_x_slider.set_value(x_index);
+ double y_index = gap_to_slider(item.gap[Geom::Y], _gap_y_slider.get_adjustment()->get_upper());
+ _gap_y_slider.set_value(y_index);
+
+ if (item.color.has_value()) {
+ _color_picker->setRgba32(item.color->toRGBA32(1.0));
+ _color_btn.set_sensitive();
+ _color_label.set_opacity(1.0); // hack: sensitivity doesn't change appearance, so using opacity directly
+ }
+ else {
+ _color_picker->setRgba32(0);
+ _color_btn.set_sensitive(false);
+ _color_label.set_opacity(0.6);
+ _color_picker->closeWindow();
+ }
+}
+
+void PatternEditor::update_ui(Glib::RefPtr<PatternItem> pattern) {
+ update_widgets_from_pattern(pattern);
+}
+
+// sort patterns in-place by name/id
+void sort_patterns(std::vector<Glib::RefPtr<PatternItem>>& list) {
+ std::sort(list.begin(), list.end(), [](Glib::RefPtr<PatternItem>& a, Glib::RefPtr<PatternItem>& b) {
+ if (!a || !b) return false;
+ if (a->label == b->label) {
+ return a->id < b->id;
+ }
+ return a->label < b->label;
+ });
+}
+
+// given a pattern, create a PatternItem instance that describes it;
+// input pattern can be a link or a root pattern
+Glib::RefPtr<PatternItem> create_pattern_item(PatternManager& manager, SPPattern* pattern, int tile_size, double scale) {
+ auto item = manager.get_item(pattern);
+ if (item && scale > 0) {
+ item->pix = manager.get_image(pattern, tile_size, tile_size, scale);
+ }
+ return item;
+}
+
+// update editor UI
+void PatternEditor::set_selected(SPPattern* pattern) {
+ auto scoped(_update.block());
+
+ _stock_gallery.unselect_all();
+
+ // current pattern (should be a link)
+ auto link_pattern = pattern;
+ if (pattern) pattern = pattern->rootPattern();
+
+ if (pattern && pattern != link_pattern) {
+ _current_pattern.id = pattern->getId();
+ _current_pattern.link_id = link_pattern->getId();
+ }
+ else {
+ _current_pattern.id.clear();
+ _current_pattern.link_id.clear();
+ }
+
+ auto item = create_pattern_item(_manager, link_pattern, 0, 0);
+
+ update_widgets_from_pattern(item);
+
+ auto list = update_doc_pattern_list(pattern ? pattern->document : nullptr);
+ if (pattern) {
+ // patch up tile image on a list of document root patterns, it might have changed;
+ // color attribute for instance is being set directly on the root pattern;
+ // other attributes are per-object, so should not be taken into account when rendering tile
+ for (auto& pattern_item : list) {
+ if (pattern_item->id == item->id && pattern_item->collection == nullptr) {
+ // update preview
+ const double device_scale = get_scale_factor();
+ pattern_item->pix = _manager.get_image(pattern, _tile_size, _tile_size, device_scale);
+ item->pix = pattern_item->pix;
+ break;
+ }
+ }
+ }
+
+ set_active(_doc_gallery, _doc_pattern_store, item);
+
+ // generate large preview of selected pattern
+ Cairo::RefPtr<Cairo::Surface> surface;
+ if (link_pattern) {
+ const double device_scale = get_scale_factor();
+ auto size = _preview.get_allocation();
+ const int m = 1;
+ if (size.get_width() <= m || size.get_height() <= m) {
+ // widgets not resized yet, choose arbitrary size, so preview is not missing when widget is shown
+ size.set_width(200);
+ size.set_height(200);
+ }
+ // use white for checkerboard since most stock patterns are black
+ unsigned int background = 0xffffffff;
+ surface = _manager.get_preview(link_pattern, size.get_width(), size.get_height(), background, device_scale);
+ }
+ _preview_img.set(surface);
+}
+
+// generate preview images for patterns
+std::vector<Glib::RefPtr<PatternItem>> create_pattern_items(PatternManager& manager, const std::vector<SPPattern*>& list, int tile_size, double device_scale) {
+ std::vector<Glib::RefPtr<PatternItem>> output;
+ output.reserve(list.size());
+
+ for (auto pat : list) {
+ if (auto item = create_pattern_item(manager, pat, tile_size, device_scale)) {
+ output.push_back(item);
+ }
+ }
+
+ return output;
+}
+
+// populate store with document patterns if list has changed, minimize amount of work by using cached previews
+std::vector<Glib::RefPtr<PatternItem>> PatternEditor::update_doc_pattern_list(SPDocument* document) {
+ auto list = sp_get_pattern_list(document);
+ std::shared_ptr<SPDocument> nil;
+ const double device_scale = get_scale_factor();
+ // create pattern items (cheap), but skip preview generation (expansive)
+ auto patterns = create_pattern_items(_manager, list, 0, 0);
+ bool modified = false;
+ for (auto&& item : patterns) {
+ auto it = _cached_items.find(item->id);
+ if (it != end(_cached_items)) {
+ // reuse cached preview image
+ if (!item->pix) item->pix = it->second->pix;
+ }
+ else {
+ if (!item->pix) {
+ // generate preview for newly added pattern
+ item->pix = _manager.get_image(cast<SPPattern>(document->getObjectById(item->id)), _tile_size, _tile_size, device_scale);
+ }
+ modified = true;
+ _cached_items[item->id] = item;
+ }
+ }
+
+ update_store(patterns, _doc_gallery, _doc_pattern_store);
+
+ return patterns;
+}
+
+void PatternEditor::set_document(SPDocument* document) {
+ _current_document = document;
+ _cached_items.clear();
+ update_doc_pattern_list(document);
+}
+
+// populate store with stock patterns
+void PatternEditor::set_stock_patterns(const std::vector<SPPattern*>& list) {
+ const double device_scale = get_scale_factor();
+ auto patterns = create_pattern_items(_manager, list, _tile_size, device_scale);
+ sort_patterns(patterns);
+ update_store(patterns, _stock_gallery, _stock_pattern_store);
+}
+
+void PatternEditor::apply_filter(bool stock) {
+ auto scoped(_update.block());
+ if (!stock) {
+ _doc_pattern_store.store.apply_filter();
+ }
+ else {
+ _stock_pattern_store.store.apply_filter();
+ }
+}
+
+void PatternEditor::update_store(const std::vector<Glib::RefPtr<PatternItem>>& list, Gtk::FlowBox& gallery, PatternStore& pat) {
+ auto selected = get_active(gallery, pat);
+ if (pat.store.assign(list)) {
+ // reselect current
+ set_active(gallery, pat, selected);
+ }
+}
+
+Glib::RefPtr<PatternItem> PatternEditor::get_active(Gtk::FlowBox& gallery, PatternStore& pat) {
+ auto empty = Glib::RefPtr<PatternItem>();
+
+ auto sel = gallery.get_selected_children();
+ if (sel.size() == 1) {
+ return pat.widgets_to_pattern[sel.front()];
+ }
+ else {
+ return empty;
+ }
+}
+
+std::pair<Glib::RefPtr<PatternItem>, SPDocument*> PatternEditor::get_active() {
+ SPDocument* stock = nullptr;
+ auto sel = get_active(_doc_gallery, _doc_pattern_store);
+ if (!sel) {
+ sel = get_active(_stock_gallery, _stock_pattern_store);
+ stock = sel ? sel->collection : nullptr;
+ }
+ return std::make_pair(sel, stock);
+}
+
+void PatternEditor::set_active(Gtk::FlowBox& gallery, PatternStore& pat, Glib::RefPtr<PatternItem> item) {
+ bool selected = false;
+ if (item) {
+ gallery.foreach([=,&selected,&pat,&gallery](Gtk::Widget& widget){
+ if (auto box = dynamic_cast<Gtk::FlowBoxChild*>(&widget)) {
+ if (auto pattern = pat.widgets_to_pattern[box]) {
+ if (pattern->id == item->id && pattern->collection == item->collection) {
+ gallery.select_child(*box);
+ if (item->pix) {
+ // update preview, it might be stale
+ sp_traverse_widget_tree(box->get_child(), [&](Gtk::Widget* widget){
+ if (auto image = dynamic_cast<Gtk::Image*>(widget)) {
+ image->set(item->pix);
+ return true; // stop
+ }
+ return false; // continue
+ });
+ }
+ selected = true;
+ }
+ }
+ }
+ });
+ }
+
+ if (!selected) {
+ gallery.unselect_all();
+ }
+}
+
+std::pair<std::string, SPDocument*> PatternEditor::get_selected() {
+ // document patterns first
+ auto active = get_active();
+ auto sel = active.first;
+ auto stock_doc = active.second;
+ std::string id;
+ if (sel) {
+ if (stock_doc) {
+ // for stock pattern, report its root pattern ID
+ return std::make_pair(sel->id, stock_doc);
+ }
+ else {
+ // for current document, if selection hasn't changed return linked pattern ID
+ // so that we can modify its properties (transform, offset, gap)
+ if (sel->id == _current_pattern.id) {
+ return std::make_pair(_current_pattern.link_id, nullptr);
+ }
+ // different pattern from current document selected; use its root pattern
+ // as a starting point; link pattern will be injected by adjust_pattern()
+ return std::make_pair(sel->id, nullptr);
+ }
+ }
+ else {
+ // if nothing is selected, pick first stock pattern, so we have something to assign
+ // to selected object(s); without it, pattern editing will not be activated
+ if (auto first = _stock_pattern_store.store.get_store()->get_item(0)) {
+ return std::make_pair(first->id, first->collection);
+ }
+
+ // no stock patterns available
+ return std::make_pair("", nullptr);
+ }
+}
+
+std::optional<unsigned int> PatternEditor::get_selected_color() {
+ auto pat = get_active();
+ if (pat.first && pat.first->color.has_value()) {
+ return _color_picker->get_current_color();
+ }
+ return std::optional<unsigned int>(); // color not supported
+}
+
+Geom::Point PatternEditor::get_selected_offset() {
+ return Geom::Point(_offset_x.get_value(), _offset_y.get_value());
+}
+
+Geom::Affine PatternEditor::get_selected_transform() {
+ Geom::Affine matrix;
+
+ matrix *= Geom::Scale(_scale_x.get_value(), _scale_y.get_value());
+ matrix *= Geom::Rotate(_angle_btn.get_value() / 180.0 * M_PI);
+ auto pat = get_active();
+ if (pat.first) {
+ //TODO: this is imperfect; calculate better offset, if possible
+ // this translation is kept so there's no sudden jump when editing pattern attributes
+ matrix.setTranslation(pat.first->transform.translation());
+ }
+ return matrix;
+}
+
+bool PatternEditor::is_selected_scale_uniform() {
+ return _scale_linked;
+}
+
+Geom::Scale PatternEditor::get_selected_gap() {
+ auto vx = _gap_x_slider.get_value();
+ auto gap_x = slider_to_gap(vx, _gap_x_slider.get_adjustment()->get_upper());
+
+ auto vy = _gap_y_slider.get_value();
+ auto gap_y = slider_to_gap(vy, _gap_y_slider.get_adjustment()->get_upper());
+
+ return Geom::Scale(gap_x, gap_y);
+}
+
+Glib::ustring PatternEditor::get_label() {
+ return _name_box.get_text();
+}
+
+SPPattern* get_pattern(const PatternItem& item, SPDocument* document) {
+ auto doc = item.collection ? item.collection : document;
+ if (!doc) return nullptr;
+
+ return cast<SPPattern>(doc->getObjectById(item.id));
+}
+
+void regenerate_tile_images(PatternManager& manager, PatternStore& pat_store, int tile_size, double device_scale, SPDocument* current) {
+ auto& patterns = pat_store.store.get_items();
+ for (auto& item : patterns) {
+ if (auto pattern = get_pattern(*item.get(), current)) {
+ item->pix = manager.get_image(pattern, tile_size, tile_size, device_scale);
+ }
+ }
+ pat_store.store.refresh();
+}
+
+void PatternEditor::update_pattern_tiles() {
+ const double device_scale = get_scale_factor();
+ regenerate_tile_images(_manager, _doc_pattern_store, _tile_size, device_scale, _current_document);
+ regenerate_tile_images(_manager, _stock_pattern_store, _tile_size, device_scale, nullptr);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/pattern-editor.h b/src/ui/widget/pattern-editor.h
new file mode 100644
index 0000000..9c52e74
--- /dev/null
+++ b/src/ui/widget/pattern-editor.h
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef SEEN_PATTERN_EDITOR_H
+#define SEEN_PATTERN_EDITOR_H
+
+#include <unordered_map>
+#include <vector>
+#include <gtkmm/box.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/button.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/builder.h>
+#include <optional>
+#include <2geom/transforms.h>
+#include "color.h"
+#include "object/sp-pattern.h"
+#include "pattern-manager.h"
+#include "spin-scale.h"
+#include "ui/operation-blocker.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/pattern-store.h"
+
+class SPDocument;
+class ColorPicker;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class PatternEditor : public Gtk::Box {
+public:
+ PatternEditor(const char* prefs, PatternManager& manager);
+ ~PatternEditor() noexcept override;
+
+ // pass current document to extract patterns
+ void set_document(SPDocument* document);
+ // set selected pattern
+ void set_selected(SPPattern* pattern);
+ // selected pattern ID if any plus stock pattern collection document (or null)
+ std::pair<std::string, SPDocument*> get_selected();
+ // and its color
+ std::optional<unsigned int> get_selected_color();
+ // return combined scale and rotation
+ Geom::Affine get_selected_transform();
+ // return pattern offset
+ Geom::Point get_selected_offset();
+ // is scale uniform?
+ bool is_selected_scale_uniform();
+ // return gap size for pattern tiles
+ Geom::Scale get_selected_gap();
+ // get pattern label
+ Glib::ustring get_label();
+
+private:
+ sigc::signal<void> _signal_changed;
+ sigc::signal<void, unsigned int> _signal_color_changed;
+ sigc::signal<void> _signal_edit;
+
+public:
+ decltype(_signal_changed) signal_changed() const { return _signal_changed; }
+ decltype(_signal_color_changed) signal_color_changed() const { return _signal_color_changed; }
+ decltype(_signal_edit) signal_edit() const { return _signal_edit; }
+
+private:
+ void bind_store(Gtk::FlowBox& list, PatternStore& store);
+ void update_store(const std::vector<Glib::RefPtr<PatternItem>>& list, Gtk::FlowBox& gallery, PatternStore& store);
+ Glib::RefPtr<PatternItem> get_active(Gtk::FlowBox& gallery, PatternStore& pat);
+ std::pair<Glib::RefPtr<PatternItem>, SPDocument*> get_active();
+ void set_active(Gtk::FlowBox& gallery, PatternStore& pat, Glib::RefPtr<PatternItem> item);
+ void update_widgets_from_pattern(Glib::RefPtr<PatternItem>& pattern);
+ void update_scale_link();
+ void update_ui(Glib::RefPtr<PatternItem> pattern);
+ std::vector<Glib::RefPtr<PatternItem>> update_doc_pattern_list(SPDocument* document);
+ void set_stock_patterns(const std::vector<SPPattern*>& patterns);
+ void select_pattern_set(int index);
+ void apply_filter(bool stock);
+ void update_pattern_tiles();
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::Paned& _paned;
+ Gtk::Box& _main_grid;
+ Gtk::Grid& _input_grid;
+ Gtk::SpinButton& _offset_x;
+ Gtk::SpinButton& _offset_y;
+ Gtk::SpinButton& _scale_x;
+ Gtk::SpinButton& _scale_y;
+ Gtk::SpinButton& _angle_btn;
+ Gtk::Scale& _orient_slider;
+ Gtk::Scale& _gap_x_slider;
+ Gtk::Scale& _gap_y_slider;
+ Gtk::Button& _edit_btn;
+ Gtk::Label& _color_label;
+ Gtk::Button& _color_btn;
+ Gtk::Button& _link_scale;
+ Gtk::Image& _preview_img;
+ Gtk::Viewport& _preview;
+ Gtk::FlowBox& _doc_gallery;
+ Gtk::FlowBox& _stock_gallery;
+ Gtk::Entry& _name_box;
+ Gtk::ComboBoxText& _combo_set;
+ Gtk::SearchEntry& _search_box;
+ Gtk::Scale& _tile_slider;
+ Gtk::CheckButton& _show_names;
+ Glib::RefPtr<Gtk::TreeModel> _categories;
+ bool _scale_linked = true;
+ Glib::ustring _prefs;
+ PatternStore _doc_pattern_store;
+ PatternStore _stock_pattern_store;
+ std::unique_ptr<ColorPicker> _color_picker;
+ OperationBlocker _update;
+ std::unordered_map<std::string, Glib::RefPtr<PatternItem>> _cached_items; // cached current document patterns
+ Inkscape::PatternManager& _manager;
+ Glib::ustring _filter_text;
+ int _tile_size = 0;
+ SPDocument* _current_document = nullptr;
+ // pattern being currently edited: id for a root pattern, and link id of a pattern with href set
+ struct { Glib::ustring id; Glib::ustring link_id; } _current_pattern;
+};
+
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif
diff --git a/src/ui/widget/pattern-store.h b/src/ui/widget/pattern-store.h
new file mode 100644
index 0000000..9c183d1
--- /dev/null
+++ b/src/ui/widget/pattern-store.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_WIDGET_PATTERN_STORE_H
+#define INKSCAPE_UI_WIDGET_PATTERN_STORE_H
+/*
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <optional>
+#include <2geom/transforms.h>
+#include <giomm/liststore.h>
+#include <gtkmm/widget.h>
+#include "color.h"
+#include "ui/filtered-store.h"
+
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// pattern parameters
+struct PatternItem : Glib::Object {
+ Cairo::RefPtr<Cairo::Surface> pix;
+ std::string id;
+ std::string label;
+ bool stock = false;
+ bool uniform_scale = false;
+ Geom::Affine transform;
+ Geom::Point offset;
+ std::optional<SPColor> color;
+ Geom::Scale gap;
+ SPDocument* collection = nullptr;
+
+ bool operator == (const PatternItem& item) const {
+ // compare all attributes apart from pixmap preview
+ return
+ id == item.id &&
+ label == item.label &&
+ stock == item.stock &&
+ uniform_scale == item.uniform_scale &&
+ transform == item.transform &&
+ offset == item.offset &&
+ color == item.color &&
+ gap == item.gap &&
+ collection == item.collection;
+ }
+};
+
+struct PatternStore {
+ Inkscape::FilteredStore<PatternItem> store;
+ std::map<Gtk::Widget*, Glib::RefPtr<PatternItem>> widgets_to_pattern;
+};
+
+}
+}
+}
+
+#endif
diff --git a/src/ui/widget/point.cpp b/src/ui/widget/point.cpp
new file mode 100644
index 0000000..9099988
--- /dev/null
+++ b/src/ui/widget/point.cpp
@@ -0,0 +1,186 @@
+// 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:","")
+{
+ xwidget.drag_dest_unset();
+ ywidget.drag_dest_unset();
+ 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)
+{
+ xwidget.drag_dest_unset();
+ ywidget.drag_dest_unset();
+ 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)
+{
+ xwidget.drag_dest_unset();
+ ywidget.drag_dest_unset();
+ 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..b76f077
--- /dev/null
+++ b/src/ui/widget/preferences-widget.cpp
@@ -0,0 +1,1117 @@
+// 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();
+ if (!label.empty())
+ 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());
+
+ context->render_background(cr, 0, 0, w, _height + _border * 2);
+
+ cr->set_line_width(1);
+ cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue());
+
+ 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);
+
+ 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 const labels[], int const 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::cerr << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path.raw() << 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::cerr << "PrefCombo::"
+ << "Different number of values/labels in " << prefs_path.raw() << 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 PrefEntryFile::on_changed()
+{
+ if (this->get_visible()) //only take action if user changed value
+ {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(_prefs_path, Glib::filename_to_utf8(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..f4a990f
--- /dev/null
+++ b/src/ui/widget/preferences-widget.h
@@ -0,0 +1,357 @@
+// 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);
+ // Allow use with the GtkBuilder get_derived_widget
+ PrefCheckButton(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade, Glib::ustring pref, bool def)
+ : Gtk::CheckButton(cobject)
+ {
+ init("", pref, def);
+ }
+ PrefCheckButton() : Gtk::CheckButton() {};
+ 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 const labels[], int const 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 PrefEntryFile : public PrefEntry
+{
+ 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/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..5464227
--- /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..6df1981
--- /dev/null
+++ b/src/ui/widget/registered-widget.cpp
@@ -0,0 +1,830 @@
+// 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 (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);
+}
+
+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 (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);
+}
+
+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);
+ }
+ {
+ DocumentUndo::ScopedInsensitive _no_undo(local_doc);
+ local_repr->setAttribute(_ckey, c);
+ local_repr->setAttributeCssDouble(_akey.c_str(), (rgba & 0xff) / 255.0);
+ }
+ 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..9e3d815
--- /dev/null
+++ b/src/ui/widget/registered-widget.h
@@ -0,0 +1,452 @@
+// 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();
+ }
+ const char * svgstr_old = local_repr->attribute(_key.c_str());
+ {
+ DocumentUndo::ScopedInsensitive _no_undo(local_doc);
+ if (!write_undo) {
+ local_repr->setAttribute(_key, svgstr);
+ }
+ }
+ 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 = default;
+ 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;
+ void on_toggled() override;
+};
+
+class RegisteredToggleButton : public RegisteredWidget<Gtk::ToggleButton> {
+public:
+ ~RegisteredToggleButton() override = default;
+ 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:
+ 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..639f8d1
--- /dev/null
+++ b/src/ui/widget/rotateable.cpp
@@ -0,0 +1,180 @@
+// 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);
+ }
+ }
+ Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK);
+ 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..a7a2bf6
--- /dev/null
+++ b/src/ui/widget/scalar.cpp
@@ -0,0 +1,212 @@
+// 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::setNoLeadingZeros()
+{
+ g_assert(_widget != nullptr);
+ if (getDigits()) {
+ static_cast<SpinButton*>(_widget)->set_numeric(false);
+ static_cast<SpinButton*>(_widget)->set_update_policy(Gtk::UPDATE_ALWAYS);
+ static_cast<SpinButton*>(_widget)->signal_output().connect(sigc::mem_fun(*this, &Scalar::setNoLeadingZerosOutput));
+ }
+}
+
+bool
+Scalar::setNoLeadingZerosOutput()
+{
+ g_assert(_widget != nullptr);
+ double digits = (double)pow(10.0,static_cast<SpinButton*>(_widget)->get_digits());
+ double val = std::round(static_cast<SpinButton*>(_widget)->get_value() * digits) / digits;
+ static_cast<SpinButton*>(_widget)->set_text(Glib::ustring::format(val).c_str());
+ return true;
+}
+
+void
+Scalar::setWidthChars(gint width_chars) {
+ g_assert(_widget != nullptr);
+ static_cast<SpinButton*>(_widget)->property_width_chars() = width_chars;
+}
+
+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..85ed72e
--- /dev/null
+++ b/src/ui/widget/scalar.h
@@ -0,0 +1,202 @@
+// 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();
+
+ /**
+ * remove leading zeros fron widget.
+ */
+ void setNoLeadingZeros();
+ bool setNoLeadingZerosOutput();
+
+ /**
+ * Set the number of set width chars of entry.
+ */
+ void setWidthChars(gint width_chars);
+
+ /**
+ * 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..43a9039
--- /dev/null
+++ b/src/ui/widget/selected-style.cpp
@@ -0,0 +1,1416 @@
+// 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/color-preview.h"
+#include "ui/widget/gradient-image.h"
+
+#include "widgets/paintdef.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
+};
+
+static const std::vector<Gtk::TargetEntry> ui_drop_target_entries = {
+ Gtk::TargetEntry("application/x-oswb-color", Gtk::TargetFlags(0), APP_OSWB_COLOR)
+};
+
+/* 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) {
+ 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));
+ if (worked) {
+ if (color.get_type() == PaintDef::NONE) {
+ colorspec = "none";
+ } else {
+ auto [r, g, b] = color.get_rgb();
+ 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]) {
+ auto widget = i == SS_FILL ? &_fill_place : &_stroke_place;
+ widget->drag_dest_unset();
+ _dropEnabled[i] = false;
+ }
+ break;
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME: {
+ if (!_dropEnabled[i]) {
+ auto widget = i == SS_FILL ? &_fill_place : &_stroke_place;
+ widget->drag_dest_set(ui_drop_target_entries,
+ Gtk::DestDefaults::DEST_DEFAULT_ALL,
+ Gdk::DragAction::ACTION_COPY | Gdk::DragAction::ACTION_MOVE);
+ _dropEnabled[i] = true;
+ }
+ auto paint = i == SS_FILL ? query.fill.upcast() : query.stroke.upcast();
+ 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 (is<SPLinearGradient>(server)) {
+ auto vector = cast<SPGradient>(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 (is<SPRadialGradient>(server)) {
+ auto vector = cast<SPGradient>(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 (is<SPMeshGradient>(server)) {
+ auto array = cast<SPGradient>(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 (is<SPPattern>(server)) {
+ place->add(_pattern[i]);
+ place->set_tooltip_text(__pattern[i]);
+ _mode[i] = SS_PATTERN;
+ } else if (is<SPHatch>(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..7a451ab
--- /dev/null
+++ b/src/ui/widget/shapeicon.cpp
@@ -0,0 +1,148 @@
+// 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 <gtkmm/cellrenderer.h>
+#include <gtkmm/enums.h>
+#include "color.h"
+#include "ui/util.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)
+{
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+
+ std::string shape_type = _property_shape_type.get_value();
+ if (shape_type == "-") return; // "-" is an explicit request not to draw any icon
+
+ std::string highlight;
+ auto color = _property_color.get_value();
+ if (color == 0) {
+ auto style_context = widget.get_style_context();
+ Gdk::RGBA fg = style_context->get_color(cell_flags_to_state_flags(flags));
+ highlight = fg.to_string();
+ }
+ else {
+ highlight = SPColor(color).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 (!_both_overlay) {
+ _both_overlay = sp_get_icon_pixbuf("overlay-clipmask", 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);
+ }
+ if (clipmask == OVERLAY_BOTH && _both_overlay) {
+ paint_icon(cr, widget, _both_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;
+}
+
+bool CellRendererItemIcon::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_activated.emit(path);
+ return true;
+}
+
+} // 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..cc6daab
--- /dev/null
+++ b/src/ui/widget/shapeicon.h
@@ -0,0 +1,115 @@
+// 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
+ OVERLAY_BOTH = 3, // Object has both clip and 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),
+ _both_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();
+ }
+
+ typedef sigc::signal<void (Glib::ustring)> type_signal_activated;
+ type_signal_activated signal_activated() {
+ return _signal_activated;
+ }
+
+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;
+
+ 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:
+ type_signal_activated _signal_activated;
+ 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;
+ Glib::RefPtr<Gdk::Pixbuf> _both_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..ee59b22
--- /dev/null
+++ b/src/ui/widget/spin-scale.cpp
@@ -0,0 +1,247 @@
+// 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>
+#include <gtkmm/enums.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.drag_dest_unset();
+ _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)
+{
+ 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.set_relief(Gtk::RELIEF_NONE);
+ _link.set_focus_on_click(false);
+ _link.set_can_focus(false);
+ _link.get_style_context()->add_class("link-edit-button");
+ _link.set_valign(Gtk::ALIGN_CENTER);
+ _link.signal_clicked().connect(sigc::mem_fun(*this, &DualSpinScale::link_toggled));
+
+ Gtk::Box* vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ vb->add(_s1);
+ _s1.set_margin_bottom(3);
+ vb->add(_s2);
+ pack_start(*vb);
+ pack_start(_link, false, false);
+ set_link_active(true);
+ _s2.set_sensitive(false);
+
+ show_all();
+}
+
+void DualSpinScale::set_link_active(bool link) {
+ _linked = link;
+ _link.set_image_from_icon_name(_linked ? "entries-linked" : "entries-unlinked", Gtk::ICON_SIZE_LARGE_TOOLBAR);
+}
+
+Glib::ustring DualSpinScale::get_as_attribute() const
+{
+ if (_linked) {
+ 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]);
+
+ set_link_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()
+{
+ _linked = !_linked;
+ set_link_active(_linked);
+ _s2.set_sensitive(!_linked);
+ update_linked();
+}
+
+void DualSpinScale::update_linked()
+{
+ if (_linked) {
+ _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..c924a43
--- /dev/null
+++ b/src/ui/widget/spin-scale.h
@@ -0,0 +1,116 @@
+// 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();
+ void set_link_active(bool link);
+ sigc::signal<void ()> _signal_value_changed;
+ SpinScale _s1, _s2;
+ bool _linked = true;
+ Gtk::Button _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..f63ffd4
--- /dev/null
+++ b/src/ui/widget/spinbutton.cpp
@@ -0,0 +1,144 @@
+// 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 {
+
+MathSpinButton::MathSpinButton(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Gtk::SpinButton(cobject)
+{
+ drag_dest_unset();
+}
+
+int MathSpinButton::on_input(double *newvalue)
+{
+ try {
+ auto eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), nullptr);
+ auto result = eval.evaluate();
+ *newvalue = result.value;
+ } catch (Inkscape::Util::EvaluatorException &e) {
+ g_message ("%s", e.what());
+ return false;
+ }
+ return true;
+}
+
+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();
+ 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..2c211a7
--- /dev/null
+++ b/src/ui/widget/spinbutton.h
@@ -0,0 +1,124 @@
+// 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;
+
+/**
+ * A spin button for use with builders.
+ */
+class MathSpinButton : public Gtk::SpinButton
+{
+public:
+ MathSpinButton(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade);
+ ~MathSpinButton() override{};
+protected:
+ int on_input(double* newvalue) override;
+};
+
+/**
+ * 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..9233586
--- /dev/null
+++ b/src/ui/widget/stroke-style.cpp
@@ -0,0 +1,1223 @@
+// 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.
+ */
+
+#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());
+
+ for (auto item : desktop->getSelection()->items()) {
+ if (!is<SPShape>(item)) {
+ continue;
+ }
+ if (Inkscape::XML::Node* selrepr = item->getRepr()) {
+ 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);
+ // perform update to make sure any previously referenced markers are released,
+ // so they can be collected by DocumentUndo::done collect orphans
+ document->ensureUpToDate();
+
+ DocumentUndo::done(document, _("Set markers"), INKSCAPE_ICON("dialog-fill-and-stroke"));
+ }
+
+ // edit marker mode - update
+ if (auto mt = dynamic_cast<Inkscape::UI::Tools::MarkerTool*>(desktop->event_context)) {
+ 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 (is<SPGroup>(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 (!is<SPText>(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..ad5b3ff
--- /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..05d149e
--- /dev/null
+++ b/src/ui/widget/style-swatch.cpp
@@ -0,0 +1,405 @@
+// 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/enums.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::Orientation orient)
+ : 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);
+
+ if (orient == Gtk::ORIENTATION_VERTICAL) {
+ _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);
+ }
+ else {
+ _table->set_column_spacing(4);
+ _table->attach(_label[SS_FILL], 0, 0, 1, 1);
+ _table->attach(_place[SS_FILL], 1, 0, 1, 1);
+ _label[SS_STROKE].set_margin_start(6);
+ _table->attach(_label[SS_STROKE], 2, 0, 1, 1);
+ _table->attach(_stroke, 3, 0, 1, 1);
+ _opacity_place.set_margin_start(6);
+ _table->attach(_opacity_place, 4, 0, 1, 1);
+ _swatch.add(*_table);
+ pack_start(_swatch, true, true, 0);
+
+ int patch_w = 6 * 6;
+ _place[SS_FILL].set_size_request(patch_w, -1);
+ _place[SS_STROKE].set_size_request(patch_w, -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 (is<SPLinearGradient>(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 (is<SPRadialGradient>(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 (is<SPPattern>(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..514cd6d
--- /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, Gtk::Orientation orient = Gtk::ORIENTATION_VERTICAL);
+
+ ~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..54376fc
--- /dev/null
+++ b/src/ui/widget/swatch-selector.cpp
@@ -0,0 +1,101 @@
+// 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 "ui/icon-names.h"
+#include "ui/widget/color-notebook.h"
+#include "ui/widget/gradient-selector.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+SwatchSelector::SwatchSelector()
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+{
+ using Inkscape::UI::Widget::ColorNotebook;
+
+ _gsel = Gtk::make_managed<GradientSelector>();
+ _gsel->setMode(GradientSelector::MODE_SWATCH);
+
+ _gsel->show();
+
+ pack_start(*_gsel);
+
+ auto color_selector = Gtk::make_managed<ColorNotebook>(_selected_color);
+ color_selector->set_label(_("Swatch color"));
+ color_selector->show();
+ pack_start(*color_selector);
+
+ _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));
+}
+
+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();
+
+ if (auto stop = ngr->getFirstStop()) {
+ stop->setColor(_selected_color.color(), _selected_color.alpha());
+ DocumentUndo::done(ngr->document, _("Change swatch color"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+}
+
+void SwatchSelector::setVector(SPDocument */*doc*/, SPGradient *vector)
+{
+ _gsel->setVector(vector ? vector->document : nullptr, vector);
+
+ if (vector && vector->isSolid()) {
+ _updating_color = true;
+ auto stop = vector->getFirstStop();
+ _selected_color.setColorAlpha(stop->getColor(), stop->getOpacity(), true);
+ _updating_color = 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/swatch-selector.h b/src/ui/widget/swatch-selector.h
new file mode 100644
index 0000000..c67b013
--- /dev/null
+++ b/src/ui/widget/swatch-selector.h
@@ -0,0 +1,58 @@
+// 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();
+
+ void setVector(SPDocument *doc, SPGradient *vector);
+
+ GradientSelector *getGradientSelector() { return _gsel; }
+
+private:
+ void _changedCb();
+
+ GradientSelector *_gsel = nullptr;
+ Inkscape::UI::SelectedColor _selected_color;
+ bool _updating_color = false;
+};
+
+} // 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/template-list.cpp b/src/ui/widget/template-list.cpp
new file mode 100644
index 0000000..0538439
--- /dev/null
+++ b/src/ui/widget/template-list.cpp
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "template-list.h"
+
+#include <glibmm/i18n.h>
+
+#include "extension/db.h"
+#include "extension/template.h"
+#include "inkscape-application.h"
+#include "io/resource.h"
+#include "ui/util.h"
+#include "ui/icon-loader.h"
+#include "ui/svg-renderer.h"
+
+using namespace Inkscape::IO::Resource;
+using Inkscape::Extension::TemplatePreset;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class TemplateCols : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ // These types must match those for the model in the .glade file
+ TemplateCols()
+ {
+ this->add(this->name);
+ this->add(this->label);
+ this->add(this->icon);
+ this->add(this->key);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> icon;
+ Gtk::TreeModelColumn<Glib::ustring> key;
+};
+
+TemplateList::TemplateList() {}
+
+TemplateList::TemplateList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
+ : Gtk::Notebook(cobject)
+{
+ TemplateList();
+}
+
+/**
+ * Initialise this template list with categories and icons
+ */
+void TemplateList::init(Inkscape::Extension::TemplateShow mode)
+{
+ TemplateCols cols;
+ std::map<std::string, Glib::RefPtr<Gtk::ListStore>> _stores;
+
+ Inkscape::Extension::DB::TemplateList extensions;
+ Inkscape::Extension::db.get_template_list(extensions);
+
+ for (auto tmod : extensions) {
+ std::string cat = tmod->get_category();
+ if (!_stores.count(cat)) {
+ try {
+ _stores[cat] = this->generate_category(cat);
+ _stores[cat]->clear();
+ } catch (UIBuilderError &e) {
+ return;
+ }
+ }
+ for (auto preset : tmod->get_presets(mode)) {
+ Gtk::TreeModel::Row row = *(_stores[cat]->append());
+ auto name = preset->get_name();
+ row[cols.name] = name.empty() ? "" : _(name.c_str());
+ row[cols.icon] = icon_to_pixbuf(preset->get_icon_path());
+ auto label = preset->get_label();
+ row[cols.label] = label.empty() ? "" : _(label.c_str());
+ row[cols.key] = preset->get_key();
+ }
+ }
+
+ reset_selection();
+}
+
+/**
+ * Turn the requested template icon name into a pixbuf
+ */
+Glib::RefPtr<Gdk::Pixbuf> TemplateList::icon_to_pixbuf(std::string path)
+{
+ // TODO: Add some caching here.
+ if (!path.empty()) {
+ Inkscape::svg_renderer renderer(path.c_str());
+ return renderer.render(1.0);
+ }
+ Glib::RefPtr<Gdk::Pixbuf> no_image;
+ return no_image;
+}
+
+/**
+ * Generate a new category with the given label and return it's list store.
+ */
+Glib::RefPtr<Gtk::ListStore> TemplateList::generate_category(std::string label)
+{
+ static Glib::ustring uifile = get_filename(UIS, "widget-new-from-template.ui");
+
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(uifile);
+ } catch (const Glib::Error &ex) {
+ g_error("UI file loading failed for template list widget: %s", ex.what().c_str());
+ throw UIFileUnavailable();
+ }
+
+ Gtk::Widget *container = nullptr;
+ Gtk::IconView *icons = nullptr;
+ builder->get_widget("container", container);
+ builder->get_widget("iconview", icons);
+
+ if (!icons || !container) {
+ throw WidgetUnavailable();
+ }
+
+ // This packing keeps the Gtk widget alive, beyond the builder's lifetime
+ this->append_page(*container, g_dpgettext2(nullptr, "TemplateCategory", label.c_str()));
+
+ icons->signal_selection_changed().connect([=]() { _item_selected_signal.emit(); });
+ icons->signal_item_activated().connect([=](const Gtk::TreeModel::Path) { _item_activated_signal.emit(); });
+
+ return Glib::RefPtr<Gtk::ListStore>::cast_dynamic(icons->get_model());
+}
+
+/**
+ * Returns true if the template list has a visible, selected preset.
+ */
+bool TemplateList::has_selected_preset()
+{
+ return (bool)get_selected_preset();
+}
+
+/**
+ * Returns the selected template preset, if one is not selected returns nullptr.
+ */
+std::shared_ptr<TemplatePreset> TemplateList::get_selected_preset()
+{
+ TemplateCols cols;
+ if (auto iconview = get_iconview(get_nth_page(get_current_page()))) {
+ auto items = iconview->get_selected_items();
+ if (!items.empty()) {
+ auto iter = iconview->get_model()->get_iter(items[0]);
+ if (Gtk::TreeModel::Row row = *iter) {
+ Glib::ustring key = row[cols.key];
+ return Extension::Template::get_any_preset(key);
+ }
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Create a new document based on the selected item and return.
+ */
+SPDocument *TemplateList::new_document()
+{
+ auto app = InkscapeApplication::instance();
+ if (auto preset = get_selected_preset()) {
+ if (auto doc = preset->new_from_template()) {
+ // TODO: Add memory to remember this preset for next time.
+ app->document_add(doc);
+ return doc;
+ } else {
+ // Cancel pressed in options box.
+ return nullptr;
+ }
+ }
+ // Fallback to the default template (already added)!
+ return app->document_new();
+}
+
+/**
+ * Reset the selection, forcing the use of the default template.
+ */
+void TemplateList::reset_selection()
+{
+ // TODO: Add memory here for the new document default (see new_document).
+ for (auto widget : get_children()) {
+ if (auto iconview = get_iconview(widget)) {
+ iconview->unselect_all();
+ }
+ }
+}
+
+/**
+ * Returns the internal iconview for the given widget.
+ */
+Gtk::IconView *TemplateList::get_iconview(Gtk::Widget *widget)
+{
+ if (auto container = dynamic_cast<Gtk::Container *>(widget)) {
+ for (auto child : container->get_children()) {
+ if (auto iconview = get_iconview(child)) {
+ return iconview;
+ }
+ }
+ }
+ return dynamic_cast<Gtk::IconView *>(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/template-list.h b/src/ui/widget/template-list.h
new file mode 100644
index 0000000..7b7d425
--- /dev/null
+++ b/src/ui/widget/template-list.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Martin Owens <doctormo@geek-2.com>
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef WIDGET_TEMPLATE_LIST_H
+#define WIDGET_TEMPLATE_LIST_H
+
+#include <gtkmm.h>
+#include "extension/template.h"
+
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class TemplateList : public Gtk::Notebook
+{
+public:
+ TemplateList();
+ TemplateList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade);
+ ~TemplateList() override{};
+
+ void init(Extension::TemplateShow mode);
+ void reset_selection();
+ bool has_selected_preset();
+ std::shared_ptr<Extension::TemplatePreset> get_selected_preset();
+ SPDocument *new_document();
+
+ sigc::connection connectItemSelected(const sigc::slot<void ()> &slot) { return _item_selected_signal.connect(slot); }
+ sigc::connection connectItemActivated(const sigc::slot<void ()> &slot) { return _item_activated_signal.connect(slot); }
+
+private:
+ Glib::RefPtr<Gtk::ListStore> generate_category(std::string label);
+ Glib::RefPtr<Gdk::Pixbuf> icon_to_pixbuf(std::string name);
+ Gtk::IconView *get_iconview(Gtk::Widget *widget);
+ std::shared_ptr<Extension::TemplatePreset> get_preset(std::string key);
+
+ sigc::signal<void ()> _item_selected_signal;
+ sigc::signal<void ()> _item_activated_signal;
+};
+
+} // 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/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..2d1463d
--- /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();
+ {
+ DocumentUndo::ScopedInsensitive _no_undo(doc);
+ Inkscape::XML::Node *repr = dt->getNamedView()->getRepr();
+ repr->setAttribute(_key, os.str());
+ }
+
+ 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