summaryrefslogtreecommitdiffstats
path: root/src/ui/widget/pattern-editor.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/ui/widget/pattern-editor.cpp
parentInitial commit. (diff)
downloadinkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz
inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/ui/widget/pattern-editor.cpp')
-rw-r--r--src/ui/widget/pattern-editor.cpp685
1 files changed, 685 insertions, 0 deletions
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 <gtkmm/widget.h>
+#include <optional>
+#include <gtkmm/builder.h>
+#include <gtkmm/grid.h>
+#include <gtkmm/togglebutton.h>
+#include <gtkmm/button.h>
+#include <gtkmm/menubutton.h>
+#include <gtkmm/treeview.h>
+#include <gtkmm/treemodelcolumn.h>
+#include <glibmm/i18n.h>
+#include <cairo.h>
+#include <iomanip>
+
+#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<int>(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<Gtk::SpinButton>(_builder, "offset-x")),
+ _offset_y(get_widget<Gtk::SpinButton>(_builder, "offset-y")),
+ _scale_x(get_widget<Gtk::SpinButton>(_builder, "scale-x")),
+ _scale_y(get_widget<Gtk::SpinButton>(_builder, "scale-y")),
+ _angle_btn(get_widget<Gtk::SpinButton>(_builder, "angle")),
+ _orient_slider(get_widget<Gtk::Scale>(_builder, "orient")),
+ _gap_x_slider(get_widget<Gtk::Scale>(_builder, "gap-x")),
+ _gap_y_slider(get_widget<Gtk::Scale>(_builder, "gap-y")),
+ _edit_btn(get_widget<Gtk::Button>(_builder, "edit-pattern")),
+ _preview_img(get_widget<Gtk::Image>(_builder, "preview")),
+ _preview(get_widget<Gtk::Viewport>(_builder, "preview-box")),
+ _color_btn(get_widget<Gtk::Button>(_builder, "color-btn")),
+ _color_label(get_widget<Gtk::Label>(_builder, "color-label")),
+ _paned(get_widget<Gtk::Paned>(_builder, "paned")),
+ _main_grid(get_widget<Gtk::Box>(_builder, "main-box")),
+ _input_grid(get_widget<Gtk::Grid>(_builder, "input-grid")),
+ _stock_gallery(get_widget<Gtk::FlowBox>(_builder, "flowbox")),
+ _doc_gallery(get_widget<Gtk::FlowBox>(_builder, "doc-flowbox")),
+ _link_scale(get_widget<Gtk::Button>(_builder, "link-scale")),
+ _name_box(get_widget<Gtk::Entry>(_builder, "pattern-name")),
+ _combo_set(get_widget<Gtk::ComboBoxText>(_builder, "pattern-combo")),
+ _search_box(get_widget<Gtk::SearchEntry>(_builder, "search")),
+ _tile_slider(get_widget<Gtk::Scale>(_builder, "tile-slider")),
+ _show_names(get_widget<Gtk::CheckButton>(_builder, "show-names")),
+ _prefs(prefs)
+{
+ _color_picker = std::make_unique<ColorPicker>(
+ _("Pattern color"), "", 0x7f7f7f00, true,
+ &get_widget<Gtk::Button>(_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<Gtk::Button>(_builder, "previous").signal_clicked().connect([=](){
+ int previous = _combo_set.get_active_row_number() - 1;
+ if (previous >= 0) _combo_set.set_active(previous);
+ });
+ get_widget<Gtk::Button>(_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<PatternItem>& 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<PatternItem>& item){
+ auto box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_VERTICAL);
+ auto image = Gtk::make_managed<Gtk::Image>(item->pix);
+ box->pack_start(*image);
+ auto name = Glib::ustring(item->label.c_str());
+ if (_show_names.get_active()) {
+ auto label = Gtk::make_managed<Gtk::Label>(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<Gtk::FlowBoxChild>();
+ 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<Gtk::Image>(_builder, _scale_linked ? "image-linked" : "image-unlinked"));
+}
+
+void PatternEditor::update_widgets_from_pattern(Glib::RefPtr<PatternItem>& 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<PatternItem> pattern) {
+ update_widgets_from_pattern(pattern);
+}
+
+// sort patterns in-place by name/id
+void sort_patterns(std::vector<Glib::RefPtr<PatternItem>>& list) {
+ std::sort(list.begin(), list.end(), [](Glib::RefPtr<PatternItem>& a, Glib::RefPtr<PatternItem>& 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<PatternItem> 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<Cairo::Surface> 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<Glib::RefPtr<PatternItem>> create_pattern_items(PatternManager& manager, const std::vector<SPPattern*>& list, int tile_size, double device_scale) {
+ std::vector<Glib::RefPtr<PatternItem>> 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<Glib::RefPtr<PatternItem>> PatternEditor::update_doc_pattern_list(SPDocument* document) {
+ auto list = sp_get_pattern_list(document);
+ std::shared_ptr<SPDocument> 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<SPPattern>(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<SPPattern*>& 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<Glib::RefPtr<PatternItem>>& 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<PatternItem> PatternEditor::get_active(Gtk::FlowBox& gallery, PatternStore& pat) {
+ auto empty = Glib::RefPtr<PatternItem>();
+
+ auto sel = gallery.get_selected_children();
+ if (sel.size() == 1) {
+ return pat.widgets_to_pattern[sel.front()];
+ }
+ else {
+ return empty;
+ }
+}
+
+std::pair<Glib::RefPtr<PatternItem>, 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<PatternItem> item) {
+ bool selected = false;
+ if (item) {
+ gallery.foreach([=,&selected,&pat,&gallery](Gtk::Widget& widget){
+ if (auto box = dynamic_cast<Gtk::FlowBoxChild*>(&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<Gtk::Image*>(widget)) {
+ image->set(item->pix);
+ return true; // stop
+ }
+ return false; // continue
+ });
+ }
+ selected = true;
+ }
+ }
+ }
+ });
+ }
+
+ if (!selected) {
+ gallery.unselect_all();
+ }
+}
+
+std::pair<std::string, SPDocument*> 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<unsigned int> 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<unsigned int>(); // 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<SPPattern>(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