From 4e18127da28a34003582ad2eb0b42a37871def09 Mon Sep 17 00:00:00 2001 From: mike kowalski Date: Sat, 1 Feb 2025 17:42:03 -0800 Subject: [PATCH 1/8] Inkscape new settings dialog Replacement dialog for Inkscape preferences. The idea is to define it in XML along with preference paths to limit or eliminate code changes for adding new settings. --- share/ui/settings-dialog.xml | 377 ++++++++++ share/ui/style.css | 74 ++ src/actions/actions-dialogs.cpp | 8 + src/preferences-skeleton.h | 7 +- src/preferences.cpp | 35 +- src/preferences.h | 7 + src/ui/CMakeLists.txt | 2 + src/ui/dialog/settings-dialog.cpp | 1084 +++++++++++++++++++++++++++++ src/ui/dialog/settings-dialog.h | 61 ++ 9 files changed, 1647 insertions(+), 8 deletions(-) create mode 100644 share/ui/settings-dialog.xml create mode 100644 src/ui/dialog/settings-dialog.cpp create mode 100644 src/ui/dialog/settings-dialog.h diff --git a/share/ui/settings-dialog.xml b/share/ui/settings-dialog.xml new file mode 100644 index 0000000000..eff9b0aba7 --- /dev/null +++ b/share/ui/settings-dialog.xml @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + +
+ + + + + + Select, move, rotate and transform objects + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Edit paths by nodes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Build shapes with the boolean tool + + + + + + + + + + + + + + + Create rectangles and squares + + + + + + + + + + + Create ellipses, squares, chords and arcs + + + + + + + + + + + Create stars and polygons + + + + + + + + + + + Create 3D boxes + + + + + + + + + + + Create spirals + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
diff --git a/share/ui/style.css b/share/ui/style.css index 15e7972653..2673d15ed9 100644 --- a/share/ui/style.css +++ b/share/ui/style.css @@ -1638,6 +1638,80 @@ listview row.top-separator { min-height: 2.25em; } +#Settings .row { margin: 2px; } +#Settings .row .label:first-child { margin-right: 0.5em; margin-left: 2px; } +#Settings .text { margin: 0 4px; } +#Settings .text.strong { font-weight: 500; } +#Settings .expander { + padding-left: 2px; /* to align with other labels in a panel */ + font-weight: bold; +} +#Settings .expander label { + text-transform: uppercase; +} +#Settings .checkbox { padding-left: 1px; } +#Settings .number { min-height: 24px; } +#Settings .panel { + margin-bottom: 4px; + padding: 0; + border-radius: 3px; + border: solid 1px alpha(@theme_fg_color, 0.12); +} +#Settings .panel.open { + padding-bottom: 4px; +} +#Settings .panel .panel-shortcut { + margin-left: 0.5em; + font-weight: 500; + opacity: 0.6; +} +#Settings .header { + min-height: 2em; + font-weight: 500; + padding: 0; + border-radius: 3px; + background-color: alpha(@theme_fg_color, 0.08); +} +#Settings .open .header { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +#Settings .header .button { + padding: 0; +} +#Settings .header .icon { + margin: 0 6px; +} +#Settings .group.indent { + margin-left: 26px; +} +#Settings #PageSelector { + margin: 12px; +} +#Settings #Pages { + background-color: @theme_bg_color; + min-width: 10em; +} +#Settings #Pages row { + min-height: 2em; + border-radius: 2px; +} +#Settings .switch.small { + margin: 0 14px 0 6px; +} +#Settings .switch.small slider { + min-width: 10px; + min-height: 10px; +} +#Settings .switch.small image { + -gtk-icon-size: 8px; +} +#Settings .error { + color: red; /* mark invalid settings, so it possible to spot them */ + outline: solid 1px red; + outline-offset: 0px; +} + .border-box { border-radius: 2px; border: 1px solid @borders; diff --git a/src/actions/actions-dialogs.cpp b/src/actions/actions-dialogs.cpp index 26db490c1c..254009533e 100644 --- a/src/actions/actions-dialogs.cpp +++ b/src/actions/actions-dialogs.cpp @@ -21,9 +21,11 @@ #include "inkscape-application.h" #include "inkscape-window.h" +#include "ui/dialog-run.h" #include "ui/dialog/dialog-container.h" #include "ui/dialog/dialog-data.h" +#include "ui/dialog/settings-dialog.h" // 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.) @@ -160,6 +162,12 @@ void add_actions_dialogs(InkscapeWindow *win) return; } + gapp->add_action("settings", [win] { + Inkscape::UI::Dialog::SettingsDialog dialog(*win); + Inkscape::UI::dialog_run(dialog); + dialog.close(); + }); + app->get_action_extra_data().add_data(raw_data_dialogs); } diff --git a/src/preferences-skeleton.h b/src/preferences-skeleton.h index 87e024a4b1..85c44d66ae 100644 --- a/src/preferences-skeleton.h +++ b/src/preferences-skeleton.h @@ -444,8 +444,11 @@ static char const preferences_skeleton[] = small="0" iconsize="16"> + id="buttons" + showLPETool="0" + show3DBox="1" showPencil="1" showSelect="1" showNode="1" showSpiral="1" showCalligraphic="1" showTweak="1" showSymbol="1" + showGradient="1" showBooleans="1" showRect="1" showStar="1" showArc="1" showPen="1" showText="1" showMesh="1" showDropper="1" + showPaintBucket="1" showSpray="1" showPages="1" showZoom="1" showLPETool="1" showMeasure="1" showEraser="1" showConnector="1" /> root()->duplicate(_prefs_defaults); + _prefs_defaults->appendChild(node); } /** @@ -661,29 +666,26 @@ void Preferences::removeObserver(Observer &o) } } - /** * Get the XML node corresponding to the given pref key. * * @param pref_key Preference key (path) to get. * @param create Whether to create the corresponding node if it doesn't exist. - * @param separator The character used to separate parts of the pref key. * @return XML node corresponding to the specified key. * * Derived from former inkscape_get_repr(). Private because it assumes that the backend is * a flat XML file, which may not be the case e.g. if we are using GConf (in future). */ -Inkscape::XML::Node *Preferences::_getNode(Glib::ustring const &pref_key, bool create) -{ +XML::Node* Preferences::_get_node(XML::Document* document, const Glib::ustring& pref_key, bool create) { // verify path g_assert( pref_key.empty() || pref_key.at(0) == '/' ); // empty corresponds to root node // No longer necessary, can cause problems with input devices which have a dot in the name // g_assert( pref_key.find('.') == Glib::ustring::npos ); - if (_prefs_doc == nullptr){ + if (document == nullptr){ return nullptr; } - Inkscape::XML::Node *node = _prefs_doc->root(); + Inkscape::XML::Node *node = document->root(); Inkscape::XML::Node *child = nullptr; gchar **splits = g_strsplit(pref_key.c_str(), "/", 0); @@ -733,6 +735,10 @@ Inkscape::XML::Node *Preferences::_getNode(Glib::ustring const &pref_key, bool c return node; } +Inkscape::XML::Node *Preferences::_getNode(Glib::ustring const &pref_key, bool create) { + return _get_node(_prefs_doc, pref_key, create); +} + /** Get raw value for preference path, without any caching. * std::nullopt is returned when the requested entry does not exist */ @@ -1118,6 +1124,23 @@ Glib::ustring Preferences::Entry::getEntryName() const return path_base; } +bool Preferences::is_default_valid(const Glib::ustring& path) { + Glib::ustring node_key, attr_key; + _keySplit(path, node_key, attr_key); + + // retrieve the attribute from preference defaults + auto node = _get_node(_prefs_defaults, node_key, false); + if (!node) { + return false; + } + auto attr = node->attribute(attr_key.c_str()); + if (!attr) { + return false; + } + + return true; +} + SPCSSAttr *Preferences::_getInheritedStyleForPath(Glib::ustring const &prefPath) { Glib::ustring node_key, attr_key; diff --git a/src/preferences.h b/src/preferences.h index c56ba6abf8..cacf9a5a96 100644 --- a/src/preferences.h +++ b/src/preferences.h @@ -715,6 +715,10 @@ public: }); } + // is there a value present in preference defaults (skeleton preferences)? + // this function is used to verify that all settings have corresponding default values + bool is_default_valid(const Glib::ustring& path); + protected: // helper methods used by Entry /** @@ -739,6 +743,7 @@ private: void _keySplit(Glib::ustring const &pref_path, Glib::ustring &node_key, Glib::ustring &attr_key); XML::Node *_getNode(Glib::ustring const &pref_path, bool create=false); XML::Node *_findObserverNode(Glib::ustring const &pref_path, Glib::ustring &node_key, Glib::ustring &attr_key, bool create); + XML::Node* _get_node(XML::Document* document, const Glib::ustring& pref_path, bool create = false); std::string _prefs_filename; ///< Full filename (with directory) of the prefs file Glib::ustring _lastErrPrimary; ///< Last primary error message, if any. @@ -751,6 +756,8 @@ private: /// Cache for getEntry() // cache key has type std::string because Glib::ustring is slower for equality checks std::unordered_map cachedEntry; + // defaults from preferences skeleton document + XML::Document* _prefs_defaults = nullptr; ///< XML document storing defaults only /// Wrapper class for XML node observers class PrefNodeObserver; diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 3044e19f27..0817630b4f 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -162,6 +162,7 @@ set(ui_SRC dialog/prototype.cpp dialog/save-image.cpp dialog/selectorsdialog.cpp + dialog/settings-dialog.cpp dialog/startup.cpp dialog/styledialog.cpp dialog/svg-fonts-dialog.cpp @@ -397,6 +398,7 @@ set(ui_SRC dialog/prototype.h dialog/save-image.h dialog/selectorsdialog.h + dialog/settings-dialog.h dialog/startup.h dialog/styledialog.h dialog/svg-fonts-dialog.h diff --git a/src/ui/dialog/settings-dialog.cpp b/src/ui/dialog/settings-dialog.cpp new file mode 100644 index 0000000000..59062f49e8 --- /dev/null +++ b/src/ui/dialog/settings-dialog.cpp @@ -0,0 +1,1084 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Created by Michael Kowalski on 1/31/25. +// + +#include "settings-dialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "preferences.h" +#include "ui/modifiers.h" +#include "ui/widget/color-picker.h" +#include "ui/widget/ink-spin-button.h" +#include "util-string/ustring-format.h" +#include "util/action-accel.h" +#include "xml/attribute-record.h" +#include "xml/node.h" +#include "xml/repr.h" + +namespace Inkscape::UI::Dialog { + +static Glib::ustring join(const std::vector& accels) { + auto capacity = std::accumulate(accels.begin(), accels.end(), std::size_t{0}, + [](std::size_t capacity, auto& accel){ return capacity += accel.size() + 2; }); + Glib::ustring result; + result.reserve(capacity); + for (auto& accel : accels) { + if (!result.empty()) result += ", "; + result += accel; + } + return result; +} + +struct PreferencesIO : ReadWrite::IO { + PreferencesIO() {} + ~PreferencesIO() override = default; + + std::optional read(const std::string& path) override { + // printf("get at '%s'\n", path.c_str()); + if (path.starts_with("/shortcuts/")) { + // retrieve shortcut + auto subpath = path.substr(strlen("/shortcuts/")); + if (subpath.starts_with("modifiers/")) { + // modifier keys are handled separately + subpath = subpath.substr(strlen("modifiers/")); + auto sep = subpath.find('/'); + // printf("get at '%s'\n", path.c_str()); + auto m = Modifiers::Modifier::get(subpath.substr(0, sep).c_str()); + if (m) { + auto mask = m->get_and_mask(); + if (mask <= 0) { + return "0"; + } + auto key = subpath.substr(sep + 1); + if (key == "shift") { + return mask & Modifiers::Key::SHIFT ? "1" : "0"; + } + else if (key == "ctrl") { + return mask & Modifiers::Key::CTRL ? "1" : "0"; + } + else if (key == "alt") { + return mask & Modifiers::Key::ALT ? "1" : "0"; + } + else if (key == "meta") { + return mask & Modifiers::Key::META ? "1" : "0"; + } + } + return {}; + } + else { + Util::ActionAccel accel(subpath); + auto keys = accel.getShortcutText(); + return join(keys).raw(); + } + } + else if (auto entry = Preferences::get()->getEntry(path); entry.isValid()) { + return {entry.getString()}; + } + return {}; + } + + void write(const std::string& path, std::string_view value) override { + // printf("set '%s' at '%s'\n", value.data(), path.c_str()); + if (value.empty()) { + printf(" Empty pref value!\n"); + } + else if (path.starts_with("/shortcuts/")) { + //todo: set shortcuts + } + else { + Preferences::get()->setString(path, Glib::ustring(value.data(), value.size())); + } + } + + bool is_valid(const std::string& path) override { + return read(path).has_value(); + + //todo: for later... + return Preferences::get()->is_default_valid(path); + } +}; + +namespace { + +// Size of a single column in a 12-column grid; settings are built on such a grid +constexpr int ONE_COLUMN = 24; +constexpr int WHOLE = 12 * ONE_COLUMN; +constexpr int HALF = 6 * ONE_COLUMN; +constexpr int THIRD = 4 * ONE_COLUMN; +constexpr int QUARTER= 3 * ONE_COLUMN; + +const Glib::Quark NODE_KEY{"node-element"}; + +Glib::ustring get_modifier_name(std::string_view str) { + if (str.empty()) return {}; + + static std::map> key_names = { + { "Shift", _("Shift") }, +#ifdef __APPLE__ + { "Ctrl", _("Control") }, + { "Alt", _("Option") }, + { "Meta", _("Command") }, +#else + { "Ctrl", _("Ctrl") }, + { "Alt", _("Alt") }, + { "Meta", _("Meta") }, +#endif + { "Super", _("Super") }, + { "Hyper", _("Hyper") }, + }; + + if (auto it = key_names.find(str); it != key_names.end()) { + return std::string(it->second); + } + + return Glib::ustring(str.data(), str.length()); +} + +XML::Document* get_ui_xml() { + auto content = Glib::file_get_contents("/Users/mike/dev/inkscape/share/ui/settings-dialog.xml"); + return sp_repr_read_mem(content.data(), content.length(), nullptr); +} + +std::string_view element_name(const XML::Node* node) { + auto name = node->name(); + // ReSharper disable once CppDFALocalValueEscapesFunction + return std::string_view(name, name ? strlen(name) : 0); +} + +std::string_view element_attr(const XML::Node* node, const char* attr_name) { + if (!node) return {}; + auto attrib = node->attribute(attr_name); + // ReSharper disable once CppDFALocalValueEscapesFunction + return std::string_view(attrib, attrib ? strlen(attrib) : 0); +} + +Glib::ustring to_string(std::string_view str) { + return Glib::ustring(str.data(), str.length()); +} + +double to_number(std::string_view str, double default_val = 0.0) { + if (str.empty()) return default_val; + + // return std::from_chars(str.data(), str.data() + str.length(), d); + + // this is cheating, b/c those attribute strings are null-terminated + return strtod(str.data(), nullptr); +} + +int to_size(std::string_view size, int default_size) { + if (size.empty()) return default_size; + + if (size == "whole") { + return WHOLE; + } + else if (size == "half") { + return HALF; + } + else if (size == "third") { + return THIRD; + } + else if (size == "quarter") { + return QUARTER; + } + else { + std::cerr << "Element size request " << size << " not recognized"; + return default_size; + } +} + +std::string to_path(XML::Node* node) { + std::string abs_path; + while (node) { + auto path_segment = element_attr(node, "path"); + if (path_segment.size()) { + // prepend path segment + if (!abs_path.empty()) abs_path.insert(0, "/"); + abs_path.insert(abs_path.begin(), path_segment.data(), path_segment.data() + path_segment.length()); + if (abs_path[0] == '/') break; + } + + node = node->parent(); + } + return abs_path; +} + +bool read_bool(ReadWrite::IO* io, const std::string& path) { + auto value = io->read(path); + if (value->empty()) { + std::cerr << "Missing preference value for '" << path << "'. Fix preferences-skeleton.h file" << std::endl; + return false; + } + + static std::regex on{"true|on|1"}; + std::smatch match; + if (std::regex_match(*value, match, on)) { + return !match.empty(); + } + return false; +} +bool read_bool(XML::Node* node, ReadWrite::IO* io) { + return read_bool(io, to_path(node)); +} + +void validate_path(Gtk::Widget* widget, ReadWrite::IO* io, const std::string& path) { + if (io->is_valid(path)) { + widget->remove_css_class("error"); + } + else { + widget->add_css_class("error"); + } +} + +void validate(Gtk::Widget* widget, XML::Node* node, ReadWrite::IO* io) { + if (element_attr(node->parent(), "validation") == "off") { + return; + } + auto check_node = node; + // verify path requirement - for all radio buttons it is on a parent node + if (element_name(node) == "radiobutton" || element_attr(node->parent(), "type") == "radio") { + check_node = node->parent(); + } + // detect missing path attribute + auto path_segment = element_attr(check_node, "path"); + if (path_segment.empty()) { + auto name = element_name(check_node); + std::cerr << "Settings - element '" << (name.empty() ? "?" : name) << "' without 'path' property detected\n"; + } + validate_path(widget, io, to_path(node)); +} + +// initialize widget with a value read from settings +void set_widget(auto* button, XML::Node* node, ReadWrite::IO* io) { + auto active_value = element_attr(node, "value"); + if (active_value.empty()) { + auto value = read_bool(node, io); + button->set_active(value); + } + else { + auto value = io->read(to_path(node)).value_or(""); + button->set_active(value == active_value); + } +} +// ditto, but for spin button +void set_widget(Widget::InkSpinButton* button, XML::Node* node, ReadWrite::IO* io) { + auto value = io->read(to_path(node)).value_or("0"); + button->set_value(std::stod(value)); +} + +void set_widget(Gtk::Switch* switch_widget, XML::Node* node, ReadWrite::IO* io) { + auto value = read_bool(io, to_path(node)); + switch_widget->set_active(value); +} + +void set_widget(Gtk::TextView* text, XML::Node* node, ReadWrite::IO* io, char separator) { + auto value = io->read(to_path(node)).value_or(""); + std::replace(value.begin(), value.end(), separator, '\n'); + text->get_buffer()->set_text(value); +} + +Glib::ustring to_label(XML::Node* node) { + // auto label = to_string(element_attr(node, "label")); + auto label = element_attr(node, "label"); + auto translate = to_string(element_attr(node, "translate")); + if (label.empty()) return {}; + + if (translate != "no") { + //NOTE: node attribute strings are null-terminatted, and gettext relies on it here + label = gettext(label.data()); + } + else if (label[0] == '@') { // special sequence? + // todo - add other sequences to transform as/if needed + return get_modifier_name(label.substr(1)); + } + + return to_string(label); +} + +Gtk::Image* create_icon(std::string_view name) { + if (name.empty()) return nullptr; + + auto icon = Gtk::make_managed(); + icon->add_css_class("icon"); + icon->set_from_icon_name(to_string(name)); + // this call fires warnings: + // icon->set_icon_size(Gtk::IconSize::NORMAL); + return icon; +} + +int parse_element(XML::Node* node) { + auto name = element_name(node); + if (name == "gap") { + return 8; + } + else if (name == "comment") { + // skip comments + return 0; + } + else { + throw std::runtime_error(std::string("Unrecognized element in settings UI: ") + std::string(name)); + } +} + +void _subst_argument(XML::Node* node, const std::string& placeholder, const std::string& arg) { + // substitute text in attributes + for (auto& attr : node->attributeList()) { + if (!attr.value) continue; + + std::string_view value{attr.value, strlen(attr.value)}; + if (auto it = value.find(placeholder); it != std::string_view::npos) { + auto val = std::string(value.substr(0, it)) + arg + std::string(value.substr(it + placeholder.length())); + node->setAttribute(g_quark_to_string(attr.key), val); + } + } + + // substitute text in content + auto content_str = node->content(); + if (content_str && *content_str) { + auto content = std::string_view(content_str); + if (auto it = content.find(placeholder); it != std::string_view::npos) { + auto val = std::string(content.substr(0, it)) + arg + std::string(content.substr(it + placeholder.length())); + node->setContent(val.c_str()); + } + } + + // substiute in children + for (auto el = node->firstChild(); el; el = el->next()) { + _subst_argument(el, placeholder, arg); + } +} + +std::string_view get_attribute_name(int key) { + auto str = g_quark_to_string(key); + return std::string_view{str, str ? strlen(str) : 0}; +} + +void subst_arguments(XML::Node* source, XML::Node* dest) { + for (auto& attr : source->attributeList()) { + if (!attr.value) continue; + auto name = get_attribute_name(attr.key); + if (name == "template") continue; + + std::string placeholder; + placeholder.reserve(1 + name.size() + 1); + placeholder += '{'; + placeholder += name; + placeholder += '}'; + + _subst_argument(dest, placeholder, std::string(attr.value)); + } +} + +// find element in node's children, and if there is one, return its path +std::string find_shortcut(XML::Node* node) { + for (auto element = node->firstChild(); element; element = element->next()) { + auto name = element_name(element); + if (name == "shortcut") { + return to_path(element); + } + else { + auto path = find_shortcut(element); + if (!path.empty()) return path; + } + } + return {}; +} + +// read shortcut and set it on the label widget +void set_shortcut(ReadWrite::IO* io, std::string path, Gtk::Label* label) { + if (!label) return; + + if (path.empty()) { + label->set_text({}); + } + else { + auto keys = io->read(path); + label->set_text(keys.has_value() ? *keys : ""); + } +} + +// set group on radio-buttons to link them +void connect_radio_buttons(Gtk::Widget* parent) { + // link checkbox radio buttons + Gtk::CheckButton* group = nullptr; + for (auto w : parent->get_children()) { + if (auto radio = dynamic_cast(w); radio && radio->has_css_class("radio-button")) { + if (group) { + radio->set_group(*group); + } + else { + group = radio; + } + } + } + + // link toggle radio buttons + if (parent->has_css_class("radio") && parent->has_css_class("group")) { + Gtk::ToggleButton* group = nullptr; + for (auto w : parent->get_children()) { + if (auto radio = dynamic_cast(w)) { + if (group) { + radio->set_group(*group); + } + else { + group = radio; + } + } + } + } +} + +using Templates = std::map>; +using Observers = std::map>; +using Visibility = std::map>; + +// Widget construction context used while traversing XML UI file +struct Context { + Context(XML::Document& ui, const Templates& templates, ReadWrite::IO* io, Observers& observers, Visibility& visibility) + : ui(ui), templates(templates), io(io), observers(observers), visibility(visibility) + {} + + XML::Document& ui; + const Templates& templates; + ReadWrite::IO* io; + // int gap = 0; + Glib::RefPtr first_col; + Observers& observers; + Visibility& visibility; +}; + +void add_visibility_observer(Context& ctx, Gtk::Widget* widget, XML::Node* node) { + auto visible = element_attr(node, "visible"); + auto path = to_path(node); + if (visible.size()) { + path += '/'; + path += visible; + } + // check if widget should be hidden initially + if (ctx.io->read(path) != element_attr(node, "value")) { + widget->set_visible(false); + } + if (auto it = ctx.observers.find(path); it == ctx.observers.end()) { + auto vis = &ctx.visibility; + ctx.observers[path] = Preferences::PreferencesObserver::create(path, [path, vis](auto& value) { + for (auto w : (*vis)[path]) { + auto element = static_cast(w->get_data(NODE_KEY)); + if (!element) continue; + auto on = element_attr(element, "value"); + w->set_visible(value.getString().raw() == on); + } + }); + } + + if (auto it = ctx.visibility.find(path); it == ctx.visibility.end()) { + ctx.visibility[path] = {}; + } + + ctx.visibility[path].push_back(widget); +} + + +Gtk::Widget* create_ui_element(Context& ctx, XML::Node* node); +void build_ui(Context& ctx, Gtk::Widget* parent, XML::Node* node, std::function append = {}); + +// Header of the expandable panel; host for icon, label, and more +struct Header : Gtk::Box { + Header(const Glib::ustring& title, std::string_view icon_name) { + add_css_class("header"); + set_hexpand(); + _header = Gtk::make_managed(); + _header->set_has_frame(false); + _header->set_hexpand(); + _header->set_focus_on_click(false); + _header->add_css_class("button"); + auto box = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); + if (auto icon = create_icon(icon_name)) { + box->append(*icon); + } + // title + auto l = Gtk::make_managed(title); + box->append(*l); + // shortcut + _shortcut = Gtk::make_managed(); + _shortcut->add_css_class("panel-shortcut"); + _shortcut->set_xalign(0); + _shortcut->set_hexpand(); + box->append(*_shortcut); + // "expander" icon + _arrow = create_icon("pan-down"); + box->append(*_arrow); + _header->set_child(*box); + append(*_header); + } + + Gtk::Button* button() { + return _header; + } + + void set_icon(const Glib::ustring& icon) { + _arrow->set_from_icon_name(icon); + } + + Gtk::Image* _arrow; + Gtk::Button* _header; + Gtk::Label* _shortcut; +}; + +// Collapsible panel with a header +struct Panel : Gtk::Box { + Panel(bool indent) : Box(Gtk::Orientation::VERTICAL) { + add_css_class("panel"); + // content of the panel goes into the collapsible subgroup + _subgroup = Gtk::make_managed(Gtk::Orientation::VERTICAL); + _subgroup->add_css_class("group"); + if (indent) { + _subgroup->add_css_class("indent"); + } + append(*_subgroup); + } + ~Panel() override = default; + + bool is_expanded() const { + return _subgroup->get_visible(); + } + + void set_expanded(bool expand) { + _subgroup->set_visible(expand); + if (_header) _header->set_icon(expand ? "pan-down" : "pan-end"); + if (expand) { + add_css_class("open"); + } + else { + remove_css_class("open"); + } + } + + void add_header(Header* header) { + if (_header) { + std::cerr << "Panel already has header element set\n"; + return; + } + + _header = header; + prepend(*_header); + // expand/collapse panel's content + _header->button()->signal_clicked().connect([this]{ + set_expanded(!_subgroup->get_visible()); + }); + } + + Header* get_header() { + return _header; + } + + Gtk::Box* _subgroup; + Header* _header = nullptr; +}; + +struct Section : Gtk::ListBoxRow { + Section(Context& ctx, XML::Node* node): _root(node) { + _box.add_css_class("section"); + auto& content = get_content(false); + build_ui(ctx, &content, node); + + // int panel_index = 0; + for (auto widget : content.get_children()) { + if (auto panel = dynamic_cast(widget)) { + panel->set_expanded(false); + // panel_index++; + if (auto header = panel->get_header()) { + header->button()->signal_clicked().connect([this, panel] { + if (panel->is_expanded()) { + // collapse other panels, show only this one + expand_panel(panel); + } + }); + } + } + } + auto title = to_label(node); + auto label = Gtk::make_managed(title); + label->set_xalign(0); + label->set_margin_start(4); + set_child(*label); + } + + void expand_panel(Panel* expand_panel) { + auto& content = get_content(false); + for (auto widget : content.get_children()) { + if (auto panel = dynamic_cast(widget)) { + panel->set_expanded(panel == expand_panel); + } + } + } + + Gtk::Widget& get_content(bool create) { + //TODO: create on first use? + return _box; + } +private: + Gtk::Box _box{Gtk::Orientation::VERTICAL, 0}; + XML::Node* _root; +}; + + +void build_ui(Context& ctx, Gtk::Widget* parent, XML::Node* node, std::function append) { + Gtk::Widget* previous = nullptr; + + for (auto element = node->firstChild(); element; element = element->next()) { + auto name = element_name(element); + if (name == "insert") { + // insert template + auto templ_name = element_attr(element, "template"); + if (auto it = ctx.templates.find(templ_name); it != ctx.templates.end()) { + // clone template content, so child - parent relation works + auto clone = it->second->duplicate(&ctx.ui); + // pass parameters to cloned template from element + subst_arguments(element, clone); + element->appendChild(clone); + build_ui(ctx, parent, clone, append); + } + else { + //fmt lib? + throw std::runtime_error(std::string("Missing template in settings UI: ") + std::string(templ_name)); + } + } + else { + // parse nodes and create corresponding widget + auto widget = create_ui_element(ctx, element); + if (!widget) { + // current node does not represent a widget; parse all other types here + int gap = parse_element(element); + if (gap > 0 && previous) { + // using margin here, because it is cheaper + previous->set_margin_bottom(gap); + } + else { + // no previous widget, so we need to inject widget-gap + widget = Gtk::make_managed(); + widget->set_size_request(1, gap); + } + } + if (widget) { + // remember the node in a widget, so we can query it later + widget->set_data(NODE_KEY, element); + if (append) { + append(widget); + } + else { + widget->insert_at_end(*parent); + } + // add observers for elements that need to be visible conditionally only + if (!element_attr(element, "visible").empty()) { + add_visibility_observer(ctx, widget, element); + } + } + + previous = widget; + } + } + + // link radio buttons into single group, if any + connect_radio_buttons(parent); +} + +// find tooltip on given element or its group/row parents +Glib::ustring get_tooltip(XML::Node* node) { + auto tooltip = to_string(element_attr(node, "tooltip")); + while (tooltip.empty()) { + node = node->parent(); + if (!node) break; + auto name = element_name(node); + if (name == "row" || name == "group" || name == "insert" || name == "template") { + tooltip = to_string(element_attr(node, "tooltip")); + } + else { + break; + } + } + return tooltip; +} + +// given XML node, create corresponding UI widget +Gtk::Widget* create_ui_element(Context& ctx, XML::Node* node) { + auto name = element_name(node); + auto label = to_label(node); + auto tooltip = get_tooltip(node); + auto path = to_string(element_attr(node, "path")); + auto io = ctx.io; + + if (name == "panel") { + auto indent = element_attr(node, "indent"); + auto switch_path = std::string(element_attr(node, "switch")); + auto panel = Gtk::make_managed(indent.empty() || indent == "true"); + build_ui(ctx, panel->_subgroup, node, [panel](Gtk::Widget* widget) { + if (auto header = dynamic_cast(widget)) { + panel->add_header(header); + } + else { + panel->_subgroup->append(*widget); + } + }); + if (auto header = panel->get_header()) { + set_shortcut(io, find_shortcut(node), header->_shortcut); + } + return panel; + } + else if (name == "group") { + auto type = element_attr(node, "type"); + auto group = Gtk::make_managed(type == "radio" || type == "segmented" ? Gtk::Orientation::HORIZONTAL : Gtk::Orientation::VERTICAL); + group->add_css_class("group"); + if (type.size()) { + group->add_css_class(to_string(type)); + } + auto subgroup = group; + if (type == "radio" || type == "segmented") { + group->add_css_class("linked"); + } + build_ui(ctx, subgroup, node); + return group; + } + else if (name == "row") { + auto row = Gtk::make_managed(); + row->set_column_spacing(4); + row->set_row_spacing(0); + row->add_css_class("row"); + if (element_attr(node, "label").data()) { + auto l = Gtk::make_managed(label); + l->add_css_class("label"); + l->set_xalign(0); + l->set_valign(Gtk::Align::BASELINE); + l->set_tooltip_text(tooltip); + ctx.first_col->add_widget(*l); + row->attach(*l, 0, 0); + } + int new_row = 0; + build_ui(ctx, row, node, [row, &new_row](Gtk::Widget* widget){ row->attach(*widget, 1, new_row++); }); + return row; + } + else if (name == "toggle") { + auto mnemonic = false; // todo + auto toggle = Gtk::make_managed(label, mnemonic); + toggle->add_css_class("toggle"); + auto size = to_size(element_attr(node, "size"), THIRD); + toggle->set_size_request(size); + toggle->set_tooltip_text(tooltip); + validate(toggle, node, io); + set_widget(toggle, node, io); + toggle->signal_toggled().connect([toggle, node, io] { + bool on = toggle->get_active(); + auto value = element_attr(node, "value"); + if (!on) { + // radio buttons only perform write when they are checked + if (element_attr(node->parent(), "type") == "radio") return; + // normal toggle "unchecked" + value = "0"; + } + io->write(to_path(node), value); + }); + return toggle; + } + else if (name == "checkbox") { + auto checkbox = Gtk::make_managed(label); + checkbox->add_css_class("checkbox"); + checkbox->set_tooltip_text(tooltip); + checkbox->set_active(read_bool(node, io)); + checkbox->set_halign(Gtk::Align::START); + validate(checkbox, node, io); + set_widget(checkbox, node, io); + checkbox->signal_toggled().connect([checkbox, node, io]() { + auto on = checkbox->get_active(); + auto onvalue = element_attr(node, "value"); + auto offvalue = element_attr(node, "off-value"); + io->write(to_path(node), on ? (onvalue.empty() ? "1" : onvalue) : (offvalue.empty() ? "0" : offvalue)); + }); + return checkbox; + } + else if (name == "radiobutton") { + auto radiobutton = Gtk::make_managed(label); + radiobutton->add_css_class("radio-button"); + radiobutton->set_tooltip_text(tooltip); + radiobutton->set_halign(Gtk::Align::START); + validate(radiobutton, node, io); + set_widget(radiobutton, node, io); + return radiobutton; + } + else if (name == "text") { + //todo - text attributes + auto content = node->firstChild(); + auto str = content ? content->content() : nullptr; + auto text = Gtk::make_managed(str ? str : ""); + text->add_css_class("text"); + text->set_valign(Gtk::Align::BASELINE_CENTER); + auto cls = element_attr(node, "class"); + if (!cls.empty()) { + text->add_css_class(to_string(cls)); + } + return text; + } + else if (name == "number") { + auto number = Gtk::make_managed(); + number->add_css_class("number"); + auto min = element_attr(node, "min"); + auto max = element_attr(node, "max"); + if (min.empty() || max.empty()) { + std::cerr << "Missing min/max range for element in UI definition\n"; + } + number->set_range(to_number(min), to_number(max)); + if (auto step = element_attr(node, "step"); step.size()) { + number->set_step(to_number(step)); + } + if (auto digits = element_attr(node, "precision"); digits.size()) { + number->set_digits(to_number(digits)); + } + if (auto unit = element_attr(node, "unit"); unit.size()) { + number->set_suffix(to_string(unit)); + } + auto size = to_size(element_attr(node, "size"), HALF); + number->set_size_request(size); + number->set_tooltip_text(tooltip); + validate(number, node, io); + set_widget(number, node, io); + number->signal_value_changed().connect([io, node](auto value) { + static constexpr auto digits10 = std::numeric_limits::digits10; + io->write(to_path(node), Inkscape::ustring::format_classic(std::setprecision(digits10), value).raw()); + }); + return number; + } + else if (name == "shortcut") { + auto shortcut = Gtk::make_managed(); + shortcut->add_css_class("shortcut"); + shortcut->set_editable(false); + auto size = to_size(element_attr(node, "size"), WHOLE); + shortcut->set_size_request(size); + //todo: validate(shortcut, node, io); + auto keys = ctx.io->read(to_path(node)); + if (keys.has_value()) { + shortcut->set_text(*keys); + } + return shortcut; + } + else if (name == "expander") { + // auto mnemonic = false; // todo + auto button = Gtk::make_managed(); + auto box = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); + box->append(*Gtk::make_managed(label)); + auto icon = create_icon("pan-end"); + icon->set_margin_start(8); + box->append(*icon); + button->set_child(*box); + button->add_css_class("expander"); + button->set_halign(Gtk::Align::START); + button->set_has_frame(false); + button->set_focus_on_click(false); + auto panel = Gtk::make_managed(Gtk::Orientation::VERTICAL); + auto group = Gtk::make_managed(Gtk::Orientation::VERTICAL); + panel->append(*button); + panel->append(*group); + button->signal_clicked().connect([group, icon]() { + // toggle expander + group->set_visible(!group->get_visible()); + icon->set_from_icon_name(group->get_visible() ? "pan-down" : "pan-end"); + }); + build_ui(ctx, group, node); + group->set_visible(false); + button->set_tooltip_text(tooltip); + return panel; + } + else if (name == "color-picker") { + Colors::Color c{0xff000000, false}; + //todo: more than a color picker is needed here; style picker? + auto picker = Gtk::make_managed(label, tooltip, c, false, false); + picker->add_css_class("color-picker"); + picker->set_size_request(HALF); + validate(picker, node, io); + return picker; + } + else if (name == "button") { + auto button = Gtk::make_managed(label); + button->set_tooltip_text(tooltip); + button->set_size_request(HALF); + auto icon = element_attr(node, "icon"); + if (icon.size()) { + auto box = Gtk::make_managed(); + box->set_spacing(4); + box->append(*create_icon(icon)); + box->append(*Gtk::make_managed(label)); + box->set_halign(Gtk::Align::CENTER); + button->set_child(*box); + } + //todo rest - action to fire + return button; + } + else if (name == "header") { + auto icon = element_attr(node, "icon"); + auto header = Gtk::make_managed
(label, icon); + build_ui(ctx, header, node); + return header; + } + else if (name == "switch") { + auto toggle_switch = Gtk::make_managed(); + toggle_switch->add_css_class("switch"); + toggle_switch->add_css_class("small"); + toggle_switch->set_tooltip_text(tooltip); + toggle_switch->set_valign(Gtk::Align::CENTER); + validate(toggle_switch, node, io); + set_widget(toggle_switch, node, io); + // connect on/off + toggle_switch->property_state().signal_changed().connect([toggle_switch, io, node] { + io->write(to_path(node), toggle_switch->get_state() ? "1" : "0"); + }); + return toggle_switch; + } + else if (name == "path") { + //TODO: path editor + auto wnd = Gtk::make_managed(); + wnd->set_policy(Gtk::PolicyType::NEVER, Gtk::PolicyType::AUTOMATIC); + auto path_edit = Gtk::make_managed(); + path_edit->set_wrap_mode(Gtk::WrapMode::WORD); + path_edit->set_pixels_above_lines(0); + path_edit->set_pixels_below_lines(2); + path_edit->set_left_margin(3); + path_edit->set_right_margin(3); + path_edit->set_top_margin(3); + validate(wnd, node, io); + set_widget(path_edit, node, io, '|'); + wnd->set_size_request(WHOLE, 120); + wnd->set_child(*path_edit); + wnd->set_has_frame(); + path_edit->get_buffer()->signal_changed().connect([path_edit, io, node]{ + std::string value = path_edit->get_buffer()->get_text().raw(); + std::replace(value.begin(), value.end(), '\n', '|'); + io->write(to_path(node), value); + }); + return wnd; + } + else { + // all other elements are not handled here + return nullptr; + } +} + +Section* create_section(Context& ctx, XML::Node* node) { + auto section = Gtk::make_managed
(ctx, node); + auto title = element_attr(node, "label"); + auto label = Gtk::make_managed(Glib::ustring(title.data(), title.size())); + label->set_xalign(0); + label->set_margin_start(4); + section->set_child(*label); + section->set_visible(); + return section; +} + +} // namespace + +void SettingsDialog::collect_templates(XML::Node* node, Templates& templates) { + for (auto element = node->firstChild(); element; element = element->next()) { + if (element_name(element) == "template") { + if (auto name = element_attr(element, "name"); !name.empty()) { + templates[std::string(name)] = element; + } + else { + std::cerr << "Missing template name in UI settings\n"; + } + } + else { + std::cerr << "Expected element 'template' in UI settings\n"; + } + } +} + +SettingsDialog::SettingsDialog(Gtk::Window& parent): + Dialog(_("Inkscape Settings"), true), + _ui(get_ui_xml()) +{ + set_default_size(800, 600); + set_name("Settings"); + + _page_selector.append(_search); + _search.set_max_width_chars(6); + _search.set_placeholder_text(_("Search")); + _search.signal_search_changed().connect([this]() { + // filter + }); + _page_selector.append(_pages); + _page_selector.set_name("PageSelector"); + _pages.set_vexpand(); + _pages.set_name("Pages"); + _hbox.append(_page_selector); + auto sep = Gtk::make_managed(Gtk::Orientation::VERTICAL); + sep->set_size_request(1); + _hbox.append(*sep); + _hbox.append(_wnd); + _wnd.set_expand(); + _wnd.set_has_frame(false); + _wnd.set_child(_content); + _content.set_margin_start(8); + _content.set_margin_end(8); + _content.set_expand(); + + // access to preferences + _io = std::make_unique(); + + auto ui = _ui->root(); + try { + Context ctx(*_ui, _templates, _io.get(), *_observers, *_visibility); + for (auto node = ui->firstChild(); node; node = node->next()) { + auto name = element_name(node); + if (name == "templates") { + // ui templates for reuse + collect_templates(node, _templates); + } + else if (name == "section") { + // sections (or pages) + ctx.first_col = Gtk::SizeGroup::create(Gtk::SizeGroup::Mode::HORIZONTAL); + auto section = create_section(ctx, node); + _pages.append(*section); + _content.append(section->get_content(false)); + } + else if (name == "comment") { + } + else { + // anything else? + throw std::runtime_error(std::string("Unexpected element in settings UI: ") + std::string(name)); + } + } + } + catch (std::exception& ex) { + std::cerr << "Error creating settings dialog: " << ex.what() << std::endl; + } + + _pages.signal_row_selected().connect([this](Gtk::ListBoxRow* row) { + // show content of selected page + if (auto section = dynamic_cast(row)) { + for (auto c : _content.get_children()) { + _content.remove(*c); + } + _content.append(section->get_content(true)); + } + }); + if (auto row = _pages.get_row_at_index(0)) { + _pages.select_row(*row); + } + + get_content_area()->append(_hbox); + set_transient_for(parent); + set_visible(); + _pages.grab_focus(); +} + +SettingsDialog::~SettingsDialog() { + //todo: destroy widgets first + + Inkscape::GC::release(_ui); +} + +} // namespaces diff --git a/src/ui/dialog/settings-dialog.h b/src/ui/dialog/settings-dialog.h new file mode 100644 index 0000000000..2e6b10ac12 --- /dev/null +++ b/src/ui/dialog/settings-dialog.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Created by Michael Kowalski on 1/31/25. +// + +#ifndef SETTINGS_DIALOG_H +#define SETTINGS_DIALOG_H + +#include +#include +#include + +#include "preferences.h" +#include "ui/widget/font-list.h" + + +namespace Inkscape::XML { +class Node; +struct Document; +} + +namespace Inkscape::UI::Dialog { + +namespace ReadWrite { +struct IO { + virtual std::optional read(const std::string& path) = 0; + virtual void write(const std::string& path, std::string_view value) = 0; + virtual bool is_valid(const std::string& path) = 0; + virtual ~IO() = default; +}; +} // ReadWrite + +class SettingsDialog : public Gtk::Dialog { +public: + SettingsDialog(Gtk::Window& parent); + ~SettingsDialog() override; + +private: + // map of all UI templates (with heterogeneous lookup) + using Templates = std::map>; + void collect_templates(XML::Node* node, Templates& templates); + + using Observers = std::map>; + std::unique_ptr _observers = std::make_unique(); + + std::unique_ptr>> _visibility = std::make_unique>>(); + + std::unique_ptr _io; + Gtk::Box _hbox{Gtk::Orientation::HORIZONTAL, 0}; + Gtk::Box _page_selector{Gtk::Orientation::VERTICAL, 8}; + Gtk::SearchEntry2 _search; + Gtk::ListBox _pages; + Gtk::ScrolledWindow _wnd; + Gtk::Box _content{Gtk::Orientation::VERTICAL, 4}; + XML::Document* _ui; + Templates _templates; +}; + +} // namespace + +#endif //SETTINGS_DIALOG_H -- GitLab From 5f21c7233814dfd3212890926f52b5aad4c620b7 Mon Sep 17 00:00:00 2001 From: mike kowalski Date: Thu, 13 Feb 2025 06:16:03 -0800 Subject: [PATCH 2/8] Add shortcut editing in settings dialog Add shortcut editing widget and key formatting utilities. --- src/ui/dialog/settings-dialog.cpp | 148 +++++++++++++++++++++++++++++- src/ui/dialog/settings-dialog.h | 2 +- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/src/ui/dialog/settings-dialog.cpp b/src/ui/dialog/settings-dialog.cpp index 59062f49e8..5dc9f8c588 100644 --- a/src/ui/dialog/settings-dialog.cpp +++ b/src/ui/dialog/settings-dialog.cpp @@ -5,7 +5,15 @@ #include "settings-dialog.h" +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include @@ -16,6 +24,7 @@ #include "preferences.h" #include "ui/modifiers.h" +#include "ui/shortcuts.h" #include "ui/widget/color-picker.h" #include "ui/widget/ink-spin-button.h" #include "util-string/ustring-format.h" @@ -436,6 +445,36 @@ void connect_radio_buttons(Gtk::Widget* parent) { } } +void on_key_pressed(unsigned int keyval, unsigned int keycode, Gdk::ModifierType mod) { +// printf("keydown: %x %x %x\n", keyval, keycode, mod); + auto consumed_modifiers = 0u; + auto const state = static_cast(mod); + int event_group = 0; + // auto keyval2 = Inkscape::UI::Tools::get_latin_keyval_impl( + // keyval, keycode, state, event_group, &consumed_modifiers); +// printf("%x, mod %x \n", keyval2, consumed_modifiers); +} + +void edit_shortcut(Gtk::Entry& entry, ReadWrite::IO* io, const std::string& path) { + entry.set_icon_from_icon_name("close-button", Gtk::Entry::IconPosition::PRIMARY); + entry.set_alignment(Gtk::Align::CENTER); + entry.set_text(_("New accelerator...")); + entry.set_can_focus(); + entry.set_focusable(); + entry.grab_focus_without_selecting(); + entry.property_cursor_position(); + return; + // + auto& shortcuts = Inkscape::Shortcuts::getInstance(); + auto const state = static_cast(0); + unsigned int accel_key = 0; + unsigned int hardware_keycode = 0; + auto const new_shortcut_key = shortcuts.get_from(nullptr, accel_key, hardware_keycode, state, true); + // if (new_shortcut_key.is_null()) return; + + entry.set_text("x"); +} + using Templates = std::map>; using Observers = std::map>; using Visibility = std::map>; @@ -449,7 +488,6 @@ struct Context { XML::Document& ui; const Templates& templates; ReadWrite::IO* io; - // int gap = 0; Glib::RefPtr first_col; Observers& observers; Visibility& visibility; @@ -485,6 +523,83 @@ void add_visibility_observer(Context& ctx, Gtk::Widget* widget, XML::Node* node) ctx.visibility[path].push_back(widget); } +struct ShortcutEdit : Gtk::Entry { + ShortcutEdit(XML::Node* node, ReadWrite::IO* io) { + // auto shortcut = Gtk::make_managed(); + add_css_class("shortcut"); + set_editable(false); + auto pos = Gtk::Entry::IconPosition::SECONDARY; + set_icon_from_icon_name("edit", pos); + set_icon_activatable(true, pos); + signal_icon_release().connect([this, node, io, pos](auto icon){ + if (icon == pos) { + edit_shortcut(*this, io, to_path(node)); + } + else { + // cancel + end_shortcut_edit(false); + } + }); + set_can_focus(false); + set_focus_on_click(false); + set_focusable(false); + auto size = to_size(element_attr(node, "size"), WHOLE); + set_size_request(size); + //todo: validate(shortcut, node, io); + auto keys = io->read(to_path(node)); + if (keys.has_value()) { + set_text(*keys); + } + auto keyctrl = Gtk::EventControllerKey::create(); + keyctrl->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); + keyctrl->signal_key_pressed().connect([this](auto keyval, auto keycode, auto mod){ + if (keyval == GDK_KEY_Escape) { + end_shortcut_edit(false); + } + on_key_pressed(keyval, keycode, mod); + // todo + return true; + }, false); + keyctrl->signal_key_released().connect([this](auto keyval, auto keycode, auto mod){ + auto root = get_root(); + if (!root) return; + GdkKeymapKey* keymap = 0; + guint* keys = 0; + int n = 0; + gdk_display_map_keycode(root->get_display()->gobj(), keycode, nullptr /*&keymap*/, &keys, &n); + for (int i = 0; i < n; ++i) { + printf("%x ", keys[i]); + } + // auto str = gtk_accelerator_get_label_with_keycode(root->get_display()->gobj(), 0, keycode, (GdkModifierType)mod); + + // was
+
- - + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ diff --git a/share/ui/style.css b/share/ui/style.css index d9e29f0741..633dc9755f 100644 --- a/share/ui/style.css +++ b/share/ui/style.css @@ -1655,11 +1655,17 @@ listview row.top-separator { margin-bottom: 4px; padding: 0; border-radius: 3px; - border: solid 1px alpha(@theme_fg_color, 0.12); + border: solid 1px alpha(@theme_fg_color, 0.20); } #Settings .panel.open { padding-bottom: 4px; } +#Settings .panel.open .header { + border-bottom: solid 1px alpha(@theme_fg_color, 0.08); +} +#Settings .panel > .group { + padding-top: 6px; +} #Settings .panel .panel-shortcut { margin-left: 0.5em; font-weight: 500; @@ -1686,7 +1692,7 @@ listview row.top-separator { margin-left: 26px; } #Settings #PageSelector { - margin: 12px; + margin: 8px 0 0 8px; } #Settings #Pages { background-color: @theme_bg_color; @@ -1711,6 +1717,13 @@ listview row.top-separator { outline: solid 1px red; outline-offset: 0px; } +#Settings .reset-icon { + opacity: 0.6; + margin-left: 0.5em; +} +#Settings .ruler { + opacity: 0.7; +} .key-pillbox { min-width: 10px; diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 0817630b4f..3b076bb376 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -163,6 +163,7 @@ set(ui_SRC dialog/save-image.cpp dialog/selectorsdialog.cpp dialog/settings-dialog.cpp + dialog/settings-helpers.cpp dialog/startup.cpp dialog/styledialog.cpp dialog/svg-fonts-dialog.cpp @@ -399,6 +400,7 @@ set(ui_SRC dialog/save-image.h dialog/selectorsdialog.h dialog/settings-dialog.h + dialog/settings-helpers.h dialog/startup.h dialog/styledialog.h dialog/svg-fonts-dialog.h diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index f5eb8c58c2..62010ebaf7 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -1551,7 +1551,7 @@ void InkscapePreferences::symbolicThemeCheck() } } -static Cairo::RefPtr draw_color_preview(unsigned int rgb, unsigned int frame_rgb, int device_scale) { +Cairo::RefPtr draw_color_preview(unsigned int rgb, unsigned int frame_rgb, int device_scale) { int size = Widget::IconComboBox::get_image_size(); auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, size * device_scale, size * device_scale); cairo_surface_set_device_scale(surface->cobj(), device_scale, device_scale); diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index 2dcce2619f..177dc5b64a 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -709,6 +709,9 @@ private: Inkscape::PrefObserver _theme_oberver; }; +// draw circle filled with rgb color, to use as a color indicator/sample +Cairo::RefPtr draw_color_preview(unsigned int rgb, unsigned int frame_rgb, int device_scale); + } // namespace Inkscape::UI::Dialog #endif //INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H diff --git a/src/ui/dialog/settings-dialog.cpp b/src/ui/dialog/settings-dialog.cpp index cbfb6f1595..c567ea20a7 100644 --- a/src/ui/dialog/settings-dialog.cpp +++ b/src/ui/dialog/settings-dialog.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include "inkscape-application.h" #include "io/resource.h" #include "preferences.h" +#include "settings-helpers.h" #include "ui/containerize.h" #include "ui/modifiers.h" #include "ui/popup-menu.h" @@ -48,6 +50,7 @@ #include "ui/util.h" #include "ui/widget/color-picker.h" #include "ui/widget/ink-spin-button.h" +#include "ui/widget/preferences-widget.h" #include "util-string/ustring-format.h" #include "util/action-accel.h" #include "util/key-helpers.h" @@ -146,9 +149,18 @@ constexpr int WHOLE = 12 * ONE_COLUMN; constexpr int HALF = 6 * ONE_COLUMN; constexpr int THIRD = 4 * ONE_COLUMN; constexpr int QUARTER= 3 * ONE_COLUMN; +constexpr int SIXTH = 2 * ONE_COLUMN; const Glib::Quark NODE_KEY{"node-element"}; +XML::Node* get_widget_node(const Gtk::Widget* widget) { + if (!widget) return nullptr; + + return static_cast(const_cast(widget)->get_data(NODE_KEY)); +} + +const Glib::ustring settings_path("/dialogs/settings/"); + const char* get_modifier_key_name(GdkModifierType key) { static std::map> key_names = { #ifdef __APPLE__ @@ -229,6 +241,13 @@ double to_number(std::string_view str, double default_val = 0.0) { return strtod(str.data(), nullptr); } +std::string get_widget_id(const Gtk::Widget* widget) { + if (auto node = get_widget_node(widget)) { + return to_string(element_attr(node, "id")); + } + return {}; +} + int to_size(std::string_view size, int default_size) { if (size.empty()) return default_size; @@ -244,6 +263,9 @@ int to_size(std::string_view size, int default_size) { else if (size == "quarter") { return QUARTER; } + else if (size == "sixth") { + return SIXTH; + } else { std::cerr << "Element size request " << size << " not recognized"; return default_size; @@ -497,8 +519,8 @@ using Visibility = std::map>; // Widget construction context used while traversing XML UI file struct Context { - Context(XML::Document& ui, const Templates& templates, ReadWrite::IO* io, Observers& observers, Visibility& visibility) - : ui(ui), templates(templates), io(io), observers(observers), visibility(visibility) + Context(XML::Document& ui, const Templates& templates, ReadWrite::IO* io, Observers& observers, Visibility& visibility, int scaling_factor) + : ui(ui), templates(templates), io(io), observers(observers), visibility(visibility), scaling_factor(scaling_factor) {} XML::Document& ui; @@ -507,6 +529,7 @@ struct Context { Glib::RefPtr first_col; Observers& observers; Visibility& visibility; + int scaling_factor; }; void add_visibility_observer(Context& ctx, Gtk::Widget* widget, XML::Node* node) { @@ -524,7 +547,7 @@ void add_visibility_observer(Context& ctx, Gtk::Widget* widget, XML::Node* node) auto vis = &ctx.visibility; ctx.observers[path] = Preferences::PreferencesObserver::create(path, [path, vis](auto& value) { for (auto w : (*vis)[path]) { - auto element = static_cast(w->get_data(NODE_KEY)); + auto element = get_widget_node(w); if (!element) continue; auto on = element_attr(element, "value"); w->set_visible(value.getString().raw() == on); @@ -870,7 +893,16 @@ struct Panel : Gtk::Box { return _subgroup->get_visible(); } - void set_expanded(bool expand) { + // std::string get_panel_unique_id() const { + // if (auto node = get_widget_node(this)) { + // auto section = element_attr(node->parent(), "id"); + // auto id = element_attr(node, "id"); + // return std::string(section) + "_" + std::string(id); + // } + // return {}; + // } + + void set_expanded(bool expand = true) { _subgroup->set_visible(expand); if (_header) _header->set_icon(expand ? "pan-down" : "pan-end"); if (expand) { @@ -879,6 +911,14 @@ struct Panel : Gtk::Box { else { remove_css_class("open"); } + if (expand) { + // remember which panel is visible/expanded + if (auto node = get_widget_node(this)) { + // remember for each section separately + auto section = to_string(element_attr(node->parent(), "id")); + Preferences::get()->setString(settings_path + "showPanel/" + section, get_widget_id(this)); + } + } } void add_header(Header* header) { @@ -904,26 +944,49 @@ struct Panel : Gtk::Box { }; struct Section : Gtk::ListBoxRow { + template + Panel* for_each_panel(F f) { + auto& content = get_content(false); + for (auto widget : content.get_children()) { + if (auto panel = dynamic_cast(widget)) { + if (f(panel)) return panel; + } + } + return nullptr; + } + Section(Context& ctx, XML::Node* node): _root(node) { _box.add_css_class("section"); auto& content = get_content(false); build_ui(ctx, &content, node); - // int panel_index = 0; - for (auto widget : content.get_children()) { - if (auto panel = dynamic_cast(widget)) { - panel->set_expanded(false); - // panel_index++; - if (auto header = panel->get_header()) { - header->button()->signal_clicked().connect([this, panel] { - if (panel->is_expanded()) { - // collapse other panels, show only this one - expand_panel(panel); - } - }); - } + for_each_panel([this](auto panel) { + panel->set_expanded(false); + if (auto header = panel->get_header()) { + header->button()->signal_clicked().connect([this, panel] { + if (panel->is_expanded()) { + // collapse other panels, show only this one + expand_panel(panel); + } + }); } - } + return false; // not done + }); + + // for (auto widget : content.get_children()) { + // if (auto panel = dynamic_cast(widget)) { + // panel->set_expanded(false); + // // panel_index++; + // if (auto header = panel->get_header()) { + // header->button()->signal_clicked().connect([this, panel] { + // if (panel->is_expanded()) { + // // collapse other panels, show only this one + // expand_panel(panel); + // } + // }); + // } + // } + // } auto title = to_label(node); auto label = Gtk::make_managed(title); label->set_xalign(0); @@ -931,15 +994,41 @@ struct Section : Gtk::ListBoxRow { set_child(*label); } + // expand given panel, collapse all others void expand_panel(Panel* expand_panel) { - auto& content = get_content(false); - for (auto widget : content.get_children()) { - if (auto panel = dynamic_cast(widget)) { - panel->set_expanded(panel == expand_panel); - } - } + for_each_panel([expand_panel](auto panel) { + panel->set_expanded(panel == expand_panel); + return false; // not finished + }); + // auto& content = get_content(false); + // for (auto widget : content.get_children()) { + // if (auto panel = dynamic_cast(widget)) { + // panel->set_expanded(panel == expand_panel); + // } + // } } + Panel* find_panel_by_id(const std::string& id) { + return for_each_panel([&id](auto panel) { + return get_widget_id(panel) == id; + }); + // auto& content = get_content(false); + // for (auto widget : content.get_children()) { + // if (auto panel = dynamic_cast(widget)) { + // if (get_widget_id(panel) == id) { + // return panel; + // } + // } + // } + // return nullptr; + } + + Panel* get_first_panel() { + return for_each_panel([](auto panel) { + return true; // return first one found + }); + + } Gtk::Widget& get_content(bool create) { //TODO: create on first use? return _box; @@ -977,7 +1066,7 @@ void build_ui(Context& ctx, Gtk::Widget* parent, XML::Node* node, std::function< if (!widget) { // current node does not represent a widget; parse all other types here int gap = parse_element(element); - if (gap > 0 && previous) { + if (gap > 0 && previous && !dynamic_cast(previous)) { // using margin here, because it is cheaper previous->set_margin_bottom(gap); } @@ -1037,7 +1126,7 @@ Gtk::Widget* create_ui_element(Context& ctx, XML::Node* node) { if (name == "panel") { auto indent = element_attr(node, "indent"); - auto switch_path = std::string(element_attr(node, "switch")); + // auto switch_path = std::string(element_attr(node, "switch")); auto panel = Gtk::make_managed(indent.empty() || indent == "true"); build_ui(ctx, panel->_subgroup, node, [panel](Gtk::Widget* widget) { if (auto header = dynamic_cast(widget)) { @@ -1088,6 +1177,12 @@ Gtk::Widget* create_ui_element(Context& ctx, XML::Node* node) { ctx.first_col->add_widget(*l); row->attach(*l, 0, 0); } + if (element_attr(node, "reset-icon") == "yes") { + auto icon = create_icon("reset"); + icon->set_tooltip_text(_("Requires restart to take effect")); + icon->add_css_class("reset-icon"); + row->attach(*icon, 2, 0); + } int new_row = 0; build_ui(ctx, row, node, [row, &new_row](Gtk::Widget* widget){ row->attach(*widget, 1, new_row++); }); return row; @@ -1175,6 +1270,9 @@ Gtk::Widget* create_ui_element(Context& ctx, XML::Node* node) { if (auto unit = element_attr(node, "unit"); unit.size()) { number->set_suffix(to_string(unit)); } + if (auto scale = element_attr(node, "scaling-factor"); scale.size()) { + number->set_scaling_factor(to_number(scale)); + } auto size = to_size(element_attr(node, "size"), HALF); number->set_size_request(size); number->set_tooltip_text(tooltip); @@ -1285,6 +1383,46 @@ Gtk::Widget* create_ui_element(Context& ctx, XML::Node* node) { }); return wnd; } + else if (name == "selector") { + auto search = element_attr(node, "search") == "yes"; + auto selector = Settings::create_combobox(element_attr(node, "source"), ctx.scaling_factor, search); + if (!selector) { + throw std::runtime_error(std::string("Selector's source is not known: ") + std::string(element_attr(node, "source"))); + } + selector->add_css_class("selector"); + auto size = to_size(element_attr(node, "size"), WHOLE); + selector->set_size_request(size); + return selector; + } + else if (name == "font-selector") { + auto font_selector = Gtk::make_managed(); + font_selector->add_css_class("font-selector"); + auto pos = Gtk::Entry::IconPosition::SECONDARY; + font_selector->set_icon_from_icon_name("edit", pos); + font_selector->set_icon_activatable(true, pos); + font_selector->signal_icon_release().connect([node, io](auto icon){ + auto dlg = std::make_unique(); + // if (_in_edit_mode) { + // cancel_editing(); + // } + // else { + // edit_shortcut(); + // } + }); + font_selector->set_can_focus(false); + font_selector->set_focus_on_click(false); + font_selector->set_focusable(false); + auto size = to_size(element_attr(node, "size"), WHOLE); + font_selector->set_size_request(size); + + return font_selector; + } + else if (name == "ruler") { + auto ruler = Gtk::make_managed(); + ruler->set_hexpand(); + ruler->add_css_class("ruler"); + return ruler; + } else { // all other elements are not handled here return nullptr; @@ -1299,6 +1437,7 @@ Section* create_section(Context& ctx, XML::Node* node) { label->set_margin_start(4); section->set_child(*label); section->set_visible(); + section->set_data(NODE_KEY, node); return section; } @@ -1338,9 +1477,6 @@ SettingsDialog::SettingsDialog(Gtk::Window& parent): _pages.set_vexpand(); _pages.set_name("Pages"); _hbox.append(_page_selector); - auto sep = Gtk::make_managed(Gtk::Orientation::VERTICAL); - sep->set_size_request(1); - _hbox.append(*sep); _hbox.append(_wnd); _wnd.set_expand(); _wnd.set_has_frame(false); @@ -1352,9 +1488,10 @@ SettingsDialog::SettingsDialog(Gtk::Window& parent): // access to preferences _io = std::make_unique(get_root()->get_display()); + int pages = 0; auto ui = _ui->root(); try { - Context ctx(*_ui, _templates, _io.get(), *_observers, *_visibility); + Context ctx(*_ui, _templates, _io.get(), *_observers, *_visibility, get_scale_factor()); for (auto node = ui->firstChild(); node; node = node->next()) { auto name = element_name(node); @@ -1367,6 +1504,7 @@ SettingsDialog::SettingsDialog(Gtk::Window& parent): ctx.first_col = Gtk::SizeGroup::create(Gtk::SizeGroup::Mode::HORIZONTAL); auto section = create_section(ctx, node); _pages.append(*section); + pages++; _content.append(section->get_content(false)); } else if (name == "comment") { @@ -1379,8 +1517,11 @@ SettingsDialog::SettingsDialog(Gtk::Window& parent): } catch (std::exception& ex) { std::cerr << "Error creating settings dialog: " << ex.what() << std::endl; + return; } + auto selected_page = Preferences::get()->getString(settings_path + "selectedPage"); + _pages.signal_row_selected().connect([this](Gtk::ListBoxRow* row) { // show content of selected page if (auto section = dynamic_cast(row)) { @@ -1388,10 +1529,39 @@ SettingsDialog::SettingsDialog(Gtk::Window& parent): _content.remove(*c); } _content.append(section->get_content(true)); + + if (auto element = get_widget_node(row)) { + auto id = element_attr(element, "id"); + Preferences::get()->setString(settings_path + "selectedPage", std::string(id)); + } + + auto expanded = Preferences::get()->getString(settings_path + "showPanel/" + get_widget_id(section)); + auto panel = section->find_panel_by_id(expanded); + if (!panel) { + panel = section->get_first_panel(); + } + if (panel) { + section->expand_panel(panel); + } } }); - if (auto row = _pages.get_row_at_index(0)) { - _pages.select_row(*row); + + for (int i = 0; i < pages; ++i) { + if (auto row = _pages.get_row_at_index(i)) { + auto element = get_widget_node(row); + if (!element) continue; + auto id = element_attr(element, "id"); + if (id == selected_page.raw()) { + _pages.select_row(*row); + break; + } + } + } + + if (!_pages.get_selected_row()) { + if (auto row = _pages.get_row_at_index(0)) { + _pages.select_row(*row); + } } get_content_area()->append(_hbox); diff --git a/src/ui/dialog/settings-helpers.cpp b/src/ui/dialog/settings-helpers.cpp new file mode 100644 index 0000000000..4b6593b0e0 --- /dev/null +++ b/src/ui/dialog/settings-helpers.cpp @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Created by Michael Kowalski on 2/22/25. +// + +#include "settings-helpers.h" + +#include +#include +#include + +#include "inkscape-preferences.h" +#include "inkscape.h" +#include "display/control/ctrl-handle-manager.h" +#include "io/resource.h" +#include "ui/themes.h" +#include "ui/widget/icon-combobox.h" + +namespace Inkscape::UI::Dialog::Settings { + +std::pair get_ui_languages() { + + static LanguageArray 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)")}; + + static LanguageArray 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 = languages.size();// 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; + } + } + + return std::make_pair(languages, langValues); +} + +Gtk::DropDown* create_combobox(std::string_view source_name, int scale_factor, bool enable_search) { + if (source_name == "languages") { + auto list = Gtk::make_managed(); + auto languages = get_ui_languages(); + for (auto& name : languages.first) { + list->append(name); + } + if (enable_search) { + list->enable_search(); + } + return list; + } + + if (source_name == "ui-themes") { + auto list = Gtk::make_managed(); + // + auto themes = INKSCAPE.themecontext->get_available_themes(); + std::vector labels; + for (auto const &[theme, dark] : themes) { + if (theme == "Empty") continue; + if (theme == "Default") continue; + // if (theme == default_theme) { + // continue; + // } + labels.emplace_back(theme); + } + std::sort(labels.begin(), labels.end()); + labels.erase(unique(labels.begin(), labels.end()), labels.end()); + auto it = std::find(labels.begin(), labels.end(), "Inkscape"); + if (it != labels.end()) { + labels.erase(it); + labels.insert(labels.begin(), "Inkscape"); + } + + for (auto& name : labels) { + list->append(name); + } + // values.emplace_back(""); + // Glib::ustring default_theme_label = _("Use system theme"); + // default_theme_label += " (" + default_theme + ")"; + // labels.emplace_back(default_theme_label); + return list; + } + + if (source_name == "icon-themes") { + auto list = Gtk::make_managed(); + std::vector labels; + // Glib::ustring default_icon_theme = prefs->getString("/theme/defaultIconTheme"); + for (auto &&folder : IO::Resource::get_foldernames(IO::Resource::ICONS, { "application" })) { + // 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 + // auto const folder_utf8 = Glib::filename_to_utf8(folder); + // if (folder_utf8 == default_icon_theme) { + // continue; + // } + + labels.emplace_back( folder) ; + // values.emplace_back(std::move(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()); + for (auto& name : labels) { + list->append(name); + } + return list; + } + + if (source_name == "xml-themes") { + } + + if (source_name == "handle-colors") { + auto cb = Gtk::make_managed(false); + cb->set_valign(Gtk::Align::CENTER); + auto& mgr = Handles::Manager::get(); + int i = 0; + for (auto theme : mgr.get_handle_themes()) { + unsigned int frame = theme.positive ? 0x000000 : 0xffffff; // black or white + cb->add_row(draw_color_preview(theme.rgb_accent_color, frame, scale_factor), theme.title, i++); + } + cb->refilter(); + cb->set_active_by_id(mgr.get_selected_theme()); + return cb; + } + + //todo + + return Gtk::make_managed(); +} + +} // namespace diff --git a/src/ui/dialog/settings-helpers.h b/src/ui/dialog/settings-helpers.h new file mode 100644 index 0000000000..43ab13e12b --- /dev/null +++ b/src/ui/dialog/settings-helpers.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Created by Michael Kowalski on 2/22/25. +// + +#ifndef SETTINGS_HELPERS_H +#define SETTINGS_HELPERS_H +#include +#include + +#include "ui/widget/drop-down-list.h" + +namespace Inkscape::UI::Dialog::Settings { + +using LanguageArray = std::array; + +// list of translated languages and their symbols +std::pair get_ui_languages(); + +// +Gtk::DropDown* create_combobox(std::string_view source_name, int scale_factor, bool enable_search = false); + +} // namespace + +#endif //SETTINGS_HELPERS_H -- GitLab From 79da6a0b5f25672aaded0e344a5496cd36a35fba Mon Sep 17 00:00:00 2001 From: mike kowalski Date: Mon, 30 Jun 2025 10:25:19 -0700 Subject: [PATCH 7/8] Add settings UI file --- share/ui/settings-dialog.ui | 351 ++++++++++++++++++++++++++++++ share/ui/style.css | 14 +- src/inkscape.cpp | 2 + src/preferences-skeleton.h | 1 - src/ui/CMakeLists.txt | 2 + src/ui/dialog/settings-dialog.cpp | 83 +++++-- src/ui/dialog/settings-dialog.h | 17 +- src/ui/widget/property-widget.cpp | 57 +++++ src/ui/widget/property-widget.h | 61 ++++++ 9 files changed, 562 insertions(+), 26 deletions(-) create mode 100644 share/ui/settings-dialog.ui create mode 100644 src/ui/widget/property-widget.cpp create mode 100644 src/ui/widget/property-widget.h diff --git a/share/ui/settings-dialog.ui b/share/ui/settings-dialog.ui new file mode 100644 index 0000000000..3c15130fa1 --- /dev/null +++ b/share/ui/settings-dialog.ui @@ -0,0 +1,351 @@ + + + + + + Page + 0 + 0 + 4 + 8 + + + + start + Language + + 1 + 1 + + + + + + start + Welcome dialog + + 1 + 2 + + + + + + true + + 2 + 1 + + + + + + start + reset + + 3 + 1 + + + + + + true + false + center + + + 0 + 3 + 5 + + + + + + 1 + 16 + 0 + 0 + + + 0 + 1 + + + + + + Show on startup + + 2 + 2 + 2 + + + + + + start + Scale + + + 1 + 4 + + + + + + Text labels + start + + 1 + 5 + + + + + + 0 + % + 300.000000 + 50.000000 + + 2 + 5 + + + + + + start + Icons in toolbox + + 1 + 6 + + + + + + start + Icons in control bars + + 1 + 7 + + + + + + % + + 2 + 6 + + + + + + % + + 2 + 7 + + + + + + center + + + 0 + 8 + 5 + + + + + + start + Status bar + + + 1 + 9 + + + + + + Current object style + + 2 + 10 + 2 + + + + + + start + Display items + + 1 + 10 + + + + + + User Interface + + 1 + 0 + + + + + + true + true + + 4 + 1 + + + + + + /some/settings + + 4 + 2 + + + + + + + + 0 + 0 + 0 + 0 + PageSelector + 8 + vertical + + + + 8 + + + + + Pages + true + true + true + + + + + + + vertical + + + + + true + true + true + true + + + PageContent + 0 + 0 + 0 + 0 + 4 + true + true + true + true + + + + + + + + Page + + + + Viewport + + 1 + 0 + + + + + + Page + + + + Bitmap Copy + + 1 + 0 + + + + + + + Page + + + File Management + + 1 + 0 + + + + + + + Page + + + + System + + 1 + 0 + + + + + + + diff --git a/share/ui/style.css b/share/ui/style.css index 633dc9755f..4fe469bf25 100644 --- a/share/ui/style.css +++ b/share/ui/style.css @@ -1692,7 +1692,10 @@ listview row.top-separator { margin-left: 26px; } #Settings #PageSelector { - margin: 8px 0 0 8px; + margin: 16px; +} +#Settings #PageContent { + margin-top: 16px; } #Settings #Pages { background-color: @theme_bg_color; @@ -1724,6 +1727,15 @@ listview row.top-separator { #Settings .ruler { opacity: 0.7; } +#Settings .subheader { + font-weight: 500; + text-transform: uppercase; + padding: 0; + margin: 2px 0 2px 0; +} +#Settings .settings-separator { + margin: 0.75em 0; +} .key-pillbox { min-width: 10px; diff --git a/src/inkscape.cpp b/src/inkscape.cpp index 3e0c251957..bf342b6ab8 100644 --- a/src/inkscape.cpp +++ b/src/inkscape.cpp @@ -54,6 +54,7 @@ #include "ui/tools/tool-base.h" #include "ui/util.h" #include "ui/widget/generic/spin-button.h" +#include "ui/widget/property-widget.h" #include "util/font-discovery.h" static bool desktop_is_active(SPDesktop const *d) @@ -165,6 +166,7 @@ Application::Application(bool use_gui) : Inkscape::UI::ThemeContext::initialize_source_syntax_styles(); // register custom widget types Inkscape::UI::Widget::InkSpinButton::register_type(); + Inkscape::UI::Widget::PropertyWidget::register_type(); } /* set language for user interface according setting in preferences */ diff --git a/src/preferences-skeleton.h b/src/preferences-skeleton.h index 85c44d66ae..db05bdcc75 100644 --- a/src/preferences-skeleton.h +++ b/src/preferences-skeleton.h @@ -445,7 +445,6 @@ static char const preferences_skeleton[] = iconsize="16"> diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 3b076bb376..91f5287bdc 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -275,6 +275,7 @@ set(ui_SRC widget/pattern-editor.cpp widget/point.cpp widget/preferences-widget.cpp + widget/property-widget.cpp widget/random.cpp widget/registered-widget.cpp widget/registry.cpp @@ -607,6 +608,7 @@ set(ui_SRC widget/pattern-editor.h widget/point.h widget/preferences-widget.h + widget/property-widget.h widget/random.h widget/registered-enums.h widget/registered-widget.h diff --git a/src/ui/dialog/settings-dialog.cpp b/src/ui/dialog/settings-dialog.cpp index c567ea20a7..7bf73f4bf2 100644 --- a/src/ui/dialog/settings-dialog.cpp +++ b/src/ui/dialog/settings-dialog.cpp @@ -43,6 +43,7 @@ #include "io/resource.h" #include "preferences.h" #include "settings-helpers.h" +#include "ui/builder-utils.h" #include "ui/containerize.h" #include "ui/modifiers.h" #include "ui/popup-menu.h" @@ -112,7 +113,7 @@ struct PreferencesIO : ReadWrite::IO { return Util::format_accel_keys(_display, accel.getKeys()); } } - else if (auto entry = Preferences::get()->getEntry(path); entry.isValid()) { + else if (auto entry = Preferences::get()->getEntry(path); entry.isSet()) { return {entry.getString()}; } return {}; @@ -957,6 +958,8 @@ struct Section : Gtk::ListBoxRow { Section(Context& ctx, XML::Node* node): _root(node) { _box.add_css_class("section"); + if (!node) return; + auto& content = get_content(false); build_ui(ctx, &content, node); @@ -1033,6 +1036,9 @@ struct Section : Gtk::ListBoxRow { //TODO: create on first use? return _box; } + void set_content(Gtk::Widget& content) { + _box.append(content); + } private: Gtk::Box _box{Gtk::Orientation::VERTICAL, 0}; XML::Node* _root; @@ -1429,12 +1435,17 @@ Gtk::Widget* create_ui_element(Context& ctx, XML::Node* node) { } } -Section* create_section(Context& ctx, XML::Node* node) { +Section* create_section(Context& ctx, XML::Node* node, Gtk::Widget* page, const Glib::ustring& page_title) { auto section = Gtk::make_managed
(ctx, node); auto title = element_attr(node, "label"); auto label = Gtk::make_managed(Glib::ustring(title.data(), title.size())); + if (page) { + label->set_text(page_title); + section->set_content(*page); + } label->set_xalign(0); label->set_margin_start(4); + // label->set_margin(5); section->set_child(*label); section->set_visible(); section->set_data(NODE_KEY, node); @@ -1461,29 +1472,37 @@ void SettingsDialog::collect_templates(XML::Node* node, Templates& templates) { SettingsDialog::SettingsDialog(Gtk::Window& parent): Dialog(_("Inkscape Settings"), true), + _builder(create_builder("settings-dialog.ui")), + _content(get_widget(_builder, "page-content")), + _search(get_widget(_builder, "global-search")), + _pages(get_widget(_builder, "page-list")), _ui(get_ui_xml()) { set_default_size(800, 600); set_name("Settings"); - _page_selector.append(_search); - _search.set_max_width_chars(6); + auto objects = _builder->get_objects(); + + // _page_selector.append(_search); + // _search.set_max_width_chars(6); _search.set_placeholder_text(_("Search")); _search.signal_search_changed().connect([this]() { // filter }); - _page_selector.append(_pages); - _page_selector.set_name("PageSelector"); - _pages.set_vexpand(); - _pages.set_name("Pages"); - _hbox.append(_page_selector); - _hbox.append(_wnd); - _wnd.set_expand(); - _wnd.set_has_frame(false); - _wnd.set_child(_content); - _content.set_margin_start(8); - _content.set_margin_end(8); - _content.set_expand(); + // _page_selector.append(_pages); + // _page_selector.set_name("PageSelector"); + // _pages.set_vexpand(); + // _pages.set_name("Pages"); + // _hbox.append(_page_selector); + // _hbox.append(*Gtk::make_managed(Gtk::Orientation::VERTICAL)); + // _hbox.append(_wnd); + // _wnd.set_expand(); + // _wnd.set_has_frame(false); + // _wnd.set_child(_content); + // _page_selector.set_margin_end(8); + // _content.set_margin_start(8); + // _content.set_margin_end(8); + // _content.set_expand(); // access to preferences _io = std::make_unique(get_root()->get_display()); @@ -1502,7 +1521,7 @@ SettingsDialog::SettingsDialog(Gtk::Window& parent): else if (name == "section") { // sections (or pages) ctx.first_col = Gtk::SizeGroup::create(Gtk::SizeGroup::Mode::HORIZONTAL); - auto section = create_section(ctx, node); + auto section = create_section(ctx, node, 0, ""); _pages.append(*section); pages++; _content.append(section->get_content(false)); @@ -1520,6 +1539,24 @@ SettingsDialog::SettingsDialog(Gtk::Window& parent): return; } + Context ctx(*_ui, _templates, _io.get(), *_observers, *_visibility, get_scale_factor()); + for (auto obj : objects) { + auto widget = dynamic_cast(obj.get()); + if (widget && widget->get_name() == "Page") { + auto title = dynamic_cast(widget->get_child_at(1, 0)); + if (title) title->set_visible(false); + auto page = create_section(ctx, nullptr, widget, title->get_text()); + _pages.append(*page); + pages++; + } + } + + // auto test = create_section(ctx, 0, &get_widget(_builder, "ui-page"), "User Interface"); + // _pages.append(*test); + // pages++; + // _content.append(test->get_content(false)); + + auto selected_page = Preferences::get()->getString(settings_path + "selectedPage"); _pages.signal_row_selected().connect([this](Gtk::ListBoxRow* row) { @@ -1564,10 +1601,20 @@ SettingsDialog::SettingsDialog(Gtk::Window& parent): } } - get_content_area()->append(_hbox); + get_content_area()->append(get_widget(_builder, "main-box")); set_transient_for(parent); set_visible(); _pages.grab_focus(); + + auto& s = get_widget(_builder, "ui-text-scale"); + s.signal_value_changed().connect([](auto value) { + // set text scale + if (auto const settings = Gtk::Settings::get_default()) { + auto normal = 72 * 1024; + auto adjusted = static_cast(value / 100 * normal); + settings->property_gtk_xft_dpi().set_value(adjusted); + } + }); } SettingsDialog::~SettingsDialog() { diff --git a/src/ui/dialog/settings-dialog.h b/src/ui/dialog/settings-dialog.h index 1b50d064ed..fa79f84d0b 100644 --- a/src/ui/dialog/settings-dialog.h +++ b/src/ui/dialog/settings-dialog.h @@ -14,6 +14,10 @@ #include "preferences.h" +namespace Gtk { +class Builder; +} + namespace Inkscape::XML { class Node; struct Document; @@ -36,6 +40,7 @@ public: ~SettingsDialog() override; private: + Glib::RefPtr _builder; // map of all UI templates (with heterogeneous lookup) using Templates = std::map>; void collect_templates(XML::Node* node, Templates& templates); @@ -46,12 +51,12 @@ private: std::unique_ptr>> _visibility = std::make_unique>>(); std::unique_ptr _io; - Gtk::Box _hbox{Gtk::Orientation::HORIZONTAL, 0}; - Gtk::Box _page_selector{Gtk::Orientation::VERTICAL, 8}; - Gtk::SearchEntry2 _search; - Gtk::ListBox _pages; - Gtk::ScrolledWindow _wnd; - Gtk::Box _content{Gtk::Orientation::VERTICAL, 4}; + // Gtk::Box _hbox{Gtk::Orientation::HORIZONTAL, 0}; + // Gtk::Box& _page_selector;//{Gtk::Orientation::VERTICAL, 8}; + Gtk::SearchEntry2& _search; + Gtk::ListBox& _pages; + // Gtk::ScrolledWindow _wnd; + Gtk::Box& _content;//{Gtk::Orientation::VERTICAL, 4}; XML::Document* _ui; Templates _templates; }; diff --git a/src/ui/widget/property-widget.cpp b/src/ui/widget/property-widget.cpp new file mode 100644 index 0000000000..3e7f2b5f32 --- /dev/null +++ b/src/ui/widget/property-widget.cpp @@ -0,0 +1,57 @@ +// +// Created by Michael Kowalski on 6/28/25. +// + +#include "property-widget.h" + +namespace Inkscape::UI::Widget { + +#define INIT_PROPERTIES \ + _path(*this, "path", {}) + +PropertyWidget::PropertyWidget() : + Glib::ObjectBase("PropertyWidget"), + INIT_PROPERTIES { + + construct(); +} + +PropertyWidget::PropertyWidget(BaseObjectType* cobject, const Glib::RefPtr& builder): + Glib::ObjectBase("PropertyWidget"), + Gtk::DrawingArea(cobject), + INIT_PROPERTIES { + + construct(); +} + +PropertyWidget::PropertyWidget(BaseObjectType* cobject): + Glib::ObjectBase("PropertyWidget"), + Gtk::DrawingArea(cobject), + INIT_PROPERTIES { + + construct(); +} + +#undef INIT_PROPERTIES + +GType PropertyWidget::gtype = 0; + +void PropertyWidget::construct() { + set_draw_func([this](auto& ctx, auto w, auto h) { + draw_text(ctx, w, h); + }); + + property_path().signal_changed().connect([this]{ queue_draw(); }); +} + +void PropertyWidget::draw_text(const Cairo::RefPtr& ctx, int width, int height) { + if (!_design_time) return; + + ctx->select_font_face("Sans", Cairo::ToyFontFace::Slant::NORMAL, Cairo::ToyFontFace::Weight::NORMAL); + ctx->set_font_size(12); + ctx->set_source_rgb(0, 0.6, 0); + ctx->move_to(0, 12); + ctx->show_text(_path.get_value().raw()); +} + +} // namespace \ No newline at end of file diff --git a/src/ui/widget/property-widget.h b/src/ui/widget/property-widget.h new file mode 100644 index 0000000000..6ad4189844 --- /dev/null +++ b/src/ui/widget/property-widget.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Created by Michael Kowalski on 6/28/25. +// +// This is a helper widget used to store string path property in a UI file + +#ifndef PROPERTY_WIDGET_H +#define PROPERTY_WIDGET_H + +#include +#include + +namespace Gtk { +class Builder; +} + +namespace Inkscape::UI::Widget { + +class PropertyWidget : public Gtk::DrawingArea { +public: + PropertyWidget(); + PropertyWidget(BaseObjectType* cobject, const Glib::RefPtr& builder); + explicit PropertyWidget(BaseObjectType* cobject); + + ~PropertyWidget() override = default; + + void set_design_time(bool design_time); + + // Construct a C++ object from a parent (=base) C class object + static Glib::ObjectBase* wrap_new(GObject* o) { + auto obj = new PropertyWidget(GTK_DRAWING_AREA(o)); + return Gtk::manage(obj); + } + + // Register a "new" type in Glib and bind it to the C++ wrapper function + static void register_type() { + if (gtype) return; + + PropertyWidget dummy; + gtype = G_OBJECT_TYPE(dummy.gobj()); + + Glib::wrap_register(gtype, PropertyWidget::wrap_new); + } + + Glib::PropertyProxy property_path() { return _path.get_proxy(); } + +private: + void construct(); + void draw_text(const Cairo::RefPtr& ctx, int width, int height); + + bool _design_time = true; + Glib::Property _path; + + static GType gtype; + +public: +}; + +} // namespace + +#endif //PROPERTY_WIDGET_H -- GitLab From e1ae60de3d1910bd3c21d8e107373925835e7c18 Mon Sep 17 00:00:00 2001 From: mike kowalski Date: Wed, 29 Oct 2025 19:25:15 -0700 Subject: [PATCH 8/8] Add some behavior and system page settings Added two preference-specific widgets for use in UI file. --- share/ui/settings-dialog.ui | 1651 +++++++++++++++++++++++++- share/ui/settings-dialog.xml | 64 +- share/ui/style.css | 24 +- src/inkscape.cpp | 3 + src/ui/CMakeLists.txt | 2 + src/ui/dialog/settings-dialog.cpp | 38 +- src/ui/widget/preference-widgets.cpp | 58 + src/ui/widget/preference-widgets.h | 104 ++ 8 files changed, 1880 insertions(+), 64 deletions(-) create mode 100644 src/ui/widget/preference-widgets.cpp create mode 100644 src/ui/widget/preference-widgets.h diff --git a/share/ui/settings-dialog.ui b/share/ui/settings-dialog.ui index 3c15130fa1..02a48e1afe 100644 --- a/share/ui/settings-dialog.ui +++ b/share/ui/settings-dialog.ui @@ -1,6 +1,6 @@ - + Page @@ -8,7 +8,7 @@ 0 4 8 - + start @@ -249,7 +249,7 @@ - 8 + 10 @@ -292,8 +292,10 @@ + 0 + 8 Page - + Viewport @@ -305,46 +307,1661 @@ + fill + 0 + 8 Page - + + start Bitmap Copy + 1 - 0 + 1 + + + + + + start + Resolution + + 1 + 2 + + + + + + center + + + 0 + 3 + 6 + + + + + start + Clipboard + + 1 + 4 + - - - Page - File Management + fill + start + When copying objects 1 - 0 + 5 + + + + + + Copy computed style + + 2 + 5 + 2 + + + + + + clipboard1 + Copy class and style attributes verbatim + + 2 + 6 + 2 - - - - Page - - System + 0 + 0 + start + When pasting objects + + 1 + 8 + + + + + + center + + + 0 + 10 + 6 + + + + + + start + Clones + + + 1 + 11 + + + + + + baseline-fill + start + Moving + + 1 + 12 + + + + + + start + Deleting + + 1 + 16 + + + + + + start + Linked offset + + 1 + 19 + + + + + + Relink clones when duplicated with the original + + 2 + 19 + 3 + + + + + + start + Unlinking clones + + 1 + 21 + + + + + + Behaviors 1 0 + + + -1 + 14 + + 0 + 0 + + + + + + + Paste above selection + + 2 + 8 + 2 + + + + + + clipboard-paste + Paste on layer top + + 2 + 9 + 2 + + + + + + start + Command Palette + + 1 + 24 + + + + + + + + + 0 + 7 + + + + + + /options/clonecompensation/value + Moves clones in parallel + + 2 + 12 + 2 + + + + + + 1 + /options/clonecompensation/value + clone-move-1 + Does not move clones + + 2 + 13 + 2 + + + + + + 2 + /options/clonecompensation/value + clone-move-1 + Clones move according to transform + + 2 + 14 + 2 + + + + + + + + 0 + 15 + + + + + + /options/cloneorphans/value + Unlinks clones + + 2 + 16 + 2 + + + + + + 1 + /options/cloneorphans/value + clone-del-1 + Deletes clones + + 2 + 17 + 2 + + + + + + + + 0 + 18 + + + + + + + 0 + 20 + + + + + + + When performing path operations + + 2 + 21 + 2 + + + + + + "Object to path" will unlink, keeping LPEs and shapes + + 2 + 22 + 2 + + + + + + + + 0 + 23 + 6 + + + + + + 96.000000 + /options/createbitmap/resolution + Resolution used by the Create Bitmap Copy command + dpi + 6000.000000 + 1.000000 + 0 + + 2 + 2 + + + + + + start + Display options + + 1 + 25 + + + + + + Show command line argument names + + 2 + 25 + 2 + + + + + + Show untranslated names (English) + + 2 + 26 + 2 + + + + + + + + 0 + 31 + + + + + + + + 0 + 27 + 6 + + + + + + start + Guides + + 1 + 28 + + + + + + + start + Objects to guides + + 1 + 29 + + + + + + Keep + + 2 + 29 + 2 + + + + + + Treat + + 2 + 30 + 2 + + + + + + start + Rotation + + 1 + 32 + + + + + + Relative snapping of guideline angles + + 2 + 32 + 2 + + + + + + + + 0 + 33 + 6 + + + + + + start + Keyboard object manipulation + + + 1 + 34 + 3 + + + + + + start + Move increment + + 1 + 35 + + + + + + 1.000000 + 0 + + 2 + 35 + + + + + + + 0 + 37 + + + + + + Move increment relative to screen + + 2 + 36 + 3 + + + + + + start + Scale increment + + 1 + 38 + + + + + + + 2 + 38 + + + + + + + start + Inset / Outset increment + + 1 + 39 + + + + + + + 2 + 39 + + + + + + true + true + + 5 + 0 + + + + + + 0 + 8 + true + true + Page + + + File Management + + 1 + 0 + + + + + + + true + true + 0 + 8 + Page + + + + System + + 1 + 0 + + + + + + start + Color management + + + 1 + 1 + + + + + + start + User monitor profile + + 1 + 2 + + + + + + + + 0 + 5 + 8 + + + + + + start + Input devices + + + 1 + 6 + + + + + + start + Grab sensitivity + + 1 + 7 + + + + + + px + 1.000000 + 0 + + + 2 + 7 + + + + + + px + 0 + + 2 + 8 + + + + + + start + Click/drag threshold + + 1 + 8 + + + + + + Pressure-sensitive + + 2 + 9 + 3 + + + + + + Switch tool based on tablet device + + 2 + 10 + 3 + + + + + + + + 0 + 11 + 8 + + + + + + start + Transformation storage + + + 1 + 12 + 2 + + + + + + start + Default + + 1 + 13 + + + + + + Optimized + + 2 + 13 + 3 + + + + + + transf-storage + Preserved + + 2 + 14 + 3 + + + + + + + + 0 + 15 + 8 + + + + + + Viewport rendering + + + 1 + 16 + + + + + + 0 + + + 2 + 17 + + + + + + Number of threads + start + + 1 + 17 + + + + + + MiB + + + 2 + 18 + + + + + + start + Rendering cache size + + 1 + 18 + + + + + + start + X-ray radius + + 1 + 19 + + + + + + 0 + + + 2 + 19 + + + + + + % + 0 + + + 2 + 20 + + + + + + start + Outline overlay opacity + + 1 + 20 + + + + + + Update strategy + start + + 1 + 21 + + + + + + Enable OpenGL + + 2 + 22 + 3 + + + + + + + + 0 + 23 + + + + + + start + Gaussian blur display + + 1 + 24 + + + + + + Filter effects display + start + + 1 + 26 + + + + + + + + 0 + 28 + 8 + + + + + + System directories + + + 1 + 29 + + + + + + start + Shared default resources + + 1 + 30 + + + + + + + + 0 + 0 + + + + + + true + true + + 5 + 0 + + + + + + start + User configuration + + 1 + 31 + + + + + + + + + false + true + true + + + + + folder-open + + + + 2 + 30 + 4 + + + + + + + + + false + true + true + + + + + folder-open + + + + 2 + 31 + 4 + + + + + + start + • Preferences + + 1 + 32 + + + + + + + + + true + true + false + + + + + folder-open + + + + + 2 + 32 + 4 + + + + + + start + • Extensions + + 1 + 33 + + + + + + + + + + ext + true + true + false + + + + + folder-open + + + + 2 + 33 + 4 + + + + + + start + • Fonts + + 1 + 34 + + + + + + + reset + + 6 + 30 + + + + + + reset-settings + false + + 6 + 32 + + + + + + + + 7 + 0 + + + + + + + 2 + 34 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + • Icons + + 1 + 35 + + + + + + + + 2 + 35 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + • Templates + + 1 + 36 + + + + + + + + 2 + 36 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + • Symbols + + 1 + 37 + + + + + + + + 2 + 37 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + • Paint + + 1 + 38 + + + + + + + + 2 + 38 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + • Palettes + + 1 + 39 + + + + + + + + 2 + 39 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + • Keys + + 1 + 40 + + + + + + + + 2 + 40 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + • UI + + 1 + 41 + + + + + + + + 2 + 41 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + User cache + + 1 + 42 + + + + + + + + 2 + 42 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + Temporary files + + 1 + 43 + + + + + + + + 2 + 43 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + Inkscape data + + 1 + 44 + + + + + + + + 2 + 44 + 4 + + + + + true + true + + + + + folder-open + + + + + + + start + Inkscape extensions + + 1 + 45 + + + + + + + + 2 + 45 + 4 + + + + + true + true + + + + + folder-open + + + + diff --git a/share/ui/settings-dialog.xml b/share/ui/settings-dialog.xml index 7210c11af1..43d185339b 100644 --- a/share/ui/settings-dialog.xml +++ b/share/ui/settings-dialog.xml @@ -31,7 +31,7 @@