diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2fb79b801b8ef4aec94234ec97140e667cc77af8..5dcbf95ed79eb3226d3191ce7b7539994f75e1d9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -123,6 +123,7 @@ option(WITH_LIBCDR "Compile with support of libcdr for CorelDRAW Diagrams" ON)
option(WITH_LIBVISIO "Compile with support of libvisio for Microsoft Visio Diagrams" ON)
option(WITH_LIBWPG "Compile with support of libwpg for WordPerfect Graphics" ON)
option(WITH_GSPELL "Compile with support of gspell" ON)
+option(WITH_GSOURCEVIEW "Compile with support of gsourceview (text syntax coloring)" ON)
option(WITH_NLS "Compile with Native Language Support (using gettext)" ON)
option(WITH_JEMALLOC "Compile with JEMALLOC support" OFF)
option(WITH_ASAN "Compile with Clang's AddressSanitizer (for debugging purposes)" OFF)
@@ -282,6 +283,7 @@ message("ENABLE_POPPLER: ${ENABLE_POPPLER}")
message("ENABLE_POPPLER_CAIRO: ${ENABLE_POPPLER_CAIRO}")
message("WITH_GNU_READLINE: ${WITH_GNU_READLINE}")
message("WITH_GSPELL: ${WITH_GSPELL}")
+message("WITH_GSOURCEVIEW: ${WITH_GSOURCEVIEW}")
message("WITH_IMAGE_MAGICK: ${WITH_IMAGE_MAGICK}")
message("WITH_GRAPHICS_MAGICK: ${WITH_GRAPHICS_MAGICK}")
message("WITH_LIBCDR: ${WITH_LIBCDR}")
diff --git a/CMakeScripts/DefineDependsandFlags.cmake b/CMakeScripts/DefineDependsandFlags.cmake
index ee4ca22582e3d4a02176ff08a71b4a35728077b3..de815251bf4f45662a067bbef51b14ecf30a3cb8 100644
--- a/CMakeScripts/DefineDependsandFlags.cmake
+++ b/CMakeScripts/DefineDependsandFlags.cmake
@@ -283,6 +283,18 @@ if(WITH_GSPELL)
endif()
endif()
+if(WITH_GSOURCEVIEW)
+ pkg_check_modules(GSOURCEVIEW gtksourceview-4)
+ if("${GSOURCEVIEW_FOUND}")
+ message(STATUS "Using gtksourceview-4")
+ list(APPEND INKSCAPE_INCS_SYS ${GSOURCEVIEW_INCLUDE_DIRS})
+ sanitize_ldflags_for_libs(GSOURCEVIEW_LDFLAGS)
+ list(APPEND INKSCAPE_LIBS ${GSOURCEVIEW_LDFLAGS})
+ else()
+ set(WITH_GSOURCEVIEW OFF)
+ endif()
+endif()
+
find_package(Boost 1.19.0 REQUIRED COMPONENTS filesystem)
if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 7 AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9)
diff --git a/CMakeScripts/InstallMSYS2.cmake b/CMakeScripts/InstallMSYS2.cmake
index 2d732ac6af8500aa04fc0c3e7298b539494717a4..99d74b26211eddb7ab034082976435617b474d4f 100644
--- a/CMakeScripts/InstallMSYS2.cmake
+++ b/CMakeScripts/InstallMSYS2.cmake
@@ -59,6 +59,7 @@ if(WIN32)
${MINGW_BIN}/libgspell-1-[0-9]*.dll
${MINGW_BIN}/libgtk-3-[0-9]*.dll
${MINGW_BIN}/libgtkmm-3.0-[0-9]*.dll
+ ${MINGW_BIN}/libgtksourceview-4-[0-9]*.dll
${MINGW_BIN}/libharfbuzz-[0-9]*.dll
${MINGW_BIN}/libheif.dll
${MINGW_BIN}/libiconv-[0-9]*.dll
@@ -200,6 +201,9 @@ if(WIN32)
install(DIRECTORY ${MINGW_PATH}/share/glib-2.0/schemas
DESTINATION share/glib-2.0)
+ install(DIRECTORY ${MINGW_PATH}/share/gtksourceview-4
+ DESTINATION share)
+
# fontconfig
install(DIRECTORY ${MINGW_PATH}/etc/fonts
DESTINATION etc
diff --git a/buildtools/msys2installdeps.sh b/buildtools/msys2installdeps.sh
index 98dd4c562396c4cd3fffda91f4b2b3a1f237ee46..7d68e3d640bbe5f598d745abc8b1c7f4a8ba0b44 100644
--- a/buildtools/msys2installdeps.sh
+++ b/buildtools/msys2installdeps.sh
@@ -66,6 +66,7 @@ $ARCH-libwpg \
$ARCH-aspell \
$ARCH-aspell-en \
$ARCH-gspell \
+$ARCH-gtksourceview4 \
$ARCH-graphicsmagick \
$ARCH-libjxl
diff --git a/config.h.cmake b/config.h.cmake
index 29a5826e4a3514e4d10dab6ac3f6d256dc207a2f..5035015589be963fef1c86e2167f2f06a4b0b5bd 100644
--- a/config.h.cmake
+++ b/config.h.cmake
@@ -71,6 +71,9 @@
/* enable gtk spelling widget and built-in spellchecker */
#cmakedefine WITH_GSPELL 1
+/* enable gtk syntax coloring text view widget */
+#cmakedefine WITH_GSOURCEVIEW 1
+
/* Image/Graphics Magick++ support for bitmap effects */
#cmakedefine WITH_MAGICK 1
diff --git a/po/POTFILES.ui.in b/po/POTFILES.ui.in
index 493cdb5b4708ad6b4dc05d7122e1bf02da41412a..7eea602a5c2e44ee18c0040e7a9f42d1ed70fb55 100644
--- a/po/POTFILES.ui.in
+++ b/po/POTFILES.ui.in
@@ -1,3 +1,4 @@
+../share/ui/attribute-edit-component.glade
../share/ui/color-palette.glade
../share/ui/command-palette-operation.glade
../share/ui/completion-box.glade
diff --git a/share/icons/hicolor/scalable/actions/rounding.svg b/share/icons/hicolor/scalable/actions/rounding.svg
new file mode 100644
index 0000000000000000000000000000000000000000..233d16fa00eeda36555ab48c834c51359dc395ef
--- /dev/null
+++ b/share/icons/hicolor/scalable/actions/rounding.svg
@@ -0,0 +1,83 @@
+
+
+
+
diff --git a/share/icons/hicolor/symbolic/actions/rounding-symbolic.svg b/share/icons/hicolor/symbolic/actions/rounding-symbolic.svg
new file mode 100644
index 0000000000000000000000000000000000000000..233d16fa00eeda36555ab48c834c51359dc395ef
--- /dev/null
+++ b/share/icons/hicolor/symbolic/actions/rounding-symbolic.svg
@@ -0,0 +1,83 @@
+
+
+
+
diff --git a/share/icons/multicolor/symbolic/actions/rounding-symbolic.svg b/share/icons/multicolor/symbolic/actions/rounding-symbolic.svg
new file mode 100644
index 0000000000000000000000000000000000000000..233d16fa00eeda36555ab48c834c51359dc395ef
--- /dev/null
+++ b/share/icons/multicolor/symbolic/actions/rounding-symbolic.svg
@@ -0,0 +1,83 @@
+
+
+
+
diff --git a/share/ui/CMakeLists.txt b/share/ui/CMakeLists.txt
index 4e1843894dab8b7201b159ccf098b5e1ec9afa6d..2a4627b68b7f7dfb9a03b123c6ed0e38d82ed9a0 100644
--- a/share/ui/CMakeLists.txt
+++ b/share/ui/CMakeLists.txt
@@ -1,6 +1,9 @@
# SPDX-License-Identifier: GPL-2.0-or-later
-file(GLOB _FILES "*.xml" "*.rc" "*.css" "*.ui" "*.glade" "*.svg" "*.ini")
+file(GLOB _FILES "*.xml" "*.rc" "*.css" "*.ui" "*.glade" "*.svg" "*.ini" "*.lang")
install(FILES ${_FILES} DESTINATION ${INKSCAPE_SHARE_INSTALL}/ui)
file(GLOB _RESOURCES "resources/*.png" "resources/*.svg")
install(FILES ${_RESOURCES} DESTINATION ${INKSCAPE_SHARE_INSTALL}/ui/resources)
+
+file(GLOB _SYNTAX_THEMES "syntax-themes/*.xml")
+install(FILES ${_SYNTAX_THEMES} DESTINATION ${INKSCAPE_SHARE_INSTALL}/ui/syntax-themes)
diff --git a/share/ui/attribute-edit-component.glade b/share/ui/attribute-edit-component.glade
new file mode 100644
index 0000000000000000000000000000000000000000..e4537d611ad8a522048ba4e2c0de8588418f2cb3
--- /dev/null
+++ b/share/ui/attribute-edit-component.glade
@@ -0,0 +1,335 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/share/ui/inline-css.lang b/share/ui/inline-css.lang
new file mode 100644
index 0000000000000000000000000000000000000000..29a04df24718c4421ff643fb0e5f4d0a1afc30fd
--- /dev/null
+++ b/share/ui/inline-css.lang
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/share/ui/style.css b/share/ui/style.css
index b19b8078b8ae618e5e898e96826a1629b59f488f..45c04cab7db42b56e824fd4d1159aac80caade2a 100644
--- a/share/ui/style.css
+++ b/share/ui/style.css
@@ -168,9 +168,8 @@ notebook.blink > header > tabs > tab:checked {
color: inherit;
}
-/* attribute dialog popup size */
-.attrpop textview {
- font-size: 13px;
+.mono-font {
+ /* note: monospaced font is set by ThemeContext::adjust_global_font_scale() */
}
/*
@@ -225,12 +224,12 @@ iconview *:hover {
}
.menu-category {
- font-size: small;
+ font-size: smaller;
font-weight: bold;
}
.small-heading {
- font-size: small;
+ font-size: smaller;
font-weight: bold;
opacity: 0.5;
}
@@ -250,7 +249,7 @@ iconview *:hover {
}
-/* Selector and XML dialog */
+/* Selector dialog */
#SelectorsAndStyleDialog button {
padding: 4px;
min-height: 16px;
@@ -266,14 +265,12 @@ iconview *:hover {
padding-bottom: 0;
}
-#XMLAndAttributesDialog .radio.image-button,
#SelectorsAndStyleDialog .radio.image-button {
margin: 0;
padding: 0px;
border-radius: 2px 0 0 2px;
}
-#XMLAndAttributesDialog .radio.image-button:last-child,
#SelectorsAndStyleDialog .radio.image-button:last-child {
border-radius: 0 2px 2px 0;
border-left-width: 0;
@@ -455,7 +452,7 @@ SPCanvas {
#Objects .search {
padding: 2px 4px;
min-height: 16px;
- font-size: small;
+ font-size: smaller;
}
#SimpleFilterModifier #InkSpinScale {
@@ -595,7 +592,7 @@ scale.tight-slider {
/* add .small-marks class to Gtk::Scale to have smaller font used, since markup didn't work there */
scale.small-marks.marks-after {
- font-size: small;
+ font-size: smaller;
}
/* Square toolbar buttons: request square buttons for all toolbars. */
@@ -617,7 +614,7 @@ button.square-button image {
}
#StyleSwatch label {
- font-size: small;
+ font-size: smaller;
}
#DocumentProperties #NotebookPage {
@@ -646,7 +643,7 @@ button.square-button image {
}
.small-font {
- font-size: small;
+ font-size: smaller;
}
.toolbar-separator {
diff --git a/share/ui/svgd.lang b/share/ui/svgd.lang
new file mode 100644
index 0000000000000000000000000000000000000000..0c86bd8b660817f4de923ef02b69d7911fdbda07
--- /dev/null
+++ b/share/ui/svgd.lang
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (?m)[\s,\r\n]*
+ e[+\-]?\d+
+ \d*\.\d+|\d+
+ \%{fractional-constant}(\%{exponent})?
+ [+\-]?\%{number}
+ \+?\%{number}
+ [MLHVCSQTAZ]
+ [^\-+\s\r\n,\.MLHVCSQTAZmlhvcsqtaz0-9]
+ (?m)(?=\%{command-letter}|\%{invalid})
+
+
+
+ \%{command-letter}
+
+
+
+ \%{invalid}
+
+
+
+
+ \%{coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+ \%{coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+ \%{coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+
+ \%{coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+ \%{coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+
+ \%{coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+
+ \%{coordinate}
+ (0|1)\%{maybecwsp}(0|1)
+
+
+
+
+
+
+
+
+
+
+ \%{positive-coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+
+ \%{positive-coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+
+ \%{coordinate}
+
+
+
+
+
+
+
+
+ M|L|T
+ \%{command-end}
+
+
+
+
+
+
+
+
+ H|V
+ \%{command-end}
+
+
+
+
+
+
+
+
+ S|Q
+ \%{command-end}
+
+
+
+
+
+
+
+
+
+ C
+ \%{command-end}
+
+
+
+
+
+
+
+
+
+ A
+ \%{command-end}
+
+
+
+
+
+
+
+ Z
+
+
+
+
+
+
+
diff --git a/share/ui/svgpoints.lang b/share/ui/svgpoints.lang
new file mode 100644
index 0000000000000000000000000000000000000000..f341af66e9ce29297651c64422a1c62b296b7573
--- /dev/null
+++ b/share/ui/svgpoints.lang
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+ e[+\-]?\d+
+ \d*\.\d+|\d+
+ \%{fractional-constant}(\%{exponent})?
+ [+\-]?\%{number}
+ [^\-+\s\r\n,\.0-9eE]
+
+
+
+ \%{invalid}
+
+
+
+
+
+
+
+ \%{coordinate}
+ \%{coordinate}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/share/ui/syntax-themes/syntax-theme-dark.xml b/share/ui/syntax-themes/syntax-theme-dark.xml
new file mode 100644
index 0000000000000000000000000000000000000000..74ddf0b370cd0dbd16bcfe94663608439a813456
--- /dev/null
+++ b/share/ui/syntax-themes/syntax-theme-dark.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+ MK
+ <_description>Dark color scheme for syntax highlighting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/share/ui/syntax-themes/syntax-theme-light.xml b/share/ui/syntax-themes/syntax-theme-light.xml
new file mode 100644
index 0000000000000000000000000000000000000000..78e6ae1414249db7492ec7a9bd3cb261f35351c9
--- /dev/null
+++ b/share/ui/syntax-themes/syntax-theme-light.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+ MK
+ <_description>Light color scheme for syntax highlighting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/share/ui/syntax-themes/syntax-theme-none.xml b/share/ui/syntax-themes/syntax-theme-none.xml
new file mode 100644
index 0000000000000000000000000000000000000000..307f032c32a3c537bcdb4508e16bf99287717865
--- /dev/null
+++ b/share/ui/syntax-themes/syntax-theme-none.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ MK
+ <_description>Empty color scheme to disable syntax highlighting
+
+
+
+
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 8b5c0ef61e86fbe664dd2ca8d91b513de11adf5f..d5b9d543640dae336f4f9797b114d5d0622bcc47 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -48,6 +48,7 @@ parts:
- libgspell-1-dev
- libgtk-3-dev
- libgtkmm-3.0-dev
+ - libgtksourceview-4-dev
- libharfbuzz-dev
- liblcms2-dev
- libmagick++-dev
@@ -82,6 +83,8 @@ parts:
- libgslcblas0
- libgspell-1-2
- libgtkmm-3.0-1v5
+ - libgtksourceview-4-common
+ - libgtksourceview-4-0
- liblcms2-2
- libmagick++-6.q16-8
- libpangomm-1.4-1v5
diff --git a/src/attribute-rel-util.cpp b/src/attribute-rel-util.cpp
index f88cf8b449bcb80d6d19e799a46d969778c8d163..4527aae42cb897aceb728b794a2f2004f6dcc4dd 100644
--- a/src/attribute-rel-util.cpp
+++ b/src/attribute-rel-util.cpp
@@ -24,6 +24,9 @@
#include
#include
+#include <2geom/path-sink.h>
+#include <2geom/svg-path-parser.h>
+
#include "attribute-rel-css.h"
#include "attribute-rel-svg.h"
#include "preferences.h"
@@ -330,6 +333,36 @@ bool sp_attribute_check_attribute(Glib::ustring const &element, Glib::ustring co
return is_useful;
}
+bool sp_is_valid_svg_path_d(Glib::ustring const &d)
+{
+ /** A PathSink going straight to /dev/null */
+ class PathBlackHole final : public Geom::PathSink
+ {
+ using Geom::PathSink::feed;
+ void moveTo(Geom::Point const &) final {}
+ void lineTo(Geom::Point const &) final {}
+ void curveTo(Geom::Point const &, Geom::Point const &, Geom::Point const &) final {}
+ void quadTo(Geom::Point const &, Geom::Point const &) final {}
+ void arcTo(Geom::Coord, Geom::Coord, Geom::Coord, bool, bool, Geom::Point const &) final {}
+ void closePath() final {}
+ void flush() final {}
+ void feed(Geom::Curve const &, bool) final {}
+ void feed(Geom::Path const &) final {}
+ void feed(Geom::PathVector const &) final {}
+ void feed(Geom::Rect const &) final {}
+ void feed(Geom::Circle const &) final {}
+ void feed(Geom::Ellipse const &) final {}
+ } dev_null;
+
+ auto validator = Geom::SVGPathParser(dev_null);
+ try {
+ validator.parse(static_cast(d));
+ } catch (Geom::SVGPathParseError &) {
+ return false;
+ }
+ return true;
+}
+
/*
Local Variables:
mode:c++
diff --git a/src/attribute-rel-util.h b/src/attribute-rel-util.h
index 3b4f327e9ddc29599fb3a7d2b03d8db843af148e..490c9bc5da9342fdd6737bdd7de0a480cb0c5c72 100644
--- a/src/attribute-rel-util.h
+++ b/src/attribute-rel-util.h
@@ -1,21 +1,16 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/** @file
- * TODO: insert short description here
+ * Utility functions related to parsing and validation of XML attributes.
*//*
- * Authors: see git history
+ * Authors:
+ * tavmjong
*
* Copyright (C) 2016 Authors
- * Released under GNU GPL v2+, read the file 'COPYING' for more information.
- */
-#ifndef __SP_ATTRIBUTE_REL_UTIL_H__
-#define __SP_ATTRIBUTE_REL_UTIL_H__
-
-/*
- * attribute-rel-util.h
*
- * Created on: Sep 8, 2011
- * Author: tavmjong
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
*/
+#ifndef SEEN_ATTRIBUTE_REL_UTIL_H
+#define SEEN_ATTRIBUTE_REL_UTIL_H
#include
@@ -91,7 +86,15 @@ void sp_attribute_purge_default_style(SPCSSAttr *css, unsigned int flags);
bool sp_attribute_check_attribute(Glib::ustring const &element, Glib::ustring const &id, Glib::ustring const &attribute,
bool warn);
-#endif /* __SP_ATTRIBUTE_REL_UTIL_H__ */
+/**
+ * @brief Check whether the 'd' attribute on a element parses correctly.
+ *
+ * @param d The value of the 'd' attribute.
+ * @return false if the value causes a parser error, true otherwise.
+ */
+bool sp_is_valid_svg_path_d(Glib::ustring const &d);
+
+#endif // SEEN_ATTRIBUTE_REL_UTIL_H
/*
Local Variables:
diff --git a/src/inkscape.cpp b/src/inkscape.cpp
index 6d1aefb2dad48a75cec4b356404e67a306c68e1f..c35c78dce9c5b2d46a1f3bcfeda3654f405c1fe8 100644
--- a/src/inkscape.cpp
+++ b/src/inkscape.cpp
@@ -214,8 +214,9 @@ Application::Application(bool use_gui) :
icon_theme->prepend_search_path(get_path_ustring(USER, ICONS));
themecontext = new Inkscape::UI::ThemeContext();
themecontext->add_gtk_css(false);
- auto scale = prefs->getDoubleLimited("/theme/fontscale", 100, 50, 150);
- themecontext->adjust_global_font_scale(scale / 100.0);
+ auto scale = prefs->getDoubleLimited(UI::ThemeContext::get_font_scale_pref_path(), 100, 50, 150);
+ themecontext->adjustGlobalFontScale(scale / 100.0);
+ Inkscape::UI::ThemeContext::initialize_source_syntax_styles();
Inkscape::DeviceManager::getManager().loadConfig();
}
@@ -272,6 +273,10 @@ Application::Application(bool use_gui) :
}
}
}
+ // select default syntax coloring theme, if needed
+ if (auto desktop = active_desktop()) {
+ UI::ThemeContext::select_default_syntax_style(themecontext->isCurrentThemeDark(desktop->getToplevel()));
+ }
});
}
diff --git a/src/object/sp-factory.cpp b/src/object/sp-factory.cpp
index 4dc0d6605c0bfa986396f140f51f099659a9b80f..ddd9afcb2e43599d43999ba84731d4721f43e15a 100644
--- a/src/object/sp-factory.cpp
+++ b/src/object/sp-factory.cpp
@@ -114,6 +114,17 @@ public:
return it->second();
}
+ bool supportsId(std::string const &id) const
+ {
+ return map.find(id) != map.end();
+ }
+
+ static Factory const &get()
+ {
+ static Factory const singleton;
+ return singleton;
+ }
+
private:
using Func = SPObject*(*)();
@@ -273,8 +284,12 @@ private:
SPObject *SPFactory::createObject(std::string const &id)
{
- static Factory const factory;
- return factory.create(id);
+ return Factory::get().create(id);
+}
+
+bool SPFactory::supportsType(std::string const &id)
+{
+ return Factory::get().supportsId(id);
}
std::string NodeTraits::get_type_string(Inkscape::XML::Node const &node)
diff --git a/src/object/sp-factory.h b/src/object/sp-factory.h
index 4db5891126ee1bcd1243f76498d195d78a01a694..b8eba25fb66486ca864fdee6861bb6e1d7f3a773 100644
--- a/src/object/sp-factory.h
+++ b/src/object/sp-factory.h
@@ -24,6 +24,7 @@ class Node;
struct SPFactory {
static SPObject *createObject(std::string const &id);
+ static bool supportsType(std::string const &id);
};
struct NodeTraits {
diff --git a/src/preferences.h b/src/preferences.h
index 1ffb96916b212126eb1ea31998161100324f179a..13bff80c00c2a02e945131c97cb935acc2c6e6e2 100644
--- a/src/preferences.h
+++ b/src/preferences.h
@@ -897,6 +897,21 @@ private:
auto changed(Preferences::Entry const &e) const { return e.getDoubleLimited(def, min, max); }
};
+template<>
+class Pref : public PrefBase
+{
+public:
+ Pref(Glib::ustring path, Glib::ustring def = "")
+ : PrefBase(std::move(path), def)
+ {
+ init();
+ }
+private:
+ friend PrefBase;
+ auto read() const { return Preferences::get()->getString(observed_path, def); }
+ auto changed(Preferences::Entry const &e) const { return e.getString(def); }
+};
+
template<>
class Pref : public Preferences::Observer
{
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt
index fe6aada92d8b02b8482c12741dd4305a4c54e367..0aaa4e12f24b49e38400d22489afd5ba83c59956 100644
--- a/src/ui/CMakeLists.txt
+++ b/src/ui/CMakeLists.txt
@@ -16,6 +16,7 @@ set(ui_SRC
simple-pref-pusher.cpp
shortcuts.cpp
svg-renderer.cpp
+ syntax.cpp
themes.cpp
tool-factory.cpp
util.cpp
@@ -280,6 +281,7 @@ set(ui_SRC
shape-editor.h
simple-pref-pusher.h
shortcuts.h
+ syntax.h
themes.h
tool-factory.h
util.h
diff --git a/src/ui/dialog/attrdialog.cpp b/src/ui/dialog/attrdialog.cpp
index efd0895f0b31a67549d9ddae22d93d252797cf3b..22d2bd77903c0c6d4064869e52d9743e19ce3021 100644
--- a/src/ui/dialog/attrdialog.cpp
+++ b/src/ui/dialog/attrdialog.cpp
@@ -12,20 +12,53 @@
#include "attrdialog.h"
+#include "preferences.h"
#include "selection.h"
#include "document-undo.h"
#include "message-context.h"
#include "message-stack.h"
#include "style.h"
+#include "io/resource.h"
+
+#include "ui/builder-utils.h"
+#include "ui/dialog/inkscape-preferences.h"
#include "ui/icon-loader.h"
#include "ui/icon-names.h"
-#include "ui/widget/iconrenderer.h"
-
+#include "ui/syntax.h"
+#include "ui/util.h"
+#include "ui/widget/shapeicon.h"
+#include "util/numeric/converters.h"
+#include "util/trim.h"
#include "xml/attribute-record.h"
+#include
#include
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "config.h"
+#if WITH_GSOURCEVIEW
+# include
+#endif
/**
* Return true if `node` is a text or comment node
@@ -41,11 +74,43 @@ static bool is_text_or_comment_node(Inkscape::XML::Node const &node)
}
}
-namespace Inkscape {
-namespace UI {
-namespace Dialog {
+static Glib::ustring get_syntax_theme()
+{
+ return Inkscape::Preferences::get()->getString("/theme/syntax-color-theme", "-none-");
+}
+
+namespace Inkscape::UI::Dialog {
+
+// arbitrarily selected size limits
+constexpr int MAX_POPOVER_HEIGHT = 450;
+constexpr int MAX_POPOVER_WIDTH = 520;
+constexpr int TEXT_MARGIN = 3;
+
+std::unique_ptr AttrDialog::init_text_view(AttrDialog* owner, Syntax::SyntaxMode coloring, bool map)
+{
+ auto edit = Syntax::TextEditView::create(coloring);
+ auto& textview = edit->getTextView();
+ textview.set_wrap_mode(Gtk::WrapMode::WRAP_WORD);
+
+ // this actually sets padding rather than margin and extends textview's background color to the sides
+ textview.set_top_margin(TEXT_MARGIN);
+ textview.set_left_margin(TEXT_MARGIN);
+ textview.set_right_margin(TEXT_MARGIN);
+ textview.set_bottom_margin(TEXT_MARGIN);
+
+ if (map) {
+ textview.signal_map().connect([owner](){
+ // this is not effective: text view recalculates its size on idle, so it's too early to call on 'map';
+ // (note: there's no signal on a TextView to tell us that formatting has been done)
+ // delay adjustment; this will work if UI is fast enough, but at the cost of popup jumping,
+ // but at least it will be sized properly
+ owner->_adjust_size = Glib::signal_timeout().connect([=](){ owner->adjust_popup_edit_size(); return false; }, 50);
+ });
+ }
+
+ return edit;
+}
-static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog);
/**
* Constructor
* A treeview whose each row corresponds to an XML attribute of a selected node
@@ -53,46 +118,45 @@ static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *
*/
AttrDialog::AttrDialog()
: DialogBase("/dialogs/attr", "AttrDialog")
- , _mainBox(Gtk::ORIENTATION_VERTICAL)
- , status_box(Gtk::ORIENTATION_HORIZONTAL)
+ , _builder(create_builder("attribute-edit-component.glade"))
+ , _scrolled_text_view(get_widget(_builder, "scroll-wnd"))
+ , _content_sw(get_widget(_builder, "content-sw"))
+ , _scrolled_window(get_widget(_builder, "scrolled-wnd"))
+ , _treeView(get_widget(_builder, "tree-view"))
+ , _popover(&get_widget(_builder, "popup"))
+ , _status_box(get_widget(_builder, "status-box"))
+ , _status(get_widget(_builder, "status-label"))
{
+ // Attribute value editing (with syntax highlighting).
+ using namespace Syntax;
+ _css_edit = init_text_view(this, SyntaxMode::InlineCss, true);
+ _svgd_edit = init_text_view(this, SyntaxMode::SvgPathData, true);
+ _points_edit = init_text_view(this, SyntaxMode::SvgPolyPoints, true);
+ _attr_edit = init_text_view(this, SyntaxMode::PlainText, true);
+
+ // string content editing
+ _text_edit = init_text_view(this, SyntaxMode::PlainText, false);
+ _style_edit = init_text_view(this, SyntaxMode::CssStyle, false);
+
set_size_request(20, 15);
- _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET);
-
- // For text and comment nodes
- _content_tv = Gtk::manage(new Gtk::TextView());
- _content_tv->show();
- _content_tv->set_wrap_mode(Gtk::WrapMode::WRAP_CHAR);
- _content_tv->set_monospace(true);
- _content_tv->set_border_width(4);
- _content_tv->set_buffer(Gtk::TextBuffer::create());
- _content_tv->get_buffer()->signal_end_user_action().connect([this]() {
- if (_repr) {
- _repr->setContent(_content_tv->get_buffer()->get_text().c_str());
- setUndo(_("Type text"));
- }
- });
- _content_sw = Gtk::manage(new Gtk::ScrolledWindow());
- _content_sw->hide();
- _content_sw->set_no_show_all();
- _content_sw->add(*_content_tv);
- _mainBox.pack_start(*_content_sw);
-
- // For element nodes
- _treeView.set_headers_visible(true);
- _treeView.set_hover_selection(true);
- _treeView.set_activate_on_single_click(true);
- _treeView.set_can_focus(false);
- _scrolledWindow.add(_treeView);
- _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
+
+ // For text and comment nodes: update XML on the fly, as users type
+ for (auto tv : {&_text_edit->getTextView(), &_style_edit->getTextView()}) {
+ tv->get_buffer()->signal_end_user_action().connect([=]() {
+ if (_repr) {
+ _repr->setContent(tv->get_buffer()->get_text().c_str());
+ setUndo(_("Type text"));
+ }
+ });
+ }
_store = Gtk::ListStore::create(_attrColumns);
_treeView.set_model(_store);
- Inkscape::UI::Widget::IconRenderer * addRenderer = manage(new Inkscape::UI::Widget::IconRenderer());
- addRenderer->add_icon("edit-delete");
-
- _treeView.append_column("", *addRenderer);
+ // high-res aware icon renderer for a trash can
+ auto delete_renderer = manage(new Inkscape::UI::Widget::CellRendererItemIcon());
+ delete_renderer->property_shape_type().set_value("edit-delete");
+ _treeView.append_column("", *delete_renderer);
Gtk::TreeViewColumn *col = _treeView.get_column(0);
if (col) {
auto add_icon = Gtk::manage(sp_get_icon_image("list-add", Gtk::ICON_SIZE_SMALL_TOOLBAR));
@@ -105,11 +169,10 @@ AttrDialog::AttrDialog()
// isn't in this exact way, the onAttrDelete is called when the header lines are pressed.
button->signal_button_release_event().connect(sigc::mem_fun(*this, &AttrDialog::onAttrCreate), false);
}
- addRenderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete));
+ delete_renderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete));
_treeView.signal_key_press_event().connect(sigc::mem_fun(*this, &AttrDialog::onKeyPressed));
- _treeView.set_search_column(-1);
- _nameRenderer = Gtk::manage(new Gtk::CellRendererText());
+ _nameRenderer = Gtk::make_managed();
_nameRenderer->property_editable() = true;
_nameRenderer->property_placeholder_text().set_value(_("Attribute Name"));
_nameRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::nameEdited));
@@ -120,114 +183,174 @@ AttrDialog::AttrDialog()
_nameCol->set_resizable(true);
_nameCol->add_attribute(_nameRenderer->property_text(), _attrColumns._attributeName);
}
- status.set_halign(Gtk::ALIGN_START);
- status.set_valign(Gtk::ALIGN_CENTER);
- status.set_size_request(1, -1);
- status.set_markup("");
- status.set_line_wrap(true);
- status.get_style_context()->add_class("inksmall");
- status_box.pack_start(status, TRUE, TRUE, 0);
- pack_end(status_box, false, false, 2);
_message_stack = std::make_shared();
- _message_context = std::unique_ptr(new Inkscape::MessageContext(_message_stack));
- _message_changed_connection =
- _message_stack->connectChanged(sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj())));
+ _message_context = std::make_unique(_message_stack);
+ _message_changed_connection = _message_stack->connectChanged([=](MessageType, const char* message) {
+ _status.set_markup(message ? message : "");
+ });
- _valueRenderer = Gtk::manage(new Gtk::CellRendererText());
+ _valueRenderer = Gtk::make_managed();
_valueRenderer->property_editable() = true;
_valueRenderer->property_placeholder_text().set_value(_("Attribute Value"));
_valueRenderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END);
_valueRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::valueEdited));
- _valueRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startValueEdit));
+ _valueRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startValueEdit), true);
_treeView.append_column(_("Value"), *_valueRenderer);
_valueCol = _treeView.get_column(2);
if (_valueCol) {
_valueCol->add_attribute(_valueRenderer->property_text(), _attrColumns._attributeValueRender);
}
- _popover = Gtk::manage(new Gtk::Popover());
- Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL));
- Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
- _textview = Gtk::manage(new Gtk::TextView());
- _textview->set_wrap_mode(Gtk::WrapMode::WRAP_CHAR);
- _textview->set_editable(true);
- _textview->set_monospace(true);
- _textview->set_border_width(6);
- _textview->signal_map().connect(sigc::mem_fun(*this, &AttrDialog::textViewMap));
- Glib::RefPtr textbuffer = Gtk::TextBuffer::create();
- textbuffer->set_text("");
- _textview->set_buffer(textbuffer);
- _scrolled_text_view.add(*_textview);
- _scrolled_text_view.set_max_content_height(450);
- _scrolled_text_view.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
- _scrolled_text_view.set_propagate_natural_width(true);
- Gtk::Label *helpreturn = Gtk::manage(new Gtk::Label(_("Shift+Return for a new line")));
- helpreturn->get_style_context()->add_class("inksmall");
- Gtk::Button *apply = Gtk::manage(new Gtk::Button());
- Gtk::Image *icon = Gtk::manage(sp_get_icon_image("on-outline", 26));
- apply->set_relief(Gtk::RELIEF_NONE);
- icon->show();
- apply->add(*icon);
- apply->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueEditedPop));
- Gtk::Button *cancel = Gtk::manage(new Gtk::Button());
- icon = Gtk::manage(sp_get_icon_image("off-outline", 26));
- cancel->set_relief(Gtk::RELIEF_NONE);
- icon->show();
- cancel->add(*icon);
- cancel->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueCanceledPop));
- hbox->pack_end(*apply, Gtk::PACK_SHRINK, 3);
- hbox->pack_end(*cancel, Gtk::PACK_SHRINK, 3);
- hbox->pack_end(*helpreturn, Gtk::PACK_SHRINK, 3);
- vbox->pack_start(_scrolled_text_view, Gtk::PACK_EXPAND_WIDGET, 3);
- vbox->pack_start(*hbox, Gtk::PACK_EXPAND_WIDGET, 3);
- _popover->add(*vbox);
- _popover->show();
- _popover->set_relative_to(_treeView);
- _popover->set_position(Gtk::PositionType::POS_BOTTOM);
- _popover->signal_closed().connect(sigc::mem_fun(*this, &AttrDialog::popClosed));
- _popover->get_style_context()->add_class("attrpop");
- attr_reset_context(0);
- pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET);
- // I couldn't get the signal go well not using C way signals
- g_signal_connect(GTK_WIDGET(_popover->gobj()), "key-press-event", G_CALLBACK(key_callback), this);
+
+ set_current_textedit(_attr_edit.get());
+ _scrolled_text_view.set_max_content_height(MAX_POPOVER_HEIGHT);
+
+ auto& apply = get_widget(_builder, "btn-ok");
+ apply.signal_clicked().connect([=]() { valueEditedPop(); });
+
+ auto& cancel = get_widget(_builder, "btn-cancel");
+ cancel.signal_clicked().connect([=](){
+ if (!_value_editing.empty()) {
+ _activeTextView().get_buffer()->set_text(_value_editing);
+ }
+ _popover->popdown();
+ });
+
+ _popover->signal_closed().connect([=]() { popClosed(); });
+ _popover->signal_key_press_event().connect([=](GdkEventKey* ev) { return key_callback(ev); }, false);
_popover->hide();
+
+ get_widget(_builder, "btn-truncate").signal_clicked().connect([=](){ truncateDigits(); });
+
+ const int N = 5;
+ _rounding_precision = Inkscape::Preferences::get()->getIntLimited("/dialogs/attrib/precision", 2, 0, N);
+ for (int n = 0; n <= N; ++n) {
+ auto id = '_' + std::to_string(n);
+ auto item = &get_widget(_builder, id.c_str());
+ auto action = [=](){
+ _rounding_precision = n;
+ get_widget(_builder, "precision").set_label(' ' + item->get_label());
+ Inkscape::Preferences::get()->setInt("/dialogs/attrib/precision", n);
+ };
+ item->signal_activate().connect(action);
+
+ if (n == _rounding_precision) {
+ action();
+ }
+ }
+
+ attr_reset_context(0);
+ pack_start(get_widget(_builder, "main-box"), Gtk::PACK_EXPAND_WIDGET);
_updating = false;
}
-void AttrDialog::textViewMap()
+AttrDialog::~AttrDialog()
{
- auto vscroll = _scrolled_text_view.get_vadjustment();
- int height = vscroll->get_upper() + 12; // padding 6+6
- if (height < 450) {
- _scrolled_text_view.set_min_content_height(height);
- vscroll->set_value(vscroll->get_lower());
+ _current_text_edit = nullptr;
+ _popover->hide();
+}
+
+
+static int fmt_number(_GMatchInfo const *match, _GString *ret, void *prec)
+{
+ auto number = g_match_info_fetch(match, 1);
+
+ char *end;
+ double val = g_ascii_strtod(number, &end);
+ if (*number && (end == nullptr || end > number)) {
+ auto precision = *static_cast(prec);
+ auto fmt = Util::format_number(val, precision);
+ g_string_append(ret, fmt.c_str());
} else {
- _scrolled_text_view.set_min_content_height(450);
+ g_string_append(ret, number);
+ }
+
+ auto text = g_match_info_fetch(match, 2);
+ g_string_append(ret, text);
+
+ g_free(number);
+ g_free(text);
+
+ return false;
+}
+
+Glib::ustring AttrDialog::round_numbers(const Glib::ustring& text, int precision)
+{
+ // match floating point number followed by something else (not a number); repeat
+ static const auto numbers = Glib::Regex::create("([-+]?(?:(?:\\d+\\.?\\d*)|(?:\\.\\d+))(?:[eE][-+]?\\d*)?)([^+\\-0-9]*)", Glib::REGEX_MULTILINE);
+
+ return numbers->replace_eval(text, text.size(), 0, Glib::RegexMatchFlags::REGEX_MATCH_NOTEMPTY, &fmt_number, &precision);
+}
+
+/** Round the selected floating point numbers in the attribute edit popover. */
+void AttrDialog::truncateDigits() const
+{
+ if (!_current_text_edit) {
+ return;
+ }
+
+ auto buffer = _current_text_edit->getTextView().get_buffer();
+ auto start = buffer->begin();
+ auto end = buffer->end();
+
+ bool const had_selection = buffer->get_has_selection();
+ int start_idx = 0, end_idx = 0;
+ if (had_selection) {
+ buffer->get_selection_bounds(start, end);
+ start_idx = start.get_offset();
+ end_idx = end.get_offset();
+ }
+
+ auto text = buffer->get_text(start, end);
+ auto ret = round_numbers(text, _rounding_precision);
+ buffer->erase(start, end);
+ buffer->insert_at_cursor(ret);
+
+ if (had_selection) {
+ // Restore selection but note that its length may have decreased.
+ end_idx -= text.size() - ret.size();
+ if (end_idx < start_idx) {
+ end_idx = start_idx;
+ }
+ buffer->select_range(buffer->get_iter_at_offset(start_idx), buffer->get_iter_at_offset(end_idx));
}
}
-gboolean sp_show_pop_map(gpointer data)
+void AttrDialog::set_current_textedit(Syntax::TextEditView* edit)
{
- AttrDialog *attrdialog = reinterpret_cast(data);
- attrdialog->textViewMap();
- return FALSE;
+ _current_text_edit = edit ? edit : _attr_edit.get();
+ _scrolled_text_view.remove();
+ _scrolled_text_view.add(_current_text_edit->getTextView());
+ _scrolled_text_view.show_all();
}
-static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog)
+void AttrDialog::adjust_popup_edit_size()
{
+ auto vscroll = _scrolled_text_view.get_vadjustment();
+ int height = vscroll->get_upper() + 2 * TEXT_MARGIN;
+ if (height < MAX_POPOVER_HEIGHT) {
+ _scrolled_text_view.set_min_content_height(height);
+ vscroll->set_value(vscroll->get_lower());
+ } else {
+ _scrolled_text_view.set_min_content_height(MAX_POPOVER_HEIGHT);
+ }
+}
+
+bool AttrDialog::key_callback(GdkEventKey* event) {
switch (event->keyval) {
case GDK_KEY_Return:
- case GDK_KEY_KP_Enter: {
- if (attrdialog->_popover->is_visible()) {
- if (!(event->state & GDK_SHIFT_MASK)) {
- attrdialog->valueEditedPop();
- attrdialog->_popover->hide();
+ case GDK_KEY_KP_Enter:
+ if (_popover->is_visible()) {
+ if (event->state & GDK_SHIFT_MASK) {
+ valueEditedPop();
return true;
- } else {
- g_timeout_add(50, &sp_show_pop_map, attrdialog);
+ }
+ else {
+ // as we type and content grows, resize the popup to accommodate it
+ _adjust_size = Glib::signal_timeout().connect([=](){ adjust_popup_edit_size(); return false; }, 50);
}
}
- } break;
+ break;
}
return false;
}
@@ -245,7 +368,6 @@ static Glib::ustring prepare_rendervalue(const char *value)
{
constexpr int MAX_LENGTH = 500; // maximum length of string before it's truncated for performance reasons
// ~400 characters fit horizontally on a WQHD display, so 500 should be plenty
-
Glib::ustring renderval;
// truncate to MAX_LENGTH
@@ -264,16 +386,25 @@ static Glib::ustring prepare_rendervalue(const char *value)
return renderval;
}
+void set_mono_class(Gtk::Widget* widget, bool mono)
+{
+ if (!widget) {
+ return;
+ }
+ Glib::ustring class_name = "mono-font";
+ auto style = widget->get_style_context();
+ auto has_class = style->has_class(class_name);
+
+ if (mono && !has_class) {
+ style->add_class(class_name);
+ } else if (!mono && has_class) {
+ style->remove_class(class_name);
+ }
+}
-/**
- * @brief AttrDialog::~AttrDialog
- * Class destructor
- */
-AttrDialog::~AttrDialog()
+void AttrDialog::set_mono_font(bool mono)
{
- _message_changed_connection.disconnect();
- _message_context = nullptr;
- _message_stack = nullptr;
+ set_mono_class(&_treeView, mono);
}
void AttrDialog::startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
@@ -282,67 +413,100 @@ void AttrDialog::startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &pat
entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &AttrDialog::onNameKeyPressed), entry));
}
-
-gboolean sp_show_attr_pop(gpointer data)
+Gtk::TextView &AttrDialog::_activeTextView() const
{
- AttrDialog *attrdialog = reinterpret_cast(data);
- attrdialog->_popover->show_all();
-
- return FALSE;
+ return _current_text_edit->getTextView();
}
-gboolean sp_close_entry(gpointer data)
+void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
{
- Gtk::CellEditable *cell = reinterpret_cast(data);
- if (cell) {
- cell->property_editing_canceled() = true;
- cell->remove_widget();
+ _value_path = path;
+ Gtk::TreeIter iter = *_store->get_iter(path);
+ Gtk::TreeModel::Row row = *iter;
+ if (!row || !_repr || !cell) {
+ return;
}
- return FALSE;
-}
-void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path)
-{
- Gtk::Entry *entry = dynamic_cast(cell);
- int width = 0;
- int height = 0;
- int colwidth = _valueCol->get_width();
// popover in GTK3 is clipped to dialog window (in a floating dialog); limit size:
- int dlg_width = get_allocated_width() - 10;
- _textview->set_size_request(std::min(510, dlg_width), -1);
- _popover->set_size_request(std::min(520, dlg_width), -1);
- valuepath = path;
+ const int dlg_width = get_allocated_width() - 10;
+ _popover->set_size_request(std::min(MAX_POPOVER_WIDTH, dlg_width), -1);
+
+ auto const attribute = row[_attrColumns._attributeName];
+ bool edit_in_popup =
+#if WITH_GSOURCEVIEW
+ true;
+#else
+ false;
+#endif
+ bool enable_rouding = false;
+
+ if (attribute == "style") {
+ set_current_textedit(_css_edit.get());
+ } else if (attribute == "d") {
+ enable_rouding = true;
+ set_current_textedit(_svgd_edit.get());
+ } else if (attribute == "points") {
+ enable_rouding = true;
+ set_current_textedit(_points_edit.get());
+ } else {
+ set_current_textedit(_attr_edit.get());
+ edit_in_popup = false;
+ }
+
+ // number rounding functionality
+ widget_show(get_widget(_builder, "rounding-box"), enable_rouding);
+
+ _activeTextView().set_size_request(std::min(MAX_POPOVER_WIDTH - 10, dlg_width), -1);
+
+ auto theme = get_syntax_theme();
+
+ auto entry = dynamic_cast(cell);
+ int width, height;
entry->get_layout()->get_pixel_size(width, height);
- Gtk::TreeIter iter = *_store->get_iter(path);
- Gtk::TreeModel::Row row = *iter;
- if (row && this->_repr) {
- Glib::ustring name = row[_attrColumns._attributeName];
- if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] || colwidth - 10 < width) {
- valueediting = entry->get_text();
- Gdk::Rectangle rect;
- _treeView.get_cell_area((Gtk::TreeModel::Path)iter, *_valueCol, rect);
- if (_popover->get_position() == Gtk::PositionType::POS_BOTTOM) {
- rect.set_y(rect.get_y() + 20);
- }
- _popover->set_pointing_to(rect);
- Glib::RefPtr textbuffer = Gtk::TextBuffer::create();
- textbuffer->set_text(row[_attrColumns._attributeValue]);
- _textview->set_buffer(textbuffer);
- g_timeout_add(50, &sp_close_entry, cell);
- g_timeout_add(50, &sp_show_attr_pop, this);
- } else {
- entry->signal_key_press_event().connect(
- sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry));
+ int colwidth = _valueCol->get_width();
+
+ if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] ||
+ edit_in_popup || colwidth - 10 < width)
+ {
+ _value_editing = entry->get_text();
+ Gdk::Rectangle rect;
+ _treeView.get_cell_area((Gtk::TreeModel::Path)iter, *_valueCol, rect);
+ if (_popover->get_position() == Gtk::PositionType::POS_BOTTOM) {
+ rect.set_y(rect.get_y() + 20);
+ }
+ if (rect.get_x() >= dlg_width) {
+ rect.set_x(dlg_width - 1);
}
+ _popover->set_pointing_to(rect);
+
+ auto current_value = row[_attrColumns._attributeValue];
+ _current_text_edit->setStyle(theme);
+ _current_text_edit->setText(current_value);
+
+ // close in-line entry
+ cell->property_editing_canceled() = true;
+ cell->remove_widget();
+ // cannot dismiss it right away without warning from GTK, so delay it
+ Glib::signal_timeout().connect_once([=](){
+ cell->editing_done(); // only this call will actually remove in-line edit widget
+ cell->remove_widget();
+ }, 0);
+ // and show popup edit instead
+ Glib::signal_timeout().connect_once([=](){ _popover->popup(); }, 10);
+ } else {
+ entry->signal_key_press_event().connect(
+ sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry));
}
}
void AttrDialog::popClosed()
{
- Glib::RefPtr textbuffer = Gtk::TextBuffer::create();
- textbuffer->set_text("");
- _textview->set_buffer(textbuffer);
- _scrolled_text_view.set_min_content_height(20);
+ if (!_current_text_edit) {
+ return;
+ }
+ _activeTextView().get_buffer()->set_text("");
+ // delay this resizing, so it is not visible as popover fades out
+ _close_popup = Glib::signal_timeout().connect([=](){ _scrolled_text_view.set_min_content_height(20); return false; }, 250);
}
/**
@@ -351,7 +515,9 @@ void AttrDialog::popClosed()
*/
void AttrDialog::setRepr(Inkscape::XML::Node * repr)
{
- if ( repr == _repr ) return;
+ if (repr == _repr) {
+ return;
+ }
if (_repr) {
_store->clear();
_repr->removeObserver(*this);
@@ -362,12 +528,25 @@ void AttrDialog::setRepr(Inkscape::XML::Node * repr)
if (repr) {
Inkscape::GC::anchor(_repr);
_repr->addObserver(*this);
- _repr->synthesizeEvents(*this);
// show either attributes or content
bool show_content = is_text_or_comment_node(*_repr);
- _scrolledWindow.set_visible(!show_content);
- _content_sw->set_visible(show_content);
+ if (show_content) {
+ _content_sw.remove();
+ auto type = repr->name();
+ auto elem = repr->parent();
+ if (type && strcmp(type, "string") == 0 && elem && elem->name() && strcmp(elem->name(), "svg:style") == 0) {
+ // editing embedded CSS style
+ _style_edit->setStyle(get_syntax_theme());
+ _content_sw.add(_style_edit->getTextView());
+ } else {
+ _content_sw.add(_text_edit->getTextView());
+ }
+ }
+
+ _repr->synthesizeEvents(*this);
+ _scrolled_window.set_visible(!show_content);
+ _content_sw.set_visible(show_content);
}
}
@@ -376,13 +555,6 @@ void AttrDialog::setUndo(Glib::ustring const &event_description)
DocumentUndo::done(getDocument(), event_description, INKSCAPE_ICON("dialog-xml-editor"));
}
-void AttrDialog::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget)
-{
- if (widget) {
- gtk_label_set_markup(GTK_LABEL(widget), message ? message : "");
- }
-}
-
/**
* Sets the AttrDialog status bar, depending on which attr is selected.
*/
@@ -414,12 +586,11 @@ void AttrDialog::notifyAttributeChanged(XML::Node&, GQuark name_, Util::ptr_shar
if (new_value) {
renderval = prepare_rendervalue(new_value.pointer());
}
- for(auto iter: this->_store->children())
- {
+ for (auto&& iter : _store->children()) {
Gtk::TreeModel::Row row = *iter;
Glib::ustring col_name = row[_attrColumns._attributeName];
- if(name == col_name) {
- if(new_value) {
+ if (name == col_name) {
+ if (new_value) {
row[_attrColumns._attributeValue] = new_value.pointer();
row[_attrColumns._attributeValueRender] = renderval;
new_value = Util::ptr_shared(); // Don't make a new one
@@ -430,7 +601,7 @@ void AttrDialog::notifyAttributeChanged(XML::Node&, GQuark name_, Util::ptr_shar
}
}
if (new_value) {
- Gtk::TreeModel::Row row = *(_store->prepend());
+ Gtk::TreeModel::Row row = *_store->prepend();
row[_attrColumns._attributeName] = name;
row[_attrColumns._attributeValue] = new_value.pointer();
row[_attrColumns._attributeValueRender] = renderval;
@@ -476,10 +647,14 @@ void AttrDialog::notifyContentChanged(XML::Node &,
Util::ptr_shared,
Util::ptr_shared new_content)
{
- auto buffer = _content_tv->get_buffer();
+ auto textview = dynamic_cast(_content_sw.get_child());
+ if (!textview) {
+ return;
+ }
+ auto buffer = textview->get_buffer();
if (!buffer->get_modified()) {
- auto const c = new_content.pointer();
- buffer->set_text(c ? c : "");
+ auto str = new_content.pointer();
+ buffer->set_text(str ? str : "");
}
buffer->set_modified(false);
}
@@ -494,45 +669,41 @@ void AttrDialog::notifyContentChanged(XML::Node &,
bool AttrDialog::onKeyPressed(GdkEventKey *event)
{
bool ret = false;
- if(this->_repr) {
- auto selection = this->_treeView.get_selection();
- Gtk::TreeModel::Row row = *(selection->get_selected());
- Gtk::TreeIter iter = *(selection->get_selected());
- switch (event->keyval)
- {
- case GDK_KEY_Delete:
- case GDK_KEY_KP_Delete: {
- // Create new attribute (repeat code, fold into above event!)
- Glib::ustring name = row[_attrColumns._attributeName];
- {
- this->_store->erase(row);
- this->_repr->removeAttribute(name);
- this->setUndo(_("Delete attribute"));
- }
- ret = true;
+ if (!_repr) {
+ return ret;
+ }
+ auto selection = _treeView.get_selection();
+ auto row = *selection->get_selected();
+
+ switch (event->keyval) {
+ case GDK_KEY_Delete:
+ case GDK_KEY_KP_Delete: {
+ // Create new attribute (repeat code, fold into above event!)
+ Glib::ustring name = row[_attrColumns._attributeName];
+ _store->erase(row);
+ _repr->removeAttribute(name);
+ setUndo(_("Delete attribute"));
+ ret = true;
} break;
- case GDK_KEY_plus:
- case GDK_KEY_Insert:
- {
- // Create new attribute (repeat code, fold into above event!)
- Gtk::TreeIter iter = this->_store->prepend();
- Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
- this->_treeView.set_cursor(path, *this->_nameCol, true);
- grab_focus();
- ret = true;
+
+ case GDK_KEY_plus:
+ case GDK_KEY_Insert: {
+ // Create new attribute (repeat code, fold into above event!)
+ Gtk::TreeIter iter = _store->prepend();
+ Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ _treeView.set_cursor(path, *_nameCol, true);
+ grab_focus();
+ ret = true;
} break;
- case GDK_KEY_Return:
- case GDK_KEY_KP_Enter: {
- if (_popover->is_visible()) {
- if (!(event->state & GDK_SHIFT_MASK)) {
- valueEditedPop();
- _popover->hide();
- ret = true;
- }
- }
+
+ case GDK_KEY_Return:
+ case GDK_KEY_KP_Enter:
+ if (_popover->is_visible() && (event->state & GDK_SHIFT_MASK)) {
+ valueEditedPop();
+ ret = true;
} break;
- }
}
+
return ret;
}
@@ -550,7 +721,6 @@ bool AttrDialog::onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
return ret;
}
-
bool AttrDialog::onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
{
g_debug("StyleDialog::_onValueKeyPressed");
@@ -565,37 +735,30 @@ bool AttrDialog::onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry)
return ret;
}
-gboolean sp_attrdialog_store_move_to_next(gpointer data)
+void AttrDialog::storeMoveToNext(Gtk::TreeModel::Path modelpath)
{
- AttrDialog *attrdialog = reinterpret_cast(data);
- auto selection = attrdialog->_treeView.get_selection();
- Gtk::TreeIter iter = *(selection->get_selected());
- Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter;
+ auto selection = _treeView.get_selection();
+ auto iter = *(selection->get_selected());
+ auto path = static_cast(iter);
Gtk::TreeViewColumn *focus_column;
- attrdialog->_treeView.get_cursor(path, focus_column);
- if (path == attrdialog->_modelpath && focus_column == attrdialog->_treeView.get_column(1)) {
- attrdialog->_treeView.set_cursor(attrdialog->_modelpath, *attrdialog->_valueCol, true);
+ _treeView.get_cursor(path, focus_column);
+ if (path == modelpath && focus_column == _treeView.get_column(1)) {
+ _treeView.set_cursor(modelpath, *_valueCol, true);
}
- return FALSE;
}
/**
- *
- *
- * @brief AttrDialog::nameEdited
- * @param event
- * @return
* Called when the name is edited in the TreeView editable column
*/
void AttrDialog::nameEdited (const Glib::ustring& path, const Glib::ustring& name)
{
Gtk::TreeIter iter = *_store->get_iter(path);
- _modelpath = (Gtk::TreeModel::Path)iter;
+ auto modelpath = static_cast(iter);
Gtk::TreeModel::Row row = *iter;
if(row && this->_repr) {
Glib::ustring old_name = row[_attrColumns._attributeName];
if (old_name == name) {
- g_timeout_add(50, &sp_attrdialog_store_move_to_next, this);
+ Glib::signal_timeout().connect_once([=](){ storeMoveToNext(modelpath); }, 50);
grab_focus();
return;
}
@@ -628,27 +791,16 @@ void AttrDialog::nameEdited (const Glib::ustring& path, const Glib::ustring& nam
_updating = true;
_repr->setAttributeOrRemoveIfEmpty(name, value); // use char * overload (allows empty attribute values)
_updating = false;
- g_timeout_add(50, &sp_attrdialog_store_move_to_next, this);
- this->setUndo(_("Rename attribute"));
+ Glib::signal_timeout().connect_once([=](){ storeMoveToNext(modelpath); }, 50);
+ setUndo(_("Rename attribute"));
}
}
void AttrDialog::valueEditedPop()
{
- Glib::ustring value = _textview->get_buffer()->get_text();
- valueEdited(valuepath, value);
- valueediting = "";
- _popover->hide();
-}
-
-void AttrDialog::valueCanceledPop()
-{
- if (!valueediting.empty()) {
- Glib::RefPtr textbuffer = Gtk::TextBuffer::create();
- textbuffer->set_text(valueediting);
- _textview->set_buffer(textbuffer);
- }
- _popover->hide();
+ valueEdited(_value_path, _current_text_edit->getText());
+ _value_editing.clear();
+ _popover->popdown();
}
/**
@@ -664,17 +816,16 @@ void AttrDialog::valueEdited (const Glib::ustring& path, const Glib::ustring& va
}
Gtk::TreeModel::Row row = *_store->get_iter(path);
- if(row && this->_repr) {
+ if (row && _repr) {
Glib::ustring name = row[_attrColumns._attributeName];
Glib::ustring old_value = row[_attrColumns._attributeValue];
- if (old_value == value) {
+ if (old_value == value || name.empty()) {
return;
}
- if(name.empty()) return;
- {
- _repr->setAttributeOrRemoveIfEmpty(name, value);
- }
- if(!value.empty()) {
+
+ _repr->setAttributeOrRemoveIfEmpty(name, value);
+
+ if (!value.empty()) {
row[_attrColumns._attributeValue] = value;
Glib::ustring renderval = prepare_rendervalue(value.c_str());
row[_attrColumns._attributeValueRender] = renderval;
@@ -683,6 +834,4 @@ void AttrDialog::valueEdited (const Glib::ustring& path, const Glib::ustring& va
}
}
-} // namespace Dialog
-} // namespace UI
-} // namespace Inkscape
+} // namespace Inkscape::UI::Dialog
diff --git a/src/ui/dialog/attrdialog.h b/src/ui/dialog/attrdialog.h
index 6d33a6668c4f2fa948611c969af8db3b614572af..e85d42d7229c892353e3522b4a616de349d67aa2 100644
--- a/src/ui/dialog/attrdialog.h
+++ b/src/ui/dialog/attrdialog.h
@@ -13,16 +13,20 @@
#ifndef SEEN_UI_DIALOGS_ATTRDIALOG_H
#define SEEN_UI_DIALOGS_ATTRDIALOG_H
+#include
#include
#include
#include
#include
#include
#include
+#include
+#include "helper/auto-connection.h"
+#include "inkscape-application.h"
#include "message.h"
#include "ui/dialog/dialog-base.h"
-
+#include "ui/syntax.h"
#include "xml/node-observer.h"
namespace Inkscape {
@@ -44,6 +48,16 @@ public:
AttrDialog();
~AttrDialog() override;
+ void setRepr(Inkscape::XML::Node * repr);
+ Gtk::ScrolledWindow& get_scrolled_window() { return _scrolled_window; }
+ Gtk::Box& get_status_box() { return _status_box; }
+ void adjust_popup_edit_size();
+ void set_mono_font(bool mono);
+
+private:
+ // builder comes first, so it is initialized before other data members
+ Glib::RefPtr _builder;
+
// Data structure
class AttrColumns : public Gtk::TreeModel::ColumnRecord
{
@@ -61,69 +75,75 @@ public:
AttrColumns _attrColumns;
// TreeView
- Gtk::TreeView _treeView;
+ Gtk::TreeView& _treeView;
Glib::RefPtr _store;
Gtk::CellRendererText *_nameRenderer;
Gtk::CellRendererText *_valueRenderer;
Gtk::TreeViewColumn *_nameCol;
Gtk::TreeViewColumn *_valueCol;
- Gtk::TreeModel::Path _modelpath;
Gtk::Popover *_popover;
- Gtk::TextView *_textview;
- Glib::ustring valuepath;
- Glib::ustring valueediting;
-
- // Text/comment nodes
- Gtk::TextView *_content_tv;
- Gtk::ScrolledWindow *_content_sw;
-
- /**
- * Status bar
- */
+ Glib::ustring _value_path;
+ Glib::ustring _value_editing;
+ // Status bar
std::shared_ptr _message_stack;
std::unique_ptr _message_context;
-
// Widgets
- Gtk::Box _mainBox;
- Gtk::ScrolledWindow _scrolledWindow;
- Gtk::ScrolledWindow _scrolled_text_view;
- Gtk::Button _buttonAddAttribute;
+ Gtk::ScrolledWindow& _scrolled_window;
+ Gtk::ScrolledWindow& _scrolled_text_view;
// Variables - Inkscape
Inkscape::XML::Node* _repr{nullptr};
- Gtk::Box status_box;
- Gtk::Label status;
- bool _updating;
+ Gtk::Box& _status_box;
+ Gtk::Label& _status;
+ bool _updating = true;
// Helper functions
- void setRepr(Inkscape::XML::Node * repr);
void setUndo(Glib::ustring const &event_description);
/**
* Sets the XML status bar, depending on which attr is selected.
*/
void attr_reset_context(gint attr);
- static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog);
/**
* Signal handlers
*/
- sigc::connection _message_changed_connection;
+ auto_connection _message_changed_connection;
bool onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
bool onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry);
void onAttrDelete(Glib::ustring path);
bool onAttrCreate(GdkEventButton *event);
bool onKeyPressed(GdkEventKey *event);
+ void truncateDigits() const;
void popClosed();
void startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path);
void startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path);
void nameEdited(const Glib::ustring &path, const Glib::ustring &name);
void valueEdited(const Glib::ustring &path, const Glib::ustring &value);
- void textViewMap();
- void valueCanceledPop();
void valueEditedPop();
+ void storeMoveToNext(Gtk::TreeModel::Path modelpath);
private:
+ // Text/comment nodes
+ Gtk::ScrolledWindow& _content_sw;
+ std::unique_ptr _text_edit; // text content editing (plain text)
+ std::unique_ptr _style_edit; // embedded CSS style (with syntax coloring)
+
+ // Attribute value editing
+ std::unique_ptr _css_edit; // in-line CSS style
+ std::unique_ptr _svgd_edit; // SVG path data
+ std::unique_ptr _points_edit; // points in a or
+ std::unique_ptr _attr_edit; // all other attributes (plain text)
+ Syntax::TextEditView* _current_text_edit = nullptr; // current text edit for attribute value editing
+ auto_connection _adjust_size;
+ auto_connection _close_popup;
+ int _rounding_precision = 0;
+
+ bool key_callback(GdkEventKey* event);
void notifyAttributeChanged(XML::Node &repr, GQuark name, Util::ptr_shared old_value, Util::ptr_shared new_value) final;
void notifyContentChanged(XML::Node &node, Util::ptr_shared old_content, Util::ptr_shared new_content) final;
+ static Glib::ustring round_numbers(const Glib::ustring& text, int precision);
+ Gtk::TextView &_activeTextView() const;
+ void set_current_textedit(Syntax::TextEditView* edit);
+ static std::unique_ptr init_text_view(AttrDialog* owner, Syntax::SyntaxMode coloring, bool map);
};
} // namespace Dialog
diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp
index 36d38961c9b555d5d47b5eefc65e3c4579b5cae5..29f99f4bfe9adba0e95e7e034d76e79708f2dccc 100644
--- a/src/ui/dialog/inkscape-preferences.cpp
+++ b/src/ui/dialog/inkscape-preferences.cpp
@@ -14,6 +14,11 @@
* Released under GNU GPL v2+, read the file 'COPYING' for more information.
*/
+#include
+#include
+#include
+#include
+#include
#include
#ifdef HAVE_CONFIG_H
# include "config.h" // only include where actually required!
@@ -90,6 +95,10 @@
# endif
#endif
+#if WITH_GSOURCEVIEW
+# include
+#endif
+
namespace Inkscape {
namespace UI {
namespace Dialog {
@@ -1692,7 +1701,7 @@ void InkscapePreferences::initPageUI()
{
auto font_scale = new Inkscape::UI::Widget::PrefSlider();
font_scale = Gtk::manage(font_scale);
- font_scale->init("/theme/fontscale", 50, 150, 5, 5, 100, 0); // 50% to 150%
+ font_scale->init(ThemeContext::get_font_scale_pref_path(), 50, 150, 5, 5, 100, 0); // 50% to 150%
font_scale->getSlider()->signal_format_value().connect([=](double val) {
return Glib::ustring::format(std::fixed, std::setprecision(0), val) + "%";
});
@@ -1715,10 +1724,10 @@ void InkscapePreferences::initPageUI()
space->add(*reset);
reset->signal_clicked().connect([=](){
font_scale->getSlider()->set_value(100);
- INKSCAPE.themecontext->adjust_global_font_scale(1.0);
+ INKSCAPE.themecontext->adjustGlobalFontScale(1.0);
});
apply->signal_clicked().connect([=](){
- INKSCAPE.themecontext->adjust_global_font_scale(font_scale->getSlider()->get_value() / 100.0);
+ INKSCAPE.themecontext->adjustGlobalFontScale(font_scale->getSlider()->get_value() / 100.0);
});
_page_theme.add_line(false, _("_Font scale:"), *font_scale, "", _("Adjust size of UI fonts"), true, space);
}
@@ -1834,6 +1843,61 @@ void InkscapePreferences::initPageUI()
_page_theme.add_line(true, "", _shift_icons, "",
_("This preference fixes icon positions in menus."), false, reset_icon());
+ _page_theme.add_group_header(_("XML dialog"));
+#if WITH_GSOURCEVIEW
+ {
+ auto manager = gtk_source_style_scheme_manager_get_default();
+ auto ids = gtk_source_style_scheme_manager_get_scheme_ids(manager);
+
+ auto syntax = Gtk::make_managed();
+ std::vector labels;
+ std::vector values;
+ for (const char* style = *ids; style; style = *++ids) {
+ if (auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, style)) {
+ auto name = gtk_source_style_scheme_get_name(scheme);
+ labels.emplace_back(name);
+ }
+ else {
+ labels.emplace_back(style);
+ }
+ values.emplace_back(style);
+ }
+ syntax->init("/theme/syntax-color-theme", labels, values, "");
+ _page_theme.add_line(false, _("Color theme:"), *syntax, "", _("Syntax coloring for XML dialog"), false);
+ }
+#endif
+ {
+ auto font_button = Gtk::make_managed("...");
+ font_button->set_halign(Gtk::ALIGN_START);
+ auto font_box = Gtk::make_managed();
+ font_box->set_editable(false);
+ font_box->set_sensitive(false);
+ auto theme = INKSCAPE.themecontext;
+ font_box->set_text(theme->getMonospacedFont().to_string());
+ font_button->signal_clicked().connect([=](){
+ Gtk::FontChooserDialog dlg;
+ // show fixed-size fonts only
+ dlg.set_filter_func([](const Glib::RefPtr& family, const Glib::RefPtr& face) {
+ return family && family->is_monospace();
+ });
+ dlg.set_font_desc(theme->getMonospacedFont());
+ dlg.set_position(Gtk::WIN_POS_MOUSE);
+ dlg.set_modal();
+ if (dlg.run() == Gtk::RESPONSE_OK) {
+ auto desc = dlg.get_font_desc();
+ theme->saveMonospacedFont(desc);
+ theme->adjustGlobalFontScale(theme->getFontScale() / 100);
+ font_box->set_text(desc.to_string());
+ }
+ });
+ _page_theme.add_line(false, _("Monospaced font:"), *font_box, "", _("Select fixed-width font"), true, font_button);
+
+ auto mono_font = Gtk::make_managed();
+ mono_font->init( _("Use monospaced font"), "/dialogs/xml/mono-font", false);
+ _page_theme.add_line(false, _("XML tree:"), *mono_font, "", _("Use fixed-width font in XML dialog"), false);
+ }
+
+ //=======================================================================================================
this->AddPage(_page_theme, _("Theming"), iter_ui, PREFS_PAGE_UI_THEME);
symbolicThemeCheck();
diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h
index 82141a32a5485b807eb4ec2a3c043f17b5d3b01b..9ed9f1cbc28a548ef00f4adfb638d35f8843fe5d 100644
--- a/src/ui/dialog/inkscape-preferences.h
+++ b/src/ui/dialog/inkscape-preferences.h
@@ -17,6 +17,7 @@
#define INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H
// checking if cairo supports dithering
+#include
#ifdef WITH_PATCHED_CAIRO
#include "3rdparty/cairo/src/cairo.h"
#else
diff --git a/src/ui/dialog/xml-tree.cpp b/src/ui/dialog/xml-tree.cpp
index 2dfad025cbfb3a78681b9eca46e576df334afd1e..c14ce4fb26a8072a87aa67698e72ddf18cf46c77 100644
--- a/src/ui/dialog/xml-tree.cpp
+++ b/src/ui/dialog/xml-tree.cpp
@@ -11,6 +11,7 @@
* David Turner
* Jon A. Cruz
* Abhishek Sharma
+ * Mike Kowalski
*
* Copyright (C) 1999-2006 Authors
* Released under GNU GPL v2+, read the file 'COPYING' for more information.
@@ -25,6 +26,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -41,12 +43,15 @@
#include "object/sp-root.h"
#include "object/sp-string.h"
+#include "preferences.h"
#include "ui/builder-utils.h"
#include "ui/dialog-events.h"
#include "ui/icon-loader.h"
#include "ui/icon-names.h"
#include "ui/tools/tool-base.h"
+#include "ui/syntax.h"
+#include "util/trim.h"
#include "widgets/sp-xmlview-tree.h"
namespace {
@@ -70,43 +75,44 @@ void paned_set_vertical(Gtk::Paned &paned, bool vertical)
}
} // namespace
-namespace Inkscape {
-namespace UI {
-namespace Dialog {
+namespace Inkscape::UI::Dialog {
XmlTree::XmlTree()
- : DialogBase("/dialogs/xml/", "XMLEditor"),
- _builder(create_builder("dialog-xml.glade")),
- _paned(get_widget(_builder, "pane")),
- xml_element_new_button(get_widget(_builder, "new-elem")),
- xml_text_new_button(get_widget(_builder, "new-text")),
- xml_node_delete_button(get_widget(_builder, "del")),
- xml_node_duplicate_button(get_widget(_builder, "dup")),
- unindent_node_button(get_widget(_builder, "unindent")),
- indent_node_button(get_widget(_builder, "indent")),
- lower_node_button(get_widget(_builder, "lower")),
- raise_node_button(get_widget(_builder, "raise"))
+ : DialogBase("/dialogs/xml/", "XMLEditor")
+ , _builder(create_builder("dialog-xml.glade"))
+ , _paned(get_widget(_builder, "pane"))
+ , xml_element_new_button(get_widget(_builder, "new-elem"))
+ , xml_text_new_button(get_widget(_builder, "new-text"))
+ , xml_node_delete_button(get_widget(_builder, "del"))
+ , xml_node_duplicate_button(get_widget(_builder, "dup"))
+ , unindent_node_button(get_widget(_builder, "unindent"))
+ , indent_node_button(get_widget(_builder, "indent"))
+ , lower_node_button(get_widget(_builder, "lower"))
+ , raise_node_button(get_widget(_builder, "raise"))
+ , _syntax_theme("/theme/syntax-color-theme")
+ , _mono_font("/dialogs/xml/mono-font", false)
{
/* tree view */
tree = SP_XMLVIEW_TREE(sp_xmlview_tree_new(nullptr, nullptr, nullptr));
gtk_widget_set_tooltip_text( GTK_WIDGET(tree), _("Drag to reorder nodes") );
Gtk::ScrolledWindow& tree_scroller = get_widget(_builder, "tree-wnd");
+ _treemm = Gtk::manage(Glib::wrap(GTK_WIDGET(tree)));
tree_scroller.add(*Gtk::manage(Glib::wrap(GTK_WIDGET(tree))));
fix_inner_scroll(&tree_scroller);
Inkscape::Preferences *prefs = Inkscape::Preferences::get();
/* attributes */
- attributes = new AttrDialog();
+ attributes = Gtk::make_managed();
attributes->set_margin_top(0);
attributes->set_margin_bottom(0);
attributes->set_margin_start(0);
attributes->set_margin_end(0);
- attributes->_scrolledWindow.set_shadow_type(Gtk::SHADOW_IN);
+ attributes->get_scrolled_window().set_shadow_type(Gtk::SHADOW_IN);
attributes->show();
- attributes->status_box.hide();
- attributes->status_box.set_no_show_all();
+ attributes->get_status_box().hide();
+ attributes->get_status_box().set_no_show_all();
_paned.pack2(*attributes, true, false);
/* Signal handlers */
@@ -184,8 +190,7 @@ XmlTree::XmlTree()
Glib::ustring icon = "layout-auto";
if (layout == Horizontal) {
icon = "layout-horizontal";
- }
- else if (layout == Vertical) {
+ } else if (layout == Vertical) {
icon = "layout-vertical";
}
get_widget(_builder, "layout-img").set_from_icon_name(icon + "-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR);
@@ -207,6 +212,45 @@ XmlTree::XmlTree()
_layout = static_cast(prefs->getIntLimited("/dialogs/xml/layout", Auto, Auto, Vertical));
static_cast(menu_items.at(_layout))->set_active();
set_layout(_layout);
+ // establish initial layout to prevent unwanted panels resize in auto layout mode
+ paned_set_vertical(_paned, true);
+
+ _syntax_theme.action = [=]() {
+ setSyntaxStyle(Inkscape::UI::Syntax::build_xml_styles(_syntax_theme));
+ // rebuild tree to change markup
+ rebuildTree();
+ };
+
+ setSyntaxStyle(Inkscape::UI::Syntax::build_xml_styles(_syntax_theme));
+
+ _mono_font.action = [=]() {
+ Glib::ustring mono("mono-font");
+ if (_mono_font) {
+ _treemm->get_style_context()->add_class(mono);
+ } else {
+ _treemm->get_style_context()->remove_class(mono);
+ }
+ attributes->set_mono_font(_mono_font);
+ };
+ _mono_font.action();
+
+ tree->renderer->signal_editing_canceled().connect([=]() {
+ stopNodeEditing(false, "", "");
+ });
+ tree->renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& name) {
+ stopNodeEditing(true, path, name);
+ });
+ tree->renderer->signal_editing_started().connect([=](Gtk::CellEditable* cell, const Glib::ustring& path) {
+ startNodeEditing(cell, path);
+ });
+}
+
+void XmlTree::rebuildTree()
+{
+ sp_xmlview_tree_set_repr(tree, nullptr);
+ if (auto document = getDocument()) {
+ set_tree_repr(document->getReprRoot());
+ }
}
void XmlTree::_resized()
@@ -226,8 +270,6 @@ void XmlTree::on_unrealize() {
DialogBase::on_unrealize();
}
-XmlTree::~XmlTree () { }
-
void XmlTree::unsetDocument()
{
document_uri_set_connection.disconnect();
@@ -299,7 +341,7 @@ static void expand_parents(SPXMLViewTree *tree, Inkscape::XML::Node *repr)
}
}
-void XmlTree::set_tree_select(Inkscape::XML::Node *repr)
+void XmlTree::set_tree_select(Inkscape::XML::Node *repr, bool edit)
{
if (selected_repr) {
Inkscape::GC::release(selected_repr);
@@ -326,7 +368,8 @@ void XmlTree::set_tree_select(Inkscape::XML::Node *repr)
GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node);
gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree), path, nullptr, TRUE, 0.66, 0.0);
gtk_tree_selection_select_iter(selection, &node);
- gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree), path, NULL, false);
+ auto col = gtk_tree_view_get_column(&tree->tree, 0);
+ gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree), path, edit ? col : nullptr, edit);
gtk_tree_path_free(path);
} else {
@@ -646,31 +689,82 @@ void XmlTree::cmd_new_element_node()
if (!document)
return;
- Gtk::Dialog dialog;
- Gtk::Entry entry;
-
- dialog.get_content_area()->pack_start(entry);
- dialog.add_button("Cancel", Gtk::RESPONSE_CANCEL);
- dialog.add_button("Create", Gtk::RESPONSE_OK);
- dialog.show_all();
-
- int result = dialog.run();
- if (result == Gtk::RESPONSE_OK) {
- Glib::ustring new_name = entry.get_text();
- if (!new_name.empty()) {
- Inkscape::XML::Document *xml_doc = document->getReprDoc();
- Inkscape::XML::Node *new_repr;
- new_repr = xml_doc->createElement(new_name.c_str());
- Inkscape::GC::release(new_repr);
- selected_repr->appendChild(new_repr);
- set_tree_select(new_repr);
- set_dt_select(new_repr);
-
- DocumentUndo::done(document, Q_("Undo History / XML dialog|Create new element node"), INKSCAPE_ICON("dialog-xml-editor"));
+ // enable in-place node name editing
+ tree->renderer->property_editable() = true;
+
+ auto dummy = ""; // this element has no corresponding SP* object and its construction is silent
+ auto xml_doc = document->getReprDoc();
+ _dummy = xml_doc->createElement(dummy); // create dummy placeholder so we can have a new temporary row in xml tree
+ _node_parent = selected_repr; // remember where the node is inserted
+ selected_repr->appendChild(_dummy);
+ set_tree_select(_dummy, true); // enter in-place node name editing
+}
+
+void XmlTree::startNodeEditing(Gtk::CellEditable* cell, const Glib::ustring& path)
+{
+ if (!cell) {
+ return;
+ }
+ // remove dummy element name so user can start with an empty name
+ auto entry = dynamic_cast(cell);
+ entry->get_buffer()->set_text("");
+}
+
+void XmlTree::stopNodeEditing(bool ok, const Glib::ustring& path, Glib::ustring element)
+{
+ tree->renderer->property_editable() = false;
+
+ auto document = getDocument();
+ if (!document) {
+ return;
+ }
+ // delete dummy node
+ if (_dummy) {
+ document->setXMLDialogSelectedObject(nullptr);
+
+ auto parent = _dummy->parent();
+ Inkscape::GC::release(_dummy);
+ sp_repr_unparent(_dummy);
+ if (parent) {
+ auto parentobject = document->getObjectByRepr(parent);
+ if (parentobject) {
+ parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG);
+ }
}
+
+ _dummy = nullptr;
}
-} // end of cmd_new_element_node()
+ Util::trim(element);
+ if (!ok || element.empty() || !_node_parent) {
+ return;
+ }
+
+ Inkscape::XML::Document* xml_doc = document->getReprDoc();
+ // Extract tag name
+ {
+ static auto const extract_tagname = Glib::Regex::create("^\\s*(\\w[\\w:\\-\\d]*)");
+ Glib::MatchInfo match_info;
+ extract_tagname->match(element, match_info);
+ if (!match_info.matches()) {
+ return;
+ }
+ element = match_info.fetch(1);
+ }
+
+ // prepend "svg:" namespace if none is given
+ if (element.find(':') == Glib::ustring::npos) {
+ element = "svg:" + element;
+ }
+ auto repr = xml_doc->createElement(element.c_str());
+ Inkscape::GC::release(repr);
+ _node_parent->appendChild(repr);
+ set_dt_select(repr);
+ set_tree_select(repr, true);
+ _node_parent = nullptr;
+
+ DocumentUndo::done(document, Q_("Undo History / XML dialog|Create new element node"), INKSCAPE_ICON("dialog-xml-editor"));
+}
void XmlTree::cmd_new_text_node()
{
@@ -876,9 +970,12 @@ void XmlTree::desktopReplaced() {
}
}
+void XmlTree::setSyntaxStyle(Inkscape::UI::Syntax::XMLStyles const &new_style)
+{
+ tree->formatter->setStyle(new_style);
}
-}
-}
+
+} // namespace Inkscape::UI::Dialog
/*
Local Variables:
diff --git a/src/ui/dialog/xml-tree.h b/src/ui/dialog/xml-tree.h
index b56739e6797851f60422db09227912d7cad80865..2389c6fdb843d1708b89070282c13cda01676f22 100644
--- a/src/ui/dialog/xml-tree.h
+++ b/src/ui/dialog/xml-tree.h
@@ -13,6 +13,9 @@
#ifndef INKSCAPE_UI_DIALOG_XML_TREE_H
#define INKSCAPE_UI_DIALOG_XML_TREE_H
+#include
+#include
+#include
#include
#include
#include
@@ -29,6 +32,8 @@
#include "message.h"
#include "attrdialog.h"
#include "dialog-base.h"
+#include "preferences.h"
+#include "ui/syntax.h"
class SPObject;
struct SPXMLViewAttrList;
@@ -51,7 +56,9 @@ class XmlTree : public DialogBase
{
public:
XmlTree();
- ~XmlTree() override;
+ ~XmlTree() override = default;
+
+ void setSyntaxStyle(Inkscape::UI::Syntax::XMLStyles const &new_style);
private:
void unsetDocument();
@@ -77,7 +84,7 @@ private:
/**
* Select a node in the xml tree
*/
- void set_tree_select(Inkscape::XML::Node *repr);
+ void set_tree_select(Inkscape::XML::Node *repr, bool edit = false);
/**
* Set the attribute list to match the selected node in the tree
@@ -146,6 +153,9 @@ private:
bool in_dt_coordsys(SPObject const &item);
void on_unrealize() override;
+ void rebuildTree();
+ void stopNodeEditing(bool ok, Glib::ustring const &path, Glib::ustring name);
+ void startNodeEditing(Gtk::CellEditable *cell, Glib::ustring const &path);
/**
* Flag to ensure only one operation is performed at once
@@ -161,6 +171,7 @@ private:
/* XmlTree Widgets */
SPXMLViewTree *tree = nullptr;
+ Gtk::Widget* _treemm = nullptr;
AttrDialog *attributes;
Gtk::Box *_attrbox;
@@ -186,6 +197,10 @@ private:
gulong _tree_move = 0;
enum DialogLayout: int { Auto = 0, Horizontal, Vertical };
DialogLayout _layout = Auto;
+ Pref _syntax_theme;
+ Pref _mono_font;
+ Inkscape::XML::Node* _dummy = nullptr;
+ Inkscape::XML::Node* _node_parent = nullptr;
};
} // namespace Dialog
diff --git a/src/ui/icon-loader.cpp b/src/ui/icon-loader.cpp
index f524028ce2335667a57406b746a19e38101ce4e6..a5361621a51664297a83b9837a23fb960463f878 100644
--- a/src/ui/icon-loader.cpp
+++ b/src/ui/icon-loader.cpp
@@ -116,6 +116,11 @@ Glib::RefPtr sp_get_shape_icon(Glib::ustring shape_type, Gdk::RGBA
Gtk::IconInfo iconinfo = icon_theme->lookup_icon("shape-" + shape_type + "-symbolic",
size * scale, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ if (!iconinfo) {
+ // fallback to regular icons
+ iconinfo = icon_theme->lookup_icon(shape_type + "-symbolic", size * scale, Gtk::ICON_LOOKUP_FORCE_SIZE);
+ }
+
if (!iconinfo) {
iconinfo = icon_theme->lookup_icon("shape-unknown-symbolic", size * scale, Gtk::ICON_LOOKUP_FORCE_SIZE);
// We know this could fail, but it should exist, so persist.
diff --git a/src/ui/shortcuts.cpp b/src/ui/shortcuts.cpp
index 574a42bb111d5bea1ba6df0c9ea99a2020581cbb..07758c9ac83489759ef1f7cb24fc21e28b1020e8 100644
--- a/src/ui/shortcuts.cpp
+++ b/src/ui/shortcuts.cpp
@@ -487,8 +487,8 @@ Shortcuts::add_shortcut(Glib::ustring name, const Gtk::AccelKey& shortcut, bool
{
// Remove previous use of shortcut (already removed if new user shortcut).
if (Glib::ustring old_name = remove_shortcut(shortcut); old_name != "") {
- std::cerr << "Shortcut::add_shortcut: duplicate shortcut found for: " << shortcut.get_abbrev().raw()
- << " Old: " << old_name.raw() << " New: " << name.raw() << " !" << std::endl;
+ // std::cerr << "Shortcut::add_shortcut: duplicate shortcut found for: " << shortcut.get_abbrev().raw()
+ // << " Old: " << old_name.raw() << " New: " << name.raw() << " !" << std::endl;
}
// Add shortcut
diff --git a/src/ui/syntax.cpp b/src/ui/syntax.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b802d7d0cd381192693de95670478658ef181e1c
--- /dev/null
+++ b/src/ui/syntax.cpp
@@ -0,0 +1,395 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file Syntax coloring via Gtksourceview and Pango markup.
+ */
+/* Authors:
+ * Rafael Siejakowski
+ * Mike Kowalski
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/syntax.h"
+
+#include
+#include
+#include
+#include
+
+#include "color.h"
+#include "config.h"
+#include "io/resource.h"
+#include "object/sp-factory.h"
+#include "util/trim.h"
+
+#if WITH_GSOURCEVIEW
+# include
+#endif
+
+namespace Inkscape::UI::Syntax {
+
+Glib::ustring XMLFormatter::_format(Style const &style, Glib::ustring const &content) const
+{
+ return _format(style, content.c_str());
+}
+
+/** Get the opening tag of the Pango markup for this style. */
+Glib::ustring Style::openingTag() const
+{
+ if (isDefault()) {
+ return "";
+ }
+
+ std::ostringstream ost;
+ ost << "raw() << '"';
+ }
+ if (background) {
+ ost << " bgcolor=\"" << background->raw() << '"';
+ }
+ if (bold) {
+ ost << " weight=\"bold\"";
+ }
+ if (italic) {
+ ost << " font_style=\"italic\"";
+ }
+ if (underline) {
+ ost << " underline=\"single\"";
+ }
+
+ ost << ">";
+ return Glib::ustring(ost.str());
+}
+
+/** Get the closing tag of Pango markup for this style. */
+Glib::ustring Style::closingTag() const
+{
+ return isDefault() ? "" : "";
+}
+
+Glib::ustring quote(const char* text)
+{
+ return Glib::ustring::compose("\"%1\"", text);
+}
+
+/** Open a new XML tag with the given tag name. */
+void XMLFormatter::openTag(char const *tag_name)
+{
+ _wip = _format(_style.angular_brackets, "<");
+
+ // Highlight as errors unsupported tags in SVG namespace (explicit or implicit).
+ bool error = false;
+ std::string fully_qualified_name(tag_name);
+ if (fully_qualified_name.empty()) {
+ return;
+ }
+ bool is_svg = false;
+ if (fully_qualified_name.find(':') == std::string::npos) {
+ fully_qualified_name = std::string("svg:") + fully_qualified_name;
+ is_svg = true;
+ } else if (fully_qualified_name.find("svg:") == 0) {
+ is_svg = true;
+ }
+ if (is_svg && !SPFactory::supportsType(fully_qualified_name)) {
+ error = true;
+ }
+ _wip += _format(error ? _style.error : _style.tag_name, tag_name);
+}
+
+void XMLFormatter::addAttribute(char const *name, char const *value)
+{
+ _wip += Glib::ustring::compose(" %1%2%3",
+ _format(_style.attribute_name, name),
+ _format(_style.angular_brackets, "="),
+ _format(_style.attribute_value, quote(value)));
+}
+
+Glib::ustring XMLFormatter::finishTag(bool self_close)
+{
+ return _wip + _format(_style.angular_brackets, self_close ? "/>" : ">");
+}
+
+Glib::ustring XMLFormatter::formatContent(char const* content, bool wrap_in_quotes) const
+{
+ Glib::ustring text = wrap_in_quotes ? quote(content) : content;
+ return _format(_style.content, text);
+}
+
+Glib::ustring XMLFormatter::formatComment(char const* comment, bool wrap_in_marks) const
+{
+ if (wrap_in_marks) {
+ auto wrapped = Glib::ustring::compose("", comment);
+ return _format(_style.comment, wrapped.c_str());
+ }
+ return _format(_style.comment, comment);
+}
+
+XMLStyles build_xml_styles(const Glib::ustring& syntax_theme)
+{
+ XMLStyles styles;
+
+#if WITH_GSOURCEVIEW
+ auto manager = gtk_source_style_scheme_manager_get_default();
+ if (auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, syntax_theme.c_str())) {
+
+ auto get_color = [](GtkSourceStyle* style, const char* prop) -> std::optional {
+ std::optional maybe_color;
+ Glib::ustring name(prop);
+ gboolean set;
+ gchar* color = 0;
+ g_object_get(style, (name + "-set").c_str(), &set, name.c_str(), &color, nullptr);
+ if (set && color && *color == '#') {
+ maybe_color = Glib::ustring(color);
+ }
+ g_free(color);
+ return maybe_color;
+ };
+
+ auto get_bool = [](GtkSourceStyle* style, const char* prop, bool def = false) -> bool {
+ Glib::ustring name(prop);
+ gboolean set;
+ gboolean flag;
+ g_object_get(style, (name + "-set").c_str(), &set, name.c_str(), &flag, nullptr);
+ return set ? !!flag : def;
+ };
+
+ auto get_underline = [](GtkSourceStyle* style, bool def = false) -> bool {
+ Glib::ustring name("underline");
+ gboolean set;
+ PangoUnderline underline;
+ g_object_get(style, (name + "-set").c_str(), &set, ("pango-" + name).c_str(), &underline, nullptr);
+ return set ? underline != PANGO_UNDERLINE_NONE : def;
+ };
+
+ auto to_style = [&](char const *id) -> Style {
+ auto s = gtk_source_style_scheme_get_style(scheme, id);
+ if (!s) {
+ return Style();
+ }
+
+ Style style;
+
+ style.color = get_color(s, "foreground");
+ style.background = get_color(s, "background");
+ style.bold = get_bool(s, "bold");
+ style.italic = get_bool(s, "italic");
+ style.underline = get_underline(s);
+
+ return style;
+ };
+
+ styles.tag_name = to_style("def:statement");
+ styles.attribute_name = to_style("def:number");
+ styles.attribute_value = to_style("def:string");
+ styles.content = to_style("def:string");
+ styles.comment = to_style("def:comment");
+ styles.prolog = to_style("def:warning");
+ styles.angular_brackets = to_style("draw-spaces");
+ styles.error = to_style("def:error");
+ }
+#endif
+
+ return styles;
+}
+
+/** @brief Reformat CSS for better readability.
+ */
+Glib::ustring prettify_css(Glib::ustring const &css)
+{
+ // Ensure that there's a space after every colon, unless there's a slash (as in a URL).
+ static auto const colon_without_space = Glib::Regex::create(":([^\\s\\/])");
+ auto reformatted = colon_without_space->replace(css, 0, ": \\1", Glib::RegexMatchFlags::REGEX_MATCH_NOTEMPTY);
+ // Ensure that there's a newline after every semicolon.
+ static auto const semicolon_without_newline = Glib::Regex::create(";([^\r\n])");
+ reformatted = semicolon_without_newline->replace(reformatted, 0, ";\n\\1", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANYCRLF);
+ // If the last character is not a semicolon, append one.
+ if (auto len = css.size(); len && css[len - 1] != ';') {
+ reformatted += ";";
+ }
+ return reformatted;
+}
+
+/** Undo the CSS prettification by stripping some whitespace from CSS markup. */
+Glib::ustring minify_css(Glib::ustring const &css)
+{
+ static auto const space_after = Glib::Regex::create("(:|;)[\\s]+");
+ auto minified = space_after->replace(css, 0, "\\1", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+ // Strip final semicolon
+ if (auto const len = minified.size(); len && minified[len - 1] == ';') {
+ minified = minified.erase(len - 1);
+ }
+ return minified;
+}
+
+/** @brief Reformat a path 'd' attibute for better readability. */
+Glib::ustring prettify_svgd(Glib::ustring const &d)
+{
+ auto result = d;
+ Util::trim(result);
+ // Ensure that a non-M command is preceded only by a newline.
+ static auto const space_b4_command = Glib::Regex::create("(?<=\\S)\\s*(?=[LHVCSQTAZlhvcsqtaz])");
+ result = space_b4_command->replace(result, 1, "\n", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+
+ // Before a non-initial M command, we want to have two newlines to visually separate the subpaths.
+ static auto const space_b4_m = Glib::Regex::create("(?<=\\S)\\s*(?=[Mm])");
+ result = space_b4_m->replace(result, 1, "\n\n", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+
+ // Ensure that there's a space after each command letter other than Z.
+ static auto const nospace = Glib::Regex::create("([MLHVCSQTAmlhvcsqta])(?=\\S)");
+ return nospace->replace(result, 0, "\\1 ", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+}
+
+/** @brief Remove excessive space, including newlines, from a path 'd' attibute. */
+Glib::ustring minify_svgd(Glib::ustring const &d)
+{
+ static auto const excessive_space = Glib::Regex::create("[\\s]+");
+ auto result = excessive_space->replace(d, 0, " ", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY);
+ Util::trim(result);
+ return result;
+}
+
+/** Set default options on a TextView widget used for syntax-colored editing. */
+static void init_text_view(Gtk::TextView* textview)
+{
+ textview->set_wrap_mode(Gtk::WrapMode::WRAP_WORD);
+ textview->set_editable(true);
+ textview->show();
+}
+
+/// Plain text view widget without syntax coloring
+class PlainTextView : public TextEditView
+{
+public:
+ PlainTextView()
+ : _textview(std::make_unique(Gtk::TextBuffer::create()))
+ {
+ init_text_view(_textview.get());
+ }
+
+ void setStyle(const Glib::ustring& theme) override { /* no op */ }
+ void setText(const Glib::ustring& text) override { _textview->get_buffer()->set_text(text); }
+
+ Glib::ustring getText() const override { return _textview->get_buffer()->get_text(); }
+ Gtk::TextView& getTextView() const override { return *_textview; }
+
+private:
+ std::unique_ptr _textview;
+};
+
+#if WITH_GSOURCEVIEW
+
+/** @brief Return a pointer to a language manager which is aware of both
+ * default and custom syntaxes.
+ */
+static GtkSourceLanguageManager* get_language_manager()
+{
+ auto ui_path = IO::Resource::get_path_string(IO::Resource::SYSTEM, IO::Resource::UIS);
+ auto default_manager = gtk_source_language_manager_get_default();
+ auto default_paths = gtk_source_language_manager_get_search_path(default_manager);
+
+ std::vector all_paths;
+ for (auto path = default_paths; *path; path++) {
+ all_paths.push_back(*path);
+ }
+ all_paths.push_back(ui_path.c_str());
+ all_paths.push_back(nullptr);
+
+ auto result = gtk_source_language_manager_new();
+ gtk_source_language_manager_set_search_path(result, (gchar **)all_paths.data());
+ return result;
+}
+
+class SyntaxHighlighting : public TextEditView
+{
+public:
+ SyntaxHighlighting() = delete;
+ /** @brief Construct a syntax highlighter for a given language. */
+ SyntaxHighlighting(char const* const language,
+ Glib::ustring (*prettify_func)(Glib::ustring const &),
+ Glib::ustring (*minify_func)(Glib::ustring const &))
+ : _prettify{prettify_func}
+ , _minify{minify_func}
+ {
+ auto manager = get_language_manager();
+ auto lang = gtk_source_language_manager_get_language(manager, language);
+ _buffer = gtk_source_buffer_new_with_language(lang);
+ auto view = gtk_source_view_new_with_buffer(_buffer);
+ _textview = std::unique_ptr(Glib::wrap((GtkTextView*)view));
+ if (!_textview) {
+ // don't crash when sourceview cannot be created; substitute with a regular one;
+ // in this case GTK has already outputted warnings
+ _textview = std::make_unique(Gtk::TextBuffer::create());
+ }
+ init_text_view(_textview.get());
+ }
+
+ ~SyntaxHighlighting() override { g_object_unref(_buffer); }
+private:
+ GtkSourceBuffer *_buffer = nullptr; // Owned by us
+ std::unique_ptr _textview;
+ Glib::ustring (*_prettify)(Glib::ustring const &);
+ Glib::ustring (*_minify)(Glib::ustring const &);
+
+public:
+ void setStyle(Glib::ustring const &theme) override
+ {
+ auto manager = gtk_source_style_scheme_manager_get_default();
+ auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, theme.c_str());
+ gtk_source_buffer_set_style_scheme(_buffer, scheme);
+ }
+
+ /** @brief Set the displayed text to a prettified version of the passed string. */
+ void setText(Glib::ustring const &text) override
+ {
+ _textview->get_buffer()->set_text(_prettify(text));
+ }
+
+ /** @brief Get a minified version of the buffer contents, suitable for inserting into XML. */
+ Glib::ustring getText() const override
+ {
+ return _minify(_textview->get_buffer()->get_text());
+ }
+
+ Gtk::TextView &getTextView() const override { return *_textview; };
+};
+
+#endif // WITH_GSOURCEVIEW
+
+/** Create a styled text view using the desired syntax highlighting mode. */
+std::unique_ptr TextEditView::create(SyntaxMode mode)
+{
+#if WITH_GSOURCEVIEW
+ auto const no_reformat = [](auto &s) { return s; };
+ switch (mode) {
+ case SyntaxMode::PlainText:
+ return std::make_unique();
+ case SyntaxMode::InlineCss:
+ return std::make_unique("inline-css", &prettify_css, &minify_css);
+ case SyntaxMode::CssStyle:
+ return std::make_unique("css", no_reformat, no_reformat);
+ case SyntaxMode::SvgPathData:
+ return std::make_unique("svgd", &prettify_svgd, &minify_svgd);
+ case SyntaxMode::SvgPolyPoints:
+ return std::make_unique("svgpoints", no_reformat, no_reformat);
+ default:
+ throw std::runtime_error("Missing case statement in TetxEditView::create()");
+ }
+#else
+ return std::make_unique();
+#endif
+}
+
+} // namespace Inkscape::UI::Syntax
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/ui/syntax.h b/src/ui/syntax.h
new file mode 100644
index 0000000000000000000000000000000000000000..528ad93281ee41e287ce16279d4af3bd95f7dfba
--- /dev/null
+++ b/src/ui/syntax.h
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file Syntax coloring via Gtksourceview and Pango markup.
+ */
+/* Authors:
+ * Rafael Siejakowski
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_UI_UI_SYNTAX_H
+#define SEEN_UI_UI_SYNTAX_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "color.h"
+
+namespace Inkscape::UI::Syntax {
+
+/** The style of a single element in a (Pango markup)-enabled widget. */
+struct Style
+{
+ std::optional color;
+ std::optional background;
+ uint8_t bold : 1;
+ uint8_t italic : 1;
+ uint8_t underline : 1;
+
+ Style()
+ : bold{false}
+ , italic{false}
+ , underline{false}
+ {}
+
+ bool isDefault() const { return !color && !background && !bold && !italic && !underline; }
+ Glib::ustring openingTag() const;
+ Glib::ustring closingTag() const;
+};
+
+/** The styles used for simple XML syntax highlighting. */
+struct XMLStyles
+{
+ Style prolog;
+ Style comment;
+ Style angular_brackets;
+ Style tag_name;
+ Style attribute_name;
+ Style attribute_value;
+ Style content;
+ Style error;
+};
+
+/** @brief A formatter for XML syntax, based on Pango markup.
+ *
+ * This mechanism is used in the TreeView in the XML Dialog,
+ * where the syntax highlighting of XML tags is accomplished
+ * via Pango markup.
+ */
+class XMLFormatter
+{
+public:
+ XMLFormatter() = default;
+ XMLFormatter(XMLStyles &&styles)
+ : _style{styles}
+ {}
+
+ void setStyle(XMLStyles const &new_style) { _style = new_style; }
+ void setStyle(XMLStyles &&new_style) { _style = new_style; }
+
+ void openTag(char const *tag_name);
+ void addAttribute(char const *attribute_name, char const *attribute_value);
+ Glib::ustring finishTag(bool self_close = false);
+
+ Glib::ustring formatContent(char const* content, bool wrap_in_quotes = true) const;
+ Glib::ustring formatComment(char const* comment, bool wrap_in_comment_marks = true) const;
+ Glib::ustring formatProlog(char const* prolog) const { return _format(_style.prolog, prolog); }
+
+private:
+ Glib::ustring _format(Style const &style, Glib::ustring const &content) const;
+ Glib::ustring _format(Style const &style, char const *content) const
+ {
+ return style.openingTag() + Glib::Markup::escape_text(content) + style.closingTag();
+ }
+
+ XMLStyles _style;
+ Glib::ustring _wip;
+};
+
+/// Build XML styles from a GTKSourceView syntax color theme.
+XMLStyles build_xml_styles(const Glib::ustring& syntax_theme);
+
+/// Syntax highlighting mode (language).
+enum class SyntaxMode
+{
+ PlainText, ///< Plain text (no highlighting).
+ InlineCss, ///< Inline CSS (contents of a style="..." attribute).
+ CssStyle, ///< File-scope CSS (contents of a CSS file or a