Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
pattern-editor.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
10#include "pattern-editor.h"
11
12#include <iomanip>
13#include <cairo.h>
14#include <glibmm/i18n.h>
15#include <gtkmm/builder.h>
16#include <gtkmm/button.h>
17#include <gtkmm/checkbutton.h>
18#include <gtkmm/comboboxtext.h>
19#include <gtkmm/entry.h>
20#include <gtkmm/flowbox.h>
21#include <gtkmm/grid.h>
22#include <gtkmm/label.h>
23#include <gtkmm/paned.h>
24#include <gtkmm/picture.h>
25#include <gtkmm/scale.h>
26#include <gtkmm/searchentry2.h>
27#include <gtkmm/spinbutton.h>
28#include <gtkmm/treemodel.h>
29#include <gtkmm/viewport.h>
30
31#include "document.h"
32#include "preferences.h"
33
34#include "pattern-manager.h"
36#include "ui/builder-utils.h"
37#include "ui/pack.h"
38#include "ui/util.h"
40
41namespace Inkscape::UI::Widget {
42
43using namespace Inkscape::IO;
44
45// default size of pattern image in a list
46static constexpr int ITEM_WIDTH = 45;
47
48// get slider position 'index' (linear) and transform that into gap percentage (non-linear)
49static double slider_to_gap(double index, double upper) {
50 auto v = std::tan(index / (upper + 1) * M_PI / 2.0) * 500;
51 return std::round(v / 20) * 20;
52}
53// transform gap percentage value into slider position
54static double gap_to_slider(double gap, double upper) {
55 return std::atan(gap / 500) * (upper + 1) / M_PI * 2;
56}
57
58// tile size slider functions
59static int slider_to_tile(double index) {
60 return 30 + static_cast<int>(index) * 5;
61}
62static double tile_to_slider(int tile) {
63 return (tile - 30) / 5.0;
64}
65
66Glib::ustring get_attrib(SPPattern* pattern, const char* attrib) {
67 auto value = pattern->getAttribute(attrib);
68 return value ? value : "";
69}
70
71double get_attrib_num(SPPattern* pattern, const char* attrib) {
72 auto val = get_attrib(pattern, attrib);
73 return strtod(val.c_str(), nullptr);
74}
75
76const double ANGLE_STEP = 15.0;
77
79 _manager(manager),
80 _builder(create_builder("pattern-edit.glade")),
81 _offset_x(get_widget<Gtk::SpinButton>(_builder, "offset-x")),
82 _offset_y(get_widget<Gtk::SpinButton>(_builder, "offset-y")),
83 _scale_x(get_widget<Gtk::SpinButton>(_builder, "scale-x")),
84 _scale_y(get_widget<Gtk::SpinButton>(_builder, "scale-y")),
85 _angle_btn(get_widget<Gtk::SpinButton>(_builder, "angle")),
86 _orient_slider(get_widget<Gtk::Scale>(_builder, "orient")),
87 _gap_x_slider(get_widget<Gtk::Scale>(_builder, "gap-x")),
88 _gap_y_slider(get_widget<Gtk::Scale>(_builder, "gap-y")),
89 _gap_x_spin(get_widget<Gtk::SpinButton>(_builder, "gap-x-spin")),
90 _gap_y_spin(get_widget<Gtk::SpinButton>(_builder, "gap-y-spin")),
91 _edit_btn(get_widget<Gtk::Button>(_builder, "edit-pattern")),
92 _preview_img(get_widget<Gtk::Picture>(_builder, "preview")),
93 _preview(get_widget<Gtk::Viewport>(_builder, "preview-box")),
94 _color_label(get_widget<Gtk::Label>(_builder, "color-label")),
95 _paned(get_widget<Gtk::Paned>(_builder, "paned")),
96 _main_grid(get_widget<Gtk::Box>(_builder, "main-box")),
97 _input_grid(get_widget<Gtk::Grid>(_builder, "input-grid")),
98 _stock_gallery(get_widget<Gtk::FlowBox>(_builder, "flowbox")),
99 _doc_gallery(get_widget<Gtk::FlowBox>(_builder, "doc-flowbox")),
100 _link_scale(get_widget<Gtk::Button>(_builder, "link-scale")),
101 _name_box(get_widget<Gtk::Entry>(_builder, "pattern-name")),
102 _combo_set(get_widget<Gtk::ComboBoxText>(_builder, "pattern-combo")),
103 _search_box(get_widget<Gtk::SearchEntry2>(_builder, "search")),
104 _tile_slider(get_widget<Gtk::Scale>(_builder, "tile-slider")),
105 _show_names(get_widget<Gtk::CheckButton>(_builder, "show-names")),
106 _color_picker(get_derived_widget<ColorPicker>(_builder, "color-btn", _("Pattern color"), false)),
107 _prefs(prefs)
108{
109 _color_picker.connectChanged([this](Colors::Color const &color){
110 if (_update.pending()) return;
111 _signal_color_changed.emit(color);
112 });
113
114 // there's enough space for one set of controls only:
115 auto set_gap_control = [this](){
117 _gap_x_slider.set_visible(false);
118 _gap_y_slider.set_visible(false);
119 _gap_x_spin.set_visible();
120 _gap_y_spin.set_visible();
121 }
122 else {
123 _gap_x_spin.set_visible(false);
124 _gap_y_spin.set_visible(false);
125 _gap_x_slider.set_visible();
126 _gap_y_slider.set_visible();
127 }
128 };
129
132 _tile_slider.signal_change_value().connect([this](Gtk::ScrollType st, double value){
133 if (_update.pending()) return true;
134 auto scoped(_update.block());
135 auto size = slider_to_tile(value);
136 if (size != _tile_size) {
137 _tile_slider.set_value(tile_to_slider(size));
138 // change pattern tile size
141 Inkscape::Preferences::get()->setInt(_prefs + "/tileSize", size);
142 }
143 return true;
144 }, true);
145 _precise_gap_control = Inkscape::Preferences::get()->getBool(_prefs + "/preciseGapControl", false);
146 auto precise_gap = &get_widget<Gtk::CheckButton>(_builder, "gap-spin");
147 auto& mouse_friendly = get_widget<Gtk::CheckButton>(_builder, "gap-slider");
148 precise_gap->set_active(_precise_gap_control);
149 mouse_friendly.set_active(!_precise_gap_control);
150 precise_gap->signal_toggled().connect([=, this] {
151 auto precise = precise_gap->get_active();
152 if (_precise_gap_control != precise) {
153 _precise_gap_control = precise;
154 set_gap_control();
155 Inkscape::Preferences::get()->setBool(_prefs + "/preciseGapControl", precise);
156 }
157 });
158
159 auto show_labels = Inkscape::Preferences::get()->getBool(_prefs + "/showLabels", false);
160 _show_names.set_active(show_labels);
161 _show_names.signal_toggled().connect([this](){
162 // toggle pattern labels
163 _stock_pattern_store.store.refresh();
164 _doc_pattern_store.store.refresh();
165 Inkscape::Preferences::get()->setBool(_prefs + "/showLabels", _show_names.get_active());
166 });
167
168 const auto max = 180.0 / ANGLE_STEP;
169 _orient_slider.set_range(-max, max);
170 _orient_slider.set_increments(1, 1);
171 _orient_slider.set_digits(0);
172 _orient_slider.set_value(0);
173 _orient_slider.signal_change_value().connect([=, this](Gtk::ScrollType st, double value){
174 if (_update.pending()) return false;
175 auto scoped(_update.block());
176 // slider works with 15deg discrete steps
177 _angle_btn.set_value(round(CLAMP(value, -max, max)) * ANGLE_STEP);
178 _signal_changed.emit();
179 return true;
180 }, true);
181
182 for (auto spin : {&_gap_x_spin, &_gap_y_spin}) {
183 spin->signal_value_changed().connect([spin, this](){
184 if (_update.pending() || !spin->is_sensitive()) return;
185 _signal_changed.emit();
186 });
187 }
188
189 for (auto slider : {&_gap_x_slider, &_gap_y_slider}) {
190 slider->set_increments(1, 1);
191 slider->set_digits(0);
192 slider->set_value(0);
193 slider->set_format_value_func([=](double val){
194 auto upper = slider->get_adjustment()->get_upper();
195 return Inkscape::ustring::format_classic(std::fixed, std::setprecision(0), slider_to_gap(val, upper)) + "%";
196 });
197 slider->signal_change_value().connect([this](Gtk::ScrollType st, double value){
198 if (_update.pending()) return false;
199 _signal_changed.emit();
200 return true;
201 }, true);
202 }
203
204 set_gap_control();
205
206 _angle_btn.signal_value_changed().connect([this]() {
207 if (_update.pending() || !_angle_btn.is_sensitive()) return;
208 auto scoped(_update.block());
209 auto angle = _angle_btn.get_value();
210 _orient_slider.set_value(round(angle / ANGLE_STEP));
211 _signal_changed.emit();
212 });
213
214 _link_scale.signal_clicked().connect([this](){
215 if (_update.pending()) return;
216 auto scoped(_update.block());
218 if (_scale_linked) {
219 // this is simplistic
220 _scale_x.set_value(_scale_y.get_value());
221 }
223 _signal_changed.emit();
224 });
225
226 for (auto el : {&_scale_x, &_scale_y, &_offset_x, &_offset_y}) {
227 el->signal_value_changed().connect([el, this]() {
228 if (_update.pending()) return;
229 if (_scale_linked && (el == &_scale_x || el == &_scale_y)) {
230 auto scoped(_update.block());
231 // enforce uniform scaling
232 (el == &_scale_x) ? _scale_y.set_value(el->get_value()) : _scale_x.set_value(el->get_value());
233 }
234 _signal_changed.emit();
235 });
236 }
237
238 _name_box.signal_changed().connect([this](){
239 if (_update.pending()) return;
240
241 _signal_changed.emit();
242 });
243
244 _search_box.signal_search_changed().connect([this](){
245 if (_update.pending()) return;
246
247 // filter patterns
248 _filter_text = _search_box.get_text();
249 apply_filter(false);
250 apply_filter(true);
251 });
252
253 // populate combo box with all patern categories
254 auto pattern_categories = _manager.get_categories()->children();
255 int cat_count = pattern_categories.size();
256 for (auto row : pattern_categories) {
257 auto name = row.get_value(_manager.columns.name);
258 _combo_set.append(name);
259 }
260
261 get_widget<Gtk::Button>(_builder, "previous").signal_clicked().connect([this](){
262 int previous = _combo_set.get_active_row_number() - 1;
263 if (previous >= 0) _combo_set.set_active(previous);
264 });
265 get_widget<Gtk::Button>(_builder, "next").signal_clicked().connect([cat_count, this](){
266 auto next = _combo_set.get_active_row_number() + 1;
267 if (next < cat_count) _combo_set.set_active(next);
268 });
269 _combo_set.signal_changed().connect([this](){
270 // select pattern category to show
271 auto index = _combo_set.get_active_row_number();
273 Inkscape::Preferences::get()->setInt(_prefs + "/currentSet", index);
274 });
275
278
279 _stock_gallery.signal_child_activated().connect([this](Gtk::FlowBoxChild* box){
280 if (_update.pending()) return;
281 auto scoped(_update.block());
283 update_ui(pat);
284 _doc_gallery.unselect_all();
285 _signal_changed.emit();
286 });
287
288 _doc_gallery.signal_child_activated().connect([this](Gtk::FlowBoxChild* box){
289 if (_update.pending()) return;
290 auto scoped(_update.block());
292 update_ui(pat);
293 _stock_gallery.unselect_all();
294 _signal_changed.emit();
295 });
296
297 _edit_btn.signal_clicked().connect([this](){
298 _signal_edit.emit();
299 });
300
301 _paned.set_position(Inkscape::Preferences::get()->getIntLimited(_prefs + "/handlePos", 50, 10, 9999));
302 _paned.property_position().signal_changed().connect([this](){
303 Inkscape::Preferences::get()->setInt(_prefs + "/handlePos", _paned.get_position());
304 });
305
306 // current pattern category
307 _combo_set.set_active(Inkscape::Preferences::get()->getIntLimited(_prefs + "/currentSet", 0, 0, std::max(cat_count - 1, 0)));
308
311}
312
313void PatternEditor::bind_store(Gtk::FlowBox& list, PatternStore& pat) {
314 pat.store.set_filter([this](const Glib::RefPtr<PatternItem>& p){
315 if (!p) return false;
316 if (_filter_text.empty()) return true;
317
318 auto name = Glib::ustring(p->label).lowercase();
319 auto expr = _filter_text.lowercase();
320 auto pos = name.find(expr);
321 return pos != Glib::ustring::npos;
322 });
323
324 list.bind_list_store(pat.store.get_store(), [&pat, this](const Glib::RefPtr<PatternItem>& item){
325 auto const box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
326 auto const image = Gtk::make_managed<Gtk::Image>(to_texture(item->pix));
327 image->set_size_request(_tile_size, _tile_size);
328 UI::pack_start(*box, *image);
329 auto name = Glib::ustring(item->label.c_str());
330 if (_show_names.get_active()) {
331 auto const label = Gtk::make_managed<Gtk::Label>(name);
332 label->add_css_class("small-font");
333 // limit label size to tile size
334 label->set_ellipsize(Pango::EllipsizeMode::END);
335 label->set_max_width_chars(0);
336 label->set_size_request(_tile_size);
337 UI::pack_end(*box, *label);
338 }
339 image->set_tooltip_text(name);
340
341 auto const cbox = Gtk::make_managed<Gtk::FlowBoxChild>();
342 cbox->set_child(*box);
343 cbox->add_css_class("pattern-item-box");
344 pat.widgets_to_pattern[cbox] = item;
345 return cbox;
346 });
347}
348
350 auto sets = _manager.get_categories()->children();
351 if (index >= 0 && index < sets.size()) {
352 auto row = sets[index];
353 if (auto category = row.get_value(_manager.columns.category)) {
354 set_stock_patterns(category->patterns);
355 }
356 }
357}
358
360 _link_scale.set_child(get_widget<Gtk::Image>(_builder, _scale_linked ? "image-linked" : "image-unlinked"));
361}
362
363void PatternEditor::update_widgets_from_pattern(Glib::RefPtr<PatternItem>& pattern) {
364 _input_grid.set_sensitive(!!pattern);
365
366 static auto const empty = PatternItem::create();
367 auto const &item = pattern ? *pattern : *empty;
368
369 _name_box.set_text(item.label.c_str());
370
371 _scale_x.set_value(item.transform.xAxis().length());
372 _scale_y.set_value(item.transform.yAxis().length());
373
374 // TODO if needed
375 // auto units = get_attrib(pattern, "patternUnits");
376
377 _scale_linked = item.uniform_scale;
379
380 _offset_x.set_value(item.offset.x());
381 _offset_y.set_value(item.offset.y());
382
383 auto degrees = 180.0 / M_PI * Geom::atan2(item.transform.xAxis());
384 _orient_slider.set_value(round(degrees / ANGLE_STEP));
385 _angle_btn.set_value(degrees);
386
387 double x_index = gap_to_slider(item.gap[Geom::X], _gap_x_slider.get_adjustment()->get_upper());
388 _gap_x_slider.set_value(x_index);
389 _gap_x_spin.set_value(item.gap[Geom::X]);
390 double y_index = gap_to_slider(item.gap[Geom::Y], _gap_y_slider.get_adjustment()->get_upper());
391 _gap_y_slider.set_value(y_index);
392 _gap_y_spin.set_value(item.gap[Geom::Y]);
393
394 if (item.color.has_value()) {
396 _color_picker.set_sensitive();
397 _color_label.set_opacity(1.0); // hack: sensitivity doesn't change appearance, so using opacity directly
398 }
399 else {
401 _color_picker.set_sensitive(false);
402 _color_label.set_opacity(0.6);
404 }
405}
406
407void PatternEditor::update_ui(Glib::RefPtr<PatternItem> pattern) {
409}
410
411// sort patterns in-place by name/id
412void sort_patterns(std::vector<Glib::RefPtr<PatternItem>>& list) {
413 std::sort(list.begin(), list.end(), [](Glib::RefPtr<PatternItem>& a, Glib::RefPtr<PatternItem>& b) {
414 if (!a || !b) return false;
415 if (a->label == b->label) {
416 return a->id < b->id;
417 }
418 return a->label < b->label;
419 });
420}
421
422// given a pattern, create a PatternItem instance that describes it;
423// input pattern can be a link or a root pattern
424Glib::RefPtr<PatternItem> create_pattern_item(PatternManager& manager, SPPattern* pattern, int tile_size, double scale) {
425 auto item = manager.get_item(pattern);
426 if (item && scale > 0) {
427 item->pix = manager.get_image(pattern, tile_size, tile_size, scale);
428 }
429 return item;
430}
431
432// update editor UI
433void PatternEditor::set_selected(SPPattern* pattern) {
434 auto scoped(_update.block());
435
436 _stock_gallery.unselect_all();
437
438 // current pattern (should be a link)
439 auto link_pattern = pattern;
440 if (pattern) pattern = pattern->rootPattern();
441
442 if (pattern && pattern != link_pattern) {
443 _current_pattern.id = pattern->getId();
444 _current_pattern.link_id = link_pattern->getId();
445 _current_pattern.offset = link_pattern->getTransform().translation();
446 }
447 else {
448 _current_pattern.id.clear();
449 _current_pattern.link_id.clear();
450 _current_pattern.offset = {};
451 }
452
453 auto item = create_pattern_item(_manager, link_pattern, 0, 0);
454
455 update_widgets_from_pattern(item);
456
457 auto list = update_doc_pattern_list(pattern ? pattern->document : nullptr);
458 if (pattern) {
459 // patch up tile image on a list of document root patterns, it might have changed;
460 // color attribute for instance is being set directly on the root pattern;
461 // other attributes are per-object, so should not be taken into account when rendering tile
462 for (auto& pattern_item : list) {
463 if (pattern_item->id == item->id && pattern_item->collection == nullptr) {
464 // update preview
465 const double device_scale = get_scale_factor();
466 pattern_item->pix = _manager.get_image(pattern, _tile_size, _tile_size, device_scale);
467 item->pix = pattern_item->pix;
468 break;
469 }
470 }
471 }
472
473 set_active(_doc_gallery, _doc_pattern_store, item);
474
475 // generate large preview of selected pattern
476 if (link_pattern) {
477 const double device_scale = get_scale_factor();
478 auto size = _preview.get_allocation();
479 const int m = 1;
480 if (size.get_width() <= m || size.get_height() <= m) {
481 // widgets not resized yet, choose arbitrary size, so preview is not missing when widget is shown
482 size.set_width(200);
483 size.set_height(200);
484 }
485 // use white for checkerboard since most stock patterns are black
486 unsigned int background = 0xffffffff;
487 auto surface = _manager.get_preview(link_pattern, size.get_width(), size.get_height(), background, device_scale);
488 _preview_img.set_paintable(to_texture(surface));
489 } else {
490 _preview_img.set_paintable(nullptr);
491 }
492}
493
494// generate preview images for patterns
495std::vector<Glib::RefPtr<PatternItem>> create_pattern_items(PatternManager& manager, const std::vector<SPPattern*>& list, int tile_size, double device_scale) {
496 std::vector<Glib::RefPtr<PatternItem>> output;
497 output.reserve(list.size());
498
499 for (auto pat : list) {
500 if (auto item = create_pattern_item(manager, pat, tile_size, device_scale)) {
501 output.push_back(item);
502 }
503 }
504
505 return output;
506}
507
508// populate store with document patterns if list has changed, minimize amount of work by using cached previews
509std::vector<Glib::RefPtr<PatternItem>> PatternEditor::update_doc_pattern_list(SPDocument* document) {
510 auto list = sp_get_pattern_list(document);
511 const double device_scale = get_scale_factor();
512 // create pattern items (cheap), but skip preview generation (expansive)
513 auto patterns = create_pattern_items(_manager, list, 0, 0);
514 bool modified = false;
515 for (auto&& item : patterns) {
516 auto it = _cached_items.find(item->id);
517 if (it != end(_cached_items)) {
518 // reuse cached preview image
519 if (!item->pix) item->pix = it->second->pix;
520 }
521 else {
522 if (!item->pix) {
523 // generate preview for newly added pattern
524 item->pix = _manager.get_image(cast<SPPattern>(document->getObjectById(item->id)), _tile_size, _tile_size, device_scale);
525 }
526 modified = true;
527 _cached_items[item->id] = item;
528 }
529 }
530
531 update_store(patterns, _doc_gallery, _doc_pattern_store);
532
533 return patterns;
534}
535
536void PatternEditor::set_document(SPDocument* document) {
537 _current_document = document;
538 _cached_items.clear();
539 update_doc_pattern_list(document);
540}
541
542// populate store with stock patterns
543void PatternEditor::set_stock_patterns(const std::vector<SPPattern*>& list) {
544 const double device_scale = get_scale_factor();
545 auto patterns = create_pattern_items(_manager, list, _tile_size, device_scale);
546 sort_patterns(patterns);
547 update_store(patterns, _stock_gallery, _stock_pattern_store);
548}
549
550void PatternEditor::apply_filter(bool stock) {
551 auto scoped(_update.block());
552 if (!stock) {
553 _doc_pattern_store.store.apply_filter();
554 }
555 else {
556 _stock_pattern_store.store.apply_filter();
557 }
558}
559
560void PatternEditor::update_store(const std::vector<Glib::RefPtr<PatternItem>>& list, Gtk::FlowBox& gallery, PatternStore& pat) {
561 auto selected = get_active(gallery, pat);
562 if (pat.store.assign(list)) {
563 // reselect current
564 set_active(gallery, pat, selected);
565 }
566}
567
568Glib::RefPtr<PatternItem> PatternEditor::get_active(Gtk::FlowBox& gallery, PatternStore& pat) {
569 auto empty = Glib::RefPtr<PatternItem>();
570
571 auto sel = gallery.get_selected_children();
572 if (sel.size() == 1) {
573 return pat.widgets_to_pattern[sel.front()];
574 }
575 else {
576 return empty;
577 }
578}
579
580std::pair<Glib::RefPtr<PatternItem>, SPDocument*> PatternEditor::get_active() {
581 SPDocument* stock = nullptr;
582 auto sel = get_active(_doc_gallery, _doc_pattern_store);
583 if (!sel) {
584 sel = get_active(_stock_gallery, _stock_pattern_store);
585 stock = sel ? sel->collection : nullptr;
586 }
587 return std::make_pair(sel, stock);
588}
589
590void PatternEditor::set_active(Gtk::FlowBox& gallery, PatternStore& pat, Glib::RefPtr<PatternItem> item) {
591 bool selected = false;
592 if (item) {
593 UI::for_each_child(gallery, [=,&selected,&pat,&gallery](Gtk::Widget& widget){
594 if (auto box = dynamic_cast<Gtk::FlowBoxChild*>(&widget)) {
595 if (auto pattern = pat.widgets_to_pattern[box]) {
596 if (pattern->id == item->id && pattern->collection == item->collection) {
597 gallery.select_child(*box);
598 if (item->pix) {
599 // update preview, it might be stale
600 for_each_descendant(*box, [&](Gtk::Widget &widget){
601 if (auto const image = dynamic_cast<Gtk::Image *>(&widget)) {
602 image->set(to_texture(item->pix));
603 return ForEachResult::_break;
604 }
605 return ForEachResult::_continue;
606 });
607 }
608 selected = true;
609 }
610 }
611 }
613 });
614 }
615
616 if (!selected) {
617 gallery.unselect_all();
618 }
619}
620
621std::pair<std::string, SPDocument*> PatternEditor::get_selected() {
622 // document patterns first
623 auto active = get_active();
624 auto sel = active.first;
625 auto stock_doc = active.second;
626 std::string id;
627 if (sel) {
628 if (stock_doc) {
629 // for stock pattern, report its root pattern ID
630 return std::make_pair(sel->id, stock_doc);
631 }
632 else {
633 // for current document, if selection hasn't changed return linked pattern ID
634 // so that we can modify its properties (transform, offset, gap)
635 if (sel->id == _current_pattern.id.raw()) {
636 return std::make_pair(_current_pattern.link_id, nullptr);
637 }
638 // different pattern from current document selected; use its root pattern
639 // as a starting point; link pattern will be injected by adjust_pattern()
640 return std::make_pair(sel->id, nullptr);
641 }
642 }
643 else {
644 // if nothing is selected, pick first stock pattern, so we have something to assign
645 // to selected object(s); without it, pattern editing will not be activated
646 if (auto first = _stock_pattern_store.store.get_store()->get_item(0)) {
647 return std::make_pair(first->id, first->collection);
648 }
649
650 // no stock patterns available
651 return std::make_pair("", nullptr);
652 }
653}
654
655std::optional<Colors::Color> PatternEditor::get_selected_color() {
656 auto pat = get_active();
657 if (pat.first && pat.first->color.has_value()) {
658 return _color_picker.get_current_color();
659 }
660 return {}; // color not supported
661}
662
663Geom::Point PatternEditor::get_selected_offset() {
664 return Geom::Point(_offset_x.get_value(), _offset_y.get_value());
665}
666
667Geom::Affine PatternEditor::get_selected_transform() {
668 Geom::Affine matrix;
669
670 matrix *= Geom::Scale(_scale_x.get_value(), _scale_y.get_value());
671 matrix *= Geom::Rotate(_angle_btn.get_value() / 180.0 * M_PI);
672 matrix.setTranslation(_current_pattern.offset);
673 return matrix;
674}
675
676bool PatternEditor::is_selected_scale_uniform() {
677 return _scale_linked;
678}
679
680Geom::Scale PatternEditor::get_selected_gap() {
681 auto vx = _gap_x_slider.get_value();
682 auto gap_x = _precise_gap_control ? _gap_x_spin.get_value() : slider_to_gap(vx, _gap_x_slider.get_adjustment()->get_upper());
683
684 auto vy = _gap_y_slider.get_value();
685 auto gap_y = _precise_gap_control ? _gap_y_spin.get_value() : slider_to_gap(vy, _gap_y_slider.get_adjustment()->get_upper());
686
687 return Geom::Scale(gap_x, gap_y);
688}
689
690Glib::ustring PatternEditor::get_label() {
691 return _name_box.get_text();
692}
693
695 auto doc = item.collection ? item.collection : document;
696 if (!doc) return nullptr;
697
698 return cast<SPPattern>(doc->getObjectById(item.id));
699}
700
701void regenerate_tile_images(PatternManager& manager, PatternStore& pat_store, int tile_size, double device_scale, SPDocument* current) {
702 auto& patterns = pat_store.store.get_items();
703 for (auto& item : patterns) {
704 if (auto pattern = get_pattern(*item.get(), current)) {
705 item->pix = manager.get_image(pattern, tile_size, tile_size, device_scale);
706 }
707 }
708 pat_store.store.refresh();
709}
710
711void PatternEditor::update_pattern_tiles() {
712 const double device_scale = get_scale_factor();
713 regenerate_tile_images(_manager, _doc_pattern_store, _tile_size, device_scale, _current_document);
714 regenerate_tile_images(_manager, _stock_pattern_store, _tile_size, device_scale, nullptr);
715}
716
717} // namespace Inkscape::UI::Widget
718
719/*
720 Local Variables:
721 mode:c++
722 c-file-style:"stroustrup"
723 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
724 indent-tabs-mode:nil
725 fill-column:99
726 End:
727*/
728// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99:
double scale
Definition aa.cpp:228
Gtk builder utilities.
Cairo::RefPtr< Cairo::ImageSurface > surface
Definition canvas.cpp:137
int tile_size
Definition canvas.cpp:164
3x3 matrix representing an affine transformation.
Definition affine.h:70
void setTranslation(Point const &loc)
Sets the translation imparted by the Affine.
Definition affine.cpp:56
Point yAxis() const
Definition affine.cpp:36
Point xAxis() const
Definition affine.cpp:32
Two-dimensional point that doubles as a vector.
Definition point.h:66
Coord length() const
Compute the distance from origin.
Definition point.h:118
Rotation around the origin.
Definition transforms.h:187
Scaling from the origin.
Definition transforms.h:150
Gtk::TreeModelColumn< Glib::ustring > name
Gtk::TreeModelColumn< std::shared_ptr< Category > > category
Glib::RefPtr< Gtk::TreeModel > get_categories()
PatternCategoryColumns columns
Cairo::RefPtr< Cairo::Surface > get_image(SPPattern *pattern, int width, int height, double device_scale)
Glib::RefPtr< Inkscape::UI::Widget::PatternItem > get_item(SPPattern *pattern)
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
void setInt(Glib::ustring const &pref_path, int value)
Set an integer value.
int getIntLimited(Glib::ustring const &pref_path, int def=0, int min=INT_MIN, int max=INT_MAX)
Retrieve a limited integer.
void setBool(Glib::ustring const &pref_path, bool value)
Set a Boolean value.
sigc::connection connectChanged(sigc::slot< void(Colors::Color const &)> slot)
void setColor(Colors::Color const &)
Helperclass for Gtk::Entry widgets.
Definition entry.h:24
void update_widgets_from_pattern(Glib::RefPtr< PatternItem > &pattern)
sigc::signal< void(Colors::Color const &)> _signal_color_changed
Inkscape::PatternManager & _manager
void update_ui(Glib::RefPtr< PatternItem > pattern)
sigc::signal< void()> _signal_changed
Glib::RefPtr< Gtk::Builder > _builder
void set_stock_patterns(const std::vector< SPPattern * > &patterns)
void bind_store(Gtk::FlowBox &list, PatternStore &store)
PatternEditor(const char *prefs, PatternManager &manager)
static Glib::RefPtr< PatternItem > create()
SpinButton widget, that allows entry of simple math expressions (also units, when linked with UnitMen...
Definition spinbutton.h:52
scoped_block block()
Typed SVG document implementation.
Definition document.h:103
SPObject * getObjectById(std::string const &id) const
Geom::Affine transform
Definition sp-item.h:138
char const * label() const
Gets the author-visible label property for the object or a default if no label is defined.
SPDocument * document
Definition sp-object.h:188
char * id
Definition sp-object.h:192
char const * getId() const
Returns the objects current ID string.
char const * getAttribute(char const *name) const
SPPattern const * rootPattern() const
static char const *const current
Definition dir-util.cpp:71
@ Y
Definition coord.h:48
@ X
Definition coord.h:48
std::unique_ptr< Magick::Image > image
SPItem * item
Glib::ustring label
double atan2(Point const &p)
Definition desktop.h:50
Low-level IO code.
Custom widgets.
Definition desktop.h:126
std::vector< Glib::RefPtr< PatternItem > > create_pattern_items(PatternManager &manager, const std::vector< SPPattern * > &list, int tile_size, double device_scale)
SPPattern * get_pattern(const PatternItem &item, SPDocument *document)
Glib::RefPtr< PatternItem > create_pattern_item(PatternManager &manager, SPPattern *pattern, int tile_size, double scale)
Glib::ustring get_attrib(SPMarker *marker, const char *attrib)
void regenerate_tile_images(PatternManager &manager, PatternStore &pat_store, int tile_size, double device_scale, SPDocument *current)
void sort_patterns(std::vector< Glib::RefPtr< PatternItem > > &list)
static double slider_to_gap(double index, double upper)
static int slider_to_tile(double index)
static double gap_to_slider(double gap, double upper)
static double tile_to_slider(int tile)
static constexpr int ITEM_WIDTH
double get_attrib_num(SPMarker *marker, const char *attrib)
void pack_end(Gtk::Box &box, Gtk::Widget &child, bool const expand, bool const fill, unsigned const padding)
Adds child to box, packed with reference to the end of box.
Definition pack.cpp:153
W & get_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id)
Gtk::Widget * for_each_child(Gtk::Widget &widget, Func &&func, bool const plus_self=false, bool const recurse=false, int const level=0)
Call Func with a reference to each child of parent, until it returns _break.
Definition util.h:103
void pack_start(Gtk::Box &box, Gtk::Widget &child, bool const expand, bool const fill, unsigned const padding)
Adds child to box, packed with reference to the start of box.
Definition pack.cpp:141
W & get_derived_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id, Args &&... args)
Glib::RefPtr< Gtk::Builder > create_builder(const char *filename)
Glib::ustring format_classic(T const &... args)
Helpers for using Gtk::Boxes, encapsulating large changes between GTK3 & GTK4.
std::vector< SPPattern * > sp_get_pattern_list(SPDocument *source)
Singleton class to access the preferences file in a convenient way.
std::map< Gtk::Widget *, Glib::RefPtr< PatternItem > > widgets_to_pattern
Inkscape::FilteredStore< PatternItem > store
int index
Glib::ustring name
Definition toolbars.cpp:55
Glib::RefPtr< Gdk::Texture > to_texture(Cairo::RefPtr< Cairo::Surface > const &surface)
Convert an image surface in ARGB32 format to a texture.
Definition util.cpp:509