diff --git a/share/icons/hicolor/symbolic/actions/canvas-symbolic.svg b/share/icons/hicolor/symbolic/actions/canvas-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..b1e021d3ed19f1c60941f6f31c9e7655ac616fd9 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/canvas-symbolic.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/keyboard-symbolic.svg b/share/icons/hicolor/symbolic/actions/keyboard-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..2a6add3cbe7686afda9d3d496af5a38fffe70984 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/keyboard-symbolic.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/share/icons/hicolor/symbolic/actions/ui-symbolic.svg b/share/icons/hicolor/symbolic/actions/ui-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..cf6f8d53d7eacffce205d74f3e876722f87c281c --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/ui-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/share/ui/settings-dialog.ui b/share/ui/settings-dialog.ui new file mode 100644 index 0000000000000000000000000000000000000000..02a48e1afe867f2db709a8b25378b251d887d16e --- /dev/null +++ b/share/ui/settings-dialog.ui @@ -0,0 +1,1968 @@ + + + + + + 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 + + + + 10 + + + + + Pages + true + true + true + + + + + + + vertical + + + + + true + true + true + true + + + PageContent + 0 + 0 + 0 + 0 + 4 + true + true + true + true + + + + + + + + 0 + 8 + Page + + + + Viewport + + 1 + 0 + + + + + + fill + 0 + 8 + Page + + + + start + Bitmap Copy + + + 1 + 1 + + + + + + start + Resolution + + 1 + 2 + + + + + + center + + + 0 + 3 + 6 + + + + + + start + Clipboard + + + 1 + 4 + + + + + + fill + start + When copying objects + + 1 + 5 + + + + + + Copy computed style + + 2 + 5 + 2 + + + + + + clipboard1 + Copy class and style attributes verbatim + + 2 + 6 + 2 + + + + + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..43d185339bfe56295c3395b5e0ded87d1ea58885 --- /dev/null +++ b/share/ui/settings-dialog.xml @@ -0,0 +1,485 @@ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
diff --git a/share/ui/style.css b/share/ui/style.css index 15e7972653b90cd4493d0be7e96c596468a31d10..2d2e3b2640e531734dd1b665c50e8c2c3b1a46fc 100644 --- a/share/ui/style.css +++ b/share/ui/style.css @@ -1638,6 +1638,141 @@ 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.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; + opacity: 0.6; +} +#Settings .header { + min-height: 22px; + 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: 8px; +} +#Settings #PageContent { + margin-top: 16px; +} +#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's possible to spot them */ + outline: solid 1px red; + outline-offset: 0px; +} +#Settings .reset-icon { + opacity: 0.6; + margin-left: 0.5em; +} +#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; +} +#Settings .item-separator { + margin: 0.6em 0 0 0; +} +#Settings .heading { + font-weight: 500; + text-transform: uppercase; + margin-top: 0.2em; + margin-bottom: 0.5em; +} +#Settings .spin-entry-margin { + margin-bottom: 4px; +} +#Settings .page-indentation { + margin: 0 14px 0 0; +} +#Settings .page-indentation-right { + /* we need some space on the right, so scrollbar doesn't overlap buttons */ + margin: 0 5px 0 0; +} +#Settings .label-indentation { + margin-left: 14px; +} + +.key-pillbox { + min-width: 10px; + padding: 1px 2.5px; + border-radius: 2px; + margin: 0 1px; + background-color: alpha(@theme_fg_color, 0.04); + box-shadow: 1px 1px 2px alpha(black, 0.5); +} + +.dark .key-pillbox { + background-color: alpha(@theme_fg_color, 0.15); + box-shadow: 1px 1px 2px alpha(black, 0.6); +} + .border-box { border-radius: 2px; border: 1px solid @borders; diff --git a/src/actions/actions-dialogs.cpp b/src/actions/actions-dialogs.cpp index 26db490c1ce2bb9513bcdd070464145a9418796f..06362658716eef15e4c049a428e9528c19373774 100644 --- a/src/actions/actions-dialogs.cpp +++ b/src/actions/actions-dialogs.cpp @@ -21,9 +21,12 @@ #include "inkscape-application.h" #include "inkscape-window.h" +#include "preferences.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.) @@ -71,6 +74,7 @@ static const std::vector> raw_data_dialogs = { #endif {"win.dialog-toggle", N_("Toggle all dialogs"), SECTION, N_("Show or hide all dialogs") }, + {"app.settings", N_("Open Settings"), SECTION, N_("Edit Inkscape settings") }, // clang-format on }; @@ -160,6 +164,22 @@ void add_actions_dialogs(InkscapeWindow *win) return; } + gapp->add_action("settings", [win] { + Inkscape::UI::Dialog::SettingsDialog dialog(*win); + if (win->has_css_class("dark")) { + dialog.add_css_class("dark"); + } else { + dialog.add_css_class("bright"); + } + if (Inkscape::Preferences::get()->getBool("/theme/symbolicIcons", false)) { + dialog.add_css_class("symbolic"); + } else { + dialog.add_css_class("regular"); + } + Inkscape::UI::dialog_run(dialog); + dialog.close(); + }); + app->get_action_extra_data().add_data(raw_data_dialogs); } diff --git a/src/inkscape.cpp b/src/inkscape.cpp index 3e0c2519578a0b8e47a4207a3eb991d499e55594..9113f153dc8e6e24361a9d88fdf028e189f73fe0 100644 --- a/src/inkscape.cpp +++ b/src/inkscape.cpp @@ -54,6 +54,8 @@ #include "ui/tools/tool-base.h" #include "ui/util.h" #include "ui/widget/generic/spin-button.h" +#include "ui/widget/preference-widgets.h" +#include "ui/widget/property-widget.h" #include "util/font-discovery.h" static bool desktop_is_active(SPDesktop const *d) @@ -165,6 +167,9 @@ 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(); + UI::Widget::PreferenceCheckButton::register_type(); + UI::Widget::PreferenceSpinButton::register_type(); } /* set language for user interface according setting in preferences */ diff --git a/src/preferences-skeleton.h b/src/preferences-skeleton.h index 87e024a4b1755691cc6a740878b9b5819a8b2531..db05bdcc75e6e9f89015db017b6d679e44fee073 100644 --- a/src/preferences-skeleton.h +++ b/src/preferences-skeleton.h @@ -444,8 +444,10 @@ static char const preferences_skeleton[] = small="0" iconsize="16"> + id="buttons" + 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 c56ba6abf83d8820cfe4f8e3fa7a6ac93312f472..cacf9a5a964bebcb7afcf0a0bde5b802c0c524bc 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 3044e19f278ed913e49ae7ec94b7e0e581e3ae92..2ffcaf182495c799f5ea082f2624f2be5de1f9d1 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -162,6 +162,8 @@ set(ui_SRC dialog/prototype.cpp 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 @@ -273,6 +275,8 @@ set(ui_SRC widget/pattern-editor.cpp widget/point.cpp widget/preferences-widget.cpp + widget/preference-widgets.cpp + widget/property-widget.cpp widget/random.cpp widget/registered-widget.cpp widget/registry.cpp @@ -397,6 +401,8 @@ set(ui_SRC dialog/prototype.h 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 @@ -603,6 +609,8 @@ set(ui_SRC widget/pattern-editor.h widget/point.h widget/preferences-widget.h + widget/preference-widgets.h + widget/property-widget.h widget/random.h widget/registered-enums.h widget/registered-widget.h diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 8e570ec7e6addd5c6da7aac540ebc27d40c36256..62010ebaf7f2045a28c1ba3605542bfcdede0363 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -80,6 +80,8 @@ #include "util-string/ustring-format.h" #include "util/recently-used-fonts.h" #include "util/trim.h" +#include "util-string/ustring-format.h" +#include "util/key-helpers.h" #include "widgets/spw-utilities.h" namespace Inkscape::UI::Dialog { @@ -1549,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); @@ -3637,6 +3639,8 @@ void InkscapePreferences::onKBListKeyboardShortcuts() Glib::ustring old_section; Gtk::TreeStore::iterator iter_group; + auto display = get_root()->get_display(); + // Fill sections for (auto const &action : actions) { Glib::ustring section = action_data.get_section_for_action(action); @@ -3666,7 +3670,7 @@ void InkscapePreferences::onKBListKeyboardShortcuts() unsigned int key = 0; Gdk::ModifierType mod = Gdk::ModifierType(0); Gtk::Accelerator::parse(accel, key, mod); - shortcut_label += Gtk::Accelerator::get_label(key, mod) + ", "; + shortcut_label += Gtk::Accelerator::get_label(key, mod);// get_key_label(display, key, -1, mod) /* Gtk::Accelerator::get_label(key, mod)*/ + ", "; } if (shortcut_label.size() > 1) { diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index 2dcce2619f8c46310a925eab7d41c7d3901a8c70..177dc5b64aed0af6154f24774de8c85491f713f6 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 new file mode 100644 index 0000000000000000000000000000000000000000..8aa52bfc0a8008440535b1cab49e8e2a9c59c66c --- /dev/null +++ b/src/ui/dialog/settings-dialog.cpp @@ -0,0 +1,1636 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "inkscape-application.h" +#include "preferences.h" +#include "settings-helpers.h" +#include "io/resource.h" +#include "ui/builder-utils.h" +#include "ui/containerize.h" +#include "ui/modifiers.h" +#include "ui/popup-menu.h" +#include "ui/shortcuts.h" +#include "ui/util.h" +#include "ui/widget/color-picker.h" +#include "ui/widget/preferences-widget.h" +#include "ui/widget/generic/spin-button.h" +#include "util-string/ustring-format.h" +#include "util/action-accel.h" +#include "util/key-helpers.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(Glib::RefPtr display): _display(display) {} + ~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('/'); + 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); + return Util::format_accel_keys(_display, accel.getKeys()); + } + } + else if (auto entry = Preferences::get()->getEntry(path); entry.isSet()) { + 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); + } + + Glib::RefPtr _display; +}; + +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; +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__ + { GDK_SHIFT_MASK, _("⇧") }, + { GDK_CONTROL_MASK, _("^") }, + { GDK_ALT_MASK, _("⌥") }, + { GDK_META_MASK, _("⌘") }, +#else + { GDK_SHIFT_MASK, _("Shift") }, + { GDK_CONTROL_MASK, _("Ctrl") }, + { GDK_ALT_MASK, _("Alt") }, + { GDK_META_MASK, _("Meta") }, +#endif + { GDK_SUPER_MASK, _("Super") }, + { GDK_HYPER_MASK, _("Hyper") }, + }; + + if (auto it = key_names.find(key); it != key_names.end()) { + return it->second.c_str(); + } + return ""; +} + +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 fname = IO::Resource::get_path_string(IO::Resource::SYSTEM, IO::Resource::UIS, "settings-dialog.xml"); + auto content = Glib::file_get_contents(fname); + 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); +} + +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; + + if (size == "whole") { + return WHOLE; + } + else if (size == "half") { + return HALF; + } + else if (size == "third") { + return THIRD; + } + else if (size == "quarter") { + return QUARTER; + } + else if (size == "sixth") { + return SIXTH; + } + 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()); + } + } + + // substitute 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, int scaling_factor) + : ui(ui), templates(templates), io(io), observers(observers), visibility(visibility), scaling_factor(scaling_factor) + {} + + XML::Document& ui; + const Templates& templates; + ReadWrite::IO* io; + Glib::RefPtr first_col; + Observers& observers; + Visibility& visibility; + int scaling_factor; +}; + +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 = get_widget_node(w); + 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::Box* create_shortcut_label(const Glib::RefPtr& display, Gtk::Box* container, int keyval, Gdk::ModifierType modifier) { + auto create_key = [](const char* text) { + auto key = Gtk::make_managed(text); + key->add_css_class("key-pillbox"); + return key; + }; + + auto accel = Util::transform_key_value(display, keyval, modifier); + keyval = accel.get_key(); + modifier = accel.get_mod(); + int mod = static_cast(modifier); + std::array mod_keys{GDK_SHIFT_MASK, GDK_CONTROL_MASK, GDK_ALT_MASK, GDK_META_MASK, GDK_SUPER_MASK, GDK_HYPER_MASK}; + for (auto mask : mod_keys) { + if (mod & mask) { + container->append(*create_key(get_modifier_key_name(mask))); + } + } + auto key = Gtk::Accelerator::get_label(keyval, Gdk::ModifierType::NO_MODIFIER_MASK); + + if (key.size() == 1 && key[0] >= 'a' && key[0] <= 'z') { + key = key.uppercase(); + } + else if (key.empty()) { + auto name = gdk_keyval_name(keyval); + if (name) { + key = name; + } + else { + key = ""; + } + } + container->append(*create_key(key.c_str())); + + return container; +} + + +struct ShortcutEdit : Gtk::Overlay { + ShortcutEdit(XML::Node* node, ReadWrite::IO* io) : _node(node), _io(io) { + set_name("ShortcutEdit"); + add_css_class("shortcut"); + set_child(_edit); + _action_id = to_path(node).substr(strlen("/shortcuts/")); + _edit.set_editable(false); + // _edit.set_alignment(Gtk::Align::CENTER); + auto pos = Gtk::Entry::IconPosition::SECONDARY; + _edit.set_icon_from_icon_name("edit", pos); + _edit.set_icon_activatable(true, pos); + _edit.signal_icon_release().connect([this, node, io](auto icon){ + if (_in_edit_mode) { + cancel_editing(); + } + else { + edit_shortcut(); + } + }); + _edit.set_can_focus(false); + _edit.set_focus_on_click(false); + _edit.set_focusable(false); + auto size = to_size(element_attr(node, "size"), WHOLE); + _edit.set_size_request(size); + _focus->signal_leave().connect([this]{ cancel_editing(); }); + containerize(*this); + _confirm.set_size_request(size); + _confirm.set_child(_content); + _confirm.set_has_arrow(false); + _confirm.set_autohide(); + _confirm.set_parent(*this); + _confirm.signal_closed().connect([this]{ cancel_editing(); }); + _content.set_margin(4); + _content.set_hexpand(); + _content.set_row_spacing(4); + _content.set_column_spacing(4); + _message.set_max_width_chars(40); + _message.set_wrap(); + _message.set_wrap_mode(Pango::WrapMode::WORD); + _content.attach(_message, 0, 0); + auto hbox = Gtk::make_managed(); + hbox->set_halign(Gtk::Align::CENTER); + hbox->set_hexpand(); + hbox->set_spacing(4); + hbox->append(_ok); + hbox->append(_cancel); + _ok.set_size_request(QUARTER); + _cancel.set_size_request(QUARTER); + _content.attach(*hbox, 0, 1); + _cancel.signal_clicked().connect([this]{ + _confirm.popdown(); + edit_shortcut(); + }); + _ok.signal_clicked().connect([this]{ + end_shortcut_edit(_new_shortcut); + }); + + //todo: validate(shortcut, node, io); + // auto keys = io->read(to_path(node)); + + show_shortcuts(_action_id); + auto keyctrl = Gtk::EventControllerKey::create(); + keyctrl->set_propagation_phase(Gtk::PropagationPhase::CAPTURE); + + // allow Esc to cancel out of shortcut editing + keyctrl->signal_key_pressed().connect([this](auto keyval, auto keycode, auto mod){ + if (keyval == GDK_KEY_Escape && mod == Gdk::ModifierType::NO_MODIFIER_MASK) { + cancel_editing(); + } + if (_in_edit_mode && !Util::is_key_modifier(keyval)) { + // assign new shortcut + auto& shortcuts = Inkscape::Shortcuts::getInstance(); + auto state = static_cast(mod); + auto new_shortcut_key = shortcuts.get_from(nullptr, keyval, keycode, state, true); +// printf("new keyval: %x code %d mod: %x - '%s', kv: %x\n", keyval, keycode, mod, Util::get_accel_key_abbrev( new_shortcut_key).c_str(),new_shortcut_key.get_key()); + if (verify_shortcut(new_shortcut_key)) { + end_shortcut_edit(new_shortcut_key); + } + } + return true; + }, false); + + add_controller(keyctrl); + add_controller(_focus); + } + + ~ShortcutEdit() override = default; + + void edit_shortcut() { + if (_in_edit_mode) return; + + _in_edit_mode = true; + if (_keys) _keys->set_visible(false); + _edit.set_icon_from_icon_name("close-button", Gtk::Entry::IconPosition::SECONDARY); + _edit.set_can_focus(); + _edit.set_focusable(); + show_new_accel(); + } + + void cancel_editing() { + if (_in_edit_mode) { + _confirm.popdown(); + end_shortcut_edit({}); + } + } + + void end_shortcut_edit(std::optional new_key) { + if (!_in_edit_mode) return; + + _in_edit_mode = false; + _confirm.popdown(); + _edit.set_icon_from_icon_name("edit", Gtk::Entry::IconPosition::SECONDARY); + _edit.set_can_focus(false); + _edit.set_focusable(false); + // "unfocus" + if (auto root = get_root()) { + root->unset_focus(); + } + if (new_key.has_value()) { + // save + auto& shortcuts = Inkscape::Shortcuts::getInstance(); + shortcuts.add_user_shortcut(_action_id, new_key.value()); + _signal_changed.emit(*new_key); + } + show_shortcuts(_action_id); + } + + void show_new_accel() { + _edit.set_placeholder_text(_("New accelerator...")); + _edit.grab_focus(); + // _edit.grab_focus_without_selecting(); + if (_keys) _keys->set_visible(false); + } + + void show_shortcuts(const std::string& action_id) { + Util::ActionAccel accel(action_id); + show_shortcuts(accel.getKeys()); + } + + void show_shortcuts(const std::vector& keys) { + auto container = Gtk::make_managed(); + container->set_spacing(1); + container->set_valign(Gtk::Align::CENTER); + container->set_halign(Gtk::Align::CENTER); + bool first = true; + auto display = Gdk::Display::get_default(); + for (auto key : keys) { + if (!first) { + container->append(*Gtk::make_managed(", ")); + } + create_shortcut_label(display, container, key.get_key(), key.get_mod()); + first = false; + } + container->set_margin_end(16); // space for icon + + _edit.set_placeholder_text({}); + if (_keys) remove_overlay(*_keys); + _keys = container; + add_overlay(*_keys); + } + + bool verify_shortcut(const Gtk::AccelKey& new_key) { + _new_shortcut.reset(); + + if (new_key.is_null()) return false; + + // test roundtrip; gtk cannot parse what gdk created... (like shift+option+1) + auto test = Util::parse_accelerator_string(Util::get_accel_key_abbrev(new_key)); + if (Util::get_accel_key_abbrev(test).empty()) return false; + + Util::ActionAccel action_accel(_action_id); + for (auto& acc_key : action_accel.getKeys()) { + // same accelerator? + if (new_key.get_key() == acc_key.get_key() && new_key.get_mod() == acc_key.get_mod()) { + return true; + } + } + + auto iapp = InkscapeApplication::instance(); + InkActionExtraData& action_data = iapp->get_action_extra_data(); + + // Check if there is currently an action assigned to this shortcut; if yes ask if the shortcut should be reassigned + auto& shortcuts = Inkscape::Shortcuts::getInstance(); + Glib::ustring action_name; + Glib::ustring accel = Gtk::Accelerator::name(new_key.get_key(), new_key.get_mod()); + const auto& actions = shortcuts.get_actions(accel); + + for (auto possible_action : actions) { + if (action_data.isSameContext(_action_id, possible_action)) { + // TODO: Reformat the data attached here so it's compatible with action_data + action_name = possible_action; + break; + } + } + + _new_shortcut = new_key; + show_shortcuts({new_key}); + + if (!action_name.empty()) { + auto action_label = action_data.get_label_for_action(action_name); + _message.set_markup(Glib::ustring::compose(_("This shortcut is already assigned to %1."), Glib::Markup::escape_text(action_label.empty() ? action_name : action_label))); + Inkscape::UI::popup_at(_confirm, *this, get_width() / 2, get_height() + 1); + + // wait for user's confirmation + return false; + } + else { + // shortcuts.add_user_shortcut(_action_id, new_key); + + return true; // done + } + } + + sigc::signal _signal_changed; + Gtk::Popover _confirm; + Gtk::Widget* _keys = nullptr; + Gtk::Entry _edit; + Gtk::Grid _content; + Gtk::Label _message; + Gtk::Button _ok{_("Reassign")}; + Gtk::Button _cancel{_("Cancel")}; + ReadWrite::IO* _io; + XML::Node* _node; + bool _in_edit_mode = false; + std::string _action_id; + Glib::RefPtr _focus = Gtk::EventControllerFocus::create(); + std::optional _new_shortcut; +}; + + +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(); + } + + // 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) { + add_css_class("open"); + } + 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) { + 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 { + 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"); + _box.set_hexpand(); + if (!node) return; + + auto& content = get_content(false); + build_ui(ctx, &content, node); + + 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); + label->set_margin_start(4); + set_child(*label); + } + + // expand given panel, collapse all others + void expand_panel(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; + } + void set_content(Gtk::Widget& content) { + _box.append(content); + } +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 && !dynamic_cast(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); + } + }); + auto show_shortcut = [panel, io, node]{ + if (auto header = panel->get_header()) { + set_shortcut(io, find_shortcut(node), header->_shortcut); + } + }; + if (auto shortcut = dynamic_cast(find_widget_by_name(*panel->_subgroup, "ShortcutEdit", false))) { + shortcut->_signal_changed.connect([=](auto&){ + show_shortcut(); + }); + } + show_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); + } + 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; + } + else if (name == "toggle") { + auto mnemonic = false; // todo + auto toggle = Gtk::make_managed(label, mnemonic); + // ellipsize long labels, so they obey grid column constraints + if (auto l = dynamic_cast(toggle->get_children().at(0))) { + l->set_ellipsize(Pango::EllipsizeMode::END); + l->set_max_width_chars(0); + } + 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)); + } + 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); + validate(number, node, io); + set_widget(number, node, io); + number->signal_value_changed().connect([node, io](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(node, ctx.io); + 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 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; + } +} + +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); + 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), + _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"); + + 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(*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(); +_builder->get_widget("user-config-path")->set_text("/Users/mike/Library/Application Support/org.inkscape.Inkscape/config/inkscape"); + + // 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, get_scale_factor()); + + 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, 0, ""); + _pages.append(*section); + pages++; + _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; + 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)); + // printf("page: '%s'\n", title->get_text().c_str()); + if (title) title->set_visible(false); + auto page = create_section(ctx, nullptr, widget, title->get_text()); + // page->set_hexpand(); + _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) { + // 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 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); + } + } + }); + + 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(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() { + //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 0000000000000000000000000000000000000000..fa79f84d0bfc92115276ed64b4364722f0c94bdc --- /dev/null +++ b/src/ui/dialog/settings-dialog.h @@ -0,0 +1,66 @@ +// 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 + +#include "preferences.h" + + +namespace Gtk { +class Builder; +} + +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: + Glib::RefPtr _builder; + // 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 diff --git a/src/ui/dialog/settings-helpers.cpp b/src/ui/dialog/settings-helpers.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4b6593b0e0ccdd564018531d7b0c84d1e81e5f6f --- /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 0000000000000000000000000000000000000000..43ab13e12bacd8ca9c528f2dd5179aaf6cf84a61 --- /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 diff --git a/src/ui/shortcuts.cpp b/src/ui/shortcuts.cpp index a4852af060f77fd0550e104739ad3bf402a1ea72..5acdce46b411c1cecc0bc44ffe014deb594c025c 100644 --- a/src/ui/shortcuts.cpp +++ b/src/ui/shortcuts.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include "actions/actions-helper.h" #include "document.h" @@ -31,6 +33,7 @@ #include "ui/tools/tool-base.h" // For latin keyval #include "ui/util.h" #include "ui/widget/events/canvas-event.h" +#include "util/key-helpers.h" #include "xml/simple-document.h" using namespace Inkscape::IO::Resource; @@ -139,7 +142,7 @@ Shortcuts::add_user_shortcut(Glib::ustring const &detailed_action_name,const Gtk if (_add_shortcut( detailed_action_name, - trigger.get_abbrev(), + Util::get_accel_key_abbrev(trigger), true /* user shortcut */, false /* do not cache action-names */ )) { @@ -150,7 +153,7 @@ Shortcuts::add_user_shortcut(Glib::ustring const &detailed_action_name,const Gtk } std::cerr << "Shortcut::add_user_shortcut: Failed to add: " << detailed_action_name.raw() - << " with shortcut " << trigger.get_abbrev().raw() << std::endl; + << " with shortcut " << Util::get_accel_key_abbrev(trigger).raw() << std::endl; return false; }; @@ -375,7 +378,7 @@ Shortcuts::get_label(const Gtk::AccelKey& shortcut) if (!shortcut.is_null()) { // ::get_label shows key pad and numeric keys identically. // TODO: Results in labels like "Numpad Alt+5" - if (shortcut.get_abbrev().find("KP") != Glib::ustring::npos) { + if (Util::get_accel_key_abbrev(shortcut).find("KP") != Glib::ustring::npos) { label += _("Numpad"); label += " "; } @@ -397,7 +400,10 @@ get_from_event_impl(unsigned const event_keyval, unsigned const event_keycode, auto const initial_modifiers = static_cast(event_state) & default_mod_mask; auto consumed_modifiers = 0u; - auto keyval = Inkscape::UI::Tools::get_latin_keyval_impl( + auto keyval = event_keyval; + // on macOS leave Shift key and keyval case alone +#ifndef __APPLE__ + keyval = Inkscape::UI::Tools::get_latin_keyval_impl( event_keyval, event_keycode, event_state, event_group, &consumed_modifiers); // If a key value is "convertible", i.e. it has different lower case and upper case versions, @@ -407,7 +413,7 @@ get_from_event_impl(unsigned const event_keyval, unsigned const event_keycode, keyval = gdk_keyval_to_lower(keyval); consumed_modifiers &= ~static_cast(Gdk::ModifierType::SHIFT_MASK); } - +#endif // The InkscapePreferences dialog returns an event structure where the Shift modifier is not // set for keys like '('. This causes '(' to be converted to '9' by get_latin_keyval. It also // returns 'Shift-k' for 'K' (instead of 'Shift-K') but this is not a problem. @@ -934,9 +940,8 @@ bool Shortcuts::_add_shortcut(Glib::ustring const &detailed_action_name, Glib::u } #endif - Gtk::AccelKey key(str); - - auto trigger_normalized = key.get_abbrev(); + Gtk::AccelKey key = Util::parse_accelerator_string(str); + auto trigger_normalized = Util::get_accel_key_abbrev(key); // Check if action actually exists. Need to compare action names without values... Glib::ustring action_name; @@ -962,7 +967,7 @@ bool Shortcuts::_add_shortcut(Glib::ustring const &detailed_action_name, Glib::u _remove_shortcuts(detailed_action_name); } - auto const trigger = Gtk::ShortcutTrigger::parse_string(trigger_normalized); + auto trigger = Gtk::KeyvalTrigger::create(key.get_key(), key.get_mod()); g_assert(trigger); auto const action = Gtk::NamedAction::create(action_name); diff --git a/src/ui/widget/preference-widgets.cpp b/src/ui/widget/preference-widgets.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4a109fa5b11ef40bc632ad8f463bd087b6b052d2 --- /dev/null +++ b/src/ui/widget/preference-widgets.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Created by Michael Kowalski on 10/24/25. +// + +#include "preference-widgets.h" + +namespace Inkscape::UI::Widget { + +GType PreferenceCheckButton::gtype = 0; + +void PreferenceCheckButton::construct() { + auto set_value = [this]{ + auto path = prop_path.get_value(); + if (!path.empty()) { + if (get_accessible_role() == Role::RADIO) { + // radio button + auto value = Preferences::get()->getInt(path); + set_active(value == prop_enum.get_value()); + } + else { + // check box + auto value = Preferences::get()->getBool(path); + set_active(value); + } + } + }; + property_pref_path().signal_changed().connect([set_value] { set_value(); }); + set_value(); + +printf("PrefCheckBtn %p - path %s\n",this,prop_path.get_value().c_str()); +} + +GType PreferenceSpinButton::gtype = 0; + +void PreferenceSpinButton::construct() { + auto set_num_value = [this]{ + auto path = prop_path.get_value(); + if (!path.empty()) { + if (get_digits() == 0) { + // no decimal digits - use integer + auto value = Preferences::get()->getInt(path); + set_value(value); + } + else { + auto value = Preferences::get()->getDouble(path); + set_value(value); + } + } + }; + property_pref_path().signal_changed().connect([set_num_value] { set_num_value(); }); + set_num_value(); + + printf("PrefCheckBtn %p - path %s\n",this,prop_path.get_value().c_str()); +} + +} // namespace + diff --git a/src/ui/widget/preference-widgets.h b/src/ui/widget/preference-widgets.h new file mode 100644 index 0000000000000000000000000000000000000000..9d7a12bdca98fc60ee01318a105144f6da37fadf --- /dev/null +++ b/src/ui/widget/preference-widgets.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Created by Michael Kowalski on 10/24/25. +// + +#ifndef INKSCAPE_PREFERENCEWIDGETS_H +#define INKSCAPE_PREFERENCEWIDGETS_H + +#include +#include + +#include "generic/spin-button.h" +#include "preferences.h" + +namespace Inkscape::UI::Widget { + +class PreferenceCheckButton : public Gtk::CheckButton { +public: + PreferenceCheckButton(BaseObjectType* cobject, const Glib::RefPtr& builder) + : Glib::ObjectBase("PrefCheckButton"), Gtk::CheckButton(cobject), + prop_path(*this, "pref-path"), + prop_enum(*this, "pref-enum") + { + construct(); + } + PreferenceCheckButton() : Glib::ObjectBase("PrefCheckButton"), + prop_path(*this, "pref-path"), + prop_enum(*this, "pref-enum") + { + construct(); + } + + Glib::PropertyProxy property_pref_path() { + return prop_path.get_proxy(); + } + + static Glib::ObjectBase* wrap_new(GObject* o) { + Glib::RefPtr builder; + auto obj = new PreferenceCheckButton(GTK_CHECK_BUTTON(o), builder); + 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; + + PreferenceCheckButton dummy; + gtype = G_OBJECT_TYPE(dummy.gobj()); + + Glib::wrap_register(gtype, PreferenceCheckButton::wrap_new); + } +private: + static GType gtype; + void construct(); + Glib::Property prop_path; + Glib::Property prop_enum; +}; + +class PreferenceSpinButton : public InkSpinButton { +public: + PreferenceSpinButton(BaseObjectType* cobject, const Glib::RefPtr& builder) + : Glib::ObjectBase("PrefSpinButton"), InkSpinButton(cobject), + prop_path(*this, "pref-path"), + prop_value(*this, "pref-value") + { + construct(); + } + PreferenceSpinButton() : Glib::ObjectBase("PrefSpinButton"), + prop_path(*this, "pref-path"), + prop_value(*this, "pref-value") + { + construct(); + } + + Glib::PropertyProxy property_pref_path() { + return prop_path.get_proxy(); + } + + static Glib::ObjectBase* wrap_new(GObject* o) { + Glib::RefPtr builder; + auto obj = new PreferenceSpinButton(GTK_WIDGET(o), builder); + 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; + + PreferenceSpinButton dummy; + gtype = G_OBJECT_TYPE(dummy.gobj()); + + Glib::wrap_register(gtype, PreferenceSpinButton::wrap_new); + } +private: + static GType gtype; + void construct(); + Glib::Property prop_path; + Glib::Property prop_value; +}; + + +} + +#endif //INKSCAPE_PREFERENCEWIDGETS_H diff --git a/src/ui/widget/property-widget.cpp b/src/ui/widget/property-widget.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3e7f2b5f32b7d9b8901e2a0df81ec08167ac4e4c --- /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 0000000000000000000000000000000000000000..6ad41898446412c6862a25cc95b1665dea2697c6 --- /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 diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 6318dcc7c65d109d042fbb2902f2d1da15f98ff2..afe825f1ef8b979c8861268608f6ca1da2bc4c79 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -12,6 +12,7 @@ set(util_SRC funclog.cpp glyph-draw.cpp share.cpp + key-helpers.cpp object-renderer.cpp object-modified-tags.cpp paper.cpp @@ -49,6 +50,7 @@ set(util_SRC funclog.h glyph-draw.h hybrid-pointer.h + key-helpers.h longest-common-suffix.h object-renderer.h object-modified-tags.h diff --git a/src/util/action-accel.cpp b/src/util/action-accel.cpp index 2ca5904bae079867b4af37af70083d4f0025a30e..9f96f6049792863eabb98062f1b3945fde9123ad 100644 --- a/src/util/action-accel.cpp +++ b/src/util/action-accel.cpp @@ -16,6 +16,7 @@ #include #include "inkscape-application.h" +#include "key-helpers.h" #include "ui/shortcuts.h" namespace Inkscape::Util { @@ -54,7 +55,10 @@ bool ActionAccel::_query() } auto const &accels = Shortcuts::getInstance().get_triggers(_action); - std::set new_keys{accels.begin(), accels.end()}; + std::set new_keys; + for (auto& acc : accels) { + new_keys.insert(parse_accelerator_string(acc)); + } if (new_keys == _accels) { return false; } diff --git a/src/util/key-helpers.cpp b/src/util/key-helpers.cpp new file mode 100644 index 0000000000000000000000000000000000000000..70728e2cde59a40a0fdfbae85fcc2ba63c6ba1fd --- /dev/null +++ b/src/util/key-helpers.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Created by Michael Kowalski on 2/13/25. +// + +#include "key-helpers.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Inkscape::Util { + +Gtk::AccelKey transform_key_value(const Glib::RefPtr& display, int keyval, Gdk::ModifierType mod) { + if (!display) return Gtk::AccelKey(keyval, mod); + +#ifdef __APPLE__ + // Special treatment for all key combinations with