From 351aa08e9a609c9003ec2b3baf6ae7e4830f516a Mon Sep 17 00:00:00 2001 From: ftomara Date: Sat, 24 May 2025 00:05:53 +0300 Subject: [PATCH 1/2] Recolor Artwork project Implements https://gitlab.com/inkscape/ux/-/issues/318 Fixes https://gitlab.com/inkscape/inbox/-/issues/6095 --- .../symbolic/actions/go-right-symbolic.svg | 1 + .../symbolic/actions/lightness-symbolic.svg | 115 +++ .../actions/object-recolor-art-symbolic.svg | 5 + .../actions/reset-colors-symbolic.svg | 4 + .../symbolic/actions/saturation-symbolic.svg | 1 + .../symbolic/actions/go-right-symbolic.svg | 1 + .../symbolic/actions/lightness-symbolic.svg | 115 +++ .../actions/object-recolor-art-symbolic.svg | 5 + .../actions/reset-colors-symbolic.svg | 4 + .../symbolic/actions/reset-symbolic.svg | 61 +- .../symbolic/actions/saturation-symbolic.svg | 1 + .../symbolic/actions/go-right-symbolic.svg | 1 + .../symbolic/actions/lightness-symbolic.svg | 115 +++ .../actions/object-recolor-art-symbolic.svg | 5 + .../actions/reset-colors-symbolic.svg | 4 + .../symbolic/actions/reset-symbolic.svg | 58 -- .../symbolic/actions/saturation-symbolic.svg | 1 + share/ui/style.css | 45 ++ share/ui/widget-recolor.ui | 176 +++++ src/CMakeLists.txt | 2 + src/desktop.cpp | 9 +- src/desktop.h | 11 + src/object-colors.cpp | 443 +++++++++++ src/object-colors.h | 88 +++ src/selcue.cpp | 10 +- src/selcue.h | 3 + src/seltrans.h | 1 + src/ui/CMakeLists.txt | 10 +- src/ui/tools/select-tool.cpp | 5 + src/ui/tools/select-tool.h | 1 + src/ui/tools/tool-base.cpp | 2 + src/ui/tools/tool-base.h | 3 + src/ui/widget/color-notebook.cpp | 12 +- src/ui/widget/color-notebook.h | 1 + src/ui/widget/color-page.h | 8 + src/ui/widget/color-preview.cpp | 4 +- src/ui/widget/fill-style.cpp | 3 + src/ui/widget/gradient-editor.cpp | 9 +- src/ui/widget/gradient-editor.h | 2 + src/ui/widget/ink-color-wheel.cpp | 688 +++++++++++++++++- src/ui/widget/ink-color-wheel.h | 100 +++ src/ui/widget/marker-combo-box.cpp | 53 +- src/ui/widget/marker-combo-box.h | 31 +- src/ui/widget/multi-marker-color-plate.cpp | 178 +++++ src/ui/widget/multi-marker-color-plate.h | 103 +++ src/ui/widget/paint-selector.cpp | 150 +++- src/ui/widget/paint-selector.h | 19 +- src/ui/widget/recolor-art-manager.cpp | 129 ++++ src/ui/widget/recolor-art-manager.h | 42 ++ src/ui/widget/recolor-art.cpp | 630 ++++++++++++++++ src/ui/widget/recolor-art.h | 118 +++ src/ui/widget/stroke-style.cpp | 3 + testfiles/CMakeLists.txt | 2 + .../src/multi-marker-color-wheel-test.cpp | 100 +++ testfiles/src/object-colors-test.cpp | 228 ++++++ 55 files changed, 3764 insertions(+), 155 deletions(-) create mode 100644 share/icons/Dash/symbolic/actions/go-right-symbolic.svg create mode 100644 share/icons/Dash/symbolic/actions/lightness-symbolic.svg create mode 100644 share/icons/Dash/symbolic/actions/object-recolor-art-symbolic.svg create mode 100644 share/icons/Dash/symbolic/actions/reset-colors-symbolic.svg create mode 100644 share/icons/Dash/symbolic/actions/saturation-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/go-right-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/lightness-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/object-recolor-art-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/reset-colors-symbolic.svg create mode 100644 share/icons/hicolor/symbolic/actions/saturation-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/go-right-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/lightness-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/object-recolor-art-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/reset-colors-symbolic.svg delete mode 100644 share/icons/multicolor/symbolic/actions/reset-symbolic.svg create mode 100644 share/icons/multicolor/symbolic/actions/saturation-symbolic.svg create mode 100644 share/ui/widget-recolor.ui create mode 100644 src/object-colors.cpp create mode 100644 src/object-colors.h create mode 100644 src/ui/widget/multi-marker-color-plate.cpp create mode 100644 src/ui/widget/multi-marker-color-plate.h create mode 100644 src/ui/widget/recolor-art-manager.cpp create mode 100644 src/ui/widget/recolor-art-manager.h create mode 100644 src/ui/widget/recolor-art.cpp create mode 100644 src/ui/widget/recolor-art.h create mode 100644 testfiles/src/multi-marker-color-wheel-test.cpp create mode 100644 testfiles/src/object-colors-test.cpp diff --git a/share/icons/Dash/symbolic/actions/go-right-symbolic.svg b/share/icons/Dash/symbolic/actions/go-right-symbolic.svg new file mode 100644 index 0000000000..5d08323d90 --- /dev/null +++ b/share/icons/Dash/symbolic/actions/go-right-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/Dash/symbolic/actions/lightness-symbolic.svg b/share/icons/Dash/symbolic/actions/lightness-symbolic.svg new file mode 100644 index 0000000000..1e8d4bc38e --- /dev/null +++ b/share/icons/Dash/symbolic/actions/lightness-symbolic.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/share/icons/Dash/symbolic/actions/object-recolor-art-symbolic.svg b/share/icons/Dash/symbolic/actions/object-recolor-art-symbolic.svg new file mode 100644 index 0000000000..b078cf421c --- /dev/null +++ b/share/icons/Dash/symbolic/actions/object-recolor-art-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/share/icons/Dash/symbolic/actions/reset-colors-symbolic.svg b/share/icons/Dash/symbolic/actions/reset-colors-symbolic.svg new file mode 100644 index 0000000000..7d47572893 --- /dev/null +++ b/share/icons/Dash/symbolic/actions/reset-colors-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/share/icons/Dash/symbolic/actions/saturation-symbolic.svg b/share/icons/Dash/symbolic/actions/saturation-symbolic.svg new file mode 100644 index 0000000000..7f0b0fda5a --- /dev/null +++ b/share/icons/Dash/symbolic/actions/saturation-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/hicolor/symbolic/actions/go-right-symbolic.svg b/share/icons/hicolor/symbolic/actions/go-right-symbolic.svg new file mode 100644 index 0000000000..5d08323d90 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/go-right-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/hicolor/symbolic/actions/lightness-symbolic.svg b/share/icons/hicolor/symbolic/actions/lightness-symbolic.svg new file mode 100644 index 0000000000..1e8d4bc38e --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/lightness-symbolic.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/share/icons/hicolor/symbolic/actions/object-recolor-art-symbolic.svg b/share/icons/hicolor/symbolic/actions/object-recolor-art-symbolic.svg new file mode 100644 index 0000000000..b078cf421c --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/object-recolor-art-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/share/icons/hicolor/symbolic/actions/reset-colors-symbolic.svg b/share/icons/hicolor/symbolic/actions/reset-colors-symbolic.svg new file mode 100644 index 0000000000..7d47572893 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/reset-colors-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/share/icons/hicolor/symbolic/actions/reset-symbolic.svg b/share/icons/hicolor/symbolic/actions/reset-symbolic.svg index 7dcef2b3f7..09c40a1e30 100644 --- a/share/icons/hicolor/symbolic/actions/reset-symbolic.svg +++ b/share/icons/hicolor/symbolic/actions/reset-symbolic.svg @@ -1,58 +1,5 @@ - - - - - - - - - - - - + + + + diff --git a/share/icons/hicolor/symbolic/actions/saturation-symbolic.svg b/share/icons/hicolor/symbolic/actions/saturation-symbolic.svg new file mode 100644 index 0000000000..7f0b0fda5a --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/saturation-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/multicolor/symbolic/actions/go-right-symbolic.svg b/share/icons/multicolor/symbolic/actions/go-right-symbolic.svg new file mode 100644 index 0000000000..5d08323d90 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/go-right-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/multicolor/symbolic/actions/lightness-symbolic.svg b/share/icons/multicolor/symbolic/actions/lightness-symbolic.svg new file mode 100644 index 0000000000..1e8d4bc38e --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/lightness-symbolic.svg @@ -0,0 +1,115 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/share/icons/multicolor/symbolic/actions/object-recolor-art-symbolic.svg b/share/icons/multicolor/symbolic/actions/object-recolor-art-symbolic.svg new file mode 100644 index 0000000000..b078cf421c --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/object-recolor-art-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/share/icons/multicolor/symbolic/actions/reset-colors-symbolic.svg b/share/icons/multicolor/symbolic/actions/reset-colors-symbolic.svg new file mode 100644 index 0000000000..7d47572893 --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/reset-colors-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/share/icons/multicolor/symbolic/actions/reset-symbolic.svg b/share/icons/multicolor/symbolic/actions/reset-symbolic.svg deleted file mode 100644 index 7dcef2b3f7..0000000000 --- a/share/icons/multicolor/symbolic/actions/reset-symbolic.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - diff --git a/share/icons/multicolor/symbolic/actions/saturation-symbolic.svg b/share/icons/multicolor/symbolic/actions/saturation-symbolic.svg new file mode 100644 index 0000000000..7f0b0fda5a --- /dev/null +++ b/share/icons/multicolor/symbolic/actions/saturation-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/ui/style.css b/share/ui/style.css index 9b07b7c878..8b4a234221 100644 --- a/share/ui/style.css +++ b/share/ui/style.css @@ -1727,3 +1727,48 @@ editablelabel.editing { outline-offset: 0; outline: 1px solid @theme_fg_color; } + +/* +********************** +* Recolor Art class * +********************** +*/ + +#recolor-art #original, +#recolored { + min-height: 12px; + border-radius: 4px; + /* border: 2px solid transparent; */ +} + +#original-recolor-box{ +padding:0 1px; +} +.type_box { + background-color: alpha(@theme_bg_color,0.8); + border-radius: 2px; + padding-left: 2px; + padding-right: 6px; +} +#recolor-art listview row:selected{ + background-color: transparent; + border: 2px solid @theme_selected_bg_color; + border-radius: 4px; + color: @theme_fg_color; +} + +#recolor-art gridview child:selected { + background-color: transparent; + border: 1px solid @theme_selected_bg_color; + border-radius: 1px; + color: @theme_fg_color; +} + +#recolor-art scrollbar slider { + min-width: 0px; + min-height: 0px; + margin: 0px; + padding: 0px; + background: transparent; + border: none; +} diff --git a/share/ui/widget-recolor.ui b/share/ui/widget-recolor.ui new file mode 100644 index 0000000000..e1ea7ca509 --- /dev/null +++ b/share/ui/widget-recolor.ui @@ -0,0 +1,176 @@ + + + + + recolor-art + vertical + true + true + + + list-wheel-box + top + false + 250 + + + original-recolor-colors-list-box + vertical + true + true + + + original-reset-recolor + 3 + none + + + Original + fill + + + + + reset + true + false + false + center + 16 + + + reset-settings + normal + + + + + + + New + center + + + + + + + true + true + true + never + false + 240 + + + colors-list + vertical + true + fill + fill + + + false + fill + true + fill + + + + + + + + + + vertical + true + + + + + + + + + horizontal + 6 + center + true + + + hamburger-menu + normal + + + + + Color List + center + center + + + + + + + + color-wheel-page + vertical + true + true + + + + + + horizontal + 6 + center + true + + + color-wheel-symbolic + normal + + + + + Color Wheel + center + center + + + + + + + + + + + + liveP-apply + horizontal + 300 + + + liveP + Live Preview + end + start + + + + + + + + \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a7ca11c1a4..daa9b86527 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,6 +44,7 @@ set(inkscape_SRC message-context.cpp message-stack.cpp mod360.cpp + object-colors.cpp object-hierarchy.cpp object-snapper.cpp page-manager.cpp @@ -126,6 +127,7 @@ set(inkscape_SRC message.h mod360.h number-opt-number.h + object-colors.h object-hierarchy.h object-snapper.h page-manager.h diff --git a/src/desktop.cpp b/src/desktop.cpp index f499f3c6ff..9b1dce62d5 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -212,6 +212,14 @@ void SPDesktop::setDesktopWidget(SPDesktopWidget *dtw) _widget = dtw; } +void SPDesktop::setHideSelectionBoxes(bool hide) +{ + if (_hide_selection_boxes != hide) { + _hide_selection_boxes = hide; + _signal_hide_selection_boxes_changed.emit(hide); + } +} + //-------------------------------------------------------------------- /* Public methods */ @@ -1184,7 +1192,6 @@ SPDesktop::getCurrentOrToolStylePath(Glib::ustring const &tool_path) } } - void SPDesktop::setToolboxFocusTo(char const * const label) { diff --git a/src/desktop.h b/src/desktop.h index 8f9d1e8a47..57be44d08d 100644 --- a/src/desktop.h +++ b/src/desktop.h @@ -162,6 +162,9 @@ public: void setDesktopWidget(SPDesktopWidget *dtw); + void setHideSelectionBoxes(bool hide); + bool getHideSelectionBoxes() const { return _hide_selection_boxes; } + private: SPDocument *document = nullptr; std::unique_ptr _message_stack; @@ -183,6 +186,8 @@ private: std::unique_ptr canvas; + bool _hide_selection_boxes = false; + public: Inkscape::UI::Tools::ToolBase *getTool () const { return _tool.get(); } Inkscape::Selection *getSelection () const { return _selection.get(); } @@ -270,6 +275,10 @@ public: return _query_style_signal.connect(std::forward(slot)); } + template sigc::connection connectHideSelectionBoxes(F &&slot) { + return _signal_hide_selection_boxes_changed.connect(std::forward(slot)); + } + // there's an object selected and it has a gradient fill and/or stroke; one of the gradient stops has been selected // callback receives sender pointer and selected stop pointer sigc::connection connect_gradient_stop_selected(sigc::slot const &slot); @@ -549,6 +558,8 @@ private: sigc::signal _control_point_selected; sigc::signal _text_cursor_moved; + sigc::signal _signal_hide_selection_boxes_changed; + sigc::scoped_connection _reconstruction_start_connection; sigc::scoped_connection _reconstruction_finish_connection; sigc::scoped_connection _schedule_zoom_from_document_connection; diff --git a/src/object-colors.cpp b/src/object-colors.cpp new file mode 100644 index 0000000000..b7382d2d37 --- /dev/null +++ b/src/object-colors.cpp @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "object-colors.h" + +#include "desktop-style.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "object/sp-gradient.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-marker.h" +#include "object/sp-mask.h" +#include "object/sp-use.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" +#include "object/sp-tspan.h" +#include "object/sp-text.h" +#include "style.h" + +namespace Inkscape { + +/* + * reset selected object colors to their original colors all at once + * used when LP checkbox is unchecked + */ +void ObjectColorSet::revertToOriginalColors(bool is_reset_clicked) +{ + for (auto &[key, items] : _selected_colors) { + if (is_reset_clicked) { + items.second->new_color = items.second->old_color; + } + applyNewColorToSelection(key, items.second->old_color); + } +} + +/* + * convert selected object colors to the new choosen colors all at once + * used when LP checkbox is unchecked then checked again + */ +void ObjectColorSet::convertToRecoloredColors() +{ + for (auto const &[key, items] : _selected_colors) { + if (items.second) { + Color new_color = items.second->new_color; + applyNewColorToSelection(key, new_color); + } + } +} + +/* +* loop over selection and lowers opacity for items with color +* that doesn't match the parameter color for when user hovers on +* any colorpreview in the list to highlight the hovered on colored objects +*/ +void ObjectColorSet::changeOpacity(bool change_opacity,uint32_t color ,bool is_preview) +{ + for (auto const &[key, value] : _selected_colors) { + Color new_color = is_preview ? value.second->new_color : value.second->old_color; + if (change_opacity && key != color) { + new_color.setOpacity(0.05); + } + applyNewColorToSelection(key, new_color); + } +} + +/* + * get stops vector from the _gradient_stops map and loop over it to + * set them to the new color + */ +void ObjectColorSet::recolorStops(uint32_t old_color, Color new_color) +{ + auto stops_vector = _gradient_stops.find(old_color); + if (stops_vector != _gradient_stops.end()) { + for (auto stop : stops_vector->second) { + stop->setColor(new_color); + } + } +} + +/* + * loop over stops list and populate the _gradient_stops + * it has a different type of access than the _selected_colors map + * so it has a independent map as it stores just a part of the item not the whole item + * like _selected_colors map + */ +void ObjectColorSet::populateStopsMap(SPStop *stop) +{ + while (stop) { + uint32_t color = stop->getColor().toRGBA(); + _gradient_stops[color].push_back(stop); + stop = stop->getNextStop(); + } +} + +/* +* populate _selected_colors map with the color as a string key with vector of objects that +* have the same color and a pair of colors that has the old and new colors of type color +* to ensure easy access on both colors +* +*/ +void ObjectColorSet::populateMap(Color color, SPObject *item, ObjectStyleType type, std::string const &kind) +{ + color.enableOpacity(true); + ColorRef ref {item, kind , type}; + ColorPair pair {color, color}; + uint32_t color_rgba = color.toRGBA(); + auto _selected = _selected_colors.find(color_rgba); + // search if key exist and just push the object to the objects vector + if (_selected != _selected_colors.end()) { + _selected->second.first.push_back(ref); + } else { // create key and push the object and their color ref + colors.push_back(color); + color_wheel_colors_map[color_rgba] = colors.size() - 1; + _selected_colors.emplace(color_rgba, std::make_pair(std::vector{ref}, pair)); + } +} + +void ObjectColorSet::changeObjectColor(ColorRef const &item, Color const &color) +{ + std::string c = color.toString(true); + if (item.kind == "stop") { + return; + } + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property_string(css, item.kind.c_str(), c); + if (!item.item->getId()) { // for handling text content that inheirts its style from parent + auto repr = item.item->parent->getRepr(); + sp_repr_css_change(repr, css, "style"); + } else { + sp_desktop_apply_css_recursive(item.item, css, true); + } + sp_repr_css_attr_unref(css); +} + +void ObjectColorSet::clearData() +{ + colors.clear(); + _gradient_stops.clear(); + _selected_colors.clear(); + color_wheel_colors_map.clear(); +} + +bool ObjectColorSet::setSelectedNewColor(std::vector const &new_colors) +{ + if (new_colors.empty() || new_colors.size() != colors.size()) { + return false; + } + for (auto &[key, value] : _selected_colors) { + int index = color_wheel_colors_map[key]; + value.second->new_color = new_colors[index]; + } + return true; +} + +std::vector &ObjectColorSet::getSelectedItems(uint32_t key_color) +{ + static std::vector empty; + if (auto it = _selected_colors.find(key_color); it != _selected_colors.end()) { + return it->second.first; + } + return empty; +} + +int ObjectColorSet::getColorIndex(uint32_t key_color) const +{ + if (auto it = color_wheel_colors_map.find(key_color); it != color_wheel_colors_map.end()) { + return it->second; + } + return -1; +} + +std::optional ObjectColorSet::getColor(int index) const +{ + if (index < 0 || index >= colors.size()) { + return {}; + } + + return colors[index]; +} + +bool ObjectColorSet::applyNewColorToSelection(uint32_t key_color, Color const &new_color) +{ + std::vector &items = getSelectedItems(key_color); + if (items.empty()) { + return false; + } + + for (auto const &item : items) { + changeObjectColor(item, new_color); + } + recolorStops(key_color, new_color); + return true; +} + +void ObjectColorSet::setSelectedNewColor(uint32_t key_color, Color const &new_color) +{ + if (auto it = _selected_colors.find(key_color); it != _selected_colors.end()) { + it->second.second->new_color = new_color; + } +} + +std::optional ObjectColorSet::getSelectedNewColor(uint32_t key_color) const +{ + auto _selected = _selected_colors.find(key_color); + if (_selected != _selected_colors.end()) { + return _selected->second.second->new_color; + } + return {}; +} + +namespace { + +class ColorsExtractor +{ +public: + explicit ColorsExtractor(ObjectColorSet &m) + : manager{m} + {} + + void collectColors(std::vector objects, ObjectStyleType type = ObjectStyleType::None); + +private: + ObjectColorSet &manager; + + void extractGradientStops(SPObject *object, bool isFill); + void extractMeshStops(std::vector> &mesh_nodes, SPObject *object, ObjectStyleType type); + void extractObjectColors(SPObject *object, ObjectStyleType type = ObjectStyleType::None); + void extractObjectStyle(SPObject *object, ObjectStyleType type = ObjectStyleType::None , SPUse *use = nullptr); + void extractPatternColors(SPPattern *pattern); + void extractMarkerColors(Glib::ustring const &marker, SPObject *object); +}; + +/* + * loops over the vector of objects , firstly try to dynamically cast the spobject to spitem + * if it is casted check if it is a mask or not if mask extract the spobjects + * from it push into vector of spobjects then call collectColors recursivley with this vector + * after this it calls extractObjectColors on the object it self + */ +void ColorsExtractor::collectColors(std::vector objects, ObjectStyleType type) +{ + for (auto object : objects) { + if (auto item = cast(object)) { + if (auto mask = cast(item->getMaskObject())) { + std::vector children_vec; + for (auto &child : mask->children) { + children_vec.push_back(&child); + } + collectColors(children_vec, ObjectStyleType::Mask); + } + if (auto text = cast(item)) { // handle text objects color collection by collecting the colors of its tspans children + if (auto tspan = cast(&text->children.front())) { + std::vector children_vec; + bool noid = true; + for (auto &child : tspan->children) { + children_vec.push_back(&child); + } + collectColors(children_vec, type); + continue; + } + } + } + extractObjectColors(object, type); + } +} + +/* + * checks if object is an spgroup if it is loop over group's children + * call extractObjectColors recursivley on group's children + * if it is not group call extractObjectStyle + */ +void ColorsExtractor::extractObjectColors(SPObject *object, ObjectStyleType type) +{ + if (auto group = cast(object)) { + for (auto &child : group->children) { + extractObjectColors(&child, type); + } + } else if (auto use = cast(object)) { + extractObjectStyle(use->child, type, use); + } else if (object) { + extractObjectStyle(object, type); + } +} + +/* + * firstly extract the objects markers value which has 3 markers per object (optional) + * check for fill types (flat fill, pattern fill, gradient fill) to populate the _selected_colors map + * do same for stroke types + */ +void ColorsExtractor::extractObjectStyle(SPObject *object, ObjectStyleType type, SPUse *use) +{ + // check object style + if (!object || !object->style) { + return; + } + SPStyle *style = object->style; + extractMarkerColors(style->marker_start.get_value(), object); + extractMarkerColors(style->marker_mid.get_value(), object); + extractMarkerColors(style->marker_end.get_value(), object); + + // get flat fills + if (style->fill.isColor()) { + auto color = style->fill.getColor(); + ObjectStyleType fill_type = type == ObjectStyleType::None ? ObjectStyleType::Fill : type; + manager.populateMap(color, use ? use : object, fill_type, "fill"); + + } else if (style->fill.isPaintserver()) { + // paint server can be pattern or gradient + // get gradient stops strokes + auto ps = style->getFillPaintServer(); + if (auto pattern = cast(ps)) { + extractPatternColors(pattern); + } + extractGradientStops(object, true); + } + + if (style->stroke.isColor()) { + auto color = style->stroke.getColor(); + ObjectStyleType stroke_type = type == ObjectStyleType::None ? ObjectStyleType::Stroke : type; + manager.populateMap(color, use ? use : object, stroke_type, "stroke"); + } else if (style->stroke.isPaintserver()) { + // get gradient stops strokes + auto ps = style->getStrokePaintServer(); + if (auto pattern = cast(ps)) { + extractPatternColors(pattern); + } + extractGradientStops(object, false); + } +} + +/* + * check if paint server is spgradient then check if it has patches for extracting mesh gradient + * if it is mesh get its node array and pass it to extractMeshStops + * if not mesh we firstly fork the gradient so we unlink its shared stops with other similar gradients + * so change in selected one doesn't affect the unselected similar one (has same stops colors) + * then call populateStopsMap to save stops refrencess + * then call populateMap to save the spgradient object as a whole to have gradients colors in + * the color list + */ +void ColorsExtractor::extractGradientStops(SPObject *object, bool isFill) +{ + auto paint_server = isFill ? object->style->getFillPaintServer() : object->style->getStrokePaintServer(); + if (paint_server && cast(paint_server)) { + auto gradient = cast(paint_server); + if (!gradient) { + return; + } + if (auto vectorGradient = gradient->getVector()) { + if (vectorGradient->hasPatches()) { + vectorGradient->ensureArray(); + std::unique_ptr nodeArray; + if (auto mesh = cast(gradient)) { + nodeArray = std::make_unique(mesh); + extractMeshStops(nodeArray->nodes, object,ObjectStyleType::Mesh); + } + + } else { + gradient = sp_gradient_get_forked_vector_if_necessary(gradient, true); + if (!gradient) { + return; + } + gradient->ensureVector(); + manager.populateStopsMap(gradient->getFirstStop()); + } + } + bool is_swatch = gradient->getVector()->isSwatch(); + ObjectStyleType type; + if (is_swatch) { + type = ObjectStyleType::Swatch; + } else if (is(gradient)) { + type = ObjectStyleType::Linear; + } else if (is(gradient)) { + type = ObjectStyleType::Radial; + } + for (auto stop : gradient->getGradientVector().stops) { + if (stop.color.has_value()) { + manager.populateMap(stop.color.value(), object, type, "stop"); + } + } + } +} + +/* + * mesh_nodes is a vector of vector of stops so we loop over it normally + * call populateStopsMap and populateMap to populate both maps + */ +void ColorsExtractor::extractMeshStops(std::vector> &mesh_nodes, SPObject *item, ObjectStyleType type) +{ + for (auto const &nodes : mesh_nodes) { + for (auto const &node : nodes) { + manager.populateStopsMap(node->stop); + if (node->color.has_value()) { + manager.populateMap(node->color.value(), item, type , "stop"); + } + } + } +} + +/* + * get root pattern then loop over its children and do the whole extraction process by calling + * extractObjectColors to check for spgroups in pattern children + */ +void ColorsExtractor::extractPatternColors(SPPattern *pattern) +{ + auto root = pattern->rootPattern(); + for (auto &child : root->children) { + extractObjectColors(&child, ObjectStyleType::Pattern); + } +} + +/* +* extract marker id from marker to get it by herf from the xml tree +* then try to cast the result to spmarker +* loop over the spmarker children and do the extraction process on every child +* by calling extractObjectColors +*/ +void ColorsExtractor::extractMarkerColors(Glib::ustring const &marker, SPObject *object) +{ + if (marker.size() >= 5 && object->document) { + std::string marker_id = marker.substr(4, marker.size() - 5); + auto m = object->document->getObjectByHref(marker_id); + if (!m) { + return; + } + if (auto marker_obj = cast(m)) { + for (auto child : marker_obj->item_list()) { + extractObjectColors(child, ObjectStyleType::Marker); + } + } + } +} + +} // namespace + +ObjectColorSet collect_colours(std::vector const &objects, ObjectStyleType type) +{ + ObjectColorSet result; + ColorsExtractor(result).collectColors(objects, type); + return result; +} + +} // namespace Inkscape diff --git a/src/object-colors.h b/src/object-colors.h new file mode 100644 index 0000000000..307df81160 --- /dev/null +++ b/src/object-colors.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * This file is for the logic behind the RecolorArt widget. + */ +/* + * Authors: + * Fatma Omara + * + * Copyright (C) 2025 authors + */ + +#ifndef INKSCAPE_OBJECT_COLORS_H +#define INKSCAPE_OBJECT_COLORS_H + +#include +#include "colors/color.h" + +class SPObject; +class SPStop; + +namespace Inkscape { + +enum class ObjectStyleType +{ + None, + Fill, + Stroke, + Pattern, + Swatch, + Linear, + Radial, + Mesh, + Mask, + Marker, +}; + +struct ColorRef +{ + SPObject *item; + std::string kind; + ObjectStyleType type; +}; + +struct ColorPair +{ + Colors::Color old_color; + Colors::Color new_color; +}; + +class ObjectColorSet +{ +public: + using SelectedColorsMap = std::unordered_map, std::optional>>; + + void populateMap(Colors::Color color, SPObject *style, ObjectStyleType type, std::string const &kind); + void revertToOriginalColors(bool is_reset_clicked = false); + void convertToRecoloredColors(); + void populateStopsMap(SPStop *stop); + void recolorStops(uint32_t old_color, Colors::Color new_color); + void changeObjectColor(ColorRef const &item, Colors::Color const &color); + void changeOpacity(bool change_opacity = false , uint32_t color= 0 ,bool is_preview = true); + void setSelectedNewColor(uint32_t key_color, Colors::Color const &new_color); + std::optional getSelectedNewColor(uint32_t key_color) const; + bool isGradientStopsEmpty() const { return _gradient_stops.empty(); } + bool isColorsEmpty() const { return colors.empty(); } + void clearData(); + bool setSelectedNewColor(std::vector const &new_colors); + uint32_t getFirstKey() const { return _selected_colors.begin()->first; } + std::vector &getSelectedItems(uint32_t key_color); + int getColorIndex(uint32_t key_color) const; + std::vector &getColors() { return colors; } + std::optionalgetColor(int index) const; + SelectedColorsMap const &getSelectedColorsMap() const { return _selected_colors; } + bool applyNewColorToSelection(uint32_t key_color, Colors::Color const &new_color); + +private: + SelectedColorsMap _selected_colors; + std::unordered_map> _gradient_stops; + std::vector colors; + std::unordered_map color_wheel_colors_map; +}; + +/// Extract the colors from a list of objects. +ObjectColorSet collect_colours(std::vector const &objects, ObjectStyleType type = ObjectStyleType::None); + +} // namespace Inkscape + +#endif // INKSCAPE_OBJECT_COLORS_H diff --git a/src/selcue.cpp b/src/selcue.cpp index d52bbb8f69..b3ee8bcc3a 100644 --- a/src/selcue.cpp +++ b/src/selcue.cpp @@ -102,7 +102,7 @@ void SelCue::_updateItemBboxes(gint mode, int prefs_bbox) } else if (auto rect = dynamic_cast(canvas_item)) { rect->set_rect(*b); } - canvas_item->set_visible(true); + canvas_item->set_visible(_bboxes_visible); } else { // no bbox canvas_item->set_visible(false); } @@ -150,7 +150,7 @@ void SelCue::_newItemBboxes() if (canvas_item) { canvas_item->set_pickable(false); canvas_item->lower_to_bottom(); // Just low enough to not get in the way of other draggable knots. - canvas_item->set_visible(true); + canvas_item->set_visible(_bboxes_visible); _item_bboxes.emplace_back(std::move(canvas_item)); } } @@ -219,6 +219,12 @@ void SelCue::_boundingBoxPrefsChanged(int prefs_bbox) _updateItemBboxes(mode, prefs_bbox); } +void SelCue::setBboxesVisible(bool visible) +{ + _bboxes_visible = visible; + _updateItemBboxes(); +} + } // namespace Inkscape /* diff --git a/src/selcue.h b/src/selcue.h index 0cb55b486b..246b8c1aa7 100644 --- a/src/selcue.h +++ b/src/selcue.h @@ -43,6 +43,8 @@ public: BBOX }; + void setBboxesVisible(bool visible); + private: class BoundingBoxPrefsObserver: public Preferences::Observer { @@ -65,6 +67,7 @@ private: void _newTextBaselines(); void _boundingBoxPrefsChanged(int prefs_bbox); + bool _bboxes_visible = true; SPDesktop *_desktop; Selection *_selection; sigc::connection _sel_changed_connection; diff --git a/src/seltrans.h b/src/seltrans.h index 64695430fe..2b85a2f621 100644 --- a/src/seltrans.h +++ b/src/seltrans.h @@ -97,6 +97,7 @@ public: } void getNextClosestPoint(bool reverse); + SelCue &getSelCue() { return _selcue; } private: class BoundingBoxPrefsObserver: public Preferences::Observer diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 116cf6192f..0269f20d91 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -301,10 +301,11 @@ set(ui_SRC widget/widget-vfuncs-class-init.cpp widget/xml-treeview.cpp widget/property-utils.cpp - + widget/recolor-art.cpp + widget/multi-marker-color-plate.cpp + widget/recolor-art-manager.cpp view/svg-view-widget.cpp - # ------- # Headers builder-utils.h @@ -631,7 +632,10 @@ set(ui_SRC widget/widget-vfuncs-class-init.h widget/xml-treeview.h widget/property-utils.h - + widget/recolor-art.h + widget/multi-marker-color-plate.h + widget/recolor-art-manager.h + view/svg-view-widget.h ) diff --git a/src/ui/tools/select-tool.cpp b/src/ui/tools/select-tool.cpp index d83c0cf137..8b4690ebfb 100644 --- a/src/ui/tools/select-tool.cpp +++ b/src/ui/tools/select-tool.cpp @@ -1046,6 +1046,11 @@ std::pair SelectTool::get_default_rubberba return {mode, handle}; } +void SelectTool::onHideSelectionChanged(bool hide) +{ + _seltrans->getSelCue().setBboxesVisible(!hide); +} + } // namespace Inkscape::UI::Tools /* diff --git a/src/ui/tools/select-tool.h b/src/ui/tools/select-tool.h index 7880e351e1..1da9ad25f7 100644 --- a/src/ui/tools/select-tool.h +++ b/src/ui/tools/select-tool.h @@ -61,6 +61,7 @@ private: bool _force_dragging = false; std::string _default_cursor; + void onHideSelectionChanged(bool hide) override; }; } // namespace Inkscape::UI::Tools diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp index 991a436fcb..2d15d29867 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -114,6 +114,8 @@ ToolBase::ToolBase(SPDesktop *desktop, std::string &&prefs_path, std::string &&c sp_event_context_read(this, "changelayer"); sp_event_context_read(this, "changepage"); + + _hide_selection_connection = desktop->connectHideSelectionBoxes(sigc::mem_fun(*this, &ToolBase::onHideSelectionChanged)); } ToolBase::~ToolBase() diff --git a/src/ui/tools/tool-base.h b/src/ui/tools/tool-base.h index e66df77882..207c39abf7 100644 --- a/src/ui/tools/tool-base.h +++ b/src/ui/tools/tool-base.h @@ -152,6 +152,9 @@ protected: EventType::BUTTON_PRESS); void ungrabCanvasEvents(); + sigc::scoped_connection _hide_selection_connection; + virtual void onHideSelectionChanged(bool hide) {}; + private: enum Panning { diff --git a/src/ui/widget/color-notebook.cpp b/src/ui/widget/color-notebook.cpp index 39117b6506..b2ee1cbb6c 100644 --- a/src/ui/widget/color-notebook.cpp +++ b/src/ui/widget/color-notebook.cpp @@ -126,7 +126,7 @@ void ColorNotebook::_initUI() _book->set_margin_start(2); _book->set_margin_end(2); _book->set_hexpand(); - _book->set_vexpand(); + _book->set_vexpand(false); attach(*_book, 0, row, 2, 1); // restore the last active page @@ -193,7 +193,7 @@ void ColorNotebook::_initUI() gtk_widget_set_margin_start(rgbabox, XPAD); gtk_widget_set_margin_end(rgbabox, XPAD); - gtk_widget_set_margin_top(rgbabox, YPAD); + gtk_widget_set_margin_top(rgbabox, 8); gtk_widget_set_margin_bottom(rgbabox, YPAD); attach(*Glib::wrap(rgbabox), 0, row, 2, 1); @@ -311,6 +311,14 @@ void ColorNotebook::_addPageForSpace(std::shared_ptr sp _visibility_observers.emplace_back(std::move(obs)); } +void ColorNotebook::setCurrentColor(std::shared_ptr colors) +{ + auto visible_child = _book->get_visible_child(); + if (auto current_page = dynamic_cast(visible_child)) { + current_page->setCurrentColor(colors); + } +} + } // namespace Inkscape::UI::Widget /* diff --git a/src/ui/widget/color-notebook.h b/src/ui/widget/color-notebook.h index 637379c716..d0d9e9cc97 100644 --- a/src/ui/widget/color-notebook.h +++ b/src/ui/widget/color-notebook.h @@ -39,6 +39,7 @@ public: ~ColorNotebook() override; void set_label(const Glib::ustring& label); + void setCurrentColor(std::shared_ptr colors); protected: void _initUI(); diff --git a/src/ui/widget/color-page.h b/src/ui/widget/color-page.h index eb7efc7ffc..26a28a54a8 100644 --- a/src/ui/widget/color-page.h +++ b/src/ui/widget/color-page.h @@ -22,6 +22,7 @@ #include "color-slider.h" #include "color-preview.h" #include "ui/widget/color-slider.h" +#include "ui/widget/ink-color-wheel.h" using namespace Inkscape::Colors; @@ -55,6 +56,13 @@ public: void attach_page(Glib::RefPtr first_column, Glib::RefPtr last_column); void detach_page(Glib::RefPtr first_column, Glib::RefPtr last_column); + void setCurrentColor(std::shared_ptr color) + { + if (_color_wheel) { + _color_wheel->set_color(color->get().value()); + } + } + protected: std::shared_ptr _space; std::shared_ptr _selected_colors; diff --git a/src/ui/widget/color-preview.cpp b/src/ui/widget/color-preview.cpp index 4854cbbad6..ecf80c0255 100644 --- a/src/ui/widget/color-preview.cpp +++ b/src/ui/widget/color-preview.cpp @@ -49,8 +49,8 @@ void ColorPreview::setRgba32(std::uint32_t const rgba) { _gradient.clear(); queue_draw(); } - -void ColorPreview::setPattern(Cairo::RefPtr pattern) { +void ColorPreview::setPattern(Cairo::RefPtr pattern) +{ if (_pattern == pattern) return; _pattern = pattern; diff --git a/src/ui/widget/fill-style.cpp b/src/ui/widget/fill-style.cpp index cc03b511fc..d02c14aaf4 100644 --- a/src/ui/widget/fill-style.cpp +++ b/src/ui/widget/fill-style.cpp @@ -129,6 +129,9 @@ void FillNStroke::setDesktop(SPDesktop *desktop) stop_selected_connection.disconnect(); } _desktop = desktop; + if (_psel) { + _psel->setDesktop(desktop); + } if (desktop && desktop->getSelection()) { subselChangedConn = desktop->connect_text_cursor_moved([this] (Inkscape::UI::Tools::TextTool *) { performUpdate(); diff --git a/src/ui/widget/gradient-editor.cpp b/src/ui/widget/gradient-editor.cpp index 3aa4c6d1c1..b7b2c67ff4 100644 --- a/src/ui/widget/gradient-editor.cpp +++ b/src/ui/widget/gradient-editor.cpp @@ -66,13 +66,12 @@ GradientEditor::GradientEditor(const char* prefs, Space::Type space, bool show_t _turn_gradient(get_widget(_builder, "turnBtn")), _angle_adj(get_object(_builder, "adjustmentAngle")), _angle_btn(get_widget(_builder, "angle")), + _main_box(get_widget(_builder, "main-box")), _color_picker(ColorPickerPanel::create(space, get_plate_type_preference(prefs, ColorPickerPanel::None), _colors)), _linear_btn(get_widget(_builder, "type-linear")), _radial_btn(get_widget(_builder, "type-radial")), _repeat_mode_btn(get_widget(_builder, "repeat-mode")) { - auto& main_box(get_widget(_builder, "main-box")); - // gradient type buttons _linear_btn.set_active(); _linear_btn.signal_clicked().connect([this]{ fire_change_type(true); }); @@ -114,11 +113,11 @@ GradientEditor::GradientEditor(const char* prefs, Space::Type space, bool show_t expander->property_expanded().signal_changed().connect([this, expander]{ _color_picker->set_plate_type(expander->get_expanded() ? ColorPickerPanel::Circle : ColorPickerPanel::None); }); - main_box.append(*expander); + _main_box.append(*expander); } // add color selector - main_box.append(*_color_picker); + _main_box.append(*_color_picker); // gradient library in a popup get_widget(_builder, "libraryPopover").set_child(*_selector); @@ -169,7 +168,7 @@ GradientEditor::GradientEditor(const char* prefs, Space::Type space, bool show_t _color_picker->get_last_column_size()->add_widget(get_widget(_builder, "offset-box")); _color_picker->get_last_column_size()->add_widget(get_widget(_builder, "angle-box")); - append(main_box); + append(_main_box); } void GradientEditor::set_stop_color(Inkscape::Colors::Color const &color) diff --git a/src/ui/widget/gradient-editor.h b/src/ui/widget/gradient-editor.h index 7123f91a53..f457e41a44 100644 --- a/src/ui/widget/gradient-editor.h +++ b/src/ui/widget/gradient-editor.h @@ -70,6 +70,7 @@ public: ColorPickerPanel::PlateType get_color_picker_plate() const; SPGradientType get_type() const; ColorPickerPanel& get_picker() { return *_color_picker; } + Gtk::Box &getColorBox() { return _main_box; } private: void set_gradient(SPGradient* gradient); @@ -102,6 +103,7 @@ private: InkSpinButton& _offset_btn; InkSpinButton& _angle_btn; int _current_stop_index = 0; + Gtk::Box &_main_box; SPGradient* _gradient = nullptr; SPDocument* _document = nullptr; OperationBlocker _update; diff --git a/src/ui/widget/ink-color-wheel.cpp b/src/ui/widget/ink-color-wheel.cpp index 8561f92cab..4ae41d7380 100644 --- a/src/ui/widget/ink-color-wheel.cpp +++ b/src/ui/widget/ink-color-wheel.cpp @@ -27,6 +27,7 @@ #include "ui/widget/bin.h" #include "util/drawing-utils.h" #include "util/theme-utils.h" +#include "ink-color-wheel.h" using namespace Inkscape::Colors; using Inkscape::Colors::Space::Luv; @@ -133,14 +134,15 @@ ColorWheelBase::ColorWheelBase(BaseObjectType* cobject, const Glib::RefPtr colors) +{ + _values_vector = std::move(colors); + for (auto &col : _values_vector) { + col.convert(Colors::Space::Type::HSV); + } + _source_wheel.reset(); + _markers_points.clear(); + _markers_points.resize(_values_vector.size()); + _active_index = _values_vector.empty() ? -1 : 0; + color_changed(); +} + +/** + * takes cairo context , color value and index of the color + * get the center of the colorwheel by dividing width and height by 2 + * get the center of the marker by passing the index to get_marker_point function + * then choose the color of the marker (black or white) based on its luminance + * then start start drawing with cairo , if the index == hover index just make the radius bigger + * then if the marker has foucs add focus dash to it + */ +void MultiMarkerWheel::_draw_marker(Cairo::RefPtr const &cr, Colors::Color const &value, int index) +{ + auto const [width, height] = *_cache_size; + auto const cx = width / 2.0; + auto const cy = height / 2.0; + auto const &[mx, my] = get_marker_point(index); + + _draw_line_to_marker(cr,mx,my,cx,cy,value,index); + + auto color_on_wheel = Color(Type::HSV, {value[0], 1.0, 1.0}); + double a = luminance(color_on_wheel) < 0.5 ? 1.0 : 0.0; + if (index == _active_index) { + cr->set_source_rgb(0.2588, 0.5216, 0.9255); + } else { + cr->set_source_rgb(a, a, a); + } + cr->set_dash(std::valarray(), 0); + cr->begin_new_path(); + if (index == _hover_index) { + cr->arc(mx, my, marker_radius+2, 0, 2 * M_PI); + } else { + cr->arc(mx, my, marker_radius, 0, 2 * M_PI); + } + cr->stroke(); + + // Draw focus + if (drawing_area_has_focus()) { + // The focus_dash width & alpha(foreground_color) are from GTK3 Adwaita. + if (index == _active_index) { + cr->set_dash(focus_dash, 0); + cr->set_line_width(1.0); + cr->set_source_rgb(1 - a, 1 - a, 1 - a); + cr->begin_new_path(); + cr->arc(mx, my, marker_radius + focus_padding, 0, 2 * M_PI); + } + + cr->stroke(); + } +} + +/** + * try to get marker index from the input position (x,y) + * by getting the distance between the marker center and the point (x,y) + * if it is less than marker_radius + marker_click_tolerance it means that + * the point is inside the marker area then return its index + * if not found return -1 + */ +int MultiMarkerWheel::_get_marker_index(Geom::Point const &p) +{ + for (int i = 0; i < _values_vector.size(); i++) { + auto m = get_marker_point(i); + if (Geom::distance(p, m) <= marker_radius + marker_click_tolerance) { + return i; + } + } + return -1; +} + +/** + * If hue lock is enabled, this function calculates how far each marker's hue + * is from the active marker's hue. + * + * Because hue is a circle (0.0 and 1.0 are the same color), the raw difference + * can sometimes look too big (e.g. 0.9 - 0.1 = 0.8), even though the real + * distance around the circle is much smaller (0.2). + * + * To fix this, the delta is adjusted: + * - If delta > 0.5 → subtract 1.0 + * - If delta < -0.5 → add 1.0 + * + * This makes sure the difference is always the shortest distance on the color wheel, + * inside the range [-0.5, +0.5]. + */ +void MultiMarkerWheel::_update_hue_lock_positions() +{ + if (!_hue_lock) { + return; + } + + std::vector delta_angles; + auto active_hue = _values_vector[_active_index][0]; + for (int i = 0; i < _values_vector.size(); i++) { + if (i == _active_index) { + delta_angles.push_back(0.0); + continue; + } + auto delta_hue = _values_vector[i][0] - active_hue; + if (delta_hue > 0.5) delta_hue -= 1.0; + if (delta_hue < -0.5) delta_hue += 1.0; + delta_angles.push_back(delta_hue); + } + + _relative_hue_angles = delta_angles; +} + +/** + * function to draw line to the begining of the marker by calculating the distance from the wheel center + * to marker center + * normalize the differences between centers by dividing them by the length of the line + * to be a unit vector between [-1,1] + * then calculate the end points ty,tx by subtracting the marker radius multiplied by the direction of the vector + * from the marker center , calculate the luminance of the line + * move the cairo context tot the center of the colorwheel by converting the polar coordinates to cartisain ones + * then draw the line to point (tx,ty) + */ +void MultiMarkerWheel::_draw_line_to_marker(Cairo::RefPtr const &cr, double mx, double my, double cx, + double cy, Colors::Color const &value, int index) +{ + auto const &[r_min, r_max] = get_radii(); + auto color_on_wheel = Color(Type::HSV, {value[0], 1.0, 1.0}); + double dy = my-cy; + double dx = mx-cx; + double len = std::sqrt(dx*dx+dy*dy); + if (len > 1e-5) { + dx /= len; + dy /= len; + } + double mr = index == _hover_index ? marker_radius+2 : marker_radius; // bigger radius for on hover effect + double tx = mx - dx * mr; + double ty = my - dy * mr; + double l = luminance(color_on_wheel) < 0.5 ? 1.0 : 0.0; + cr->save(); + cr->set_source_rgb(l, l, l); + cr->move_to(cx + cos(value[0] * M_PI * 2.0) * r_min, + cy - sin(value[0] * M_PI * 2.0) * r_min); // x = r*cos(angel) , y = r*sin(angel) + // adding cx and subtracting cy to start from wheel center not the origin (0,0) + cr->line_to(tx,ty); + if (index != _active_index && !_hue_lock) { + cr->set_dash(focus_dash, 0); + cr->set_line_width(1.0); + } else if (!_hue_lock) { + auto const dash = std::vector{3.0}; // wider dashes for focused line + cr->set_dash(dash,0); + cr->set_line_width(2.0); + } else { + cr->set_dash(std::valarray(), 0); + if (index == _active_index) { + cr->set_line_width(3.0); + } + } + cr->stroke(); + cr->restore(); +} + +/** + * draw the colorwheel pixel by pixel + */ +void MultiMarkerWheel::update_wheel_source() +{ + if (_radii && _source_wheel) { + return; + } + + auto const [width, height] = *_cache_size; + auto const cx = width / 2.0; + auto const cy = height / 2.0; + + auto const stride = Cairo::ImageSurface::format_stride_for_width(Cairo::Surface::Format::RGB24, width); + _source_wheel.reset(); + _buffer_wheel.resize(height * stride / 4); + + auto const &[r_min, r_max] = get_radii(); + double r2_max = (r_max + 2) * (r_max + 2); // Must expand a bit to avoid edge effects. + double r2_min = (r_min - 2) * (r_min - 2); // Must shrink a bit to avoid edge effects. + + for (int i = 0; i < height; ++i) { + auto p = _buffer_wheel.data() + i * width; + double dy = (cy - i); + for (int j = 0; j < width; ++j) { + double dx = (j - cx); + double r2 = dx * dx + dy * dy; + if (r2 < r2_min || r2 > r2_max) { + *p++ = 0; // Save calculation time. + } else { + double angle = atan2(dy, dx); + if (angle < 0.0) { + angle += 2.0 * M_PI; + } + double hue = angle / (2.0 * M_PI); + + double saturation = sqrt(r2)/r_max; + saturation = std::clamp(saturation,0.0,1.0); + // double value = 1.0 - ((dy+(height/2.0))/height); + // value = std::clamp(value,0.0,1.0); + + *p++ = Color(Type::HSV, {hue, saturation,lightness}).toARGB(); + } + } + } + + auto const data = reinterpret_cast(_buffer_wheel.data()); + _source_wheel = Cairo::ImageSurface::create(data, Cairo::Surface::Format::RGB24, width, height, stride); +} + +/** + * takes index of the requested changed color and the new color + * change it in the _values_vector and reset its marker and emit color changed signal to update the widget + * and return true if succeeded + * used to sync wheel's colors if the color chnaged from the colorlist + */ +bool MultiMarkerWheel::changeColor(int index, Colors::Color const &color) +{ + if (index < 0 || index >= _values_vector.size()) { + return false; + } + + if (_values_vector[index].set(color, true)) { + _markers_points[index].reset(); + color_changed(); + return true; + } + + return false; +} + +/** + * set lightness for all colors in the wheel when hue lock is on + * if it is off just changed lightness for the active color + */ +void MultiMarkerWheel::setLightness(double value) +{ + lightness = value / 100.0; + _source_wheel.reset(); + if (_hue_lock) { + for (size_t i = 0; i < _values_vector.size(); i++) { + _values_vector[i].set(2, lightness); + if (i < _markers_points.size()) { + _markers_points[i].reset(); + } + } + color_changed(); + } else { + int index = getActiveIndex(); + if (index > -1) { + _values_vector[index].set(2, lightness); + _markers_points[index].reset(); + color_changed(); + } + } +} + +/** + * set saturation for all colors in the wheel when hue lock is on + * if it is off just change saturation for the active color + */ +void MultiMarkerWheel::setSaturation(double value) +{ + saturation = value / 100.0; + if (_hue_lock) { + for (size_t i = 0; i < _values_vector.size(); i++) { + _values_vector[i].set(1, saturation); + if (i < _markers_points.size()) { + _markers_points[i].reset(); + } + } + color_changed(); + } else { + int index = getActiveIndex(); + if (index >- 1) { + _values_vector[index].set(1, saturation); + _markers_points[index].reset(); + color_changed(); + } + } +} + +void MultiMarkerWheel::on_drawing_area_size(int width, int height, int baseline) +{ + auto const size = Geom::IntPoint{width, height}; + if (size == _cache_size) { + return; + } + _cache_size = size; + _radii.reset(); + _source_wheel.reset(); +} + +/** + * main function for drawing the whole wheel and markers and lines + */ +void MultiMarkerWheel::on_drawing_area_draw(Cairo::RefPtr const &cr, int, int) +{ + + auto const [width, height] = *_cache_size; + auto const cx = width / 2.0; + auto const cy = height / 2.0; + + cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL); + + // Update caches + update_wheel_source(); + auto const &[r_min, r_max] = get_radii(); + + // Paint with ring surface, clipping to ring. + cr->save(); + cr->set_source(_source_wheel, 0, 0); + cr->set_line_width(r_max - r_min); + cr->begin_new_path(); + cr->arc(cx, cy, (r_max + r_min) / 2.0, 0, 2.0 * M_PI); + cr->stroke(); + cr->restore(); + // Paint line to markers and markers + if (_markers_points.size() != _values_vector.size()) { + _markers_points.resize(_values_vector.size()); + } + + for (int i = 0; i < _values_vector.size(); i++) { + _draw_marker(cr, _values_vector[i], i); + } +} + +std::optional MultiMarkerWheel::focus(Gtk::DirectionType const direction) +{ + // Any focus change must update focus indicators (add or remove). + queue_drawing_area_draw(); + + // In forward direction, focus passes from no focus to ring focus to triangle + // focus to no focus. + if (!drawing_area_has_focus()) { + _focus_on_wheel = (direction == Gtk::DirectionType::TAB_FORWARD); + focus_drawing_area(); + return true; + } + + // Already have focus + bool keep_focus = true; + + switch (direction) { + case Gtk::DirectionType::TAB_BACKWARD: + if (!_focus_on_wheel) { + _focus_on_wheel = true; + } else { + keep_focus = false; + } + break; + + case Gtk::DirectionType::TAB_FORWARD: + if (_focus_on_wheel) { + _focus_on_wheel = false; + } else { + keep_focus = false; + } + } + + return keep_focus; +} + +/** + * checks whether the point is inside the wheel or not + * by checking if the distance is less than the wheel radius + */ +bool MultiMarkerWheel::_is_in_wheel(double x, double y) +{ + // std::cout<<"x: "<= 0) { + _active_index = index; + } + _update_hue_lock_positions(); + if (_active_index >= 0 && _active_index < _values_vector.size()) { + _update_wheel_color(x, y, _active_index); + } + return Gtk::EventSequenceState::CLAIMED; + } + + return Gtk::EventSequenceState::NONE; +} + +Gtk::EventSequenceState MultiMarkerWheel::on_click_released(int /*n_press*/, double /*x*/, double /*y*/) +{ + _mode = DragMode::NONE; + _adjusting = false; + return Gtk::EventSequenceState::CLAIMED; +} + +/** + * if not adusting a marker + * it detects if the point is on or near some marker gets its index and emits _signal_color_hovered + * so the marker gets redrawed with a bigger radius and call for any action related to hover + * (e.g. highlighting objects that has the hovered marker color) + * -1 _hover_index to cancel the hover effect when start moving the marker + * also checks for the _hue_lock to change reset of the markers accordingly if it is on + */ +void MultiMarkerWheel::on_motion(Gtk::EventControllerMotion const &motion, double x, double y) +{ + if (!_adjusting) { + int hover_index = _get_marker_index({x, y}); + _signal_color_hovered.emit(); + if (_hover_index != hover_index) { + _hover_index = hover_index; + if (hover_index >= 0 && hover_index < _values_vector.size()) { + queue_drawing_area_draw(); + } + } + return; + } + auto state = motion.get_current_event_state(); + if (!Controller::has_flag(state, Gdk::ModifierType::BUTTON1_MASK)) { + // lost button release event + _mode = DragMode::NONE; + _adjusting = false; + return; + } + + if (_mode == DragMode::HUE || _mode == DragMode::SATURATION_VALUE) { + _hover_index = -1; + _signal_color_hovered.emit(); + if (_active_index >= 0 && _active_index < _values_vector.size()) { + _update_wheel_color(x, y, _active_index); + } + if (_hue_lock && !_relative_hue_angles.empty()) { + bool changed = false; + double hue = _values_vector[_active_index][0]; + for (int i = 0; i < _values_vector.size(); i++) { + if (i != _active_index) { + double new_hue = hue + _relative_hue_angles[i]; + new_hue = fmod(new_hue + 1.0, 1.0); + if (_values_vector[i].set(0, new_hue)) { + _markers_points[i].reset(); + changed = true; + } + } + } + if (changed) { + color_changed(); + } + } + } +} + +/** + * signal handler function that handels keyboard adjustments to the wheel on hue and saturation values + * same as the one in ColorWheelHSL too + */ +bool MultiMarkerWheel::on_key_pressed(unsigned keyval, unsigned /*keycode*/, Gdk::ModifierType state) +{ + static constexpr double delta_hue = 2.0 / MAX_HUE; + static constexpr double delta_sat = 2.0 / MAX_SATURATION; + auto dx = 0.0, dy = 0.0; + + switch (keyval) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + dy = -1.0; + break; + + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + dy = +1.0; + break; + + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + dx = -1.0; + break; + + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + dx = +1.0; + } + + if (dx == 0.0 && dy == 0.0) { + return false; + } + + bool changed = false; + if (_focus_on_wheel) { + changed = _values_vector[_active_index].set(0, _values_vector[_active_index][0] - ((dx != 0 ? dx : dy) * delta_hue)); + changed = _values_vector[_active_index].set(1, _values_vector[_active_index][1] - ((dy != 0 ? dy : dx) * delta_sat)); + } + + _values_vector[_active_index].normalize(); + + if (changed) { + _markers_points[_active_index].reset(); + color_changed(); + } + + return changed; +} + +/** + * that is the same as get radii in the ColorWheelHSL i didn't change any thing even + * though it has only one radius (r_max = r_min) now as a whole circle not a ring but i dare not change a working function + * (and i am lazy too) + */ +MultiMarkerWheel::MinMax const &MultiMarkerWheel::get_radii() +{ + if (_radii) { + return *_radii; + } + + _radii.emplace(); + auto &[r_min, r_max] = *_radii; + auto const [width, height] = *_cache_size; + r_max = std::min(width, height) / 2.0 - 2 * (focus_line_width + focus_padding); + r_min = r_max * (1.0 - _wheel_width); + return *_radii; +} + +/** + * if the marker isn't in _markers_points it calculates the marker position + * by the hue angle and saturation as the distance from the center to the desired color + */ +Geom::Point MultiMarkerWheel::get_marker_point(int index) +{ + if (index < 0 || index >= _values_vector.size()) { + return {}; + } + + if (index >= _markers_points.size()) { + _markers_points.resize(_values_vector.size()); + } + + if (_markers_points[index]) { + return *_markers_points[index]; + } + + auto const [width, height] = *_cache_size; + auto const cx = width / 2.0; + auto const cy = height / 2.0; + auto const&[r_min,r_max] = get_radii(); + double hue = _values_vector[index][0]; + double saturation = _values_vector[index][1]; + double angle = (1.0 - hue) * 2 * M_PI; + _markers_points[index].emplace(); + auto &[mx, my] = *_markers_points[index]; + mx = cx + r_max * saturation * cos(angle); // polar cooordinates to cartesian coordinates calculation + my = cy + r_max * saturation * sin(angle); + return *_markers_points[index]; +} + +MultiMarkerWheel::MultiMarkerWheel() + : Glib::ObjectBase{"MultiMarkerWheel"} + , WidgetVfuncsClassInit{} // All the calculations are based on HSV, not HSL + , ColorWheelBase(Type::HSV, {0.5, 0.2, 0.7, 1}) // redundant values nothing important +{} + +MultiMarkerWheel::MultiMarkerWheel(BaseObjectType *cobject, Glib::RefPtr const &builder) + : ColorWheelBase(cobject, builder, Type::HSV, {0.5, 0.2, 0.7, 1}) // redundant values nothing important +{} + /** * Update the PickerGeometry structure owned by the instance. */ diff --git a/src/ui/widget/ink-color-wheel.h b/src/ui/widget/ink-color-wheel.h index 8169bd86b9..eaa77b5509 100644 --- a/src/ui/widget/ink-color-wheel.h +++ b/src/ui/widget/ink-color-wheel.h @@ -225,6 +225,106 @@ private: int _square_size = 1; }; +/** + * @class MultiMarkerWheel + */ +class MultiMarkerWheel + : public WidgetVfuncsClassInit + , public ColorWheelBase +{ +public: + MultiMarkerWheel(); + MultiMarkerWheel(BaseObjectType *cobject, Glib::RefPtr const &builder); + bool setColor(Colors::Color const &color, bool overrideHue = true, bool emit = true) override; + void setColors(std::vector colors); + Colors::Color getColor() const override + { + if (!_values_vector.empty() && _active_index >= 0 && _active_index < _values_vector.size()) + return _values_vector[_active_index]; + else + return Colors::Color(0x00000000); + } + bool setActiveIndex(int index) + { + if (!_values_vector.empty() && index >= 0 && index < _values_vector.size()) { + _active_index = index; + return true; + } else + return false; + } + int getActiveIndex() + { + if (!_values_vector.empty() && _active_index >= 0 && _active_index < _values_vector.size()) + return _active_index; + else + return -1; + } + int getHoverIndex() + { + if (!_values_vector.empty() && _hover_index >= 0 && _hover_index < _values_vector.size()) + return _hover_index; + else + return -1; + } + bool changeColor(int index, Colors::Color const &color); + sigc::connection connect_color_hovered(sigc::slot slot) { return _signal_color_hovered.connect(std::move(slot)); } + void toggleHueLock(bool locked){_hue_lock = locked ;} + bool getHueLock(){return _hue_lock;} + std::vector const &getColors() const { return _values_vector; } + void setLightness(double value); + void setSaturation(double value); + void redrawOnHueLocked(){queue_drawing_area_draw();} + +private: + void on_drawing_area_size(int width, int height, int baseline) override; + void on_drawing_area_draw(Cairo::RefPtr const &cr, int, int) override; + std::optional focus(Gtk::DirectionType direction) override; + bool _is_in_wheel(double x, double y); + void _update_wheel_color(double x, double y, int index); + void _draw_line_to_marker(Cairo::RefPtr const &cr, double mx, double my, double cx, double cy, Colors::Color const &value, int index); + void _draw_marker(Cairo::RefPtr const &cr, Colors::Color const &value, int index); + int _get_marker_index(Geom::Point const &p); + void _update_hue_lock_positions(); + + enum class DragMode + { + NONE, + HUE, + SATURATION_VALUE + }; + + static constexpr double _wheel_width = 1.0; + DragMode _mode = DragMode::NONE; + bool _focus_on_wheel = true; + + Gtk::EventSequenceState on_click_pressed(Gtk::GestureClick const &controller, int n_press, double x, + double y) final; + Gtk::EventSequenceState on_click_released(int n_press, double x, double y) final; + void on_motion(Gtk::EventControllerMotion const &motion, double x, double y) final; + bool on_key_pressed(unsigned keyval, unsigned keycode, Gdk::ModifierType state) final; + + // caches to speed up drawing + using MinMax = std::array; + std::vector _values_vector; + std::optional _cache_size; + std::optional _radii; + std::optional _marker_point; + std::vector> _markers_points; + std::vector _buffer_wheel; + Cairo::RefPtr _source_wheel; + MinMax const &get_radii(); + Geom::Point get_marker_point(int index); + int _active_index = 0; + int _hover_index = -1; + bool _hue_lock = 0; + std::vector_relative_hue_angles; + static constexpr double marker_click_tolerance = 5.0; + sigc::signal _signal_color_hovered; + double lightness = 1.0; + double saturation = 1.0; + void update_wheel_source(); +}; + } // namespace Inkscape::UI::Widget #endif // INK_COLORWHEEL_HSLUV_H diff --git a/src/ui/widget/marker-combo-box.cpp b/src/ui/widget/marker-combo-box.cpp index f6c9f17e2e..f32611f717 100644 --- a/src/ui/widget/marker-combo-box.cpp +++ b/src/ui/widget/marker-combo-box.cpp @@ -16,7 +16,6 @@ #include "marker-combo-box.h" #include -#include #include #include #include @@ -159,7 +158,8 @@ MarkerComboBox::MarkerComboBox(Glib::ustring id, int l) : _orient_auto(get_widget(_builder, "orient-auto")), _orient_angle(get_widget(_builder, "orient-angle")), _orient_flip_horz(get_widget(_builder, "btn-horz-flip")), - _edit_marker(get_widget(_builder, "edit-marker")) + _edit_marker(get_widget(_builder, "edit-marker")), + _recolorButtonTrigger(Gtk::make_managed()) { set_name("MarkerComboBox"); @@ -345,7 +345,36 @@ MarkerComboBox::MarkerComboBox(Glib::ustring id, int l) : init_combo(); update_scale_link(); update_menu_btn(); - set_visible(true); + + _recolorButtonTrigger->set_label(_("Recolor Marker")); + _recolorButtonTrigger->set_hexpand(true); + _recolorButtonTrigger->set_vexpand(false); + _recolorButtonTrigger->set_size_request(180); + _recolorButtonTrigger->set_halign(Gtk::Align::FILL); + _recolorButtonTrigger->set_valign(Gtk::Align::START); + _recolorButtonTrigger->set_margin_top(8); + + _grid.add_full_row(_recolorButtonTrigger); + _recolorButtonTrigger->signal_clicked().connect([this] { + if (!_recolorManager) { + // Lazy-load the recolour widget and popover. + _recolorManager = &RecolorArtManager::get(); + if (_recolorManager->getPopOver().get_parent()) { + _recolorManager->getPopOver().unparent(); + } + _recolorManager->getPopOver().set_parent(*_recolorButtonTrigger); + _recolorManager->setDesktop(_desktop); + + } else if (_recolorManager->getPopOver().get_parent() != _recolorButtonTrigger) { + // Reparent the popover to this button if necessary. + _recolorManager->getPopOver().unparent(); + _recolorManager->getPopOver().set_parent(*_recolorButtonTrigger); + } + _recolorManager->getPopOver().popup(); + _recolorManager->performMarkerUpdate(get_current()); + }); + + _recolorButtonTrigger->hide(); } void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) { @@ -380,6 +409,8 @@ void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) { _angle_btn.set_sensitive(true); } } + + _recolorButtonTrigger->set_visible(marker); } void MarkerComboBox::update_scale_link() { @@ -479,6 +510,22 @@ Glib::RefPtr MarkerComboBox::get_active() { } } +void MarkerComboBox::setDesktop(SPDesktop *desktop) +{ + if (_desktop == desktop) { + return; + } + + if (_recolorManager) { + _recolorManager->getPopOver().popdown(); + } + + _desktop = desktop; + + if (_recolorManager) { + _recolorManager->setDesktop(_desktop); + } +} void MarkerComboBox::setDocument(SPDocument *document) { if (_document != document) { diff --git a/src/ui/widget/marker-combo-box.h b/src/ui/widget/marker-combo-box.h index 1af86289e3..cadb5c5874 100644 --- a/src/ui/widget/marker-combo-box.h +++ b/src/ui/widget/marker-combo-box.h @@ -18,6 +18,8 @@ #define SEEN_SP_MARKER_COMBO_BOX_H #include +#include +#include #include #include "display/drawing.h" @@ -26,6 +28,9 @@ #include "ink-spin-button.h" #include "snapshot-widget.h" #include "ui/operation-blocker.h" +#include "ui/widget/widget-vfuncs-class-init.h" +#include "ui/widget/recolor-art.h" +#include "ui/widget/recolor-art-manager.h" namespace Gtk { class Builder; @@ -55,7 +60,7 @@ public: MarkerComboBox(Glib::ustring id, int loc); void setDocument(SPDocument *); - + void setDesktop(SPDesktop *desktop); void set_current(SPObject *marker); std::string get_active_marker_uri(); bool in_update() const { return _update.pending(); }; @@ -123,8 +128,30 @@ private: std::unique_ptr _sandbox; InkPropertyGrid _grid; WidgetGroup _widgets; + Gtk::CellRendererPixbuf _image_renderer; + + UI::Widget::RecolorArtManager* _recolorManager = nullptr; + SPDesktop *_desktop = nullptr; + sigc::scoped_connection _selection_changed_connection; + Gtk::Button *_recolorButtonTrigger = nullptr; + + class MarkerColumns : public Gtk::TreeModel::ColumnRecord + { + public: + Gtk::TreeModelColumn label; + Gtk::TreeModelColumn marker; // ustring doesn't work here on windows due to unicode + Gtk::TreeModelColumn stock; + Gtk::TreeModelColumn> pixbuf; + Gtk::TreeModelColumn history; + Gtk::TreeModelColumn separator; + + MarkerColumns() { + add(label); add(stock); add(marker); add(history); add(separator); add(pixbuf); + } + }; + MarkerColumns marker_columns; - void update_ui(SPMarker* marker, bool select); + void update_ui(SPMarker *marker, bool select); void update_widgets_from_marker(SPMarker* marker); void update_store(); Glib::RefPtr add_separator(bool filler); diff --git a/src/ui/widget/multi-marker-color-plate.cpp b/src/ui/widget/multi-marker-color-plate.cpp new file mode 100644 index 0000000000..922c4e3504 --- /dev/null +++ b/src/ui/widget/multi-marker-color-plate.cpp @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "multi-marker-color-plate.h" + +#include +#include +#include +#include +#include + +#include "colors/spaces/base.h" +#include "colors/spaces/enum.h" +#include "ui/icon-names.h" +#include "ui/widget/color-page.h" +#include "ui/widget/color-preview.h" +#include "ui/widget/color-slider.h" +#include "ui/widget/icon-combobox.h" +#include "ui/widget/ink-spin-button.h" + +namespace Inkscape::UI::Widget { + +MultiMarkerColorPlate::MultiMarkerColorPlate(Colors::ColorSet const &colors) + : Gtk::Box(Gtk::Orientation::VERTICAL) + , _specific_colors(std::make_shared(manager->find(Space::Type::HSL), + colors.getAlphaConstraint().value_or(true))) + , _color_wheel(Gtk::make_managed()) + , _hue_lock(*Gtk::make_managed()) + , _lightness_bar(*Gtk::make_managed(adjustment, Gtk::Orientation::HORIZONTAL)) + , _saturation_bar(*Gtk::make_managed(_saturation_adjustment, Gtk::Orientation::HORIZONTAL)) + , _color_wheel_preview(Gtk::make_managed()) + , _grid(Gtk::make_managed()) + , _spaces_combo(Gtk::make_managed()) + , _switcher(Gtk::make_managed()) + , _spaces_stack(Gtk::make_managed()) + , _reset(Gtk::make_managed()) +{ + _specific_colors->set(Color(0xFF0000FF)); + + _switcher->set_stack(*_spaces_stack); + + _spaces_combo->add_css_class("regular"); + _spaces_combo->set_focusable(false); + _spaces_combo->set_tooltip_text(_("Choose style of color selection")); + _spaces_combo->set_hexpand(false); + _spaces_combo->set_halign(Gtk::Align::END); + _spaces_combo->set_margin_top(4); + _spaces_combo->set_margin_bottom(8); + + _spaces_combo->signal_changed().connect([this](int index) { + _specific_colors = _color_sets[index].second; + _spaces_stack->set_visible_child(_color_sets[index].first); + _specific_colors_changed.disconnect(); + _specific_colors_changed = _specific_colors->signal_changed.connect([this]() { + Color new_color = _specific_colors->get().value(); + if (_color_wheel->getActiveIndex() != -1) { + _color_wheel->changeColor(_color_wheel->getActiveIndex(), new_color); + _color_wheel_preview->setRgba32(new_color.toRGBA()); + } + }); + }); + + int index = 0; + for (auto &space : Colors::Manager::get().spaces(Space::Traits::Picker)) { + _createSlidersForSpace(space, _specific_colors, index); + _addPageForSpace(space, index++); + } + + _lightness_icon->set_from_icon_name(INKSCAPE_ICON("lightness")); + _lightness_icon->set_tooltip_text(_("change saturation for all if hue lock is on")); + _lightness_bar.set_value_pos(Gtk::PositionType::RIGHT); + _lightness_bar.set_hexpand(true); + _lightness_bar.set_draw_value(true); + _lightness_bar.signal_value_changed().connect([this]() { + double value = _lightness_bar.get_value(); + _color_wheel->setLightness(value); + }); + + _saturation_icon->set_from_icon_name(INKSCAPE_ICON("saturation")); + _saturation_icon->set_tooltip_text(_("change saturation for all if hue lock is on")); + _saturation_bar.set_value_pos(Gtk::PositionType::RIGHT); + _saturation_bar.set_hexpand(true); + _saturation_bar.set_draw_value(true); + _saturation_bar.signal_value_changed().connect([this]() { + double value = _saturation_bar.get_value(); + _color_wheel->setSaturation(value); + }); + + _hue_lock_image = Gtk::make_managed(); + _hue_lock_image->set_from_icon_name(INKSCAPE_ICON("object-unlocked")); + _hue_lock.set_child(*_hue_lock_image); + _hue_lock.signal_toggled().connect([this]() { + _color_wheel->toggleHueLock(_hue_lock.get_active()); + if (_hue_lock.get_active()) { + _hue_lock_image->set_from_icon_name(INKSCAPE_ICON("object-locked")); + _hue_lock.set_child(*_hue_lock_image); + } else { + _hue_lock_image->set_from_icon_name(INKSCAPE_ICON("object-unlocked")); + _hue_lock.set_child(*_hue_lock_image); + } + _color_wheel->redrawOnHueLocked(); + }); + _hue_lock.set_tooltip_text(_("lock hue angles for colors set")); + _hue_lock.set_hexpand(false); + _hue_lock.set_margin_top(8); + _hue_lock.set_halign(Gtk::Align::END); + + _color_wheel_preview->set_hexpand(false); + _color_wheel_preview->set_can_focus(false); + _color_wheel_preview->set_size_request(35,35); + _color_wheel_preview->set_halign(Gtk::Align::START); + _color_wheel_preview->set_margin_top(8); + _color_wheel_preview->setStyle(_color_wheel_preview->Style::Outlined); + + auto image = Gtk::make_managed(); + image->set_from_icon_name(INKSCAPE_ICON("reset-settings")); + _reset->set_child(*image); + _reset->set_margin_top(8); + _reset->signal_clicked().connect([this] { + if (_ra) { + _ra->onResetClicked(); + } + }); + + auto box = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); + box->set_spacing(64); + box->append(*_color_wheel_preview); + box->append(*_reset); + box->append(_hue_lock); + + _lightness_box->append(*_lightness_icon); + _lightness_box->append(_lightness_bar); + + _saturation_box->append(*_saturation_icon); + _saturation_box->append(_saturation_bar); + + _spaces_stack->set_visible_child("RGB"); + + append(*box); + append(*_color_wheel); + append(*_lightness_box); + append(*_saturation_box); + append(*_spaces_combo); + append(*_spaces_stack); +} + +void MultiMarkerColorPlate::_addPageForSpace(std::shared_ptr space, int page_num) +{ + auto mode_name = space->getName(); + _spaces_combo->add_row(space->getIcon(), mode_name, page_num); +} + +void MultiMarkerColorPlate::_createSlidersForSpace(std::shared_ptr space, + std::shared_ptr &colors, int index) +{ + auto mode_name = space->getName(); + Gtk::Grid *_grid = Gtk::make_managed(); + auto new_colors = std::make_shared(space, colors->getAlphaConstraint().value_or(true)); + new_colors->set(colors->get().value()); + int row = 0; + for (auto &component : new_colors->getComponents()) { + auto label = Gtk::make_managed(); + auto slider = Gtk::make_managed(new_colors, component); + auto spin = Gtk::make_managed(); + spin->set_digits(component.id == "alpha" ? 0 : 1); + if (component.scale < 100) { + // for small values increase precision + spin->set_digits(2); + spin->get_adjustment()->set_step_increment(0.1); + } + _grid->attach(*label, 0, row); + _grid->attach(*slider, 1, row); + _grid->attach(*spin, 2, row++); + _channels.emplace_back(std::make_unique(new_colors, *label, *slider, *spin)); + } + _color_sets[index] = {mode_name, new_colors}; + _spaces_stack->add(*_grid, mode_name, mode_name); +} + +} // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/multi-marker-color-plate.h b/src/ui/widget/multi-marker-color-plate.h new file mode 100644 index 0000000000..e7322d42b6 --- /dev/null +++ b/src/ui/widget/multi-marker-color-plate.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_WIDGET_MULTI_MARKER_COLOR_PLATE_H +#define INKSCAPE_UI_WIDGET_MULTI_MARKER_COLOR_PLATE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "src/colors/color-set.h" +#include "src/colors/manager.h" +#include "ui/widget/color-page.h" +#include "ui/widget/color-preview.h" +#include "ui/widget/icon-combobox.h" +#include "ui/widget/ink-color-wheel.h" +#include "recolor-art.h" + +namespace Inkscape::Colors { +class Color; +class ColorSet; +namespace Space { +class AnySpace; +} +} // namespace Inkscape::Colors + +namespace Inkscape::UI::Widget { + +class MultiMarkerColorPlate : public Gtk::Box +{ +public: + explicit MultiMarkerColorPlate(Colors::ColorSet const &colors); + + void setColors(std::vector colors) { _color_wheel->setColors(std::move(colors)); } + void setLightness(double value) + { + _color_wheel->setLightness(value); + _lightness_bar.set_value(value); + } + + void setSaturation(double value) + { + _color_wheel->setSaturation(value); + _saturation_bar.set_value(value); + } + void setRecolorWidget(RecolorArt *ra) { _ra = ra; } + void setActiveIndex(int index) { _color_wheel->setActiveIndex(index); } + void toggleHueLock(bool locked) { _color_wheel->toggleHueLock(locked); _hue_lock.set_active(locked);} + std::vector getColors() const { return _color_wheel->getColors(); } + Colors::Color getColor() const { return _color_wheel->getColor(); } + bool getHueLock() const { return _color_wheel->getHueLock(); } + int getActiveIndex() const { return _color_wheel->getActiveIndex(); } + int getHoverIndex() const { return _color_wheel->getHoverIndex(); } + void connect_color_hovered(sigc::slot slot) { _color_wheel->connect_color_hovered(slot); } + void connect_color_changed(sigc::slot slot) { _color_wheel->connect_color_changed(slot); } + void changeColor(int index, Color color) + { + _color_wheel->changeColor(index, color); + _color_wheel_preview->setRgba32(color.toRGBA()); + _specific_colors->set(color); + } + +private: + MultiMarkerWheel *_color_wheel = nullptr; + Gtk::Image *_lightness_icon = Gtk::make_managed(); + Gtk::Box *_lightness_box = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); + Glib::RefPtr adjustment = Gtk::Adjustment::create(100.0, 0.0, 100.0, 1.0, 10.0); + Gtk::Scale &_lightness_bar; + + Gtk::Image *_saturation_icon = Gtk::make_managed(); + Gtk::Box *_saturation_box = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); + Glib::RefPtr _saturation_adjustment = Gtk::Adjustment::create(100.0, 0.0, 100.0, 1.0, 10.0); + Gtk::Scale &_saturation_bar; + + Gtk::ToggleButton &_hue_lock; + ColorPreview *_color_wheel_preview = nullptr; + Manager *manager = &Manager::get(); + Gtk::Grid *_grid = nullptr; + std::shared_ptr _specific_colors; + std::vector> _channels; + Gtk::Image *_hue_lock_image = nullptr; + IconComboBox *_spaces_combo = nullptr; + Gtk::Stack *_spaces_stack = nullptr; + Gtk::StackSwitcher *_switcher = nullptr; + RecolorArt *_ra = nullptr; + Gtk::Button *_reset = nullptr; + std::map>> _color_sets; + sigc::scoped_connection _specific_colors_changed; + + void _addPageForSpace(std::shared_ptr space, int page_num); + void _createSlidersForSpace(std::shared_ptr space, + std::shared_ptr &colors, int index); +}; + +} // namespace Inkscape::UI::Widget + +#endif // INKSCAPE_UI_WIDGET_MULTI_MARKER_COLOR_PLATE_H diff --git a/src/ui/widget/paint-selector.cpp b/src/ui/widget/paint-selector.cpp index 3858bee04b..c124c5c318 100644 --- a/src/ui/widget/paint-selector.cpp +++ b/src/ui/widget/paint-selector.cpp @@ -25,6 +25,8 @@ #include #include +#include "desktop-style.h" +#include "desktop.h" #include "document.h" #include "inkscape.h" #include "object/sp-hatch.h" @@ -33,12 +35,18 @@ #include "object/sp-pattern.h" #include "object/sp-radial-gradient.h" #include "object/sp-stop.h" +#include "pattern-manipulation.h" +#include "selection.h" +#include "style.h" #include "ui/icon-names.h" #include "ui/pack.h" #include "ui/widget/color-notebook.h" #include "ui/widget/gradient-editor.h" #include "ui/widget/pattern-editor.h" +#include "ui/widget/swatch-selector.h" +#include "ui/widget/recolor-art-manager.h" #include "widgets/widget-sizes.h" +#include "recolor-art-manager.h" #ifdef SP_PS_VERBOSE static gchar const *modeStrings[] = { @@ -114,11 +122,15 @@ GradientSelectorInterface *PaintSelector::getGradientFromData() const PaintSelector::PaintSelector(FillOrStroke kind, std::shared_ptr colors) : _selected_colors(std::move(colors)) -{ +{ set_orientation(Gtk::Orientation::VERTICAL); _mode = static_cast(-1); // huh? do you mean 0xff? -- I think this means "not in the enum" + for (int i = 0; i < 5; i++) { + _recolorButtonTrigger[i] = std::make_unique(); + } + /* Paint style button box */ _style = Gtk::make_managed(); _style->set_name("PaintSelector"); @@ -188,6 +200,66 @@ PaintSelector::PaintSelector(FillOrStroke kind, std::shared_ptrset_visible(kind == FILL); + + for (auto const &b : _recolorButtonTrigger) { + b->set_label(_("Recolor Selection")); + b->set_hexpand(false); + b->set_vexpand(false); + b->set_size_request(180); + b->set_halign(Gtk::Align::CENTER); + b->set_valign(Gtk::Align::START); + b->set_margin_top(8); + b->set_visible(false); + + b->signal_clicked().connect([b = b.get(), this] { + auto guard = _blocker.block(); + if (!_recolorManager) { + // Lazy-load the recolour widget and popover. + _recolorManager = &RecolorArtManager::get(); + if (_recolorManager->getPopOver().get_parent()) { + _recolorManager->getPopOver().unparent(); + } + _recolorManager->getPopOver().set_parent(*b); + _recolorManager->setDesktop(_desktop); + } else if (_recolorManager->getPopOver().get_parent() != b) { + // Reparent the popover to this button if necessary. + _recolorManager->getPopOver().unparent(); + _recolorManager->getPopOver().set_parent(*b); + } + _recolorManager->getPopOver().popup(); + _recolorManager->performUpdate(); + }); + } + + _frame->append(*_recolorButtonTrigger[0]); +} + +void PaintSelector::setDesktop(SPDesktop *desktop) +{ + if (_desktop == desktop) { + return; + } + + if (_recolorManager) { + _recolorManager->getPopOver().popdown(); + } + + if (_selection_changed_connection) { + _selection_changed_connection.disconnect(); + } + + _desktop = desktop; + + if (_desktop) { + if (auto selection = _desktop->getSelection()) { + _selection_changed_connection = + selection->connectChanged(sigc::mem_fun(*this, &PaintSelector::onSelectionChanged)); + } + } + + if (_recolorManager) { + _recolorManager->setDesktop(_desktop); + } } StyleToggleButton *PaintSelector::style_button_add(gchar const *pixmap, PaintSelector::Mode mode, gchar const *tip) @@ -225,7 +297,11 @@ void PaintSelector::fillrule_toggled(FillRuleRadioButton *tb) } } -void PaintSelector::setMode(Mode mode) { +void PaintSelector::setMode(Mode mode) +{ + if (_recolorManager && _recolorManager->getPopOver().get_visible() && checkSelection(_desktop->getSelection())) { + return; + } set_mode_ex(mode, false); } @@ -276,6 +352,11 @@ void PaintSelector::set_mode_ex(Mode mode, bool switch_style) { } _mode = mode; _signal_mode_changed.emit(_mode, switch_style); + if (_desktop) { + if (auto sel = _desktop->getSelection()) { + onSelectionChanged(sel); + } + } _update = false; } } @@ -497,12 +578,15 @@ void PaintSelector::set_mode_color() auto const color_selector = Gtk::make_managed(_selected_colors); color_selector->set_visible(true); UI::pack_start(*_selector_solid_color, *color_selector, true, true); + UI::pack_start(*_selector_solid_color, *_recolorButtonTrigger[1], false, false); + /* Pack everything to frame */ _frame->append(*_selector_solid_color); color_selector->set_label(_("Flat color")); } _selector_solid_color->set_visible(true); + _selector_solid_color->set_vexpand(false); } _label->set_markup(""); //_("Flat color")); @@ -547,6 +631,7 @@ void PaintSelector::set_mode_gradient(PaintSelector::Mode mode) _selector_gradient->signal_changed().connect(sigc::mem_fun(*this, &PaintSelector::gradient_changed)); _selector_gradient->signal_stop_selected().connect([this](SPStop* stop) { _signal_stop_selected.emit(stop); }); /* Pack everything to frame */ + _selector_gradient->getColorBox().append(*_recolorButtonTrigger[2]); _frame->append(*_selector_gradient); } catch (std::exception& ex) { @@ -816,6 +901,7 @@ void PaintSelector::set_mode_mesh(PaintSelector::Mode mode) UI::pack_start(*_selector_mesh, *hb2, false, false, AUX_BETWEEN_BUTTON_GROUPS); _frame->append(*_selector_mesh); + _frame->reorder_child_after(*_recolorButtonTrigger[0], *_selector_mesh); } _selector_mesh->set_visible(true); @@ -902,8 +988,10 @@ void PaintSelector::pattern_destroy(GtkWidget *widget, PaintSelector * /*psel*/) g_object_unref(G_OBJECT(widget)); } -void PaintSelector::pattern_change(GtkWidget * /*widget*/, PaintSelector *psel) { psel->_signal_changed.emit(); } - +void PaintSelector::pattern_change(GtkWidget * /*widget*/, PaintSelector *psel) +{ + psel->_signal_changed.emit(); +} /*update pattern list*/ void PaintSelector::updatePatternList(SPPattern *pattern) @@ -932,7 +1020,9 @@ void PaintSelector::set_mode_pattern(PaintSelector::Mode mode) _selector_pattern->signal_changed().connect([this](){ _signal_changed.emit(); }); _selector_pattern->signal_color_changed().connect([this](Colors::Color const &){ _signal_changed.emit(); }); _selector_pattern->signal_edit().connect([this](){ _signal_edit_pattern.emit(); }); + _recolorButtonTrigger[3]->set_label(_("Recolor Pattern")); _frame->append(*_selector_pattern); + _frame->append(*_recolorButtonTrigger[3]); } SPDocument* document = SP_ACTIVE_DOCUMENT; @@ -1059,6 +1149,8 @@ void PaintSelector::set_mode_swatch(PaintSelector::Mode mode) gsel->signal_released().connect(sigc::mem_fun(*this, &PaintSelector::gradient_released)); gsel->signal_changed().connect(sigc::mem_fun(*this, &PaintSelector::gradient_changed)); + _selector_swatch->append(*_recolorButtonTrigger[4]); + _recolorButtonTrigger[4]->hide(); // Pack everything to frame _frame->append(*_selector_swatch); } else { @@ -1123,9 +1215,59 @@ PaintSelector::Mode PaintSelector::getModeForStyle(SPStyle const &style, FillOrS return mode; } +void PaintSelector::onSelectionChanged(Inkscape::Selection *selection) +{ + if (_blocker.pending()) { + return; + } + + if (checkSelection(selection)) { + if (_mode == MODE_MULTIPLE || _mode == MODE_UNSET || _mode == MODE_GRADIENT_MESH) { + hideAllExcept(_recolorButtonTrigger[0].get()); + } else if (_mode == MODE_SOLID_COLOR) { + hideAllExcept(_recolorButtonTrigger[1].get()); + } else if (_mode == MODE_GRADIENT_RADIAL || _mode == MODE_GRADIENT_LINEAR) { + hideAllExcept(_recolorButtonTrigger[2].get()); + } else if (_mode == MODE_PATTERN) { + hideAllExcept(_recolorButtonTrigger[3].get()); + } else if (_mode == MODE_SWATCH) { + hideAllExcept(_recolorButtonTrigger[4].get()); + } else { + hideAllExcept(); + } + } else { + hideAllExcept(); + } + + if (_recolorManager && _recolorManager->getPopOver().get_visible() && checkSelection(selection)) { + auto guard = _blocker.block(); + _recolorManager->performUpdate(); + } +} + +bool PaintSelector::checkSelection(Inkscape::Selection *selection) +{ + return (_mode == MODE_GRADIENT_MESH && (selection->size() > 1 || RecolorArtManager::checkMeshObject(selection))) || + RecolorArtManager::checkSelection(selection); +} + +void PaintSelector::hideAllExcept(Gtk::Button *recolorButtonTrigger) +{ + if (recolorButtonTrigger) { + recolorButtonTrigger->show(); + } + + for (auto const &b : _recolorButtonTrigger) { + if (b.get() != recolorButtonTrigger) { + b->hide(); + } + } +} + } // namespace Widget } // namespace UI } // namespace Inkscape + /* Local Variables: mode:c++ diff --git a/src/ui/widget/paint-selector.h b/src/ui/widget/paint-selector.h index c126f04252..b17b44e273 100644 --- a/src/ui/widget/paint-selector.h +++ b/src/ui/widget/paint-selector.h @@ -17,8 +17,11 @@ #define SEEN_SP_PAINT_SELECTOR_H #include "fill-or-stroke.h" +#include "ui/operation-blocker.h" #include "ui/widget/gradient-selector.h" #include "ui/widget/swatch-selector.h" +#include "selection.h" +#include "ui/widget/recolor-art-manager.h" class SPGradient; class SPLinearGradient; @@ -27,12 +30,15 @@ class SPRadialGradient; class SPMeshGradient; #endif class SPDesktop; +class Selection; class SPPattern; class SPStyle; namespace Gtk { class Label; class ToggleButton; +class Button; +class Popover; } // namespace Gtk namespace Inkscape::UI::Widget { @@ -41,6 +47,8 @@ class FillRuleRadioButton; class GradientEditor; class PatternEditor; class StyleToggleButton; +class RecolorArt; +class RecolorArtManager; /** * Generic paint selector widget. @@ -93,7 +101,14 @@ class PaintSelector : public Gtk::Box { Gtk::Box *_selector_mesh = nullptr; SwatchSelector *_selector_swatch = nullptr; PatternEditor* _selector_pattern = nullptr; - + + UI::Widget::RecolorArtManager *_recolorManager = nullptr; + std::array, 5> _recolorButtonTrigger; + OperationBlocker _blocker; + SPDesktop *_desktop = nullptr; + void onSelectionChanged(Inkscape::Selection *selection); + bool checkSelection(Inkscape::Selection *selection); + void hideAllExcept(Gtk::Button *recolorButtonTrigger = nullptr); Gtk::Label *_label; GtkWidget *_patternmenu = nullptr; bool _patternmenu_update = false; @@ -116,6 +131,7 @@ class PaintSelector : public Gtk::Box { sigc::signal _signal_changed; sigc::signal _signal_stop_selected; sigc::signal _signal_edit_pattern; + sigc::scoped_connection _selection_changed_connection; StyleToggleButton *style_button_add(gchar const *px, PaintSelector::Mode mode, gchar const *tip); void style_button_toggled(StyleToggleButton *tb); @@ -192,6 +208,7 @@ class PaintSelector : public Gtk::Box { Geom::Scale get_pattern_gap(); Glib::ustring get_pattern_label(); bool is_pattern_scale_uniform(); + void setDesktop(SPDesktop *desktop); }; enum { diff --git a/src/ui/widget/recolor-art-manager.cpp b/src/ui/widget/recolor-art-manager.cpp new file mode 100644 index 0000000000..dfb2c45dbc --- /dev/null +++ b/src/ui/widget/recolor-art-manager.cpp @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Fatma Omara + * + * Copyright (C) 2025 authors + */ + +#include "recolor-art-manager.h" + +#include "object/sp-gradient.h" +#include "object/sp-mask.h" +#include "object/sp-pattern.h" +#include "object/sp-use.h" +#include "style.h" + +namespace Inkscape::UI::Widget { +namespace { + +bool has_colors_pattern(SPItem const *item) +{ + std::set colors; + SPPattern *patternstroke = nullptr; + SPPattern *patternfill = nullptr; + SPPattern *pattern = nullptr; + if (item && item->style) { + patternstroke = cast(item->style->getStrokePaintServer()); + patternfill = cast(item->style->getFillPaintServer()); + } + + if (patternstroke) + pattern = patternstroke; + if (patternfill) + pattern = patternfill; + if (!pattern) + return false; + SPPattern *root = pattern->rootPattern(); + for (auto &child : root->children) { + if (auto group = cast(&child)) { + for (auto &child : group->children) { + if (auto c = dynamic_cast(&child)) { + if (c->style->fill.isColor()) { + std::string rgba = c->style->fill.getColor().toString(true); + colors.insert(rgba); + } + if (c->style->stroke.isColor()) { + std::string rgba = c->style->stroke.getColor().toString(true); + colors.insert(rgba); + } + } + } + } + auto item = cast(&child); + if (!item || !item->style) + continue; + + if (item->style->fill.isColor()) { + std::string rgba = item->style->fill.getColor().toString(true); + colors.insert(rgba); + } + if (item->style->stroke.isColor()) { + std::string rgba = item->style->stroke.getColor().toString(true); + colors.insert(rgba); + } + } + return colors.size() > 1; +} + +} // namespace + +RecolorArtManager &RecolorArtManager::get() +{ + static RecolorArtManager instance; + return instance; +} + +RecolorArtManager::RecolorArtManager() +{ + _recolorPopOver.set_autohide(false); + _recolorPopOver.set_position(Gtk::PositionType::LEFT); + _recolorPopOver.set_child(_recolor_widget); +} + +bool RecolorArtManager::checkSelection(Inkscape::Selection *selection) +{ + auto group = cast(selection->single()); + auto use_group = cast(selection->single()); + auto item = cast(selection->single()); + bool pattern_colors = false; + SPMask *mask = nullptr; + if (item) { + mask = cast(item->getMaskObject()); + pattern_colors = has_colors_pattern(item); + } + return selection->size() > 1 || group || use_group || mask || pattern_colors; +} + +bool RecolorArtManager::checkMeshObject(Inkscape::Selection *selection) +{ + if (selection->items().empty()) { + return false; + } + auto fill_gradient = cast(selection->single()->style->getFillPaintServer()); + auto stroke_gradient = cast(selection->single()->style->getStrokePaintServer()); + SPGradient *gradient = fill_gradient ? cast(fill_gradient) : cast(stroke_gradient); + return gradient && gradient->hasPatches(); +} + +void RecolorArtManager::setDesktop(SPDesktop *desktop) +{ + _recolor_widget.setDesktop(desktop); +} + +void RecolorArtManager::performUpdate() +{ + _recolor_widget.performUpdate(); +} + +void RecolorArtManager::performMarkerUpdate(SPMarker *marker) +{ + _recolor_widget.performMarkerUpdate(marker); +} + +Gtk::Popover &RecolorArtManager::getPopOver() +{ + return _recolorPopOver; +} + +} // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/recolor-art-manager.h b/src/ui/widget/recolor-art-manager.h new file mode 100644 index 0000000000..be30750e56 --- /dev/null +++ b/src/ui/widget/recolor-art-manager.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_WIDGET_RECOLOR_ART_MANAGER_H +#define INKSCAPE_UI_WIDGET_RECOLOR_ART_MANAGER_H +/* + * Authors: + * Fatma Omara + * + * Copyright (C) 2025 authors + */ + +#include + +#include "ui/widget/recolor-art.h" +#include "selection.h" + +namespace Inkscape::UI::Widget { + +class RecolorArtManager +{ +public: + static RecolorArtManager &get(); + + Gtk::Popover &getPopOver(); + + static bool checkSelection(Inkscape::Selection *selection); + static bool checkMeshObject(Inkscape::Selection *selection); + + void setDesktop(SPDesktop *desktop); + void performUpdate(); + void performMarkerUpdate(SPMarker *marker); + +private: + RecolorArtManager(); + + RecolorArt _recolor_widget; + Gtk::Popover _recolorPopOver; +}; + +} // namespace Inkscape::UI::Widget + +#endif // INKSCAPE_UI_WIDGET_RECOLOR_ART_MANAGER_H diff --git a/src/ui/widget/recolor-art.cpp b/src/ui/widget/recolor-art.cpp new file mode 100644 index 0000000000..cfe623c258 --- /dev/null +++ b/src/ui/widget/recolor-art.cpp @@ -0,0 +1,630 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "recolor-art.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "actions/actions-tools.h" +#include "color-notebook.h" +#include "desktop.h" +#include "document-undo.h" +#include "multi-marker-color-plate.h" +#include "ui/builder-utils.h" +#include "ui/icon-names.h" +#include "ui/widget/color-preview.h" +#include "selcue.h" +#include "seltrans.h" +#include "selection.h" +#include "ui/tools/select-tool.h" +#include "object/sp-marker.h" + + +namespace Inkscape::UI::Widget { + +/* + * this class is showed by paint-selector class that has a button + * to trigger the popover that has this widget as its child + * + * related classes : + * 1- ink-colorwheel for the multimarkercolorwheel in the colorwheel page + * 2- multi-marker-color-wheel-plate that manges the multimarkercolorwheel and sliders under it + * 3- object-colors manages data and extract objects colors + */ + +RecolorArt::RecolorArt() + : _builder{create_builder("widget-recolor.ui")} + , _notebook(*_builder->get_widget("list-wheel-box")) + , _color_wheel_page(*_builder->get_widget("color-wheel-page")) + , _color_wheel(Gtk::make_managed(Colors::ColorSet{})) + , _color_list(*_builder->get_widget("colors-list")) + , _reset(*_builder->get_widget("reset")) + , _live_preview(*_builder->get_widget("liveP")) +{ + set_name("RecolorArt"); + append(get_widget(_builder, "recolor-art")); + _solid_colors->set(Color(0x000000ff)); + // when recolor widget is closed it resets opacity to mitigate the effect of getSelection function + // and reshow selection boxes again + signal_unmap().connect([&]() { + if (!_is_preview) { + _manager.convertToRecoloredColors(); + DocumentUndo::done(_desktop->getDocument(), _("changed Item color"), + INKSCAPE_ICON("object-recolor-art")); + } + if (_desktop) { + _desktop->setHideSelectionBoxes(false); + } + }); + // hide selection boxes after widget gets mapped (this is why it connects to signal idle to activate after finishing + // mapping + signal_map().connect([&]() { + Glib::signal_idle().connect([this]() { + if (_desktop) { + _desktop->setHideSelectionBoxes(true); + } + return false; + }); + }); + _color_wheel->connect_color_changed(static_cast>([this]() { + if(_blocker.pending()) { + return; // to stop recursive calling to signal if changed from the color list page + } + uint32_t cc = _color_wheel->getColor().toRGBA(); + Color c(cc,true); + if(_color_wheel->getActiveIndex() != -1) { + int index = _color_wheel->getActiveIndex(); + if (!_manager.getColor(index)) { + return; + } + _current_color_id = _manager.getColor(index)->toRGBA(); + auto idx = findColorItemByKey(_current_color_id); + _selection_model->set_selected(idx.second); + onColorPickerChanged(c, true); + onOriginalColorClicked(_current_color_id); + if (_color_wheel->getHueLock()) { + if (_manager.isColorsEmpty()) { + return; + } + std::vector new_colors = _color_wheel->getColors(); + _manager.setSelectedNewColor(new_colors); + updateColorModel(new_colors); + if (_is_preview) + _manager.convertToRecoloredColors(); + } + } + })); + _color_wheel->setRecolorWidget(this); + // add hover opacity effect when hovering over markers in the wheel + _color_wheel->connect_color_hovered([this] { + uint32_t cc = _color_wheel->getColor().toRGBA(); + Color c(cc,true); + if (_color_wheel->getHoverIndex() != -1) { + int index = _color_wheel->getHoverIndex(); + if (!_manager.getColor(index)) { + return; + } + _current_color_id = _manager.getColor(index)->toRGBA(); + } + }); + + layoutColorPicker(); + _live_preview.set_active(true); + _live_preview.signal_toggled().connect(sigc::mem_fun(*this, &RecolorArt::onLivePreviewToggled)); + _reset.signal_clicked().connect(sigc::mem_fun(*this, &RecolorArt::onResetClicked)); + + // setting up list view for the color list + _list_view = _builder->get_widget("recolor-art-list"); + _color_model = Gio::ListStore::create(); + _selection_model = Gtk::SingleSelection::create(_color_model); + _color_factory = Gtk::SignalListItemFactory::create(); + + // setup how the list item should look + _color_factory->signal_setup().connect([](Glib::RefPtr const &list_item) { + auto box = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); + auto original = Gtk::make_managed(); + auto arrow = Gtk::make_managed(); + auto recolored = Gtk::make_managed(); + + auto original_preview = Gtk::make_managed(Color(0x00000000).toRGBA()); + auto recolored_preview = Gtk::make_managed(Color(0x00000000).toRGBA()); + + auto type_box = Gtk::make_managed(Gtk::Orientation::HORIZONTAL); + type_box->set_spacing(2); + type_box->set_margin_start(4); + type_box->set_hexpand(false); + type_box->set_vexpand(false); + type_box->set_halign(Gtk::Align::START); + type_box->set_valign(Gtk::Align::CENTER); + type_box->get_style_context()->add_class("type_box"); + + original_preview->set_hexpand(true); + recolored_preview->set_hexpand(true); + + original_preview->set_vexpand(true); + recolored_preview->set_vexpand(true); + + auto original_overlay = Gtk::make_managed(); + + original_overlay->set_child(*original_preview); + original_overlay->add_overlay(*type_box); + + original->append(*original_overlay); + recolored->append(*recolored_preview); + + original->set_hexpand(true); + + recolored->set_hexpand(true); + + arrow->set_from_icon_name(INKSCAPE_ICON("go-right")); + arrow->set_halign(Gtk::Align::CENTER); + arrow->set_valign(Gtk::Align::CENTER); + arrow->set_margin_top(3); + arrow->set_margin_start(6); + arrow->set_margin_end(6); + + box->set_name("original-recolor-box"); + box->append(*original); + box->append(*arrow); + box->append(*recolored); + list_item->set_data("typebox", type_box); + list_item->set_child(*box); + }); + + // setup signals for the list item children after they are created + _color_factory->signal_bind().connect([this](Glib::RefPtr const &list_item) { + auto item = std::dynamic_pointer_cast(list_item->get_item()); + if (!item) { + return; + } + + auto box = dynamic_cast(list_item->get_child()); + if (!box || !box->get_first_child() || !box->get_last_child()) { + return; + } + + auto original = dynamic_cast(box->get_first_child()); + auto recolored = dynamic_cast(box->get_last_child()); + + if (original && recolored) { + colorButtons(original, item->old_color, true); + colorButtons(recolored, item->new_color); + setUpTypeBox(static_cast(list_item->get_data("typebox")), item->old_color); + + original->set_name("original"); + recolored->set_name("recolored"); + + auto original_click = Gtk::GestureClick::create(); + auto recolored_click = Gtk::GestureClick::create(); + + original_click->signal_pressed().connect( + [this, item, index = list_item->get_position()](int n_press, double x, double y) { + _selection_model->set_selected(index); + onOriginalColorClicked(item->key); + }); + + recolored_click->signal_pressed().connect( + [this, item, index = list_item->get_position()](int n_press, double x, double y) { + _selection_model->set_selected(index); + onOriginalColorClicked(item->key); + }); + + original->add_controller(original_click); + recolored->add_controller(recolored_click); + } + }); + + _list_view->set_model(_selection_model); + _list_view->set_factory(_color_factory); + + auto lm = _list_view->get_layout_manager(); + if (auto grid_layout = std::dynamic_pointer_cast(lm)) { + grid_layout->set_row_spacing(0); + } + _list_view->set_hexpand(false); + _list_view->set_vexpand(false); + + _selection_model->signal_selection_changed().connect([this](guint pos, guint n_items) { + int index = _selection_model->get_selected(); + if (index < 0) { + return; + } + + auto item = _color_model->get_item(index); + auto color_item = std::dynamic_pointer_cast(item); + if (!color_item) { + return; + } + + onOriginalColorClicked(color_item->key); + }); + + _color_wheel_page.append(*_color_wheel); +} + +void RecolorArt::setDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + _desktop = desktop; + _color_wheel->toggleHueLock(false); + _color_wheel->setLightness(100.0); + _color_wheel->setSaturation(100.0); + } +} + +/* + * prepare color model by creating color items and populate the color model + * then push the _list_view to color list page to show it in the ui + */ +void RecolorArt::generateVisualList() +{ + _color_model->remove_all(); + std::vector> items; + for (auto const &[key, value] : _manager.getSelectedColorsMap()) { + auto old_color = value.second.value().old_color; + auto new_color = value.second.value().new_color; + items.push_back(ColorItem::create(key, old_color, new_color)); + } + _color_model->splice(0,0,items); + if (_color_model->get_n_items() > 0) { + _selection_model->set_selected(0); + } +} + +/* + * setup the layout of the colornotebook ui in the colorlist page + * connect _solid_colors to color changed signal and call the signal handler + */ +void RecolorArt::layoutColorPicker(std::shared_ptr updated_color) +{ + _color_picker_wdgt = Gtk::make_managed(_solid_colors); + _color_picker_wdgt->set_label(_("Selected Color")); + + _solid_colors->signal_changed.connect([this]() { onColorPickerChanged(); }); + + auto container = _builder->get_widget("color-picker"); + if (container) { + for (auto child : container->get_children()) { + container->remove(*child); + } + container->append(*_color_picker_wdgt); + } else { + g_warning("color picker not found"); + } +} + +/* + * set colorpreview colors used in color factory signal bind + */ +void RecolorArt::colorButtons(Gtk::Box *button, Color color, bool is_original) +{ + if (button) { + auto child = + is_original + ? dynamic_cast(dynamic_cast(button->get_first_child())->get_child()) + : dynamic_cast(button->get_first_child()); + if (child) { + auto rgba = color.toRGBA(); + child->setRgba32(rgba); + } + } +} + +/* + * set up the type box which shows the color usage in what kind (fill,strok,pattern...etc) + */ +void RecolorArt::setUpTypeBox(Gtk::Box *box, Color const &color) +{ + if (!box->get_children().empty()) { + return; + } + + auto items = _manager.getSelectedItems(color.toRGBA()); + if (!items.empty()) { + std::string size = "" + std::to_string(items.size()) + ""; + std::map> kinds; + for (auto item : items) { + if (item.type == ObjectStyleType::Fill) { + kinds[INKSCAPE_ICON("object-fill")].first++; + kinds[INKSCAPE_ICON("object-fill")].second = "fill"; + } else if (item.type == ObjectStyleType::Stroke) { + kinds[INKSCAPE_ICON("object-stroke")].first++; + kinds[INKSCAPE_ICON("object-stroke")].second = "stroke"; + } else if (item.type == ObjectStyleType::Mesh) { + kinds[INKSCAPE_ICON("paint-gradient-mesh")].first++; + kinds[INKSCAPE_ICON("paint-gradient-mesh")].second = "mesh gradient"; + } else if (item.type == ObjectStyleType::Linear) { + kinds[INKSCAPE_ICON("paint-gradient-linear")].first++; + kinds[INKSCAPE_ICON("paint-gradient-linear")].second = "linear gradient"; + } else if (item.type == ObjectStyleType::Radial) { + kinds[INKSCAPE_ICON("paint-gradient-radial")].first++; + kinds[INKSCAPE_ICON("paint-gradient-radial")].second = "radial gradient"; + } else if (item.type == ObjectStyleType::Pattern) { + kinds[INKSCAPE_ICON("paint-pattern")].first++; + kinds[INKSCAPE_ICON("paint-pattern")].second = "pattern"; + } else if (item.type == ObjectStyleType::Marker) { + kinds[INKSCAPE_ICON("markers")].first++; + kinds[INKSCAPE_ICON("markers")].second = "marker"; + } else if (item.type == ObjectStyleType::Mask) { + kinds[INKSCAPE_ICON("overlay-mask")].first++; + kinds[INKSCAPE_ICON("overlay-mask")].second = "mask"; + } else if (item.type == ObjectStyleType::Swatch) { + size = "" + std::to_string(items.size() / 2) + ""; + kinds[INKSCAPE_ICON("paint-swatch")].first++; + kinds[INKSCAPE_ICON("paint-swatch")].second = "swatch"; + } + } + auto label = Gtk::make_managed(); + label->set_use_markup(true); + label->set_markup(size); + box->append(*label); + std::string tooltip; + int sz = kinds.size(), s = 0; + for (auto [icon, pair] : kinds) { + Gtk::Image *img = nullptr; + img = Gtk::make_managed(); + img->set_from_icon_name(icon); + + if (img) { + if (icon == INKSCAPE_ICON("overlay-mask")) { + img->set_pixel_size(16); + img->set_halign(Gtk::Align::CENTER); + img->set_valign(Gtk::Align::CENTER); + } else { + img->set_pixel_size(8); + } + box->append(*img); + if(icon == INKSCAPE_ICON("paint-swatch")) pair.first /= 2; + tooltip += std::to_string(pair.first)+" x " + pair.second; + if (s != sz - 1) { + tooltip += "\n"; + } + } + s++; + } + box->set_tooltip_text(tooltip); + } +} + +/* + * signal handler to set solid colors(color notebook at color list page),_color_picker_wdgt and the active index in + * colorwheel page to the color of the colorpreview clicked + */ +void RecolorArt::onOriginalColorClicked(uint32_t color_id) +{ + if (!_manager.isColorsEmpty()) { + int index = _manager.getColorIndex(color_id); + if (index > -1) { + _color_wheel->setActiveIndex(_manager.getColorIndex(color_id)); + } + } + _current_color_id = color_id; + if (auto color = _manager.getSelectedNewColor(color_id)) { + _solid_colors->set(*color); // update sliders under the colorwheel in the colorlist page + _color_picker_wdgt->setCurrentColor(_solid_colors); /* solves the issue of needing to create new + colornotebook every time the _solid_colors changes because it only changes the sliders not the + color wheel it self in colornotebook */ + } +} + +/* +* if LP is checked it searches for the items that has a key matching to the parameter color +* and loop on them to change their color lively +* put the recolor action into the undo stack as well +*/ +void RecolorArt::lpChecked(Color color, bool wheel) +{ + std::optional new_color = wheel ? color : _solid_colors->get(); + if (!new_color.has_value()) { + return; + } + if (!_manager.applyNewColorToSelection(_current_color_id, new_color.value())) { + return; + } + + DocumentUndo::maybeDone(_desktop->getDocument(), _("changed Item color"), _("Recolor items"), + INKSCAPE_ICON("object-recolor-art")); +} + +/* + * this is a signal handler for when solid color changes either in the sliders or the color wheels in + * both notebook pages + * searches for the selected color items then change them through lpchecked() function and update the pair + * in _selected_colors map + * sync the change through notebook pages what happens in one get updated in the other + * update color model item to refresh the listview ui + */ +void RecolorArt::onColorPickerChanged(Color color, bool wheel) +{ + auto guard = _blocker.block(); + std::optional new_color = wheel ? color : _solid_colors->get(); + if (!new_color.has_value()) { + return; + } + // to prevent unnecessary changes if the "new_color" is still equal to the current color on the wheel + if (_manager.getSelectedNewColor(_current_color_id) == new_color.value()) { + return; + } + std::string _color_string = new_color.value().toString(); + _manager.setSelectedNewColor(_current_color_id, new_color.value()); + + // apply changes to selected items + if (_live_preview.property_active()) { + if (wheel) + lpChecked(color, wheel); + else + lpChecked(); + } + guint index = _selection_model->get_selected(); + Glib::RefPtr item; + // if change is coming from colorlist page sync that to colorwheel page + if (!wheel){ + if (index < 0) { + return; + } + item = _color_model->get_item(index); + int i = _manager.getColorIndex(_current_color_id); + if (i > -1) { + _color_wheel->changeColor(i, new_color.value()); + } + } + else { // if change is coming from colorwheel page sync that to colorlist page + auto item_index = findColorItemByKey(_current_color_id); + item = item_index.first; + index = item_index.second; + } + if (!item) { + return; + } + // update colormodel item to refresh listview ui + auto color_item = std::dynamic_pointer_cast(item); + auto new_item = ColorItem::create(color_item->key, color_item->old_color, new_color.value()); + _color_model->splice(index, 1, {new_item}); +} + +/* + * update color model to refresh the listview ui with the new chossen colors + */ +void RecolorArt::updateColorModel(std::vector const &new_colors) +{ + if (!new_colors.empty() && new_colors.size() != _color_model->get_n_items()) { + return; + } + std::vector> new_colors_buttons; + for (auto i = 0; i < _color_model->get_n_items(); i++) { + auto item = _color_model->get_item(i); + if (!item) { + continue; + } + auto color_item = std::dynamic_pointer_cast(item); + if(!color_item) { + continue; + } + int index = _manager.getColorIndex(color_item->key); + auto new_item = ColorItem::create(color_item->key, color_item->old_color, + new_colors.empty() ? color_item->old_color : new_colors[index]); + new_colors_buttons.push_back(new_item); + } + _color_model->splice(0, _color_model->get_n_items(), new_colors_buttons); +} + +/* + * finding a color model item by key + */ +std::pair, guint> RecolorArt::findColorItemByKey(uint32_t key) +{ + for (auto i = 0; i < _color_model->get_n_items(); i++) { + auto item = _color_model->get_item(i); + auto color_item = std::dynamic_pointer_cast(item); + if (key == color_item->key) { + return {item, i}; + } + } + return {nullptr, -1}; +} + +/* + * signal function handler for reset button clicked + * that reset every thing to its original states + */ +void RecolorArt::onResetClicked() +{ + _color_wheel->toggleHueLock(false); + _color_wheel->setLightness(100.0); + _color_wheel->setSaturation(100.0); + _color_wheel->setColors(_manager.getColors()); + updateColorModel(); + _manager.revertToOriginalColors(true); + guint index = _selection_model->get_selected(); + auto item = _color_model->get_item(index); + auto color_item = std::dynamic_pointer_cast(item); + + onOriginalColorClicked(color_item->key); +} + +/* + * apply recoloring when the LP check box is checked + * and get back to original colors when it is unchecked + */ +void RecolorArt::onLivePreviewToggled() +{ + _is_preview = _live_preview.property_active(); + if (_is_preview) { + _manager.convertToRecoloredColors(); + } else { + _manager.revertToOriginalColors(); + } +} + +/* + * main function that : + * 1- clears old data + * 2- get selection items from desktop + * 3- unlink selection items if there are clones + * 4- call collect colors func + * 5- put the generated list in the UI + */ +void RecolorArt::performUpdate() +{ + if (_selection_blocker.pending()) { + return; + } + if (!_desktop) { + g_warning("Desktop is NULL in Performupdate in recolor widget"); + return; + } + + _manager.clearData(); + + _color_wheel->toggleHueLock(false); + _color_wheel->setLightness(100.0); + _color_wheel->setSaturation(100.0); + + auto selection = _desktop->getSelection(); + + auto items = selection->items(); + auto vec = std::vector(items.begin(), items.end()); + _manager = collect_colours(vec); + if (!_manager.isColorsEmpty()) { + generateVisualList(); + auto first_button_id = _manager.getFirstKey(); + onOriginalColorClicked(first_button_id); + } + if (!_manager.isColorsEmpty()) { + _color_wheel->setColors(_manager.getColors()); + } +} + +void RecolorArt::performMarkerUpdate(SPMarker *marker) +{ + if (!marker) { + return; + } + if (!_desktop) { + g_warning("Desktop is NULL in Performupdate in recolor widget"); + return; + } + + _manager.clearData(); + + _color_wheel->toggleHueLock(false); + _color_wheel->setLightness(100.0); + _color_wheel->setSaturation(100.0); + + _manager = collect_colours({marker}); + if (!_manager.isColorsEmpty()) { + generateVisualList(); + auto first_button_id = _manager.getFirstKey(); + onOriginalColorClicked(first_button_id); + } + if (!_manager.isColorsEmpty()) { + _color_wheel->setColors(_manager.getColors()); + } +} + +} // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/recolor-art.h b/src/ui/widget/recolor-art.h new file mode 100644 index 0000000000..6f0f8cf228 --- /dev/null +++ b/src/ui/widget/recolor-art.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef INKSCAPE_UI_WIDGET_RECOLOR_ART_H +#define INKSCAPE_UI_WIDGET_RECOLOR_ART_H +/* + * Authors: + * Fatma Omara + * + * Copyright (C) 2025 authors + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "colors/color-set.h" +#include "object-colors.h" +#include "ui/operation-blocker.h" +#include "ui/widget/ink-color-wheel.h" + +class SPMarker; + +namespace Inkscape::Colors { +class Color; +class ColorSet; +} // namespace Inkscape::Colors + +namespace Gtk { +class Builder; +class ListStore; +class Notebook; +} // namespace Gtk + +class SPDesktop; + +namespace Inkscape::UI::Widget { + +class ColorNotebook; +class MultiMarkerColorPlate; + +struct ColorItem : public Glib::Object +{ + uint32_t key; + Colors::Color old_color{0}; + Colors::Color new_color{0}; + + static Glib::RefPtr create(uint32_t k, Colors::Color const &old_c, Colors::Color const &new_c) + { + auto item = Glib::make_refptr_for_instance(new ColorItem()); + item->key = k; + item->old_color = old_c; + item->new_color = new_c; + return item; + } +}; + +class RecolorArt : public Gtk::Box +{ +public: + RecolorArt(); + + void performUpdate(); + bool isInPreviewMode() const { return _is_preview; } + void setDesktop(SPDesktop *desktop); + void onResetClicked(); + void performMarkerUpdate(SPMarker *marker); + +private: + SPDesktop *_desktop = nullptr; + Glib::RefPtr _builder; + Gtk::Notebook &_notebook; + Gtk::Box &_color_wheel_page; + std::shared_ptr _solid_colors = std::make_shared(); + sigc::connection _solid_color_changed; + Gtk::Box &_color_list; + Gtk::Button &_reset; + Gtk::CheckButton &_live_preview; + Inkscape::UI::Widget::ColorNotebook *_color_picker_wdgt = nullptr; + Gtk::ListView *_list_view = nullptr; + uint32_t _current_color_id; + bool _is_preview = true; + + Glib::RefPtr> _color_model; + Glib::RefPtr _color_factory; + Glib::RefPtr _selection_model; + + ObjectColorSet _manager; + + MultiMarkerColorPlate *_color_wheel = nullptr; + + OperationBlocker _blocker; + OperationBlocker _selection_blocker; + + void generateVisualList(); + void layoutColorPicker(std::shared_ptr updated_color = nullptr); + void colorButtons(Gtk::Box *button, Colors::Color color, bool is_original = false); + + // signal handlers + void onOriginalColorClicked(uint32_t color_id); + void onColorPickerChanged(Colors::Color color = Colors::Color{0}, bool wheel = false); + void onLivePreviewToggled(); + void lpChecked(Colors::Color color = Colors::Color(0), bool wheel = false); + void setUpTypeBox(Gtk::Box *box, Colors::Color const &color); + void updateColorModel(std::vector const &new_colors = {}); + std::pair,guint> findColorItemByKey(uint32_t key); +}; + +} // namespace Inkscape::UI::Widget + +#endif // INKSCAPE_UI_WIDGET_RECOLOR_ART_H diff --git a/src/ui/widget/stroke-style.cpp b/src/ui/widget/stroke-style.cpp index fbfbab6a51..5e12c35954 100644 --- a/src/ui/widget/stroke-style.cpp +++ b/src/ui/widget/stroke-style.cpp @@ -489,6 +489,9 @@ void StrokeStyle::setDesktop(SPDesktop *desktop) desktop->connectDocumentReplaced(sigc::mem_fun(*this, &StrokeStyle::_handleDocumentReplaced)); _handleDocumentReplaced(nullptr, desktop->getDocument()); + for (MarkerComboBox *combo : {startMarkerCombo, midMarkerCombo, endMarkerCombo}) { + combo->setDesktop(desktop); + } updateLine(); } diff --git a/testfiles/CMakeLists.txt b/testfiles/CMakeLists.txt index ac57a780a6..a5525d5f90 100644 --- a/testfiles/CMakeLists.txt +++ b/testfiles/CMakeLists.txt @@ -114,6 +114,8 @@ set(TEST_SOURCES lpe-test ui-util-test sp-document-test + object-colors-test + multi-marker-color-wheel-test ${LPE_TESTS_64bit} ) diff --git a/testfiles/src/multi-marker-color-wheel-test.cpp b/testfiles/src/multi-marker-color-wheel-test.cpp new file mode 100644 index 0000000000..1ba61409d2 --- /dev/null +++ b/testfiles/src/multi-marker-color-wheel-test.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Test the multimarker color wheel public functions functionality of Recolor Art Widget + * + * Authors: + * Fatma Omara + * + * Copyright (C) 2025 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "src/colors/color.h" +#include "ui/widget/ink-color-wheel.h" + +using namespace Inkscape; +using Inkscape::Colors::Color; +using namespace Inkscape::Colors::Space; +class ColorWheelTestFixture : public ::testing::Test +{ +protected: + std::unique_ptr wheel; + std::vector colors; + + void SetUp() override + { + char const *gui_env = std::getenv("INKSCAPE_TEST_GUI"); + if (!gui_env || std::string(gui_env) != "1") { + GTEST_SKIP() << "Skipping GUI tests: GUI testing not enabled"; + } else { + gtk_init(); + } + colors = {Color(Type::CMYK, {0.1, 0.8, 0.0, 0.0}), + Color(0xff0000ff), + Color(0x00ff007f), + Color(0x0000ff32), + Color(0x7e1a9cff), + Color(Type::HSLUV, {120.0, 100.0, 50.0}), + Color(Type::HSL, {0.33, 1.0, 0.5}), + Color(Type::HSV, {0.66, 1.0, 1.0}), + Color(Type::LAB, {60.0, -40.0, 30.0})}; + + wheel = std::make_unique(); + } +}; + +TEST_F(ColorWheelTestFixture, TestColorWheelBasics) +{ + EXPECT_TRUE(wheel->getColors().empty()); + EXPECT_EQ(wheel->getActiveIndex(), -1); + + wheel->setColors(colors); + EXPECT_EQ(wheel->getColors().size(), 9); + EXPECT_EQ(wheel->getActiveIndex(), 0); +} + +TEST_F(ColorWheelTestFixture, TestColorWheelActiveIndex) +{ + wheel->setColors(colors); + EXPECT_TRUE(wheel->setActiveIndex(8)); + EXPECT_EQ(wheel->getActiveIndex(), 8); + + EXPECT_FALSE(wheel->setActiveIndex(-1)); + EXPECT_FALSE(wheel->setActiveIndex(99)); + EXPECT_EQ(wheel->getActiveIndex(), 8); +} + +TEST_F(ColorWheelTestFixture, TestColorWheelLightnessAndSaturation) +{ + wheel->setColors(colors); + wheel->setLightness(90); + EXPECT_EQ(wheel->getColor()[2], 0.9); + + wheel->setSaturation(40); + EXPECT_EQ(wheel->getColor()[1], 0.4); + + auto color = Color(0xffffffff); + EXPECT_TRUE(wheel->changeColor(8, color)); + EXPECT_TRUE(wheel->setActiveIndex(8)); + EXPECT_EQ(wheel->getColor().toRGBA(), color.toRGBA()); +} + +TEST_F(ColorWheelTestFixture, TestColorWheelHueLocking) +{ + wheel->setColors(colors); + EXPECT_FALSE(wheel->getColors().empty()); + wheel->toggleHueLock(true); + EXPECT_EQ(wheel->getHueLock(), true); + + wheel->setLightness(50); + wheel->setSaturation(83); + EXPECT_EQ(wheel->getColors()[4][2], 0.5); + + for (auto color : wheel->getColors()) { + EXPECT_EQ(color[2], 0.5); + EXPECT_EQ(color[1], 0.83); + } +} diff --git a/testfiles/src/object-colors-test.cpp b/testfiles/src/object-colors-test.cpp new file mode 100644 index 0000000000..5535f86d6f --- /dev/null +++ b/testfiles/src/object-colors-test.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Test the Object Colors Extraction and Data Population functionality of Recolor Art Widget + * + * Authors: + * Fatma Omara + * + * Copyright (C) 2025 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "src/object-colors.h" + +#include +#include + +#include "object/sp-defs.h" +#include "object/sp-object.h" +#include "object/sp-root.h" +#include "object/sp-stop.h" +#include "src/colors/color.h" +#include "src/document.h" +#include "xml/node.h" +#include "style.h" +using namespace Inkscape; + +class ObjectColorSetFixture : public DocPerCaseTest +{ +protected: + std::vector nodes; + std::vector vector; + + ObjectColorSet set; + + ObjectColorSetFixture() + { + SetUpTestCase(); + + Inkscape::XML::Document *xml_doc = _doc->getReprDoc(); + + Inkscape::XML::Node *grad_node = xml_doc->createElement("svg:linearGradient"); + grad_node->setAttribute("id", "test-gradient"); + + Inkscape::XML::Node *stop1_node = xml_doc->createElement("svg:stop"); + stop1_node->setAttribute("offset", "0"); + stop1_node->setAttribute("stop-color", "#ffA000ff"); + stop1_node->setAttribute("stop-opacity", "1"); + grad_node->appendChild(stop1_node); + + Inkscape::XML::Node *stop2_node = xml_doc->createElement("svg:stop"); + stop2_node->setAttribute("offset", "1"); + stop2_node->setAttribute("stop-color", "#00ffffff"); + stop2_node->setAttribute("stop-opacity", "1"); + grad_node->appendChild(stop2_node); + + for (int i = 0; i < 6; i++) { + nodes.push_back(xml_doc->createElement("svg:rect")); + } + + nodes[0]->setAttribute("fill", "#ffff00ff"); + nodes[0]->setAttribute("stroke", "#6c7ad2ff"); + nodes[1]->setAttribute("fill", "#6c7ad2ff"); + nodes[1]->setAttribute("stroke", "#ffff00ff"); + nodes[2]->setAttribute("fill", "#ff00d4ff"); + nodes[2]->setAttribute("stroke", "#ff00d4ff"); + nodes[3]->setAttribute("fill", "#ff0000ff"); + nodes[3]->setAttribute("stroke", "#ff70ffff"); + nodes[4]->setAttribute("fill", "#00ff00ff"); + nodes[4]->setAttribute("stroke", "#ba6cd2ff"); + nodes[5]->setAttribute("fill", "url(#test-gradient)"); + + _doc->getDefs()->getRepr()->appendChild(grad_node); + + for (int i = 0; i < 6; i++) { + _doc->getRoot()->getRepr()->appendChild(nodes[i]); + vector.push_back(_doc->getObjectByRepr(nodes[i])); + } + + set = collect_colours(vector); + } + + ~ObjectColorSetFixture() + { + TearDownTestCase(); + } +}; + +TEST(ObjectColorSet, HandleEmptyObjects) +{ + EXPECT_TRUE(collect_colours({}).isColorsEmpty()); +} + +TEST(ObjectColorSet, HandleNullObjects) +{ + EXPECT_TRUE(collect_colours({nullptr}).isColorsEmpty()); +} + +TEST_F(ObjectColorSetFixture, PopulateAndFindColor) +{ + EXPECT_FALSE(set.isColorsEmpty()); + EXPECT_FALSE(set.isGradientStopsEmpty()); + EXPECT_EQ(set.getColors().size(), 9); + + auto key = Colors::Color(0xffff00ff).toRGBA(); + + EXPECT_EQ(set.getColorIndex(key), 0); + EXPECT_EQ(set.getColor(0).value().toRGBA(), key); + + auto false_key = Colors::Color(0x000000ff).toRGBA(); + EXPECT_EQ(set.getColorIndex(false_key), -1); +} + +TEST_F(ObjectColorSetFixture, ClearData) +{ + EXPECT_EQ(set.getColors().size(), 9); + set.clearData(); + EXPECT_TRUE(set.isColorsEmpty()); + EXPECT_TRUE(set.isGradientStopsEmpty()); +} + +TEST_F(ObjectColorSetFixture, SetAndGetSelectedColors) +{ + Colors::Color new_color(0xff00ffff); + auto key = Colors::Color(0xffff00ff).toRGBA(); + set.setSelectedNewColor(key, new_color); + EXPECT_EQ(set.getSelectedNewColor(key).value().toRGBA(), new_color.toRGBA()); +} + +TEST_F(ObjectColorSetFixture, SetSelectedNewColors) +{ + std::vector colors{Colors::Color(Colors::Space::Type::CMYK, {0.1, 0.8, 0.0, 0.0}), + Colors::Color(0xff0000ff), + Colors::Color(0x00ff00ff), + Colors::Color(0x0000ffff), + Colors::Color(0x7e1a9cff), + Colors::Color(Colors::Space::Type::HSLUV, {120.0, 100.0, 50.0}), + Colors::Color(Colors::Space::Type::HSL, {0.33, 1.0, 0.5}), + Colors::Color(Colors::Space::Type::HSV, {0.66, 1.0, 1.0}), + Colors::Color(Colors::Space::Type::LAB, {60.0, -40.0, 30.0})}; + set.setSelectedNewColor(colors); + std::vector new_colors; + for (auto [key, value] : set.getSelectedColorsMap()) { + new_colors.push_back(value.second.value().new_color.toRGBA()); + } + std::vector color{ + Colors::Color(Colors::Space::Type::CMYK, {0.1, 0.8, 0.0, 0.0}).toRGBA(), + Colors::Color(0xff0000ff).toRGBA(), + Colors::Color(0x00ff00ff).toRGBA(), + Colors::Color(0x0000ffff).toRGBA(), + Colors::Color(0x7e1a9cff).toRGBA(), + Colors::Color(Colors::Space::Type::HSLUV, {120.0, 100.0, 50.0}).toRGBA(), + Colors::Color(Colors::Space::Type::HSL, {0.33, 1.0, 0.5}).toRGBA(), + Colors::Color(Colors::Space::Type::HSV, {0.66, 1.0, 1.0}).toRGBA(), + Colors::Color(Colors::Space::Type::LAB, {60.0, -40.0, 30.0}).toRGBA()}; + sort(color.begin(), color.end()); + sort(new_colors.begin(), new_colors.end()); + EXPECT_EQ(color, new_colors); + + set.convertToRecoloredColors(); + EXPECT_EQ(vector[0]->style->fill.getColor().toRGBA(), Colors::Color(Colors::Space::Type::CMYK, {0.1, 0.8, 0.0, 0.0}).toRGBA()); + char const *style1 = nodes[0]->attribute("style"); + EXPECT_NE(strstr(style1, "fill:device-cmyk(0.1 0.8 0 0)"), nullptr); + + // test reseting just object color without reseting it in the map for livepreview purposes + set.revertToOriginalColors(false); + EXPECT_EQ(vector[0]->style->fill.getColor().toRGBA(), Colors::Color(0xffff00ff).toRGBA()); + char const *style2 = nodes[0]->attribute("style"); + EXPECT_NE(strstr(style2, "fill:#ffff00ff"), nullptr); + auto map1 = set.getSelectedColorsMap(); + auto value1 = map1[Colors::Color(0xffff00ff).toRGBA()].second.value().new_color.toRGBA(); + EXPECT_NE(value1, Colors::Color(0xffff00ff).toRGBA()); + + // test with resetting map entry for reset button + set.revertToOriginalColors(true); + auto map2 = set.getSelectedColorsMap(); + auto value2 = map2[Colors::Color(0xffff00ff).toRGBA()].second.value().new_color.toRGBA(); + EXPECT_EQ(value2, Colors::Color(0xffff00ff).toRGBA()); +} + +TEST_F(ObjectColorSetFixture, ChangeObjectsColors) +{ + EXPECT_FALSE(set.isColorsEmpty()); + + EXPECT_FALSE(set.isGradientStopsEmpty()); + EXPECT_EQ(set.getColors().size(), 9); + + set.applyNewColorToSelection(Colors::Color(0xffff00ff).toRGBA(), Colors::Color(0x7e1a9cff)); + EXPECT_EQ(vector[0]->style->fill.getColor().toRGBA(), Colors::Color(0x7e1a9cff).toRGBA()); + EXPECT_EQ(vector[1]->style->stroke.getColor().toRGBA(), Colors::Color(0x7e1a9cff).toRGBA()); + + char const *style1 = nodes[0]->attribute("style"); + char const *style2 = nodes[1]->attribute("style"); + EXPECT_NE(strstr(style1, "fill:#7e1a9cff"), nullptr); + EXPECT_NE(strstr(style2, "stroke:#7e1a9cff"), nullptr); +} + +TEST_F(ObjectColorSetFixture, HandleLargeColorSets) +{ + set.clearData(); + Inkscape::XML::Document *xml_doc = _doc->getReprDoc(); + std::vector large_vector; + + for (int i = 0; i < 100000; ++i) { + Inkscape::XML::Node *rect = xml_doc->createElement("svg:rect"); + char color_str[16]; + snprintf(color_str, sizeof(color_str), "#%06xff", i % 0xFFFFFF); + rect->setAttribute("fill", color_str); + + _doc->getDefs()->getRepr()->appendChild(rect); + large_vector.push_back(_doc->getObjectByRepr(rect)); + } + + set = collect_colours(large_vector); + EXPECT_EQ(set.getColors().size(), 100000); +} + +TEST_F(ObjectColorSetFixture, TestColorIndexBoundaryConditions) +{ + EXPECT_EQ(set.getColor(-1), std::nullopt); + EXPECT_EQ(set.getColor(set.getColors().size()), std::nullopt); + EXPECT_EQ(set.getColorIndex(0x99999999), -1); +} + +TEST_F(ObjectColorSetFixture, TestColorApplicationFailure) +{ + auto false_key = Colors::Color(0x99999999).toRGBA(); + EXPECT_FALSE(set.applyNewColorToSelection(false_key, Colors::Color(0xff0000ff))); +} -- GitLab From 1f1ef1534282876513ded53f4812b149b339a7c2 Mon Sep 17 00:00:00 2001 From: PBS Date: Sat, 25 Oct 2025 01:13:32 +0200 Subject: [PATCH 2/2] Fix popover and lifetime issues in Recolor widget Fixes a crash on exit, and separately, GTK popover warnings on exit. Allows dismissing the popover by clicking the recolor button again. * Use Gtk::MenuButton instead of Gtk::Button. Fixes GTK warnings on exit. * Only change Recolor widget's desktop on showing it. * Don't block F&S updates in PaintSelector::setMode(). * Avoid string keys in has_colors_pattern, and quit quickly as soon as >= 2 colours found. * Make RecolorArtManager's widget and popover public, as it has no invariants to protect. * Repurpose performUpdate() -> showForSelection(). This function now tells the RecolorArt widget to stick to the desktop's selection, updating with it. * Repurpose performMarkerUpdate() -> showForObject(). Allow it to work for any object, not just markers. * Make RecolorArt responsible for disconnecting from desktop on destruction, rather than the containing widget. * Avoid use of _values in MultiMarkerWheel. A multi-colour wheel has a vector of colours, not a single colour, so it doesn't make sense. Consequently setColor() becomes a no-op as it is never called. * Discard _builder after construction, remove idle connection. * Simplify map/unmap handling through setDesktop() function. --- src/ui/widget/ink-color-wheel.cpp | 16 +-- src/ui/widget/marker-combo-box.cpp | 36 ++---- src/ui/widget/marker-combo-box.h | 3 +- src/ui/widget/paint-selector.cpp | 85 ++++---------- src/ui/widget/paint-selector.h | 9 +- src/ui/widget/recolor-art-manager.cpp | 157 ++++++++++++++------------ src/ui/widget/recolor-art-manager.h | 14 +-- src/ui/widget/recolor-art.cpp | 132 +++++++++++----------- src/ui/widget/recolor-art.h | 21 ++-- 9 files changed, 206 insertions(+), 267 deletions(-) diff --git a/src/ui/widget/ink-color-wheel.cpp b/src/ui/widget/ink-color-wheel.cpp index 4ae41d7380..2d71f54bd0 100644 --- a/src/ui/widget/ink-color-wheel.cpp +++ b/src/ui/widget/ink-color-wheel.cpp @@ -760,21 +760,9 @@ bool ColorWheelHSLuv::setColor(Color const &color, /* Multi-marker Color Wheel */ -/** - * takes the color parameter and push it into the _values_vectors vector - * them if emit is true it calls the color_changed() to emit signal changed then queue redraw the area - */ -bool MultiMarkerWheel::setColor(Color const &color, bool /*overrideHue*/, bool emit) +bool MultiMarkerWheel::setColor(Color const &, bool, bool) { - if (_values.set(color, true)) { - _values_vector.push_back(_values); - if (emit) { - color_changed(); - } else { - queue_drawing_area_draw(); - } - return true; - } + // Doesn't make sense to set the colour for a multi-colour wheel. return false; } diff --git a/src/ui/widget/marker-combo-box.cpp b/src/ui/widget/marker-combo-box.cpp index f32611f717..e5bf8836be 100644 --- a/src/ui/widget/marker-combo-box.cpp +++ b/src/ui/widget/marker-combo-box.cpp @@ -159,7 +159,7 @@ MarkerComboBox::MarkerComboBox(Glib::ustring id, int l) : _orient_angle(get_widget(_builder, "orient-angle")), _orient_flip_horz(get_widget(_builder, "btn-horz-flip")), _edit_marker(get_widget(_builder, "edit-marker")), - _recolorButtonTrigger(Gtk::make_managed()) + _recolorButtonTrigger(Gtk::make_managed()) { set_name("MarkerComboBox"); @@ -353,28 +353,15 @@ MarkerComboBox::MarkerComboBox(Glib::ustring id, int l) : _recolorButtonTrigger->set_halign(Gtk::Align::FILL); _recolorButtonTrigger->set_valign(Gtk::Align::START); _recolorButtonTrigger->set_margin_top(8); - + _recolorButtonTrigger->set_direction(Gtk::ArrowType::NONE); + _recolorButtonTrigger->set_visible(false); _grid.add_full_row(_recolorButtonTrigger); - _recolorButtonTrigger->signal_clicked().connect([this] { - if (!_recolorManager) { - // Lazy-load the recolour widget and popover. - _recolorManager = &RecolorArtManager::get(); - if (_recolorManager->getPopOver().get_parent()) { - _recolorManager->getPopOver().unparent(); - } - _recolorManager->getPopOver().set_parent(*_recolorButtonTrigger); - _recolorManager->setDesktop(_desktop); - } else if (_recolorManager->getPopOver().get_parent() != _recolorButtonTrigger) { - // Reparent the popover to this button if necessary. - _recolorManager->getPopOver().unparent(); - _recolorManager->getPopOver().set_parent(*_recolorButtonTrigger); - } - _recolorManager->getPopOver().popup(); - _recolorManager->performMarkerUpdate(get_current()); + _recolorButtonTrigger->set_create_popup_func([this] { + auto &mgr = RecolorArtManager::get(); + mgr.reparentPopoverTo(*_recolorButtonTrigger); + mgr.widget.showForObject(_desktop, get_current()); }); - - _recolorButtonTrigger->hide(); } void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) { @@ -516,16 +503,11 @@ void MarkerComboBox::setDesktop(SPDesktop *desktop) return; } - if (_recolorManager) { - _recolorManager->getPopOver().popdown(); - } + RecolorArtManager::get().popover.popdown(); _desktop = desktop; - - if (_recolorManager) { - _recolorManager->setDesktop(_desktop); - } } + void MarkerComboBox::setDocument(SPDocument *document) { if (_document != document) { diff --git a/src/ui/widget/marker-combo-box.h b/src/ui/widget/marker-combo-box.h index cadb5c5874..1493578b9e 100644 --- a/src/ui/widget/marker-combo-box.h +++ b/src/ui/widget/marker-combo-box.h @@ -130,10 +130,9 @@ private: WidgetGroup _widgets; Gtk::CellRendererPixbuf _image_renderer; - UI::Widget::RecolorArtManager* _recolorManager = nullptr; SPDesktop *_desktop = nullptr; sigc::scoped_connection _selection_changed_connection; - Gtk::Button *_recolorButtonTrigger = nullptr; + Gtk::MenuButton *_recolorButtonTrigger = nullptr; class MarkerColumns : public Gtk::TreeModel::ColumnRecord { diff --git a/src/ui/widget/paint-selector.cpp b/src/ui/widget/paint-selector.cpp index c124c5c318..188a9ee6fd 100644 --- a/src/ui/widget/paint-selector.cpp +++ b/src/ui/widget/paint-selector.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "desktop-style.h" #include "desktop.h" @@ -128,7 +129,7 @@ PaintSelector::PaintSelector(FillOrStroke kind, std::shared_ptr(-1); // huh? do you mean 0xff? -- I think this means "not in the enum" for (int i = 0; i < 5; i++) { - _recolorButtonTrigger[i] = std::make_unique(); + _recolorButtonTrigger[i] = std::make_unique(); } /* Paint style button box */ @@ -209,25 +210,13 @@ PaintSelector::PaintSelector(FillOrStroke kind, std::shared_ptrset_halign(Gtk::Align::CENTER); b->set_valign(Gtk::Align::START); b->set_margin_top(8); + b->set_direction(Gtk::ArrowType::NONE); b->set_visible(false); - b->signal_clicked().connect([b = b.get(), this] { - auto guard = _blocker.block(); - if (!_recolorManager) { - // Lazy-load the recolour widget and popover. - _recolorManager = &RecolorArtManager::get(); - if (_recolorManager->getPopOver().get_parent()) { - _recolorManager->getPopOver().unparent(); - } - _recolorManager->getPopOver().set_parent(*b); - _recolorManager->setDesktop(_desktop); - } else if (_recolorManager->getPopOver().get_parent() != b) { - // Reparent the popover to this button if necessary. - _recolorManager->getPopOver().unparent(); - _recolorManager->getPopOver().set_parent(*b); - } - _recolorManager->getPopOver().popup(); - _recolorManager->performUpdate(); + b->set_create_popup_func([b = b.get(), this] { + auto &mgr = RecolorArtManager::get(); + mgr.reparentPopoverTo(*b); + mgr.widget.showForSelection(_desktop); }); } @@ -239,10 +228,8 @@ void PaintSelector::setDesktop(SPDesktop *desktop) if (_desktop == desktop) { return; } - - if (_recolorManager) { - _recolorManager->getPopOver().popdown(); - } + + RecolorArtManager::get().popover.popdown(); if (_selection_changed_connection) { _selection_changed_connection.disconnect(); @@ -256,10 +243,6 @@ void PaintSelector::setDesktop(SPDesktop *desktop) selection->connectChanged(sigc::mem_fun(*this, &PaintSelector::onSelectionChanged)); } } - - if (_recolorManager) { - _recolorManager->setDesktop(_desktop); - } } StyleToggleButton *PaintSelector::style_button_add(gchar const *pixmap, PaintSelector::Mode mode, gchar const *tip) @@ -299,9 +282,6 @@ void PaintSelector::fillrule_toggled(FillRuleRadioButton *tb) void PaintSelector::setMode(Mode mode) { - if (_recolorManager && _recolorManager->getPopOver().get_visible() && checkSelection(_desktop->getSelection())) { - return; - } set_mode_ex(mode, false); } @@ -1217,50 +1197,27 @@ PaintSelector::Mode PaintSelector::getModeForStyle(SPStyle const &style, FillOrS void PaintSelector::onSelectionChanged(Inkscape::Selection *selection) { - if (_blocker.pending()) { - return; - } + bool show_recolor = (_mode == MODE_GRADIENT_MESH && RecolorArtManager::checkMeshObject(selection)) || + RecolorArtManager::checkSelection(selection); + + int btn_index = -1; - if (checkSelection(selection)) { + if (show_recolor) { if (_mode == MODE_MULTIPLE || _mode == MODE_UNSET || _mode == MODE_GRADIENT_MESH) { - hideAllExcept(_recolorButtonTrigger[0].get()); + btn_index = 0; } else if (_mode == MODE_SOLID_COLOR) { - hideAllExcept(_recolorButtonTrigger[1].get()); + btn_index = 1; } else if (_mode == MODE_GRADIENT_RADIAL || _mode == MODE_GRADIENT_LINEAR) { - hideAllExcept(_recolorButtonTrigger[2].get()); + btn_index = 2; } else if (_mode == MODE_PATTERN) { - hideAllExcept(_recolorButtonTrigger[3].get()); + btn_index = 3; } else if (_mode == MODE_SWATCH) { - hideAllExcept(_recolorButtonTrigger[4].get()); - } else { - hideAllExcept(); + btn_index = 4; } - } else { - hideAllExcept(); - } - - if (_recolorManager && _recolorManager->getPopOver().get_visible() && checkSelection(selection)) { - auto guard = _blocker.block(); - _recolorManager->performUpdate(); - } -} - -bool PaintSelector::checkSelection(Inkscape::Selection *selection) -{ - return (_mode == MODE_GRADIENT_MESH && (selection->size() > 1 || RecolorArtManager::checkMeshObject(selection))) || - RecolorArtManager::checkSelection(selection); -} - -void PaintSelector::hideAllExcept(Gtk::Button *recolorButtonTrigger) -{ - if (recolorButtonTrigger) { - recolorButtonTrigger->show(); } - for (auto const &b : _recolorButtonTrigger) { - if (b.get() != recolorButtonTrigger) { - b->hide(); - } + for (int i = 0; i < _recolorButtonTrigger.size(); i++) { + _recolorButtonTrigger[i]->set_visible(i == btn_index); } } diff --git a/src/ui/widget/paint-selector.h b/src/ui/widget/paint-selector.h index b17b44e273..1e49d9dd75 100644 --- a/src/ui/widget/paint-selector.h +++ b/src/ui/widget/paint-selector.h @@ -16,8 +16,9 @@ #ifndef SEEN_SP_PAINT_SELECTOR_H #define SEEN_SP_PAINT_SELECTOR_H +#include + #include "fill-or-stroke.h" -#include "ui/operation-blocker.h" #include "ui/widget/gradient-selector.h" #include "ui/widget/swatch-selector.h" #include "selection.h" @@ -102,13 +103,9 @@ class PaintSelector : public Gtk::Box { SwatchSelector *_selector_swatch = nullptr; PatternEditor* _selector_pattern = nullptr; - UI::Widget::RecolorArtManager *_recolorManager = nullptr; - std::array, 5> _recolorButtonTrigger; - OperationBlocker _blocker; + std::array, 5> _recolorButtonTrigger; SPDesktop *_desktop = nullptr; void onSelectionChanged(Inkscape::Selection *selection); - bool checkSelection(Inkscape::Selection *selection); - void hideAllExcept(Gtk::Button *recolorButtonTrigger = nullptr); Gtk::Label *_label; GtkWidget *_patternmenu = nullptr; bool _patternmenu_update = false; diff --git a/src/ui/widget/recolor-art-manager.cpp b/src/ui/widget/recolor-art-manager.cpp index dfb2c45dbc..8780d3a25c 100644 --- a/src/ui/widget/recolor-art-manager.cpp +++ b/src/ui/widget/recolor-art-manager.cpp @@ -8,8 +8,9 @@ #include "recolor-art-manager.h" +#include + #include "object/sp-gradient.h" -#include "object/sp-mask.h" #include "object/sp-pattern.h" #include "object/sp-use.h" #include "style.h" @@ -19,51 +20,54 @@ namespace { bool has_colors_pattern(SPItem const *item) { - std::set colors; - SPPattern *patternstroke = nullptr; - SPPattern *patternfill = nullptr; - SPPattern *pattern = nullptr; - if (item && item->style) { - patternstroke = cast(item->style->getStrokePaintServer()); - patternfill = cast(item->style->getFillPaintServer()); + if (!item || !item->style) { + return false; } - if (patternstroke) - pattern = patternstroke; - if (patternfill) - pattern = patternfill; - if (!pattern) - return false; - SPPattern *root = pattern->rootPattern(); - for (auto &child : root->children) { - if (auto group = cast(&child)) { - for (auto &child : group->children) { - if (auto c = dynamic_cast(&child)) { - if (c->style->fill.isColor()) { - std::string rgba = c->style->fill.getColor().toString(true); - colors.insert(rgba); - } - if (c->style->stroke.isColor()) { - std::string rgba = c->style->stroke.getColor().toString(true); - colors.insert(rgba); + std::optional first_col; + + // Return true when a second colour is found. + auto check_color = [&] (SPIPaint const &paint) { + if (!paint.isColor()) { + return false; + } + + if (!first_col) { + first_col = paint.getColor(); + return false; + } else { + return paint.getColor() != first_col; + } + }; + + // Search a pattern for colours, returning true when a second colour is found. + auto search_pattern = [&] (SPPaintServer const *ps) { + auto pat = cast(ps); + if (!pat) { + return false; + } + + for (auto const &child : pat->rootPattern()->children) { + if (auto group = cast(&child)) { + for (auto const &child : group->children) { + if (auto c = cast(&child)) { + if (check_color(c->style->fill) || check_color(c->style->stroke)) { + return true; + } } } } - } - auto item = cast(&child); - if (!item || !item->style) - continue; - if (item->style->fill.isColor()) { - std::string rgba = item->style->fill.getColor().toString(true); - colors.insert(rgba); - } - if (item->style->stroke.isColor()) { - std::string rgba = item->style->stroke.getColor().toString(true); - colors.insert(rgba); + if (check_color(child.style->fill) || check_color(child.style->stroke)) { + return true; + } } - } - return colors.size() > 1; + + return false; + }; + + return search_pattern(item->style->getFillPaintServer()) || + search_pattern(item->style->getStrokePaintServer()); } } // namespace @@ -74,56 +78,63 @@ RecolorArtManager &RecolorArtManager::get() return instance; } +void RecolorArtManager::reparentPopoverTo(Gtk::MenuButton &button) +{ + if (popover.get_parent() == &button) { + return; + } + + if (auto oldbutton = dynamic_cast(popover.get_parent())) { + oldbutton->unset_popover(); + } + + button.set_popover(popover); + + // The previous call causes GTK to reset the popover direction to down. Override it to left. + popover.set_position(Gtk::PositionType::LEFT); +} + RecolorArtManager::RecolorArtManager() { - _recolorPopOver.set_autohide(false); - _recolorPopOver.set_position(Gtk::PositionType::LEFT); - _recolorPopOver.set_child(_recolor_widget); + popover.set_autohide(false); + popover.set_child(widget); } bool RecolorArtManager::checkSelection(Inkscape::Selection *selection) { - auto group = cast(selection->single()); - auto use_group = cast(selection->single()); - auto item = cast(selection->single()); - bool pattern_colors = false; - SPMask *mask = nullptr; - if (item) { - mask = cast(item->getMaskObject()); - pattern_colors = has_colors_pattern(item); + if (selection->size() > 1) { + return true; } - return selection->size() > 1 || group || use_group || mask || pattern_colors; -} -bool RecolorArtManager::checkMeshObject(Inkscape::Selection *selection) -{ - if (selection->items().empty()) { + auto item = selection->singleItem(); + if (!item) { return false; } - auto fill_gradient = cast(selection->single()->style->getFillPaintServer()); - auto stroke_gradient = cast(selection->single()->style->getStrokePaintServer()); - SPGradient *gradient = fill_gradient ? cast(fill_gradient) : cast(stroke_gradient); - return gradient && gradient->hasPatches(); -} -void RecolorArtManager::setDesktop(SPDesktop *desktop) -{ - _recolor_widget.setDesktop(desktop); + return is(item) || + is(item) || + item->getMaskObject() || + has_colors_pattern(item); } -void RecolorArtManager::performUpdate() +bool RecolorArtManager::checkMeshObject(Inkscape::Selection *selection) { - _recolor_widget.performUpdate(); -} + if (selection->size() > 1) { + return true; + } -void RecolorArtManager::performMarkerUpdate(SPMarker *marker) -{ - _recolor_widget.performMarkerUpdate(marker); -} + auto item = selection->singleItem(); + if (!item) { + return false; + } -Gtk::Popover &RecolorArtManager::getPopOver() -{ - return _recolorPopOver; + auto is_mesh = [] (SPPaintServer *ps) { + auto grad = cast(ps); + return grad && grad->hasPatches(); + }; + + return is_mesh(item->style->getFillPaintServer()) || + is_mesh(item->style->getStrokePaintServer()); } } // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/recolor-art-manager.h b/src/ui/widget/recolor-art-manager.h index be30750e56..0726aecf75 100644 --- a/src/ui/widget/recolor-art-manager.h +++ b/src/ui/widget/recolor-art-manager.h @@ -14,6 +14,8 @@ #include "ui/widget/recolor-art.h" #include "selection.h" +namespace Gtk { class MenuButton; } + namespace Inkscape::UI::Widget { class RecolorArtManager @@ -21,20 +23,16 @@ class RecolorArtManager public: static RecolorArtManager &get(); - Gtk::Popover &getPopOver(); + RecolorArt widget; + Gtk::Popover popover; + + void reparentPopoverTo(Gtk::MenuButton &button); static bool checkSelection(Inkscape::Selection *selection); static bool checkMeshObject(Inkscape::Selection *selection); - void setDesktop(SPDesktop *desktop); - void performUpdate(); - void performMarkerUpdate(SPMarker *marker); - private: RecolorArtManager(); - - RecolorArt _recolor_widget; - Gtk::Popover _recolorPopOver; }; } // namespace Inkscape::UI::Widget diff --git a/src/ui/widget/recolor-art.cpp b/src/ui/widget/recolor-art.cpp index cfe623c258..f5e1303ee4 100644 --- a/src/ui/widget/recolor-art.cpp +++ b/src/ui/widget/recolor-art.cpp @@ -22,8 +22,6 @@ #include "seltrans.h" #include "selection.h" #include "ui/tools/select-tool.h" -#include "object/sp-marker.h" - namespace Inkscape::UI::Widget { @@ -38,39 +36,25 @@ namespace Inkscape::UI::Widget { */ RecolorArt::RecolorArt() - : _builder{create_builder("widget-recolor.ui")} - , _notebook(*_builder->get_widget("list-wheel-box")) - , _color_wheel_page(*_builder->get_widget("color-wheel-page")) + : RecolorArt{create_builder("widget-recolor.ui")} +{} + +RecolorArt::RecolorArt(Glib::RefPtr const &builder) + : _color_picker_container{get_widget(builder, "color-picker")} + , _notebook(get_widget(builder, "list-wheel-box")) + , _color_wheel_page(get_widget(builder, "color-wheel-page")) , _color_wheel(Gtk::make_managed(Colors::ColorSet{})) - , _color_list(*_builder->get_widget("colors-list")) - , _reset(*_builder->get_widget("reset")) - , _live_preview(*_builder->get_widget("liveP")) + , _color_list(get_widget(builder, "colors-list")) + , _reset(get_widget(builder, "reset")) + , _live_preview(get_widget(builder, "liveP")) + , _list_view{get_widget(builder, "recolor-art-list")} { set_name("RecolorArt"); - append(get_widget(_builder, "recolor-art")); + append(get_widget(builder, "recolor-art")); _solid_colors->set(Color(0x000000ff)); - // when recolor widget is closed it resets opacity to mitigate the effect of getSelection function - // and reshow selection boxes again - signal_unmap().connect([&]() { - if (!_is_preview) { - _manager.convertToRecoloredColors(); - DocumentUndo::done(_desktop->getDocument(), _("changed Item color"), - INKSCAPE_ICON("object-recolor-art")); - } - if (_desktop) { - _desktop->setHideSelectionBoxes(false); - } - }); - // hide selection boxes after widget gets mapped (this is why it connects to signal idle to activate after finishing - // mapping - signal_map().connect([&]() { - Glib::signal_idle().connect([this]() { - if (_desktop) { - _desktop->setHideSelectionBoxes(true); - } - return false; - }); - }); + // when recolor widget is closed it detaches from desktop, ending session + signal_unmap().connect([this] { setDesktop(nullptr); }); + _color_wheel->connect_color_changed(static_cast>([this]() { if(_blocker.pending()) { return; // to stop recursive calling to signal if changed from the color list page @@ -119,7 +103,6 @@ RecolorArt::RecolorArt() _reset.signal_clicked().connect(sigc::mem_fun(*this, &RecolorArt::onResetClicked)); // setting up list view for the color list - _list_view = _builder->get_widget("recolor-art-list"); _color_model = Gio::ListStore::create(); _selection_model = Gtk::SingleSelection::create(_color_model); _color_factory = Gtk::SignalListItemFactory::create(); @@ -219,15 +202,15 @@ RecolorArt::RecolorArt() } }); - _list_view->set_model(_selection_model); - _list_view->set_factory(_color_factory); + _list_view.set_model(_selection_model); + _list_view.set_factory(_color_factory); - auto lm = _list_view->get_layout_manager(); + auto lm = _list_view.get_layout_manager(); if (auto grid_layout = std::dynamic_pointer_cast(lm)) { grid_layout->set_row_spacing(0); } - _list_view->set_hexpand(false); - _list_view->set_vexpand(false); + _list_view.set_hexpand(false); + _list_view.set_vexpand(false); _selection_model->signal_selection_changed().connect([this](guint pos, guint n_items) { int index = _selection_model->get_selected(); @@ -249,12 +232,33 @@ RecolorArt::RecolorArt() void RecolorArt::setDesktop(SPDesktop *desktop) { - if (_desktop != desktop) { - _desktop = desktop; - _color_wheel->toggleHueLock(false); - _color_wheel->setLightness(100.0); - _color_wheel->setSaturation(100.0); + if (_desktop == desktop) { + return; + } + + if (_desktop) { + _sel_changed_conn.disconnect(); + _desktop_destroyed_conn.disconnect(); + + _desktop->setHideSelectionBoxes(false); + + if (!_is_preview) { + _manager.convertToRecoloredColors(); + DocumentUndo::done(_desktop->getDocument(), _("Change item color"), INKSCAPE_ICON("object-recolor-art")); + } + } + + _desktop = desktop; + + if (_desktop) { + _desktop->setHideSelectionBoxes(true); + + _desktop_destroyed_conn = _desktop->connectDestroy([this] (auto) { + setDesktop(nullptr); + }); } + + set_sensitive(_desktop); } /* @@ -287,15 +291,10 @@ void RecolorArt::layoutColorPicker(std::shared_ptr updated_col _solid_colors->signal_changed.connect([this]() { onColorPickerChanged(); }); - auto container = _builder->get_widget("color-picker"); - if (container) { - for (auto child : container->get_children()) { - container->remove(*child); - } - container->append(*_color_picker_wdgt); - } else { - g_warning("color picker not found"); + for (auto child : _color_picker_container.get_children()) { + _color_picker_container.remove(*child); } + _color_picker_container.append(*_color_picker_wdgt); } /* @@ -561,6 +560,18 @@ void RecolorArt::onLivePreviewToggled() } } +void RecolorArt::showForSelection(SPDesktop *desktop) +{ + assert(desktop); + + setDesktop(desktop); + _sel_changed_conn = _desktop->getSelection()->connectChanged([this] (auto) { + updateFromSelection(); + }); + + updateFromSelection(); +} + /* * main function that : * 1- clears old data @@ -569,15 +580,11 @@ void RecolorArt::onLivePreviewToggled() * 4- call collect colors func * 5- put the generated list in the UI */ -void RecolorArt::performUpdate() +void RecolorArt::updateFromSelection() { if (_selection_blocker.pending()) { return; } - if (!_desktop) { - g_warning("Desktop is NULL in Performupdate in recolor widget"); - return; - } _manager.clearData(); @@ -600,15 +607,12 @@ void RecolorArt::performUpdate() } } -void RecolorArt::performMarkerUpdate(SPMarker *marker) +void RecolorArt::showForObject(SPDesktop *desktop, SPObject *object) { - if (!marker) { - return; - } - if (!_desktop) { - g_warning("Desktop is NULL in Performupdate in recolor widget"); - return; - } + assert(desktop); + assert(object); + + setDesktop(desktop); _manager.clearData(); @@ -616,7 +620,7 @@ void RecolorArt::performMarkerUpdate(SPMarker *marker) _color_wheel->setLightness(100.0); _color_wheel->setSaturation(100.0); - _manager = collect_colours({marker}); + _manager = collect_colours({object}); if (!_manager.isColorsEmpty()) { generateVisualList(); auto first_button_id = _manager.getFirstKey(); diff --git a/src/ui/widget/recolor-art.h b/src/ui/widget/recolor-art.h index 6f0f8cf228..ffd2bb3fc2 100644 --- a/src/ui/widget/recolor-art.h +++ b/src/ui/widget/recolor-art.h @@ -26,8 +26,6 @@ #include "ui/operation-blocker.h" #include "ui/widget/ink-color-wheel.h" -class SPMarker; - namespace Inkscape::Colors { class Color; class ColorSet; @@ -67,24 +65,26 @@ class RecolorArt : public Gtk::Box public: RecolorArt(); - void performUpdate(); - bool isInPreviewMode() const { return _is_preview; } - void setDesktop(SPDesktop *desktop); + void showForSelection(SPDesktop *desktop); + void showForObject(SPDesktop *desktop, SPObject *object); + void onResetClicked(); - void performMarkerUpdate(SPMarker *marker); private: + RecolorArt(Glib::RefPtr const &builder); + SPDesktop *_desktop = nullptr; - Glib::RefPtr _builder; + sigc::scoped_connection _sel_changed_conn; + sigc::scoped_connection _desktop_destroyed_conn; + Gtk::Box &_color_picker_container; Gtk::Notebook &_notebook; Gtk::Box &_color_wheel_page; std::shared_ptr _solid_colors = std::make_shared(); - sigc::connection _solid_color_changed; Gtk::Box &_color_list; Gtk::Button &_reset; Gtk::CheckButton &_live_preview; + Gtk::ListView &_list_view; Inkscape::UI::Widget::ColorNotebook *_color_picker_wdgt = nullptr; - Gtk::ListView *_list_view = nullptr; uint32_t _current_color_id; bool _is_preview = true; @@ -99,6 +99,9 @@ private: OperationBlocker _blocker; OperationBlocker _selection_blocker; + void setDesktop(SPDesktop *desktop); + void updateFromSelection(); + void generateVisualList(); void layoutColorPicker(std::shared_ptr updated_color = nullptr); void colorButtons(Gtk::Box *button, Colors::Color color, bool is_original = false); -- GitLab