summaryrefslogtreecommitdiffstats
path: root/src/ui/dialog
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/dialog
parentInitial commit. (diff)
downloadinkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz
inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui/dialog')
-rw-r--r--src/ui/dialog/README.md46
-rw-r--r--src/ui/dialog/about.cpp206
-rw-r--r--src/ui/dialog/about.h43
-rw-r--r--src/ui/dialog/align-and-distribute.cpp315
-rw-r--r--src/ui/dialog/align-and-distribute.h97
-rw-r--r--src/ui/dialog/arrange-tab.h55
-rw-r--r--src/ui/dialog/attrdialog.cpp848
-rw-r--r--src/ui/dialog/attrdialog.h153
-rw-r--r--src/ui/dialog/calligraphic-profile-rename.cpp142
-rw-r--r--src/ui/dialog/calligraphic-profile-rename.h87
-rw-r--r--src/ui/dialog/clonetiler.cpp2821
-rw-r--r--src/ui/dialog/clonetiler.h208
-rw-r--r--src/ui/dialog/color-item.cpp506
-rw-r--r--src/ui/dialog/color-item.h127
-rw-r--r--src/ui/dialog/command-palette.cpp1659
-rw-r--r--src/ui/dialog/command-palette.h271
-rw-r--r--src/ui/dialog/debug.cpp259
-rw-r--r--src/ui/dialog/debug.h101
-rw-r--r--src/ui/dialog/dialog-base.cpp318
-rw-r--r--src/ui/dialog/dialog-base.h139
-rw-r--r--src/ui/dialog/dialog-container.cpp1153
-rw-r--r--src/ui/dialog/dialog-container.h130
-rw-r--r--src/ui/dialog/dialog-data.cpp81
-rw-r--r--src/ui/dialog/dialog-data.h54
-rw-r--r--src/ui/dialog/dialog-manager.cpp323
-rw-r--r--src/ui/dialog/dialog-manager.h97
-rw-r--r--src/ui/dialog/dialog-multipaned.cpp1312
-rw-r--r--src/ui/dialog/dialog-multipaned.h203
-rw-r--r--src/ui/dialog/dialog-notebook.cpp1023
-rw-r--r--src/ui/dialog/dialog-notebook.h128
-rw-r--r--src/ui/dialog/dialog-window.cpp281
-rw-r--r--src/ui/dialog/dialog-window.h78
-rw-r--r--src/ui/dialog/document-properties.cpp1863
-rw-r--r--src/ui/dialog/document-properties.h275
-rw-r--r--src/ui/dialog/document-resources.cpp1170
-rw-r--r--src/ui/dialog/document-resources.h105
-rw-r--r--src/ui/dialog/export-batch.cpp830
-rw-r--r--src/ui/dialog/export-batch.h182
-rw-r--r--src/ui/dialog/export-single.cpp1058
-rw-r--r--src/ui/dialog/export-single.h211
-rw-r--r--src/ui/dialog/export.cpp531
-rw-r--r--src/ui/dialog/export.h112
-rw-r--r--src/ui/dialog/filedialog.cpp186
-rw-r--r--src/ui/dialog/filedialog.h200
-rw-r--r--src/ui/dialog/filedialogimpl-gtkmm.cpp676
-rw-r--r--src/ui/dialog/filedialogimpl-gtkmm.h274
-rw-r--r--src/ui/dialog/filedialogimpl-win32.cpp1923
-rw-r--r--src/ui/dialog/filedialogimpl-win32.h371
-rw-r--r--src/ui/dialog/fill-and-stroke.cpp215
-rw-r--r--src/ui/dialog/fill-and-stroke.h92
-rw-r--r--src/ui/dialog/filter-effects-dialog.cpp3376
-rw-r--r--src/ui/dialog/filter-effects-dialog.h355
-rw-r--r--src/ui/dialog/find.cpp1145
-rw-r--r--src/ui/dialog/find.h313
-rw-r--r--src/ui/dialog/font-collections-manager.cpp172
-rw-r--r--src/ui/dialog/font-collections-manager.h90
-rw-r--r--src/ui/dialog/font-substitution.cpp241
-rw-r--r--src/ui/dialog/font-substitution.h39
-rw-r--r--src/ui/dialog/global-palettes.cpp99
-rw-r--r--src/ui/dialog/global-palettes.h66
-rw-r--r--src/ui/dialog/glyphs.cpp777
-rw-r--r--src/ui/dialog/glyphs.h84
-rw-r--r--src/ui/dialog/grid-arrange-tab.cpp660
-rw-r--r--src/ui/dialog/grid-arrange-tab.h152
-rw-r--r--src/ui/dialog/guides.cpp387
-rw-r--r--src/ui/dialog/guides.h106
-rw-r--r--src/ui/dialog/icon-preview.cpp637
-rw-r--r--src/ui/dialog/icon-preview.h109
-rw-r--r--src/ui/dialog/inkscape-preferences.cpp3886
-rw-r--r--src/ui/dialog/inkscape-preferences.h738
-rw-r--r--src/ui/dialog/input.cpp1792
-rw-r--r--src/ui/dialog/input.h46
-rw-r--r--src/ui/dialog/knot-properties.cpp189
-rw-r--r--src/ui/dialog/knot-properties.h96
-rw-r--r--src/ui/dialog/layer-properties.cpp435
-rw-r--r--src/ui/dialog/layer-properties.h158
-rw-r--r--src/ui/dialog/livepatheffect-add.cpp1000
-rw-r--r--src/ui/dialog/livepatheffect-add.h147
-rw-r--r--src/ui/dialog/livepatheffect-editor.cpp1258
-rw-r--r--src/ui/dialog/livepatheffect-editor.h127
-rw-r--r--src/ui/dialog/lpe-fillet-chamfer-properties.cpp255
-rw-r--r--src/ui/dialog/lpe-fillet-chamfer-properties.h110
-rw-r--r--src/ui/dialog/lpe-powerstroke-properties.cpp184
-rw-r--r--src/ui/dialog/lpe-powerstroke-properties.h90
-rw-r--r--src/ui/dialog/memory.cpp212
-rw-r--r--src/ui/dialog/memory.h51
-rw-r--r--src/ui/dialog/messages.cpp214
-rw-r--r--src/ui/dialog/messages.h96
-rw-r--r--src/ui/dialog/new-from-template.cpp90
-rw-r--r--src/ui/dialog/new-from-template.h43
-rw-r--r--src/ui/dialog/object-attributes.cpp760
-rw-r--r--src/ui/dialog/object-attributes.h118
-rw-r--r--src/ui/dialog/object-properties.cpp581
-rw-r--r--src/ui/dialog/object-properties.h143
-rw-r--r--src/ui/dialog/objects.cpp1860
-rw-r--r--src/ui/dialog/objects.h204
-rw-r--r--src/ui/dialog/paint-servers.cpp657
-rw-r--r--src/ui/dialog/paint-servers.h144
-rw-r--r--src/ui/dialog/polar-arrange-tab.cpp409
-rw-r--r--src/ui/dialog/polar-arrange-tab.h104
-rw-r--r--src/ui/dialog/print.cpp308
-rw-r--r--src/ui/dialog/print.h80
-rw-r--r--src/ui/dialog/prototype.cpp101
-rw-r--r--src/ui/dialog/prototype.h67
-rw-r--r--src/ui/dialog/save-template-dialog.cpp86
-rw-r--r--src/ui/dialog/save-template-dialog.h56
-rw-r--r--src/ui/dialog/selectorsdialog.cpp1340
-rw-r--r--src/ui/dialog/selectorsdialog.h194
-rw-r--r--src/ui/dialog/spellcheck.cpp751
-rw-r--r--src/ui/dialog/spellcheck.h281
-rw-r--r--src/ui/dialog/startup.cpp789
-rw-r--r--src/ui/dialog/startup.h87
-rw-r--r--src/ui/dialog/styledialog.cpp1601
-rw-r--r--src/ui/dialog/styledialog.h195
-rw-r--r--src/ui/dialog/svg-fonts-dialog.cpp1794
-rw-r--r--src/ui/dialog/svg-fonts-dialog.h384
-rw-r--r--src/ui/dialog/svg-preview.cpp448
-rw-r--r--src/ui/dialog/svg-preview.h123
-rw-r--r--src/ui/dialog/swatches.cpp447
-rw-r--r--src/ui/dialog/swatches.h114
-rw-r--r--src/ui/dialog/symbols.cpp1361
-rw-r--r--src/ui/dialog/symbols.h192
-rw-r--r--src/ui/dialog/text-edit.cpp647
-rw-r--r--src/ui/dialog/text-edit.h222
-rw-r--r--src/ui/dialog/tile.cpp136
-rw-r--r--src/ui/dialog/tile.h81
-rw-r--r--src/ui/dialog/tracedialog.cpp553
-rw-r--r--src/ui/dialog/tracedialog.h47
-rw-r--r--src/ui/dialog/transformation.cpp1216
-rw-r--r--src/ui/dialog/transformation.h231
-rw-r--r--src/ui/dialog/undo-history.cpp356
-rw-r--r--src/ui/dialog/undo-history.h161
-rw-r--r--src/ui/dialog/xml-tree.cpp928
-rw-r--r--src/ui/dialog/xml-tree.h200
134 files changed, 64120 insertions, 0 deletions
diff --git a/src/ui/dialog/README.md b/src/ui/dialog/README.md
new file mode 100644
index 0000000..17217d2
--- /dev/null
+++ b/src/ui/dialog/README.md
@@ -0,0 +1,46 @@
+# Dialog System
+
+Author: vanntile
+
+The dialog system used for dialog-type widgets is made out of the classes
+defined in the following header files (each explained later on):
+
+- dialog-container.h
+- dialog-multipaned.h
+- dialog-notebook.h
+- dialog-window.h
+- dialog-base.h
+
+This is a Gtk::Notebook based dialog manager with a GIMP style multipane.
+Dialogs can live only inside DialogNotebooks, as pages in the inner
+Gtk::Notebook, with their own tabs. A DialogNotebook itself is one of the
+children of a DialogMultipaned. There can be several levels of DialogMultipaned,
+but the top parent is a DialogContainer, which manages the existence of such
+dialogs.
+
+DialogMultipaned is a paned-type container which supports resizing the children
+by dragging the separator "handle" widgets. More than this, it supports the
+addition of new children (in DialogNotebook) by drag-and-dropping them at the
+extremeties of a multipane, where you can find dropzones (left and right for
+horizontal ones, or top and bottom for vertical ones).
+
+Dialogs can also live independently in their own DialogWindow. In this floating
+state, they track the last active window, while in the attached (docked) state,
+they track the InkscapeWindow they are in. You can drag tabs from DialogNotebook
+to move a DialogBase (or child class instance) between windows. In Wayland,
+if you drag the tab to an invalid position, it will create automatically a
+DialogWindow to live in.
+
+DialogContainers are instantiated with a horizontal DialogMultipaned.
+
+## Initialisation
+
+The initial DialogContainer is created inside a DesktopWidget, then inserting
+toolbars and an empty vertical DialogMultipaned where new dialogs will be added.
+
+## Adding a new dialog
+
+In order to add a new dialog to a window, you have to call the method
+`new_dialog(const Glib::ustring& dialog_type)` on the container inside
+the desktop of the window. Allowed dialog types can be found in the
+dialog-container.cpp file.
diff --git a/src/ui/dialog/about.cpp b/src/ui/dialog/about.cpp
new file mode 100644
index 0000000..fec809a
--- /dev/null
+++ b/src/ui/dialog/about.cpp
@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for the about screen
+ *
+ * Copyright (C) Martin Owens 2019 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "about.h"
+
+#include <algorithm>
+#include <fstream>
+#include <random>
+#include <regex>
+#include <streambuf>
+#include <string>
+
+#include "document.h"
+#include "inkscape-version-info.h"
+#include "io/resource.h"
+#include "ui/util.h"
+#include "ui/view/svg-view-widget.h"
+#include "util/units.h"
+
+using namespace Inkscape::IO;
+using namespace Inkscape::UI::View;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+static Gtk::Window *window = nullptr;
+static Gtk::Notebook *tabs = nullptr;
+
+void close_about_screen() {
+ window->hide();
+}
+bool show_copy_button(Gtk::Button *button, Gtk::Label *label) {
+ reveal_widget(button, true);
+ reveal_widget(label, false);
+ return false;
+}
+void copy_version(Gtk::Button *button, Gtk::Label *label) {
+ auto clipboard = Gtk::Clipboard::get();
+ clipboard->set_text(Inkscape::inkscape_version());
+ if (label) {
+ reveal_widget(button, false);
+ reveal_widget(label, true);
+ Glib::signal_timeout().connect_seconds(
+ sigc::bind(sigc::ptr_fun(&show_copy_button), button, label), 2);
+ }
+}
+void copy_debug_info(Gtk::Button *button, Gtk::Label *label) {
+ auto clipboard = Gtk::Clipboard::get();
+ clipboard->set_text(Inkscape::debug_info());
+ if (label) {
+ reveal_widget(button, false);
+ reveal_widget(label, true);
+ Glib::signal_timeout().connect_seconds(
+ sigc::bind(sigc::ptr_fun(&show_copy_button), button, label), 2);
+ }
+}
+
+void AboutDialog::show_about() {
+
+ if(!window) {
+ // Load glade file here
+ Glib::ustring gladefile = Resource::get_filename(Resource::UIS, "inkscape-about.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_error("Glade file loading failed for about screen dialog");
+ return;
+ }
+ builder->get_widget("about-screen-window", window);
+ builder->get_widget("tabs", tabs);
+ if(!tabs || !window) {
+ g_error("Window or tabs in glade file are missing or do not have the right ids.");
+ return;
+ }
+ // Automatic signal handling (requires -rdynamic compile flag)
+ //gtk_builder_connect_signals(builder->gobj(), NULL);
+
+ // When automatic handling fails
+ Gtk::Button *version;
+ Gtk::Label *label;
+ builder->get_widget("version", version);
+ builder->get_widget("version-copied", label);
+ if(version) {
+ version->set_label(Inkscape::inkscape_version());
+ version->signal_clicked().connect(
+ sigc::bind(sigc::ptr_fun(&copy_version), version, label));
+ }
+
+ Gtk::Button *debug_info;
+ Gtk::Label *label2;
+ builder->get_widget("debug_info", debug_info);
+ builder->get_widget("debug-info-copied", label2);
+ if (debug_info) {
+ debug_info->signal_clicked().connect(
+ sigc::bind(sigc::ptr_fun(&copy_debug_info), version, label2));
+ }
+
+ Gtk::Label *copyright;
+ builder->get_widget("copyright", copyright);
+ if (copyright) {
+ copyright->set_label(
+ Glib::ustring::compose(copyright->get_label(), Inkscape::inkscape_build_year()));
+ }
+
+ // Render the about screen image via inkscape SPDocument
+ auto filename = Resource::get_filename(Resource::SCREENS, "about.svg", true, false);
+ SPDocument *doc = SPDocument::createNewDoc(filename.c_str(), TRUE);
+
+ // Bind glade's container to our SVGViewWidget class
+ if(doc) {
+ //SVGViewWidget *viewer;
+ //builder->get_widget_derived("image-container", viewer, doc);
+ //Gtk::manage(viewer);
+ auto viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc));
+ double width = doc->getWidth().value("px");
+ double height = doc->getHeight().value("px");
+ viewer->setResize(width, height);
+
+ Gtk::AspectFrame *splash_widget;
+ builder->get_widget("aspect-frame", splash_widget);
+ splash_widget->unset_label();
+ splash_widget->set_shadow_type(Gtk::SHADOW_NONE);
+ splash_widget->property_ratio() = width / height;
+ splash_widget->add(*viewer);
+ splash_widget->show_all();
+ } else {
+ g_error("Error loading about screen SVG.");
+ }
+
+ Gtk::TextView *authors;
+ builder->get_widget("credits-authors", authors);
+ std::random_device rd;
+ std::mt19937 g(rd());
+
+ if(authors) {
+ std::ifstream fn(Resource::get_filename(Resource::DOCS, "AUTHORS"));
+ std::vector<std::string> authors_data;
+ std::string line;
+ while (getline(fn, line)) {
+ authors_data.push_back(line);
+ }
+ std::shuffle(std::begin(authors_data), std::end(authors_data), g);
+ std::string str = "";
+ for (auto author : authors_data) {
+ str += author + "\n";
+ }
+ authors->get_buffer()->set_text(str.c_str());
+ }
+
+ Gtk::TextView *translators;
+ builder->get_widget("credits-translators", translators);
+ if(translators) {
+ std::ifstream fn(Resource::get_filename(Resource::DOCS, "TRANSLATORS"));
+ std::vector<std::string> translators_data;
+ std::string line;
+ while (getline(fn, line)) {
+ translators_data.push_back(line);
+ }
+ std::string str = "";
+ std::regex e("(.*?)(<.*|)");
+ std::shuffle(std::begin(translators_data), std::end(translators_data), g);
+ for (auto translator : translators_data) {
+ str += std::regex_replace(translator, e, "$1") + "\n";
+ }
+ translators->get_buffer()->set_text(str.c_str());
+ }
+
+ Gtk::Label *license;
+ builder->get_widget("license-text", license);
+ if(license) {
+ std::ifstream fn(Resource::get_filename(Resource::DOCS, "LICENSE"));
+ std::string str((std::istreambuf_iterator<char>(fn)),
+ std::istreambuf_iterator<char>());
+ license->set_markup(str.c_str());
+ }
+ }
+ if(window) {
+ window->show();
+ tabs->set_current_page(0);
+ } else {
+ g_error("About screen window couldn't be loaded. Missing window id in glade file.");
+ }
+}
+
+} // 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/dialog/about.h b/src/ui/dialog/about.h
new file mode 100644
index 0000000..b6d7923
--- /dev/null
+++ b/src/ui/dialog/about.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for the about screen
+ *
+ * Copyright (C) Martin Owens 2019 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef ABOUTDIALOG_H
+#define ABOUTDIALOG_H
+
+#include <gtkmm.h>
+#include <gtkmm/builder.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class AboutDialog {
+
+ public:
+ static void show_about();
+
+ private:
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // ABOUTDIALOG_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/dialog/align-and-distribute.cpp b/src/ui/dialog/align-and-distribute.cpp
new file mode 100644
index 0000000..709f221
--- /dev/null
+++ b/src/ui/dialog/align-and-distribute.cpp
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Align and Distribute widget
+ */
+/* Authors:
+ * Tavmjong Bah
+ *
+ * Based on dialog by:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Aubanel MONNIER <aubi@libertysurf.fr>
+ * Frank Felfe <innerspace@iname.com>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2021 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "align-and-distribute.h" // widget
+
+#include <iostream>
+
+#include <giomm.h>
+
+#include "desktop.h" // Tool switching.
+#include "inkscape-window.h" // Activate window action.
+#include "actions/actions-tools.h" // Tool switching.
+#include "io/resource.h"
+#include "ui/dialog/dialog-base.h" // Tool switching.
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+using Inkscape::IO::Resource::get_filename;
+using Inkscape::IO::Resource::UIS;
+
+AlignAndDistribute::AlignAndDistribute(Inkscape::UI::Dialog::DialogBase* dlg)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ Glib::ustring builder_file = get_filename(UIS, "align-and-distribute.ui");
+ auto builder = Gtk::Builder::create();
+ try
+ {
+ builder->add_from_file(builder_file);
+ }
+ catch (const Glib::Error& ex)
+ {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: " << builder_file.raw() << " file not read! " << ex.what().raw() << std::endl;
+ }
+
+ builder->get_widget("align-and-distribute-box", align_and_distribute_box);
+ if (!align_and_distribute_box) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (box)!" << std::endl;
+ } else {
+ add(*align_and_distribute_box);
+ }
+
+ builder->get_widget("align-and-distribute-object", align_and_distribute_object);
+ if (!align_and_distribute_object) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (object)!" << std::endl;
+ } else {
+ align_and_distribute_object->show();
+ }
+
+ builder->get_widget("align-and-distribute-node", align_and_distribute_node);
+ if (!align_and_distribute_node) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (node)!" << std::endl;
+ } else {
+ align_and_distribute_node->hide();
+ }
+
+ // ------------ Object Align -------------
+
+ builder->get_widget("align-relative-object", align_relative_object);
+ if (!align_relative_object) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (combobox)!" << std::endl;
+ } else {
+ std::string align_to = prefs->getString("/dialogs/align/objects-align-to", "selection");
+ align_relative_object->set_active_id(align_to);
+ align_relative_object->signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_relative_object_changed));
+ }
+
+ builder->get_widget("align-move-as-group", align_move_as_group);
+ if (!align_move_as_group) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (group button)!" << std::endl;
+ } else {
+ bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups");
+ align_move_as_group->set_active(sel_as_group);
+
+ align_move_as_group->signal_clicked().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_as_group_clicked));
+ }
+
+ // clang-format off
+ std::vector<std::pair<std::string, std::string>> align_buttons = {
+ {"align-horizontal-right-to-anchor", "right anchor" },
+ {"align-horizontal-left", "left" },
+ {"align-horizontal-center", "hcenter" },
+ {"align-horizontal-right", "right" },
+ {"align-horizontal-left-to-anchor", "left anchor" },
+ {"align-horizontal-baseline", "horizontal" },
+ {"align-vertical-bottom-to-anchor", "bottom anchor" },
+ {"align-vertical-top", "top" },
+ {"align-vertical-center", "vcenter" },
+ {"align-vertical-bottom", "bottom" },
+ {"align-vertical-top-to-anchor", "top anchor" },
+ {"align-vertical-baseline", "vertical" }
+ };
+ // clang-format on
+
+ for (auto align_button: align_buttons) {
+ Gtk::Button* button;
+ builder->get_widget(align_button.first, button);
+ if (!button) {
+ std::cerr << "AlignAndDistribute::AlignAndDisribute: failed to get button: "
+ << align_button.first << " " << align_button.second << std::endl;
+ } else {
+ button->signal_button_press_event().connect(
+ sigc::bind<std::string>(sigc::mem_fun(*this, &AlignAndDistribute::on_align_button_press_event), align_button.second), false);
+ }
+ }
+
+
+ // ------------ Remove overlap -------------
+
+ builder->get_widget("remove-overlap-button", remove_overlap_button);
+ if (!remove_overlap_button) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl;
+ } else {
+ remove_overlap_button->signal_button_press_event().connect(
+ sigc::mem_fun(*this, &AlignAndDistribute::on_remove_overlap_button_press_event), false); // false => run first.
+ }
+
+ builder->get_widget("remove-overlap-hgap", remove_overlap_hgap);
+ if (!remove_overlap_hgap) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl;
+ }
+
+ builder->get_widget("remove-overlap-vgap", remove_overlap_vgap);
+ if (!remove_overlap_vgap) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget!" << std::endl;
+ }
+
+ // ------------ Node Align -------------
+
+ builder->get_widget("align-relative-node", align_relative_node);
+ if (!align_relative_node) {
+ std::cerr << "AlignAndDistribute::AlignAndDistribute: failed to load widget (combobox)!" << std::endl;
+ } else {
+ std::string align_nodes_to = prefs->getString("/dialogs/align/nodes-align-to", "first");
+ align_relative_node->set_active_id(align_nodes_to);
+ align_relative_node->signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_align_relative_node_changed));
+ }
+
+ std::vector<std::pair<std::string, std::string>> align_node_buttons = {
+ {"align-node-horizontal", "horizontal"},
+ {"align-node-vertical", "vertical" }
+ };
+
+ for (auto align_button: align_node_buttons) {
+ Gtk::Button* button;
+ builder->get_widget(align_button.first, button);
+ if (!button) {
+ std::cerr << "AlignAndDistribute::AlignAndDisribute: failed to get button: "
+ << align_button.first << " " << align_button.second << std::endl;
+ } else {
+ button->signal_button_press_event().connect(
+ sigc::bind<std::string>(sigc::mem_fun(*this, &AlignAndDistribute::on_align_node_button_press_event), align_button.second), false);
+ }
+ }
+
+
+ // ------------ Set initial values ------------
+
+ // Normal or node alignment?
+ auto desktop = dlg->getDesktop();
+ if (desktop) {
+ desktop_changed(desktop);
+ }
+
+ auto set_icon_size_prefs = [=]() {
+ int size = prefs->getIntLimited("/toolbox/tools/iconsize", -1, 16, 48);
+ Inkscape::UI::set_icon_sizes(this, size);
+ };
+
+ // For now we are going to track the toolbox icon size, in the future we will have our own
+ // dialog based icon sizes, perhaps done via css instead.
+ _icon_sizes_changed = prefs->createObserver("/toolbox/tools/iconsize", set_icon_size_prefs);
+ set_icon_size_prefs();
+}
+
+void
+AlignAndDistribute::desktop_changed(SPDesktop* desktop)
+{
+ tool_connection.disconnect();
+ if (desktop) {
+ tool_connection =
+ desktop->connectEventContextChanged(sigc::mem_fun(*this, &AlignAndDistribute::tool_changed_callback));
+ tool_changed(desktop);
+ }
+}
+
+void
+AlignAndDistribute::tool_changed(SPDesktop* desktop)
+{
+ bool node = get_active_tool(desktop) == "Node";
+ if (node) {
+ align_and_distribute_object->hide();
+ align_and_distribute_node->show();
+ } else {
+ align_and_distribute_object->show();
+ align_and_distribute_node->hide();
+ }
+}
+
+void
+AlignAndDistribute::tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec)
+{
+ tool_changed(desktop);
+}
+
+
+void
+AlignAndDistribute::on_align_as_group_clicked()
+{
+ bool state = align_move_as_group->get_active();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/align/sel-as-groups", state);
+}
+
+void
+AlignAndDistribute::on_align_relative_object_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/dialogs/align/objects-align-to", align_relative_object->get_active_id());
+}
+
+void
+AlignAndDistribute::on_align_relative_node_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/dialogs/align/nodes-align-to", align_relative_node->get_active_id());
+}
+
+bool
+AlignAndDistribute::on_align_button_press_event(GdkEventButton* button_event, const std::string& align_to)
+{
+ Glib::ustring argument = align_to;
+
+ argument += " " + align_relative_object->get_active_id();
+
+ if (align_move_as_group->get_active()) {
+ argument += " group";
+ }
+
+ auto variant = Glib::Variant<Glib::ustring>::create(argument);
+ auto app = Gio::Application::get_default();
+
+ if (align_to.find("vertical") != Glib::ustring::npos or align_to.find("horizontal") != Glib::ustring::npos) {
+ app->activate_action("object-align-text", variant);
+ } else {
+ app->activate_action("object-align", variant);
+ }
+
+ return true;
+}
+
+bool
+AlignAndDistribute::on_remove_overlap_button_press_event(GdkEventButton* button_event)
+{
+ double hgap = remove_overlap_hgap->get_value();
+ double vgap = remove_overlap_vgap->get_value();
+
+ auto variant = Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(hgap, vgap));
+ auto app = Gio::Application::get_default();
+ app->activate_action("object-remove-overlaps", variant);
+ return true;
+}
+
+bool
+AlignAndDistribute::on_align_node_button_press_event(GdkEventButton* button_event, const std::string& direction)
+{
+ Glib::ustring argument = align_relative_node->get_active_id();
+
+ auto variant = Glib::Variant<Glib::ustring>::create(argument);
+ InkscapeWindow* win = InkscapeApplication::instance()->get_active_window();
+ if (!win) {
+ return true;
+ }
+ if (direction == "horizontal") {
+ win->activate_action("node-align-horizontal", variant);
+ } else {
+ win->activate_action("node-align-vertical", variant);
+ }
+
+ return true;
+}
+
+} // 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/dialog/align-and-distribute.h b/src/ui/dialog/align-and-distribute.h
new file mode 100644
index 0000000..1e94e1a
--- /dev/null
+++ b/src/ui/dialog/align-and-distribute.h
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Align and Distribute widget
+ */
+/* Authors:
+ * Tavmjong Bah
+ *
+ * Based on dialog by:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Aubanel MONNIER <aubi@libertysurf.fr>
+ * Frank Felfe <innerspace@iname.com>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ *
+ * Copyright (C) 2021 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H
+#define INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_H
+
+#include <sigc++/connection.h>
+#include <gtkmm.h>
+
+#include "preferences.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Tools {
+class ToolBase;
+}
+
+namespace Dialog {
+class DialogBase;
+
+class AlignAndDistribute : public Gtk::Box
+{
+public:
+ AlignAndDistribute(Inkscape::UI::Dialog::DialogBase* dlg);
+ ~AlignAndDistribute() override = default;
+
+ void desktop_changed(SPDesktop* desktop);
+ void tool_changed(SPDesktop* desktop); // Need to show different widgets for node vs. other tools.
+ void tool_changed_callback(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec);
+
+private:
+
+ // ********* Widgets ********** //
+
+ Gtk::Box* align_and_distribute_box = nullptr;
+ Gtk::Box* align_and_distribute_object = nullptr; // Hidden when node tool active.
+ Gtk::Box* align_and_distribute_node = nullptr; // Visible when node tool active.
+
+ // Align
+ Gtk::ToggleButton* align_move_as_group = nullptr;
+ Gtk::ComboBox* align_relative_object = nullptr;
+ Gtk::ComboBox* align_relative_node = nullptr;
+
+ // Remove overlap
+ Gtk::Button* remove_overlap_button = nullptr;
+ Gtk::SpinButton* remove_overlap_hgap = nullptr;
+ Gtk::SpinButton* remove_overlap_vgap = nullptr;
+
+
+ // ********* Signal handlers ********** //
+
+ void on_align_as_group_clicked();
+ void on_align_relative_object_changed();
+ void on_align_relative_node_changed();
+
+ bool on_align_button_press_event(GdkEventButton* button_event, const std::string& align_to);
+ bool on_remove_overlap_button_press_event(GdkEventButton* button_event);
+ bool on_align_node_button_press_event(GdkEventButton* button_event, const std::string& align_to);
+
+ sigc::connection tool_connection;
+ Inkscape::PrefObserver _icon_sizes_changed;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_ALIGN_AND_DISTRIBUTE_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/dialog/arrange-tab.h b/src/ui/dialog/arrange-tab.h
new file mode 100644
index 0000000..6f464b9
--- /dev/null
+++ b/src/ui/dialog/arrange-tab.h
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @brief Arrange tools base class
+ */
+/* Authors:
+ * * Declara Denis
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_ARRANGE_TAB_H
+#define INKSCAPE_UI_DIALOG_ARRANGE_TAB_H
+
+#include <gtkmm/box.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * This interface should be implemented by each arrange mode.
+ * The class is a Gtk::VBox and will be displayed as a tab in
+ * the dialog
+ */
+class ArrangeTab : public Gtk::Box
+{
+public:
+ ArrangeTab() : Gtk::Box(Gtk::ORIENTATION_VERTICAL) {}
+ ~ArrangeTab() override = default;
+
+ /**
+ * Do the actual work! This method is invoked to actually arrange the
+ * selection
+ */
+ virtual void arrange() = 0;
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+
+#endif /* INKSCAPE_UI_DIALOG_ARRANGE_TAB_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/dialog/attrdialog.cpp b/src/ui/dialog/attrdialog.cpp
new file mode 100644
index 0000000..88b411d
--- /dev/null
+++ b/src/ui/dialog/attrdialog.cpp
@@ -0,0 +1,848 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for XML attributes
+ */
+/* Authors:
+ * Martin Owens
+ *
+ * Copyright (C) Martin Owens 2018 <doctormo@gmail.com>
+ *
+ * Released under GNU GPLv2 or later, read the file 'COPYING' for more information
+ */
+
+#include "attrdialog.h"
+
+#include "preferences.h"
+#include "selection.h"
+#include "document-undo.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "style.h"
+
+#include "io/resource.h"
+
+#include "ui/builder-utils.h"
+#include "ui/dialog/inkscape-preferences.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/syntax.h"
+#include "ui/util.h"
+#include "ui/widget/shapeicon.h"
+#include "util/numeric/converters.h"
+#include "util/trim.h"
+#include "xml/attribute-record.h"
+
+#include <cstddef>
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/regex.h>
+#include <glibmm/timer.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/label.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/object.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/targetlist.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/widget.h>
+#include <memory>
+#include <string>
+
+#include "config.h"
+#if WITH_GSOURCEVIEW
+# include <gtksourceview/gtksource.h>
+#endif
+
+/**
+ * Return true if `node` is a text or comment node
+ */
+static bool is_text_or_comment_node(Inkscape::XML::Node const &node)
+{
+ switch (node.type()) {
+ case Inkscape::XML::NodeType::TEXT_NODE:
+ case Inkscape::XML::NodeType::COMMENT_NODE:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static Glib::ustring get_syntax_theme()
+{
+ return Inkscape::Preferences::get()->getString("/theme/syntax-color-theme", "-none-");
+}
+
+namespace Inkscape::UI::Dialog {
+
+// arbitrarily selected size limits
+constexpr int MAX_POPOVER_HEIGHT = 450;
+constexpr int MAX_POPOVER_WIDTH = 520;
+constexpr int TEXT_MARGIN = 3;
+
+std::unique_ptr<Syntax::TextEditView> AttrDialog::init_text_view(AttrDialog* owner, Syntax::SyntaxMode coloring, bool map)
+{
+ auto edit = Syntax::TextEditView::create(coloring);
+ auto& textview = edit->getTextView();
+ textview.set_wrap_mode(Gtk::WrapMode::WRAP_WORD);
+
+ // this actually sets padding rather than margin and extends textview's background color to the sides
+ textview.set_top_margin(TEXT_MARGIN);
+ textview.set_left_margin(TEXT_MARGIN);
+ textview.set_right_margin(TEXT_MARGIN);
+ textview.set_bottom_margin(TEXT_MARGIN);
+
+ if (map) {
+ textview.signal_map().connect([owner](){
+ // this is not effective: text view recalculates its size on idle, so it's too early to call on 'map';
+ // (note: there's no signal on a TextView to tell us that formatting has been done)
+ // delay adjustment; this will work if UI is fast enough, but at the cost of popup jumping,
+ // but at least it will be sized properly
+ owner->_adjust_size = Glib::signal_timeout().connect([=](){ owner->adjust_popup_edit_size(); return false; }, 50);
+ });
+ }
+
+ return edit;
+}
+
+/**
+ * Constructor
+ * A treeview whose each row corresponds to an XML attribute of a selected node
+ * New attribute can be added by clicking '+' at bottom of the attr pane. '-'
+ */
+AttrDialog::AttrDialog()
+ : DialogBase("/dialogs/attr", "AttrDialog")
+ , _builder(create_builder("attribute-edit-component.glade"))
+ , _scrolled_text_view(get_widget<Gtk::ScrolledWindow>(_builder, "scroll-wnd"))
+ , _content_sw(get_widget<Gtk::ScrolledWindow>(_builder, "content-sw"))
+ , _scrolled_window(get_widget<Gtk::ScrolledWindow>(_builder, "scrolled-wnd"))
+ , _treeView(get_widget<Gtk::TreeView>(_builder, "tree-view"))
+ , _popover(&get_widget<Gtk::Popover>(_builder, "popup"))
+ , _status_box(get_widget<Gtk::Box>(_builder, "status-box"))
+ , _status(get_widget<Gtk::Label>(_builder, "status-label"))
+{
+ // Attribute value editing (with syntax highlighting).
+ using namespace Syntax;
+ _css_edit = init_text_view(this, SyntaxMode::InlineCss, true);
+ _svgd_edit = init_text_view(this, SyntaxMode::SvgPathData, true);
+ _points_edit = init_text_view(this, SyntaxMode::SvgPolyPoints, true);
+ _attr_edit = init_text_view(this, SyntaxMode::PlainText, true);
+
+ // string content editing
+ _text_edit = init_text_view(this, SyntaxMode::PlainText, false);
+ _style_edit = init_text_view(this, SyntaxMode::CssStyle, false);
+
+ set_size_request(20, 15);
+
+ // For text and comment nodes: update XML on the fly, as users type
+ for (auto tv : {&_text_edit->getTextView(), &_style_edit->getTextView()}) {
+ tv->get_buffer()->signal_end_user_action().connect([=]() {
+ if (_repr) {
+ _repr->setContent(tv->get_buffer()->get_text().c_str());
+ setUndo(_("Type text"));
+ }
+ });
+ }
+
+ _store = Gtk::ListStore::create(_attrColumns);
+ _treeView.set_model(_store);
+
+ // high-res aware icon renderer for a trash can
+ auto delete_renderer = manage(new Inkscape::UI::Widget::CellRendererItemIcon());
+ delete_renderer->property_shape_type().set_value("edit-delete");
+ _treeView.append_column("", *delete_renderer);
+ Gtk::TreeViewColumn *col = _treeView.get_column(0);
+ if (col) {
+ auto add_icon = Gtk::manage(sp_get_icon_image("list-add", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ col->set_clickable(true);
+ col->set_widget(*add_icon);
+ add_icon->set_tooltip_text(_("Add a new attribute"));
+ add_icon->show();
+ auto button = add_icon->get_parent()->get_parent()->get_parent();
+ // Assign the button event so that create happens BEFORE delete. If this code
+ // isn't in this exact way, the onAttrDelete is called when the header lines are pressed.
+ button->signal_button_release_event().connect(sigc::mem_fun(*this, &AttrDialog::onAttrCreate), false);
+ }
+ delete_renderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete));
+ _treeView.signal_key_press_event().connect(sigc::mem_fun(*this, &AttrDialog::onKeyPressed));
+
+ _nameRenderer = Gtk::make_managed<Gtk::CellRendererText>();
+ _nameRenderer->property_editable() = true;
+ _nameRenderer->property_placeholder_text().set_value(_("Attribute Name"));
+ _nameRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::nameEdited));
+ _nameRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startNameEdit));
+ _treeView.append_column(_("Name"), *_nameRenderer);
+ _nameCol = _treeView.get_column(1);
+ if (_nameCol) {
+ _nameCol->set_resizable(true);
+ _nameCol->add_attribute(_nameRenderer->property_text(), _attrColumns._attributeName);
+ }
+
+ _message_stack = std::make_shared<Inkscape::MessageStack>();
+ _message_context = std::make_unique<Inkscape::MessageContext>(_message_stack);
+ _message_changed_connection = _message_stack->connectChanged([=](MessageType, const char* message) {
+ _status.set_markup(message ? message : "");
+ });
+
+ _valueRenderer = Gtk::make_managed<Gtk::CellRendererText>();
+ _valueRenderer->property_editable() = true;
+ _valueRenderer->property_placeholder_text().set_value(_("Attribute Value"));
+ _valueRenderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END);
+ _valueRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::valueEdited));
+ _valueRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startValueEdit), true);
+ _treeView.append_column(_("Value"), *_valueRenderer);
+ _valueCol = _treeView.get_column(2);
+ if (_valueCol) {
+ _valueCol->add_attribute(_valueRenderer->property_text(), _attrColumns._attributeValueRender);
+ }
+
+ set_current_textedit(_attr_edit.get());
+ _scrolled_text_view.set_max_content_height(MAX_POPOVER_HEIGHT);
+
+ auto& apply = get_widget<Gtk::Button>(_builder, "btn-ok");
+ apply.signal_clicked().connect([=]() { valueEditedPop(); });
+
+ auto& cancel = get_widget<Gtk::Button>(_builder, "btn-cancel");
+ cancel.signal_clicked().connect([=](){
+ if (!_value_editing.empty()) {
+ _activeTextView().get_buffer()->set_text(_value_editing);
+ }
+ _popover->popdown();
+ });
+
+ _popover->signal_closed().connect([=]() { popClosed(); });
+ _popover->signal_key_press_event().connect([=](GdkEventKey* ev) { return key_callback(ev); }, false);
+ _popover->hide();
+
+ get_widget<Gtk::Button>(_builder, "btn-truncate").signal_clicked().connect([=](){ truncateDigits(); });
+
+ const int N = 5;
+ _rounding_precision = Inkscape::Preferences::get()->getIntLimited("/dialogs/attrib/precision", 2, 0, N);
+ for (int n = 0; n <= N; ++n) {
+ auto id = '_' + std::to_string(n);
+ auto item = &get_widget<Gtk::MenuItem>(_builder, id.c_str());
+ auto action = [=](){
+ _rounding_precision = n;
+ get_widget<Gtk::Label>(_builder, "precision").set_label(' ' + item->get_label());
+ Inkscape::Preferences::get()->setInt("/dialogs/attrib/precision", n);
+ };
+ item->signal_activate().connect(action);
+
+ if (n == _rounding_precision) {
+ action();
+ }
+ }
+
+ attr_reset_context(0);
+ pack_start(get_widget<Gtk::Box>(_builder, "main-box"), Gtk::PACK_EXPAND_WIDGET);
+ _updating = false;
+}
+
+AttrDialog::~AttrDialog()
+{
+ _current_text_edit = nullptr;
+ _popover->hide();
+
+ // remove itself from the list of node observers
+ setRepr(nullptr);
+}
+
+static int fmt_number(_GMatchInfo const *match, _GString *ret, void *prec)
+{
+ auto number = g_match_info_fetch(match, 1);
+
+ char *end;
+ double val = g_ascii_strtod(number, &end);
+ if (*number && (end == nullptr || end > number)) {
+ auto precision = *static_cast<int*>(prec);
+ auto fmt = Util::format_number(val, precision);
+ g_string_append(ret, fmt.c_str());
+ } else {
+ g_string_append(ret, number);
+ }
+
+ auto text = g_match_info_fetch(match, 2);
+ g_string_append(ret, text);
+
+ g_free(number);
+ g_free(text);
+
+ return false;
+}
+
+Glib::ustring AttrDialog::round_numbers(const Glib::ustring& text, int precision)
+{
+ // match floating point number followed by something else (not a number); repeat
+ static const auto numbers = Glib::Regex::create("([-+]?(?:(?:\\d+\\.?\\d*)|(?:\\.\\d+))(?:[eE][-+]?\\d*)?)([^+\\-0-9]*)", Glib::REGEX_MULTILINE);
+
+ return numbers->replace_eval(text, text.size(), 0, Glib::RegexMatchFlags::REGEX_MATCH_NOTEMPTY, &fmt_number, &precision);
+}
+
+/** Round the selected floating point numbers in the attribute edit popover. */
+void AttrDialog::truncateDigits() const
+{
+ if (!_current_text_edit) {
+ return;
+ }
+
+ auto buffer = _current_text_edit->getTextView().get_buffer();
+ auto start = buffer->begin();
+ auto end = buffer->end();
+
+ bool const had_selection = buffer->get_has_selection();
+ int start_idx = 0, end_idx = 0;
+ if (had_selection) {
+ buffer->get_selection_bounds(start, end);
+ start_idx = start.get_offset();
+ end_idx = end.get_offset();
+ }
+
+ auto text = buffer->get_text(start, end);
+ auto ret = round_numbers(text, _rounding_precision);
+ buffer->erase(start, end);
+ buffer->insert_at_cursor(ret);
+
+ if (had_selection) {
+ // Restore selection but note that its length may have decreased.
+ end_idx -= text.size() - ret.size();
+ if (end_idx < start_idx) {
+ end_idx = start_idx;
+ }
+ buffer->select_range(buffer->get_iter_at_offset(start_idx), buffer->get_iter_at_offset(end_idx));
+ }
+}
+
+void AttrDialog::set_current_textedit(Syntax::TextEditView* edit)
+{
+ _current_text_edit = edit ? edit : _attr_edit.get();
+ _scrolled_text_view.remove();
+ _scrolled_text_view.add(_current_text_edit->getTextView());
+ _scrolled_text_view.show_all();
+}
+
+void AttrDialog::adjust_popup_edit_size()
+{
+ auto vscroll = _scrolled_text_view.get_vadjustment();
+ int height = vscroll->get_upper() + 2 * TEXT_MARGIN;
+ if (height < MAX_POPOVER_HEIGHT) {
+ _scrolled_text_view.set_min_content_height(height);
+ vscroll->set_value(vscroll->get_lower());
+ } else {
+ _scrolled_text_view.set_min_content_height(MAX_POPOVER_HEIGHT);
+ }
+}
+
+bool AttrDialog::key_callback(GdkEventKey* event) {
+ switch (event->keyval) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (_popover->is_visible()) {
+ if (event->state & GDK_SHIFT_MASK) {
+ valueEditedPop();
+ return true;
+ }
+ else {
+ // as we type and content grows, resize the popup to accommodate it
+ _adjust_size = Glib::signal_timeout().connect([=](){ adjust_popup_edit_size(); return false; }, 50);
+ }
+ }
+ break;
+ }
+ return false;
+}
+
+/**
+ * Prepare value string suitable for display in a Gtk::CellRendererText
+ *
+ * Value is truncated at the first new line character (if any) and a visual indicator and ellipsis is added.
+ * Overall length is limited as well to prevent performance degradation for very long values.
+ *
+ * @param value Raw attribute value as UTF-8 encoded string
+ * @return Single-line string with fixed maximum length
+ */
+static Glib::ustring prepare_rendervalue(const char *value)
+{
+ constexpr int MAX_LENGTH = 500; // maximum length of string before it's truncated for performance reasons
+ // ~400 characters fit horizontally on a WQHD display, so 500 should be plenty
+ Glib::ustring renderval;
+
+ // truncate to MAX_LENGTH
+ if (g_utf8_strlen(value, -1) > MAX_LENGTH) {
+ renderval = Glib::ustring(value, MAX_LENGTH) + "…";
+ } else {
+ renderval = value;
+ }
+
+ // truncate at first newline (if present) and add a visual indicator
+ auto ind = renderval.find('\n');
+ if (ind != Glib::ustring::npos) {
+ renderval.replace(ind, Glib::ustring::npos, " ⏎ …");
+ }
+
+ return renderval;
+}
+
+void set_mono_class(Gtk::Widget* widget, bool mono)
+{
+ if (!widget) {
+ return;
+ }
+ Glib::ustring class_name = "mono-font";
+ auto style = widget->get_style_context();
+ auto has_class = style->has_class(class_name);
+
+ if (mono && !has_class) {
+ style->add_class(class_name);
+ } else if (!mono && has_class) {
+ style->remove_class(class_name);
+ }
+}
+
+void AttrDialog::set_mono_font(bool mono)
+{
+ set_mono_class(&_treeView, mono);
+}
+
+void AttrDialog::startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
+{
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell);
+ entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &AttrDialog::onNameKeyPressed), entry));
+}
+
+Gtk::TextView &AttrDialog::_activeTextView() const
+{
+ return _current_text_edit->getTextView();
+}
+
+void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
+{
+ _value_path = path;
+ Gtk::TreeIter iter = *_store->get_iter(path);
+ Gtk::TreeModel::Row row = *iter;
+ if (!row || !_repr || !cell) {
+ return;
+ }
+
+ // popover in GTK3 is clipped to dialog window (in a floating dialog); limit size:
+ const int dlg_width = get_allocated_width() - 10;
+ _popover->set_size_request(std::min(MAX_POPOVER_WIDTH, dlg_width), -1);
+
+ auto const attribute = row[_attrColumns._attributeName];
+ bool edit_in_popup =
+#if WITH_GSOURCEVIEW
+ true;
+#else
+ false;
+#endif
+ bool enable_rouding = false;
+
+ if (attribute == "style") {
+ set_current_textedit(_css_edit.get());
+ } else if (attribute == "d" || attribute == "inkscape:original-d") {
+ enable_rouding = true;
+ set_current_textedit(_svgd_edit.get());
+ } else if (attribute == "points") {
+ enable_rouding = true;
+ set_current_textedit(_points_edit.get());
+ } else {
+ set_current_textedit(_attr_edit.get());
+ edit_in_popup = false;
+ }
+
+ // number rounding functionality
+ widget_show(get_widget<Gtk::Box>(_builder, "rounding-box"), enable_rouding);
+
+ _activeTextView().set_size_request(std::min(MAX_POPOVER_WIDTH - 10, dlg_width), -1);
+
+ auto theme = get_syntax_theme();
+
+ auto entry = dynamic_cast<Gtk::Entry*>(cell);
+ int width, height;
+ entry->get_layout()->get_pixel_size(width, height);
+ int colwidth = _valueCol->get_width();
+
+ if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] ||
+ edit_in_popup || colwidth - 10 < width)
+ {
+ _value_editing = entry->get_text();
+ Gdk::Rectangle rect;
+ _treeView.get_cell_area((Gtk::TreeModel::Path)iter, *_valueCol, rect);
+ if (_popover->get_position() == Gtk::PositionType::POS_BOTTOM) {
+ rect.set_y(rect.get_y() + 20);
+ }
+ if (rect.get_x() >= dlg_width) {
+ rect.set_x(dlg_width - 1);
+ }
+ _popover->set_pointing_to(rect);
+
+ auto current_value = row[_attrColumns._attributeValue];
+ _current_text_edit->setStyle(theme);
+ _current_text_edit->setText(current_value);
+
+ // close in-line entry
+ cell->property_editing_canceled() = true;
+ cell->remove_widget();
+ // cannot dismiss it right away without warning from GTK, so delay it
+ Glib::signal_timeout().connect_once([=](){
+ cell->editing_done(); // only this call will actually remove in-line edit widget
+ cell->remove_widget();
+ }, 0);
+ // and show popup edit instead
+ Glib::signal_timeout().connect_once([=](){ _popover->popup(); }, 10);
+ } else {
+ entry->signal_key_press_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry));
+ }
+}
+
+void AttrDialog::popClosed()
+{
+ if (!_current_text_edit) {
+ return;
+ }
+ _activeTextView().get_buffer()->set_text("");
+ // delay this resizing, so it is not visible as popover fades out
+ _close_popup = Glib::signal_timeout().connect([=](){ _scrolled_text_view.set_min_content_height(20); return false; }, 250);
+}
+
+/**
+ * @brief AttrDialog::setRepr
+ * Set the internal xml object that I'm working on right now.
+ */
+void AttrDialog::setRepr(Inkscape::XML::Node * repr)
+{
+ if (repr == _repr) {
+ return;
+ }
+ if (_repr) {
+ _store->clear();
+ _repr->removeObserver(*this);
+ Inkscape::GC::release(_repr);
+ _repr = nullptr;
+ }
+ _repr = repr;
+ if (repr) {
+ Inkscape::GC::anchor(_repr);
+ _repr->addObserver(*this);
+
+ // show either attributes or content
+ bool show_content = is_text_or_comment_node(*_repr);
+ if (show_content) {
+ _content_sw.remove();
+ auto type = repr->name();
+ auto elem = repr->parent();
+ if (type && strcmp(type, "string") == 0 && elem && elem->name() && strcmp(elem->name(), "svg:style") == 0) {
+ // editing embedded CSS style
+ _style_edit->setStyle(get_syntax_theme());
+ _content_sw.add(_style_edit->getTextView());
+ } else {
+ _content_sw.add(_text_edit->getTextView());
+ }
+ }
+
+ _repr->synthesizeEvents(*this);
+ _scrolled_window.set_visible(!show_content);
+ _content_sw.set_visible(show_content);
+ }
+}
+
+void AttrDialog::setUndo(Glib::ustring const &event_description)
+{
+ DocumentUndo::done(getDocument(), event_description, INKSCAPE_ICON("dialog-xml-editor"));
+}
+
+/**
+ * Sets the AttrDialog status bar, depending on which attr is selected.
+ */
+void AttrDialog::attr_reset_context(gint attr)
+{
+ if (attr == 0) {
+ _message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> attribute to edit."));
+ } else {
+ const gchar *name = g_quark_to_string(attr);
+ _message_context->setF(
+ Inkscape::NORMAL_MESSAGE,
+ _("Attribute <b>%s</b> selected. Press <b>Ctrl+Enter</b> when done editing to commit changes."), name);
+ }
+}
+
+/**
+ * @brief AttrDialog::notifyAttributeChanged
+ * This is called when the XML has an updated attribute
+ */
+void AttrDialog::notifyAttributeChanged(XML::Node&, GQuark name_, Util::ptr_shared, Util::ptr_shared new_value)
+{
+ if (_updating) {
+ return;
+ }
+
+ auto const name = g_quark_to_string(name_);
+
+ Glib::ustring renderval;
+ if (new_value) {
+ renderval = prepare_rendervalue(new_value.pointer());
+ }
+ for (auto&& iter : _store->children()) {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring col_name = row[_attrColumns._attributeName];
+ if (name == col_name) {
+ if (new_value) {
+ row[_attrColumns._attributeValue] = new_value.pointer();
+ row[_attrColumns._attributeValueRender] = renderval;
+ new_value = Util::ptr_shared(); // Don't make a new one
+ } else {
+ _store->erase(iter);
+ }
+ break;
+ }
+ }
+ if (new_value) {
+ Gtk::TreeModel::Row row = *_store->prepend();
+ row[_attrColumns._attributeName] = name;
+ row[_attrColumns._attributeValue] = new_value.pointer();
+ row[_attrColumns._attributeValueRender] = renderval;
+ }
+}
+
+/**
+ * @brief AttrDialog::onAttrCreate
+ * This function is a slot to signal_clicked for '+' button panel.
+ */
+bool AttrDialog::onAttrCreate(GdkEventButton *event)
+{
+ if(event->type == GDK_BUTTON_RELEASE && event->button == 1 && this->_repr) {
+ Gtk::TreeIter iter = _store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ _treeView.set_cursor(path, *_nameCol, true);
+ grab_focus();
+ return true;
+ }
+ return false;
+}
+
+/**
+ * @brief AttrDialog::onAttrDelete
+ * @param event
+ * @return true
+ * Delete the attribute from the xml
+ */
+void AttrDialog::onAttrDelete(Glib::ustring path)
+{
+ Gtk::TreeModel::Row row = *_store->get_iter(path);
+ if (row) {
+ Glib::ustring name = row[_attrColumns._attributeName];
+ {
+ this->_store->erase(row);
+ this->_repr->removeAttribute(name);
+ this->setUndo(_("Delete attribute"));
+ }
+ }
+}
+
+void AttrDialog::notifyContentChanged(XML::Node &,
+ Util::ptr_shared,
+ Util::ptr_shared new_content)
+{
+ auto textview = dynamic_cast<Gtk::TextView *>(_content_sw.get_child());
+ if (!textview) {
+ return;
+ }
+ auto buffer = textview->get_buffer();
+ if (!buffer->get_modified()) {
+ auto str = new_content.pointer();
+ buffer->set_text(str ? str : "");
+ }
+ buffer->set_modified(false);
+}
+
+
+/**
+ * @brief AttrDialog::onKeyPressed
+ * @param event
+ * @return true
+ * Delete or create elements based on key presses
+ */
+bool AttrDialog::onKeyPressed(GdkEventKey *event)
+{
+ bool ret = false;
+ if (!_repr) {
+ return ret;
+ }
+ auto selection = _treeView.get_selection();
+ auto row = *selection->get_selected();
+
+ switch (event->keyval) {
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete: {
+ // Create new attribute (repeat code, fold into above event!)
+ Glib::ustring name = row[_attrColumns._attributeName];
+ _store->erase(row);
+ _repr->removeAttribute(name);
+ setUndo(_("Delete attribute"));
+ ret = true;
+ } break;
+
+ case GDK_KEY_plus:
+ case GDK_KEY_Insert: {
+ // Create new attribute (repeat code, fold into above event!)
+ Gtk::TreeIter iter = _store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ _treeView.set_cursor(path, *_nameCol, true);
+ grab_focus();
+ ret = true;
+ } break;
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (_popover->is_visible() && (event->state & GDK_SHIFT_MASK)) {
+ valueEditedPop();
+ ret = true;
+ } break;
+ }
+
+ return ret;
+}
+
+bool AttrDialog::onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onNameKeyPressed");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+bool AttrDialog::onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onValueKeyPressed");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (event->state & GDK_SHIFT_MASK) {
+ int pos = entry->get_position();
+ entry->insert_text("\n", 1, pos);
+ entry->set_position(pos + 1);
+ ret = true;
+ }
+ break;
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+void AttrDialog::storeMoveToNext(Gtk::TreeModel::Path modelpath)
+{
+ auto selection = _treeView.get_selection();
+ auto iter = *(selection->get_selected());
+ auto path = static_cast<Gtk::TreeModel::Path>(iter);
+ Gtk::TreeViewColumn *focus_column;
+ _treeView.get_cursor(path, focus_column);
+ if (path == modelpath && focus_column == _treeView.get_column(1)) {
+ _treeView.set_cursor(modelpath, *_valueCol, true);
+ }
+}
+
+/**
+ * Called when the name is edited in the TreeView editable column
+ */
+void AttrDialog::nameEdited (const Glib::ustring& path, const Glib::ustring& name)
+{
+ Gtk::TreeIter iter = *_store->get_iter(path);
+ auto modelpath = static_cast<Gtk::TreeModel::Path>(iter);
+ Gtk::TreeModel::Row row = *iter;
+ if(row && this->_repr) {
+ Glib::ustring old_name = row[_attrColumns._attributeName];
+ if (old_name == name) {
+ Glib::signal_timeout().connect_once([=](){ storeMoveToNext(modelpath); }, 50);
+ grab_focus();
+ return;
+ }
+ // Do not allow empty name (this would delete the attribute)
+ if (name.empty()) {
+ return;
+ }
+ // Do not allow duplicate names
+ const auto children = _store->children();
+ for (const auto &child : children) {
+ if (name == child[_attrColumns._attributeName]) {
+ return;
+ }
+ }
+ if(std::any_of(name.begin(), name.end(), isspace)) {
+ return;
+ }
+ // Copy old value and remove old name
+ Glib::ustring value;
+ if (!old_name.empty()) {
+ value = row[_attrColumns._attributeValue];
+ _updating = true;
+ _repr->removeAttribute(old_name);
+ _updating = false;
+ }
+
+ // Do the actual renaming and set new value
+ row[_attrColumns._attributeName] = name;
+ grab_focus();
+ _updating = true;
+ _repr->setAttributeOrRemoveIfEmpty(name, value); // use char * overload (allows empty attribute values)
+ _updating = false;
+ Glib::signal_timeout().connect_once([=](){ storeMoveToNext(modelpath); }, 50);
+ setUndo(_("Rename attribute"));
+ }
+}
+
+void AttrDialog::valueEditedPop()
+{
+ valueEdited(_value_path, _current_text_edit->getText());
+ _value_editing.clear();
+ _popover->popdown();
+}
+
+/**
+ * @brief AttrDialog::valueEdited
+ * @param event
+ * @return
+ * Called when the value is edited in the TreeView editable column
+ */
+void AttrDialog::valueEdited (const Glib::ustring& path, const Glib::ustring& value)
+{
+ if (!getDesktop()) {
+ return;
+ }
+
+ Gtk::TreeModel::Row row = *_store->get_iter(path);
+ if (row && _repr) {
+ Glib::ustring name = row[_attrColumns._attributeName];
+ Glib::ustring old_value = row[_attrColumns._attributeValue];
+ if (old_value == value || name.empty()) {
+ return;
+ }
+
+ _repr->setAttributeOrRemoveIfEmpty(name, value);
+
+ if (!value.empty()) {
+ row[_attrColumns._attributeValue] = value;
+ Glib::ustring renderval = prepare_rendervalue(value.c_str());
+ row[_attrColumns._attributeValueRender] = renderval;
+ }
+ setUndo(_("Change attribute value"));
+ }
+}
+
+} // namespace Inkscape::UI::Dialog
diff --git a/src/ui/dialog/attrdialog.h b/src/ui/dialog/attrdialog.h
new file mode 100644
index 0000000..e85d42d
--- /dev/null
+++ b/src/ui/dialog/attrdialog.h
@@ -0,0 +1,153 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for XML attributes based on Gtk TreeView
+ */
+/* Authors:
+ * Martin Owens
+ *
+ * Copyright (C) Martin Owens 2018 <doctormo@gmail.com>
+ *
+ * Released under GNU GPLv2 or later, read the file 'COPYING' for more information
+ */
+
+#ifndef SEEN_UI_DIALOGS_ATTRDIALOG_H
+#define SEEN_UI_DIALOGS_ATTRDIALOG_H
+
+#include <gtkmm/builder.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/treeview.h>
+#include <memory>
+
+#include "helper/auto-connection.h"
+#include "inkscape-application.h"
+#include "message.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/syntax.h"
+#include "xml/node-observer.h"
+
+namespace Inkscape {
+class MessageStack;
+class MessageContext;
+namespace UI {
+namespace Dialog {
+
+/**
+ * @brief The AttrDialog class
+ * This dialog allows to add, delete and modify XML attributes created in the
+ * xml editor.
+ */
+class AttrDialog
+ : public DialogBase
+ , private XML::NodeObserver
+{
+public:
+ AttrDialog();
+ ~AttrDialog() override;
+
+ void setRepr(Inkscape::XML::Node * repr);
+ Gtk::ScrolledWindow& get_scrolled_window() { return _scrolled_window; }
+ Gtk::Box& get_status_box() { return _status_box; }
+ void adjust_popup_edit_size();
+ void set_mono_font(bool mono);
+
+private:
+ // builder comes first, so it is initialized before other data members
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ // Data structure
+ class AttrColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ AttrColumns()
+ {
+ add(_attributeName);
+ add(_attributeValue);
+ add(_attributeValueRender);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> _attributeName;
+ Gtk::TreeModelColumn<Glib::ustring> _attributeValue;
+ Gtk::TreeModelColumn<Glib::ustring> _attributeValueRender;
+ };
+ AttrColumns _attrColumns;
+
+ // TreeView
+ Gtk::TreeView& _treeView;
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Gtk::CellRendererText *_nameRenderer;
+ Gtk::CellRendererText *_valueRenderer;
+ Gtk::TreeViewColumn *_nameCol;
+ Gtk::TreeViewColumn *_valueCol;
+ Gtk::Popover *_popover;
+ Glib::ustring _value_path;
+ Glib::ustring _value_editing;
+ // Status bar
+ std::shared_ptr<Inkscape::MessageStack> _message_stack;
+ std::unique_ptr<Inkscape::MessageContext> _message_context;
+ // Widgets
+ Gtk::ScrolledWindow& _scrolled_window;
+ Gtk::ScrolledWindow& _scrolled_text_view;
+ // Variables - Inkscape
+ Inkscape::XML::Node* _repr{nullptr};
+ Gtk::Box& _status_box;
+ Gtk::Label& _status;
+ bool _updating = true;
+
+ // Helper functions
+ void setUndo(Glib::ustring const &event_description);
+ /**
+ * Sets the XML status bar, depending on which attr is selected.
+ */
+ void attr_reset_context(gint attr);
+
+ /**
+ * Signal handlers
+ */
+ auto_connection _message_changed_connection;
+ bool onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
+ bool onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
+ void onAttrDelete(Glib::ustring path);
+ bool onAttrCreate(GdkEventButton *event);
+ bool onKeyPressed(GdkEventKey *event);
+ void truncateDigits() const;
+ void popClosed();
+ void startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path);
+ void startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path);
+ void nameEdited(const Glib::ustring &path, const Glib::ustring &name);
+ void valueEdited(const Glib::ustring &path, const Glib::ustring &value);
+ void valueEditedPop();
+ void storeMoveToNext(Gtk::TreeModel::Path modelpath);
+
+private:
+ // Text/comment nodes
+ Gtk::ScrolledWindow& _content_sw;
+ std::unique_ptr<Syntax::TextEditView> _text_edit; // text content editing (plain text)
+ std::unique_ptr<Syntax::TextEditView> _style_edit; // embedded CSS style (with syntax coloring)
+
+ // Attribute value editing
+ std::unique_ptr<Syntax::TextEditView> _css_edit; // in-line CSS style
+ std::unique_ptr<Syntax::TextEditView> _svgd_edit; // SVG path data
+ std::unique_ptr<Syntax::TextEditView> _points_edit; // points in a <polygon> or <polyline>
+ std::unique_ptr<Syntax::TextEditView> _attr_edit; // all other attributes (plain text)
+ Syntax::TextEditView* _current_text_edit = nullptr; // current text edit for attribute value editing
+ auto_connection _adjust_size;
+ auto_connection _close_popup;
+ int _rounding_precision = 0;
+
+ bool key_callback(GdkEventKey* event);
+ void notifyAttributeChanged(XML::Node &repr, GQuark name, Util::ptr_shared old_value, Util::ptr_shared new_value) final;
+ void notifyContentChanged(XML::Node &node, Util::ptr_shared old_content, Util::ptr_shared new_content) final;
+ static Glib::ustring round_numbers(const Glib::ustring& text, int precision);
+ Gtk::TextView &_activeTextView() const;
+ void set_current_textedit(Syntax::TextEditView* edit);
+ static std::unique_ptr<Syntax::TextEditView> init_text_view(AttrDialog* owner, Syntax::SyntaxMode coloring, bool map);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_UI_DIALOGS_ATTRDIALOG_H
diff --git a/src/ui/dialog/calligraphic-profile-rename.cpp b/src/ui/dialog/calligraphic-profile-rename.cpp
new file mode 100644
index 0000000..604ac7f
--- /dev/null
+++ b/src/ui/dialog/calligraphic-profile-rename.cpp
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for naming calligraphic profiles.
+ *
+ * @note This file is in the wrong directory because of link order issues -
+ * it is required by widgets/toolbox.cpp, and libspwidgets.a comes after
+ * libinkdialogs.a in the current link order.
+ */
+/* Author:
+ * Aubanel MONNIER
+ *
+ * Copyright (C) 2007 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "calligraphic-profile-rename.h"
+#include <glibmm/i18n.h>
+#include <gtkmm/grid.h>
+
+#include "desktop.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+CalligraphicProfileRename::CalligraphicProfileRename() :
+ _layout_table(Gtk::manage(new Gtk::Grid())),
+ _applied(false)
+{
+ set_title(_("Edit profile"));
+
+ auto mainVBox = get_content_area();
+ _layout_table->set_column_spacing(4);
+ _layout_table->set_row_spacing(4);
+
+ _profile_name_entry.set_activates_default(true);
+
+ _profile_name_label.set_label(_("Profile name:"));
+ _profile_name_label.set_halign(Gtk::ALIGN_END);
+ _profile_name_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _layout_table->attach(_profile_name_label, 0, 0, 1, 1);
+
+ _profile_name_entry.set_hexpand();
+ _layout_table->attach(_profile_name_entry, 1, 0, 1, 1);
+
+ mainVBox->pack_start(*_layout_table, false, false, 4);
+ // Buttons
+ _close_button.set_use_underline();
+ _close_button.set_label(_("_Cancel"));
+ _close_button.set_can_default();
+
+ _delete_button.set_use_underline(true);
+ _delete_button.set_label(_("_Delete"));
+ _delete_button.set_can_default();
+ _delete_button.set_visible(false);
+
+ _apply_button.set_use_underline(true);
+ _apply_button.set_label(_("_Save"));
+ _apply_button.set_can_default();
+
+ _close_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_close));
+ _delete_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_delete));
+ _apply_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_apply));
+
+ signal_delete_event().connect( sigc::bind_return(
+ sigc::hide(sigc::mem_fun(*this, &CalligraphicProfileRename::_close)), true ) );
+
+ add_action_widget(_close_button, Gtk::RESPONSE_CLOSE);
+ add_action_widget(_delete_button, Gtk::RESPONSE_DELETE_EVENT);
+ add_action_widget(_apply_button, Gtk::RESPONSE_APPLY);
+
+ _apply_button.grab_default();
+
+ show_all_children();
+}
+
+void CalligraphicProfileRename::_apply()
+{
+ _profile_name = _profile_name_entry.get_text();
+ _applied = true;
+ _deleted = false;
+ _close();
+}
+
+void CalligraphicProfileRename::_delete()
+{
+ _profile_name = _profile_name_entry.get_text();
+ _applied = true;
+ _deleted = true;
+ _close();
+}
+
+void CalligraphicProfileRename::_close()
+{
+ this->Gtk::Dialog::hide();
+}
+
+void CalligraphicProfileRename::show(SPDesktop *desktop, const Glib::ustring profile_name)
+{
+ CalligraphicProfileRename &dial = instance();
+ dial._applied=false;
+ dial._deleted=false;
+ dial.set_modal(true);
+
+ dial._profile_name = profile_name;
+ dial._profile_name_entry.set_text(profile_name);
+
+ if (profile_name.empty()) {
+ dial.set_title(_("Add profile"));
+ dial._delete_button.set_visible(false);
+
+ } else {
+ dial.set_title(_("Edit profile"));
+ dial._delete_button.set_visible(true);
+ }
+
+ desktop->setWindowTransient (dial.gobj());
+ dial.property_destroy_with_parent() = true;
+ // dial.Gtk::Dialog::show();
+ //dial.present();
+ dial.run();
+}
+
+} // 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/dialog/calligraphic-profile-rename.h b/src/ui/dialog/calligraphic-profile-rename.h
new file mode 100644
index 0000000..195275b
--- /dev/null
+++ b/src/ui/dialog/calligraphic-profile-rename.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Dialog for naming calligraphic profiles
+ */
+/* Author:
+ * Aubanel MONNIER
+ *
+ * Copyright (C) 2007 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H
+#define INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H
+
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/label.h>
+
+namespace Gtk {
+class Grid;
+}
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class CalligraphicProfileRename : public Gtk::Dialog {
+public:
+ CalligraphicProfileRename();
+ ~CalligraphicProfileRename() override = default;
+ Glib::ustring getName() const {
+ return "CalligraphicProfileRename";
+ }
+
+ static void show(SPDesktop *desktop, const Glib::ustring profile_name);
+ static bool applied() {
+ return instance()._applied;
+ }
+ static bool deleted() {
+ return instance()._deleted;
+ }
+ static Glib::ustring getProfileName() {
+ return instance()._profile_name;
+ }
+
+protected:
+ void _close();
+ void _apply();
+ void _delete();
+
+ Gtk::Label _profile_name_label;
+ Gtk::Entry _profile_name_entry;
+ Gtk::Grid* _layout_table;
+
+ Gtk::Button _close_button;
+ Gtk::Button _delete_button;
+ Gtk::Button _apply_button;
+ Glib::ustring _profile_name;
+ bool _applied;
+ bool _deleted;
+private:
+ static CalligraphicProfileRename &instance() {
+ static CalligraphicProfileRename instance_;
+ return instance_;
+ }
+ CalligraphicProfileRename(CalligraphicProfileRename const &) = delete; // no copy
+ CalligraphicProfileRename &operator=(CalligraphicProfileRename const &) = delete; // no assign
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_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/dialog/clonetiler.cpp b/src/ui/dialog/clonetiler.cpp
new file mode 100644
index 0000000..79e272e
--- /dev/null
+++ b/src/ui/dialog/clonetiler.cpp
@@ -0,0 +1,2821 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/**
+ * @file
+ * Clone tiling dialog
+ */
+/* Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Romain de Bossoreille
+ *
+ * Copyright (C) 2004-2011 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "clonetiler.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/adjustment.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/combobox.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/sizegroup.h>
+
+#include <2geom/transforms.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "message-stack.h"
+
+#include "display/cairo-utils.h"
+#include "display/drawing-context.h"
+#include "display/drawing.h"
+
+#include "ui/icon-loader.h"
+
+#include "object/algorithms/unclump.h"
+
+#include "object/sp-item.h"
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+#include "object/sp-use.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/unit-menu.h"
+
+#include "svg/svg-color.h"
+#include "svg/svg.h"
+#include "xml/href-attribute-helper.h"
+
+using Inkscape::DocumentUndo;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+/**
+ * Simple extension of Gtk::CheckButton, which adds a flag
+ * to indicate whether the box should be unticked when reset
+ */
+class CheckButtonInternal : public Gtk::CheckButton {
+ private:
+ bool _uncheckable = false;
+ public:
+ CheckButtonInternal() = default;
+
+ CheckButtonInternal(const Glib::ustring &label)
+ : Gtk::CheckButton(label)
+ {}
+
+ void set_uncheckable(const bool val = true) { _uncheckable = val; }
+ bool get_uncheckable() const { return _uncheckable; }
+};
+}
+
+namespace Dialog {
+
+#define SB_MARGIN 1
+#define VB_MARGIN 4
+
+static Glib::ustring const prefs_path = "/dialogs/clonetiler/";
+
+static Inkscape::Drawing *trace_drawing = nullptr;
+static unsigned trace_visionkey;
+static gdouble trace_zoom;
+static SPDocument *trace_doc = nullptr;
+
+CloneTiler::CloneTiler()
+ : DialogBase("/dialogs/clonetiler/", "CloneTiler")
+ , table_row_labels(nullptr)
+{
+ set_spacing(0);
+
+ {
+ auto prefs = Inkscape::Preferences::get();
+
+ auto mainbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ mainbox->set_homogeneous(false);
+ mainbox->set_border_width(6);
+
+ pack_start(*mainbox, true, true, 0);
+
+ nb = Gtk::manage(new Gtk::Notebook());
+ mainbox->pack_start(*nb, false, false, 0);
+
+
+ // Symmetry
+ {
+ auto vb = new_tab(nb, _("_Symmetry"));
+
+ /* TRANSLATORS: For the following 17 symmetry groups, see
+ * http://www.bib.ulb.ac.be/coursmath/doc/17.htm (visual examples);
+ * http://www.clarku.edu/~djoyce/wallpaper/seventeen.html (English vocabulary); or
+ * http://membres.lycos.fr/villemingerard/Geometri/Sym1D.htm (French vocabulary).
+ */
+ struct SymGroups {
+ gint group;
+ Glib::ustring label;
+ } const sym_groups[] = {
+ // TRANSLATORS: "translation" means "shift" / "displacement" here.
+ {TILE_P1, _("<b>P1</b>: simple translation")},
+ {TILE_P2, _("<b>P2</b>: 180&#176; rotation")},
+ {TILE_PM, _("<b>PM</b>: reflection")},
+ // TRANSLATORS: "glide reflection" is a reflection and a translation combined.
+ // For more info, see http://mathforum.org/sum95/suzanne/symsusan.html
+ {TILE_PG, _("<b>PG</b>: glide reflection")},
+ {TILE_CM, _("<b>CM</b>: reflection + glide reflection")},
+ {TILE_PMM, _("<b>PMM</b>: reflection + reflection")},
+ {TILE_PMG, _("<b>PMG</b>: reflection + 180&#176; rotation")},
+ {TILE_PGG, _("<b>PGG</b>: glide reflection + 180&#176; rotation")},
+ {TILE_CMM, _("<b>CMM</b>: reflection + reflection + 180&#176; rotation")},
+ {TILE_P4, _("<b>P4</b>: 90&#176; rotation")},
+ {TILE_P4M, _("<b>P4M</b>: 90&#176; rotation + 45&#176; reflection")},
+ {TILE_P4G, _("<b>P4G</b>: 90&#176; rotation + 90&#176; reflection")},
+ {TILE_P3, _("<b>P3</b>: 120&#176; rotation")},
+ {TILE_P31M, _("<b>P31M</b>: reflection + 120&#176; rotation, dense")},
+ {TILE_P3M1, _("<b>P3M1</b>: reflection + 120&#176; rotation, sparse")},
+ {TILE_P6, _("<b>P6</b>: 60&#176; rotation")},
+ {TILE_P6M, _("<b>P6M</b>: reflection + 60&#176; rotation")},
+ };
+
+ gint current = prefs->getInt(prefs_path + "symmetrygroup", 0);
+
+ // Add a new combo box widget with the list of symmetry groups to the vbox
+ auto combo = Gtk::manage(new Gtk::ComboBoxText());
+ combo->set_tooltip_text(_("Select one of the 17 symmetry groups for the tiling"));
+
+ // Hack to add markup support
+ auto cell_list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(combo->gobj()));
+ gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo->gobj()),
+ GTK_CELL_RENDERER(cell_list->data),
+ "markup", 0, nullptr);
+
+ for (const auto & sg : sym_groups) {
+ // Add the description of the symgroup to a new row
+ combo->append(sg.label);
+ }
+
+ vb->pack_start(*combo, false, false, SB_MARGIN);
+
+ combo->set_active(current);
+ combo->signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::symgroup_changed), combo));
+ }
+
+ table_row_labels = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+
+ // Shift
+ {
+ auto vb = new_tab(nb, _("S_hift"));
+
+ auto table = table_x_y_rand (3);
+ vb->pack_start(*table, false, false, 0);
+
+ // X
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "shift" means: the tiles will be shifted (offset) horizontally by this amount
+ // xgettext:no-c-format
+ l->set_markup(_("<b>Shift X:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 2, 1);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Horizontal shift per row (in % of tile width)"), "shiftx_per_j",
+ -10000, 10000, "%");
+ table_attach (table, l, 0, 2, 2);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Horizontal shift per column (in % of tile width)"), "shiftx_per_i",
+ -10000, 10000, "%");
+ table_attach (table, l, 0, 2, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the horizontal shift by this percentage"), "shiftx_rand",
+ 0, 1000, "%");
+ table_attach (table, l, 0, 2, 4);
+ }
+
+ // Y
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "shift" means: the tiles will be shifted (offset) vertically by this amount
+ // xgettext:no-c-format
+ l->set_markup(_("<b>Shift Y:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 3, 1);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Vertical shift per row (in % of tile height)"), "shifty_per_j",
+ -10000, 10000, "%");
+ table_attach (table, l, 0, 3, 2);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Vertical shift per column (in % of tile height)"), "shifty_per_i",
+ -10000, 10000, "%");
+ table_attach (table, l, 0, 3, 3);
+ }
+
+ {
+ auto l = spinbox (
+ _("Randomize the vertical shift by this percentage"), "shifty_rand",
+ 0, 1000, "%");
+ table_attach (table, l, 0, 3, 4);
+ }
+
+ // Exponent
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Exponent:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 4, 1);
+ }
+
+ {
+ auto l = spinbox (
+ _("Whether rows are spaced evenly (1), converge (<1) or diverge (>1)"), "shifty_exp",
+ 0, 10, "", true);
+ table_attach (table, l, 0, 4, 2);
+ }
+
+ {
+ auto l = spinbox (
+ _("Whether columns are spaced evenly (1), converge (<1) or diverge (>1)"), "shiftx_exp",
+ 0, 10, "", true);
+ table_attach (table, l, 0, 4, 3);
+ }
+
+ { // alternates
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 5, 1);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of shifts for each row"), "shifty_alternate");
+ table_attach (table, l, 0, 5, 2);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of shifts for each column"), "shiftx_alternate");
+ table_attach (table, l, 0, 5, 3);
+ }
+
+ { // Cumulate
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Cumulate" is a verb here
+ l->set_markup(_("<small>Cumulate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 6, 1);
+ }
+
+ {
+ auto l = checkbox (_("Cumulate the shifts for each row"), "shifty_cumulate");
+ table_attach (table, l, 0, 6, 2);
+ }
+
+ {
+ auto l = checkbox (_("Cumulate the shifts for each column"), "shiftx_cumulate");
+ table_attach (table, l, 0, 6, 3);
+ }
+
+ { // Exclude tile width and height in shift
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Cumulate" is a verb here
+ l->set_markup(_("<small>Exclude tile:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 7, 1);
+ }
+
+ {
+ auto l = checkbox (_("Exclude tile height in shift"), "shifty_excludeh");
+ table_attach (table, l, 0, 7, 2);
+ }
+
+ {
+ auto l = checkbox (_("Exclude tile width in shift"), "shiftx_excludew");
+ table_attach (table, l, 0, 7, 3);
+ }
+
+ }
+
+
+ // Scale
+ {
+ auto vb = new_tab(nb, _("Sc_ale"));
+
+ auto table = table_x_y_rand(2);
+ vb->pack_start(*table, false, false, 0);
+
+ // X
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Scale X:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 2, 1);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Horizontal scale per row (in % of tile width)"), "scalex_per_j",
+ -100, 1000, "%");
+ table_attach (table, l, 0, 2, 2);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Horizontal scale per column (in % of tile width)"), "scalex_per_i",
+ -100, 1000, "%");
+ table_attach (table, l, 0, 2, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the horizontal scale by this percentage"), "scalex_rand",
+ 0, 1000, "%");
+ table_attach (table, l, 0, 2, 4);
+ }
+
+ // Y
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Scale Y:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 3, 1);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Vertical scale per row (in % of tile height)"), "scaley_per_j",
+ -100, 1000, "%");
+ table_attach (table, l, 0, 3, 2);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Vertical scale per column (in % of tile height)"), "scaley_per_i",
+ -100, 1000, "%");
+ table_attach (table, l, 0, 3, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the vertical scale by this percentage"), "scaley_rand",
+ 0, 1000, "%");
+ table_attach (table, l, 0, 3, 4);
+ }
+
+ // Exponent
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Exponent:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 4, 1);
+ }
+
+ {
+ auto l = spinbox (_("Whether row scaling is uniform (1), converge (<1) or diverge (>1)"), "scaley_exp",
+ 0, 10, "", true);
+ table_attach (table, l, 0, 4, 2);
+ }
+
+ {
+ auto l = spinbox (_("Whether column scaling is uniform (1), converge (<1) or diverge (>1)"), "scalex_exp",
+ 0, 10, "", true);
+ table_attach (table, l, 0, 4, 3);
+ }
+
+ // Logarithmic (as in logarithmic spiral)
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Base:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 5, 1);
+ }
+
+ {
+ auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scaley_log",
+ 0, 10, "", false);
+ table_attach (table, l, 0, 5, 2);
+ }
+
+ {
+ auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scalex_log",
+ 0, 10, "", false);
+ table_attach (table, l, 0, 5, 3);
+ }
+
+ { // alternates
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 6, 1);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of scales for each row"), "scaley_alternate");
+ table_attach (table, l, 0, 6, 2);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of scales for each column"), "scalex_alternate");
+ table_attach (table, l, 0, 6, 3);
+ }
+
+ { // Cumulate
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Cumulate" is a verb here
+ l->set_markup(_("<small>Cumulate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 7, 1);
+ }
+
+ {
+ auto l = checkbox (_("Cumulate the scales for each row"), "scaley_cumulate");
+ table_attach (table, l, 0, 7, 2);
+ }
+
+ {
+ auto l = checkbox (_("Cumulate the scales for each column"), "scalex_cumulate");
+ table_attach (table, l, 0, 7, 3);
+ }
+
+ }
+
+
+ // Rotation
+ {
+ auto vb = new_tab(nb, _("_Rotation"));
+
+ auto table = table_x_y_rand (1);
+ vb->pack_start(*table, false, false, 0);
+
+ // Angle
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Angle:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 2, 1);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Rotate tiles by this angle for each row"), "rotate_per_j",
+ -180, 180, "&#176;");
+ table_attach (table, l, 0, 2, 2);
+ }
+
+ {
+ auto l = spinbox (
+ // xgettext:no-c-format
+ _("Rotate tiles by this angle for each column"), "rotate_per_i",
+ -180, 180, "&#176;");
+ table_attach (table, l, 0, 2, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the rotation angle by this percentage"), "rotate_rand",
+ 0, 100, "%");
+ table_attach (table, l, 0, 2, 4);
+ }
+
+ { // alternates
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 3, 1);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the rotation direction for each row"), "rotate_alternatej");
+ table_attach (table, l, 0, 3, 2);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the rotation direction for each column"), "rotate_alternatei");
+ table_attach (table, l, 0, 3, 3);
+ }
+
+ { // Cumulate
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Cumulate" is a verb here
+ l->set_markup(_("<small>Cumulate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 4, 1);
+ }
+
+ {
+ auto l = checkbox (_("Cumulate the rotation for each row"), "rotate_cumulatej");
+ table_attach (table, l, 0, 4, 2);
+ }
+
+ {
+ auto l = checkbox (_("Cumulate the rotation for each column"), "rotate_cumulatei");
+ table_attach (table, l, 0, 4, 3);
+ }
+
+ }
+
+
+ // Blur and opacity
+ {
+ auto vb = new_tab(nb, _("_Blur & opacity"));
+
+ auto table = table_x_y_rand(1);
+ vb->pack_start(*table, false, false, 0);
+
+
+ // Blur
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Blur:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 2, 1);
+ }
+
+ {
+ auto l = spinbox (_("Blur tiles by this percentage for each row"), "blur_per_j",
+ 0, 100, "%");
+ table_attach (table, l, 0, 2, 2);
+ }
+
+ {
+ auto l = spinbox (_("Blur tiles by this percentage for each column"), "blur_per_i",
+ 0, 100, "%");
+ table_attach (table, l, 0, 2, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the tile blur by this percentage"), "blur_rand",
+ 0, 100, "%");
+ table_attach (table, l, 0, 2, 4);
+ }
+
+ { // alternates
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 3, 1);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of blur change for each row"), "blur_alternatej");
+ table_attach (table, l, 0, 3, 2);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of blur change for each column"), "blur_alternatei");
+ table_attach (table, l, 0, 3, 3);
+ }
+
+
+
+ // Dissolve
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>Opacity:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 4, 1);
+ }
+
+ {
+ auto l = spinbox (_("Decrease tile opacity by this percentage for each row"), "opacity_per_j",
+ 0, 100, "%");
+ table_attach (table, l, 0, 4, 2);
+ }
+
+ {
+ auto l = spinbox (_("Decrease tile opacity by this percentage for each column"), "opacity_per_i",
+ 0, 100, "%");
+ table_attach (table, l, 0, 4, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the tile opacity by this percentage"), "opacity_rand",
+ 0, 100, "%");
+ table_attach (table, l, 0, 4, 4);
+ }
+
+ { // alternates
+ auto l = Gtk::manage(new Gtk::Label(""));
+ // TRANSLATORS: "Alternate" is a verb here
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 5, 1);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of opacity change for each row"), "opacity_alternatej");
+ table_attach (table, l, 0, 5, 2);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of opacity change for each column"), "opacity_alternatei");
+ table_attach (table, l, 0, 5, 3);
+ }
+ }
+
+
+ // Color
+ {
+ auto vb = new_tab(nb, _("Co_lor"));
+
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb->set_homogeneous(false);
+
+ auto l = Gtk::manage(new Gtk::Label(_("Initial color: ")));
+ hb->pack_start(*l, false, false, 0);
+
+ guint32 rgba = 0x000000ff | sp_svg_read_color (prefs->getString(prefs_path + "initial_color").data(), 0x000000ff);
+ color_picker = new Inkscape::UI::Widget::ColorPicker (*new Glib::ustring(_("Initial color of tiled clones")), *new Glib::ustring(_("Initial color for clones (works only if the original has unset fill or stroke or on spray tool in copy mode)")), rgba, false);
+ color_changed_connection = color_picker->connectChanged(sigc::mem_fun(*this, &CloneTiler::on_picker_color_changed));
+
+ hb->pack_start(*color_picker, false, false, 0);
+
+ vb->pack_start(*hb, false, false, 0);
+ }
+
+
+ auto table = table_x_y_rand(3);
+ vb->pack_start(*table, false, false, 0);
+
+ // Hue
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>H:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 2, 1);
+ }
+
+ {
+ auto l = spinbox (_("Change the tile hue by this percentage for each row"), "hue_per_j",
+ -100, 100, "%");
+ table_attach (table, l, 0, 2, 2);
+ }
+
+ {
+ auto l = spinbox (_("Change the tile hue by this percentage for each column"), "hue_per_i",
+ -100, 100, "%");
+ table_attach (table, l, 0, 2, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the tile hue by this percentage"), "hue_rand",
+ 0, 100, "%");
+ table_attach (table, l, 0, 2, 4);
+ }
+
+
+ // Saturation
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>S:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 3, 1);
+ }
+
+ {
+ auto l = spinbox (_("Change the color saturation by this percentage for each row"), "saturation_per_j",
+ -100, 100, "%");
+ table_attach (table, l, 0, 3, 2);
+ }
+
+ {
+ auto l = spinbox (_("Change the color saturation by this percentage for each column"), "saturation_per_i",
+ -100, 100, "%");
+ table_attach (table, l, 0, 3, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the color saturation by this percentage"), "saturation_rand",
+ 0, 100, "%");
+ table_attach (table, l, 0, 3, 4);
+ }
+
+ // Lightness
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<b>L:</b>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 4, 1);
+ }
+
+ {
+ auto l = spinbox (_("Change the color lightness by this percentage for each row"), "lightness_per_j",
+ -100, 100, "%");
+ table_attach (table, l, 0, 4, 2);
+ }
+
+ {
+ auto l = spinbox (_("Change the color lightness by this percentage for each column"), "lightness_per_i",
+ -100, 100, "%");
+ table_attach (table, l, 0, 4, 3);
+ }
+
+ {
+ auto l = spinbox (_("Randomize the color lightness by this percentage"), "lightness_rand",
+ 0, 100, "%");
+ table_attach (table, l, 0, 4, 4);
+ }
+
+
+ { // alternates
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<small>Alternate:</small>"));
+ l->set_xalign(0.0);
+ table_row_labels->add_widget(*l);
+ table_attach (table, l, 1, 5, 1);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of color changes for each row"), "color_alternatej");
+ table_attach (table, l, 0, 5, 2);
+ }
+
+ {
+ auto l = checkbox (_("Alternate the sign of color changes for each column"), "color_alternatei");
+ table_attach (table, l, 0, 5, 3);
+ }
+
+ }
+
+ // Trace
+ {
+ auto vb = new_tab(nb, _("_Trace"));
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ hb->set_border_width(4);
+ hb->set_homogeneous(false);
+ vb->pack_start(*hb, false, false, 0);
+
+ _b = Gtk::manage(new UI::Widget::CheckButtonInternal(_("Trace the drawing under the clones/sprayed items")));
+ _b->set_uncheckable();
+ bool old = prefs->getBool(prefs_path + "dotrace");
+ _b->set_active(old);
+ _b->set_tooltip_text(_("For each clone/sprayed item, pick a value from the drawing in its location and apply it"));
+ hb->pack_start(*_b, false, false, 0);
+ _b->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::do_pick_toggled));
+ }
+
+ {
+ auto vvb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0));
+ vvb->set_homogeneous(false);
+ vb->pack_start(*vvb, false, false, 0);
+ _dotrace = vvb;
+
+ {
+ auto frame = Gtk::manage(new Gtk::Frame(_("1. Pick from the drawing:")));
+ frame->set_shadow_type(Gtk::SHADOW_NONE);
+ vvb->pack_start(*frame, false, false, 0);
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(4);
+ table->set_column_spacing(6);
+ table->set_border_width(4);
+ frame->add(*table);
+
+ Gtk::RadioButtonGroup rb_group;
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Color")));
+ radio->set_tooltip_text(_("Pick the visible color and opacity"));
+ table_attach(table, radio, 0.0, 1, 1);
+ radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_COLOR));
+ radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_COLOR);
+ }
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Opacity")));
+ radio->set_tooltip_text(_("Pick the total accumulated opacity"));
+ table_attach (table, radio, 0.0, 2, 1);
+ radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_OPACITY));
+ radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_OPACITY);
+ }
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("R")));
+ radio->set_tooltip_text(_("Pick the Red component of the color"));
+ table_attach (table, radio, 0.0, 1, 2);
+ radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_R));
+ radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_R);
+ }
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("G")));
+ radio->set_tooltip_text(_("Pick the Green component of the color"));
+ table_attach (table, radio, 0.0, 2, 2);
+ radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_G));
+ radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_G);
+ }
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("B")));
+ radio->set_tooltip_text(_("Pick the Blue component of the color"));
+ table_attach (table, radio, 0.0, 3, 2);
+ radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_B));
+ radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_B);
+ }
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color hue", "H")));
+ radio->set_tooltip_text(_("Pick the hue of the color"));
+ table_attach (table, radio, 0.0, 1, 3);
+ radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_H));
+ radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_H);
+ }
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color saturation", "S")));
+ radio->set_tooltip_text(_("Pick the saturation of the color"));
+ table_attach (table, radio, 0.0, 2, 3);
+ radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_S));
+ radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_S);
+ }
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color lightness", "L")));
+ radio->set_tooltip_text(_("Pick the lightness of the color"));
+ table_attach (table, radio, 0.0, 3, 3);
+ radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_L));
+ radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_L);
+ }
+
+ }
+
+ {
+ auto frame = Gtk::manage(new Gtk::Frame(_("2. Tweak the picked value:")));
+ frame->set_shadow_type(Gtk::SHADOW_NONE);
+ vvb->pack_start(*frame, false, false, VB_MARGIN);
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(4);
+ table->set_column_spacing(6);
+ table->set_border_width(4);
+ frame->add(*table);
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("Gamma-correct:"));
+ table_attach (table, l, 1.0, 1, 1);
+ }
+ {
+ auto l = spinbox (_("Shift the mid-range of the picked value upwards (>0) or downwards (<0)"), "gamma_picked",
+ -10, 10, "");
+ table_attach (table, l, 0.0, 1, 2);
+ }
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("Randomize:"));
+ table_attach (table, l, 1.0, 1, 3);
+ }
+ {
+ auto l = spinbox (_("Randomize the picked value by this percentage"), "rand_picked",
+ 0, 100, "%");
+ table_attach (table, l, 0.0, 1, 4);
+ }
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("Invert:"));
+ table_attach (table, l, 1.0, 2, 1);
+ }
+ {
+ auto l = checkbox (_("Invert the picked value"), "invert_picked");
+ table_attach (table, l, 0.0, 2, 2);
+ }
+ }
+
+ {
+ auto frame = Gtk::manage(new Gtk::Frame(_("3. Apply the value to the clones':")));
+ frame->set_shadow_type(Gtk::SHADOW_NONE);
+ vvb->pack_start(*frame, false, false, 0);
+
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(4);
+ table->set_column_spacing(6);
+ table->set_border_width(4);
+ frame->add(*table);
+
+ {
+ auto b = Gtk::manage(new Gtk::CheckButton(_("Presence")));
+ bool old = prefs->getBool(prefs_path + "pick_to_presence", true);
+ b->set_active(old);
+ b->set_tooltip_text(_("Each clone is created with the probability determined by the picked value in that point"));
+ table_attach (table, b, 0.0, 1, 1);
+ b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_presence"));
+ }
+
+ {
+ auto b = Gtk::manage(new Gtk::CheckButton(_("Size")));
+ bool old = prefs->getBool(prefs_path + "pick_to_size");
+ b->set_active(old);
+ b->set_tooltip_text(_("Each clone's size is determined by the picked value in that point"));
+ table_attach (table, b, 0.0, 2, 1);
+ b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_size"));
+ }
+
+ {
+ auto b = Gtk::manage(new Gtk::CheckButton(_("Color")));
+ bool old = prefs->getBool(prefs_path + "pick_to_color", false);
+ b->set_active(old);
+ b->set_tooltip_text(_("Each clone is painted by the picked color (the original must have unset fill or stroke)"));
+ table_attach (table, b, 0.0, 1, 2);
+ b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_color"));
+ }
+
+ {
+ auto b = Gtk::manage(new Gtk::CheckButton(_("Opacity")));
+ bool old = prefs->getBool(prefs_path + "pick_to_opacity", false);
+ b->set_active(old);
+ b->set_tooltip_text(_("Each clone's opacity is determined by the picked value in that point"));
+ table_attach (table, b, 0.0, 2, 2);
+ b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_opacity"));
+ }
+ }
+ vvb->set_sensitive(prefs->getBool(prefs_path + "dotrace"));
+ }
+ }
+
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ hb->set_homogeneous(false);
+ mainbox->pack_start(*hb, false, false, 0);
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("Apply to tiled clones:"));
+ hb->pack_start(*l, false, false, 0);
+ }
+ // Rows/columns, width/height
+ {
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(4);
+ table->set_column_spacing(6);
+
+ table->set_border_width(VB_MARGIN);
+ mainbox->pack_start(*table, false, false, 0);
+
+ {
+ {
+ auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0);
+ int value = prefs->getInt(prefs_path + "jmax", 2);
+ a->set_value (value);
+
+ auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0);
+ sb->set_tooltip_text (_("How many rows in the tiling"));
+ sb->set_width_chars (7);
+ sb->set_name("row");
+ table_attach(table, sb, 0.0f, 1, 2);
+ _rowscols.push_back(sb);
+
+ a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "jmax"));
+ }
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup("&#215;");
+ table_attach(table, l, 0.0f, 1, 3);
+ _rowscols.push_back(l);
+ }
+
+ {
+ auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0);
+ int value = prefs->getInt(prefs_path + "imax", 2);
+ a->set_value (value);
+
+ auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0);
+ sb->set_tooltip_text (_("How many columns in the tiling"));
+ sb->set_width_chars (7);
+ table_attach(table, sb, 0.0f, 1, 4);
+ _rowscols.push_back(sb);
+
+ a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "imax"));
+ }
+ }
+
+ {
+ // unitmenu
+ unit_menu = new Inkscape::UI::Widget::UnitMenu();
+ unit_menu->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR);
+ unit_menu->setUnit(SP_ACTIVE_DESKTOP->getNamedView()->display_units->abbr);
+ unitChangedConn = unit_menu->signal_changed().connect(sigc::mem_fun(*this, &CloneTiler::unit_changed));
+
+ {
+ // Width spinbutton
+ fill_width = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0);
+
+ double value = prefs->getDouble(prefs_path + "fillwidth", 50.0);
+ Inkscape::Util::Unit const *unit = unit_menu->getUnit();
+ gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit);
+ fill_width->set_value (units);
+
+ auto e = new Inkscape::UI::Widget::SpinButton(fill_width, 1.0, 2);
+ e->set_tooltip_text (_("Width of the rectangle to be filled"));
+ e->set_width_chars (7);
+ e->set_digits (4);
+ table_attach(table, e, 0.0f, 2, 2);
+ _widthheight.push_back(e);
+ fill_width->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_width_changed));
+ }
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup("&#215;");
+ table_attach(table, l, 0.0f, 2, 3);
+ _widthheight.push_back(l);
+ }
+
+ {
+ // Height spinbutton
+ fill_height = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0);
+
+ double value = prefs->getDouble(prefs_path + "fillheight", 50.0);
+ Inkscape::Util::Unit const *unit = unit_menu->getUnit();
+ gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit);
+ fill_height->set_value (units);
+
+ auto e = new Inkscape::UI::Widget::SpinButton(fill_height, 1.0, 2);
+ e->set_tooltip_text (_("Height of the rectangle to be filled"));
+ e->set_width_chars (7);
+ e->set_digits (4);
+ table_attach(table, e, 0.0f, 2, 4);
+ _widthheight.push_back(e);
+ fill_height->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_height_changed));
+ }
+
+ table_attach(table, unit_menu, 0.0f, 2, 5);
+ _widthheight.push_back(unit_menu);
+ }
+
+ // Switch
+ Gtk::RadioButtonGroup rb_group;
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Rows, columns: ")));
+ radio->set_tooltip_text(_("Create the specified number of rows and columns"));
+ table_attach(table, radio, 0.0, 1, 1);
+
+ if (!prefs->getBool(prefs_path + "fillrect")) {
+ radio->set_active(true);
+ switch_to_create();
+ }
+ radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_create));
+ }
+ {
+ auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Width, height: ")));
+ radio->set_tooltip_text(_("Fill the specified width and height with the tiling"));
+ table_attach(table, radio, 0.0, 2, 1);
+
+ if (prefs->getBool(prefs_path + "fillrect")) {
+ radio->set_active(true);
+ switch_to_fill();
+ }
+ radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_fill));
+ }
+ }
+
+
+ // Use saved pos
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ mainbox->pack_start(*hb, false, false, 0);
+
+ _cb_keep_bbox = Gtk::manage(new UI::Widget::CheckButtonInternal(_("Use saved size and position of the tile")));
+ auto keepbbox = prefs->getBool(prefs_path + "keepbbox", true);
+ _cb_keep_bbox->set_active(keepbbox);
+ _cb_keep_bbox->set_tooltip_text(_("Pretend that the size and position of the tile are the same "
+ "as the last time you tiled it (if any), instead of using the "
+ "current size"));
+ hb->pack_start(*_cb_keep_bbox, false, false, 0);
+ _cb_keep_bbox->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::keep_bbox_toggled));
+ }
+
+ // Statusbar
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ hb->set_homogeneous(false);
+ mainbox->pack_end(*hb, false, false, 0);
+ auto l = Gtk::manage(new Gtk::Label(""));
+ _status = l;
+ hb->pack_start(*l, false, false, 0);
+ }
+
+ // Buttons
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ hb->set_homogeneous(false);
+ mainbox->pack_start(*hb, false, false, 0);
+
+ {
+ auto b = Gtk::manage(new Gtk::Button());
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup_with_mnemonic(_(" <b>_Create</b> "));
+ b->add(*l);
+ b->set_tooltip_text(_("Create and tile the clones of the selection"));
+ b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::apply));
+ hb->pack_end(*b, false, false, 0);
+ }
+
+ { // buttons which are enabled only when there are tiled clones
+ auto sb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+ sb->set_homogeneous(false);
+ hb->pack_end(*sb, false, false, 0);
+ _buttons_on_tiles = sb;
+ {
+ // TRANSLATORS: if a group of objects are "clumped" together, then they
+ // are unevenly spread in the given amount of space - as shown in the
+ // diagrams on the left in the following screenshot:
+ // http://www.inkscape.org/screenshots/gallery/inkscape-0.42-CVS-tiles-unclump.png
+ // So unclumping is the process of spreading a number of objects out more evenly.
+ auto b = Gtk::manage(new Gtk::Button(_(" _Unclump "), true));
+ b->set_tooltip_text(_("Spread out clones to reduce clumping; can be applied repeatedly"));
+ b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::unclump));
+ sb->pack_end(*b, false, false, 0);
+ }
+
+ {
+ auto b = Gtk::manage(new Gtk::Button(_(" Re_move "), true));
+ b->set_tooltip_text(_("Remove existing tiled clones of the selected object (siblings only)"));
+ b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::on_remove_button_clicked));
+ sb->pack_end(*b, false, false, 0);
+ }
+
+ // connect to global selection changed signal (so we can change desktops) and
+ // external_change (so we're not fooled by undo)
+ selectChangedConn = INKSCAPE.signal_selection_changed.connect(sigc::mem_fun(*this, &CloneTiler::change_selection));
+ externChangedConn = INKSCAPE.signal_external_change.connect(sigc::mem_fun(*this, &CloneTiler::external_change));
+
+ // update now
+ change_selection(SP_ACTIVE_DESKTOP->getSelection());
+ }
+
+ {
+ auto b = Gtk::manage(new Gtk::Button(_(" R_eset "), true));
+ // TRANSLATORS: "change" is a noun here
+ b->set_tooltip_text(_("Reset all shifts, scales, rotates, opacity and color changes in the dialog to zero"));
+ b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::reset));
+ hb->pack_start(*b, false, false, 0);
+ }
+ }
+
+ mainbox->show_all();
+ }
+
+ show_all();
+}
+
+CloneTiler::~CloneTiler ()
+{
+ selectChangedConn.disconnect();
+ externChangedConn.disconnect();
+ color_changed_connection.disconnect();
+}
+
+void CloneTiler::on_picker_color_changed(guint rgba)
+{
+ static bool is_updating = false;
+ if (is_updating || !SP_ACTIVE_DESKTOP)
+ return;
+
+ is_updating = true;
+
+ gchar c[32];
+ sp_svg_write_color(c, sizeof(c), rgba);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString(prefs_path + "initial_color", c);
+
+ is_updating = false;
+}
+
+void CloneTiler::change_selection(Inkscape::Selection *selection)
+{
+ if (selection->isEmpty()) {
+ _buttons_on_tiles->set_sensitive(false);
+ _status->set_markup(_("<small>Nothing selected.</small>"));
+ return;
+ }
+
+ if (boost::distance(selection->items()) > 1) {
+ _buttons_on_tiles->set_sensitive(false);
+ _status->set_markup(_("<small>More than one object selected.</small>"));
+ return;
+ }
+
+ guint n = number_of_clones(selection->singleItem());
+ if (n > 0) {
+ _buttons_on_tiles->set_sensitive(true);
+ gchar *sta = g_strdup_printf (_("<small>Object has <b>%d</b> tiled clones.</small>"), n);
+ _status->set_markup(sta);
+ g_free (sta);
+ } else {
+ _buttons_on_tiles->set_sensitive(false);
+ _status->set_markup(_("<small>Object has no tiled clones.</small>"));
+ }
+}
+
+void CloneTiler::external_change()
+{
+ change_selection(SP_ACTIVE_DESKTOP->getSelection());
+}
+
+Geom::Affine CloneTiler::get_transform(
+ // symmetry group
+ int type,
+
+ // row, column
+ int i, int j,
+
+ // center, width, height of the tile
+ double cx, double cy,
+ double w, double h,
+
+ // values from the dialog:
+ // Shift
+ double shiftx_per_i, double shifty_per_i,
+ double shiftx_per_j, double shifty_per_j,
+ double shiftx_rand, double shifty_rand,
+ double shiftx_exp, double shifty_exp,
+ int shiftx_alternate, int shifty_alternate,
+ int shiftx_cumulate, int shifty_cumulate,
+ int shiftx_excludew, int shifty_excludeh,
+
+ // Scale
+ double scalex_per_i, double scaley_per_i,
+ double scalex_per_j, double scaley_per_j,
+ double scalex_rand, double scaley_rand,
+ double scalex_exp, double scaley_exp,
+ double scalex_log, double scaley_log,
+ int scalex_alternate, int scaley_alternate,
+ int scalex_cumulate, int scaley_cumulate,
+
+ // Rotation
+ double rotate_per_i, double rotate_per_j,
+ double rotate_rand,
+ int rotate_alternatei, int rotate_alternatej,
+ int rotate_cumulatei, int rotate_cumulatej
+ )
+{
+
+ // Shift (in units of tile width or height) -------------
+ double delta_shifti = 0.0;
+ double delta_shiftj = 0.0;
+
+ if( shiftx_alternate ) {
+ delta_shifti = (double)(i%2);
+ } else {
+ if( shiftx_cumulate ) { // Should the delta shifts be cumulative (i.e. 1, 1+2, 1+2+3, ...)
+ delta_shifti = (double)(i*i);
+ } else {
+ delta_shifti = (double)i;
+ }
+ }
+
+ if( shifty_alternate ) {
+ delta_shiftj = (double)(j%2);
+ } else {
+ if( shifty_cumulate ) {
+ delta_shiftj = (double)(j*j);
+ } else {
+ delta_shiftj = (double)j;
+ }
+ }
+
+ // Random shift, only calculate if non-zero.
+ double delta_shiftx_rand = 0.0;
+ double delta_shifty_rand = 0.0;
+ if( shiftx_rand != 0.0 ) delta_shiftx_rand = shiftx_rand * g_random_double_range (-1, 1);
+ if( shifty_rand != 0.0 ) delta_shifty_rand = shifty_rand * g_random_double_range (-1, 1);
+
+
+ // Delta shift (units of tile width/height)
+ double di = shiftx_per_i * delta_shifti + shiftx_per_j * delta_shiftj + delta_shiftx_rand;
+ double dj = shifty_per_i * delta_shifti + shifty_per_j * delta_shiftj + delta_shifty_rand;
+
+ // Shift in actual x and y, used below
+ double dx = w * di;
+ double dy = h * dj;
+
+ double shifti = di;
+ double shiftj = dj;
+
+ // Include tile width and height in shift if required
+ if( !shiftx_excludew ) shifti += i;
+ if( !shifty_excludeh ) shiftj += j;
+
+ // Add exponential shift if necessary
+ double shifti_sign = (shifti > 0.0) ? 1.0 : -1.0;
+ shifti = shifti_sign * pow(fabs(shifti), shiftx_exp);
+ double shiftj_sign = (shiftj > 0.0) ? 1.0 : -1.0;
+ shiftj = shiftj_sign * pow(fabs(shiftj), shifty_exp);
+
+ // Final shift
+ Geom::Affine rect_translate (Geom::Translate (w * shifti, h * shiftj));
+
+ // Rotation (in degrees) ------------
+ double delta_rotationi = 0.0;
+ double delta_rotationj = 0.0;
+
+ if( rotate_alternatei ) {
+ delta_rotationi = (double)(i%2);
+ } else {
+ if( rotate_cumulatei ) {
+ delta_rotationi = (double)(i*i + i)/2.0;
+ } else {
+ delta_rotationi = (double)i;
+ }
+ }
+
+ if( rotate_alternatej ) {
+ delta_rotationj = (double)(j%2);
+ } else {
+ if( rotate_cumulatej ) {
+ delta_rotationj = (double)(j*j + j)/2.0;
+ } else {
+ delta_rotationj = (double)j;
+ }
+ }
+
+ double delta_rotate_rand = 0.0;
+ if( rotate_rand != 0.0 ) delta_rotate_rand = rotate_rand * 180.0 * g_random_double_range (-1, 1);
+
+ double dr = rotate_per_i * delta_rotationi + rotate_per_j * delta_rotationj + delta_rotate_rand;
+
+ // Scale (times the original) -----------
+ double delta_scalei = 0.0;
+ double delta_scalej = 0.0;
+
+ if( scalex_alternate ) {
+ delta_scalei = (double)(i%2);
+ } else {
+ if( scalex_cumulate ) { // Should the delta scales be cumulative (i.e. 1, 1+2, 1+2+3, ...)
+ delta_scalei = (double)(i*i + i)/2.0;
+ } else {
+ delta_scalei = (double)i;
+ }
+ }
+
+ if( scaley_alternate ) {
+ delta_scalej = (double)(j%2);
+ } else {
+ if( scaley_cumulate ) {
+ delta_scalej = (double)(j*j + j)/2.0;
+ } else {
+ delta_scalej = (double)j;
+ }
+ }
+
+ // Random scale, only calculate if non-zero.
+ double delta_scalex_rand = 0.0;
+ double delta_scaley_rand = 0.0;
+ if( scalex_rand != 0.0 ) delta_scalex_rand = scalex_rand * g_random_double_range (-1, 1);
+ if( scaley_rand != 0.0 ) delta_scaley_rand = scaley_rand * g_random_double_range (-1, 1);
+ // But if random factors are same, scale x and y proportionally
+ if( scalex_rand == scaley_rand ) delta_scalex_rand = delta_scaley_rand;
+
+ // Total delta scale
+ double scalex = 1.0 + scalex_per_i * delta_scalei + scalex_per_j * delta_scalej + delta_scalex_rand;
+ double scaley = 1.0 + scaley_per_i * delta_scalei + scaley_per_j * delta_scalej + delta_scaley_rand;
+
+ if( scalex < 0.0 ) scalex = 0.0;
+ if( scaley < 0.0 ) scaley = 0.0;
+
+ // Add exponential scale if necessary
+ if ( scalex_exp != 1.0 ) scalex = pow( scalex, scalex_exp );
+ if ( scaley_exp != 1.0 ) scaley = pow( scaley, scaley_exp );
+
+ // Add logarithmic factor if necessary
+ if ( scalex_log > 0.0 ) scalex = pow( scalex_log, scalex - 1.0 );
+ if ( scaley_log > 0.0 ) scaley = pow( scaley_log, scaley - 1.0 );
+ // Alternative using rotation angle
+ //if ( scalex_log != 1.0 ) scalex *= pow( scalex_log, M_PI*dr/180 );
+ //if ( scaley_log != 1.0 ) scaley *= pow( scaley_log, M_PI*dr/180 );
+
+
+ // Calculate transformation matrices, translating back to "center of tile" (rotation center) before transforming
+ Geom::Affine drot_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI*dr/180) * Geom::Translate(cx, cy);
+
+ Geom::Affine dscale_c = Geom::Translate(-cx, -cy) * Geom::Scale (scalex, scaley) * Geom::Translate(cx, cy);
+
+ Geom::Affine d_s_r = dscale_c * drot_c;
+
+ Geom::Affine rotate_180_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI) * Geom::Translate(cx, cy);
+
+ Geom::Affine rotate_90_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/2) * Geom::Translate(cx, cy);
+ Geom::Affine rotate_m90_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/2) * Geom::Translate(cx, cy);
+
+ Geom::Affine rotate_120_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-2*M_PI/3) * Geom::Translate(cx, cy);
+ Geom::Affine rotate_m120_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( 2*M_PI/3) * Geom::Translate(cx, cy);
+
+ Geom::Affine rotate_60_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/3) * Geom::Translate(cx, cy);
+ Geom::Affine rotate_m60_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/3) * Geom::Translate(cx, cy);
+
+ Geom::Affine flip_x = Geom::Translate(-cx, -cy) * Geom::Scale (-1, 1) * Geom::Translate(cx, cy);
+ Geom::Affine flip_y = Geom::Translate(-cx, -cy) * Geom::Scale (1, -1) * Geom::Translate(cx, cy);
+
+
+ // Create tile with required symmetry
+ const double cos60 = cos(M_PI/3);
+ const double sin60 = sin(M_PI/3);
+ const double cos30 = cos(M_PI/6);
+ const double sin30 = sin(M_PI/6);
+
+ switch (type) {
+
+ case TILE_P1:
+ return d_s_r * rect_translate;
+ break;
+
+ case TILE_P2:
+ if (i % 2 == 0) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * rotate_180_c * rect_translate;
+ }
+ break;
+
+ case TILE_PM:
+ if (i % 2 == 0) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * flip_x * rect_translate;
+ }
+ break;
+
+ case TILE_PG:
+ if (j % 2 == 0) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * flip_x * rect_translate;
+ }
+ break;
+
+ case TILE_CM:
+ if ((i + j) % 2 == 0) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * flip_x * rect_translate;
+ }
+ break;
+
+ case TILE_PMM:
+ if (j % 2 == 0) {
+ if (i % 2 == 0) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * flip_x * rect_translate;
+ }
+ } else {
+ if (i % 2 == 0) {
+ return d_s_r * flip_y * rect_translate;
+ } else {
+ return d_s_r * flip_x * flip_y * rect_translate;
+ }
+ }
+ break;
+
+ case TILE_PMG:
+ if (j % 2 == 0) {
+ if (i % 2 == 0) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * rotate_180_c * rect_translate;
+ }
+ } else {
+ if (i % 2 == 0) {
+ return d_s_r * flip_y * rect_translate;
+ } else {
+ return d_s_r * rotate_180_c * flip_y * rect_translate;
+ }
+ }
+ break;
+
+ case TILE_PGG:
+ if (j % 2 == 0) {
+ if (i % 2 == 0) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * flip_y * rect_translate;
+ }
+ } else {
+ if (i % 2 == 0) {
+ return d_s_r * rotate_180_c * rect_translate;
+ } else {
+ return d_s_r * rotate_180_c * flip_y * rect_translate;
+ }
+ }
+ break;
+
+ case TILE_CMM:
+ if (j % 4 == 0) {
+ if (i % 2 == 0) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * flip_x * rect_translate;
+ }
+ } else if (j % 4 == 1) {
+ if (i % 2 == 0) {
+ return d_s_r * flip_y * rect_translate;
+ } else {
+ return d_s_r * flip_x * flip_y * rect_translate;
+ }
+ } else if (j % 4 == 2) {
+ if (i % 2 == 1) {
+ return d_s_r * rect_translate;
+ } else {
+ return d_s_r * flip_x * rect_translate;
+ }
+ } else {
+ if (i % 2 == 1) {
+ return d_s_r * flip_y * rect_translate;
+ } else {
+ return d_s_r * flip_x * flip_y * rect_translate;
+ }
+ }
+ break;
+
+ case TILE_P4:
+ {
+ Geom::Affine ori (Geom::Translate ((w + h) * pow((i/2), shiftx_exp) + dx, (h + w) * pow((j/2), shifty_exp) + dy));
+ Geom::Affine dia1 (Geom::Translate (w/2 + h/2, -h/2 + w/2));
+ Geom::Affine dia2 (Geom::Translate (-w/2 + h/2, h/2 + w/2));
+ if (j % 2 == 0) {
+ if (i % 2 == 0) {
+ return d_s_r * ori;
+ } else {
+ return d_s_r * rotate_m90_c * dia1 * ori;
+ }
+ } else {
+ if (i % 2 == 0) {
+ return d_s_r * rotate_90_c * dia2 * ori;
+ } else {
+ return d_s_r * rotate_180_c * dia1 * dia2 * ori;
+ }
+ }
+ }
+ break;
+
+ case TILE_P4M:
+ {
+ double max = MAX(w, h);
+ Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow((j/2), shifty_exp) + dy));
+ Geom::Affine dia1 (Geom::Translate ( w/2 - h/2, h/2 - w/2));
+ Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 - h/2));
+ if (j % 2 == 0) {
+ if (i % 4 == 0) {
+ return d_s_r * ori;
+ } else if (i % 4 == 1) {
+ return d_s_r * flip_y * rotate_m90_c * dia1 * ori;
+ } else if (i % 4 == 2) {
+ return d_s_r * rotate_m90_c * dia1 * Geom::Translate (h, 0) * ori;
+ } else if (i % 4 == 3) {
+ return d_s_r * flip_x * Geom::Translate (w, 0) * ori;
+ }
+ } else {
+ if (i % 4 == 0) {
+ return d_s_r * flip_y * Geom::Translate(0, h) * ori;
+ } else if (i % 4 == 1) {
+ return d_s_r * rotate_90_c * dia2 * Geom::Translate(0, h) * ori;
+ } else if (i % 4 == 2) {
+ return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate(h, 0) * Geom::Translate(0, h) * ori;
+ } else if (i % 4 == 3) {
+ return d_s_r * flip_y * flip_x * Geom::Translate(w, 0) * Geom::Translate(0, h) * ori;
+ }
+ }
+ }
+ break;
+
+ case TILE_P4G:
+ {
+ double max = MAX(w, h);
+ Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow(j, shifty_exp) + dy));
+ Geom::Affine dia1 (Geom::Translate ( w/2 + h/2, h/2 - w/2));
+ Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 + h/2));
+ if (((i/4) + j) % 2 == 0) {
+ if (i % 4 == 0) {
+ return d_s_r * ori;
+ } else if (i % 4 == 1) {
+ return d_s_r * rotate_m90_c * dia1 * ori;
+ } else if (i % 4 == 2) {
+ return d_s_r * rotate_90_c * dia2 * ori;
+ } else if (i % 4 == 3) {
+ return d_s_r * rotate_180_c * dia1 * dia2 * ori;
+ }
+ } else {
+ if (i % 4 == 0) {
+ return d_s_r * flip_y * Geom::Translate (0, h) * ori;
+ } else if (i % 4 == 1) {
+ return d_s_r * flip_y * rotate_m90_c * dia1 * Geom::Translate (-h, 0) * ori;
+ } else if (i % 4 == 2) {
+ return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate (h, 0) * ori;
+ } else if (i % 4 == 3) {
+ return d_s_r * flip_x * Geom::Translate (w, 0) * ori;
+ }
+ }
+ }
+ break;
+
+ case TILE_P3:
+ {
+ double width;
+ double height;
+ Geom::Affine dia1;
+ Geom::Affine dia2;
+ if (w > h) {
+ width = w + w * cos60;
+ height = 2 * w * sin60;
+ dia1 = Geom::Affine (Geom::Translate (w/2 + w/2 * cos60, -(w/2 * sin60)));
+ dia2 = dia1 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60)));
+ } else {
+ width = h * cos (M_PI/6);
+ height = h;
+ dia1 = Geom::Affine (Geom::Translate (h/2 * cos30, -(h/2 * sin30)));
+ dia2 = dia1 * Geom::Affine (Geom::Translate (0, h/2));
+ }
+ Geom::Affine ori (Geom::Translate (width * pow((2*(i/3) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy));
+ if (i % 3 == 0) {
+ return d_s_r * ori;
+ } else if (i % 3 == 1) {
+ return d_s_r * rotate_m120_c * dia1 * ori;
+ } else if (i % 3 == 2) {
+ return d_s_r * rotate_120_c * dia2 * ori;
+ }
+ }
+ break;
+
+ case TILE_P31M:
+ {
+ Geom::Affine ori;
+ Geom::Affine dia1;
+ Geom::Affine dia2;
+ Geom::Affine dia3;
+ Geom::Affine dia4;
+ if (w > h) {
+ ori = Geom::Affine(Geom::Translate (w * pow((i/6) + 0.5*(j%2), shiftx_exp) + dx, (w * cos30) * pow(j, shifty_exp) + dy));
+ dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) );
+ dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30));
+ dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30)));
+ dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30));
+ } else {
+ ori = Geom::Affine (Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (2*h - h * sin30) * pow(j, shifty_exp) + dy));
+ dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30));
+ dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30));
+ dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2));
+ dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30));
+ }
+ if (i % 6 == 0) {
+ return d_s_r * ori;
+ } else if (i % 6 == 1) {
+ return d_s_r * flip_y * rotate_m120_c * dia1 * ori;
+ } else if (i % 6 == 2) {
+ return d_s_r * rotate_m120_c * dia2 * ori;
+ } else if (i % 6 == 3) {
+ return d_s_r * flip_y * rotate_120_c * dia3 * ori;
+ } else if (i % 6 == 4) {
+ return d_s_r * rotate_120_c * dia4 * ori;
+ } else if (i % 6 == 5) {
+ return d_s_r * flip_y * Geom::Translate(0, h) * ori;
+ }
+ }
+ break;
+
+ case TILE_P3M1:
+ {
+ double width;
+ double height;
+ Geom::Affine dia1;
+ Geom::Affine dia2;
+ Geom::Affine dia3;
+ Geom::Affine dia4;
+ if (w > h) {
+ width = w + w * cos60;
+ height = 2 * w * sin60;
+ dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) );
+ dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30));
+ dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30)));
+ dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30));
+ } else {
+ width = 2 * h * cos (M_PI/6);
+ height = 2 * h;
+ dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30));
+ dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30));
+ dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2));
+ dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30));
+ }
+ Geom::Affine ori (Geom::Translate (width * pow((2*(i/6) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy));
+ if (i % 6 == 0) {
+ return d_s_r * ori;
+ } else if (i % 6 == 1) {
+ return d_s_r * flip_y * rotate_m120_c * dia1 * ori;
+ } else if (i % 6 == 2) {
+ return d_s_r * rotate_m120_c * dia2 * ori;
+ } else if (i % 6 == 3) {
+ return d_s_r * flip_y * rotate_120_c * dia3 * ori;
+ } else if (i % 6 == 4) {
+ return d_s_r * rotate_120_c * dia4 * ori;
+ } else if (i % 6 == 5) {
+ return d_s_r * flip_y * Geom::Translate(0, h) * ori;
+ }
+ }
+ break;
+
+ case TILE_P6:
+ {
+ Geom::Affine ori;
+ Geom::Affine dia1;
+ Geom::Affine dia2;
+ Geom::Affine dia3;
+ Geom::Affine dia4;
+ Geom::Affine dia5;
+ if (w > h) {
+ ori = Geom::Affine(Geom::Translate (w * pow((2*(i/6) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy));
+ dia1 = Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60));
+ dia2 = dia1 * Geom::Affine (Geom::Translate (w/2, 0));
+ dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, w/2 * sin60));
+ dia4 = dia3 * Geom::Affine (Geom::Translate (-w/2 * cos60, w/2 * sin60));
+ dia5 = dia4 * Geom::Affine (Geom::Translate (-w/2, 0));
+ } else {
+ ori = Geom::Affine(Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (h + h * sin30) * pow(j, shifty_exp) + dy));
+ dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60));
+ dia2 = dia1 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60));
+ dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2));
+ dia4 = dia3 * dia1.inverse();
+ dia5 = dia3 * dia2.inverse();
+ }
+ if (i % 6 == 0) {
+ return d_s_r * ori;
+ } else if (i % 6 == 1) {
+ return d_s_r * rotate_m60_c * dia1 * ori;
+ } else if (i % 6 == 2) {
+ return d_s_r * rotate_m120_c * dia2 * ori;
+ } else if (i % 6 == 3) {
+ return d_s_r * rotate_180_c * dia3 * ori;
+ } else if (i % 6 == 4) {
+ return d_s_r * rotate_120_c * dia4 * ori;
+ } else if (i % 6 == 5) {
+ return d_s_r * rotate_60_c * dia5 * ori;
+ }
+ }
+ break;
+
+ case TILE_P6M:
+ {
+
+ Geom::Affine ori;
+ Geom::Affine dia1, dia2, dia3, dia4, dia5, dia6, dia7, dia8, dia9, dia10;
+ if (w > h) {
+ ori = Geom::Affine(Geom::Translate (w * pow((2*(i/12) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy));
+ dia1 = Geom::Affine (Geom::Translate (w/2, h/2) * Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, h/2 * sin30));
+ dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30));
+ dia3 = dia2 * Geom::Affine (Geom::Translate (-h/2 * cos30, h/2 * sin30) * Geom::Translate (w * cos60, 0) * Geom::Translate (-h/2 * cos30, -h/2 * sin30));
+ dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30));
+ dia5 = dia4 * Geom::Affine (Geom::Translate (-h/2 * cos30, -h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60) * Geom::Translate (w/2, -h/2));
+ dia6 = dia5 * Geom::Affine (Geom::Translate (0, h));
+ dia7 = dia6 * dia1.inverse();
+ dia8 = dia6 * dia2.inverse();
+ dia9 = dia6 * dia3.inverse();
+ dia10 = dia6 * dia4.inverse();
+ } else {
+ ori = Geom::Affine(Geom::Translate (4*h * cos30 * pow((i/12 + 0.5*(j%2)), shiftx_exp) + dx, (2*h + 2*h * sin30) * pow(j, shifty_exp) + dy));
+ dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60));
+ dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30));
+ dia3 = dia2 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h * cos30, 0) * Geom::Translate (-w/2 * cos60, w/2 * sin60));
+ dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30));
+ dia5 = dia4 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2));
+ dia6 = dia5 * Geom::Affine (Geom::Translate (0, h));
+ dia7 = dia6 * dia1.inverse();
+ dia8 = dia6 * dia2.inverse();
+ dia9 = dia6 * dia3.inverse();
+ dia10 = dia6 * dia4.inverse();
+ }
+ if (i % 12 == 0) {
+ return d_s_r * ori;
+ } else if (i % 12 == 1) {
+ return d_s_r * flip_y * rotate_m60_c * dia1 * ori;
+ } else if (i % 12 == 2) {
+ return d_s_r * rotate_m60_c * dia2 * ori;
+ } else if (i % 12 == 3) {
+ return d_s_r * flip_y * rotate_m120_c * dia3 * ori;
+ } else if (i % 12 == 4) {
+ return d_s_r * rotate_m120_c * dia4 * ori;
+ } else if (i % 12 == 5) {
+ return d_s_r * flip_x * dia5 * ori;
+ } else if (i % 12 == 6) {
+ return d_s_r * flip_x * flip_y * dia6 * ori;
+ } else if (i % 12 == 7) {
+ return d_s_r * flip_y * rotate_120_c * dia7 * ori;
+ } else if (i % 12 == 8) {
+ return d_s_r * rotate_120_c * dia8 * ori;
+ } else if (i % 12 == 9) {
+ return d_s_r * flip_y * rotate_60_c * dia9 * ori;
+ } else if (i % 12 == 10) {
+ return d_s_r * rotate_60_c * dia10 * ori;
+ } else if (i % 12 == 11) {
+ return d_s_r * flip_y * Geom::Translate (0, h) * ori;
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return Geom::identity();
+}
+
+bool CloneTiler::is_a_clone_of(SPObject *tile, SPObject *obj)
+{
+ bool result = false;
+ char *id_href = nullptr;
+
+ if (obj) {
+ Inkscape::XML::Node *obj_repr = obj->getRepr();
+ id_href = g_strdup_printf("#%s", obj_repr->attribute("id"));
+ }
+
+ auto href = Inkscape::getHrefAttribute(*tile->getRepr()).second;
+
+ if (is<SPUse>(tile) &&
+ href && (!id_href || !strcmp(id_href, href)) &&
+ tile->getRepr()->attribute("inkscape:tiled-clone-of") &&
+ (!id_href || !strcmp(id_href, tile->getRepr()->attribute("inkscape:tiled-clone-of"))))
+ {
+ result = true;
+ } else {
+ result = false;
+ }
+ if (id_href) {
+ g_free(id_href);
+ id_href = nullptr;
+ }
+ return result;
+}
+
+void CloneTiler::trace_hide_tiled_clones_recursively(SPObject *from)
+{
+ if (!trace_drawing)
+ return;
+
+ for (auto& o: from->children) {
+ auto item = cast<SPItem>(&o);
+ if (item && is_a_clone_of(&o, nullptr)) {
+ item->invoke_hide(trace_visionkey); // FIXME: hide each tiled clone's original too!
+ }
+ trace_hide_tiled_clones_recursively (&o);
+ }
+}
+
+void CloneTiler::trace_setup(SPDocument *doc, gdouble zoom, SPItem *original)
+{
+ trace_drawing = new Inkscape::Drawing();
+ /* Create ArenaItem and set transform */
+ trace_visionkey = SPItem::display_key_new(1);
+ trace_doc = doc;
+ trace_drawing->setRoot(trace_doc->getRoot()->invoke_show(*trace_drawing, trace_visionkey, SP_ITEM_SHOW_DISPLAY));
+
+ // hide the (current) original and any tiled clones, we only want to pick the background
+ original->invoke_hide(trace_visionkey);
+ trace_hide_tiled_clones_recursively(trace_doc->getRoot());
+
+ trace_doc->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ trace_doc->ensureUpToDate();
+
+ trace_zoom = zoom;
+}
+
+guint32 CloneTiler::trace_pick(Geom::Rect box)
+{
+ if (!trace_drawing) {
+ return 0;
+ }
+
+ trace_drawing->root()->setTransform(Geom::Scale(trace_zoom));
+ trace_drawing->update();
+
+ /* Item integer bbox in points */
+ Geom::IntRect ibox = (box * Geom::Scale(trace_zoom)).roundOutwards();
+
+ /* Find visible area */
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ibox.width(), ibox.height());
+ Inkscape::DrawingContext dc(s, ibox.min());
+ /* Render */
+ trace_drawing->render(dc, ibox);
+ double R = 0, G = 0, B = 0, A = 0;
+ ink_cairo_surface_average_color(s, R, G, B, A);
+ cairo_surface_destroy(s);
+
+ return SP_RGBA32_F_COMPOSE (R, G, B, A);
+}
+
+void CloneTiler::trace_finish()
+{
+ if (trace_doc) {
+ trace_doc->getRoot()->invoke_hide(trace_visionkey);
+ delete trace_drawing;
+ trace_doc = nullptr;
+ trace_drawing = nullptr;
+ }
+}
+
+void CloneTiler::unclump()
+{
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ // check if something is selected
+ if (selection->isEmpty() || boost::distance(selection->items()) > 1) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>one object</b> whose tiled clones to unclump."));
+ return;
+ }
+
+ auto obj = selection->singleItem();
+ auto parent = obj->parent;
+
+ std::vector<SPItem*> to_unclump; // not including the original
+
+ for (auto& child: parent->children) {
+ if (is_a_clone_of (&child, obj)) {
+ to_unclump.push_back((SPItem*)&child);
+ }
+ }
+
+ getDocument()->ensureUpToDate();
+ reverse(to_unclump.begin(),to_unclump.end());
+ ::unclump (to_unclump);
+
+ DocumentUndo::done(getDocument(), _("Unclump tiled clones"), INKSCAPE_ICON("dialog-tile-clones"));
+}
+
+guint CloneTiler::number_of_clones(SPObject *obj)
+{
+ SPObject *parent = obj->parent;
+
+ guint n = 0;
+
+ for (auto& child: parent->children) {
+ if (is_a_clone_of (&child, obj)) {
+ n ++;
+ }
+ }
+
+ return n;
+}
+
+void CloneTiler::remove(bool do_undo/* = true*/)
+{
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ // check if something is selected
+ if (selection->isEmpty() || boost::distance(selection->items()) > 1) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>one object</b> whose tiled clones to remove."));
+ return;
+ }
+
+ SPObject *obj = selection->singleItem();
+ SPObject *parent = obj->parent;
+
+// remove old tiling
+ std::vector<SPObject *> to_delete;
+ for (auto& child: parent->children) {
+ if (is_a_clone_of (&child, obj)) {
+ to_delete.push_back(&child);
+ }
+ }
+ for (auto obj:to_delete) {
+ g_assert(obj != nullptr);
+ obj->deleteObject();
+ }
+
+ change_selection (selection);
+
+ if (do_undo) {
+ DocumentUndo::done(getDocument(), _("Delete tiled clones"), INKSCAPE_ICON("dialog-tile-clones"));
+ }
+}
+
+Geom::Rect CloneTiler::transform_rect(Geom::Rect const &r, Geom::Affine const &m)
+{
+ using Geom::X;
+ using Geom::Y;
+ Geom::Point const p1 = r.corner(1) * m;
+ Geom::Point const p2 = r.corner(2) * m;
+ Geom::Point const p3 = r.corner(3) * m;
+ Geom::Point const p4 = r.corner(4) * m;
+ return Geom::Rect(
+ Geom::Point(
+ std::min(std::min(p1[X], p2[X]), std::min(p3[X], p4[X])),
+ std::min(std::min(p1[Y], p2[Y]), std::min(p3[Y], p4[Y]))),
+ Geom::Point(
+ std::max(std::max(p1[X], p2[X]), std::max(p3[X], p4[X])),
+ std::max(std::max(p1[Y], p2[Y]), std::max(p3[Y], p4[Y]))));
+}
+
+/**
+Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same
+probability of being displaced.
+ */
+double CloneTiler::randomize01(double val, double rand)
+{
+ double base = MIN (val - rand, 1 - 2*rand);
+ if (base < 0) {
+ base = 0;
+ }
+ val = base + g_random_double_range (0, MIN (2 * rand, 1 - base));
+ return CLAMP(val, 0, 1); // this should be unnecessary with the above provisions, but just in case...
+}
+
+
+void CloneTiler::apply()
+{
+ auto desktop = getDesktop();
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ // check if something is selected
+ if (selection->isEmpty()) {
+ desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select an <b>object</b> to clone."));
+ return;
+ }
+
+ // Check if more than one object is selected.
+ if (boost::distance(selection->items()) > 1) {
+ desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("If you want to clone several objects, <b>group</b> them and <b>clone the group</b>."));
+ return;
+ }
+
+ // set "busy" cursor
+ desktop->setWaitingCursor();
+
+ // set statusbar text
+ _status->set_markup(_("<small>Creating tiled clones...</small>"));
+ _status->queue_draw();
+
+ SPObject *obj = selection->singleItem();
+ if (!obj) {
+ // Should never happen (empty selection checked above).
+ std::cerr << "CloneTiler::clonetile_apply(): No object in single item selection!!!" << std::endl;
+ return;
+ }
+ Inkscape::XML::Node *obj_repr = obj->getRepr();
+ const char *id_href = g_strdup_printf("#%s", obj_repr->attribute("id"));
+ SPObject *parent = obj->parent;
+
+ remove(false);
+
+ Geom::Scale scale = getDocument()->getDocumentScale().inverse();
+ double scale_units = scale[Geom::X]; // Use just x direction....
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ double shiftx_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_i", 0, -10000, 10000);
+ double shifty_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_i", 0, -10000, 10000);
+ double shiftx_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_j", 0, -10000, 10000);
+ double shifty_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_j", 0, -10000, 10000);
+ double shiftx_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_rand", 0, 0, 1000);
+ double shifty_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_rand", 0, 0, 1000);
+ double shiftx_exp = prefs->getDoubleLimited(prefs_path + "shiftx_exp", 1, 0, 10);
+ double shifty_exp = prefs->getDoubleLimited(prefs_path + "shifty_exp", 1, 0, 10);
+ bool shiftx_alternate = prefs->getBool(prefs_path + "shiftx_alternate");
+ bool shifty_alternate = prefs->getBool(prefs_path + "shifty_alternate");
+ bool shiftx_cumulate = prefs->getBool(prefs_path + "shiftx_cumulate");
+ bool shifty_cumulate = prefs->getBool(prefs_path + "shifty_cumulate");
+ bool shiftx_excludew = prefs->getBool(prefs_path + "shiftx_excludew");
+ bool shifty_excludeh = prefs->getBool(prefs_path + "shifty_excludeh");
+
+ double scalex_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_i", 0, -100, 1000);
+ double scaley_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_i", 0, -100, 1000);
+ double scalex_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_j", 0, -100, 1000);
+ double scaley_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_j", 0, -100, 1000);
+ double scalex_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_rand", 0, 0, 1000);
+ double scaley_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_rand", 0, 0, 1000);
+ double scalex_exp = prefs->getDoubleLimited(prefs_path + "scalex_exp", 1, 0, 10);
+ double scaley_exp = prefs->getDoubleLimited(prefs_path + "scaley_exp", 1, 0, 10);
+ double scalex_log = prefs->getDoubleLimited(prefs_path + "scalex_log", 0, 0, 10);
+ double scaley_log = prefs->getDoubleLimited(prefs_path + "scaley_log", 0, 0, 10);
+ bool scalex_alternate = prefs->getBool(prefs_path + "scalex_alternate");
+ bool scaley_alternate = prefs->getBool(prefs_path + "scaley_alternate");
+ bool scalex_cumulate = prefs->getBool(prefs_path + "scalex_cumulate");
+ bool scaley_cumulate = prefs->getBool(prefs_path + "scaley_cumulate");
+
+ double rotate_per_i = prefs->getDoubleLimited(prefs_path + "rotate_per_i", 0, -180, 180);
+ double rotate_per_j = prefs->getDoubleLimited(prefs_path + "rotate_per_j", 0, -180, 180);
+ double rotate_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "rotate_rand", 0, 0, 100);
+ bool rotate_alternatei = prefs->getBool(prefs_path + "rotate_alternatei");
+ bool rotate_alternatej = prefs->getBool(prefs_path + "rotate_alternatej");
+ bool rotate_cumulatei = prefs->getBool(prefs_path + "rotate_cumulatei");
+ bool rotate_cumulatej = prefs->getBool(prefs_path + "rotate_cumulatej");
+
+ double blur_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_i", 0, 0, 100);
+ double blur_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_j", 0, 0, 100);
+ bool blur_alternatei = prefs->getBool(prefs_path + "blur_alternatei");
+ bool blur_alternatej = prefs->getBool(prefs_path + "blur_alternatej");
+ double blur_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_rand", 0, 0, 100);
+
+ double opacity_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_i", 0, 0, 100);
+ double opacity_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_j", 0, 0, 100);
+ bool opacity_alternatei = prefs->getBool(prefs_path + "opacity_alternatei");
+ bool opacity_alternatej = prefs->getBool(prefs_path + "opacity_alternatej");
+ double opacity_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_rand", 0, 0, 100);
+
+ Glib::ustring initial_color = prefs->getString(prefs_path + "initial_color");
+ double hue_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_j", 0, -100, 100);
+ double hue_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_i", 0, -100, 100);
+ double hue_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_rand", 0, 0, 100);
+ double saturation_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_j", 0, -100, 100);
+ double saturation_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_i", 0, -100, 100);
+ double saturation_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_rand", 0, 0, 100);
+ double lightness_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_j", 0, -100, 100);
+ double lightness_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_i", 0, -100, 100);
+ double lightness_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_rand", 0, 0, 100);
+ bool color_alternatej = prefs->getBool(prefs_path + "color_alternatej");
+ bool color_alternatei = prefs->getBool(prefs_path + "color_alternatei");
+
+ int type = prefs->getInt(prefs_path + "symmetrygroup", 0);
+ bool keepbbox = prefs->getBool(prefs_path + "keepbbox", true);
+ int imax = prefs->getInt(prefs_path + "imax", 2);
+ int jmax = prefs->getInt(prefs_path + "jmax", 2);
+
+ bool fillrect = prefs->getBool(prefs_path + "fillrect");
+ double fillwidth = scale_units*prefs->getDoubleLimited(prefs_path + "fillwidth", 50, 0, 1e6);
+ double fillheight = scale_units*prefs->getDoubleLimited(prefs_path + "fillheight", 50, 0, 1e6);
+
+ bool dotrace = prefs->getBool(prefs_path + "dotrace");
+ int pick = prefs->getInt(prefs_path + "pick");
+ bool pick_to_presence = prefs->getBool(prefs_path + "pick_to_presence");
+ bool pick_to_size = prefs->getBool(prefs_path + "pick_to_size");
+ bool pick_to_color = prefs->getBool(prefs_path + "pick_to_color");
+ bool pick_to_opacity = prefs->getBool(prefs_path + "pick_to_opacity");
+ double rand_picked = 0.01 * prefs->getDoubleLimited(prefs_path + "rand_picked", 0, 0, 100);
+ bool invert_picked = prefs->getBool(prefs_path + "invert_picked");
+ double gamma_picked = prefs->getDoubleLimited(prefs_path + "gamma_picked", 0, -10, 10);
+
+ auto item = cast<SPItem>(obj);
+ if (dotrace) {
+ trace_setup(getDocument(), 1.0, item);
+ }
+
+ Geom::Point center;
+ double w = 0;
+ double h = 0;
+ double x0 = 0;
+ double y0 = 0;
+
+ if (keepbbox &&
+ obj_repr->attribute("inkscape:tile-w") &&
+ obj_repr->attribute("inkscape:tile-h") &&
+ obj_repr->attribute("inkscape:tile-x0") &&
+ obj_repr->attribute("inkscape:tile-y0") &&
+ obj_repr->attribute("inkscape:tile-cx") &&
+ obj_repr->attribute("inkscape:tile-cy")) {
+
+ double cx = obj_repr->getAttributeDouble("inkscape:tile-cx", 0);
+ double cy = obj_repr->getAttributeDouble("inkscape:tile-cy", 0);
+ center = Geom::Point (cx, cy);
+
+ w = obj_repr->getAttributeDouble("inkscape:tile-w", w);
+ h = obj_repr->getAttributeDouble("inkscape:tile-h", h);
+ x0 = obj_repr->getAttributeDouble("inkscape:tile-x0", x0);
+ y0 = obj_repr->getAttributeDouble("inkscape:tile-y0", y0);
+ } else {
+ bool prefs_bbox = prefs->getBool("/tools/bounding_box", false);
+ SPItem::BBoxType bbox_type = ( !prefs_bbox ?
+ SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX );
+ Geom::OptRect r = item->documentBounds(bbox_type);
+ if (r) {
+ w = scale_units*r->dimensions()[Geom::X];
+ h = scale_units*r->dimensions()[Geom::Y];
+ x0 = scale_units*r->min()[Geom::X];
+ y0 = scale_units*r->min()[Geom::Y];
+ center = scale_units*desktop->dt2doc(item->getCenter());
+
+ obj_repr->setAttributeSvgDouble("inkscape:tile-cx", center[Geom::X]);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-cy", center[Geom::Y]);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-w", w);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-h", h);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-x0", x0);
+ obj_repr->setAttributeSvgDouble("inkscape:tile-y0", y0);
+ } else {
+ center = Geom::Point(0, 0);
+ w = h = 0;
+ x0 = y0 = 0;
+ }
+ }
+
+ Geom::Point cur(0, 0);
+ Geom::Rect bbox_original (Geom::Point (x0, y0), Geom::Point (x0 + w, y0 + h));
+ double perimeter_original = (w + h)/4;
+
+ // The integers i and j are reserved for tile column and row.
+ // The doubles x and y are used for coordinates
+ for (int i = 0;
+ fillrect?
+ (fabs(cur[Geom::X]) < fillwidth && i < 200) // prevent "freezing" with too large fillrect, arbitrarily limit rows
+ : (i < imax);
+ i ++) {
+ for (int j = 0;
+ fillrect?
+ (fabs(cur[Geom::Y]) < fillheight && j < 200) // prevent "freezing" with too large fillrect, arbitrarily limit cols
+ : (j < jmax);
+ j ++) {
+
+ // Note: We create a clone at 0,0 too, right over the original, in case our clones are colored
+
+ // Get transform from symmetry, shift, scale, rotation
+ Geom::Affine orig_t = get_transform (type, i, j, center[Geom::X], center[Geom::Y], w, h,
+ shiftx_per_i, shifty_per_i,
+ shiftx_per_j, shifty_per_j,
+ shiftx_rand, shifty_rand,
+ shiftx_exp, shifty_exp,
+ shiftx_alternate, shifty_alternate,
+ shiftx_cumulate, shifty_cumulate,
+ shiftx_excludew, shifty_excludeh,
+ scalex_per_i, scaley_per_i,
+ scalex_per_j, scaley_per_j,
+ scalex_rand, scaley_rand,
+ scalex_exp, scaley_exp,
+ scalex_log, scaley_log,
+ scalex_alternate, scaley_alternate,
+ scalex_cumulate, scaley_cumulate,
+ rotate_per_i, rotate_per_j,
+ rotate_rand,
+ rotate_alternatei, rotate_alternatej,
+ rotate_cumulatei, rotate_cumulatej );
+ Geom::Affine parent_transform = (((SPItem*)item->parent)->i2doc_affine())*(item->document->getRoot()->c2p.inverse());
+ Geom::Affine t = parent_transform*orig_t*parent_transform.inverse();
+ cur = center * t - center;
+ if (fillrect) {
+ if ((cur[Geom::X] > fillwidth) || (cur[Geom::Y] > fillheight)) { // off limits
+ continue;
+ }
+ }
+
+ gchar color_string[32]; *color_string = 0;
+
+ // Color tab
+ if (!initial_color.empty()) {
+ guint32 rgba = sp_svg_read_color (initial_color.data(), 0x000000ff);
+ float hsl[3];
+ SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+
+ double eff_i = (color_alternatei? (i%2) : (i));
+ double eff_j = (color_alternatej? (j%2) : (j));
+
+ hsl[0] += hue_per_i * eff_i + hue_per_j * eff_j + hue_rand * g_random_double_range (-1, 1);
+ double notused;
+ hsl[0] = modf( hsl[0], &notused ); // Restrict to 0-1
+ hsl[1] += saturation_per_i * eff_i + saturation_per_j * eff_j + saturation_rand * g_random_double_range (-1, 1);
+ hsl[1] = CLAMP (hsl[1], 0, 1);
+ hsl[2] += lightness_per_i * eff_i + lightness_per_j * eff_j + lightness_rand * g_random_double_range (-1, 1);
+ hsl[2] = CLAMP (hsl[2], 0, 1);
+
+ float rgb[3];
+ SPColor::hsl_to_rgb_floatv (rgb, hsl[0], hsl[1], hsl[2]);
+ sp_svg_write_color(color_string, sizeof(color_string), SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1.0));
+ }
+
+ // Blur
+ double blur = 0.0;
+ {
+ int eff_i = (blur_alternatei? (i%2) : (i));
+ int eff_j = (blur_alternatej? (j%2) : (j));
+ blur = (blur_per_i * eff_i + blur_per_j * eff_j + blur_rand * g_random_double_range (-1, 1));
+ blur = CLAMP (blur, 0, 1);
+ }
+
+ // Opacity
+ double opacity = 1.0;
+ {
+ int eff_i = (opacity_alternatei? (i%2) : (i));
+ int eff_j = (opacity_alternatej? (j%2) : (j));
+ opacity = 1 - (opacity_per_i * eff_i + opacity_per_j * eff_j + opacity_rand * g_random_double_range (-1, 1));
+ opacity = CLAMP (opacity, 0, 1);
+ }
+
+ // Trace tab
+ if (dotrace) {
+ Geom::Rect bbox_t = transform_rect (bbox_original, t*Geom::Scale(1.0/scale_units));
+
+ guint32 rgba = trace_pick (bbox_t);
+ float r = SP_RGBA32_R_F(rgba);
+ float g = SP_RGBA32_G_F(rgba);
+ float b = SP_RGBA32_B_F(rgba);
+ float a = SP_RGBA32_A_F(rgba);
+
+ float hsl[3];
+ SPColor::rgb_to_hsl_floatv (hsl, r, g, b);
+
+ gdouble val = 0;
+ switch (pick) {
+ case PICK_COLOR:
+ val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max
+ break;
+ case PICK_OPACITY:
+ val = a;
+ break;
+ case PICK_R:
+ val = r;
+ break;
+ case PICK_G:
+ val = g;
+ break;
+ case PICK_B:
+ val = b;
+ break;
+ case PICK_H:
+ val = hsl[0];
+ break;
+ case PICK_S:
+ val = hsl[1];
+ break;
+ case PICK_L:
+ val = 1 - hsl[2];
+ break;
+ default:
+ break;
+ }
+
+ if (rand_picked > 0) {
+ val = randomize01 (val, rand_picked);
+ r = randomize01 (r, rand_picked);
+ g = randomize01 (g, rand_picked);
+ b = randomize01 (b, rand_picked);
+ }
+
+ if (gamma_picked != 0) {
+ double power;
+ if (gamma_picked > 0)
+ power = 1/(1 + fabs(gamma_picked));
+ else
+ power = 1 + fabs(gamma_picked);
+
+ val = pow (val, power);
+ r = pow (r, power);
+ g = pow (g, power);
+ b = pow (b, power);
+ }
+
+ if (invert_picked) {
+ val = 1 - val;
+ r = 1 - r;
+ g = 1 - g;
+ b = 1 - b;
+ }
+
+ val = CLAMP (val, 0, 1);
+ r = CLAMP (r, 0, 1);
+ g = CLAMP (g, 0, 1);
+ b = CLAMP (b, 0, 1);
+
+ // recompose tweaked color
+ rgba = SP_RGBA32_F_COMPOSE(r, g, b, a);
+
+ if (pick_to_presence) {
+ if (g_random_double_range (0, 1) > val) {
+ continue; // skip!
+ }
+ }
+ if (pick_to_size) {
+ t = parent_transform * Geom::Translate(-center[Geom::X], -center[Geom::Y])
+ * Geom::Scale (val, val) * Geom::Translate(center[Geom::X], center[Geom::Y])
+ * parent_transform.inverse() * t;
+ }
+ if (pick_to_opacity) {
+ opacity *= val;
+ }
+ if (pick_to_color) {
+ sp_svg_write_color(color_string, sizeof(color_string), rgba);
+ }
+ }
+
+ if (opacity < 1e-6) { // invisibly transparent, skip
+ continue;
+ }
+
+ if (fabs(t[0]) + fabs (t[1]) + fabs(t[2]) + fabs(t[3]) < 1e-6) { // too small, skip
+ continue;
+ }
+
+ // Create the clone
+ Inkscape::XML::Node *clone = obj_repr->document()->createElement("svg:use");
+ clone->setAttribute("x", "0");
+ clone->setAttribute("y", "0");
+ clone->setAttribute("inkscape:tiled-clone-of", id_href);
+ clone->setAttribute("xlink:href", id_href);
+
+ Geom::Point new_center;
+ bool center_set = false;
+ if (obj_repr->attribute("inkscape:transform-center-x") || obj_repr->attribute("inkscape:transform-center-y")) {
+ new_center = scale_units*desktop->dt2doc(item->getCenter()) * orig_t;
+ center_set = true;
+ }
+
+ clone->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(t));
+
+ if (opacity < 1.0) {
+ clone->setAttributeCssDouble("opacity", opacity);
+ }
+
+ if (*color_string) {
+ clone->setAttribute("fill", color_string);
+ clone->setAttribute("stroke", color_string);
+ }
+
+ // add the new clone to the top of the original's parent
+ parent->getRepr()->appendChild(clone);
+
+ if (blur > 0.0) {
+ SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone);
+ auto item = cast<SPItem>(clone_object);
+ double radius = blur * perimeter_original * t.descrim();
+ // this is necessary for all newly added clones to have correct bboxes,
+ // otherwise filters won't work:
+ desktop->getDocument()->ensureUpToDate();
+ SPFilter *constructed = new_filter_gaussian_blur(desktop->getDocument(), radius, t.descrim());
+ constructed->update_filter_region(item);
+ sp_style_set_property_url (clone_object, "filter", constructed, false);
+ }
+
+ if (center_set) {
+ SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone);
+ auto item = cast<SPItem>(clone_object);
+ if (clone_object && item) {
+ clone_object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ item->setCenter(desktop->doc2dt(new_center));
+ clone_object->updateRepr();
+ }
+ }
+
+ Inkscape::GC::release(clone);
+ }
+ cur[Geom::Y] = 0;
+ }
+
+ if (dotrace) {
+ trace_finish ();
+ }
+
+ change_selection(selection);
+
+ desktop->clearWaitingCursor();
+ DocumentUndo::done(getDocument(), _("Create tiled clones"), INKSCAPE_ICON("dialog-tile-clones"));
+}
+
+Gtk::Box * CloneTiler::new_tab(Gtk::Notebook *nb, const gchar *label)
+{
+ auto l = Gtk::manage(new Gtk::Label(label, true));
+ auto vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, VB_MARGIN));
+ vb->set_homogeneous(false);
+ vb->set_border_width(VB_MARGIN);
+ nb->append_page(*vb, *l);
+ return vb;
+}
+
+void CloneTiler::checkbox_toggled(Gtk::ToggleButton *tb,
+ const Glib::ustring &attr)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(prefs_path + attr, tb->get_active());
+}
+
+Gtk::Widget * CloneTiler::checkbox(const char *tip,
+ const Glib::ustring &attr)
+{
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN));
+ auto b = Gtk::manage(new UI::Widget::CheckButtonInternal());
+ b->set_tooltip_text(tip);
+
+ auto const prefs = Inkscape::Preferences::get();
+ auto const value = prefs->getBool(prefs_path + attr);
+ b->set_active(value);
+
+ hb->pack_start(*b, false, true);
+ b->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::checkbox_toggled), b, attr));
+
+ b->set_uncheckable();
+
+ return hb;
+}
+
+void CloneTiler::value_changed(Glib::RefPtr<Gtk::Adjustment> &adj,
+ Glib::ustring const &pref)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble(prefs_path + pref, adj->get_value());
+}
+
+Gtk::Widget * CloneTiler::spinbox(const char *tip,
+ const Glib::ustring &attr,
+ double lower,
+ double upper,
+ const gchar *suffix,
+ bool exponent/* = false*/)
+{
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+
+ {
+ // Parameters for adjustment
+ auto const initial_value = (exponent ? 1.0 : 0.0);
+ auto const step_increment = (exponent ? 0.01 : 0.1);
+ auto const page_increment = (exponent ? 0.05 : 0.4);
+
+ auto a = Gtk::Adjustment::create(initial_value,
+ lower,
+ upper,
+ step_increment,
+ page_increment);
+
+ auto const climb_rate = (exponent ? 0.01 : 0.1);
+ auto const digits = (exponent ? 2 : 1);
+
+ auto sb = new Inkscape::UI::Widget::SpinButton(a, climb_rate, digits);
+
+ sb->set_tooltip_text (tip);
+ sb->set_width_chars (5);
+ sb->set_digits(3);
+ hb->pack_start(*sb, false, false, SB_MARGIN);
+
+ auto prefs = Inkscape::Preferences::get();
+ auto value = prefs->getDoubleLimited(prefs_path + attr, exponent? 1.0 : 0.0, lower, upper);
+ a->set_value (value);
+ a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::value_changed), a, attr));
+
+ if (exponent) {
+ sb->set_oneable();
+ } else {
+ sb->set_zeroable();
+ }
+ }
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(suffix);
+ hb->pack_start(*l);
+ }
+
+ return hb;
+}
+
+void CloneTiler::symgroup_changed(Gtk::ComboBox *cb)
+{
+ auto prefs = Inkscape::Preferences::get();
+ auto group_new = cb->get_active_row_number();
+ prefs->setInt(prefs_path + "symmetrygroup", group_new);
+}
+
+void CloneTiler::xy_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt(prefs_path + pref, (int) floor(adj->get_value() + 0.5));
+}
+
+void CloneTiler::keep_bbox_toggled()
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(prefs_path + "keepbbox", _cb_keep_bbox->get_active());
+}
+
+void CloneTiler::pick_to(Gtk::ToggleButton *tb, Glib::ustring const &pref)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(prefs_path + pref, tb->get_active());
+}
+
+
+void CloneTiler::reset_recursive(Gtk::Widget *w)
+{
+ if (w) {
+ auto sb = dynamic_cast<Inkscape::UI::Widget::SpinButton *>(w);
+ auto tb = dynamic_cast<Inkscape::UI::Widget::CheckButtonInternal *>(w);
+
+ {
+ if (sb && sb->get_zeroable()) { // spinbutton
+ auto a = sb->get_adjustment();
+ a->set_value(0);
+ }
+ }
+ {
+ if (sb && sb->get_oneable()) { // spinbutton
+ auto a = sb->get_adjustment();
+ a->set_value(1);
+ }
+ }
+ {
+ if (tb && tb->get_uncheckable()) { // checkbox
+ tb->set_active(false);
+ }
+ }
+ }
+
+ auto container = dynamic_cast<Gtk::Container *>(w);
+
+ if (container) {
+ auto c = container->get_children();
+ for (auto i : c) {
+ reset_recursive(i);
+ }
+ }
+}
+
+void CloneTiler::reset()
+{
+ reset_recursive(this);
+}
+
+void CloneTiler::table_attach(Gtk::Grid *table, Gtk::Widget *widget, float align, int row, int col)
+{
+ widget->set_halign(Gtk::ALIGN_FILL);
+ widget->set_valign(Gtk::ALIGN_CENTER);
+ table->attach(*widget, col, row, 1, 1);
+}
+
+Gtk::Grid * CloneTiler::table_x_y_rand(int values)
+{
+ auto table = Gtk::manage(new Gtk::Grid());
+ table->set_row_spacing(6);
+ table->set_column_spacing(8);
+
+ table->set_border_width(VB_MARGIN);
+
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb->set_homogeneous(false);
+
+ auto i = Glib::wrap(sp_get_icon_image("object-rows", GTK_ICON_SIZE_MENU));
+ hb->pack_start(*i, false, false, 2);
+
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<small>Per row:</small>"));
+ hb->pack_start(*l, false, false, 2);
+
+ table_attach(table, hb, 0, 1, 2);
+ }
+
+ {
+ auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ hb->set_homogeneous(false);
+
+ auto i = Glib::wrap(sp_get_icon_image("object-columns", GTK_ICON_SIZE_MENU));
+ hb->pack_start(*i, false, false, 2);
+
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<small>Per column:</small>"));
+ hb->pack_start(*l, false, false, 2);
+
+ table_attach(table, hb, 0, 1, 3);
+ }
+
+ {
+ auto l = Gtk::manage(new Gtk::Label(""));
+ l->set_markup(_("<small>Randomize:</small>"));
+ table_attach(table, l, 0, 1, 4);
+ }
+
+ return table;
+}
+
+void CloneTiler::pick_switched(PickType v)
+{
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setInt(prefs_path + "pick", v);
+}
+
+void CloneTiler::switch_to_create()
+{
+ for (auto w : _rowscols) {
+ w->set_sensitive(true);
+ }
+ for (auto w : _widthheight) {
+ w->set_sensitive(false);
+ }
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(prefs_path + "fillrect", false);
+}
+
+
+void CloneTiler::switch_to_fill()
+{
+ for (auto w : _rowscols) {
+ w->set_sensitive(false);
+ }
+ for (auto w : _widthheight) {
+ w->set_sensitive(true);
+ }
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool(prefs_path + "fillrect", true);
+}
+
+void CloneTiler::fill_width_changed()
+{
+ auto const raw_dist = fill_width->get_value();
+ auto const unit = unit_menu->getUnit();
+ auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px");
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble(prefs_path + "fillwidth", pixels);
+}
+
+void CloneTiler::fill_height_changed()
+{
+ auto const raw_dist = fill_height->get_value();
+ auto const unit = unit_menu->getUnit();
+ auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px");
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setDouble(prefs_path + "fillheight", pixels);
+}
+
+void CloneTiler::unit_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gdouble width_pixels = prefs->getDouble(prefs_path + "fillwidth");
+ gdouble height_pixels = prefs->getDouble(prefs_path + "fillheight");
+
+ Inkscape::Util::Unit const *unit = unit_menu->getUnit();
+
+ gdouble width_value = Inkscape::Util::Quantity::convert(width_pixels, "px", unit);
+ gdouble height_value = Inkscape::Util::Quantity::convert(height_pixels, "px", unit);
+ fill_width->set_value(width_value);
+ fill_height->set_value(height_value);
+}
+
+void CloneTiler::do_pick_toggled()
+{
+ auto prefs = Inkscape::Preferences::get();
+ auto active = _b->get_active();
+ prefs->setBool(prefs_path + "dotrace", active);
+
+ if (_dotrace) {
+ _dotrace->set_sensitive(active);
+ }
+}
+
+void CloneTiler::show_page_trace()
+{
+ nb->set_current_page(6);
+ _b->set_active(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/dialog/clonetiler.h b/src/ui/dialog/clonetiler.h
new file mode 100644
index 0000000..f6ef6e0
--- /dev/null
+++ b/src/ui/dialog/clonetiler.h
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Clone tiling dialog
+ */
+/* Authors:
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 2004 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef __SP_CLONE_TILER_H__
+#define __SP_CLONE_TILER_H__
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/color-picker.h"
+
+namespace Gtk {
+ class Box;
+ class ComboBox;
+ class Grid;
+ class Notebook;
+ class SizeGroup;
+ class ToggleButton;
+}
+
+class SPItem;
+class SPObject;
+
+namespace Geom {
+ class Rect;
+ class Affine;
+}
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+ class CheckButtonInternal;
+ class UnitMenu;
+}
+
+namespace Dialog {
+
+class CloneTiler : public DialogBase
+{
+public:
+ CloneTiler();
+ ~CloneTiler() override;
+
+ void show_page_trace();
+
+protected:
+ enum PickType
+ {
+ PICK_COLOR,
+ PICK_OPACITY,
+ PICK_R,
+ PICK_G,
+ PICK_B,
+ PICK_H,
+ PICK_S,
+ PICK_L
+ };
+
+ Gtk::Box * new_tab(Gtk::Notebook *nb, const gchar *label);
+ Gtk::Grid * table_x_y_rand(int values);
+ Gtk::Widget * spinbox(const char *tip,
+ const Glib::ustring &attr,
+ double lower,
+ double upper,
+ const gchar *suffix,
+ bool exponent = false);
+ Gtk::Widget * checkbox(const char *tip,
+ const Glib::ustring &attr);
+ void table_attach(Gtk::Grid *table, Gtk::Widget *widget, float align, int row, int col);
+
+ void symgroup_changed(Gtk::ComboBox *cb);
+ void on_picker_color_changed(guint rgba);
+ void trace_hide_tiled_clones_recursively(SPObject *from);
+ guint number_of_clones(SPObject *obj);
+ void trace_setup(SPDocument *doc, gdouble zoom, SPItem *original);
+ guint32 trace_pick(Geom::Rect box);
+ void trace_finish();
+ bool is_a_clone_of(SPObject *tile, SPObject *obj);
+ Geom::Rect transform_rect(Geom::Rect const &r, Geom::Affine const &m);
+ double randomize01(double val, double rand);
+
+ void apply();
+ void change_selection(Inkscape::Selection *selection);
+ void checkbox_toggled(Gtk::ToggleButton *tb,
+ Glib::ustring const &attr);
+ void do_pick_toggled();
+ void external_change();
+ void fill_width_changed();
+ void fill_height_changed();
+ void keep_bbox_toggled();
+ void on_remove_button_clicked() {remove();}
+ void pick_switched(PickType);
+ void pick_to(Gtk::ToggleButton *tb,
+ Glib::ustring const &pref);
+ void remove(bool do_undo = true);
+ void reset();
+ void reset_recursive(Gtk::Widget *w);
+ void switch_to_create();
+ void switch_to_fill();
+ void unclump();
+ void unit_changed();
+ void value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref);
+ void xy_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref);
+
+ Geom::Affine get_transform(
+ // symmetry group
+ int type,
+
+ // row, column
+ int i, int j,
+
+ // center, width, height of the tile
+ double cx, double cy,
+ double w, double h,
+
+ // values from the dialog:
+ // Shift
+ double shiftx_per_i, double shifty_per_i,
+ double shiftx_per_j, double shifty_per_j,
+ double shiftx_rand, double shifty_rand,
+ double shiftx_exp, double shifty_exp,
+ int shiftx_alternate, int shifty_alternate,
+ int shiftx_cumulate, int shifty_cumulate,
+ int shiftx_excludew, int shifty_excludeh,
+
+ // Scale
+ double scalex_per_i, double scaley_per_i,
+ double scalex_per_j, double scaley_per_j,
+ double scalex_rand, double scaley_rand,
+ double scalex_exp, double scaley_exp,
+ double scalex_log, double scaley_log,
+ int scalex_alternate, int scaley_alternate,
+ int scalex_cumulate, int scaley_cumulate,
+
+ // Rotation
+ double rotate_per_i, double rotate_per_j,
+ double rotate_rand,
+ int rotate_alternatei, int rotate_alternatej,
+ int rotate_cumulatei, int rotate_cumulatej
+ );
+
+
+private:
+ UI::Widget::CheckButtonInternal *_b;
+ UI::Widget::CheckButtonInternal *_cb_keep_bbox;
+ Gtk::Notebook *nb = nullptr;
+ Inkscape::UI::Widget::ColorPicker *color_picker;
+ Glib::RefPtr<Gtk::SizeGroup> table_row_labels;
+ Inkscape::UI::Widget::UnitMenu *unit_menu;
+
+ Glib::RefPtr<Gtk::Adjustment> fill_width;
+ Glib::RefPtr<Gtk::Adjustment> fill_height;
+
+ sigc::connection selectChangedConn;
+ sigc::connection externChangedConn;
+ sigc::connection color_changed_connection;
+ sigc::connection unitChangedConn;
+
+ Gtk::Box *_buttons_on_tiles;
+ Gtk::Box *_dotrace;
+ Gtk::Label *_status;
+ std::vector<Gtk::Widget*> _rowscols;
+ std::vector<Gtk::Widget*> _widthheight;
+};
+
+enum {
+ TILE_P1,
+ TILE_P2,
+ TILE_PM,
+ TILE_PG,
+ TILE_CM,
+ TILE_PMM,
+ TILE_PMG,
+ TILE_PGG,
+ TILE_CMM,
+ TILE_P4,
+ TILE_P4M,
+ TILE_P4G,
+ TILE_P3,
+ TILE_P31M,
+ TILE_P3M1,
+ TILE_P6,
+ TILE_P6M
+};
+
+} // 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/dialog/color-item.cpp b/src/ui/dialog/color-item.cpp
new file mode 100644
index 0000000..6ed95c4
--- /dev/null
+++ b/src/ui/dialog/color-item.cpp
@@ -0,0 +1,506 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "color-item.h"
+
+#include <cstdint>
+#include <cairomm/cairomm.h>
+#include <glibmm/convert.h>
+#include <glibmm/i18n.h>
+#include <gdkmm/general.h>
+
+#include "helper/sigc-track-obj.h"
+#include "inkscape-preferences.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "object/sp-gradient.h"
+#include "svg/svg-color.h"
+#include "hsluv.h"
+#include "display/cairo-utils.h"
+#include "desktop-style.h"
+#include "actions/actions-tools.h"
+#include "message-context.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/icon-names.h"
+
+namespace {
+
+class Globals
+{
+ Globals()
+ {
+ load_removecolor();
+ load_mimetargets();
+ }
+
+ void load_removecolor()
+ {
+ auto path_utf8 = (Glib::ustring)Inkscape::IO::Resource::get_path(Inkscape::IO::Resource::SYSTEM, Inkscape::IO::Resource::PIXMAPS, "remove-color.png");
+ auto path = Glib::filename_from_utf8(path_utf8);
+ auto pixbuf = Gdk::Pixbuf::create_from_file(path);
+ if (!pixbuf) {
+ g_warning("Null pixbuf for %p [%s]", path.c_str(), path.c_str());
+ }
+ removecolor = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, 1);
+ }
+
+ void load_mimetargets()
+ {
+ auto &mimetypes = PaintDef::getMIMETypes();
+ mimetargets.reserve(mimetypes.size());
+ for (int i = 0; i < mimetypes.size(); i++) {
+ mimetargets.emplace_back(mimetypes[i], (Gtk::TargetFlags)0, i);
+ }
+ }
+
+public:
+ static Globals &get()
+ {
+ static Globals instance;
+ return instance;
+ }
+
+ // The "remove-color" image.
+ Cairo::RefPtr<Cairo::ImageSurface> removecolor;
+
+ // The MIME targets for drag and drop, in the format expected by GTK.
+ std::vector<Gtk::TargetEntry> mimetargets;
+};
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ColorItem::ColorItem(PaintDef const &paintdef, DialogBase *dialog)
+ : dialog(dialog)
+{
+ if (paintdef.get_type() == PaintDef::RGB) {
+ pinned_default = false;
+ data = RGBData{paintdef.get_rgb()};
+ } else {
+ pinned_default = true;
+ data = NoneData{};
+ }
+ description = paintdef.get_description();
+ color_id = paintdef.get_color_id();
+
+ common_setup();
+}
+
+ColorItem::ColorItem(SPGradient *gradient, DialogBase *dialog)
+ : dialog(dialog)
+{
+ data = GradientData{gradient};
+ description = gradient->defaultLabel();
+ color_id = gradient->getId();
+
+ gradient->connectRelease(SIGC_TRACKING_ADAPTOR([this] (SPObject*) {
+ boost::get<GradientData>(data).gradient = nullptr;
+ }, *this));
+
+ gradient->connectModified(SIGC_TRACKING_ADAPTOR([this] (SPObject *obj, unsigned flags) {
+ if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) {
+ cache_dirty = true;
+ queue_draw();
+ }
+ description = obj->defaultLabel();
+ _signal_modified.emit();
+ if (is_pinned() != was_grad_pinned) {
+ was_grad_pinned = is_pinned();
+ _signal_pinned.emit();
+ }
+ }, *this));
+
+ was_grad_pinned = is_pinned();
+ common_setup();
+}
+
+void ColorItem::common_setup()
+{
+ set_name("ColorItem");
+ set_tooltip_text(description);
+ add_events(Gdk::ENTER_NOTIFY_MASK |
+ Gdk::LEAVE_NOTIFY_MASK |
+ Gdk::BUTTON_PRESS_MASK |
+ Gdk::BUTTON_RELEASE_MASK);
+ drag_source_set(Globals::get().mimetargets, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE | Gdk::ACTION_COPY);
+}
+
+void ColorItem::set_pinned_pref(const std::string &path)
+{
+ pinned_pref = path + "/pinned/" + color_id;
+}
+
+void ColorItem::draw_color(Cairo::RefPtr<Cairo::Context> const &cr, int w, int h) const
+{
+ if (boost::get<NoneData>(&data)) {
+ if (auto surface = Globals::get().removecolor) {
+ const auto device_scale = get_scale_factor();
+ cr->save();
+ cr->scale((double)w / surface->get_width() / device_scale, (double)h / surface->get_height() / device_scale);
+ cr->set_source(surface, 0, 0);
+ cr->paint();
+ cr->restore();
+ }
+ } else if (auto rgbdata = boost::get<RGBData>(&data)) {
+ auto [r, g, b] = rgbdata->rgb;
+ cr->set_source_rgb(r / 255.0, g / 255.0, b / 255.0);
+ cr->paint();
+ } else if (auto graddata = boost::get<GradientData>(&data)) {
+ // Gradient pointer may be null if the gradient was destroyed.
+ auto grad = graddata->gradient;
+ if (!grad) return;
+
+ auto pat_checkerboard = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(ink_cairo_pattern_create_checkerboard(), true));
+ auto pat_gradient = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(grad->create_preview_pattern(w), true));
+
+ cr->set_source(pat_checkerboard);
+ cr->paint();
+ cr->set_source(pat_gradient);
+ cr->paint();
+ }
+}
+
+bool ColorItem::on_draw(Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ auto w = get_width();
+ auto h = get_height();
+
+ // Only using caching for none and gradients. None is included because the image is huge.
+ bool use_cache = boost::get<NoneData>(&data) || boost::get<GradientData>(&data);
+
+ if (use_cache) {
+ auto scale = get_scale_factor();
+ // Ensure cache exists and has correct size.
+ if (!cache || cache->get_width() != w * scale || cache->get_height() != h * scale) {
+ cache = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, w * scale, h * scale);
+ cairo_surface_set_device_scale(cache->cobj(), scale, scale);
+ cache_dirty = true;
+ }
+ // Ensure cache contents is up-to-date.
+ if (cache_dirty) {
+ draw_color(Cairo::Context::create(cache), w * scale, h * scale);
+ cache_dirty = false;
+ }
+ // Paint from cache.
+ cr->set_source(cache, 0, 0);
+ cr->paint();
+ } else {
+ // Paint directly.
+ draw_color(cr, w, h);
+ }
+
+ // Draw fill/stroke indicators.
+ if (is_fill || is_stroke) {
+ double const lightness = Hsluv::rgb_to_perceptual_lightness(average_color());
+ auto [gray, alpha] = Hsluv::get_contrasting_color(lightness);
+ cr->set_source_rgba(gray, gray, gray, alpha);
+
+ // Scale so that the square -1...1 is the biggest possible square centred in the widget.
+ auto minwh = std::min(w, h);
+ cr->translate((w - minwh) / 2.0, (h - minwh) / 2.0);
+ cr->scale(minwh / 2.0, minwh / 2.0);
+ cr->translate(1.0, 1.0);
+
+ if (is_fill) {
+ cr->arc(0.0, 0.0, 0.35, 0.0, 2 * M_PI);
+ cr->fill();
+ }
+
+ if (is_stroke) {
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->arc(0.0, 0.0, 0.65, 0.0, 2 * M_PI);
+ cr->arc(0.0, 0.0, 0.5, 0.0, 2 * M_PI);
+ cr->fill();
+ }
+ }
+
+ return true;
+}
+
+void ColorItem::on_size_allocate(Gtk::Allocation &allocation)
+{
+ Gtk::DrawingArea::on_size_allocate(allocation);
+ cache_dirty = true;
+}
+
+bool ColorItem::on_enter_notify_event(GdkEventCrossing*)
+{
+ mouse_inside = true;
+ if (auto desktop = dialog->getDesktop()) {
+ auto msg = Glib::ustring::compose(_("Color: <b>%1</b>; <b>Click</b> to set fill, <b>Shift+click</b> to set stroke"), description);
+ desktop->tipsMessageContext()->set(Inkscape::INFORMATION_MESSAGE, msg.c_str());
+ }
+ return false;
+}
+
+bool ColorItem::on_leave_notify_event(GdkEventCrossing*)
+{
+ mouse_inside = false;
+ if (auto desktop = dialog->getDesktop()) {
+ desktop->tipsMessageContext()->clear();
+ }
+ return false;
+}
+
+bool ColorItem::on_button_press_event(GdkEventButton *event)
+{
+ if (event->button == 3) {
+ on_rightclick(event);
+ return true;
+ }
+ // Return true necessary to avoid stealing the canvas focus.
+ return true;
+}
+
+bool ColorItem::on_button_release_event(GdkEventButton* event)
+{
+ if (mouse_inside && (event->button == 1 || event->button == 2)) {
+ bool stroke = event->button == 2 || (event->state & GDK_SHIFT_MASK);
+ on_click(stroke);
+ return true;
+ }
+ return false;
+}
+
+void ColorItem::on_click(bool stroke)
+{
+ auto desktop = dialog->getDesktop();
+ if (!desktop) return;
+
+ auto attr_name = stroke ? "stroke" : "fill";
+ auto css = std::unique_ptr<SPCSSAttr, void(*)(SPCSSAttr*)>(sp_repr_css_attr_new(), [] (auto p) {sp_repr_css_attr_unref(p);});
+
+ Glib::ustring descr;
+ if (boost::get<NoneData>(&data)) {
+ sp_repr_css_set_property(css.get(), attr_name, "none");
+ descr = stroke ? _("Set stroke color to none") : _("Set fill color to none");
+ } else if (auto rgbdata = boost::get<RGBData>(&data)) {
+ auto [r, g, b] = rgbdata->rgb;
+ uint32_t rgba = (r << 24) | (g << 16) | (b << 8) | 0xff;
+ char buf[64];
+ sp_svg_write_color(buf, sizeof(buf), rgba);
+ sp_repr_css_set_property(css.get(), attr_name, buf);
+ descr = stroke ? _("Set stroke color from swatch") : _("Set fill color from swatch");
+ } else if (auto graddata = boost::get<GradientData>(&data)) {
+ auto grad = graddata->gradient;
+ if (!grad) return;
+ auto colorspec = "url(#" + Glib::ustring(grad->getId()) + ")";
+ sp_repr_css_set_property(css.get(), attr_name, colorspec.c_str());
+ descr = stroke ? _("Set stroke color from swatch") : _("Set fill color from swatch");
+ }
+
+ sp_desktop_set_style(desktop, css.get());
+
+ DocumentUndo::done(desktop->getDocument(), descr.c_str(), INKSCAPE_ICON("swatches"));
+}
+
+void ColorItem::on_rightclick(GdkEventButton *event)
+{
+ auto menu_gobj = gtk_menu_new(); /* C */
+ auto menu = Glib::wrap(GTK_MENU(menu_gobj)); /* C */
+
+ auto additem = [&, this] (Glib::ustring const &name, sigc::slot<void()> slot) {
+ auto item = Gtk::make_managed<Gtk::MenuItem>(name);
+ menu->append(*item);
+ item->signal_activate().connect(SIGC_TRACKING_ADAPTOR(slot, *this));
+ };
+
+ // TRANSLATORS: An item in context menu on a colour in the swatches
+ additem(_("Set fill"), [this] { on_click(false); });
+ additem(_("Set stroke"), [this] { on_click(true); });
+
+ if (boost::get<GradientData>(&data)) {
+ menu->append(*Gtk::make_managed<Gtk::SeparatorMenuItem>());
+
+ additem(_("Delete"), [this] {
+ auto grad = boost::get<GradientData>(data).gradient;
+ if (!grad) return;
+
+ grad->setSwatch(false);
+ DocumentUndo::done(grad->document, _("Delete swatch"), INKSCAPE_ICON("color-gradient"));
+ });
+
+ additem(_("Edit..."), [this] {
+ auto grad = boost::get<GradientData>(data).gradient;
+ if (!grad) return;
+
+ auto desktop = dialog->getDesktop();
+ auto selection = desktop->getSelection();
+ auto items = std::vector<SPItem*>(selection->items().begin(), selection->items().end());
+
+ if (!items.empty()) {
+ auto query = SPStyle(desktop->doc());
+ int result = objects_query_fillstroke(items, &query, true);
+ if (result == QUERY_STYLE_MULTIPLE_SAME || result == QUERY_STYLE_SINGLE) {
+ if (query.fill.isPaintserver()) {
+ if (cast<SPGradient>(query.getFillPaintServer()) == grad) {
+ desktop->getContainer()->new_dialog("FillStroke");
+ return;
+ }
+ }
+ }
+ }
+
+ // Otherwise, invoke the gradient tool.
+ set_active_tool(desktop, "Gradient");
+ });
+ }
+
+ additem(is_pinned() ? _("Unpin Color") : _("Pin Color"), [this] {
+ if (boost::get<GradientData>(&data)) {
+ auto grad = boost::get<GradientData>(data).gradient;
+ if (!grad) return;
+
+ grad->setPinned(!is_pinned());
+ DocumentUndo::done(grad->document, is_pinned() ? _("Pin swatch") : _("Unpin swatch"), INKSCAPE_ICON("color-gradient"));
+ } else {
+ Inkscape::Preferences::get()->setBool(pinned_pref, !is_pinned());
+ }
+ });
+
+ Gtk::Menu *convert_submenu = nullptr;
+
+ auto create_convert_submenu = [&] {
+ menu->append(*Gtk::make_managed<Gtk::SeparatorMenuItem>());
+
+ auto convert_item = Gtk::make_managed<Gtk::MenuItem>(_("Convert"));
+ menu->append(*convert_item);
+
+ convert_submenu = Gtk::make_managed<Gtk::Menu>();
+ convert_item->set_submenu(*convert_submenu);
+ };
+
+ auto add_convert_subitem = [&, this] (Glib::ustring const &name, sigc::slot<void()> slot) {
+ if (!convert_submenu) {
+ create_convert_submenu();
+ }
+
+ auto item = Gtk::make_managed<Gtk::MenuItem>(name);
+ convert_submenu->append(*item);
+ item->signal_activate().connect(slot);
+ };
+
+ auto grads = dialog->getDesktop()->getDocument()->getResourceList("gradient");
+ for (auto obj : grads) {
+ auto grad = static_cast<SPGradient*>(obj);
+ if (grad->hasStops() && !grad->isSwatch()) {
+ add_convert_subitem(grad->getId(), [name = grad->getId(), this] {
+ auto doc = dialog->getDesktop()->getDocument();
+ auto grads = doc->getResourceList("gradient");
+ for (auto obj : grads) {
+ auto grad = static_cast<SPGradient*>(obj);
+ if (grad->getId() == name) {
+ grad->setSwatch();
+ DocumentUndo::done(doc, _("Add gradient stop"), INKSCAPE_ICON("color-gradient"));
+ }
+ }
+ });
+ }
+ }
+
+ menu->show_all();
+ menu->popup_at_pointer(reinterpret_cast<GdkEvent*>(event));
+
+ // Todo: All lines marked /* C */ in this function are required in order for the menu to
+ // self-destruct after it has finished. Please replace upon discovery of a better method.
+ g_object_ref_sink(menu_gobj); /* C */
+ g_object_unref(menu_gobj); /* C */
+}
+
+PaintDef ColorItem::to_paintdef() const
+{
+ if (boost::get<NoneData>(&data)) {
+ return PaintDef();
+ } else if (auto rgbdata = boost::get<RGBData>(&data)) {
+ return PaintDef(rgbdata->rgb, description);
+ } else if (boost::get<GradientData>(&data)) {
+ auto grad = boost::get<GradientData>(data).gradient;
+ return PaintDef({0, 0, 0}, grad->getId());
+ }
+
+ // unreachable
+ return {};
+}
+
+void ColorItem::on_drag_data_get(Glib::RefPtr<Gdk::DragContext> const &context, Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ auto &mimetypes = PaintDef::getMIMETypes();
+ if (info < 0 || info >= mimetypes.size()) {
+ g_warning("ERROR: unknown value (%d)", info);
+ return;
+ }
+ auto &key = mimetypes[info];
+
+ auto def = to_paintdef();
+ auto [vec, format] = def.getMIMEData(key);
+ if (vec.empty()) return;
+
+ selection_data.set(key, format, reinterpret_cast<guint8 const*>(vec.data()), vec.size());
+}
+
+void ColorItem::on_drag_begin(Glib::RefPtr<Gdk::DragContext> const &context)
+{
+ constexpr int w = 32;
+ constexpr int h = 24;
+
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, w, h);
+ draw_color(Cairo::Context::create(surface), w, h);
+
+ context->set_icon(Gdk::Pixbuf::create(surface, 0, 0, w, h), 0, 0);
+}
+
+void ColorItem::set_fill(bool b)
+{
+ is_fill = b;
+ queue_draw();
+}
+
+void ColorItem::set_stroke(bool b)
+{
+ is_stroke = b;
+ queue_draw();
+}
+
+bool ColorItem::is_pinned() const
+{
+ if (boost::get<GradientData>(&data)) {
+ auto grad = boost::get<GradientData>(data).gradient;
+ if (!grad) {
+ return false;
+ }
+ return grad->isPinned();
+ } else {
+ return Inkscape::Preferences::get()->getBool(pinned_pref, pinned_default);
+ }
+}
+
+std::array<double, 3> ColorItem::average_color() const
+{
+ if (boost::get<NoneData>(&data)) {
+ return {1.0, 1.0, 1.0};
+ } else if (auto rgbdata = boost::get<RGBData>(&data)) {
+ auto [r, g, b] = rgbdata->rgb;
+ return {r / 255.0, g / 255.0, b / 255.0};
+ } else if (auto graddata = boost::get<GradientData>(&data)) {
+ auto grad = graddata->gradient;
+ auto pat = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(grad->create_preview_pattern(1), true));
+ auto img = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 1, 1);
+ auto cr = Cairo::Context::create(img);
+ cr->set_source_rgb(196.0 / 255.0, 196.0 / 255.0, 196.0 / 255.0);
+ cr->paint();
+ cr->set_source(pat);
+ cr->paint();
+ cr.clear();
+ auto rgb = img->get_data();
+ return {rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0};
+ }
+
+ // unreachable
+ return {1.0, 1.0, 1.0};
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/dialog/color-item.h b/src/ui/dialog/color-item.h
new file mode 100644
index 0000000..a0609c1
--- /dev/null
+++ b/src/ui/dialog/color-item.h
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color item used in palettes and swatches UI.
+ */
+/* Authors: PBS <pbs3141@gmail.com>
+ * Copyright (C) 2022 PBS
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_COLOR_ITEM_H
+#define INKSCAPE_UI_DIALOG_COLOR_ITEM_H
+
+#include <boost/variant.hpp> // TODO: Upgrade to boost::variant2 or std::variant when possible.
+#include <boost/noncopyable.hpp>
+#include <cairomm/cairomm.h>
+#include <gtkmm/drawingarea.h>
+
+#include "inkscape-preferences.h"
+#include "widgets/paintdef.h"
+
+class SPGradient;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogBase;
+
+/**
+ * The color item you see on-screen as a clickable box.
+ *
+ * Note: This widget must be outlived by its parent dialog, passed in the constructor.
+ */
+class ColorItem final : public Gtk::DrawingArea, boost::noncopyable
+{
+public:
+ /// Create a static color from a paintdef.
+ ColorItem(PaintDef const&, DialogBase*);
+
+ /**
+ * Create a dynamically-updating color from a gradient, to which it remains linked.
+ * If the gradient is destroyed, the widget will go into an inactive state.
+ */
+ ColorItem(SPGradient*, DialogBase*);
+
+ /// Update the fill indicator, showing this widget is the fill of the current selection.
+ void set_fill(bool);
+
+ /// Update the stroke indicator, showing this widget is the stroke of the current selection.
+ void set_stroke(bool);
+
+ /// Update whether this item is pinned.
+ bool is_pinned() const;
+ void set_pinned_pref(const std::string &path);
+
+ const Glib::ustring &get_description() const { return description; }
+
+ sigc::signal<void ()>& signal_modified() { return _signal_modified; };
+ sigc::signal<void ()>& signal_pinned() { return _signal_pinned; };
+
+protected:
+ bool on_draw(Cairo::RefPtr<Cairo::Context> const&) override;
+ void on_size_allocate(Gtk::Allocation&) override;
+ bool on_enter_notify_event(GdkEventCrossing*) override;
+ bool on_leave_notify_event(GdkEventCrossing*) override;
+ bool on_button_press_event(GdkEventButton*) override;
+ bool on_button_release_event(GdkEventButton*) override;
+ void on_drag_data_get(Glib::RefPtr<Gdk::DragContext> const &context, Gtk::SelectionData &selection_data, guint info, guint time) override;
+ void on_drag_begin(Glib::RefPtr<Gdk::DragContext> const&) override;
+
+private:
+ // Common post-construction setup.
+ void common_setup();
+
+ // Perform the on-click action of setting the fill or stroke.
+ void on_click(bool stroke);
+
+ // Perform the right-click action of showing the context menu.
+ void on_rightclick(GdkEventButton *event);
+
+ // Draw the color only (i.e. no indicators) to a Cairo context. Used for drawing both the widget and the drag/drop icon.
+ void draw_color(Cairo::RefPtr<Cairo::Context> const &cr, int w, int h) const;
+
+ // Construct an equivalent paintdef for use during drag/drop.
+ PaintDef to_paintdef() const;
+
+ // Return the color (or average if a gradient), for choosing the color of the fill/stroke indicators.
+ std::array<double, 3> average_color() const;
+
+ // Description of the color, shown in help text.
+ Glib::ustring description;
+ Glib::ustring color_id;
+
+ /// The pinned preference path
+ Glib::ustring pinned_pref;
+ bool pinned_default = false;
+
+ // The color.
+ struct NoneData {};
+ struct RGBData { std::array<unsigned, 3> rgb; };
+ struct GradientData { SPGradient *gradient; };
+ boost::variant<NoneData, RGBData, GradientData> data;
+
+ // The dialog this widget belongs to. Used for determining what desktop to take action on.
+ DialogBase *dialog;
+
+ // Whether this color is in use as the fill or stroke of the current selection.
+ bool is_fill = false;
+ bool is_stroke = false;
+
+ // A cache of the widget contents, if necessary.
+ Cairo::RefPtr<Cairo::ImageSurface> cache;
+ bool cache_dirty = true;
+ bool was_grad_pinned = false;
+
+ // For ensuring that clicks that release outside the widget don't count.
+ bool mouse_inside = false;
+
+ sigc::signal<void ()> _signal_modified;
+ sigc::signal<void ()> _signal_pinned;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_COLOR_ITEM_H
diff --git a/src/ui/dialog/command-palette.cpp b/src/ui/dialog/command-palette.cpp
new file mode 100644
index 0000000..bf82ba7
--- /dev/null
+++ b/src/ui/dialog/command-palette.cpp
@@ -0,0 +1,1659 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for adding a live path effect.
+ *
+ * Author:
+ * Abhay Raj Singh <abhayonlyone@gmail.com>
+ *
+ * Copyright (C) 2020 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "command-palette.h"
+
+#include <cstddef>
+#include <cstring>
+#include <ctime>
+#include <gdk/gdkkeysyms.h>
+#include <giomm/action.h>
+#include <giomm/application.h>
+#include <giomm/file.h>
+#include <giomm/fileinfo.h>
+#include <glib/gi18n.h>
+#include <glibconfig.h>
+#include <glibmm/convert.h>
+#include <glibmm/date.h>
+#include <glibmm/error.h>
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/application.h>
+#include <gtkmm/box.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/label.h>
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/recentinfo.h>
+#include <iostream>
+#include <iterator>
+#include <memory>
+#include <optional>
+#include <ostream>
+#include <sigc++/adaptors/bind.h>
+#include <sigc++/functors/mem_fun.h>
+#include <string>
+
+#include "actions/actions-extra-data.h"
+#include "file.h"
+#include "gc-anchored.h"
+#include "include/glibmm_version.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "object/uri.h"
+#include "preferences.h"
+#include "ui/interface.h"
+#include "xml/repr.h"
+
+namespace Inkscape {
+class MessageStack;
+namespace UI {
+namespace Dialog {
+
+namespace {
+template <typename T>
+void debug_print(T variable)
+{
+ std::cerr << variable << std::endl;
+}
+} // namespace
+
+// constructor
+CommandPalette::CommandPalette()
+{
+ // setup _builder
+ {
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-main.glade");
+ try {
+ _builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for command palette dialog");
+ return;
+ }
+ }
+
+ // Setup Base UI Components
+ _builder->get_widget("CPBase", _CPBase);
+ _builder->get_widget("CPHeader", _CPHeader);
+ _builder->get_widget("CPListBase", _CPListBase);
+
+ _builder->get_widget("CPSearchBar", _CPSearchBar);
+ _builder->get_widget("CPFilter", _CPFilter);
+
+ _builder->get_widget("CPSuggestions", _CPSuggestions);
+ _builder->get_widget("CPHistory", _CPHistory);
+
+ _builder->get_widget("CPSuggestionsScroll", _CPSuggestionsScroll);
+ _builder->get_widget("CPHistoryScroll", _CPHistoryScroll);
+
+ _CPBase->add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK |
+ Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::KEY_PRESS_MASK);
+
+ // TODO: Customise on user language RTL, LTR or better user preference
+ _CPBase->set_halign(Gtk::ALIGN_CENTER);
+ _CPBase->set_valign(Gtk::ALIGN_START);
+
+ auto esc_func = sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_escape);
+ _CPFilter->signal_key_press_event().connect(esc_func, false);
+ _CPSuggestions->signal_key_release_event().connect(esc_func, false);
+ _CPHistory->signal_key_press_event().connect(esc_func, false);
+ set_mode(CPMode::SEARCH);
+
+ _CPSuggestions->set_activate_on_single_click();
+ _CPSuggestions->set_selection_mode(Gtk::SELECTION_SINGLE);
+
+ // Setup operations [actions, extensions]
+ {
+ // setup recent files
+ {
+ //TODO: refactor this ==============================
+ // this code is repeated in menubar.cpp
+ auto recent_manager = Gtk::RecentManager::get_default();
+ auto recent_files = recent_manager->get_items(); // all recent files not necessarily inkscape only
+
+ int max_files = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value");
+
+ for (auto const &recent_file : recent_files) {
+ // check if given was generated by inkscape
+ bool valid_file = recent_file->has_application(g_get_prgname()) or
+ recent_file->has_application("org.inkscape.Inkscape") or
+ recent_file->has_application("inkscape") or
+ recent_file->has_application("inkscape.exe");
+
+ valid_file = valid_file and recent_file->exists();
+
+ if (not valid_file) {
+ continue;
+ }
+
+ if (max_files-- <= 0) {
+ break;
+ }
+
+ append_recent_file_operation(recent_file->get_uri_display(), true,
+ false); // open - second param true to append in _CPSuggestions
+ append_recent_file_operation(recent_file->get_uri_display(), true,
+ true); // import - last param true for import operation
+ }
+ // ==================================================
+ }
+ }
+
+ // History management
+ {
+ const auto history = _history_xml.get_operation_history();
+
+ for (const auto &page : history) {
+ // second params false to append in history
+ switch (page.history_type) {
+ case HistoryType::ACTION:
+ generate_action_operation(get_action_ptr_name(page.data), false);
+ break;
+ case HistoryType::IMPORT_FILE:
+ append_recent_file_operation(page.data, false, true);
+ break;
+ case HistoryType::OPEN_FILE:
+ append_recent_file_operation(page.data, false, false);
+ break;
+ default:
+ continue;
+ }
+ }
+ }
+ // for `enter to execute` feature
+ _CPSuggestions->signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated));
+}
+
+void CommandPalette::open()
+{
+ if (not _win_doc_actions_loaded) {
+ // loading actions can be very slow
+ load_app_actions();
+ // win doc don't exist at construction so loading at first time opening Command Palette
+ load_win_doc_actions();
+ _win_doc_actions_loaded = true;
+ }
+ _CPBase->show_all();
+ _CPFilter->grab_focus();
+ _is_open = true;
+}
+
+void CommandPalette::close()
+{
+ _CPBase->hide();
+
+ // Reset filtering - show all suggestions
+ _CPFilter->set_text("");
+ _CPSuggestions->invalidate_filter();
+
+ set_mode(CPMode::SEARCH);
+
+ _is_open = false;
+}
+
+void CommandPalette::toggle()
+{
+ if (not _is_open) {
+ open();
+ return;
+ }
+ close();
+}
+
+void CommandPalette::append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import)
+{
+ static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade");
+ Glib::RefPtr<Gtk::Builder> operation_builder;
+ try {
+ operation_builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for Command Palette operation dialog");
+ }
+
+ // declaring required widgets pointers
+ Gtk::EventBox *CPOperation;
+ Gtk::Box *CPSynapseBox;
+
+ Gtk::Label *CPGroup;
+ Gtk::Label *CPName;
+ Gtk::Label *CPShortcut;
+ Gtk::Button *CPActionFullButton;
+ Gtk::Label *CPActionFullLabel;
+ Gtk::Label *CPDescription;
+
+ // Reading widgets
+ operation_builder->get_widget("CPOperation", CPOperation);
+ operation_builder->get_widget("CPSynapseBox", CPSynapseBox);
+
+ operation_builder->get_widget("CPGroup", CPGroup);
+ operation_builder->get_widget("CPName", CPName);
+ operation_builder->get_widget("CPShortcut", CPShortcut);
+ operation_builder->get_widget("CPActionFullButton", CPActionFullButton);
+ operation_builder->get_widget("CPActionFullLabel", CPActionFullLabel);
+ operation_builder->get_widget("CPDescription", CPDescription);
+
+ const auto file = Gio::File::create_for_path(path);
+ if (file->query_exists()) {
+ const Glib::ustring file_name = file->get_basename();
+
+ if (is_import) {
+ // Used for Activate row signal of listbox and not
+ CPGroup->set_text("import");
+ CPActionFullLabel->set_text("import"); // For filtering only
+
+ } else {
+ CPGroup->set_text("open");
+ CPActionFullLabel->set_text("open"); // For filtering only
+ }
+
+ // Hide for recent_file, not required
+ CPActionFullButton->set_no_show_all();
+ CPActionFullButton->hide();
+
+ CPName->set_text((is_import ? _("Import") : _("Open")) + (": " + file_name));
+ CPName->set_tooltip_text((is_import ? ("Import") : ("Open")) + (": " + file_name)); // Tooltip_text are not translatable
+ CPDescription->set_text(path);
+ CPDescription->set_tooltip_text(path);
+
+ {
+ Glib::DateTime mod_time;
+#if GLIBMM_CHECK_VERSION(2, 62, 0)
+ mod_time = file->query_info()->get_modification_date_time();
+ // Using this to reduce instead of ActionFullName widget because fullname is searched
+#else
+ mod_time.create_now_local(file->query_info()->modification_time());
+#endif
+ CPShortcut->set_text(mod_time.format("%d %b %R"));
+ }
+ // Add to suggestions
+ if (is_suggestion) {
+ _CPSuggestions->append(*CPOperation);
+ } else {
+ _CPHistory->append(*CPOperation);
+ }
+ }
+}
+
+bool CommandPalette::generate_action_operation(const ActionPtrName &action_ptr_name, bool is_suggestion)
+{
+ static const auto app = InkscapeApplication::instance();
+ static const auto gapp = app->gtk_app();
+ static InkActionExtraData &action_data = app->get_action_extra_data();
+ static const bool show_full_action_name =
+ Inkscape::Preferences::get()->getBool("/options/commandpalette/showfullactionname/value");
+ static const auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "command-palette-operation.glade");
+
+ Glib::RefPtr<Gtk::Builder> operation_builder;
+ try {
+ operation_builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for Command Palette operation dialog");
+ return false;
+ }
+
+ // declaring required widgets pointers
+ Gtk::EventBox *CPOperation;
+ Gtk::Box *CPSynapseBox;
+
+ Gtk::Label *CPGroup;
+ Gtk::Label *CPName;
+ Gtk::Label *CPShortcut;
+ Gtk::Label *CPDescription;
+ Gtk::Button *CPActionFullButton;
+ Gtk::Label *CPActionFullLabel;
+
+ // Reading widgets
+ operation_builder->get_widget("CPOperation", CPOperation);
+ operation_builder->get_widget("CPSynapseBox", CPSynapseBox);
+
+ operation_builder->get_widget("CPGroup", CPGroup);
+ operation_builder->get_widget("CPName", CPName);
+ operation_builder->get_widget("CPShortcut", CPShortcut);
+ operation_builder->get_widget("CPActionFullButton", CPActionFullButton);
+ operation_builder->get_widget("CPActionFullLabel", CPActionFullLabel);
+ operation_builder->get_widget("CPDescription", CPDescription);
+
+ CPGroup->set_text(action_data.get_section_for_action(Glib::ustring(action_ptr_name.second)));
+
+ // Setting CPName
+ {
+ auto name = action_data.get_label_for_action(action_ptr_name.second);
+ auto untranslated_name = action_data.get_label_for_action(action_ptr_name.second, false);
+ if (name.empty()) {
+ // If action doesn't have a label, set the name = full action name
+ name = action_ptr_name.second;
+ untranslated_name = action_ptr_name.second;
+ }
+
+ CPName->set_text(name);
+ CPName->set_tooltip_text(untranslated_name);
+ }
+
+ {
+ CPActionFullLabel->set_text(action_ptr_name.second);
+
+ if (not show_full_action_name) {
+ CPActionFullButton->set_no_show_all();
+ CPActionFullButton->hide();
+ } else {
+ CPActionFullButton->signal_clicked().connect(
+ sigc::bind<Glib::ustring>(sigc::mem_fun(*this, &CommandPalette::on_action_fullname_clicked),
+ action_ptr_name.second),
+ false);
+ }
+ }
+
+ {
+ std::vector<Glib::ustring> accels = gapp->get_accels_for_action(action_ptr_name.second);
+ std::stringstream ss;
+ for (const auto &accel : accels) {
+ guint key = 0;
+ Gdk::ModifierType mods;
+ Gtk::AccelGroup::parse(accel, key, mods);
+ Glib::ustring label = Gtk::AccelGroup::get_label(key, mods);
+ ss << label.raw() << ' ';
+ }
+ std::string accel_label = ss.str();
+
+ if (not accel_label.empty()) {
+ accel_label.pop_back();
+ CPShortcut->set_text(accel_label);
+ } else {
+ CPShortcut->set_no_show_all();
+ CPShortcut->hide();
+ }
+ }
+
+ CPDescription->set_text(action_data.get_tooltip_for_action(action_ptr_name.second));
+ CPDescription->set_tooltip_text(action_data.get_tooltip_for_action(action_ptr_name.second, false));
+
+ // Add to suggestions
+ if (is_suggestion) {
+ _CPSuggestions->append(*CPOperation);
+ } else {
+ _CPHistory->append(*CPOperation);
+ }
+
+ return true;
+}
+
+void CommandPalette::on_search()
+{
+ _CPSuggestions->unset_sort_func();
+ _CPSuggestions->set_sort_func(sigc::mem_fun(*this, &CommandPalette::on_sort));
+ _search_text = _CPFilter->get_text();
+ _CPSuggestions->invalidate_filter(); // Remove old filter constraint and apply new one
+ if (auto top_row = _CPSuggestions->get_row_at_y(0); top_row) {
+ _CPSuggestions->select_row(*top_row); // select top row
+ }
+ _CPSuggestionsScroll->get_vadjustment()->set_value(0);
+}
+
+bool CommandPalette::on_filter_full_action_name(Gtk::ListBoxRow *child)
+{
+ if (auto CPActionFullLabel = get_full_action_name(child);
+ CPActionFullLabel and _search_text == CPActionFullLabel->get_text()) {
+ return true;
+ }
+ return false;
+}
+
+bool CommandPalette::on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import)
+{
+ auto CPActionFullLabel = get_full_action_name(child);
+ if (is_import) {
+ if (CPActionFullLabel and CPActionFullLabel->get_text() == "import") {
+ auto [CPName, CPDescription] = get_name_desc(child);
+ if (CPDescription && CPDescription->get_text() == _search_text) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (CPActionFullLabel and CPActionFullLabel->get_text() == "open") {
+ auto [CPName, CPDescription] = get_name_desc(child);
+ if (CPDescription && CPDescription->get_text() == _search_text) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool CommandPalette::on_key_press_cpfilter_escape(GdkEventKey *evt)
+{
+ if (evt->keyval == GDK_KEY_Escape || evt->keyval == GDK_KEY_question) {
+ close();
+ return true; // stop propagation of key press, not needed anymore
+ }
+ return false; // Pass the key event which are not used
+}
+
+bool CommandPalette::on_key_press_cpfilter_search_mode(GdkEventKey *evt)
+{
+ auto key = evt->keyval;
+ if (key == GDK_KEY_Return or key == GDK_KEY_Linefeed) {
+ if (auto selected_row = _CPSuggestions->get_selected_row(); selected_row) {
+ selected_row->activate();
+ }
+ return true;
+ } else if (key == GDK_KEY_Up) {
+ if (!_CPHistory->get_children().empty()) {
+ set_mode(CPMode::HISTORY);
+ return true;
+ }
+ } else if (key == GDK_KEY_Down) {
+ if (!_CPSuggestions->get_children().empty()) {
+ _CPSuggestions->unselect_all();
+ }
+ }
+ return false;
+}
+
+bool CommandPalette::on_key_press_cpfilter_history_mode(GdkEventKey *evt)
+{
+ if (evt->keyval == GDK_KEY_BackSpace) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Executes action when enter pressed
+ */
+bool CommandPalette::on_key_press_cpfilter_input_mode(GdkEventKey *evt, const ActionPtrName &action_ptr_name)
+{
+ switch (evt->keyval) {
+ case GDK_KEY_Return:
+ [[fallthrough]];
+ case GDK_KEY_Linefeed:
+ execute_action(action_ptr_name, _CPFilter->get_text());
+ close();
+ return true;
+ }
+ return false;
+}
+
+void CommandPalette::hide_suggestions()
+{
+ _CPBase->set_size_request(-1, 10);
+ _CPListBase->hide();
+}
+void CommandPalette::show_suggestions()
+{
+ _CPBase->set_size_request(-1, _max_height_requestable);
+ _CPListBase->show_all();
+}
+
+void CommandPalette::on_action_fullname_clicked(const Glib::ustring &action_fullname)
+{
+ static auto clipboard = Gtk::Clipboard::get();
+ clipboard->set_text(action_fullname);
+ clipboard->store();
+}
+
+void CommandPalette::on_row_activated(Gtk::ListBoxRow *activated_row)
+{
+ // this is set to import/export or full action name
+ const auto full_action_name = get_full_action_name(activated_row)->get_label();
+ if (full_action_name == "import" or full_action_name == "open") {
+ const auto [name, description] = get_name_desc(activated_row);
+ operate_recent_file(description->get_text(), full_action_name == "import");
+ } else {
+ ask_action_parameter(get_action_ptr_name(full_action_name));
+ // this is an action
+ }
+}
+
+void CommandPalette::on_history_selection_changed(Gtk::ListBoxRow *lb)
+{
+ // set the search box text to current selection
+ if (const auto name_label = get_name_desc(lb).first; name_label) {
+ _CPFilter->set_text(name_label->get_text());
+ }
+}
+
+bool CommandPalette::operate_recent_file(Glib::ustring const &uri, bool const import)
+{
+ static auto prefs = Inkscape::Preferences::get();
+
+ bool write_to_history = true;
+
+ // if the last element in CPHistory is already this, don't update history file
+ if (not _CPHistory->get_children().empty()) {
+ if (const auto last_operation = _history_xml.get_last_operation(); last_operation.has_value()) {
+ if (uri == last_operation->data) {
+ bool last_operation_was_import = last_operation->history_type == HistoryType::IMPORT_FILE;
+ // As previous uri is verfied to be the same as current uri we can write to history if current and
+ // previous operation are not the same.
+ // For example: if we want to import and previous operation was import (with same uri) we should not
+ // write ot history, similarly if current is open and previous was open to then dont WTH.
+ // But in case previous operation was open and current is import and vice-versa we should write to
+ // history.
+ if (not(import xor last_operation_was_import)) {
+ write_to_history = false;
+ }
+ }
+ }
+ }
+
+ if (import) {
+ prefs->setBool("/options/onimport", true);
+ file_import(SP_ACTIVE_DOCUMENT, uri, nullptr);
+ prefs->setBool("/options/onimport", true);
+
+ if (write_to_history) {
+ _history_xml.add_import(uri);
+ }
+
+ close();
+ return true;
+ }
+
+ // open
+ {
+ get_action_ptr_name("app.file-open").first->activate(uri);
+ if (write_to_history) {
+ _history_xml.add_open(uri);
+ }
+ }
+
+ close();
+ return true;
+} // namespace Dialog
+
+/**
+ * Maybe replaced by: Temporary arrangement may be replaced by snippets
+ * This can help us provide parameters for multiple argument function
+ * whose actions take a string as param
+ */
+bool CommandPalette::ask_action_parameter(const ActionPtrName &action_ptr_name)
+{
+ // Avoid writing same last action again
+ // TODO: Merge the if else parts
+ if (const auto last_of_history = _history_xml.get_last_operation(); last_of_history.has_value()) {
+ // operation history is not empty
+ const auto last_full_action_name = last_of_history->data;
+ if (last_full_action_name != action_ptr_name.second) {
+ // last action is not the same so write this one
+ _history_xml.add_action(action_ptr_name.second); // to history file
+ generate_action_operation(action_ptr_name, false); // to _CPHistory
+ }
+ } else {
+ // History is empty so no need to check
+ _history_xml.add_action(action_ptr_name.second); // to history file
+ generate_action_operation(action_ptr_name, false); // to _CPHistory
+ }
+
+ // Checking if action has handleable parameter type
+ TypeOfVariant action_param_type = get_action_variant_type(action_ptr_name.first);
+ if (action_param_type == TypeOfVariant::UNKNOWN) {
+ std::cerr << "CommandPalette::ask_action_parameter: unhandled action value type (Unknown Type) "
+ << action_ptr_name.second.raw() << std::endl;
+ return false;
+ }
+
+ if (action_param_type != TypeOfVariant::NONE) {
+ set_mode(CPMode::INPUT);
+
+ _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
+ sigc::bind<ActionPtrName>(sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_input_mode),
+ action_ptr_name),
+ false);
+
+ // get type string NOTE: Temporary should be replaced by adding some data to InkActionExtraDataj
+ Glib::ustring type_string;
+ switch (action_param_type) {
+ case TypeOfVariant::BOOL:
+ type_string = "bool";
+ break;
+ case TypeOfVariant::INT:
+ type_string = "integer";
+ break;
+ case TypeOfVariant::DOUBLE:
+ type_string = "double";
+ break;
+ case TypeOfVariant::STRING:
+ type_string = "string";
+ break;
+ case TypeOfVariant::TUPLE_DD:
+ type_string = "pair of doubles";
+ break;
+ default:
+ break;
+ }
+
+ const auto app = InkscapeApplication::instance();
+ InkActionHintData &action_hint_data = app->get_action_hint_data();
+ auto action_hint = action_hint_data.get_tooltip_hint_for_action(action_ptr_name.second, false);
+
+
+ // Indicate user about what to enter FIXME Dialog generation
+ if (action_hint.length()) {
+ _CPFilter->set_placeholder_text(action_hint);
+ _CPFilter->set_tooltip_text(action_hint);
+ } else {
+ _CPFilter->set_placeholder_text("Enter a " + type_string + "...");
+ _CPFilter->set_tooltip_text("Enter a " + type_string + "...");
+ }
+
+
+ return true;
+ }
+
+ execute_action(action_ptr_name, "");
+ close();
+
+ return true;
+}
+
+/**
+ * Color removal
+ */
+void CommandPalette::remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip)
+{
+ /* if (tooltip) {
+ label->set_tooltip_text(subject);
+ } else if (label->get_use_markup()) {
+ label->set_text(subject);
+ } */
+}
+
+/**
+ * Color addition
+ */
+Glib::ustring make_bold(const Glib::ustring &search)
+{
+ // TODO: Add a CSS class that changes the color of the search
+ return "<span weight=\"bold\">" + search + "</span>";
+}
+
+void CommandPalette::add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip)
+{
+ //is no working on master fill all chars so I comment to speedup
+ /* Glib::ustring text = "";
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+ int j = 0;
+
+ if (search_string.length() > 7) {
+ for (gunichar i : search_string) {
+ if (i == ' ') {
+ continue;
+ }
+ while (j < subject_string.length()) {
+ if (i == subject_string[j]) {
+ text += make_bold(Glib::Markup::escape_text(subject.substr(j, 1)));
+ j++;
+ break;
+ } else {
+ text += Glib::Markup::escape_text(subject.substr(j, 1));
+ }
+ j++;
+ }
+ }
+ if (j < subject.length()) {
+ text += Glib::Markup::escape_text(subject.substr(j));
+ }
+ } else {
+ std::map<gunichar, int> search_string_character;
+
+ for (const auto &character : search_string) {
+ search_string_character[character]++;
+ }
+
+ int subject_length = subject_string.length();
+
+ for (int i = 0; i < subject_length; i++) {
+ if (search_string_character[subject_string[i]]--) {
+ text += make_bold(Glib::Markup::escape_text(subject.substr(i, 1)));
+ } else {
+ text += Glib::Markup::escape_text(subject.substr(i, 1));
+ }
+ }
+ }
+
+ if (tooltip) {
+ label->set_tooltip_markup(text);
+ } else {
+ label->set_markup(text);
+ } */
+}
+
+/**
+ * Color addition for description text
+ * Coloring complete consecutive search text in the description text
+ */
+void CommandPalette::add_color_description(Gtk::Label *label, const Glib::ustring &search)
+{
+ /* Glib::ustring subject = label->get_text();
+
+ Glib::ustring const subject_normalize = subject.lowercase().normalize();
+ Glib::ustring const search_normalize = search.lowercase().normalize();
+
+ auto const position = subject_normalize.find(search_normalize);
+ auto const search_length = search_normalize.size();
+
+ subject = Glib::Markup::escape_text(subject.substr(0, position)) +
+ make_bold(Glib::Markup::escape_text(subject.substr(position, search_length))) +
+ Glib::Markup::escape_text(subject.substr(position + search_length));
+
+ label->set_markup(subject); */
+}
+
+/**
+ * The Searching algorithm consists of fuzzy search and fuzzy points.
+ *
+ * Ever search of the label can contain up to three subjects to search
+ * CPName text,CPName tooltip text,CPDescription text
+ *
+ * Fuzzy search searches the search text in these subjects and returns a boolean
+ * Searching of a search text as a subsequence of the subject
+ *
+ * Fuzzy points give an integer of a particular search text concerning a particular subject.
+ * Less the fuzzy point more is the precedence.
+ *
+ * Special case for CPDescription text search by searching text as a substring of the subject
+ *
+ * TODO: Adding more conditions in fuzzy points and fuzzy search for creating better user experience
+ */
+
+bool CommandPalette::fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+ std::map<gunichar, int> subject_string_character, search_string_character;
+ for (const auto &character : subject_string) {
+ subject_string_character[character]++;
+ }
+ for (const auto &character : search_string) {
+ search_string_character[character]++;
+ }
+ for (const auto &character : search_string_character) {
+ auto [alphabet, occurrence] = character;
+ if (subject_string_character[alphabet] < occurrence) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool CommandPalette::fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+
+ for (int j = 0, i = 0; i < search_string.length(); i++) {
+ bool alphabet_present = false;
+
+ while (j < subject_string.length()) {
+ if (search_string[i] == subject_string[j]) {
+ alphabet_present = true;
+ j++;
+ break;
+ }
+ j++;
+ }
+
+ if (!alphabet_present) {
+ return false; // If not present
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Searching the full search_text in the subject string
+ * used for CPDescription text
+ */
+bool CommandPalette::normal_search(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ if (subject.lowercase().find(search.lowercase()) != -1) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Calculates the fuzzy_point
+ */
+int CommandPalette::fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ int fuzzy_cost = 100; // Taking initial fuzzy_cost as 100
+
+ constexpr int SEQUENTIAL_BONUS = -15; // bonus for adjacent matches
+ constexpr int SEPARATOR_BONUS = -30; // bonus if search occurs after a separator
+ constexpr int CAMEL_BONUS = -30; // bonus if search is uppercase and subject is lower
+ constexpr int FIRST_LETTET_BONUS = -15; // bonus if the first letter is matched
+ constexpr int LEADING_LETTER_PENALTY = +5; // penalty applied for every letter in subject before the first match
+ constexpr int MAX_LEADING_LETTER_PENALTY = +15; // maximum penalty for leading letters
+ constexpr int UNMATCHED_LETTER_PENALTY = +1; // penalty for every letter that doesn't matter
+
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+
+ bool sequential_compare = false;
+ bool leading_letter = true;
+ int total_leading_letter_penalty = 0;
+ int j = 0, i = 0;
+
+ while (i < search_string.length() && j < subject_string.length()) {
+ if (search_string[i] != subject_string[j]) {
+ j++;
+ sequential_compare = false;
+ fuzzy_cost += UNMATCHED_LETTER_PENALTY;
+
+ if (leading_letter) {
+ if (total_leading_letter_penalty < MAX_LEADING_LETTER_PENALTY) {
+ fuzzy_cost += LEADING_LETTER_PENALTY;
+ total_leading_letter_penalty += LEADING_LETTER_PENALTY;
+ }
+ }
+
+ continue;
+ }
+
+ if (search_string[i] == subject_string[j]) {
+ leading_letter = false;
+
+ if (j > 0 && subject_string[j - 1] == ' ') {
+ fuzzy_cost += SEPARATOR_BONUS;
+ }
+
+ if (i == 0 && j == 0) {
+ fuzzy_cost += FIRST_LETTET_BONUS;
+ }
+
+ if (search[i] == subject_string[j]) {
+ fuzzy_cost += CAMEL_BONUS;
+ }
+
+ if (sequential_compare) {
+ fuzzy_cost += SEQUENTIAL_BONUS;
+ }
+
+ sequential_compare = true;
+ i++;
+ }
+ }
+
+ return fuzzy_cost;
+}
+
+int CommandPalette::fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search)
+{
+ int fuzzy_cost = 200; // Taking initial fuzzy_cost as 200
+ constexpr int FIRST_LETTET_BONUS = -15; // bonus if the first letter is matched
+
+ Glib::ustring subject_string = subject.lowercase();
+ Glib::ustring search_string = search.lowercase();
+
+ std::map<gunichar, int> search_string_character;
+
+ for (const auto &character : search_string) {
+ search_string_character[character]++;
+ }
+
+ for (const auto &character : search_string_character) {
+ auto [alphabet, occurrence] = character;
+ for (int i = 0; i < subject_string.length() && occurrence; i++) {
+ if (subject_string[i] == alphabet) {
+ if (i == 0)
+ fuzzy_cost += FIRST_LETTET_BONUS;
+ fuzzy_cost += i;
+ occurrence--;
+ }
+ }
+ }
+
+ return fuzzy_cost;
+}
+
+int CommandPalette::on_filter_general(Gtk::ListBoxRow *child)
+{
+ auto [CPName, CPDescription] = get_name_desc(child);
+ if (CPName) {
+ remove_color(CPName, CPName->get_text());
+ remove_color(CPName, CPName->get_tooltip_text(), true);
+ }
+ if (CPDescription) {
+ remove_color(CPDescription, CPDescription->get_text());
+ }
+
+ if (_search_text.empty()) {
+ return 1;
+ } // Every operation is visible if search text is empty
+
+ if (CPName) {
+ if (fuzzy_search(CPName->get_text(), _search_text)) {
+ add_color(CPName, _search_text, CPName->get_text());
+ return fuzzy_points(CPName->get_text(), _search_text);
+ }
+
+ if (fuzzy_search(CPName->get_tooltip_text(), _search_text)) {
+ add_color(CPName, _search_text, CPName->get_tooltip_text(), true);
+ return fuzzy_points(CPName->get_tooltip_text(), _search_text);
+ }
+
+ if (fuzzy_tolerance_search(CPName->get_text(), _search_text)) {
+ add_color(CPName, _search_text, CPName->get_text());
+ return fuzzy_tolerance_points(CPName->get_text(), _search_text);
+ }
+
+ if (fuzzy_tolerance_search(CPName->get_tooltip_text(), _search_text)) {
+ add_color(CPName, _search_text, CPName->get_tooltip_text(), true);
+ return fuzzy_tolerance_points(CPName->get_tooltip_text(), _search_text);
+ }
+ }
+ if (CPDescription && normal_search(CPDescription->get_text(), _search_text)) {
+ add_color_description(CPDescription, _search_text);
+ return fuzzy_points(CPDescription->get_text(), _search_text);
+ }
+
+ return 0;
+}
+
+int CommandPalette::fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1,
+ int text_len_2)
+{
+ if (fuzzy_points_count_1 && fuzzy_points_count_2) {
+ if (fuzzy_points_count_1 < fuzzy_points_count_2) {
+ return -1;
+ } else if (fuzzy_points_count_1 == fuzzy_points_count_2) {
+ if (text_len_1 > text_len_2) {
+ return 1;
+ } else {
+ return -1;
+ }
+ } else {
+ return 1;
+ }
+ }
+
+ if (fuzzy_points_count_1 == 0 && fuzzy_points_count_2) {
+ return 1;
+ }
+ if (fuzzy_points_count_2 == 0 && fuzzy_points_count_1) {
+ return -1;
+ }
+
+ return 0;
+}
+
+/**
+ * compare different rows for order of display
+ * priority of comparison
+ * 1) CPName->get_text()
+ * 2) CPName->get_tooltip_text()
+ * 3) CPDescription->get_text()
+ */
+int CommandPalette::on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2)
+{
+ // tests for fuzzy_search
+ assert(fuzzy_search("Export background", "ebo") == true);
+ assert(fuzzy_search("Query y", "qyy") == true);
+ assert(fuzzy_search("window close", "qt") == false);
+
+ // tests for fuzzy_points
+ assert(fuzzy_points("Export background", "ebo") == -22);
+ assert(fuzzy_points("Query y", "qyy") == -16);
+ assert(fuzzy_points("window close", "wc") == 2);
+
+ // tests for fuzzy_tolerance_search
+ assert(fuzzy_tolerance_search("object to path", "ebo") == true);
+ assert(fuzzy_tolerance_search("execute verb", "qyy") == false);
+ assert(fuzzy_tolerance_search("color mode", "moco") == true);
+
+ // tests for fuzzy_tolerance_points
+ assert(fuzzy_tolerance_points("object to path", "ebo") == 189);
+ assert(fuzzy_tolerance_points("execute verb", "vec") == 196);
+ assert(fuzzy_tolerance_points("color mode", "moco") == 195);
+
+ if (_search_text.empty()) {
+ return -1;
+ } // No change in the order
+
+ auto [cp_name_1, cp_description_1] = get_name_desc(row1);
+ auto [cp_name_2, cp_description_2] = get_name_desc(row2);
+
+ int fuzzy_points_count_1 = 0, fuzzy_points_count_2 = 0;
+ int text_len_1 = 0, text_len_2 = 0;
+ int points_compare = 0;
+
+ constexpr int TOOLTIP_PENALTY = 100;
+ constexpr int DESCRIPTION_PENALTY = 500;
+
+ if (cp_name_1 && cp_name_2) {
+ if (fuzzy_search(cp_name_1->get_text(), _search_text)) {
+ text_len_1 = cp_name_1->get_text().length();
+ fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_text(), _search_text);
+ }
+ if (fuzzy_search(cp_name_2->get_text(), _search_text)) {
+ text_len_2 = cp_name_2->get_text().length();
+ fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_text(), _search_text);
+ }
+
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+
+ if (fuzzy_tolerance_search(cp_name_1->get_text(), _search_text)) {
+ text_len_1 = cp_name_1->get_text().length();
+ fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_text(), _search_text);
+ }
+ if (fuzzy_tolerance_search(cp_name_2->get_text(), _search_text)) {
+ text_len_2 = cp_name_2->get_text().length();
+ fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_text(), _search_text);
+ }
+
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+
+ if (fuzzy_search(cp_name_1->get_tooltip_text(), _search_text)) {
+ text_len_1 = cp_name_1->get_tooltip_text().length();
+ fuzzy_points_count_1 = fuzzy_points(cp_name_1->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY;
+ }
+ if (fuzzy_search(cp_name_2->get_tooltip_text(), _search_text)) {
+ text_len_2 = cp_name_2->get_tooltip_text().length();
+ fuzzy_points_count_2 = fuzzy_points(cp_name_2->get_tooltip_text(), _search_text) + TOOLTIP_PENALTY;
+ }
+
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+
+ if (fuzzy_tolerance_search(cp_name_1->get_tooltip_text(), _search_text)) {
+ text_len_1 = cp_name_1->get_tooltip_text().length();
+ fuzzy_points_count_1 = fuzzy_tolerance_points(cp_name_1->get_tooltip_text(), _search_text) +
+ TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
+ }
+ if (fuzzy_tolerance_search(cp_name_2->get_tooltip_text(), _search_text)) {
+ text_len_2 = cp_name_2->get_tooltip_text().length();
+ fuzzy_points_count_2 = fuzzy_tolerance_points(cp_name_2->get_tooltip_text(), _search_text) +
+ TOOLTIP_PENALTY; // Adding a constant integer to decrease the prefrence
+ }
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+ }
+
+ if (cp_description_1 && normal_search(cp_description_1->get_text(), _search_text)) {
+ text_len_1 = cp_description_1->get_text().length();
+ fuzzy_points_count_1 = fuzzy_points(cp_description_1->get_text(), _search_text) +
+ DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
+ }
+ if (cp_description_2 && normal_search(cp_description_2->get_text(), _search_text)) {
+ text_len_2 = cp_description_2->get_text().length();
+ fuzzy_points_count_2 = fuzzy_points(cp_description_2->get_text(), _search_text) +
+ DESCRIPTION_PENALTY; // Adding a constant integer to decrease the prefrence
+ }
+
+ points_compare = fuzzy_points_compare(fuzzy_points_count_1, fuzzy_points_count_2, text_len_1, text_len_2);
+ if (points_compare != 0) {
+ return points_compare;
+ }
+ return 0;
+}
+
+void CommandPalette::set_mode(CPMode mode)
+{
+ switch (mode) {
+ case CPMode::SEARCH:
+ if (_mode == CPMode::SEARCH) {
+ return;
+ }
+
+ _CPFilter->set_text("");
+ _CPFilter->set_icon_from_icon_name("edit-find-symbolic");
+ _CPFilter->set_placeholder_text("Search operation...");
+ _CPFilter->set_tooltip_text("Search operation...");
+ show_suggestions();
+
+ // Show Suggestions instead of history
+ _CPHistoryScroll->set_no_show_all();
+ _CPHistoryScroll->hide();
+
+ _CPSuggestionsScroll->set_no_show_all(false);
+ _CPSuggestionsScroll->show_all();
+
+ _CPSuggestions->unset_filter_func();
+ _CPSuggestions->set_filter_func(sigc::mem_fun(*this, &CommandPalette::on_filter_general));
+
+ _cpfilter_search_connection.disconnect(); // to be sure
+ _cpfilter_key_press_connection.disconnect();
+
+ _cpfilter_search_connection =
+ _CPFilter->signal_search_changed().connect(sigc::mem_fun(*this, &CommandPalette::on_search));
+ _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
+ sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_search_mode), false);
+
+ _search_text = "";
+ _CPSuggestions->invalidate_filter();
+ break;
+
+ case CPMode::INPUT:
+ if (_mode == CPMode::INPUT) {
+ return;
+ }
+ _cpfilter_search_connection.disconnect();
+ _cpfilter_key_press_connection.disconnect();
+
+ hide_suggestions();
+ _CPFilter->set_text("");
+ _CPFilter->grab_focus();
+
+ _CPFilter->set_icon_from_icon_name("input-keyboard");
+ _CPFilter->set_placeholder_text("Enter action argument");
+ _CPFilter->set_tooltip_text("Enter action argument");
+
+ break;
+
+ case CPMode::SHELL:
+ if (_mode == CPMode::SHELL) {
+ return;
+ }
+
+ hide_suggestions();
+ _CPFilter->set_icon_from_icon_name("gtk-search");
+ _cpfilter_search_connection.disconnect();
+ _cpfilter_key_press_connection.disconnect();
+
+ break;
+
+ case CPMode::HISTORY:
+ if (_mode == CPMode::HISTORY) {
+ return;
+ }
+
+ if (_CPHistory->get_children().empty()) {
+ return;
+ }
+
+ // Show history instead of suggestions
+ _CPSuggestionsScroll->set_no_show_all();
+ _CPHistoryScroll->set_no_show_all(false);
+
+ _CPSuggestionsScroll->hide();
+ _CPHistoryScroll->show_all();
+
+ _CPFilter->set_icon_from_icon_name("format-justify-fill");
+ _CPFilter->set_icon_tooltip_text(N_("History mode"));
+ _cpfilter_search_connection.disconnect();
+ _cpfilter_key_press_connection.disconnect();
+
+ _cpfilter_key_press_connection = _CPFilter->signal_key_press_event().connect(
+ sigc::mem_fun(*this, &CommandPalette::on_key_press_cpfilter_history_mode), false);
+
+ _CPHistory->signal_row_selected().connect(
+ sigc::mem_fun(*this, &CommandPalette::on_history_selection_changed));
+ _CPHistory->signal_row_activated().connect(sigc::mem_fun(*this, &CommandPalette::on_row_activated));
+
+ {
+ // select last row
+ const auto last_row = _CPHistory->get_row_at_index(_CPHistory->get_children().size() - 1);
+ _CPHistory->select_row(*last_row);
+ last_row->grab_focus();
+ }
+
+ {
+ // FIXME: scroll to bottom
+ const auto adjustment = _CPHistoryScroll->get_vadjustment();
+ adjustment->set_value(adjustment->get_upper());
+ }
+
+ break;
+ }
+ _mode = mode;
+}
+
+/**
+ * Calls actions with parameters
+ */
+CommandPalette::ActionPtrName CommandPalette::get_action_ptr_name(const Glib::ustring &full_action_name)
+{
+ static auto gapp = InkscapeApplication::instance()->gtk_app();
+ // TODO: Optimisation: only try to assign if null, make static
+ const auto win = InkscapeApplication::instance()->get_active_window();
+ const auto doc = InkscapeApplication::instance()->get_active_document();
+ auto action_domain_string = full_action_name.substr(0, full_action_name.find('.')); // app, win, doc
+ auto action_name = full_action_name.substr(full_action_name.find('.') + 1);
+
+ ActionPtr action_ptr;
+ if (action_domain_string == "app") {
+ action_ptr = gapp->lookup_action(action_name);
+ } else if (action_domain_string == "win" and win) {
+ action_ptr = win->lookup_action(action_name);
+ } else if (action_domain_string == "doc" and doc) {
+ if (const auto map = doc->getActionGroup(); map) {
+ action_ptr = map->lookup_action(action_name);
+ }
+ }
+
+ return {action_ptr, full_action_name};
+}
+
+bool CommandPalette::execute_action(const ActionPtrName &action_ptr_name, const Glib::ustring &value)
+{
+ if (not value.empty()) {
+ _history_xml.add_action_parameter(action_ptr_name.second, value);
+ }
+ auto [action_ptr, action_name] = action_ptr_name;
+
+ switch (get_action_variant_type(action_ptr)) {
+ case TypeOfVariant::BOOL:
+ if (value == "1" || value == "t" || value == "true" || value.empty()) {
+ action_ptr->activate(Glib::Variant<bool>::create(true));
+ } else if (value == "0" || value == "f" || value == "false") {
+ action_ptr->activate(Glib::Variant<bool>::create(false));
+ } else {
+ std::cerr << "CommandPalette::execute_action: Invalid boolean value: " << action_name.raw() << ":" << value
+ << std::endl;
+ }
+ break;
+ case TypeOfVariant::INT:
+ try {
+ action_ptr->activate(Glib::Variant<int>::create(std::stoi(value)));
+ } catch (...) {
+ if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
+ dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter an integer number."));
+ }
+ }
+ break;
+ case TypeOfVariant::DOUBLE:
+ try {
+ action_ptr->activate(Glib::Variant<double>::create(std::stod(value)));
+ } catch (...) {
+ if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
+ dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter a decimal number."));
+ }
+ }
+ break;
+ case TypeOfVariant::STRING:
+ action_ptr->activate(Glib::Variant<Glib::ustring>::create(value));
+ break;
+ case TypeOfVariant::TUPLE_DD:
+ try {
+ double d0 = 0;
+ double d1 = 0;
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", value);
+
+ try {
+ if (tokens.size() != 2) {
+ throw std::invalid_argument("requires two numbers");
+ }
+ } catch (...) {
+ throw;
+ }
+
+ try {
+ d0 = std::stod(tokens[0]);
+ d1 = std::stod(tokens[1]);
+ } catch (...) {
+ throw;
+ }
+
+ auto variant = Glib::Variant<std::tuple<double, double>>::create(std::tuple<double, double>(d0, d1));
+ action_ptr->activate(variant);
+ } catch (...) {
+ if (SPDesktop *dt = SP_ACTIVE_DESKTOP; dt) {
+ dt->messageStack()->flash(ERROR_MESSAGE, _("Invalid input! Enter two comma separated numbers."));
+ }
+ }
+ break;
+ case TypeOfVariant::UNKNOWN:
+ std::cerr << "CommandPalette::execute_action: unhandled action value type (Unknown Type) " << action_name.raw()
+ << std::endl;
+ break;
+ case TypeOfVariant::NONE:
+ default:
+ action_ptr->activate();
+ break;
+ }
+ return false;
+}
+
+TypeOfVariant CommandPalette::get_action_variant_type(const ActionPtr &action_ptr)
+{
+ const GVariantType *gtype = g_action_get_parameter_type(action_ptr->gobj());
+ if (gtype) {
+ Glib::VariantType type = action_ptr->get_parameter_type();
+ if (type.get_string() == "b") {
+ return TypeOfVariant::BOOL;
+ } else if (type.get_string() == "i") {
+ return TypeOfVariant::INT;
+ } else if (type.get_string() == "d") {
+ return TypeOfVariant::DOUBLE;
+ } else if (type.get_string() == "s") {
+ return TypeOfVariant::STRING;
+ } else if (type.get_string() == "(dd)") {
+ return TypeOfVariant::TUPLE_DD;
+ } else {
+ std::cerr << "CommandPalette::get_action_variant_type: unknown variant type: " << type.get_string() << std::endl;
+ return TypeOfVariant::UNKNOWN;
+ }
+ }
+ // With value.
+ return TypeOfVariant::NONE;
+}
+
+std::pair<Gtk::Label *, Gtk::Label *> CommandPalette::get_name_desc(Gtk::ListBoxRow *child)
+{
+ auto event_box = dynamic_cast<Gtk::EventBox *>(child->get_child());
+ if (event_box) {
+ // NOTE: These variables have same name as in the glade file command-palette-operation.glade
+ // FIXME: When structure of Gladefile of CPOperation changes, refactor this
+ auto CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child());
+ if (CPSynapseBox) {
+ auto synapse_children = CPSynapseBox->get_children();
+ auto CPNameBox = dynamic_cast<Gtk::Box *>(synapse_children[0]);
+ if (CPNameBox) {
+ auto name_children = CPNameBox->get_children();
+ auto CPName = dynamic_cast<Gtk::Label *>(name_children[0]);
+ auto CPDescription = dynamic_cast<Gtk::Label *>(name_children[1]);
+ return std::pair(CPName, CPDescription);
+ }
+ }
+ }
+ return std::pair(nullptr, nullptr);
+}
+
+Gtk::Label *CommandPalette::get_full_action_name(Gtk::ListBoxRow *child)
+{
+ auto event_box = dynamic_cast<Gtk::EventBox *>(child->get_child());
+ if (event_box) {
+ auto CPSynapseBox = dynamic_cast<Gtk::Box *>(event_box->get_child());
+ if (CPSynapseBox) {
+ auto synapse_children = CPSynapseBox->get_children();
+ auto CPActionFullButton = dynamic_cast<Gtk::Button *>(synapse_children[1]);
+ if (CPActionFullButton) {
+ auto synapse_button = CPActionFullButton->get_children();
+ auto CPSinapseButtonBox = dynamic_cast<Gtk::Box *>(synapse_button[0]);
+ if (CPSinapseButtonBox) {
+ auto synapse_button_content = CPSinapseButtonBox->get_children();
+ return dynamic_cast<Gtk::Label *>(synapse_button_content[1]);
+ }
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+void CommandPalette::load_app_actions()
+{
+ auto gapp = InkscapeApplication::instance()->gtk_app();
+ std::vector<ActionPtrName> all_actions_info;
+
+ std::vector<Glib::ustring> actions = gapp->list_actions();
+ for (const auto &action : actions) {
+ generate_action_operation(get_action_ptr_name("app." + action), true);
+ }
+}
+
+void CommandPalette::load_win_doc_actions()
+{
+ if (auto window = InkscapeApplication::instance()->get_active_window(); window) {
+ std::vector<Glib::ustring> actions = window->list_actions();
+ for (auto action : actions) {
+ generate_action_operation(get_action_ptr_name("win." + action), true);
+ }
+
+ if (auto document = window->get_document(); document) {
+ auto map = document->getActionGroup();
+ if (map) {
+ std::vector<Glib::ustring> actions = map->list_actions();
+ for (auto action : actions) {
+ generate_action_operation(get_action_ptr_name("doc." + action), true);
+ }
+ } else {
+ std::cerr << "CommandPalette::load_win_doc_actions: No document map!" << std::endl;
+ }
+ }
+ }
+}
+
+Gtk::Box *CommandPalette::get_base_widget()
+{
+ return _CPBase;
+}
+
+// CPHistoryXML ---------------------------------------------------------------
+CPHistoryXML::CPHistoryXML()
+ : _file_path(IO::Resource::profile_path("cphistory.xml"))
+{
+ _xml_doc = sp_repr_read_file(_file_path.c_str(), nullptr);
+ if (not _xml_doc) {
+ _xml_doc = sp_repr_document_new("cphistory");
+
+ /* STRUCTURE EXAMPLE ------------------ Illustration 1
+ <cphistory>
+ <operations>
+ <action> full.action_name </action>
+ <import> uri </import>
+ <export> uri </export>
+ </operations>
+ <params>
+ <action name="app.transfor-rotate">
+ <param> 30 </param>
+ <param> 23.5 </param>
+ </action>
+ </params>
+ </cphistory>
+ */
+
+ // Just a pointer, we don't own it, don't free/release/delete
+ auto root = _xml_doc->root();
+
+ // add operation history in this element
+ auto operations = _xml_doc->createElement("operations");
+ root->appendChild(operations);
+
+ // add param history in this element
+ auto params = _xml_doc->createElement("params");
+ root->appendChild(params);
+
+ // This was created by allocated
+ Inkscape::GC::release(operations);
+ Inkscape::GC::release(params);
+
+ // only save if created new
+ save();
+ }
+
+ // Only two children :) check and ensure Illustration 1
+ _operations = _xml_doc->root()->firstChild();
+ _params = _xml_doc->root()->lastChild();
+}
+
+CPHistoryXML::~CPHistoryXML()
+{
+ Inkscape::GC::release(_xml_doc);
+}
+void CPHistoryXML::add_action(const std::string &full_action_name)
+{
+ add_operation(HistoryType::ACTION, full_action_name);
+}
+
+void CPHistoryXML::add_import(const std::string &uri)
+{
+ add_operation(HistoryType::IMPORT_FILE, uri);
+}
+void CPHistoryXML::add_open(const std::string &uri)
+{
+ add_operation(HistoryType::OPEN_FILE, uri);
+}
+
+void CPHistoryXML::add_action_parameter(const std::string &full_action_name, const std::string &param)
+{
+ /* Creates
+ * <params>
+ * +1 <action name="full.action-name">
+ * + <param>30</param>
+ * + <param>60</param>
+ * + <param>90</param>
+ * +1 <action name="full.action-name">
+ * <params>
+ *
+ * + : generally creates
+ * +1: creates once
+ */
+ const auto parameter_node = _xml_doc->createElement("param");
+ const auto parameter_text = _xml_doc->createTextNode(param.c_str());
+
+ parameter_node->appendChild(parameter_text);
+ Inkscape::GC::release(parameter_text);
+
+ for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->next()) {
+ // If this action's node already exists
+ if (full_action_name == action_iter->attribute("name")) {
+ // If the last parameter was the same don't do anything, inner text is also a node hence 2 times last
+ // child
+ if (action_iter->lastChild()->lastChild() && action_iter->lastChild()->lastChild()->content() == param) {
+ Inkscape::GC::release(parameter_node);
+ return;
+ }
+
+ // If last current than parameter is different, add current
+ action_iter->appendChild(parameter_node);
+ Inkscape::GC::release(parameter_node);
+
+ save();
+ return;
+ }
+ }
+
+ // only encountered when the actions element doesn't already exists,so we create that action's element
+ const auto action_node = _xml_doc->createElement("action");
+ action_node->setAttribute("name", full_action_name.c_str());
+ action_node->appendChild(parameter_node);
+
+ _params->appendChild(action_node);
+ save();
+
+ Inkscape::GC::release(action_node);
+ Inkscape::GC::release(parameter_node);
+}
+
+std::optional<History> CPHistoryXML::get_last_operation()
+{
+ auto last_child = _operations->lastChild();
+ if (last_child) {
+ if (const auto operation_type = _get_operation_type(last_child); operation_type.has_value()) {
+ // inner text is a text Node thus last child
+ return History{*operation_type, last_child->lastChild()->content()};
+ }
+ }
+ return std::nullopt;
+}
+std::vector<History> CPHistoryXML::get_operation_history() const
+{
+ // TODO: add max items in history
+ std::vector<History> history;
+ for (auto operation_iter = _operations->firstChild(); operation_iter; operation_iter = operation_iter->next()) {
+ if (const auto operation_type = _get_operation_type(operation_iter); operation_type.has_value()) {
+ history.emplace_back(*operation_type, operation_iter->firstChild()->content());
+ }
+ }
+ return history;
+}
+
+std::vector<std::string> CPHistoryXML::get_action_parameter_history(const std::string &full_action_name) const
+{
+ std::vector<std::string> params;
+ for (auto action_iter = _params->firstChild(); action_iter; action_iter = action_iter->prev()) {
+ // If this action's node already exists
+ if (full_action_name == action_iter->attribute("name")) {
+ // lastChild and prev for LIFO order
+ for (auto param_iter = _params->lastChild(); param_iter; param_iter = param_iter->prev()) {
+ params.emplace_back(param_iter->content());
+ }
+ return params;
+ }
+ }
+ // action not used previously so no params;
+ return {};
+}
+
+void CPHistoryXML::save() const
+{
+ sp_repr_save_file(_xml_doc, _file_path.c_str());
+}
+
+void CPHistoryXML::add_operation(const HistoryType history_type, const std::string &data)
+{
+ std::string operation_type_name;
+ switch (history_type) {
+ // see Illustration 1
+ case HistoryType::ACTION:
+ operation_type_name = "action";
+ break;
+ case HistoryType::IMPORT_FILE:
+ operation_type_name = "import";
+ break;
+ case HistoryType::OPEN_FILE:
+ operation_type_name = "open";
+ break;
+ default:
+ return;
+ }
+ auto operation_to_add = _xml_doc->createElement(operation_type_name.c_str()); // action, import, open
+ auto operation_data = _xml_doc->createTextNode(data.c_str());
+ operation_data->setContent(data.c_str());
+
+ operation_to_add->appendChild(operation_data);
+ _operations->appendChild(operation_to_add);
+
+ Inkscape::GC::release(operation_data);
+ Inkscape::GC::release(operation_to_add);
+
+ save();
+}
+std::optional<HistoryType> CPHistoryXML::_get_operation_type(Inkscape::XML::Node *operation)
+{
+ const std::string operation_type_name = operation->name();
+
+ if (operation_type_name == "action") {
+ return HistoryType::ACTION;
+ } else if (operation_type_name == "import") {
+ return HistoryType::IMPORT_FILE;
+ } else if (operation_type_name == "open") {
+ return HistoryType::OPEN_FILE;
+ } else {
+ return std::nullopt;
+ // unknown HistoryType
+ }
+}
+
+} // 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/dialog/command-palette.h b/src/ui/dialog/command-palette.h
new file mode 100644
index 0000000..4b848fa
--- /dev/null
+++ b/src/ui/dialog/command-palette.h
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * CommandPalette: Class providing Command Palette feature
+ *
+ * Authors:
+ * Abhay Raj Singh <abhayonlyone@gmail.com>
+ *
+ * Copyright (C) 2020 Autors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_COMMAND_PALETTE_H
+#define INKSCAPE_DIALOG_COMMAND_PALETTE_H
+
+#include <utility>
+#include <vector>
+
+#include <giomm/action.h>
+#include <giomm/application.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/label.h>
+#include <gtkmm/listbox.h>
+#include <gtkmm/listboxrow.h>
+#include <gtkmm/recentinfo.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/searchbar.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/viewport.h>
+
+#include "inkscape.h"
+#include "ui/dialog/align-and-distribute.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+// Enables using switch case
+enum class TypeOfVariant
+{
+ NONE,
+ UNKNOWN,
+ BOOL,
+ INT,
+ DOUBLE,
+ STRING,
+ TUPLE_DD
+};
+
+enum class CPMode
+{
+ SEARCH,
+ INPUT, // Input arguments
+ SHELL,
+ HISTORY
+};
+
+enum class HistoryType
+{
+ LPE,
+ ACTION,
+ OPEN_FILE,
+ IMPORT_FILE,
+};
+
+struct History
+{
+ HistoryType history_type;
+ std::string data;
+
+ History(HistoryType ht, std::string &&data)
+ : history_type(ht)
+ , data(data)
+ {}
+};
+
+class CPHistoryXML
+{
+public:
+ // constructors, asssignment, destructor
+ CPHistoryXML();
+ ~CPHistoryXML();
+
+ // Handy wrappers for code clearity
+ void add_action(const std::string &full_action_name);
+
+ void add_import(const std::string &uri);
+ void add_open(const std::string &uri);
+
+ // Remember parameter for action
+ void add_action_parameter(const std::string &full_action_name, const std::string &param);
+
+ std::optional<History> get_last_operation();
+
+ // To construct _CPHistory
+ std::vector<History> get_operation_history() const;
+ // To get parameter history when an action is selected, LIFO stack like so more recent first
+ std::vector<std::string> get_action_parameter_history(const std::string &full_action_name) const;
+
+private:
+ void save() const;
+
+ void add_operation(const HistoryType history_type, const std::string &data);
+
+ static std::optional<HistoryType> _get_operation_type(Inkscape::XML::Node *operation);
+
+ const std::string _file_path;
+
+ Inkscape::XML::Document *_xml_doc;
+ // handy for xml doc child
+ Inkscape::XML::Node *_operations;
+ Inkscape::XML::Node *_params;
+};
+
+class CommandPalette
+{
+public: // API
+ CommandPalette();
+ ~CommandPalette() = default;
+
+ CommandPalette(CommandPalette const &) = delete; // no copy
+ CommandPalette &operator=(CommandPalette const &) = delete; // no assignment
+
+ void open();
+ void close();
+ void toggle();
+
+ Gtk::Box *get_base_widget();
+
+private: // Helpers
+ using ActionPtr = Glib::RefPtr<Gio::Action>;
+ using ActionPtrName = std::pair<ActionPtr, Glib::ustring>;
+
+ /**
+ * Insert actions in _CPSuggestions
+ */
+ void load_app_actions();
+ void load_win_doc_actions();
+
+ void append_recent_file_operation(const Glib::ustring &path, bool is_suggestion, bool is_import = true);
+ bool generate_action_operation(const ActionPtrName &action_ptr_name, const bool is_suggestion);
+
+private: // Signal handlers
+ void on_search();
+
+ int on_filter_general(Gtk::ListBoxRow *child);
+ bool on_filter_full_action_name(Gtk::ListBoxRow *child);
+ bool on_filter_recent_file(Gtk::ListBoxRow *child, bool const is_import);
+
+ bool on_key_press_cpfilter_escape(GdkEventKey *evt);
+ bool on_key_press_cpfilter_search_mode(GdkEventKey *evt);
+ bool on_key_press_cpfilter_input_mode(GdkEventKey *evt, const ActionPtrName &action_ptr_name);
+ bool on_key_press_cpfilter_history_mode(GdkEventKey *evt);
+
+ /**
+ * when search bar is empty
+ */
+ void hide_suggestions();
+
+ /**
+ * when search bar isn't empty
+ */
+ void show_suggestions();
+
+ void on_row_activated(Gtk::ListBoxRow *activated_row);
+ void on_history_selection_changed(Gtk::ListBoxRow *lb);
+
+ bool operate_recent_file(Glib::ustring const &uri, bool const import);
+
+ void on_action_fullname_clicked(const Glib::ustring &action_fullname);
+
+ /**
+ * Implements text matching logic
+ */
+ static bool fuzzy_search(const Glib::ustring &subject, const Glib::ustring &search);
+ static bool normal_search(const Glib::ustring &subject, const Glib::ustring &search);
+ static bool fuzzy_tolerance_search(const Glib::ustring &subject, const Glib::ustring &search);
+ static int fuzzy_points(const Glib::ustring &subject, const Glib::ustring &search);
+ static int fuzzy_tolerance_points(const Glib::ustring &subject, const Glib::ustring &search);
+ static int fuzzy_points_compare(int fuzzy_points_count_1, int fuzzy_points_count_2, int text_len_1, int text_len_2);
+ int on_sort(Gtk::ListBoxRow *row1, Gtk::ListBoxRow *row2);
+ void set_mode(CPMode mode);
+
+ /**
+ * Color addition in searched character
+ */
+ void add_color(Gtk::Label *label, const Glib::ustring &search, const Glib::ustring &subject, bool tooltip=false);
+ void remove_color(Gtk::Label *label, const Glib::ustring &subject, bool tooltip=false);
+ static void add_color_description(Gtk::Label *label, const Glib::ustring &search);
+
+ /**
+ * Executes Action
+ */
+ bool ask_action_parameter(const ActionPtrName &action);
+ static ActionPtrName get_action_ptr_name(const Glib::ustring &full_action_name);
+ bool execute_action(const ActionPtrName &action, const Glib::ustring &value);
+
+ static TypeOfVariant get_action_variant_type(const ActionPtr &action_ptr);
+
+ static std::pair<Gtk::Label *, Gtk::Label *> get_name_desc(Gtk::ListBoxRow *child);
+ Gtk::Label *get_full_action_name(Gtk::ListBoxRow *child);
+
+private: // variables
+ // Widgets
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ Gtk::Box *_CPBase;
+ Gtk::Box *_CPHeader;
+ Gtk::Box *_CPListBase;
+
+ Gtk::SearchBar *_CPSearchBar;
+ Gtk::SearchEntry *_CPFilter;
+
+ Gtk::ListBox *_CPSuggestions;
+ Gtk::ListBox *_CPHistory;
+
+ Gtk::ScrolledWindow *_CPSuggestionsScroll;
+ Gtk::ScrolledWindow *_CPHistoryScroll;
+
+ // Data
+ const int _max_height_requestable = 360;
+ Glib::ustring _search_text;
+
+ // States
+ bool _is_open = false;
+ bool _win_doc_actions_loaded = false;
+
+ // History
+ CPHistoryXML _history_xml;
+ /**
+ * Remember the mode we are in helps in unnecessary signal disconnection and reconnection
+ * Used by set_mode()
+ */
+ CPMode _mode = CPMode::SHELL;
+ // Default value other than SEARCH required
+ // set_mode() switches between mode hence checks if it already in the target mode.
+ // Constructed value is sometimes SEARCH being the first Item for now
+ // set_mode() never attaches the on search listener then
+ // This initialising value can be any thing other than the initial required mode
+ // Example currently it's open in search mode
+
+ /**
+ * Stores the search connection to deactivate when not needed
+ */
+ sigc::connection _cpfilter_search_connection;
+ /**
+ * Stores the key_press connection to deactivate when not needed
+ */
+ sigc::connection _cpfilter_key_press_connection;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_DIALOG_COMMAND_PALETTE_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/dialog/debug.cpp b/src/ui/dialog/debug.cpp
new file mode 100644
index 0000000..b9e7f28
--- /dev/null
+++ b/src/ui/dialog/debug.cpp
@@ -0,0 +1,259 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * A dialog that displays log messages.
+ */
+/* Authors:
+ * Bob Jamison
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004 The Inkscape Organization
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm/box.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/menubar.h>
+#include <gtkmm/scrolledwindow.h>
+#include <glibmm/i18n.h>
+
+#include "debug.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * A very simple dialog for displaying Inkscape messages - implementation.
+ */
+class DebugDialogImpl : public DebugDialog, public Gtk::Dialog
+{
+public:
+ DebugDialogImpl();
+ ~DebugDialogImpl() override;
+
+ void show() override;
+ void hide() override;
+ void clear() override;
+ void message(char const *msg) override;
+ void captureLogMessages() override;
+ void releaseLogMessages() override;
+
+private:
+ Gtk::MenuBar menuBar;
+ Gtk::Menu fileMenu;
+ Gtk::ScrolledWindow textScroll;
+ Gtk::TextView messageText;
+
+ //Handler ID's
+ guint handlerDefault;
+ guint handlerGlibmm;
+ guint handlerAtkmm;
+ guint handlerPangomm;
+ guint handlerGdkmm;
+ guint handlerGtkmm;
+};
+
+void DebugDialogImpl::clear()
+{
+ Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer();
+ buffer->erase(buffer->begin(), buffer->end());
+}
+
+DebugDialogImpl::DebugDialogImpl()
+{
+ set_title(_("Messages"));
+ set_size_request(300, -1);
+ auto mainVBox = get_content_area();
+
+ //## Add a menu for clear()
+ Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem(_("_File"), true));
+ item->set_submenu(fileMenu);
+ menuBar.append(*item);
+
+ item = Gtk::manage(new Gtk::MenuItem(_("_Clear"), true));
+ item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::clear));
+ fileMenu.append(*item);
+
+ item = Gtk::manage(new Gtk::MenuItem(_("Capture log messages")));
+ item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::captureLogMessages));
+ fileMenu.append(*item);
+
+ item = Gtk::manage(new Gtk::MenuItem(_("Release log messages")));
+ item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::releaseLogMessages));
+ fileMenu.append(*item);
+
+ mainVBox->pack_start(menuBar, Gtk::PACK_SHRINK);
+
+
+ //### Set up the text widget
+ messageText.set_editable(false);
+ textScroll.add(messageText);
+ textScroll.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS);
+ mainVBox->pack_start(textScroll);
+
+ show_all_children();
+
+ message("ready.");
+ message("enable log display by setting ");
+ message("dialogs.debug 'redirect' attribute to 1 in preferences.xml");
+
+ handlerDefault = 0;
+ handlerGlibmm = 0;
+ handlerAtkmm = 0;
+ handlerPangomm = 0;
+ handlerGdkmm = 0;
+ handlerGtkmm = 0;
+}
+
+
+DebugDialog *DebugDialog::create()
+{
+ DebugDialog *dialog = new DebugDialogImpl();
+ return dialog;
+}
+
+DebugDialogImpl::~DebugDialogImpl()
+= default;
+
+void DebugDialogImpl::show()
+{
+ //call super()
+ Gtk::Dialog::show();
+ //sp_transientize(GTK_WIDGET(gobj())); //Make transient
+ raise();
+ Gtk::Dialog::present();
+}
+
+void DebugDialogImpl::hide()
+{
+ // call super
+ Gtk::Dialog::hide();
+}
+
+void DebugDialogImpl::message(char const *msg)
+{
+ Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer();
+ Glib::ustring uMsg = msg;
+ if (uMsg[uMsg.length()-1] != '\n')
+ uMsg += '\n';
+ buffer->insert (buffer->end(), uMsg);
+}
+
+/* static instance, to reduce dependencies */
+static DebugDialog *debugDialogInstance = nullptr;
+
+DebugDialog *DebugDialog::getInstance()
+{
+ if (!debugDialogInstance) {
+ debugDialogInstance = new DebugDialogImpl();
+ }
+ return debugDialogInstance;
+}
+
+
+
+void DebugDialog::showInstance()
+{
+ DebugDialog *debugDialog = getInstance();
+ debugDialog->show();
+ // this is not a real memleak because getInstance() only creates a debug dialog once, and returns that instance for all subsequent calls
+ // cppcheck-suppress memleak
+}
+
+
+
+
+/*##### THIS IS THE IMPORTANT PART ##### */
+static void dialogLoggingFunction(const gchar */*log_domain*/,
+ GLogLevelFlags /*log_level*/,
+ const gchar *messageText,
+ gpointer user_data)
+{
+ DebugDialogImpl *dlg = static_cast<DebugDialogImpl *>(user_data);
+ dlg->message(messageText);
+}
+
+
+void DebugDialogImpl::captureLogMessages()
+{
+ /*
+ This might likely need more code, to capture Gtkmm
+ and Glibmm warnings, or maybe just simply grab stdout/stderr
+ */
+ GLogLevelFlags flags = (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL |
+ G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE |
+ G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG);
+ if ( !handlerDefault ) {
+ handlerDefault = g_log_set_handler(nullptr, flags,
+ dialogLoggingFunction, (gpointer)this);
+ }
+ if ( !handlerGlibmm ) {
+ handlerGlibmm = g_log_set_handler("glibmm", flags,
+ dialogLoggingFunction, (gpointer)this);
+ }
+ if ( !handlerAtkmm ) {
+ handlerAtkmm = g_log_set_handler("atkmm", flags,
+ dialogLoggingFunction, (gpointer)this);
+ }
+ if ( !handlerPangomm ) {
+ handlerPangomm = g_log_set_handler("pangomm", flags,
+ dialogLoggingFunction, (gpointer)this);
+ }
+ if ( !handlerGdkmm ) {
+ handlerGdkmm = g_log_set_handler("gdkmm", flags,
+ dialogLoggingFunction, (gpointer)this);
+ }
+ if ( !handlerGtkmm ) {
+ handlerGtkmm = g_log_set_handler("gtkmm", flags,
+ dialogLoggingFunction, (gpointer)this);
+ }
+ message("log capture started");
+}
+
+void DebugDialogImpl::releaseLogMessages()
+{
+ if ( handlerDefault ) {
+ g_log_remove_handler(nullptr, handlerDefault);
+ handlerDefault = 0;
+ }
+ if ( handlerGlibmm ) {
+ g_log_remove_handler("glibmm", handlerGlibmm);
+ handlerGlibmm = 0;
+ }
+ if ( handlerAtkmm ) {
+ g_log_remove_handler("atkmm", handlerAtkmm);
+ handlerAtkmm = 0;
+ }
+ if ( handlerPangomm ) {
+ g_log_remove_handler("pangomm", handlerPangomm);
+ handlerPangomm = 0;
+ }
+ if ( handlerGdkmm ) {
+ g_log_remove_handler("gdkmm", handlerGdkmm);
+ handlerGdkmm = 0;
+ }
+ if ( handlerGtkmm ) {
+ g_log_remove_handler("gtkmm", handlerGtkmm);
+ handlerGtkmm = 0;
+ }
+ message("log capture discontinued");
+}
+
+
+
+} //namespace Dialogs
+} //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/dialog/debug.h b/src/ui/dialog/debug.h
new file mode 100644
index 0000000..4520c73
--- /dev/null
+++ b/src/ui/dialog/debug.h
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Dialog for displaying Inkscape messages
+ */
+/* Authors:
+ * Bob Jamison
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_DIALOGS_DEBUGDIALOG_H
+#define SEEN_UI_DIALOGS_DEBUGDIALOG_H
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/**
+ * @brief A very simple dialog for displaying Inkscape messages.
+ *
+ * Messages sent to g_log(), g_warning(), g_message(), ets, are routed here,
+ * in order to avoid messing with the startup console.
+ */
+class DebugDialog
+{
+public:
+ DebugDialog() = default;;
+ /**
+ * Factory method
+ */
+ static DebugDialog *create();
+
+ /**
+ * Destructor
+ */
+ virtual ~DebugDialog() = default;;
+
+
+ /**
+ * Show the dialog
+ */
+ virtual void show() = 0;
+
+ /**
+ * Do not show the dialog
+ */
+ virtual void hide() = 0;
+
+ /**
+ * @brief Clear all information from the dialog
+ *
+ * Also a public method. Remove all text from the dialog
+ */
+ virtual void clear() = 0;
+
+ /**
+ * Display a message
+ */
+ virtual void message(char const *msg) = 0;
+
+ /**
+ * Redirect g_log() messages to this widget
+ */
+ virtual void captureLogMessages() = 0;
+
+ /**
+ * Return g_log() messages to normal handling
+ */
+ virtual void releaseLogMessages() = 0;
+
+ /**
+ * Factory method. Use this to create a new DebugDialog
+ */
+ static DebugDialog *getInstance();
+
+ /**
+ * Show the instance above
+ */
+ static void showInstance();
+};
+
+} //namespace Dialogs
+} //namespace UI
+} //namespace Inkscape
+
+#endif /* __DEBUGDIALOG_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/dialog/dialog-base.cpp b/src/ui/dialog/dialog-base.cpp
new file mode 100644
index 0000000..154c811
--- /dev/null
+++ b/src/ui/dialog/dialog-base.cpp
@@ -0,0 +1,318 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A base class for all dialogs.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-base.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+#include <glibmm/refptr.h>
+#include <gtkmm/cssprovider.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/scrolledwindow.h>
+#include <iostream>
+
+#include "inkscape.h"
+#include "desktop.h"
+#include "ui/dialog/dialog-data.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog-events.h"
+// get_latin_keyval
+#include "ui/tools/tool-base.h"
+#include "widgets/spw-utilities.h"
+#include "ui/widget/canvas.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * DialogBase constructor.
+ *
+ * @param prefs_path characteristic path to load/save dialog position.
+ * @param dialog_type is the "type" string for the dialog.
+ */
+DialogBase::DialogBase(gchar const *prefs_path, Glib::ustring dialog_type)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , desktop(nullptr)
+ , document(nullptr)
+ , selection(nullptr)
+ , _name("DialogBase")
+ , _prefs_path(prefs_path)
+ , _dialog_type(dialog_type)
+ , _app(InkscapeApplication::instance())
+{
+ auto const &dialog_data = get_dialog_data();
+
+ // Derive a pretty display name for the dialog.
+ auto it = dialog_data.find(dialog_type);
+ if (it != dialog_data.end()) {
+
+ _name = it->second.label; // Already translated
+
+ // remove ellipsis and mnemonics
+ int pos = _name.find("...", 0);
+ if (pos >= 0 && pos < _name.length() - 2) {
+ _name.erase(pos, 3);
+ }
+ pos = _name.find("…", 0);
+ if (pos >= 0 && pos < _name.length()) {
+ _name.erase(pos, 1);
+ }
+ pos = _name.find("_", 0);
+ if (pos >= 0 && pos < _name.length()) {
+ _name.erase(pos, 1);
+ }
+ }
+
+ set_name(_dialog_type); // Essential for dialog functionality
+ property_margin().set_value(1); // Essential for dialog UI
+}
+
+DialogBase::~DialogBase() {
+#ifdef _WIN32
+ // this is bad, but it supposedly fixes some resizng problem on Windows
+ ensure_size();
+#endif
+
+ unsetDesktop();
+};
+
+void DialogBase::ensure_size() {
+ if (desktop) {
+ resize_widget_children(desktop->getToplevel());
+ resize_widget_children(this);
+ }
+}
+
+void DialogBase::on_map() {
+ // Update asks the dialogs if they need their Gtk widgets updated.
+ update();
+ // Set the desktop on_map, although we might want to be smarter about this.
+ // Note: Inkscape::Application::instance().active_desktop() is used here, as it contains current desktop at
+ // the time of dialog creation. Formerly used _app.get_active_view() did not at application start-up.
+ setDesktop(Inkscape::Application::instance().active_desktop());
+ parent_type::on_map();
+ ensure_size();
+}
+
+bool DialogBase::on_key_press_event(GdkEventKey* key_event) {
+ switch (Inkscape::UI::Tools::get_latin_keyval(key_event)) {
+ case GDK_KEY_Escape:
+ defocus_dialog();
+ return true;
+ }
+
+ return parent_type::on_key_press_event(key_event);
+}
+
+
+/**
+ * Highlight notebook where dialog already exists.
+ */
+void DialogBase::blink()
+{
+ Gtk::Notebook *notebook = dynamic_cast<Gtk::Notebook *>(get_parent());
+ if (notebook && notebook->get_is_drawable()) {
+ // Switch notebook to this dialog.
+ notebook->set_current_page(notebook->page_num(*this));
+ notebook->get_style_context()->add_class("blink");
+
+ // Add timer to turn off blink.
+ sigc::slot<bool ()> slot = sigc::mem_fun(*this, &DialogBase::blink_off);
+ sigc::connection connection = Glib::signal_timeout().connect(slot, 1000); // msec
+ }
+}
+
+void DialogBase::focus_dialog() {
+ if (auto window = dynamic_cast<Gtk::Window*>(get_toplevel())) {
+ window->present();
+ }
+
+ // widget that had focus, if any
+ if (auto child = get_focus_child()) {
+ child->grab_focus();
+ }
+ else {
+ // find first focusable widget
+ if (auto child = sp_find_focusable_widget(this)) {
+ child->grab_focus();
+ }
+ }
+}
+
+void DialogBase::defocus_dialog() {
+ if (auto wnd = dynamic_cast<Gtk::Window*>(get_toplevel())) {
+ // defocus floating dialog:
+ sp_dialog_defocus_cpp(wnd);
+
+ // for docked dialogs, move focus to canvas
+ if (auto desktop = getDesktop()) {
+ desktop->getCanvas()->grab_focus();
+ }
+ }
+}
+
+/**
+ * Callback to reset the dialog highlight.
+ */
+bool DialogBase::blink_off()
+{
+ Gtk::Notebook *notebook = dynamic_cast<Gtk::Notebook *>(get_parent());
+ if (notebook && notebook->get_is_drawable()) {
+ notebook->get_style_context()->remove_class("blink");
+ }
+ return false;
+}
+
+/**
+ * Called when the desktop might have changed for this dialog.
+ */
+void DialogBase::setDesktop(SPDesktop *new_desktop)
+{
+ if (desktop == new_desktop) {
+ return;
+ }
+
+ unsetDesktop();
+
+ if (new_desktop) {
+ desktop = new_desktop;
+
+ if (auto sel = desktop->getSelection()) {
+ selection = sel;
+ _select_changed = selection->connectChanged([this](Inkscape::Selection *selection) {
+ _changed_while_hidden = !_showing;
+ if (_showing)
+ selectionChanged(selection);
+ });
+ _select_modified = selection->connectModified([this](Inkscape::Selection *selection, guint flags) {
+ _modified_while_hidden = !_showing;
+ _modified_flags = flags;
+ if (_showing)
+ selectionModified(selection, flags);
+ });
+ }
+
+ _doc_replaced = desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(*this, &DialogBase::setDocument)));
+ _desktop_destroyed = desktop->connectDestroy(sigc::mem_fun(*this, &DialogBase::desktopDestroyed));
+ this->setDocument(desktop->getDocument());
+
+ if (desktop->getSelection()) {
+ selectionChanged(selection);
+ }
+ set_sensitive(true);
+ }
+
+ desktopReplaced();
+}
+
+//
+void DialogBase::fix_inner_scroll(Gtk::Widget *scrollwindow)
+{
+ auto scrollwin = dynamic_cast<Gtk::ScrolledWindow *>(scrollwindow);
+ auto viewport = dynamic_cast<Gtk::ScrolledWindow *>(scrollwin->get_child());
+ Gtk::Widget *child = nullptr;
+ if (viewport) { //some widgets has viewportother not
+ child = viewport->get_child();
+ } else {
+ child = scrollwin->get_child();
+ }
+ if (child && scrollwin) {
+ Glib::RefPtr<Gtk::Adjustment> adjustment = scrollwin->get_vadjustment();
+ child->signal_scroll_event().connect([=](GdkEventScroll* event) {
+ auto container = dynamic_cast<Gtk::Container *>(this);
+ if (container) {
+ std::vector<Gtk::Widget*> widgets = container->get_children();
+ if (widgets.size()) {
+ auto parentscroll = dynamic_cast<Gtk::ScrolledWindow *>(widgets[0]);
+ if (parentscroll) {
+ if (event->delta_y > 0 && (adjustment->get_value() + adjustment->get_page_size()) == adjustment->get_upper()) {
+ parentscroll->event((GdkEvent*)event);
+ return true;
+ } else if (event->delta_y < 0 && adjustment->get_value() == adjustment->get_lower()) {
+ parentscroll->event((GdkEvent*)event);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ });
+ }
+}
+
+/**
+ * function called from notebook dialog that performs an update of the dialog and sets the dialog showing state true
+ */
+void
+DialogBase::setShowing(bool showing) {
+ _showing = showing;
+ if (showing && _changed_while_hidden) {
+ selectionChanged(getSelection());
+ _changed_while_hidden = false;
+ }
+ if (showing && _modified_while_hidden) {
+ selectionModified(getSelection(), _modified_flags);
+ _modified_while_hidden = false;
+ }
+}
+
+/**
+ * Called to destruct desktops, must not call virtuals
+ */
+void DialogBase::unsetDesktop()
+{
+ desktop = nullptr;
+ document = nullptr;
+ selection = nullptr;
+ _desktop_destroyed.disconnect();
+ _doc_replaced.disconnect();
+ _select_changed.disconnect();
+ _select_modified.disconnect();
+}
+
+void DialogBase::desktopDestroyed(SPDesktop* old_desktop)
+{
+ if (old_desktop == desktop && desktop) {
+ unsetDesktop();
+ desktopReplaced();
+ set_sensitive(false);
+ }
+}
+
+/**
+ * Called when the document might have changed, called from setDesktop too.
+ */
+void DialogBase::setDocument(SPDocument *new_document)
+{
+ if (document != new_document) {
+ document = new_document;
+ documentReplaced();
+ }
+}
+
+} // 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/dialog/dialog-base.h b/src/ui/dialog/dialog-base.h
new file mode 100644
index 0000000..a0f44ea
--- /dev/null
+++ b/src/ui/dialog/dialog-base.h
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INK_DIALOG_BASE_H
+#define INK_DIALOG_BASE_H
+
+/** @file
+ * @brief A base class for all dialogs.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+
+#include "inkscape-application.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * DialogBase is the base class for the dialog system.
+ *
+ * Each dialog has a reference to the application, in order to update its inner focus
+ * (be it of the active desktop, document, selection, etc.) in the update() method.
+ *
+ * DialogBase derived classes' instances live in DialogNotebook classes and are managed by
+ * DialogContainer classes. DialogContainer instances can have at most one type of dialog,
+ * differentiated by the associated type.
+ */
+class DialogBase : public Gtk::Box
+{
+ using parent_type = Gtk::Box;
+
+public:
+ DialogBase(char const *prefs_path = nullptr, Glib::ustring dialog_type = "");
+ DialogBase(DialogBase const &) = delete;
+ DialogBase &operator=(DialogBase const &) = delete;
+ ~DialogBase() override;
+
+ /**
+ * The update() method is essential to Gtk state management. DialogBase implementations get updated whenever
+ * a new focus event happens if they are in a DialogWindow or if they are in the currently focused window.
+ *
+ * DO NOT use update to keep SPDesktop, SPDocument or Selection states, use the virtual functions below.
+ */
+ virtual void update() {}
+
+ // Public for future use, say if the desktop is smartly set when docking dialogs.
+ void setDesktop(SPDesktop *new_desktop);
+
+ void on_map() override;
+
+ /*
+ * Often the dialog won't request the right size until the window has
+ * been pushed to resize all it's children. We do this on dialog creation
+ * and destruction.
+ */
+ void ensure_size();
+
+ // Getters and setters
+ Glib::ustring get_name() { return _name; };
+ const Glib::ustring& getPrefsPath() const { return _prefs_path; }
+ Glib::ustring const &get_type() const { return _dialog_type; }
+
+ void blink();
+ // find focusable widget to grab focus
+ void focus_dialog();
+ // return focus back to canvas
+ void defocus_dialog();
+ bool getShowing() { return _showing; }
+ // fix children scrolled windows to send outer scroll when his own reach limits
+ void fix_inner_scroll(Gtk::Widget *child);
+ // Too many dialogs have unprotected calls to ask for this data
+ SPDesktop *getDesktop() const { return desktop; }
+protected:
+ InkscapeApplication *getApp() const { return _app; }
+ SPDocument *getDocument() const { return document; }
+ Selection *getSelection() const { return selection; }
+ friend class DialogNotebook;
+ void setShowing(bool showing);
+ Glib::ustring _name; // Gtk widget name (must be set!)
+ Glib::ustring const _prefs_path; // Stores characteristic path for loading/saving the dialog position.
+ Glib::ustring const _dialog_type; // Type of dialog (we could just use _pref_path?).
+private:
+ bool blink_off(); // timer callback
+ bool on_key_press_event(GdkEventKey* key_event) override;
+ // return if dialog is on visible tab
+ bool _showing = true;
+ void unsetDesktop();
+ void desktopDestroyed(SPDesktop* old_desktop);
+ void setDocument(SPDocument *new_document);
+ /**
+ * Called when the desktop has certainly changed. It may have changed to nullptr
+ * when destructing the dialog, so the override should expect nullptr too.
+ */
+ virtual void desktopReplaced() {}
+ virtual void documentReplaced() {}
+ virtual void selectionChanged(Inkscape::Selection *selection) {};
+ virtual void selectionModified(Inkscape::Selection *selection, guint flags) {};
+
+ sigc::connection _desktop_destroyed;
+ sigc::connection _doc_replaced;
+ sigc::connection _select_changed;
+ sigc::connection _select_modified;
+
+ int _modified_flags = 0;
+ bool _modified_while_hidden = false;
+ bool _changed_while_hidden = false;
+
+ InkscapeApplication *_app; // Used for state management
+ SPDesktop *desktop;
+ SPDocument *document;
+ Selection *selection;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INK_DIALOG_BASE_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/dialog/dialog-container.cpp b/src/ui/dialog/dialog-container.cpp
new file mode 100644
index 0000000..be5e587
--- /dev/null
+++ b/src/ui/dialog/dialog-container.cpp
@@ -0,0 +1,1153 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A widget that manages DialogNotebook's and other widgets inside a horizontal DialogMultipaned.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-container.h"
+
+#include <glibmm/i18n.h>
+#include <giomm/file.h>
+#include <glibmm/keyfile.h>
+#include <gtkmm/box.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/image.h>
+
+#include "enums.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+// #include "ui/dialog/align-and-distribute.h"
+#include "ui/dialog/clonetiler.h"
+#include "ui/dialog/dialog-data.h"
+#include "ui/dialog/dialog-multipaned.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/dialog-window.h"
+#include "ui/dialog/document-properties.h"
+#include "ui/dialog/document-resources.h"
+#include "ui/dialog/export.h"
+#include "ui/dialog/fill-and-stroke.h"
+#include "ui/dialog/filter-effects-dialog.h"
+#include "ui/dialog/find.h"
+#include "ui/dialog/font-collections-manager.h"
+#include "ui/dialog/glyphs.h"
+#include "ui/dialog/icon-preview.h"
+#include "ui/dialog/inkscape-preferences.h"
+#include "ui/dialog/input.h"
+#include "ui/dialog/livepatheffect-editor.h"
+#include "ui/dialog/memory.h"
+#include "ui/dialog/messages.h"
+#include "ui/dialog/object-attributes.h"
+#include "ui/dialog/object-properties.h"
+#include "ui/dialog/objects.h"
+#include "ui/dialog/paint-servers.h"
+#include "ui/dialog/selectorsdialog.h"
+#if WITH_GSPELL
+#include "ui/dialog/spellcheck.h"
+#endif
+#include "ui/dialog/svg-fonts-dialog.h"
+#include "ui/dialog/swatches.h"
+#include "ui/dialog/symbols.h"
+#include "ui/dialog/text-edit.h"
+#include "ui/dialog/tile.h"
+#include "ui/dialog/tracedialog.h"
+#include "ui/dialog/transformation.h"
+#include "ui/dialog/undo-history.h"
+#include "ui/dialog/xml-tree.h"
+#include "ui/icon-names.h"
+#include "ui/themes.h"
+#include "ui/widget/canvas-grid.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+DialogContainer::~DialogContainer() {
+ // delete columns; desktop widget deletes dialog container before it get "unrealized",
+ // so it doesn't get a chance to remove them
+ delete columns;
+}
+
+DialogContainer::DialogContainer(InkscapeWindow* inkscape_window)
+ : _inkscape_window(inkscape_window)
+{
+ g_assert(_inkscape_window != nullptr);
+
+ get_style_context()->add_class("DialogContainer");
+
+ // Setup main column
+ columns = Gtk::manage(new DialogMultipaned(Gtk::ORIENTATION_HORIZONTAL));
+
+ connections.emplace_back(columns->signal_prepend_drag_data().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::prepend_drop), columns)));
+
+ connections.emplace_back(columns->signal_append_drag_data().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::append_drop), columns)));
+
+ // Setup drop targets.
+ target_entries.emplace_back(Gtk::TargetEntry("GTK_NOTEBOOK_TAB"));
+ columns->set_target_entries(target_entries);
+
+ add(*columns);
+
+ // Should probably be moved to window.
+ // connections.emplace_back(signal_unmap().connect(sigc::mem_fun(*this, &DialogContainer::cb_on_unmap)));
+
+ show_all_children();
+}
+
+DialogMultipaned *DialogContainer::create_column()
+{
+ DialogMultipaned *column = Gtk::manage(new DialogMultipaned(Gtk::ORIENTATION_VERTICAL));
+
+ connections.emplace_back(column->signal_prepend_drag_data().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::prepend_drop), column)));
+
+ connections.emplace_back(column->signal_append_drag_data().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::append_drop), column)));
+
+ connections.emplace_back(column->signal_now_empty().connect(
+ sigc::bind<DialogMultipaned *>(sigc::mem_fun(*this, &DialogContainer::column_empty), column)));
+
+ column->set_target_entries(target_entries);
+
+ return column;
+}
+
+/**
+ * Get an instance of a DialogBase dialog using the associated dialog name.
+ */
+std::unique_ptr<DialogBase> DialogContainer::dialog_factory(Glib::ustring const &dialog_type)
+{
+ // clang-format off
+ if (dialog_type == "AlignDistribute") return std::make_unique<ArrangeDialog>();
+ else if (dialog_type == "CloneTiler") return std::make_unique<CloneTiler>();
+ else if (dialog_type == "DocumentProperties") return std::make_unique<DocumentProperties>();
+ else if (dialog_type == "DocumentResources") return std::make_unique<DocumentResources>();
+ else if (dialog_type == "Export") return std::make_unique<Export>();
+ else if (dialog_type == "FillStroke") return std::make_unique<FillAndStroke>();
+ else if (dialog_type == "FilterEffects") return std::make_unique<FilterEffectsDialog>();
+ else if (dialog_type == "Find") return std::make_unique<Find>();
+ else if (dialog_type == "FontCollections") return std::make_unique<FontCollectionsManager>();
+ else if (dialog_type == "Glyphs") return std::make_unique<GlyphsPanel>();
+ else if (dialog_type == "IconPreview") return std::make_unique<IconPreviewPanel>();
+ else if (dialog_type == "Input") return InputDialog::create();
+ else if (dialog_type == "LivePathEffect") return std::make_unique<LivePathEffectEditor>();
+ else if (dialog_type == "Memory") return std::make_unique<Memory>();
+ else if (dialog_type == "Messages") return std::make_unique<Messages>();
+ else if (dialog_type == "ObjectAttributes") return std::make_unique<ObjectAttributes>();
+ else if (dialog_type == "ObjectProperties") return std::make_unique<ObjectProperties>();
+ else if (dialog_type == "Objects") return std::make_unique<ObjectsPanel>();
+ else if (dialog_type == "PaintServers") return std::make_unique<PaintServersDialog>();
+ else if (dialog_type == "Preferences") return std::make_unique<InkscapePreferences>();
+ else if (dialog_type == "Selectors") return std::make_unique<SelectorsDialog>();
+ else if (dialog_type == "SVGFonts") return std::make_unique<SvgFontsDialog>();
+ else if (dialog_type == "Swatches") return std::make_unique<SwatchesPanel>();
+ else if (dialog_type == "Symbols") return std::make_unique<SymbolsDialog>();
+ else if (dialog_type == "Text") return std::make_unique<TextEdit>();
+ else if (dialog_type == "Trace") return TraceDialog::create();
+ else if (dialog_type == "Transform") return std::make_unique<Transformation>();
+ else if (dialog_type == "UndoHistory") return std::make_unique<UndoHistory>();
+ else if (dialog_type == "XMLEditor") return std::make_unique<XmlTree>();
+#if WITH_GSPELL
+ else if (dialog_type == "Spellcheck") return std::make_unique<SpellCheck>();
+#endif
+#ifdef DEBUG
+ else if (dialog_type == "Prototype") return std::make_unique<Prototype>();
+#endif
+ else {
+ std::cerr << "DialogContainer::dialog_factory: Unhandled dialog: " << dialog_type.raw() << std::endl;
+ return nullptr;
+ }
+ // clang-format on
+}
+
+// Create the notebook tab
+Gtk::Widget *DialogContainer::create_notebook_tab(Glib::ustring label_str, Glib::ustring image_str, const Glib::ustring shortcut)
+{
+ Gtk::Label *label = Gtk::manage(new Gtk::Label(label_str));
+ Gtk::Image *image = Gtk::manage(new Gtk::Image());
+ Gtk::Button *close = Gtk::manage(new Gtk::Button());
+ image->set_from_icon_name(image_str, Gtk::ICON_SIZE_MENU);
+ Gtk::Box *tab = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 2));
+ close->set_image_from_icon_name("window-close");
+ close->set_halign(Gtk::ALIGN_END);
+ close->set_tooltip_text(_("Close Tab"));
+ close->get_style_context()->add_class("close-button");
+ Glib::ustring label_str_fix = label_str;
+ label_str_fix = Glib::Regex::create("\\W")->replace_literal(label_str_fix, 0, "-", (Glib::RegexMatchFlags)0);
+ tab->get_style_context()->add_class(label_str_fix);
+ tab->pack_start(*image);
+ tab->pack_end(*close);
+ tab->pack_end(*label);
+ tab->show_all();
+
+ // Workaround to the fact that Gtk::Box doesn't receive on_button_press event
+ Gtk::EventBox *cover = Gtk::manage(new Gtk::EventBox());
+ cover->add(*tab);
+
+ // Add shortcut tooltip
+ if (shortcut.size() > 0) {
+ auto tlabel = shortcut;
+ int pos = tlabel.find("&", 0);
+ if (pos >= 0 && pos < tlabel.length()) {
+ tlabel.replace(pos, 1, "&amp;");
+ }
+ tab->set_tooltip_markup(label_str + " (<b>" + tlabel + "</b>)");
+ } else {
+ tab->set_tooltip_text(label_str);
+ }
+
+ return cover;
+}
+
+// find dialog's multipaned parent; is there a better way?
+DialogMultipaned* get_dialog_parent(DialogBase* dialog) {
+ if (!dialog) return nullptr;
+
+ // dialogs are nested inside Gtk::Notebook
+ if (auto notebook = dynamic_cast<Gtk::Notebook*>(dialog->get_parent())) {
+ // notebooks are inside viewport, inside scrolled window
+ if (auto viewport = dynamic_cast<Gtk::Viewport*>(notebook->get_parent())) {
+ if (auto scroll = dynamic_cast<Gtk::ScrolledWindow*>(viewport->get_parent())) {
+ // finally get the panel
+ if (auto panel = dynamic_cast<DialogMultipaned*>(scroll->get_parent())) {
+ return panel;
+ }
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+/**
+ * Add new dialog to the current container or in a floating window, based on preferences.
+ */
+void DialogContainer::new_dialog(const Glib::ustring& dialog_type )
+{
+ // Open all dialogs as floating, if set in preferences
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs == nullptr) {
+ return;
+ }
+
+ int dockable = prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE);
+ bool floating = DialogManager::singleton().should_open_floating(dialog_type);
+ if (dockable == PREFS_DIALOGS_BEHAVIOR_FLOATING || floating) {
+ new_floating_dialog(dialog_type);
+ } else {
+ new_dialog(dialog_type, nullptr);
+ }
+
+ if (DialogBase* dialog = find_existing_dialog(dialog_type)) {
+ dialog->focus_dialog();
+ }
+}
+
+
+DialogBase* DialogContainer::find_existing_dialog(const Glib::ustring& dialog_type) {
+ DialogBase *existing_dialog = get_dialog(dialog_type);
+ if (!existing_dialog) {
+ existing_dialog = DialogManager::singleton().find_floating_dialog(dialog_type);
+ }
+ return existing_dialog;
+}
+
+/**
+ * Overloaded new_dialog
+ */
+void DialogContainer::new_dialog(const Glib::ustring& dialog_type, DialogNotebook *notebook)
+{
+ columns->ensure_multipaned_children();
+
+ // Limit each container to containing one of any type of dialog.
+ if (DialogBase* existing_dialog = find_existing_dialog(dialog_type)) {
+ // make sure parent window is not hidden/collapsed
+ if (auto panel = get_dialog_parent(existing_dialog)) {
+ panel->show();
+ }
+ // found existing dialog; blink & exit
+ existing_dialog->blink();
+ return;
+ }
+
+ // Create the dialog widget
+ DialogBase *dialog = dialog_factory(dialog_type).release(); // Evil, but necessitated by GTK.
+
+ if (!dialog) {
+ std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type.raw() << std::endl;
+ return;
+ }
+
+ // manage the dialog instance
+ dialog = Gtk::manage(dialog);
+
+ // Create the notebook tab
+ auto const &dialog_data = get_dialog_data();
+ Glib::ustring image("inkscape-logo");
+ auto it = dialog_data.find(dialog_type);
+ if (it != dialog_data.end()) {
+ image = it->second.icon_name;
+ }
+
+ Glib::ustring label;
+ Glib::ustring action_name = "win.dialog-open('" + dialog_type + "')";
+ auto app = InkscapeApplication::instance();
+ std::vector<Glib::ustring> accels = app->gtk_app()->get_accels_for_action(action_name);
+ if (accels.size() > 0) {
+ guint key = 0;
+ Gdk::ModifierType mods;
+ Gtk::AccelGroup::parse(accels[0], key, mods);
+ label = Gtk::AccelGroup::get_label(key, mods);
+ }
+
+ Gtk::Widget *tab = create_notebook_tab(dialog->get_name(), image, label);
+
+ // If not from notebook menu add at top of last column.
+ if (!notebook) {
+ // Look to see if last column contains a multipane. If not, add one.
+ DialogMultipaned *last_column = dynamic_cast<DialogMultipaned *>(columns->get_last_widget());
+ if (!last_column) {
+ last_column = create_column();
+ columns->append(last_column);
+ }
+
+ // Look to see if first widget in column is notebook, if not add one.
+ notebook = dynamic_cast<DialogNotebook *>(last_column->get_first_widget());
+ if (!notebook) {
+ notebook = Gtk::manage(new DialogNotebook(this));
+ last_column->prepend(notebook);
+ }
+ }
+
+ // Add dialog
+ notebook->add_page(*dialog, *tab, dialog->get_name());
+
+ if (auto panel = dynamic_cast<DialogMultipaned*>(notebook->get_parent())) {
+ // if panel is collapsed, show it now, or else new dialog will be mysteriously missing
+ panel->show_all();
+ }
+}
+
+// recreate dialogs hosted (docked) in a floating DialogWindow; window will be created
+bool DialogContainer::recreate_dialogs_from_state(InkscapeWindow* inkscape_window, const Glib::KeyFile* keyfile)
+{
+ g_assert(inkscape_window != nullptr);
+
+ bool restored = false;
+ // Step 1: check if we want to load the state
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int save_state = prefs->getInt("/options/savedialogposition/value", PREFS_DIALOGS_STATE_SAVE);
+ if (save_state == PREFS_DIALOGS_STATE_NONE) {
+ return restored; // User has turned off this feature in Preferences
+ }
+
+ // if it isn't dockable, all saved docked dialogs are made floating
+ bool is_dockable =
+ prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE) != PREFS_DIALOGS_BEHAVIOR_FLOATING;
+
+ if (!is_dockable)
+ return false; // not applicable if docking is off
+
+ // Step 2: get the number of windows; should be 1
+ int windows_count = 0;
+ try {
+ // we may have no 'Windows' initially when recreating floating dialog state (state is empty)
+ if (keyfile->has_group("Windows") && keyfile->has_key("Windows", "Count")) {
+ windows_count = keyfile->get_integer("Windows", "Count");
+ }
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+
+ // Step 3: for each window, load its state.
+ for (int window_idx = 0; window_idx < windows_count; ++window_idx) {
+ Glib::ustring group_name = "Window" + std::to_string(window_idx);
+
+ bool has_position = keyfile->has_key(group_name, "Position") && keyfile->get_boolean(group_name, "Position");
+ window_position_t pos;
+ if (has_position) { // floating window position recorded?
+ pos.x = keyfile->get_integer(group_name, "x");
+ pos.y = keyfile->get_integer(group_name, "y");
+ pos.width = keyfile->get_integer(group_name, "width");
+ pos.height = keyfile->get_integer(group_name, "height");
+ }
+ // Step 3.0: read the window parameters
+ int column_count = 0;
+ try {
+ column_count = keyfile->get_integer(group_name, "ColumnCount");
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+
+ // Step 3.1: get the window's container columns where we want to create the dialogs
+ DialogWindow *dialog_window = new DialogWindow(inkscape_window, nullptr);
+ DialogContainer *active_container = dialog_window->get_container();
+ DialogMultipaned *active_columns = active_container ? active_container->get_columns() : nullptr;
+
+ if (!active_container || !active_columns) {
+ continue;
+ }
+
+ // Step 3.2: for each column, load its state
+ for (int column_idx = 0; column_idx < column_count; ++column_idx) {
+ Glib::ustring column_group_name = group_name + "Column" + std::to_string(column_idx);
+
+ // Step 3.2.0: read the column parameters
+ int notebook_count = 0;
+ bool before_canvas = false;
+ try {
+ notebook_count = keyfile->get_integer(column_group_name, "NotebookCount");
+ if (keyfile->has_key(column_group_name, "BeforeCanvas")) {
+ before_canvas = keyfile->get_boolean(column_group_name, "BeforeCanvas");
+ }
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+
+ // Step 3.2.1: create the column
+ DialogMultipaned *column = active_container->create_column();
+
+ before_canvas ? active_columns->prepend(column) : active_columns->append(column);
+
+ // Step 3.2.2: for each noteboook, load its dialogs
+ for (int notebook_idx = 0; notebook_idx < notebook_count; ++notebook_idx) {
+ Glib::ustring key = "Notebook" + std::to_string(notebook_idx) + "Dialogs";
+
+ // Step 3.2.2.0 read the list of dialogs in the current notebook
+ std::vector<Glib::ustring> dialogs;
+ try {
+ dialogs = keyfile->get_string_list(column_group_name, key);
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+
+ if (!dialogs.size()) {
+ continue;
+ }
+
+ DialogNotebook *notebook = nullptr;
+ auto const &dialog_data = get_dialog_data();
+
+ // Step 3.2.2.1 create each dialog in the current notebook
+ for (auto type : dialogs) {
+ if (DialogManager::singleton().find_floating_dialog(type)) {
+ // avoid duplicates
+ continue;
+ }
+
+ if (dialog_data.find(type) != dialog_data.end()) {
+ if (!notebook) {
+ notebook = Gtk::manage(new DialogNotebook(active_container));
+ column->append(notebook);
+ }
+ active_container->new_dialog(type, notebook);
+ } else {
+ std::cerr << "recreate_dialogs_from_state: invalid dialog type: " << type.raw() << std::endl;
+ }
+ }
+ }
+ }
+
+ if (has_position) {
+ dm_restore_window_position(*dialog_window, pos);
+ }
+ else {
+ dialog_window->update_window_size_to_fit_children();
+ }
+ dialog_window->show_all();
+ // Set the style and icon theme of the new menu based on the desktop
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+ restored = true;
+ }
+
+ return restored;
+}
+
+/**
+ * Add a new floating dialog (or reuse existing one if it's already up)
+ */
+DialogWindow *DialogContainer::new_floating_dialog(const Glib::ustring& dialog_type)
+{
+ return create_new_floating_dialog(dialog_type, true);
+}
+
+DialogWindow *DialogContainer::create_new_floating_dialog(const Glib::ustring& dialog_type, bool blink)
+{
+ // check if this dialog is already open
+ if (DialogBase* existing_dialog = find_existing_dialog(dialog_type)) {
+ // found existing dialog; blink & exit
+ if (blink) {
+ existing_dialog->blink();
+ // show its window if it is hidden
+ if (auto window = DialogManager::singleton().find_floating_dialog_window(dialog_type)) {
+ DialogManager::singleton().set_floating_dialog_visibility(window, true);
+ }
+ }
+ return nullptr;
+ }
+
+ // check if this dialog *was* open and floating; if so recreate its window
+ if (auto state = DialogManager::singleton().find_dialog_state(dialog_type)) {
+ if (recreate_dialogs_from_state(_inkscape_window, state.get())) {
+ return nullptr;
+ }
+ }
+
+ // Create the dialog widget
+ DialogBase *dialog = dialog_factory(dialog_type).release(); // Evil, but necessitated by GTK.
+
+ if (!dialog) {
+ std::cerr << "DialogContainer::new_dialog(): couldn't find dialog for: " << dialog_type.raw() << std::endl;
+ return nullptr;
+ }
+
+ // manage the dialog instance
+ dialog = Gtk::manage(dialog);
+
+ // Create the notebook tab
+ gchar* image = nullptr;
+
+ Glib::ustring label;
+ Glib::ustring action_name = "win.dialog-open('" + dialog_type + "')";
+ auto app = InkscapeApplication::instance();
+ std::vector<Glib::ustring> accels = app->gtk_app()->get_accels_for_action(action_name);
+ if (accels.size() > 0) {
+ guint key = 0;
+ Gdk::ModifierType mods;
+ Gtk::AccelGroup::parse(accels[0], key, mods);
+ label = Gtk::AccelGroup::get_label(key, mods);
+ }
+
+ Gtk::Widget *tab =
+ create_notebook_tab(dialog->get_name(), image ? Glib::ustring(image) : INKSCAPE_ICON("inkscape-logo"), label);
+
+ // New temporary noteboook
+ DialogNotebook *notebook = Gtk::manage(new DialogNotebook(this));
+ notebook->add_page(*dialog, *tab, dialog->get_name());
+
+ return notebook->pop_tab_callback();
+}
+
+// toggle dialogs (visibility) is invoked on a top container embedded in Inkscape window
+void DialogContainer::toggle_dialogs()
+{
+ // check how many dialog panels are visible and how many are hidden
+ // we use this info to decide what it means to toggle visibility
+ int visible = 0;
+ int hidden = 0;
+ for (auto child : columns->get_children()) {
+ // only examine panels, skip drop zones and handles
+ if (auto panel = dynamic_cast<DialogMultipaned*>(child)) {
+ if (panel->is_visible()) {
+ ++visible;
+ }
+ else {
+ ++hidden;
+ }
+ }
+ }
+
+ // next examine floating dialogs
+ auto windows = DialogManager::singleton().get_all_floating_dialog_windows();
+ for (auto wnd : windows) {
+ if (wnd->is_visible()) {
+ ++visible;
+ }
+ else {
+ ++hidden;
+ }
+ }
+
+ bool show_dialogs = true;
+ // if some dialogs are hidden, toggle will first show them;
+ // another option could be to hide all if some dialogs are visible
+ if (hidden > 0) {
+ show_dialogs = true;
+ }
+ else {
+ // if everything's visible, hide them
+ show_dialogs = false;
+ }
+
+ // set visibility of floating dialogs
+ for (auto wnd : windows) {
+ DialogManager::singleton().set_floating_dialog_visibility(wnd, show_dialogs);
+ }
+
+ // set visibility of docked dialogs
+ columns->toggle_multipaned_children(show_dialogs);
+}
+
+// Update dialogs
+void DialogContainer::update_dialogs()
+{
+ for_each(dialogs.begin(), dialogs.end(), [&](auto dialog) { dialog.second->update(); });
+}
+
+void DialogContainer::set_inkscape_window(InkscapeWindow* inkscape_window)
+{
+ g_assert(inkscape_window != nullptr);
+ _inkscape_window = inkscape_window;
+ auto desktop = _inkscape_window->get_desktop();
+ for_each(dialogs.begin(), dialogs.end(), [&](auto dialog) { dialog.second->setDesktop(desktop); });
+}
+
+bool DialogContainer::has_dialog_of_type(DialogBase *dialog)
+{
+ return (dialogs.find(dialog->get_type()) != dialogs.end());
+}
+
+DialogBase *DialogContainer::get_dialog(const Glib::ustring& dialog_type)
+{
+ auto found = dialogs.find(dialog_type);
+ if (found != dialogs.end()) {
+ return found->second;
+ }
+ return nullptr;
+}
+
+// Add dialog to list.
+void DialogContainer::link_dialog(DialogBase *dialog)
+{
+ dialogs.insert(std::pair<Glib::ustring, DialogBase *>(dialog->get_type(), dialog));
+
+ DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel());
+ if (window) {
+ window->update_dialogs();
+ }
+ else {
+ // dialog without DialogWindow has been docked; remove it's floating state
+ // so if user closes and reopens it, it shows up docked again, not floating
+ DialogManager::singleton().remove_dialog_floating_state(dialog->get_type());
+ }
+}
+
+// Remove dialog from list.
+void DialogContainer::unlink_dialog(DialogBase *dialog)
+{
+ if (!dialog) {
+ return;
+ }
+
+ auto found = dialogs.find(dialog->get_type());
+ if (found != dialogs.end()) {
+ dialogs.erase(found);
+ }
+
+ DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel());
+ if (window) {
+ window->update_dialogs();
+ }
+}
+
+/**
+ * Load last open window's dialog configuration state.
+ *
+ * For the keyfile format, check `save_container_state()`.
+ */
+void DialogContainer::load_container_state(Glib::KeyFile *keyfile, bool include_floating)
+{
+ // Step 1: check if we want to load the state
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // if it isn't dockable, all saved docked dialogs are made floating
+ bool is_dockable =
+ prefs->getInt("/options/dialogtype/value", PREFS_DIALOGS_BEHAVIOR_DOCKABLE) != PREFS_DIALOGS_BEHAVIOR_FLOATING;
+
+ // Step 2: get the number of windows
+ int windows_count = keyfile->get_integer("Windows", "Count");
+
+ // Step 3: for each window, load its state. Only the first window is not floating (the others are DialogWindow)
+ for (int window_idx = 0; window_idx < windows_count; ++window_idx) {
+ if (window_idx > 0 && !include_floating)
+ break;
+
+ Glib::ustring group_name = "Window" + std::to_string(window_idx);
+
+ // Step 3.0: read the window parameters
+ int column_count = 0;
+ bool floating = window_idx != 0;
+ window_position_t pos;
+ bool has_position = false;
+ try {
+ column_count = keyfile->get_integer(group_name, "ColumnCount");
+ floating = keyfile->get_boolean(group_name, "Floating");
+ if (keyfile->has_key(group_name, "Position") && keyfile->get_boolean(group_name, "Position")) {
+ pos.x = keyfile->get_integer(group_name, "x");
+ pos.y = keyfile->get_integer(group_name, "y");
+ pos.width = keyfile->get_integer(group_name, "width");
+ pos.height = keyfile->get_integer(group_name, "height");
+ has_position = true;
+ }
+ } catch (Glib::Error &error) {
+ std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl;
+ }
+
+ // Step 3.1: get the window's container columns where we want to create the dialogs
+ DialogContainer *active_container = nullptr;
+ DialogMultipaned *active_columns = nullptr;
+ DialogWindow *dialog_window = nullptr;
+
+ if (is_dockable) {
+ if (floating) {
+ dialog_window = new DialogWindow(_inkscape_window, nullptr);
+ if (dialog_window) {
+ active_container = dialog_window->get_container();
+ active_columns = dialog_window->get_container()->get_columns();
+ }
+ } else {
+ active_container = this;
+ active_columns = columns;
+ }
+
+ if (!active_container || !active_columns) {
+ continue;
+ }
+ }
+
+ // Step 3.2: for each column, load its state
+ for (int column_idx = 0; column_idx < column_count; ++column_idx) {
+ Glib::ustring column_group_name = group_name + "Column" + std::to_string(column_idx);
+
+ // Step 3.2.0: read the column parameters
+ int notebook_count = 0;
+ bool before_canvas = false;
+ try {
+ notebook_count = keyfile->get_integer(column_group_name, "NotebookCount");
+ before_canvas = keyfile->get_boolean(column_group_name, "BeforeCanvas");
+ } catch (Glib::Error &error) {
+ std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl;
+ }
+
+ // Step 3.2.1: create the column
+ DialogMultipaned *column = nullptr;
+ if (is_dockable) {
+ column = active_container->create_column();
+ if (!column) {
+ continue;
+ }
+
+ if (keyfile->has_key(column_group_name, "ColumnWidth")) {
+ auto width = keyfile->get_integer(column_group_name, "ColumnWidth");
+ column->set_restored_width(width);
+ }
+
+ before_canvas ? active_columns->prepend(column) : active_columns->append(column);
+ }
+
+ // Step 3.2.2: for each noteboook, load its dialogs
+ for (int notebook_idx = 0; notebook_idx < notebook_count; ++notebook_idx) {
+ Glib::ustring key = "Notebook" + std::to_string(notebook_idx) + "Dialogs";
+
+ // Step 3.2.2.0 read the list of dialogs in the current notebook
+ std::vector<Glib::ustring> dialogs;
+ try {
+ dialogs = keyfile->get_string_list(column_group_name, key);
+ } catch (Glib::Error &error) {
+ std::cerr << "DialogContainer::load_container_state: " << error.what().raw() << std::endl;
+ }
+
+ if (!dialogs.size()) {
+ continue;
+ }
+
+ DialogNotebook *notebook = nullptr;
+ if (is_dockable) {
+ notebook = Gtk::manage(new DialogNotebook(active_container));
+ column->append(notebook);
+ }
+
+ auto const &dialog_data = get_dialog_data();
+ // Step 3.2.2.1 create each dialog in the current notebook
+ for (auto type : dialogs) {
+
+ if (dialog_data.find(type) != dialog_data.end()) {
+ if (is_dockable) {
+ active_container->new_dialog(type, notebook);
+ } else {
+ dialog_window = create_new_floating_dialog(type, false);
+ }
+ } else {
+ std::cerr << "load_container_state: invalid dialog type: " << type.raw() << std::endl;
+ }
+ }
+
+ if (notebook) {
+ Glib::ustring row = "Notebook" + std::to_string(notebook_idx) + "Height";
+ if (keyfile->has_key(column_group_name, row)) {
+ auto height = keyfile->get_integer(column_group_name, row);
+ notebook->set_requested_height(height);
+ }
+ Glib::ustring tab = "Notebook" + std::to_string(notebook_idx) + "ActiveTab";
+ if (keyfile->has_key(column_group_name, tab)) {
+ if (auto nb = notebook->get_notebook()) {
+ auto page = keyfile->get_integer(column_group_name, tab);
+ nb->set_current_page(page);
+ }
+ }
+ }
+ }
+ }
+
+ if (dialog_window) {
+ if (has_position) {
+ dm_restore_window_position(*dialog_window, pos);
+ }
+ else {
+ dialog_window->update_window_size_to_fit_children();
+ }
+ dialog_window->show_all();
+ // Set the style and icon theme of the new menu based on the desktop
+ }
+ }
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+}
+
+void save_wnd_position(Glib::KeyFile *keyfile, const Glib::ustring &group_name, const window_position_t *position)
+{
+ keyfile->set_boolean(group_name, "Position", position != nullptr);
+ if (position) { // floating window position?
+ keyfile->set_integer(group_name, "x", position->x);
+ keyfile->set_integer(group_name, "y", position->y);
+ keyfile->set_integer(group_name, "width", position->width);
+ keyfile->set_integer(group_name, "height", position->height);
+ }
+}
+
+// get *this* container's state only; store window 'position' in the state if given
+std::shared_ptr<Glib::KeyFile> DialogContainer::get_container_state(const window_position_t *position) const
+{
+ std::shared_ptr<Glib::KeyFile> keyfile = std::make_shared<Glib::KeyFile>();
+
+ DialogMultipaned *window = columns;
+ const int window_idx = 0;
+
+ // Step 2: save the number of windows
+ keyfile->set_integer("Windows", "Count", 1);
+
+ // Step 3.0: get all the multipanes of the window
+ std::vector<DialogMultipaned *> multipanes;
+
+ for (auto const &column : window->get_children()) {
+ if (auto paned = dynamic_cast<DialogMultipaned *>(column)) {
+ multipanes.push_back(paned);
+ }
+ }
+
+ // Step 3.1: for each non-empty column, save its data.
+ int column_count = 0; // non-empty columns count
+ for (size_t column_idx = 0; column_idx < multipanes.size(); ++column_idx) {
+ Glib::ustring group_name = "Window" + std::to_string(window_idx) + "Column" + std::to_string(column_idx);
+ int notebook_count = 0; // non-empty notebooks count
+
+ // Step 3.1.0: for each notebook, get its dialogs
+ for (auto const &columns_widget : multipanes[column_idx]->get_children()) {
+ if (auto dialog_notebook = dynamic_cast<DialogNotebook *>(columns_widget)) {
+ std::vector<Glib::ustring> dialogs;
+
+ for (auto const &widget : dialog_notebook->get_notebook()->get_children()) {
+ if (DialogBase *dialog = dynamic_cast<DialogBase *>(widget)) {
+ dialogs.push_back(dialog->get_type());
+ }
+ }
+
+ // save the dialogs type
+ Glib::ustring key = "Notebook" + std::to_string(notebook_count) + "Dialogs";
+ keyfile->set_string_list(group_name, key, dialogs);
+
+ // increase the notebook count
+ notebook_count++;
+ }
+ }
+
+ // Step 3.1.1: increase the column count
+ if (notebook_count != 0) {
+ column_count++;
+ }
+
+ // Step 3.1.2: Save the column's data
+ keyfile->set_integer(group_name, "NotebookCount", notebook_count);
+ }
+
+ // Step 3.2: save the window group
+ Glib::ustring group_name = "Window" + std::to_string(window_idx);
+ keyfile->set_integer(group_name, "ColumnCount", column_count);
+ save_wnd_position(keyfile.get(), group_name, position);
+
+ return keyfile;
+}
+
+/**
+ * Save container state. The configuration of open dialogs and the relative positions of the notebooks are saved.
+ *
+ * The structure of such a KeyFile is:
+ *
+ * There is a "Windows" group that records the number of the windows:
+ * [Windows]
+ * Count=1
+ *
+ * A "WindowX" group saves the number of columns the window's container has and whether the window is floating:
+ *
+ * [Window0]
+ * ColumnCount=1
+ * Floating=false
+ *
+ * For each column, we have a "WindowWColumnX" group, where X is the index of the column. "BeforeCanvas" checks
+ * if the column is before the canvas or not. "NotebookCount" records how many notebooks are in each column and
+ * "NotebookXDialogs" records a list of the types for the dialogs in notebook X.
+ *
+ * [Window0Column0]
+ * Notebook0Dialogs=Text;
+ * NotebookCount=2
+ * BeforeCanvas=false
+ *
+ */
+std::unique_ptr<Glib::KeyFile> DialogContainer::save_container_state()
+{
+ std::unique_ptr<Glib::KeyFile> keyfile = std::make_unique<Glib::KeyFile>();
+ auto app = InkscapeApplication::instance();
+
+ // Step 1: get all the container columns (in order, from the current container and all DialogWindow containers)
+ std::vector<DialogMultipaned *> windows(1, columns);
+ std::vector<DialogWindow *> dialog_windows(1, nullptr);
+
+ for (auto const &window : app->gtk_app()->get_windows()) {
+ DialogWindow *dialog_window = dynamic_cast<DialogWindow *>(window);
+ if (dialog_window) {
+ windows.push_back(dialog_window->get_container()->get_columns());
+ dialog_windows.push_back(dialog_window);
+ }
+ }
+
+ // Step 2: save the number of windows
+ keyfile->set_integer("Windows", "Count", windows.size());
+
+ // Step 3: for each window, save its data. Only the first window is not floating (the others are DialogWindow)
+ for (int window_idx = 0; window_idx < (int)windows.size(); ++window_idx) {
+ // Step 3.0: get all the multipanes of the window
+ std::vector<DialogMultipaned *> multipanes;
+
+ // used to check if the column is before or after canvas
+ std::vector<DialogMultipaned *>::iterator multipanes_it = multipanes.begin();
+ bool canvas_seen = window_idx != 0; // no floating windows (window_idx > 0) have a canvas
+ int before_canvas_columns_count = 0;
+
+ for (auto const &column : windows[window_idx]->get_children()) {
+ if (!canvas_seen) {
+ UI::Widget::CanvasGrid *canvas = dynamic_cast<UI::Widget::CanvasGrid *>(column);
+ if (canvas) {
+ canvas_seen = true;
+ } else {
+ DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(column);
+ if (paned) {
+ multipanes_it = multipanes.insert(multipanes_it, paned);
+ before_canvas_columns_count++;
+ }
+ }
+ } else {
+ DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(column);
+ if (paned) {
+ multipanes.push_back(paned);
+ }
+ }
+ }
+
+ // Step 3.1: for each non-empty column, save its data.
+ int column_count = 0; // non-empty columns count
+ for (int column_idx = 0; column_idx < (int)multipanes.size(); ++column_idx) {
+ Glib::ustring group_name = "Window" + std::to_string(window_idx) + "Column" + std::to_string(column_idx);
+ int notebook_count = 0; // non-empty notebooks count
+ int width = multipanes[column_idx]->get_allocated_width();
+
+ // Step 3.1.0: for each notebook, get its dialogs' types
+ for (auto const &columns_widget : multipanes[column_idx]->get_children()) {
+ DialogNotebook *dialog_notebook = dynamic_cast<DialogNotebook *>(columns_widget);
+
+ if (dialog_notebook) {
+ std::vector<Glib::ustring> dialogs;
+
+ for (auto const &widget : dialog_notebook->get_notebook()->get_children()) {
+ DialogBase *dialog = dynamic_cast<DialogBase *>(widget);
+ if (dialog) {
+ dialogs.push_back(dialog->get_type());
+ }
+ }
+
+ // save the dialogs type
+ Glib::ustring key = "Notebook" + std::to_string(notebook_count) + "Dialogs";
+ keyfile->set_string_list(group_name, key, dialogs);
+ // save height; useful when there are multiple "rows" of docked dialogs
+ Glib::ustring row = "Notebook" + std::to_string(notebook_count) + "Height";
+ keyfile->set_integer(group_name, row, dialog_notebook->get_allocated_height());
+ if (auto notebook = dialog_notebook->get_notebook()) {
+ Glib::ustring row = "Notebook" + std::to_string(notebook_count) + "ActiveTab";
+ keyfile->set_integer(group_name, row, notebook->get_current_page());
+ }
+
+ // increase the notebook count
+ notebook_count++;
+ }
+ }
+
+ // Step 3.1.1: increase the column count
+ if (notebook_count != 0) {
+ column_count++;
+ }
+
+ keyfile->set_integer(group_name, "ColumnWidth", width);
+
+ // Step 3.1.2: Save the column's data
+ keyfile->set_integer(group_name, "NotebookCount", notebook_count);
+ keyfile->set_boolean(group_name, "BeforeCanvas", (column_idx < before_canvas_columns_count));
+ }
+
+ // Step 3.2: save the window group
+ Glib::ustring group_name = "Window" + std::to_string(window_idx);
+ keyfile->set_integer(group_name, "ColumnCount", column_count);
+ keyfile->set_boolean(group_name, "Floating", window_idx != 0);
+ if (window_idx != 0) { // floating?
+ if (auto wnd = dynamic_cast<DialogWindow *>(dialog_windows.at(window_idx))) {
+ // store window position
+ auto pos = dm_get_window_position(*wnd);
+ save_wnd_position(keyfile.get(), group_name, pos ? &*pos : nullptr);
+ }
+ }
+ }
+
+ return keyfile;
+}
+
+// Signals -----------------------------------------------------
+
+/**
+ * No zombie windows. TODO: Need to work on this as it still leaves Gtk::Window! (?)
+ */
+void DialogContainer::on_unrealize() {
+ // Disconnect all signals
+ for_each(connections.begin(), connections.end(), [&](auto c) { c.disconnect(); });
+
+ delete columns;
+ columns = nullptr;
+
+ parent_type::on_unrealize();
+}
+
+// Create a new notebook and move page.
+DialogNotebook *DialogContainer::prepare_drop(const Glib::RefPtr<Gdk::DragContext> context)
+{
+ Gtk::Widget *source = Gtk::Widget::drag_get_source_widget(context);
+
+ // Find source notebook and page
+ Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(source);
+ if (!old_notebook) {
+ std::cerr << "DialogContainer::prepare_drop: notebook not found!" << std::endl;
+ return nullptr;
+ }
+
+ // Find page
+ Gtk::Widget *page = old_notebook->get_nth_page(old_notebook->get_current_page());
+ if (!page) {
+ std::cerr << "DialogContainer::prepare_drop: page not found!" << std::endl;
+ return nullptr;
+ }
+
+ // Create new notebook and move page.
+ DialogNotebook *new_notebook = Gtk::manage(new DialogNotebook(this));
+ new_notebook->move_page(*page);
+
+ // move_page() takes care of updating dialog lists.
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+ return new_notebook;
+}
+
+// Notebook page dropped on prepend target. Call function to create new notebook and then insert.
+void DialogContainer::prepend_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *multipane)
+{
+ DialogNotebook *new_notebook = prepare_drop(context); // Creates notebook, moves page.
+ if (!new_notebook) {
+ std::cerr << "DialogContainer::prepend_drop: no new notebook!" << std::endl;
+ return;
+ }
+
+ if (multipane->get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ // Columns
+ // Create column
+ DialogMultipaned *column = create_column();
+ column->prepend(new_notebook);
+ columns->prepend(column);
+ } else {
+ // Column
+ multipane->prepend(new_notebook);
+ }
+
+ update_dialogs(); // Always update dialogs on Notebook change
+}
+
+// Notebook page dropped on append target. Call function to create new notebook and then insert.
+void DialogContainer::append_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *multipane)
+{
+ DialogNotebook *new_notebook = prepare_drop(context); // Creates notebook, moves page.
+ if (!new_notebook) {
+ std::cerr << "DialogContainer::append_drop: no new notebook!" << std::endl;
+ return;
+ }
+
+ if (multipane->get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ // Columns
+ // Create column
+ DialogMultipaned *column = create_column();
+ column->append(new_notebook);
+ columns->append(column);
+ } else {
+ // Column
+ multipane->append(new_notebook);
+ }
+
+ update_dialogs(); // Always update dialogs on Notebook change
+}
+
+/**
+ * If a DialogMultipaned column is empty and it can be removed, remove it
+ */
+void DialogContainer::column_empty(DialogMultipaned *column)
+{
+ DialogMultipaned *parent = dynamic_cast<DialogMultipaned *>(column->get_parent());
+ if (parent) {
+ parent->remove(*column);
+ }
+
+ DialogWindow *window = dynamic_cast<DialogWindow *>(get_toplevel());
+ if (window && parent) {
+ auto children = parent->get_children();
+ // Close the DialogWindow if you're in an empty one
+ if (children.size() == 3 && parent->has_empty_widget()) {
+ window->close();
+ }
+ }
+}
+
+} // 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/dialog/dialog-container.h b/src/ui/dialog/dialog-container.h
new file mode 100644
index 0000000..cb6e5c6
--- /dev/null
+++ b/src/ui/dialog/dialog-container.h
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_CONTAINER_H
+#define INKSCAPE_UI_DIALOG_CONTAINER_H
+
+/** @file
+ * @brief A widget that manages DialogNotebook's and other widgets inside a horizontal DialogMultipaned.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdkmm/dragcontext.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <glibmm/keyfile.h>
+#include <gtkmm/accelkey.h>
+#include <gtkmm/box.h>
+#include <gtkmm/targetentry.h>
+#include <iostream>
+#include <map>
+#include <set>
+#include <memory>
+#include "dialog-manager.h"
+#include "desktop.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogBase;
+class DialogNotebook;
+class DialogMultipaned;
+class DialogWindow;
+
+/**
+ * A widget that manages DialogNotebook's and other widgets inside a
+ * horizontal DialogMultipaned containing vertical DialogMultipaned's or other widgets.
+ */
+class DialogContainer : public Gtk::Box
+{
+ using parent_type = Gtk::Box;
+
+public:
+ DialogContainer(InkscapeWindow* inkscape_window);
+ ~DialogContainer() override;
+
+ // Columns-related functions
+ DialogMultipaned *get_columns() { return columns; }
+ DialogMultipaned *create_column();
+
+ // Dialog-related functions
+ void new_dialog(const Glib::ustring& dialog_type);
+
+ DialogWindow* new_floating_dialog(const Glib::ustring& dialog_type);
+ bool has_dialog_of_type(DialogBase *dialog);
+ DialogBase *get_dialog(const Glib::ustring& dialog_type);
+ void link_dialog(DialogBase *dialog);
+ void unlink_dialog(DialogBase *dialog);
+ const std::multimap<Glib::ustring, DialogBase *> *get_dialogs() { return &dialogs; };
+ void toggle_dialogs();
+ void update_dialogs(); // Update all linked dialogs
+ void set_inkscape_window(InkscapeWindow *inkscape_window);
+ InkscapeWindow* get_inkscape_window() { return _inkscape_window; }
+
+ // State saving functionality
+ std::unique_ptr<Glib::KeyFile> save_container_state();
+ void load_container_state(Glib::KeyFile* keyfile, bool include_floating);
+
+ void restore_window_position(DialogWindow* window);
+ void store_window_position(DialogWindow* window);
+
+ // get this container's state; provide window position for container embedded in DialogWindow
+ std::shared_ptr<Glib::KeyFile> get_container_state(const window_position_t* position) const;
+ void load_container_state(Glib::KeyFile& state, const std::string& window_id);
+
+private:
+ InkscapeWindow *_inkscape_window = nullptr; // Every container is attached to an InkscapeWindow.
+ DialogMultipaned *columns = nullptr; // The main widget inside which other children are kept.
+ std::vector<Gtk::TargetEntry> target_entries; // What kind of object can be dropped.
+
+ /**
+ * Due to the way Gtk handles dragging between notebooks, one can
+ * either allow multiple instances of the same dialog in a notebook
+ * or restrict dialogs to docks tied to a particular document
+ * window. (More explicitly, use one group name for all notebooks or
+ * use a unique group name for each document window with related
+ * floating docks.) For the moment we choose the former which
+ * requires a multimap here as we use the dialog type as a key.
+ */
+ std::multimap<Glib::ustring, DialogBase *>dialogs;
+
+ void new_dialog(const Glib::ustring& dialog_type, DialogNotebook* notebook);
+ std::unique_ptr<DialogBase> dialog_factory(Glib::ustring const &dialog_type);
+ Gtk::Widget *create_notebook_tab(Glib::ustring label, Glib::ustring image, const Glib::ustring shortcut);
+ DialogWindow* create_new_floating_dialog(const Glib::ustring& dialog_type, bool blink);
+
+ // Signal connections
+ std::vector<sigc::connection> connections;
+
+ // Handlers
+ void on_unrealize() override;
+ DialogNotebook *prepare_drop(const Glib::RefPtr<Gdk::DragContext> context);
+ void prepend_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *column);
+ void append_drop(const Glib::RefPtr<Gdk::DragContext> context, DialogMultipaned *column);
+ void column_empty(DialogMultipaned *column);
+ DialogBase* find_existing_dialog(const Glib::ustring& dialog_type);
+ static bool recreate_dialogs_from_state(InkscapeWindow* inkscape_window, const Glib::KeyFile* keyfile);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_CONTAINER_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/dialog/dialog-data.cpp b/src/ui/dialog/dialog-data.cpp
new file mode 100644
index 0000000..b8acdb1
--- /dev/null
+++ b/src/ui/dialog/dialog-data.cpp
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <map>
+
+#include <glibmm/i18n.h>
+#include <glibmm/ustring.h>
+
+#include "config.h" // Needed for WITH_GSPELL
+
+#include "ui/dialog/dialog-data.h"
+#include "ui/icon-names.h" // INKSCAPE_ICON macro
+
+/*
+ * In an ideal world, this information would be in .ui files for each
+ * dialog (the .ui file would describe a dialog wrapped by a notebook
+ * tab). At the moment we create each dialog notebook tab on the fly
+ * so we need a place to keep this information.
+ */
+
+std::map<std::string, DialogData> const &get_dialog_data()
+{
+ static std::map<std::string, DialogData> dialog_data;
+
+ // Note the "AttrDialog" is now part of the "XMLDialog" and the "Style" dialog is part of the
+ // "Selectors" dialog. Also note that the "AttrDialog" does not correspond to SP_VERB_DIALOG_ATTR!!!
+ // (That would be the "ObjectAttributes" dialog.)
+
+ if (dialog_data.empty()) {
+ dialog_data = {
+ // clang-format off
+ {"AlignDistribute", {_("_Align and Distribute"), INKSCAPE_ICON("dialog-align-and-distribute"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"CloneTiler", {_("Create Tiled Clones"), INKSCAPE_ICON("dialog-tile-clones"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"DocumentProperties", {_("_Document Properties"), INKSCAPE_ICON("document-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }},
+ {"DocumentResources", {_("_Document Resources"), INKSCAPE_ICON("document-resources"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Export", {_("_Export"), INKSCAPE_ICON("document-export"), DialogData::Basic, ScrollProvider::PROVIDE }},
+ {"FillStroke", {_("_Fill and Stroke"), INKSCAPE_ICON("dialog-fill-and-stroke"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"FilterEffects", {_("Filter _Editor"), INKSCAPE_ICON("dialog-filters"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Find", {_("_Find/Replace"), INKSCAPE_ICON("edit-find"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"FontCollections", {_("_Font Collections"), INKSCAPE_ICON("font_collections"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Glyphs", {_("_Unicode Characters"), INKSCAPE_ICON("accessories-character-map"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"IconPreview", {_("Icon Preview"), INKSCAPE_ICON("dialog-icon-preview"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"Input", {_("_Input Devices"), INKSCAPE_ICON("dialog-input-devices"), DialogData::Settings, ScrollProvider::NOPROVIDE }},
+ {"LivePathEffect", {_("Path E_ffects"), INKSCAPE_ICON("dialog-path-effects"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Memory", {_("About _Memory"), INKSCAPE_ICON("dialog-memory"), DialogData::Diagnostics, ScrollProvider::PROVIDE }},
+ {"Messages", {_("_Messages"), INKSCAPE_ICON("dialog-messages"), DialogData::Diagnostics, ScrollProvider::NOPROVIDE }},
+ {"ObjectAttributes", {_("_Object Attributes"), INKSCAPE_ICON("dialog-object-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }},
+ {"ObjectProperties", {_("_Object Properties"), INKSCAPE_ICON("dialog-object-properties"), DialogData::Settings, ScrollProvider::NOPROVIDE }},
+ {"Objects", {_("Layers and Object_s"), INKSCAPE_ICON("dialog-objects"), DialogData::Basic, ScrollProvider::PROVIDE }},
+ {"PaintServers", {_("_Paint Servers"), INKSCAPE_ICON("dialog-paint-server"), DialogData::Advanced, ScrollProvider::PROVIDE }},
+ {"Preferences", {_("P_references"), INKSCAPE_ICON("preferences-system"), DialogData::Settings, ScrollProvider::PROVIDE }},
+ {"Selectors", {_("_Selectors and CSS"), INKSCAPE_ICON("dialog-selectors"), DialogData::Advanced, ScrollProvider::PROVIDE }},
+ {"SVGFonts", {_("SVG Font Editor"), INKSCAPE_ICON("dialog-svg-font"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+ {"Swatches", {_("S_watches"), INKSCAPE_ICON("swatches"), DialogData::Basic, ScrollProvider::PROVIDE }},
+ {"Symbols", {_("S_ymbols"), INKSCAPE_ICON("symbols"), DialogData::Basic, ScrollProvider::PROVIDE }},
+ {"Text", {_("_Text and Font"), INKSCAPE_ICON("dialog-text-and-font"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"Trace", {_("_Trace Bitmap"), INKSCAPE_ICON("bitmap-trace"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"Transform", {_("Transfor_m"), INKSCAPE_ICON("dialog-transform"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"UndoHistory", {_("Undo _History"), INKSCAPE_ICON("edit-undo-history"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+ {"XMLEditor", {_("_XML Editor"), INKSCAPE_ICON("dialog-xml-editor"), DialogData::Advanced, ScrollProvider::NOPROVIDE }},
+#if WITH_GSPELL
+ {"Spellcheck", {_("Check Spellin_g"), INKSCAPE_ICON("tools-check-spelling"), DialogData::Basic, ScrollProvider::NOPROVIDE }},
+#endif
+#if DEBUG
+ {"Prototype", {_("Prototype"), INKSCAPE_ICON("document-properties"), DialogData::Other, ScrollProvider::NOPROVIDE }},
+#endif
+ // clang-format on
+ };
+ }
+ return dialog_data;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/dialog-data.h b/src/ui/dialog/dialog-data.h
new file mode 100644
index 0000000..1816600
--- /dev/null
+++ b/src/ui/dialog/dialog-data.h
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief Basic dialog info.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2021 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <map>
+
+#include <glibmm/i18n.h>
+#include <glibmm/ustring.h>
+
+enum class ScrollProvider {
+ PROVIDE = 0,
+ NOPROVIDE
+};
+
+class DialogData {
+public:
+ Glib::ustring label;
+ Glib::ustring icon_name;
+ enum Category { Basic = 0, Advanced, Settings, Diagnostics, Other, _num_categories };
+ Category category;
+ ScrollProvider provide_scroll;
+};
+
+// dialog categories (used to group them in a dialog submenu)
+char const *const dialog_categories[DialogData::_num_categories] = {
+ N_("Basic"),
+ N_("Advanced"),
+ N_("Settings"),
+ N_("Diagnostic"),
+ N_("Other")
+};
+
+/** Get the data about all existing dialogs. */
+std::map<std::string, DialogData> const &get_dialog_data();
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/dialog-manager.cpp b/src/ui/dialog/dialog-manager.cpp
new file mode 100644
index 0000000..f12c9be
--- /dev/null
+++ b/src/ui/dialog/dialog-manager.cpp
@@ -0,0 +1,323 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "dialog-manager.h"
+
+#include <gdkmm/monitor.h>
+#include <limits>
+#ifdef G_OS_WIN32
+#include <filesystem>
+namespace filesystem = std::filesystem;
+#else
+// Waiting for compiler on MacOS to catch up to C++x17
+#include <boost/filesystem.hpp>
+namespace filesystem = boost::filesystem;
+#endif
+
+#include "io/resource.h"
+#include "inkscape-application.h"
+#include "dialog-base.h"
+#include "dialog-container.h"
+#include "dialog-window.h"
+#include "enums.h"
+#include "preferences.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+std::optional<window_position_t> dm_get_window_position(Gtk::Window &window)
+{
+ std::optional<window_position_t> position = std::nullopt;
+
+ const int max = std::numeric_limits<int>::max();
+ int x = max;
+ int y = max;
+ int width = 0;
+ int height = 0;
+ // gravity NW to include window decorations
+ window.property_gravity() = Gdk::GRAVITY_NORTH_WEST;
+ window.get_position(x, y);
+ window.get_size(width, height);
+
+ if (x != max && y != max && width > 0 && height > 0) {
+ position = window_position_t{x, y, width, height};
+ }
+
+ return position;
+}
+
+void dm_restore_window_position(Gtk::Window &window, const window_position_t &position)
+{
+ // note: Gtk window methods are recommended over low-level Gdk ones to resize and position window
+ window.property_gravity() = Gdk::GRAVITY_NORTH_WEST;
+ window.set_default_size(position.width, position.height);
+ // move & resize positions window on the screen making sure it is not clipped
+ // (meaning it is visible; this works with two monitors too)
+ window.move(position.x, position.y);
+ window.resize(position.width, position.height);
+}
+
+DialogManager &DialogManager::singleton()
+{
+ static DialogManager dm;
+ return dm;
+}
+
+// store complete dialog window state (including its container state)
+void DialogManager::store_state(DialogWindow &wnd)
+{
+ // get window's size and position
+ if (auto pos = dm_get_window_position(wnd)) {
+ if (auto container = wnd.get_container()) {
+ // get container's state
+ auto state = container->get_container_state(&*pos);
+
+ // find dialogs hosted in this window
+ for (auto dlg : *container->get_dialogs()) {
+ _floating_dialogs[dlg.first] = state;
+ }
+ }
+ }
+}
+
+//
+bool DialogManager::should_open_floating(const Glib::ustring& dialog_type)
+{
+ return _floating_dialogs.count(dialog_type) > 0;
+}
+
+void DialogManager::set_floating_dialog_visibility(DialogWindow* wnd, bool show) {
+ if (!wnd) return;
+
+ if (show) {
+ if (wnd->is_visible()) return;
+
+ // wnd->present(); - not sure which one is better, show or present...
+ wnd->show();
+ _hidden_dlg_windows.erase(wnd);
+ // re-add it to application; hiding removed it
+ if (auto app = InkscapeApplication::instance()) {
+ app->gtk_app()->add_window(*wnd);
+ }
+ }
+ else {
+ if (!wnd->is_visible()) return;
+
+ _hidden_dlg_windows.insert(wnd);
+ wnd->hide();
+ }
+}
+
+std::vector<DialogWindow*> DialogManager::get_all_floating_dialog_windows() {
+ std::vector<Gtk::Window*> windows = InkscapeApplication::instance()->gtk_app()->get_windows();
+
+ std::vector<DialogWindow*> result(_hidden_dlg_windows.begin(), _hidden_dlg_windows.end());
+ for (auto wnd : windows) {
+ if (auto dlg_wnd = dynamic_cast<DialogWindow*>(wnd)) {
+ result.push_back(dlg_wnd);
+ }
+ }
+
+ return result;
+}
+
+DialogWindow* DialogManager::find_floating_dialog_window(const Glib::ustring& dialog_type) {
+ auto windows = get_all_floating_dialog_windows();
+
+ for (auto dlg_wnd : windows) {
+ if (auto container = dlg_wnd->get_container()) {
+ if (container->get_dialog(dialog_type)) {
+ return dlg_wnd;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+DialogBase *DialogManager::find_floating_dialog(const Glib::ustring& dialog_type)
+{
+ auto windows = get_all_floating_dialog_windows();
+
+ for (auto dlg_wnd : windows) {
+ if (auto container = dlg_wnd->get_container()) {
+ if (auto dlg = container->get_dialog(dialog_type)) {
+ return dlg;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+std::shared_ptr<Glib::KeyFile> DialogManager::find_dialog_state(const Glib::ustring& dialog_type)
+{
+ auto it = _floating_dialogs.find(dialog_type);
+ if (it != _floating_dialogs.end()) {
+ return it->second;
+ }
+ return nullptr;
+}
+
+const char dialogs_state[] = "dialogs-state-ex.ini";
+const char save_dialog_position[] = "/options/savedialogposition/value";
+const char transient_group[] = "transient";
+
+// list of dialogs sharing the same state
+std::vector<Glib::ustring> DialogManager::count_dialogs(const Glib::KeyFile *state) const
+{
+ std::vector<Glib::ustring> dialogs;
+ if (!state) return dialogs;
+
+ for (auto dlg : _floating_dialogs) {
+ if (dlg.second.get() == state) {
+ dialogs.emplace_back(dlg.first);
+ }
+ }
+ return dialogs;
+}
+
+void DialogManager::save_dialogs_state(DialogContainer *docking_container)
+{
+ if (!docking_container) return;
+
+ // check if we want to save the state
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int save_state = prefs->getInt(save_dialog_position, PREFS_DIALOGS_STATE_SAVE);
+ if (save_state == PREFS_DIALOGS_STATE_NONE) return;
+
+ // save state of docked dialogs and currently open floating ones
+ auto keyfile = docking_container->save_container_state();
+
+ // save transient state of floating dialogs that user might have opened interacting with the app
+ int idx = 1;
+ for (auto dlg : _floating_dialogs) {
+ auto state = dlg.second.get();
+ auto&& type = dlg.first;
+ auto index = std::to_string(idx++);
+ // state may be empty; all that means it that dialog hasn't been opened yet,
+ // but when it is, then it should be open in a floating state
+ keyfile->set_string(transient_group, "state" + index, state ? state->to_data() : "");
+ auto dialogs = count_dialogs(state);
+ if (!state) {
+ dialogs.emplace_back(type);
+ }
+ keyfile->set_string_list(transient_group, "dialogs" + index, dialogs);
+ }
+ keyfile->set_integer(transient_group, "count", _floating_dialogs.size());
+
+ std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), dialogs_state);
+ try {
+ keyfile->save_to_file(filename);
+ } catch (Glib::FileError &error) {
+ std::cerr << G_STRFUNC << ": " << error.what().raw() << std::endl;
+ }
+}
+
+// load transient dialog state - it includes state of floating dialogs that may or may not be open
+void DialogManager::load_transient_state(Glib::KeyFile *file)
+{
+ int count = file->get_integer(transient_group, "count");
+ for (int i = 0; i < count; ++i) {
+ auto index = std::to_string(i + 1);
+ auto dialogs = file->get_string_list(transient_group, "dialogs" + index);
+ auto state = file->get_string(transient_group, "state" + index);
+
+ auto keyfile = std::make_shared<Glib::KeyFile>();
+ if (!state.empty()) {
+ keyfile->load_from_data(state);
+ }
+ for (auto type : dialogs) {
+ _floating_dialogs[type] = keyfile;
+ }
+ }
+}
+
+bool file_exists(const std::string& filepath) {
+#ifdef G_OS_WIN32
+ bool exists = filesystem::exists(filesystem::u8path(filepath));
+#else
+ bool exists = filesystem::exists(filesystem::path(filepath));
+#endif
+ return exists;
+}
+
+// restore state of dialogs; populate docking container and open visible floating dialogs
+void DialogManager::restore_dialogs_state(DialogContainer *docking_container, bool include_floating)
+{
+ if (!docking_container) return;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int save_state = prefs->getInt(save_dialog_position, PREFS_DIALOGS_STATE_SAVE);
+ if (save_state == PREFS_DIALOGS_STATE_NONE) return;
+
+ try {
+ auto keyfile = std::make_unique<Glib::KeyFile>();
+ std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), dialogs_state);
+
+ bool exists = file_exists(filename);
+
+ if (exists && keyfile->load_from_file(filename)) {
+ // restore visible dialogs first; that state is up-to-date
+ docking_container->load_container_state(keyfile.get(), include_floating);
+
+ // then load transient data too; it may be older than above
+ if (include_floating) {
+ try {
+ load_transient_state(keyfile.get());
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": transient state not loaded - " << error.what().raw() << std::endl;
+ }
+ }
+ }
+ else {
+ // state not available or not valid; prepare defaults
+ dialog_defaults(docking_container);
+ }
+ } catch (Glib::Error &error) {
+ std::cerr << G_STRFUNC << ": dialogs state not loaded - " << error.what().raw() << std::endl;
+ }
+}
+
+void DialogManager::remove_dialog_floating_state(const Glib::ustring& dialog_type) {
+ auto it = _floating_dialogs.find(dialog_type);
+ if (it != _floating_dialogs.end()) {
+ _floating_dialogs.erase(it);
+ }
+}
+
+// apply defaults when dialog state cannot be loaded / doesn't exist:
+// here we load defaults from dedicated ini file
+void DialogManager::dialog_defaults(DialogContainer* docking_container) {
+ auto keyfile = std::make_unique<Glib::KeyFile>();
+ // default/initial state used when running Inkscape for the first time
+ std::string filename = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::UIS, "default-dialog-state.ini");
+
+ bool exists = file_exists(filename);
+
+ if (exists && keyfile->load_from_file(filename)) {
+ // populate info about floating dialogs, so when users try opening them,
+ // they will pop up in a window, not docked
+ load_transient_state(keyfile.get());
+ // create docked dialogs only, if any
+ docking_container->load_container_state(keyfile.get(), false);
+ }
+ else {
+ g_warning("Cannot load default dialog state %s", filename.c_str());
+ }
+}
+
+} // 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/dialog/dialog-manager.h b/src/ui/dialog/dialog-manager.h
new file mode 100644
index 0000000..b9b95dc
--- /dev/null
+++ b/src/ui/dialog/dialog-manager.h
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_MANAGER_H
+#define INKSCAPE_UI_DIALOG_MANAGER_H
+
+#include <glibmm/keyfile.h>
+#include <gtkmm/window.h>
+#include <map>
+#include <set>
+#include <memory>
+#include <optional>
+#include <vector>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogWindow;
+class DialogBase;
+class DialogContainer;
+
+struct window_position_t
+{
+ int x, y, width, height;
+};
+
+// try to read window's geometry
+std::optional<window_position_t> dm_get_window_position(Gtk::Window &window);
+
+// restore window's geometry
+void dm_restore_window_position(Gtk::Window &window, const window_position_t &position);
+
+class DialogManager
+{
+public:
+ static DialogManager &singleton();
+
+ // store complete dialog window state (including its container state)
+ void store_state(DialogWindow &wnd);
+
+ // return true if dialog 'type' should be opened as floating
+ bool should_open_floating(const Glib::ustring& dialog_type);
+
+ // find instance of dialog 'type' in one of currently open floating dialog windows
+ DialogBase *find_floating_dialog(const Glib::ustring& dialog_type);
+
+ // find window hosting floating dialog
+ DialogWindow* find_floating_dialog_window(const Glib::ustring& dialog_type);
+
+ // find floating window state hosting dialog 'code', if there was one
+ std::shared_ptr<Glib::KeyFile> find_dialog_state(const Glib::ustring& dialog_type);
+
+ // remove dialog floating state
+ void remove_dialog_floating_state(const Glib::ustring& dialog_type);
+
+ // save configuration of docked and floating dialogs
+ void save_dialogs_state(DialogContainer *docking_container);
+
+ // restore state of dialogs
+ void restore_dialogs_state(DialogContainer *docking_container, bool include_floating);
+
+ // find all floating dialog windows
+ std::vector<DialogWindow*> get_all_floating_dialog_windows();
+
+ // show/hide dialog window and keep track of it
+ void set_floating_dialog_visibility(DialogWindow* wnd, bool show);
+
+private:
+ DialogManager() = default;
+ ~DialogManager() = default;
+
+ std::vector<Glib::ustring> count_dialogs(const Glib::KeyFile *state) const;
+ void load_transient_state(Glib::KeyFile *keyfile);
+ void dialog_defaults(DialogContainer* docking_container);
+
+ // transient dialog state for floating windows user closes
+ std::map<std::string, std::shared_ptr<Glib::KeyFile>> _floating_dialogs;
+ // temp set used when dialogs are hidden (F12 toggle)
+ std::set<DialogWindow*> _hidden_dlg_windows;
+};
+
+} // 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/dialog/dialog-multipaned.cpp b/src/ui/dialog/dialog-multipaned.cpp
new file mode 100644
index 0000000..5483f46
--- /dev/null
+++ b/src/ui/dialog/dialog-multipaned.cpp
@@ -0,0 +1,1312 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A widget with multiple panes. Agnostic to type what kind of widgets panes contain.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2020 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-multipaned.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/objectbase.h>
+#include <gtkmm/container.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <iostream>
+#include <numeric>
+
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/util.h"
+#include "ui/widget/canvas-grid.h"
+#include "dialog-window.h"
+
+#define DROPZONE_SIZE 5
+#define DROPZONE_EXPANSION 15
+#define HANDLE_SIZE 12
+#define HANDLE_CROSS_SIZE 25
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*
+ * References:
+ * https://blog.gtk.org/2017/06/
+ * https://developer.gnome.org/gtkmm-tutorial/stable/sec-custom-containers.html.en
+ * https://wiki.gnome.org/HowDoI/Gestures
+ *
+ * The children widget sizes are "sticky". They change a minimal
+ * amount when the parent widget is resized or a child is added or
+ * removed.
+ *
+ * A gesture is used to track handle movement. This must be attached
+ * to the parent widget (the offset_x/offset_y values are relative to
+ * the widget allocation which changes for the handles as they are
+ * moved).
+ */
+
+int get_handle_size() {
+ return HANDLE_SIZE;
+}
+
+/* ============ MyDropZone ============ */
+
+std::list<MyDropZone *> MyDropZone::_instances_list;
+
+MyDropZone::MyDropZone(Gtk::Orientation orientation)
+ : Glib::ObjectBase("MultipanedDropZone")
+ , Gtk::Orientable()
+ , Gtk::EventBox()
+{
+ set_name("MultipanedDropZone");
+ set_orientation(orientation);
+ set_size(DROPZONE_SIZE);
+
+ get_style_context()->add_class("backgnd-passive");
+
+ signal_drag_motion().connect([=](const Glib::RefPtr<Gdk::DragContext>& ctx, int x, int y, guint time) {
+ if (!_active) {
+ _active = true;
+ add_highlight();
+ set_size(DROPZONE_SIZE + DROPZONE_EXPANSION);
+ }
+ return true;
+ });
+
+ signal_drag_leave().connect([=](const Glib::RefPtr<Gdk::DragContext>&, guint time) {
+ if (_active) {
+ _active = false;
+ set_size(DROPZONE_SIZE);
+ }
+ });
+
+ _instances_list.push_back(this);
+}
+
+MyDropZone::~MyDropZone()
+{
+ _instances_list.remove(this);
+}
+
+void MyDropZone::add_highlight_instances()
+{
+ for (auto *instance : _instances_list) {
+ instance->add_highlight();
+ }
+}
+
+void MyDropZone::remove_highlight_instances()
+{
+ for (auto *instance : _instances_list) {
+ instance->remove_highlight();
+ // instance->set_size(DROPZONE_SIZE);
+ }
+}
+
+void MyDropZone::add_highlight()
+{
+ const auto &style = get_style_context();
+ style->remove_class("backgnd-passive");
+ style->add_class("backgnd-active");
+}
+
+void MyDropZone::remove_highlight()
+{
+ const auto &style = get_style_context();
+ style->remove_class("backgnd-active");
+ style->add_class("backgnd-passive");
+}
+
+void MyDropZone::set_size(int size)
+{
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ set_size_request(size, -1);
+ } else {
+ set_size_request(-1, size);
+ }
+}
+
+/* ============ MyHandle ============ */
+
+MyHandle::MyHandle(Gtk::Orientation orientation, int size = get_handle_size())
+ : Glib::ObjectBase("MultipanedHandle")
+ , Gtk::Orientable()
+ , Gtk::EventBox()
+ , _cross_size(0)
+ , _child(nullptr)
+{
+ set_name("MultipanedHandle");
+ set_orientation(orientation);
+
+ add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | Gdk::POINTER_MOTION_MASK);
+
+ Gtk::Image *image = Gtk::manage(new Gtk::Image());
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ image->set_from_icon_name("view-more-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
+ set_size_request(size, -1);
+ } else {
+ image->set_from_icon_name("view-more-horizontal-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
+ set_size_request(-1, size);
+ }
+ image->set_pixel_size(size);
+ add(*image);
+
+ // Signal
+ signal_size_allocate().connect(sigc::mem_fun(*this, &MyHandle::resize_handler));
+
+ show_all();
+}
+
+// draw rectangle with rounded corners
+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();
+}
+
+// part of the handle where clicking makes it automatically collapse/expand docked dialogs
+Cairo::Rectangle MyHandle::get_active_click_zone() {
+ const Gtk::Allocation& allocation = get_allocation();
+ double width = allocation.get_width();
+ double height = allocation.get_height();
+ double h = height / 5;
+
+ Cairo::Rectangle rect = { .x = 0, .y = (height - h) / 2, .width = width, .height = h };
+ return rect;
+}
+
+bool MyHandle::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
+ bool ret = EventBox::on_draw(cr);
+
+ // show click indicator/highlight?
+ if (_click_indicator && is_click_resize_active() && !_dragging) {
+ auto rect = get_active_click_zone();
+
+ if (rect.width > 4 && rect.height > 0) {
+ Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context();
+ Gdk::RGBA fg = style_context->get_color(get_state_flags());
+ rounded_rectangle(cr, rect.x + 2, rect.y, rect.width - 4, rect.height, 3);
+ cr->set_source_rgba(fg.get_red(), fg.get_green(), fg.get_blue(), 0.26);
+ cr->fill();
+ }
+ }
+
+ return ret;
+}
+
+void MyHandle::set_dragging(bool dragging) {
+ if (_dragging != dragging) {
+ _dragging = dragging;
+ if (_click_indicator) {
+ queue_draw();
+ }
+ }
+}
+
+/**
+ * Change the mouse pointer into a resize icon to show you can drag.
+ */
+bool MyHandle::on_enter_notify_event(GdkEventCrossing *crossing_event)
+{
+ auto window = get_window();
+ auto display = get_display();
+
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ auto cursor = Gdk::Cursor::create(display, "col-resize");
+ window->set_cursor(cursor);
+ } else {
+ auto cursor = Gdk::Cursor::create(display, "row-resize");
+ window->set_cursor(cursor);
+ }
+
+ update_click_indicator(crossing_event->x, crossing_event->y);
+
+ return false;
+}
+
+bool MyHandle::on_leave_notify_event(GdkEventCrossing* crossing_event) {
+ show_click_indicator(false);
+ return false;
+}
+
+void MyHandle::show_click_indicator(bool show) {
+ if (!is_click_resize_active()) return;
+
+ if (show != _click_indicator) {
+ _click_indicator = show;
+ this->queue_draw();
+ }
+}
+
+void MyHandle::update_click_indicator(double x, double y) {
+ if (!is_click_resize_active()) return;
+
+ auto rect = get_active_click_zone();
+ bool inside =
+ x >= rect.x && x < rect.x + rect.width &&
+ y >= rect.y && y < rect.y + rect.height;
+
+ show_click_indicator(inside);
+}
+
+bool MyHandle::is_click_resize_active() const {
+ return get_orientation() == Gtk::ORIENTATION_HORIZONTAL;
+}
+
+bool MyHandle::on_button_press_event(GdkEventButton* button_event) {
+ // detect single-clicks
+ _click = button_event->button == 1 && button_event->type == GDK_BUTTON_PRESS;
+ return false;
+}
+
+bool MyHandle::on_button_release_event(GdkEventButton* event) {
+ // single-click on active zone?
+ if (_click && event->type == GDK_BUTTON_RELEASE && event->button == 1 && _click_indicator) {
+ _click = false;
+ _dragging = false;
+ // handle clicked
+ if (is_click_resize_active()) {
+ toggle_multipaned();
+ return true;
+ }
+ }
+
+ _click = false;
+ return false;
+}
+
+void MyHandle::toggle_multipaned() {
+ // visibility toggle of multipaned in a floating dialog window doesn't make sense; skip
+ if (dynamic_cast<DialogWindow*>(get_toplevel())) return;
+
+ auto panel = dynamic_cast<DialogMultipaned*>(get_parent());
+ if (!panel) return;
+
+ auto children = panel->get_children();
+ Gtk::Widget* multi = nullptr; // multipaned widget to toggle
+ bool left_side = true; // panels to the left of canvas
+ size_t i = 0;
+
+ // find multipaned widget to resize; it is adjacent (sibling) to 'this' handle in widget hierarchy
+ for (auto widget : children) {
+ if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(widget)) {
+ // widget past canvas are on the right side (of canvas)
+ left_side = false;
+ }
+
+ if (widget == this) {
+ if (left_side && i > 0) {
+ // handle to the left of canvas toggles preceeding panel
+ multi = dynamic_cast<DialogMultipaned*>(children[i - 1]);
+ }
+ else if (!left_side && i + 1 < children.size()) {
+ // handle to the right of canvas toggles next panel
+ multi = dynamic_cast<DialogMultipaned*>(children[i + 1]);
+ }
+
+ if (multi) {
+ if (multi->is_visible()) {
+ multi->hide();
+ }
+ else {
+ multi->show();
+ }
+ // resize parent
+ panel->children_toggled();
+ }
+ break;
+ }
+
+ ++i;
+ }
+}
+
+bool MyHandle::on_motion_notify_event(GdkEventMotion* motion_event) {
+ // motion invalidates click; it activates resizing
+ _click = false;
+ // show_click_indicator(false);
+ update_click_indicator(motion_event->x, motion_event->y);
+ return false;
+}
+
+/**
+ * This allocation handler function is used to add/remove handle icons in order to be able
+ * to hide completely a transversal handle into the sides of a DialogMultipaned.
+ *
+ * The image has a specific size set up in the constructor and will not naturally shrink/hide.
+ * In conclusion, we remove it from the handle and save it into an internal reference.
+ */
+void MyHandle::resize_handler(Gtk::Allocation &allocation)
+{
+ int size = (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) ? allocation.get_height() : allocation.get_width();
+
+ if (_cross_size > size && HANDLE_CROSS_SIZE > size && !_child) {
+ _child = get_child();
+ remove();
+ } else if (_cross_size < size && HANDLE_CROSS_SIZE < size && _child) {
+ add(*_child);
+ _child = nullptr;
+ }
+
+ _cross_size = size;
+}
+
+/* ============ DialogMultipaned ============= */
+
+DialogMultipaned::DialogMultipaned(Gtk::Orientation orientation)
+ : Glib::ObjectBase("DialogMultipaned")
+ , Gtk::Orientable()
+ , Gtk::Container()
+ , _empty_widget(nullptr)
+{
+ set_name("DialogMultipaned");
+ set_orientation(orientation);
+ set_has_window(false);
+ set_redraw_on_allocate(false);
+
+ // ============= Add dropzones ==============
+ MyDropZone *dropzone_s = Gtk::manage(new MyDropZone(orientation));
+ MyDropZone *dropzone_e = Gtk::manage(new MyDropZone(orientation));
+
+ dropzone_s->set_parent(*this);
+ dropzone_e->set_parent(*this);
+
+ children.push_back(dropzone_s);
+ children.push_back(dropzone_e);
+
+ // ============ Connect signals =============
+ gesture = Gtk::GestureDrag::create(*this);
+
+ _connections.emplace_back(
+ gesture->signal_drag_begin().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_begin)));
+ _connections.emplace_back(gesture->signal_drag_end().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_end)));
+ _connections.emplace_back(
+ gesture->signal_drag_update().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_update)));
+
+ _connections.emplace_back(
+ signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_drag_data)));
+ _connections.emplace_back(
+ dropzone_s->signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_prepend_drag_data)));
+ _connections.emplace_back(
+ dropzone_e->signal_drag_data_received().connect(sigc::mem_fun(*this, &DialogMultipaned::on_append_drag_data)));
+
+ // add empty widget to initiate the container
+ add_empty_widget();
+
+ show_all();
+}
+
+DialogMultipaned::~DialogMultipaned()
+{
+ // Disconnect all signals
+ for_each(_connections.begin(), _connections.end(), [&](auto c) { c.disconnect(); });
+ /*
+ for (std::vector<Gtk::Widget *>::iterator it = children.begin(); it != children.end();) {
+ if (dynamic_cast<DialogMultipaned *>(*it) || dynamic_cast<DialogNotebook *>(*it)) {
+ delete *it;
+ } else {
+ it++;
+ }
+ }
+ */
+
+ for (;;) {
+ auto it = std::find_if(children.begin(), children.end(), [](auto w) {
+ return dynamic_cast<DialogMultipaned *>(w) || dynamic_cast<DialogNotebook *>(w);
+ });
+ if (it != children.end()) {
+ // delete dialog multipanel or notebook; this action results in its removal from 'children'!
+ delete *it;
+ } else {
+ // no more dialog panels
+ break;
+ }
+ }
+
+ // need to remove CanvasGrid from this container to avoid on idle repainting and crash:
+ // Gtk:ERROR:../gtk/gtkwidget.c:5871:gtk_widget_get_frame_clock: assertion failed: (window != NULL)
+ // Bail out! Gtk:ERROR:../gtk/gtkwidget.c:5871:gtk_widget_get_frame_clock: assertion failed: (window != NULL)
+ for (auto child : children) {
+ if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child)) {
+ remove(*child);
+ }
+ }
+
+ children.clear();
+}
+
+void DialogMultipaned::prepend(Gtk::Widget *child)
+{
+ remove_empty_widget(); // Will remove extra widget if existing
+
+ // If there are MyMultipane children that are empty, they will be removed
+ for (auto const &child1 : children) {
+ DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child1);
+ if (paned && paned->has_empty_widget()) {
+ remove(*child1);
+ remove_empty_widget();
+ }
+ }
+
+ if (child) {
+ // Add handle
+ if (children.size() > 2) {
+ MyHandle *my_handle = Gtk::manage(new MyHandle(get_orientation()));
+ my_handle->set_parent(*this);
+ children.insert(children.begin() + 1, my_handle); // After start dropzone
+ }
+
+ // Add child
+ children.insert(children.begin() + 1, child);
+ if (!child->get_parent())
+ child->set_parent(*this);
+
+ // Ideally, we would only call child->show() here and assume that the
+ // child has already configured visibility of all its own children.
+ child->show_all();
+ }
+}
+
+void DialogMultipaned::append(Gtk::Widget *child)
+{
+ remove_empty_widget(); // Will remove extra widget if existing
+
+ // If there are MyMultipane children that are empty, they will be removed
+ for (auto const &child1 : children) {
+ DialogMultipaned *paned = dynamic_cast<DialogMultipaned *>(child1);
+ if (paned && paned->has_empty_widget()) {
+ remove(*child1);
+ remove_empty_widget();
+ }
+ }
+
+ if (child) {
+ // Add handle
+ if (children.size() > 2) {
+ MyHandle *my_handle = Gtk::manage(new MyHandle(get_orientation()));
+ my_handle->set_parent(*this);
+ children.insert(children.end() - 1, my_handle); // Before end dropzone
+ }
+
+ // Add child
+ children.insert(children.end() - 1, child);
+ if (!child->get_parent())
+ child->set_parent(*this);
+
+ // See comment in DialogMultipaned::prepend
+ child->show_all();
+ }
+}
+
+void DialogMultipaned::add_empty_widget()
+{
+ const int EMPTY_WIDGET_SIZE = 60; // magic number
+
+ // The empty widget is a label
+ auto label = Gtk::manage(new Gtk::Label(_("You can drop dockable dialogs here.")));
+ label->set_line_wrap();
+ label->set_justify(Gtk::JUSTIFY_CENTER);
+ label->set_valign(Gtk::ALIGN_CENTER);
+ label->set_vexpand();
+
+ append(label);
+ _empty_widget = label;
+
+ if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
+ int dropzone_size = (get_height() - EMPTY_WIDGET_SIZE) / 2;
+ if (dropzone_size > DROPZONE_SIZE) {
+ set_dropzone_sizes(dropzone_size, dropzone_size);
+ }
+ }
+}
+
+void DialogMultipaned::remove_empty_widget()
+{
+ if (_empty_widget) {
+ auto it = std::find(children.begin(), children.end(), _empty_widget);
+ if (it != children.end()) {
+ children.erase(it);
+ }
+ _empty_widget->unparent();
+ _empty_widget = nullptr;
+ }
+
+ if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
+ set_dropzone_sizes(DROPZONE_SIZE, DROPZONE_SIZE);
+ }
+}
+
+Gtk::Widget *DialogMultipaned::get_first_widget()
+{
+ if (children.size() > 2) {
+ return children[1];
+ } else {
+ return nullptr;
+ }
+}
+
+Gtk::Widget *DialogMultipaned::get_last_widget()
+{
+ if (children.size() > 2) {
+ return children[children.size() - 2];
+ } else {
+ return nullptr;
+ }
+}
+
+/**
+ * Set the sizes of the DialogMultipaned dropzones.
+ * @param start, the size you want or -1 for the default `DROPZONE_SIZE`
+ * @param end, the size you want or -1 for the default `DROPZONE_SIZE`
+ */
+void DialogMultipaned::set_dropzone_sizes(int start, int end)
+{
+ bool orientation = get_orientation() == Gtk::ORIENTATION_HORIZONTAL;
+
+ if (start == -1) {
+ start = DROPZONE_SIZE;
+ }
+
+ MyDropZone *dropzone_s = dynamic_cast<MyDropZone *>(children[0]);
+
+ if (dropzone_s) {
+ if (orientation) {
+ dropzone_s->set_size_request(start, -1);
+ } else {
+ dropzone_s->set_size_request(-1, start);
+ }
+ }
+
+ if (end == -1) {
+ end = DROPZONE_SIZE;
+ }
+
+ MyDropZone *dropzone_e = dynamic_cast<MyDropZone *>(children[children.size() - 1]);
+
+ if (dropzone_e) {
+ if (orientation) {
+ dropzone_e->set_size_request(end, -1);
+ } else {
+ dropzone_e->set_size_request(-1, end);
+ }
+ }
+}
+
+/**
+ * Show/hide as requested all children of this container that are of type multipaned
+ */
+void DialogMultipaned::toggle_multipaned_children(bool show)
+{
+ _handle = -1;
+ _drag_handle = -1;
+
+ for (auto child : children) {
+ if (auto panel = dynamic_cast<DialogMultipaned*>(child)) {
+ if (show) {
+ panel->show();
+ }
+ else {
+ panel->hide();
+ }
+ }
+ }
+}
+
+/**
+ * Ensure that this dialog container is visible.
+ */
+void DialogMultipaned::ensure_multipaned_children()
+{
+ toggle_multipaned_children(true);
+ // hide_multipaned = false;
+ // queue_allocate();
+}
+
+// ****************** OVERRIDES ******************
+
+// The following functions are here to define the behavior of our custom container
+
+Gtk::SizeRequestMode DialogMultipaned::get_request_mode_vfunc() const
+{
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ return Gtk::SIZE_REQUEST_WIDTH_FOR_HEIGHT;
+ } else {
+ return Gtk::SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+ }
+}
+
+void DialogMultipaned::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const
+{
+ minimum_width = 0;
+ natural_width = 0;
+ for (auto const &child : children) {
+ if (child && child->is_visible()) {
+ int child_minimum_width = 0;
+ int child_natural_width = 0;
+ child->get_preferred_width(child_minimum_width, child_natural_width);
+ if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
+ minimum_width = std::max(minimum_width, child_minimum_width);
+ natural_width = std::max(natural_width, child_natural_width);
+ } else {
+ minimum_width += child_minimum_width;
+ natural_width += child_natural_width;
+ }
+ }
+ }
+ if (_natural_width > natural_width) {
+ natural_width = _natural_width;
+ }
+}
+
+void DialogMultipaned::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const
+{
+ minimum_height = 0;
+ natural_height = 0;
+ for (auto const &child : children) {
+ if (child && child->is_visible()) {
+ int child_minimum_height = 0;
+ int child_natural_height = 0;
+ child->get_preferred_height(child_minimum_height, child_natural_height);
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ minimum_height = std::max(minimum_height, child_minimum_height);
+ natural_height = std::max(natural_height, child_natural_height);
+ } else {
+ minimum_height += child_minimum_height;
+ natural_height += child_natural_height;
+ }
+ }
+ }
+}
+
+void DialogMultipaned::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const
+{
+ minimum_width = 0;
+ natural_width = 0;
+ for (auto const &child : children) {
+ if (child && child->is_visible()) {
+ int child_minimum_width = 0;
+ int child_natural_width = 0;
+ child->get_preferred_width_for_height(height, child_minimum_width, child_natural_width);
+ if (get_orientation() == Gtk::ORIENTATION_VERTICAL) {
+ minimum_width = std::max(minimum_width, child_minimum_width);
+ natural_width = std::max(natural_width, child_natural_width);
+ } else {
+ minimum_width += child_minimum_width;
+ natural_width += child_natural_width;
+ }
+ }
+ }
+
+ if (_natural_width > natural_width) {
+ natural_width = _natural_width;
+ }
+}
+
+void DialogMultipaned::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const
+{
+ minimum_height = 0;
+ natural_height = 0;
+ for (auto const &child : children) {
+ if (child && child->is_visible()) {
+ int child_minimum_height = 0;
+ int child_natural_height = 0;
+ child->get_preferred_height_for_width(width, child_minimum_height, child_natural_height);
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+ minimum_height = std::max(minimum_height, child_minimum_height);
+ natural_height = std::max(natural_height, child_natural_height);
+ } else {
+ minimum_height += child_minimum_height;
+ natural_height += child_natural_height;
+ }
+ }
+ }
+}
+
+
+void DialogMultipaned::children_toggled() {
+ _handle = -1;
+ _drag_handle = -1;
+ queue_allocate();
+}
+
+/**
+ * This function allocates the sizes of the children widgets (be them internal or not) from
+ * the container's allocated size.
+ *
+ * Natural width: The width the widget really wants.
+ * Minimum width: The minimum width for a widget to be useful.
+ * Minimum <= Natural.
+ */
+void DialogMultipaned::on_size_allocate(Gtk::Allocation &allocation)
+{
+ set_allocation(allocation);
+ bool horizontal = get_orientation() == Gtk::ORIENTATION_HORIZONTAL;
+
+ if (_drag_handle != -1) { // Exchange allocation between the widgets on either side of moved handle
+ // Allocation values calculated in on_drag_update();
+ children[_drag_handle - 1]->size_allocate(allocation1);
+ children[_drag_handle]->size_allocate(allocationh);
+ children[_drag_handle + 1]->size_allocate(allocation2);
+ _drag_handle = -1;
+ }
+ // initially widgets get created with a 1x1 size; ignore it and wait for the final resize
+ else if (allocation.get_width() > 1 && allocation.get_height() > 1) {
+ _natural_width = allocation.get_width();
+ }
+
+ std::vector<bool> expandables; // Is child expandable?
+ std::vector<int> sizes_minimums; // Difference between allocated space and minimum space.
+ std::vector<int> sizes_naturals; // Difference between allocated space and natural space.
+ std::vector<int> sizes_current; // The current sizes along main axis
+ int left = horizontal ? allocation.get_width() : allocation.get_height();
+
+ int index = 0;
+ bool force_resize = false; // initially panels are not sized yet, so we will apply their natural sizes
+ int canvas_index = -1;
+ for (auto child : children) {
+ bool visible = child->get_visible();
+
+ if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child)) {
+ canvas_index = index;
+ }
+
+ {
+ expandables.push_back(child->compute_expand(get_orientation()));
+
+ Gtk::Requisition req_minimum;
+ Gtk::Requisition req_natural;
+ child->get_preferred_size(req_minimum, req_natural);
+ if (child == _resizing_widget1 || child == _resizing_widget2) {
+ // ignore limits for widget being resized interactively and use their current size
+ req_minimum.width = req_minimum.height = 0;
+ auto alloc = child->get_allocation();
+ req_natural.width = alloc.get_width();
+ req_natural.height = alloc.get_height();
+ }
+
+ sizes_minimums.push_back(visible ? horizontal ? req_minimum.width : req_minimum.height : 0);
+ sizes_naturals.push_back(visible ? horizontal ? req_natural.width : req_natural.height : 0);
+ }
+
+ Gtk::Allocation child_allocation = child->get_allocation();
+ sizes_current.push_back(visible ? horizontal ? child_allocation.get_width() : child_allocation.get_height()
+ : 0);
+ index++;
+
+ if (sizes_current.back() < sizes_minimums.back()) force_resize = true;
+ }
+
+ std::vector<int> sizes = sizes_current; // The new allocation sizes
+
+ const int sum_current = std::accumulate(sizes_current.begin(), sizes_current.end(), 0);
+ {
+ // Precalculate the minimum, natural and current totals
+ const int sum_minimums = std::accumulate(sizes_minimums.begin(), sizes_minimums.end(), 0);
+ const int sum_naturals = std::accumulate(sizes_naturals.begin(), sizes_naturals.end(), 0);
+
+ // initial resize requested?
+ if (force_resize && sum_naturals <= left) {
+ sizes = sizes_naturals;
+ left -= sum_naturals;
+ } else if (sum_minimums <= left && left < sum_current) {
+ // requested size exeeds available space; try shrinking it by starting from the last element
+ sizes = sizes_current;
+ auto excess = sum_current - left;
+ for (int i = static_cast<int>(sizes.size() - 1); excess > 0 && i >= 0; --i) {
+ auto extra = sizes_current[i] - sizes_minimums[i];
+ if (extra > 0) {
+ if (extra >= excess) {
+ // we are done, enough space found
+ sizes[i] -= excess;
+ excess = 0;
+ }
+ else {
+ // shrink as far as possible, then continue to the next panel
+ sizes[i] -= extra;
+ excess -= extra;
+ }
+ }
+ }
+
+ if (excess > 0) {
+ sizes = sizes_minimums;
+ left -= sum_minimums;
+ }
+ else {
+ left = 0;
+ }
+ }
+ else {
+ left = std::max(0, left - sum_current);
+ }
+ }
+
+ if (canvas_index >= 0) { // give remaining space to canvas element
+ sizes[canvas_index] += left;
+ } else { // or, if in a sub-dialogmultipaned, give it to the last panel
+
+ for (int i = static_cast<int>(children.size()) - 1; i >= 0; --i) {
+ if (expandables[i]) {
+ sizes[i] += left;
+ break;
+ }
+ }
+ }
+
+ // Check if we actually need to change the sizes on the main axis
+ left = horizontal ? allocation.get_width() : allocation.get_height();
+ if (left == sum_current) {
+ bool valid = true;
+ for (size_t i = 0; i < children.size(); ++i) {
+ valid = sizes_minimums[i] <= sizes_current[i] && // is it over the minimums?
+ (expandables[i] || sizes_current[i] <= sizes_naturals[i]); // but does it want to be expanded?
+ if (!valid)
+ break;
+ }
+ if (valid) {
+ sizes = sizes_current; // The current sizes are good, don't change anything;
+ }
+ }
+
+ // Set x and y values of allocations (widths should be correct).
+ int current_x = allocation.get_x();
+ int current_y = allocation.get_y();
+
+ // Allocate
+ for (size_t i = 0; i < children.size(); ++i) {
+ Gtk::Allocation child_allocation = children[i]->get_allocation();
+ child_allocation.set_x(current_x);
+ child_allocation.set_y(current_y);
+
+ int size = sizes[i];
+
+ if (horizontal) {
+ child_allocation.set_width(size);
+ current_x += size;
+ child_allocation.set_height(allocation.get_height());
+ } else {
+ child_allocation.set_height(size);
+ current_y += size;
+ child_allocation.set_width(allocation.get_width());
+ }
+
+ children[i]->size_allocate(child_allocation);
+ }
+}
+
+void DialogMultipaned::forall_vfunc(gboolean, GtkCallback callback, gpointer callback_data)
+{
+ for (auto const &child : children) {
+ if (child) {
+ callback(child->gobj(), callback_data);
+ }
+ }
+}
+
+void DialogMultipaned::on_add(Gtk::Widget *child)
+{
+ if (child) {
+ append(child);
+ }
+}
+
+/**
+ * Callback when a widget is removed from DialogMultipaned and executes the removal.
+ * It does not remove handles or dropzones.
+ */
+void DialogMultipaned::on_remove(Gtk::Widget *child)
+{
+ if (child) {
+ MyDropZone *dropzone = dynamic_cast<MyDropZone *>(child);
+ if (dropzone) {
+ return;
+ }
+ MyHandle *my_handle = dynamic_cast<MyHandle *>(child);
+ if (my_handle) {
+ return;
+ }
+
+ const bool visible = child->get_visible();
+ if (children.size() > 2) {
+ auto it = std::find(children.begin(), children.end(), child);
+ if (it != children.end()) { // child found
+ if (it + 2 != children.end()) { // not last widget
+ my_handle = dynamic_cast<MyHandle *>(*(it + 1));
+ my_handle->unparent();
+ child->unparent();
+ children.erase(it, it + 2);
+ } else { // last widget
+ if (children.size() == 3) { // only widget
+ child->unparent();
+ children.erase(it);
+ } else { // not only widget, delete preceding handle
+ my_handle = dynamic_cast<MyHandle *>(*(it - 1));
+ my_handle->unparent();
+ child->unparent();
+ children.erase(it - 1, it + 1);
+ }
+ }
+ }
+ }
+ if (visible) {
+ queue_resize();
+ }
+
+ if (children.size() == 2) {
+ add_empty_widget();
+ _empty_widget->set_size_request(300, -1);
+ _signal_now_empty.emit();
+ }
+ }
+}
+
+void DialogMultipaned::on_drag_begin(double start_x, double start_y)
+{
+ _hide_widget1 = _hide_widget2 = nullptr;
+ _resizing_widget1 = _resizing_widget2 = nullptr;
+ // We clicked on handle.
+ bool found = false;
+ int child_number = 0;
+ Gtk::Allocation allocation = get_allocation();
+ for (auto const &child : children) {
+ MyHandle *my_handle = dynamic_cast<MyHandle *>(child);
+ if (my_handle) {
+ Gtk::Allocation child_allocation = my_handle->get_allocation();
+
+ // Did drag start in handle?
+ int x = child_allocation.get_x() - allocation.get_x();
+ int y = child_allocation.get_y() - allocation.get_y();
+ if (x < start_x && start_x < x + child_allocation.get_width() && y < start_y &&
+ start_y < y + child_allocation.get_height()) {
+ found = true;
+ my_handle->set_dragging(true);
+ break;
+ }
+ }
+ ++child_number;
+ }
+
+ if (!found) {
+ gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (child_number < 1 || child_number > (int)(children.size() - 2)) {
+ std::cerr << "DialogMultipaned::on_drag_begin: Invalid child (" << child_number << "!!" << std::endl;
+ gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ gesture->set_state(Gtk::EVENT_SEQUENCE_CLAIMED);
+
+ // Save for use in on_drag_update().
+ _handle = child_number;
+ start_allocation1 = children[_handle - 1]->get_allocation();
+ if (!children[_handle - 1]->is_visible()) {
+ start_allocation1.set_width(0);
+ start_allocation1.set_height(0);
+ }
+ start_allocationh = children[_handle]->get_allocation();
+ start_allocation2 = children[_handle + 1]->get_allocation();
+ if (!children[_handle + 1]->is_visible()) {
+ start_allocation2.set_width(0);
+ start_allocation2.set_height(0);
+ }
+}
+
+void DialogMultipaned::on_drag_end(double offset_x, double offset_y)
+{
+ if (_handle >= 0 && _handle < children.size()) {
+ if (auto my_handle = dynamic_cast<MyHandle*>(children[_handle])) {
+ my_handle->set_dragging(false);
+ }
+ }
+
+ gesture->set_state(Gtk::EVENT_SEQUENCE_DENIED);
+ _handle = -1;
+ _drag_handle = -1;
+ if (_hide_widget1) {
+ _hide_widget1->hide();
+ }
+ if (_hide_widget2) {
+ _hide_widget2->hide();
+ }
+ _hide_widget1 = nullptr;
+ _hide_widget2 = nullptr;
+ _resizing_widget1 = nullptr;
+ _resizing_widget2 = nullptr;
+
+ queue_allocate(); // reimpose limits if any were bent during interactive resizing
+}
+
+// docking panels in application window can be collapsed (to left or right side) to make more
+// room for canvas; this functionality is only meaningful in app window, not in floating dialogs
+bool can_collapse(Gtk::Widget* widget, Gtk::Widget* handle) {
+ // can only collapse DialogMultipaned widgets
+ if (!widget || dynamic_cast<DialogMultipaned*>(widget) == nullptr) return false;
+
+ // collapsing is not supported in floating dialogs
+ if (dynamic_cast<DialogWindow*>(widget->get_toplevel())) return false;
+
+ auto parent = handle->get_parent();
+ if (!parent) return false;
+
+ // find where the resizing handle is in relation to canvas area: left or right side;
+ // next, find where the panel is in relation to the handle: on its left or right
+ bool left_side = true;
+ bool left_handle = false;
+ size_t panel_index = 0;
+ size_t handle_index = 0;
+ size_t i = 0;
+ for (auto child : parent->get_children()) {
+ if (dynamic_cast<Inkscape::UI::Widget::CanvasGrid*>(child)) {
+ left_side = false;
+ }
+ else if (child == handle) {
+ left_handle = left_side;
+ handle_index = i;
+ }
+ else if (child == widget) {
+ panel_index = i;
+ }
+ ++i;
+ }
+
+ if (left_handle && panel_index < handle_index) {
+ return true;
+ }
+ if (!left_handle && panel_index > handle_index) {
+ return true;
+ }
+
+ return false;
+}
+
+// return minimum widget size; this fn works for hidden widgets too
+int get_min_width(Gtk::Widget* widget) {
+ bool hidden = !widget->is_visible();
+ if (hidden) widget->show();
+ int minimum_size = 0;
+ int natural_size = 0;
+ widget->get_preferred_width(minimum_size, natural_size);
+ if (hidden) widget->hide();
+ return minimum_size;
+}
+
+// Different docking resizing activities use easing functions to speed up or slow down certain phases of resizing
+// Below are two picewise linear functions used for that purpose
+
+// easing function for revealing collapsed panels
+double reveal_curve(double val, double size) {
+ if (size > 0 && val <= size && val >= 0) {
+ // slow start (resistance to opening) and then quick reveal
+ auto x = val / size;
+ auto pos = x;
+ if (x <= 0.2) {
+ pos = x * 0.25;
+ }
+ else {
+ pos = x * 9.5 - 1.85;
+ if (pos > 1) pos = 1;
+ }
+ return size * pos;
+ }
+
+ return val;
+}
+
+// easing function for collapsing panels
+// note: factors for x dictate how fast resizing happens when moving mouse (with 1 being at the same speed);
+// other constants are to make this fn produce values in 0..1 range and seamlessly connect three segments
+double collapse_curve(double val, double size) {
+ if (size > 0 && val <= size && val >= 0) {
+ // slow start (resistance), short pause and then quick collapse
+ auto x = val / size;
+ auto pos = x;
+ if (x < 0.5) {
+ // fast collapsing
+ pos = x * 10 - 5 + 0.92;
+ if (pos < 0) {
+ // panel collapsed
+ pos = 0;
+ }
+ }
+ else if (x < 0.6) {
+ // pause
+ pos = 0.2 * 0.6 + 0.8; // = 0.92;
+ }
+ else {
+ // resistance to collapsing (move slow, x 0.2 decrease)
+ pos = x * 0.2 + 0.8;
+ }
+ return size * pos;
+ }
+
+ return val;
+}
+
+void DialogMultipaned::on_drag_update(double offset_x, double offset_y)
+{
+ if (_handle < 0) return;
+
+ auto child1 = children[_handle - 1];
+ auto child2 = children[_handle + 1];
+ allocation1 = children[_handle - 1]->get_allocation();
+ allocationh = children[_handle]->get_allocation();
+ allocation2 = children[_handle + 1]->get_allocation();
+
+ // HACK: The bias prevents erratic resizing when dragging the handle fast, outside the bounds of the app.
+ const int BIAS = 1;
+
+ if (get_orientation() == Gtk::ORIENTATION_HORIZONTAL) {
+
+ auto handle = children[_handle];
+
+ // function to resize panel
+ auto resize_fn = [](Gtk::Widget* handle, Gtk::Widget* child, int start_width, double& offset_x) {
+ int minimum_size = get_min_width(child);
+ auto width = start_width + offset_x;
+ bool resizing = false;
+ Gtk::Widget* hide = nullptr;
+
+ if (!child->is_visible() && can_collapse(child, handle)) {
+ child->show();
+ resizing = true;
+ }
+
+ if (width < minimum_size) {
+ if (can_collapse(child, handle)) {
+ resizing = true;
+ auto w = start_width == 0 ? reveal_curve(width, minimum_size) : collapse_curve(width, minimum_size);
+ offset_x = w - start_width;
+ // facilitate closing/opening panels: users don't have to drag handle all the
+ // way to collapse/expand a panel, they just need to move it fraction of the way;
+ // note: those thresholds correspond to the easing functions used
+ auto threshold = start_width == 0 ? minimum_size * 0.20 : minimum_size * 0.42;
+ hide = width <= threshold ? child : nullptr;
+ }
+ else {
+ offset_x = -(start_width - minimum_size) + BIAS;
+ }
+ }
+
+ return std::make_pair(resizing, hide);
+ };
+
+ /*
+ TODO NOTE:
+ Resizing should ideally take into account all columns, not just adjacent ones (left and right here).
+ Without it, expanding second collapsed column does not work, since first one may already have min width,
+ and cannot be shrunk anymore. Instead it should be pushed out of the way (canvas should be shrunk).
+ */
+
+ // panel on the left
+ auto action1 = resize_fn(handle, child1, start_allocation1.get_width(), offset_x);
+ _resizing_widget1 = action1.first ? child1 : nullptr;
+ _hide_widget1 = action1.second ? child1 : nullptr;
+
+ // panel on the right (needs reversing offset_x, so it can use the same logic)
+ offset_x = -offset_x;
+ auto action2 = resize_fn(handle, child2, start_allocation2.get_width(), offset_x);
+ _resizing_widget2 = action2.first ? child2 : nullptr;
+ _hide_widget2 = action2.second ? child2 : nullptr;
+ offset_x = -offset_x;
+
+ // set new sizes; they may temporarily violate min size panel requirements
+ // GTK is not happy about 0-size allocations
+ allocation1.set_width(start_allocation1.get_width() + offset_x);
+ allocationh.set_x(start_allocationh.get_x() + offset_x);
+ allocation2.set_x(start_allocation2.get_x() + offset_x);
+ allocation2.set_width(start_allocation2.get_width() - offset_x);
+ } else {
+ // nothing fancy about resizing in vertical direction; no panel collapsing happens here
+ int minimum_size;
+ int natural_size;
+ children[_handle - 1]->get_preferred_height(minimum_size, natural_size);
+ if (start_allocation1.get_height() + offset_y < minimum_size)
+ offset_y = -(start_allocation1.get_height() - minimum_size) + BIAS;
+ children[_handle + 1]->get_preferred_height(minimum_size, natural_size);
+ if (start_allocation2.get_height() - offset_y < minimum_size)
+ offset_y = start_allocation2.get_height() - minimum_size - BIAS;
+
+ allocation1.set_height(start_allocation1.get_height() + offset_y);
+ allocationh.set_y(start_allocationh.get_y() + offset_y);
+ allocation2.set_y(start_allocation2.get_y() + offset_y);
+ allocation2.set_height(start_allocation2.get_height() - offset_y);
+ }
+
+ _drag_handle = _handle;
+ queue_allocate(); // Relayout DialogMultipaned content.
+}
+
+void DialogMultipaned::set_target_entries(const std::vector<Gtk::TargetEntry> &target_entries)
+{
+ drag_dest_set(target_entries);
+ ((MyDropZone *)children[0])->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
+ ((MyDropZone *)children[children.size() - 1])
+ ->drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
+}
+
+void DialogMultipaned::on_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ _signal_prepend_drag_data.emit(context);
+}
+
+void DialogMultipaned::on_prepend_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ _signal_prepend_drag_data.emit(context);
+}
+
+void DialogMultipaned::on_append_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time)
+{
+ _signal_append_drag_data.emit(context);
+}
+
+// Signals
+sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> DialogMultipaned::signal_prepend_drag_data()
+{
+ resize_widget_children(this);
+ return _signal_prepend_drag_data;
+}
+
+sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> DialogMultipaned::signal_append_drag_data()
+{
+ resize_widget_children(this);
+ return _signal_append_drag_data;
+}
+
+sigc::signal<void ()> DialogMultipaned::signal_now_empty()
+{
+ return _signal_now_empty;
+}
+
+void DialogMultipaned::set_restored_width(int width) {
+ _natural_width = width;
+}
+
+} // 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/dialog/dialog-multipaned.h b/src/ui/dialog/dialog-multipaned.h
new file mode 100644
index 0000000..c7014af
--- /dev/null
+++ b/src/ui/dialog/dialog-multipaned.h
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_MULTIPANED_H
+#define INKSCAPE_UI_DIALOG_MULTIPANED_H
+
+/** @file
+ * @brief A widget with multiple panes. Agnostic to type what kind of widgets panes contain.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2020 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/refptr.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/gesturedrag.h>
+#include <gtkmm/orientable.h>
+#include <gtkmm/widget.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/** A widget with multiple panes */
+
+class MyDropZone;
+class MyHandle;
+class DialogMultipaned;
+
+/* ============ DROPZONE ============ */
+
+/**
+ * Dropzones are eventboxes at the ends of a DialogMultipaned where you can drop dialogs.
+ */
+class MyDropZone
+ : public Gtk::Orientable
+ , public Gtk::EventBox
+{
+public:
+ MyDropZone(Gtk::Orientation orientation);
+ ~MyDropZone() override;
+
+ static void add_highlight_instances();
+ static void remove_highlight_instances();
+
+private:
+ void set_size(int size);
+ bool _active = false;
+ void add_highlight();
+ void remove_highlight();
+
+ static std::list<MyDropZone *> _instances_list;
+};
+
+/* ============ HANDLE ============ */
+
+/**
+ * Handles are event boxes that help with resizing DialogMultipaned' children.
+ */
+class MyHandle
+ : public Gtk::Orientable
+ , public Gtk::EventBox
+{
+public:
+ MyHandle(Gtk::Orientation orientation, int size);
+ ~MyHandle() override = default;
+
+ bool on_enter_notify_event(GdkEventCrossing *crossing_event) override;
+ void set_dragging(bool dragging);
+private:
+ bool on_leave_notify_event(GdkEventCrossing* crossing_event) override;
+ bool on_button_press_event(GdkEventButton* button_event) override;
+ bool on_button_release_event(GdkEventButton *event) override;
+ bool on_motion_notify_event(GdkEventMotion* motion_event) override;
+ void toggle_multipaned();
+ void update_click_indicator(double x, double y);
+ void show_click_indicator(bool show);
+ bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;
+ Cairo::Rectangle get_active_click_zone();
+ int _cross_size;
+ Gtk::Widget *_child;
+ void resize_handler(Gtk::Allocation &allocation);
+ bool is_click_resize_active() const;
+ bool _click = false;
+ bool _click_indicator = false;
+ bool _dragging = false;
+};
+
+/* ============ MULTIPANE ============ */
+
+/*
+ * A widget with multiple panes. Agnostic to type what kind of widgets panes contain.
+ * Handles allow a user to resize children widgets. Drop zones allow adding widgets
+ * at either end.
+ */
+class DialogMultipaned
+ : public Gtk::Orientable
+ , public Gtk::Container
+{
+public:
+ DialogMultipaned(Gtk::Orientation orientation = Gtk::ORIENTATION_HORIZONTAL);
+ ~DialogMultipaned() override;
+
+ void prepend(Gtk::Widget *new_widget);
+ void append(Gtk::Widget *new_widget);
+
+ // Getters and setters
+ Gtk::Widget *get_first_widget();
+ Gtk::Widget *get_last_widget();
+ std::vector<Gtk::Widget *> get_children() { return children; }
+ void set_target_entries(const std::vector<Gtk::TargetEntry> &target_entries);
+ bool has_empty_widget() { return (bool)_empty_widget; }
+
+ // Signals
+ sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> signal_prepend_drag_data();
+ sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> signal_append_drag_data();
+ sigc::signal<void ()> signal_now_empty();
+
+ // UI functions
+ void set_dropzone_sizes(int start, int end);
+ void toggle_multipaned_children(bool show);
+ void children_toggled();
+ void ensure_multipaned_children();
+ void set_restored_width(int width);
+
+protected:
+ // Overrides
+ Gtk::SizeRequestMode get_request_mode_vfunc() const override;
+ 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;
+ void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override;
+ void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override;
+ void on_size_allocate(Gtk::Allocation &allocation) override;
+
+ // Allow us to keep track of our widgets ourselves.
+ void forall_vfunc(gboolean include_internals, GtkCallback callback, gpointer callback_data) override;
+
+ void on_add(Gtk::Widget *child) override;
+ void on_remove(Gtk::Widget *child) override;
+
+ // Signals
+ sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> _signal_prepend_drag_data;
+ sigc::signal<void (const Glib::RefPtr<Gdk::DragContext>)> _signal_append_drag_data;
+ sigc::signal<void ()> _signal_now_empty;
+
+private:
+ // We must manage children ourselves.
+ std::vector<Gtk::Widget *> children;
+
+ // Values used when dragging handle.
+ int _handle = -1; // Child number of active handle
+ int _drag_handle = -1;
+ Gtk::Widget* _resizing_widget1 = nullptr;
+ Gtk::Widget* _resizing_widget2 = nullptr;
+ Gtk::Widget* _hide_widget1 = nullptr;
+ Gtk::Widget* _hide_widget2 = nullptr;
+ Gtk::Allocation start_allocation1;
+ Gtk::Allocation start_allocationh;
+ Gtk::Allocation start_allocation2;
+ Gtk::Allocation allocation1;
+ Gtk::Allocation allocationh;
+ Gtk::Allocation allocation2;
+
+ Glib::RefPtr<Gtk::GestureDrag> gesture;
+ // Signal callbacks
+ void on_drag_begin(double start_x, double start_y);
+ void on_drag_end(double offset_x, double offset_y);
+ void on_drag_update(double offset_x, double offset_y);
+ void on_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time);
+ void on_prepend_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time);
+ void on_append_drag_data(const Glib::RefPtr<Gdk::DragContext> context, int x, int y,
+ const Gtk::SelectionData &selection_data, guint info, guint time);
+
+ // Others
+ Gtk::Widget *_empty_widget; // placeholder in an empty container
+ void add_empty_widget();
+ void remove_empty_widget();
+ std::vector<sigc::connection> _connections;
+ int _natural_width = 0;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_MULTIPANED_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/dialog/dialog-notebook.cpp b/src/ui/dialog/dialog-notebook.cpp
new file mode 100644
index 0000000..e8960e9
--- /dev/null
+++ b/src/ui/dialog/dialog-notebook.cpp
@@ -0,0 +1,1023 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A wrapper for Gtk::Notebook.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "dialog-notebook.h"
+
+#include <vector>
+#include <glibmm/i18n.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/scrollbar.h>
+#include <gtkmm/separatormenuitem.h>
+#include <gtkmm/menu.h>
+
+#include "enums.h"
+#include "inkscape.h"
+#include "inkscape-window.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/dialog-data.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/dialog-multipaned.h"
+#include "ui/dialog/dialog-window.h"
+#include "ui/icon-loader.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+std::list<DialogNotebook *> DialogNotebook::_instances;
+
+/**
+ * DialogNotebook constructor.
+ *
+ * @param container the parent DialogContainer of the notebook.
+ */
+DialogNotebook::DialogNotebook(DialogContainer *container)
+ : Gtk::ScrolledWindow()
+ , _container(container)
+ , _labels_auto(true)
+ , _detaching_duplicate(false)
+ , _selected_page(nullptr)
+ , _label_visible(true)
+{
+ set_name("DialogNotebook");
+ set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_NEVER);
+ set_shadow_type(Gtk::SHADOW_NONE);
+ set_vexpand(true);
+ set_hexpand(true);
+
+ // =========== Getting preferences ==========
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs == nullptr) {
+ return;
+ }
+ gint labelstautus = prefs->getInt("/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_AUTO);
+ _labels_auto = labelstautus == PREFS_NOTEBOOK_LABELS_AUTO;
+ _labels_off = labelstautus == PREFS_NOTEBOOK_LABELS_OFF;
+
+ // ============= Notebook menu ==============
+ _notebook.set_name("DockedDialogNotebook");
+ _notebook.set_show_border(false);
+ _notebook.set_group_name("InkscapeDialogGroup");
+ _notebook.set_scrollable(true);
+
+ Gtk::MenuItem *new_menu_item = nullptr;
+
+ int row = 0;
+ // Close tab
+ new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Close Current Tab")));
+ _conn.emplace_back(
+ new_menu_item->signal_activate().connect(sigc::mem_fun(*this, &DialogNotebook::close_tab_callback)));
+ _menu.attach(*new_menu_item, 0, 2, row, row + 1);
+ row++;
+
+ // Close notebook
+ new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Close Panel")));
+ _conn.emplace_back(
+ new_menu_item->signal_activate().connect(sigc::mem_fun(*this, &DialogNotebook::close_notebook_callback)));
+ _menu.attach(*new_menu_item, 0, 2, row, row + 1);
+ row++;
+
+ // Move to new window
+ new_menu_item = Gtk::manage(new Gtk::MenuItem(_("Move Tab to New Window")));
+ _conn.emplace_back(
+ new_menu_item->signal_activate().connect([=]() { pop_tab_callback(); }));
+ _menu.attach(*new_menu_item, 0, 2, row, row + 1);
+ row++;
+
+ // Separator menu item
+ // new_menu_item = Gtk::manage(new Gtk::SeparatorMenuItem());
+ // _menu.attach(*new_menu_item, 0, 2, row, row + 1);
+ // row++;
+
+ struct Dialog {
+ Glib::ustring key;
+ Glib::ustring label;
+ Glib::ustring order;
+ Glib::ustring icon_name;
+ DialogData::Category category;
+ ScrollProvider provide_scroll;
+ };
+ std::vector<Dialog> all_dialogs;
+ auto const &dialog_data = get_dialog_data();
+ all_dialogs.reserve(dialog_data.size());
+ for (auto&& kv : dialog_data) {
+ const auto& key = kv.first;
+ const auto& data = kv.second;
+ if (data.category == DialogData::Other) {
+ continue;
+ }
+ // for sorting dialogs alphabetically, remove '_' (used for accelerators)
+ Glib::ustring order = data.label; // Already translated
+ auto underscore = order.find('_');
+ if (underscore != Glib::ustring::npos) {
+ order = order.erase(underscore, 1);
+ }
+ all_dialogs.emplace_back(Dialog {
+ .key = key,
+ .label = data.label,
+ .order = order,
+ .icon_name = data.icon_name,
+ .category = data.category,
+ .provide_scroll = data.provide_scroll
+ });
+ }
+ // sort by categories and then by names
+ std::sort(all_dialogs.begin(), all_dialogs.end(), [](const Dialog& a, const Dialog& b){
+ if (a.category != b.category) return a.category < b.category;
+ return a.order < b.order;
+ });
+
+ int col = 0;
+ DialogData::Category category = DialogData::Other;
+ for (auto&& data : all_dialogs) {
+ if (data.category != category) {
+ if (col > 0) row++;
+
+ auto separator = Gtk::make_managed<Gtk::SeparatorMenuItem>();
+ _menu.attach(*separator, 0, 2, row, row + 1);
+ row++;
+
+ category = data.category;
+ auto sep = Gtk::make_managed<Gtk::MenuItem>();
+ sep->set_label(Glib::ustring(gettext(dialog_categories[category])).uppercase());
+ sep->get_style_context()->add_class("menu-category");
+ sep->set_sensitive(false);
+ _menu.attach(*sep, 0, 2, row, row + 1);
+ col = 0;
+ row++;
+ }
+ auto key = data.key;
+ auto dlg = Gtk::make_managed<Gtk::MenuItem>();
+ auto *grid = Gtk::make_managed<Gtk::Grid>();
+ grid->set_row_spacing(10);
+ grid->set_column_spacing(8);
+ grid->insert_row(0);
+ grid->insert_column(0);
+ grid->insert_column(1);
+ grid->attach(*Gtk::make_managed<Gtk::Image>(data.icon_name, Gtk::ICON_SIZE_MENU),0,0);
+ grid->attach(*Gtk::make_managed<Gtk::Label>(data.label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true),1,0);
+ dlg->add(*grid);
+ dlg->signal_activate().connect([=](){
+ // get desktop's container, it may be different than current '_container'!
+ if (auto desktop = SP_ACTIVE_DESKTOP) {
+ if (auto container = desktop->getContainer()) {
+ container->new_dialog(key);
+ }
+ }
+ });
+ _menu.attach(*dlg, col, col + 1, row, row + 1);
+ col++;
+ if (col > 1) {
+ col = 0;
+ row++;
+ }
+ }
+ if (prefs->getBool("/theme/symbolicIcons", true)) {
+ _menu.get_style_context()->add_class("symbolic");
+ }
+
+ _menu.show_all_children();
+
+ Gtk::Button* menubtn = Gtk::manage(new Gtk::Button());
+ menubtn->set_image_from_icon_name("go-down-symbolic");
+ menubtn->signal_clicked().connect([=](){ _menu.popup_at_widget(menubtn, Gdk::GRAVITY_SOUTH, Gdk::GRAVITY_NORTH, nullptr); });
+ _notebook.set_action_widget(menubtn, Gtk::PACK_END);
+ menubtn->show();
+ menubtn->set_relief(Gtk::RELIEF_NORMAL);
+ menubtn->set_valign(Gtk::ALIGN_CENTER);
+ menubtn->set_halign(Gtk::ALIGN_CENTER);
+ menubtn->set_can_focus(false);
+ menubtn->set_name("DialogMenuButton");
+
+ // =============== Signals ==================
+ _conn.emplace_back(signal_size_allocate().connect(sigc::mem_fun(*this, &DialogNotebook::on_size_allocate_scroll)));
+ _conn.emplace_back(_notebook.signal_drag_begin().connect(sigc::mem_fun(*this, &DialogNotebook::on_drag_begin)));
+ _conn.emplace_back(_notebook.signal_drag_end().connect(sigc::mem_fun(*this, &DialogNotebook::on_drag_end)));
+ _conn.emplace_back(_notebook.signal_page_added().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_added)));
+ _conn.emplace_back(_notebook.signal_page_removed().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_removed)));
+ _conn.emplace_back(_notebook.signal_switch_page().connect(sigc::mem_fun(*this, &DialogNotebook::on_page_switch)));
+
+ // ============= Finish setup ===============
+ _reload_context = true;
+ add(_notebook);
+ show_all();
+
+ _instances.push_back(this);
+}
+
+DialogNotebook::~DialogNotebook()
+{
+ // disconnect signals first, so no handlers are invoked when removing pages
+ for_each(_conn.begin(), _conn.end(), [&](auto c) { c.disconnect(); });
+ for_each(_connmenu.begin(), _connmenu.end(), [&](auto c) { c.disconnect(); });
+ for_each(_tab_connections.begin(), _tab_connections.end(), [&](auto it) { it.second.disconnect(); });
+
+ // Unlink and remove pages
+ for (int i = _notebook.get_n_pages(); i >= 0; --i) {
+ DialogBase *dialog = dynamic_cast<DialogBase *>(_notebook.get_nth_page(i));
+ _container->unlink_dialog(dialog);
+ _notebook.remove_page(i);
+ }
+
+ _conn.clear();
+ _connmenu.clear();
+ _tab_connections.clear();
+
+ _instances.remove(this);
+}
+
+void DialogNotebook::add_highlight_header()
+{
+ const auto &style = _notebook.get_style_context();
+ style->add_class("nb-highlight");
+}
+
+void DialogNotebook::remove_highlight_header()
+{
+ const auto &style = _notebook.get_style_context();
+ style->remove_class("nb-highlight");
+}
+
+/**
+ * get provide scroll
+ */
+bool
+DialogNotebook::provide_scroll(Gtk::Widget &page) {
+ auto const &dialog_data = get_dialog_data();
+ auto dialogbase = dynamic_cast<DialogBase*>(&page);
+ if (dialogbase) {
+ auto data = dialog_data.find(dialogbase->get_type());
+ if ((*data).second.provide_scroll == ScrollProvider::PROVIDE) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Set provide scroll
+ */
+Gtk::ScrolledWindow *
+DialogNotebook::get_current_scrolledwindow(bool skip_scroll_provider) {
+
+ gint pagenum = _notebook.get_current_page();
+ Gtk::Widget *page = _notebook.get_nth_page(pagenum);
+ if (page) {
+ if (skip_scroll_provider && provide_scroll(*page)) {
+ return nullptr;
+ }
+ auto container = dynamic_cast<Gtk::Container *>(page);
+ if (container) {
+ std::vector<Gtk::Widget *> widgs = container->get_children();
+ if (widgs.size()) {
+ auto scrolledwindow = dynamic_cast<Gtk::ScrolledWindow *>(widgs[0]);
+ if (scrolledwindow) {
+ return scrolledwindow;
+ }
+ }
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Adds a widget as a new page with a tab.
+ */
+void DialogNotebook::add_page(Gtk::Widget &page, Gtk::Widget &tab, Glib::ustring)
+{
+ _reload_context = true;
+ page.set_vexpand();
+
+ auto container = dynamic_cast<Gtk::Box *>(&page);
+ if (container) {
+ auto *wrapper = Gtk::manage(new Gtk::ScrolledWindow());
+ wrapper->set_vexpand(true);
+ wrapper->set_propagate_natural_height(true);
+ wrapper->set_valign(Gtk::ALIGN_FILL);
+ wrapper->set_overlay_scrolling(false);
+ wrapper->set_can_focus(false);
+ wrapper->get_style_context()->add_class("noborder");
+ auto *wrapperbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0));
+ wrapperbox->set_valign(Gtk::ALIGN_FILL);
+ wrapperbox->set_vexpand(true);
+ std::vector<Gtk::Widget *> widgs = container->get_children();
+ for (auto widg : widgs) {
+ bool expand = container->child_property_expand(*widg);
+ bool fill = container->child_property_fill(*widg);
+ guint padding = container->child_property_expand(*widg);
+ Gtk::PackType pack_type = container->child_property_pack_type(*widg);
+ container->remove(*widg);
+ if (pack_type == Gtk::PACK_START) {
+ wrapperbox->pack_start(*widg, expand, fill, padding);
+ } else {
+ wrapperbox->pack_end (*widg, expand, fill, padding);
+ }
+ }
+ wrapper->add(*wrapperbox);
+ container->add(*wrapper);
+ if (provide_scroll(page)) {
+ wrapper->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_EXTERNAL);
+ } else {
+ wrapper->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ }
+ }
+
+ int page_number = _notebook.append_page(page, tab);
+ _notebook.set_tab_reorderable(page);
+ _notebook.set_tab_detachable(page);
+ _notebook.show_all();
+ _notebook.set_current_page(page_number);
+}
+
+/**
+ * Moves a page from a different notebook to this one.
+ */
+void DialogNotebook::move_page(Gtk::Widget &page)
+{
+ // Find old notebook
+ Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(page.get_parent());
+ if (!old_notebook) {
+ std::cerr << "DialogNotebook::move_page: page not in notebook!" << std::endl;
+ return;
+ }
+
+ Gtk::Widget *tab = old_notebook->get_tab_label(page);
+ Glib::ustring text = old_notebook->get_menu_label_text(page);
+
+ // Keep references until re-attachment
+ tab->reference();
+ page.reference();
+
+ old_notebook->detach_tab(page);
+ _notebook.append_page(page, *tab);
+ // Remove unnecessary references
+ tab->unreference();
+ page.unreference();
+
+ // Set default settings for a new page
+ _notebook.set_tab_reorderable(page);
+ _notebook.set_tab_detachable(page);
+ _notebook.show_all();
+ _reload_context = true;
+}
+
+// ============ Notebook callbacks ==============
+
+/**
+ * Callback to close the current active tab.
+ */
+void DialogNotebook::close_tab_callback()
+{
+ int page_number = _notebook.get_current_page();
+
+ if (_selected_page) {
+ page_number = _notebook.page_num(*_selected_page);
+ _selected_page = nullptr;
+ }
+
+ if (dynamic_cast<DialogBase*>(_notebook.get_nth_page(page_number))) {
+ // is this a dialog in a floating window?
+ if (auto window = dynamic_cast<DialogWindow*>(_container->get_toplevel())) {
+ // store state of floating dialog before it gets deleted
+ DialogManager::singleton().store_state(*window);
+ }
+ }
+
+ // Remove page from notebook
+ _notebook.remove_page(page_number);
+
+ // Delete the signal connection
+ remove_close_tab_callback(_selected_page);
+
+ if (_notebook.get_n_pages() == 0) {
+ close_notebook_callback();
+ return;
+ }
+
+ // Update tab labels by comparing the sum of their widths to the allocation
+ Gtk::Allocation allocation = get_allocation();
+ on_size_allocate_scroll(allocation);
+ _reload_context = true;
+}
+
+/**
+ * Shutdown callback - delete the parent DialogMultipaned before destructing.
+ */
+void DialogNotebook::close_notebook_callback()
+{
+ // Search for DialogMultipaned
+ DialogMultipaned *multipaned = dynamic_cast<DialogMultipaned *>(get_parent());
+ if (multipaned) {
+ multipaned->remove(*this);
+ } else if (get_parent()) {
+ std::cerr << "DialogNotebook::close_notebook_callback: Unexpected parent!" << std::endl;
+ get_parent()->remove(*this);
+ }
+ delete this;
+}
+
+/**
+ * Callback to move the current active tab.
+ */
+DialogWindow* DialogNotebook::pop_tab_callback()
+{
+ // Find page.
+ Gtk::Widget *page = _notebook.get_nth_page(_notebook.get_current_page());
+
+ if (_selected_page) {
+ page = _selected_page;
+ _selected_page = nullptr;
+ }
+
+ if (!page) {
+ std::cerr << "DialogNotebook::pop_tab_callback: page not found!" << std::endl;
+ return nullptr;
+ }
+
+ // Move page to notebook in new dialog window (attached to active InkscapeWindow).
+ auto inkscape_window = _container->get_inkscape_window();
+ auto window = new DialogWindow(inkscape_window, page);
+ window->show_all();
+
+ if (_notebook.get_n_pages() == 0) {
+ close_notebook_callback();
+ return window;
+ }
+
+ // Update tab labels by comparing the sum of their widths to the allocation
+ Gtk::Allocation allocation = get_allocation();
+ on_size_allocate_scroll(allocation);
+
+ return window;
+}
+
+// ========= Signal handlers - notebook =========
+
+/**
+ * Signal handler to pop a dragged tab into its own DialogWindow.
+ *
+ * A failed drag means that the page was not dropped on an existing notebook.
+ * Thus create a new window with notebook to move page to.
+ *
+ * BUG: this has inconsistent behavior on Wayland.
+ */
+void DialogNotebook::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ // Remove dropzone highlights
+ MyDropZone::remove_highlight_instances();
+ for (auto instance : _instances) {
+ instance->remove_highlight_header();
+ }
+
+ bool set_floating = !context->get_dest_window();
+ if (!set_floating && context->get_dest_window()->get_window_type() == Gdk::WINDOW_FOREIGN) {
+ set_floating = true;
+ }
+
+ if (set_floating) {
+ Gtk::Widget *source = Gtk::Widget::drag_get_source_widget(context);
+
+ // Find source notebook and page
+ Gtk::Notebook *old_notebook = dynamic_cast<Gtk::Notebook *>(source);
+ if (!old_notebook) {
+ std::cerr << "DialogNotebook::on_drag_end: notebook not found!" << std::endl;
+ } else {
+ // Find page
+ Gtk::Widget *page = old_notebook->get_nth_page(old_notebook->get_current_page());
+ if (page) {
+ // Move page to notebook in new dialog window
+
+ auto inkscape_window = _container->get_inkscape_window();
+ auto window = new DialogWindow(inkscape_window, page);
+
+ // Move window to mouse pointer
+ if (auto device = context->get_device()) {
+ int x = 0, y = 0;
+ device->get_position(x, y);
+ window->move(std::max(0, x - 50), std::max(0, y - 50));
+ }
+
+ window->show_all();
+ }
+ }
+ }
+
+ // Closes the notebook if empty.
+ if (_notebook.get_n_pages() == 0) {
+ close_notebook_callback();
+ return;
+ }
+
+ // Update tab labels by comparing the sum of their widths to the allocation
+ Gtk::Allocation allocation = get_allocation();
+ on_size_allocate_scroll(allocation);
+}
+
+void DialogNotebook::on_drag_begin(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ MyDropZone::add_highlight_instances();
+ for (auto instance : _instances) {
+ instance->add_highlight_header();
+ }
+}
+
+/**
+ * Signal handler to update dialog list when adding a page.
+ */
+void DialogNotebook::on_page_added(Gtk::Widget *page, int page_num)
+{
+ DialogBase *dialog = dynamic_cast<DialogBase *>(page);
+
+ // Does current container/window already have such a dialog?
+ if (dialog && _container->has_dialog_of_type(dialog)) {
+ // We already have a dialog of the same type
+
+ // Highlight first dialog
+ DialogBase *other_dialog = _container->get_dialog(dialog->get_type());
+ other_dialog->blink();
+
+ // Remove page from notebook
+ _detaching_duplicate = true; // HACK: prevent removing the initial dialog of the same type
+ _notebook.detach_tab(*page);
+ return;
+ } else if (dialog) {
+ // We don't have a dialog of this type
+
+ // Add to dialog list
+ _container->link_dialog(dialog);
+ } else {
+ // This is not a dialog
+ return;
+ }
+
+ // add close tab signal
+ add_close_tab_callback(page);
+
+ // Switch tab labels if needed
+ if (!_labels_auto) {
+ toggle_tab_labels_callback(false);
+ }
+
+ // Update tab labels by comparing the sum of their widths to the allocation
+ Gtk::Allocation allocation = get_allocation();
+ on_size_allocate_scroll(allocation);
+}
+
+/**
+ * Signal handler to update dialog list when removing a page.
+ */
+void DialogNotebook::on_page_removed(Gtk::Widget *page, int page_num)
+{
+ /**
+ * When adding a dialog in a notebooks header zone of the same type as an existing one,
+ * we remove it immediately, which triggers a call to this method. We use `_detaching_duplicate`
+ * to prevent reemoving the initial dialog.
+ */
+ if (_detaching_duplicate) {
+ _detaching_duplicate = false;
+ return;
+ }
+
+ // Remove from dialog list
+ DialogBase *dialog = dynamic_cast<DialogBase *>(page);
+ if (dialog) {
+ _container->unlink_dialog(dialog);
+ }
+
+ // remove old close tab signal
+ remove_close_tab_callback(page);
+}
+
+/**
+ * We need to remove the scrollbar to snap a whole DialogNotebook to width 0.
+ *
+ */
+void DialogNotebook::on_size_allocate_scroll(Gtk::Allocation &a)
+{
+ // magic number
+ const int MIN_HEIGHT = 60;
+ // set or unset scrollbars to completely hide a notebook
+ // because we have a "blocking" scroll per tab we need to loop to aboid
+ // other page stop out scroll
+ for (auto const &page : _notebook.get_children()) {
+ auto *container = dynamic_cast<Gtk::Container *>(page);
+ if (container && !provide_scroll(*page)) {
+ std::vector<Gtk::Widget *> widgs = container->get_children();
+ if (widgs.size()) {
+ auto scrolledwindow = dynamic_cast<Gtk::ScrolledWindow *>(widgs[0]);
+ if (scrolledwindow) {
+ double height = scrolledwindow->get_allocation().get_height();
+ if (height > 1) {
+ Gtk::PolicyType policy = scrolledwindow->property_vscrollbar_policy().get_value();
+ if (height >= MIN_HEIGHT && policy != Gtk::POLICY_AUTOMATIC) {
+ scrolledwindow->property_vscrollbar_policy().set_value(Gtk::POLICY_AUTOMATIC);
+ } else if(height < MIN_HEIGHT && policy != Gtk::POLICY_EXTERNAL) {
+ scrolledwindow->property_vscrollbar_policy().set_value(Gtk::POLICY_EXTERNAL);
+ } else {
+ // we don't need to update; break
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ set_allocation(a);
+ // only update notebook tabs on horizontal changes
+ if (a.get_width() != _prev_alloc_width) {
+ on_size_allocate_notebook(a);
+ }
+}
+
+/**
+ * This function hides the tab labels if necessary (and _labels_auto == true)
+ */
+void DialogNotebook::on_size_allocate_notebook(Gtk::Allocation &a)
+{
+
+ // we unset scrollable when FULL mode on to prevent overflow with
+ // container at full size that makes an unmaximized desktop freeze
+ _notebook.set_scrollable(false);
+ if (!_labels_set_off && !_labels_auto) {
+ toggle_tab_labels_callback(false);
+ }
+ if (!_labels_auto) {
+ return;
+ }
+
+ int alloc_width = get_allocation().get_width();
+ // Don't update on closed dialog container, prevent console errors
+ if (alloc_width < 2) {
+ _notebook.set_scrollable(true);
+ return;
+ }
+ int nat_width = 0;
+ int initial_width = 0;
+ int total_width = 0;
+ _notebook.get_preferred_width(initial_width, nat_width); // get current notebook allocation
+ for (auto const &page : _notebook.get_children()) {
+ Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page));
+ if (!cover) {
+ continue;
+ }
+ cover->show_all();
+ }
+ _notebook.get_preferred_width(total_width, nat_width); // get full notebook allocation (all open)
+ prev_tabstatus = tabstatus;
+ if (_single_tab_width != _none_tab_width &&
+ ((_none_tab_width && _none_tab_width > alloc_width) ||
+ (_single_tab_width > alloc_width && _single_tab_width < total_width)))
+ {
+ tabstatus = TabsStatus::NONE;
+ if (_single_tab_width != initial_width || prev_tabstatus == TabsStatus::NONE) {
+ _none_tab_width = initial_width;
+ }
+ } else {
+ tabstatus = (alloc_width <= total_width) ? TabsStatus::SINGLE : TabsStatus::ALL;
+ if (total_width != initial_width &&
+ prev_tabstatus == TabsStatus::SINGLE &&
+ tabstatus == TabsStatus::SINGLE)
+ {
+ _single_tab_width = initial_width;
+ }
+ }
+ if ((_single_tab_width && !_none_tab_width) ||
+ (_single_tab_width && _single_tab_width == _none_tab_width))
+ {
+ _none_tab_width = _single_tab_width - 1;
+ }
+
+ /*
+ std::cout << "::::::::::tabstatus::" << (int)tabstatus << std::endl;
+ std::cout << ":::::prev_tabstatus::" << (int)prev_tabstatus << std::endl;
+ std::cout << "::::::::alloc_width::" << alloc_width << std::endl;
+ std::cout << "::_prev_alloc_width::" << _prev_alloc_width << std::endl;
+ std::cout << "::::::initial_width::" << initial_width << std::endl;
+ std::cout << "::::::::::nat_width::" << nat_width << std::endl;
+ std::cout << "::::::::total_width::" << total_width << std::endl;
+ std::cout << "::::_none_tab_width::" << _none_tab_width << std::endl;
+ std::cout << "::_single_tab_width::" << _single_tab_width << std::endl;
+ std::cout << ":::::::::::::::::::::" << std::endl;
+ */
+
+ _prev_alloc_width = alloc_width;
+ bool show = tabstatus == TabsStatus::ALL;
+ toggle_tab_labels_callback(show);
+}
+
+/**
+ * Signal handler to close a tab when middle-clicking.
+ */
+bool DialogNotebook::on_tab_click_event(GdkEventButton *event, Gtk::Widget *page)
+{
+ if (event->type == GDK_BUTTON_PRESS) {
+ if (event->button == 2) { // Close tab
+ _selected_page = page;
+ close_tab_callback();
+ } else if (event->button == 3) { // Show menu
+ _selected_page = page;
+ reload_tab_menu();
+ _menutabs.popup_at_pointer((GdkEvent *)event);
+ }
+ }
+
+ return false;
+}
+
+void DialogNotebook::on_close_button_click_event(Gtk::Widget *page)
+{
+ _selected_page = page;
+ close_tab_callback();
+}
+
+// ================== Helpers ===================
+
+/**
+ * Reload tab menu
+ */
+void DialogNotebook::reload_tab_menu()
+{
+ if (_reload_context) {
+ _reload_context = false;
+ Gtk::MenuItem* menuitem = nullptr;
+ for_each(_connmenu.begin(), _connmenu.end(), [&](auto c) { c.disconnect(); });
+ _connmenu.clear();
+ for (auto widget : _menutabs.get_children()) {
+ delete widget;
+ }
+ auto prefs = Inkscape::Preferences::get();
+ bool symbolic = false;
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ symbolic = true;
+ }
+
+ for (auto const &page : _notebook.get_children()) {
+ Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page));
+ if (!cover) {
+ continue;
+ }
+
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child());
+
+ if (!box) {
+ continue;
+ }
+ auto childs = box->get_children();
+ if (childs.size() < 2) {
+ continue;
+ }
+ // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can
+ // only hold one child
+ Gtk::Box *boxmenu = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ boxmenu->set_halign(Gtk::ALIGN_START);
+ menuitem = Gtk::manage(new Gtk::MenuItem());
+ menuitem->add(*boxmenu);
+
+ Gtk::Label *label = dynamic_cast<Gtk::Label *>(childs[1]);
+ Gtk::Label *labelto = Gtk::manage(new Gtk::Label(label->get_text()));
+
+ Gtk::Image *icon = dynamic_cast<Gtk::Image *>(childs[0]);
+ if (icon) {
+ int min_width, nat_width;
+ icon->get_preferred_width(min_width, nat_width);
+ _icon_width = min_width;
+ auto name = icon->get_icon_name();
+ if (!name.empty()) {
+ if (symbolic && name.find("-symbolic") == Glib::ustring::npos) {
+ name += Glib::ustring("-symbolic");
+ }
+ Gtk::Image *iconend = sp_get_icon_image(name, Gtk::ICON_SIZE_MENU);
+ boxmenu->pack_start(*iconend, false, false, 0);
+ }
+ }
+ boxmenu->pack_start(*labelto, true, true, 0);
+ size_t pagenum = _notebook.page_num(*page);
+ _connmenu.emplace_back(
+ menuitem->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &DialogNotebook::change_page),pagenum)));
+
+ _menutabs.append(*Gtk::manage(menuitem));
+ }
+ }
+ _menutabs.show_all();
+}
+/**
+ * Callback to toggle all tab labels to the selected state.
+ * @param show: whether you want the labels to show or not
+ */
+void DialogNotebook::toggle_tab_labels_callback(bool show)
+{
+ _label_visible = show;
+ for (auto const &page : _notebook.get_children()) {
+ Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page));
+ if (!cover) {
+ continue;
+ }
+
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child());
+ if (!box) {
+ continue;
+ }
+
+ Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]);
+ Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin());
+ int n = _notebook.get_current_page();
+ if (close && label) {
+ if (page != _notebook.get_nth_page(n)) {
+ show ? close->show() : close->hide();
+ show ? label->show() : label->hide();
+ } else if (tabstatus == TabsStatus::NONE || _labels_off) {
+ if (page != _notebook.get_nth_page(n)) {
+ close->hide();
+ } else {
+ close->show();
+ }
+ label->hide();
+ } else {
+ close->show();
+ label->show();
+ }
+ }
+ }
+ _labels_set_off = _labels_off;
+ if (_prev_alloc_width && prev_tabstatus != tabstatus && (show || tabstatus != TabsStatus::NONE || !_labels_off)) {
+ resize_widget_children(&_notebook);
+ }
+ if (show && _single_tab_width) {
+ _notebook.set_scrollable(true);
+ }
+}
+
+void DialogNotebook::on_page_switch(Gtk::Widget *curr_page, guint)
+{
+ if (auto container = dynamic_cast<Gtk::Container *>(curr_page)) {
+ container->show_all_children();
+ }
+ for (auto const &page : _notebook.get_children()) {
+ auto dialogbase = dynamic_cast<DialogBase*>(page);
+ if (dialogbase) {
+ std::vector<Gtk::Widget *> widgs = dialogbase->get_children();
+ if (widgs.size()) {
+ if (curr_page == page) {
+ widgs[0]->show_now();
+ } else {
+ widgs[0]->hide();
+ }
+ }
+ if (_prev_alloc_width) {
+ dialogbase->setShowing(curr_page == page);
+ }
+ }
+ if (_label_visible) {
+ continue;
+ }
+ Gtk::EventBox *cover = dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*page));
+ if (!cover) {
+ continue;
+ }
+
+ if (cover == dynamic_cast<Gtk::EventBox *>(_notebook.get_tab_label(*curr_page))) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child());
+ Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]);
+ Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin());
+
+ if (label) {
+ if (tabstatus == TabsStatus::NONE) {
+ label->hide();
+ } else {
+ label->show();
+ }
+ }
+
+ if (close) {
+ if (tabstatus == TabsStatus::NONE && curr_page != page) {
+ close->hide();
+ } else {
+ close->show();
+ }
+ }
+ continue;
+ }
+
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(cover->get_child());
+ if (!box) {
+ continue;
+ }
+
+ Gtk::Label *label = dynamic_cast<Gtk::Label *>(box->get_children()[1]);
+ Gtk::Button *close = dynamic_cast<Gtk::Button *>(*box->get_children().rbegin());
+
+
+ close->hide();
+ label->hide();
+ }
+ if (_prev_alloc_width) {
+ if (!_label_visible) {
+ queue_allocate();
+ }
+ auto window = dynamic_cast<DialogWindow*>(_container->get_toplevel());
+ if (window) {
+ resize_widget_children(window->get_container());
+ } else {
+ if (auto desktop = SP_ACTIVE_DESKTOP) {
+ if (auto container = desktop->getContainer()) {
+ resize_widget_children(container);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Helper method that change the page
+ */
+void DialogNotebook::change_page(size_t pagenum)
+{
+ _notebook.set_current_page(pagenum);
+}
+
+/**
+ * Helper method that adds the close tab signal connection for the page given.
+ */
+void DialogNotebook::add_close_tab_callback(Gtk::Widget *page)
+{
+ Gtk::Widget *tab = _notebook.get_tab_label(*page);
+ auto *eventbox = static_cast<Gtk::EventBox *>(tab);
+ auto *box = static_cast<Gtk::Box *>(*eventbox->get_children().begin());
+ auto children = box->get_children();
+ auto *close = static_cast<Gtk::Button *>(*children.crbegin());
+
+ sigc::connection close_connection = close->signal_clicked().connect(
+ sigc::bind<Gtk::Widget *>(sigc::mem_fun(*this, &DialogNotebook::on_close_button_click_event), page), true);
+
+ sigc::connection tab_connection = tab->signal_button_press_event().connect(
+ sigc::bind<Gtk::Widget *>(sigc::mem_fun(*this, &DialogNotebook::on_tab_click_event), page), true);
+
+ _tab_connections.insert(std::pair<Gtk::Widget *, sigc::connection>(page, tab_connection));
+ _tab_connections.insert(std::pair<Gtk::Widget *, sigc::connection>(page, close_connection));
+}
+
+/**
+ * Helper method that removes the close tab signal connection for the page given.
+ */
+void DialogNotebook::remove_close_tab_callback(Gtk::Widget *page)
+{
+ auto tab_connection_it = _tab_connections.find(page);
+
+ while (tab_connection_it != _tab_connections.end()) {
+ (*tab_connection_it).second.disconnect();
+ _tab_connections.erase(tab_connection_it);
+ tab_connection_it = _tab_connections.find(page);
+ }
+}
+
+void DialogNotebook::get_preferred_height_for_width_vfunc(int width, int& minimum_height, int& natural_height) const {
+ Gtk::ScrolledWindow::get_preferred_height_for_width_vfunc(width, minimum_height, natural_height);
+ if (_natural_height > 0) {
+ natural_height = _natural_height;
+ if (minimum_height > _natural_height) {
+ minimum_height = _natural_height;
+ }
+ }
+}
+
+void DialogNotebook::get_preferred_height_vfunc(int& minimum_height, int& natural_height) const {
+ Gtk::ScrolledWindow::get_preferred_height_vfunc(minimum_height, natural_height);
+ if (_natural_height > 0) {
+ natural_height = _natural_height;
+ if (minimum_height > _natural_height) {
+ minimum_height = _natural_height;
+ }
+ }
+}
+
+void DialogNotebook::set_requested_height(int height) {
+ _natural_height = height;
+}
+
+} // 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/dialog/dialog-notebook.h b/src/ui/dialog/dialog-notebook.h
new file mode 100644
index 0000000..a4505a6
--- /dev/null
+++ b/src/ui/dialog/dialog-notebook.h
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_NOTEBOOK_H
+#define INKSCAPE_UI_DIALOG_NOTEBOOK_H
+
+/** @file
+ * @brief A wrapper for Gtk::Notebook.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gdkmm/dragcontext.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/widget.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+enum class TabsStatus {
+ NONE,
+ SINGLE,
+ ALL
+};
+
+class DialogContainer;
+class DialogWindow;
+
+/**
+ * A widget that wraps a Gtk::Notebook with dialogs as pages.
+ *
+ * A notebook is fixed to a specific DialogContainer which manages the dialogs inside the notebook.
+ */
+class DialogNotebook : public Gtk::ScrolledWindow
+{
+public:
+ DialogNotebook(DialogContainer *container);
+ ~DialogNotebook() override;
+
+ void add_page(Gtk::Widget &page, Gtk::Widget &tab, Glib::ustring label);
+ void move_page(Gtk::Widget &page);
+
+ // Getters
+ Gtk::Notebook *get_notebook() { return &_notebook; }
+ DialogContainer *get_container() { return _container; }
+
+ // Notebook callbacks
+ void close_tab_callback();
+ void close_notebook_callback();
+ DialogWindow* pop_tab_callback();
+ Gtk::ScrolledWindow * get_current_scrolledwindow(bool skip_scroll_provider);
+ void set_requested_height(int height);
+private:
+ // Widgets
+ DialogContainer *_container;
+ Gtk::Menu _menu;
+ Gtk::Menu _menutabs;
+ Gtk::Notebook _notebook;
+
+ // State variables
+ bool _label_visible;
+ bool _labels_auto;
+ bool _labels_off;
+ bool _labels_set_off = false;
+ bool _detaching_duplicate;
+ bool _reload_context = true;
+ gint _prev_alloc_width = 0;
+ gint _none_tab_width = 0;
+ gint _single_tab_width = 0;
+ gint _icon_width = 0;
+ TabsStatus tabstatus = TabsStatus::NONE;
+ TabsStatus prev_tabstatus = TabsStatus::NONE;
+ Gtk::Widget *_selected_page;
+ std::vector<sigc::connection> _conn;
+ std::vector<sigc::connection> _connmenu;
+ std::multimap<Gtk::Widget *, sigc::connection> _tab_connections;
+
+ static std::list<DialogNotebook *> _instances;
+ void add_highlight_header();
+ void remove_highlight_header();
+
+ // Signal handlers - notebook
+ void on_drag_begin(const Glib::RefPtr<Gdk::DragContext> &context) override;
+ void on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context) override;
+ void on_page_added(Gtk::Widget *page, int page_num);
+ void on_page_removed(Gtk::Widget *page, int page_num);
+ void on_size_allocate_scroll(Gtk::Allocation &allocation);
+ void on_size_allocate_notebook(Gtk::Allocation &allocation);
+ bool on_tab_click_event(GdkEventButton *event, Gtk::Widget *page);
+ void on_close_button_click_event(Gtk::Widget *page);
+ void on_page_switch(Gtk::Widget *page, guint page_number);
+ // Helpers
+ bool provide_scroll(Gtk::Widget &page);
+ void preventOverflow();
+ void change_page(size_t pagenum);
+ void reload_tab_menu();
+ void toggle_tab_labels_callback(bool show);
+ void add_close_tab_callback(Gtk::Widget *page);
+ void remove_close_tab_callback(Gtk::Widget *page);
+ void get_preferred_height_for_width_vfunc(int width, int& minimum_height, int& natural_height) const override;
+ void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override;
+ int _natural_height = 0;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_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/dialog/dialog-window.cpp b/src/ui/dialog/dialog-window.cpp
new file mode 100644
index 0000000..9ec49a7
--- /dev/null
+++ b/src/ui/dialog/dialog-window.cpp
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/** @file
+ * @brief A window for floating dialogs.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/dialog/dialog-window.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/application.h>
+#include <gtkmm/box.h>
+#include <gtkmm/label.h>
+#include <iostream>
+
+#include "enums.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "preferences.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/dialog-manager.h"
+#include "ui/dialog/dialog-multipaned.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/shortcuts.h"
+#include "ui/util.h"
+
+// Sizing constants
+const int MINIMUM_WINDOW_WIDTH = 210;
+const int MINIMUM_WINDOW_HEIGHT = 320;
+const int INITIAL_WINDOW_WIDTH = 360;
+const int INITIAL_WINDOW_HEIGHT = 520;
+const int WINDOW_DROPZONE_SIZE = 10;
+const int WINDOW_DROPZONE_SIZE_LARGE = 16;
+const int NOTEBOOK_TAB_HEIGHT = 36;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogNotebook;
+class DialogContainer;
+
+DialogWindow::~DialogWindow() {}
+
+// Create a dialog window and move page from old notebook.
+DialogWindow::DialogWindow(InkscapeWindow *inkscape_window, Gtk::Widget *page)
+ : Gtk::Window()
+ , _app(InkscapeApplication::instance())
+ , _inkscape_window(inkscape_window)
+ , _title(_("Dialog Window"))
+{
+ g_assert(_app != nullptr);
+ g_assert(_inkscape_window != nullptr);
+
+ // ============ Initialization ===============
+ // Setting the window type
+ set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG);
+ set_transient_for(*inkscape_window);
+
+ // Add the dialog window to our app
+ _app->gtk_app()->add_window(*this);
+
+ this->signal_delete_event().connect([=](GdkEventAny *) {
+ DialogManager::singleton().store_state(*this);
+ delete this;
+ return true;
+ });
+
+ auto win_action_group = dynamic_cast<Gio::ActionGroup *>(inkscape_window);
+ if (win_action_group) {
+ // Must use C API as C++ API takes a RefPtr which we can't get (easily).
+ gtk_widget_insert_action_group(GTK_WIDGET(this->gobj()), "win", win_action_group->gobj());
+ } else {
+ std::cerr << "DialogWindow::DialogWindow: Can't find InkscapeWindow Gio:ActionGroup!" << std::endl;
+ }
+
+ insert_action_group("doc", inkscape_window->get_document()->getActionGroup());
+
+ // ============ Theming: icons ==============
+
+
+ // ================ Window ==================
+ set_title(_title);
+ set_name(_title);
+ int window_width = INITIAL_WINDOW_WIDTH;
+ int window_height = INITIAL_WINDOW_HEIGHT;
+
+ // =============== Outer Box ================
+ Gtk::Box *box_outer = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ add(*box_outer);
+
+ // =============== Container ================
+ _container = Gtk::manage(new DialogContainer(inkscape_window));
+ DialogMultipaned *columns = _container->get_columns();
+ auto drop_size = Inkscape::Preferences::get()->getBool("/options/dockingzone/value", true) ? WINDOW_DROPZONE_SIZE / 2 : WINDOW_DROPZONE_SIZE;
+ columns->set_dropzone_sizes(drop_size, drop_size);
+ box_outer->pack_end(*_container);
+
+ // If there is no page, create an empty Dialogwindow to be populated later
+ if (page) {
+ // ============= Initial Column =============
+ DialogMultipaned *column = _container->create_column();
+ columns->append(column);
+
+ // ============== New Notebook ==============
+ DialogNotebook *dialog_notebook = Gtk::manage(new DialogNotebook(_container));
+ column->append(dialog_notebook);
+ column->set_dropzone_sizes(drop_size, drop_size);
+ dialog_notebook->move_page(*page);
+
+ // Set window title
+ DialogBase *dialog = dynamic_cast<DialogBase *>(page);
+ if (dialog) {
+ _title = dialog->get_name();
+ set_title(_title);
+ }
+
+ // Set window size considering what the dialog needs
+ Gtk::Requisition minimum_size, natural_size;
+ dialog->get_preferred_size(minimum_size, natural_size);
+ int overhead = 2 * (drop_size + dialog->property_margin().get_value());
+ int width = natural_size.width + overhead;
+ int height = natural_size.height + overhead + NOTEBOOK_TAB_HEIGHT;
+ window_width = std::max(width, window_width);
+ window_height = std::max(height, window_height);
+ }
+
+ // Set window sizing
+ set_size_request(MINIMUM_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT);
+ set_default_size(window_width, window_height);
+
+ if (page) {
+ update_dialogs();
+ }
+
+ // window is created hidden; don't show it now, its size needs to be restored
+}
+
+/**
+ * Change InkscapeWindow that DialogWindow is linked to.
+ */
+void DialogWindow::set_inkscape_window(InkscapeWindow* inkscape_window)
+{
+ if (!inkscape_window) {
+ std::cerr << "DialogWindow::set_inkscape_window: no inkscape_window!" << std::endl;
+ return;
+ }
+
+ _inkscape_window = inkscape_window;
+ update_dialogs();
+}
+
+/**
+ * Update all dialogs that are owned by the DialogWindow's _container.
+ */
+void DialogWindow::update_dialogs()
+{
+ g_assert(_app != nullptr);
+ g_assert(_container != nullptr);
+ g_assert(_inkscape_window != nullptr);
+
+ _container->set_inkscape_window(_inkscape_window);
+ _container->update_dialogs(); // Updating dialogs is not using the _app reference here.
+
+ // Set window title.
+ const std::multimap<Glib::ustring, DialogBase *> *dialogs = _container->get_dialogs();
+ if (dialogs->size() > 1) {
+ _title = "Multiple dialogs";
+ } else if (dialogs->size() == 1) {
+ _title = dialogs->begin()->second->get_name();
+ } else {
+ // Should not happen... but does on closing a window!
+ // std::cerr << "DialogWindow::update_dialogs(): No dialogs!" << std::endl;
+ _title = "";
+ }
+
+ auto document_name = _inkscape_window->get_document()->getDocumentName();
+ if (document_name) {
+ set_title(_title + " - " + Glib::ustring(document_name));
+ }
+}
+
+/**
+ * Update window width and height in order to fit all dialogs inisde its container.
+ *
+ * The intended use of this function is at initialization.
+ */
+void DialogWindow::update_window_size_to_fit_children()
+{
+ // Declare variables
+ int pos_x = 0, pos_y = 0;
+ int width = 0, height = 0;
+ int overhead = 0, baseline;
+ Gtk::Allocation allocation;
+ Gtk::Requisition minimum_size, natural_size;
+
+ // Read needed data
+ get_position(pos_x, pos_y);
+ get_allocated_size(allocation, baseline);
+ const std::multimap<Glib::ustring, DialogBase *> *dialogs = _container->get_dialogs();
+
+ // Get largest sizes for dialogs
+ for (auto dialog : *dialogs) {
+ dialog.second->get_preferred_size(minimum_size, natural_size);
+ width = std::max(natural_size.width, width);
+ height = std::max(natural_size.height, height);
+ overhead = std::max(overhead, dialog.second->property_margin().get_value());
+ }
+
+ // Compute sizes including overhead
+ overhead = 2 * (WINDOW_DROPZONE_SIZE_LARGE + overhead);
+ width = width + overhead;
+ height = height + overhead + NOTEBOOK_TAB_HEIGHT;
+
+ // If sizes are lower then current, don't change them
+ if (allocation.get_width() >= width && allocation.get_height() >= height) {
+ return;
+ }
+
+ // Compute largest sizes on both axis
+ width = std::max(width, allocation.get_width());
+ height = std::max(height, allocation.get_height());
+
+ // Compute new positions to keep window centered
+ pos_x = pos_x - (width - allocation.get_width()) / 2;
+ pos_y = pos_y - (height - allocation.get_height()) / 2;
+
+ // Keep window inside the screen
+ pos_x = std::max(pos_x, 0);
+ pos_y = std::max(pos_y, 0);
+
+ // Resize window
+ move(pos_x, pos_y);
+ resize(width, height);
+}
+
+// mimic InkscapeWindow handling of shortcuts to make them work with active floating dialog window
+bool DialogWindow::on_key_press_event(GdkEventKey *key_event)
+{
+ auto focus = get_focus();
+ if (focus) {
+ if (focus->event(reinterpret_cast<GdkEvent *>(key_event))) {
+ return true;
+ }
+ }
+
+ // Pass key event to this window or to app (via this window).
+ if (Gtk::Window::on_key_press_event(key_event)) {
+ return true;
+ }
+
+ // Pass key event to active InkscapeWindow to handle win (and app) level shortcuts.
+ if (auto win = _app->get_active_window(); win && win->on_key_press_event(key_event)) {
+ return true;
+ }
+
+ return 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/dialog/dialog-window.h b/src/ui/dialog/dialog-window.h
new file mode 100644
index 0000000..56d9247
--- /dev/null
+++ b/src/ui/dialog/dialog-window.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#ifndef INKSCAPE_UI_DIALOG_WINDOW_H
+#define INKSCAPE_UI_DIALOG_WINDOW_H
+
+/** @file
+ * @brief A window for floating docks.
+ *
+ * Authors: see git history
+ * Tavmjong Bah
+ *
+ * Copyright (c) 2018 Tavmjong Bah, Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/applicationwindow.h>
+
+#include "inkscape-application.h"
+
+using Gtk::Label;
+using Gtk::Widget;
+
+class InkscapeWindow;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class DialogContainer;
+class DialogMultipaned;
+
+/**
+ * DialogWindow holds DialogContainer instances for undocked dialogs.
+ *
+ * It watches the last active InkscapeWindow and updates its inner dialogs, if any.
+ */
+class DialogWindow : public Gtk::Window
+{
+public:
+ DialogWindow(InkscapeWindow* window, Gtk::Widget *page = nullptr);
+ ~DialogWindow() override;
+
+ void set_inkscape_window(InkscapeWindow *window);
+ InkscapeWindow* get_inkscape_window() { return _inkscape_window; }
+ void update_dialogs();
+ void update_window_size_to_fit_children();
+
+ // Getters
+ DialogContainer *get_container() { return _container; }
+
+private:
+ bool on_key_press_event(GdkEventKey* key_event) override;
+
+ InkscapeApplication *_app = nullptr;
+ InkscapeWindow *_inkscape_window = nullptr; // The Inkscape window that dialog window is attached to, changes when mouse moves into new Inkscape window.
+ DialogContainer *_container = nullptr;
+ Glib::ustring _title;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_WINDOW_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/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp
new file mode 100644
index 0000000..5bf5976
--- /dev/null
+++ b/src/ui/dialog/document-properties.cpp
@@ -0,0 +1,1863 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Document properties dialog, Gtkmm-style.
+ */
+/* 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)
+ * Diederik van Lierop <mail@diedenrezi.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2000 - 2008 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 <vector>
+
+#include "actions/actions-tools.h"
+#include "document-properties.h"
+#include "include/gtkmm_version.h"
+#include "io/sys.h"
+#include "object/color-profile.h"
+#include "object/sp-root.h"
+#include "object/sp-grid.h"
+#include "object/sp-script.h"
+#include "page-manager.h"
+#include "rdf.h"
+#include "style.h"
+#include "svg/svg-color.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/shape-editor.h"
+#include "ui/widget/alignment-selector.h"
+#include "ui/widget/entity-entry.h"
+#include "ui/widget/notebook-page.h"
+#include "ui/widget/page-properties.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+#define SPACE_SIZE_X 15
+#define SPACE_SIZE_Y 10
+
+static void docprops_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(*Gtk::manage(Glib::wrap(child)));
+ btn.set_relief(Gtk::RELIEF_NONE);
+}
+
+DocumentProperties::DocumentProperties()
+ : DialogBase("/dialogs/documentoptions", "DocumentProperties")
+ , _page_page(Gtk::manage(new UI::Widget::NotebookPage(1, 1, false, true)))
+ , _page_guides(Gtk::manage(new UI::Widget::NotebookPage(1, 1)))
+ , _page_cms(Gtk::manage(new UI::Widget::NotebookPage(1, 1)))
+ , _page_scripting(Gtk::manage(new UI::Widget::NotebookPage(1, 1)))
+ , _page_external_scripts(Gtk::manage(new UI::Widget::NotebookPage(1, 1)))
+ , _page_embedded_scripts(Gtk::manage(new UI::Widget::NotebookPage(1, 1)))
+ , _page_metadata1(Gtk::manage(new UI::Widget::NotebookPage(1, 1)))
+ , _page_metadata2(Gtk::manage(new UI::Widget::NotebookPage(1, 1)))
+ //---------------------------------------------------------------
+ // General guide options
+ , _rcb_sgui(_("Show _guides"), _("Show or hide guides"), "showguides", _wr)
+ , _rcb_lgui(_("Lock all guides"), _("Toggle lock of all guides in the document"), "inkscape:lockguides", _wr)
+ , _rcp_gui(_("Guide co_lor:"), _("Guideline color"), _("Color of guidelines"), "guidecolor", "guideopacity", _wr)
+ , _rcp_hgui(_("_Highlight color:"), _("Highlighted guideline color"),
+ _("Color of a guideline when it is under mouse"), "guidehicolor", "guidehiopacity", _wr)
+ , _create_guides_btn(_("Create guides around the current page"))
+ , _delete_guides_btn(_("Delete all guides"))
+ //---------------------------------------------------------------
+ , _grids_label_crea("", Gtk::ALIGN_START)
+ , _grids_button_new(C_("Grid", "_New"), _("Create new grid."))
+ , _grids_button_remove(C_("Grid", "_Remove"), _("Remove selected grid."))
+ , _grids_label_def("", Gtk::ALIGN_START)
+ , _grids_vbox(Gtk::ORIENTATION_VERTICAL)
+ , _grids_hbox_crea(Gtk::ORIENTATION_HORIZONTAL)
+ , _grids_space(Gtk::ORIENTATION_HORIZONTAL)
+ // Attach nodeobservers to this document
+ , _namedview_connection(this)
+ , _root_connection(this)
+{
+ set_spacing (0);
+ pack_start(_notebook, true, true);
+
+ _notebook.append_page(*_page_page, _("Display"));
+ _notebook.append_page(*_page_guides, _("Guides"));
+ _notebook.append_page(_grids_vbox, _("Grids"));
+ _notebook.append_page(*_page_cms, _("Color"));
+ _notebook.append_page(*_page_scripting, _("Scripting"));
+ _notebook.append_page(*_page_metadata1, _("Metadata"));
+ _notebook.append_page(*_page_metadata2, _("License"));
+
+ _wr.setUpdating (true);
+ build_page();
+ build_guides();
+ build_gridspage();
+ build_cms();
+ build_scripting();
+ build_metadata();
+ _wr.setUpdating (false);
+
+ _grids_button_new.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::onNewGrid));
+ _grids_button_remove.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::onRemoveGrid));
+
+ show_all_children();
+ _grids_button_remove.hide();
+}
+
+DocumentProperties::~DocumentProperties()
+{
+ for (auto &it : _rdflist)
+ delete it;
+}
+
+//========================================================================
+
+/**
+ * Helper function that sets widgets in a 2 by n table.
+ * arr has two entries per table row. Each row is in the following form:
+ * widget, widget -> function adds a widget in each column.
+ * nullptr, widget -> function adds a widget that occupies the row.
+ * label, nullptr -> function adds label that occupies the row.
+ * nullptr, nullptr -> function adds an empty box that occupies the row.
+ * This used to be a helper function for a 3 by n table
+ */
+void attach_all(Gtk::Grid &table, Gtk::Widget *const arr[], unsigned const n)
+{
+ for (unsigned i = 0, r = 0; i < n; i += 2) {
+ if (arr[i] && arr[i+1]) {
+ arr[i]->set_hexpand();
+ arr[i+1]->set_hexpand();
+ arr[i]->set_valign(Gtk::ALIGN_CENTER);
+ arr[i+1]->set_valign(Gtk::ALIGN_CENTER);
+ table.attach(*arr[i], 0, r, 1, 1);
+ table.attach(*arr[i+1], 1, r, 1, 1);
+ } else {
+ if (arr[i+1]) {
+ Gtk::AttachOptions yoptions = (Gtk::AttachOptions)0;
+ arr[i+1]->set_hexpand();
+
+ if (yoptions & Gtk::EXPAND)
+ arr[i+1]->set_vexpand();
+ else
+ arr[i+1]->set_valign(Gtk::ALIGN_CENTER);
+
+ table.attach(*arr[i+1], 0, r, 2, 1);
+ } else if (arr[i]) {
+ Gtk::Label& label = reinterpret_cast<Gtk::Label&>(*arr[i]);
+
+ label.set_hexpand();
+ label.set_halign(Gtk::ALIGN_START);
+ label.set_valign(Gtk::ALIGN_CENTER);
+ table.attach(label, 0, r, 2, 1);
+ } else {
+ auto space = Gtk::manage (new Gtk::Box);
+ space->set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y);
+
+ space->set_halign(Gtk::ALIGN_CENTER);
+ space->set_valign(Gtk::ALIGN_CENTER);
+ table.attach(*space, 0, r, 1, 1);
+ }
+ }
+ ++r;
+ }
+}
+
+void set_namedview_bool(SPDesktop* desktop, const Glib::ustring& operation, SPAttr key, bool on) {
+ if (!desktop || !desktop->getDocument()) return;
+
+ desktop->getNamedView()->change_bool_setting(key, on);
+
+ desktop->getDocument()->setModifiedSinceSave();
+ DocumentUndo::done(desktop->getDocument(), operation, "");
+}
+
+void set_color(SPDesktop* desktop, Glib::ustring operation, unsigned int rgba, SPAttr color_key, SPAttr opacity_key = SPAttr::INVALID) {
+ if (!desktop || !desktop->getDocument()) return;
+
+ desktop->getNamedView()->change_color(rgba, color_key, opacity_key);
+
+ desktop->getDocument()->setModifiedSinceSave();
+ DocumentUndo::maybeDone(desktop->getDocument(), ("document-color-" + operation).c_str(), operation, "");
+}
+
+void set_document_dimensions(SPDesktop* desktop, double width, double height, const Inkscape::Util::Unit* unit) {
+ if (!desktop) return;
+
+ Inkscape::Util::Quantity width_quantity = Inkscape::Util::Quantity(width, unit);
+ Inkscape::Util::Quantity height_quantity = Inkscape::Util::Quantity(height, unit);
+ SPDocument* doc = desktop->getDocument();
+ Inkscape::Util::Quantity const old_height = doc->getHeight();
+ auto rect = Geom::Rect(Geom::Point(0, 0), Geom::Point(width_quantity.value("px"), height_quantity.value("px")));
+ doc->fitToRect(rect, false);
+
+ // The origin for the user is in the lower left corner; this point should remain stationary when
+ // changing the page size. The SVG's origin however is in the upper left corner, so we must compensate for this
+ if (!doc->is_yaxisdown()) {
+ Geom::Translate const vert_offset(Geom::Point(0, (old_height.value("px") - height_quantity.value("px"))));
+ doc->getRoot()->translateChildItems(vert_offset);
+ }
+ // units: this is most likely not needed, units are part of document size attributes
+ // if (unit) {
+ // set_namedview_value(desktop, "", SPAttr::UNITS)
+ // write_str_to_xml(desktop, _("Set document unit"), "unit", unit->abbr.c_str());
+ // }
+ doc->setWidthAndHeight(width_quantity, height_quantity, true);
+
+ DocumentUndo::done(doc, _("Set page size"), "");
+}
+
+void DocumentProperties::set_viewbox_pos(SPDesktop* desktop, double x, double y) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ auto box = document->getViewBox();
+ document->setViewBox(Geom::Rect::from_xywh(x, y, box.width(), box.height()));
+ DocumentUndo::done(document, _("Set viewbox position"), "");
+ update_scale_ui(desktop);
+}
+
+void DocumentProperties::set_viewbox_size(SPDesktop* desktop, double width, double height) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ auto box = document->getViewBox();
+ document->setViewBox(Geom::Rect::from_xywh(box.min()[Geom::X], box.min()[Geom::Y], width, height));
+ DocumentUndo::done(document, _("Set viewbox size"), "");
+ update_scale_ui(desktop);
+}
+
+// helper function to set document scale; uses magnitude of document width/height only, not computed (pixel) values
+void set_document_scale_helper(SPDocument& document, double scale) {
+ if (scale <= 0) return;
+
+ auto root = document.getRoot();
+ auto box = document.getViewBox();
+ document.setViewBox(Geom::Rect::from_xywh(
+ box.min()[Geom::X], box.min()[Geom::Y],
+ root->width.value / scale, root->height.value / scale)
+ );
+}
+
+void DocumentProperties::set_document_scale(SPDesktop* desktop, double scale) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ if (scale > 0) {
+ set_document_scale_helper(*document, scale);
+ update_viewbox_ui(desktop);
+ update_scale_ui(desktop);
+ DocumentUndo::done(document, _("Set page scale"), "");
+ }
+}
+
+// document scale as a ratio of document size and viewbox size
+// as described in Wiki: https://wiki.inkscape.org/wiki/index.php/Units_In_Inkscape
+// for example: <svg width="100mm" height="100mm" viewBox="0 0 100 100"> will report 1:1 scale
+std::optional<Geom::Scale> get_document_scale_helper(SPDocument& doc) {
+ auto root = doc.getRoot();
+ if (root &&
+ root->width._set && root->width.unit != SVGLength::PERCENT &&
+ root->height._set && root->height.unit != SVGLength::PERCENT) {
+ if (root->viewBox_set) {
+ // viewbox and document size present
+ auto vw = root->viewBox.width();
+ auto vh = root->viewBox.height();
+ if (vw > 0 && vh > 0) {
+ return Geom::Scale(root->width.value / vw, root->height.value / vh);
+ }
+ } else {
+ // no viewbox, use SVG size in pixels
+ auto w = root->width.computed;
+ auto h = root->height.computed;
+ if (w > 0 && h > 0) {
+ return Geom::Scale(root->width.value / w, root->height.value / h);
+ }
+ }
+ }
+
+ // there is no scale concept applicable in the current state
+ return std::optional<Geom::Scale>();
+}
+
+void DocumentProperties::update_scale_ui(SPDesktop* desktop) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ using UI::Widget::PageProperties;
+ if (auto scale = get_document_scale_helper(*document)) {
+ auto sx = (*scale)[Geom::X];
+ auto sy = (*scale)[Geom::Y];
+ double eps = 0.0001; // TODO: tweak this value
+ bool uniform = fabs(sx - sy) < eps;
+ _page->set_dimension(PageProperties::Dimension::Scale, sx, sx); // only report one, only one "scale" is used
+ _page->set_check(PageProperties::Check::NonuniformScale, !uniform);
+ _page->set_check(PageProperties::Check::DisabledScale, false);
+ } else {
+ // no scale
+ _page->set_dimension(PageProperties::Dimension::Scale, 1, 1);
+ _page->set_check(PageProperties::Check::NonuniformScale, false);
+ _page->set_check(PageProperties::Check::DisabledScale, true);
+ }
+}
+
+void DocumentProperties::update_viewbox_ui(SPDesktop* desktop) {
+ if (!desktop) return;
+
+ auto document = desktop->getDocument();
+ if (!document) return;
+
+ using UI::Widget::PageProperties;
+ Geom::Rect viewBox = document->getViewBox();
+ _page->set_dimension(PageProperties::Dimension::ViewboxPosition, viewBox.min()[Geom::X], viewBox.min()[Geom::Y]);
+ _page->set_dimension(PageProperties::Dimension::ViewboxSize, viewBox.width(), viewBox.height());
+}
+
+void DocumentProperties::build_page()
+{
+ using UI::Widget::PageProperties;
+ _page = Gtk::manage(PageProperties::create());
+ _page_page->table().attach(*_page, 0, 0);
+ _page_page->show();
+
+ _page->signal_color_changed().connect([=](unsigned int color, PageProperties::Color element){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ _wr.setUpdating(true);
+ switch (element) {
+ case PageProperties::Color::Desk:
+ set_color(_wr.desktop(), _("Desk color"), color, SPAttr::INKSCAPE_DESK_COLOR);
+ break;
+ case PageProperties::Color::Background:
+ set_color(_wr.desktop(), _("Background color"), color, SPAttr::PAGECOLOR);
+ break;
+ case PageProperties::Color::Border:
+ set_color(_wr.desktop(), _("Border color"), color, SPAttr::BORDERCOLOR, SPAttr::BORDEROPACITY);
+ break;
+ }
+ _wr.setUpdating(false);
+ });
+
+ _page->signal_dimmension_changed().connect([=](double x, double y, const Inkscape::Util::Unit* unit, PageProperties::Dimension element){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ _wr.setUpdating(true);
+ switch (element) {
+ case PageProperties::Dimension::PageTemplate:
+ case PageProperties::Dimension::PageSize:
+ set_document_dimensions(_wr.desktop(), x, y, unit);
+ update_viewbox(_wr.desktop());
+ break;
+
+ case PageProperties::Dimension::ViewboxSize:
+ set_viewbox_size(_wr.desktop(), x, y);
+ break;
+
+ case PageProperties::Dimension::ViewboxPosition:
+ set_viewbox_pos(_wr.desktop(), x, y);
+ break;
+
+ case PageProperties::Dimension::Scale:
+ set_document_scale(_wr.desktop(), x); // only uniform scale; there's no 'y' in the dialog
+ }
+ _wr.setUpdating(false);
+ });
+
+ _page->signal_check_toggled().connect([=](bool checked, PageProperties::Check element){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ _wr.setUpdating(true);
+ switch (element) {
+ case PageProperties::Check::Checkerboard:
+ set_namedview_bool(_wr.desktop(), _("Toggle checkerboard"), SPAttr::INKSCAPE_DESK_CHECKERBOARD, checked);
+ break;
+ case PageProperties::Check::Border:
+ set_namedview_bool(_wr.desktop(), _("Toggle page border"), SPAttr::SHOWBORDER, checked);
+ break;
+ case PageProperties::Check::BorderOnTop:
+ set_namedview_bool(_wr.desktop(), _("Toggle border on top"), SPAttr::BORDERLAYER, checked);
+ break;
+ case PageProperties::Check::Shadow:
+ set_namedview_bool(_wr.desktop(), _("Toggle page shadow"), SPAttr::SHOWPAGESHADOW, checked);
+ break;
+ case PageProperties::Check::AntiAlias:
+ set_namedview_bool(_wr.desktop(), _("Toggle anti-aliasing"), SPAttr::SHAPE_RENDERING, checked);
+ break;
+ case PageProperties::Check::ClipToPage:
+ set_namedview_bool(_wr.desktop(), _("Toggle clip to page mode"), SPAttr::INKSCAPE_CLIP_TO_PAGE_RENDERING, checked);
+ break;
+ case PageProperties::Check::PageLabelStyle:
+ set_namedview_bool(_wr.desktop(), _("Toggle page label style"), SPAttr::PAGELABELSTYLE, checked);
+ }
+ _wr.setUpdating(false);
+ });
+
+ _page->signal_unit_changed().connect([=](const Inkscape::Util::Unit* unit, PageProperties::Units element){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ if (element == PageProperties::Units::Display) {
+ // display only units
+ display_unit_change(unit);
+ }
+ else if (element == PageProperties::Units::Document) {
+ // not used, fired with page size
+ }
+ });
+
+ _page->signal_resize_to_fit().connect([=](){
+ if (_wr.isUpdating() || !_wr.desktop()) return;
+
+ if (auto document = getDocument()) {
+ auto &page_manager = document->getPageManager();
+ page_manager.selectPage(0);
+ // fit page to selection or content, if there's no selection
+ page_manager.fitToSelection(_wr.desktop()->getSelection());
+ DocumentUndo::done(document, _("Resize page to fit"), INKSCAPE_ICON("tool-pages"));
+ update_widgets();
+ }
+ });
+}
+
+void DocumentProperties::build_guides()
+{
+ _page_guides->show();
+
+ Gtk::Label *label_gui = Gtk::manage (new Gtk::Label);
+ label_gui->set_markup (_("<b>Guides</b>"));
+
+ _rcp_gui.set_margin_start(0);
+ _rcp_hgui.set_margin_start(0);
+ _rcp_gui.set_hexpand();
+ _rcp_hgui.set_hexpand();
+ _rcb_sgui.set_hexpand();
+ auto inner = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4));
+ inner->add(_rcb_sgui);
+ inner->add(_rcb_lgui);
+ inner->add(_rcp_gui);
+ inner->add(_rcp_hgui);
+ auto spacer = Gtk::manage(new Gtk::Label());
+ Gtk::Widget *const widget_array[] =
+ {
+ label_gui, nullptr,
+ inner, spacer,
+ nullptr, nullptr,
+ nullptr, &_create_guides_btn,
+ nullptr, &_delete_guides_btn
+ };
+ attach_all(_page_guides->table(), widget_array, G_N_ELEMENTS(widget_array));
+ inner->set_hexpand(false);
+
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(_create_guides_btn.gobj()), "doc.create-guides-around-page");
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(_delete_guides_btn.gobj()), "doc.delete-all-guides");
+}
+
+/// Populates the available color profiles combo box
+void DocumentProperties::populate_available_profiles(){
+ _AvailableProfilesListStore->clear(); // Clear any existing items in the combo box
+
+ // Iterate through the list of profiles and add the name to the combo box.
+ bool home = true; // initial value doesn't matter, it's just to avoid a compiler warning
+ bool first = true;
+ for (auto &profile: ColorProfile::getProfileFilesWithNames()) {
+ Gtk::TreeModel::Row row;
+
+ // add a separator between profiles from the user's home directory and system profiles
+ if (!first && profile.isInHome != home)
+ {
+ row = *(_AvailableProfilesListStore->append());
+ row[_AvailableProfilesListColumns.fileColumn] = "<separator>";
+ row[_AvailableProfilesListColumns.nameColumn] = "<separator>";
+ row[_AvailableProfilesListColumns.separatorColumn] = true;
+ }
+ home = profile.isInHome;
+ first = false;
+
+ row = *(_AvailableProfilesListStore->append());
+ row[_AvailableProfilesListColumns.fileColumn] = profile.filename;
+ row[_AvailableProfilesListColumns.nameColumn] = profile.name;
+ row[_AvailableProfilesListColumns.separatorColumn] = false;
+ }
+}
+
+
+/// Links the selected color profile in the combo box to the document
+void DocumentProperties::linkSelectedProfile()
+{
+ //store this profile in the SVG document (create <color-profile> element in the XML)
+ if (auto document = getDocument()){
+ // Find the index of the currently-selected row in the color profiles combobox
+ Gtk::TreeModel::iterator iter = _AvailableProfilesList.get_active();
+ if (!iter)
+ return;
+
+ // Read the filename and description from the list of available profiles
+ Glib::ustring file = (*iter)[_AvailableProfilesListColumns.fileColumn];
+ Glib::ustring name = (*iter)[_AvailableProfilesListColumns.nameColumn];
+
+ std::vector<SPObject *> current = document->getResourceList( "iccprofile" );
+ for (auto obj : current) {
+ Inkscape::ColorProfile* prof = reinterpret_cast<Inkscape::ColorProfile*>(obj);
+ if (!strcmp(prof->href, file.c_str()))
+ return;
+ }
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *cprofRepr = xml_doc->createElement("svg:color-profile");
+ gchar* tmp = g_strdup(name.c_str());
+ std::string nameStr = tmp ? tmp : "profile"; // TODO add some auto-numbering to avoid collisions
+ ColorProfile::sanitizeName(nameStr);
+ cprofRepr->setAttribute("name", nameStr);
+ cprofRepr->setAttribute("xlink:href", Glib::filename_to_uri(Glib::filename_from_utf8(file)));
+ cprofRepr->setAttribute("id", file);
+
+
+ // Checks whether there is a defs element. Creates it when needed
+ Inkscape::XML::Node *defsRepr = sp_repr_lookup_name(xml_doc, "svg:defs");
+ if (!defsRepr) {
+ defsRepr = xml_doc->createElement("svg:defs");
+ xml_doc->root()->addChild(defsRepr, nullptr);
+ }
+
+ g_assert(document->getDefs());
+ defsRepr->addChild(cprofRepr, nullptr);
+
+ // TODO check if this next line was sometimes needed. It being there caused an assertion.
+ //Inkscape::GC::release(defsRepr);
+
+ // inform the document, so we can undo
+ DocumentUndo::done(document, _("Link Color Profile"), "");
+
+ populate_linked_profiles_box();
+ }
+}
+
+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 DocumentProperties::populate_linked_profiles_box()
+{
+ _LinkedProfilesListStore->clear();
+ if (auto document = getDocument()) {
+ std::vector<SPObject *> current = document->getResourceList( "iccprofile" );
+ if (! current.empty()) {
+ _emb_profiles_observer.set((*(current.begin()))->parent);
+ }
+
+ std::set<Inkscape::ColorProfile *> _current;
+ std::transform(current.begin(),
+ current.end(),
+ std::inserter(_current, _current.begin()),
+ static_caster<SPObject, Inkscape::ColorProfile>());
+
+ for (auto &profile: _current) {
+ Gtk::TreeModel::Row row = *(_LinkedProfilesListStore->append());
+ row[_LinkedProfilesListColumns.nameColumn] = profile->name;
+ // row[_LinkedProfilesListColumns.previewColumn] = "Color Preview";
+ }
+ }
+}
+
+void DocumentProperties::external_scripts_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _ExternalScriptsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void DocumentProperties::embedded_scripts_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _EmbeddedScriptsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void DocumentProperties::linked_profiles_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _EmbProfContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void DocumentProperties::cms_create_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem)
+{
+ Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ _EmbProfContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ _EmbProfContextMenu.accelerate(parent);
+}
+
+
+void DocumentProperties::external_create_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem)
+{
+ Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ _ExternalScriptsContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ _ExternalScriptsContextMenu.accelerate(parent);
+}
+
+void DocumentProperties::embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem)
+{
+ Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ _EmbeddedScriptsContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ _EmbeddedScriptsContextMenu.accelerate(parent);
+}
+
+void DocumentProperties::onColorProfileSelectRow()
+{
+ Glib::RefPtr<Gtk::TreeSelection> sel = _LinkedProfilesList.get_selection();
+ if (sel) {
+ _unlink_btn.set_sensitive(sel->count_selected_rows () > 0);
+ }
+}
+
+
+void DocumentProperties::removeSelectedProfile(){
+ Glib::ustring name;
+ if(_LinkedProfilesList.get_selection()) {
+ Gtk::TreeModel::iterator i = _LinkedProfilesList.get_selection()->get_selected();
+
+ if(i){
+ name = (*i)[_LinkedProfilesListColumns.nameColumn];
+ } else {
+ return;
+ }
+ }
+ if (auto document = getDocument()) {
+ std::vector<SPObject *> current = document->getResourceList( "iccprofile" );
+ for (auto obj : current) {
+ Inkscape::ColorProfile* prof = reinterpret_cast<Inkscape::ColorProfile*>(obj);
+ if (!name.compare(prof->name)){
+ prof->deleteObject(true, false);
+ DocumentUndo::done(document, _("Remove linked color profile"), "");
+ break; // removing the color profile likely invalidates part of the traversed list, stop traversing here.
+ }
+ }
+ }
+
+ populate_linked_profiles_box();
+ onColorProfileSelectRow();
+}
+
+bool DocumentProperties::_AvailableProfilesList_separator(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter)
+{
+ bool separator = (*iter)[_AvailableProfilesListColumns.separatorColumn];
+ return separator;
+}
+
+void DocumentProperties::build_cms()
+{
+ _page_cms->show();
+ Gtk::Label *label_link= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START));
+ label_link->set_markup (_("<b>Linked Color Profiles:</b>"));
+ Gtk::Label *label_avail = Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START));
+ label_avail->set_markup (_("<b>Available Color Profiles:</b>"));
+
+ _unlink_btn.set_tooltip_text(_("Unlink Profile"));
+ docprops_style_button(_unlink_btn, INKSCAPE_ICON("list-remove"));
+
+ gint row = 0;
+
+ label_link->set_hexpand();
+ label_link->set_halign(Gtk::ALIGN_START);
+ label_link->set_valign(Gtk::ALIGN_CENTER);
+ _page_cms->table().attach(*label_link, 0, row, 3, 1);
+
+ row++;
+
+ _LinkedProfilesListScroller.set_hexpand();
+ _LinkedProfilesListScroller.set_valign(Gtk::ALIGN_CENTER);
+ _page_cms->table().attach(_LinkedProfilesListScroller, 0, row, 3, 1);
+
+ row++;
+
+ Gtk::Box* spacer = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ spacer->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y);
+
+ spacer->set_hexpand();
+ spacer->set_valign(Gtk::ALIGN_CENTER);
+ _page_cms->table().attach(*spacer, 0, row, 3, 1);
+
+ row++;
+
+ label_avail->set_hexpand();
+ label_avail->set_halign(Gtk::ALIGN_START);
+ label_avail->set_valign(Gtk::ALIGN_CENTER);
+ _page_cms->table().attach(*label_avail, 0, row, 3, 1);
+
+ row++;
+
+ _AvailableProfilesList.set_hexpand();
+ _AvailableProfilesList.set_valign(Gtk::ALIGN_CENTER);
+ _page_cms->table().attach(_AvailableProfilesList, 0, row, 1, 1);
+
+ _unlink_btn.set_halign(Gtk::ALIGN_CENTER);
+ _unlink_btn.set_valign(Gtk::ALIGN_CENTER);
+ _page_cms->table().attach(_unlink_btn, 2, row, 1, 1);
+
+ // Set up the Available Profiles combo box
+ _AvailableProfilesListStore = Gtk::ListStore::create(_AvailableProfilesListColumns);
+ _AvailableProfilesList.set_model(_AvailableProfilesListStore);
+ _AvailableProfilesList.pack_start(_AvailableProfilesListColumns.nameColumn);
+ _AvailableProfilesList.set_row_separator_func(sigc::mem_fun(*this, &DocumentProperties::_AvailableProfilesList_separator));
+ _AvailableProfilesList.signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::linkSelectedProfile) );
+
+ populate_available_profiles();
+
+ //# Set up the Linked Profiles combo box
+ _LinkedProfilesListStore = Gtk::ListStore::create(_LinkedProfilesListColumns);
+ _LinkedProfilesList.set_model(_LinkedProfilesListStore);
+ _LinkedProfilesList.append_column(_("Profile Name"), _LinkedProfilesListColumns.nameColumn);
+// _LinkedProfilesList.append_column(_("Color Preview"), _LinkedProfilesListColumns.previewColumn);
+ _LinkedProfilesList.set_headers_visible(false);
+// TODO restore? _LinkedProfilesList.set_fixed_height_mode(true);
+
+ populate_linked_profiles_box();
+
+ _LinkedProfilesListScroller.add(_LinkedProfilesList);
+ _LinkedProfilesListScroller.set_shadow_type(Gtk::SHADOW_IN);
+ _LinkedProfilesListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _LinkedProfilesListScroller.set_size_request(-1, 90);
+
+ _unlink_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile));
+
+ _LinkedProfilesList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onColorProfileSelectRow) );
+
+ _LinkedProfilesList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::linked_profiles_list_button_release));
+ cms_create_popup_menu(_LinkedProfilesList, sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile));
+
+ if (auto document = getDocument()) {
+ std::vector<SPObject *> current = document->getResourceList( "defs" );
+ if (!current.empty()) {
+ _emb_profiles_observer.set((*(current.begin()))->parent);
+ }
+ _emb_profiles_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_linked_profiles_box));
+ onColorProfileSelectRow();
+ }
+}
+
+void DocumentProperties::build_scripting()
+{
+ _page_scripting->show();
+
+ _page_scripting->table().attach(_scripting_notebook, 0, 0, 1, 1);
+
+ _scripting_notebook.append_page(*_page_external_scripts, _("External scripts"));
+ _scripting_notebook.append_page(*_page_embedded_scripts, _("Embedded scripts"));
+
+ //# External scripts tab
+ _page_external_scripts->show();
+ Gtk::Label *label_external= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START));
+ label_external->set_markup (_("<b>External script files:</b>"));
+
+ _external_add_btn.set_tooltip_text(_("Add the current file name or browse for a file"));
+ docprops_style_button(_external_add_btn, INKSCAPE_ICON("list-add"));
+
+ _external_remove_btn.set_tooltip_text(_("Remove"));
+ docprops_style_button(_external_remove_btn, INKSCAPE_ICON("list-remove"));
+
+ gint row = 0;
+
+ label_external->set_hexpand();
+ label_external->set_halign(Gtk::ALIGN_START);
+ label_external->set_valign(Gtk::ALIGN_CENTER);
+ _page_external_scripts->table().attach(*label_external, 0, row, 3, 1);
+
+ row++;
+
+ _ExternalScriptsListScroller.set_hexpand();
+ _ExternalScriptsListScroller.set_valign(Gtk::ALIGN_CENTER);
+ _page_external_scripts->table().attach(_ExternalScriptsListScroller, 0, row, 3, 1);
+
+ row++;
+
+ Gtk::Box* spacer_external = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ spacer_external->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y);
+
+ spacer_external->set_hexpand();
+ spacer_external->set_valign(Gtk::ALIGN_CENTER);
+ _page_external_scripts->table().attach(*spacer_external, 0, row, 3, 1);
+
+ row++;
+
+ _script_entry.set_hexpand();
+ _script_entry.set_valign(Gtk::ALIGN_CENTER);
+ _page_external_scripts->table().attach(_script_entry, 0, row, 1, 1);
+
+ _external_add_btn.set_halign(Gtk::ALIGN_CENTER);
+ _external_add_btn.set_valign(Gtk::ALIGN_CENTER);
+ _external_add_btn.set_margin_start(2);
+ _external_add_btn.set_margin_end(2);
+
+ _page_external_scripts->table().attach(_external_add_btn, 1, row, 1, 1);
+
+ _external_remove_btn.set_halign(Gtk::ALIGN_CENTER);
+ _external_remove_btn.set_valign(Gtk::ALIGN_CENTER);
+ _page_external_scripts->table().attach(_external_remove_btn, 2, row, 1, 1);
+
+ //# Set up the External Scripts box
+ _ExternalScriptsListStore = Gtk::ListStore::create(_ExternalScriptsListColumns);
+ _ExternalScriptsList.set_model(_ExternalScriptsListStore);
+ _ExternalScriptsList.append_column(_("Filename"), _ExternalScriptsListColumns.filenameColumn);
+ _ExternalScriptsList.set_headers_visible(true);
+// TODO restore? _ExternalScriptsList.set_fixed_height_mode(true);
+
+
+ //# Embedded scripts tab
+ _page_embedded_scripts->show();
+ Gtk::Label *label_embedded= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START));
+ label_embedded->set_markup (_("<b>Embedded script files:</b>"));
+
+ _embed_new_btn.set_tooltip_text(_("New"));
+ docprops_style_button(_embed_new_btn, INKSCAPE_ICON("list-add"));
+
+ _embed_remove_btn.set_tooltip_text(_("Remove"));
+ docprops_style_button(_embed_remove_btn, INKSCAPE_ICON("list-remove"));
+
+ _embed_button_box.set_layout (Gtk::BUTTONBOX_START);
+ _embed_button_box.add(_embed_new_btn);
+ _embed_button_box.add(_embed_remove_btn);
+
+ row = 0;
+
+ label_embedded->set_hexpand();
+ label_embedded->set_halign(Gtk::ALIGN_START);
+ label_embedded->set_valign(Gtk::ALIGN_CENTER);
+ _page_embedded_scripts->table().attach(*label_embedded, 0, row, 3, 1);
+
+ row++;
+
+ _EmbeddedScriptsListScroller.set_hexpand();
+ _EmbeddedScriptsListScroller.set_valign(Gtk::ALIGN_CENTER);
+ _page_embedded_scripts->table().attach(_EmbeddedScriptsListScroller, 0, row, 3, 1);
+
+ row++;
+
+ _embed_button_box.set_hexpand();
+ _embed_button_box.set_valign(Gtk::ALIGN_CENTER);
+ _page_embedded_scripts->table().attach(_embed_button_box, 0, row, 1, 1);
+
+ row++;
+
+ Gtk::Box* spacer_embedded = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ spacer_embedded->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y);
+ spacer_embedded->set_hexpand();
+ spacer_embedded->set_valign(Gtk::ALIGN_CENTER);
+ _page_embedded_scripts->table().attach(*spacer_embedded, 0, row, 3, 1);
+
+ row++;
+
+ //# Set up the Embedded Scripts box
+ _EmbeddedScriptsListStore = Gtk::ListStore::create(_EmbeddedScriptsListColumns);
+ _EmbeddedScriptsList.set_model(_EmbeddedScriptsListStore);
+ _EmbeddedScriptsList.append_column(_("Script ID"), _EmbeddedScriptsListColumns.idColumn);
+ _EmbeddedScriptsList.set_headers_visible(true);
+// TODO restore? _EmbeddedScriptsList.set_fixed_height_mode(true);
+
+ //# Set up the Embedded Scripts content box
+ Gtk::Label *label_embedded_content= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START));
+ label_embedded_content->set_markup (_("<b>Content:</b>"));
+
+ label_embedded_content->set_hexpand();
+ label_embedded_content->set_halign(Gtk::ALIGN_START);
+ label_embedded_content->set_valign(Gtk::ALIGN_CENTER);
+ _page_embedded_scripts->table().attach(*label_embedded_content, 0, row, 3, 1);
+
+ row++;
+
+ _EmbeddedContentScroller.set_hexpand();
+ _EmbeddedContentScroller.set_valign(Gtk::ALIGN_CENTER);
+ _page_embedded_scripts->table().attach(_EmbeddedContentScroller, 0, row, 3, 1);
+
+ _EmbeddedContentScroller.add(_EmbeddedContent);
+ _EmbeddedContentScroller.set_shadow_type(Gtk::SHADOW_IN);
+ _EmbeddedContentScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _EmbeddedContentScroller.set_size_request(-1, 140);
+
+ _EmbeddedScriptsList.signal_cursor_changed().connect(sigc::mem_fun(*this, &DocumentProperties::changeEmbeddedScript));
+ _EmbeddedScriptsList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onEmbeddedScriptSelectRow) );
+
+ _ExternalScriptsList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onExternalScriptSelectRow) );
+
+ _EmbeddedContent.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::editEmbeddedScript));
+
+ populate_script_lists();
+
+ _ExternalScriptsListScroller.add(_ExternalScriptsList);
+ _ExternalScriptsListScroller.set_shadow_type(Gtk::SHADOW_IN);
+ _ExternalScriptsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _ExternalScriptsListScroller.set_size_request(-1, 90);
+
+ _external_add_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::addExternalScript));
+
+ _EmbeddedScriptsListScroller.add(_EmbeddedScriptsList);
+ _EmbeddedScriptsListScroller.set_shadow_type(Gtk::SHADOW_IN);
+ _EmbeddedScriptsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _EmbeddedScriptsListScroller.set_size_request(-1, 90);
+
+ _embed_new_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::addEmbeddedScript));
+
+ _external_remove_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeExternalScript));
+ _embed_remove_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeEmbeddedScript));
+
+ _ExternalScriptsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::external_scripts_list_button_release));
+ external_create_popup_menu(_ExternalScriptsList, sigc::mem_fun(*this, &DocumentProperties::removeExternalScript));
+
+ _EmbeddedScriptsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::embedded_scripts_list_button_release));
+ embedded_create_popup_menu(_EmbeddedScriptsList, sigc::mem_fun(*this, &DocumentProperties::removeEmbeddedScript));
+
+//TODO: review this observers code:
+ if (auto document = getDocument()) {
+ std::vector<SPObject *> current = document->getResourceList( "script" );
+ if (! current.empty()) {
+ _scripts_observer.set((*(current.begin()))->parent);
+ }
+ _scripts_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_script_lists));
+ onEmbeddedScriptSelectRow();
+ onExternalScriptSelectRow();
+ }
+}
+
+void DocumentProperties::build_metadata()
+{
+ using Inkscape::UI::Widget::EntityEntry;
+
+ _page_metadata1->show();
+
+ Gtk::Label *label = Gtk::manage (new Gtk::Label);
+ label->set_markup (_("<b>Dublin Core Entities</b>"));
+ label->set_halign(Gtk::ALIGN_START);
+ label->set_valign(Gtk::ALIGN_CENTER);
+ _page_metadata1->table().attach (*label, 0,0,2,1);
+
+ /* add generic metadata entry areas */
+ struct rdf_work_entity_t * entity;
+ int row = 1;
+ for (entity = rdf_work_entities; entity && entity->name; entity++, row++) {
+ if ( entity->editable == RDF_EDIT_GENERIC ) {
+ EntityEntry *w = EntityEntry::create (entity, _wr);
+ _rdflist.push_back(w);
+
+ w->_label.set_halign(Gtk::ALIGN_START);
+ w->_label.set_valign(Gtk::ALIGN_CENTER);
+ _page_metadata1->table().attach(w->_label, 0, row, 1, 1);
+
+ w->_packable->set_hexpand();
+ w->_packable->set_valign(Gtk::ALIGN_CENTER);
+ _page_metadata1->table().attach(*w->_packable, 1, row, 1, 1);
+ }
+ }
+
+ Gtk::Button *button_save = Gtk::manage (new Gtk::Button(_("_Save as default"),true));
+ button_save->set_tooltip_text(_("Save this metadata as the default metadata"));
+ Gtk::Button *button_load = Gtk::manage (new Gtk::Button(_("Use _default"),true));
+ button_load->set_tooltip_text(_("Use the previously saved default metadata here"));
+
+ auto box_buttons = Gtk::manage (new Gtk::ButtonBox);
+
+ box_buttons->set_layout(Gtk::BUTTONBOX_END);
+ box_buttons->set_spacing(4);
+ box_buttons->pack_start(*button_save, true, true, 6);
+ box_buttons->pack_start(*button_load, true, true, 6);
+ _page_metadata1->pack_end(*box_buttons, false, false, 0);
+
+ button_save->signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::save_default_metadata));
+ button_load->signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::load_default_metadata));
+
+ _page_metadata2->show();
+
+ row = 0;
+ Gtk::Label *llabel = Gtk::manage (new Gtk::Label);
+ llabel->set_markup (_("<b>License</b>"));
+ llabel->set_halign(Gtk::ALIGN_START);
+ llabel->set_valign(Gtk::ALIGN_CENTER);
+ _page_metadata2->table().attach(*llabel, 0, row, 2, 1);
+
+ /* add license selector pull-down and URI */
+ ++row;
+ _licensor.init (_wr);
+
+ _licensor.set_hexpand();
+ _licensor.set_valign(Gtk::ALIGN_CENTER);
+ _page_metadata2->table().attach(_licensor, 0, row, 2, 1);
+}
+
+void DocumentProperties::addExternalScript(){
+
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ if (_script_entry.get_text().empty() ) {
+ // Click Add button with no filename, show a Browse dialog
+ browseExternalScript();
+ }
+
+ if (!_script_entry.get_text().empty()) {
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *scriptRepr = xml_doc->createElement("svg:script");
+ scriptRepr->setAttributeOrRemoveIfEmpty("xlink:href", _script_entry.get_text());
+ _script_entry.set_text("");
+
+ xml_doc->root()->addChild(scriptRepr, nullptr);
+
+ // inform the document, so we can undo
+ DocumentUndo::done(document, _("Add external script..."), "");
+
+ populate_script_lists();
+ }
+}
+
+static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr;
+
+void DocumentProperties::browseExternalScript() {
+
+ //# 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 = "";
+
+ //# 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 = getDesktop();
+ if (desktop && !selectPrefsFileInstance) {
+ selectPrefsFileInstance =
+ Inkscape::UI::Dialog::FileOpenDialog::create(
+ *desktop->getToplevel(),
+ open_path,
+ Inkscape::UI::Dialog::CUSTOM_TYPE,
+ _("Select a script to load"));
+ selectPrefsFileInstance->addFilterMenu("Javascript Files", "*.js");
+ }
+
+ //# Show the dialog
+ bool const success = selectPrefsFileInstance->show();
+
+ if (!success) {
+ return;
+ }
+
+ //# User selected something. Get name and type
+ Glib::ustring fileName = selectPrefsFileInstance->getFilename();
+
+ _script_entry.set_text(fileName);
+}
+
+void DocumentProperties::addEmbeddedScript(){
+ if(auto document = getDocument()) {
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *scriptRepr = xml_doc->createElement("svg:script");
+
+ xml_doc->root()->addChild(scriptRepr, nullptr);
+
+ // inform the document, so we can undo
+ DocumentUndo::done(document, _("Add embedded script..."), "");
+ populate_script_lists();
+ }
+}
+
+void DocumentProperties::removeExternalScript(){
+ Glib::ustring name;
+ if(_ExternalScriptsList.get_selection()) {
+ Gtk::TreeModel::iterator i = _ExternalScriptsList.get_selection()->get_selected();
+
+ if(i){
+ name = (*i)[_ExternalScriptsListColumns.filenameColumn];
+ } else {
+ return;
+ }
+ }
+
+ auto document = getDocument();
+ if (!document)
+ return;
+ std::vector<SPObject *> current = document->getResourceList( "script" );
+ for (auto obj : current) {
+ if (obj) {
+ auto script = cast<SPScript>(obj);
+ if (script && (name == script->xlinkhref)) {
+
+ //XML Tree being used directly here while it shouldn't be.
+ Inkscape::XML::Node *repr = obj->getRepr();
+ if (repr){
+ sp_repr_unparent(repr);
+
+ // inform the document, so we can undo
+ DocumentUndo::done(document, _("Remove external script"), "");
+ }
+ }
+ }
+ }
+
+ populate_script_lists();
+}
+
+void DocumentProperties::removeEmbeddedScript(){
+ Glib::ustring id;
+ if(_EmbeddedScriptsList.get_selection()) {
+ Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected();
+
+ if(i){
+ id = (*i)[_EmbeddedScriptsListColumns.idColumn];
+ } else {
+ return;
+ }
+ }
+
+ if (auto document = getDocument()) {
+ if (auto obj = document->getObjectById(id)) {
+ //XML Tree being used directly here while it shouldn't be.
+ if (auto repr = obj->getRepr()){
+ sp_repr_unparent(repr);
+
+ // inform the document, so we can undo
+ DocumentUndo::done(document, _("Remove embedded script"), "");
+ }
+ }
+ }
+
+ populate_script_lists();
+}
+
+void DocumentProperties::onExternalScriptSelectRow()
+{
+ Glib::RefPtr<Gtk::TreeSelection> sel = _ExternalScriptsList.get_selection();
+ if (sel) {
+ _external_remove_btn.set_sensitive(sel->count_selected_rows () > 0);
+ }
+}
+
+void DocumentProperties::onEmbeddedScriptSelectRow()
+{
+ Glib::RefPtr<Gtk::TreeSelection> sel = _EmbeddedScriptsList.get_selection();
+ if (sel) {
+ _embed_remove_btn.set_sensitive(sel->count_selected_rows () > 0);
+ }
+}
+
+void DocumentProperties::changeEmbeddedScript(){
+ Glib::ustring id;
+ if(_EmbeddedScriptsList.get_selection()) {
+ Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected();
+
+ if(i){
+ id = (*i)[_EmbeddedScriptsListColumns.idColumn];
+ } else {
+ return;
+ }
+ }
+
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ bool voidscript=true;
+ std::vector<SPObject *> current = document->getResourceList( "script" );
+ for (auto obj : current) {
+ if (id == obj->getId()){
+ int count = (int) obj->children.size();
+
+ if (count>1)
+ g_warning("TODO: Found a script element with multiple (%d) child nodes! We must implement support for that!", count);
+
+ //XML Tree being used directly here while it shouldn't be.
+ SPObject* child = obj->firstChild();
+ //TODO: shouldn't we get all children instead of simply the first child?
+
+ if (child && child->getRepr()){
+ const gchar* content = child->getRepr()->content();
+ if (content){
+ voidscript=false;
+ _EmbeddedContent.get_buffer()->set_text(content);
+ }
+ }
+ }
+ }
+
+ if (voidscript)
+ _EmbeddedContent.get_buffer()->set_text("");
+}
+
+void DocumentProperties::editEmbeddedScript(){
+ Glib::ustring id;
+ if(_EmbeddedScriptsList.get_selection()) {
+ Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected();
+
+ if(i){
+ id = (*i)[_EmbeddedScriptsListColumns.idColumn];
+ } else {
+ return;
+ }
+ }
+
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ for (auto obj : document->getResourceList("script")) {
+ if (id == obj->getId()) {
+ //XML Tree being used directly here while it shouldn't be.
+ Inkscape::XML::Node *repr = obj->getRepr();
+ if (repr){
+ auto tmp = obj->children | boost::adaptors::transformed([](SPObject& o) { return &o; });
+ std::vector<SPObject*> vec(tmp.begin(), tmp.end());
+ for (auto &child: vec) {
+ child->deleteObject();
+ }
+ obj->appendChildRepr(document->getReprDoc()->createTextNode(_EmbeddedContent.get_buffer()->get_text().c_str()));
+
+ //TODO repr->set_content(_EmbeddedContent.get_buffer()->get_text());
+
+ // inform the document, so we can undo
+ DocumentUndo::done(document, _("Edit embedded script"), "");
+ }
+ }
+ }
+}
+
+void DocumentProperties::populate_script_lists(){
+ _ExternalScriptsListStore->clear();
+ _EmbeddedScriptsListStore->clear();
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ std::vector<SPObject *> current = getDocument()->getResourceList( "script" );
+ if (!current.empty()) {
+ SPObject *obj = *(current.begin());
+ g_assert(obj != nullptr);
+ _scripts_observer.set(obj->parent);
+ }
+ for (auto obj : current) {
+ auto script = cast<SPScript>(obj);
+ g_assert(script != nullptr);
+ if (script->xlinkhref)
+ {
+ Gtk::TreeModel::Row row = *(_ExternalScriptsListStore->append());
+ row[_ExternalScriptsListColumns.filenameColumn] = script->xlinkhref;
+ }
+ else // Embedded scripts
+ {
+ Gtk::TreeModel::Row row = *(_EmbeddedScriptsListStore->append());
+ row[_EmbeddedScriptsListColumns.idColumn] = obj->getId();
+ }
+ }
+}
+
+/**
+* Called for _updating_ the dialog. DO NOT call this a lot. It's expensive!
+* Will need to probably create a GridManager with signals to each Grid attribute
+*/
+void DocumentProperties::update_gridspage()
+{
+ SPNamedView *nv = getDesktop()->getNamedView();
+
+ int prev_page_count = _grids_notebook.get_n_pages();
+ int prev_page_pos = _grids_notebook.get_current_page();
+
+ //remove all tabs
+ while (_grids_notebook.get_n_pages() != 0) {
+ _grids_notebook.remove_page(-1); // this also deletes the page.
+ }
+
+ //add tabs
+ for(auto grid : nv->grids) {
+ if (!grid->getRepr()->attribute("id")) continue; // update_gridspage is called again when "id" is added
+ Glib::ustring name(grid->getRepr()->attribute("id"));
+ const char *icon = grid->typeName();
+ _grids_notebook.append_page(*createNewGridWidget(grid), _createPageTabLabel(name, icon));
+ }
+ _grids_notebook.show_all();
+
+ int cur_page_count = _grids_notebook.get_n_pages();
+ if (cur_page_count > 0) {
+ _grids_button_remove.set_sensitive(true);
+
+ // The following is not correct if grid added/removed via XML
+ if (cur_page_count == prev_page_count + 1) {
+ _grids_notebook.set_current_page(cur_page_count - 1);
+ } else if (cur_page_count == prev_page_count) {
+ _grids_notebook.set_current_page(prev_page_pos);
+ } else if (cur_page_count == prev_page_count - 1) {
+ _grids_notebook.set_current_page(prev_page_pos < 1 ? 0 : prev_page_pos - 1);
+ }
+ } else {
+ _grids_button_remove.set_sensitive(false);
+ }
+}
+
+void *DocumentProperties::notifyGridWidgetsDestroyed(void *data)
+{
+ if (auto prop = reinterpret_cast<DocumentProperties *>(data)) {
+ prop->_grid_rcb_enabled = nullptr;
+ prop->_grid_rcb_snap_visible_only = nullptr;
+ prop->_grid_rcb_visible = nullptr;
+ prop->_grid_rcb_dotted = nullptr;
+ prop->_grid_as_alignment = nullptr;
+ }
+ return nullptr;
+}
+
+Gtk::Widget *DocumentProperties::createNewGridWidget(SPGrid *grid)
+{
+ auto vbox = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL);
+ auto namelabel = Gtk::make_managed<Gtk::Label>("", Gtk::ALIGN_CENTER);
+
+ Inkscape::XML::Node *repr = grid->getRepr();
+ auto doc = getDocument();
+
+ namelabel->set_markup(Glib::ustring("<b>") + grid->displayName() + "</b>");
+ vbox->pack_start(*namelabel, false, false);
+
+ _grid_rcb_enabled = Gtk::make_managed<Inkscape::UI::Widget::RegisteredCheckButton>(
+ _("_Enabled"),
+ _("Makes the grid available for working with on the canvas."),
+ "enabled", _wr, false, repr, doc);
+ // grid_rcb_enabled serves as a canary that tells us that the widgets have been destroyed
+ _grid_rcb_enabled->add_destroy_notify_callback(this, notifyGridWidgetsDestroyed);
+
+ _grid_rcb_snap_visible_only = Gtk::make_managed<Inkscape::UI::Widget::RegisteredCheckButton>(
+ _("Snap to visible _grid lines only"),
+ _("When zoomed out, not all grid lines will be displayed. Only the visible ones will be snapped to"),
+ "snapvisiblegridlinesonly", _wr, false, repr, doc);
+
+ _grid_rcb_visible = Gtk::make_managed<Inkscape::UI::Widget::RegisteredCheckButton>(
+ _("_Visible"),
+ _("Determines whether the grid is displayed or not. Objects are still snapped to invisible grids."),
+ "visible", _wr, false, repr, doc);
+
+ _grid_as_alignment = Gtk::make_managed<Inkscape::UI::Widget::AlignmentSelector>();
+ _grid_as_alignment->on_alignmentClicked().connect([this, grid](int align) {
+ auto doc = getDocument();
+ Geom::Point dimensions = doc->getDimensions();
+ dimensions[Geom::X] *= align % 3 * 0.5;
+ dimensions[Geom::Y] *= align / 3 * 0.5;
+ dimensions *= doc->doc2dt();
+ grid->setOrigin(dimensions);
+ });
+
+ auto left = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL, 4);
+ left->pack_start(*_grid_rcb_enabled, false, false);
+ left->pack_start(*_grid_rcb_visible, false, false);
+ left->pack_start(*_grid_rcb_snap_visible_only, false, false);
+
+ if (grid->getType() == GridType::RECTANGULAR) {
+ _grid_rcb_dotted = Gtk::make_managed<Inkscape::UI::Widget::RegisteredCheckButton>(
+ _("_Show dots instead of lines"), _("If set, displays dots at gridpoints instead of gridlines"),
+ "dotted", _wr, false, repr, doc );
+ left->pack_start(*_grid_rcb_dotted, false, false);
+ }
+
+ left->pack_start(*Gtk::make_managed<Gtk::Label>(_("Align to page:")), false, false);
+ left->pack_start(*_grid_as_alignment, false, false);
+
+ auto right = createRightGridColumn(grid);
+ right->set_hexpand(false);
+
+ auto inner = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
+ inner->pack_start(*left, true, true);
+ inner->pack_start(*right, false, false);
+ vbox->pack_start(*inner, false, false);
+ vbox->set_border_width(4);
+
+ std::list<Gtk::Widget*> slaves;
+ for (auto &item : left->get_children()) {
+ if (item != _grid_rcb_enabled) {
+ slaves.push_back(item);
+ }
+ }
+ slaves.push_back(right);
+ _grid_rcb_enabled->setSlaveWidgets(slaves);
+
+ // set widget values
+ _wr.setUpdating (true);
+ _grid_rcb_enabled->setActive(grid->isEnabled());
+ _grid_rcb_visible->setActive(grid->isVisible());
+
+ if (_grid_rcb_dotted)
+ _grid_rcb_dotted->setActive(grid->isDotted());
+
+ _grid_rcb_snap_visible_only->setActive(grid->getSnapToVisibleOnly());
+ _grid_rcb_enabled->setActive(grid->snapper()->getEnabled());
+ _grid_rcb_snap_visible_only->setActive(grid->snapper()->getSnapVisibleOnly());
+ _wr.setUpdating (false);
+
+ return vbox;
+}
+
+// needs to switch based on grid type, need to find a better way
+Gtk::Widget *DocumentProperties::createRightGridColumn(SPGrid *grid)
+{
+ using namespace Inkscape::UI::Widget;
+ Inkscape::XML::Node *repr = grid->getRepr();
+ auto doc = getDocument();
+
+ auto rumg = Gtk::make_managed<RegisteredUnitMenu>(
+ _("Grid _units:"), "units", _wr, repr, doc);
+ auto rsu_ox = Gtk::make_managed<RegisteredScalarUnit>(
+ _("_Origin X:"), _("X coordinate of grid origin"), "originx",
+ *rumg, _wr, repr, doc, RSU_x);
+ auto rsu_oy = Gtk::make_managed<RegisteredScalarUnit>(
+ _("O_rigin Y:"), _("Y coordinate of grid origin"), "originy",
+ *rumg, _wr, repr, doc, RSU_y);
+ auto rsu_sx = Gtk::make_managed<RegisteredScalarUnit>(
+ _("Spacing _X:"), _("Distance between vertical grid lines"), "spacingx",
+ *rumg, _wr, repr, doc, RSU_x);
+ auto rsu_sy = Gtk::make_managed<RegisteredScalarUnit>(
+ _("Spacing _Y:"), _("Base length of z-axis"), "spacingy",
+ *rumg, _wr, repr, doc, RSU_y);
+ auto rsu_ax = Gtk::make_managed<RegisteredScalar>(
+ _("Angle X:"), _("Angle of x-axis"), "gridanglex", _wr, repr, doc);
+ auto rsu_az = Gtk::make_managed<RegisteredScalar>(
+ _("Angle Z:"), _("Angle of z-axis"), "gridanglez", _wr, repr, doc);
+ auto rcp_gcol = Gtk::make_managed<RegisteredColorPicker>(
+ _("Minor grid line _color:"), _("Minor grid line color"), _("Color of the minor grid lines"),
+ "color", "opacity", _wr, repr, doc);
+ auto rcp_gmcol = Gtk::make_managed<RegisteredColorPicker>(
+ _("Ma_jor grid line color:"), _("Major grid line color"),
+ _("Color of the major (highlighted) grid lines"),
+ "empcolor", "empopacity", _wr, repr, doc);
+ auto rsi = Gtk::make_managed<RegisteredSuffixedInteger>(
+ _("_Major grid line every:"), "", _("lines"), "empspacing", _wr, repr, doc);
+
+ rumg->set_hexpand();
+ rsu_ox->set_hexpand();
+ rsu_oy->set_hexpand();
+ rsu_sx->set_hexpand();
+ rsu_sy->set_hexpand();
+ rsu_ax->set_hexpand();
+ rsu_az->set_hexpand();
+ rcp_gcol->set_hexpand();
+ rcp_gmcol->set_hexpand();
+ rsi->set_hexpand();
+
+ // set widget values
+ _wr.setUpdating (true);
+
+ rsu_ox->setDigits(5);
+ rsu_ox->setIncrements(0.1, 1.0);
+
+ rsu_oy->setDigits(5);
+ rsu_oy->setIncrements(0.1, 1.0);
+
+ rsu_sx->setDigits(5);
+ rsu_sx->setIncrements(0.1, 1.0);
+
+ rsu_sy->setDigits(5);
+ rsu_sy->setIncrements(0.1, 1.0);
+
+ rumg->setUnit(grid->getUnit()->abbr);
+
+ // Doc to px so unit is conserved in RegisteredScalerUnit
+ auto origin = grid->getOrigin() * doc->getDocumentScale();
+ rsu_ox->setValueKeepUnit(origin[Geom::X], "px");
+ rsu_oy->setValueKeepUnit(origin[Geom::Y], "px");
+
+ auto spacing = grid->getSpacing() * doc->getDocumentScale();
+ rsu_sx->setValueKeepUnit(spacing[Geom::X], "px");
+ rsu_sy->setValueKeepUnit(spacing[Geom::Y], "px");
+
+ rsu_ax->setValue(grid->getAngleX());
+ rsu_az->setValue(grid->getAngleZ());
+
+ rcp_gcol->setRgba32 (grid->getMinorColor());
+ rcp_gmcol->setRgba32 (grid->getMajorColor());
+ rsi->setValue (grid->getMajorLineInterval());
+
+ _wr.setUpdating (false);
+
+ rsu_ox->setProgrammatically = false;
+ rsu_oy->setProgrammatically = false;
+
+ auto column = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL, 4);
+ column->pack_start(*rumg, true, false);
+ column->pack_start(*rsu_ox, true, false);
+ column->pack_start(*rsu_oy, true, false);
+
+ if (grid->getType() == GridType::RECTANGULAR) {
+ column->pack_start(*rsu_sx, true, false);
+ }
+
+ column->pack_start(*rsu_sy, true, false);
+
+ if (grid->getType() == GridType::AXONOMETRIC) {
+ column->pack_start(*rsu_ax, true, false);
+ column->pack_start(*rsu_az, true, false);
+ }
+
+ column->pack_start(*rcp_gcol, true, false);
+ column->pack_start(*rcp_gmcol, true, false);
+ column->pack_start(*rsi, true, false);
+
+ return column;
+}
+
+/**
+ * Build grid page of dialog.
+ */
+void DocumentProperties::build_gridspage()
+{
+ /// \todo FIXME: gray out snapping when grid is off.
+ /// Dissenting view: you want snapping without grid.
+
+ _grids_label_crea.set_markup(_("<b>Creation</b>"));
+ _grids_label_def.set_markup(_("<b>Defined grids</b>"));
+ _grids_hbox_crea.pack_start(_grids_combo_gridtype, true, true);
+ _grids_hbox_crea.pack_start(_grids_button_new, true, true);
+
+ _grids_combo_gridtype.append( _("Rectangular Grid") );
+ _grids_combo_gridtype.append( _("Axonometric Grid") );
+
+ _grids_combo_gridtype.set_active_text( _("Rectangular Grid") );
+
+ _grids_space.set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y);
+
+ _grids_vbox.set_name("NotebookPage");
+ _grids_vbox.set_border_width(4);
+ _grids_vbox.set_spacing(4);
+ _grids_vbox.pack_start(_grids_label_crea, false, false);
+ _grids_vbox.pack_start(_grids_hbox_crea, false, false);
+ _grids_vbox.pack_start(_grids_space, false, false);
+ _grids_vbox.pack_start(_grids_label_def, false, false);
+ _grids_vbox.pack_start(_grids_notebook, false, false);
+ _grids_vbox.pack_start(_grids_button_remove, false, false);
+}
+
+void DocumentProperties::update_viewbox(SPDesktop* desktop) {
+ if (!desktop) return;
+
+ auto* document = desktop->getDocument();
+ if (!document) return;
+
+ using UI::Widget::PageProperties;
+ SPRoot* root = document->getRoot();
+ if (root->viewBox_set) {
+ auto& vb = root->viewBox;
+ _page->set_dimension(PageProperties::Dimension::ViewboxPosition, vb.min()[Geom::X], vb.min()[Geom::Y]);
+ _page->set_dimension(PageProperties::Dimension::ViewboxSize, vb.width(), vb.height());
+ }
+
+ update_scale_ui(desktop);
+}
+
+/**
+ * Update dialog widgets from desktop. Also call updateWidget routines of the grids.
+ */
+void DocumentProperties::update_widgets()
+{
+ auto desktop = getDesktop();
+ auto document = getDocument();
+ if (_wr.isUpdating() || !document) return;
+
+ auto nv = desktop->getNamedView();
+ auto &page_manager = document->getPageManager();
+
+ _wr.setUpdating(true);
+
+ SPRoot *root = document->getRoot();
+
+ double doc_w = root->width.value;
+ Glib::ustring doc_w_unit = unit_table.getUnit(root->width.unit)->abbr;
+ bool percent = doc_w_unit == "%";
+ if (doc_w_unit == "") {
+ doc_w_unit = "px";
+ } else if (doc_w_unit == "%" && root->viewBox_set) {
+ doc_w_unit = "px";
+ doc_w = root->viewBox.width();
+ }
+ double doc_h = root->height.value;
+ Glib::ustring doc_h_unit = unit_table.getUnit(root->height.unit)->abbr;
+ percent = percent || doc_h_unit == "%";
+ if (doc_h_unit == "") {
+ doc_h_unit = "px";
+ } else if (doc_h_unit == "%" && root->viewBox_set) {
+ doc_h_unit = "px";
+ doc_h = root->viewBox.height();
+ }
+ using UI::Widget::PageProperties;
+ // dialog's behavior is not entirely correct when document sizes are expressed in '%', so put up a disclaimer
+ _page->set_check(PageProperties::Check::UnsupportedSize, percent);
+
+ _page->set_dimension(PageProperties::Dimension::PageSize, doc_w, doc_h);
+ _page->set_unit(PageProperties::Units::Document, doc_w_unit);
+
+ update_viewbox_ui(desktop);
+ update_scale_ui(desktop);
+
+ if (nv->display_units) {
+ _page->set_unit(PageProperties::Units::Display, nv->display_units->abbr);
+ }
+ _page->set_check(PageProperties::Check::Checkerboard, nv->desk_checkerboard);
+ _page->set_color(PageProperties::Color::Desk, nv->desk_color);
+ _page->set_color(PageProperties::Color::Background, page_manager.background_color);
+ _page->set_check(PageProperties::Check::Border, page_manager.border_show);
+ _page->set_check(PageProperties::Check::BorderOnTop, page_manager.border_on_top);
+ _page->set_color(PageProperties::Color::Border, page_manager.border_color);
+ _page->set_check(PageProperties::Check::Shadow, page_manager.shadow_show);
+ _page->set_check(PageProperties::Check::PageLabelStyle, page_manager.label_style != "default");
+
+ _page->set_check(PageProperties::Check::AntiAlias, root->style->shape_rendering.computed != SP_CSS_SHAPE_RENDERING_CRISPEDGES);
+ _page->set_check(PageProperties::Check::ClipToPage, nv->clip_to_page);
+
+ //-----------------------------------------------------------guide page
+
+ _rcb_sgui.setActive (nv->getShowGuides());
+ _rcb_lgui.setActive (nv->getLockGuides());
+ _rcp_gui.setRgba32 (nv->guidecolor);
+ _rcp_hgui.setRgba32 (nv->guidehicolor);
+
+ //-----------------------------------------------------------grids page
+
+ update_gridspage();
+
+ //------------------------------------------------Color Management page
+
+ populate_linked_profiles_box();
+ populate_available_profiles();
+
+ //-----------------------------------------------------------meta pages
+ // update the RDF entities; note that this may modify document, maybe doc-undo should be called?
+ if (auto document = getDocument()) {
+ for (auto &it : _rdflist) {
+ bool read_only = false;
+ it->update(document, read_only);
+ }
+ _licensor.update(document);
+ }
+ _wr.setUpdating (false);
+}
+
+// TODO: copied from fill-and-stroke.cpp factor out into new ui/widget file?
+Gtk::Box&
+DocumentProperties::_createPageTabLabel(const Glib::ustring& label, const char *label_image)
+{
+ Gtk::Box *_tab_label_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0));
+ _tab_label_box->set_spacing(4);
+
+ auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU));
+ _tab_label_box->pack_start(*img);
+
+ Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true));
+ _tab_label_box->pack_start(*_tab_label);
+ _tab_label_box->show_all();
+
+ return *_tab_label_box;
+}
+
+//--------------------------------------------------------------------
+
+void DocumentProperties::on_response (int id)
+{
+ if (id == Gtk::RESPONSE_DELETE_EVENT || id == Gtk::RESPONSE_CLOSE)
+ {
+ _rcp_gui.closeWindow();
+ _rcp_hgui.closeWindow();
+ }
+
+ if (id == Gtk::RESPONSE_CLOSE)
+ hide();
+}
+
+void DocumentProperties::load_default_metadata()
+{
+ /* Get the data RDF entities data from preferences*/
+ for (auto &it : _rdflist) {
+ it->load_from_preferences ();
+ }
+}
+
+void DocumentProperties::save_default_metadata()
+{
+ /* Save these RDF entities to preferences*/
+ if (auto document = getDocument()) {
+ for (auto &it : _rdflist) {
+ it->save_to_preferences(document);
+ }
+ }
+}
+
+void DocumentProperties::WatchConnection::connect(Inkscape::XML::Node *node)
+{
+ disconnect();
+ if (!node) return;
+
+ _node = node;
+ _node->addObserver(*this);
+}
+
+void DocumentProperties::WatchConnection::disconnect() {
+ if (_node) {
+ _node->removeObserver(*this);
+ _node = nullptr;
+ }
+}
+
+void DocumentProperties::WatchConnection::notifyChildAdded(XML::Node&, XML::Node&, XML::Node*)
+{
+ _dialog->update_gridspage();
+}
+
+void DocumentProperties::WatchConnection::notifyChildRemoved(XML::Node&, XML::Node&, XML::Node*)
+{
+ _dialog->update_gridspage();
+}
+
+void DocumentProperties::WatchConnection::notifyAttributeChanged(XML::Node&, GQuark, Util::ptr_shared, Util::ptr_shared)
+{
+ _dialog->update_widgets();
+}
+
+void DocumentProperties::documentReplaced()
+{
+ _root_connection.disconnect();
+ _namedview_connection.disconnect();
+
+ if (auto desktop = getDesktop()) {
+ _wr.setDesktop(desktop);
+ _namedview_connection.connect(desktop->getNamedView()->getRepr());
+ if (auto document = desktop->getDocument()) {
+ _root_connection.connect(document->getRoot()->getRepr());
+ }
+ populate_linked_profiles_box();
+ update_widgets();
+ }
+}
+
+void DocumentProperties::update()
+{
+ update_widgets();
+}
+
+/*########################################################################
+# BUTTON CLICK HANDLERS (callbacks)
+########################################################################*/
+
+void DocumentProperties::onNewGrid()
+{
+ auto desktop = getDesktop();
+ auto document = getDocument();
+ if (!desktop || !document) return;
+
+ auto selected_grid_type = _grids_combo_gridtype.get_active_row_number();
+ GridType grid_type;
+ switch (selected_grid_type) {
+ case 0: grid_type = GridType::RECTANGULAR; break;
+ case 1: grid_type = GridType::AXONOMETRIC; break;
+ default: g_assert_not_reached(); return;
+ }
+
+ auto repr = desktop->getNamedView()->getRepr();
+ SPGrid::create_new(document, repr, grid_type);
+
+ // toggle grid showing to ON:
+ // side effect: any pre-existing grids set to invisible will be set to visible
+ desktop->getNamedView()->setShowGrids(true);
+ DocumentUndo::done(document, _("Create new grid"), INKSCAPE_ICON("document-properties"));
+}
+
+
+void DocumentProperties::onRemoveGrid()
+{
+ gint pagenum = _grids_notebook.get_current_page();
+ if (pagenum == -1) // no pages
+ return;
+
+ SPNamedView *nv = getDesktop()->getNamedView();
+ SPGrid *found_grid = nullptr;
+ if( pagenum < (gint)nv->grids.size())
+ found_grid = nv->grids[pagenum];
+
+ if (auto document = getDocument()) {
+ if (found_grid) {
+ // delete the grid that corresponds with the selected tab
+ // when the grid is deleted from SVG, the SPNamedview handler automatically deletes the object, so found_grid becomes an invalid pointer!
+ found_grid->getRepr()->parent()->removeChild(found_grid->getRepr());
+ DocumentUndo::done(document, _("Remove grid"), INKSCAPE_ICON("document-properties"));
+ }
+ }
+}
+
+/* This should not effect anything in the SVG tree (other than "inkscape:document-units").
+ This should only effect values displayed in the GUI. */
+void DocumentProperties::display_unit_change(const Inkscape::Util::Unit* doc_unit)
+{
+ SPDocument *document = getDocument();
+ // Don't execute when change is being undone
+ if (!document || !DocumentUndo::getUndoSensitive(document)) {
+ return;
+ }
+ // Don't execute when initializing widgets
+ if (_wr.isUpdating()) {
+ return;
+ }
+
+ auto action = document->getActionGroup()->lookup_action("set-display-unit");
+ action->activate(doc_unit->abbr);
+}
+
+} // 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/dialog/document-properties.h b/src/ui/dialog/document-properties.h
new file mode 100644
index 0000000..0240652
--- /dev/null
+++ b/src/ui/dialog/document-properties.h
@@ -0,0 +1,275 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * \brief Document Properties dialog
+ */
+/* Authors:
+ * Ralf Stephan <ralf@ark.in-berlin.de>
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2004, 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H
+#define INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H
+
+#ifdef HAVE_CONFIG_H
+#include "config.h" // only include where actually required!
+#endif
+
+#include <cstddef>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/textview.h>
+#include <sigc++/sigc++.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/licensor.h"
+#include "ui/widget/registered-widget.h"
+#include "ui/widget/registry.h"
+#include "ui/widget/tolerance-slider.h"
+#include "xml/helper-observer.h"
+#include "xml/node-observer.h"
+
+namespace Inkscape {
+namespace XML { class Node; }
+namespace UI {
+
+namespace Widget {
+class AlignmentSelector;
+class EntityEntry;
+class NotebookPage;
+class PageProperties;
+} // namespace Widget
+
+namespace Dialog {
+
+using RDEList = std::vector<UI::Widget::EntityEntry *>;
+
+class DocumentProperties : public DialogBase
+{
+public:
+ DocumentProperties();
+ ~DocumentProperties() override;
+
+ void update_widgets();
+ static DocumentProperties &getInstance();
+ static void destroy();
+
+ void documentReplaced() override;
+
+ void update() override;
+ void update_gridspage();
+
+protected:
+ void build_page();
+ void build_grid();
+ void build_guides();
+ void build_snap();
+ void build_gridspage();
+
+ void build_cms();
+ void build_scripting();
+ void build_metadata();
+
+ virtual void on_response (int);
+ void populate_available_profiles();
+ void populate_linked_profiles_box();
+ void linkSelectedProfile();
+ void removeSelectedProfile();
+ void onColorProfileSelectRow();
+ void linked_profiles_list_button_release(GdkEventButton* event);
+ void cms_create_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem);
+
+ void external_scripts_list_button_release(GdkEventButton* event);
+ void embedded_scripts_list_button_release(GdkEventButton* event);
+ void populate_script_lists();
+ void addExternalScript();
+ void browseExternalScript();
+ void addEmbeddedScript();
+ void removeExternalScript();
+ void removeEmbeddedScript();
+ void changeEmbeddedScript();
+ void onExternalScriptSelectRow();
+ void onEmbeddedScriptSelectRow();
+ void editEmbeddedScript();
+ void external_create_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem);
+ void embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem);
+ void load_default_metadata();
+ void save_default_metadata();
+ void update_viewbox(SPDesktop* desktop);
+ void update_scale_ui(SPDesktop* desktop);
+ void update_viewbox_ui(SPDesktop* desktop);
+ void set_document_scale(SPDesktop* desktop, double scale_x);
+ void set_viewbox_pos(SPDesktop* desktop, double x, double y);
+ void set_viewbox_size(SPDesktop* desktop, double width, double height);
+
+ Inkscape::XML::SignalObserver _emb_profiles_observer, _scripts_observer;
+ Gtk::Notebook _notebook;
+
+ UI::Widget::NotebookPage *_page_page;
+ UI::Widget::NotebookPage *_page_guides;
+ UI::Widget::NotebookPage *_page_cms;
+ UI::Widget::NotebookPage *_page_scripting;
+
+ Gtk::Notebook _scripting_notebook;
+ UI::Widget::NotebookPage *_page_external_scripts;
+ UI::Widget::NotebookPage *_page_embedded_scripts;
+
+ UI::Widget::NotebookPage *_page_metadata1;
+ UI::Widget::NotebookPage *_page_metadata2;
+
+ Gtk::Box _grids_vbox;
+
+ UI::Widget::Registry _wr;
+ //---------------------------------------------------------------
+ UI::Widget::RegisteredCheckButton _rcb_sgui;
+ UI::Widget::RegisteredCheckButton _rcb_lgui;
+ UI::Widget::RegisteredColorPicker _rcp_gui;
+ UI::Widget::RegisteredColorPicker _rcp_hgui;
+ Gtk::Button _create_guides_btn;
+ Gtk::Button _delete_guides_btn;
+ //---------------------------------------------------------------
+ UI::Widget::PageProperties* _page;
+ //---------------------------------------------------------------
+ Gtk::Button _unlink_btn;
+ class AvailableProfilesColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ AvailableProfilesColumns()
+ { add(fileColumn); add(nameColumn); add(separatorColumn); }
+ Gtk::TreeModelColumn<Glib::ustring> fileColumn;
+ Gtk::TreeModelColumn<Glib::ustring> nameColumn;
+ Gtk::TreeModelColumn<bool> separatorColumn;
+ };
+ AvailableProfilesColumns _AvailableProfilesListColumns;
+ Glib::RefPtr<Gtk::ListStore> _AvailableProfilesListStore;
+ Gtk::ComboBox _AvailableProfilesList;
+ bool _AvailableProfilesList_separator(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter);
+ class LinkedProfilesColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ LinkedProfilesColumns()
+ { add(nameColumn); add(previewColumn); }
+ Gtk::TreeModelColumn<Glib::ustring> nameColumn;
+ Gtk::TreeModelColumn<Glib::ustring> previewColumn;
+ };
+ LinkedProfilesColumns _LinkedProfilesListColumns;
+ Glib::RefPtr<Gtk::ListStore> _LinkedProfilesListStore;
+ Gtk::TreeView _LinkedProfilesList;
+ Gtk::ScrolledWindow _LinkedProfilesListScroller;
+ Gtk::Menu _EmbProfContextMenu;
+
+ //---------------------------------------------------------------
+ Gtk::Button _external_add_btn;
+ Gtk::Button _external_remove_btn;
+ Gtk::Button _embed_new_btn;
+ Gtk::Button _embed_remove_btn;
+ Gtk::ButtonBox _embed_button_box;
+
+ class ExternalScriptsColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ ExternalScriptsColumns()
+ { add(filenameColumn); }
+ Gtk::TreeModelColumn<Glib::ustring> filenameColumn;
+ };
+ ExternalScriptsColumns _ExternalScriptsListColumns;
+ class EmbeddedScriptsColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ EmbeddedScriptsColumns()
+ { add(idColumn); }
+ Gtk::TreeModelColumn<Glib::ustring> idColumn;
+ };
+ EmbeddedScriptsColumns _EmbeddedScriptsListColumns;
+ Glib::RefPtr<Gtk::ListStore> _ExternalScriptsListStore;
+ Glib::RefPtr<Gtk::ListStore> _EmbeddedScriptsListStore;
+ Gtk::TreeView _ExternalScriptsList;
+ Gtk::TreeView _EmbeddedScriptsList;
+ Gtk::ScrolledWindow _ExternalScriptsListScroller;
+ Gtk::ScrolledWindow _EmbeddedScriptsListScroller;
+ Gtk::Menu _ExternalScriptsContextMenu;
+ Gtk::Menu _EmbeddedScriptsContextMenu;
+ Gtk::Entry _script_entry;
+ Gtk::TextView _EmbeddedContent;
+ Gtk::ScrolledWindow _EmbeddedContentScroller;
+ //---------------------------------------------------------------
+
+ Gtk::Notebook _grids_notebook;
+ Gtk::Box _grids_hbox_crea;
+ Gtk::Label _grids_label_crea;
+ Gtk::Button _grids_button_new;
+ Gtk::Button _grids_button_remove;
+ Gtk::ComboBoxText _grids_combo_gridtype;
+ Gtk::Label _grids_label_def;
+ Gtk::Box _grids_space;
+ //---------------------------------------------------------------
+
+ RDEList _rdflist;
+ UI::Widget::Licensor _licensor;
+
+ Gtk::Box& _createPageTabLabel(const Glib::ustring& label, const char *label_image);
+
+private:
+ // callback methods for buttons on grids page.
+ void onNewGrid();
+ void onRemoveGrid();
+
+ // callback for display unit change
+ void display_unit_change(const Inkscape::Util::Unit* unit);
+
+ Gtk::Widget *createNewGridWidget(SPGrid *grid);
+ Gtk::Widget *createRightGridColumn(SPGrid *grid);
+ static void *notifyGridWidgetsDestroyed(void *data);
+
+ UI::Widget::RegisteredCheckButton *_grid_rcb_enabled = nullptr;
+ UI::Widget::RegisteredCheckButton *_grid_rcb_snap_visible_only = nullptr;
+ UI::Widget::RegisteredCheckButton *_grid_rcb_visible = nullptr;
+ UI::Widget::RegisteredCheckButton *_grid_rcb_dotted = nullptr;
+ UI::Widget::AlignmentSelector *_grid_as_alignment = nullptr;
+
+ class WatchConnection : private XML::NodeObserver
+ {
+ public:
+ WatchConnection(DocumentProperties *dialog)
+ : _dialog(dialog)
+ {}
+ ~WatchConnection() override { disconnect(); }
+ void connect(Inkscape::XML::Node *node);
+ void disconnect();
+
+ private:
+ void notifyChildAdded(XML::Node &node, XML::Node &child, XML::Node *prev) final;
+ void notifyChildRemoved(XML::Node &node, XML::Node &child, XML::Node *prev) final;
+ void notifyAttributeChanged(XML::Node &node, GQuark name, Util::ptr_shared old_value,
+ Util::ptr_shared new_value) final;
+
+ Inkscape::XML::Node *_node{nullptr};
+ DocumentProperties *_dialog;
+ };
+ // nodes connected to listeners
+ WatchConnection _namedview_connection;
+ WatchConnection _root_connection;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_DOCUMENT_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/dialog/document-resources.cpp b/src/ui/dialog/document-resources.cpp
new file mode 100644
index 0000000..7cc4aae
--- /dev/null
+++ b/src/ui/dialog/document-resources.cpp
@@ -0,0 +1,1170 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "document-resources.h"
+#include <cairo.h>
+#include <cairomm/enums.h>
+#include <cairomm/refptr.h>
+#include <cairomm/surface.h>
+#include <cassert>
+#include <cstddef>
+#include <gdkmm/pixbuf.h>
+#include <gdkmm/rgba.h>
+#include <glib/gi18n.h>
+#include <glibmm/exception.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/main.h>
+#include <glibmm/markup.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/filechooser.h>
+#include <gtkmm/filechooserdialog.h>
+#include <gtkmm/filefilter.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/object.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/stack.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treemodelsort.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/window.h>
+#include <memory>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <typeindex>
+#include <unordered_map>
+#include <vector>
+#include "color.h"
+#include "display/cairo-utils.h"
+#include "document.h"
+#include "extension/system.h"
+#include "helper/choose-file.h"
+#include "helper/save-image.h"
+#include "inkscape.h"
+#include "object/sp-filter.h"
+#include "object/filters/sp-filter-primitive.h"
+#include "object/color-profile.h"
+#include "object/sp-gradient.h"
+#include "object/sp-font.h"
+#include "object/sp-image.h"
+#include "object/sp-item-group.h"
+#include "object/sp-marker.h"
+#include "object/sp-mesh-gradient.h"
+#include "object/sp-object.h"
+#include "object/sp-offset.h"
+#include "object/sp-path.h"
+#include "object/sp-pattern.h"
+#include "object/tags.h"
+#include "pattern-manipulation.h"
+#include "rdf.h"
+#include "selection.h"
+#include "ui/builder-utils.h"
+#include "object/sp-defs.h"
+#include "object/sp-root.h"
+#include "object/sp-symbol.h"
+#include "object/sp-use.h"
+#include "style.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-names.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+#include "ui/widget/shapeicon.h"
+#include "util/object-renderer.h"
+#include "util/trim.h"
+#include "xml/href-attribute-helper.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+struct ItemColumns : public Gtk::TreeModel::ColumnRecord {
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<Cairo::RefPtr<Cairo::Surface>> image;
+ Gtk::TreeModelColumn<bool> editable;
+ Gtk::TreeModelColumn<SPObject*> object;
+ Gtk::TreeModelColumn<int> color;
+
+ ItemColumns() {
+ add(id);
+ add(label);
+ add(image);
+ add(editable);
+ add(object);
+ add(color);
+ }
+} g_item_columns;
+
+struct InfoColumns : public Gtk::TreeModel::ColumnRecord {
+ Gtk::TreeModelColumn<Glib::ustring> item;
+ Gtk::TreeModelColumn<Glib::ustring> value;
+ Gtk::TreeModelColumn<uint32_t> count;
+ Gtk::TreeModelColumn<SPObject*> object;
+
+ InfoColumns() {
+ add(item);
+ add(value);
+ add(count);
+ add(object);
+ }
+} g_info_columns;
+
+enum Resources : int {
+ Stats, Colors, Fonts, Styles, Patterns, Symbols, Markers, Gradients, Swatches, Images, Filters, External, Metadata
+};
+
+const std::unordered_map<std::string, Resources> g_id_to_resource = {
+ {"colors", Colors},
+ {"swatches", Swatches},
+ {"fonts", Fonts},
+ {"stats", Stats},
+ {"styles", Styles},
+ {"patterns", Patterns},
+ {"symbols", Symbols},
+ {"markers", Markers},
+ {"gradients", Gradients},
+ {"images", Images},
+ {"filters", Filters},
+ {"external", External},
+ {"metadata", Metadata},
+ // to do: SVG fonts
+ // other resources
+};
+
+size_t get_resource_count(const details::Statistics& stats, Resources rsrc) {
+ switch (rsrc) {
+ case Colors: return stats.colors;
+ case Swatches: return stats.swatches;
+ case Fonts: return stats.fonts;
+ case Symbols: return stats.symbols;
+ case Gradients: return stats.gradients;
+ case Patterns: return stats.patterns;
+ case Images: return stats.images;
+ case Filters: return stats.filters;
+ case Markers: return stats.markers;
+ case Metadata: return stats.metadata;
+ case Styles: return stats.styles;
+ case External: return stats.external_uris;
+
+ case Stats: return 1;
+
+ default:
+ break;
+ }
+ return 0;
+}
+
+Resources id_to_resource(const std::string& id) {
+ auto it = g_id_to_resource.find(id);
+ if (it == end(g_id_to_resource)) return Stats;
+
+ return it->second;
+}
+
+size_t get_resource_count(const std::string& id, const details::Statistics& stats) {
+ auto it = g_id_to_resource.find(id);
+ if (it == end(g_id_to_resource)) return 0;
+
+ return get_resource_count(stats, it->second);
+}
+
+bool is_resource_present(const std::string& id, const details::Statistics& stats) {
+ return get_resource_count(id, stats) > 0;
+}
+
+std::string choose_file(Glib::ustring title, Gtk::Window* parent, Glib::ustring mime_type, Glib::ustring file_name) {
+ static std::string current_folder;
+ return Inkscape::choose_file_save(title, parent, mime_type, file_name, current_folder);
+}
+
+void save_gimp_palette(std::string fname, const std::vector<int>& colors, const char* name) {
+ try {
+ std::ostringstream ost;
+ ost << "GIMP Palette\n";
+ if (name && *name) {
+ ost << "Name: " << name << "\n";
+ }
+ ost << "#\n";
+ for (auto c : colors) {
+ auto r = (c >> 16) & 0xff;
+ auto g = (c >> 8) & 0xff;
+ auto b = c & 0xff;
+ ost << r << ' ' << g << ' ' << b << '\n';
+ }
+ Glib::file_set_contents(fname, ost.str());
+ }
+ catch (Glib::Exception& ex) {
+ g_warning("Error saving color palette: %s", ex.what().c_str());
+ }
+ catch (...) {
+ g_warning("Error saving color palette.");
+ }
+}
+
+void extract_colors(Gtk::Window* parent, const std::vector<int>& colors, const char* name) {
+ if (colors.empty() || !parent) return;
+
+ auto fname = choose_file(_("Export Color Palette"), parent, "application/color-palette", "color-palette.gpl");
+ if (fname.empty()) return;
+
+ // export palette
+ save_gimp_palette(fname, colors, name);
+}
+
+void delete_object(SPObject* object, Inkscape::Selection* selection) {
+ if (!object || !selection) return;
+
+ auto document = object->document;
+
+ if (auto pattern = cast<SPPattern>(object)) {
+ // delete action fails for patterns; remove them by deleting their nodes
+ sp_repr_unparent(pattern->getRepr());
+ DocumentUndo::done(document, _("Delete pattern"), INKSCAPE_ICON("document-resources"));
+ }
+ else if (auto gradient = cast<SPGradient>(object)) {
+ // delete action fails for gradients; remove them by deleting their nodes
+ sp_repr_unparent(gradient->getRepr());
+ DocumentUndo::done(document, _("Delete gradient"), INKSCAPE_ICON("document-resources"));
+ }
+ else {
+ selection->set(object);
+ selection->deleteItems();
+ }
+}
+
+namespace details {
+ // editing "inkscape:label"
+ Glib::ustring get_inkscape_label(const SPObject& object) {
+ auto label = object.getAttribute("inkscape:label");
+ return Glib::ustring(label ? label : "");
+ }
+ void set_inkscape_label(SPObject& object, const Glib::ustring& label) {
+ object.setAttribute("inkscape:label", label.c_str());
+ }
+
+ // editing title element
+ Glib::ustring get_title(const SPObject& object) {
+ auto title = object.title();
+ Glib::ustring str(title ? title : "");
+ g_free(title);
+ return str;
+ }
+ void set_title(SPObject& object, const Glib::ustring& title) {
+ object.setTitle(title.c_str());
+ }
+}
+
+// label editing: get/set functions for various object types;
+// by default "inkscape:label" will be used (expressed as SPObject);
+// if some types need exceptions to this ruke, they can provide their own edit functions;
+// note: all most-derived types need to be listed to specify overrides
+std::map<std::type_index, std::function<Glib::ustring (const SPObject&)>> g_get_label = {
+ // default: editing "inkscape:label" as a description;
+ // patterns use Inkscape-specific "inkscape:label" attribute;
+ // gradients can also use labels instead of IDs;
+ // filters; to do - editing in a tree view;
+ // images can use both, label & title; defaulting to label for consistency
+ {typeid(SPObject), details::get_inkscape_label},
+ // exception: symbols use <title> element for description
+ {typeid(SPSymbol), details::get_title},
+ // markers use stockid for some reason - label: to do
+ {typeid(SPMarker), details::get_inkscape_label},
+};
+
+std::map<std::type_index, std::function<void (SPObject&, const Glib::ustring&)>> g_set_label = {
+ {typeid(SPObject), details::set_inkscape_label},
+ {typeid(SPSymbol), details::set_title},
+ {typeid(SPMarker), details::set_inkscape_label},
+};
+
+// liststore columns from glade file
+constexpr int COL_ID = 1;
+constexpr int COL_ICON = 2;
+constexpr int COL_COUNT = 3;
+
+DocumentResources::DocumentResources()
+ : DialogBase("/dialogs/document-resources", "DocumentResources"),
+ _builder(create_builder("dialog-document-resources.glade")),
+ _iconview(get_widget<Gtk::IconView>(_builder, "iconview")),
+ _treeview(get_widget<Gtk::TreeView>(_builder, "treeview")),
+ _selector(get_widget<Gtk::TreeView>(_builder, "tree")),
+ _edit(get_widget<Gtk::Button>(_builder, "edit")),
+ _select(get_widget<Gtk::Button>(_builder, "select")),
+ _delete(get_widget<Gtk::Button>(_builder, "delete")),
+ _extract(get_widget<Gtk::Button>(_builder, "extract")),
+ _search(get_widget<Gtk::SearchEntry>(_builder, "search")) {
+
+ _info_store = Gtk::ListStore::create(g_info_columns);
+ _item_store = Gtk::ListStore::create(g_item_columns);
+ auto filtered_info = Gtk::TreeModelFilter::create(_info_store);
+ auto filtered_items = Gtk::TreeModelFilter::create(_item_store);
+ auto model = Gtk::TreeModelSort::create(filtered_items);
+ model->set_sort_column(g_item_columns.label.index(), Gtk::SORT_ASCENDING);
+
+ add(get_widget<Gtk::Box>(_builder, "main"));
+
+ _iconview.set_model(model);
+ _iconview.set_text_column(g_item_columns.label);
+ _label_renderer = dynamic_cast<Gtk::CellRendererText*>(_iconview.get_first_cell());
+ assert(_label_renderer);
+ _label_renderer->property_editable() = true;
+ _label_renderer->signal_editing_started().connect([=](Gtk::CellEditable* cell, const Glib::ustring& path){
+ start_editing(cell, path);
+ });
+ _label_renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& new_text){
+ end_editing(path, new_text);
+ });
+
+ _iconview.pack_start(_image_renderer);
+ _iconview.add_attribute(_image_renderer, "surface", g_item_columns.image);
+
+ _treeview.set_model(filtered_info);
+
+ auto treestore = get_object<Gtk::ListStore>(_builder, "liststore");
+ _selector.set_row_separator_func([=](const Glib::RefPtr<Gtk::TreeModel>&, const Gtk::TreeModel::iterator& it){
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+ return id == "-";
+ });
+ _categories = Gtk::TreeModelFilter::create(treestore);
+ _categories->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+ return id == "-" || is_resource_present(id, _stats);
+ });
+ _selector.set_model(_categories);
+ auto icon_renderer = manage(new Inkscape::UI::Widget::CellRendererItemIcon());
+ _selector.insert_column("", *icon_renderer, 0);
+ auto column = _selector.get_column(0);
+ column->add_attribute(*icon_renderer, icon_renderer->property_shape_type().get_name(), COL_ICON);
+ auto count_renderer = Gtk::make_managed<Gtk::CellRendererText>();
+ auto count_column = _selector.get_column(_selector.append_column("", *count_renderer) - 1);
+ count_column->add_attribute(*count_renderer, "text", COL_COUNT);
+ count_column->set_cell_data_func(*count_renderer, [=](Gtk::CellRenderer* r, const Gtk::TreeModel::iterator& it){
+ uint64_t count;
+ it->get_value(COL_COUNT, count);
+ count_renderer->property_text().set_value(count > 0 ? std::to_string(count) : "");
+ });
+ count_renderer->set_padding(3, 4);
+
+ _wr.setUpdating(true); // set permanently
+
+ for (auto entity = rdf_work_entities; entity && entity->name; ++entity) {
+ if (entity->editable != RDF_EDIT_GENERIC) continue;
+
+ auto w = Inkscape::UI::Widget::EntityEntry::create(entity, _wr);
+ _rdf_list.push_back(w);
+ }
+
+ _page_selection = _selector.get_selection();
+ _selection_change = _page_selection->signal_changed().connect([=](){
+ if (auto it = _page_selection->get_selected()) {
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+ select_page(id);
+ }
+ });
+
+ auto paned = &get_widget<Gtk::Paned>(_builder, "paned");
+ auto move = [=](){
+ auto pos = paned->get_position();
+ get_widget<Gtk::Label>(_builder, "spacer").set_size_request(pos);
+ };
+ paned->property_position().signal_changed().connect([=](){ move(); });
+ move();
+
+ _edit.signal_clicked().connect([=](){
+ auto sel = _iconview.get_selected_items();
+ if (sel.size() == 1) {
+ // todo: investigate why this doesn't work initially:
+ _iconview.set_cursor(sel.front(), true);
+ }
+ else {
+ // treeview todo if needed
+ }
+ });
+
+ // selectable elements can be selected on the canvas;
+ // even elements in <defs> can be selected (same as in XML dialog)
+ _select.signal_clicked().connect([=](){
+ auto document = getDocument();
+ auto desktop = getDesktop();
+ if (!document || !desktop) return;
+
+ if (auto row = selected_item()) {
+ Glib::ustring id = row[g_item_columns.id];
+ if (auto object = document->getObjectById(id)) {
+ // select object
+ desktop->getSelection()->set(object);
+ }
+ }
+ else {
+ // to do: select from treeview if needed
+ }
+ });
+
+ _search.signal_search_changed().connect([=](){
+ filtered_items->freeze_notify();
+ filtered_items->refilter();
+ filtered_items->thaw_notify();
+
+ filtered_info->freeze_notify();
+ filtered_info->refilter();
+ filtered_info->thaw_notify();
+ });
+
+ // filter gridview
+ filtered_items->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ if (_search.get_text_length() == 0) return true;
+
+ auto str = _search.get_text().lowercase();
+ Glib::ustring label = (*it)[g_item_columns.label];
+ return label.lowercase().find(str) != Glib::ustring::npos;
+ });
+ // filter treeview too
+ filtered_info->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ if (_search.get_text_length() == 0) return true;
+
+ auto str = _search.get_text().lowercase();
+ Glib::ustring value = (*it)[g_info_columns.value];
+ return value.lowercase().find(str) != Glib::ustring::npos;
+ });
+
+ _delete.signal_clicked().connect([=](){
+ // delete selected object
+ if (auto row = selected_item()) {
+ SPObject* object = row[g_item_columns.object];
+ delete_object(object, getDesktop()->getSelection());
+ }
+ });
+
+ _extract.signal_clicked().connect([=](){
+ auto window = dynamic_cast<Gtk::Window*>(get_toplevel());
+
+ switch (_showing_resource) {
+ case Images:
+ // extract selected image
+ if (auto row = selected_item()) {
+ SPObject* object = row[g_item_columns.object];
+ extract_image(window, cast<SPImage>(object));
+ }
+ break;
+ case Colors:
+ // export colors into a GIMP palette
+ if (_document) {
+ std::vector<int> colors;
+ _item_store->foreach_iter([&](const Gtk::TreeModel::iterator& it){
+ int c;
+ it->get_value(g_item_columns.color.index(), c);
+ colors.push_back(c);
+ return false; // false means continue
+ });
+ extract_colors(window, colors, _document->getDocumentName());
+ }
+ break;
+ default:
+ // nothing else so far
+ break;
+ }
+ });
+
+ _iconview.signal_selection_changed().connect([=](){
+ update_buttons();
+ });
+
+}
+
+Gtk::TreeModel::Row DocumentResources::selected_item() {
+ auto sel = _iconview.get_selected_items();
+ auto model = _iconview.get_model();
+ Gtk::TreeModel::Row row;
+ if (sel.size() == 1 && model) {
+ row = *model->get_iter(sel.front());
+ }
+ return row;
+}
+
+void DocumentResources::update_buttons() {
+ if (!_iconview.get_visible()) return;
+
+ auto single_sel = !!selected_item();
+
+ _edit.set_sensitive(single_sel);
+ _extract.set_sensitive(single_sel || _showing_resource == Colors);
+ _delete.set_sensitive(single_sel);
+ _select.set_sensitive(single_sel);
+}
+
+Cairo::RefPtr<Cairo::Surface> render_color(uint32_t rgb, double size, double radius, int device_scale) {
+ Cairo::RefPtr<Cairo::Surface> nul;
+ return add_background_to_image(nul, rgb, size / 2, radius, device_scale, 0x7f7f7f00);
+}
+
+void collect_object_colors(SPObject& obj, std::map<std::string, SPColor>& colors) {
+ auto style = obj.style;
+
+ if (style->stroke.set && style->stroke.colorSet) {
+ const auto& c = style->stroke.value.color;
+ colors[c.toString()] = c;
+ }
+
+ if (style->color.set) {
+ const auto& c = style->color.value.color;
+ colors[c.toString()] = c;
+ }
+
+ if (style->fill.set) {
+ const auto& c = style->fill.value.color;
+ colors[c.toString()] = c;
+ }
+
+ if (style->solid_color.set) {
+ const auto& c = style->solid_color.value.color;
+ colors[c.toString()] = c;
+ }
+}
+
+// traverse all nodes starting from given 'object'
+template<typename V>
+void apply_visitor(SPObject& object, V&& visitor) {
+ visitor(object);
+
+ // SPUse inserts referenced object as a child; skip it
+ if (is<SPUse>(&object)) return;
+
+ for (auto&& child : object.children) {
+ apply_visitor(child, visitor);
+ }
+}
+
+std::map<std::string, SPColor> collect_colors(SPObject* object) {
+ std::map<std::string, SPColor> colors;
+ if (object) {
+ apply_visitor(*object, [&](SPObject& obj){ collect_object_colors(obj, colors); });
+ }
+ return colors;
+}
+
+void collect_used_fonts(SPObject& object, std::set<std::string>& fonts) {
+ auto style = object.style;
+
+ if (style->font_specification.set) {
+ auto fspec = style->font_specification.value();
+ if (fspec && *fspec) {
+ fonts.insert(fspec);
+ }
+ }
+ else if (style->font.set) {
+ // some SVG files won't have Inkscape-specific fontspec; read font settings instead
+ auto font = style->font.get_value();
+ if (style->font_style.set) {
+ font += ' ' + style->font_style.get_value();
+ }
+ fonts.insert(font);
+ }
+}
+
+std::set<std::string> collect_fontspecs(SPObject* object) {
+ std::set<std::string> fonts;
+ if (object) {
+ apply_visitor(*object, [&](SPObject& obj){ collect_used_fonts(obj, fonts); });
+ }
+ return fonts;
+}
+
+template<typename T>
+bool filter_element(T& object) { return true; }
+
+template<>
+bool filter_element<SPPattern>(SPPattern& object) { return object.hasChildren(); }
+
+template<>
+bool filter_element<SPGradient>(SPGradient& object) { return object.hasStops(); }
+
+template<typename T>
+std::vector<T*> collect_items(SPObject* object, bool (*filter)(T&) = filter_element<T>) {
+ std::vector<T*> items;
+ if (object) {
+ apply_visitor(*object, [&](SPObject& obj){
+ if (auto t = cast<T>(&obj)) {
+ if (filter(*t)) items.push_back(t);
+ }
+ });
+ }
+ return items;
+}
+
+std::unordered_map<std::string, size_t> collect_styles(SPObject* root) {
+ std::unordered_map<std::string, size_t> map;
+ if (!root) return map;
+
+ apply_visitor(*root, [&](SPObject& obj){
+ if (auto style = obj.getAttribute("style")) {
+ map[style]++;
+ }
+ });
+
+ return map;
+}
+
+bool has_external_ref(SPObject& obj) {
+ bool present = false;
+ if (auto href = Inkscape::getHrefAttribute(*obj.getRepr()).second) {
+ if (*href && *href != '#' && *href != '?') {
+ auto scheme = Glib::uri_parse_scheme(href);
+ // There are tens of schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
+ // TODO: Which ones to collect as external resources?
+ if (scheme == "file" || scheme == "http" || scheme == "https" || scheme.empty()) {
+ present = true;
+ }
+ }
+ }
+ return present;
+}
+
+details::Statistics collect_statistics(SPObject* root) {
+ details::Statistics stats;
+
+ if (!root) {
+ return stats;
+ }
+
+ std::map<std::string, SPColor> colors;
+ std::set<std::string> fonts;
+
+ apply_visitor(*root, [&](SPObject& obj){
+ // order of tests is important; derived classes first, before base,
+ // so meshgradient first, gradient next
+
+ if (auto pattern = cast<SPPattern>(&obj)) {
+ if (filter_element(*pattern)) {
+ stats.patterns++;
+ }
+ }
+ else if (is<SPMeshGradient>(&obj)) {
+ stats.meshgradients++;
+ }
+ else if (auto gradient = cast<SPGradient>(&obj)) {
+ if (filter_element(*gradient)) {
+ if (gradient->isSwatch()) {
+ stats.swatches++;
+ }
+ else {
+ stats.gradients++;
+ }
+ }
+ }
+ else if (auto marker = cast<SPMarker>(&obj)) {
+ if (filter_element(*marker)) {
+ stats.markers++;
+ }
+ }
+ else if (auto symbol = cast<SPSymbol>(&obj)) {
+ if (filter_element(*symbol)) {
+ stats.symbols++;
+ }
+ }
+ else if (is<SPFont>(&obj)) { // SVG font
+ stats.svg_fonts++;
+ }
+ else if (is<SPImage>(&obj)) {
+ stats.images++;
+ }
+ else if (auto group = cast<SPGroup>(&obj)) {
+ if (strcmp(group->getRepr()->name(), "svg:g") == 0) {
+ switch (group->layerMode()) {
+ case SPGroup::GROUP:
+ stats.groups++;
+ break;
+ case SPGroup::LAYER:
+ stats.layers++;
+ break;
+ }
+ }
+ }
+ else if (is<SPPath>(&obj)) {
+ stats.paths++;
+ }
+ else if (is<SPFilter>(&obj)) {
+ stats.filters++;
+ }
+ else if (is<ColorProfile>(&obj)) {
+ stats.colorprofiles++;
+ }
+
+ if (auto style = obj.getAttribute("style")) {
+ if (*style) stats.styles++;
+ }
+
+ if (has_external_ref(obj)) {
+ stats.external_uris++;
+ }
+
+ collect_object_colors(obj, colors);
+ collect_used_fonts(obj, fonts);
+
+ // verify:
+ stats.nodes++;
+ });
+
+ stats.colors = colors.size();
+ stats.fonts = fonts.size();
+
+ return stats;
+}
+
+details::Statistics DocumentResources::collect_statistics() {
+
+ auto root = _document ? _document->getRoot() : nullptr;
+ auto stats = ::Inkscape::UI::Dialog::collect_statistics(root);
+
+ if (_document) {
+ for (auto& el : _rdf_list) {
+ bool read_only = true;
+ el.update(_document, read_only);
+ if (!el.content().empty()) stats.metadata++;
+ }
+ }
+
+ return stats;
+}
+
+void DocumentResources::rebuild_stats() {
+ _stats = collect_statistics();
+
+ if (auto desktop = getDesktop()) {
+ _wr.setDesktop(desktop);
+ }
+
+ _categories->refilter();
+ _categories->foreach_iter([=](const Gtk::TreeModel::iterator& it){
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+ auto count = get_resource_count(id, _stats);
+ if (id == "stats") count = 0; // don't show count 1 for "overview"
+ it->set_value(COL_COUNT, count);
+ return false; // false means continue
+ });
+ _selector.columns_autosize();
+}
+
+void DocumentResources::documentReplaced() {
+ _document = getDocument();
+ if (_document) {
+ _document_modified = _document->connectModified([=](unsigned){
+ // brute force refresh, but throttled
+ _idle_refresh = Glib::signal_timeout().connect([=](){
+ rebuild_stats();
+ refresh_current_page();
+ return false;
+ }, 200);
+ });
+ }
+ else {
+ _document_modified.disconnect();
+ }
+
+ rebuild_stats();
+ refresh_current_page();
+}
+
+void DocumentResources::refresh_current_page() {
+ auto page = _cur_page_id;
+ if (!is_resource_present(page, _stats)) {
+ page = "stats";
+ }
+ auto model = _selector.get_model();
+
+ model->foreach([=](const Gtk::TreeModel::Path& path, const Gtk::TreeModel::iterator& it) {
+ Glib::ustring id;
+ it->get_value(COL_ID, id);
+
+ if (id == page) {
+ _page_selection->select(path);
+ refresh_page(id);
+ return true;
+ }
+ return false;
+ });
+}
+
+void DocumentResources::selectionModified(Inkscape::Selection* selection, guint flags) {
+ // no op so far
+}
+
+auto get_id = [](const SPObject* object) { auto id = object->getId(); return id ? id : ""; };
+auto label_fmt = [](const char* label, const Glib::ustring& id) { return label && *label ? label : '#' + id; };
+
+void add_colors(Glib::RefPtr<Gtk::ListStore> item_store, const std::map<std::string, SPColor>& colors, int device_scale) {
+ for (auto&& it : colors) {
+ const auto& color = it.second;
+
+ auto row = *item_store->append();
+ auto name = color.toString();
+ auto rgba32 = color.toRGBA32(0xff);
+ auto rgb24 = rgba32 >> 8;
+
+ row[g_item_columns.id] = name;
+ row[g_item_columns.label] = name;
+ row[g_item_columns.color] = rgb24;
+ int size = 20;
+ double radius = 2.0;
+ row[g_item_columns.image] = render_color(rgba32, size, radius, device_scale);
+ row[g_item_columns.object] = nullptr;
+ }
+}
+
+void _add_items_with_images(Glib::RefPtr<Gtk::ListStore> item_store, const std::vector<SPObject*>& items, double width, double height, int device_scale, bool use_title, object_renderer::options opt) {
+ object_renderer renderer;
+ item_store->freeze_notify();
+
+ for (auto item : items) {
+ auto row = *item_store->append();
+
+ auto id = get_id(item);
+ row[g_item_columns.id] = id;
+
+ if (use_title) {
+ auto title = item->title();
+ row[g_item_columns.label] = label_fmt(title, id);
+ g_free(title);
+ }
+ else {
+ auto label = item->getAttribute("inkscape:label");
+ row[g_item_columns.label] = label_fmt(label, id);
+ }
+ row[g_item_columns.image] = renderer.render(*item, width, height, device_scale, opt);
+ row[g_item_columns.object] = item;
+ }
+
+ item_store->thaw_notify();
+}
+
+template<typename T>
+void add_items_with_images(Glib::RefPtr<Gtk::ListStore> item_store, const std::vector<T*>& items, double width, double height, int device_scale, bool use_title = false, object_renderer::options opt = {}) {
+ static_assert(std::is_base_of<SPObject, T>::value);
+ _add_items_with_images(item_store, reinterpret_cast<const std::vector<SPObject*>&>(items), width, height, device_scale, use_title, opt);
+}
+
+void add_fonts(Glib::RefPtr<Gtk::ListStore> store, const std::set<std::string>& fontspecs) {
+ size_t i = 1;
+ for (auto&& fs : fontspecs) {
+ auto row = *store->append();
+ row[g_info_columns.item] = Glib::ustring::compose("%1 %2", _("Font"), i++);
+ auto name = Glib::Markup::escape_text(fs);
+ row[g_info_columns.value] = Glib::ustring::format(
+ "<span allow_breaks='false' size='xx-large' font='", fs, "'>", name, "</span>\n",
+ "<span allow_breaks='false' size='small' alpha='60%'>", name, "</span>"
+ );
+ }
+}
+
+void add_stats(Glib::RefPtr<Gtk::ListStore> info_store, SPDocument* document, const details::Statistics& stats) {
+ auto read_only = true;
+ auto license = document ? rdf_get_license(document, read_only) : nullptr;
+
+ std::pair<const char*, std::string> str[] = {
+ {_("Document"), document && document->getDocumentFilename() ? document->getDocumentFilename() : "-"},
+ {_("License"), license && license->name ? license->name : "-"},
+ {_("Metadata"), stats.metadata > 0 ? C_("Adjective for Metadata status", "Present") : "-"},
+ };
+ for (auto& pair : str) {
+ auto row = *info_store->append();
+ row[g_info_columns.item] = pair.first;
+ row[g_info_columns.value] = Glib::Markup::escape_text(pair.second);
+ }
+
+ std::pair<const char*, size_t> kv[] = {
+ {_("Colors"), stats.colors},
+ {_("Color profiles"), stats.colorprofiles},
+ {_("Swatches"), stats.swatches},
+ {_("Fonts"), stats.fonts},
+ {_("Gradients"), stats.gradients},
+ {_("Mesh gradients"), stats.meshgradients},
+ {_("Patterns"), stats.patterns},
+ {_("Symbols"), stats.symbols},
+ {_("Markers"), stats.markers},
+ {_("Filters"), stats.filters},
+ {_("Images"), stats.images},
+ {_("SVG fonts"), stats.svg_fonts},
+ {_("Layers"), stats.layers},
+ {_("Total elements"), stats.nodes},
+ {_("Groups"), stats.groups},
+ {_("Paths"), stats.paths},
+ {_("External URIs"), stats.external_uris},
+ };
+ for (auto& pair : kv) {
+ auto row = *info_store->append();
+ row[g_info_columns.item] = pair.first;
+ row[g_info_columns.value] = pair.second ? std::to_string(pair.second) : "-";
+ }
+}
+
+void add_metadata(Glib::RefPtr<Gtk::ListStore> info_store, SPDocument* document,
+ const boost::ptr_vector<Inkscape::UI::Widget::EntityEntry>& rdf_list) {
+
+ for (auto& entry : rdf_list) {
+ auto row = *info_store->append();
+ auto label = entry._label.get_label();
+ Util::trim(label, ":");
+ row[g_info_columns.item] = label;
+ row[g_info_columns.value] = Glib::Markup::escape_text(entry.content());
+ }
+}
+
+void add_filters(Glib::RefPtr<Gtk::ListStore> info_store, const std::vector<SPFilter*>& filters) {
+ for (auto& filter : filters) {
+ auto row = *info_store->append();
+ auto label = filter->getAttribute("inkscape:label");
+ auto name = Glib::ustring(label ? label : filter->getId());
+ row[g_info_columns.item] = name;
+ std::ostringstream ost;
+ bool first = true;
+ for (auto& obj : filter->children) {
+ if (auto primitive = cast<SPFilterPrimitive>(&obj)) {
+ if (!first) ost << ", ";
+ Glib::ustring name = primitive->getRepr()->name();
+ if (name.find("svg:") != std::string::npos) {
+ name.erase(name.find("svg:"), 4);
+ }
+ ost << name;
+ first = false;
+ }
+ }
+ row[g_info_columns.value] = Glib::Markup::escape_text(ost.str());
+ }
+}
+
+void add_styles(Glib::RefPtr<Gtk::ListStore> info_store, const std::unordered_map<std::string, size_t>& map) {
+ std::vector<std::string> vect;
+ vect.reserve(map.size());
+ for (auto style : map) {
+ vect.emplace_back(style.first);
+ }
+ std::sort(vect.begin(), vect.end());
+ info_store->freeze_notify();
+ int n = 1;
+ for (auto& style : vect) {
+ auto row = *info_store->append();
+ row[g_info_columns.item] = _("Style ") + std::to_string(n++);
+ row[g_info_columns.count] = map.find(style)->second;
+ row[g_info_columns.value] = Glib::Markup::escape_text(style);
+ }
+ info_store->thaw_notify();
+}
+
+void add_refs(Glib::RefPtr<Gtk::ListStore> info_store, const std::vector<SPObject*>& objects) {
+ info_store->freeze_notify();
+ for (auto& obj : objects) {
+ auto href = Inkscape::getHrefAttribute(*obj->getRepr()).second;
+ if (!href) continue;
+
+ auto row = *info_store->append();
+ row[g_info_columns.item] = label_fmt(nullptr, get_id(obj));
+ row[g_info_columns.value] = href;
+ row[g_info_columns.object] = obj;
+ }
+ info_store->thaw_notify();
+}
+
+void DocumentResources::select_page(const Glib::ustring& id) {
+ if (_cur_page_id == id) return;
+
+ _cur_page_id = id;
+ refresh_page(id);
+}
+
+void DocumentResources::clear_stores() {
+ _item_store->freeze_notify();
+ _item_store->clear();
+ _item_store->thaw_notify();
+
+ _info_store->freeze_notify();
+ _info_store->clear();
+ _info_store->thaw_notify();
+}
+
+void DocumentResources::refresh_page(const Glib::ustring& id) {
+ auto rsrc = id_to_resource(id);
+
+ // GTK spits out a lot of warnings and errors from filtered model.
+ // I don't know how to fix them.
+ // https://gitlab.gnome.org/GNOME/gtk/-/issues/1150
+ // Clear sorting? Remove filtering?
+ // GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID
+
+ clear_stores();
+
+ auto root = _document ? _document->getRoot() : nullptr;
+ auto defs = _document ? _document->getDefs() : nullptr;
+
+ int device_scale = get_scale_factor();
+ auto tab = "iconview";
+ auto has_count = false;
+ auto item_width = 90;
+ auto context = get_style_context();
+ Gdk::RGBA color = context->get_color(get_state_flags());
+ auto label_editable = false;
+ auto items_selectable = true;
+ auto can_delete = false; // enable where supported
+ auto can_extract = false;
+
+ switch (rsrc) {
+ case Colors:
+ add_colors(_item_store, collect_colors(root), device_scale);
+ item_width = 70;
+ items_selectable = false; // to do: make selectable?
+ can_extract = true;
+ break;
+
+ case Symbols:
+ {
+ auto opt = object_renderer::options();
+ if (INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container*>(this))) {
+ // white background for typically black symbols, so they don't disappear in a dark theme
+ opt.solid_background(0xf0f0f0ff, 3, 3);
+ }
+ opt.symbol_style_from_use();
+ add_items_with_images(_item_store, collect_items<SPSymbol>(defs), 70, 60, device_scale, true, opt);
+ }
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Patterns:
+ add_items_with_images(_item_store, collect_items<SPPattern>(defs), 80, 70, device_scale);
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Markers:
+ add_items_with_images(_item_store, collect_items<SPMarker>(defs), 70, 60, device_scale, false,
+ object_renderer::options().foreground(color));
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Gradients:
+ add_items_with_images(_item_store,
+ collect_items<SPGradient>(defs, [](auto& g){ return filter_element(g) && !g.isSwatch(); }),
+ 180, 22, device_scale);
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Swatches:
+ add_items_with_images(_item_store,
+ collect_items<SPGradient>(defs, [](auto& g){ return filter_element(g) && g.isSwatch(); }),
+ 100, 22, device_scale);
+ label_editable = true;
+ can_delete = true;
+ break;
+
+ case Fonts:
+ add_fonts(_info_store, collect_fontspecs(root));
+ tab = "treeview";
+ items_selectable = false;
+ break;
+
+ case Filters:
+ add_filters(_info_store, collect_items<SPFilter>(defs));
+ label_editable = true;
+ tab = "treeview";
+ items_selectable = false; // to do: make selectable
+ break;
+
+ case Styles:
+ add_styles(_info_store, collect_styles(root));
+ tab = "treeview";
+ has_count = true;
+ items_selectable = false; // to do: make selectable?
+ break;
+
+ case Images:
+ add_items_with_images(_item_store, collect_items<SPImage>(root), 110, 110, device_scale);
+ label_editable = true;
+ can_extract = true;
+ can_delete = true;
+ break;
+
+ case External:
+ add_refs(_info_store, collect_items<SPObject>(root, [](auto& obj){ return has_external_ref(obj); }));
+ tab = "treeview";
+ items_selectable = false; // to do: make selectable
+ break;
+
+ case Stats:
+ add_stats(_info_store, _document, _stats);
+ tab = "treeview";
+ items_selectable = false;
+ break;
+
+ case Metadata:
+ add_metadata(_info_store, _document, _rdf_list);
+ tab = "treeview";
+ items_selectable = false;
+ break;
+ }
+
+ _showing_resource = rsrc;
+
+ _treeview.get_column(1)->set_visible(has_count);
+ _label_renderer->property_editable() = label_editable;
+ widget_show(_edit, label_editable);
+ widget_show(_select, items_selectable);
+ widget_show(_delete, can_delete);
+ widget_show(_extract, can_extract);
+
+ _iconview.set_item_width(item_width);
+ get_widget<Gtk::Stack>(_builder, "stack").set_visible_child(tab);
+ update_buttons();
+}
+
+void DocumentResources::start_editing(Gtk::CellEditable* cell, const Glib::ustring& path) {
+ auto entry = dynamic_cast<Gtk::Entry*>(cell);
+ entry->set_has_frame();
+}
+
+void DocumentResources::end_editing(const Glib::ustring& path, const Glib::ustring& new_text) {
+ auto model = _iconview.get_model();
+ Gtk::TreeModel::Row row = *model->get_iter(path);
+ if (!row) return;
+
+ SPObject* object = row[g_item_columns.object];
+ if (!object) {
+ g_warning("Missing object ptr, cannot edit object's name.");
+ return;
+ }
+
+ // try object-specific edit functions first; if not present fall back to generic
+ auto getter = g_get_label[typeid(*object)];
+ auto setter = g_set_label[typeid(*object)];
+ if (!getter || !setter) {
+ getter = g_get_label[typeid(SPObject)];
+ setter = g_set_label[typeid(SPObject)];
+ }
+
+ auto name = getter(*object);
+ if (new_text == name) return;
+
+ setter(*object, new_text);
+
+ auto id = get_id(object);
+ row[g_item_columns.label] = label_fmt(new_text.c_str(), id);
+
+ if (auto document = object->document) {
+ DocumentUndo::done(document, _("Edit object title"), INKSCAPE_ICON("document-resources"));
+ }
+}
+
+} } } // namespaces
diff --git a/src/ui/dialog/document-resources.h b/src/ui/dialog/document-resources.h
new file mode 100644
index 0000000..bf670cd
--- /dev/null
+++ b/src/ui/dialog/document-resources.h
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A simple dialog for previewing document resources
+ *
+ * Copyright (C) 2023 Michael Kowalski
+ */
+
+#ifndef SEEN_DOC_RESOURCES_H
+#define SEEN_DOC_RESOURCES_H
+
+#include "document.h"
+#include "helper/auto-connection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/entity-entry.h"
+#include "ui/widget/registry.h"
+#include <cstddef>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/treeview.h>
+#include <memory>
+#include <string>
+#include <boost/ptr_container/ptr_vector.hpp>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+namespace details {
+ struct Statistics {
+ size_t nodes = 0;
+ size_t groups = 0;
+ size_t layers = 0;
+ size_t paths = 0;
+ size_t images = 0;
+ size_t patterns = 0;
+ size_t symbols = 0;
+ size_t markers = 0;
+ size_t fonts = 0;
+ size_t filters = 0;
+ size_t svg_fonts = 0;
+ size_t colors = 0;
+ size_t gradients = 0;
+ size_t swatches = 0;
+ size_t metadata = 0;
+ size_t styles = 0;
+ size_t meshgradients = 0;
+ size_t colorprofiles = 0;
+ size_t external_uris = 0;
+ };
+}
+
+class DocumentResources : public DialogBase {
+public:
+ DocumentResources();
+
+private:
+ void documentReplaced() override;
+ void select_page(const Glib::ustring& id);
+ void refresh_page(const Glib::ustring& id);
+ void refresh_current_page();
+ void rebuild_stats();
+ details::Statistics collect_statistics();
+ void start_editing(Gtk::CellEditable* cell, const Glib::ustring& path);
+ void end_editing(const Glib::ustring& path, const Glib::ustring& new_text);
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+ void update_buttons();
+ Gtk::TreeModel::Row selected_item();
+ void clear_stores();
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Glib::RefPtr<Gtk::ListStore> _item_store;
+ Glib::RefPtr<Gtk::TreeModelFilter> _categories;
+ Glib::RefPtr<Gtk::ListStore> _info_store;
+ Gtk::CellRendererPixbuf _image_renderer;
+ SPDocument* _document = nullptr;
+ auto_connection _selection_change;
+ details::Statistics _stats;
+ std::string _cur_page_id; // the last category that user selected
+ int _showing_resource = -1; // ID of the resource that's currently presented
+ Glib::RefPtr<Gtk::TreeSelection> _page_selection;
+ Gtk::IconView& _iconview;
+ Gtk::TreeView& _treeview;
+ Gtk::TreeView& _selector;
+ Gtk::Button& _edit;
+ Gtk::Button& _select;
+ Gtk::Button& _delete;
+ Gtk::Button& _extract;
+ Gtk::SearchEntry& _search;
+ boost::ptr_vector<Inkscape::UI::Widget::EntityEntry> _rdf_list;
+ UI::Widget::Registry _wr;
+ Gtk::CellRendererText* _label_renderer;
+ auto_connection _document_modified;
+ auto_connection _idle_refresh;
+};
+
+} } } // namespaces
+
+#endif // SEEN_DOC_RESOURCES_H
diff --git a/src/ui/dialog/export-batch.cpp b/src/ui/dialog/export-batch.cpp
new file mode 100644
index 0000000..43f2b96
--- /dev/null
+++ b/src/ui/dialog/export-batch.cpp
@@ -0,0 +1,830 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/convert.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <gtkmm.h>
+#include <png.h>
+#include <regex>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "extension/db.h"
+#include "extension/output.h"
+#include "file.h"
+#include "helper/auto-connection.h"
+#include "helper/png-write.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "layer-manager.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/export.h"
+#include "ui/dialog/export-batch.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/interface.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/export-lists.h"
+#include "ui/widget/export-preview.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/unit-menu.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+BatchItem::BatchItem(SPItem *item, std::shared_ptr<PreviewDrawing> drawing)
+{
+ _item = item;
+ init(drawing);
+ _object_modified_conn = _item->connectModified([=](SPObject *obj, unsigned int flags) {
+ update_label();
+ });
+ update_label();
+}
+
+BatchItem::BatchItem(SPPage *page, std::shared_ptr<PreviewDrawing> drawing)
+{
+ _page = page;
+ init(drawing);
+ _object_modified_conn = _page->connectModified([=](SPObject *obj, unsigned int flags) {
+ update_label();
+ });
+ update_label();
+}
+
+void BatchItem::update_label()
+{
+ Glib::ustring label = "no-name";
+ if (_page) {
+ label = _page->getDefaultLabel();
+ if (auto id = _page->label()) {
+ label = id;
+ }
+ } else if (_item) {
+ label = _item->defaultLabel();
+ if (label.empty()) {
+ if (auto _id = _item->getId()) {
+ label = _id;
+ } else {
+ label = "no-id";
+ }
+ }
+ }
+ _label_str = label;
+ _label.set_text(label);
+ set_tooltip_text(label);
+}
+
+void BatchItem::init(std::shared_ptr<PreviewDrawing> drawing) {
+
+
+ _grid.set_row_spacing(5);
+ _grid.set_column_spacing(5);
+ _grid.set_valign(Gtk::Align::ALIGN_CENTER);
+
+ _selector.set_active(true);
+ _selector.set_can_focus(false);
+ _selector.set_margin_start(2);
+ _selector.set_margin_bottom(2);
+ _selector.set_valign(Gtk::ALIGN_END);
+
+ _option.set_active(false);
+ _option.set_can_focus(false);
+ _option.set_margin_start(2);
+ _option.set_margin_bottom(2);
+ _option.set_valign(Gtk::ALIGN_END);
+
+ _preview.set_name("export_preview_batch");
+ _preview.setItem(_item);
+ _preview.setDrawing(drawing);
+ _preview.setSize(64);
+ _preview.set_halign(Gtk::ALIGN_CENTER);
+ _preview.set_valign(Gtk::ALIGN_CENTER);
+
+ _label.set_width_chars(10);
+ _label.set_ellipsize(Pango::ELLIPSIZE_END);
+ _label.set_halign(Gtk::Align::ALIGN_CENTER);
+
+ set_valign(Gtk::Align::ALIGN_START);
+ set_halign(Gtk::Align::ALIGN_START);
+ add(_grid);
+ show();
+ this->set_can_focus(false);
+
+ _selector.signal_toggled().connect([=]() {
+ set_selected(_selector.get_active());
+ });
+ _option.signal_toggled().connect([=]() {
+ set_selected(_option.get_active());
+ });
+
+ // This initially packs the widgets with a hidden preview.
+ refresh(!is_hide, 0);
+}
+
+/**
+ * Syncronise the FlowBox selection to the active widget activity.
+ */
+void BatchItem::set_selected(bool selected)
+{
+ auto box = dynamic_cast<Gtk::FlowBox *>(get_parent());
+ if (box && selected != is_selected()) {
+ if (selected) {
+ box->select_child(*this);
+ } else {
+ box->unselect_child(*this);
+ }
+ }
+}
+
+/**
+ * Syncronise the FlowBox selection to the existing active widget state.
+ */
+void BatchItem::update_selected()
+{
+ if (auto parent = dynamic_cast<Gtk::FlowBox *>(get_parent()))
+ on_mode_changed(parent->get_selection_mode());
+ if (_selector.get_visible()) {
+ set_selected(_selector.get_active());
+ } else if (_option.get_visible()) {
+ set_selected(_option.get_active());
+ }
+}
+
+/**
+ * A change in the selection mode for the flow box.
+ */
+void BatchItem::on_mode_changed(Gtk::SelectionMode mode)
+{
+ _selector.set_visible(mode == Gtk::SELECTION_MULTIPLE);
+ _option.set_visible(mode == Gtk::SELECTION_SINGLE);
+}
+
+/**
+ * Update the connection to the parent FlowBox
+ */
+void BatchItem::on_parent_changed(Gtk::Widget *previous) {
+ auto parent = dynamic_cast<Gtk::FlowBox *>(get_parent());
+ if (!parent)
+ return;
+
+ _selection_widget_changed_conn = parent->signal_selected_children_changed().connect([=]() {
+ // Syncronise the active widget state to the Flowbox selection.
+ if (_selector.get_visible()) {
+ _selector.set_active(is_selected());
+ } else if (_option.get_visible()) {
+ _option.set_active(is_selected());
+ }
+ });
+ update_selected();
+
+ if (auto first = dynamic_cast<BatchItem *>(parent->get_child_at_index(0))) {
+ auto group = first->get_radio_group();
+ _option.set_group(group);
+ }
+}
+
+
+void BatchItem::refresh(bool hide, guint32 bg_color)
+{
+ if (_page) {
+ _preview.setBox(_page->getDocumentRect());
+ }
+
+ _preview.setBackgroundColor(bg_color);
+
+ // When hiding the preview, we show the items as a checklist
+ // So all items must be packed differently on refresh.
+ if (hide != is_hide) {
+ is_hide = hide;
+ _grid.remove(_selector);
+ _grid.remove(_option);
+ _grid.remove(_label);
+ _grid.remove(_preview);
+
+ if (hide) {
+ _selector.set_valign(Gtk::Align::ALIGN_BASELINE);
+ _label.set_xalign(0.0);
+ _grid.attach(_selector, 0, 1, 1, 1);
+ _grid.attach(_option, 0, 1, 1, 1);
+ _grid.attach(_label, 1, 1, 1, 1);
+ } else {
+ _selector.set_valign(Gtk::Align::ALIGN_END);
+ _label.set_xalign(0.5);
+ _grid.attach(_selector, 0, 1, 1, 1);
+ _grid.attach(_option, 0, 1, 1, 1);
+ _grid.attach(_label, 0, 2, 2, 1);
+ _grid.attach(_preview, 0, 0, 2, 2);
+ }
+ show_all_children();
+ update_selected();
+ }
+
+ if (!hide) {
+ _preview.queueRefresh();
+ }
+}
+
+
+BatchExport::BatchExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder>& builder)
+ : Gtk::Box(cobject) {
+ prefs = Inkscape::Preferences::get();
+
+ builder->get_widget("b_s_selection", selection_buttons[SELECTION_SELECTION]);
+ selection_names[SELECTION_SELECTION] = "selection";
+ builder->get_widget("b_s_layers", selection_buttons[SELECTION_LAYER]);
+ selection_names[SELECTION_LAYER] = "layer";
+ builder->get_widget("b_s_pages", selection_buttons[SELECTION_PAGE]);
+ selection_names[SELECTION_PAGE] = "page";
+
+ builder->get_widget("b_preview_box", preview_container);
+ builder->get_widget("b_show_preview", show_preview);
+ builder->get_widget("b_num_elements", num_elements);
+ builder->get_widget("b_hide_all", hide_all);
+ builder->get_widget("b_filename", filename_entry);
+ builder->get_widget("b_export", export_btn);
+ builder->get_widget("b_cancel", cancel_btn);
+ builder->get_widget("b_inprogress", progress_box);
+
+ builder->get_widget("b_progress", _prog);
+ builder->get_widget("b_progress_batch", _prog_batch);
+ builder->get_widget_derived("b_export_list", export_list);
+
+ Gtk::Button* button = nullptr;
+ builder->get_widget("b_backgnd", button);
+ assert(button);
+ _bgnd_color_picker = std::make_unique<Inkscape::UI::Widget::ColorPicker>(
+ _("Background color"), _("Color used to fill the image background"), 0xffffff00, true, button);
+ setup();
+}
+
+void BatchExport::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ if (!_desktop || _desktop->getSelection() != selection) {
+ return;
+ }
+ if (!(flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
+ return;
+ }
+ queueRefreshItems();
+}
+
+void BatchExport::selectionChanged(Inkscape::Selection *selection)
+{
+ if (!_desktop || _desktop->getSelection() != selection) {
+ return;
+ }
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(!selection->isEmpty());
+ if (selection->isEmpty()) {
+ if (current_key == SELECTION_SELECTION) {
+ selection_buttons[SELECTION_LAYER]->set_active(true); // This causes refresh area
+ // return otherwise refreshArea will be called again
+ // even though we are at default key, selection is the one which was original key.
+ prefs->setString("/dialogs/export/batchexportarea/value", selection_names[SELECTION_SELECTION]);
+ return;
+ }
+ } else {
+ Glib::ustring pref_key_name = prefs->getString("/dialogs/export/batchexportarea/value");
+ if (selection_names[SELECTION_SELECTION] == pref_key_name && current_key != SELECTION_SELECTION) {
+ selection_buttons[SELECTION_SELECTION]->set_active();
+ return;
+ }
+ }
+ queueRefresh();
+}
+
+void BatchExport::pagesChanged()
+{
+ if (!_desktop || !_document) return;
+
+ bool has_pages = _document->getPageManager().hasPages();
+ selection_buttons[SELECTION_PAGE]->set_sensitive(has_pages);
+
+ if (current_key == SELECTION_PAGE && !has_pages) {
+ current_key = SELECTION_LAYER;
+ selection_buttons[SELECTION_LAYER]->set_active();
+ }
+
+ queueRefresh();
+}
+
+// Setup Single Export.Called by export on realize
+void BatchExport::setup()
+{
+ if (setupDone) {
+ return;
+ }
+ setupDone = true;
+
+ export_list->setup();
+
+ // set them before connecting to signals
+ setDefaultSelectionMode();
+ setExporting(false);
+ queueRefresh();
+
+ // Connect Signals
+ for (auto [key, button] : selection_buttons) {
+ button->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &BatchExport::onAreaTypeToggle), key));
+ }
+ show_preview->signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview));
+ filename_conn = filename_entry->signal_changed().connect(sigc::mem_fun(*this, &BatchExport::onFilenameModified));
+ export_conn = export_btn->signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onExport));
+ cancel_conn = cancel_btn->signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onCancel));
+ browse_conn = filename_entry->signal_icon_release().connect(sigc::mem_fun(*this, &BatchExport::onBrowse));
+ hide_all->signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview));
+ _bgnd_color_picker->connectChanged([=](guint32 color){
+ if (_desktop) {
+ Inkscape::UI::Dialog::set_export_bg_color(_desktop->getNamedView(), color);
+ }
+ refreshPreview();
+ });
+}
+
+void BatchExport::refreshItems()
+{
+ if (!_desktop || !_document) return;
+
+ // Create New List of Items
+ std::set<SPItem *> itemsList;
+ std::set<SPPage *, SPPage::PageIndexOrder> pageList;
+ std::set<SPPage *> pageUnsorted;
+
+ char *num_str = nullptr;
+ switch (current_key) {
+ case SELECTION_SELECTION: {
+ auto items = _desktop->getSelection()->items();
+ for (auto i = items.begin(); i != items.end(); ++i) {
+ if (SPItem *item = *i) {
+ // Ignore empty items (empty groups, other bad items)
+ if (item->visualBounds()) {
+ itemsList.insert(item);
+ }
+ }
+ }
+ num_str = g_strdup_printf(ngettext("%d Item", "%d Items", itemsList.size()), (int)itemsList.size());
+ break;
+ }
+ case SELECTION_LAYER: {
+ for (auto layer : _desktop->layerManager().getAllLayers()) {
+ // Ignore empty layers, they have no size.
+ if (layer->geometricBounds()) {
+ itemsList.insert(layer);
+ }
+ }
+ num_str = g_strdup_printf(ngettext("%d Layer", "%d Layers", itemsList.size()), (int)itemsList.size());
+ break;
+ }
+ case SELECTION_PAGE: {
+ for (auto page : _desktop->getDocument()->getPageManager().getPages()) {
+ pageList.insert(page);
+ pageUnsorted.insert(page);
+ }
+ num_str = g_strdup_printf(ngettext("%d Page", "%d Pages", pageList.size()), (int)pageList.size());
+ break;
+ }
+ default:
+ break;
+ }
+ if (num_str) {
+ num_elements->set_text(num_str);
+ g_free(num_str);
+ }
+
+ // Create a list of items which are already present but will be removed as they are not present anymore
+ std::vector<std::string> toRemove;
+ for (auto &[key, val] : current_items) {
+ if (SPItem *item = val->getItem()) {
+ // if item is not present in itemList add it to remove list so that we can remove it
+ auto itemItr = itemsList.find(item);
+ if (itemItr == itemsList.end() || !(*itemItr)->getId() || (*itemItr)->getId() != key) {
+ toRemove.push_back(key);
+ }
+ }
+ if (SPPage *page = val->getPage()) {
+ auto pageItr = pageUnsorted.find(page);
+ if (pageItr == pageUnsorted.end() || !(*pageItr)->getId() || (*pageItr)->getId() != key) {
+ toRemove.push_back(key);
+ }
+ }
+ }
+
+ // now remove all the items
+ for (auto const &key : toRemove) {
+ if (current_items[key]) {
+ preview_container->remove(*current_items[key]);
+ current_items.erase(key);
+ }
+ }
+
+ // now add which were are new
+ for (auto &item : itemsList) {
+ if (auto id = item->getId()) {
+ // If an Item with same Id is already present, Skip
+ if (current_items[id] && current_items[id]->getItem() == item) {
+ continue;
+ }
+ // Add new item to the end of list
+ current_items[id] = std::make_unique<BatchItem>(item, _preview_drawing);
+ preview_container->insert(*current_items[id], -1);
+ current_items[id]->set_selected(true);
+ }
+ }
+ for (auto &page : pageList) {
+ if (auto id = page->getId()) {
+ if (current_items[id] && current_items[id]->getPage() == page) {
+ continue;
+ }
+ current_items[id] = std::make_unique<BatchItem>(page, _preview_drawing);
+ preview_container->insert(*current_items[id], -1);
+ current_items[id]->set_selected(true);
+ }
+ }
+
+ refreshPreview();
+}
+
+void BatchExport::refreshPreview()
+{
+ if (!_desktop) return;
+
+ // For Batch Export we are now hiding all object except current object
+ bool hide = hide_all->get_active();
+ bool preview = show_preview->get_active();
+ preview_container->set_orientation(preview ? Gtk::ORIENTATION_HORIZONTAL : Gtk::ORIENTATION_VERTICAL);
+
+ if (preview) {
+ std::vector<SPItem *> selected;
+ for (auto &[key, val] : current_items) {
+ if (hide) {
+ // Assumption: This will never alternate between these branches in the same
+ // list of current_items. Either it's a selection, layers xor pages.
+ if (auto item = val->getItem()) {
+ selected.push_back(item);
+ } else if (val->getPage()) {
+ auto sels = _desktop->getSelection()->items();
+ selected = std::vector<SPItem *>(sels.begin(), sels.end());
+ break;
+ }
+ }
+ }
+ _preview_drawing->set_shown_items(std::move(selected));
+
+ for (auto &[key, val] : current_items) {
+ val->refresh(!preview, _bgnd_color_picker->get_current_color());
+ }
+ }
+}
+
+void BatchExport::loadExportHints()
+{
+ if (!_desktop) return;
+
+ SPDocument *doc = _desktop->getDocument();
+ auto old_filename = filename_entry->get_text();
+ if (old_filename.empty()) {
+ Glib::ustring filename = doc->getRoot()->getExportFilename();
+ if (filename.empty()) {
+ Glib::ustring filename_entry_text = filename_entry->get_text();
+ Glib::ustring extension = ".png";
+ filename = Export::defaultFilename(doc, original_name, extension);
+ }
+ filename_entry->set_text(filename);
+ filename_entry->set_position(filename.length());
+ doc_export_name = filename;
+ }
+}
+
+// Signals CallBack
+
+void BatchExport::onAreaTypeToggle(selection_mode key)
+{
+ // Prevent executing function twice
+ if (!selection_buttons[key]->get_active()) {
+ return;
+ }
+ // If you have reached here means the current key is active one ( not sure if multiple transitions happen but
+ // last call will change values)
+ current_key = key;
+ prefs->setString("/dialogs/export/batchexportarea/value", selection_names[current_key]);
+
+ queueRefresh();
+}
+
+void BatchExport::onFilenameModified()
+{
+}
+
+void BatchExport::onCancel()
+{
+ interrupted = true;
+ setExporting(false);
+}
+
+void BatchExport::onExport()
+{
+ interrupted = false;
+ if (!_desktop)
+ return;
+
+ // If there are no selected button, simply flash message in status bar
+ int num = current_items.size();
+ if (current_items.size() == 0) {
+ _desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No items selected."));
+ return;
+ }
+
+ setExporting(true);
+
+ // Find and remove any extension from filename so that we can add suffix to it.
+ Glib::ustring filename = filename_entry->get_text();
+ export_list->removeExtension(filename);
+
+ bool hide = hide_all->get_active();
+ auto sels = _desktop->getSelection()->items();
+ std::vector<SPItem *> selected_items(sels.begin(), sels.end());
+
+ // Start Exporting Each Item
+ int num_rows = export_list->get_rows();
+ for (int j = 0; j < num_rows && !interrupted; j++) {
+
+ auto suffix = export_list->get_suffix(j);
+ auto ext = export_list->getExtension(j);
+ float dpi = export_list->get_dpi(j);
+
+ if (!ext || ext->deactivated()) {
+ continue;
+ }
+
+ int count = 0;
+ for (auto i = current_items.begin(); i != current_items.end() && !interrupted; ++i) {
+ count++;
+
+ auto &batchItem = i->second;
+ if (!batchItem->is_selected()) {
+ continue;
+ }
+
+ SPItem *item = batchItem->getItem();
+ SPPage *page = batchItem->getPage();
+
+ std::vector<SPItem *> show_only;
+ Geom::Rect area;
+ if (item) {
+ if (auto bounds = item->documentVisualBounds()) {
+ area = *bounds;
+ } else {
+ continue;
+ }
+ show_only.emplace_back(item);
+ } else if (page) {
+ area = page->getDesktopRect();
+ show_only = selected_items; // Maybe stuff here
+ } else {
+ continue;
+ }
+
+ Glib::ustring id = batchItem->getLabel();
+ if (id.empty()) {
+ continue;
+ }
+
+ Glib::ustring item_filename = filename;
+ if (!filename.empty()) {
+ Glib::ustring::value_type last_char = filename.at(filename.length() - 1);
+ if (last_char != '/' && last_char != '\\') {
+ item_filename += "_";
+ }
+ }
+ if (id.at(0) == '#' && batchItem->getItem() && !batchItem->getItem()->label()) {
+ item_filename += id.substr(1);
+ } else {
+ item_filename += id;
+ }
+
+ if (!suffix.empty()) {
+ if (ext->is_raster()) {
+ // Put the dpi in at the user's requested location.
+ suffix = std::regex_replace(suffix.c_str(), std::regex("\\{dpi\\}"), std::to_string((int)dpi));
+ }
+ item_filename = item_filename + "_" + suffix;
+ }
+
+ bool found = Export::unConflictFilename(_document, item_filename, ext->get_extension());
+ if (!found) {
+ continue;
+ }
+
+ // Set the progress bar with our updated information
+ double progress = (((double)count / num) + j) / num_rows;
+ _prog_batch->set_fraction(progress);
+
+ setExporting(true,
+ Glib::ustring::compose(_("Exporting %1"), item_filename),
+ Glib::ustring::compose(_("Format %1, Selection %2"), j + 1, count));
+
+
+ if (ext->is_raster()) {
+ unsigned long int width = (int)(area.width() * dpi / DPI_BASE + 0.5);
+ unsigned long int height = (int)(area.height() * dpi / DPI_BASE + 0.5);
+
+ Export::exportRaster(
+ area, width, height, dpi, _bgnd_color_picker->get_current_color(),
+ item_filename, true, onProgressCallback, this, ext, hide ? &show_only : nullptr);
+ } else {
+ auto copy_doc = _document->copy();
+ Export::exportVector(ext, copy_doc.get(), item_filename, true, show_only, page);
+ }
+ }
+ }
+ // Do this right at the end to finish up
+ setExporting(false);
+}
+
+void BatchExport::onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev)
+{
+ if (!_app || !_app->get_active_window()) {
+ return;
+ }
+ Gtk::Window *window = _app->get_active_window();
+ browse_conn.block();
+ Glib::ustring filename = Glib::filename_from_utf8(filename_entry->get_text());
+
+ if (filename.empty()) {
+ filename = Export::defaultFilename(_document, filename, ".png");
+ }
+
+ Inkscape::UI::Dialog::FileSaveDialog *dialog = Inkscape::UI::Dialog::FileSaveDialog::create(
+ *window, filename, Inkscape::UI::Dialog::EXPORT_TYPES, _("Select a filename for exporting"), "", "",
+ Inkscape::Extension::FILE_SAVE_METHOD_EXPORT);
+
+ if (dialog->show()) {
+ filename = dialog->getFilename();
+ // Remove extension and don't add a new one, for obvious reasons.
+ export_list->removeExtension(filename);
+
+ filename_entry->set_text(filename);
+ filename_entry->set_position(filename.length());
+
+ // deleting dialog before exporting is important
+ // proper delete function should be made for dialog IMO
+ delete dialog;
+ } else {
+ delete dialog;
+ }
+ browse_conn.unblock();
+}
+
+void BatchExport::setDefaultSelectionMode()
+{
+ current_key = (selection_mode)0; // default key
+ bool found = false;
+ Glib::ustring pref_key_name = prefs->getString("/dialogs/export/batchexportarea/value");
+ for (auto [key, name] : selection_names) {
+ if (pref_key_name == name) {
+ current_key = key;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ pref_key_name = selection_names[current_key];
+ }
+ if (_desktop) {
+ if (auto _sel = _desktop->getSelection()) {
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(!_sel->isEmpty());
+ }
+ selection_buttons[SELECTION_PAGE]->set_sensitive(_document->getPageManager().hasPages());
+ }
+ if (!selection_buttons[current_key]->get_sensitive()) {
+ current_key = SELECTION_LAYER;
+ }
+ selection_buttons[current_key]->set_active(true);
+
+ // we need to set pref key because signals above will set set pref == current key but we sometimes change
+ // current key like selection key
+ prefs->setString("/dialogs/export/batchexportarea/value", pref_key_name);
+}
+
+void BatchExport::setExporting(bool exporting, Glib::ustring const &text, Glib::ustring const &text_batch)
+{
+ if (exporting) {
+ set_sensitive(false);
+ set_opacity(0.2);
+ progress_box->show();
+ _prog->set_text(text);
+ _prog->set_fraction(0.0);
+ _prog_batch->set_text(text_batch);
+ } else {
+ set_sensitive(true);
+ set_opacity(1.0);
+ progress_box->hide();
+ _prog->set_text("");
+ _prog->set_fraction(0.0);
+ _prog_batch->set_text("");
+ }
+}
+
+unsigned int BatchExport::onProgressCallback(float value, void *data)
+{
+ if (auto bi = static_cast<BatchExport *>(data)) {
+ bi->_prog->set_fraction(value);
+ Gtk::Main::iteration(false);
+ return !bi->interrupted;
+ }
+ return false;
+}
+
+void BatchExport::setDesktop(SPDesktop *desktop)
+{
+ if (desktop != _desktop) {
+ _pages_changed_connection.disconnect();
+ _desktop = desktop;
+ }
+}
+
+void BatchExport::setDocument(SPDocument *document)
+{
+ if (!_desktop) {
+ document = nullptr;
+ }
+ if (_document == document)
+ return;
+
+ _document = document;
+ _pages_changed_connection.disconnect();
+ if (document) {
+ // when the page selected is changed, update the export area
+ _pages_changed_connection = document->getPageManager().connectPagesChanged([=]() { pagesChanged(); });
+
+ auto bg_color = get_export_bg_color(document->getNamedView(), 0xffffff00);
+ _bgnd_color_picker->setRgba32(bg_color);
+ _preview_drawing = std::make_shared<PreviewDrawing>(document);
+ } else {
+ _preview_drawing.reset();
+ }
+
+ refreshItems();
+}
+
+void BatchExport::queueRefreshItems()
+{
+ if (refresh_items_conn) {
+ return;
+ }
+ // Asynchronously refresh the preview
+ refresh_items_conn = Glib::signal_idle().connect([this] {
+ refreshItems();
+ return false;
+ }, Glib::PRIORITY_HIGH);
+}
+
+void BatchExport::queueRefresh()
+{
+ if (refresh_conn) {
+ return;
+ }
+ refresh_conn = Glib::signal_idle().connect([this] {
+ refreshItems();
+ loadExportHints();
+ return false;
+ }, Glib::PRIORITY_HIGH);
+}
+
+} // 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/dialog/export-batch.h b/src/ui/dialog/export-batch.h
new file mode 100644
index 0000000..78c6d80
--- /dev/null
+++ b/src/ui/dialog/export-batch.h
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SP_EXPORT_BATCH_H
+#define SP_EXPORT_BATCH_H
+
+#include <gtkmm.h>
+
+#include "helper/auto-connection.h"
+#include "ui/widget/export-preview.h"
+#include "ui/widget/scrollprotected.h"
+
+
+class ExportProgressDialog;
+class InkscapeApplication;
+class SPDesktop;
+class SPDocument;
+class SPItem;
+class SPPage;
+
+namespace Inkscape {
+class Preferences;
+class Selection;
+
+namespace UI {
+
+namespace Widget {
+class ColorPicker;
+} // namespace Widget
+
+namespace Dialog {
+
+class ExportList;
+
+class BatchItem : public Gtk::FlowBoxChild
+{
+public:
+ BatchItem(SPItem *item, std::shared_ptr<PreviewDrawing> drawing);
+ BatchItem(SPPage *page, std::shared_ptr<PreviewDrawing> drawing);
+ ~BatchItem() override = default;
+
+ Glib::ustring getLabel() { return _label_str; }
+ SPItem *getItem() { return _item; }
+ SPPage *getPage() { return _page; }
+ void refresh(bool hide, guint32 bg_color);
+ void setDrawing(std::shared_ptr<PreviewDrawing> drawing) { _preview.setDrawing(drawing); }
+
+ auto get_radio_group() { return _option.get_group(); }
+ void on_parent_changed(Gtk::Widget *) override;
+ void on_mode_changed(Gtk::SelectionMode mode);
+ void set_selected(bool selected);
+ void update_selected();
+
+private:
+ void init(std::shared_ptr<PreviewDrawing> drawing);
+ void update_label();
+
+ Glib::ustring _label_str;
+ Gtk::Grid _grid;
+ Gtk::Label _label;
+ Gtk::CheckButton _selector;
+ Gtk::RadioButton _option;
+ ExportPreview _preview;
+ SPItem *_item = nullptr;
+ SPPage *_page = nullptr;
+ bool is_hide = false;
+
+ auto_connection _selection_widget_changed_conn;
+ auto_connection _object_modified_conn;
+};
+
+class BatchExport : public Gtk::Box
+{
+public:
+ BatchExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder>& builder);
+ ~BatchExport() override = default;
+
+ void setApp(InkscapeApplication *app) { _app = app; }
+ void setDocument(SPDocument *document);
+ void setDesktop(SPDesktop *desktop);
+ void selectionChanged(Inkscape::Selection *selection);
+ void selectionModified(Inkscape::Selection *selection, guint flags);
+ void pagesChanged();
+ void queueRefreshItems();
+ void queueRefresh();
+
+private:
+ enum selection_mode
+ {
+ SELECTION_LAYER = 0, // Default is alaways placed first
+ SELECTION_SELECTION,
+ SELECTION_PAGE,
+ };
+
+ typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton;
+
+ InkscapeApplication *_app;
+ SPDesktop *_desktop = nullptr;
+ SPDocument *_document = nullptr;
+ std::shared_ptr<PreviewDrawing> _preview_drawing;
+ bool setupDone = false; // To prevent setup() call add connections again.
+
+ std::map<selection_mode, Gtk::RadioButton *> selection_buttons;
+ Gtk::FlowBox *preview_container = nullptr;
+ Gtk::CheckButton *show_preview = nullptr;
+ Gtk::Label *num_elements = nullptr;
+ Gtk::CheckButton *hide_all = nullptr;
+ Gtk::Entry *filename_entry = nullptr;
+ Gtk::Button *export_btn = nullptr;
+ Gtk::Button *cancel_btn = nullptr;
+ Gtk::ProgressBar *_prog = nullptr;
+ Gtk::ProgressBar *_prog_batch = nullptr;
+ ExportList *export_list = nullptr;
+ Gtk::Widget *progress_box = nullptr;
+
+ // Store all items to be displayed in flowbox
+ std::map<std::string, std::unique_ptr<BatchItem>> current_items;
+
+ Glib::ustring original_name;
+ Glib::ustring doc_export_name;
+
+ Inkscape::Preferences *prefs = nullptr;
+ std::map<selection_mode, Glib::ustring> selection_names;
+ selection_mode current_key;
+
+ // initialise variables from builder
+ void initialise(const Glib::RefPtr<Gtk::Builder> &builder);
+ void setup();
+ void setDefaultSelectionMode();
+ void onFilenameModified();
+ void onAreaTypeToggle(selection_mode key);
+ void onExport();
+ void onCancel();
+ void onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev);
+
+ void refreshPreview();
+ void refreshItems();
+ void loadExportHints();
+
+ void setExporting(bool exporting, Glib::ustring const &text = "", Glib::ustring const &test_batch = "");
+
+ static unsigned int onProgressCallback(float value, void *);
+
+ bool interrupted;
+
+ // Gtk Signals
+ auto_connection filename_conn;
+ auto_connection export_conn;
+ auto_connection cancel_conn;
+ auto_connection browse_conn;
+ auto_connection refresh_conn;
+ auto_connection refresh_items_conn;
+ // SVG Signals
+ auto_connection _pages_changed_connection;
+
+ std::unique_ptr<Inkscape::UI::Widget::ColorPicker> _bgnd_color_picker;
+};
+} // 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/dialog/export-single.cpp b/src/ui/dialog/export-single.cpp
new file mode 100644
index 0000000..c37548c
--- /dev/null
+++ b/src/ui/dialog/export-single.cpp
@@ -0,0 +1,1058 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "export-single.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-root.h"
+#include "object/sp-page.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/export.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-names.h"
+#include "ui/interface.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/export-lists.h"
+#include "ui/widget/export-preview.h"
+#include "ui/dialog/export-batch.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/unit-menu.h"
+#ifdef _WIN32
+
+#endif
+
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+SingleExport::SingleExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder>& builder)
+ : Gtk::Box(cobject) {
+ prefs = Inkscape::Preferences::get();
+
+ builder->get_widget("si_s_document", selection_buttons[SELECTION_DRAWING]);
+ selection_names[SELECTION_DRAWING] = "drawing";
+ builder->get_widget("si_s_page", selection_buttons[SELECTION_PAGE]);
+ selection_names[SELECTION_PAGE] = "page";
+ builder->get_widget("si_s_selection", selection_buttons[SELECTION_SELECTION]);
+ selection_names[SELECTION_SELECTION] = "selection";
+ builder->get_widget("si_s_custom", selection_buttons[SELECTION_CUSTOM]);
+ selection_names[SELECTION_CUSTOM] = "custom";
+
+ builder->get_widget_derived("si_left_sb", spin_buttons[SPIN_X0]);
+ builder->get_widget_derived("si_right_sb", spin_buttons[SPIN_X1]);
+ builder->get_widget_derived("si_top_sb", spin_buttons[SPIN_Y0]);
+ builder->get_widget_derived("si_bottom_sb", spin_buttons[SPIN_Y1]);
+ builder->get_widget_derived("si_height_sb", spin_buttons[SPIN_HEIGHT]);
+ builder->get_widget_derived("si_width_sb", spin_buttons[SPIN_WIDTH]);
+
+ builder->get_widget("si_label_left", spin_labels[SPIN_X0]);
+ builder->get_widget("si_label_right", spin_labels[SPIN_X1]);
+ builder->get_widget("si_label_top", spin_labels[SPIN_Y0]);
+ builder->get_widget("si_label_bottom", spin_labels[SPIN_Y1]);
+ builder->get_widget("si_label_height", spin_labels[SPIN_HEIGHT]);
+ builder->get_widget("si_label_width", spin_labels[SPIN_WIDTH]);
+
+ builder->get_widget_derived("si_img_height_sb", spin_buttons[SPIN_BMHEIGHT]);
+ builder->get_widget_derived("si_img_width_sb", spin_buttons[SPIN_BMWIDTH]);
+ builder->get_widget_derived("si_dpi_sb", spin_buttons[SPIN_DPI]);
+
+ builder->get_widget("si_pages", pages_list);
+ builder->get_widget("si_pages_box", pages_list_box);
+ builder->get_widget("si_sizes", size_box);
+
+ builder->get_widget_derived("si_units", units);
+ builder->get_widget("si_units_row", si_units_row);
+
+ builder->get_widget("si_hide_all", si_hide_all);
+ builder->get_widget("si_show_preview", si_show_preview);
+ builder->get_widget_derived("si_preview", preview);
+ builder->get_widget("si_preview_box", preview_box);
+
+ builder->get_widget_derived("si_extention", si_extension_cb);
+ Gtk::Box *pref_button_box = nullptr;
+ builder->get_widget("si_prefs", pref_button_box);
+ pref_button_box->add(*si_extension_cb->getPrefButton());
+
+ builder->get_widget("si_filename", si_filename_entry);
+ builder->get_widget("si_export", si_export);
+
+ builder->get_widget("si_progress", _prog);
+ builder->get_widget("si_cancel", cancel_button);
+ builder->get_widget("si_inprogress", progress_box);
+
+ Gtk::Button* button = nullptr;
+ builder->get_widget("si_backgnd", button);
+ _bgnd_color_picker = std::make_unique<Inkscape::UI::Widget::ColorPicker>(
+ _("Background color"), _("Color used to fill background"), 0xffffff00, true, button);
+
+ setup();
+}
+
+// Inkscape Selection Modified CallBack
+void SingleExport::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ if (!_desktop || _desktop->getSelection() != selection) {
+ return;
+ }
+ if (!(flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
+ return;
+ }
+ refreshArea();
+ // Do not load export hits for modifications
+}
+
+void SingleExport::selectionChanged(Inkscape::Selection *selection)
+{
+ if (!_desktop || _desktop->getSelection() != selection) {
+ return;
+ }
+
+ Glib::ustring pref_key_name = prefs->getString("/dialogs/export/exportarea/value");
+ for (auto [key, name] : selection_names) {
+ if (name == pref_key_name && current_key != key && key != SELECTION_SELECTION) {
+ selection_buttons[key]->set_active(true);
+ current_key = key;
+ break;
+ }
+ }
+ if (selection->isEmpty()) {
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(false);
+ if (current_key == SELECTION_SELECTION) {
+ selection_buttons[(selection_mode)0]->set_active(true); // This causes refresh area
+ // even though we are at default key, selection is the one which was original key.
+ prefs->setString("/dialogs/export/exportarea/value", selection_names[SELECTION_SELECTION]);
+ // return otherwise refreshArea will be called again
+ return;
+ }
+ } else {
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(true);
+ if (selection_names[SELECTION_SELECTION] == pref_key_name && current_key != SELECTION_SELECTION) {
+ selection_buttons[SELECTION_SELECTION]->set_active();
+ return;
+ }
+ }
+
+ refreshArea();
+ loadExportHints();
+}
+
+// Setup Single Export.Called by export on realize
+void SingleExport::setup()
+{
+ if (setupDone) {
+ // We need to setup only once
+ return;
+ }
+ setupDone = true;
+
+ si_extension_cb->setup();
+
+ setupUnits();
+ setupSpinButtons();
+
+ // set them before connecting to signals
+ setDefaultSelectionMode();
+ setPagesMode(false);
+ setExporting(false);
+
+ // Refresh the filename when the user selects a different page
+ _pages_list_changed = pages_list->signal_selected_children_changed().connect([=]() {
+ loadExportHints();
+ refreshArea();
+ });
+
+ // Connect Signals Here
+ for (auto [key, button] : selection_buttons) {
+ button->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SingleExport::onAreaTypeToggle), key));
+ }
+ units->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onUnitChanged));
+ extensionConn = si_extension_cb->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onExtensionChanged));
+ exportConn = si_export->signal_clicked().connect(sigc::mem_fun(*this, &SingleExport::onExport));
+ filenameConn = si_filename_entry->signal_changed().connect(sigc::mem_fun(*this, &SingleExport::onFilenameModified));
+ browseConn = si_filename_entry->signal_icon_release().connect(sigc::mem_fun(*this, &SingleExport::onBrowse));
+ cancelConn = cancel_button->signal_clicked().connect(sigc::mem_fun(*this, &SingleExport::onCancel));
+ si_filename_entry->signal_activate().connect(sigc::mem_fun(*this, &SingleExport::onExport));
+ si_show_preview->signal_toggled().connect(sigc::mem_fun(*this, &SingleExport::refreshPreview));
+ si_hide_all->signal_toggled().connect(sigc::mem_fun(*this, &SingleExport::refreshPreview));
+ _bgnd_color_picker->connectChanged([=](guint32 color){
+ if (_desktop) {
+ Inkscape::UI::Dialog::set_export_bg_color(_desktop->getNamedView(), color);
+ }
+ refreshPreview();
+ });
+}
+
+// Setup units combobox
+void SingleExport::setupUnits()
+{
+ units->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR);
+ if (_desktop) {
+ units->setUnit(_desktop->getNamedView()->display_units->abbr);
+ }
+}
+
+// Create all spin buttons
+void SingleExport::setupSpinButtons()
+{
+ setupSpinButton<sb_type>(spin_buttons[SPIN_X0], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true,
+ &SingleExport::onAreaXChange, SPIN_X0);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_X1], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true,
+ &SingleExport::onAreaXChange, SPIN_X1);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_Y0], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true,
+ &SingleExport::onAreaYChange, SPIN_Y0);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_Y1], 0.0, -1000000.0, 1000000.0, 0.1, 1.0, EXPORT_COORD_PRECISION, true,
+ &SingleExport::onAreaYChange, SPIN_Y1);
+
+ setupSpinButton<sb_type>(spin_buttons[SPIN_HEIGHT], 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, EXPORT_COORD_PRECISION,
+ true, &SingleExport::onAreaYChange, SPIN_HEIGHT);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_WIDTH], 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, EXPORT_COORD_PRECISION,
+ true, &SingleExport::onAreaXChange, SPIN_WIDTH);
+
+ setupSpinButton<sb_type>(spin_buttons[SPIN_BMHEIGHT], 1.0, 1.0, 1000000.0, 1.0, 10.0, 0, true,
+ &SingleExport::onDpiChange, SPIN_BMHEIGHT);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_BMWIDTH], 1.0, 1.0, 1000000.0, 1.0, 10.0, 0, true,
+ &SingleExport::onDpiChange, SPIN_BMWIDTH);
+ setupSpinButton<sb_type>(spin_buttons[SPIN_DPI], prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE),
+ 1.0, 100000.0, 0.1, 1.0, 2, true, &SingleExport::onDpiChange, SPIN_DPI);
+}
+
+template <typename T>
+void SingleExport::setupSpinButton(Gtk::SpinButton *sb, double val, double min, double max, double step, double page,
+ int digits, bool sensitive, void (SingleExport::*cb)(T), T param)
+{
+ if (sb) {
+ sb->set_digits(digits);
+ sb->set_increments(step, page);
+ sb->set_range(min, max);
+ sb->set_value(val);
+ sb->set_sensitive(sensitive);
+ sb->set_width_chars(0);
+ sb->set_max_width_chars(0);
+ if (cb) {
+ auto signal = sb->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, cb), param));
+ // add signals to list to block all easily
+ spinButtonConns.push_back(signal);
+ }
+ }
+}
+
+void SingleExport::refreshArea()
+{
+ if (_document) {
+ Geom::OptRect bbox;
+ auto sel = getSelectedPages();
+
+ switch (current_key) {
+ case SELECTION_SELECTION:
+ if ((_desktop->getSelection())->isEmpty() == false) {
+ bbox = _desktop->getSelection()->visualBounds();
+ break;
+ }
+ case SELECTION_DRAWING:
+ bbox = _document->getRoot()->desktopVisualBounds();
+ if (bbox) {
+ break;
+ }
+ case SELECTION_PAGE:
+ // If the page is set in the multi-selection use that.
+ if (sel.size() == 1) {
+ bbox = sel[0]->getDesktopRect();
+ } else {
+ bbox = _document->getPageManager().getSelectedPageRect();
+ }
+ break;
+ case SELECTION_CUSTOM:
+ break;
+ default:
+ break;
+ }
+ if (current_key != SELECTION_CUSTOM && bbox) {
+ setArea(bbox->min()[Geom::X], bbox->min()[Geom::Y], bbox->max()[Geom::X], bbox->max()[Geom::Y]);
+ }
+ }
+ refreshPreview();
+}
+
+void SingleExport::refreshPage()
+{
+ if (!_document)
+ return;
+
+ bool multi = pages_list->get_selection_mode() == Gtk::SELECTION_MULTIPLE;
+ auto &pm = _document->getPageManager();
+ bool has_pages = current_key == SELECTION_PAGE && pm.getPageCount() > 1;
+ pages_list_box->set_visible(has_pages);
+ preview_box->set_visible(!has_pages);
+ size_box->set_visible(!has_pages || !multi);
+}
+
+void SingleExport::setPagesMode(bool multi)
+{
+ // Set set the internal mode to NONE to preserve selections while changing
+ pages_list->foreach([=](Gtk::Widget& widget) {
+ if (auto item = dynamic_cast<BatchItem *>(&widget))
+ item->on_mode_changed(Gtk::SELECTION_NONE);
+ });
+ pages_list->set_selection_mode(multi ? Gtk::SELECTION_MULTIPLE : Gtk::SELECTION_SINGLE);
+ // A second call it needed in it's own loop because of how updates happen in the FlowBox
+ pages_list->foreach([=](Gtk::Widget& widget) {
+ if (auto item = dynamic_cast<BatchItem *>(&widget))
+ item->update_selected();
+ });
+ refreshPage();
+}
+
+void SingleExport::selectPage(SPPage *page)
+{
+ pages_list->foreach([=](Gtk::Widget& widget) {
+ if (auto item = dynamic_cast<BatchItem *>(&widget)) {
+ if (item->getPage() == page) {
+ item->set_selected(true);
+ }
+ }
+ });
+}
+
+std::vector<SPPage *> SingleExport::getSelectedPages()
+{
+ std::vector<SPPage *> pages;
+ pages_list->selected_foreach([&pages](Gtk::FlowBox *box, Gtk::FlowBoxChild *child) {
+ if (auto item = dynamic_cast<BatchItem *>(child))
+ pages.push_back(item->getPage());
+ });
+ return pages;
+}
+
+/**
+ * Clear all page preview widgets and halting any in-progress updates.
+ */
+void SingleExport::clearPagePreviews()
+{
+ _pages_list_changed.block();
+ while (auto widget = pages_list->get_child_at_index(0)) {
+ pages_list->remove(*widget);
+ }
+ _pages_list_changed.unblock();
+}
+
+void SingleExport::onPagesChanged()
+{
+ clearPagePreviews();
+ if (!_document)
+ return;
+ _pages_list_changed.block();
+ auto &pm = _document->getPageManager();
+ if (pm.getPageCount() > 1) {
+ for (auto page : pm.getPages()) {
+ auto item = Gtk::manage(new BatchItem(page, _preview_drawing));
+ pages_list->insert(*item, -1);
+ }
+ }
+ refreshPage();
+ if (auto ext = si_extension_cb->getExtension()) {
+ setPagesMode(!ext->is_raster());
+ }
+ _pages_list_changed.unblock();
+}
+
+void SingleExport::onPagesModified(SPPage *page)
+{
+ refreshArea();
+}
+
+void SingleExport::onPagesSelected(SPPage *page) {
+ if (pages_list->get_selection_mode() != Gtk::SELECTION_MULTIPLE) {
+ selectPage(page);
+ }
+ refreshArea();
+}
+
+void SingleExport::loadExportHints()
+{
+ if (filename_modified || !_document || !_desktop) return;
+
+ Glib::ustring old_filename = si_filename_entry->get_text();
+ Glib::ustring filename;
+ Geom::Point dpi;
+ switch (current_key) {
+ case SELECTION_PAGE:
+ {
+ auto pages = getSelectedPages();
+ if (pages.size() == 1) {
+ dpi = pages[0]->getExportDpi();
+ filename = pages[0]->getExportFilename();
+ if (filename.empty()) {
+ filename = Export::filePathFromId(_document, pages[0]->getLabel(), old_filename);
+ }
+ break;
+ }
+ // No or many pages means output is drawing, continue.
+ }
+ case SELECTION_CUSTOM:
+ case SELECTION_DRAWING:
+ {
+ dpi = _document->getRoot()->getExportDpi();
+ filename = _document->getRoot()->getExportFilename();
+ break;
+ }
+ case SELECTION_SELECTION:
+ {
+ auto selection = _desktop->getSelection();
+ if (selection->isEmpty()) break;
+
+ // Get filename and dpi from selected items
+ for (auto item : selection->items()) {
+ if (!dpi.x()) {
+ dpi = item->getExportDpi();
+ }
+ if (filename.empty()) {
+ filename = item->getExportFilename();
+ }
+ }
+
+ if (filename.empty()) {
+ filename = Export::filePathFromObject(_document, selection->firstItem(), old_filename);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ if (filename.empty()) {
+ filename = Export::defaultFilename(_document, old_filename, ".png");
+ }
+ if (auto ext = si_extension_cb->getExtension()) {
+ si_extension_cb->removeExtension(filename);
+ ext->add_extension(filename);
+ }
+
+ original_name = filename;
+ si_filename_entry->set_text(filename);
+ si_filename_entry->set_position(filename.length());
+
+ if (dpi.x() != 0.0) { // XXX Should this deal with dpi.y() ?
+ spin_buttons[SPIN_DPI]->set_value(dpi.x());
+ }
+}
+
+void SingleExport::saveExportHints(SPObject *target)
+{
+ if (target) {
+ target->setExportFilename(si_filename_entry->get_text());
+ target->setExportDpi(Geom::Point(
+ spin_buttons[SPIN_DPI]->get_value(),
+ spin_buttons[SPIN_DPI]->get_value()
+ ));
+ }
+}
+
+void SingleExport::setArea(double x0, double y0, double x1, double y1)
+{
+ blockSpinConns(true);
+
+ Unit const *unit = units->getUnit();
+ auto px = unit_table.getUnit("px");
+ spin_buttons[SPIN_X0]->get_adjustment()->set_value(px->convert(x0, unit));
+ spin_buttons[SPIN_X1]->get_adjustment()->set_value(px->convert(x1, unit));
+ spin_buttons[SPIN_Y0]->get_adjustment()->set_value(px->convert(y0, unit));
+ spin_buttons[SPIN_Y1]->get_adjustment()->set_value(px->convert(y1, unit));
+
+ areaXChange(SPIN_X1);
+ areaYChange(SPIN_Y1);
+
+ blockSpinConns(false);
+}
+
+// Signals CallBack
+
+void SingleExport::onUnitChanged()
+{
+ refreshArea();
+}
+
+void SingleExport::onAreaTypeToggle(selection_mode key)
+{
+ // Prevent executing function twice
+ if (!selection_buttons[key]->get_active()) {
+ return;
+ }
+ // If you have reached here means the current key is active one ( not sure if multiple transitions happen but
+ // last call will change values)
+ current_key = key;
+ prefs->setString("/dialogs/export/exportarea/value", selection_names[current_key]);
+
+ refreshArea();
+ loadExportHints();
+ toggleSpinButtonVisibility();
+ refreshPage();
+}
+
+void SingleExport::toggleSpinButtonVisibility()
+{
+ bool show = current_key == SELECTION_CUSTOM;
+ spin_buttons[SPIN_X0]->set_visible(show);
+ spin_buttons[SPIN_X1]->set_visible(show);
+ spin_buttons[SPIN_Y0]->set_visible(show);
+ spin_buttons[SPIN_Y1]->set_visible(show);
+ spin_buttons[SPIN_WIDTH]->set_visible(show);
+ spin_buttons[SPIN_HEIGHT]->set_visible(show);
+
+ spin_labels[SPIN_X0]->set_visible(show);
+ spin_labels[SPIN_X1]->set_visible(show);
+ spin_labels[SPIN_Y0]->set_visible(show);
+ spin_labels[SPIN_Y1]->set_visible(show);
+ spin_labels[SPIN_WIDTH]->set_visible(show);
+ spin_labels[SPIN_HEIGHT]->set_visible(show);
+
+ si_units_row->set_visible(show);
+}
+
+void SingleExport::onAreaXChange(sb_type type)
+{
+ blockSpinConns(true);
+ areaXChange(type);
+ selection_buttons[SELECTION_CUSTOM]->set_active(true);
+ refreshPreview();
+ blockSpinConns(false);
+}
+void SingleExport::onAreaYChange(sb_type type)
+{
+ blockSpinConns(true);
+ areaYChange(type);
+ selection_buttons[SELECTION_CUSTOM]->set_active(true);
+ refreshPreview();
+ blockSpinConns(false);
+}
+void SingleExport::onDpiChange(sb_type type)
+{
+ blockSpinConns(true);
+ dpiChange(type);
+ blockSpinConns(false);
+}
+
+void SingleExport::onFilenameModified()
+{
+ extensionConn.block();
+ Glib::ustring filename = si_filename_entry->get_text();
+
+ if (original_name == filename) {
+ filename_modified = false;
+ } else {
+ filename_modified = true;
+ }
+
+ si_extension_cb->setExtensionFromFilename(filename);
+
+ extensionConn.unblock();
+}
+
+void SingleExport::onExtensionChanged()
+{
+ if (auto ext = si_extension_cb->getExtension()) {
+ setPagesMode(!ext->is_raster());
+ loadExportHints();
+ }
+}
+
+void SingleExport::onCancel()
+{
+ interrupted = true;
+ setExporting(false);
+}
+
+void SingleExport::onExport()
+{
+ interrupted = false;
+ if (!_desktop || !_document)
+ return;
+
+ auto &page_manager = _document->getPageManager();
+ auto selection = _desktop->getSelection();
+ bool exportSuccessful = false;
+ auto omod = si_extension_cb->getExtension();
+ if (!omod) {
+ return;
+ }
+
+ setExporting(true, _("Exporting"));
+
+ bool selected_only = si_hide_all->get_active();
+ Unit const *unit = units->getUnit();
+ Glib::ustring filename = si_filename_entry->get_text();
+
+ float x0 = unit->convert(spin_buttons[SPIN_X0]->get_value(), "px");
+ float x1 = unit->convert(spin_buttons[SPIN_X1]->get_value(), "px");
+ float y0 = unit->convert(spin_buttons[SPIN_Y0]->get_value(), "px");
+ float y1 = unit->convert(spin_buttons[SPIN_Y1]->get_value(), "px");
+ auto area = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1));
+
+ if (omod->is_raster()) {
+ area *= _desktop->dt2doc();
+ unsigned long int width = int(spin_buttons[SPIN_BMWIDTH]->get_value() + 0.5);
+ unsigned long int height = int(spin_buttons[SPIN_BMHEIGHT]->get_value() + 0.5);
+
+ float dpi = spin_buttons[SPIN_DPI]->get_value();
+
+ setExporting(true, Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), filename, width, height));
+
+ std::vector<SPItem *> selected(selection->items().begin(), selection->items().end());
+
+ exportSuccessful = Export::exportRaster(
+ area, width, height, dpi, _bgnd_color_picker->get_current_color(),
+ filename, false, onProgressCallback, this,
+ omod, selected_only ? &selected : nullptr);
+
+ } else {
+ setExporting(true, Glib::ustring::compose(_("Exporting %1"), filename));
+
+ auto copy_doc = _document->copy();
+
+ std::vector<SPItem *> items;
+ if (selected_only) {
+ auto itemlist = selection->items();
+ for (auto i = itemlist.begin(); i != itemlist.end(); ++i) {
+ SPItem *item = *i;
+ items.push_back(item);
+ }
+ }
+
+ if (current_key == SELECTION_PAGE && page_manager.getPageCount() > 1) {
+ auto pages = getSelectedPages();
+ exportSuccessful = Export::exportVector(omod, copy_doc.get(), filename, false, items, pages);
+ } else {
+ // To get the right kind of export, we're going to make a page
+ // This allows all the same raster options to work for vectors
+ auto page = copy_doc->getPageManager().newDocumentPage(area);
+ exportSuccessful = Export::exportVector(omod, copy_doc.get(), filename, false, items, page);
+ }
+ }
+ // Save the export hints back to the svg document
+ if (exportSuccessful) {
+
+ std::string path = Export::absolutizePath(_document, Glib::filename_from_utf8(filename));
+ auto recentmanager = Gtk::RecentManager::get_default();
+ if (recentmanager && Glib::path_is_absolute(path)) {
+ Glib::ustring uri = Glib::filename_to_uri(path);
+ recentmanager->add_item(uri);
+ }
+
+ SPObject *target;
+ switch (current_key) {
+ case SELECTION_CUSTOM:
+ case SELECTION_DRAWING:
+ target = _document->getRoot();
+ break;
+ case SELECTION_PAGE:
+ target = page_manager.getSelected();
+ if (!target)
+ target = _document->getRoot();
+ break;
+ case SELECTION_SELECTION:
+ target = _desktop->getSelection()->firstItem();
+ break;
+ default:
+ break;
+ }
+ if (target) {
+ saveExportHints(target);
+ DocumentUndo::done(_document, _("Set Export Options"), INKSCAPE_ICON("export"));
+ }
+ }
+ setExporting(false);
+ original_name = filename;
+ filename_modified = false;
+ interrupted = false;
+}
+
+void SingleExport::onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev)
+{
+ if (!_app || !_app->get_active_window() || !_document) {
+ return;
+ }
+ Gtk::Window *window = _app->get_active_window();
+ browseConn.block();
+ Glib::ustring filename = Glib::filename_from_utf8(si_filename_entry->get_text());
+
+ if (filename.empty()) {
+ filename = Export::defaultFilename(_document, filename, ".png");
+ }
+
+ Inkscape::UI::Dialog::FileSaveDialog *dialog = Inkscape::UI::Dialog::FileSaveDialog::create(
+ *window, filename, Inkscape::UI::Dialog::EXPORT_TYPES, _("Select a filename for exporting"), "", "",
+ Inkscape::Extension::FILE_SAVE_METHOD_EXPORT);
+
+ // Tell the browse dialog what extension to start with
+ if (auto omod = si_extension_cb->getExtension()) {
+ dialog->setExtension(omod);
+ }
+
+ if (dialog->show()) {
+ filename = dialog->getFilename();
+ // Once complete, we use the extension selected to save the file
+ if (auto ext = dialog->getExtension()) {
+ si_extension_cb->set_active_id(ext->get_id());
+ } else {
+ si_extension_cb->setExtensionFromFilename(filename);
+ }
+
+ si_filename_entry->set_text(filename);
+ si_filename_entry->set_position(filename.length());
+
+ // deleting dialog before exporting is important
+ delete dialog;
+ onExport();
+ } else {
+ delete dialog;
+ }
+ browseConn.unblock();
+}
+
+// Utils Functions
+
+void SingleExport::blockSpinConns(bool status = true)
+{
+ for (auto signal : spinButtonConns) {
+ if (status) {
+ signal.block();
+ } else {
+ signal.unblock();
+ }
+ }
+}
+
+void SingleExport::areaXChange(sb_type type)
+{
+ auto x0_adj = spin_buttons[SPIN_X0]->get_adjustment();
+ auto x1_adj = spin_buttons[SPIN_X1]->get_adjustment();
+ auto width_adj = spin_buttons[SPIN_WIDTH]->get_adjustment();
+
+ float x0, x1, dpi, width, bmwidth;
+
+ // Get all values in px
+ Unit const *unit = units->getUnit();
+ x0 = unit->convert(x0_adj->get_value(), "px");
+ x1 = unit->convert(x1_adj->get_value(), "px");
+ width = unit->convert(width_adj->get_value(), "px");
+ bmwidth = spin_buttons[SPIN_BMWIDTH]->get_value();
+ dpi = spin_buttons[SPIN_DPI]->get_value();
+
+ switch (type) {
+ case SPIN_X0:
+ bmwidth = (x1 - x0) * dpi / DPI_BASE;
+ if (bmwidth < SP_EXPORT_MIN_SIZE) {
+ x0 = x1 - (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ break;
+ case SPIN_X1:
+ bmwidth = (x1 - x0) * dpi / DPI_BASE;
+ if (bmwidth < SP_EXPORT_MIN_SIZE) {
+ x1 = x0 + (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ break;
+ case SPIN_WIDTH:
+ bmwidth = width * dpi / DPI_BASE;
+ if (bmwidth < SP_EXPORT_MIN_SIZE) {
+ width = (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ x1 = x0 + width;
+ break;
+ default:
+ break;
+ }
+
+ width = x1 - x0;
+ bmwidth = floor(width * dpi / DPI_BASE + 0.5);
+
+ auto px = unit_table.getUnit("px");
+ x0_adj->set_value(px->convert(x0, unit));
+ x1_adj->set_value(px->convert(x1, unit));
+ width_adj->set_value(px->convert(width, unit));
+ spin_buttons[SPIN_BMWIDTH]->set_value(bmwidth);
+}
+
+void SingleExport::areaYChange(sb_type type)
+{
+ auto y0_adj = spin_buttons[SPIN_Y0]->get_adjustment();
+ auto y1_adj = spin_buttons[SPIN_Y1]->get_adjustment();
+ auto height_adj = spin_buttons[SPIN_HEIGHT]->get_adjustment();
+
+ float y0, y1, dpi, height, bmheight;
+
+ // Get all values in px
+ Unit const *unit = units->getUnit();
+ y0 = unit->convert(y0_adj->get_value(), "px");
+ y1 = unit->convert(y1_adj->get_value(), "px");
+ height = unit->convert(height_adj->get_value(), "px");
+ bmheight = spin_buttons[SPIN_BMHEIGHT]->get_value();
+ dpi = spin_buttons[SPIN_DPI]->get_value();
+
+ switch (type) {
+ case SPIN_Y0:
+ bmheight = (y1 - y0) * dpi / DPI_BASE;
+ if (bmheight < SP_EXPORT_MIN_SIZE) {
+ y0 = y1 - (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ break;
+ case SPIN_Y1:
+ bmheight = (y1 - y0) * dpi / DPI_BASE;
+ if (bmheight < SP_EXPORT_MIN_SIZE) {
+ y1 = y0 + (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ break;
+ case SPIN_HEIGHT:
+ bmheight = height * dpi / DPI_BASE;
+ if (bmheight < SP_EXPORT_MIN_SIZE) {
+ height = (SP_EXPORT_MIN_SIZE * DPI_BASE) / dpi;
+ }
+ y1 = y0 + height;
+ break;
+ default:
+ break;
+ }
+
+ height = y1 - y0;
+ bmheight = floor(height * dpi / DPI_BASE + 0.5);
+
+ auto px = unit_table.getUnit("px");
+ y0_adj->set_value(px->convert(y0, unit));
+ y1_adj->set_value(px->convert(y1, unit));
+ height_adj->set_value(px->convert(height, unit));
+ spin_buttons[SPIN_BMHEIGHT]->set_value(bmheight);
+}
+
+void SingleExport::dpiChange(sb_type type)
+{
+ float dpi, height, width, bmheight, bmwidth;
+
+ // Get all values in px
+ Unit const *unit = units->getUnit();
+ height = unit->convert(spin_buttons[SPIN_HEIGHT]->get_value(), "px");
+ width = unit->convert(spin_buttons[SPIN_WIDTH]->get_value(), "px");
+ bmheight = spin_buttons[SPIN_BMHEIGHT]->get_value();
+ bmwidth = spin_buttons[SPIN_BMWIDTH]->get_value();
+ dpi = spin_buttons[SPIN_DPI]->get_value();
+
+ switch (type) {
+ case SPIN_BMHEIGHT:
+ if (bmheight < SP_EXPORT_MIN_SIZE) {
+ bmheight = SP_EXPORT_MIN_SIZE;
+ }
+ dpi = bmheight * DPI_BASE / height;
+ break;
+ case SPIN_BMWIDTH:
+ if (bmwidth < SP_EXPORT_MIN_SIZE) {
+ bmwidth = SP_EXPORT_MIN_SIZE;
+ }
+ dpi = bmwidth * DPI_BASE / width;
+ break;
+ case SPIN_DPI:
+ prefs->setDouble("/dialogs/export/defaultdpi/value", dpi);
+ break;
+ default:
+ break;
+ }
+
+ bmwidth = floor(width * dpi / DPI_BASE + 0.5);
+ bmheight = floor(height * dpi / DPI_BASE + 0.5);
+
+ spin_buttons[SPIN_BMHEIGHT]->set_value(bmheight);
+ spin_buttons[SPIN_BMWIDTH]->set_value(bmwidth);
+ spin_buttons[SPIN_DPI]->set_value(dpi);
+}
+
+void SingleExport::setDefaultSelectionMode()
+{
+ current_key = (selection_mode)0; // default key
+ bool found = false;
+ Glib::ustring pref_key_name = prefs->getString("/dialogs/export/exportarea/value");
+ for (auto [key, name] : selection_names) {
+ if (pref_key_name == name) {
+ current_key = key;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ pref_key_name = selection_names[current_key];
+ }
+
+ if (_desktop) {
+ if (current_key == SELECTION_SELECTION && (_desktop->getSelection())->isEmpty()) {
+ current_key = (selection_mode)0;
+ }
+ if ((_desktop->getSelection())->isEmpty()) {
+ selection_buttons[SELECTION_SELECTION]->set_sensitive(false);
+ }
+ if (current_key == SELECTION_CUSTOM &&
+ (spin_buttons[SPIN_HEIGHT]->get_value() == 0 || spin_buttons[SPIN_WIDTH]->get_value() == 0)) {
+ Geom::OptRect bbox = _document->preferredBounds();
+ setArea(bbox->min()[Geom::X], bbox->min()[Geom::Y], bbox->max()[Geom::X], bbox->max()[Geom::Y]);
+ }
+ } else {
+ current_key = (selection_mode)0;
+ }
+ selection_buttons[current_key]->set_active(true);
+ prefs->setString("/dialogs/export/exportarea/value", pref_key_name);
+
+ toggleSpinButtonVisibility();
+ refreshPage();
+}
+
+void SingleExport::setExporting(bool exporting, Glib::ustring const &text)
+{
+ if (exporting) {
+ set_sensitive(false);
+ set_opacity(0.2);
+ progress_box->show();
+ _prog->set_text(text);
+ _prog->set_fraction(0.0);
+ } else {
+ set_sensitive(true);
+ set_opacity(1.0);
+ progress_box->hide();
+ _prog->set_text("");
+ _prog->set_fraction(0.0);
+ }
+ Gtk::Main::iteration(false);
+}
+
+// Called for every progress iteration
+unsigned int SingleExport::onProgressCallback(float value, void *data)
+{
+ if (auto si = static_cast<SingleExport *>(data)) {
+ si->_prog->set_fraction(value);
+ Gtk::Main::iteration(false);
+ return !si->interrupted;
+ }
+ return false;
+}
+
+void SingleExport::refreshPreview()
+{
+ if (!_desktop) {
+ preview->resetPixels();
+ return;
+ }
+
+ std::vector<SPItem *> selected;
+ if (si_hide_all->get_active()) {
+ // This is because selection items is not a std::vector yet. FIXME.
+ selected =
+ std::vector<SPItem *>(_desktop->getSelection()->items().begin(), _desktop->getSelection()->items().end());
+ }
+ _preview_drawing->set_shown_items(std::move(selected));
+
+ bool show = si_show_preview->get_active();
+ if (!show || current_key == SELECTION_PAGE) {
+ bool have_pages = false;
+ for (auto child : pages_list->get_children()) {
+ if (auto bi = dynamic_cast<BatchItem *>(child)) {
+ bi->refresh(!show, _bgnd_color_picker->get_current_color());
+ have_pages = true;
+ }
+ }
+ if (have_pages) {
+ // We don't want to update the main preview for pages, it's hidden
+ preview->resetPixels();
+ return;
+ }
+ }
+
+ Unit const *unit = units->getUnit();
+ float x0 = unit->convert(spin_buttons[SPIN_X0]->get_value(), "px");
+ float x1 = unit->convert(spin_buttons[SPIN_X1]->get_value(), "px");
+ float y0 = unit->convert(spin_buttons[SPIN_Y0]->get_value(), "px");
+ float y1 = unit->convert(spin_buttons[SPIN_Y1]->get_value(), "px");
+ preview->setBox(Geom::Rect(x0, y0, x1, y1) * _document->dt2doc());
+ preview->setBackgroundColor(_bgnd_color_picker->get_current_color());
+ preview->queueRefresh();
+}
+
+void SingleExport::setDesktop(SPDesktop *desktop)
+{
+ if (desktop != _desktop) {
+ _page_selected_connection.disconnect();
+ _desktop = desktop;
+ }
+}
+
+void SingleExport::setDocument(SPDocument *document)
+{
+ if (_document == document || !_desktop)
+ return;
+
+ _document = document;
+ _page_changed_connection.disconnect();
+ _page_selected_connection.disconnect();
+ if (document) {
+ auto &pm = document->getPageManager();
+ _page_selected_connection = pm.connectPageSelected(sigc::mem_fun(*this, &SingleExport::onPagesSelected));
+ _page_modified_connection = pm.connectPageModified(sigc::mem_fun(*this, &SingleExport::onPagesModified));
+ _page_changed_connection = pm.connectPagesChanged(sigc::mem_fun(*this, &SingleExport::onPagesChanged));
+ auto bg_color = get_export_bg_color(document->getNamedView(), 0xffffff00);
+ _bgnd_color_picker->setRgba32(bg_color);
+ _preview_drawing = std::make_shared<PreviewDrawing>(document);
+ preview->setDrawing(_preview_drawing);
+
+ // Refresh values to sync them with defaults.
+ onPagesChanged();
+ refreshArea();
+ loadExportHints();
+ } else {
+ _preview_drawing.reset();
+ clearPagePreviews();
+ }
+}
+
+SingleExport::~SingleExport() { _page_selected_connection.disconnect(); }
+
+} // 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/dialog/export-single.h b/src/ui/dialog/export-single.h
new file mode 100644
index 0000000..814cea0
--- /dev/null
+++ b/src/ui/dialog/export-single.h
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SP_EXPORT_SINGLE_H
+#define SP_EXPORT_SINGLE_H
+
+#include "ui/widget/scrollprotected.h"
+
+class InkscapeApplication;
+class SPDesktop;
+class SPDocument;
+class SPObject;
+class SPPage;
+
+namespace Inkscape {
+ class Selection;
+ class Preferences;
+
+namespace Util {
+ class Unit;
+}
+namespace UI {
+ namespace Widget {
+ class UnitMenu;
+ class ColorPicker;
+ }
+namespace Dialog {
+ class PreviewDrawing;
+ class ExportPreview;
+ class ExtensionList;
+
+class SingleExport : public Gtk::Box
+{
+public:
+ SingleExport(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade);
+ ~SingleExport() override;
+
+ void setApp(InkscapeApplication *app) { _app = app; }
+ void setDocument(SPDocument *document);
+ void setDesktop(SPDesktop *desktop);
+ void selectionChanged(Inkscape::Selection *selection);
+ void selectionModified(Inkscape::Selection *selection, guint flags);
+ void refresh()
+ {
+ refreshArea();
+ refreshPage();
+ loadExportHints();
+ };
+
+private:
+ enum sb_type
+ {
+ SPIN_X0 = 0,
+ SPIN_X1,
+ SPIN_Y0,
+ SPIN_Y1,
+ SPIN_WIDTH,
+ SPIN_HEIGHT,
+ SPIN_BMWIDTH,
+ SPIN_BMHEIGHT,
+ SPIN_DPI
+ };
+
+ enum selection_mode
+ {
+ SELECTION_PAGE = 0, // Default is alaways placed first
+ SELECTION_SELECTION,
+ SELECTION_DRAWING,
+ SELECTION_CUSTOM,
+ };
+
+ InkscapeApplication *_app = nullptr;
+ SPDesktop *_desktop = nullptr;
+ SPDocument *_document = nullptr;
+ std::shared_ptr<PreviewDrawing> _preview_drawing;
+
+ bool setupDone = false; // To prevent setup() call add connections again.
+
+ typedef Inkscape::UI::Widget::ScrollProtected<Gtk::SpinButton> SpinButton;
+
+ std::map<sb_type, SpinButton *> spin_buttons;
+ std::map<sb_type, Gtk::Label *> spin_labels;
+ std::map<selection_mode, Gtk::RadioButton *> selection_buttons;
+
+ Gtk::Box *si_units_row = nullptr;
+ Gtk::CheckButton *show_export_area = nullptr;
+ Inkscape::UI::Widget::UnitMenu *units = nullptr;
+ Gtk::FlowBox *pages_list = nullptr;
+
+ Gtk::CheckButton *si_hide_all = nullptr;
+ Gtk::CheckButton *si_show_preview = nullptr;
+
+ ExportPreview *preview = nullptr;
+
+ ExtensionList *si_extension_cb = nullptr;
+ Gtk::Entry *si_filename_entry = nullptr;
+ Gtk::Button *si_export = nullptr;
+ Gtk::Box *adv_box = nullptr;
+ Gtk::Grid *size_box = nullptr;
+ Gtk::ProgressBar *_prog = nullptr;
+ Gtk::Widget *pages_list_box = nullptr;
+ Gtk::Widget *preview_box = nullptr;
+ Gtk::Widget *progress_box = nullptr;
+ Gtk::Button *cancel_button = nullptr;
+
+ bool filename_modified = false;
+ Glib::ustring original_name;
+ Glib::ustring doc_export_name;
+
+ Inkscape::Preferences *prefs = nullptr;
+ std::map<selection_mode, Glib::ustring> selection_names;
+ selection_mode current_key = (selection_mode)0;
+
+ void setup();
+ void setupUnits();
+ void setupExtensionList();
+ void setupSpinButtons();
+ void toggleSpinButtonVisibility();
+ void refreshPreview();
+
+ // change range and callbacks to spinbuttons
+ template <typename T>
+ void setupSpinButton(Gtk::SpinButton *sb, double val, double min, double max, double step, double page, int digits,
+ bool sensitive, void (SingleExport::*cb)(T), T param);
+
+ void setDefaultSelectionMode();
+ void onAreaXChange(sb_type type);
+ void onAreaYChange(sb_type type);
+ void onDpiChange(sb_type type);
+ void onAreaTypeToggle(selection_mode key);
+ void onUnitChanged();
+ void onFilenameModified();
+ void onExtensionChanged();
+ void onExport();
+ void onCancel();
+ void onBrowse(Gtk::EntryIconPosition pos, const GdkEventButton *ev);
+ void on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags);
+ void on_inkscape_selection_changed(Inkscape::Selection *selection);
+
+ void refreshArea();
+ void refreshPage();
+ void loadExportHints();
+ void saveExportHints(SPObject *target);
+ void areaXChange(sb_type type);
+ void areaYChange(sb_type type);
+ void dpiChange(sb_type type);
+ void setArea(double x0, double y0, double x1, double y1);
+ void blockSpinConns(bool status);
+
+ void setExporting(bool exporting, Glib::ustring const &text = "");
+ /**
+ * Callback to be used in for loop to update the progress bar.
+ *
+ * @param value number between 0 and 1 indicating the fraction of progress (0.17 = 17 % progress)
+ */
+ static unsigned int onProgressCallback(float value, void *data);
+
+ /**
+ * Page functions
+ */
+ void clearPagePreviews();
+ void onPagesChanged();
+ void onPagesModified(SPPage *page);
+ void onPagesSelected(SPPage *page);
+ void setPagesMode(bool multi);
+ void selectPage(SPPage *page);
+ std::vector<SPPage *> getSelectedPages();
+
+ bool interrupted;
+
+ // Gtk Signals
+ std::vector<sigc::connection> spinButtonConns;
+ sigc::connection filenameConn;
+ sigc::connection extensionConn;
+ sigc::connection exportConn;
+ sigc::connection cancelConn;
+ sigc::connection browseConn;
+ sigc::connection prefsConn;
+ sigc::connection _pages_list_changed;
+ // Document Signals
+ sigc::connection _page_selected_connection;
+ sigc::connection _page_modified_connection;
+ sigc::connection _page_changed_connection;
+
+ std::unique_ptr<Inkscape::UI::Widget::ColorPicker> _bgnd_color_picker;
+};
+} // 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/dialog/export.cpp b/src/ui/dialog/export.cpp
new file mode 100644
index 0000000..522acf0
--- /dev/null
+++ b/src/ui/dialog/export.cpp
@@ -0,0 +1,531 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Peter Bostrom
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2012, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+// This has to be included prior to anything that includes setjmp.h, it croaks otherwise
+#include "export.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 "color/color-conv.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-root.h"
+#include "object/sp-page.h"
+#include "object/weakptr.h"
+#include "preferences.h"
+#include "selection-chemistry.h"
+#include "ui/dialog-events.h"
+#include "ui/dialog/export-single.h"
+#include "ui/dialog/export-batch.h"
+#include "ui/dialog/dialog-notebook.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/interface.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/unit-menu.h"
+
+#ifdef _WIN32
+
+#endif
+
+using Inkscape::Util::unit_table;
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Export::Export()
+ : DialogBase("/dialogs/export/", "Export")
+{
+ std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-export.glade");
+
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_error("Glade file loading failed for export screen");
+ return;
+ }
+
+ prefs = Inkscape::Preferences::get();
+
+ builder->get_widget("export-box", container);
+ add(*container);
+ show_all_children();
+
+ builder->get_widget("export-notebook", export_notebook);
+
+ // Initialise Single Export and its objects
+ builder->get_widget_derived("single-image", single_image);
+
+ // Initialise Batch Export and its objects
+ builder->get_widget_derived("batch-export", batch_export);
+
+ container->signal_realize().connect([=]() {
+ setDefaultNotebookPage();
+ notebook_signal = export_notebook->signal_switch_page().connect(sigc::mem_fun(*this, &Export::onNotebookPageSwitch));
+ });
+ container->signal_unrealize().connect([=]() {
+ notebook_signal.disconnect();
+ });
+}
+
+// Set current page based on preference/last visited page
+void Export::setDefaultNotebookPage()
+{
+ pages[BATCH_EXPORT] = export_notebook->page_num(*batch_export->get_parent());
+ pages[SINGLE_IMAGE] = export_notebook->page_num(*single_image->get_parent());
+ export_notebook->set_current_page(pages[SINGLE_IMAGE]);
+}
+
+void Export::documentReplaced()
+{
+ single_image->setDocument(getDocument());
+ batch_export->setDocument(getDocument());
+}
+
+void Export::desktopReplaced()
+{
+ single_image->setDesktop(getDesktop());
+ single_image->setApp(getApp());
+ batch_export->setDesktop(getDesktop());
+ batch_export->setApp(getApp());
+ // Called previously, but we need post-desktop call too
+ documentReplaced();
+}
+
+void Export::selectionChanged(Inkscape::Selection *selection)
+{
+ auto current_page = export_notebook->get_current_page();
+ if (current_page == pages[SINGLE_IMAGE]) {
+ single_image->selectionChanged(selection);
+ }
+ if (current_page == pages[BATCH_EXPORT]) {
+ batch_export->selectionChanged(selection);
+ }
+}
+void Export::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ auto current_page = export_notebook->get_current_page();
+ if (current_page == pages[SINGLE_IMAGE]) {
+ single_image->selectionModified(selection, flags);
+ }
+ if (current_page == pages[BATCH_EXPORT]) {
+ batch_export->selectionModified(selection, flags);
+ }
+}
+
+void Export::onNotebookPageSwitch(Widget *page, guint page_number)
+{
+ auto desktop = getDesktop();
+ if (desktop) {
+ auto selection = desktop->getSelection();
+
+ if (page_number == pages[SINGLE_IMAGE]) {
+ single_image->selectionChanged(selection);
+ }
+ if (page_number == pages[BATCH_EXPORT]) {
+ batch_export->selectionChanged(selection);
+ }
+ }
+}
+
+std::string Export::absolutizePath(SPDocument *doc, const std::string &filename)
+{
+ std::string path;
+ // Make relative paths go from the document location, if possible:
+ if (!Glib::path_is_absolute(filename) && doc->getDocumentFilename()) {
+ auto dirname = Glib::path_get_dirname(doc->getDocumentFilename());
+ if (!dirname.empty()) {
+ path = Glib::build_filename(dirname, filename);
+ }
+ }
+ if (path.empty()) {
+ path = filename;
+ }
+ return path;
+}
+
+bool Export::unConflictFilename(SPDocument *doc, Glib::ustring &filename, Glib::ustring const extension)
+{
+ std::string path = absolutizePath(doc, Glib::filename_from_utf8(filename));
+ Glib::ustring test_filename = path + extension;
+ if (!Inkscape::IO::file_test(test_filename.c_str(), G_FILE_TEST_EXISTS)) {
+ filename = test_filename;
+ return true;
+ }
+ for (int i = 1; i <= 100; i++) {
+ test_filename = path + "_copy_" + std::to_string(i) + extension;
+ if (!Inkscape::IO::file_test(test_filename.c_str(), G_FILE_TEST_EXISTS)) {
+ filename = test_filename;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool Export::exportRaster(
+ Geom::Rect const &area, unsigned long int const &width, unsigned long int const &height,
+ float const &dpi, guint32 bg_color, Glib::ustring const &filename, bool overwrite,
+ unsigned (*callback)(float, void *), void *data,
+ Inkscape::Extension::Output *extension, std::vector<SPItem *> *items)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!desktop)
+ return false;
+ SPDocument *doc = desktop->getDocument();
+
+ if (area.hasZeroArea() || width == 0 || height == 0) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The chosen area to be exported is invalid."));
+ sp_ui_error_dialog(_("The chosen area to be exported is invalid"));
+ return false;
+ }
+ if (filename.empty()) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("You have to enter a filename."));
+ sp_ui_error_dialog(_("You have to enter a filename"));
+ return false;
+ }
+
+ if (!extension || !extension->is_raster()) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Raster Export Error"));
+ sp_ui_error_dialog(_("Raster export Method is used for NON RASTER EXTENSION"));
+ return false;
+ }
+
+ float pHYs = extension->get_param_float("png_phys", dpi);
+ if (pHYs < 0.01) pHYs = dpi;
+
+ bool use_interlacing = extension->get_param_bool("png_interlacing", false);
+ int antialiasing = extension->get_param_int("png_antialias", 2); // Cairo anti aliasing
+ int zlib = extension->get_param_int("png_compression", 1); // Default is 6 for png, but 1 for non-png
+ auto val = extension->get_param_int("png_bitdepth", 99); // corresponds to RGBA 8
+
+ int bit_depth = pow(2, (val & 0x0F));
+ int color_type = (val & 0xF0) >> 4;
+
+ std::string path = absolutizePath(doc, Glib::filename_from_utf8(filename));
+ Glib::ustring dirname = Glib::path_get_dirname(path);
+
+ if (dirname.empty() ||
+ !Inkscape::IO::file_test(dirname.c_str(), (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) {
+ Glib::ustring safeDir = Inkscape::IO::sanitizeString(dirname.c_str());
+ Glib::ustring error =
+ g_strdup_printf(_("Directory <b>%s</b> does not exist or is not a directory.\n"), safeDir.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+ return false;
+ }
+
+ // Do the over-write protection now, since the png is just a temp file.
+ if (!overwrite && !sp_ui_overwrite_file(path.c_str())) {
+ return false;
+ }
+
+ auto fn = Glib::path_get_basename(path);
+ auto png_filename = path;
+ {
+ // Select the extension and set the filename to a temporary file
+ int tempfd_out = Glib::file_open_tmp(png_filename, "ink_ext_");
+ close(tempfd_out);
+ }
+
+ // Export Start Here
+ std::vector<SPItem *> selected;
+ if (items && items->size() > 0) {
+ selected = *items;
+ }
+
+ ExportResult result = sp_export_png_file(desktop->getDocument(), png_filename.c_str(), area, width, height, pHYs,
+ pHYs, // previously xdpi, ydpi.
+ bg_color, callback, data, true, selected,
+ use_interlacing, color_type, bit_depth, zlib, antialiasing);
+
+ bool failed = result == EXPORT_ERROR; // || prog_dialog->get_stopped();
+
+ if (failed) {
+ Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str());
+ Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>.\n"), safeFile.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+ return false;
+ } else if (result == EXPORT_OK) {
+ // Don't ask for preferences on every run.
+ try {
+ extension->export_raster(doc, png_filename, path.c_str(), false);
+ } catch (Inkscape::Extension::Output::save_failed &e) {
+ return false;
+ }
+ } else {
+ // Extensions have their own error popup, so this only tracks failures in the png step
+ desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Export aborted."));
+ return false;
+ }
+
+ Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str());
+ desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to <b>%s</b>."),
+ safeFile.c_str());
+
+ unlink(png_filename.c_str());
+ return true;
+}
+
+bool Export::exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *doc,
+ Glib::ustring const &filename,
+ bool overwrite, const std::vector<SPItem *> &items, SPPage *page)
+{
+ std::vector<SPPage *> pages;
+ if (page)
+ pages.push_back(page);
+ return exportVector(extension, doc, filename, overwrite, items, pages);
+}
+
+bool Export::exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *copy_doc,
+ Glib::ustring const &filename,
+ bool overwrite, const std::vector<SPItem *> &items, const std::vector<SPPage *> &pages)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (!desktop)
+ return false;
+
+ if (filename.empty()) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("You have to enter a filename."));
+ sp_ui_error_dialog(_("You have to enter a filename"));
+ return false;
+ }
+
+ if (!extension || extension->is_raster()) {
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Vector Export Error"));
+ sp_ui_error_dialog(_("Vector export Method is used for RASTER EXTENSION"));
+ return false;
+ }
+
+ std::string path = absolutizePath(copy_doc, Glib::filename_from_utf8(filename));
+ Glib::ustring dirname = Glib::path_get_dirname(path);
+ Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str());
+ Glib::ustring safeDir = Inkscape::IO::sanitizeString(dirname.c_str());
+
+ if (dirname.empty() ||
+ !Inkscape::IO::file_test(dirname.c_str(), (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) {
+ Glib::ustring error =
+ g_strdup_printf(_("Directory <b>%s</b> does not exist or is not a directory.\n"), safeDir.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+
+ return false;
+ }
+
+ // Do the over-write protection now
+ if (!overwrite && !sp_ui_overwrite_file(path.c_str())) {
+ return false;
+ }
+ copy_doc->ensureUpToDate();
+
+ std::vector<SPItem *> objects = items;
+ std::set<std::string> obj_ids;
+ std::set<std::string> page_ids;
+ Geom::OptRect page_rect;
+ for (auto page : pages) {
+ // Save the first page rect, must be done before everything else
+ if (!page_rect)
+ page_rect = page->getDesktopRect();
+
+ if (auto _id = page->getId()) {
+ page_ids.insert(std::string(_id));
+ }
+ // If page then our item set is limited to the overlapping items
+ auto page_items = page->getOverlappingItems(true, true);
+
+ if (items.empty()) {
+ // Items is page_items, remove all items not in this page.
+ objects.insert(objects.end(), page_items.begin(), page_items.end());
+ } else {
+ for (auto &item : page_items) {
+ item->getIds(obj_ids);
+ }
+ }
+ }
+
+ // Delete any pages not specified, delete all pages if none specified
+ auto &pm = copy_doc->getPageManager();
+
+ // Make weak pointers to pages, since deletePage() can delete more than just the requested page.
+ std::vector<SPWeakPtr<SPPage>> copy_pages;
+ copy_pages.reserve(pm.getPageCount());
+ for (auto *page : pm.getPages()) {
+ copy_pages.emplace_back(page);
+ }
+
+ // We refuse to delete anything if everything would be deleted.
+ for (auto &page : copy_pages) {
+ if (page) {
+ auto _id = page->getId();
+ if (_id && page_ids.find(_id) == page_ids.end()) {
+ pm.deletePage(page.get(), false);
+ }
+ }
+ }
+
+ // Page export ALWAYS restricts, even if nothing would be on the page.
+ if (!objects.empty() || !pages.empty()) {
+ std::vector<SPObject *> objects_to_export;
+ Inkscape::ObjectSet object_set(copy_doc);
+ for (auto &object : objects) {
+ auto _id = object->getId();
+ if (!_id || (!obj_ids.empty() && obj_ids.find(_id) == obj_ids.end())) {
+ // This item is off the page so can be ignored for export
+ continue;
+ }
+
+ SPObject *obj = copy_doc->getObjectById(_id);
+ if (!obj) {
+ Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>. (missing object)\n"), safeFile.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+
+ return false;
+ }
+ copy_doc->ensureUpToDate();
+
+ object_set.add(obj, true);
+ objects_to_export.push_back(obj);
+ }
+
+ copy_doc->getRoot()->cropToObjects(objects_to_export);
+
+ if (pages.empty()) {
+ object_set.fitCanvas(true, true);
+ }
+ }
+
+ // Remove all unused definitions
+ copy_doc->vacuumDocument();
+
+ try {
+ extension->save(copy_doc, path.c_str());
+ } catch (Inkscape::Extension::Output::save_failed &e) {
+ Glib::ustring safeFile = Inkscape::IO::sanitizeString(path.c_str());
+ Glib::ustring error = g_strdup_printf(_("Could not export to filename <b>%s</b>.\n"), safeFile.c_str());
+
+ desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error.c_str());
+ sp_ui_error_dialog(error.c_str());
+
+ return false;
+ }
+
+ desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to <b>%s</b>."),
+ safeFile.c_str());
+ return true;
+}
+
+std::string Export::filePathFromObject(SPDocument *doc, SPObject *obj, const Glib::ustring &file_entry_text)
+{
+ Glib::ustring id = _("bitmap");
+ if (obj && obj->getId()) {
+ id = obj->getId();
+ }
+ return filePathFromId(doc, id, file_entry_text);
+}
+
+std::string Export::filePathFromId(SPDocument *doc, Glib::ustring id, const Glib::ustring &file_entry_text)
+{
+ g_assert(!id.empty());
+
+ std::string directory;
+
+ if (!file_entry_text.empty()) {
+ directory = Glib::path_get_dirname(Glib::filename_from_utf8(file_entry_text));
+ }
+
+ if (directory.empty()) {
+ /* Grab document directory */
+ const gchar *docFilename = doc->getDocumentFilename();
+ if (docFilename) {
+ directory = Glib::path_get_dirname(docFilename);
+ }
+ }
+
+ if (directory.empty()) {
+ directory = Inkscape::IO::Resource::homedir_path();
+ }
+
+ return Glib::build_filename(directory, Glib::filename_from_utf8(id));
+}
+
+Glib::ustring Export::defaultFilename(SPDocument *doc, Glib::ustring &filename_entry_text, Glib::ustring extension)
+{
+ Glib::ustring filename;
+ if (doc && doc->getDocumentFilename()) {
+ filename = doc->getDocumentFilename();
+ //appendExtensionToFilename(filename, extension);
+ } else if (doc) {
+ filename = filePathFromId(doc, _("bitmap"), filename_entry_text);
+ filename = filename + extension;
+ }
+ return filename;
+}
+
+void set_export_bg_color(SPObject* object, guint32 color) {
+ if (object) {
+ object->setAttribute("inkscape:export-bgcolor", Inkscape::Util::rgba_color_to_string(color).c_str());
+ }
+}
+
+guint32 get_export_bg_color(SPObject* object, guint32 default_color) {
+ if (object) {
+ if (auto color = Inkscape::Util::string_to_rgba_color(object->getAttribute("inkscape:export-bgcolor"))) {
+ return *color;
+ }
+ }
+ return default_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 :
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/export.h b/src/ui/dialog/export.h
new file mode 100644
index 0000000..4ce4da3
--- /dev/null
+++ b/src/ui/dialog/export.h
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
+ *
+ * Copyright (C) 1999-2007, 2021 Authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SP_EXPORT_H
+#define SP_EXPORT_H
+
+#include <gtkmm.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/scrollprotected.h"
+
+class SPPage;
+
+namespace Inkscape {
+ class Preferences;
+ namespace Util {
+ class Unit;
+ }
+ namespace Extension {
+ class Output;
+ }
+
+namespace UI {
+namespace Dialog {
+ class SingleExport;
+ class BatchExport;
+
+enum notebook_page
+{
+ SINGLE_IMAGE = 0,
+ BATCH_EXPORT
+};
+
+void set_export_bg_color(SPObject* object, guint32 color);
+guint32 get_export_bg_color(SPObject* object, guint32 default_color);
+
+class Export : public DialogBase
+{
+public:
+ Export();
+ ~Export() override = default;
+
+private:
+ Glib::RefPtr<Gtk::Builder> builder;
+ Gtk::Box *container = nullptr; // Main Container
+ Gtk::Notebook *export_notebook = nullptr; // Notebook Container for single and batch export
+
+ SingleExport *single_image = nullptr;
+ BatchExport *batch_export = nullptr;
+
+ Inkscape::Preferences *prefs = nullptr;
+
+ // setup default values of widgets
+ void setDefaultNotebookPage();
+ std::map<notebook_page, int> pages;
+
+ sigc::connection notebook_signal;
+
+ // signals callback
+ void onNotebookPageSwitch(Widget *page, guint page_number);
+ void documentReplaced() override;
+ void desktopReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+
+public:
+ static std::string absolutizePath(SPDocument *doc, const std::string &filename);
+ static bool unConflictFilename(SPDocument *doc, Glib::ustring &filename, Glib::ustring const extension);
+ static std::string filePathFromObject(SPDocument *doc, SPObject *obj, const Glib::ustring &file_entry_text);
+ static std::string filePathFromId(SPDocument *doc, Glib::ustring id, const Glib::ustring &file_entry_text);
+ static Glib::ustring defaultFilename(SPDocument *doc, Glib::ustring &filename_entry_text, Glib::ustring extension);
+
+ static bool exportRaster(
+ Geom::Rect const &area, unsigned long int const &width, unsigned long int const &height,
+ float const &dpi, guint32 bg_color, Glib::ustring const &filename, bool overwrite,
+ unsigned (*callback)(float, void *), void *data,
+ Inkscape::Extension::Output *extension, std::vector<SPItem *> *items = nullptr);
+
+ static bool exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *doc, Glib::ustring const &filename,
+ bool overwrite, const std::vector<SPItem *> &items, SPPage *page);
+ static bool exportVector(
+ Inkscape::Extension::Output *extension, SPDocument *doc, Glib::ustring const &filename,
+ bool overwrite, const std::vector<SPItem *> &items, const std::vector<SPPage *> &pages);
+
+};
+
+} // 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/dialog/filedialog.cpp b/src/ui/dialog/filedialog.cpp
new file mode 100644
index 0000000..d56d0da
--- /dev/null
+++ b/src/ui/dialog/filedialog.cpp
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Implementation of the file dialog interfaces defined in filedialog.h.
+ */
+/* Authors:
+ * Bob Jamison
+ * Joel Holdsworth
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004-2007 Bob Jamison
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2007-2008 Joel Holdsworth
+ * Copyright (C) 2004-2008 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef _WIN32
+# include "filedialogimpl-win32.h"
+# include "preferences.h"
+#endif
+
+#include "filedialogimpl-gtkmm.h"
+
+#include "ui/dialog-events.h"
+#include "extension/output.h"
+
+#include <glibmm/convert.h>
+
+namespace Inkscape
+{
+namespace UI
+{
+namespace Dialog
+{
+
+/*#########################################################################
+### U T I L I T Y
+#########################################################################*/
+
+bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext)
+{
+ int strLen = str.length();
+ int extLen = ext.length();
+ if (extLen > strLen)
+ return false;
+ int strpos = strLen-1;
+ for (int extpos = extLen-1 ; extpos>=0 ; extpos--, strpos--)
+ {
+ Glib::ustring::value_type ch = str[strpos];
+ if (ch != ext[extpos])
+ {
+ if ( ((ch & 0xff80) != 0) ||
+ static_cast<Glib::ustring::value_type>( g_ascii_tolower( static_cast<gchar>(0x07f & ch) ) ) != ext[extpos] )
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool isValidImageFile(const Glib::ustring &fileName)
+{
+ std::vector<Gdk::PixbufFormat>formats = Gdk::Pixbuf::get_formats();
+ for (auto format : formats)
+ {
+ std::vector<Glib::ustring>extensions = format.get_extensions();
+ for (auto ext : extensions)
+ {
+ if (hasSuffix(fileName, ext))
+ return true;
+ }
+ }
+ return false;
+}
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+/**
+ * Public factory. Called by file.cpp, among others.
+ */
+FileOpenDialog *FileOpenDialog::create(Gtk::Window &parentWindow,
+ const Glib::ustring &path,
+ FileDialogType fileTypes,
+ const char *title)
+{
+#ifdef _WIN32
+ FileOpenDialog *dialog = NULL;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool( "/options/desktopintegration/value")) {
+ dialog = new FileOpenDialogImplWin32(parentWindow, path, fileTypes, title);
+ } else {
+ dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title);
+ }
+#else
+ FileOpenDialog *dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title);
+#endif
+
+ return dialog;
+}
+
+//########################################################################
+//# F I L E S A V E
+//########################################################################
+
+/**
+ * Public factory method. Used in file.cpp
+ */
+FileSaveDialog *FileSaveDialog::create(Gtk::Window& parentWindow,
+ const Glib::ustring &path,
+ FileDialogType fileTypes,
+ const char *title,
+ const Glib::ustring &default_key,
+ const gchar *docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method)
+{
+#ifdef _WIN32
+ FileSaveDialog *dialog = NULL;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool( "/options/desktopintegration/value")) {
+ dialog = new FileSaveDialogImplWin32(parentWindow, path, fileTypes, title, default_key, docTitle, save_method);
+ } else {
+ dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method);
+ }
+#else
+ FileSaveDialog *dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method);
+#endif
+ return dialog;
+}
+
+Glib::ustring FileSaveDialog::getDocTitle()
+{
+ return myDocTitle;
+}
+
+void FileSaveDialog::appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension)
+{
+ if (!outputExtension)
+ return;
+
+ try {
+ bool appendExtension = true;
+ Glib::ustring utf8Name = Glib::filename_to_utf8( path );
+ Glib::ustring::size_type pos = utf8Name.rfind('.');
+ if ( pos != Glib::ustring::npos ) {
+ Glib::ustring trail = utf8Name.substr( pos );
+ Glib::ustring foldedTrail = trail.casefold();
+ if ( (trail == ".")
+ | (foldedTrail != Glib::ustring( outputExtension->get_extension() ).casefold()
+ && ( knownExtensions.find(foldedTrail) != knownExtensions.end() ) ) ) {
+ utf8Name = utf8Name.erase( pos );
+ } else {
+ appendExtension = false;
+ }
+ }
+
+ if (appendExtension) {
+ utf8Name = utf8Name + outputExtension->get_extension();
+ _filename = Glib::filename_from_utf8(utf8Name);
+ }
+ } catch ( Glib::ConvertError& e ) {
+ // ignore
+ }
+}
+
+
+} //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/dialog/filedialog.h b/src/ui/dialog/filedialog.h
new file mode 100644
index 0000000..49cbcd8
--- /dev/null
+++ b/src/ui/dialog/filedialog.h
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Virtual base definitions for native file dialogs
+ */
+/* Authors:
+ * Bob Jamison <rwjj@earthlink.net>
+ * Joel Holdsworth
+ * Inkscape Guys
+ *
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2007-2008 Joel Holdsworth
+ * Copyright (C) 2004-2008, Inkscape Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __FILE_DIALOG_H__
+#define __FILE_DIALOG_H__
+
+#include <vector>
+#include <set>
+
+#include "extension/system.h"
+
+#include <glibmm/ustring.h>
+
+class SPDocument;
+
+namespace Inkscape {
+namespace Extension {
+class Extension;
+class Output;
+}
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * Used for setting filters and options, and
+ * reading them back from user selections.
+ */
+enum FileDialogType
+{
+ SVG_TYPES,
+ IMPORT_TYPES,
+ EXPORT_TYPES,
+ EXE_TYPES,
+ SWATCH_TYPES,
+ CUSTOM_TYPE
+};
+
+/**
+ * Used for returning the type selected in a SaveAs
+ */
+enum FileDialogSelectionType
+{
+ SVG_NAMESPACE,
+ SVG_NAMESPACE_WITH_EXTENSIONS
+};
+
+/**
+ * Return true if the string ends with the given suffix
+ */
+bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext);
+
+/**
+ * Return true if the image is loadable by Gdk, else false
+ */
+bool isValidImageFile(const Glib::ustring &fileName);
+
+class FileDialog
+{
+public:
+ /**
+ * Return the 'key' (filetype) of the selection, if any
+ * @return a pointer to a string if successful (which must
+ * be later freed with g_free(), else NULL.
+ */
+ virtual Inkscape::Extension::Extension *getExtension() { return _extension; }
+ virtual void setExtension(Inkscape::Extension::Extension *key) { _extension = key; }
+
+ const Glib::ustring &getFilename() { return _filename; }
+ void setFilename(const Glib::ustring &path) { _filename = path; }
+
+ /**
+ * Show file selector.
+ * @return the selected path if user selected one, else NULL
+ */
+ virtual bool show() = 0;
+
+ /**
+ * Add a filter menu to the file dialog.
+ */
+ virtual void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "",
+ Inkscape::Extension::Extension *mod = nullptr) = 0;
+
+ /**
+ * Get the current directory of the file dialog.
+ */
+ virtual Glib::ustring getCurrentDirectory() = 0;
+
+protected:
+ // The selected extension
+ Inkscape::Extension::Extension *_extension;
+
+ // Filename that was given
+ Glib::ustring _filename;
+};
+
+/**
+ * This class provides an implementation-independent API for
+ * file "Open" dialogs. Using a standard interface obviates the need
+ * for ugly #ifdefs in file open code
+ */
+class FileOpenDialog : public FileDialog
+{
+public:
+ // Constructor. Do not call directly. Use the factory.
+ FileOpenDialog() = default;
+ virtual ~FileOpenDialog() = default;
+
+ /**
+ * Factory.
+ * @param path the directory where to start searching
+ * @param fileTypes one of FileDialogTypes
+ * @param title the title of the dialog
+ */
+ static FileOpenDialog *create(Gtk::Window& parentWindow,
+ const Glib::ustring &path,
+ FileDialogType fileTypes,
+ const char *title);
+
+ virtual std::vector<Glib::ustring> getFilenames() = 0;
+
+protected:
+
+}; //FileOpenDialog
+
+
+/**
+ * This class provides an implementation-independent API for
+ * file "Save" dialogs.
+ */
+class FileSaveDialog : public FileDialog
+{
+public:
+ // Constructor. Do not call directly. Use the factory.
+ FileSaveDialog() = default;
+ virtual ~FileSaveDialog() = default;
+
+ /**
+ * Factory.
+ * @param path the directory where to start searching
+ * @param fileTypes one of FileDialogTypes
+ * @param title the title of the dialog
+ * @param key a list of file types from which the user can select
+ */
+ static FileSaveDialog *create(Gtk::Window& parentWindow,
+ const Glib::ustring &path,
+ FileDialogType fileTypes,
+ const char *title,
+ const Glib::ustring &default_key,
+ const gchar *docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method);
+
+ /**
+ * Get the document title chosen by the user.
+ * Valid after an [OK]
+ */
+ Glib::ustring getDocTitle ();
+
+protected:
+ // Doc Title that was given
+ Glib::ustring myDocTitle;
+
+ // List of known file extensions.
+ std::map<Glib::ustring, Inkscape::Extension::Output *> knownExtensions;
+
+ void appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension);
+
+}; //FileSaveDialog
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif /* __FILE_DIALOG_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/dialog/filedialogimpl-gtkmm.cpp b/src/ui/dialog/filedialogimpl-gtkmm.cpp
new file mode 100644
index 0000000..44667b6
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-gtkmm.cpp
@@ -0,0 +1,676 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Implementation of the file dialog interfaces defined in filedialogimpl.h.
+ */
+/* Authors:
+ * Bob Jamison
+ * Joel Holdsworth
+ * Bruno Dilly
+ * Other dudes from The Inkscape Organization
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-2007 Bob Jamison
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2007-2008 Joel Holdsworth
+ * Copyright (C) 2004-2007 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "filedialogimpl-gtkmm.h"
+
+#include <glibmm/convert.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/regex.h>
+#include <gtkmm/expander.h>
+#include <iostream>
+
+#include "document.h"
+#include "extension/db.h"
+#include "extension/input.h"
+#include "extension/output.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "path-prefix.h"
+#include "preferences.h"
+#include "ui/dialog-events.h"
+#include "ui/util.h"
+#include "ui/view/svg-view-widget.h"
+
+// Routines from file.cpp
+#undef INK_DUMP_FILENAME_CONV
+
+#ifdef INK_DUMP_FILENAME_CONV
+void dump_str(const gchar *str, const gchar *prefix);
+void dump_ustr(const Glib::ustring &ustr);
+#endif
+
+/**
+ * Information stored about all save and open filters applied to the dialog.
+ */
+struct FilterListClass : public Gtk::TreeModelColumnRecord
+{
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<Inkscape::Extension::Extension *> extension;
+ Gtk::TreeModelColumn<bool> enabled;
+
+ FilterListClass()
+ {
+ add(label);
+ add(extension);
+ add(enabled);
+ }
+};
+FilterListClass FilterList;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*#########################################################################
+### F I L E D I A L O G B A S E C L A S S
+#########################################################################*/
+
+void FileDialogBaseGtk::internalSetup()
+{
+ filterComboBox = dynamic_cast<Gtk::ComboBoxText *>(get_widget_by_name(this, "GtkComboBoxText"));
+ g_assert(filterComboBox);
+
+ filterStore = Gtk::ListStore::create(FilterList);
+ filterComboBox->set_model(filterStore);
+ filterComboBox->signal_changed().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::filterChangedCallback));
+
+ auto cell_renderer = filterComboBox->get_first_cell();
+ if (cell_renderer) {
+ // Add enabled column to cell_renderer property
+ filterComboBox->add_attribute(cell_renderer->property_sensitive(), FilterList.enabled);
+ }
+
+ // Open executable file dialogs don't need the preview panel
+ if (_dialogType != EXE_TYPES) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool enablePreview = prefs->getBool(preferenceBase + "/enable_preview", true);
+ bool enableSVGExport = prefs->getBool(preferenceBase + "/enable_svgexport", false);
+
+ previewCheckbox.set_label(Glib::ustring(_("Enable preview")));
+ previewCheckbox.set_active(enablePreview);
+
+ previewCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_updatePreviewCallback));
+
+ svgexportCheckbox.set_label(Glib::ustring(_("Export as SVG 1.1 per settings in Preferences dialog")));
+ svgexportCheckbox.set_active(enableSVGExport);
+
+ svgexportCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_svgexportEnabledCB));
+
+ // Catch selection-changed events, so we can adjust the text widget
+ signal_update_preview().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_updatePreviewCallback));
+
+ //###### Add a preview widget
+ set_preview_widget(svgPreview);
+ set_preview_widget_active(enablePreview);
+ set_use_preview_label(false);
+ }
+}
+
+
+void FileDialogBaseGtk::cleanup(bool showConfirmed)
+{
+ if (_dialogType != EXE_TYPES) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (showConfirmed) {
+ prefs->setBool(preferenceBase + "/enable_preview", previewCheckbox.get_active());
+ }
+ }
+}
+
+void FileDialogBaseGtk::_svgexportEnabledCB()
+{
+ bool enabled = svgexportCheckbox.get_active();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool(preferenceBase + "/enable_svgexport", enabled);
+}
+
+/**
+ * Callback for checking if the preview needs to be redrawn
+ */
+void FileDialogBaseGtk::_updatePreviewCallback()
+{
+ bool enabled = previewCheckbox.get_active();
+
+ set_preview_widget_active(enabled);
+
+ if (!enabled)
+ return;
+
+ Glib::ustring fileName = get_preview_filename();
+ if (fileName.empty()) {
+ fileName = get_preview_uri();
+ }
+
+ if (!fileName.empty()) {
+ svgPreview.set(fileName, _dialogType);
+ } else {
+ svgPreview.showNoPreview();
+ }
+}
+
+Glib::RefPtr<Gtk::FileFilter> FileDialogBaseGtk::addFilter(const Glib::ustring &name, Glib::ustring ext,
+ Inkscape::Extension::Extension *extension)
+{
+ auto filter = Gtk::FileFilter::create();
+ filter->set_name(name);
+ add_filter(filter);
+
+ if (!ext.empty()) {
+ filter->add_pattern(extToPattern(ext));
+ }
+
+ // ListStore is populated by add_filter, so get the last row to add the rest
+ Gtk::TreeRow row;
+ for (auto child : filterStore->children()) {
+ row = child;
+ }
+ if (row) {
+ row[FilterList.extension] = extension;
+ row[FilterList.enabled] = !extension || !extension->deactivated();
+ }
+ return filter;
+}
+
+// Replace this with add_suffix in Gtk4
+Glib::ustring FileDialogBaseGtk::extToPattern(const Glib::ustring &extension) const
+{
+ Glib::ustring pattern = "*";
+ for (unsigned int ch : extension) {
+ if (Glib::Unicode::isalpha(ch)) {
+ pattern += '[';
+ pattern += Glib::Unicode::toupper(ch);
+ pattern += Glib::Unicode::tolower(ch);
+ pattern += ']';
+ } else {
+ pattern += ch;
+ }
+ }
+ return pattern;
+}
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+/**
+ * Constructor. Not called directly. Use the factory.
+ */
+FileOpenDialogImplGtk::FileOpenDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir,
+ FileDialogType fileTypes, const Glib::ustring &title)
+ : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_OPEN, fileTypes, "/dialogs/open")
+{
+
+
+ if (_dialogType == EXE_TYPES) {
+ /* One file at a time */
+ set_select_multiple(false);
+ } else {
+ /* And also Multiple Files */
+ set_select_multiple(true);
+ }
+
+ set_local_only(false);
+
+ /* Set our dialog type (open, import, etc...)*/
+ _dialogType = fileTypes;
+
+ /* Set the pwd and/or the filename */
+ if (dir.size() > 0) {
+ Glib::ustring udir(dir);
+ Glib::ustring::size_type len = udir.length();
+ // leaving a trailing backslash on the directory name leads to the infamous
+ // double-directory bug on win32
+ if (len != 0 && udir[len - 1] == '\\')
+ udir.erase(len - 1);
+ if (_dialogType == EXE_TYPES) {
+ set_filename(udir.c_str());
+ } else {
+ set_current_folder(udir.c_str());
+ }
+ }
+
+ if (_dialogType != EXE_TYPES) {
+ set_extra_widget(previewCheckbox);
+ }
+
+ //###### Add the file types menu
+ createFilterMenu();
+
+ add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ set_default(*add_button(_("_Open"), Gtk::RESPONSE_OK));
+
+ //###### Allow easy access to our examples folder
+
+ using namespace Inkscape::IO::Resource;
+ auto examplesdir = get_path_string(SYSTEM, EXAMPLES);
+ if (Glib::file_test(examplesdir, Glib::FILE_TEST_IS_DIR) && //
+ Glib::path_is_absolute(examplesdir)) {
+ add_shortcut_folder(examplesdir);
+ }
+}
+
+void FileOpenDialogImplGtk::createFilterMenu()
+{
+ if (_dialogType == CUSTOM_TYPE) {
+ return;
+ }
+
+ addFilter(_("All Files"), "*");
+
+ if (_dialogType != EXE_TYPES) {
+ auto allInkscapeFilter = addFilter(_("All Inkscape Files"));
+ auto allImageFilter = addFilter(_("All Images"));
+ auto allVectorFilter = addFilter(_("All Vectors"));
+ auto allBitmapFilter = addFilter(_("All Bitmaps"));
+
+ // patterns added dynamically below
+ Inkscape::Extension::DB::InputList extension_list;
+ Inkscape::Extension::db.get_input_list(extension_list);
+
+ for (auto imod : extension_list)
+ {
+ addFilter(imod->get_filetypename(true), imod->get_extension(), imod);
+
+ auto upattern = extToPattern(imod->get_extension());
+ allInkscapeFilter->add_pattern(upattern);
+ if (strncmp("image", imod->get_mimetype(), 5) == 0)
+ allImageFilter->add_pattern(upattern);
+
+ // I don't know of any other way to define "bitmap" formats other than by listing them
+ if (strncmp("image/png", imod->get_mimetype(), 9) == 0 ||
+ strncmp("image/jpeg", imod->get_mimetype(), 10) == 0 ||
+ strncmp("image/gif", imod->get_mimetype(), 9) == 0 ||
+ strncmp("image/x-icon", imod->get_mimetype(), 12) == 0 ||
+ strncmp("image/x-navi-animation", imod->get_mimetype(), 22) == 0 ||
+ strncmp("image/x-cmu-raster", imod->get_mimetype(), 18) == 0 ||
+ strncmp("image/x-xpixmap", imod->get_mimetype(), 15) == 0 ||
+ strncmp("image/bmp", imod->get_mimetype(), 9) == 0 ||
+ strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18) == 0 ||
+ strncmp("image/tiff", imod->get_mimetype(), 10) == 0 ||
+ strncmp("image/x-xbitmap", imod->get_mimetype(), 15) == 0 ||
+ strncmp("image/x-tga", imod->get_mimetype(), 11) == 0 ||
+ strncmp("image/x-pcx", imod->get_mimetype(), 11) == 0)
+ {
+ allBitmapFilter->add_pattern(upattern);
+ } else {
+ allVectorFilter->add_pattern(upattern);
+ }
+ }
+ }
+ return;
+}
+
+/**
+ * Show this dialog modally. Return true if user hits [OK]
+ */
+bool FileOpenDialogImplGtk::show()
+{
+ set_modal(TRUE); // Window
+ sp_transientize(GTK_WIDGET(gobj())); // Make transient
+ gint b = run(); // Dialog
+ svgPreview.showNoPreview();
+ hide();
+
+ if (b == Gtk::RESPONSE_OK) {
+ if (auto iter = filterComboBox->get_active()) {
+ setExtension((*iter)[FilterList.extension]);
+ }
+
+ auto fn = get_filename();
+ setFilename(fn.empty() ? get_uri() : Glib::ustring(fn));
+
+ cleanup(true);
+ return true;
+ } else {
+ cleanup(false);
+ return false;
+ }
+}
+
+
+/**
+ * To Get Multiple filenames selected at-once.
+ */
+std::vector<Glib::ustring> FileOpenDialogImplGtk::getFilenames()
+{
+ auto result_tmp = get_filenames();
+
+ // Copy filenames to a vector of type Glib::ustring
+ std::vector<Glib::ustring> result;
+
+ for (auto it : result_tmp)
+ result.emplace_back(it);
+
+ if (result.empty()) {
+ result = get_uris();
+ }
+
+ return result;
+}
+
+Glib::ustring FileOpenDialogImplGtk::getCurrentDirectory()
+{
+ return get_current_folder();
+}
+
+
+
+//########################################################################
+//# F I L E S A V E
+//########################################################################
+
+/**
+ * Constructor
+ */
+FileSaveDialogImplGtk::FileSaveDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir,
+ FileDialogType fileTypes, const Glib::ustring &title,
+ const Glib::ustring & /*default_key*/, const gchar *docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method)
+ : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_SAVE, fileTypes,
+ (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "/dialogs/save_copy"
+ : "/dialogs/save_as")
+ , save_method(save_method)
+ , fromCB(false)
+ , checksBox(Gtk::ORIENTATION_VERTICAL)
+ , childBox(Gtk::ORIENTATION_HORIZONTAL)
+{
+ FileSaveDialog::myDocTitle = docTitle;
+
+ /* One file at a time */
+ set_select_multiple(false);
+
+ set_local_only(false);
+
+ /* Set our dialog type (save, export, etc...)*/
+ _dialogType = fileTypes;
+
+ /* Set the pwd and/or the filename */
+ if (dir.size() > 0) {
+ Glib::ustring udir(dir);
+ Glib::ustring::size_type len = udir.length();
+ // leaving a trailing backslash on the directory name leads to the infamous
+ // double-directory bug on win32
+ if ((len != 0) && (udir[len - 1] == '\\')) {
+ udir.erase(len - 1);
+ }
+ setFilename(udir);
+ }
+
+ //###### Do we want the .xxx extension automatically added?
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ fileTypeCheckbox.set_label(Glib::ustring(_("Append filename extension automatically")));
+ if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) {
+ fileTypeCheckbox.set_active(prefs->getBool("/dialogs/save_copy/append_extension", true));
+ } else {
+ fileTypeCheckbox.set_active(prefs->getBool("/dialogs/save_as/append_extension", true));
+ }
+
+ if (_dialogType != CUSTOM_TYPE)
+ createFilterMenu();
+
+ childBox.pack_start(checksBox);
+ checksBox.pack_start(fileTypeCheckbox);
+ checksBox.pack_start(previewCheckbox);
+ checksBox.pack_start(svgexportCheckbox);
+
+ set_extra_widget(childBox);
+
+ // Let's do some customization
+ fileNameEntry = dynamic_cast<Gtk::Entry *>(get_widget_by_name(this, "GtkEntry"));
+ if (fileNameEntry) {
+ // Catch when user hits [return] on the text field
+ fileNameEntry->signal_activate().connect(
+ sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameEntryChangedCallback));
+ }
+ if (auto expander = dynamic_cast<Gtk::Expander *>(get_widget_by_name(this, "GtkExpander"))) {
+ // Always show the file list
+ expander->set_expanded(true);
+ }
+
+ signal_selection_changed().connect(sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameChanged));
+
+ // allow easy access to the user's own templates folder
+ using namespace Inkscape::IO::Resource;
+ char const *templates = Inkscape::IO::Resource::get_path(USER, TEMPLATES);
+ if (Inkscape::IO::file_test(templates, G_FILE_TEST_EXISTS) &&
+ Inkscape::IO::file_test(templates, G_FILE_TEST_IS_DIR) && g_path_is_absolute(templates)) {
+ add_shortcut_folder(templates);
+ }
+
+ add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+ set_default(*add_button(_("_Save"), Gtk::RESPONSE_OK));
+
+ show_all_children();
+}
+
+/**
+ * Callback for fileNameEntry widget
+ */
+void FileSaveDialogImplGtk::fileNameEntryChangedCallback()
+{
+ if (!fileNameEntry)
+ return;
+
+ Glib::ustring fileName = fileNameEntry->get_text();
+ if (!Glib::get_charset()) // If we are not utf8
+ fileName = Glib::filename_to_utf8(fileName);
+
+ // g_message("User hit return. Text is '%s'\n", fileName.c_str());
+
+ if (!Glib::path_is_absolute(fileName)) {
+ // try appending to the current path
+ // not this way: fileName = get_current_folder() + "/" + fileName;
+ std::vector<Glib::ustring> pathSegments;
+ pathSegments.emplace_back(get_current_folder());
+ pathSegments.push_back(fileName);
+ fileName = Glib::build_filename(pathSegments);
+ }
+
+ // g_message("path:'%s'\n", fileName.c_str());
+
+ if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) {
+ set_current_folder(fileName);
+ } else if (/*Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)*/ true) {
+ // dialog with either (1) select a regular file or (2) cd to dir
+ // simulate an 'OK'
+ set_filename(fileName);
+ response(Gtk::RESPONSE_OK);
+ }
+}
+
+
+
+/**
+ * Callback for fileNameEntry widget
+ */
+void FileSaveDialogImplGtk::filterChangedCallback()
+{
+ if (auto iter = filterComboBox->get_active())
+ setExtension((*iter)[FilterList.extension]);
+ if (!fromCB)
+ updateNameAndExtension();
+}
+
+void FileSaveDialogImplGtk::fileNameChanged() {
+ Glib::ustring name = get_filename();
+ Glib::ustring::size_type pos = name.rfind('.');
+ if ( pos == Glib::ustring::npos ) return;
+ Glib::ustring ext = name.substr( pos ).casefold();
+ if (auto output = dynamic_cast<Inkscape::Extension::Output *>(_extension))
+ if (Glib::ustring(output->get_extension()).casefold() == ext)
+ return;
+ if (knownExtensions.find(ext) == knownExtensions.end()) return;
+ fromCB = true;
+ filterComboBox->set_active_text(knownExtensions[ext]->get_filetypename(true));
+}
+
+void FileSaveDialogImplGtk::createFilterMenu()
+{
+ Inkscape::Extension::DB::OutputList extension_list;
+ Inkscape::Extension::db.get_output_list(extension_list);
+ knownExtensions.clear();
+
+ addFilter(_("Guess from extension"), "*");
+
+ for (auto omod : extension_list) {
+ // Export types are either exported vector types, or any raster type.
+ if (!omod->is_exported() && omod->is_raster() != (_dialogType == EXPORT_TYPES))
+ continue;
+
+ // This extension is limited to save copy only.
+ if (omod->savecopy_only() && save_method != Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY)
+ continue;
+
+ Glib::ustring extension = omod->get_extension();
+ addFilter(omod->get_filetypename(true), extension, omod);
+ knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(extension.casefold(), omod));
+ }
+
+ filterComboBox->set_active(0);
+ filterChangedCallback(); // call at least once to set the filter
+}
+
+/**
+ * Show this dialog modally. Return true if user hits [OK]
+ */
+bool FileSaveDialogImplGtk::show()
+{
+ change_path(getFilename());
+ set_modal(TRUE); // Window
+ sp_transientize(GTK_WIDGET(gobj())); // Make transient
+ gint b = run(); // Dialog
+ svgPreview.showNoPreview();
+ set_preview_widget_active(false);
+ hide();
+
+ if (b == Gtk::RESPONSE_OK) {
+ updateNameAndExtension();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ // Store changes of the "Append filename automatically" checkbox back to preferences.
+ if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) {
+ prefs->setBool("/dialogs/save_copy/append_extension", fileTypeCheckbox.get_active());
+ } else {
+ prefs->setBool("/dialogs/save_as/append_extension", fileTypeCheckbox.get_active());
+ }
+
+ auto extension = getExtension();
+ Inkscape::Extension::store_file_extension_in_prefs((extension != nullptr ? extension->get_id() : ""), save_method);
+
+ cleanup(true);
+
+ return true;
+ } else {
+ cleanup(false);
+ return false;
+ }
+}
+
+void FileSaveDialogImplGtk::setExtension(Inkscape::Extension::Extension *key)
+{
+ // If no pointer to extension is passed in, look up based on filename extension.
+ if (!key) {
+ auto fn = getFilename().casefold();
+
+ for (auto const &iter : knownExtensions) {
+ auto ext = Glib::ustring(iter.second->get_extension()).casefold();
+ if (Glib::str_has_suffix(fn, ext))
+ key = iter.second;
+ }
+ }
+
+ FileDialog::setExtension(key);
+
+ // Ensure the proper entry in the combo box is selected.
+ if (auto omod = dynamic_cast<Inkscape::Extension::Output *>(key)) {
+ filterComboBox->set_active_text(omod->get_filetypename(true));
+ }
+}
+
+Glib::ustring FileSaveDialogImplGtk::getCurrentDirectory()
+{
+ return get_current_folder();
+}
+
+
+/**
+ * Change the default save path location.
+ */
+void FileSaveDialogImplGtk::change_path(const Glib::ustring &path)
+{
+ setFilename(path);
+
+ if (Glib::file_test(_filename, Glib::FILE_TEST_IS_DIR)) {
+ // fprintf(stderr,"set_current_folder(%s)\n",_filename.c_str());
+ set_current_folder(_filename);
+ } else {
+ // fprintf(stderr,"set_filename(%s)\n",_filename.c_str());
+ if (Glib::file_test(_filename, Glib::FILE_TEST_EXISTS)) {
+ set_filename(_filename);
+ } else {
+ std::string dirName = Glib::path_get_dirname(_filename);
+ if (dirName != get_current_folder()) {
+ set_current_folder(dirName);
+ }
+ }
+ Glib::ustring basename = Glib::path_get_basename(_filename);
+ // fprintf(stderr,"set_current_name(%s)\n",basename.c_str());
+ try
+ {
+ set_current_name(Glib::filename_to_utf8(basename));
+ }
+ catch (Glib::ConvertError &e)
+ {
+ g_warning("Error converting save filename to UTF-8.");
+ // try a fallback.
+ set_current_name(basename);
+ }
+ }
+}
+
+void FileSaveDialogImplGtk::updateNameAndExtension()
+{
+ // Pick up any changes the user has typed in.
+ Glib::ustring tmp = get_filename();
+
+ if (tmp.empty()) {
+ tmp = get_uri();
+ }
+
+ if (!tmp.empty()) {
+ setFilename(tmp);
+ }
+
+ auto output = dynamic_cast<Inkscape::Extension::Output *>(getExtension());
+ if (fileTypeCheckbox.get_active() && output) {
+ // Append the file extension if it's not already present and display it in the file name entry field
+ appendExtension(_filename, output);
+ change_path(_filename);
+ }
+}
+
+
+} // 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/dialog/filedialogimpl-gtkmm.h b/src/ui/dialog/filedialogimpl-gtkmm.h
new file mode 100644
index 0000000..9f3d085
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-gtkmm.h
@@ -0,0 +1,274 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h
+ */
+/* Authors:
+ * Bob Jamison
+ * Johan Engelen <johan@shouraizou.nl>
+ * Joel Holdsworth
+ * Bruno Dilly
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004-2008 Authors
+ * Copyright (C) 2004-2007 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __FILE_DIALOGIMPL_H__
+#define __FILE_DIALOGIMPL_H__
+
+//Gtk includes
+#include <gtkmm/filechooserdialog.h>
+#include <glib/gstdio.h>
+
+#include "filedialog.h"
+#include "svg-preview.h"
+
+namespace Gtk {
+class CheckButton;
+class ComboBoxText;
+class Expander;
+}
+
+namespace Inkscape {
+ class URI;
+
+namespace UI {
+
+namespace View {
+ class SVGViewWidget;
+}
+
+namespace Dialog {
+
+/*#########################################################################
+### Utility
+#########################################################################*/
+
+void
+findEntryWidgets(Gtk::Container *parent,
+ std::vector<Gtk::Entry *> &result);
+
+void
+findExpanderWidgets(Gtk::Container *parent,
+ std::vector<Gtk::Expander *> &result);
+
+/*#########################################################################
+### F I L E D I A L O G B A S E C L A S S
+#########################################################################*/
+
+/**
+ * This class is the base implementation for the others. This
+ * reduces redundancies and bugs.
+ */
+class FileDialogBaseGtk : public Gtk::FileChooserDialog
+{
+public:
+
+ FileDialogBaseGtk(Gtk::Window& parentWindow, const Glib::ustring &title,
+ Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) :
+ Gtk::FileChooserDialog(parentWindow, title, dialogType),
+ preferenceBase(preferenceBase ? preferenceBase : "unknown"),
+ _dialogType(type)
+ {
+ internalSetup();
+ }
+
+ FileDialogBaseGtk(Gtk::Window& parentWindow, const char *title,
+ Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) :
+ Gtk::FileChooserDialog(parentWindow, title, dialogType),
+ preferenceBase(preferenceBase ? preferenceBase : "unknown"),
+ _dialogType(type)
+ {
+ internalSetup();
+ }
+
+ ~FileDialogBaseGtk() override = default;
+
+ /**
+ * Add a Gtk filter to our specially controlled filter dropdown.
+ */
+ Glib::RefPtr<Gtk::FileFilter> addFilter(const Glib::ustring &name, Glib::ustring pattern = "",
+ Inkscape::Extension::Extension *mod = nullptr);
+
+ Glib::ustring extToPattern(const Glib::ustring &extension) const;
+
+ virtual void filterChangedCallback() {}
+
+protected:
+ void cleanup( bool showConfirmed );
+
+ Glib::ustring const preferenceBase;
+ /**
+ * What type of 'open' are we? (open, import, place, etc)
+ */
+ FileDialogType _dialogType;
+
+ /**
+ * Our svg preview widget
+ */
+ SVGPreview svgPreview;
+
+ /**
+ * Child widgets
+ */
+ Gtk::CheckButton previewCheckbox;
+ Gtk::CheckButton svgexportCheckbox;
+
+ /**
+ * Aquired Widgets
+ */
+ Gtk::ComboBoxText *filterComboBox;
+
+private:
+ void internalSetup();
+
+ /**
+ * Callback for seeing if the preview needs to be drawn
+ */
+ void _updatePreviewCallback();
+
+ /**
+ * Callback to for SVG 2 to SVG 1.1 export.
+ */
+ void _svgexportEnabledCB();
+
+ /**
+ * Overriden filter store.
+ */
+ Glib::RefPtr<Gtk::ListStore> filterStore;
+};
+
+
+
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+/**
+ * Our implementation class for the FileOpenDialog interface..
+ */
+class FileOpenDialogImplGtk : public FileOpenDialog, public FileDialogBaseGtk
+{
+public:
+
+ FileOpenDialogImplGtk(Gtk::Window& parentWindow,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const Glib::ustring &title);
+
+ ~FileOpenDialogImplGtk() override = default;
+
+ bool show() override;
+
+ Glib::ustring getFilename();
+
+ std::vector<Glib::ustring> getFilenames() override;
+
+ Glib::ustring getCurrentDirectory() override;
+
+ void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "",
+ Inkscape::Extension::Extension *mod = nullptr) override
+ {
+ addFilter(name, pattern, mod);
+ }
+
+private:
+
+ /**
+ * Create a filter menu for this type of dialog
+ */
+ void createFilterMenu();
+};
+
+
+
+//########################################################################
+//# F I L E S A V E
+//########################################################################
+
+/**
+ * Our implementation of the FileSaveDialog interface.
+ */
+class FileSaveDialogImplGtk : public FileSaveDialog, public FileDialogBaseGtk
+{
+
+public:
+ FileSaveDialogImplGtk(Gtk::Window &parentWindow,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const Glib::ustring &title,
+ const Glib::ustring &default_key,
+ const gchar* docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method);
+
+ ~FileSaveDialogImplGtk() override = default;
+
+ bool show() override;
+
+ Glib::ustring getCurrentDirectory() override;
+
+ void setExtension(Inkscape::Extension::Extension *key) override;
+
+ void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "",
+ Inkscape::Extension::Extension *mod = nullptr) override
+ {
+ addFilter(name, pattern, mod);
+ }
+
+private:
+ //void change_title(const Glib::ustring& title);
+ void change_path(const Glib::ustring& path);
+ void updateNameAndExtension();
+
+ /**
+ * The file save method (essentially whether the dialog was invoked by "Save as ..." or "Save a
+ * copy ..."), which is used to determine file extensions and save paths.
+ */
+ Inkscape::Extension::FileSaveMethod save_method;
+
+ /**
+ * Fix to allow the user to type the file name
+ */
+ Gtk::Entry *fileNameEntry;
+ Gtk::Box childBox;
+ Gtk::Box checksBox;
+ Gtk::CheckButton fileTypeCheckbox;
+
+ /**
+ * Callback for user input into fileNameEntry
+ */
+ void filterChangedCallback() override;
+
+ /**
+ * Create a filter menu for this type of dialog
+ */
+ void createFilterMenu();
+
+ /**
+ * Callback for user input into fileNameEntry
+ */
+ void fileNameEntryChangedCallback();
+ void fileNameChanged();
+ bool fromCB;
+};
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif /*__FILE_DIALOGIMPL_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/dialog/filedialogimpl-win32.cpp b/src/ui/dialog/filedialogimpl-win32.cpp
new file mode 100644
index 0000000..a75eb98
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-win32.cpp
@@ -0,0 +1,1923 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Implementation of native file dialogs for Win32.
+ */
+/* Authors:
+ * Joel Holdsworth
+ * The Inkscape Organization
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-2008 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifdef _WIN32
+
+#include "filedialogimpl-win32.h"
+// General includes
+#include <cairomm/win32_surface.h>
+#include <gdk/gdkwin32.h>
+#include <gdkmm/general.h>
+#include <glibmm/fileutils.h>
+#include <glibmm/i18n.h>
+#include <list>
+#include <thread>
+#include <vector>
+
+//Inkscape includes
+#include "display/cairo-utils.h"
+#include "document.h"
+#include "extension/db.h"
+#include "extension/input.h"
+#include "extension/output.h"
+#include "filedialog.h"
+#include "helper/pixbuf-ops.h"
+#include "preferences.h"
+#include "util/units.h"
+
+
+using namespace Glib;
+using namespace Cairo;
+using namespace Gdk::Cairo;
+
+namespace Inkscape
+{
+namespace UI
+{
+namespace Dialog
+{
+
+const int PREVIEW_WIDENING = 150;
+const int WINDOW_WIDTH_MINIMUM = 32;
+const int WINDOW_WIDTH_FALLBACK = 450;
+const int WINDOW_HEIGHT_MINIMUM = 32;
+const int WINDOW_HEIGHT_FALLBACK = 360;
+const char PreviewWindowClassName[] = "PreviewWnd";
+const unsigned long MaxPreviewFileSize = 10240; // kB
+
+#define IDC_SHOW_PREVIEW 1000
+
+struct Filter
+{
+ gunichar2* name;
+ glong name_length;
+ gunichar2* filter;
+ glong filter_length;
+ Inkscape::Extension::Extension* mod;
+};
+
+ustring utf16_to_ustring(const wchar_t *utf16string, int utf16length = -1)
+{
+ gchar *utf8string = g_utf16_to_utf8((const gunichar2*)utf16string,
+ utf16length, NULL, NULL, NULL);
+ ustring result(utf8string);
+ g_free(utf8string);
+
+ return result;
+}
+
+namespace {
+
+int sanitizeWindowSizeParam( int size, int delta, int minimum, int fallback )
+{
+ int result = size;
+ if ( size < minimum ) {
+ g_warning( "Window size %d is less than cutoff.", size );
+ result = fallback - delta;
+ }
+ result += delta;
+ return result;
+}
+
+} // namespace
+
+/*#########################################################################
+### F I L E D I A L O G B A S E C L A S S
+#########################################################################*/
+
+FileDialogBaseWin32::FileDialogBaseWin32(Gtk::Window &parent,
+ const Glib::ustring &dir, const gchar *title,
+ FileDialogType type, gchar const* /*preferenceBase*/) :
+ dialogType(type),
+ parent(parent),
+ _current_directory(dir)
+{
+ _main_loop = NULL;
+
+ _filter_index = 1;
+ _filter_count = 0;
+
+ _title = (wchar_t*)g_utf8_to_utf16(title, -1, NULL, NULL, NULL);
+ g_assert(_title != NULL);
+
+ Glib::RefPtr<const Gdk::Window> parentWindow = parent.get_window();
+ g_assert(parentWindow->gobj() != NULL);
+ _ownerHwnd = (HWND)gdk_win32_window_get_handle((GdkWindow*)parentWindow->gobj());
+}
+
+FileDialogBaseWin32::~FileDialogBaseWin32()
+{
+ g_free(_title);
+}
+
+Glib::ustring FileDialogBaseWin32::getCurrentDirectory()
+{
+ return _current_directory;
+}
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+bool FileOpenDialogImplWin32::_show_preview = true;
+
+/**
+ * Constructor. Not called directly. Use the factory.
+ */
+FileOpenDialogImplWin32::FileOpenDialogImplWin32(Gtk::Window &parent,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const gchar *title) :
+ FileDialogBaseWin32(parent, dir, title, fileTypes, "dialogs.open")
+{
+ // Initialize to Autodetect
+ _extension = NULL;
+
+ // Set our dialog type (open, import, etc...)
+ dialogType = fileTypes;
+
+ _show_preview_button_bitmap = NULL;
+ _preview_wnd = NULL;
+ _file_dialog_wnd = NULL;
+ _base_window_proc = NULL;
+
+ _preview_file_size = 0;
+ _preview_bitmap = NULL;
+ _preview_file_icon = NULL;
+ _preview_document_width = 0;
+ _preview_document_height = 0;
+ _preview_image_width = 0;
+ _preview_image_height = 0;
+ _preview_emf_image = false;
+
+ _mutex = NULL;
+
+ if (dialogType != CUSTOM_TYPE)
+ createFilterMenu();
+}
+
+
+/**
+ * Destructor
+ */
+FileOpenDialogImplWin32::~FileOpenDialogImplWin32()
+{
+ if(_filter != NULL)
+ delete[] _filter;
+ if(_extension_map != NULL)
+ delete[] _extension_map;
+}
+
+void FileOpenDialogImplWin32::addFilterMenu(const Glib::ustring &name, Glib::ustring pattern, Inkscape::Extension::Extension *mod)
+{
+ std::list<Filter> filter_list;
+
+ Filter all_exe_files;
+
+ const gchar *all_exe_files_filter_name = name.data();
+ const gchar *all_exe_files_filter = pattern.data();
+
+ // Calculate the amount of memory required
+ int filter_count = 1;
+ int filter_length = 1;
+
+ int extension_index = 0;
+ _extension_map = new Inkscape::Extension::Extension*[filter_count];
+
+ // Filter Executable Files
+ all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name,
+ -1, NULL, &all_exe_files.name_length, NULL);
+ all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter,
+ -1, NULL, &all_exe_files.filter_length, NULL);
+ all_exe_files.mod = NULL;
+ filter_list.push_front(all_exe_files);
+
+ filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a *
+
+ _filter = new wchar_t[filter_length];
+ wchar_t *filterptr = _filter;
+
+ for(std::list<Filter>::iterator filter_iterator = filter_list.begin();
+ filter_iterator != filter_list.end(); ++filter_iterator)
+ {
+ const Filter &filter = *filter_iterator;
+
+ wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length);
+ filterptr += filter.name_length;
+ g_free(filter.name);
+
+ *(filterptr++) = L'\0';
+ *(filterptr++) = L'*';
+
+ if(filter.filter != NULL)
+ {
+ wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length);
+ filterptr += filter.filter_length;
+ g_free(filter.filter);
+ }
+
+ *(filterptr++) = L'\0';
+
+ // Associate this input extension with the file type name
+ _extension_map[extension_index++] = filter.mod;
+ }
+ *(filterptr++) = L'\0';
+
+ _filter_count = extension_index;
+ _filter_index = 1; // Select the 1st filter in the list
+}
+
+void FileOpenDialogImplWin32::createFilterMenu()
+{
+ std::list<Filter> filter_list;
+
+ int extension_index = 0;
+ int filter_length = 1;
+
+ if (dialogType == CUSTOM_TYPE) {
+ return;
+ }
+
+ if (dialogType != EXE_TYPES) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _show_preview = prefs->getBool("/dialogs/open/enable_preview", true);
+
+ // Compose the filter string
+ Inkscape::Extension::DB::InputList extension_list;
+ Inkscape::Extension::db.get_input_list(extension_list);
+
+ ustring all_inkscape_files_filter, all_image_files_filter, all_vectors_filter, all_bitmaps_filter;
+ Filter all_files, all_inkscape_files, all_image_files, all_vectors, all_bitmaps;
+
+ const gchar *all_files_filter_name = _("All Files");
+ const gchar *all_inkscape_files_filter_name = _("All Inkscape Files");
+ const gchar *all_image_files_filter_name = _("All Images");
+ const gchar *all_vectors_filter_name = _("All Vectors");
+ const gchar *all_bitmaps_filter_name = _("All Bitmaps");
+
+ // Calculate the amount of memory required
+ int filter_count = 5; // 5 - one for each filter type
+
+ for (Inkscape::Extension::DB::InputList::iterator current_item = extension_list.begin();
+ current_item != extension_list.end(); ++current_item)
+ {
+ Filter filter;
+
+ Inkscape::Extension::Input *imod = *current_item;
+ if (imod->deactivated()) continue;
+
+ // Type
+ filter.name = g_utf8_to_utf16(imod->get_filetypename(true), -1, NULL, &filter.name_length, NULL);
+
+ // Extension
+ const gchar *file_extension_name = imod->get_extension();
+ filter.filter = g_utf8_to_utf16(file_extension_name, -1, NULL, &filter.filter_length, NULL);
+
+ filter.mod = imod;
+ filter_list.push_back(filter);
+
+ filter_length += filter.name_length + filter.filter_length + 3; // Add 3 for two \0s and a *
+
+ // Add to the "All Inkscape Files" Entry
+ if(all_inkscape_files_filter.length() > 0)
+ all_inkscape_files_filter += ";*";
+ all_inkscape_files_filter += file_extension_name;
+ if( strncmp("image", imod->get_mimetype(), 5) == 0)
+ {
+ // Add to the "All Image Files" Entry
+ if(all_image_files_filter.length() > 0)
+ all_image_files_filter += ";*";
+ all_image_files_filter += file_extension_name;
+ }
+
+ // I don't know of any other way to define "bitmap" formats other than by listing them
+ // if you change it here, do the same change in filedialogimpl-gtkmm
+ if (
+ strncmp("image/png", imod->get_mimetype(), 9)==0 ||
+ strncmp("image/jpeg", imod->get_mimetype(), 10)==0 ||
+ strncmp("image/gif", imod->get_mimetype(), 9)==0 ||
+ strncmp("image/x-icon", imod->get_mimetype(), 12)==0 ||
+ strncmp("image/x-navi-animation", imod->get_mimetype(), 22)==0 ||
+ strncmp("image/x-cmu-raster", imod->get_mimetype(), 18)==0 ||
+ strncmp("image/x-xpixmap", imod->get_mimetype(), 15)==0 ||
+ strncmp("image/bmp", imod->get_mimetype(), 9)==0 ||
+ strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18)==0 ||
+ strncmp("image/tiff", imod->get_mimetype(), 10)==0 ||
+ strncmp("image/x-xbitmap", imod->get_mimetype(), 15)==0 ||
+ strncmp("image/x-tga", imod->get_mimetype(), 11)==0 ||
+ strncmp("image/x-pcx", imod->get_mimetype(), 11)==0
+ ) {
+ if(all_bitmaps_filter.length() > 0)
+ all_bitmaps_filter += ";*";
+ all_bitmaps_filter += file_extension_name;
+ } else {
+ if(all_vectors_filter.length() > 0)
+ all_vectors_filter += ";*";
+ all_vectors_filter += file_extension_name;
+ }
+
+ filter_count++;
+ }
+
+ _extension_map = new Inkscape::Extension::Extension*[filter_count];
+
+ // Filter bitmap files
+ all_bitmaps.name = g_utf8_to_utf16(all_bitmaps_filter_name,
+ -1, NULL, &all_bitmaps.name_length, NULL);
+ all_bitmaps.filter = g_utf8_to_utf16(all_bitmaps_filter.data(),
+ -1, NULL, &all_bitmaps.filter_length, NULL);
+ all_bitmaps.mod = NULL;
+ filter_list.push_front(all_bitmaps);
+
+ // Filter vector files
+ all_vectors.name = g_utf8_to_utf16(all_vectors_filter_name,
+ -1, NULL, &all_vectors.name_length, NULL);
+ all_vectors.filter = g_utf8_to_utf16(all_vectors_filter.data(),
+ -1, NULL, &all_vectors.filter_length, NULL);
+ all_vectors.mod = NULL;
+ filter_list.push_front(all_vectors);
+
+ // Filter Image Files
+ all_image_files.name = g_utf8_to_utf16(all_image_files_filter_name,
+ -1, NULL, &all_image_files.name_length, NULL);
+ all_image_files.filter = g_utf8_to_utf16(all_image_files_filter.data(),
+ -1, NULL, &all_image_files.filter_length, NULL);
+ all_image_files.mod = NULL;
+ filter_list.push_front(all_image_files);
+
+ // Filter Inkscape Files
+ all_inkscape_files.name = g_utf8_to_utf16(all_inkscape_files_filter_name,
+ -1, NULL, &all_inkscape_files.name_length, NULL);
+ all_inkscape_files.filter = g_utf8_to_utf16(all_inkscape_files_filter.data(),
+ -1, NULL, &all_inkscape_files.filter_length, NULL);
+ all_inkscape_files.mod = NULL;
+ filter_list.push_front(all_inkscape_files);
+
+ // Filter All Files
+ all_files.name = g_utf8_to_utf16(all_files_filter_name,
+ -1, NULL, &all_files.name_length, NULL);
+ all_files.filter = NULL;
+ all_files.filter_length = 0;
+ all_files.mod = NULL;
+ filter_list.push_front(all_files);
+
+ filter_length += all_files.name_length + 3 +
+ all_inkscape_files.filter_length +
+ all_inkscape_files.name_length + 3 +
+ all_image_files.filter_length +
+ all_image_files.name_length + 3 +
+ all_vectors.filter_length +
+ all_vectors.name_length + 3 +
+ all_bitmaps.filter_length +
+ all_bitmaps.name_length + 3 +
+ 1;
+ // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0
+ } else {
+ // Executables only
+ ustring all_exe_files_filter = "*.exe;*.bat;*.com";
+ Filter all_exe_files, all_files;
+
+ const gchar *all_files_filter_name = _("All Files");
+ const gchar *all_exe_files_filter_name = _("All Executable Files");
+
+ // Calculate the amount of memory required
+ int filter_count = 2; // 2 - All Files and All Executable Files
+
+ _extension_map = new Inkscape::Extension::Extension*[filter_count];
+
+ // Filter Executable Files
+ all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name,
+ -1, NULL, &all_exe_files.name_length, NULL);
+ all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter.data(),
+ -1, NULL, &all_exe_files.filter_length, NULL);
+ all_exe_files.mod = NULL;
+ filter_list.push_front(all_exe_files);
+
+ // Filter All Files
+ all_files.name = g_utf8_to_utf16(all_files_filter_name,
+ -1, NULL, &all_files.name_length, NULL);
+ all_files.filter = NULL;
+ all_files.filter_length = 0;
+ all_files.mod = NULL;
+ filter_list.push_front(all_files);
+
+ filter_length += all_files.name_length + 3 +
+ all_exe_files.filter_length +
+ all_exe_files.name_length + 3 +
+ 1;
+ // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0
+ }
+
+ _filter = new wchar_t[filter_length];
+ wchar_t *filterptr = _filter;
+
+ for(std::list<Filter>::iterator filter_iterator = filter_list.begin();
+ filter_iterator != filter_list.end(); ++filter_iterator)
+ {
+ const Filter &filter = *filter_iterator;
+
+ wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length);
+ filterptr += filter.name_length;
+ g_free(filter.name);
+
+ *(filterptr++) = L'\0';
+ *(filterptr++) = L'*';
+
+ if(filter.filter != NULL)
+ {
+ wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length);
+ filterptr += filter.filter_length;
+ g_free(filter.filter);
+ }
+
+ *(filterptr++) = L'\0';
+
+ // Associate this input extension with the file type name
+ _extension_map[extension_index++] = filter.mod;
+ }
+ *(filterptr++) = L'\0';
+
+ _filter_count = extension_index;
+ _filter_index = 2; // Select the 2nd filter in the list - 2 is NOT the 3rd
+}
+
+void FileOpenDialogImplWin32::GetOpenFileName_thread()
+{
+ OPENFILENAMEW ofn;
+
+ g_assert(_mutex != NULL);
+
+ WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16(
+ _current_directory.data(), _current_directory.length(),
+ NULL, NULL, NULL);
+
+ memset(&ofn, 0, sizeof(ofn));
+
+ // Copy the selected file name, converting from UTF-8 to UTF-16
+ memset(_path_string, 0, sizeof(_path_string));
+ gunichar2* utf16_path_string = g_utf8_to_utf16(
+ getFilename().data(), -1, NULL, NULL, NULL);
+ wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH);
+ g_free(utf16_path_string);
+
+ ofn.lStructSize = sizeof(ofn);
+ ofn.hwndOwner = _ownerHwnd;
+ ofn.lpstrFile = _path_string;
+ ofn.nMaxFile = _MAX_PATH;
+ ofn.lpstrFileTitle = NULL;
+ ofn.nMaxFileTitle = 0;
+ ofn.lpstrInitialDir = current_directory_string;
+ ofn.lpstrTitle = _title;
+ ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_HIDEREADONLY | OFN_ENABLESIZING;
+ ofn.lpstrFilter = _filter;
+ ofn.nFilterIndex = _filter_index;
+ ofn.lpfnHook = GetOpenFileName_hookproc;
+ ofn.lCustData = (LPARAM)this;
+
+ _result = GetOpenFileNameW(&ofn) != 0;
+
+ g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count);
+ _filter_index = ofn.nFilterIndex;
+ _extension = _extension_map[ofn.nFilterIndex - 1];
+
+ // Copy the selected file name, converting from UTF-16 to UTF-8
+ setFilename(utf16_to_ustring(_path_string, _MAX_PATH));
+
+ // Tidy up
+ g_free(current_directory_string);
+
+ _mutex->lock();
+ _finished = true;
+ _mutex->unlock();
+}
+
+void FileOpenDialogImplWin32::register_preview_wnd_class()
+{
+ HINSTANCE hInstance = GetModuleHandle(NULL);
+ const WNDCLASSA PreviewWndClass =
+ {
+ CS_HREDRAW | CS_VREDRAW,
+ preview_wnd_proc,
+ 0,
+ 0,
+ hInstance,
+ NULL,
+ LoadCursor(hInstance, IDC_ARROW),
+ (HBRUSH)(COLOR_BTNFACE + 1),
+ NULL,
+ PreviewWindowClassName
+ };
+
+ RegisterClassA(&PreviewWndClass);
+}
+
+UINT_PTR CALLBACK FileOpenDialogImplWin32::GetOpenFileName_hookproc(
+ HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam)
+{
+ FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*>
+ (GetWindowLongPtr(hdlg, GWLP_USERDATA));
+
+ switch(uiMsg)
+ {
+ case WM_INITDIALOG:
+ {
+ HWND hParentWnd = GetParent(hdlg);
+ HINSTANCE hInstance = GetModuleHandle(NULL);
+
+ // Set the pointer to the object
+ OPENFILENAMEW *ofn = reinterpret_cast<OPENFILENAMEW*>(lParam);
+ SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData);
+ SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData);
+ pImpl = reinterpret_cast<FileOpenDialogImplWin32*>(ofn->lCustData);
+
+ // Make the window a bit wider
+ RECT rcRect;
+ GetWindowRect(hParentWnd, &rcRect);
+
+ // Don't show the preview when opening executable files
+ if ( pImpl->dialogType == EXE_TYPES) {
+ MoveWindow(hParentWnd, rcRect.left, rcRect.top,
+ rcRect.right - rcRect.left,
+ rcRect.bottom - rcRect.top,
+ FALSE);
+ } else {
+ MoveWindow(hParentWnd, rcRect.left, rcRect.top,
+ rcRect.right - rcRect.left + PREVIEW_WIDENING,
+ rcRect.bottom - rcRect.top,
+ FALSE);
+ }
+
+ // Subclass the parent
+ pImpl->_base_window_proc = (WNDPROC)GetWindowLongPtr(hParentWnd, GWLP_WNDPROC);
+ SetWindowLongPtr(hParentWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(file_dialog_subclass_proc));
+
+ if ( pImpl->dialogType != EXE_TYPES) {
+ // Add a button to the toolbar
+ pImpl->_toolbar_wnd = FindWindowEx(hParentWnd, NULL, "ToolbarWindow32", NULL);
+
+ pImpl->_show_preview_button_bitmap = LoadBitmap(
+ hInstance, MAKEINTRESOURCE(IDC_SHOW_PREVIEW));
+ TBADDBITMAP tbAddBitmap = {NULL, reinterpret_cast<UINT_PTR>(pImpl->_show_preview_button_bitmap)};
+ const int iBitmapIndex = SendMessage(pImpl->_toolbar_wnd,
+ TB_ADDBITMAP, 1, (LPARAM)&tbAddBitmap);
+
+
+ TBBUTTON tbButton;
+ memset(&tbButton, 0, sizeof(TBBUTTON));
+ tbButton.iBitmap = iBitmapIndex;
+ tbButton.idCommand = IDC_SHOW_PREVIEW;
+ tbButton.fsState = (pImpl->_show_preview ? TBSTATE_CHECKED : 0)
+ | TBSTATE_ENABLED;
+ tbButton.fsStyle = TBSTYLE_CHECK;
+ tbButton.iString = (INT_PTR)_("Show Preview");
+ SendMessage(pImpl->_toolbar_wnd, TB_ADDBUTTONS, 1, (LPARAM)&tbButton);
+
+ // Create preview pane
+ register_preview_wnd_class();
+ }
+
+ pImpl->_mutex->lock();
+
+ pImpl->_file_dialog_wnd = hParentWnd;
+
+ if ( pImpl->dialogType != EXE_TYPES) {
+ pImpl->_preview_wnd =
+ CreateWindowA(PreviewWindowClassName, "",
+ WS_CHILD | WS_VISIBLE,
+ 0, 0, 100, 100, hParentWnd, NULL, hInstance, NULL);
+ SetWindowLongPtr(pImpl->_preview_wnd, GWLP_USERDATA, ofn->lCustData);
+ }
+
+ pImpl->_mutex->unlock();
+
+ pImpl->layout_dialog();
+ }
+ break;
+
+ case WM_NOTIFY:
+ {
+
+ OFNOTIFY *pOFNotify = reinterpret_cast<OFNOTIFY*>(lParam);
+ switch(pOFNotify->hdr.code)
+ {
+ case CDN_SELCHANGE:
+ {
+ if(pImpl != NULL)
+ {
+ // Get the file name
+ pImpl->_mutex->lock();
+
+ SendMessage(pOFNotify->hdr.hwndFrom, CDM_GETFILEPATH,
+ sizeof(pImpl->_path_string) / sizeof(wchar_t),
+ (LPARAM)pImpl->_path_string);
+
+ pImpl->_file_selected = true;
+
+ pImpl->_mutex->unlock();
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case WM_CLOSE:
+ pImpl->_mutex->lock();
+ pImpl->_preview_file_size = 0;
+
+ pImpl->_file_dialog_wnd = NULL;
+ DestroyWindow(pImpl->_preview_wnd);
+ pImpl->_preview_wnd = NULL;
+ DeleteObject(pImpl->_show_preview_button_bitmap);
+ pImpl->_show_preview_button_bitmap = NULL;
+ pImpl->_mutex->unlock();
+
+ break;
+ }
+
+ // Use default dialog behaviour
+ return 0;
+}
+
+LRESULT CALLBACK FileOpenDialogImplWin32::file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*>
+ (GetWindowLongPtr(hwnd, GWLP_USERDATA));
+
+ LRESULT lResult = CallWindowProc(pImpl->_base_window_proc, hwnd, uMsg, wParam, lParam);
+
+ switch(uMsg)
+ {
+ case WM_SHOWWINDOW:
+ if(wParam != 0)
+ pImpl->layout_dialog();
+ break;
+
+ case WM_SIZE:
+ pImpl->layout_dialog();
+ break;
+
+ case WM_COMMAND:
+ if(wParam == IDC_SHOW_PREVIEW)
+ {
+ const bool enable = SendMessage(pImpl->_toolbar_wnd,
+ TB_ISBUTTONCHECKED, IDC_SHOW_PREVIEW, 0) != 0;
+ pImpl->enable_preview(enable);
+ }
+ break;
+ }
+
+ return lResult;
+}
+
+LRESULT CALLBACK FileOpenDialogImplWin32::preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ const int CaptionPadding = 4;
+ const int IconSize = 32;
+
+ FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*>
+ (GetWindowLongPtr(hwnd, GWLP_USERDATA));
+
+ LRESULT lResult = 0;
+
+ switch(uMsg)
+ {
+ case WM_ERASEBKGND:
+ // Do nothing to erase the background
+ // - otherwise there'll be flicker
+ lResult = 1;
+ break;
+
+ case WM_PAINT:
+ {
+ // Get the client rect
+ RECT rcClient;
+ GetClientRect(hwnd, &rcClient);
+
+ // Prepare to paint
+ PAINTSTRUCT paint_struct;
+ HDC dc = BeginPaint(hwnd, &paint_struct);
+
+ HFONT hCaptionFont = reinterpret_cast<HFONT>(SendMessage(GetParent(hwnd),
+ WM_GETFONT, 0, 0));
+ HFONT hOldFont = static_cast<HFONT>(SelectObject(dc, hCaptionFont));
+ SetBkMode(dc, TRANSPARENT);
+
+ pImpl->_mutex->lock();
+
+ if(pImpl->_path_string[0] == 0)
+ {
+ WCHAR* noFileText=(WCHAR*)g_utf8_to_utf16(_("No file selected"),
+ -1, NULL, NULL, NULL);
+ FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1));
+ DrawTextW(dc, noFileText, -1, &rcClient,
+ DT_CENTER | DT_VCENTER | DT_NOPREFIX);
+ g_free(noFileText);
+ }
+ else if(pImpl->_preview_bitmap != NULL)
+ {
+ BITMAP bitmap;
+ GetObject(pImpl->_preview_bitmap, sizeof(bitmap), &bitmap);
+ const int destX = (rcClient.right - bitmap.bmWidth) / 2;
+
+ // Render the image
+ HDC hSrcDC = CreateCompatibleDC(dc);
+ HBITMAP hOldBitmap = (HBITMAP)SelectObject(hSrcDC, pImpl->_preview_bitmap);
+
+ BitBlt(dc, destX, 0, bitmap.bmWidth, bitmap.bmHeight,
+ hSrcDC, 0, 0, SRCCOPY);
+
+ SelectObject(hSrcDC, hOldBitmap);
+ DeleteDC(hSrcDC);
+
+ // Fill in the background area
+ HRGN hEraseRgn = CreateRectRgn(rcClient.left, rcClient.top,
+ rcClient.right, rcClient.bottom);
+ HRGN hImageRgn = CreateRectRgn(destX, 0,
+ destX + bitmap.bmWidth, bitmap.bmHeight);
+ CombineRgn(hEraseRgn, hEraseRgn, hImageRgn, RGN_DIFF);
+
+ FillRgn(dc, hEraseRgn, GetSysColorBrush(COLOR_3DFACE));
+
+ DeleteObject(hImageRgn);
+ DeleteObject(hEraseRgn);
+
+ // Draw the caption on
+ RECT rcCaptionRect = {rcClient.left,
+ rcClient.top + bitmap.bmHeight + CaptionPadding,
+ rcClient.right, rcClient.bottom};
+
+ WCHAR szCaption[_MAX_FNAME + 32];
+ const int iLength = pImpl->format_caption(
+ szCaption, sizeof(szCaption) / sizeof(WCHAR));
+
+ DrawTextW(dc, szCaption, iLength, &rcCaptionRect,
+ DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS);
+ }
+ else if(pImpl->_preview_file_icon != NULL)
+ {
+ FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1));
+
+ // Draw the files icon
+ const int destX = (rcClient.right - IconSize) / 2;
+ DrawIconEx(dc, destX, 0, pImpl->_preview_file_icon,
+ IconSize, IconSize, 0, NULL,
+ DI_NORMAL | DI_COMPAT);
+
+ // Draw the caption on
+ RECT rcCaptionRect = {rcClient.left,
+ rcClient.top + IconSize + CaptionPadding,
+ rcClient.right, rcClient.bottom};
+
+ WCHAR szFileName[_MAX_FNAME], szCaption[_MAX_FNAME + 32];
+ _wsplitpath(pImpl->_path_string, NULL, NULL, szFileName, NULL);
+
+ const int iLength = snwprintf(szCaption,
+ sizeof(szCaption), L"%ls\n%d kB",
+ szFileName, pImpl->_preview_file_size);
+
+ DrawTextW(dc, szCaption, iLength, &rcCaptionRect,
+ DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS);
+ }
+ else
+ {
+ // Can't show anything!
+ FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1));
+ }
+
+ pImpl->_mutex->unlock();
+
+ // Finish painting
+ SelectObject(dc, hOldFont);
+ EndPaint(hwnd, &paint_struct);
+ }
+
+ break;
+
+ case WM_DESTROY:
+ pImpl->free_preview();
+ break;
+
+ default:
+ lResult = DefWindowProc(hwnd, uMsg, wParam, lParam);
+ break;
+ }
+
+ return lResult;
+}
+
+void FileOpenDialogImplWin32::enable_preview(bool enable)
+{
+ if (_show_preview != enable) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/open/enable_preview", enable);
+ }
+ _show_preview = enable;
+
+ // Relayout the dialog
+ ShowWindow(_preview_wnd, enable ? SW_SHOW : SW_HIDE);
+ layout_dialog();
+
+ // Load or unload the preview
+ if(enable)
+ {
+ _mutex->lock();
+ _file_selected = true;
+ _mutex->unlock();
+ }
+ else free_preview();
+}
+
+void FileOpenDialogImplWin32::layout_dialog()
+{
+ union RECTPOINTS
+ {
+ RECT r;
+ POINT p[2];
+ };
+
+ const float MaxExtentScale = 2.0f / 3.0f;
+
+ RECT rcClient;
+ GetClientRect(_file_dialog_wnd, &rcClient);
+
+ // Re-layout the dialog
+ HWND hFileListWnd = GetDlgItem(_file_dialog_wnd, lst2);
+ HWND hFolderComboWnd = GetDlgItem(_file_dialog_wnd, cmb2);
+
+
+ RECT rcFolderComboRect;
+ RECTPOINTS rcFileList;
+ GetWindowRect(hFileListWnd, &rcFileList.r);
+ GetWindowRect(hFolderComboWnd, &rcFolderComboRect);
+ const int iPadding = rcFileList.r.top - rcFolderComboRect.bottom;
+ MapWindowPoints(NULL, _file_dialog_wnd, rcFileList.p, 2);
+
+ RECT rcPreview;
+ RECT rcBody = {rcFileList.r.left, rcFileList.r.top,
+ rcClient.right - iPadding, rcFileList.r.bottom};
+ rcFileList.r.right = rcBody.right;
+
+ if(_show_preview && dialogType != EXE_TYPES)
+ {
+ rcPreview.top = rcBody.top;
+ rcPreview.left = rcClient.right - (rcBody.bottom - rcBody.top);
+ const int iMaxExtent = (int)(MaxExtentScale * (float)(rcBody.left + rcBody.right)) + iPadding / 2;
+ if(rcPreview.left < iMaxExtent) rcPreview.left = iMaxExtent;
+ rcPreview.bottom = rcBody.bottom;
+ rcPreview.right = rcBody.right;
+
+ // Re-layout the preview box
+ _mutex->lock();
+
+ _preview_width = rcPreview.right - rcPreview.left;
+ _preview_height = rcPreview.bottom - rcPreview.top;
+
+ _mutex->unlock();
+
+ render_preview();
+
+ MoveWindow(_preview_wnd, rcPreview.left, rcPreview.top,
+ _preview_width, _preview_height, TRUE);
+
+ rcFileList.r.right = rcPreview.left - iPadding;
+ }
+
+ // Re-layout the file list box
+ MoveWindow(hFileListWnd, rcFileList.r.left, rcFileList.r.top,
+ rcFileList.r.right - rcFileList.r.left,
+ rcFileList.r.bottom - rcFileList.r.top, TRUE);
+
+ // Re-layout the toolbar
+ RECTPOINTS rcToolBar;
+ GetWindowRect(_toolbar_wnd, &rcToolBar.r);
+ MapWindowPoints(NULL, _file_dialog_wnd, rcToolBar.p, 2);
+ MoveWindow(_toolbar_wnd, rcToolBar.r.left, rcToolBar.r.top,
+ rcToolBar.r.right - rcToolBar.r.left, rcToolBar.r.bottom - rcToolBar.r.top, TRUE);
+}
+
+void FileOpenDialogImplWin32::file_selected()
+{
+ // Destroy any previous previews
+ free_preview();
+
+
+ // Determine if the file exists
+ DWORD attributes = GetFileAttributesW(_path_string);
+ if(attributes == 0xFFFFFFFF ||
+ attributes == FILE_ATTRIBUTE_DIRECTORY)
+ {
+ InvalidateRect(_preview_wnd, NULL, FALSE);
+ return;
+ }
+
+ // Check the file exists and get the file size
+ HANDLE file_handle = CreateFileW(_path_string, GENERIC_READ,
+ FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+ if(file_handle == INVALID_HANDLE_VALUE) return;
+ const DWORD file_size = GetFileSize(file_handle, NULL);
+ if (file_size == INVALID_FILE_SIZE) return;
+ _preview_file_size = file_size / 1024;
+ CloseHandle(file_handle);
+
+ if(_show_preview) load_preview();
+}
+
+void FileOpenDialogImplWin32::load_preview()
+{
+ // Destroy any previous previews
+ free_preview();
+
+ // Try to get the file icon
+ SHFILEINFOW fileInfo;
+ if(SUCCEEDED(SHGetFileInfoW(_path_string, 0, &fileInfo,
+ sizeof(fileInfo), SHGFI_ICON | SHGFI_LARGEICON)))
+ _preview_file_icon = fileInfo.hIcon;
+
+ // Will this file be too big?
+ if(_preview_file_size > MaxPreviewFileSize)
+ {
+ InvalidateRect(_preview_wnd, NULL, FALSE);
+ return;
+ }
+
+ // Prepare to render a preview
+ const Glib::ustring svg = ".svg";
+ const Glib::ustring svgz = ".svgz";
+ const Glib::ustring emf = ".emf";
+ const Glib::ustring wmf = ".wmf";
+ const Glib::ustring path = utf16_to_ustring(_path_string);
+
+ bool success = false;
+
+ _preview_document_width = _preview_document_height = 0;
+
+ if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) &&
+ (hasSuffix(path, svg) || hasSuffix(path, svgz)))
+ success = set_svg_preview();
+ else if (hasSuffix(path, emf) || hasSuffix(path, wmf))
+ success = set_emf_preview();
+ else if (isValidImageFile(path))
+ success = set_image_preview();
+ else {
+ // Show no preview
+ }
+
+ if(success) render_preview();
+
+ InvalidateRect(_preview_wnd, NULL, FALSE);
+}
+
+void FileOpenDialogImplWin32::free_preview()
+{
+ _mutex->lock();
+ if(_preview_bitmap != NULL)
+ DeleteObject(_preview_bitmap);
+ _preview_bitmap = NULL;
+
+ if(_preview_file_icon != NULL)
+ DestroyIcon(_preview_file_icon);
+ _preview_file_icon = NULL;
+
+ _preview_bitmap_image.reset();
+ _preview_emf_image = false;
+ _mutex->unlock();
+}
+
+bool FileOpenDialogImplWin32::set_svg_preview()
+{
+ const int PreviewSize = 512;
+
+ gchar *utf8string = g_utf16_to_utf8((const gunichar2*)_path_string,
+ _MAX_PATH, NULL, NULL, NULL);
+ std::unique_ptr<SPDocument> svgDoc(SPDocument::createNewDoc(utf8string, 0));
+ g_free(utf8string);
+
+ // Check the document loaded properly
+ if (svgDoc == NULL) {
+ return false;
+ }
+ if (svgDoc->getRoot() == NULL)
+ {
+ return false;
+ }
+
+ // Get the size of the document
+ Inkscape::Util::Quantity svgWidth = svgDoc->getWidth();
+ Inkscape::Util::Quantity svgHeight = svgDoc->getHeight();
+ const double svgWidth_px = svgWidth.value("px");
+ const double svgHeight_px = svgHeight.value("px");
+
+ // Find the minimum scale to fit the image inside the preview area
+ const double scaleFactorX = PreviewSize / svgWidth_px;
+ const double scaleFactorY = PreviewSize / svgHeight_px;
+ const double scaleFactor = (scaleFactorX > scaleFactorY) ? scaleFactorY : scaleFactorX;
+
+ const double dpi = 96 * scaleFactor;
+ Geom::Rect area(0, 0, svgWidth_px, svgHeight_px);
+ Inkscape::Pixbuf *pixbuf =
+ sp_generate_internal_bitmap(svgDoc.get(), area, dpi);
+
+ // Tidy up
+ if (pixbuf == NULL) {
+ return false;
+ }
+
+ // Create the GDK pixbuf
+ _mutex->lock();
+ _preview_bitmap_image = Glib::wrap(pixbuf->getPixbufRaw(), /* ref */ true);
+ delete pixbuf;
+ _preview_document_width = svgWidth_px;
+ _preview_document_height = svgHeight_px;
+ _preview_image_width = round(scaleFactor * svgWidth_px);
+ _preview_image_height = round(scaleFactor * svgHeight_px);
+ _mutex->unlock();
+
+ return true;
+}
+
+void FileOpenDialogImplWin32::destroy_svg_rendering(const guint8 *buffer)
+{
+ g_assert(buffer != NULL);
+ g_free((void*)buffer);
+}
+
+bool FileOpenDialogImplWin32::set_image_preview()
+{
+ const Glib::ustring path = utf16_to_ustring(_path_string, _MAX_PATH);
+
+ bool successful = false;
+
+ _mutex->lock();
+
+ try {
+ _preview_bitmap_image = Gdk::Pixbuf::create_from_file(path);
+ if (_preview_bitmap_image) {
+ _preview_image_width = _preview_bitmap_image->get_width();
+ _preview_document_width = _preview_image_width;
+ _preview_image_height = _preview_bitmap_image->get_height();
+ _preview_document_height = _preview_image_height;
+ successful = true;
+ }
+ }
+ catch (const Gdk::PixbufError&) {}
+ catch (const Glib::FileError&) {}
+
+ _mutex->unlock();
+
+ return successful;
+}
+
+// Aldus Placeable Header ===================================================
+// Since we are a 32bit app, we have to be sure this structure compiles to
+// be identical to a 16 bit app's version. To do this, we use the #pragma
+// to adjust packing, we use a WORD for the hmf handle, and a SMALL_RECT
+// for the bbox rectangle.
+#pragma pack( push )
+#pragma pack( 2 )
+struct APMHEADER
+{
+ DWORD dwKey;
+ WORD hmf;
+ SMALL_RECT bbox;
+ WORD wInch;
+ DWORD dwReserved;
+ WORD wCheckSum;
+};
+using PAPMHEADER = APMHEADER *;
+#pragma pack( pop )
+
+
+static HENHMETAFILE
+MyGetEnhMetaFileW( const WCHAR *filename )
+{
+ // Try open as Enhanced Metafile
+ HENHMETAFILE hemf = GetEnhMetaFileW(filename);
+
+ if (!hemf) {
+ // Try open as Windows Metafile
+ HMETAFILE hmf = GetMetaFileW(filename);
+
+ METAFILEPICT mp;
+ HDC hDC;
+
+ if (!hmf) {
+ WCHAR szTemp[MAX_PATH];
+
+ DWORD dw = GetShortPathNameW( filename, szTemp, MAX_PATH );
+ if (dw) {
+ hmf = GetMetaFileW( szTemp );
+ }
+ }
+
+ if (hmf) {
+ // Convert Windows Metafile to Enhanced Metafile
+ DWORD nSize = GetMetaFileBitsEx( hmf, 0, NULL );
+
+ if (nSize) {
+ BYTE *lpvData = new BYTE[nSize];
+ if (lpvData) {
+ DWORD dw = GetMetaFileBitsEx( hmf, nSize, lpvData );
+ if (dw) {
+ // Fill out a METAFILEPICT structure
+ mp.mm = MM_ANISOTROPIC;
+ mp.xExt = 1000;
+ mp.yExt = 1000;
+ mp.hMF = NULL;
+ // Get a reference DC
+ hDC = GetDC( NULL );
+ // Make an enhanced metafile from the windows metafile
+ hemf = SetWinMetaFileBits( nSize, lpvData, hDC, &mp );
+ // Clean up
+ ReleaseDC( NULL, hDC );
+ DeleteMetaFile( hmf );
+ }
+ delete[] lpvData;
+ }
+ else {
+ DeleteMetaFile( hmf );
+ }
+ }
+ else {
+ DeleteMetaFile( hmf );
+ }
+ }
+ else {
+ // Try open as Aldus Placeable Metafile
+ HANDLE hFile;
+ hFile = CreateFileW( filename, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
+
+ if (hFile != INVALID_HANDLE_VALUE) {
+ DWORD nSize = GetFileSize( hFile, NULL );
+ if (nSize) {
+ BYTE *lpvData = new BYTE[nSize];
+ if (lpvData) {
+ DWORD dw = ReadFile( hFile, lpvData, nSize, &nSize, NULL );
+ if (dw) {
+ if ( ((PAPMHEADER)lpvData)->dwKey == 0x9ac6cdd7l ) {
+ // Fill out a METAFILEPICT structure
+ mp.mm = MM_ANISOTROPIC;
+ mp.xExt = ((PAPMHEADER)lpvData)->bbox.Right - ((PAPMHEADER)lpvData)->bbox.Left;
+ mp.xExt = ( mp.xExt * 2540l ) / (DWORD)(((PAPMHEADER)lpvData)->wInch);
+ mp.yExt = ((PAPMHEADER)lpvData)->bbox.Bottom - ((PAPMHEADER)lpvData)->bbox.Top;
+ mp.yExt = ( mp.yExt * 2540l ) / (DWORD)(((PAPMHEADER)lpvData)->wInch);
+ mp.hMF = NULL;
+ // Get a reference DC
+ hDC = GetDC( NULL );
+ // Create an enhanced metafile from the bits
+ hemf = SetWinMetaFileBits( nSize, lpvData+sizeof(APMHEADER), hDC, &mp );
+ // Clean up
+ ReleaseDC( NULL, hDC );
+ }
+ }
+ delete[] lpvData;
+ }
+ }
+ CloseHandle( hFile );
+ }
+ }
+ }
+
+ return hemf;
+}
+
+
+bool FileOpenDialogImplWin32::set_emf_preview()
+{
+ _mutex->lock();
+
+ BOOL ok = FALSE;
+
+ DWORD w = 0;
+ DWORD h = 0;
+
+ HENHMETAFILE hemf = MyGetEnhMetaFileW( _path_string );
+
+ if (hemf)
+ {
+ ENHMETAHEADER emh;
+ ZeroMemory(&emh, sizeof(emh));
+ ok = GetEnhMetaFileHeader(hemf, sizeof(emh), &emh) != 0;
+
+ w = (emh.rclFrame.right - emh.rclFrame.left);
+ h = (emh.rclFrame.bottom - emh.rclFrame.top);
+
+ DeleteEnhMetaFile(hemf);
+ }
+
+ if (ok)
+ {
+ const int PreviewSize = 512;
+
+ // Get the size of the document
+ const double emfWidth = w;
+ const double emfHeight = h;
+
+ _preview_document_width = emfWidth / 2540 * 96; // width is in units of 0.01 mm
+ _preview_document_height = emfHeight / 2540 * 96; // height is in units of 0.01 mm
+ _preview_image_width = emfWidth;
+ _preview_image_height = emfHeight;
+
+ _preview_emf_image = true;
+ }
+
+ _mutex->unlock();
+
+ return ok;
+}
+
+void FileOpenDialogImplWin32::render_preview()
+{
+ double x, y;
+ const double blurRadius = 8;
+ const double halfBlurRadius = blurRadius / 2;
+ const int shaddowOffsetX = 0;
+ const int shaddowOffsetY = 2;
+ const int pagePadding = 5;
+ const double shaddowAlpha = 0.75;
+
+ // Is the preview showing?
+ if(!_show_preview)
+ return;
+
+ // Do we have anything to render?
+ _mutex->lock();
+
+ if(!_preview_bitmap_image && !_preview_emf_image)
+ {
+ _mutex->unlock();
+ return;
+ }
+
+ // Tidy up any previous bitmap renderings
+ if(_preview_bitmap != NULL)
+ DeleteObject(_preview_bitmap);
+ _preview_bitmap = NULL;
+
+ // Calculate the size of the caption
+ int captionHeight = 0;
+
+ if(_preview_wnd != NULL)
+ {
+ RECT rcCaptionRect;
+ WCHAR szCaption[_MAX_FNAME + 32];
+ const int iLength = format_caption(szCaption,
+ sizeof(szCaption) / sizeof(WCHAR));
+
+ HDC dc = GetDC(_preview_wnd);
+ DrawTextW(dc, szCaption, iLength, &rcCaptionRect,
+ DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS | DT_CALCRECT);
+ ReleaseDC(_preview_wnd, dc);
+
+ captionHeight = rcCaptionRect.bottom - rcCaptionRect.top;
+ }
+
+ // Find the minimum scale to fit the image inside the preview area
+ const double scaleFactorX = ((double)_preview_width - pagePadding * 2 - blurRadius) / _preview_image_width;
+ const double scaleFactorY = ((double)_preview_height - pagePadding * 2 - shaddowOffsetY - halfBlurRadius - captionHeight) / _preview_image_height;
+ const double scaleFactor = (scaleFactorX > scaleFactorY) ? scaleFactorY : scaleFactorX;
+
+ // Now get the resized values
+ const double scaledSvgWidth = scaleFactor * _preview_image_width;
+ const double scaledSvgHeight = scaleFactor * _preview_image_height;
+
+ const int svgX = pagePadding + halfBlurRadius;
+ const int svgY = pagePadding;
+
+ const int frameX = svgX - pagePadding;
+ const int frameY = svgY - pagePadding;
+ const int frameWidth = scaledSvgWidth + pagePadding * 2;
+ const int frameHeight = scaledSvgHeight + pagePadding * 2;
+
+ const int totalWidth = (int)ceil(frameWidth + blurRadius);
+ const int totalHeight = (int)ceil(frameHeight + blurRadius);
+
+ // Prepare the drawing surface
+ HDC hDC = GetDC(_preview_wnd);
+ HDC hMemDC = CreateCompatibleDC(hDC);
+ _preview_bitmap = CreateCompatibleBitmap(hDC, totalWidth, totalHeight);
+ HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, _preview_bitmap);
+ Cairo::RefPtr<Win32Surface> surface = Win32Surface::create(hMemDC);
+ Cairo::RefPtr<Context> context = Context::create(surface);
+
+ // Paint the background to match the dialog colour
+ const COLORREF background = GetSysColor(COLOR_3DFACE);
+ context->set_source_rgb(
+ GetRValue(background) / 255.0,
+ GetGValue(background) / 255.0,
+ GetBValue(background) / 255.0);
+ context->paint();
+
+ //----- Draw the drop shadow -----//
+
+ // Left Edge
+ x = frameX + shaddowOffsetX - halfBlurRadius;
+ Cairo::RefPtr<LinearGradient> leftEdgeFade = LinearGradient::create(
+ x, 0.0, x + blurRadius, 0.0);
+ leftEdgeFade->add_color_stop_rgba (0, 0, 0, 0, 0);
+ leftEdgeFade->add_color_stop_rgba (1, 0, 0, 0, shaddowAlpha);
+ context->set_source(leftEdgeFade);
+ context->rectangle (x, frameY + shaddowOffsetY + halfBlurRadius,
+ blurRadius, frameHeight - blurRadius);
+ context->fill();
+
+ // Right Edge
+ x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius;
+ Cairo::RefPtr<LinearGradient> rightEdgeFade = LinearGradient::create(
+ x, 0.0, x + blurRadius, 0.0);
+ rightEdgeFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha);
+ rightEdgeFade->add_color_stop_rgba (1, 0, 0, 0, 0);
+ context->set_source(rightEdgeFade);
+ context->rectangle (frameX + frameWidth + shaddowOffsetX - halfBlurRadius,
+ frameY + shaddowOffsetY + halfBlurRadius,
+ blurRadius, frameHeight - blurRadius);
+ context->fill();
+
+ // Top Edge
+ y = frameY + shaddowOffsetY - halfBlurRadius;
+ Cairo::RefPtr<LinearGradient> topEdgeFade = LinearGradient::create(
+ 0.0, y, 0.0, y + blurRadius);
+ topEdgeFade->add_color_stop_rgba (0, 0, 0, 0, 0);
+ topEdgeFade->add_color_stop_rgba (1, 0, 0, 0, shaddowAlpha);
+ context->set_source(topEdgeFade);
+ context->rectangle (frameX + shaddowOffsetX + halfBlurRadius, y,
+ frameWidth - blurRadius, blurRadius);
+ context->fill();
+
+ // Bottom Edge
+ y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius;
+ Cairo::RefPtr<LinearGradient> bottomEdgeFade = LinearGradient::create(
+ 0.0, y, 0.0, y + blurRadius);
+ bottomEdgeFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha);
+ bottomEdgeFade->add_color_stop_rgba (1, 0, 0, 0, 0);
+ context->set_source(bottomEdgeFade);
+ context->rectangle (frameX + shaddowOffsetX + halfBlurRadius, y,
+ frameWidth - blurRadius, blurRadius);
+ context->fill();
+
+ // Top Left Corner
+ x = frameX + shaddowOffsetX - halfBlurRadius;
+ y = frameY + shaddowOffsetY - halfBlurRadius;
+ Cairo::RefPtr<RadialGradient> topLeftCornerFade = RadialGradient::create(
+ x + blurRadius, y + blurRadius, 0, x + blurRadius, y + blurRadius, blurRadius);
+ topLeftCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha);
+ topLeftCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0);
+ context->set_source(topLeftCornerFade);
+ context->rectangle (x, y, blurRadius, blurRadius);
+ context->fill();
+
+ // Top Right Corner
+ x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius;
+ y = frameY + shaddowOffsetY - halfBlurRadius;
+ Cairo::RefPtr<RadialGradient> topRightCornerFade = RadialGradient::create(
+ x, y + blurRadius, 0, x, y + blurRadius, blurRadius);
+ topRightCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha);
+ topRightCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0);
+ context->set_source(topRightCornerFade);
+ context->rectangle (x, y, blurRadius, blurRadius);
+ context->fill();
+
+ // Bottom Left Corner
+ x = frameX + shaddowOffsetX - halfBlurRadius;
+ y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius;
+ Cairo::RefPtr<RadialGradient> bottomLeftCornerFade = RadialGradient::create(
+ x + blurRadius, y, 0, x + blurRadius, y, blurRadius);
+ bottomLeftCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha);
+ bottomLeftCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0);
+ context->set_source(bottomLeftCornerFade);
+ context->rectangle (x, y, blurRadius, blurRadius);
+ context->fill();
+
+ // Bottom Right Corner
+ x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius;
+ y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius;
+ Cairo::RefPtr<RadialGradient> bottomRightCornerFade = RadialGradient::create(
+ x, y, 0, x, y, blurRadius);
+ bottomRightCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha);
+ bottomRightCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0);
+ context->set_source(bottomRightCornerFade);
+ context->rectangle (frameX + frameWidth + shaddowOffsetX - halfBlurRadius,
+ frameY + frameHeight + shaddowOffsetY - halfBlurRadius,
+ blurRadius, blurRadius);
+ context->fill();
+
+ // Draw the frame
+ context->set_line_width(1);
+ context->rectangle (frameX, frameY, frameWidth, frameHeight);
+
+ context->set_source_rgb(1.0, 1.0, 1.0);
+ context->fill_preserve();
+ context->set_source_rgb(0.25, 0.25, 0.25);
+ context->stroke_preserve();
+
+ // Draw the image
+ if(_preview_bitmap_image) // Is the image a pixbuf?
+ {
+ // Set the transformation
+ const Cairo::Matrix matrix(
+ scaleFactor, 0,
+ 0, scaleFactor,
+ svgX, svgY);
+ context->set_matrix (matrix);
+
+ // Render the image
+ set_source_pixbuf (context, _preview_bitmap_image, 0, 0);
+ context->paint();
+
+ // Reset the transformation
+ context->set_identity_matrix();
+ }
+
+ // Draw the inner frame
+ context->set_source_rgb(0.75, 0.75, 0.75);
+ context->rectangle (svgX, svgY, scaledSvgWidth, scaledSvgHeight);
+ context->stroke();
+
+ _mutex->unlock();
+
+ // Finish drawing
+ surface->finish();
+
+ if (_preview_emf_image) {
+ HENHMETAFILE hemf = MyGetEnhMetaFileW(_path_string);
+ if (hemf) {
+ RECT rc;
+ rc.top = svgY+2;
+ rc.left = svgX+2;
+ rc.bottom = scaledSvgHeight-2;
+ rc.right = scaledSvgWidth-2;
+ PlayEnhMetaFile(hMemDC, hemf, &rc);
+ DeleteEnhMetaFile(hemf);
+ }
+ }
+
+ SelectObject(hMemDC, hOldBitmap) ;
+ DeleteDC(hMemDC);
+
+ // Refresh the preview pane
+ InvalidateRect(_preview_wnd, NULL, FALSE);
+}
+
+int FileOpenDialogImplWin32::format_caption(wchar_t *caption, int caption_size)
+{
+ wchar_t szFileName[_MAX_FNAME];
+ _wsplitpath(_path_string, NULL, NULL, szFileName, NULL);
+
+ return snwprintf(caption, caption_size,
+ L"%ls\n%d\u2009kB\n%d\u2009px \xD7 %d\u2009px", szFileName, _preview_file_size,
+ (int)_preview_document_width, (int)_preview_document_height);
+}
+
+/**
+ * Show this dialog modally. Return true if user hits [OK]
+ */
+bool
+FileOpenDialogImplWin32::show()
+{
+ // We can only run one worker thread at a time
+ if(_mutex != NULL) return false;
+
+ _result = false;
+ _finished = false;
+ _file_selected = false;
+ _main_loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ _mutex = std::make_unique<std::mutex>();
+
+ std::thread thethread([this] { GetOpenFileName_thread(); });
+
+ {
+ while(1)
+ {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+
+ if(_mutex->try_lock())
+ {
+ // Read mutexed data
+ const bool finished = _finished;
+ const bool is_file_selected = _file_selected;
+ _file_selected = false;
+ _mutex->unlock();
+
+ if(finished) break;
+ if(is_file_selected) file_selected();
+ }
+
+ Sleep(10);
+ }
+ }
+
+ thethread.join();
+
+ // Tidy up
+ _mutex = NULL;
+
+ return _result;
+}
+
+/**
+ * To Get Multiple filenames selected at-once.
+ */
+std::vector<Glib::ustring>FileOpenDialogImplWin32::getFilenames()
+{
+ std::vector<Glib::ustring> result;
+ result.push_back(getFilename());
+ return result;
+}
+
+
+/*#########################################################################
+### F I L E S A V E
+#########################################################################*/
+
+/**
+ * Constructor
+ */
+FileSaveDialogImplWin32::FileSaveDialogImplWin32(Gtk::Window &parent,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const char *title,
+ const Glib::ustring &/*default_key*/,
+ const char *docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method)
+ : FileDialogBaseWin32(parent, dir, title, fileTypes,
+ (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "dialogs.save_copy"
+ : "dialogs.save_as")
+ , save_method(save_method)
+ , _title_label(NULL)
+ , _title_edit(NULL)
+{
+ FileSaveDialog::myDocTitle = docTitle;
+
+ if (dialogType != CUSTOM_TYPE)
+ createFilterMenu();
+
+ /* The code below sets the default file name */
+ setFilename("");
+ if (dir.size() > 0) {
+ Glib::ustring udir(dir);
+ Glib::ustring::size_type len = udir.length();
+ // leaving a trailing backslash on the directory name leads to the infamous
+ // double-directory bug on win32
+ if (len != 0 && udir[len - 1] == '\\') udir.erase(len - 1);
+
+ // Remove the extension: remove everything past the last period found past the last slash
+ // (not for CUSTOM_TYPE as we can not automatically add a file extension in that case yet)
+ if (dialogType == CUSTOM_TYPE) {
+ setFilename(udir);
+ } else {
+ size_t last_slash_index = udir.find_last_of( '\\' );
+ size_t last_period_index = udir.find_last_of( '.' );
+ if (last_period_index > last_slash_index) {
+ setFilename(udir.substr(0, last_period_index ));
+ }
+ }
+
+ // remove one slash if double
+ auto myFilename = getFilename();
+ if (1 + myFilename.find("\\\\",2)) {
+ myFilename.replace(myFilename.find("\\\\",2), 1, "");
+ }
+ setFilename(myFilename);
+ }
+}
+
+FileSaveDialogImplWin32::~FileSaveDialogImplWin32()
+{
+}
+
+void FileSaveDialogImplWin32::createFilterMenu()
+{
+ std::list<Filter> filter_list;
+
+ knownExtensions.clear();
+
+ // Compose the filter string
+ Glib::ustring all_inkscape_files_filter, all_image_files_filter;
+ Inkscape::Extension::DB::OutputList extension_list;
+ Inkscape::Extension::db.get_output_list(extension_list);
+
+ int filter_count = 0;
+ int filter_length = 1;
+
+ for (auto omod : extension_list) {
+ // Windows OFN dialog does not allow disabled entries in the dialog
+ // So we just remove them. This is a regression from the Gtk dialog.
+ if (omod->deactivated())
+ continue;
+
+ // Export types are either exported vector types, or any raster type.
+ if (!omod->is_exported() && omod->is_raster() != (dialogType == EXPORT_TYPES))
+ continue;
+
+ // This extension is limited to save copy only.
+ if (omod->savecopy_only() && save_method != Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY)
+ continue;
+
+ filter_count++;
+
+ Filter filter;
+
+ // Extension
+ const gchar *filter_extension = omod->get_extension();
+ filter.filter = g_utf8_to_utf16(
+ filter_extension, -1, NULL, &filter.filter_length, NULL);
+ knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(Glib::ustring(filter_extension).casefold(), omod));
+
+ // Type
+ filter.name = g_utf8_to_utf16(omod->get_filetypename(true), -1, NULL, &filter.name_length, NULL);
+
+ filter.mod = omod;
+
+ filter_length += filter.name_length +
+ filter.filter_length + 3; // Add 3 for two \0s and a *
+
+ filter_list.push_back(filter);
+ }
+
+ int extension_index = 0;
+ _extension_map = new Inkscape::Extension::Extension*[filter_count];
+
+ _filter = new wchar_t[filter_length];
+ wchar_t *filterptr = _filter;
+
+ for(std::list<Filter>::iterator filter_iterator = filter_list.begin();
+ filter_iterator != filter_list.end(); ++filter_iterator)
+ {
+ const Filter &filter = *filter_iterator;
+
+ wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length);
+ filterptr += filter.name_length;
+ g_free(filter.name);
+
+ *(filterptr++) = L'\0';
+ *(filterptr++) = L'*';
+
+ wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length);
+ filterptr += filter.filter_length;
+ g_free(filter.filter);
+
+ *(filterptr++) = L'\0';
+
+ // Associate this input extension with the file type name
+ _extension_map[extension_index++] = filter.mod;
+ }
+ *(filterptr++) = 0;
+
+ _filter_count = extension_index;
+ _filter_index = 1; // A value of 1 selects the 1st filter - NOT the 2nd
+}
+
+
+void FileSaveDialogImplWin32::addFileType(Glib::ustring name, Glib::ustring pattern)
+{
+ std::list<Filter> filter_list;
+
+ knownExtensions.clear();
+
+ Filter all_exe_files;
+
+ const gchar *all_exe_files_filter_name = name.data();
+ const gchar *all_exe_files_filter = pattern.data();
+
+ // Calculate the amount of memory required
+ int filter_count = 1;
+ int filter_length = 1;
+
+ // Filter Executable Files
+ all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name,
+ -1, NULL, &all_exe_files.name_length, NULL);
+ all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter,
+ -1, NULL, &all_exe_files.filter_length, NULL);
+ all_exe_files.mod = NULL;
+ filter_list.push_front(all_exe_files);
+
+ filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a *
+
+ knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(Glib::ustring(all_exe_files_filter).casefold(), NULL));
+
+ int extension_index = 0;
+ _extension_map = new Inkscape::Extension::Extension*[filter_count];
+
+ _filter = new wchar_t[filter_length];
+ wchar_t *filterptr = _filter;
+
+ for(std::list<Filter>::iterator filter_iterator = filter_list.begin();
+ filter_iterator != filter_list.end(); ++filter_iterator)
+ {
+ const Filter &filter = *filter_iterator;
+
+ wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length);
+ filterptr += filter.name_length;
+ g_free(filter.name);
+
+ *(filterptr++) = L'\0';
+ *(filterptr++) = L'*';
+
+ if(filter.filter != NULL)
+ {
+ wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length);
+ filterptr += filter.filter_length;
+ g_free(filter.filter);
+ }
+
+ *(filterptr++) = L'\0';
+
+ // Associate this input extension with the file type name
+ _extension_map[extension_index++] = filter.mod;
+ }
+ *(filterptr++) = L'\0';
+
+ _filter_count = extension_index;
+ _filter_index = 1; // Select the 1st filter in the list
+
+
+}
+
+void FileSaveDialogImplWin32::GetSaveFileName_thread()
+{
+ OPENFILENAMEW ofn;
+
+ g_assert(_main_loop != NULL);
+
+ WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16(
+ _current_directory.data(), _current_directory.length(),
+ NULL, NULL, NULL);
+
+ // Copy the selected file name, converting from UTF-8 to UTF-16
+ memset(_path_string, 0, sizeof(_path_string));
+ gunichar2* utf16_path_string = g_utf8_to_utf16(
+ getFilename().data(), -1, NULL, NULL, NULL);
+ wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH);
+ g_free(utf16_path_string);
+
+ ZeroMemory(&ofn, sizeof(ofn));
+ ofn.lStructSize = sizeof(ofn);
+ ofn.hwndOwner = _ownerHwnd;
+ ofn.lpstrFile = _path_string;
+ ofn.nMaxFile = _MAX_PATH;
+ ofn.nFilterIndex = _filter_index;
+ ofn.lpstrFileTitle = NULL;
+ ofn.nMaxFileTitle = 0;
+ ofn.lpstrInitialDir = current_directory_string;
+ ofn.lpstrTitle = _title;
+ ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLESIZING;
+ ofn.lpstrFilter = _filter;
+ ofn.nFilterIndex = _filter_index;
+ ofn.lpfnHook = GetSaveFileName_hookproc;
+ ofn.lpstrDefExt = L"svg\0";
+ ofn.lCustData = (LPARAM)this;
+ _result = GetSaveFileNameW(&ofn) != 0;
+
+ g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count);
+ _filter_index = ofn.nFilterIndex;
+ _extension = _extension_map[ofn.nFilterIndex - 1];
+
+ // Copy the selected file name, converting from UTF-16 to UTF-8
+ setFilename(utf16_to_ustring(_path_string, _MAX_PATH));
+
+ // Tidy up
+ g_free(current_directory_string);
+
+ g_main_loop_quit(_main_loop);
+}
+
+/**
+ * Show this dialog modally. Return true if user hits [OK]
+ */
+bool
+FileSaveDialogImplWin32::show()
+{
+ _result = false;
+ _main_loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ if(_main_loop != NULL)
+ {
+ std::thread thethread([this] { GetSaveFileName_thread(); });
+ g_main_loop_run(_main_loop);
+
+ if(_result && _extension)
+ appendExtension(_filename, (Inkscape::Extension::Output*)_extension);
+
+ thethread.join();
+ }
+
+ return _result;
+}
+
+UINT_PTR CALLBACK FileSaveDialogImplWin32::GetSaveFileName_hookproc(
+ HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam)
+{
+ FileSaveDialogImplWin32 *pImpl = reinterpret_cast<FileSaveDialogImplWin32*>
+ (GetWindowLongPtr(hdlg, GWLP_USERDATA));
+
+ switch(uiMsg)
+ {
+ case WM_INITDIALOG:
+ {
+ HWND hParentWnd = GetParent(hdlg);
+ HINSTANCE hInstance = GetModuleHandle(NULL);
+
+ // get size/pos of typical combo box
+ RECT rEDT1, rCB1, rROOT, rST;
+ GetWindowRect(GetDlgItem(hParentWnd, cmb1), &rCB1);
+ GetWindowRect(GetDlgItem(hParentWnd, cmb13), &rEDT1);
+ GetWindowRect(GetDlgItem(hParentWnd, stc2), &rST);
+ GetWindowRect(hdlg, &rROOT);
+ int ydelta = rCB1.top - rEDT1.top;
+ if ( ydelta < 0 ) {
+ g_warning("Negative dialog ydelta");
+ ydelta = 0;
+ }
+
+ // Make the window a bit longer
+ // Note: we have a width delta of 1 because there is a suspicion that MoveWindow() to the same size causes zero-width results.
+ RECT rcRect;
+ GetWindowRect(hParentWnd, &rcRect);
+ MoveWindow(hParentWnd, rcRect.left, rcRect.top,
+ sanitizeWindowSizeParam( rcRect.right - rcRect.left, 1, WINDOW_WIDTH_MINIMUM, WINDOW_WIDTH_FALLBACK ),
+ sanitizeWindowSizeParam( rcRect.bottom - rcRect.top, ydelta, WINDOW_HEIGHT_MINIMUM, WINDOW_HEIGHT_FALLBACK ),
+ FALSE);
+
+ // It is not necessary to delete stock objects by calling DeleteObject
+ HGDIOBJ dlgFont = GetStockObject(DEFAULT_GUI_FONT);
+
+ // Set the pointer to the object
+ OPENFILENAMEW *ofn = reinterpret_cast<OPENFILENAMEW*>(lParam);
+ SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData);
+ SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData);
+ pImpl = reinterpret_cast<FileSaveDialogImplWin32*>(ofn->lCustData);
+
+ // Create the Title label and edit control
+ wchar_t *title_label_str = (wchar_t *)g_utf8_to_utf16(_("Title:"), -1, NULL, NULL, NULL);
+ pImpl->_title_label = CreateWindowExW(0, L"STATIC", title_label_str,
+ WS_VISIBLE|WS_CHILD,
+ CW_USEDEFAULT, CW_USEDEFAULT, rCB1.left-rST.left, rST.bottom-rST.top,
+ hParentWnd, NULL, hInstance, NULL);
+ g_free(title_label_str);
+
+ if(pImpl->_title_label) {
+ if(dlgFont) SendMessage(pImpl->_title_label, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0));
+ SetWindowPos(pImpl->_title_label, NULL, rST.left-rROOT.left, rST.top+ydelta-rROOT.top,
+ rCB1.left-rST.left, rST.bottom-rST.top, SWP_SHOWWINDOW|SWP_NOZORDER);
+ }
+
+ pImpl->_title_edit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "",
+ WS_VISIBLE|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL,
+ CW_USEDEFAULT, CW_USEDEFAULT, rCB1.right-rCB1.left, rCB1.bottom-rCB1.top,
+ hParentWnd, NULL, hInstance, NULL);
+ if(pImpl->_title_edit) {
+ if(dlgFont) SendMessage(pImpl->_title_edit, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0));
+ SetWindowPos(pImpl->_title_edit, NULL, rCB1.left-rROOT.left, rCB1.top+ydelta-rROOT.top,
+ rCB1.right-rCB1.left, rCB1.bottom-rCB1.top, SWP_SHOWWINDOW|SWP_NOZORDER);
+ SetWindowTextW(pImpl->_title_edit,
+ (const wchar_t*)g_utf8_to_utf16(pImpl->myDocTitle.c_str(), -1, NULL, NULL, NULL));
+ }
+ }
+ break;
+ case WM_DESTROY:
+ {
+ if(pImpl->_title_edit) {
+ int length = GetWindowTextLengthW(pImpl->_title_edit)+1;
+ wchar_t* temp_title = new wchar_t[length];
+ GetWindowTextW(pImpl->_title_edit, temp_title, length);
+ pImpl->myDocTitle = g_utf16_to_utf8((gunichar2*)temp_title, -1, NULL, NULL, NULL);
+ delete[] temp_title;
+ DestroyWindow(pImpl->_title_label);
+ pImpl->_title_label = NULL;
+ DestroyWindow(pImpl->_title_edit);
+ pImpl->_title_edit = NULL;
+ }
+ }
+ break;
+ }
+
+ // Use default dialog behaviour
+ return 0;
+}
+
+} } } // namespace Dialog, UI, Inkscape
+
+#endif // ifdef _WIN32
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/filedialogimpl-win32.h b/src/ui/dialog/filedialogimpl-win32.h
new file mode 100644
index 0000000..3b12f7c
--- /dev/null
+++ b/src/ui/dialog/filedialogimpl-win32.h
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Implementation of native file dialogs for Win32
+ */
+/* Authors:
+ * Joel Holdsworth
+ * Inkscape Authors
+ *
+ * Copyright (C) 2004-2008 Inkscape Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm.h>
+
+#ifdef _WIN32
+
+#include "filedialogimpl-gtkmm.h"
+
+#include "inkgc/gc-core.h"
+
+#include <memory>
+#include <mutex>
+#include <windows.h>
+
+
+namespace Inkscape
+{
+namespace UI
+{
+namespace Dialog
+{
+
+/*#########################################################################
+### F I L E D I A L O G B A S E C L A S S
+#########################################################################*/
+
+/// This class is the base implementation of a MS Windows
+/// file dialog.
+class FileDialogBaseWin32
+{
+protected:
+ /// Abstract Constructor
+ /// @param parent The parent window for the dialog
+ /// @param dir The directory to begin browsing from
+ /// @param title The title caption for the dialog in UTF-8
+ /// @param type The dialog type
+ /// @param preferenceBase The preferences key
+ FileDialogBaseWin32(Gtk::Window &parent, const Glib::ustring &dir,
+ const char *title, FileDialogType type,
+ gchar const *preferenceBase);
+
+ /// Destructor
+ ~FileDialogBaseWin32();
+
+public:
+
+ /// Get the path of the current directory
+ Glib::ustring getCurrentDirectory();
+
+protected:
+ /// The dialog type
+ FileDialogType dialogType;
+
+ /// A pointer to the GTK main-loop context object. This
+ /// is used to keep the rest of the inkscape UI running
+ /// while the file dialog is displayed
+ GMainLoop *_main_loop;
+
+ /// The result of the call to GetOpenFileName. If true
+ /// the user clicked OK, if false the user clicked cancel
+ bool _result;
+
+ /// The parent window
+ Gtk::Window &parent;
+
+ /// The windows handle of the parent window
+ HWND _ownerHwnd;
+
+ /// The path of the directory that is currently being
+ /// browsed
+ Glib::ustring _current_directory;
+
+ /// The title of the dialog in UTF-16
+ wchar_t *_title;
+
+ /// The path of the currently selected file in UTF-16
+ wchar_t _path_string[_MAX_PATH];
+
+ /// The filter string for GetOpenFileName in UTF-16
+ wchar_t *_filter;
+
+ /// The index of the currently selected filter.
+ /// This value must be greater than or equal to 1,
+ /// and less than or equal to _filter_count.
+ unsigned int _filter_index;
+
+ /// The number of filters registered
+ unsigned int _filter_count;
+
+ /// An array of the extensions associated with the
+ /// file types of each filter. So the Nth entry of
+ /// this array corresponds to the extension of the Nth
+ /// filter in the list. NULL if no specific extension is
+ /// specified/
+ Inkscape::Extension::Extension **_extension_map;
+};
+
+
+/*#########################################################################
+### F I L E O P E N
+#########################################################################*/
+
+/// An Inkscape compatible wrapper around MS Windows GetOpenFileName API
+class FileOpenDialogImplWin32 : public FileOpenDialog, public FileDialogBaseWin32
+{
+public:
+ /// Constructor
+ /// @param parent The parent window for the dialog
+ /// @param dir The directory to begin browsing from
+ /// @param title The title caption for the dialog in UTF-8
+ /// @param type The dialog type
+ FileOpenDialogImplWin32(Gtk::Window &parent,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const char *title);
+
+ /// Destructor
+ virtual ~FileOpenDialogImplWin32();
+
+ /// Shows the file dialog, and blocks until a file
+ /// has been selected.
+ /// @return Returns true if the user selected a
+ /// file, or false if the user pressed cancel.
+ bool show();
+
+ /// Gets a list of the selected file names
+ /// @return Returns an STL vector filled with the
+ /// GTK names of the selected files
+ std::vector<Glib::ustring> getFilenames();
+
+ /// Get the path of the current directory
+ virtual Glib::ustring getCurrentDirectory()
+ { return FileDialogBaseWin32::getCurrentDirectory(); }
+
+ /// Add a custom file filter menu item
+ /// @param name - Name of the filter (such as "Javscript")
+ /// @param pattern - File filtering pattern (such as "*.js")
+ /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types
+ void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "", Inkscape::Extension::Extension *mod = nullptr) override;
+
+private:
+
+ /// Create filter menu for this type of dialog
+ void createFilterMenu();
+
+ /// The handle of the preview pane window
+ HWND _preview_wnd;
+
+ /// The handle of the file dialog window
+ HWND _file_dialog_wnd;
+
+ /// A pointer to the standard window proc of the
+ /// unhooked file dialog
+ WNDPROC _base_window_proc;
+
+ /// The handle of the bitmap of the "show preview"
+ /// toggle button
+ HBITMAP _show_preview_button_bitmap;
+
+ /// The handle of the toolbar's window
+ HWND _toolbar_wnd;
+
+ /// This flag is set true when the preview should be
+ /// shown, or false when it should be hidden
+ static bool _show_preview;
+
+
+ /// The current width of the preview pane in pixels
+ int _preview_width;
+
+ /// The current height of the preview pane in pixels
+ int _preview_height;
+
+ /// The handle of the windows to display within the
+ /// preview pane, or NULL if no image should be displayed
+ HBITMAP _preview_bitmap;
+
+ /// The windows shell icon for the selected file
+ HICON _preview_file_icon;
+
+ /// The size of the preview file in kilobytes
+ unsigned long _preview_file_size;
+
+
+ /// The width of the document to be shown in the preview panel
+ double _preview_document_width;
+
+ /// The width of the document to be shown in the preview panel
+ double _preview_document_height;
+
+ /// The width of the rendered preview image in pixels
+ int _preview_image_width;
+
+ /// The height of the rendered preview image in pixels
+ int _preview_image_height;
+
+ /// A GDK Pixbuf of the rendered preview to be displayed
+ Glib::RefPtr<Gdk::Pixbuf> _preview_bitmap_image;
+
+ /// This flag is set true if a file has been selected
+ bool _file_selected;
+
+ /// This flag is set true when the GetOpenFileName call
+ /// has returned
+ bool _finished;
+
+ /// This mutex is used to ensure that the worker thread
+ /// that calls GetOpenFileName cannot collide with the
+ /// main Inkscape thread
+ std::unique_ptr<std::mutex> _mutex;
+
+ /// The controller function for the thread which calls
+ /// GetOpenFileName
+ void GetOpenFileName_thread();
+
+ /// Registers the Windows Class of the preview panel window
+ static void register_preview_wnd_class();
+
+ /// A message proc which is called by the standard dialog
+ /// proc
+ static UINT_PTR CALLBACK GetOpenFileName_hookproc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam);
+
+ /// A message proc which wraps the standard dialog proc,
+ /// but intercepts some calls
+ static LRESULT CALLBACK file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+
+ /// The message proc for the preview panel window
+ static LRESULT CALLBACK preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+
+ /// Lays out the controls in the file dialog given it's
+ /// current size
+ /// GetOpenFileName thread only.
+ void layout_dialog();
+
+ /// Enables or disables the file preview.
+ /// GetOpenFileName thread only.
+ void enable_preview(bool enable);
+
+ /// This function is called in the App thread when a file had
+ /// been selected
+ void file_selected();
+
+ /// Loads and renders the unshrunk preview image.
+ /// Main app thread only.
+ void load_preview();
+
+ /// Frees all the allocated objects associated with the file
+ /// currently being previewed
+ /// Main app thread only.
+ void free_preview();
+
+ /// Loads preview for an SVG or SVGZ file.
+ /// Main app thread only.
+ /// @return Returns true if the SVG loaded successfully
+ bool set_svg_preview();
+
+ /// A callback to allow this class to dispose of the
+ /// memory block of the rendered SVG bitmap
+ /// @buffer buffer The buffer to free
+ static void destroy_svg_rendering(const guint8 *buffer);
+
+ /// Loads the preview for a raster image
+ /// Main app thread only.
+ /// @return Returns true if the image loaded successfully
+ bool set_image_preview();
+
+ /// Loads the preview for a meta file
+ /// Main app thread only.
+ /// @return Returns true if the image loaded successfully
+ bool set_emf_preview();
+
+ /// This flag is set true when a meta file is previewed
+ bool _preview_emf_image;
+
+ /// Renders the unshrunk preview image to a windows HTBITMAP
+ /// which can be painted in the preview pain.
+ /// Main app thread only.
+ void render_preview();
+
+ /// Formats the caption in UTF-16 for the preview image
+ /// @param caption The buffer to format the caption string into
+ /// @param caption_size The number of wchar_ts in the caption buffer
+ /// @return Returns the number of characters in caption string
+ int format_caption(wchar_t *caption, int caption_size);
+};
+
+
+/*#########################################################################
+### F I L E S A V E
+#########################################################################*/
+
+/// An Inkscape compatible wrapper around MS Windows GetSaveFileName API
+class FileSaveDialogImplWin32 : public FileSaveDialog, public FileDialogBaseWin32
+{
+
+public:
+ FileSaveDialogImplWin32(Gtk::Window &parent,
+ const Glib::ustring &dir,
+ FileDialogType fileTypes,
+ const char *title,
+ const Glib::ustring &default_key,
+ const char *docTitle,
+ const Inkscape::Extension::FileSaveMethod save_method);
+
+ /// Destructor
+ virtual ~FileSaveDialogImplWin32();
+
+ /// Shows the file dialog, and blocks until a file
+ /// has been selected.
+ /// @return Returns true if the user selected a
+ /// file, or false if the user pressed cancel.
+ bool show();
+
+ /// Get the path of the current directory
+ virtual Glib::ustring getCurrentDirectory()
+ { return FileDialogBaseWin32::getCurrentDirectory(); }
+
+ void addFileType(Glib::ustring name, Glib::ustring pattern);
+ void addFilterMenu(const Glib::ustring &name, Glib::ustring pattern = "", Inkscape::Extension::Extension *mod = nullptr) override
+ {
+ }
+
+private:
+ /// A handle to the title label and edit box
+ HWND _title_label;
+ HWND _title_edit;
+
+ /// Create a filter menu for this type of dialog
+ void createFilterMenu();
+
+ // SaveAs or SaveAsCopy
+ Inkscape::Extension::FileSaveMethod save_method;
+
+ /// The controller function for the thread which calls
+ /// GetSaveFileName
+ void GetSaveFileName_thread();
+
+ /// A message proc which is called by the standard dialog
+ /// proc
+ static UINT_PTR CALLBACK GetSaveFileName_hookproc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam);
+
+};
+
+
+}
+}
+}
+
+#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/dialog/fill-and-stroke.cpp b/src/ui/dialog/fill-and-stroke.cpp
new file mode 100644
index 0000000..5f24214
--- /dev/null
+++ b/src/ui/dialog/fill-and-stroke.cpp
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Fill and Stroke dialog - implementation.
+ *
+ * Based on the old sp_object_properties_dialog.
+ */
+/* Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2004--2007 Authors
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#include "desktop-style.h"
+#include "document.h"
+#include "fill-and-stroke.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "preferences.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+#include "ui/widget/fill-style.h"
+#include "ui/widget/stroke-style.h"
+#include "ui/widget/notebook-page.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+FillAndStroke::FillAndStroke()
+ : DialogBase("/dialogs/fillstroke", "FillStroke")
+ , _page_fill(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true)))
+ , _page_stroke_paint(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true)))
+ , _page_stroke_style(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true)))
+ , _composite_settings(INKSCAPE_ICON("dialog-fill-and-stroke"),
+ "fillstroke",
+ UI::Widget::SimpleFilterModifier::ISOLATION |
+ UI::Widget::SimpleFilterModifier::BLEND |
+ UI::Widget::SimpleFilterModifier::BLUR |
+ UI::Widget::SimpleFilterModifier::OPACITY)
+ , fillWdgt(nullptr)
+ , strokeWdgt(nullptr)
+{
+ set_spacing(2);
+ pack_start(_notebook, true, true);
+
+ _notebook.append_page(*_page_fill, _createPageTabLabel(_("_Fill"), INKSCAPE_ICON("object-fill")));
+ _notebook.append_page(*_page_stroke_paint, _createPageTabLabel(_("Stroke _paint"), INKSCAPE_ICON("object-stroke")));
+ _notebook.append_page(*_page_stroke_style, _createPageTabLabel(_("Stroke st_yle"), INKSCAPE_ICON("object-stroke-style")));
+ _notebook.set_vexpand(true);
+
+ _notebook.signal_switch_page().connect(sigc::mem_fun(*this, &FillAndStroke::_onSwitchPage));
+
+ _layoutPageFill();
+ _layoutPageStrokePaint();
+ _layoutPageStrokeStyle();
+
+ pack_end(_composite_settings, Gtk::PACK_SHRINK);
+
+ show_all_children();
+
+ _composite_settings.setSubject(&_subject);
+}
+
+FillAndStroke::~FillAndStroke()
+{
+ // Disconnect signals from composite settings
+ _composite_settings.setSubject(nullptr);
+ fillWdgt->setDesktop(nullptr);
+ strokeWdgt->setDesktop(nullptr);
+ strokeStyleWdgt->setDesktop(nullptr);
+ _subject.setDesktop(nullptr);
+}
+
+void FillAndStroke::selectionChanged(Selection *selection)
+{
+ if (fillWdgt) {
+ fillWdgt->performUpdate();
+ }
+ if (strokeWdgt) {
+ strokeWdgt->performUpdate();
+ }
+ if (strokeStyleWdgt) {
+ strokeStyleWdgt->selectionChangedCB();
+ }
+}
+void FillAndStroke::selectionModified(Selection *selection, guint flags)
+{
+ if (fillWdgt) {
+ fillWdgt->selectionModifiedCB(flags);
+ }
+ if (strokeWdgt) {
+ strokeWdgt->selectionModifiedCB(flags);
+ }
+ if (strokeStyleWdgt) {
+ strokeStyleWdgt->selectionModifiedCB(flags);
+ }
+}
+
+void FillAndStroke::desktopReplaced()
+{
+ if (fillWdgt) {
+ fillWdgt->setDesktop(getDesktop());
+ }
+ if (strokeWdgt) {
+ strokeWdgt->setDesktop(getDesktop());
+ }
+ if (strokeStyleWdgt) {
+ strokeStyleWdgt->setDesktop(getDesktop());
+ }
+ _subject.setDesktop(getDesktop());
+}
+
+void FillAndStroke::_onSwitchPage(Gtk::Widget * /*page*/, guint pagenum)
+{
+ _savePagePref(pagenum);
+}
+
+void
+FillAndStroke::_savePagePref(guint page_num)
+{
+ // remember the current page
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/dialogs/fillstroke/page", page_num);
+}
+
+void
+FillAndStroke::_layoutPageFill()
+{
+ fillWdgt = Gtk::manage(new UI::Widget::FillNStroke(FILL));
+ _page_fill->table().attach(*fillWdgt, 0, 0, 1, 1);
+}
+
+void
+FillAndStroke::_layoutPageStrokePaint()
+{
+ strokeWdgt = Gtk::manage(new UI::Widget::FillNStroke(STROKE));
+ _page_stroke_paint->table().attach(*strokeWdgt, 0, 0, 1, 1);
+}
+
+void
+FillAndStroke::_layoutPageStrokeStyle()
+{
+ strokeStyleWdgt = Gtk::manage(new UI::Widget::StrokeStyle());
+ strokeStyleWdgt->set_hexpand();
+ strokeStyleWdgt->set_halign(Gtk::ALIGN_START);
+ _page_stroke_style->table().attach(*strokeStyleWdgt, 0, 0, 1, 1);
+}
+
+void
+FillAndStroke::showPageFill()
+{
+ blink();
+ _notebook.set_current_page(0);
+ _savePagePref(0);
+
+}
+
+void
+FillAndStroke::showPageStrokePaint()
+{
+ blink();
+ _notebook.set_current_page(1);
+ _savePagePref(1);
+}
+
+void
+FillAndStroke::showPageStrokeStyle()
+{
+ blink();
+ _notebook.set_current_page(2);
+ _savePagePref(2);
+
+}
+
+Gtk::Box&
+FillAndStroke::_createPageTabLabel(const Glib::ustring& label, const char *label_image)
+{
+ Gtk::Box *_tab_label_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+
+ auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU));
+ _tab_label_box->pack_start(*img);
+
+ Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true));
+ _tab_label_box->pack_start(*_tab_label);
+ _tab_label_box->show_all();
+
+ return *_tab_label_box;
+}
+
+} // 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/dialog/fill-and-stroke.h b/src/ui/dialog/fill-and-stroke.h
new file mode 100644
index 0000000..9780f33
--- /dev/null
+++ b/src/ui/dialog/fill-and-stroke.h
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Fill and Stroke dialog
+ */
+/* Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Gustav Broberg <broberg@kth.se>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2004--2007 Authors
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H
+#define INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H
+
+#include <gtkmm/notebook.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/object-composite-settings.h"
+#include "ui/widget/style-subject.h"
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+class FillNStroke;
+class NotebookPage;
+class StrokeStyle;
+}
+
+namespace Dialog {
+
+class FillAndStroke : public DialogBase
+{
+public:
+ FillAndStroke();
+ ~FillAndStroke() override;
+
+ void desktopReplaced() override;
+
+ void showPageFill();
+ void showPageStrokePaint();
+ void showPageStrokeStyle();
+
+protected:
+ Gtk::Notebook _notebook;
+
+ UI::Widget::NotebookPage *_page_fill;
+ UI::Widget::NotebookPage *_page_stroke_paint;
+ UI::Widget::NotebookPage *_page_stroke_style;
+
+ UI::Widget::StyleSubject::Selection _subject;
+ UI::Widget::ObjectCompositeSettings _composite_settings;
+
+ Gtk::Box &_createPageTabLabel(const Glib::ustring &label,
+ const char *label_image);
+
+ void _layoutPageFill();
+ void _layoutPageStrokePaint();
+ void _layoutPageStrokeStyle();
+ void _savePagePref(guint page_num);
+ void _onSwitchPage(Gtk::Widget *page, guint pagenum);
+
+private:
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+ UI::Widget::FillNStroke *fillWdgt;
+ UI::Widget::FillNStroke *strokeWdgt;
+ UI::Widget::StrokeStyle *strokeStyleWdgt;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+
+#endif // INKSCAPE_UI_DIALOG_FILL_AND_STROKE_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/dialog/filter-effects-dialog.cpp b/src/ui/dialog/filter-effects-dialog.cpp
new file mode 100644
index 0000000..7429398
--- /dev/null
+++ b/src/ui/dialog/filter-effects-dialog.cpp
@@ -0,0 +1,3376 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Filter Effects dialog.
+ */
+/* Authors:
+ * Nicholas Bishop <nicholasbishop@gmail.org>
+ * Rodrigo Kumpera <kumpera@gmail.com>
+ * Felipe C. da S. Sanches <juca@members.fsf.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * insaner
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cmath>
+#include <gdkmm/pixbufanimation.h>
+#include <gdkmm/rgba.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/image.h>
+#include <gtkmm/imagemenuitem.h>
+
+#include <gdkmm/display.h>
+#include <gdkmm/general.h>
+#include <gdkmm/seat.h>
+
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/colorbutton.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/label.h>
+#include <gtkmm/menuitem.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/sizegroup.h>
+
+#include <glibmm/i18n.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/main.h>
+#include <glibmm/convert.h>
+
+#include <gtkmm/textview.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treeviewcolumn.h>
+#include <gtkmm/widget.h>
+#include <pangomm/layout.h>
+#include <utility>
+#include <vector>
+
+#include "desktop.h"
+#include "display/nr-filter-types.h"
+#include "document-undo.h"
+#include "document.h"
+#include "filter-chemistry.h"
+#include "filter-effects-dialog.h"
+#include "filter-enums.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "selection-chemistry.h"
+#include "style.h"
+
+#include "include/gtkmm_version.h"
+
+#include "object/filters/blend.h"
+#include "object/filters/colormatrix.h"
+#include "object/filters/componenttransfer.h"
+#include "object/filters/componenttransfer-funcnode.h"
+#include "object/filters/convolvematrix.h"
+#include "object/filters/distantlight.h"
+#include "object/filters/merge.h"
+#include "object/filters/mergenode.h"
+#include "object/filters/pointlight.h"
+#include "object/filters/spotlight.h"
+
+#include "svg/svg-color.h"
+
+#include "ui/builder-utils.h"
+#include "ui/column-menu-builder.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/icon-names.h"
+#include "ui/util.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/completion-popup.h"
+#include "ui/widget/custom-tooltip.h"
+#include "ui/widget/filter-effect-chooser.h"
+#include "ui/widget/spinbutton.h"
+
+#include "io/sys.h"
+
+
+using namespace Inkscape::Filters;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+using Inkscape::UI::Widget::AttrWidget;
+using Inkscape::UI::Widget::ComboBoxEnum;
+using Inkscape::UI::Widget::DualSpinScale;
+using Inkscape::UI::Widget::SpinScale;
+
+constexpr int max_convolution_kernel_size = 10;
+
+// Returns the number of inputs available for the filter primitive type
+static int input_count(const SPFilterPrimitive* prim)
+{
+ if(!prim)
+ return 0;
+ else if(is<SPFeBlend>(prim) || is<SPFeComposite>(prim) || is<SPFeDisplacementMap>(prim))
+ return 2;
+ else if(is<SPFeMerge>(prim)) {
+ // Return the number of feMergeNode connections plus an extra
+ return (int) (prim->children.size() + 1);
+ }
+ else
+ return 1;
+}
+
+class CheckButtonAttr : public Gtk::CheckButton, public AttrWidget
+{
+public:
+ CheckButtonAttr(bool def, const Glib::ustring& label,
+ Glib::ustring tv, Glib::ustring fv,
+ const SPAttr a, char* tip_text)
+ : Gtk::CheckButton(label),
+ AttrWidget(a, def),
+ _true_val(std::move(tv)), _false_val(std::move(fv))
+ {
+ signal_toggled().connect(signal_attr_changed().make_slot());
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ return get_active() ? _true_val : _false_val;
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ if(val) {
+ if(_true_val == val)
+ set_active(true);
+ else if(_false_val == val)
+ set_active(false);
+ } else {
+ set_active(get_default()->as_bool());
+ }
+ }
+private:
+ const Glib::ustring _true_val, _false_val;
+};
+
+class SpinButtonAttr : public Inkscape::UI::Widget::SpinButton, public AttrWidget
+{
+public:
+ SpinButtonAttr(double lower, double upper, double step_inc,
+ double climb_rate, int digits, const SPAttr a, double def, char* tip_text)
+ : Inkscape::UI::Widget::SpinButton(climb_rate, digits),
+ AttrWidget(a, def)
+ {
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+ set_range(lower, upper);
+ set_increments(step_inc, 0);
+
+ signal_value_changed().connect(signal_attr_changed().make_slot());
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ const double val = get_value();
+
+ if(get_digits() == 0)
+ return Glib::Ascii::dtostr((int)val);
+ else
+ return Glib::Ascii::dtostr(val);
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ if(val){
+ set_value(Glib::Ascii::strtod(val));
+ } else {
+ set_value(get_default()->as_double());
+ }
+ }
+};
+
+template< typename T> class ComboWithTooltip : public Gtk::EventBox
+{
+public:
+ ComboWithTooltip<T>(T default_value, const Util::EnumDataConverter<T>& c, const SPAttr a = SPAttr::INVALID, char* tip_text = nullptr)
+ {
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+ combo = new ComboBoxEnum<T>(default_value, c, a, false);
+ add(*combo);
+ show_all();
+ }
+
+ ~ComboWithTooltip() override
+ {
+ delete combo;
+ }
+
+ ComboBoxEnum<T>* get_attrwidget()
+ {
+ return combo;
+ }
+private:
+ ComboBoxEnum<T>* combo;
+};
+
+// Contains an arbitrary number of spin buttons that use separate attributes
+class MultiSpinButton : public Gtk::Box
+{
+public:
+ MultiSpinButton(double lower, double upper, double step_inc,
+ double climb_rate, int digits, std::vector<SPAttr> attrs, std::vector<double> default_values, std::vector<char*> tip_text)
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ {
+ g_assert(attrs.size()==default_values.size());
+ g_assert(attrs.size()==tip_text.size());
+ set_spacing(4);
+ for(unsigned i = 0; i < attrs.size(); ++i) {
+ unsigned index = attrs.size() - 1 - i;
+ _spins.push_back(new SpinButtonAttr(lower, upper, step_inc, climb_rate, digits, attrs[index], default_values[index], tip_text[index]));
+ pack_end(*_spins.back(), true, true);
+ _spins.back()->set_width_chars(3); // allow spin buttons to shrink to save space
+ }
+ }
+
+ ~MultiSpinButton() override
+ {
+ for(auto & _spin : _spins)
+ delete _spin;
+ }
+
+ std::vector<SpinButtonAttr*>& get_spinbuttons()
+ {
+ return _spins;
+ }
+private:
+ std::vector<SpinButtonAttr*> _spins;
+};
+
+// Contains two spinbuttons that describe a NumberOptNumber
+class DualSpinButton : public Gtk::Box, public AttrWidget
+{
+public:
+ DualSpinButton(char* def, double lower, double upper, double step_inc,
+ double climb_rate, int digits, const SPAttr a, char* tt1, char* tt2)
+ : AttrWidget(a, def), //TO-DO: receive default num-opt-num as parameter in the constructor
+ Gtk::Box(Gtk::ORIENTATION_HORIZONTAL),
+ _s1(climb_rate, digits), _s2(climb_rate, digits)
+ {
+ if (tt1) {
+ _s1.set_tooltip_text(tt1);
+ }
+ if (tt2) {
+ _s2.set_tooltip_text(tt2);
+ }
+ _s1.set_range(lower, upper);
+ _s2.set_range(lower, upper);
+ _s1.set_increments(step_inc, 0);
+ _s2.set_increments(step_inc, 0);
+
+ _s1.signal_value_changed().connect(signal_attr_changed().make_slot());
+ _s2.signal_value_changed().connect(signal_attr_changed().make_slot());
+
+ set_spacing(4);
+ pack_end(_s2, true, true);
+ pack_end(_s1, true, true);
+ }
+
+ Inkscape::UI::Widget::SpinButton& get_spinbutton1()
+ {
+ return _s1;
+ }
+
+ Inkscape::UI::Widget::SpinButton& get_spinbutton2()
+ {
+ return _s2;
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ double v1 = _s1.get_value();
+ double v2 = _s2.get_value();
+
+ if(_s1.get_digits() == 0) {
+ v1 = (int)v1;
+ v2 = (int)v2;
+ }
+
+ return Glib::Ascii::dtostr(v1) + " " + Glib::Ascii::dtostr(v2);
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ NumberOptNumber n;
+ if(val) {
+ n.set(val);
+ } else {
+ n.set(get_default()->as_charptr());
+ }
+ _s1.set_value(n.getNumber());
+ _s2.set_value(n.getOptNumber());
+
+ }
+private:
+ Inkscape::UI::Widget::SpinButton _s1, _s2;
+};
+
+class ColorButton : public Widget::ColorPicker, public AttrWidget
+{
+public:
+ ColorButton(unsigned int def, const SPAttr a, char* tip_text)
+ : ColorPicker(_("Select color"), tip_text ? tip_text : "", 0x000000ff, false),
+ AttrWidget(a, def)
+ {
+ use_transparency(false);
+ connectChanged([=](guint rgba){ signal_attr_changed().emit(); });
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+ setRgba32(0xffffffff);
+ }
+
+ // Returns the color in 'rgb(r,g,b)' form.
+ Glib::ustring get_as_attribute() const override
+ {
+ // no doubles here, so we can use the standard string stream.
+ std::ostringstream os;
+
+ const auto c = get_current_color();
+ int r = SP_RGBA32_R_U(c);
+ int g = SP_RGBA32_G_U(c);
+ int b = SP_RGBA32_B_U(c);
+ os << "rgb(" << r << "," << g << "," << b << ")";
+ return os.str();
+ }
+
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ guint32 i = 0;
+ if(val) {
+ i = sp_svg_read_color(val, 0xFFFFFFFF);
+ } else {
+ i = (guint32) get_default()->as_uint();
+ }
+ setRgba32(i);
+ }
+};
+
+// Used for tableValue in feComponentTransfer
+class EntryAttr : public Gtk::Entry, public AttrWidget
+{
+public:
+ EntryAttr(const SPAttr a, char* tip_text)
+ : AttrWidget(a)
+ {
+ set_width_chars(3); // let it get narrow
+ signal_changed().connect(signal_attr_changed().make_slot());
+ if (tip_text) {
+ set_tooltip_text(tip_text);
+ }
+ }
+
+ // No validity checking is done
+ Glib::ustring get_as_attribute() const override
+ {
+ return get_text();
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ if(val) {
+ set_text( val );
+ } else {
+ set_text( "" );
+ }
+ }
+};
+
+/* Displays/Edits the matrix for feConvolveMatrix or feColorMatrix */
+class FilterEffectsDialog::MatrixAttr : public Gtk::Frame, public AttrWidget
+{
+public:
+ MatrixAttr(const SPAttr a, char* tip_text = nullptr)
+ : AttrWidget(a), _locked(false)
+ {
+ _model = Gtk::ListStore::create(_columns);
+ _tree.set_model(_model);
+ _tree.set_headers_visible(false);
+ _tree.show();
+ add(_tree);
+ set_shadow_type(Gtk::SHADOW_IN);
+ if (tip_text) {
+ _tree.set_tooltip_text(tip_text);
+ }
+ }
+
+ std::vector<double> get_values() const
+ {
+ std::vector<double> vec;
+ for(const auto & iter : _model->children()) {
+ for(unsigned c = 0; c < _tree.get_columns().size(); ++c)
+ vec.push_back(iter[_columns.cols[c]]);
+ }
+ return vec;
+ }
+
+ void set_values(const std::vector<double>& v)
+ {
+ unsigned i = 0;
+ for(const auto & iter : _model->children()) {
+ for(unsigned c = 0; c < _tree.get_columns().size(); ++c) {
+ if(i >= v.size())
+ return;
+ iter[_columns.cols[c]] = v[i];
+ ++i;
+ }
+ }
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ // use SVGOStringStream to output SVG-compatible doubles
+ Inkscape::SVGOStringStream os;
+
+ for(const auto & iter : _model->children()) {
+ for(unsigned c = 0; c < _tree.get_columns().size(); ++c) {
+ os << iter[_columns.cols[c]] << " ";
+ }
+ }
+
+ return os.str();
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ if(o) {
+ if(is<SPFeConvolveMatrix>(o)) {
+ auto conv = cast<SPFeConvolveMatrix>(o);
+ int cols, rows;
+ cols = (int)conv->get_order().getNumber();
+ if (cols > max_convolution_kernel_size)
+ cols = max_convolution_kernel_size;
+ rows = conv->get_order().optNumIsSet() ? (int)conv->get_order().getOptNumber() : cols;
+ update(o, rows, cols);
+ }
+ else if(is<SPFeColorMatrix>(o))
+ update(o, 4, 5);
+ }
+ }
+private:
+ class MatrixColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ MatrixColumns()
+ {
+ cols.resize(max_convolution_kernel_size);
+ for(auto & col : cols)
+ add(col);
+ }
+ std::vector<Gtk::TreeModelColumn<double> > cols;
+ };
+
+ void update(SPObject* o, const int rows, const int cols)
+ {
+ if(_locked)
+ return;
+
+ _model->clear();
+
+ _tree.remove_all_columns();
+
+ std::vector<gdouble> const *values = nullptr;
+ if(is<SPFeColorMatrix>(o))
+ values = &cast<SPFeColorMatrix>(o)->get_values();
+ else if(is<SPFeConvolveMatrix>(o))
+ values = &cast<SPFeConvolveMatrix>(o)->get_kernel_matrix();
+ else
+ return;
+
+ if(o) {
+ int ndx = 0;
+
+ for(int i = 0; i < cols; ++i) {
+ _tree.append_column_numeric_editable("", _columns.cols[i], "%.2f");
+ static_cast<Gtk::CellRendererText*>(
+ _tree.get_column_cell_renderer(i))->signal_edited().connect(
+ sigc::mem_fun(*this, &MatrixAttr::rebind));
+ }
+
+ for(int r = 0; r < rows; ++r) {
+ Gtk::TreeRow row = *(_model->append());
+ // Default to identity matrix
+ for(int c = 0; c < cols; ++c, ++ndx)
+ row[_columns.cols[c]] = ndx < (int)values->size() ? (*values)[ndx] : (r == c ? 1 : 0);
+ }
+ }
+ }
+
+ void rebind(const Glib::ustring&, const Glib::ustring&)
+ {
+ _locked = true;
+ signal_attr_changed()();
+ _locked = false;
+ }
+
+ bool _locked;
+ Gtk::TreeView _tree;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ MatrixColumns _columns;
+};
+
+// Displays a matrix or a slider for feColorMatrix
+class FilterEffectsDialog::ColorMatrixValues : public Gtk::Frame, public AttrWidget
+{
+public:
+ ColorMatrixValues()
+ : AttrWidget(SPAttr::VALUES),
+ // TRANSLATORS: this dialog is accessible via menu Filters - Filter editor
+ _matrix(SPAttr::VALUES, _("This matrix determines a linear transform on color space. Each line affects one of the color components. Each column determines how much of each color component from the input is passed to the output. The last column does not depend on input colors, so can be used to adjust a constant component value.")),
+ _saturation("", 1, 0, 1, 0.1, 0.01, 2, SPAttr::VALUES),
+ _angle("", 0, 0, 360, 0.1, 0.01, 1, SPAttr::VALUES),
+ _label(C_("Label", "None"), Gtk::ALIGN_START),
+ _use_stored(false),
+ _saturation_store(1.0),
+ _angle_store(0)
+ {
+ _matrix.signal_attr_changed().connect(signal_attr_changed().make_slot());
+ _saturation.signal_attr_changed().connect(signal_attr_changed().make_slot());
+ _angle.signal_attr_changed().connect(signal_attr_changed().make_slot());
+ signal_attr_changed().connect(sigc::mem_fun(*this, &ColorMatrixValues::update_store));
+
+ _matrix.show();
+ _saturation.show();
+ _angle.show();
+ _label.show();
+ _label.set_sensitive(false);
+
+ set_shadow_type(Gtk::SHADOW_NONE);
+ }
+
+ void set_from_attribute(SPObject* o) override
+ {
+ std::string values_string;
+ if(is<SPFeColorMatrix>(o)) {
+ auto col = cast<SPFeColorMatrix>(o);
+ remove();
+ switch(col->get_type()) {
+ case COLORMATRIX_SATURATE:
+ add(_saturation);
+ if(_use_stored)
+ _saturation.set_value(_saturation_store);
+ else
+ _saturation.set_from_attribute(o);
+ values_string = Glib::Ascii::dtostr(_saturation.get_value());
+ break;
+ case COLORMATRIX_HUEROTATE:
+ add(_angle);
+ if(_use_stored)
+ _angle.set_value(_angle_store);
+ else
+ _angle.set_from_attribute(o);
+ values_string = Glib::Ascii::dtostr(_angle.get_value());
+ break;
+ case COLORMATRIX_LUMINANCETOALPHA:
+ add(_label);
+ break;
+ case COLORMATRIX_MATRIX:
+ default:
+ add(_matrix);
+ if(_use_stored)
+ _matrix.set_values(_matrix_store);
+ else
+ _matrix.set_from_attribute(o);
+ for (auto v : _matrix.get_values()) {
+ values_string += Glib::Ascii::dtostr(v) + " ";
+ }
+ values_string.pop_back();
+ break;
+ }
+
+ // The filter effects widgets derived from AttrWidget automatically update the
+ // attribute on use. In this case, however, we must also update "values" whenever
+ // "type" is changed.
+ auto repr = o->getRepr();
+ if (values_string.empty()) {
+ repr->removeAttribute("values");
+ } else {
+ repr->setAttribute("values", values_string);
+ }
+
+ _use_stored = true;
+ }
+ }
+
+ Glib::ustring get_as_attribute() const override
+ {
+ const Widget* w = get_child();
+ if(w == &_label)
+ return "";
+ if (auto attrw = dynamic_cast<const AttrWidget *>(w))
+ return attrw->get_as_attribute();
+ g_assert_not_reached();
+ return "";
+ }
+
+ void clear_store()
+ {
+ _use_stored = false;
+ }
+private:
+ void update_store()
+ {
+ const Widget* w = get_child();
+ if(w == &_matrix)
+ _matrix_store = _matrix.get_values();
+ else if(w == &_saturation)
+ _saturation_store = _saturation.get_value();
+ else if(w == &_angle)
+ _angle_store = _angle.get_value();
+ }
+
+ MatrixAttr _matrix;
+ SpinScale _saturation;
+ SpinScale _angle;
+ Gtk::Label _label;
+
+ // Store separate values for the different color modes
+ bool _use_stored;
+ std::vector<double> _matrix_store;
+ double _saturation_store;
+ double _angle_store;
+};
+
+static Inkscape::UI::Dialog::FileOpenDialog * selectFeImageFileInstance = nullptr;
+
+//Displays a chooser for feImage input
+//It may be a filename or the id for an SVG Element
+//described in xlink:href syntax
+class FileOrElementChooser : public Gtk::Box, public AttrWidget
+{
+public:
+ FileOrElementChooser(FilterEffectsDialog& d, const SPAttr a)
+ : AttrWidget(a)
+ , _dialog(d)
+ , Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)
+ {
+ set_spacing(3);
+ pack_start(_entry, true, true);
+ pack_start(_fromFile, false, false);
+ pack_start(_fromSVGElement, false, false);
+
+ _fromFile.set_image_from_icon_name("document-open");
+ _fromFile.set_tooltip_text(_("Choose image file"));
+ _fromFile.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_file));
+
+ _fromSVGElement.set_label(_("SVG Element"));
+ _fromSVGElement.set_tooltip_text(_("Use selected SVG element"));
+ _fromSVGElement.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_svg_element));
+
+ _entry.set_width_chars(1);
+ _entry.signal_changed().connect(signal_attr_changed().make_slot());
+
+ show_all();
+ }
+
+ // Returns the element in xlink:href form.
+ Glib::ustring get_as_attribute() const override
+ {
+ return _entry.get_text();
+ }
+
+
+ void set_from_attribute(SPObject* o) override
+ {
+ const gchar* val = attribute_value(o);
+ if(val) {
+ _entry.set_text(val);
+ } else {
+ _entry.set_text("");
+ }
+ }
+
+private:
+ void select_svg_element() {
+ Inkscape::Selection* sel = _dialog.getDesktop()->getSelection();
+ if (sel->isEmpty()) return;
+ Inkscape::XML::Node* node = sel->xmlNodes().front();
+ if (!node || !node->matchAttributeName("id")) return;
+
+ std::ostringstream xlikhref;
+ xlikhref << "#" << node->attribute("id");
+ _entry.set_text(xlikhref.str());
+ }
+
+ void select_file(){
+
+ //# Get the current directory for finding files
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring open_path;
+ Glib::ustring attr = prefs->getString("/dialogs/open/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 = "";
+
+ //# If no open path, default to our home directory
+ if (open_path.size() < 1)
+ {
+ open_path = g_get_home_dir();
+ open_path.append(G_DIR_SEPARATOR_S);
+ }
+
+ //# Create a dialog if we don't already have one
+ if (!selectFeImageFileInstance) {
+ selectFeImageFileInstance =
+ Inkscape::UI::Dialog::FileOpenDialog::create(
+ *_dialog.getDesktop()->getToplevel(),
+ open_path,
+ Inkscape::UI::Dialog::SVG_TYPES,/*TODO: any image, not just svg*/
+ (char const *)_("Select an image to be used as input."));
+ }
+
+ //# Show the dialog
+ bool const success = selectFeImageFileInstance->show();
+ if (!success)
+ return;
+
+ //# User selected something. Get name and type
+ Glib::ustring fileName = selectFeImageFileInstance->getFilename();
+
+ if (fileName.size() > 0) {
+
+ Glib::ustring newFileName = Glib::filename_to_utf8(fileName);
+
+ if ( newFileName.size() > 0)
+ fileName = newFileName;
+ else
+ g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" );
+
+ open_path = fileName;
+ open_path.append(G_DIR_SEPARATOR_S);
+ prefs->setString("/dialogs/open/path", open_path);
+
+ _entry.set_text(fileName);
+ }
+ return;
+ }
+
+ Gtk::Entry _entry;
+ Gtk::Button _fromFile;
+ Gtk::Button _fromSVGElement;
+ FilterEffectsDialog &_dialog;
+};
+
+class FilterEffectsDialog::Settings
+{
+public:
+ typedef sigc::slot<void (const AttrWidget*)> SetAttrSlot;
+
+ Settings(FilterEffectsDialog& d, Gtk::Box& b, SetAttrSlot slot, const int maxtypes)
+ : _dialog(d), _set_attr_slot(std::move(slot)), _current_type(-1), _max_types(maxtypes)
+ {
+ _groups.resize(_max_types);
+ _attrwidgets.resize(_max_types);
+ _size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+
+ for(int i = 0; i < _max_types; ++i) {
+ _groups[i] = new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 3);
+ b.set_spacing(4);
+ b.pack_start(*_groups[i], Gtk::PACK_SHRINK);
+ }
+ //_current_type = 0; If set to 0 then update_and_show() fails to update properly.
+ }
+
+ ~Settings()
+ {
+ for(int i = 0; i < _max_types; ++i) {
+ delete _groups[i];
+ for(auto & j : _attrwidgets[i])
+ delete j;
+ }
+ }
+
+ void show_current_only() {
+ for (auto& group : _groups) {
+ group->hide();
+ }
+ auto t = get_current_type();
+ if (t >= 0) {
+ _groups[t]->show();
+ }
+ }
+
+ // Show the active settings group and update all the AttrWidgets with new values
+ void show_and_update(const int t, SPObject* ob)
+ {
+ if (t != _current_type) {
+ type(t);
+
+ for (auto& group : _groups) {
+ group->hide();
+ }
+ }
+
+ if (t >= 0) {
+ _groups[t]->show(); // Do not use show_all(), it shows children than should be hidden
+ }
+
+ _dialog.set_attrs_locked(true);
+ for(auto & i : _attrwidgets[_current_type])
+ i->set_from_attribute(ob);
+ _dialog.set_attrs_locked(false);
+ }
+
+ int get_current_type() const
+ {
+ return _current_type;
+ }
+
+ void type(const int t)
+ {
+ _current_type = t;
+ }
+
+ void add_no_params()
+ {
+ Gtk::Label* lbl = Gtk::manage(new Gtk::Label(_("This SVG filter effect does not require any parameters.")));
+ lbl->set_line_wrap();
+ lbl->set_line_wrap_mode(Pango::WRAP_WORD);
+ add_widget(lbl, "");
+ }
+
+ // LightSource
+ LightSourceControl* add_lightsource();
+
+ // Component Transfer Values
+ ComponentTransferValues* add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel);
+
+ // CheckButton
+ CheckButtonAttr* add_checkbutton(bool def, const SPAttr attr, const Glib::ustring& label,
+ const Glib::ustring& tv, const Glib::ustring& fv, char* tip_text = nullptr)
+ {
+ CheckButtonAttr* cb = new CheckButtonAttr(def, label, tv, fv, attr, tip_text);
+ add_widget(cb, "");
+ add_attr_widget(cb);
+ return cb;
+ }
+
+ // ColorButton
+ ColorButton* add_color(unsigned int def, const SPAttr attr, const Glib::ustring& label, char* tip_text = nullptr)
+ {
+ ColorButton* col = new ColorButton(def, attr, tip_text);
+ add_widget(col, label);
+ add_attr_widget(col);
+ return col;
+ }
+
+ // Matrix
+ MatrixAttr* add_matrix(const SPAttr attr, const Glib::ustring& label, char* tip_text)
+ {
+ MatrixAttr* conv = new MatrixAttr(attr, tip_text);
+ add_widget(conv, label);
+ add_attr_widget(conv);
+ return conv;
+ }
+
+ // ColorMatrixValues
+ ColorMatrixValues* add_colormatrixvalues(const Glib::ustring& label)
+ {
+ ColorMatrixValues* cmv = new ColorMatrixValues();
+ add_widget(cmv, label);
+ add_attr_widget(cmv);
+ return cmv;
+ }
+
+ // SpinScale
+ SpinScale* add_spinscale(double def, const SPAttr attr, const Glib::ustring& label,
+ const double lo, const double hi, const double step_inc, const double page_inc, const int digits, char* tip_text = nullptr)
+ {
+ Glib::ustring tip_text2;
+ if (tip_text)
+ tip_text2 = tip_text;
+ SpinScale* spinslider = new SpinScale("", def, lo, hi, step_inc, page_inc, digits, attr, tip_text2);
+ add_widget(spinslider, label);
+ add_attr_widget(spinslider);
+ return spinslider;
+ }
+
+ // DualSpinScale
+ DualSpinScale* add_dualspinscale(const SPAttr attr, const Glib::ustring& label,
+ const double lo, const double hi, const double step_inc,
+ const double climb, const int digits,
+ const Glib::ustring tip_text1 = "",
+ const Glib::ustring tip_text2 = "")
+ {
+ DualSpinScale* dss = new DualSpinScale("", "", lo, lo, hi, step_inc, climb, digits, attr, tip_text1, tip_text2);
+ add_widget(dss, label);
+ add_attr_widget(dss);
+ return dss;
+ }
+
+ // SpinButton
+ SpinButtonAttr* add_spinbutton(double defalt_value, const SPAttr attr, const Glib::ustring& label,
+ const double lo, const double hi, const double step_inc,
+ const double climb, const int digits, char* tip = nullptr)
+ {
+ SpinButtonAttr* sb = new SpinButtonAttr(lo, hi, step_inc, climb, digits, attr, defalt_value, tip);
+ add_widget(sb, label);
+ add_attr_widget(sb);
+ return sb;
+ }
+
+ // DualSpinButton
+ DualSpinButton* add_dualspinbutton(char* defalt_value, const SPAttr attr, const Glib::ustring& label,
+ const double lo, const double hi, const double step_inc,
+ const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr)
+ {
+ DualSpinButton* dsb = new DualSpinButton(defalt_value, lo, hi, step_inc, climb, digits, attr, tip1, tip2);
+ add_widget(dsb, label);
+ add_attr_widget(dsb);
+ return dsb;
+ }
+
+ // MultiSpinButton
+ MultiSpinButton* add_multispinbutton(double def1, double def2, const SPAttr attr1, const SPAttr attr2,
+ const Glib::ustring& label, const double lo, const double hi,
+ const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr)
+ {
+ std::vector<SPAttr> attrs;
+ attrs.push_back(attr1);
+ attrs.push_back(attr2);
+
+ std::vector<double> default_values;
+ default_values.push_back(def1);
+ default_values.push_back(def2);
+
+ std::vector<char*> tips;
+ tips.push_back(tip1);
+ tips.push_back(tip2);
+
+ MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs, default_values, tips);
+ add_widget(msb, label);
+ for(auto & i : msb->get_spinbuttons())
+ add_attr_widget(i);
+ return msb;
+ }
+ MultiSpinButton* add_multispinbutton(double def1, double def2, double def3, const SPAttr attr1, const SPAttr attr2,
+ const SPAttr attr3, const Glib::ustring& label, const double lo,
+ const double hi, const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr, char* tip3 = nullptr)
+ {
+ std::vector<SPAttr> attrs;
+ attrs.push_back(attr1);
+ attrs.push_back(attr2);
+ attrs.push_back(attr3);
+
+ std::vector<double> default_values;
+ default_values.push_back(def1);
+ default_values.push_back(def2);
+ default_values.push_back(def3);
+
+ std::vector<char*> tips;
+ tips.push_back(tip1);
+ tips.push_back(tip2);
+ tips.push_back(tip3);
+
+ MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs, default_values, tips);
+ add_widget(msb, label);
+ for(auto & i : msb->get_spinbuttons())
+ add_attr_widget(i);
+ return msb;
+ }
+
+ // FileOrElementChooser
+ FileOrElementChooser* add_fileorelement(const SPAttr attr, const Glib::ustring& label)
+ {
+ FileOrElementChooser* foech = new FileOrElementChooser(_dialog, attr);
+ add_widget(foech, label);
+ add_attr_widget(foech);
+ return foech;
+ }
+
+ // ComboBoxEnum
+ template<typename T> ComboBoxEnum<T>* add_combo(T default_value, const SPAttr attr,
+ const Glib::ustring& label,
+ const Util::EnumDataConverter<T>& conv, char* tip_text = nullptr)
+ {
+ ComboWithTooltip<T>* combo = new ComboWithTooltip<T>(default_value, conv, attr, tip_text);
+ add_widget(combo, label);
+ add_attr_widget(combo->get_attrwidget());
+ return combo->get_attrwidget();
+ }
+
+ // Entry
+ EntryAttr* add_entry(const SPAttr attr,
+ const Glib::ustring& label,
+ char* tip_text = nullptr)
+ {
+ EntryAttr* entry = new EntryAttr(attr, tip_text);
+ add_widget(entry, label);
+ add_attr_widget(entry);
+ return entry;
+ }
+
+ Glib::RefPtr<Gtk::SizeGroup> _size_group;
+private:
+ void add_attr_widget(AttrWidget* a)
+ {
+ _attrwidgets[_current_type].push_back(a);
+ a->signal_attr_changed().connect(sigc::bind(_set_attr_slot, a));
+ }
+
+ /* Adds a new settings widget using the specified label. The label will be formatted with a colon
+ and all widgets within the setting group are aligned automatically. */
+ void add_widget(Gtk::Widget* w, const Glib::ustring& label)
+ {
+ Gtk::Box *hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ hb->set_spacing(6);
+
+ if (label != "") {
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(label));
+ lbl->set_xalign(0.0);
+ hb->pack_start(*lbl, Gtk::PACK_SHRINK);
+ _size_group->add_widget(*lbl);
+ }
+
+ hb->pack_start(*w, Gtk::PACK_EXPAND_WIDGET);
+ _groups[_current_type]->pack_start(*hb, Gtk::PACK_EXPAND_WIDGET);
+ hb->show_all();
+ }
+
+ std::vector<Gtk::Box*> _groups;
+ FilterEffectsDialog& _dialog;
+ SetAttrSlot _set_attr_slot;
+ std::vector<std::vector< AttrWidget*> > _attrwidgets;
+ int _current_type, _max_types;
+};
+
+// Displays sliders and/or tables for feComponentTransfer
+class FilterEffectsDialog::ComponentTransferValues : public Gtk::Frame, public AttrWidget
+{
+public:
+ ComponentTransferValues(FilterEffectsDialog& d, SPFeFuncNode::Channel channel)
+ : AttrWidget(SPAttr::INVALID),
+ _dialog(d),
+ _settings(d, _box, sigc::mem_fun(*this, &ComponentTransferValues::set_func_attr), COMPONENTTRANSFER_TYPE_ERROR),
+ _type(ComponentTransferTypeConverter, SPAttr::TYPE, false),
+ _channel(channel),
+ _funcNode(nullptr),
+ _box(Gtk::ORIENTATION_VERTICAL)
+ {
+ set_shadow_type(Gtk::SHADOW_NONE);
+ add(_box);
+ _box.add(_type);
+ _box.reorder_child(_type, 0);
+ _type.signal_changed().connect(sigc::mem_fun(*this, &ComponentTransferValues::on_type_changed));
+
+ _settings.type(COMPONENTTRANSFER_TYPE_LINEAR);
+ _settings.add_spinscale(1, SPAttr::SLOPE, _("Slope"), -10, 10, 0.1, 0.01, 2);
+ _settings.add_spinscale(0, SPAttr::INTERCEPT, _("Intercept"), -10, 10, 0.1, 0.01, 2);
+
+ _settings.type(COMPONENTTRANSFER_TYPE_GAMMA);
+ _settings.add_spinscale(1, SPAttr::AMPLITUDE, _("Amplitude"), 0, 10, 0.1, 0.01, 2);
+ _settings.add_spinscale(1, SPAttr::EXPONENT, _("Exponent"), 0, 10, 0.1, 0.01, 2);
+ _settings.add_spinscale(0, SPAttr::OFFSET, _("Offset"), -10, 10, 0.1, 0.01, 2);
+
+ _settings.type(COMPONENTTRANSFER_TYPE_TABLE);
+ _settings.add_entry(SPAttr::TABLEVALUES, _("Values"), _("List of stops with interpolated output"));
+
+ _settings.type(COMPONENTTRANSFER_TYPE_DISCRETE);
+ _settings.add_entry(SPAttr::TABLEVALUES, _("Values"), _("List of discrete values for a step function"));
+
+ //_settings.type(COMPONENTTRANSFER_TYPE_IDENTITY);
+ _settings.type(-1); // Force update_and_show() to show/hide windows correctly
+ }
+
+ // FuncNode can be in any order so we must search to find correct one.
+ SPFeFuncNode* find_node(SPFeComponentTransfer* ct)
+ {
+ SPFeFuncNode* funcNode = nullptr;
+ bool found = false;
+ for(auto& node: ct->children) {
+ funcNode = cast<SPFeFuncNode>(&node);
+ if( funcNode->channel == _channel ) {
+ found = true;
+ break;
+ }
+ }
+ if( !found )
+ funcNode = nullptr;
+
+ return funcNode;
+ }
+
+ void set_func_attr(const AttrWidget* input)
+ {
+ _dialog.set_attr( _funcNode, input->get_attribute(), input->get_as_attribute().c_str());
+ }
+
+ // Set new type and update widget visibility
+ void set_from_attribute(SPObject* o) override
+ {
+ // See componenttransfer.cpp
+ if(is<SPFeComponentTransfer>(o)) {
+ auto ct = cast<SPFeComponentTransfer>(o);
+
+ _funcNode = find_node(ct);
+ if( _funcNode ) {
+ _type.set_from_attribute( _funcNode );
+ } else {
+ // Create <funcNode>
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if(prim) {
+ Inkscape::XML::Document *xml_doc = prim->document->getReprDoc();
+ Inkscape::XML::Node *repr = nullptr;
+ switch(_channel) {
+ case SPFeFuncNode::R:
+ repr = xml_doc->createElement("svg:feFuncR");
+ break;
+ case SPFeFuncNode::G:
+ repr = xml_doc->createElement("svg:feFuncG");
+ break;
+ case SPFeFuncNode::B:
+ repr = xml_doc->createElement("svg:feFuncB");
+ break;
+ case SPFeFuncNode::A:
+ repr = xml_doc->createElement("svg:feFuncA");
+ break;
+ }
+
+ //XML Tree being used directly here while it shouldn't be.
+ prim->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // Now we should find it!
+ _funcNode = find_node(ct);
+ if( _funcNode ) {
+ _funcNode->setAttribute( "type", "identity" );
+ } else {
+ //std::cerr << "ERROR ERROR: feFuncX not found!" << std::endl;
+ }
+ }
+ }
+
+ update();
+ }
+ }
+
+private:
+ void on_type_changed()
+ {
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if(prim) {
+
+ _funcNode->setAttributeOrRemoveIfEmpty("type", _type.get_as_attribute());
+
+ SPFilter* filter = _dialog._filter_modifier.get_selected_filter();
+ filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ DocumentUndo::done(prim->document, _("New transfer function type"), INKSCAPE_ICON("dialog-filters"));
+ update();
+ }
+ }
+
+ void update()
+ {
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if(prim && _funcNode) {
+ _settings.show_and_update(_type.get_active_data()->id, _funcNode);
+ }
+ }
+
+public:
+ Glib::ustring get_as_attribute() const override
+ {
+ return "";
+ }
+
+ FilterEffectsDialog& _dialog;
+ Gtk::Box _box;
+ Settings _settings;
+ ComboBoxEnum<FilterComponentTransferType> _type;
+ SPFeFuncNode::Channel _channel; // RGBA
+ SPFeFuncNode* _funcNode;
+};
+
+// Settings for the three light source objects
+class FilterEffectsDialog::LightSourceControl : public AttrWidget
+{
+public:
+ LightSourceControl(FilterEffectsDialog& d)
+ : AttrWidget(SPAttr::INVALID),
+ _dialog(d),
+ _settings(d, _box, sigc::mem_fun(_dialog, &FilterEffectsDialog::set_child_attr_direct), LIGHT_ENDSOURCE),
+ _light_label(_("Light Source:")),
+ _light_source(LightSourceConverter),
+ _locked(false),
+ _box(Gtk::ORIENTATION_VERTICAL),
+ _light_box(Gtk::ORIENTATION_HORIZONTAL)
+ {
+ _light_label.set_xalign(0.0);
+ _settings._size_group->add_widget(_light_label);
+ _light_box.pack_start(_light_label, Gtk::PACK_SHRINK);
+ _light_box.pack_start(_light_source, Gtk::PACK_EXPAND_WIDGET);
+ _light_box.show_all();
+ _light_box.set_spacing(6);
+
+ _box.add(_light_box);
+ _box.reorder_child(_light_box, 0);
+ _light_source.signal_changed().connect(sigc::mem_fun(*this, &LightSourceControl::on_source_changed));
+
+ // FIXME: these range values are complete crap
+
+ _settings.type(LIGHT_DISTANT);
+ _settings.add_spinscale(0, SPAttr::AZIMUTH, _("Azimuth:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the XY plane, in degrees"));
+ _settings.add_spinscale(0, SPAttr::ELEVATION, _("Elevation:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the YZ plane, in degrees"));
+
+ _settings.type(LIGHT_POINT);
+ _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SPAttr::X, SPAttr::Y, SPAttr::Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate"));
+
+ _settings.type(LIGHT_SPOT);
+ _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SPAttr::X, SPAttr::Y, SPAttr::Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate"));
+ _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0,
+ SPAttr::POINTSATX, SPAttr::POINTSATY, SPAttr::POINTSATZ,
+ _("Points at:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate"));
+ _settings.add_spinscale(1, SPAttr::SPECULAREXPONENT, _("Specular Exponent:"), 0.1, 100, 0.1, 1, 1, _("Exponent value controlling the focus for the light source"));
+ //TODO: here I have used 100 degrees as default value. But spec says that if not specified, no limiting cone is applied. So, there should be a way for the user to set a "no limiting cone" option.
+ _settings.add_spinscale(100, SPAttr::LIMITINGCONEANGLE, _("Cone Angle:"), 0, 180, 1, 5, 0, _("This is the angle between the spot light axis (i.e. the axis between the light source and the point to which it is pointing at) and the spot light cone. No light is projected outside this cone."));
+
+ _settings.type(-1); // Force update_and_show() to show/hide windows correctly
+ }
+
+ Gtk::Box& get_box()
+ {
+ return _box;
+ }
+protected:
+ Glib::ustring get_as_attribute() const override
+ {
+ return "";
+ }
+ void set_from_attribute(SPObject* o) override
+ {
+ if(_locked)
+ return;
+
+ _locked = true;
+
+ SPObject* child = o->firstChild();
+
+ if(is<SPFeDistantLight>(child))
+ _light_source.set_active(0);
+ else if(is<SPFePointLight>(child))
+ _light_source.set_active(1);
+ else if(is<SPFeSpotLight>(child))
+ _light_source.set_active(2);
+ else
+ _light_source.set_active(-1);
+
+ update();
+
+ _locked = false;
+ }
+private:
+ void on_source_changed()
+ {
+ if(_locked)
+ return;
+
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if(prim) {
+ _locked = true;
+
+ SPObject* child = prim->firstChild();
+ const int ls = _light_source.get_active_row_number();
+ // Check if the light source type has changed
+ if(!(ls == -1 && !child) &&
+ !(ls == 0 && is<SPFeDistantLight>(child)) &&
+ !(ls == 1 && is<SPFePointLight>(child)) &&
+ !(ls == 2 && is<SPFeSpotLight>(child))) {
+ if(child)
+ //XML Tree being used directly here while it shouldn't be.
+ sp_repr_unparent(child->getRepr());
+
+ if(ls != -1) {
+ Inkscape::XML::Document *xml_doc = prim->document->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement(_light_source.get_active_data()->key.c_str());
+ //XML Tree being used directly here while it shouldn't be.
+ prim->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+ }
+
+ DocumentUndo::done(prim->document, _("New light source"), INKSCAPE_ICON("dialog-filters"));
+ update();
+ }
+
+ _locked = false;
+ }
+ }
+
+ void update()
+ {
+ _box.show();
+
+ SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
+ if (prim && prim->firstChild()) {
+ _settings.show_and_update(_light_source.get_active_data()->id, prim->firstChild());
+ }
+ else {
+ _settings.show_current_only();
+ }
+ }
+
+ FilterEffectsDialog& _dialog;
+ Gtk::Box _box;
+ Settings _settings;
+ Gtk::Box _light_box;
+ Gtk::Label _light_label;
+ ComboBoxEnum<LightSource> _light_source;
+ bool _locked;
+};
+
+ // ComponentTransferValues
+FilterEffectsDialog::ComponentTransferValues* FilterEffectsDialog::Settings::add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel)
+ {
+ ComponentTransferValues* ct = new ComponentTransferValues(_dialog, channel);
+ add_widget(ct, label);
+ add_attr_widget(ct);
+ ct->set_margin_top(4);
+ ct->set_margin_bottom(4);
+ return ct;
+ }
+
+
+FilterEffectsDialog::LightSourceControl* FilterEffectsDialog::Settings::add_lightsource()
+{
+ LightSourceControl* ls = new LightSourceControl(_dialog);
+ add_attr_widget(ls);
+ add_widget(&ls->get_box(), "");
+ return ls;
+}
+
+static Gtk::Menu * create_popup_menu(Gtk::Widget& parent,
+ sigc::slot<void ()> dup,
+ sigc::slot<void ()> rem)
+{
+ auto menu = Gtk::manage(new Gtk::Menu);
+
+ Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Duplicate"),true));
+ mi->signal_activate().connect(dup);
+ mi->show();
+ menu->append(*mi);
+
+ mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ menu->append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ menu->accelerate(parent);
+
+ return menu;
+}
+
+/*** FilterModifier ***/
+FilterEffectsDialog::FilterModifier::FilterModifier(FilterEffectsDialog& d, Glib::RefPtr<Gtk::Builder> builder)
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL),
+ _builder(std::move(builder)),
+ _list(get_widget<Gtk::TreeView>(_builder, "filter-list")),
+ _dialog(d),
+ _add(get_widget<Gtk::Button>(_builder, "btn-new")),
+ _dup(get_widget<Gtk::Button>(_builder, "btn-dup")),
+ _del(get_widget<Gtk::Button>(_builder, "btn-del")),
+ _select(get_widget<Gtk::Button>(_builder, "btn-select")),
+ _menu(get_widget<Gtk::Menu>(_builder, "filters-ctx-menu")),
+ _observer(new Inkscape::XML::SignalObserver)
+{
+ _filters_model = Gtk::ListStore::create(_columns);
+ _list.set_model(_filters_model);
+ _cell_toggle.set_radio();
+ _cell_toggle.set_active(true);
+ const int selcol = _list.append_column("", _cell_toggle);
+ Gtk::TreeViewColumn* col = _list.get_column(selcol - 1);
+ if(col)
+ col->add_attribute(_cell_toggle.property_active(), _columns.sel);
+ _list.append_column_editable(_("_Filter"), _columns.label);
+ static_cast<Gtk::CellRendererText*>(_list.get_column(1)->get_first_cell())->
+ signal_edited().connect(sigc::mem_fun(*this, &FilterEffectsDialog::FilterModifier::on_name_edited));
+
+ _list.append_column(_("Used"), _columns.count);
+ _list.get_column(2)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE);
+ _list.get_column(2)->set_expand(false);
+ _list.get_column(2)->set_reorderable(true);
+
+ _list.get_column(1)->set_resizable(true);
+ _list.get_column(1)->set_sizing(Gtk::TREE_VIEW_COLUMN_FIXED);
+ _list.get_column(1)->set_expand(true);
+
+ _list.set_reorderable(false);
+ _list.enable_model_drag_dest(Gdk::ACTION_MOVE);
+
+ _list.signal_drag_drop().connect( sigc::mem_fun(*this, &FilterModifier::on_filter_move), false );
+
+ _add.signal_clicked().connect([=]() { add_filter(); });
+ _del.signal_clicked().connect([=]() { remove_filter(); });
+ _dup.signal_clicked().connect([=]() { duplicate_filter(); });
+ _select.signal_clicked().connect([=]() { select_filter_elements(); });
+
+ _cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &FilterModifier::on_selection_toggled));
+ _list.signal_button_release_event().connect_notify(
+ sigc::mem_fun(*this, &FilterModifier::filter_list_button_release));
+
+ // connect handlers to context menu items
+ auto&& items = _menu.get_children();
+ auto funcs = { &FilterModifier::duplicate_filter, &FilterModifier::remove_filter, &FilterModifier::rename_filter, &FilterModifier::select_filter_elements };
+ int index = 0;
+ for (auto fn : funcs) {
+ static_cast<Gtk::MenuItem*>(items.at(index++))->signal_activate().connect([=](){
+ (this->*fn)();
+ });
+ }
+
+ _list.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FilterModifier::on_filter_selection_changed));
+ _observer->signal_changed().connect(signal_filter_changed().make_slot());
+}
+
+// Update each filter's sel property based on the current object selection;
+// If the filter is not used by any selected object, sel = 0,
+// otherwise sel is set to the total number of filters in use by selected objects
+// If only one filter is in use, it is selected
+void FilterEffectsDialog::FilterModifier::update_selection(Selection *sel)
+{
+ if (!sel) {
+ return;
+ }
+
+ std::set<SPFilter*> used;
+
+ for (auto obj : sel->items()) {
+ SPStyle *style = obj->style;
+ if (!style || !obj) {
+ continue;
+ }
+
+ if (style->filter.set && style->getFilter()) {
+ //TODO: why is this needed?
+ obj->bbox_valid = FALSE;
+ used.insert(style->getFilter());
+ }
+ }
+
+ const int size = used.size();
+
+ for (auto&& item : _filters_model->children()) {
+ if (used.count(item[_columns.filter])) {
+ // If only one filter is in use by the selection, select it
+ if (size == 1) {
+ _list.get_selection()->select(item);
+ }
+ item[_columns.sel] = size;
+ } else {
+ item[_columns.sel] = 0;
+ }
+ }
+ update_counts();
+ _signal_filters_updated.emit();
+}
+
+void FilterEffectsDialog::FilterModifier::on_filter_selection_changed()
+{
+ _observer->set(get_selected_filter());
+ signal_filter_changed()();
+}
+
+void FilterEffectsDialog::FilterModifier::on_name_edited(const Glib::ustring& path, const Glib::ustring& text)
+{
+ if (auto iter = _filters_model->get_iter(path)) {
+ SPFilter* filter = (*iter)[_columns.filter];
+ filter->setLabel(text.c_str());
+ DocumentUndo::done(filter->document, _("Rename filter"), INKSCAPE_ICON("dialog-filters"));
+ if (iter) {
+ (*iter)[_columns.label] = text;
+ }
+ }
+}
+
+bool FilterEffectsDialog::FilterModifier::on_filter_move(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int /*x*/, int /*y*/, guint /*time*/) {
+
+//const Gtk::TreeModel::Path& /*path*/) {
+/* The code below is bugged. Use of "object->getRepr()->setPosition(0)" is dangerous!
+ Writing back the reordered list to XML (reordering XML nodes) should be implemented differently.
+ Note that the dialog does also not update its list of filters when the order is manually changed
+ using the XML dialog
+ for(Gtk::TreeModel::iterator i = _model->children().begin(); i != _model->children().end(); ++i) {
+ SPObject* object = (*i)[_columns.filter];
+ if(object && object->getRepr()) ;
+ object->getRepr()->setPosition(0);
+ }
+*/
+ return false;
+}
+
+void FilterEffectsDialog::FilterModifier::on_selection_toggled(const Glib::ustring& path)
+{
+ Gtk::TreeIter iter = _filters_model->get_iter(path);
+ selection_toggled(iter, false);
+}
+
+void FilterEffectsDialog::FilterModifier::selection_toggled(Gtk::TreeIter iter, bool toggle) {
+ if (!iter) return;
+
+ SPDesktop *desktop = _dialog.getDesktop();
+ SPDocument *doc = desktop->getDocument();
+ Inkscape::Selection *sel = desktop->getSelection();
+ SPFilter* filter = (*iter)[_columns.filter];
+
+ /* If this filter is the only one used in the selection, unset it */
+ if ((*iter)[_columns.sel] == 1 && toggle) {
+ filter = nullptr;
+ }
+
+ for (auto item : sel->items()) {
+ SPStyle *style = item->style;
+ g_assert(style != nullptr);
+
+ if (filter && filter->valid_for(item)) {
+ sp_style_set_property_url(item, "filter", filter, false);
+ } else {
+ ::remove_filter(item, false);
+ }
+
+ item->requestDisplayUpdate((SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG ));
+ }
+
+ update_selection(sel);
+ DocumentUndo::done(doc, _("Apply filter"), INKSCAPE_ICON("dialog-filters"));
+}
+
+void FilterEffectsDialog::FilterModifier::update_counts()
+{
+ for (auto&& item : _filters_model->children()) {
+ SPFilter* f = item[_columns.filter];
+ item[_columns.count] = f->getRefCount();
+ }
+}
+
+static Glib::ustring get_filter_name(SPFilter* filter) {
+ if (!filter) return Glib::ustring();
+
+ if (auto label = filter->label()) {
+ return label;
+ }
+ else if (auto id = filter->getId()) {
+ return id;
+ }
+ else {
+ return _("filter");
+ }
+}
+
+/* Add all filters in the document to the combobox.
+ Keeps the same selection if possible, otherwise selects the first element */
+void FilterEffectsDialog::FilterModifier::update_filters()
+{
+ auto document = _dialog.getDocument();
+ if (!document) return; // no document at shut down
+
+ std::vector<SPObject *> filters = document->getResourceList("filter");
+
+ _filters_model->clear();
+ SPFilter* first = nullptr;
+
+ for (auto filter : filters) {
+ Gtk::TreeModel::Row row = *_filters_model->append();
+ auto f = cast<SPFilter>(filter);
+ row[_columns.filter] = f;
+ row[_columns.label] = get_filter_name(f);
+ if (!first) {
+ first = f;
+ }
+ }
+
+ update_selection(_dialog.getSelection());
+ if (first) {
+ select_filter(first);
+ }
+ _dialog.update_filter_general_settings_view();
+ _dialog.update_settings_view();
+}
+
+bool FilterEffectsDialog::FilterModifier::is_selected_filter_active() {
+ if (auto&& sel = _list.get_selection()) {
+ if (Gtk::TreeModel::iterator it = sel->get_selected()) {
+ return (*it)[_columns.sel] > 0;
+ }
+ }
+
+ return false;
+}
+
+bool FilterEffectsDialog::FilterModifier::filters_present() const {
+ return !_filters_model->children().empty();
+}
+
+void FilterEffectsDialog::FilterModifier::toggle_current_filter() {
+ if (auto&& sel = _list.get_selection()) {
+ selection_toggled(sel->get_selected(), true);
+ }
+}
+
+SPFilter* FilterEffectsDialog::FilterModifier::get_selected_filter()
+{
+ if(_list.get_selection()) {
+ Gtk::TreeModel::iterator i = _list.get_selection()->get_selected();
+
+ if(i)
+ return (*i)[_columns.filter];
+ }
+
+ return nullptr;
+}
+
+void FilterEffectsDialog::FilterModifier::select_filter(const SPFilter* filter)
+{
+ if (!filter) return;
+
+ for (auto&& item : _filters_model->children()) {
+ if (item[_columns.filter] == filter) {
+ _list.get_selection()->select(item);
+ break;
+ }
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::filter_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ const bool sensitive = get_selected_filter() != nullptr;
+ auto items = _menu.get_children();
+ items[0]->set_sensitive(sensitive);
+ items[1]->set_sensitive(sensitive);
+ items[3]->set_sensitive(sensitive);
+
+ _menu.popup_at_pointer(reinterpret_cast<GdkEvent*>(event));
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::add_filter()
+{
+ SPDocument* doc = _dialog.getDocument();
+ SPFilter* filter = new_filter(doc);
+
+ const int count = _filters_model->children().size();
+ std::ostringstream os;
+ os << _("filter") << count;
+ filter->setLabel(os.str().c_str());
+
+ update_filters();
+
+ select_filter(filter);
+
+ DocumentUndo::done(doc, _("Add filter"), INKSCAPE_ICON("dialog-filters"));
+}
+
+void FilterEffectsDialog::FilterModifier::remove_filter()
+{
+ SPFilter *filter = get_selected_filter();
+
+ if(filter) {
+ auto desktop = _dialog.getDesktop();
+ SPDocument* doc = filter->document;
+
+ // Delete all references to this filter
+ auto all = get_all_items(desktop->layerManager().currentRoot(), desktop, false, false, true);
+ for (auto item : all) {
+ if (!item) {
+ continue;
+ }
+ if (!item->style) {
+ continue;
+ }
+
+ const SPIFilter *ifilter = &(item->style->filter);
+ if (ifilter && ifilter->href) {
+ const SPObject *obj = ifilter->href->getObject();
+ if (obj && obj == (SPObject *)filter) {
+ ::remove_filter(item, false);
+ }
+ }
+ }
+
+ //XML Tree being used directly here while it shouldn't be.
+ sp_repr_unparent(filter->getRepr());
+
+ DocumentUndo::done(doc, _("Remove filter"), INKSCAPE_ICON("dialog-filters"));
+
+ update_filters();
+
+ // select first filter to avoid empty dialog after filter deletion
+ const auto& filters = _filters_model->children();
+ if (!filters.empty()) {
+ _list.get_selection()->select(filters[0]);
+ }
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::duplicate_filter()
+{
+ SPFilter* filter = get_selected_filter();
+
+ if (filter) {
+ Inkscape::XML::Node *repr = filter->getRepr();
+ Inkscape::XML::Node *parent = repr->parent();
+ repr = repr->duplicate(repr->document());
+ parent->appendChild(repr);
+
+ DocumentUndo::done(filter->document, _("Duplicate filter"), INKSCAPE_ICON("dialog-filters"));
+
+ update_filters();
+ }
+}
+
+void FilterEffectsDialog::FilterModifier::rename_filter()
+{
+ _list.set_cursor(_filters_model->get_path(_list.get_selection()->get_selected()), *_list.get_column(1), true);
+}
+
+void FilterEffectsDialog::FilterModifier::select_filter_elements()
+{
+ SPFilter *filter = get_selected_filter();
+ auto desktop = _dialog.getDesktop();
+
+ if(!filter)
+ return;
+
+ std::vector<SPItem*> items;
+ auto all = get_all_items(desktop->layerManager().currentRoot(), desktop, false, false, true);
+ for(SPItem *item: all) {
+ if (!item->style) {
+ continue;
+ }
+
+ SPIFilter const &ifilter = item->style->filter;
+ if (ifilter.href) {
+ const SPObject *obj = ifilter.href->getObject();
+ if (obj && obj == (SPObject *)filter) {
+ items.push_back(item);
+ }
+ }
+ }
+ desktop->getSelection()->setList(items);
+}
+
+FilterEffectsDialog::CellRendererConnection::CellRendererConnection()
+ : Glib::ObjectBase(typeid(CellRendererConnection))
+ , _primitive(*this, "primitive", nullptr)
+{}
+
+Glib::PropertyProxy<void*> FilterEffectsDialog::CellRendererConnection::property_primitive()
+{
+ return _primitive.get_proxy();
+}
+
+void FilterEffectsDialog::CellRendererConnection::get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& minimum_width,
+ int& natural_width) const
+{
+ auto& primlist = dynamic_cast<PrimitiveList&>(widget);
+ int count = primlist.get_inputs_count();
+ minimum_width = natural_width = size_w * primlist.primitive_count() + primlist.get_input_type_width() * count;
+}
+
+void FilterEffectsDialog::CellRendererConnection::get_preferred_width_for_height_vfunc(Gtk::Widget& widget,
+ int /* height */,
+ int& minimum_width,
+ int& natural_width) const
+{
+ get_preferred_width(widget, minimum_width, natural_width);
+}
+
+void FilterEffectsDialog::CellRendererConnection::get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& minimum_height,
+ int& natural_height) const
+{
+ // Scale the height depending on the number of inputs, unless it's
+ // the first primitive, in which case there are no connections.
+ auto prim = reinterpret_cast<SPFilterPrimitive*>(_primitive.get_value());
+ minimum_height = natural_height = size_h * input_count(prim);
+}
+
+void FilterEffectsDialog::CellRendererConnection::get_preferred_height_for_width_vfunc(Gtk::Widget& widget,
+ int /* width */,
+ int& minimum_height,
+ int& natural_height) const
+{
+ get_preferred_height(widget, minimum_height, natural_height);
+}
+
+/*** PrimitiveList ***/
+FilterEffectsDialog::PrimitiveList::PrimitiveList(FilterEffectsDialog& d)
+ : _dialog(d),
+ _in_drag(0),
+ _observer(new Inkscape::XML::SignalObserver)
+{
+ _inputs_count = FPInputConverter._length;
+
+ signal_draw().connect(sigc::mem_fun(*this, &PrimitiveList::on_draw_signal));
+
+ add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK);
+
+ _model = Gtk::ListStore::create(_columns);
+
+ set_reorderable(true);
+
+ set_model(_model);
+ append_column(_("_Effect"), _columns.type);
+ get_column(0)->set_resizable(true);
+ set_headers_visible(false);
+
+ _observer->signal_changed().connect(signal_primitive_changed().make_slot());
+ get_selection()->signal_changed().connect(sigc::mem_fun(*this, &PrimitiveList::on_primitive_selection_changed));
+ signal_primitive_changed().connect(sigc::mem_fun(*this, &PrimitiveList::queue_draw));
+
+ init_text();
+
+ int cols_count = append_column(_("Connections"), _connection_cell);
+ Gtk::TreeViewColumn* col = get_column(cols_count - 1);
+ if(col)
+ col->add_attribute(_connection_cell.property_primitive(), _columns.primitive);
+}
+
+// Sets up a vertical Pango context/layout, and returns the largest
+// width needed to render the FilterPrimitiveInput labels.
+void FilterEffectsDialog::PrimitiveList::init_text()
+{
+ // Set up a vertical context+layout
+ Glib::RefPtr<Pango::Context> context = create_pango_context();
+ const Pango::Matrix matrix = {0, -1, 1, 0, 0, 0};
+ context->set_matrix(matrix);
+ _vertical_layout = Pango::Layout::create(context);
+
+ // Store the maximum height and width of the an input type label
+ // for later use in drawing and measuring.
+ _input_type_height = _input_type_width = 0;
+ for(unsigned int i = 0; i < FPInputConverter._length; ++i) {
+ _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
+ int fontw, fonth;
+ _vertical_layout->get_pixel_size(fontw, fonth);
+ if(fonth > _input_type_width)
+ _input_type_width = fonth;
+ if (fontw > _input_type_height)
+ _input_type_height = fontw;
+ }
+}
+
+sigc::signal<void ()>& FilterEffectsDialog::PrimitiveList::signal_primitive_changed()
+{
+ return _signal_primitive_changed;
+}
+
+void FilterEffectsDialog::PrimitiveList::on_primitive_selection_changed()
+{
+ _observer->set(get_selected());
+ signal_primitive_changed()();
+ _dialog._color_matrix_values->clear_store();
+}
+
+/* Add all filter primitives in the current to the list.
+ Keeps the same selection if possible, otherwise selects the first element */
+void FilterEffectsDialog::PrimitiveList::update()
+{
+ SPFilter* f = _dialog._filter_modifier.get_selected_filter();
+ const SPFilterPrimitive* active_prim = get_selected();
+ _model->clear();
+
+ if(f) {
+ bool active_found = false;
+ _dialog._primitive_box->set_sensitive(true);
+ _dialog.update_filter_general_settings_view();
+ for(auto& prim_obj: f->children) {
+ auto prim = cast<SPFilterPrimitive>(&prim_obj);
+ if(!prim) {
+ break;
+ }
+ Gtk::TreeModel::Row row = *_model->append();
+ row[_columns.primitive] = prim;
+
+ //XML Tree being used directly here while it shouldn't be.
+ row[_columns.type_id] = FPConverter.get_id_from_key(prim->getRepr()->name());
+ row[_columns.type] = _(FPConverter.get_label(row[_columns.type_id]).c_str());
+
+ if (prim->getId()) {
+ row[_columns.id] = Glib::ustring(prim->getId());
+ }
+
+ if(prim == active_prim) {
+ get_selection()->select(row);
+ active_found = true;
+ }
+ }
+
+ if(!active_found && _model->children().begin())
+ get_selection()->select(_model->children().begin());
+
+ columns_autosize();
+
+ int width, height;
+ get_size_request(width, height);
+ if (height == -1) {
+ // Need to account for the height of the input type text (rotated text) as well as the
+ // column headers. Input type text height determined in init_text() by measuring longest
+ // string. Column header height determined by mapping y coordinate of visible
+ // rectangle to widget coordinates.
+ Gdk::Rectangle vis;
+ int vis_x, vis_y;
+ get_visible_rect(vis);
+ convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y);
+ set_size_request(width, _input_type_height + 2 + vis_y);
+ }
+ }
+ else {
+ _dialog._primitive_box->set_sensitive(false);
+ set_size_request(-1, -1);
+ }
+}
+
+void FilterEffectsDialog::PrimitiveList::set_menu(Gtk::Widget& parent,
+ sigc::slot<void ()> dup,
+ sigc::slot<void ()> rem)
+{
+ _primitive_menu = create_popup_menu(parent, dup, rem);
+}
+
+SPFilterPrimitive* FilterEffectsDialog::PrimitiveList::get_selected()
+{
+ if(_dialog._filter_modifier.get_selected_filter()) {
+ Gtk::TreeModel::iterator i = get_selection()->get_selected();
+ if(i)
+ return (*i)[_columns.primitive];
+ }
+
+ return nullptr;
+}
+
+void FilterEffectsDialog::PrimitiveList::select(SPFilterPrimitive* prim)
+{
+ for (auto&& item : _model->children()) {
+ if (item[_columns.primitive] == prim) {
+ get_selection()->select(item);
+ break;
+ }
+ }
+}
+
+void FilterEffectsDialog::PrimitiveList::remove_selected()
+{
+ if (SPFilterPrimitive* prim = get_selected()) {
+ _observer->set(nullptr);
+ _model->erase(get_selection()->get_selected());
+
+ //XML Tree being used directly here while it shouldn't be.
+ sp_repr_unparent(prim->getRepr());
+
+ DocumentUndo::done(_dialog.getDocument(), _("Remove filter primitive"), INKSCAPE_ICON("dialog-filters"));
+
+ update();
+ }
+}
+
+void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
+ const std::vector<Gdk::Point>& points,
+ Gdk::RGBA fill, Gdk::RGBA stroke);
+
+bool FilterEffectsDialog::PrimitiveList::on_draw_signal(const Cairo::RefPtr<Cairo::Context> & cr)
+{
+ cr->set_line_width(1.0);
+ // In GTK+ 3, the draw function receives the widget window, not the
+ // bin_window (i.e., just the area under the column headers). We
+ // therefore translate the origin of our coordinate system to account for this
+ int x_origin, y_origin;
+ convert_bin_window_to_widget_coords(0,0,x_origin,y_origin);
+ cr->translate(x_origin, y_origin);
+
+ auto sc = get_style_context();
+
+ // TODO: In Gtk+ 4, the state is not used in get_color
+ auto state = sc->get_state();
+ auto bg_color = get_background_color(sc, state);
+ auto orig_color = sc->get_color(state);
+ Gdk::RGBA fg_color = orig_color;
+ auto bar_color = mix_colors(bg_color, orig_color, 0.06);
+ // color of connector arrow heads and effect separator lines
+ auto mid_color = mix_colors(bg_color, fg_color, 0.16);
+
+ SPFilterPrimitive* prim = get_selected();
+ int row_count = get_model()->children().size();
+
+ int fwidth = CellRendererConnection::size_w;
+ Gdk::Rectangle rct, vis;
+ Gtk::TreeIter row = get_model()->children().begin();
+ int text_start_x = 0;
+ if(row) {
+ get_cell_area(get_model()->get_path(row), *get_column(1), rct);
+ get_visible_rect(vis);
+ text_start_x = rct.get_x() + rct.get_width() - get_input_type_width() * _inputs_count + 1;
+
+ auto w = get_input_type_width();
+ auto h = vis.get_height();
+ cr->save();
+ // erase selection color from selected item
+ Gdk::Cairo::set_source_rgba(cr, bg_color);
+ cr->rectangle(text_start_x + 1, 0, w * _inputs_count, h);
+ cr->fill();
+ auto text_color = fg_color;
+ text_color.set_alpha(0.7);
+
+ // draw vertical bars corresponding to possible filter inputs
+ for(unsigned int i = 0; i < _inputs_count; ++i) {
+ _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
+ const int x = text_start_x + w * i;
+ cr->save();
+
+ Gdk::Cairo::set_source_rgba(cr, bar_color);
+ cr->rectangle(x + 1, 0, w - 2, h);
+ cr->fill();
+
+ Gdk::Cairo::set_source_rgba(cr, text_color);
+ cr->move_to(x + w, 5);
+ cr->rotate_degrees(90);
+ _vertical_layout->show_in_cairo_context(cr);
+
+ cr->restore();
+ }
+
+ cr->restore();
+ cr->rectangle(vis.get_x(), 0, vis.get_width(), vis.get_height());
+ cr->clip();
+ }
+
+ int row_index = 0;
+ for(; row != get_model()->children().end(); ++row, ++row_index) {
+ get_cell_area(get_model()->get_path(row), *get_column(1), rct);
+ const int x = rct.get_x(), y = rct.get_y(), h = rct.get_height();
+
+ // Check mouse state
+ int mx, my;
+ Gdk::ModifierType mask;
+
+ auto display = get_bin_window()->get_display();
+ auto seat = display->get_default_seat();
+ auto device = seat->get_pointer();
+ cr->set_line_width(1);
+ get_bin_window()->get_device_position(device, mx, my, mask);
+
+ // Outline the bottom of the connection area
+ const int outline_x = x + fwidth * (row_count - row_index);
+ cr->save();
+
+ Gdk::Cairo::set_source_rgba(cr, mid_color);
+
+ cr->move_to(vis.get_x(), y + h);
+ cr->line_to(outline_x, y + h);
+ // Side outline
+ cr->line_to(outline_x, y - 1);
+
+ cr->stroke();
+ cr->restore();
+
+ std::vector<Gdk::Point> con_poly;
+ int con_drag_y = 0;
+ int con_drag_x = 0;
+ bool inside;
+ const SPFilterPrimitive* row_prim = (*row)[_columns.primitive];
+ const int inputs = input_count(row_prim);
+
+ if(is<SPFeMerge>(row_prim)) {
+ for(int i = 0; i < inputs; ++i) {
+ inside = do_connection_node(row, i, con_poly, mx, my);
+
+ draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
+
+ if(_in_drag == (i + 1)) {
+ con_drag_y = con_poly[2].get_y();
+ con_drag_x = con_poly[2].get_x();
+ }
+
+ if(_in_drag != (i + 1) || row_prim != prim) {
+ draw_connection(cr, row, SPAttr::INVALID, text_start_x, outline_x, con_poly[2].get_y(), row_count, i, fg_color, mid_color);
+ }
+ }
+ }
+ else {
+ // Draw "in" shape
+ inside = do_connection_node(row, 0, con_poly, mx, my);
+ con_drag_y = con_poly[2].get_y();
+ con_drag_x = con_poly[2].get_x();
+
+ draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
+
+ // Draw "in" connection
+ if(_in_drag != 1 || row_prim != prim) {
+ draw_connection(cr, row, SPAttr::IN_, text_start_x, outline_x, con_poly[2].get_y(), row_count, -1, fg_color, mid_color);
+ }
+
+ if(inputs == 2) {
+ // Draw "in2" shape
+ inside = do_connection_node(row, 1, con_poly, mx, my);
+ if(_in_drag == 2) {
+ con_drag_y = con_poly[2].get_y();
+ con_drag_x = con_poly[2].get_x();
+ }
+
+ draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
+
+ // Draw "in2" connection
+ if(_in_drag != 2 || row_prim != prim) {
+ draw_connection(cr, row, SPAttr::IN2, text_start_x, outline_x, con_poly[2].get_y(), row_count, -1, fg_color, mid_color);
+ }
+ }
+ }
+
+ // Draw drag connection
+ if(row_prim == prim && _in_drag) {
+ cr->save();
+ Gdk::Cairo::set_source_rgba(cr, orig_color);
+ cr->move_to(con_drag_x, con_drag_y);
+ cr->line_to(mx, con_drag_y);
+ cr->line_to(mx, my);
+ cr->stroke();
+ cr->restore();
+ }
+ }
+
+ return true;
+}
+
+void FilterEffectsDialog::PrimitiveList::draw_connection(const Cairo::RefPtr<Cairo::Context>& cr,
+ const Gtk::TreeIter& input, const SPAttr attr,
+ const int text_start_x, const int x1, const int y1,
+ const int row_count, const int pos,
+ const Gdk::RGBA fg_color, const Gdk::RGBA mid_color)
+{
+ cr->save();
+
+ int src_id = 0;
+ Gtk::TreeIter res = find_result(input, attr, src_id, pos);
+
+ const bool is_first = input == get_model()->children().begin();
+ const bool is_merge = is<SPFeMerge>((SPFilterPrimitive*)(*input)[_columns.primitive]);
+ const bool use_default = !res && !is_merge;
+
+ if(res == input || (use_default && is_first)) {
+ // Draw straight connection to a standard input
+ // Draw a lighter line for an implicit connection to a standard input
+ const int tw = get_input_type_width();
+ gint end_x = text_start_x + tw * src_id + 1;
+
+ if(use_default && is_first) {
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ cr->set_dash(std::vector<double> {1.0, 1.0}, 0);
+ } else {
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ }
+
+ // draw a half-circle touching destination band
+ cr->move_to(x1, y1);
+ cr->line_to(end_x, y1);
+ cr->stroke();
+ cr->arc(end_x, y1, 4, M_PI / 2, M_PI * 1.5);
+ cr->fill();
+ }
+ else {
+ // Draw an 'L'-shaped connection to another filter primitive
+ // If no connection is specified, draw a light connection to the previous primitive
+ if(use_default) {
+ res = input;
+ --res;
+ }
+
+ if(res) {
+ Gdk::Rectangle rct;
+
+ get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct);
+ const int fheight = CellRendererConnection::size_h;
+ const int fwidth = CellRendererConnection::size_w;
+
+ get_cell_area(get_model()->get_path(res), *get_column(1), rct);
+ const int row_index = find_index(res);
+ const int x2 = rct.get_x() + fwidth * (row_count - row_index) - fwidth / 2;
+ const int y2 = rct.get_y() + rct.get_height();
+
+ // Draw a bevelled 'L'-shaped connection
+ Gdk::Cairo::set_source_rgba(cr, fg_color);
+ cr->move_to(x1, y1);
+ cr->line_to(x2 - fwidth/4, y1);
+ cr->line_to(x2, y1 - fheight/4);
+ cr->line_to(x2, y2);
+ cr->stroke();
+ }
+ }
+ cr->restore();
+}
+
+// Draw the triangular outline of the connection node, and fill it
+// if desired
+void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
+ const std::vector<Gdk::Point>& points,
+ Gdk::RGBA fill, Gdk::RGBA stroke)
+{
+ cr->save();
+ cr->move_to(points[0].get_x()+0.5, points[0].get_y()+0.5);
+ cr->line_to(points[1].get_x()+0.5, points[1].get_y()+0.5);
+ cr->line_to(points[2].get_x()+0.5, points[2].get_y()+0.5);
+ cr->line_to(points[0].get_x()+0.5, points[0].get_y()+0.5);
+ cr->close_path();
+
+ Gdk::Cairo::set_source_rgba(cr, fill);
+ cr->fill_preserve();
+ cr->set_line_width(1);
+ Gdk::Cairo::set_source_rgba(cr, stroke);
+ cr->stroke();
+
+ cr->restore();
+}
+
+// Creates a triangle outline of the connection node and returns true if (x,y) is inside the node
+bool FilterEffectsDialog::PrimitiveList::do_connection_node(const Gtk::TreeIter& row, const int input,
+ std::vector<Gdk::Point>& points,
+ const int ix, const int iy)
+{
+ Gdk::Rectangle rct;
+ const int icnt = input_count((*row)[_columns.primitive]);
+
+ get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct);
+ const int fheight = CellRendererConnection::size_h;
+ const int fwidth = CellRendererConnection::size_w;
+
+ get_cell_area(_model->get_path(row), *get_column(1), rct);
+ const float h = rct.get_height() / icnt;
+
+ const int x = rct.get_x() + fwidth * (_model->children().size() - find_index(row));
+ // this is how big arrowhead appears:
+ const int con_w = (int)(fwidth * 0.70f);
+ const int con_h = (int)(fheight * 0.35f);
+ const int con_y = (int)(rct.get_y() + (h / 2) - con_h + (input * h));
+ points.clear();
+ points.emplace_back(x, con_y);
+ points.emplace_back(x, con_y + con_h * 2);
+ points.emplace_back(x - con_w, con_y + con_h);
+
+ return ix >= x - h && iy >= con_y && ix <= x && iy <= points[1].get_y();
+}
+
+const Gtk::TreeIter FilterEffectsDialog::PrimitiveList::find_result(const Gtk::TreeIter& start,
+ const SPAttr attr, int& src_id,
+ const int pos)
+{
+ SPFilterPrimitive* prim = (*start)[_columns.primitive];
+ Gtk::TreeIter target = _model->children().end();
+ int image = 0;
+
+ if(is<SPFeMerge>(prim)) {
+ int c = 0;
+ bool found = false;
+ for (auto& o: prim->children) {
+ if(c == pos && is<SPFeMergeNode>(&o)) {
+ image = cast<SPFeMergeNode>(&o)->get_in();
+ found = true;
+ }
+ ++c;
+ }
+ if(!found)
+ return target;
+ }
+ else {
+ if(attr == SPAttr::IN_)
+ image = prim->get_in();
+ else if(attr == SPAttr::IN2) {
+ if(is<SPFeBlend>(prim))
+ image = cast<SPFeBlend>(prim)->get_in2();
+ else if(is<SPFeComposite>(prim))
+ image = cast<SPFeComposite>(prim)->get_in2();
+ else if(is<SPFeDisplacementMap>(prim))
+ image = cast<SPFeDisplacementMap>(prim)->get_in2();
+ else
+ return target;
+ }
+ else
+ return target;
+ }
+
+ if(image >= 0) {
+ for(Gtk::TreeIter i = _model->children().begin();
+ i != start; ++i) {
+ if(((SPFilterPrimitive*)(*i)[_columns.primitive])->get_out() == image)
+ target = i;
+ }
+ return target;
+ }
+ else if(image < -1) {
+ src_id = -(image + 2);
+ return start;
+ }
+
+ return target;
+}
+
+int FilterEffectsDialog::PrimitiveList::find_index(const Gtk::TreeIter& target)
+{
+ int i = 0;
+ for (Gtk::TreeIter iter = _model->children().begin();
+ iter != target; ++iter, ++i){};
+ return i;
+}
+
+bool FilterEffectsDialog::PrimitiveList::on_button_press_event(GdkEventButton* e)
+{
+ Gtk::TreePath path;
+ Gtk::TreeViewColumn* col;
+ const int x = (int)e->x, y = (int)e->y;
+ int cx, cy;
+
+ _drag_prim = nullptr;
+
+ if(get_path_at_pos(x, y, path, col, cx, cy)) {
+ Gtk::TreeIter iter = _model->get_iter(path);
+ std::vector<Gdk::Point> points;
+
+ _drag_prim = (*iter)[_columns.primitive];
+ const int icnt = input_count(_drag_prim);
+
+ for(int i = 0; i < icnt; ++i) {
+ if(do_connection_node(_model->get_iter(path), i, points, x, y)) {
+ _in_drag = i + 1;
+ break;
+ }
+ }
+
+ queue_draw();
+ }
+
+ if(_in_drag) {
+ _scroll_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &PrimitiveList::on_scroll_timeout), 150);
+ _autoscroll_x = 0;
+ _autoscroll_y = 0;
+ get_selection()->select(path);
+ return true;
+ }
+ else
+ return Gtk::TreeView::on_button_press_event(e);
+}
+
+bool FilterEffectsDialog::PrimitiveList::on_motion_notify_event(GdkEventMotion* e)
+{
+ const int speed = 10;
+ const int limit = 15;
+
+ Gdk::Rectangle vis;
+ get_visible_rect(vis);
+ int vis_x, vis_y;
+
+ int vis_x2, vis_y2;
+ convert_widget_to_tree_coords(vis.get_x(), vis.get_y(), vis_x2, vis_y2);
+
+ convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y);
+ const int top = vis_y + vis.get_height();
+ const int right_edge = vis_x + vis.get_width();
+
+ // When autoscrolling during a connection drag, set the speed based on
+ // where the mouse is in relation to the edges.
+ if(e->y < vis_y)
+ _autoscroll_y = -(int)(speed + (vis_y - e->y) / 5);
+ else if(e->y < vis_y + limit)
+ _autoscroll_y = -speed;
+ else if(e->y > top)
+ _autoscroll_y = (int)(speed + (e->y - top) / 5);
+ else if(e->y > top - limit)
+ _autoscroll_y = speed;
+ else
+ _autoscroll_y = 0;
+
+ double e2 = ( e->x - vis_x2/2);
+ // horizontal scrolling
+ if(e2 < vis_x)
+ _autoscroll_x = -(int)(speed + (vis_x - e2) / 5);
+ else if(e2 < vis_x + limit)
+ _autoscroll_x = -speed;
+ else if(e2 > right_edge)
+ _autoscroll_x = (int)(speed + (e2 - right_edge) / 5);
+ else if(e2 > right_edge - limit)
+ _autoscroll_x = speed;
+ else
+ _autoscroll_x = 0;
+
+
+
+ queue_draw();
+
+ return Gtk::TreeView::on_motion_notify_event(e);
+}
+
+bool FilterEffectsDialog::PrimitiveList::on_button_release_event(GdkEventButton* e)
+{
+ SPFilterPrimitive *prim = get_selected(), *target;
+
+ _scroll_connection.disconnect();
+
+ if(_in_drag && prim) {
+ Gtk::TreePath path;
+ Gtk::TreeViewColumn* col;
+ int cx, cy;
+
+ if(get_path_at_pos((int)e->x, (int)e->y, path, col, cx, cy)) {
+ const gchar *in_val = nullptr;
+ Glib::ustring result;
+ Gtk::TreeIter target_iter = _model->get_iter(path);
+ target = (*target_iter)[_columns.primitive];
+ col = get_column(1);
+
+ Gdk::Rectangle rct;
+ get_cell_area(path, *col, rct);
+ const int twidth = get_input_type_width();
+ const int sources_x = rct.get_width() - twidth * _inputs_count;
+ if(cx > sources_x) {
+ int src = (cx - sources_x) / twidth;
+ if (src < 0) {
+ src = 0;
+ } else if(src >= static_cast<int>(_inputs_count)) {
+ src = _inputs_count - 1;
+ }
+ result = FPInputConverter.get_key((FilterPrimitiveInput)src);
+ in_val = result.c_str();
+ }
+ else {
+ // Ensure that the target comes before the selected primitive
+ for(Gtk::TreeIter iter = _model->children().begin();
+ iter != get_selection()->get_selected(); ++iter) {
+ if(iter == target_iter) {
+ Inkscape::XML::Node *repr = target->getRepr();
+ // Make sure the target has a result
+ const gchar *gres = repr->attribute("result");
+ if(!gres) {
+ result = cast<SPFilter>(prim->parent)->get_new_result_name();
+ repr->setAttributeOrRemoveIfEmpty("result", result);
+ in_val = result.c_str();
+ }
+ else
+ in_val = gres;
+ break;
+ }
+ }
+ }
+
+ if(is<SPFeMerge>(prim)) {
+ int c = 1;
+ bool handled = false;
+ for (auto& o: prim->children) {
+ if(c == _in_drag && is<SPFeMergeNode>(&o)) {
+ // If input is null, delete it
+ if(!in_val) {
+
+ //XML Tree being used directly here while it shouldn't be.
+ sp_repr_unparent(o.getRepr());
+ DocumentUndo::done(prim->document, _("Remove merge node"), INKSCAPE_ICON("dialog-filters"));
+ (*get_selection()->get_selected())[_columns.primitive] = prim;
+ } else {
+ _dialog.set_attr(&o, SPAttr::IN_, in_val);
+ }
+ handled = true;
+ break;
+ }
+ ++c;
+ }
+ // Add new input?
+ if(!handled && c == _in_drag && in_val) {
+ Inkscape::XML::Document *xml_doc = prim->document->getReprDoc();
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:feMergeNode");
+ repr->setAttribute("inkscape:collect", "always");
+
+ //XML Tree being used directly here while it shouldn't be.
+ prim->getRepr()->appendChild(repr);
+ auto node = cast<SPFeMergeNode>(prim->document->getObjectByRepr(repr));
+ Inkscape::GC::release(repr);
+ _dialog.set_attr(node, SPAttr::IN_, in_val);
+ (*get_selection()->get_selected())[_columns.primitive] = prim;
+ }
+ }
+ else {
+ if(_in_drag == 1)
+ _dialog.set_attr(prim, SPAttr::IN_, in_val);
+ else if(_in_drag == 2)
+ _dialog.set_attr(prim, SPAttr::IN2, in_val);
+ }
+ }
+
+ _in_drag = 0;
+ queue_draw();
+
+ _dialog.update_settings_view();
+ }
+
+ if((e->type == GDK_BUTTON_RELEASE) && (e->button == 3)) {
+ const bool sensitive = get_selected() != nullptr;
+ auto items = _primitive_menu->get_children();
+ items[0]->set_sensitive(sensitive);
+ items[1]->set_sensitive(sensitive);
+
+ _primitive_menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(e));
+
+ return true;
+ }
+ else
+ return Gtk::TreeView::on_button_release_event(e);
+}
+
+// Checks all of prim's inputs, removes any that use result
+static void check_single_connection(SPFilterPrimitive* prim, const int result)
+{
+ if (prim && (result >= 0)) {
+ if (prim->get_in() == result) {
+ prim->removeAttribute("in");
+ }
+
+ if (auto blend = cast<SPFeBlend>(prim)) {
+ if (blend->get_in2() == result) {
+ prim->removeAttribute("in2");
+ }
+ } else if (auto comp = cast<SPFeComposite>(prim)) {
+ if (comp->get_in2() == result) {
+ prim->removeAttribute("in2");
+ }
+ } else if (auto disp = cast<SPFeDisplacementMap>(prim)) {
+ if (disp->get_in2() == result) {
+ prim->removeAttribute("in2");
+ }
+ }
+ }
+}
+
+// Remove any connections going to/from prim_iter that forward-reference other primitives
+void FilterEffectsDialog::PrimitiveList::sanitize_connections(const Gtk::TreeIter& prim_iter)
+{
+ SPFilterPrimitive *prim = (*prim_iter)[_columns.primitive];
+ bool before = true;
+
+ for(Gtk::TreeIter iter = _model->children().begin();
+ iter != _model->children().end(); ++iter) {
+ if(iter == prim_iter)
+ before = false;
+ else {
+ SPFilterPrimitive* cur_prim = (*iter)[_columns.primitive];
+ if(before)
+ check_single_connection(cur_prim, prim->get_out());
+ else
+ check_single_connection(prim, cur_prim->get_out());
+ }
+ }
+}
+
+// Reorder the filter primitives to match the list order
+void FilterEffectsDialog::PrimitiveList::on_drag_end(const Glib::RefPtr<Gdk::DragContext>& /*dc*/)
+{
+ SPFilter* filter = _dialog._filter_modifier.get_selected_filter();
+ int ndx = 0;
+
+ for (Gtk::TreeModel::iterator iter = _model->children().begin();
+ iter != _model->children().end(); ++iter, ++ndx) {
+ SPFilterPrimitive* prim = (*iter)[_columns.primitive];
+ if (prim && prim == _drag_prim) {
+ prim->getRepr()->setPosition(ndx);
+ break;
+ }
+ }
+
+ for (Gtk::TreeModel::iterator iter = _model->children().begin();
+ iter != _model->children().end(); ++iter, ++ndx) {
+ SPFilterPrimitive* prim = (*iter)[_columns.primitive];
+ if (prim && prim == _drag_prim) {
+ sanitize_connections(iter);
+ get_selection()->select(iter);
+ break;
+ }
+ }
+
+ filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ DocumentUndo::done(filter->document, _("Reorder filter primitive"), INKSCAPE_ICON("dialog-filters"));
+}
+
+// If a connection is dragged towards the top or bottom of the list, the list should scroll to follow.
+bool FilterEffectsDialog::PrimitiveList::on_scroll_timeout()
+{
+ if(_autoscroll_y) {
+ auto a = static_cast<Gtk::ScrolledWindow*>(get_parent())->get_vadjustment();
+ double v = a->get_value() + _autoscroll_y;
+
+ if(v < 0)
+ v = 0;
+ if(v > a->get_upper() - a->get_page_size())
+ v = a->get_upper() - a->get_page_size();
+
+ a->set_value(v);
+
+ queue_draw();
+ }
+
+
+ if(_autoscroll_x) {
+ auto a_h = static_cast<Gtk::ScrolledWindow*>(get_parent())->get_hadjustment();
+ double h = a_h->get_value() + _autoscroll_x;
+
+ if(h < 0)
+ h = 0;
+ if(h > a_h->get_upper() - a_h->get_page_size())
+ h = a_h->get_upper() - a_h->get_page_size();
+
+ a_h->set_value(h);
+
+ queue_draw();
+ }
+
+ return true;
+}
+
+int FilterEffectsDialog::PrimitiveList::primitive_count() const
+{
+ return _model->children().size();
+}
+
+int FilterEffectsDialog::PrimitiveList::get_input_type_width() const
+{
+ // Maximum font height calculated in initText() and stored in _input_type_width.
+ // Add 2 to font height to account for rectangle around text.
+ return _input_type_width + 2;
+}
+
+int FilterEffectsDialog::PrimitiveList::get_inputs_count() const {
+ return _inputs_count;
+}
+
+void FilterEffectsDialog::PrimitiveList::set_inputs_count(int count) {
+ _inputs_count = count;
+ queue_allocate();
+ queue_draw();
+}
+
+enum class EffectCategory { Effect, Compose, Colors, Generation };
+
+const Glib::ustring& get_category_name(EffectCategory category) {
+ static const std::map<EffectCategory, Glib::ustring> category_names = {
+ { EffectCategory::Effect, _("Effect") },
+ { EffectCategory::Compose, _("Compositing") },
+ { EffectCategory::Colors, _("Color editing") },
+ { EffectCategory::Generation, _("Generating") },
+ };
+ return category_names.at(category);
+}
+
+struct EffectMetadata {
+ EffectCategory category;
+ Glib::ustring icon_name;
+ Glib::ustring tooltip;
+};
+
+static const std::map<Inkscape::Filters::FilterPrimitiveType, EffectMetadata>& get_effects() {
+ static std::map<Inkscape::Filters::FilterPrimitiveType, EffectMetadata> effects = {
+ { NR_FILTER_GAUSSIANBLUR, { EffectCategory::Effect, "feGaussianBlur-icon",
+ _("Uniformly blurs its input. Commonly used together with Offset to create a drop shadow effect.") }},
+ { NR_FILTER_MORPHOLOGY, { EffectCategory::Effect, "feMorphology-icon",
+ _("Provides erode and dilate effects. For single-color objects erode makes the object thinner and dilate makes it thicker.") }},
+ { NR_FILTER_OFFSET, { EffectCategory::Effect, "feOffset-icon",
+ _("Offsets the input by an user-defined amount. Commonly used for drop shadow effects.") }},
+ { NR_FILTER_CONVOLVEMATRIX, { EffectCategory::Effect, "feConvolveMatrix-icon",
+ _("Performs a convolution on the input image enabling effects like blur, sharpening, embossing and edge detection.") }},
+ { NR_FILTER_DISPLACEMENTMAP, { EffectCategory::Effect, "feDisplacementMap-icon",
+ _("Displaces pixels from the first input using the second as a map of displacement intensity. Classical examples are whirl and pinch effects.") }},
+ { NR_FILTER_TILE, { EffectCategory::Effect, "feTile-icon",
+ _("Tiles a region with an input graphic. The source tile is defined by the filter primitive subregion of the input.") }},
+ { NR_FILTER_COMPOSITE, { EffectCategory::Compose, "feComposite-icon",
+ _("Composites two images using one of the Porter-Duff blending modes or the arithmetic mode described in SVG standard.") }},
+ { NR_FILTER_BLEND, { EffectCategory::Compose, "feBlend-icon",
+ _("Provides image blending modes, such as screen, multiply, darken and lighten.") }},
+ { NR_FILTER_MERGE, { EffectCategory::Compose, "feMerge-icon",
+ _("Merges multiple inputs using normal alpha compositing. Equivalent to using several Blend primitives in 'normal' mode or several Composite primitives in 'over' mode.") }},
+ { NR_FILTER_COLORMATRIX, { EffectCategory::Colors, "feColorMatrix-icon",
+ _("Modifies pixel colors based on a transformation matrix. Useful for adjusting color hue and saturation.") }},
+ { NR_FILTER_COMPONENTTRANSFER, { EffectCategory::Colors, "feComponentTransfer-icon",
+ _("Manipulates color components according to particular transfer functions. Useful for brightness and contrast adjustment, color balance, and thresholding.") }},
+ { NR_FILTER_DIFFUSELIGHTING, { EffectCategory::Colors, "feDiffuseLighting-icon",
+ _("Creates \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.") }},
+ { NR_FILTER_SPECULARLIGHTING, { EffectCategory::Colors, "feSpecularLighting-icon",
+ _("Creates \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.") }},
+ { NR_FILTER_FLOOD, { EffectCategory::Generation, "feFlood-icon",
+ _("Fills the region with a given color and opacity. Often used as input to other filters to apply color to a graphic.") }},
+ { NR_FILTER_IMAGE, { EffectCategory::Generation, "feImage-icon",
+ _("Fills the region with graphics from an external file or from another portion of the document.") }},
+ { NR_FILTER_TURBULENCE, { EffectCategory::Generation, "feTurbulence-icon",
+ _("Renders Perlin noise, which is useful to generate textures such as clouds, fire, smoke, marble or granite.") }},
+ };
+ return effects;
+}
+
+// populate popup with filter effects and completion list for a search box
+void FilterEffectsDialog::add_effects(Inkscape::UI::Widget::CompletionPopup& popup, bool symbolic) {
+ auto& menu = popup.get_menu();
+
+ struct Effect {
+ Inkscape::Filters::FilterPrimitiveType type;
+ Glib::ustring label;
+ EffectCategory category;
+ Glib::ustring icon_name;
+ Glib::ustring tooltip;
+ };
+ std::vector<Effect> effects;
+ effects.reserve(get_effects().size());
+ for (auto&& effect : get_effects()) {
+ effects.push_back({
+ effect.first,
+ _(FPConverter.get_label(effect.first).c_str()),
+ effect.second.category,
+ effect.second.icon_name,
+ effect.second.tooltip
+ });
+ }
+ std::sort(begin(effects), end(effects), [=](auto&& a, auto&& b) {
+ if (a.category != b.category) {
+ return a.category < b.category;
+ }
+ return a.label < b.label;
+ });
+
+ popup.clear_completion_list();
+
+ // 2-column menu
+ Inkscape::UI::ColumnMenuBuilder<EffectCategory> builder(menu, 2, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+
+ for (auto& effect : effects) {
+ // build popup menu
+ auto type = effect.type;
+ auto * menuitem = builder.add_item(effect.label, effect.category, effect.tooltip, effect.icon_name, true, true, [=](){ add_filter_primitive(type); });
+ gint id = (gint)type;
+ menuitem->property_has_tooltip() = true;
+ menuitem->signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltipw){
+ return sp_query_custom_tooltip(x, y, kbd, tooltipw, id, effect.tooltip, effect.icon_name);
+ });
+ if (builder.new_section()) {
+ builder.set_section(get_category_name(effect.category));
+ }
+
+ // build completion list
+ popup.add_to_completion_list(static_cast<int>(effect.type), effect.label, effect.icon_name + (symbolic ? "-symbolic" : ""));
+ }
+
+ if (symbolic) {
+ menu.get_style_context()->add_class("symbolic");
+ }
+}
+
+/*** FilterEffectsDialog ***/
+
+FilterEffectsDialog::FilterEffectsDialog()
+ : DialogBase("/dialogs/filtereffects", "FilterEffects"),
+ _builder(create_builder("dialog-filter-editor.glade")),
+ _paned(get_widget<Gtk::Paned>(_builder, "paned")),
+ _main_grid(get_widget<Gtk::Grid>(_builder, "main")),
+ _params_box(get_widget<Gtk::Box>(_builder, "params")),
+ _search_box(get_widget<Gtk::Box>(_builder, "search")),
+ _search_wide_box(get_widget<Gtk::Box>(_builder, "search-wide")),
+ _filter_wnd(get_widget<Gtk::ScrolledWindow>(_builder, "filter")),
+ _cur_filter_btn(get_widget<Gtk::CheckButton>(_builder, "label"))
+ , _add_primitive_type(FPConverter)
+ , _add_primitive(_("Add Effect:"))
+ , _empty_settings("", Gtk::ALIGN_CENTER)
+ , _no_filter_selected(_("No filter selected"), Gtk::ALIGN_START)
+ , _settings_initialized(false)
+ , _locked(false)
+ , _attr_lock(false)
+ , _filter_modifier(*this, _builder)
+ , _primitive_list(*this)
+ , _settings_effect(Gtk::ORIENTATION_VERTICAL)
+ , _settings_filter(Gtk::ORIENTATION_VERTICAL)
+{
+ _settings = new Settings(*this, _settings_effect, [=](auto a){ set_attr_direct(a); }, NR_FILTER_ENDPRIMITIVETYPE);
+ _cur_effect_name = &get_widget<Gtk::Label>(_builder, "cur-effect");
+ _settings->_size_group->add_widget(*_cur_effect_name);
+ _filter_general_settings = new Settings(*this, _settings_filter, [=](auto a){ set_filternode_attr(a); }, 1);
+
+ // Initialize widget hierarchy
+ _primitive_box = &get_widget<Gtk::ScrolledWindow>(_builder, "filter");
+ _primitive_list.set_enable_search(false);
+ _primitive_list.show_all();
+ _primitive_box->add(_primitive_list);
+
+ auto symbolic = Inkscape::Preferences::get()->getBool("/theme/symbolicIcons", true);
+ add_effects(_effects_popup, symbolic);
+ _effects_popup.get_entry().set_placeholder_text(_("Add effect"));
+ _effects_popup.on_match_selected().connect([=](int id){ add_filter_primitive(static_cast<FilterPrimitiveType>(id)); });
+ _search_box.pack_start(_effects_popup);
+
+ _filter_modifier.show_all();
+
+ _settings_effect.show_all();
+ _params_box.pack_end(_settings_effect);
+
+ _settings_filter.show_all();
+ get_widget<Gtk::Popover>(_builder, "gen-settings").add(_settings_filter);
+
+ get_widget<Gtk::Popover>(_builder, "info-popover").signal_show().connect([=](){
+ if (auto prim = _primitive_list.get_selected()) {
+ if (prim->getRepr()) {
+ auto id = FPConverter.get_id_from_key(prim->getRepr()->name());
+ const auto& effect = get_effects().at(id);
+ get_widget<Gtk::Image>(_builder, "effect-icon").set_from_icon_name(effect.icon_name, Gtk::ICON_SIZE_DND);
+ auto buffer = get_widget<Gtk::TextView>(_builder, "effect-info").get_buffer();
+ buffer->set_text("");
+ buffer->insert_markup(buffer->begin(), effect.tooltip);
+ get_widget<Gtk::TextView>(_builder, "effect-desc").get_buffer()->set_text("");
+ }
+ }
+ });
+
+ _primitive_list.signal_primitive_changed().connect([=](){
+ update_settings_view();
+ });
+
+ _cur_filter_toggle = _cur_filter_btn.signal_toggled().connect([=](){
+ _filter_modifier.toggle_current_filter();
+ });
+
+ auto update_checkbox = [=](){
+ auto active = _filter_modifier.is_selected_filter_active();
+ _cur_filter_toggle.block();
+ _cur_filter_btn.set_active(active);
+ _cur_filter_toggle.unblock();
+ };
+
+ auto update_widgets = [=](){
+ auto& opt = get_widget<Gtk::MenuButton>(_builder, "filter-opt");
+ _primitive_list.update();
+ Glib::ustring name = "-";
+ if (auto filter = _filter_modifier.get_selected_filter()) {
+ name = get_filter_name(filter);
+ _effects_popup.set_sensitive();
+ _cur_filter_btn.set_sensitive(); // ideally this should also be selection-dependent
+ opt.set_sensitive();
+ }
+ else {
+ _effects_popup.set_sensitive(false);
+ _cur_filter_btn.set_sensitive(false);
+ opt.set_sensitive(false);
+ }
+ get_widget<Gtk::Label>(_builder, "filter-name").set_label(name);
+ update_checkbox();
+ update_settings_view();
+ };
+
+ //TODO: adding animated GIFs to the info popup once they are ready:
+ // auto a = Gdk::PixbufAnimation::create_from_file("/Users/mike/blur-effect.gif");
+ // get_widget<Gtk::Image>(_builder, "effect-image").property_pixbuf_animation().set_value(a);
+
+ init_settings_widgets();
+
+ _filter_modifier.signal_filter_changed().connect([=](){
+ update_widgets();
+ });
+
+ _filter_modifier.signal_filters_updated().connect([=](){
+ update_checkbox();
+ });
+
+ _add_primitive.signal_clicked().connect(sigc::mem_fun(*this, &FilterEffectsDialog::add_primitive));
+ _primitive_list.set_menu(*this, sigc::mem_fun(*this, &FilterEffectsDialog::duplicate_primitive),
+ sigc::mem_fun(_primitive_list, &PrimitiveList::remove_selected));
+
+ get_widget<Gtk::Button>(_builder, "new-filter").signal_clicked().connect([=](){ _filter_modifier.add_filter(); });
+ pack_start(_main_grid);
+
+ get_widget<Gtk::Button>(_builder, "dup-btn").signal_clicked().connect([=](){ duplicate_primitive(); });
+ get_widget<Gtk::Button>(_builder, "del-btn").signal_clicked().connect([=](){ _primitive_list.remove_selected(); });
+ get_widget<Gtk::Button>(_builder, "info-btn").signal_clicked().connect([=](){ /* todo */ });
+
+ auto* show_sources = &get_widget<Gtk::ToggleButton>(_builder, "btn-connect");
+ auto set_inputs = [=](bool all){
+ int count = all ? FPInputConverter._length : 2;
+ _primitive_list.set_inputs_count(count);
+ // full rebuild: this is what it takes to make cell renderer new min width into account to adjust scrollbar
+ _primitive_list.update();
+ };
+ auto show_all_sources = Inkscape::Preferences::get()->getBool(_prefs + "/dialogs/filters/showAllSources", false);
+ show_sources->set_active(show_all_sources);
+ set_inputs(show_all_sources);
+ show_sources->signal_toggled().connect([=](){
+ bool show_all = show_sources->get_active();
+ set_inputs(show_all);
+ Inkscape::Preferences::get()->setBool(_prefs + "/dialogs/filters/showAllSources", show_all);
+ });
+
+ _paned.set_position(Inkscape::Preferences::get()->getIntLimited(_prefs + "/handlePos", 200, 10, 9999));
+ _paned.property_position().signal_changed().connect([=](){
+ Inkscape::Preferences::get()->setInt(_prefs + "/handlePos", _paned.get_position());
+ });
+
+ _primitive_list.update();
+
+ show();
+
+ // reading minimal width at this point should reflect space needed for fitting effect parameters panel
+ int min_width = 0, dummy = 0;
+ get_preferred_width(min_width, dummy);
+ int min_effects = 0;
+ _effects_popup.get_preferred_width(min_effects, dummy);
+ // calculate threshold/minimum width of filters dialog in horizontal layout;
+ // use this size to decide where transition from vertical to horizontal layout is;
+ // if this size is too small dialog can get stuck in horizontal layout - users won't be able
+ // to make it narrow again, due to min dialog size enforced by GTK
+ int thresold_width = min_width + min_effects * 3;
+
+ // two alternative layout arrangements depending on the dialog size;
+ // one is tall and narrow with widgets in one column, while the other
+ // is for wide dialogs with filter parameters and effects side by side
+ signal_size_allocate().connect([=] (const Gtk::Allocation& alloc) {
+ if (alloc.get_width() < 10 || alloc.get_height() < 10) return;
+
+ double const ratio = alloc.get_width() / static_cast<double>(alloc.get_height());
+
+ double constexpr hysteresis = 0.01;
+ if (ratio < 1 - hysteresis || alloc.get_width() <= thresold_width) {
+ // make narrow/tall
+ if (!_narrow_dialog) {
+ _main_grid.remove(_filter_wnd);
+ _search_wide_box.remove(_effects_popup);
+ _paned.add1(_filter_wnd);
+ _search_box.pack_start(_effects_popup);
+ _paned.set_size_request();
+ get_widget<Gtk::Box>(_builder, "connect-box-wide").remove(*show_sources);
+ get_widget<Gtk::Box>(_builder, "connect-box").add(*show_sources);
+ _narrow_dialog = true;
+ ensure_size();
+ }
+ }
+ else if (ratio > 1 + hysteresis && alloc.get_width() > thresold_width) {
+ // make wide/short
+ if (_narrow_dialog) {
+ _paned.remove(_filter_wnd);
+ _search_box.remove(_effects_popup);
+ _main_grid.attach(_filter_wnd, 2, 1, 1, 2);
+ _search_wide_box.pack_start(_effects_popup);
+ _paned.set_size_request(min_width);
+ get_widget<Gtk::Box>(_builder, "connect-box").remove(*show_sources);
+ get_widget<Gtk::Box>(_builder, "connect-box-wide").add(*show_sources);
+ _narrow_dialog = false;
+ ensure_size();
+ }
+ }
+ });
+
+ update_widgets();
+ show_all_children();
+ update();
+ update_settings_view();
+}
+
+FilterEffectsDialog::~FilterEffectsDialog()
+{
+ delete _settings;
+ delete _filter_general_settings;
+}
+
+void FilterEffectsDialog::documentReplaced()
+{
+ _resource_changed.disconnect();
+ if (auto document = getDocument()) {
+ _resource_changed = document->connectResourcesChanged("filter", [=](){ _filter_modifier.update_filters(); });
+
+ _filter_modifier.update_filters();
+ }
+}
+
+void FilterEffectsDialog::selectionChanged(Inkscape::Selection *selection)
+{
+ if (selection) {
+ _filter_modifier.update_selection(selection);
+ }
+}
+
+void FilterEffectsDialog::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ if (flags & ( SP_OBJECT_MODIFIED_FLAG |
+ SP_OBJECT_PARENT_MODIFIED_FLAG |
+ SP_OBJECT_STYLE_MODIFIED_FLAG) ) {
+ _filter_modifier.update_selection(selection);
+ }
+}
+
+void FilterEffectsDialog::set_attrs_locked(const bool l)
+{
+ _locked = l;
+}
+
+void FilterEffectsDialog::show_all_vfunc()
+{
+ update_settings_view();
+}
+
+void FilterEffectsDialog::init_settings_widgets()
+{
+ // TODO: Find better range/climb-rate/digits values for the SpinScales,
+ // most of the current values are complete guesses!
+
+ _empty_settings.set_sensitive(false);
+ _settings_effect.pack_start(_empty_settings);
+
+ _no_filter_selected.set_sensitive(false);
+ _settings_filter.pack_start(_no_filter_selected);
+ _settings_initialized = true;
+
+ _filter_general_settings->type(0);
+ auto _region_auto = _filter_general_settings->add_checkbutton(true, SPAttr::AUTO_REGION, _("Automatic Region"), "true", "false", _("If unset, the coordinates and dimensions won't be updated automatically."));
+ _region_pos = _filter_general_settings->add_multispinbutton(/*default x:*/ (double) -0.1, /*default y:*/ (double) -0.1, SPAttr::X, SPAttr::Y, _("Coordinates:"), -100, 100, 0.01, 0.1, 2, _("X coordinate of the left corners of filter effects region"), _("Y coordinate of the upper corners of filter effects region"));
+ _region_size = _filter_general_settings->add_multispinbutton(/*default width:*/ (double) 1.2, /*default height:*/ (double) 1.2, SPAttr::WIDTH, SPAttr::HEIGHT, _("Dimensions:"), 0, 1000, 0.01, 0.1, 2, _("Width of filter effects region"), _("Height of filter effects region"));
+ _region_auto->signal_attr_changed().connect( sigc::bind(sigc::mem_fun(*this, &FilterEffectsDialog::update_automatic_region), _region_auto));
+
+ _settings->type(NR_FILTER_BLEND);
+ _settings->add_combo(SP_CSS_BLEND_NORMAL, SPAttr::MODE, _("Mode:"), SPBlendModeConverter);
+
+ _settings->type(NR_FILTER_COLORMATRIX);
+ ComboBoxEnum<FilterColorMatrixType>* colmat = _settings->add_combo(COLORMATRIX_MATRIX, SPAttr::TYPE, _("Type:"), ColorMatrixTypeConverter, _("Indicates the type of matrix operation. The keyword 'matrix' indicates that a full 5x4 matrix of values will be provided. The other keywords represent convenience shortcuts to allow commonly used color operations to be performed without specifying a complete matrix."));
+ _color_matrix_values = _settings->add_colormatrixvalues(_("Value(s):"));
+ colmat->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::update_color_matrix));
+
+ _settings->type(NR_FILTER_COMPONENTTRANSFER);
+ // TRANSLATORS: Abbreviation for red color channel in RGBA
+ _settings->add_componenttransfervalues(C_("color", "R:"), SPFeFuncNode::R);
+ // TRANSLATORS: Abbreviation for green color channel in RGBA
+ _settings->add_componenttransfervalues(C_("color", "G:"), SPFeFuncNode::G);
+ // TRANSLATORS: Abbreviation for blue color channel in RGBA
+ _settings->add_componenttransfervalues(C_("color", "B:"), SPFeFuncNode::B);
+ // TRANSLATORS: Abbreviation for alpha channel in RGBA
+ _settings->add_componenttransfervalues(C_("color", "A:"), SPFeFuncNode::A);
+
+ _settings->type(NR_FILTER_COMPOSITE);
+ _settings->add_combo(COMPOSITE_OVER, SPAttr::OPERATOR, _("Operator:"), CompositeOperatorConverter);
+ _k1 = _settings->add_spinscale(0, SPAttr::K1, _("K1:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively."));
+ _k2 = _settings->add_spinscale(0, SPAttr::K2, _("K2:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively."));
+ _k3 = _settings->add_spinscale(0, SPAttr::K3, _("K3:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively."));
+ _k4 = _settings->add_spinscale(0, SPAttr::K4, _("K4:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively."));
+
+ _settings->type(NR_FILTER_CONVOLVEMATRIX);
+ _convolve_order = _settings->add_dualspinbutton((char*)"3", SPAttr::ORDER, _("Size:"), 1, max_convolution_kernel_size, 1, 1, 0, _("width of the convolve matrix"), _("height of the convolve matrix"));
+ _convolve_target = _settings->add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, SPAttr::TARGETX, SPAttr::TARGETY, _("Target:"), 0, max_convolution_kernel_size - 1, 1, 1, 0, _("X coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."), _("Y coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."));
+ //TRANSLATORS: for info on "Kernel", see http://en.wikipedia.org/wiki/Kernel_(matrix)
+ _convolve_matrix = _settings->add_matrix(SPAttr::KERNELMATRIX, _("Kernel:"), _("This matrix describes the convolve operation that is applied to the input image in order to calculate the pixel colors at the output. Different arrangements of values in this matrix result in various possible visual effects. An identity matrix would lead to a motion blur effect (parallel to the matrix diagonal) while a matrix filled with a constant non-zero value would lead to a common blur effect."));
+ _convolve_order->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::convolve_order_changed));
+ _settings->add_spinscale(0, SPAttr::DIVISOR, _("Divisor:"), 0, 1000, 1, 0.1, 2, _("After applying the kernelMatrix to the input image to yield a number, that number is divided by divisor to yield the final destination color value. A divisor that is the sum of all the matrix values tends to have an evening effect on the overall color intensity of the result."));
+ _settings->add_spinscale(0, SPAttr::BIAS, _("Bias:"), -10, 10, 0.1, 0.5, 2, _("This value is added to each component. This is useful to define a constant value as the zero response of the filter."));
+ _settings->add_combo(CONVOLVEMATRIX_EDGEMODE_NONE, SPAttr::EDGEMODE, _("Edge Mode:"), ConvolveMatrixEdgeModeConverter, _("Determines how to extend the input image as necessary with color values so that the matrix operations can be applied when the kernel is positioned at or near the edge of the input image."));
+ _settings->add_checkbutton(false, SPAttr::PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive."));
+
+ _settings->type(NR_FILTER_DIFFUSELIGHTING);
+ _settings->add_color(/*default: white*/ 0xffffffff, SPAttr::LIGHTING_COLOR, _("Diffuse Color:"), _("Defines the color of the light source"));
+ _settings->add_spinscale(1, SPAttr::SURFACESCALE, _("Surface Scale:"), -5, 5, 0.01, 0.001, 3, _("This value amplifies the heights of the bump map defined by the input alpha channel"));
+ _settings->add_spinscale(1, SPAttr::DIFFUSECONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model."));
+ // deprecated (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/kernelUnitLength)
+ // _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1);
+ _settings->add_lightsource();
+
+ _settings->type(NR_FILTER_DISPLACEMENTMAP);
+ _settings->add_spinscale(0, SPAttr::SCALE, _("Scale:"), 0, 100, 1, 0.01, 1, _("This defines the intensity of the displacement effect."));
+ _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SPAttr::XCHANNELSELECTOR, _("X displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the X direction"));
+ _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SPAttr::YCHANNELSELECTOR, _("Y displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the Y direction"));
+
+ _settings->type(NR_FILTER_FLOOD);
+ _settings->add_color(/*default: black*/ 0, SPAttr::FLOOD_COLOR, _("Color:"), _("The whole filter region will be filled with this color."));
+ _settings->add_spinscale(1, SPAttr::FLOOD_OPACITY, _("Opacity:"), 0, 1, 0.1, 0.01, 2);
+
+ _settings->type(NR_FILTER_GAUSSIANBLUR);
+ _settings->add_dualspinscale(SPAttr::STDDEVIATION, _("Size:"), 0, 100, 1, 0.01, 2, _("The standard deviation for the blur operation."));
+
+ _settings->type(NR_FILTER_MERGE);
+ _settings->add_no_params();
+
+ _settings->type(NR_FILTER_MORPHOLOGY);
+ _settings->add_combo(MORPHOLOGY_OPERATOR_ERODE, SPAttr::OPERATOR, _("Operator:"), MorphologyOperatorConverter, _("Erode: performs \"thinning\" of input image.\nDilate: performs \"fattening\" of input image."));
+ _settings->add_dualspinscale(SPAttr::RADIUS, _("Radius:"), 0, 100, 1, 0.01, 1);
+
+ _settings->type(NR_FILTER_IMAGE);
+ _settings->add_fileorelement(SPAttr::XLINK_HREF, _("Source of Image:"));
+ _image_x = _settings->add_entry(SPAttr::X, _("Position X:"), _("Position X"));
+ _image_x->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_x_changed));
+ //This is commented out because we want the default empty value of X or Y and couldn't get it from SpinButton
+ //_image_y = _settings->add_spinbutton(0, SPAttr::Y, _("Y:"), -DBL_MAX, DBL_MAX, 1, 1, 5, _("Y"));
+ _image_y = _settings->add_entry(SPAttr::Y, _("Position Y:"), _("Position Y"));
+ _image_y->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_y_changed));
+ _settings->add_entry(SPAttr::WIDTH, _("Width:"), _("Width"));
+ _settings->add_entry(SPAttr::HEIGHT, _("Height:"), _("Height"));
+
+ _settings->type(NR_FILTER_OFFSET);
+ _settings->add_checkbutton(false, SPAttr::PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive."));
+ _settings->add_spinscale(0, SPAttr::DX, _("Delta X:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted to the right"));
+ _settings->add_spinscale(0, SPAttr::DY, _("Delta Y:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted downwards"));
+
+ _settings->type(NR_FILTER_SPECULARLIGHTING);
+ _settings->add_color(/*default: white*/ 0xffffffff, SPAttr::LIGHTING_COLOR, _("Specular Color:"), _("Defines the color of the light source"));
+ _settings->add_spinscale(1, SPAttr::SURFACESCALE, _("Surface Scale:"), -5, 5, 0.1, 0.01, 2, _("This value amplifies the heights of the bump map defined by the input alpha channel"));
+ _settings->add_spinscale(1, SPAttr::SPECULARCONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model."));
+ _settings->add_spinscale(1, SPAttr::SPECULAREXPONENT, _("Exponent:"), 1, 50, 1, 0.01, 1, _("Exponent for specular term, larger is more \"shiny\"."));
+ // deprecated (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/kernelUnitLength)
+ // _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1);
+ _settings->add_lightsource();
+
+ _settings->type(NR_FILTER_TILE);
+ // add some filter primitive attributes: https://drafts.fxtf.org/filter-effects/#feTileElement
+ // issue: https://gitlab.com/inkscape/inkscape/-/issues/1417
+ _settings->add_entry(SPAttr::X, _("Position X:"), _("Position X"));
+ _settings->add_entry(SPAttr::Y, _("Position Y:"), _("Position Y"));
+ _settings->add_entry(SPAttr::WIDTH, _("Width:"), _("Width"));
+ _settings->add_entry(SPAttr::HEIGHT, _("Height:"), _("Height"));
+
+ _settings->type(NR_FILTER_TURBULENCE);
+// _settings->add_checkbutton(false, SPAttr::STITCHTILES, _("Stitch Tiles"), "stitch", "noStitch");
+ _settings->add_combo(TURBULENCE_TURBULENCE, SPAttr::TYPE, _("Type:"), TurbulenceTypeConverter, _("Indicates whether the filter primitive should perform a noise or turbulence function."));
+ _settings->add_dualspinscale(SPAttr::BASEFREQUENCY, _("Size:"), 0.001, 10, 0.001, 0.1, 3);
+ _settings->add_spinscale(1, SPAttr::NUMOCTAVES, _("Detail:"), 1, 10, 1, 1, 0);
+ _settings->add_spinscale(0, SPAttr::SEED, _("Seed:"), 0, 1000, 1, 1, 0, _("The starting number for the pseudo random number generator."));
+}
+
+void FilterEffectsDialog::add_filter_primitive(Filters::FilterPrimitiveType type) {
+ if (auto filter = _filter_modifier.get_selected_filter()) {
+ SPFilterPrimitive* prim = filter_add_primitive(filter, type);
+ _primitive_list.select(prim);
+ DocumentUndo::done(filter->document, _("Add filter primitive"), INKSCAPE_ICON("dialog-filters"));
+ }
+}
+
+void FilterEffectsDialog::add_primitive()
+{
+ add_filter_primitive(_add_primitive_type.get_active_data()->id);
+}
+
+void FilterEffectsDialog::duplicate_primitive()
+{
+ SPFilter* filter = _filter_modifier.get_selected_filter();
+ SPFilterPrimitive* origprim = _primitive_list.get_selected();
+
+ if (filter && origprim) {
+ Inkscape::XML::Node *repr;
+ repr = origprim->getRepr()->duplicate(origprim->getRepr()->document());
+ filter->getRepr()->appendChild(repr);
+
+ DocumentUndo::done(filter->document, _("Duplicate filter primitive"), INKSCAPE_ICON("dialog-filters"));
+
+ _primitive_list.update();
+ }
+}
+
+void FilterEffectsDialog::convolve_order_changed()
+{
+ _convolve_matrix->set_from_attribute(_primitive_list.get_selected());
+ // MultiSpinButtons orders widgets backwards: so use index 1 and 0
+ _convolve_target->get_spinbuttons()[1]->get_adjustment()->set_upper(_convolve_order->get_spinbutton1().get_value() - 1);
+ _convolve_target->get_spinbuttons()[0]->get_adjustment()->set_upper(_convolve_order->get_spinbutton2().get_value() - 1);
+}
+
+bool number_or_empy(const Glib::ustring& text) {
+ if (text.empty()) {
+ return true;
+ }
+ double n = g_strtod(text.c_str(), nullptr);
+ if (n == 0.0 && strcmp(text.c_str(), "0") != 0 && strcmp(text.c_str(), "0.0") != 0) {
+ return false;
+ }
+ else {
+ return true;
+ }
+}
+
+void FilterEffectsDialog::image_x_changed()
+{
+ if (number_or_empy(_image_x->get_text())) {
+ _image_x->set_from_attribute(_primitive_list.get_selected());
+ }
+}
+
+void FilterEffectsDialog::image_y_changed()
+{
+ if (number_or_empy(_image_y->get_text())) {
+ _image_y->set_from_attribute(_primitive_list.get_selected());
+ }
+}
+
+void FilterEffectsDialog::set_attr_direct(const AttrWidget* input)
+{
+ set_attr(_primitive_list.get_selected(), input->get_attribute(), input->get_as_attribute().c_str());
+}
+
+void FilterEffectsDialog::set_filternode_attr(const AttrWidget* input)
+{
+ if(!_locked) {
+ _attr_lock = true;
+ SPFilter *filter = _filter_modifier.get_selected_filter();
+ const gchar* name = (const gchar*)sp_attribute_name(input->get_attribute());
+ if (filter && name && filter->getRepr()){
+ filter->setAttributeOrRemoveIfEmpty(name, input->get_as_attribute());
+ filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
+ }
+ _attr_lock = false;
+ }
+}
+
+void FilterEffectsDialog::set_child_attr_direct(const AttrWidget* input)
+{
+ set_attr(_primitive_list.get_selected()->firstChild(), input->get_attribute(), input->get_as_attribute().c_str());
+}
+
+void FilterEffectsDialog::set_attr(SPObject* o, const SPAttr attr, const gchar* val)
+{
+ if(!_locked) {
+ _attr_lock = true;
+
+ SPFilter *filter = _filter_modifier.get_selected_filter();
+ const gchar* name = (const gchar*)sp_attribute_name(attr);
+ if(filter && name && o) {
+ update_settings_sensitivity();
+
+ o->setAttribute(name, val);
+ filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ Glib::ustring undokey = "filtereffects:";
+ undokey += name;
+ DocumentUndo::maybeDone(filter->document, undokey.c_str(), _("Set filter primitive attribute"), INKSCAPE_ICON("dialog-filters"));
+ }
+
+ _attr_lock = false;
+ }
+}
+
+void FilterEffectsDialog::update_filter_general_settings_view()
+{
+ if(_settings_initialized != true) return;
+
+ if(!_locked) {
+ _attr_lock = true;
+
+ SPFilter* filter = _filter_modifier.get_selected_filter();
+
+ if(filter) {
+ _filter_general_settings->show_and_update(0, filter);
+ _no_filter_selected.hide();
+ }
+ else {
+ std::vector<Gtk::Widget*> vect = _settings_filter.get_children();
+ vect[0]->hide();
+ _no_filter_selected.show();
+ }
+
+ _attr_lock = false;
+ }
+}
+
+void FilterEffectsDialog::update_settings_view()
+{
+ update_settings_sensitivity();
+
+ if (_attr_lock)
+ return;
+
+ // selected effect parameters
+
+ for (auto& i : _settings_effect.get_children()) {
+ i->hide();
+ }
+
+ SPFilterPrimitive* prim = _primitive_list.get_selected();
+ auto& header = get_widget<Gtk::Box>(_builder, "effect-header");
+ SPFilter* filter = _filter_modifier.get_selected_filter();
+ bool present = _filter_modifier.filters_present();
+
+ if (prim && prim->getRepr()) {
+ //XML Tree being used directly here while it shouldn't be.
+ auto id = FPConverter.get_id_from_key(prim->getRepr()->name());
+ _settings->show_and_update(id, prim);
+ _empty_settings.hide();
+ _cur_effect_name->set_text(_(FPConverter.get_label(id).c_str()));
+ header.show();
+ }
+ else {
+ if (filter) {
+ _empty_settings.set_text(_("Add effect from the search bar"));
+ }
+ else if (present) {
+ _empty_settings.set_text(_("Select a filter"));
+ }
+ else {
+ _empty_settings.set_text(_("No filters in the document"));
+ }
+ _empty_settings.show();
+ _cur_effect_name->set_text(Glib::ustring());
+ header.hide();
+ }
+
+ // current filter parameters (area size)
+
+ std::vector<Gtk::Widget*> vect2 = _settings_filter.get_children();
+ vect2[0]->hide();
+ _no_filter_selected.show();
+
+ if (filter) {
+ _filter_general_settings->show_and_update(0, filter);
+ _no_filter_selected.hide();
+ }
+
+ ensure_size();
+}
+
+void FilterEffectsDialog::update_settings_sensitivity()
+{
+ SPFilterPrimitive* prim = _primitive_list.get_selected();
+ const bool use_k = is<SPFeComposite>(prim) && cast<SPFeComposite>(prim)->get_composite_operator() == COMPOSITE_ARITHMETIC;
+ _k1->set_sensitive(use_k);
+ _k2->set_sensitive(use_k);
+ _k3->set_sensitive(use_k);
+ _k4->set_sensitive(use_k);
+}
+
+void FilterEffectsDialog::update_color_matrix()
+{
+ _color_matrix_values->set_from_attribute(_primitive_list.get_selected());
+}
+
+void FilterEffectsDialog::update_automatic_region(Gtk::CheckButton *btn)
+{
+ bool automatic = btn->get_active();
+ _region_pos->set_sensitive(!automatic);
+ _region_size->set_sensitive(!automatic);
+
+}
+
+} // 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/dialog/filter-effects-dialog.h b/src/ui/dialog/filter-effects-dialog.h
new file mode 100644
index 0000000..7965283
--- /dev/null
+++ b/src/ui/dialog/filter-effects-dialog.h
@@ -0,0 +1,355 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Filter Effects dialog
+ */
+/* Authors:
+ * Nicholas Bishop <nicholasbishop@gmail.com>
+ * Rodrigo Kumpera <kumpera@gmail.com>
+ * insaner
+ *
+ * Copyright (C) 2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H
+#define INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H
+
+#include <glibmm/property.h>
+#include <glibmm/propertyproxy.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeiter.h>
+#include <memory>
+#include <sigc++/connection.h>
+
+#include "attributes.h"
+#include "display/nr-filter-types.h"
+#include "helper/auto-connection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/combo-enums.h"
+#include "ui/widget/completion-popup.h"
+#include "ui/widget/spin-scale.h"
+#include "xml/helper-observer.h"
+
+class SPFilter;
+class SPFilterPrimitive;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class EntryAttr;
+class FileOrElementChooser;
+class DualSpinButton;
+class MultiSpinButton;
+
+class FilterEffectsDialog : public DialogBase
+{
+public:
+ FilterEffectsDialog();
+ ~FilterEffectsDialog() override;
+
+ void set_attrs_locked(const bool);
+protected:
+ void show_all_vfunc() override;
+private:
+
+ void documentReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+
+ Inkscape::auto_connection _resource_changed;
+
+ friend class FileOrElementChooser;
+
+ class FilterModifier : public Gtk::Box
+ {
+ public:
+ FilterModifier(FilterEffectsDialog& d, Glib::RefPtr<Gtk::Builder> builder);
+
+ void update_filters();
+ void update_selection(Selection *);
+
+ SPFilter* get_selected_filter();
+ void select_filter(const SPFilter*);
+ void add_filter();
+ bool is_selected_filter_active();
+ void toggle_current_filter();
+ bool filters_present() const;
+
+ sigc::signal<void ()>& signal_filter_changed()
+ {
+ return _signal_filter_changed;
+ }
+ sigc::signal<void ()>& signal_filters_updated() {
+ return _signal_filters_updated;
+ }
+
+ private:
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns()
+ {
+ add(filter);
+ add(label);
+ add(sel);
+ add(count);
+ }
+
+ Gtk::TreeModelColumn<SPFilter*> filter;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ Gtk::TreeModelColumn<int> sel;
+ Gtk::TreeModelColumn<int> count;
+ };
+
+ void on_filter_selection_changed();
+ void on_name_edited(const Glib::ustring&, const Glib::ustring&);
+ bool on_filter_move(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/);
+ void on_selection_toggled(const Glib::ustring&);
+ void selection_toggled(Gtk::TreeIter iter, bool toggle);
+
+ void update_counts();
+ void filter_list_button_release(GdkEventButton*);
+ void remove_filter();
+ void duplicate_filter();
+ void rename_filter();
+ void select_filter_elements();
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ FilterEffectsDialog& _dialog;
+ Gtk::TreeView& _list;
+ Glib::RefPtr<Gtk::ListStore> _filters_model;
+ Columns _columns;
+ Gtk::CellRendererToggle _cell_toggle;
+ Gtk::Button& _add;
+ Gtk::Button& _dup;
+ Gtk::Button& _del;
+ Gtk::Button& _select;
+ Gtk::Menu& _menu;
+ sigc::signal<void ()> _signal_filter_changed;
+ std::unique_ptr<Inkscape::XML::SignalObserver> _observer;
+ sigc::signal<void ()> _signal_filters_updated;
+ };
+
+ class PrimitiveColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ PrimitiveColumns()
+ {
+ add(primitive);
+ add(type_id);
+ add(type);
+ add(id);
+ }
+
+ Gtk::TreeModelColumn<SPFilterPrimitive*> primitive;
+ Gtk::TreeModelColumn<Inkscape::Filters::FilterPrimitiveType> type_id;
+ Gtk::TreeModelColumn<Glib::ustring> type;
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ };
+
+ class CellRendererConnection : public Gtk::CellRenderer
+ {
+ public:
+ CellRendererConnection();
+ Glib::PropertyProxy<void*> property_primitive();
+
+ static const int size_w = 16;
+ static const int size_h = 21;
+
+ protected:
+ void get_preferred_width_vfunc(Gtk::Widget& widget,
+ int& minimum_width,
+ int& natural_width) const override;
+
+ void get_preferred_width_for_height_vfunc(Gtk::Widget& widget,
+ int height,
+ int& minimum_width,
+ int& natural_width) const override;
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget,
+ int& minimum_height,
+ int& natural_height) const override;
+
+ void get_preferred_height_for_width_vfunc(Gtk::Widget& widget,
+ int width,
+ int& minimum_height,
+ int& natural_height) const override;
+ private:
+ // void* should be SPFilterPrimitive*, some weirdness with properties prevents this
+ Glib::Property<void*> _primitive;
+ };
+
+ class PrimitiveList : public Gtk::TreeView
+ {
+ public:
+ PrimitiveList(FilterEffectsDialog&);
+
+ sigc::signal<void ()>& signal_primitive_changed();
+
+ void update();
+ void set_menu(Gtk::Widget &parent,
+ sigc::slot<void ()> dup,
+ sigc::slot<void ()> rem);
+
+ SPFilterPrimitive* get_selected();
+ void select(SPFilterPrimitive *prim);
+ void remove_selected();
+ int primitive_count() const;
+ int get_input_type_width() const;
+ void set_inputs_count(int count);
+ int get_inputs_count() const;
+
+ protected:
+ bool on_draw_signal(const Cairo::RefPtr<Cairo::Context> &cr);
+
+
+ bool on_button_press_event(GdkEventButton*) override;
+ bool on_motion_notify_event(GdkEventMotion*) override;
+ bool on_button_release_event(GdkEventButton*) override;
+ void on_drag_end(const Glib::RefPtr<Gdk::DragContext>&) override;
+ private:
+ void init_text();
+
+ bool do_connection_node(const Gtk::TreeIter& row, const int input, std::vector<Gdk::Point>& points,
+ const int ix, const int iy);
+
+ const Gtk::TreeIter find_result(const Gtk::TreeIter& start, const SPAttr attr, int& src_id, const int pos);
+ int find_index(const Gtk::TreeIter& target);
+ void draw_connection(const Cairo::RefPtr<Cairo::Context>& cr,
+ const Gtk::TreeIter&, const SPAttr attr, const int text_start_x,
+ const int x1, const int y1, const int row_count, const int pos,
+ const Gdk::RGBA fg_color, const Gdk::RGBA mid_color);
+ void sanitize_connections(const Gtk::TreeIter& prim_iter);
+ void on_primitive_selection_changed();
+ bool on_scroll_timeout();
+
+ FilterEffectsDialog& _dialog;
+ Glib::RefPtr<Gtk::ListStore> _model;
+ PrimitiveColumns _columns;
+ CellRendererConnection _connection_cell;
+ Gtk::Menu *_primitive_menu;
+ Glib::RefPtr<Pango::Layout> _vertical_layout;
+ int _in_drag;
+ SPFilterPrimitive* _drag_prim;
+ sigc::signal<void ()> _signal_primitive_changed;
+ sigc::connection _scroll_connection;
+ int _autoscroll_y;
+ int _autoscroll_x;
+ std::unique_ptr<Inkscape::XML::SignalObserver> _observer;
+ int _input_type_width;
+ int _input_type_height;
+ int _inputs_count;
+ };
+
+ void init_settings_widgets();
+
+ // Handlers
+ void add_primitive();
+ void remove_primitive();
+ void duplicate_primitive();
+ void convolve_order_changed();
+ void image_x_changed();
+ void image_y_changed();
+ void add_filter_primitive(Filters::FilterPrimitiveType type);
+
+ void set_attr_direct(const UI::Widget::AttrWidget*);
+ void set_child_attr_direct(const UI::Widget::AttrWidget*);
+ void set_filternode_attr(const UI::Widget::AttrWidget*);
+ void set_attr(SPObject*, const SPAttr, const gchar* val);
+ void update_settings_view();
+ void update_filter_general_settings_view();
+ void update_settings_sensitivity();
+ void update_color_matrix();
+ void update_automatic_region(Gtk::CheckButton *btn);
+ void add_effects(Inkscape::UI::Widget::CompletionPopup& popup, bool symbolic);
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Glib::ustring _prefs = "/dialogs/filters";
+ Gtk::Paned& _paned;
+ Gtk::Grid& _main_grid;
+ Gtk::Box& _params_box;
+ Gtk::Box& _search_box;
+ Gtk::Box& _search_wide_box;
+ Gtk::ScrolledWindow& _filter_wnd;
+ bool _narrow_dialog = true;
+ Gtk::CheckButton& _cur_filter_btn;
+ sigc::connection _cur_filter_toggle;
+ // View/add primitives
+ Gtk::ScrolledWindow* _primitive_box;
+
+ UI::Widget::ComboBoxEnum<Inkscape::Filters::FilterPrimitiveType> _add_primitive_type;
+ Gtk::Button _add_primitive;
+
+ // Bottom pane (filter effect primitive settings)
+ Gtk::Box _settings_filter;
+ Gtk::Box _settings_effect;
+ Gtk::Label _empty_settings;
+ Gtk::Label _no_filter_selected;
+ Gtk::Label* _cur_effect_name;
+ bool _settings_initialized;
+
+ class Settings;
+ class MatrixAttr;
+ class ColorMatrixValues;
+ class ComponentTransferValues;
+ class LightSourceControl;
+ Settings* _settings;
+ Settings* _filter_general_settings;
+
+ // General settings
+ MultiSpinButton *_region_pos, *_region_size;
+
+ // Color Matrix
+ ColorMatrixValues* _color_matrix_values;
+
+ // Component Transfer
+ ComponentTransferValues* _component_transfer_values;
+
+ // Convolve Matrix
+ MatrixAttr* _convolve_matrix;
+ DualSpinButton* _convolve_order;
+ MultiSpinButton* _convolve_target;
+
+ // Image
+ EntryAttr* _image_x;
+ EntryAttr* _image_y;
+
+ // For controlling setting sensitivity
+ Gtk::Widget* _k1, *_k2, *_k3, *_k4;
+
+ // To prevent unwanted signals
+ bool _locked;
+ bool _attr_lock;
+
+ // These go last since they depend on the prior initialization of
+ // other FilterEffectsDialog members
+ FilterModifier _filter_modifier;
+ PrimitiveList _primitive_list;
+ Inkscape::UI::Widget::CompletionPopup _effects_popup;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_FILTER_EFFECTS_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/dialog/find.cpp b/src/ui/dialog/find.cpp
new file mode 100644
index 0000000..300ab4a
--- /dev/null
+++ b/src/ui/dialog/find.cpp
@@ -0,0 +1,1145 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-2006 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "find.h"
+
+#include <gtkmm/entry.h>
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/label.h>
+#include <gtkmm/sizegroup.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+#include "text-editing.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-ellipse.h"
+#include "object/sp-flowdiv.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-image.h"
+#include "object/sp-line.h"
+#include "object/sp-offset.h"
+#include "object/sp-path.h"
+#include "object/sp-polyline.h"
+#include "object/sp-rect.h"
+#include "object/sp-root.h"
+#include "object/sp-spiral.h"
+#include "object/sp-star.h"
+#include "object/sp-text.h"
+#include "object/sp-tref.h"
+#include "object/sp-tspan.h"
+#include "object/sp-use.h"
+
+#include "ui/icon-names.h"
+#include "ui/dialog-events.h"
+
+#include "xml/attribute-record.h"
+#include "xml/node-iterators.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Find::Find()
+ : DialogBase("/dialogs/find", "Find"),
+
+ entry_find(_("F_ind:"), _("Find objects by their content or properties (exact or partial match)")),
+ entry_replace(_("R_eplace:"), _("Replace match with this value")),
+
+ check_scope_all(_("_All")),
+ check_scope_layer(_("Current _layer")),
+ check_scope_selection(_("Sele_ction")),
+ check_searchin_text(_("_Text")),
+ check_searchin_property(_("_Properties")),
+ frame_searchin(_("Search in")),
+ frame_scope(_("Scope")),
+
+ check_case_sensitive(_("Case sensiti_ve")),
+ check_exact_match(_("E_xact match")),
+ check_include_hidden(_("Include _hidden")),
+ check_include_locked(_("Include loc_ked")),
+ expander_options(_("Options")),
+ frame_options(_("General")),
+
+ check_ids(_("_ID")),
+ check_attributename(_("Attribute _name")),
+ check_attributevalue(_("Attri_bute value")),
+ check_style(_("_Style")),
+ check_font(_("F_ont")),
+ check_desc(_("_Desc")),
+ check_title(_("Title")),
+ frame_properties(_("Properties")),
+
+ check_alltypes(_("All types")),
+ check_rects(_("Rectangles")),
+ check_ellipses(_("Ellipses")),
+ check_stars(_("Stars")),
+ check_spirals(_("Spirals")),
+ check_paths(_("Paths")),
+ check_texts(_("Texts")),
+ check_groups(_("Groups")),
+ check_clones(
+ //TRANSLATORS: "Clones" is a noun indicating type of object to find
+ C_("Find dialog", "Clones")),
+
+ check_images(_("Images")),
+ check_offsets(_("Offsets")),
+ frame_types(_("Object types")),
+
+ _left_size_group(Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL)),
+ _right_size_group(Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL)),
+
+ status(""),
+ button_find(_("_Find")),
+ button_replace(_("_Replace All")),
+ _action_replace(false),
+ blocked(false),
+
+ hbox_searchin(Gtk::ORIENTATION_HORIZONTAL),
+ vbox_scope(Gtk::ORIENTATION_VERTICAL),
+ vbox_searchin(Gtk::ORIENTATION_VERTICAL),
+ vbox_options1(Gtk::ORIENTATION_VERTICAL),
+ vbox_options2(Gtk::ORIENTATION_VERTICAL),
+ hbox_options(Gtk::ORIENTATION_HORIZONTAL),
+ vbox_expander(Gtk::ORIENTATION_VERTICAL),
+ hbox_properties(Gtk::ORIENTATION_HORIZONTAL),
+ vbox_properties1(Gtk::ORIENTATION_VERTICAL),
+ vbox_properties2(Gtk::ORIENTATION_VERTICAL),
+ vbox_types1(Gtk::ORIENTATION_VERTICAL),
+ vbox_types2(Gtk::ORIENTATION_VERTICAL),
+ hbox_types(Gtk::ORIENTATION_HORIZONTAL),
+ hboxbutton_row(Gtk::ORIENTATION_HORIZONTAL)
+
+{
+ label_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+ auto label1 = const_cast<Gtk::Label*>(entry_find.getLabel());
+ label_group->add_widget(*label1);
+ label1->set_xalign(0);
+ auto label2 = const_cast<Gtk::Label*>(entry_replace.getLabel());
+ label_group->add_widget(*label2);
+ label2->set_xalign(0);
+ const int MARGIN = 4;
+ set_margin_start(MARGIN);
+ set_margin_end(MARGIN);
+ entry_find.set_margin_top(MARGIN);
+ entry_replace.set_margin_top(MARGIN);
+ frame_searchin.set_margin_top(MARGIN);
+ frame_scope.set_margin_top(MARGIN);
+ button_find.set_use_underline();
+ button_find.set_tooltip_text(_("Select all objects matching the selection criteria"));
+ button_replace.set_use_underline();
+ button_replace.set_tooltip_text(_("Replace all matches"));
+ check_scope_all.set_use_underline();
+ check_scope_all.set_tooltip_text(_("Search in all layers"));
+ check_scope_layer.set_use_underline();
+ check_scope_layer.set_tooltip_text(_("Limit search to the current layer"));
+ check_scope_selection.set_use_underline();
+ check_scope_selection.set_tooltip_text(_("Limit search to the current selection"));
+ check_searchin_text.set_use_underline();
+ check_searchin_text.set_tooltip_text(_("Search in text objects"));
+ check_searchin_property.set_use_underline();
+ check_searchin_property.set_tooltip_text(_("Search in object properties, styles, attributes and IDs"));
+ check_case_sensitive.set_use_underline();
+ check_case_sensitive.set_tooltip_text(_("Match upper/lower case"));
+ check_case_sensitive.set_active(false);
+ check_exact_match.set_use_underline();
+ check_exact_match.set_tooltip_text(_("Match whole objects only"));
+ check_exact_match.set_active(false);
+ check_include_hidden.set_use_underline();
+ check_include_hidden.set_tooltip_text(_("Include hidden objects in search"));
+ check_include_hidden.set_active(false);
+ check_include_locked.set_use_underline();
+ check_include_locked.set_tooltip_text(_("Include locked objects in search"));
+ check_include_locked.set_active(false);
+ check_ids.set_use_underline();
+ check_ids.set_tooltip_text(_("Search ID name"));
+ check_ids.set_active(true);
+ check_attributename.set_use_underline();
+ check_attributename.set_tooltip_text(_("Search attribute name"));
+ check_attributename.set_active(false);
+ check_attributevalue.set_use_underline();
+ check_attributevalue.set_tooltip_text(_("Search attribute value"));
+ check_attributevalue.set_active(true);
+ check_style.set_use_underline();
+ check_style.set_tooltip_text(_("Search style"));
+ check_style.set_active(true);
+ check_font.set_use_underline();
+ check_font.set_tooltip_text(_("Search fonts"));
+ check_font.set_active(false);
+ check_desc.set_use_underline();
+ check_desc.set_tooltip_text(_("Search description"));
+ check_desc.set_active(false);
+ check_title.set_use_underline();
+ check_title.set_tooltip_text(_("Search title"));
+ check_title.set_active(false);
+ check_alltypes.set_use_underline();
+ check_alltypes.set_tooltip_text(_("Search all object types"));
+ check_alltypes.set_active(true);
+ check_rects.set_use_underline();
+ check_rects.set_tooltip_text(_("Search rectangles"));
+ check_rects.set_active(false);
+ check_ellipses.set_use_underline();
+ check_ellipses.set_tooltip_text(_("Search ellipses, arcs, circles"));
+ check_ellipses.set_active(false);
+ check_stars.set_use_underline();
+ check_stars.set_tooltip_text(_("Search stars and polygons"));
+ check_stars.set_active(false);
+ check_spirals.set_use_underline();
+ check_spirals.set_tooltip_text(_("Search spirals"));
+ check_spirals.set_active(false);
+ check_paths.set_use_underline();
+ check_paths.set_tooltip_text(_("Search paths, lines, polylines"));
+ check_paths.set_active(false);
+ check_texts.set_use_underline();
+ check_texts.set_tooltip_text(_("Search text objects"));
+ check_texts.set_active(false);
+ check_groups.set_use_underline();
+ check_groups.set_tooltip_text(_("Search groups"));
+ check_groups.set_active(false),
+ check_clones.set_use_underline();
+ check_clones.set_tooltip_text(_("Search clones"));
+ check_clones.set_active(false);
+ check_images.set_use_underline();
+ check_images.set_tooltip_text(_("Search images"));
+ check_images.set_active(false);
+ check_offsets.set_use_underline();
+ check_offsets.set_tooltip_text(_("Search offset objects"));
+ check_offsets.set_active(false);
+
+ entry_find.getEntry()->set_width_chars(25);
+ entry_find.child_property_fill(*entry_find.getEntry()) = true;
+ entry_find.child_property_expand(*entry_find.getEntry()) = true;
+ entry_replace.getEntry()->set_width_chars(25);
+ entry_replace.child_property_fill(*entry_replace.getEntry()) = true;
+ entry_replace.child_property_expand(*entry_replace.getEntry()) = true;
+
+ Gtk::RadioButtonGroup grp_searchin = check_searchin_text.get_group();
+ check_searchin_property.set_group(grp_searchin);
+ vbox_searchin.pack_start(check_searchin_text, Gtk::PACK_SHRINK);
+ vbox_searchin.pack_start(check_searchin_property, Gtk::PACK_SHRINK);
+ frame_searchin.add(vbox_searchin);
+
+ Gtk::RadioButtonGroup grp_scope = check_scope_all.get_group();
+ check_scope_layer.set_group(grp_scope);
+ check_scope_selection.set_group(grp_scope);
+ vbox_scope.pack_start(check_scope_all, Gtk::PACK_SHRINK);
+ vbox_scope.pack_start(check_scope_layer, Gtk::PACK_SHRINK);
+ vbox_scope.pack_start(check_scope_selection, Gtk::PACK_SHRINK);
+ hbox_searchin.set_spacing(12);
+ hbox_searchin.pack_start(frame_searchin, Gtk::PACK_SHRINK);
+ hbox_searchin.pack_start(frame_scope, Gtk::PACK_SHRINK);
+ frame_scope.add(vbox_scope);
+
+ vbox_options1.pack_start(check_case_sensitive, Gtk::PACK_SHRINK);
+ vbox_options1.pack_start(check_include_hidden, Gtk::PACK_SHRINK);
+ vbox_options2.pack_start(check_exact_match, Gtk::PACK_SHRINK);
+ vbox_options2.pack_start(check_include_locked, Gtk::PACK_SHRINK);
+ _left_size_group->add_widget(check_case_sensitive);
+ _left_size_group->add_widget(check_include_hidden);
+ _right_size_group->add_widget(check_exact_match);
+ _right_size_group->add_widget(check_include_locked);
+ hbox_options.set_spacing(4);
+ hbox_options.pack_start(vbox_options1, Gtk::PACK_SHRINK);
+ hbox_options.pack_start(vbox_options2, Gtk::PACK_SHRINK);
+ frame_options.add(hbox_options);
+
+ vbox_properties1.pack_start(check_ids, Gtk::PACK_SHRINK);
+ vbox_properties1.pack_start(check_style, Gtk::PACK_SHRINK);
+ vbox_properties1.pack_start(check_font, Gtk::PACK_SHRINK);
+ vbox_properties1.pack_start(check_desc, Gtk::PACK_SHRINK);
+ vbox_properties1.pack_start(check_title, Gtk::PACK_SHRINK);
+ vbox_properties2.pack_start(check_attributevalue, Gtk::PACK_SHRINK);
+ vbox_properties2.pack_start(check_attributename, Gtk::PACK_SHRINK);
+ vbox_properties2.set_valign(Gtk::ALIGN_START);
+ _left_size_group->add_widget(check_ids);
+ _left_size_group->add_widget(check_style);
+ _left_size_group->add_widget(check_font);
+ _left_size_group->add_widget(check_desc);
+ _left_size_group->add_widget(check_title);
+ _right_size_group->add_widget(check_attributevalue);
+ _right_size_group->add_widget(check_attributename);
+ hbox_properties.set_spacing(4);
+ hbox_properties.pack_start(vbox_properties1, Gtk::PACK_SHRINK);
+ hbox_properties.pack_start(vbox_properties2, Gtk::PACK_SHRINK);
+ frame_properties.add(hbox_properties);
+
+ vbox_types1.pack_start(check_alltypes, Gtk::PACK_SHRINK);
+ vbox_types1.pack_start(check_paths, Gtk::PACK_SHRINK);
+ vbox_types1.pack_start(check_texts, Gtk::PACK_SHRINK);
+ vbox_types1.pack_start(check_groups, Gtk::PACK_SHRINK);
+ vbox_types1.pack_start(check_clones, Gtk::PACK_SHRINK);
+ vbox_types1.pack_start(check_images, Gtk::PACK_SHRINK);
+ vbox_types2.pack_start(check_offsets, Gtk::PACK_SHRINK);
+ vbox_types2.pack_start(check_rects, Gtk::PACK_SHRINK);
+ vbox_types2.pack_start(check_ellipses, Gtk::PACK_SHRINK);
+ vbox_types2.pack_start(check_stars, Gtk::PACK_SHRINK);
+ vbox_types2.pack_start(check_spirals, Gtk::PACK_SHRINK);
+ vbox_types2.set_valign(Gtk::ALIGN_END);
+ _left_size_group->add_widget(check_alltypes);
+ _left_size_group->add_widget(check_paths);
+ _left_size_group->add_widget(check_texts);
+ _left_size_group->add_widget(check_groups);
+ _left_size_group->add_widget(check_clones);
+ _left_size_group->add_widget(check_images);
+ _right_size_group->add_widget(check_offsets);
+ _right_size_group->add_widget(check_rects);
+ _right_size_group->add_widget(check_ellipses);
+ _right_size_group->add_widget(check_stars);
+ _right_size_group->add_widget(check_spirals);
+ hbox_types.set_spacing(4);
+ hbox_types.pack_start(vbox_types1, Gtk::PACK_SHRINK);
+ hbox_types.pack_start(vbox_types2, Gtk::PACK_SHRINK);
+ frame_types.add(hbox_types);
+
+ vbox_expander.set_spacing(4);
+ vbox_expander.pack_start(frame_options, true, true);
+ vbox_expander.pack_start(frame_properties, true, true);
+ vbox_expander.pack_start(frame_types, true, true);
+
+ expander_options.set_use_underline();
+ expander_options.add(vbox_expander);
+
+ box_buttons.set_layout(Gtk::BUTTONBOX_END);
+ box_buttons.set_spacing(6);
+ box_buttons.pack_start(button_find, true, true);
+ box_buttons.pack_start(button_replace, true, true);
+ hboxbutton_row.set_spacing(6);
+ hboxbutton_row.pack_start(status, true, true);
+ hboxbutton_row.pack_end(box_buttons, true, true);
+
+ set_spacing(6);
+ pack_start(entry_find, false, false);
+ pack_start(entry_replace, false, false);
+ pack_start(hbox_searchin, false, false);
+ pack_start(expander_options, false, false);
+ pack_end(hboxbutton_row, false, false);
+
+ checkProperties.push_back(&check_ids);
+ checkProperties.push_back(&check_style);
+ checkProperties.push_back(&check_font);
+ checkProperties.push_back(&check_desc);
+ checkProperties.push_back(&check_title);
+ checkProperties.push_back(&check_attributevalue);
+ checkProperties.push_back(&check_attributename);
+
+ checkTypes.push_back(&check_paths);
+ checkTypes.push_back(&check_texts);
+ checkTypes.push_back(&check_groups);
+ checkTypes.push_back(&check_clones);
+ checkTypes.push_back(&check_images);
+ checkTypes.push_back(&check_offsets);
+ checkTypes.push_back(&check_rects);
+ checkTypes.push_back(&check_ellipses);
+ checkTypes.push_back(&check_stars);
+ checkTypes.push_back(&check_spirals);
+
+ // set signals to handle clicks
+ expander_options.property_expanded().signal_changed().connect(sigc::mem_fun(*this, &Find::onExpander));
+ button_find.signal_clicked().connect(sigc::mem_fun(*this, &Find::onFind));
+ button_replace.signal_clicked().connect(sigc::mem_fun(*this, &Find::onReplace));
+ check_searchin_text.signal_clicked().connect(sigc::mem_fun(*this, &Find::onSearchinText));
+ check_searchin_property.signal_clicked().connect(sigc::mem_fun(*this, &Find::onSearchinProperty));
+ check_alltypes.signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleAlltypes));
+
+ for (auto & checkProperty : checkProperties) {
+ checkProperty->signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleCheck));
+ }
+
+ for (auto & checkType : checkTypes) {
+ checkType->signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleCheck));
+ }
+
+ onSearchinText();
+ onToggleAlltypes();
+
+ show_all_children();
+
+ button_find.set_can_default();
+ //button_find.grab_default(); // activatable by Enter
+ entry_find.getEntry()->grab_focus();
+}
+
+void Find::desktopReplaced()
+{
+ if (auto selection = getSelection()) {
+ SPItem *item = selection->singleItem();
+ if (item && entry_find.getEntry()->get_text_length() == 0) {
+ Glib::ustring str = sp_te_get_string_multiline(item);
+ if (!str.empty()) {
+ entry_find.getEntry()->set_text(str);
+ }
+ }
+ }
+}
+
+void Find::selectionChanged(Selection *selection)
+{
+ if (!blocked) {
+ status.set_text("");
+ }
+}
+
+/*########################################################################
+# FIND helper functions
+########################################################################*/
+
+Glib::ustring Find::find_replace(const gchar *str, const gchar *find, const gchar *replace, bool exact, bool casematch, bool replaceall)
+{
+ Glib::ustring ustr = str;
+ Glib::ustring ufind = find;
+ gsize replace_length = Glib::ustring(replace).length();
+ if (!casematch) {
+ ufind = ufind.lowercase();
+ }
+ gsize n = find_strcmp_pos(ustr.c_str(), ufind.c_str(), exact, casematch);
+ while (n != std::string::npos) {
+ ustr.replace(n, ufind.length(), replace);
+ if (!replaceall) {
+ return ustr;
+ }
+ // Start the next search after the last replace character to avoid infinite loops (replace "a" with "aaa" etc)
+ n = find_strcmp_pos(ustr.c_str(), ufind.c_str(), exact, casematch, n + replace_length);
+ }
+ return ustr;
+}
+
+gsize Find::find_strcmp_pos(const gchar *str, const gchar *find, bool exact, bool casematch, gsize start/*=0*/)
+{
+ Glib::ustring ustr = str ? str : "";
+ Glib::ustring ufind = find;
+
+ if (!casematch) {
+ ustr = ustr.lowercase();
+ ufind = ufind.lowercase();
+ }
+
+ gsize pos = std::string::npos;
+ if (exact) {
+ if (ustr == ufind) {
+ pos = 0;
+ }
+ } else {
+ pos = ustr.find(ufind, start);
+ }
+
+ return pos;
+}
+
+
+bool Find::find_strcmp(const gchar *str, const gchar *find, bool exact, bool casematch)
+{
+ return (std::string::npos != find_strcmp_pos(str, find, exact, casematch));
+}
+
+bool Find::item_desc_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace)
+{
+ gchar* desc = item->desc();
+ bool found = find_strcmp(desc, text, exact, casematch);
+ if (found && replace) {
+ Glib::ustring r = find_replace(desc, text, entry_replace.getEntry()->get_text().c_str(), exact, casematch, replace);
+ item->setDesc(r.c_str());
+ }
+ g_free(desc);
+ return found;
+}
+
+bool Find::item_title_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace)
+{
+ gchar* title = item->title();
+ bool found = find_strcmp(title, text, exact, casematch);
+ if (found && replace) {
+ Glib::ustring r = find_replace(title, text, entry_replace.getEntry()->get_text().c_str(), exact, casematch, replace);
+ item->setTitle(r.c_str());
+ }
+ g_free(title);
+ return found;
+}
+
+bool Find::item_text_match (SPItem *item, const gchar *find, bool exact, bool casematch, bool replace/*=false*/)
+{
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ Glib::ustring item_text = sp_te_get_string_multiline(item);
+
+ if (!item_text.empty()) {
+ bool found = find_strcmp(item_text.c_str(), find, exact, casematch);
+
+ if (found && replace) {
+ Glib::ustring ufind = find;
+ if (!casematch) {
+ ufind = ufind.lowercase();
+ }
+
+ Inkscape::Text::Layout const *layout = te_get_layout (item);
+ if (!layout) {
+ return found;
+ }
+
+ Glib::ustring replace = entry_replace.getEntry()->get_text();
+ gsize n = find_strcmp_pos(item_text.c_str(), ufind.c_str(), exact, casematch);
+ static Inkscape::Text::Layout::iterator _begin_w;
+ static Inkscape::Text::Layout::iterator _end_w;
+ while (n != std::string::npos) {
+ _begin_w = layout->charIndexToIterator(n);
+ _end_w = layout->charIndexToIterator(n + ufind.length());
+ sp_te_replace(item, _begin_w, _end_w, replace.c_str());
+ item_text = sp_te_get_string_multiline (item);
+ n = find_strcmp_pos(item_text.c_str(), ufind.c_str(), exact, casematch, n + replace.length());
+ }
+ }
+
+ return found;
+ }
+ return false;
+}
+
+
+bool Find::item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace/*=false*/)
+{
+ if (!item->getRepr()) {
+ return false;
+ }
+
+ const gchar *item_id = item->getRepr()->attribute("id");
+ if (!item_id) {
+ return false;
+ }
+
+ bool found = find_strcmp(item_id, id, exact, casematch);
+
+ if (found && replace) {
+ gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str());
+ Glib::ustring new_item_style = find_replace(item_id, id, replace_text , exact, casematch, true);
+ if (new_item_style != item_id) {
+ item->setAttribute("id", new_item_style);
+ }
+ g_free(replace_text);
+ }
+
+ return found;
+}
+
+bool Find::item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/)
+{
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ gchar *item_style = g_strdup(item->getRepr()->attribute("style"));
+ if (item_style == nullptr) {
+ return false;
+ }
+
+ bool found = find_strcmp(item_style, text, exact, casematch);
+
+ if (found && replace) {
+ gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str());
+ Glib::ustring new_item_style = find_replace(item_style, text, replace_text , exact, casematch, true);
+ if (new_item_style != item_style) {
+ item->setAttribute("style", new_item_style);
+ }
+ g_free(replace_text);
+ }
+
+ g_free(item_style);
+ return found;
+}
+
+bool Find::item_attr_match(SPItem *item, const gchar *text, bool exact, bool /*casematch*/, bool replace/*=false*/)
+{
+ bool found = false;
+
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ gchar *attr_value = g_strdup(item->getRepr()->attribute(text));
+ if (exact) {
+ found = (attr_value != nullptr);
+ } else {
+ found = item->getRepr()->matchAttributeName(text);
+ }
+ g_free(attr_value);
+
+ // TODO - Rename attribute name ?
+ if (found && replace) {
+ found = false;
+ }
+
+ return found;
+}
+
+bool Find::item_attrvalue_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/)
+{
+ bool ret = false;
+
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ for (const auto & iter:item->getRepr()->attributeList()) {
+ const gchar* key = g_quark_to_string(iter.key);
+ gchar *attr_value = g_strdup(item->getRepr()->attribute(key));
+ bool found = find_strcmp(attr_value, text, exact, casematch);
+ if (found) {
+ ret = true;
+ }
+
+ if (found && replace) {
+ gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str());
+ Glib::ustring new_item_style = find_replace(attr_value, text, replace_text , exact, casematch, true);
+ if (new_item_style != attr_value) {
+ item->setAttribute(key, new_item_style);
+ }
+ }
+
+ g_free(attr_value);
+ }
+
+ return ret;
+}
+
+
+bool Find::item_font_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool /*replace*/ /*=false*/)
+{
+ bool ret = false;
+
+ if (item->getRepr() == nullptr) {
+ return false;
+ }
+
+ const gchar *item_style = item->getRepr()->attribute("style");
+ if (item_style == nullptr) {
+ return false;
+ }
+
+ std::vector<Glib::ustring> vFontTokenNames;
+ vFontTokenNames.emplace_back("font-family:");
+ vFontTokenNames.emplace_back("-inkscape-font-specification:");
+
+ std::vector<Glib::ustring> vStyleTokens = Glib::Regex::split_simple(";", item_style);
+ for (auto & vStyleToken : vStyleTokens) {
+ Glib::ustring token = vStyleToken;
+ for (const auto & vFontTokenName : vFontTokenNames) {
+ if ( token.find(vFontTokenName) != std::string::npos) {
+ Glib::ustring font1 = Glib::ustring(vFontTokenName).append(text);
+ bool found = find_strcmp(token.c_str(), font1.c_str(), exact, casematch);
+ if (found) {
+ ret = true;
+ if (_action_replace) {
+ gchar *replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str());
+ gchar *orig_str = g_strdup(token.c_str());
+ // Exact match fails since the "font-family:" is in the token, since the find was exact it still works with false below
+ Glib::ustring new_item_style = find_replace(orig_str, text, replace_text , false /*exact*/, casematch, true);
+ if (new_item_style != orig_str) {
+ vStyleToken = new_item_style;
+ }
+ g_free(orig_str);
+ g_free(replace_text);
+ }
+ }
+ }
+ }
+ }
+
+ if (ret && _action_replace) {
+ Glib::ustring new_item_style;
+ for (const auto & vStyleToken : vStyleTokens) {
+ new_item_style.append(vStyleToken).append(";");
+ }
+ new_item_style.erase(new_item_style.size()-1);
+ item->setAttribute("style", new_item_style);
+ }
+
+ return ret;
+}
+
+
+std::vector<SPItem*> Find::filter_fields (std::vector<SPItem*> &l, bool exact, bool casematch)
+{
+ Glib::ustring tmp = entry_find.getEntry()->get_text();
+ if (tmp.empty()) {
+ return l;
+ }
+ gchar* text = g_strdup(tmp.c_str());
+
+ std::vector<SPItem*> in = l;
+ std::vector<SPItem*> out;
+
+ if (check_searchin_text.get_active()) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_text_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_text_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+ else if (check_searchin_property.get_active()) {
+
+ bool ids = check_ids.get_active();
+ bool style = check_style.get_active();
+ bool font = check_font.get_active();
+ bool desc = check_desc.get_active();
+ bool title = check_title.get_active();
+ bool attrname = check_attributename.get_active();
+ bool attrvalue = check_attributevalue.get_active();
+
+ if (ids) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ if (item_id_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_id_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+
+ if (style) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_style_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)){
+ out.push_back(*i);
+ if (_action_replace) {
+ item_style_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+
+ if (attrname) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_attr_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_attr_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+
+ if (attrvalue) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_attrvalue_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(), *i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_attrvalue_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+
+ if (font) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_font_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(),*i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_font_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+ if (desc) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_desc_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(),*i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_desc_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+ if (title) {
+ for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_title_match(item, text, exact, casematch)) {
+ if (out.end()==find(out.begin(),out.end(),*i)) {
+ out.push_back(*i);
+ if (_action_replace) {
+ item_title_match(item, text, exact, casematch, _action_replace);
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ g_free(text);
+
+ return out;
+}
+
+
+bool Find::item_type_match (SPItem *item)
+{
+ bool all =check_alltypes.get_active();
+
+ if (is<SPRect>(item)) {
+ return ( all ||check_rects.get_active());
+
+ } else if (is<SPGenericEllipse>(item)) {
+ return ( all || check_ellipses.get_active());
+
+ } else if (is<SPStar>(item) || is<SPPolygon>(item)) {
+ return ( all || check_stars.get_active());
+
+ } else if (is<SPSpiral>(item)) {
+ return ( all || check_spirals.get_active());
+
+ } else if (is<SPPath>(item) || is<SPLine>(item) || is<SPPolyLine>(item)) {
+ return (all || check_paths.get_active());
+
+ } else if (is<SPText>(item) || is<SPTSpan>(item) ||
+ is<SPTRef>(item) ||
+ is<SPFlowtext>(item) || is<SPFlowdiv>(item) ||
+ is<SPFlowtspan>(item) || is<SPFlowpara>(item)) {
+ return (all || check_texts.get_active());
+
+ } else if (is<SPGroup>(item) &&
+ !getDesktop()->layerManager().isLayer(item)) { // never select layers!
+ return (all || check_groups.get_active());
+
+ } else if (is<SPUse>(item)) {
+ return (all || check_clones.get_active());
+
+ } else if (is<SPImage>(item)) {
+ return (all || check_images.get_active());
+
+ } else if (is<SPOffset>(item)) {
+ return (all || check_offsets.get_active());
+ }
+
+ return false;
+}
+
+std::vector<SPItem*> Find::filter_types (std::vector<SPItem*> &l)
+{
+ std::vector<SPItem*> n;
+ for (std::vector<SPItem*>::const_reverse_iterator i=l.rbegin(); l.rend() != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item_type_match(item)) {
+ n.push_back(*i);
+ }
+ }
+ return n;
+}
+
+
+std::vector<SPItem*> &Find::filter_list (std::vector<SPItem*> &l, bool exact, bool casematch)
+{
+ l = filter_types (l);
+ l = filter_fields (l, exact, casematch);
+ return l;
+}
+
+std::vector<SPItem*> &Find::all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked)
+{
+ if (is<SPDefs>(r)) {
+ return l; // we're not interested in items in defs
+ }
+
+ if (!strcmp(r->getRepr()->name(), "svg:metadata")) {
+ return l; // we're not interested in metadata
+ }
+
+ auto desktop = getDesktop();
+ for (auto& child: r->children) {
+ auto item = cast<SPItem>(&child);
+ if (item && !child.cloned && !desktop->layerManager().isLayer(item)) {
+ if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) {
+ l.insert(l.begin(),(SPItem*)&child);
+ }
+ }
+ l = all_items (&child, l, hidden, locked);
+ }
+ return l;
+}
+
+std::vector<SPItem*> &Find::all_selection_items (Inkscape::Selection *s, std::vector<SPItem*> &l, SPObject *ancestor, bool hidden, bool locked)
+{
+ auto desktop = getDesktop();
+ auto itemlist = s->items();
+ for (auto i=boost::rbegin(itemlist); boost::rend(itemlist) != i; ++i) {
+ SPObject *obj = *i;
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ if (item && !item->cloned && !desktop->layerManager().isLayer(item)) {
+ if (!ancestor || ancestor->isAncestorOf(item)) {
+ if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) {
+ l.push_back(*i);
+ }
+ }
+ }
+ if (!ancestor || ancestor->isAncestorOf(item)) {
+ l = all_items(item, l, hidden, locked);
+ }
+ }
+ return l;
+}
+
+
+
+/*########################################################################
+# BUTTON CLICK HANDLERS (callbacks)
+########################################################################*/
+
+void Find::onFind()
+{
+ _action_replace = false;
+ onAction();
+
+ // Return focus to the find entry
+ entry_find.getEntry()->grab_focus();
+}
+
+void Find::onReplace()
+{
+ if (entry_find.getEntry()->get_text().length() < 1) {
+ status.set_text(_("Nothing to replace"));
+ return;
+ }
+ _action_replace = true;
+ onAction();
+
+ // Return focus to the find entry
+ entry_find.getEntry()->grab_focus();
+}
+
+void Find::onAction()
+{
+ auto desktop = getDesktop();
+ bool hidden = check_include_hidden.get_active();
+ bool locked = check_include_locked.get_active();
+ bool exact = check_exact_match.get_active();
+ bool casematch = check_case_sensitive.get_active();
+ blocked = true;
+
+ std::vector<SPItem*> l;
+ if (check_scope_selection.get_active()) {
+ if (check_scope_layer.get_active()) {
+ l = all_selection_items (desktop->getSelection(), l, desktop->layerManager().currentLayer(), hidden, locked);
+ } else {
+ l = all_selection_items (desktop->getSelection(), l, nullptr, hidden, locked);
+ }
+ } else {
+ if (check_scope_layer.get_active()) {
+ l = all_items (desktop->layerManager().currentLayer(), l, hidden, locked);
+ } else {
+ l = all_items(desktop->getDocument()->getRoot(), l, hidden, locked);
+ }
+ }
+ guint all = l.size();
+
+ std::vector<SPItem*> n = filter_list (l, exact, casematch);
+
+ if (!n.empty()) {
+ int count = n.size();
+ desktop->messageStack()->flashF(Inkscape::NORMAL_MESSAGE,
+ // TRANSLATORS: "%s" is replaced with "exact" or "partial" when this string is displayed
+ ngettext("<b>%d</b> object found (out of <b>%d</b>), %s match.",
+ "<b>%d</b> objects found (out of <b>%d</b>), %s match.",
+ count),
+ count, all, exact? _("exact") : _("partial"));
+ if (_action_replace){
+ // TRANSLATORS: "%1" is replaced with the number of matches
+ status.set_text(Glib::ustring::compose(ngettext("%1 match replaced","%1 matches replaced",count), count));
+ }
+ else {
+ // TRANSLATORS: "%1" is replaced with the number of matches
+ status.set_text(Glib::ustring::compose(ngettext("%1 object found","%1 objects found",count), count));
+ bool attributenameyok = !check_attributename.get_active();
+ button_replace.set_sensitive(attributenameyok);
+ }
+
+ Inkscape::Selection *selection = desktop->getSelection();
+ selection->clear();
+ selection->setList(n);
+ SPObject *obj = n[0];
+ auto item = cast<SPItem>(obj);
+ g_assert(item != nullptr);
+ scroll_to_show_item(desktop, item);
+
+ if (_action_replace) {
+ DocumentUndo::done(desktop->getDocument(), _("Replace text or property"), INKSCAPE_ICON("draw-text"));
+ }
+
+ } else {
+ status.set_text(_("Nothing found"));
+ if (!check_scope_selection.get_active()) {
+ Inkscape::Selection *selection = desktop->getSelection();
+ selection->clear();
+ }
+ desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No objects found"));
+ }
+ blocked = false;
+
+}
+
+void Find::onToggleCheck ()
+{
+ bool objectok = false;
+ status.set_text("");
+
+ if (check_alltypes.get_active()) {
+ objectok = true;
+ }
+ for (auto & checkType : checkTypes) {
+ if (checkType->get_active()) {
+ objectok = true;
+ }
+ }
+
+ if (!objectok) {
+ status.set_text(_("Select an object type"));
+ }
+
+
+ bool propertyok = false;
+
+ if (!check_searchin_property.get_active()) {
+ propertyok = true;
+ } else {
+
+ for (auto & checkProperty : checkProperties) {
+ if (checkProperty->get_active()) {
+ propertyok = true;
+ }
+ }
+ }
+
+ if (!propertyok) {
+ status.set_text(_("Select a property"));
+ }
+
+ // Can't replace attribute names
+ // bool attributenameyok = !check_attributename.get_active();
+
+ button_find.set_sensitive(objectok && propertyok);
+ // button_replace.set_sensitive(objectok && propertyok && attributenameyok);
+ button_replace.set_sensitive(false);
+}
+
+void Find::onToggleAlltypes ()
+{
+ bool all =check_alltypes.get_active();
+ for (auto & checkType : checkTypes) {
+ checkType->set_sensitive(!all);
+ }
+
+ onToggleCheck();
+}
+
+void Find::onSearchinText ()
+{
+ searchinToggle(false);
+ onToggleCheck();
+}
+
+void Find::onSearchinProperty ()
+{
+ searchinToggle(true);
+ onToggleCheck();
+}
+
+void Find::searchinToggle(bool on)
+{
+ for (auto & checkProperty : checkProperties) {
+ checkProperty->set_sensitive(on);
+ }
+}
+
+void Find::onExpander ()
+{
+ if (!expander_options.get_expanded())
+ squeeze_window();
+}
+
+/*########################################################################
+# UTILITY
+########################################################################*/
+
+void Find::squeeze_window()
+{
+ // TODO: resize dialog window when the expander is closed
+ // set_size_request(-1, -1);
+}
+
+} // 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/dialog/find.h b/src/ui/dialog/find.h
new file mode 100644
index 0000000..fd2dc89
--- /dev/null
+++ b/src/ui/dialog/find.h
@@ -0,0 +1,313 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Find dialog
+ */
+/* Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004, 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_FIND_H
+#define INKSCAPE_UI_DIALOG_FIND_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/sizegroup.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/entry.h"
+#include "ui/widget/frame.h"
+
+class SPItem;
+class SPObject;
+
+namespace Inkscape {
+class Selection;
+
+namespace UI {
+namespace Dialog {
+
+/**
+ * The Find class defines the Find and replace dialog.
+ *
+ * The Find and replace dialog allows you to search within the
+ * current document for specific text or properties of items.
+ * Matches items are highlighted and can be replaced as well.
+ * Scope can be limited to the entire document, current layer or selected items.
+ * Other options allow searching on specific object types and properties.
+ */
+
+class Find : public DialogBase
+{
+public:
+ Find();
+ ~Find() override {};
+
+ void desktopReplaced() override;
+ void selectionChanged(Selection *selection) override;
+
+protected:
+
+
+ /**
+ * Callbacks for pressing the dialog buttons.
+ */
+ void onFind();
+ void onReplace();
+ void onExpander();
+ void onAction();
+ void onToggleAlltypes();
+ void onToggleCheck();
+ void onSearchinText();
+ void onSearchinProperty();
+
+ /**
+ * Toggle all the properties checkboxes
+ */
+ void searchinToggle(bool on);
+
+ /**
+ * Returns true if the SPItem 'item' has the same id
+ *
+ * @param item the SPItem to check
+ * @param id the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ */
+ bool item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if the SPItem 'item' has the same text content
+ *
+ * @param item the SPItem to check
+ * @param name the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ *
+ */
+ bool item_text_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if the SPItem 'item' has the same text in the style attribute
+ *
+ * @param item the SPItem to check
+ * @param name the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ */
+ bool item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if the SPItem 'item' has a <title> or <desc> child that
+ * matches
+ *
+ * @param item the SPItem to check
+ * @param name the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ */
+ bool item_desc_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false);
+ bool item_title_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if found the SPItem 'item' has the same attribute name
+ *
+ * @param item the SPItem to check
+ * @param name the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ */
+ bool item_attr_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if the SPItem 'item' has the same attribute value
+ *
+ * @param item the SPItem to check
+ * @param name the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ */
+ bool item_attrvalue_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false);
+ /**
+ * Returns true if the SPItem 'item' has the same font values
+ *
+ * @param item the SPItem to check
+ * @param name the value to compare with
+ * @param exact do an exact match
+ * @param casematch match the text case exactly
+ * @param replace replace the value if found
+ *
+ */
+ bool item_font_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false);
+ /**
+ * Function to filter a list of items based on the item type by calling each item_XXX_match function
+ */
+ std::vector<SPItem*> filter_fields (std::vector<SPItem*> &l, bool exact, bool casematch);
+ bool item_type_match (SPItem *item);
+ std::vector<SPItem*> filter_types (std::vector<SPItem*> &l);
+ std::vector<SPItem*> & filter_list (std::vector<SPItem*> &l, bool exact, bool casematch);
+
+ /**
+ * Find a string within a string and returns true if found with options for exact and casematching
+ */
+ bool find_strcmp(const gchar *str, const gchar *find, bool exact, bool casematch);
+
+ /**
+ * Find a string within a string and return the position with options for exact, casematching and search start location
+ */
+ gsize find_strcmp_pos(const gchar *str, const gchar *find, bool exact, bool casematch, gsize start=0);
+
+ /**
+ * Replace a string with another string with options for exact and casematching and replace once/all
+ */
+ Glib::ustring find_replace(const gchar *str, const gchar *find, const gchar *replace, bool exact, bool casematch, bool replaceall);
+
+ /**
+ * recursive function to return a list of all the items in the SPObject tree
+ *
+ */
+ std::vector<SPItem*> & all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked);
+ /**
+ * to return a list of all the selected items
+ *
+ */
+ std::vector<SPItem*> & all_selection_items (Inkscape::Selection *s, std::vector<SPItem*> &l, SPObject *ancestor, bool hidden, bool locked);
+
+ /**
+ * Shrink the dialog size when the expander widget is closed
+ * Currently not working, no known way to do this
+ */
+ void squeeze_window();
+
+private:
+ /*
+ * Find and replace combo box widgets
+ */
+ UI::Widget::Entry entry_find;
+ UI::Widget::Entry entry_replace;
+ Glib::RefPtr<Gtk::SizeGroup> label_group;
+
+ /**
+ * Scope and search in widgets
+ */
+ Gtk::RadioButton check_scope_all;
+ Gtk::RadioButton check_scope_layer;
+ Gtk::RadioButton check_scope_selection;
+ Gtk::RadioButton check_searchin_text;
+ Gtk::RadioButton check_searchin_property;
+ Gtk::Box hbox_searchin;
+ Gtk::Box vbox_scope;
+ Gtk::Box vbox_searchin;
+ UI::Widget::Frame frame_searchin;
+ UI::Widget::Frame frame_scope;
+
+ /**
+ * General option widgets
+ */
+ Gtk::CheckButton check_case_sensitive;
+ Gtk::CheckButton check_exact_match;
+ Gtk::CheckButton check_include_hidden;
+ Gtk::CheckButton check_include_locked;
+ Gtk::Box vbox_options1;
+ Gtk::Box vbox_options2;
+ Gtk::Box hbox_options;
+ Gtk::Box vbox_expander;
+ Gtk::Expander expander_options;
+ UI::Widget::Frame frame_options;
+
+ /**
+ * Property type widgets
+ */
+ Gtk::CheckButton check_ids;
+ Gtk::CheckButton check_attributename;
+ Gtk::CheckButton check_attributevalue;
+ Gtk::CheckButton check_style;
+ Gtk::CheckButton check_font;
+ Gtk::CheckButton check_desc;
+ Gtk::CheckButton check_title;
+ Gtk::Box hbox_properties;
+ Gtk::Box vbox_properties1;
+ Gtk::Box vbox_properties2;
+ UI::Widget::Frame frame_properties;
+
+ /**
+ * A vector of all the properties widgets for easy processing
+ */
+ std::vector<Gtk::CheckButton *> checkProperties;
+
+ /**
+ * Object type widgets
+ */
+ Gtk::CheckButton check_alltypes;
+ Gtk::CheckButton check_rects;
+ Gtk::CheckButton check_ellipses;
+ Gtk::CheckButton check_stars;
+ Gtk::CheckButton check_spirals;
+ Gtk::CheckButton check_paths;
+ Gtk::CheckButton check_texts;
+ Gtk::CheckButton check_groups;
+ Gtk::CheckButton check_clones;
+ Gtk::CheckButton check_images;
+ Gtk::CheckButton check_offsets;
+ Gtk::Box vbox_types1;
+ Gtk::Box vbox_types2;
+ Gtk::Box hbox_types;
+ UI::Widget::Frame frame_types;
+
+ Glib::RefPtr<Gtk::SizeGroup> _left_size_group;
+ Glib::RefPtr<Gtk::SizeGroup> _right_size_group;
+
+ /**
+ * A vector of all the check option widgets for easy processing
+ */
+ std::vector<Gtk::CheckButton *> checkTypes;
+
+ //Gtk::Box hbox_text;
+
+ /**
+ * Action Buttons and status
+ */
+ Gtk::Label status;
+ Gtk::Button button_find;
+ Gtk::Button button_replace;
+ Gtk::ButtonBox box_buttons;
+ Gtk::Box hboxbutton_row;
+
+ /**
+ * Finding or replacing
+ */
+ bool _action_replace;
+ bool blocked;
+
+ sigc::connection selectChangedConn;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_FIND_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/dialog/font-collections-manager.cpp b/src/ui/dialog/font-collections-manager.cpp
new file mode 100644
index 0000000..9e35831
--- /dev/null
+++ b/src/ui/dialog/font-collections-manager.cpp
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog to manage the font collections.
+ */
+/* Authors:
+ * Vaibhav Malik
+ *
+ * Released under GNU GPLv2 or later, read the file 'COPYING' for more information
+ */
+
+#include "font-collections-manager.h"
+
+#include "io/resource.h"
+#include "ui/icon-names.h"
+#include "util/font-collections.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+#include "libnrtype/font-lister.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+FontCollectionsManager::FontCollectionsManager()
+ : DialogBase("/dialogs/fontcollections", "FontCollections")
+{
+ std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-font-collections.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_error("Cannot load glade file: %s", ex.what().c_str());
+ throw;
+ }
+
+ builder->get_widget("contents", _contents);
+ builder->get_widget("paned", _paned);
+ builder->get_widget("collections_box", _collections_box);
+ builder->get_widget("buttons_box", _buttons_box);
+ builder->get_widget("font_list_box", _font_list_box);
+ builder->get_widget("font_count_label", _font_count_label);
+ builder->get_widget("font_list_filter_box", _font_list_filter_box);
+ builder->get_widget("search_entry", _search_entry);
+ builder->get_widget("reset_button", _reset_button);
+ builder->get_widget("create_button", _create_button);
+ builder->get_widget("edit_button", _edit_button);
+ builder->get_widget("delete_button", _delete_button);
+
+ _font_list_box->pack_start(_font_selector, true, true);
+ _font_list_box->reorder_child(_font_selector, 1);
+
+ _collections_box->pack_start(_user_font_collections, true, true);
+ _collections_box->reorder_child(_user_font_collections, 0);
+
+ _user_font_collections.populate_system_collections();
+ _user_font_collections.populate_user_collections();
+ _user_font_collections.change_frame_name(_("Font Collections"));
+
+ add(*_contents);
+
+ // Set the button images.
+ _create_button->set_image_from_icon_name(INKSCAPE_ICON("list-add"));
+ _edit_button->set_image_from_icon_name(INKSCAPE_ICON("document-edit"));
+ _delete_button->set_image_from_icon_name(INKSCAPE_ICON("edit-delete"));
+
+ // Paned settings.
+ _paned->child_property_resize(*_paned->get_child1()) = false;
+ _paned->child_property_resize(*_paned->get_child2()) = true;
+
+ change_font_count_label();
+ _font_selector.hide_others();
+ show_all_children();
+
+ // Setup the signals.
+ _font_count_changed_connection = Inkscape::FontLister::get_instance()->connectUpdate(sigc::mem_fun(*this, &FontCollectionsManager::change_font_count_label));
+ _search_entry->signal_search_changed().connect([=](){ on_search_entry_changed(); });
+ _user_font_collections.connect_signal_changed([=](int s){ on_selection_changed(s); });
+ _create_button->signal_clicked().connect([=](){ on_create_button_pressed(); });
+ _edit_button->signal_clicked().connect([=](){ on_edit_button_pressed(); });
+ _delete_button->signal_clicked().connect([=](){ on_delete_button_pressed(); });
+ _reset_button->signal_clicked().connect([=](){ on_reset_button_pressed(); });
+
+ // Edit and delete are initially insensitive because nothing is selected.
+ _edit_button->set_sensitive(false);
+ _delete_button->set_sensitive(false);
+}
+
+void FontCollectionsManager::on_search_entry_changed()
+{
+ auto search_txt = _search_entry->get_text();
+ _font_selector.unset_model();
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ font_lister->show_results(search_txt);
+ _font_selector.set_model();
+ change_font_count_label();
+}
+
+void FontCollectionsManager::on_create_button_pressed()
+{
+ _user_font_collections.on_create_collection();
+}
+
+void FontCollectionsManager::on_delete_button_pressed()
+{
+ _user_font_collections.on_delete_button_pressed();
+}
+
+void FontCollectionsManager::on_edit_button_pressed()
+{
+ _user_font_collections.on_edit_button_pressed();
+}
+
+void FontCollectionsManager::on_reset_button_pressed()
+{
+ _search_entry->set_text("");
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ if(font_lister->get_font_families_size() == font_lister->get_font_list()->children().size()) {
+ // _user_font_collections.populate_collections();
+ return;
+ }
+
+ Inkscape::FontCollections::get()->clear_selected_collections();
+ font_lister->init_font_families();
+ font_lister->init_default_styles();
+ SPDocument *document = getDesktop()->getDocument();
+ font_lister->add_document_fonts_at_top(document);
+}
+
+void FontCollectionsManager::change_font_count_label()
+{
+ auto label = Inkscape::FontLister::get_instance()->get_font_count_label();
+ _font_count_label->set_label(label);
+}
+
+// This function will set the sensitivity of the edit and delete buttons
+// Whenever the selection changes.
+void FontCollectionsManager::on_selection_changed(int state)
+{
+ bool edit = false, del = false;
+ switch(state) {
+ case SYSTEM_COLLECTION:
+ break;
+ case USER_COLLECTION:
+ edit = true;
+ del = true;
+ break;
+ case USER_COLLECTION_FONT:
+ edit = false;
+ del = true;
+ default:
+ break;
+ }
+ _edit_button->set_sensitive(edit);
+ _delete_button->set_sensitive(del);
+}
+
+} // 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/dialog/font-collections-manager.h b/src/ui/dialog/font-collections-manager.h
new file mode 100644
index 0000000..d974e58
--- /dev/null
+++ b/src/ui/dialog/font-collections-manager.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Text-edit
+ */
+/* Authors:
+ * Vaihav Malik <vaibhavmalik2018@gmail.com>
+ *
+ * Copyright (C) 1999-2013 Authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_FONT_COLLECTIONS_MANAGER_H
+#define INKSCAPE_UI_DIALOG_FONT_COLLECTIONS_MANAGER_H
+
+#include "helper/auto-connection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/font-selector.h"
+#include "ui/widget/font-collection-selector.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * The font collections manager dialog allows the user to:
+ * 1. Create
+ * 2. Read
+ * 3. Update
+ * 4. Delete
+ * the font collections and the fonts associated with each collection.
+ *
+ * User can add new fonts in font collections by dragging the fonts from the
+ * font list and dropping them a user font collection.
+ */
+class FontCollectionsManager : public DialogBase
+{
+public:
+ enum SelectionStates {SYSTEM_COLLECTION = -1, USER_COLLECTION, USER_COLLECTION_FONT};
+
+ FontCollectionsManager();
+
+private:
+ void on_search_entry_changed();
+ void on_create_button_pressed();
+ void on_edit_button_pressed();
+ void on_delete_button_pressed();
+ void on_reset_button_pressed();
+ void change_font_count_label();
+ void on_selection_changed(int state);
+
+ /*
+ * All the dialogs widgets
+ */
+ Gtk::Box *_contents;
+ Gtk::Paned *_paned;
+ Gtk::Box *_collections_box;
+ Gtk::Box *_buttons_box;
+ Gtk::Box *_font_list_box;
+ Gtk::Label *_font_count_label;
+ Gtk::Box *_font_list_filter_box;
+ Gtk::SearchEntry *_search_entry;
+ Gtk::Button *_reset_button;
+ Gtk::Button *_create_button;
+ Gtk::Button *_edit_button;
+ Gtk::Button *_delete_button;
+ Inkscape::UI::Widget::FontSelector _font_selector;
+ Inkscape::UI::Widget::FontCollectionSelector _user_font_collections;
+
+ // Signals
+ auto_connection _font_count_changed_connection;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_FONT_COLLECTIONS_MANAGER_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/dialog/font-substitution.cpp b/src/ui/dialog/font-substitution.cpp
new file mode 100644
index 0000000..0e34b84
--- /dev/null
+++ b/src/ui/dialog/font-substitution.cpp
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Authors:
+ *
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <set>
+#include <vector>
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+#include <glibmm/ustring.h>
+
+#include <gtkmm/messagedialog.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+
+#include "font-substitution.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "inkscape.h"
+#include "selection-chemistry.h"
+#include "text-editing.h"
+
+#include "object/sp-item.h"
+#include "object/sp-root.h"
+#include "object/sp-text.h"
+#include "object/sp-textpath.h"
+#include "object/sp-flowdiv.h"
+#include "object/sp-tspan.h"
+
+#include "libnrtype/font-factory.h"
+#include "libnrtype/font-instance.h"
+
+#include "ui/dialog-events.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+namespace {
+
+void show(std::vector<SPItem*> const &list, Glib::ustring const &out)
+{
+ Gtk::MessageDialog warning(_("Some fonts are not available and have been substituted."),
+ false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, true);
+ warning.set_resizable(true);
+ warning.set_title(_("Font substitution"));
+
+ sp_transientize(GTK_WIDGET(warning.gobj()));
+
+ Gtk::TextView textview;
+ textview.set_editable(false);
+ textview.set_wrap_mode(Gtk::WRAP_WORD);
+ textview.show();
+ textview.get_buffer()->set_text(_(out.c_str()));
+
+ Gtk::ScrolledWindow scrollwindow;
+ scrollwindow.add(textview);
+ scrollwindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scrollwindow.set_shadow_type(Gtk::SHADOW_IN);
+ scrollwindow.set_size_request(0, 100);
+ scrollwindow.show();
+
+ Gtk::CheckButton cbSelect;
+ cbSelect.set_label(_("Select all the affected items"));
+ cbSelect.set_active(true);
+ cbSelect.show();
+
+ Gtk::CheckButton cbWarning;
+ cbWarning.set_label(_("Don't show this warning again"));
+ cbWarning.show();
+
+ auto box = warning.get_content_area();
+ box->set_border_width(5);
+ box->set_spacing(2);
+ box->pack_start(scrollwindow, true, true, 4);
+ box->pack_start(cbSelect, false, false, 0);
+ box->pack_start(cbWarning, false, false, 0);
+
+ warning.run();
+
+ if (cbWarning.get_active()) {
+ Inkscape::Preferences::get()->setBool("/options/font/substitutedlg", false);
+ }
+
+ if (cbSelect.get_active()) {
+ auto desktop = SP_ACTIVE_DESKTOP;
+ auto selection = desktop->getSelection();
+ selection->clear();
+ selection->setList(list);
+ }
+}
+
+/*
+ * Find all the fonts that are in the document but not available on the user's system
+ * and have been substituted for other fonts.
+ *
+ * Return a list of SPItems where fonts have been substituted.
+ *
+ * Walk through all the objects ...
+ * a. Build up a list of the objects with fonts defined in the style attribute
+ * b. Build up a list of the objects rendered fonts - taken for the objects layout/spans
+ * If there are fonts in a. that are not in b. then those fonts have been substituted.
+ */
+std::pair<std::vector<SPItem*>, Glib::ustring> getFontReplacedItems(SPDocument *doc)
+{
+ std::vector<SPItem*> outList;
+ std::set<Glib::ustring> setErrors;
+ std::set<Glib::ustring> setFontSpans;
+ std::map<SPItem*, Glib::ustring> mapFontStyles;
+ Glib::ustring out;
+
+ auto const allList = get_all_items(doc->getRoot(), SP_ACTIVE_DESKTOP, false, false, true);
+ for (auto item : allList) {
+ auto style = item->style;
+ Glib::ustring family = "";
+
+ if (is_top_level_text_object(item)) {
+ // Should only need to check the first span, since the others should be covered by TSPAN's etc
+ family = te_get_layout(item)->getFontFamily(0);
+ setFontSpans.insert(family);
+ }
+ else if (auto textpath = cast<SPTextPath>(item)) {
+ if (textpath->originalPath) {
+ family = cast<SPText>(item->parent)->layout.getFontFamily(0);
+ setFontSpans.insert(family);
+ }
+ }
+ else if (is<SPTSpan>(item) || is<SPFlowtspan>(item)) {
+ // is_part_of_text_subtree (item)
+ // TSPAN layout comes from the parent->layout->_spans
+ SPObject *parent_text = item;
+ while (parent_text && !is<SPText>(parent_text)) {
+ parent_text = parent_text->parent;
+ }
+ if (parent_text) {
+ family = cast<SPText>(parent_text)->layout.getFontFamily(0);
+ // Add all the spans fonts to the set
+ for (unsigned int f = 0; f < parent_text->children.size(); f++) {
+ family = cast<SPText>(parent_text)->layout.getFontFamily(f);
+ setFontSpans.insert(family);
+ }
+ }
+ }
+
+ if (style) {
+ char const *style_font = nullptr;
+ if (style->font_family.set) {
+ style_font = style->font_family.value();
+ } else if (style->font_specification.set) {
+ style_font = style->font_specification.value();
+ } else {
+ style_font = style->font_family.value();
+ }
+
+ if (style_font) {
+ if (has_visible_text(item)) {
+ mapFontStyles.insert(std::make_pair(item, style_font));
+ }
+ }
+ }
+ }
+
+ // Check if any document styles are not in the actual layout
+ for (auto mapIter = mapFontStyles.rbegin(); mapIter != mapFontStyles.rend(); ++mapIter) {
+ SPItem *item = mapIter->first;
+ Glib::ustring fonts = mapIter->second;
+
+ // CSS font fallbacks can have more that one font listed, split the font list
+ std::vector<Glib::ustring> vFonts = Glib::Regex::split_simple("," , fonts);
+ bool fontFound = false;
+ for (auto const &font : vFonts) {
+ // trim whitespace
+ size_t startpos = font.find_first_not_of(" \n\r\t");
+ size_t endpos = font.find_last_not_of(" \n\r\t");
+ if (startpos == std::string::npos || endpos == std::string::npos) {
+ continue; // empty font name
+ }
+ auto const trimmed = font.substr(startpos, endpos - startpos + 1);
+ if (setFontSpans.find(trimmed) != setFontSpans.end() ||
+ trimmed == Glib::ustring("sans-serif") ||
+ trimmed == Glib::ustring("Sans") ||
+ trimmed == Glib::ustring("serif") ||
+ trimmed == Glib::ustring("Serif") ||
+ trimmed == Glib::ustring("monospace") ||
+ trimmed == Glib::ustring("Monospace"))
+ {
+ fontFound = true;
+ break;
+ }
+ }
+ if (!fontFound) {
+ Glib::ustring subName = getSubstituteFontName(fonts);
+ Glib::ustring err = Glib::ustring::compose(_("Font '%1' substituted with '%2'"), fonts.c_str(), subName.c_str());
+ setErrors.insert(err);
+ outList.emplace_back(item);
+ }
+ }
+
+ for (auto const &err : setErrors) {
+ out.append(err + "\n");
+ g_warning("%s", err.c_str());
+ }
+
+ return std::make_pair(std::move(outList), std::move(out));
+}
+
+} // namespace
+
+void checkFontSubstitutions(SPDocument *doc)
+{
+ bool show_dlg = Inkscape::Preferences::get()->getBool("/options/font/substitutedlg");
+ if (!show_dlg) {
+ return;
+ }
+
+ auto [list, msg] = getFontReplacedItems(doc);
+ if (!msg.empty()) {
+ show(list, msg);
+ }
+}
+
+} // 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/dialog/font-substitution.h b/src/ui/dialog/font-substitution.h
new file mode 100644
index 0000000..b13de12
--- /dev/null
+++ b/src/ui/dialog/font-substitution.h
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief FontSubstitution dialog
+ */
+/* Authors:
+ *
+ *
+ * Copyright (C) 2012 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_FONT_SUBSTITUTION_H
+#define INKSCAPE_UI_DIALOG_FONT_SUBSTITUTION_H
+
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+void checkFontSubstitutions(SPDocument *doc);
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_FONT_SUBSTITUTION_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/dialog/global-palettes.cpp b/src/ui/dialog/global-palettes.cpp
new file mode 100644
index 0000000..27b337e
--- /dev/null
+++ b/src/ui/dialog/global-palettes.cpp
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "global-palettes.h"
+
+#include <iomanip>
+
+// Using Glib::regex because
+// - std::regex is too slow in debug mode.
+// - boost::regex requires a library not present in the CI image.
+#include <glibmm/i18n.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/regex.h>
+
+#include "io/resource.h"
+#include "io/sys.h"
+
+Inkscape::UI::Dialog::PaletteFileData::PaletteFileData(Glib::ustring const &path)
+{
+ name = Glib::path_get_basename(path);
+ columns = 1;
+ user = Inkscape::IO::file_is_writable(path.c_str());
+
+ auto f = std::unique_ptr<FILE, void(*)(FILE*)>(Inkscape::IO::fopen_utf8name(path.c_str(), "r"), [] (FILE *f) {if (f) std::fclose(f);});
+ if (!f) throw std::runtime_error("Failed to open file");
+
+ char buf[1024];
+ if (!std::fgets(buf, sizeof(buf), f.get())) throw std::runtime_error("File is empty");
+ if (std::strncmp("GIMP Palette", buf, 12) != 0) throw std::runtime_error("First line is wrong");
+
+ static auto const regex_rgb = Glib::Regex::create("\\s*(\\d+)\\s+(\\d+)\\s+(\\d+)\\s*(?:\\s(.*\\S)\\s*)?$", Glib::REGEX_OPTIMIZE | Glib::REGEX_ANCHORED);
+ static auto const regex_name = Glib::Regex::create("\\s*Name:\\s*(.*\\S)", Glib::REGEX_OPTIMIZE | Glib::REGEX_ANCHORED);
+ static auto const regex_cols = Glib::Regex::create("\\s*Columns:\\s*(.*\\S)", Glib::REGEX_OPTIMIZE | Glib::REGEX_ANCHORED);
+ static auto const regex_blank = Glib::Regex::create("\\s*(?:$|#)", Glib::REGEX_OPTIMIZE | Glib::REGEX_ANCHORED);
+
+ while (std::fgets(buf, sizeof(buf), f.get())) {
+ auto line = Glib::ustring(buf); // Unnecessary copy required until using a glibmm with support for string views. TODO: Fix when possible.
+ Glib::MatchInfo match;
+ if (regex_rgb->match(line, match)) { // ::regex_match(line, match, boost::regex(), boost::regex_constants::match_continuous)) {
+ // RGB color, followed by an optional name.
+ Color color;
+ for (int i = 0; i < 3; i++) {
+ color.rgb[i] = std::clamp(std::stoi(match.fetch(i + 1)), 0, 255);
+ }
+ color.name = match.fetch(4);
+
+ if (!color.name.empty()) {
+ // Translate the name if present.
+ color.name = g_dpgettext2(nullptr, "Palette", color.name.c_str());
+ } else {
+ // Otherwise, set the name to be the hex value.
+ color.name = Glib::ustring::compose("#%1%2%3",
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), color.rgb[0]),
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), color.rgb[1]),
+ Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), color.rgb[2])
+ ).uppercase();
+ }
+
+ colors.emplace_back(std::move(color));
+ } else if (regex_name->match(line, match)) {
+ // Header entry for name.
+ name = match.fetch(1);
+ } else if (regex_cols->match(line, match)) {
+ // Header entry for columns.
+ columns = std::clamp(std::stoi(match.fetch(1)), 1, 1000);
+ } else if (regex_blank->match(line, match)) {
+ // Comment or blank line.
+ } else {
+ // Unrecognised.
+ throw std::runtime_error("Invalid line " + std::string(line));
+ }
+ }
+}
+
+Inkscape::UI::Dialog::GlobalPalettes::GlobalPalettes()
+{
+ // Load the palettes.
+ for (auto &path : Inkscape::IO::Resource::get_filenames(Inkscape::IO::Resource::PALETTES, {".gpl"})) {
+ try {
+ palettes.emplace_back(path);
+ } catch (std::runtime_error const &e) {
+ g_warning("Error loading palette %s: %s", path.c_str(), e.what());
+ } catch (std::logic_error const &e) {
+ g_warning("Error loading palette %s: %s", path.c_str(), e.what());
+ }
+ }
+
+ std::sort(palettes.begin(), palettes.end(), [] (decltype(palettes)::const_reference a, decltype(palettes)::const_reference b) {
+ // Sort by user/system first...
+ if (a.user > b.user) return true;
+ if (b.user > a.user) return false;
+ // ... then by name.
+ return a.name.compare(b.name) < 0;
+ });
+}
+
+Inkscape::UI::Dialog::GlobalPalettes const &Inkscape::UI::Dialog::GlobalPalettes::get()
+{
+ static GlobalPalettes instance;
+ return instance;
+}
diff --git a/src/ui/dialog/global-palettes.h b/src/ui/dialog/global-palettes.h
new file mode 100644
index 0000000..f0a1fe0
--- /dev/null
+++ b/src/ui/dialog/global-palettes.h
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Global color palette information.
+ */
+/* Authors: PBS <pbs3141@gmail.com>
+ * Copyright (C) 2022 PBS
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_GLOBAL_PALETTES_H
+#define INKSCAPE_UI_DIALOG_GLOBAL_PALETTES_H
+
+#include <array>
+#include <vector>
+#include <glibmm/ustring.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * The data loaded from a palette file.
+ */
+struct PaletteFileData
+{
+ /// Name of the palette, either specified in the file or taken from the filename.
+ Glib::ustring name;
+
+ /// The preferred number of columns (unused).
+ int columns;
+
+ /// Whether this is a user or system palette.
+ bool user;
+
+ struct Color
+ {
+ /// RGB color.
+ std::array<unsigned, 3> rgb;
+
+ /// Name of the color, either specified in the file or generated from the rgb.
+ Glib::ustring name;
+ };
+
+ /// The list of colors in the palette.
+ std::vector<Color> colors;
+
+ /// Load from the given file, throwing std::runtime_error on fail.
+ PaletteFileData(Glib::ustring const &path);
+};
+
+/**
+ * Singleton class that manages the static list of global palettes.
+ */
+class GlobalPalettes
+{
+ GlobalPalettes();
+public:
+ static GlobalPalettes const &get();
+ std::vector<PaletteFileData> palettes;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_GLOBAL_PALETTES_H
diff --git a/src/ui/dialog/glyphs.cpp b/src/ui/dialog/glyphs.cpp
new file mode 100644
index 0000000..620f385
--- /dev/null
+++ b/src/ui/dialog/glyphs.cpp
@@ -0,0 +1,777 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Jon A. Cruz
+ * Abhishek Sharma
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <vector>
+
+#include "glyphs.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h" // for SPDocumentUndo::done()
+#include "selection.h"
+#include "text-editing.h"
+
+#include "libnrtype/font-instance.h"
+#include "libnrtype/font-lister.h"
+#include "libnrtype/font-factory.h"
+
+#include "object/sp-flowtext.h"
+#include "object/sp-text.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/font-selector.h"
+#include "ui/widget/scrollprotected.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+static std::map<GUnicodeScript, Glib::ustring> & getScriptToName()
+{
+ static bool init = false;
+ static std::map<GUnicodeScript, Glib::ustring> mappings;
+ if (!init) {
+ init = true;
+ mappings[G_UNICODE_SCRIPT_INVALID_CODE] = _("all");
+ mappings[G_UNICODE_SCRIPT_COMMON] = _("common");
+ mappings[G_UNICODE_SCRIPT_INHERITED] = _("inherited");
+ mappings[G_UNICODE_SCRIPT_ARABIC] = _("Arabic");
+ mappings[G_UNICODE_SCRIPT_ARMENIAN] = _("Armenian");
+ mappings[G_UNICODE_SCRIPT_BENGALI] = _("Bengali");
+ mappings[G_UNICODE_SCRIPT_BOPOMOFO] = _("Bopomofo");
+ mappings[G_UNICODE_SCRIPT_CHEROKEE] = _("Cherokee");
+ mappings[G_UNICODE_SCRIPT_COPTIC] = _("Coptic");
+ mappings[G_UNICODE_SCRIPT_CYRILLIC] = _("Cyrillic");
+ mappings[G_UNICODE_SCRIPT_DESERET] = _("Deseret");
+ mappings[G_UNICODE_SCRIPT_DEVANAGARI] = _("Devanagari");
+ mappings[G_UNICODE_SCRIPT_ETHIOPIC] = _("Ethiopic");
+ mappings[G_UNICODE_SCRIPT_GEORGIAN] = _("Georgian");
+ mappings[G_UNICODE_SCRIPT_GOTHIC] = _("Gothic");
+ mappings[G_UNICODE_SCRIPT_GREEK] = _("Greek");
+ mappings[G_UNICODE_SCRIPT_GUJARATI] = _("Gujarati");
+ mappings[G_UNICODE_SCRIPT_GURMUKHI] = _("Gurmukhi");
+ mappings[G_UNICODE_SCRIPT_HAN] = _("Han");
+ mappings[G_UNICODE_SCRIPT_HANGUL] = _("Hangul");
+ mappings[G_UNICODE_SCRIPT_HEBREW] = _("Hebrew");
+ mappings[G_UNICODE_SCRIPT_HIRAGANA] = _("Hiragana");
+ mappings[G_UNICODE_SCRIPT_KANNADA] = _("Kannada");
+ mappings[G_UNICODE_SCRIPT_KATAKANA] = _("Katakana");
+ mappings[G_UNICODE_SCRIPT_KHMER] = _("Khmer");
+ mappings[G_UNICODE_SCRIPT_LAO] = _("Lao");
+ mappings[G_UNICODE_SCRIPT_LATIN] = _("Latin");
+ mappings[G_UNICODE_SCRIPT_MALAYALAM] = _("Malayalam");
+ mappings[G_UNICODE_SCRIPT_MONGOLIAN] = _("Mongolian");
+ mappings[G_UNICODE_SCRIPT_MYANMAR] = _("Myanmar");
+ mappings[G_UNICODE_SCRIPT_OGHAM] = _("Ogham");
+ mappings[G_UNICODE_SCRIPT_OLD_ITALIC] = _("Old Italic");
+ mappings[G_UNICODE_SCRIPT_ORIYA] = _("Oriya");
+ mappings[G_UNICODE_SCRIPT_RUNIC] = _("Runic");
+ mappings[G_UNICODE_SCRIPT_SINHALA] = _("Sinhala");
+ mappings[G_UNICODE_SCRIPT_SYRIAC] = _("Syriac");
+ mappings[G_UNICODE_SCRIPT_TAMIL] = _("Tamil");
+ mappings[G_UNICODE_SCRIPT_TELUGU] = _("Telugu");
+ mappings[G_UNICODE_SCRIPT_THAANA] = _("Thaana");
+ mappings[G_UNICODE_SCRIPT_THAI] = _("Thai");
+ mappings[G_UNICODE_SCRIPT_TIBETAN] = _("Tibetan");
+ mappings[G_UNICODE_SCRIPT_CANADIAN_ABORIGINAL] = _("Canadian Aboriginal");
+ mappings[G_UNICODE_SCRIPT_YI] = _("Yi");
+ mappings[G_UNICODE_SCRIPT_TAGALOG] = _("Tagalog");
+ mappings[G_UNICODE_SCRIPT_HANUNOO] = _("Hanunoo");
+ mappings[G_UNICODE_SCRIPT_BUHID] = _("Buhid");
+ mappings[G_UNICODE_SCRIPT_TAGBANWA] = _("Tagbanwa");
+ mappings[G_UNICODE_SCRIPT_BRAILLE] = _("Braille");
+ mappings[G_UNICODE_SCRIPT_CYPRIOT] = _("Cypriot");
+ mappings[G_UNICODE_SCRIPT_LIMBU] = _("Limbu");
+ mappings[G_UNICODE_SCRIPT_OSMANYA] = _("Osmanya");
+ mappings[G_UNICODE_SCRIPT_SHAVIAN] = _("Shavian");
+ mappings[G_UNICODE_SCRIPT_LINEAR_B] = _("Linear B");
+ mappings[G_UNICODE_SCRIPT_TAI_LE] = _("Tai Le");
+ mappings[G_UNICODE_SCRIPT_UGARITIC] = _("Ugaritic");
+ mappings[G_UNICODE_SCRIPT_NEW_TAI_LUE] = _("New Tai Lue");
+ mappings[G_UNICODE_SCRIPT_BUGINESE] = _("Buginese");
+ mappings[G_UNICODE_SCRIPT_GLAGOLITIC] = _("Glagolitic");
+ mappings[G_UNICODE_SCRIPT_TIFINAGH] = _("Tifinagh");
+ mappings[G_UNICODE_SCRIPT_SYLOTI_NAGRI] = _("Syloti Nagri");
+ mappings[G_UNICODE_SCRIPT_OLD_PERSIAN] = _("Old Persian");
+ mappings[G_UNICODE_SCRIPT_KHAROSHTHI] = _("Kharoshthi");
+ mappings[G_UNICODE_SCRIPT_UNKNOWN] = _("unassigned");
+ mappings[G_UNICODE_SCRIPT_BALINESE] = _("Balinese");
+ mappings[G_UNICODE_SCRIPT_CUNEIFORM] = _("Cuneiform");
+ mappings[G_UNICODE_SCRIPT_PHOENICIAN] = _("Phoenician");
+ mappings[G_UNICODE_SCRIPT_PHAGS_PA] = _("Phags-pa");
+ mappings[G_UNICODE_SCRIPT_NKO] = _("N'Ko");
+ mappings[G_UNICODE_SCRIPT_KAYAH_LI] = _("Kayah Li");
+ mappings[G_UNICODE_SCRIPT_LEPCHA] = _("Lepcha");
+ mappings[G_UNICODE_SCRIPT_REJANG] = _("Rejang");
+ mappings[G_UNICODE_SCRIPT_SUNDANESE] = _("Sundanese");
+ mappings[G_UNICODE_SCRIPT_SAURASHTRA] = _("Saurashtra");
+ mappings[G_UNICODE_SCRIPT_CHAM] = _("Cham");
+ mappings[G_UNICODE_SCRIPT_OL_CHIKI] = _("Ol Chiki");
+ mappings[G_UNICODE_SCRIPT_VAI] = _("Vai");
+ mappings[G_UNICODE_SCRIPT_CARIAN] = _("Carian");
+ mappings[G_UNICODE_SCRIPT_LYCIAN] = _("Lycian");
+ mappings[G_UNICODE_SCRIPT_LYDIAN] = _("Lydian");
+ mappings[G_UNICODE_SCRIPT_AVESTAN] = _("Avestan"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_BAMUM] = _("Bamum"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_EGYPTIAN_HIEROGLYPHS] = _("Egyptian Hieroglpyhs"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_IMPERIAL_ARAMAIC] = _("Imperial Aramaic"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PAHLAVI]= _("Inscriptional Pahlavi"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PARTHIAN]= _("Inscriptional Parthian"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_JAVANESE] = _("Javanese"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_KAITHI] = _("Kaithi"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_LISU] = _("Lisu"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_MEETEI_MAYEK] = _("Meetei Mayek"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_OLD_SOUTH_ARABIAN] = _("Old South Arabian"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_OLD_TURKIC] = _("Old Turkic"); // Since: 2.28
+ mappings[G_UNICODE_SCRIPT_SAMARITAN] = _("Samaritan"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_TAI_THAM] = _("Tai Tham"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_TAI_VIET] = _("Tai Viet"); // Since: 2.26
+ mappings[G_UNICODE_SCRIPT_BATAK] = _("Batak"); // Since: 2.28
+ mappings[G_UNICODE_SCRIPT_BRAHMI] = _("Brahmi"); // Since: 2.28
+ mappings[G_UNICODE_SCRIPT_MANDAIC] = _("Mandaic"); // Since: 2.28
+ mappings[G_UNICODE_SCRIPT_CHAKMA] = _("Chakma"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_MEROITIC_CURSIVE] = _("Meroitic Cursive"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_MEROITIC_HIEROGLYPHS] = _("Meroitic Hieroglyphs"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_MIAO] = _("Miao"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_SHARADA] = _("Sharada"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_SORA_SOMPENG] = _("Sora Sompeng"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_TAKRI] = _("Takri"); // Since: 2.32
+ mappings[G_UNICODE_SCRIPT_BASSA_VAH] = _("Bassa"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_CAUCASIAN_ALBANIAN] = _("Caucasian Albanian"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_DUPLOYAN] = _("Duployan"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_ELBASAN] = _("Elbasan"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_GRANTHA] = _("Grantha"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_KHOJKI] = _("Khojki"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_KHUDAWADI] = _("Khudawadi, Sindhi"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_LINEAR_A] = _("Linear A"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MAHAJANI] = _("Mahajani"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MANICHAEAN] = _("Manichaean"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MENDE_KIKAKUI] = _("Mende Kikakui"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MODI] = _("Modi"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_MRO] = _("Mro"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_NABATAEAN] = _("Nabataean"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_OLD_NORTH_ARABIAN] = _("Old North Arabian"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_OLD_PERMIC] = _("Old Permic"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_PAHAWH_HMONG] = _("Pahawh Hmong"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_PALMYRENE] = _("Palmyrene"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_PAU_CIN_HAU] = _("Pau Cin Hau"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_PSALTER_PAHLAVI] = _("Psalter Pahlavi"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_SIDDHAM] = _("Siddham"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_TIRHUTA] = _("Tirhuta"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_WARANG_CITI] = _("Warang Citi"); // Since: 2.42
+ mappings[G_UNICODE_SCRIPT_AHOM] = _("Ahom"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_ANATOLIAN_HIEROGLYPHS]= _("Anatolian Hieroglyphs"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_HATRAN] = _("Hatran"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_MULTANI] = _("Multani"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_OLD_HUNGARIAN] = _("Old Hungarian"); // Since: 2.48
+ mappings[G_UNICODE_SCRIPT_SIGNWRITING] = _("Signwriting"); // Since: 2.48
+/*
+ mappings[G_UNICODE_SCRIPT_ADLAM] = _("Adlam"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_BHAIKSUKI] = _("Bhaiksuki"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_MARCHEN] = _("Marchen"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_NEWA] = _("Newa"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_OSAGE] = _("Osage"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_TANGUT] = _("Tangut"); // Since: 2.50
+ mappings[G_UNICODE_SCRIPT_MASARAM_GONDI] = _("Masaram Gondi"); // Since: 2.54
+ mappings[G_UNICODE_SCRIPT_NUSHU] = _("Nushu"); // Since: 2.54
+ mappings[G_UNICODE_SCRIPT_SOYOMBO] = _("Soyombo"); // Since: 2.54
+ mappings[G_UNICODE_SCRIPT_ZANABAZAR_SQUARE] = _("Zanabazar Square"); // Since: 2.54
+ mappings[G_UNICODE_SCRIPT_DOGRA] = _("Dogra"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_GUNJALA_GONDI] = _("Gunjala Gondi"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_HANIFI_ROHINGYA] = _("Hanifi Rohingya"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_MAKASAR] = _("Makasar"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_MEDEFAIDRIN] = _("Medefaidrin"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_OLD_SOGDIAN] = _("Old Sogdian"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_SOGDIAN] = _("Sogdian"); // Since: 2.58
+ mappings[G_UNICODE_SCRIPT_ELYMAIC] = _("Elym"); // Since: 2.62
+ mappings[G_UNICODE_SCRIPT_NANDINAGARI] = _("Nand"); // Since: 2.62
+ mappings[G_UNICODE_SCRIPT_NYIAKENG_PUACHUE_HMONG]= _("Rohg"); // Since: 2.62
+ mappings[G_UNICODE_SCRIPT_WANCHO] = _("Wcho"); // Since: 2.62
+*/
+ }
+ return mappings;
+}
+
+typedef std::pair<gunichar, gunichar> Range;
+typedef std::pair<Range, Glib::ustring> NamedRange;
+
+static std::vector<NamedRange> & getRanges()
+{
+ static bool init = false;
+ static std::vector<NamedRange> ranges;
+ if (!init) {
+ init = true;
+ ranges.emplace_back(std::make_pair(0x00000, 0x2FFFF), _("all"));
+ ranges.emplace_back(std::make_pair(0x00000, 0x0FFFF), _("Basic Plane"));
+ ranges.emplace_back(std::make_pair(0x10000, 0x1FFFF), _("Extended Multilingual Plane"));
+ ranges.emplace_back(std::make_pair(0x20000, 0x2FFFF), _("Supplementary Ideographic Plane"));
+
+ ranges.emplace_back(std::make_pair(0x0000, 0x007F), _("Basic Latin"));
+ ranges.emplace_back(std::make_pair(0x0080, 0x00FF), _("Latin-1 Supplement"));
+ ranges.emplace_back(std::make_pair(0x0100, 0x017F), _("Latin Extended-A"));
+ ranges.emplace_back(std::make_pair(0x0180, 0x024F), _("Latin Extended-B"));
+ ranges.emplace_back(std::make_pair(0x0250, 0x02AF), _("IPA Extensions"));
+ ranges.emplace_back(std::make_pair(0x02B0, 0x02FF), _("Spacing Modifier Letters"));
+ ranges.emplace_back(std::make_pair(0x0300, 0x036F), _("Combining Diacritical Marks"));
+ ranges.emplace_back(std::make_pair(0x0370, 0x03FF), _("Greek and Coptic"));
+ ranges.emplace_back(std::make_pair(0x0400, 0x04FF), _("Cyrillic"));
+ ranges.emplace_back(std::make_pair(0x0500, 0x052F), _("Cyrillic Supplement"));
+ ranges.emplace_back(std::make_pair(0x0530, 0x058F), _("Armenian"));
+ ranges.emplace_back(std::make_pair(0x0590, 0x05FF), _("Hebrew"));
+ ranges.emplace_back(std::make_pair(0x0600, 0x06FF), _("Arabic"));
+ ranges.emplace_back(std::make_pair(0x0700, 0x074F), _("Syriac"));
+ ranges.emplace_back(std::make_pair(0x0750, 0x077F), _("Arabic Supplement"));
+ ranges.emplace_back(std::make_pair(0x0780, 0x07BF), _("Thaana"));
+ ranges.emplace_back(std::make_pair(0x07C0, 0x07FF), _("NKo"));
+ ranges.emplace_back(std::make_pair(0x0800, 0x083F), _("Samaritan"));
+ ranges.emplace_back(std::make_pair(0x0900, 0x097F), _("Devanagari"));
+ ranges.emplace_back(std::make_pair(0x0980, 0x09FF), _("Bengali"));
+ ranges.emplace_back(std::make_pair(0x0A00, 0x0A7F), _("Gurmukhi"));
+ ranges.emplace_back(std::make_pair(0x0A80, 0x0AFF), _("Gujarati"));
+ ranges.emplace_back(std::make_pair(0x0B00, 0x0B7F), _("Oriya"));
+ ranges.emplace_back(std::make_pair(0x0B80, 0x0BFF), _("Tamil"));
+ ranges.emplace_back(std::make_pair(0x0C00, 0x0C7F), _("Telugu"));
+ ranges.emplace_back(std::make_pair(0x0C80, 0x0CFF), _("Kannada"));
+ ranges.emplace_back(std::make_pair(0x0D00, 0x0D7F), _("Malayalam"));
+ ranges.emplace_back(std::make_pair(0x0D80, 0x0DFF), _("Sinhala"));
+ ranges.emplace_back(std::make_pair(0x0E00, 0x0E7F), _("Thai"));
+ ranges.emplace_back(std::make_pair(0x0E80, 0x0EFF), _("Lao"));
+ ranges.emplace_back(std::make_pair(0x0F00, 0x0FFF), _("Tibetan"));
+ ranges.emplace_back(std::make_pair(0x1000, 0x109F), _("Myanmar"));
+ ranges.emplace_back(std::make_pair(0x10A0, 0x10FF), _("Georgian"));
+ ranges.emplace_back(std::make_pair(0x1100, 0x11FF), _("Hangul Jamo"));
+ ranges.emplace_back(std::make_pair(0x1200, 0x137F), _("Ethiopic"));
+ ranges.emplace_back(std::make_pair(0x1380, 0x139F), _("Ethiopic Supplement"));
+ ranges.emplace_back(std::make_pair(0x13A0, 0x13FF), _("Cherokee"));
+ ranges.emplace_back(std::make_pair(0x1400, 0x167F), _("Unified Canadian Aboriginal Syllabics"));
+ ranges.emplace_back(std::make_pair(0x1680, 0x169F), _("Ogham"));
+ ranges.emplace_back(std::make_pair(0x16A0, 0x16FF), _("Runic"));
+ ranges.emplace_back(std::make_pair(0x1700, 0x171F), _("Tagalog"));
+ ranges.emplace_back(std::make_pair(0x1720, 0x173F), _("Hanunoo"));
+ ranges.emplace_back(std::make_pair(0x1740, 0x175F), _("Buhid"));
+ ranges.emplace_back(std::make_pair(0x1760, 0x177F), _("Tagbanwa"));
+ ranges.emplace_back(std::make_pair(0x1780, 0x17FF), _("Khmer"));
+ ranges.emplace_back(std::make_pair(0x1800, 0x18AF), _("Mongolian"));
+ ranges.emplace_back(std::make_pair(0x18B0, 0x18FF), _("Unified Canadian Aboriginal Syllabics Extended"));
+ ranges.emplace_back(std::make_pair(0x1900, 0x194F), _("Limbu"));
+ ranges.emplace_back(std::make_pair(0x1950, 0x197F), _("Tai Le"));
+ ranges.emplace_back(std::make_pair(0x1980, 0x19DF), _("New Tai Lue"));
+ ranges.emplace_back(std::make_pair(0x19E0, 0x19FF), _("Khmer Symbols"));
+ ranges.emplace_back(std::make_pair(0x1A00, 0x1A1F), _("Buginese"));
+ ranges.emplace_back(std::make_pair(0x1A20, 0x1AAF), _("Tai Tham"));
+ ranges.emplace_back(std::make_pair(0x1B00, 0x1B7F), _("Balinese"));
+ ranges.emplace_back(std::make_pair(0x1B80, 0x1BBF), _("Sundanese"));
+ ranges.emplace_back(std::make_pair(0x1C00, 0x1C4F), _("Lepcha"));
+ ranges.emplace_back(std::make_pair(0x1C50, 0x1C7F), _("Ol Chiki"));
+ ranges.emplace_back(std::make_pair(0x1CD0, 0x1CFF), _("Vedic Extensions"));
+ ranges.emplace_back(std::make_pair(0x1D00, 0x1D7F), _("Phonetic Extensions"));
+ ranges.emplace_back(std::make_pair(0x1D80, 0x1DBF), _("Phonetic Extensions Supplement"));
+ ranges.emplace_back(std::make_pair(0x1DC0, 0x1DFF), _("Combining Diacritical Marks Supplement"));
+ ranges.emplace_back(std::make_pair(0x1E00, 0x1EFF), _("Latin Extended Additional"));
+ ranges.emplace_back(std::make_pair(0x1F00, 0x1FFF), _("Greek Extended"));
+ ranges.emplace_back(std::make_pair(0x2000, 0x206F), _("General Punctuation"));
+ ranges.emplace_back(std::make_pair(0x2070, 0x209F), _("Superscripts and Subscripts"));
+ ranges.emplace_back(std::make_pair(0x20A0, 0x20CF), _("Currency Symbols"));
+ ranges.emplace_back(std::make_pair(0x20D0, 0x20FF), _("Combining Diacritical Marks for Symbols"));
+ ranges.emplace_back(std::make_pair(0x2100, 0x214F), _("Letterlike Symbols"));
+ ranges.emplace_back(std::make_pair(0x2150, 0x218F), _("Number Forms"));
+ ranges.emplace_back(std::make_pair(0x2190, 0x21FF), _("Arrows"));
+ ranges.emplace_back(std::make_pair(0x2200, 0x22FF), _("Mathematical Operators"));
+ ranges.emplace_back(std::make_pair(0x2300, 0x23FF), _("Miscellaneous Technical"));
+ ranges.emplace_back(std::make_pair(0x2400, 0x243F), _("Control Pictures"));
+ ranges.emplace_back(std::make_pair(0x2440, 0x245F), _("Optical Character Recognition"));
+ ranges.emplace_back(std::make_pair(0x2460, 0x24FF), _("Enclosed Alphanumerics"));
+ ranges.emplace_back(std::make_pair(0x2500, 0x257F), _("Box Drawing"));
+ ranges.emplace_back(std::make_pair(0x2580, 0x259F), _("Block Elements"));
+ ranges.emplace_back(std::make_pair(0x25A0, 0x25FF), _("Geometric Shapes"));
+ ranges.emplace_back(std::make_pair(0x2600, 0x26FF), _("Miscellaneous Symbols"));
+ ranges.emplace_back(std::make_pair(0x2700, 0x27BF), _("Dingbats"));
+ ranges.emplace_back(std::make_pair(0x27C0, 0x27EF), _("Miscellaneous Mathematical Symbols-A"));
+ ranges.emplace_back(std::make_pair(0x27F0, 0x27FF), _("Supplemental Arrows-A"));
+ ranges.emplace_back(std::make_pair(0x2800, 0x28FF), _("Braille Patterns"));
+ ranges.emplace_back(std::make_pair(0x2900, 0x297F), _("Supplemental Arrows-B"));
+ ranges.emplace_back(std::make_pair(0x2980, 0x29FF), _("Miscellaneous Mathematical Symbols-B"));
+ ranges.emplace_back(std::make_pair(0x2A00, 0x2AFF), _("Supplemental Mathematical Operators"));
+ ranges.emplace_back(std::make_pair(0x2B00, 0x2BFF), _("Miscellaneous Symbols and Arrows"));
+ ranges.emplace_back(std::make_pair(0x2C00, 0x2C5F), _("Glagolitic"));
+ ranges.emplace_back(std::make_pair(0x2C60, 0x2C7F), _("Latin Extended-C"));
+ ranges.emplace_back(std::make_pair(0x2C80, 0x2CFF), _("Coptic"));
+ ranges.emplace_back(std::make_pair(0x2D00, 0x2D2F), _("Georgian Supplement"));
+ ranges.emplace_back(std::make_pair(0x2D30, 0x2D7F), _("Tifinagh"));
+ ranges.emplace_back(std::make_pair(0x2D80, 0x2DDF), _("Ethiopic Extended"));
+ ranges.emplace_back(std::make_pair(0x2DE0, 0x2DFF), _("Cyrillic Extended-A"));
+ ranges.emplace_back(std::make_pair(0x2E00, 0x2E7F), _("Supplemental Punctuation"));
+ ranges.emplace_back(std::make_pair(0x2E80, 0x2EFF), _("CJK Radicals Supplement"));
+ ranges.emplace_back(std::make_pair(0x2F00, 0x2FDF), _("Kangxi Radicals"));
+ ranges.emplace_back(std::make_pair(0x2FF0, 0x2FFF), _("Ideographic Description Characters"));
+ ranges.emplace_back(std::make_pair(0x3000, 0x303F), _("CJK Symbols and Punctuation"));
+ ranges.emplace_back(std::make_pair(0x3040, 0x309F), _("Hiragana"));
+ ranges.emplace_back(std::make_pair(0x30A0, 0x30FF), _("Katakana"));
+ ranges.emplace_back(std::make_pair(0x3100, 0x312F), _("Bopomofo"));
+ ranges.emplace_back(std::make_pair(0x3130, 0x318F), _("Hangul Compatibility Jamo"));
+ ranges.emplace_back(std::make_pair(0x3190, 0x319F), _("Kanbun"));
+ ranges.emplace_back(std::make_pair(0x31A0, 0x31BF), _("Bopomofo Extended"));
+ ranges.emplace_back(std::make_pair(0x31C0, 0x31EF), _("CJK Strokes"));
+ ranges.emplace_back(std::make_pair(0x31F0, 0x31FF), _("Katakana Phonetic Extensions"));
+ ranges.emplace_back(std::make_pair(0x3200, 0x32FF), _("Enclosed CJK Letters and Months"));
+ ranges.emplace_back(std::make_pair(0x3300, 0x33FF), _("CJK Compatibility"));
+ ranges.emplace_back(std::make_pair(0x3400, 0x4DBF), _("CJK Unified Ideographs Extension A"));
+ ranges.emplace_back(std::make_pair(0x4DC0, 0x4DFF), _("Yijing Hexagram Symbols"));
+ ranges.emplace_back(std::make_pair(0x4E00, 0x9FFF), _("CJK Unified Ideographs"));
+ ranges.emplace_back(std::make_pair(0xA000, 0xA48F), _("Yi Syllables"));
+ ranges.emplace_back(std::make_pair(0xA490, 0xA4CF), _("Yi Radicals"));
+ ranges.emplace_back(std::make_pair(0xA4D0, 0xA4FF), _("Lisu"));
+ ranges.emplace_back(std::make_pair(0xA500, 0xA63F), _("Vai"));
+ ranges.emplace_back(std::make_pair(0xA640, 0xA69F), _("Cyrillic Extended-B"));
+ ranges.emplace_back(std::make_pair(0xA6A0, 0xA6FF), _("Bamum"));
+ ranges.emplace_back(std::make_pair(0xA700, 0xA71F), _("Modifier Tone Letters"));
+ ranges.emplace_back(std::make_pair(0xA720, 0xA7FF), _("Latin Extended-D"));
+ ranges.emplace_back(std::make_pair(0xA800, 0xA82F), _("Syloti Nagri"));
+ ranges.emplace_back(std::make_pair(0xA830, 0xA83F), _("Common Indic Number Forms"));
+ ranges.emplace_back(std::make_pair(0xA840, 0xA87F), _("Phags-pa"));
+ ranges.emplace_back(std::make_pair(0xA880, 0xA8DF), _("Saurashtra"));
+ ranges.emplace_back(std::make_pair(0xA8E0, 0xA8FF), _("Devanagari Extended"));
+ ranges.emplace_back(std::make_pair(0xA900, 0xA92F), _("Kayah Li"));
+ ranges.emplace_back(std::make_pair(0xA930, 0xA95F), _("Rejang"));
+ ranges.emplace_back(std::make_pair(0xA960, 0xA97F), _("Hangul Jamo Extended-A"));
+ ranges.emplace_back(std::make_pair(0xA980, 0xA9DF), _("Javanese"));
+ ranges.emplace_back(std::make_pair(0xAA00, 0xAA5F), _("Cham"));
+ ranges.emplace_back(std::make_pair(0xAA60, 0xAA7F), _("Myanmar Extended-A"));
+ ranges.emplace_back(std::make_pair(0xAA80, 0xAADF), _("Tai Viet"));
+ ranges.emplace_back(std::make_pair(0xABC0, 0xABFF), _("Meetei Mayek"));
+ ranges.emplace_back(std::make_pair(0xAC00, 0xD7AF), _("Hangul Syllables"));
+ ranges.emplace_back(std::make_pair(0xD7B0, 0xD7FF), _("Hangul Jamo Extended-B"));
+ ranges.emplace_back(std::make_pair(0xD800, 0xDB7F), _("High Surrogates"));
+ ranges.emplace_back(std::make_pair(0xDB80, 0xDBFF), _("High Private Use Surrogates"));
+ ranges.emplace_back(std::make_pair(0xDC00, 0xDFFF), _("Low Surrogates"));
+ ranges.emplace_back(std::make_pair(0xE000, 0xF8FF), _("Private Use Area"));
+ ranges.emplace_back(std::make_pair(0xF900, 0xFAFF), _("CJK Compatibility Ideographs"));
+ ranges.emplace_back(std::make_pair(0xFB00, 0xFB4F), _("Alphabetic Presentation Forms"));
+ ranges.emplace_back(std::make_pair(0xFB50, 0xFDFF), _("Arabic Presentation Forms-A"));
+ ranges.emplace_back(std::make_pair(0xFE00, 0xFE0F), _("Variation Selectors"));
+ ranges.emplace_back(std::make_pair(0xFE10, 0xFE1F), _("Vertical Forms"));
+ ranges.emplace_back(std::make_pair(0xFE20, 0xFE2F), _("Combining Half Marks"));
+ ranges.emplace_back(std::make_pair(0xFE30, 0xFE4F), _("CJK Compatibility Forms"));
+ ranges.emplace_back(std::make_pair(0xFE50, 0xFE6F), _("Small Form Variants"));
+ ranges.emplace_back(std::make_pair(0xFE70, 0xFEFF), _("Arabic Presentation Forms-B"));
+ ranges.emplace_back(std::make_pair(0xFF00, 0xFFEF), _("Halfwidth and Fullwidth Forms"));
+ ranges.emplace_back(std::make_pair(0xFFF0, 0xFFFF), _("Specials"));
+
+ // Selected ranges in Extended Multilingual Plane
+ ranges.emplace_back(std::make_pair(0x1F300, 0x1F5FF), _("Miscellaneous Symbols and Pictographs"));
+ ranges.emplace_back(std::make_pair(0x1F600, 0x1F64F), _("Emoticons"));
+ ranges.emplace_back(std::make_pair(0x1F650, 0x1F67F), _("Ornamental Dingbats"));
+ ranges.emplace_back(std::make_pair(0x1F680, 0x1F6FF), _("Transport and Map Symbols"));
+ ranges.emplace_back(std::make_pair(0x1F700, 0x1F77F), _("Alchemical Symbols"));
+ ranges.emplace_back(std::make_pair(0x1F780, 0x1F7FF), _("Geometric Shapes Extended"));
+ ranges.emplace_back(std::make_pair(0x1F800, 0x1F8FF), _("Supplemental Arrows-C"));
+ ranges.emplace_back(std::make_pair(0x1F900, 0x1F9FF), _("Supplemental Symbols and Pictographs"));
+ ranges.emplace_back(std::make_pair(0x1FA00, 0x1FA7F), _("Chess Symbols"));
+ ranges.emplace_back(std::make_pair(0x1FA80, 0x1FAFF), _("Symbols and Pictographs Extended-A"));
+
+ }
+
+ return ranges;
+}
+
+class GlyphColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ Gtk::TreeModelColumn<gunichar> code;
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> tooltip;
+
+ GlyphColumns()
+ {
+ add(code);
+ add(name);
+ add(tooltip);
+ }
+};
+
+GlyphColumns *GlyphsPanel::getColumns()
+{
+ static GlyphColumns *columns = new GlyphColumns();
+
+ return columns;
+}
+
+/**
+ * Constructor
+ */
+GlyphsPanel::GlyphsPanel()
+ : DialogBase("/dialogs/glyphs", "Glyphs")
+ , store(Gtk::ListStore::create(*getColumns()))
+ , instanceConns()
+{
+ set_orientation(Gtk::ORIENTATION_VERTICAL);
+ auto table = new Gtk::Grid();
+ table->set_row_spacing(4);
+ table->set_column_spacing(4);
+ pack_start(*Gtk::manage(table), Gtk::PACK_EXPAND_WIDGET);
+ guint row = 0;
+
+// -------------------------------
+
+ {
+ fontSelector = new Inkscape::UI::Widget::FontSelector (false, false);
+ fontSelector->set_name ("UnicodeCharacters");
+
+ sigc::connection conn =
+ fontSelector->connectChanged(sigc::hide(sigc::mem_fun(*this, &GlyphsPanel::rebuild)));
+ instanceConns.push_back(conn);
+
+ table->attach(*Gtk::manage(fontSelector), 0, row, 3, 1);
+ row++;
+ }
+
+// -------------------------------
+
+ {
+ auto label = new Gtk::Label(_("Script: "));
+
+ table->attach( *Gtk::manage(label), 0, row, 1, 1);
+
+ scriptCombo = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>());
+ for (auto & it : getScriptToName())
+ {
+ scriptCombo->append(it.second);
+ }
+
+ scriptCombo->set_active_text(getScriptToName()[G_UNICODE_SCRIPT_INVALID_CODE]);
+ sigc::connection conn = scriptCombo->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::rebuild));
+ instanceConns.push_back(conn);
+
+ scriptCombo->set_halign(Gtk::ALIGN_START);
+ scriptCombo->set_valign(Gtk::ALIGN_START);
+ scriptCombo->set_hexpand();
+ table->attach(*scriptCombo, 1, row, 1, 1);
+ }
+
+ row++;
+
+// -------------------------------
+
+ {
+ auto label = new Gtk::Label(_("Range: "));
+ table->attach( *Gtk::manage(label), 0, row, 1, 1);
+
+ rangeCombo = Gtk::manage(new Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText>());
+ for (auto & it : getRanges()) {
+ rangeCombo->append(it.second);
+ }
+
+ rangeCombo->set_active_text(getRanges()[4].second);
+ sigc::connection conn = rangeCombo->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::rebuild));
+ instanceConns.push_back(conn);
+
+ rangeCombo->set_halign(Gtk::ALIGN_START);
+ rangeCombo->set_valign(Gtk::ALIGN_START);
+ rangeCombo->set_hexpand();
+ table->attach(*rangeCombo, 1, row, 1, 1);
+ }
+
+ row++;
+
+// -------------------------------
+
+ GlyphColumns *columns = getColumns();
+
+ iconView = new Gtk::IconView(static_cast<Glib::RefPtr<Gtk::TreeModel> >(store));
+ iconView->set_name("UnicodeIconView");
+ iconView->set_markup_column(columns->name);
+ iconView->set_tooltip_column(2); // Uses Pango markup, must use column number.
+ iconView->set_margin(0);
+ iconView->set_item_padding(0);
+ iconView->set_row_spacing(0);
+ iconView->set_column_spacing(0);
+
+ sigc::connection conn;
+ conn = iconView->signal_item_activated().connect(sigc::mem_fun(*this, &GlyphsPanel::glyphActivated));
+ instanceConns.push_back(conn);
+ conn = iconView->signal_selection_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::glyphSelectionChanged));
+ instanceConns.push_back(conn);
+
+
+ Gtk::ScrolledWindow *scroller = new Gtk::ScrolledWindow();
+ scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
+ scroller->add(*Gtk::manage(iconView));
+ scroller->set_hexpand();
+ scroller->set_vexpand();
+ table->attach(*Gtk::manage(scroller), 0, row, 3, 1);
+
+ row++;
+
+// -------------------------------
+
+ Gtk::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL);
+
+ entry = std::make_shared<Gtk::Entry>();
+ conn = entry->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::calcCanInsert));
+ instanceConns.push_back(conn);
+ entry->set_width_chars(18);
+ box->pack_start(*entry.get(), Gtk::PACK_SHRINK);
+
+ Gtk::Label *pad = new Gtk::Label(" ");
+ box->pack_start(*Gtk::manage(pad), Gtk::PACK_SHRINK);
+
+ label = std::make_shared<Gtk::Label>(" ");
+ box->pack_start(*label.get(), Gtk::PACK_SHRINK);
+
+ pad = new Gtk::Label("");
+ box->pack_start(*Gtk::manage(pad), Gtk::PACK_EXPAND_WIDGET);
+
+ insertBtn = std::make_shared<Gtk::Button>(_("Append"));
+ conn = insertBtn->signal_clicked().connect(sigc::mem_fun(*this, &GlyphsPanel::insertText));
+ instanceConns.push_back(conn);
+ insertBtn->set_can_default();
+ insertBtn->set_sensitive(false);
+
+ box->pack_end(*insertBtn.get(), Gtk::PACK_SHRINK);
+ box->set_hexpand();
+ table->attach( *Gtk::manage(box), 0, row, 3, 1);
+
+ row++;
+
+// -------------------------------
+
+
+ show_all_children();
+}
+
+GlyphsPanel::~GlyphsPanel()
+{
+ for (auto & instanceConn : instanceConns) {
+ instanceConn.disconnect();
+ }
+ instanceConns.clear();
+}
+
+void GlyphsPanel::selectionChanged(Selection *selection)
+{
+ readSelection(true, true);
+}
+void GlyphsPanel::selectionModified(Selection *selection, guint flags)
+{
+ bool style = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG |
+ SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 );
+
+ bool content = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG |
+ SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 );
+
+ readSelection(style, content);
+}
+
+// Append selected glyphs to selected text
+void GlyphsPanel::insertText()
+{
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ SPItem *textItem = nullptr;
+ auto itemlist = selection->items();
+ for(auto i=itemlist.begin(); itemlist.end() != i; ++i) {
+ if (is<SPText>(*i) || is<SPFlowtext>(*i)) {
+ textItem = *i;
+ break;
+ }
+ }
+
+ if (textItem) {
+ Glib::ustring glyphs;
+ if (entry->get_text_length() > 0) {
+ glyphs = entry->get_text();
+ } else {
+ auto itemArray = iconView->get_selected_items();
+
+ if (!itemArray.empty()) {
+ Gtk::TreeModel::Path const & path = *itemArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ gunichar ch = (*row)[getColumns()->code];
+ glyphs = ch;
+ }
+ }
+
+ if (!glyphs.empty()) {
+ Glib::ustring combined = sp_te_get_string_multiline(textItem);
+ combined += glyphs;
+ sp_te_set_repr_text_multiline(textItem, combined.c_str());
+ DocumentUndo::done(getDocument(), _("Append text"), INKSCAPE_ICON("draw-text"));
+ }
+ }
+}
+
+void GlyphsPanel::glyphActivated(Gtk::TreeModel::Path const & path)
+{
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ gunichar ch = (*row)[getColumns()->code];
+ Glib::ustring tmp;
+ tmp += ch;
+
+ int startPos = 0;
+ int endPos = 0;
+ if (entry->get_selection_bounds(startPos, endPos)) {
+ // there was something selected.
+ entry->delete_text(startPos, endPos);
+ }
+ startPos = entry->get_position();
+ entry->insert_text(tmp, -1, startPos);
+ entry->set_position(startPos);
+}
+
+void GlyphsPanel::glyphSelectionChanged()
+{
+ auto itemArray = iconView->get_selected_items();
+
+ if (itemArray.empty()) {
+ label->set_text(" ");
+ } else {
+ Gtk::TreeModel::Path const & path = *itemArray.begin();
+ Gtk::ListStore::iterator row = store->get_iter(path);
+ gunichar ch = (*row)[getColumns()->code];
+
+
+ Glib::ustring scriptName;
+ GUnicodeScript script = g_unichar_get_script(ch);
+ std::map<GUnicodeScript, Glib::ustring> mappings = getScriptToName();
+ if (mappings.find(script) != mappings.end()) {
+ scriptName = mappings[script];
+ }
+ gchar * tmp = g_strdup_printf("U+%04X %s", ch, scriptName.c_str());
+ label->set_text(tmp);
+ }
+ calcCanInsert();
+}
+
+void GlyphsPanel::calcCanInsert()
+{
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ int items = 0;
+ auto itemlist = selection->items();
+ for(auto i=itemlist.begin(); itemlist.end() != i; ++i) {
+ if (is<SPText>(*i) || is<SPFlowtext>(*i)) {
+ ++items;
+ }
+ }
+
+ bool enable = (items == 1);
+ if (enable) {
+ enable &= (!iconView->get_selected_items().empty()
+ || (entry->get_text_length() > 0));
+ }
+
+ if (enable != insertBtn->is_sensitive()) {
+ insertBtn->set_sensitive(enable);
+ }
+}
+
+void GlyphsPanel::readSelection( bool updateStyle, bool updateContent )
+{
+ calcCanInsert();
+
+ if (updateStyle) {
+ Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance();
+
+ // Update family/style based on selection.
+ fontlister->selection_update();
+
+ // Update GUI (based on fontlister values).
+ fontSelector->update_font ();
+ }
+}
+
+
+void GlyphsPanel::rebuild()
+{
+ Glib::ustring fontspec = fontSelector->get_fontspec();
+
+ std::shared_ptr<FontInstance> font;
+ if (!fontspec.empty()) {
+ font = FontFactory::get().FaceFromFontSpecification(fontspec.c_str());
+ }
+
+ if (font) {
+
+ GUnicodeScript script = G_UNICODE_SCRIPT_INVALID_CODE;
+ Glib::ustring scriptName = scriptCombo->get_active_text();
+ std::map<GUnicodeScript, Glib::ustring> items = getScriptToName();
+ for (auto & item : items) {
+ if (scriptName == item.second) {
+ script = item.first;
+ break;
+ }
+ }
+
+ // Disconnect the model while we update it. Simple work-around for 5x+ performance boost.
+ Glib::RefPtr<Gtk::ListStore> tmp = Gtk::ListStore::create(*getColumns());
+ iconView->set_model(tmp);
+
+ gunichar lower = 0x00001;
+ gunichar upper = 0x2FFFF;
+ int active = rangeCombo->get_active_row_number();
+ if (active >= 0) {
+ lower = getRanges()[active].first.first;
+ upper = getRanges()[active].first.second;
+ }
+ std::vector<gunichar> present;
+ for (gunichar ch = lower; ch <= upper; ch++) {
+ int glyphId = font->MapUnicodeChar(ch);
+ if (glyphId > 0) {
+ if ((script == G_UNICODE_SCRIPT_INVALID_CODE) || (script == g_unichar_get_script(ch))) {
+ present.push_back(ch);
+ }
+ }
+ }
+
+ GlyphColumns *columns = getColumns();
+ store->clear();
+ for (unsigned int & it : present)
+ {
+ Gtk::ListStore::iterator row = store->append();
+ Glib::ustring tmp;
+ tmp += it;
+ tmp = Glib::Markup::escape_text(tmp); // Escape '&', '<', etc.
+ (*row)[columns->code] = it;
+ (*row)[columns->name] = "<span font_desc=\"" + fontspec + "\">" + tmp + "</span>";
+ (*row)[columns->tooltip] = "<span font_desc=\"" + fontspec + "\" size=\"42000\">" + tmp + "</span>";
+ }
+
+ // Reconnect the model once it has been updated:
+ iconView->set_model(store);
+ }
+}
+
+
+} // namespace Dialogs
+} // 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/dialog/glyphs.h b/src/ui/dialog/glyphs.h
new file mode 100644
index 0000000..2720436
--- /dev/null
+++ b/src/ui/dialog/glyphs.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2010 Jon A. Cruz
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_DIALOGS_GLYPHS_H
+#define SEEN_DIALOGS_GLYPHS_H
+
+#include <gtkmm/treemodel.h>
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Gtk {
+class ComboBoxText;
+class Entry;
+class IconView;
+class Label;
+class ListStore;
+}
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+class FontSelector;
+}
+
+namespace Dialog {
+
+class GlyphColumns;
+
+/**
+ * A panel that displays character glyphs.
+ */
+class GlyphsPanel : public DialogBase
+{
+public:
+ GlyphsPanel();
+ ~GlyphsPanel() override;
+
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+private:
+ static GlyphColumns *getColumns();
+
+ void rebuild();
+
+ void glyphActivated(Gtk::TreeModel::Path const & path);
+ void glyphSelectionChanged();
+ void readSelection( bool updateStyle, bool updateContent );
+ void calcCanInsert();
+ void insertText();
+
+ Glib::RefPtr<Gtk::ListStore> store;
+ Gtk::IconView *iconView;
+ std::shared_ptr<Gtk::Entry> entry;
+ std::shared_ptr<Gtk::Label> label;
+ std::shared_ptr<Gtk::Button> insertBtn;
+ Gtk::ComboBoxText *scriptCombo;
+ Gtk::ComboBoxText *rangeCombo;
+ Inkscape::UI::Widget::FontSelector *fontSelector;
+
+ std::vector<sigc::connection> instanceConns;
+};
+
+
+} // namespace Dialogs
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN_DIALOGS_GLYPHS_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/dialog/grid-arrange-tab.cpp b/src/ui/dialog/grid-arrange-tab.cpp
new file mode 100644
index 0000000..2003367
--- /dev/null
+++ b/src/ui/dialog/grid-arrange-tab.cpp
@@ -0,0 +1,660 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple dialog for creating grid type arrangements of selected objects
+ *
+ * Authors:
+ * Bob Jamison ( based off trace dialog)
+ * John Cliff
+ * Other dudes from The Inkscape Organization
+ * Abhishek Sharma
+ * Declara Denis
+ *
+ * Copyright (C) 2004 Bob Jamison
+ * Copyright (C) 2004 John Cliff
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+//#define DEBUG_GRID_ARRANGE 1
+
+#include "ui/dialog/grid-arrange-tab.h"
+#include <numeric>
+#include <glibmm/i18n.h>
+
+#include <gtkmm/grid.h>
+#include <gtkmm/sizegroup.h>
+
+#include <2geom/transforms.h>
+
+#include "preferences.h"
+#include "inkscape.h"
+
+#include "document.h"
+#include "document-undo.h"
+#include "desktop.h"
+
+#include "ui/icon-names.h"
+#include "ui/dialog/tile.h" // for Inkscape::UI::Dialog::ArrangeDialog
+
+
+/**
+ * Sort ObjectSet by an existing grid arrangement
+ *
+ * This is based on this paper here: DOI:10.1049/iet-ipr.2015.0126
+ *
+ * @param items - The unsorted object set to sort.
+ * @returns a new grid-sorted std::vector of SPItems.
+ */
+static std::vector<SPItem *> grid_item_sort(Inkscape::ObjectSet *items)
+{
+ std::vector<SPItem *> results;
+ Inkscape::ObjectSet rest;
+
+ // 1. Find middle Y position of the largest top object.
+ double box_top = items->visualBounds()->min()[Geom::Y];
+ double last_height = 0.0;
+ double target = box_top;
+ for (auto item : items->items()) {
+ if (auto item_box = item->desktopVisualBounds()) {
+ if (Geom::are_near(item_box->min()[Geom::Y], box_top, 2.0)) {
+ if (item_box->height() > last_height) {
+ last_height = item_box->height();
+ target = item_box->midpoint()[Geom::Y];
+ }
+ }
+ }
+ }
+
+ // 2. Loop through all remaining items
+ for (auto item : items->items()) {
+ // Items without visual bounds are completely ignored.
+ if (auto item_box = item->desktopVisualBounds()) {
+ auto radius = item_box->height() / 2;
+ auto min = item_box->midpoint()[Geom::Y] - radius;
+ auto max = item_box->midpoint()[Geom::Y] + radius;
+
+ if (max > target && min < target) {
+ // 2a. if the item's radius falls on the Y position above
+ results.push_back(item);
+ } else {
+ // 2b. Save items not in this row for later
+ rest.add(item);
+ }
+ }
+ }
+
+ // 3. Sort this single row according to the X position
+ std::sort(results.begin(), results.end(), [](SPItem *a, SPItem *b) {
+ // These boxes always exist because of the above filtering.
+ return (a->desktopVisualBounds()->min()[Geom::X] < b->desktopVisualBounds()->min()[Geom::X]);
+ });
+
+ if (results.size() == 0) {
+ g_warning("Bad grid detection when sorting items!");
+ } else if (!rest.isEmpty()) {
+ // 4. If there's any remaining, run this function again.
+ auto sorted_rest = grid_item_sort(&rest);
+ results.reserve(items->size());
+ results.insert(results.end(), sorted_rest.begin(), sorted_rest.end());
+ }
+ return results;
+}
+
+ namespace Inkscape {
+ namespace UI {
+ namespace Dialog {
+
+
+ //#########################################################################
+ //## E V E N T S
+ //#########################################################################
+
+ /*
+ *
+ * This arranges the selection in a grid pattern.
+ *
+ */
+
+ void GridArrangeTab::arrange()
+ {
+ // check for correct numbers in the row- and col-spinners
+ on_col_spinbutton_changed();
+ on_row_spinbutton_changed();
+
+ // set padding to manual values
+ double paddingx = XPadding.getValue("px");
+ double paddingy = YPadding.getValue("px");
+ int NoOfCols = NoOfColsSpinner.get_value_as_int();
+ int NoOfRows = NoOfRowsSpinner.get_value_as_int();
+
+ SPDesktop *desktop = Parent->getDesktop();
+ desktop->getDocument()->ensureUpToDate();
+ Inkscape::Selection *selection = desktop->getSelection();
+ if (!selection || selection->isEmpty()) return;
+
+ auto sel_box = selection->documentBounds(SPItem::VISUAL_BBOX);
+ if (sel_box.empty()) return;
+ double grid_left = sel_box->min()[Geom::X];
+ double grid_top = sel_box->min()[Geom::Y];
+
+ // require the sorting done before we can calculate row heights etc.
+ auto sorted = grid_item_sort(selection);
+
+ // Calculate individual Row and Column sizes if necessary
+ auto row_heights = std::vector<double>(NoOfRows, 0.0);
+ auto col_widths = std::vector<double>(NoOfCols, 0.0);
+ for(int i = 0; i < sorted.size(); i++) {
+ if (Geom::OptRect box = sorted[i]->documentVisualBounds()) {
+ double width = box->dimensions()[Geom::X];
+ double height = box->dimensions()[Geom::Y];
+ if (width > col_widths[(i % NoOfCols)]) {
+ col_widths[(i % NoOfCols)] = width;
+ }
+ if (height > row_heights[(i / NoOfCols)]) {
+ row_heights[(i / NoOfCols)] = height;
+ }
+ }
+ }
+
+ double col_width = *std::max_element(std::begin(col_widths), std::end(col_widths));
+ double row_height = *std::max_element(std::begin(row_heights), std::end(row_heights));
+
+ /// Make sure the top and left of the grid don't move by compensating for align values.
+ if (RowHeightButton.get_active()){
+ grid_top = grid_top - (((row_height - row_heights[0]) / 2)*(VertAlign));
+ }
+ if (ColumnWidthButton.get_active()){
+ grid_left = grid_left - (((col_width - col_widths[0]) /2)*(HorizAlign));
+ }
+
+ // Calculate total widths and heights, allowing for columns and rows non uniformly sized.
+ double last_col_padding = 0.0;
+ double total_col_width = 0.0;
+ if (ColumnWidthButton.get_active()){
+ total_col_width = col_width * NoOfCols;
+ // Remember the amount of padding the box will lose on the right side
+ last_col_padding = (col_width - col_widths[NoOfCols-1]) / 2;
+ std::fill(col_widths.begin(), col_widths.end(), col_width);
+ } else {
+ total_col_width = std::accumulate(col_widths.begin(), col_widths.end(), 0);
+ }
+
+ double last_row_padding = 0.0;
+ double total_row_height = 0.0;
+ if (RowHeightButton.get_active()){
+ total_row_height = row_height * NoOfRows;
+ // Remember the amount of padding the box will lose on the bottom side
+ last_row_padding = (row_height - row_heights[NoOfRows-1]) / 2;
+ std::fill(row_heights.begin(), row_heights.end(), row_height);
+ } else {
+ total_row_height = std::accumulate(row_heights.begin(), row_heights.end(), 0);
+ }
+
+ // Fit to bbox, calculate padding between rows accordingly.
+ if (SpaceByBBoxRadioButton.get_active()) {
+ paddingx = (sel_box->width() - total_col_width + last_col_padding) / (NoOfCols -1);
+ paddingy = (sel_box->height() - total_row_height + last_row_padding) / (NoOfRows -1);
+ }
+
+/*
+ Horizontal align - Left = 0
+ Centre = 1
+ Right = 2
+
+ Vertical align - Top = 0
+ Middle = 1
+ Bottom = 2
+
+ X position is calculated by taking the grids left co-ord, adding the distance to the column,
+ then adding 1/2 the spacing multiplied by the align variable above,
+ Y position likewise, takes the top of the grid, adds the y to the current row then adds the padding in to align it.
+
+*/
+
+ // Calculate row and column x and y coords required to allow for columns and rows which are non uniformly sized.
+ std::vector<double> col_xs = {0.0};
+ for (int col=1; col < NoOfCols; col++) {
+ col_xs.push_back(col_widths[col-1] + paddingx + col_xs[col-1]);
+ }
+
+ std::vector<double> row_ys = {0.0};
+ for (int row=1; row < NoOfRows; row++) {
+ row_ys.push_back(row_heights[row-1] + paddingy + row_ys[row-1]);
+ }
+
+ int cnt = 0;
+ std::vector<SPItem*>::iterator it = sorted.begin();
+ for (int row_cnt=0; ((it != sorted.end()) && (row_cnt<NoOfRows)); ++row_cnt) {
+
+ std::vector<SPItem *> current_row;
+ int col_cnt = 0;
+ for(;it!=sorted.end()&&col_cnt<NoOfCols;++it) {
+ current_row.push_back(*it);
+ col_cnt++;
+ }
+
+ for (auto item:current_row) {
+ auto min = Geom::Point(0, 0);
+ double width = 0, height = 0;
+ if (auto vbox = item->documentVisualBounds()) {
+ width = vbox->dimensions()[Geom::X];
+ height = vbox->dimensions()[Geom::Y];
+ min = vbox->min();
+ }
+
+ int row = cnt / NoOfCols;
+ int col = cnt % NoOfCols;
+
+ double new_x = grid_left + (((col_widths[col] - width)/2)*HorizAlign) + col_xs[col];
+ double new_y = grid_top + (((row_heights[row] - height)/2)*VertAlign) + row_ys[row];
+
+ Geom::Point move = Geom::Point(new_x, new_y) - min;
+ Geom::Affine const affine = Geom::Affine(Geom::Translate(move));
+ item->set_i2d_affine(item->i2doc_affine() * affine * item->document->doc2dt());
+ item->doWriteTransform(item->transform);
+ item->updateRepr();
+ cnt +=1;
+ }
+ }
+
+ DocumentUndo::done(desktop->getDocument(), _("Arrange in a grid"), INKSCAPE_ICON("dialog-align-and-distribute"));
+
+}
+
+
+//#########################################################################
+//## E V E N T S
+//#########################################################################
+
+/**
+ * changed value in # of columns spinbox.
+ */
+void GridArrangeTab::on_row_spinbutton_changed()
+{
+ SPDesktop *desktop = Parent->getDesktop();
+ Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr;
+ if (!selection) return;
+
+ int selcount = (int) boost::distance(selection->items());
+
+ double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value());
+ NoOfRowsSpinner.set_value(NoOfRows);
+}
+
+/**
+ * changed value in # of rows spinbox.
+ */
+void GridArrangeTab::on_col_spinbutton_changed()
+{
+ SPDesktop *desktop = Parent->getDesktop();
+ Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr;
+ if (!selection) return;
+
+ int selcount = (int) boost::distance(selection->items());
+
+ double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value());
+ NoOfColsSpinner.set_value(NoOfCols);
+}
+
+/**
+ * changed value in x padding spinbox.
+ */
+void GridArrangeTab::on_xpad_spinbutton_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/dialogs/gridtiler/XPad", XPadding.getValue("px"));
+
+}
+
+/**
+ * changed value in y padding spinbox.
+ */
+void GridArrangeTab::on_ypad_spinbutton_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/dialogs/gridtiler/YPad", YPadding.getValue("px"));
+}
+
+
+/**
+ * checked/unchecked autosize Rows button.
+ */
+void GridArrangeTab::on_RowSize_checkbutton_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (RowHeightButton.get_active()) {
+ prefs->setDouble("/dialogs/gridtiler/AutoRowSize", 20);
+ } else {
+ prefs->setDouble("/dialogs/gridtiler/AutoRowSize", -20);
+ }
+ RowHeightBox.set_sensitive ( !RowHeightButton.get_active());
+}
+
+/**
+ * checked/unchecked autosize Rows button.
+ */
+void GridArrangeTab::on_ColSize_checkbutton_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (ColumnWidthButton.get_active()) {
+ prefs->setDouble("/dialogs/gridtiler/AutoColSize", 20);
+ } else {
+ prefs->setDouble("/dialogs/gridtiler/AutoColSize", -20);
+ }
+ ColumnWidthBox.set_sensitive ( !ColumnWidthButton.get_active());
+}
+
+/**
+ * changed value in columns spinbox.
+ */
+void GridArrangeTab::on_rowSize_spinbutton_changed()
+{
+ // quit if run by the attr_changed listener
+ if (updating) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ updating = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/dialogs/gridtiler/RowHeight", RowHeightSpinner.get_value());
+ updating=false;
+
+}
+
+/**
+ * changed value in rows spinbox.
+ */
+void GridArrangeTab::on_colSize_spinbutton_changed()
+{
+ // quit if run by the attr_changed listener
+ if (updating) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ updating = true;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setDouble("/dialogs/gridtiler/ColWidth", ColumnWidthSpinner.get_value());
+ updating=false;
+
+}
+
+/**
+ * changed Radio button in Spacing group.
+ */
+void GridArrangeTab::Spacing_button_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (SpaceManualRadioButton.get_active()) {
+ prefs->setDouble("/dialogs/gridtiler/SpacingType", 20);
+ } else {
+ prefs->setDouble("/dialogs/gridtiler/SpacingType", -20);
+ }
+
+ XPadding.set_sensitive ( SpaceManualRadioButton.get_active());
+ YPadding.set_sensitive ( SpaceManualRadioButton.get_active());
+}
+
+/**
+ * changed Anchor selection widget.
+ */
+void GridArrangeTab::Align_changed()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ VertAlign = AlignmentSelector.getVerticalAlignment();
+ prefs->setInt("/dialogs/gridtiler/VertAlign", VertAlign);
+ HorizAlign = AlignmentSelector.getHorizontalAlignment();
+ prefs->setInt("/dialogs/gridtiler/HorizAlign", HorizAlign);
+}
+
+/**
+ * Desktop selection changed
+ */
+void GridArrangeTab::updateSelection()
+{
+ // quit if run by the attr_changed listener
+ if (updating) {
+ return;
+ }
+
+ // in turn, prevent listener from responding
+ updating = true;
+ SPDesktop *desktop = Parent->getDesktop();
+ Inkscape::Selection *selection = desktop ? desktop->getSelection() : nullptr;
+ std::vector<SPItem*> items;
+ if (selection) {
+ items.insert(items.end(), selection->items().begin(), selection->items().end());
+ }
+
+ if (!items.empty()) {
+ int selcount = items.size();
+
+ if (NoOfColsSpinner.get_value() > 1 && NoOfRowsSpinner.get_value() > 1){
+ // Update the number of rows assuming number of columns wanted remains same.
+ double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value());
+ NoOfRowsSpinner.set_value(NoOfRows);
+
+ // if the selection has less than the number set for one row, reduce it appropriately
+ if (selcount < NoOfColsSpinner.get_value()) {
+ double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value());
+ NoOfColsSpinner.set_value(NoOfCols);
+ }
+ } else {
+ double PerRow = ceil(sqrt(selcount));
+ double PerCol = ceil(sqrt(selcount));
+ NoOfRowsSpinner.set_value(PerRow);
+ NoOfColsSpinner.set_value(PerCol);
+ }
+ }
+
+ updating = false;
+}
+
+void GridArrangeTab::setDesktop(SPDesktop *desktop)
+{
+ _selection_changed_connection.disconnect();
+
+ if (desktop) {
+ updateSelection();
+
+ _selection_changed_connection = INKSCAPE.signal_selection_changed.connect(
+ sigc::hide<0>(sigc::mem_fun(*this, &GridArrangeTab::updateSelection)));
+ }
+}
+
+
+//#########################################################################
+//## C O N S T R U C T O R / D E S T R U C T O R
+//#########################################################################
+/**
+ * Constructor
+ */
+GridArrangeTab::GridArrangeTab(ArrangeDialog *parent)
+ : Parent(parent),
+ XPadding(_("X:"), _("Horizontal spacing between columns."), UNIT_TYPE_LINEAR, "", "object-columns", &PaddingUnitMenu),
+ YPadding(_("Y:"), _("Vertical spacing between rows."), XPadding, "", "object-rows"),
+ PaddingTable(Gtk::manage(new Gtk::Grid()))
+{
+ // bool used by spin button callbacks to stop loops where they change each other.
+ updating = false;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ auto _col1 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+ auto _col2 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+ auto _col3 = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
+
+ Gtk::Box *contents = this;
+ set_valign(Gtk::ALIGN_START);
+
+#define MARGIN 2
+
+ //##Set up the panel
+
+ NoOfRowsLabel.set_text_with_mnemonic(_("_Rows:"));
+ NoOfRowsLabel.set_mnemonic_widget(NoOfRowsSpinner);
+ NoOfRowsBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ NoOfRowsBox.pack_start(NoOfRowsLabel, false, false, MARGIN);
+
+ NoOfRowsSpinner.set_digits(0);
+ NoOfRowsSpinner.set_increments(1, 0);
+ NoOfRowsSpinner.set_range(1.0, 10000.0);
+ NoOfRowsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_col_spinbutton_changed));
+ NoOfRowsSpinner.set_tooltip_text(_("Number of rows"));
+ NoOfRowsBox.pack_start(NoOfRowsSpinner, false, false, MARGIN);
+ _col1->add_widget(NoOfRowsBox);
+
+ RowHeightButton.set_label(_("Equal _height"));
+ RowHeightButton.set_use_underline(true);
+ double AutoRow = prefs->getDouble("/dialogs/gridtiler/AutoRowSize", 15);
+ if (AutoRow>0)
+ AutoRowSize=true;
+ else
+ AutoRowSize=false;
+ RowHeightButton.set_active(AutoRowSize);
+
+ NoOfRowsBox.pack_start(RowHeightButton, false, false, MARGIN);
+
+ RowHeightButton.set_tooltip_text(_("If not set, each row has the height of the tallest object in it"));
+ RowHeightButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_RowSize_checkbutton_changed));
+
+ SpinsHBox.pack_start(NoOfRowsBox, false, false, MARGIN);
+
+
+ /*#### Label for X ####*/
+ padXByYLabel.set_label(" ");
+ XByYLabelVBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ XByYLabelVBox.pack_start(padXByYLabel, false, false, MARGIN);
+ XByYLabel.set_markup(" &#215; ");
+ XByYLabelVBox.pack_start(XByYLabel, false, false, MARGIN);
+ SpinsHBox.pack_start(XByYLabelVBox, false, false, MARGIN);
+ _col2->add_widget(XByYLabelVBox);
+
+ /*#### Number of columns ####*/
+
+ NoOfColsLabel.set_text_with_mnemonic(_("_Columns:"));
+ NoOfColsLabel.set_mnemonic_widget(NoOfColsSpinner);
+ NoOfColsBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ NoOfColsBox.pack_start(NoOfColsLabel, false, false, MARGIN);
+
+ NoOfColsSpinner.set_digits(0);
+ NoOfColsSpinner.set_increments(1, 0);
+ NoOfColsSpinner.set_range(1.0, 10000.0);
+ NoOfColsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_row_spinbutton_changed));
+ NoOfColsSpinner.set_tooltip_text(_("Number of columns"));
+ NoOfColsBox.pack_start(NoOfColsSpinner, false, false, MARGIN);
+ _col3->add_widget(NoOfColsBox);
+
+ ColumnWidthButton.set_label(_("Equal _width"));
+ ColumnWidthButton.set_use_underline(true);
+ double AutoCol = prefs->getDouble("/dialogs/gridtiler/AutoColSize", 15);
+ if (AutoCol>0)
+ AutoColSize=true;
+ else
+ AutoColSize=false;
+ ColumnWidthButton.set_active(AutoColSize);
+ NoOfColsBox.pack_start(ColumnWidthButton, false, false, MARGIN);
+
+ ColumnWidthButton.set_tooltip_text(_("If not set, each column has the width of the widest object in it"));
+ ColumnWidthButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ColSize_checkbutton_changed));
+
+ SpinsHBox.pack_start(NoOfColsBox, false, false, MARGIN);
+
+ TileBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ TileBox.pack_start(SpinsHBox, false, false, MARGIN);
+
+ VertAlign = prefs->getInt("/dialogs/gridtiler/VertAlign", 1);
+ HorizAlign = prefs->getInt("/dialogs/gridtiler/HorizAlign", 1);
+
+ // Anchor selection widget
+ AlignLabel.set_label(_("Alignment:"));
+ AlignLabel.set_halign(Gtk::ALIGN_START);
+ AlignLabel.set_valign(Gtk::ALIGN_CENTER);
+ AlignmentSelector.setAlignment(HorizAlign, VertAlign);
+ AlignmentSelector.on_selectionChanged().connect(sigc::mem_fun(*this, &GridArrangeTab::Align_changed));
+ TileBox.pack_start(AlignLabel, false, false, MARGIN);
+ TileBox.pack_start(AlignmentSelector, true, false, MARGIN);
+
+ {
+ /*#### Radio buttons to control spacing manually or to fit selection bbox ####*/
+ SpaceByBBoxRadioButton.set_label(_("_Fit into selection box"));
+ SpaceByBBoxRadioButton.set_use_underline (true);
+ SpaceByBBoxRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed));
+ SpacingGroup = SpaceByBBoxRadioButton.get_group();
+
+ SpacingVBox.pack_start(SpaceByBBoxRadioButton, false, false, MARGIN);
+
+ SpaceManualRadioButton.set_label(_("_Set spacing:"));
+ SpaceManualRadioButton.set_use_underline (true);
+ SpaceManualRadioButton.set_group(SpacingGroup);
+ SpaceManualRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed));
+ SpacingVBox.pack_start(SpaceManualRadioButton, false, false, MARGIN);
+
+ TileBox.pack_start(SpacingVBox, false, false, MARGIN);
+ }
+
+ {
+ /*#### Padding ####*/
+ PaddingUnitMenu.setUnitType(UNIT_TYPE_LINEAR);
+ PaddingUnitMenu.setUnit("px");
+
+ YPadding.setDigits(5);
+ YPadding.setIncrements(0.2, 0);
+ YPadding.setRange(-10000, 10000);
+ double yPad = prefs->getDouble("/dialogs/gridtiler/YPad", 15);
+ YPadding.setValue(yPad, "px");
+ YPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ypad_spinbutton_changed));
+
+ XPadding.setDigits(5);
+ XPadding.setIncrements(0.2, 0);
+ XPadding.setRange(-10000, 10000);
+ double xPad = prefs->getDouble("/dialogs/gridtiler/XPad", 15);
+ XPadding.setValue(xPad, "px");
+
+ XPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_xpad_spinbutton_changed));
+ }
+
+ PaddingTable->set_border_width(MARGIN);
+ PaddingTable->set_row_spacing(MARGIN);
+ PaddingTable->set_column_spacing(MARGIN);
+ PaddingTable->attach(XPadding, 0, 0, 1, 1);
+ PaddingTable->attach(PaddingUnitMenu, 1, 0, 1, 1);
+ PaddingTable->attach(YPadding, 0, 1, 1, 1);
+
+ TileBox.pack_start(*PaddingTable, false, false, MARGIN);
+
+ contents->set_border_width(4);
+ contents->pack_start(TileBox);
+
+ double SpacingType = prefs->getDouble("/dialogs/gridtiler/SpacingType", 15);
+ if (SpacingType>0) {
+ ManualSpacing=true;
+ } else {
+ ManualSpacing=false;
+ }
+ SpaceManualRadioButton.set_active(ManualSpacing);
+ SpaceByBBoxRadioButton.set_active(!ManualSpacing);
+ XPadding.set_sensitive (ManualSpacing);
+ YPadding.set_sensitive (ManualSpacing);
+
+ show_all_children();
+}
+
+
+GridArrangeTab::~GridArrangeTab() {
+ setDesktop(nullptr);
+}
+
+} //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/dialog/grid-arrange-tab.h b/src/ui/dialog/grid-arrange-tab.h
new file mode 100644
index 0000000..6c244b3
--- /dev/null
+++ b/src/ui/dialog/grid-arrange-tab.h
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @brief Arranges Objects into a Grid
+ */
+/* Authors:
+ * Bob Jamison ( based off trace dialog)
+ * John Cliff
+ * Other dudes from The Inkscape Organization
+ * Abhishek Sharma
+ * Declara Denis
+ *
+ * Copyright (C) 2004 Bob Jamison
+ * Copyright (C) 2004 John Cliff
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H
+#define INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H
+
+#include "ui/widget/scalar-unit.h"
+#include "ui/dialog/arrange-tab.h"
+
+#include "ui/widget/anchor-selector.h"
+#include "ui/widget/spinbutton.h"
+
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/radiobuttongroup.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class ArrangeDialog;
+
+/**
+ * Dialog for tiling an object
+ */
+class GridArrangeTab : public ArrangeTab {
+public:
+ GridArrangeTab(ArrangeDialog *parent);
+ ~GridArrangeTab() override;
+
+ /**
+ * Do the actual work
+ */
+ void arrange() override;
+
+ /**
+ * Respond to selection change
+ */
+ void updateSelection();
+
+ // Callbacks from spinbuttons
+ void on_row_spinbutton_changed();
+ void on_col_spinbutton_changed();
+ void on_xpad_spinbutton_changed();
+ void on_ypad_spinbutton_changed();
+ void on_RowSize_checkbutton_changed();
+ void on_ColSize_checkbutton_changed();
+ void on_rowSize_spinbutton_changed();
+ void on_colSize_spinbutton_changed();
+ void Spacing_button_changed();
+ void Align_changed();
+
+
+private:
+ GridArrangeTab(GridArrangeTab const &d) = delete; // no copy
+ void operator=(GridArrangeTab const &d) = delete; // no assign
+
+ ArrangeDialog *Parent;
+
+ bool updating;
+
+ Gtk::Box TileBox;
+
+ // Number selected label
+ Gtk::Label SelectionContentsLabel;
+
+
+ Gtk::Box AlignHBox;
+ Gtk::Box SpinsHBox;
+
+ // Number per Row
+ Gtk::Box NoOfColsBox;
+ Gtk::Label NoOfColsLabel;
+ Inkscape::UI::Widget::SpinButton NoOfColsSpinner;
+ bool AutoRowSize;
+ Gtk::CheckButton RowHeightButton;
+
+ Gtk::Box XByYLabelVBox;
+ Gtk::Label padXByYLabel;
+ Gtk::Label XByYLabel;
+
+ // Number per Column
+ Gtk::Box NoOfRowsBox;
+ Gtk::Label NoOfRowsLabel;
+ Inkscape::UI::Widget::SpinButton NoOfRowsSpinner;
+ bool AutoColSize;
+ Gtk::CheckButton ColumnWidthButton;
+
+ // Alignment
+ Gtk::Label AlignLabel;
+ Inkscape::UI::Widget::AnchorSelector AlignmentSelector;
+ double VertAlign;
+ double HorizAlign;
+
+ Inkscape::UI::Widget::UnitMenu PaddingUnitMenu;
+ Inkscape::UI::Widget::ScalarUnit XPadding;
+ Inkscape::UI::Widget::ScalarUnit YPadding;
+ Gtk::Grid *PaddingTable;
+
+ // BBox or manual spacing
+ Gtk::Box SpacingVBox;
+ Gtk::RadioButtonGroup SpacingGroup;
+ Gtk::RadioButton SpaceByBBoxRadioButton;
+ Gtk::RadioButton SpaceManualRadioButton;
+ bool ManualSpacing;
+
+ // Row height
+ Gtk::Box RowHeightBox;
+ Inkscape::UI::Widget::SpinButton RowHeightSpinner;
+
+ // Column width
+ Gtk::Box ColumnWidthBox;
+ Inkscape::UI::Widget::SpinButton ColumnWidthSpinner;
+
+ sigc::connection _selection_changed_connection;
+
+ public:
+ void setDesktop(SPDesktop *);
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif /* INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_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/dialog/guides.cpp b/src/ui/dialog/guides.cpp
new file mode 100644
index 0000000..dcb1d40
--- /dev/null
+++ b/src/ui/dialog/guides.cpp
@@ -0,0 +1,387 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Simple guideline dialog.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Andrius R. <knutux@gmail.com>
+ * Johan Engelen
+ * Abhishek Sharma
+ *
+ * Copyright (C) 1999-2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "guides.h"
+
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "message-context.h"
+
+#include "include/gtkmm_version.h"
+
+#include "object/sp-guide.h"
+#include "object/sp-namedview.h"
+
+#include "page-manager.h"
+
+#include "ui/dialog-events.h"
+#include "ui/tools/tool-base.h"
+#include "ui/widget/spinbutton.h"
+
+#include "widgets/desktop-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+GuidelinePropertiesDialog::GuidelinePropertiesDialog(SPGuide *guide, SPDesktop *desktop)
+: _desktop(desktop), _guide(guide),
+ _locked_toggle(_("Lo_cked")),
+ _relative_toggle(_("Rela_tive change")),
+ _spin_button_x(C_("Guides", "_X:"), "", UNIT_TYPE_LINEAR, "", "", &_unit_menu),
+ _spin_button_y(C_("Guides", "_Y:"), "", UNIT_TYPE_LINEAR, "", "", &_unit_menu),
+ _label_entry(_("_Label:"), _("Optionally give this guideline a name")),
+ _spin_angle(_("_Angle:"), "", UNIT_TYPE_RADIAL),
+ _mode(true), _oldpos(0.,0.), _oldangle(0.0)
+{
+ _locked_toggle.set_use_underline();
+ _locked_toggle.set_tooltip_text(_("Lock the movement of guides"));
+ _relative_toggle.set_use_underline();
+ _relative_toggle.set_tooltip_text(_("Move and/or rotate the guide relative to current settings"));
+}
+
+bool GuidelinePropertiesDialog::_relative_toggle_status = false; // initialize relative checkbox status for when this dialog is opened for first time
+Glib::ustring GuidelinePropertiesDialog::_angle_unit_status = DEG; // initialize angle unit status
+
+GuidelinePropertiesDialog::~GuidelinePropertiesDialog() {
+ // save current status
+ _relative_toggle_status = _relative_toggle.get_active();
+ _angle_unit_status = _spin_angle.getUnit()->abbr;
+}
+
+void GuidelinePropertiesDialog::showDialog(SPGuide *guide, SPDesktop *desktop) {
+ GuidelinePropertiesDialog dialog(guide, desktop);
+ dialog._setup();
+ dialog.run();
+}
+
+void GuidelinePropertiesDialog::_modeChanged()
+{
+ _mode = !_relative_toggle.get_active();
+ if (!_mode) {
+ // relative
+ _spin_angle.setValue(0);
+
+ _spin_button_y.setValue(0);
+ _spin_button_x.setValue(0);
+ } else {
+ // absolute
+ _spin_angle.setValueKeepUnit(_oldangle, DEG);
+
+ auto pos = _oldpos;
+
+ // Adjust position by the page position
+ auto prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/options/origincorrection/page", true)) {
+ auto &pm = _guide->document->getPageManager();
+ pos *= pm.getSelectedPageAffine().inverse();
+ }
+
+ _spin_button_x.setValueKeepUnit(pos[Geom::X], "px");
+ _spin_button_y.setValueKeepUnit(pos[Geom::Y], "px");
+ }
+}
+
+void GuidelinePropertiesDialog::_onOK()
+{
+ this->_onOKimpl();
+ DocumentUndo::done(_guide->document, _("Set guide properties"), "");
+
+}
+
+void GuidelinePropertiesDialog::_onOKimpl()
+{
+ double deg_angle = _spin_angle.getValue(DEG);
+ if (!_mode)
+ deg_angle += _oldangle;
+ Geom::Point normal;
+ if ( deg_angle == 90. || deg_angle == 270. || deg_angle == -90. || deg_angle == -270.) {
+ normal = Geom::Point(1.,0.);
+ } else if ( deg_angle == 0. || deg_angle == 180. || deg_angle == -180.) {
+ normal = Geom::Point(0.,1.);
+ } else {
+ double rad_angle = Geom::rad_from_deg( deg_angle );
+ normal = Geom::rot90(Geom::Point::polar(rad_angle, 1.0));
+ }
+ //To allow reposition from dialog
+ _guide->set_locked(false, false);
+
+ _guide->set_normal(normal, true);
+
+ double const points_x = _spin_button_x.getValue("px");
+ double const points_y = _spin_button_y.getValue("px");
+ Geom::Point newpos(points_x, points_y);
+
+ // Adjust position by either the relative position, or the page offset
+ auto prefs = Inkscape::Preferences::get();
+ if (!_mode) {
+ newpos += _oldpos;
+ } else if (prefs->getBool("/options/origincorrection/page", true)) {
+ auto &pm = _guide->document->getPageManager();
+ newpos *= pm.getSelectedPageAffine();
+ }
+
+ _guide->moveto(newpos, true);
+
+ const gchar* name = g_strdup( _label_entry.getEntry()->get_text().c_str() );
+
+ _guide->set_label(name, true);
+
+ const bool locked = _locked_toggle.get_active();
+
+ _guide->set_locked(locked, true);
+
+ g_free((gpointer) name);
+
+ const auto c = _color.get_rgba();
+ unsigned r = c.get_red_u()/257, g = c.get_green_u()/257, b = c.get_blue_u()/257;
+ //TODO: why 257? verify this!
+ // don't know why, but introduced: 761f7da58cd6d625b88c24eee6fae1b7fa3bfcdd
+
+ _guide->set_color(r, g, b, true);
+}
+
+void GuidelinePropertiesDialog::_onDelete()
+{
+ SPDocument *doc = _guide->document;
+ if (_guide->remove(true))
+ DocumentUndo::done(doc, _("Delete guide"), "");
+}
+
+void GuidelinePropertiesDialog::_onDuplicate()
+{
+ _guide = _guide->duplicate();
+ this->_onOKimpl();
+ DocumentUndo::done(_guide->document, _("Duplicate guide"), "");
+}
+
+void GuidelinePropertiesDialog::_response(gint response)
+{
+ switch (response) {
+ case Gtk::RESPONSE_OK:
+ _onOK();
+ break;
+ case -12:
+ _onDelete();
+ break;
+ case -13:
+ _onDuplicate();
+ break;
+ case Gtk::RESPONSE_CANCEL:
+ break;
+ case Gtk::RESPONSE_DELETE_EVENT:
+ break;
+ default:
+ g_assert_not_reached();
+ }
+}
+
+void GuidelinePropertiesDialog::_setup() {
+ set_title(_("Guideline"));
+ add_button(_("_OK"), Gtk::RESPONSE_OK);
+ add_button(_("_Duplicate"), -13);
+ add_button(_("_Delete"), -12);
+ add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL);
+
+ auto mainVBox = get_content_area();
+ _layout_table.set_row_spacing(4);
+ _layout_table.set_column_spacing(4);
+ _layout_table.set_border_width(4);
+
+ mainVBox->pack_start(_layout_table, false, false, 0);
+
+ _label_name.set_label("foo0");
+ _label_name.set_halign(Gtk::ALIGN_START);
+ _label_name.set_valign(Gtk::ALIGN_CENTER);
+
+ _label_descr.set_label("foo1");
+ _label_descr.set_halign(Gtk::ALIGN_START);
+ _label_descr.set_valign(Gtk::ALIGN_CENTER);
+
+ _label_name.set_halign(Gtk::ALIGN_FILL);
+ _label_name.set_valign(Gtk::ALIGN_FILL);
+ _layout_table.attach(_label_name, 0, 0, 3, 1);
+
+ _label_descr.set_halign(Gtk::ALIGN_FILL);
+ _label_descr.set_valign(Gtk::ALIGN_FILL);
+ _layout_table.attach(_label_descr, 0, 1, 3, 1);
+
+ _label_entry.set_halign(Gtk::ALIGN_FILL);
+ _label_entry.set_valign(Gtk::ALIGN_FILL);
+ _label_entry.set_hexpand();
+ _layout_table.attach(_label_entry, 1, 2, 2, 1);
+
+ _color.set_halign(Gtk::ALIGN_FILL);
+ _color.set_valign(Gtk::ALIGN_FILL);
+ _color.set_hexpand();
+ _color.set_margin_end(6);
+ _layout_table.attach(_color, 1, 3, 2, 1);
+
+ // unitmenus
+ /* fixme: We should allow percents here too, as percents of the canvas size */
+ _unit_menu.setUnitType(UNIT_TYPE_LINEAR);
+ _unit_menu.setUnit("px");
+ if (_desktop->namedview->display_units) {
+ _unit_menu.setUnit( _desktop->namedview->display_units->abbr );
+ }
+ _spin_angle.setUnit(_angle_unit_status);
+
+ // position spinbuttons
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ size_t minimumexponent = std::min(std::abs(prefs->getInt("/options/svgoutput/minimumexponent", -8)), 5); //we limit to 5 to minimize rounding errors
+ _spin_button_x.setDigits(minimumexponent);
+ _spin_button_x.setAlignment(1.0);
+ _spin_button_x.setIncrements(1.0, 10.0);
+ _spin_button_x.setRange(-1e6, 1e6);
+ _spin_button_y.setDigits(minimumexponent);
+
+ _spin_button_y.setAlignment(1.0);
+ _spin_button_y.setIncrements(1.0, 10.0);
+ _spin_button_y.setRange(-1e6, 1e6);
+
+ _spin_button_x.set_halign(Gtk::ALIGN_FILL);
+ _spin_button_x.set_valign(Gtk::ALIGN_FILL);
+ _spin_button_x.set_hexpand();
+ _layout_table.attach(_spin_button_x, 1, 4, 1, 1);
+
+ _spin_button_y.set_halign(Gtk::ALIGN_FILL);
+ _spin_button_y.set_valign(Gtk::ALIGN_FILL);
+ _spin_button_y.set_hexpand();
+ _layout_table.attach(_spin_button_y, 1, 5, 1, 1);
+
+ _unit_menu.set_halign(Gtk::ALIGN_FILL);
+ _unit_menu.set_valign(Gtk::ALIGN_FILL);
+ _unit_menu.set_margin_end(6);
+ _layout_table.attach(_unit_menu, 2, 4, 1, 1);
+
+ // angle spinbutton
+ _spin_angle.setDigits(3);
+ _spin_angle.setDigits(minimumexponent);
+ _spin_angle.setAlignment(1.0);
+ _spin_angle.setIncrements(1.0, 10.0);
+ _spin_angle.setRange(-3600., 3600.);
+
+ _spin_angle.set_halign(Gtk::ALIGN_FILL);
+ _spin_angle.set_valign(Gtk::ALIGN_FILL);
+ _spin_angle.set_hexpand();
+ _layout_table.attach(_spin_angle, 1, 6, 2, 1);
+
+ // mode radio button
+ _relative_toggle.set_halign(Gtk::ALIGN_FILL);
+ _relative_toggle.set_valign(Gtk::ALIGN_FILL);
+ _relative_toggle.set_hexpand();
+ _relative_toggle.set_margin_start(6);
+ _layout_table.attach(_relative_toggle, 1, 7, 2, 1);
+
+ // locked radio button
+ _locked_toggle.set_halign(Gtk::ALIGN_FILL);
+ _locked_toggle.set_valign(Gtk::ALIGN_FILL);
+ _locked_toggle.set_hexpand();
+ _locked_toggle.set_margin_start(6);
+ _layout_table.attach(_locked_toggle, 1, 8, 2, 1);
+
+ _relative_toggle.signal_toggled().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::_modeChanged));
+ _relative_toggle.set_active(_relative_toggle_status);
+
+ bool global_guides_lock = _desktop->namedview->getLockGuides();
+ if(global_guides_lock){
+ _locked_toggle.set_sensitive(false);
+ }
+ _locked_toggle.set_active(_guide->getLocked());
+
+ // This results in the dialog closing when entering a value in one of the spinbuttons and pressing enter (see LP bug 484187)
+ auto sbx = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_x.getWidget());
+ auto sby = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_y.getWidget());
+ auto sba = dynamic_cast<UI::Widget::SpinButton *>(_spin_button_y.getWidget());
+
+ if(sbx) sbx->signal_activate().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::on_sb_activate));
+ if(sby) sby->signal_activate().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::on_sb_activate));
+ if(sba) sba->signal_activate().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::on_sb_activate));
+
+ // dialog
+ set_default_response(Gtk::RESPONSE_OK);
+ signal_response().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::_response));
+
+ // initialize dialog
+ _oldpos = _guide->getPoint();
+ if (_guide->isVertical()) {
+ _oldangle = 90;
+ } else if (_guide->isHorizontal()) {
+ _oldangle = 0;
+ } else {
+ _oldangle = Geom::deg_from_rad( std::atan2( - _guide->getNormal()[Geom::X], _guide->getNormal()[Geom::Y] ) );
+ }
+
+ {
+ gchar *label = g_strdup_printf(_("Guideline ID: %s"), _guide->getId());
+ _label_name.set_label(label);
+ g_free(label);
+ }
+ {
+ gchar *guide_description = _guide->description(false);
+ gchar *label = g_strdup_printf(_("Current: %s"), guide_description);
+ g_free(guide_description);
+ _label_descr.set_markup(label);
+ g_free(label);
+ }
+
+ // init name entry
+ _label_entry.getEntry()->set_text(_guide->getLabel() ? _guide->getLabel() : "");
+
+ Gdk::RGBA c;
+ c.set_rgba(((_guide->getColor()>>24)&0xff) / 255.0, ((_guide->getColor()>>16)&0xff) / 255.0, ((_guide->getColor()>>8)&0xff) / 255.0);
+ _color.set_rgba(c);
+
+ _modeChanged(); // sets values of spinboxes.
+
+ if ( _oldangle == 90. || _oldangle == 270. || _oldangle == -90. || _oldangle == -270.) {
+ _spin_button_x.grabFocusAndSelectEntry();
+ } else if ( _oldangle == 0. || _oldangle == 180. || _oldangle == -180.) {
+ _spin_button_y.grabFocusAndSelectEntry();
+ } else {
+ _spin_angle.grabFocusAndSelectEntry();
+ }
+
+ set_position(Gtk::WIN_POS_MOUSE);
+
+ show_all_children();
+ set_modal(true);
+ _desktop->setWindowTransient (gobj());
+ property_destroy_with_parent() = true;
+}
+
+void
+GuidelinePropertiesDialog::on_sb_activate()
+{
+ activate_default();
+}
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/guides.h b/src/ui/dialog/guides.h
new file mode 100644
index 0000000..9c449ce
--- /dev/null
+++ b/src/ui/dialog/guides.h
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Andrius R. <knutux@gmail.com>
+ * Johan Engelen
+ *
+ * Copyright (C) 2006-2007 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_GUIDELINE_H
+#define INKSCAPE_DIALOG_GUIDELINE_H
+
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/colorbutton.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+
+#include "ui/widget/unit-menu.h"
+#include "ui/widget/scalar-unit.h"
+#include "ui/widget/entry.h"
+#include <2geom/point.h>
+
+class SPGuide;
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+ class UnitMenu;
+};
+
+namespace Dialogs {
+
+/**
+ * Dialog for modifying guidelines.
+ */
+class GuidelinePropertiesDialog : public Gtk::Dialog {
+public:
+ GuidelinePropertiesDialog(SPGuide *guide, SPDesktop *desktop);
+ ~GuidelinePropertiesDialog() override;
+
+ Glib::ustring getName() const { return "GuidelinePropertiesDialog"; }
+
+ static void showDialog(SPGuide *guide, SPDesktop *desktop);
+
+protected:
+ void _setup();
+
+ void _onOK();
+ void _onOKimpl();
+ void _onDelete();
+ void _onDuplicate();
+
+ void _response(gint response);
+ void _modeChanged();
+
+private:
+ GuidelinePropertiesDialog(GuidelinePropertiesDialog const &) = delete; // no copy
+ GuidelinePropertiesDialog &operator=(GuidelinePropertiesDialog const &) = delete; // no assign
+
+ SPDesktop *_desktop;
+ SPGuide *_guide;
+
+ Gtk::Grid _layout_table;
+ Gtk::Label _label_name;
+ Gtk::Label _label_descr;
+ Gtk::CheckButton _locked_toggle;
+ Gtk::CheckButton _relative_toggle;
+ static bool _relative_toggle_status; // remember the status of the _relative_toggle_status button across instances
+ Inkscape::UI::Widget::UnitMenu _unit_menu;
+ Inkscape::UI::Widget::ScalarUnit _spin_button_x;
+ Inkscape::UI::Widget::ScalarUnit _spin_button_y;
+ Inkscape::UI::Widget::Entry _label_entry;
+ Gtk::ColorButton _color;
+
+ Inkscape::UI::Widget::ScalarUnit _spin_angle;
+ static Glib::ustring _angle_unit_status; // remember the status of the _relative_toggle_status button across instances
+
+ bool _mode;
+ Geom::Point _oldpos;
+ gdouble _oldangle;
+
+ void on_sb_activate();
+};
+
+} // namespace
+} // namespace
+} // namespace
+
+
+#endif // INKSCAPE_DIALOG_GUIDELINE_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/dialog/icon-preview.cpp b/src/ui/dialog/icon-preview.cpp
new file mode 100644
index 0000000..d62937c
--- /dev/null
+++ b/src/ui/dialog/icon-preview.cpp
@@ -0,0 +1,637 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * A simple dialog for previewing icon representation.
+ */
+/* Authors:
+ * Jon A. Cruz
+ * Bob Jamison
+ * Other dudes from The Inkscape Organization
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004 Bob Jamison
+ * Copyright (C) 2005,2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <glibmm/timer.h>
+#include <glibmm/main.h>
+
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/frame.h>
+
+#include "desktop.h"
+#include "document.h"
+#include "inkscape.h"
+#include "page-manager.h"
+
+#include "display/cairo-utils.h"
+#include "display/drawing.h"
+#include "display/drawing-context.h"
+
+#include "object/sp-namedview.h"
+#include "object/sp-root.h"
+
+#include "icon-preview.h"
+
+#include "ui/widget/frame.h"
+
+extern "C" {
+// takes doc, drawing, icon, and icon name to produce pixels
+guchar *
+sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing,
+ const gchar *name, unsigned int psize, unsigned &stride);
+}
+
+#define noICON_VERBOSE 1
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+//#########################################################################
+//## E V E N T S
+//#########################################################################
+
+void IconPreviewPanel::on_button_clicked(int which)
+{
+ if ( hot != which ) {
+ buttons[hot]->set_active( false );
+
+ hot = which;
+ updateMagnify();
+ queue_draw();
+ }
+}
+
+
+
+
+//#########################################################################
+//## C O N S T R U C T O R / D E S T R U C T O R
+//#########################################################################
+/**
+ * Constructor
+ */
+IconPreviewPanel::IconPreviewPanel()
+ : DialogBase("/dialogs/iconpreview", "IconPreview")
+ , drawing(nullptr)
+ , drawing_doc(nullptr)
+ , visionkey(0)
+ , timer(nullptr)
+ , renderTimer(nullptr)
+ , pending(false)
+ , minDelay(0.1)
+ , targetId()
+ , hot(1)
+ , selectionButton(nullptr)
+ , docModConn()
+ , iconBox(Gtk::ORIENTATION_VERTICAL)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ numEntries = 0;
+
+ bool pack = prefs->getBool("/iconpreview/pack", true);
+
+ std::vector<Glib::ustring> pref_sizes = prefs->getAllDirs("/iconpreview/sizes/default");
+ std::vector<int> rawSizes;
+
+ for (auto & pref_size : pref_sizes) {
+ if (prefs->getBool(pref_size + "/show", true)) {
+ int sizeVal = prefs->getInt(pref_size + "/value", -1);
+ if (sizeVal > 0) {
+ rawSizes.push_back(sizeVal);
+ }
+ }
+ }
+
+ if ( !rawSizes.empty() ) {
+ numEntries = rawSizes.size();
+ sizes = new int[numEntries];
+ int i = 0;
+ for ( std::vector<int>::iterator it = rawSizes.begin(); it != rawSizes.end(); ++it, ++i ) {
+ sizes[i] = *it;
+ }
+ }
+
+ if ( numEntries < 1 )
+ {
+ numEntries = 5;
+ sizes = new int[numEntries];
+ sizes[0] = 16;
+ sizes[1] = 24;
+ sizes[2] = 32;
+ sizes[3] = 48;
+ sizes[4] = 128;
+ }
+
+ pixMem = new guchar*[numEntries];
+ images = new Gtk::Image*[numEntries];
+ labels = new Glib::ustring*[numEntries];
+ buttons = new Gtk::ToggleToolButton*[numEntries];
+
+
+ for ( int i = 0; i < numEntries; i++ ) {
+ char *label = g_strdup_printf(_("%d x %d"), sizes[i], sizes[i]);
+ labels[i] = new Glib::ustring(label);
+ g_free(label);
+ pixMem[i] = nullptr;
+ images[i] = nullptr;
+ }
+
+
+ magLabel.set_label( *labels[hot] );
+
+ Gtk::Box* magBox = new Gtk::Box(Gtk::ORIENTATION_VERTICAL);
+
+ UI::Widget::Frame *magFrame = Gtk::manage(new UI::Widget::Frame(_("Magnified:")));
+ magFrame->add( magnified );
+
+ magBox->pack_start( *magFrame, Gtk::PACK_EXPAND_WIDGET );
+ magBox->pack_start( magLabel, Gtk::PACK_SHRINK );
+
+
+ Gtk::Box *verts = new Gtk::Box(Gtk::ORIENTATION_VERTICAL);
+ Gtk::Box *horiz = nullptr;
+ int previous = 0;
+ int avail = 0;
+ for ( int i = numEntries - 1; i >= 0; --i ) {
+ int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]);
+ pixMem[i] = new guchar[sizes[i] * stride];
+ memset( pixMem[i], 0x00, sizes[i] * stride );
+
+ auto pb = Gdk::Pixbuf::create_from_data(pixMem[i], Gdk::COLORSPACE_RGB, true, 8, sizes[i], sizes[i], stride);
+ images[i] = Gtk::make_managed<Gtk::Image>(pb);
+ Glib::ustring label(*labels[i]);
+ buttons[i] = new Gtk::ToggleToolButton(label);
+ buttons[i]->set_active( i == hot );
+ if ( prefs->getBool("/iconpreview/showFrames", true) ) {
+ Gtk::Frame *frame = new Gtk::Frame();
+ frame->set_shadow_type(Gtk::SHADOW_ETCHED_IN);
+ frame->add(*images[i]);
+ buttons[i]->set_icon_widget(*Gtk::manage(frame));
+ } else {
+ buttons[i]->set_icon_widget(*images[i]);
+ }
+
+ buttons[i]->set_tooltip_text(label);
+
+ buttons[i]->signal_clicked().connect( sigc::bind<int>( sigc::mem_fun(*this, &IconPreviewPanel::on_button_clicked), i) );
+
+ buttons[i]->set_halign(Gtk::ALIGN_CENTER);
+ buttons[i]->set_valign(Gtk::ALIGN_CENTER);
+
+ if ( !pack || ( (avail == 0) && (previous == 0) ) ) {
+ verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK);
+ previous = sizes[i];
+ avail = sizes[i];
+ } else {
+ int pad = 12;
+ if ((avail < pad) || ((sizes[i] > avail) && (sizes[i] < previous))) {
+ horiz = nullptr;
+ }
+ if ((horiz == nullptr) && (sizes[i] <= previous)) {
+ avail = previous;
+ }
+ if (sizes[i] <= avail) {
+ if (!horiz) {
+ horiz = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ avail = previous;
+ verts->pack_end(*horiz, Gtk::PACK_SHRINK);
+ }
+ horiz->pack_start(*(buttons[i]), Gtk::PACK_EXPAND_WIDGET);
+ avail -= sizes[i];
+ avail -= pad; // a little extra for padding
+ } else {
+ horiz = nullptr;
+ verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK);
+ }
+ }
+ }
+
+ iconBox.pack_start(splitter);
+ splitter.pack1( *magBox, true, false );
+ UI::Widget::Frame *actuals = Gtk::manage(new UI::Widget::Frame (_("Actual Size:")));
+ actuals->set_border_width(4);
+ actuals->add(*verts);
+ splitter.pack2( *actuals, false, false );
+
+
+ selectionButton = new Gtk::CheckButton(C_("Icon preview window", "Sele_ction"), true);//selectionButton = (Gtk::ToggleButton*) gtk_check_button_new_with_mnemonic(_("_Selection")); // , GTK_RESPONSE_APPLY
+ magBox->pack_start( *selectionButton, Gtk::PACK_SHRINK );
+ selectionButton->set_tooltip_text(_("Selection only or whole document"));
+ selectionButton->signal_clicked().connect( sigc::mem_fun(*this, &IconPreviewPanel::modeToggled) );
+
+ gint val = prefs->getBool("/iconpreview/selectionOnly");
+ selectionButton->set_active( val != 0 );
+
+ pack_start(iconBox, Gtk::PACK_SHRINK);
+
+ show_all_children();
+
+ refreshPreview();
+}
+
+IconPreviewPanel::~IconPreviewPanel()
+{
+ removeDrawing();
+ if (timer) {
+ timer->stop();
+ delete timer;
+ timer = nullptr;
+ }
+ if ( renderTimer ) {
+ renderTimer->stop();
+ delete renderTimer;
+ renderTimer = nullptr;
+ }
+
+ docModConn.disconnect();
+}
+
+//#########################################################################
+//## M E T H O D S
+//#########################################################################
+
+
+#if ICON_VERBOSE
+static Glib::ustring getTimestr()
+{
+ Glib::ustring str;
+ gint64 micr = g_get_monotonic_time();
+ gint64 mins = ((int)round(micr / 60000000)) % 60;
+ gdouble dsecs = micr / 1000000;
+ gchar *ptr = g_strdup_printf(":%02u:%f", mins, dsecs);
+ str = ptr;
+ g_free(ptr);
+ ptr = 0;
+ return str;
+}
+#endif // ICON_VERBOSE
+
+void IconPreviewPanel::selectionModified(Selection *selection, guint flags)
+{
+ if (getDesktop() && Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true)) {
+ queueRefresh();
+ }
+}
+
+void IconPreviewPanel::documentReplaced()
+{
+ removeDrawing();
+ drawing_doc = getDocument();
+ if (drawing_doc) {
+ drawing = new Inkscape::Drawing();
+ visionkey = SPItem::display_key_new(1);
+ drawing->setRoot(drawing_doc->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
+ docDesConn = drawing_doc->connectDestroy([=]() { removeDrawing(); });
+ queueRefresh();
+ }
+}
+
+/// Safely delete the Inkscape::Drawing and references to it.
+void IconPreviewPanel::removeDrawing()
+{
+ docDesConn.disconnect();
+ if (!drawing) {
+ return;
+ }
+ drawing_doc->getRoot()->invoke_hide(visionkey);
+ delete drawing;
+ drawing = nullptr;
+ drawing_doc = nullptr;
+}
+
+void IconPreviewPanel::refreshPreview()
+{
+ auto document = getDocument();
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ if (timer->elapsed() < minDelay) {
+#if ICON_VERBOSE
+ g_message( "%s Deferring refresh as too soon. calling queueRefresh()", getTimestr().c_str() );
+#endif //ICON_VERBOSE
+ // Do not refresh too quickly
+ queueRefresh();
+ } else if (document) {
+#if ICON_VERBOSE
+ g_message( "%s Refreshing preview.", getTimestr().c_str() );
+#endif // ICON_VERBOSE
+ bool hold = Inkscape::Preferences::get()->getBool("/iconpreview/selectionHold", true);
+ SPObject *target = nullptr;
+ if ( selectionButton && selectionButton->get_active() )
+ {
+ target = (hold && !targetId.empty()) ? document->getObjectById( targetId.c_str() ) : nullptr;
+ if ( !target ) {
+ targetId.clear();
+ if (auto selection = getSelection()) {
+ for (auto item : selection->items()) {
+ if (gchar const *id = item->getId()) {
+ targetId = id;
+ target = item;
+ }
+ }
+ }
+ }
+ } else {
+ target = getDesktop()->getDocument()->getRoot();
+ }
+ if (target) {
+ renderPreview(target);
+ }
+#if ICON_VERBOSE
+ g_message( "%s resetting timer", getTimestr().c_str() );
+#endif // ICON_VERBOSE
+ timer->reset();
+ }
+}
+
+bool IconPreviewPanel::refreshCB()
+{
+ bool callAgain = true;
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ if ( timer->elapsed() > minDelay ) {
+#if ICON_VERBOSE
+ g_message( "%s refreshCB() timer has progressed", getTimestr().c_str() );
+#endif // ICON_VERBOSE
+ callAgain = false;
+ refreshPreview();
+#if ICON_VERBOSE
+ g_message( "%s refreshCB() setting pending false", getTimestr().c_str() );
+#endif // ICON_VERBOSE
+ pending = false;
+ }
+ return callAgain;
+}
+
+void IconPreviewPanel::queueRefresh()
+{
+ if (!pending) {
+ pending = true;
+#if ICON_VERBOSE
+ g_message( "%s queueRefresh() Setting pending true", getTimestr().c_str() );
+#endif // ICON_VERBOSE
+ if (!timer) {
+ timer = new Glib::Timer();
+ }
+ Glib::signal_idle().connect( sigc::mem_fun(*this, &IconPreviewPanel::refreshCB), Glib::PRIORITY_DEFAULT_IDLE );
+ }
+}
+
+void IconPreviewPanel::modeToggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool selectionOnly = (selectionButton && selectionButton->get_active());
+ prefs->setBool("/iconpreview/selectionOnly", selectionOnly);
+ if ( !selectionOnly ) {
+ targetId.clear();
+ }
+
+ refreshPreview();
+}
+
+void overlayPixels(guchar *px, int width, int height, int stride,
+ unsigned r, unsigned g, unsigned b)
+{
+ int bytesPerPixel = 4;
+ int spacing = 4;
+ for ( int y = 0; y < height; y += spacing ) {
+ guchar *ptr = px + y * stride;
+ for ( int x = 0; x < width; x += spacing ) {
+ *(ptr++) = r;
+ *(ptr++) = g;
+ *(ptr++) = b;
+ *(ptr++) = 0xff;
+
+ ptr += bytesPerPixel * (spacing - 1);
+ }
+ }
+
+ if ( width > 1 && height > 1 ) {
+ // point at the last pixel
+ guchar *ptr = px + ((height-1) * stride) + ((width - 1) * bytesPerPixel);
+
+ if ( width > 2 ) {
+ px[4] = r;
+ px[5] = g;
+ px[6] = b;
+ px[7] = 0xff;
+
+ ptr[-12] = r;
+ ptr[-11] = g;
+ ptr[-10] = b;
+ ptr[-9] = 0xff;
+ }
+
+ ptr[-4] = r;
+ ptr[-3] = g;
+ ptr[-2] = b;
+ ptr[-1] = 0xff;
+
+ px[0 + stride] = r;
+ px[1 + stride] = g;
+ px[2 + stride] = b;
+ px[3 + stride] = 0xff;
+
+ ptr[0 - stride] = r;
+ ptr[1 - stride] = g;
+ ptr[2 - stride] = b;
+ ptr[3 - stride] = 0xff;
+
+ if ( height > 2 ) {
+ ptr[0 - stride * 3] = r;
+ ptr[1 - stride * 3] = g;
+ ptr[2 - stride * 3] = b;
+ ptr[3 - stride * 3] = 0xff;
+ }
+ }
+}
+
+// takes doc, drawing, icon, and icon name to produce pixels
+extern "C" guchar *
+sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing,
+ gchar const *name, unsigned psize,
+ unsigned &stride)
+{
+ bool const dump = Inkscape::Preferences::get()->getBool("/debug/icons/dumpSvg");
+ guchar *px = nullptr;
+
+ if (doc) {
+ SPObject *object = doc->getObjectById(name);
+ if (object && is<SPItem>(object)) {
+ auto item = cast<SPItem>(object);
+ // Find bbox in document
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if ( object->parent == nullptr )
+ {
+ dbox = *(doc->preferredBounds());
+ }
+
+ /* This is in document coordinates, i.e. pixels */
+ if ( dbox ) {
+ /* Update to renderable state */
+ double sf = 1.0;
+ drawing.root()->setTransform(Geom::Scale(sf));
+ drawing.update();
+ /* Item integer bbox in points */
+ // NOTE: previously, each rect coordinate was rounded using floor(c + 0.5)
+ Geom::IntRect ibox = dbox->roundOutwards();
+
+ if ( dump ) {
+ g_message( " box --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() );
+ }
+
+ /* Find button visible area */
+ int width = ibox.width();
+ int height = ibox.height();
+
+ if ( dump ) {
+ g_message( " vis --'%s' (%d,%d)", name, width, height );
+ }
+
+ {
+ int block = std::max(width, height);
+ if (block != static_cast<int>(psize) ) {
+ if ( dump ) {
+ g_message(" resizing" );
+ }
+ sf = (double)psize / (double)block;
+
+ drawing.root()->setTransform(Geom::Scale(sf));
+ drawing.update();
+
+ auto scaled_box = *dbox * Geom::Scale(sf);
+ ibox = scaled_box.roundOutwards();
+ if ( dump ) {
+ g_message( " box2 --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() );
+ }
+
+ /* Find button visible area */
+ width = ibox.width();
+ height = ibox.height();
+ if ( dump ) {
+ g_message( " vis2 --'%s' (%d,%d)", name, width, height );
+ }
+ }
+ }
+
+ Geom::IntPoint pdim(psize, psize);
+ int dx, dy;
+ //dx = (psize - width) / 2;
+ //dy = (psize - height) / 2;
+ dx=dy=psize;
+ dx=(dx-width)/2; // watch out for psize, since 'unsigned'-'signed' can cause problems if the result is negative
+ dy=(dy-height)/2;
+ Geom::IntRect area = Geom::IntRect::from_xywh(ibox.min() - Geom::IntPoint(dx,dy), pdim);
+ /* Actual renderable area */
+ Geom::IntRect ua = *Geom::intersect(ibox, area);
+
+ if ( dump ) {
+ g_message( " area --'%s' (%f,%f)-(%f,%f)", name, (double)area.left(), (double)area.top(), (double)area.right(), (double)area.bottom() );
+ g_message( " ua --'%s' (%f,%f)-(%f,%f)", name, (double)ua.left(), (double)ua.top(), (double)ua.right(), (double)ua.bottom() );
+ }
+
+ stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, psize);
+
+ /* Set up pixblock */
+ px = g_new(guchar, stride * psize);
+ memset(px, 0x00, stride * psize);
+
+ /* Render */
+ cairo_surface_t *s = cairo_image_surface_create_for_data(px,
+ CAIRO_FORMAT_ARGB32, psize, psize, stride);
+ Inkscape::DrawingContext dc(s, ua.min());
+
+ auto bg = doc->getPageManager().getDefaultBackgroundColor();
+
+ cairo_t *cr = cairo_create(s);
+ cairo_set_source_rgba(cr, bg[0], bg[1], bg[2], bg[3]);
+ cairo_rectangle(cr, 0, 0, psize, psize);
+ cairo_fill(cr);
+ cairo_save(cr);
+ cairo_destroy(cr);
+
+ drawing.render(dc, ua);
+ cairo_surface_destroy(s);
+
+ // convert to GdkPixbuf format
+ convert_pixels_argb32_to_pixbuf(px, psize, psize, stride);
+
+ if ( Inkscape::Preferences::get()->getBool("/debug/icons/overlaySvg") ) {
+ overlayPixels( px, psize, psize, stride, 0x00, 0x00, 0xff );
+ }
+ }
+ }
+ }
+
+ return px;
+} // end of sp_icon_doc_icon()
+
+
+void IconPreviewPanel::renderPreview( SPObject* obj )
+{
+ SPDocument * doc = obj->document;
+ gchar const * id = obj->getId();
+ if ( !renderTimer ) {
+ renderTimer = new Glib::Timer();
+ }
+ renderTimer->reset();
+
+#if ICON_VERBOSE
+ g_message("%s setting up to render '%s' as the icon", getTimestr().c_str(), id );
+#endif // ICON_VERBOSE
+
+ for ( int i = 0; i < numEntries; i++ ) {
+ unsigned unused;
+ int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]);
+ guchar *px = sp_icon_doc_icon(doc, *drawing, id, sizes[i], unused);
+// g_message( " size %d %s", sizes[i], (px ? "worked" : "failed") );
+ if ( px ) {
+ memcpy( pixMem[i], px, sizes[i] * stride );
+ g_free( px );
+ px = nullptr;
+ } else {
+ memset( pixMem[i], 0, sizes[i] * stride );
+ }
+ images[i]->set(images[i]->get_pixbuf());
+ // images[i]->queue_draw();
+ }
+ updateMagnify();
+
+ renderTimer->stop();
+ minDelay = std::max( 0.1, renderTimer->elapsed() * 3.0 );
+#if ICON_VERBOSE
+ g_message(" render took %f seconds.", renderTimer->elapsed());
+#endif // ICON_VERBOSE
+}
+
+void IconPreviewPanel::updateMagnify()
+{
+ Glib::RefPtr<Gdk::Pixbuf> buf = images[hot]->get_pixbuf()->scale_simple( 128, 128, Gdk::INTERP_NEAREST );
+ magLabel.set_label( *labels[hot] );
+ magnified.set( buf );
+ // magnified.queue_draw();
+ // magnified.get_parent()->queue_draw();
+}
+
+} //namespace Dialogs
+} //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/dialog/icon-preview.h b/src/ui/dialog/icon-preview.h
new file mode 100644
index 0000000..e65229b
--- /dev/null
+++ b/src/ui/dialog/icon-preview.h
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A simple dialog for previewing icon representation.
+ */
+/* Authors:
+ * Jon A. Cruz
+ * Bob Jamison
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004,2005 The Inkscape Organization
+ * Copyright (C) 2010 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_ICON_PREVIEW_H
+#define SEEN_ICON_PREVIEW_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/toggletoolbutton.h>
+
+#include "ui/dialog/dialog-base.h"
+
+class SPObject;
+namespace Glib {
+class Timer;
+}
+
+namespace Inkscape {
+class Drawing;
+namespace UI {
+namespace Dialog {
+
+
+/**
+ * A panel that displays an icon preview
+ */
+class IconPreviewPanel : public DialogBase
+{
+public:
+ IconPreviewPanel();
+ ~IconPreviewPanel() override;
+
+ void selectionModified(Selection *selection, guint flags) override;
+ void documentReplaced() override;
+
+ void refreshPreview();
+ void modeToggled();
+
+private:
+ Drawing *drawing;
+ SPDocument *drawing_doc;
+ unsigned int visionkey;
+ Glib::Timer *timer;
+ Glib::Timer *renderTimer;
+ bool pending;
+ gdouble minDelay;
+
+ Gtk::Box iconBox;
+ Gtk::Paned splitter;
+ Glib::ustring targetId;
+ int hot;
+ int numEntries;
+ int* sizes;
+
+ Gtk::Image magnified;
+ Gtk::Label magLabel;
+
+ Gtk::ToggleButton *selectionButton;
+
+ guchar** pixMem;
+ Gtk::Image** images;
+ Glib::ustring** labels;
+ Gtk::ToggleToolButton** buttons;
+ sigc::connection docModConn;
+ sigc::connection docDesConn;
+
+ void setDocument( SPDocument *document );
+ void removeDrawing();
+ void on_button_clicked(int which);
+ void renderPreview( SPObject* obj );
+ void updateMagnify();
+ void queueRefresh();
+ bool refreshCB();
+};
+
+} //namespace Dialogs
+} //namespace UI
+} //namespace Inkscape
+
+
+
+#endif // SEEN_ICON_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/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp
new file mode 100644
index 0000000..d18aaaf
--- /dev/null
+++ b/src/ui/dialog/inkscape-preferences.cpp
@@ -0,0 +1,3886 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Inkscape Preferences dialog - implementation.
+ */
+/* Authors:
+ * Carl Hetherington
+ * Marco Scholten
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * Copyright (C) 2004-2013 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/ustring.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/fontbutton.h>
+#include <gtkmm/fontchooserdialog.h>
+#include <gtkmm/widget.h>
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <gtkmm/box.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/object.h>
+#include <gtkmm/togglebutton.h>
+#include "ui/widget/color-scales.h"
+
+#include "inkscape-preferences.h"
+
+#include <fstream>
+#include <strings.h>
+#include <sstream>
+#include <iomanip>
+#include <algorithm>
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include <glibmm/i18n.h>
+#include <gtkmm.h>
+
+#include "auto-save.h"
+#include "cms-system.h"
+#include "document.h"
+#include "enums.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "path-prefix.h"
+#include "preferences.h"
+#include "selcue.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+#include "style.h"
+
+#include "display/control/canvas-item-grid.h"
+#include "display/nr-filter-gaussian.h"
+
+#include "extension/internal/gdkpixbuf-input.h"
+
+#include "io/resource.h"
+
+#include "object/color-profile.h"
+
+#include "svg/svg-color.h"
+
+#include "ui/desktop/menubar.h"
+#include "ui/interface.h"
+#include "ui/shortcuts.h"
+#include "ui/modifiers.h"
+#include "ui/util.h"
+#include "ui/widget/style-swatch.h"
+#include "ui/widget/canvas.h"
+#include "ui/themes.h"
+#include "ui/builder-utils.h"
+
+#include "util/trim.h"
+#include "util/recently-used-fonts.h"
+
+#include "widgets/desktop-widget.h"
+#include "widgets/toolbox.h"
+#include "widgets/spw-utilities.h"
+
+#include <gtkmm/accelgroup.h>
+
+#if WITH_GSPELL
+# include "ui/dialog/spellcheck.h" // for get_available_langs
+# ifdef _WIN32
+# include <windows.h>
+# endif
+#endif
+
+#if WITH_GSOURCEVIEW
+# include <gtksourceview/gtksource.h>
+#endif
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+using Inkscape::UI::Widget::DialogPage;
+using Inkscape::UI::Widget::PrefCheckButton;
+using Inkscape::UI::Widget::PrefRadioButton;
+using Inkscape::UI::Widget::PrefItem;
+using Inkscape::UI::Widget::PrefRadioButtons;
+using Inkscape::UI::Widget::PrefSpinButton;
+using Inkscape::UI::Widget::StyleSwatch;
+using Inkscape::CMSSystem;
+using Inkscape::IO::Resource::get_filename;
+using Inkscape::IO::Resource::UIS;
+
+std::function<Gtk::Image*()> reset_icon = []() {
+ auto image = Gtk::make_managed<Gtk::Image>();
+ image->set_from_icon_name("reset", Gtk::ICON_SIZE_BUTTON);
+ image->set_opacity(0.6);
+ image->set_tooltip_text(_("Requires restart to take effect"));
+ return image;
+};
+
+/**
+ * Case-insensitive and unicode normalized search of `pattern` in `string`.
+ *
+ * @param pattern Text to find
+ * @param string Text to search in
+ * @param[out] score Match score between 0.0 (not found) and 1.0 (pattern == string)
+ * @return True if `pattern` is a substring of `string`.
+ */
+static bool fuzzy_search(Glib::ustring const &pattern, Glib::ustring const &string, float &score)
+{
+ Glib::ustring norm_patt = pattern.lowercase().normalize();
+ Glib::ustring norm_str = string.lowercase().normalize();
+ bool found = (norm_str.find(norm_patt) != Glib::ustring::npos);
+ score = found ? (float)pattern.size() / (float)string.size() : 0;
+ return score > 0.0 ? true : false;
+}
+
+/**
+ * Case-insensitive and unicode normalized search of `pattern` in `string`.
+ *
+ * @param pattern Text to find
+ * @param string Text to search in
+ * @return True if `pattern` is a substring of `string`.
+ */
+static bool fuzzy_search(Glib::ustring const &pattern, Glib::ustring const &string)
+{
+ float score;
+ return fuzzy_search(pattern, string, score);
+}
+
+/**
+ * Get number of child Labels that match a key in a widget
+ *
+ * @param key Text to find
+ * @param widget Gtk::Widget to search in
+ * @return Number of matches found
+ */
+static int get_num_matches(Glib::ustring const &key, Gtk::Widget *widget)
+{
+ int matches = 0;
+ if (auto label = dynamic_cast<Gtk::Label *>(widget)) {
+ if (fuzzy_search(key, label->get_text().lowercase())) {
+ // set score
+ ++matches;
+ }
+ }
+ std::vector<Gtk::Widget *> children;
+ if (auto container = dynamic_cast<Gtk::Container *>(widget)) {
+ children = container->get_children();
+ } else {
+ children = widget->list_mnemonic_labels();
+ }
+ for (auto *child : children) {
+ if (int child_matches = get_num_matches(key, child)) {
+ matches += child_matches;
+ }
+ }
+ return matches;
+}
+
+// Shortcuts model =============
+
+class ModelColumns: public Gtk::TreeModel::ColumnRecord {
+public:
+ ModelColumns() {
+ add(name);
+ add(id);
+ add(shortcut);
+ add(description);
+ add(shortcutkey);
+ add(user_set);
+ }
+ ~ModelColumns() override = default;
+
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> shortcut;
+ Gtk::TreeModelColumn<Glib::ustring> description;
+ Gtk::TreeModelColumn<Gtk::AccelKey> shortcutkey;
+ Gtk::TreeModelColumn<unsigned int> user_set;
+};
+ModelColumns _kb_columns;
+static ModelColumns& onKBGetCols() {
+ return _kb_columns;
+}
+
+/**
+ * Add CSS-based highlight-class and pango highlight to a Gtk::Label
+ *
+ * @param label Label to add highlight to
+ * @param key Text to add pango highlight
+ */
+void InkscapePreferences::add_highlight(Gtk::Label *label, Glib::ustring const &key)
+{
+ Glib::ustring text = label->get_text();
+ Glib::ustring const n_text = text.lowercase().normalize();
+ Glib::ustring const n_key = key.lowercase().normalize();
+ label->get_style_context()->add_class("highlight");
+ auto const pos = n_text.find(n_key);
+ auto const len = n_key.size();
+ text = Glib::Markup::escape_text(text.substr(0, pos)) + "<span weight=\"bold\" underline=\"single\">" +
+ Glib::Markup::escape_text(text.substr(pos, len)) + "</span>" +
+ Glib::Markup::escape_text(text.substr(pos + len));
+ label->set_markup(text);
+}
+
+/**
+ * Remove CSS-based highlight-class and pango highlight from a Gtk::Label
+ *
+ * @param label Label to remove highlight from
+ * @param key Text to remove pango highlight from
+ */
+void InkscapePreferences::remove_highlight(Gtk::Label *label)
+{
+ if (label->get_use_markup()) {
+ Glib::ustring text = label->get_text();
+ label->set_text(text);
+ label->get_style_context()->remove_class("highlight");
+ }
+}
+
+InkscapePreferences::InkscapePreferences()
+ : DialogBase("/dialogs/preferences", "Preferences"),
+ _minimum_width(0),
+ _minimum_height(0),
+ _natural_width(900),
+ _natural_height(700),
+ _current_page(nullptr),
+ _init(true)
+{
+ //get the width of a spinbutton
+ Inkscape::UI::Widget::SpinButton* sb = new Inkscape::UI::Widget::SpinButton;
+ sb->set_width_chars(6);
+ add(*sb);
+ show_all_children();
+ Gtk::Requisition sreq;
+ Gtk::Requisition sreq_natural;
+ sb->get_preferred_size(sreq_natural, sreq);
+ _sb_width = sreq.width;
+ remove(*sb);
+ delete sb;
+
+ //Main HBox
+ auto hbox_list_page = Gtk::manage(new Gtk::Box());
+ hbox_list_page->set_border_width(12);
+ hbox_list_page->set_spacing(12);
+ add(*hbox_list_page);
+
+ //Pagelist
+ auto list_box = Gtk::manage(new Gtk::Box(Gtk::Orientation::ORIENTATION_VERTICAL, 3));
+ Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow());
+ _search.set_valign(Gtk::Align::ALIGN_START);
+ list_box->pack_start(_search, false, true, 0);
+ list_box->pack_start(*scrolled_window, false, true, 0);
+ hbox_list_page->pack_start(*list_box, false, true, 0);
+ _page_list.set_headers_visible(false);
+ scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ scrolled_window->set_valign(Gtk::Align::ALIGN_FILL);
+ scrolled_window->set_propagate_natural_width();
+ scrolled_window->set_propagate_natural_height();
+ scrolled_window->add(_page_list);
+ scrolled_window->set_vexpand_set(true);
+ scrolled_window->set_vexpand(true);
+ scrolled_window->set_shadow_type(Gtk::SHADOW_IN);
+ _page_list_model = Gtk::TreeStore::create(_page_list_columns);
+ _page_list_model_filter = Gtk::TreeModelFilter::create(_page_list_model);
+ _page_list_model_sort = Gtk::TreeModelSort::create(_page_list_model_filter);
+ _page_list_model_sort->set_sort_column(_page_list_columns._col_name, Gtk::SortType::SORT_ASCENDING);
+ _page_list.set_enable_search(false);
+ _page_list.set_model(_page_list_model_sort);
+ _page_list.append_column("name",_page_list_columns._col_name);
+ Glib::RefPtr<Gtk::TreeSelection> page_list_selection = _page_list.get_selection();
+ page_list_selection->signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_pagelist_selection_changed));
+ page_list_selection->set_mode(Gtk::SELECTION_BROWSE);
+
+ // Search
+ _page_list.set_search_column(-1); // this disables pop-up search!
+ _search.signal_search_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_search_changed));
+ _search.set_tooltip_text("Search");
+ _page_list_model_sort->set_sort_func(
+ _page_list_columns._col_name, [=](Gtk::TreeModel::iterator const &a, Gtk::TreeModel::iterator const &b) -> int {
+ float score_a, score_b;
+ Glib::ustring key = _search.get_text().lowercase();
+ if (key == "") {
+ return -1;
+ }
+ Glib::ustring label_a = a->get_value(_page_list_columns._col_name).lowercase();
+ Glib::ustring label_b = b->get_value(_page_list_columns._col_name).lowercase();
+ auto *grid_a = a->get_value(_page_list_columns._col_page);
+ auto *grid_b = b->get_value(_page_list_columns._col_page);
+ int num_res_a = num_widgets_in_grid(key, grid_a);
+ int num_res_b = num_widgets_in_grid(key, grid_b);
+ fuzzy_search(key, label_a, score_a);
+ fuzzy_search(key, label_b, score_b);
+ if (score_a > score_b) {
+ return -1;
+ } else if (score_a < score_b) {
+ return 1;
+ } else if (num_res_a >= num_res_b) {
+ return -1;
+ } else if (num_res_a < num_res_b) {
+ return 1;
+ } else {
+ return a->get_value(_page_list_columns._col_id) > b->get_value(_page_list_columns._col_id) ? -1 : 1;
+ }
+ });
+
+ _search.signal_next_match().connect([=]() {
+ if (_search_results.size() > 0) {
+ Gtk::TreeIter curr = _page_list.get_selection()->get_selected();
+ auto _page_list_selection = _page_list.get_selection();
+ auto next = get_next_result(curr);
+ if (next) {
+ _page_list.get_model()->get_iter(next);
+ _page_list.scroll_to_cell(next, *_page_list.get_column(0));
+ _page_list.set_cursor(next);
+ }
+ }
+ });
+
+ _search.signal_previous_match().connect([=]() {
+ if (_search_results.size() > 0) {
+ Gtk::TreeIter curr = _page_list.get_selection()->get_selected();
+ auto _page_list_selection = _page_list.get_selection();
+ auto prev = get_prev_result(curr);
+ if (prev) {
+ _page_list.get_model()->get_iter(prev);
+ _page_list.scroll_to_cell(prev, *_page_list.get_column(0));
+ _page_list.set_cursor(prev);
+ }
+ }
+ });
+
+ _search.signal_key_press_event().connect(sigc::mem_fun(*this, &InkscapePreferences::on_navigate_key_press));
+
+ _page_list_model_filter->set_visible_func([=](Gtk::TreeModel::const_iterator const &row) -> bool {
+ auto key_lower = _search.get_text().lowercase();
+ return recursive_filter(key_lower, row);
+ });
+
+ //Pages
+ auto vbox_page = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ Gtk::Frame* title_frame = Gtk::manage(new Gtk::Frame());
+
+ Gtk::ScrolledWindow* pageScroller = Gtk::manage(new Gtk::ScrolledWindow());
+ pageScroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ pageScroller->set_propagate_natural_width();
+ pageScroller->set_propagate_natural_height();
+ pageScroller->add(*vbox_page);
+ hbox_list_page->pack_start(*pageScroller, true, true, 0);
+
+ title_frame->add(_page_title);
+ vbox_page->pack_start(*title_frame, false, false, 0);
+ vbox_page->pack_start(_page_frame, true, true, 0);
+ _page_frame.set_shadow_type(Gtk::SHADOW_NONE);
+ title_frame->set_shadow_type(Gtk::SHADOW_NONE);
+
+ initPageTools();
+ initPageUI();
+ initPageBehavior();
+ initPageIO();
+
+ initPageSystem();
+ initPageBitmaps();
+ initPageRendering();
+ initPageSpellcheck();
+
+ signal_map().connect(sigc::mem_fun(*this, &InkscapePreferences::showPage));
+
+ //calculate the size request for this dialog
+ _page_list.expand_all();
+ _page_list_model->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::GetSizeRequest));
+ _page_list.collapse_all();
+
+ // Set Custom theme
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _theme_oberver = prefs->createObserver("/theme/", [=]() {
+ prefs->setString("/options/boot/theme", "custom");
+ });
+}
+
+InkscapePreferences::~InkscapePreferences()
+= default;
+
+/**
+ * Get child Labels that match a key in a widget grid
+ * and add the results to global _search_results vector
+ *
+ * @param key Text to find
+ * @param widget Gtk::Widget to search in
+ */
+void InkscapePreferences::get_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget)
+{
+ if (auto label = dynamic_cast<Gtk::Label *>(widget)) {
+ if (fuzzy_search(key, label->get_text())) {
+ _search_results.push_back(widget);
+ }
+ }
+ std::vector<Gtk::Widget *> children;
+ if (auto container = dynamic_cast<Gtk::Container *>(widget)) {
+ children = container->get_children();
+ } else {
+ children = widget->list_mnemonic_labels();
+ }
+ for (auto *child : children) {
+ get_widgets_in_grid(key, child);
+ }
+}
+
+/**
+ * Get number of child Labels that match a key in a widget grid
+ * and add the results to global _search_results vector
+ *
+ * @param key Text to find
+ * @param widget Gtk::Widget to search in
+ * @return Number of results found
+ */
+int InkscapePreferences::num_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget)
+{
+ int results = 0;
+ if (auto label = dynamic_cast<Gtk::Label *>(widget)) {
+ float score;
+ if (fuzzy_search(key, label->get_text(), score)) {
+ ++results;
+ }
+ }
+ std::vector<Gtk::Widget *> children;
+ if (auto container = dynamic_cast<Gtk::Container *>(widget)) {
+ children = container->get_children();
+ } else {
+ children = widget->list_mnemonic_labels();
+ }
+ for (auto *child : children) {
+ int num_child = num_widgets_in_grid(key, child);
+ results += num_child;
+ }
+ return results;
+}
+
+/**
+ * Implementation of the search functionality executes each time
+ * search entry is changed
+ */
+void InkscapePreferences::on_search_changed()
+{
+ _num_results = 0;
+ if (_search_results.size() > 0) {
+ for (auto *result : _search_results) {
+ remove_highlight(static_cast<Gtk::Label *>(result));
+ }
+ _search_results.clear();
+ }
+ auto key = _search.get_text();
+ _page_list_model_filter->refilter();
+ // get first iter
+ Gtk::TreeModel::Children children = _page_list.get_model()->children();
+ Gtk::TreeModel::iterator iter = children.begin();
+
+ highlight_results(key, iter);
+ goto_first_result();
+ if (key == "") {
+ Gtk::TreeModel::Children children = _page_list.get_model()->children();
+ Gtk::TreeModel::iterator iter = children.begin();
+ _page_list.scroll_to_cell(Gtk::TreePath(iter), *_page_list.get_column(0));
+ _page_list.set_cursor(Gtk::TreePath(iter));
+ } else if (_num_results == 0 && key != "") {
+ _page_list.set_has_tooltip(false);
+ _show_all = true;
+ _page_list_model_filter->refilter();
+ _show_all = false;
+ show_not_found();
+ } else {
+ _page_list.expand_all();
+ }
+}
+
+/**
+ * Select the first row in the tree that has a result
+ */
+void InkscapePreferences::goto_first_result()
+{
+ auto key = _search.get_text();
+ if (_num_results > 0) {
+ Gtk::TreeIter curr = _page_list.get_model()->children().begin();
+ if (fuzzy_search(key, curr->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, curr->get_value(_page_list_columns._col_page)) > 0) {
+ _page_list.scroll_to_cell(Gtk::TreePath(curr), *_page_list.get_column(0));
+ _page_list.set_cursor(Gtk::TreePath(curr));
+ } else {
+ auto next = get_next_result(curr);
+ if (next) {
+ _page_list.scroll_to_cell(next, *_page_list.get_column(0));
+ _page_list.set_cursor(next);
+ }
+ }
+ }
+}
+
+/**
+ * Look for the immediate next row in the tree that contains a search result
+ *
+ * @param iter Current iter the that is selected in the tree
+ * @param check_children Bool whether to check if the children of the iter
+ * contain search result
+ * @return Immediate next row than contains a search result
+ */
+Gtk::TreePath InkscapePreferences::get_next_result(Gtk::TreeIter &iter, bool check_children)
+{
+ auto key = _search.get_text();
+ Gtk::TreePath path = Gtk::TreePath(iter);
+ if (iter->children().begin() && check_children) { // check for search results in children
+ auto child = iter->children().begin();
+ _page_list.expand_row(path, false);
+ if (fuzzy_search(key, child->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, child->get_value(_page_list_columns._col_page)) > 0) {
+ return Gtk::TreePath(child);
+ } else {
+ return get_next_result(child);
+ }
+ } else {
+ ++iter; // go to next row
+ if (iter) { // if row exists
+ if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, iter->get_value(_page_list_columns._col_page))) {
+ path.next();
+ return path;
+ } else {
+ return get_next_result(iter);
+ }
+ } else if (path.up() && path) {
+ path.next();
+ iter = _page_list.get_model()->get_iter(path);
+ if (iter) {
+ if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, iter->get_value(_page_list_columns._col_page))) {
+ return Gtk::TreePath(iter);
+ } else {
+ return get_next_result(iter);
+ }
+ } else {
+ path.up();
+ if (path) {
+ iter = _page_list.get_model()->get_iter(path);
+ return get_next_result(iter, false); // dont check for children
+ } else {
+ return Gtk::TreePath(_page_list.get_model()->children().begin());
+ }
+ }
+ }
+ }
+ assert(!iter);
+ return Gtk::TreePath();
+}
+
+/**
+ * Look for the immediate previous row in the tree that contains a search result
+ *
+ * @param iter Current iter that is selected in the tree
+ * @param check_children Bool whether to check if the children of the iter
+ * contain search result
+ * @return Immediate previous row than contains a search result
+ */
+Gtk::TreePath InkscapePreferences::get_prev_result(Gtk::TreeIter &iter, bool iterate)
+{
+ auto key = _search.get_text();
+ Gtk::TreePath path = Gtk::TreePath(iter);
+ if (iterate) {
+ --iter;
+ }
+ if (iter) {
+ if (iter->children().begin()) {
+ auto child = iter->children().end();
+ --child;
+ Gtk::TreePath path = Gtk::TreePath(iter);
+ _page_list.expand_row(path, false);
+ return get_prev_result(child, false);
+ } else if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, iter->get_value(_page_list_columns._col_page))) {
+ return (Gtk::TreePath(iter));
+ } else {
+ return get_prev_result(iter);
+ }
+ } else if (path.up()) {
+ if (path) {
+ iter = _page_list.get_model()->get_iter(path);
+ if (fuzzy_search(key, iter->get_value(_page_list_columns._col_name)) ||
+ get_num_matches(key, iter->get_value(_page_list_columns._col_page))) {
+ return path;
+ } else {
+ return get_prev_result(iter);
+ }
+ } else {
+ auto lastIter = _page_list.get_model()->children().end();
+ --lastIter;
+ return get_prev_result(lastIter, false);
+ }
+ } else {
+ return Gtk::TreePath(iter);
+ }
+}
+
+/**
+ * Handle key F3 and Shift+F3 key press events to navigate to the next search
+ * result
+ *
+ * @param evt event object
+ * @return Always returns False to label the key press event as handled, this
+ * prevents the search bar from retaining focus for other keyboard event.
+ */
+bool InkscapePreferences::on_navigate_key_press(GdkEventKey *evt)
+{
+ if (evt->keyval == GDK_KEY_F3) {
+ if (_search_results.size() > 0) {
+ GdkModifierType modmask = gtk_accelerator_get_default_mod_mask();
+ if ((evt->state & modmask) == Gdk::SHIFT_MASK) {
+ Gtk::TreeIter curr = _page_list.get_selection()->get_selected();
+ auto _page_list_selection = _page_list.get_selection();
+ auto prev = get_prev_result(curr);
+ if (prev) {
+ _page_list.scroll_to_cell(prev, *_page_list.get_column(0));
+ _page_list.set_cursor(prev);
+ }
+ } else {
+ Gtk::TreeIter curr = _page_list.get_selection()->get_selected();
+ auto _page_list_selection = _page_list.get_selection();
+ auto next = get_next_result(curr);
+ if (next) {
+ _page_list.scroll_to_cell(next, *_page_list.get_column(0));
+ _page_list.set_cursor(next);
+ }
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Add highlight to all the search results
+ *
+ * @param key Text to search
+ * @param iter pointing to the first row of the tree
+ */
+void InkscapePreferences::highlight_results(Glib::ustring const &key, Gtk::TreeModel::iterator &iter)
+{
+ Gtk::TreeModel::Path path;
+ Glib::ustring Txt;
+ while (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ auto *grid = row->get_value(_page_list_columns._col_page);
+ get_widgets_in_grid(key, grid);
+ if (_search_results.size() > 0) {
+ for (auto *result : _search_results) {
+ // underline and highlight
+ add_highlight(static_cast<Gtk::Label *>(result), key);
+ }
+ }
+ if (iter->children()) {
+ auto children = iter->children();
+ auto child_iter = children.begin();
+ highlight_results(key, child_iter);
+ }
+ iter++;
+ }
+}
+
+/**
+ * Filter function for the search functionality to show only matching
+ * rows or rows with matching search resuls in the tree view
+ *
+ * @param key Text to search for
+ * @param iter iter pointing to the first row of the tree view
+ * @return True if the row is to be shown else return False
+ */
+bool InkscapePreferences::recursive_filter(Glib::ustring &key, Gtk::TreeModel::const_iterator const &iter)
+{
+ if(_show_all)
+ return true;
+ auto row_label = iter->get_value(_page_list_columns._col_name).lowercase();
+ if (key == "") {
+ return true;
+ }
+ if (fuzzy_search(key, row_label)) {
+ ++_num_results;
+ return true;
+ }
+ auto *grid = iter->get_value(_page_list_columns._col_page);
+ int matches = get_num_matches(key, grid);
+ _num_results += matches;
+ if (matches) {
+ return true;
+ }
+ auto child = iter->children().begin();
+ if (child) {
+ for (auto inner = child; inner; ++inner) {
+ if (recursive_filter(key, inner)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+Gtk::TreeModel::iterator InkscapePreferences::AddPage(DialogPage& p, Glib::ustring title, int id)
+{
+ return AddPage(p, title, Gtk::TreeModel::iterator() , id);
+}
+
+Gtk::TreeModel::iterator InkscapePreferences::AddPage(DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id)
+{
+ Gtk::TreeModel::iterator iter;
+ if (parent)
+ iter = _page_list_model->append((*parent).children());
+ else
+ iter = _page_list_model->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[_page_list_columns._col_name] = title;
+ row[_page_list_columns._col_id] = id;
+ row[_page_list_columns._col_page] = &p;
+ return iter;
+}
+
+void InkscapePreferences::AddSelcueCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value)
+{
+ PrefCheckButton* cb = Gtk::manage( new PrefCheckButton);
+ cb->init ( _("Show selection cue"), prefs_path + "/selcue", def_value);
+ p.add_line( false, "", *cb, "", _("Whether selected objects display a selection cue (the same as in selector)"));
+}
+
+void InkscapePreferences::AddGradientCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value)
+{
+ PrefCheckButton* cb = Gtk::manage( new PrefCheckButton);
+ cb->init ( _("Enable gradient editing"), prefs_path + "/gradientdrag", def_value);
+ p.add_line( false, "", *cb, "", _("Whether selected objects display gradient editing controls"));
+}
+
+void InkscapePreferences::AddConvertGuidesCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) {
+ PrefCheckButton* cb = Gtk::manage( new PrefCheckButton);
+ cb->init ( _("Conversion to guides uses edges instead of bounding box"), prefs_path + "/convertguides", def_value);
+ p.add_line( false, "", *cb, "", _("Converting an object to guides places these along the object's true edges (imitating the object's shape), not along the bounding box"));
+}
+
+void InkscapePreferences::AddDotSizeSpinbutton(DialogPage &p, Glib::ustring const &prefs_path, double def_value)
+{
+ PrefSpinButton* sb = Gtk::manage( new PrefSpinButton);
+ sb->init ( prefs_path + "/dot-size", 0.0, 1000.0, 0.1, 10.0, def_value, false, false);
+ p.add_line( false, _("Ctrl+click _dot size:"), *sb, _("times current stroke width"),
+ _("Size of dots created with Ctrl+click (relative to current stroke width)"),
+ false );
+}
+
+void InkscapePreferences::AddBaseSimplifySpinbutton(DialogPage &p, Glib::ustring const &prefs_path, double def_value)
+{
+ PrefSpinButton* sb = Gtk::manage( new PrefSpinButton);
+ sb->init ( prefs_path + "/base-simplify", 0.0, 100.0, 1.0, 10.0, def_value, false, false);
+ p.add_line( false, _("Base simplify:"), *sb, _("on dynamic LPE simplify"),
+ _("Base simplify of dynamic LPE based simplify"),
+ false );
+}
+
+
+static void StyleFromSelectionToTool(Glib::ustring const &prefs_path, StyleSwatch *swatch)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop == nullptr)
+ return;
+
+ Inkscape::Selection *selection = desktop->getSelection();
+
+ if (selection->isEmpty()) {
+ desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE,
+ _("<b>No objects selected</b> to take the style from."));
+ return;
+ }
+ SPItem *item = selection->singleItem();
+ if (!item) {
+ /* TODO: If each item in the selection has the same style then don't consider it an error.
+ * Maybe we should try to handle multiple selections anyway, e.g. the intersection of the
+ * style attributes for the selected items. */
+ desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE,
+ _("<b>More than one object selected.</b> Cannot take style from multiple objects."));
+ return;
+ }
+
+ SPCSSAttr *css = take_style_from_item (item);
+
+ if (!css) return;
+
+ // remove black-listed properties
+ css = sp_css_attr_unset_blacklist (css);
+
+ // only store text style for the text tool
+ if (prefs_path != "/tools/text") {
+ css = sp_css_attr_unset_text (css);
+ }
+
+ // we cannot store properties with uris - they will be invalid in other documents
+ css = sp_css_attr_unset_uris (css);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setStyle(prefs_path + "/style", css);
+ sp_repr_css_attr_unref (css);
+
+ // update the swatch
+ if (swatch) {
+ SPCSSAttr *css = prefs->getInheritedStyle(prefs_path + "/style");
+ swatch->setStyle (css);
+ sp_repr_css_attr_unref(css);
+ }
+}
+
+void InkscapePreferences::AddNewObjectsStyle(DialogPage &p, Glib::ustring const &prefs_path, const gchar *banner)
+{
+ if (banner)
+ p.add_group_header(banner);
+ else
+ p.add_group_header( _("Style of new objects"));
+ PrefRadioButton* current = Gtk::manage( new PrefRadioButton);
+ current->init ( _("Last used style"), prefs_path + "/usecurrent", 1, true, nullptr);
+ p.add_line( true, "", *current, "",
+ _("Apply the style you last set on an object"));
+
+ PrefRadioButton* own = Gtk::manage( new PrefRadioButton);
+ auto hb = Gtk::manage( new Gtk::Box);
+ own->init ( _("This tool's own style:"), prefs_path + "/usecurrent", 0, false, current);
+ own->set_halign(Gtk::ALIGN_START);
+ own->set_valign(Gtk::ALIGN_START);
+ hb->add(*own);
+ p.set_tip( *own, _("Each tool may store its own style to apply to the newly created objects. Use the button below to set it."));
+ p.add_line( true, "", *hb, "", "");
+
+ // style swatch
+ Gtk::Button* button = Gtk::manage( new Gtk::Button(_("Take from selection"), true));
+ StyleSwatch *swatch = nullptr;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (prefs->getInt(prefs_path + "/usecurrent")) {
+ button->set_sensitive(false);
+ }
+
+ SPCSSAttr *css = prefs->getStyle(prefs_path + "/style");
+ swatch = new StyleSwatch(css, _("This tool's style of new objects"));
+ hb->add(*swatch);
+ sp_repr_css_attr_unref(css);
+
+ button->signal_clicked().connect( sigc::bind( sigc::ptr_fun(StyleFromSelectionToTool), prefs_path, swatch) );
+ own->changed_signal.connect( sigc::mem_fun(*button, &Gtk::Button::set_sensitive) );
+ p.add_line( true, "", *button, "",
+ _("Remember the style of the (first) selected object as this tool's style"));
+}
+#define get_tool_action(toolname) ("win.tool-switch('" + toolname + "')")
+Glib::ustring get_tool_action_name(Glib::ustring toolname)
+{
+ auto *iapp = InkscapeApplication::instance();
+ if (iapp)
+ return iapp->get_action_extra_data().get_label_for_action(get_tool_action(toolname));
+ return "";
+}
+void InkscapePreferences::initPageTools()
+{
+ Gtk::TreeModel::iterator iter_tools = this->AddPage(_page_tools, _("Tools"), PREFS_PAGE_TOOLS);
+ this->AddPage(_page_selector, get_tool_action_name("Select"), iter_tools, PREFS_PAGE_TOOLS_SELECTOR);
+ this->AddPage(_page_node, get_tool_action_name("Node"), iter_tools, PREFS_PAGE_TOOLS_NODE);
+
+ // shapes
+ Gtk::TreeModel::iterator iter_shapes = this->AddPage(_page_shapes, _("Shapes"), iter_tools, PREFS_PAGE_TOOLS_SHAPES);
+ this->AddPage(_page_rectangle, get_tool_action_name("Rect"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_RECT);
+ this->AddPage(_page_ellipse, get_tool_action_name("Arc"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_ELLIPSE);
+ this->AddPage(_page_star, get_tool_action_name("Star"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_STAR);
+ this->AddPage(_page_3dbox, get_tool_action_name("3DBox"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_3DBOX);
+ this->AddPage(_page_spiral, get_tool_action_name("Spiral"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_SPIRAL);
+
+ this->AddPage(_page_pen, get_tool_action_name("Pen"), iter_tools, PREFS_PAGE_TOOLS_PEN);
+ this->AddPage(_page_pencil, get_tool_action_name("Pencil"), iter_tools, PREFS_PAGE_TOOLS_PENCIL);
+ this->AddPage(_page_calligraphy, get_tool_action_name("Calligraphic"), iter_tools, PREFS_PAGE_TOOLS_CALLIGRAPHY);
+ this->AddPage(_page_text, get_tool_action_name("Text"), iter_tools, PREFS_PAGE_TOOLS_TEXT);
+
+ this->AddPage(_page_gradient, get_tool_action_name("Gradient"), iter_tools, PREFS_PAGE_TOOLS_GRADIENT);
+ this->AddPage(_page_dropper, get_tool_action_name("Dropper"), iter_tools, PREFS_PAGE_TOOLS_DROPPER);
+ this->AddPage(_page_paintbucket, get_tool_action_name("PaintBucket"), iter_tools, PREFS_PAGE_TOOLS_PAINTBUCKET);
+
+ this->AddPage(_page_tweak, get_tool_action_name("Tweak"), iter_tools, PREFS_PAGE_TOOLS_TWEAK);
+ this->AddPage(_page_spray, get_tool_action_name("Spray"), iter_tools, PREFS_PAGE_TOOLS_SPRAY);
+ this->AddPage(_page_eraser, get_tool_action_name("Eraser"), iter_tools, PREFS_PAGE_TOOLS_ERASER);
+ this->AddPage(_page_connector, get_tool_action_name("Connector"), iter_tools, PREFS_PAGE_TOOLS_CONNECTOR);
+#ifdef WITH_LPETOOL
+ this->AddPage(_page_lpetool, get_tool_action_name("LPETool"), iter_tools, PREFS_PAGE_TOOLS_LPETOOL);
+#endif // WITH_LPETOOL
+ this->AddPage(_page_measure, get_tool_action_name("Measure"), iter_tools, PREFS_PAGE_TOOLS_MEASURE);
+ this->AddPage(_page_zoom, get_tool_action_name("Zoom"), iter_tools, PREFS_PAGE_TOOLS_ZOOM);
+ _page_tools.add_group_header( _("Bounding box to use"));
+ _t_bbox_visual.init ( _("Visual bounding box"), "/tools/bounding_box", 0, false, nullptr); // 0 means visual
+ _page_tools.add_line( true, "", _t_bbox_visual, "",
+ _("This bounding box includes stroke width, markers, filter margins, etc."));
+ _t_bbox_geometric.init ( _("Geometric bounding box"), "/tools/bounding_box", 1, true, &_t_bbox_visual); // 1 means geometric
+ _page_tools.add_line( true, "", _t_bbox_geometric, "",
+ _("This bounding box includes only the bare path"));
+
+ _page_tools.add_group_header( _("Conversion to guides"));
+ _t_cvg_keep_objects.init ( _("Keep objects after conversion to guides"), "/tools/cvg_keep_objects", false);
+ _page_tools.add_line( true, "", _t_cvg_keep_objects, "",
+ _("When converting an object to guides, don't delete the object after the conversion"));
+ _t_cvg_convert_whole_groups.init ( _("Treat groups as a single object"), "/tools/cvg_convert_whole_groups", false);
+ _page_tools.add_line( true, "", _t_cvg_convert_whole_groups, "",
+ _("Treat groups as a single object during conversion to guides rather than converting each child separately"));
+
+ _pencil_average_all_sketches.init ( _("Average all sketches"), "/tools/freehand/pencil/average_all_sketches", false);
+ _calligrapy_keep_selected.init ( _("Select new path"), "/tools/calligraphic/keep_selected", true);
+ _connector_ignore_text.init( _("Don't attach connectors to text objects"), "/tools/connector/ignoretext", true);
+
+ //Selector
+
+
+ AddSelcueCheckbox(_page_selector, "/tools/select", false);
+ AddGradientCheckbox(_page_selector, "/tools/select", false);
+
+ _page_selector.add_group_header( _("When transforming, show"));
+ _t_sel_trans_obj.init ( _("Objects"), "/tools/select/show", "content", true, nullptr);
+ _page_selector.add_line( true, "", _t_sel_trans_obj, "",
+ _("Show the actual objects when moving or transforming"));
+ _t_sel_trans_outl.init ( _("Box outline"), "/tools/select/show", "outline", false, &_t_sel_trans_obj);
+ _page_selector.add_line( true, "", _t_sel_trans_outl, "",
+ _("Show only a box outline of the objects when moving or transforming"));
+ _page_selector.add_group_header( _("Per-object selection cue"));
+ _t_sel_cue_none.init ( C_("Selection cue", "None"), "/options/selcue/value", Inkscape::SelCue::NONE, false, nullptr);
+ _page_selector.add_line( true, "", _t_sel_cue_none, "",
+ _("No per-object selection indication"));
+ _t_sel_cue_mark.init ( _("Mark"), "/options/selcue/value", Inkscape::SelCue::MARK, true, &_t_sel_cue_none);
+ _page_selector.add_line( true, "", _t_sel_cue_mark, "",
+ _("Each selected object has a diamond mark in the top left corner"));
+ _t_sel_cue_box.init ( _("Box"), "/options/selcue/value", Inkscape::SelCue::BBOX, false, &_t_sel_cue_none);
+ _page_selector.add_line( true, "", _t_sel_cue_box, "",
+ _("Each selected object displays its bounding box"));
+
+ //Node
+ AddSelcueCheckbox(_page_node, "/tools/nodes", true);
+ AddGradientCheckbox(_page_node, "/tools/nodes", true);
+ _page_node.add_group_header( _("Path outline"));
+ _t_node_pathoutline_color.init(_("Path outline color"), "/tools/nodes/highlight_color", 0xff0000ff);
+ _page_node.add_line( false, "", _t_node_pathoutline_color, "", _("Selects the color used for showing the path outline"), false);
+ _t_node_show_outline.init(_("Always show outline"), "/tools/nodes/show_outline", false);
+ _page_node.add_line( true, "", _t_node_show_outline, "", _("Show outlines for all paths, not only invisible paths"));
+ _t_node_live_outline.init(_("Update outline when dragging nodes"), "/tools/nodes/live_outline", false);
+ _page_node.add_line( true, "", _t_node_live_outline, "", _("Update the outline when dragging or transforming nodes; if this is off, the outline will only update when completing a drag"));
+ _t_node_live_objects.init(_("Update paths when dragging nodes"), "/tools/nodes/live_objects", false);
+ _page_node.add_line( true, "", _t_node_live_objects, "", _("Update paths when dragging or transforming nodes; if this is off, paths will only be updated when completing a drag"));
+ _t_node_show_path_direction.init(_("Show path direction on outlines"), "/tools/nodes/show_path_direction", false);
+ _page_node.add_line( true, "", _t_node_show_path_direction, "", _("Visualize the direction of selected paths by drawing small arrows in the middle of each outline segment"));
+ _t_node_pathflash_enabled.init ( _("Show temporary path outline"), "/tools/nodes/pathflash_enabled", false);
+ _page_node.add_line( true, "", _t_node_pathflash_enabled, "", _("When hovering over a path, briefly flash its outline"));
+ _t_node_pathflash_selected.init ( _("Show temporary outline for selected paths"), "/tools/nodes/pathflash_selected", false);
+ _page_node.add_line( true, "", _t_node_pathflash_selected, "", _("Show temporary outline even when a path is selected for editing"));
+ _t_node_pathflash_timeout.init("/tools/nodes/pathflash_timeout", 0, 10000.0, 100.0, 100.0, 1000.0, true, false);
+ _page_node.add_line( false, _("_Flash time:"), _t_node_pathflash_timeout, "ms", _("Specifies how long the path outline will be visible after a mouse-over (in milliseconds); specify 0 to have the outline shown until mouse leaves the path"), false);
+ _page_node.add_group_header(_("Editing preferences"));
+ _t_node_single_node_transform_handles.init(_("Show transform handles for single nodes"), "/tools/nodes/single_node_transform_handles", false);
+ _page_node.add_line( true, "", _t_node_single_node_transform_handles, "", _("Show transform handles even when only a single node is selected"));
+ _t_node_delete_preserves_shape.init(_("Deleting nodes preserves shape"), "/tools/nodes/delete_preserves_shape", true);
+ _page_node.add_line( true, "", _t_node_delete_preserves_shape, "", _("Move handles next to deleted nodes to resemble original shape; hold Ctrl to get the other behavior"));
+
+ //Tweak
+ this->AddNewObjectsStyle(_page_tweak, "/tools/tweak", _("Object paint style"));
+ AddSelcueCheckbox(_page_tweak, "/tools/tweak", true);
+ AddGradientCheckbox(_page_tweak, "/tools/tweak", false);
+
+ //Zoom
+ AddSelcueCheckbox(_page_zoom, "/tools/zoom", true);
+ AddGradientCheckbox(_page_zoom, "/tools/zoom", false);
+
+ //Measure
+ PrefCheckButton* cb = Gtk::manage( new PrefCheckButton);
+ cb->init ( _("Ignore first and last points"), "/tools/measure/ignore_1st_and_last", true);
+ _page_measure.add_line( false, "", *cb, "", _("The start and end of the measurement tool's control line will not be considered for calculating lengths. Only lengths between actual curve intersections will be displayed."));
+
+ //Shapes
+ this->AddSelcueCheckbox(_page_shapes, "/tools/shapes", true);
+ this->AddGradientCheckbox(_page_shapes, "/tools/shapes", true);
+
+ //Rectangle
+ this->AddNewObjectsStyle(_page_rectangle, "/tools/shapes/rect");
+ this->AddConvertGuidesCheckbox(_page_rectangle, "/tools/shapes/rect", true);
+
+ //3D box
+ this->AddNewObjectsStyle(_page_3dbox, "/tools/shapes/3dbox");
+ this->AddConvertGuidesCheckbox(_page_3dbox, "/tools/shapes/3dbox", true);
+
+ //Ellipse
+ this->AddNewObjectsStyle(_page_ellipse, "/tools/shapes/arc");
+
+ //Star
+ this->AddNewObjectsStyle(_page_star, "/tools/shapes/star");
+
+ //Spiral
+ this->AddNewObjectsStyle(_page_spiral, "/tools/shapes/spiral");
+
+ //Pencil
+ this->AddSelcueCheckbox(_page_pencil, "/tools/freehand/pencil", true);
+ this->AddNewObjectsStyle(_page_pencil, "/tools/freehand/pencil");
+ this->AddDotSizeSpinbutton(_page_pencil, "/tools/freehand/pencil", 3.0);
+ this->AddBaseSimplifySpinbutton(_page_pencil, "/tools/freehand/pencil", 25.0);
+ _page_pencil.add_group_header( _("Sketch mode"));
+ _page_pencil.add_line( true, "", _pencil_average_all_sketches, "",
+ _("If on, the sketch result will be the normal average of all sketches made, instead of averaging the old result with the new sketch"));
+
+ //Pen
+ this->AddSelcueCheckbox(_page_pen, "/tools/freehand/pen", true);
+ this->AddNewObjectsStyle(_page_pen, "/tools/freehand/pen");
+ this->AddDotSizeSpinbutton(_page_pen, "/tools/freehand/pen", 3.0);
+
+ //Calligraphy
+ this->AddSelcueCheckbox(_page_calligraphy, "/tools/calligraphic", false);
+ this->AddNewObjectsStyle(_page_calligraphy, "/tools/calligraphic");
+ _page_calligraphy.add_line( false, "", _calligrapy_keep_selected, "",
+ _("If on, each newly created object will be selected (deselecting previous selection)"));
+
+ //Text
+ this->AddSelcueCheckbox(_page_text, "/tools/text", true);
+ this->AddGradientCheckbox(_page_text, "/tools/text", true);
+ {
+ PrefCheckButton* cb = Gtk::manage( new PrefCheckButton);
+ cb->init ( _("Show font samples in the drop-down list"), "/tools/text/show_sample_in_list", true);
+ _page_text.add_line( false, "", *cb, "", _("Show font samples alongside font names in the drop-down list in Text bar"));
+
+ _font_dialog.init(_("Show font substitution warning dialog"), "/options/font/substitutedlg", false);
+ _page_text.add_line( false, "", _font_dialog, "", _("Show font substitution warning dialog when requested fonts are not available on the system"));
+ _font_sample.init("/tools/text/font_sample", true);
+ _page_text.add_line( false, _("Font sample"), _font_sample, "", _("Change font preview sample text"), true);
+
+ cb = Gtk::manage(new PrefCheckButton);
+ cb->init ( _("Use SVG2 auto-flowed text"), "/tools/text/use_svg2", true);
+ _page_text.add_line( false, "", *cb, "", _("Use SVG2 auto-flowed text instead of SVG1.2 auto-flowed text. (Recommended)"));
+
+ _recently_used_fonts_size.init("/tools/text/recently_used_fonts_size", 0, 100, 1, 10, 10, true, false);
+ _page_text.add_line( false, _("Fonts in 'Recently used' collection:"), _recently_used_fonts_size, "",
+ _("Maximum number of fonts in the 'Recently used' font collection"), false);
+ _recently_used_fonts_size.changed_signal.connect([](double new_size) {
+ Inkscape::RecentlyUsedFonts* recently_used_fonts = Inkscape::RecentlyUsedFonts::get();
+ recently_used_fonts->change_max_list_size(new_size); });
+ }
+
+ //_page_text.add_group_header( _("Text units"));
+ //_font_output_px.init ( _("Always output text size in pixels (px)"), "/options/font/textOutputPx", true);
+ //_page_text.add_line( true, "", _font_output_px, "", _("Always convert the text size units above into pixels (px) before saving to file"));
+
+ _page_text.add_group_header( _("Font directories"));
+ _font_fontsdir_system.init( _("Use Inkscape's fonts directory"), "/options/font/use_fontsdir_system", true);
+ _page_text.add_line( true, "", _font_fontsdir_system, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's global \"share\" directory"));
+ _font_fontsdir_user.init( _("Use user's fonts directory"), "/options/font/use_fontsdir_user", true);
+ _page_text.add_line( true, "", _font_fontsdir_user, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's user configuration directory"));
+ _font_fontdirs_custom.init("/options/font/custom_fontdirs", 50);
+ _page_text.add_line(true, _("Additional font directories"), _font_fontdirs_custom, "", _("Load additional fonts from custom locations (one path per line)"), true);
+
+
+ this->AddNewObjectsStyle(_page_text, "/tools/text");
+
+ //Spray
+ AddSelcueCheckbox(_page_spray, "/tools/spray", true);
+ AddGradientCheckbox(_page_spray, "/tools/spray", false);
+
+ //Eraser
+ this->AddNewObjectsStyle(_page_eraser, "/tools/eraser");
+
+ //Paint Bucket
+ this->AddSelcueCheckbox(_page_paintbucket, "/tools/paintbucket", false);
+ this->AddNewObjectsStyle(_page_paintbucket, "/tools/paintbucket");
+
+ //Gradient
+ this->AddSelcueCheckbox(_page_gradient, "/tools/gradient", true);
+ _misc_forkvectors.init( _("Prevent sharing of gradient definitions"), "/options/forkgradientvectors/value", true);
+ _page_gradient.add_line( false, "", _misc_forkvectors, "",
+ _("When on, shared gradient definitions are automatically forked on change; uncheck to allow sharing of gradient definitions so that editing one object may affect other objects using the same gradient"), true);
+
+ _misc_gradientangle.init("/dialogs/gradienteditor/angle", -359, 359, 1, 90, 0, false, false);
+ _page_gradient.add_line( false, _("Linear gradient _angle:"), _misc_gradientangle, "",
+ _("Default angle of new linear gradients in degrees (clockwise from horizontal)"), false);
+
+ _misc_gradient_collect.init(_("Auto-delete unused gradients"), "/option/gradient/auto_collect", true);
+ _page_gradient.add_line(
+ false, "", _misc_gradient_collect, "",
+ _("When enabled, gradients that are not used will be deleted (auto-collected) automatically "
+ "from the SVG file. When disabled, unused gradients will be preserved in "
+ "the file for later use. (Note: This setting only affects new gradients.)"),
+ true);
+
+ //Dropper
+ this->AddSelcueCheckbox(_page_dropper, "/tools/dropper", true);
+ this->AddGradientCheckbox(_page_dropper, "/tools/dropper", true);
+
+ //Connector
+ this->AddSelcueCheckbox(_page_connector, "/tools/connector", true);
+ _page_connector.add_line(false, "", _connector_ignore_text, "",
+ _("If on, connector attachment points will not be shown for text objects"));
+
+#ifdef WITH_LPETOOL
+ //LPETool
+ //disabled, because the LPETool is not finished yet.
+ this->AddNewObjectsStyle(_page_lpetool, "/tools/lpetool");
+#endif // WITH_LPETOOL
+}
+
+void InkscapePreferences::get_highlight_colors(guint32 &colorsetbase, guint32 &colorsetsuccess,
+ guint32 &colorsetwarning, guint32 &colorseterror)
+{
+ using namespace Inkscape::IO::Resource;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ Glib::ustring prefix = "";
+ if (prefs->getBool("/theme/darkTheme", false)) {
+ prefix = ".dark ";
+ }
+ auto higlight = get_filename_string(ICONS, (themeiconname + "/highlights.css").c_str(), false, true);
+ if (!higlight.empty()) {
+ std::ifstream ifs(higlight);
+ std::string content((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
+ Glib::ustring result;
+ size_t startpos = content.find(prefix + ".base");
+ size_t endpos = content.find("}");
+ if (startpos != std::string::npos) {
+ result = content.substr(startpos, endpos - startpos);
+ size_t startposin = result.find("fill:");
+ size_t endposin = result.find(";");
+ result = result.substr(startposin + 5, endposin - (startposin + 5));
+ Util::trim(result);
+ Gdk::RGBA base_color = Gdk::RGBA(result);
+ SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue());
+ colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha());
+ }
+ content.erase(0, endpos + 1);
+ startpos = content.find(prefix + ".success");
+ endpos = content.find("}");
+ if (startpos != std::string::npos) {
+ result = content.substr(startpos, endpos - startpos);
+ size_t startposin = result.find("fill:");
+ size_t endposin = result.find(";");
+ result = result.substr(startposin + 5, endposin - (startposin + 5));
+ Util::trim(result);
+ Gdk::RGBA success_color = Gdk::RGBA(result);
+ SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue());
+ colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha());
+ }
+ content.erase(0, endpos + 1);
+ startpos = content.find(prefix + ".warning");
+ endpos = content.find("}");
+ if (startpos != std::string::npos) {
+ result = content.substr(startpos, endpos - startpos);
+ size_t startposin = result.find("fill:");
+ size_t endposin = result.find(";");
+ result = result.substr(startposin + 5, endposin - (startposin + 5));
+ Util::trim(result);
+ Gdk::RGBA warning_color = Gdk::RGBA(result);
+ SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue());
+ colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha());
+ }
+ content.erase(0, endpos + 1);
+ startpos = content.find(prefix + ".error");
+ endpos = content.find("}");
+ if (startpos != std::string::npos) {
+ result = content.substr(startpos, endpos - startpos);
+ size_t startposin = result.find("fill:");
+ size_t endposin = result.find(";");
+ result = result.substr(startposin + 5, endposin - (startposin + 5));
+ Util::trim(result);
+ Gdk::RGBA error_color = Gdk::RGBA(result);
+ SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue());
+ colorseterror = error_color_sp.toRGBA32(error_color.get_alpha());
+ }
+ }
+}
+
+void InkscapePreferences::resetIconsColors(bool themechange)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ if (!prefs->getBool("/theme/symbolicIcons", false)) {
+ _symbolic_base_colors.set_sensitive(false);
+ _symbolic_highlight_colors.set_sensitive(false);
+ _symbolic_base_color.setSensitive(false);
+ _symbolic_success_color.setSensitive(false);
+ _symbolic_warning_color.setSensitive(false);
+ _symbolic_error_color.setSensitive(false);
+ return;
+ }
+ if (prefs->getBool("/theme/symbolicDefaultBaseColors", true) ||
+ !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) {
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ // This colors are set on style.css of inkscape
+ Gdk::RGBA base_color = _symbolic_base_color.get_style_context()->get_color();
+ // This is a hack to fix a problematic style which isn't updated fast enough on
+ // change from dark to bright themes
+ if (themechange) {
+ auto sc = _symbolic_base_color.get_style_context();
+ base_color = get_background_color(sc);
+ }
+ SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue());
+ //we copy highlight to not use
+ guint32 colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha());
+ guint32 colorsetsuccess = colorsetbase;
+ guint32 colorsetwarning = colorsetbase;
+ guint32 colorseterror = colorsetbase;
+ get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror);
+ _symbolic_base_color.setRgba32(colorsetbase);
+ prefs->setUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase);
+ _symbolic_base_color.setSensitive(false);
+ changeIconsColors();
+ } else {
+ _symbolic_base_color.setSensitive(true);
+ }
+ if (prefs->getBool("/theme/symbolicDefaultHighColors", true)) {
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ Gdk::RGBA success_color = _symbolic_success_color.get_style_context()->get_color();
+ Gdk::RGBA warning_color = _symbolic_warning_color.get_style_context()->get_color();
+ Gdk::RGBA error_color = _symbolic_error_color.get_style_context()->get_color();
+ SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue());
+ SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue());
+ SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue());
+ //we copy base to not use
+ guint32 colorsetbase = success_color_sp.toRGBA32(success_color.get_alpha());
+ guint32 colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha());
+ guint32 colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha());
+ guint32 colorseterror = error_color_sp.toRGBA32(error_color.get_alpha());
+ get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror);
+ _symbolic_success_color.setRgba32(colorsetsuccess);
+ _symbolic_warning_color.setRgba32(colorsetwarning);
+ _symbolic_error_color.setRgba32(colorseterror);
+ prefs->setUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess);
+ prefs->setUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning);
+ prefs->setUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror);
+ _symbolic_success_color.setSensitive(false);
+ _symbolic_warning_color.setSensitive(false);
+ _symbolic_error_color.setSensitive(false);
+ changeIconsColors();
+ } else {
+ _symbolic_success_color.setSensitive(true);
+ _symbolic_warning_color.setSensitive(true);
+ _symbolic_error_color.setSensitive(true);
+ /* _complementary_colors->get_style_context()->remove_class("disabled"); */
+ }
+}
+
+void InkscapePreferences::resetIconsColorsWrapper() { resetIconsColors(false); }
+
+void InkscapePreferences::changeIconsColors()
+{
+ 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);
+ guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff);
+ guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff);
+ guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff);
+ _symbolic_base_color.setRgba32(colorsetbase);
+ _symbolic_success_color.setRgba32(colorsetsuccess);
+ _symbolic_warning_color.setRgba32(colorsetwarning);
+ _symbolic_error_color.setRgba32(colorseterror);
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ Gtk::CssProvider::create();
+ Glib::ustring css_str = "";
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ css_str = INKSCAPE.themecontext->get_symbolic_colors();
+ }
+ try {
+ INKSCAPE.themecontext->getColorizeProvider()->load_from_data(css_str);
+ } catch (const Gtk::CssProviderError &ex) {
+ g_critical("CSSProviderError::load_from_data(): failed to load '%s'\n(%s)", css_str.c_str(), ex.what().c_str());
+ }
+ Gtk::StyleContext::add_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider(),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+}
+
+void InkscapePreferences::toggleSymbolic()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ if (window ) {
+ window->get_style_context()->add_class("symbolic");
+ window->get_style_context()->remove_class("regular");
+ }
+ _symbolic_base_colors.set_sensitive(true);
+ _symbolic_highlight_colors.set_sensitive(true);
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ if (prefs->getBool("/theme/symbolicDefaultColors", true) ||
+ !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) {
+ resetIconsColors();
+ } else {
+ changeIconsColors();
+ }
+ } else {
+ if (window) {
+ window->get_style_context()->add_class("regular");
+ window->get_style_context()->remove_class("symbolic");
+ }
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ _symbolic_base_colors.set_sensitive(false);
+ _symbolic_highlight_colors.set_sensitive(false);
+ }
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+}
+
+bool InkscapePreferences::contrastChange(GdkEventButton *button_event)
+{
+ themeChange();
+ return true;
+}
+
+void InkscapePreferences::comboThemeChange()
+{
+ //we reset theming on combo change
+ _dark_theme.set_active(false);
+ _symbolic_base_colors.set_active(true);
+ if (_contrast_theme.getSpinButton()->get_value() != 10.0){
+ _contrast_theme.getSpinButton()->set_value(10.0);
+ } else {
+ themeChange();
+ }
+}
+void InkscapePreferences::contrastThemeChange()
+{
+ //we reset theming on combo change
+ themeChange(true);
+}
+
+void InkscapePreferences::themeChange(bool contrastslider)
+{
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ if (window) {
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getContrastThemeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getContrastThemeProvider());
+ }
+ if (INKSCAPE.themecontext->getThemeProvider() ) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getThemeProvider() );
+ }
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+ _dark_theme.get_parent()->set_no_show_all(false);
+ if (dark_themes[current_theme]) {
+ _dark_theme.get_parent()->show_all();
+ } else {
+ _dark_theme.get_parent()->hide();
+ }
+ auto settings = Gtk::Settings::get_default();
+ settings->property_gtk_theme_name() = current_theme;
+ bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window));
+ bool toggled = prefs->getBool("/theme/darkTheme", false) != dark;
+ prefs->setBool("/theme/darkTheme", dark);
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true, contrastslider);
+ resetIconsColors(toggled);
+ }
+}
+
+void InkscapePreferences::preferDarkThemeChange()
+{
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ if (window) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool dark = INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container *>(window));
+ bool toggled = prefs->getBool("/theme/darkTheme", false) != dark;
+ prefs->setBool("/theme/darkTheme", dark);
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+ INKSCAPE.themecontext->add_gtk_css(true);
+ // we avoid switched base colors
+ if (!_symbolic_base_colors.get_active()) {
+ prefs->setBool("/theme/symbolicDefaultBaseColors", true);
+ resetIconsColors(false);
+ _symbolic_base_colors.set_sensitive(true);
+ prefs->setBool("/theme/symbolicDefaultBaseColors", false);
+ } else {
+ resetIconsColors(toggled);
+ }
+ }
+}
+
+void InkscapePreferences::symbolicThemeCheck()
+{
+ using namespace Inkscape::IO::Resource;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ bool symbolic = false;
+ auto settings = Gtk::Settings::get_default();
+ if (settings) {
+ if (themeiconname != "") {
+ settings->property_gtk_icon_theme_name() = themeiconname;
+ }
+ }
+ // we always show symbolic in default theme (relays in hicolor theme)
+ if (themeiconname != prefs->getString("/theme/defaultIconTheme", "")) {
+
+ auto folders = get_foldernames(ICONS, { "application" });
+ for (auto &folder : folders) {
+ auto path = folder;
+ const size_t last_slash_idx = folder.find_last_of("\\/");
+ if (std::string::npos != last_slash_idx) {
+ folder.erase(0, last_slash_idx + 1);
+ }
+ if (folder == themeiconname) {
+#ifdef _WIN32
+ path += g_win32_locale_filename_from_utf8("/symbolic/actions");
+#else
+ path += "/symbolic/actions";
+#endif
+ std::vector<Glib::ustring> symbolic_icons = get_filenames(path, { ".svg" }, {});
+ if (symbolic_icons.size() > 0) {
+ symbolic = true;
+ symbolic_icons.clear();
+ }
+ }
+ }
+ } else {
+ symbolic = true;
+ }
+ if (_symbolic_icons.get_parent()) {
+ if (!symbolic) {
+ _symbolic_icons.set_active(false);
+ _symbolic_icons.get_parent()->hide();
+ _symbolic_base_colors.get_parent()->hide();
+ _symbolic_highlight_colors.get_parent()->hide();
+ _symbolic_base_color.get_parent()->get_parent()->hide();
+ _symbolic_success_color.get_parent()->get_parent()->hide();
+ } else {
+ _symbolic_icons.get_parent()->show();
+ _symbolic_base_colors.get_parent()->show();
+ _symbolic_highlight_colors.get_parent()->show();
+ _symbolic_base_color.get_parent()->get_parent()->show();
+ _symbolic_success_color.get_parent()->get_parent()->show();
+ }
+ }
+ if (symbolic) {
+ if (prefs->getBool("/theme/symbolicDefaultHighColors", true) ||
+ prefs->getBool("/theme/symbolicDefaultBaseColors", true) ||
+ !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) {
+ resetIconsColors();
+ } else {
+ changeIconsColors();
+ }
+ guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff);
+ guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff);
+ guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff);
+ guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff);
+ _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor",
+ colorsetbase);
+ _symbolic_success_color.init(_("Color for symbolic success icons:"),
+ "/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess);
+ _symbolic_warning_color.init(_("Color for symbolic warning icons:"),
+ "/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning);
+ _symbolic_error_color.init(_("Color for symbolic error icons:"),
+ "/theme/" + themeiconname + "/symbolicErrorColor", colorseterror);
+ }
+}
+/* void sp_mix_colors(cairo_t *ct, int pos, SPColor a, SPColor b)
+{
+ double arcEnd=2*M_PI;
+ cairo_set_source_rgba(ct, 1, 1, 1, 1);
+ cairo_arc(ct,pos,13,12,0,arcEnd);
+ cairo_fill(ct);
+ cairo_set_source_rgba(ct, a.v.c[0], a.v.c[1], a.v.c[2], 0.5);
+ cairo_arc(ct,pos,13,12,0,arcEnd);
+ cairo_fill(ct);
+ cairo_set_source_rgba(ct, b.v.c[0], b.v.c[1], b.v.c[2], 0.5);
+ cairo_arc(ct,pos,13,12,0,arcEnd);
+ cairo_fill(ct);
+}
+
+Glib::RefPtr< Gdk::Pixbuf > sp_mix_colors()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme");
+ guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff);
+ guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff);
+ guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff);
+ SPColor success(colorsetsuccess);
+ SPColor warning(colorsetwarning);
+ SPColor error(colorseterror);
+ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 82, 26);
+ cairo_t *ct = cairo_create(s);
+ // success + warning
+ sp_mix_colors(ct, 13, success, warning);
+ sp_mix_colors(ct, 41, success, error);
+ sp_mix_colors(ct, 69, warning, error);
+ 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, 82, 26);
+ cairo_surface_destroy(s);
+ return pixbuf;
+} */
+
+void InkscapePreferences::changeIconsColor(guint32 /*color*/)
+{
+ changeIconsColors();
+ /* _complementary_colors->set(sp_mix_colors()); */
+}
+
+void InkscapePreferences::initPageUI()
+{
+ Gtk::TreeModel::iterator iter_ui = this->AddPage(_page_ui, _("Interface"), PREFS_PAGE_UI);
+
+ Glib::ustring languages[] = {_("System default"),
+ _("Albanian (sq)"), _("Arabic (ar)"), _("Armenian (hy)"), _("Assamese (as)"), _("Azerbaijani (az)"),
+ _("Basque (eu)"), _("Belarusian (be)"), _("Bulgarian (bg)"), _("Bengali (bn)"), _("Bengali/Bangladesh (bn_BD)"), _("Bodo (brx)"), _("Breton (br)"),
+ _("Catalan (ca)"), _("Valencian Catalan (ca@valencia)"), _("Chinese/China (zh_CN)"), _("Chinese/Taiwan (zh_TW)"), _("Croatian (hr)"), _("Czech (cs)"),
+ _("Danish (da)"), _("Dogri (doi)"), _("Dutch (nl)"), _("Dzongkha (dz)"),
+ _("German (de)"), _("Greek (el)"),
+ _("English (en)"), _("English/Australia (en_AU)"), _("English/Canada (en_CA)"), _("English/Great Britain (en_GB)"), _("Esperanto (eo)"), _("Estonian (et)"),
+ _("Farsi (fa)"), _("Finnish (fi)"), _("French (fr)"),
+ _("Galician (gl)"), _("Gujarati (gu)"),
+ _("Hebrew (he)"), _("Hindi (hi)"), _("Hungarian (hu)"),
+ _("Icelandic (is)"), _("Indonesian (id)"), _("Irish (ga)"), _("Italian (it)"),
+ _("Japanese (ja)"),
+ _("Kannada (kn)"), _("Kashmiri in Perso-Arabic script (ks@aran)"), _("Kashmiri in Devanagari script (ks@deva)"), _("Khmer (km)"), _("Kinyarwanda (rw)"), _("Konkani (kok)"), _("Konkani in Latin script (kok@latin)"), _("Korean (ko)"),
+ _("Latvian (lv)"), _("Lithuanian (lt)"),
+ _("Macedonian (mk)"), _("Maithili (mai)"), _("Malayalam (ml)"), _("Manipuri (mni)"), _("Manipuri in Bengali script (mni@beng)"), _("Marathi (mr)"), _("Mongolian (mn)"),
+ _("Nepali (ne)"), _("Norwegian Bokmål (nb)"), _("Norwegian Nynorsk (nn)"),
+ _("Odia (or)"),
+ _("Panjabi (pa)"), _("Polish (pl)"), _("Portuguese (pt)"), _("Portuguese/Brazil (pt_BR)"),
+ _("Romanian (ro)"), _("Russian (ru)"),
+ _("Sanskrit (sa)"), _("Santali (sat)"), _("Santali in Devanagari script (sat@deva)"), _("Serbian (sr)"), _("Serbian in Latin script (sr@latin)"),
+ _("Sindhi (sd)"), _("Sindhi in Devanagari script (sd@deva)"), _("Slovak (sk)"), _("Slovenian (sl)"), _("Spanish (es)"), _("Spanish/Mexico (es_MX)"), _("Swedish (sv)"),
+ _("Tamil (ta)"), _("Telugu (te)"), _("Thai (th)"), _("Turkish (tr)"),
+ _("Ukrainian (uk)"), _("Urdu (ur)"),
+ _("Vietnamese (vi)")};
+ Glib::ustring langValues[] = {"",
+ "sq", "ar", "hy", "as", "az",
+ "eu", "be", "bg", "bn", "bn_BD", "brx", "br",
+ "ca", "ca@valencia", "zh_CN", "zh_TW", "hr", "cs",
+ "da", "doi", "nl", "dz",
+ "de", "el",
+ "en", "en_AU", "en_CA", "en_GB", "eo", "et",
+ "fa", "fi", "fr",
+ "gl", "gu",
+ "he", "hi", "hu",
+ "is", "id", "ga", "it",
+ "ja",
+ "kn", "ks@aran", "ks@deva", "km", "rw", "kok", "kok@latin", "ko",
+ "lv", "lt",
+ "mk", "mai", "ml", "mni", "mni@beng", "mr", "mn",
+ "ne", "nb", "nn",
+ "or",
+ "pa", "pl", "pt", "pt_BR",
+ "ro", "ru",
+ "sa", "sat", "sat@deva", "sr", "sr@latin",
+ "sd", "sd@deva", "sk", "sl", "es", "es_MX", "sv",
+ "ta", "te", "th", "tr",
+ "uk", "ur",
+ "vi" };
+
+ {
+ // sorting languages according to translated name
+ int i = 0;
+ int j = 0;
+ int n = sizeof( languages ) / sizeof( Glib::ustring );
+ Glib::ustring key_language;
+ Glib::ustring key_langValue;
+ for ( j = 1 ; j < n ; j++ ) {
+ key_language = languages[j];
+ key_langValue = langValues[j];
+ i = j-1;
+ while ( i >= 0
+ && ( ( languages[i] > key_language
+ && langValues[i] != "" )
+ || key_langValue == "" ) )
+ {
+ languages[i+1] = languages[i];
+ langValues[i+1] = langValues[i];
+ i--;
+ }
+ languages[i+1] = key_language;
+ langValues[i+1] = key_langValue;
+ }
+ }
+
+ _ui_languages.init( "/ui/language", languages, langValues, G_N_ELEMENTS(languages), languages[0]);
+ _page_ui.add_line( false, _("Language:"), _ui_languages, "",
+ _("Set the language for menus and number formats"), false, reset_icon());
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ _ui_colorsliders_top.init( _("Work-around color sliders not drawing"), "/options/workarounds/colorsontop", false);
+ _page_ui.add_line( false, "", _ui_colorsliders_top, "",
+ _("When on, will attempt to work around bugs in certain GTK themes drawing color sliders"), true);
+
+
+ _misc_recent.init("/options/maxrecentdocuments/value", 0.0, 1000.0, 1.0, 1.0, 1.0, true, false);
+
+ Gtk::Button* reset_recent = Gtk::manage(new Gtk::Button(_("Clear list")));
+ reset_recent->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_open_recent_clicked));
+
+ _page_ui.add_line( false, _("Maximum documents in Open _Recent:"), _misc_recent, "",
+ _("Set the maximum length of the Open Recent list in the File menu, or clear the list"), false, reset_recent);
+
+ _page_ui.add_group_header(_("_Zoom correction factor (in %)"));
+ _page_ui.add_group_note(_("Adjust the slider until the length of the ruler on your screen matches its real length. This information is used when zooming to 1:1, 1:2, etc., to display objects in their true sizes"));
+ _ui_zoom_correction.init(300, 30, 0.01, 500.0, 1.0, 10.0, 1.0);
+ _page_ui.add_line( true, "", _ui_zoom_correction, "", "", true);
+
+ _ui_realworldzoom.init( _("Show zoom percentage corrected by factor"), "/options/zoomcorrection/shown", true);
+ _page_ui.add_line( false, "", _ui_realworldzoom, "", _("Zoom percentage can be either by the physical units or by pixels."));
+
+ _ui_pageorigin.init( _("Origin always on current page"), "/options/origincorrection/page", true);
+ _page_ui.add_line( false, "", _ui_pageorigin, "", _("Rulers and tools will display position information relative to the current page, instead of the position on the canvas (corresponding to the first page's position)."));
+
+ _ui_yaxisdown.init( _("Origin at upper left with y-axis pointing down"), "/options/yaxisdown", true);
+ _page_ui.add_line( false, "", _ui_yaxisdown, "",
+ _("When off, origin is at lower left corner and y-axis points up"), false, reset_icon());
+
+ _ui_rotationlock.init(_("Lock canvas rotation by default"), "/options/rotationlock", false);
+ _page_ui.add_line(false, "", _ui_rotationlock, "",
+ _("Prevent accidental canvas rotation by disabling on-canvas keyboard and mouse actions for rotation"), true);
+
+ _ui_rulersel.init( _("Show selection in ruler"), "/options/ruler/show_bbox", true);
+ _page_ui.add_line( false, "", _ui_rulersel, "", _("Shows a blue line in the ruler where the selection is."));
+
+ _page_ui.add_group_header(_("User Interface"));
+ // _page_ui.add_group_header(_("Handle size"));
+ _mouse_grabsize.init("/options/grabsize/value", 1, 15, 1, 2, 3, 0);
+ _page_ui.add_line(true, _("Handle size"), _mouse_grabsize, "", _("Set the relative size of node handles"), true);
+ _narrow_spinbutton.init(_("Use narrow number entry boxes"), "/theme/narrowSpinButton", false);
+ _page_ui.add_line(false, "", _narrow_spinbutton, "", _("Make number editing boxes smaller by limiting padding"), false);
+
+ _page_ui.add_group_header(_("Status bar"));
+ auto sb_style = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ sb_style->init(_("Show current style"), "/statusbar/visibility/style", true);
+ _page_ui.add_line(false, "", *sb_style, "", _("Control visibility of current fill, stroke and opacity in status bar."), true);
+ auto sb_layer = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ sb_layer->init(_("Show layer selector"), "/statusbar/visibility/layer", true);
+ _page_ui.add_line(false, "", *sb_layer, "", _("Control visibility of layer selection menu in status bar."), true);
+ auto sb_coords = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ sb_coords->init(_("Show mouse coordinates"), "/statusbar/visibility/coordinates", true);
+ _page_ui.add_line(false, "", *sb_coords, "", _("Control visibility of mouse coordinates X & Y in status bar."), true);
+ auto sb_rotate = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ sb_rotate->init(_("Show canvas rotation"), "/statusbar/visibility/rotation", true);
+ _page_ui.add_line(false, "", *sb_rotate, "", _("Control visibility of canvas rotation in status bar."), true);
+
+ _page_ui.add_group_header(_("Mouse cursors"));
+ _ui_cursorscaling.init(_("Enable scaling"), "/options/cursorscaling", true);
+ _page_ui.add_line(false, "", _ui_cursorscaling, "", _("When off, cursor scaling is disabled. Cursor scaling may be broken when fractional scaling is enabled."), true);
+ _ui_cursor_shadow.init(_("Show drop shadow"), "/options/cursor-drop-shadow", true);
+ _page_ui.add_line(false, "", _ui_cursor_shadow, "", _("Control visibility of drop shadow for Inkscape cursors."), true);
+
+ // Theme
+ _page_theme.add_group_header(_("Theme"));
+ _dark_theme.init(_("Use dark theme"), "/theme/preferDarkTheme", false);
+ Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+ Glib::ustring default_theme = prefs->getString("/theme/defaultGtkTheme");
+ Glib::ustring theme = "";
+ {
+ dark_themes = INKSCAPE.themecontext->get_available_themes();
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> values;
+ std::map<Glib::ustring, bool>::iterator it = dark_themes.begin();
+ // Iterate over the map using Iterator till end.
+ for (std::pair<std::string, int> element : dark_themes) {
+ Glib::ustring theme = element.first;
+ ++it;
+ if (theme == default_theme) {
+ continue;
+ }
+ values.emplace_back(theme);
+ labels.emplace_back(theme);
+ }
+ std::sort(labels.begin(), labels.end());
+ std::sort(values.begin(), values.end());
+ labels.erase(unique(labels.begin(), labels.end()), labels.end());
+ values.erase(unique(values.begin(), values.end()), values.end());
+ values.emplace_back("");
+ Glib::ustring default_theme_label = _("Use system theme");
+ default_theme_label += " (" + default_theme + ")";
+ labels.emplace_back(default_theme_label);
+
+ _gtk_theme.init("/theme/gtkTheme", labels, values, "");
+ _page_theme.add_line(false, _("Change GTK theme:"), _gtk_theme, "", "", false);
+ _gtk_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::comboThemeChange));
+ }
+
+ _sys_user_themes_dir_copy.init(g_build_filename(g_get_user_data_dir(), "themes", nullptr), _("Open themes folder"));
+ _page_theme.add_line(true, _("User themes:"), _sys_user_themes_dir_copy, "", _("Location of the user’s themes"), true, Gtk::manage(new Gtk::Box()));
+ _contrast_theme.init("/theme/contrast", 1, 10, 1, 2, 10, 1);
+
+ _page_theme.add_line(true, "", _dark_theme, "", _("Use dark theme"), true);
+ {
+ auto font_scale = new Inkscape::UI::Widget::PrefSlider();
+ font_scale = Gtk::manage(font_scale);
+ font_scale->init(ThemeContext::get_font_scale_pref_path(), 50, 150, 5, 5, 100, 0); // 50% to 150%
+ font_scale->getSlider()->signal_format_value().connect([=](double val) {
+ return Glib::ustring::format(std::fixed, std::setprecision(0), val) + "%";
+ });
+ // Live updates commented out; too disruptive
+ // font_scale->getSlider()->signal_value_changed().connect([=](){
+ // INKSCAPE.themecontext->adjust_global_font_scale(font_scale->getSlider()->get_value() / 100.0);
+ // });
+ auto space = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
+ space->set_valign(Gtk::ALIGN_CENTER);
+ auto reset = Gtk::make_managed<Gtk::Button>();
+ reset->set_tooltip_text(_("Reset font size to 100%"));
+ reset->set_image_from_icon_name("reset-settings-symbolic");
+ reset->set_size_request(30, -1);
+ auto apply = Gtk::make_managed<Gtk::Button>(_("Apply"));
+ apply->set_tooltip_text(_("Apply font size changes to the UI"));
+ apply->set_valign(Gtk::ALIGN_FILL);
+ apply->set_margin_end(5);
+ reset->set_valign(Gtk::ALIGN_FILL);
+ space->add(*apply);
+ space->add(*reset);
+ reset->signal_clicked().connect([=](){
+ font_scale->getSlider()->set_value(100);
+ INKSCAPE.themecontext->adjustGlobalFontScale(1.0);
+ });
+ apply->signal_clicked().connect([=](){
+ INKSCAPE.themecontext->adjustGlobalFontScale(font_scale->getSlider()->get_value() / 100.0);
+ });
+ _page_theme.add_line(false, _("_Font scale:"), *font_scale, "", _("Adjust size of UI fonts"), true, space);
+ }
+ auto space = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
+ space->set_size_request(_sb_width / 3, -1);
+ _page_theme.add_line(false, _("_Contrast:"), _contrast_theme, "",
+ _("Make background brighter or darker to adjust contrast"), true, space);
+ _contrast_theme.getSlider()->signal_value_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::contrastThemeChange));
+
+ if (dark_themes[current_theme]) {
+ _dark_theme.get_parent()->set_no_show_all(false);
+ _dark_theme.get_parent()->show_all();
+ } else {
+ _dark_theme.get_parent()->set_no_show_all(true);
+ _dark_theme.get_parent()->hide();
+ }
+ _dark_theme.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::preferDarkThemeChange));
+ // Icons
+ _page_theme.add_group_header(_("Icons"));
+ {
+ using namespace Inkscape::IO::Resource;
+ auto folders = get_foldernames(ICONS, { "application" });
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> values;
+ Glib::ustring default_icon_theme = prefs->getString("/theme/defaultIconTheme");
+ for (auto &folder : folders) {
+ // from https://stackoverflow.com/questions/8520560/get-a-file-name-from-a-path#8520871
+ // Maybe we can link boost path utilities
+ // Remove directory if present.
+ // Do this before extension removal in case the directory has a period character.
+ const size_t last_slash_idx = folder.find_last_of("\\/");
+ if (std::string::npos != last_slash_idx) {
+ folder.erase(0, last_slash_idx + 1);
+ }
+ // we want use Adwaita instead fallback hicolor theme
+ if (folder == default_icon_theme) {
+ continue;
+ }
+ labels.emplace_back(folder);
+ values.emplace_back(folder);
+ }
+ std::sort(labels.begin(), labels.end());
+ std::sort(values.begin(), values.end());
+ labels.erase(unique(labels.begin(), labels.end()), labels.end());
+ values.erase(unique(values.begin(), values.end()), values.end());
+ values.emplace_back("");
+ Glib::ustring default_icon_label = _("Use system icons");
+ default_icon_label += " (" + default_icon_theme + ")";
+ labels.emplace_back(default_icon_label);
+
+ _icon_theme.init("/theme/iconTheme", labels, values, "");
+ _page_theme.add_line(false, _("Change icon theme:"), _icon_theme, "", "", false);
+ _icon_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::symbolicThemeCheck));
+ _sys_user_icons_dir_copy.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::ICONS, ""),
+ _("Open icons folder"));
+ _page_theme.add_line(true, _("User icons: "), _sys_user_icons_dir_copy, "", _("Location of the user’s icons"), true, Gtk::manage(new Gtk::Box()));
+ }
+ Glib::ustring themeiconname = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+ _symbolic_icons.init(_("Use symbolic icons"), "/theme/symbolicIcons", false);
+ _symbolic_icons.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::toggleSymbolic));
+ _page_theme.add_line(true, "", _symbolic_icons, "", "", true);
+ _symbolic_base_colors.init(_("Use default base color for icons"), "/theme/symbolicDefaultBaseColors", true);
+ _symbolic_base_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper));
+ _page_theme.add_line(true, "", _symbolic_base_colors, "", "", true);
+ _symbolic_highlight_colors.init(_("Use default highlight colors for icons"), "/theme/symbolicDefaultHighColors", true);
+ _symbolic_highlight_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper));
+ _page_theme.add_line(true, "", _symbolic_highlight_colors, "", "", true);
+ _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor",
+ 0x2E3436ff);
+ _symbolic_success_color.init(_("Color for symbolic success icons:"),
+ "/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff);
+ _symbolic_warning_color.init(_("Color for symbolic warning icons:"),
+ "/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff);
+ _symbolic_error_color.init(_("Color for symbolic error icons:"), "/theme/" + themeiconname + "/symbolicErrorColor",
+ 0xCC0000ff);
+ _symbolic_base_color.get_style_context()->add_class("system_base_color");
+ _symbolic_success_color.get_style_context()->add_class("system_success_color");
+ _symbolic_warning_color.get_style_context()->add_class("system_warning_color");
+ _symbolic_error_color.get_style_context()->add_class("system_error_color");
+ _symbolic_base_color.get_style_context()->add_class("symboliccolors");
+ _symbolic_success_color.get_style_context()->add_class("symboliccolors");
+ _symbolic_warning_color.get_style_context()->add_class("symboliccolors");
+ _symbolic_error_color.get_style_context()->add_class("symboliccolors");
+ _symbolic_base_color.connectChanged(sigc::mem_fun(*this, &InkscapePreferences::changeIconsColor));
+ _symbolic_warning_color.connectChanged(sigc::mem_fun(*this, &InkscapePreferences::changeIconsColor));
+ _symbolic_success_color.connectChanged(sigc::mem_fun(*this, &InkscapePreferences::changeIconsColor));
+ _symbolic_error_color.connectChanged(sigc::mem_fun(*this, &InkscapePreferences::changeIconsColor));
+ /* _complementary_colors = Gtk::manage(new Gtk::Image()); */
+ Gtk::Box *icon_buttons = Gtk::manage(new Gtk::Box());
+ icon_buttons->pack_start(_symbolic_base_color, true, true, 4);
+ _page_theme.add_line(false, "", *icon_buttons, _("Icon color base"),
+ _("Base color for icons"), false);
+ Gtk::Box *icon_buttons_hight = Gtk::manage(new Gtk::Box());
+ icon_buttons_hight->pack_start(_symbolic_success_color, true, true, 4);
+ icon_buttons_hight->pack_start(_symbolic_warning_color, true, true, 4);
+ icon_buttons_hight->pack_start(_symbolic_error_color, true, true, 4);
+ /* icon_buttons_hight->pack_start(*_complementary_colors, true, true, 4); */
+ _page_theme.add_line(false, "", *icon_buttons_hight, _("Icon color highlights"),
+ _("Highlight colors supported by some symbolic icon themes"),
+ false);
+ Gtk::Box *icon_buttons_def = Gtk::manage(new Gtk::Box());
+ resetIconsColors();
+ changeIconsColor(0xffffffff);
+ _page_theme.add_line(false, "", *icon_buttons_def, "",
+ _("Reset theme colors for some symbolic icon themes"),
+ false);
+ Glib::ustring menu_icons_labels[] = {_("Yes"), _("No"), _("Theme decides")};
+ int menu_icons_values[] = {1, -1, 0};
+ _menu_icons.init("/theme/menuIcons", menu_icons_labels, menu_icons_values, G_N_ELEMENTS(menu_icons_labels), 0);
+ _page_theme.add_line(false, _("Show icons in menus:"), _menu_icons, "",
+ _("You can either enable or disable all icons in menus. By default, the setting for the 'use-icon' attribute in the 'menus.ui' file determines whether to display icons in menus."), false, reset_icon());
+ _shift_icons.init(_("Shift icons in menus"), "/theme/shiftIcons", true);
+ _page_theme.add_line(true, "", _shift_icons, "",
+ _("This preference fixes icon positions in menus."), false, reset_icon());
+
+ _page_theme.add_group_header(_("XML Editor"));
+#if WITH_GSOURCEVIEW
+ {
+ auto manager = gtk_source_style_scheme_manager_get_default();
+ auto ids = gtk_source_style_scheme_manager_get_scheme_ids(manager);
+
+ auto syntax = Gtk::make_managed<UI::Widget::PrefCombo>();
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> values;
+ for (const char* style = *ids; style; style = *++ids) {
+ if (auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, style)) {
+ auto name = gtk_source_style_scheme_get_name(scheme);
+ labels.emplace_back(name);
+ }
+ else {
+ labels.emplace_back(style);
+ }
+ values.emplace_back(style);
+ }
+ syntax->init("/theme/syntax-color-theme", labels, values, "");
+ _page_theme.add_line(false, _("Color theme:"), *syntax, "", _("Syntax coloring for XML Editor"), false);
+ }
+#endif
+ {
+ auto font_button = Gtk::make_managed<Gtk::Button>("...");
+ font_button->set_halign(Gtk::ALIGN_START);
+ auto font_box = Gtk::make_managed<Gtk::Entry>();
+ font_box->set_editable(false);
+ font_box->set_sensitive(false);
+ auto theme = INKSCAPE.themecontext;
+ font_box->set_text(theme->getMonospacedFont().to_string());
+ font_button->signal_clicked().connect([=](){
+ Gtk::FontChooserDialog dlg;
+ // show fixed-size fonts only
+ dlg.set_filter_func([](const Glib::RefPtr<const Pango::FontFamily>& family, const Glib::RefPtr<const Pango::FontFace>& face) {
+ return family && family->is_monospace();
+ });
+ dlg.set_font_desc(theme->getMonospacedFont());
+ dlg.set_position(Gtk::WIN_POS_MOUSE);
+ dlg.set_modal();
+ if (dlg.run() == Gtk::RESPONSE_OK) {
+ auto desc = dlg.get_font_desc();
+ theme->saveMonospacedFont(desc);
+ theme->adjustGlobalFontScale(theme->getFontScale() / 100);
+ font_box->set_text(desc.to_string());
+ }
+ });
+ _page_theme.add_line(false, _("Monospaced font:"), *font_box, "", _("Select fixed-width font"), true, font_button);
+
+ auto mono_font = Gtk::make_managed<UI::Widget::PrefCheckButton>();
+ mono_font->init( _("Use monospaced font"), "/dialogs/xml/mono-font", false);
+ _page_theme.add_line(false, _("XML tree:"), *mono_font, "", _("Use fixed-width font in XML Editor"), false);
+ }
+
+ //=======================================================================================================
+
+ this->AddPage(_page_theme, _("Theming"), iter_ui, PREFS_PAGE_UI_THEME);
+ symbolicThemeCheck();
+
+ // Toolbars
+ _page_toolbars.add_group_header(_("Toolbars"));
+ try {
+ auto builder = Inkscape::UI::create_builder("toolbar-tool-prefs.ui");
+ Gtk::Widget* toolbox = nullptr;
+ builder->get_widget("tool-toolbar-prefs", toolbox);
+
+ sp_traverse_widget_tree(toolbox, [=](Gtk::Widget* widget){
+ if (auto button = dynamic_cast<Gtk::ToggleButton*>(widget)) {
+ assert(GTK_IS_ACTIONABLE(widget->gobj()));
+ // do not execute any action:
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(widget->gobj()), "");
+
+ button->set_sensitive();
+ auto action_name = sp_get_action_target(button);
+ auto path = ToolboxFactory::get_tool_visible_buttons_path(action_name);
+ auto visible = Inkscape::Preferences::get()->getBool(path, true);
+ button->set_active(visible);
+ button->signal_toggled().connect([=](){
+ Inkscape::Preferences::get()->setBool(path, button->get_active());
+ });
+ auto *iapp = InkscapeApplication::instance();
+ if (iapp) {
+ auto tooltip =
+ iapp->get_action_extra_data().get_tooltip_for_action(get_tool_action(action_name), true, true);
+ button->set_tooltip_markup(tooltip);
+ }
+ }
+ return false;
+ });
+ _page_toolbars.add_line(false, "", *toolbox, "", _("Select visible tool buttons"), true);
+
+ struct tbar_info {const char* label; const char* prefs;} toolbars[] = {
+ {_("Toolbox icon size:"), ToolboxFactory::tools_icon_size},
+ {_("Control bar icon size:"), ToolboxFactory::ctrlbars_icon_size},
+ };
+ for (auto&& tbox : toolbars) {
+ auto slider = Gtk::manage(new UI::Widget::PrefSlider(false));
+ const int min = ToolboxFactory::min_pixel_size;
+ const int max = ToolboxFactory::max_pixel_size;
+ slider->init(tbox.prefs, min, max, 1, 4, min, 0);
+ slider->getSlider()->signal_format_value().connect([](double val){
+ return Glib::ustring::format(std::fixed, std::setprecision(0), val * 100.0 / min) + "%";
+ });
+ slider->getSlider()->get_style_context()->add_class("small-marks");
+ for (int i = min; i <= max; i += 8) {
+ slider->getSlider()->add_mark(i, Gtk::POS_BOTTOM, i % min ? "" : (std::to_string(100 * i / min) + "%").c_str());
+ }
+ _page_toolbars.add_line(false, tbox.label, *slider, "", _("Adjust toolbar icon size"));
+ }
+
+ std::vector<PrefItem> snap = {
+ { _("Simple"), 1, _("Present simplified snapping options that manage all advanced settings"), true },
+ { _("Advanced"), 0, _("Expose all snapping options for manual control") },
+ { _("Permanent"), 2, _("All advanced snap options appear in a permanent bar") },
+ };
+ _page_toolbars.add_line(false, _("Snap controls bar:"), *Gtk::make_managed<PrefRadioButtons>(snap, "/toolbox/simplesnap"), "", "");
+ } catch (const Glib::Error &ex) {
+ g_error("Couldn't load toolbar-tool-prefs user interface file.");
+ }
+
+ this->AddPage(_page_toolbars, _("Toolbars"), iter_ui, PREFS_PAGE_UI_TOOLBARS);
+
+ // Windows
+ _win_save_geom.init ( _("Save and restore window geometry for each document"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_FILE, true, nullptr);
+ _win_save_geom_prefs.init ( _("Remember and use last window's geometry"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_LAST, false, &_win_save_geom);
+ _win_save_geom_off.init ( _("Don't save window geometry"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_NONE, false, &_win_save_geom);
+
+ _win_native.init ( _("Native open/save dialogs"), "/options/desktopintegration/value", 1, true, nullptr);
+ _win_gtk.init ( _("GTK open/save dialogs"), "/options/desktopintegration/value", 0, false, &_win_native);
+
+ _win_show_boot.init ( _("Show Welcome dialog"), "/options/boot/enabled", true);
+ _win_hide_task.init ( _("Dialogs are hidden in taskbar"), "/options/dialogsskiptaskbar/value", true);
+ _win_save_viewport.init ( _("Save and restore documents viewport"), "/options/savedocviewport/value", true);
+ _win_zoom_resize.init ( _("Zoom when window is resized"), "/options/stickyzoom/value", false);
+ _win_ontop_none.init ( C_("Dialog on top", "None"), "/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_NONE, false, nullptr);
+ _win_ontop_normal.init ( _("Normal"), "/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_NORMAL, true, &_win_ontop_none);
+ _win_ontop_agressive.init ( _("Aggressive"), "/options/transientpolicy/value", PREFS_DIALOGS_WINDOWS_AGGRESSIVE, false, &_win_ontop_none);
+
+ _win_dialogs_labels_auto.init( _("Automatic"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_AUTO, true, nullptr);
+ _win_dialogs_labels_active.init( _("Active"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_ACTIVE, true, nullptr);
+ _win_dialogs_labels_off.init( _("Off"), "/options/notebooklabels/value", PREFS_NOTEBOOK_LABELS_OFF, false, &_win_dialogs_labels_auto);
+
+ {
+ Glib::ustring defaultSizeLabels[] = {C_("Window size", "Default"),
+ C_("Window size", "Small"),
+ C_("Window size", "Large"),
+ C_("Window size", "Maximized")};
+ int defaultSizeValues[] = {PREFS_WINDOW_SIZE_NATURAL,
+ PREFS_WINDOW_SIZE_SMALL,
+ PREFS_WINDOW_SIZE_LARGE,
+ PREFS_WINDOW_SIZE_MAXIMIZED};
+
+ _win_default_size.init( "/options/defaultwindowsize/value", defaultSizeLabels, defaultSizeValues, G_N_ELEMENTS(defaultSizeLabels), PREFS_WINDOW_SIZE_NATURAL);
+ _page_windows.add_line( false, _("Default window size:"), _win_default_size, "",
+ _("Set the default window size"), false);
+ }
+
+ _page_windows.add_group_header( _("Saving window size and position"), 4);
+ _page_windows.add_line( true, "", _win_save_geom_off, "",
+ _("Let the window manager determine placement of all windows"));
+ _page_windows.add_line( true, "", _win_save_geom_prefs, "",
+ _("Remember and use the last window's geometry (saves geometry to user preferences)"));
+ _page_windows.add_line( true, "", _win_save_geom, "",
+ _("Save and restore window geometry for each document (saves geometry in the document)"));
+
+#ifdef _WIN32
+ _page_windows.add_group_header( _("Desktop integration"));
+ _page_windows.add_line( true, "", _win_native, "",
+ _("Use Windows like open and save dialogs"));
+ _page_windows.add_line( true, "", _win_gtk, "",
+ _("Use GTK open and save dialogs "));
+#endif
+ _page_windows.add_group_header(_("Dialogs settings"), 4);
+
+ std::vector<PrefItem> dock = {
+ { _("Docked"), PREFS_DIALOGS_BEHAVIOR_DOCKABLE, _("Allow dialog docking"), true },
+ { _("Floating"), PREFS_DIALOGS_BEHAVIOR_FLOATING, _("Disable dialog docking") }
+ };
+ _page_windows.add_line(true, _("Dialog behavior"), *Gtk::make_managed<PrefRadioButtons>(dock, "/options/dialogtype/value"), "", "", false, reset_icon());
+
+#ifndef _WIN32 // non-Win32 special code to enable transient dialogs
+ std::vector<PrefItem> on_top = {
+ { C_("Dialog on top", "None"), PREFS_DIALOGS_WINDOWS_NONE, _("Dialogs are treated as regular windows") },
+ { _("Normal"), PREFS_DIALOGS_WINDOWS_NORMAL, _("Dialogs stay on top of document windows"), true },
+ { _("Aggressive"), PREFS_DIALOGS_WINDOWS_AGGRESSIVE, _("Same as Normal but may work better with some window managers") }
+ };
+ _page_windows.add_line(true, _("Dialog on top"), *Gtk::make_managed<PrefRadioButtons>(on_top, "/options/transientpolicy/value"), "", "");
+#endif
+ std::vector<PrefItem> labels = {
+ { _("Automatic"), PREFS_NOTEBOOK_LABELS_AUTO, _("Dialog names will be displayed when there is enough space"), true },
+ { _("Active"), PREFS_NOTEBOOK_LABELS_ACTIVE, _("Only show label on active") },
+ { _("Off"), PREFS_NOTEBOOK_LABELS_OFF, _("Only show dialog icons") }
+ };
+ _page_windows.add_line(true, _("Labels behavior"), *Gtk::make_managed<PrefRadioButtons>(labels, "/options/notebooklabels/value"), "", "", false, reset_icon());
+
+ auto save_dlg = Gtk::make_managed<PrefCheckButton>();
+ save_dlg->init(_("Save and restore dialogs' status"), "/options/savedialogposition/value", true);
+ _page_windows.add_line(true, "", *save_dlg, "", _("Save and restore dialogs' status (the last open windows dialogs are saved when it closes)"));
+
+#ifndef _WIN32 // FIXME: Temporary Win32 special code to enable transient dialogs
+ _page_windows.add_line( true, "", _win_hide_task, "",
+ _("Whether dialog windows are to be hidden in the window manager taskbar"));
+#endif
+
+ _page_windows.add_group_header( _("Miscellaneous"));
+
+ _page_windows.add_line( true, "", _win_show_boot, "",
+ _("Whether the Welcome dialog will be shown when Inkscape starts."));
+ _page_windows.add_line( true, "", _win_zoom_resize, "",
+ _("Zoom drawing when document window is resized, to keep the same area visible (this is the default which can be changed in any window using the button above the right scrollbar)"));
+ _page_windows.add_line( true, "", _win_save_viewport, "",
+ _("Save documents viewport (zoom and panning position). Useful to turn off when sharing version controlled files."));
+
+ this->AddPage(_page_windows, _("Windows"), iter_ui, PREFS_PAGE_UI_WINDOWS);
+
+ // Color pickers
+ _compact_colorselector.init(_("Use compact color selector mode switch"), "/colorselector/switcher", true);
+ _page_color_pickers.add_line(false, "", _compact_colorselector, "", _("Use compact combo box for selecting color modes"), false);
+
+ _page_color_pickers.add_group_header(_("Visible color pickers"));
+ {
+ auto container = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
+ auto prefs = Inkscape::Preferences::get();
+ for (auto&& picker : Inkscape::UI::Widget::get_color_pickers()) {
+ auto btn = Gtk::make_managed<Gtk::ToggleButton>();
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
+ auto label = Gtk::make_managed<Gtk::Label>(picker.label);
+ label->set_valign(Gtk::ALIGN_CENTER);
+ box->pack_start(*label);
+ box->pack_start(*Gtk::make_managed<Gtk::Image>(picker.icon, Gtk::ICON_SIZE_BUTTON));
+ box->set_spacing(3);
+ auto path = picker.visibility_path;
+ btn->set_active(prefs->getBool(path));
+ btn->add(*box);
+ btn->signal_toggled().connect([=]() {
+ prefs->setBool(path, btn->get_active());
+ auto buttons = container->get_children();
+ if (std::find_if(begin(buttons), end(buttons), [](Gtk::Widget* b) { return static_cast<Gtk::ToggleButton*>(b)->get_active(); }) == end(buttons)) {
+ // all pickers hidden; not a good combination; select first one
+ static_cast<Gtk::ToggleButton*>(buttons.front())->set_active();
+ }
+ });
+ container->pack_start(*btn);
+ }
+ container->show_all();
+ container->set_spacing(5);
+ _page_color_pickers.add_line(true, "", *container, "", _("Select color pickers"), false);
+ }
+
+ AddPage(_page_color_pickers, _("Color Selector"), iter_ui, PREFS_PAGE_UI_COLOR_PICKERS);
+ // end of Color pickers
+
+ // Grids
+ _page_grids.add_group_header( _("Line color when zooming out"));
+
+ _grids_no_emphasize_on_zoom.init( _("Minor grid line color"), "/options/grids/no_emphasize_when_zoomedout", 1, true, nullptr);
+ _page_grids.add_line( true, "", _grids_no_emphasize_on_zoom, "", _("The gridlines will be shown in minor grid line color"), false);
+ _grids_emphasize_on_zoom.init( _("Major grid line color"), "/options/grids/no_emphasize_when_zoomedout", 0, false, &_grids_no_emphasize_on_zoom);
+ _page_grids.add_line( true, "", _grids_emphasize_on_zoom, "", _("The gridlines will be shown in major grid line color"), false);
+
+ _page_grids.add_group_header( _("Default grid settings"));
+
+ _page_grids.add_line( true, "", _grids_notebook, "", "", false);
+ _grids_notebook.append_page(_grids_xy, N_("Rectangular Grid"));
+ _grids_notebook.append_page(_grids_axonom, N_("Axonometric Grid"));
+ // Rectangular SPGrid properties
+ _grids_xy_units.init("/options/grids/xy/units");
+ _grids_xy.add_line( false, _("Grid units:"), _grids_xy_units, "", "", false);
+ _grids_xy_origin_x.init("/options/grids/xy/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false);
+ _grids_xy_origin_y.init("/options/grids/xy/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false);
+ _grids_xy_origin_x.set_digits(3);
+ _grids_xy_origin_y.set_digits(3);
+ _grids_xy.add_line( false, _("Origin X:"), _grids_xy_origin_x, "", _("X coordinate of grid origin"), false);
+ _grids_xy.add_line( false, _("Origin Y:"), _grids_xy_origin_y, "", _("Y coordinate of grid origin"), false);
+ _grids_xy_spacing_x.init("/options/grids/xy/spacing_x", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false);
+ _grids_xy_spacing_y.init("/options/grids/xy/spacing_y", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false);
+ _grids_xy_spacing_x.set_digits(3);
+ _grids_xy_spacing_y.set_digits(3);
+ _grids_xy.add_line( false, _("Spacing X:"), _grids_xy_spacing_x, "", _("Distance between vertical grid lines"), false);
+ _grids_xy.add_line( false, _("Spacing Y:"), _grids_xy_spacing_y, "", _("Distance between horizontal grid lines"), false);
+
+ _grids_xy_color.init(_("Minor grid line color:"), "/options/grids/xy/color", GRID_DEFAULT_MINOR_COLOR);
+ _grids_xy.add_line( false, _("Minor grid line color:"), _grids_xy_color, "", _("Color used for normal grid lines"), false);
+ _grids_xy_empcolor.init(_("Major grid line color:"), "/options/grids/xy/empcolor", GRID_DEFAULT_MAJOR_COLOR);
+ _grids_xy.add_line( false, _("Major grid line color:"), _grids_xy_empcolor, "", _("Color used for major (highlighted) grid lines"), false);
+ _grids_xy_empspacing.init("/options/grids/xy/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false);
+ _grids_xy.add_line( false, _("Major grid line every:"), _grids_xy_empspacing, "", "", false);
+ _grids_xy_dotted.init( _("Show dots instead of lines"), "/options/grids/xy/dotted", false);
+ _grids_xy.add_line( false, "", _grids_xy_dotted, "", _("If set, display dots at gridpoints instead of gridlines"), false);
+
+ // Axonometric SPGrid properties:
+ _grids_axonom_units.init("/options/grids/axonom/units");
+ _grids_axonom.add_line( false, _("Grid units:"), _grids_axonom_units, "", "", false);
+ _grids_axonom_origin_x.init("/options/grids/axonom/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false);
+ _grids_axonom_origin_y.init("/options/grids/axonom/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false);
+ _grids_axonom_origin_x.set_digits(3);
+ _grids_axonom_origin_y.set_digits(3);
+ _grids_axonom.add_line( false, _("Origin X:"), _grids_axonom_origin_x, "", _("X coordinate of grid origin"), false);
+ _grids_axonom.add_line( false, _("Origin Y:"), _grids_axonom_origin_y, "", _("Y coordinate of grid origin"), false);
+ _grids_axonom_spacing_y.init("/options/grids/axonom/spacing_y", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false);
+ _grids_axonom_spacing_y.set_digits(3);
+ _grids_axonom.add_line( false, _("Spacing Y:"), _grids_axonom_spacing_y, "", _("Base length of z-axis"), false);
+ _grids_axonom_angle_x.init("/options/grids/axonom/angle_x", -360.0, 360.0, 1.0, 10.0, 30.0, false, false);
+ _grids_axonom_angle_z.init("/options/grids/axonom/angle_z", -360.0, 360.0, 1.0, 10.0, 30.0, false, false);
+ _grids_axonom.add_line( false, _("Angle X:"), _grids_axonom_angle_x, "", _("Angle of x-axis"), false);
+ _grids_axonom.add_line( false, _("Angle Z:"), _grids_axonom_angle_z, "", _("Angle of z-axis"), false);
+ _grids_axonom_color.init(_("Minor grid line color:"), "/options/grids/axonom/color", GRID_DEFAULT_MINOR_COLOR);
+ _grids_axonom.add_line( false, _("Minor grid line color:"), _grids_axonom_color, "", _("Color used for normal grid lines"), false);
+ _grids_axonom_empcolor.init(_("Major grid line color:"), "/options/grids/axonom/empcolor", GRID_DEFAULT_MAJOR_COLOR);
+ _grids_axonom.add_line( false, _("Major grid line color:"), _grids_axonom_empcolor, "", _("Color used for major (highlighted) grid lines"), false);
+ _grids_axonom_empspacing.init("/options/grids/axonom/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false);
+ _grids_axonom.add_line( false, _("Major grid line every:"), _grids_axonom_empspacing, "", "", false);
+
+ this->AddPage(_page_grids, _("Grids"), iter_ui, PREFS_PAGE_UI_GRIDS);
+
+ // Command Palette
+ _page_command_palette.add_group_header(_("Display Options"));
+
+ _cp_show_full_action_name.init(_("Show command line argument names"), "/options/commandpalette/showfullactionname/value", false);
+ _page_command_palette.add_line(true, "", _cp_show_full_action_name, "", _("Show action argument names in the command palette suggestions, most useful for using them on the command line"));
+
+ _cp_show_untranslated_name.init(_("Show untranslated (English) names"), "/options/commandpalette/showuntranslatedname/value", true);
+ _page_command_palette.add_line(true, "", _cp_show_untranslated_name, "", _("Also show the English names of the command"));
+
+ this->AddPage(_page_command_palette, _("Command Palette"), iter_ui, PREFS_PAGE_COMMAND_PALETTE);
+ // /Command Palette
+
+
+ initKeyboardShortcuts(iter_ui);
+}
+
+static void profileComboChanged( Gtk::ComboBoxText* combo )
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int rowNum = combo->get_active_row_number();
+ if ( rowNum < 1 ) {
+ prefs->setString("/options/displayprofile/uri", "");
+ } else {
+ Glib::ustring active = combo->get_active_text();
+
+ Glib::ustring path = CMSSystem::getPathForProfile(active);
+ if ( !path.empty() ) {
+ prefs->setString("/options/displayprofile/uri", path);
+ }
+ }
+}
+
+static void proofComboChanged( Gtk::ComboBoxText* combo )
+{
+ Glib::ustring active = combo->get_active_text();
+ Glib::ustring path = CMSSystem::getPathForProfile(active);
+
+ if ( !path.empty() ) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/options/softproof/uri", path);
+ }
+}
+
+static void gamutColorChanged( Gtk::ColorButton* btn ) {
+ auto rgba = btn->get_rgba();
+ auto r = rgba.get_red_u();
+ auto g = rgba.get_green_u();
+ auto b = rgba.get_blue_u();
+
+ gchar* tmp = g_strdup_printf("#%02x%02x%02x", (r >> 8), (g >> 8), (b >> 8) );
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setString("/options/softproof/gamutcolor", tmp);
+ g_free(tmp);
+}
+
+void InkscapePreferences::initPageIO()
+{
+ Gtk::TreeModel::iterator iter_io = this->AddPage(_page_io, _("Input/Output"), PREFS_PAGE_IO);
+
+ _save_use_current_dir.init( _("Use current directory for \"Save As ...\""), "/dialogs/save_as/use_current_dir", true);
+ _page_io.add_line( false, "", _save_use_current_dir, "",
+ _("When this option is on, the \"Save as...\" and \"Save a Copy...\" dialogs will always open in the directory where the currently open document is; when it's off, each will open in the directory where you last saved a file using it"), true);
+
+ _misc_default_metadata.init( _("Add default metadata to new documents"), "/metadata/addToNewFile", false);
+ _page_io.add_line( false, "", _misc_default_metadata, "",
+ _("Add default metadata to new documents. Default metadata can be set from Document Properties->Metadata."), true);
+
+ _export_all_extensions.init( _("Show all outputs in Export Dialog"), "/dialogs/export/show_all_extensions", false);
+ _page_io.add_line( false, "", _export_all_extensions, "",
+ _("Will list all possible output extensions in the Export Dialog selection."), true);
+
+ // Input devices options
+ _mouse_sens.init ( "/options/cursortolerance/value", 0.0, 30.0, 1.0, 1.0, 8.0, true, false);
+ _page_mouse.add_line( false, _("_Grab sensitivity:"), _mouse_sens, _("pixels"),
+ _("How close on the screen you need to be to an object to be able to grab it with mouse (in screen pixels)"), false, reset_icon());
+ _mouse_thres.init ( "/options/dragtolerance/value", 0.0, 100.0, 1.0, 1.0, 8.0, true, false);
+ _page_mouse.add_line( false, _("_Click/drag threshold:"), _mouse_thres, _("pixels"),
+ _("Maximum mouse drag (in screen pixels) which is considered a click, not a drag"), false);
+
+ _mouse_use_ext_input.init( _("Use pressure-sensitive tablet"), "/options/useextinput/value", true);
+ _page_mouse.add_line(false, "",_mouse_use_ext_input, "",
+ _("Use the capabilities of a tablet or other pressure-sensitive device. Disable this only if you have problems with the tablet (you can still use it as a mouse)"), false, reset_icon());
+
+ _mouse_switch_on_ext_input.init( _("Switch tool based on tablet device"), "/options/switchonextinput/value", false);
+ _page_mouse.add_line(false, "",_mouse_switch_on_ext_input, "",
+ _("Change tool as different devices are used on the tablet (pen, eraser, mouse)"), false, reset_icon());
+ this->AddPage(_page_mouse, _("Input devices"), iter_io, PREFS_PAGE_IO_MOUSE);
+
+ // SVG output options
+ _svgoutput_usenamedcolors.init( _("Use named colors"), "/options/svgoutput/usenamedcolors", false);
+ _page_svgoutput.add_line( false, "", _svgoutput_usenamedcolors, "", _("If set, write the CSS name of the color when available (e.g. 'red' or 'magenta') instead of the numeric value"), false);
+
+ _page_svgoutput.add_group_header( _("XML formatting"));
+
+ _svgoutput_inlineattrs.init( _("Inline attributes"), "/options/svgoutput/inlineattrs", false);
+ _page_svgoutput.add_line( true, "", _svgoutput_inlineattrs, "", _("Put attributes on the same line as the element tag"), false);
+
+ _svgoutput_indent.init("/options/svgoutput/indent", 0.0, 1000.0, 1.0, 2.0, 2.0, true, false);
+ _page_svgoutput.add_line( true, _("_Indent, spaces:"), _svgoutput_indent, "", _("The number of spaces to use for indenting nested elements; set to 0 for no indentation"), false);
+
+ _page_svgoutput.add_group_header( _("Path data"));
+
+ int const numPathstringFormat = 3;
+ Glib::ustring pathstringFormatLabels[numPathstringFormat] = {_("Absolute"), _("Relative"), _("Optimized")};
+ int pathstringFormatValues[numPathstringFormat] = {0, 1, 2};
+
+ _svgoutput_pathformat.init("/options/svgoutput/pathstring_format", pathstringFormatLabels, pathstringFormatValues, numPathstringFormat, 2);
+ _page_svgoutput.add_line( true, _("Path string format:"), _svgoutput_pathformat, "", _("Path data should be written: only with absolute coordinates, only with relative coordinates, or optimized for string length (mixed absolute and relative coordinates)"), false);
+
+ _svgoutput_forcerepeatcommands.init( _("Force repeat commands"), "/options/svgoutput/forcerepeatcommands", false);
+ _page_svgoutput.add_line( true, "", _svgoutput_forcerepeatcommands, "", _("Force repeating of the same path command (for example, 'L 1,2 L 3,4' instead of 'L 1,2 3,4')"), false);
+
+ _page_svgoutput.add_group_header( _("Numbers"));
+
+ _svgoutput_numericprecision.init("/options/svgoutput/numericprecision", 1.0, 16.0, 1.0, 2.0, 8.0, true, false);
+ _page_svgoutput.add_line( true, _("_Numeric precision:"), _svgoutput_numericprecision, "", _("Significant figures of the values written to the SVG file"), false);
+
+ _svgoutput_minimumexponent.init("/options/svgoutput/minimumexponent", -32.0, -1, 1.0, 2.0, -8.0, true, false);
+ _page_svgoutput.add_line( true, _("Minimum _exponent:"), _svgoutput_minimumexponent, "", _("The smallest number written to SVG is 10 to the power of this exponent; anything smaller is written as zero"), false);
+
+ /* Code to add controls for attribute checking options */
+
+ /* Add incorrect style properties options */
+ _page_svgoutput.add_group_header( _("Improper Attributes Actions"));
+
+ _svgoutput_attrwarn.init( _("Print warnings"), "/options/svgoutput/incorrect_attributes_warn", true);
+ _page_svgoutput.add_line( true, "", _svgoutput_attrwarn, "", _("Print warning if invalid or non-useful attributes found. Database files located in inkscape_data_dir/attributes."), false);
+ _svgoutput_attrremove.init( _("Remove attributes"), "/options/svgoutput/incorrect_attributes_remove", false);
+ _page_svgoutput.add_line( true, "", _svgoutput_attrremove, "", _("Delete invalid or non-useful attributes from element tag"), false);
+
+ /* Add incorrect style properties options */
+ _page_svgoutput.add_group_header( _("Inappropriate Style Properties Actions"));
+
+ _svgoutput_stylepropwarn.init( _("Print warnings"), "/options/svgoutput/incorrect_style_properties_warn", true);
+ _page_svgoutput.add_line( true, "", _svgoutput_stylepropwarn, "", _("Print warning if inappropriate style properties found (i.e. 'font-family' set on a <rect>). Database files located in inkscape_data_dir/attributes."), false);
+ _svgoutput_stylepropremove.init( _("Remove style properties"), "/options/svgoutput/incorrect_style_properties_remove", false);
+ _page_svgoutput.add_line( true, "", _svgoutput_stylepropremove, "", _("Delete inappropriate style properties"), false);
+
+ /* Add default or inherited style properties options */
+ _page_svgoutput.add_group_header( _("Non-useful Style Properties Actions"));
+
+ _svgoutput_styledefaultswarn.init( _("Print warnings"), "/options/svgoutput/style_defaults_warn", true);
+ _page_svgoutput.add_line( true, "", _svgoutput_styledefaultswarn, "", _("Print warning if redundant style properties found (i.e. if a property has the default value and a different value is not inherited or if value is the same as would be inherited). Database files located in inkscape_data_dir/attributes."), false);
+ _svgoutput_styledefaultsremove.init( _("Remove style properties"), "/options/svgoutput/style_defaults_remove", false);
+ _page_svgoutput.add_line( true, "", _svgoutput_styledefaultsremove, "", _("Delete redundant style properties"), false);
+
+ _page_svgoutput.add_group_header( _("Check Attributes and Style Properties on"));
+
+ _svgoutput_check_reading.init( _("Reading"), "/options/svgoutput/check_on_reading", false);
+ _page_svgoutput.add_line( true, "", _svgoutput_check_reading, "", _("Check attributes and style properties on reading in SVG files (including those internal to Inkscape which will slow down startup)"), false);
+ _svgoutput_check_editing.init( _("Editing"), "/options/svgoutput/check_on_editing", false);
+ _page_svgoutput.add_line( true, "", _svgoutput_check_editing, "", _("Check attributes and style properties while editing SVG files (may slow down Inkscape, mostly useful for debugging)"), false);
+ _svgoutput_check_writing.init( _("Writing"), "/options/svgoutput/check_on_writing", true);
+ _page_svgoutput.add_line( true, "", _svgoutput_check_writing, "", _("Check attributes and style properties on writing out SVG files"), false);
+
+ this->AddPage(_page_svgoutput, _("SVG output"), iter_io, PREFS_PAGE_IO_SVGOUTPUT);
+
+ // SVG Export Options ==========================================
+
+ // SVG 2 Fallbacks
+ _page_svgexport.add_group_header( _("SVG 2"));
+ _svgexport_insert_text_fallback.init( _("Insert SVG 1.1 fallback in text"), "/options/svgexport/text_insertfallback", true );
+ _svgexport_insert_mesh_polyfill.init( _("Insert JavaScript code for mesh gradients"), "/options/svgexport/mesh_insertpolyfill", true );
+ _svgexport_insert_hatch_polyfill.init( _("Insert JavaScript code for SVG2 hatches"), "/options/svgexport/hatch_insertpolyfill", true );
+
+ _page_svgexport.add_line( false, "", _svgexport_insert_text_fallback, "", _("Adds fallback options for non-SVG 2 renderers."), false);
+ _page_svgexport.add_line( false, "", _svgexport_insert_mesh_polyfill, "", _("Adds a JavaScript polyfill for rendering meshes in web browsers."), false);
+ _page_svgexport.add_line( false, "", _svgexport_insert_hatch_polyfill, "", _("Adds a JavaScript polyfill for rendering hatches in web browsers."), false);
+
+ // SVG Export Options (SVG 2 -> SVG 1)
+ _page_svgexport.add_group_header( _("SVG 2 to SVG 1.1"));
+
+ _svgexport_remove_marker_auto_start_reverse.init( _("Use correct marker direction in SVG 1.1 renderers"), "/options/svgexport/marker_autostartreverse", false);
+ _svgexport_remove_marker_context_paint.init( _("Use correct marker colors in SVG 1.1 renderers"), "/options/svgexport/marker_contextpaint", false);
+
+ _page_svgexport.add_line( false, "", _svgexport_remove_marker_auto_start_reverse, "", _("SVG 2 allows markers to automatically be reversed at the start of a path with 'auto_start_reverse'. This adds a rotated duplicate of the marker's definition."), false);
+ _page_svgexport.add_line( false, "", _svgexport_remove_marker_context_paint, "", _("SVG 2 allows markers to automatically match the stroke color by using 'context_paint' or 'context_fill'. This adjusts the markers own colors."), false);
+
+ this->AddPage(_page_svgexport, _("SVG export"), iter_io, PREFS_PAGE_IO_SVGEXPORT);
+
+
+ // CMS options
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int const numIntents = 4;
+ /* TRANSLATORS: see http://www.newsandtech.com/issues/2004/03-04/pt/03-04_rendering.htm */
+ Glib::ustring intentLabels[numIntents] = {_("Perceptual"), _("Relative Colorimetric"), _("Saturation"), _("Absolute Colorimetric")};
+ int intentValues[numIntents] = {0, 1, 2, 3};
+
+ _page_cms.add_group_header( _("Display adjustment"));
+
+ Glib::ustring tmpStr;
+ for (auto &profile: ColorProfile::getBaseProfileDirs()) {
+ gchar* part = g_strdup_printf( "\n%s", profile.filename.c_str() );
+ tmpStr += part;
+ g_free(part);
+ }
+
+ gchar* profileTip = g_strdup_printf(_("The ICC profile to use to calibrate display output.\nSearched directories:%s"), tmpStr.c_str());
+ _page_cms.add_line( true, _("Display profile:"), _cms_display_profile, "",
+ profileTip, false);
+ g_free(profileTip);
+ profileTip = nullptr;
+
+ _cms_from_display.init( _("Retrieve profile from display"), "/options/displayprofile/from_display", false);
+ _page_cms.add_line( true, "", _cms_from_display, "",
+#ifdef GDK_WINDOWING_X11
+ _("Retrieve profiles from those attached to displays via XICC"), false);
+#else
+ _("Retrieve profiles from those attached to displays"), false);
+#endif // GDK_WINDOWING_X11
+
+
+ _cms_intent.init("/options/displayprofile/intent", intentLabels, intentValues, numIntents, 0);
+ _page_cms.add_line( true, _("Display rendering intent:"), _cms_intent, "",
+ _("The rendering intent to use to calibrate display output"), false);
+
+ _page_cms.add_group_header( _("Proofing"));
+
+ _cms_softproof.init( _("Simulate output on screen"), "/options/softproof/enable", false);
+ _page_cms.add_line( true, "", _cms_softproof, "",
+ _("Simulates output of target device"), false);
+
+ _cms_gamutwarn.init( _("Mark out of gamut colors"), "/options/softproof/gamutwarn", false);
+ _page_cms.add_line( true, "", _cms_gamutwarn, "",
+ _("Highlights colors that are out of gamut for the target device"), false);
+
+ Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor");
+
+ Gdk::RGBA tmpColor( colorStr.empty() ? "#00ff00" : colorStr);
+ _cms_gamutcolor.set_rgba( tmpColor );
+
+ _page_cms.add_line( true, _("Out of gamut warning color:"), _cms_gamutcolor, "",
+ _("Selects the color used for out of gamut warning"), false);
+
+ _page_cms.add_line( true, _("Device profile:"), _cms_proof_profile, "",
+ _("The ICC profile to use to simulate device output"), false);
+
+ _cms_proof_intent.init("/options/softproof/intent", intentLabels, intentValues, numIntents, 0);
+ _page_cms.add_line( true, _("Device rendering intent:"), _cms_proof_intent, "",
+ _("The rendering intent to use to calibrate device output"), false);
+
+ _cms_proof_blackpoint.init( _("Black point compensation"), "/options/softproof/bpc", false);
+ _page_cms.add_line( true, "", _cms_proof_blackpoint, "",
+ _("Enables black point compensation"), false);
+
+ {
+ std::vector<Glib::ustring> names = ::Inkscape::CMSSystem::getDisplayNames();
+ Glib::ustring current = prefs->getString( "/options/displayprofile/uri" );
+
+ gint index = 0;
+ _cms_display_profile.append(_("<none>"));
+ index++;
+ for (auto & name : names) {
+ _cms_display_profile.append( name );
+ Glib::ustring path = CMSSystem::getPathForProfile(name);
+ if ( !path.empty() && path == current ) {
+ _cms_display_profile.set_active(index);
+ }
+ index++;
+ }
+ if ( current.empty() ) {
+ _cms_display_profile.set_active(0);
+ }
+
+ names = ::Inkscape::CMSSystem::getSoftproofNames();
+ current = prefs->getString("/options/softproof/uri");
+ index = 0;
+ for (auto & name : names) {
+ _cms_proof_profile.append( name );
+ Glib::ustring path = CMSSystem::getPathForProfile(name);
+ if ( !path.empty() && path == current ) {
+ _cms_proof_profile.set_active(index);
+ }
+ index++;
+ }
+ }
+
+ _cms_gamutcolor.signal_color_set().connect( sigc::bind( sigc::ptr_fun(gamutColorChanged), &_cms_gamutcolor) );
+
+ _cms_display_profile.signal_changed().connect( sigc::bind( sigc::ptr_fun(profileComboChanged), &_cms_display_profile) );
+ _cms_proof_profile.signal_changed().connect( sigc::bind( sigc::ptr_fun(proofComboChanged), &_cms_proof_profile) );
+
+ this->AddPage(_page_cms, _("Color management"), iter_io, PREFS_PAGE_IO_CMS);
+
+ // Autosave options
+ _save_autosave_enable.init( _("Enable autosave"), "/options/autosave/enable", true);
+ _page_autosave.add_line(false, "", _save_autosave_enable, "", _("Automatically save the current document(s) at a given interval, thus minimizing loss in case of a crash"), false);
+ _save_autosave_path.init("/options/autosave/path", true);
+ if (prefs->getString("/options/autosave/path").empty()) {
+ // Show the default fallback "tmp dir" if autosave path is not set.
+ _save_autosave_path.set_text(Glib::build_filename(Glib::get_user_cache_dir(), "inkscape"));
+ }
+ _page_autosave.add_line(false, C_("Filesystem", "Autosave _directory:"), _save_autosave_path, "", _("The directory where autosaves will be written. This should be an absolute path (starts with / on UNIX or a drive letter such as C: on Windows)."), false);
+ _save_autosave_interval.init("/options/autosave/interval", 1.0, 10800.0, 1.0, 10.0, 10.0, true, false);
+ _page_autosave.add_line(false, _("_Interval (in minutes):"), _save_autosave_interval, "", _("Interval (in minutes) at which document will be autosaved"), false);
+ _save_autosave_max.init("/options/autosave/max", 1.0, 10000.0, 1.0, 10.0, 10.0, true, false);
+ _page_autosave.add_line(false, _("_Maximum number of autosaves:"), _save_autosave_max, "", _("Maximum number of autosaved files; use this to limit the storage space used"), false);
+
+ // When changing the interval or enabling/disabling the autosave function,
+ // update our running configuration
+ _save_autosave_enable.changed_signal.connect([](bool) { Inkscape::AutoSave::restart(); });
+ _save_autosave_interval.changed_signal.connect([](double) { Inkscape::AutoSave::restart(); });
+
+ this->AddPage(_page_autosave, _("Autosave"), iter_io, PREFS_PAGE_IO_AUTOSAVE);
+
+ // No Result
+ _page_notfound.add_group_header(_("No matches were found, try another search!"));
+}
+
+void InkscapePreferences::initPageBehavior()
+{
+ Gtk::TreeModel::iterator iter_behavior = this->AddPage(_page_behavior, _("Behavior"), PREFS_PAGE_BEHAVIOR);
+
+ _misc_simpl.init("/options/simplifythreshold/value", 0.0001, 1.0, 0.0001, 0.0010, 0.0010, false, false);
+ _page_behavior.add_line( false, _("_Simplification threshold:"), _misc_simpl, "",
+ _("How strong is the Node tool's Simplify command by default. If you invoke this command several times in quick succession, it will act more and more aggressively; invoking it again after a pause restores the default threshold."), false);
+
+ _markers_color_stock.init ( _("Color stock markers the same color as object"), "/options/markers/colorStockMarkers", true);
+ _markers_color_custom.init ( _("Color custom markers the same color as object"), "/options/markers/colorCustomMarkers", false);
+ _markers_color_update.init ( _("Update marker color when object color changes"), "/options/markers/colorUpdateMarkers", true);
+
+ // Selecting options
+ _sel_all.init ( _("Select in all layers"), "/options/kbselection/inlayer", PREFS_SELECTION_ALL, false, nullptr);
+ _sel_current.init ( _("Select only within current layer"), "/options/kbselection/inlayer", PREFS_SELECTION_LAYER, true, &_sel_all);
+ _sel_recursive.init ( _("Select in current layer and sublayers"), "/options/kbselection/inlayer", PREFS_SELECTION_LAYER_RECURSIVE, false, &_sel_all);
+ _sel_hidden.init ( _("Ignore hidden objects and layers"), "/options/kbselection/onlyvisible", true);
+ _sel_locked.init ( _("Ignore locked objects and layers"), "/options/kbselection/onlysensitive", true);
+ _sel_inlayer_same.init ( _("Select same behaves like select all"), "/options/selection/samelikeall", false);
+
+ _sel_layer_deselects.init ( _("Deselect upon layer change"), "/options/selection/layerdeselect", true);
+
+ _sel_touch_topmost_only.init ( _("Select the topmost items only when in touch selection mode"), "/options/selection/touchsel_topmost_only", true);
+ _sel_zero_opacity.init(_("Select transparent objects, strokes, and fills"), "/options/selection/zeroopacity", false);
+
+ _page_select.add_line( false, "", _sel_layer_deselects, "",
+ _("Uncheck this to be able to keep the current objects selected when the current layer changes"));
+ _page_select.add_line(
+ false, "", _sel_zero_opacity, "",
+ _("Check to make objects, strokes, and fills which are completely transparent selectable even if not in outline mode"));
+
+ _page_select.add_line( false, "", _sel_touch_topmost_only, "",
+ _("In touch selection mode, if multiple items overlap at a point, select only the topmost item"));
+
+ _page_select.add_group_header( _("Ctrl+A, Tab, Shift+Tab"));
+ _page_select.add_line( true, "", _sel_all, "",
+ _("Make keyboard selection commands work on objects in all layers"));
+ _page_select.add_line( true, "", _sel_current, "",
+ _("Make keyboard selection commands work on objects in current layer only"));
+ _page_select.add_line( true, "", _sel_recursive, "",
+ _("Make keyboard selection commands work on objects in current layer and all its sublayers"));
+ _page_select.add_line( true, "", _sel_hidden, "",
+ _("Uncheck this to be able to select objects that are hidden (either by themselves or by being in a hidden layer)"));
+ _page_select.add_line( true, "", _sel_locked, "",
+ _("Uncheck this to be able to select objects that are locked (either by themselves or by being in a locked layer)"));
+ _page_select.add_line( true, "", _sel_inlayer_same, "",
+ _("Check this to make the 'select same' functions work like the select all functions, restricting to current layer only."));
+
+ _sel_cycle.init ( _("Wrap when cycling objects in z-order"), "/options/selection/cycleWrap", true);
+
+ _page_select.add_group_header( _("Alt+Scroll Wheel"));
+ _page_select.add_line( true, "", _sel_cycle, "",
+ _("Wrap around at start and end when cycling objects in z-order"));
+
+ auto paste_above_selected = Gtk::manage(new UI::Widget::PrefCheckButton());
+ paste_above_selected->init(_("Paste above selection instead of layer-top"), "/options/paste/aboveselected", true);
+ _page_select.add_line(false, "", *paste_above_selected, "",
+ _("If checked, pasted items and imported documents will be placed immediately above the "
+ "current selection (z-order). Otherwise, insertion happens on top of all objects in the current layer."));
+
+ this->AddPage(_page_select, _("Selecting"), iter_behavior, PREFS_PAGE_BEHAVIOR_SELECTING);
+
+ // Transforms options
+ _trans_scale_stroke.init ( _("Scale stroke width"), "/options/transform/stroke", true);
+ _trans_scale_corner.init ( _("Scale rounded corners in rectangles"), "/options/transform/rectcorners", false);
+ _trans_gradient.init ( _("Transform gradients"), "/options/transform/gradient", true);
+ _trans_pattern.init ( _("Transform patterns"), "/options/transform/pattern", false);
+ _trans_dash_scale.init(_("Scale dashes with stroke"), "/options/dash/scale", true);
+ _trans_optimized.init ( _("Optimized"), "/options/preservetransform/value", 0, true, nullptr);
+ _trans_preserved.init ( _("Preserved"), "/options/preservetransform/value", 1, false, &_trans_optimized);
+
+ _page_transforms.add_line( false, "", _trans_scale_stroke, "",
+ _("When scaling objects, scale the stroke width by the same proportion"));
+ _page_transforms.add_line( false, "", _trans_scale_corner, "",
+ _("When scaling rectangles, scale the radii of rounded corners"));
+ _page_transforms.add_line( false, "", _trans_gradient, "",
+ _("Move gradients (in fill or stroke) along with the objects"));
+ _page_transforms.add_line( false, "", _trans_pattern, "",
+ _("Move patterns (in fill or stroke) along with the objects"));
+ _page_transforms.add_line(false, "", _trans_dash_scale, "", _("When changing stroke width, scale dash array"));
+ _page_transforms.add_group_header( _("Store transformation"));
+ _page_transforms.add_line( true, "", _trans_optimized, "",
+ _("If possible, apply transformation to objects without adding a transform= attribute"));
+ _page_transforms.add_line( true, "", _trans_preserved, "",
+ _("Always store transformation as a transform= attribute on objects"));
+
+ this->AddPage(_page_transforms, _("Transforms"), iter_behavior, PREFS_PAGE_BEHAVIOR_TRANSFORMS);
+
+ // Scrolling options
+ _scroll_wheel.init ( "/options/wheelscroll/value", 0.0, 1000.0, 1.0, 1.0, 40.0, true, false);
+ _page_scrolling.add_line( false, _("Mouse _wheel scrolls by:"), _scroll_wheel, _("pixels"),
+ _("One mouse wheel notch scrolls by this distance in screen pixels (horizontally with Shift)"), false);
+ _page_scrolling.add_group_header( _("Ctrl+arrows"));
+ _scroll_arrow_px.init ( "/options/keyscroll/value", 0.0, 1000.0, 1.0, 1.0, 10.0, true, false);
+ _page_scrolling.add_line( true, _("Sc_roll by:"), _scroll_arrow_px, _("pixels"),
+ _("Pressing Ctrl+arrow key scrolls by this distance (in screen pixels)"), false);
+ _scroll_arrow_acc.init ( "/options/scrollingacceleration/value", 0.0, 5.0, 0.01, 1.0, 0.35, false, false);
+ _page_scrolling.add_line( true, _("_Acceleration:"), _scroll_arrow_acc, "",
+ _("Pressing and holding Ctrl+arrow will gradually speed up scrolling (0 for no acceleration)"), false);
+ _page_scrolling.add_group_header( _("Autoscrolling"));
+ _scroll_auto_speed.init ( "/options/autoscrollspeed/value", 0.0, 5.0, 0.01, 1.0, 0.7, false, false);
+ _page_scrolling.add_line( true, _("_Speed:"), _scroll_auto_speed, "",
+ _("How fast the canvas autoscrolls when you drag beyond canvas edge (0 to turn autoscroll off)"), false);
+ _scroll_auto_thres.init ( "/options/autoscrolldistance/value", -600.0, 600.0, 1.0, 1.0, -10.0, true, false);
+ _page_scrolling.add_line( true, _("_Threshold:"), _scroll_auto_thres, _("pixels"),
+ _("How far (in screen pixels) you need to be from the canvas edge to trigger autoscroll; positive is outside the canvas, negative is within the canvas"), false);
+ _scroll_space.init ( _("Mouse move pans when Space is pressed"), "/options/spacebarpans/value", true);
+ _page_scrolling.add_line( true, "", _scroll_space, "",
+ _("When on, pressing and holding Space and dragging pans canvas"));
+ this->AddPage(_page_scrolling, _("Scrolling"), iter_behavior, PREFS_PAGE_BEHAVIOR_SCROLLING);
+
+ // Snapping options
+ _page_snapping.add_group_header( _("Snap indicator"));
+
+ _snap_indicator.init( _("Enable snap indicator"), "/options/snapindicator/value", true);
+ _page_snapping.add_line( true, "", _snap_indicator, "",
+ _("After snapping, a symbol is drawn at the point that has snapped"));
+
+ _snap_indicator.changed_signal.connect( sigc::mem_fun(_snap_persistence, &Gtk::Widget::set_sensitive) );
+
+ _snap_persistence.init("/options/snapindicatorpersistence/value", 0.1, 10, 0.1, 1, 2, 1);
+ _page_snapping.add_line( true, _("Snap indicator persistence (in seconds):"), _snap_persistence, "",
+ _("Controls how long the snap indicator message will be shown, before it disappears"), true);
+
+ _snap_indicator_distance.init( _("Show snap distance in case of alignment or distribution snap"), "/options/snapindicatordistance/value", false);
+ _page_snapping.add_line( true, "", _snap_indicator_distance, "",
+ _("Show snap distance in case of alignment or distribution snap"));
+
+ _page_snapping.add_group_header( _("What should snap"));
+
+ _snap_closest_only.init( _("Only snap the node closest to the pointer"), "/options/snapclosestonly/value", false);
+ _page_snapping.add_line( true, "", _snap_closest_only, "",
+ _("Only try to snap the node that is initially closest to the mouse pointer"));
+
+ _snap_weight.init("/options/snapweight/value", 0, 1, 0.1, 0.2, 0.5, 1);
+ _page_snapping.add_line( true, _("_Weight factor:"), _snap_weight, "",
+ _("When multiple snap solutions are found, then Inkscape can either prefer the closest transformation (when set to 0), or prefer the node that was initially the closest to the pointer (when set to 1)"), true);
+
+ _snap_mouse_pointer.init( _("Snap the mouse pointer when dragging a constrained knot"), "/options/snapmousepointer/value", false);
+ _page_snapping.add_line( true, "", _snap_mouse_pointer, "",
+ _("When dragging a knot along a constraint line, then snap the position of the mouse pointer instead of snapping the projection of the knot onto the constraint line"));
+
+ _page_snapping.add_group_header( _("Delayed snap"));
+
+ _snap_delay.init("/options/snapdelay/value", 0, 1, 0.1, 0.2, 0, 1);
+ _page_snapping.add_line( true, _("Delay (in seconds):"), _snap_delay, "",
+ _("Postpone snapping as long as the mouse is moving, and then wait an additional fraction of a second. This additional delay is specified here. When set to zero or to a very small number, snapping will be immediate."), true);
+
+
+ this->AddPage(_page_snapping, _("Snapping"), iter_behavior, PREFS_PAGE_BEHAVIOR_SNAPPING);
+
+ // Steps options
+ _steps_arrow.init ( "/options/nudgedistance/value", 0.0, 1000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px");
+ //nudgedistance is limited to 1000 in select-context.cpp: use the same limit here
+ _page_steps.add_line( false, _("_Arrow keys move by:"), _steps_arrow, "",
+ _("Pressing an arrow key moves selected object(s) or node(s) by this distance"), false);
+ _steps_scale.init ( "/options/defaultscale/value", 0.0, 1000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px");
+ //defaultscale is limited to 1000 in select-context.cpp: use the same limit here
+ _page_steps.add_line( false, _("&gt; and &lt; _scale by:"), _steps_scale, "",
+ _("Pressing > or < scales selection up or down by this increment"), false);
+ _steps_inset.init ( "/options/defaultoffsetwidth/value", 0.0, 3000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px");
+ _page_steps.add_line( false, _("_Inset/Outset by:"), _steps_inset, "",
+ _("Inset and Outset commands displace the path by this distance"), false);
+ _steps_compass.init ( _("Compass-like display of angles"), "/options/compassangledisplay/value", true);
+ _page_steps.add_line( false, "", _steps_compass, "",
+ _("When on, angles are displayed with 0 at north, 0 to 360 range, positive clockwise; otherwise with 0 at east, -180 to 180 range, positive counterclockwise"));
+ int const num_items = 18;
+ Glib::ustring labels[num_items] = {"90", "60", "45", "36", "30", "22.5", "18", "15", "12", "10", "7.5", "6", "5", "3", "2", "1", "0.5", C_("Rotation angle", "None")};
+ int values[num_items] = {2, 3, 4, 5, 6, 8, 10, 12, 15, 18, 24, 30, 36, 60, 90, 180, 360, 0};
+ _steps_rot_snap.set_size_request(_sb_width);
+ _steps_rot_snap.init("/options/rotationsnapsperpi/value", labels, values, num_items, 12);
+ _page_steps.add_line( false, _("_Rotation snaps every:"), _steps_rot_snap, _("degrees"),
+ _("Rotating with Ctrl pressed snaps every that much degrees; also, pressing [ or ] rotates by this amount"), false);
+ _steps_rot_relative.init ( _("Relative snapping of guideline angles"), "/options/relativeguiderotationsnap/value", false);
+ _page_steps.add_line( false, "", _steps_rot_relative, "",
+ _("When on, the snap angles when rotating a guideline will be relative to the original angle"));
+ _steps_zoom.init ( "/options/zoomincrement/value", 101.0, 500.0, 1.0, 1.0, M_SQRT2, true, true);
+ _page_steps.add_line( false, _("_Zoom in/out by:"), _steps_zoom, _("%"),
+ _("Zoom tool click, +/- keys, and middle click zoom in and out by this multiplier"), false);
+ _middle_mouse_zoom.init ( _("Zoom with middle mouse click"), "/options/middlemousezoom/value", true);
+ _page_steps.add_line( true, "", _middle_mouse_zoom, "",
+ _("When activated, clicking the middle mouse button (usually the mouse wheel) zooms."));
+ _steps_rotate.init ( "/options/rotateincrement/value", 1, 90, 1.0, 5.0, 15, false, false);
+ _page_steps.add_line( false, _("_Rotate canvas by:"), _steps_rotate, _("degrees"),
+ _("Rotate canvas clockwise and counter-clockwise by this amount."), false);
+ this->AddPage(_page_steps, _("Steps"), iter_behavior, PREFS_PAGE_BEHAVIOR_STEPS);
+
+ // Clones options
+ _clone_option_parallel.init ( _("Move in parallel"), "/options/clonecompensation/value",
+ SP_CLONE_COMPENSATION_PARALLEL, true, nullptr);
+ _clone_option_stay.init ( _("Stay unmoved"), "/options/clonecompensation/value",
+ SP_CLONE_COMPENSATION_UNMOVED, false, &_clone_option_parallel);
+ _clone_option_transform.init ( _("Move according to transform"), "/options/clonecompensation/value",
+ SP_CLONE_COMPENSATION_NONE, false, &_clone_option_parallel);
+ _clone_option_unlink.init ( _("Are unlinked"), "/options/cloneorphans/value",
+ SP_CLONE_ORPHANS_UNLINK, true, nullptr);
+ _clone_option_delete.init ( _("Are deleted"), "/options/cloneorphans/value",
+ SP_CLONE_ORPHANS_DELETE, false, &_clone_option_unlink);
+
+ _page_clones.add_group_header( _("Moving original: clones and linked offsets"));
+ _page_clones.add_line(true, "", _clone_option_parallel, "",
+ _("Clones are translated by the same vector as their original"));
+ _page_clones.add_line(true, "", _clone_option_stay, "",
+ _("Clones preserve their positions when their original is moved"));
+ _page_clones.add_line(true, "", _clone_option_transform, "",
+ _("Each clone moves according to the value of its transform= attribute; for example, a rotated clone will move in a different direction than its original"));
+ _page_clones.add_group_header( _("Deleting original: clones"));
+ _page_clones.add_line(true, "", _clone_option_unlink, "",
+ _("Orphaned clones are converted to regular objects"));
+ _page_clones.add_line(true, "", _clone_option_delete, "",
+ _("Orphaned clones are deleted along with their original"));
+
+ _page_clones.add_group_header( _("Duplicating original+clones/linked offset"));
+
+ _clone_relink_on_duplicate.init ( _("Relink duplicated clones"), "/options/relinkclonesonduplicate/value", false);
+ _page_clones.add_line(true, "", _clone_relink_on_duplicate, "",
+ _("When duplicating a selection containing both a clone and its original (possibly in groups), relink the duplicated clone to the duplicated original instead of the old original"));
+
+ _page_clones.add_group_header( _("Unlinking clones"));
+ _clone_to_curves.init ( _("Path operations unlink clones"), "/options/pathoperationsunlink/value", true);
+ _page_clones.add_line(true, "", _clone_to_curves, "",
+ _("The following path operations will unlink clones: Stroke to path, Object to path, Boolean operations, Combine, Break apart"));
+ _clone_ignore_to_curves.init ( _("'Object to Path' only unlinks (keeps LPEs, shapes)"), "/options/clonestocurvesjustunlink/value", true);
+ _page_clones.add_line(true, "", _clone_ignore_to_curves, "",
+ _("'Object to path' only unlinks clones when they are converted to paths, but preserves any LPEs and shapes within the clones."));
+ //TRANSLATORS: Heading for the Inkscape Preferences "Clones" Page
+ this->AddPage(_page_clones, _("Clones"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLONES);
+
+ // Clip paths and masks options
+ _mask_mask_on_top.init ( _("When applying, use the topmost selected object as clippath/mask"), "/options/maskobject/topmost", true);
+ _page_mask.add_line(false, "", _mask_mask_on_top, "",
+ _("Uncheck this to use the bottom selected object as the clipping path or mask"));
+ _mask_mask_on_ungroup.init ( _("When ungrouping, clips/masks are preserved in children"), "/options/maskobject/maskonungroup", true);
+ _page_mask.add_line(false, "", _mask_mask_on_ungroup, "",
+ _("Uncheck this to remove clip/mask on ungroup"));
+ _mask_mask_remove.init ( _("Remove clippath/mask object after applying"), "/options/maskobject/remove", true);
+ _page_mask.add_line(false, "", _mask_mask_remove, "",
+ _("After applying, remove the object used as the clipping path or mask from the drawing"));
+
+ _page_mask.add_group_header( _("Before applying"));
+
+ _mask_grouping_none.init( _("Do not group clipped/masked objects"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE, true, nullptr);
+ _mask_grouping_separate.init( _("Put every clipped/masked object in its own group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_SEPARATE, false, &_mask_grouping_none);
+ _mask_grouping_all.init( _("Put all clipped/masked objects into one group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_ALL, false, &_mask_grouping_none);
+
+ _page_mask.add_line(true, "", _mask_grouping_none, "",
+ _("Apply clippath/mask to every object"));
+
+ _page_mask.add_line(true, "", _mask_grouping_separate, "",
+ _("Apply clippath/mask to groups containing single object"));
+
+ _page_mask.add_line(true, "", _mask_grouping_all, "",
+ _("Apply clippath/mask to group containing all objects"));
+
+ _page_mask.add_group_header( _("After releasing"));
+
+ _mask_ungrouping.init ( _("Ungroup automatically created groups"), "/options/maskobject/ungrouping", true);
+ _page_mask.add_line(true, "", _mask_ungrouping, "",
+ _("Ungroup groups created when setting clip/mask"));
+
+ this->AddPage(_page_mask, _("Clippaths and masks"), iter_behavior, PREFS_PAGE_BEHAVIOR_MASKS);
+
+ // Markers options
+ _page_markers.add_group_header( _("Stroke Style Markers"));
+ _page_markers.add_line( true, "", _markers_color_stock, "",
+ _("Stroke color same as object, fill color either object fill color or marker fill color"));
+ _page_markers.add_line( true, "", _markers_color_custom, "",
+ _("Stroke color same as object, fill color either object fill color or marker fill color"));
+ _page_markers.add_line( true, "", _markers_color_update, "",
+ _("Update marker color when object color changes"));
+
+ this->AddPage(_page_markers, _("Markers"), iter_behavior, PREFS_PAGE_BEHAVIOR_MARKERS);
+
+ // Clipboard options
+ _clipboard_style_computed.init(_("Copy computed style"), "/options/copycomputedstyle/value", 1, true, nullptr);
+ _clipboard_style_verbatim.init(_("Copy class and style attributes verbatim"), "/options/copycomputedstyle/value", 0,
+ false, &_clipboard_style_computed);
+
+ _page_clipboard.add_group_header(_("Copying objects to the clipboard"));
+ _page_clipboard.add_line(true, "", _clipboard_style_computed, "",
+ _("The object's 'style' attribute will be set to the computed style, "
+ "preserving the object's appearance as in previous Inkscape versions"));
+ _page_clipboard.add_line(
+ true, "", _clipboard_style_verbatim, "",
+ _("The object's 'style' and 'class' values will be copied verbatim, and will replace those of the target object when using 'Paste style'"));
+
+ this->AddPage(_page_clipboard, _("Clipboard"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLIPBOARD);
+
+ // Document cleanup options
+ _page_cleanup.add_group_header( _("Document cleanup"));
+ _cleanup_swatches.init ( _("Remove unused swatches when doing a document cleanup"), "/options/cleanupswatches/value", false); // text label
+ _page_cleanup.add_line( true, "", _cleanup_swatches, "",
+ _("Remove unused swatches when doing a document cleanup")); // tooltip
+ this->AddPage(_page_cleanup, _("Cleanup"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLEANUP);
+ _page_lpe.add_group_header( _("General"));
+ _lpe_show_experimental.init ( _("Show experimental effects"), "/dialogs/livepatheffect/showexperimental", false); // text label
+ _page_lpe.add_line( true, "", _lpe_show_experimental, "",
+ _("Show experimental effects")); // tooltip
+ _lpe_show_gallery.init ( _("Show deprecated LPE gallery"), "/dialogs/livepatheffect/showgallery", false); // text label
+ _page_lpe.add_line( true, "", _lpe_show_gallery, "",
+ _("Adds a button to the LPE dialog that opens the old-style LPE selection dialog")); // tooltip
+ _page_lpe.add_group_header( _("Tiling"));
+ _lpe_copy_mirroricons.init ( _("Add advanced tiling options"), "/live_effects/copy/mirroricons", true); // text label
+ _page_lpe.add_line( true, "", _lpe_copy_mirroricons, "",
+ _("Enables using 16 advanced mirror options between the copies (so there can be copies that are mirrored differently between the rows and the columns) for Tiling LPE")); // tooltip
+ this->AddPage(_page_lpe, _("Live Path Effects (LPE)"), iter_behavior, PREFS_PAGE_BEHAVIOR_LPE);
+}
+
+void InkscapePreferences::initPageRendering()
+{
+ // render threads
+ _filter_multi_threaded.init("/options/threading/numthreads", 0.0, 32.0, 1.0, 2.0, 0.0, true, false);
+ _page_rendering.add_line(false, _("Number of _Threads:"), _filter_multi_threaded, "", _("Configure number of threads to use when rendering. The default value of zero means choose automatically."), false);
+
+ // rendering cache
+ _rendering_cache_size.init("/options/renderingcache/size", 0.0, 4096.0, 1.0, 32.0, 64.0, true, false);
+ _page_rendering.add_line( false, _("Rendering _cache size:"), _rendering_cache_size, C_("mebibyte (2^20 bytes) abbreviation","MiB"), _("Set the amount of memory per document which can be used to store rendered parts of the drawing for later reuse; set to zero to disable caching"), false);
+
+ // rendering x-ray radius
+ _rendering_xray_radius.init("/options/rendering/xray-radius", 1.0, 1500.0, 1.0, 100.0, 100.0, true, false);
+ _page_rendering.add_line( false, _("X-ray radius:"), _rendering_xray_radius, "", _("Radius of the circular area around the mouse cursor in X-ray mode"), false);
+
+ // rendering outline overlay opacity
+ _rendering_outline_overlay_opacity.init("/options/rendering/outline-overlay-opacity", 0.0, 100.0, 1.0, 5.0, 50.0, true, false);
+ _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), _("Opacity of the overlay in outline overlay view mode"), false);
+
+ // update strategy
+ {
+ constexpr int values[] = { 1, 2, 3 };
+ Glib::ustring const labels[] = { _("Responsive"), _("Full redraw"), _("Multiscale") };
+ _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3);
+ _page_rendering.add_line(false, _("Update strategy:"), _canvas_update_strategy, "", _("How to update continually changing content when it can't be redrawn fast enough"), false);
+ }
+
+ // opengl
+ _canvas_request_opengl.init(_("Enable OpenGL"), "/options/rendering/request_opengl", false);
+ _page_rendering.add_line(false, "", _canvas_request_opengl, "", _("Request that the canvas should be painted with OpenGL rather than Cairo. If OpenGL is unsupported, it will fall back to Cairo."), false);
+
+ // blur quality
+ _blur_quality_best.init ( _("Best quality (slowest)"), "/options/blurquality/value",
+ BLUR_QUALITY_BEST, false, nullptr);
+ _blur_quality_better.init ( _("Better quality (slower)"), "/options/blurquality/value",
+ BLUR_QUALITY_BETTER, false, &_blur_quality_best);
+ _blur_quality_normal.init ( _("Average quality"), "/options/blurquality/value",
+ BLUR_QUALITY_NORMAL, true, &_blur_quality_best);
+ _blur_quality_worse.init ( _("Lower quality (faster)"), "/options/blurquality/value",
+ BLUR_QUALITY_WORSE, false, &_blur_quality_best);
+ _blur_quality_worst.init ( _("Lowest quality (fastest)"), "/options/blurquality/value",
+ BLUR_QUALITY_WORST, false, &_blur_quality_best);
+
+ _page_rendering.add_group_header( _("Gaussian blur quality for display"));
+ _page_rendering.add_line( true, "", _blur_quality_best, "",
+ _("Best quality, but display may be very slow at high zooms (bitmap export always uses best quality)"));
+ _page_rendering.add_line( true, "", _blur_quality_better, "",
+ _("Better quality, but slower display"));
+ _page_rendering.add_line( true, "", _blur_quality_normal, "",
+ _("Average quality, acceptable display speed"));
+ _page_rendering.add_line( true, "", _blur_quality_worse, "",
+ _("Lower quality (some artifacts), but display is faster"));
+ _page_rendering.add_line( true, "", _blur_quality_worst, "",
+ _("Lowest quality (considerable artifacts), but display is fastest"));
+
+ // filter quality
+ _filter_quality_best.init ( _("Best quality (slowest)"), "/options/filterquality/value",
+ Inkscape::Filters::FILTER_QUALITY_BEST, false, nullptr);
+ _filter_quality_better.init ( _("Better quality (slower)"), "/options/filterquality/value",
+ Inkscape::Filters::FILTER_QUALITY_BETTER, false, &_filter_quality_best);
+ _filter_quality_normal.init ( _("Average quality"), "/options/filterquality/value",
+ Inkscape::Filters::FILTER_QUALITY_NORMAL, true, &_filter_quality_best);
+ _filter_quality_worse.init ( _("Lower quality (faster)"), "/options/filterquality/value",
+ Inkscape::Filters::FILTER_QUALITY_WORSE, false, &_filter_quality_best);
+ _filter_quality_worst.init ( _("Lowest quality (fastest)"), "/options/filterquality/value",
+ Inkscape::Filters::FILTER_QUALITY_WORST, false, &_filter_quality_best);
+
+ _page_rendering.add_group_header( _("Filter effects quality for display"));
+ _page_rendering.add_line( true, "", _filter_quality_best, "",
+ _("Best quality, but display may be very slow at high zooms (bitmap export always uses best quality)"));
+ _page_rendering.add_line( true, "", _filter_quality_better, "",
+ _("Better quality, but slower display"));
+ _page_rendering.add_line( true, "", _filter_quality_normal, "",
+ _("Average quality, acceptable display speed"));
+ _page_rendering.add_line( true, "", _filter_quality_worse, "",
+ _("Lower quality (some artifacts), but display is faster"));
+ _page_rendering.add_line( true, "", _filter_quality_worst, "",
+ _("Lowest quality (considerable artifacts), but display is fastest"));
+
+#ifdef CAIRO_HAS_DITHER
+ _cairo_dithering.init(_("Use dithering"), "/options/dithering/value", true);
+ _page_rendering.add_line(false, "", _cairo_dithering, "", _("Makes gradients smoother. This can significantly impact the size of generated PNG files."));
+#endif
+
+ auto grid = Gtk::make_managed<Gtk::Grid>();
+ grid->set_border_width(12);
+ grid->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ grid->set_column_spacing(12);
+ grid->set_row_spacing(6);
+ auto revealer = Gtk::make_managed<Gtk::Revealer>();
+ revealer->add(*grid);
+ revealer->set_reveal_child(Inkscape::Preferences::get()->getBool("/options/rendering/devmode"));
+ _canvas_developer_mode_enabled.init(_("Enable developer mode"), "/options/rendering/devmode", false);
+ _canvas_developer_mode_enabled.signal_toggled().connect([revealer, this] { revealer->set_reveal_child(_canvas_developer_mode_enabled.get_active()); });
+ _page_rendering.add_group_header(_("Developer mode"));
+ _page_rendering.add_line(true, "", _canvas_developer_mode_enabled, "", _("Enable additional debugging options"), false);
+ _page_rendering.add(*revealer);
+
+ auto add_devmode_line = [&] (Glib::ustring const &label, Gtk::Widget &widget, Glib::ustring const &suffix, Glib::ustring const &tip) {
+ widget.set_tooltip_text(tip);
+
+ auto hb = Gtk::make_managed<Gtk::Box>();
+ hb->set_spacing(12);
+ hb->set_hexpand(true);
+ hb->pack_start(widget, false, false);
+ hb->set_valign(Gtk::ALIGN_CENTER);
+
+ auto label_widget = Gtk::make_managed<Gtk::Label>(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true);
+ label_widget->set_mnemonic_widget(widget);
+ label_widget->set_markup(label_widget->get_text());
+ label_widget->set_margin_start(12);
+
+ label_widget->set_valign(Gtk::ALIGN_CENTER);
+ grid->add(*label_widget);
+ grid->attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1);
+
+ if (!suffix.empty()) {
+ auto suffix_widget = Gtk::make_managed<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);
+ }
+ };
+
+ auto add_devmode_group_header = [&] (Glib::ustring const &name) {
+ auto label_widget = Gtk::make_managed<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);
+ grid->add(*label_widget);
+ };
+
+ //TRANSLATORS: The following are options for fine-tuning rendering, meant to be used by developers,
+ //find more explanations at https://gitlab.com/inkscape/inbox/-/issues/6544#note_886540227
+ add_devmode_group_header(_("Low-level tuning options"));
+ _canvas_tile_size.init("/options/rendering/tile_size", 1.0, 10000.0, 1.0, 0.0, 300.0, true, false);
+ add_devmode_line(_("Tile size"), _canvas_tile_size, "", _("Halve rendering tile rectangles until their largest dimension is this small"));
+ _canvas_render_time_limit.init("/options/rendering/render_time_limit", 1.0, 5000.0, 1.0, 0.0, 80.0, true, false);
+ add_devmode_line(_("Render time limit"), _canvas_render_time_limit, C_("millisecond abbreviation", "ms"), _("The maximum time allowed for a rendering time slice"));
+ _canvas_block_updates.init("", "/options/rendering/block_updates", true);
+ add_devmode_line(_("Use block updates"), _canvas_block_updates, "", _("Update the dragged region as a single block"));
+ {
+ constexpr int values[] = { 1, 2, 3, 4 };
+ Glib::ustring const labels[] = { _("Auto"), _("Persistent"), _("Asynchronous"), _("Synchronous") };
+ _canvas_pixelstreamer_method.init("/options/rendering/pixelstreamer_method", labels, values, 4, 1);
+ add_devmode_line(_("Pixel streaming method"), _canvas_pixelstreamer_method, "", _("Change the method used for streaming pixel data to the GPU. The default is Auto, which picks the best method available at runtime. As for the other options, higher up is better."));
+ }
+ _canvas_padding.init("/options/rendering/padding", 0.0, 1000.0, 1.0, 0.0, 350.0, true, false);
+ add_devmode_line(_("Buffer padding"), _canvas_padding, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount"));
+ _canvas_prerender.init("/options/rendering/prerender", 0.0, 1000.0, 1.0, 0.0, 100.0, true, false);
+ add_devmode_line(_("Prerender margin"), _canvas_prerender, "", _("Pre-render a margin around the visible region."));
+ _canvas_preempt.init("/options/rendering/preempt", 0.0, 1000.0, 1.0, 0.0, 250.0, true, false);
+ add_devmode_line(_("Preempt size"), _canvas_preempt, "", _("Prevent thin tiles at the rendering edge by making them at least this size."));
+ _canvas_coarsener_min_size.init("/options/rendering/coarsener_min_size", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false);
+ add_devmode_line(_("Min size for coarsener algorithm"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Coarsener algorithm only processes rectangles smaller/thinner than this."));
+ _canvas_coarsener_glue_size.init("/options/rendering/coarsener_glue_size", 0.0, 1000.0, 1.0, 0.0, 80.0, true, false);
+ add_devmode_line(_("Glue size for coarsener algorithm"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Coarsener algorithm absorbs nearby rectangles within this distance."));
+ _canvas_coarsener_min_fullness.init("/options/rendering/coarsener_min_fullness", 0.0, 1.0, 0.0, 0.0, 0.3, false, false);
+ add_devmode_line(_("Min fullness for coarsener algorithm"), _canvas_coarsener_min_fullness, "", _("Refuse coarsening algorithm's attempt if the result would be more empty than this."));
+
+ add_devmode_group_header(_("Debugging, profiling and experiments"));
+ _canvas_debug_framecheck.init("", "/options/rendering/debug_framecheck", false);
+ add_devmode_line(_("Framecheck"), _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file"));
+ _canvas_debug_logging.init("", "/options/rendering/debug_logging", false);
+ add_devmode_line(_("Logging"), _canvas_debug_logging, "", _("Log certain events to the console"));
+ _canvas_debug_delay_redraw.init("", "/options/rendering/debug_delay_redraw", false);
+ add_devmode_line(_("Delay redraw"), _canvas_debug_delay_redraw, "", _("Introduce a fixed delay for each tile"));
+ _canvas_debug_delay_redraw_time.init("/options/rendering/debug_delay_redraw_time", 0.0, 1000000.0, 1.0, 0.0, 50.0, true, false);
+ add_devmode_line(_("Delay redraw time"), _canvas_debug_delay_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile"));
+ _canvas_debug_show_redraw.init("", "/options/rendering/debug_show_redraw", false);
+ add_devmode_line(_("Show redraw"), _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile"));
+ _canvas_debug_show_unclean.init("", "/options/rendering/debug_show_unclean", false);
+ add_devmode_line(_("Show unclean region"), _canvas_debug_show_unclean, "", _("Show the region that needs to be redrawn in red (only in Cairo mode)"));
+ _canvas_debug_show_snapshot.init("", "/options/rendering/debug_show_snapshot", false);
+ add_devmode_line(_("Show snapshot region"), _canvas_debug_show_snapshot, "", _("Show the region that still contains a saved copy of previously rendered content in blue (only in Cairo mode)"));
+ _canvas_debug_show_clean.init("", "/options/rendering/debug_show_clean", false);
+ add_devmode_line(_("Show clean region's fragmentation"), _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the region where rendering is complete in green (only in Cairo mode)"));
+ _canvas_debug_disable_redraw.init("", "/options/rendering/debug_disable_redraw", false);
+ add_devmode_line(_("Disable redraw"), _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely"));
+ _canvas_debug_sticky_decoupled.init("", "/options/rendering/debug_sticky_decoupled", false);
+ add_devmode_line(_("Sticky decoupled mode"), _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete"));
+ _canvas_debug_animate.init("", "/options/rendering/debug_animate", false);
+ add_devmode_line(_("Animate"), _canvas_debug_animate, "", _("Continuously adjust viewing parameters in an animation loop."));
+
+ AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING);
+}
+
+void InkscapePreferences::initPageBitmaps()
+{
+ /* Note: /options/bitmapoversample removed with Cairo renderer */
+ _page_bitmaps.add_group_header( _("Edit"));
+ _misc_bitmap_autoreload.init(_("Automatically reload images"), "/options/bitmapautoreload/value", true);
+ _page_bitmaps.add_line( false, "", _misc_bitmap_autoreload, "",
+ _("Automatically reload linked images when file is changed on disk"));
+ _misc_bitmap_editor.init("/options/bitmapeditor/value", true);
+ _page_bitmaps.add_line( false, _("_Bitmap editor:"), _misc_bitmap_editor, "", "", true);
+ _misc_svg_editor.init("/options/svgeditor/value", true);
+ _page_bitmaps.add_line( false, _("_SVG editor:"), _misc_svg_editor, "", "", true);
+
+ _page_bitmaps.add_group_header( _("Export"));
+ _importexport_export_res.init("/dialogs/export/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false);
+ _page_bitmaps.add_line( false, _("Default export _resolution:"), _importexport_export_res, _("dpi"),
+ _("Default image resolution (in dots per inch) in the Export dialog"), false);
+ _page_bitmaps.add_group_header( _("Create"));
+ _bitmap_copy_res.init("/options/createbitmap/resolution", 1.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false);
+ _page_bitmaps.add_line( false, _("Resolution for Create Bitmap _Copy:"), _bitmap_copy_res, _("dpi"),
+ _("Resolution used by the Create Bitmap Copy command"), false);
+
+ _page_bitmaps.add_group_header( _("Import"));
+ _bitmap_ask.init(_("Ask about linking and scaling when importing bitmap images"), "/dialogs/import/ask", true);
+ _page_bitmaps.add_line( true, "", _bitmap_ask, "",
+ _("Pop-up linking and scaling dialog when importing bitmap image."));
+ _svg_ask.init(_("Ask about linking and scaling when importing SVG images"), "/dialogs/import/ask_svg", true);
+ _page_bitmaps.add_line( true, "", _svg_ask, "",
+ _("Pop-up linking and scaling dialog when importing SVG image."));
+
+ _svgoutput_usesodipodiabsref.init(_("Store absolute file path for linked images"),
+ "/options/svgoutput/usesodipodiabsref", false);
+ _page_bitmaps.add_line(
+ true, "", _svgoutput_usesodipodiabsref, "",
+ _("By default, image links are stored as relative paths whenever possible. If this option is enabled, Inkscape "
+ "will additionally add an absolute path ('sodipodi:absref' attribute) to the image. This is used as a "
+ "fall-back for locating the linked image, for example if the SVG document has been moved on disk. Note that this "
+ "will expose your directory structure in the file's source code, which can include personal information like your username."),
+ false);
+
+ {
+ Glib::ustring labels[] = {_("Embed"), _("Link")};
+ Glib::ustring values[] = {"embed", "link"};
+ _bitmap_link.init("/dialogs/import/link", labels, values, G_N_ELEMENTS(values), "link");
+ _page_bitmaps.add_line( false, _("Bitmap import/open mode:"), _bitmap_link, "", "", false);
+ }
+
+ {
+ Glib::ustring labels[] = {_("Include"), _("Pages"), _("Embed"), _("Link"), _("New")};
+ Glib::ustring values[] = {"include", "pages", "embed", "link", "new"};
+ _svg_link.init("/dialogs/import/import_mode_svg", labels, values, G_N_ELEMENTS(values), "include");
+ _page_bitmaps.add_line( false, _("SVG import mode:"), _svg_link, "", "", false);
+ }
+
+ {
+ Glib::ustring labels[] = {_("None (auto)"), _("Smooth (optimizeQuality)"), _("Blocky (optimizeSpeed)") };
+ Glib::ustring values[] = {"auto", "optimizeQuality", "optimizeSpeed"};
+ _bitmap_scale.init("/dialogs/import/scale", labels, values, G_N_ELEMENTS(values), "scale");
+ _page_bitmaps.add_line( false, _("Image scale (image-rendering):"), _bitmap_scale, "", "", false);
+ }
+
+ /* Note: /dialogs/import/quality removed use of in r12542 */
+ _importexport_import_res.init("/dialogs/import/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false);
+ _page_bitmaps.add_line( false, _("Default _import resolution:"), _importexport_import_res, _("dpi"),
+ _("Default import resolution (in dots per inch) for bitmap and SVG import"), false);
+ _importexport_import_res_override.init(_("Override file resolution"), "/dialogs/import/forcexdpi", false);
+ _page_bitmaps.add_line( false, "", _importexport_import_res_override, "",
+ _("Use default bitmap resolution in favor of information from file"));
+
+ _page_bitmaps.add_group_header( _("Render"));
+ // rendering outlines for pixmap image tags
+ _rendering_image_outline.init( _("Images in Outline Mode"), "/options/rendering/imageinoutlinemode", false);
+ _page_bitmaps.add_line(false, "", _rendering_image_outline, "", _("When active will render images while in outline mode instead of a red box with an x. This is useful for manual tracing."));
+
+ this->AddPage(_page_bitmaps, _("Imported Images"), PREFS_PAGE_BITMAPS);
+}
+
+void InkscapePreferences::initKeyboardShortcuts(Gtk::TreeModel::iterator iter_ui)
+{
+ // ------- Shortcut file --------
+ auto labels_and_names = Inkscape::Shortcuts::get_file_names();
+ _kb_filelist.init( "/options/kbshortcuts/shortcutfile", labels_and_names, labels_and_names[0].second);
+
+ auto tooltip =
+ Glib::ustring::compose(_("Select a file of predefined shortcuts and modifiers to use. Any customizations you "
+ "create will be added separately to %1"),
+ IO::Resource::get_path_string(IO::Resource::USER, IO::Resource::KEYS, "default.xml"));
+
+ _page_keyshortcuts.add_line( false, _("Keyboard file:"), _kb_filelist, "", tooltip.c_str(), false);
+
+
+ // ---------- Tree --------
+ _kb_store = Gtk::TreeStore::create( _kb_columns );
+ _kb_store->set_sort_column ( GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING ); // only sort in onKBListKeyboardShortcuts()
+
+ _kb_filter = Gtk::TreeModelFilter::create(_kb_store);
+ _kb_filter->set_visible_func (sigc::mem_fun(*this, &InkscapePreferences::onKBSearchFilter));
+
+ _kb_shortcut_renderer.property_editable() = true;
+
+ _kb_tree.set_model(_kb_filter);
+ _kb_tree.append_column(_("Name"), _kb_columns.name);
+ _kb_tree.append_column(_("Shortcut"), _kb_shortcut_renderer);
+ _kb_tree.append_column(_("Description"), _kb_columns.description);
+ _kb_tree.append_column(_("ID"), _kb_columns.id);
+
+ _kb_tree.set_expander_column(*_kb_tree.get_column(0));
+
+ // Name
+ _kb_tree.get_column(0)->set_resizable(true);
+ _kb_tree.get_column(0)->set_clickable(true);
+ _kb_tree.get_column(0)->set_fixed_width (200);
+
+ // Shortcut
+ _kb_tree.get_column(1)->set_resizable(true);
+ _kb_tree.get_column(1)->set_clickable(true);
+ _kb_tree.get_column(1)->set_fixed_width (150);
+ //_kb_tree.get_column(1)->add_attribute(_kb_shortcut_renderer.property_text(), _kb_columns.shortcut);
+ _kb_tree.get_column(1)->set_cell_data_func(_kb_shortcut_renderer, sigc::ptr_fun(InkscapePreferences::onKBShortcutRenderer));
+
+ // Description
+ auto desc_renderer = dynamic_cast<Gtk::CellRendererText*>(_kb_tree.get_column_cell_renderer(2));
+ desc_renderer->property_wrap_mode() = Pango::WRAP_WORD;
+ desc_renderer->property_wrap_width() = 600;
+ _kb_tree.get_column(2)->set_resizable(true);
+ _kb_tree.get_column(2)->set_clickable(true);
+ _kb_tree.get_column(2)->set_expand(true);
+
+ // ID
+ _kb_tree.get_column(3)->set_resizable(true);
+ _kb_tree.get_column(3)->set_clickable(true);
+
+ _kb_shortcut_renderer.signal_accel_edited().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBTreeEdited) );
+ _kb_shortcut_renderer.signal_accel_cleared().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBTreeCleared) );
+
+ _kb_notebook.append_page(_kb_page_shortcuts, _("Shortcuts"));
+ Gtk::ScrolledWindow* shortcut_scroller = new Gtk::ScrolledWindow();
+ shortcut_scroller->add(_kb_tree);
+ shortcut_scroller->set_hexpand();
+ shortcut_scroller->set_vexpand();
+ // -------- Search --------
+ _kb_search.init("/options/kbshortcuts/value", true);
+ // clear filter initially to show all shortcuts;
+ // this entry will typically be stale and long forgotten and not what user is looking for
+ _kb_search.set_text({});
+ _kb_page_shortcuts.add_line( false, _("Search:"), _kb_search, "", "", true);
+ _kb_page_shortcuts.attach(*shortcut_scroller, 0, 3, 2, 1);
+
+ _mod_store = Gtk::TreeStore::create( _mod_columns );
+ _mod_tree.set_model(_mod_store);
+ _mod_tree.append_column(_("Name"), _mod_columns.name);
+ _mod_tree.append_column("hot", _mod_columns.and_modifiers);
+ _mod_tree.append_column(_("ID"), _mod_columns.id);
+ _mod_tree.set_tooltip_column(2);
+
+ // In order to get tooltips on header, we must create our own label.
+ auto and_keys_header = Gtk::manage(new Gtk::Label(_("Modifier")));
+ and_keys_header->set_tooltip_text(_("All keys specified must be held down to activate this functionality."));
+ and_keys_header->show();
+ auto and_keys_column = _mod_tree.get_column(1);
+ and_keys_column->set_widget(*and_keys_header);
+
+ auto edit_bar = Gtk::manage(new Gtk::Box());
+ _kb_mod_ctrl.set_label("Ctrl");
+ _kb_mod_shift.set_label("Shift");
+ _kb_mod_alt.set_label("Alt");
+ _kb_mod_meta.set_label("Meta");
+ _kb_mod_enabled.set_label(_("Enabled"));
+ edit_bar->add(_kb_mod_ctrl);
+ edit_bar->add(_kb_mod_shift);
+ edit_bar->add(_kb_mod_alt);
+ edit_bar->add(_kb_mod_meta);
+ edit_bar->add(_kb_mod_enabled);
+ _kb_mod_ctrl.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited));
+ _kb_mod_shift.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited));
+ _kb_mod_alt.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited));
+ _kb_mod_meta.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_edited));
+ _kb_mod_enabled.signal_toggled().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_enabled));
+ _kb_page_modifiers.add_line(false, _("Change:"), *edit_bar, "", "", true);
+
+ _mod_tree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_modifier_selection_changed));
+ on_modifier_selection_changed();
+
+ _kb_notebook.append_page(_kb_page_modifiers, _("Modifiers"));
+ Gtk::ScrolledWindow* mod_scroller = new Gtk::ScrolledWindow();
+ mod_scroller->add(_mod_tree);
+ mod_scroller->set_hexpand();
+ mod_scroller->set_vexpand();
+ //_kb_page_modifiers.add(*mod_scroller);
+ _kb_page_modifiers.attach(*mod_scroller, 0, 1, 2, 1);
+
+ int row = 2;
+ _page_keyshortcuts.attach(_kb_notebook, 0, row, 2, 1);
+
+ row++;
+
+ // ------ Reset/Import/Export -------
+ auto box_buttons = Gtk::manage(new Gtk::ButtonBox);
+
+ box_buttons->set_layout(Gtk::BUTTONBOX_END);
+ box_buttons->set_spacing(4);
+
+ box_buttons->set_hexpand();
+ _page_keyshortcuts.attach(*box_buttons, 0, row, 3, 1);
+
+ auto kb_reset = Gtk::manage(new Gtk::Button(_("Reset")));
+ kb_reset->set_use_underline();
+ kb_reset->set_tooltip_text(_("Remove all your customized keyboard shortcuts, and revert to the shortcuts in the shortcut file listed above"));
+ box_buttons->pack_start(*kb_reset, true, true, 6);
+ box_buttons->set_child_secondary(*kb_reset);
+
+ auto kb_import = Gtk::manage(new Gtk::Button(_("Import ...")));
+ kb_import->set_use_underline();
+ kb_import->set_tooltip_text(_("Import custom keyboard shortcuts from a file"));
+ box_buttons->pack_end(*kb_import, true, true, 6);
+
+ auto kb_export = Gtk::manage(new Gtk::Button(_("Export ...")));
+ kb_export->set_use_underline();
+ kb_export->set_tooltip_text(_("Export custom keyboard shortcuts to a file"));
+ box_buttons->pack_end(*kb_export, true, true, 6);
+
+ kb_reset->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBReset) );
+ kb_import->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBImport) );
+ kb_export->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBExport) );
+ _kb_search.signal_key_release_event().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBSearchKeyEvent) );
+ _kb_filelist.signal_changed().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBList) );
+ _page_keyshortcuts.signal_realize().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBRealize) );
+
+ this->AddPage(_page_keyshortcuts, _("Keyboard"), iter_ui, PREFS_PAGE_UI_KEYBOARD_SHORTCUTS);
+
+ _kb_shortcuts_loaded = false;
+ Gtk::TreeStore::iterator iter_group = _kb_store->append();
+ (*iter_group)[_kb_columns.name] = _("Loading ...");
+ (*iter_group)[_kb_columns.shortcut] = "";
+ (*iter_group)[_kb_columns.id] = "";
+ (*iter_group)[_kb_columns.description] = "";
+ (*iter_group)[_kb_columns.shortcutkey] = Gtk::AccelKey();
+ (*iter_group)[_kb_columns.user_set] = 0;
+
+ Gtk::TreeStore::iterator iter_mods = _mod_store->append();
+ (*iter_mods)[_mod_columns.name] = _("Loading ...");
+ (*iter_group)[_mod_columns.id] = "";
+ (*iter_group)[_mod_columns.description] = _("Unable to load keyboard modifier list.");
+ (*iter_group)[_mod_columns.and_modifiers] = "";
+}
+
+void InkscapePreferences::onKBList()
+{
+ // New file path already stored in preferences.
+ Inkscape::Shortcuts::getInstance().init();
+ onKBListKeyboardShortcuts();
+}
+
+void InkscapePreferences::onKBReset()
+{
+ Inkscape::Shortcuts::getInstance().clear_user_shortcuts();
+ onKBListKeyboardShortcuts();
+}
+
+void InkscapePreferences::onKBImport()
+{
+ if (Inkscape::Shortcuts::getInstance().import_shortcuts()) {
+ onKBListKeyboardShortcuts();
+ }
+}
+
+void InkscapePreferences::onKBExport()
+{
+ Inkscape::Shortcuts::getInstance().export_shortcuts();
+}
+
+bool InkscapePreferences::onKBSearchKeyEvent(GdkEventKey * /*event*/)
+{
+ _kb_filter->refilter();
+ auto search = _kb_search.get_text();
+ if (search.length() > 2) {
+ _kb_tree.expand_all();
+ }
+ else {
+ _kb_tree.collapse_all();
+ }
+ return FALSE;
+}
+
+void InkscapePreferences::onKBTreeCleared(const Glib::ustring& path)
+{
+ // Triggered by "Back" key.
+ Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path);
+ Glib::ustring id = (*iter)[_kb_columns.id];
+ // Gtk::AccelKey current_shortcut_key = (*iter)[_kb_columns.shortcutkey];
+
+ // Remove current shortcut from user file (won't remove from other files).
+ Inkscape::Shortcuts::getInstance().remove_user_shortcut(id);
+
+ onKBListKeyboardShortcuts();
+}
+
+void InkscapePreferences::onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode)
+{
+ Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance();
+
+ Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path);
+
+ Glib::ustring id = (*iter)[_kb_columns.id];
+ Glib::ustring current_shortcut = (*iter)[_kb_columns.shortcut];
+ Gtk::AccelKey const current_shortcut_key = (*iter)[_kb_columns.shortcutkey];
+
+ GdkEventKey event;
+ event.keyval = accel_key;
+ event.state = accel_mods;
+ event.hardware_keycode = hardware_keycode;
+ Gtk::AccelKey const new_shortcut_key = shortcuts.get_from_event(&event, true);
+
+ if (!new_shortcut_key.is_null() &&
+ (new_shortcut_key.get_key() != current_shortcut_key.get_key() ||
+ new_shortcut_key.get_mod() != current_shortcut_key.get_mod())
+ ) {
+ // Check if there is currently an actions assigned to this shortcut; if yes ask if the shortcut should be reassigned
+ Glib::ustring action_name;
+ Glib::ustring accel = Gtk::AccelGroup::name(accel_key, accel_mods);
+ auto *app = InkscapeApplication::instance()->gtk_app();
+ std::vector<Glib::ustring> actions = app->get_actions_for_accel(accel);
+ if (!actions.empty()) {
+ action_name = actions[0];
+ }
+
+ if (!action_name.empty()) {
+ // Warn user about duplicated shortcuts.
+ Glib::ustring message =
+ Glib::ustring::compose(_("Keyboard shortcut \"%1\"\nis already assigned to \"%2\""),
+ shortcuts.get_label(new_shortcut_key), action_name);
+ Gtk::MessageDialog dialog(message, false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO, true);
+ dialog.set_title(_("Reassign shortcut?"));
+ dialog.set_secondary_text(_("Are you sure you want to reassign this shortcut?"));
+ dialog.set_transient_for(*dynamic_cast<Gtk::Window *>(get_toplevel()));
+ int response = dialog.run();
+ if (response != Gtk::RESPONSE_YES) {
+ return;
+ }
+ }
+
+ // Add the new shortcut.
+ shortcuts.add_user_shortcut(id, new_shortcut_key);
+
+ onKBListKeyboardShortcuts();
+ }
+}
+
+static bool is_leaf_visible(const Gtk::TreeModel::const_iterator& iter, const Glib::ustring& search) {
+ Glib::ustring name = (*iter)[_kb_columns.name];
+ Glib::ustring desc = (*iter)[_kb_columns.description];
+ Glib::ustring shortcut = (*iter)[_kb_columns.shortcut];
+ Glib::ustring id = (*iter)[_kb_columns.id];
+
+ if (name.lowercase().find(search) != name.npos
+ || shortcut.lowercase().find(search) != name.npos
+ || desc.lowercase().find(search) != name.npos
+ || id.lowercase().find(search) != name.npos) {
+ return true;
+ }
+
+ for (auto& child : iter->children()) {
+ if (is_leaf_visible(child, search)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool InkscapePreferences::onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter)
+{
+ Glib::ustring search = _kb_search.get_text().lowercase();
+ if (search.empty()) {
+ return true;
+ }
+
+ return is_leaf_visible(iter, search);
+}
+
+void InkscapePreferences::onKBRealize()
+{
+ if (!_kb_shortcuts_loaded /*&& _current_page == &_page_keyshortcuts*/) {
+ _kb_shortcuts_loaded = true;
+ onKBListKeyboardShortcuts();
+ }
+}
+
+void InkscapePreferences::onKBShortcutRenderer(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) {
+
+ Glib::ustring shortcut = (*iter)[onKBGetCols().shortcut];
+ unsigned int user_set = (*iter)[onKBGetCols().user_set];
+ Gtk::CellRendererAccel *accel = dynamic_cast<Gtk::CellRendererAccel *>(renderer);
+ if (user_set) {
+ accel->property_markup() = Glib::ustring("<span font-weight='bold'> " + shortcut + " </span>").c_str();
+ } else {
+ accel->property_markup() = Glib::ustring("<span> " + shortcut + " </span>").c_str();
+ }
+}
+
+void InkscapePreferences::on_modifier_selection_changed()
+{
+ _kb_is_updated = true;
+ Gtk::TreeStore::iterator iter = _mod_tree.get_selection()->get_selected();
+ auto selected = static_cast<bool>(iter);
+
+ _kb_mod_ctrl.set_sensitive(selected);
+ _kb_mod_shift.set_sensitive(selected);
+ _kb_mod_alt.set_sensitive(selected);
+ _kb_mod_meta.set_sensitive(selected);
+ _kb_mod_enabled.set_sensitive(selected);
+
+ _kb_mod_ctrl.set_active(false);
+ _kb_mod_shift.set_active(false);
+ _kb_mod_alt.set_active(false);
+ _kb_mod_meta.set_active(false);
+ _kb_mod_enabled.set_active(false);
+
+ if (selected) {
+ Glib::ustring modifier_id = (*iter)[_mod_columns.id];
+ auto modifier = Modifiers::Modifier::get(modifier_id.c_str());
+ Inkscape::Modifiers::KeyMask mask = Inkscape::Modifiers::NEVER;
+ if(modifier != nullptr) {
+ mask = modifier->get_and_mask();
+ } else {
+ _kb_mod_enabled.set_sensitive(false);
+ }
+ if(mask != Inkscape::Modifiers::NEVER) {
+ _kb_mod_enabled.set_active(true);
+ _kb_mod_ctrl.set_active(mask & Inkscape::Modifiers::CTRL);
+ _kb_mod_shift.set_active(mask & Inkscape::Modifiers::SHIFT);
+ _kb_mod_alt.set_active(mask & Inkscape::Modifiers::ALT);
+ _kb_mod_meta.set_active(mask & Inkscape::Modifiers::META);
+ } else {
+ _kb_mod_ctrl.set_sensitive(false);
+ _kb_mod_shift.set_sensitive(false);
+ _kb_mod_alt.set_sensitive(false);
+ _kb_mod_meta.set_sensitive(false);
+ }
+ }
+ _kb_is_updated = false;
+}
+
+void InkscapePreferences::on_modifier_enabled()
+{
+ auto active = _kb_mod_enabled.get_active();
+ _kb_mod_ctrl.set_sensitive(active);
+ _kb_mod_shift.set_sensitive(active);
+ _kb_mod_alt.set_sensitive(active);
+ _kb_mod_meta.set_sensitive(active);
+ on_modifier_edited();
+}
+
+void InkscapePreferences::on_modifier_edited()
+{
+ Gtk::TreeStore::iterator iter = _mod_tree.get_selection()->get_selected();
+ if (!iter || _kb_is_updated) return;
+
+ Glib::ustring modifier_id = (*iter)[_mod_columns.id];
+ auto modifier = Modifiers::Modifier::get(modifier_id.c_str());
+ if(!_kb_mod_enabled.get_active()) {
+ modifier->set_user(Inkscape::Modifiers::NEVER, Inkscape::Modifiers::NOT_SET);
+ } else {
+ Inkscape::Modifiers::KeyMask mask = 0;
+ if(_kb_mod_ctrl.get_active()) mask |= Inkscape::Modifiers::CTRL;
+ if(_kb_mod_shift.get_active()) mask |= Inkscape::Modifiers::SHIFT;
+ if(_kb_mod_alt.get_active()) mask |= Inkscape::Modifiers::ALT;
+ if(_kb_mod_meta.get_active()) mask |= Inkscape::Modifiers::META;
+ modifier->set_user(mask, Inkscape::Modifiers::NOT_SET);
+ }
+ Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance();
+ shortcuts.write_user();
+ (*iter)[_mod_columns.and_modifiers] = modifier->get_label();
+}
+
+void InkscapePreferences::onKBListKeyboardShortcuts()
+{
+ Inkscape::Shortcuts& shortcuts = Inkscape::Shortcuts::getInstance();
+
+ // Save the current selection
+ Gtk::TreeStore::iterator iter = _kb_tree.get_selection()->get_selected();
+ Glib::ustring selected_id = "";
+ if (iter) {
+ selected_id = (*iter)[_kb_columns.id];
+ }
+
+ _kb_store->clear();
+ _mod_store->clear();
+
+ // Gio::Actions
+
+ auto iapp = InkscapeApplication::instance();
+ auto gapp = iapp->gtk_app();
+
+ // std::vector<Glib::ustring> actions = shortcuts.list_all_actions(); // All actions (app, win, doc)
+
+ // Simpler and better to get action list from extra data (contains "detailed action names").
+ InkActionExtraData& action_data = iapp->get_action_extra_data();
+ std::vector<Glib::ustring> actions = action_data.get_actions();
+
+ // Sort actions by section
+ auto action_sort =
+ [&](Glib::ustring &a, Glib::ustring &b) {
+ return action_data.get_section_for_action(a) < action_data.get_section_for_action(b);
+ };
+ std::sort (actions.begin(), actions.end(), action_sort);
+
+ Glib::ustring old_section;
+ Gtk::TreeStore::iterator iter_group;
+
+ // Fill sections
+ for (auto action : actions) {
+
+ Glib::ustring section = action_data.get_section_for_action(action);
+ if (section.empty()) section = "Misc";
+ if (section != old_section) {
+ iter_group = _kb_store->append();
+ (*iter_group)[_kb_columns.name] = section;
+ (*iter_group)[_kb_columns.shortcut] = "";
+ (*iter_group)[_kb_columns.description] = "";
+ (*iter_group)[_kb_columns.shortcutkey] = Gtk::AccelKey();
+ (*iter_group)[_kb_columns.id] = "";
+ (*iter_group)[_kb_columns.user_set] = 0;
+ old_section = section;
+ }
+
+ // Find accelerators
+ std::vector<Glib::ustring> accels = gapp->get_accels_for_action(action);
+ Glib::ustring shortcut_label;
+ for (auto accel : accels) {
+ // Convert to more user friendly notation.
+
+ // ::get_label shows key pad and numeric keys identically.
+ // TODO: Results in labels like "Numpad Alt+5"
+ if (accel.find("KP") != Glib::ustring::npos) {
+ shortcut_label += _("Numpad");
+ shortcut_label += " ";
+ }
+ unsigned int key = 0;
+ Gdk::ModifierType mod = Gdk::ModifierType(0);
+ Gtk::AccelGroup::parse(accel, key, mod);
+ shortcut_label += Gtk::AccelGroup::get_label(key, mod) + ", ";
+ }
+
+ if (shortcut_label.size() > 1) {
+ shortcut_label.erase(shortcut_label.size()-2);
+ }
+
+ // Find primary (i.e. first) shortcut.
+ Gtk::AccelKey shortcut_key;
+ if (accels.size() > 0) {
+ unsigned int key = 0;
+ Gdk::ModifierType mod = Gdk::ModifierType(0);
+ Gtk::AccelGroup::parse(accels[0], key, mod);
+ shortcut_key = Gtk::AccelKey(key, mod);
+ }
+
+ // Add the action to the group
+ Gtk::TreeStore::iterator row = _kb_store->append(iter_group->children());
+ (*row)[_kb_columns.name] = action_data.get_label_for_action(action);
+ (*row)[_kb_columns.shortcut] = shortcut_label;
+ (*row)[_kb_columns.description] = action_data.get_tooltip_for_action(action);
+ (*row)[_kb_columns.shortcutkey] = shortcut_key;
+ (*row)[_kb_columns.id] = action;
+ (*row)[_kb_columns.user_set] = shortcuts.is_user_set(action);
+
+ if (selected_id == action) {
+ Gtk::TreeStore::Path sel_path = _kb_filter->convert_child_path_to_path(_kb_store->get_path(row));
+ _kb_tree.expand_to_path(sel_path);
+ _kb_tree.get_selection()->select(sel_path);
+ }
+ }
+
+ std::string old_mod_group;
+ Gtk::TreeStore::iterator iter_mod_group;
+
+ // Modifiers (mouse specific keys)
+ for(auto modifier: Inkscape::Modifiers::Modifier::getList()) {
+ auto cat_name = modifier->get_category();
+ if (cat_name != old_mod_group) {
+ iter_mod_group = _mod_store->append();
+ (*iter_mod_group)[_mod_columns.name] = cat_name.empty() ? "" : _(cat_name.c_str());
+ (*iter_mod_group)[_mod_columns.id] = "";
+ (*iter_mod_group)[_mod_columns.description] = "";
+ (*iter_mod_group)[_mod_columns.and_modifiers] = "";
+ (*iter_mod_group)[_mod_columns.user_set] = 0;
+ old_mod_group = cat_name;
+ }
+
+ // Find accelerators
+ Gtk::TreeStore::iterator iter_modifier = _mod_store->append(iter_mod_group->children());
+ (*iter_modifier)[_mod_columns.name] = (modifier->get_name() && strlen(modifier->get_name())) ? _(modifier->get_name()) : "";
+ (*iter_modifier)[_mod_columns.id] = modifier->get_id();
+ (*iter_modifier)[_mod_columns.description] = (modifier->get_description() && strlen(modifier->get_description())) ? _(modifier->get_description()) : "";
+ (*iter_modifier)[_mod_columns.and_modifiers] = modifier->get_label();
+ (*iter_modifier)[_mod_columns.user_set] = modifier->is_set_user();
+ }
+
+ // re-order once after updating (then disable ordering again to increase performance)
+ _kb_store->set_sort_column (_kb_columns.id, Gtk::SORT_ASCENDING );
+ _kb_store->set_sort_column ( GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING );
+
+ if (selected_id.empty()) {
+ _kb_tree.expand_to_path(_kb_store->get_path(_kb_store->get_iter("0:1")));
+ }
+
+ // Update all GUI text that includes shortcuts.
+ for (auto win : gapp->get_windows()) {
+ shortcuts.update_gui_text_recursive(win);
+ }
+
+ // Update all GUI text
+ std::list< SPDesktop* > listbuf;
+ // Get list of all available desktops
+ INKSCAPE.get_all_desktops(listbuf);
+
+ // Update text of each desktop to correct Shortcuts
+ for(SPDesktop *desktop: listbuf) {
+ // Caution: Checking if pointers are not NULL
+ if(desktop) {
+ InkscapeWindow *window = desktop->getInkscapeWindow();
+ if(window) {
+ SPDesktopWidget *dtw = window->get_desktop_widget();
+ if(dtw)
+ build_menu();
+ }
+ }
+ }
+}
+
+void InkscapePreferences::initPageSpellcheck()
+{
+#if WITH_GSPELL
+
+ _spell_ignorenumbers.init( _("Ignore words with digits"), "/dialogs/spellcheck/ignorenumbers", true);
+ _page_spellcheck.add_line( false, "", _spell_ignorenumbers, "",
+ _("Ignore words containing digits, such as \"R2D2\""), true);
+
+ _spell_ignoreallcaps.init( _("Ignore words in ALL CAPITALS"), "/dialogs/spellcheck/ignoreallcaps", false);
+ _page_spellcheck.add_line( false, "", _spell_ignoreallcaps, "",
+ _("Ignore words in all capitals, such as \"IUPAC\""), true);
+
+ this->AddPage(_page_spellcheck, _("Spellcheck"), PREFS_PAGE_SPELLCHECK);
+#endif
+}
+
+template <typename string_type>
+static void appendList(Glib::ustring& tmp, const std::vector<string_type> &listing)
+{
+ for (auto const & str : listing) {
+ tmp += str;
+ tmp += "\n";
+ }
+}
+
+
+void InkscapePreferences::initPageSystem()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _sys_shared_path.init("/options/resources/sharedpath", true);
+ auto box = new Gtk::Box();
+ box->pack_start(_sys_shared_path);
+ box->set_size_request(300, -1);
+ _page_system.add_line( false, _("Shared default resources folder:"), *box, "",
+ _("A folder structured like a user's Inkscape preferences directory. This makes it possible to share a set of resources, such as extensions, fonts, icon sets, keyboard shortcuts, patterns/hatches, palettes, symbols, templates, themes and user interface definition files, between multiple users who have access to that folder (on the same computer or in the network). Requires a restart of Inkscape to work when changed."), false, reset_icon());
+ _page_system.add_group_header( _("System info"));
+
+ _sys_user_prefs.set_text(prefs->getPrefsFilename());
+ _sys_user_prefs.set_editable(false);
+ Gtk::Button* reset_prefs = Gtk::manage(new Gtk::Button(_("Reset Preferences")));
+ reset_prefs->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_prefs_clicked));
+
+ _page_system.add_line(true, _("User preferences:"), _sys_user_prefs, "",
+ _("Location of the user’s preferences file"), true, reset_prefs);
+ auto profilefolder = Inkscape::IO::Resource::profile_path();
+ _sys_user_config.init(profilefolder.c_str(), _("Open preferences folder"));
+ _page_system.add_line(true, _("User config:"), _sys_user_config, "", _("Location of users configuration"), true);
+
+ auto extensions_folder = IO::Resource::get_path_string(IO::Resource::USER, IO::Resource::EXTENSIONS);
+ _sys_user_extension_dir.init(extensions_folder,
+ _("Open extensions folder"));
+ _page_system.add_line(true, _("User extensions:"), _sys_user_extension_dir, "",
+ _("Location of the user’s extensions"), true);
+
+ _sys_user_fonts_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::FONTS, ""),
+ _("Open fonts folder"));
+ _page_system.add_line(true, _("User fonts:"), _sys_user_fonts_dir, "", _("Location of the user’s fonts"), true);
+
+ _sys_user_themes_dir.init(g_build_filename(g_get_user_data_dir(), "themes", nullptr), _("Open themes folder"));
+ _page_system.add_line(true, _("User themes:"), _sys_user_themes_dir, "", _("Location of the user’s themes"), true);
+
+ _sys_user_icons_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::ICONS, ""),
+ _("Open icons folder"));
+ _page_system.add_line(true, _("User icons:"), _sys_user_icons_dir, "", _("Location of the user’s icons"), true);
+
+ _sys_user_templates_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::TEMPLATES, ""),
+ _("Open templates folder"));
+ _page_system.add_line(true, _("User templates:"), _sys_user_templates_dir, "",
+ _("Location of the user’s templates"), true);
+
+ _sys_user_symbols_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::SYMBOLS, ""),
+ _("Open symbols folder"));
+
+ _page_system.add_line(true, _("User symbols:"), _sys_user_symbols_dir, "", _("Location of the user’s symbols"),
+ true);
+
+ _sys_user_paint_servers_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::PAINT, ""),
+ _("Open paint servers folder"));
+
+ _page_system.add_line(true, _("User paint servers:"), _sys_user_paint_servers_dir, "",
+ _("Location of the user’s paint servers"), true);
+
+ _sys_user_palettes_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::PALETTES, ""),
+ _("Open palettes folder"));
+ _page_system.add_line(true, _("User palettes:"), _sys_user_palettes_dir, "", _("Location of the user’s palettes"),
+ true);
+
+ _sys_user_keys_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::KEYS, ""),
+ _("Open keyboard shortcuts folder"));
+ _page_system.add_line(true, _("User keys:"), _sys_user_keys_dir, "",
+ _("Location of the user’s keyboard mapping files"), true);
+
+ _sys_user_ui_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::UIS, ""),
+ _("Open user interface folder"));
+ _page_system.add_line(true, _("User UI:"), _sys_user_ui_dir, "",
+ _("Location of the user’s user interface description files"), true);
+
+ _sys_user_cache.set_text(g_get_user_cache_dir());
+ _sys_user_cache.set_editable(false);
+ _page_system.add_line(true, _("User cache:"), _sys_user_cache, "", _("Location of user’s cache"), true);
+
+ Glib::ustring tmp_dir = prefs->getString("/options/autosave/path");
+ if (tmp_dir.empty()) {
+ tmp_dir = Glib::build_filename(Glib::get_user_cache_dir(), "inkscape");
+ }
+ _sys_tmp_files.set_text(tmp_dir);
+ _sys_tmp_files.set_editable(false);
+ _page_system.add_line(true, _("Temporary files:"), _sys_tmp_files, "", _("Location of the temporary files used for autosave"), true);
+
+ _sys_data.set_text(get_inkscape_datadir());
+ _sys_data.set_editable(false);
+ _page_system.add_line(true, _("Inkscape data:"), _sys_data, "", _("Location of Inkscape data"), true);
+
+ extensions_folder = IO::Resource::get_path_string(IO::Resource::SYSTEM, IO::Resource::EXTENSIONS);
+ _sys_extension_dir.set_text(extensions_folder);
+ _sys_extension_dir.set_editable(false);
+ _page_system.add_line(true, _("Inkscape extensions:"), _sys_extension_dir, "", _("Location of the Inkscape extensions"), true);
+
+ Glib::ustring tmp;
+ auto system_data_dirs = Glib::get_system_data_dirs();
+ appendList(tmp, system_data_dirs);
+ _sys_systemdata.get_buffer()->insert(_sys_systemdata.get_buffer()->end(), tmp);
+ _sys_systemdata.set_editable(false);
+ _sys_systemdata_scroll.add(_sys_systemdata);
+ _sys_systemdata_scroll.set_size_request(100, 80);
+ _sys_systemdata_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _sys_systemdata_scroll.set_shadow_type(Gtk::SHADOW_IN);
+ _page_system.add_line(true, _("System data:"), _sys_systemdata_scroll, "", _("Locations of system data"), true);
+
+ _sys_fontdirs_custom.init("/options/font/custom_fontdirs", 50);
+ _page_system.add_line(true, _("Custom Font directories"), _sys_fontdirs_custom, "", _("Load additional fonts from custom locations (one path per line)"), true);
+
+ tmp = "";
+ auto icon_theme = Gtk::IconTheme::get_default();
+ auto paths = icon_theme->get_search_path();
+ appendList( tmp, paths );
+ _sys_icon.get_buffer()->insert(_sys_icon.get_buffer()->end(), tmp);
+ _sys_icon.set_editable(false);
+ _sys_icon_scroll.add(_sys_icon);
+ _sys_icon_scroll.set_size_request(100, 80);
+ _sys_icon_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _sys_icon_scroll.set_shadow_type(Gtk::SHADOW_IN);
+ _page_system.add_line(true, _("Icon theme:"), _sys_icon_scroll, "", _("Locations of icon themes"), true);
+
+ this->AddPage(_page_system, _("System"), PREFS_PAGE_SYSTEM);
+}
+
+bool InkscapePreferences::GetSizeRequest(const Gtk::TreeModel::iterator& iter)
+{
+ Gtk::TreeModel::Row row = *iter;
+ DialogPage* page = row[_page_list_columns._col_page];
+ _page_frame.add(*page);
+ this->show_all_children();
+ Gtk::Requisition sreq_minimum;
+ Gtk::Requisition sreq_natural;
+ get_preferred_size(sreq_minimum, sreq_natural);
+ _minimum_width = std::max(_minimum_width, sreq_minimum.width);
+ _minimum_height = std::max(_minimum_height, sreq_minimum.height);
+ _natural_width = std::max(_natural_width, sreq_natural.width);
+ _natural_height = std::max(_natural_height, sreq_natural.height);
+ _page_frame.remove();
+ return false;
+}
+
+// Check if iter points to page indicated in preferences.
+bool InkscapePreferences::matchPage(const Gtk::TreeModel::iterator& iter)
+{
+ Gtk::TreeModel::Row row = *iter;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int desired_page = prefs->getInt("/dialogs/preferences/page", 0);
+ _init = false;
+ if (desired_page == row[_page_list_columns._col_id])
+ {
+ auto const path = _page_list.get_model()->get_path(*iter);
+ _page_list.expand_to_path(path);
+ _page_list.get_selection()->select(iter);
+ if (desired_page == PREFS_PAGE_UI_THEME)
+ symbolicThemeCheck();
+ return true;
+ }
+ return false;
+}
+
+void InkscapePreferences::on_reset_open_recent_clicked()
+{
+ Glib::RefPtr<Gtk::RecentManager> manager = Gtk::RecentManager::get_default();
+ std::vector< Glib::RefPtr< Gtk::RecentInfo > > recent_list = manager->get_items();
+
+ // Remove only elements that were added by Inkscape
+ // TODO: This should likely preserve items that were also accessed by other apps.
+ // However there does not seem to be straightforward way to delete only an application from an item.
+ for (auto e : recent_list) {
+ if (e->has_application(g_get_prgname())
+ || e->has_application("org.inkscape.Inkscape")
+ || e->has_application("inkscape")
+#ifdef _WIN32
+ || e->has_application("inkscape.exe")
+#endif
+ ) {
+ manager->remove_item(e->get_uri());
+ }
+ }
+}
+
+void InkscapePreferences::on_reset_prefs_clicked()
+{
+ Inkscape::Preferences::get()->reset();
+}
+
+void InkscapePreferences::show_not_found()
+{
+ if (_current_page)
+ _page_frame.remove();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _current_page = &_page_notfound;
+ _page_title.set_markup(_("<span size='large'><b>No Results</b></span>"));
+ _page_frame.add(*_current_page);
+ _current_page->show();
+ this->show_all_children();
+ if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) {
+ symbolicThemeCheck();
+ }
+}
+
+void InkscapePreferences::show_nothing_on_page()
+{
+ _page_frame.remove();
+ _page_title.set_text("");
+}
+
+void InkscapePreferences::on_pagelist_selection_changed()
+{
+ // show new selection
+ Glib::RefPtr<Gtk::TreeSelection> selection = _page_list.get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if(iter)
+ {
+ if (_current_page)
+ _page_frame.remove();
+ Gtk::TreeModel::Row row = *iter;
+ _current_page = row[_page_list_columns._col_page];
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!_init) {
+ prefs->setInt("/dialogs/preferences/page", row[_page_list_columns._col_id]);
+ }
+ Glib::ustring col_name_escaped = Glib::Markup::escape_text( row[_page_list_columns._col_name] );
+ _page_title.set_markup("<span size='large'><b>" + col_name_escaped + "</b></span>");
+ _page_frame.add(*_current_page);
+ _current_page->show();
+ this->show_all_children();
+ if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) {
+ symbolicThemeCheck();
+ }
+ }
+}
+
+// Show page indicated in preferences file.
+void InkscapePreferences::showPage()
+{
+ _search.set_text("");
+ _page_list.get_model()->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::matchPage));
+}
+
+} // 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/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h
new file mode 100644
index 0000000..5a1d2fc
--- /dev/null
+++ b/src/ui/dialog/inkscape-preferences.h
@@ -0,0 +1,738 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Inkscape Preferences dialog
+ */
+/* Authors:
+ * Carl Hetherington
+ * Marco Scholten
+ * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
+ * Bruno Dilly <bruno.dilly@gmail.com>
+ *
+ * Copyright (C) 2004-2013 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H
+#define INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H
+
+// checking if cairo supports dithering
+#include <gtkmm/sizegroup.h>
+#ifdef WITH_PATCHED_CAIRO
+#include "3rdparty/cairo/src/cairo.h"
+#else
+#include <cairo.h>
+#endif
+
+
+
+#include <gtkmm/treerowreference.h>
+#include <iostream>
+#include <iterator>
+#include <vector>
+#include "ui/widget/preferences-widget.h"
+#include <cstddef>
+#include <gtkmm/colorbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treemodelsort.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treemodelfilter.h>
+#include <glibmm/regex.h>
+
+#include "ui/dialog/dialog-base.h"
+
+// UPDATE THIS IF YOU'RE ADDING PREFS PAGES.
+// Otherwise the commands that open the dialog with the new page will fail.
+
+enum
+{
+ PREFS_PAGE_TOOLS,
+ PREFS_PAGE_TOOLS_SELECTOR,
+ PREFS_PAGE_TOOLS_NODE,
+ PREFS_PAGE_TOOLS_TWEAK,
+ PREFS_PAGE_TOOLS_ZOOM,
+ PREFS_PAGE_TOOLS_MEASURE,
+ PREFS_PAGE_TOOLS_SHAPES,
+ PREFS_PAGE_TOOLS_SHAPES_RECT,
+ PREFS_PAGE_TOOLS_SHAPES_3DBOX,
+ PREFS_PAGE_TOOLS_SHAPES_ELLIPSE,
+ PREFS_PAGE_TOOLS_SHAPES_STAR,
+ PREFS_PAGE_TOOLS_SHAPES_SPIRAL,
+ PREFS_PAGE_TOOLS_PENCIL,
+ PREFS_PAGE_TOOLS_PEN,
+ PREFS_PAGE_TOOLS_CALLIGRAPHY,
+ PREFS_PAGE_TOOLS_TEXT,
+ PREFS_PAGE_TOOLS_SPRAY,
+ PREFS_PAGE_TOOLS_ERASER,
+ PREFS_PAGE_TOOLS_PAINTBUCKET,
+ PREFS_PAGE_TOOLS_GRADIENT,
+ PREFS_PAGE_TOOLS_DROPPER,
+ PREFS_PAGE_TOOLS_CONNECTOR,
+ PREFS_PAGE_TOOLS_LPETOOL,
+ PREFS_PAGE_UI,
+ PREFS_PAGE_UI_THEME,
+ PREFS_PAGE_UI_TOOLBARS,
+ PREFS_PAGE_UI_WINDOWS,
+ PREFS_PAGE_UI_COLOR_PICKERS,
+ PREFS_PAGE_UI_GRIDS,
+ PREFS_PAGE_COMMAND_PALETTE,
+ PREFS_PAGE_UI_KEYBOARD_SHORTCUTS,
+ PREFS_PAGE_BEHAVIOR,
+ PREFS_PAGE_BEHAVIOR_SELECTING,
+ PREFS_PAGE_BEHAVIOR_TRANSFORMS,
+ PREFS_PAGE_BEHAVIOR_SCROLLING,
+ PREFS_PAGE_BEHAVIOR_SNAPPING,
+ PREFS_PAGE_BEHAVIOR_STEPS,
+ PREFS_PAGE_BEHAVIOR_CLONES,
+ PREFS_PAGE_BEHAVIOR_MASKS,
+ PREFS_PAGE_BEHAVIOR_MARKERS,
+ PREFS_PAGE_BEHAVIOR_CLIPBOARD,
+ PREFS_PAGE_BEHAVIOR_CLEANUP,
+ PREFS_PAGE_BEHAVIOR_LPE,
+ PREFS_PAGE_IO,
+ PREFS_PAGE_IO_MOUSE,
+ PREFS_PAGE_IO_SVGOUTPUT,
+ PREFS_PAGE_IO_SVGEXPORT,
+ PREFS_PAGE_IO_CMS,
+ PREFS_PAGE_IO_AUTOSAVE,
+ PREFS_PAGE_IO_OPENCLIPART,
+ PREFS_PAGE_SYSTEM,
+ PREFS_PAGE_BITMAPS,
+ PREFS_PAGE_RENDERING,
+ PREFS_PAGE_SPELLCHECK,
+ PREFS_PAGE_NOTFOUND
+};
+
+namespace Gtk {
+class Scale;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class InkscapePreferences : public DialogBase
+{
+public:
+ InkscapePreferences();
+ ~InkscapePreferences() override;
+
+ void showPage(); // Show page indicated by "/dialogs/preferences/page".
+
+protected:
+ Gtk::Frame _page_frame;
+ Gtk::Label _page_title;
+ Gtk::TreeView _page_list;
+ Gtk::SearchEntry _search;
+ Glib::RefPtr<Gtk::TreeStore> _page_list_model;
+ Gtk::Widget *_highlighted_widget = nullptr;
+ Glib::RefPtr<Gtk::TreeModelFilter> _page_list_model_filter;
+ Glib::RefPtr<Gtk::TreeModelSort> _page_list_model_sort;
+ std::vector<Gtk::Widget *> _search_results;
+ Glib::RefPtr<Glib::Regex> _rx;
+ int _num_results = 0;
+ bool _show_all = false;
+
+ //Pagelist model columns:
+ class PageListModelColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ PageListModelColumns()
+ { Gtk::TreeModelColumnRecord::add(_col_name); Gtk::TreeModelColumnRecord::add(_col_page); Gtk::TreeModelColumnRecord::add(_col_id); }
+ Gtk::TreeModelColumn<Glib::ustring> _col_name;
+ Gtk::TreeModelColumn<int> _col_id;
+ Gtk::TreeModelColumn<UI::Widget::DialogPage*> _col_page;
+ };
+ PageListModelColumns _page_list_columns;
+
+ UI::Widget::DialogPage _page_tools;
+ UI::Widget::DialogPage _page_selector;
+ UI::Widget::DialogPage _page_node;
+ UI::Widget::DialogPage _page_tweak;
+ UI::Widget::DialogPage _page_spray;
+ UI::Widget::DialogPage _page_zoom;
+ UI::Widget::DialogPage _page_measure;
+ UI::Widget::DialogPage _page_shapes;
+ UI::Widget::DialogPage _page_pencil;
+ UI::Widget::DialogPage _page_pen;
+ UI::Widget::DialogPage _page_calligraphy;
+ UI::Widget::DialogPage _page_text;
+ UI::Widget::DialogPage _page_gradient;
+ UI::Widget::DialogPage _page_connector;
+ UI::Widget::DialogPage _page_dropper;
+ UI::Widget::DialogPage _page_lpetool;
+
+ UI::Widget::DialogPage _page_rectangle;
+ UI::Widget::DialogPage _page_3dbox;
+ UI::Widget::DialogPage _page_ellipse;
+ UI::Widget::DialogPage _page_star;
+ UI::Widget::DialogPage _page_spiral;
+ UI::Widget::DialogPage _page_paintbucket;
+ UI::Widget::DialogPage _page_eraser;
+
+ UI::Widget::DialogPage _page_ui;
+ UI::Widget::DialogPage _page_notfound;
+ UI::Widget::DialogPage _page_theme;
+ UI::Widget::DialogPage _page_toolbars;
+ UI::Widget::DialogPage _page_windows;
+ UI::Widget::DialogPage _page_grids;
+ UI::Widget::DialogPage _page_command_palette;
+ UI::Widget::DialogPage _page_color_pickers;
+
+ UI::Widget::DialogPage _page_behavior;
+ UI::Widget::DialogPage _page_select;
+ UI::Widget::DialogPage _page_transforms;
+ UI::Widget::DialogPage _page_scrolling;
+ UI::Widget::DialogPage _page_snapping;
+ UI::Widget::DialogPage _page_steps;
+ UI::Widget::DialogPage _page_clones;
+ UI::Widget::DialogPage _page_mask;
+ UI::Widget::DialogPage _page_markers;
+ UI::Widget::DialogPage _page_clipboard;
+ UI::Widget::DialogPage _page_cleanup;
+ UI::Widget::DialogPage _page_lpe;
+
+ UI::Widget::DialogPage _page_io;
+ UI::Widget::DialogPage _page_mouse;
+ UI::Widget::DialogPage _page_svgoutput;
+ UI::Widget::DialogPage _page_svgexport;
+ UI::Widget::DialogPage _page_cms;
+ UI::Widget::DialogPage _page_autosave;
+
+ UI::Widget::DialogPage _page_rendering;
+ UI::Widget::DialogPage _page_system;
+ UI::Widget::DialogPage _page_bitmaps;
+ UI::Widget::DialogPage _page_spellcheck;
+
+ UI::Widget::DialogPage _page_keyshortcuts;
+
+ UI::Widget::PrefSpinButton _mouse_sens;
+ UI::Widget::PrefSpinButton _mouse_thres;
+ UI::Widget::PrefSlider _mouse_grabsize;
+ UI::Widget::PrefCheckButton _mouse_use_ext_input;
+ UI::Widget::PrefCheckButton _mouse_switch_on_ext_input;
+
+ UI::Widget::PrefSpinButton _scroll_wheel;
+ UI::Widget::PrefSpinButton _scroll_arrow_px;
+ UI::Widget::PrefSpinButton _scroll_arrow_acc;
+ UI::Widget::PrefSpinButton _scroll_auto_speed;
+ UI::Widget::PrefSpinButton _scroll_auto_thres;
+ UI::Widget::PrefCheckButton _scroll_space;
+
+ Gtk::Scale *_slider_snapping_delay;
+
+ UI::Widget::PrefCheckButton _snap_default;
+ UI::Widget::PrefCheckButton _snap_indicator;
+ UI::Widget::PrefCheckButton _snap_closest_only;
+ UI::Widget::PrefCheckButton _snap_mouse_pointer;
+ UI::Widget::PrefCheckButton _snap_indicator_distance;
+
+ UI::Widget::PrefCombo _steps_rot_snap;
+ UI::Widget::PrefCheckButton _steps_rot_relative;
+ UI::Widget::PrefCheckButton _steps_compass;
+ UI::Widget::PrefSpinUnit _steps_arrow;
+ UI::Widget::PrefSpinUnit _steps_scale;
+ UI::Widget::PrefSpinUnit _steps_inset;
+ UI::Widget::PrefSpinButton _steps_zoom;
+ UI::Widget::PrefCheckButton _middle_mouse_zoom;
+ UI::Widget::PrefSpinButton _steps_rotate;
+
+ UI::Widget::PrefRadioButton _t_sel_trans_obj;
+ UI::Widget::PrefRadioButton _t_sel_trans_outl;
+ UI::Widget::PrefRadioButton _t_sel_cue_none;
+ UI::Widget::PrefRadioButton _t_sel_cue_mark;
+ UI::Widget::PrefRadioButton _t_sel_cue_box;
+ UI::Widget::PrefRadioButton _t_bbox_visual;
+ UI::Widget::PrefRadioButton _t_bbox_geometric;
+
+ UI::Widget::PrefCheckButton _t_cvg_keep_objects;
+ UI::Widget::PrefCheckButton _t_cvg_convert_whole_groups;
+ UI::Widget::PrefCheckButton _t_node_show_outline;
+ UI::Widget::PrefCheckButton _t_node_live_outline;
+ UI::Widget::PrefCheckButton _t_node_live_objects;
+ UI::Widget::PrefCheckButton _t_node_pathflash_enabled;
+ UI::Widget::PrefCheckButton _t_node_pathflash_selected;
+ UI::Widget::PrefSpinButton _t_node_pathflash_timeout;
+ UI::Widget::PrefCheckButton _t_node_show_path_direction;
+ UI::Widget::PrefCheckButton _t_node_single_node_transform_handles;
+ UI::Widget::PrefCheckButton _t_node_delete_preserves_shape;
+ UI::Widget::PrefColorPicker _t_node_pathoutline_color;
+
+ // Command Palette
+ UI::Widget::PrefCheckButton _cp_show_full_action_name;
+ UI::Widget::PrefCheckButton _cp_show_untranslated_name;
+
+ UI::Widget::PrefCombo _gtk_theme;
+ UI::Widget::PrefOpenFolder _sys_user_themes_dir_copy;
+ UI::Widget::PrefOpenFolder _sys_user_icons_dir_copy;
+ UI::Widget::PrefCombo _icon_theme;
+ UI::Widget::PrefCheckButton _dark_theme;
+ UI::Widget::PrefSlider _contrast_theme;
+ UI::Widget::PrefCheckButton _narrow_spinbutton;
+ UI::Widget::PrefCheckButton _compact_colorselector;
+ UI::Widget::PrefCheckButton _symbolic_icons;
+ UI::Widget::PrefCheckButton _symbolic_base_colors;
+ UI::Widget::PrefCheckButton _symbolic_highlight_colors;
+ UI::Widget::PrefColorPicker _symbolic_base_color;
+ UI::Widget::PrefColorPicker _symbolic_warning_color;
+ UI::Widget::PrefColorPicker _symbolic_error_color;
+ UI::Widget::PrefColorPicker _symbolic_success_color;
+ /* Gtk::Image *_complementary_colors; */
+ UI::Widget::PrefCombo _misc_small_toolbar;
+ UI::Widget::PrefCombo _misc_small_secondary;
+ UI::Widget::PrefCombo _misc_small_tools;
+ UI::Widget::PrefCombo _menu_icons;
+ UI::Widget::PrefCheckButton _shift_icons;
+
+ UI::Widget::PrefRadioButton _win_dockable;
+ UI::Widget::PrefRadioButton _win_floating;
+ UI::Widget::PrefRadioButton _win_native;
+ UI::Widget::PrefRadioButton _win_gtk;
+ UI::Widget::PrefRadioButton _win_save_dialog_pos_on;
+ UI::Widget::PrefRadioButton _win_save_dialog_pos_off;
+ UI::Widget::PrefCombo _win_default_size;
+ UI::Widget::PrefRadioButton _win_ontop_none;
+ UI::Widget::PrefRadioButton _win_ontop_normal;
+ UI::Widget::PrefRadioButton _win_ontop_agressive;
+ UI::Widget::PrefRadioButton _win_dialogs_labels_auto;
+ UI::Widget::PrefRadioButton _win_dialogs_labels_active;
+ UI::Widget::PrefRadioButton _win_dialogs_labels_off;
+ UI::Widget::PrefRadioButton _win_save_geom_off;
+ UI::Widget::PrefRadioButton _win_save_geom;
+ UI::Widget::PrefRadioButton _win_save_geom_prefs;
+ UI::Widget::PrefCheckButton _win_show_boot;
+ UI::Widget::PrefCheckButton _win_hide_task;
+ UI::Widget::PrefCheckButton _win_save_viewport;
+ UI::Widget::PrefCheckButton _win_zoom_resize;
+
+ UI::Widget::PrefCheckButton _pencil_average_all_sketches;
+
+ UI::Widget::PrefCheckButton _calligrapy_keep_selected;
+
+ UI::Widget::PrefCheckButton _connector_ignore_text;
+
+ UI::Widget::PrefRadioButton _clone_option_parallel;
+ UI::Widget::PrefRadioButton _clone_option_stay;
+ UI::Widget::PrefRadioButton _clone_option_transform;
+ UI::Widget::PrefRadioButton _clone_option_unlink;
+ UI::Widget::PrefRadioButton _clone_option_delete;
+ UI::Widget::PrefCheckButton _clone_relink_on_duplicate;
+ UI::Widget::PrefCheckButton _clone_to_curves;
+ UI::Widget::PrefCheckButton _clone_ignore_to_curves;
+
+ UI::Widget::PrefCheckButton _mask_mask_on_top;
+ UI::Widget::PrefCheckButton _mask_mask_remove;
+ UI::Widget::PrefCheckButton _mask_mask_on_ungroup;
+ UI::Widget::PrefRadioButton _mask_grouping_none;
+ UI::Widget::PrefRadioButton _mask_grouping_separate;
+ UI::Widget::PrefRadioButton _mask_grouping_all;
+ UI::Widget::PrefCheckButton _mask_ungrouping;
+
+ UI::Widget::PrefSpinButton _filter_multi_threaded;
+ UI::Widget::PrefSpinButton _rendering_cache_size;
+ UI::Widget::PrefSpinButton _rendering_xray_radius;
+ UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity;
+ UI::Widget::PrefCombo _canvas_update_strategy;
+ UI::Widget::PrefCheckButton _canvas_request_opengl;
+ UI::Widget::PrefRadioButton _blur_quality_best;
+ UI::Widget::PrefRadioButton _blur_quality_better;
+ UI::Widget::PrefRadioButton _blur_quality_normal;
+ UI::Widget::PrefRadioButton _blur_quality_worse;
+ UI::Widget::PrefRadioButton _blur_quality_worst;
+ UI::Widget::PrefRadioButton _filter_quality_best;
+ UI::Widget::PrefRadioButton _filter_quality_better;
+ UI::Widget::PrefRadioButton _filter_quality_normal;
+ UI::Widget::PrefRadioButton _filter_quality_worse;
+ UI::Widget::PrefRadioButton _filter_quality_worst;
+#ifdef CAIRO_HAS_DITHER
+ UI::Widget::PrefCheckButton _cairo_dithering;
+#endif
+
+ UI::Widget::PrefCheckButton _canvas_developer_mode_enabled;
+ UI::Widget::PrefSpinButton _canvas_tile_size;
+ UI::Widget::PrefSpinButton _canvas_render_time_limit;
+ UI::Widget::PrefCheckButton _canvas_block_updates;
+ UI::Widget::PrefCombo _canvas_pixelstreamer_method;
+ UI::Widget::PrefSpinButton _canvas_padding;
+ UI::Widget::PrefSpinButton _canvas_prerender;
+ UI::Widget::PrefSpinButton _canvas_preempt;
+ UI::Widget::PrefSpinButton _canvas_coarsener_min_size;
+ UI::Widget::PrefSpinButton _canvas_coarsener_glue_size;
+ UI::Widget::PrefSpinButton _canvas_coarsener_min_fullness;
+ UI::Widget::PrefCheckButton _canvas_debug_framecheck;
+ UI::Widget::PrefCheckButton _canvas_debug_logging;
+ UI::Widget::PrefCheckButton _canvas_debug_delay_redraw;
+ UI::Widget::PrefSpinButton _canvas_debug_delay_redraw_time;
+ UI::Widget::PrefCheckButton _canvas_debug_show_redraw;
+ UI::Widget::PrefCheckButton _canvas_debug_show_unclean;
+ UI::Widget::PrefCheckButton _canvas_debug_show_snapshot;
+ UI::Widget::PrefCheckButton _canvas_debug_show_clean;
+ UI::Widget::PrefCheckButton _canvas_debug_disable_redraw;
+ UI::Widget::PrefCheckButton _canvas_debug_sticky_decoupled;
+ UI::Widget::PrefCheckButton _canvas_debug_animate;
+
+ UI::Widget::PrefCheckButton _trans_scale_stroke;
+ UI::Widget::PrefCheckButton _trans_scale_corner;
+ UI::Widget::PrefCheckButton _trans_gradient;
+ UI::Widget::PrefCheckButton _trans_pattern;
+ UI::Widget::PrefCheckButton _trans_dash_scale;
+ UI::Widget::PrefRadioButton _trans_optimized;
+ UI::Widget::PrefRadioButton _trans_preserved;
+
+ UI::Widget::PrefRadioButton _sel_all;
+ UI::Widget::PrefRadioButton _sel_current;
+ UI::Widget::PrefRadioButton _sel_recursive;
+ UI::Widget::PrefCheckButton _sel_hidden;
+ UI::Widget::PrefCheckButton _sel_locked;
+ UI::Widget::PrefCheckButton _sel_inlayer_same;
+ UI::Widget::PrefCheckButton _sel_touch_topmost_only;
+ UI::Widget::PrefCheckButton _sel_layer_deselects;
+ UI::Widget::PrefCheckButton _sel_cycle;
+ UI::Widget::PrefCheckButton _sel_zero_opacity;
+
+ UI::Widget::PrefCheckButton _markers_color_stock;
+ UI::Widget::PrefCheckButton _markers_color_custom;
+ UI::Widget::PrefCheckButton _markers_color_update;
+
+ UI::Widget::PrefRadioButton _clipboard_style_computed;
+ UI::Widget::PrefRadioButton _clipboard_style_verbatim;
+
+ UI::Widget::PrefCheckButton _cleanup_swatches;
+
+ UI::Widget::PrefCheckButton _lpe_copy_mirroricons;
+ UI::Widget::PrefCheckButton _lpe_show_experimental;
+ UI::Widget::PrefCheckButton _lpe_show_gallery;
+
+ UI::Widget::PrefSpinButton _importexport_export_res;
+ UI::Widget::PrefSpinButton _importexport_import_res;
+ UI::Widget::PrefCheckButton _importexport_import_res_override;
+ UI::Widget::PrefCheckButton _rendering_image_outline;
+ UI::Widget::PrefSlider _snap_delay;
+ UI::Widget::PrefSlider _snap_weight;
+ UI::Widget::PrefSlider _snap_persistence;
+ UI::Widget::PrefEntry _font_sample;
+ UI::Widget::PrefCheckButton _font_dialog;
+ UI::Widget::PrefCombo _font_unit_type;
+ UI::Widget::PrefCheckButton _font_output_px;
+ UI::Widget::PrefCheckButton _font_fontsdir_system;
+ UI::Widget::PrefCheckButton _font_fontsdir_user;
+ UI::Widget::PrefMultiEntry _font_fontdirs_custom;
+
+ UI::Widget::PrefCheckButton _misc_comment;
+ UI::Widget::PrefCheckButton _misc_default_metadata;
+ UI::Widget::PrefCheckButton _export_all_extensions;
+ UI::Widget::PrefCheckButton _misc_forkvectors;
+ UI::Widget::PrefSpinButton _misc_gradientangle;
+ UI::Widget::PrefSpinButton _recently_used_fonts_size;
+ UI::Widget::PrefCheckButton _misc_gradient_collect;
+ UI::Widget::PrefCheckButton _misc_scripts;
+
+ // System page
+ UI::Widget::PrefSpinButton _misc_simpl;
+ Gtk::Entry _sys_user_prefs;
+ Gtk::Entry _sys_tmp_files;
+ Gtk::Entry _sys_extension_dir;
+ UI::Widget::PrefOpenFolder _sys_user_config;
+ UI::Widget::PrefOpenFolder _sys_user_extension_dir;
+ UI::Widget::PrefOpenFolder _sys_user_themes_dir;
+ UI::Widget::PrefOpenFolder _sys_user_ui_dir;
+ UI::Widget::PrefOpenFolder _sys_user_fonts_dir;
+ UI::Widget::PrefOpenFolder _sys_user_icons_dir;
+ UI::Widget::PrefOpenFolder _sys_user_keys_dir;
+ UI::Widget::PrefOpenFolder _sys_user_palettes_dir;
+ UI::Widget::PrefOpenFolder _sys_user_templates_dir;
+ UI::Widget::PrefOpenFolder _sys_user_symbols_dir;
+ UI::Widget::PrefOpenFolder _sys_user_paint_servers_dir;
+ UI::Widget::PrefMultiEntry _sys_fontdirs_custom;
+ UI::Widget::PrefEntryFile _sys_shared_path;
+ Gtk::Entry _sys_user_cache;
+ Gtk::Entry _sys_data;
+ Gtk::TextView _sys_icon;
+ Gtk::ScrolledWindow _sys_icon_scroll;
+ Gtk::TextView _sys_systemdata;
+ Gtk::ScrolledWindow _sys_systemdata_scroll;
+
+ // UI page
+ UI::Widget::PrefCombo _ui_languages;
+ UI::Widget::PrefCheckButton _ui_colorsliders_top;
+ UI::Widget::PrefSpinButton _misc_recent;
+ UI::Widget::PrefCheckButton _ui_rulersel;
+ UI::Widget::PrefCheckButton _ui_realworldzoom;
+ UI::Widget::PrefCheckButton _ui_pageorigin;
+ UI::Widget::PrefCheckButton _ui_partialdynamic;
+ UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction;
+ UI::Widget::PrefCheckButton _ui_yaxisdown;
+ UI::Widget::PrefCheckButton _ui_rotationlock;
+ UI::Widget::PrefCheckButton _ui_cursorscaling;
+ UI::Widget::PrefCheckButton _ui_cursor_shadow;
+
+ //Spellcheck
+ UI::Widget::PrefCombo _spell_language;
+ UI::Widget::PrefCombo _spell_language2;
+ UI::Widget::PrefCombo _spell_language3;
+ UI::Widget::PrefCheckButton _spell_ignorenumbers;
+ UI::Widget::PrefCheckButton _spell_ignoreallcaps;
+
+ // Bitmaps
+ UI::Widget::PrefCombo _misc_overs_bitmap;
+ UI::Widget::PrefEntryFileButtonHBox _misc_bitmap_editor;
+ UI::Widget::PrefEntryFileButtonHBox _misc_svg_editor;
+ UI::Widget::PrefCheckButton _misc_bitmap_autoreload;
+ UI::Widget::PrefSpinButton _bitmap_copy_res;
+ UI::Widget::PrefCheckButton _bitmap_ask;
+ UI::Widget::PrefCheckButton _svg_ask;
+ UI::Widget::PrefCombo _bitmap_link;
+ UI::Widget::PrefCombo _svg_link;
+ UI::Widget::PrefCombo _bitmap_scale;
+ UI::Widget::PrefSpinButton _bitmap_import_quality;
+
+ UI::Widget::PrefEntry _kb_search;
+ UI::Widget::PrefCombo _kb_filelist;
+
+ UI::Widget::PrefCheckButton _save_use_current_dir;
+ UI::Widget::PrefCheckButton _save_autosave_enable;
+ UI::Widget::PrefSpinButton _save_autosave_interval;
+ UI::Widget::PrefEntry _save_autosave_path;
+ UI::Widget::PrefSpinButton _save_autosave_max;
+
+ Gtk::ComboBoxText _cms_display_profile;
+ UI::Widget::PrefCheckButton _cms_from_display;
+ UI::Widget::PrefCombo _cms_intent;
+
+ UI::Widget::PrefCheckButton _cms_softproof;
+ UI::Widget::PrefCheckButton _cms_gamutwarn;
+ Gtk::ColorButton _cms_gamutcolor;
+ Gtk::ComboBoxText _cms_proof_profile;
+ UI::Widget::PrefCombo _cms_proof_intent;
+ UI::Widget::PrefCheckButton _cms_proof_blackpoint;
+
+ Gtk::Notebook _grids_notebook;
+ UI::Widget::PrefRadioButton _grids_no_emphasize_on_zoom;
+ UI::Widget::PrefRadioButton _grids_emphasize_on_zoom;
+ UI::Widget::DialogPage _grids_xy;
+ UI::Widget::DialogPage _grids_axonom;
+ // CanvasXYGrid properties:
+ UI::Widget::PrefUnit _grids_xy_units;
+ UI::Widget::PrefSpinButton _grids_xy_origin_x;
+ UI::Widget::PrefSpinButton _grids_xy_origin_y;
+ UI::Widget::PrefSpinButton _grids_xy_spacing_x;
+ UI::Widget::PrefSpinButton _grids_xy_spacing_y;
+ UI::Widget::PrefColorPicker _grids_xy_color;
+ UI::Widget::PrefColorPicker _grids_xy_empcolor;
+ UI::Widget::PrefSpinButton _grids_xy_empspacing;
+ UI::Widget::PrefCheckButton _grids_xy_dotted;
+ // CanvasAxonomGrid properties:
+ UI::Widget::PrefUnit _grids_axonom_units;
+ UI::Widget::PrefSpinButton _grids_axonom_origin_x;
+ UI::Widget::PrefSpinButton _grids_axonom_origin_y;
+ UI::Widget::PrefSpinButton _grids_axonom_spacing_y;
+ UI::Widget::PrefSpinButton _grids_axonom_angle_x;
+ UI::Widget::PrefSpinButton _grids_axonom_angle_z;
+ UI::Widget::PrefColorPicker _grids_axonom_color;
+ UI::Widget::PrefColorPicker _grids_axonom_empcolor;
+ UI::Widget::PrefSpinButton _grids_axonom_empspacing;
+
+ // SVG Output page:
+ UI::Widget::PrefCheckButton _svgoutput_usenamedcolors;
+ UI::Widget::PrefCheckButton _svgoutput_usesodipodiabsref;
+ UI::Widget::PrefSpinButton _svgoutput_numericprecision;
+ UI::Widget::PrefSpinButton _svgoutput_minimumexponent;
+ UI::Widget::PrefCheckButton _svgoutput_inlineattrs;
+ UI::Widget::PrefSpinButton _svgoutput_indent;
+ UI::Widget::PrefCombo _svgoutput_pathformat;
+ UI::Widget::PrefCheckButton _svgoutput_forcerepeatcommands;
+
+ // Attribute Checking controls for SVG Output page:
+ UI::Widget::PrefCheckButton _svgoutput_attrwarn;
+ UI::Widget::PrefCheckButton _svgoutput_attrremove;
+ UI::Widget::PrefCheckButton _svgoutput_stylepropwarn;
+ UI::Widget::PrefCheckButton _svgoutput_stylepropremove;
+ UI::Widget::PrefCheckButton _svgoutput_styledefaultswarn;
+ UI::Widget::PrefCheckButton _svgoutput_styledefaultsremove;
+ UI::Widget::PrefCheckButton _svgoutput_check_reading;
+ UI::Widget::PrefCheckButton _svgoutput_check_editing;
+ UI::Widget::PrefCheckButton _svgoutput_check_writing;
+
+ // SVG Output export:
+ UI::Widget::PrefCheckButton _svgexport_insert_text_fallback;
+ UI::Widget::PrefCheckButton _svgexport_insert_mesh_polyfill;
+ UI::Widget::PrefCheckButton _svgexport_insert_hatch_polyfill;
+ UI::Widget::PrefCheckButton _svgexport_remove_marker_auto_start_reverse;
+ UI::Widget::PrefCheckButton _svgexport_remove_marker_context_paint;
+
+
+ Gtk::Notebook _kb_notebook;
+ UI::Widget::DialogPage _kb_page_shortcuts;
+ UI::Widget::DialogPage _kb_page_modifiers;
+ gboolean _kb_shortcuts_loaded;
+ /*
+ * Keyboard shortcut members
+ */
+ Glib::RefPtr<Gtk::TreeStore> _kb_store;
+ Gtk::TreeView _kb_tree;
+ Gtk::CellRendererAccel _kb_shortcut_renderer;
+ Glib::RefPtr<Gtk::TreeModelFilter> _kb_filter;
+
+ /*
+ * Keyboard modifiers interface
+ */
+ class ModifierColumns: public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModifierColumns() {
+ add(name);
+ add(id);
+ add(description);
+ add(and_modifiers);
+ add(user_set);
+ }
+ ~ModifierColumns() override = default;
+
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> description;
+ Gtk::TreeModelColumn<Glib::ustring> and_modifiers;
+ Gtk::TreeModelColumn<unsigned int> user_set;
+ Gtk::TreeModelColumn<unsigned int> is_enabled;
+ };
+ ModifierColumns _mod_columns;
+ Glib::RefPtr<Gtk::TreeStore> _mod_store;
+ Gtk::TreeView _mod_tree;
+ Gtk::ToggleButton _kb_mod_ctrl;
+ Gtk::ToggleButton _kb_mod_shift;
+ Gtk::ToggleButton _kb_mod_alt;
+ Gtk::ToggleButton _kb_mod_meta;
+ Gtk::CheckButton _kb_mod_enabled;
+ bool _kb_is_updated;
+
+ int _minimum_width;
+ int _minimum_height;
+ int _natural_width;
+ int _natural_height;
+ bool GetSizeRequest(const Gtk::TreeModel::iterator& iter);
+ void get_preferred_width_vfunc (int& minimum_width, int& natural_width) const override {
+ minimum_width = _minimum_width;
+ natural_width = _natural_width;
+ }
+ void get_preferred_width_for_height_vfunc (int height, int& minimum_width, int& natural_width) const override {
+ minimum_width = _minimum_width;
+ natural_width = _natural_width;
+ }
+ void get_preferred_height_vfunc (int& minimum_height, int& natural_height) const override {
+ minimum_height = _minimum_height;
+ natural_height = _natural_height;
+ }
+ void get_preferred_height_for_width_vfunc (int width, int& minimum_height, int& natural_height) const override {
+ minimum_height = _minimum_height;
+ natural_height = _natural_height;
+ }
+ int _sb_width;
+ UI::Widget::DialogPage* _current_page;
+
+ Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, int id);
+ Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id);
+ Gtk::TreePath get_next_result(Gtk::TreeIter& iter, bool check_children = true);
+ Gtk::TreePath get_prev_result(Gtk::TreeIter& iter, bool iterate = true);
+ bool matchPage(const Gtk::TreeModel::iterator& iter);
+
+ static void AddSelcueCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value);
+ static void AddGradientCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value);
+ static void AddConvertGuidesCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value);
+ static void AddFirstAndLastCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value);
+ static void AddDotSizeSpinbutton(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, double def_value);
+ static void AddBaseSimplifySpinbutton(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, double def_value);
+ static void AddNewObjectsStyle(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, const gchar* banner = nullptr);
+
+ void on_pagelist_selection_changed();
+ void show_not_found();
+ void show_nothing_on_page();
+ void show_try_search();
+ void on_reset_open_recent_clicked();
+ void on_reset_prefs_clicked();
+ void on_search_changed();
+ void highlight_results(Glib::ustring const &key, Gtk::TreeModel::iterator &iter);
+ void goto_first_result();
+
+ void get_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget);
+ int num_widgets_in_grid(Glib::ustring const &key, Gtk::Widget *widget);
+ void remove_highlight(Gtk::Label *label);
+ void add_highlight(Gtk::Label *label, Glib::ustring const &key);
+
+ bool recursive_filter(Glib::ustring &key, Gtk::TreeModel::const_iterator const &row);
+ bool on_navigate_key_press(GdkEventKey *evt);
+
+ void initPageTools();
+ void initPageUI();
+ void initPageBehavior();
+ void initPageIO();
+
+ void initPageRendering();
+ void initPageSpellcheck();
+ void initPageBitmaps();
+ void initPageSystem();
+ void initPageI18n(); // Do we still need it?
+ void initKeyboardShortcuts(Gtk::TreeModel::iterator iter_ui);
+
+ /*
+ * Functions for the Keyboard shortcut editor panel
+ */
+ void onKBReset();
+ void onKBImport();
+ void onKBExport();
+ void onKBList();
+ void onKBRealize();
+ void onKBListKeyboardShortcuts();
+ void onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode);
+ void onKBTreeCleared(const Glib::ustring& path_string);
+ bool onKBSearchKeyEvent(GdkEventKey *event);
+ bool onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter);
+ static void onKBShortcutRenderer(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter);
+ void on_modifier_selection_changed();
+ void on_modifier_enabled();
+ void on_modifier_edited();
+
+private:
+ Gtk::TreeModel::iterator searchRows(char const* srch, Gtk::TreeModel::iterator& iter, Gtk::TreeModel::Children list_model_childern);
+ void themeChange(bool contrastslider = false);
+ void comboThemeChange();
+ void contrastThemeChange();
+ void preferDarkThemeChange();
+ bool contrastChange(GdkEventButton* button_event);
+ void symbolicThemeCheck();
+ void toggleSymbolic();
+ void changeIconsColors();
+ void resetIconsColors(bool themechange = false);
+ void resetIconsColorsWrapper();
+ void changeIconsColor(guint32 /*color*/);
+ void get_highlight_colors(guint32 &colorsetbase, guint32 &colorsetsuccess, guint32 &colorsetwarning,
+ guint32 &colorseterror);
+
+ std::map<Glib::ustring, bool> dark_themes;
+ bool _init;
+ Inkscape::PrefObserver _theme_oberver;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif //INKSCAPE_UI_DIALOG_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/dialog/input.cpp b/src/ui/dialog/input.cpp
new file mode 100644
index 0000000..494843a
--- /dev/null
+++ b/src/ui/dialog/input.cpp
@@ -0,0 +1,1792 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Input devices dialog (new) - implementation.
+ */
+/* Author:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2008 Author
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <map>
+#include <set>
+#include <list>
+#include "ui/widget/frame.h"
+#include "ui/widget/scrollprotected.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/cellrenderercombo.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menubar.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/progressbar.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/eventbox.h>
+
+#include "device-manager.h"
+#include "preferences.h"
+
+#include "input.h"
+
+// clang-format off
+/* XPM */
+static char const * core_xpm[] = {
+"16 16 4 1",
+" c None",
+". c #808080",
+"+ c #000000",
+"@ c #FFFFFF",
+" ",
+" ",
+" ",
+" .++++++. ",
+" +@+@@+@+ ",
+" +@+@@+@+ ",
+" +.+..+.+ ",
+" +@@@@@@+ ",
+" +@@@@@@+ ",
+" +@@@@@@+ ",
+" +@@@@@@+ ",
+" +@@@@@@+ ",
+" .++++++. ",
+" ",
+" ",
+" "};
+
+/* XPM */
+static char const *eraser[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 5 1",
+" c black",
+". c green",
+"X c #808080",
+"o c gray100",
+"O c None",
+/* pixels */
+"OOOOOOOOOOOOOOOO",
+"OOOOOOOOOOOOO OO",
+"OOOOOOOOOOOO . O",
+"OOOOOOOOOOO . OO",
+"OOOOOOOOOO . OOO",
+"OOOOOOOOO . OOOO",
+"OOOOOOOO . OOOOO",
+"OOOOOOOXo OOOOOO",
+"OOOOOOXoXOOOOOOO",
+"OOOOOXoXOOOOOOOO",
+"OOOOXoXOOOOOOOOO",
+"OOOXoXOOOOOOOOOO",
+"OOXoXOOOOOOOOOOO",
+"OOXXOOOOOOOOOOOO",
+"OOOOOOOOOOOOOOOO",
+"OOOOOOOOOOOOOOOO"
+};
+
+/* XPM */
+static char const *mouse[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 3 1",
+" c black",
+". c gray100",
+"X c None",
+/* pixels */
+"XXXXXXXXXXXXXXXX",
+"XXXXXXXXXXXXXXXX",
+"XXXXXXXXXXXXXXXX",
+"XXXXXXXXXXXXXXXX",
+"XXXXXXX XXXXXXX",
+"XXXXX . XXXXXXX",
+"XXXX .... XXXXXX",
+"XXXX .... XXXXXX",
+"XXXXX .... XXXXX",
+"XXXXX .... XXXXX",
+"XXXXXX .... XXXX",
+"XXXXXX .... XXXX",
+"XXXXXXX . XXXXX",
+"XXXXXXX XXXXXXX",
+"XXXXXXXXXXXXXXXX",
+"XXXXXXXXXXXXXXXX"
+};
+
+/* XPM */
+static char const *pen[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 3 1",
+" c black",
+". c gray100",
+"X c None",
+/* pixels */
+"XXXXXXXXXXXXXXXX",
+"XXXXXXXXXXXXX XX",
+"XXXXXXXXXXXX . X",
+"XXXXXXXXXXX . XX",
+"XXXXXXXXXX . XXX",
+"XXXXXXXXX . XXXX",
+"XXXXXXXX . XXXXX",
+"XXXXXXX . XXXXXX",
+"XXXXXX . XXXXXXX",
+"XXXXX . XXXXXXXX",
+"XXXX . XXXXXXXXX",
+"XXX . XXXXXXXXXX",
+"XX . XXXXXXXXXXX",
+"XX XXXXXXXXXXXX",
+"XXXXXXXXXXXXXXXX",
+"XXXXXXXXXXXXXXXX"
+};
+
+/* XPM */
+static char const *sidebuttons[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 4 1",
+" c black",
+". c #808080",
+"o c green",
+"O c None",
+/* pixels */
+"OOOOOOOOOOOOOOOO",
+"OOOOOOOOOOOOOOOO",
+"O..............O",
+"O.OOOOOOOOOOOO.O",
+"O OOOOOOOO O",
+"O o OOOOOOOO o O",
+"O o OOOOOOOO o O",
+"O OOOOOOOO O",
+"O.OOOOOOOOOOOO.O",
+"O.OOOOOOOOOOOO.O",
+"O.OOOOOOOOOOOO.O",
+"O.OOOOOOOOOOOO.O",
+"O.OOOOOOOOOOOO.O",
+"O..............O",
+"OOOOOOOOOOOOOOOO",
+"OOOOOOOOOOOOOOOO"
+};
+
+/* XPM */
+static char const *tablet[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 3 1",
+" c black",
+". c gray100",
+"X c None",
+/* pixels */
+"XXXXXXXXXXXXXXXX",
+"XXXXXXXXXXXXXXXX",
+"X X",
+"X ............ X",
+"X ............ X",
+"X ............ X",
+"X ............ X",
+"X ............ X",
+"X ............ X",
+"X ............ X",
+"X ............ X",
+"X ............ X",
+"X ............ X",
+"X X",
+"XXXXXXXXXXXXXXXX",
+"XXXXXXXXXXXXXXXX"
+};
+
+/* XPM */
+static char const *tip[] = {
+/* columns rows colors chars-per-pixel */
+"16 16 5 1",
+" c black",
+". c green",
+"X c #808080",
+"o c gray100",
+"O c None",
+/* pixels */
+"OOOOOOOOOOOOOOOO",
+"OOOOOOOOOOOOOXOO",
+"OOOOOOOOOOOOXoXO",
+"OOOOOOOOOOOXoXOO",
+"OOOOOOOOOOXoXOOO",
+"OOOOOOOOOXoXOOOO",
+"OOOOOOOOXoXOOOOO",
+"OOOOOOO oXOOOOOO",
+"OOOOOO . OOOOOOO",
+"OOOOO . OOOOOOOO",
+"OOOO . OOOOOOOOO",
+"OOO . OOOOOOOOOO",
+"OO . OOOOOOOOOOO",
+"OO OOOOOOOOOOOO",
+"OOOOXXXXXOOOOOOO",
+"OOOOOOOOOXXXXXOO"
+};
+
+/* XPM */
+static char const *button_none[] = {
+/* columns rows colors chars-per-pixel */
+"8 8 3 1",
+" c black",
+". c #808080",
+"X c None",
+/* pixels */
+"XXXXXXXX",
+"XX .. XX",
+"X .XX. X",
+"X.XX X.X",
+"X.X XX.X",
+"X .XX. X",
+"XX .. XX",
+"XXXXXXXX"
+};
+/* XPM */
+static char const *button_off[] = {
+/* columns rows colors chars-per-pixel */
+"8 8 4 1",
+" c black",
+". c #808080",
+"X c gray100",
+"o c None",
+/* pixels */
+"oooooooo",
+"oo. .oo",
+"o. XX .o",
+"o XXXX o",
+"o XXXX o",
+"o. XX .o",
+"oo. .oo",
+"oooooooo"
+};
+/* XPM */
+static char const *button_on[] = {
+/* columns rows colors chars-per-pixel */
+"8 8 3 1",
+" c black",
+". c green",
+"X c None",
+/* pixels */
+"XXXXXXXX",
+"XX XX",
+"X .. X",
+"X .... X",
+"X .... X",
+"X .. X",
+"XX XX",
+"XXXXXXXX"
+};
+
+/* XPM */
+static char const * axis_none_xpm[] = {
+"24 8 3 1",
+" c None",
+". c #000000",
+"+ c #808080",
+" ",
+" .++++++++++++++++++. ",
+" .+ . .+. ",
+" + . . . + ",
+" + . . . + ",
+" .+. . +. ",
+" .++++++++++++++++++. ",
+" "};
+/* XPM */
+static char const * axis_off_xpm[] = {
+"24 8 4 1",
+" c None",
+". c #808080",
+"+ c #000000",
+"@ c #FFFFFF",
+" ",
+" .++++++++++++++++++. ",
+" .+@@@@@@@@@@@@@@@@@@+. ",
+" +@@@@@@@@@@@@@@@@@@@@+ ",
+" +@@@@@@@@@@@@@@@@@@@@+ ",
+" .+@@@@@@@@@@@@@@@@@@+. ",
+" .++++++++++++++++++. ",
+" "};
+/* XPM */
+static char const * axis_on_xpm[] = {
+"24 8 3 1",
+" c None",
+". c #000000",
+"+ c #00FF00",
+" ",
+" .................... ",
+" ..++++++++++++++++++.. ",
+" .++++++++++++++++++++. ",
+" .++++++++++++++++++++. ",
+" ..++++++++++++++++++.. ",
+" .................... ",
+" "};
+// clang-format on
+
+using Inkscape::InputDevice;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+
+class DeviceModelColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ Gtk::TreeModelColumn<bool> toggler;
+ Gtk::TreeModelColumn<Glib::ustring> expander;
+ Gtk::TreeModelColumn<Glib::ustring> description;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > thumbnail;
+ Gtk::TreeModelColumn<Glib::RefPtr<InputDevice const> > device;
+ Gtk::TreeModelColumn<Gdk::InputMode> mode;
+
+ DeviceModelColumns() { add(toggler), add(expander), add(description); add(thumbnail); add(device); add(mode); }
+};
+
+static std::map<Gdk::InputMode, Glib::ustring> &getModeToString()
+{
+ static std::map<Gdk::InputMode, Glib::ustring> mapping;
+ if (mapping.empty()) {
+ mapping[Gdk::MODE_DISABLED] = _("Disabled");
+ mapping[Gdk::MODE_SCREEN] = C_("Input device", "Screen");
+ mapping[Gdk::MODE_WINDOW] = _("Window");
+ }
+
+ return mapping;
+}
+
+static int getModeId(Gdk::InputMode im)
+{
+ if (im == Gdk::MODE_DISABLED) return 0;
+ if (im == Gdk::MODE_SCREEN) return 1;
+ if (im == Gdk::MODE_WINDOW) return 2;
+
+ return 0;
+}
+
+static std::map<Glib::ustring, Gdk::InputMode> &getStringToMode()
+{
+ static std::map<Glib::ustring, Gdk::InputMode> mapping;
+ if (mapping.empty()) {
+ mapping[_("Disabled")] = Gdk::MODE_DISABLED;
+ mapping[_("Screen")] = Gdk::MODE_SCREEN;
+ mapping[_("Window")] = Gdk::MODE_WINDOW;
+ }
+
+ return mapping;
+}
+
+
+
+class InputDialogImpl : public InputDialog {
+public:
+ InputDialogImpl();
+ ~InputDialogImpl() override = default;
+
+private:
+ class ConfPanel : public Gtk::Box
+ {
+ public:
+ ConfPanel();
+ ~ConfPanel() override;
+
+ class Blink : public Preferences::Observer
+ {
+ public:
+ Blink(ConfPanel &parent);
+ ~Blink() override;
+ void notify(Preferences::Entry const &new_val) override;
+
+ ConfPanel &parent;
+ };
+
+ static void commitCellModeChange(Glib::ustring const &path, Glib::ustring const &newText, Glib::RefPtr<Gtk::TreeStore> store);
+ static void setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter);
+
+ static void commitCellStateChange(Glib::ustring const &path, Glib::RefPtr<Gtk::TreeStore> store);
+ static void setCellStateToggle(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter);
+
+ void saveSettings();
+ void onTreeSelect();
+ void useExtToggled();
+
+ void onModeChange();
+ void setKeys(gint count);
+ void setAxis(gint count);
+
+ Glib::RefPtr<Gtk::TreeStore> confDeviceStore;
+ Gtk::TreeIter confDeviceIter;
+ Gtk::TreeView confDeviceTree;
+ Gtk::ScrolledWindow confDeviceScroller;
+ Blink watcher;
+ Gtk::CheckButton useExt;
+ Gtk::Button save;
+ Gtk::Paned pane;
+ Gtk::Box detailsBox;
+ Gtk::Box titleFrame;
+ Gtk::Label titleLabel;
+ Inkscape::UI::Widget::Frame axisFrame;
+ Inkscape::UI::Widget::Frame keysFrame;
+ Gtk::Box axisVBox;
+ Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> modeCombo;
+ Gtk::Label modeLabel;
+ Gtk::Box modeBox;
+
+ class KeysColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ KeysColumns()
+ {
+ add(name);
+ add(value);
+ }
+ ~KeysColumns() override = default;
+
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> value;
+ };
+
+ KeysColumns keysColumns;
+ KeysColumns axisColumns;
+
+ Glib::RefPtr<Gtk::ListStore> axisStore;
+ Gtk::TreeView axisTree;
+ Gtk::ScrolledWindow axisScroll;
+
+ Glib::RefPtr<Gtk::ListStore> keysStore;
+ Gtk::TreeView keysTree;
+ Gtk::ScrolledWindow keysScroll;
+ Gtk::CellRendererAccel _kb_shortcut_renderer;
+
+
+ };
+
+ static DeviceModelColumns &getCols();
+
+ enum PixId {PIX_CORE, PIX_PEN, PIX_MOUSE, PIX_TIP, PIX_TABLET, PIX_ERASER, PIX_SIDEBUTTONS,
+ PIX_BUTTONS_NONE, PIX_BUTTONS_ON, PIX_BUTTONS_OFF,
+ PIX_AXIS_NONE, PIX_AXIS_ON, PIX_AXIS_OFF};
+
+ static Glib::RefPtr<Gdk::Pixbuf> getPix(PixId id);
+
+ std::map<Glib::ustring, std::set<guint> > buttonMap;
+ std::map<Glib::ustring, std::map<guint, std::pair<guint, gdouble> > > axesMap;
+
+ Gdk::InputSource lastSourceSeen;
+ Glib::ustring lastDevnameSeen;
+
+ Glib::RefPtr<Gtk::TreeStore> deviceStore;
+ Gtk::TreeIter deviceIter;
+ Gtk::TreeView deviceTree;
+ Inkscape::UI::Widget::Frame testFrame;
+ Inkscape::UI::Widget::Frame axisFrame;
+ Gtk::ScrolledWindow treeScroller;
+ Gtk::ScrolledWindow detailScroller;
+ Gtk::Paned splitter;
+ Gtk::Paned split2;
+ Gtk::Label devName;
+ Gtk::Label devKeyCount;
+ Gtk::Label devAxesCount;
+ Gtk::ComboBoxText axesCombo;
+ Gtk::ProgressBar axesValues[6];
+ Gtk::Grid axisTable;
+ Gtk::ComboBoxText buttonCombo;
+ Gtk::ComboBoxText linkCombo;
+ sigc::connection linkConnection;
+ Gtk::Label keyVal;
+ Gtk::Entry keyEntry;
+ Gtk::Notebook topHolder;
+ Gtk::Image testThumb;
+ Gtk::Image testButtons[24];
+ Gtk::Image testAxes[8];
+ Gtk::Grid imageTable;
+ Gtk::EventBox testDetector;
+
+ ConfPanel cfgPanel;
+
+
+ static void setupTree( Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeIter &tablet );
+ void setupValueAndCombo( gint reported, gint actual, Gtk::Label& label, Gtk::ComboBoxText& combo );
+ void updateTestButtons( Glib::ustring const& key, gint hotButton );
+ void updateTestAxes( Glib::ustring const& key, GdkDevice* dev );
+ void mapAxesValues( Glib::ustring const& key, gdouble const * axes, GdkDevice* dev);
+ Glib::ustring getKeyFor( GdkDevice* device );
+ bool eventSnoop(GdkEvent* event);
+ void linkComboChanged();
+ void resyncToSelection();
+ void handleDeviceChange(Glib::RefPtr<InputDevice const> device);
+ void updateDeviceAxes(Glib::RefPtr<InputDevice const> device);
+ void updateDeviceButtons(Glib::RefPtr<InputDevice const> device);
+ static void updateDeviceLinks(Glib::RefPtr<InputDevice const> device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree);
+
+ static bool findDevice(const Gtk::TreeModel::iterator& iter,
+ Glib::ustring id,
+ Gtk::TreeModel::iterator* result);
+ static bool findDeviceByLink(const Gtk::TreeModel::iterator& iter,
+ Glib::ustring link,
+ Gtk::TreeModel::iterator* result);
+
+}; // class InputDialogImpl
+
+
+DeviceModelColumns &InputDialogImpl::getCols()
+{
+ static DeviceModelColumns cols;
+ return cols;
+}
+
+Glib::RefPtr<Gdk::Pixbuf> InputDialogImpl::getPix(PixId id)
+{
+ static std::map<PixId, Glib::RefPtr<Gdk::Pixbuf> > mappings;
+
+ mappings[PIX_CORE] = Gdk::Pixbuf::create_from_xpm_data(core_xpm);
+ mappings[PIX_PEN] = Gdk::Pixbuf::create_from_xpm_data(pen);
+ mappings[PIX_MOUSE] = Gdk::Pixbuf::create_from_xpm_data(mouse);
+ mappings[PIX_TIP] = Gdk::Pixbuf::create_from_xpm_data(tip);
+ mappings[PIX_TABLET] = Gdk::Pixbuf::create_from_xpm_data(tablet);
+ mappings[PIX_ERASER] = Gdk::Pixbuf::create_from_xpm_data(eraser);
+ mappings[PIX_SIDEBUTTONS] = Gdk::Pixbuf::create_from_xpm_data(sidebuttons);
+
+ mappings[PIX_BUTTONS_NONE] = Gdk::Pixbuf::create_from_xpm_data(button_none);
+ mappings[PIX_BUTTONS_ON] = Gdk::Pixbuf::create_from_xpm_data(button_on);
+ mappings[PIX_BUTTONS_OFF] = Gdk::Pixbuf::create_from_xpm_data(button_off);
+
+ mappings[PIX_AXIS_NONE] = Gdk::Pixbuf::create_from_xpm_data(axis_none_xpm);
+ mappings[PIX_AXIS_ON] = Gdk::Pixbuf::create_from_xpm_data(axis_on_xpm);
+ mappings[PIX_AXIS_OFF] = Gdk::Pixbuf::create_from_xpm_data(axis_off_xpm);
+
+ Glib::RefPtr<Gdk::Pixbuf> pix;
+ if (mappings.find(id) != mappings.end()) {
+ pix = mappings[id];
+ }
+
+ return pix;
+}
+
+std::unique_ptr<InputDialog> InputDialog::create()
+{
+ return std::make_unique<InputDialogImpl>();
+}
+
+InputDialogImpl::InputDialogImpl() :
+ InputDialog(),
+ lastSourceSeen(static_cast<Gdk::InputSource>(-1)),
+ lastDevnameSeen(""),
+ deviceStore(Gtk::TreeStore::create(getCols())),
+ deviceIter(),
+ deviceTree(deviceStore),
+ testFrame(_("Test Area")),
+ axisFrame(_("Axis")),
+ treeScroller(),
+ detailScroller(),
+ splitter(),
+ split2(Gtk::ORIENTATION_VERTICAL),
+ axisTable(),
+ linkCombo(),
+ topHolder(),
+ imageTable(),
+ testDetector(),
+ cfgPanel()
+{
+ treeScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ treeScroller.set_shadow_type(Gtk::SHADOW_IN);
+ treeScroller.add(deviceTree);
+ treeScroller.set_size_request(50, 0);
+
+ split2.pack1(axisFrame, false, false);
+ split2.pack2(testFrame, true, true);
+
+ splitter.pack1(treeScroller);
+ splitter.pack2(split2);
+
+ testDetector.add(imageTable);
+ testFrame.add(testDetector);
+ testThumb.set(getPix(PIX_TABLET));
+ testThumb.set_margin_top(24);
+ testThumb.set_margin_bottom(24);
+ testThumb.set_margin_start(24);
+ testThumb.set_margin_end(24);
+ testThumb.set_hexpand();
+ testThumb.set_vexpand();
+ imageTable.attach(testThumb, 0, 0, 8, 1);
+
+ {
+ guint col = 0;
+ guint row = 1;
+ for (auto & testButton : testButtons) {
+ testButton.set(getPix(PIX_BUTTONS_NONE));
+ imageTable.attach(testButton, col, row, 1, 1);
+ col++;
+ if (col > 7) {
+ col = 0;
+ row++;
+ }
+ }
+
+ col = 0;
+ for (auto & testAxe : testAxes) {
+ testAxe.set(getPix(PIX_AXIS_NONE));
+ imageTable.attach(testAxe, col * 2, row, 2, 1);
+ col++;
+ if (col > 3) {
+ col = 0;
+ row++;
+ }
+ }
+ }
+
+
+ // This is a hidden preference to enable the "hardware" details in a separate tab
+ // By default this is not available to users
+ if (Preferences::get()->getBool("/dialogs/inputdevices/test")) {
+ topHolder.append_page(cfgPanel, _("Configuration"));
+ topHolder.append_page(splitter, _("Hardware"));
+ topHolder.show_all();
+ topHolder.set_current_page(0);
+ pack_start(topHolder);
+ } else {
+ pack_start(cfgPanel);
+ }
+
+
+ int rowNum = 0;
+
+ axisFrame.add(axisTable);
+
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(_("Link:")));
+ axisTable.attach(*lbl, 0, rowNum, 1, 1);
+ linkCombo.append(_("None"));
+ linkCombo.set_active_text(_("None"));
+ linkCombo.set_sensitive(false);
+ linkConnection = linkCombo.signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::linkComboChanged));
+ axisTable.attach(linkCombo, 1, rowNum, 1, 1);
+ rowNum++;
+
+ lbl = Gtk::manage(new Gtk::Label(_("Axes count:")));
+ axisTable.attach(*lbl, 0, rowNum, 1, 1);
+ axisTable.attach(devAxesCount, 1, rowNum, 1, 1);
+ rowNum++;
+
+ for (auto & axesValue : axesValues) {
+ lbl = Gtk::manage(new Gtk::Label(_("axis:")));
+ lbl->set_hexpand();
+ axisTable.attach(*lbl, 0, rowNum, 1, 1);
+
+ axesValue.set_hexpand();
+ axisTable.attach(axesValue, 1, rowNum, 1, 1);
+ axesValue.set_sensitive(false);
+
+ rowNum++;
+
+
+ }
+
+ lbl = Gtk::manage(new Gtk::Label(_("Button count:")));
+
+ axisTable.attach(*lbl, 0, rowNum, 1, 1);
+ axisTable.attach(devKeyCount, 1, rowNum, 1, 1);
+
+ rowNum++;
+
+ axisTable.attach(keyVal, 0, rowNum, 2, 1);
+
+ rowNum++;
+
+ testDetector.signal_event().connect(sigc::mem_fun(*this, &InputDialogImpl::eventSnoop));
+
+ // TODO: Extension event stuff has been removed from public API in GTK+ 3
+ // Need to check that this hasn't broken anything
+ testDetector.add_events(Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK |Gdk::PROXIMITY_IN_MASK|Gdk::PROXIMITY_OUT_MASK|Gdk::SCROLL_MASK);
+
+ axisTable.attach(keyEntry, 0, rowNum, 2, 1);
+
+ rowNum++;
+
+
+ axisTable.set_sensitive(false);
+
+//- 16x16/devices
+// gnome-dev-mouse-optical
+// input-mouse
+// input-tablet
+// mouse
+
+ //Add the TreeView's view columns:
+ deviceTree.append_column("I", getCols().thumbnail);
+ deviceTree.append_column("Bar", getCols().description);
+
+ deviceTree.set_enable_tree_lines();
+ deviceTree.set_headers_visible(false);
+ deviceTree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::resyncToSelection));
+
+
+ setupTree( deviceStore, deviceIter );
+
+ Inkscape::DeviceManager::getManager().signalDeviceChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::handleDeviceChange));
+ Inkscape::DeviceManager::getManager().signalAxesChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::updateDeviceAxes));
+ Inkscape::DeviceManager::getManager().signalButtonsChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::updateDeviceButtons));
+ Inkscape::DeviceManager::getManager().signalLinkChanged().connect(sigc::bind(sigc::ptr_fun(&InputDialogImpl::updateDeviceLinks), deviceIter, &deviceTree));
+
+ deviceTree.expand_all();
+ show_all_children();
+}
+
+class TabletTmp {
+public:
+ TabletTmp() = default;
+
+ Glib::ustring name;
+ std::list<Glib::RefPtr<InputDevice const> > devices;
+};
+
+static Glib::ustring getCommon( std::list<Glib::ustring> const &names )
+{
+ Glib::ustring result;
+
+ if ( !names.empty() ) {
+ size_t pos = 0;
+ bool match = true;
+ while ( match ) {
+ if ( names.begin()->length() > pos ) {
+ gunichar ch = (*names.begin())[pos];
+ for (const auto & name : names) {
+ if ( (pos >= name.length())
+ || (name[pos] != ch) ) {
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ result += ch;
+ pos++;
+ }
+ } else {
+ match = false;
+ }
+ }
+ }
+
+ return result;
+}
+
+
+void InputDialogImpl::ConfPanel::onModeChange()
+{
+ Glib::ustring newText = modeCombo.get_active_text();
+
+ Glib::RefPtr<Gtk::TreeSelection> sel = confDeviceTree.get_selection();
+ Gtk::TreeModel::iterator iter = sel->get_selected();
+ if (iter) {
+ Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device];
+ if (dev && (getStringToMode().find(newText) != getStringToMode().end())) {
+ Gdk::InputMode mode = getStringToMode()[newText];
+ Inkscape::DeviceManager::getManager().setMode( dev->getId(), mode );
+ }
+ }
+
+}
+
+
+void InputDialogImpl::setupTree( Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeIter &tablet )
+{
+ std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices();
+ if ( !devList.empty() ) {
+ //Gtk::TreeModel::Row row = *(store->append());
+ //row[getCols().description] = _("Hardware");
+
+ // Let's make some tablets!!!
+ std::list<TabletTmp> tablets;
+ std::set<Glib::ustring> consumed;
+
+ // Phase 1 - figure out which tablets are present
+ for (auto dev : devList) {
+ if ( dev ) {
+ if ( dev->getSource() != Gdk::SOURCE_MOUSE ) {
+ consumed.insert( dev->getId() );
+ if ( tablets.empty() ) {
+ TabletTmp tmp;
+ tablets.push_back(tmp);
+ }
+ tablets.back().devices.push_back(dev);
+ }
+ } else {
+ g_warning("Null device in list");
+ }
+ }
+
+ // Phase 2 - build a UI for the present devices
+ for (auto & it : tablets) {
+ tablet = store->prepend(/*row.children()*/);
+ Gtk::TreeModel::Row childrow = *tablet;
+ if ( it.name.empty() ) {
+ // Check to see if we can derive one
+ std::list<Glib::ustring> names;
+ for (auto & device : it.devices) {
+ names.push_back( device->getName() );
+ }
+ Glib::ustring common = getCommon(names);
+ if ( !common.empty() ) {
+ it.name = common;
+ }
+ }
+ childrow[getCols().description] = it.name.empty() ? _("Tablet") : it.name ;
+ childrow[getCols().thumbnail] = getPix(PIX_TABLET);
+
+ // Check if there is an eraser we can link to a pen
+ for ( std::list<Glib::RefPtr<InputDevice const> >::iterator it2 = it.devices.begin(); it2 != it.devices.end(); ++it2 ) {
+ Glib::RefPtr<InputDevice const> dev = *it2;
+ if ( dev->getSource() == Gdk::SOURCE_PEN ) {
+ for (auto dev2 : it.devices) {
+ if ( dev2->getSource() == Gdk::SOURCE_ERASER ) {
+ DeviceManager::getManager().setLinkedTo(dev->getId(), dev2->getId());
+ break; // only check the first eraser... for now
+ }
+ break; // only check the first pen... for now
+ }
+ }
+ }
+
+ for (auto dev : it.devices) {
+ Gtk::TreeModel::Row deviceRow = *(store->append(childrow.children()));
+ deviceRow[getCols().description] = dev->getName();
+ deviceRow[getCols().device] = dev;
+ deviceRow[getCols().mode] = dev->getMode();
+ switch ( dev->getSource() ) {
+ case Gdk::SOURCE_MOUSE:
+ deviceRow[getCols().thumbnail] = getPix(PIX_CORE);
+ break;
+ case Gdk::SOURCE_PEN:
+ if (deviceRow[getCols().description] == _("pad")) {
+ deviceRow[getCols().thumbnail] = getPix(PIX_SIDEBUTTONS);
+ } else {
+ deviceRow[getCols().thumbnail] = getPix(PIX_TIP);
+ }
+ break;
+ case Gdk::SOURCE_CURSOR:
+ deviceRow[getCols().thumbnail] = getPix(PIX_MOUSE);
+ break;
+ case Gdk::SOURCE_ERASER:
+ deviceRow[getCols().thumbnail] = getPix(PIX_ERASER);
+ break;
+ default:
+ ; // nothing
+ }
+ }
+ }
+
+ for (auto dev : devList) {
+ if ( dev && (consumed.find( dev->getId() ) == consumed.end()) ) {
+ Gtk::TreeModel::Row deviceRow = *(store->prepend(/*row.children()*/));
+ deviceRow[getCols().description] = dev->getName();
+ deviceRow[getCols().device] = dev;
+ deviceRow[getCols().mode] = dev->getMode();
+ deviceRow[getCols().thumbnail] = getPix(PIX_CORE);
+ }
+ }
+
+ } else {
+ g_warning("No devices found");
+ }
+}
+
+
+InputDialogImpl::ConfPanel::ConfPanel() :
+ Gtk::Box(Gtk::ORIENTATION_VERTICAL),
+ confDeviceStore(Gtk::TreeStore::create(getCols())),
+ confDeviceIter(),
+ confDeviceTree(confDeviceStore),
+ confDeviceScroller(),
+ watcher(*this),
+ useExt(_("_Use pressure-sensitive tablet (requires restart)"), true),
+ save(_("_Save"), true),
+ detailsBox(Gtk::ORIENTATION_VERTICAL, 4),
+ titleFrame(Gtk::ORIENTATION_HORIZONTAL, 4),
+ titleLabel(""),
+ axisFrame(_("Axes")),
+ keysFrame(_("Keys")),
+ modeLabel(_("Mode:")),
+ modeBox(Gtk::ORIENTATION_HORIZONTAL, 4),
+ axisVBox(Gtk::ORIENTATION_VERTICAL)
+
+{
+
+
+ confDeviceScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ confDeviceScroller.set_shadow_type(Gtk::SHADOW_IN);
+ confDeviceScroller.add(confDeviceTree);
+ confDeviceScroller.set_size_request(120, 0);
+
+ /* class Foo : public Gtk::TreeModel::ColumnRecord {
+ public :
+ Gtk::TreeModelColumn<Glib::ustring> one;
+ Foo() {add(one);}
+ };
+ static Foo foo;
+
+ //Add the TreeView's view columns:
+ {
+ Gtk::CellRendererToggle *rendr = new Gtk::CellRendererToggle();
+ Gtk::TreeViewColumn *col = new Gtk::TreeViewColumn("xx", *rendr);
+ if (col) {
+ confDeviceTree.append_column(*col);
+ col->set_cell_data_func(*rendr, sigc::ptr_fun(setCellStateToggle));
+ rendr->signal_toggled().connect(sigc::bind(sigc::ptr_fun(commitCellStateChange), confDeviceStore));
+ }
+ }*/
+
+ //int expPos = confDeviceTree.append_column("", getCols().expander);
+
+ confDeviceTree.append_column("I", getCols().thumbnail);
+ confDeviceTree.append_column("Bar", getCols().description);
+
+ //confDeviceTree.get_column(0)->set_fixed_width(100);
+ //confDeviceTree.get_column(1)->set_expand();
+
+/* {
+ Gtk::TreeViewColumn *col = new Gtk::TreeViewColumn("X", *rendr);
+ if (col) {
+ confDeviceTree.append_column(*col);
+ col->set_cell_data_func(*rendr, sigc::ptr_fun(setModeCellString));
+ rendr->signal_edited().connect(sigc::bind(sigc::ptr_fun(commitCellModeChange), confDeviceStore));
+ rendr->property_editable() = true;
+ }
+ }*/
+
+ //confDeviceTree.set_enable_tree_lines();
+ confDeviceTree.property_enable_tree_lines() = false;
+ confDeviceTree.property_enable_grid_lines() = false;
+ confDeviceTree.set_headers_visible(false);
+ //confDeviceTree.set_expander_column( *confDeviceTree.get_column(expPos - 1) );
+
+ confDeviceTree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::onTreeSelect));
+
+ setupTree( confDeviceStore, confDeviceIter );
+
+ Inkscape::DeviceManager::getManager().signalLinkChanged().connect(sigc::bind(sigc::ptr_fun(&InputDialogImpl::updateDeviceLinks), confDeviceIter, &confDeviceTree));
+
+ confDeviceTree.expand_all();
+
+ useExt.set_active(Preferences::get()->getBool("/options/useextinput/value"));
+ useExt.signal_toggled().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::useExtToggled));
+
+ auto buttonBox = Gtk::manage(new Gtk::ButtonBox);
+ buttonBox->set_layout (Gtk::BUTTONBOX_END);
+ //Gtk::Alignment *align = new Gtk::Alignment(Gtk::ALIGN_END, Gtk::ALIGN_START, 0, 0);
+ buttonBox->add(save);
+ save.signal_clicked().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::saveSettings));
+
+ titleFrame.pack_start(titleLabel, true, true);
+ //titleFrame.set_shadow_type(Gtk::SHADOW_IN);
+
+ modeCombo.append(getModeToString()[Gdk::MODE_DISABLED]);
+ modeCombo.append(getModeToString()[Gdk::MODE_SCREEN]);
+ modeCombo.append(getModeToString()[Gdk::MODE_WINDOW]);
+ modeCombo.set_tooltip_text(_("A device can be 'Disabled', its coordinates mapped to the whole 'Screen', or to a single (usually focused) 'Window'"));
+ modeCombo.signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::onModeChange));
+
+ modeBox.pack_start(modeLabel, false, false);
+ modeBox.pack_start(modeCombo, true, true);
+
+ axisVBox.add(axisScroll);
+ axisFrame.add(axisVBox);
+
+ keysFrame.add(keysScroll);
+
+ /**
+ * Scrolled Window
+ */
+ keysScroll.add(keysTree);
+ keysScroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ keysScroll.set_shadow_type(Gtk::SHADOW_IN);
+ keysScroll.set_size_request(120, 80);
+
+ keysStore = Gtk::ListStore::create(keysColumns);
+
+ _kb_shortcut_renderer.property_editable() = true;
+
+ keysTree.set_model(keysStore);
+ keysTree.set_headers_visible(false);
+ keysTree.append_column("Name", keysColumns.name);
+ keysTree.append_column("Value", keysColumns.value);
+
+ //keysTree.append_column("Value", _kb_shortcut_renderer);
+ //keysTree.get_column(1)->add_attribute(_kb_shortcut_renderer.property_text(), keysColumns.value);
+ //_kb_shortcut_renderer.signal_accel_edited().connect( sigc::mem_fun(*this, &InputDialogImpl::onKBTreeEdited) );
+ //_kb_shortcut_renderer.signal_accel_cleared().connect( sigc::mem_fun(*this, &InputDialogImpl::onKBTreeCleared) );
+
+ axisStore = Gtk::ListStore::create(axisColumns);
+
+ axisTree.set_model(axisStore);
+ axisTree.set_headers_visible(false);
+ axisTree.append_column("Name", axisColumns.name);
+ axisTree.append_column("Value", axisColumns.value);
+
+ /**
+ * Scrolled Window
+ */
+ axisScroll.add(axisTree);
+ axisScroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ axisScroll.set_shadow_type(Gtk::SHADOW_IN);
+ axisScroll.set_size_request(0, 150);
+
+ pane.pack1(confDeviceScroller);
+ pane.pack2(detailsBox);
+
+ detailsBox.pack_start(titleFrame, false, false, 6);
+ detailsBox.pack_start(modeBox, false, false, 6);
+ detailsBox.pack_start(axisFrame, false, false);
+ detailsBox.pack_start(keysFrame, false, false);
+ detailsBox.set_border_width(4);
+
+ pack_start(pane, true, true);
+ pack_start(useExt, Gtk::PACK_SHRINK);
+ pack_start(*buttonBox, false, false);
+
+ // Select the first device
+ confDeviceTree.get_selection()->select(confDeviceStore->get_iter("0"));
+
+}
+
+InputDialogImpl::ConfPanel::~ConfPanel()
+= default;
+
+void InputDialogImpl::ConfPanel::setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter)
+{
+ if (iter) {
+ Gtk::CellRendererCombo *combo = dynamic_cast<Gtk::CellRendererCombo *>(rndr);
+ if (combo) {
+ Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device];
+ Gdk::InputMode mode = (*iter)[getCols().mode];
+ if (dev && (getModeToString().find(mode) != getModeToString().end())) {
+ combo->property_text() = getModeToString()[mode];
+ } else {
+ combo->property_text() = "";
+ }
+ }
+ }
+}
+
+void InputDialogImpl::ConfPanel::commitCellModeChange(Glib::ustring const &path, Glib::ustring const &newText, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ Gtk::TreeIter iter = store->get_iter(path);
+ if (iter) {
+ Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device];
+ if (dev && (getStringToMode().find(newText) != getStringToMode().end())) {
+ Gdk::InputMode mode = getStringToMode()[newText];
+ Inkscape::DeviceManager::getManager().setMode( dev->getId(), mode );
+ }
+ }
+
+
+}
+
+void InputDialogImpl::ConfPanel::setCellStateToggle(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter)
+{
+ if (iter) {
+ Gtk::CellRendererToggle *toggle = dynamic_cast<Gtk::CellRendererToggle *>(rndr);
+ if (toggle) {
+ Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device];
+ if (dev) {
+ Gdk::InputMode mode = (*iter)[getCols().mode];
+ toggle->set_active(mode != Gdk::MODE_DISABLED);
+ } else {
+ toggle->set_active(false);
+ }
+ }
+ }
+}
+
+void InputDialogImpl::ConfPanel::commitCellStateChange(Glib::ustring const &path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ Gtk::TreeIter iter = store->get_iter(path);
+ if (iter) {
+ Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device];
+ if (dev) {
+ Gdk::InputMode mode = (*iter)[getCols().mode];
+ if (mode == Gdk::MODE_DISABLED) {
+ Inkscape::DeviceManager::getManager().setMode( dev->getId(), Gdk::MODE_SCREEN );
+ } else {
+ Inkscape::DeviceManager::getManager().setMode( dev->getId(), Gdk::MODE_DISABLED );
+ }
+ }
+ }
+}
+
+void InputDialogImpl::ConfPanel::onTreeSelect()
+{
+ Glib::RefPtr<Gtk::TreeSelection> treeSel = confDeviceTree.get_selection();
+ Gtk::TreeModel::iterator iter = treeSel->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring val = row[getCols().description];
+ Glib::RefPtr<InputDevice const> dev = row[getCols().device];
+ Gdk::InputMode mode = (*iter)[getCols().mode];
+ modeCombo.set_active(getModeId(mode));
+
+ titleLabel.set_markup("<b>" + row[getCols().description] + "</b>");
+
+ if (dev) {
+ setKeys(dev->getNumKeys());
+ setAxis(dev->getNumAxes());
+ }
+ }
+}
+void InputDialogImpl::ConfPanel::saveSettings()
+{
+ Inkscape::DeviceManager::getManager().saveConfig();
+}
+
+void InputDialogImpl::ConfPanel::useExtToggled()
+{
+ bool active = useExt.get_active();
+ if (active != Preferences::get()->getBool("/options/useextinput/value")) {
+ Preferences::get()->setBool("/options/useextinput/value", active);
+ if (active) {
+ // As a work-around for a common problem, enable tablet toggles on the calligraphic tool.
+ // Covered in Launchpad bug #196195.
+ Preferences::get()->setBool("/tools/tweak/usepressure", true);
+ Preferences::get()->setBool("/tools/calligraphic/usepressure", true);
+ Preferences::get()->setBool("/tools/calligraphic/usetilt", true);
+ }
+ }
+}
+
+InputDialogImpl::ConfPanel::Blink::Blink(ConfPanel &parent) :
+ Preferences::Observer("/options/useextinput/value"),
+ parent(parent)
+{
+ Preferences::get()->addObserver(*this);
+}
+
+InputDialogImpl::ConfPanel::Blink::~Blink()
+{
+ Preferences::get()->removeObserver(*this);
+}
+
+void InputDialogImpl::ConfPanel::Blink::notify(Preferences::Entry const &new_val)
+{
+ parent.useExt.set_active(new_val.getBool());
+}
+
+void InputDialogImpl::handleDeviceChange(Glib::RefPtr<InputDevice const> device)
+{
+// g_message("OUCH!!!! for %p hits %s", &device, device->getId().c_str());
+ std::vector<Glib::RefPtr<Gtk::TreeStore> > stores;
+ stores.push_back(deviceStore);
+ stores.push_back(cfgPanel.confDeviceStore);
+
+ for (auto & store : stores) {
+ Gtk::TreeModel::iterator deviceIter;
+ store->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>(
+ sigc::ptr_fun(&InputDialogImpl::findDevice),
+ device->getId(),
+ &deviceIter) );
+ if ( deviceIter ) {
+ Gdk::InputMode mode = device->getMode();
+ Gtk::TreeModel::Row row = *deviceIter;
+ if (row[getCols().mode] != mode) {
+ row[getCols().mode] = mode;
+ }
+ }
+ }
+}
+
+void InputDialogImpl::updateDeviceAxes(Glib::RefPtr<InputDevice const> device)
+{
+ gint live = device->getLiveAxes();
+
+ std::map<guint, std::pair<guint, gdouble> > existing = axesMap[device->getId()];
+ gint mask = 0x1;
+ for ( gint num = 0; num < 32; num++, mask <<= 1) {
+ if ( (mask & live) != 0 ) {
+ if ( (existing.find(num) == existing.end()) || (existing[num].first < 2) ) {
+ axesMap[device->getId()][num].first = 2;
+ axesMap[device->getId()][num].second = 0.0;
+ }
+ }
+ }
+ updateTestAxes( device->getId(), nullptr );
+}
+
+void InputDialogImpl::updateDeviceButtons(Glib::RefPtr<InputDevice const> device)
+{
+ gint live = device->getLiveButtons();
+ std::set<guint> existing = buttonMap[device->getId()];
+ gint mask = 0x1;
+ for ( gint num = 0; num < 32; num++, mask <<= 1) {
+ if ( (mask & live) != 0 ) {
+ if ( existing.find(num) == existing.end() ) {
+ buttonMap[device->getId()].insert(num);
+ }
+ }
+ }
+ updateTestButtons(device->getId(), -1);
+}
+
+
+bool InputDialogImpl::findDevice(const Gtk::TreeModel::iterator& iter,
+ Glib::ustring id,
+ Gtk::TreeModel::iterator* result)
+{
+ bool stop = false;
+ Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device];
+ if ( dev && (dev->getId() == id) ) {
+ if ( result ) {
+ *result = iter;
+ }
+ stop = true;
+ }
+ return stop;
+}
+
+bool InputDialogImpl::findDeviceByLink(const Gtk::TreeModel::iterator& iter,
+ Glib::ustring link,
+ Gtk::TreeModel::iterator* result)
+{
+ bool stop = false;
+ Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device];
+ if ( dev && (dev->getLink() == link) ) {
+ if ( result ) {
+ *result = iter;
+ }
+ stop = true;
+ }
+ return stop;
+}
+
+void InputDialogImpl::updateDeviceLinks(Glib::RefPtr<InputDevice const> device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree)
+{
+ Glib::RefPtr<Gtk::TreeStore> deviceStore = Glib::RefPtr<Gtk::TreeStore>::cast_dynamic(tree->get_model());
+
+// g_message("Links!!!! for %p hits [%s] with link of [%s]", &device, device->getId().c_str(), device->getLink().c_str());
+ Gtk::TreeModel::iterator deviceIter;
+ deviceStore->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>(
+ sigc::ptr_fun(&InputDialogImpl::findDevice),
+ device->getId(),
+ &deviceIter) );
+
+ if ( deviceIter ) {
+ // Found the device concerned. Can proceed.
+
+ if ( device->getLink().empty() ) {
+ // is now unlinked
+// g_message("Item %s is unlinked", device->getId().c_str());
+ if ( deviceIter->parent() != tabletIter ) {
+ // Not the child of the tablet. move on up
+
+ Glib::RefPtr<InputDevice const> dev = (*deviceIter)[getCols().device];
+ Glib::ustring descr = (*deviceIter)[getCols().description];
+ Glib::RefPtr<Gdk::Pixbuf> thumb = (*deviceIter)[getCols().thumbnail];
+
+ Gtk::TreeModel::Row deviceRow = *deviceStore->append(tabletIter->children());
+ deviceRow[getCols().description] = descr;
+ deviceRow[getCols().thumbnail] = thumb;
+ deviceRow[getCols().device] = dev;
+ deviceRow[getCols().mode] = dev->getMode();
+
+ Gtk::TreeModel::iterator oldParent = deviceIter->parent();
+ deviceStore->erase(deviceIter);
+ if ( oldParent->children().empty() ) {
+ deviceStore->erase(oldParent);
+ }
+ }
+ } else {
+ // is linking
+ if ( deviceIter->parent() == tabletIter ) {
+ // Simple case. Not already linked
+
+ Gtk::TreeIter newGroup = deviceStore->append(tabletIter->children());
+ (*newGroup)[getCols().description] = _("Pen");
+ (*newGroup)[getCols().thumbnail] = getPix(PIX_PEN);
+
+ Glib::RefPtr<InputDevice const> dev = (*deviceIter)[getCols().device];
+ Glib::ustring descr = (*deviceIter)[getCols().description];
+ Glib::RefPtr<Gdk::Pixbuf> thumb = (*deviceIter)[getCols().thumbnail];
+
+ Gtk::TreeModel::Row deviceRow = *deviceStore->append(newGroup->children());
+ deviceRow[getCols().description] = descr;
+ deviceRow[getCols().thumbnail] = thumb;
+ deviceRow[getCols().device] = dev;
+ deviceRow[getCols().mode] = dev->getMode();
+
+
+ Gtk::TreeModel::iterator linkIter;
+ deviceStore->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>(
+ sigc::ptr_fun(&InputDialogImpl::findDeviceByLink),
+ device->getId(),
+ &linkIter) );
+ if ( linkIter ) {
+ dev = (*linkIter)[getCols().device];
+ descr = (*linkIter)[getCols().description];
+ thumb = (*linkIter)[getCols().thumbnail];
+
+ deviceRow = *deviceStore->append(newGroup->children());
+ deviceRow[getCols().description] = descr;
+ deviceRow[getCols().thumbnail] = thumb;
+ deviceRow[getCols().device] = dev;
+ deviceRow[getCols().mode] = dev->getMode();
+ Gtk::TreeModel::iterator oldParent = linkIter->parent();
+ deviceStore->erase(linkIter);
+ if ( oldParent->children().empty() ) {
+ deviceStore->erase(oldParent);
+ }
+ }
+
+ Gtk::TreeModel::iterator oldParent = deviceIter->parent();
+ deviceStore->erase(deviceIter);
+ if ( oldParent->children().empty() ) {
+ deviceStore->erase(oldParent);
+ }
+ tree->expand_row(Gtk::TreePath(newGroup), true);
+ }
+ }
+ }
+}
+
+void InputDialogImpl::linkComboChanged() {
+ Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection();
+ Gtk::TreeModel::iterator iter = treeSel->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring val = row[getCols().description];
+ Glib::RefPtr<InputDevice const> dev = row[getCols().device];
+ if ( dev ) {
+ if ( linkCombo.get_active_row_number() == 0 ) {
+ // It is the "None" entry
+ DeviceManager::getManager().setLinkedTo(dev->getId(), "");
+ } else {
+ Glib::ustring linkName = linkCombo.get_active_text();
+ std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices();
+ for ( std::list<Glib::RefPtr<InputDevice const> >::const_iterator it = devList.begin(); it != devList.end(); ++it ) {
+ if ( linkName == (*it)->getName() ) {
+ DeviceManager::getManager().setLinkedTo(dev->getId(), (*it)->getId());
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+void InputDialogImpl::resyncToSelection() {
+ bool clear = true;
+ Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection();
+ Gtk::TreeModel::iterator iter = treeSel->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring val = row[getCols().description];
+ Glib::RefPtr<InputDevice const> dev = row[getCols().device];
+
+ if ( dev ) {
+ axisTable.set_sensitive(true);
+
+ linkConnection.block();
+ linkCombo.remove_all();
+ linkCombo.append(_("None"));
+ linkCombo.set_active(0);
+ if ( dev->getSource() != Gdk::SOURCE_MOUSE ) {
+ Glib::ustring linked = dev->getLink();
+ std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices();
+ for ( std::list<Glib::RefPtr<InputDevice const> >::const_iterator it = devList.begin(); it != devList.end(); ++it ) {
+ if ( ((*it)->getSource() != Gdk::SOURCE_MOUSE) && ((*it) != dev) ) {
+ linkCombo.append((*it)->getName().c_str());
+ if ( (linked.length() > 0) && (linked == (*it)->getId()) ) {
+ linkCombo.set_active_text((*it)->getName().c_str());
+ }
+ }
+ }
+ linkCombo.set_sensitive(true);
+ } else {
+ linkCombo.set_sensitive(false);
+ }
+ linkConnection.unblock();
+
+ clear = false;
+ devName.set_label(row[getCols().description]);
+ axisFrame.set_label(row[getCols().description]);
+ setupValueAndCombo( dev->getNumAxes(), dev->getNumAxes(), devAxesCount, axesCombo);
+ setupValueAndCombo( dev->getNumKeys(), dev->getNumKeys(), devKeyCount, buttonCombo);
+
+
+ }
+ }
+
+ axisTable.set_sensitive(!clear);
+ if (clear) {
+ axisFrame.set_label("");
+ devName.set_label("");
+ devAxesCount.set_label("");
+ devKeyCount.set_label("");
+ }
+}
+
+void InputDialogImpl::ConfPanel::setAxis(gint count)
+{
+ /*
+ * TODO - Make each axis editable
+ */
+ axisStore->clear();
+
+ static Glib::ustring axesLabels[6] = {_("X"), _("Y"), _("Pressure"), _("X tilt"), _("Y tilt"), _("Wheel")};
+
+ for ( gint barNum = 0; barNum < static_cast<gint>(G_N_ELEMENTS(axesLabels)); barNum++ ) {
+
+ Gtk::TreeModel::Row row = *(axisStore->append());
+ row[axisColumns.name] = axesLabels[barNum];
+ if (barNum < count) {
+ row[axisColumns.value] = Glib::ustring::format(barNum+1);
+ } else {
+ row[axisColumns.value] = C_("Input device axe", "None");
+ }
+ }
+
+}
+void InputDialogImpl::ConfPanel::setKeys(gint count)
+{
+ /*
+ * TODO - Make each key assignable
+ */
+
+ keysStore->clear();
+
+ for (gint i = 0; i < count; i++) {
+ Gtk::TreeModel::Row row = *(keysStore->append());
+ row[keysColumns.name] = Glib::ustring::format(i+1);
+ row[keysColumns.value] = _("Disabled");
+ }
+
+
+}
+void InputDialogImpl::setupValueAndCombo( gint reported, gint actual, Gtk::Label& label, Gtk::ComboBoxText& combo )
+{
+ gchar *tmp = g_strdup_printf("%d", reported);
+ label.set_label(tmp);
+ g_free(tmp);
+
+ combo.remove_all();
+ for ( gint i = 1; i <= reported; ++i ) {
+ tmp = g_strdup_printf("%d", i);
+ combo.append(tmp);
+ g_free(tmp);
+ }
+
+ if ( (1 <= actual) && (actual <= reported) ) {
+ combo.set_active(actual - 1);
+ }
+}
+
+void InputDialogImpl::updateTestButtons( Glib::ustring const& key, gint hotButton )
+{
+ for ( gint i = 0; i < static_cast<gint>(G_N_ELEMENTS(testButtons)); i++ ) {
+ if ( buttonMap[key].find(i) != buttonMap[key].end() ) {
+ if ( i == hotButton ) {
+ testButtons[i].set(getPix(PIX_BUTTONS_ON));
+ } else {
+ testButtons[i].set(getPix(PIX_BUTTONS_OFF));
+ }
+ } else {
+ testButtons[i].set(getPix(PIX_BUTTONS_NONE));
+ }
+ }
+}
+
+void InputDialogImpl::updateTestAxes( Glib::ustring const& key, GdkDevice* dev )
+{
+ //static gdouble epsilon = 0.0001;
+ {
+ Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection();
+ Gtk::TreeModel::iterator iter = treeSel->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring val = row[getCols().description];
+ Glib::RefPtr<InputDevice const> idev = row[getCols().device];
+ if ( !idev || (idev->getId() != key) ) {
+ dev = nullptr;
+ }
+ }
+ }
+
+ for ( gint i = 0; i < static_cast<gint>(G_N_ELEMENTS(testAxes)); i++ ) {
+ if ( axesMap[key].find(i) != axesMap[key].end() ) {
+ switch ( axesMap[key][i].first ) {
+ case 0:
+ case 1:
+ testAxes[i].set(getPix(PIX_AXIS_NONE));
+ if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) {
+ axesValues[i].set_sensitive(false);
+ }
+ break;
+ case 2:
+ testAxes[i].set(getPix(PIX_AXIS_OFF));
+ axesValues[i].set_sensitive(true);
+ if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) {
+ // FIXME: Device axis ranges are inaccessible in GTK+ 3 and
+ // are deprecated in GTK+ 2. Progress-bar ranges are disabled
+ // until we find an alternative solution
+
+ // if ( (dev->axes[i].max - dev->axes[i].min) > epsilon ) {
+ axesValues[i].set_sensitive(true);
+ // axesValues[i].set_fraction( (axesMap[key][i].second- dev->axes[i].min) / (dev->axes[i].max - dev->axes[i].min) );
+ // }
+
+ gchar* str = g_strdup_printf("%f", axesMap[key][i].second);
+ axesValues[i].set_text(str);
+ g_free(str);
+ }
+ break;
+ case 3:
+ testAxes[i].set(getPix(PIX_AXIS_ON));
+ axesValues[i].set_sensitive(true);
+ if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) {
+
+ // FIXME: Device axis ranges are inaccessible in GTK+ 3 and
+ // are deprecated in GTK+ 2. Progress-bar ranges are disabled
+ // until we find an alternative solution
+
+ // if ( (dev->axes[i].max - dev->axes[i].min) > epsilon ) {
+ axesValues[i].set_sensitive(true);
+ // axesValues[i].set_fraction( (axesMap[key][i].second- dev->axes[i].min) / (dev->axes[i].max - dev->axes[i].min) );
+ // }
+
+ gchar* str = g_strdup_printf("%f", axesMap[key][i].second);
+ axesValues[i].set_text(str);
+ g_free(str);
+ }
+ }
+
+ } else {
+ testAxes[i].set(getPix(PIX_AXIS_NONE));
+ }
+ }
+ if ( !dev ) {
+ for (auto & axesValue : axesValues) {
+ axesValue.set_fraction(0.0);
+ axesValue.set_text("");
+ axesValue.set_sensitive(false);
+ }
+ }
+}
+
+void InputDialogImpl::mapAxesValues( Glib::ustring const& key, gdouble const * axes, GdkDevice* dev )
+{
+ auto device = Glib::wrap(dev);
+ auto numAxes = device->get_n_axes();
+
+ static gdouble epsilon = 0.0001;
+ if ( (numAxes > 0) && axes) {
+ for ( guint axisNum = 0; axisNum < numAxes; axisNum++ ) {
+ // 0 == new, 1 == set value, 2 == changed value, 3 == active
+ gdouble diff = axesMap[key][axisNum].second - axes[axisNum];
+ switch(axesMap[key][axisNum].first) {
+ case 0:
+ {
+ axesMap[key][axisNum].first = 1;
+ axesMap[key][axisNum].second = axes[axisNum];
+ }
+ break;
+ case 1:
+ {
+ if ( (diff > epsilon) || (diff < -epsilon) ) {
+// g_message("Axis %d changed on %s]", axisNum, key.c_str());
+ axesMap[key][axisNum].first = 3;
+ axesMap[key][axisNum].second = axes[axisNum];
+ updateTestAxes(key, dev);
+ DeviceManager::getManager().addAxis(key, axisNum);
+ }
+ }
+ break;
+ case 2:
+ {
+ if ( (diff > epsilon) || (diff < -epsilon) ) {
+ axesMap[key][axisNum].first = 3;
+ axesMap[key][axisNum].second = axes[axisNum];
+ updateTestAxes(key, dev);
+ }
+ }
+ break;
+ case 3:
+ {
+ if ( (diff > epsilon) || (diff < -epsilon) ) {
+ axesMap[key][axisNum].second = axes[axisNum];
+ } else {
+ axesMap[key][axisNum].first = 2;
+ updateTestAxes(key, dev);
+ }
+ }
+ }
+ }
+ }
+ // std::map<Glib::ustring, std::map<guint, std::pair<guint, gdouble> > > axesMap;
+}
+
+Glib::ustring InputDialogImpl::getKeyFor( GdkDevice* device )
+{
+ Glib::ustring key;
+ auto devicemm = Glib::wrap(device);
+
+ auto source = devicemm->get_source();
+ const auto name = devicemm->get_name();
+
+ switch ( source ) {
+ case Gdk::SOURCE_MOUSE:
+ key = "M:";
+ break;
+ case Gdk::SOURCE_CURSOR:
+ key = "C:";
+ break;
+ case Gdk::SOURCE_PEN:
+ key = "P:";
+ break;
+ case Gdk::SOURCE_ERASER:
+ key = "E:";
+ break;
+ default:
+ key = "?:";
+ }
+ key += name;
+
+ return key;
+}
+
+bool InputDialogImpl::eventSnoop(GdkEvent* event)
+{
+ int modmod = 0;
+
+ auto source = lastSourceSeen;
+ Glib::ustring devName = lastDevnameSeen;
+ Glib::ustring key;
+ gint hotButton = -1;
+
+ /* Code for determining which input device caused the event fails with GTK3
+ * because event->device gives a "generic" input device, not the one that
+ * actually caused the event. See snoop_extended in desktop-events.cpp
+ */
+
+ switch ( event->type ) {
+ case GDK_KEY_PRESS:
+ case GDK_KEY_RELEASE:
+ {
+ auto keyEvt = reinterpret_cast<GdkEventKey*>(event);
+ auto name = Gtk::AccelGroup::name(keyEvt->keyval, static_cast<Gdk::ModifierType>(keyEvt->state));
+ keyVal.set_label(name);
+// g_message("%d KEY state:0x%08x 0x%04x [%s]", keyEvt->type, keyEvt->state, keyEvt->keyval, name);
+ }
+ break;
+ case GDK_BUTTON_PRESS:
+ modmod = 1;
+ // fallthrough
+ case GDK_BUTTON_RELEASE:
+ {
+ auto btnEvt = reinterpret_cast<GdkEventButton*>(event);
+ auto device = Glib::wrap(btnEvt->device);
+ if (device) {
+ key = getKeyFor(btnEvt->device);
+ source = device->get_source();
+ devName = device->get_name();
+ mapAxesValues(key, btnEvt->axes, btnEvt->device);
+
+ if ( buttonMap[key].find(btnEvt->button) == buttonMap[key].end() ) {
+// g_message("New button found for %s = %d", key.c_str(), btnEvt->button);
+ buttonMap[key].insert(btnEvt->button);
+ DeviceManager::getManager().addButton(key, btnEvt->button);
+ }
+ hotButton = modmod ? btnEvt->button : -1;
+ updateTestButtons(key, hotButton);
+ }
+ auto name = Gtk::AccelGroup::name(0, static_cast<Gdk::ModifierType>(btnEvt->state));
+ keyVal.set_label(name);
+// g_message("%d BTN state:0x%08x %c %4d [%s] dev:%p [%s] ",
+// btnEvt->type, btnEvt->state,
+// (modmod ? '+':'-'),
+// btnEvt->button, name, btnEvt->device,
+// (btnEvt->device ? btnEvt->device->name : "null")
+
+// );
+ }
+ break;
+ case GDK_MOTION_NOTIFY:
+ {
+ GdkEventMotion* btnMtn = reinterpret_cast<GdkEventMotion*>(event);
+ auto device = Glib::wrap(btnMtn->device);
+ if (device) {
+ key = getKeyFor(btnMtn->device);
+ source = device->get_source();
+ devName = device->get_name();
+ mapAxesValues(key, btnMtn->axes, btnMtn->device);
+ }
+ auto name = Gtk::AccelGroup::name(0, static_cast<Gdk::ModifierType>(btnMtn->state));
+ keyVal.set_label(name);
+// g_message("%d MOV state:0x%08x [%s] dev:%p [%s] %3.2f %3.2f %3.2f %3.2f %3.2f %3.2f", btnMtn->type, btnMtn->state,
+// name, btnMtn->device,
+// (btnMtn->device ? btnMtn->device->name : "null"),
+// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 0)) ? btnMtn->axes[0]:0),
+// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 1)) ? btnMtn->axes[1]:0),
+// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 2)) ? btnMtn->axes[2]:0),
+// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 3)) ? btnMtn->axes[3]:0),
+// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 4)) ? btnMtn->axes[4]:0),
+// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 5)) ? btnMtn->axes[5]:0)
+// );
+ }
+ break;
+ default:
+ ;// nothing
+ }
+
+
+ if ( (lastSourceSeen != source) || (lastDevnameSeen != devName) ) {
+ switch (source) {
+ case Gdk::SOURCE_MOUSE: {
+ testThumb.set(getPix(PIX_CORE));
+ break;
+ }
+ case Gdk::SOURCE_CURSOR: {
+// g_message("flip to cursor");
+ testThumb.set(getPix(PIX_MOUSE));
+ break;
+ }
+ case Gdk::SOURCE_PEN: {
+ if (devName == _("pad")) {
+// g_message("flip to pad");
+ testThumb.set(getPix(PIX_SIDEBUTTONS));
+ } else {
+// g_message("flip to pen");
+ testThumb.set(getPix(PIX_TIP));
+ }
+ break;
+ }
+ case Gdk::SOURCE_ERASER: {
+// g_message("flip to eraser");
+ testThumb.set(getPix(PIX_ERASER));
+ break;
+ }
+ /// \fixme GTK3 added new GDK_SOURCEs that should be handled here!
+ case Gdk::SOURCE_KEYBOARD:
+ case Gdk::SOURCE_TOUCHSCREEN:
+ case Gdk::SOURCE_TOUCHPAD:
+ case Gdk::SOURCE_TRACKPOINT:
+ case Gdk::SOURCE_TABLET_PAD:
+ g_warning("InputDialogImpl::eventSnoop : unhandled GDK_SOURCE type!");
+ break;
+ }
+
+ updateTestButtons(key, hotButton);
+ lastSourceSeen = source;
+ lastDevnameSeen = devName;
+ }
+
+ return false;
+}
+
+} // end namespace Inkscape
+} // end namespace UI
+} // end namespace Dialog
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/input.h b/src/ui/dialog/input.h
new file mode 100644
index 0000000..d4e7477
--- /dev/null
+++ b/src/ui/dialog/input.h
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Input devices dialog (new)
+ */
+/* Author:
+ * Jon A. Cruz
+ *
+ * Copyright (C) 2008 Author
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_INPUT_H
+#define INKSCAPE_UI_DIALOG_INPUT_H
+
+#include <memory>
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class InputDialog : public DialogBase
+{
+public:
+ static std::unique_ptr<InputDialog> create();
+
+protected:
+ InputDialog() : DialogBase("/dialogs/inputdevices", "Input") {}
+};
+
+} // namespace Dialog
+} // namesapce UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_INPUT_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/dialog/knot-properties.cpp b/src/ui/dialog/knot-properties.cpp
new file mode 100644
index 0000000..048cd33
--- /dev/null
+++ b/src/ui/dialog/knot-properties.cpp
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for moving knots. Only used by Measure Tool.
+ */
+
+/* Author:
+ * Bryce W. Harrington <bryce@bryceharrington.com>
+ * Andrius R. <knutux@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ * Copyright (C) 2006 Andrius R.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "knot-properties.h"
+
+#include <boost/lexical_cast.hpp>
+
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+
+#include "desktop.h"
+#include "ui/knot/knot.h"
+#include "util/units.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+KnotPropertiesDialog::KnotPropertiesDialog()
+ : _knotpoint(nullptr),
+ _position_visible(false),
+ _close_button(_("_Close"), true)
+{
+ Gtk::Box *mainVBox = get_content_area();
+
+ _layout_table.set_row_spacing(4);
+ _layout_table.set_column_spacing(4);
+ _unit_name = "";
+
+ // Layer name widgets
+ _knot_x_entry.set_activates_default(true);
+ _knot_x_entry.set_digits(4);
+ _knot_x_entry.set_increments(1,1);
+ _knot_x_entry.set_range(-G_MAXDOUBLE, G_MAXDOUBLE);
+ _knot_x_entry.set_hexpand();
+ _knot_x_label.set_label(_("Position X:"));
+ _knot_x_label.set_halign(Gtk::ALIGN_END);
+ _knot_x_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _knot_y_entry.set_activates_default(true);
+ _knot_y_entry.set_digits(4);
+ _knot_y_entry.set_increments(1,1);
+ _knot_y_entry.set_range(-G_MAXDOUBLE, G_MAXDOUBLE);
+ _knot_y_entry.set_hexpand();
+ _knot_y_label.set_label(_("Position Y:"));
+ _knot_y_label.set_halign(Gtk::ALIGN_END);
+ _knot_y_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _layout_table.attach(_knot_x_label, 0, 0, 1, 1);
+ _layout_table.attach(_knot_x_entry, 1, 0, 1, 1);
+
+ _layout_table.attach(_knot_y_label, 0, 1, 1, 1);
+ _layout_table.attach(_knot_y_entry, 1, 1, 1, 1);
+
+ mainVBox->pack_start(_layout_table, true, true, 4);
+
+ // Buttons
+ _close_button.set_can_default();
+
+ _apply_button.set_use_underline(true);
+ _apply_button.set_can_default();
+
+ _close_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &KnotPropertiesDialog::_close));
+ _apply_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &KnotPropertiesDialog::_apply));
+
+ signal_delete_event().connect(
+ sigc::bind_return(
+ sigc::hide(sigc::mem_fun(*this, &KnotPropertiesDialog::_close)),
+ true
+ )
+ );
+ add_action_widget(_close_button, Gtk::RESPONSE_CLOSE);
+ add_action_widget(_apply_button, Gtk::RESPONSE_APPLY);
+
+ _apply_button.grab_default();
+
+ show_all_children();
+
+ set_focus(_knot_y_entry);
+}
+
+KnotPropertiesDialog::~KnotPropertiesDialog() {
+}
+
+void KnotPropertiesDialog::showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name)
+{
+ KnotPropertiesDialog *dialog = new KnotPropertiesDialog();
+ dialog->_setKnotPoint(pt->position(), unit_name);
+ dialog->_setPt(pt);
+
+ dialog->set_title(_("Modify Knot Position"));
+ dialog->_apply_button.set_label(_("_Move"));
+
+ dialog->set_modal(true);
+ desktop->setWindowTransient (dialog->gobj());
+ dialog->property_destroy_with_parent() = true;
+
+ dialog->show();
+ dialog->present();
+}
+
+void
+KnotPropertiesDialog::_apply()
+{
+ double d_x = Inkscape::Util::Quantity::convert(_knot_x_entry.get_value(), _unit_name, "px");
+ double d_y = Inkscape::Util::Quantity::convert(_knot_y_entry.get_value(), _unit_name, "px");
+ _knotpoint->moveto(Geom::Point(d_x, d_y));
+ _knotpoint->moved_signal.emit(_knotpoint, _knotpoint->position(), 0);
+ _close();
+}
+
+void
+KnotPropertiesDialog::_close()
+{
+ destroy_();
+ Glib::signal_idle().connect(
+ sigc::bind_return(
+ sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this),
+ false
+ )
+ );
+}
+
+bool KnotPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/)
+{
+
+ /*switch (get_latin_keyval(event)) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter: {
+ _apply();
+ return true;
+ }
+ break;
+ }*/
+ return false;
+}
+
+void KnotPropertiesDialog::_handleButtonEvent(GdkEventButton* event)
+{
+ if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) {
+ _apply();
+ }
+}
+
+void KnotPropertiesDialog::_setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name)
+{
+ _unit_name = unit_name;
+ _knot_x_entry.set_value( Inkscape::Util::Quantity::convert(knotpoint.x(), "px", _unit_name));
+ _knot_y_entry.set_value( Inkscape::Util::Quantity::convert(knotpoint.y(), "px", _unit_name));
+ _knot_x_label.set_label(g_strdup_printf(_("Position X (%s):"), _unit_name.c_str()));
+ _knot_y_label.set_label(g_strdup_printf(_("Position Y (%s):"), _unit_name.c_str()));
+}
+
+void KnotPropertiesDialog::_setPt(const SPKnot *pt)
+{
+ _knotpoint = const_cast<SPKnot *>(pt);
+}
+
+} // namespace
+} // namespace
+} // namespace
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/knot-properties.h b/src/ui/dialog/knot-properties.h
new file mode 100644
index 0000000..18f6745
--- /dev/null
+++ b/src/ui/dialog/knot-properties.h
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief
+ */
+/* Author:
+ * Bryce W. Harrington <bryce@bryceharrington.com>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_KNOT_PROPERTIES_H
+#define INKSCAPE_DIALOG_KNOT_PROPERTIES_H
+
+#include <gtkmm/dialog.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/spinbutton.h>
+#include <2geom/point.h>
+
+#include "ui/tools/measure-tool.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+// Used in Measure tool to set ends of "ruler" (via Shift-click)."
+
+class KnotPropertiesDialog : public Gtk::Dialog {
+ public:
+ KnotPropertiesDialog();
+ ~KnotPropertiesDialog() override;
+
+ Glib::ustring getName() const { return "LayerPropertiesDialog"; }
+
+ static void showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name);
+
+protected:
+
+ SPKnot *_knotpoint;
+
+ Gtk::Label _knot_x_label;
+ Gtk::SpinButton _knot_x_entry;
+ Gtk::Label _knot_y_label;
+ Gtk::SpinButton _knot_y_entry;
+ Gtk::Grid _layout_table;
+ bool _position_visible;
+
+ Gtk::Button _close_button;
+ Gtk::Button _apply_button;
+ Glib::ustring _unit_name;
+
+ sigc::connection _destroy_connection;
+
+ static KnotPropertiesDialog &_instance() {
+ static KnotPropertiesDialog instance;
+ return instance;
+ }
+
+ void _setPt(const SPKnot *pt);
+
+ void _apply();
+ void _close();
+
+ void _setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name);
+ void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row);
+
+ bool _handleKeyEvent(GdkEventKey *event);
+ void _handleButtonEvent(GdkEventButton* event);
+ friend class Inkscape::UI::Tools::MeasureTool;
+
+private:
+ KnotPropertiesDialog(KnotPropertiesDialog const &); // no copy
+ KnotPropertiesDialog &operator=(KnotPropertiesDialog const &); // no assign
+};
+
+} // namespace
+} // namespace
+} // namespace
+
+
+#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_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/dialog/layer-properties.cpp b/src/ui/dialog/layer-properties.cpp
new file mode 100644
index 0000000..1149f04
--- /dev/null
+++ b/src/ui/dialog/layer-properties.cpp
@@ -0,0 +1,435 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for renaming layers.
+ */
+/* Author:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * Andrius R. <knutux@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ * Copyright (C) 2006 Andrius R.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "layer-properties.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+
+#include "inkscape.h"
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "layer-manager.h"
+#include "message-stack.h"
+#include "preferences.h"
+#include "selection-chemistry.h"
+
+#include "ui/icon-names.h"
+#include "ui/widget/imagetoggler.h"
+#include "ui/tools/tool-base.h"
+#include "object/sp-root.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+LayerPropertiesDialog::LayerPropertiesDialog(LayerPropertiesDialogType type)
+ : _type{type}
+ , _close_button(_("_Cancel"), true)
+{
+ auto mainVBox = get_content_area();
+ _layout_table.set_row_spacing(4);
+ _layout_table.set_column_spacing(4);
+
+ // Layer name widgets
+ _layer_name_entry.set_activates_default(true);
+ _layer_name_label.set_label(_("Layer name:"));
+ _layer_name_label.set_halign(Gtk::ALIGN_START);
+ _layer_name_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _layout_table.attach(_layer_name_label, 0, 0, 1, 1);
+
+ _layer_name_entry.set_halign(Gtk::ALIGN_FILL);
+ _layer_name_entry.set_valign(Gtk::ALIGN_FILL);
+ _layer_name_entry.set_hexpand();
+ _layout_table.attach(_layer_name_entry, 1, 0, 1, 1);
+
+ mainVBox->pack_start(_layout_table, true, true, 4);
+
+ // Buttons
+ _close_button.set_can_default();
+
+ _apply_button.set_use_underline(true);
+ _apply_button.set_can_default();
+
+ _close_button.signal_clicked().connect([=]() {_close();});
+ _apply_button.signal_clicked().connect([=]() {_apply();});
+
+ signal_delete_event().connect([=](GdkEventAny*) -> bool {
+ _close();
+ return true;
+ });
+
+ add_action_widget(_close_button, Gtk::RESPONSE_CLOSE);
+ add_action_widget(_apply_button, Gtk::RESPONSE_APPLY);
+
+ _apply_button.grab_default();
+
+ show_all_children();
+}
+
+LayerPropertiesDialog::~LayerPropertiesDialog() = default;
+
+/** Static member function which displays a modal dialog of the given type */
+void LayerPropertiesDialog::_showDialog(LayerPropertiesDialogType type, SPDesktop *desktop, SPObject *layer)
+{
+ auto dialog = new LayerPropertiesDialog(type); // Will be destroyed on idle - see _close()
+
+ dialog->_setDesktop(desktop);
+ dialog->_setLayer(layer);
+
+ dialog->_setup();
+
+ dialog->set_modal(true);
+ desktop->setWindowTransient(dialog->gobj());
+ dialog->property_destroy_with_parent() = true;
+
+ dialog->show();
+ dialog->present();
+}
+
+/** Performs an action depending on the type of the dialog */
+void LayerPropertiesDialog::_apply()
+{
+ switch (_type) {
+ case LayerPropertiesDialogType::CREATE:
+ _doCreate();
+ break;
+
+ case LayerPropertiesDialogType::MOVE:
+ _doMove();
+ break;
+
+ case LayerPropertiesDialogType::RENAME:
+ _doRename();
+ break;
+
+ case LayerPropertiesDialogType::NONE:
+ default:
+ break;
+ }
+ _close();
+}
+
+/** Closes the dialog and asks the idle thread to destroy it */
+void LayerPropertiesDialog::_close()
+{
+ _setLayer(nullptr);
+ _setDesktop(nullptr);
+ destroy_();
+ Glib::signal_idle().connect_once([=]() {delete this;});
+}
+
+/** Creates a new layer based on the input entered in the dialog window */
+void LayerPropertiesDialog::_doCreate()
+{
+ LayerRelativePosition position = LPOS_ABOVE;
+ if (_position_visible) {
+ Gtk::ListStore::iterator activeRow(_layer_position_combo.get_active());
+ position = activeRow->get_value(_dropdown_columns.position);
+ int index = _layer_position_combo.get_active_row_number();
+ Preferences::get()->setInt("/dialogs/layerProp/addLayerPosition", index);
+ }
+ Glib::ustring name(_layer_name_entry.get_text());
+ if (name.empty()) {
+ return;
+ }
+
+ auto root = _desktop->getDocument()->getRoot();
+ SPObject *new_layer = Inkscape::create_layer(root, _layer, position);
+
+ if (!name.empty()) {
+ _desktop->layerManager().renameLayer(new_layer, name.c_str(), true);
+ }
+ _desktop->getSelection()->clear();
+ _desktop->layerManager().setCurrentLayer(new_layer);
+ DocumentUndo::done(_desktop->getDocument(), _("Add layer"), INKSCAPE_ICON("layer-new"));
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("New layer created."));
+}
+
+/** Moves selection to the chosen layer */
+void LayerPropertiesDialog::_doMove()
+{
+ if (auto moveto = _selectedLayer()) {
+ _desktop->getSelection()->toLayer(moveto);
+ DocumentUndo::done(_desktop->getDocument(), _("Move selection to layer"), INKSCAPE_ICON("selection-move-to-layer"));
+ }
+}
+
+/** Renames a layer based on the user input in the dialog window */
+void LayerPropertiesDialog::_doRename()
+{
+ Glib::ustring name(_layer_name_entry.get_text());
+ if (name.empty()) {
+ return;
+ }
+ LayerManager &layman = _desktop->layerManager();
+ layman.renameLayer(layman.currentLayer(), name.c_str(), false);
+
+ DocumentUndo::done(_desktop->getDocument(), _("Rename layer"), INKSCAPE_ICON("layer-rename"));
+ // TRANSLATORS: This means "The layer has been renamed"
+ _desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Renamed layer"));
+}
+
+/** Sets up the dialog depending on its type */
+void LayerPropertiesDialog::_setup()
+{
+ g_assert(_desktop != nullptr);
+ LayerManager &layman = _desktop->layerManager();
+
+ switch (_type) {
+ case LayerPropertiesDialogType::CREATE: {
+ set_title(_("Add Layer"));
+ Glib::ustring new_name = layman.getNextLayerName(nullptr, layman.currentLayer()->label());
+ _layer_name_entry.set_text(new_name);
+ _apply_button.set_label(_("_Add"));
+ _setup_position_controls();
+ break;
+ }
+
+ case LayerPropertiesDialogType::MOVE: {
+ set_title(_("Move to Layer"));
+ _layer_name_entry.set_text(_("Layer"));
+ _apply_button.set_label(_("_Move"));
+ _apply_button.set_sensitive(layman.getLayerCount());
+ _setup_layers_controls();
+ break;
+ }
+
+ case LayerPropertiesDialogType::RENAME: {
+ set_title(_("Rename Layer"));
+ gchar const *name = layman.currentLayer()->label();
+ _layer_name_entry.set_text(name ? name : _("Layer"));
+ _apply_button.set_label(_("_Rename"));
+ break;
+ }
+
+ case LayerPropertiesDialogType::NONE:
+ default:
+ break;
+ }
+}
+
+/** Sets up the combo box for choosing the relative position of the new layer */
+void LayerPropertiesDialog::_setup_position_controls()
+{
+ if (!_layer || _desktop->getDocument()->getRoot() == _layer) {
+ // no layers yet, so option above/below/sublayer is useless
+ return;
+ }
+
+ _position_visible = true;
+ _dropdown_list = Gtk::ListStore::create(_dropdown_columns);
+ _layer_position_combo.set_model(_dropdown_list);
+ _layer_position_combo.pack_start(_label_renderer);
+ _layer_position_combo.set_cell_data_func(_label_renderer,
+ [=](Gtk::TreeModel::const_iterator const &row) {
+ _prepareLabelRenderer(row);
+ });
+
+ Gtk::ListStore::iterator row;
+ row = _dropdown_list->append();
+ row->set_value(_dropdown_columns.position, LPOS_ABOVE);
+ row->set_value(_dropdown_columns.name, Glib::ustring(_("Above current")));
+ _layer_position_combo.set_active(row);
+ row = _dropdown_list->append();
+ row->set_value(_dropdown_columns.position, LPOS_BELOW);
+ row->set_value(_dropdown_columns.name, Glib::ustring(_("Below current")));
+ row = _dropdown_list->append();
+ row->set_value(_dropdown_columns.position, LPOS_CHILD);
+ row->set_value(_dropdown_columns.name, Glib::ustring(_("As sublayer of current")));
+
+ int position = Preferences::get()->getIntLimited("/dialogs/layerProp/addLayerPosition", 0, 0, 2);
+ _layer_position_combo.set_active(position);
+
+ _layer_position_label.set_label(_("Position:"));
+ _layer_position_label.set_halign(Gtk::ALIGN_START);
+ _layer_position_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _layer_position_combo.set_halign(Gtk::ALIGN_FILL);
+ _layer_position_combo.set_valign(Gtk::ALIGN_FILL);
+ _layer_position_combo.set_hexpand();
+ _layout_table.attach(_layer_position_combo, 1, 1, 1, 1);
+
+ _layout_table.attach(_layer_position_label, 0, 1, 1, 1);
+
+ show_all_children();
+}
+
+/** Sets up the tree view of current layers */
+void LayerPropertiesDialog::_setup_layers_controls()
+{
+ ModelColumns *zoop = new ModelColumns();
+ _model = zoop;
+ _store = Gtk::TreeStore::create( *zoop );
+ _tree.set_model( _store );
+ _tree.set_headers_visible(false);
+
+ auto *eyeRenderer = Gtk::manage(new UI::Widget::ImageToggler(INKSCAPE_ICON("object-visible"),
+ INKSCAPE_ICON("object-hidden")));
+ int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1;
+ Gtk::TreeViewColumn *col = _tree.get_column(visibleColNum);
+ if (col) {
+ col->add_attribute(eyeRenderer->property_active(), _model->_colVisible);
+ }
+
+ auto *renderer = Gtk::manage(new UI::Widget::ImageToggler(INKSCAPE_ICON("object-locked"),
+ INKSCAPE_ICON("object-unlocked")));
+ int lockedColNum = _tree.append_column("lock", *renderer) - 1;
+ col = _tree.get_column(lockedColNum);
+ if (col) {
+ col->add_attribute(renderer->property_active(), _model->_colLocked);
+ }
+
+ Gtk::CellRendererText *_text_renderer = Gtk::manage(new Gtk::CellRendererText());
+ int nameColNum = _tree.append_column("Name", *_text_renderer) - 1;
+ Gtk::TreeView::Column *_name_column = _tree.get_column(nameColNum);
+ _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel);
+
+ _tree.set_expander_column(*_tree.get_column(nameColNum));
+ _tree.signal_key_press_event().connect([=](GdkEventKey *ev) {return _handleKeyEvent(ev);}, false);
+ _tree.signal_button_press_event().connect_notify([=](GdkEventButton *b) {_handleButtonEvent(b);});
+
+ _scroller.add(_tree);
+ _scroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _scroller.set_shadow_type(Gtk::SHADOW_IN);
+ _scroller.set_size_request(220, 180);
+
+ SPDocument* document = _desktop->doc();
+ SPRoot* root = document->getRoot();
+ if (root) {
+ SPObject* target = _desktop->layerManager().currentLayer();
+ _store->clear();
+ _addLayer(root, nullptr, target, 0);
+ }
+
+ _layout_table.remove(_layer_name_entry);
+ _layout_table.remove(_layer_name_label);
+
+ _scroller.set_halign(Gtk::ALIGN_FILL);
+ _scroller.set_valign(Gtk::ALIGN_FILL);
+ _scroller.set_hexpand();
+ _scroller.set_vexpand();
+ _scroller.set_propagate_natural_width(true);
+ _scroller.set_propagate_natural_height(true);
+ _layout_table.attach(_scroller, 0, 1, 2, 1);
+
+ show_all_children();
+}
+
+/** Inserts the new layer into the document */
+void LayerPropertiesDialog::_addLayer(SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target,
+ int level)
+{
+ int const max_nest_depth = 20;
+ if (!_desktop || !layer || level >= max_nest_depth) {
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__, "Maximum layer nesting reached.");
+ return;
+ }
+ LayerManager &layman = _desktop->layerManager();
+ unsigned int counter = layman.childCount(layer);
+ for (unsigned int i = 0; i < counter; i++) {
+ SPObject *child = _desktop->layerManager().nthChildOf(layer, i);
+ if (!child) {
+ continue;
+ }
+#if DUMP_LAYERS
+ g_message(" %3d layer:%p {%s} [%s]", level, child, child->id, child->label() );
+#endif // DUMP_LAYERS
+
+ Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend();
+ Gtk::TreeModel::Row row = *iter;
+ row[_model->_colObject] = child;
+ row[_model->_colLabel] = child->label() ? child->label() : child->getId();
+ row[_model->_colVisible] = is<SPItem>(child) ? !cast_unsafe<SPItem>(child)->isHidden() : false;
+ row[_model->_colLocked] = is<SPItem>(child) ? cast_unsafe<SPItem>(child)->isLocked() : false;
+
+ if (target && child == target) {
+ _tree.expand_to_path(_store->get_path(iter));
+ Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection();
+ select->select(iter);
+ }
+
+ _addLayer(child, &row, target, level + 1);
+ }
+}
+
+SPObject* LayerPropertiesDialog::_selectedLayer()
+{
+ SPObject* obj = nullptr;
+
+ Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ obj = row[_model->_colObject];
+ }
+
+ return obj;
+}
+
+bool LayerPropertiesDialog::_handleKeyEvent(GdkEventKey *event)
+{
+ switch (Inkscape::UI::Tools::get_latin_keyval(event)) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter: {
+ _apply();
+ return true;
+ }
+ }
+ return false;
+}
+
+void LayerPropertiesDialog::_handleButtonEvent(GdkEventButton* event)
+{
+ if ((event->type == GDK_2BUTTON_PRESS) && (event->button == 1)) {
+ _apply();
+ }
+}
+
+/** Formats the label for a given layer row
+ */
+void LayerPropertiesDialog::_prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row)
+{
+ Glib::ustring name = (*row)[_dropdown_columns.name];
+ _label_renderer.property_markup() = name;
+}
+
+
+void LayerPropertiesDialog::_setLayer(SPObject *layer) {
+ if (layer) {
+ sp_object_ref(layer, nullptr);
+ }
+ if (_layer) {
+ sp_object_unref(_layer, nullptr);
+ }
+ _layer = layer;
+}
+
+} // namespace Dialogs
+} // 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/dialog/layer-properties.h b/src/ui/dialog/layer-properties.h
new file mode 100644
index 0000000..6ebbc07
--- /dev/null
+++ b/src/ui/dialog/layer-properties.h
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Dialog for renaming layers
+ */
+/* Author:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_LAYER_PROPERTIES_H
+#define INKSCAPE_DIALOG_LAYER_PROPERTIES_H
+
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/label.h>
+#include <gtkmm/grid.h>
+
+#include <gtkmm/combobox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/scrolledwindow.h>
+
+#include "layer-manager.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+/* FIXME: split the LayerPropertiesDialog class into three separate dialogs */
+enum class LayerPropertiesDialogType
+{
+ NONE,
+ CREATE,
+ MOVE,
+ RENAME
+};
+
+class LayerPropertiesDialog : public Gtk::Dialog {
+public:
+ LayerPropertiesDialog(LayerPropertiesDialogType type);
+ ~LayerPropertiesDialog() override;
+ LayerPropertiesDialog(LayerPropertiesDialog const &) = delete; // no copy
+ LayerPropertiesDialog &operator=(LayerPropertiesDialog const &) = delete; // no assign
+
+ Glib::ustring getName() const { return "LayerPropertiesDialog"; }
+
+ static void showRename(SPDesktop *desktop, SPObject *layer) {
+ _showDialog(LayerPropertiesDialogType::RENAME, desktop, layer);
+ }
+ static void showCreate(SPDesktop *desktop, SPObject *layer) {
+ _showDialog(LayerPropertiesDialogType::CREATE, desktop, layer);
+ }
+ static void showMove(SPDesktop *desktop, SPObject *layer) {
+ _showDialog(LayerPropertiesDialogType::MOVE, desktop, layer);
+ }
+
+private:
+ LayerPropertiesDialogType _type = LayerPropertiesDialogType::NONE;
+ SPDesktop *_desktop = nullptr;
+ SPObject *_layer = nullptr;
+
+ class PositionDropdownColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<LayerRelativePosition> position;
+ Gtk::TreeModelColumn<Glib::ustring> name;
+
+ PositionDropdownColumns() {
+ add(position); add(name);
+ }
+ };
+
+ Gtk::Label _layer_name_label;
+ Gtk::Entry _layer_name_entry;
+ Gtk::Label _layer_position_label;
+ Gtk::ComboBox _layer_position_combo;
+ Gtk::Grid _layout_table;
+
+ bool _position_visible = false;
+
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+
+ ModelColumns()
+ {
+ add(_colObject);
+ add(_colVisible);
+ add(_colLocked);
+ add(_colLabel);
+ }
+ ~ModelColumns() override = default;
+
+ Gtk::TreeModelColumn<SPObject*> _colObject;
+ Gtk::TreeModelColumn<Glib::ustring> _colLabel;
+ Gtk::TreeModelColumn<bool> _colVisible;
+ Gtk::TreeModelColumn<bool> _colLocked;
+ };
+
+ Gtk::TreeView _tree;
+ ModelColumns* _model;
+ Glib::RefPtr<Gtk::TreeStore> _store;
+ Gtk::ScrolledWindow _scroller;
+
+
+ PositionDropdownColumns _dropdown_columns;
+ Gtk::CellRendererText _label_renderer;
+ Glib::RefPtr<Gtk::ListStore> _dropdown_list;
+
+ Gtk::Button _close_button;
+ Gtk::Button _apply_button;
+
+ sigc::connection _destroy_connection;
+
+ void _setDesktop(SPDesktop *desktop) { _desktop = desktop; };
+ void _setLayer(SPObject *layer);
+
+ static void _showDialog(LayerPropertiesDialogType type, SPDesktop *desktop, SPObject *layer);
+ void _apply();
+ void _close();
+
+ void _setup_position_controls();
+ void _setup_layers_controls();
+ void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row);
+
+ void _addLayer(SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level);
+ SPObject* _selectedLayer();
+ bool _handleKeyEvent(GdkEventKey *event);
+ void _handleButtonEvent(GdkEventButton* event);
+
+ void _doCreate();
+ void _doMove();
+ void _doRename();
+ void _setup();
+};
+
+} // namespace
+} // namespace
+} // namespace
+
+
+#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_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/dialog/livepatheffect-add.cpp b/src/ui/dialog/livepatheffect-add.cpp
new file mode 100644
index 0000000..150aada
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-add.cpp
@@ -0,0 +1,1000 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for adding a live path effect.
+ *
+ * Author:
+ *
+ * Copyright (C) 2012 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "livepatheffect-add.h"
+
+#include <cmath>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "io/resource.h"
+#include "live_effects/effect.h"
+#include "object/sp-clippath.h"
+#include "object/sp-item-group.h"
+#include "object/sp-mask.h"
+#include "object/sp-path.h"
+#include "object/sp-shape.h"
+#include "object/sp-use.h"
+#include "preferences.h"
+#include "ui/widget/canvas.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+bool sp_has_fav_dialog(Glib::ustring effect)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ size_t pos = favlist.find(effect);
+ if (pos != std::string::npos) {
+ return true;
+ }
+ return false;
+}
+
+void sp_add_fav_dialog(Glib::ustring effect)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ if (!sp_has_fav_dialog(effect)) {
+ prefs->setString("/dialogs/livepatheffect/favs", favlist + effect + ";");
+ }
+}
+
+void sp_remove_fav_dialog(Glib::ustring effect)
+{
+ if (sp_has_fav_dialog(effect)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ effect += ";";
+ size_t pos = favlist.find(effect);
+ if (pos != std::string::npos) {
+ favlist.erase(pos, effect.length());
+ prefs->setString("/dialogs/livepatheffect/favs", favlist);
+ }
+ }
+}
+
+void sp_add_top_window_classes_callback(Gtk::Widget *widg)
+{
+ SPDesktop *desktop = SP_ACTIVE_DESKTOP;
+ if (desktop) {
+ Gtk::Widget *canvas = desktop->canvas;
+ Gtk::Window *toplevel_window = dynamic_cast<Gtk::Window *>(canvas->get_toplevel());
+ if (toplevel_window) {
+ Gtk::Window *current_window = dynamic_cast<Gtk::Window *>(widg);
+ if (!current_window) {
+ current_window = dynamic_cast<Gtk::Window *>(widg->get_toplevel());
+ }
+ if (current_window) {
+ if (toplevel_window->get_style_context()->has_class("dark")) {
+ current_window->get_style_context()->add_class("dark");
+ current_window->get_style_context()->remove_class("bright");
+ } else {
+ current_window->get_style_context()->add_class("bright");
+ current_window->get_style_context()->remove_class("dark");
+ }
+ if (toplevel_window->get_style_context()->has_class("symbolic")) {
+ current_window->get_style_context()->add_class("symbolic");
+ current_window->get_style_context()->remove_class("regular");
+ } else {
+ current_window->get_style_context()->remove_class("symbolic");
+ current_window->get_style_context()->add_class("regular");
+ }
+ }
+ }
+ }
+}
+
+void sp_add_top_window_classes(Gtk::Widget *widg)
+{
+ if (!widg) {
+ return;
+ }
+ if (!widg->get_realized()) {
+ widg->signal_realize().connect(sigc::bind(sigc::ptr_fun(&sp_add_top_window_classes_callback), widg));
+ } else {
+ sp_add_top_window_classes_callback(widg);
+ }
+}
+
+bool LivePathEffectAdd::mouseover(GdkEventCrossing *evt, GtkWidget *wdg)
+{
+ GdkDisplay *display = gdk_display_get_default();
+ GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_HAND2);
+ GdkWindow *window = gtk_widget_get_window(wdg);
+ gdk_window_set_cursor(window, cursor);
+ g_object_unref(cursor);
+ return true;
+}
+
+bool LivePathEffectAdd::mouseout(GdkEventCrossing *evt, GtkWidget *wdg)
+{
+ GdkWindow *window = gtk_widget_get_window(wdg);
+ gdk_window_set_cursor(window, nullptr);
+ hide_pop_description(evt);
+ return true;
+}
+
+LivePathEffectAdd::LivePathEffectAdd()
+ : converter(Inkscape::LivePathEffect::LPETypeConverter)
+ , _applied(false)
+ , _showfavs(false)
+{
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-add.glade");
+ try {
+ _builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for path effect dialog");
+ return;
+ }
+ _builder->get_widget("LPEDialogSelector", _LPEDialogSelector);
+ _builder->get_widget("LPESelectorFlowBox", _LPESelectorFlowBox);
+ _builder->get_widget("LPESelectorEffectInfoPop", _LPESelectorEffectInfoPop);
+ _builder->get_widget("LPEFilter", _LPEFilter);
+ _builder->get_widget("LPEInfo", _LPEInfo);
+ _builder->get_widget("LPEExperimental", _LPEExperimental);
+ _builder->get_widget("LPEScrolled", _LPEScrolled);
+ _builder->get_widget("LPESelectorEffectEventFavShow", _LPESelectorEffectEventFavShow);
+ _builder->get_widget("LPESelectorEffectInfoEventBox", _LPESelectorEffectInfoEventBox);
+ _builder->get_widget("LPESelectorEffectRadioPackLess", _LPESelectorEffectRadioPackLess);
+ _builder->get_widget("LPESelectorEffectRadioPackMore", _LPESelectorEffectRadioPackMore);
+ _builder->get_widget("LPESelectorEffectRadioList", _LPESelectorEffectRadioList);
+
+ _LPEFilter->signal_search_changed().connect(sigc::mem_fun(*this, &LivePathEffectAdd::on_search));
+ _LPEDialogSelector->add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK |
+ Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::KEY_PRESS_MASK);
+ _LPESelectorFlowBox->signal_set_focus_child().connect(sigc::mem_fun(*this, &LivePathEffectAdd::on_focus));
+
+ gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-effect.glade");
+ for (int i = 0; i < static_cast<int>(converter._length); ++i) {
+ Glib::RefPtr<Gtk::Builder> builder_effect;
+ try {
+ builder_effect = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for filter effect dialog");
+ return;
+ }
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = &converter.data(i);
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ LPESelectorEffect->signal_button_press_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i)));
+ Gtk::EventBox *LPESelectorEffectEventExpander;
+ builder_effect->get_widget("LPESelectorEffectEventExpander", LPESelectorEffectEventExpander);
+ LPESelectorEffectEventExpander->signal_button_press_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::Builder>>(sigc::mem_fun(*this, &LivePathEffectAdd::expand), builder_effect));
+ LPESelectorEffectEventExpander->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorEffectEventExpander->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj())));
+ Gtk::Label *LPEName;
+ builder_effect->get_widget("LPEName", LPEName);
+ Gtk::Label *LPEUntranslatedName;
+ builder_effect->get_widget("LPEUntranslatedName", LPEUntranslatedName);
+ const Glib::ustring label = _(converter.get_label(data->id).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(data->id);
+ LPEUntranslatedName->set_text(untranslated_label);
+ if (untranslated_label == label) {
+ LPEName->set_text(label);
+ } else {
+ LPEName->set_markup((label + "\n<span size='x-small'>" + untranslated_label + "</span>").c_str());
+ }
+ Gtk::Label *LPEDescription;
+ builder_effect->get_widget("LPEDescription", LPEDescription);
+ const Glib::ustring description = _(converter.get_description(data->id).c_str());
+ LPEDescription->set_text(description);
+ Gtk::ToggleButton *LPEExperimentalToggle;
+ builder_effect->get_widget("LPEExperimentalToggle", LPEExperimentalToggle);
+ bool active = converter.get_experimental(data->id) ? true : false;
+ LPEExperimentalToggle->set_active(active);
+ Gtk::Image *LPEIcon;
+ builder_effect->get_widget("LPEIcon", LPEIcon);
+ LPEIcon->set_from_icon_name(converter.get_icon(data->id), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG));
+ Gtk::EventBox *LPESelectorEffectEventInfo;
+ builder_effect->get_widget("LPESelectorEffectEventInfo", LPESelectorEffectEventInfo);
+ LPESelectorEffectEventInfo->signal_enter_notify_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::pop_description), builder_effect));
+ Gtk::EventBox *LPESelectorEffectEventFav;
+ builder_effect->get_widget("LPESelectorEffectEventFav", LPESelectorEffectEventFav);
+ Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFav->get_child());
+ if (sp_has_fav_dialog(untranslated_label)) {
+ fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ } else {
+ fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ }
+ Gtk::EventBox *LPESelectorEffectEventFavTop;
+ builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop);
+ LPESelectorEffectEventFav->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect));
+ LPESelectorEffectEventFavTop->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect));
+ Gtk::Image *favtop = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child());
+ if (sp_has_fav_dialog(untranslated_label)) {
+ favtop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ } else {
+ favtop->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ }
+ Gtk::EventBox *LPESelectorEffectEventApply;
+ builder_effect->get_widget("LPESelectorEffectEventApply", LPESelectorEffectEventApply);
+ LPESelectorEffectEventApply->signal_button_press_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i)));
+ LPESelectorEffectEventApply->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffectEventApply->gobj())));
+ LPESelectorEffectEventApply->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffectEventApply->gobj())));
+ Gtk::ButtonBox *LPESelectorButtonBox;
+ builder_effect->get_widget("LPESelectorButtonBox", LPESelectorButtonBox);
+ LPESelectorButtonBox->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorButtonBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorEffect->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj())));
+ LPESelectorEffect->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj())));
+ _LPESelectorFlowBox->insert(*LPESelectorEffect, i);
+ LPESelectorEffect->get_parent()->signal_key_press_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::on_press_enter), builder_effect, &converter.data(i)));
+ LPESelectorEffect->get_parent()->get_style_context()->add_class(
+ ("LPEIndex" + Glib::ustring::format(i)).c_str());
+ }
+ _LPESelectorFlowBox->set_activate_on_single_click(false);
+ _visiblelpe = _LPESelectorFlowBox->get_children().size();
+ _LPEInfo->set_visible(false);
+ _LPESelectorEffectRadioPackLess->signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 0));
+ _LPESelectorEffectRadioPackMore->signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 1));
+ _LPESelectorEffectRadioList->signal_clicked().connect(
+ sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 2));
+ _LPESelectorEffectEventFavShow->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj())));
+ _LPESelectorEffectEventFavShow->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj())));
+ _LPESelectorEffectEventFavShow->signal_button_press_event().connect(
+ sigc::mem_fun(*this, &LivePathEffectAdd::show_fav_toggler));
+ _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect(
+ sigc::mem_fun(*this, &LivePathEffectAdd::hide_pop_description));
+ _LPESelectorEffectInfoEventBox->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj())));
+ _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>(
+ sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj())));
+ _LPEExperimental->property_active().signal_changed().connect(
+ sigc::mem_fun(*this, &LivePathEffectAdd::reload_effect_list));
+ Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel();
+ int width;
+ int height;
+ window->get_size(width, height);
+ _LPEDialogSelector->resize(std::min(width - 300, 1440), std::min(height - 300, 900));
+ _LPESelectorFlowBox->set_focus_vadjustment(_LPEScrolled->get_vadjustment());
+ _LPEDialogSelector->show_all_children();
+ _lasteffect = nullptr;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0);
+ switch (mode) {
+ case 0:
+ _LPESelectorEffectRadioPackLess->set_active();
+ viewChanged(0);
+ break;
+ case 1:
+ _LPESelectorEffectRadioPackMore->set_active();
+ viewChanged(1);
+ break;
+ default:
+ _LPESelectorEffectRadioList->set_active();
+ viewChanged(2);
+ }
+ Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(_LPEDialogSelector);
+ INKSCAPE.themecontext->getChangeThemeSignal().connect(sigc::bind(sigc::ptr_fun(sp_add_top_window_classes), widg));
+ sp_add_top_window_classes(widg);
+}
+const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *LivePathEffectAdd::getActiveData()
+{
+ return instance()._to_add;
+}
+
+void LivePathEffectAdd::viewChanged(gint mode)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool changed = false;
+ if (mode == 2 && !_LPEDialogSelector->get_style_context()->has_class("LPEList")) {
+ _LPEDialogSelector->get_style_context()->add_class("LPEList");
+ _LPEDialogSelector->get_style_context()->remove_class("LPEPackLess");
+ _LPEDialogSelector->get_style_context()->remove_class("LPEPackMore");
+ _LPESelectorFlowBox->set_max_children_per_line(1);
+ changed = true;
+ } else if (mode == 1 && !_LPEDialogSelector->get_style_context()->has_class("LPEPackMore")) {
+ _LPEDialogSelector->get_style_context()->remove_class("LPEList");
+ _LPEDialogSelector->get_style_context()->remove_class("LPEPackLess");
+ _LPEDialogSelector->get_style_context()->add_class("LPEPackMore");
+ _LPESelectorFlowBox->set_max_children_per_line(30);
+ changed = true;
+ } else if (mode == 0 && !_LPEDialogSelector->get_style_context()->has_class("LPEPackLess")) {
+ _LPEDialogSelector->get_style_context()->remove_class("LPEList");
+ _LPEDialogSelector->get_style_context()->add_class("LPEPackLess");
+ _LPEDialogSelector->get_style_context()->remove_class("LPEPackMore");
+ _LPESelectorFlowBox->set_max_children_per_line(30);
+ changed = true;
+ }
+ prefs->setInt("/dialogs/livepatheffect/dialogmode", mode);
+ if (changed) {
+ _LPESelectorFlowBox->unset_sort_func();
+ _LPESelectorFlowBox->set_sort_func(sigc::mem_fun(*this, &LivePathEffectAdd::on_sort));
+ std::vector<Gtk::FlowBoxChild *> selected = _LPESelectorFlowBox->get_selected_children();
+ if (selected.size() == 1) {
+ _LPESelectorFlowBox->get_selected_children()[0]->grab_focus();
+ }
+ }
+}
+
+void LivePathEffectAdd::on_focus(Gtk::Widget *widget)
+{
+ Gtk::FlowBoxChild *child = dynamic_cast<Gtk::FlowBoxChild *>(widget);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0);
+ if (child && mode != 2) {
+ for (auto i : _LPESelectorFlowBox->get_children()) {
+ Gtk::FlowBoxChild *leitem = dynamic_cast<Gtk::FlowBoxChild *>(i);
+ Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(leitem->get_child());
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Box *actions = dynamic_cast<Gtk::Box *>(contents[5]);
+ if (actions) {
+ actions->set_visible(false);
+ }
+ Gtk::EventBox *expander = dynamic_cast<Gtk::EventBox *>(contents[4]);
+ if (expander) {
+ expander->set_visible(true);
+ }
+ }
+ }
+ }
+ Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child->get_child());
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::EventBox *expander = dynamic_cast<Gtk::EventBox *>(contents[4]);
+ if (expander) {
+ expander->set_visible(false);
+ }
+ }
+ }
+
+ child->show_all_children();
+ _LPESelectorFlowBox->select_child(*child);
+ }
+}
+
+bool LivePathEffectAdd::pop_description(GdkEventCrossing *evt, Glib::RefPtr<Gtk::Builder> builder_effect)
+{
+ Gtk::Image *LPESelectorEffectInfo;
+ builder_effect->get_widget("LPESelectorEffectInfo", LPESelectorEffectInfo);
+ _LPESelectorEffectInfoPop->set_relative_to(*LPESelectorEffectInfo);
+
+ Gtk::Label *LPEName;
+ builder_effect->get_widget("LPEName", LPEName);
+ Gtk::Label *LPEDescription;
+ builder_effect->get_widget("LPEDescription", LPEDescription);
+ Gtk::Image *LPEIcon;
+ builder_effect->get_widget("LPEIcon", LPEIcon);
+
+ Gtk::Image *LPESelectorEffectInfoIcon;
+ _builder->get_widget("LPESelectorEffectInfoIcon", LPESelectorEffectInfoIcon);
+ LPESelectorEffectInfoIcon->set_from_icon_name(LPEIcon->get_icon_name(), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG));
+
+ Gtk::Label *LPESelectorEffectInfoName;
+ _builder->get_widget("LPESelectorEffectInfoName", LPESelectorEffectInfoName);
+ LPESelectorEffectInfoName->set_text(LPEName->get_text());
+
+ Gtk::Label *LPESelectorEffectInfoDescription;
+ _builder->get_widget("LPESelectorEffectInfoDescription", LPESelectorEffectInfoDescription);
+ LPESelectorEffectInfoDescription->set_text(LPEDescription->get_text());
+
+ _LPESelectorEffectInfoPop->show();
+
+ return true;
+}
+
+bool LivePathEffectAdd::hide_pop_description(GdkEventCrossing *evt)
+{
+ _LPESelectorEffectInfoPop->hide();
+ return true;
+}
+
+bool LivePathEffectAdd::fav_toggler(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect)
+{
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::Label *LPEName;
+ builder_effect->get_widget("LPEName", LPEName);
+ Gtk::Label *LPEUntranslatedName;
+ builder_effect->get_widget("LPEUntranslatedName", LPEUntranslatedName);
+ Gtk::Image *LPESelectorEffectFav;
+ builder_effect->get_widget("LPESelectorEffectFav", LPESelectorEffectFav);
+ Gtk::Image *LPESelectorEffectFavTop;
+ builder_effect->get_widget("LPESelectorEffectFavTop", LPESelectorEffectFavTop);
+ Gtk::EventBox *LPESelectorEffectEventFavTop;
+ builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop);
+ if (LPESelectorEffectFav && LPESelectorEffectEventFavTop) {
+ if (sp_has_fav_dialog(LPEUntranslatedName->get_text())) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0);
+ if (mode == 2) {
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ } else {
+ LPESelectorEffectEventFavTop->set_visible(false);
+ LPESelectorEffectEventFavTop->hide();
+ }
+ LPESelectorEffectFavTop->set_from_icon_name("draw-star-outline",
+ Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectFav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ sp_remove_fav_dialog(LPEUntranslatedName->get_text());
+ LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpefav");
+ LPESelectorEffect->get_parent()->get_style_context()->add_class("lpenormal");
+ LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe");
+ if (_showfavs) {
+ reload_effect_list();
+ }
+ } else {
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ LPESelectorEffectFavTop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectFav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ sp_add_fav_dialog(LPEUntranslatedName->get_text());
+ LPESelectorEffect->get_parent()->get_style_context()->add_class("lpefav");
+ LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpenormal");
+ LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe");
+ }
+ }
+ return true;
+}
+
+bool LivePathEffectAdd::show_fav_toggler(GdkEventButton *evt)
+{
+ _showfavs = !_showfavs;
+ Gtk::Image *favimage = dynamic_cast<Gtk::Image *>(_LPESelectorEffectEventFavShow->get_child());
+ if (favimage) {
+ if (_showfavs) {
+ favimage->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ } else {
+ favimage->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ }
+ }
+ reload_effect_list();
+ return true;
+}
+
+bool LivePathEffectAdd::apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add)
+{
+ _to_add = to_add;
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent());
+ _LPESelectorFlowBox->select_child(*flowboxchild);
+ if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) {
+ return true;
+ }
+ _applied = true;
+ _lasteffect = flowboxchild;
+ _LPEDialogSelector->response(Gtk::RESPONSE_APPLY);
+ _LPEDialogSelector->hide();
+ return true;
+}
+
+bool LivePathEffectAdd::on_press_enter(GdkEventKey *key, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add)
+{
+ if (key->keyval == 65293 || key->keyval == 65421) {
+ _to_add = to_add;
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent());
+ if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) {
+ return true;
+ }
+ _applied = true;
+ _lasteffect = flowboxchild;
+ _LPEDialogSelector->response(Gtk::RESPONSE_APPLY);
+ _LPEDialogSelector->hide();
+ return true;
+ }
+ return false;
+}
+
+bool LivePathEffectAdd::expand(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect)
+{
+ Gtk::EventBox *LPESelectorEffect;
+ builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect);
+ Gtk::FlowBoxChild *child = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent());
+ if (child) {
+ child->grab_focus();
+ }
+ return true;
+}
+
+
+
+bool LivePathEffectAdd::on_filter(Gtk::FlowBoxChild *child)
+{
+ std::vector<Glib::ustring> classes = child->get_style_context()->list_classes();
+ int pos = 0;
+ for (auto childclass : classes) {
+ size_t s = childclass.find("LPEIndex", 0);
+ if (s != -1) {
+ childclass = childclass.erase(0, 8);
+ pos = std::stoi(childclass.raw());
+ }
+ }
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = &converter.data(pos);
+ bool disable = false;
+ if (_item_type == "group" && !converter.get_on_group(data->id)) {
+ disable = true;
+ } else if (_item_type == "shape" && !converter.get_on_shape(data->id)) {
+ disable = true;
+ } else if (_item_type == "path" && !converter.get_on_path(data->id)) {
+ disable = true;
+ }
+
+ if (!_has_clip && data->id == Inkscape::LivePathEffect::POWERCLIP) {
+ disable = true;
+ }
+ if (!_has_mask && data->id == Inkscape::LivePathEffect::POWERMASK) {
+ disable = true;
+ }
+
+ if (disable) {
+ child->get_style_context()->add_class("lpedisabled");
+ } else {
+ child->get_style_context()->remove_class("lpedisabled");
+ }
+ child->set_valign(Gtk::ALIGN_START);
+ Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child->get_child());
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]);
+ std::vector<Gtk::Widget *> content_overlay = overlay->get_children();
+
+ Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]);
+ Gtk::Label *untranslatedname = dynamic_cast<Gtk::Label *>(contents[6]);
+ if (!sp_has_fav_dialog(untranslatedname->get_text()) && _showfavs) {
+ return false;
+ }
+ Gtk::ToggleButton *experimental = dynamic_cast<Gtk::ToggleButton *>(contents[3]);
+ if (experimental) {
+ if (experimental->get_active() && !_LPEExperimental->get_active()) {
+ return false;
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]);
+ if (lpedesc) {
+ size_t s = lpedesc->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0);
+ if (s != -1) {
+ _visiblelpe++;
+ return true;
+ }
+ }
+ if (_LPEFilter->get_text().length() < 1) {
+ _visiblelpe++;
+ return true;
+ }
+ if (lpename) {
+ size_t s = lpename->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0);
+ if (s != -1) {
+ _visiblelpe++;
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+void LivePathEffectAdd::reload_effect_list()
+{
+ /* if(_LPEExperimental->get_active()) {
+ _LPEExperimental->get_style_context()->add_class("active");
+ } else {
+ _LPEExperimental->get_style_context()->remove_class("active");
+ } */
+ _visiblelpe = 0;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/livepatheffect/showexperimental", _LPEExperimental->get_active());
+ _LPESelectorFlowBox->invalidate_filter();
+ if (_showfavs) {
+ if (_visiblelpe == 0) {
+ _LPEInfo->set_text(_("You don't have any favorites yet. Click on the favorites star again to see all LPEs."));
+ _LPEInfo->set_visible(true);
+ _LPEInfo->get_style_context()->add_class("lpeinfowarn");
+ } else {
+ _LPEInfo->set_text(_("These are your favorite effects"));
+ _LPEInfo->set_visible(true);
+ _LPEInfo->get_style_context()->add_class("lpeinfowarn");
+ }
+ } else {
+ _LPEInfo->set_text(_("Nothing found! Please try again with different search terms."));
+ _LPEInfo->set_visible(false);
+ _LPEInfo->get_style_context()->remove_class("lpeinfowarn");
+ }
+}
+
+void LivePathEffectAdd::on_search()
+{
+ _visiblelpe = 0;
+ _LPESelectorFlowBox->invalidate_filter();
+ if (_showfavs) {
+ if (_visiblelpe == 0) {
+ _LPEInfo->set_text(_("Nothing found! Please try again with different search terms."));
+ _LPEInfo->set_visible(true);
+ _LPEInfo->get_style_context()->add_class("lpeinfowarn");
+ } else {
+ _LPEInfo->set_visible(true);
+ _LPEInfo->get_style_context()->add_class("lpeinfowarn");
+ }
+ } else {
+ if (_visiblelpe == 0) {
+ _LPEInfo->set_text(_("Nothing found! Please try again with different search terms."));
+ _LPEInfo->set_visible(true);
+ _LPEInfo->get_style_context()->add_class("lpeinfowarn");
+ } else {
+ _LPEInfo->set_visible(false);
+ _LPEInfo->get_style_context()->remove_class("lpeinfowarn");
+ }
+ }
+}
+
+int LivePathEffectAdd::on_sort(Gtk::FlowBoxChild *child1, Gtk::FlowBoxChild *child2)
+{
+ Glib::ustring name1 = "";
+ Glib::ustring uname1 = "";
+ Glib::ustring name2 = "";
+ Glib::ustring uname2 = "";
+ Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child1->get_child());
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0);
+ if (mode == 2) {
+ eventbox->set_halign(Gtk::ALIGN_START);
+ } else {
+ eventbox->set_halign(Gtk::ALIGN_CENTER);
+ }
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (mode == 2) {
+ box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ } else {
+ box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ }
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]);
+ Gtk::Label *ulpename = dynamic_cast<Gtk::Label *>(contents[6]);
+ name1 = lpename->get_text();
+ uname1 = ulpename->get_text();
+ if (lpename) {
+ if (mode == 2) {
+ lpename->set_justify(Gtk::JUSTIFY_LEFT);
+ lpename->set_halign(Gtk::ALIGN_START);
+ lpename->set_valign(Gtk::ALIGN_CENTER);
+ lpename->set_width_chars(-1);
+ lpename->set_max_width_chars(-1);
+ } else {
+ lpename->set_justify(Gtk::JUSTIFY_CENTER);
+ lpename->set_halign(Gtk::ALIGN_CENTER);
+ lpename->set_valign(Gtk::ALIGN_CENTER);
+ lpename->set_width_chars(14);
+ lpename->set_max_width_chars(23);
+ }
+ }
+ Gtk::EventBox *lpemore = dynamic_cast<Gtk::EventBox *>(contents[4]);
+ if (lpemore) {
+ if (mode == 2) {
+ lpemore->hide();
+ } else {
+ if (child1->is_selected()) {
+ lpemore->hide();
+ } else {
+ lpemore->show();
+ }
+ }
+ }
+ Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]);
+ if (lpebuttonbox) {
+ if (mode == 2) {
+ lpebuttonbox->hide();
+ } else {
+ if (child1->is_selected()) {
+ lpebuttonbox->show();
+ } else {
+ lpebuttonbox->hide();
+ }
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]);
+ if (lpedesc) {
+ if (mode == 2) {
+ lpedesc->show();
+ lpedesc->set_justify(Gtk::JUSTIFY_LEFT);
+ lpedesc->set_halign(Gtk::ALIGN_START);
+ lpedesc->set_valign(Gtk::ALIGN_CENTER);
+ lpedesc->set_ellipsize(Pango::ELLIPSIZE_END);
+ } else {
+ lpedesc->hide();
+ lpedesc->set_justify(Gtk::JUSTIFY_CENTER);
+ lpedesc->set_halign(Gtk::ALIGN_CENTER);
+ lpedesc->set_valign(Gtk::ALIGN_CENTER);
+ lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE);
+ }
+ }
+ Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]);
+ if (overlay) {
+ std::vector<Gtk::Widget *> contents_overlay = overlay->get_children();
+ Gtk::Image *icon = dynamic_cast<Gtk::Image *>(contents_overlay[0]);
+ if (icon) {
+ if (mode == 2) {
+ icon->set_pixel_size(40);
+ icon->set_margin_end(25);
+ overlay->set_margin_end(5);
+ } else {
+ icon->set_pixel_size(60);
+ icon->set_margin_end(0);
+ overlay->set_margin_end(0);
+ }
+ }
+ Gtk::EventBox *LPESelectorEffectEventFavTop = dynamic_cast<Gtk::EventBox *>(contents_overlay[1]);
+ if (LPESelectorEffectEventFavTop) {
+ Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child());
+ if (sp_has_fav_dialog(uname1)) {
+ fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ child1->get_style_context()->add_class("lpefav");
+ child1->get_style_context()->remove_class("lpenormal");
+ } else if (!sp_has_fav_dialog(uname1)) {
+ fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectEventFavTop->set_visible(false);
+ LPESelectorEffectEventFavTop->hide();
+ child1->get_style_context()->remove_class("lpefav");
+ child1->get_style_context()->add_class("lpenormal");
+ }
+ if (mode == 2) {
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END);
+ LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER);
+ } else {
+ LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END);
+ LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START);
+ }
+ child1->get_style_context()->add_class("lpe");
+ }
+ }
+ }
+ }
+ eventbox = dynamic_cast<Gtk::EventBox *>(child2->get_child());
+ if (mode == 2) {
+ eventbox->set_halign(Gtk::ALIGN_START);
+ } else {
+ eventbox->set_halign(Gtk::ALIGN_CENTER);
+ }
+ if (eventbox) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child());
+ if (mode == 2) {
+ box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ } else {
+ box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ }
+ if (box) {
+ std::vector<Gtk::Widget *> contents = box->get_children();
+ Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]);
+ name2 = lpename->get_text();
+ Gtk::Label *ulpename = dynamic_cast<Gtk::Label *>(contents[6]);
+ uname2 = ulpename->get_text();
+ if (lpename) {
+ if (mode == 2) {
+ lpename->set_justify(Gtk::JUSTIFY_LEFT);
+ lpename->set_halign(Gtk::ALIGN_START);
+ lpename->set_valign(Gtk::ALIGN_CENTER);
+ lpename->set_width_chars(-1);
+ lpename->set_max_width_chars(-1);
+ } else {
+ lpename->set_justify(Gtk::JUSTIFY_CENTER);
+ lpename->set_halign(Gtk::ALIGN_CENTER);
+ lpename->set_valign(Gtk::ALIGN_CENTER);
+ lpename->set_width_chars(14);
+ lpename->set_max_width_chars(23);
+ }
+ }
+ Gtk::EventBox *lpemore = dynamic_cast<Gtk::EventBox *>(contents[4]);
+ if (lpemore) {
+ if (mode == 2) {
+ lpemore->hide();
+ } else {
+ if (child2->is_selected()) {
+ lpemore->hide();
+ } else {
+ lpemore->show();
+ }
+ }
+ }
+ Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]);
+ if (lpebuttonbox) {
+ if (mode == 2) {
+ lpebuttonbox->hide();
+ } else {
+ if (child2->is_selected()) {
+ lpebuttonbox->show();
+ } else {
+ lpebuttonbox->hide();
+ }
+ }
+ }
+ Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]);
+ if (lpedesc) {
+ if (mode == 2) {
+ lpedesc->show();
+ lpedesc->set_justify(Gtk::JUSTIFY_LEFT);
+ lpedesc->set_halign(Gtk::ALIGN_START);
+ lpedesc->set_valign(Gtk::ALIGN_CENTER);
+ lpedesc->set_ellipsize(Pango::ELLIPSIZE_END);
+ } else {
+ lpedesc->hide();
+ lpedesc->set_justify(Gtk::JUSTIFY_CENTER);
+ lpedesc->set_halign(Gtk::ALIGN_CENTER);
+ lpedesc->set_valign(Gtk::ALIGN_CENTER);
+ lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE);
+ }
+ }
+ Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]);
+ if (overlay) {
+ std::vector<Gtk::Widget *> contents_overlay = overlay->get_children();
+ Gtk::Image *icon = dynamic_cast<Gtk::Image *>(contents_overlay[0]);
+ if (icon) {
+ if (mode == 2) {
+ icon->set_pixel_size(33);
+ icon->set_margin_end(40);
+ } else {
+ icon->set_pixel_size(60);
+ icon->set_margin_end(0);
+ }
+ }
+ Gtk::EventBox *LPESelectorEffectEventFavTop = dynamic_cast<Gtk::EventBox *>(contents_overlay[1]);
+ Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child());
+ if (LPESelectorEffectEventFavTop) {
+ if (sp_has_fav_dialog(uname2)) {
+ fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ child2->get_style_context()->add_class("lpefav");
+ child2->get_style_context()->remove_class("lpenormal");
+ } else if (!sp_has_fav_dialog(uname2)) {
+ fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ LPESelectorEffectEventFavTop->set_visible(false);
+ LPESelectorEffectEventFavTop->hide();
+ child2->get_style_context()->remove_class("lpefav");
+ child2->get_style_context()->add_class("lpenormal");
+ }
+ if (mode == 2) {
+ LPESelectorEffectEventFavTop->set_visible(true);
+ LPESelectorEffectEventFavTop->show();
+ LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END);
+ LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER);
+ } else {
+ LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END);
+ LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START);
+ }
+ child2->get_style_context()->add_class("lpe");
+ }
+ }
+ }
+ }
+ std::vector<Glib::ustring> effect;
+ effect.push_back(name1);
+ effect.push_back(name2);
+ sort(effect.begin(), effect.end());
+ /* if (sp_has_fav_dialog(name1) && sp_has_fav_dialog(name2)) {
+ return effect[0] == name1?-1:1;
+ }
+ if (sp_has_fav_dialog(name1)) {
+ return -1;
+ } */
+ if (effect[0] == name1) { //&& !sp_has_fav_dialog(name2)) {
+ return -1;
+ }
+ return 1;
+}
+
+
+void LivePathEffectAdd::onClose() { _LPEDialogSelector->hide(); }
+
+void LivePathEffectAdd::onKeyEvent(GdkEventKey *evt)
+{
+ if (evt->keyval == GDK_KEY_Escape) {
+ onClose();
+ }
+}
+
+void LivePathEffectAdd::show(SPDesktop *desktop)
+{
+ LivePathEffectAdd &dial = instance();
+ Inkscape::Selection *sel = desktop->getSelection();
+ if (sel && !sel->isEmpty()) {
+ SPItem *item = sel->singleItem();
+ if (item) {
+ auto use = cast<SPUse>(item);
+ if (use) {
+ item = use->get_original();
+ }
+ auto shape = cast<SPShape>(item);
+ auto path = cast<SPPath>(item);
+ auto group = cast<SPGroup>(item);
+ dial._has_clip = (item->getClipObject() != nullptr);
+ dial._has_mask = (item->getMaskObject() != nullptr);
+ dial._item_type = "";
+ if (group) {
+ dial._item_type = "group";
+ } else if (path) {
+ dial._item_type = "path";
+ } else if (shape) {
+ dial._item_type = "shape";
+ } else {
+ dial._LPEDialogSelector->hide();
+ return;
+ }
+ }
+ }
+ dial._applied = false;
+ dial._LPESelectorFlowBox->unset_sort_func();
+ dial._LPESelectorFlowBox->unset_filter_func();
+ dial._LPESelectorFlowBox->set_filter_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_filter));
+ dial._LPESelectorFlowBox->set_sort_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_sort));
+ Glib::RefPtr<Gtk::Adjustment> vadjust = dial._LPEScrolled->get_vadjustment();
+ vadjust->set_value(vadjust->get_lower());
+ Gtk::Window *window = desktop->getToplevel();
+ dial._LPEDialogSelector->set_transient_for(*window);
+ dial._LPEDialogSelector->show();
+ int searchlen = dial._LPEFilter->get_text().length();
+ if (searchlen > 0) {
+ dial._LPEFilter->select_region (0, searchlen);
+ dial._LPESelectorFlowBox->unselect_all();
+ } else if (dial._lasteffect) {
+ dial._lasteffect->grab_focus();
+ }
+ dial._LPEDialogSelector->run();
+ dial._LPEDialogSelector->hide();
+}
+
+} // 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/dialog/livepatheffect-add.h b/src/ui/dialog/livepatheffect-add.h
new file mode 100644
index 0000000..bd8dca4
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-add.h
@@ -0,0 +1,147 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for adding a live path effect.
+ *
+ * Author:
+ *
+ * Copyright (C) 2012 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H
+#define INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H
+
+#include "live_effects/effect-enum.h"
+#include <gtkmm/adjustment.h>
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/flowbox.h>
+#include <gtkmm/flowboxchild.h>
+#include <gtkmm/image.h>
+#include <gtkmm/label.h>
+#include <gtkmm/overlay.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/stylecontext.h>
+#include <gtkmm/switch.h>
+#include <gtkmm/viewport.h>
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * A dialog widget to list the live path effects that can be added
+ *
+ */
+class LivePathEffectAdd {
+ public:
+ LivePathEffectAdd();
+ ~LivePathEffectAdd() = default;
+ ;
+
+ /**
+ * Show the dialog
+ */
+ static void show(SPDesktop *desktop);
+ /**
+ * Returns true is the "Add" button was pressed
+ */
+ static bool isApplied() { return instance()._applied; }
+
+ static const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *getActiveData();
+
+ protected:
+ /**
+ * Close button was clicked
+ */
+ void onClose();
+ bool on_filter(Gtk::FlowBoxChild *child);
+ int on_sort(Gtk::FlowBoxChild *child1, Gtk::FlowBoxChild *child2);
+ void on_search();
+ void on_focus(Gtk::Widget *widg);
+ bool pop_description(GdkEventCrossing *evt, Glib::RefPtr<Gtk::Builder> builder_effect);
+ bool hide_pop_description(GdkEventCrossing *evt);
+ bool fav_toggler(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect);
+ bool apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add);
+ bool on_press_enter(GdkEventKey *key, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add);
+ bool expand(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect);
+ bool show_fav_toggler(GdkEventButton *evt);
+ void viewChanged(gint mode);
+ bool mouseover(GdkEventCrossing *evt, GtkWidget *wdg);
+ bool mouseout(GdkEventCrossing *evt, GtkWidget *wdg);
+ void reload_effect_list();
+ /**
+ * Add button was clicked
+ */
+ void onAdd();
+ /**
+ * Tree was clicked
+ */
+ void onButtonEvent(GdkEventButton* evt);
+
+ /**
+ * Key event
+ */
+ void onKeyEvent(GdkEventKey* evt);
+private:
+ Gtk::Button _add_button;
+ Gtk::Button _close_button;
+ Gtk::Dialog *_LPEDialogSelector;
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::FlowBox *_LPESelectorFlowBox;
+ Gtk::Popover *_LPESelectorEffectInfoPop;
+ Gtk::EventBox *_LPESelectorEffectEventFavShow;
+ Gtk::EventBox *_LPESelectorEffectInfoEventBox;
+ Gtk::RadioButton *_LPESelectorEffectRadioList;
+ Gtk::RadioButton *_LPESelectorEffectRadioPackLess;
+ Gtk::RadioButton *_LPESelectorEffectRadioPackMore;
+ Gtk::Switch *_LPEExperimental;
+ Gtk::SearchEntry *_LPEFilter;
+ Gtk::ScrolledWindow *_LPEScrolled;
+ Gtk::Label *_LPEInfo;
+ Gtk::Box *_LPESelector;
+ guint _visiblelpe;
+ Glib::ustring _item_type;
+ bool _has_clip;
+ bool _has_mask;
+ Gtk::FlowBoxChild *_lasteffect;
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *_to_add;
+ bool _showfavs;
+ bool _applied;
+ class Effect;
+ const LivePathEffect::EnumEffectDataConverter<LivePathEffect::EffectType> &converter;
+ static LivePathEffectAdd &instance()
+ {
+ static LivePathEffectAdd instance_;
+ return instance_;
+ }
+ LivePathEffectAdd(LivePathEffectAdd const &) = delete; // no copy
+ LivePathEffectAdd &operator=(LivePathEffectAdd const &) = delete; // no assign
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_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/dialog/livepatheffect-editor.cpp b/src/ui/dialog/livepatheffect-editor.cpp
new file mode 100644
index 0000000..51e7437
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-editor.cpp
@@ -0,0 +1,1258 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for Live Path Effects (LPE)
+ */
+/* Authors:
+ * Jabiertxof
+ * Adam Belis (UX/Design)
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "livepatheffect-editor.h"
+#include "live_effects/effect-enum.h"
+#include "livepatheffect-add.h"
+#include "live_effects/effect.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpeobject.h"
+
+#include "object/sp-lpe-item.h"
+#include "svg/svg.h"
+#include "ui/icon-names.h"
+#include "ui/icon-loader.h"
+#include "ui/builder-utils.h"
+#include "io/resource.h"
+#include "object/sp-use.h"
+#include "object/sp-shape.h"
+#include "object/sp-path.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-tspan.h"
+#include "object/sp-item-group.h"
+#include "object/sp-text.h"
+#include "ui/tools/node-tool.h"
+#include "ui/widget/custom-tooltip.h"
+#include "util/optstr.h"
+#include <cstddef>
+#include <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace UI {
+
+
+bool sp_can_apply_lpeffect(SPLPEItem* item, LivePathEffect::EffectType etype) {
+ if (!item) return false;
+
+ auto shape = cast<SPShape>(item);
+ auto path = cast<SPPath>(item);
+ auto group = cast<SPGroup>(item);
+ Glib::ustring item_type;
+ if (group) {
+ item_type = "group";
+ } else if (path) {
+ item_type = "path";
+ } else if (shape) {
+ item_type = "shape";
+ }
+ bool has_clip = item->getClipObject() != nullptr;
+ bool has_mask = item->getMaskObject() != nullptr;
+ bool applicable = true;
+ if (!has_clip && etype == LivePathEffect::POWERCLIP) {
+ applicable = false;
+ }
+ if (!has_mask && etype == LivePathEffect::POWERMASK) {
+ applicable = false;
+ }
+ if (item_type == "group" && !Inkscape::LivePathEffect::LPETypeConverter.get_on_group(etype)) {
+ applicable = false;
+ } else if (item_type == "shape" && !Inkscape::LivePathEffect::LPETypeConverter.get_on_shape(etype)) {
+ applicable = false;
+ } else if (item_type == "path" && !Inkscape::LivePathEffect::LPETypeConverter.get_on_path(etype)) {
+ applicable = false;
+ }
+ return applicable;
+}
+
+void sp_apply_lpeffect(SPDesktop* desktop, SPLPEItem* item, LivePathEffect::EffectType etype) {
+ if (!sp_can_apply_lpeffect(item, etype)) return;
+
+ Glib::ustring key = Inkscape::LivePathEffect::LPETypeConverter.get_key(etype);
+ LivePathEffect::Effect::createAndApply(key.c_str(), item->document, item);
+ item->getCurrentLPE()->refresh_widgets = true;
+ DocumentUndo::done(item->document, _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+
+ if (desktop) {
+ // this is rotten - UI LPE knots refresh
+ // force selection change
+ desktop->getSelection()->clear();
+ desktop->getSelection()->add(item);
+ Inkscape::UI::Tools::sp_update_helperpath(desktop);
+ }
+}
+
+namespace Dialog {
+
+/*####################
+ * Callback functions
+ */
+
+bool sp_has_fav(Glib::ustring effect)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ size_t pos = favlist.find(effect);
+ if (pos != Glib::ustring::npos) {
+ return true;
+ }
+ return false;
+}
+
+void sp_add_fav(Glib::ustring effect)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ if (!sp_has_fav(effect)) {
+ prefs->setString("/dialogs/livepatheffect/favs", favlist + effect + ";");
+ }
+}
+
+void sp_remove_fav(Glib::ustring effect)
+{
+ if (sp_has_fav(effect)) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ effect += ";";
+ size_t pos = favlist.find(effect);
+ if (pos != Glib::ustring::npos) {
+ favlist.erase(pos, effect.length());
+ prefs->setString("/dialogs/livepatheffect/favs", favlist);
+ }
+ }
+}
+
+void sp_toggle_fav(Glib::ustring effect, Gtk::MenuItem *LPEtoggleFavorite)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs");
+ if (sp_has_fav(effect)) {
+ sp_remove_fav(effect);
+ LPEtoggleFavorite->set_label(_("Set Favorite"));
+ } else {
+ sp_add_fav(effect);
+ LPEtoggleFavorite->set_label(_("Unset Favorite"));
+ }
+}
+
+void LivePathEffectEditor::selectionChanged(Inkscape::Selection * selection)
+{
+ if (selection_changed_lock) {
+ return;
+ }
+ onSelectionChanged(selection);
+ clearMenu();
+}
+void LivePathEffectEditor::selectionModified(Inkscape::Selection * selection, guint flags)
+{
+ current_lpeitem = cast<SPLPEItem>(selection->singleItem());
+ if (!selection_changed_lock && current_lpeitem && effectlist != current_lpeitem->getEffectList()) {
+ onSelectionChanged(selection);
+ } else if (current_lpeitem && current_lperef.first) {
+ showParams(current_lperef, false);
+ }
+ clearMenu();
+}
+
+/**
+ * Constructor
+ */
+LivePathEffectEditor::LivePathEffectEditor()
+ : DialogBase("/dialogs/livepatheffect", "LivePathEffect"),
+ _builder(create_builder("dialog-livepatheffect.glade")),
+ LPEListBox(get_widget<Gtk::ListBox>(_builder, "LPEListBox")),
+ _LPEContainer(get_widget<Gtk::Box>(_builder, "LPEContainer")),
+ _LPEAddContainer(get_widget<Gtk::Box>(_builder, "LPEAddContainer")),
+ _LPEParentBox(get_widget<Gtk::ListBox>(_builder, "LPEParentBox")),
+ _LPECurrentItem(get_widget<Gtk::Box>(_builder, "LPECurrentItem")),
+ _LPESelectionInfo(get_widget<Gtk::Label>(_builder, "LPESelectionInfo")),
+ _LPEGallery(get_widget<Gtk::Button>(_builder, "LPEGallery")),
+ _showgallery_observer(Preferences::PreferencesObserver::create(
+ "/dialogs/livepatheffect/showgallery", sigc::mem_fun(*this, &LivePathEffectEditor::on_showgallery_notify))),
+ converter(Inkscape::LivePathEffect::LPETypeConverter)
+{
+ _LPEGallery.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onAddGallery));
+ _showgallery_observer->call(); // Set initial visibility per Preference (widget is :no-show-all)
+
+ Glib::RefPtr<Gtk::EntryCompletion> LPECompletionList = Glib::RefPtr<Gtk::EntryCompletion>::cast_dynamic(_builder->get_object("LPECompletionList"));
+
+ _LPEContainer.signal_map().connect(sigc::mem_fun(*this, &LivePathEffectEditor::map_handler) );
+ _LPEContainer.signal_button_press_event().connect([=](GdkEventButton* const evt){dnd = false; /*hack to fix dnd freze expander*/ return false; }, false);
+ setMenu();
+ add(_LPEContainer);
+ selection_info();
+ _lpes_popup.get_entry().set_placeholder_text(_("Add Live Path Effect"));
+ _lpes_popup.on_match_selected().connect([=](int id){ onAdd((LivePathEffect::EffectType)id); });
+ _lpes_popup.on_button_press().connect([=](){ setMenu(); });
+ _lpes_popup.on_focus().connect([=](){ setMenu(); return true; });
+ _LPEAddContainer.pack_start(_lpes_popup);
+ show_all();
+}
+
+LivePathEffectEditor::~LivePathEffectEditor()
+{
+ sp_clear_custom_tooltip();
+}
+
+bool separator_func(const Glib::RefPtr<Gtk::TreeModel>& model,
+ const Gtk::TreeModel::iterator& iter) {
+ Gtk::TreeModel::Row row = *iter;
+ bool *separator;
+ row->get_value(3, separator);
+ return separator;
+}
+
+bool
+LivePathEffectEditor::is_appliable(LivePathEffect::EffectType etype, Glib::ustring item_type, bool has_clip, bool has_mask) {
+ bool appliable = true;
+
+ if (!has_clip && etype == LivePathEffect::POWERCLIP) {
+ appliable = false;
+ }
+ if (!has_mask && etype == LivePathEffect::POWERMASK) {
+ appliable = false;
+ }
+ if (item_type == "group" && !converter.get_on_group(etype)) {
+ appliable = false;
+ } else if (item_type == "shape" && !converter.get_on_shape(etype)) {
+ appliable = false;
+ } else if (item_type == "path" && !converter.get_on_path(etype)) {
+ appliable = false;
+ }
+ return appliable;
+}
+
+void align(Gtk::Widget* top, gint spinbutton_width_chars) {
+ auto box = dynamic_cast<Gtk::Box*>(top);
+ if (!box) return;
+ box->set_spacing(2);
+
+ // traverse container, locate n-th child in each row
+ auto for_child_n = [=](int child_index, const std::function<void (Gtk::Widget*)>& action) {
+ for (auto child : box->get_children()) {
+ auto container = dynamic_cast<Gtk::Box*>(child);
+ if (!container) continue;
+ container->set_spacing(2);
+ const auto& children = container->get_children();
+ if (children.size() > child_index) {
+ action(children[child_index]);
+ }
+ }
+ };
+
+ // column 0 - labels
+ int max_width = 0;
+ for_child_n(0, [&](Gtk::Widget* child){
+ if (auto label = dynamic_cast<Gtk::Label*>(child)) {
+ label->set_xalign(0); // left-align
+ int label_width = 0, dummy = 0;
+ label->get_preferred_width(dummy, label_width);
+ if (label_width > max_width) {
+ max_width = label_width;
+ }
+ }
+ });
+ // align
+ for_child_n(0, [=](Gtk::Widget* child) {
+ if (auto label = dynamic_cast<Gtk::Label*>(child)) {
+ label->set_size_request(max_width);
+ }
+ });
+
+ // column 1 - align spin buttons, if any
+ int button_width = 0;
+ for_child_n(1, [&](Gtk::Widget* child) {
+ if (auto spin = dynamic_cast<Gtk::SpinButton*>(child)) {
+ // selected spinbutton size by each LPE default 7
+ spin->set_width_chars(spinbutton_width_chars);
+ int dummy = 0;
+ spin->get_preferred_width(dummy, button_width);
+ }
+ });
+ // set min size for comboboxes, if any
+ int combo_size = button_width > 0 ? button_width : 50; // match with spinbuttons, or just min of 50px
+ for_child_n(1, [=](Gtk::Widget* child) {
+ if (auto combo = dynamic_cast<Gtk::ComboBox*>(child)) {
+ combo->set_size_request(combo_size);
+ }
+ });
+}
+
+void
+LivePathEffectEditor::clearMenu()
+{
+ sp_clear_custom_tooltip();
+ _reload_menu = true;
+}
+
+void
+LivePathEffectEditor::toggleVisible(Inkscape::LivePathEffect::Effect *lpe , Gtk::EventBox *visbutton) {
+ auto *visimage = dynamic_cast<Gtk::Image *>(dynamic_cast<Gtk::Button *>(visbutton->get_children()[0])->get_image());
+ bool hide = false;
+ if (!g_strcmp0(lpe->getRepr()->attribute("is_visible"),"true")) {
+ visimage->set_from_icon_name("object-hidden-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ lpe->getRepr()->setAttribute("is_visible", "false");
+ hide = true;
+ } else {
+ visimage->set_from_icon_name("object-visible-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ lpe->getRepr()->setAttribute("is_visible", "true");
+ }
+ lpe->doOnVisibilityToggled(current_lpeitem);
+ DocumentUndo::done(getDocument(), hide ? _("Deactivate path effect") : _("Activate path effect"), INKSCAPE_ICON("dialog-path-effects"));
+}
+
+const Glib::ustring& get_category_name(Inkscape::LivePathEffect::LPECategory category) {
+ static const std::map<Inkscape::LivePathEffect::LPECategory, Glib::ustring> category_names = {
+ { Inkscape::LivePathEffect::LPECategory::Favorites, _("Favorites") },
+ { Inkscape::LivePathEffect::LPECategory::EditTools, _("Edit/Tools") },
+ { Inkscape::LivePathEffect::LPECategory::Distort, _("Distort") },
+ { Inkscape::LivePathEffect::LPECategory::Generate, _("Generate") },
+ { Inkscape::LivePathEffect::LPECategory::Convert, _("Convert") },
+ { Inkscape::LivePathEffect::LPECategory::Experimental, _("Experimental") },
+ };
+ return category_names.at(category);
+}
+
+struct LPEMetadata {
+ Inkscape::LivePathEffect::LPECategory category;
+ Glib::ustring icon_name;
+ Glib::ustring tooltip;
+ bool sensitive;
+};
+
+static std::map<Inkscape::LivePathEffect::EffectType, LPEMetadata> g_lpes;
+// populate popup with lpes and completion list for a search box
+void LivePathEffectEditor::add_lpes(Inkscape::UI::Widget::CompletionPopup& popup, bool symbolic) {
+ auto& menu = popup.get_menu();
+ struct LPE {
+ Inkscape::LivePathEffect::EffectType type;
+ Glib::ustring label;
+ Inkscape::LivePathEffect::LPECategory category;
+ Glib::ustring icon_name;
+ Glib::ustring tooltip;
+ bool sensitive;
+ };
+ std::vector<LPE> lpes;
+ lpes.reserve(g_lpes.size());
+ for (auto&& lpe : g_lpes) {
+ lpes.push_back({
+ lpe.first,
+ g_dpgettext2(0, "path effect", converter.get_label(lpe.first).c_str()),
+ lpe.second.category,
+ lpe.second.icon_name,
+ lpe.second.tooltip,
+ lpe.second.sensitive
+ });
+ }
+ std::sort(begin(lpes), end(lpes), [=](auto&& a, auto&& b) {
+ if (a.category != b.category) {
+ return a.category < b.category;
+ }
+ return a.label < b.label;
+ });
+
+ popup.clear_completion_list();
+
+ // 2-column menu
+ for (auto w:menu.get_children()) {
+ menu.remove(*w);
+ }
+ Inkscape::UI::ColumnMenuBuilder<Inkscape::LivePathEffect::LPECategory> builder(menu, 3, Gtk::ICON_SIZE_LARGE_TOOLBAR);
+
+ for (auto& lpe : lpes) {
+ // build popup menu
+ auto type = lpe.type;
+ auto *menuitem = builder.add_item(lpe.label, lpe.category, lpe.tooltip, lpe.icon_name, lpe.sensitive, true, [=](){ onAdd((LivePathEffect::EffectType)type); });
+ gint id = (gint)type;
+ menuitem->property_has_tooltip() = true;
+ menuitem->signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltipw){
+ return sp_query_custom_tooltip(x, y, kbd, tooltipw, id, lpe.tooltip, lpe.icon_name);
+ });
+ if (builder.new_section()) {
+ builder.set_section(get_category_name(lpe.category));
+ }
+
+ // build completion list
+ if (lpe.sensitive) {
+ popup.add_to_completion_list(static_cast<int>(lpe.type), lpe.label, lpe.icon_name + (symbolic ? "-symbolic" : ""));
+ }
+ }
+
+ if (symbolic) {
+ menu.get_style_context()->add_class("symbolic");
+ }
+}
+
+
+void
+LivePathEffectEditor::setMenu()
+{
+ if (!_reload_menu) {
+ return;
+ }
+ auto shape = cast<SPShape>(current_lpeitem);
+ auto path = cast<SPPath>(current_lpeitem);
+ auto group = cast<SPGroup>(current_lpeitem);
+ bool has_clip = current_lpeitem && (current_lpeitem->getClipObject() != nullptr);
+ bool has_mask = current_lpeitem && (current_lpeitem->getMaskObject() != nullptr);
+ Glib::ustring item_type = "";
+ if (group) {
+ item_type = "group";
+ } else if (path) {
+ item_type = "path";
+ } else if (shape) {
+ item_type = "shape";
+ }
+ if (_item_type != item_type || has_clip != _has_clip || has_mask != _has_mask) {
+ _item_type = item_type;
+ _has_clip = has_clip;
+ _has_mask = has_mask;
+ g_lpes.clear();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool experimental = prefs->getBool("/dialogs/livepatheffect/showexperimental", false);
+ std::map<Inkscape::LivePathEffect::LPECategory, std::map< Glib::ustring, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *> > lpesorted;
+ for (int i = 0; i < static_cast<int>(converter._length); ++i) {
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = &converter.data(i);
+ const Glib::ustring label = _(converter.get_label(data->id).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(data->id);
+ Glib::ustring name = label;
+ if (untranslated_label != label) {
+ name =+ "\n<span size='x-small'>" + untranslated_label + "</span>";
+ }
+ Inkscape::LivePathEffect::LPECategory category = converter.get_category(data->id);
+ if (sp_has_fav(untranslated_label)) {
+ //category = 0;
+ category = Inkscape::LivePathEffect::LPECategory::Favorites;
+ }
+ if (!experimental && category == Inkscape::LivePathEffect::LPECategory::Experimental) {
+ continue;
+ }
+ lpesorted[category][name] = data;
+ }
+ for (auto e : lpesorted) {
+ for (auto e2 : e.second) {
+ const Glib::ustring label = _(converter.get_label(e2.second->id).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(e2.second->id);
+ Glib::ustring tooltip = _(converter.get_description(e2.second->id).c_str());
+ if (untranslated_label != label) {
+ tooltip = "[" + untranslated_label + "] " + _(converter.get_description(e2.second->id).c_str());
+ }
+ Glib::ustring name = label;
+ Glib::ustring icon = converter.get_icon(e2.second->id);
+ LPEMetadata mdata;
+ mdata.category = e.first;
+ mdata.icon_name = icon;
+ mdata.tooltip = tooltip;
+ mdata.sensitive = is_appliable(e2.second->id, item_type, has_clip, has_mask);
+ g_lpes[e2.second->id] = mdata;
+ }
+ }
+ auto symbolic = Inkscape::Preferences::get()->getBool("/theme/symbolicIcons", true);
+ add_lpes(_lpes_popup, symbolic);
+ }
+}
+
+void LivePathEffectEditor::onAdd(LivePathEffect::EffectType etype)
+{
+ selection_changed_lock = true;
+ Glib::ustring key = converter.get_key(etype);
+ SPLPEItem *fromclone = clonetolpeitem();
+ if (fromclone) {
+ current_lpeitem = fromclone;
+ if (key == "clone_original") {
+ current_lpeitem->getCurrentLPE()->refresh_widgets = true;
+ selection_changed_lock = false;
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ return;
+ }
+ }
+ selection_changed_lock = false;
+ if (current_lpeitem) {
+ LivePathEffect::Effect::createAndApply(key.c_str(), getDocument(), current_lpeitem);
+ current_lpeitem->getCurrentLPE()->refresh_widgets = true;
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ }
+}
+
+void
+LivePathEffectEditor::map_handler()
+{
+ ensure_size();
+}
+
+void
+LivePathEffectEditor::selection_info()
+{
+ auto selection = getSelection();
+ SPItem * selected = nullptr;
+ _LPESelectionInfo.hide();
+ if (selection && (selected = selection->singleItem()) ) {
+ if (is<SPText>(selected) || is<SPFlowtext>(selected)) {
+ _LPESelectionInfo.set_text(_("Text objects do not support Live Path Effects"));
+ _LPESelectionInfo.show();
+ Glib::ustring labeltext = _("Convert text to paths");
+ Gtk::Button *selectbutton = Gtk::manage(new Gtk::Button());
+ Gtk::Box *boxc = Gtk::manage(new Gtk::Box());
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(labeltext));
+ std::string shape_type = "group";
+ std::string highlight = SPColor(selected->highlight_color()).toString();
+ Gtk::Image *type = Gtk::manage(new Gtk::Image(sp_get_shape_icon(shape_type, Gdk::RGBA(highlight),20, 1)));
+ boxc->pack_start(*type, false, false);
+ boxc->pack_start(*lbl, false, false);
+ type->set_margin_start(4);
+ type->set_margin_end(4);
+ selectbutton->add(*boxc);
+ selectbutton->signal_clicked().connect([=](){
+ selection->toCurves();
+ });
+ _LPEParentBox.add(*selectbutton);
+ Glib::ustring labeltext2 = _("Clone");
+ Gtk::Button *selectbutton2 = Gtk::manage(new Gtk::Button());
+ Gtk::Box *boxc2 = Gtk::manage(new Gtk::Box());
+ Gtk::Label *lbl2 = Gtk::manage(new Gtk::Label(labeltext2));
+ std::string shape_type2 = "clone";
+ std::string highlight2 = SPColor(selected->highlight_color()).toString();
+ Gtk::Image *type2 = Gtk::manage(new Gtk::Image(sp_get_shape_icon(shape_type2, Gdk::RGBA(highlight2),20, 1)));
+ boxc2->pack_start(*type2, false, false);
+ boxc2->pack_start(*lbl2, false, false);
+ type2->set_margin_start(4);
+ type2->set_margin_end(4);
+ selectbutton2->add(*boxc2);
+ selectbutton2->signal_clicked().connect([=](){
+ selection->clone();;
+ });
+ _LPEParentBox.add(*selectbutton2);
+ _LPEParentBox.show_all();
+ } else if (!is<SPLPEItem>(selected) && !is<SPUse>(selected)) {
+ _LPESelectionInfo.set_text(_("Select a path, shape, clone or group"));
+ _LPESelectionInfo.show();
+ } else {
+ if (selected->getId()) {
+ Glib::ustring labeltext = selected->label() ? selected->label() : selected->getId();
+ Gtk::Box *boxc = Gtk::manage(new Gtk::Box());
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(labeltext));
+ lbl->set_ellipsize(Pango::ELLIPSIZE_END);
+ std::string shape_type = selected->typeName();
+ std::string highlight = SPColor(selected->highlight_color()).toString();
+ Gtk::Image *type = Gtk::manage(new Gtk::Image(sp_get_shape_icon(shape_type, Gdk::RGBA(highlight),20, 1)));
+ boxc->pack_start(*type, false, false);
+ boxc->pack_start(*lbl, false, false);
+ _LPECurrentItem.add(*boxc);
+ _LPECurrentItem.get_children()[0]->set_halign(Gtk::ALIGN_CENTER);
+ _LPESelectionInfo.hide();
+ }
+ std::vector<std::pair <Glib::ustring, Glib::ustring> > newrootsatellites;
+ for (auto root : selected->rootsatellites) {
+ auto lpeobj = cast<LivePathEffectObject>(selected->document->getObjectById(root.second));
+ Inkscape::LivePathEffect::Effect *lpe = nullptr;
+ if (lpeobj) {
+ lpe = lpeobj->get_lpe();
+ }
+ if (lpe) {
+ const Glib::ustring label = _(converter.get_label(lpe->effectType()).c_str());
+ Glib::ustring labeltext = Glib::ustring::compose(_("Select %1 with %2 LPE"), root.first, label);
+ auto lpeitem = cast<SPLPEItem>(selected->document->getObjectById(root.first));
+ if (lpeitem && lpeitem->getLPEIndex(lpe) != Glib::ustring::npos) {
+ newrootsatellites.emplace_back(root.first, root.second);
+ Gtk::Button *selectbutton = Gtk::manage(new Gtk::Button());
+ Gtk::Box *boxc = Gtk::manage(new Gtk::Box());
+ Gtk::Label *lbl = Gtk::manage(new Gtk::Label(labeltext));
+ std::string shape_type = selected->typeName();
+ std::string highlight = SPColor(selected->highlight_color()).toString();
+ Gtk::Image *type = Gtk::manage(new Gtk::Image(sp_get_shape_icon(shape_type, Gdk::RGBA(highlight),20, 1)));
+ boxc->pack_start(*type, false, false);
+ boxc->pack_start(*lbl, false, false);
+ type->set_margin_start(4);
+ type->set_margin_end(4);
+ selectbutton->add(*boxc);
+ selectbutton->signal_clicked().connect([=](){
+ selection->set(lpeitem);
+ });
+ _LPEParentBox.add(*selectbutton);
+ }
+ }
+ }
+ selected->rootsatellites = newrootsatellites;
+ _LPEParentBox.show_all();
+ _LPEParentBox.drag_dest_unset();
+ _LPECurrentItem.show_all();
+ }
+ } else if (!selection || selection->isEmpty()) {
+ _LPESelectionInfo.set_text(_("Select a path, shape, clone or group"));
+ _LPESelectionInfo.show();
+ } else if (selection->size() > 1) {
+ _LPESelectionInfo.set_text(_("Select only one path, shape, clone or group"));
+ _LPESelectionInfo.show();
+ }
+}
+
+void
+LivePathEffectEditor::onSelectionChanged(Inkscape::Selection *sel)
+{
+ SPUse *use = nullptr;
+ _reload_menu = true;
+ if ( sel && !sel->isEmpty() ) {
+ SPItem *item = sel->singleItem();
+ if ( item ) {
+ auto lpeitem = cast<SPLPEItem>(item);
+ use = cast<SPUse>(item);
+ if (lpeitem) {
+ lpeitem->update_satellites();
+ current_lpeitem = lpeitem;
+ _LPEAddContainer.set_sensitive(true);
+ effect_list_reload(lpeitem);
+ return;
+ }
+ }
+ }
+ current_lpeitem = nullptr;
+ _LPEAddContainer.set_sensitive(use != nullptr);
+ clear_lpe_list();
+ selection_info();
+}
+
+void
+LivePathEffectEditor::move_list(gint origin, gint dest)
+{
+ Inkscape::Selection *sel = getDesktop()->getSelection();
+
+ if ( sel && !sel->isEmpty() ) {
+ SPItem *item = sel->singleItem();
+ if ( item ) {
+ auto lpeitem = cast<SPLPEItem>(item);
+ if ( lpeitem ) {
+ lpeitem->movePathEffect(origin, dest);
+ }
+ }
+ }
+}
+
+static const std::vector<Gtk::TargetEntry> entries = {Gtk::TargetEntry("GTK_LIST_BOX_ROW", Gtk::TARGET_SAME_APP, 0 )};
+
+void
+LivePathEffectEditor::showParams(std::pair<Gtk::Expander *, std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> > expanderdata, bool changed)
+{
+ LivePathEffectObject *lpeobj = expanderdata.second->lpeobject;
+
+ if (lpeobj) {
+ Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe();
+ if (lpe) {
+ if (effectwidget && !lpe->refresh_widgets && expanderdata == current_lperef && !changed) {
+ return;
+ }
+ if (effectwidget) {
+ effectwidget->get_parent()->remove(*effectwidget);
+ delete effectwidget;
+ effectwidget = nullptr;
+ }
+ effectwidget = lpe->newWidget();
+ if (!dynamic_cast<Gtk::Container *>(effectwidget)->get_children().size()) {
+ auto * label = new Gtk::Label("", Gtk::ALIGN_START, Gtk::ALIGN_CENTER);
+ label->set_markup(_("<small>Without parameters</small>"));
+ label->set_margin_top(5);
+ label->set_margin_bottom(5);
+ label->set_margin_start(5);
+ effectwidget = label;
+ }
+ expanderdata.first->add(*effectwidget);
+ expanderdata.first->show_all_children();
+ align(effectwidget, lpe->spinbutton_width_chars);
+ // fixme: add resizing of dialog
+ lpe->refresh_widgets = false;
+ ensure_size();
+ } else {
+ current_lperef = std::make_pair(nullptr, nullptr);
+ }
+ } else {
+ current_lperef = std::make_pair(nullptr, nullptr);
+ }
+
+ // effectwidget = effect.newWidget();
+ // effectcontrol_frame.set_label(effect.getName());
+ // effectcontrol_vbox.pack_start(*effectwidget, true, true);
+
+ // button_remove.show();
+ // status_label.hide();
+ // effectcontrol_vbox.show_all_children();
+ // align(effectwidget);
+ // effectcontrol_frame.show();
+ // // fixme: add resizing of dialog
+ // effect.refresh_widgets = false;
+}
+
+bool
+LivePathEffectEditor::closeExpander(GdkEventButton * evt) {
+ current_lperef.first->set_expanded(false);
+ return false;
+}
+
+/*
+ * First clears the effectlist_store, then appends all effects from the effectlist.
+ */
+void
+LivePathEffectEditor::effect_list_reload(SPLPEItem *lpeitem)
+{
+ clear_lpe_list();
+ _LPEExpanders.clear();
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-item.glade");
+ gint counter = -1;
+ Gtk::Expander *LPEExpanderCurrent = nullptr;
+ effectlist = lpeitem->getEffectList();
+ gint total = effectlist.size();
+ if (total > 1) {
+ _LPECurrentItem.drag_dest_unset();
+ _lpes_popup.drag_dest_unset();
+ _lpes_popup.get_entry().drag_dest_unset();
+ _LPEAddContainer.drag_dest_unset();
+ _LPEContainer.drag_dest_set(entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
+ _LPEContainer.signal_drag_data_received().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, const Gtk::SelectionData& selection_data, guint info, guint time)
+ {
+ if (dnd) {
+ unsigned int pos_target, pos_source;
+ Gtk::Widget *target = &_LPEContainer;
+ pos_source = atoi(reinterpret_cast<char const*>(selection_data.get_data()));
+ pos_target = LPEListBox.get_children().size()-1;
+ if (y < 90) {
+ pos_target = 0;
+ }
+ if (pos_target == pos_source) {
+ gtk_drag_finish(context->gobj(), FALSE, FALSE, time);
+ dnd = false;
+ return;
+ }
+ Glib::RefPtr<Gtk::StyleContext> stylec = target->get_style_context();
+ if (pos_source > pos_target) {
+ if (stylec->has_class("after")) {
+ pos_target ++;
+ }
+ } else if (pos_source < pos_target) {
+ if (stylec->has_class("before")) {
+ pos_target --;
+ }
+ }
+ Gtk::Widget *source = LPEListBox.get_row_at_index(pos_source);
+ g_object_ref(source->gobj());
+ LPEListBox.remove(*source);
+ LPEListBox.insert(*source, pos_target);
+ g_object_unref(source->gobj());
+ move_list(pos_source,pos_target);
+ gtk_drag_finish(context->gobj(), TRUE, TRUE, time);
+ dnd = false;
+ }
+ });
+ _LPEContainer.signal_drag_motion().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time)
+ {
+ Glib::RefPtr<Gtk::StyleContext> stylec = _LPEContainer.get_style_context();
+ if (y < 90) {
+ stylec->add_class("before");
+ stylec->remove_class("after");
+ } else {
+ stylec->remove_class("before");
+ stylec->add_class("after");
+ }
+ return true;
+ }, true);
+ }
+ PathEffectList::iterator it;
+ Gtk::MenuItem *LPEMoveUpExtrem = nullptr;
+ Gtk::MenuItem *LPEMoveDownExtrem = nullptr;
+ Gtk::EventBox *LPEDrag = nullptr;
+ for( it = effectlist.begin() ; it!=effectlist.end(); ++it)
+ {
+ if ( !(*it)->lpeobject ) {
+ continue;
+ }
+ auto lpe = (*it)->lpeobject->get_lpe();
+ bool current = lpeitem->getCurrentLPE() == lpe;
+ counter++;
+ Glib::RefPtr<Gtk::Builder> builder;
+ if (lpe) {
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for path effect dialog");
+ return;
+ }
+ Gtk::Box *LPEEffect;
+ Gtk::Box *LPEExpanderBox;
+ Gtk::Box *LPEActionButtons;
+ Gtk::EventBox *LPEOpenExpander;
+ Gtk::Expander *LPEExpander;
+ Gtk::Image *LPEIconImage;
+ Gtk::EventBox *LPEErase;
+ Gtk::EventBox *LPEHide;
+ Gtk::MenuItem *LPEtoggleFavorite;
+ Gtk::Label *LPENameLabel;
+ Gtk::Menu *LPEEffectMenu;
+ Gtk::MenuItem *LPEMoveUp;
+ Gtk::MenuItem *LPEMoveDown;
+ Gtk::MenuItem *LPEResetDefault;
+ Gtk::MenuItem *LPESetDefault;
+ builder->get_widget("LPEMoveUp", LPEMoveUp);
+ builder->get_widget("LPEMoveDown", LPEMoveDown);
+ builder->get_widget("LPEResetDefault", LPEResetDefault);
+ builder->get_widget("LPESetDefault", LPESetDefault);
+ builder->get_widget("LPENameLabel", LPENameLabel);
+ builder->get_widget("LPEEffectMenu", LPEEffectMenu);
+ builder->get_widget("LPEHide", LPEHide);
+ builder->get_widget("LPEIconImage", LPEIconImage);
+ builder->get_widget("LPEExpanderBox", LPEExpanderBox);
+ builder->get_widget("LPEEffect", LPEEffect);
+ builder->get_widget("LPEExpander", LPEExpander);
+ builder->get_widget("LPEOpenExpander", LPEOpenExpander);
+ builder->get_widget("LPEErase", LPEErase);
+ builder->get_widget("LPEDrag", LPEDrag);
+ builder->get_widget("LPEActionButtons", LPEActionButtons);
+ builder->get_widget("LPEtoggleFavorite", LPEtoggleFavorite);
+ LPEExpander->drag_dest_unset();
+ LPEActionButtons->drag_dest_unset();
+ LPEMoveUp->show();
+ LPEMoveDown->show();
+ LPEDrag->get_children()[0]->show();
+ LPEDrag->set_tooltip_text(_("Drag to change position in path effects stack"));
+ if (current) {
+ LPEExpanderCurrent = LPEExpander;
+ }
+ if (counter == 0) {
+ LPEMoveUpExtrem = LPEMoveUp;
+ }
+ LPEMoveDownExtrem = LPEMoveDown;
+ auto effectype = lpe->effectType();
+ const Glib::ustring label = _(converter.get_label(effectype).c_str());
+ const Glib::ustring untranslated_label = converter.get_label(effectype);
+ const Glib::ustring icon = converter.get_icon(effectype);
+ LPEIconImage->set_from_icon_name(icon, Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ Glib::ustring lpename = "";
+ if (untranslated_label == label) {
+ lpename = label;
+ } else {
+ lpename = (label + "\n<span size='x-small'>" + untranslated_label + "</span>");
+ }
+ auto *visimage = dynamic_cast<Gtk::Image *>(dynamic_cast<Gtk::Button *>(LPEHide->get_children()[0])->get_image());
+ if (!g_strcmp0(lpe->getRepr()->attribute("is_visible"),"true")) {
+ visimage->set_from_icon_name("object-visible-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ } else {
+ visimage->set_from_icon_name("object-hidden-symbolic", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ }
+ _LPEExpanders.emplace_back(LPEExpander, (*it));
+ LPEListBox.add(*LPEEffect);
+
+ Glib::ustring name = "drag_";
+ name += Glib::ustring::format(counter);
+ LPEDrag->set_name(name);
+ if (total > 1) {
+ //DnD
+ LPEDrag->drag_source_set(entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE);
+ }
+ Glib::ustring tooltip = _(converter.get_description(effectype).c_str());
+ if (untranslated_label != label) {
+ tooltip = "[" + untranslated_label + "] " + _(converter.get_description(effectype).c_str());
+ }
+ gint id = (gint)effectype;
+ LPEExpanderBox->property_has_tooltip() = true;
+ LPEExpanderBox->signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltipw){
+ return sp_query_custom_tooltip(x, y, kbd, tooltipw, id, tooltip, icon);
+ });
+ size_t pos = 0;
+ std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef = (*it);
+ for (auto w : LPEEffectMenu->get_children()) {
+ auto * mitem = dynamic_cast<Gtk::MenuItem *>(w);
+ if (mitem) {
+ mitem->signal_activate().connect([=](){
+ if (pos == 0) {
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lpeitem->duplicateCurrentPathEffect();
+ effect_list_reload(current_lpeitem);
+ DocumentUndo::done(getDocument(), _("Duplicate path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ } else if (pos == 1) {
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lpeitem->upCurrentPathEffect();
+ effect_list_reload(current_lpeitem);
+ DocumentUndo::done(getDocument(), _("Move path effect up"), INKSCAPE_ICON("dialog-path-effects"));
+ } else if (pos == 2) {
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lpeitem->downCurrentPathEffect();
+ effect_list_reload(current_lpeitem);
+ DocumentUndo::done(getDocument(), _("Move path effect down"), INKSCAPE_ICON("dialog-path-effects"));
+ } else if (pos == 3) {
+ lpeFlatten(lperef);
+ } else if (pos == 4) {
+ lpe->setDefaultParameters();
+ effect_list_reload(current_lpeitem);
+ } else if (pos == 5) {
+ lpe->resetDefaultParameters();
+ effect_list_reload(current_lpeitem);
+ } else if (pos == 6) {
+ sp_toggle_fav(untranslated_label, LPEtoggleFavorite);
+ _reload_menu = true;
+ _item_type = ""; // here we force reload even with the same tipe item selected
+ }
+
+ });
+ if (pos == 6) {
+ if (sp_has_fav(untranslated_label)) {
+ LPEtoggleFavorite->set_label(_("Unset Favorite"));
+ } else {
+ LPEtoggleFavorite->set_label(_("Set Favorite"));
+ }
+ }
+ }
+ pos ++;
+ }
+ if (total > 1) {
+ LPEDrag->signal_drag_begin().connect([=](const Glib::RefPtr<Gdk::DragContext> context){
+ cairo_surface_t *surface;
+ cairo_t *cr;
+ int x, y;
+ double sx = 1;
+ double sy = 1;
+ dnd = true;
+ Gtk::Allocation alloc = LPEEffect->get_allocation ();
+ auto device_scale = get_scale_factor();
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, alloc.get_width() * device_scale, alloc.get_height() * device_scale);
+ cairo_surface_set_device_scale(surface, device_scale, device_scale);
+ cr = cairo_create (surface);
+ LPEEffect->get_style_context()->add_class("drag-icon");
+ gtk_widget_draw (GTK_WIDGET(LPEEffect->gobj()), cr);
+ LPEEffect->get_style_context()->remove_class("drag-icon");
+ LPEDrag->translate_coordinates(*LPEEffect, dndx, dndy, x, y);
+ #ifndef __APPLE__
+ cairo_surface_get_device_scale (surface, &sx, &sy);
+ #endif
+ cairo_surface_set_device_offset (surface, -x * sx, -y * sy);
+ gtk_drag_set_icon_surface (context->gobj(), surface);
+ cairo_destroy (cr);
+ cairo_surface_destroy (surface);
+ });
+ auto row = dynamic_cast<Gtk::ListBoxRow *>(LPEEffect->get_parent());
+ LPEDrag->signal_drag_data_get().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, Gtk::SelectionData& selection_data, guint info, guint time)
+ {
+ selection_data.set("GTK_LIST_BOX_ROW", Glib::ustring::format(row->get_index()));
+ });
+ LPEDrag->signal_drag_end().connect([=](const Glib::RefPtr<Gdk::DragContext>& context)
+ {
+ dnd = false;
+ });
+ row->signal_drag_data_received().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, const Gtk::SelectionData& selection_data, guint info, guint time)
+ {
+ if (dnd) {
+ unsigned int pos_target, pos_source;
+ Gtk::Widget *target = row;
+ pos_target = row->get_index();
+ pos_source = atoi(reinterpret_cast<char const*>(selection_data.get_data()));
+ Glib::RefPtr<Gtk::StyleContext> stylec = target->get_style_context();
+ if (pos_source > pos_target) {
+ if (stylec->has_class("after")) {
+ pos_target ++;
+ }
+ } else if (pos_source < pos_target) {
+ if (stylec->has_class("before")) {
+ pos_target --;
+ }
+ }
+ Gtk::Widget *source = LPEListBox.get_row_at_index(pos_source);
+ if (source == target) {
+ gtk_drag_finish(context->gobj(), FALSE, FALSE, time);
+ dnd = false;
+ return;
+ }
+ g_object_ref(source->gobj());
+ LPEListBox.remove(*source);
+ LPEListBox.insert(*source, pos_target);
+ g_object_unref(source->gobj());
+ move_list(pos_source,pos_target);
+ gtk_drag_finish(context->gobj(), TRUE, TRUE, time);
+ dnd = false;
+ }
+ });
+ row->drag_dest_set(entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE);
+ row->signal_drag_motion().connect([=](const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time)
+ {
+ gint half = row->get_allocated_height()/2;
+ Glib::RefPtr<Gtk::StyleContext> stylec = row->get_style_context();
+ if (y < half) {
+ stylec->add_class("before");
+ stylec->remove_class("after");
+ } else {
+ stylec->remove_class("before");
+ stylec->add_class("after");
+ }
+ return true;
+ }, true);
+ }
+ // other
+ LPEEffect->set_name("LPEEffectItem");
+ LPENameLabel->set_label(g_dpgettext2(nullptr, "path effect", (*it)->lpeobject->get_lpe()->getName().c_str()));
+ LPEExpander->property_expanded().signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &LivePathEffectEditor::expanded_notify),LPEExpander));
+ LPEOpenExpander->signal_button_press_event().connect([=](GdkEventButton* const evt){
+ LPEExpander->set_expanded(!LPEExpander->property_expanded());
+ return false;
+ }, false);
+ dynamic_cast<Gtk::Button *>(LPEHide->get_children()[0])->signal_clicked().connect(sigc::bind<Inkscape::LivePathEffect::Effect *, Gtk::EventBox *>(sigc::mem_fun(*this, &LivePathEffectEditor::toggleVisible), lpe, LPEHide));
+ LPEDrag->signal_button_press_event().connect([=](GdkEventButton* const evt){dndx = evt->x; dndy = evt->y; return false; }, false);
+ dynamic_cast<Gtk::Button *>(LPEErase->get_children()[0])->signal_clicked().connect([=](){ removeEffect(LPEExpander);});
+ if (total > 1) {
+ LPEDrag->signal_enter_notify_event().connect([=](GdkEventCrossing*){
+ auto window = get_window();
+ auto display = get_display();
+ auto cursor = Gdk::Cursor::create(display, "grab");
+ window->set_cursor(cursor);
+ return false;
+ }, false);
+ LPEDrag->signal_leave_notify_event().connect([=](GdkEventCrossing*){
+ auto window = get_window();
+ auto display = get_display();
+ auto cursor = Gdk::Cursor::create(display, "default");
+ window->set_cursor(cursor);
+ return false;
+ }, false);
+ }
+ if (lpe->hasDefaultParameters()) {
+ LPEResetDefault->show();
+ LPESetDefault->hide();
+
+ } else {
+ LPEResetDefault->hide();
+ LPESetDefault->show();
+ }
+ }
+ }
+ if (counter == 0 && LPEDrag) {
+ LPEDrag->get_children()[0]->hide();
+ LPEDrag->set_tooltip_text("");
+ }
+ if (LPEMoveUpExtrem) {
+ LPEMoveUpExtrem->hide();
+ LPEMoveDownExtrem->hide();
+ }
+ if (LPEExpanderCurrent) {
+ _LPESelectionInfo.hide();
+ LPEExpanderCurrent->set_expanded(true);
+ Gtk::Window *current_window = dynamic_cast<Gtk::Window *>(LPEExpanderCurrent->get_toplevel());
+ if (current_window) {
+ current_window->set_focus(*LPEExpanderCurrent);
+ }
+ }
+ selection_info();
+ LPEListBox.show_all_children();
+ ensure_size();
+}
+
+void LivePathEffectEditor::expanded_notify(Gtk::Expander *expander) {
+
+ if (updating) {
+ return;
+ }
+ if (!dnd) {
+ _freezeexpander = false;
+ }
+ if (_freezeexpander) {
+ _freezeexpander = false;
+ return;
+ }
+ if (dnd) {
+ _freezeexpander = true;
+ expander->set_expanded(!expander->get_expanded());
+ return;
+ };
+ updating = true;
+ if (expander->get_expanded()) {
+ for (auto &w : _LPEExpanders){
+ if (w.first == expander) {
+ w.first->set_expanded(true);
+ w.first->get_parent()->get_parent()->get_parent()->set_name("currentlpe");
+ current_lperef = w;
+ current_lpeitem->setCurrentPathEffect(w.second);
+ showParams(w, true);
+ } else {
+ w.first->set_expanded(false);
+ w.first->get_parent()->get_parent()->get_parent()->set_name("unactive_lpe");
+ }
+ }
+ }
+ auto selection = SP_ACTIVE_DESKTOP->getSelection();
+ if (selection && current_lpeitem && !selection->isEmpty()) {
+ selection_changed_lock = true;
+ selection->clear();
+ selection->add(current_lpeitem);
+ Inkscape::UI::Tools::sp_update_helperpath(getDesktop());
+ selection_changed_lock = false;
+ }
+ updating = false;
+}
+
+bool
+LivePathEffectEditor::lpeFlatten(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef)
+{
+ current_lpeitem->setCurrentPathEffect(lperef);
+ current_lpeitem = current_lpeitem->flattenCurrentPathEffect();
+ auto selection = getSelection();
+ if (selection && selection->isEmpty() ) {
+ selection->add(current_lpeitem);
+ }
+ DocumentUndo::done(getDocument(), _("Flatten path effect(s)"), INKSCAPE_ICON("dialog-path-effects"));
+ return false;
+}
+
+void
+LivePathEffectEditor::removeEffect(Gtk::Expander * expander) {
+ bool reload = current_lperef.first != expander;
+ auto current_lperef_tmp = current_lperef;
+ for (auto &w : _LPEExpanders){
+ if (w.first == expander) {
+ current_lpeitem->setCurrentPathEffect(w.second);
+ current_lpeitem = current_lpeitem->removeCurrentPathEffect(false);
+ }
+ }
+ if (reload) {
+ current_lpeitem->setCurrentPathEffect(current_lperef_tmp.second);
+ }
+ effect_list_reload(current_lpeitem);
+ DocumentUndo::done(getDocument(), _("Remove path effect"), INKSCAPE_ICON("dialog-path-effects"));
+}
+
+bool
+LivePathEffectEditor::toggleFavInLpe(GdkEventButton * evt, Glib::ustring name, Gtk::Button *favbutton) {
+ auto *favimage = dynamic_cast<Gtk::Image *>(favbutton->get_image());
+ if (favimage->get_icon_name() == "draw-star") {
+ favbutton->set_image_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ sp_remove_fav(name);
+ } else {
+ favbutton->set_image_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR));
+ sp_add_fav(name);
+ }
+ clearMenu();
+ return false;
+}
+
+
+
+
+
+/*
+ * Clears the effectlist
+ */
+void
+LivePathEffectEditor::clear_lpe_list()
+{
+ for (auto &w : LPEListBox.get_children()) {
+ LPEListBox.remove(*w);
+ }
+ for (auto &w : _LPEParentBox.get_children()) {
+ _LPEParentBox.remove(*w);
+ }
+ for (auto &w : _LPECurrentItem.get_children()) {
+ _LPECurrentItem.remove(*w);
+ }
+}
+
+SPLPEItem * LivePathEffectEditor::clonetolpeitem()
+{
+ auto selection = getSelection();
+ if (selection && !selection->isEmpty() ) {
+ auto use = cast<SPUse>(selection->singleItem());
+ if ( use ) {
+ DocumentUndo::ScopedInsensitive tmp(getDocument());
+ // item is a clone. do not show effectlist dialog.
+ // convert to path, apply CLONE_ORIGINAL LPE, link it to the cloned path
+
+ // test whether linked object is supported by the CLONE_ORIGINAL LPE
+ SPItem *orig = use->get_original();
+ if ( is<SPShape>(orig) || is<SPGroup>(orig) || is<SPText>(orig) ) {
+ // select original
+ selection->set(orig);
+
+ // delete clone but remember its id and transform
+ auto id_copy = Util::to_opt(use->getAttribute("id"));
+ auto transform_copy = Util::to_opt(use->getAttribute("transform"));
+ use->deleteObject(false);
+ use = nullptr;
+
+ // run sp_selection_clone_original_path_lpe
+ selection->cloneOriginalPathLPE(true, true, true);
+
+ SPItem *new_item = selection->singleItem();
+ // Check that the cloning was successful. We don't want to change the ID of the original referenced path!
+ if (new_item && (new_item != orig)) {
+ new_item->setAttribute("id", Util::to_cstr(id_copy));
+ if (Util::to_cstr(transform_copy)) {
+ Geom::Affine item_t(Geom::identity());
+ sp_svg_transform_read(Util::to_cstr(transform_copy), &item_t);
+ new_item->transform *= item_t;
+ new_item->doWriteTransform(new_item->transform);
+ new_item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+ new_item->setAttribute("class", "fromclone");
+ }
+
+ auto *lpeitem = cast<SPLPEItem>(new_item);
+ if (lpeitem) {
+ sp_lpe_item_update_patheffect(lpeitem, true, true);
+ return lpeitem;
+ }
+ }
+ }
+ }
+ return nullptr;
+}
+
+void LivePathEffectEditor::onAddGallery()
+{
+ // show effectlist dialog
+ using Inkscape::UI::Dialog::LivePathEffectAdd;
+ LivePathEffectAdd::show(getDesktop());
+ clearMenu();
+ if ( !LivePathEffectAdd::isApplied()) {
+ return;
+ }
+
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = LivePathEffectAdd::getActiveData();;
+ if (!data) {
+ return;
+ }
+ selection_changed_lock = true;
+ SPLPEItem *fromclone = clonetolpeitem();
+ if (fromclone) {
+ current_lpeitem = fromclone;
+ if (data->key == "clone_original") {
+ current_lpeitem->getCurrentLPE()->refresh_widgets = true;
+ selection_changed_lock = false;
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ return;
+ }
+
+ }
+ selection_changed_lock = false;
+ if (current_lpeitem) {
+ LivePathEffect::Effect::createAndApply(data->key.c_str(), getDocument(), current_lpeitem);
+ current_lpeitem->getCurrentLPE()->refresh_widgets = true;
+ DocumentUndo::done(getDocument(), _("Create and apply path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ }
+}
+
+void LivePathEffectEditor::on_showgallery_notify(Preferences::Entry const &new_val)
+{
+ _LPEGallery.set_visible(new_val.getBool());
+}
+
+} // 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/dialog/livepatheffect-editor.h b/src/ui/dialog/livepatheffect-editor.h
new file mode 100644
index 0000000..8169270
--- /dev/null
+++ b/src/ui/dialog/livepatheffect-editor.h
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for Live Path Effects (LPE)
+ */
+/* Authors:
+ * Jabiertxof
+ * Adam Belis (UX/Design)
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef LIVEPATHEFFECTEDITOR_H
+#define LIVEPATHEFFECTEDITOR_H
+
+#include <memory>
+#include <gtkmm/builder.h>
+#include "live_effects/effect-enum.h"
+#include "preferences.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/completion-popup.h"
+#include "ui/column-menu-builder.h"
+
+namespace Gtk {
+class Button;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*
+ * @brief The LivePathEffectEditor class
+ */
+class LivePathEffectEditor : public DialogBase
+{
+public:
+ // No default constructor, noncopyable, nonassignable
+ LivePathEffectEditor();
+ ~LivePathEffectEditor() override;
+ LivePathEffectEditor(LivePathEffectEditor const &d) = delete;
+ LivePathEffectEditor operator=(LivePathEffectEditor const &d) = delete;
+ static LivePathEffectEditor &getInstance() { return *new LivePathEffectEditor(); }
+ void move_list(gint origin, gint dest);
+ std::vector<std::pair<Gtk::Expander *, std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> > > _LPEExpanders;
+ void showParams(std::pair<Gtk::Expander *, std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> > expanderdata, bool changed);
+ bool updating = false;
+ SPLPEItem *current_lpeitem = nullptr;
+ std::pair<Gtk::Expander *, std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> > current_lperef = std::make_pair(nullptr, nullptr);
+ static const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *getActiveData();
+ bool selection_changed_lock = false;
+ bool dnd = false;
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+public:
+ Gtk::ListBox& LPEListBox;
+ gint dndx = 0;
+ gint dndy = 0;
+protected:
+ bool apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect,
+ const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add);
+ void reload_effect_list();
+ void onButtonEvent(GdkEventButton* evt);
+
+private:
+ void add_lpes(Inkscape::UI::Widget::CompletionPopup& popup, bool symbolic);
+ void clear_lpe_list();
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+ void onSelectionChanged(Inkscape::Selection *selection);
+ bool toggleFavInLpe(GdkEventButton * evt, Glib::ustring name, Gtk::Button *favbutton);
+ bool closeExpander(GdkEventButton * evt);
+ void onAddGallery();
+ void expanded_notify(Gtk::Expander *expander);
+ void onAdd(Inkscape::LivePathEffect::EffectType etype);
+ void toggleVisible(Inkscape::LivePathEffect::Effect *lpe , Gtk::EventBox *visbutton);
+ bool is_appliable(LivePathEffect::EffectType etypen, Glib::ustring item_type, bool has_clip, bool has_mask);
+ void removeEffect(Gtk::Expander * expander);
+ void effect_list_reload(SPLPEItem *lpeitem);
+
+ SPLPEItem * clonetolpeitem();
+ void selection_info();
+ Inkscape::UI::Widget::CompletionPopup _lpes_popup;
+ void map_handler();
+ void clearMenu();
+ void setMenu();
+ bool lpeFlatten(std::shared_ptr<Inkscape::LivePathEffect::LPEObjectReference> lperef);
+ Gtk::Box& _LPEContainer;
+ Gtk::Box& _LPEAddContainer;
+ Gtk::Label&_LPESelectionInfo;
+ Gtk::ListBox&_LPEParentBox;
+ Gtk::Box&_LPECurrentItem;
+ PathEffectList effectlist;
+ Glib::RefPtr<Gtk::ListStore> _LPEList;
+ Glib::RefPtr<Gtk::ListStore> _LPEListFilter;
+ const LivePathEffect::EnumEffectDataConverter<LivePathEffect::EffectType> &converter;
+ Gtk::Widget *effectwidget = nullptr;
+ Gtk::Widget *popupwidg = nullptr;
+ GtkWidget *currentdrag = nullptr;
+ bool _reload_menu = false;
+ gint _buttons_width = 0;
+ bool _freezeexpander = false;
+ Glib::ustring _item_type;
+ bool _has_clip;
+ bool _has_mask;
+ bool _frezee = false;
+
+ Gtk::Button &_LPEGallery;
+ std::unique_ptr<Preferences::PreferencesObserver> const _showgallery_observer;
+ void on_showgallery_notify(Preferences::Entry const &new_val);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // LIVEPATHEFFECTEDITOR_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/dialog/lpe-fillet-chamfer-properties.cpp b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp
new file mode 100644
index 0000000..f2a2655
--- /dev/null
+++ b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp
@@ -0,0 +1,255 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * From the code of Liam P.White from his Power Stroke Knot dialog
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <gtkmm.h>
+#include "lpe-fillet-chamfer-properties.h"
+#include <boost/lexical_cast.hpp>
+#include <glibmm/i18n.h>
+#include "inkscape.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "layer-manager.h"
+#include "message-stack.h"
+
+#include "selection-chemistry.h"
+
+//#include "event-context.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+FilletChamferPropertiesDialog::FilletChamferPropertiesDialog()
+ : _knotpoint(nullptr),
+ _position_visible(false),
+ _close_button(_("_Cancel"), true)
+{
+ Gtk::Box *mainVBox = get_content_area();
+ mainVBox->set_homogeneous(false);
+ _layout_table.set_row_spacing(4);
+ _layout_table.set_column_spacing(4);
+
+ // Layer name widgets
+ _fillet_chamfer_position_numeric.set_digits(4);
+ _fillet_chamfer_position_numeric.set_increments(1,1);
+ //todo: get the max allowable infinity freeze the widget
+ _fillet_chamfer_position_numeric.set_range(0., SCALARPARAM_G_MAXDOUBLE);
+ _fillet_chamfer_position_numeric.set_hexpand();
+ _fillet_chamfer_position_label.set_label(_("Radius (pixels):"));
+ _fillet_chamfer_position_label.set_halign(Gtk::ALIGN_END);
+ _fillet_chamfer_position_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _layout_table.attach(_fillet_chamfer_position_label, 0, 0, 1, 1);
+ _layout_table.attach(_fillet_chamfer_position_numeric, 1, 0, 1, 1);
+ _fillet_chamfer_chamfer_subdivisions.set_digits(0);
+ _fillet_chamfer_chamfer_subdivisions.set_increments(1,1);
+ //todo: get the max allowable infinity freeze the widget
+ _fillet_chamfer_chamfer_subdivisions.set_range(0, SCALARPARAM_G_MAXDOUBLE);
+ _fillet_chamfer_chamfer_subdivisions.set_hexpand();
+ _fillet_chamfer_chamfer_subdivisions_label.set_label(_("Chamfer subdivisions:"));
+ _fillet_chamfer_chamfer_subdivisions_label.set_halign(Gtk::ALIGN_END);
+ _fillet_chamfer_chamfer_subdivisions_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _layout_table.attach(_fillet_chamfer_chamfer_subdivisions_label, 0, 1, 1, 1);
+ _layout_table.attach(_fillet_chamfer_chamfer_subdivisions, 1, 1, 1, 1);
+ _fillet_chamfer_type_fillet.set_label(_("Fillet"));
+ _fillet_chamfer_type_fillet.set_group(_fillet_chamfer_type_group);
+ _fillet_chamfer_type_inverse_fillet.set_label(_("Inverse fillet"));
+ _fillet_chamfer_type_inverse_fillet.set_group(_fillet_chamfer_type_group);
+ _fillet_chamfer_type_chamfer.set_label(_("Chamfer"));
+ _fillet_chamfer_type_chamfer.set_group(_fillet_chamfer_type_group);
+ _fillet_chamfer_type_inverse_chamfer.set_label(_("Inverse chamfer"));
+ _fillet_chamfer_type_inverse_chamfer.set_group(_fillet_chamfer_type_group);
+
+
+ mainVBox->pack_start(_layout_table, true, true, 4);
+ mainVBox->pack_start(_fillet_chamfer_type_fillet, true, true, 4);
+ mainVBox->pack_start(_fillet_chamfer_type_inverse_fillet, true, true, 4);
+ mainVBox->pack_start(_fillet_chamfer_type_chamfer, true, true, 4);
+ mainVBox->pack_start(_fillet_chamfer_type_inverse_chamfer, true, true, 4);
+
+ // Buttons
+ _close_button.set_can_default();
+
+ _apply_button.set_use_underline(true);
+ _apply_button.set_can_default();
+
+ _close_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_close));
+ _apply_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_apply));
+
+ signal_delete_event().connect(sigc::bind_return(
+ sigc::hide(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_close)),
+ true));
+
+ add_action_widget(_close_button, Gtk::RESPONSE_CLOSE);
+ add_action_widget(_apply_button, Gtk::RESPONSE_APPLY);
+
+ _apply_button.grab_default();
+
+ show_all_children();
+
+ set_focus(_fillet_chamfer_position_numeric);
+}
+
+FilletChamferPropertiesDialog::~FilletChamferPropertiesDialog()
+{
+}
+
+void FilletChamferPropertiesDialog::showDialog(SPDesktop *desktop, double _amount,
+ const Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *pt,
+ bool _use_distance, bool _aprox_radius, NodeSatellite _nodesatellite)
+{
+ FilletChamferPropertiesDialog *dialog = new FilletChamferPropertiesDialog();
+
+ dialog->_setUseDistance(_use_distance);
+ dialog->_setAprox(_aprox_radius);
+ dialog->_setAmount(_amount);
+ dialog->_setNodeSatellite(_nodesatellite);
+ dialog->_setPt(pt);
+
+ dialog->set_title(_("Modify Fillet-Chamfer"));
+ dialog->_apply_button.set_label(_("_Modify"));
+
+ dialog->set_modal(true);
+ desktop->setWindowTransient(dialog->gobj());
+ dialog->property_destroy_with_parent() = true;
+
+ dialog->show();
+ dialog->present();
+}
+
+void FilletChamferPropertiesDialog::_apply()
+{
+
+ double d_pos = _fillet_chamfer_position_numeric.get_value();
+ if (d_pos >= 0) {
+ if (_fillet_chamfer_type_fillet.get_active() == true) {
+ _nodesatellite.nodesatellite_type = FILLET;
+ } else if (_fillet_chamfer_type_inverse_fillet.get_active() == true) {
+ _nodesatellite.nodesatellite_type = INVERSE_FILLET;
+ } else if (_fillet_chamfer_type_inverse_chamfer.get_active() == true) {
+ _nodesatellite.nodesatellite_type = INVERSE_CHAMFER;
+ } else {
+ _nodesatellite.nodesatellite_type = CHAMFER;
+ }
+ if (_flexible) {
+ if (d_pos > 99.99999 || d_pos < 0) {
+ d_pos = 0;
+ }
+ d_pos = d_pos / 100;
+ }
+ _nodesatellite.amount = d_pos;
+ size_t steps = (size_t)_fillet_chamfer_chamfer_subdivisions.get_value();
+ if (steps < 1) {
+ steps = 1;
+ }
+ _nodesatellite.steps = steps;
+ _knotpoint->knot_set_offset(_nodesatellite);
+ }
+ _close();
+}
+
+void FilletChamferPropertiesDialog::_close()
+{
+ destroy_();
+ Glib::signal_idle().connect(
+ sigc::bind_return(
+ sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this),
+ false
+ )
+ );
+}
+
+bool FilletChamferPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/)
+{
+ return false;
+}
+
+void FilletChamferPropertiesDialog::_handleButtonEvent(GdkEventButton *event)
+{
+ if ((event->type == GDK_2BUTTON_PRESS) && (event->button == 1)) {
+ _apply();
+ }
+}
+
+void FilletChamferPropertiesDialog::_setNodeSatellite(NodeSatellite nodesatellite)
+{
+ double position;
+ std::string distance_or_radius = std::string(_("Radius"));
+ if (_aprox) {
+ distance_or_radius = std::string(_("Radius approximated"));
+ }
+ if (_use_distance) {
+ distance_or_radius = std::string(_("Knot distance"));
+ }
+ if (nodesatellite.is_time) {
+ position = _amount * 100;
+ _flexible = true;
+ _fillet_chamfer_position_label.set_label(_("Position (%):"));
+ } else {
+ _flexible = false;
+ auto posConcat = Glib::ustring::compose (_("%1:"), distance_or_radius);
+ _fillet_chamfer_position_label.set_label(_(posConcat.c_str()));
+ position = _amount;
+ }
+ _fillet_chamfer_position_numeric.set_value(position);
+ _fillet_chamfer_chamfer_subdivisions.set_value(nodesatellite.steps);
+ if (nodesatellite.nodesatellite_type == FILLET) {
+ _fillet_chamfer_type_fillet.set_active(true);
+ } else if (nodesatellite.nodesatellite_type == INVERSE_FILLET) {
+ _fillet_chamfer_type_inverse_fillet.set_active(true);
+ } else if (nodesatellite.nodesatellite_type == CHAMFER) {
+ _fillet_chamfer_type_chamfer.set_active(true);
+ } else if (nodesatellite.nodesatellite_type == INVERSE_CHAMFER) {
+ _fillet_chamfer_type_inverse_chamfer.set_active(true);
+ }
+ _nodesatellite = nodesatellite;
+}
+
+void FilletChamferPropertiesDialog::_setPt(
+ const Inkscape::LivePathEffect::
+ FilletChamferKnotHolderEntity *pt)
+{
+ _knotpoint = const_cast<
+ Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *>(
+ pt);
+}
+
+
+void FilletChamferPropertiesDialog::_setAmount(double amount)
+{
+ _amount = amount;
+}
+
+
+
+void FilletChamferPropertiesDialog::_setUseDistance(bool use_knot_distance)
+{
+ _use_distance = use_knot_distance;
+}
+
+void FilletChamferPropertiesDialog::_setAprox(bool _aprox_radius)
+{
+ _aprox = _aprox_radius;
+}
+
+} // namespace
+} // namespace
+} // namespace
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/lpe-fillet-chamfer-properties.h b/src/ui/dialog/lpe-fillet-chamfer-properties.h
new file mode 100644
index 0000000..99e446f
--- /dev/null
+++ b/src/ui/dialog/lpe-fillet-chamfer-properties.h
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ *
+ * From the code of Liam P.White from his Power Stroke Knot dialog
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_FILLET_CHAMFER_PROPERTIES_H
+#define INKSCAPE_DIALOG_FILLET_CHAMFER_PROPERTIES_H
+
+#include <2geom/point.h>
+#include <gtkmm.h>
+
+#include "live_effects/parameter/nodesatellitesarray.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+class FilletChamferPropertiesDialog : public Gtk::Dialog {
+public:
+ FilletChamferPropertiesDialog();
+ ~FilletChamferPropertiesDialog() override;
+
+ Glib::ustring getName() const
+ {
+ return "LayerPropertiesDialog";
+ }
+
+ static void showDialog(SPDesktop *desktop, double _amount,
+ const Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *pt, bool _use_distance,
+ bool _aprox_radius, NodeSatellite _nodesatellite);
+
+protected:
+
+ Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *
+ _knotpoint;
+
+ Gtk::Label _fillet_chamfer_position_label;
+ Gtk::SpinButton _fillet_chamfer_position_numeric;
+ Gtk::RadioButton::Group _fillet_chamfer_type_group;
+ Gtk::RadioButton _fillet_chamfer_type_fillet;
+ Gtk::RadioButton _fillet_chamfer_type_inverse_fillet;
+ Gtk::RadioButton _fillet_chamfer_type_chamfer;
+ Gtk::RadioButton _fillet_chamfer_type_inverse_chamfer;
+ Gtk::Label _fillet_chamfer_chamfer_subdivisions_label;
+ Gtk::SpinButton _fillet_chamfer_chamfer_subdivisions;
+
+ Gtk::Grid _layout_table;
+ bool _position_visible;
+
+ Gtk::Button _close_button;
+ Gtk::Button _apply_button;
+
+ sigc::connection _destroy_connection;
+
+ static FilletChamferPropertiesDialog &_instance()
+ {
+ static FilletChamferPropertiesDialog instance;
+ return instance;
+ }
+
+ void _setPt(const Inkscape::LivePathEffect::
+ FilletChamferKnotHolderEntity *pt);
+ void _setUseDistance(bool use_knot_distance);
+ void _setAprox(bool aprox_radius);
+ void _setAmount(double amount);
+ void _setNodeSatellite(NodeSatellite nodesatellite);
+ void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row);
+
+ bool _handleKeyEvent(GdkEventKey *event);
+ void _handleButtonEvent(GdkEventButton *event);
+
+ void _apply();
+ void _close();
+ bool _flexible;
+ NodeSatellite _nodesatellite;
+ bool _use_distance;
+ double _amount;
+ bool _aprox;
+
+ friend class Inkscape::LivePathEffect::
+ FilletChamferKnotHolderEntity;
+
+private:
+ FilletChamferPropertiesDialog(
+ FilletChamferPropertiesDialog const &); // no copy
+ FilletChamferPropertiesDialog &operator=(
+ FilletChamferPropertiesDialog const &); // no assign
+};
+
+} // namespace
+} // namespace
+} // namespace
+
+#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_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/dialog/lpe-powerstroke-properties.cpp b/src/ui/dialog/lpe-powerstroke-properties.cpp
new file mode 100644
index 0000000..cd5fa28
--- /dev/null
+++ b/src/ui/dialog/lpe-powerstroke-properties.cpp
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Dialog for renaming layers.
+ */
+/* Author:
+ * Bryce W. Harrington <bryce@bryceharrington.com>
+ * Andrius R. <knutux@gmail.com>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ * Copyright (C) 2006 Andrius R.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "lpe-powerstroke-properties.h"
+#include <boost/lexical_cast.hpp>
+#include <glibmm/i18n.h>
+#include "inkscape.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "layer-manager.h"
+
+#include "selection-chemistry.h"
+//#include "event-context.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+PowerstrokePropertiesDialog::PowerstrokePropertiesDialog()
+ : _knotpoint(nullptr),
+ _position_visible(false),
+ _close_button(_("_Cancel"), true)
+{
+ Gtk::Box *mainVBox = get_content_area();
+
+ _layout_table.set_row_spacing(4);
+ _layout_table.set_column_spacing(4);
+
+ // Layer name widgets
+ _powerstroke_position_entry.set_activates_default(true);
+ _powerstroke_position_entry.set_digits(4);
+ _powerstroke_position_entry.set_increments(1,1);
+ _powerstroke_position_entry.set_range(-SCALARPARAM_G_MAXDOUBLE, SCALARPARAM_G_MAXDOUBLE);
+ _powerstroke_position_entry.set_hexpand();
+ _powerstroke_position_label.set_label(_("Position:"));
+ _powerstroke_position_label.set_halign(Gtk::ALIGN_END);
+ _powerstroke_position_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _powerstroke_width_entry.set_activates_default(true);
+ _powerstroke_width_entry.set_digits(4);
+ _powerstroke_width_entry.set_increments(1,1);
+ _powerstroke_width_entry.set_range(-SCALARPARAM_G_MAXDOUBLE, SCALARPARAM_G_MAXDOUBLE);
+ _powerstroke_width_entry.set_hexpand();
+ _powerstroke_width_label.set_label(_("Width:"));
+ _powerstroke_width_label.set_halign(Gtk::ALIGN_END);
+ _powerstroke_width_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _layout_table.attach(_powerstroke_position_label,0,0,1,1);
+ _layout_table.attach(_powerstroke_position_entry,1,0,1,1);
+ _layout_table.attach(_powerstroke_width_label, 0,1,1,1);
+ _layout_table.attach(_powerstroke_width_entry, 1,1,1,1);
+
+ mainVBox->pack_start(_layout_table, true, true, 4);
+
+ // Buttons
+ _close_button.set_can_default();
+
+ _apply_button.set_use_underline(true);
+ _apply_button.set_can_default();
+
+ _close_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_close));
+ _apply_button.signal_clicked()
+ .connect(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_apply));
+
+ signal_delete_event().connect(
+ sigc::bind_return(
+ sigc::hide(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_close)),
+ true
+ )
+ );
+
+ add_action_widget(_close_button, Gtk::RESPONSE_CLOSE);
+ add_action_widget(_apply_button, Gtk::RESPONSE_APPLY);
+
+ _apply_button.grab_default();
+
+ show_all_children();
+
+ set_focus(_powerstroke_width_entry);
+}
+
+PowerstrokePropertiesDialog::~PowerstrokePropertiesDialog() {
+}
+
+void PowerstrokePropertiesDialog::showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt)
+{
+ PowerstrokePropertiesDialog *dialog = new PowerstrokePropertiesDialog();
+
+ dialog->_setKnotPoint(knotpoint);
+ dialog->_setPt(pt);
+
+ dialog->set_title(_("Modify Node Position"));
+ dialog->_apply_button.set_label(_("_Move"));
+
+ dialog->set_modal(true);
+ desktop->setWindowTransient (dialog->gobj());
+ dialog->property_destroy_with_parent() = true;
+
+ dialog->show();
+ dialog->present();
+}
+
+void
+PowerstrokePropertiesDialog::_apply()
+{
+ double d_pos = _powerstroke_position_entry.get_value();
+ double d_width = _powerstroke_width_entry.get_value();
+ _knotpoint->knot_set_offset(Geom::Point(d_pos, d_width));
+ _close();
+}
+
+void
+PowerstrokePropertiesDialog::_close()
+{
+ destroy_();
+ Glib::signal_idle().connect(
+ sigc::bind_return(
+ sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this),
+ false
+ )
+ );
+}
+
+bool PowerstrokePropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/)
+{
+
+ /*switch (get_latin_keyval(event)) {
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter: {
+ _apply();
+ return true;
+ }
+ break;
+ }*/
+ return false;
+}
+
+void PowerstrokePropertiesDialog::_handleButtonEvent(GdkEventButton* event)
+{
+ if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) {
+ _apply();
+ }
+}
+
+void PowerstrokePropertiesDialog::_setKnotPoint(Geom::Point knotpoint)
+{
+ _powerstroke_position_entry.set_value(knotpoint.x());
+ _powerstroke_width_entry.set_value(knotpoint.y());
+}
+
+void PowerstrokePropertiesDialog::_setPt(const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt)
+{
+ _knotpoint = const_cast<Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *>(pt);
+}
+
+} // namespace
+} // namespace
+} // namespace
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/lpe-powerstroke-properties.h b/src/ui/dialog/lpe-powerstroke-properties.h
new file mode 100644
index 0000000..161d1fc
--- /dev/null
+++ b/src/ui/dialog/lpe-powerstroke-properties.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Dialog for renaming layers
+ */
+/* Author:
+ * Bryce W. Harrington <bryce@bryceharrington.com>
+ *
+ * Copyright (C) 2004 Bryce Harrington
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_DIALOG_POWERSTROKE_PROPERTIES_H
+#define INKSCAPE_DIALOG_POWERSTROKE_PROPERTIES_H
+
+#include <2geom/point.h>
+#include <gtkmm.h>
+#include "live_effects/parameter/powerstrokepointarray.h"
+
+class SPDesktop;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialogs {
+
+class PowerstrokePropertiesDialog : public Gtk::Dialog {
+ public:
+ PowerstrokePropertiesDialog();
+ ~PowerstrokePropertiesDialog() override;
+
+ Glib::ustring getName() const { return "LayerPropertiesDialog"; }
+
+ static void showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt);
+
+protected:
+
+ Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *_knotpoint;
+
+ Gtk::Label _powerstroke_position_label;
+ Gtk::SpinButton _powerstroke_position_entry;
+ Gtk::Label _powerstroke_width_label;
+ Gtk::SpinButton _powerstroke_width_entry;
+ Gtk::Grid _layout_table;
+ bool _position_visible;
+
+ Gtk::Button _close_button;
+ Gtk::Button _apply_button;
+
+ sigc::connection _destroy_connection;
+
+ static PowerstrokePropertiesDialog &_instance() {
+ static PowerstrokePropertiesDialog instance;
+ return instance;
+ }
+
+ void _setPt(const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt);
+
+ void _apply();
+ void _close();
+
+ void _setKnotPoint(Geom::Point knotpoint);
+ void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row);
+
+ bool _handleKeyEvent(GdkEventKey *event);
+ void _handleButtonEvent(GdkEventButton* event);
+
+ friend class Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity;
+
+private:
+ PowerstrokePropertiesDialog(PowerstrokePropertiesDialog const &); // no copy
+ PowerstrokePropertiesDialog &operator=(PowerstrokePropertiesDialog const &); // no assign
+};
+
+} // namespace
+} // namespace
+} // namespace
+
+
+#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_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/dialog/memory.cpp b/src/ui/dialog/memory.cpp
new file mode 100644
index 0000000..a28c065
--- /dev/null
+++ b/src/ui/dialog/memory.cpp
@@ -0,0 +1,212 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Memory statistics dialog.
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 2005
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/dialog/memory.h"
+#include <glibmm/i18n.h>
+#include <glibmm/main.h>
+
+#include <gtkmm/liststore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/dialog.h>
+
+#include "inkgc/gc-core.h"
+#include "debug/heap.h"
+#include "util/format_size.h"
+
+using Inkscape::Util::format_size;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+struct Memory::Private {
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> used;
+ Gtk::TreeModelColumn<Glib::ustring> slack;
+ Gtk::TreeModelColumn<Glib::ustring> total;
+
+ ModelColumns() { add(name); add(used); add(slack); add(total); }
+ };
+
+ Private() {
+ model = Gtk::ListStore::create(columns);
+ view.set_model(model);
+ view.append_column(_("Heap"), columns.name);
+ view.append_column(_("In Use"), columns.used);
+ // TRANSLATORS: "Slack" refers to memory which is in the heap but currently unused.
+ // More typical usage is to call this memory "free" rather than "slack".
+ view.append_column(_("Slack"), columns.slack);
+ view.append_column(_("Total"), columns.total);
+ }
+
+ void update();
+
+ void start_update_task();
+ void stop_update_task();
+
+ ModelColumns columns;
+ Glib::RefPtr<Gtk::ListStore> model;
+ Gtk::TreeView view;
+
+ sigc::connection update_task;
+};
+
+void Memory::Private::update() {
+ Debug::Heap::Stats total = { 0, 0 };
+
+ int aggregate_features = Debug::Heap::SIZE_AVAILABLE | Debug::Heap::USED_AVAILABLE;
+ Gtk::ListStore::iterator row;
+
+ row = model->children().begin();
+
+ for ( unsigned i = 0 ; i < Debug::heap_count() ; i++ ) {
+ Debug::Heap *heap=Debug::get_heap(i);
+ if (heap) {
+ Debug::Heap::Stats stats=heap->stats();
+ int features=heap->features();
+
+ aggregate_features &= features;
+
+ if ( row == model->children().end() ) {
+ row = model->append();
+ }
+
+ row->set_value(columns.name, Glib::ustring(heap->name()));
+ if ( features & Debug::Heap::SIZE_AVAILABLE ) {
+ row->set_value(columns.total, format_size(stats.size));
+ total.size += stats.size;
+ } else {
+ row->set_value(columns.total, Glib::ustring(_("Unknown")));
+ }
+ if ( features & Debug::Heap::USED_AVAILABLE ) {
+ row->set_value(columns.used, format_size(stats.bytes_used));
+ total.bytes_used += stats.bytes_used;
+ } else {
+ row->set_value(columns.used, Glib::ustring(_("Unknown")));
+ }
+ if ( features & Debug::Heap::SIZE_AVAILABLE &&
+ features & Debug::Heap::USED_AVAILABLE )
+ {
+ row->set_value(columns.slack, format_size(stats.size - stats.bytes_used));
+ } else {
+ row->set_value(columns.slack, Glib::ustring(_("Unknown")));
+ }
+
+ ++row;
+ }
+ }
+
+ if ( row == model->children().end() ) {
+ row = model->append();
+ }
+
+ Glib::ustring value;
+
+ row->set_value(columns.name, Glib::ustring(_("Combined")));
+
+ if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE ) {
+ row->set_value(columns.total, format_size(total.size));
+ } else {
+ row->set_value(columns.total, Glib::ustring("> ") + format_size(total.size));
+ }
+
+ if ( aggregate_features & Debug::Heap::USED_AVAILABLE ) {
+ row->set_value(columns.used, format_size(total.bytes_used));
+ } else {
+ row->set_value(columns.used, Glib::ustring("> ") + format_size(total.bytes_used));
+ }
+
+ if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE &&
+ aggregate_features & Debug::Heap::USED_AVAILABLE )
+ {
+ row->set_value(columns.slack, format_size(total.size - total.bytes_used));
+ } else {
+ row->set_value(columns.slack, Glib::ustring(_("Unknown")));
+ }
+
+ ++row;
+
+ while ( row != model->children().end() ) {
+ row = model->erase(row);
+ }
+}
+
+void Memory::Private::start_update_task() {
+ update_task.disconnect();
+ update_task = Glib::signal_timeout().connect(
+ sigc::bind_return(sigc::mem_fun(*this, &Private::update), true),
+ 500
+ );
+}
+
+void Memory::Private::stop_update_task() {
+ update_task.disconnect();
+}
+
+Memory::Memory()
+ : DialogBase("/dialogs/memory", "Memory")
+ , _private(std::make_unique<Private>())
+{
+ // Private conf
+ pack_start(_private->view);
+
+ _private->update();
+
+ signal_show().connect(sigc::mem_fun(*_private, &Private::start_update_task));
+ signal_hide().connect(sigc::mem_fun(*_private, &Private::stop_update_task));
+
+ // Add button
+ auto button = Gtk::make_managed<Gtk::Button>(_("Recalculate"));
+ button->signal_button_press_event().connect(sigc::mem_fun(*this, &Memory::_apply));
+
+ auto button_box = Gtk::make_managed<Gtk::ButtonBox>();
+ button_box->set_layout(Gtk::BUTTONBOX_END);
+ button_box->set_spacing(6);
+ button_box->set_border_width(4);
+ button_box->pack_end(*button);
+ pack_end(*button_box, Gtk::PACK_SHRINK, 0);
+
+ // Start
+ _private->start_update_task();
+
+ show_all_children();
+}
+
+Memory::~Memory()
+{
+ _private->stop_update_task();
+}
+
+bool Memory::_apply(GdkEventButton*)
+{
+ GC::Core::gcollect();
+ _private->update();
+ return 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/dialog/memory.h b/src/ui/dialog/memory.h
new file mode 100644
index 0000000..5975fc2
--- /dev/null
+++ b/src/ui/dialog/memory.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Memory statistics dialog
+ */
+/* Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright 2005 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_UI_DIALOG_MEMORY_H
+#define SEEN_INKSCAPE_UI_DIALOG_MEMORY_H
+
+#include <memory>
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class Memory : public DialogBase
+{
+public:
+ Memory();
+ ~Memory() override;
+
+protected:
+ bool _apply(GdkEventButton *);
+
+private:
+ struct Private;
+ std::unique_ptr<Private> _private;
+};
+
+} // 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/dialog/messages.cpp b/src/ui/dialog/messages.cpp
new file mode 100644
index 0000000..3e29501
--- /dev/null
+++ b/src/ui/dialog/messages.cpp
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Messages dialog - implementation.
+ */
+/* Authors:
+ * Bob Jamison
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004, 2005 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "messages.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+//#########################################################################
+//## E V E N T S
+//#########################################################################
+
+/**
+ * Also a public method. Remove all text from the dialog
+ */
+void Messages::clear()
+{
+ Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer();
+ buffer->erase(buffer->begin(), buffer->end());
+}
+
+
+//#########################################################################
+//## C O N S T R U C T O R / D E S T R U C T O R
+//#########################################################################
+/**
+ * Constructor
+ */
+Messages::Messages()
+ : DialogBase("/dialogs/messages", "Messages")
+ , buttonClear(_("_Clear"), _("Clear log messages"))
+ , checkCapture(_("Capture log messages"), _("Capture log messages"))
+ , buttonBox(Gtk::ORIENTATION_HORIZONTAL)
+{
+ /*
+ * Menu replaced with buttons
+ *
+ menuBar.items().push_back( Gtk::Menu_Helpers::MenuElem(_("_File"), fileMenu) );
+ fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("_Clear"),
+ sigc::mem_fun(*this, &Messages::clear) ) );
+ fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("Capture log messages"),
+ sigc::mem_fun(*this, &Messages::captureLogMessages) ) );
+ fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("Release log messages"),
+ sigc::mem_fun(*this, &Messages::releaseLogMessages) ) );
+ contents->pack_start(menuBar, Gtk::PACK_SHRINK);
+ */
+
+ //### Set up the text widget
+ messageText.set_editable(false);
+ textScroll.add(messageText);
+ textScroll.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS);
+ pack_start(textScroll);
+
+ buttonBox.set_spacing(6);
+ buttonBox.pack_start(checkCapture, true, true, 6);
+ buttonBox.pack_end(buttonClear, false, false, 10);
+ pack_start(buttonBox, Gtk::PACK_SHRINK);
+
+ // sick of this thing shrinking too much
+ set_size_request(400, -1);
+
+ show_all_children();
+
+ message(_("Ready."));
+
+ buttonClear.signal_clicked().connect(sigc::mem_fun(*this, &Messages::clear));
+ checkCapture.signal_clicked().connect(sigc::mem_fun(*this, &Messages::toggleCapture));
+
+ /*
+ * TODO - Setting this preference doesn't capture messages that the user can see.
+ * Inkscape creates an instance of a dialog on startup and sends messages there, but when the user
+ * opens the dialog View > Messages the DialogManager creates a new instance of this class that is not capturing messages.
+ *
+ * message(_("Enable log display by setting dialogs.debug 'redirect' attribute to 1 in preferences.xml"));
+ */
+
+ handlerDefault = 0;
+ handlerGlibmm = 0;
+ handlerAtkmm = 0;
+ handlerPangomm = 0;
+ handlerGdkmm = 0;
+ handlerGtkmm = 0;
+
+}
+
+Messages::~Messages()
+= default;
+
+
+//#########################################################################
+//## M E T H O D S
+//#########################################################################
+
+void Messages::message(char *msg)
+{
+ Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer();
+ Glib::ustring uMsg = msg;
+ if (uMsg[uMsg.length()-1] != '\n')
+ uMsg += '\n';
+ buffer->insert (buffer->end(), uMsg);
+}
+
+// dialogLoggingCallback is already used in debug.cpp
+static void dialogLoggingCallback(const gchar */*log_domain*/,
+ GLogLevelFlags /*log_level*/,
+ const gchar *messageText,
+ gpointer user_data)
+{
+ Messages *dlg = static_cast<Messages *>(user_data);
+ dlg->message(const_cast<char*>(messageText));
+
+}
+
+void Messages::toggleCapture()
+{
+ if (checkCapture.get_active()) {
+ captureLogMessages();
+ } else {
+ releaseLogMessages();
+ }
+}
+
+void Messages::captureLogMessages()
+{
+ /*
+ This might likely need more code, to capture Gtkmm
+ and Glibmm warnings, or maybe just simply grab stdout/stderr
+ */
+ GLogLevelFlags flags = (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL |
+ G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE |
+ G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG);
+ if ( !handlerDefault ) {
+ handlerDefault = g_log_set_handler(nullptr, flags,
+ dialogLoggingCallback, (gpointer)this);
+ }
+ if ( !handlerGlibmm ) {
+ handlerGlibmm = g_log_set_handler("glibmm", flags,
+ dialogLoggingCallback, (gpointer)this);
+ }
+ if ( !handlerAtkmm ) {
+ handlerAtkmm = g_log_set_handler("atkmm", flags,
+ dialogLoggingCallback, (gpointer)this);
+ }
+ if ( !handlerPangomm ) {
+ handlerPangomm = g_log_set_handler("pangomm", flags,
+ dialogLoggingCallback, (gpointer)this);
+ }
+ if ( !handlerGdkmm ) {
+ handlerGdkmm = g_log_set_handler("gdkmm", flags,
+ dialogLoggingCallback, (gpointer)this);
+ }
+ if ( !handlerGtkmm ) {
+ handlerGtkmm = g_log_set_handler("gtkmm", flags,
+ dialogLoggingCallback, (gpointer)this);
+ }
+ message(_("Log capture started."));
+}
+
+void Messages::releaseLogMessages()
+{
+ if ( handlerDefault ) {
+ g_log_remove_handler(nullptr, handlerDefault);
+ handlerDefault = 0;
+ }
+ if ( handlerGlibmm ) {
+ g_log_remove_handler("glibmm", handlerGlibmm);
+ handlerGlibmm = 0;
+ }
+ if ( handlerAtkmm ) {
+ g_log_remove_handler("atkmm", handlerAtkmm);
+ handlerAtkmm = 0;
+ }
+ if ( handlerPangomm ) {
+ g_log_remove_handler("pangomm", handlerPangomm);
+ handlerPangomm = 0;
+ }
+ if ( handlerGdkmm ) {
+ g_log_remove_handler("gdkmm", handlerGdkmm);
+ handlerGdkmm = 0;
+ }
+ if ( handlerGtkmm ) {
+ g_log_remove_handler("gtkmm", handlerGtkmm);
+ handlerGtkmm = 0;
+ }
+ message(_("Log capture stopped."));
+}
+
+} //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/dialog/messages.h b/src/ui/dialog/messages.h
new file mode 100644
index 0000000..78b5f33
--- /dev/null
+++ b/src/ui/dialog/messages.h
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Messages dialog
+ *
+ * A very simple dialog for displaying Inkscape messages. Messages
+ * sent to g_log(), g_warning(), g_message(), ets, are routed here,
+ * in order to avoid messing with the startup console.
+ */
+/* Authors:
+ * Bob Jamison
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004, 2005 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_MESSAGES_H
+#define INKSCAPE_UI_DIALOG_MESSAGES_H
+
+#include <glibmm/i18n.h>
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/menu.h>
+#include <gtkmm/menubar.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/textview.h>
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class Messages : public DialogBase
+{
+public:
+ Messages();
+ ~Messages() override;
+
+ /**
+ * Clear all information from the dialog
+ */
+ void clear();
+
+ /**
+ * Display a message
+ */
+ void message(char *msg);
+
+ /**
+ * Redirect g_log() messages to this widget
+ */
+ void captureLogMessages();
+
+ /**
+ * Return g_log() messages to normal handling
+ */
+ void releaseLogMessages();
+
+ void toggleCapture();
+
+protected:
+ //Gtk::MenuBar menuBar;
+ //Gtk::Menu fileMenu;
+ Gtk::ScrolledWindow textScroll;
+ Gtk::TextView messageText;
+ Gtk::Box buttonBox;
+ Gtk::Button buttonClear;
+ Gtk::CheckButton checkCapture;
+
+ //Handler ID's
+ guint handlerDefault;
+ guint handlerGlibmm;
+ guint handlerAtkmm;
+ guint handlerPangomm;
+ guint handlerGdkmm;
+ guint handlerGtkmm;
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_MESSAGES_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/dialog/new-from-template.cpp b/src/ui/dialog/new-from-template.cpp
new file mode 100644
index 0000000..d5fa843
--- /dev/null
+++ b/src/ui/dialog/new-from-template.cpp
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template main dialog - implementation
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "new-from-template.h"
+
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "file.h"
+#include "inkscape-application.h"
+#include "inkscape-window.h"
+#include "inkscape.h"
+#include "object/sp-namedview.h"
+#include "ui/widget/template-list.h"
+
+namespace Inkscape {
+namespace UI {
+
+
+NewFromTemplate::NewFromTemplate()
+ : _create_template_button(_("Create from template"))
+{
+ set_title(_("New From Template"));
+ resize(750, 500);
+
+ templates = Gtk::manage(new Inkscape::UI::Widget::TemplateList());
+ get_content_area()->pack_start(*templates);
+ templates->init(Inkscape::Extension::TEMPLATE_NEW_FROM);
+
+ _create_template_button.set_halign(Gtk::ALIGN_END);
+ _create_template_button.set_valign(Gtk::ALIGN_END);
+ _create_template_button.set_margin_end(15);
+
+ get_content_area()->pack_end(_create_template_button, Gtk::PACK_SHRINK);
+
+ _create_template_button.signal_clicked().connect(
+ sigc::mem_fun(*this, &NewFromTemplate::_createFromTemplate));
+ _create_template_button.set_sensitive(false);
+
+ templates->connectItemSelected([=]() { _create_template_button.set_sensitive(true); });
+ templates->connectItemActivated(sigc::mem_fun(*this, &NewFromTemplate::_createFromTemplate));
+ templates->signal_switch_page().connect([=](Gtk::Widget *const widget, int num) {
+ _create_template_button.set_sensitive(templates->has_selected_preset());
+ });
+
+ show_all();
+}
+
+void NewFromTemplate::_createFromTemplate()
+{
+ SPDesktop *old_desktop = SP_ACTIVE_DESKTOP;
+
+ auto doc = templates->new_document();
+
+ // Cancel button was pressed.
+ if (!doc)
+ return;
+
+ auto app = InkscapeApplication::instance();
+ InkscapeWindow *win = app->window_open(doc);
+ SPDesktop *new_desktop = win->get_desktop();
+ sp_namedview_window_from_document(new_desktop);
+
+ if (old_desktop)
+ old_desktop->clearWaitingCursor();
+
+ _onClose();
+}
+
+void NewFromTemplate::_onClose()
+{
+ response(0);
+}
+
+void NewFromTemplate::load_new_from_template()
+{
+ NewFromTemplate dl;
+ dl.run();
+}
+
+}
+}
diff --git a/src/ui/dialog/new-from-template.h b/src/ui/dialog/new-from-template.h
new file mode 100644
index 0000000..f286c21
--- /dev/null
+++ b/src/ui/dialog/new-from-template.h
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief New From Template main dialog
+ */
+/* Authors:
+ * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H
+#define INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H
+
+#include <gtkmm/dialog.h>
+#include <gtkmm/button.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class TemplateList;
+}
+
+class NewFromTemplate : public Gtk::Dialog
+{
+
+friend class TemplateLoadTab;
+public:
+ static void load_new_from_template();
+ ~NewFromTemplate() override{};
+
+private:
+ NewFromTemplate();
+ Gtk::Button _create_template_button;
+ Inkscape::UI::Widget::TemplateList *templates;
+
+ void _createFromTemplate();
+ void _onClose();
+};
+
+}
+}
+#endif
diff --git a/src/ui/dialog/object-attributes.cpp b/src/ui/dialog/object-attributes.cpp
new file mode 100644
index 0000000..dd5d8e4
--- /dev/null
+++ b/src/ui/dialog/object-attributes.cpp
@@ -0,0 +1,760 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Generic object attribute editor
+ *//*
+ * Authors:
+ * see git history
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/rect.h>
+#include <cmath>
+#include <cstddef>
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/button.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/widget.h>
+#include <memory>
+#include <optional>
+#include <string>
+#include <tuple>
+#include "actions/actions-tools.h"
+#include "desktop.h"
+#include "live_effects/effect-enum.h"
+#include "mod360.h"
+#include "object/sp-anchor.h"
+#include "object/sp-ellipse.h"
+#include "object/sp-image.h"
+#include "object/sp-lpe-item.h"
+#include "object/sp-namedview.h"
+#include "object/sp-object.h"
+#include "object/sp-path.h"
+#include "object/sp-rect.h"
+#include "object/sp-star.h"
+#include "object/tags.h"
+#include "streq.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog/object-attributes.h"
+#include "ui/icon-names.h"
+#include "ui/tools/node-tool.h"
+#include "ui/util.h"
+#include "ui/widget/image-properties.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/style-swatch.h"
+#include "widgets/sp-attribute-widget.h"
+#include "xml/href-attribute-helper.h"
+#include "live_effects/lpeobject-reference.h"
+#include "live_effects/lpeobject.h"
+#include "live_effects/effect.h"
+
+namespace Inkscape {
+namespace UI {
+
+void sp_apply_lpeffect(SPDesktop* desktop, SPLPEItem* item, LivePathEffect::EffectType etype);
+
+namespace Dialog {
+
+
+struct SPAttrDesc {
+ gchar const *label;
+ gchar const *attribute;
+};
+
+static const SPAttrDesc anchor_desc[] = {
+ { N_("Href:"), "xlink:href"},
+ { N_("Target:"), "target"},
+ { N_("Type:"), "xlink:type"},
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkRoleAttribute
+ // Identifies the type of the related resource with an absolute URI
+ { N_("Role:"), "xlink:role"},
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkArcRoleAttribute
+ // For situations where the nature/role alone isn't enough, this offers an additional URI defining the purpose of the link.
+ { N_("Arcrole:"), "xlink:arcrole"},
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkTitleAttribute
+ { N_("Title:"), "xlink:title"},
+ { N_("Show:"), "xlink:show"},
+ // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkActuateAttribute
+ { N_("Actuate:"), "xlink:actuate"},
+ { nullptr, nullptr}
+};
+
+ObjectAttributes::ObjectAttributes()
+ : DialogBase("/dialogs/objectattr/", "ObjectAttributes"),
+ _builder(create_builder("object-attributes.glade")),
+ _main_panel(get_widget<Gtk::Box>(_builder, "main-panel")),
+ _obj_title(get_widget<Gtk::Label>(_builder, "main-obj-name")),
+ _style_swatch(nullptr, _("Item's fill, stroke and opacity"), Gtk::ORIENTATION_HORIZONTAL)
+{
+ auto& main = get_widget<Gtk::Box>(_builder, "main-widget");
+ _obj_title.set_text("");
+ _style_swatch.set_hexpand(false);
+ _style_swatch.set_valign(Gtk::ALIGN_CENTER);
+ get_widget<Gtk::Box>(_builder, "main-header").pack_end(_style_swatch, false, true);
+ add(main);
+ create_panels();
+ _style_swatch.hide();
+}
+
+void ObjectAttributes::widget_setup() {
+ if (_update.pending() || !getDesktop()) return;
+
+ auto selection = getDesktop()->getSelection();
+ auto item = selection->singleItem();
+
+ auto scoped(_update.block());
+
+ auto panel = get_panel(item);
+ if (panel != _current_panel && _current_panel) {
+ _current_panel->update_panel(nullptr, nullptr);
+ _main_panel.remove(_current_panel->widget());
+ _obj_title.set_text("");
+ }
+
+ _current_panel = panel;
+ _current_item = nullptr;
+
+ Glib::ustring title = panel ? panel->get_title() : "";
+ if (!panel) {
+ if (item) {
+ if (auto name = item->displayName()) {
+ title = name;
+ }
+ }
+ else if (selection->size() > 1) {
+ title = _("Multiple objects selected");
+ }
+ }
+ _obj_title.set_markup("<b>" + Glib::Markup::escape_text(title) + "</b>");
+
+ if (!panel) {
+ _style_swatch.hide();
+ return;
+ }
+
+ _main_panel.pack_start(panel->widget(), true, true);
+ bool show_style = false;
+ if (panel->supports_fill_stroke()) {
+ if (auto style = item ? item->style : nullptr) {
+ _style_swatch.setStyle(style);
+ show_style = true;
+ }
+ }
+ widget_show(_style_swatch, show_style);
+ panel->update_panel(item, getDesktop());
+ panel->widget().show();
+ _current_item = item;
+
+ // TODO
+ // show no of LPEs?
+ // show locked status?
+}
+
+void ObjectAttributes::update_panel(SPObject* item) {
+ if (!_current_panel) return;
+
+ if (_current_panel->supports_fill_stroke()) {
+ if (auto style = item ? item->style : nullptr) {
+ _style_swatch.setStyle(style);
+ }
+ }
+ _current_panel->update_panel(item, getDesktop());
+}
+
+void ObjectAttributes::desktopReplaced() {
+ //todo; if anything
+}
+
+void ObjectAttributes::selectionChanged(Selection* selection) {
+ widget_setup();
+}
+
+void ObjectAttributes::selectionModified(Selection* _selection, guint flags) {
+ if (_update.pending() || !getDesktop() || !_current_panel) return;
+
+ auto selection = getDesktop()->getSelection();
+ if (flags & (SP_OBJECT_MODIFIED_FLAG |
+ SP_OBJECT_PARENT_MODIFIED_FLAG |
+ SP_OBJECT_STYLE_MODIFIED_FLAG)) {
+
+ auto item = selection->singleItem();
+ if (item == _current_item) {
+ update_panel(item);
+ }
+ else {
+ g_warning("ObjectAttributes: missed selection change?");
+ }
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+std::tuple<bool, double, double> round_values(double x, double y) {
+ auto a = std::round(x);
+ auto b = std::round(y);
+ return std::make_tuple(a != x || b != y, a, b);
+}
+
+std::tuple<bool, double, double> round_values(Gtk::SpinButton& x, Gtk::SpinButton& y) {
+ return round_values(x.get_adjustment()->get_value(), y.get_adjustment()->get_value());
+}
+
+const LivePathEffectObject* find_lpeffect(SPLPEItem* item, LivePathEffect::EffectType etype) {
+ if (!item) return nullptr;
+
+ auto lpe = item->getFirstPathEffectOfType(Inkscape::LivePathEffect::FILLET_CHAMFER);
+ if (!lpe) return nullptr;
+ return lpe->getLPEObj();
+}
+
+void remove_lpeffect(SPLPEItem* item, LivePathEffect::EffectType type) {
+ if (auto effect = find_lpeffect(item, type)) {
+ item->setCurrentPathEffect(effect);
+ item->removeCurrentPathEffect(false);
+ DocumentUndo::done(item->document, _("Removed live path effect"), INKSCAPE_ICON("dialog-path-effects"));
+ }
+}
+
+std::optional<double> get_number(SPItem* item, const char* attribute) {
+ if (!item) return {};
+
+ auto val = item->getAttribute(attribute);
+ if (!val) return {};
+
+ return item->getRepr()->getAttributeDouble(attribute);
+}
+
+void align_star_shape(SPStar* path) {
+ if (!path || !path->sides) return;
+
+ auto arg1 = path->arg[0];
+ auto arg2 = path->arg[1];
+ auto delta = arg2 - arg1;
+ auto top = -M_PI / 2;
+ auto odd = path->sides & 1;
+ if (odd) {
+ arg1 = top;
+ }
+ else {
+ arg1 = top - M_PI / path->sides;
+ }
+ arg2 = arg1 + delta;
+
+ path->setAttributeDouble("sodipodi:arg1", arg1);
+ path->setAttributeDouble("sodipodi:arg2", arg2);
+ path->updateRepr();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+details::AttributesPanel::AttributesPanel() {
+ _tracker = std::make_unique<UI::Widget::UnitTracker>(Inkscape::Util::UNIT_TYPE_LINEAR);
+ //todo:
+ // auto init_units = desktop->getNamedView()->display_units;
+ // _tracker->setActiveUnit(init_units);
+}
+
+void details::AttributesPanel::update_panel(SPObject* object, SPDesktop* desktop) {
+ if (object) {
+ auto scoped(_update.block());
+ auto units = object->document->getNamedView() ? object->document->getNamedView()->display_units : nullptr;
+ // auto init_units = desktop->getNamedView()->display_units;
+ if (units) _tracker->setActiveUnit(units);
+ }
+
+ _desktop = desktop;
+
+ if (!_update.pending()) {
+ update(object);
+ }
+}
+
+void details::AttributesPanel::change_value_px(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, const char* attr, std::function<void (double)>&& setter) {
+ if (_update.pending() || !object) return;
+
+ auto scoped(_update.block());
+
+ const auto unit = _tracker->getActiveUnit();
+ auto value = Util::Quantity::convert(adj->get_value(), unit, "px");
+ if (value != 0 || attr == nullptr) {
+ setter(value);
+ }
+ else if (attr) {
+ object->removeAttribute(attr);
+ }
+
+ DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle"));
+}
+
+void details::AttributesPanel::change_angle(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, std::function<void (double)>&& setter) {
+ if (_update.pending() || !object) return;
+
+ auto scoped(_update.block());
+
+ auto value = degree_to_radians_mod2pi(adj->get_value());
+ setter(value);
+
+ DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle"));
+}
+
+void details::AttributesPanel::change_value(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, std::function<void (double)>&& setter) {
+ if (_update.pending() || !object) return;
+
+ auto scoped(_update.block());
+
+ auto value = adj ? adj->get_value() : 0;
+ setter(value);
+
+ DocumentUndo::done(object->document, _("Change object attribute"), ""); //TODO INKSCAPE_ICON("draw-rectangle"));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+class ImagePanel : public details::AttributesPanel {
+public:
+ ImagePanel() {
+ _title = _("Image");
+ _show_fill_stroke = false;
+ _panel = std::make_unique<Inkscape::UI::Widget::ImageProperties>();
+ _widget = _panel.get();
+ }
+ ~ImagePanel() override = default;
+
+ void update(SPObject* object) override { _panel->update(cast<SPImage>(object)); }
+
+private:
+ std::unique_ptr<Inkscape::UI::Widget::ImageProperties> _panel;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class AnchorPanel : public details::AttributesPanel {
+public:
+ AnchorPanel() {
+ _title = _("Anchor");
+ _show_fill_stroke = false;
+ _table = std::make_unique<SPAttributeTable>();
+ _table->show();
+ _table->set_hexpand();
+ _table->set_vexpand(false);
+ _widget = _table.get();
+ }
+ ~AnchorPanel() override = default;
+
+ void update(SPObject* object) override {
+ auto anchor = cast<SPAnchor>(object);
+ auto changed = _anchor != anchor;
+ _anchor = anchor;
+ if (!anchor) return;
+
+ std::vector<Glib::ustring> labels;
+ std::vector<Glib::ustring> attrs;
+ if (changed) {
+ int len = 0;
+ while (anchor_desc[len].label) {
+ labels.emplace_back(anchor_desc[len].label);
+ attrs.emplace_back(anchor_desc[len].attribute);
+ len += 1;
+ }
+ _table->set_object(anchor, labels, attrs, (GtkWidget*)_table->gobj());
+ }
+ else {
+ _table->reread_properties();
+ }
+ }
+
+private:
+ std::unique_ptr<SPAttributeTable> _table;
+ SPAnchor* _anchor = nullptr;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class RectPanel : public details::AttributesPanel {
+public:
+ RectPanel(Glib::RefPtr<Gtk::Builder> builder) :
+ _main(get_widget<Gtk::Grid>(builder, "rect-main")),
+ _width(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "rect-width")),
+ _height(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "rect-height")),
+ _rx(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "rect-rx")),
+ _ry(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "rect-ry")),
+ _sharp(get_widget<Gtk::Button>(builder, "rect-sharp")),
+ _round(get_widget<Gtk::Button>(builder, "rect-corners"))
+ {
+ _title = _("Rectangle");
+ _widget = &_main;
+
+ _width.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_rect, _width.get_adjustment(), "width", [=](double w){ _rect->setVisibleWidth(w); });
+ });
+ _height.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_rect, _height.get_adjustment(), "height", [=](double h){ _rect->setVisibleHeight(h); });
+ });
+ _rx.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_rect, _rx.get_adjustment(), "rx", [=](double rx){ _rect->setVisibleRx(rx); });
+ });
+ _ry.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_rect, _ry.get_adjustment(), "ry", [=](double ry){ _rect->setVisibleRy(ry); });
+ });
+ get_widget<Gtk::Button>(builder, "rect-round").signal_clicked().connect([=](){
+ auto [changed, x, y] = round_values(_width, _height);
+ if (changed) {
+ _width.get_adjustment()->set_value(x);
+ _height.get_adjustment()->set_value(y);
+ }
+ });
+ _sharp.signal_clicked().connect([=](){
+ if (!_rect) return;
+
+ // remove rounded corners if LPE is there (first one found)
+ remove_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER);
+ _rx.get_adjustment()->set_value(0);
+ _ry.get_adjustment()->set_value(0);
+ });
+ _round.signal_clicked().connect([=](){
+ if (!_rect || !_desktop) return;
+
+ // switch to node tool to show handles
+ set_active_tool(_desktop, "Node");
+ // rx/ry need to be reset first, LPE doesn't handle them too well
+ _rx.get_adjustment()->set_value(0);
+ _ry.get_adjustment()->set_value(0);
+ // add flexible corners effect if not yet present
+ if (!find_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER)) {
+ sp_apply_lpeffect(_desktop, _rect, LivePathEffect::FILLET_CHAMFER);
+ }
+ });
+ }
+
+ ~RectPanel() override = default;
+
+ void update(SPObject* object) override {
+ _rect = cast<SPRect>(object);
+ if (!_rect) return;
+
+ auto scoped(_update.block());
+ _width.set_value(_rect->width.value);
+ _height.set_value(_rect->height.value);
+ _rx.set_value(_rect->rx.value);
+ _ry.set_value(_rect->ry.value);
+ auto lpe = find_lpeffect(_rect, LivePathEffect::FILLET_CHAMFER);
+ _sharp.set_sensitive(_rect->rx.value > 0 || _rect->ry.value > 0 || lpe);
+ _round.set_sensitive(!lpe);
+ }
+
+private:
+ SPRect* _rect = nullptr;
+ Gtk::Widget& _main;
+ Inkscape::UI::Widget::SpinButton& _width;
+ Inkscape::UI::Widget::SpinButton& _height;
+ Inkscape::UI::Widget::SpinButton& _rx;
+ Inkscape::UI::Widget::SpinButton& _ry;
+ Gtk::Button& _sharp;
+ Gtk::Button& _round;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class EllipsePanel : public details::AttributesPanel {
+public:
+ EllipsePanel(Glib::RefPtr<Gtk::Builder> builder) :
+ _main(get_widget<Gtk::Grid>(builder, "ellipse-main")),
+ _rx(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "el-rx")),
+ _ry(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "el-ry")),
+ _start(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "el-start")),
+ _end(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "el-end")),
+ _slice(get_widget<Gtk::RadioButton>(builder, "el-slice")),
+ _arc(get_widget<Gtk::RadioButton>(builder, "el-arc")),
+ _chord(get_widget<Gtk::RadioButton>(builder, "el-chord")),
+ _whole(get_widget<Gtk::Button>(builder, "el-whole"))
+ {
+ _title = _("Ellipse");
+ _widget = &_main;
+
+ _type[0] = &_slice;
+ _type[1] = &_arc;
+ _type[2] = &_chord;
+
+ int type = 0;
+ for (auto btn : _type) {
+ btn->signal_toggled().connect([=](){ set_type(type); });
+ type++;
+ }
+
+ _whole.signal_clicked().connect([=](){
+ _start.get_adjustment()->set_value(0);
+ _end.get_adjustment()->set_value(0);
+ });
+
+ auto normalize = [=](){
+ _ellipse->normalize();
+ _ellipse->updateRepr();
+ _ellipse->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ };
+
+ _rx.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_ellipse, _rx.get_adjustment(), nullptr, [=](double rx){ _ellipse->setVisibleRx(rx); normalize(); });
+ });
+ _ry.get_adjustment()->signal_value_changed().connect([=](){
+ change_value_px(_ellipse, _ry.get_adjustment(), nullptr, [=](double ry){ _ellipse->setVisibleRy(ry); normalize(); });
+ });
+ _start.get_adjustment()->signal_value_changed().connect([=](){
+ change_angle(_ellipse, _start.get_adjustment(), [=](double s){ _ellipse->start = s; normalize(); });
+ });
+ _end.get_adjustment()->signal_value_changed().connect([=](){
+ change_angle(_ellipse, _end.get_adjustment(), [=](double e){ _ellipse->end = e; normalize(); });
+ });
+
+ get_widget<Gtk::Button>(builder, "el-round").signal_clicked().connect([=](){
+ auto [changed, x, y] = round_values(_rx, _ry);
+ if (changed && x > 0 && y > 0) {
+ _rx.get_adjustment()->set_value(x);
+ _ry.get_adjustment()->set_value(y);
+ }
+ });
+ }
+
+ ~EllipsePanel() override = default;
+
+ void update(SPObject* object) override {
+ _ellipse = cast<SPGenericEllipse>(object);
+ if (!_ellipse) return;
+
+ auto scoped(_update.block());
+ _rx.set_value(_ellipse->rx.value);
+ _ry.set_value(_ellipse->ry.value);
+ _start.set_value(radians_to_degree_mod360(_ellipse->start));
+ _end.set_value(radians_to_degree_mod360(_ellipse->end));
+
+ _slice.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE);
+ _arc.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_ARC);
+ _chord.set_active(_ellipse->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD);
+
+ auto slice = !_ellipse->is_whole();
+ _whole.set_sensitive(slice);
+ for (auto btn : _type) {
+ btn->set_sensitive(slice);
+ }
+ }
+
+ void set_type(int type) {
+ if (!_ellipse) return;
+
+ auto scoped(_update.block());
+
+ Glib::ustring arc_type = "slice";
+ bool open = false;
+ switch (type) {
+ case 0:
+ arc_type = "slice";
+ open = false;
+ break;
+ case 1:
+ arc_type = "arc";
+ open = true;
+ break;
+ case 2:
+ arc_type = "chord";
+ open = true; // For backward compat, not truly open but chord most like arc.
+ break;
+ default:
+ std::cerr << "Ellipse type change - bad arc type: " << type << std::endl;
+ break;
+ }
+ _ellipse->setAttribute("sodipodi:open", open ? "true" : nullptr);
+ _ellipse->setAttribute("sodipodi:arc-type", arc_type.c_str());
+ _ellipse->updateRepr();
+ DocumentUndo::done(_ellipse->document, _("Change arc type"), INKSCAPE_ICON("draw-ellipse"));
+ }
+
+private:
+ SPGenericEllipse* _ellipse = nullptr;
+ Gtk::Widget& _main;
+ Inkscape::UI::Widget::SpinButton& _rx;
+ Inkscape::UI::Widget::SpinButton& _ry;
+ Inkscape::UI::Widget::SpinButton& _start;
+ Inkscape::UI::Widget::SpinButton& _end;
+ Gtk::RadioButton& _slice;
+ Gtk::RadioButton& _arc;
+ Gtk::RadioButton& _chord;
+ Gtk::Button& _whole;
+ Gtk::RadioButton* _type[3];
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class StarPanel : public details::AttributesPanel {
+public:
+ StarPanel(Glib::RefPtr<Gtk::Builder> builder) :
+ _main(get_widget<Gtk::Grid>(builder, "star-main")),
+ _corners(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "star-corners")),
+ _ratio(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "star-ratio")),
+ _rounded(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "star-rounded")),
+ _rand(get_derived_widget<Inkscape::UI::Widget::SpinButton>(builder, "star-rand")),
+ _poly(get_widget<Gtk::RadioButton>(builder, "star-poly")),
+ _star(get_widget<Gtk::RadioButton>(builder, "star-star")),
+ _align(get_widget<Gtk::Button>(builder, "star-align")),
+ _clear_rnd(get_widget<Gtk::Button>(builder, "star-rnd-clear")),
+ _clear_round(get_widget<Gtk::Button>(builder, "star-round-clear")),
+ _clear_ratio(get_widget<Gtk::Button>(builder, "star-ratio-clear"))
+ {
+ _title = _("Star");
+ _widget = &_main;
+
+ _corners.get_adjustment()->signal_value_changed().connect([=](){
+ change_value(_path, _corners.get_adjustment(), [=](double sides) {
+ _path->setAttributeDouble("sodipodi:sides", (int)sides);
+ auto arg1 = get_number(_path, "sodipodi:arg1").value_or(0.5);
+ _path->setAttributeDouble("sodipodi:arg2", arg1 + M_PI / sides);
+ _path->updateRepr();
+ });
+ });
+ _rounded.get_adjustment()->signal_value_changed().connect([=](){
+ change_value(_path, _rounded.get_adjustment(), [=](double rounded) {
+ _path->setAttributeDouble("inkscape:rounded", rounded);
+ _path->updateRepr();
+ });
+ });
+ _ratio.get_adjustment()->signal_value_changed().connect([=](){
+ change_value(_path, _ratio.get_adjustment(), [=](double ratio){
+ auto r1 = get_number(_path, "sodipodi:r1").value_or(1.0);
+ auto r2 = get_number(_path, "sodipodi:r2").value_or(1.0);
+ if (r2 < r1) {
+ _path->setAttributeDouble("sodipodi:r2", r1 * ratio);
+ } else {
+ _path->setAttributeDouble("sodipodi:r1", r2 * ratio);
+ }
+ _path->updateRepr();
+ });
+ });
+ _rand.get_adjustment()->signal_value_changed().connect([=](){
+ change_value(_path, _rand.get_adjustment(), [=](double rnd){
+ _path->setAttributeDouble("inkscape:randomized", rnd);
+ _path->updateRepr();
+ });
+ });
+ _clear_rnd.signal_clicked().connect([=](){ _rand.get_adjustment()->set_value(0); });
+ _clear_round.signal_clicked().connect([=](){ _rounded.get_adjustment()->set_value(0); });
+ _clear_ratio.signal_clicked().connect([=](){ _ratio.get_adjustment()->set_value(0.5); });
+
+ _poly.signal_toggled().connect([=](){ set_flat(true); });
+ _star.signal_toggled().connect([=](){ set_flat(false); });
+
+ _align.signal_clicked().connect([=](){
+ change_value(_path, {}, [=](double) { align_star_shape(_path); });
+ });
+ }
+
+ ~StarPanel() override = default;
+
+ void update(SPObject* object) override {
+ _path = cast<SPStar>(object);
+ if (!_path) return;
+
+ auto scoped(_update.block());
+ _corners.set_value(_path->sides);
+ double r1 = get_number(_path, "sodipodi:r1").value_or(0.5);
+ double r2 = get_number(_path, "sodipodi:r2").value_or(0.5);
+ if (r2 < r1) {
+ _ratio.set_value(r1 > 0 ? r2 / r1 : 0.5);
+ } else {
+ _ratio.set_value(r2 > 0 ? r1 / r2 : 0.5);
+ }
+ _rounded.set_value(_path->rounded);
+ _rand.set_value(_path->randomized);
+ widget_show(_clear_rnd, _path->randomized != 0);
+ widget_show(_clear_round, _path->rounded != 0);
+ widget_show(_clear_ratio, std::abs(_ratio.get_value() - 0.5) > 0.0005);
+
+ _poly.set_active(_path->flatsided);
+ _star.set_active(!_path->flatsided);
+ }
+
+ void set_flat(bool flat) {
+ change_value(_path, {}, [=](double){
+ _path->setAttribute("inkscape:flatsided", flat ? "true" : "false");
+ _path->updateRepr();
+ });
+ // adjust corners/sides
+ _corners.get_adjustment()->set_lower(flat ? 3 : 2);
+ if (flat && _corners.get_value() < 3) {
+ _corners.get_adjustment()->set_value(3);
+ }
+ }
+
+private:
+ SPStar* _path = nullptr;
+ Gtk::Widget& _main;
+ Inkscape::UI::Widget::SpinButton& _corners;
+ Inkscape::UI::Widget::SpinButton& _ratio;
+ Inkscape::UI::Widget::SpinButton& _rounded;
+ Inkscape::UI::Widget::SpinButton& _rand;
+ Gtk::Button& _clear_rnd;
+ Gtk::Button& _clear_round;
+ Gtk::Button& _clear_ratio;
+ Gtk::Button& _align;
+ Gtk::RadioButton& _poly;
+ Gtk::RadioButton& _star;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+class TextPanel : public details::AttributesPanel {
+public:
+ TextPanel(Glib::RefPtr<Gtk::Builder> builder) :
+ _main(get_widget<Gtk::Grid>(builder, "text-main"))
+ {
+ // TODO - text panel
+ }
+
+private:
+ Gtk::Widget& _main;
+
+};
+
+///////////////////////////////////////////////////////////////////////////////
+
+std::string get_key(SPObject* object) {
+ if (!object) return {};
+
+ return typeid(*object).name();
+}
+
+details::AttributesPanel* ObjectAttributes::get_panel(SPObject* object) {
+ if (!object) return nullptr;
+
+ std::string name = get_key(object);
+ auto it = _panels.find(name);
+ return it == _panels.end() ? nullptr : it->second.get();
+}
+
+void ObjectAttributes::create_panels() {
+ _panels[typeid(SPImage).name()] = std::make_unique<ImagePanel>();
+ _panels[typeid(SPRect).name()] = std::make_unique<RectPanel>(_builder);
+ _panels[typeid(SPGenericEllipse).name()] = std::make_unique<EllipsePanel>(_builder);
+ _panels[typeid(SPStar).name()] = std::make_unique<StarPanel>(_builder);
+ _panels[typeid(SPAnchor).name()] = std::make_unique<AnchorPanel>();
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/object-attributes.h b/src/ui/dialog/object-attributes.h
new file mode 100644
index 0000000..1136e85
--- /dev/null
+++ b/src/ui/dialog/object-attributes.h
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Generic object attribute editor
+ *//*
+ * Authors:
+ * see git history
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_DIALOGS_OBJECT_ATTRIBUTES_H
+#define SEEN_DIALOGS_OBJECT_ATTRIBUTES_H
+
+#include "desktop.h"
+#include "object/sp-object.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/operation-blocker.h"
+#include "ui/widget/spinbutton.h"
+#include "ui/widget/style-swatch.h"
+#include "ui/widget/unit-tracker.h"
+#include <glibmm/ustring.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/label.h>
+#include <gtkmm/widget.h>
+#include <memory>
+#include <string>
+#include <map>
+
+class SPAttributeTable;
+class SPItem;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+namespace details {
+ class AttributesPanel {
+ public:
+ AttributesPanel();
+ virtual ~AttributesPanel() = default;
+
+ void update_panel(SPObject* object, SPDesktop* desktop);
+ Gtk::Widget& widget() { if(!_widget) throw "crap"; return *_widget; }
+ Glib::ustring get_title() const { return _title; }
+ bool supports_fill_stroke() const {return _show_fill_stroke; }
+
+ protected:
+ virtual void update(SPObject* object) = 0;
+ // value with units changed by the user; modify current object
+ void change_value_px(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, const char* attr, std::function<void (double)>&& setter);
+ // angle in degrees changed by the user; modify current object
+ void change_angle(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, std::function<void (double)>&& setter);
+ // modify current object
+ void change_value(SPObject* object, const Glib::RefPtr<Gtk::Adjustment>& adj, std::function<void (double)>&& setter);
+
+ SPDesktop* _desktop = nullptr;
+ OperationBlocker _update;
+ bool _show_fill_stroke = true;
+ Glib::ustring _title;
+ Gtk::Widget* _widget = nullptr;
+ std::unique_ptr<UI::Widget::UnitTracker> _tracker;
+ };
+}
+
+/**
+ * A dialog widget to show object attributes (currently for images and links).
+ */
+class ObjectAttributes : public DialogBase
+{
+public:
+ ObjectAttributes();
+ ~ObjectAttributes() override = default;
+
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+ void desktopReplaced() override;
+
+ /**
+ * Updates entries and other child widgets on selection change, object modification, etc.
+ */
+ void widget_setup();
+
+private:
+ Glib::RefPtr<Gtk::Builder> _builder;
+
+ void create_panels();
+ std::map<std::string, std::unique_ptr<details::AttributesPanel>> _panels;
+ details::AttributesPanel* get_panel(SPObject* object);
+ void update_panel(SPObject* object);
+
+ details::AttributesPanel* _current_panel = nullptr;
+ OperationBlocker _update;
+ Gtk::Box& _main_panel;
+ Gtk::Label& _obj_title;
+ // Contains a pointer to the currently selected item (NULL in case nothing is or multiple objects are selected).
+ SPItem* _current_item = nullptr;
+ Inkscape::UI::Widget::StyleSwatch _style_swatch;
+};
+
+}
+}
+}
+
+#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/dialog/object-properties.cpp b/src/ui/dialog/object-properties.cpp
new file mode 100644
index 0000000..6476b40
--- /dev/null
+++ b/src/ui/dialog/object-properties.cpp
@@ -0,0 +1,581 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file Object properties dialog.
+ */
+/*
+ * Inkscape, an Open Source vector graphics editor
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * Copyright (C) 2012 Kris De Gussem <Kris.DeGussem@gmail.com>
+ * c++ version based on former C-version (GPL v2+) with authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Abhishek Sharma
+ */
+
+#include "object-properties.h"
+
+#include <glibmm/i18n.h>
+
+#include <gtkmm/grid.h>
+
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "style.h"
+#include "style-enums.h"
+
+#include "object/sp-image.h"
+#include "ui/icon-names.h"
+#include "widgets/sp-attribute-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+ObjectProperties::ObjectProperties()
+ : DialogBase("/dialogs/object/", "ObjectProperties")
+ , _blocked(false)
+ , _current_item(nullptr)
+ , _label_id(_("_ID:"), true)
+ , _label_label(_("_Label:"), true)
+ , _label_title(_("_Title:"), true)
+ , _label_dpi(_("_DPI SVG:"), true)
+ , _label_image_rendering(_("_Image Rendering:"), true)
+ , _label_color(_("Highlight Color:"), true)
+ , _highlight_color(_("Highlight Color"), "", 0xff0000ff, true)
+ , _cb_hide(_("_Hide"), true)
+ , _cb_lock(_("L_ock"), true)
+ , _cb_aspect_ratio(_("Preserve Ratio"), true)
+ , _exp_interactivity(_("_Interactivity"), true)
+ , _attr_table(Gtk::manage(new SPAttributeTable()))
+{
+ //initialize labels for the table at the bottom of the dialog
+ _int_attrs.emplace_back("onclick");
+ _int_attrs.emplace_back("onmouseover");
+ _int_attrs.emplace_back("onmouseout");
+ _int_attrs.emplace_back("onmousedown");
+ _int_attrs.emplace_back("onmouseup");
+ _int_attrs.emplace_back("onmousemove");
+ _int_attrs.emplace_back("onfocusin");
+ _int_attrs.emplace_back("onfocusout");
+ _int_attrs.emplace_back("onload");
+
+ _int_labels.emplace_back("onclick:");
+ _int_labels.emplace_back("onmouseover:");
+ _int_labels.emplace_back("onmouseout:");
+ _int_labels.emplace_back("onmousedown:");
+ _int_labels.emplace_back("onmouseup:");
+ _int_labels.emplace_back("onmousemove:");
+ _int_labels.emplace_back("onfocusin:");
+ _int_labels.emplace_back("onfocusout:");
+ _int_labels.emplace_back("onload:");
+
+ _init();
+}
+
+void ObjectProperties::_init()
+{
+ set_spacing(0);
+
+ auto grid_top = Gtk::manage(new Gtk::Grid());
+ grid_top->set_row_spacing(4);
+ grid_top->set_column_spacing(0);
+ grid_top->set_border_width(4);
+
+ pack_start(*grid_top, false, false, 0);
+
+
+ /* Create the label for the object id */
+ _label_id.set_label(_label_id.get_label() + " ");
+ _label_id.set_halign(Gtk::ALIGN_START);
+ _label_id.set_valign(Gtk::ALIGN_CENTER);
+
+ /* Create the entry box for the object id */
+ _entry_id.set_tooltip_text(_("The id= attribute (only letters, digits, and the characters .-_: allowed)"));
+ _entry_id.set_max_length(64);
+ _entry_id.set_hexpand();
+ _entry_id.set_valign(Gtk::ALIGN_CENTER);
+
+ _label_id.set_mnemonic_widget(_entry_id);
+
+ // pressing enter in the id field is the same as clicking Set:
+ _entry_id.signal_activate().connect(sigc::mem_fun(*this, &ObjectProperties::_labelChanged));
+ // focus is in the id field initially:
+ _entry_id.grab_focus();
+
+
+ /* Create the label for the object label */
+ _label_label.set_label(_label_label.get_label() + " ");
+ _label_label.set_halign(Gtk::ALIGN_START);
+ _label_label.set_valign(Gtk::ALIGN_CENTER);
+
+ /* Create the entry box for the object label */
+ _entry_label.set_tooltip_text(_("A freeform label for the object"));
+ _entry_label.set_max_length(256);
+
+ _entry_label.set_hexpand();
+ _entry_label.set_valign(Gtk::ALIGN_CENTER);
+
+ _label_label.set_mnemonic_widget(_entry_label);
+
+ // pressing enter in the label field is the same as clicking Set:
+ _entry_label.signal_activate().connect(sigc::mem_fun(*this, &ObjectProperties::_labelChanged));
+
+
+ /* Create the label for the object title */
+ _label_title.set_label(_label_title.get_label() + " ");
+ _label_title.set_halign(Gtk::ALIGN_START);
+ _label_title.set_valign(Gtk::ALIGN_CENTER);
+
+ /* Create the entry box for the object title */
+ _entry_title.set_sensitive (FALSE);
+ _entry_title.set_max_length (256);
+
+ _entry_title.set_hexpand();
+ _entry_title.set_valign(Gtk::ALIGN_CENTER);
+
+ _label_title.set_mnemonic_widget(_entry_title);
+ // pressing enter in the label field is the same as clicking Set:
+ _entry_title.signal_activate().connect(sigc::mem_fun(*this, &ObjectProperties::_labelChanged));
+
+ _label_color.set_mnemonic_widget(_highlight_color);
+ _label_color.set_halign(Gtk::ALIGN_START);
+ _highlight_color.connectChanged(sigc::mem_fun(*this, &ObjectProperties::_highlightChanged));
+
+ /* Create the frame for the object description */
+ Gtk::Label *label_desc = Gtk::manage(new Gtk::Label(_("_Description:"), true));
+ UI::Widget::Frame *frame_desc = Gtk::manage(new UI::Widget::Frame("", FALSE));
+ frame_desc->set_label_widget(*label_desc);
+ frame_desc->set_padding (0,0,0,0);
+ pack_start(*frame_desc, true, true, 0);
+
+ /* Create the text view box for the object description */
+ _ft_description.set_border_width(4);
+ _ft_description.set_sensitive(FALSE);
+ frame_desc->add(_ft_description);
+ _ft_description.set_shadow_type(Gtk::SHADOW_IN);
+
+ _tv_description.set_wrap_mode(Gtk::WRAP_WORD);
+ _tv_description.get_buffer()->set_text("");
+ _ft_description.add(_tv_description);
+ _tv_description.add_mnemonic_label(*label_desc);
+
+ /* Create the label for the object title */
+ _label_dpi.set_label(_label_dpi.get_label() + " ");
+ _label_dpi.set_halign(Gtk::ALIGN_START);
+ _label_dpi.set_valign(Gtk::ALIGN_CENTER);
+
+ /* Create the entry box for the SVG DPI */
+ _spin_dpi.set_digits(2);
+ _spin_dpi.set_range(1, 1200);
+
+ _label_dpi.set_mnemonic_widget(_spin_dpi);
+ // pressing enter in the label field is the same as clicking Set:
+ _spin_dpi.signal_activate().connect(sigc::mem_fun(*this, &ObjectProperties::_labelChanged));
+
+ /* Image rendering */
+ /* Create the label for the object ImageRendering */
+ _label_image_rendering.set_label(_label_image_rendering.get_label() + " ");
+ _label_image_rendering.set_halign(Gtk::ALIGN_START);
+ _label_image_rendering.set_valign(Gtk::ALIGN_CENTER);
+
+ /* Create the combo box text for the 'image-rendering' property */
+ for (unsigned i = 0; enum_image_rendering[i].key; ++i) {
+ _combo_image_rendering.append(enum_image_rendering[i].key);
+ }
+ _combo_image_rendering.set_tooltip_text(_("The 'image-rendering' property can influence how a bitmap is re-scaled:\n"
+ "\t• 'auto': no preference (scaled image is usually smooth but blurred)\n"
+ "\t• 'optimizeQuality': prefer rendering quality (usually smooth but blurred)\n"
+ "\t• 'optimizeSpeed': prefer rendering speed (usually blocky)\n"
+ "\t• 'crisp-edges': rescale without blurring edges (often blocky)\n"
+ "\t• 'pixelated': render blocky\n"
+ "Note that the specification of this property is not finalized. "
+ "Support and interpretation of these values varies between renderers."));
+
+ _combo_image_rendering.set_valign(Gtk::ALIGN_CENTER);
+
+ _label_image_rendering.set_mnemonic_widget(_combo_image_rendering);
+
+ _combo_image_rendering.signal_changed().connect(
+ sigc::mem_fun(*this, &ObjectProperties::_imageRenderingChanged)
+ );
+
+
+ grid_top->attach(_label_id, 0, 0, 1, 1);
+ grid_top->attach(_entry_id, 1, 0, 1, 1);
+ grid_top->attach(_label_label, 0, 1, 1, 1);
+ grid_top->attach(_entry_label, 1, 1, 1, 1);
+ grid_top->attach(_label_title, 0, 2, 1, 1);
+ grid_top->attach(_entry_title, 1, 2, 1, 1);
+ grid_top->attach(_label_color, 0, 3, 1, 1);
+ grid_top->attach(_highlight_color, 1, 3, 1, 1);
+ grid_top->attach(_label_dpi, 0, 4, 1, 1);
+ grid_top->attach(_spin_dpi, 1, 4, 1, 1);
+ grid_top->attach(_label_image_rendering, 0, 5, 1, 1);
+ grid_top->attach(_combo_image_rendering, 1, 5, 1, 1);
+
+ /* Check boxes */
+ Gtk::Box *hb_checkboxes = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ pack_start(*hb_checkboxes, Gtk::PACK_SHRINK, 0);
+
+ auto grid_cb = Gtk::manage(new Gtk::Grid());
+ grid_cb->set_row_homogeneous();
+ grid_cb->set_column_homogeneous(true);
+
+ grid_cb->set_border_width(4);
+ hb_checkboxes->pack_start(*grid_cb, true, true, 0);
+
+ /* Hide */
+ _cb_hide.set_tooltip_text (_("Check to make the object invisible"));
+ _cb_hide.set_hexpand();
+ _cb_hide.set_valign(Gtk::ALIGN_CENTER);
+ grid_cb->attach(_cb_hide, 0, 0, 1, 1);
+
+ _cb_hide.signal_toggled().connect(sigc::mem_fun(*this, &ObjectProperties::_hiddenToggled));
+
+ /* Lock */
+ // TRANSLATORS: "Lock" is a verb here
+ _cb_lock.set_tooltip_text(_("Check to make the object insensitive (not selectable by mouse)"));
+ _cb_lock.set_hexpand();
+ _cb_lock.set_valign(Gtk::ALIGN_CENTER);
+ grid_cb->attach(_cb_lock, 1, 0, 1, 1);
+
+ _cb_lock.signal_toggled().connect(sigc::mem_fun(*this, &ObjectProperties::_sensitivityToggled));
+
+ /* Preserve aspect ratio */
+ _cb_aspect_ratio.set_tooltip_text(_("Check to preserve aspect ratio on images"));
+ _cb_aspect_ratio.set_hexpand();
+ _cb_aspect_ratio.set_valign(Gtk::ALIGN_CENTER);
+ grid_cb->attach(_cb_aspect_ratio, 0, 1, 1, 1);
+
+ _cb_aspect_ratio.signal_toggled().connect(sigc::mem_fun(*this, &ObjectProperties::_aspectRatioToggled));
+
+
+ /* Button for setting the object's id, label, title and description. */
+ Gtk::Button *btn_set = Gtk::manage(new Gtk::Button(_("_Set"), true));
+ btn_set->set_hexpand();
+ btn_set->set_valign(Gtk::ALIGN_CENTER);
+ grid_cb->attach(*btn_set, 1, 1, 1, 1);
+
+ btn_set->signal_clicked().connect(sigc::mem_fun(*this, &ObjectProperties::_labelChanged));
+
+ /* Interactivity options */
+ _exp_interactivity.set_vexpand(false);
+ pack_start(_exp_interactivity, Gtk::PACK_SHRINK);
+ show_all();
+}
+
+void ObjectProperties::update_entries()
+{
+ if (_blocked || !getDesktop()) {
+ return;
+ }
+
+ auto selection = getSelection();
+ if (!selection) return;
+
+ if (!selection->singleItem()) {
+ set_sensitive (false);
+ _current_item = nullptr;
+ //no selection anymore or multiple objects selected, means that we need
+ //to close the connections to the previously selected object
+ _attr_table->clear();
+ _highlight_color.setRgba32(0x0);
+ return;
+ } else {
+ set_sensitive (true);
+ }
+
+ SPItem *item = selection->singleItem();
+ if (_current_item == item)
+ {
+ //otherwise we would end up wasting resources through the modify selection
+ //callback when moving an object (endlessly setting the labels and recreating _attr_table)
+ return;
+ }
+ _blocked = true;
+ _cb_aspect_ratio.set_active(g_strcmp0(item->getAttribute("preserveAspectRatio"), "none") != 0);
+ _cb_lock.set_active(item->isLocked()); /* Sensitive */
+ _cb_hide.set_active(item->isExplicitlyHidden()); /* Hidden */
+ _highlight_color.setRgba32(item->highlight_color());
+ _highlight_color.closeWindow();
+
+ if (item->cloned) {
+ /* ID */
+ _entry_id.set_text("");
+ _entry_id.set_sensitive(FALSE);
+ _label_id.set_text(_("Ref"));
+
+ /* Label */
+ _entry_label.set_text("");
+ _entry_label.set_sensitive(FALSE);
+ _label_label.set_text(_("Ref"));
+
+ } else {
+ SPObject *obj = static_cast<SPObject*>(item);
+
+ /* ID */
+ _entry_id.set_text(obj->getId() ? obj->getId() : "");
+ _entry_id.set_sensitive(TRUE);
+ _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" "));
+
+ /* Label */
+ char const *currentlabel = obj->label();
+ char const *placeholder = "";
+ if (!currentlabel) {
+ currentlabel = "";
+ placeholder = obj->defaultLabel();
+ }
+ _entry_label.set_text(currentlabel);
+ _entry_label.set_placeholder_text(placeholder);
+ _entry_label.set_sensitive(TRUE);
+
+ /* Title */
+ gchar *title = obj->title();
+ if (title) {
+ _entry_title.set_text(title);
+ g_free(title);
+ }
+ else {
+ _entry_title.set_text("");
+ }
+ _entry_title.set_sensitive(TRUE);
+
+ /* Image Rendering */
+ if (is<SPImage>(item)) {
+ _combo_image_rendering.show();
+ _label_image_rendering.show();
+ _combo_image_rendering.set_active(obj->style->image_rendering.value);
+ if (obj->getAttribute("inkscape:svg-dpi")) {
+ _spin_dpi.set_value(std::stod(obj->getAttribute("inkscape:svg-dpi")));
+ _spin_dpi.show();
+ _label_dpi.show();
+ } else {
+ _spin_dpi.hide();
+ _label_dpi.hide();
+ }
+ } else {
+ _combo_image_rendering.hide();
+ _combo_image_rendering.unset_active();
+ _label_image_rendering.hide();
+ _spin_dpi.hide();
+ _label_dpi.hide();
+ }
+
+ /* Description */
+ gchar *desc = obj->desc();
+ if (desc) {
+ _tv_description.get_buffer()->set_text(desc);
+ g_free(desc);
+ } else {
+ _tv_description.get_buffer()->set_text("");
+ }
+ _ft_description.set_sensitive(TRUE);
+
+ if (_current_item == nullptr) {
+ _attr_table->set_object(obj, _int_labels, _int_attrs, (GtkWidget*) _exp_interactivity.gobj());
+ } else {
+ _attr_table->change_object(obj);
+ }
+ _attr_table->show_all();
+ }
+ _current_item = item;
+ _blocked = false;
+}
+
+void ObjectProperties::_labelChanged()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = getSelection()->singleItem();
+ g_return_if_fail (item != nullptr);
+
+ _blocked = true;
+
+ /* Retrieve the label widget for the object's id */
+ gchar *id = g_strdup(_entry_id.get_text().c_str());
+ g_strcanon(id, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:", '_');
+ if (g_strcmp0(id, item->getId()) == 0) {
+ _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" "));
+ } else if (!*id || !isalnum (*id)) {
+ _label_id.set_text(_("Id invalid! "));
+ } else if (getDocument()->getObjectById(id) != nullptr) {
+ _label_id.set_text(_("Id exists! "));
+ } else {
+ _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" "));
+ item->setAttribute("id", id);
+ DocumentUndo::done(getDocument(), _("Set object ID"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ g_free(id);
+
+ /* Retrieve the label widget for the object's label */
+ Glib::ustring label = _entry_label.get_text();
+
+ /* Give feedback on success of setting the drawing object's label
+ * using the widget's label text
+ */
+ SPObject *obj = static_cast<SPObject*>(item);
+ char const *currentlabel = obj->label();
+ if (label.compare(currentlabel ? currentlabel : "")) {
+ obj->setLabel(label.c_str());
+ DocumentUndo::done(getDocument(), _("Set object label"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+
+ /* Retrieve the title */
+ if (obj->setTitle(_entry_title.get_text().c_str())) {
+ DocumentUndo::done(getDocument(), _("Set object title"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+
+ /* Retrieve the DPI */
+ if (is<SPImage>(obj)) {
+ Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value());
+ obj->setAttribute("inkscape:svg-dpi", dpi_value);
+ DocumentUndo::done(getDocument(), _("Set image DPI"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+
+ /* Retrieve the description */
+ Gtk::TextBuffer::iterator start, end;
+ _tv_description.get_buffer()->get_bounds(start, end);
+ Glib::ustring desc = _tv_description.get_buffer()->get_text(start, end, TRUE);
+ if (obj->setDesc(desc.c_str())) {
+ DocumentUndo::done(getDocument(), _("Set object description"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+
+ _blocked = false;
+}
+
+void ObjectProperties::_highlightChanged(guint rgba)
+{
+ if (_blocked)
+ return;
+
+ if (auto item = getSelection()->singleItem()) {
+ item->setHighlight(rgba);
+ DocumentUndo::done(getDocument(), _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+}
+
+void ObjectProperties::_imageRenderingChanged()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = getSelection()->singleItem();
+ g_return_if_fail (item != nullptr);
+
+ _blocked = true;
+
+ Glib::ustring scale = _combo_image_rendering.get_active_text();
+
+ // We should unset if the parent computed value is auto and the desired value is auto.
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "image-rendering", scale.c_str());
+ Inkscape::XML::Node *image_node = item->getRepr();
+ if (image_node) {
+ sp_repr_css_change(image_node, css, "style");
+ DocumentUndo::done(getDocument(), _("Set image rendering option"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ sp_repr_css_attr_unref(css);
+
+ _blocked = false;
+}
+
+void ObjectProperties::_sensitivityToggled()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = getSelection()->singleItem();
+ g_return_if_fail(item != nullptr);
+
+ _blocked = true;
+ item->setLocked(_cb_lock.get_active());
+ DocumentUndo::done(getDocument(), _cb_lock.get_active() ? _("Lock object") : _("Unlock object"), INKSCAPE_ICON("dialog-object-properties"));
+ _blocked = false;
+}
+
+void ObjectProperties::_aspectRatioToggled()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = getSelection()->singleItem();
+ g_return_if_fail(item != nullptr);
+
+ _blocked = true;
+
+ const char *active;
+ if (_cb_aspect_ratio.get_active()) {
+ active = "xMidYMid";
+ }
+ else {
+ active = "none";
+ }
+ /* Retrieve the DPI */
+ if (is<SPImage>(item)) {
+ Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value());
+ item->setAttribute("preserveAspectRatio", active);
+ DocumentUndo::done(getDocument(), _("Set preserve ratio"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ _blocked = false;
+}
+
+void ObjectProperties::_hiddenToggled()
+{
+ if (_blocked) {
+ return;
+ }
+
+ SPItem *item = getSelection()->singleItem();
+ g_return_if_fail(item != nullptr);
+
+ _blocked = true;
+ item->setExplicitlyHidden(_cb_hide.get_active());
+ DocumentUndo::done(getDocument(), _cb_hide.get_active() ? _("Hide object") : _("Unhide object"), INKSCAPE_ICON("dialog-object-properties"));
+ _blocked = false;
+}
+
+void ObjectProperties::selectionChanged(Selection *selection)
+{
+ update_entries();
+}
+
+void ObjectProperties::desktopReplaced()
+{
+ update_entries();
+}
+
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/object-properties.h b/src/ui/dialog/object-properties.h
new file mode 100644
index 0000000..f52f019
--- /dev/null
+++ b/src/ui/dialog/object-properties.h
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file Object properties dialog.
+ */
+/*
+ * Inkscape, an Open Source vector graphics editor
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * Copyright (C) 2012 Kris De Gussem <Kris.DeGussem@gmail.com>
+ * c++version based on former C-version (GPL v2+) with authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Abhishek Sharma
+ */
+
+#ifndef SEEN_DIALOGS_ITEM_PROPERTIES_H
+#define SEEN_DIALOGS_ITEM_PROPERTIES_H
+
+
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/frame.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/textview.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/scrollprotected.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/frame.h"
+
+class SPAttributeTable;
+class SPItem;
+
+namespace Gtk {
+class Grid;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * A dialog widget to show object properties.
+ *
+ * A widget to enter an ID, label, title and description for an object.
+ * In addition it allows to edit the properties of an object.
+ */
+class ObjectProperties : public DialogBase
+{
+public:
+ ObjectProperties();
+ ~ObjectProperties() override {};
+
+ /// Updates entries and other child widgets on selection change, object modification, etc.
+ void update_entries();
+ void selectionChanged(Selection *selection) override;
+
+private:
+ bool _blocked;
+ SPItem *_current_item; //to store the current item, for not wasting resources
+ std::vector<Glib::ustring> _int_attrs;
+ std::vector<Glib::ustring> _int_labels;
+
+ Gtk::Label _label_id; //the label for the object ID
+ Gtk::Entry _entry_id; //the entry for the object ID
+ Gtk::Label _label_label; //the label for the object label
+ Gtk::Entry _entry_label; //the entry for the object label
+ Gtk::Label _label_title; //the label for the object title
+ Gtk::Entry _entry_title; //the entry for the object title
+
+ Gtk::Label _label_color; //the label for the object highlight
+ Inkscape::UI::Widget::ColorPicker _highlight_color; // color picker for the object highlight
+
+ Gtk::Label _label_image_rendering; // the label for 'image-rendering'
+ Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> _combo_image_rendering; // the combo box text for 'image-rendering'
+
+ Gtk::Frame _ft_description; //the frame for the text of the object description
+ Gtk::TextView _tv_description; //the text view object showing the object description
+
+ Gtk::CheckButton _cb_hide; //the check button hide
+ Gtk::CheckButton _cb_lock; //the check button lock
+ Gtk::CheckButton _cb_aspect_ratio; //the preserve aspect ratio of images
+
+ Gtk::Label _label_dpi; //the entry for the dpi value
+ Gtk::SpinButton _spin_dpi; //the expander for interactivity
+ Gtk::Expander _exp_interactivity; //the expander for interactivity
+ SPAttributeTable *_attr_table; //the widget for showing the on... names at the bottom
+
+ /// Constructor auxiliary function creating the child widgets.
+ void _init();
+
+ /// Sets object properties (ID, label, title, description) on user input.
+ void _labelChanged();
+
+ // Callback for highlight color
+ void _highlightChanged(guint rgba);
+
+ /// Callback for 'image-rendering'.
+ void _imageRenderingChanged();
+
+ /// Callback for checkbox Lock.
+ void _sensitivityToggled();
+
+ /// Callback for checkbox Hide.
+ void _hiddenToggled();
+
+ /// Callback for checkbox Preserve Aspect Ratio.
+ void _aspectRatioToggled();
+
+ void desktopReplaced() override;
+};
+}
+}
+}
+
+#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/dialog/objects.cpp b/src/ui/dialog/objects.cpp
new file mode 100644
index 0000000..92f0892
--- /dev/null
+++ b/src/ui/dialog/objects.cpp
@@ -0,0 +1,1860 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A panel for listing objects in a document.
+ *
+ * Authors:
+ * Martin Owens
+ * Mike Kowalski
+ * Adam Belis (UX/Design)
+ *
+ * Copyright (C) Authors 2020-2022
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "objects.h"
+
+#include <glibmm/ustring.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/cellrenderer.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/icontheme.h>
+#include <gtkmm/imagemenuitem.h>
+#include <gtkmm/modelbutton.h>
+#include <gtkmm/object.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/separatormenuitem.h>
+#include <glibmm/main.h>
+#include <glibmm/i18n.h>
+#include <iomanip>
+#include <pango/pango-utils.h>
+#include <string>
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "filter-chemistry.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "message-stack.h"
+
+#include "actions/actions-tools.h"
+
+#include "include/gtkmm_version.h"
+
+#include "display/drawing-group.h"
+
+#include "object/filters/blend.h"
+#include "object/filters/gaussian-blur.h"
+#include "object/sp-clippath.h"
+#include "object/sp-mask.h"
+#include "object/sp-root.h"
+#include "object/sp-shape.h"
+#include "style-enums.h"
+#include "style.h"
+#include "svg/css-ostringstream.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/selected-color.h"
+#include "ui/shortcuts.h"
+#include "ui/tools/node-tool.h"
+
+#include "ui/contextmenu.h"
+#include "ui/util.h"
+#include "ui/widget/canvas.h"
+#include "ui/widget/filter-effect-chooser.h"
+#include "ui/widget/imagetoggler.h"
+#include "ui/widget/shapeicon.h"
+#include "ui/widget/objects-dialog-cells.h"
+#include "util/numeric/converters.h"
+
+// alpha (transparency) multipliers corresponding to item selection state combinations (SelectionState)
+// when 0 - do not color item's background
+static double const SELECTED_ALPHA[8] = {
+ 0.00, //0 not selected
+ 0.90, //1 selected
+ 0.50, //2 layer focused
+ 0.20, //3 layer focused & selected
+ 0.00, //4 child of focused layer
+ 0.90, //5 selected child of focused layer
+ 0.50, //6 2 and 4
+ 0.90 //7 1, 2 and 4
+};
+
+//#define DUMP_LAYERS 1
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class ObjectWatcher : public Inkscape::XML::NodeObserver
+{
+public:
+ ObjectWatcher() = delete;
+ ObjectWatcher(ObjectsPanel *panel, SPItem *, Gtk::TreeRow *row, bool is_filtered);
+ ~ObjectWatcher() override;
+
+ void initRowInfo();
+ void updateRowInfo();
+ void updateRowHighlight();
+ void updateRowAncestorState(bool invisible, bool locked);
+ void updateRowBg(guint32 rgba = 0.0);
+
+ ObjectWatcher *findChild(Node *node);
+ void addDummyChild();
+ bool addChild(SPItem *, bool dummy = true);
+ void addChildren(SPItem *, bool dummy = false);
+ void setSelectedBit(SelectionState mask, bool enabled);
+ void setSelectedBitRecursive(SelectionState mask, bool enabled);
+ void setSelectedBitChildren(SelectionState mask, bool enabled);
+ void rememberExtendedItems();
+ void moveChild(Node &child, Node *sibling);
+ bool isFiltered() const { return is_filtered; }
+
+ Gtk::TreeNodeChildren getChildren() const;
+ Gtk::TreeIter getChildIter(Node *) const;
+
+ void notifyChildAdded(Node &, Node &, Node *) override;
+ void notifyChildRemoved(Node &, Node &, Node *) override;
+ void notifyChildOrderChanged(Node &, Node &child, Node *, Node *) override;
+ void notifyAttributeChanged(Node &, GQuark, Util::ptr_shared, Util::ptr_shared) override;
+
+ /// Associate this watcher with a tree row
+ void setRow(const Gtk::TreeModel::Path &path)
+ {
+ assert(path);
+ row_ref = Gtk::TreeModel::RowReference(panel->_store, path);
+ }
+ void setRow(const Gtk::TreeModel::Row &row)
+ {
+ setRow(panel->_store->get_path(row));
+ }
+
+ // Get the path out of this watcher
+ Gtk::TreeModel::Path getTreePath() const {
+ return row_ref.get_path();
+ }
+
+ /// True if this watchr has a valid row reference.
+ bool hasRow() const { return bool(row_ref); }
+
+ /// Transfer a child watcher to its new parent
+ void transferChild(Node *childnode)
+ {
+ auto *target = panel->getWatcher(childnode->parent());
+ assert(target != this);
+ auto nh = child_watchers.extract(childnode);
+ assert(nh);
+ bool inserted = target->child_watchers.insert(std::move(nh)).inserted;
+ assert(inserted);
+ }
+
+ /// The XML node associated with this watcher.
+ Node *getRepr() const { return node; }
+ std::optional<Gtk::TreeRow> getRow() const {
+ if (auto path = row_ref.get_path()) {
+ if(auto iter = panel->_store->get_iter(path)) {
+ return *iter;
+ }
+ }
+ return std::nullopt;
+ }
+
+ std::unordered_map<Node const *, std::unique_ptr<ObjectWatcher>> child_watchers;
+
+private:
+ Node *node;
+ Gtk::TreeModel::RowReference row_ref;
+ ObjectsPanel *panel;
+ SelectionState selection_state;
+ bool is_filtered;
+};
+
+class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ ModelColumns()
+ {
+ add(_colNode);
+ add(_colLabel);
+ add(_colType);
+ add(_colIconColor);
+ add(_colClipMask);
+ add(_colBgColor);
+ add(_colInvisible);
+ add(_colLocked);
+ add(_colAncestorInvisible);
+ add(_colAncestorLocked);
+ add(_colHover);
+ add(_colItemStateSet);
+ add(_colBlendMode);
+ add(_colOpacity);
+ add(_colItemState);
+ add(_colHoverColor);
+ }
+ ~ModelColumns() override = default;
+ Gtk::TreeModelColumn<Node*> _colNode;
+ Gtk::TreeModelColumn<Glib::ustring> _colLabel;
+ Gtk::TreeModelColumn<Glib::ustring> _colType;
+ Gtk::TreeModelColumn<unsigned int> _colIconColor;
+ Gtk::TreeModelColumn<unsigned int> _colClipMask;
+ Gtk::TreeModelColumn<Gdk::RGBA> _colBgColor;
+ Gtk::TreeModelColumn<bool> _colInvisible;
+ Gtk::TreeModelColumn<bool> _colLocked;
+ Gtk::TreeModelColumn<bool> _colAncestorInvisible;
+ Gtk::TreeModelColumn<bool> _colAncestorLocked;
+ Gtk::TreeModelColumn<bool> _colHover;
+ Gtk::TreeModelColumn<bool> _colItemStateSet;
+ Gtk::TreeModelColumn<SPBlendMode> _colBlendMode;
+ Gtk::TreeModelColumn<double> _colOpacity;
+ Gtk::TreeModelColumn<Glib::ustring> _colItemState;
+ // Set when hovering over the color tag cell
+ Gtk::TreeModelColumn<bool> _colHoverColor;
+};
+
+/**
+ * Creates a new ObjectWatcher, a gtk TreeView iterated watching device.
+ *
+ * @param panel The panel to which the object watcher belongs
+ * @param obj The SPItem to watch in the document
+ * @param row The optional list store tree row for the item,
+ if not provided, assumes this is the root 'document' object.
+ * @param filtered, if true this watcher will filter all chldren using the panel filtering function on each item to decide if it should be shown.
+ */
+ObjectWatcher::ObjectWatcher(ObjectsPanel* panel, SPItem* obj, Gtk::TreeRow *row, bool filtered)
+ : panel(panel)
+ , row_ref()
+ , selection_state(0)
+ , is_filtered(filtered)
+ , node(obj->getRepr())
+{
+ if(row != nullptr) {
+ assert(row->children().empty());
+ setRow(*row);
+ initRowInfo();
+ updateRowInfo();
+ }
+ node->addObserver(*this);
+
+ // Only show children for groups (and their subclasses like SPAnchor or SPRoot)
+ if (!is<SPGroup>(obj)) {
+ return;
+ }
+
+ // Add children as a dummy row to avoid excensive execution when
+ // the tree is really large, but not in layers mode.
+ addChildren(obj, (bool)row && !obj->isExpanded());
+}
+ObjectWatcher::~ObjectWatcher()
+{
+ node->removeObserver(*this);
+ Gtk::TreeModel::Path path;
+ if (bool(row_ref) && (path = row_ref.get_path())) {
+ if (auto iter = panel->_store->get_iter(path)) {
+ panel->_store->erase(iter);
+ }
+ }
+ child_watchers.clear();
+}
+
+void ObjectWatcher::initRowInfo()
+{
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colHover] = false;
+}
+
+/**
+ * Update the information in the row from the stored node
+ */
+void ObjectWatcher::updateRowInfo()
+{
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ assert(row_ref);
+ assert(row_ref.get_path());
+
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colNode] = node;
+
+ // show ids without "#"
+ char const *id = item->getId();
+ row[_model->_colLabel] = (id && !item->label()) ? id : item->defaultLabel();
+
+ row[_model->_colType] = item->typeName();
+ row[_model->_colClipMask] =
+ (item->getClipObject() ? Inkscape::UI::Widget::OVERLAY_CLIP : 0) |
+ (item->getMaskObject() ? Inkscape::UI::Widget::OVERLAY_MASK : 0);
+ row[_model->_colInvisible] = item->isHidden();
+ row[_model->_colLocked] = !item->isSensitive();
+ auto blend = item->style && item->style->mix_blend_mode.set ? item->style->mix_blend_mode.value : SP_CSS_BLEND_NORMAL;
+ row[_model->_colBlendMode] = blend;
+ auto opacity = 1.0;
+ if (item->style && item->style->opacity.set) {
+ opacity = SP_SCALE24_TO_FLOAT(item->style->opacity.value);
+ }
+ row[_model->_colOpacity] = opacity;
+ std::string item_state;
+ if (opacity == 0.0) {
+ item_state = "object-transparent";
+ }
+ else if (blend != SP_CSS_BLEND_NORMAL) {
+ item_state = opacity == 1.0 ? "object-blend-mode" : "object-translucent-blend-mode";
+ }
+ else if (opacity < 1.0) {
+ item_state = "object-translucent";
+ }
+ row[_model->_colItemState] = item_state;
+ row[_model->_colItemStateSet] = !item_state.empty();
+
+ updateRowHighlight();
+ updateRowAncestorState(row[_model->_colAncestorInvisible], row[_model->_colAncestorLocked]);
+ }
+}
+
+/**
+ * Propagate changes to the highlight color to all children.
+ */
+void ObjectWatcher::updateRowHighlight() {
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ auto new_color = item->highlight_color();
+ if (new_color != row[panel->_model->_colIconColor]) {
+ row[panel->_model->_colIconColor] = new_color;
+ updateRowBg(new_color);
+ for (auto &watcher : child_watchers) {
+ watcher.second->updateRowHighlight();
+ }
+ }
+ }
+}
+
+/**
+ * Propagate a change in visibility or locked state to all children
+ */
+void ObjectWatcher::updateRowAncestorState(bool invisible, bool locked) {
+ auto _model = panel->_model;
+ auto row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colAncestorInvisible] = invisible;
+ row[_model->_colAncestorLocked] = locked;
+ for (auto &watcher : child_watchers) {
+ watcher.second->updateRowAncestorState(
+ invisible || row[_model->_colInvisible],
+ locked || row[_model->_colLocked]);
+ }
+}
+
+Gdk::RGBA selection_color;
+
+/**
+ * Updates the row's background colour as indicated by its selection.
+ */
+void ObjectWatcher::updateRowBg(guint32 rgba)
+{
+ assert(row_ref);
+ if (auto row = *panel->_store->get_iter(row_ref.get_path())) {
+ auto alpha = SELECTED_ALPHA[selection_state];
+ if (alpha == 0.0) {
+ row[panel->_model->_colBgColor] = Gdk::RGBA();
+ return;
+ }
+
+ const auto& sel = selection_color;
+ auto gdk_color = Gdk::RGBA();
+ gdk_color.set_red(sel.get_red());
+ gdk_color.set_green(sel.get_green());
+ gdk_color.set_blue(sel.get_blue());
+ gdk_color.set_alpha(sel.get_alpha() * alpha);
+ row[panel->_model->_colBgColor] = gdk_color;
+ }
+}
+
+/**
+ * Flip the selected state bit on or off as needed, calls updateRowBg if changed.
+ *
+ * @param mask - The selection bit to set or unset
+ * @param enabled - If the bit should be set or unset
+ */
+void ObjectWatcher::setSelectedBit(SelectionState mask, bool enabled) {
+ if (!row_ref) return;
+ SelectionState value = selection_state;
+ SelectionState original = value;
+ if (enabled) {
+ value |= mask;
+ } else {
+ value &= ~mask;
+ }
+ if (value != original) {
+ selection_state = value;
+ updateRowBg();
+ }
+}
+
+/**
+ * Flip the selected state bit on or off as needed, on this watcher and all
+ * its direct and indirect children.
+ */
+void ObjectWatcher::setSelectedBitRecursive(SelectionState mask, bool enabled)
+{
+ setSelectedBit(mask, enabled);
+ setSelectedBitChildren(mask, enabled);
+}
+void ObjectWatcher::setSelectedBitChildren(SelectionState mask, bool enabled)
+{
+ for (auto &pair : child_watchers) {
+ pair.second->setSelectedBitRecursive(mask, enabled);
+ }
+}
+
+/**
+ * Keep expanded rows expanded and recurse through all children.
+ */
+void ObjectWatcher::rememberExtendedItems()
+{
+ if (auto item = cast<SPItem>(panel->getObject(node))) {
+ if (item->isExpanded())
+ panel->_tree.expand_row(row_ref.get_path(), false);
+ }
+ for (auto &pair : child_watchers) {
+ pair.second->rememberExtendedItems();
+ }
+}
+
+/**
+ * Find the child watcher for the given node.
+ */
+ObjectWatcher *ObjectWatcher::findChild(Node *node)
+{
+ auto it = child_watchers.find(node);
+ if (it != child_watchers.end()) {
+ return it->second.get();
+ }
+ return nullptr;
+}
+
+/**
+ * Add the child object to this node.
+ *
+ * @param child - SPObject to be added
+ * @param dummy - Add a dummy objects (hidden) instead
+ *
+ * @returns true if child added was a dummy objects
+ */
+bool ObjectWatcher::addChild(SPItem *child, bool dummy)
+{
+ if (is_filtered && !panel->showChildInTree(child)) {
+ return false;
+ }
+
+ auto const children = getChildren();
+ if (!is_filtered && dummy && row_ref) {
+ if (children.empty()) {
+ auto const iter = panel->_store->append(children);
+ assert(panel->isDummy(*iter));
+ return true;
+ } else if (panel->isDummy(children[0])) {
+ return false;
+ }
+ }
+
+ auto *node = child->getRepr();
+ assert(node);
+ Gtk::TreeModel::Row row = *(panel->_store->prepend(children));
+
+ // Ancestor states are handled inside the list store (so we don't have to re-ask every update)
+ auto _model = panel->_model;
+ if (row_ref) {
+ auto parent_row = *panel->_store->get_iter(row_ref.get_path());
+ row[_model->_colAncestorInvisible] = parent_row[_model->_colAncestorInvisible] || parent_row[_model->_colInvisible];
+ row[_model->_colAncestorLocked] = parent_row[_model->_colAncestorLocked] || parent_row[_model->_colLocked];
+ } else {
+ row[_model->_colAncestorInvisible] = false;
+ row[_model->_colAncestorLocked] = false;
+ }
+
+ auto &watcher = child_watchers[node];
+ assert(!watcher);
+ watcher.reset(new ObjectWatcher(panel, child, &row, is_filtered));
+
+ // Make sure new children have the right focus set.
+ if ((selection_state & LAYER_FOCUSED) != 0) {
+ watcher->setSelectedBit(LAYER_FOCUS_CHILD, true);
+ }
+ return false;
+}
+
+/**
+ * Add all SPItem children as child rows.
+ */
+void ObjectWatcher::addChildren(SPItem *obj, bool dummy)
+{
+ assert(child_watchers.empty());
+
+ for (auto &child : obj->children) {
+ if (auto item = cast<SPItem>(&child)) {
+ if (addChild(item, dummy) && dummy) {
+ // one dummy child is enough to make the group expandable
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Move the child to just after the given sibling
+ *
+ * @param child - SPObject to be moved
+ * @param sibling - Optional sibling Object to add next to, if nullptr the
+ * object is moved to BEFORE the first item.
+ */
+void ObjectWatcher::moveChild(Node &child, Node *sibling)
+{
+ auto child_iter = getChildIter(&child);
+ if (!child_iter)
+ return; // This means the child was never added, probably not an SPItem.
+
+ // sibling might not be an SPItem and thus not be represented in the
+ // TreeView. Find the closest SPItem and use that for the reordering.
+ while (sibling && !is<SPItem>(panel->getObject(sibling))) {
+ sibling = sibling->prev();
+ }
+
+ auto sibling_iter = getChildIter(sibling);
+ panel->_store->move(child_iter, sibling_iter);
+}
+
+/**
+ * Get the TreeRow's children iterator
+ *
+ * @returns Gtk Tree Node Children iterator
+ */
+Gtk::TreeNodeChildren ObjectWatcher::getChildren() const
+{
+ Gtk::TreeModel::Path path;
+ if (row_ref && (path = row_ref.get_path())) {
+ return panel->_store->get_iter(path)->children();
+ }
+ assert(!row_ref);
+ return panel->_store->children();
+}
+
+/**
+ * Convert SPObject to TreeView Row, assuming the object is a child.
+ *
+ * @param child - The child object to find in this branch
+ * @returns Gtk TreeRow for the child, or end() if not found
+ */
+Gtk::TreeIter ObjectWatcher::getChildIter(Node *node) const
+{
+ auto childrows = getChildren();
+
+ if (!node) {
+ return childrows.end();
+ }
+
+ // Note: TreeRow inherits from TreeIter, so this `row` variable is
+ // also an iterator and a valid return value.
+ for (auto &row : childrows) {
+ if (panel->getRepr(row) == node) {
+ return row;
+ }
+ }
+ // In layer mode, we will come here for all non-layers
+ return childrows.begin();
+}
+
+void ObjectWatcher::notifyChildAdded( Node &node, Node &child, Node *prev )
+{
+ assert(this->node == &node);
+ // Ignore XML nodes which are not displayable items
+ if (auto item = cast<SPItem>(panel->getObject(&child))) {
+ addChild(item);
+ moveChild(child, prev);
+ }
+}
+void ObjectWatcher::notifyChildRemoved( Node &node, Node &child, Node* /*prev*/ )
+{
+ assert(this->node == &node);
+
+ if (child_watchers.erase(&child) > 0) {
+ return;
+ }
+
+ if (node.firstChild() == nullptr) {
+ assert(row_ref);
+ auto iter = panel->_store->get_iter(row_ref.get_path());
+ panel->removeDummyChildren(*iter);
+ }
+}
+void ObjectWatcher::notifyChildOrderChanged( Node &parent, Node &child, Node */*old_prev*/, Node *new_prev )
+{
+ assert(this->node == &parent);
+
+ moveChild(child, new_prev);
+}
+void ObjectWatcher::notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ )
+{
+ assert(this->node == &node);
+
+ // The root <svg> node doesn't have a row
+ if (this == panel->getRootWatcher()) {
+ return;
+ }
+
+ // Almost anything could change the icon, so update upon any change, defer for lots of updates.
+
+ // examples of not-so-obvious cases:
+ // - width/height: Can change type "circle" to an "ellipse"
+
+ static std::set<GQuark> const excluded{
+ g_quark_from_static_string("transform"),
+ g_quark_from_static_string("x"),
+ g_quark_from_static_string("y"),
+ g_quark_from_static_string("d"),
+ g_quark_from_static_string("sodipodi:nodetypes"),
+ };
+
+ if (excluded.count(name)) {
+ return;
+ }
+
+ updateRowInfo();
+}
+
+
+/**
+ * Get the object from the node.
+ *
+ * @param node - XML Node involved in the signal.
+ * @returns SPObject matching the node, returns nullptr if not found.
+ */
+SPObject *ObjectsPanel::getObject(Node *node) {
+ if (node != nullptr && getDocument())
+ return getDocument()->getObjectByRepr(node);
+ return nullptr;
+}
+
+/**
+ * Get the object watcher from the xml node (reverse lookup), it uses a ancesstor
+ * recursive pattern to match up with the root_watcher.
+ *
+ * @param node - The node to look up.
+ * @return the ObjectWatcher object if it's possible to find.
+ */
+ObjectWatcher* ObjectsPanel::getWatcher(Node *node)
+{
+ assert(node);
+ if (root_watcher->getRepr() == node) {
+ return root_watcher;
+ } else if (node->parent()) {
+ if (auto parent_watcher = getWatcher(node->parent())) {
+ return parent_watcher->findChild(node);
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Constructor
+ */
+ObjectsPanel::ObjectsPanel()
+ : DialogBase("/dialogs/objects", "Objects")
+ , root_watcher(nullptr)
+ , _model(new ModelColumns())
+ , _layer(nullptr)
+ , _is_editing(false)
+ , _page(Gtk::ORIENTATION_VERTICAL)
+ , _color_picker(_("Highlight color"), "", 0, true)
+ , _builder(create_builder("dialog-objects.glade"))
+ , _settings_menu(get_widget<Gtk::Popover>(_builder, "settings-menu"))
+ , _object_menu(get_widget<Gtk::Popover>(_builder, "object-menu"))
+ , _searchBox(get_widget<Gtk::SearchEntry>(_builder, "search"))
+ , _opacity_slider(get_widget<Gtk::Scale>(_builder, "opacity-slider"))
+ , _setting_layers(get_derived_widget<PrefCheckButton, Glib::ustring, bool>(_builder, "setting-layers", "/dialogs/objects/layers_only", false))
+ , _setting_track(get_derived_widget<PrefCheckButton, Glib::ustring, bool>(_builder, "setting-track", "/dialogs/objects/expand_to_layer", true))
+{
+ _store = Gtk::TreeStore::create(*_model);
+ _color_picker.hide();
+
+ //Set up the tree
+ _tree.set_model(_store);
+ _tree.set_headers_visible(false);
+ // Reorderable means that we allow drag-and-drop, but we only allow that
+ // when at least one row is selected
+ _tree.enable_model_drag_dest (Gdk::ACTION_MOVE);
+ _tree.set_name("ObjectsTreeView");
+
+ auto& header = get_widget<Gtk::Box>(_builder, "header");
+ // Search
+ _searchBox.signal_activate().connect(sigc::mem_fun(*this, &ObjectsPanel::_searchActivated));
+ _searchBox.signal_search_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_searchChanged));
+
+ //Label
+ _name_column = Gtk::manage(new Gtk::TreeViewColumn());
+ _text_renderer = Gtk::manage(new Gtk::CellRendererText());
+ _text_renderer->property_editable() = true;
+ _text_renderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END);
+ _text_renderer->signal_editing_started().connect([=](Gtk::CellEditable*,const Glib::ustring&){
+ _is_editing = true;
+ });
+ _text_renderer->signal_editing_canceled().connect([=](){
+ _is_editing = false;
+ });
+ _text_renderer->signal_edited().connect([=](const Glib::ustring&,const Glib::ustring&){
+ _is_editing = false;
+ });
+
+ const int icon_col_width = 24;
+ auto icon_renderer = Gtk::manage(new Inkscape::UI::Widget::CellRendererItemIcon());
+ icon_renderer->property_xpad() = 2;
+ icon_renderer->property_width() = icon_col_width;
+ _tree.append_column(*_name_column);
+ _name_column->set_expand(true);
+ _name_column->pack_start(*icon_renderer, false);
+ _name_column->pack_start(*_text_renderer, true);
+ _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel);
+ _name_column->add_attribute(_text_renderer->property_cell_background_rgba(), _model->_colBgColor);
+ _name_column->add_attribute(icon_renderer->property_shape_type(), _model->_colType);
+ _name_column->add_attribute(icon_renderer->property_color(), _model->_colIconColor);
+ _name_column->add_attribute(icon_renderer->property_clipmask(), _model->_colClipMask);
+ _name_column->add_attribute(icon_renderer->property_cell_background_rgba(), _model->_colBgColor);
+
+ // blend mode and opacity icon(s)
+ _item_state_toggler = Gtk::manage(new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-blend-mode"), INKSCAPE_ICON("object-opaque")));
+ int modeColNum = _tree.append_column("mode", *_item_state_toggler) - 1;
+ if (auto col = _tree.get_column(modeColNum)) {
+ col->add_attribute(_item_state_toggler->property_active(), _model->_colItemStateSet);
+ col->add_attribute(_item_state_toggler->property_active_icon(), _model->_colItemState);
+ col->add_attribute(_item_state_toggler->property_cell_background_rgba(), _model->_colBgColor);
+ col->add_attribute(_item_state_toggler->property_activatable(), _model->_colHover);
+ col->set_fixed_width(icon_col_width);
+ _blend_mode_column = col;
+ }
+
+ _tree.set_has_tooltip();
+ _tree.signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltip){
+ Gtk::TreeModel::iterator iter;
+ if (!_tree.get_tooltip_context_iter(x, y, kbd, iter) || !iter) {
+ return false;
+ }
+ auto blend = (*iter)[_model->_colBlendMode];
+ auto opacity = (*iter)[_model->_colOpacity];
+ auto templt = !pango_version_check(1, 50, 0) ?
+ "<span>%1 %2%%\n</span><span line_height=\"0.5\">\n</span><span>%3\n<i>%4</i></span>" :
+ "<span>%1 %2%%\n</span><span>\n</span><span>%3\n<i>%4</i></span>";
+ auto label = Glib::ustring::compose(templt,
+ _("Opacity:"), Util::format_number(opacity * 100.0, 1),
+ _("Blend mode:"), _blend_mode_names[blend]
+ );
+ tooltip->set_markup(label);
+ _tree.set_tooltip_cell(tooltip, nullptr, _blend_mode_column, _item_state_toggler);
+ return true;
+ });
+
+ _object_menu.set_relative_to(_tree);
+ _object_menu.signal_closed().connect([=](){ _item_state_toggler->set_active(false); _tree.queue_draw(); });
+ auto& modes = get_widget<Gtk::Grid>(_builder, "modes");
+ _opacity_slider.signal_format_value().connect([](double val){
+ return Util::format_number(val, 1) + "%";
+ });
+ const int min = 0, max = 100;
+ for (int i = min; i <= max; i += 50) {
+ _opacity_slider.add_mark(i, Gtk::POS_BOTTOM, "");
+ }
+ _opacity_slider.signal_value_changed().connect([=](){
+ if (current_item) {
+ auto value = _opacity_slider.get_value() / 100.0;
+ Inkscape::CSSOStringStream os;
+ os << CLAMP(value, 0.0, 1.0);
+ auto css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "opacity", os.str().c_str());
+ current_item->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+ DocumentUndo::maybeDone(current_item->document, ":opacity", _("Change opacity"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ });
+
+ // object blend mode and opacity popup
+ int top = 0;
+ int left = 0;
+ int width = 2;
+ for (size_t i = 0; i < Inkscape::SPBlendModeConverter._length; ++i) {
+ auto& data = Inkscape::SPBlendModeConverter.data(i);
+ auto label = _blend_mode_names[data.id] = g_dpgettext2(nullptr, "BlendMode", data.label.c_str());
+ if (Inkscape::SPBlendModeConverter.get_key(data.id) == "-") {
+ if (top >= (Inkscape::SPBlendModeConverter._length + 1) / 2) {
+ ++left;
+ top = 2;
+ } else if (!left) {
+ auto sep = Gtk::make_managed<Gtk::Separator>();
+ sep->show();
+ modes.attach(*sep, left, top, 2, 1);
+ }
+ } else {
+ // Manual correction that indicates this should all be done in glade
+ if (left == 1 && top == 9)
+ top++;
+
+ auto check = Gtk::make_managed<Gtk::ModelButton>();
+ check->set_label(label);
+ check->property_role().set_value(Gtk::BUTTON_ROLE_RADIO);
+ check->property_inverted().set_value(true);
+ check->property_centered().set_value(false);
+ check->set_halign(Gtk::ALIGN_START);
+ check->signal_clicked().connect([=](){
+ // set blending mode
+ if (set_blend_mode(current_item, data.id)) {
+ for (auto btn : _blend_items) {
+ btn.second->property_active().set_value(btn.first == data.id);
+ }
+ DocumentUndo::done(getDocument(), "set-blend-mode", _("Change blend mode"));
+ }
+ });
+ _blend_items[data.id] = check;
+ _blend_mode_names[data.id] = label;
+ check->show();
+ modes.attach(*check, left, top, width, 1);
+ width = 1; // First element takes whole width
+ }
+ top++;
+ }
+
+ // Visible icon
+ auto *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-hidden"), INKSCAPE_ICON("object-visible")));
+ int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1;
+ if (auto eye = _tree.get_column(visibleColNum)) {
+ eye->add_attribute(eyeRenderer->property_active(), _model->_colInvisible);
+ eye->add_attribute(eyeRenderer->property_cell_background_rgba(), _model->_colBgColor);
+ eye->add_attribute(eyeRenderer->property_activatable(), _model->_colHover);
+ eye->add_attribute(eyeRenderer->property_gossamer(), _model->_colAncestorInvisible);
+ eye->set_fixed_width(icon_col_width);
+ _eye_column = eye;
+ }
+
+ // Unlocked icon
+ Inkscape::UI::Widget::ImageToggler * lockRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler(
+ INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")));
+ int lockedColNum = _tree.append_column("lock", *lockRenderer) - 1;
+ if (auto lock = _tree.get_column(lockedColNum)) {
+ lock->add_attribute(lockRenderer->property_active(), _model->_colLocked);
+ lock->add_attribute(lockRenderer->property_cell_background_rgba(), _model->_colBgColor);
+ lock->add_attribute(lockRenderer->property_activatable(), _model->_colHover);
+ lock->add_attribute(lockRenderer->property_gossamer(), _model->_colAncestorLocked);
+ lock->set_fixed_width(icon_col_width);
+ _lock_column = lock;
+ }
+
+ // hierarchy indicator - using item's layer highlight color
+ auto tag_renderer = Gtk::manage(new Inkscape::UI::Widget::ColorTagRenderer());
+ int tag_column = _tree.append_column("tag", *tag_renderer) - 1;
+ if (auto tag = _tree.get_column(tag_column)) {
+ tag->add_attribute(tag_renderer->property_color(), _model->_colIconColor);
+ tag->add_attribute(tag_renderer->property_hover(), _model->_colHoverColor);
+ tag->set_fixed_width(tag_renderer->get_width());
+ _color_tag_column = tag;
+ }
+ tag_renderer->signal_clicked().connect([=](const Glib::ustring& path) {
+ // object's color indicator clicked - open color picker
+ _clicked_item_row = *_store->get_iter(path);
+ if (auto item = getItem(_clicked_item_row)) {
+ // find object's color
+ _color_picker.setRgba32(item->highlight_color());
+ _color_picker.open();
+ }
+ });
+
+ _color_picker.connectChanged([=](guint rgba) {
+ if (auto item = getItem(_clicked_item_row)) {
+ item->setHighlight(rgba);
+ DocumentUndo::maybeDone(getDocument(), "highlight-color", _("Set item highlight color"), INKSCAPE_ICON("dialog-object-properties"));
+ }
+ });
+
+ //Set the expander columns and search columns
+ _tree.set_expander_column(*_name_column);
+ _tree.set_search_column(-1);
+ _tree.set_enable_search(false);
+ _tree.get_selection()->set_mode(Gtk::SELECTION_NONE);
+
+ //Set up tree signals
+ _tree.signal_button_press_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false);
+ _tree.signal_button_release_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false);
+ _tree.signal_key_press_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleKeyPress), false);
+ _tree.signal_key_release_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false);
+ _tree.signal_motion_notify_event().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleMotionEvent), false);
+
+ // Set a status bar text when entering the widget
+ _tree.signal_enter_notify_event().connect([=](GdkEventCrossing*){
+ _msg_id = getDesktop()->messageStack()->push(Inkscape::NORMAL_MESSAGE,
+ _("<b>Hold ALT</b> while hovering over item to highlight, <b>hold SHIFT</b> and click to hide/lock all."));
+ return false;
+ }, false);
+ // watch mouse leave too to clear any state.
+ _tree.signal_leave_notify_event().connect([=](GdkEventCrossing*){
+ getDesktop()->messageStack()->cancel(_msg_id);
+ return _handleMotionEvent(nullptr);
+ }, false);
+
+ // Before expanding a row, replace the dummy child with the actual children
+ _tree.signal_test_expand_row().connect([this](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (cleanDummyChildren(*iter)) {
+ if (getSelection()) {
+ _selectionChanged();
+ }
+ }
+ return false;
+ });
+ _tree.signal_row_expanded().connect([=](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (auto item = getItem(*iter)) {
+ item->setExpanded(true);
+ }
+ });
+ _tree.signal_row_collapsed().connect([=](const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &) {
+ if (auto item = getItem(*iter)) {
+ item->setExpanded(false);
+ }
+ });
+
+ _tree.signal_drag_motion().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_motion), false);
+ _tree.signal_drag_drop().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_drop), false);
+ _tree.signal_drag_begin().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_start), false);
+ _tree.signal_drag_end().connect(sigc::mem_fun(*this, &ObjectsPanel::on_drag_end), false);
+
+ //Set up the label editing signals
+ _text_renderer->signal_edited().connect(sigc::mem_fun(*this, &ObjectsPanel::_handleEdited));
+
+ //Set up the scroller window and pack the page
+ // turn off overlay scrollbars - they block access to the 'lock' icon
+ _scroller.set_overlay_scrolling(false);
+ _scroller.add(_tree);
+ _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC );
+ _scroller.set_shadow_type(Gtk::SHADOW_IN);
+ Gtk::Requisition sreq;
+ Gtk::Requisition sreq_natural;
+ _scroller.get_preferred_size(sreq_natural, sreq);
+ int minHeight = 70;
+ if (sreq.height < minHeight) {
+ // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar
+ _scroller.set_size_request(sreq.width, minHeight);
+ }
+
+ _page.pack_start(header, false, true);
+ _page.pack_end(_scroller, Gtk::PACK_EXPAND_WIDGET);
+ pack_start(_page, Gtk::PACK_EXPAND_WIDGET);
+
+ selection_color = get_background_color(_tree.get_style_context(), Gtk::STATE_FLAG_SELECTED);
+ _tree_style = _tree.signal_style_updated().connect([=](){
+ selection_color = get_background_color(_tree.get_style_context(), Gtk::STATE_FLAG_SELECTED);
+
+ if (!root_watcher) return;
+ for (auto&& kv : root_watcher->child_watchers) {
+ if (kv.second) {
+ kv.second->updateRowHighlight();
+ }
+ }
+ });
+ // Clear and update entire tree (do not use this in changed/modified signals)
+ auto prefs = Inkscape::Preferences::get();
+ _watch_object_mode = prefs->createObserver("/dialogs/objects/layers_only", [=]() { setRootWatcher(); });
+
+ update();
+ show_all_children();
+}
+
+/**
+ * Destructor
+ */
+ObjectsPanel::~ObjectsPanel()
+{
+ if (root_watcher) {
+ delete root_watcher;
+ }
+ root_watcher = nullptr;
+
+ if (_model) {
+ delete _model;
+ _model = nullptr;
+ }
+}
+
+void ObjectsPanel::desktopReplaced()
+{
+ layer_changed.disconnect();
+
+ if (auto desktop = getDesktop()) {
+ layer_changed = desktop->layerManager().connectCurrentLayerChanged(sigc::mem_fun(*this, &ObjectsPanel::layerChanged));
+ }
+}
+
+void ObjectsPanel::documentReplaced()
+{
+ setRootWatcher();
+}
+
+void ObjectsPanel::setRootWatcher()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ if (root_watcher) {
+ delete root_watcher;
+ }
+ root_watcher = nullptr;
+
+ if (auto document = getDocument()) {
+ bool filtered = prefs->getBool("/dialogs/objects/layers_only", false) || _searchBox.get_text_length();
+
+ // A filtered object watcher behaves differently to an unfiltered one.
+ // Filtering disables creating dummy children and instead processes entire trees.
+ root_watcher = new ObjectWatcher(this, document->getRoot(), nullptr, filtered);
+ root_watcher->rememberExtendedItems();
+ layerChanged(getDesktop()->layerManager().currentLayer());
+ _selectionChanged();
+ }
+}
+
+/**
+ * Apply any ongoing filters to the items.
+ */
+bool ObjectsPanel::showChildInTree(SPItem *item) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ bool show_child = true;
+
+ // Filter by object type, the layers dialog here.
+ if (prefs->getBool("/dialogs/objects/layers_only", false)) {
+ auto group = cast<SPGroup>(item);
+ if (!group || group->layerMode() != SPGroup::LAYER) {
+ show_child = false;
+ }
+ }
+
+ // Filter by text search, if the search text box has any contents
+ auto term = _searchBox.get_text().lowercase();
+ if (show_child && term.length()) {
+ // A source document allows search for different pieces of metadata
+ std::stringstream source;
+ source << "#" << item->getId();
+ if (auto label = item->label())
+ source << " " << label;
+ source << " @" << item->getTagName();
+ // Might want to add class names here as ".class"
+
+ auto doc = source.str();
+ transform(doc.begin(), doc.end(), doc.begin(), ::tolower);
+ show_child = doc.find(term) != std::string::npos;
+ }
+
+ // Now the terrible bit, searching all the children causing a
+ // duplication of work as it must re-scan up the tree multiple times
+ // when the tree is very deep.
+ for (auto child_obj : item->childList(false)) {
+ if (show_child)
+ break;
+ if (auto child = cast<SPItem>(child_obj)) {
+ show_child = showChildInTree(child);
+ }
+ }
+
+ return show_child;
+}
+
+/**
+ * This both unpacks the tree, and populates lazy loading
+ */
+ObjectWatcher *ObjectsPanel::unpackToObject(SPObject *item)
+{
+ ObjectWatcher *watcher = nullptr;
+ for (auto &parent : item->ancestorList(true)) {
+ if (parent->getRepr() == root_watcher->getRepr()) {
+ watcher = root_watcher;
+ } else if (watcher) {
+ if ((watcher = watcher->findChild(parent->getRepr()))) {
+ if (auto row = watcher->getRow()) {
+ cleanDummyChildren(*row);
+ }
+ }
+ }
+ }
+ return watcher;
+}
+
+// Same definition as in 'document.cpp'
+#define SP_DOCUMENT_UPDATE_PRIORITY (G_PRIORITY_HIGH_IDLE - 2)
+
+void ObjectsPanel::selectionChanged(Selection *selected /* not used */)
+{
+ if (!_idle_connection.connected()) {
+ auto handler = sigc::mem_fun(*this, &ObjectsPanel::_selectionChanged);
+ int priority = SP_DOCUMENT_UPDATE_PRIORITY + 1;
+ _idle_connection = Glib::signal_idle().connect(handler, priority);
+ }
+}
+
+bool ObjectsPanel::_selectionChanged()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ root_watcher->setSelectedBitRecursive(SELECTED_OBJECT, false);
+ bool keep_current_item = false;
+
+ for (auto item : getSelection()->items()) {
+ keep_current_item |= (item == current_item);
+ // Failing to find the watchers here means the object is filtered out
+ // of the current object view and can be safely skipped.
+ if (auto watcher = unpackToObject(item)) {
+ if (auto child_watcher = watcher->findChild(item->getRepr())) {
+ // Expand layers themselves, but do not expand groups.
+ auto group = cast<SPGroup>(item);
+ auto focus_watcher = (group && group->isLayer()) ? child_watcher : watcher;
+ child_watcher->setSelectedBit(SELECTED_OBJECT, true);
+
+ if (prefs->getBool("/dialogs/objects/expand_to_layer", true)) {
+ _tree.expand_to_path(focus_watcher->getTreePath());
+ if (!_scroll_lock) {
+ _tree.scroll_to_row(child_watcher->getTreePath(), 0.5);
+ }
+ }
+ }
+ }
+ }
+ if (!keep_current_item) {
+ current_item = nullptr;
+ }
+ _scroll_lock = false;
+
+ // Returning 'false' disconnects idle signal handler
+ return false;
+}
+
+/**
+ * Happens when the layer selected is changed.
+ *
+ * @param layer - The layer now selected
+ */
+void ObjectsPanel::layerChanged(SPObject *layer)
+{
+ root_watcher->setSelectedBitRecursive(LAYER_FOCUS_CHILD | LAYER_FOCUSED, false);
+
+ if (!layer || !layer->getRepr()) return;
+ auto watcher = getWatcher(layer->getRepr());
+ if (watcher && watcher != root_watcher) {
+ watcher->setSelectedBitChildren(LAYER_FOCUS_CHILD, true);
+ watcher->setSelectedBit(LAYER_FOCUSED, true);
+ }
+ _layer = layer;
+}
+
+
+/**
+ * Stylizes a button using the given icon name and tooltip
+ */
+Gtk::Button* ObjectsPanel::_addBarButton(char const* iconName, char const* tooltip, char const *action_name)
+{
+ Gtk::Button* btn = Gtk::manage(new Gtk::Button());
+ auto child = Glib::wrap(sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR));
+ child->show();
+ btn->add(*child);
+ btn->set_relief(Gtk::RELIEF_NONE);
+ btn->set_tooltip_text(tooltip);
+ // Must use C API until GTK4.
+ gtk_actionable_set_action_name(GTK_ACTIONABLE(btn->gobj()), action_name);
+ return btn;
+}
+
+/**
+ * Sets visibility of items in the tree
+ * @param iter Current item in the tree
+ */
+bool ObjectsPanel::toggleVisible(unsigned int state, Gtk::TreeModel::Row row)
+{
+ auto desktop = getDesktop();
+ auto selection = getSelection();
+
+ if (SPItem* item = getItem(row)) {
+ if (state & GDK_SHIFT_MASK) {
+ // Toggle Visible for layers (hide all other layers)
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLayerSolo(item);
+ DocumentUndo::done(getDocument(), _("Hide other layers"), "");
+ }
+ return true;
+ }
+ bool visible = !row[_model->_colInvisible];
+ if (state & GDK_CONTROL_MASK || !selection->includes(item)) {
+ item->setHidden(visible);
+ } else {
+ for (auto sitem : selection->items()) {
+ sitem->setHidden(visible);
+ }
+ }
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
+ return visible;
+ }
+ return false;
+}
+
+// show blend mode popup menu for current item
+bool ObjectsPanel::blendModePopup(GdkEventButton* event, Gtk::TreeModel::Row row) {
+ if (SPItem* item = getItem(row)) {
+ current_item = nullptr;
+ auto blend = SP_CSS_BLEND_NORMAL;
+ if (item->style && item->style->mix_blend_mode.set) {
+ blend = item->style->mix_blend_mode.value;
+ }
+ auto opacity = 1.0;
+ if (item->style && item->style->opacity.set) {
+ opacity = SP_SCALE24_TO_FLOAT(item->style->opacity.value);
+ }
+ for (auto btn : _blend_items) {
+ btn.second->property_active().set_value(btn.first == blend);
+ }
+ _opacity_slider.set_value(opacity * 100);
+ current_item = item;
+
+ Gdk::Rectangle rect(event->x, event->y, 1, 1);
+ _object_menu.set_pointing_to(rect);
+ _item_state_toggler->set_active();
+ _object_menu.popup();
+ }
+
+ return true;
+}
+
+/**
+ * Sets sensitivity of items in the tree
+ * @param iter Current item in the tree
+ * @param locked Whether the item should be locked
+ */
+bool ObjectsPanel::toggleLocked(unsigned int state, Gtk::TreeModel::Row row)
+{
+ auto desktop = getDesktop();
+ auto selection = getSelection();
+
+ if (SPItem* item = getItem(row)) {
+ if (state & GDK_SHIFT_MASK) {
+ // Toggle lock for layers (lock all other layers)
+ if (desktop->layerManager().isLayer(item)) {
+ desktop->layerManager().toggleLockOtherLayers(item);
+ DocumentUndo::done(getDocument(), _("Lock other layers"), "");
+ }
+ return true;
+ }
+ bool locked = !row[_model->_colLocked];
+ if (state & GDK_CONTROL_MASK || !selection->includes(item)) {
+ item->setLocked(locked);
+ } else {
+ for (auto sitem : selection->items()) {
+ sitem->setLocked(locked);
+ }
+ }
+ // Use maybeDone so user can flip back and forth without making loads of undo items
+ DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
+ return locked;
+ }
+ return false;
+}
+
+bool ObjectsPanel::_handleKeyPress(GdkEventKey *event)
+{
+ auto desktop = getDesktop();
+ if (!desktop)
+ return false;
+
+ // This isn't needed in Gtk4, use expand_collapse_cursor_row instead.
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn *column;
+ _tree.get_cursor(path, column);
+
+ auto selection = getSelection();
+ bool shift = event->state & GDK_SHIFT_MASK;
+ Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event(event);
+ switch (shortcut.get_key()) {
+ case GDK_KEY_Escape:
+ if (desktop->canvas) {
+ desktop->canvas->grab_focus();
+ return true;
+ }
+ break;
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (path && shift) {
+ _tree.collapse_row(path);
+ return true;
+ }
+ break;
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (path && shift) {
+ _tree.expand_row(path, false);
+ return true;
+ }
+ break;
+ case GDK_KEY_space:
+ selectCursorItem(event->state);
+ return true;
+ // Depending on the action to cover this causes it's special
+ // text and node handling to block deletion of objects. DIY
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete:
+ case GDK_KEY_BackSpace:
+ getSelection()->deleteItems();
+ // NOTE: We could select a sibling object here to make deleting many objects easier.
+ return true;
+ case GDK_KEY_Page_Up:
+ case GDK_KEY_KP_Page_Up:
+ if (shift) {
+ selection->raiseToTop();
+ return true;
+ }
+ break;
+ case GDK_KEY_Page_Down:
+ case GDK_KEY_KP_Page_Down:
+ if (shift) {
+ selection->lowerToBottom();
+ return true;
+ }
+ break;
+ case GDK_KEY_Up:
+ case GDK_KEY_KP_Up:
+ if (shift) {
+ selection->stackUp();
+ return true;
+ }
+ break;
+ case GDK_KEY_Down:
+ case GDK_KEY_KP_Down:
+ if (shift) {
+ selection->stackDown();
+ return true;
+ }
+ break;
+
+ }
+ return _handleKeyEvent(event);
+}
+
+/**
+ * Handles keyboard events
+ * @param event Keyboard event passed in from GDK
+ * @return Whether the event should be eaten (om nom nom)
+ */
+bool ObjectsPanel::_handleKeyEvent(GdkEventKey *event)
+{
+ auto desktop = getDesktop();
+ if (!desktop)
+ return false;
+
+ bool press = event->type == GDK_KEY_PRESS;
+ Gtk::AccelKey shortcut = Inkscape::Shortcuts::get_from_event(event);
+ switch (shortcut.get_key()) {
+ // space and return enter label editing mode; leave them for the tree to handle
+ case GDK_KEY_space:
+ case GDK_KEY_Return:
+ return false;
+ case GDK_KEY_Alt_L:
+ case GDK_KEY_Alt_R:
+ _handleTransparentHover(press);
+ return false;
+ }
+ return false;
+}
+
+/**
+ * Handles mouse movements
+ * @param event Motion event passed in from GDK
+ * @returns Whether the event should be eaten.
+ */
+bool ObjectsPanel::_handleMotionEvent(GdkEventMotion* motion_event)
+{
+ if (_is_editing) return false;
+
+ // Unhover any existing hovered row.
+ if (_hovered_row_ref) {
+ if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) {
+ row[_model->_colHover] = false;
+ row[_model->_colHoverColor] = false;
+ }
+ }
+ // Allow this function to be called by LEAVE motion
+ if (!motion_event) {
+ _hovered_row_ref = Gtk::TreeModel::RowReference();
+ _handleTransparentHover(false);
+ return false;
+ }
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn* col = nullptr;
+ int x, y;
+ if (_tree.get_path_at_pos((int)motion_event->x, (int)motion_event->y, path, col, x, y)) {
+ // Only allow drag and drop from the name column, not any others
+ if (col == _name_column) {
+ _drag_column = nullptr;
+ }
+ // Only allow drag and drop when not filtering. Otherwise bad things happen
+ _tree.set_reorderable(col == _name_column);
+ if (auto row = *_store->get_iter(path)) {
+ row[_model->_colHover] = true;
+ _hovered_row_ref = Gtk::TreeModel::RowReference(_store, path);
+ _tree.set_cursor(path);
+
+ if (col == _color_tag_column) {
+ row[_model->_colHoverColor] = true;
+ }
+
+ // Dragging over the eye or locks will set them all
+ auto item = getItem(row);
+ if (item && _drag_column && col == _drag_column) {
+ if (col == _eye_column) {
+ // Defer visibility to th idle thread (it's expensive)
+ Glib::signal_idle().connect_once([=]() {
+ item->setHidden(_drag_flip);
+ DocumentUndo::maybeDone(getDocument(), "toggle-vis", _("Toggle item visibility"), "");
+ }, Glib::PRIORITY_DEFAULT_IDLE);
+ } else if (col == _lock_column) {
+ item->setLocked(_drag_flip);
+ DocumentUndo::maybeDone(getDocument(), "toggle-lock", _("Toggle item locking"), "");
+ }
+ }
+ }
+ }
+
+ _handleTransparentHover(motion_event->state & GDK_MOD1_MASK);
+ return false;
+}
+
+void ObjectsPanel::_handleTransparentHover(bool enabled)
+{
+ SPItem *item = nullptr;
+ if (enabled && _hovered_row_ref) {
+ if (auto row = *_store->get_iter(_hovered_row_ref.get_path())) {
+ item = getItem(row);
+ }
+ }
+
+ if (item == _solid_item)
+ return;
+
+ // Set the target item, this prevents rerunning too.
+ _solid_item = item;
+ auto desktop = getDesktop();
+
+ // Reset all the items in the list.
+ for (auto &item : _translucent_items) {
+ Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(item->style->opacity.value));
+ }
+ _translucent_items.clear();
+
+ if (item) {
+ _generateTranslucentItems(getDocument()->getRoot());
+
+ for (auto &item : _translucent_items) {
+ Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey);
+ arenaitem->setOpacity(0.2);
+ }
+ }
+}
+
+/**
+ * Generate a new list of sibling items (recursive)
+ */
+void ObjectsPanel::_generateTranslucentItems(SPItem *parent)
+{
+ if (parent == _solid_item)
+ return;
+ if (parent->isAncestorOf(_solid_item)) {
+ for (auto &child: parent->children) {
+ if (auto item = cast<SPItem>(&child)) {
+ _generateTranslucentItems(item);
+ }
+ }
+ } else {
+ _translucent_items.push_back(parent);
+ }
+}
+
+/**
+ * Handles mouse up events
+ * @param event Mouse event from GDK
+ * @return whether to eat the event (om nom nom)
+ */
+bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event)
+{
+ auto selection = getSelection();
+ if (!selection)
+ return false;
+
+ if (event->type == GDK_BUTTON_RELEASE) {
+ _drag_column = nullptr;
+ }
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn* col = nullptr;
+ int x, y;
+ if (_tree.get_path_at_pos((int)event->x, (int)event->y, path, col, x, y)) {
+ if (auto row = *_store->get_iter(path)) {
+ if (event->type == GDK_BUTTON_PRESS) {
+ // Remember column for dragging feature
+ _drag_column = col;
+ if (col == _eye_column) {
+ _drag_flip = toggleVisible(event->state, row);
+ } else if (col == _lock_column) {
+ _drag_flip = toggleLocked(event->state, row);
+ }
+ else if (col == _blend_mode_column) {
+ return blendModePopup(event, row);
+ }
+ }
+ }
+
+ // Gtk lacks the ability to detect if the user is clicking on the
+ // expander icon. So we must detect it using the cell_area check.
+ Gdk::Rectangle r;
+ _tree.get_cell_area(path, *_name_column, r);
+ bool is_expander = x < r.get_x();
+
+ if (col != _name_column || is_expander)
+ return false;
+
+ // This doesn't work, it might be being eaten.
+ if (event->type == GDK_2BUTTON_PRESS) {
+ _tree.set_cursor(path, *col, true);
+ _is_editing = true;
+ return true;
+ }
+ _is_editing = _is_editing && event->type == GDK_BUTTON_RELEASE;
+ auto row = *_store->get_iter(path);
+ if (!row) return false;
+ SPItem *item = getItem(row);
+
+ if (!item) return false;
+ auto group = cast<SPGroup>(item);
+
+ // Load the right click menu
+ const bool context_menu = event->type == GDK_BUTTON_PRESS && event->button == 3;
+
+ // Select items on button release to not confuse drag (unless it's a right-click)
+ // Right-click selects too to set up the stage for context menu which frequently relies on current selection!
+ if (!_is_editing && (event->type == GDK_BUTTON_RELEASE || context_menu)) {
+ if (context_menu) {
+ // if right-clicking on a layer, make it current for context menu actions to work correctly
+ if (group && group->layerMode() == SPGroup::LAYER && getDesktop()->layerManager().currentLayer() != item) {
+ getDesktop()->layerManager().setCurrentLayer(item, true);
+ }
+ ContextMenu *menu = new ContextMenu(getDesktop(), item, true); // true == hide menu item for opening this dialog!
+ menu->attach_to_widget(*this); // So actions work!
+ menu->show();
+ menu->popup_at_pointer(nullptr);
+ } else {
+ selectCursorItem(event->state);
+ }
+ return true;
+ } else {
+ // Remember the item for we are about to drag it!
+ current_item = item;
+ }
+ }
+ return false;
+}
+
+/**
+ * Handle a successful item label edit
+ * @param path Tree path of the item currently being edited
+ * @param new_text New label text
+ */
+void ObjectsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text)
+{
+ _is_editing = false;
+ if (auto row = *_store->get_iter(path)) {
+ if (auto item = getItem(row)) {
+ if (!new_text.empty() && (!item->label() || new_text != item->label())) {
+ item->setLabel(new_text.c_str());
+ DocumentUndo::done(getDocument(), _("Rename object"), "");
+ }
+ }
+ }
+}
+
+/**
+ * Take over the select row functionality from the TreeView, this is because
+ * we have two selections (layer and object selection) and require a custom
+ * method of rendering the result to the treeview.
+ */
+bool ObjectsPanel::select_row( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const &path, bool /*sel*/ )
+{
+ return true;
+}
+
+/**
+ * Get the XML node which is associated with a row. Can be NULL for dummy children.
+ */
+Node *ObjectsPanel::getRepr(Gtk::TreeModel::Row const &row) const
+{
+ return row[_model->_colNode];
+}
+
+/**
+ * Get the item which is associated with a row. If getRepr(row) is not NULL,
+ * then this call is expected to also not be NULL.
+ */
+SPItem *ObjectsPanel::getItem(Gtk::TreeModel::Row const &row) const
+{
+ auto const this_const = const_cast<ObjectsPanel *>(this);
+ return cast<SPItem>(this_const->getObject(getRepr(row)));
+}
+
+/**
+ * Return true if this row has dummy children.
+ */
+bool ObjectsPanel::hasDummyChildren(Gtk::TreeModel::Row const &row) const
+{
+ for (auto &c : row.children()) {
+ if (isDummy(c)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * If the given row has dummy children, remove them.
+ * @pre Eiter all, or no children are dummies
+ * @post If the function returns true, the row has no children
+ * @return False if there are children and they are not dummies
+ */
+bool ObjectsPanel::removeDummyChildren(Gtk::TreeModel::Row const &row)
+{
+ auto &children = row.children();
+ if (!children.empty()) {
+ Gtk::TreeStore::iterator child = children[0];
+ if (!isDummy(*child)) {
+ assert(!hasDummyChildren(row));
+ return false;
+ }
+
+ do {
+ assert(child->parent() == row);
+ assert(isDummy(*child));
+ child = _store->erase(child);
+ } while (child && child->parent() == row);
+ }
+ return true;
+}
+
+bool ObjectsPanel::cleanDummyChildren(Gtk::TreeModel::Row const &row)
+{
+ if (removeDummyChildren(row)) {
+ assert(row);
+ if (auto watcher = getWatcher(getRepr(row))) {
+ watcher->addChildren(getItem(row));
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Signal handler for "drag-motion"
+ *
+ * Refuses drops into non-group items.
+ */
+bool ObjectsPanel::on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+
+ auto selection = getSelection();
+ auto document = getDocument();
+
+ if (!selection || !document)
+ goto finally;
+
+ _tree.get_dest_row_at_pos(x, y, path, pos);
+
+ if (path) {
+ auto item = getItem(*_store->get_iter(path));
+
+ // don't drop on self
+ if (selection->includes(item)) {
+ goto finally;
+ }
+
+ context->drag_status(Gdk::ACTION_MOVE, time);
+ return false;
+ }
+
+finally:
+ // remove drop highlight
+ _tree.unset_drag_dest_row();
+ context->drag_refuse(time);
+ return true;
+}
+
+/**
+ * Signal handler for "drag-drop".
+ *
+ * Do the actual work of drag-and-drop.
+ */
+bool ObjectsPanel::on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, guint time)
+{
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewDropPosition pos;
+ _tree.get_dest_row_at_pos(x, y, path, pos);
+
+ if (!path) {
+ return true;
+ }
+
+ auto drop_repr = getRepr(*_store->get_iter(path));
+ bool const drop_into = pos != Gtk::TREE_VIEW_DROP_BEFORE && //
+ pos != Gtk::TREE_VIEW_DROP_AFTER;
+
+ auto selection = getSelection();
+ auto document = getDocument();
+ if (selection && document) {
+ auto item = document->getObjectByRepr(drop_repr);
+ // We always try to drop the item, even if we end up dropping it after the non-group item
+ if (drop_into && is<SPGroup>(item)) {
+ selection->toLayer(item);
+ } else {
+ Node *after = (pos == Gtk::TREE_VIEW_DROP_BEFORE) ? drop_repr : drop_repr->prev();
+ selection->toLayer(item->parent, after);
+ }
+ DocumentUndo::done(document, _("Move items"), INKSCAPE_ICON("selection-move-to-layer"));
+ }
+
+ on_drag_end(context);
+ return true;
+}
+
+void ObjectsPanel::on_drag_start(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ _scroll_lock = true;
+
+ auto selection = _tree.get_selection();
+ selection->set_mode(Gtk::SELECTION_MULTIPLE);
+ selection->unselect_all();
+
+ auto obj_selection = getSelection();
+ if (!obj_selection)
+ return;
+
+ if (current_item && !obj_selection->includes(current_item)) {
+ // This means the item the user started to drag is not one that is selected
+ // So we'll deselect everything and start dragging this item instead.
+ auto watcher = getWatcher(current_item->getRepr());
+ if (watcher) {
+ auto path = watcher->getTreePath();
+ selection->select(path);
+ obj_selection->set(current_item);
+ }
+ } else {
+ // Drag all the items currently selected (multi-row)
+ for (auto item : obj_selection->items()) {
+ auto watcher = getWatcher(item->getRepr());
+ if (watcher) {
+ auto path = watcher->getTreePath();
+ selection->select(path);
+ }
+ }
+ }
+}
+
+void ObjectsPanel::on_drag_end(const Glib::RefPtr<Gdk::DragContext> &context)
+{
+ auto selection = _tree.get_selection();
+ selection->unselect_all();
+ selection->set_mode(Gtk::SELECTION_NONE);
+ current_item = nullptr;
+}
+
+/**
+ * Select the object currently under the list-cursor (keyboard or mouse)
+ */
+bool ObjectsPanel::selectCursorItem(unsigned int state)
+{
+ auto &layers = getDesktop()->layerManager();
+ auto selection = getSelection();
+ if (!selection)
+ return false;
+
+ Gtk::TreeModel::Path path;
+ Gtk::TreeViewColumn *column;
+ _tree.get_cursor(path, column);
+ if (!path || !column)
+ return false;
+
+ auto row = *_store->get_iter(path);
+ if (!row)
+ return false;
+
+ if (column == _eye_column) {
+ toggleVisible(state, row);
+ } else if (column == _lock_column) {
+ toggleLocked(state, row);
+ } else if (column == _name_column) {
+ auto item = getItem(row);
+ auto group = cast<SPGroup>(item);
+ _scroll_lock = true; // Clicking to select shouldn't scroll the treeview.
+ if (state & GDK_SHIFT_MASK && !selection->isEmpty()) {
+ // Select everything between this row and the last selected item
+ selection->setBetween(item);
+ } else if (state & GDK_CONTROL_MASK) {
+ selection->toggle(item);
+ } else if (group && selection->includes(item) && !group->isLayer()) {
+ // Clicking off a group (second click) will enter the group
+ layers.setCurrentLayer(item, true);
+ } else {
+ if (layers.currentLayer() == item) {
+ layers.setCurrentLayer(item->parent);
+ }
+ selection->set(item);
+ }
+ return true;
+ }
+ return false;
+}
+
+
+/**
+ * User pressed return in search box, process search query.
+ */
+void ObjectsPanel::_searchActivated()
+{
+ // The root watcher and watcher tree handles the search operations
+ setRootWatcher();
+}
+
+/**
+ * User has typed more into the search box
+ */
+void ObjectsPanel::_searchChanged()
+{
+ if (root_watcher->isFiltered() && !_searchBox.get_text_length()) {
+ _searchActivated();
+ }
+}
+
+} //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/dialog/objects.h b/src/ui/dialog/objects.h
new file mode 100644
index 0000000..5a19a53
--- /dev/null
+++ b/src/ui/dialog/objects.h
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple dialog for objects UI.
+ *
+ * Authors:
+ * Theodore Janeczko
+ * Tavmjong Bah
+ *
+ * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com>
+ * Tavmjong Bah 2017
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_OBJECTS_PANEL_H
+#define SEEN_OBJECTS_PANEL_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/modelbutton.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scale.h>
+
+#include "helper/auto-connection.h"
+#include "xml/node-observer.h"
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/color-picker.h"
+#include "ui/widget/preferences-widget.h"
+
+#include "selection.h"
+#include "style-enums.h"
+#include "color-rgba.h"
+
+using Inkscape::XML::Node;
+using namespace Inkscape::UI::Widget;
+
+class SPObject;
+class SPGroup;
+// struct SPColorSelector;
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget { class ImageToggler; }
+namespace Dialog {
+
+class ObjectsPanel;
+class ObjectWatcher;
+
+enum {COL_LABEL, COL_VISIBLE, COL_LOCKED};
+
+using SelectionState = int;
+enum SelectionStates : SelectionState {
+ SELECTED_NOT = 0, // Object is NOT in desktop's selection
+ SELECTED_OBJECT = 1, // Object is in the desktop's selection
+ LAYER_FOCUSED = 2, // This layer is the desktop's focused layer
+ LAYER_FOCUS_CHILD = 4 // This object is a child of the focused layer
+};
+
+/**
+ * A panel that displays objects.
+ */
+class ObjectsPanel : public DialogBase
+{
+public:
+ ObjectsPanel();
+ ~ObjectsPanel() override;
+
+ class ModelColumns;
+
+protected:
+ void desktopReplaced() override;
+ void documentReplaced() override;
+ void layerChanged(SPObject *obj);
+ void selectionChanged(Selection *selected) override;
+ ObjectWatcher *unpackToObject(SPObject *item);
+
+ // Accessed by ObjectWatcher directly (friend class)
+ SPObject* getObject(Node *node);
+ ObjectWatcher* getWatcher(Node *node);
+ ObjectWatcher *getRootWatcher() const { return root_watcher; };
+ bool showChildInTree(SPItem *item);
+
+ Node *getRepr(Gtk::TreeModel::Row const &row) const;
+ SPItem *getItem(Gtk::TreeModel::Row const &row) const;
+ std::optional<Gtk::TreeRow> getRow(SPItem *item) const;
+
+ bool isDummy(Gtk::TreeModel::Row const &row) const { return getRepr(row) == nullptr; }
+ bool hasDummyChildren(Gtk::TreeModel::Row const &row) const;
+ bool removeDummyChildren(Gtk::TreeModel::Row const &row);
+ bool cleanDummyChildren(Gtk::TreeModel::Row const &row);
+
+ Glib::RefPtr<Gtk::TreeStore> _store;
+ ModelColumns* _model;
+
+ void setRootWatcher();
+private:
+
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Inkscape::PrefObserver _watch_object_mode;
+ ObjectWatcher* root_watcher;
+ SPItem *current_item = nullptr;
+
+ Inkscape::auto_connection layer_changed;
+ SPObject *_layer;
+ Gtk::TreeModel::RowReference _hovered_row_ref;
+
+ //Show icons in the context menu
+ bool _show_contextmenu_icons;
+ bool _is_editing;
+ bool _scroll_lock = false;
+
+ std::vector<Gtk::Widget*> _watching;
+ std::vector<Gtk::Widget*> _watchingNonTop;
+ std::vector<Gtk::Widget*> _watchingNonBottom;
+
+ Gtk::TreeView _tree;
+ Gtk::CellRendererText *_text_renderer;
+ Gtk::TreeView::Column *_name_column;
+ Gtk::TreeView::Column *_blend_mode_column = nullptr;
+ Gtk::TreeView::Column *_eye_column = nullptr;
+ Gtk::TreeView::Column *_lock_column = nullptr;
+ Gtk::TreeView::Column *_color_tag_column = nullptr;
+ Gtk::Box _buttonsRow;
+ Gtk::Box _buttonsPrimary;
+ Gtk::Box _buttonsSecondary;
+ Gtk::SearchEntry& _searchBox;
+ Gtk::ScrolledWindow _scroller;
+ Gtk::Menu _popupMenu;
+ Gtk::Box _page;
+ Inkscape::auto_connection _tree_style;
+ Inkscape::UI::Widget::ColorPicker _color_picker;
+ Gtk::TreeRow _clicked_item_row;
+
+ Gtk::Button *_addBarButton(char const* iconName, char const* tooltip, char const *action_name);
+
+ bool blendModePopup(GdkEventButton* event, Gtk::TreeModel::Row row);
+ bool toggleVisible(unsigned int state, Gtk::TreeModel::Row row);
+ bool toggleLocked(unsigned int state, Gtk::TreeModel::Row row);
+
+ bool _handleButtonEvent(GdkEventButton *event);
+ bool _handleKeyPress(GdkEventKey *event);
+ bool _handleKeyEvent(GdkEventKey *event);
+ bool _handleMotionEvent(GdkEventMotion* motion_event);
+ void _searchActivated();
+ void _searchChanged();
+
+ void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text);
+ void _handleTransparentHover(bool enabled);
+ void _generateTranslucentItems(SPItem *parent);
+
+ bool select_row( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b );
+
+ bool on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override;
+ bool on_drag_drop(const Glib::RefPtr<Gdk::DragContext> &, int, int, guint) override;
+ void on_drag_start(const Glib::RefPtr<Gdk::DragContext> &);
+ void on_drag_end(const Glib::RefPtr<Gdk::DragContext> &) override;
+
+ bool selectCursorItem(unsigned int state);
+ SPItem *_getCursorItem(Gtk::TreeViewColumn *column);
+
+ friend class ObjectWatcher;
+
+ SPItem *_solid_item;
+ std::list<SPItem *> _translucent_items;
+ int _msg_id;
+ Gtk::Popover& _settings_menu;
+ Gtk::Popover& _object_menu;
+ Gtk::Scale& _opacity_slider;
+ std::map<SPBlendMode, Gtk::ModelButton*> _blend_items;
+ std::map<SPBlendMode, Glib::ustring> _blend_mode_names;
+ Inkscape::UI::Widget::ImageToggler* _item_state_toggler;
+ // Special column dragging mode
+ Gtk::TreeViewColumn* _drag_column = nullptr;
+ PrefCheckButton& _setting_layers;
+ PrefCheckButton& _setting_track;
+ bool _drag_flip;
+
+ bool _selectionChanged();
+ auto_connection _idle_connection;
+};
+
+
+
+} //namespace Dialogs
+} //namespace UI
+} //namespace Inkscape
+
+
+
+#endif // SEEN_OBJECTS_PANEL_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/dialog/paint-servers.cpp b/src/ui/dialog/paint-servers.cpp
new file mode 100644
index 0000000..42f437c
--- /dev/null
+++ b/src/ui/dialog/paint-servers.cpp
@@ -0,0 +1,657 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Paint Servers dialog
+ */
+/* Authors:
+ * Valentin Ionita
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * Copyright (C) 2019 Valentin Ionita
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <algorithm>
+#include <map>
+
+#include <giomm/listmodel.h>
+#include <glibmm/regex.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/stockid.h>
+#include <gtkmm/switch.h>
+
+#include "document.h"
+#include "inkscape.h"
+#include "paint-servers.h"
+#include "path-prefix.h"
+#include "style.h"
+
+#include "io/resource.h"
+#include "object/sp-defs.h"
+#include "object/sp-hatch.h"
+#include "object/sp-pattern.h"
+#include "object/sp-root.h"
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/widget/scrollprotected.h"
+#include "xml/href-attribute-helper.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+static Glib::ustring const wrapper = R"=====(
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <defs id="Defs"/>
+ <rect id="Back" x="0" y="0" width="100px" height="100px" fill="lightgray"/>
+ <rect id="Rect" x="0" y="0" width="100px" height="100px" stroke="black"/>
+</svg>
+)=====";
+
+const char *ALLDOCS = N_("All paint servers");
+const char *CURRENTDOC = N_("Current document");
+
+PaintServersDialog::PaintServersDialog()
+ : DialogBase("/dialogs/paint", "PaintServers")
+ , _targetting_fill(true)
+ , columns()
+{
+ current_store = ALLDOCS;
+ store[ALLDOCS] = Gtk::ListStore::create(columns);
+
+ // Get wrapper document (rectangle to fill with paint server).
+ preview_document = SPDocument::createNewDocFromMem(wrapper.c_str(), wrapper.length(), true);
+ SPObject *rect = preview_document->getObjectById("Rect");
+ SPObject *defs = preview_document->getObjectById("Defs");
+ if (!rect || !defs) {
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__,
+ "Failed to get wrapper defs or rectangle for preview document!");
+ }
+ unsigned key = SPItem::display_key_new(1);
+ preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ preview_document->ensureUpToDate();
+ renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY));
+
+ _buildDialogWindow("dialog-paint-servers.glade");
+ _loadStockPaints();
+}
+
+
+PaintServersDialog::~PaintServersDialog()
+{
+ _defs_changed.disconnect();
+ _document_closed.disconnect();
+}
+
+/** Handles the replacement of the document that we edit */
+void PaintServersDialog::documentReplaced()
+{
+ _defs_changed.disconnect();
+ _document_closed.disconnect();
+
+ auto document = getDocument();
+ if (!document) {
+ return;
+ }
+ document_map[CURRENTDOC] = document;
+ _loadFromCurrentDocument();
+ _regenerateAll();
+
+ if (auto const defs = document->getDefs()) {
+ _defs_changed = defs->connectModified([=](SPObject *, unsigned) -> void {
+ _loadFromCurrentDocument();
+ _regenerateAll();
+ });
+ }
+ _document_closed = document->connectDestroy([=]() { _documentClosed(); });
+}
+
+/** Builds the dialog window from a Glade file and attaches event handlers */
+void PaintServersDialog::_buildDialogWindow(char const *const glade_file)
+{
+ // Load the dialog from the Glade file
+ auto file_path = get_filename_string(IO::Resource::UIS, glade_file);
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(file_path);
+ } catch (Glib::Error const &e) {
+ Glib::ustring message{"Could not load the Glade file for the Paint Servers dialog: "};
+ message += file_path + "\n" + e.what();
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__, message.c_str());
+ return;
+ }
+
+ // Place top-level grid container in the window
+ Gtk::Grid *container = nullptr;
+ builder->get_widget("PaintServersContainerGrid", container);
+ if (container) {
+ pack_start(*container, Gtk::PACK_EXPAND_WIDGET);
+ } else {
+ return;
+ }
+
+ builder->get_widget("ServersDropdown", dropdown);
+ dropdown->append(ALLDOCS, _(ALLDOCS));
+ dropdown->set_active_id(ALLDOCS);
+ dropdown->signal_changed().connect([=]() { onPaintSourceDocumentChanged(); });
+
+ builder->get_widget("PaintIcons", icon_view);
+ icon_view->set_model(static_cast<Glib::RefPtr<Gtk::TreeModel>>(store[current_store]));
+ icon_view->set_tooltip_column(columns.id.index());
+ icon_view->set_pixbuf_column(columns.pixbuf.index());
+ _item_activated = icon_view->signal_item_activated().connect([=](Gtk::TreeModel::Path const &p) {
+ onPaintClicked(p);
+ });
+
+ Gtk::RadioButton *fill_radio = nullptr;
+ builder->get_widget("TargetRadioFill", fill_radio);
+ fill_radio->signal_toggled().connect([=]() {
+ _targetting_fill = fill_radio->get_active();
+ _updateActiveItem();
+ });
+}
+
+/** Handles the destruction of the current document */
+void PaintServersDialog::_documentClosed()
+{
+ _defs_changed.disconnect();
+ _document_closed.disconnect();
+
+ document_map.erase(CURRENTDOC);
+ store[CURRENTDOC]->clear();
+ _regenerateAll();
+}
+
+/**
+ * @brief Returns the Fill and Stroke, in this order, common to a list of objects
+ * @param objects - a vector of pointers to objects whose fill and stroke will be analysed
+ * @return a tuple of optionals which are empty when the vector of objects is empty or
+ * when either fill or stroke are not common to all of them. Otherwise, the first
+ * element of the tuple is the common fill, as a Glib::ustring, and the second one
+ * is the common stroke.
+ */
+std::tuple<std::optional<Glib::ustring>, std::optional<Glib::ustring>>
+PaintServersDialog::_findCommonFillAndStroke(std::vector<SPObject *> const &objects) const
+{
+ MaybeString common_fill, common_stroke;
+ if (!objects.empty()) {
+ Glib::ustring candidate_fill = objects[0]->style->fill.get_value();
+ Glib::ustring candidate_stroke = objects[0]->style->stroke.get_value();
+ bool fills_agree = true;
+ bool strokes_agree = true;
+ size_t const count = objects.size();
+ for (size_t i = 1; i < count; i++) {
+ if (fills_agree && candidate_fill != objects[i]->style->fill.get_value()) {
+ fills_agree = false;
+ }
+ if (strokes_agree && candidate_stroke != objects[i]->style->stroke.get_value()) {
+ strokes_agree = false;
+ }
+ }
+ if (fills_agree) {
+ common_fill = candidate_fill;
+ }
+ if (strokes_agree) {
+ common_stroke = candidate_stroke;
+ }
+ }
+ return std::tuple(common_fill, common_stroke);
+}
+
+/** Finds paints used by an object and (recursively) by its descendants
+ * @param in - the object whose paints to grab
+ * @param list - the paints will be added to this vector as strings usable in the `fill` CSS property
+ */
+void PaintServersDialog::_findPaints(SPObject *in, std::vector<Glib::ustring> &list)
+{
+ g_return_if_fail(in != nullptr);
+
+ // Add paint servers in <defs> section.
+ if (is<SPPaintServer>(in)) {
+ if (in->getId()) {
+ // Need to check as one can't construct Glib::ustring with nullptr.
+ list.push_back(Glib::ustring("url(#") + in->getId() + ")");
+ }
+ // Don't recurse into paint servers.
+ return;
+ }
+
+ // Add paint servers referenced by shapes.
+ if (is<SPShape>(in)) {
+ auto const style = in->style;
+ list.push_back(style->fill.get_value());
+ list.push_back(style->stroke.get_value());
+ }
+
+ for (auto child: in->childList(false)) {
+ PaintServersDialog::_findPaints(child, list);
+ }
+}
+
+/** Load stock paints from files in share/paint */
+void PaintServersDialog::_loadStockPaints()
+{
+ std::vector<PaintDescription> paints;
+
+ // Extract out paints from files in share/paint.
+ for (auto const &path : get_filenames(Inkscape::IO::Resource::PAINT, {".svg"})) {
+ try { // createNewDoc throws
+ auto doc = std::unique_ptr<SPDocument>(SPDocument::createNewDoc(path.c_str(), false));
+ if (!doc) {
+ throw std::exception();
+ }
+ _loadPaintsFromDocument(doc.get(), paints);
+ _stock_documents.push_back(std::move(doc)); // Ensures eventual destruction in our dtor
+ } catch (std::exception &e) {
+ auto message = Glib::ustring{"Cannot open paint server resource file '"} + path + "'!";
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__, message.c_str());
+ continue;
+ }
+ }
+
+ _createPaints(paints);
+}
+
+/** Load paint servers from the <defs> of the current document */
+void PaintServersDialog::_loadFromCurrentDocument()
+{
+ auto document = getDocument();
+ if (!document) {
+ return;
+ }
+
+ std::vector<PaintDescription> paints;
+ _loadPaintsFromDocument(document, paints);
+
+ // There can only be one current document, so we clear the corresponding store
+ store[CURRENTDOC]->clear();
+ _createPaints(paints);
+}
+
+/** Creates a collection of paints from the given vector of descriptions */
+void PaintServersDialog::_createPaints(std::vector<PaintDescription> &collection)
+{
+ // Sort and remove duplicates.
+ auto paints_cmp = [](PaintDescription const &a, PaintDescription const &b) -> bool {
+ return a.url < b.url;
+ };
+ std::sort(collection.begin(), collection.end(), paints_cmp);
+ collection.erase(std::unique(collection.begin(), collection.end()), collection.end());
+
+ for (auto &paint : collection) {
+ _instantiatePaint(paint);
+ }
+}
+
+/** Create a paint from a description and generate its bitmap preview */
+void PaintServersDialog::_instantiatePaint(PaintDescription &paint)
+{
+ if (!paint.has_preview()) {
+ _generateBitmapPreview(paint);
+ }
+ if (paint.has_preview()) { // don't add the paint if preview generation failed.
+ _addToStore(paint);
+ }
+}
+
+/** Adds a paint to store */
+void PaintServersDialog::_addToStore(PaintDescription &paint)
+{
+ if (store.find(paint.doc_title) == store.end()) {
+ store[paint.doc_title] = Gtk::ListStore::create(columns);
+ }
+
+ auto iter = store[paint.doc_title]->append();
+ paint.write_to_iterator(iter, &columns);
+
+ if (document_map.find(paint.doc_title) == document_map.end()) {
+ document_map[paint.doc_title] = paint.source_document;
+ dropdown->append(paint.doc_title, _(paint.doc_title.c_str()));
+ }
+}
+
+/** Returns a PaintDescription for a paint already present in the store */
+PaintDescription PaintServersDialog::_descriptionFromIterator(Gtk::ListStore::iterator const &iter) const
+{
+ Glib::ustring doc_title = (*iter)[columns.document];
+ SPDocument *doc_ptr;
+ try {
+ doc_ptr = document_map.at(doc_title);
+ } catch (std::out_of_range &exception) {
+ doc_ptr = nullptr;
+ }
+ Glib::ustring paint_url = (*iter)[columns.paint];
+ PaintDescription result(doc_ptr, doc_title, std::move(paint_url));
+
+ // Fill in fields that are set only on instantiation
+ result.id = (*iter)[columns.id];
+ result.bitmap = (*iter)[columns.pixbuf];
+ return result;
+}
+
+/** Regenerates the list of all paint servers from the already loaded paints */
+void PaintServersDialog::_regenerateAll()
+{
+ bool showing_all = (current_store == ALLDOCS);
+ std::vector<PaintDescription> all_paints;
+
+ for (auto const &[doc, paint_list] : store) {
+ if (doc == ALLDOCS) {
+ continue; // ignore the target store
+ }
+ paint_list->foreach_iter([&](Gtk::ListStore::iterator const &paint) -> bool
+ {
+ all_paints.push_back(_descriptionFromIterator(paint));
+ return false;
+ });
+ }
+
+ // Sort and remove duplicates. When the duplicate entry is from the current document,
+ // we remove it preferentially, keeping the stock paint if available.
+ std::sort(all_paints.begin(), all_paints.end(),
+ [=](PaintDescription const &a, PaintDescription const &b) -> bool
+ {
+ int cmp = a.url.compare(b.url);
+ if (cmp < 0) return true;
+ if (cmp > 0) return false;
+
+ auto external = [=] (PaintDescription const &p) { return p.doc_title != CURRENTDOC; };
+ return external(a) && !external(b);
+ });
+ all_paints.erase(std::unique(all_paints.begin(), all_paints.end()), all_paints.end());
+
+ store[ALLDOCS]->clear();
+
+ // Add paints from the cleaned up list to the store
+ for (auto &&paint : all_paints) {
+ auto iter = store[ALLDOCS]->append();
+ paint.write_to_iterator(iter, &columns);
+ }
+
+ if (showing_all) {
+ selectionChanged(getSelection());
+ }
+}
+
+/** Generates the bitmap preview for the given paint */
+void PaintServersDialog::_generateBitmapPreview(PaintDescription &paint)
+{
+ SPObject *rect = preview_document->getObjectById("Rect");
+ SPObject *defs = preview_document->getObjectById("Defs");
+
+ paint.bitmap = Glib::RefPtr<Gdk::Pixbuf>(nullptr);
+ if (paint.url.empty()) {
+ return;
+ }
+
+ // Set style on the preview rectangle
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill", paint.url.c_str());
+ rect->changeCSS(css, "style");
+ sp_repr_css_attr_unref(css);
+
+ // Insert paint into the defs of the preview document if required
+ Glib::MatchInfo matchInfo;
+ static Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("url\\(#([A-Za-z0-9#._-]*)\\)");
+
+ regex->match(paint.url, matchInfo);
+ if (!matchInfo.matches()) {
+ // Currently we only show previews for hatches/patterns of the form url(#some-id)
+ // TODO: handle colors, gradients, etc.
+ // See https://wiki.inkscape.org/wiki/Google_Summer_of_Code#P11._Improvements_to_Paint_Server_Dialog
+ return;
+ }
+ paint.id = matchInfo.fetch(1);
+
+ // Delete old paints if necessary
+ std::vector<SPObject *> old_paints = preview_document->getObjectsBySelector("defs > *");
+ for (auto paint : old_paints) {
+ paint->deleteObject(false);
+ }
+
+ // Find the new paint
+ SPObject *new_paint = paint.source_document->getObjectById(paint.id);
+ if (!new_paint) {
+ Glib::ustring error_message = Glib::ustring{"Cannot find paint server: "} + paint.id;
+ g_warn_message("Inkscape", __FILE__, __LINE__, __func__, error_message.c_str());
+ return;
+ }
+
+ // Add the new paint along with all paints it refers to
+ XML::Document *xml_doc = preview_document->getReprDoc();
+ std::vector<SPObject *> encountered{new_paint}; ///< For the prevention of cyclic refs
+
+ while (new_paint) {
+ auto const *new_repr = new_paint->getRepr();
+ if (!new_repr) {
+ break;
+ }
+
+ // Create a copy repr of the paint
+ defs->appendChild(new_repr->duplicate(xml_doc));
+
+ // Check for cross-references in the paint
+ auto ref = Inkscape::getHrefAttribute(*new_repr).second;
+ if (ref) {
+ // Paint is cross-referencing another object (probably another paint);
+ // we must copy the referenced object as well
+ new_paint = paint.source_document->getObjectByHref(ref);
+ using namespace std;
+ if (find(begin(encountered), end(encountered), new_paint) == end(encountered)) {
+ encountered.push_back(new_paint);
+ } else {
+ break; // Break reference cycle
+ }
+ } else { // No more hrefs
+ break;
+ }
+ }
+
+ preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ preview_document->ensureUpToDate();
+
+ if (Geom::OptRect dbox = static_cast<SPItem *>(rect)->visualBounds())
+ {
+ unsigned size = std::ceil(std::max(dbox->width(), dbox->height()));
+ paint.bitmap = Glib::wrap(render_pixbuf(renderDrawing, 1, *dbox, size));
+ }
+}
+
+/** @brief Load paint servers from the given source document
+ * @param document - the source document
+ * @param output - the paint descriptions will be added to this vector */
+void PaintServersDialog::_loadPaintsFromDocument(SPDocument *document, std::vector<PaintDescription> &output)
+{
+ Glib::ustring document_title;
+ if (!document->getRoot()->title()) {
+ document_title = CURRENTDOC;
+ } else {
+ document_title = Glib::ustring(document->getRoot()->title());
+ }
+
+ // Find all paints
+ std::vector<Glib::ustring> urls;
+ _findPaints(document->getRoot(), urls);
+
+ for (auto const &url : urls) {
+ output.emplace_back(document, document_title, std::move(url));
+ }
+}
+
+/** Handles the change of the dropdown for selecting paint sources */
+void PaintServersDialog::onPaintSourceDocumentChanged()
+{
+ current_store = dropdown->get_active_id();
+ icon_view->set_model(store[current_store]);
+ _updateActiveItem();
+}
+
+/** Event handler for when a paint entry in the dialog has been activated */
+void PaintServersDialog::onPaintClicked(Gtk::TreeModel::Path const &path)
+{
+ // Get the current selected elements
+ Selection *selection = getSelection();
+ std::vector<SPObject *> items = _unpackSelection(selection);
+
+ if (items.empty()) {
+ return;
+ }
+
+ Gtk::ListStore::iterator iter = store[current_store]->get_iter(path);
+ Glib::ustring id = (*iter)[columns.id];
+ Glib::ustring paint = (*iter)[columns.paint];
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf = (*iter)[columns.pixbuf];
+ Glib::ustring hatches_document_title = (*iter)[columns.document];
+ SPDocument *hatches_document = document_map[hatches_document_title];
+ SPObject *paint_server = hatches_document->getObjectById(id);
+
+ bool paint_server_exists = false;
+ for (auto const &server : store[CURRENTDOC]->children()) {
+ if (server[columns.id] == id) {
+ paint_server_exists = true;
+ break;
+ }
+ }
+
+ SPDocument *document = getDocument();
+ if (!paint_server_exists) {
+ // Add the paint server to the current document definition
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *repr = paint_server->getRepr()->duplicate(xml_doc);
+ document->getDefs()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // Add the pixbuf to the current document store
+ iter = store[CURRENTDOC]->append();
+ (*iter)[columns.id] = id;
+ (*iter)[columns.paint] = paint;
+ (*iter)[columns.pixbuf] = pixbuf;
+ (*iter)[columns.document] = CURRENTDOC;
+ }
+
+ for (auto item : items) {
+ item->style->getFillOrStroke(_targetting_fill)->read(paint.c_str());
+ item->updateRepr();
+ }
+
+ document->collectOrphans();
+}
+
+/**
+ * @brief Handles the change in the selection, finding common fill or stroke for selected objects
+ * @param selection - the new selection
+ */
+void PaintServersDialog::selectionChanged(Selection* selection)
+{
+ if (!selection || selection->isEmpty()) {
+ _common_fill.reset();
+ _common_stroke.reset();
+ } else {
+ auto const selected_items = _unpackSelection(selection);
+ auto const &[fill, stroke] = _findCommonFillAndStroke(selected_items);
+ _common_fill = std::move(fill);
+ _common_stroke = std::move(stroke);
+ }
+ _updateActiveItem();
+}
+
+/**
+ * Recursively extracts non-group elements from groups, if any
+ * @param parent - the parent object which will be unpacked recursively
+ * @param output - the resulting SPObject pointers will be added to this vector
+ */
+void PaintServersDialog::_unpackGroups(SPObject *parent, std::vector<SPObject *> &output) const
+{
+ std::vector<SPObject *> children = parent->childList(false);
+ if (children.empty()) {
+ output.push_back(parent);
+ } else {
+ for (auto child : children) {
+ _unpackGroups(child, output);
+ }
+ }
+}
+
+/**
+ * @brief Recursively unpacks groups in the given selection
+ * @param selection - a pointer to an Inkscape::Selection object to be unpacked
+ * @return a vector of SPObject pointers to the unpacked selected items.
+ */
+std::vector<SPObject *> PaintServersDialog::_unpackSelection(Selection *selection) const
+{
+ std::vector<SPObject *> result;
+ if (!selection) {
+ return result;
+ }
+
+ auto const &selected_range = selection->items();
+ for (auto const item : selected_range) {
+ _unpackGroups(static_cast<SPObject *>(item), result);
+ }
+ return result;
+}
+
+/**
+ * @brief Updates the active item in the icon view to reflect the common paint of the
+ * fill or stroke of selected objects
+ */
+void PaintServersDialog::_updateActiveItem()
+{
+ _item_activated.block();
+ MaybeString &common = (_targetting_fill ? _common_fill : _common_stroke);
+ if (common) {
+ bool found = false;
+ store[current_store]->foreach(
+ [&](Gtk::ListStore::Path const &path, Gtk::ListStore::iterator const &icon) -> bool {
+ if ((*icon)[columns.paint] == *common) {
+ icon_view->select_path(path);
+ found = true;
+ return true; // Finish iterating
+ }
+ return false;
+ });
+ if (!found) {
+ icon_view->unselect_all();
+ }
+ } else {
+ icon_view->unselect_all();
+ }
+ _item_activated.unblock();
+}
+
+//----------------------------------------------------------------------------------------------------
+
+PaintDescription::PaintDescription(SPDocument *source_doc, Glib::ustring title, Glib::ustring const &&paint_url)
+ : source_document{source_doc}
+ , doc_title{std::move(title)}
+ , id{} // id will be filled in when generating the bitmap
+ , url{paint_url}
+ , bitmap{nullptr}
+{}
+
+/** Write the data stored in this struct to a list store
+ * @param it - the iterator to the ListStore to write to
+ */
+void PaintDescription::write_to_iterator(Gtk::ListStore::iterator &it, PaintServersColumns const *cols) const {
+ (*it)[cols->id] = id;
+ (*it)[cols->paint] = url;
+ (*it)[cols->pixbuf] = bitmap;
+ (*it)[cols->document] = doc_title;
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-basic-offset:2
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/paint-servers.h b/src/ui/dialog/paint-servers.h
new file mode 100644
index 0000000..c170318
--- /dev/null
+++ b/src/ui/dialog/paint-servers.h
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Paint Servers dialog
+ */
+/* Authors:
+ * Valentin Ionita
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * Copyright (C) 2019 Valentin Ionita
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_PAINT_SERVERS_H
+#define INKSCAPE_UI_DIALOG_PAINT_SERVERS_H
+
+#include <glibmm/i18n.h>
+#include <gtkmm.h>
+#include <sigc++/sigc++.h>
+
+#include "display/drawing.h"
+#include "ui/dialog/dialog-base.h"
+
+class SPObject;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class PaintServersColumns : public Gtk::TreeModel::ColumnRecord
+{
+public:
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> paint;
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> pixbuf;
+ Gtk::TreeModelColumn<Glib::ustring> document;
+
+ PaintServersColumns()
+ {
+ add(id);
+ add(paint);
+ add(pixbuf);
+ add(document);
+ }
+};
+
+struct PaintDescription
+{
+ /** Pointer to the document from which the paint originates */
+ SPDocument *source_document = nullptr;
+
+ /** Title of the document from which the paint originates, or "Current document" */
+ Glib::ustring doc_title;
+
+ /** ID of the the paint server within the document */
+ Glib::ustring id;
+
+ /** URL of the paint within the document */
+ Glib::ustring url;
+
+ /** Bitmap preview of the paint */
+ Glib::RefPtr<Gdk::Pixbuf> bitmap;
+
+ PaintDescription(SPDocument *source_doc, Glib::ustring title, Glib::ustring const &&paint_url);
+ void write_to_iterator(Gtk::ListStore::iterator &it, PaintServersColumns const *cols) const;
+
+ /** Whether the paint with this description has a valid pixbuf preview */
+ inline bool has_preview() const { return (bool)bitmap; }
+
+ /** Two paints are considered the same if they have the same urls */
+ inline bool operator==(PaintDescription const &other) const { return url == other.url; }
+};
+
+/**
+ * This dialog serves as a preview for different types of paint servers,
+ * currently only predefined. It can set the fill or stroke of the selected
+ * object to the to the paint server you select.
+ *
+ * Patterns and hatches are loaded from the preferences paths and displayed
+ * for each document, for all documents and for the current document.
+ */
+class PaintServersDialog : public DialogBase
+{
+ using MaybeString = std::optional<Glib::ustring>;
+
+public:
+ PaintServersDialog();
+ ~PaintServersDialog() override;
+
+ void documentReplaced() override;
+
+private:
+ void _addToStore(PaintDescription &paint);
+ void _buildDialogWindow(char const *const glade_file);
+ void _createPaints(std::vector<PaintDescription> &collection);
+ PaintDescription _descriptionFromIterator(Gtk::ListStore::iterator const &iter) const;
+ void _documentClosed();
+ std::tuple<MaybeString, MaybeString> _findCommonFillAndStroke(std::vector<SPObject *> const &objects) const;
+ static void _findPaints(SPObject *in, std::vector<Glib::ustring> &list);
+ void _generateBitmapPreview(PaintDescription& paint);
+ void _instantiatePaint(PaintDescription &paint);
+ void _loadFromCurrentDocument();
+ void _loadPaintsFromDocument(SPDocument *document, std::vector<PaintDescription> &output);
+ void _loadStockPaints();
+ void _regenerateAll();
+ void _unpackGroups(SPObject *parent, std::vector<SPObject *> &output) const;
+ std::vector<SPObject *> _unpackSelection(Selection *selection) const;
+ void _updateActiveItem();
+ void onPaintClicked(const Gtk::TreeModel::Path &path);
+ void onPaintSourceDocumentChanged();
+ void selectionChanged(Selection *selection) final;
+
+ bool _targetting_fill; ///< whether setting fill (true) or stroke (false)
+ std::map<Glib::ustring, Glib::RefPtr<Gtk::ListStore>> store;
+ Glib::ustring current_store;
+ std::vector<std::unique_ptr<SPDocument>> _stock_documents;
+ std::map<Glib::ustring, SPDocument *> document_map;
+ SPDocument *preview_document = nullptr;
+ Inkscape::Drawing renderDrawing;
+ Gtk::ComboBoxText *dropdown = nullptr;
+ Gtk::IconView *icon_view = nullptr;
+ PaintServersColumns const columns;
+ sigc::connection _defs_changed, _document_closed;
+ MaybeString _common_stroke, _common_fill; ///< Common fill/stroke to all selected elements
+ sigc::connection _item_activated;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SEEN INKSCAPE_UI_DIALOG_PAINT_SERVERS_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-basic-offset:2
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/polar-arrange-tab.cpp b/src/ui/dialog/polar-arrange-tab.cpp
new file mode 100644
index 0000000..960048e
--- /dev/null
+++ b/src/ui/dialog/polar-arrange-tab.cpp
@@ -0,0 +1,409 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @brief Arranges Objects into a Circle/Ellipse
+ */
+/* Authors:
+ * Declara Denis
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/i18n.h>
+#include <gtkmm/messagedialog.h>
+
+#include <2geom/transforms.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "preferences.h"
+
+#include "object/sp-ellipse.h"
+#include "object/sp-item-transform.h"
+
+#include "ui/dialog/polar-arrange-tab.h"
+#include "ui/dialog/tile.h"
+#include "ui/icon-names.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+PolarArrangeTab::PolarArrangeTab(ArrangeDialog *parent_)
+ : parent(parent_),
+ parametersTable(),
+ centerY("", C_("Polar arrange tab", "Y coordinate of the center"), UNIT_TYPE_LINEAR),
+ centerX("", C_("Polar arrange tab", "X coordinate of the center"), centerY),
+ radiusY("", C_("Polar arrange tab", "Y coordinate of the radius"), UNIT_TYPE_LINEAR),
+ radiusX("", C_("Polar arrange tab", "X coordinate of the radius"), radiusY),
+ angleY("", C_("Polar arrange tab", "Ending angle"), UNIT_TYPE_RADIAL),
+ angleX("", C_("Polar arrange tab", "Starting angle"), angleY)
+{
+ anchorPointLabel.set_text(C_("Polar arrange tab", "Anchor point:"));
+ anchorPointLabel.set_halign(Gtk::ALIGN_START);
+ pack_start(anchorPointLabel, false, false);
+
+ anchorBoundingBoxRadio.set_label(C_("Polar arrange tab", "Objects' bounding boxes:"));
+ anchorRadioGroup = anchorBoundingBoxRadio.get_group();
+ anchorBoundingBoxRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed));
+ pack_start(anchorBoundingBoxRadio, false, false);
+
+ pack_start(anchorSelector, false, false);
+
+ anchorObjectPivotRadio.set_label(C_("Polar arrange tab", "Objects' rotational centers"));
+ anchorObjectPivotRadio.set_group(anchorRadioGroup);
+ anchorObjectPivotRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed));
+ pack_start(anchorObjectPivotRadio, false, false);
+
+ arrangeOnLabel.set_text(C_("Polar arrange tab", "Arrange on:"));
+ arrangeOnLabel.set_halign(Gtk::ALIGN_START);
+ pack_start(arrangeOnLabel, false, false);
+
+ arrangeOnFirstCircleRadio.set_label(C_("Polar arrange tab", "First selected circle/ellipse/arc"));
+ arrangeRadioGroup = arrangeOnFirstCircleRadio.get_group();
+ arrangeOnFirstCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed));
+ pack_start(arrangeOnFirstCircleRadio, false, false);
+
+ arrangeOnLastCircleRadio.set_label(C_("Polar arrange tab", "Last selected circle/ellipse/arc"));
+ arrangeOnLastCircleRadio.set_group(arrangeRadioGroup);
+ arrangeOnLastCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed));
+ pack_start(arrangeOnLastCircleRadio, false, false);
+
+ arrangeOnParametersRadio.set_label(C_("Polar arrange tab", "Parameterized:"));
+ arrangeOnParametersRadio.set_group(arrangeRadioGroup);
+ arrangeOnParametersRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed));
+ pack_start(arrangeOnParametersRadio, false, false);
+
+ centerLabel.set_text(C_("Polar arrange tab", "Center X/Y:"));
+ parametersTable.attach(centerLabel, 0, 0, 1, 1);
+ centerX.setDigits(2);
+ centerX.setIncrements(0.2, 0);
+ centerX.setRange(-10000, 10000);
+ centerX.setValue(0, "px");
+ centerY.setDigits(2);
+ centerY.setIncrements(0.2, 0);
+ centerY.setRange(-10000, 10000);
+ centerY.setValue(0, "px");
+ parametersTable.attach(centerX, 1, 0, 1, 1);
+ parametersTable.attach(centerY, 2, 0, 1, 1);
+
+ radiusLabel.set_text(C_("Polar arrange tab", "Radius X/Y:"));
+ parametersTable.attach(radiusLabel, 0, 1, 1, 1);
+ radiusX.setDigits(2);
+ radiusX.setIncrements(0.2, 0);
+ radiusX.setRange(0.001, 10000);
+ radiusX.setValue(100, "px");
+ radiusY.setDigits(2);
+ radiusY.setIncrements(0.2, 0);
+ radiusY.setRange(0.001, 10000);
+ radiusY.setValue(100, "px");
+ parametersTable.attach(radiusX, 1, 1, 1, 1);
+ parametersTable.attach(radiusY, 2, 1, 1, 1);
+
+ angleLabel.set_text(_("Angle X/Y:"));
+ parametersTable.attach(angleLabel, 0, 2, 1, 1);
+ angleX.setDigits(2);
+ angleX.setIncrements(0.2, 0);
+ angleX.setRange(-10000, 10000);
+ angleX.setValue(0, "°");
+ angleY.setDigits(2);
+ angleY.setIncrements(0.2, 0);
+ angleY.setRange(-10000, 10000);
+ angleY.setValue(180, "°");
+ parametersTable.attach(angleX, 1, 2, 1, 1);
+ parametersTable.attach(angleY, 2, 2, 1, 1);
+ parametersTable.set_row_spacing(4);
+ parametersTable.set_column_spacing(4);
+ pack_start(parametersTable, false, false);
+
+ rotateObjectsCheckBox.set_label(_("Rotate objects"));
+ rotateObjectsCheckBox.set_active(true);
+ pack_start(rotateObjectsCheckBox, false, false);
+
+ centerX.set_sensitive(false);
+ centerY.set_sensitive(false);
+ angleX.set_sensitive(false);
+ angleY.set_sensitive(false);
+ radiusX.set_sensitive(false);
+ radiusY.set_sensitive(false);
+
+ set_border_width(4);
+
+ parametersTable.show_all();
+ parametersTable.set_no_show_all();
+ parametersTable.hide();
+}
+
+/**
+ * This function rotates an item around a given point by a given amount
+ * @param item item to rotate
+ * @param center center of the rotation to perform
+ * @param rotation amount to rotate the object by
+ */
+static void rotateAround(SPItem *item, Geom::Point center, Geom::Rotate const &rotation)
+{
+ Geom::Translate const s(center);
+ Geom::Affine affine = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s);
+
+ // Save old center
+ center = item->getCenter();
+
+ item->set_i2d_affine(item->i2dt_affine() * affine);
+ item->doWriteTransform(item->transform);
+
+ if(item->isCenterSet())
+ {
+ item->setCenter(center * affine);
+ item->updateRepr();
+ }
+}
+
+/**
+ * Calculates the angle at which to put an object given the total amount
+ * of objects, the index of the objects as well as the arc start and end
+ * points
+ * @param arcBegin angle at which the arc begins
+ * @param arcEnd angle at which the arc ends
+ * @param count number of objects in the selection
+ * @param n index of the object in the selection
+ */
+static float calcAngle(float arcBegin, float arcEnd, int count, int n)
+{
+ float arcLength = arcEnd - arcBegin;
+ float delta = std::abs(std::abs(arcLength) - 2*M_PI);
+ if(delta > 0.01) count--; // If not a complete circle, put an object also at the extremes of the arc;
+
+ float angle = n / (float)count;
+ // Normalize for arcLength:
+ angle = angle * arcLength;
+ angle += arcBegin;
+
+ return angle;
+}
+
+/**
+ * Calculates the point at which an object needs to be, given the center of the ellipse,
+ * it's radius (x and y), as well as the angle
+ */
+static Geom::Point calcPoint(float cx, float cy, float rx, float ry, float angle)
+{
+ return Geom::Point(cx + cos(angle) * rx, cy + sin(angle) * ry);
+}
+
+/**
+ * Returns the selected anchor point in desktop coordinates. If anchor
+ * is 0 to 8, then a bounding box point has been chosen. If it is 9 however
+ * the rotational center is chosen.
+ */
+static Geom::Point getAnchorPoint(int anchor, SPItem *item)
+{
+ Geom::Point source;
+
+ Geom::OptRect bbox = item->documentVisualBounds();
+
+ switch(anchor)
+ {
+ case 0: // Top - Left
+ case 3: // Middle - Left
+ case 6: // Bottom - Left
+ source[0] = bbox->min()[Geom::X];
+ break;
+ case 1: // Top - Middle
+ case 4: // Middle - Middle
+ case 7: // Bottom - Middle
+ source[0] = (bbox->min()[Geom::X] + bbox->max()[Geom::X]) / 2.0f;
+ break;
+ case 2: // Top - Right
+ case 5: // Middle - Right
+ case 8: // Bottom - Right
+ source[0] = bbox->max()[Geom::X];
+ break;
+ };
+
+ switch(anchor)
+ {
+ case 0: // Top - Left
+ case 1: // Top - Middle
+ case 2: // Top - Right
+ source[1] = bbox->min()[Geom::Y];
+ break;
+ case 3: // Middle - Left
+ case 4: // Middle - Middle
+ case 5: // Middle - Right
+ source[1] = (bbox->min()[Geom::Y] + bbox->max()[Geom::Y]) / 2.0f;
+ break;
+ case 6: // Bottom - Left
+ case 7: // Bottom - Middle
+ case 8: // Bottom - Right
+ source[1] = bbox->max()[Geom::Y];
+ break;
+ };
+
+ // If using center
+ if(anchor == 9)
+ source = item->getCenter();
+ else
+ {
+ source *= item->document->doc2dt();
+ }
+
+ return source;
+}
+
+/**
+ * Moves an SPItem to a given location, the location is based on the given anchor point.
+ * @param anchor 0 to 8 are the various bounding box points like follows:
+ * 0 1 2
+ * 3 4 5
+ * 6 7 8
+ * Anchor mode 9 is the rotational center of the object
+ * @param item Item to move
+ * @param p point at which to move the object
+ */
+static void moveToPoint(int anchor, SPItem *item, Geom::Point p)
+{
+ item->move_rel(Geom::Translate(p - getAnchorPoint(anchor, item)));
+}
+
+void PolarArrangeTab::arrange()
+{
+ Inkscape::Selection *selection = parent->getDesktop()->getSelection();
+ const std::vector<SPItem*> tmp(selection->items().begin(), selection->items().end());
+ SPGenericEllipse *referenceEllipse = nullptr; // Last ellipse in selection
+
+ bool arrangeOnEllipse = !arrangeOnParametersRadio.get_active();
+ bool arrangeOnFirstEllipse = arrangeOnEllipse && arrangeOnFirstCircleRadio.get_active();
+ float yaxisdir = parent->getDesktop()->yaxisdir();
+
+ int count = 0;
+ for(auto item : tmp)
+ {
+ if(arrangeOnEllipse)
+ {
+ if(!arrangeOnFirstEllipse)
+ {
+ if(is<SPGenericEllipse>(item))
+ referenceEllipse = cast<SPGenericEllipse>(item);
+ } else {
+ if(is<SPGenericEllipse>(item) && referenceEllipse == nullptr)
+ referenceEllipse = cast<SPGenericEllipse>(item);
+ }
+ }
+ ++count;
+ }
+
+ float cx, cy; // Center of the ellipse
+ float rx, ry; // Radiuses of the ellipse in x and y direction
+ float arcBeg, arcEnd; // begin and end angles for arcs
+ Geom::Affine transformation; // Any additional transformation to apply to the objects
+
+ if(arrangeOnEllipse)
+ {
+ if(referenceEllipse == nullptr)
+ {
+ Gtk::MessageDialog dialog(_("Couldn't find an ellipse in selection"), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true);
+ dialog.run();
+ return;
+ } else {
+ cx = referenceEllipse->cx.value;
+ cy = referenceEllipse->cy.value;
+ rx = referenceEllipse->rx.value;
+ ry = referenceEllipse->ry.value;
+ arcBeg = referenceEllipse->start;
+ arcEnd = referenceEllipse->end;
+
+ transformation = referenceEllipse->i2dt_affine();
+
+ // We decrement the count by 1 as we are not going to lay
+ // out the reference ellipse
+ --count;
+ }
+
+ } else {
+ // Read options from UI
+ cx = centerX.getValue("px");
+ cy = centerY.getValue("px");
+ rx = radiusX.getValue("px");
+ ry = radiusY.getValue("px");
+ arcBeg = angleX.getValue("rad");
+ arcEnd = angleY.getValue("rad") * yaxisdir;
+ transformation.setIdentity();
+ referenceEllipse = nullptr;
+ }
+
+ int anchor = 9;
+ if(anchorBoundingBoxRadio.get_active())
+ {
+ anchor = anchorSelector.getHorizontalAlignment() +
+ anchorSelector.getVerticalAlignment() * 3;
+ }
+
+ Geom::Point realCenter = Geom::Point(cx, cy) * transformation;
+
+ int i = 0;
+ for(auto item : tmp)
+ {
+ // Ignore the reference ellipse if any
+ if(item != referenceEllipse)
+ {
+ float angle = calcAngle(arcBeg, arcEnd, count, i);
+ Geom::Point newLocation = calcPoint(cx, cy, rx, ry, angle) * transformation;
+
+ moveToPoint(anchor, item, newLocation);
+
+ if(rotateObjectsCheckBox.get_active()) {
+ // Calculate the angle by which to rotate each object
+ angle = -atan2f(-yaxisdir * (newLocation.x() - realCenter.x()), -yaxisdir * (newLocation.y() - realCenter.y()));
+ rotateAround(item, newLocation, Geom::Rotate(angle));
+ }
+
+ ++i;
+ }
+ }
+
+ DocumentUndo::done(parent->getDesktop()->getDocument(), _("Arrange on ellipse"), INKSCAPE_ICON("dialog-align-and-distribute"));
+}
+
+void PolarArrangeTab::updateSelection()
+{
+}
+
+void PolarArrangeTab::on_arrange_radio_changed()
+{
+ bool arrangeParametric = arrangeOnParametersRadio.get_active();
+
+ centerX.set_sensitive(arrangeParametric);
+ centerY.set_sensitive(arrangeParametric);
+
+ angleX.set_sensitive(arrangeParametric);
+ angleY.set_sensitive(arrangeParametric);
+
+ radiusX.set_sensitive(arrangeParametric);
+ radiusY.set_sensitive(arrangeParametric);
+
+ parametersTable.set_visible(arrangeParametric);
+}
+
+void PolarArrangeTab::on_anchor_radio_changed()
+{
+ bool anchorBoundingBox = anchorBoundingBoxRadio.get_active();
+
+ anchorSelector.set_sensitive(anchorBoundingBox);
+}
+
+} //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/dialog/polar-arrange-tab.h b/src/ui/dialog/polar-arrange-tab.h
new file mode 100644
index 0000000..0c5acde
--- /dev/null
+++ b/src/ui/dialog/polar-arrange-tab.h
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @brief Arranges Objects into a Circle/Ellipse
+ */
+/* Authors:
+ * Declara Denis
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H
+#define INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H
+
+#include "ui/widget/scalar-unit.h"
+#include "ui/widget/anchor-selector.h"
+#include "ui/dialog/arrange-tab.h"
+
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/radiobuttongroup.h>
+#include <gtkmm/grid.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class ArrangeDialog;
+
+/**
+ * PolarArrangeTab is a Tab displayed in the Arrange dialog and contains
+ * enables the user to arrange objects on a circular or elliptical shape
+ */
+class PolarArrangeTab : public ArrangeTab {
+public:
+ PolarArrangeTab(ArrangeDialog *parent_);
+ ~PolarArrangeTab() override = default;;
+
+ /**
+ * Do the actual arrangement
+ */
+ void arrange() override;
+
+ /**
+ * Respond to selection change
+ */
+ void updateSelection();
+
+ void on_anchor_radio_changed();
+ void on_arrange_radio_changed();
+
+private:
+ PolarArrangeTab(PolarArrangeTab const &d) = delete; // no copy
+ void operator=(PolarArrangeTab const &d) = delete; // no assign
+
+ ArrangeDialog *parent;
+
+ Gtk::Label anchorPointLabel;
+
+ Gtk::RadioButtonGroup anchorRadioGroup;
+ Gtk::RadioButton anchorBoundingBoxRadio;
+ Gtk::RadioButton anchorObjectPivotRadio;
+ Inkscape::UI::Widget::AnchorSelector anchorSelector;
+
+ Gtk::Label arrangeOnLabel;
+
+ Gtk::RadioButtonGroup arrangeRadioGroup;
+ Gtk::RadioButton arrangeOnFirstCircleRadio;
+ Gtk::RadioButton arrangeOnLastCircleRadio;
+ Gtk::RadioButton arrangeOnParametersRadio;
+
+ Gtk::Grid parametersTable;
+
+ Gtk::Label centerLabel;
+ Inkscape::UI::Widget::ScalarUnit centerY;
+ Inkscape::UI::Widget::ScalarUnit centerX;
+
+ Gtk::Label radiusLabel;
+ Inkscape::UI::Widget::ScalarUnit radiusY;
+ Inkscape::UI::Widget::ScalarUnit radiusX;
+
+ Gtk::Label angleLabel;
+ Inkscape::UI::Widget::ScalarUnit angleY;
+ Inkscape::UI::Widget::ScalarUnit angleX;
+
+ Gtk::CheckButton rotateObjectsCheckBox;
+
+
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif /* INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_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/dialog/print.cpp b/src/ui/dialog/print.cpp
new file mode 100644
index 0000000..da35219
--- /dev/null
+++ b/src/ui/dialog/print.cpp
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Print dialog.
+ */
+/* Authors:
+ * Kees Cook <kees@outflux.net>
+ * Abhishek Sharma
+ * Patrick McDermott
+ *
+ * Copyright (C) 2007 Kees Cook
+ * Copyright (C) 2017 Patrick McDermott
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cmath>
+
+#include <gtkmm.h>
+
+#include "inkscape.h"
+#include "preferences.h"
+#include "print.h"
+
+#include "extension/internal/cairo-render-context.h"
+#include "extension/internal/cairo-renderer.h"
+#include "document.h"
+#include "object/sp-page.h"
+
+#include "util/units.h"
+#include "helper/png-write.h"
+#include "page-manager.h"
+#include "svg/svg-color.h"
+
+#include <glibmm/i18n.h>
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Glib::RefPtr<Gtk::PrintSettings> &get_printer_settings()
+{
+ static Glib::RefPtr<Gtk::PrintSettings> printer_settings;
+ return printer_settings;
+}
+
+Print::Print(SPDocument *doc, SPItem *base) :
+ _doc (doc),
+ _base (base)
+{
+ g_assert (_doc);
+ g_assert (_base);
+
+ _printop = Gtk::PrintOperation::create();
+
+ // set up dialog title, based on document name
+ const Glib::ustring jobname = _doc->getDocumentName() ? _doc->getDocumentName() : _("SVG Document");
+ Glib::ustring title = _("Print");
+ title += " ";
+ title += jobname;
+ _printop->set_job_name(title);
+
+ _printop->set_unit(Gtk::UNIT_POINTS);
+ Glib::RefPtr<Gtk::PageSetup> page_setup = Gtk::PageSetup::create();
+
+ // Default to a custom paper size, in case we can't find a more specific size
+ set_paper_size(page_setup, _doc->getWidth().value("pt"), _doc->getHeight().value("pt"));
+ _printop->set_default_page_setup(page_setup);
+ _printop->set_use_full_page(true);
+ _printop->set_n_pages(1);
+
+ // Now process actual multi-page setup.
+ auto &pm = _doc->getPageManager();
+ if (pm.hasPages()) {
+ // This appears to be limiting which pages get rendered
+ _printop->set_n_pages(pm.getPageCount());
+ _printop->set_current_page(pm.getSelectedPageIndex());
+ _printop->signal_request_page_setup().connect(sigc::mem_fun(*this, &Print::setup_page));
+ }
+
+ // set up signals
+ _workaround._doc = _doc;
+ _workaround._base = _base;
+ _workaround._tab = &_tab;
+ _printop->signal_create_custom_widget().connect(sigc::mem_fun(*this, &Print::create_custom_widget));
+ _printop->signal_begin_print().connect(sigc::mem_fun(*this, &Print::begin_print));
+ _printop->signal_draw_page().connect(sigc::mem_fun(*this, &Print::draw_page));
+
+ // build custom preferences tab
+ _printop->set_custom_tab_label(_("Rendering"));
+}
+
+/**
+ * Return the required page setup, only connected for multi-page documents
+ * and only required where there are pages of different sizes.
+ */
+void Print::setup_page(const Glib::RefPtr<Gtk::PrintContext>& context, int page_nr,
+ const Glib::RefPtr<Gtk::PageSetup> &setup)
+{
+ auto &pm = _workaround._doc->getPageManager();
+ if (auto page = pm.getPage(page_nr)) {
+ auto rect = page->getDesktopRect();
+ auto width = Inkscape::Util::Quantity::convert(rect.width(), "px", "pt");
+ auto height = Inkscape::Util::Quantity::convert(rect.height(), "px", "pt");
+ set_paper_size(setup, width, height);
+ }
+}
+
+/**
+ * Set the paper size with correct orientation.
+ */
+void Print::set_paper_size(const Glib::RefPtr<Gtk::PageSetup> &page_setup, double page_width, double page_height)
+{
+ auto p_size = Gtk::PaperSize("custom", "custom", page_width, page_height, Gtk::UNIT_POINTS);
+
+ // Some print drivers, like the EPSON's ESC/P-R CUPS driver, don't accept custom
+ // page sizes, so we'll try to find a known page size.
+ // GTK+'s known paper sizes always have a longer height than width, so we'll rotate
+ // the page and set its orientation to landscape as necessary in order to match a paper size.
+ // Unfortunately, some printers, like Epilog laser cutters, don't understand landscape
+ // mode.
+ // As a compromise, we'll only rotate the page if we actually find a matching paper size,
+ // since laser cutter beds tend to be custom sizes.
+ Gtk::PageOrientation orientation = Gtk::PAGE_ORIENTATION_PORTRAIT;
+ if (page_width > page_height) {
+ orientation = Gtk::PAGE_ORIENTATION_REVERSE_LANDSCAPE;
+ std::swap(page_width, page_height);
+ }
+
+ // attempt to match document size against known paper sizes
+ std::vector<Gtk::PaperSize> known_sizes = Gtk::PaperSize::get_paper_sizes(false);
+ for (auto& size : known_sizes) {
+ if (fabs(size.get_width(Gtk::UNIT_POINTS) - page_width) >= 1.0) {
+ // width (short edge) doesn't match
+ continue;
+ }
+ if (fabs(size.get_height(Gtk::UNIT_POINTS) - page_height) >= 1.0) {
+ // height (short edge) doesn't match
+ continue;
+ }
+ // size matches
+ p_size = size;
+ break;
+ }
+ page_setup->set_paper_size(p_size);
+ page_setup->set_orientation(orientation);
+}
+
+void Print::draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int page_nr)
+{
+ // TODO: If the user prints multiple copies we render the whole page for each copy
+ // It would be more efficient to render the page once (e.g. in "begin_print")
+ // and simply print this result as often as necessary
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ //printf("%s %d\n",__FUNCTION__, page_nr);
+
+ auto &pm = _workaround._doc->getPageManager();
+ auto page = pm.getPage(page_nr); // nullptr when no pages.
+
+ if (_workaround._tab->as_bitmap()) {
+ // Render as exported PNG
+ prefs->setBool("/dialogs/printing/asbitmap", true);
+ gdouble dpi = _workaround._tab->bitmap_dpi();
+ prefs->setDouble("/dialogs/printing/dpi", dpi);
+
+ auto rect = *(_workaround._doc->preferredBounds());
+ if (page) {
+ rect = page->getDesktopRect();
+ }
+
+ std::string tmp_png;
+ std::string tmp_base = "inkscape-print-png-XXXXXX";
+
+ int tmp_fd;
+ if ( (tmp_fd = Glib::file_open_tmp(tmp_png, tmp_base)) >= 0) {
+ close(tmp_fd);
+
+ guint32 bgcolor = 0x00000000;
+ Inkscape::XML::Node *nv = _workaround._doc->getReprNamedView();
+ if (nv && nv->attribute("pagecolor")){
+ bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00);
+ }
+ if (nv && nv->attribute("inkscape:pageopacity")){
+ double opacity = nv->getAttributeDouble("inkscape:pageopacity", 1.0);
+ bgcolor |= SP_COLOR_F_TO_U(opacity);
+ }
+
+ sp_export_png_file(_workaround._doc, tmp_png.c_str(), rect,
+ (unsigned long)(Inkscape::Util::Quantity::convert(rect.width(), "px", "in") * dpi),
+ (unsigned long)(Inkscape::Util::Quantity::convert(rect.height(), "px", "in") * dpi),
+ dpi, dpi, bgcolor, nullptr, nullptr, true, std::vector<SPItem*>());
+
+ // This doesn't seem to work:
+ //context->set_cairo_context ( Cairo::Context::create (Cairo::ImageSurface::create_from_png (tmp_png) ), dpi, dpi );
+ //
+ // so we'll use a surface pattern blat instead...
+ //
+ // but the C++ interface isn't implemented in cairomm:
+ //context->get_cairo_context ()->set_source_surface(Cairo::ImageSurface::create_from_png (tmp_png) );
+ //
+ // so do it in C:
+ {
+ auto png = Cairo::ImageSurface::create_from_png(tmp_png);
+ auto pattern = Cairo::SurfacePattern::create(png);
+ auto cr = context->get_cairo_context();
+ auto m = cr->get_matrix();
+ cr->scale(Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi,
+ Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi);
+ // FIXME: why is the origin offset??
+ cr->set_source(pattern);
+ cr->paint();
+ cr->set_matrix(m);
+ }
+
+ // Clean up
+ unlink (tmp_png.c_str());
+ }
+ else {
+ g_warning("%s", _("Could not open temporary PNG for bitmap printing"));
+ }
+ }
+ else {
+ // Render as vectors
+ prefs->setBool("/dialogs/printing/asbitmap", false);
+ Inkscape::Extension::Internal::CairoRenderer renderer;
+ Inkscape::Extension::Internal::CairoRenderContext *ctx = renderer.createContext();
+
+ // ctx->setPSLevel(CAIRO_PS_LEVEL_3);
+ ctx->setTextToPath(false);
+ ctx->setFilterToBitmap(true);
+ ctx->setBitmapResolution(72);
+
+ auto cr = context->get_cairo_context();
+ auto surface = cr->get_target();
+ auto ctm = cr->get_matrix();
+
+ bool ret = ctx->setSurfaceTarget(surface->cobj(), true, &ctm);
+ if (ret) {
+ ret = renderer.setupDocument (ctx, _workaround._doc);
+ if (ret) {
+ if (auto page = pm.getPage(page_nr)) {
+ renderer.renderPage(ctx, _workaround._doc, page, false);
+ } else {
+ renderer.renderItem(ctx, _workaround._base);
+ }
+ ctx->finish(false); // do not finish the cairo_surface_t - it's owned by our GtkPrintContext!
+ }
+ else {
+ g_warning("%s", _("Could not set up Document"));
+ }
+ }
+ else {
+ g_warning("%s", _("Failed to set CairoRenderContext"));
+ }
+
+ // Clean up
+ renderer.destroyContext(ctx);
+ }
+
+}
+
+Gtk::Widget *Print::create_custom_widget()
+{
+ return &_tab;
+}
+
+void Print::begin_print(const Glib::RefPtr<Gtk::PrintContext>&)
+{
+ // Could change which pages get printed here, but nothing to do.
+}
+
+Gtk::PrintOperationResult Print::run(Gtk::PrintOperationAction, Gtk::Window &parent_window)
+{
+ // Remember to restore the previous print settings
+ _printop->set_print_settings(get_printer_settings());
+
+ try {
+ Gtk::PrintOperationResult res = _printop->run(Gtk::PRINT_OPERATION_ACTION_PRINT_DIALOG, parent_window);
+
+ // Save printer settings (but only on success)
+ if (res == Gtk::PRINT_OPERATION_RESULT_APPLY) {
+ get_printer_settings() = _printop->get_print_settings();
+ }
+
+ return res;
+ } catch (const Glib::Error &e) {
+ g_warning("Failed to print '%s': %s", _doc->getDocumentName(), e.what().c_str());
+ }
+
+ return Gtk::PRINT_OPERATION_RESULT_ERROR;
+}
+
+
+} // 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/dialog/print.h b/src/ui/dialog/print.h
new file mode 100644
index 0000000..cbbee41
--- /dev/null
+++ b/src/ui/dialog/print.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Print dialog
+ */
+/* Authors:
+ * Kees Cook <kees@outflux.net>
+ *
+ * Copyright (C) 2007 Kees Cook
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_PRINT_H
+#define INKSCAPE_UI_DIALOG_PRINT_H
+
+#include "ui/widget/rendering-options.h"
+#include <gtkmm/printoperation.h> // GtkMM
+
+class SPItem;
+class SPDocument;
+
+
+/*
+ * gtk 2.12.0 has a bug (http://bugzilla.gnome.org/show_bug.cgi?id=482089)
+ * where it fails to correctly deal with gtkmm signal management. As a result
+ * we have call gtk directly instead of doing a much cleaner version of
+ * this printing dialog, using full gtkmmification. (The bug was fixed
+ * in 2.12.1, so when the Inkscape gtk minimum version is bumped there,
+ * we can revert Inkscape commit 16865.
+ */
+struct workaround_gtkmm
+{
+ SPDocument *_doc;
+ SPItem *_base;
+ Inkscape::UI::Widget::RenderingOptions *_tab;
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class Print {
+public:
+ Print(SPDocument *doc, SPItem *base);
+ Gtk::PrintOperationResult run(Gtk::PrintOperationAction, Gtk::Window &parent_window);
+
+protected:
+
+private:
+ void set_paper_size(const Glib::RefPtr<Gtk::PageSetup> &, double width, double height);
+
+ Glib::RefPtr<Gtk::PrintOperation> _printop;
+ SPDocument *_doc;
+ SPItem *_base;
+ Inkscape::UI::Widget::RenderingOptions _tab;
+
+ struct workaround_gtkmm _workaround;
+
+ void setup_page(const Glib::RefPtr<Gtk::PrintContext>& context, int page_nr,
+ const Glib::RefPtr<Gtk::PageSetup> &setup);
+ void draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int page_nr);
+ Gtk::Widget *create_custom_widget();
+ void begin_print(const Glib::RefPtr<Gtk::PrintContext>&);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_PRINT_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/dialog/prototype.cpp b/src/ui/dialog/prototype.cpp
new file mode 100644
index 0000000..d0ee467
--- /dev/null
+++ b/src/ui/dialog/prototype.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A bare minimum example of deriving from Inkscape::UI:Widget::Panel.
+ *
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef DEBUG
+
+#include "prototype.h"
+
+#include "document.h"
+#include "inkscape-application.h"
+
+// Only for use in demonstration widget.
+#include "object/sp-root.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Prototype::Prototype()
+ : DialogBase("/dialogs/prototype", "Prototype")
+{
+ // A widget for demonstration that displays the current SVG's id.
+ _label = Gtk::make_managed<Gtk::Label>(_name);
+ _label->set_line_wrap();
+
+ _debug_button.set_name("PrototypeDebugButton");
+ _debug_button.set_hexpand();
+ _debug_button.signal_clicked().connect(sigc::mem_fun(*this, &Prototype::on_click));
+
+ _debug_button.add(*_label);
+ add(_debug_button);
+}
+
+void Prototype::documentReplaced()
+{
+ if (document && document->getRoot()) {
+ const gchar *root_id = document->getRoot()->getId();
+ Glib::ustring label_string("Document's SVG id: ");
+ label_string += (root_id ? root_id : "null");
+ _label->set_label(label_string);
+ }
+}
+
+void Prototype::selectionChanged(Inkscape::Selection *selection)
+{
+ if (!selection) {
+ return;
+ }
+
+ // Update demonstration widget.
+ Glib::ustring label = _label->get_text() + "\nSelection changed to ";
+ SPObject* object = selection->single();
+ if (object) {
+ label = label + object->getId();
+ } else {
+ object = selection->activeContext();
+
+ if (object) {
+ label = label + object->getId();
+ } else {
+ label = label + "unknown";
+ }
+ }
+
+ _label->set_label(label);
+}
+
+void Prototype::on_click()
+{
+ auto window = dynamic_cast<Gtk::Window*>(get_toplevel());
+ if (window) {
+ std::cerr << "Dialog is part of: " << window->get_name() << " (" << window->get_title() << ")" << std::endl;
+ } else {
+ std::cerr << "Prototype::on_click(): Dialog not attached to window!" << std::endl;
+ }
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // DEBUG
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/prototype.h b/src/ui/dialog/prototype.h
new file mode 100644
index 0000000..1f36a73
--- /dev/null
+++ b/src/ui/dialog/prototype.h
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A bare minimum example of deriving from Inkscape::UI:Widget::Panel.
+ *
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) Tavmjong Bah <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_PROTOTYPE_PANEL_H
+#define SEEN_PROTOTYPE_PANEL_H
+
+#ifdef DEBUG
+
+#include <iostream>
+
+#include "selection.h"
+#include "ui/dialog/dialog-base.h"
+
+// Only to display status.
+#include <gtkmm/label.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * A panel that does almost nothing!
+ */
+class Prototype : public DialogBase
+{
+public:
+ Prototype();
+ ~Prototype() override { std::cerr << "Prototype::~Prototype()" << std::endl; }
+
+ void documentReplaced(SPDocument *document) override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+
+private:
+ // Just for example
+ Gtk::Label *_label;
+ Gtk::Button _debug_button; // For printing to console.
+
+ virtual void on_click();
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // DEBUG
+
+#endif // SEEN_PROTOTYPE_PANEL_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/dialog/save-template-dialog.cpp b/src/ui/dialog/save-template-dialog.cpp
new file mode 100644
index 0000000..a240f61
--- /dev/null
+++ b/src/ui/dialog/save-template-dialog.cpp
@@ -0,0 +1,86 @@
+// 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 "save-template-dialog.h"
+#include "file.h"
+#include "io/resource.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/window.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+SaveTemplate::SaveTemplate(Gtk::Window &parent) {
+
+ std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-save-template.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("GtkBuilder file loading failed for save template dialog");
+ return;
+ }
+
+ builder->get_widget("dialog", dialog);
+ builder->get_widget("name", name);
+ builder->get_widget("author", author);
+ builder->get_widget("description", description);
+ builder->get_widget("keywords", keywords);
+ builder->get_widget("set-default", set_default_template);
+
+ name->signal_changed().connect(sigc::mem_fun(*this, &SaveTemplate::on_name_changed));
+
+ dialog->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL);
+ dialog->add_button(_("Save"), Gtk::RESPONSE_OK);
+
+ dialog->set_response_sensitive(Gtk::RESPONSE_OK, false);
+ dialog->set_default_response(Gtk::RESPONSE_CANCEL);
+
+ dialog->set_transient_for(parent);
+ dialog->show_all();
+}
+
+void SaveTemplate::on_name_changed() {
+
+ bool has_text = name->get_text_length() != 0;
+ dialog->set_response_sensitive(Gtk::RESPONSE_OK, has_text);
+}
+
+void SaveTemplate::save_template(Gtk::Window &parent) {
+
+ sp_file_save_template(parent, name->get_text(), author->get_text(), description->get_text(),
+ keywords->get_text(), set_default_template->get_active());
+}
+
+void SaveTemplate::save_document_as_template(Gtk::Window &parent) {
+
+ SaveTemplate dialog(parent);
+ int response = dialog.dialog->run();
+
+ switch (response) {
+ case Gtk::RESPONSE_OK:
+ dialog.save_template(parent);
+ break;
+ default:
+ break;
+ }
+
+ dialog.dialog->close();
+}
+
+}
+}
+}
diff --git a/src/ui/dialog/save-template-dialog.h b/src/ui/dialog/save-template-dialog.h
new file mode 100644
index 0000000..eb702ca
--- /dev/null
+++ b/src/ui/dialog/save-template-dialog.h
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2017 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_SEEN_UI_DIALOG_SAVE_TEMPLATE_H
+#define INKSCAPE_SEEN_UI_DIALOG_SAVE_TEMPLATE_H
+
+#include <glibmm/refptr.h>
+
+namespace Gtk {
+class Builder;
+class CheckButton;
+class Dialog;
+class Entry;
+class Window;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class SaveTemplate
+{
+
+public:
+
+ static void save_document_as_template(Gtk::Window &parentWindow);
+
+protected:
+
+ void on_name_changed();
+
+private:
+
+ Gtk::Dialog *dialog;
+
+ Gtk::Entry *name;
+ Gtk::Entry *author;
+ Gtk::Entry *description;
+ Gtk::Entry *keywords;
+
+ Gtk::CheckButton *set_default_template;
+
+ SaveTemplate(Gtk::Window &parent);
+ void save_template(Gtk::Window &parent);
+
+};
+}
+}
+}
+#endif
diff --git a/src/ui/dialog/selectorsdialog.cpp b/src/ui/dialog/selectorsdialog.cpp
new file mode 100644
index 0000000..fd448a9
--- /dev/null
+++ b/src/ui/dialog/selectorsdialog.cpp
@@ -0,0 +1,1340 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for CSS selectors
+ */
+/* Authors:
+ * Kamalpreet Kaur Grewal
+ * Tavmjong Bah
+ * Jabiertxof
+ *
+ * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com>
+ * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "selectorsdialog.h"
+
+#include <map>
+#include <regex>
+#include <utility>
+
+#include <glibmm/i18n.h>
+#include <glibmm/regex.h>
+
+#include "attribute-rel-svg.h"
+#include "document-undo.h"
+#include "inkscape.h"
+#include "selection.h"
+#include "style.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/widget/iconrenderer.h"
+
+#include "util/trim.h"
+
+#include "xml/attribute-record.h"
+#include "xml/node-observer.h"
+#include "xml/sp-css-attr.h"
+
+
+// G_MESSAGES_DEBUG=DEBUG_SELECTORSDIALOG gdb ./inkscape
+// #define DEBUG_SELECTORSDIALOG
+// #define G_LOG_DOMAIN "SELECTORSDIALOG"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+// Keeps a watch on style element
+class SelectorsDialog::NodeObserver : public Inkscape::XML::NodeObserver
+{
+public:
+ NodeObserver(SelectorsDialog *selectorsdialog)
+ : _selectorsdialog(selectorsdialog)
+ {
+ g_debug("SelectorsDialog::NodeObserver: Constructor");
+ };
+
+ void notifyContentChanged(Inkscape::XML::Node &node,
+ Inkscape::Util::ptr_shared old_content,
+ Inkscape::Util::ptr_shared new_content) override;
+
+ SelectorsDialog *_selectorsdialog;
+};
+
+void SelectorsDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node &,
+ Inkscape::Util::ptr_shared,
+ Inkscape::Util::ptr_shared)
+{
+ g_debug("SelectorsDialog::NodeObserver::notifyContentChanged");
+ _selectorsdialog->_scrollock = true;
+ _selectorsdialog->_updating = false;
+ _selectorsdialog->_readStyleElement();
+ _selectorsdialog->_selectRow();
+}
+
+// Keeps a watch for new/removed/changed nodes
+// (Must update objects that selectors match.)
+class SelectorsDialog::NodeWatcher : public Inkscape::XML::NodeObserver
+{
+public:
+ NodeWatcher(SelectorsDialog *selectorsdialog)
+ : _selectorsdialog(selectorsdialog)
+ {
+ g_debug("SelectorsDialog::NodeWatcher: Constructor");
+ };
+
+ void notifyChildAdded(Inkscape::XML::Node &,
+ Inkscape::XML::Node &child,
+ Inkscape::XML::Node *) override
+ {
+ _selectorsdialog->_nodeAdded(child);
+ }
+
+ void notifyChildRemoved(Inkscape::XML::Node &,
+ Inkscape::XML::Node &child,
+ Inkscape::XML::Node *) override
+ {
+ _selectorsdialog->_nodeRemoved(child);
+ }
+
+ void notifyAttributeChanged(Inkscape::XML::Node &node,
+ GQuark qname,
+ Util::ptr_shared,
+ Util::ptr_shared) override
+ {
+ static GQuark const CODE_id = g_quark_from_static_string("id");
+ static GQuark const CODE_class = g_quark_from_static_string("class");
+
+ if (qname == CODE_id || qname == CODE_class) {
+ _selectorsdialog->_nodeChanged(node);
+ }
+ }
+
+ SelectorsDialog *_selectorsdialog;
+};
+
+void SelectorsDialog::_nodeAdded(Inkscape::XML::Node &node)
+{
+ _readStyleElement();
+ _selectRow();
+}
+
+void SelectorsDialog::_nodeRemoved(Inkscape::XML::Node &repr)
+{
+ if (_textNode == &repr) {
+ _textNode = nullptr;
+ }
+
+ _readStyleElement();
+ _selectRow();
+}
+
+void SelectorsDialog::_nodeChanged(Inkscape::XML::Node &object)
+{
+ g_debug("SelectorsDialog::NodeChanged");
+
+ _scrollock = true;
+
+ _readStyleElement();
+ _selectRow();
+}
+
+SelectorsDialog::TreeStore::TreeStore() = default;
+
+/**
+ * Allow dragging only selectors.
+ */
+bool SelectorsDialog::TreeStore::row_draggable_vfunc(const Gtk::TreeModel::Path &path) const
+{
+ g_debug("SelectorsDialog::TreeStore::row_draggable_vfunc");
+
+ auto unconstThis = const_cast<SelectorsDialog::TreeStore *>(this);
+ const_iterator iter = unconstThis->get_iter(path);
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ bool is_draggable = row[_selectorsdialog->_mColumns._colType] == SELECTOR;
+ return is_draggable;
+ }
+ return Gtk::TreeStore::row_draggable_vfunc(path);
+}
+
+/**
+ * Allow dropping only in between other selectors.
+ */
+bool SelectorsDialog::TreeStore::row_drop_possible_vfunc(const Gtk::TreeModel::Path &dest,
+ const Gtk::SelectionData &selection_data) const
+{
+ g_debug("SelectorsDialog::TreeStore::row_drop_possible_vfunc");
+
+ Gtk::TreeModel::Path dest_parent = dest;
+ dest_parent.up();
+ return dest_parent.empty();
+}
+
+
+// This is only here to handle updating style element after a drag and drop.
+void SelectorsDialog::TreeStore::on_row_deleted(const TreeModel::Path &path)
+{
+ if (_selectorsdialog->_updating)
+ return; // Don't write if we deleted row (other than from DND)
+
+ g_debug("on_row_deleted");
+ _selectorsdialog->_writeStyleElement();
+ _selectorsdialog->_readStyleElement();
+}
+
+
+Glib::RefPtr<SelectorsDialog::TreeStore> SelectorsDialog::TreeStore::create(SelectorsDialog *selectorsdialog)
+{
+ g_debug("SelectorsDialog::TreeStore::create");
+
+ SelectorsDialog::TreeStore *store = new SelectorsDialog::TreeStore();
+ store->_selectorsdialog = selectorsdialog;
+ store->set_column_types(store->_selectorsdialog->_mColumns);
+ return Glib::RefPtr<SelectorsDialog::TreeStore>(store);
+}
+
+/**
+ * Constructor
+ * A treeview and a set of two buttons are added to the dialog. _addSelector
+ * adds selectors to treeview. _delSelector deletes the selector from the dialog.
+ * Any addition/deletion of the selectors updates XML style element accordingly.
+ */
+SelectorsDialog::SelectorsDialog()
+ : DialogBase("/dialogs/selectors", "Selectors")
+{
+ g_debug("SelectorsDialog::SelectorsDialog");
+
+ m_nodewatcher = std::make_unique<NodeWatcher>(this);
+ m_styletextwatcher = std::make_unique<NodeObserver>(this);
+
+ // Tree
+ Inkscape::UI::Widget::IconRenderer * addRenderer = manage(
+ new Inkscape::UI::Widget::IconRenderer() );
+ addRenderer->add_icon("edit-delete");
+ addRenderer->add_icon("list-add");
+ addRenderer->add_icon("empty-icon");
+ _store = TreeStore::create(this);
+ _treeView.set_model(_store);
+
+ // ALWAYS be a single selection widget
+ _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE);
+
+ _treeView.set_headers_visible(false);
+ _treeView.enable_model_drag_source();
+ _treeView.enable_model_drag_dest( Gdk::ACTION_MOVE );
+ int addCol = _treeView.append_column("", *addRenderer) - 1;
+ Gtk::TreeViewColumn *col = _treeView.get_column(addCol);
+ if ( col ) {
+ col->add_attribute(addRenderer->property_icon(), _mColumns._colType);
+ }
+
+ Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText());
+ addCol = _treeView.append_column("CSS Selector", *label) - 1;
+ col = _treeView.get_column(addCol);
+ if (col) {
+ col->add_attribute(label->property_text(), _mColumns._colSelector);
+ col->add_attribute(label->property_weight(), _mColumns._colSelected);
+ }
+ _treeView.set_expander_column(*(_treeView.get_column(1)));
+
+
+ // Signal handlers
+ _treeView.signal_button_release_event().connect( // Needs to be release, not press.
+ sigc::mem_fun(*this, &SelectorsDialog::_handleButtonEvent), false);
+
+ _treeView.signal_button_release_event().connect_notify(
+ sigc::mem_fun(*this, &SelectorsDialog::_buttonEventsSelectObjs), false);
+
+ _treeView.signal_row_expanded().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowExpand));
+
+ _treeView.signal_row_collapsed().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowCollapse));
+
+ _showWidgets();
+
+ show_all();
+}
+
+
+void SelectorsDialog::_vscroll()
+{
+ if (!_scrollock) {
+ _scrollpos = _vadj->get_value();
+ } else {
+ _vadj->set_value(_scrollpos);
+ _scrollock = false;
+ }
+}
+
+void SelectorsDialog::_showWidgets()
+{
+ // Pack widgets
+ g_debug("SelectorsDialog::_showWidgets");
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool dir = prefs->getBool("/dialogs/selectors/vertical", true);
+ _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
+ _selectors_box.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ _selectors_box.set_name("SelectorsDialog");
+ _scrolled_window_selectors.add(_treeView);
+ _scrolled_window_selectors.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _scrolled_window_selectors.set_overlay_scrolling(false);
+ _vadj = _scrolled_window_selectors.get_vadjustment();
+ _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_vscroll));
+ _selectors_box.pack_start(_scrolled_window_selectors, Gtk::PACK_EXPAND_WIDGET);
+ /* Gtk::Label *dirtogglerlabel = Gtk::manage(new Gtk::Label(_("Paned vertical")));
+ dirtogglerlabel->get_style_context()->add_class("inksmall");
+ _direction.property_active() = dir;
+ _direction.property_active().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection));
+ _direction.get_style_context()->add_class("inkswitch"); */
+ _styleButton(_create, "list-add", "Add a new CSS Selector");
+ _create.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_addSelector));
+ _styleButton(_del, "list-remove", "Remove a CSS Selector");
+ _button_box.pack_start(_create, Gtk::PACK_SHRINK);
+ _button_box.pack_start(_del, Gtk::PACK_SHRINK);
+ Gtk::RadioButton::Group group;
+ Gtk::RadioButton *_horizontal = Gtk::manage(new Gtk::RadioButton());
+ Gtk::RadioButton *_vertical = Gtk::manage(new Gtk::RadioButton());
+ _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal"));
+ _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical"));
+ _horizontal->set_group(group);
+ _vertical->set_group(group);
+ _vertical->set_active(dir);
+ _vertical->signal_toggled().connect(
+ sigc::bind(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection), _vertical));
+ _horizontal->property_draw_indicator() = false;
+ _vertical->property_draw_indicator() = false;
+ _button_box.pack_end(*_horizontal, false, false, 0);
+ _button_box.pack_end(*_vertical, false, false, 0);
+ _del.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_delSelector));
+ _del.hide();
+ _style_dialog = Gtk::make_managed<StyleDialog>();
+ _style_dialog->set_name("StyleDialog");
+ _paned.pack1(*_style_dialog, Gtk::SHRINK);
+ _paned.pack2(_selectors_box, true, true);
+ _paned.set_wide_handle(true);
+ Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ contents->pack_start(_paned, Gtk::PACK_EXPAND_WIDGET);
+ contents->pack_start(_button_box, false, false, 0);
+ contents->set_valign(Gtk::ALIGN_FILL);
+ contents->child_property_fill(_paned);
+ pack_start(*contents, Gtk::PACK_EXPAND_WIDGET);
+ show_all();
+ _updating = true;
+ _paned.property_position() = 200;
+ _updating = false;
+ set_size_request(320, -1);
+ set_name("SelectorsAndStyleDialog");
+}
+
+void SelectorsDialog::_toggleDirection(Gtk::RadioButton *vertical)
+{
+ g_debug("SelectorsDialog::_toggleDirection");
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool dir = vertical->get_active();
+ prefs->setBool("/dialogs/selectors/vertical", dir);
+ _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
+ _paned.check_resize();
+ int widthpos = _paned.property_max_position() - _paned.property_min_position();
+ prefs->setInt("/dialogs/selectors/panedpos", widthpos / 2);
+ _paned.property_position() = widthpos / 2;
+}
+
+/**
+ * @return Inkscape::XML::Node* pointing to a style element's text node.
+ * Returns the style element's text node. If there is no style element, one is created.
+ * Ditto for text node.
+ */
+Inkscape::XML::Node *SelectorsDialog::_getStyleTextNode(bool create_if_missing)
+{
+ g_debug("SelectorsDialog::_getStyleTextNode");
+
+ auto textNode = Inkscape::get_first_style_text_node(m_root, create_if_missing);
+
+ if (_textNode != textNode) {
+ if (_textNode) {
+ _textNode->removeObserver(*m_styletextwatcher);
+ }
+
+ _textNode = textNode;
+
+ if (_textNode) {
+ _textNode->addObserver(*m_styletextwatcher);
+ }
+ }
+
+ return textNode;
+}
+
+/**
+ * Fill the Gtk::TreeStore from the svg:style element.
+ */
+void SelectorsDialog::_readStyleElement()
+{
+ g_debug("SelectorsDialog::_readStyleElement(): updating %s", (_updating ? "true" : "false"));
+
+ if (_updating) return; // Don't read if we wrote style element.
+ _updating = true;
+ _scrollock = true;
+ Inkscape::XML::Node * textNode = _getStyleTextNode();
+
+ // Get content from style text node.
+ std::string content = (textNode && textNode->content()) ? textNode->content() : "";
+
+ // Remove end-of-lines (check it works on Windoze).
+ content.erase(std::remove(content.begin(), content.end(), '\n'), content.end());
+
+ // Remove comments (/* xxx */)
+#if 0
+ while(content.find("/*") != std::string::npos) {
+ size_t start = content.find("/*");
+ content.erase(start, (content.find("*\/", start) - start) +2);
+ }
+#endif
+
+ // First split into selector/value chunks.
+ // An attempt to use Glib::Regex failed. A C++11 version worked but
+ // reportedly has problems on Windows. Using split_simple() is simpler
+ // and probably faster.
+ //
+ // Glib::RefPtr<Glib::Regex> regex1 =
+ // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}");
+ //
+ // Glib::MatchInfo minfo;
+ // regex1->match(content, minfo);
+
+ // Split on curly brackets. Even tokens are selectors, odd are values.
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", content);
+
+ // If text node is empty, return (avoids problem with negative below).
+ if (tokens.size() == 0) {
+ _store->clear();
+ _updating = false;
+ return;
+ }
+ _treeView.show_all();
+ std::vector<std::pair<Glib::ustring, bool>> expanderstatus;
+ for (unsigned i = 0; i < tokens.size() - 1; i += 2) {
+ Glib::ustring selector = tokens[i];
+ Util::trim(selector, ","); // Remove leading/trailing spaces and commas
+ std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector);
+ if (!selectordata.empty()) {
+ selector = selectordata.back();
+ }
+ selector = _style_dialog->fixCSSSelectors(selector);
+ for (auto &row : _store->children()) {
+ Glib::ustring selectorold = row[_mColumns._colSelector];
+ if (selectorold == selector) {
+ expanderstatus.emplace_back(selector, row[_mColumns._colExpand]);
+ }
+ }
+ }
+ _store->clear();
+ bool rewrite = false;
+
+
+ for (unsigned i = 0; i < tokens.size()-1; i += 2) {
+ Glib::ustring selector = tokens[i];
+ Util::trim(selector, ","); // Remove leading/trailing spaces and commas
+ std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector);
+ for (auto selectoritem : selectordata) {
+ if (selectordata[selectordata.size() - 1] == selectoritem) {
+ selector = selectoritem;
+ } else {
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_mColumns._colSelector] = selectoritem + ";";
+ row[_mColumns._colExpand] = false;
+ row[_mColumns._colType] = OTHER;
+ row[_mColumns._colObj] = nullptr;
+ row[_mColumns._colProperties] = "";
+ row[_mColumns._colVisible] = true;
+ row[_mColumns._colSelected] = 400;
+ }
+ }
+ Glib::ustring selector_old = selector;
+ selector = _style_dialog->fixCSSSelectors(selector);
+ if (selector_old != selector) {
+ rewrite = true;
+ }
+
+ if (selector.empty() || selector == "* > .inkscapehacktmp") {
+ continue;
+ }
+ std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[,]+", selector);
+ coltype colType = SELECTOR;
+
+ Glib::ustring properties;
+ // Check to make sure we do have a value to match selector.
+ if ((i+1) < tokens.size()) {
+ properties = tokens[i+1];
+ } else {
+ std::cerr << "SelectorsDialog::_readStyleElement(): Missing values "
+ "for last selector!"
+ << std::endl;
+ }
+ Util::trim(properties);
+ bool colExpand = false;
+ for (auto rowstatus : expanderstatus) {
+ if (selector == rowstatus.first) {
+ colExpand = rowstatus.second;
+ }
+ }
+ std::vector<Glib::ustring> properties_data = Glib::Regex::split_simple(";", properties);
+ Gtk::TreeModel::Row row = *(_store->append());
+ row[_mColumns._colSelector] = selector;
+ row[_mColumns._colExpand] = colExpand;
+ row[_mColumns._colType] = colType;
+ row[_mColumns._colObj] = nullptr;
+ row[_mColumns._colProperties] = properties;
+ row[_mColumns._colVisible] = true;
+ row[_mColumns._colSelected] = 400;
+ // Add as children, objects that match selector.
+ for (auto &obj : _getObjVec(selector)) {
+ auto *id = obj->getId();
+ if (!id)
+ continue;
+ Gtk::TreeModel::Row childrow = *(_store->append(row->children()));
+ childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
+ childrow[_mColumns._colExpand] = false;
+ childrow[_mColumns._colType] = colType == OBJECT;
+ childrow[_mColumns._colObj] = obj;
+ childrow[_mColumns._colProperties] = ""; // Unused
+ childrow[_mColumns._colVisible] = true; // Unused
+ childrow[_mColumns._colSelected] = 400;
+ }
+ }
+
+
+ _updating = false;
+ if (rewrite) {
+ _writeStyleElement();
+ }
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
+}
+
+void SelectorsDialog::_rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path)
+{
+ g_debug("SelectorsDialog::_row_expand()");
+ Gtk::TreeModel::Row row = *iter;
+ row[_mColumns._colExpand] = true;
+}
+
+void SelectorsDialog::_rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path)
+{
+ g_debug("SelectorsDialog::_row_collapse()");
+ Gtk::TreeModel::Row row = *iter;
+ row[_mColumns._colExpand] = false;
+}
+/**
+ * Update the content of the style element as selectors (or objects) are added/removed.
+ */
+void SelectorsDialog::_writeStyleElement()
+{
+
+ if (_updating) {
+ return;
+ }
+
+ g_debug("SelectorsDialog::_writeStyleElement");
+
+ _scrollock = true;
+ _updating = true;
+ Glib::ustring styleContent = "";
+ for (auto& row: _store->children()) {
+ Glib::ustring selector = row[_mColumns._colSelector];
+#if 0
+ Util::trim(selector, ",");
+ row[_mColumns._colSelector] = selector;
+#endif
+ if (row[_mColumns._colType] == OTHER) {
+ styleContent = selector + styleContent;
+ } else {
+ styleContent = styleContent + selector + " { " + row[_mColumns._colProperties] + " }\n";
+ }
+ }
+ // We could test if styleContent is empty and then delete the style node here but there is no
+ // harm in keeping it around ...
+ Inkscape::XML::Node *textNode = _getStyleTextNode(true);
+ bool empty = false;
+ if (styleContent.empty()) {
+ empty = true;
+ styleContent = "* > .inkscapehacktmp{}";
+ }
+ textNode->setContent(styleContent.c_str());
+ if (empty) {
+ styleContent = "";
+ textNode->setContent(styleContent.c_str());
+ }
+ textNode->setContent(styleContent.c_str());
+ DocumentUndo::done(SP_ACTIVE_DOCUMENT, _("Edited style element."), INKSCAPE_ICON("dialog-selectors"));
+
+ _updating = false;
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
+ g_debug("SelectorsDialog::_writeStyleElement(): | %s |", styleContent.c_str());
+}
+
+Glib::ustring SelectorsDialog::_getSelectorClasses(Glib::ustring selector)
+{
+ g_debug("SelectorsDialog::_getSelectorClasses");
+
+ std::pair<Glib::ustring, Glib::ustring> result;
+ std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[ ]+", selector);
+ selector = tokensplus[tokensplus.size() - 1];
+ // Erase any comma/space
+ Util::trim(selector, ",");
+ Glib::ustring toparse = Glib::ustring(selector);
+ selector = Glib::ustring("");
+ auto i = toparse.find(".");
+ if (i == std::string::npos) {
+ return "";
+ }
+ if (toparse[0] != '.' && toparse[0] != '#') {
+ i = std::min(toparse.find("#"), toparse.find("."));
+ Glib::ustring tag = toparse.substr(0, i);
+ if (!SPAttributeRelSVG::isSVGElement(tag)) {
+ return selector;
+ }
+ if (i != std::string::npos) {
+ toparse.erase(0, i);
+ }
+ }
+ i = toparse.find("#");
+ if (i != std::string::npos) {
+ toparse.erase(i, 1);
+ }
+ auto j = toparse.find("#");
+ if (j != std::string::npos) {
+ return selector;
+ }
+ if (i != std::string::npos) {
+ toparse.insert(i, "#");
+ if (i) {
+ Glib::ustring post = toparse.substr(0, i);
+ Glib::ustring pre = toparse.substr(i, toparse.size() - i);
+ toparse = pre + post;
+ }
+ auto k = toparse.find(".");
+ if (k != std::string::npos) {
+ toparse = toparse.substr(k, toparse.size() - k);
+ }
+ }
+ return toparse;
+}
+
+/**
+ * @param row
+ * Add selected objects on the desktop to the selector corresponding to 'row'.
+ */
+void SelectorsDialog::_addToSelector(Gtk::TreeModel::Row row)
+{
+ g_debug("SelectorsDialog::_addToSelector: Entrance");
+ if (*row) {
+ // Store list of selected elements on desktop (not to be confused with selector).
+ _updating = true;
+ if (row[_mColumns._colType] == OTHER) {
+ return;
+ }
+ Inkscape::Selection *selection = getDesktop()->getSelection();
+ std::vector<SPObject *> toAddObjVec(selection->objects().begin(), selection->objects().end());
+ Glib::ustring multiselector = row[_mColumns._colSelector];
+ row[_mColumns._colExpand] = true;
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector);
+ for (auto &obj : toAddObjVec) {
+ auto *id = obj->getId();
+ if (!id)
+ continue;
+ for (auto tok : tokens) {
+ Glib::ustring clases = _getSelectorClasses(tok);
+ if (!clases.empty()) {
+ _insertClass(obj, clases);
+ std::vector<SPObject *> currentobjs = _getObjVec(multiselector);
+ bool removeclass = true;
+ for (auto currentobj : currentobjs) {
+ if (g_strcmp0(currentobj->getId(), id) == 0) {
+ removeclass = false;
+ }
+ }
+ if (removeclass) {
+ _removeClass(obj, clases);
+ }
+ }
+ }
+ std::vector<SPObject *> currentobjs = _getObjVec(multiselector);
+ bool insertid = true;
+ for (auto currentobj : currentobjs) {
+ if (g_strcmp0(currentobj->getId(), id) == 0) {
+ insertid = false;
+ }
+ }
+ if (insertid) {
+ multiselector = multiselector + ",#" + id;
+ }
+ Gtk::TreeModel::Row childrow = *(_store->prepend(row->children()));
+ childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
+ childrow[_mColumns._colExpand] = false;
+ childrow[_mColumns._colType] = OBJECT;
+ childrow[_mColumns._colObj] = obj;
+ childrow[_mColumns._colProperties] = ""; // Unused
+ childrow[_mColumns._colVisible] = true; // Unused
+ childrow[_mColumns._colSelected] = 400;
+ }
+ row[_mColumns._colSelector] = multiselector;
+ _updating = false;
+
+ // Add entry to style element
+ for (auto &obj : toAddObjVec) {
+ Glib::ustring css_str = "";
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ SPCSSAttr *css_selector = sp_repr_css_attr_new();
+ sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style"));
+ Glib::ustring selprops = row[_mColumns._colProperties];
+ sp_repr_css_attr_add_from_string(css_selector, selprops.c_str());
+ for (const auto & iter : css_selector->attributeList()) {
+ gchar const *key = g_quark_to_string(iter.key);
+ css->removeAttribute(key);
+ }
+ sp_repr_css_write_string(css, css_str);
+ sp_repr_css_attr_unref(css);
+ sp_repr_css_attr_unref(css_selector);
+ obj->getRepr()->setAttribute("style", css_str);
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ _writeStyleElement();
+ }
+}
+
+/**
+ * @param row
+ * Remove the object corresponding to 'row' from the parent selector.
+ */
+void SelectorsDialog::_removeFromSelector(Gtk::TreeModel::Row row)
+{
+ g_debug("SelectorsDialog::_removeFromSelector: Entrance");
+ if (*row) {
+ _scrollock = true;
+ _updating = true;
+ SPObject *obj = nullptr;
+ Glib::ustring objectLabel = row[_mColumns._colSelector];
+ Gtk::TreeModel::iterator iter = row->parent();
+ if (iter) {
+ Gtk::TreeModel::Row parent = *iter;
+ Glib::ustring multiselector = parent[_mColumns._colSelector];
+ Util::trim(multiselector, ",");
+ obj = _getObjVec(objectLabel)[0];
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector);
+ Glib::ustring selector = "";
+ for (auto tok : tokens) {
+ if (tok.empty()) {
+ continue;
+ }
+ // TODO: handle when other selectors has the removed class applied to maybe not remove
+ Glib::ustring clases = _getSelectorClasses(tok);
+ if (!clases.empty()) {
+ _removeClass(obj, tok, true);
+ }
+ auto i = tok.find(row[_mColumns._colSelector]);
+ if (i == std::string::npos) {
+ selector = selector.empty() ? tok : selector + "," + tok;
+ }
+ }
+ Util::trim(selector);
+ if (selector.empty()) {
+ _store->erase(parent);
+
+ } else {
+ _store->erase(row);
+ parent[_mColumns._colSelector] = selector;
+ parent[_mColumns._colExpand] = true;
+ parent[_mColumns._colObj] = nullptr;
+ }
+ }
+ _updating = false;
+
+ // Add entry to style element
+ _writeStyleElement();
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
+ }
+}
+
+
+/**
+ * @param sel
+ * @return This function returns a comma separated list of ids for objects in input vector.
+ * It is used in creating an 'id' selector. It relies on objects having 'id's.
+ */
+Glib::ustring SelectorsDialog::_getIdList(std::vector<SPObject *> sel)
+{
+ g_debug("SelectorsDialog::_getIdList");
+
+ Glib::ustring str;
+ for (auto& obj: sel) {
+ char const *id = obj->getId();
+ if (id) {
+ if (!str.empty()) {
+ str.append(", ");
+ }
+ str.append("#").append(id);
+ }
+ }
+ return str;
+}
+
+/**
+ * @param selector: a valid CSS selector string.
+ * @return objVec: a vector of pointers to SPObject's the selector matches.
+ * Return a vector of all objects that selector matches.
+ */
+std::vector<SPObject *> SelectorsDialog::_getObjVec(Glib::ustring selector)
+{
+
+ g_debug("SelectorsDialog::_getObjVec: | %s |", selector.c_str());
+
+ g_assert(selector.find(";") == Glib::ustring::npos);
+
+ return getDesktop()->getDocument()->getObjectsBySelector(selector);
+}
+
+
+/**
+ * @param objs: list of objects to insert class
+ * @param class: class to insert
+ * Insert a class name into objects' 'class' attribute.
+ */
+void SelectorsDialog::_insertClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className)
+{
+ g_debug("SelectorsDialog::_insertClass");
+
+ for (auto& obj: objVec) {
+ _insertClass(obj, className);
+ }
+}
+
+/**
+ * @param objs: list of objects to insert class
+ * @param class: class to insert
+ * Insert a class name into objects' 'class' attribute.
+ */
+void SelectorsDialog::_insertClass(SPObject *obj, const Glib::ustring &className)
+{
+ g_debug("SelectorsDialog::_insertClass");
+
+ Glib::ustring classAttr = Glib::ustring("");
+ if (obj->getRepr()->attribute("class")) {
+ classAttr = obj->getRepr()->attribute("class");
+ }
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className);
+ std::sort(tokens.begin(), tokens.end());
+ tokens.erase(std::unique(tokens.begin(), tokens.end()), tokens.end());
+ std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[\\s]+", classAttr);
+ for (auto tok : tokens) {
+ bool exist = false;
+ for (auto &tokenplus : tokensplus) {
+ if (tokenplus == tok) {
+ exist = true;
+ }
+ }
+ if (!exist) {
+ classAttr = classAttr.empty() ? tok : classAttr + " " + tok;
+ }
+ }
+ obj->getRepr()->setAttribute("class", classAttr);
+}
+
+/**
+ * @param objs: list of objects to insert class
+ * @param class: class to insert
+ * Insert a class name into objects' 'class' attribute.
+ */
+void SelectorsDialog::_removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all)
+{
+ g_debug("SelectorsDialog::_removeClass");
+
+ for (auto &obj : objVec) {
+ _removeClass(obj, className, all);
+ }
+}
+
+/**
+ * @param objs: list of objects to insert class
+ * @param class: class to insert
+ * Insert a class name into objects' 'class' attribute.
+ */
+void SelectorsDialog::_removeClass(SPObject *obj, const Glib::ustring &className, bool all) // without "."
+{
+ g_debug("SelectorsDialog::_removeClass");
+
+ if (obj->getRepr()->attribute("class")) {
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className);
+ Glib::ustring classAttr = obj->getRepr()->attribute("class");
+ Glib::ustring classAttrRestore = classAttr;
+ bool notfound = false;
+ for (auto tok : tokens) {
+ auto i = classAttr.find(tok);
+ if (i != std::string::npos) {
+ classAttr.erase(i, tok.length());
+ } else {
+ notfound = true;
+ }
+ }
+ if (all && notfound) {
+ classAttr = classAttrRestore;
+ }
+ Util::trim(classAttr, ",");
+ if (classAttr.empty()) {
+ obj->getRepr()->removeAttribute("class");
+ } else {
+ obj->getRepr()->setAttribute("class", classAttr);
+ }
+ }
+}
+
+
+/**
+ * @param eventX
+ * @param eventY
+ * This function selects objects in the drawing corresponding to the selector
+ * selected in the treeview.
+ */
+void SelectorsDialog::_selectObjects(int eventX, int eventY)
+{
+ g_debug("SelectorsDialog::_selectObjects: %d, %d", eventX, eventY);
+ Gtk::TreeViewColumn *col = _treeView.get_column(1);
+ Gtk::TreeModel::Path path;
+ int x2 = 0;
+ int y2 = 0;
+ // To do: We should be able to do this via passing in row.
+ if (_treeView.get_path_at_pos(eventX, eventY, path, col, x2, y2)) {
+ if (_lastpath.size() && _lastpath == path) {
+ return;
+ }
+ if (col == _treeView.get_column(1) && x2 > 25) {
+ getDesktop()->getSelection()->clear();
+ Gtk::TreeModel::iterator iter = _store->get_iter(path);
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ if (row[_mColumns._colObj]) {
+ getDesktop()->getSelection()->add(row[_mColumns._colObj]);
+ }
+ Gtk::TreeModel::Children children = row.children();
+ if (children.empty() || children.size() == 1) {
+ _del.show();
+ }
+ for (auto child : row.children()) {
+ Gtk::TreeModel::Row child_row = *child;
+ if (child[_mColumns._colObj]) {
+ getDesktop()->getSelection()->add(child[_mColumns._colObj]);
+ }
+ }
+ }
+ _lastpath = path;
+ }
+ }
+}
+
+/**
+ * This function opens a dialog to add a selector. The dialog is prefilled
+ * with an 'id' selector containing a list of the id's of selected objects
+ * or with a 'class' selector if no objects are selected.
+ */
+void SelectorsDialog::_addSelector()
+{
+ g_debug("SelectorsDialog::_addSelector: Entrance");
+ _scrollock = true;
+ // Store list of selected elements on desktop (not to be confused with selector).
+ Inkscape::Selection* selection = getDesktop()->getSelection();
+ std::vector<SPObject *> objVec( selection->objects().begin(),
+ selection->objects().end() );
+
+ // ==== Create popup dialog ====
+ Gtk::Dialog *textDialogPtr = new Gtk::Dialog();
+ textDialogPtr->property_modal() = true;
+ textDialogPtr->property_title() = _("CSS selector");
+ textDialogPtr->property_window_position() = Gtk::WIN_POS_CENTER_ON_PARENT;
+ textDialogPtr->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL);
+ textDialogPtr->add_button(_("Add"), Gtk::RESPONSE_OK);
+
+ Gtk::Entry *textEditPtr = manage ( new Gtk::Entry() );
+ textEditPtr->signal_activate().connect(
+ sigc::bind<Gtk::Dialog *>(sigc::mem_fun(*this, &SelectorsDialog::_closeDialog), textDialogPtr));
+ textDialogPtr->get_content_area()->pack_start(*textEditPtr, Gtk::PACK_SHRINK);
+
+ Gtk::Label *textLabelPtr = manage(new Gtk::Label(_("Invalid CSS selector.")));
+ textDialogPtr->get_content_area()->pack_start(*textLabelPtr, Gtk::PACK_SHRINK);
+
+ /**
+ * By default, the entrybox contains 'Class1' as text. However, if object(s)
+ * is(are) selected and user clicks '+' at the bottom of dialog, the
+ * entrybox will have the id(s) of the selected objects as text.
+ */
+ if (getDesktop()->getSelection()->isEmpty()) {
+ textEditPtr->set_text(".Class1");
+ } else {
+ textEditPtr->set_text(_getIdList(objVec));
+ }
+
+ Gtk::Requisition sreq1, sreq2;
+ textDialogPtr->get_preferred_size(sreq1, sreq2);
+ int minWidth = 200;
+ int minHeight = 100;
+ minWidth = (sreq2.width > minWidth ? sreq2.width : minWidth );
+ minHeight = (sreq2.height > minHeight ? sreq2.height : minHeight);
+ textDialogPtr->set_size_request(minWidth, minHeight);
+ textEditPtr->show();
+ textLabelPtr->hide();
+ textDialogPtr->show();
+
+
+ // ==== Get response ====
+ int result = -1;
+ bool invalid = true;
+ Glib::ustring selectorValue;
+ Glib::ustring originalValue;
+ while (invalid) {
+ result = textDialogPtr->run();
+ if (result != Gtk::RESPONSE_OK) { // Cancel, close dialog, etc.
+ textDialogPtr->hide();
+ delete textDialogPtr;
+ return;
+ }
+ /**
+ * @brief selectorName
+ * This string stores selector name. The text from entrybox is saved as name
+ * for selector. If the entrybox is empty, the text (thus selectorName) is
+ * set to ".Class1"
+ */
+ originalValue = Glib::ustring(textEditPtr->get_text());
+ selectorValue = _style_dialog->fixCSSSelectors(originalValue);
+ _del.show();
+ if (originalValue.find("@import ") == std::string::npos && selectorValue.empty()) {
+ textLabelPtr->show();
+ } else {
+ invalid = false;
+ }
+ }
+ delete textDialogPtr;
+ // ==== Handle response ====
+ // If class selector, add selector name to class attribute for each object
+ Util::trim(selectorValue, ",");
+ if (originalValue.find("@import ") != std::string::npos) {
+ Gtk::TreeModel::Row row = *(_store->prepend());
+ row[_mColumns._colSelector] = originalValue;
+ row[_mColumns._colExpand] = false;
+ row[_mColumns._colType] = OTHER;
+ row[_mColumns._colObj] = nullptr;
+ row[_mColumns._colProperties] = "";
+ row[_mColumns._colVisible] = true;
+ row[_mColumns._colSelected] = 400;
+ } else {
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selectorValue);
+ for (auto &obj : objVec) {
+ for (auto tok : tokens) {
+ Glib::ustring clases = _getSelectorClasses(tok);
+ if (clases.empty()) {
+ continue;
+ }
+ _insertClass(obj, clases);
+ std::vector<SPObject *> currentobjs = _getObjVec(selectorValue);
+ bool removeclass = true;
+ for (auto currentobj : currentobjs) {
+ if (currentobj == obj) {
+ removeclass = false;
+ }
+ }
+ if (removeclass) {
+ _removeClass(obj, clases);
+ }
+ }
+ }
+ Gtk::TreeModel::Row row = *(_store->prepend());
+ row[_mColumns._colExpand] = true;
+ row[_mColumns._colType] = SELECTOR;
+ row[_mColumns._colSelector] = selectorValue;
+ row[_mColumns._colObj] = nullptr;
+ row[_mColumns._colProperties] = "";
+ row[_mColumns._colVisible] = true;
+ row[_mColumns._colSelected] = 400;
+ for (auto &obj : _getObjVec(selectorValue)) {
+ auto *id = obj->getId();
+ if (!id)
+ continue;
+ Gtk::TreeModel::Row childrow = *(_store->prepend(row->children()));
+ childrow[_mColumns._colSelector] = "#" + Glib::ustring(id);
+ childrow[_mColumns._colExpand] = false;
+ childrow[_mColumns._colType] = OBJECT;
+ childrow[_mColumns._colObj] = obj;
+ childrow[_mColumns._colProperties] = ""; // Unused
+ childrow[_mColumns._colVisible] = true; // Unused
+ childrow[_mColumns._colSelected] = 400;
+ }
+ }
+ // Add entry to style element
+ _writeStyleElement();
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
+}
+
+void SelectorsDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); }
+
+/**
+ * This function deletes selector when '-' at the bottom is clicked.
+ * Note: If deleting a class selector, class attributes are NOT changed.
+ */
+void SelectorsDialog::_delSelector()
+{
+ g_debug("SelectorsDialog::_delSelector");
+
+ _scrollock = true;
+ Glib::RefPtr<Gtk::TreeSelection> refTreeSelection = _treeView.get_selection();
+ Gtk::TreeModel::iterator iter = refTreeSelection->get_selected();
+ if (iter) {
+ _vscroll();
+ Gtk::TreeModel::Row row = *iter;
+ if (row.children().size() > 2) {
+ return;
+ }
+ _updating = true;
+ _store->erase(iter);
+ _updating = false;
+ _writeStyleElement();
+ _del.hide();
+ _scrollock = false;
+ _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
+ }
+}
+
+/**
+ * @param event
+ * @return
+ * Handles the event when '+' button in front of a selector name is clicked or when a '-' button in
+ * front of a child object is clicked. In the first case, the selected objects on the desktop (if
+ * any) are added as children of the selector in the treeview. In the latter case, the object
+ * corresponding to the row is removed from the selector.
+ */
+bool SelectorsDialog::_handleButtonEvent(GdkEventButton *event)
+{
+ g_debug("SelectorsDialog::_handleButtonEvent: Entrance");
+ if (event->type == GDK_BUTTON_RELEASE && event->button == 1) {
+ _scrollock = true;
+ Gtk::TreeViewColumn *col = nullptr;
+ Gtk::TreeModel::Path path;
+ int x = static_cast<int>(event->x);
+ int y = static_cast<int>(event->y);
+ int x2 = 0;
+ int y2 = 0;
+
+ if (_treeView.get_path_at_pos(x, y, path, col, x2, y2)) {
+ if (col == _treeView.get_column(0)) {
+ _vscroll();
+ Gtk::TreeModel::iterator iter = _store->get_iter(path);
+ Gtk::TreeModel::Row row = *iter;
+ if (!row.parent()) {
+ _addToSelector(row);
+ } else {
+ _removeFromSelector(row);
+ }
+ _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
+ }
+ }
+ }
+ return false;
+}
+
+// -------------------------------------------------------------------
+
+class PropertyData
+{
+public:
+ PropertyData() = default;;
+ PropertyData(Glib::ustring name) : _name(std::move(name)) {};
+
+ void _setSheetValue(Glib::ustring value) { _sheetValue = value; };
+ void _setAttrValue(Glib::ustring value) { _attrValue = value; };
+ Glib::ustring _getName() { return _name; };
+ Glib::ustring _getSheetValue() { return _sheetValue; };
+ Glib::ustring _getAttrValue() { return _attrValue; };
+
+private:
+ Glib::ustring _name;
+ Glib::ustring _sheetValue;
+ Glib::ustring _attrValue;
+};
+
+// -------------------------------------------------------------------
+
+SelectorsDialog::~SelectorsDialog()
+{
+ removeObservers();
+ _style_dialog->setDesktop(nullptr);
+}
+
+void SelectorsDialog::update()
+{
+ _style_dialog->update();
+}
+
+void SelectorsDialog::desktopReplaced()
+{
+ _style_dialog->setDesktop(getDesktop());
+}
+
+void SelectorsDialog::removeObservers()
+{
+ if (_textNode) {
+ _textNode->removeObserver(*m_styletextwatcher);
+ _textNode = nullptr;
+ }
+ if (m_root) {
+ m_root->removeSubtreeObserver(*m_nodewatcher);
+ m_root = nullptr;
+ }
+}
+
+void SelectorsDialog::documentReplaced()
+{
+ removeObservers();
+ if (auto document = getDocument()) {
+ m_root = document->getReprRoot();
+ m_root->addSubtreeObserver(*m_nodewatcher);
+ }
+ selectionChanged(getSelection());
+}
+
+void SelectorsDialog::selectionChanged(Selection *selection)
+{
+ _lastpath.clear();
+ _readStyleElement();
+ _selectRow();
+}
+
+/**
+ * @param event
+ * This function detects single or double click on a selector in any row. Clicking
+ * on a selector selects the matching objects on the desktop. A double click will
+ * in addition open the CSS dialog.
+ */
+void SelectorsDialog::_buttonEventsSelectObjs(GdkEventButton *event)
+{
+ g_debug("SelectorsDialog::_buttonEventsSelectObjs");
+ if (event->type == GDK_BUTTON_RELEASE && event->button == 1) {
+ _updating = true;
+ _del.show();
+ int x = static_cast<int>(event->x);
+ int y = static_cast<int>(event->y);
+ _selectObjects(x, y);
+ _updating = false;
+ _selectRow();
+ }
+}
+
+
+/**
+ * This function selects the row in treeview corresponding to an object selected
+ * in the drawing. If more than one row matches, the first is chosen.
+ */
+void SelectorsDialog::_selectRow()
+{
+ _scrollock = true;
+ g_debug("SelectorsDialog::_selectRow: updating: %s", (_updating ? "true" : "false"));
+ _del.hide();
+ std::vector<Gtk::TreeModel::Path> selectedrows = _treeView.get_selection()->get_selected_rows();
+ if (selectedrows.size() == 1) {
+ Gtk::TreeModel::Row row = *_store->get_iter(selectedrows[0]);
+ if (!row->parent() && row->children().size() < 2) {
+ _del.show();
+ }
+ if (row) {
+ _style_dialog->setCurrentSelector(row[_mColumns._colSelector]);
+ }
+ } else if (selectedrows.size() == 0) {
+ _del.show();
+ }
+ if (_updating || !getDesktop()) return; // Avoid updating if we have set row via dialog.
+
+ Gtk::TreeModel::Children children = _store->children();
+ Inkscape::Selection* selection = getDesktop()->getSelection();
+ if (selection->isEmpty()) {
+ _style_dialog->setCurrentSelector("");
+ }
+ for (auto row : children) {
+ row[_mColumns._colSelected] = 400;
+ Gtk::TreeModel::Children subchildren = row->children();
+ for (auto subrow : subchildren) {
+ subrow[_mColumns._colSelected] = 400;
+ }
+ }
+
+ // Sort selection for matching.
+ std::vector<SPObject *> selected_objs(
+ selection->objects().begin(), selection->objects().end());
+ std::sort(selected_objs.begin(), selected_objs.end());
+
+ for (auto row : children) {
+ // Recalculate the selector, in real time.
+ auto row_children = _getObjVec(row[_mColumns._colSelector]);
+ std::sort(row_children.begin(), row_children.end());
+
+ // If all selected objects are in the css-selector, select it.
+ if (row_children == selected_objs) {
+ row[_mColumns._colSelected] = 700;
+ }
+
+ Gtk::TreeModel::Children subchildren = row->children();
+
+ for (auto subrow : subchildren) {
+ if (subrow[_mColumns._colObj] && selection->includes(subrow[_mColumns._colObj])) {
+ subrow[_mColumns._colSelected] = 700;
+ }
+ if (row[_mColumns._colExpand]) {
+ _treeView.expand_to_path(Gtk::TreePath(row));
+ }
+ }
+ if (row[_mColumns._colExpand]) {
+ _treeView.expand_to_path(Gtk::TreePath(row));
+ }
+ }
+ _vadj->set_value(std::min(_scrollpos, _vadj->get_upper()));
+}
+
+/**
+ * @param btn
+ * @param iconName
+ * @param tooltip
+ * Set the style of '+' and '-' buttons at the bottom of dialog.
+ */
+void SelectorsDialog::_styleButton(Gtk::Button &btn, char const *iconName, char const *tooltip)
+{
+ g_debug("SelectorsDialog::_styleButton");
+
+ 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);
+ btn.set_tooltip_text (tooltip);
+}
+
+
+} // 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/dialog/selectorsdialog.h b/src/ui/dialog/selectorsdialog.h
new file mode 100644
index 0000000..674d6aa
--- /dev/null
+++ b/src/ui/dialog/selectorsdialog.h
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for CSS selectors
+ */
+/* Authors:
+ * Kamalpreet Kaur Grewal
+ * Tavmjong Bah
+ *
+ * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com>
+ * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SELECTORSDIALOG_H
+#define SELECTORSDIALOG_H
+
+#include <gtkmm/dialog.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/switch.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treeselection.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/treeview.h>
+#include <memory>
+#include <vector>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/dialog/styledialog.h"
+#include "xml/helper-observer.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * @brief The SelectorsDialog class
+ * A list of CSS selectors will show up in this dialog. This dialog allows one to
+ * add and delete selectors. Elements can be added to and removed from the selectors
+ * in the dialog. Selection of any selector row selects the matching objects in
+ * the drawing and vice-versa. (Only simple selectors supported for now.)
+ *
+ * This class must keep two things in sync:
+ * 1. The text node of the style element.
+ * 2. The Gtk::TreeModel.
+ */
+class SelectorsDialog : public DialogBase
+{
+public:
+ SelectorsDialog();
+ ~SelectorsDialog() override;
+
+ void update() override;
+ void desktopReplaced() override;
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+
+ private:
+ // Monitor <style> element for changes.
+ class NodeObserver;
+
+ void removeObservers();
+
+ // Monitor all objects for addition/removal/attribute change
+ class NodeWatcher;
+ enum SelectorType { CLASS, ID, TAG };
+ void _nodeAdded( Inkscape::XML::Node &repr );
+ void _nodeRemoved( Inkscape::XML::Node &repr );
+ void _nodeChanged( Inkscape::XML::Node &repr );
+ // Data structure
+ enum coltype { OBJECT, SELECTOR, OTHER };
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModelColumns() {
+ add(_colSelector);
+ add(_colExpand);
+ add(_colType);
+ add(_colObj);
+ add(_colProperties);
+ add(_colVisible);
+ add(_colSelected);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> _colSelector; // Selector or matching object id.
+ Gtk::TreeModelColumn<bool> _colExpand; // Open/Close store row.
+ Gtk::TreeModelColumn<gint> _colType; // Selector row or child object row.
+ Gtk::TreeModelColumn<SPObject *> _colObj; // Matching object (if any).
+ Gtk::TreeModelColumn<Glib::ustring> _colProperties; // List of properties.
+ Gtk::TreeModelColumn<bool> _colVisible; // Make visible or not.
+ Gtk::TreeModelColumn<gint> _colSelected; // Make selected.
+ };
+ ModelColumns _mColumns;
+
+ // Override Gtk::TreeStore to control drag-n-drop (only allow dragging and dropping of selectors).
+ // See: https://developer.gnome.org/gtkmm-tutorial/stable/sec-treeview-examples.html.en
+ //
+ // TreeStore implements simple drag and drop (DND) but there appears no way to know when a DND
+ // has been completed (other than doing the whole DND ourselves). As a hack, we use
+ // on_row_deleted to trigger write of style element.
+ class TreeStore : public Gtk::TreeStore {
+ protected:
+ TreeStore();
+ bool row_draggable_vfunc(const Gtk::TreeModel::Path& path) const override;
+ bool row_drop_possible_vfunc(const Gtk::TreeModel::Path& path,
+ const Gtk::SelectionData& selection_data) const override;
+ void on_row_deleted(const TreeModel::Path& path) override;
+
+ public:
+ static Glib::RefPtr<SelectorsDialog::TreeStore> create(SelectorsDialog *styledialog);
+
+ private:
+ SelectorsDialog *_selectorsdialog;
+ };
+
+ // TreeView
+ Glib::RefPtr<Gtk::TreeModelFilter> _modelfilter;
+ Glib::RefPtr<TreeStore> _store;
+ Gtk::TreeView _treeView;
+ Gtk::TreeModel::Path _lastpath;
+ // Widgets
+ StyleDialog *_style_dialog;
+ Gtk::Paned _paned;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+ Gtk::Box _button_box;
+ Gtk::Box _selectors_box;
+ Gtk::ScrolledWindow _scrolled_window_selectors;
+
+ Gtk::Button _del;
+ Gtk::Button _create;
+ // Reading and writing the style element.
+ Inkscape::XML::Node *_getStyleTextNode(bool create_if_missing = false);
+ void _readStyleElement();
+ void _writeStyleElement();
+
+ // Update watchers
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher;
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher;
+
+ // Manipulate Tree
+ void _addToSelector(Gtk::TreeModel::Row row);
+ void _removeFromSelector(Gtk::TreeModel::Row row);
+ Glib::ustring _getIdList(std::vector<SPObject *>);
+ std::vector<SPObject *> _getObjVec(Glib::ustring selector);
+ void _insertClass(const std::vector<SPObject *>& objVec, const Glib::ustring& className);
+ void _insertClass(SPObject *obj, const Glib::ustring &className);
+ void _removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all = false);
+ void _removeClass(SPObject *obj, const Glib::ustring &className, bool all = false);
+ void _toggleDirection(Gtk::RadioButton *vertical);
+ void _showWidgets();
+
+ void _selectObjects(int, int);
+ // Variables
+ double _scrollpos{0.0};
+ bool _scrollock{false};
+ bool _updating{false}; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop
+ Inkscape::XML::Node *m_root{nullptr};
+ Inkscape::XML::Node *_textNode{nullptr}; // Track so we know when to add a NodeObserver.
+
+ void _rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+ void _rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+ void _closeDialog(Gtk::Dialog *textDialogPtr);
+
+ Inkscape::XML::SignalObserver _objObserver; // Track object in selected row (for style change).
+
+ // Signal and handlers - Internal
+ void _addSelector();
+ void _delSelector();
+ static Glib::ustring _getSelectorClasses(Glib::ustring selector);
+ bool _handleButtonEvent(GdkEventButton *event);
+ void _buttonEventsSelectObjs(GdkEventButton *event);
+ void _selectRow(); // Select row in tree when selection changed.
+ void _vscroll();
+
+ // GUI
+ void _styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // SELECTORSDIALOG_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/dialog/spellcheck.cpp b/src/ui/dialog/spellcheck.cpp
new file mode 100644
index 0000000..51560a7
--- /dev/null
+++ b/src/ui/dialog/spellcheck.cpp
@@ -0,0 +1,751 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Spellcheck dialog.
+ */
+/* Authors:
+ * bulia byak <bulia@users.sf.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2009 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 "spellcheck.h"
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "layer-manager.h"
+#include "selection-chemistry.h"
+#include "text-editing.h"
+
+#include "display/control/canvas-item-rect.h"
+
+#include "object/sp-defs.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-object.h"
+#include "object/sp-root.h"
+#include "object/sp-string.h"
+#include "object/sp-text.h"
+#include "object/sp-tref.h"
+
+#include "ui/dialog/dialog-container.h"
+#include "ui/dialog/inkscape-preferences.h" // for PREFS_PAGE_SPELLCHECK
+#include "ui/icon-names.h"
+#include "ui/tools/text-tool.h"
+
+#include <glibmm/i18n.h>
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * Get the list of installed dictionaries/languages
+ */
+std::vector<LanguagePair> SpellCheck::get_available_langs()
+{
+ std::vector<LanguagePair> langs;
+
+#if WITH_GSPELL
+ // TODO: write a gspellmm library.
+ // TODO: why is this not const?
+ GList *list = const_cast<GList *>(gspell_language_get_available());
+ g_list_foreach(list, [](gpointer data, gpointer user_data) {
+ GspellLanguage *language = reinterpret_cast<GspellLanguage*>(data);
+ std::vector<LanguagePair> *langs = reinterpret_cast<std::vector<LanguagePair>*>(user_data);
+ const gchar *name = gspell_language_get_name(language);
+ const gchar *code = gspell_language_get_code(language);
+ langs->emplace_back(name, code);
+ }, &langs);
+#endif
+
+ return langs;
+}
+
+static void show_spellcheck_preferences_dialog()
+{
+ Inkscape::Preferences::get()->setInt("/dialogs/preferences/page", PREFS_PAGE_SPELLCHECK);
+ SP_ACTIVE_DESKTOP->getContainer()->new_dialog("Spellcheck");
+}
+
+SpellCheck::SpellCheck()
+ : DialogBase("/dialogs/spellcheck/", "Spellcheck")
+ , _text(nullptr)
+ , _layout(nullptr)
+ , _stops(0)
+ , _adds(0)
+ , _working(false)
+ , _local_change(false)
+ , _prefs(nullptr)
+ , accept_button(_("_Accept"), true)
+ , ignoreonce_button(_("_Ignore once"), true)
+ , ignore_button(_("_Ignore"), true)
+ , add_button(_("A_dd"), true)
+ , dictionary_label(_("Language"))
+ , dictionary_hbox(Gtk::ORIENTATION_HORIZONTAL, 0)
+ , stop_button(_("_Stop"), true)
+ , start_button(_("_Start"), true)
+ , suggestion_hbox(Gtk::ORIENTATION_HORIZONTAL)
+ , changebutton_vbox(Gtk::ORIENTATION_VERTICAL)
+{
+ _prefs = Inkscape::Preferences::get();
+
+ banner_hbox.set_layout(Gtk::BUTTONBOX_START);
+ banner_hbox.add(banner_label);
+
+ if (_langs.empty()) {
+ _langs = get_available_langs();
+
+ if (_langs.empty()) {
+ banner_label.set_markup(Glib::ustring::compose("<i>%1</i>", _("No dictionaries installed")));
+ }
+ }
+
+ scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ scrolled_window.set_shadow_type(Gtk::SHADOW_IN);
+ scrolled_window.set_size_request(120, 96);
+ scrolled_window.add(tree_view);
+
+ model = Gtk::ListStore::create(tree_columns);
+ tree_view.set_model(model);
+ tree_view.append_column(_("Suggestions:"), tree_columns.suggestions);
+
+ if (!_langs.empty()) {
+ for (const LanguagePair &pair : _langs) {
+ dictionary_combo.append(pair.second, pair.first);
+ }
+ // Set previously set language (or the first item)
+ if(!dictionary_combo.set_active_id(_prefs->getString("/dialogs/spellcheck/lang"))) {
+ dictionary_combo.set_active(0);
+ }
+ }
+
+ accept_button.set_tooltip_text(_("Accept the chosen suggestion"));
+ ignoreonce_button.set_tooltip_text(_("Ignore this word only once"));
+ ignore_button.set_tooltip_text(_("Ignore this word in this session"));
+ add_button.set_tooltip_text(_("Add this word to the chosen dictionary"));
+ pref_button.set_tooltip_text(_("Preferences"));
+ pref_button.set_image_from_icon_name("preferences-system");
+
+ dictionary_hbox.pack_start(dictionary_label, false, false, 6);
+ dictionary_hbox.pack_start(dictionary_combo, true, true, 0);
+ dictionary_hbox.pack_start(pref_button, false, false, 0);
+
+ changebutton_vbox.set_spacing(4);
+ changebutton_vbox.pack_start(accept_button, false, false, 0);
+ changebutton_vbox.pack_start(ignoreonce_button, false, false, 0);
+ changebutton_vbox.pack_start(ignore_button, false, false, 0);
+ changebutton_vbox.pack_start(add_button, false, false, 0);
+
+ suggestion_hbox.pack_start (scrolled_window, true, true, 4);
+ suggestion_hbox.pack_end (changebutton_vbox, false, false, 0);
+
+ stop_button.set_tooltip_text(_("Stop the check"));
+ start_button.set_tooltip_text(_("Start the check"));
+
+ actionbutton_hbox.set_layout(Gtk::BUTTONBOX_END);
+ actionbutton_hbox.set_spacing(4);
+ actionbutton_hbox.add(stop_button);
+ actionbutton_hbox.add(start_button);
+
+ /*
+ * Main dialog
+ */
+ set_spacing(6);
+ pack_start (banner_hbox, false, false, 0);
+ pack_start (suggestion_hbox, true, true, 0);
+ pack_start (dictionary_hbox, false, false, 0);
+ pack_start (action_sep, false, false, 6);
+ pack_start (actionbutton_hbox, false, false, 0);
+
+ /*
+ * Signal handlers
+ */
+ accept_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAccept));
+ ignoreonce_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnoreOnce));
+ ignore_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnore));
+ add_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAdd));
+ start_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStart));
+ stop_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStop));
+ tree_view.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onTreeSelectionChange));
+ dictionary_combo.signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onLanguageChanged));
+ pref_button.signal_clicked().connect(sigc::ptr_fun(show_spellcheck_preferences_dialog));
+
+ show_all_children ();
+
+ tree_view.set_sensitive(false);
+ accept_button.set_sensitive(false);
+ ignore_button.set_sensitive(false);
+ ignoreonce_button.set_sensitive(false);
+ add_button.set_sensitive(false);
+ stop_button.set_sensitive(false);
+}
+
+SpellCheck::~SpellCheck()
+{
+ disconnect();
+}
+
+void SpellCheck::documentReplaced()
+{
+ if (_working) {
+ // Stop and start on the new desktop
+ finished();
+ onStart();
+ }
+}
+
+void SpellCheck::clearRects()
+{
+ _rects.clear();
+}
+
+void SpellCheck::disconnect()
+{
+ if (_release_connection) {
+ _release_connection.disconnect();
+ }
+ if (_modified_connection) {
+ _modified_connection.disconnect();
+ }
+}
+
+void SpellCheck::allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked)
+{
+ if (is<SPDefs>(r))
+ return; // we're not interested in items in defs
+
+ if (!strcmp(r->getRepr()->name(), "svg:metadata")) {
+ return; // we're not interested in metadata
+ }
+
+ if (auto desktop = getDesktop()) {
+ for (auto& child: r->children) {
+ if (auto item = cast<SPItem>(&child)) {
+ if (!child.cloned && !desktop->layerManager().isLayer(item)) {
+ if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) {
+ if (is<SPText>(item) || is<SPFlowtext>(item))
+ l.push_back(item);
+ }
+ }
+ }
+ allTextItems (&child, l, hidden, locked);
+ }
+ }
+ return;
+}
+
+bool
+SpellCheck::textIsValid (SPObject *root, SPItem *text)
+{
+ std::vector<SPItem*> l;
+ allTextItems (root, l, false, true);
+ return (std::find(l.begin(), l.end(), text) != l.end());
+}
+
+bool SpellCheck::compareTextBboxes(SPItem const *i1, SPItem const *i2)//returns a<b
+{
+ Geom::OptRect bbox1 = i1->documentVisualBounds();
+ Geom::OptRect bbox2 = i2->documentVisualBounds();
+ if (!bbox1 || !bbox2) {
+ return false;
+ }
+
+ // vector between top left corners
+ Geom::Point diff = bbox1->min() - bbox2->min();
+
+ return diff[Geom::Y] == 0 ? (diff[Geom::X] < 0) : (diff[Geom::Y] < 0);
+}
+
+// We regenerate and resort the list every time, because user could have changed it while the
+// dialog was waiting
+SPItem *SpellCheck::getText (SPObject *root)
+{
+ std::vector<SPItem*> l;
+ allTextItems (root, l, false, true);
+ std::sort(l.begin(),l.end(),SpellCheck::compareTextBboxes);
+
+ for (auto item:l) {
+ if(_seen_objects.insert(item).second)
+ return item;
+ }
+ return nullptr;
+}
+
+void
+SpellCheck::nextText()
+{
+ disconnect();
+
+ _text = getText(_root);
+ if (_text) {
+
+ _modified_connection = _text->connectModified(sigc::mem_fun(*this, &SpellCheck::onObjModified));
+ _release_connection = _text->connectRelease(sigc::mem_fun(*this, &SpellCheck::onObjReleased));
+
+ _layout = te_get_layout (_text);
+ _begin_w = _layout->begin();
+ }
+ _end_w = _begin_w;
+ _word.clear();
+}
+
+void SpellCheck::deleteSpeller() {
+}
+
+bool SpellCheck::updateSpeller() {
+#if WITH_GSPELL
+ auto lang = dictionary_combo.get_active_id();
+ if (!lang.empty()) {
+ const GspellLanguage *language = gspell_language_lookup(lang.c_str());
+ _checker = gspell_checker_new(language);
+ }
+
+ return _checker != nullptr;
+#else
+ return false;
+#endif
+}
+
+void SpellCheck::onStart()
+{
+ if (!getDocument())
+ return;
+
+ start_button.set_sensitive(false);
+
+ _stops = 0;
+ _adds = 0;
+ clearRects();
+
+ if (!updateSpeller())
+ return;
+
+ _root = getDocument()->getRoot();
+
+ // empty the list of objects we've checked
+ _seen_objects.clear();
+
+ // grab first text
+ nextText();
+
+ _working = true;
+
+ doSpellcheck();
+}
+
+void
+SpellCheck::finished ()
+{
+ deleteSpeller();
+
+ clearRects();
+ disconnect();
+
+ tree_view.unset_model();
+ tree_view.set_sensitive(false);
+ accept_button.set_sensitive(false);
+ ignore_button.set_sensitive(false);
+ ignoreonce_button.set_sensitive(false);
+ add_button.set_sensitive(false);
+ stop_button.set_sensitive(false);
+ start_button.set_sensitive(true);
+
+ {
+ gchar *label;
+ if (_stops)
+ label = g_strdup_printf(_("<b>Finished</b>, <b>%d</b> words added to dictionary"), _adds);
+ else
+ label = g_strdup_printf("%s", _("<b>Finished</b>, nothing suspicious found"));
+ banner_label.set_markup(label);
+ g_free(label);
+ }
+
+ _seen_objects.clear();
+
+ _root = nullptr;
+
+ _working = false;
+}
+
+bool
+SpellCheck::nextWord()
+{
+ auto desktop = getDesktop();
+ if (!_working || !desktop)
+ return false;
+
+ if (!_text) {
+ finished();
+ return false;
+ }
+ _word.clear();
+
+ while (_word.size() == 0) {
+ _begin_w = _end_w;
+
+ if (!_layout || _begin_w == _layout->end()) {
+ nextText();
+ return false;
+ }
+
+ if (!_layout->isStartOfWord(_begin_w)) {
+ _begin_w.nextStartOfWord();
+ }
+
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ _word = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ }
+
+ // try to link this word with the next if separated by '
+ SPObject *char_item = nullptr;
+ Glib::ustring::iterator text_iter;
+ _layout->getSourceOfCharacter(_end_w, &char_item, &text_iter);
+ if (is<SPString>(char_item)) {
+ int this_char = *text_iter;
+ if (this_char == '\'' || this_char == 0x2019) {
+ Inkscape::Text::Layout::iterator end_t = _end_w;
+ end_t.nextCharacter();
+ _layout->getSourceOfCharacter(end_t, &char_item, &text_iter);
+ if (is<SPString>(char_item)) {
+ int this_char = *text_iter;
+ if (g_ascii_isalpha(this_char)) { // 's
+ _end_w.nextEndOfWord();
+ _word = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ }
+ }
+ }
+ }
+
+ // skip words containing digits
+ if (_prefs->getInt(_prefs_path + "ignorenumbers") != 0) {
+ bool digits = false;
+ for (unsigned int i : _word) {
+ if (g_unichar_isdigit(i)) {
+ digits = true;
+ break;
+ }
+ }
+ if (digits) {
+ return false;
+ }
+ }
+
+ // skip ALL-CAPS words
+ if (_prefs->getInt(_prefs_path + "ignoreallcaps") != 0) {
+ bool allcaps = true;
+ for (unsigned int i : _word) {
+ if (!g_unichar_isupper(i)) {
+ allcaps = false;
+ break;
+ }
+ }
+ if (allcaps) {
+ return false;
+ }
+ }
+
+ int have = 0;
+
+#if WITH_GSPELL
+ if (_checker) {
+ GError *error = nullptr;
+ have += gspell_checker_check_word(_checker, _word.c_str(), -1, &error);
+ }
+#endif /* WITH_GSPELL */
+
+ if (have == 0) { // not found in any!
+ _stops ++;
+
+ // display it in window
+ {
+ gchar *label = g_strdup_printf(_("Not in dictionary: <b>%s</b>"), _word.c_str());
+ banner_label.set_markup(label);
+ g_free(label);
+ }
+
+ tree_view.set_sensitive(true);
+ ignore_button.set_sensitive(true);
+ ignoreonce_button.set_sensitive(true);
+ add_button.set_sensitive(true);
+ stop_button.set_sensitive(true);
+
+ // draw rect
+ std::vector<Geom::Point> points =
+ _layout->createSelectionShape(_begin_w, _end_w, _text->i2dt_affine());
+ if (points.size() >= 4) { // We may not have a single quad if this is a clipped part of text on path;
+ // in that case skip drawing the rect
+ Geom::Point tl, br;
+ tl = br = points.front();
+ for (auto & point : points) {
+ if (point[Geom::X] < tl[Geom::X])
+ tl[Geom::X] = point[Geom::X];
+ if (point[Geom::Y] < tl[Geom::Y])
+ tl[Geom::Y] = point[Geom::Y];
+ if (point[Geom::X] > br[Geom::X])
+ br[Geom::X] = point[Geom::X];
+ if (point[Geom::Y] > br[Geom::Y])
+ br[Geom::Y] = point[Geom::Y];
+ }
+
+ // expand slightly
+ Geom::Rect area = Geom::Rect(tl, br);
+ double mindim = fabs(tl[Geom::Y] - br[Geom::Y]);
+ if (fabs(tl[Geom::X] - br[Geom::X]) < mindim)
+ mindim = fabs(tl[Geom::X] - br[Geom::X]);
+ area.expandBy(MAX(0.05 * mindim, 1));
+
+ // Create canvas item rect with red stroke. (TODO: a quad could allow non-axis aligned rects.)
+ auto rect = new Inkscape::CanvasItemRect(desktop->getCanvasSketch(), area);
+ rect->set_stroke(0xff0000ff);
+ rect->show();
+ _rects.emplace_back(rect);
+
+ // scroll to make it all visible
+ Geom::Point const center = desktop->current_center();
+ area.expandBy(0.5 * mindim);
+ Geom::Point scrollto;
+ double dist = 0;
+ for (unsigned corner = 0; corner < 4; corner ++) {
+ if (Geom::L2(area.corner(corner) - center) > dist) {
+ dist = Geom::L2(area.corner(corner) - center);
+ scrollto = area.corner(corner);
+ }
+ }
+ desktop->scroll_to_point(scrollto);
+ }
+
+ // select text; if in Text tool, position cursor to the beginning of word
+ // unless it is already in the word
+ if (desktop->getSelection()->singleItem() != _text) {
+ desktop->getSelection()->set (_text);
+ }
+
+ if (dynamic_cast<Inkscape::UI::Tools::TextTool *>(desktop->event_context)) {
+ Inkscape::Text::Layout::iterator *cursor =
+ sp_text_context_get_cursor_position(SP_TEXT_CONTEXT(desktop->event_context), _text);
+ if (!cursor) // some other text is selected there
+ desktop->getSelection()->set (_text);
+ else if (*cursor <= _begin_w || *cursor >= _end_w)
+ sp_text_context_place_cursor (SP_TEXT_CONTEXT(desktop->event_context), _text, _begin_w);
+ }
+
+#if WITH_GSPELL
+
+ // get suggestions
+ model = Gtk::ListStore::create(tree_columns);
+ tree_view.set_model(model);
+ unsigned n_sugg = 0;
+
+ if (_checker) {
+ GSList *list = gspell_checker_get_suggestions(_checker, _word.c_str(), -1);
+ std::vector<std::string> suggs;
+
+ // TODO: use a better API for that, or figure out how to make gspellmm.
+ g_slist_foreach(list, [](gpointer data, gpointer user_data) {
+ const gchar *suggestion = reinterpret_cast<const gchar*>(data);
+ std::vector<std::string> *suggs = reinterpret_cast<std::vector<std::string>*>(user_data);
+ suggs->push_back(suggestion);
+ }, &suggs);
+ g_slist_free_full(list, g_free);
+
+ Gtk::TreeModel::iterator iter;
+ for (std::string sugg : suggs) {
+ iter = model->append();
+ Gtk::TreeModel::Row row = *iter;
+ row[tree_columns.suggestions] = sugg;
+
+ // select first suggestion
+ if (++n_sugg == 1) {
+ tree_view.get_selection()->select(iter);
+ }
+ }
+ }
+
+ accept_button.set_sensitive(n_sugg > 0);
+
+#endif /* WITH_GSPELL */
+
+ return true;
+
+ }
+ return false;
+}
+
+void SpellCheck::deleteLastRect()
+{
+ if (!_rects.empty()) {
+ _rects.pop_back();
+ }
+}
+
+void SpellCheck::doSpellcheck ()
+{
+ if (_langs.empty()) {
+ return;
+ }
+
+ banner_label.set_markup(_("<i>Checking...</i>"));
+
+ while (_working)
+ if (nextWord())
+ break;
+}
+
+void SpellCheck::onTreeSelectionChange()
+{
+ accept_button.set_sensitive(true);
+}
+
+void SpellCheck::onObjModified (SPObject* /* blah */, unsigned int /* bleh */)
+{
+ if (_local_change) { // this was a change by this dialog, i.e. an Accept, skip it
+ _local_change = false;
+ return;
+ }
+
+ if (_working && _root) {
+ // user may have edited the text we're checking; try to do the most sensible thing in this
+ // situation
+
+ // just in case, re-get text's layout
+ _layout = te_get_layout (_text);
+
+ // re-get the word
+ _layout->validateIterator(&_begin_w);
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ Glib::ustring word_new = sp_te_get_string_multiline (_text, _begin_w, _end_w);
+ if (word_new != _word) {
+ _end_w = _begin_w;
+ deleteLastRect ();
+ doSpellcheck (); // recheck this word and go ahead if it's ok
+ }
+ }
+}
+
+void SpellCheck::onObjReleased (SPObject* /* blah */)
+{
+ if (_working && _root) {
+ // the text object was deleted
+ deleteLastRect ();
+ nextText();
+ doSpellcheck (); // get next text and continue
+ }
+}
+
+void SpellCheck::onAccept ()
+{
+ // insert chosen suggestion
+
+ Glib::RefPtr<Gtk::TreeSelection> selection = tree_view.get_selection();
+ Gtk::TreeModel::iterator iter = selection->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ Glib::ustring sugg = row[tree_columns.suggestions];
+
+ if (sugg.length() > 0) {
+ //g_print("chosen: %s\n", sugg);
+ _local_change = true;
+ sp_te_replace(_text, _begin_w, _end_w, sugg.c_str());
+ // find the end of the word anew
+ _end_w = _begin_w;
+ _end_w.nextEndOfWord();
+ DocumentUndo::done(getDocument(), _("Fix spelling"), INKSCAPE_ICON("draw-text"));
+ }
+ }
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onIgnore ()
+{
+#if WITH_GSPELL
+ if (_checker) {
+ gspell_checker_add_word_to_session(_checker, _word.c_str(), -1);
+ }
+#endif /* WITH_GSPELL */
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onIgnoreOnce ()
+{
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onAdd ()
+{
+ _adds++;
+
+#if WITH_GSPELL
+ if (_checker) {
+ gspell_checker_add_word_to_personal(_checker, _word.c_str(), -1);
+ }
+#endif /* WITH_GSPELL */
+
+ deleteLastRect();
+ doSpellcheck();
+}
+
+void
+SpellCheck::onStop ()
+{
+ finished();
+}
+
+void SpellCheck::onLanguageChanged()
+{
+ // First, save language for next load
+ auto lang = dictionary_combo.get_active_id();
+ _prefs->setString("/dialogs/spellcheck/lang", lang);
+
+ if (!_working) {
+ onStart();
+ return;
+ }
+
+ if (!updateSpeller()) {
+ return;
+ }
+
+ // recheck current word
+ _end_w = _begin_w;
+ deleteLastRect();
+ doSpellcheck();
+}
+}
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/spellcheck.h b/src/ui/dialog/spellcheck.h
new file mode 100644
index 0000000..753eb80
--- /dev/null
+++ b/src/ui/dialog/spellcheck.h
@@ -0,0 +1,281 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Spellcheck dialog
+ */
+/* Authors:
+ * bulia byak <bulia@users.sf.net>
+ *
+ * Copyright (C) 2009 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_SPELLCHECK_H
+#define SEEN_SPELLCHECK_H
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include <gtkmm/box.h>
+#include <gtkmm/button.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/separator.h>
+#include <gtkmm/treeview.h>
+#include <set>
+#include <vector>
+
+#include "text-editing.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/scrollprotected.h"
+#include "display/control/canvas-item-ptr.h"
+
+#if WITH_GSPELL
+#include <gspell/gspell.h>
+#endif /* WITH_GSPELL */
+
+class SPObject;
+class SPItem;
+class SPCanvasItem;
+
+namespace Inkscape {
+class Preferences;
+class CanvasItemRect;
+
+namespace UI {
+namespace Dialog {
+
+using LanguagePair = std::pair<std::string, std::string>;
+
+/**
+ *
+ * A dialog widget to checking spelling of text elements in the document
+ * Uses gspell and one of the languages set in the users preference file
+ *
+ */
+class SpellCheck : public DialogBase
+{
+public:
+ SpellCheck();
+ ~SpellCheck() override;
+
+ static std::vector<LanguagePair> get_available_langs();
+
+private:
+ void documentReplaced() override;
+
+ /**
+ * Remove the highlight rectangle form the canvas
+ */
+ void clearRects();
+
+ /**
+ * Release handlers to the selected item
+ */
+ void disconnect();
+
+ /**
+ * Returns a list of all the text items in the SPObject
+ */
+ void allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked);
+
+ /**
+ * Is text inside the SPOject's tree
+ */
+ bool textIsValid (SPObject *root, SPItem *text);
+
+ /**
+ * Compare the visual bounds of 2 SPItems referred to by a and b
+ */
+ static bool compareTextBboxes(SPItem const *i1, SPItem const *i2);
+ SPItem *getText (SPObject *root);
+ void nextText ();
+
+ /**
+ * Cleanup after spellcheck is finished
+ */
+ void finished ();
+
+ /**
+ * Find the next word to spell check
+ */
+ bool nextWord();
+ void deleteLastRect ();
+ void doSpellcheck ();
+
+ /**
+ * Update speller from language combobox
+ * @return true if update was successful
+ */
+ bool updateSpeller();
+ void deleteSpeller();
+
+ /**
+ * Accept button clicked
+ */
+ void onAccept ();
+
+ /**
+ * Ignore button clicked
+ */
+ void onIgnore ();
+
+ /**
+ * Ignore once button clicked
+ */
+ void onIgnoreOnce ();
+
+ /**
+ * Add button clicked
+ */
+ void onAdd ();
+
+ /**
+ * Stop button clicked
+ */
+ void onStop ();
+
+ /**
+ * Start button clicked
+ */
+ void onStart ();
+
+ /**
+ * Language selection changed
+ */
+ void onLanguageChanged();
+
+ /**
+ * Selected object modified on canvas
+ */
+ void onObjModified (SPObject* /* blah */, unsigned int /* bleh */);
+
+ /**
+ * Selected object removed from canvas
+ */
+ void onObjReleased (SPObject* /* blah */);
+
+ /**
+ * Selection in suggestions text view changed
+ */
+ void onTreeSelectionChange();
+
+ SPObject *_root;
+
+#if WITH_GSPELL
+ GspellChecker *_checker = nullptr;
+#endif /* WITH_GSPELL */
+
+ /**
+ * list of canvasitems (currently just rects) that mark misspelled things on canvas
+ */
+ std::vector<CanvasItemPtr<CanvasItemRect>> _rects;
+
+ /**
+ * list of text objects we have already checked in this session
+ */
+ std::set<SPItem *> _seen_objects;
+
+ /**
+ * the object currently being checked
+ */
+ SPItem *_text;
+
+ /**
+ * current objects layout
+ */
+ Inkscape::Text::Layout const *_layout;
+
+ /**
+ * iterators for the start and end of the current word
+ */
+ Inkscape::Text::Layout::iterator _begin_w;
+ Inkscape::Text::Layout::iterator _end_w;
+
+ /**
+ * the word we're checking
+ */
+ Glib::ustring _word;
+
+ /**
+ * counters for the number of stops and dictionary adds
+ */
+ int _stops;
+ int _adds;
+
+ /**
+ * true if we are in the middle of a check
+ */
+ bool _working;
+
+ /**
+ * connect to the object being checked in case it is modified or deleted by user
+ */
+ sigc::connection _modified_connection;
+ sigc::connection _release_connection;
+
+ /**
+ * true if the spell checker dialog has changed text, to suppress modified callback
+ */
+ bool _local_change;
+
+ Inkscape::Preferences *_prefs;
+
+ std::vector<LanguagePair> _langs;
+
+ /*
+ * Dialogs widgets
+ */
+ Gtk::Label banner_label;
+ Gtk::ButtonBox banner_hbox;
+ Gtk::ScrolledWindow scrolled_window;
+ Gtk::TreeView tree_view;
+ Glib::RefPtr<Gtk::ListStore> model;
+
+ Gtk::Box suggestion_hbox;
+ Gtk::Box changebutton_vbox;
+ Gtk::Button accept_button;
+ Gtk::Button ignoreonce_button;
+ Gtk::Button ignore_button;
+
+ Gtk::Button add_button;
+ Gtk::Button pref_button;
+ Gtk::Label dictionary_label;
+ Inkscape::UI::Widget::ScrollProtected<Gtk::ComboBoxText> dictionary_combo;
+ Gtk::Box dictionary_hbox;
+ Gtk::Separator action_sep;
+ Gtk::Button stop_button;
+ Gtk::Button start_button;
+ Gtk::ButtonBox actionbutton_hbox;
+
+ class TreeColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ TreeColumns()
+ {
+ add(suggestions);
+ }
+ ~TreeColumns() override = default;
+ Gtk::TreeModelColumn<Glib::ustring> suggestions;
+ };
+ TreeColumns tree_columns;
+};
+}
+}
+}
+
+#endif /* !SEEN_SPELLCHECK_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/dialog/startup.cpp b/src/ui/dialog/startup.cpp
new file mode 100644
index 0000000..b459bc1
--- /dev/null
+++ b/src/ui/dialog/startup.cpp
@@ -0,0 +1,789 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for the about screen
+ *
+ * Copyright (C) Martin Owens 2019 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "startup.h"
+
+#include <fstream>
+#include <glibmm/i18n.h>
+#include <streambuf>
+#include <string>
+#include <limits>
+
+#include "color-rgba.h"
+#include "file.h"
+#include "inkscape-application.h"
+#include "inkscape-version-info.h"
+#include "inkscape-version.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "object/sp-namedview.h"
+#include "preferences.h"
+#include "ui/dialog/filedialog.h"
+#include "ui/shortcuts.h"
+#include "ui/themes.h"
+#include "ui/util.h"
+#include "ui/widget/template-list.h"
+#include "util/units.h"
+
+using namespace Inkscape::IO;
+using namespace Inkscape::UI::View;
+using Inkscape::Util::unit_table;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class NameIdCols: public Gtk::TreeModel::ColumnRecord {
+ public:
+ // These types must match those for the model in the .glade file
+ NameIdCols() {
+ this->add(this->col_name);
+ this->add(this->col_id);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> col_name;
+ Gtk::TreeModelColumn<Glib::ustring> col_id;
+};
+
+class RecentCols: public Gtk::TreeModel::ColumnRecord {
+ public:
+ // These types must match those for the model in the .glade file
+ RecentCols() {
+ this->add(this->col_name);
+ this->add(this->col_id);
+ this->add(this->col_dt);
+ this->add(this->col_crash);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> col_name;
+ Gtk::TreeModelColumn<Glib::ustring> col_id;
+ Gtk::TreeModelColumn<gint64> col_dt;
+ Gtk::TreeModelColumn<bool> col_crash;
+};
+
+class CanvasCols: public Gtk::TreeModel::ColumnRecord {
+ public:
+ // These types must match those for the model in the .glade file
+ CanvasCols() {
+ this->add(this->id);
+ this->add(this->name);
+ this->add(this->icon_filename);
+ this->add(this->pagecolor);
+ this->add(this->checkered);
+ this->add(this->bordercolor);
+ this->add(this->shadow);
+ this->add(this->deskcolor);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> icon_filename;
+ Gtk::TreeModelColumn<Glib::ustring> pagecolor;
+ Gtk::TreeModelColumn<bool> checkered;
+ Gtk::TreeModelColumn<Glib::ustring> bordercolor;
+ Gtk::TreeModelColumn<bool> shadow;
+ Gtk::TreeModelColumn<Glib::ustring> deskcolor;
+};
+
+class ThemeCols: public Gtk::TreeModel::ColumnRecord {
+ public:
+ // These types must match those for the model in the .glade file
+ ThemeCols() {
+ this->add(this->id);
+ this->add(this->name);
+ this->add(this->theme);
+ this->add(this->icons);
+ this->add(this->base);
+ this->add(this->base_dark);
+ this->add(this->success);
+ this->add(this->warn);
+ this->add(this->error);
+ this->add(this->symbolic);
+ this->add(this->smallicons);
+ this->add(this->enabled);
+ }
+ Gtk::TreeModelColumn<Glib::ustring> id;
+ Gtk::TreeModelColumn<Glib::ustring> name;
+ Gtk::TreeModelColumn<Glib::ustring> theme;
+ Gtk::TreeModelColumn<Glib::ustring> icons;
+ Gtk::TreeModelColumn<Glib::ustring> base;
+ Gtk::TreeModelColumn<Glib::ustring> base_dark;
+ Gtk::TreeModelColumn<Glib::ustring> success;
+ Gtk::TreeModelColumn<Glib::ustring> warn;
+ Gtk::TreeModelColumn<Glib::ustring> error;
+ Gtk::TreeModelColumn<bool> symbolic;
+ Gtk::TreeModelColumn<bool> smallicons;
+ Gtk::TreeModelColumn<bool> enabled;
+};
+
+/**
+ * Color is store as a string in the form #RRGGBBAA, '0' means "unset"
+ *
+ * @param color - The string color from glade.
+ */
+unsigned int get_color_value(const Glib::ustring color)
+{
+ Gdk::RGBA gdk_color = Gdk::RGBA(color);
+ ColorRGBA sp_color(gdk_color.get_red(), gdk_color.get_green(),
+ gdk_color.get_blue(), gdk_color.get_alpha());
+ return sp_color.getIntValue();
+}
+
+StartScreen::StartScreen()
+ : Gtk::Dialog()
+{
+ set_can_focus(true);
+ grab_focus();
+ set_can_default(true);
+ grab_default();
+ set_urgency_hint(true); // Draw user's attention to this window!
+ set_modal(true);
+ set_position(Gtk::WIN_POS_CENTER_ALWAYS);
+ set_default_size(700, 360);
+
+ Glib::ustring gladefile = Resource::get_filename(Resource::UIS, "inkscape-start.glade");
+
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_error("Glade file loading failed for boot screen");
+ // cleanup?
+ }
+
+ // Get window from Glade file.
+ builder->get_widget("start-screen-window", window);
+ set_name("start-screen-window");
+ set_title(Inkscape::inkscape_version());
+
+ // Get references to various widgets used globally.
+ builder->get_widget("tabs", tabs);
+ builder->get_widget_derived("kinds", templates);
+ builder->get_widget("banner", banners);
+ builder->get_widget("themes", themes);
+ builder->get_widget("recent_treeview", recent_treeview);
+
+ // Populate with template extensions
+ templates->init(Inkscape::Extension::TEMPLATE_NEW_WELCOME);
+
+ // Get references to various widget used locally. (In order of appearance.)
+ Gtk::ComboBox* canvas = nullptr;
+ Gtk::ComboBox* keys = nullptr;
+ Gtk::Button* save = nullptr;
+ Gtk::Button* thanks = nullptr;
+ Gtk::Button* close_btn = nullptr;
+ Gtk::Button* new_btn = nullptr;
+ Gtk::Button* show_toggle = nullptr;
+ Gtk::Switch* dark_toggle = nullptr;
+ builder->get_widget("canvas", canvas);
+ builder->get_widget("keys", keys);
+ builder->get_widget("save", save);
+ builder->get_widget("thanks", thanks);
+ builder->get_widget("show_toggle", show_toggle);
+ builder->get_widget("dark_toggle", dark_toggle);
+ builder->get_widget("load", load_btn);
+ builder->get_widget("new", new_btn);
+ builder->get_widget("close_window", close_btn);
+
+ // Unparent to move to our dialog window.
+ auto parent = banners->get_parent();
+ parent->remove(*banners);
+ parent->remove(*tabs);
+
+ // Add signals and setup things.
+ auto prefs = Inkscape::Preferences::get();
+
+ tabs->signal_switch_page().connect(sigc::mem_fun(*this, &StartScreen::notebook_switch));
+
+ // Setup the lists of items
+ enlist_recent_files();
+ enlist_keys();
+ filter_themes();
+ set_active_combo("themes", prefs->getString("/options/boot/theme"));
+ set_active_combo("canvas", prefs->getString("/options/boot/canvas"));
+
+ // initialise dark depending on prefs and background
+ refresh_dark_switch();
+
+ // Welcome! tab
+ std::string welcome_text_file = Resource::get_filename_string(Resource::SCREENS, "start-welcome-text.svg", true);
+ Gtk::Image *welcome_text;
+ builder->get_widget("welcome_text", welcome_text);
+ welcome_text->set(welcome_text_file);
+
+ canvas->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::canvas_changed));
+ keys->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::keyboard_changed));
+ themes->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::theme_changed));
+ dark_toggle->property_active().signal_changed().connect(sigc::mem_fun(*this, &StartScreen::theme_changed));
+ save->signal_clicked().connect(sigc::bind<Gtk::Button *>(sigc::mem_fun(*this, &StartScreen::notebook_next), save));
+
+ // "Supported by You" tab
+ thanks->signal_clicked().connect(sigc::bind<Gtk::Button *>(sigc::mem_fun(*this, &StartScreen::notebook_next), thanks));
+
+ // "Time to Draw" tab
+ recent_treeview->signal_row_activated().connect(sigc::hide(sigc::hide((sigc::mem_fun(*this, &StartScreen::load_document)))));
+ recent_treeview->get_selection()->signal_changed().connect(sigc::mem_fun(*this, &StartScreen::on_recent_changed));
+ templates->signal_switch_page().connect(sigc::mem_fun(*this, &StartScreen::on_kind_changed));
+ load_btn->set_sensitive(true);
+
+ show_toggle->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::show_toggle));
+ load_btn->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::load_document));
+ templates->connectItemSelected(sigc::mem_fun(*this, &StartScreen::new_document));
+ new_btn->signal_clicked().connect(sigc::mem_fun(*this, &StartScreen::new_document));
+ close_btn->signal_clicked().connect([=] { response(GTK_RESPONSE_CANCEL); });
+
+ // Reparent to our dialog window
+ set_titlebar(*banners);
+ Gtk::Box* box = get_content_area();
+ box->add(*tabs);
+
+ // Show the first tab ONLY on the first run for this version
+ std::string opt_shown = "/options/boot/shown/ver";
+ opt_shown += Inkscape::version_string_without_revision;
+ if (!prefs->getBool(opt_shown, false)) {
+ theme_changed();
+ tabs->set_current_page(0);
+ prefs->setBool(opt_shown, true);
+ } else {
+ tabs->set_current_page(2);
+ notebook_switch(nullptr, 2);
+ }
+
+ set_modal(true);
+ set_position(Gtk::WIN_POS_CENTER_ALWAYS);
+ property_resizable() = false;
+ set_default_size(700, 360);
+ show();
+}
+
+StartScreen::~StartScreen()
+{
+ // These are "owned" by builder... don't delete them!
+ banners->get_parent()->remove(*banners);
+ tabs->get_parent()->remove(*tabs);
+}
+
+/**
+ * Return the active row of the named combo box.
+ *
+ * @param widget_name - The name of the widget in the glade file
+ * @return Gtk Row object ready for use.
+ * @throws Three errors depending on where it failed.
+ */
+Gtk::TreeModel::Row
+StartScreen::active_combo(std::string widget_name)
+{
+ Gtk::ComboBox *combo;
+ builder->get_widget(widget_name, combo);
+ if (!combo) throw 1;
+ Gtk::TreeModel::iterator iter = combo->get_active();
+ if (!iter) throw 2;
+ Gtk::TreeModel::Row row = *iter;
+ if (!row) throw 3;
+ return row;
+}
+
+/**
+ * Set the active item in the combo based on the unique_id (column set in glade)
+ *
+ * @param widget_name - The name of the widget in the glade file
+ * @param unique_id - The column id to activate, sets to first item if blank.
+ */
+void
+StartScreen::set_active_combo(std::string widget_name, std::string unique_id)
+{
+ Gtk::ComboBox *combo;
+ builder->get_widget(widget_name, combo);
+ if (combo) {
+ if (unique_id.empty()) {
+ combo->set_active(0); // Select the first
+ } else if(!combo->set_active_id(unique_id)) {
+ combo->set_active(-1); // Select nothing
+ }
+ }
+}
+
+/**
+ * When a notbook is switched, reveal the right banner image (gtk signal).
+ */
+void
+StartScreen::notebook_switch(Gtk::Widget *tab, guint page_num)
+{
+ int page = 0;
+ for (auto banner : banners->get_children()) {
+ if (auto revealer = dynamic_cast<Gtk::Revealer *>(banner)) {
+ revealer->set_reveal_child(page == page_num);
+ page++;
+ }
+ }
+}
+
+void
+StartScreen::enlist_recent_files()
+{
+ RecentCols cols;
+ if (!recent_treeview) return;
+ // We're not sure why we have to ask C for the TreeStore object
+ auto store = Glib::wrap(GTK_LIST_STORE(gtk_tree_view_get_model(recent_treeview->gobj())));
+ store->clear();
+ // Now sort the result by visited time
+ store->set_sort_column(cols.col_dt, Gtk::SORT_DESCENDING);
+
+ // Open [other]
+ Gtk::TreeModel::Row first_row = *(store->append());
+ first_row[cols.col_name] = _("Browse for other files...");
+ first_row[cols.col_id] = "";
+ first_row[cols.col_dt] = std::numeric_limits<gint64>::max();
+ recent_treeview->get_selection()->select(store->get_path(first_row));
+
+ Glib::RefPtr<Gtk::RecentManager> manager = Gtk::RecentManager::get_default();
+ for (auto item : manager->get_items()) {
+ if (item->has_application(g_get_prgname())
+ || item->has_application("org.inkscape.Inkscape")
+ || item->has_application("inkscape")
+ || item->has_application("inkscape.exe")
+ ) {
+ // This uri is a GVFS uri, so parse it with that or it will fail.
+ auto file = Gio::File::create_for_uri(item->get_uri());
+ std::string path = file->get_path();
+ if (!path.empty() && Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR)
+ && item->get_mime_type() == "image/svg+xml") {
+ Gtk::TreeModel::Row row = *(store->append());
+ row[cols.col_name] = item->get_display_name();
+ row[cols.col_id] = item->get_uri();
+ row[cols.col_dt] = item->get_modified();
+ row[cols.col_crash] = item->has_group("Crash");
+ }
+ }
+ }
+
+}
+
+/**
+ * Called when a new recent document is selected.
+ */
+void
+StartScreen::on_recent_changed()
+{
+ // TODO: In the future this is where previews and other information can be loaded.
+}
+
+/**
+ * Called when the left side tabs are changed.
+ */
+void
+StartScreen::on_kind_changed(Gtk::Widget *tab, guint page_num)
+{
+ if (page_num == 0) {
+ load_btn->show();
+ } else {
+ load_btn->hide();
+ }
+}
+
+/**
+ * Called when new button clicked or template is double clicked, or escape pressed.
+ */
+void
+StartScreen::new_document()
+{
+ // Generate a new document from the selected template.
+ _document = templates->new_document();
+ if (_document) {
+ // Quit welcome screen if options not 'canceled'
+ response(GTK_RESPONSE_APPLY);
+ }
+}
+
+/**
+ * Called when load button clicked.
+ */
+void
+StartScreen::load_document()
+{
+ RecentCols cols;
+ auto prefs = Inkscape::Preferences::get();
+ auto app = InkscapeApplication::instance();
+
+ if (!recent_treeview)
+ return;
+
+ auto iter = recent_treeview->get_selection()->get_selected();
+ if (iter) {
+ Gtk::TreeModel::Row row = *iter;
+ if (row) {
+ Glib::ustring _file = row[cols.col_id];
+ Glib::RefPtr<Gio::File> file;
+
+ if (!_file.empty()) {
+ file = Gio::File::create_for_uri(_file);
+ } else {
+ Glib::ustring open_path = prefs->getString("/dialogs/open/path");
+ if (open_path.empty()) {
+ open_path = g_get_home_dir();
+ open_path.append(G_DIR_SEPARATOR_S);
+ }
+
+ // Browse for file instead
+ auto browser = Inkscape::UI::Dialog::FileOpenDialog::create(
+ *this, open_path, Inkscape::UI::Dialog::SVG_TYPES, _("Open a different file"));
+
+ if (browser->show()) {
+ prefs->setString("/dialogs/open/path", browser->getCurrentDirectory());
+ file = Gio::File::create_for_path(browser->getFilename());
+ delete browser;
+ } else {
+ delete browser;
+ return; // Cancel
+ }
+ }
+
+ // Now we have filename, open document.
+ bool canceled = false;
+ _document = app->document_open(file, &canceled);
+
+ if (!canceled && _document) {
+ // We're done, hand back to app.
+ response(GTK_RESPONSE_OK);
+ }
+ }
+ }
+}
+
+/**
+ * When a button needs to go to the next notebook page.
+ */
+void
+StartScreen::notebook_next(Gtk::Widget *button)
+{
+ int page = tabs->get_current_page();
+ if (page == 2) {
+ response(GTK_RESPONSE_CANCEL); // Only occurs from keypress.
+ } else {
+ tabs->set_current_page(page + 1);
+ }
+}
+
+/**
+ * When a key is pressed in the main window.
+ */
+bool
+StartScreen::on_key_press_event(GdkEventKey* event)
+{
+#ifdef GDK_WINDOWING_QUARTZ
+ // On macOS only, if user press Cmd+Q => exit
+ if (event->keyval == 'q' && event->state == (GDK_MOD2_MASK | GDK_META_MASK)) {
+ close();
+ return false;
+ }
+#endif
+ switch (event->keyval) {
+ case GDK_KEY_Escape:
+ // Prevent loading any selected items
+ response(GTK_RESPONSE_CANCEL);
+ return true;
+ case GDK_KEY_Return:
+ notebook_next(nullptr);
+ return true;
+ }
+
+ return false;
+}
+
+void
+StartScreen::on_response(int response_id)
+{
+ if (response_id == GTK_RESPONSE_DELETE_EVENT) {
+ // Don't open a window for force closing.
+ return;
+ }
+ if (response_id == GTK_RESPONSE_CANCEL) {
+ templates->reset_selection();
+ }
+ if (response_id != GTK_RESPONSE_OK && !_document) {
+ // Last ditch attempt to generate a new document while exiting.
+ _document = templates->new_document();
+ }
+}
+
+void
+StartScreen::show_toggle()
+{
+ Gtk::ToggleButton *button;
+ builder->get_widget("show_toggle", button);
+ if (button) {
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setBool("/options/boot/enabled", button->get_active());
+ } else {
+ g_warning("Can't find toggle button widget.");
+ }
+}
+
+/**
+ * Refresh theme in-place so user can see a semi-preview. This theme selection
+ * is not meant to be perfect, but hint to the user that they can set the
+ * theme if they want.
+ *
+ * @param theme_name - The name of the theme to load.
+ */
+void
+StartScreen::refresh_theme(Glib::ustring theme_name)
+{
+ auto const screen = Gdk::Screen::get_default();
+ if (INKSCAPE.themecontext->getContrastThemeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getContrastThemeProvider());
+ }
+ auto settings = Gtk::Settings::get_default();
+
+ auto prefs = Inkscape::Preferences::get();
+
+ settings->property_gtk_theme_name() = theme_name;
+ settings->property_gtk_application_prefer_dark_theme() = prefs->getBool("/theme/preferDarkTheme", true);
+ settings->property_gtk_icon_theme_name() = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
+
+ if (prefs->getBool("/theme/symbolicIcons", false)) {
+ get_style_context()->add_class("symbolic");
+ get_style_context()->remove_class("regular");
+ } else {
+ get_style_context()->add_class("regular");
+ get_style_context()->remove_class("symbolic");
+ }
+
+ if (INKSCAPE.themecontext->getColorizeProvider()) {
+ Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider());
+ }
+ if (!prefs->getBool("/theme/symbolicDefaultHighColors", false)) {
+ Gtk::CssProvider::create();
+ Glib::ustring css_str = INKSCAPE.themecontext->get_symbolic_colors();
+ try {
+ INKSCAPE.themecontext->getColorizeProvider()->load_from_data(css_str);
+ } catch (const Gtk::CssProviderError &ex) {
+ g_critical("CSSProviderError::load_from_data(): failed to load '%s'\n(%s)", css_str.c_str(), ex.what().c_str());
+ }
+ Gtk::StyleContext::add_provider_for_screen(screen, INKSCAPE.themecontext->getColorizeProvider(),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
+ // set dark switch and disable if there is no prefer option for dark
+ refresh_dark_switch();
+
+ INKSCAPE.themecontext->getChangeThemeSignal().emit();
+}
+
+/**
+ * Set the theme, icon pack and other theme options from a set defined
+ * in the glade file. The combo box has a number of columns with the needed
+ * data describing how to set up the theme.
+ */
+void
+StartScreen::theme_changed()
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ ThemeCols cols;
+ try {
+ auto row = active_combo("themes");
+ Glib::ustring theme_id = row[cols.id];
+ if (theme_id == "custom") return;
+ prefs->setString("/options/boot/theme", row[cols.id]);
+
+ // Update theme from combo.
+ Glib::ustring icons = row[cols.icons];
+ prefs->setBool("/toolbox/tools/small", row[cols.smallicons]);
+ prefs->setString("/theme/gtkTheme", row[cols.theme]);
+ prefs->setString("/theme/iconTheme", icons);
+ prefs->setBool("/theme/symbolicIcons", row[cols.symbolic]);
+
+ Gtk::Switch* dark_toggle = nullptr;
+ builder->get_widget("dark_toggle", dark_toggle);
+ bool is_dark = dark_toggle->get_active();
+ prefs->setBool("/theme/preferDarkTheme", is_dark);
+ prefs->setBool("/theme/darkTheme", is_dark);
+ // Symbolic icon colours
+ if (get_color_value(row[cols.base]) == 0) {
+ prefs->setBool("/theme/symbolicDefaultBaseColors", true);
+ prefs->setBool("/theme/symbolicDefaultHighColors", true);
+ } else {
+ Glib::ustring prefix = "/theme/" + icons;
+ prefs->setBool("/theme/symbolicDefaultBaseColors", false);
+ prefs->setBool("/theme/symbolicDefaultHighColors", false);
+ if (is_dark) {
+ prefs->setUInt(prefix + "/symbolicBaseColor", get_color_value(row[cols.base_dark]));
+ } else {
+ prefs->setUInt(prefix + "/symbolicBaseColor", get_color_value(row[cols.base]));
+ }
+ prefs->setUInt(prefix + "/symbolicSuccessColor", get_color_value(row[cols.success]));
+ prefs->setUInt(prefix + "/symbolicWarningColor", get_color_value(row[cols.warn]));
+ prefs->setUInt(prefix + "/symbolicErrorColor", get_color_value(row[cols.error]));
+ }
+
+ refresh_theme(prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", "")));
+ } catch(int e) {
+ g_warning("Couldn't find theme value.");
+ }
+}
+
+/**
+ * Called when the canvas dropdown changes.
+ */
+void
+StartScreen::canvas_changed()
+{
+ CanvasCols cols;
+ try {
+ auto row = active_combo("canvas");
+
+ auto prefs = Inkscape::Preferences::get();
+ prefs->setString("/options/boot/canvas", row[cols.id]);
+
+ Gdk::RGBA gdk_color = Gdk::RGBA(row[cols.pagecolor]);
+ SPColor sp_color(gdk_color.get_red(), gdk_color.get_green(), gdk_color.get_blue());
+ prefs->setString("/template/base/pagecolor", sp_color.toString());
+ prefs->setDouble("/template/base/pageopacity", gdk_color.get_alpha());
+
+ Gdk::RGBA gdk_border = Gdk::RGBA(row[cols.bordercolor]);
+ SPColor sp_border(gdk_border.get_red(), gdk_border.get_green(), gdk_border.get_blue());
+ prefs->setString("/template/base/bordercolor", sp_border.toString());
+ prefs->setDouble("/template/base/borderopacity", gdk_border.get_alpha());
+
+ prefs->setBool("/template/base/pagecheckerboard", row[cols.checkered]);
+ prefs->setInt("/template/base/pageshadow", row[cols.shadow] ? 2 : 0);
+
+ Gdk::RGBA gdk_desk = Gdk::RGBA(row[cols.deskcolor]);
+ SPColor sp_desk(gdk_desk.get_red(), gdk_desk.get_green(), gdk_desk.get_blue());
+ prefs->setString("/template/base/deskcolor", sp_desk.toString());
+ } catch(int e) {
+ g_warning("Couldn't find canvas value.");
+ }
+}
+
+void
+StartScreen::filter_themes()
+{
+ ThemeCols cols;
+ // We need to disable themes which aren't available.
+ auto store = Glib::wrap(GTK_LIST_STORE(gtk_combo_box_get_model(themes->gobj())));
+ auto available = INKSCAPE.themecontext->get_available_themes();
+
+ // Detect use of custom theme here, detect defaults used in many systems.
+ auto settings = Gtk::Settings::get_default();
+ Glib::ustring theme_name = settings->property_gtk_theme_name();
+ Glib::ustring icons_name = settings->property_gtk_icon_theme_name();
+
+ bool has_system_theme = false;
+ if (theme_name != "Adwaita" || icons_name != "hicolor") {
+ has_system_theme = true;
+ /* Enable if/when we want custom to be the default.
+ if (prefs->getString("/options/boot/theme").empty()) {
+ prefs->setString("/options/boot/theme", "system")
+ theme_changed();
+ }*/
+ }
+
+ for(auto row : store->children()) {
+ Glib::ustring theme = row[cols.theme];
+ if (!row[cols.enabled]) {
+ // Available themes; We only "enable" them, we don't disable them.
+ row[cols.enabled] = available.find(theme) != available.end();
+ } else if(row[cols.id] == "system" && !has_system_theme) {
+ // Disable system theme option if not available.
+ row[cols.enabled] = false;
+ }
+ }
+}
+
+void
+StartScreen::enlist_keys()
+{
+ NameIdCols cols;
+ Gtk::ComboBox *keys;
+ builder->get_widget("keys", keys);
+ if (!keys) return;
+
+ auto store = Glib::wrap(GTK_LIST_STORE(gtk_combo_box_get_model(keys->gobj())));
+ store->clear();
+
+ for(auto item: Inkscape::Shortcuts::get_file_names()){
+ Gtk::TreeModel::Row row = *(store->append());
+ row[cols.col_name] = item.first;
+ row[cols.col_id] = item.second;
+ }
+
+ auto prefs = Inkscape::Preferences::get();
+ auto current = prefs->getString("/options/kbshortcuts/shortcutfile");
+ if (current.empty()) {
+ current = "inkscape.xml";
+ }
+ keys->set_active_id(current);
+}
+
+/**
+ * Set the keys file based on the keys set in the enlist above
+ */
+void
+StartScreen::keyboard_changed()
+{
+ NameIdCols cols;
+ try {
+ auto row = active_combo("keys");
+ auto prefs = Inkscape::Preferences::get();
+ Glib::ustring set_to = row[cols.col_id];
+ prefs->setString("/options/kbshortcuts/shortcutfile", set_to);
+ Inkscape::Shortcuts::getInstance().init();
+
+ Gtk::InfoBar* keys_warning;
+ builder->get_widget("keys_warning", keys_warning);
+ if (set_to != "inkscape.xml" && set_to != "default.xml") {
+ keys_warning->set_message_type(Gtk::MessageType::MESSAGE_WARNING);
+ keys_warning->show();
+ } else {
+ keys_warning->hide();
+ }
+ } catch(int e) {
+ g_warning("Couldn't find keys value.");
+ }
+}
+
+/**
+ * Set Dark Switch based on current selected theme.
+ * We will disable switch if current theme doesn't have prefer dark theme option.
+ */
+
+void StartScreen::refresh_dark_switch()
+{
+ auto prefs = Inkscape::Preferences::get();
+
+ Gtk::Container *window = dynamic_cast<Gtk::Container *>(get_toplevel());
+ bool dark = INKSCAPE.themecontext->isCurrentThemeDark(window);
+ prefs->setBool("/theme/preferDarkTheme", dark);
+ prefs->setBool("/theme/darkTheme", dark);
+
+ auto themes = INKSCAPE.themecontext->get_available_themes();
+ Glib::ustring current_theme = prefs->getString("/theme/gtkTheme", prefs->getString("/theme/defaultGtkTheme", ""));
+
+ Gtk::Switch *dark_toggle = nullptr;
+ builder->get_widget("dark_toggle", dark_toggle);
+
+ if (!themes[current_theme]) {
+ dark_toggle->set_sensitive(false);
+ } else {
+ dark_toggle->set_sensitive(true);
+ }
+ dark_toggle->set_active(dark);
+}
+
+} // 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/dialog/startup.h b/src/ui/dialog/startup.h
new file mode 100644
index 0000000..ca0fcf3
--- /dev/null
+++ b/src/ui/dialog/startup.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for the start screen
+ *
+ * Copyright (C) Martin Owens 2020 <doctormo@gmail.com>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef STARTSCREEN_H
+#define STARTSCREEN_H
+
+#include <gtkmm.h>
+
+class SPDocument;
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class TemplateList;
+}
+
+namespace Dialog {
+
+class StartScreen : public Gtk::Dialog {
+
+public:
+ StartScreen();
+ ~StartScreen() override;
+
+ SPDocument* get_document() { return _document; }
+
+protected:
+ bool on_key_press_event(GdkEventKey* event) override;
+ void on_response(int response_id) override;
+
+private:
+ void notebook_next(Gtk::Widget *button);
+ Gtk::TreeModel::Row active_combo(std::string widget_name);
+ void set_active_combo(std::string widget_name, std::string unique_id);
+ void show_toggle();
+ void enlist_recent_files();
+ void enlist_keys();
+ void filter_themes();
+ void keyboard_changed();
+ void notebook_switch(Gtk::Widget *tab, guint page_num);
+
+ void theme_changed();
+ void canvas_changed();
+ void refresh_theme(Glib::ustring theme_name);
+ void refresh_dark_switch();
+
+ void new_document();
+ void load_document();
+ void on_recent_changed();
+ void on_kind_changed(Gtk::Widget *tab, guint page_num);
+
+
+private:
+ Glib::RefPtr<Gtk::Builder> builder;
+ Gtk::Window *window = nullptr;
+ Gtk::Notebook *tabs = nullptr;
+ Gtk::Fixed *banners = nullptr;
+ Gtk::ComboBox *themes = nullptr;
+ Gtk::TreeView *recent_treeview = nullptr;
+ Gtk::Button *load_btn = nullptr;
+ Inkscape::UI::Widget::TemplateList *templates = nullptr;
+
+ SPDocument* _document = nullptr;
+};
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // STARTSCREEN_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/dialog/styledialog.cpp b/src/ui/dialog/styledialog.cpp
new file mode 100644
index 0000000..efb7199
--- /dev/null
+++ b/src/ui/dialog/styledialog.cpp
@@ -0,0 +1,1601 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for CSS styles
+ */
+/* Authors:
+ * Kamalpreet Kaur Grewal
+ * Tavmjong Bah
+ * Jabiertxof
+ *
+ * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com>
+ * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "styledialog.h"
+
+#include <map>
+#include <regex>
+#include <utility>
+
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include "attribute-rel-svg.h"
+#include "attributes.h"
+#include "document-undo.h"
+#include "inkscape.h"
+#include "io/resource.h"
+#include "selection.h"
+#include "style-internal.h"
+#include "style.h"
+
+#include "svg/svg-color.h"
+#include "ui/icon-loader.h"
+#include "ui/widget/iconrenderer.h"
+#include "util/trim.h"
+#include "xml/attribute-record.h"
+#include "xml/node-observer.h"
+#include "xml/sp-css-attr.h"
+
+// G_MESSAGES_DEBUG=DEBUG_STYLEDIALOG gdb ./inkscape
+// #define DEBUG_STYLEDIALOG
+// #define G_LOG_DOMAIN "STYLEDIALOG"
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+
+/**
+ * Get the first <style> element's first text node. If no such node exists and
+ * `create_if_missing` is false, then return NULL.
+ *
+ * Only finds <style> elements in root or in root-level <defs>.
+ */
+XML::Node *get_first_style_text_node(XML::Node *root, bool create_if_missing)
+{
+ static GQuark const CODE_svg_style = g_quark_from_static_string("svg:style");
+ static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs");
+
+ XML::Node *styleNode = nullptr;
+ XML::Node *textNode = nullptr;
+
+ if (!root) {
+ return nullptr;
+ }
+
+ for (auto *node = root->firstChild(); node; node = node->next()) {
+ if (node->code() == CODE_svg_defs) {
+ textNode = get_first_style_text_node(node, false);
+ if (textNode != nullptr) {
+ return textNode;
+ }
+ }
+
+ if (node->code() == CODE_svg_style) {
+ styleNode = node;
+ break;
+ }
+ }
+
+ if (styleNode == nullptr) {
+ if (!create_if_missing)
+ return nullptr;
+
+ styleNode = root->document()->createElement("svg:style");
+ root->addChild(styleNode, nullptr);
+ Inkscape::GC::release(styleNode);
+ }
+
+ for (auto *node = styleNode->firstChild(); node; node = node->next()) {
+ if (node->type() == XML::NodeType::TEXT_NODE) {
+ textNode = node;
+ break;
+ }
+ }
+
+ if (textNode == nullptr) {
+ if (!create_if_missing)
+ return nullptr;
+
+ textNode = root->document()->createTextNode("");
+ styleNode->appendChild(textNode);
+ Inkscape::GC::release(textNode);
+ }
+
+ return textNode;
+}
+
+namespace UI {
+namespace Dialog {
+
+// Keeps a watch on style element
+class StyleDialog::NodeObserver : public Inkscape::XML::NodeObserver {
+ public:
+ NodeObserver(StyleDialog *styledialog)
+ : _styledialog(styledialog)
+ {
+ g_debug("StyleDialog::NodeObserver: Constructor");
+ };
+
+ void notifyContentChanged(Inkscape::XML::Node &node, Inkscape::Util::ptr_shared old_content,
+ Inkscape::Util::ptr_shared new_content) override;
+
+ StyleDialog *_styledialog;
+};
+
+
+void StyleDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node & /*node*/,
+ Inkscape::Util::ptr_shared /*old_content*/,
+ Inkscape::Util::ptr_shared /*new_content*/)
+{
+
+ g_debug("StyleDialog::NodeObserver::notifyContentChanged");
+ _styledialog->_updating = false;
+ _styledialog->readStyleElement();
+}
+
+
+// Keeps a watch for new/removed/changed nodes
+// (Must update objects that selectors match.)
+class StyleDialog::NodeWatcher : public Inkscape::XML::NodeObserver {
+ public:
+ NodeWatcher(StyleDialog *styledialog)
+ : _styledialog(styledialog)
+ {
+ g_debug("StyleDialog::NodeWatcher: Constructor");
+ };
+
+ void notifyChildAdded(Inkscape::XML::Node & /*node*/, Inkscape::XML::Node &child,
+ Inkscape::XML::Node * /*prev*/) override
+ {
+ _styledialog->_nodeAdded(child);
+ }
+
+ void notifyChildRemoved(Inkscape::XML::Node & /*node*/, Inkscape::XML::Node &child,
+ Inkscape::XML::Node * /*prev*/) override
+ {
+ _styledialog->_nodeRemoved(child);
+ }
+ void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark qname, Util::ptr_shared /*old_value*/,
+ Util::ptr_shared /*new_value*/) override
+ {
+ static GQuark const CODE_id = g_quark_from_static_string("id");
+ static GQuark const CODE_class = g_quark_from_static_string("class");
+ static GQuark const CODE_style = g_quark_from_static_string("style");
+
+ if (qname == CODE_id || qname == CODE_class || qname == CODE_style) {
+ _styledialog->_nodeChanged(node);
+ }
+ }
+
+ StyleDialog *_styledialog;
+};
+
+void StyleDialog::_nodeAdded(Inkscape::XML::Node &node)
+{
+ if (!getShowing()) {
+ return;
+ }
+ readStyleElement();
+}
+
+void StyleDialog::_nodeRemoved(Inkscape::XML::Node &repr)
+{
+ if (!getShowing()) {
+ return;
+ }
+ if (_textNode == &repr) {
+ _textNode = nullptr;
+ }
+
+ readStyleElement();
+}
+
+void StyleDialog::_nodeChanged(Inkscape::XML::Node &object)
+{
+ if (!getShowing()) {
+ return;
+ }
+ g_debug("StyleDialog::_nodeChanged");
+ readStyleElement();
+}
+
+/**
+ * Constructor
+ * A treeview and a set of two buttons are added to the dialog. _addSelector
+ * adds selectors to treeview. _delSelector deletes the selector from the dialog.
+ * Any addition/deletion of the selectors updates XML style element accordingly.
+ */
+StyleDialog::StyleDialog()
+ : DialogBase("/dialogs/style", "Style")
+{
+ g_debug("StyleDialog::StyleDialog");
+
+ m_nodewatcher.reset(new StyleDialog::NodeWatcher(this));
+ m_styletextwatcher.reset(new StyleDialog::NodeObserver(this));
+
+ // Pack widgets
+ _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET);
+ _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _styleBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+ _styleBox.set_valign(Gtk::ALIGN_START);
+ _scrolledWindow.add(_styleBox);
+ _scrolledWindow.set_overlay_scrolling(false);
+ _vadj = _scrolledWindow.get_vadjustment();
+ _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &StyleDialog::_vscroll));
+ _mainBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
+
+ pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET);
+}
+
+StyleDialog::~StyleDialog()
+{
+ removeObservers();
+}
+
+void StyleDialog::_vscroll()
+{
+ if (!_scrollock) {
+ _scrollpos = _vadj->get_value();
+ } else {
+ _vadj->set_value(_scrollpos);
+ _scrollock = false;
+ }
+}
+
+Glib::ustring StyleDialog::fixCSSSelectors(Glib::ustring selector)
+{
+ g_debug("SelectorsDialog::fixCSSSelectors");
+ Util::trim(selector);
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selector);
+ CRSelector *cr_selector = cr_selector_parse_from_buf((guchar const *)selector.c_str(), CR_UTF_8);
+ for (auto token : tokens) {
+ Util::trim(token);
+ std::vector<Glib::ustring> subtokens = Glib::Regex::split_simple("[ ]+", token);
+ for (auto subtoken : subtokens) {
+ Util::trim(subtoken);
+ CRSelector *cr_selector = cr_selector_parse_from_buf((guchar const *)subtoken.c_str(), CR_UTF_8);
+ gchar *selectorchar = reinterpret_cast<gchar *>(cr_selector_to_string(cr_selector));
+ if (selectorchar) {
+ Glib::ustring toadd = Glib::ustring(selectorchar);
+ g_free(selectorchar);
+ if (toadd[0] != '.' && toadd[0] != '#' && toadd.size() > 1) {
+ auto i = std::min(toadd.find("#"), toadd.find("."));
+ Glib::ustring tag = toadd;
+ if (i != std::string::npos) {
+ tag = tag.substr(0, i);
+ }
+ if (!SPAttributeRelSVG::isSVGElement(tag)) {
+ if (tokens.size() == 1) {
+ tag = "." + tag;
+ return tag;
+ } else {
+ return "";
+ }
+ }
+ }
+ }
+ }
+ }
+ if (cr_selector) {
+ return selector;
+ }
+ return "";
+}
+
+void StyleDialog::_reload() { readStyleElement(); }
+
+/**
+ * @return Inkscape::XML::Node* pointing to a style element's text node.
+ * Returns the style element's text node. If there is no style element, one is created.
+ * Ditto for text node.
+ */
+Inkscape::XML::Node *StyleDialog::_getStyleTextNode(bool create_if_missing)
+{
+ g_debug("StyleDialog::_getStyleTextNoded");
+
+ auto textNode = Inkscape::get_first_style_text_node(m_root, create_if_missing);
+
+ if (_textNode != textNode) {
+ if (_textNode) {
+ _textNode->removeObserver(*m_styletextwatcher);
+ }
+
+ _textNode = textNode;
+
+ if (_textNode) {
+ _textNode->addObserver(*m_styletextwatcher);
+ }
+ }
+
+ return textNode;
+}
+
+Glib::RefPtr<Gtk::TreeModel> StyleDialog::_selectTree(Glib::ustring selector)
+{
+ g_debug("StyleDialog::_selectTree");
+
+ Gtk::Label *selectorlabel;
+ Glib::RefPtr<Gtk::TreeModel> model;
+ for (auto fullstyle : _styleBox.get_children()) {
+ Gtk::Box *style = dynamic_cast<Gtk::Box *>(fullstyle);
+ for (auto stylepart : style->get_children()) {
+ switch (style->child_property_position(*stylepart)) {
+ case 0: {
+ Gtk::Box *selectorbox = dynamic_cast<Gtk::Box *>(stylepart);
+ for (auto styleheader : selectorbox->get_children()) {
+ if (!selectorbox->child_property_position(*styleheader)) {
+ selectorlabel = dynamic_cast<Gtk::Label *>(styleheader);
+ }
+ }
+ break;
+ }
+ case 1: {
+ Glib::ustring wdg_selector = selectorlabel->get_text();
+ if (wdg_selector == selector) {
+ Gtk::TreeView *treeview = dynamic_cast<Gtk::TreeView *>(stylepart);
+ if (treeview) {
+ return treeview->get_model();
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+ return model;
+}
+
+void StyleDialog::setCurrentSelector(Glib::ustring current_selector)
+{
+ g_debug("StyleDialog::setCurrentSelector");
+ _current_selector = current_selector;
+ readStyleElement();
+}
+
+// copied from style.cpp:1499
+static bool is_url(char const *p)
+{
+ if (p == nullptr)
+ return false;
+ /** \todo
+ * FIXME: I'm not sure if this applies to SVG as well, but CSS2 says any URIs
+ * in property values must start with 'url('.
+ */
+ return (g_ascii_strncasecmp(p, "url(", 4) == 0);
+}
+
+/**
+ * Fill the Gtk::TreeStore from the svg:style element.
+ */
+void StyleDialog::readStyleElement()
+{
+ g_debug("StyleDialog::readStyleElement");
+
+ auto document = getDocument();
+ if (_updating || !document || _deletion)
+ return; // Don't read if we wrote style element.
+ _updating = true;
+ _scrollock = true;
+ Inkscape::XML::Node *textNode = _getStyleTextNode();
+
+ // Get content from style text node.
+ std::string content = (textNode && textNode->content()) ? textNode->content() : "";
+
+ // Remove end-of-lines (check it works on Windoze).
+ content.erase(std::remove(content.begin(), content.end(), '\n'), content.end());
+
+ // Remove comments (/* xxx */)
+
+ bool breakme = false;
+ size_t start = content.find("/*");
+ size_t open = content.find("{", start + 1);
+ size_t close = content.find("}", start + 1);
+ size_t end = content.find("*/", close + 1);
+ while (!breakme) {
+ if (open == std::string::npos || close == std::string::npos || end == std::string::npos) {
+ breakme = true;
+ break;
+ }
+ while (open < close) {
+ open = content.find("{", close + 1);
+ close = content.find("}", close + 1);
+ end = content.find("*/", close + 1);
+ size_t reopen = content.find("{", close + 1);
+ if (open == std::string::npos || end == std::string::npos || end < reopen) {
+ if (end < reopen) {
+ content = content.erase(start, end - start + 2);
+ } else {
+ breakme = true;
+ }
+ break;
+ }
+ }
+ start = content.find("/*", start + 1);
+ open = content.find("{", start + 1);
+ close = content.find("}", start + 1);
+ end = content.find("*/", close + 1);
+ }
+
+ // First split into selector/value chunks.
+ // An attempt to use Glib::Regex failed. A C++11 version worked but
+ // reportedly has problems on Windows. Using split_simple() is simpler
+ // and probably faster.
+ //
+ // Glib::RefPtr<Glib::Regex> regex1 =
+ // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}");
+ //
+ // Glib::MatchInfo minfo;
+ // regex1->match(content, minfo);
+
+ // Split on curly brackets. Even tokens are selectors, odd are values.
+ std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", content);
+ _owner_style.clear();
+ // If text node is empty, return (avoids problem with negative below).
+
+ for (auto child : _styleBox.get_children()) {
+ _styleBox.remove(*child);
+ delete child;
+ }
+ Inkscape::Selection *selection = getSelection();
+ SPObject *obj = nullptr;
+ if (selection->objects().size() == 1) {
+ obj = selection->objects().back();
+ }
+ if (!obj) {
+ obj = document->getXMLDialogSelectedObject();
+ if (obj && !obj->getRepr()) {
+ obj = nullptr; // treat detached object as no selection
+ }
+ }
+
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-css.glade");
+ Glib::RefPtr<Gtk::Builder> _builder;
+ try {
+ _builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for filter effect dialog");
+ return;
+ }
+ gint selectorpos = 0;
+ Gtk::Box *css_selector_container;
+ _builder->get_widget("CSSSelectorContainer", css_selector_container);
+ Gtk::Label *css_selector;
+ _builder->get_widget("CSSSelector", css_selector);
+ Gtk::EventBox *css_selector_event_add;
+ _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add);
+ css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK);
+ css_selector->set_text("element");
+ Gtk::TreeView *css_tree;
+ _builder->get_widget("CSSTree", css_tree);
+ css_tree->get_style_context()->add_class("style_element");
+ Glib::RefPtr<Gtk::TreeStore> store = Gtk::TreeStore::create(_mColumns);
+ css_tree->set_model(store);
+ css_selector_event_add->signal_button_release_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>(
+ sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, "style_properties", selectorpos));
+ Inkscape::UI::Widget::IconRenderer *addRenderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ addRenderer->add_icon("edit-delete");
+ int addCol = css_tree->append_column(" ", *addRenderer) - 1;
+ Gtk::TreeViewColumn *col = css_tree->get_column(addCol);
+ if (col) {
+ addRenderer->signal_activated().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store));
+ }
+ Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText());
+ label->property_placeholder_text() = _("property");
+ label->property_editable() = true;
+ label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>(
+ sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree));
+ label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit));
+ addCol = css_tree->append_column(" ", *label) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->set_resizable(true);
+ col->add_attribute(label->property_text(), _mColumns._colName);
+ }
+ Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText());
+ value->property_placeholder_text() = _("value");
+ value->property_editable() = true;
+ value->signal_edited().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_valueEdited), store));
+ value->signal_editing_started().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store));
+ addCol = css_tree->append_column(" ", *value) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->add_attribute(value->property_text(), _mColumns._colValue);
+ col->set_expand(true);
+ col->add_attribute(value->property_strikethrough(), _mColumns._colStrike);
+ }
+ Inkscape::UI::Widget::IconRenderer *urlRenderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ urlRenderer->add_icon("empty-icon");
+ urlRenderer->add_icon("edit-redo");
+ int urlCol = css_tree->append_column(" ", *urlRenderer) - 1;
+ Gtk::TreeViewColumn *urlcol = css_tree->get_column(urlCol);
+ if (urlcol) {
+ urlcol->set_min_width(40);
+ urlcol->set_max_width(40);
+ urlRenderer->signal_activated().connect(sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onLinkObj), store));
+ urlcol->add_attribute(urlRenderer->property_icon(), _mColumns._colLinked);
+ }
+ std::map<Glib::ustring, Glib::ustring> attr_prop;
+ Gtk::TreeModel::Path path;
+ bool empty = true;
+ if (obj && obj->getRepr()->attribute("style")) {
+ Glib::ustring style = obj->getRepr()->attribute("style");
+ attr_prop = parseStyle(style);
+ for (auto iter : obj->style->properties()) {
+ if (attr_prop.count(iter->name())) {
+ auto value = attr_prop[iter->name()];
+ empty = false;
+ Gtk::TreeModel::Row row = *(store->prepend());
+ row[_mColumns._colSelector] = "style_properties";
+ row[_mColumns._colSelectorPos] = 0;
+ row[_mColumns._colActive] = true;
+ row[_mColumns._colName] = iter->name();
+ row[_mColumns._colValue] = value;
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Current value");
+ row[_mColumns._colHref] = nullptr;
+ row[_mColumns._colLinked] = false;
+ if (is_url(value.c_str())) {
+ auto id = value.substr(5, value.size() - 6);
+ SPObject *elemref = nullptr;
+ if ((elemref = document->getObjectById(id.c_str()))) {
+ row[_mColumns._colHref] = elemref;
+ row[_mColumns._colLinked] = true;
+ }
+ }
+ _addOwnerStyle(iter->name(), "style attribute");
+ }
+ }
+ // this is to fix a bug on cairo win:
+ // https://gitlab.freedesktop.org/cairo/cairo/issues/338
+ // TODO: check if inkscape min cairo version has applied the patch proposed and remove (3 times)
+ if (empty) {
+ css_tree->hide();
+ }
+ _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET);
+ }
+ selectorpos++;
+ if (tokens.size() == 0) {
+ _updating = false;
+ return;
+ }
+ for (unsigned i = 0; i < tokens.size() - 1; i += 2) {
+ Glib::ustring selector = tokens[i];
+ Util::trim(selector); // Remove leading/trailing spaces
+ // Get list of objects selector matches
+ std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector);
+ Glib::ustring selector_orig = selector;
+ if (!selectordata.empty()) {
+ selector = selectordata.back();
+ }
+ std::vector<SPObject *> objVec = _getObjVec(selector);
+ if (obj) {
+ bool stop = true;
+ for (auto objel : objVec) {
+ if (objel == obj) {
+ stop = false;
+ }
+ }
+ if (stop) {
+ _updating = false;
+ selectorpos++;
+ continue;
+ }
+ }
+ if (!obj && _current_selector != "" && _current_selector != selector) {
+ _updating = false;
+ selectorpos++;
+ continue;
+ }
+ if (!obj) {
+ bool present = false;
+ for (auto objv : objVec) {
+ for (auto objsel : selection->objects()) {
+ if (objv == objsel) {
+ present = true;
+ break;
+ }
+ }
+ if (present) {
+ break;
+ }
+ }
+ if (!present) {
+ _updating = false;
+ selectorpos++;
+ continue;
+ }
+ }
+ Glib::ustring properties;
+ // Check to make sure we do have a value to match selector.
+ if ((i + 1) < tokens.size()) {
+ properties = tokens[i + 1];
+ } else {
+ std::cerr << "StyleDialog::readStyleElement: Missing values "
+ "for last selector!"
+ << std::endl;
+ }
+ Glib::RefPtr<Gtk::Builder> _builder;
+ try {
+ _builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for filter effect dialog");
+ return;
+ }
+ Gtk::Box *css_selector_container;
+ _builder->get_widget("CSSSelectorContainer", css_selector_container);
+ Gtk::Label *css_selector;
+ _builder->get_widget("CSSSelector", css_selector);
+ Gtk::EventBox *css_selector_event_box;
+ _builder->get_widget("CSSSelectorEventBox", css_selector_event_box);
+ Gtk::Entry *css_edit_selector;
+ _builder->get_widget("CSSEditSelector", css_edit_selector);
+ Gtk::EventBox *css_selector_event_add;
+ _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add);
+ css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK);
+ css_selector->set_text(selector);
+ Gtk::TreeView *css_tree;
+ _builder->get_widget("CSSTree", css_tree);
+ css_tree->get_style_context()->add_class("style_sheet");
+ Glib::RefPtr<Gtk::TreeStore> store = Gtk::TreeStore::create(_mColumns);
+ css_tree->set_model(store);
+ // I comment this feature, is working but seems obscure to understand
+ // the user can edit selector name in current implementation
+ /* css_selector_event_box->signal_button_release_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_selectorStartEdit), css_selector, css_edit_selector));
+ css_edit_selector->signal_key_press_event().connect(sigc::bind(
+ sigc::mem_fun(*this, &StyleDialog::_selectorEditKeyPress), store, css_selector, css_edit_selector));
+ css_edit_selector->signal_activate().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_selectorActivate), store, css_selector, css_edit_selector));
+ */
+ Inkscape::UI::Widget::IconRenderer *addRenderer = manage(new Inkscape::UI::Widget::IconRenderer());
+ addRenderer->add_icon("edit-delete");
+ int addCol = css_tree->append_column(" ", *addRenderer) - 1;
+ Gtk::TreeViewColumn *col = css_tree->get_column(addCol);
+ if (col) {
+ addRenderer->signal_activated().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store));
+ }
+ Gtk::CellRendererToggle *isactive = Gtk::manage(new Gtk::CellRendererToggle());
+ isactive->property_activatable() = true;
+ addCol = css_tree->append_column(" ", *isactive) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->add_attribute(isactive->property_active(), _mColumns._colActive);
+ isactive->signal_toggled().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_activeToggled), store));
+ }
+ Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText());
+ label->property_placeholder_text() = _("property");
+ label->property_editable() = true;
+ label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>(
+ sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree));
+ label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit));
+ addCol = css_tree->append_column(" ", *label) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->set_resizable(true);
+ col->add_attribute(label->property_text(), _mColumns._colName);
+ }
+ Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText());
+ value->property_editable() = true;
+ value->property_placeholder_text() = _("value");
+ value->signal_edited().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_valueEdited), store));
+ value->signal_editing_started().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store));
+ addCol = css_tree->append_column(" ", *value) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->add_attribute(value->property_text(), _mColumns._colValue);
+ col->add_attribute(value->property_strikethrough(), _mColumns._colStrike);
+ }
+ Glib::ustring style = properties;
+ Glib::ustring comments = "";
+ while (style.find("/*") != std::string::npos) {
+ size_t beg = style.find("/*");
+ size_t end = style.find("*/");
+ if (end != std::string::npos && beg != std::string::npos) {
+ comments = comments.append(style, beg + 2, end - beg - 2);
+ style = style.erase(beg, end - beg + 2);
+ }
+ }
+ std::map<Glib::ustring, Glib::ustring> attr_prop_styleshet = parseStyle(style);
+ std::map<Glib::ustring, Glib::ustring> attr_prop_styleshet_comments = parseStyle(comments);
+ std::map<Glib::ustring, std::pair<Glib::ustring, bool>> result_props;
+ for (auto styled : attr_prop_styleshet) {
+ result_props[styled.first] = std::make_pair(styled.second, true);
+ }
+ for (auto styled : attr_prop_styleshet_comments) {
+ result_props[styled.first] = std::make_pair(styled.second, false);
+ }
+ empty = true;
+ css_selector_event_add->signal_button_release_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>(
+ sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, selector_orig, selectorpos));
+ if (obj) {
+ for (auto iter : result_props) {
+ empty = false;
+ Gtk::TreeIter iterstore = store->append();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iterstore;
+ Gtk::TreeModel::Row row = *(iterstore);
+ row[_mColumns._colSelector] = selector_orig;
+ row[_mColumns._colSelectorPos] = selectorpos;
+ row[_mColumns._colActive] = iter.second.second;
+ row[_mColumns._colName] = iter.first;
+ row[_mColumns._colValue] = iter.second.first;
+ const Glib::ustring value = row[_mColumns._colValue];
+ if (iter.second.second) {
+ Glib::ustring val = "";
+ for (auto iterprop : obj->style->properties()) {
+ if (iterprop->style_src != SPStyleSrc::UNSET && iterprop->name() == iter.first) {
+ val = iterprop->get_value();
+ break;
+ }
+ }
+ guint32 r1 = 0; // if there's no color, return black
+ r1 = sp_svg_read_color(value.c_str(), r1);
+ guint32 r2 = 0; // if there's no color, return black
+ r2 = sp_svg_read_color(val.c_str(), r2);
+ if (attr_prop.count(iter.first) || (value != val && (r1 == 0 || r1 != r2))) {
+ row[_mColumns._colStrike] = true;
+ row[_mColumns._colOwner] = Glib::ustring("");
+ } else {
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Current value");
+ _addOwnerStyle(iter.first, selector);
+ }
+ } else {
+ row[_mColumns._colStrike] = true;
+ Glib::ustring tooltiptext = _("This value is commented out.");
+ row[_mColumns._colOwner] = tooltiptext;
+ }
+ }
+ } else {
+ for (auto iter : result_props) {
+ empty = false;
+ Gtk::TreeModel::Row row = *(store->prepend());
+ row[_mColumns._colSelector] = selector_orig;
+ row[_mColumns._colSelectorPos] = selectorpos;
+ row[_mColumns._colActive] = iter.second.second;
+ row[_mColumns._colName] = iter.first;
+ row[_mColumns._colValue] = iter.second.first;
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Stylesheet value");
+ }
+ }
+ if (empty) {
+ css_tree->hide();
+ }
+ _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET);
+ selectorpos++;
+ }
+ try {
+ _builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("Glade file loading failed for filter effect dialog.");
+ return;
+ }
+ _builder->get_widget("CSSSelector", css_selector);
+ css_selector->set_text("element.attributes");
+ _builder->get_widget("CSSSelectorContainer", css_selector_container);
+ _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add);
+ css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK);
+ store = Gtk::TreeStore::create(_mColumns);
+ _builder->get_widget("CSSTree", css_tree);
+ css_tree->get_style_context()->add_class("style_attribute");
+ css_tree->set_model(store);
+ css_selector_event_add->signal_button_release_event().connect(
+ sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>(
+ sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, "attributes", selectorpos));
+ bool hasattributes = false;
+ empty = true;
+ if (obj) {
+ for (auto iter : obj->style->properties()) {
+ if (iter->style_src != SPStyleSrc::UNSET) {
+ auto key = iter->id();
+ if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) {
+ const gchar *attr = obj->getRepr()->attribute(iter->name().c_str());
+ if (attr) {
+ if (!hasattributes) {
+ Inkscape::UI::Widget::IconRenderer *addRenderer =
+ manage(new Inkscape::UI::Widget::IconRenderer());
+ addRenderer->add_icon("edit-delete");
+ int addCol = css_tree->append_column(" ", *addRenderer) - 1;
+ Gtk::TreeViewColumn *col = css_tree->get_column(addCol);
+ if (col) {
+ addRenderer->signal_activated().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(
+ sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store));
+ }
+ Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText());
+ label->property_placeholder_text() = _("property");
+ label->property_editable() = true;
+ label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>(
+ sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree));
+ label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit));
+ addCol = css_tree->append_column(" ", *label) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->set_resizable(true);
+ col->add_attribute(label->property_text(), _mColumns._colName);
+ }
+ Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText());
+ value->property_placeholder_text() = _("value");
+ value->property_editable() = true;
+ value->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(
+ sigc::mem_fun(*this, &StyleDialog::_valueEdited), store));
+ value->signal_editing_started().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(
+ sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store));
+
+ addCol = css_tree->append_column(" ", *value) - 1;
+ col = css_tree->get_column(addCol);
+ if (col) {
+ col->add_attribute(value->property_text(), _mColumns._colValue);
+ col->add_attribute(value->property_strikethrough(), _mColumns._colStrike);
+ }
+ }
+ empty = false;
+ Gtk::TreeIter iterstore = store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iterstore;
+ Gtk::TreeModel::Row row = *(iterstore);
+ row[_mColumns._colSelector] = "attributes";
+ row[_mColumns._colSelectorPos] = selectorpos;
+ row[_mColumns._colActive] = true;
+ row[_mColumns._colName] = iter->name();
+ row[_mColumns._colValue] = attr;
+ if (_owner_style.find(iter->name()) != _owner_style.end()) {
+ row[_mColumns._colStrike] = true;
+ Glib::ustring tooltiptext = Glib::ustring("");
+ row[_mColumns._colOwner] = tooltiptext;
+ } else {
+ row[_mColumns._colStrike] = false;
+ row[_mColumns._colOwner] = Glib::ustring("Current value");
+ _addOwnerStyle(iter->name(), "inline attributes");
+ }
+ hasattributes = true;
+ }
+ }
+ }
+ }
+ if (empty) {
+ css_tree->hide();
+ }
+ if (!hasattributes) {
+ for (auto widg : css_selector_container->get_children()) {
+ delete widg;
+ }
+ }
+ _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET);
+ }
+ for (auto selector : _styleBox.get_children()) {
+ Gtk::Box *box = dynamic_cast<Gtk::Box *>(&selector[0]);
+ if (box) {
+ std::vector<Gtk::Widget *> childs = box->get_children();
+ if (childs.size() > 1) {
+ Gtk::TreeView *css_tree = dynamic_cast<Gtk::TreeView *>(childs[1]);
+ if (css_tree) {
+ Glib::RefPtr<Gtk::TreeModel> model = css_tree->get_model();
+ if (model) {
+ model->foreach_iter(sigc::mem_fun(*this, &StyleDialog::_on_foreach_iter));
+ }
+ }
+ }
+ }
+ }
+ if (obj) {
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ _mainBox.show_all_children();
+ _updating = false;
+}
+
+bool StyleDialog::_selectorStartEdit(GdkEventButton *event, Gtk::Label *selector, Gtk::Entry *selector_edit)
+{
+ g_debug("StyleDialog::_selectorStartEdit");
+ if (event->type == GDK_BUTTON_RELEASE && event->button == 1) {
+ selector->hide();
+ selector_edit->set_text(selector->get_text());
+ selector_edit->show();
+ }
+ return false;
+}
+
+/* void StyleDialog::_selectorActivate(Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, Gtk::Entry
+*selector_edit)
+{
+ g_debug("StyleDialog::_selectorEditKeyPress");
+ Glib::ustring newselector = fixCSSSelectors(selector_edit->get_text());
+ if (newselector.empty()) {
+ selector_edit->get_style_context()->add_class("system_error_color");
+ return;
+ }
+ _writeStyleElement(store, selector->get_text(), selector_edit->get_text());
+} */
+
+bool StyleDialog::_selectorEditKeyPress(GdkEventKey *event, Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector,
+ Gtk::Entry *selector_edit)
+{
+ g_debug("StyleDialog::_selectorEditKeyPress");
+ switch (event->keyval) {
+ case GDK_KEY_Escape:
+ selector->show();
+ selector_edit->hide();
+ selector_edit->get_style_context()->remove_class("system_error_color");
+ break;
+ }
+ return false;
+}
+
+bool StyleDialog::_on_foreach_iter(const Gtk::TreeModel::iterator &iter)
+{
+ g_debug("StyleDialog::_on_foreach_iter");
+
+ Gtk::TreeModel::Row row = *(iter);
+ Glib::ustring owner = row[_mColumns._colOwner];
+ if (owner.empty()) {
+ Glib::ustring value = _owner_style[row[_mColumns._colName]];
+ Glib::ustring tooltiptext = Glib::ustring(_("Current value"));
+ if (!value.empty()) {
+ tooltiptext = Glib::ustring::compose(_("Used in %1"), _owner_style[row[_mColumns._colName]]);
+ row[_mColumns._colStrike] = true;
+ } else {
+ row[_mColumns._colStrike] = false;
+ }
+ row[_mColumns._colOwner] = tooltiptext;
+ }
+ return false;
+}
+
+void StyleDialog::_onLinkObj(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_onLinkObj");
+
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row && row[_mColumns._colLinked]) {
+ SPObject *linked = row[_mColumns._colHref];
+ if (linked) {
+ auto selection = getSelection();
+ getDocument()->setXMLDialogSelectedObject(linked);
+ selection->clear();
+ selection->set(linked);
+ }
+ }
+}
+
+/**
+ * @brief StyleDialog::_onPropDelete
+ * @param event
+ * @return true
+ * Delete the attribute from the style
+ */
+void StyleDialog::_onPropDelete(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_onPropDelete");
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row) {
+ Glib::ustring selector = row[_mColumns._colSelector];
+ row[_mColumns._colName] = "";
+ _deleted_pos = row[_mColumns._colSelectorPos];
+ store->erase(row);
+ _deletion = true;
+ _writeStyleElement(store, selector);
+ _deletion = false;
+ }
+}
+
+void StyleDialog::_addOwnerStyle(Glib::ustring name, Glib::ustring selector)
+{
+ g_debug("StyleDialog::_addOwnerStyle");
+
+ if (_owner_style.find(name) == _owner_style.end()) {
+ _owner_style[name] = selector;
+ }
+}
+
+
+/**
+ * @brief StyleDialog::parseStyle
+ *
+ * Convert a style string into a vector map. This should be moved to style.cpp
+ *
+ */
+std::map<Glib::ustring, Glib::ustring> StyleDialog::parseStyle(Glib::ustring style_string)
+{
+ g_debug("StyleDialog::parseStyle");
+
+ std::map<Glib::ustring, Glib::ustring> ret;
+
+ Util::trim(style_string); // We'd use const, but we need to trip spaces
+ std::vector<Glib::ustring> props = r_props->split(style_string);
+
+ for (auto token : props) {
+ Util::trim(token);
+
+ if (token.empty())
+ break;
+ std::vector<Glib::ustring> pair = r_pair->split(token);
+
+ if (pair.size() > 1) {
+ ret[pair[0]] = pair[1];
+ }
+ }
+ return ret;
+}
+
+
+/**
+ * Update the content of the style element as selectors (or objects) are added/removed.
+ */
+void StyleDialog::_writeStyleElement(Glib::RefPtr<Gtk::TreeStore> store, Glib::ustring selector,
+ Glib::ustring new_selector)
+{
+ g_debug("StyleDialog::_writeStyleElemen");
+ auto selection = getSelection();
+ if (_updating && selection)
+ return;
+ _scrollock = true;
+ SPObject *obj = nullptr;
+ if (selection->objects().size() == 1) {
+ obj = selection->objects().back();
+ }
+ if (!obj) {
+ obj = getDocument()->getXMLDialogSelectedObject();
+ }
+ if (selection->objects().size() < 2 && !obj) {
+ readStyleElement();
+ return;
+ }
+ _updating = true;
+ gint selectorpos = 0;
+ std::string styleContent = "";
+ if (selector != "style_properties" && selector != "attributes") {
+ if (!new_selector.empty()) {
+ selector = new_selector;
+ }
+ std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector);
+ for (auto selectoritem : selectordata) {
+ if (selectordata[selectordata.size() - 1] == selectoritem) {
+ selector = selectoritem;
+ } else {
+ styleContent = styleContent + selectoritem + ";\n";
+ }
+ }
+ styleContent.append("\n").append(selector.raw()).append(" { \n");
+ }
+ selectorpos = _deleted_pos;
+ for (auto &row : store->children()) {
+ selector = row[_mColumns._colSelector];
+ selectorpos = row[_mColumns._colSelectorPos];
+ const char *opencomment = "";
+ const char *closecomment = "";
+ if (selector != "style_properties" && selector != "attributes") {
+ opencomment = row[_mColumns._colActive] ? " " : " /*";
+ closecomment = row[_mColumns._colActive] ? "\n" : "*/\n";
+ }
+ Glib::ustring const &name = row[_mColumns._colName];
+ Glib::ustring const &value = row[_mColumns._colValue];
+ if (!(name.empty() && value.empty())) {
+ styleContent = styleContent + opencomment + name.raw() + ":" + value.raw() + ";" + closecomment;
+ }
+ }
+ if (selector != "style_properties" && selector != "attributes") {
+ styleContent = styleContent + "}";
+ }
+ if (selector == "style_properties") {
+ _updating = true;
+ obj->getRepr()->setAttribute("style", styleContent);
+ _updating = false;
+ } else if (selector == "attributes") {
+ for (auto iter : obj->style->properties()) {
+ auto key = iter->id();
+ if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) {
+ const gchar *attr = obj->getRepr()->attribute(iter->name().c_str());
+ if (attr) {
+ _updating = true;
+ obj->getRepr()->removeAttribute(iter->name());
+ _updating = false;
+ }
+ }
+ }
+ for (auto &row : store->children()) {
+ Glib::ustring const &name = row[_mColumns._colName];
+ Glib::ustring const &value = row[_mColumns._colValue];
+ if (!(name.empty() && value.empty())) {
+ _updating = true;
+ obj->getRepr()->setAttribute(name, value);
+ _updating = false;
+ }
+ }
+ } else if (!selector.empty()) { // styleshet
+ // We could test if styleContent is empty and then delete the style node here but there is no
+ // harm in keeping it around ...
+
+ std::string pos = std::to_string(selectorpos);
+ std::string selectormatch = "(";
+ for (; selectorpos > 1; selectorpos--) {
+ selectormatch = selectormatch + "[^\\}]*?\\}";
+ }
+ selectormatch = selectormatch + ")([^\\}]*?\\})((.|\n)*)";
+
+ Inkscape::XML::Node *textNode = _getStyleTextNode(true);
+ std::regex e(selectormatch.c_str());
+ std::string content = (textNode->content() ? textNode->content() : "");
+ std::string result;
+ std::regex_replace(std::back_inserter(result), content.begin(), content.end(), e, "$1" + styleContent + "$3");
+ bool empty = false;
+ if (result.empty()) {
+ empty = true;
+ result = "* > .inkscapehacktmp{}";
+ }
+ textNode->setContent(result.c_str());
+ if (empty) {
+ textNode->setContent("");
+ }
+ }
+ _updating = false;
+ readStyleElement();
+ for (auto iter : getDocument()->getObjectsBySelector(selector)) {
+ iter->style->readFromObject(iter);
+ iter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ DocumentUndo::done(SP_ACTIVE_DOCUMENT, _("Edited style element."), "");
+
+ g_debug("StyleDialog::_writeStyleElement(): | %s |", styleContent.c_str());
+}
+
+bool StyleDialog::_addRow(GdkEventButton *evt, Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeView *css_tree,
+ Glib::ustring selector, gint pos)
+{
+ g_debug("StyleDialog::_addRow");
+
+ if (evt->type == GDK_BUTTON_RELEASE && evt->button == 1) {
+ Gtk::TreeIter iter = store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ Gtk::TreeModel::Row row = *(iter);
+ row[_mColumns._colSelector] = selector;
+ row[_mColumns._colSelectorPos] = pos;
+ row[_mColumns._colActive] = true;
+ row[_mColumns._colName] = "";
+ row[_mColumns._colValue] = "";
+ row[_mColumns._colStrike] = false;
+ gint col = 2;
+ if (pos < 1) {
+ col = 1;
+ }
+ css_tree->show();
+ css_tree->set_cursor(path, *(css_tree->get_column(col)), true);
+ grab_focus();
+ return true;
+ }
+ return false;
+}
+
+void StyleDialog::_setAutocompletion(Gtk::Entry *entry, SPStyleEnum const cssenum[])
+{
+ g_debug("StyleDialog::_setAutocompletion");
+
+ Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData);
+ Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create();
+ entry_completion->set_model(completionModel);
+ entry_completion->set_text_column (_mCSSData._colCSSData);
+ entry_completion->set_minimum_key_length(0);
+ entry_completion->set_popup_completion(true);
+ gint counter = 0;
+ const char * key = cssenum[counter].key;
+ while (key) {
+ Gtk::TreeModel::Row row = *(completionModel->prepend());
+ row[_mCSSData._colCSSData] = Glib::ustring(key);
+ counter++;
+ key = cssenum[counter].key;
+ }
+ entry->set_completion(entry_completion);
+}
+/*Hardcode values non in enum*/
+void StyleDialog::_setAutocompletion(Gtk::Entry *entry, Glib::ustring name)
+{
+ g_debug("StyleDialog::_setAutocompletion");
+
+ Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData);
+ Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create();
+ entry_completion->set_model(completionModel);
+ entry_completion->set_text_column(_mCSSData._colCSSData);
+ entry_completion->set_minimum_key_length(0);
+ entry_completion->set_popup_completion(true);
+ if (name == "paint-order") {
+ Gtk::TreeModel::Row row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("fill markers stroke");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("fill stroke markers");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("stroke markers fill");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("stroke fill markers");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("markers fill stroke");
+ row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = Glib::ustring("markers stroke fill");
+ }
+ entry->set_completion(entry_completion);
+}
+
+void
+StyleDialog::_startValueEdit(Gtk::CellEditable* cell, const Glib::ustring& path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_startValueEdit");
+ _scrollock = true;
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row) {
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell);
+ Glib::ustring name = row[_mColumns._colName];
+ if (name == "paint-order") {
+ _setAutocompletion(entry, name);
+ } else if (name == "fill-rule") {
+ _setAutocompletion(entry, enum_fill_rule);
+ } else if (name == "stroke-linecap") {
+ _setAutocompletion(entry, enum_stroke_linecap);
+ } else if (name == "stroke-linejoin") {
+ _setAutocompletion(entry, enum_stroke_linejoin);
+ } else if (name == "font-style") {
+ _setAutocompletion(entry, enum_font_style);
+ } else if (name == "font-variant") {
+ _setAutocompletion(entry, enum_font_variant);
+ } else if (name == "font-weight") {
+ _setAutocompletion(entry, enum_font_weight);
+ } else if (name == "font-stretch") {
+ _setAutocompletion(entry, enum_font_stretch);
+ } else if (name == "font-variant-position") {
+ _setAutocompletion(entry, enum_font_variant_position);
+ } else if (name == "text-align") {
+ _setAutocompletion(entry, enum_text_align);
+ } else if (name == "text-transform") {
+ _setAutocompletion(entry, enum_text_transform);
+ } else if (name == "text-anchor") {
+ _setAutocompletion(entry, enum_text_anchor);
+ } else if (name == "white-space") {
+ _setAutocompletion(entry, enum_white_space);
+ } else if (name == "direction") {
+ _setAutocompletion(entry, enum_direction);
+ } else if (name == "baseline-shift") {
+ _setAutocompletion(entry, enum_baseline_shift);
+ } else if (name == "visibility") {
+ _setAutocompletion(entry, enum_visibility);
+ } else if (name == "overflow") {
+ _setAutocompletion(entry, enum_overflow);
+ } else if (name == "display") {
+ _setAutocompletion(entry, enum_display);
+ } else if (name == "shape-rendering") {
+ _setAutocompletion(entry, enum_shape_rendering);
+ } else if (name == "color-rendering") {
+ _setAutocompletion(entry, enum_color_rendering);
+ } else if (name == "clip-rule") {
+ _setAutocompletion(entry, enum_clip_rule);
+ } else if (name == "color-interpolation") {
+ _setAutocompletion(entry, enum_color_interpolation);
+ }
+ entry->signal_key_release_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyReleased), entry));
+ entry->signal_key_press_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyPressed), entry));
+ }
+}
+
+void StyleDialog::_startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
+{
+ g_debug("StyleDialog::_startNameEdit");
+ _scrollock = true;
+ Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData);
+ Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create();
+ entry_completion->set_model(completionModel);
+ entry_completion->set_text_column(_mCSSData._colCSSData);
+ entry_completion->set_minimum_key_length(1);
+ entry_completion->set_popup_completion(true);
+ for (auto prop : sp_attribute_name_list(true)) {
+ Gtk::TreeModel::Row row = *(completionModel->append());
+ row[_mCSSData._colCSSData] = prop;
+ }
+ Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell);
+ entry->set_completion(entry_completion);
+ entry->signal_key_release_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onNameKeyReleased), entry));
+ entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onNameKeyPressed), entry));
+}
+
+
+gboolean sp_styledialog_store_move_to_next(gpointer data)
+{
+ StyleDialog *styledialog = reinterpret_cast<StyleDialog *>(data);
+ auto selection = styledialog->_current_css_tree->get_selection();
+ Gtk::TreeIter iter = *(selection->get_selected());
+ if (!iter) {
+ return FALSE;
+ }
+ Gtk::TreeModel::Path model = (Gtk::TreeModel::Path)iter;
+ if (model == styledialog->_current_path) {
+ styledialog->_current_css_tree->set_cursor(styledialog->_current_path, *styledialog->_current_value_col,
+ true);
+ }
+ return FALSE;
+}
+
+/**
+ * @brief StyleDialog::nameEdited
+ * @param event
+ * @return
+ * Called when the name is edited in the TreeView editable column
+ */
+void StyleDialog::_nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store,
+ Gtk::TreeView *css_tree)
+{
+ g_debug("StyleDialog::_nameEdited");
+
+ _scrollock = true;
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ _current_path = (Gtk::TreeModel::Path)*store->get_iter(path);
+
+ if (row) {
+ _current_css_tree = css_tree;
+ Glib::ustring finalname = name;
+ auto i = finalname.find_first_of(";:=");
+ if (i != std::string::npos) {
+ finalname.erase(i, name.size() - i);
+ }
+ gint pos = row[_mColumns._colSelectorPos];
+ bool write = false;
+ if (row[_mColumns._colName] != finalname && row[_mColumns._colValue] != "") {
+ write = true;
+ }
+ Glib::ustring selector = row[_mColumns._colSelector];
+ Glib::ustring value = row[_mColumns._colValue];
+ bool is_attr = selector == "attributes";
+ Glib::ustring old_name = row[_mColumns._colName];
+ row[_mColumns._colName] = finalname;
+ if (finalname.empty() && value.empty()) {
+ _deleted_pos = row[_mColumns._colSelectorPos];
+ store->erase(row);
+ }
+ gint col = 3;
+ if (pos < 1 || is_attr) {
+ col = 2;
+ }
+ _current_value_col = css_tree->get_column(col);
+ if (write && old_name != name) {
+ _writeStyleElement(store, selector);
+ /*
+ I think is better comment this, is enough update on value change
+ if (selector != "style_properties" && selector != "attributes") {
+ std::vector<SPObject *> objs = _getObjVec(selector);
+ for (auto obj : objs){
+ Glib::ustring css_str = "";
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style"));
+ css->removeAttribute(name);
+ sp_repr_css_write_string(css, css_str);
+ obj->getRepr()->setAttributeOrRemoveIfEmpty("style", css_str);
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ } */
+ } else {
+ g_timeout_add(50, &sp_styledialog_store_move_to_next, this);
+ grab_focus();
+ }
+ }
+}
+
+/**
+ * @brief StyleDialog::valueEdited
+ * @param event
+ * @return
+ * Called when the value is edited in the TreeView editable column
+ */
+void StyleDialog::_valueEdited(const Glib::ustring &path, const Glib::ustring &value,
+ Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_valueEdited");
+
+ _scrollock = true;
+
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row) {
+ Glib::ustring finalvalue = value;
+ auto i = std::min(finalvalue.find(";"), finalvalue.find(":"));
+ if (i != std::string::npos) {
+ finalvalue.erase(i, finalvalue.size() - i);
+ }
+ Glib::ustring old_value = row[_mColumns._colValue];
+ if (old_value == finalvalue) {
+ return;
+ }
+ row[_mColumns._colValue] = finalvalue;
+ Glib::ustring selector = row[_mColumns._colSelector];
+ Glib::ustring name = row[_mColumns._colName];
+ if (name.empty() && finalvalue.empty()) {
+ _deleted_pos = row[_mColumns._colSelectorPos];
+ store->erase(row);
+ }
+ _writeStyleElement(store, selector);
+ if (selector != "style_properties" && selector != "attributes") {
+ std::vector<SPObject *> objs = _getObjVec(selector);
+ for (auto obj : objs) {
+ Glib::ustring css_str = "";
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style"));
+ css->removeAttribute(name);
+ sp_repr_css_write_string(css, css_str);
+ obj->getRepr()->setAttribute("style", css_str);
+ obj->style->readFromObject(obj);
+ obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
+ }
+ }
+ }
+}
+
+void StyleDialog::_activeToggled(const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store)
+{
+ g_debug("StyleDialog::_activeToggled");
+
+ _scrollock = true;
+ Gtk::TreeModel::Row row = *store->get_iter(path);
+ if (row) {
+ row[_mColumns._colActive] = !row[_mColumns._colActive];
+ Glib::ustring selector = row[_mColumns._colSelector];
+ _writeStyleElement(store, selector);
+ }
+}
+
+bool StyleDialog::_onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onNameKeyReleased");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+bool StyleDialog::_onNameKeyReleased(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onNameKeyReleased");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_equal:
+ case GDK_KEY_colon:
+ entry->editing_done();
+ ret = true;
+ break;
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_semicolon: {
+ Glib::ustring text = entry->get_text();
+ auto i = std::min(text.find(";"), text.find(":"));
+ if (i != std::string::npos) {
+ entry->editing_done();
+ ret = true;
+ }
+ break;
+ }
+ }
+ return ret;
+}
+
+bool StyleDialog::_onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onValueKeyReleased");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ entry->editing_done();
+ ret = true;
+ break;
+ }
+ return ret;
+}
+
+bool StyleDialog::_onValueKeyReleased(GdkEventKey *event, Gtk::Entry *entry)
+{
+ g_debug("StyleDialog::_onValueKeyReleased");
+ bool ret = false;
+ switch (event->keyval) {
+ case GDK_KEY_semicolon:
+ entry->editing_done();
+ ret = true;
+ break;
+ case GDK_KEY_Shift_L:
+ case GDK_KEY_Shift_R:
+ case GDK_KEY_colon: {
+ Glib::ustring text = entry->get_text();
+ auto i = std::min(text.find(";"), text.find(":"));
+ if (i != std::string::npos) {
+ entry->editing_done();
+ ret = true;
+ }
+ break;
+ }
+ }
+ return ret;
+}
+
+/**
+ * @param selector: a valid CSS selector string.
+ * @return objVec: a vector of pointers to SPObject's the selector matches.
+ * Return a vector of all objects that selector matches.
+ */
+std::vector<SPObject *> StyleDialog::_getObjVec(Glib::ustring selector)
+{
+ g_debug("StyleDialog::_getObjVec");
+
+ g_assert(selector.find(";") == Glib::ustring::npos);
+
+ return getDocument()->getObjectsBySelector(selector);
+}
+
+void StyleDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); }
+
+
+void StyleDialog::removeObservers()
+{
+ if (_textNode) {
+ _textNode->removeObserver(*m_styletextwatcher);
+ _textNode = nullptr;
+ }
+ if (m_root) {
+ m_root->removeSubtreeObserver(*m_nodewatcher);
+ m_root = nullptr;
+ }
+}
+
+/**
+ * Handle document replaced. (Happens when a default document is immediately replaced by another
+ * document in a new window.)
+ */
+void StyleDialog::documentReplaced()
+{
+ removeObservers();
+ if (auto document = getDocument()) {
+ m_root = document->getReprRoot();
+ m_root->addSubtreeObserver(*m_nodewatcher);
+ }
+ readStyleElement();
+}
+
+/*
+ * Handle a change in which objects are selected in a document.
+ */
+void StyleDialog::selectionChanged(Selection * /*selection*/)
+{
+ _scrollpos = 0;
+ _vadj->set_value(0);
+ // Sometimes the selection changes because inkscape is closing.
+ if (getDesktop()) {
+ readStyleElement();
+ }
+}
+
+} // 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/dialog/styledialog.h b/src/ui/dialog/styledialog.h
new file mode 100644
index 0000000..21292af
--- /dev/null
+++ b/src/ui/dialog/styledialog.h
@@ -0,0 +1,195 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief A dialog for CSS selectors
+ */
+/* Authors:
+ * Kamalpreet Kaur Grewal
+ * Tavmjong Bah
+ *
+ * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com>
+ * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef STYLEDIALOG_H
+#define STYLEDIALOG_H
+
+#include <glibmm/regex.h>
+#include <gtkmm/adjustment.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/celleditable.h>
+#include <gtkmm/cellrenderercombo.h>
+#include <gtkmm/dialog.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/entrycompletion.h>
+#include <gtkmm/eventbox.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/switch.h>
+#include <gtkmm/tooltip.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treeselection.h>
+#include <gtkmm/treestore.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/viewport.h>
+#include <memory>
+#include <vector>
+
+#include "style-enums.h"
+#include "ui/dialog/dialog-base.h"
+#include "xml/helper-observer.h"
+
+namespace Inkscape {
+
+XML::Node *get_first_style_text_node(XML::Node *root, bool create_if_missing);
+
+namespace UI {
+namespace Dialog {
+
+/**
+ * @brief The StyleDialog class
+ * A list of CSS selectors will show up in this dialog. This dialog allows one to
+ * add and delete selectors. Elements can be added to and removed from the selectors
+ * in the dialog. Selection of any selector row selects the matching objects in
+ * the drawing and vice-versa. (Only simple selectors supported for now.)
+ *
+ * This class must keep two things in sync:
+ * 1. The text node of the style element.
+ * 2. The Gtk::TreeModel.
+ */
+class StyleDialog : public DialogBase
+{
+public:
+ StyleDialog();
+ ~StyleDialog() override;
+
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+
+ void setCurrentSelector(Glib::ustring current_selector);
+ Gtk::TreeView *_current_css_tree;
+ Gtk::TreeViewColumn *_current_value_col;
+ Gtk::TreeModel::Path _current_path;
+ bool _deletion{false};
+ Glib::ustring fixCSSSelectors(Glib::ustring selector);
+ void readStyleElement();
+
+ private:
+ // Monitor <style> element for changes.
+ class NodeObserver;
+ // Monitor all objects for addition/removal/attribute change
+ class NodeWatcher;
+ Glib::RefPtr<Glib::Regex> r_props = Glib::Regex::create("\\s*;\\s*");
+ Glib::RefPtr<Glib::Regex> r_pair = Glib::Regex::create("\\s*:\\s*");
+ void _nodeAdded(Inkscape::XML::Node &repr);
+ void _nodeRemoved(Inkscape::XML::Node &repr);
+ void _nodeChanged(Inkscape::XML::Node &repr);
+ void removeObservers();
+ /* void _stylesheetChanged( Inkscape::XML::Node &repr ); */
+ // Data structure
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
+ public:
+ ModelColumns()
+ {
+ add(_colActive);
+ add(_colName);
+ add(_colValue);
+ add(_colStrike);
+ add(_colSelector);
+ add(_colSelectorPos);
+ add(_colOwner);
+ add(_colLinked);
+ add(_colHref);
+ }
+ Gtk::TreeModelColumn<bool> _colActive; // Active or inactive property
+ Gtk::TreeModelColumn<Glib::ustring> _colName; // Name of the property.
+ Gtk::TreeModelColumn<Glib::ustring> _colValue; // Value of the property.
+ Gtk::TreeModelColumn<bool> _colStrike; // Property not used, overloaded
+ Gtk::TreeModelColumn<Glib::ustring> _colSelector; // Style or matching object id.
+ Gtk::TreeModelColumn<gint> _colSelectorPos; // Position of the selector to handle dup selectors
+ Gtk::TreeModelColumn<Glib::ustring> _colOwner; // Store the owner of the property for popup
+ Gtk::TreeModelColumn<bool> _colLinked; // Other object linked
+ Gtk::TreeModelColumn<SPObject *> _colHref; // Is going to another object
+ };
+ ModelColumns _mColumns;
+
+ class CSSData : public Gtk::TreeModel::ColumnRecord {
+ public:
+ CSSData() { add(_colCSSData); }
+ Gtk::TreeModelColumn<Glib::ustring> _colCSSData; // Name of the property.
+ };
+ CSSData _mCSSData;
+ guint _deleted_pos{0};
+ // Widgets
+ Gtk::ScrolledWindow _scrolledWindow;
+ Glib::RefPtr<Gtk::Adjustment> _vadj;
+ Gtk::Box _mainBox;
+ Gtk::Box _styleBox;
+ // Reading and writing the style element.
+ Inkscape::XML::Node *_getStyleTextNode(bool create_if_missing = false);
+ Glib::RefPtr<Gtk::TreeModel> _selectTree(Glib::ustring selector);
+ void _writeStyleElement(Glib::RefPtr<Gtk::TreeStore> store, Glib::ustring selector,
+ Glib::ustring new_selector = "");
+ // void _selectorActivate(Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, Gtk::Entry *selector_edit);
+ bool _selectorEditKeyPress(GdkEventKey *event, Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector,
+ Gtk::Entry *selector_edit);
+ bool _selectorStartEdit(GdkEventButton *event, Gtk::Label *selector, Gtk::Entry *selector_edit);
+ void _activeToggled(const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store);
+ bool _addRow(GdkEventButton *evt, Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeView *css_tree,
+ Glib::ustring selector, gint pos);
+ void _onPropDelete(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store);
+ void _nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store,
+ Gtk::TreeView *css_tree);
+ bool _onNameKeyReleased(GdkEventKey *event, Gtk::Entry *entry);
+ bool _onValueKeyReleased(GdkEventKey *event, Gtk::Entry *entry);
+ bool _onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
+ bool _onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
+ void _onLinkObj(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store);
+ void _valueEdited(const Glib::ustring &path, const Glib::ustring &value, Glib::RefPtr<Gtk::TreeStore> store);
+ void _startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path);
+
+ void _startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store);
+ void _setAutocompletion(Gtk::Entry *entry, SPStyleEnum const cssenum[]);
+ void _setAutocompletion(Gtk::Entry *entry, Glib::ustring name);
+ bool _on_foreach_iter(const Gtk::TreeModel::iterator &iter);
+ void _reload();
+ void _vscroll();
+ bool _scrollock;
+ double _scrollpos{0};
+ Glib::ustring _current_selector;
+
+ // Update watchers
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher;
+ std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher;
+
+ // Manipulate Tree
+ std::vector<SPObject *> _getObjVec(Glib::ustring selector);
+ std::map<Glib::ustring, Glib::ustring> parseStyle(Glib::ustring style_string);
+ std::map<Glib::ustring, Glib::ustring> _owner_style;
+ void _addOwnerStyle(Glib::ustring name, Glib::ustring selector);
+ // Variables
+ Inkscape::XML::Node *m_root{nullptr};
+ Inkscape::XML::Node *_textNode{nullptr}; // Track so we know when to add a NodeObserver.
+ bool _updating{false}; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop
+
+ void _closeDialog(Gtk::Dialog *textDialogPtr);
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // STYLEDIALOG_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/dialog/svg-fonts-dialog.cpp b/src/ui/dialog/svg-fonts-dialog.cpp
new file mode 100644
index 0000000..049653c
--- /dev/null
+++ b/src/ui/dialog/svg-fonts-dialog.cpp
@@ -0,0 +1,1794 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * SVG Fonts dialog - implementation.
+ */
+/* Authors:
+ * Felipe C. da S. Sanches <juca@members.fsf.org>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2008 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <message-stack.h>
+#include <sstream>
+#include <iomanip>
+
+#include <gtkmm/scale.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/expander.h>
+#include <gtkmm/imagemenuitem.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/i18n.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "layer-manager.h"
+#include "selection.h"
+#include "svg-fonts-dialog.h"
+
+#include "display/nr-svgfonts.h"
+#include "include/gtkmm_version.h"
+#include "object/sp-defs.h"
+#include "object/sp-font-face.h"
+#include "object/sp-font.h"
+#include "object/sp-glyph-kerning.h"
+#include "object/sp-glyph.h"
+#include "object/sp-guide.h"
+#include "object/sp-missing-glyph.h"
+#include "object/sp-path.h"
+#include "svg/svg.h"
+#include "util/units.h"
+#include "xml/repr.h"
+#include "document.h"
+
+SvgFontDrawingArea::SvgFontDrawingArea():
+ _x(0),
+ _y(0),
+ _svgfont(nullptr),
+ _text()
+{
+}
+
+void SvgFontDrawingArea::set_svgfont(SvgFont* svgfont){
+ _svgfont = svgfont;
+}
+
+void SvgFontDrawingArea::set_text(Glib::ustring text){
+ _text = text;
+ redraw();
+}
+
+void SvgFontDrawingArea::set_size(int x, int y){
+ _x = x;
+ _y = y;
+ ((Gtk::Widget*) this)->set_size_request(_x, _y);
+}
+
+void SvgFontDrawingArea::redraw(){
+ ((Gtk::Widget*) this)->queue_draw();
+}
+
+bool SvgFontDrawingArea::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) {
+ if (_svgfont){
+ cr->set_font_face( Cairo::RefPtr<Cairo::FontFace>(new Cairo::FontFace(_svgfont->get_font_face(), false /* does not have reference */)) );
+ cr->set_font_size (_y-20);
+ cr->move_to (10, 10);
+ auto context = get_style_context();
+ Gdk::RGBA fg = context->get_color(get_state_flags());
+ cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue());
+ // crash on macos: https://gitlab.com/inkscape/inkscape/-/issues/266
+ try {
+ cr->show_text(_text.c_str());
+ }
+ catch (std::exception& ex) {
+ g_warning("Error drawing custom SVG font text: %s", ex.what());
+ }
+ }
+ return true;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+void SvgGlyphRenderer::render_vfunc(
+ const Cairo::RefPtr<Cairo::Context>& cr, Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area, const Gdk::Rectangle& cell_area, Gtk::CellRendererState flags) {
+
+ if (!_font || !_tree) return;
+
+ cr->set_font_face(Cairo::RefPtr<Cairo::FontFace>(new Cairo::FontFace(_font->get_font_face(), false /* does not have reference */)));
+ cr->set_font_size(_font_size);
+ Glib::ustring glyph = _property_glyph.get_value();
+ Cairo::TextExtents ext;
+ cr->get_text_extents(glyph, ext);
+ cr->move_to(cell_area.get_x() + (_width - ext.width) / 2, cell_area.get_y() + 1);
+ auto context = _tree->get_style_context();
+ Gtk::StateFlags sflags = _tree->get_state_flags();
+ if (flags & Gtk::CELL_RENDERER_SELECTED) {
+ sflags |= Gtk::STATE_FLAG_SELECTED;
+ }
+ Gdk::RGBA fg = context->get_color(sflags);
+ cr->set_source_rgb(fg.get_red(), fg.get_green(), fg.get_blue());
+ // crash on macos: https://gitlab.com/inkscape/inkscape/-/issues/266
+ try {
+ cr->show_text(glyph);
+ }
+ catch (std::exception& ex) {
+ g_warning("Error drawing custom SVG font glyphs: %s", ex.what());
+ }
+}
+
+bool SvgGlyphRenderer::activate_vfunc(
+ GdkEvent* event, Gtk::Widget& widget, const Glib::ustring& path, const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area, Gtk::CellRendererState flags) {
+
+ Glib::ustring glyph = _property_glyph.get_value();
+ _signal_clicked.emit(event, glyph);
+ return false;
+}
+
+SvgFontsDialog::AttrEntry::AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr)
+{
+ this->dialog = d;
+ this->attr = attr;
+ entry.set_tooltip_text(tooltip);
+ _label = Gtk::make_managed<Gtk::Label>(lbl);
+ _label->show();
+ _label->set_halign(Gtk::ALIGN_START);
+ entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrEntry::on_attr_changed));
+}
+
+void SvgFontsDialog::AttrEntry::set_text(const char* t){
+ if (!t) return;
+ entry.set_text(t);
+}
+
+// 'font-family' has a problem as it is also a presentation attribute for <text>
+void SvgFontsDialog::AttrEntry::on_attr_changed(){
+ if (dialog->_update.pending()) return;
+
+ SPObject* o = nullptr;
+ for (auto& node: dialog->get_selected_spfont()->children) {
+ switch(this->attr){
+ case SPAttr::FONT_FAMILY:
+ if (is<SPFontFace>(&node)){
+ o = &node;
+ continue;
+ }
+ break;
+ default:
+ o = nullptr;
+ }
+ }
+
+ const gchar* name = (const gchar*)sp_attribute_name(this->attr);
+ if(name && o) {
+ o->setAttribute((const gchar*) name, this->entry.get_text());
+ o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ Glib::ustring undokey = "svgfonts:";
+ undokey += name;
+ DocumentUndo::maybeDone(o->document, undokey.c_str(), _("Set SVG Font attribute"), "");
+ }
+
+}
+
+SvgFontsDialog::AttrSpin::AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr)
+{
+ this->dialog = d;
+ this->attr = attr;
+ spin.set_tooltip_text(tooltip);
+ spin.show();
+ _label = Gtk::make_managed<Gtk::Label>(lbl);
+ _label->show();
+ _label->set_halign(Gtk::ALIGN_START);
+ spin.set_range(0, 4096);
+ spin.set_increments(10, 0);
+ spin.signal_value_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrSpin::on_attr_changed));
+}
+
+void SvgFontsDialog::AttrSpin::set_range(double low, double high){
+ spin.set_range(low, high);
+}
+
+void SvgFontsDialog::AttrSpin::set_value(double v){
+ spin.set_value(v);
+}
+
+void SvgFontsDialog::AttrSpin::on_attr_changed(){
+ if (dialog->_update.pending()) return;
+
+ SPObject* o = nullptr;
+ switch (this->attr) {
+
+ // <font> attributes
+ case SPAttr::HORIZ_ORIGIN_X:
+ case SPAttr::HORIZ_ORIGIN_Y:
+ case SPAttr::HORIZ_ADV_X:
+ case SPAttr::VERT_ORIGIN_X:
+ case SPAttr::VERT_ORIGIN_Y:
+ case SPAttr::VERT_ADV_Y:
+ o = this->dialog->get_selected_spfont();
+ break;
+
+ // <font-face> attributes
+ case SPAttr::UNITS_PER_EM:
+ case SPAttr::ASCENT:
+ case SPAttr::DESCENT:
+ case SPAttr::CAP_HEIGHT:
+ case SPAttr::X_HEIGHT:
+ for (auto& node: dialog->get_selected_spfont()->children){
+ if (is<SPFontFace>(&node)){
+ o = &node;
+ continue;
+ }
+ }
+ break;
+
+ default:
+ o = nullptr;
+ }
+
+ const gchar* name = (const gchar*)sp_attribute_name(this->attr);
+ if(name && o) {
+ std::ostringstream temp;
+ temp << this->spin.get_value();
+ o->setAttribute(name, temp.str());
+ o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ Glib::ustring undokey = "svgfonts:";
+ undokey += name;
+ DocumentUndo::maybeDone(o->document, undokey.c_str(), _("Set SVG Font attribute"), "");
+ }
+
+}
+
+Gtk::Box* SvgFontsDialog::AttrCombo(gchar* lbl, const SPAttr /*attr*/){
+ Gtk::Box* hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ hbox->add(* Gtk::manage(new Gtk::Label(lbl)) );
+ hbox->add(* Gtk::manage(new Gtk::ComboBox()) );
+ hbox->show_all();
+ return hbox;
+}
+
+/*** SvgFontsDialog ***/
+
+GlyphComboBox::GlyphComboBox() {
+}
+
+void GlyphComboBox::update(SPFont* spfont){
+ if (!spfont) return;
+
+ // remove wrapping - it has severe performance penalty for appending items
+ set_wrap_width(0);
+
+ this->remove_all();
+
+ for (auto& node: spfont->children) {
+ if (is<SPGlyph>(&node)){
+ this->append((static_cast<SPGlyph*>(&node))->unicode);
+ }
+ }
+
+ // set desired wrpping now
+ set_wrap_width(4);
+}
+
+void SvgFontsDialog::on_kerning_value_changed(){
+ if (!get_selected_kerning_pair()) {
+ return;
+ }
+
+ //TODO: I am unsure whether this is the correct way of calling SPDocumentUndo::maybe_done
+ Glib::ustring undokey = "svgfonts:hkern:k:";
+ undokey += this->kerning_pair->u1->attribute_string();
+ undokey += ":";
+ undokey += this->kerning_pair->u2->attribute_string();
+
+ //slider values increase from right to left so that they match the kerning pair preview
+
+ //XML Tree being directly used here while it shouldn't be.
+ this->kerning_pair->setAttribute("k", Glib::Ascii::dtostr(get_selected_spfont()->horiz_adv_x - kerning_slider->get_value()));
+ DocumentUndo::maybeDone(getDocument(), undokey.c_str(), _("Adjust kerning value"), "");
+
+ //populate_kerning_pairs_box();
+ kerning_preview.redraw();
+ _font_da.redraw();
+}
+
+void SvgFontsDialog::glyphs_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _GlyphsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void SvgFontsDialog::kerning_pairs_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _KerningPairsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void SvgFontsDialog::fonts_list_button_release(GdkEventButton* event)
+{
+ if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
+ _FontsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event));
+ }
+}
+
+void SvgFontsDialog::sort_glyphs(SPFont* font) {
+ if (!font) return;
+
+ {
+ auto scoped(_update.block());
+ font->sort_glyphs();
+ }
+ update_glyphs();
+}
+
+// return U+<code> ... string
+Glib::ustring create_unicode_name(const Glib::ustring& unicode, int max_chars) {
+ std::ostringstream ost;
+ if (unicode.empty()) {
+ ost << "-";
+ }
+ else {
+ auto it = unicode.begin();
+ for (int i = 0; i < max_chars && it != unicode.end(); ++i) {
+ if (i > 0) {
+ ost << " ";
+ }
+ unsigned int code = *it++;
+ ost << "U+" << std::hex << std::uppercase << std::setw(6) << std::setfill('0') << code;
+ }
+ if (it != unicode.end()) {
+ ost << "..."; // there's more, but we skip them
+ }
+ }
+ return ost.str();
+}
+
+// synthetic name consists for unicode hex numbers derived from glyph's "unicode" attribute
+Glib::ustring get_glyph_synthetic_name(const SPGlyph& glyph) {
+ auto unicode_name = create_unicode_name(glyph.unicode, 3);
+ // U+<code> plus character
+ return unicode_name + " " + glyph.unicode;
+}
+
+// full name consists of user-defined name combined with synthetic one
+Glib::ustring get_glyph_full_name(const SPGlyph& glyph) {
+ auto name = get_glyph_synthetic_name(glyph);
+ if (!glyph.glyph_name.empty()) {
+ // unicode name first, followed by user name - for sorting layers
+ return name + " " + glyph.glyph_name;
+ }
+ else {
+ return name;
+ }
+}
+
+// look for a layer by its label; looking only in direct sublayers of 'root_layer'
+SPItem* find_layer(SPDesktop* desktop, SPObject* root_layer, const Glib::ustring& name) {
+ if (!desktop) return nullptr;
+
+ const auto& layers = desktop->layerManager();
+ auto root = root_layer == nullptr ? layers.currentRoot() : root_layer;
+ if (!root) return nullptr;
+
+ // check only direct child layers
+ auto it = std::find_if(root->children.begin(), root->children.end(), [&](SPObject& obj) {
+ return layers.isLayer(&obj) && obj.label() && strcmp(obj.label(), name.c_str()) == 0;
+ });
+ if (it != root->children.end()) {
+ return static_cast<SPItem*>(&*it);
+ }
+
+ return nullptr; // not found
+}
+
+std::vector<SPGroup*> get_direct_sublayers(SPObject* layer) {
+ std::vector<SPGroup*> layers;
+ if (!layer) return layers;
+
+ for (auto&& item : layer->children) {
+ if (auto l = LayerManager::asLayer(&item)) {
+ layers.push_back(l);
+ }
+ }
+
+ return layers;
+}
+
+void rename_glyph_layer(SPDesktop* desktop, SPItem* layer, const Glib::ustring& font, const Glib::ustring& name) {
+ if (!desktop || !layer || font.empty() || name.empty()) return;
+
+ auto parent_layer = find_layer(desktop, desktop->layerManager().currentRoot(), font);
+ if (!parent_layer) return;
+
+ // before renaming the layer find new place to move it into to keep sorted order intact
+ auto glyph_layers = get_direct_sublayers(parent_layer);
+
+ auto it = std::lower_bound(glyph_layers.rbegin(), glyph_layers.rend(), name, [&](auto&& layer, const Glib::ustring n) {
+ auto label = layer->label();
+ if (!label) return false;
+
+ Glib::ustring temp(label);
+ return std::lexicographical_compare(temp.begin(), temp.end(), n.begin(), n.end());
+ });
+ SPObject* after = nullptr;
+ if (it != glyph_layers.rend()) {
+ after = *it;
+ }
+
+ // SPItem changeOrder messes up inserting into first position, so dropping to Node level
+ if (layer != after && parent_layer->getRepr() && layer->getRepr()) {
+ parent_layer->getRepr()->changeOrder(layer->getRepr(), after ? after->getRepr() : nullptr);
+ }
+
+ desktop->layerManager().renameLayer(layer, name.c_str(), false);
+}
+
+SPItem* get_layer_for_glyph(SPDesktop* desktop, const Glib::ustring& font, const Glib::ustring& name) {
+ if (!desktop || name.empty() || font.empty()) return nullptr;
+
+ auto parent_layer = find_layer(desktop, desktop->layerManager().currentRoot(), font);
+ if (!parent_layer) return nullptr;
+
+ return find_layer(desktop, parent_layer, name);
+}
+
+SPItem* get_or_create_layer_for_glyph(SPDesktop* desktop, const Glib::ustring& font, const Glib::ustring& name) {
+ if (!desktop || name.empty() || font.empty()) return nullptr;
+
+ auto& layers = desktop->layerManager();
+ auto parent_layer = find_layer(desktop, layers.currentRoot(), font);
+ if (!parent_layer) {
+ // create a new layer for a font
+ parent_layer = static_cast<SPItem*>(create_layer(layers.currentRoot(), layers.currentRoot(), Inkscape::LayerRelativePosition::LPOS_CHILD));
+ if (!parent_layer) return nullptr;
+
+ layers.renameLayer(parent_layer, font.c_str(), false);
+ }
+
+ if (auto layer = find_layer(desktop, parent_layer, name)) {
+ return layer;
+ }
+
+ // find the right place for a new layer, so they appear sorted
+ auto glyph_layers = get_direct_sublayers(parent_layer);
+ // auto& glyph_layers = parent_layer->children;
+ auto it = std::lower_bound(glyph_layers.rbegin(), glyph_layers.rend(), name, [&](auto&& layer, const Glib::ustring n) {
+ auto label = layer->label();
+ if (!label) return false;
+
+ Glib::ustring temp(label);
+ return std::lexicographical_compare(temp.begin(), temp.end(), n.begin(), n.end());
+ });
+ SPObject* insert = parent_layer;
+ Inkscape::LayerRelativePosition pos = Inkscape::LayerRelativePosition::LPOS_ABOVE;
+ if (it != glyph_layers.rend()) {
+ insert = *it;
+ }
+ else {
+ // auto first = std::find_if(glyph_layers.begin(), glyph_layers.end(), [&](auto&& obj) {
+ // return layers.isLayer(&obj);
+ // });
+ if (!glyph_layers.empty()) {
+ insert = glyph_layers.front();
+ pos = Inkscape::LayerRelativePosition::LPOS_BELOW;
+ }
+ }
+
+ // create a new layer for a glyph
+ auto layer = create_layer(parent_layer, insert, pos);
+ if (!layer) return nullptr;
+
+ layers.renameLayer(layer, name.c_str(), false);
+
+ DocumentUndo::done(desktop->getDocument(), _("Add layer"), "");
+ return cast<SPItem>(layer);
+}
+
+void SvgFontsDialog::create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem)
+{
+ // - edit glyph (show its layer)
+ // - sort glyphs and their layers
+ // - remove current glyph
+ auto mi = Gtk::make_managed<Gtk::MenuItem>(_("_Edit current glyph"), true);
+ mi->show();
+ mi->signal_activate().connect([=](){
+ edit_glyph(get_selected_glyph());
+ });
+ _GlyphsContextMenu.append(*mi);
+
+ mi = Gtk::make_managed<Gtk::SeparatorMenuItem>();
+ mi->show();
+ _GlyphsContextMenu.append(*mi);
+
+ mi = Gtk::make_managed<Gtk::MenuItem>(_("_Sort glyphs"), true);
+ mi->show();
+ mi->signal_activate().connect([=](){
+ sort_glyphs(get_selected_spfont());
+ });
+ _GlyphsContextMenu.append(*mi);
+
+ mi = Gtk::make_managed<Gtk::SeparatorMenuItem>();
+ mi->show();
+ _GlyphsContextMenu.append(*mi);
+
+ mi = Gtk::make_managed<Gtk::MenuItem>(_("_Remove"), true);
+ _GlyphsContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+
+ _GlyphsContextMenu.accelerate(parent);
+}
+
+void SvgFontsDialog::create_kerning_pairs_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem)
+{
+ auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ _KerningPairsContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ _KerningPairsContextMenu.accelerate(parent);
+}
+
+void SvgFontsDialog::create_fonts_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem)
+{
+ auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true));
+ _FontsContextMenu.append(*mi);
+ mi->signal_activate().connect(rem);
+ mi->show();
+ _FontsContextMenu.accelerate(parent);
+}
+
+void SvgFontsDialog::update_sensitiveness(){
+ if (get_selected_spfont()){
+ _grid.set_sensitive(true);
+ glyphs_vbox.set_sensitive(true);
+ kerning_vbox.set_sensitive(true);
+ } else {
+ _grid.set_sensitive(false);
+ glyphs_vbox.set_sensitive(false);
+ kerning_vbox.set_sensitive(false);
+ }
+}
+
+Glib::ustring get_font_label(SPFont* font) {
+ if (!font) return Glib::ustring();
+
+ const gchar* label = font->label();
+ const gchar* id = font->getId();
+ return Glib::ustring(label ? label : (id ? id : "font"));
+};
+
+/** Add all fonts in the getDocument() to the combobox.
+ * This function is called when new document is selected as well as when SVG "definition" section changes.
+ * Try to detect if font(s) have actually been modified to eliminate some expensive refreshes.
+ */
+void SvgFontsDialog::update_fonts(bool document_replaced)
+{
+ std::vector<SPObject*> fonts;
+ if (auto document = getDocument()) {
+ fonts = document->getResourceList( "font" );
+ }
+
+ auto children = _model->children();
+ bool equal = false;
+ bool selected_font = false;
+
+ // compare model and resources
+ if (!document_replaced && children.size() == fonts.size()) {
+ equal = true; // assume they are the same
+ auto it = fonts.begin();
+ for (auto&& node : children) {
+ SPFont* sp_font = node[_columns.spfont];
+ if (it == fonts.end() || *it != sp_font) {
+ // difference detected; update model
+ equal = false;
+ break;
+ }
+ ++it;
+ }
+ }
+
+ // rebuild model if list of fonts is different
+ if (!equal) {
+ _model->clear();
+ for (auto font : fonts) {
+ Gtk::TreeModel::Row row = *_model->append();
+ auto f = cast<SPFont>(font);
+ row[_columns.spfont] = f;
+ row[_columns.svgfont] = new SvgFont(f);
+ row[_columns.label] = get_font_label(f);
+ }
+ if (!fonts.empty()) {
+ // select a font, this dialog is disabled without a font
+ auto selection = _FontsList.get_selection();
+ if (selection) {
+ selection->select(_model->get_iter("0"));
+ selected_font = true;
+ }
+ }
+ }
+ else {
+ // list of fonts is the same, but attributes may have changed
+ auto it = fonts.begin();
+ for (auto&& node : children) {
+ if (auto font = cast<SPFont>(*it++)) {
+ node[_columns.label] = get_font_label(font);
+ }
+ }
+ }
+
+ if (document_replaced && !selected_font) {
+ // replace fonts, they are stale
+ font_selected(nullptr, nullptr);
+ }
+ else {
+ update_sensitiveness();
+ }
+}
+
+void SvgFontsDialog::on_preview_text_changed(){
+ _font_da.set_text(_preview_entry.get_text());
+}
+
+void SvgFontsDialog::on_kerning_pair_selection_changed(){
+ SPGlyphKerning* kern = get_selected_kerning_pair();
+ if (!kern) {
+ kerning_preview.set_text("");
+ return;
+ }
+ Glib::ustring str;
+ str += kern->u1->sample_glyph();
+ str += kern->u2->sample_glyph();
+
+ kerning_preview.set_text(str);
+ this->kerning_pair = kern;
+
+ //slider values increase from right to left so that they match the kerning pair preview
+ kerning_slider->set_value(get_selected_spfont()->horiz_adv_x - kern->k);
+}
+
+void SvgFontsDialog::update_global_settings_tab(){
+ SPFont* font = get_selected_spfont();
+ if (!font) {
+ //TODO: perhaps reset all values when there's no font
+ _familyname_entry->set_text("");
+ return;
+ }
+
+ _horiz_adv_x_spin->set_value(font->horiz_adv_x);
+ _horiz_origin_x_spin->set_value(font->horiz_origin_x);
+ _horiz_origin_y_spin->set_value(font->horiz_origin_y);
+
+ for (auto& obj: font->children) {
+ if (is<SPFontFace>(&obj)){
+ _familyname_entry->set_text((cast<SPFontFace>(&obj))->font_family);
+ _units_per_em_spin->set_value((cast<SPFontFace>(&obj))->units_per_em);
+ _ascent_spin->set_value((cast<SPFontFace>(&obj))->ascent);
+ _descent_spin->set_value((cast<SPFontFace>(&obj))->descent);
+ _x_height_spin->set_value((cast<SPFontFace>(&obj))->x_height);
+ _cap_height_spin->set_value((cast<SPFontFace>(&obj))->cap_height);
+ }
+ }
+}
+
+void SvgFontsDialog::font_selected(SvgFont* svgfont, SPFont* spfont) {
+ // in update
+ auto scoped(_update.block());
+
+ first_glyph.update(spfont);
+ second_glyph.update(spfont);
+ kerning_preview.set_svgfont(svgfont);
+ _font_da.set_svgfont(svgfont);
+ _font_da.redraw();
+ _glyph_renderer->set_svg_font(svgfont);
+ _glyph_cell_renderer->set_svg_font(svgfont);
+
+ kerning_slider->set_range(0, spfont ? spfont->horiz_adv_x : 0);
+ kerning_slider->set_draw_value(false);
+ kerning_slider->set_value(0);
+
+ update_global_settings_tab();
+ populate_glyphs_box();
+ populate_kerning_pairs_box();
+ update_sensitiveness();
+}
+
+void SvgFontsDialog::on_font_selection_changed(){
+ SPFont* spfont = get_selected_spfont();
+ SvgFont* svgfont = get_selected_svgfont();
+ font_selected(svgfont, spfont);
+}
+
+SPGlyphKerning* SvgFontsDialog::get_selected_kerning_pair()
+{
+ Gtk::TreeModel::iterator i = _KerningPairsList.get_selection()->get_selected();
+ if(i)
+ return (*i)[_KerningPairsListColumns.spnode];
+ return nullptr;
+}
+
+SvgFont* SvgFontsDialog::get_selected_svgfont()
+{
+ Gtk::TreeModel::iterator i = _FontsList.get_selection()->get_selected();
+ if(i)
+ return (*i)[_columns.svgfont];
+ return nullptr;
+}
+
+SPFont* SvgFontsDialog::get_selected_spfont()
+{
+ Gtk::TreeModel::iterator i = _FontsList.get_selection()->get_selected();
+ if(i)
+ return (*i)[_columns.spfont];
+ return nullptr;
+}
+
+Gtk::TreeModel::iterator SvgFontsDialog::get_selected_glyph_iter() {
+ if (_GlyphsListScroller.get_visible()) {
+ if (auto selection = _GlyphsList.get_selection()) {
+ Gtk::TreeModel::iterator it = selection->get_selected();
+ return it;
+ }
+ }
+ else {
+ std::vector<Gtk::TreePath> selected = _glyphs_grid.get_selected_items();
+ if (selected.size() == 1) {
+ Gtk::ListStore::iterator it = _GlyphsListStore->get_iter(selected.front());
+ return it;
+ }
+ }
+ return Gtk::TreeModel::iterator();
+}
+
+SPGlyph* SvgFontsDialog::get_selected_glyph()
+{
+ if (auto it = get_selected_glyph_iter()) {
+ return (*it)[_GlyphsListColumns.glyph_node];
+ }
+ return nullptr;
+}
+
+void SvgFontsDialog::set_selected_glyph(SPGlyph* glyph) {
+ if (!glyph) return;
+
+ _GlyphsListStore->foreach_iter([=](const Gtk::TreeModel::iterator& it) {
+ if (it->get_value(_GlyphsListColumns.glyph_node) == glyph) {
+ if (auto selection = _GlyphsList.get_selection()) {
+ selection->select(it);
+ }
+ auto selected_item = _GlyphsListStore->get_path(it);
+ _glyphs_grid.select_path(selected_item);
+ return true; // stop
+ }
+ return false; // continue
+ });
+}
+
+SPGuide* get_guide(SPDocument& doc, const Glib::ustring& id) {
+ auto object = doc.getObjectById(id);
+ if (!object) return nullptr;
+
+ // get guide line
+ if (auto guide = cast<SPGuide>(object)) {
+ return guide;
+ }
+ // remove colliding object
+ object->deleteObject();
+ return nullptr;
+}
+
+SPGuide* create_guide(SPDocument& doc, double x0, double y0, double x1, double y1) {
+ return SPGuide::createSPGuide(&doc, Geom::Point(x0, y1), Geom::Point(x1, y1));
+}
+
+void set_up_typography_canvas(SPDocument* document, double em, double asc, double cap, double xheight, double des) {
+ if (!document || em <= 0) return;
+
+ // set size and viewbox
+ auto size = Inkscape::Util::Quantity(em, "px");
+ bool change_size = false;
+ document->setWidthAndHeight(size, size, change_size);
+ document->setViewBox(Geom::Rect::from_xywh(0, 0, em, em));
+
+ // baseline
+ double base = des;
+ double ascPos = base + asc;
+ double capPos = base + cap;
+ double xPos = base + xheight;
+ double desPos = base - des;
+
+ if (!document->is_yaxisdown()) {
+ base = size.quantity - des;
+ ascPos = base - asc;
+ capPos = base - cap;
+ xPos = base - xheight;
+ desPos = base + des;
+ }
+
+ // add/move guide lines
+ struct { double pos; const char* name; const char* id; } guides[5] = {
+ {ascPos, _("ascender"), "ink-font-guide-ascender"},
+ {capPos, _("caps"), "ink-font-guide-caps"},
+ {xPos, _("x-height"), "ink-font-guide-x-height"},
+ {base, _("baseline"), "ink-font-guide-baseline"},
+ {desPos, _("descender"), "ink-font-guide-descender"},
+ };
+
+ double left = 0;
+ double right = em;
+
+ for (auto&& g : guides) {
+ double y = em - g.pos;
+ auto guide = get_guide(*document, g.id);
+ if (guide) {
+ guide->set_locked(false, true);
+ guide->moveto(Geom::Point(left, y), true);
+ }
+ else {
+ guide = create_guide(*document, left, y, right, y);
+ guide->getRepr()->setAttributeOrRemoveIfEmpty("id", g.id);
+ }
+ guide->set_label(g.name, true);
+ guide->set_locked(true, true);
+ }
+
+ DocumentUndo::done(document, _("Set up typography canvas"), "");
+}
+
+const int MARGIN_SPACE = 4;
+
+Gtk::Box* SvgFontsDialog::global_settings_tab(){
+
+ _fonts_scroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+ _fonts_scroller.add(_FontsList);
+ _fonts_scroller.set_hexpand();
+ _fonts_scroller.show();
+ _header_box.set_column_spacing(MARGIN_SPACE);
+ _header_box.set_row_spacing(MARGIN_SPACE);
+ _header_box.attach(_fonts_scroller, 0, 0, 1, 3);
+ _header_box.attach(*Gtk::make_managed<Gtk::Label>(), 1, 0);
+ _header_box.attach(_add, 1, 1);
+ _header_box.attach(_remove, 1, 2);
+ _header_box.set_margin_bottom(MARGIN_SPACE);
+ _header_box.set_margin_end(MARGIN_SPACE);
+ _add.set_valign(Gtk::ALIGN_CENTER);
+ _remove.set_valign(Gtk::ALIGN_CENTER);
+ _remove.set_halign(Gtk::ALIGN_CENTER);
+ _add.set_image_from_icon_name("list-add", Gtk::ICON_SIZE_BUTTON);
+ _remove.set_image_from_icon_name("list-remove", Gtk::ICON_SIZE_BUTTON);
+
+ global_vbox.pack_start(_header_box, false, false);
+
+ _font_label = new Gtk::Label(Glib::ustring("<b>") + _("Font Attributes") + "</b>", Gtk::ALIGN_START, Gtk::ALIGN_CENTER);
+ _horiz_adv_x_spin = new AttrSpin( this, (gchar*) _("Horizontal advance X:"), _("Default glyph width for horizontal text"), SPAttr::HORIZ_ADV_X);
+ _horiz_origin_x_spin = new AttrSpin( this, (gchar*) _("Horizontal origin X:"), _("Default X-coordinate of the origin of a glyph (for horizontal text)"), SPAttr::HORIZ_ORIGIN_X);
+ _horiz_origin_y_spin = new AttrSpin( this, (gchar*) _("Horizontal origin Y:"), _("Default Y-coordinate of the origin of a glyph (for horizontal text)"), SPAttr::HORIZ_ORIGIN_Y);
+ _font_face_label = new Gtk::Label(Glib::ustring("<b>") + _("Font face attributes") + "</b>", Gtk::ALIGN_START, Gtk::ALIGN_CENTER);
+ _familyname_entry = new AttrEntry(this, (gchar*) _("Family name:"), _("Name of the font as it appears in font selectors and css font-family properties"), SPAttr::FONT_FAMILY);
+ _units_per_em_spin = new AttrSpin( this, (gchar*) _("Em-size:"), _("Display units per <italic>em</italic> (nominally width of 'M' character)"), SPAttr::UNITS_PER_EM);
+ _ascent_spin = new AttrSpin( this, (gchar*) _("Ascender:"), _("Amount of space taken up by ascenders like the tall line on the letter 'h'"), SPAttr::ASCENT);
+ _cap_height_spin = new AttrSpin( this, (gchar*) _("Caps height:"), _("The height of a capital letter above the baseline like the letter 'H' or 'I'"), SPAttr::CAP_HEIGHT);
+ _x_height_spin = new AttrSpin( this, (gchar*) _("x-height:"), _("The height of a lower-case letter above the baseline like the letter 'x'"), SPAttr::X_HEIGHT);
+ _descent_spin = new AttrSpin( this, (gchar*) _("Descender:"), _("Amount of space taken up by descenders like the tail on the letter 'g'"), SPAttr::DESCENT);
+
+ //_descent_spin->set_range(-4096,0);
+ _font_label->set_use_markup();
+ _font_face_label->set_use_markup();
+
+ _grid.set_column_spacing(MARGIN_SPACE);
+ _grid.set_row_spacing(MARGIN_SPACE);
+ _grid.set_margin_start(MARGIN_SPACE);
+ _grid.set_margin_bottom(MARGIN_SPACE);
+ const int indent = 2 * MARGIN_SPACE;
+ int row = 0;
+
+ _grid.attach(*_font_label, 0, row++, 2);
+ SvgFontsDialog::AttrSpin* font[] = {_horiz_adv_x_spin, _horiz_origin_x_spin, _horiz_origin_y_spin};
+ for (auto spin : font) {
+ spin->get_label()->set_margin_start(indent);
+ _grid.attach(*spin->get_label(), 0, row);
+ _grid.attach(*spin->getSpin(), 1, row++);
+ }
+
+ _grid.attach(*_font_face_label, 0, row++, 2);
+ _familyname_entry->get_label()->set_margin_start(indent);
+ _familyname_entry->get_entry()->set_margin_end(MARGIN_SPACE);
+ _grid.attach(*_familyname_entry->get_label(), 0, row);
+ _grid.attach(*_familyname_entry->get_entry(), 1, row++, 2);
+
+ SvgFontsDialog::AttrSpin* face[] = {_units_per_em_spin, _ascent_spin, _cap_height_spin, _x_height_spin, _descent_spin};
+ for (auto spin : face) {
+ spin->get_label()->set_margin_start(indent);
+ _grid.attach(*spin->get_label(), 0, row);
+ _grid.attach(*spin->getSpin(), 1, row++);
+ }
+ auto setup = Gtk::make_managed<Gtk::Button>(_("Set up canvas"));
+ _grid.attach(*setup, 0, row++, 2);
+ setup->set_halign(Gtk::ALIGN_START);
+ setup->signal_clicked().connect([=](){
+ // set up typography canvas
+ set_up_typography_canvas(
+ getDocument(),
+ _units_per_em_spin->getSpin()->get_value(),
+ _ascent_spin->getSpin()->get_value(),
+ _cap_height_spin->getSpin()->get_value(),
+ _x_height_spin->getSpin()->get_value(),
+ _descent_spin->getSpin()->get_value()
+ );
+ });
+
+ global_vbox.set_border_width(2);
+ global_vbox.pack_start(_grid, false, true);
+
+/* global_vbox->add(*AttrCombo((gchar*) _("Style:"), SPAttr::FONT_STYLE));
+ global_vbox->add(*AttrCombo((gchar*) _("Variant:"), SPAttr::FONT_VARIANT));
+ global_vbox->add(*AttrCombo((gchar*) _("Weight:"), SPAttr::FONT_WEIGHT));
+*/
+ return &global_vbox;
+}
+
+void SvgFontsDialog::set_glyph_row(const Gtk::TreeRow& row, SPGlyph& glyph) {
+ auto unicode_name = create_unicode_name(glyph.unicode, 3);
+ row[_GlyphsListColumns.glyph_node] = &glyph;
+ row[_GlyphsListColumns.glyph_name] = glyph.glyph_name;
+ row[_GlyphsListColumns.unicode] = glyph.unicode;
+ row[_GlyphsListColumns.UplusCode] = unicode_name;
+ row[_GlyphsListColumns.advance] = glyph.horiz_adv_x;
+ row[_GlyphsListColumns.name_markup] = "<small>" + Glib::Markup::escape_text(get_glyph_synthetic_name(glyph)) + "</small>";
+}
+
+void
+SvgFontsDialog::populate_glyphs_box()
+{
+ if (!_GlyphsListStore) return;
+
+ _GlyphsListStore->freeze_notify();
+
+ // try to keep selected glyph
+ Gtk::TreeModel::Path selected_item;
+ if (auto selected = get_selected_glyph_iter()) {
+ selected_item = _GlyphsListStore->get_path(selected);
+ }
+ _GlyphsListStore->clear();
+
+ SPFont* spfont = get_selected_spfont();
+ _glyphs_observer.set(spfont);
+
+ if (spfont) {
+ for (auto& node: spfont->children) {
+ if (is<SPGlyph>(&node)) {
+ auto& glyph = static_cast<SPGlyph&>(node);
+ Gtk::TreeModel::Row row = *_GlyphsListStore->append();
+ set_glyph_row(row, glyph);
+ }
+ }
+
+ if (!selected_item.empty()) {
+ if (auto selection = _GlyphsList.get_selection()) {
+ selection->select(selected_item);
+ _GlyphsList.scroll_to_row(selected_item);
+ }
+ _glyphs_grid.select_path(selected_item);
+ }
+ }
+
+ _GlyphsListStore->thaw_notify();
+}
+
+void
+SvgFontsDialog::populate_kerning_pairs_box()
+{
+ if (!_KerningPairsListStore) return;
+
+ _KerningPairsListStore->clear();
+
+ if (SPFont* spfont = get_selected_spfont()) {
+ for (auto& node: spfont->children) {
+ if (is<SPHkern>(&node)){
+ Gtk::TreeModel::Row row = *(_KerningPairsListStore->append());
+ row[_KerningPairsListColumns.first_glyph] = (static_cast<SPGlyphKerning*>(&node))->u1->attribute_string().c_str();
+ row[_KerningPairsListColumns.second_glyph] = (static_cast<SPGlyphKerning*>(&node))->u2->attribute_string().c_str();
+ row[_KerningPairsListColumns.kerning_value] = (static_cast<SPGlyphKerning*>(&node))->k;
+ row[_KerningPairsListColumns.spnode] = static_cast<SPGlyphKerning*>(&node);
+ }
+ }
+ }
+}
+
+// update existing glyph in the tree model
+void SvgFontsDialog::update_glyph(SPGlyph* glyph) {
+ if (_update.pending() || !glyph) return;
+
+ _GlyphsListStore->foreach_iter([&](const Gtk::TreeModel::iterator& it) {
+ if (it->get_value(_GlyphsListColumns.glyph_node) == glyph) {
+ const Gtk::TreeRow& row = *it;
+ set_glyph_row(row, *glyph);
+ return true; // stop
+ }
+ return false; // continue
+ });
+}
+
+void SvgFontsDialog::update_glyphs(SPGlyph* changed_glyph) {
+ if (_update.pending()) return;
+
+ SPFont* font = get_selected_spfont();
+ if (!font) return;
+
+ if (changed_glyph) {
+ update_glyph(changed_glyph);
+ }
+ else {
+ populate_glyphs_box();
+ }
+
+ populate_kerning_pairs_box();
+ refresh_svgfont();
+}
+
+void SvgFontsDialog::refresh_svgfont() {
+ if (auto font = get_selected_svgfont()) {
+ font->refresh();
+ }
+ _font_da.redraw();
+}
+
+void SvgFontsDialog::add_glyph(){
+ auto document = getDocument();
+ if (!document) return;
+ auto font = get_selected_spfont();
+ if (!font) return;
+
+ auto glyphs = _GlyphsListStore->children();
+ // initialize "unicode" field; if there are glyphs look for the last one and take next unicode
+ gunichar unicode = ' ';
+ if (!glyphs.empty()) {
+ const auto& last = glyphs[glyphs.size() - 1];
+ if (SPGlyph* last_glyph = last[_GlyphsListColumns.glyph_node]) {
+ const Glib::ustring& code = last_glyph->unicode;
+ if (!code.empty()) {
+ auto value = code[0];
+ // skip control chars 7f-9f
+ if (value == 0x7e) value = 0x9f;
+ // wrap around
+ if (value == 0x10ffff) value = 0x1f;
+ unicode = value + 1;
+ }
+ }
+ }
+ auto str = Glib::ustring(1, unicode);
+
+ // empty name to begin with
+ SPGlyph* glyph = font->create_new_glyph("", str.c_str());
+ DocumentUndo::done(document, _("Add glyph"), "");
+
+ // select newly added glyph
+ set_selected_glyph(glyph);
+}
+
+double get_font_units_per_em(const SPFont* font) {
+ double units_per_em = 0.0;
+ if (font) {
+ for (auto& obj: font->children) {
+ if (is<SPFontFace>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ units_per_em = obj.getRepr()->getAttributeDouble("units-per-em", units_per_em);
+ break;
+ }
+ }
+ }
+ return units_per_em;
+}
+
+Geom::PathVector flip_coordinate_system(Geom::PathVector pathv, const SPFont* font, double units_per_em) {
+ if (!font) return pathv;
+
+ if (units_per_em <= 0) {
+ g_warning("Units per em not defined, path will be misplaced.");
+ }
+
+ double baseline_offset = units_per_em - font->horiz_origin_y;
+ // This matrix flips y-axis and places the origin at baseline
+ Geom::Affine m(1, 0, 0, -1, 0, baseline_offset);
+ return pathv * m;
+}
+
+void SvgFontsDialog::set_glyph_description_from_selected_path() {
+ auto font = get_selected_spfont();
+ if (!font) return;
+
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ Inkscape::MessageStack *msgStack = getDesktop()->getMessageStack();
+ if (selection->isEmpty()){
+ char *msg = _("Select a <b>path</b> to define the curves of a glyph");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ }
+
+ Inkscape::XML::Node* node = selection->xmlNodes().front();
+ if (!node) return;//TODO: should this be an assert?
+ if (!node->matchAttributeName("d") || !node->attribute("d")){
+ char *msg = _("The selected object does not have a <b>path</b> description.");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ } //TODO: //Is there a better way to tell it to to the user?
+
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph){
+ char *msg = _("No glyph selected in the SVGFonts dialog.");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ }
+
+ Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d"));
+
+ auto units_per_em = get_font_units_per_em(font);
+ //XML Tree being directly used here while it shouldn't be.
+ glyph->setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em)));
+ DocumentUndo::done(getDocument(), _("Set glyph curves"), "");
+
+ update_glyphs(glyph);
+}
+
+void SvgFontsDialog::missing_glyph_description_from_selected_path(){
+ auto font = get_selected_spfont();
+ if (!font) return;
+
+ auto selection = getSelection();
+ if (!selection)
+ return;
+
+ Inkscape::MessageStack *msgStack = getDesktop()->getMessageStack();
+ if (selection->isEmpty()){
+ char *msg = _("Select a <b>path</b> to define the curves of a glyph");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ }
+
+ Inkscape::XML::Node* node = selection->xmlNodes().front();
+ if (!node) return;//TODO: should this be an assert?
+ if (!node->matchAttributeName("d") || !node->attribute("d")){
+ char *msg = _("The selected object does not have a <b>path</b> description.");
+ msgStack->flash(Inkscape::ERROR_MESSAGE, msg);
+ return;
+ } //TODO: //Is there a better way to tell it to the user?
+
+ Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d"));
+
+ auto units_per_em = get_font_units_per_em(font);
+ for (auto& obj: font->children) {
+ if (is<SPMissingGlyph>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em)));
+ DocumentUndo::done(getDocument(), _("Set glyph curves"), "");
+ }
+ }
+
+ refresh_svgfont();
+}
+
+void SvgFontsDialog::reset_missing_glyph_description(){
+ for (auto& obj: get_selected_spfont()->children) {
+ if (is<SPMissingGlyph>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("d", "M0,0h1000v1024h-1000z");
+ DocumentUndo::done(getDocument(), _("Reset missing-glyph"), "");
+ }
+ }
+ refresh_svgfont();
+}
+
+void change_glyph_attribute(SPDesktop* desktop, SPGlyph& glyph, std::function<void ()> change) {
+ assert(glyph.parent);
+
+ auto name = get_glyph_full_name(glyph);
+ auto font_label = glyph.parent->label();
+ auto layer = get_layer_for_glyph(desktop, font_label, name);
+
+ change();
+
+ if (!layer) return;
+
+ name = get_glyph_full_name(glyph);
+ font_label = glyph.parent->label();
+ rename_glyph_layer(desktop, layer, font_label, name);
+}
+
+void SvgFontsDialog::glyph_name_edit(const Glib::ustring&, const Glib::ustring& str){
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph) return;
+
+ if (glyph->glyph_name == str) return; // no change
+
+ change_glyph_attribute(getDesktop(), *glyph, [=](){
+ //XML Tree being directly used here while it shouldn't be.
+ glyph->setAttribute("glyph-name", str);
+
+ DocumentUndo::done(getDocument(), _("Edit glyph name"), "");
+ update_glyphs(glyph);
+ });
+}
+
+void SvgFontsDialog::glyph_unicode_edit(const Glib::ustring&, const Glib::ustring& str){
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph) return;
+
+ if (glyph->unicode == str) return; // no change
+
+ change_glyph_attribute(getDesktop(), *glyph, [=]() {
+ // XML Tree being directly used here while it shouldn't be.
+ glyph->setAttribute("unicode", str);
+
+ DocumentUndo::done(getDocument(), _("Set glyph unicode"), "");
+ update_glyphs(glyph);
+ });
+}
+
+void SvgFontsDialog::glyph_advance_edit(const Glib::ustring&, const Glib::ustring& str){
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph) return;
+
+ if (auto val = glyph->getAttribute("horiz-adv-x")) {
+ if (str == val) return; // no change
+ }
+
+ //XML Tree being directly used here while it shouldn't be.
+ std::istringstream is(str.raw());
+ double value;
+ // Check if input valid
+ if ((is >> value)) {
+ glyph->setAttribute("horiz-adv-x", str);
+ DocumentUndo::done(getDocument(), _("Set glyph advance"), "");
+
+ update_glyphs(glyph);
+ } else {
+ std::cerr << "SvgFontDialog::glyph_advance_edit: Error in input: " << str.raw() << std::endl;
+ }
+}
+
+void SvgFontsDialog::remove_selected_font(){
+ SPFont* font = get_selected_spfont();
+ if (!font) return;
+
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_unparent(font->getRepr());
+ DocumentUndo::done(getDocument(), _("Remove font"), "");
+
+ update_fonts(false);
+}
+
+void SvgFontsDialog::remove_selected_glyph(){
+ SPGlyph* glyph = get_selected_glyph();
+ if (!glyph) return;
+
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_unparent(glyph->getRepr());
+ DocumentUndo::done(getDocument(), _("Remove glyph"), "");
+
+ update_glyphs();
+}
+
+void SvgFontsDialog::remove_selected_kerning_pair() {
+ SPGlyphKerning* pair = get_selected_kerning_pair();
+ if (!pair) return;
+
+ //XML Tree being directly used here while it shouldn't be.
+ sp_repr_unparent(pair->getRepr());
+ DocumentUndo::done(getDocument(), _("Remove kerning pair"), "");
+
+ update_glyphs();
+}
+
+Inkscape::XML::Node* create_path_from_glyph(const SPGlyph& glyph) {
+ Geom::PathVector pathv = sp_svg_read_pathv(glyph.getAttribute("d"));
+ auto path = glyph.document->getReprDoc()->createElement("svg:path");
+ // auto path = new SPPath();
+ auto font = cast<SPFont>(glyph.parent);
+ auto units_per_em = get_font_units_per_em(font);
+ path->setAttribute("d", sp_svg_write_path(flip_coordinate_system(pathv, font, units_per_em)));
+ return path;
+}
+
+// switch to a glyph layer (and create this dedicated layer if necessary)
+void SvgFontsDialog::edit_glyph(SPGlyph* glyph) {
+ if (!glyph || !glyph->parent) return;
+
+ auto desktop = getDesktop();
+ if (!desktop) return;
+ auto document = getDocument();
+ if (!document) return;
+
+ // glyph's full name to match layer name
+ auto name = get_glyph_full_name(*glyph);
+ if (name.empty()) return;
+ // font's name to match parent layer name
+ auto font_label = get_font_label(cast<SPFont>(glyph->parent));
+ if (font_label.empty()) return;
+
+ auto layer = get_or_create_layer_for_glyph(desktop, font_label, name);
+ if (!layer) return;
+
+ // is layer empty?
+ if (!layer->hasChildren()) {
+ // since layer is empty try to initialize it by copying font glyph into it
+ auto path = create_path_from_glyph(*glyph);
+ if (path) {
+ // layer->attach(path, nullptr);
+ layer->addChild(path);
+ }
+ }
+
+ auto& layers = desktop->layerManager();
+ // set layer as "solo" - only one visible and unlocked
+ if (layers.isLayer(layer) && layer != layers.currentRoot()) {
+ layers.setCurrentLayer(layer, true);
+ layers.toggleLayerSolo(layer, true);
+ layers.toggleLockOtherLayers(layer, true);
+ DocumentUndo::done(document, _("Toggle layer solo"), "");
+ }
+}
+
+void SvgFontsDialog::set_glyphs_view_mode(bool list) {
+ if (list) {
+ _glyphs_icon_scroller.hide();
+ _GlyphsListScroller.show();
+ }
+ else {
+ _GlyphsListScroller.hide();
+ _glyphs_icon_scroller.show();
+ }
+}
+
+Gtk::Box* SvgFontsDialog::glyphs_tab() {
+ _GlyphsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::glyphs_list_button_release));
+ _glyphs_grid.signal_button_release_event().connect_notify([=](GdkEventButton* event){ glyphs_list_button_release(event); });
+ create_glyphs_popup_menu(_GlyphsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_glyph));
+
+ auto missing_glyph = Gtk::make_managed<Gtk::Expander>();
+ missing_glyph->set_label(_("Missing glyph"));
+ Gtk::Box* missing_glyph_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+ missing_glyph->add(*missing_glyph_hbox);
+ missing_glyph->set_valign(Gtk::ALIGN_CENTER);
+
+ missing_glyph_hbox->set_hexpand(false);
+ missing_glyph_hbox->pack_start(missing_glyph_button, false,false);
+ missing_glyph_hbox->pack_start(missing_glyph_reset_button, false,false);
+
+ missing_glyph_button.set_label(_("From selection"));
+ missing_glyph_button.set_margin_top(MARGIN_SPACE);
+ missing_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::missing_glyph_description_from_selected_path));
+ missing_glyph_reset_button.set_label(_("Reset"));
+ missing_glyph_reset_button.set_margin_top(MARGIN_SPACE);
+ missing_glyph_reset_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::reset_missing_glyph_description));
+
+ glyphs_vbox.set_border_width(4);
+ glyphs_vbox.set_spacing(4);
+
+ _GlyphsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _GlyphsListScroller.add(_GlyphsList);
+ fix_inner_scroll(&_GlyphsListScroller);
+ _GlyphsList.set_model(_GlyphsListStore);
+ _GlyphsList.set_enable_search(false);
+
+ _glyph_renderer = Gtk::manage(new SvgGlyphRenderer());
+ const int size = 20; // arbitrarily chosen to keep glyphs small but still legible
+ _glyph_renderer->set_font_size(size * 9 / 10);
+ _glyph_renderer->set_cell_size(size * 3 / 2, size);
+ _glyph_renderer->set_tree(&_GlyphsList);
+ _glyph_renderer->signal_clicked().connect([=](const GdkEvent*, const Glib::ustring& unicodes) {
+ // set preview: show clicked glyph only
+ _preview_entry.set_text(unicodes);
+ });
+ auto col_index = _GlyphsList.append_column(_("Glyph"), *_glyph_renderer) - 1;
+ if (auto column = _GlyphsList.get_column(col_index)) {
+ column->add_attribute(_glyph_renderer->property_glyph(), _GlyphsListColumns.unicode);
+ }
+ _GlyphsList.append_column_editable(_("Name"), _GlyphsListColumns.glyph_name);
+ _GlyphsList.append_column_editable(_("Characters"), _GlyphsListColumns.unicode);
+ _GlyphsList.append_column(_("Unicode"), _GlyphsListColumns.UplusCode);
+ _GlyphsList.append_column_numeric_editable(_("Advance"), _GlyphsListColumns.advance, "%.2f");
+ _GlyphsList.show();
+ _GlyphsList.signal_row_activated().connect([=](const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn*) {
+ edit_glyph(get_selected_glyph());
+ });
+
+ Gtk::Box* hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4));
+ add_glyph_button.set_image_from_icon_name("list-add");
+ add_glyph_button.set_tooltip_text(_("Add new glyph"));
+ add_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_glyph));
+ remove_glyph_button.set_image_from_icon_name("list-remove");
+ remove_glyph_button.set_tooltip_text(_("Delete current glyph"));
+ remove_glyph_button.signal_clicked().connect([=](){ remove_selected_glyph(); });
+
+ glyph_from_path_button.set_label(_("Get curves"));
+ glyph_from_path_button.set_always_show_image();
+ glyph_from_path_button.set_image_from_icon_name("glyph-copy-from");
+ glyph_from_path_button.set_tooltip_text(_("Get curves from selection to replace current glyph"));
+ glyph_from_path_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::set_glyph_description_from_selected_path));
+
+ auto edit = Gtk::make_managed<Gtk::Button>();
+ edit->set_label(_("Edit"));
+ edit->set_always_show_image();
+ edit->set_image_from_icon_name("edit");
+ edit->set_tooltip_text(_("Switch to a layer with the same name as current glyph"));
+ edit->signal_clicked().connect([=]() {
+ edit_glyph(get_selected_glyph());
+ });
+
+ hb->pack_start(glyph_from_path_button, false, false);
+ hb->pack_start(*edit, false, false);
+ hb->pack_end(remove_glyph_button, false, false);
+ hb->pack_end(add_glyph_button, false, false);
+
+ _glyph_cell_renderer = Gtk::manage(new SvgGlyphRenderer());
+ _glyph_cell_renderer->set_tree(&_glyphs_grid);
+ const int cell_width = 70;
+ const int cell_height = 50;
+ _glyph_cell_renderer->set_cell_size(cell_width, cell_height);
+ _glyph_cell_renderer->set_font_size(cell_height * 8 / 10); // font size: 80% of height
+ _glyphs_icon_scroller.add(_glyphs_grid);
+ _glyphs_icon_scroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+ _glyphs_grid.set_name("GlyphsGrid");
+ _glyphs_grid.set_model(_GlyphsListStore);
+ _glyphs_grid.set_item_width(cell_width);
+ _glyphs_grid.set_selection_mode(Gtk::SELECTION_SINGLE);
+ _glyphs_grid.show_all_children();
+ _glyphs_grid.set_margin(0);
+ _glyphs_grid.set_item_padding(0);
+ _glyphs_grid.set_row_spacing(0);
+ _glyphs_grid.set_column_spacing(0);
+ _glyphs_grid.set_columns(-1);
+ _glyphs_grid.set_markup_column(_GlyphsListColumns.name_markup);
+ _glyphs_grid.pack_start(*_glyph_cell_renderer);
+ _glyphs_grid.add_attribute(*_glyph_cell_renderer, "glyph", _GlyphsListColumns.unicode);
+ _glyphs_grid.show();
+ _glyphs_grid.signal_item_activated().connect([=](const Gtk::TreeModel::Path& path) {
+ edit_glyph(get_selected_glyph());
+ });
+
+ // keep selection in sync between the two views: list and grid
+ _glyphs_grid.signal_selection_changed().connect([=]() {
+ if (_glyphs_icon_scroller.get_visible()) {
+ if (auto selected = get_selected_glyph_iter()) {
+ if (auto selection = _GlyphsList.get_selection()) {
+ selection->select(selected);
+ }
+ }
+ }
+ });
+ if (auto selection = _GlyphsList.get_selection()) {
+ selection->signal_changed().connect([=]() {
+ if (_GlyphsListScroller.get_visible()) {
+ if (auto selected = get_selected_glyph_iter()) {
+ auto selected_item = _GlyphsListStore->get_path(selected);
+ _glyphs_grid.select_path(selected_item);
+ }
+ }
+ });
+ }
+
+ // display mode switching buttons
+ auto hbox = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
+ Gtk::RadioButtonGroup group;
+ auto list = Gtk::make_managed<Gtk::RadioButton>(group);
+ list->set_mode(false);
+ list->set_image_from_icon_name("glyph-list");
+ list->set_tooltip_text(_("Glyph list view"));
+ list->set_valign(Gtk::ALIGN_START);
+ list->signal_toggled().connect([=]() { set_glyphs_view_mode(true); });
+ auto grid = Gtk::make_managed<Gtk::RadioButton>(group);
+ grid->set_mode(false);
+ grid->set_image_from_icon_name("glyph-grid");
+ grid->set_tooltip_text(_("Glyph grid view"));
+ grid->set_valign(Gtk::ALIGN_START);
+ grid->signal_toggled().connect([=]() { set_glyphs_view_mode(false); });
+ hbox->pack_start(*missing_glyph);
+ hbox->pack_end(*grid, false, false);
+ hbox->pack_end(*list, false, false);
+
+ glyphs_vbox.pack_start(*hb, false, false);
+ glyphs_vbox.pack_start(_GlyphsListScroller, true, true);
+ glyphs_vbox.pack_start(_glyphs_icon_scroller, true, true);
+ glyphs_vbox.pack_start(*hbox, false,false);
+
+ _GlyphsListScroller.set_no_show_all();
+ _glyphs_icon_scroller.set_no_show_all();
+ (_show_glyph_list ? list : grid)->set_active();
+ set_glyphs_view_mode(_show_glyph_list);
+
+ for (auto&& col : _GlyphsList.get_columns()) {
+ col->set_resizable();
+ }
+
+ static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColName))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_name_edit));
+
+ static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColString))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_unicode_edit));
+
+ static_cast<Gtk::CellRendererText*>(_GlyphsList.get_column_cell_renderer(ColAdvance))->signal_edited().connect(
+ sigc::mem_fun(*this, &SvgFontsDialog::glyph_advance_edit));
+
+ _glyphs_observer.signal_changed().connect([=]() { update_glyphs(); });
+
+ return &glyphs_vbox;
+}
+
+void SvgFontsDialog::add_kerning_pair(){
+ if (first_glyph.get_active_text() == "" ||
+ second_glyph.get_active_text() == "") return;
+
+ //look for this kerning pair on the currently selected font
+ this->kerning_pair = nullptr;
+ for (auto& node: get_selected_spfont()->children) {
+ //TODO: It is not really correct to get only the first byte of each string.
+ //TODO: We should also support vertical kerning
+ if (is<SPHkern>(&node) && (static_cast<SPGlyphKerning*>(&node))->u1->contains((gchar) first_glyph.get_active_text().c_str()[0])
+ && (static_cast<SPGlyphKerning*>(&node))->u2->contains((gchar) second_glyph.get_active_text().c_str()[0]) ){
+ this->kerning_pair = static_cast<SPGlyphKerning*>(&node);
+ continue;
+ }
+ }
+
+ if (this->kerning_pair) return; //We already have this kerning pair
+
+ Inkscape::XML::Document *xml_doc = getDocument()->getReprDoc();
+
+ // create a new hkern node
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:hkern");
+
+ repr->setAttribute("u1", first_glyph.get_active_text());
+ repr->setAttribute("u2", second_glyph.get_active_text());
+ repr->setAttribute("k", "0");
+
+ // Append the new hkern node to the current font
+ get_selected_spfont()->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // get corresponding object
+ kerning_pair = cast<SPHkern>(getDocument()->getObjectByRepr(repr));
+
+ // select newly added pair
+ if (auto selection = _KerningPairsList.get_selection()) {
+ _KerningPairsListStore->foreach_iter([=](const Gtk::TreeModel::iterator& it) {
+ if (it->get_value(_KerningPairsListColumns.spnode) == kerning_pair) {
+ selection->select(it);
+ return true; // stop
+ }
+ return false; // continue
+ });
+ }
+
+ DocumentUndo::done(getDocument(), _("Add kerning pair"), "");
+}
+
+Gtk::Box* SvgFontsDialog::kerning_tab(){
+ _KerningPairsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::kerning_pairs_list_button_release));
+ create_kerning_pairs_popup_menu(_KerningPairsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_kerning_pair));
+
+//Kerning Setup:
+ kerning_vbox.set_border_width(4);
+ kerning_vbox.set_spacing(4);
+ // kerning_vbox.add(*Gtk::manage(new Gtk::Label(_("Kerning Setup"))));
+ Gtk::Box* kerning_selector = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+ kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("Select glyphs:"))), false, false);
+ kerning_selector->pack_start(first_glyph, false, false, MARGIN_SPACE / 2);
+ kerning_selector->pack_start(second_glyph, false, false, MARGIN_SPACE / 2);
+ kerning_selector->pack_start(add_kernpair_button, false, false, MARGIN_SPACE / 2);
+ add_kernpair_button.set_label(_("Add pair"));
+ add_kernpair_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_kerning_pair));
+ _KerningPairsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_kerning_pair_selection_changed));
+ kerning_slider->signal_value_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_kerning_value_changed));
+
+ kerning_vbox.pack_start(*kerning_selector, false,false);
+
+ kerning_vbox.pack_start(_KerningPairsListScroller, true,true);
+ _KerningPairsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
+ _KerningPairsListScroller.add(_KerningPairsList);
+ _KerningPairsList.set_model(_KerningPairsListStore);
+ _KerningPairsList.append_column(_("First glyph"), _KerningPairsListColumns.first_glyph);
+ _KerningPairsList.append_column(_("Second glyph"), _KerningPairsListColumns.second_glyph);
+// _KerningPairsList.append_column_numeric_editable(_("Kerning Value"), _KerningPairsListColumns.kerning_value, "%f");
+
+ kerning_vbox.pack_start((Gtk::Widget&) kerning_preview, false,false);
+
+ // kerning_slider has a big handle. Extra padding added
+ Gtk::Box* kerning_amount_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 8));
+ kerning_vbox.pack_start(*kerning_amount_hbox, false,false);
+ kerning_amount_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Kerning value:"))), false,false);
+ kerning_amount_hbox->pack_start(*kerning_slider, true,true);
+
+ kerning_preview.set_size(-1, 150 + 20);
+ _font_da.set_size(-1, 60 + 20);
+
+ return &kerning_vbox;
+}
+
+SPFont *new_font(SPDocument *document)
+{
+ g_return_val_if_fail(document != nullptr, NULL);
+
+ SPDefs *defs = document->getDefs();
+
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+
+ // create a new font
+ Inkscape::XML::Node *repr = xml_doc->createElement("svg:font");
+
+ //By default, set the horizontal advance to 1000 units
+ repr->setAttribute("horiz-adv-x", "1000");
+
+ // Append the new font node to defs
+ defs->getRepr()->appendChild(repr);
+
+ // add some default values
+ Inkscape::XML::Node *fontface;
+ fontface = xml_doc->createElement("svg:font-face");
+ fontface->setAttribute("units-per-em", "1000");
+ fontface->setAttribute("ascent", "750");
+ fontface->setAttribute("cap-height", "600");
+ fontface->setAttribute("x-height", "400");
+ fontface->setAttribute("descent", "200");
+ repr->appendChild(fontface);
+
+ //create a missing glyph
+ Inkscape::XML::Node *mg;
+ mg = xml_doc->createElement("svg:missing-glyph");
+ mg->setAttribute("d", "M0,0h1000v1000h-1000z");
+ repr->appendChild(mg);
+
+ // get corresponding object
+ auto f = cast<SPFont>( document->getObjectByRepr(repr) );
+
+ g_assert(f != nullptr);
+ Inkscape::GC::release(mg);
+ Inkscape::GC::release(repr);
+ return f;
+}
+
+void set_font_family(SPFont* font, char* str){
+ if (!font) return;
+ for (auto& obj: font->children) {
+ if (is<SPFontFace>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("font-family", str);
+ }
+ }
+
+ DocumentUndo::done(font->document, _("Set font family"), "");
+}
+
+void SvgFontsDialog::add_font(){
+ SPDocument* doc = this->getDesktop()->getDocument();
+ SPFont* font = new_font(doc);
+
+ const int count = _model->children().size();
+ std::ostringstream os, os2;
+ os << _("font") << " " << count;
+ font->setLabel(os.str().c_str());
+
+ os2 << "SVGFont " << count;
+ for (auto& obj: font->children) {
+ if (is<SPFontFace>(&obj)){
+ //XML Tree being directly used here while it shouldn't be.
+ obj.setAttribute("font-family", os2.str());
+ }
+ }
+
+ update_fonts(false);
+ on_font_selection_changed();
+
+ DocumentUndo::done(doc, _("Add font"), "");
+}
+
+SvgFontsDialog::SvgFontsDialog()
+ : DialogBase("/dialogs/svgfonts", "SVGFonts")
+ , global_vbox(Gtk::ORIENTATION_VERTICAL)
+ , glyphs_vbox(Gtk::ORIENTATION_VERTICAL)
+ , kerning_vbox(Gtk::ORIENTATION_VERTICAL)
+{
+ kerning_slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
+
+ // kerning pairs store
+ _KerningPairsListStore = Gtk::ListStore::create(_KerningPairsListColumns);
+
+ // list of glyphs in a current font; this store is reused if there are multiple fonts
+ _GlyphsListStore = Gtk::ListStore::create(_GlyphsListColumns);
+
+ // List of SVGFonts declared in a document:
+ _model = Gtk::ListStore::create(_columns);
+ _FontsList.set_model(_model);
+ _FontsList.set_enable_search(false);
+ _FontsList.append_column_editable(_("_Fonts"), _columns.label);
+ _FontsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_font_selection_changed));
+ // connect to the cell renderer's edit signal; there's also model's row_changed, but it is less specific
+ if (auto renderer = dynamic_cast<Gtk::CellRendererText*>(_FontsList.get_column_cell_renderer(0))) {
+ // commit font names when user edits them
+ renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& new_name) {
+ if (auto it = _model->get_iter(path)) {
+ auto font = it->get_value(_columns.spfont);
+ font->setLabel(new_name.c_str());
+ Glib::ustring undokey = "svgfonts:fontName";
+ DocumentUndo::maybeDone(font->document, undokey.c_str(), _("Set SVG font name"), "");
+ }
+ });
+ }
+
+ _add.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_font));
+ _remove.signal_clicked().connect([=](){ remove_selected_font(); });
+
+ Gtk::Notebook *tabs = Gtk::manage(new Gtk::Notebook());
+ tabs->set_scrollable();
+
+ tabs->append_page(*global_settings_tab(), _("_Global settings"), true);
+ tabs->append_page(*glyphs_tab(), _("_Glyphs"), true);
+ tabs->append_page(*kerning_tab(), _("_Kerning"), true);
+ tabs->signal_switch_page().connect([=](Gtk::Widget*, guint page) {
+ if (page == 2) {
+ // update kerning glyph combos
+ if (SPFont* font = get_selected_spfont()) {
+ first_glyph.update(font);
+ second_glyph.update(font);
+ }
+ }
+ });
+
+ pack_start(*tabs, true, true, 0);
+
+ // Text Preview:
+ _preview_entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_preview_text_changed));
+ pack_start((Gtk::Widget&) _font_da, false, false);
+ _preview_entry.set_text(_("Sample text"));
+ _font_da.set_text(_("Sample text"));
+
+ Gtk::Box* preview_entry_hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, MARGIN_SPACE));
+ pack_start(*preview_entry_hbox, false, false); // Non-latin characters may need more height.
+ preview_entry_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Preview text:"))), false, false);
+ preview_entry_hbox->pack_start(_preview_entry, true, true);
+ preview_entry_hbox->set_margin_bottom(MARGIN_SPACE);
+ preview_entry_hbox->set_margin_start(MARGIN_SPACE);
+ preview_entry_hbox->set_margin_end(MARGIN_SPACE);
+
+ _FontsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::fonts_list_button_release));
+ create_fonts_popup_menu(_FontsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_font));
+
+ show_all();
+}
+
+void SvgFontsDialog::documentReplaced()
+{
+ _defs_observer_connection.disconnect();
+ if (auto document = getDocument()) {
+ _defs_observer.set(document->getDefs());
+ _defs_observer_connection = _defs_observer.signal_changed().connect([=](){ update_fonts(false); });
+ }
+ update_fonts(true);
+}
+
+} // 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/dialog/svg-fonts-dialog.h b/src/ui/dialog/svg-fonts-dialog.h
new file mode 100644
index 0000000..4095029
--- /dev/null
+++ b/src/ui/dialog/svg-fonts-dialog.h
@@ -0,0 +1,384 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief SVG Fonts dialog
+ */
+/* Authors:
+ * Felipe Corrêa da Silva Sanches <juca@members.fsf.org>
+ *
+ * Copyright (C) 2008 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_SVG_FONTS_H
+#define INKSCAPE_UI_DIALOG_SVG_FONTS_H
+
+#include <2geom/pathvector.h>
+#include <gtkmm/box.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/drawingarea.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treeview.h>
+
+#include "attributes.h"
+#include "helper/auto-connection.h"
+#include "ui/operation-blocker.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/spinbutton.h"
+#include "xml/helper-observer.h"
+
+namespace Gtk {
+class Scale;
+}
+
+class SPGlyph;
+class SPGlyphKerning;
+class SvgFont;
+
+class SvgFontDrawingArea : Gtk::DrawingArea{
+public:
+ SvgFontDrawingArea();
+ void set_text(Glib::ustring);
+ void set_svgfont(SvgFont*);
+ void set_size(int x, int y);
+ void redraw();
+private:
+ int _x,_y;
+ SvgFont* _svgfont;
+ Glib::ustring _text;
+ bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override;
+};
+
+class SPFont;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class GlyphComboBox : public Gtk::ComboBoxText {
+public:
+ GlyphComboBox();
+ void update(SPFont*);
+};
+
+// cell text renderer for SVG font glyps (relying on Cairo "user font");
+// it can accept mouse clicks and report them via signal_clicked()
+class SvgGlyphRenderer : public Gtk::CellRenderer {
+public:
+ SvgGlyphRenderer() :
+ Glib::ObjectBase(typeid(CellRenderer)),
+ Gtk::CellRenderer(),
+ _property_active(*this, "active", true),
+ _property_activatable(*this, "activatable", true),
+ _property_glyph(*this, "glyph", "") {
+
+ property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE;
+ }
+
+ ~SvgGlyphRenderer() override = default;
+
+ Glib::PropertyProxy<Glib::ustring> property_glyph() { return _property_glyph.get_proxy(); }
+ Glib::PropertyProxy<bool> property_active() { return _property_active.get_proxy(); }
+ Glib::PropertyProxy<bool> property_activatable() { return _property_activatable.get_proxy(); }
+
+ sigc::signal<void (const GdkEvent*, const Glib::ustring&)>& signal_clicked() {
+ return _signal_clicked;
+ }
+
+ void set_svg_font(SvgFont* font) {
+ _font = font;
+ }
+
+ void set_font_size(int size) {
+ _font_size = size;
+ }
+
+ void set_tree(Gtk::Widget* tree) {
+ _tree = tree;
+ }
+
+ void set_cell_size(int w, int h) {
+ _width = w;
+ _height = h;
+ }
+
+ int get_width() const {
+ return _width;
+ }
+
+ 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;
+
+ 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;
+
+ void get_preferred_width_vfunc(Gtk::Widget& widget, int& min_w, int& nat_w) const override {
+ min_w = nat_w = _width;
+ }
+
+ void get_preferred_height_vfunc(Gtk::Widget& widget, int& min_h, int& nat_h) const override {
+ min_h = nat_h = _height;
+ }
+
+private:
+ int _width = 0;
+ int _height = 0;
+ int _font_size = 0;
+ Glib::Property<Glib::ustring> _property_glyph;
+ Glib::Property<bool> _property_active;
+ Glib::Property<bool> _property_activatable;
+ SvgFont* _font = nullptr;
+ Gtk::Widget* _tree = nullptr;
+ sigc::signal<void (const GdkEvent*, const Glib::ustring&)> _signal_clicked;
+};
+
+
+class SvgFontsDialog : public DialogBase
+{
+public:
+ SvgFontsDialog();
+ ~SvgFontsDialog() override = default;
+
+ void documentReplaced() override;
+
+ void update_fonts(bool document_replaced);
+ SvgFont* get_selected_svgfont();
+ SPFont* get_selected_spfont();
+ SPGlyph* get_selected_glyph();
+ SPGlyphKerning* get_selected_kerning_pair();
+
+ //TODO: these methods should be private, right?!
+ void on_font_selection_changed();
+ void on_kerning_pair_selection_changed();
+ void on_preview_text_changed();
+ void on_kerning_pair_changed();
+ void on_kerning_value_changed();
+ void on_setfontdata_changed();
+ void add_font();
+
+ // Used for font-family
+ class AttrEntry
+ {
+ public:
+ AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr);
+ void set_text(const char*);
+ Gtk::Entry* get_entry() { return &entry; }
+ Gtk::Label* get_label() { return _label; }
+ private:
+ SvgFontsDialog* dialog;
+ void on_attr_changed();
+ Gtk::Entry entry;
+ SPAttr attr;
+ Gtk::Label* _label;
+ };
+
+ class AttrSpin
+ {
+ public:
+ AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttr attr);
+ void set_value(double v);
+ void set_range(double low, double high);
+ Inkscape::UI::Widget::SpinButton* getSpin() { return &spin; }
+ Gtk::Label* get_label() { return _label; }
+ private:
+ SvgFontsDialog* dialog;
+ void on_attr_changed();
+ Inkscape::UI::Widget::SpinButton spin;
+ SPAttr attr;
+ Gtk::Label* _label;
+ };
+
+ OperationBlocker _update;
+
+private:
+ void update_glyphs(SPGlyph* changed_glyph = nullptr);
+ void update_glyph(SPGlyph* glyph);
+ void set_glyph_row(const Gtk::TreeRow& row, SPGlyph& glyph);
+ void refresh_svgfont();
+ void update_sensitiveness();
+ void update_global_settings_tab();
+ void populate_glyphs_box();
+ void populate_kerning_pairs_box();
+ void set_glyph_description_from_selected_path();
+ void missing_glyph_description_from_selected_path();
+ void reset_missing_glyph_description();
+ void add_glyph();
+ void glyph_unicode_edit(const Glib::ustring&, const Glib::ustring&);
+ void glyph_name_edit( const Glib::ustring&, const Glib::ustring&);
+ void glyph_advance_edit(const Glib::ustring&, const Glib::ustring&);
+ void remove_selected_glyph();
+ void remove_selected_font();
+ void remove_selected_kerning_pair();
+ void font_selected(SvgFont* svgfont, SPFont* spfont);
+
+ void add_kerning_pair();
+
+ void create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem);
+ void glyphs_list_button_release(GdkEventButton* event);
+
+ void create_fonts_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem);
+ void fonts_list_button_release(GdkEventButton* event);
+
+ void create_kerning_pairs_popup_menu(Gtk::Widget& parent, sigc::slot<void ()> rem);
+ void kerning_pairs_list_button_release(GdkEventButton* event);
+
+ Gtk::TreeModel::iterator get_selected_glyph_iter();
+ void set_selected_glyph(SPGlyph* glyph);
+ void edit_glyph(SPGlyph* glyph);
+ void sort_glyphs(SPFont* font);
+
+ Inkscape::XML::SignalObserver _defs_observer; //in order to update fonts
+ Inkscape::XML::SignalObserver _glyphs_observer;
+ Inkscape::auto_connection _defs_observer_connection;
+
+ Gtk::Box* AttrCombo(gchar* lbl, const SPAttr attr);
+ Gtk::Box* global_settings_tab();
+
+ // <font>
+ Gtk::Label* _font_label;
+ AttrSpin* _horiz_adv_x_spin;
+ AttrSpin* _horiz_origin_x_spin;
+ AttrSpin* _horiz_origin_y_spin;
+
+ // <font-face>
+ Gtk::Label* _font_face_label;
+ AttrEntry* _familyname_entry;
+ AttrSpin* _units_per_em_spin;
+ AttrSpin* _ascent_spin;
+ AttrSpin* _descent_spin;
+ AttrSpin* _cap_height_spin;
+ AttrSpin* _x_height_spin;
+
+ Gtk::Box* kerning_tab();
+ Gtk::Box* glyphs_tab();
+ Gtk::Button _add;
+ Gtk::Button _remove;
+ Gtk::Button add_glyph_button;
+ Gtk::Button remove_glyph_button;
+ Gtk::Button glyph_from_path_button;
+ Gtk::Button missing_glyph_button;
+ Gtk::Button missing_glyph_reset_button;
+
+ class Columns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ Columns() {
+ add(spfont);
+ add(svgfont);
+ add(label);
+ }
+
+ Gtk::TreeModelColumn<SPFont*> spfont;
+ Gtk::TreeModelColumn<SvgFont*> svgfont;
+ Gtk::TreeModelColumn<Glib::ustring> label;
+ };
+ Glib::RefPtr<Gtk::ListStore> _model;
+ Columns _columns;
+ Gtk::TreeView _FontsList;
+ Gtk::ScrolledWindow _fonts_scroller;
+
+ class GlyphsColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ GlyphsColumns() {
+ add(glyph_node);
+ add(glyph_name);
+ add(unicode);
+ add(UplusCode);
+ add(advance);
+ add(name_markup);
+ }
+
+ Gtk::TreeModelColumn<SPGlyph*> glyph_node;
+ Gtk::TreeModelColumn<Glib::ustring> glyph_name;
+ Gtk::TreeModelColumn<Glib::ustring> unicode;
+ Gtk::TreeModelColumn<Glib::ustring> UplusCode;
+ Gtk::TreeModelColumn<double> advance;
+ Gtk::TreeModelColumn<Glib::ustring> name_markup;
+ };
+ enum GlyphColumnIndex { ColGlyph, ColName, ColString, ColUplusCode, ColAdvance };
+ GlyphsColumns _GlyphsListColumns;
+ Glib::RefPtr<Gtk::ListStore> _GlyphsListStore;
+ Gtk::TreeView _GlyphsList;
+ Gtk::ScrolledWindow _GlyphsListScroller;
+ Gtk::ScrolledWindow _glyphs_icon_scroller;
+ Gtk::IconView _glyphs_grid;
+ SvgGlyphRenderer* _glyph_renderer = nullptr;
+ SvgGlyphRenderer* _glyph_cell_renderer = nullptr;
+
+ class KerningPairColumns : public Gtk::TreeModel::ColumnRecord
+ {
+ public:
+ KerningPairColumns() {
+ add(first_glyph);
+ add(second_glyph);
+ add(kerning_value);
+ add(spnode);
+ }
+
+ Gtk::TreeModelColumn<Glib::ustring> first_glyph;
+ Gtk::TreeModelColumn<Glib::ustring> second_glyph;
+ Gtk::TreeModelColumn<double> kerning_value;
+ Gtk::TreeModelColumn<SPGlyphKerning *> spnode;
+ };
+
+ KerningPairColumns _KerningPairsListColumns;
+ Glib::RefPtr<Gtk::ListStore> _KerningPairsListStore;
+ Gtk::TreeView _KerningPairsList;
+ Gtk::ScrolledWindow _KerningPairsListScroller;
+ Gtk::Button add_kernpair_button;
+
+ Gtk::Grid _header_box;
+ Gtk::Grid _grid;
+ Gtk::Box global_vbox;
+ Gtk::Box glyphs_vbox;
+ Gtk::Box kerning_vbox;
+ Gtk::Entry _preview_entry;
+ bool _show_glyph_list = true;
+ void set_glyphs_view_mode(bool list);
+
+ Gtk::Menu _FontsContextMenu;
+ Gtk::Menu _GlyphsContextMenu;
+ Gtk::Menu _KerningPairsContextMenu;
+
+ SvgFontDrawingArea _font_da, kerning_preview;
+ GlyphComboBox first_glyph, second_glyph;
+ SPGlyphKerning* kerning_pair;
+ Inkscape::UI::Widget::SpinButton setwidth_spin;
+ Gtk::Scale* kerning_slider;
+
+ class EntryWidget : public Gtk::Box
+ {
+ public:
+ EntryWidget()
+ : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {
+ this->add(this->_label);
+ this->add(this->_entry);
+ }
+ void set_label(const gchar* l){
+ this->_label.set_text(l);
+ }
+ private:
+ Gtk::Label _label;
+ Gtk::Entry _entry;
+ };
+ EntryWidget _font_family, _font_variant;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif //#ifndef INKSCAPE_UI_DIALOG_SVG_FONTS_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/dialog/svg-preview.cpp b/src/ui/dialog/svg-preview.cpp
new file mode 100644
index 0000000..1474d99
--- /dev/null
+++ b/src/ui/dialog/svg-preview.cpp
@@ -0,0 +1,448 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Implementation of the file dialog interfaces defined in filedialogimpl.h.
+ */
+/* Authors:
+ * Bob Jamison
+ * Joel Holdsworth
+ * Bruno Dilly
+ * Other dudes from The Inkscape Organization
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004-2007 Bob Jamison
+ * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl>
+ * Copyright (C) 2007-2008 Joel Holdsworth
+ * Copyright (C) 2004-2007 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <iostream>
+#include <fstream>
+
+#include <glibmm/i18n.h>
+#include <glib/gstdio.h> // GStatBuf
+
+#include "svg-preview.h"
+
+#include "ui/view/svg-view-widget.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+bool SVGPreview::setDocument(SPDocument *doc)
+{
+ if (viewer) {
+ viewer->setDocument(doc);
+ } else {
+ viewer = std::make_unique<Inkscape::UI::View::SVGViewWidget>(doc);
+ pack_start(*viewer, true, true);
+ }
+
+ document.reset(doc);
+
+ show_all();
+
+ return true;
+}
+
+bool SVGPreview::setFileName(Glib::ustring const &theFileName)
+{
+ auto fileName = Glib::filename_to_utf8(theFileName);
+
+ /**
+ * I don't know why passing false to keepalive is bad. But it
+ * prevents the display of an svg with a non-ascii filename
+ */
+ SPDocument *doc = SPDocument::createNewDoc(fileName.c_str(), true);
+ if (!doc) {
+ g_warning("SVGView: error loading document '%s'\n", fileName.c_str());
+ return false;
+ }
+
+ setDocument(doc);
+
+ return true;
+}
+
+bool SVGPreview::setFromMem(char const *xmlBuffer)
+{
+ if (!xmlBuffer)
+ return false;
+
+ gint len = (gint)strlen(xmlBuffer);
+ SPDocument *doc = SPDocument::createNewDocFromMem(xmlBuffer, len, false);
+ if (!doc) {
+ g_warning("SVGView: error loading buffer '%s'\n", xmlBuffer);
+ return false;
+ }
+
+ setDocument(doc);
+
+ return true;
+}
+
+void SVGPreview::showImage(Glib::ustring const &theFileName)
+{
+ Glib::ustring fileName = theFileName;
+
+ // Let's get real width and height from SVG file. These are template
+ // files so we assume they are well formed.
+
+ // std::cout << "SVGPreview::showImage: " << theFileName.raw() << std::endl;
+ std::string width;
+ std::string height;
+
+ /*#####################################
+ # LET'S HAVE SOME FUN WITH SVG!
+ # Instead of just loading an image, why
+ # don't we make a lovely little svg and
+ # display it nicely?
+ #####################################*/
+
+ // Arbitrary size of svg doc -- rather 'portrait' shaped
+ gint previewWidth = 400;
+ gint previewHeight = 600;
+
+ // Get some image info. Smart pointer does not need to be deleted
+ Glib::RefPtr<Gdk::Pixbuf> img(nullptr);
+ try
+ {
+ img = Gdk::Pixbuf::create_from_file(fileName);
+ }
+ catch (const Glib::FileError &e)
+ {
+ g_message("caught Glib::FileError in SVGPreview::showImage");
+ return;
+ }
+ catch (const Gdk::PixbufError &e)
+ {
+ g_message("Gdk::PixbufError in SVGPreview::showImage");
+ return;
+ }
+ catch (...)
+ {
+ g_message("Caught ... in SVGPreview::showImage");
+ return;
+ }
+
+ gint imgWidth = img->get_width();
+ gint imgHeight = img->get_height();
+
+ Glib::ustring svg = ".svg";
+ if (hasSuffix(fileName, svg)) {
+ std::ifstream input(theFileName);
+ if( !input ) {
+ std::cerr << "SVGPreview::showImage: Failed to open file: " << theFileName.raw() << std::endl;
+ } else {
+
+ Glib::ustring token;
+
+ Glib::MatchInfo match_info;
+ Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create("width=\"(.*)\"");
+ Glib::RefPtr<Glib::Regex> regex2 = Glib::Regex::create("height=\"(.*)\"");
+
+ while( !input.eof() && (height.empty() || width.empty()) ) {
+
+ input >> token;
+ // std::cout << "|" << token << "|" << std::endl;
+
+ if (regex1->match(token, match_info)) {
+ width = match_info.fetch(1).raw();
+ }
+
+ if (regex2->match(token, match_info)) {
+ height = match_info.fetch(1).raw();
+ }
+
+ }
+ }
+ }
+
+ if (height.empty() || width.empty()) {
+ width = std::to_string(imgWidth);
+ height = std::to_string(imgHeight);
+ }
+
+ // Find the minimum scale to fit the image inside the preview area
+ double scaleFactorX = (0.9 * (double)previewWidth) / ((double)imgWidth);
+ double scaleFactorY = (0.9 * (double)previewHeight) / ((double)imgHeight);
+ double scaleFactor = scaleFactorX;
+ if (scaleFactorX > scaleFactorY)
+ scaleFactor = scaleFactorY;
+
+ // Now get the resized values
+ gint scaledImgWidth = (int)(scaleFactor * (double)imgWidth);
+ gint scaledImgHeight = (int)(scaleFactor * (double)imgHeight);
+
+ // center the image on the area
+ gint imgX = (previewWidth - scaledImgWidth) / 2;
+ gint imgY = (previewHeight - scaledImgHeight) / 2;
+
+ // wrap a rectangle around the image
+ gint rectX = imgX - 1;
+ gint rectY = imgY - 1;
+ gint rectWidth = scaledImgWidth + 2;
+ gint rectHeight = scaledImgHeight + 2;
+
+ // Our template. Modify to taste
+ gchar const *xformat = R"A(
+<svg width="%d" height="%d"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <rect width="100%" height="100%" style="fill:#eeeeee"/>
+ <image x="%d" y="%d" width="%d" height="%d" xlink:href="%s"/>
+ <rect x="%d" y="%d" width="%d" height="%d" style="fill:none;stroke:black"/>
+ <text x="50%" y="55%" style="font-family:sans-serif;font-size:24px;text-anchor:middle">%s x %s</text>
+</svg>
+)A";
+
+ // if (!Glib::get_charset()) //If we are not utf8
+ fileName = Glib::filename_to_utf8(fileName);
+ // Filenames in xlinks are decoded, so any % will break without this.
+ auto encodedName = Glib::uri_escape_string(fileName);
+
+ // Fill in the template
+ /* FIXME: Do proper XML quoting for fileName. */
+ gchar *xmlBuffer =
+ g_strdup_printf(xformat, previewWidth, previewHeight, imgX, imgY, scaledImgWidth, scaledImgHeight,
+ encodedName.c_str(), rectX, rectY, rectWidth, rectHeight, width.c_str(), height.c_str() );
+
+ // g_message("%s\n", xmlBuffer);
+
+ // now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+}
+
+void SVGPreview::showNoPreview()
+{
+ // Are we already showing it?
+ if (showingNoPreview)
+ return;
+
+ // Our template. Modify to taste
+ gchar const *xformat = R"B(
+<svg width="400" height="600"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(-160,90)" style="opacity:0.10">
+ <path style="fill:white"
+ d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225
+ 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/>
+ <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802
+ C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478
+ 356.11361 467.54478 342.93607 476.95792 339.17168 z"
+ id="droplet01" />
+ <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338
+ C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298
+ 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477
+ 287.18046 343.1206 286.46194 340.42914 z"
+ id= "droplet02"/>
+ <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634
+ C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141
+ 308.81083 505.53237 308.13423 510.35756 306.92856 z"
+ id="droplet03"/>
+ <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223
+ L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577
+ L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279
+ C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865
+ C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263
+ 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346
+ 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439
+ 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302
+ 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397
+ 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739
+ 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708
+ 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567
+ 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716
+ 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428
+ 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378
+ 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915
+ 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062
+ 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111
+ 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538
+ 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215
+ 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949
+ 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118
+ 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919
+ 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685
+ 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608
+ 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299
+ 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346
+ 274.45012 297.36372 269.98057 L 290.92738 261.61333 z"
+ id="mountainDroplet"/>
+ </g>
+ <text xml:space="preserve" x="200" y="320"
+ style="font-size:32px;font-weight:bold;text-anchor:middle">%s</text>
+</svg>
+)B";
+
+ // Fill in the template
+ gchar *xmlBuffer = g_strdup_printf(xformat, _("No preview"));
+
+ // g_message("%s\n", xmlBuffer);
+
+ // Now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+ showingNoPreview = true;
+}
+
+/**
+ * Inform the user that the svg file is too large to be displayed.
+ * This does not check for sizes of embedded images (yet)
+ */
+void SVGPreview::showTooLarge(long fileLength)
+{
+ // Our template. Modify to taste
+ gchar const *xformat = R"C(
+<svg width="400" height="600"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g transform="translate(-160,90)" style="opacity:0.10">
+ <path style="fill:white"
+ d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225
+ 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/>
+ <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802
+ C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478
+ 356.11361 467.54478 342.93607 476.95792 339.17168 z"
+ id="droplet01" />
+ <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338
+ C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298
+ 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477
+ 287.18046 343.1206 286.46194 340.42914 z"
+ id= "droplet02"/>
+ <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634
+ C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141
+ 308.81083 505.53237 308.13423 510.35756 306.92856 z"
+ id="droplet03"/>
+ <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223
+ L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577
+ L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279
+ C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865
+ C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263
+ 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346
+ 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439
+ 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302
+ 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397
+ 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739
+ 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708
+ 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567
+ 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716
+ 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428
+ 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378
+ 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915
+ 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062
+ 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111
+ 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538
+ 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215
+ 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949
+ 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118
+ 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919
+ 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685
+ 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608
+ 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299
+ 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346
+ 274.45012 297.36372 269.98057 L 290.92738 261.61333 z"
+ id="mountainDroplet"/>
+ </g>
+ <text xml:space="preserve" x="200" y="280"
+ style="font-size:20px;font-weight:bold;text-anchor:middle">%.1f MB</text>
+ <text xml:space="preserve" x="200" y="360"
+ style="font-size:20px;font-weight:bold;text-anchor:middle">%s</text>
+</svg>
+)C";
+
+ // Fill in the template
+ double floatFileLength = ((double)fileLength) / 1048576.0;
+ // printf("%ld %f\n", fileLength, floatFileLength);
+
+ gchar *xmlBuffer =
+ g_strdup_printf(xformat, floatFileLength, _("Too large for preview"));
+
+ // g_message("%s\n", xmlBuffer);
+
+ // now show it!
+ setFromMem(xmlBuffer);
+ g_free(xmlBuffer);
+}
+
+bool SVGPreview::set(Glib::ustring const &fileName, int dialogType)
+{
+ if (!Glib::file_test(fileName, Glib::FILE_TEST_EXISTS)) {
+ showNoPreview();
+ return false;
+ }
+
+ if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) {
+ showNoPreview();
+ return false;
+ }
+
+ if (Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)) {
+ Glib::ustring fileNameUtf8 = Glib::filename_to_utf8(fileName);
+ gchar *fName = const_cast<gchar *>(
+ fileNameUtf8.c_str()); // const-cast probably not necessary? (not necessary on Windows version of stat())
+ GStatBuf info;
+ if (g_stat(fName, &info)) // stat returns 0 upon success
+ {
+ g_warning("SVGPreview::set() : %s : %s", fName, strerror(errno));
+ return false;
+ }
+ if (info.st_size > 0xA00000L) {
+ showingNoPreview = false;
+ showTooLarge(info.st_size);
+ return false;
+ }
+ }
+
+ Glib::ustring svg = ".svg";
+ Glib::ustring svgz = ".svgz";
+
+ if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) &&
+ (hasSuffix(fileName, svg) || hasSuffix(fileName, svgz))) {
+ bool retval = setFileName(fileName);
+ showingNoPreview = false;
+ return retval;
+ } else if (isValidImageFile(fileName)) {
+ showImage(fileName);
+ showingNoPreview = false;
+ return true;
+ } else {
+ showNoPreview();
+ return false;
+ }
+}
+
+SVGPreview::SVGPreview()
+ : Gtk::Box(Gtk::ORIENTATION_VERTICAL)
+ , showingNoPreview(false)
+{
+ set_size_request(200, 300);
+}
+
+SVGPreview::~SVGPreview()
+{
+ // Ensure correct destruction order: viewer before document.
+ viewer.reset();
+ document.reset();
+}
+
+} // 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/dialog/svg-preview.h b/src/ui/dialog/svg-preview.h
new file mode 100644
index 0000000..0df897f
--- /dev/null
+++ b/src/ui/dialog/svg-preview.h
@@ -0,0 +1,123 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h
+ */
+/* Authors:
+ * Bob Jamison
+ * Johan Engelen <johan@shouraizou.nl>
+ * Joel Holdsworth
+ * Bruno Dilly
+ * Other dudes from The Inkscape Organization
+ *
+ * Copyright (C) 2004-2008 Authors
+ * Copyright (C) 2004-2007 The Inkscape Organization
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef __SVG_PREVIEW_H__
+#define __SVG_PREVIEW_H__
+
+//Gtk includes
+#include <gtkmm.h>
+
+//General includes
+#include <unistd.h>
+#include <sys/stat.h>
+#include <cerrno>
+
+#include "filedialog.h"
+#include "document.h"
+
+namespace Gtk {
+class Expander;
+}
+
+namespace Inkscape {
+ class URI;
+
+namespace UI {
+
+namespace View {
+ class SVGViewWidget;
+}
+
+namespace Dialog {
+
+/*#########################################################################
+### SVG Preview Widget
+#########################################################################*/
+
+/**
+ * Simple class for displaying an SVG file in the "preview widget."
+ * Currently, this is just a wrapper of the sp_svg_view Gtk widget.
+ * Hopefully we will eventually replace with a pure Gtkmm widget.
+ */
+class SVGPreview : public Gtk::Box
+{
+public:
+
+ SVGPreview();
+
+ ~SVGPreview() override;
+
+ bool setDocument(SPDocument *doc);
+
+ bool setFileName(Glib::ustring const &fileName);
+
+ bool setFromMem(char const *xmlBuffer);
+
+ bool set(Glib::ustring const &fileName, int dialogType);
+
+ bool setURI(URI &uri);
+
+ /**
+ * Show image embedded in SVG
+ */
+ void showImage(Glib::ustring const &fileName);
+
+ /**
+ * Show the "No preview" image
+ */
+ void showNoPreview();
+
+ /**
+ * Show the "Too large" image
+ */
+ void showTooLarge(long fileLength);
+
+private:
+ /**
+ * The svg document we are currently showing
+ */
+ std::unique_ptr<SPDocument> document;
+
+ /**
+ * The sp_svg_view widget
+ */
+ std::unique_ptr<Inkscape::UI::View::SVGViewWidget> viewer;
+
+ /**
+ * are we currently showing the "no preview" image?
+ */
+ bool showingNoPreview;
+
+};
+
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif /*__SVG_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/dialog/swatches.cpp b/src/ui/dialog/swatches.cpp
new file mode 100644
index 0000000..a059796
--- /dev/null
+++ b/src/ui/dialog/swatches.cpp
@@ -0,0 +1,447 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Authors:
+ * Jon A. Cruz
+ * John Bintz
+ * Abhishek Sharma
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2005 Jon A. Cruz
+ * Copyright (C) 2008 John Bintz
+ * Copyright (C) 2022 PBS
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "swatches.h"
+
+#include <algorithm>
+#include <glibmm/i18n.h>
+
+#include "document.h"
+#include "object/sp-defs.h"
+#include "style.h"
+#include "desktop-style.h"
+#include "object/sp-gradient-reference.h"
+
+#include "inkscape-preferences.h"
+#include "widgets/paintdef.h"
+#include "ui/widget/color-palette.h"
+#include "ui/dialog/global-palettes.h"
+#include "ui/dialog/color-item.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/*
+ * Lifecycle
+ */
+
+SwatchesPanel::SwatchesPanel(char const *prefsPath)
+ : DialogBase(prefsPath, "Swatches")
+{
+ _palette = Gtk::make_managed<Inkscape::UI::Widget::ColorPalette>();
+ pack_start(*_palette);
+ update_palettes();
+
+ bool embedded = _prefs_path != "/dialogs/swatches";
+ _palette->set_compact(embedded);
+
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+
+ index = name_to_index(prefs->getString(_prefs_path + "/palette"));
+
+ // restore palette settings
+ _palette->set_tile_size(prefs->getInt(_prefs_path + "/tile_size", 16));
+ _palette->set_aspect(prefs->getDoubleLimited(_prefs_path + "/tile_aspect", 0.0, -2, 2));
+ _palette->set_tile_border(prefs->getInt(_prefs_path + "/tile_border", 1));
+ _palette->set_rows(prefs->getInt(_prefs_path + "/rows", 1));
+ _palette->enable_stretch(prefs->getBool(_prefs_path + "/tile_stretch", false));
+ _palette->set_large_pinned_panel(embedded && prefs->getBool(_prefs_path + "/enlarge_pinned", true));
+ _palette->enable_labels(!embedded && prefs->getBool(_prefs_path + "/show_labels", true));
+
+ // save settings when they change
+ _palette->get_settings_changed_signal().connect([=] {
+ prefs->setInt(_prefs_path + "/tile_size", _palette->get_tile_size());
+ prefs->setDouble(_prefs_path + "/tile_aspect", _palette->get_aspect());
+ prefs->setInt(_prefs_path + "/tile_border", _palette->get_tile_border());
+ prefs->setInt(_prefs_path + "/rows", _palette->get_rows());
+ prefs->setBool(_prefs_path + "/tile_stretch", _palette->is_stretch_enabled());
+ prefs->setBool(_prefs_path + "/enlarge_pinned", _palette->is_pinned_panel_large());
+ prefs->setBool(_prefs_path + "/show_labels", !embedded && _palette->are_labels_enabled());
+ });
+
+ // Respond to requests from the palette widget to change palettes.
+ _palette->get_palette_selected_signal().connect([this] (Glib::ustring name) {
+ Preferences::get()->setString(_prefs_path + "/palette", name);
+ set_index(name_to_index(name));
+ });
+
+ // Watch for pinned palette options.
+ _pinned_observer = prefs->createObserver(_prefs_path + "/pinned/", [this]() {
+ rebuild();
+ });
+
+ rebuild();
+}
+
+SwatchesPanel::~SwatchesPanel()
+{
+ untrack_gradients();
+}
+
+/*
+ * Activation
+ */
+
+// Note: The "Auto" palette shows the list of gradients that are swatches. When this palette is
+// shown (and we have a document), we therefore need to track both addition/removal of gradients
+// and changes to the isSwatch() status to keep the palette up-to-date.
+
+void SwatchesPanel::documentReplaced()
+{
+ if (getDocument()) {
+ if (index == PALETTE_AUTO) {
+ track_gradients();
+ }
+ } else {
+ untrack_gradients();
+ }
+
+ if (index == PALETTE_AUTO) {
+ rebuild();
+ }
+}
+
+void SwatchesPanel::desktopReplaced()
+{
+ documentReplaced();
+}
+
+void SwatchesPanel::set_index(PaletteIndex new_index)
+{
+ if (index == new_index) return;
+ index = new_index;
+
+ if (index == PALETTE_AUTO) {
+ if (getDocument()) {
+ track_gradients();
+ }
+ } else {
+ untrack_gradients();
+ }
+
+ rebuild();
+}
+
+void SwatchesPanel::track_gradients()
+{
+ auto doc = getDocument();
+
+ // Subscribe to the addition and removal of gradients.
+ conn_gradients.disconnect();
+ conn_gradients = doc->connectResourcesChanged("gradient", [this] {
+ gradients_changed = true;
+ queue_resize();
+ });
+
+ // Subscribe to child modifications of the defs section. We will use this to monitor
+ // each gradient for whether its isSwatch() status changes.
+ conn_defs.disconnect();
+ conn_defs = doc->getDefs()->connectModified([this] (SPObject*, unsigned flags) {
+ if (flags & SP_OBJECT_CHILD_MODIFIED_FLAG) {
+ defs_changed = true;
+ queue_resize();
+ }
+ });
+
+ gradients_changed = false;
+ defs_changed = false;
+ rebuild_isswatch();
+}
+
+void SwatchesPanel::untrack_gradients()
+{
+ conn_gradients.disconnect();
+ conn_defs.disconnect();
+ gradients_changed = false;
+ defs_changed = false;
+}
+
+/*
+ * Updating
+ */
+
+void SwatchesPanel::selectionChanged(Selection*)
+{
+ selection_changed = true;
+ queue_resize();
+}
+
+void SwatchesPanel::selectionModified(Selection*, guint flags)
+{
+ if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) {
+ selection_changed = true;
+ queue_resize();
+ }
+}
+
+// Document updates are handled asynchronously by setting a flag and queuing a resize. This results in
+// the following function being run at the last possible moment before the widget will be repainted.
+// This ensures that multiple document updates only result in a single UI update.
+void SwatchesPanel::on_size_allocate(Gtk::Allocation &alloc)
+{
+ if (gradients_changed) {
+ assert(index = PALETTE_AUTO);
+ // We are in the "Auto" palette, and a gradient was added or removed.
+ // The list of widgets has therefore changed, and must be completely rebuilt.
+ // We must also rebuild the tracking information for each gradient's isSwatch() status.
+ rebuild_isswatch();
+ rebuild();
+ } else if (defs_changed) {
+ assert(index = PALETTE_AUTO);
+ // We are in the "Auto" palette, and a gradient's isSwatch() status was possibly modified.
+ // Check if it has; if so, then the list of widgets has changed, and must be rebuilt.
+ if (update_isswatch()) {
+ rebuild();
+ }
+ }
+
+ if (selection_changed) {
+ update_fillstroke_indicators();
+ }
+
+ selection_changed = false;
+ gradients_changed = false;
+ defs_changed = false;
+
+ // Necessary to perform *after* the above widget modifications, so GTK can process the new layout.
+ DialogBase::on_size_allocate(alloc);
+}
+
+// TODO: The following two functions can made much nicer using C++20 ranges.
+
+void SwatchesPanel::rebuild_isswatch()
+{
+ auto grads = getDocument()->getResourceList("gradient");
+
+ isswatch.resize(grads.size());
+
+ for (int i = 0; i < grads.size(); i++) {
+ isswatch[i] = static_cast<SPGradient*>(grads[i])->isSwatch();
+ }
+}
+
+bool SwatchesPanel::update_isswatch()
+{
+ auto grads = getDocument()->getResourceList("gradient");
+
+ // Should be guaranteed because we catch all size changes and call rebuild_isswatch() instead.
+ assert(isswatch.size() == grads.size());
+
+ bool modified = false;
+
+ for (int i = 0; i < grads.size(); i++) {
+ if (isswatch[i] != static_cast<SPGradient*>(grads[i])->isSwatch()) {
+ isswatch[i].flip();
+ modified = true;
+ }
+ }
+
+ return modified;
+}
+
+static auto spcolor_to_rgb(SPColor const &color)
+{
+ float rgbf[3];
+ color.get_rgb_floatv(rgbf);
+
+ std::array<unsigned, 3> rgb;
+ for (int i = 0; i < 3; i++) {
+ rgb[i] = SP_COLOR_F_TO_U(rgbf[i]);
+ };
+
+ return rgb;
+}
+
+void SwatchesPanel::update_fillstroke_indicators()
+{
+ auto doc = getDocument();
+ auto style = SPStyle(doc);
+
+ // Get the current fill or stroke as a ColorKey.
+ auto current_color = [&, this] (bool fill) -> std::optional<ColorKey> {
+ switch (sp_desktop_query_style(getDesktop(), &style, fill ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE))
+ {
+ case QUERY_STYLE_SINGLE:
+ case QUERY_STYLE_MULTIPLE_AVERAGED:
+ case QUERY_STYLE_MULTIPLE_SAME:
+ break;
+ default:
+ return {};
+ }
+
+ auto attr = style.getFillOrStroke(fill);
+ if (!attr->set) {
+ return {};
+ }
+
+ if (attr->isNone()) {
+ return std::monostate{};
+ } else if (attr->isColor()) {
+ return spcolor_to_rgb(attr->value.color);
+ } else if (attr->isPaintserver()) {
+ if (auto grad = cast<SPGradient>(fill ? style.getFillPaintServer() : style.getStrokePaintServer())) {
+ if (grad->isSwatch()) {
+ return grad;
+ } else if (grad->ref) {
+ if (auto ref = grad->ref->getObject(); ref && ref->isSwatch()) {
+ return ref;
+ }
+ }
+ }
+ }
+
+ return {};
+ };
+
+ for (auto w : current_fill) w->set_fill(false);
+ for (auto w : current_stroke) w->set_stroke(false);
+
+ current_fill.clear();
+ current_stroke.clear();
+
+ if (auto fill = current_color(true)) {
+ auto range = widgetmap.equal_range(*fill);
+ for (auto it = range.first; it != range.second; ++it) {
+ current_fill.emplace_back(it->second);
+ }
+ }
+
+ if (auto stroke = current_color(false)) {
+ auto range = widgetmap.equal_range(*stroke);
+ for (auto it = range.first; it != range.second; ++it) {
+ current_stroke.emplace_back(it->second);
+ }
+ }
+
+ for (auto w : current_fill) w->set_fill(true);
+ for (auto w : current_stroke) w->set_stroke(true);
+}
+
+SwatchesPanel::PaletteIndex SwatchesPanel::name_to_index(Glib::ustring const &name)
+{
+ auto &palettes = GlobalPalettes::get().palettes;
+ if (name == "Auto") {
+ return PALETTE_AUTO;
+ } else if (auto it = std::find_if(palettes.begin(), palettes.end(), [&] (PaletteFileData const &p) {return p.name == name;}); it != palettes.end()) {
+ return (PaletteIndex)(PALETTE_GLOBAL + std::distance(palettes.begin(), it));
+ } else {
+ return PALETTE_NONE;
+ }
+}
+
+Glib::ustring SwatchesPanel::index_to_name(PaletteIndex index)
+{
+ auto &palettes = GlobalPalettes::get().palettes;
+ if (index == PALETTE_AUTO) {
+ return "Auto";
+ } else if (auto n = index - PALETTE_GLOBAL; n >= 0 && n < palettes.size()) {
+ return palettes[n].name;
+ } else {
+ return "";
+ }
+}
+
+/**
+ * Process the list of available palettes and update the list in the _palette widget.
+ */
+void SwatchesPanel::update_palettes()
+{
+ std::vector<Inkscape::UI::Widget::ColorPalette::palette_t> palettes;
+ palettes.reserve(1 + GlobalPalettes::get().palettes.size());
+
+ // The first palette in the list is always the "Auto" palette. Although this
+ // will contain colors when selected, the preview we show for it is empty.
+ palettes.push_back({"Auto", {}});
+
+ // The remaining palettes in the list are the global palettes.
+ for (auto &p : GlobalPalettes::get().palettes) {
+ Inkscape::UI::Widget::ColorPalette::palette_t palette;
+ palette.name = p.name;
+ for (auto const &c : p.colors) {
+ auto [r, g, b] = c.rgb;
+ palette.colors.push_back({r / 255.0, g / 255.0, b / 255.0});
+ }
+ palettes.emplace_back(std::move(palette));
+ }
+
+ _palette->set_palettes(palettes);
+}
+
+/**
+ * Rebuild the list of color items shown by the palette.
+ */
+void SwatchesPanel::rebuild()
+{
+ std::vector<ColorItem*> palette;
+
+ // The pointers in widgetmap are to widgets owned by the ColorPalette. It is assumed it will not
+ // delete them unless we ask, via the call to set_colors() later in this function.
+ widgetmap.clear();
+ current_fill.clear();
+ current_stroke.clear();
+
+ // Add the "remove-color" color.
+ auto w = Gtk::make_managed<ColorItem>(PaintDef(), this);
+ w->set_pinned_pref(_prefs_path);
+ palette.emplace_back(w);
+ widgetmap.emplace(std::monostate{}, w);
+
+ if (index >= PALETTE_GLOBAL) {
+ auto &pal = GlobalPalettes::get().palettes[index - PALETTE_GLOBAL];
+ palette.reserve(palette.size() + pal.colors.size());
+ for (auto &c : pal.colors) {
+ auto w = Gtk::make_managed<ColorItem>(PaintDef(c.rgb, c.name), this);
+ w->set_pinned_pref(_prefs_path);
+ palette.emplace_back(w);
+ widgetmap.emplace(c.rgb, w);
+ }
+ } else if (index == PALETTE_AUTO && getDocument()) {
+ auto grads = getDocument()->getResourceList("gradient");
+ for (auto obj : grads) {
+ auto grad = static_cast<SPGradient*>(obj);
+ if (grad->isSwatch()) {
+ auto w = Gtk::make_managed<ColorItem>(grad, this);
+ palette.emplace_back(w);
+ widgetmap.emplace(grad, w);
+ // Rebuild if the gradient gets pinned or unpinned
+ w->signal_pinned().connect([=]() {
+ rebuild();
+ });
+ }
+ }
+ }
+
+ if (getDocument()) {
+ update_fillstroke_indicators();
+ }
+
+ _palette->set_colors(palette);
+ _palette->set_selected(index_to_name(index));
+}
+
+} //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/dialog/swatches.h b/src/ui/dialog/swatches.h
new file mode 100644
index 0000000..7cd8e50
--- /dev/null
+++ b/src/ui/dialog/swatches.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Color swatches dialog
+ */
+/* Authors:
+ * Jon A. Cruz
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef UI_DIALOG_SWATCHES_H
+#define UI_DIALOG_SWATCHES_H
+
+#include <array>
+#include <vector>
+#include <boost/unordered_map.hpp>
+#include <boost/variant.hpp>
+#include <glibmm/ustring.h>
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+
+namespace Widget {
+class ColorPalette;
+}
+
+namespace Dialog {
+class ColorItem;
+
+/**
+ * A dialog that displays paint swatches.
+ *
+ * It comes in two flavors, depending on the prefsPath argument passed to
+ * the constructor: the default "/dialog/swatches" is just a regular dialog;
+ * the "/embedded/swatches" is the horizontal color palette at the bottom
+ * of the window.
+ */
+class SwatchesPanel : public DialogBase
+{
+public:
+ SwatchesPanel(char const *prefsPath = "/dialogs/swatches");
+ ~SwatchesPanel() override;
+
+protected:
+ void documentReplaced() override;
+ void desktopReplaced() override;
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+ void on_size_allocate(Gtk::Allocation&) override;
+
+private:
+ void update_palettes();
+ void rebuild();
+
+ Inkscape::UI::Widget::ColorPalette *_palette;
+
+ // Mapping between palette names and indexes.
+ enum PaletteIndex
+ {
+ PALETTE_NONE = -2,
+ PALETTE_AUTO = -1,
+ PALETTE_GLOBAL = 0,
+ };
+ PaletteIndex index;
+ static PaletteIndex name_to_index(Glib::ustring const&);
+ static Glib::ustring index_to_name(PaletteIndex);
+ void set_index(PaletteIndex new_index);
+
+ // Asynchronous update mechanism.
+ sigc::connection conn_gradients;
+ sigc::connection conn_defs;
+ bool gradients_changed = false;
+ bool defs_changed = false;
+ bool selection_changed = false;
+ void track_gradients();
+ void untrack_gradients();
+
+ // For each gradient, whether or not it is a swatch. Used to track when isSwatch() changes.
+ std::vector<bool> isswatch;
+ void rebuild_isswatch();
+ bool update_isswatch();
+
+ // A map from colors to their respective widgets. Used to quickly find the widgets corresponding
+ // to the current fill/stroke color, in order to update their fill/stroke indicators.
+ // TODO: Upgrade to boost::variant2 or std::variant when possible.
+ using ColorKey = boost::variant<std::monostate, std::array<unsigned, 3>, SPGradient*>;
+ boost::unordered_multimap<ColorKey, ColorItem*> widgetmap; // need boost for array hash
+ std::vector<ColorItem*> current_fill;
+ std::vector<ColorItem*> current_stroke;
+ void update_fillstroke_indicators();
+
+ Inkscape::PrefObserver _pinned_observer;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // UI_DIALOG_SWATCHES_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/dialog/symbols.cpp b/src/ui/dialog/symbols.cpp
new file mode 100644
index 0000000..aa1e228
--- /dev/null
+++ b/src/ui/dialog/symbols.cpp
@@ -0,0 +1,1361 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Symbols dialog.
+ */
+/* Authors:
+ * Copyright (C) 2012 Tavmjong Bah
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/point.h>
+#include <cairo.h>
+#include <cairomm/refptr.h>
+#include <cairomm/surface.h>
+#include <cassert>
+#include <cmath>
+#include <cstddef>
+#include <gdkmm/pixbuf.h>
+#include <gdkmm/rgba.h>
+#include <glibmm/main.h>
+#include <glibmm/priorities.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/box.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/cellrenderertext.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/label.h>
+#include <gtkmm/liststore.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/popover.h>
+#include <gtkmm/scale.h>
+#include <gtkmm/searchentry.h>
+#include <gtkmm/treeiter.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treemodelfilter.h>
+#include <gtkmm/treemodelsort.h>
+#include <gtkmm/treepath.h>
+#include <pangomm/layout.h>
+#include <string>
+#include <vector>
+#include "preferences.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog/messages.h"
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include "symbols.h"
+
+#include <iostream>
+#include <algorithm>
+#include <locale>
+#include <sstream>
+#include <fstream>
+#include <regex>
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+#include <glibmm/regex.h>
+#include <glibmm/stringutils.h>
+
+#include "document.h"
+#include "inkscape.h"
+#include "path-prefix.h"
+#include "selection.h"
+#include "display/cairo-utils.h"
+#include "include/gtkmm_version.h"
+#include "io/resource.h"
+#include "io/sys.h"
+#include "object/sp-defs.h"
+#include "object/sp-root.h"
+#include "object/sp-symbol.h"
+#include "object/sp-use.h"
+#include "ui/cache/svg_preview_cache.h"
+#include "ui/clipboard.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/widget/scrollprotected.h"
+#include "xml/href-attribute-helper.h"
+
+#ifdef WITH_LIBVISIO
+ #include <libvisio/libvisio.h>
+ #include <librevenge-stream/librevenge-stream.h>
+
+ using librevenge::RVNGFileStream;
+ using librevenge::RVNGString;
+ using librevenge::RVNGStringVector;
+ using librevenge::RVNGPropertyList;
+ using librevenge::RVNGSVGDrawingGenerator;
+#endif
+
+
+namespace Inkscape {
+namespace UI {
+
+namespace Dialog {
+
+constexpr int SIZES = 51;
+int SYMBOL_ICON_SIZES[SIZES];
+
+struct SymbolSet {
+ std::vector<SPSymbol*> symbols;
+ SPDocument* document = nullptr;
+ Glib::ustring title;
+};
+
+SPDocument* load_symbol_set(std::string filename);
+void scan_all_symbol_sets(std::map<std::string, SymbolSet>& symbol_sets);
+
+// key: symbol set full file name
+// value: symbol set
+static std::map<std::string, SymbolSet> symbol_sets;
+
+struct SymbolColumns : public Gtk::TreeModel::ColumnRecord {
+ Gtk::TreeModelColumn<std::string> cache_key;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_id;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_title;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_short_title;
+ Gtk::TreeModelColumn<Glib::ustring> symbol_search_title;
+ Gtk::TreeModelColumn<Cairo::RefPtr<Cairo::Surface>> symbol_image;
+ Gtk::TreeModelColumn<Geom::Point> doc_dimensions;
+ Gtk::TreeModelColumn<SPDocument*> symbol_document;
+
+ SymbolColumns() {
+ add(cache_key);
+ add(symbol_id);
+ add(symbol_title);
+ add(symbol_short_title);
+ add(symbol_search_title);
+ add(symbol_image);
+ add(doc_dimensions);
+ add(symbol_document);
+ }
+} const g_columns;
+
+static Cairo::RefPtr<Cairo::ImageSurface> g_dummy;
+
+struct SymbolSetsColumns : public Gtk::TreeModel::ColumnRecord {
+ Gtk::TreeModelColumn<Glib::ustring> set_id;
+ Gtk::TreeModelColumn<Glib::ustring> translated_title;
+ Gtk::TreeModelColumn<std::string> set_filename;
+ Gtk::TreeModelColumn<SPDocument*> set_document;
+ Gtk::TreeModelColumn<Cairo::RefPtr<Cairo::Surface>> set_image;
+
+ SymbolSetsColumns() {
+ add(set_id);
+ add(translated_title);
+ add(set_filename);
+ add(set_document);
+ add(set_image);
+ }
+} const g_set_columns;
+
+const Glib::ustring CURRENT_DOC_ID = "{?cur-doc?}";
+const Glib::ustring ALL_SETS_ID = "{?all-sets?}";
+const char *CURRENT_DOC = N_("Current document");
+const char *ALL_SETS = N_("All symbol sets");
+
+SymbolsDialog::SymbolsDialog(const char* prefsPath)
+ : DialogBase(prefsPath, "Symbols"),
+ _builder(create_builder("dialog-symbols.glade")),
+ _zoom(get_widget<Gtk::Scale>(_builder, "zoom")),
+ _symbols_popup(get_widget<Gtk::MenuButton>(_builder, "symbol-set-popup")),
+ _set_search(get_widget<Gtk::SearchEntry>(_builder, "set-search")),
+ _search(get_widget<Gtk::SearchEntry>(_builder, "search")),
+ _symbol_sets_view(get_widget<Gtk::IconView>(_builder, "symbol-sets")),
+ _cur_set_name(get_widget<Gtk::Label>(_builder, "cur-set")),
+ _store(Gtk::ListStore::create(g_columns)),
+ _image_cache(1000) // arbitrary limit for how many rendered symbols to keep around
+{
+ auto prefs = Inkscape::Preferences::get();
+ Glib::ustring path = prefsPath;
+ path += '/';
+
+ _symbols._filtered = Gtk::TreeModelFilter::create(_store);
+ _symbols._store = _store;
+
+ _symbol_sets = Gtk::ListStore::create(g_set_columns);
+ _sets._store = _symbol_sets;
+ _sets._filtered = Gtk::TreeModelFilter::create(_symbol_sets);
+ _sets._filtered->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ if (_set_search.get_text_length() == 0) return true;
+
+ Glib::ustring id = (*it)[g_set_columns.set_id];
+ if (id == CURRENT_DOC_ID || id == ALL_SETS_ID) return true;
+
+ auto text = _set_search.get_text().lowercase();
+ Glib::ustring title = (*it)[g_set_columns.translated_title];
+ return title.lowercase().find(text) != Glib::ustring::npos;
+ });
+ _sets._sorted = Gtk::TreeModelSort::create(_sets._filtered);
+ _sets._sorted->set_sort_func(g_set_columns.translated_title, [=](const Gtk::TreeModel::iterator& a, const Gtk::TreeModel::iterator& b){
+ Glib::ustring ida = (*a)[g_set_columns.set_id];
+ Glib::ustring idb = (*b)[g_set_columns.set_id];
+ // current doc and all docs up front
+ if (ida == idb) return 0;
+ if (ida == CURRENT_DOC_ID) return -1;
+ if (idb == CURRENT_DOC_ID) return 1;
+ if (ida == ALL_SETS_ID) return -1;
+ if (idb == ALL_SETS_ID) return 1;
+ Glib::ustring ttl_a = (*a)[g_set_columns.translated_title];
+ Glib::ustring ttl_b = (*b)[g_set_columns.translated_title];
+ return ttl_a.compare(ttl_b);
+ });
+ _symbol_sets_view.set_model(_sets._sorted);
+ _symbol_sets_view.set_text_column(g_set_columns.translated_title.index());
+ _symbol_sets_view.pack_start(_renderer2);
+ _symbol_sets_view.add_attribute(_renderer2, "surface", g_set_columns.set_image);
+
+ auto row = _symbol_sets->append();
+ (*row)[g_set_columns.set_id] = CURRENT_DOC_ID;
+ (*row)[g_set_columns.translated_title] = _(CURRENT_DOC);
+ row = _symbol_sets->append();
+ (*row)[g_set_columns.set_id] = ALL_SETS_ID;
+ (*row)[g_set_columns.translated_title] = _(ALL_SETS);
+
+ _set_search.signal_search_changed().connect([=](){
+ auto scoped(_update.block());
+ _sets.refilter();
+ });
+
+ auto select_set = [=](const Gtk::TreeModel::Path& set_path) {
+ if (!set_path.empty()) {
+ // drive selection
+ _symbol_sets_view.select_path(set_path);
+ }
+ else if (auto set = get_current_set()) {
+ // populate icon view
+ rebuild(*set);
+ _cur_set_name.set_text((**set)[g_set_columns.translated_title]);
+ update_tool_buttons();
+ Glib::ustring id = (**set)[g_set_columns.set_id];
+ prefs->setString(path + "current-set", id);
+ return true;
+ }
+ return false;
+ };
+
+// _symbol_sets_view.signal_item_activated().connect([=](const Gtk::TreeModel::Path& path){
+// select_set(path);
+// get_widget<Gtk::Popover>(_builder, "set-popover").hide();
+// });
+ _symbol_sets_view.signal_selection_changed().connect([=](){
+ if (select_set({})) {
+ get_widget<Gtk::Popover>(_builder, "set-popover").hide();
+ }
+ });
+
+ const double factor = std::pow(2.0, 1.0 / 12.0);
+ for (int i = 0; i < SIZES; ++i) {
+ SYMBOL_ICON_SIZES[i] = std::round(std::pow(factor, i) * 16);
+ }
+
+ preview_document = symbolsPreviewDoc(); /* Template to render symbols in */
+ key = SPItem::display_key_new(1);
+ renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY));
+
+ auto& main = get_widget<Gtk::Box>(_builder, "main-box");
+ pack_start(main, Gtk::PACK_EXPAND_WIDGET);
+
+ _builder->get_widget("tools", tools);
+
+ icon_view = &get_widget<Gtk::IconView>(_builder, "icon-view");
+ _symbols._filtered->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){
+ if (_search.get_text_length() == 0) return true;
+
+ auto text = _search.get_text().lowercase();
+ Glib::ustring title = (*it)[g_columns.symbol_search_title];
+ return title.lowercase().find(text) != Glib::ustring::npos;
+ });
+ icon_view->set_model(_symbols._filtered);
+ icon_view->set_tooltip_column(g_columns.symbol_title.index());
+
+ _search.signal_search_changed().connect([=](){
+ int delay = _search.get_text_length() == 0 ? 0 : 300;
+ _idle_search = Glib::signal_timeout().connect([=](){
+ auto scoped(_update.block());
+ _symbols.refilter();
+ set_info();
+ return false; // disconnect
+ }, delay);
+ });
+
+ auto show_names = &get_widget<Gtk::CheckButton>(_builder, "show-names");
+ auto names = prefs->getBool(path + "show-names", true);
+ show_names->set_active(names);
+ if (names) {
+ icon_view->set_markup_column(g_columns.symbol_short_title);
+ }
+ show_names->signal_toggled().connect([=](){
+ bool show = show_names->get_active();
+ icon_view->set_markup_column(show ? g_columns.symbol_short_title.index() : -1);
+ prefs->setBool(path + "show-names", show);
+ });
+
+ std::vector<Gtk::TargetEntry> targets;
+ targets.emplace_back("application/x-inkscape-paste");
+ icon_view->enable_model_drag_source(targets, Gdk::BUTTON1_MASK, Gdk::ACTION_COPY);
+ gtk_connections.emplace_back(
+ icon_view->signal_drag_data_get().connect(sigc::mem_fun(*this, &SymbolsDialog::iconDragDataGet)));
+ gtk_connections.emplace_back(
+ icon_view->signal_selection_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::iconChanged)));
+ gtk_connections.emplace_back(icon_view->signal_button_press_event().connect([=](GdkEventButton *ev) -> bool {
+ _last_mousedown = {ev->x, ev->y - icon_view->get_vadjustment()->get_value()};
+ return false;
+ }, false));
+
+ _builder->get_widget("scroller", scroller);
+
+ // here we fix scoller to allow pass the scroll to parent scroll when reach upper or lower limit
+ // this must be added to al scrolleing window in dialogs. We dont do auto because dialogs can be recreated
+ // in the dialog code so think is safer call inside
+ fix_inner_scroll(scroller);
+
+ _builder->get_widget("overlay", overlay);
+
+ /*************************Overlays******************************/
+ // No results
+ overlay_icon = sp_get_icon_image("searching", Gtk::ICON_SIZE_DIALOG);
+ overlay_icon->set_pixel_size(40);
+ overlay_icon->set_halign(Gtk::ALIGN_CENTER);
+ overlay_icon->set_valign(Gtk::ALIGN_START);
+ overlay_icon->set_margin_top(90);
+ overlay_icon->set_no_show_all(true);
+
+ overlay_title = new Gtk::Label();
+ overlay_title->set_halign(Gtk::ALIGN_CENTER );
+ overlay_title->set_valign(Gtk::ALIGN_START );
+ overlay_title->set_justify(Gtk::JUSTIFY_CENTER);
+ overlay_title->set_margin_top(135);
+ overlay_title->set_no_show_all(true);
+
+ overlay_desc = new Gtk::Label();
+ overlay_desc->set_halign(Gtk::ALIGN_CENTER);
+ overlay_desc->set_valign(Gtk::ALIGN_START);
+ overlay_desc->set_margin_top(160);
+ overlay_desc->set_justify(Gtk::JUSTIFY_CENTER);
+ overlay_desc->set_no_show_all(true);
+
+ overlay->add_overlay(*overlay_icon);
+ overlay->add_overlay(*overlay_title);
+ overlay->add_overlay(*overlay_desc);
+
+ previous_height = 0;
+ previous_width = 0;
+
+ /******************** Tools *******************************/
+
+ _builder->get_widget("add-symbol", add_symbol);
+ add_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::insertSymbol));
+
+ _builder->get_widget("remove-symbol", remove_symbol);
+ remove_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::revertSymbol));
+
+ // Pack size (controls display area)
+ pack_size = prefs->getIntLimited(path + "tile-size", 12, 0, SIZES);
+
+ auto scale = &get_widget<Gtk::Scale>(_builder, "symbol-size");
+ scale->set_value(pack_size);
+ scale->signal_value_changed().connect([=](){
+ pack_size = scale->get_value();
+ assert(pack_size >= 0 && pack_size < SIZES);
+ _image_cache.clear();
+ rebuild();
+ prefs->setInt(path + "tile-size", pack_size);
+ });
+
+ scale_factor = prefs->getIntLimited(path + "scale-factor", 0, -10, +10);
+
+ _zoom.set_value(scale_factor);
+ _zoom.signal_value_changed().connect([=](){
+ scale_factor = _zoom.get_value();
+ rebuild();
+ prefs->setInt(path + "scale-factor", scale_factor);
+ });
+
+ icon_view->set_columns(-1);
+ icon_view->pack_start(_renderer);
+ icon_view->add_attribute(_renderer, "surface", g_columns.symbol_image);
+ icon_view->set_cell_data_func(_renderer, [=](const Gtk::TreeModel::const_iterator& it){
+ Gdk::Rectangle rect;
+ Gtk::TreeModel::Path path(it);
+ if (icon_view->get_cell_rect(path, rect)) {
+ auto height = icon_view->get_allocated_height();
+ bool visible = !(rect.get_x() < 0 && rect.get_y() < 0);
+ // cell rect coordinates are not affected by scrolling
+ if (visible && (rect.get_y() + rect.get_height() < 0 || rect.get_y() > 0 + height)) {
+ visible = false;
+ }
+ get_cell_data_func(&_renderer, *it, visible);
+ }
+ });
+
+ // Toggle scale to fit on/off
+ _builder->get_widget("zoom-to-fit", fit_symbol);
+ auto fit = prefs->getBool(path + "zoom-to-fit", true);
+ fit_symbol->set_active(fit);
+ fit_symbol->signal_clicked().connect([=](){
+ rebuild();
+ prefs->setBool(path + "zoom-to-fit", fit_symbol->get_active());
+ });
+
+ scan_all_symbol_sets(symbol_sets);
+
+ for (auto&& it : symbol_sets) {
+ auto row = _symbol_sets->append();
+ auto& set = it.second;
+ (*row)[g_set_columns.set_id] = it.first;
+ (*row)[g_set_columns.translated_title] = g_dpgettext2(nullptr, "Symbol", set.title.c_str());
+ (*row)[g_set_columns.set_document] = set.document;
+ (*row)[g_set_columns.set_filename] = it.first;
+ }
+
+ // last selected set
+ auto current = prefs->getString(path + "current-set", CURRENT_DOC_ID);
+
+ // by default select current doc (first on the list) in case nothing else gets selected
+ select_set(Gtk::TreeModel::Path("0"));
+
+ sensitive = true;
+
+ // restore set selection; check if it is still available first
+ _sets._sorted->foreach_path([&](const Gtk::TreeModel::Path& path){
+ auto it = _sets.path_to_child_iter(path);
+ if (current == (*it)[g_set_columns.set_id]) {
+ select_set(path);
+ return true;
+ }
+ return false;
+ });
+}
+
+void SymbolsDialog::on_unrealize() {
+ for (auto &connection : gtk_connections) {
+ connection.disconnect();
+ }
+ gtk_connections.clear();
+ DialogBase::on_unrealize();
+}
+
+SymbolsDialog::~SymbolsDialog()
+{
+ Inkscape::GC::release(preview_document);
+ assert(preview_document->_anchored_refcount() == 0);
+ delete preview_document;
+}
+
+void collect_symbols(SPObject* object, std::vector<SPSymbol*>& symbols) {
+ if (!object) return;
+
+ if (auto symbol = cast<SPSymbol>(object)) {
+ symbols.push_back(symbol);
+ }
+
+ if (is<SPUse>(object)) return;
+
+ for (auto& child : object->children) {
+ collect_symbols(&child, symbols);
+ }
+}
+
+void SymbolsDialog::load_all_symbols() {
+ _sets._store->foreach_iter([=](const Gtk::TreeModel::iterator& it){
+ if (!(*it)[g_set_columns.set_document]) {
+ std::string path = (*it)[g_set_columns.set_filename];
+ if (!path.empty()) {
+ auto doc = load_symbol_set(path);
+ (*it)[g_set_columns.set_document] = doc;
+ }
+ }
+ return false;
+ });
+}
+
+std::map<std::string, SymbolSet> get_all_symbols(Glib::RefPtr<Gtk::ListStore>& store) {
+ std::map<std::string, SymbolSet> map;
+
+ store->foreach_iter([&](const Gtk::TreeModel::iterator& it){
+ if (SPDocument* doc = (*it)[g_set_columns.set_document]) {
+ SymbolSet vect;
+ collect_symbols(doc->getRoot(), vect.symbols);
+ vect.title = (*it)[g_set_columns.translated_title];
+ vect.document = doc;
+ Glib::ustring id = (*it)[g_set_columns.set_id];
+ map[id.raw()] = vect;
+ }
+ return false;
+ });
+
+ return map;
+}
+
+void SymbolsDialog::rebuild(Gtk::TreeIter current) {
+ if (!sensitive || !current) {
+ return;
+ }
+
+ auto pending = _update.block();
+
+ // remove model first, or else IconView will update N times as N rows get deleted...
+ icon_view->unset_model();
+
+ _symbols._store->clear();
+
+ auto it = current;
+
+ std::map<std::string, SymbolSet> symbols;
+
+ SPDocument* document = (*it)[g_set_columns.set_document];
+ Glib::ustring set_id = (*it)[g_set_columns.set_id];
+
+ if (!document) {
+ if (set_id == CURRENT_DOC_ID) {
+ document = getDocument();
+ }
+ else if (set_id == ALL_SETS_ID) {
+ // load symbol sets, if not yet open
+ load_all_symbols();
+ // get symbols from all symbol sets (apart from current document)
+ symbols = get_all_symbols(_sets._store);
+ }
+ else {
+ std::string path = (*it)[g_set_columns.set_filename];
+ // load symbol set
+ document = load_symbol_set(path);
+ (*it)[g_set_columns.set_document] = document;
+ }
+ }
+
+ if (document) {
+ auto& vect = symbols[set_id.raw()];
+ collect_symbols(document->getRoot(), vect.symbols);
+ vect.document = set_id == CURRENT_DOC_ID ? nullptr : document;
+ vect.title = (*it)[g_set_columns.translated_title];
+ }
+
+ size_t n = 0;
+ for (auto&& it : symbols) {
+ auto& set = it.second;
+ for (auto symbol : set.symbols) {
+ addSymbol(symbol, set.title, set.document);
+ }
+ n += set.symbols.size();
+ }
+
+ for (auto r : icon_view->get_cells()) {
+ if (auto t = dynamic_cast<Gtk::CellRendererText*>(r)) {
+ // sizable boost in layout speed at the cost of showing only part of the title...
+ if (n > 1000) {
+ t->set_fixed_height_from_font(1);
+ t->property_ellipsize() = Pango::EllipsizeMode::ELLIPSIZE_END;
+ }
+ else {
+ t->set_fixed_height_from_font(-1);
+ t->property_ellipsize() = Pango::EllipsizeMode::ELLIPSIZE_NONE;
+ // t->property_wrap_mode() = Pango::WrapMode::WRAP_CHAR;
+ }
+ }
+ }
+
+ // reattach the model, have IconView content rebuilt
+ icon_view->set_model(_symbols._filtered);
+
+// layout speed test:
+// Gtk::Allocation alloc;
+// alloc.set_width(200);
+// alloc.set_height(500);
+// alloc.set_x(0);
+// alloc.set_y(0);
+// auto old_time = std::chrono::high_resolution_clock::now();
+// icon_view->size_allocate(alloc);
+// auto current_time = std::chrono::high_resolution_clock::now();
+// auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - old_time);
+// g_warning("size time: %d ms", static_cast<int>(elapsed.count()));
+
+ set_info();
+}
+
+void SymbolsDialog::rebuild() {
+ if (auto set = get_current_set()) {
+ rebuild(*set);
+ }
+}
+
+void SymbolsDialog::showOverlay() {
+ auto search = _search.get_text_length() > 0;
+ auto visible = visible_symbols();
+ auto current = get_current_set_id() == CURRENT_DOC_ID;
+
+ auto small = [](const char* str){
+ return Glib::ustring::compose("<small>%1</small>", Glib::Markup::escape_text(str));
+ };
+ auto large = [](const char* str){
+ return Glib::ustring::compose("<span size='large'>%1</span>", Glib::Markup::escape_text(str));
+ };
+
+ if (!visible && search) {
+ overlay_title->set_markup(large(_("No symbols found.")));
+ overlay_desc->set_markup(small(_("Try a different search term,\nor switch to a different symbol set.")));
+ } else if (!visible && current) {
+ overlay_title->set_markup(large(_("No symbols found.")));
+ overlay_desc->set_markup(small(_("No symbols in current document.\nChoose a different symbol set\nor add a new symbol.")));
+ }
+
+ /*
+ if (current == ALLDOCS && !_l.size())
+ {
+ overlay_icon->hide();
+ if (!all_docs_processed) {
+ overlay_icon->show();
+ overlay_title->set_markup(
+ Glib::ustring("<span size=\"large\">") + _("Search in all symbol sets...") + "</span>");
+ overlay_desc->set_markup(
+ Glib::ustring("<span size=\"small\">") + _("The first search can be slow.") + "</span>");
+ } else if (!icons_found && !search_str.empty()) {
+ overlay_title->set_markup(
+ Glib::ustring("<span size=\"large\">") + _("No symbols found.") + "</span>");
+ overlay_desc->set_markup(
+ Glib::ustring("<span size=\"small\">") + _("Try a different search term.") + "</span>");
+ } else {
+ overlay_icon->show();
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") +
+ Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + Glib::ustring("</span>"));
+ }
+ } else if (!number_symbols && (current != CURRENTDOC || !search_str.empty())) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found.")) +
+ Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) +
+ Glib::ustring("</span>"));
+ } else if (!number_symbols && current == CURRENTDOC) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found.")) +
+ Glib::ustring("</span>"));
+ overlay_desc->set_markup(
+ Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("No symbols in current document.\nChoose a different symbol set\nor add a new symbol.")) +
+ Glib::ustring("</span>"));
+ } else if (!icons_found && !search_str.empty()) {
+ overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found.")) +
+ Glib::ustring("</span>"));
+ overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") +
+ Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) +
+ Glib::ustring("</span>"));
+ }
+ */
+ gint width = scroller->get_allocated_width();
+ gint height = scroller->get_allocated_height();
+ if (previous_height != height || previous_width != width) {
+ previous_height = height;
+ previous_width = width;
+ }
+ overlay_icon->show();
+ overlay_title->show();
+ overlay_desc->show();
+}
+
+void SymbolsDialog::hideOverlay() {
+ // overlay_opacity->hide();
+ overlay_icon->hide();
+ overlay_title->hide();
+ overlay_desc->hide();
+}
+
+void SymbolsDialog::insertSymbol() {
+ getDesktop()->getSelection()->toSymbol();
+}
+
+void SymbolsDialog::revertSymbol() {
+ if (auto document = getDocument()) {
+ if (auto symbol = cast<SPSymbol>(document->getObjectById(getSymbolId(get_selected_symbol())))) {
+ symbol->unSymbol();
+ }
+ Inkscape::DocumentUndo::done(document, _("Group from symbol"), "");
+ }
+}
+
+void SymbolsDialog::iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& /*context*/, Gtk::SelectionData& data, guint /*info*/, guint /*time*/)
+{
+ auto selected = get_selected_symbol();
+ if (!selected) {
+ return;
+ }
+ Glib::ustring symbol_id = (**selected)[g_columns.symbol_id];
+ GdkAtom dataAtom = gdk_atom_intern("application/x-inkscape-paste", false);
+ gtk_selection_data_set(data.gobj(), dataAtom, 9, (guchar*)symbol_id.c_str(), symbol_id.length());
+}
+
+void SymbolsDialog::selectionChanged(Inkscape::Selection *selection) {
+ // what are we trying to do here? this code doesn't seem to accomplish anything in v1.2
+/*
+ auto selected = getSelected();
+ Glib::ustring symbol_id = getSymbolId(selected);
+ Glib::ustring doc_title = get_active_base_text(getSymbolDocTitle(selected));
+ if (!doc_title.empty()) {
+ SPDocument* symbol_document = symbol_sets[doc_title].second;
+ if (!symbol_document) {
+ //we are in global search so get the original symbol document by title
+ symbol_document = selectedSymbols();
+ }
+ if (symbol_document) {
+ SPObject* symbol = symbol_document->getObjectById(symbol_id);
+ if(symbol && !selection->includes(symbol)) {
+ icon_view->unselect_all();
+ }
+ }
+ }
+ */
+}
+
+void SymbolsDialog::refresh_on_idle(int delay) {
+ // if symbols from current document are presented...
+ if (get_current_set_id() == CURRENT_DOC_ID) {
+ // refresh them on idle; delay here helps to coalesce consecutive requests into one
+ _idle_refresh = Glib::signal_timeout().connect([=](){
+ rebuild(*get_current_set());
+ return false; // disconnect
+ }, delay, Glib::PRIORITY_DEFAULT_IDLE);
+ }
+}
+
+void SymbolsDialog::documentReplaced()
+{
+ _defs_modified.disconnect();
+ _doc_resource_changed.disconnect();
+
+ if (auto document = getDocument()) {
+ _defs_modified = document->getDefs()->connectModified([=](SPObject* ob, guint flags) {
+ refresh_on_idle();
+ });
+ _doc_resource_changed = document->connectResourcesChanged("symbol", [this](){
+ refresh_on_idle();
+ });
+ }
+
+ // if symbol set is from current document, need to rebuild
+ refresh_on_idle(0);
+ update_tool_buttons();
+}
+
+void SymbolsDialog::update_tool_buttons() {
+ if (get_current_set_id() == CURRENT_DOC_ID) {
+ add_symbol->set_sensitive();
+ remove_symbol->set_sensitive();
+ }
+ else {
+ add_symbol->set_sensitive(false);
+ remove_symbol->set_sensitive(false);
+ }
+}
+
+Glib::ustring SymbolsDialog::get_current_set_id() const {
+ auto cur = get_current_set();
+ if (cur.has_value()) {
+ return (*cur.value())[g_set_columns.set_id];
+ }
+ return {};
+}
+
+std::optional<Gtk::TreeIter> SymbolsDialog::get_current_set() const {
+ auto selected = _symbol_sets_view.get_selected_items();
+ if (selected.empty()) {
+ return std::nullopt;
+ }
+ return _sets.path_to_child_iter(selected.front());
+}
+
+SPDocument* SymbolsDialog::get_symbol_document(const std::optional<Gtk::TreeIter>& it) const {
+ if (!it) {
+ return nullptr;
+ }
+ SPDocument* doc = (**it)[g_columns.symbol_document];
+
+ return doc;
+}
+
+/** Return the path to the selected symbol, or an empty optional if nothing is selected. */
+std::optional<Gtk::TreeModel::Path> SymbolsDialog::get_selected_symbol_path() const {
+ auto selected = icon_view->get_selected_items();
+ if (selected.empty()) {
+ return std::nullopt;
+ }
+ return selected.front();
+}
+
+std::optional<Gtk::TreeIter> SymbolsDialog::get_selected_symbol() const {
+ auto selected = get_selected_symbol_path();
+ if (!selected) {
+ return std::nullopt;
+ }
+ return _symbols.path_to_child_iter(*selected);
+}
+
+/** Return the dimensions of the symbol at the given path, in document units. */
+Geom::Point SymbolsDialog::getSymbolDimensions(const std::optional<Gtk::TreeIter>& it) const
+{
+ if (!it) {
+ return Geom::Point();
+ }
+ return (**it)[g_columns.doc_dimensions];
+}
+
+/** Return the ID of the symbol at the given path, with empty string fallback. */
+Glib::ustring SymbolsDialog::getSymbolId(const std::optional<Gtk::TreeIter>& it) const
+{
+ if (!it) {
+ return "";
+ }
+ return (**it)[g_columns.symbol_id];
+}
+
+/** Store the symbol in the clipboard for further manipulation/insertion into document.
+ *
+ * @param symbol_path The path to the symbol in the tree model.
+ * @param bbox The bounding box to set on the clipboard document's clipnode.
+ */
+void SymbolsDialog::sendToClipboard(const Gtk::TreeIter& symbol_iter, Geom::Rect const &bbox)
+{
+ auto symbol_id = getSymbolId(symbol_iter);
+ if (symbol_id.empty()) return;
+
+ auto symbol_document = get_symbol_document(symbol_iter);
+ if (!symbol_document) {
+ //we are in global search so get the original symbol document by title
+ symbol_document = getDocument();
+ }
+ if (!symbol_document) {
+ return;
+ }
+ if (SPObject* symbol = symbol_document->getObjectById(symbol_id)) {
+ // Find style for use in <use>
+ // First look for default style stored in <symbol>
+ gchar const* style = symbol->getAttribute("inkscape:symbol-style");
+ if (!style) {
+ // If no default style in <symbol>, look in documents.
+ if (symbol_document == getDocument()) {
+ style = styleFromUse(symbol_id.c_str(), symbol_document);
+ } else {
+ style = symbol_document->getReprRoot()->attribute("style");
+ }
+ }
+ ClipboardManager::get()->copySymbol(symbol->getRepr(), style, symbol_document, bbox);
+ }
+}
+
+void SymbolsDialog::iconChanged()
+{
+ if (_update.pending()) return;
+
+ if (auto selected = get_selected_symbol()) {
+ auto const dims = getSymbolDimensions(selected);
+ sendToClipboard(*selected, Geom::Rect(-0.5 * dims, 0.5 * dims));
+ }
+}
+
+#ifdef WITH_LIBVISIO
+
+// Extend libvisio's native RVNGSVGDrawingGenerator with support for extracting stencil names (to be used as ID/title)
+class REVENGE_API RVNGSVGDrawingGenerator_WithTitle : public RVNGSVGDrawingGenerator {
+ public:
+ RVNGSVGDrawingGenerator_WithTitle(RVNGStringVector &output, RVNGStringVector &titles, const RVNGString &nmSpace)
+ : RVNGSVGDrawingGenerator(output, nmSpace)
+ , _titles(titles)
+ {}
+
+ void startPage(const RVNGPropertyList &propList) override
+ {
+ RVNGSVGDrawingGenerator::startPage(propList);
+ if (propList["draw:name"]) {
+ _titles.append(propList["draw:name"]->getStr());
+ } else {
+ _titles.append("");
+ }
+ }
+
+ private:
+ RVNGStringVector &_titles;
+};
+
+// Read Visio stencil files
+SPDocument* read_vss(std::string filename, std::string name) {
+ gchar *fullname;
+ #ifdef _WIN32
+ // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows
+ // therefore attempt to convert uri to the system codepage
+ // even if this is not possible the alternate short (8.3) file name will be used if available
+ fullname = g_win32_locale_filename_from_utf8(filename.c_str());
+ #else
+ fullname = strdup(filename.c_str());
+ #endif
+
+ RVNGFileStream input(fullname);
+ g_free(fullname);
+
+ if (!libvisio::VisioDocument::isSupported(&input)) {
+ return nullptr;
+ }
+ RVNGStringVector output;
+ RVNGStringVector titles;
+ RVNGSVGDrawingGenerator_WithTitle generator(output, titles, "svg");
+
+ if (!libvisio::VisioDocument::parseStencils(&input, &generator)) {
+ return nullptr;
+ }
+ if (output.empty()) {
+ return nullptr;
+ }
+
+ // prepare a valid title for the symbol file
+ Glib::ustring title = Glib::Markup::escape_text(name);
+ // prepare a valid id prefix for symbols libvisio doesn't give us a name for
+ Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create("[^a-zA-Z0-9_-]");
+ Glib::ustring id = regex1->replace(name, 0, "_", Glib::REGEX_MATCH_PARTIAL);
+
+ Glib::ustring tmpSVGOutput;
+ tmpSVGOutput += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n";
+ tmpSVGOutput += "<svg\n";
+ tmpSVGOutput += " xmlns=\"http://www.w3.org/2000/svg\"\n";
+ tmpSVGOutput += " xmlns:svg=\"http://www.w3.org/2000/svg\"\n";
+ tmpSVGOutput += " xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n";
+ tmpSVGOutput += " version=\"1.1\"\n";
+ tmpSVGOutput += " style=\"fill:none;stroke:#000000;stroke-width:2\">\n";
+ tmpSVGOutput += " <title>";
+ tmpSVGOutput += title;
+ tmpSVGOutput += "</title>\n";
+ tmpSVGOutput += " <defs>\n";
+
+ // Each "symbol" is in its own SVG file, we wrap with <symbol> and merge into one file.
+ for (unsigned i=0; i<output.size(); ++i) {
+ std::stringstream ss;
+ if (titles.size() == output.size() && titles[i] != "") {
+ // TODO: Do we need to check for duplicated titles?
+ ss << regex1->replace(titles[i].cstr(), 0, "_", Glib::REGEX_MATCH_PARTIAL);
+ } else {
+ ss << id << "_" << i;
+ }
+
+ tmpSVGOutput += "<symbol id=\"" + ss.str() + "\">\n";
+
+ if (titles.size() == output.size() && titles[i] != "") {
+ tmpSVGOutput += "<title>" + Glib::ustring(RVNGString::escapeXML(titles[i].cstr()).cstr()) + "</title>\n";
+ }
+
+ std::istringstream iss( output[i].cstr() );
+ std::string line;
+ while( std::getline( iss, line ) ) {
+ if( line.find( "svg:svg" ) == std::string::npos ) {
+ tmpSVGOutput += line + "\n";
+ }
+ }
+
+ tmpSVGOutput += "</symbol>\n";
+ }
+
+ tmpSVGOutput += " </defs>\n";
+ tmpSVGOutput += "</svg>\n";
+ return SPDocument::createNewDocFromMem(tmpSVGOutput.c_str(), tmpSVGOutput.size(), false);
+}
+#endif
+
+/* Hunts preference directories for symbol files */
+void scan_all_symbol_sets(std::map<std::string, SymbolSet>& symbol_sets) {
+
+ using namespace Inkscape::IO::Resource;
+ std::regex matchtitle(".*?<title.*?>(.*?)<(/| /)");
+
+ for (auto& filename : get_filenames(SYMBOLS, {".svg", ".vss", "vssx", "vsdx"})) {
+ if (symbol_sets.count(filename)) continue;
+
+ if (Glib::str_has_suffix(filename, ".vss") || Glib::str_has_suffix(filename, ".vssx") || Glib::str_has_suffix(filename, ".vsdx")) {
+ std::size_t found = filename.find_last_of("/\\");
+ auto title = found != Glib::ustring::npos ? filename.substr(found + 1) : filename;
+ title = title.erase(title.rfind('.'));
+ if (title.empty()) {
+ title = _("Unnamed Symbols");
+ }
+ symbol_sets[filename].title = title;
+ } else {
+ std::ifstream infile(filename);
+ std::string line;
+ while (std::getline(infile, line)) {
+ std::string title_res = std::regex_replace(line, matchtitle,"$1",std::regex_constants::format_no_copy);
+ if (!title_res.empty()) {
+ title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str());
+ symbol_sets[filename].title = title_res;
+ break;
+ }
+ auto position_exit = line.find("<defs");
+ if (position_exit != std::string::npos) {
+ std::size_t found = filename.find_last_of("/\\");
+ auto title = found != std::string::npos ? filename.substr(found + 1) : filename;
+ title = title.erase(title.rfind('.'));
+ if (title.empty()) {
+ title = _("Unnamed Symbols");
+ }
+ symbol_sets[filename].title = title;
+ break;
+ }
+ }
+ }
+ }
+}
+
+// Load SVG or VSS document and create SPDocument
+SPDocument* load_symbol_set(std::string filename)
+{
+ SPDocument* symbol_doc = nullptr;
+ if (auto doc = symbol_sets[filename].document) {
+ return doc;
+ }
+
+ using namespace Inkscape::IO::Resource;
+ if (Glib::str_has_suffix(filename, ".vss") || Glib::str_has_suffix(filename, ".vssx") || Glib::str_has_suffix(filename, ".vsdx")) {
+#ifdef WITH_LIBVISIO
+ symbol_doc = read_vss(filename, symbol_sets[filename].title);
+#endif
+ } else if (Glib::str_has_suffix(filename, ".svg")) {
+ symbol_doc = SPDocument::createNewDoc(filename.c_str(), FALSE);
+ }
+
+ if (symbol_doc) {
+ symbol_sets[filename].document = symbol_doc;
+ }
+ return symbol_doc;
+}
+
+void SymbolsDialog::useInDoc (SPObject *r, std::vector<SPUse*> &l)
+{
+ if (is<SPUse>(r) ) {
+ l.push_back(cast<SPUse>(r));
+ }
+
+ for (auto& child: r->children) {
+ useInDoc( &child, l );
+ }
+}
+
+std::vector<SPUse*> SymbolsDialog::useInDoc( SPDocument* useDocument) {
+ std::vector<SPUse*> l;
+ useInDoc (useDocument->getRoot(), l);
+ return l;
+}
+
+// Returns style from first <use> element found that references id.
+// This is a last ditch effort to find a style.
+gchar const* SymbolsDialog::styleFromUse( gchar const* id, SPDocument* document) {
+
+ gchar const* style = nullptr;
+ std::vector<SPUse*> l = useInDoc( document );
+ for( auto use:l ) {
+ if ( use ) {
+ gchar const *href = Inkscape::getHrefAttribute(*use->getRepr()).second;
+ if( href ) {
+ Glib::ustring href2(href);
+ Glib::ustring id2(id);
+ id2 = "#" + id2;
+ if( !href2.compare(id2) ) {
+ style = use->getRepr()->attribute("style");
+ break;
+ }
+ }
+ }
+ }
+ return style;
+}
+
+size_t SymbolsDialog::total_symbols() const {
+ return _symbols._store->children().size();
+}
+
+size_t SymbolsDialog::visible_symbols() const {
+ return _symbols._filtered->children().size();
+}
+
+void SymbolsDialog::set_info() {
+ auto total = total_symbols();
+ auto visible = visible_symbols();
+ if (!total) {
+ set_info("");
+ }
+ else if (total == visible) {
+ set_info(Glib::ustring::compose("%1: %2", _("Symbols"), total).c_str());
+ }
+ else if (visible == 0) {
+ set_info(Glib::ustring::compose("%1: %2 / %3", _("Symbols"), _("none"), total).c_str());
+ }
+ else {
+ set_info(Glib::ustring::compose("%1: %2 / %3", _("Symbols"), visible, total).c_str());
+ }
+
+ if (total == 0 || visible == 0) {
+ showOverlay();
+ }
+ else {
+ hideOverlay();
+ }
+}
+
+void SymbolsDialog::set_info(const Glib::ustring& text) {
+ auto info = "<small>" + Glib::Markup::escape_text(text) + "</small>";
+ get_widget<Gtk::Label>(_builder, "info").set_markup(info);
+}
+
+Cairo::RefPtr<Cairo::Surface> add_background(Cairo::RefPtr<Cairo::Surface> image, uint32_t rgb, double margin, double radius, int device_scale, std::optional<uint32_t> border = std::optional<uint32_t>()) {
+ auto w = image ? cairo_image_surface_get_width(image->cobj()) : 0;
+ auto h = image ? cairo_image_surface_get_height(image->cobj()) : 0;
+ auto width = w / device_scale + 2 * margin;
+ auto height = h / device_scale + 2 * margin;
+
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, width * device_scale, height * device_scale);
+ cairo_surface_set_device_scale(surface->cobj(), device_scale, device_scale);
+ auto ctx = Cairo::Context::create(surface);
+
+ auto x = 0;
+ auto y = 0;
+ if (border.has_value()) {
+ x += 0.5 * device_scale;
+ y += 0.5 * device_scale;
+ width -= device_scale;
+ height -= device_scale;
+ }
+ ctx->arc(x + width - radius, y + radius, radius, -M_PI_2, 0);
+ ctx->arc(x + width - radius, y + height - radius, radius, 0, M_PI_2);
+ ctx->arc(x + radius, y + height - radius, radius, M_PI_2, M_PI);
+ ctx->arc(x + radius, y + radius, radius, M_PI, 3 * M_PI_2);
+ ctx->close_path();
+
+ ctx->set_source_rgb(SP_RGBA32_R_F(rgb), SP_RGBA32_G_F(rgb), SP_RGBA32_B_F(rgb));
+ if (border.has_value()) {
+ ctx->fill_preserve();
+
+ auto b = *border;
+ ctx->set_source_rgb(SP_RGBA32_R_F(b), SP_RGBA32_G_F(b), SP_RGBA32_B_F(b));
+ ctx->set_line_width(1.0);
+ ctx->stroke();
+ }
+ else {
+ ctx->fill();
+ }
+
+ if (image) {
+ ctx->set_source(image, margin, margin);
+ ctx->paint();
+ }
+
+ return surface;
+}
+
+void SymbolsDialog::addSymbol(SPSymbol* symbol, Glib::ustring doc_title, SPDocument* document)
+{
+ auto id = symbol->getRepr()->attribute("id");
+ auto title = symbol->title(); // From title element
+ Glib::ustring short_title = title ? g_dpgettext2(nullptr, "Symbol", title) : id;
+ g_free(title);
+ auto symbol_title = Glib::ustring::compose("%1 (%2)", short_title, doc_title);
+
+ Geom::Point dimensions{64, 64}; // Default to 64x64 px if size not available.
+ if (auto rect = symbol->documentVisualBounds()) {
+ dimensions = rect->dimensions();
+ }
+ auto set = symbol->document ? symbol->document->getDocumentFilename() : "null";
+ if (!set) set = "noname";
+ Gtk::ListStore::iterator row = _store->append();
+ std::ostringstream key;
+ key << set << '\n' << id;
+ (*row)[g_columns.cache_key] = key.str();
+ (*row)[g_columns.symbol_id] = Glib::ustring(id);
+ // symbol title and document name - used in a tooltip
+ (*row)[g_columns.symbol_title] = Glib::Markup::escape_text(symbol_title);
+ // symbol title shown below image
+ (*row)[g_columns.symbol_short_title] = "<small>" + Glib::Markup::escape_text(short_title) + "</small>";
+ // symbol title verbatim, used for searching/filtering
+ (*row)[g_columns.symbol_search_title] = short_title;
+ (*row)[g_columns.doc_dimensions] = dimensions;
+ (*row)[g_columns.symbol_document] = document;
+}
+
+Cairo::RefPtr<Cairo::Surface> SymbolsDialog::draw_symbol(SPSymbol* symbol) {
+ Cairo::RefPtr<Cairo::Surface> surface;
+ Cairo::RefPtr<Cairo::Surface> image;
+ int device_scale = get_scale_factor();
+
+ if (symbol) {
+ image = drawSymbol(symbol);
+ }
+ else {
+ unsigned psize = SYMBOL_ICON_SIZES[pack_size] * device_scale;
+ image = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, psize, psize);
+ cairo_surface_set_device_scale(image->cobj(), device_scale, device_scale);
+ }
+
+ // white background for typically black symbols, so they don't disappear in a dark theme
+ if (image) {
+ uint32_t background = 0xffffff00;
+ double margin = 3.0;
+ double radius = 3.0;
+ surface = add_background(image, background, margin, radius, device_scale);
+ }
+
+ return surface;
+}
+
+/*
+ * Returns image of symbol.
+ *
+ * Symbols normally are not visible. They must be referenced by a
+ * <use> element. A temporary document is created with a dummy
+ * <symbol> element and a <use> element that references the symbol
+ * element. Each real symbol is swapped in for the dummy symbol and
+ * the temporary document is rendered.
+ */
+Cairo::RefPtr<Cairo::Surface> SymbolsDialog::drawSymbol(SPSymbol *symbol)
+{
+ if (!symbol) return Cairo::RefPtr<Cairo::Surface>();
+ // Create a copy repr of the symbol with id="the_symbol"
+ Inkscape::XML::Node *repr = symbol->getRepr()->duplicate(preview_document->getReprDoc());
+ repr->setAttribute("id", "the_symbol");
+
+ // First look for default style stored in <symbol>
+ gchar const* style = repr->attribute("inkscape:symbol-style");
+ if(!style) {
+ // If no default style in <symbol>, look in documents.
+ if(symbol->document == getDocument()) {
+ gchar const *id = symbol->getRepr()->attribute("id");
+ style = styleFromUse( id, symbol->document );
+ } else {
+ style = symbol->document->getReprRoot()->attribute("style");
+ }
+ }
+
+ // This is for display in Symbols dialog only
+ if( style ) repr->setAttribute( "style", style );
+
+ SPDocument::install_reference_document scoped(preview_document, symbol->document);
+ preview_document->getDefs()->getRepr()->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ // Uncomment this to get the preview_document documents saved (useful for debugging)
+ // FILE *fp = fopen (g_strconcat(id, ".svg", NULL), "w");
+ // sp_repr_save_stream(preview_document->getReprDoc(), fp);
+ // fclose (fp);
+
+ // Make sure preview_document is up-to-date.
+ preview_document->ensureUpToDate();
+
+ // Make sure we have symbol in preview_document
+ SPObject *object_temp = preview_document->getObjectById( "the_use" );
+
+ auto item = cast<SPItem>(object_temp);
+ g_assert(item != nullptr);
+ unsigned psize = SYMBOL_ICON_SIZES[pack_size];
+
+ cairo_surface_t* surface = 0;
+ // We could use cache here, but it doesn't really work with the structure
+ // of this user interface and we've already cached the pixbuf in the gtklist
+
+ // Find object's bbox in document.
+ // Note symbols can have own viewport... ignore for now.
+ //Geom::OptRect dbox = item->geometricBounds();
+ Geom::OptRect dbox = item->documentVisualBounds();
+
+ if (dbox) {
+ /* Scale symbols to fit */
+ double scale = 1.0;
+ double width = dbox->width();
+ double height = dbox->height();
+
+ if( width == 0.0 ) width = 1.0;
+ if( height == 0.0 ) height = 1.0;
+
+ if (fit_symbol->get_active()) {
+ scale = psize / ceil(std::max(width, height));
+ }
+ else {
+ scale = pow(2.0, scale_factor / 4.0) * psize / 32.0;
+ }
+
+ int device_scale = get_scale_factor();
+
+ surface = render_surface(renderDrawing, scale, *dbox, Geom::IntPoint(psize, psize), device_scale, nullptr, true);
+
+ if (surface) {
+ cairo_surface_set_device_scale(surface, device_scale, device_scale);
+ }
+ }
+
+ preview_document->getObjectByRepr(repr)->deleteObject(false);
+
+ return Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(surface, true));
+}
+
+/*
+ * Return empty doc to render symbols in.
+ * Symbols are by default not rendered so a <use> element is
+ * provided.
+ */
+SPDocument* SymbolsDialog::symbolsPreviewDoc()
+{
+ // BUG: <symbol> must be inside <defs>
+ const char buffer[] =
+"<svg xmlns=\"http://www.w3.org/2000/svg\""
+" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\""
+" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\""
+" xmlns:xlink=\"http://www.w3.org/1999/xlink\">"
+" <use id=\"the_use\" xlink:href=\"#the_symbol\"/>"
+"</svg>";
+ return SPDocument::createNewDocFromMem(buffer, strlen(buffer), false);
+}
+
+void SymbolsDialog::get_cell_data_func(Gtk::CellRenderer* cell_renderer, Gtk::TreeModel::Row row, bool visible)
+{
+ std::string cache_key = (row)[g_columns.cache_key];
+ Glib::ustring id = (row)[g_columns.symbol_id];
+ Cairo::RefPtr<Cairo::Surface> surface;
+
+ if (!visible) {
+ // cell is not visible, so this is layout pass; return empty image of the right size
+ int device_scale = get_scale_factor();
+ unsigned psize = SYMBOL_ICON_SIZES[pack_size] * device_scale;
+ if (!g_dummy || g_dummy->get_width() != psize) {
+ g_dummy = g_dummy.cast_static(draw_symbol(nullptr));
+ }
+ surface = g_dummy;
+ }
+ else {
+ // cell is visible, so we need to return correct symbol image and render it if it's missing
+ if (auto image = _image_cache.get(cache_key)) {
+ // cache hit
+ surface = *image;
+ }
+ else {
+ // render
+ SPDocument* doc = row[g_columns.symbol_document];
+ if (!doc) doc = getDocument();
+ SPSymbol* symbol = doc ? cast<SPSymbol>(doc->getObjectById(id)) : nullptr;
+ surface = draw_symbol(symbol);
+ _image_cache.insert(cache_key, surface);
+ }
+ }
+ cell_renderer->set_property("surface", surface);
+}
+
+} //namespace Dialogs
+} //namespace UI
+} //namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-basic-offset:2
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/dialog/symbols.h b/src/ui/dialog/symbols.h
new file mode 100644
index 0000000..92978de
--- /dev/null
+++ b/src/ui/dialog/symbols.h
@@ -0,0 +1,192 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Symbols dialog
+ */
+/* Authors:
+ * Tavmjong Bah, Martin Owens
+ *
+ * Copyright (C) 2012 Tavmjong Bah
+ * 2013 Martin Owens
+ * 2017 Jabiertxo Arraiza
+ * 2023 Mike Kowalski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_SYMBOLS_H
+#define INKSCAPE_UI_DIALOG_SYMBOLS_H
+
+#include <cstddef>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+#include <gtkmm.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/iconview.h>
+#include <gtkmm/label.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/treeiter.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treemodelcolumn.h>
+#include <sigc++/connection.h>
+#include <string>
+#include <vector>
+#include <boost/compute/detail/lru_cache.hpp>
+
+#include "desktop.h"
+#include "display/drawing.h"
+#include "document.h"
+#include "helper/auto-connection.h"
+#include "selection.h"
+#include "ui/dialog/dialog-base.h"
+#include "ui/operation-blocker.h"
+
+class SPObject;
+class SPSymbol;
+class SPUse;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/**
+ * A dialog that displays selectable symbols and allows users to drag or paste
+ * those symbols from the dialog into the document.
+ *
+ * Symbol documents are loaded from the preferences paths and displayed in a
+ * drop-down list to the user. The user then selects which of the symbols
+ * documents they want to get symbols from. The first document in the list is
+ * always the current document.
+ *
+ * This then updates an icon-view with all the symbols available. Selecting one
+ * puts it onto the clipboard. Dragging it or pasting it onto the canvas copies
+ * the symbol from the symbol document, into the current document and places a
+ * new <use> element at the correct location on the canvas.
+ *
+ * Selected groups on the canvas can be added to the current document's symbols
+ * table, and symbols can be removed from the current document. This allows
+ * new symbols documents to be constructed and if saved in the prefs folder will
+ * make those symbols available for all future documents.
+ */
+
+
+class SymbolsDialog : public DialogBase
+{
+public:
+ SymbolsDialog(char const *prefsPath = "/dialogs/symbols");
+ ~SymbolsDialog() override;
+
+private:
+ void documentReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void on_unrealize() override;
+ void rebuild();
+ void rebuild(Gtk::TreeIter current);
+ void insertSymbol();
+ void revertSymbol();
+ void iconChanged();
+ void sendToClipboard(const Gtk::TreeIter& symbol_iter, Geom::Rect const &bbox);
+ Glib::ustring getSymbolId(const std::optional<Gtk::TreeIter>& it) const;
+ Geom::Point getSymbolDimensions(const std::optional<Gtk::TreeIter>& it) const;
+ SPDocument* get_symbol_document(const std::optional<Gtk::TreeIter>& it) const;
+ void iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& context, Gtk::SelectionData& selection_data, guint info, guint time);
+ void onDragStart();
+ void addSymbol(SPSymbol* symbol, Glib::ustring doc_title, SPDocument* document);
+ SPDocument* symbolsPreviewDoc();
+ void useInDoc(SPObject *r, std::vector<SPUse*> &l);
+ std::vector<SPUse*> useInDoc( SPDocument* document);
+ void addSymbols();
+ void showOverlay();
+ void hideOverlay();
+ gchar const* styleFromUse( gchar const* id, SPDocument* document);
+ Cairo::RefPtr<Cairo::Surface> drawSymbol(SPSymbol *symbol);
+ Cairo::RefPtr<Cairo::Surface> draw_symbol(SPSymbol* symbol);
+ Glib::RefPtr<Gdk::Pixbuf> getOverlay(gint width, gint height);
+ void set_info();
+ void set_info(const Glib::ustring& text);
+ std::optional<Gtk::TreeIter> get_current_set() const;
+ Glib::ustring get_current_set_id() const;
+ std::optional<Gtk::TreeModel::Path> get_selected_symbol_path() const;
+ std::optional<Gtk::TreeIter> get_selected_symbol() const;
+ void load_all_symbols();
+ void update_tool_buttons();
+ size_t total_symbols() const;
+ size_t visible_symbols() const;
+ void get_cell_data_func(Gtk::CellRenderer* cell_renderer, Gtk::TreeModel::Row row, bool visible);
+ void refresh_on_idle(int delay = 100);
+
+ auto_connection _idle_search;
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::Scale& _zoom;
+ // Index into sizes which is selected
+ int pack_size;
+ // Scale factor
+ int scale_factor;
+ bool sensitive = false;
+ OperationBlocker _update;
+ double previous_height;
+ double previous_width;
+ Geom::Point _last_mousedown; ///< Last button press position in the icon view coordinates.
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Gtk::MenuButton& _symbols_popup;
+ Gtk::SearchEntry& _set_search;
+ Gtk::IconView& _symbol_sets_view;
+ Gtk::Label& _cur_set_name;
+ Gtk::SearchEntry& _search;
+ Gtk::IconView* icon_view;
+ Gtk::Button* add_symbol;
+ Gtk::Button* remove_symbol;
+ Gtk::Box* tools;
+ Gtk::Overlay* overlay;
+ Gtk::Image* overlay_icon;
+ Gtk::Image* overlay_opacity;
+ Gtk::Label* overlay_title;
+ Gtk::Label* overlay_desc;
+ Gtk::ScrolledWindow *scroller;
+ Gtk::CheckButton* fit_symbol;
+ Gtk::CellRendererPixbuf _renderer;
+ Gtk::CellRendererPixbuf _renderer2;
+ SPDocument* preview_document = nullptr; /* Document to render single symbol */
+ Glib::RefPtr<Gtk::ListStore> _symbol_sets;
+ struct Store {
+ Glib::RefPtr<Gtk::ListStore> _store;
+ Glib::RefPtr<Gtk::TreeModelFilter> _filtered;
+ Glib::RefPtr<Gtk::TreeModelSort> _sorted;
+
+ Gtk::TreeIter path_to_child_iter(Gtk::TreeModel::Path path) const {
+ if (_sorted) path = _sorted->convert_path_to_child_path(path);
+ if (_filtered) path = _filtered->convert_path_to_child_path(path);
+ return _store->get_iter(path);
+ }
+ void refilter() {
+ if (_filtered) _filtered->refilter();
+ }
+ } _symbols, _sets;
+
+ /* For rendering the template drawing */
+ unsigned key;
+ Inkscape::Drawing renderDrawing;
+ std::vector<sigc::connection> gtk_connections;
+ auto_connection _defs_modified;
+ auto_connection _doc_resource_changed;
+ auto_connection _idle_refresh;
+ boost::compute::detail::lru_cache<std::string, Cairo::RefPtr<Cairo::Surface>> _image_cache;
+};
+
+} //namespace Dialogs
+} //namespace UI
+} //namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_SYMBOLS_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/dialog/text-edit.cpp b/src/ui/dialog/text-edit.cpp
new file mode 100644
index 0000000..6b4a433
--- /dev/null
+++ b/src/ui/dialog/text-edit.cpp
@@ -0,0 +1,647 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Text editing dialog.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@ximian.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * Abhishek Sharma
+ * John Smith
+ * Tavmjong Bah
+ *
+ * Copyright (C) 1999-2013 Authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h" // only include where actually required!
+#endif
+
+#include "text-edit.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/markup.h>
+
+#include <gtkmm/box.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/buttonbox.h>
+#include <gtkmm/label.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/textbuffer.h>
+#include <gtkmm/textview.h>
+
+#ifdef WITH_GSPELL
+# include <gspell/gspell.h>
+#endif
+
+#include "desktop-style.h"
+#include "desktop.h"
+#include "dialog-notebook.h"
+#include "dialog-container.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "style.h"
+#include "text-editing.h"
+
+#include <libnrtype/font-factory.h>
+#include <libnrtype/font-instance.h>
+#include <libnrtype/font-lister.h>
+
+#include "object/sp-flowtext.h"
+#include "object/sp-text.h"
+
+#include "io/resource.h"
+#include "svg/css-ostringstream.h"
+#include "ui/icon-names.h"
+#include "ui/widget/font-selector.h"
+
+#include "util/units.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+TextEdit::TextEdit()
+ : DialogBase("/dialogs/textandfont", "Text")
+ , blocked(false)
+ /*
+ TRANSLATORS: Test string used in text and font dialog (when no
+ * text has been entered) to get a preview of the font. Choose
+ * some representative characters that users of your locale will be
+ * interested in.*/
+ , samplephrase(_("AaBbCcIiPpQq12369$\342\202\254\302\242?.;/()"))
+ , _undo{"doc.undo"}
+ , _redo{"doc.redo"}
+{
+ std::string gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-text-edit.glade");
+ Glib::RefPtr<Gtk::Builder> builder;
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (const Glib::Error &ex) {
+ g_warning("GtkBuilder file loading failed for save template dialog");
+ return;
+ }
+
+ Inkscape::FontCollections *font_collections = Inkscape::FontCollections::get();
+
+ Gtk::Box *contents;
+ Gtk::Notebook *notebook;
+ Gtk::Box *font_box;
+ Gtk::Box *feat_box;
+
+ builder->get_widget("contents", contents);
+ builder->get_widget("notebook", notebook);
+ builder->get_widget("font_box", font_box);
+ builder->get_widget("feat_box", feat_box);
+ builder->get_widget("preview_label", preview_label);
+ builder->get_widget("preview_label2", preview_label2);
+ builder->get_widget("text_view", text_view);
+ builder->get_widget("setasdefault_button", setasdefault_button);
+ builder->get_widget("apply_button", apply_button);
+
+ builder->get_widget("settings_and_filters_box", settings_and_filters_box);
+ builder->get_widget("filter_menu_button", filter_menu_button);
+ builder->get_widget("reset_button", reset_button);
+ builder->get_widget("search_entry", search_entry);
+ builder->get_widget("font_count_label", font_count_label);
+ builder->get_widget("filter_popover", filter_popover);
+ builder->get_widget("popover_box", popover_box);
+ builder->get_widget("frame", frame);
+ builder->get_widget("frame_label", frame_label);
+ builder->get_widget("collection_editor_button", collection_editor_button);
+ builder->get_widget("collections_list", collections_list);
+
+ text_buffer = Glib::RefPtr<Gtk::TextBuffer>::cast_static(builder->get_object("text_buffer"));
+
+ font_box->pack_start(font_selector, true, true);
+ font_box->reorder_child(font_selector, 2);
+ feat_box->pack_start(font_features, true, true);
+ feat_box->reorder_child(font_features, 1);
+
+ // filter_popover->set_modal(false); // Stay open until button clicked again.
+ filter_popover->signal_show().connect([=](){
+ // update font collections checkboxes
+ display_font_collections();
+ }, false);
+
+ filter_menu_button->set_image_from_icon_name(INKSCAPE_ICON("font_collections"));
+ filter_menu_button->set_always_show_image(true);
+ filter_menu_button->set_label(_("Collections"));
+
+#ifdef WITH_GSPELL
+ /*
+ TODO: Use computed xml:lang attribute of relevant element, if present, to specify the
+ language (either as 2nd arg of gtkspell_new_attach, or with explicit
+ gtkspell_set_language call in; see advanced.c example in gtkspell docs).
+ onReadSelection looks like a suitable place.
+ */
+ GspellTextView *gspell_view = gspell_text_view_get_from_gtk_text_view(text_view->gobj());
+ gspell_text_view_basic_setup(gspell_view);
+#endif
+
+ add(*contents);
+
+ /* Signal handlers */
+ text_view->signal_key_press_event().connect(sigc::mem_fun(*this, &TextEdit::captureUndo));
+ text_buffer->signal_changed().connect([=](){ onChange(); });
+ setasdefault_button->signal_clicked().connect([=](){ onSetDefault(); });
+ apply_button->signal_clicked().connect([=](){ onApply(); });
+ fontChangedConn = font_selector.connectChanged(sigc::mem_fun(*this, &TextEdit::onFontChange));
+ fontFeaturesChangedConn = font_features.connectChanged([=](){ onChange(); });
+ notebook->signal_switch_page().connect(sigc::mem_fun(*this, &TextEdit::onFontFeatures));
+ search_entry->signal_search_changed().connect([=](){ on_search_entry_changed(); });
+ reset_button->signal_clicked().connect([=](){ on_reset_button_pressed(); });
+ collection_editor_button->signal_clicked().connect([=](){ on_fcm_button_clicked(); });
+ Inkscape::FontLister::get_instance()->connectUpdate(sigc::mem_fun(*this, &TextEdit::change_font_count_label));
+ fontCollectionsUpdate = font_collections->connect_update([=]() { display_font_collections(); });
+ fontCollectionsChangedSelection = font_collections->connect_selection_update([=]() { display_font_collections(); });
+
+ font_selector.set_name("TextEdit");
+ change_font_count_label();
+
+ show_all_children();
+}
+
+TextEdit::~TextEdit()
+{
+ selectModifiedConn.disconnect();
+ subselChangedConn.disconnect();
+ selectChangedConn.disconnect();
+ fontChangedConn.disconnect();
+ fontFeaturesChangedConn.disconnect();
+}
+
+bool TextEdit::captureUndo(GdkEventKey *key)
+{
+ if (_undo.isTriggeredBy(key) || _redo.isTriggeredBy(key)) {
+ /*
+ * TODO: Handle these events separately after switching to GTKMM4
+ * Fixes: https://gitlab.com/inkscape/inkscape/-/issues/744
+ */
+ return true;
+ }
+
+ return false;
+}
+
+void TextEdit::onReadSelection ( gboolean dostyle, gboolean /*docontent*/ )
+{
+ if (blocked)
+ return;
+
+ blocked = true;
+
+ SPItem *text = getSelectedTextItem ();
+
+ Glib::ustring phrase = samplephrase;
+
+ if (text)
+ {
+ guint items = getSelectedTextCount ();
+ bool has_one_item = items == 1;
+ text_view->set_sensitive(has_one_item);
+ apply_button->set_sensitive(false);
+ setasdefault_button->set_sensitive(true);
+
+ Glib::ustring str = sp_te_get_string_multiline(text);
+ if (!str.empty()) {
+ if (has_one_item) {
+ text_buffer->set_text(str);
+ text_buffer->set_modified(false);
+ }
+ phrase = str;
+
+ } else {
+ text_buffer->set_text("");
+ }
+
+ text->getRepr(); // was being called but result ignored. Check this.
+ } else {
+ text_view->set_sensitive(false);
+ apply_button->set_sensitive(false);
+ setasdefault_button->set_sensitive(false);
+ }
+
+ if (dostyle && text) {
+ auto *desktop = getDesktop();
+
+ // create temporary style
+ SPStyle query(desktop->getDocument());
+
+ // Query style from desktop into it. This returns a result flag and fills query with the
+ // style of subselection, if any, or selection
+
+ int result_numbers = sp_desktop_query_style (desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS);
+
+ // If querying returned nothing, read the style from the text tool prefs (default style for new texts).
+ if (result_numbers == QUERY_STYLE_NOTHING) {
+ query.readFromPrefs("/tools/text");
+ }
+
+ Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
+
+ // Update family/style based on selection.
+ font_lister->selection_update();
+ Glib::ustring fontspec = font_lister->get_fontspec();
+
+ // Update Font Face.
+ font_selector.update_font ();
+
+ // Update Size.
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ double size = sp_style_css_size_px_to_units(query.font_size.computed, unit);
+ font_selector.update_size (size);
+ selected_fontsize = size;
+ // Update font features (variant) widget
+ //int result_features =
+ sp_desktop_query_style (desktop, &query, QUERY_STYLE_PROPERTY_FONTVARIANTS);
+ int result_features =
+ sp_desktop_query_style (desktop, &query, QUERY_STYLE_PROPERTY_FONTFEATURESETTINGS);
+ font_features.update( &query, result_features == QUERY_STYLE_MULTIPLE_DIFFERENT, fontspec );
+ Glib::ustring features = font_features.get_markup();
+
+ // Update Preview
+ setPreviewText (fontspec, features, phrase);
+ }
+
+ blocked = false;
+}
+
+
+void TextEdit::setPreviewText (Glib::ustring font_spec, Glib::ustring font_features, Glib::ustring phrase)
+{
+ if (font_spec.empty()) {
+ preview_label->set_markup("");
+ preview_label2->set_markup("");
+ return;
+ }
+
+ // Limit number of lines in preview to arbitrary amount to prevent Text and Font dialog
+ // from growing taller than a desktop
+ const int max_lines = 4;
+ // Ignore starting empty lines; they would show up as nothing
+ auto start_pos = phrase.find_first_not_of(" \n\r\t");
+ if (start_pos == Glib::ustring::npos) {
+ start_pos = 0;
+ }
+ // Now take up to max_lines
+ auto end_pos = Glib::ustring::npos;
+ auto from = start_pos;
+ for (int i = 0; i < max_lines; ++i) {
+ end_pos = phrase.find("\n", from);
+ if (end_pos == Glib::ustring::npos) { break; }
+ from = end_pos + 1;
+ }
+ Glib::ustring phrase_trimmed = phrase.substr(start_pos, end_pos != Glib::ustring::npos ? end_pos - start_pos : end_pos);
+
+ Glib::ustring font_spec_escaped = Glib::Markup::escape_text( font_spec );
+ Glib::ustring phrase_escaped = Glib::Markup::escape_text(phrase_trimmed);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ double pt_size =
+ Inkscape::Util::Quantity::convert(
+ sp_style_css_size_units_to_px(font_selector.get_fontsize(), unit), "px", "pt");
+ pt_size = std::min(pt_size, 100.0);
+
+ // Pango font size is in 1024ths of a point
+ Glib::ustring size = std::to_string( int(pt_size * PANGO_SCALE) );
+ Glib::ustring markup = "<span font=\'" + font_spec_escaped +
+ "\' size=\'" + size + "\'";
+ if (!font_features.empty()) {
+ markup += " font_features=\'" + font_features + "\'";
+ }
+ markup += ">" + phrase_escaped + "</span>";
+
+ preview_label->set_markup (markup);
+ preview_label2->set_markup (markup);
+}
+
+
+SPItem *TextEdit::getSelectedTextItem ()
+{
+ if (!getDesktop())
+ return nullptr;
+
+ auto tmp= getDesktop()->getSelection()->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i)
+ {
+ if (is<SPText>(*i) || is<SPFlowtext>(*i))
+ return *i;
+ }
+
+ return nullptr;
+}
+
+
+unsigned TextEdit::getSelectedTextCount ()
+{
+ if (!getDesktop())
+ return 0;
+
+ unsigned int items = 0;
+
+ auto tmp= getDesktop()->getSelection()->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i)
+ {
+ if (is<SPText>(*i) || is<SPFlowtext>(*i))
+ ++items;
+ }
+
+ return items;
+}
+
+void TextEdit::documentReplaced()
+{
+ onReadSelection(true, true);
+}
+
+void TextEdit::selectionChanged(Selection *selection)
+{
+ onReadSelection(true, true);
+}
+
+void TextEdit::selectionModified(Selection *selection, guint flags)
+{
+ bool style = ((flags & (SP_OBJECT_CHILD_MODIFIED_FLAG |
+ SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 );
+ bool content = ((flags & (SP_OBJECT_CHILD_MODIFIED_FLAG |
+ SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 );
+ onReadSelection (style, content);
+}
+
+
+void TextEdit::updateObjectText ( SPItem *text )
+{
+ Gtk::TextIter start, end;
+
+ // write text
+ if (text_buffer->get_modified()) {
+ text_buffer->get_bounds(start, end);
+ Glib::ustring str = text_buffer->get_text(start, end);
+ sp_te_set_repr_text_multiline (text, str.c_str());
+ text_buffer->set_modified(false);
+ }
+}
+
+SPCSSAttr *TextEdit::fillTextStyle ()
+{
+ SPCSSAttr *css = sp_repr_css_attr_new ();
+
+ Glib::ustring fontspec = font_selector.get_fontspec();
+
+ if( !fontspec.empty() ) {
+
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->fill_css( css, fontspec );
+
+ // TODO, possibly move this to FontLister::set_css to be shared.
+ Inkscape::CSSOStringStream os;
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
+ if (prefs->getBool("/options/font/textOutputPx", true)) {
+ os << sp_style_css_size_units_to_px(font_selector.get_fontsize(), unit)
+ << sp_style_get_css_unit_string(SP_CSS_UNIT_PX);
+ } else {
+ os << font_selector.get_fontsize() << sp_style_get_css_unit_string(unit);
+ }
+ sp_repr_css_set_property (css, "font-size", os.str().c_str());
+ }
+
+ // Font features
+ font_features.fill_css( css );
+
+ return css;
+}
+
+void TextEdit::onSetDefault()
+{
+ SPCSSAttr *css = fillTextStyle ();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ blocked = true;
+ prefs->mergeStyle("/tools/text/style", css);
+ blocked = false;
+
+ sp_repr_css_attr_unref (css);
+
+ setasdefault_button->set_sensitive ( false );
+}
+
+void TextEdit::onApply()
+{
+ blocked = true;
+
+ SPDesktop *desktop = getDesktop();
+
+ unsigned items = 0;
+ auto item_list = desktop->getSelection()->items();
+ SPCSSAttr *css = fillTextStyle ();
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ for(auto i=item_list.begin();i!=item_list.end();++i){
+ // apply style to the reprs of all text objects in the selection
+ if (is<SPText>(*i) || (is<SPFlowtext>(*i)) ) {
+ ++items;
+ }
+ }
+ if (items == 1) {
+ double factor = font_selector.get_fontsize() / selected_fontsize;
+ prefs->setDouble("/options/font/scaleLineHeightFromFontSIze", factor);
+ }
+ sp_desktop_set_style(desktop, css, true);
+
+ if (items == 0) {
+ // no text objects; apply style to prefs for new objects
+ prefs->mergeStyle("/tools/text/style", css);
+ setasdefault_button->set_sensitive ( false );
+
+ } else if (items == 1) {
+ // exactly one text object; now set its text, too
+ SPItem *item = desktop->getSelection()->singleItem();
+ if (is<SPText>(item) || is<SPFlowtext>(item)) {
+ updateObjectText (item);
+ SPStyle *item_style = item->style;
+ if (is<SPText>(item) && item_style->inline_size.value == 0) {
+ css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET);
+ sp_repr_css_unset_property(css, "inline-size");
+ item->changeCSS(css, "style");
+ }
+ }
+ }
+
+ // Update FontLister
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ if( !fontspec.empty() ) {
+ Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
+ fontlister->set_fontspec( fontspec, false );
+ }
+
+ // complete the transaction
+ DocumentUndo::done(desktop->getDocument(), _("Set text style"), INKSCAPE_ICON("draw-text"));
+ apply_button->set_sensitive ( false );
+
+ sp_repr_css_attr_unref (css);
+ Inkscape::FontLister::get_instance()->update_font_list(desktop->getDocument());
+
+ blocked = false;
+}
+
+void TextEdit::display_font_collections()
+{
+ // std::cout << "TextEdit::display_font_collections()" << std::endl;
+
+ for (auto row : collections_list->get_children()) {
+ if (row) {
+ collections_list->remove(*row);
+ }
+ }
+
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+
+ // Insert system collections.
+ for(auto const& col: font_collections->get_collections(true)) {
+ auto btn = Gtk::make_managed<Gtk::CheckButton>(col);
+ btn->set_margin_bottom(2);
+ btn->set_active(font_collections->is_collection_selected(col));
+ btn->signal_toggled().connect([=](){
+ // toggle font system collection
+ font_collections->update_selected_collections(col);
+ });
+// g_message("tag: %s", tag.display_name.c_str());
+ auto row = Gtk::make_managed<Gtk::ListBoxRow>();
+ row->set_can_focus(false);
+ row->add(*btn);
+ row->show_all();
+ collections_list->append(*row);
+ }
+
+ // Insert row separator.
+ auto sep = Gtk::make_managed<Gtk::Separator>();
+ sep->set_margin_bottom(2);
+ auto sep_row = Gtk::make_managed<Gtk::ListBoxRow>();
+ sep_row->set_can_focus(false);
+ sep_row->add(*sep);
+ sep_row->show_all();
+ collections_list->append(*sep_row);
+
+ // Insert user collections.
+ for (auto const& col: font_collections->get_collections()) {
+ auto btn = Gtk::make_managed<Gtk::CheckButton>(col);
+ btn->set_margin_bottom(2);
+ btn->set_active(font_collections->is_collection_selected(col));
+ btn->signal_toggled().connect([=](){
+ // toggle font collection
+ font_collections->update_selected_collections(col);
+ });
+// g_message("tag: %s", tag.display_name.c_str());
+ auto row = Gtk::make_managed<Gtk::ListBoxRow>();
+ row->set_can_focus(false);
+ row->add(*btn);
+ row->show_all();
+ collections_list->append(*row);
+ }
+}
+
+void TextEdit::onFontFeatures(Gtk::Widget * widgt, int pos)
+{
+ if (pos == 1) {
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ if (!fontspec.empty()) {
+ auto res = FontFactory::get().FaceFromFontSpecification(fontspec.c_str());
+ if (res) {
+ font_features.update_opentype(fontspec);
+ }
+ }
+ }
+}
+
+void TextEdit::on_search_entry_changed()
+{
+ auto search_txt = search_entry->get_text();
+ font_selector.unset_model();
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ font_lister->show_results(search_txt);
+ font_selector.set_model();
+}
+
+void TextEdit::on_reset_button_pressed()
+{
+ FontCollections *font_collections = Inkscape::FontCollections::get();
+ search_entry->set_text("");
+
+ // Un-select all the selected font collections.
+ font_collections->clear_selected_collections();
+
+ Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
+ font_lister->init_font_families();
+ font_lister->init_default_styles();
+ SPDocument *document = getDesktop()->getDocument();
+ font_lister->add_document_fonts_at_top(document);
+}
+
+void TextEdit::change_font_count_label()
+{
+ auto label = Inkscape::FontLister::get_instance()->get_font_count_label();
+ font_count_label->set_label(label);
+}
+
+void TextEdit::on_fcm_button_clicked()
+{
+ // Inkscape::UI::Dialog::FontCollectionsManager::getInstance();
+ if(auto desktop = SP_ACTIVE_DESKTOP) {
+ if (auto container = desktop->getContainer()) {
+ container->new_floating_dialog("FontCollections");
+ }
+ }
+}
+
+void TextEdit::onChange()
+{
+ if (blocked) {
+ return;
+ }
+
+ Gtk::TextIter start, end;
+ text_buffer->get_bounds(start, end);
+ Glib::ustring str = text_buffer->get_text(start, end);
+
+ Glib::ustring fontspec = font_selector.get_fontspec();
+ Glib::ustring features = font_features.get_markup();
+ const Glib::ustring& phrase = str.empty() ? samplephrase : str;
+ setPreviewText(fontspec, features, phrase);
+
+ SPItem *text = getSelectedTextItem();
+ if (text) {
+ apply_button->set_sensitive ( true );
+ }
+
+ setasdefault_button->set_sensitive ( true);
+}
+
+void TextEdit::onFontChange(Glib::ustring fontspec)
+{
+ // Is not necessary update open type features this done when user click on font features tab
+ onChange();
+}
+
+} //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/dialog/text-edit.h b/src/ui/dialog/text-edit.h
new file mode 100644
index 0000000..a20fcac
--- /dev/null
+++ b/src/ui/dialog/text-edit.h
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Text-edit
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@ximian.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * John Smith
+ * Kris De Gussem <Kris.DeGussem@gmail.com>
+ * Tavmjong Bah
+ *
+ * Copyright (C) 1999-2013 Authors
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_TEXT_EDIT_H
+#define INKSCAPE_UI_DIALOG_TEXT_EDIT_H
+
+#include <glibmm/refptr.h>
+
+#include "helper/auto-connection.h"
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/frame.h"
+
+#include "ui/widget/font-selector.h"
+#include "ui/widget/font-variants.h"
+
+#include "util/action-accel.h"
+#include "util/font-collections.h"
+
+namespace Gtk {
+class Box;
+class Button;
+class ButtonBox;
+class Label;
+class Notebook;
+class TextBuffer;
+class TextView;
+}
+
+class SPItem;
+class FontInstance;
+class SPCSSAttr;
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+#define VB_MARGIN 4
+/**
+ * The TextEdit class defines the Text and font dialog.
+ *
+ * The Text and font dialog allows you to set the font family, style and size
+ * and shows a preview of the result. The dialogs layout settings include
+ * horizontal and vertical alignment and inter line distance.
+ */
+class TextEdit : public DialogBase
+{
+public:
+ TextEdit();
+ ~TextEdit() override;
+
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+ void selectionModified(Selection *selection, guint flags) override;
+
+protected:
+ /**
+ * Callback for pressing the default button.
+ */
+ void onSetDefault ();
+
+ /**
+ * Callback for pressing the apply button.
+ */
+ void onApply ();
+
+ /**
+ * Function to list the font collections in the popover menu.
+ */
+ void display_font_collections();
+
+ /**
+ * Called whenever something 'changes' on canvas.
+ *
+ * onReadSelection gets the currently selected item from the canvas and sets all the controls in this dialog to the correct state.
+ *
+ * @param dostyle Indicates whether the modification of the user includes a style change.
+ * @param content Indicates whether the modification of the user includes a style change. Actually refers to the question if we do want to show the content? (Parameter currently not used)
+ */
+ void onReadSelection (gboolean style, gboolean content);
+
+ /**
+ * This function would disable undo and redo if the text_view widget is in focus
+ * It is to fix the issue: https://gitlab.com/inkscape/inkscape/-/issues/744
+ */
+ bool captureUndo(GdkEventKey *event);
+
+ /**
+ * Callback invoked when the user modifies the text of the selected text object.
+ *
+ * onTextChange is responsible for initiating the commands after the user
+ * modified the text in the selected object. The UI of the dialog is
+ * updated. The subfunction setPreviewText updates the preview label.
+ *
+ * @param self pointer to the current instance of the dialog.
+ */
+ void onChange ();
+ void onFontFeatures (Gtk::Widget * widgt, int pos);
+
+ // Callback to handle changes in the search entry.
+ void on_search_entry_changed();
+ void on_reset_button_pressed();
+ void change_font_count_label();
+ void on_fcm_button_clicked();
+
+ /**
+ * Callback invoked when the user modifies the font through the dialog or the tools control bar.
+ *
+ * onFontChange updates the dialog UI. The subfunction setPreviewText updates the preview label.
+ *
+ * @param fontspec for the text to be previewed.
+ */
+ void onFontChange (Glib::ustring fontspec);
+
+ /**
+ * Get the selected text off the main canvas.
+ *
+ * @return SPItem pointer to the selected text object
+ */
+ SPItem *getSelectedTextItem ();
+
+ /**
+ * Count the number of text objects in the selection on the canvas.
+ */
+ unsigned getSelectedTextCount ();
+
+ /**
+ * Helper function to create markup from a fontspec and display in the preview label.
+ *
+ * @param fontspec for the text to be previewed.
+ * @param font_features for text to be previewed (in CSS format).
+ * @param phrase text to be shown.
+ */
+ void setPreviewText (Glib::ustring font_spec, Glib::ustring font_features, Glib::ustring phrase);
+
+ void updateObjectText ( SPItem *text );
+ SPCSSAttr *fillTextStyle ();
+
+private:
+
+ /*
+ * All the dialogs widgets
+ */
+
+ // Tab 1: Font ---------------------- //
+ Gtk::Box *settings_and_filters_box;
+ Gtk::MenuButton *filter_menu_button;
+ Gtk::Button *reset_button;
+ Gtk::SearchEntry *search_entry;
+ Gtk::Label *font_count_label;
+ Gtk::Popover *filter_popover;
+ Gtk::Box *popover_box;
+ Gtk::Frame *frame;
+ Gtk::Label *frame_label;
+ Gtk::Button *collection_editor_button;
+ Gtk::ListBox *collections_list;
+
+ Inkscape::UI::Widget::FontSelector font_selector;
+ Inkscape::UI::Widget::FontVariations font_variations;
+ Gtk::Label *preview_label; // Share with variants tab?
+
+ // Tab 2: Text ---------------------- //
+ Gtk::TextView *text_view;
+ Glib::RefPtr<Gtk::TextBuffer> text_buffer;
+
+ // Tab 3: Features ----------------- //
+ Inkscape::UI::Widget::FontVariants font_features;
+ Gtk::Label *preview_label2; // Could reparent preview_label but having a second label is probably easier.
+
+ // Shared ------- ------------------ //
+ Gtk::Button *setasdefault_button;
+ Gtk::Button *apply_button;
+
+ // Signals
+ sigc::connection selectChangedConn;
+ sigc::connection subselChangedConn;
+ sigc::connection selectModifiedConn;
+ sigc::connection fontChangedConn;
+ sigc::connection fontFeaturesChangedConn;
+ auto_connection fontCollectionsChangedSelection;
+ auto_connection fontCollectionsUpdate;
+
+ // Other
+ double selected_fontsize;
+ bool blocked;
+ const Glib::ustring samplephrase;
+
+ // Track undo and redo keyboard shortcuts
+ Util::ActionAccel _undo, _redo;
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_TEXT_EDIT_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/dialog/tile.cpp b/src/ui/dialog/tile.cpp
new file mode 100644
index 0000000..661c4f6
--- /dev/null
+++ b/src/ui/dialog/tile.cpp
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A simple dialog for creating grid type arrangements of selected objects
+ *
+ * Authors:
+ * Bob Jamison ( based off trace dialog)
+ * John Cliff
+ * Other dudes from The Inkscape Organization
+ * Abhishek Sharma
+ * Declara Denis
+ *
+ * Copyright (C) 2004 Bob Jamison
+ * Copyright (C) 2004 John Cliff
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tile.h"
+
+#include <glibmm/i18n.h>
+
+#include "ui/dialog/grid-arrange-tab.h"
+#include "ui/dialog/polar-arrange-tab.h"
+#include "ui/dialog/align-and-distribute.h"
+#include "ui/icon-names.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+Gtk::Box& create_tab_label(const char* label_text, const char* icon_name) {
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
+ auto image = Gtk::make_managed<Gtk::Image>();
+ image->set_from_icon_name(icon_name, Gtk::ICON_SIZE_MENU);
+ auto label = Gtk::make_managed<Gtk::Label>(label_text, true);
+ box->pack_start(*image, false, true);
+ box->pack_start(*label, false, true);
+ box->show_all();
+ return *box;
+}
+
+ArrangeDialog::ArrangeDialog()
+ : DialogBase("/dialogs/gridtiler", "AlignDistribute")
+{
+ _align_tab = Gtk::manage(new AlignAndDistribute(this));
+ _arrangeBox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
+ _arrangeBox->set_valign(Gtk::ALIGN_START);
+ _notebook = Gtk::manage(new Gtk::Notebook());
+ _gridArrangeTab = Gtk::manage(new GridArrangeTab(this));
+ _polarArrangeTab = Gtk::manage(new PolarArrangeTab(this));
+
+ set_valign(Gtk::ALIGN_START);
+
+ _notebook->set_valign(Gtk::ALIGN_START);
+ _notebook->append_page(*_align_tab, create_tab_label(C_("Arrange dialog", "Align"), INKSCAPE_ICON("dialog-align-and-distribute")));
+ // TRANSLATORS: "Grid" refers to grid (columns/rows) arrangement
+ _notebook->append_page(*_gridArrangeTab, create_tab_label(C_("Arrange dialog", "Grid"), INKSCAPE_ICON("arrange-grid")));
+ // TRANSLATORS: "Circular" refers to circular/radial arrangement
+ _notebook->append_page(*_polarArrangeTab, create_tab_label(C_("Arrange dialog", "Circular"), INKSCAPE_ICON("arrange-circular")));
+ _arrangeBox->pack_start(*_notebook);
+ _notebook->signal_switch_page().connect([=](Widget*, guint page){
+ update_arrange_btn();
+ });
+ pack_start(*_arrangeBox);
+
+ // Add button
+ _arrangeButton = Gtk::manage(new Gtk::Button(C_("Arrange dialog", "_Arrange")));
+ _arrangeButton->signal_clicked().connect(sigc::mem_fun(*this, &ArrangeDialog::_apply));
+ _arrangeButton->set_use_underline(true);
+ _arrangeButton->set_tooltip_text(_("Arrange selected objects"));
+ _arrangeButton->get_style_context()->add_class("wide-apply-button");
+ _arrangeButton->set_no_show_all();
+
+ Gtk::ButtonBox *button_box = Gtk::manage(new Gtk::ButtonBox());
+ button_box->set_layout(Gtk::BUTTONBOX_CENTER);
+ button_box->set_spacing(6);
+ button_box->set_border_width(4);
+ button_box->set_valign(Gtk::ALIGN_FILL);
+
+ button_box->pack_end(*_arrangeButton);
+ pack_start(*button_box);
+
+ show();
+ show_all_children();
+ update_arrange_btn();
+}
+
+void ArrangeDialog::update_arrange_btn() {
+ // "align" page doesn't use "Arrange" button
+ if (_notebook->get_current_page() == 0) {
+ _arrangeButton->hide();
+ }
+ else {
+ _arrangeButton->show();
+ }
+}
+
+ArrangeDialog::~ArrangeDialog()
+{ }
+
+void ArrangeDialog::_apply()
+{
+ switch(_notebook->get_current_page())
+ {
+ case 0:
+ // not applicable to align panel
+ break;
+ case 1:
+ _gridArrangeTab->arrange();
+ break;
+ case 2:
+ _polarArrangeTab->arrange();
+ break;
+ }
+}
+
+void ArrangeDialog::desktopReplaced()
+{
+ _gridArrangeTab->setDesktop(getDesktop());
+ _align_tab->desktop_changed(getDesktop());
+}
+
+} //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/dialog/tile.h b/src/ui/dialog/tile.h
new file mode 100644
index 0000000..8f1313f
--- /dev/null
+++ b/src/ui/dialog/tile.h
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Dialog for creating grid type arrangements of selected objects
+ */
+/* Authors:
+ * Bob Jamison ( based off trace dialog)
+ * John Cliff
+ * Other dudes from The Inkscape Organization
+ * Declara Denis
+ *
+ * Copyright (C) 2004 Bob Jamison
+ * Copyright (C) 2004 John Cliff
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_DIALOG_TILE_H
+#define SEEN_UI_DIALOG_TILE_H
+
+#include <gtkmm/box.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+
+#include "ui/dialog/dialog-base.h"
+
+namespace Gtk {
+class Button;
+class Grid;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class AlignAndDistribute;
+class ArrangeTab;
+class GridArrangeTab;
+class PolarArrangeTab;
+
+class ArrangeDialog : public DialogBase
+{
+public:
+ ArrangeDialog();
+ ~ArrangeDialog() override;
+
+ void desktopReplaced() override;
+
+ void update_arrange_btn();
+
+ /**
+ * Callback from Apply
+ */
+ void _apply();
+
+private:
+ Gtk::Box *_arrangeBox;
+ Gtk::Notebook *_notebook;
+ AlignAndDistribute* _align_tab;
+ GridArrangeTab *_gridArrangeTab;
+ PolarArrangeTab *_polarArrangeTab;
+ Gtk::Button *_arrangeButton;
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+
+#endif /* __TILEDIALOG_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/dialog/tracedialog.cpp b/src/ui/dialog/tracedialog.cpp
new file mode 100644
index 0000000..6f173aa
--- /dev/null
+++ b/src/ui/dialog/tracedialog.cpp
@@ -0,0 +1,553 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Bitmap tracing settings dialog - second implementation.
+ */
+/* Authors:
+ * Marc Jeanmougin <marc.jeanmougin@telecom-paristech.fr>
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2019-2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "tracedialog.h"
+
+#include <glibmm/i18n.h>
+#include <gtkmm/comboboxtext.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/spinbutton.h>
+#include <gtkmm/progressbar.h>
+#include <gtkmm/stack.h>
+
+#include "desktop.h"
+#include "io/resource.h"
+#include "selection.h"
+#include "trace/autotrace/inkscape-autotrace.h"
+#include "trace/depixelize/inkscape-depixelize.h"
+#include "trace/potrace/inkscape-potrace.h"
+#include "ui/util.h"
+
+// This maps the column ids in the glade file to useful enums
+static const std::map<std::string, Inkscape::Trace::Potrace::TraceType> trace_types = {
+ {"SS_BC", Inkscape::Trace::Potrace::TraceType::BRIGHTNESS},
+ {"SS_ED", Inkscape::Trace::Potrace::TraceType::CANNY},
+ {"SS_CQ", Inkscape::Trace::Potrace::TraceType::QUANT},
+ {"SS_AT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE},
+ {"SS_CT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE},
+
+ {"MS_BS", Inkscape::Trace::Potrace::TraceType::BRIGHTNESS_MULTI},
+ {"MS_C", Inkscape::Trace::Potrace::TraceType::QUANT_COLOR},
+ {"MS_BW", Inkscape::Trace::Potrace::TraceType::QUANT_MONO},
+ {"MS_AT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI},
+};
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+enum class EngineType
+{
+ Potrace,
+ Autotrace,
+ Depixelize
+};
+
+struct TraceData
+{
+ std::unique_ptr<Trace::TracingEngine> engine;
+ bool sioxEnabled;
+};
+
+class TraceDialogImpl
+ : public TraceDialog
+{
+public:
+ TraceDialogImpl();
+ ~TraceDialogImpl() override;
+
+protected:
+ void selectionModified(Selection *selection, unsigned flags) override;
+ void selectionChanged(Selection *selection) override;
+
+private:
+ TraceData getTraceData() const;
+ bool paintPreview(Cairo::RefPtr<Cairo::Context> const &cr);
+ void setDefaults();
+ void adjustParamsVisible();
+ void onTraceClicked();
+ void onAbortClicked();
+ bool previewsEnabled() const;
+ void schedulePreviewUpdate(int msecs, bool force = false);
+ void updatePreview(bool force = false);
+ void launchPreviewGeneration();
+
+ // Handles to ongoing asynchronous computations.
+ Trace::TraceFuture trace_future;
+ Trace::TraceFuture preview_future;
+
+ // Delayed preview generation.
+ sigc::connection preview_timeout_conn;
+ bool preview_pending_recompute = false;
+ Glib::RefPtr<Gdk::Pixbuf> preview_image;
+
+ Glib::RefPtr<Gtk::Builder> builder;
+ Glib::RefPtr<Gtk::Adjustment> MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2, SS_AT_ET_T, SS_AT_FI_T, SS_BC_T, SS_CQ_T, SS_ED_T, optimize, smooth, speckles;
+ Gtk::ComboBoxText *CBT_SS, *CBT_MS;
+ Gtk::CheckButton *CB_invert, *CB_MS_smooth, *CB_MS_stack, *CB_MS_rb, *CB_speckles, *CB_smooth, *CB_optimize, *CB_PA_optimize, /* *CB_live,*/ *CB_SIOX;
+ Gtk::CheckButton* CB_SIOX1;
+ Gtk::CheckButton* CB_speckles1;
+ Gtk::CheckButton* CB_smooth1;
+ Gtk::CheckButton* CB_optimize1;
+ Gtk::RadioButton *RB_PA_voronoi;
+ Gtk::Button *B_RESET, *B_STOP, *B_OK, *B_Update;
+ Gtk::Box *mainBox;
+ Gtk::Notebook *choice_tab;
+ Gtk::DrawingArea *previewArea;
+ Gtk::Box* orient_box;
+ Gtk::Frame* _preview_frame;
+ Gtk::Grid* _param_grid;
+ Gtk::CheckButton* _live_preview;
+ Gtk::Stack *stack;
+ Gtk::ProgressBar *progressbar;
+ Gtk::Box *boxchild1, *boxchild2;
+};
+
+enum class Page
+{
+ SingleScan,
+ MultiScan,
+ PixelArt
+};
+
+TraceData TraceDialogImpl::getTraceData() const
+{
+ auto current_page = static_cast<Page>(choice_tab->get_current_page());
+
+ auto cb_siox = current_page == Page::SingleScan ? CB_SIOX : CB_SIOX1;
+ bool enable_siox = cb_siox->get_active();
+
+ auto trace_type_str = current_page == Page::SingleScan ? CBT_SS->get_active_id() : CBT_MS->get_active_id();
+ auto it = trace_types.find(trace_type_str);
+ assert(it != trace_types.end());
+ auto trace_type = it->second;
+
+ EngineType engine_type;
+ if (current_page == Page::PixelArt) {
+ engine_type = EngineType::Depixelize;
+ } else {
+ switch (trace_type) {
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE:
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE:
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI:
+ engine_type = EngineType::Autotrace;
+ break;
+ default:
+ engine_type = EngineType::Potrace;
+ break;
+ }
+ }
+
+ auto setup_potrace = [&, this] {
+ auto eng = std::make_unique<Trace::Potrace::PotraceTracingEngine>(
+ trace_type, CB_invert->get_active(), (int)SS_CQ_T->get_value(), SS_BC_T->get_value(),
+ 0, // Brightness floor
+ SS_ED_T->get_value(), (int)MS_scans->get_value(), CB_MS_stack->get_active(), CB_MS_smooth->get_active(),
+ CB_MS_rb->get_active());
+
+ auto cb_optimize = current_page == Page::SingleScan ? CB_optimize : CB_optimize1;
+ eng->setOptiCurve(cb_optimize->get_active());
+ eng->setOptTolerance(optimize->get_value());
+
+ auto cb_smooth = current_page == Page::SingleScan ? CB_smooth : CB_smooth1;
+ eng->setAlphaMax(cb_smooth->get_active() ? smooth->get_value() : 0);
+
+ auto cb_speckles = current_page == Page::SingleScan ? CB_speckles : CB_speckles1;
+ eng->setTurdSize(cb_speckles->get_active() ? (int)speckles->get_value() : 0);
+
+ return eng;
+ };
+
+ auto setup_autotrace = [&, this] {
+ auto eng = std::make_unique<Trace::Autotrace::AutotraceTracingEngine>();
+
+ switch (trace_type) {
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE:
+ eng->setColorCount(2);
+ break;
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE:
+ eng->setColorCount(2);
+ eng->setCenterLine(true);
+ eng->setPreserveWidth(true);
+ break;
+ case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI:
+ eng->setColorCount((int)MS_scans->get_value() + 1);
+ break;
+ default:
+ assert(false);
+ break;
+ }
+
+ eng->setFilterIterations((int)SS_AT_FI_T->get_value());
+ eng->setErrorThreshold(SS_AT_ET_T->get_value());
+
+ return eng;
+ };
+
+ auto setup_depixelize = [this] {
+ return std::make_unique<Trace::Depixelize::DepixelizeTracingEngine>(
+ RB_PA_voronoi->get_active() ? Inkscape::Trace::Depixelize::TraceType::VORONOI : Inkscape::Trace::Depixelize::TraceType::BSPLINES,
+ PA_curves->get_value(), (int) PA_islands->get_value(),
+ (int) PA_sparse1->get_value(), PA_sparse2->get_value(),
+ CB_PA_optimize->get_active());
+ };
+
+ TraceData data;
+ switch (engine_type) {
+ case EngineType::Potrace: data.engine = setup_potrace(); break;
+ case EngineType::Autotrace: data.engine = setup_autotrace(); break;
+ case EngineType::Depixelize: data.engine = setup_depixelize(); break;
+ default: assert(false); break;
+ }
+ data.sioxEnabled = enable_siox;
+
+ return data;
+}
+
+bool TraceDialogImpl::paintPreview(Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ if (preview_image) {
+ int width = preview_image->get_width();
+ int height = preview_image->get_height();
+ Gtk::Allocation const &allocation = previewArea->get_allocation();
+ double scaleFX = (double)allocation.get_width() / width;
+ double scaleFY = (double)allocation.get_height() / height;
+ double scaleFactor = std::min(scaleFX, scaleFY);
+ int newWidth = (double)width * scaleFactor;
+ int newHeight = (double)height * scaleFactor;
+ int offsetX = (allocation.get_width() - newWidth) / 2;
+ int offsetY = (allocation.get_height() - newHeight) / 2;
+ cr->scale(scaleFactor, scaleFactor);
+ Gdk::Cairo::set_source_pixbuf(cr, preview_image, offsetX / scaleFactor, offsetY / scaleFactor);
+ cr->paint();
+ } else {
+ cr->set_source_rgba(0, 0, 0, 0);
+ cr->paint();
+ }
+
+ return false;
+}
+
+void TraceDialogImpl::selectionChanged(Inkscape::Selection *selection)
+{
+ updatePreview();
+}
+
+void TraceDialogImpl::selectionModified(Selection *selection, unsigned flags)
+{
+ auto mask = SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG;
+ if ((flags & mask) == mask) {
+ // All flags set - preview instantly.
+ updatePreview();
+ } else if (flags & mask) {
+ // At least one flag set - preview after a long delay.
+ schedulePreviewUpdate(1000);
+ }
+}
+
+void TraceDialogImpl::setDefaults()
+{
+ MS_scans->set_value(8);
+ PA_curves->set_value(1);
+ PA_islands->set_value(5);
+ PA_sparse1->set_value(4);
+ PA_sparse2->set_value(1);
+ SS_AT_FI_T->set_value(4);
+ SS_AT_ET_T->set_value(2);
+ SS_BC_T->set_value(0.45);
+ SS_CQ_T->set_value(64);
+ SS_ED_T->set_value(.65);
+ optimize->set_value(0.2);
+ smooth->set_value(1);
+ speckles->set_value(2);
+ CB_invert->set_active(false);
+ CB_MS_smooth->set_active(true);
+ CB_MS_stack->set_active(true);
+ CB_MS_rb->set_active(false);
+ CB_speckles->set_active(true);
+ CB_smooth->set_active(true);
+ CB_optimize->set_active(true);
+ CB_speckles1->set_active(true);
+ CB_smooth1->set_active(true);
+ CB_optimize1->set_active(true);
+ CB_PA_optimize->set_active(false);
+ CB_SIOX->set_active(false);
+ CB_SIOX1->set_active(false);
+}
+
+void TraceDialogImpl::onAbortClicked()
+{
+ if (!trace_future) {
+ // Not tracing; nothing to cancel.
+ return;
+ }
+
+ stack->set_visible_child(*boxchild1);
+ if (auto desktop = getDesktop()) desktop->clearWaitingCursor();
+ trace_future.cancel();
+}
+
+void TraceDialogImpl::onTraceClicked()
+{
+ if (trace_future) {
+ // Still tracing; wait for either finished or cancelled.
+ return;
+ }
+
+ // Attempt to fire off the tracer.
+ auto data = getTraceData();
+ trace_future = Trace::trace(std::move(data.engine), data.sioxEnabled,
+ // On progress:
+ [this] (double progress) {
+ progressbar->set_fraction(progress);
+ },
+ // On completion without cancelling:
+ [this] {
+ progressbar->set_fraction(1.0);
+ stack->set_visible_child(*boxchild1);
+ if (auto desktop = getDesktop()) desktop->clearWaitingCursor();
+ trace_future.cancel();
+ }
+ );
+
+ if (trace_future) {
+ // Put the UI into the tracing state.
+ if (auto desktop = getDesktop()) desktop->setWaitingCursor();
+ stack->set_visible_child(*boxchild2);
+ progressbar->set_fraction(0.0);
+ }
+}
+
+TraceDialogImpl::TraceDialogImpl()
+{
+ Glib::ustring const req_widgets[] = { "MS_scans", "PA_curves", "PA_islands", "PA_sparse1", "PA_sparse2",
+ "SS_AT_FI_T", "SS_AT_ET_T", "SS_BC_T", "SS_CQ_T", "SS_ED_T",
+ "optimize", "smooth", "speckles", "CB_invert", "CB_MS_smooth",
+ "CB_MS_stack", "CB_MS_rb", "CB_speckles", "CB_smooth", "CB_optimize",
+ "CB_speckles1", "CB_smooth1", "CB_optimize1", "CB_SIOX1",
+ "CB_PA_optimize", /*"CB_live",*/ "CB_SIOX", "CBT_SS", "CBT_MS",
+ "B_RESET", "B_STOP", "B_OK", "mainBox", "choice_tab",
+ /*"choice_scan",*/ "previewArea", "_live_preview",
+ "stack", "progressbar", "boxchild1", "boxchild2" };
+ auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-trace.glade");
+ try {
+ builder = Gtk::Builder::create_from_file(gladefile);
+ } catch (Glib::Error const &) {
+ g_warning("Trace dialog: Glade file loading failed");
+ return;
+ }
+
+ for (auto &w : req_widgets) {
+ auto test = builder->get_object(w);
+ if (!test) {
+ g_warning("Trace dialog: Required widget %s does not exist", w.c_str());
+ return;
+ }
+ }
+
+#define GET_O(name) name = Glib::RefPtr<Gtk::Adjustment>::cast_dynamic(builder->get_object(#name));
+ GET_O(MS_scans)
+ GET_O(PA_curves)
+ GET_O(PA_islands)
+ GET_O(PA_sparse1)
+ GET_O(PA_sparse2)
+ GET_O(SS_AT_FI_T)
+ GET_O(SS_AT_ET_T)
+ GET_O(SS_BC_T)
+ GET_O(SS_CQ_T)
+ GET_O(SS_ED_T)
+ GET_O(optimize)
+ GET_O(smooth)
+ GET_O(speckles)
+#undef GET_O
+#define GET_W(name) builder->get_widget(#name, name);
+ GET_W(CB_invert)
+ GET_W(CB_MS_smooth)
+ GET_W(CB_MS_stack)
+ GET_W(CB_MS_rb)
+ GET_W(CB_speckles)
+ GET_W(CB_smooth)
+ GET_W(CB_optimize)
+ GET_W(CB_speckles1)
+ GET_W(CB_smooth1)
+ GET_W(CB_optimize1)
+ GET_W(CB_PA_optimize)
+ GET_W(CB_SIOX)
+ GET_W(CB_SIOX1)
+ GET_W(RB_PA_voronoi)
+ GET_W(CBT_SS)
+ GET_W(CBT_MS)
+ GET_W(B_RESET)
+ GET_W(B_STOP)
+ GET_W(B_OK)
+ GET_W(B_Update)
+ GET_W(mainBox)
+ GET_W(choice_tab)
+ GET_W(previewArea)
+ GET_W(orient_box)
+ GET_W(_preview_frame)
+ GET_W(_param_grid)
+ GET_W(_live_preview)
+ GET_W(stack)
+ GET_W(progressbar)
+ GET_W(boxchild1)
+ GET_W(boxchild2)
+#undef GET_W
+ add(*mainBox);
+
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+
+ _live_preview->set_active(prefs->getBool(getPrefsPath() + "liveUpdate", true));
+
+ B_Update->signal_clicked().connect([=] { updatePreview(true); });
+ B_OK->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::onTraceClicked));
+ B_STOP->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::onAbortClicked));
+ B_RESET->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::setDefaults));
+ previewArea->signal_draw().connect(sigc::mem_fun(*this, &TraceDialogImpl::paintPreview));
+
+ // attempt at making UI responsive: relocate preview to the right or bottom of dialog depending on dialog size
+ signal_size_allocate().connect([=] (Gtk::Allocation const &alloc) {
+ // skip bogus sizes
+ if (alloc.get_width() < 10 || alloc.get_height() < 10) return;
+ // ratio: is dialog wide or is it tall?
+ double const ratio = alloc.get_width() / static_cast<double>(alloc.get_height());
+ // g_warning("size alloc: %d x %d - %f", alloc.get_width(), alloc.get_height(), ratio);
+ double constexpr hysteresis = 0.01;
+ if (ratio < 1 - hysteresis) {
+ // narrow/tall
+ choice_tab->set_valign(Gtk::ALIGN_START);
+ orient_box->set_orientation(Gtk::ORIENTATION_VERTICAL);
+ }
+ else if (ratio > 1 + hysteresis) {
+ // wide/short
+ orient_box->set_orientation(Gtk::ORIENTATION_HORIZONTAL);
+ choice_tab->set_valign(Gtk::ALIGN_FILL);
+ }
+ });
+
+ CBT_SS->signal_changed().connect([=] { adjustParamsVisible(); });
+ adjustParamsVisible();
+
+ // watch for changes, but only in params that can impact preview bitmap
+ for (auto adj : {SS_BC_T, SS_ED_T, SS_CQ_T, SS_AT_FI_T, SS_AT_ET_T, /* optimize, smooth, speckles,*/ MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2 }) {
+ adj->signal_value_changed().connect([=] { updatePreview(); });
+ }
+ for (auto checkbtn : {CB_invert, CB_MS_rb, /* CB_MS_smooth, CB_MS_stack, CB_optimize1, CB_optimize, */ CB_PA_optimize, CB_SIOX1, CB_SIOX, /* CB_smooth1, CB_smooth, CB_speckles1, CB_speckles, */ _live_preview}) {
+ checkbtn->signal_toggled().connect([=] { updatePreview(); });
+ }
+ for (auto combo : {CBT_SS, CBT_MS}) {
+ combo->signal_changed().connect([=] { updatePreview(); });
+ }
+ choice_tab->signal_switch_page().connect([=] (Gtk::Widget*, unsigned) { updatePreview(); });
+
+ signal_set_focus_child().connect([=] (Gtk::Widget *w) {
+ if (w) updatePreview();
+ });
+}
+
+TraceDialogImpl::~TraceDialogImpl()
+{
+ Inkscape::Preferences* prefs = Inkscape::Preferences::get();
+ prefs->setBool(getPrefsPath() + "liveUpdate", _live_preview->get_active());
+ preview_timeout_conn.disconnect();
+}
+
+bool TraceDialogImpl::previewsEnabled() const
+{
+ return _live_preview->get_active() && is_widget_effectively_visible(this);
+}
+
+void TraceDialogImpl::schedulePreviewUpdate(int msecs, bool force)
+{
+ if (!previewsEnabled() && !force) {
+ return;
+ }
+
+ // Restart timeout.
+ preview_timeout_conn.disconnect();
+ preview_timeout_conn = Glib::signal_timeout().connect([this] {
+ updatePreview(true);
+ return false;
+ }, msecs);
+}
+
+void TraceDialogImpl::updatePreview(bool force)
+{
+ if (!previewsEnabled() && !force) {
+ return;
+ }
+
+ preview_timeout_conn.disconnect();
+
+ if (preview_future) {
+ // Preview generation already running - flag for recomputation when finished.
+ preview_pending_recompute = true;
+ return;
+ }
+
+ preview_pending_recompute = false;
+
+ auto data = getTraceData();
+ preview_future = Trace::preview(std::move(data.engine), data.sioxEnabled,
+ // On completion:
+ [this] (Glib::RefPtr<Gdk::Pixbuf> result) {
+ preview_image = std::move(result);
+ previewArea->queue_draw();
+ preview_future.cancel();
+
+ // Recompute if invalidated during computation.
+ if (preview_pending_recompute) {
+ updatePreview();
+ }
+ }
+ );
+
+ if (!preview_future) {
+ // On instant failure:
+ preview_image.reset();
+ previewArea->queue_draw();
+ }
+}
+
+void TraceDialogImpl::adjustParamsVisible()
+{
+ int constexpr start_row = 2;
+ int option = CBT_SS->get_active_row_number();
+ if (option >= 3) option = 3;
+ int show1 = start_row + option;
+ int show2 = show1;
+ if (option == 3) ++show2;
+
+ for (int row = start_row; row < start_row + 5; ++row) {
+ for (int col = 0; col < 4; ++col) {
+ if (auto widget = _param_grid->get_child_at(col, row)) {
+ if (row == show1 || row == show2) {
+ widget->show();
+ } else {
+ widget->hide();
+ }
+ }
+ }
+ }
+}
+
+std::unique_ptr<TraceDialog> TraceDialog::create()
+{
+ return std::make_unique<TraceDialogImpl>();
+}
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/dialog/tracedialog.h b/src/ui/dialog/tracedialog.h
new file mode 100644
index 0000000..6900a52
--- /dev/null
+++ b/src/ui/dialog/tracedialog.h
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Bitmap tracing settings dialog
+ */
+/* Authors:
+ * Bob Jamison
+ * Others - see git history.
+ *
+ * Copyright (C) 2004-2022 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_TRACE_H
+#define INKSCAPE_UI_DIALOG_TRACE_H
+
+#include <memory>
+#include "ui/dialog/dialog-base.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+class TraceDialog : public DialogBase
+{
+public:
+ static std::unique_ptr<TraceDialog> create();
+
+protected:
+ TraceDialog() : DialogBase("/dialogs/trace", "Trace") {}
+};
+
+} //namespace Dialog
+} //namespace UI
+} //namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_TRACE_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/dialog/transformation.cpp b/src/ui/dialog/transformation.cpp
new file mode 100644
index 0000000..9426667
--- /dev/null
+++ b/src/ui/dialog/transformation.cpp
@@ -0,0 +1,1216 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Transform dialog - implementation.
+ */
+/* Authors:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ * buliabyak@gmail.com
+ * Abhishek Sharma
+ *
+ * Copyright (C) 2004, 2005 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "transformation.h"
+
+#include <gtkmm/dialog.h>
+
+#include <2geom/transforms.h>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "selection-chemistry.h"
+
+#include "object/algorithms/bboxsort.h"
+#include "object/sp-item-transform.h"
+#include "object/sp-namedview.h"
+
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/*########################################################################
+# C O N S T R U C T O R
+########################################################################*/
+
+Transformation::Transformation()
+ : DialogBase("/dialogs/transformation", "Transform"),
+ _page_move (4, 2),
+ _page_scale (4, 2),
+ _page_rotate (4, 2),
+ _page_skew (4, 2),
+ _page_transform (3, 3),
+ _scalar_move_horizontal (_("_Horizontal:"), _("Horizontal displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR,
+ "", "transform-move-horizontal", &_units_move),
+ _scalar_move_vertical (_("_Vertical:"), _("Vertical displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR,
+ "", "transform-move-vertical", &_units_move),
+ _scalar_scale_horizontal(_("_Width:"), _("Horizontal size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS,
+ "", "transform-scale-horizontal", &_units_scale),
+ _scalar_scale_vertical (_("_Height:"), _("Vertical size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS,
+ "", "transform-scale-vertical", &_units_scale),
+ _scalar_rotate (_("A_ngle:"), _("Rotation angle (positive = counterclockwise)"), UNIT_TYPE_RADIAL,
+ "", "transform-rotate", &_units_rotate),
+ _scalar_skew_horizontal (_("_Horizontal:"), _("Horizontal skew angle (positive = counterclockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR,
+ "", "transform-skew-horizontal", &_units_skew),
+ _scalar_skew_vertical (_("_Vertical:"), _("Vertical skew angle (positive = clockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR,
+ "", "transform-skew-vertical", &_units_skew),
+
+ _scalar_transform_a ("", _("Transformation matrix element A")),
+ _scalar_transform_b ("", _("Transformation matrix element B")),
+ _scalar_transform_c ("", _("Transformation matrix element C")),
+ _scalar_transform_d ("", _("Transformation matrix element D")),
+ _scalar_transform_e ("", _("Transformation matrix element E"), UNIT_TYPE_LINEAR, "", "", &_units_transform),
+ _scalar_transform_f ("", _("Transformation matrix element F"), UNIT_TYPE_LINEAR, "", "", &_units_transform),
+
+ _counterclockwise_rotate (),
+ _clockwise_rotate (),
+
+ _check_move_relative (_("Rela_tive move")),
+ _check_scale_proportional (_("_Scale proportionally")),
+ _check_apply_separately (_("Apply to each _object separately")),
+ _check_replace_matrix (_("Edit c_urrent matrix"))
+
+{
+ _check_move_relative.set_use_underline();
+ _check_move_relative.set_tooltip_text(_("Add the specified relative displacement to the current position; otherwise, edit the current absolute position directly"));
+ _check_scale_proportional.set_use_underline();
+ _check_scale_proportional.set_tooltip_text(_("Preserve the width/height ratio of the scaled objects"));
+ _check_apply_separately.set_use_underline();
+ _check_apply_separately.set_tooltip_text(_("Apply the scale/rotate/skew to each selected object separately; otherwise, transform the selection as a whole"));
+ _check_replace_matrix.set_use_underline();
+ _check_replace_matrix.set_tooltip_text(_("Edit the current transform= matrix; otherwise, post-multiply transform= by this matrix"));
+
+ set_spacing(0);
+
+ // Notebook for individual transformations
+ pack_start(_notebook, false, false);
+
+ _page_move.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_move, _("_Move"), true);
+ layoutPageMove();
+
+ _page_scale.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_scale, _("_Scale"), true);
+ layoutPageScale();
+
+ _page_rotate.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_rotate, _("_Rotate"), true);
+ layoutPageRotate();
+
+ _page_skew.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_skew, _("Ske_w"), true);
+ layoutPageSkew();
+
+ _page_transform.set_halign(Gtk::ALIGN_START);
+ _notebook.append_page(_page_transform, _("Matri_x"), true);
+ layoutPageTransform();
+
+ _tabSwitchConn = _notebook.signal_switch_page().connect(sigc::mem_fun(*this, &Transformation::onSwitchPage));
+
+ // Apply separately
+ pack_start(_check_apply_separately, false, false);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ _check_apply_separately.set_active(prefs->getBool("/dialogs/transformation/applyseparately"));
+ _check_apply_separately.signal_toggled().connect(sigc::mem_fun(*this, &Transformation::onApplySeparatelyToggled));
+
+ // make sure all spinbuttons activate Apply on pressing Enter
+ ((Gtk::Entry *) (_scalar_move_horizontal.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_move_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_scale_horizontal.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_scale_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_rotate.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_skew_horizontal.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ ((Gtk::Entry *) (_scalar_skew_vertical.getWidget()))->signal_activate().connect(sigc::mem_fun(*this, &Transformation::_apply));
+
+ resetButton = Gtk::manage(new Gtk::Button());
+ resetButton->set_image_from_icon_name("reset-settings-symbolic");
+ resetButton->set_size_request(30, -1);
+ resetButton->set_halign(Gtk::ALIGN_CENTER);
+ resetButton->set_use_underline();
+ resetButton->set_tooltip_text(_("Reset the values on the current tab to defaults"));
+ resetButton->set_sensitive(true);
+ resetButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onClear));
+
+ applyButton = Gtk::manage(new Gtk::Button(_("_Apply")));
+ applyButton->set_use_underline();
+ applyButton->set_halign(Gtk::ALIGN_CENTER);
+ applyButton->set_tooltip_text(_("Apply transformation to selection"));
+ applyButton->set_sensitive(false);
+ applyButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::_apply));
+ applyButton->get_style_context()->add_class("wide-apply-button");
+
+ auto button_box = Gtk::manage(new Gtk::Box());
+ button_box->set_margin_top(4);
+ button_box->set_spacing(8);
+ button_box->set_halign(Gtk::ALIGN_CENTER);
+ button_box->pack_start(*applyButton);
+ button_box->pack_start(*resetButton);
+ pack_start(*button_box, Gtk::PACK_SHRINK, 0);
+
+ show_all_children();
+}
+
+Transformation::~Transformation()
+{
+ _tabSwitchConn.disconnect();
+}
+
+void Transformation::selectionChanged(Inkscape::Selection *selection)
+{
+ updateSelection((Inkscape::UI::Dialog::Transformation::PageType)getCurrentPage(), selection);
+}
+void Transformation::selectionModified(Inkscape::Selection *selection, guint flags)
+{
+ selectionChanged(selection);
+}
+
+/*########################################################################
+# U T I L I T Y
+########################################################################*/
+
+void Transformation::presentPage(Transformation::PageType page)
+{
+ _notebook.set_current_page(page);
+ show();
+}
+
+
+
+
+/*########################################################################
+# S E T U P L A Y O U T
+########################################################################*/
+
+
+void Transformation::layoutPageMove()
+{
+ _units_move.setUnitType(UNIT_TYPE_LINEAR);
+
+ _scalar_move_horizontal.initScalar(-1e6, 1e6);
+ _scalar_move_horizontal.setDigits(3);
+ _scalar_move_horizontal.setIncrements(0.1, 1.0);
+ _scalar_move_horizontal.set_hexpand();
+ _scalar_move_horizontal.setWidthChars(7);
+
+ _scalar_move_vertical.initScalar(-1e6, 1e6);
+ _scalar_move_vertical.setDigits(3);
+ _scalar_move_vertical.setIncrements(0.1, 1.0);
+ _scalar_move_vertical.set_hexpand();
+ _scalar_move_vertical.setWidthChars(7);
+
+ //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_HOR );
+
+ _page_move.table().attach(_scalar_move_horizontal, 0, 0, 2, 1);
+ _page_move.table().attach(_units_move, 2, 0, 1, 1);
+
+ _scalar_move_horizontal.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged));
+
+ //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_VER );
+ _page_move.table().attach(_scalar_move_vertical, 0, 1, 2, 1);
+
+ _scalar_move_vertical.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged));
+
+ // Relative moves
+ _page_move.table().attach(_check_move_relative, 0, 2, 2, 1);
+
+ _check_move_relative.set_active(true);
+ _check_move_relative.signal_toggled()
+ .connect(sigc::mem_fun(*this, &Transformation::onMoveRelativeToggled));
+}
+
+void Transformation::layoutPageScale()
+{
+ _units_scale.setUnitType(UNIT_TYPE_DIMENSIONLESS);
+ _units_scale.setUnitType(UNIT_TYPE_LINEAR);
+
+ _scalar_scale_horizontal.initScalar(-1e6, 1e6);
+ _scalar_scale_horizontal.setValue(100.0, "%");
+ _scalar_scale_horizontal.setDigits(3);
+ _scalar_scale_horizontal.setIncrements(0.1, 1.0);
+ _scalar_scale_horizontal.setAbsoluteIsIncrement(true);
+ _scalar_scale_horizontal.setPercentageIsIncrement(true);
+ _scalar_scale_horizontal.set_hexpand();
+ _scalar_scale_horizontal.setWidthChars(7);
+
+ _scalar_scale_vertical.initScalar(-1e6, 1e6);
+ _scalar_scale_vertical.setValue(100.0, "%");
+ _scalar_scale_vertical.setDigits(3);
+ _scalar_scale_vertical.setIncrements(0.1, 1.0);
+ _scalar_scale_vertical.setAbsoluteIsIncrement(true);
+ _scalar_scale_vertical.setPercentageIsIncrement(true);
+ _scalar_scale_vertical.set_hexpand();
+ _scalar_scale_vertical.setWidthChars(7);
+
+ _page_scale.table().attach(_scalar_scale_horizontal, 0, 0, 2, 1);
+
+ _scalar_scale_horizontal.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onScaleXValueChanged));
+
+ _page_scale.table().attach(_units_scale, 2, 0, 1, 1);
+ _page_scale.table().attach(_scalar_scale_vertical, 0, 1, 2, 1);
+
+ _scalar_scale_vertical.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onScaleYValueChanged));
+
+ _page_scale.table().attach(_check_scale_proportional, 0, 2, 2, 1);
+
+ _check_scale_proportional.set_active(false);
+ _check_scale_proportional.signal_toggled()
+ .connect(sigc::mem_fun(*this, &Transformation::onScaleProportionalToggled));
+
+ //TODO: add a widget for selecting the fixed point in scaling, or honour rotation center?
+}
+
+void Transformation::layoutPageRotate()
+{
+ _units_rotate.setUnitType(UNIT_TYPE_RADIAL);
+
+ _scalar_rotate.initScalar(-360.0, 360.0);
+ _scalar_rotate.setDigits(3);
+ _scalar_rotate.setIncrements(0.1, 1.0);
+ _scalar_rotate.set_hexpand();
+
+ auto object_rotate_left_icon = Gtk::manage(sp_get_icon_image("object-rotate-left", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ _counterclockwise_rotate.add(*object_rotate_left_icon);
+ _counterclockwise_rotate.set_mode(false);
+ _counterclockwise_rotate.set_relief(Gtk::RELIEF_NONE);
+ _counterclockwise_rotate.set_tooltip_text(_("Rotate in a counterclockwise direction"));
+
+ auto object_rotate_right_icon = Gtk::manage(sp_get_icon_image("object-rotate-right", Gtk::ICON_SIZE_SMALL_TOOLBAR));
+
+ _clockwise_rotate.add(*object_rotate_right_icon);
+ _clockwise_rotate.set_mode(false);
+ _clockwise_rotate.set_relief(Gtk::RELIEF_NONE);
+ _clockwise_rotate.set_tooltip_text(_("Rotate in a clockwise direction"));
+
+ Gtk::RadioButton::Group group = _counterclockwise_rotate.get_group();
+ _clockwise_rotate.set_group(group);
+
+ auto box = Gtk::make_managed<Gtk::Box>();
+ _counterclockwise_rotate.set_halign(Gtk::ALIGN_START);
+ _clockwise_rotate.set_halign(Gtk::ALIGN_START);
+ box->pack_start(_counterclockwise_rotate);
+ box->pack_start(_clockwise_rotate);
+
+ _page_rotate.table().attach(_scalar_rotate, 0, 0, 1, 1);
+ _page_rotate.table().attach(_units_rotate, 1, 0, 1, 1);
+ _page_rotate.table().attach(*box, 1, 1, 1, 1);
+
+ _scalar_rotate.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onRotateValueChanged));
+
+ _counterclockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateCounterclockwiseClicked));
+ _clockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateClockwiseClicked));
+
+ //TODO: honour rotation center?
+}
+
+void Transformation::layoutPageSkew()
+{
+ _units_skew.setUnitType(UNIT_TYPE_LINEAR);
+ _units_skew.setUnitType(UNIT_TYPE_DIMENSIONLESS);
+ _units_skew.setUnitType(UNIT_TYPE_RADIAL);
+
+ _scalar_skew_horizontal.initScalar(-1e6, 1e6);
+ _scalar_skew_horizontal.setDigits(3);
+ _scalar_skew_horizontal.setIncrements(0.1, 1.0);
+ _scalar_skew_horizontal.set_hexpand();
+ _scalar_skew_horizontal.setWidthChars(7);
+
+ _scalar_skew_vertical.initScalar(-1e6, 1e6);
+ _scalar_skew_vertical.setDigits(3);
+ _scalar_skew_vertical.setIncrements(0.1, 1.0);
+ _scalar_skew_vertical.set_hexpand();
+ _scalar_skew_vertical.setWidthChars(7);
+
+ _page_skew.table().attach(_scalar_skew_horizontal, 0, 0, 2, 1);
+ _page_skew.table().attach(_units_skew, 2, 0, 1, 1);
+ _page_skew.table().attach(_scalar_skew_vertical, 0, 1, 2, 1);
+
+ _scalar_skew_horizontal.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged));
+ _scalar_skew_vertical.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged));
+
+ //TODO: honour rotation center?
+}
+
+
+void Transformation::layoutPageTransform()
+{
+ _units_transform.setUnitType(UNIT_TYPE_LINEAR);
+ _units_transform.set_tooltip_text(_("E and F units"));
+ _units_transform.set_halign(Gtk::ALIGN_END);
+ _units_transform.set_margin_top(3);
+ _units_transform.set_margin_bottom(3);
+
+ UI::Widget::Scalar* labels[] = {&_scalar_transform_a, &_scalar_transform_b, &_scalar_transform_c, &_scalar_transform_d, &_scalar_transform_e, &_scalar_transform_f};
+ for (auto label : labels) {
+ label->hide_label();
+ label->set_margin_start(2);
+ label->set_margin_end(2);
+ }
+ _page_transform.table().set_column_spacing(0);
+ _page_transform.table().set_row_spacing(1);
+ _page_transform.table().set_column_homogeneous(true);
+
+ _scalar_transform_a.setWidgetSizeRequest(65, -1);
+ _scalar_transform_a.setRange(-1e10, 1e10);
+ _scalar_transform_a.setDigits(3);
+ _scalar_transform_a.setIncrements(0.1, 1.0);
+ _scalar_transform_a.setValue(1.0);
+ _scalar_transform_a.setWidthChars(6);
+ _scalar_transform_a.set_hexpand();
+
+ _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("A:"), 0, 0, 1, 1);
+ _page_transform.table().attach(_scalar_transform_a, 0, 1, 1, 1);
+
+ _scalar_transform_a.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+ _scalar_transform_b.setWidgetSizeRequest(65, -1);
+ _scalar_transform_b.setRange(-1e10, 1e10);
+ _scalar_transform_b.setDigits(3);
+ _scalar_transform_b.setIncrements(0.1, 1.0);
+ _scalar_transform_b.setValue(0.0);
+ _scalar_transform_b.setWidthChars(6);
+ _scalar_transform_b.set_hexpand();
+
+ _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("B:"), 0, 2, 1, 1);
+ _page_transform.table().attach(_scalar_transform_b, 0, 3, 1, 1);
+
+ _scalar_transform_b.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+ _scalar_transform_c.setWidgetSizeRequest(65, -1);
+ _scalar_transform_c.setRange(-1e10, 1e10);
+ _scalar_transform_c.setDigits(3);
+ _scalar_transform_c.setIncrements(0.1, 1.0);
+ _scalar_transform_c.setValue(0.0);
+ _scalar_transform_c.setWidthChars(6);
+ _scalar_transform_c.set_hexpand();
+
+ _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("C:"), 1, 0, 1, 1);
+ _page_transform.table().attach(_scalar_transform_c, 1, 1, 1, 1);
+
+ _scalar_transform_c.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+
+ _scalar_transform_d.setWidgetSizeRequest(65, -1);
+ _scalar_transform_d.setRange(-1e10, 1e10);
+ _scalar_transform_d.setDigits(3);
+ _scalar_transform_d.setIncrements(0.1, 1.0);
+ _scalar_transform_d.setValue(1.0);
+ _scalar_transform_d.setWidthChars(6);
+ _scalar_transform_d.set_hexpand();
+
+ _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("D:"), 1, 2, 1, 1);
+ _page_transform.table().attach(_scalar_transform_d, 1, 3, 1, 1);
+
+ _scalar_transform_d.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+
+ _scalar_transform_e.setWidgetSizeRequest(65, -1);
+ _scalar_transform_e.setRange(-1e10, 1e10);
+ _scalar_transform_e.setDigits(3);
+ _scalar_transform_e.setIncrements(0.1, 1.0);
+ _scalar_transform_e.setValue(0.0);
+ _scalar_transform_e.setWidthChars(6);
+ _scalar_transform_e.set_hexpand();
+
+ _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("E:"), 2, 0, 1, 1);
+ _page_transform.table().attach(_scalar_transform_e, 2, 1, 1, 1);
+
+ _scalar_transform_e.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+
+ _scalar_transform_f.setWidgetSizeRequest(65, -1);
+ _scalar_transform_f.setRange(-1e10, 1e10);
+ _scalar_transform_f.setDigits(3);
+ _scalar_transform_f.setIncrements(0.1, 1.0);
+ _scalar_transform_f.setValue(0.0);
+ _scalar_transform_f.setWidthChars(6);
+ _scalar_transform_f.set_hexpand();
+
+ _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("F:"), 2, 2, 1, 1);
+ _page_transform.table().attach(_scalar_transform_f, 2, 3, 1, 1);
+
+ auto img = Gtk::make_managed<Gtk::Image>();
+ img->set_from_icon_name("matrix-2d", Gtk::ICON_SIZE_BUTTON);
+ img->set_pixel_size(52);
+ img->set_margin_top(4);
+ img->set_margin_bottom(4);
+ _page_transform.table().attach(*img, 0, 5, 1, 1);
+
+ auto descr = Gtk::make_managed<Gtk::Label>();
+ descr->set_line_wrap();
+ descr->set_line_wrap_mode(Pango::WRAP_WORD);
+ descr->set_text(
+ _("<small>"
+ "<a href=\"https://www.w3.org/TR/SVG11/coords.html#TransformMatrixDefined\">"
+ "2D transformation matrix</a> that combines translation (E,F), scaling (A,D),"
+ " rotation (A-D) and shearing (B,C)."
+ "</small>")
+ );
+ descr->set_use_markup();
+ _page_transform.table().attach(*descr, 1, 5, 2, 1);
+
+ _page_transform.table().attach(_units_transform, 2, 4, 1, 1);
+
+ _scalar_transform_f.signal_value_changed()
+ .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
+
+ // Edit existing matrix
+ _page_transform.table().attach(_check_replace_matrix, 0, 4, 2, 1);
+
+ _check_replace_matrix.set_active(false);
+ _check_replace_matrix.signal_toggled()
+ .connect(sigc::mem_fun(*this, &Transformation::onReplaceMatrixToggled));
+}
+
+
+/*########################################################################
+# U P D A T E
+########################################################################*/
+
+void Transformation::updateSelection(PageType page, Inkscape::Selection *selection)
+{
+ applyButton->set_sensitive(selection && !selection->isEmpty());
+
+ if (!selection || selection->isEmpty())
+ return;
+
+ switch (page) {
+ case PAGE_MOVE: {
+ updatePageMove(selection);
+ break;
+ }
+ case PAGE_SCALE: {
+ updatePageScale(selection);
+ break;
+ }
+ case PAGE_ROTATE: {
+ updatePageRotate(selection);
+ break;
+ }
+ case PAGE_SKEW: {
+ updatePageSkew(selection);
+ break;
+ }
+ case PAGE_TRANSFORM: {
+ updatePageTransform(selection);
+ break;
+ }
+ case PAGE_QTY: {
+ break;
+ }
+ }
+}
+
+void Transformation::onSwitchPage(Gtk::Widget * /*page*/, guint pagenum)
+{
+ if (!getDesktop()) {
+ return;
+ }
+
+ updateSelection((PageType)pagenum, getDesktop()->getSelection());
+}
+
+
+void Transformation::updatePageMove(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ if (!_check_move_relative.get_active()) {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ double x = bbox->min()[Geom::X];
+ double y = bbox->min()[Geom::Y];
+
+ double conversion = _units_move.getConversion("px");
+ _scalar_move_horizontal.setValue(x / conversion);
+ _scalar_move_vertical.setValue(y / conversion);
+ }
+ } else {
+ // do nothing, so you can apply the same relative move to many objects in turn
+ }
+ _page_move.set_sensitive(true);
+ } else {
+ _page_move.set_sensitive(false);
+ }
+}
+
+void Transformation::updatePageScale(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ double w = bbox->dimensions()[Geom::X];
+ double h = bbox->dimensions()[Geom::Y];
+ _scalar_scale_horizontal.setHundredPercent(w);
+ _scalar_scale_vertical.setHundredPercent(h);
+ onScaleXValueChanged(); // to update x/y proportionality if switch is on
+ _page_scale.set_sensitive(true);
+ } else {
+ _page_scale.set_sensitive(false);
+ }
+ } else {
+ _page_scale.set_sensitive(false);
+ }
+}
+
+void Transformation::updatePageRotate(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ _page_rotate.set_sensitive(true);
+ } else {
+ _page_rotate.set_sensitive(false);
+ }
+}
+
+void Transformation::updatePageSkew(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ double w = bbox->dimensions()[Geom::X];
+ double h = bbox->dimensions()[Geom::Y];
+ _scalar_skew_vertical.setHundredPercent(w);
+ _scalar_skew_horizontal.setHundredPercent(h);
+ _page_skew.set_sensitive(true);
+ } else {
+ _page_skew.set_sensitive(false);
+ }
+ } else {
+ _page_skew.set_sensitive(false);
+ }
+}
+
+void Transformation::updatePageTransform(Inkscape::Selection *selection)
+{
+ if (selection && !selection->isEmpty()) {
+ if (_check_replace_matrix.get_active()) {
+ Geom::Affine current (selection->items().front()->transform); // take from the first item in selection
+
+ Geom::Affine new_displayed = current;
+
+ _scalar_transform_a.setValue(new_displayed[0]);
+ _scalar_transform_b.setValue(new_displayed[1]);
+ _scalar_transform_c.setValue(new_displayed[2]);
+ _scalar_transform_d.setValue(new_displayed[3]);
+ _scalar_transform_e.setValue(new_displayed[4], "px");
+ _scalar_transform_f.setValue(new_displayed[5], "px");
+ } else {
+ // do nothing, so you can apply the same matrix to many objects in turn
+ }
+ _page_transform.set_sensitive(true);
+ } else {
+ _page_transform.set_sensitive(false);
+ }
+}
+
+
+
+
+
+/*########################################################################
+# A P P L Y
+########################################################################*/
+
+
+
+void Transformation::_apply()
+{
+ auto selection = getSelection();
+ if (!selection || selection->isEmpty())
+ return;
+
+ int const page = _notebook.get_current_page();
+
+ switch (page) {
+ case PAGE_MOVE: {
+ applyPageMove(selection);
+ break;
+ }
+ case PAGE_ROTATE: {
+ applyPageRotate(selection);
+ break;
+ }
+ case PAGE_SCALE: {
+ applyPageScale(selection);
+ break;
+ }
+ case PAGE_SKEW: {
+ applyPageSkew(selection);
+ break;
+ }
+ case PAGE_TRANSFORM: {
+ applyPageTransform(selection);
+ break;
+ }
+ }
+
+ // Let's play with never turning this off
+ applyButton->set_sensitive(false);
+}
+
+void Transformation::applyPageMove(Inkscape::Selection *selection)
+{
+ double x = _scalar_move_horizontal.getValue("px");
+ double y = _scalar_move_vertical.getValue("px");
+ if (_check_move_relative.get_active()) {
+ y *= getDesktop()->yaxisdir();
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs->getBool("/dialogs/transformation/applyseparately")) {
+ // move selection as a whole
+ if (_check_move_relative.get_active()) {
+ selection->moveRelative(x, y);
+ } else {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]);
+ }
+ }
+ } else {
+
+ if (_check_move_relative.get_active()) {
+ // shift each object relatively to the previous one
+ std::vector<SPItem*> selected(selection->items().begin(), selection->items().end());
+ if (selected.empty()) return;
+
+ if (fabs(x) > 1e-6) {
+ std::vector< BBoxSort > sorted;
+ for (auto item : selected)
+ {
+ Geom::OptRect bbox = item->desktopPreferredBounds();
+ if (bbox) {
+ sorted.emplace_back(item, *bbox, Geom::X, x > 0? 1. : 0., x > 0? 0. : 1.);
+ }
+ }
+ //sort bbox by anchors
+ std::stable_sort(sorted.begin(), sorted.end());
+
+ double move = x;
+ for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
+ it < sorted.end();
+ ++it )
+ {
+ it->item->move_rel(Geom::Translate(move, 0));
+ // move each next object by x relative to previous
+ move += x;
+ }
+ }
+ if (fabs(y) > 1e-6) {
+ std::vector< BBoxSort > sorted;
+ for (auto item : selected)
+ {
+ Geom::OptRect bbox = item->desktopPreferredBounds();
+ if (bbox) {
+ sorted.emplace_back(item, *bbox, Geom::Y, y > 0? 1. : 0., y > 0? 0. : 1.);
+ }
+ }
+ //sort bbox by anchors
+ std::stable_sort(sorted.begin(), sorted.end());
+
+ double move = y;
+ for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
+ it < sorted.end();
+ ++it )
+ {
+ it->item->move_rel(Geom::Translate(0, move));
+ // move each next object by x relative to previous
+ move += y;
+ }
+ }
+ } else {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]);
+ }
+ }
+ }
+
+ DocumentUndo::done( selection->desktop()->getDocument(), _("Move"), INKSCAPE_ICON("dialog-transform"));
+}
+
+void Transformation::applyPageScale(Inkscape::Selection *selection)
+{
+ double scaleX = _scalar_scale_horizontal.getValue("px");
+ double scaleY = _scalar_scale_vertical.getValue("px");
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool transform_stroke = prefs->getBool("/options/transform/stroke", true);
+ bool preserve = prefs->getBool("/options/preservetransform/value", false);
+ if (prefs->getBool("/dialogs/transformation/applyseparately")) {
+ auto tmp= selection->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i){
+ SPItem *item = *i;
+ Geom::OptRect bbox_pref = item->desktopPreferredBounds();
+ Geom::OptRect bbox_geom = item->desktopGeometricBounds();
+ if (bbox_pref && bbox_geom) {
+ double new_width = scaleX;
+ double new_height = scaleY;
+ // the values are increments!
+ if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent
+ new_width = scaleX/100 * bbox_pref->width();
+ new_height = scaleY/100 * bbox_pref->height();
+ }
+ if (fabs(new_width) < 1e-6) new_width = 1e-6; // not 0, as this would result in a nasty no-bbox object
+ if (fabs(new_height) < 1e-6) new_height = 1e-6;
+
+ double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2;
+ double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2;
+ double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2;
+ double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2;
+
+ Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1);
+ item->set_i2d_affine(item->i2dt_affine() * scaler);
+ item->doWriteTransform(item->transform);
+ }
+ }
+ } else {
+ Geom::OptRect bbox_pref = selection->preferredBounds();
+ Geom::OptRect bbox_geom = selection->geometricBounds();
+ if (bbox_pref && bbox_geom) {
+ // the values are increments!
+ double new_width = scaleX;
+ double new_height = scaleY;
+ if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent
+ new_width = scaleX/100 * bbox_pref->width();
+ new_height = scaleY/100 * bbox_pref->height();
+ }
+ if (fabs(new_width) < 1e-6) new_width = 1e-6;
+ if (fabs(new_height) < 1e-6) new_height = 1e-6;
+
+ double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2;
+ double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2;
+ double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2;
+ double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2;
+ Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1);
+
+ selection->applyAffine(scaler);
+ }
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), _("Scale"), INKSCAPE_ICON("dialog-transform"));
+}
+
+void Transformation::applyPageRotate(Inkscape::Selection *selection)
+{
+ double angle = _scalar_rotate.getValue(DEG);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs->getBool("/dialogs/transformation/rotateCounterClockwise", TRUE)) {
+ angle *= -1;
+ }
+
+ if (prefs->getBool("/dialogs/transformation/applyseparately")) {
+ auto tmp= selection->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i){
+ SPItem *item = *i;
+ item->rotate_rel(Geom::Rotate (angle*M_PI/180.0));
+ }
+ } else {
+ std::optional<Geom::Point> center = selection->center();
+ if (center) {
+ selection->rotateRelative(*center, angle);
+ }
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), _("Rotate"), INKSCAPE_ICON("dialog-transform"));
+}
+
+void Transformation::applyPageSkew(Inkscape::Selection *selection)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/dialogs/transformation/applyseparately")) {
+ auto items = selection->items();
+ for(auto i = items.begin();i!=items.end();++i){
+ SPItem *item = *i;
+
+ if (!_units_skew.isAbsolute()) { // percentage
+ double skewX = _scalar_skew_horizontal.getValue("%");
+ double skewY = _scalar_skew_vertical.getValue("%");
+ skewY *= getDesktop()->yaxisdir();
+ if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ item->skew_rel(0.01*skewX, 0.01*skewY);
+ } else if (_units_skew.isRadial()) { //deg or rad
+ double angleX = _scalar_skew_horizontal.getValue("rad");
+ double angleY = _scalar_skew_vertical.getValue("rad");
+ if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON)
+ || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON)
+ || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON)
+ || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ double skewX = tan(angleX);
+ double skewY = tan(angleY);
+ skewX *= getDesktop()->yaxisdir();
+ skewY *= getDesktop()->yaxisdir();
+ item->skew_rel(skewX, skewY);
+ } else { // absolute displacement
+ double skewX = _scalar_skew_horizontal.getValue("px");
+ double skewY = _scalar_skew_vertical.getValue("px");
+ skewY *= getDesktop()->yaxisdir();
+ Geom::OptRect bbox = item->desktopPreferredBounds();
+ if (bbox) {
+ double width = bbox->dimensions()[Geom::X];
+ double height = bbox->dimensions()[Geom::Y];
+ if (fabs(skewX*skewY - width*height) < Geom::EPSILON) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ item->skew_rel(skewX/height, skewY/width);
+ }
+ }
+ }
+ } else { // transform whole selection
+ Geom::OptRect bbox = selection->preferredBounds();
+ std::optional<Geom::Point> center = selection->center();
+
+ if ( bbox && center ) {
+ double width = bbox->dimensions()[Geom::X];
+ double height = bbox->dimensions()[Geom::Y];
+
+ if (!_units_skew.isAbsolute()) { // percentage
+ double skewX = _scalar_skew_horizontal.getValue("%");
+ double skewY = _scalar_skew_vertical.getValue("%");
+ skewY *= getDesktop()->yaxisdir();
+ if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ selection->skewRelative(*center, 0.01 * skewX, 0.01 * skewY);
+ } else if (_units_skew.isRadial()) { //deg or rad
+ double angleX = _scalar_skew_horizontal.getValue("rad");
+ double angleY = _scalar_skew_vertical.getValue("rad");
+ if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON)
+ || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON)
+ || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON)
+ || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ double skewX = tan(angleX);
+ double skewY = tan(angleY);
+ skewX *= getDesktop()->yaxisdir();
+ skewY *= getDesktop()->yaxisdir();
+ selection->skewRelative(*center, skewX, skewY);
+ } else { // absolute displacement
+ double skewX = _scalar_skew_horizontal.getValue("px");
+ double skewY = _scalar_skew_vertical.getValue("px");
+ skewY *= getDesktop()->yaxisdir();
+ if (fabs(skewX*skewY - width*height) < Geom::EPSILON) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+ selection->skewRelative(*center, skewX / height, skewY / width);
+ }
+ }
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), _("Skew"), INKSCAPE_ICON("dialog-transform"));
+}
+
+
+void Transformation::applyPageTransform(Inkscape::Selection *selection)
+{
+ double a = _scalar_transform_a.getValue();
+ double b = _scalar_transform_b.getValue();
+ double c = _scalar_transform_c.getValue();
+ double d = _scalar_transform_d.getValue();
+ double e = _scalar_transform_e.getValue("px");
+ double f = _scalar_transform_f.getValue("px");
+
+ Geom::Affine displayed(a, b, c, d, e, f);
+ if (displayed.isSingular()) {
+ getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
+ return;
+ }
+
+ if (_check_replace_matrix.get_active()) {
+ auto tmp = selection->items();
+ for(auto i=tmp.begin();i!=tmp.end();++i){
+ SPItem *item = *i;
+ item->set_item_transform(displayed);
+ item->updateRepr();
+ }
+ } else {
+ selection->applyAffine(displayed); // post-multiply each object's transform
+ }
+
+ DocumentUndo::done(selection->desktop()->getDocument(), _("Edit transformation matrix"), INKSCAPE_ICON("dialog-transform"));
+}
+
+
+
+
+
+/*########################################################################
+# V A L U E - C H A N G E D C A L L B A C K S
+########################################################################*/
+
+void Transformation::onMoveValueChanged()
+{
+ applyButton->set_sensitive(true);
+}
+
+void Transformation::onMoveRelativeToggled()
+{
+ auto selection = getSelection();
+ if (!selection || selection->isEmpty())
+ return;
+
+ double x = _scalar_move_horizontal.getValue("px");
+ double y = _scalar_move_vertical.getValue("px");
+
+ double conversion = _units_move.getConversion("px");
+
+ //g_message("onMoveRelativeToggled: %f, %f px\n", x, y);
+
+ Geom::OptRect bbox = selection->preferredBounds();
+
+ if (bbox) {
+ if (_check_move_relative.get_active()) {
+ // From absolute to relative
+ _scalar_move_horizontal.setValue((x - bbox->min()[Geom::X]) / conversion);
+ _scalar_move_vertical.setValue(( y - bbox->min()[Geom::Y]) / conversion);
+ } else {
+ // From relative to absolute
+ _scalar_move_horizontal.setValue((bbox->min()[Geom::X] + x) / conversion);
+ _scalar_move_vertical.setValue(( bbox->min()[Geom::Y] + y) / conversion);
+ }
+ }
+
+ applyButton->set_sensitive(true);
+}
+
+void Transformation::onScaleXValueChanged()
+{
+ if (_scalar_scale_horizontal.setProgrammatically) {
+ _scalar_scale_horizontal.setProgrammatically = false;
+ return;
+ }
+
+ applyButton->set_sensitive(true);
+
+ if (_check_scale_proportional.get_active()) {
+ if (!_units_scale.isAbsolute()) { // percentage, just copy over
+ _scalar_scale_vertical.setValue(_scalar_scale_horizontal.getValue("%"));
+ } else {
+ double scaleXPercentage = _scalar_scale_horizontal.getAsPercentage();
+ _scalar_scale_vertical.setFromPercentage (scaleXPercentage);
+ }
+ }
+}
+
+void Transformation::onScaleYValueChanged()
+{
+ if (_scalar_scale_vertical.setProgrammatically) {
+ _scalar_scale_vertical.setProgrammatically = false;
+ return;
+ }
+
+ applyButton->set_sensitive(true);
+
+ if (_check_scale_proportional.get_active()) {
+ if (!_units_scale.isAbsolute()) { // percentage, just copy over
+ _scalar_scale_horizontal.setValue(_scalar_scale_vertical.getValue("%"));
+ } else {
+ double scaleYPercentage = _scalar_scale_vertical.getAsPercentage();
+ _scalar_scale_horizontal.setFromPercentage (scaleYPercentage);
+ }
+ }
+}
+
+void Transformation::onRotateValueChanged()
+{
+ applyButton->set_sensitive(true);
+}
+
+void Transformation::onRotateCounterclockwiseClicked()
+{
+ _scalar_rotate.setTooltipText(_("Rotation angle (positive = counterclockwise)"));
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/transformation/rotateCounterClockwise", !getDesktop()->is_yaxisdown());
+}
+
+void Transformation::onRotateClockwiseClicked()
+{
+ _scalar_rotate.setTooltipText(_("Rotation angle (positive = clockwise)"));
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/transformation/rotateCounterClockwise", getDesktop()->is_yaxisdown());
+}
+
+void Transformation::onSkewValueChanged()
+{
+ applyButton->set_sensitive(true);
+}
+
+void Transformation::onTransformValueChanged()
+{
+
+ /*
+ double a = _scalar_transform_a.getValue();
+ double b = _scalar_transform_b.getValue();
+ double c = _scalar_transform_c.getValue();
+ double d = _scalar_transform_d.getValue();
+ double e = _scalar_transform_e.getValue();
+ double f = _scalar_transform_f.getValue();
+
+ //g_message("onTransformValueChanged: (%f, %f, %f, %f, %f, %f)\n",
+ // a, b, c, d, e ,f);
+ */
+
+ applyButton->set_sensitive(true);
+}
+
+void Transformation::onReplaceMatrixToggled()
+{
+ auto selection = getSelection();
+ if (!selection || selection->isEmpty())
+ return;
+
+ double a = _scalar_transform_a.getValue();
+ double b = _scalar_transform_b.getValue();
+ double c = _scalar_transform_c.getValue();
+ double d = _scalar_transform_d.getValue();
+ double e = _scalar_transform_e.getValue("px");
+ double f = _scalar_transform_f.getValue("px");
+
+ Geom::Affine displayed (a, b, c, d, e, f);
+ Geom::Affine current = selection->items().front()->transform; // take from the first item in selection
+
+ Geom::Affine new_displayed;
+ if (_check_replace_matrix.get_active()) {
+ new_displayed = current;
+ } else {
+ new_displayed = current.inverse() * displayed;
+ }
+
+ _scalar_transform_a.setValue(new_displayed[0]);
+ _scalar_transform_b.setValue(new_displayed[1]);
+ _scalar_transform_c.setValue(new_displayed[2]);
+ _scalar_transform_d.setValue(new_displayed[3]);
+ _scalar_transform_e.setValue(new_displayed[4], "px");
+ _scalar_transform_f.setValue(new_displayed[5], "px");
+}
+
+void Transformation::onScaleProportionalToggled()
+{
+ onScaleXValueChanged();
+ if (_scalar_scale_vertical.setProgrammatically) {
+ _scalar_scale_vertical.setProgrammatically = false;
+ }
+}
+
+
+void Transformation::onClear()
+{
+ int const page = _notebook.get_current_page();
+
+ switch (page) {
+ case PAGE_MOVE: {
+ auto selection = getSelection();
+ if (!selection || selection->isEmpty() || _check_move_relative.get_active()) {
+ _scalar_move_horizontal.setValue(0);
+ _scalar_move_vertical.setValue(0);
+ } else {
+ Geom::OptRect bbox = selection->preferredBounds();
+ if (bbox) {
+ _scalar_move_horizontal.setValue(bbox->min()[Geom::X], "px");
+ _scalar_move_vertical.setValue(bbox->min()[Geom::Y], "px");
+ }
+ }
+ break;
+ }
+ case PAGE_ROTATE: {
+ _scalar_rotate.setValue(0);
+ break;
+ }
+ case PAGE_SCALE: {
+ _scalar_scale_horizontal.setValue(100, "%");
+ _scalar_scale_vertical.setValue(100, "%");
+ break;
+ }
+ case PAGE_SKEW: {
+ _scalar_skew_horizontal.setValue(0);
+ _scalar_skew_vertical.setValue(0);
+ break;
+ }
+ case PAGE_TRANSFORM: {
+ _scalar_transform_a.setValue(1);
+ _scalar_transform_b.setValue(0);
+ _scalar_transform_c.setValue(0);
+ _scalar_transform_d.setValue(1);
+ _scalar_transform_e.setValue(0, "px");
+ _scalar_transform_f.setValue(0, "px");
+ break;
+ }
+ }
+}
+
+void Transformation::onApplySeparatelyToggled()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setBool("/dialogs/transformation/applyseparately", _check_apply_separately.get_active());
+}
+
+void Transformation::desktopReplaced()
+{
+ // Setting default unit to document unit
+ if (auto desktop = getDesktop()) {
+ SPNamedView *nv = desktop->getNamedView();
+ if (nv->display_units) {
+ _units_move.setUnit(nv->display_units->abbr);
+ _units_transform.setUnit(nv->display_units->abbr);
+ }
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/dialogs/transformation/rotateCounterClockwise", true) != desktop->is_yaxisdown()) {
+ _counterclockwise_rotate.set_active();
+ onRotateCounterclockwiseClicked();
+ } else {
+ _clockwise_rotate.set_active();
+ onRotateClockwiseClicked();
+ }
+
+ updateSelection(PAGE_MOVE, getSelection());
+ }
+}
+
+} // 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/dialog/transformation.h b/src/ui/dialog/transformation.h
new file mode 100644
index 0000000..457cee0
--- /dev/null
+++ b/src/ui/dialog/transformation.h
@@ -0,0 +1,231 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Transform dialog
+ */
+/* Author:
+ * Bryce W. Harrington <bryce@bryceharrington.org>
+ *
+ * Copyright (C) 2004, 2005 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_TRANSFORMATION_H
+#define INKSCAPE_UI_DIALOG_TRANSFORMATION_H
+
+#include <glibmm/i18n.h>
+#include <gtkmm/checkbutton.h>
+#include <gtkmm/notebook.h>
+#include <gtkmm/radiobutton.h>
+
+#include "ui/dialog/dialog-base.h"
+#include "ui/widget/notebook-page.h"
+#include "ui/widget/scalar-unit.h"
+
+namespace Gtk {
+class Button;
+}
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/**
+ * Transformation dialog.
+ *
+ * The transformation dialog allows to modify Inkscape objects.
+ * 5 transformation operations are currently possible: move, scale,
+ * rotate, skew and matrix.
+ */
+class Transformation : public DialogBase
+{
+public:
+ /**
+ * Constructor for Transformation.
+ *
+ * This does the initialization
+ * and layout of the dialog used for transforming SVG objects. It
+ * consists of 5 pages for the 5 operations it handles:
+ * 'Move' allows x,y translation of SVG objects
+ * 'Scale' allows linear resizing of SVG objects
+ * 'Rotate' allows rotating SVG objects by a degree
+ * 'Skew' allows skewing SVG objects
+ * 'Matrix' allows applying a generic affine transform on SVG objects,
+ * with the user specifying the 6 degrees of freedom manually.
+ *
+ * The dialog is implemented as a Gtk::Notebook with five pages.
+ * The pages are implemented using Inkscape's NotebookPage which
+ * is used to help make sure all of Inkscape's notebooks follow
+ * the same style. We then populate the pages with our widgets,
+ * we use the ScalarUnit class for this.
+ */
+ Transformation();
+
+ /**
+ * Cleanup
+ */
+ ~Transformation() override;
+
+ /**
+ * Show the Move panel
+ */
+ void setPageMove()
+ { presentPage(PAGE_MOVE); }
+
+
+ /**
+ * Show the Scale panel
+ */
+ void setPageScale()
+ { presentPage(PAGE_SCALE); }
+
+
+ /**
+ * Show the Rotate panel
+ */
+ void setPageRotate()
+ { presentPage(PAGE_ROTATE); }
+
+ /**
+ * Show the Skew panel
+ */
+ void setPageSkew()
+ { presentPage(PAGE_SKEW); }
+
+ /**
+ * Show the Transform panel
+ */
+ void setPageTransform()
+ { presentPage(PAGE_TRANSFORM); }
+
+
+ int getCurrentPage()
+ { return _notebook.get_current_page(); }
+
+ enum PageType {
+ PAGE_MOVE, PAGE_SCALE, PAGE_ROTATE, PAGE_SKEW, PAGE_TRANSFORM, PAGE_QTY
+ };
+
+ void desktopReplaced() override;
+ void selectionChanged(Inkscape::Selection *selection) override;
+ void selectionModified(Inkscape::Selection *selection, guint flags) override;
+ void updateSelection(PageType page, Inkscape::Selection *selection);
+
+protected:
+
+ Gtk::Notebook _notebook;
+
+ UI::Widget::NotebookPage _page_move;
+ UI::Widget::NotebookPage _page_scale;
+ UI::Widget::NotebookPage _page_rotate;
+ UI::Widget::NotebookPage _page_skew;
+ UI::Widget::NotebookPage _page_transform;
+
+ UI::Widget::UnitMenu _units_move;
+ UI::Widget::UnitMenu _units_scale;
+ UI::Widget::UnitMenu _units_rotate;
+ UI::Widget::UnitMenu _units_skew;
+ UI::Widget::UnitMenu _units_transform;
+
+ UI::Widget::ScalarUnit _scalar_move_horizontal;
+ UI::Widget::ScalarUnit _scalar_move_vertical;
+ UI::Widget::ScalarUnit _scalar_scale_horizontal;
+ UI::Widget::ScalarUnit _scalar_scale_vertical;
+ UI::Widget::ScalarUnit _scalar_rotate;
+ UI::Widget::ScalarUnit _scalar_skew_horizontal;
+ UI::Widget::ScalarUnit _scalar_skew_vertical;
+
+ UI::Widget::Scalar _scalar_transform_a;
+ UI::Widget::Scalar _scalar_transform_b;
+ UI::Widget::Scalar _scalar_transform_c;
+ UI::Widget::Scalar _scalar_transform_d;
+ UI::Widget::ScalarUnit _scalar_transform_e;
+ UI::Widget::ScalarUnit _scalar_transform_f;
+
+ Gtk::RadioButton _counterclockwise_rotate;
+ Gtk::RadioButton _clockwise_rotate;
+
+ Gtk::CheckButton _check_move_relative;
+ Gtk::CheckButton _check_scale_proportional;
+ Gtk::CheckButton _check_apply_separately;
+ Gtk::CheckButton _check_replace_matrix;
+
+ /**
+ * Layout the GUI components, and prepare for use
+ */
+ void layoutPageMove();
+ void layoutPageScale();
+ void layoutPageRotate();
+ void layoutPageSkew();
+ void layoutPageTransform();
+
+ void _apply();
+ void presentPage(PageType page);
+
+ void onSwitchPage(Gtk::Widget *page, guint pagenum);
+
+ /**
+ * Callbacks for when a user changes values on the panels
+ */
+ void onMoveValueChanged();
+ void onMoveRelativeToggled();
+ void onScaleXValueChanged();
+ void onScaleYValueChanged();
+ void onRotateValueChanged();
+ void onRotateCounterclockwiseClicked();
+ void onRotateClockwiseClicked();
+ void onSkewValueChanged();
+ void onTransformValueChanged();
+ void onReplaceMatrixToggled();
+ void onScaleProportionalToggled();
+
+ void onClear();
+
+ void onApplySeparatelyToggled();
+
+ /**
+ * Called when the selection is updated, to make
+ * the panel(s) show the new values.
+ * Editor---->dialog
+ */
+ void updatePageMove(Inkscape::Selection *);
+ void updatePageScale(Inkscape::Selection *);
+ void updatePageRotate(Inkscape::Selection *);
+ void updatePageSkew(Inkscape::Selection *);
+ void updatePageTransform(Inkscape::Selection *);
+
+ /**
+ * Called when the Apply button is pushed
+ * Dialog---->editor
+ */
+ void applyPageMove(Inkscape::Selection *);
+ void applyPageScale(Inkscape::Selection *);
+ void applyPageRotate(Inkscape::Selection *);
+ void applyPageSkew(Inkscape::Selection *);
+ void applyPageTransform(Inkscape::Selection *);
+
+private:
+ Gtk::Button *applyButton;
+ Gtk::Button *resetButton;
+
+ sigc::connection _selChangeConn;
+ sigc::connection _selModifyConn;
+ sigc::connection _tabSwitchConn;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif //INKSCAPE_UI_DIALOG_TRANSFORMATION_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/dialog/undo-history.cpp b/src/ui/dialog/undo-history.cpp
new file mode 100644
index 0000000..335ec68
--- /dev/null
+++ b/src/ui/dialog/undo-history.cpp
@@ -0,0 +1,356 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Undo History dialog - implementation.
+ */
+/* Author:
+ * Gustav Broberg <broberg@kth.se>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "undo-history.h"
+
+#include "actions/actions-tools.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "ui/icon-loader.h"
+#include "util/signal-blocker.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+/* Rendering functions for custom cell renderers */
+void CellRendererSPIcon::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags)
+{
+ // if there is no icon name.
+ if ( _property_icon_name == "") return;
+
+ // if the icon isn't cached, render it to a pixbuf
+ if ( !_icon_cache[_property_icon_name] ) {
+
+ Gtk::Image* icon = Gtk::manage(new Gtk::Image());
+ icon = sp_get_icon_image(_property_icon_name, Gtk::ICON_SIZE_MENU);
+
+ if (icon) {
+
+ // check icon type (inkscape, gtk, none)
+ if ( GTK_IS_IMAGE(icon->gobj()) ) {
+ _property_icon = sp_get_icon_pixbuf(_property_icon_name, 16);
+ } else {
+ delete icon;
+ return;
+ }
+
+ delete icon;
+ property_pixbuf() = _icon_cache[_property_icon_name] = _property_icon.get_value();
+ }
+
+ } else {
+ property_pixbuf() = _icon_cache[_property_icon_name];
+ }
+
+ Gtk::CellRendererPixbuf::render_vfunc(cr, widget, background_area,
+ cell_area, flags);
+}
+
+
+void CellRendererInt::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags)
+{
+ if( _filter(_property_number) ) {
+ std::ostringstream s;
+ s << _property_number << std::flush;
+ property_text() = s.str();
+ Gtk::CellRendererText::render_vfunc(cr, widget, background_area,
+ cell_area, flags);
+ }
+}
+
+const CellRendererInt::Filter& CellRendererInt::no_filter = CellRendererInt::NoFilter();
+
+UndoHistory::UndoHistory()
+ : DialogBase("/dialogs/undo-history", "UndoHistory"),
+ _event_log(nullptr),
+ _scrolled_window(),
+ _event_list_store(),
+ _event_list_selection(_event_list_view.get_selection()),
+ _callback_connections()
+{
+ auto *_columns = &EventLog::getColumns();
+
+ set_size_request(-1, -1);
+
+ pack_start(_scrolled_window);
+ _scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+
+ _event_list_view.set_enable_search(false);
+ _event_list_view.set_headers_visible(false);
+
+ CellRendererSPIcon* icon_renderer = Gtk::manage(new CellRendererSPIcon());
+ icon_renderer->property_xpad() = 2;
+ icon_renderer->property_width() = 24;
+ int cols_count = _event_list_view.append_column("Icon", *icon_renderer);
+
+ Gtk::TreeView::Column* icon_column = _event_list_view.get_column(cols_count-1);
+ icon_column->add_attribute(icon_renderer->property_icon_name(), _columns->icon_name);
+
+ CellRendererInt* children_renderer = Gtk::manage(new CellRendererInt(greater_than_1));
+ children_renderer->property_weight() = 600; // =Pango::WEIGHT_SEMIBOLD (not defined in old versions of pangomm)
+ children_renderer->property_xalign() = 1.0;
+ children_renderer->property_xpad() = 2;
+ children_renderer->property_width() = 24;
+
+ cols_count = _event_list_view.append_column("Children", *children_renderer);
+ Gtk::TreeView::Column* children_column = _event_list_view.get_column(cols_count-1);
+ children_column->add_attribute(children_renderer->property_number(), _columns->child_count);
+
+ Gtk::CellRendererText* description_renderer = Gtk::manage(new Gtk::CellRendererText());
+ description_renderer->property_ellipsize() = Pango::ELLIPSIZE_END;
+
+ cols_count = _event_list_view.append_column("Description", *description_renderer);
+ Gtk::TreeView::Column* description_column = _event_list_view.get_column(cols_count-1);
+ description_column->add_attribute(description_renderer->property_text(), _columns->description);
+ description_column->set_resizable();
+ description_column->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE);
+ description_column->set_min_width (150);
+
+ _event_list_view.set_expander_column( *_event_list_view.get_column(cols_count-1) );
+
+ _scrolled_window.add(_event_list_view);
+ _scrolled_window.set_overlay_scrolling(false);
+ // connect EventLog callbacks
+ _callback_connections[EventLog::CALLB_SELECTION_CHANGE] =
+ _event_list_selection->signal_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onListSelectionChange));
+
+ _callback_connections[EventLog::CALLB_EXPAND] =
+ _event_list_view.signal_row_expanded().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onExpandEvent));
+
+ _callback_connections[EventLog::CALLB_COLLAPSE] =
+ _event_list_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onCollapseEvent));
+
+ show_all_children();
+}
+
+UndoHistory::~UndoHistory()
+{
+ disconnectEventLog();
+}
+
+void UndoHistory::documentReplaced()
+{
+ disconnectEventLog();
+ if (auto document = getDocument()) {
+ g_assert (document->get_event_log() != nullptr);
+ SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]);
+ _event_list_view.unset_model();
+ connectEventLog();
+ }
+}
+
+void UndoHistory::disconnectEventLog()
+{
+ if (_event_log) {
+ _event_log->removeDialogConnection(&_event_list_view, &_callback_connections);
+ _event_log->remove_destroy_notify_callback(this);
+ }
+}
+
+void UndoHistory::connectEventLog()
+{
+ if (auto document = getDocument()) {
+ _event_log = document->get_event_log();
+ _event_log->add_destroy_notify_callback(this, &_handleEventLogDestroyCB);
+ _event_list_store = _event_log->getEventListStore();
+ _event_list_view.set_model(_event_list_store);
+ _event_log->addDialogConnection(&_event_list_view, &_callback_connections);
+ _event_list_view.scroll_to_row(_event_list_store->get_path(_event_list_selection->get_selected()));
+ }
+}
+
+void *UndoHistory::_handleEventLogDestroyCB(void *data)
+{
+ void *result = nullptr;
+ if (data) {
+ UndoHistory *self = reinterpret_cast<UndoHistory*>(data);
+ result = self->_handleEventLogDestroy();
+ }
+ return result;
+}
+
+// called *after* _event_log has been destroyed.
+void *UndoHistory::_handleEventLogDestroy()
+{
+ if (_event_log) {
+ SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]);
+
+ _event_list_view.unset_model();
+ _event_list_store.reset();
+ _event_log = nullptr;
+ }
+
+ return nullptr;
+}
+
+void
+UndoHistory::_onListSelectionChange()
+{
+
+ EventLog::const_iterator selected = _event_list_selection->get_selected();
+
+ /* If no event is selected in the view, find the right one and select it. This happens whenever
+ * a branch we're currently in is collapsed.
+ */
+ if (!selected) {
+ EventLog::iterator curr_event = _event_log->getCurrEvent();
+
+ if (curr_event->parent()) {
+
+ EventLog::iterator curr_event_parent = curr_event->parent();
+ EventLog::iterator last = curr_event_parent->children().end();
+
+ _event_log->blockNotifications();
+ for ( --last ; curr_event != last ; ++curr_event ) {
+ DocumentUndo::redo(getDocument());
+ }
+ _event_log->blockNotifications(false);
+
+ _event_log->setCurrEvent(curr_event);
+ _event_list_selection->select(curr_event_parent);
+
+ } else { // this should not happen
+ _event_list_selection->select(curr_event);
+ }
+
+ } else {
+
+ EventLog::const_iterator last_selected = _event_log->getCurrEvent();
+
+ /* Selecting a collapsed parent event is equal to selecting the last child
+ * of that parent's branch.
+ */
+
+ if ( !selected->children().empty() &&
+ !_event_list_view.row_expanded(_event_list_store->get_path(selected)) )
+ {
+ selected = selected->children().end();
+ --selected;
+ }
+
+ // An event before the current one has been selected. Undo to the selected event.
+ if ( _event_list_store->get_path(selected) <
+ _event_list_store->get_path(last_selected) )
+ {
+ _event_log->blockNotifications();
+
+ while ( selected != last_selected ) {
+
+ DocumentUndo::undo(getDocument());
+
+ if ( last_selected->parent() &&
+ last_selected == last_selected->parent()->children().begin() )
+ {
+ last_selected = last_selected->parent();
+ _event_log->setCurrEventParent((EventLog::iterator)nullptr);
+ } else {
+ --last_selected;
+ if ( !last_selected->children().empty() ) {
+ _event_log->setCurrEventParent(last_selected);
+ last_selected = last_selected->children().end();
+ --last_selected;
+ }
+ }
+ }
+ _event_log->blockNotifications(false);
+ _event_log->updateUndoVerbs();
+
+ } else { // An event after the current one has been selected. Redo to the selected event.
+
+ _event_log->blockNotifications();
+
+ while (last_selected && selected != last_selected ) {
+
+ DocumentUndo::redo(getDocument());
+
+ if ( !last_selected->children().empty() ) {
+ _event_log->setCurrEventParent(last_selected);
+ last_selected = last_selected->children().begin();
+ } else {
+ ++last_selected;
+ if ( last_selected->parent() &&
+ last_selected == last_selected->parent()->children().end() )
+ {
+ last_selected = last_selected->parent();
+ ++last_selected;
+ _event_log->setCurrEventParent((EventLog::iterator)nullptr);
+ }
+ }
+ }
+ _event_log->blockNotifications(false);
+
+ }
+
+ _event_log->setCurrEvent(selected);
+ _event_log->updateUndoVerbs();
+ }
+}
+
+void
+UndoHistory::_onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/)
+{
+ if ( iter == _event_list_selection->get_selected() ) {
+ _event_list_selection->select(_event_log->getCurrEvent());
+ }
+}
+
+void
+UndoHistory::_onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/)
+{
+ // Collapsing a branch we're currently in is equal to stepping to the last event in that branch
+ if ( iter == _event_log->getCurrEvent() ) {
+ EventLog::const_iterator curr_event_parent = _event_log->getCurrEvent();
+ EventLog::const_iterator curr_event = curr_event_parent->children().begin();
+ EventLog::const_iterator last = curr_event_parent->children().end();
+
+ _event_log->blockNotifications();
+ DocumentUndo::redo(getDocument());
+
+ for ( --last ; curr_event != last ; ++curr_event ) {
+ DocumentUndo::redo(getDocument());
+ }
+ _event_log->blockNotifications(false);
+
+ _event_log->setCurrEvent(curr_event);
+ _event_log->setCurrEventParent(curr_event_parent);
+ _event_list_selection->select(curr_event_parent);
+ }
+}
+
+const CellRendererInt::Filter& UndoHistory::greater_than_1 = UndoHistory::GreaterThan(1);
+
+} // 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/dialog/undo-history.h b/src/ui/dialog/undo-history.h
new file mode 100644
index 0000000..43c99cd
--- /dev/null
+++ b/src/ui/dialog/undo-history.h
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Undo History dialog
+ */
+/* Author:
+ * Gustav Broberg <broberg@kth.se>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_UNDO_HISTORY_H
+#define INKSCAPE_UI_DIALOG_UNDO_HISTORY_H
+
+#include <functional>
+#include <glibmm/property.h>
+#include <gtkmm/cellrendererpixbuf.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/treemodel.h>
+#include <gtkmm/treeselection.h>
+#include <sstream>
+
+#include "event-log.h"
+#include "ui/dialog/dialog-base.h"
+
+
+namespace Inkscape {
+namespace UI {
+namespace Dialog {
+
+
+/* Custom cell renderers */
+
+class CellRendererSPIcon : public Gtk::CellRendererPixbuf {
+public:
+
+ CellRendererSPIcon()
+ : Glib::ObjectBase(typeid(CellRendererPixbuf))
+ , Gtk::CellRendererPixbuf()
+ , _property_icon(*this, "icon", Glib::RefPtr<Gdk::Pixbuf>(nullptr))
+ , _property_icon_name(*this, "our-icon-name", "inkscape-logo") // icon-name/icon_name used by Gtk
+ { }
+
+ Glib::PropertyProxy<Glib::ustring>
+ property_icon_name() { return _property_icon_name.get_proxy(); }
+
+protected:
+ void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
+ Gtk::Widget& widget,
+ const Gdk::Rectangle& background_area,
+ const Gdk::Rectangle& cell_area,
+ Gtk::CellRendererState flags) override;
+private:
+
+ Glib::Property<Glib::RefPtr<Gdk::Pixbuf> > _property_icon;
+ Glib::Property<Glib::ustring> _property_icon_name;
+ std::map<Glib::ustring, Glib::RefPtr<Gdk::Pixbuf> > _icon_cache;
+
+};
+
+
+class CellRendererInt : public Gtk::CellRendererText {
+public:
+
+ struct Filter
+ {
+ virtual ~Filter() = default;
+ virtual bool operator() (const int&) const =0;
+ };
+
+ CellRendererInt(const Filter& filter=no_filter) :
+ Glib::ObjectBase(typeid(CellRendererText)),
+ Gtk::CellRendererText(),
+ _property_number(*this, "number", 0),
+ _filter (filter)
+ { }
+
+
+ Glib::PropertyProxy<int>
+ property_number() { return _property_number.get_proxy(); }
+
+ static const Filter& no_filter;
+
+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;
+
+private:
+
+ Glib::Property<int> _property_number;
+ const Filter& _filter;
+
+ struct NoFilter : Filter { bool operator() (const int& /*x*/) const override { return true; } };
+};
+
+/**
+ * \brief Dialog for presenting document change history
+ *
+ * This dialog allows the user to undo and redo multiple events in a more convenient way
+ * than repateaded ctrl-z, ctrl-shift-z.
+ */
+class UndoHistory : public DialogBase
+{
+public:
+ UndoHistory();
+ ~UndoHistory() override;
+
+ void documentReplaced() override;
+
+protected:
+ EventLog *_event_log;
+
+ Gtk::ScrolledWindow _scrolled_window;
+
+ Glib::RefPtr<Gtk::TreeModel> _event_list_store;
+ Gtk::TreeView _event_list_view;
+ Glib::RefPtr<Gtk::TreeSelection> _event_list_selection;
+
+ EventLog::CallbackMap _callback_connections;
+
+ static void *_handleEventLogDestroyCB(void *data);
+
+ void disconnectEventLog();
+ void connectEventLog();
+
+ void *_handleEventLogDestroy();
+ void _onListSelectionChange();
+ void _onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+ void _onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path);
+
+private:
+ struct GreaterThan : CellRendererInt::Filter
+ {
+ GreaterThan(int _i) : i (_i) {}
+ bool operator() (const int& x) const override { return x > i; }
+ int i;
+ };
+
+ static const CellRendererInt::Filter& greater_than_1;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif //INKSCAPE_UI_DIALOG_UNDO_HISTORY_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/dialog/xml-tree.cpp b/src/ui/dialog/xml-tree.cpp
new file mode 100644
index 0000000..242a5d4
--- /dev/null
+++ b/src/ui/dialog/xml-tree.cpp
@@ -0,0 +1,928 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * XML editor.
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * MenTaLguY <mental@rydia.net>
+ * bulia byak <buliabyak@users.sf.net>
+ * Johan Engelen <goejendaagh@zonnet.nl>
+ * David Turner
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Mike Kowalski
+ *
+ * Copyright (C) 1999-2006 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ *
+ */
+
+#include "xml-tree.h"
+
+#include <glibmm/i18n.h>
+#include <glibmm/ustring.h>
+#include <gtkmm/button.h>
+#include <gtkmm/enums.h>
+#include <gtkmm/image.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/object.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/radiomenuitem.h>
+#include <gtkmm/scrolledwindow.h>
+#include <memory>
+
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "inkscape.h"
+#include "layer-manager.h"
+#include "message-context.h"
+#include "message-stack.h"
+
+#include "object/sp-root.h"
+#include "object/sp-string.h"
+
+#include "preferences.h"
+#include "ui/builder-utils.h"
+#include "ui/dialog-events.h"
+#include "ui/icon-loader.h"
+#include "ui/icon-names.h"
+#include "ui/tools/tool-base.h"
+#include "ui/syntax.h"
+
+#include "util/trim.h"
+#include "widgets/sp-xmlview-tree.h"
+
+namespace {
+/**
+ * Set the orientation of `paned` to vertical or horizontal, and make the first child resizable
+ * if vertical, and the second child resizable if horizontal.
+ * @pre `paned` has two children
+ */
+void paned_set_vertical(Gtk::Paned &paned, bool vertical)
+{
+ auto& first = *paned.get_child1();
+ auto& second = *paned.get_child2();
+ const int space = 1;
+ paned.child_property_resize(first) = vertical;
+ first.set_margin_bottom(vertical ? space : 0);
+ first.set_margin_end(vertical ? 0 : space);
+ second.set_margin_top(vertical ? space : 0);
+ second.set_margin_start(vertical ? 0 : space);
+ assert(paned.child_property_resize(second));
+ paned.set_orientation(vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL);
+}
+} // namespace
+
+namespace Inkscape::UI::Dialog {
+
+XmlTree::XmlTree()
+ : DialogBase("/dialogs/xml/", "XMLEditor")
+ , _builder(create_builder("dialog-xml.glade"))
+ , _paned(get_widget<Gtk::Paned>(_builder, "pane"))
+ , xml_element_new_button(get_widget<Gtk::Button>(_builder, "new-elem"))
+ , xml_text_new_button(get_widget<Gtk::Button>(_builder, "new-text"))
+ , xml_node_delete_button(get_widget<Gtk::Button>(_builder, "del"))
+ , xml_node_duplicate_button(get_widget<Gtk::Button>(_builder, "dup"))
+ , unindent_node_button(get_widget<Gtk::Button>(_builder, "unindent"))
+ , indent_node_button(get_widget<Gtk::Button>(_builder, "indent"))
+ , lower_node_button(get_widget<Gtk::Button>(_builder, "lower"))
+ , raise_node_button(get_widget<Gtk::Button>(_builder, "raise"))
+ , _syntax_theme("/theme/syntax-color-theme")
+ , _mono_font("/dialogs/xml/mono-font", false)
+{
+ /* tree view */
+ tree = SP_XMLVIEW_TREE(sp_xmlview_tree_new(nullptr, nullptr, nullptr));
+ gtk_widget_set_tooltip_text( GTK_WIDGET(tree), _("Drag to reorder nodes") );
+
+ Gtk::ScrolledWindow& tree_scroller = get_widget<Gtk::ScrolledWindow>(_builder, "tree-wnd");
+ _treemm = Gtk::manage(Glib::wrap(GTK_TREE_VIEW(tree)));
+ tree_scroller.add(*Gtk::manage(Glib::wrap(GTK_WIDGET(tree))));
+ fix_inner_scroll(&tree_scroller);
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+
+ /* attributes */
+ attributes = Gtk::make_managed<AttrDialog>();
+ attributes->set_margin_top(0);
+ attributes->set_margin_bottom(0);
+ attributes->set_margin_start(0);
+ attributes->set_margin_end(0);
+ attributes->get_scrolled_window().set_shadow_type(Gtk::SHADOW_IN);
+ attributes->show();
+ attributes->get_status_box().hide();
+ attributes->get_status_box().set_no_show_all();
+ _paned.pack2(*attributes, true, false);
+
+ /* Signal handlers */
+ _treemm->get_selection()->signal_changed().connect([=]() {
+ if (blocked || !getDesktop())
+ return;
+ if (!_tree_select_idle) {
+ // Defer the update after all events have been processed.
+ _tree_select_idle = Glib::signal_idle().connect(sigc::mem_fun(*this, &XmlTree::deferred_on_tree_select_row));
+ }
+ });
+ tree->connectTreeMove([=]() {
+ if (auto doc = getDocument()) {
+ DocumentUndo::done(doc, Q_("Undo History / XML Editor|Drag XML subtree"), INKSCAPE_ICON("dialog-xml-editor"));
+ }
+ });
+
+ xml_element_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_element_node));
+ xml_text_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_text_node));
+ xml_node_duplicate_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_duplicate_node));
+ xml_node_delete_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_delete_node));
+ unindent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_unindent_node));
+ indent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_indent_node));
+ raise_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_raise_node));
+ lower_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_lower_node));
+
+ set_name("XMLAndAttributesDialog");
+ set_spacing(0);
+ show_all();
+
+ int panedpos = prefs->getInt("/dialogs/xml/panedpos", 200);
+ _paned.property_position() = panedpos;
+ _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_resized));
+
+ pack_start(get_widget<Gtk::Box>(_builder, "main"), true, true);
+
+ int min_width = 0, dummy;
+ get_preferred_width(min_width, dummy);
+
+ auto auto_arrange_panels = [=](Gtk::Allocation const &alloc) {
+ // skip bogus sizes
+ if (alloc.get_width() < 10 || alloc.get_height() < 10) return;
+
+ // minimal width times fudge factor to arrive at "narrow" dialog with automatic vertical layout:
+ const bool narrow = alloc.get_width() < min_width * 1.5;
+ paned_set_vertical(_paned, narrow);
+ };
+
+ auto arrange_panels = [=](DialogLayout layout){
+ switch (layout) {
+ case Auto:
+ auto_arrange_panels(get_allocation());
+ break;
+ case Horizontal:
+ paned_set_vertical(_paned, false);
+ break;
+ case Vertical:
+ paned_set_vertical(_paned, true);
+ break;
+ }
+ // ensure_size();
+ };
+
+ signal_size_allocate().connect([=] (Gtk::Allocation const &alloc) {
+ arrange_panels(_layout);
+ });
+
+ auto& popup = get_widget<Gtk::MenuButton>(_builder, "layout-btn");
+ popup.set_has_tooltip();
+ popup.signal_query_tooltip().connect([=](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltip){
+ auto tip = "";
+ switch (_layout) {
+ case Auto: tip = _("Automatic panel layout:\nchanges with dialog size");
+ break;
+ case Horizontal: tip = _("Horizontal panel layout");
+ break;
+ case Vertical: tip = _("Vertical panel layout");
+ break;
+ }
+ tooltip->set_text(tip);
+ return true;
+ });
+
+ auto set_layout = [=](DialogLayout layout){
+ Glib::ustring icon = "layout-auto";
+ if (layout == Horizontal) {
+ icon = "layout-horizontal";
+ } else if (layout == Vertical) {
+ icon = "layout-vertical";
+ }
+ get_widget<Gtk::Image>(_builder, "layout-img").set_from_icon_name(icon + "-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
+ prefs->setInt("/dialogs/xml/layout", layout);
+ arrange_panels(layout);
+ _layout = layout;
+ };
+
+ auto menu_items = get_widget<Gtk::Menu>(_builder, "menu-popup").get_children();
+
+ DialogLayout layouts[] = {Auto, Horizontal, Vertical};
+ int index = 0;
+ for (auto item : menu_items) {
+ g_assert(index < 3);
+ auto layout = layouts[index++];
+ static_cast<Gtk::RadioMenuItem*>(item)->signal_activate().connect([=](){ set_layout(layout); });
+ }
+
+ _layout = static_cast<DialogLayout>(prefs->getIntLimited("/dialogs/xml/layout", Auto, Auto, Vertical));
+ static_cast<Gtk::RadioMenuItem*>(menu_items.at(_layout))->set_active();
+ set_layout(_layout);
+ // establish initial layout to prevent unwanted panels resize in auto layout mode
+ paned_set_vertical(_paned, true);
+
+ _syntax_theme.action = [=]() {
+ setSyntaxStyle(Inkscape::UI::Syntax::build_xml_styles(_syntax_theme));
+ // rebuild tree to change markup
+ rebuildTree();
+ };
+
+ setSyntaxStyle(Inkscape::UI::Syntax::build_xml_styles(_syntax_theme));
+
+ _mono_font.action = [=]() {
+ Glib::ustring mono("mono-font");
+ if (_mono_font) {
+ _treemm->get_style_context()->add_class(mono);
+ } else {
+ _treemm->get_style_context()->remove_class(mono);
+ }
+ attributes->set_mono_font(_mono_font);
+ };
+ _mono_font.action();
+
+ tree->renderer->signal_editing_canceled().connect([=]() {
+ stopNodeEditing(false, "", "");
+ });
+ tree->renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& name) {
+ stopNodeEditing(true, path, name);
+ });
+ tree->renderer->signal_editing_started().connect([=](Gtk::CellEditable* cell, const Glib::ustring& path) {
+ startNodeEditing(cell, path);
+ });
+}
+
+XmlTree::~XmlTree()
+{
+ unsetDocument();
+}
+
+void XmlTree::rebuildTree()
+{
+ sp_xmlview_tree_set_repr(tree, nullptr);
+ if (auto document = getDocument()) {
+ set_tree_repr(document->getReprRoot());
+ }
+}
+
+void XmlTree::_resized()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ prefs->setInt("/dialogs/xml/panedpos", _paned.property_position());
+}
+
+void XmlTree::unsetDocument()
+{
+ _tree_select_idle.disconnect();
+}
+
+void XmlTree::documentReplaced()
+{
+ unsetDocument();
+ if (auto document = getDocument()) {
+ // TODO: Why is this a document property?
+ document->setXMLDialogSelectedObject(nullptr);
+
+ set_tree_repr(document->getReprRoot());
+ } else {
+ set_tree_repr(nullptr);
+ }
+}
+
+void XmlTree::selectionChanged(Selection *selection)
+{
+ if (!blocked++) {
+ Inkscape::XML::Node *node = get_dt_select();
+ set_tree_select(node);
+ }
+ blocked--;
+}
+
+void XmlTree::set_tree_repr(Inkscape::XML::Node *repr)
+{
+ if (repr == selected_repr) {
+ return;
+ }
+
+ sp_xmlview_tree_set_repr(tree, repr);
+ if (repr) {
+ set_tree_select(get_dt_select());
+ } else {
+ set_tree_select(nullptr);
+ }
+
+ propagate_tree_select(selected_repr);
+}
+
+/**
+ * Expand all parent nodes of `repr`
+ */
+static void expand_parents(SPXMLViewTree *tree, Inkscape::XML::Node *repr)
+{
+ auto parentrepr = repr->parent();
+ if (!parentrepr) {
+ return;
+ }
+
+ expand_parents(tree, parentrepr);
+
+ GtkTreeIter node;
+ if (sp_xmlview_tree_get_repr_node(tree, parentrepr, &node)) {
+ GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node);
+ if (path) {
+ gtk_tree_view_expand_row(GTK_TREE_VIEW(tree), path, false);
+ }
+ }
+}
+
+void XmlTree::set_tree_select(Inkscape::XML::Node *repr, bool edit)
+{
+ if (selected_repr) {
+ Inkscape::GC::release(selected_repr);
+ }
+ selected_repr = repr;
+ if (selected_repr) {
+ Inkscape::GC::anchor(selected_repr);
+ }
+ if (auto document = getDocument()) {
+ document->setXMLDialogSelectedObject(nullptr);
+ }
+ if (repr) {
+ GtkTreeIter node;
+
+ Inkscape::GC::anchor(selected_repr);
+
+ expand_parents(tree, repr);
+
+ if (sp_xmlview_tree_get_repr_node(SP_XMLVIEW_TREE(tree), repr, &node)) {
+
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_unselect_all (selection);
+
+ GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node);
+ gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree), path, nullptr, TRUE, 0.66, 0.0);
+ gtk_tree_selection_select_iter(selection, &node);
+ auto col = gtk_tree_view_get_column(&tree->tree, 0);
+ gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree), path, edit ? col : nullptr, edit);
+ gtk_tree_path_free(path);
+
+ } else {
+ g_message("XmlTree::set_tree_select : Couldn't find repr node");
+ }
+ } else {
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_unselect_all (selection);
+
+ on_tree_unselect_row_disable();
+ }
+ propagate_tree_select(repr);
+}
+
+
+
+void XmlTree::propagate_tree_select(Inkscape::XML::Node *repr)
+{
+ if (repr &&
+ (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE ||
+ repr->type() == Inkscape::XML::NodeType::TEXT_NODE ||
+ repr->type() == Inkscape::XML::NodeType::COMMENT_NODE))
+ {
+ attributes->setRepr(repr);
+ } else {
+ attributes->setRepr(nullptr);
+ }
+}
+
+
+Inkscape::XML::Node *XmlTree::get_dt_select()
+{
+ if (auto selection = getSelection()) {
+ return selection->singleRepr();
+ }
+ return nullptr;
+}
+
+
+/**
+ * Like SPDesktop::isLayer(), but ignores SPGroup::effectiveLayerMode().
+ */
+static bool isRealLayer(SPObject const *object)
+{
+ auto group = cast<SPGroup>(object);
+ return group && group->layerMode() == SPGroup::LAYER;
+}
+
+void XmlTree::set_dt_select(Inkscape::XML::Node *repr)
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ SPObject *object;
+ if (repr) {
+ while ( ( repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE )
+ && repr->parent() )
+ {
+ repr = repr->parent();
+ } // end of while loop
+
+ object = document->getObjectByRepr(repr);
+ } else {
+ object = nullptr;
+ }
+
+ blocked++;
+
+ if (!object || !in_dt_coordsys(*object)) {
+ // object not on canvas
+ } else if (isRealLayer(object)) {
+ getDesktop()->layerManager().setCurrentLayer(object);
+ } else {
+ if (is<SPGroup>(object->parent)) {
+ getDesktop()->layerManager().setCurrentLayer(object->parent);
+ }
+
+ getSelection()->set(cast<SPItem>(object));
+ }
+
+ document->setXMLDialogSelectedObject(object);
+ blocked--;
+}
+
+bool XmlTree::deferred_on_tree_select_row()
+{
+ GtkTreeIter iter;
+ GtkTreeModel *model;
+
+ if (selected_repr) {
+ Inkscape::GC::release(selected_repr);
+ selected_repr = nullptr;
+ }
+
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+
+ if (!gtk_tree_selection_get_selected (selection, &model, &iter)) {
+ // Nothing selected, update widgets
+ propagate_tree_select(nullptr);
+ set_dt_select(nullptr);
+ on_tree_unselect_row_disable();
+ return false;
+ }
+
+ Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(model, &iter);
+ g_assert(repr != nullptr);
+
+
+ selected_repr = repr;
+ Inkscape::GC::anchor(selected_repr);
+
+ propagate_tree_select(selected_repr);
+ set_dt_select(selected_repr);
+ on_tree_select_row_enable(&iter);
+
+ return FALSE;
+}
+
+void XmlTree::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget)
+{
+ if (widget) {
+ gtk_label_set_markup(GTK_LABEL(widget), message ? message : "");
+ }
+}
+
+void XmlTree::on_tree_select_row_enable(GtkTreeIter *node)
+{
+ if (!node) {
+ return;
+ }
+
+ Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), node);
+ Inkscape::XML::Node *parent=repr->parent();
+
+ //on_tree_select_row_enable_if_mutable
+ xml_node_duplicate_button.set_sensitive(xml_tree_node_mutable(node));
+ xml_node_delete_button.set_sensitive(xml_tree_node_mutable(node));
+
+ //on_tree_select_row_enable_if_element
+ if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE) {
+ xml_element_new_button.set_sensitive(true);
+ xml_text_new_button.set_sensitive(true);
+
+ } else {
+ xml_element_new_button.set_sensitive(false);
+ xml_text_new_button.set_sensitive(false);
+ }
+
+ //on_tree_select_row_enable_if_has_grandparent
+ {
+ GtkTreeIter parent;
+ if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &parent, node)) {
+ GtkTreeIter grandparent;
+ if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &grandparent, &parent)) {
+ unindent_node_button.set_sensitive(true);
+ } else {
+ unindent_node_button.set_sensitive(false);
+ }
+ } else {
+ unindent_node_button.set_sensitive(false);
+ }
+ }
+ // on_tree_select_row_enable_if_indentable
+ gboolean indentable = FALSE;
+
+ if (xml_tree_node_mutable(node)) {
+ Inkscape::XML::Node *prev;
+
+ if ( parent && repr != parent->firstChild() ) {
+ g_assert(parent->firstChild());
+
+ // skip to the child just before the current repr
+ for ( prev = parent->firstChild() ;
+ prev && prev->next() != repr ;
+ prev = prev->next() ){};
+
+ if (prev && (prev->type() == Inkscape::XML::NodeType::ELEMENT_NODE)) {
+ indentable = TRUE;
+ }
+ }
+ }
+
+ indent_node_button.set_sensitive(indentable);
+
+ //on_tree_select_row_enable_if_not_first_child
+ {
+ if ( parent && repr != parent->firstChild() ) {
+ raise_node_button.set_sensitive(true);
+ } else {
+ raise_node_button.set_sensitive(false);
+ }
+ }
+
+ //on_tree_select_row_enable_if_not_last_child
+ {
+ if ( parent && (parent->parent() && repr->next())) {
+ lower_node_button.set_sensitive(true);
+ } else {
+ lower_node_button.set_sensitive(false);
+ }
+ }
+}
+
+
+gboolean XmlTree::xml_tree_node_mutable(GtkTreeIter *node)
+{
+ // top-level is immutable, obviously
+ GtkTreeIter parent;
+ if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &parent, node)) {
+ return false;
+ }
+
+
+ // if not in base level (where namedview, defs, etc go), we're mutable
+ GtkTreeIter child;
+ if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &child, &parent)) {
+ return true;
+ }
+
+ Inkscape::XML::Node *repr;
+ repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), node);
+ g_assert(repr);
+
+ // don't let "defs" or "namedview" disappear
+ if ( !strcmp(repr->name(),"svg:defs") ||
+ !strcmp(repr->name(),"sodipodi:namedview") ) {
+ return false;
+ }
+
+ // everyone else is okay, I guess. :)
+ return true;
+}
+
+
+
+void XmlTree::on_tree_unselect_row_disable()
+{
+ xml_text_new_button.set_sensitive(false);
+ xml_element_new_button.set_sensitive(false);
+ xml_node_delete_button.set_sensitive(false);
+ xml_node_duplicate_button.set_sensitive(false);
+ unindent_node_button.set_sensitive(false);
+ indent_node_button.set_sensitive(false);
+ raise_node_button.set_sensitive(false);
+ lower_node_button.set_sensitive(false);
+}
+
+void XmlTree::onCreateNameChanged()
+{
+ Glib::ustring text = name_entry->get_text();
+ /* TODO: need to do checking a little more rigorous than this */
+ create_button->set_sensitive(!text.empty());
+}
+
+void XmlTree::cmd_new_element_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ // enable in-place node name editing
+ tree->renderer->property_editable() = true;
+
+ auto dummy = ""; // this element has no corresponding SP* object and its construction is silent
+ auto xml_doc = document->getReprDoc();
+ _dummy = xml_doc->createElement(dummy); // create dummy placeholder so we can have a new temporary row in xml tree
+ _node_parent = selected_repr; // remember where the node is inserted
+ selected_repr->appendChild(_dummy);
+ set_tree_select(_dummy, true); // enter in-place node name editing
+}
+
+void XmlTree::startNodeEditing(Gtk::CellEditable* cell, const Glib::ustring& path)
+{
+ if (!cell) {
+ return;
+ }
+ // remove dummy element name so user can start with an empty name
+ auto entry = dynamic_cast<Gtk::Entry *>(cell);
+ entry->get_buffer()->set_text("");
+}
+
+void XmlTree::stopNodeEditing(bool ok, const Glib::ustring& path, Glib::ustring element)
+{
+ tree->renderer->property_editable() = false;
+
+ auto document = getDocument();
+ if (!document) {
+ return;
+ }
+ // delete dummy node
+ if (_dummy) {
+ document->setXMLDialogSelectedObject(nullptr);
+
+ auto parent = _dummy->parent();
+ Inkscape::GC::release(_dummy);
+ sp_repr_unparent(_dummy);
+ if (parent) {
+ auto parentobject = document->getObjectByRepr(parent);
+ if (parentobject) {
+ parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ }
+ }
+
+ _dummy = nullptr;
+ }
+
+ Util::trim(element);
+ if (!ok || element.empty() || !_node_parent) {
+ return;
+ }
+
+ Inkscape::XML::Document* xml_doc = document->getReprDoc();
+ // Extract tag name
+ {
+ static auto const extract_tagname = Glib::Regex::create("^<?\\s*(\\w[\\w:\\-\\d]*)");
+ Glib::MatchInfo match_info;
+ extract_tagname->match(element, match_info);
+ if (!match_info.matches()) {
+ return;
+ }
+ element = match_info.fetch(1);
+ }
+
+ // prepend "svg:" namespace if none is given
+ if (element.find(':') == Glib::ustring::npos) {
+ element = "svg:" + element;
+ }
+ auto repr = xml_doc->createElement(element.c_str());
+ Inkscape::GC::release(repr);
+ _node_parent->appendChild(repr);
+ set_dt_select(repr);
+ set_tree_select(repr, true);
+ _node_parent = nullptr;
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Create new element node"), INKSCAPE_ICON("dialog-xml-editor"));
+}
+
+void XmlTree::cmd_new_text_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ g_assert(selected_repr != nullptr);
+
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *text = xml_doc->createTextNode("");
+ selected_repr->appendChild(text);
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Create new text node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ set_tree_select(text);
+ set_dt_select(text);
+}
+
+void XmlTree::cmd_duplicate_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ g_assert(selected_repr != nullptr);
+
+ Inkscape::XML::Node *parent = selected_repr->parent();
+ Inkscape::XML::Node *dup = selected_repr->duplicate(parent->document());
+ parent->addChild(dup, selected_repr);
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Duplicate node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ GtkTreeIter node;
+
+ if (sp_xmlview_tree_get_repr_node(SP_XMLVIEW_TREE(tree), dup, &node)) {
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+ gtk_tree_selection_select_iter(selection, &node);
+ }
+}
+
+void XmlTree::cmd_delete_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ g_assert(selected_repr != nullptr);
+
+ document->setXMLDialogSelectedObject(nullptr);
+
+ Inkscape::XML::Node *parent = selected_repr->parent();
+
+ sp_repr_unparent(selected_repr);
+
+ if (parent) {
+ auto parentobject = document->getObjectByRepr(parent);
+ if (parentobject) {
+ parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ }
+ }
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Delete node"), INKSCAPE_ICON("dialog-xml-editor"));
+}
+
+void XmlTree::cmd_raise_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ g_assert(selected_repr != nullptr);
+
+ Inkscape::XML::Node *parent = selected_repr->parent();
+ g_return_if_fail(parent != nullptr);
+ g_return_if_fail(parent->firstChild() != selected_repr);
+
+ Inkscape::XML::Node *ref = nullptr;
+ Inkscape::XML::Node *before = parent->firstChild();
+ while (before && (before->next() != selected_repr)) {
+ ref = before;
+ before = before->next();
+ }
+
+ parent->changeOrder(selected_repr, ref);
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Raise node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ set_tree_select(selected_repr);
+ set_dt_select(selected_repr);
+}
+
+
+
+void XmlTree::cmd_lower_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ g_assert(selected_repr != nullptr);
+
+ g_return_if_fail(selected_repr->next() != nullptr);
+ Inkscape::XML::Node *parent = selected_repr->parent();
+
+ parent->changeOrder(selected_repr, selected_repr->next());
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Lower node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ set_tree_select(selected_repr);
+ set_dt_select(selected_repr);
+}
+
+void XmlTree::cmd_indent_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ Inkscape::XML::Node *repr = selected_repr;
+ g_assert(repr != nullptr);
+
+ Inkscape::XML::Node *parent = repr->parent();
+ g_return_if_fail(parent != nullptr);
+ g_return_if_fail(parent->firstChild() != repr);
+
+ Inkscape::XML::Node* prev = parent->firstChild();
+ while (prev && (prev->next() != repr)) {
+ prev = prev->next();
+ }
+ g_return_if_fail(prev != nullptr);
+ g_return_if_fail(prev->type() == Inkscape::XML::NodeType::ELEMENT_NODE);
+
+ Inkscape::XML::Node* ref = nullptr;
+ if (prev->firstChild()) {
+ for( ref = prev->firstChild() ; ref->next() ; ref = ref->next() ){};
+ }
+
+ parent->removeChild(repr);
+ prev->addChild(repr, ref);
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Indent node"), INKSCAPE_ICON("dialog-xml-editor"));
+ set_tree_select(repr);
+ set_dt_select(repr);
+
+} // end of cmd_indent_node()
+
+
+
+void XmlTree::cmd_unindent_node()
+{
+ auto document = getDocument();
+ if (!document)
+ return;
+
+ Inkscape::XML::Node *repr = selected_repr;
+ g_assert(repr != nullptr);
+
+ Inkscape::XML::Node *parent = repr->parent();
+ g_return_if_fail(parent);
+ Inkscape::XML::Node *grandparent = parent->parent();
+ g_return_if_fail(grandparent);
+
+ parent->removeChild(repr);
+ grandparent->addChild(repr, parent);
+
+ DocumentUndo::done(document, Q_("Undo History / XML Editor|Unindent node"), INKSCAPE_ICON("dialog-xml-editor"));
+
+ set_tree_select(repr);
+ set_dt_select(repr);
+
+} // end of cmd_unindent_node()
+
+/** Returns true iff \a item is suitable to be included in the selection, in particular
+ whether it has a bounding box in the desktop coordinate system for rendering resize handles.
+
+ Descendents of <defs> nodes (markers etc.) return false, for example.
+*/
+bool XmlTree::in_dt_coordsys(SPObject const &item)
+{
+ /* Definition based on sp_item_i2doc_affine. */
+ SPObject const *child = &item;
+ while (is<SPItem>(child)) {
+ SPObject const * const parent = child->parent;
+ if (parent == nullptr) {
+ g_assert(is<SPRoot>(child));
+ if (child == &item) {
+ // item is root
+ return false;
+ }
+ return true;
+ }
+ child = parent;
+ }
+ g_assert(!is<SPRoot>(child));
+ return false;
+}
+
+void XmlTree::desktopReplaced() {
+ // subdialog does not receive desktopReplace calls, we need to propagate desktop change
+ if (attributes) {
+ attributes->setDesktop(getDesktop());
+ }
+}
+
+void XmlTree::setSyntaxStyle(Inkscape::UI::Syntax::XMLStyles const &new_style)
+{
+ tree->formatter->setStyle(new_style);
+}
+
+} // namespace Inkscape::UI::Dialog
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-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/dialog/xml-tree.h b/src/ui/dialog/xml-tree.h
new file mode 100644
index 0000000..0a6c4d9
--- /dev/null
+++ b/src/ui/dialog/xml-tree.h
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * This is XML tree editor, which allows direct modifying of all elements
+ * of Inkscape document, including foreign ones.
+ *//*
+ * Authors: see git history
+ * Lauris Kaplinski, 2000
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_DIALOG_XML_TREE_H
+#define INKSCAPE_UI_DIALOG_XML_TREE_H
+
+#include <glibmm/ustring.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/widget.h>
+#include <memory>
+#include <glibmm/refptr.h>
+#include <gtkmm/builder.h>
+#include <gtkmm/button.h>
+#include <gtkmm/entry.h>
+#include <gtkmm/paned.h>
+#include <gtkmm/radiobutton.h>
+#include <gtkmm/scrolledwindow.h>
+#include <gtkmm/separatortoolitem.h>
+#include <gtkmm/switch.h>
+#include <gtkmm/textview.h>
+#include <gtkmm/toolbar.h>
+
+#include "message.h"
+#include "attrdialog.h"
+#include "dialog-base.h"
+#include "preferences.h"
+#include "ui/syntax.h"
+
+class SPObject;
+struct SPXMLViewAttrList;
+struct SPXMLViewContent;
+struct SPXMLViewTree;
+
+namespace Inkscape {
+class MessageStack;
+class MessageContext;
+namespace XML { class Node; }
+
+namespace UI {
+namespace Dialog {
+
+/**
+ * A dialog widget to view and edit the document xml
+ */
+
+class XmlTree : public DialogBase
+{
+public:
+ XmlTree();
+ ~XmlTree() override;
+
+ void setSyntaxStyle(Inkscape::UI::Syntax::XMLStyles const &new_style);
+
+private:
+ void unsetDocument();
+ void documentReplaced() override;
+ void selectionChanged(Selection *selection) override;
+ void desktopReplaced() override;
+
+ /**
+ * Select a node in the xml tree
+ */
+ void set_tree_repr(Inkscape::XML::Node *repr);
+
+ /**
+ * Is the selected tree node editable
+ */
+ gboolean xml_tree_node_mutable(GtkTreeIter *node);
+
+ /**
+ * Select a node in the xml tree
+ */
+ void set_tree_select(Inkscape::XML::Node *repr, bool edit = false);
+
+ /**
+ * Set the attribute list to match the selected node in the tree
+ */
+ void propagate_tree_select(Inkscape::XML::Node *repr);
+
+ /**
+ * Find the current desktop selection
+ */
+ Inkscape::XML::Node *get_dt_select();
+
+ /**
+ * Select the current desktop selection
+ */
+ void set_dt_select(Inkscape::XML::Node *repr);
+
+ /**
+ * Callback for deferring the `on_tree_select_row` response in order to
+ * skip invalid intermediate selection states. In particular,
+ * `gtk_tree_store_remove` makes an undesired selection that we will
+ * immediately revert and don't want to an early response for.
+ */
+ Inkscape::auto_connection _tree_select_idle;
+ bool deferred_on_tree_select_row();
+
+ /**
+ * Enable widgets based on current selections
+ */
+ void on_tree_select_row_enable(GtkTreeIter *node);
+ void on_tree_unselect_row_disable();
+ void on_tree_unselect_row_hide();
+ void on_attr_unselect_row_disable();
+
+ void onNameChanged();
+ void onCreateNameChanged();
+
+ /**
+ * Callbacks for changes in desktop selection and current document
+ */
+ static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog);
+
+ /**
+ * Callbacks for toolbar buttons being pressed
+ */
+ void cmd_new_element_node();
+ void cmd_new_text_node();
+ void cmd_duplicate_node();
+ void cmd_delete_node();
+ void cmd_raise_node();
+ void cmd_lower_node();
+ void cmd_indent_node();
+ void cmd_unindent_node();
+
+ void _resized();
+ bool in_dt_coordsys(SPObject const &item);
+
+ void rebuildTree();
+ void stopNodeEditing(bool ok, Glib::ustring const &path, Glib::ustring name);
+ void startNodeEditing(Gtk::CellEditable *cell, Glib::ustring const &path);
+
+ /**
+ * Flag to ensure only one operation is performed at once
+ */
+ gint blocked = 0;
+
+ /**
+ * Signal handlers
+ */
+ Inkscape::XML::Node *selected_repr = nullptr;
+
+ /* XmlTree Widgets */
+ SPXMLViewTree *tree = nullptr;
+ Gtk::TreeView* _treemm = nullptr;
+ AttrDialog *attributes;
+ Gtk::Box *_attrbox;
+
+ /* XML Node Creation pop-up window */
+ Glib::RefPtr<Gtk::Builder> _builder;
+ Gtk::Entry *name_entry;
+ Gtk::Button *create_button;
+ Gtk::Paned& _paned;
+
+ // Gtk::Box node_box;
+ Gtk::Switch _attrswitch;
+ Gtk::Label status;
+ Gtk::Button& xml_element_new_button;
+ Gtk::Button& xml_text_new_button;
+ Gtk::Button& xml_node_delete_button;
+ Gtk::Button& xml_node_duplicate_button;
+ Gtk::Button& unindent_node_button;
+ Gtk::Button& indent_node_button;
+ Gtk::Button& raise_node_button;
+ Gtk::Button& lower_node_button;
+
+ enum DialogLayout: int { Auto = 0, Horizontal, Vertical };
+ DialogLayout _layout = Auto;
+ Pref<Glib::ustring> _syntax_theme;
+ Pref<bool> _mono_font;
+ Inkscape::XML::Node* _dummy = nullptr;
+ Inkscape::XML::Node* _node_parent = nullptr;
+};
+
+} // namespace Dialog
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_DIALOG_XML_TREE_H
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :