From c853ffb5b2f75f5a889ed2e3ef89b818a736e87a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 13:50:49 +0200 Subject: Adding upstream version 1.3+ds. Signed-off-by: Daniel Baumann --- src/ui/widget/pattern-editor.cpp | 685 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 685 insertions(+) create mode 100644 src/ui/widget/pattern-editor.cpp (limited to 'src/ui/widget/pattern-editor.cpp') diff --git a/src/ui/widget/pattern-editor.cpp b/src/ui/widget/pattern-editor.cpp new file mode 100644 index 0000000..e31cad0 --- /dev/null +++ b/src/ui/widget/pattern-editor.cpp @@ -0,0 +1,685 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Pattern editor widget for "Fill and Stroke" dialog + * + * Copyright (C) 2022 Michael Kowalski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "pattern-editor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "object/sp-defs.h" +#include "object/sp-root.h" +#include "style.h" +#include "ui/builder-utils.h" +#include "ui/svg-renderer.h" +#include "io/resource.h" +#include "manipulation/copy-resource.h" +#include "pattern-manager.h" +#include "pattern-manipulation.h" +#include "preferences.h" +#include "util/units.h" +#include "widgets/spw-utilities.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +using namespace Inkscape::IO; + +// default size of pattern image in a list +static const int ITEM_WIDTH = 45; + +// get slider position 'index' (linear) and transform that into gap percentage (non-linear) +static double slider_to_gap(double index, double upper) { + auto v = std::tan(index / (upper + 1) * M_PI / 2.0) * 500; + return std::round(v / 20) * 20; +} +// transform gap percentage value into slider position +static double gap_to_slider(double gap, double upper) { + return std::atan(gap / 500) * (upper + 1) / M_PI * 2; +} + +// tile size slider functions +static int slider_to_tile(double index) { + return 30 + static_cast(index) * 5; +} +static double tile_to_slider(int tile) { + return (tile - 30) / 5.0; +} + +Glib::ustring get_attrib(SPPattern* pattern, const char* attrib) { + auto value = pattern->getAttribute(attrib); + return value ? value : ""; +} + +double get_attrib_num(SPPattern* pattern, const char* attrib) { + auto val = get_attrib(pattern, attrib); + return strtod(val.c_str(), nullptr); +} + +const double ANGLE_STEP = 15.0; + +PatternEditor::PatternEditor(const char* prefs, Inkscape::PatternManager& manager) : + _manager(manager), + _builder(create_builder("pattern-edit.glade")), + _offset_x(get_widget(_builder, "offset-x")), + _offset_y(get_widget(_builder, "offset-y")), + _scale_x(get_widget(_builder, "scale-x")), + _scale_y(get_widget(_builder, "scale-y")), + _angle_btn(get_widget(_builder, "angle")), + _orient_slider(get_widget(_builder, "orient")), + _gap_x_slider(get_widget(_builder, "gap-x")), + _gap_y_slider(get_widget(_builder, "gap-y")), + _edit_btn(get_widget(_builder, "edit-pattern")), + _preview_img(get_widget(_builder, "preview")), + _preview(get_widget(_builder, "preview-box")), + _color_btn(get_widget(_builder, "color-btn")), + _color_label(get_widget(_builder, "color-label")), + _paned(get_widget(_builder, "paned")), + _main_grid(get_widget(_builder, "main-box")), + _input_grid(get_widget(_builder, "input-grid")), + _stock_gallery(get_widget(_builder, "flowbox")), + _doc_gallery(get_widget(_builder, "doc-flowbox")), + _link_scale(get_widget(_builder, "link-scale")), + _name_box(get_widget(_builder, "pattern-name")), + _combo_set(get_widget(_builder, "pattern-combo")), + _search_box(get_widget(_builder, "search")), + _tile_slider(get_widget(_builder, "tile-slider")), + _show_names(get_widget(_builder, "show-names")), + _prefs(prefs) +{ + _color_picker = std::make_unique( + _("Pattern color"), "", 0x7f7f7f00, true, + &get_widget(_builder, "color-btn")); + _color_picker->use_transparency(false); + _color_picker->connectChanged([=](guint color){ + if (_update.pending()) return; + _signal_color_changed.emit(color); + }); + + _tile_size = Inkscape::Preferences::get()->getIntLimited(_prefs + "/tileSize", ITEM_WIDTH, 30, 1000); + _tile_slider.set_value(tile_to_slider(_tile_size)); + _tile_slider.signal_change_value().connect([=](Gtk::ScrollType st, double value){ + if (_update.pending()) return true; + auto scoped(_update.block()); + auto size = slider_to_tile(value); + if (size != _tile_size) { + _tile_slider.set_value(tile_to_slider(size)); + // change pattern tile size + _tile_size = size; + update_pattern_tiles(); + Inkscape::Preferences::get()->setInt(_prefs + "/tileSize", size); + } + return true; + }); + + auto show_labels = Inkscape::Preferences::get()->getBool(_prefs + "/showLabels", false); + _show_names.set_active(show_labels); + _show_names.signal_toggled().connect([=](){ + // toggle pattern labels + _stock_pattern_store.store.refresh(); + _doc_pattern_store.store.refresh(); + Inkscape::Preferences::get()->setBool(_prefs + "/showLabels", _show_names.get_active()); + }); + + const auto max = 180.0 / ANGLE_STEP; + _orient_slider.set_range(-max, max); + _orient_slider.set_increments(1, 1); + _orient_slider.set_digits(0); + _orient_slider.set_value(0); + _orient_slider.signal_change_value().connect([=](Gtk::ScrollType st, double value){ + if (_update.pending()) return false; + auto scoped(_update.block()); + // slider works with 15deg discrete steps + _angle_btn.set_value(round(CLAMP(value, -max, max)) * ANGLE_STEP); + _signal_changed.emit(); + return true; + }); + + for (auto slider : {&_gap_x_slider, &_gap_y_slider}) { + slider->set_increments(1, 1); + slider->set_digits(0); + slider->set_value(0); + slider->signal_format_value().connect([=](double val){ + auto upper = slider->get_adjustment()->get_upper(); + return Glib::ustring::format(std::fixed, std::setprecision(0), slider_to_gap(val, upper)) + "%"; + }); + slider->signal_change_value().connect([=](Gtk::ScrollType st, double value){ + if (_update.pending()) return false; + _signal_changed.emit(); + return true; + }); + } + + _angle_btn.signal_value_changed().connect([=]() { + if (_update.pending() || !_angle_btn.is_sensitive()) return; + auto scoped(_update.block()); + auto angle = _angle_btn.get_value(); + _orient_slider.set_value(round(angle / ANGLE_STEP)); + _signal_changed.emit(); + }); + + _link_scale.signal_clicked().connect([=](){ + if (_update.pending()) return; + auto scoped(_update.block()); + _scale_linked = !_scale_linked; + if (_scale_linked) { + // this is simplistic + _scale_x.set_value(_scale_y.get_value()); + } + update_scale_link(); + _signal_changed.emit(); + }); + + for (auto el : {&_scale_x, &_scale_y, &_offset_x, &_offset_y}) { + el->signal_value_changed().connect([=]() { + if (_update.pending()) return; + if (_scale_linked && (el == &_scale_x || el == &_scale_y)) { + auto scoped(_update.block()); + // enforce uniform scaling + (el == &_scale_x) ? _scale_y.set_value(el->get_value()) : _scale_x.set_value(el->get_value()); + } + _signal_changed.emit(); + }); + } + + _name_box.signal_changed().connect([=](){ + if (_update.pending()) return; + + _signal_changed.emit(); + }); + + _search_box.signal_search_changed().connect([=](){ + if (_update.pending()) return; + + // filter patterns + _filter_text = _search_box.get_text(); + apply_filter(false); + apply_filter(true); + }); + + // populate combo box with all patern categories + auto pattern_categories = _manager.get_categories()->children(); + int cat_count = pattern_categories.size(); + for (auto row : pattern_categories) { + auto name = row.get_value(_manager.columns.name); + _combo_set.append(name); + } + + get_widget(_builder, "previous").signal_clicked().connect([=](){ + int previous = _combo_set.get_active_row_number() - 1; + if (previous >= 0) _combo_set.set_active(previous); + }); + get_widget(_builder, "next").signal_clicked().connect([=](){ + auto next = _combo_set.get_active_row_number() + 1; + if (next < cat_count) _combo_set.set_active(next); + }); + _combo_set.signal_changed().connect([=](){ + // select pattern category to show + auto index = _combo_set.get_active_row_number(); + select_pattern_set(index); + Inkscape::Preferences::get()->setInt(_prefs + "/currentSet", index); + }); + + bind_store(_doc_gallery, _doc_pattern_store); + bind_store(_stock_gallery, _stock_pattern_store); + + _stock_gallery.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){ + if (_update.pending()) return; + auto scoped(_update.block()); + auto pat = _stock_pattern_store.widgets_to_pattern[box]; + update_ui(pat); + _doc_gallery.unselect_all(); + _signal_changed.emit(); + }); + + _doc_gallery.signal_child_activated().connect([=](Gtk::FlowBoxChild* box){ + if (_update.pending()) return; + auto scoped(_update.block()); + auto pat = _doc_pattern_store.widgets_to_pattern[box]; + update_ui(pat); + _stock_gallery.unselect_all(); + _signal_changed.emit(); + }); + + _edit_btn.signal_clicked().connect([=](){ + _signal_edit.emit(); + }); + + _paned.set_position(Inkscape::Preferences::get()->getIntLimited(_prefs + "/handlePos", 50, 10, 9999)); + _paned.property_position().signal_changed().connect([=](){ + Inkscape::Preferences::get()->setInt(_prefs + "/handlePos", _paned.get_position()); + }); + + // current pattern category + _combo_set.set_active(Inkscape::Preferences::get()->getIntLimited(_prefs + "/currentSet", 0, 0, std::max(cat_count - 1, 0))); + + update_scale_link(); + pack_start(_main_grid); +} + +PatternEditor::~PatternEditor() noexcept {} + +void PatternEditor::bind_store(Gtk::FlowBox& list, PatternStore& pat) { + pat.store.set_filter([=](const Glib::RefPtr& p){ + if (!p) return false; + if (_filter_text.empty()) return true; + + auto name = Glib::ustring(p->label).lowercase(); + auto expr = _filter_text.lowercase(); + auto pos = name.find(expr); + return pos != Glib::ustring::npos; + }); + + list.bind_list_store(pat.store.get_store(), [=, &pat](const Glib::RefPtr& item){ + auto box = Gtk::make_managed(Gtk::ORIENTATION_VERTICAL); + auto image = Gtk::make_managed(item->pix); + box->pack_start(*image); + auto name = Glib::ustring(item->label.c_str()); + if (_show_names.get_active()) { + auto label = Gtk::make_managed(name); + label->get_style_context()->add_class("small-font"); + // limit label size to tile size + label->set_ellipsize(Pango::EllipsizeMode::ELLIPSIZE_END); + label->set_max_width_chars(0); + label->set_size_request(_tile_size); + box->pack_end(*label); + } + image->set_tooltip_text(name); + box->show_all(); + auto cbox = Gtk::make_managed(); + cbox->add(*box); + cbox->get_style_context()->add_class("pattern-item-box"); + pat.widgets_to_pattern[cbox] = item; + cbox->set_size_request(_tile_size, _tile_size); + return cbox; + }); +} + +void PatternEditor::select_pattern_set(int index) { + auto sets = _manager.get_categories()->children(); + if (index >= 0 && index < sets.size()) { + auto row = sets[index]; + if (auto category = row.get_value(_manager.columns.category)) { + set_stock_patterns(category->patterns); + } + } +} + +void PatternEditor::update_scale_link() { + _link_scale.remove(); + _link_scale.add(get_widget(_builder, _scale_linked ? "image-linked" : "image-unlinked")); +} + +void PatternEditor::update_widgets_from_pattern(Glib::RefPtr& pattern) { + _input_grid.set_sensitive(!!pattern); + + PatternItem empty; + const auto& item = pattern ? *pattern.get() : empty; + + _name_box.set_text(item.label.c_str()); + + _scale_x.set_value(item.transform.xAxis().length()); + _scale_y.set_value(item.transform.yAxis().length()); + + // TODO if needed + // auto units = get_attrib(pattern, "patternUnits"); + + _scale_linked = item.uniform_scale; + update_scale_link(); + + _offset_x.set_value(item.offset.x()); + _offset_y.set_value(item.offset.y()); + + auto degrees = 180.0 / M_PI * Geom::atan2(item.transform.xAxis()); + _orient_slider.set_value(round(degrees / ANGLE_STEP)); + _angle_btn.set_value(degrees); + + double x_index = gap_to_slider(item.gap[Geom::X], _gap_x_slider.get_adjustment()->get_upper()); + _gap_x_slider.set_value(x_index); + double y_index = gap_to_slider(item.gap[Geom::Y], _gap_y_slider.get_adjustment()->get_upper()); + _gap_y_slider.set_value(y_index); + + if (item.color.has_value()) { + _color_picker->setRgba32(item.color->toRGBA32(1.0)); + _color_btn.set_sensitive(); + _color_label.set_opacity(1.0); // hack: sensitivity doesn't change appearance, so using opacity directly + } + else { + _color_picker->setRgba32(0); + _color_btn.set_sensitive(false); + _color_label.set_opacity(0.6); + _color_picker->closeWindow(); + } +} + +void PatternEditor::update_ui(Glib::RefPtr pattern) { + update_widgets_from_pattern(pattern); +} + +// sort patterns in-place by name/id +void sort_patterns(std::vector>& list) { + std::sort(list.begin(), list.end(), [](Glib::RefPtr& a, Glib::RefPtr& b) { + if (!a || !b) return false; + if (a->label == b->label) { + return a->id < b->id; + } + return a->label < b->label; + }); +} + +// given a pattern, create a PatternItem instance that describes it; +// input pattern can be a link or a root pattern +Glib::RefPtr create_pattern_item(PatternManager& manager, SPPattern* pattern, int tile_size, double scale) { + auto item = manager.get_item(pattern); + if (item && scale > 0) { + item->pix = manager.get_image(pattern, tile_size, tile_size, scale); + } + return item; +} + +// update editor UI +void PatternEditor::set_selected(SPPattern* pattern) { + auto scoped(_update.block()); + + _stock_gallery.unselect_all(); + + // current pattern (should be a link) + auto link_pattern = pattern; + if (pattern) pattern = pattern->rootPattern(); + + if (pattern && pattern != link_pattern) { + _current_pattern.id = pattern->getId(); + _current_pattern.link_id = link_pattern->getId(); + } + else { + _current_pattern.id.clear(); + _current_pattern.link_id.clear(); + } + + auto item = create_pattern_item(_manager, link_pattern, 0, 0); + + update_widgets_from_pattern(item); + + auto list = update_doc_pattern_list(pattern ? pattern->document : nullptr); + if (pattern) { + // patch up tile image on a list of document root patterns, it might have changed; + // color attribute for instance is being set directly on the root pattern; + // other attributes are per-object, so should not be taken into account when rendering tile + for (auto& pattern_item : list) { + if (pattern_item->id == item->id && pattern_item->collection == nullptr) { + // update preview + const double device_scale = get_scale_factor(); + pattern_item->pix = _manager.get_image(pattern, _tile_size, _tile_size, device_scale); + item->pix = pattern_item->pix; + break; + } + } + } + + set_active(_doc_gallery, _doc_pattern_store, item); + + // generate large preview of selected pattern + Cairo::RefPtr surface; + if (link_pattern) { + const double device_scale = get_scale_factor(); + auto size = _preview.get_allocation(); + const int m = 1; + if (size.get_width() <= m || size.get_height() <= m) { + // widgets not resized yet, choose arbitrary size, so preview is not missing when widget is shown + size.set_width(200); + size.set_height(200); + } + // use white for checkerboard since most stock patterns are black + unsigned int background = 0xffffffff; + surface = _manager.get_preview(link_pattern, size.get_width(), size.get_height(), background, device_scale); + } + _preview_img.set(surface); +} + +// generate preview images for patterns +std::vector> create_pattern_items(PatternManager& manager, const std::vector& list, int tile_size, double device_scale) { + std::vector> output; + output.reserve(list.size()); + + for (auto pat : list) { + if (auto item = create_pattern_item(manager, pat, tile_size, device_scale)) { + output.push_back(item); + } + } + + return output; +} + +// populate store with document patterns if list has changed, minimize amount of work by using cached previews +std::vector> PatternEditor::update_doc_pattern_list(SPDocument* document) { + auto list = sp_get_pattern_list(document); + std::shared_ptr nil; + const double device_scale = get_scale_factor(); + // create pattern items (cheap), but skip preview generation (expansive) + auto patterns = create_pattern_items(_manager, list, 0, 0); + bool modified = false; + for (auto&& item : patterns) { + auto it = _cached_items.find(item->id); + if (it != end(_cached_items)) { + // reuse cached preview image + if (!item->pix) item->pix = it->second->pix; + } + else { + if (!item->pix) { + // generate preview for newly added pattern + item->pix = _manager.get_image(cast(document->getObjectById(item->id)), _tile_size, _tile_size, device_scale); + } + modified = true; + _cached_items[item->id] = item; + } + } + + update_store(patterns, _doc_gallery, _doc_pattern_store); + + return patterns; +} + +void PatternEditor::set_document(SPDocument* document) { + _current_document = document; + _cached_items.clear(); + update_doc_pattern_list(document); +} + +// populate store with stock patterns +void PatternEditor::set_stock_patterns(const std::vector& list) { + const double device_scale = get_scale_factor(); + auto patterns = create_pattern_items(_manager, list, _tile_size, device_scale); + sort_patterns(patterns); + update_store(patterns, _stock_gallery, _stock_pattern_store); +} + +void PatternEditor::apply_filter(bool stock) { + auto scoped(_update.block()); + if (!stock) { + _doc_pattern_store.store.apply_filter(); + } + else { + _stock_pattern_store.store.apply_filter(); + } +} + +void PatternEditor::update_store(const std::vector>& list, Gtk::FlowBox& gallery, PatternStore& pat) { + auto selected = get_active(gallery, pat); + if (pat.store.assign(list)) { + // reselect current + set_active(gallery, pat, selected); + } +} + +Glib::RefPtr PatternEditor::get_active(Gtk::FlowBox& gallery, PatternStore& pat) { + auto empty = Glib::RefPtr(); + + auto sel = gallery.get_selected_children(); + if (sel.size() == 1) { + return pat.widgets_to_pattern[sel.front()]; + } + else { + return empty; + } +} + +std::pair, SPDocument*> PatternEditor::get_active() { + SPDocument* stock = nullptr; + auto sel = get_active(_doc_gallery, _doc_pattern_store); + if (!sel) { + sel = get_active(_stock_gallery, _stock_pattern_store); + stock = sel ? sel->collection : nullptr; + } + return std::make_pair(sel, stock); +} + +void PatternEditor::set_active(Gtk::FlowBox& gallery, PatternStore& pat, Glib::RefPtr item) { + bool selected = false; + if (item) { + gallery.foreach([=,&selected,&pat,&gallery](Gtk::Widget& widget){ + if (auto box = dynamic_cast(&widget)) { + if (auto pattern = pat.widgets_to_pattern[box]) { + if (pattern->id == item->id && pattern->collection == item->collection) { + gallery.select_child(*box); + if (item->pix) { + // update preview, it might be stale + sp_traverse_widget_tree(box->get_child(), [&](Gtk::Widget* widget){ + if (auto image = dynamic_cast(widget)) { + image->set(item->pix); + return true; // stop + } + return false; // continue + }); + } + selected = true; + } + } + } + }); + } + + if (!selected) { + gallery.unselect_all(); + } +} + +std::pair PatternEditor::get_selected() { + // document patterns first + auto active = get_active(); + auto sel = active.first; + auto stock_doc = active.second; + std::string id; + if (sel) { + if (stock_doc) { + // for stock pattern, report its root pattern ID + return std::make_pair(sel->id, stock_doc); + } + else { + // for current document, if selection hasn't changed return linked pattern ID + // so that we can modify its properties (transform, offset, gap) + if (sel->id == _current_pattern.id) { + return std::make_pair(_current_pattern.link_id, nullptr); + } + // different pattern from current document selected; use its root pattern + // as a starting point; link pattern will be injected by adjust_pattern() + return std::make_pair(sel->id, nullptr); + } + } + else { + // if nothing is selected, pick first stock pattern, so we have something to assign + // to selected object(s); without it, pattern editing will not be activated + if (auto first = _stock_pattern_store.store.get_store()->get_item(0)) { + return std::make_pair(first->id, first->collection); + } + + // no stock patterns available + return std::make_pair("", nullptr); + } +} + +std::optional PatternEditor::get_selected_color() { + auto pat = get_active(); + if (pat.first && pat.first->color.has_value()) { + return _color_picker->get_current_color(); + } + return std::optional(); // color not supported +} + +Geom::Point PatternEditor::get_selected_offset() { + return Geom::Point(_offset_x.get_value(), _offset_y.get_value()); +} + +Geom::Affine PatternEditor::get_selected_transform() { + Geom::Affine matrix; + + matrix *= Geom::Scale(_scale_x.get_value(), _scale_y.get_value()); + matrix *= Geom::Rotate(_angle_btn.get_value() / 180.0 * M_PI); + auto pat = get_active(); + if (pat.first) { + //TODO: this is imperfect; calculate better offset, if possible + // this translation is kept so there's no sudden jump when editing pattern attributes + matrix.setTranslation(pat.first->transform.translation()); + } + return matrix; +} + +bool PatternEditor::is_selected_scale_uniform() { + return _scale_linked; +} + +Geom::Scale PatternEditor::get_selected_gap() { + auto vx = _gap_x_slider.get_value(); + auto gap_x = slider_to_gap(vx, _gap_x_slider.get_adjustment()->get_upper()); + + auto vy = _gap_y_slider.get_value(); + auto gap_y = slider_to_gap(vy, _gap_y_slider.get_adjustment()->get_upper()); + + return Geom::Scale(gap_x, gap_y); +} + +Glib::ustring PatternEditor::get_label() { + return _name_box.get_text(); +} + +SPPattern* get_pattern(const PatternItem& item, SPDocument* document) { + auto doc = item.collection ? item.collection : document; + if (!doc) return nullptr; + + return cast(doc->getObjectById(item.id)); +} + +void regenerate_tile_images(PatternManager& manager, PatternStore& pat_store, int tile_size, double device_scale, SPDocument* current) { + auto& patterns = pat_store.store.get_items(); + for (auto& item : patterns) { + if (auto pattern = get_pattern(*item.get(), current)) { + item->pix = manager.get_image(pattern, tile_size, tile_size, device_scale); + } + } + pat_store.store.refresh(); +} + +void PatternEditor::update_pattern_tiles() { + const double device_scale = get_scale_factor(); + regenerate_tile_images(_manager, _doc_pattern_store, _tile_size, device_scale, _current_document); + regenerate_tile_images(_manager, _stock_pattern_store, _tile_size, device_scale, nullptr); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape -- cgit v1.2.3