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 @@
+
+
+
+
+
+
+
+
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {label}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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