diff --git a/po/POTFILES.src.in b/po/POTFILES.src.in index b227ec488bf11c7813eb09b040f0f4eed95672d4..d9c3bdfbb19c7a043a2b125b01b84f511c6820f4 100644 --- a/po/POTFILES.src.in +++ b/po/POTFILES.src.in @@ -425,6 +425,7 @@ ${_build_dir}/share/templates/templates.h ../src/ui/widget/memory.cpp ../src/ui/widget/mesh-editor.cpp ../src/ui/widget/messages.cpp +../src/ui/widget/multi-marker-color-plate.cpp ../src/ui/widget/object-composite-settings.cpp ../src/ui/widget/page-properties.cpp ../src/ui/widget/page-selector.cpp @@ -435,6 +436,7 @@ ${_build_dir}/share/templates/templates.h ../src/ui/widget/preferences-widget.cpp ../src/ui/widget/property-utils.cpp ../src/ui/widget/random.cpp +../src/ui/widget/recolor-art.cpp ../src/ui/widget/registered-widget.cpp ../src/ui/widget/rendering-options.cpp ../src/ui/widget/selected-style.cpp diff --git a/po/POTFILES.ui.in b/po/POTFILES.ui.in index 64483fff91eb40edab6e04193a4317d16912478c..26dd7fecf7f5b1ec594a909dfda59aadd534a4d2 100644 --- a/po/POTFILES.ui.in +++ b/po/POTFILES.ui.in @@ -72,3 +72,4 @@ ../share/ui/toolbar-tool-prefs.ui ../share/ui/toolbar-tweak.ui ../share/ui/toolbar-zoom.ui +../share/ui/widget-recolor.ui 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 0000000000000000000000000000000000000000..5d08323d90b8625acee64e431693cc7e5df4dc6a --- /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 0000000000000000000000000000000000000000..1e8d4bc38e00616b6ad24fb329583bbf39a72f27 --- /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 0000000000000000000000000000000000000000..b078cf421c5c1beec4852445a720247bf9c2827c --- /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 0000000000000000000000000000000000000000..7d47572893ed89f875798b4b0e353615b756fc70 --- /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 0000000000000000000000000000000000000000..7f0b0fda5a8f13cc8345ac01a092b97f67785489 --- /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 0000000000000000000000000000000000000000..5d08323d90b8625acee64e431693cc7e5df4dc6a --- /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 0000000000000000000000000000000000000000..1e8d4bc38e00616b6ad24fb329583bbf39a72f27 --- /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 0000000000000000000000000000000000000000..b078cf421c5c1beec4852445a720247bf9c2827c --- /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 0000000000000000000000000000000000000000..7d47572893ed89f875798b4b0e353615b756fc70 --- /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 7dcef2b3f70fffc0d4e14bd1fdd4381979bf9177..09c40a1e3021e4ad13a7d5a0086ef0a972d948e6 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 0000000000000000000000000000000000000000..7f0b0fda5a8f13cc8345ac01a092b97f67785489 --- /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 0000000000000000000000000000000000000000..5d08323d90b8625acee64e431693cc7e5df4dc6a --- /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 0000000000000000000000000000000000000000..1e8d4bc38e00616b6ad24fb329583bbf39a72f27 --- /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 0000000000000000000000000000000000000000..b078cf421c5c1beec4852445a720247bf9c2827c --- /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 0000000000000000000000000000000000000000..7d47572893ed89f875798b4b0e353615b756fc70 --- /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 7dcef2b3f70fffc0d4e14bd1fdd4381979bf9177..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..7f0b0fda5a8f13cc8345ac01a092b97f67785489 --- /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/pattern-edit.glade b/share/ui/pattern-edit.glade index 7993af04ab677e093c0d5a2e12460de5fcd18df0..59e90b9c2b141154d899dc68c18515101e06e23a 100644 --- a/share/ui/pattern-edit.glade +++ b/share/ui/pattern-edit.glade @@ -367,7 +367,7 @@ Gap 0 - 7 + 8 diff --git a/share/ui/style.css b/share/ui/style.css index 597ca11e192717be58acc6e4cec3458ed3e5b8d9..b17f235f67cfc64c1b8b96ee56aae98622222a07 100644 --- a/share/ui/style.css +++ b/share/ui/style.css @@ -1721,3 +1721,49 @@ button.reduced-padding { #DialogMultipaned:drop(active), #DialogMultipaned:drop(active):focus { box-shadow: none; } + +/* +********************** +* 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; +} \ No newline at end of file diff --git a/share/ui/widget-recolor.ui b/share/ui/widget-recolor.ui new file mode 100644 index 0000000000000000000000000000000000000000..e1ea7ca50934cd8e61faf7bd99cec22d72dc8f62 --- /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 a7ca11c1a49017339576bc8e9249dc0cf5f27fa6..daa9b86527455a12ae90b06bef041f4c252bc18b 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 f499f3c6fffc8199e09b03196c9d9ccd64adb540..9b1dce62d5ab0dae70b9e0b7267194c074ab4f8f 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 8f9d1e8a47d312482e6fb5909c80ea3d9c5f41ea..57be44d08dccf1da2033d526eb25576c72c646c7 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 0000000000000000000000000000000000000000..82551529958c8fd27552ddd86fe8c469dd1dc73f --- /dev/null +++ b/src/object-colors.cpp @@ -0,0 +1,447 @@ +// 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-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 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 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); +} + +bool ObjectColorSet::findSelectedColor(uint32_t key_color) const +{ + return _selected_colors.contains(key_color); +} + +void ObjectColorSet::clearData() +{ + colors.clear(); + _gradient_stops.clear(); + _selected_colors.clear(); + color_wheel_colors_map.clear(); +} + +bool ObjectColorSet::setSelectedNewColor(std::vector &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 (findSelectedColor(key_color)) { + return _selected_colors[key_color].first; + } + return empty; +} + +int ObjectColorSet::getColorIndex(uint32_t key_color) +{ + if (color_wheel_colors_map.contains(key_color)) { + return color_wheel_colors_map[key_color]; + } + 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 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 new_color) +{ + _selected_colors[key_color].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); + 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) { + if (!child.getId() && noid) { + children_vec.push_back(&child); + noid = false; + } else if (child.getId()) { + 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 (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) +{ + // 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, 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, object,stroke_type, "stroke");//ObjectColorSet + } 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 0000000000000000000000000000000000000000..b52b2c3f3d9e8fda8034e3e8579048832034aaa4 --- /dev/null +++ b/src/object-colors.h @@ -0,0 +1,91 @@ +// 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 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(const ColorRef& item, Colors::Color color); + void changeOpacity(bool change_opacity = false , uint32_t color= 0 ,bool is_preview = true); + void setSelectedNewColor(uint32_t key_color, Colors::Color new_color); + std::optional getSelectedNewColor(uint32_t key_color) const; + bool findSelectedColor(uint32_t key_color) const; + bool isSelectedColorsEmpty()const {return _selected_colors.empty();} + bool isGradientStopsEmpty() const {return _gradient_stops.empty();} + bool isColorWheelColorsMapEmpty() const {return color_wheel_colors_map.empty();} + bool isColorsEmpty() const { return colors.empty(); } + void clearData(); + bool setSelectedNewColor(std::vector &new_colors); + uint32_t getFirstKey() const { return _selected_colors.begin()->first; } + std::vector &getSelectedItems(uint32_t key_color); + int getColorIndex(uint32_t key_color); + 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 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 d52bbb8f69cf5d3d180dfbf6532250ad5fc74c86..b3ee8bcc3a74ae6553e09fdfbf5ed25ddab062a0 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 0cb55b486ba80eb0eb41f7d4cd4277aaf29cdf8b..246b8c1aa774ad25b83e7683a82d865d99776a75 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 64695430fe96d7f96541b385c0a8f5b166b94ded..2b85a2f621416a7a4ce6424ad90251f713bebde0 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 5c9d9c3544d95f02f4bddaffbdf39d89c50959c5..50673216db6e8512354f3a04853108830c21b42a 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -299,10 +299,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 @@ -627,7 +628,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 f351015c5113ddca9b11af6553cad6ca5efaeb18..9cb7c2dd4b775ed338b275786701318eea0eb3fb 100644 --- a/src/ui/tools/select-tool.cpp +++ b/src/ui/tools/select-tool.cpp @@ -1047,6 +1047,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 7880e351e13853b8fbe720b7c62c638e4131f5f3..1da9ad25f71a5be96256e0f455f05e0e6e6f16e6 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 93badabfe7dcea7b47e5b8ed1deb6c5643a832ea..677962bcd58db92975db70b840fc163d44a0f350 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -113,6 +113,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 e66df77882b3a8c5c9d7bc4de834b681cd08fd59..207c39abf736ae1622e7a94c91d5cf7a47c49da2 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 39117b6506e65a3f4fefd789ad14cccf764c81e6..b2ee1cbb6c06c69d0a9aa4a1e4ad5da5ed391929 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 637379c716012ac639c1d30007f84e06f34c5c33..d0d9e9cc979f9576eae9a391ccf43eebada24cd0 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 eb7efc7ffc50dd3b0fed41533c2d76e0039063f5..26a28a54a8e9a31e9bce9098f14e84bfce03cab4 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 4854cbbad6a6ef66a70975c078d2067684389d99..ecf80c025570c9c3df534c3fe191ef08698ce53a 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 a364570aebcf6cddaa05e73dcd16ffbfb07c26d8..b46882da9fc7e3b95f06708c7e64af98614eeb75 100644 --- a/src/ui/widget/fill-style.cpp +++ b/src/ui/widget/fill-style.cpp @@ -128,6 +128,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 3aa4c6d1c1b43f503dec8cc7906cda41a8702b42..b7b2c67ff460366f5cf3d77ea435b1468b661b64 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 7123f91a532d11b683f362ba5cb2aca056f369f7..f457e41a44dcfe1bc8dd8f6930b24335c7f19f05 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 8561f92cab9cf6f47f84fc9ff9874d024ca0e2c8..0f21882bcdeb74edc2d5276ce17777c533bfa1ba 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 const &color) +{ + _values_vector.clear(); + _markers_points.clear(); + _source_wheel.reset(); + for (auto c : color) { + if (!setColor(c, false, false)) { + return false; + } + } + _markers_points.resize(_values_vector.size()); + _reset_markers(); + color_changed(); + return true; +} + +void MultiMarkerWheel::_reset_markers() +{ + for (auto &_marker_point : _markers_points) { + _marker_point.reset(); + } + _markers_points.resize(_values_vector.size()); +} + +/** + * 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 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(); + _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(); + _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 angel 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 8169bd86b9f0461eb47d955cfa5e648d849f2857..afba4be37a8c18bf98108827d6792f6f1ade0691 100644 --- a/src/ui/widget/ink-color-wheel.h +++ b/src/ui/widget/ink-color-wheel.h @@ -225,6 +225,108 @@ 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; + bool setColor(std::vector const &color); + 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 _reset_markers(); + 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 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; + using MarkerPoints = std::vector>; + std::vector _values_vector; + std::optional _cache_size; + std::optional _radii; + std::optional _marker_point; + MarkerPoints _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 1c54b1d794f682cceb255c74c78b908f8ce949c8..3806f36eda69d804f68ad1243a54a3149ce91ece 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,23 @@ 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); + _recolorButtonTrigger->set_direction(Gtk::ArrowType::NONE); + _recolorButtonTrigger->set_visible(false); + _grid.add_full_row(_recolorButtonTrigger); + + _recolorButtonTrigger->set_create_popup_func([this] { + auto &mgr = RecolorArtManager::get(); + mgr.reparentPopoverTo(*_recolorButtonTrigger); + mgr.widget.showForObject(_desktop, get_current()); + }); } void MarkerComboBox::update_widgets_from_marker(SPMarker* marker) { @@ -380,6 +396,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 +497,17 @@ Glib::RefPtr MarkerComboBox::get_active() { } } +void MarkerComboBox::setDesktop(SPDesktop *desktop) +{ + if (_desktop == desktop) { + return; + } + + RecolorArtManager::get().popover.popdown(); + + _desktop = 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 1af86289e3a9c49413bbbdf953b6c3abf8cc5c34..1493578b9e5cb1423fdede487981fbf6a92c6482 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,29 @@ private: std::unique_ptr _sandbox; InkPropertyGrid _grid; WidgetGroup _widgets; + Gtk::CellRendererPixbuf _image_renderer; + + SPDesktop *_desktop = nullptr; + sigc::scoped_connection _selection_changed_connection; + Gtk::MenuButton *_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 0000000000000000000000000000000000000000..922c4e35043812102c38eda596f75cbb7b9b9733 --- /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 0000000000000000000000000000000000000000..2458a9b26b6850f2e2aac356e8d96886e045f132 --- /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 setColor(std::vector &colors) { _color_wheel->setColor(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 a858c807ec24592cff198b0d4c5d7ea0e2491d1b..93e931472473eb7f947c3c26a20e9186ede1039f 100644 --- a/src/ui/widget/paint-selector.cpp +++ b/src/ui/widget/paint-selector.cpp @@ -24,7 +24,10 @@ #include #include #include +#include +#include "desktop-style.h" +#include "desktop.h" #include "document.h" #include "inkscape.h" #include "object/sp-hatch.h" @@ -33,12 +36,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 +123,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 +201,48 @@ 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_direction(Gtk::ArrowType::NONE); + b->set_visible(false); + + b->set_create_popup_func([b = b.get(), this] { + auto &mgr = RecolorArtManager::get(); + mgr.reparentPopoverTo(*b); + mgr.widget.showForSelection(_desktop); + }); + } + + _frame->append(*_recolorButtonTrigger[0]); +} + +void PaintSelector::setDesktop(SPDesktop *desktop) +{ + if (_desktop == desktop) { + return; + } + + RecolorArtManager::get().popover.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)); + } + } } StyleToggleButton *PaintSelector::style_button_add(gchar const *pixmap, PaintSelector::Mode mode, gchar const *tip) @@ -225,7 +280,8 @@ void PaintSelector::fillrule_toggled(FillRuleRadioButton *tb) } } -void PaintSelector::setMode(Mode mode) { +void PaintSelector::setMode(Mode mode) +{ set_mode_ex(mode, false); } @@ -276,6 +332,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 +558,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 +611,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 +881,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 +968,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 +1000,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 +1129,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 +1195,36 @@ PaintSelector::Mode PaintSelector::getModeForStyle(SPStyle const &style, FillOrS return mode; } +void PaintSelector::onSelectionChanged(Inkscape::Selection *selection) +{ + bool show_recolor = (_mode == MODE_GRADIENT_MESH && RecolorArtManager::checkMeshObject(selection)) || + RecolorArtManager::checkSelection(selection); + + int btn_index = -1; + + if (show_recolor) { + if (_mode == MODE_MULTIPLE || _mode == MODE_UNSET || _mode == MODE_GRADIENT_MESH) { + btn_index = 0; + } else if (_mode == MODE_SOLID_COLOR) { + btn_index = 1; + } else if (_mode == MODE_GRADIENT_RADIAL || _mode == MODE_GRADIENT_LINEAR) { + btn_index = 2; + } else if (_mode == MODE_PATTERN) { + btn_index = 3; + } else if (_mode == MODE_SWATCH) { + btn_index = 4; + } + } + + for (int i = 0; i < _recolorButtonTrigger.size(); i++) { + _recolorButtonTrigger[i]->set_visible(i == btn_index); + } +} + } // 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 c126f04252b3f904aa42a84ac35c06979bd515dd..a0913a071ef3e75bb8e277506714f1ab167adf44 100644 --- a/src/ui/widget/paint-selector.h +++ b/src/ui/widget/paint-selector.h @@ -19,6 +19,8 @@ #include "fill-or-stroke.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 +29,16 @@ class SPRadialGradient; class SPMeshGradient; #endif class SPDesktop; +class Selection; class SPPattern; class SPStyle; namespace Gtk { class Label; class ToggleButton; +class Button; +class MenuButton; +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,10 @@ class PaintSelector : public Gtk::Box { Gtk::Box *_selector_mesh = nullptr; SwatchSelector *_selector_swatch = nullptr; PatternEditor* _selector_pattern = nullptr; - + + std::array, 5> _recolorButtonTrigger; + SPDesktop *_desktop = nullptr; + void onSelectionChanged(Inkscape::Selection *selection); Gtk::Label *_label; GtkWidget *_patternmenu = nullptr; bool _patternmenu_update = false; @@ -116,6 +127,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 +204,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 0000000000000000000000000000000000000000..8780d3a25c25a43543cee7dca0ee7843a637b8ee --- /dev/null +++ b/src/ui/widget/recolor-art-manager.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Fatma Omara + * + * Copyright (C) 2025 authors + */ + +#include "recolor-art-manager.h" + +#include + +#include "object/sp-gradient.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) +{ + if (!item || !item->style) { + return false; + } + + 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; + } + } + } + } + + if (check_color(child.style->fill) || check_color(child.style->stroke)) { + return true; + } + } + + return false; + }; + + return search_pattern(item->style->getFillPaintServer()) || + search_pattern(item->style->getStrokePaintServer()); +} + +} // namespace + +RecolorArtManager &RecolorArtManager::get() +{ + static RecolorArtManager instance; + 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() +{ + popover.set_autohide(false); + popover.set_child(widget); +} + +bool RecolorArtManager::checkSelection(Inkscape::Selection *selection) +{ + if (selection->size() > 1) { + return true; + } + + auto item = selection->singleItem(); + if (!item) { + return false; + } + + return is(item) || + is(item) || + item->getMaskObject() || + has_colors_pattern(item); +} + +bool RecolorArtManager::checkMeshObject(Inkscape::Selection *selection) +{ + if (selection->size() > 1) { + return true; + } + + auto item = selection->singleItem(); + if (!item) { + return false; + } + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..0726aecf75fdf5d3b45b3e8251e89a5cddb4794d --- /dev/null +++ b/src/ui/widget/recolor-art-manager.h @@ -0,0 +1,40 @@ +// 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 Gtk { class MenuButton; } + +namespace Inkscape::UI::Widget { + +class RecolorArtManager +{ +public: + static RecolorArtManager &get(); + + RecolorArt widget; + Gtk::Popover popover; + + void reparentPopoverTo(Gtk::MenuButton &button); + + static bool checkSelection(Inkscape::Selection *selection); + static bool checkMeshObject(Inkscape::Selection *selection); + +private: + RecolorArtManager(); +}; + +} // 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 0000000000000000000000000000000000000000..9bc5c873ece2da2b5af809ebe4dfd7a14d2c4376 --- /dev/null +++ b/src/ui/widget/recolor-art.cpp @@ -0,0 +1,657 @@ +// 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([&]() { + _manager.changeOpacity(false,0,_is_preview); + 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.isSelectedColorsEmpty()) + 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(); + _manager.changeOpacity(true, _current_color_id, _is_preview); + } else { + _manager.changeOpacity(false, 0, _is_preview); + } + }); + + layoutColorPicker(); + _live_preview.signal_toggled().connect(sigc::mem_fun(*this, &RecolorArt::onLivePreviewToggled)); + _live_preview.set_active(true); + _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; + } + + // for hover effect + auto item_controller = Gtk::EventControllerMotion::create(); + item_controller->signal_enter().connect([box, item, this](double x, double y) { _manager.changeOpacity(true,item->key ,_is_preview);}); + item_controller->signal_leave().connect([box, this]() { _manager.changeOpacity(false,0,_is_preview); }); + + box->add_controller(item_controller); + + 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()](gint n_press, gdouble x, gdouble y) { + _selection_model->set_selected(index); + onOriginalColorClicked(item->key); + }); + + recolored_click->signal_pressed().connect( + [this, item, index = list_item->get_position()](gint n_press, gdouble x, gdouble 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) { + return; + } + + _sel_changed_conn.disconnect(); + _desktop_destroyed_conn.disconnect(); + + _desktop = desktop; + + if (_desktop) { + _desktop_destroyed_conn = _desktop->connectDestroy([this] (auto) { + setDesktop(nullptr); + }); + } + + _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_visible(true); + _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 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 (_color_wheel && !_manager.isColorWheelColorsMapEmpty()) { + 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; + } + std::string _color_string = new_color.value().toString(); + if (_manager.findSelectedColor(_current_color_id)) { + _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 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->setColor(_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::showForSelection(SPDesktop *desktop) +{ + assert(desktop); + + setDesktop(desktop); + _sel_changed_conn = _desktop->getSelection()->connectChanged([this] (auto) { + updateFromSelection(); + }); + + updateFromSelection(); +} + +void RecolorArt::updateFromSelection() +{ + if (_selection_blocker.pending()) { + return; + } + + _manager.clearData(); + + _color_wheel->toggleHueLock(false); + _color_wheel->setLightness(100.0); + _color_wheel->setSaturation(100.0); + + auto selection = _desktop->getSelection(); + + auto guard = _selection_blocker.block(); + selection->unlinkRecursive(false, true, false); + auto items = selection->items(); + auto vec = std::vector(items.begin(), items.end()); + _manager = collect_colours(vec); + if (!_manager.isSelectedColorsEmpty()) { + generateVisualList(); + auto first_button_id = _manager.getFirstKey(); + onOriginalColorClicked(first_button_id); + } + if (!_manager.isColorsEmpty()) { + _color_wheel->setColor(_manager.getColors()); + } +} + +void RecolorArt::showForObject(SPDesktop *desktop, SPObject *object) +{ + assert(desktop); + assert(object); + + setDesktop(desktop); + + _manager.clearData(); + + _color_wheel->toggleHueLock(false); + _color_wheel->setLightness(100.0); + _color_wheel->setSaturation(100.0); + + _manager = collect_colours({object}); + if (!_manager.isSelectedColorsEmpty()) { + generateVisualList(); + auto first_button_id = _manager.getFirstKey(); + onOriginalColorClicked(first_button_id); + } + if (!_manager.isColorsEmpty()) { + _color_wheel->setColor(_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 0000000000000000000000000000000000000000..eb8f83cb903498f40ea15a4679c273200ef892a6 --- /dev/null +++ b/src/ui/widget/recolor-art.h @@ -0,0 +1,119 @@ +// 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" + +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 showForSelection(SPDesktop *desktop); + void showForObject(SPDesktop *desktop, SPObject *object); + + void onResetClicked(); + +private: + SPDesktop *_desktop = nullptr; + sigc::scoped_connection _sel_changed_conn; + sigc::scoped_connection _desktop_destroyed_conn; + Glib::RefPtr _builder; + Gtk::Notebook &_notebook; + Gtk::Box &_color_wheel_page; + std::shared_ptr _solid_colors = std::make_shared(); + 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 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); + + // 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 color); + void updateColorModel(std::vector 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 fbfbab6a51f9db83da89185186637145688dab25..5e12c35954ccc1010abadb80c1175516ef3df050 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 54c73019b64c8114b17231f956a54993738dde4c..77c656dd453921e1a092a86126770626059cf510 100644 --- a/testfiles/CMakeLists.txt +++ b/testfiles/CMakeLists.txt @@ -113,6 +113,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 0000000000000000000000000000000000000000..9fb2aec1b088363d80031b04b61bcfc7d1eba2dd --- /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); + + EXPECT_TRUE(wheel->setColor(colors)); + EXPECT_EQ(wheel->getColors().size(), 9); + EXPECT_EQ(wheel->getActiveIndex(), 0); +} + +TEST_F(ColorWheelTestFixture, TestColorWheelActiveIndex) +{ + wheel->setColor(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->setColor(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->setColor(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 0000000000000000000000000000000000000000..55f696af991c275a6d1a9e0d72314d8be866ea7e --- /dev/null +++ b/testfiles/src/object-colors-test.cpp @@ -0,0 +1,232 @@ +// 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({}).isSelectedColorsEmpty()); +} + +TEST(ObjectColorSet, HandleNullObjects) +{ + EXPECT_TRUE(collect_colours({nullptr}).isSelectedColorsEmpty()); +} + +TEST_F(ObjectColorSetFixture, PopulateAndFindColor) +{ + EXPECT_FALSE(set.isSelectedColorsEmpty()); + EXPECT_FALSE(set.isGradientStopsEmpty()); + EXPECT_EQ(set.getColors().size(), 9); + + auto key = Colors::Color(0xffff00ff).toRGBA(); + EXPECT_TRUE(set.findSelectedColor(key)); + + EXPECT_EQ(set.getColorIndex(key), 0); + EXPECT_EQ(set.getColor(0).value().toRGBA(), key); + + auto false_key = Colors::Color(0x000000ff).toRGBA(); + EXPECT_FALSE(set.findSelectedColor(false_key)); + EXPECT_EQ(set.getColorIndex(false_key), -1); +} + +TEST_F(ObjectColorSetFixture, ClearData) +{ + EXPECT_EQ(set.getColors().size(), 9); + set.clearData(); + EXPECT_TRUE(set.isSelectedColorsEmpty()); + EXPECT_TRUE(set.isGradientStopsEmpty()); + EXPECT_TRUE(set.isColorWheelColorsMapEmpty()); + EXPECT_TRUE(set.isColorsEmpty()); +} + +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.isSelectedColorsEmpty()); + + 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))); +}