Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
filter-effects-dialog.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
5/* Authors:
6 * Nicholas Bishop <nicholasbishop@gmail.org>
7 * Rodrigo Kumpera <kumpera@gmail.com>
8 * Felipe C. da S. Sanches <juca@members.fsf.org>
9 * Jon A. Cruz <jon@joncruz.org>
10 * Abhishek Sharma
11 * insaner
12 *
13 * Copyright (C) 2007 Authors
14 *
15 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
16 */
17
19
20#include <map>
21#include <set>
22#include <string>
23#include <sstream>
24#include <utility>
25#include <vector>
26#include <glibmm/convert.h>
27#include <glibmm/i18n.h>
28#include <glibmm/main.h>
29#include <glibmm/refptr.h>
30#include <glibmm/stringutils.h>
31#include <glibmm/ustring.h>
32#include <gdkmm/display.h>
33#include <gdkmm/general.h>
34#include <gdkmm/rgba.h>
35#include <gdkmm/seat.h>
36#include <gtkmm/button.h>
37#include <gtkmm/checkbutton.h>
38#include <gtkmm/dragsource.h>
39#include <gtkmm/entry.h>
40#include <gtkmm/enums.h>
41#include <gtkmm/eventcontrollermotion.h>
42#include <gtkmm/frame.h>
43#include <gtkmm/gestureclick.h>
44#include <gtkmm/grid.h>
45#include <gtkmm/image.h>
46#include <gtkmm/label.h>
47#include <gtkmm/menubutton.h>
48#include <gtkmm/paned.h>
49#include <gtkmm/popover.h>
50#include <gtkmm/scrolledwindow.h>
51#include <gtkmm/searchentry2.h>
52#include <gtkmm/sizegroup.h>
53#include <gtkmm/snapshot.h>
54#include <gtkmm/textview.h>
55#include <gtkmm/treeviewcolumn.h>
56#include <gtkmm/treeview.h>
57#include <gtkmm/widget.h>
58#include <pangomm/layout.h>
59#include <sigc++/functors/mem_fun.h>
60
61#include "desktop.h"
62#include "document-undo.h"
63#include "document.h"
64#include "filter-chemistry.h"
65#include "filter-enums.h"
66#include "inkscape-window.h"
67#include "layer-manager.h"
68#include "preferences.h"
69#include "selection.h"
70#include "style.h"
82#include "selection-chemistry.h"
83#include "ui/builder-utils.h"
85#include "ui/controller.h"
88#include "ui/icon-names.h"
89#include "ui/pack.h"
90#include "ui/util.h"
99
100using namespace Inkscape::Filters;
101
102namespace Inkscape::UI::Dialog {
103
108
110
111static Glib::ustring const prefs_path = "/dialogs/filters";
112
113// Returns the number of inputs available for the filter primitive type
114static int input_count(const SPFilterPrimitive* prim)
115{
116 if(!prim)
117 return 0;
118 else if(is<SPFeBlend>(prim) || is<SPFeComposite>(prim) || is<SPFeDisplacementMap>(prim))
119 return 2;
120 else if(is<SPFeMerge>(prim)) {
121 // Return the number of feMergeNode connections plus an extra
122 return (int) (prim->children.size() + 1);
123 }
124 else
125 return 1;
126}
127
128class CheckButtonAttr : public Gtk::CheckButton, public AttrWidget
129{
130public:
131 CheckButtonAttr(bool def, const Glib::ustring& label,
132 Glib::ustring tv, Glib::ustring fv,
133 const SPAttr a, char* tip_text)
134 : Gtk::CheckButton(label),
135 AttrWidget(a, def),
136 _true_val(std::move(tv)), _false_val(std::move(fv))
137 {
138 signal_toggled().connect(signal_attr_changed().make_slot());
139 if (tip_text) {
140 set_tooltip_text(tip_text);
141 }
142 }
143
144 Glib::ustring get_as_attribute() const override
145 {
146 return get_active() ? _true_val : _false_val;
147 }
148
149 void set_from_attribute(SPObject* o) override
150 {
151 const gchar* val = attribute_value(o);
152 if(val) {
153 if(_true_val == val)
154 set_active(true);
155 else if(_false_val == val)
156 set_active(false);
157 } else {
158 set_active(get_default()->as_bool());
159 }
160 }
161private:
162 const Glib::ustring _true_val, _false_val;
163};
164
165class SpinButtonAttr : public Inkscape::UI::Widget::SpinButton, public AttrWidget
166{
167public:
168 SpinButtonAttr(double lower, double upper, double step_inc,
169 double climb_rate, int digits, const SPAttr a, double def, char* tip_text)
170 : Inkscape::UI::Widget::SpinButton(climb_rate, digits),
171 AttrWidget(a, def)
172 {
173 if (tip_text) {
174 set_tooltip_text(tip_text);
175 }
176 set_range(lower, upper);
177 set_increments(step_inc, 0);
178
179 signal_value_changed().connect(signal_attr_changed().make_slot());
180 }
181
182 Glib::ustring get_as_attribute() const override
183 {
184 const double val = get_value();
185
186 if(get_digits() == 0)
187 return Glib::Ascii::dtostr((int)val);
188 else
189 return Glib::Ascii::dtostr(val);
190 }
191
192 void set_from_attribute(SPObject* o) override
193 {
194 const gchar* val = attribute_value(o);
195 if(val){
196 set_value(Glib::Ascii::strtod(val));
197 } else {
198 set_value(get_default()->as_double());
199 }
200 }
201};
202
203template <typename T> class ComboWithTooltip
204 : public ComboBoxEnum<T>
205{
206public:
207 ComboWithTooltip(T const default_value, Util::EnumDataConverter<T> const &c,
208 SPAttr const a = SPAttr::INVALID,
209 Glib::ustring const &tip_text = {})
210 : ComboBoxEnum<T>(default_value, c, a, false)
211 {
212 this->set_tooltip_text(tip_text);
213 }
214};
215
216// Contains an arbitrary number of spin buttons that use separate attributes
217class MultiSpinButton : public Gtk::Box
218{
219public:
220 MultiSpinButton(double lower, double upper, double step_inc,
221 double climb_rate, int digits,
222 std::vector<SPAttr> const &attrs,
223 std::vector<double> const &default_values,
224 std::vector<char *> const &tip_text)
225 : Gtk::Box(Gtk::Orientation::HORIZONTAL)
226 {
227 g_assert(attrs.size()==default_values.size());
228 g_assert(attrs.size()==tip_text.size());
229 set_spacing(4);
230 for(unsigned i = 0; i < attrs.size(); ++i) {
231 unsigned index = attrs.size() - 1 - i;
232 _spins.push_back(Gtk::make_managed<SpinButtonAttr>(lower, upper, step_inc, climb_rate, digits,
233 attrs[index], default_values[index], tip_text[index]));
234 UI::pack_end(*this, *_spins.back(), true, true);
235 _spins.back()->set_width_chars(3); // allow spin buttons to shrink to save space
236 }
237 }
238
239 std::vector<SpinButtonAttr *> const &get_spinbuttons() const
240 {
241 return _spins;
242 }
243
244private:
245 std::vector<SpinButtonAttr*> _spins;
246};
247
248// Contains two spinbuttons that describe a NumberOptNumber
249class DualSpinButton : public Gtk::Box, public AttrWidget
250{
251public:
252 DualSpinButton(char* def, double lower, double upper, double step_inc,
253 double climb_rate, int digits, const SPAttr a, char* tt1, char* tt2)
254 : AttrWidget(a, def), //TO-DO: receive default num-opt-num as parameter in the constructor
255 Gtk::Box(Gtk::Orientation::HORIZONTAL),
256 _s1(climb_rate, digits), _s2(climb_rate, digits)
257 {
258 if (tt1) {
259 _s1.set_tooltip_text(tt1);
260 }
261 if (tt2) {
262 _s2.set_tooltip_text(tt2);
263 }
264 _s1.set_range(lower, upper);
265 _s2.set_range(lower, upper);
266 _s1.set_increments(step_inc, 0);
267 _s2.set_increments(step_inc, 0);
268
269 _s1.signal_value_changed().connect(signal_attr_changed().make_slot());
270 _s2.signal_value_changed().connect(signal_attr_changed().make_slot());
271
272 set_spacing(4);
273 UI::pack_end(*this, _s2, true, true);
274 UI::pack_end(*this, _s1, true, true);
275 }
276
277 Inkscape::UI::Widget::SpinButton& get_spinbutton1()
278 {
279 return _s1;
280 }
281
282 Inkscape::UI::Widget::SpinButton& get_spinbutton2()
283 {
284 return _s2;
285 }
286
287 Glib::ustring get_as_attribute() const override
288 {
289 double v1 = _s1.get_value();
290 double v2 = _s2.get_value();
291
292 if(_s1.get_digits() == 0) {
293 v1 = (int)v1;
294 v2 = (int)v2;
295 }
296
297 return Glib::Ascii::dtostr(v1) + " " + Glib::Ascii::dtostr(v2);
298 }
299
300 void set_from_attribute(SPObject* o) override
301 {
302 const gchar* val = attribute_value(o);
304 if(val) {
305 n.set(val);
306 } else {
307 n.set(get_default()->as_charptr());
308 }
309 _s1.set_value(n.getNumber());
310 _s2.set_value(n.getOptNumber());
311
312 }
313private:
315};
316
317class ColorButton : public Widget::ColorPicker, public AttrWidget
318{
319public:
320 ColorButton(unsigned int def, const SPAttr a, char* tip_text)
321 : ColorPicker(_("Select color"), tip_text ? tip_text : "", Colors::Color(0x000000ff), false, false),
322 AttrWidget(a, def)
323 {
324 connectChanged([this](Colors::Color const &color){ signal_attr_changed().emit(); });
325 if (tip_text) {
326 set_tooltip_text(tip_text);
327 }
328 setColor(Colors::Color(0xffffffff));
329 }
330
331 Glib::ustring get_as_attribute() const override
332 {
333 return get_current_color().toString(false);
334 }
335
336 void set_from_attribute(SPObject* o) override
337 {
338 const gchar* val = attribute_value(o);
339 if (auto color = Colors::Color::parse(val)) {
340 setColor(*color);
341 } else {
342 setColor(Colors::Color(get_default()->as_uint()));
343 }
344 }
345};
346
347// Used for tableValue in feComponentTransfer
348class EntryAttr : public Gtk::Entry, public AttrWidget
349{
350public:
351 EntryAttr(const SPAttr a, char* tip_text)
352 : AttrWidget(a)
353 {
354 set_width_chars(3); // let it get narrow
355 signal_changed().connect(signal_attr_changed().make_slot());
356 if (tip_text) {
357 set_tooltip_text(tip_text);
358 }
359 }
360
361 // No validity checking is done
362 Glib::ustring get_as_attribute() const override
363 {
364 return get_text();
365 }
366
367 void set_from_attribute(SPObject* o) override
368 {
369 const gchar* val = attribute_value(o);
370 if(val) {
371 set_text( val );
372 } else {
373 set_text( "" );
374 }
375 }
376};
377
378/* Displays/Edits the matrix for feConvolveMatrix or feColorMatrix */
379class FilterEffectsDialog::MatrixAttr : public Gtk::Frame, public AttrWidget
380{
381public:
382 MatrixAttr(const SPAttr a, char* tip_text = nullptr)
383 : AttrWidget(a), _locked(false)
384 {
385 _model = Gtk::ListStore::create(_columns);
386 _tree.set_model(_model);
387 _tree.set_headers_visible(false);
388 set_child(_tree);
389 if (tip_text) {
390 _tree.set_tooltip_text(tip_text);
391 }
392 }
393
394 std::vector<double> get_values() const
395 {
396 std::vector<double> vec;
397 for(const auto & iter : _model->children()) {
398 for(unsigned c = 0; c < _tree.get_columns().size(); ++c)
399 vec.push_back(iter[_columns.cols[c]]);
400 }
401 return vec;
402 }
403
404 void set_values(const std::vector<double>& v)
405 {
406 unsigned i = 0;
407 for (auto &&iter : _model->children()) {
408 for(unsigned c = 0; c < _tree.get_columns().size(); ++c) {
409 if(i >= v.size())
410 return;
411 iter[_columns.cols[c]] = v[i];
412 ++i;
413 }
414 }
415 }
416
417 Glib::ustring get_as_attribute() const override
418 {
419 // use SVGOStringStream to output SVG-compatible doubles
421
422 for(const auto & iter : _model->children()) {
423 for(unsigned c = 0; c < _tree.get_columns().size(); ++c) {
424 os << iter[_columns.cols[c]] << " ";
425 }
426 }
427
428 return os.str();
429 }
430
431 void set_from_attribute(SPObject* o) override
432 {
433 if(o) {
434 if(is<SPFeConvolveMatrix>(o)) {
435 auto conv = cast<SPFeConvolveMatrix>(o);
436 int cols, rows;
437 cols = (int)conv->get_order().getNumber();
440 rows = conv->get_order().optNumIsSet() ? (int)conv->get_order().getOptNumber() : cols;
441 update(o, rows, cols);
442 }
443 else if(is<SPFeColorMatrix>(o))
444 update(o, 4, 5);
445 }
446 }
447private:
448 class MatrixColumns : public Gtk::TreeModel::ColumnRecord
449 {
450 public:
451 MatrixColumns()
452 {
453 cols.resize(max_convolution_kernel_size);
454 for(auto & col : cols)
455 add(col);
456 }
457 std::vector<Gtk::TreeModelColumn<double> > cols;
458 };
459
460 void update(SPObject* o, const int rows, const int cols)
461 {
462 if(_locked)
463 return;
464
465 _model->clear();
466
467 _tree.remove_all_columns();
468
469 std::vector<gdouble> const *values = nullptr;
470 if(is<SPFeColorMatrix>(o))
471 values = &cast<SPFeColorMatrix>(o)->get_values();
472 else if(is<SPFeConvolveMatrix>(o))
473 values = &cast<SPFeConvolveMatrix>(o)->get_kernel_matrix();
474 else
475 return;
476
477 if(o) {
478 for(int i = 0; i < cols; ++i) {
479 _tree.append_column_numeric_editable("", _columns.cols[i], "%.2f");
480 dynamic_cast<Gtk::CellRendererText &>(*_tree.get_column_cell_renderer(i))
481 .signal_edited().connect(
482 sigc::mem_fun(*this, &MatrixAttr::rebind));
483 }
484
485 int ndx = 0;
486 for(int r = 0; r < rows; ++r) {
487 Gtk::TreeRow row = *(_model->append());
488 // Default to identity matrix
489 for(int c = 0; c < cols; ++c, ++ndx)
490 row[_columns.cols[c]] = ndx < (int)values->size() ? (*values)[ndx] : (r == c ? 1 : 0);
491 }
492 }
493 }
494
495 void rebind(const Glib::ustring&, const Glib::ustring&)
496 {
497 _locked = true;
499 _locked = false;
500 }
501
502 bool _locked;
503 Gtk::TreeView _tree;
504 Glib::RefPtr<Gtk::ListStore> _model;
505 MatrixColumns _columns;
506};
507
508// Displays a matrix or a slider for feColorMatrix
509class FilterEffectsDialog::ColorMatrixValues : public Gtk::Frame, public AttrWidget
510{
511public:
512 ColorMatrixValues()
514 // TRANSLATORS: this dialog is accessible via menu Filters - Filter editor
515 _matrix(SPAttr::VALUES, _("This matrix determines a linear transform on color space. Each line affects one of the color components. Each column determines how much of each color component from the input is passed to the output. The last column does not depend on input colors, so can be used to adjust a constant component value.")),
516 _saturation("", 1, 0, 1, 0.1, 0.01, 2, SPAttr::VALUES),
517 _angle("", 0, 0, 360, 0.1, 0.01, 1, SPAttr::VALUES),
518 _label(C_("Label", "None"), Gtk::Align::START)
519 {
520 _matrix.signal_attr_changed().connect(signal_attr_changed().make_slot());
521 _saturation.signal_attr_changed().connect(signal_attr_changed().make_slot());
522 _angle.signal_attr_changed().connect(signal_attr_changed().make_slot());
523
524 _label.set_sensitive(false);
525
526 add_css_class("flat");
527 }
528
529 void set_from_attribute(SPObject* o) override
530 {
531 if(is<SPFeColorMatrix>(o)) {
532 auto col = cast<SPFeColorMatrix>(o);
533 unset_child();
534
535 switch(col->get_type()) {
537 set_child(_saturation);
538 _saturation.set_from_attribute(o);
539 break;
540
542 set_child(_angle);
543 _angle.set_from_attribute(o);
544 break;
545
547 set_child(_label);
548 break;
549
551 default:
552 set_child(_matrix);
553 _matrix.set_from_attribute(o);
554 break;
555 }
556 }
557 }
558
559 Glib::ustring get_as_attribute() const override
560 {
561 const Widget* w = get_child();
562 if(w == &_label)
563 return "";
564 if (auto attrw = dynamic_cast<const AttrWidget *>(w))
565 return attrw->get_as_attribute();
566 g_assert_not_reached();
567 return "";
568 }
569
570private:
571 MatrixAttr _matrix;
572 SpinScale _saturation;
573 SpinScale _angle;
574 Gtk::Label _label;
575};
576
577//Displays a chooser for feImage input
578//It may be a filename or the id for an SVG Element
579//described in xlink:href syntax
580class FileOrElementChooser : public Gtk::Box, public AttrWidget
581{
582public:
583 FileOrElementChooser(FilterEffectsDialog& d, const SPAttr a)
584 : AttrWidget(a)
585 , _dialog(d)
586 , Gtk::Box(Gtk::Orientation::HORIZONTAL)
587 {
588 set_spacing(3);
589 UI::pack_start(*this, _entry, true, true);
590 UI::pack_start(*this, _fromFile, false, false);
591 UI::pack_start(*this, _fromSVGElement, false, false);
592
593 _fromFile.set_image_from_icon_name("document-open");
594 _fromFile.set_tooltip_text(_("Choose image file"));
595 _fromFile.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_file));
596
597 _fromSVGElement.set_label(_("SVG Element"));
598 _fromSVGElement.set_tooltip_text(_("Use selected SVG element"));
599 _fromSVGElement.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_svg_element));
600
601 _entry.set_width_chars(1);
602 _entry.signal_changed().connect(signal_attr_changed().make_slot());
603
604 set_visible(true);
605 }
606
607 // Returns the element in xlink:href form.
608 Glib::ustring get_as_attribute() const override
609 {
610 return _entry.get_text();
611 }
612
613
614 void set_from_attribute(SPObject* o) override
615 {
616 const gchar* val = attribute_value(o);
617 if(val) {
618 _entry.set_text(val);
619 } else {
620 _entry.set_text("");
621 }
622 }
623
624private:
625 void select_svg_element() {
626 Inkscape::Selection* sel = _dialog.getDesktop()->getSelection();
627 if (sel->isEmpty()) return;
628 Inkscape::XML::Node* node = sel->xmlNodes().front();
629 if (!node || !node->matchAttributeName("id")) return;
630
631 std::ostringstream xlikhref;
632 xlikhref << "#" << node->attribute("id");
633 _entry.set_text(xlikhref.str());
634 }
635
636 void select_file(){
637
638 // Get the current directory for finding files.
639 std::string open_path;
640 get_start_directory(open_path, "/dialogs/open/path");
641
642 // Create a dialog.
643 auto window = _dialog.getDesktop()->getInkscapeWindow();
644 auto filters = create_open_filters();
645 auto file = choose_file_open(_("Select an image to be used as input."), window, filters, open_path);
646
647 if (!file) {
648 return; // Cancel
649 }
650
652 prefs->setString("/dialogs/open/path", file->get_path());
653
654 _entry.set_text(file->get_parse_name());
655 }
656
657 Gtk::Entry _entry;
658 Gtk::Button _fromFile;
659 Gtk::Button _fromSVGElement;
660 FilterEffectsDialog &_dialog;
661};
662
663class FilterEffectsDialog::Settings
664{
665public:
666 typedef sigc::slot<void (const AttrWidget*)> SetAttrSlot;
667
668 Settings(FilterEffectsDialog& d, Gtk::Box& b, SetAttrSlot slot, const int maxtypes)
669 : _dialog(d), _set_attr_slot(std::move(slot)), _current_type(-1), _max_types(maxtypes)
670 {
671 _groups.resize(_max_types);
672 _attrwidgets.resize(_max_types);
673 _size_group = Gtk::SizeGroup::create(Gtk::SizeGroup::Mode::HORIZONTAL);
674
675 for(int i = 0; i < _max_types; ++i) {
676 _groups[i] = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 3);
677 b.set_spacing(4);
679 }
680 //_current_type = 0; If set to 0 then update_and_show() fails to update properly.
681 }
682
683 void show_current_only() {
684 for (auto& group : _groups) {
685 group->set_visible(false);
686 }
687 auto t = get_current_type();
688 if (t >= 0) {
689 _groups[t]->set_visible(true);
690 }
691 }
692
693 // Show the active settings group and update all the AttrWidgets with new values
694 void show_and_update(const int t, SPObject* ob)
695 {
696 if (t != _current_type) {
697 type(t);
698
699 for (auto& group : _groups) {
700 group->set_visible(false);
701 }
702 }
703
704 if (t >= 0) {
705 _groups[t]->set_visible(true);
706 }
707
708 _dialog.set_attrs_locked(true);
709 for(auto & i : _attrwidgets[_current_type])
710 i->set_from_attribute(ob);
711 _dialog.set_attrs_locked(false);
712 }
713
714 int get_current_type() const
715 {
716 return _current_type;
717 }
718
719 void type(const int t)
720 {
721 _current_type = t;
722 }
723
724 void add_no_params()
725 {
726 auto const lbl = Gtk::make_managed<Gtk::Label>(_("This SVG filter effect does not require any parameters."));
727 lbl->set_wrap();
728 lbl->set_wrap_mode(Pango::WrapMode::WORD);
729 add_widget(lbl, "");
730 }
731
732 // LightSource
733 LightSourceControl* add_lightsource();
734
735 // Component Transfer Values
736 ComponentTransferValues* add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel);
737
738 // CheckButton
739 CheckButtonAttr* add_checkbutton(bool def, const SPAttr attr, const Glib::ustring& label,
740 const Glib::ustring& tv, const Glib::ustring& fv, char* tip_text = nullptr)
741 {
742 auto const cb = Gtk::make_managed<CheckButtonAttr>(def, label, tv, fv, attr, tip_text);
743 add_widget(cb, "");
744 add_attr_widget(cb);
745 return cb;
746 }
747
748 // ColorButton
749 ColorButton* add_color(unsigned int def, const SPAttr attr, const Glib::ustring& label, char* tip_text = nullptr)
750 {
751 auto const col = Gtk::make_managed<ColorButton>(def, attr, tip_text);
752 add_widget(col, label);
753 add_attr_widget(col);
754 return col;
755 }
756
757 // Matrix
758 MatrixAttr* add_matrix(const SPAttr attr, const Glib::ustring& label, char* tip_text)
759 {
760 auto const conv = Gtk::make_managed<MatrixAttr>(attr, tip_text);
761 add_widget(conv, label);
762 add_attr_widget(conv);
763 return conv;
764 }
765
766 // ColorMatrixValues
767 ColorMatrixValues* add_colormatrixvalues(const Glib::ustring& label)
768 {
769 auto const cmv = Gtk::make_managed<ColorMatrixValues>();
770 add_widget(cmv, label);
771 add_attr_widget(cmv);
772 return cmv;
773 }
774
775 // SpinScale
776 SpinScale* add_spinscale(double def, const SPAttr attr, const Glib::ustring& label,
777 const double lo, const double hi, const double step_inc, const double page_inc, const int digits, char* tip_text = nullptr)
778 {
779 Glib::ustring tip_text2;
780 if (tip_text)
781 tip_text2 = tip_text;
782 auto const spinslider = Gtk::make_managed<SpinScale>("", def, lo, hi, step_inc, page_inc, digits, attr, tip_text2);
783 add_widget(spinslider, label);
784 add_attr_widget(spinslider);
785 return spinslider;
786 }
787
788 // DualSpinScale
789 DualSpinScale* add_dualspinscale(const SPAttr attr, const Glib::ustring& label,
790 const double lo, const double hi, const double step_inc,
791 const double climb, const int digits,
792 const Glib::ustring tip_text1 = "",
793 const Glib::ustring tip_text2 = "")
794 {
795 auto const dss = Gtk::make_managed<DualSpinScale>("", "", lo, lo, hi, step_inc, climb, digits, attr, tip_text1, tip_text2);
796 add_widget(dss, label);
797 add_attr_widget(dss);
798 return dss;
799 }
800
801 // SpinButton
802 SpinButtonAttr* add_spinbutton(double defalt_value, const SPAttr attr, const Glib::ustring& label,
803 const double lo, const double hi, const double step_inc,
804 const double climb, const int digits, char* tip = nullptr)
805 {
806 auto const sb = Gtk::make_managed<SpinButtonAttr>(lo, hi, step_inc, climb, digits, attr, defalt_value, tip);
807 add_widget(sb, label);
808 add_attr_widget(sb);
809 return sb;
810 }
811
812 // DualSpinButton
813 DualSpinButton* add_dualspinbutton(char* defalt_value, const SPAttr attr, const Glib::ustring& label,
814 const double lo, const double hi, const double step_inc,
815 const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr)
816 {
817 auto const dsb = Gtk::make_managed<DualSpinButton>(defalt_value, lo, hi, step_inc, climb, digits, attr, tip1, tip2);
818 add_widget(dsb, label);
819 add_attr_widget(dsb);
820 return dsb;
821 }
822
823 // MultiSpinButton
824 MultiSpinButton* add_multispinbutton(double def1, double def2, const SPAttr attr1, const SPAttr attr2,
825 const Glib::ustring& label, const double lo, const double hi,
826 const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr)
827 {
828 auto const attrs = std::vector{attr1, attr2};
829 auto const default_values = std::vector{ def1, def2};
830 auto const tips = std::vector{ tip1, tip2};
831 auto const msb = Gtk::make_managed<MultiSpinButton>(lo, hi, step_inc, climb, digits, attrs, default_values, tips);
832 add_widget(msb, label);
833 for (auto const i : msb->get_spinbuttons())
834 add_attr_widget(i);
835 return msb;
836 }
837
838 MultiSpinButton* add_multispinbutton(double def1, double def2, double def3, const SPAttr attr1, const SPAttr attr2,
839 const SPAttr attr3, const Glib::ustring& label, const double lo,
840 const double hi, const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr, char* tip3 = nullptr)
841 {
842 auto const attrs = std::vector{attr1, attr2, attr3};
843 auto const default_values = std::vector{ def1, def2, def3};
844 auto const tips = std::vector{ tip1, tip2, tip3};
845 auto const msb = Gtk::make_managed<MultiSpinButton>(lo, hi, step_inc, climb, digits, attrs, default_values, tips);
846 add_widget(msb, label);
847 for (auto const i : msb->get_spinbuttons())
848 add_attr_widget(i);
849 return msb;
850 }
851
852 // FileOrElementChooser
853 FileOrElementChooser* add_fileorelement(const SPAttr attr, const Glib::ustring& label)
854 {
855 auto const foech = Gtk::make_managed<FileOrElementChooser>(_dialog, attr);
856 add_widget(foech, label);
857 add_attr_widget(foech);
858 return foech;
859 }
860
861 // ComboBoxEnum
862 template <typename T> ComboWithTooltip<T>* add_combo(T default_value, const SPAttr attr,
863 const Glib::ustring& label,
864 const Util::EnumDataConverter<T>& conv,
865 const Glib::ustring& tip_text = {})
866 {
867 auto const combo = Gtk::make_managed<ComboWithTooltip<T>>(default_value, conv, attr, tip_text);
868 add_widget(combo, label);
869 add_attr_widget(combo);
870 return combo;
871 }
872
873 // Entry
874 EntryAttr* add_entry(const SPAttr attr,
875 const Glib::ustring& label,
876 char* tip_text = nullptr)
877 {
878 auto const entry = Gtk::make_managed<EntryAttr>(attr, tip_text);
879 add_widget(entry, label);
880 add_attr_widget(entry);
881 return entry;
882 }
883
884 Glib::RefPtr<Gtk::SizeGroup> _size_group;
885
886private:
887 void add_attr_widget(AttrWidget* a)
888 {
889 _attrwidgets[_current_type].push_back(a);
890 a->signal_attr_changed().connect(sigc::bind(_set_attr_slot, a));
891 // add_widget() takes a managed widget, so dtor will delete & disconnect
892 }
893
894 /* Adds a new settings widget using the specified label. The label will be formatted with a colon
895 and all widgets within the setting group are aligned automatically. */
896 void add_widget(Gtk::Widget* w, const Glib::ustring& label)
897 {
898 g_assert(w->is_managed_());
899
900 auto const hb = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
901 hb->set_spacing(6);
902
903 if (label != "") {
904 auto const lbl = Gtk::make_managed<Gtk::Label>(label);
905 lbl->set_xalign(0.0);
907 _size_group->add_widget(*lbl);
908 }
909
911 UI::pack_start(*_groups[_current_type], *hb, UI::PackOptions::expand_widget);
912 }
913
914 std::vector<Gtk::Box*> _groups;
915 FilterEffectsDialog& _dialog;
916 SetAttrSlot _set_attr_slot;
917 std::vector<std::vector< AttrWidget*> > _attrwidgets;
918 int _current_type, _max_types;
919};
920
921// Displays sliders and/or tables for feComponentTransfer
922class FilterEffectsDialog::ComponentTransferValues : public Gtk::Frame, public AttrWidget
923{
924public:
925 ComponentTransferValues(FilterEffectsDialog& d, SPFeFuncNode::Channel channel)
927 _dialog(d),
928 _settings(d, _box, sigc::mem_fun(*this, &ComponentTransferValues::set_func_attr), COMPONENTTRANSFER_TYPE_ERROR),
930 _channel(channel),
931 _funcNode(nullptr),
932 _box(Gtk::Orientation::VERTICAL)
933 {
934 add_css_class("flat");
935
936 set_child(_box);
937 _box.prepend(_type);
938
939 _type.signal_changed().connect(sigc::mem_fun(*this, &ComponentTransferValues::on_type_changed));
940
941 _settings.type(COMPONENTTRANSFER_TYPE_LINEAR);
942 _settings.add_spinscale(1, SPAttr::SLOPE, _("Slope"), -10, 10, 0.1, 0.01, 2);
943 _settings.add_spinscale(0, SPAttr::INTERCEPT, _("Intercept"), -10, 10, 0.1, 0.01, 2);
944
945 _settings.type(COMPONENTTRANSFER_TYPE_GAMMA);
946 _settings.add_spinscale(1, SPAttr::AMPLITUDE, _("Amplitude"), 0, 10, 0.1, 0.01, 2);
947 _settings.add_spinscale(1, SPAttr::EXPONENT, _("Exponent"), 0, 10, 0.1, 0.01, 2);
948 _settings.add_spinscale(0, SPAttr::OFFSET, _("Offset"), -10, 10, 0.1, 0.01, 2);
949
950 _settings.type(COMPONENTTRANSFER_TYPE_TABLE);
951 _settings.add_entry(SPAttr::TABLEVALUES, _("Values"), _("List of stops with interpolated output"));
952
953 _settings.type(COMPONENTTRANSFER_TYPE_DISCRETE);
954 _settings.add_entry(SPAttr::TABLEVALUES, _("Values"), _("List of discrete values for a step function"));
955
956 //_settings.type(COMPONENTTRANSFER_TYPE_IDENTITY);
957 _settings.type(-1); // Force update_and_show() to show/hide windows correctly
958 }
959
960 // FuncNode can be in any order so we must search to find correct one.
961 SPFeFuncNode* find_node(SPFeComponentTransfer* ct)
962 {
963 SPFeFuncNode* funcNode = nullptr;
964 bool found = false;
965 for(auto& node: ct->children) {
966 funcNode = cast<SPFeFuncNode>(&node);
967 if( funcNode->channel == _channel ) {
968 found = true;
969 break;
970 }
971 }
972 if( !found )
973 funcNode = nullptr;
974
975 return funcNode;
976 }
977
978 void set_func_attr(const AttrWidget* input)
979 {
980 _dialog.set_attr( _funcNode, input->get_attribute(), input->get_as_attribute().c_str());
981 }
982
983 // Set new type and update widget visibility
984 void set_from_attribute(SPObject* o) override
985 {
986 // See componenttransfer.cpp
987 if(is<SPFeComponentTransfer>(o)) {
988 auto ct = cast<SPFeComponentTransfer>(o);
989
990 _funcNode = find_node(ct);
991 if( _funcNode ) {
992 _type.set_from_attribute( _funcNode );
993 } else {
994 // Create <funcNode>
995 SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
996 if(prim) {
997 Inkscape::XML::Document *xml_doc = prim->document->getReprDoc();
998 Inkscape::XML::Node *repr = nullptr;
999 switch(_channel) {
1000 case SPFeFuncNode::R:
1001 repr = xml_doc->createElement("svg:feFuncR");
1002 break;
1003 case SPFeFuncNode::G:
1004 repr = xml_doc->createElement("svg:feFuncG");
1005 break;
1006 case SPFeFuncNode::B:
1007 repr = xml_doc->createElement("svg:feFuncB");
1008 break;
1009 case SPFeFuncNode::A:
1010 repr = xml_doc->createElement("svg:feFuncA");
1011 break;
1012 }
1013
1014 //XML Tree being used directly here while it shouldn't be.
1015 prim->getRepr()->appendChild(repr);
1017
1018 // Now we should find it!
1019 _funcNode = find_node(ct);
1020 if( _funcNode ) {
1021 _funcNode->setAttribute( "type", "identity" );
1022 } else {
1023 //std::cerr << "ERROR ERROR: feFuncX not found!" << std::endl;
1024 }
1025 }
1026 }
1027
1028 update();
1029 }
1030 }
1031
1032private:
1033 void on_type_changed()
1034 {
1035 SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
1036 if(prim) {
1037 _funcNode->setAttributeOrRemoveIfEmpty("type", _type.get_as_attribute());
1038
1039 SPFilter* filter = _dialog._filter_modifier.get_selected_filter();
1040 g_assert(filter);
1041 filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
1042
1043 DocumentUndo::done(prim->document, _("New transfer function type"), INKSCAPE_ICON("dialog-filters"));
1044 update();
1045 }
1046 }
1047
1048 void update()
1049 {
1050 SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
1051 if(prim && _funcNode) {
1052 auto id = _type.get_selected_id();
1053 if (id.has_value()) {
1054 _settings.show_and_update(*id, _funcNode);
1055 }
1056 }
1057 }
1058
1059public:
1060 Glib::ustring get_as_attribute() const override
1061 {
1062 return "";
1063 }
1064
1065 FilterEffectsDialog& _dialog;
1066 Gtk::Box _box;
1067 Settings _settings;
1068 ComboBoxEnum<FilterComponentTransferType> _type;
1069 SPFeFuncNode::Channel _channel; // RGBA
1070 SPFeFuncNode* _funcNode;
1071};
1072
1073// Settings for the three light source objects
1074class FilterEffectsDialog::LightSourceControl
1075 : public AttrWidget
1076 , public Gtk::Box
1077{
1078public:
1079 LightSourceControl(FilterEffectsDialog& d)
1081 Gtk::Box(Gtk::Orientation::VERTICAL),
1082 _dialog(d),
1083 _settings(d, *this, sigc::mem_fun(_dialog, &FilterEffectsDialog::set_child_attr_direct), LIGHT_ENDSOURCE),
1084 _light_label(_("Light Source:")),
1085 _light_source(LightSourceConverter),
1086 _locked(false),
1087 _light_box(Gtk::Orientation::HORIZONTAL, 6)
1088 {
1089 _light_label.set_xalign(0.0);
1090 _settings._size_group->add_widget(_light_label);
1091 UI::pack_start(_light_box, _light_label, UI::PackOptions::shrink);
1092 UI::pack_start(_light_box, _light_source, UI::PackOptions::expand_widget);
1093
1094 prepend(_light_box);
1095 _light_source.signal_changed().connect(sigc::mem_fun(*this, &LightSourceControl::on_source_changed));
1096
1097 // FIXME: these range values are complete crap
1098
1099 _settings.type(LIGHT_DISTANT);
1100 _settings.add_spinscale(0, SPAttr::AZIMUTH, _("Azimuth:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the XY plane, in degrees"));
1101 _settings.add_spinscale(0, SPAttr::ELEVATION, _("Elevation:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the YZ plane, in degrees"));
1102
1103 _settings.type(LIGHT_POINT);
1104 _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SPAttr::X, SPAttr::Y, SPAttr::Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate"));
1105
1106 _settings.type(LIGHT_SPOT);
1107 _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SPAttr::X, SPAttr::Y, SPAttr::Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate"));
1108 _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0,
1110 _("Points at:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate"));
1111 _settings.add_spinscale(1, SPAttr::SPECULAREXPONENT, _("Specular Exponent:"), 0.1, 100, 0.1, 1, 1, _("Exponent value controlling the focus for the light source"));
1112 //TODO: here I have used 100 degrees as default value. But spec says that if not specified, no limiting cone is applied. So, there should be a way for the user to set a "no limiting cone" option.
1113 _settings.add_spinscale(100, SPAttr::LIMITINGCONEANGLE, _("Cone Angle:"), 0, 180, 1, 5, 0, _("This is the angle between the spot light axis (i.e. the axis between the light source and the point to which it is pointing at) and the spot light cone. No light is projected outside this cone."));
1114
1115 _settings.type(-1); // Force update_and_show() to show/hide windows correctly
1116 }
1117
1118private:
1119 Glib::ustring get_as_attribute() const override
1120 {
1121 return "";
1122 }
1123
1124 void set_from_attribute(SPObject* o) override
1125 {
1126 if(_locked)
1127 return;
1128
1129 _locked = true;
1130
1131 SPObject* child = o->firstChild();
1132
1133 if(is<SPFeDistantLight>(child))
1134 _light_source.set_active(0);
1135 else if(is<SPFePointLight>(child))
1136 _light_source.set_active(1);
1137 else if(is<SPFeSpotLight>(child))
1138 _light_source.set_active(2);
1139 else
1140 _light_source.set_active(-1);
1141
1142 update();
1143
1144 _locked = false;
1145 }
1146
1147 void on_source_changed()
1148 {
1149 if(_locked)
1150 return;
1151
1152 SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
1153 if(prim) {
1154 _locked = true;
1155
1156 SPObject* child = prim->firstChild();
1157 const int ls = _light_source.get_selected();
1158 // Check if the light source type has changed
1159 if(!(ls == -1 && !child) &&
1160 !(ls == 0 && is<SPFeDistantLight>(child)) &&
1161 !(ls == 1 && is<SPFePointLight>(child)) &&
1162 !(ls == 2 && is<SPFeSpotLight>(child))) {
1163 if(child)
1164 //XML Tree being used directly here while it shouldn't be.
1165 sp_repr_unparent(child->getRepr());
1166
1167 if(ls != -1) {
1168 Inkscape::XML::Document *xml_doc = prim->document->getReprDoc();
1169 Inkscape::XML::Node *repr = xml_doc->createElement(_light_source.get_as_attribute().c_str());
1170 //XML Tree being used directly here while it shouldn't be.
1171 prim->getRepr()->appendChild(repr);
1173 }
1174
1175 DocumentUndo::done(prim->document, _("New light source"), INKSCAPE_ICON("dialog-filters"));
1176 update();
1177 }
1178
1179 _locked = false;
1180 }
1181 }
1182
1183 void update()
1184 {
1185 set_visible(true);
1186
1187 SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
1188 if (prim && prim->firstChild()) {
1189 auto id = _light_source.get_selected_id();
1190 if (id.has_value()) {
1191 _settings.show_and_update(*id, prim->firstChild());
1192 }
1193 }
1194 else {
1195 _settings.show_current_only();
1196 }
1197 }
1198
1199 FilterEffectsDialog& _dialog;
1200 Settings _settings;
1201 Gtk::Box _light_box;
1202 Gtk::Label _light_label;
1203 ComboBoxEnum<LightSource> _light_source;
1204 bool _locked;
1205};
1206
1207FilterEffectsDialog::ComponentTransferValues* FilterEffectsDialog::Settings::add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel)
1208{
1209 auto const ct = Gtk::make_managed<ComponentTransferValues>(_dialog, channel);
1210 add_widget(ct, label);
1211 add_attr_widget(ct);
1212 ct->set_margin_top(4);
1213 ct->set_margin_bottom(4);
1214 return ct;
1215}
1216
1217FilterEffectsDialog::LightSourceControl* FilterEffectsDialog::Settings::add_lightsource()
1218{
1219 auto const ls = Gtk::make_managed<LightSourceControl>(_dialog);
1220 add_attr_widget(ls);
1221 add_widget(ls, "");
1222 return ls;
1223}
1224
1225static std::unique_ptr<UI::Widget::PopoverMenu> create_popup_menu(Gtk::Widget &parent,
1226 sigc::slot<void ()> dup,
1227 sigc::slot<void ()> rem)
1228{
1229 auto menu = std::make_unique<UI::Widget::PopoverMenu>(Gtk::PositionType::RIGHT);
1230
1231 auto mi = Gtk::make_managed<UI::Widget::PopoverMenuItem>(_("_Duplicate"), true);
1232 mi->signal_activate().connect(std::move(dup));
1233 menu->append(*mi);
1234
1235 mi = Gtk::make_managed<UI::Widget::PopoverMenuItem>(_("_Remove"), true);
1236 mi->signal_activate().connect(std::move(rem));
1237 menu->append(*mi);
1238
1239 return menu;
1240}
1241
1242/*** FilterModifier ***/
1244 : Gtk::Box(Gtk::Orientation::VERTICAL),
1245 _builder(std::move(builder)),
1246 _list(get_widget<Gtk::TreeView>(_builder, "filter-list")),
1247 _dialog(d),
1248 _add(get_widget<Gtk::Button>(_builder, "btn-new")),
1249 _dup(get_widget<Gtk::Button>(_builder, "btn-dup")),
1250 _del(get_widget<Gtk::Button>(_builder, "btn-del")),
1251 _select(get_widget<Gtk::Button>(_builder, "btn-select")),
1252 _menu(create_menu()),
1253 _observer(std::make_unique<Inkscape::XML::SignalObserver>())
1254{
1255 _filters_model = Gtk::ListStore::create(_columns);
1256 _list.set_model(_filters_model);
1257 _cell_toggle.set_radio();
1258 _cell_toggle.set_active(true);
1259 const int selcol = _list.append_column("", _cell_toggle);
1260 Gtk::TreeViewColumn* col = _list.get_column(selcol - 1);
1261 if(col)
1262 col->add_attribute(_cell_toggle.property_active(), _columns.sel);
1263 _list.append_column_editable(_("_Filter"), _columns.label);
1264 dynamic_cast<Gtk::CellRendererText &>(*_list.get_column(1)->get_first_cell()).
1265 signal_edited().connect(sigc::mem_fun(*this, &FilterEffectsDialog::FilterModifier::on_name_edited));
1266
1267 _list.append_column(_("Used"), _columns.count);
1268 _list.get_column(2)->set_sizing(Gtk::TreeViewColumn::Sizing::AUTOSIZE);
1269 _list.get_column(2)->set_expand(false);
1270 _list.get_column(2)->set_reorderable(true);
1271
1272 _list.get_column(1)->set_resizable(true);
1273 _list.get_column(1)->set_sizing(Gtk::TreeViewColumn::Sizing::FIXED);
1274 _list.get_column(1)->set_expand(true);
1275
1276 _list.set_reorderable(false);
1277 _list.enable_model_drag_dest(Gdk::DragAction::MOVE);
1278
1279#if 0 // on_filter_move() was commented-out in GTK3, so itʼs removed for GTK4. FIXME if you can...!
1280 _list.signal_drag_drop().connect( sigc::mem_fun(*this, &FilterModifier::on_filter_move), false );
1281#endif
1282
1283 _add.signal_clicked().connect([this]{ add_filter(); });
1284 _del.signal_clicked().connect([this]{ remove_filter(); });
1285 _dup.signal_clicked().connect([this]{ duplicate_filter(); });
1286 _select.signal_clicked().connect([this]{ select_filter_elements(); });
1287
1288 _cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &FilterModifier::on_selection_toggled));
1289
1290 auto const click = Gtk::GestureClick::create();
1291 click->set_button(3); // right
1292 click->signal_released().connect(Controller::use_state(sigc::mem_fun(*this, &FilterModifier::filter_list_click_released), *click));
1293 _list.add_controller(click);
1294
1295 _list.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FilterModifier::on_filter_selection_changed));
1296 _observer->signal_changed().connect(signal_filter_changed().make_slot());
1297}
1298
1299// Update each filter's sel property based on the current object selection;
1300// If the filter is not used by any selected object, sel = 0,
1301// otherwise sel is set to the total number of filters in use by selected objects
1302// If only one filter is in use, it is selected
1304{
1305 if (!sel) {
1306 return;
1307 }
1308
1309 std::set<SPFilter*> used;
1310
1311 for (auto obj : sel->items()) {
1312 SPStyle *style = obj->style;
1313 if (!style || !obj) {
1314 continue;
1315 }
1316
1317 if (style->filter.set && style->getFilter()) {
1318 //TODO: why is this needed?
1319 obj->bbox_valid = FALSE;
1320 used.insert(style->getFilter());
1321 }
1322 }
1323
1324 const int size = used.size();
1325
1326 for (auto &&item : _filters_model->children()) {
1327 if (used.count(item[_columns.filter])) {
1328 // If only one filter is in use by the selection, select it
1329 if (size == 1) {
1330 _list.get_selection()->select(item.get_iter());
1331 }
1332 item[_columns.sel] = size;
1333 } else {
1334 item[_columns.sel] = 0;
1335 }
1336 }
1337 update_counts();
1338 _signal_filters_updated.emit();
1339}
1340
1341std::unique_ptr<UI::Widget::PopoverMenu> FilterEffectsDialog::FilterModifier::create_menu()
1342{
1343 auto menu = std::make_unique<UI::Widget::PopoverMenu>(Gtk::PositionType::BOTTOM);
1344 auto append = [&](Glib::ustring const &text, auto const mem_fun)
1345 {
1346 auto &item = *Gtk::make_managed<UI::Widget::PopoverMenuItem>(text, true);
1347 item.signal_activate().connect(sigc::mem_fun(*this, mem_fun));
1348 menu->append(item);
1349 };
1350 append(_("_Duplicate" ), &FilterModifier::duplicate_filter );
1351 append(_("_Remove" ), &FilterModifier::remove_filter );
1352 append(_("R_ename" ), &FilterModifier::rename_filter );
1353 append(_("Select Filter Elements"), &FilterModifier::select_filter_elements);
1354 return menu;
1355}
1356
1358{
1359 _observer->set(get_selected_filter());
1360 signal_filter_changed()();
1361}
1362
1363void FilterEffectsDialog::FilterModifier::on_name_edited(const Glib::ustring& path, const Glib::ustring& text)
1364{
1365 if (auto iter = _filters_model->get_iter(path)) {
1366 SPFilter* filter = (*iter)[_columns.filter];
1367 filter->setLabel(text.c_str());
1368 DocumentUndo::done(filter->document, _("Rename filter"), INKSCAPE_ICON("dialog-filters"));
1369 if (iter) {
1370 (*iter)[_columns.label] = text;
1371 }
1372 }
1373}
1374
1375#if 0 // on_filter_move() was commented-out in GTK3, so itʼs removed for GTK4. FIXME if you can...!
1376bool FilterEffectsDialog::FilterModifier::on_filter_move(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int /*x*/, int /*y*/, guint /*time*/) {
1377
1378//const Gtk::TreeModel::Path& /*path*/) {
1379/* The code below is bugged. Use of "object->getRepr()->setPosition(0)" is dangerous!
1380 Writing back the reordered list to XML (reordering XML nodes) should be implemented differently.
1381 Note that the dialog does also not update its list of filters when the order is manually changed
1382 using the XML dialog
1383 for (auto i = _model->children().begin(); i != _model->children().end(); ++i) {
1384 SPObject* object = (*i)[_columns.filter];
1385 if(object && object->getRepr()) ;
1386 object->getRepr()->setPosition(0);
1387 }
1388*/
1389 return false;
1390}
1391#endif
1392
1394{
1395 Gtk::TreeModel::iterator iter = _filters_model->get_iter(path);
1396 selection_toggled(iter, false);
1397}
1398
1399void FilterEffectsDialog::FilterModifier::selection_toggled(Gtk::TreeModel::iterator iter, bool toggle) {
1400 if (!iter) return;
1401
1402 SPDesktop *desktop = _dialog.getDesktop();
1403 SPDocument *doc = desktop->getDocument();
1405 SPFilter* filter = (*iter)[_columns.filter];
1406
1407 /* If this filter is the only one used in the selection, unset it */
1408 if ((*iter)[_columns.sel] == 1 && toggle) {
1409 filter = nullptr;
1410 }
1411
1412 for (auto item : sel->items()) {
1413 SPStyle *style = item->style;
1414 g_assert(style != nullptr);
1415
1416 if (filter && filter->valid_for(item)) {
1417 sp_style_set_property_url(item, "filter", filter, false);
1418 } else {
1419 ::remove_filter(item, false);
1420 }
1421
1422 item->requestDisplayUpdate((SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG ));
1423 }
1424
1425 update_selection(sel);
1426 DocumentUndo::done(doc, _("Apply filter"), INKSCAPE_ICON("dialog-filters"));
1427}
1428
1430{
1431 for (auto&& item : _filters_model->children()) {
1432 SPFilter* f = item[_columns.filter];
1433 item[_columns.count] = f->getRefCount();
1434 }
1435}
1436
1437static Glib::ustring get_filter_name(SPFilter* filter) {
1438 if (!filter) return Glib::ustring();
1439
1440 if (auto label = filter->label()) {
1441 return label;
1442 }
1443 else if (auto id = filter->getId()) {
1444 return id;
1445 }
1446 else {
1447 return _("filter");
1448 }
1449}
1450
1451/* Add all filters in the document to the combobox.
1452 Keeps the same selection if possible, otherwise selects the first element */
1454{
1455 auto document = _dialog.getDocument();
1456 if (!document) return; // no document at shut down
1457
1458 std::vector<SPObject *> filters = document->getResourceList("filter");
1459
1460 _filters_model->clear();
1461 SPFilter* first = nullptr;
1462
1463 for (auto filter : filters) {
1464 Gtk::TreeModel::Row row = *_filters_model->append();
1465 auto f = cast<SPFilter>(filter);
1466 row[_columns.filter] = f;
1467 row[_columns.label] = get_filter_name(f);
1468 if (!first) {
1469 first = f;
1470 }
1471 }
1472
1473 update_selection(_dialog.getSelection());
1474 if (first) {
1475 select_filter(first);
1476 }
1477 _dialog.update_filter_general_settings_view();
1478 _dialog.update_settings_view();
1479}
1480
1482 if (auto&& sel = _list.get_selection()) {
1483 if (Gtk::TreeModel::iterator it = sel->get_selected()) {
1484 return (*it)[_columns.sel] > 0;
1485 }
1486 }
1487
1488 return false;
1489}
1490
1492 return !_filters_model->children().empty();
1493}
1494
1496 if (auto&& sel = _list.get_selection()) {
1497 selection_toggled(sel->get_selected(), true);
1498 }
1499}
1500
1502{
1503 if(_list.get_selection()) {
1504 Gtk::TreeModel::iterator i = _list.get_selection()->get_selected();
1505 if(i)
1506 return (*i)[_columns.filter];
1507 }
1508
1509 return nullptr;
1510}
1511
1513{
1514 if (!filter) return;
1515
1516 for (auto &&item : _filters_model->children()) {
1517 if (item[_columns.filter] == filter) {
1518 _list.get_selection()->select(item.get_iter());
1519 break;
1520 }
1521 }
1522}
1523
1524Gtk::EventSequenceState
1526 int /*n_press*/,
1527 double const x, double const y)
1528{
1529 const bool sensitive = get_selected_filter() != nullptr;
1530 auto const &items = _menu->get_items();
1531 items.at(0)->set_sensitive(sensitive);
1532 items.at(1)->set_sensitive(sensitive);
1533 items.at(3)->set_sensitive(sensitive);
1534 _dialog._popoverbin.setPopover(_menu.get());
1535 _menu->popup_at(_list, x, y);
1536 return Gtk::EventSequenceState::CLAIMED;
1537}
1538
1540{
1541 SPDocument* doc = _dialog.getDocument();
1542 SPFilter* filter = new_filter(doc);
1543
1544 const int count = _filters_model->children().size();
1545 std::ostringstream os;
1546 os << _("filter") << count;
1547 filter->setLabel(os.str().c_str());
1548
1549 update_filters();
1550
1551 select_filter(filter);
1552
1553 DocumentUndo::done(doc, _("Add filter"), INKSCAPE_ICON("dialog-filters"));
1554}
1555
1557{
1558 SPFilter *filter = get_selected_filter();
1559
1560 if(filter) {
1561 auto desktop = _dialog.getDesktop();
1562 SPDocument* doc = filter->document;
1563
1564 // Delete all references to this filter
1565 auto all = get_all_items(desktop->layerManager().currentRoot(), desktop, false, false, true);
1566 for (auto item : all) {
1567 if (!item) {
1568 continue;
1569 }
1570 if (!item->style) {
1571 continue;
1572 }
1573
1574 const SPIFilter *ifilter = &(item->style->filter);
1575 if (ifilter && ifilter->href) {
1576 const SPObject *obj = ifilter->href->getObject();
1577 if (obj && obj == (SPObject *)filter) {
1578 ::remove_filter(item, false);
1579 }
1580 }
1581 }
1582
1583 //XML Tree being used directly here while it shouldn't be.
1584 sp_repr_unparent(filter->getRepr());
1585
1586 DocumentUndo::done(doc, _("Remove filter"), INKSCAPE_ICON("dialog-filters"));
1587
1588 update_filters();
1589
1590 // select first filter to avoid empty dialog after filter deletion
1591 auto &&filters = _filters_model->children();
1592 if (!filters.empty()) {
1593 _list.get_selection()->select(filters[0].get_iter());
1594 }
1595 }
1596}
1597
1599{
1600 SPFilter* filter = get_selected_filter();
1601
1602 if (filter) {
1603 Inkscape::XML::Node *repr = filter->getRepr();
1605 repr = repr->duplicate(repr->document());
1606 parent->appendChild(repr);
1607
1608 DocumentUndo::done(filter->document, _("Duplicate filter"), INKSCAPE_ICON("dialog-filters"));
1609
1610 update_filters();
1611 }
1612}
1613
1615{
1616 _list.set_cursor(_filters_model->get_path(_list.get_selection()->get_selected()), *_list.get_column(1), true);
1617}
1618
1620{
1621 SPFilter *filter = get_selected_filter();
1622 auto desktop = _dialog.getDesktop();
1623
1624 if(!filter)
1625 return;
1626
1627 std::vector<SPItem*> items;
1628 auto all = get_all_items(desktop->layerManager().currentRoot(), desktop, false, false, true);
1629 for(SPItem *item: all) {
1630 if (!item->style) {
1631 continue;
1632 }
1633
1634 SPIFilter const &ifilter = item->style->filter;
1635 if (ifilter.href) {
1636 const SPObject *obj = ifilter.href->getObject();
1637 if (obj && obj == (SPObject *)filter) {
1638 items.push_back(item);
1639 }
1640 }
1641 }
1643}
1644
1646 : Glib::ObjectBase(typeid(CellRendererConnection))
1647 , _primitive(*this, "primitive", nullptr)
1648{}
1649
1651{
1652 return _primitive.get_proxy();
1653}
1654
1656 int& minimum_width,
1657 int& natural_width) const
1658{
1659 auto& primlist = dynamic_cast<PrimitiveList&>(widget);
1660 int count = primlist.get_inputs_count();
1661 minimum_width = natural_width = size_w * primlist.primitive_count() + primlist.get_input_type_width() * count;
1662}
1663
1665 int /* height */,
1666 int& minimum_width,
1667 int& natural_width) const
1668{
1669 get_preferred_width(widget, minimum_width, natural_width);
1670}
1671
1673 int& minimum_height,
1674 int& natural_height) const
1675{
1676 // Scale the height depending on the number of inputs, unless it's
1677 // the first primitive, in which case there are no connections.
1678 auto prim = reinterpret_cast<SPFilterPrimitive*>(_primitive.get_value());
1679 minimum_height = natural_height = size_h * input_count(prim);
1680}
1681
1683 int /* width */,
1684 int& minimum_height,
1685 int& natural_height) const
1686{
1687 get_preferred_height(widget, minimum_height, natural_height);
1688}
1689
1690/*** PrimitiveList ***/
1692 : Glib::ObjectBase{"FilterEffectsDialogPrimitiveList"}
1694 , Gtk::TreeView{}
1695 , _dialog(d)
1696 , _in_drag(0)
1697 , _observer(std::make_unique<Inkscape::XML::SignalObserver>())
1698{
1700
1701 auto const click = Gtk::GestureClick::create();
1702 click->set_button(0); // any
1703 click->set_propagation_phase(Gtk::PropagationPhase::TARGET);
1704 click->signal_pressed().connect(Controller::use_state(sigc::mem_fun(*this, &PrimitiveList::on_click_pressed), *click));
1705 click->signal_released().connect(Controller::use_state(sigc::mem_fun(*this, &PrimitiveList::on_click_released), *click));
1706 add_controller(click);
1707
1708 auto const motion = Gtk::EventControllerMotion::create();
1709 motion->set_propagation_phase(Gtk::PropagationPhase::TARGET);
1710 motion->signal_motion().connect(sigc::mem_fun(*this, &PrimitiveList::on_motion_motion));
1711 add_controller(motion);
1712
1713 _model = Gtk::ListStore::create(_columns);
1714
1715 set_reorderable(true);
1716
1717 auto const drag = Gtk::DragSource::create();
1718 drag->signal_drag_end().connect(sigc::mem_fun(*this, &PrimitiveList::on_drag_end));
1719 add_controller(drag);
1720
1721 set_model(_model);
1722 append_column(_("_Effect"), _columns.type);
1723 get_column(0)->set_resizable(true);
1724 set_headers_visible(false);
1725
1726 _observer->signal_changed().connect(signal_primitive_changed().make_slot());
1727 get_selection()->signal_changed().connect(sigc::mem_fun(*this, &PrimitiveList::on_primitive_selection_changed));
1728 signal_primitive_changed().connect(sigc::mem_fun(*this, &PrimitiveList::queue_draw));
1729
1730 init_text();
1731
1732 int cols_count = append_column(_("Connections"), _connection_cell);
1733 Gtk::TreeViewColumn* col = get_column(cols_count - 1);
1734 if(col)
1736}
1737
1739{
1740 bg_color = get_color_with_class(*this, "theme_bg_color");
1741}
1742
1743// Sets up a vertical Pango context/layout, and returns the largest
1744// width needed to render the FilterPrimitiveInput labels.
1746{
1747 // Set up a vertical context+layout
1748 Glib::RefPtr<Pango::Context> context = create_pango_context();
1749 const Pango::Matrix matrix = {0, -1, 1, 0, 0, 0};
1750 context->set_matrix(matrix);
1751 _vertical_layout = Pango::Layout::create(context);
1752
1753 // Store the maximum height and width of the an input type label
1754 // for later use in drawing and measuring.
1755 _input_type_height = _input_type_width = 0;
1756 for(unsigned int i = 0; i < FPInputConverter._length; ++i) {
1757 _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
1758 int fontw, fonth;
1759 _vertical_layout->get_pixel_size(fontw, fonth);
1760 if(fonth > _input_type_width)
1761 _input_type_width = fonth;
1762 if (fontw > _input_type_height)
1763 _input_type_height = fontw;
1764 }
1765}
1766
1768{
1769 return _signal_primitive_changed;
1770}
1771
1773{
1774 _observer->set(get_selected());
1775 signal_primitive_changed()();
1776}
1777
1778/* Add all filter primitives in the current to the list.
1779 Keeps the same selection if possible, otherwise selects the first element */
1781{
1782 SPFilter* f = _dialog._filter_modifier.get_selected_filter();
1783 const SPFilterPrimitive* active_prim = get_selected();
1784 _model->clear();
1785
1786 if(f) {
1787 bool active_found = false;
1788 _dialog._primitive_box->set_sensitive(true);
1789 _dialog.update_filter_general_settings_view();
1790 for(auto& prim_obj: f->children) {
1791 auto prim = cast<SPFilterPrimitive>(&prim_obj);
1792 if(!prim) {
1793 break;
1794 }
1795 Gtk::TreeModel::Row row = *_model->append();
1796 row[_columns.primitive] = prim;
1797
1798 //XML Tree being used directly here while it shouldn't be.
1799 row[_columns.type_id] = FPConverter.get_id_from_key(prim->getRepr()->name());
1800 row[_columns.type] = _(FPConverter.get_label(row[_columns.type_id]).c_str());
1801
1802 if (prim->getId()) {
1803 row[_columns.id] = Glib::ustring(prim->getId());
1804 }
1805
1806 if(prim == active_prim) {
1807 get_selection()->select(row.get_iter());
1808 active_found = true;
1809 }
1810 }
1811
1812 if(!active_found && _model->children().begin())
1813 get_selection()->select(_model->children().begin());
1814
1815 columns_autosize();
1816
1817 int width, height;
1818 get_size_request(width, height);
1819 if (height == -1) {
1820 // Need to account for the height of the input type text (rotated text) as well as the
1821 // column headers. Input type text height determined in init_text() by measuring longest
1822 // string. Column header height determined by mapping y coordinate of visible
1823 // rectangle to widget coordinates.
1824 Gdk::Rectangle vis;
1825 int vis_x, vis_y;
1826 get_visible_rect(vis);
1827 convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y);
1828 set_size_request(width, _input_type_height + 2 + vis_y);
1829 }
1830 } else {
1831 _dialog._primitive_box->set_sensitive(false);
1832 set_size_request(-1, -1);
1833 }
1834}
1835
1836void FilterEffectsDialog::PrimitiveList::set_menu(sigc::slot<void ()> dup, sigc::slot<void ()> rem)
1837{
1838 _primitive_menu = create_popup_menu(_dialog, std::move(dup), std::move(rem));
1839}
1840
1842{
1843 if(_dialog._filter_modifier.get_selected_filter()) {
1844 Gtk::TreeModel::iterator i = get_selection()->get_selected();
1845 if(i)
1846 return (*i)[_columns.primitive];
1847 }
1848
1849 return nullptr;
1850}
1851
1853{
1854 for (auto &&item : _model->children()) {
1855 if (item[_columns.primitive] == prim) {
1856 get_selection()->select(item.get_iter());
1857 break;
1858 }
1859 }
1860}
1861
1863{
1864 if (SPFilterPrimitive* prim = get_selected()) {
1865 _observer->set(nullptr);
1866 _model->erase(get_selection()->get_selected());
1867
1868 //XML Tree being used directly here while it shouldn't be.
1869 sp_repr_unparent(prim->getRepr());
1870
1871 DocumentUndo::done(_dialog.getDocument(), _("Remove filter primitive"), INKSCAPE_ICON("dialog-filters"));
1872
1873 update();
1874 }
1875}
1876
1877void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
1878 std::vector<Geom::Point> const &points,
1879 Gdk::RGBA const &fill, Gdk::RGBA const &stroke);
1880
1881void FilterEffectsDialog::PrimitiveList::snapshot_vfunc(Glib::RefPtr<Gtk::Snapshot> const &snapshot)
1882{
1883 parent_type::snapshot_vfunc(snapshot);
1884
1885 auto const cr = snapshot->append_cairo(get_allocation());
1886
1887 cr->set_line_width(1.0);
1888 // In GTK+ 3, the draw function receives the widget window, not the
1889 // bin_window (i.e., just the area under the column headers). We
1890 // therefore translate the origin of our coordinate system to account for this
1891 int x_origin, y_origin;
1892 convert_bin_window_to_widget_coords(0,0,x_origin,y_origin);
1893 cr->translate(x_origin, y_origin);
1894
1895 auto const fg_color = get_color();
1896 auto bar_color = mix_colors(bg_color, fg_color, 0.06);
1897 // color of connector arrow heads and effect separator lines
1898 auto mid_color = mix_colors(bg_color, fg_color, 0.16);
1899
1900 SPFilterPrimitive* prim = get_selected();
1901 int row_count = get_model()->children().size();
1902
1903 static constexpr int fwidth = CellRendererConnection::size_w;
1904 Gdk::Rectangle rct, vis;
1905 Gtk::TreeModel::iterator row = get_model()->children().begin();
1906 int text_start_x = 0;
1907 if(row) {
1908 get_cell_area(get_model()->get_path(row), *get_column(1), rct);
1909 get_visible_rect(vis);
1910 text_start_x = rct.get_x() + rct.get_width() - get_input_type_width() * _inputs_count + 1;
1911
1912 auto w = get_input_type_width();
1913 auto h = vis.get_height();
1914 cr->save();
1915 // erase selection color from selected item
1916 Gdk::Cairo::set_source_rgba(cr, bg_color);
1917 cr->rectangle(text_start_x + 1, 0, w * _inputs_count, h);
1918 cr->fill();
1919 auto const text_color = change_alpha(fg_color, 0.7);
1920
1921 // draw vertical bars corresponding to possible filter inputs
1922 for(unsigned int i = 0; i < _inputs_count; ++i) {
1923 _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
1924 const int x = text_start_x + w * i;
1925 cr->save();
1926
1927 Gdk::Cairo::set_source_rgba(cr, bar_color);
1928 cr->rectangle(x + 1, 0, w - 2, h);
1929 cr->fill();
1930
1931 Gdk::Cairo::set_source_rgba(cr, text_color);
1932 cr->move_to(x + w, 5);
1933 cr->rotate_degrees(90);
1934 _vertical_layout->show_in_cairo_context(cr);
1935
1936 cr->restore();
1937 }
1938
1939 cr->restore();
1940 cr->rectangle(vis.get_x(), 0, vis.get_width(), vis.get_height());
1941 cr->clip();
1942 }
1943
1944 int row_index = 0;
1945 for(; row != get_model()->children().end(); ++row, ++row_index) {
1946 get_cell_area(get_model()->get_path(row), *get_column(1), rct);
1947 const int x = rct.get_x(), y = rct.get_y(), h = rct.get_height();
1948
1949 // Check mouse state
1950 double mx{}, my{};
1951 Gdk::ModifierType mask;
1952 auto const display = get_display();
1953 auto seat = display->get_default_seat();
1954 auto device = seat->get_pointer();
1955 auto const surface = dynamic_cast<Gtk::Native &>(*get_root()).get_surface();
1956 g_assert(surface);
1957 surface->get_device_position(device, mx, my, mask);
1958
1959 cr->set_line_width(1);
1960
1961 // Outline the bottom of the connection area
1962 const int outline_x = x + fwidth * (row_count - row_index);
1963 cr->save();
1964
1965 Gdk::Cairo::set_source_rgba(cr, mid_color);
1966
1967 cr->move_to(vis.get_x(), y + h);
1968 cr->line_to(outline_x, y + h);
1969 // Side outline
1970 cr->line_to(outline_x, y - 1);
1971
1972 cr->stroke();
1973 cr->restore();
1974
1975 std::vector<Geom::Point> con_poly;
1976 int con_drag_y = 0;
1977 int con_drag_x = 0;
1978 bool inside;
1979 const SPFilterPrimitive* row_prim = (*row)[_columns.primitive];
1980 const int inputs = input_count(row_prim);
1981
1982 if(is<SPFeMerge>(row_prim)) {
1983 for(int i = 0; i < inputs; ++i) {
1984 inside = do_connection_node(row, i, con_poly, mx, my);
1985
1986 draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
1987
1988 if(_in_drag == (i + 1)) {
1989 con_drag_y = con_poly[2].y();
1990 con_drag_x = con_poly[2].x();
1991 }
1992
1993 if(_in_drag != (i + 1) || row_prim != prim) {
1994 draw_connection(cr, row, SPAttr::INVALID, text_start_x, outline_x,
1995 con_poly[2].y(), row_count, i, fg_color, mid_color);
1996 }
1997 }
1998 }
1999 else {
2000 // Draw "in" shape
2001 inside = do_connection_node(row, 0, con_poly, mx, my);
2002 con_drag_y = con_poly[2].y();
2003 con_drag_x = con_poly[2].x();
2004
2005 draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
2006
2007 // Draw "in" connection
2008 if(_in_drag != 1 || row_prim != prim) {
2009 draw_connection(cr, row, SPAttr::IN_, text_start_x, outline_x,
2010 con_poly[2].y(), row_count, -1, fg_color, mid_color);
2011 }
2012
2013 if(inputs == 2) {
2014 // Draw "in2" shape
2015 inside = do_connection_node(row, 1, con_poly, mx, my);
2016 if(_in_drag == 2) {
2017 con_drag_y = con_poly[2].y();
2018 con_drag_x = con_poly[2].x();
2019 }
2020
2021 draw_connection_node(cr, con_poly, inside ? fg_color : mid_color, fg_color);
2022
2023 // Draw "in2" connection
2024 if(_in_drag != 2 || row_prim != prim) {
2025 draw_connection(cr, row, SPAttr::IN2, text_start_x, outline_x,
2026 con_poly[2].y(), row_count, -1, fg_color, mid_color);
2027 }
2028 }
2029 }
2030
2031 // Draw drag connection
2032 if(row_prim == prim && _in_drag) {
2033 cr->save();
2034 Gdk::Cairo::set_source_rgba(cr, fg_color);
2035 cr->move_to(con_drag_x, con_drag_y);
2036 cr->line_to(mx, con_drag_y);
2037 cr->line_to(mx, my);
2038 cr->stroke();
2039 cr->restore();
2040 }
2041 }
2042}
2043
2044void FilterEffectsDialog::PrimitiveList::draw_connection(const Cairo::RefPtr<Cairo::Context>& cr,
2045 const Gtk::TreeModel::iterator& input, const SPAttr attr,
2046 const int text_start_x, const int x1, const int y1,
2047 const int row_count, const int pos,
2048 const Gdk::RGBA fg_color, const Gdk::RGBA mid_color)
2049{
2050 cr->save();
2051
2052 int src_id = 0;
2053 Gtk::TreeModel::iterator res = find_result(input, attr, src_id, pos);
2054
2055 const bool is_first = input == get_model()->children().begin();
2056 const bool is_selected = (get_selection()->get_selected())
2057 ? input == get_selection()->get_selected()
2058 : false; // if get_selected() is invalid, comparison crashes in gtk
2059 const bool is_merge = is<SPFeMerge>((SPFilterPrimitive*)(*input)[_columns.primitive]);
2060 const bool use_default = !res && !is_merge;
2061 int arc_radius = 4;
2062
2063 if (is_selected) {
2064 cr->set_line_width(2.5);
2065 arc_radius = 6;
2066 }
2067
2068 if(res == input || (use_default && is_first)) {
2069 // Draw straight connection to a standard input
2070 // Draw a lighter line for an implicit connection to a standard input
2071 const int tw = get_input_type_width();
2072 gint end_x = text_start_x + tw * src_id + 1;
2073
2074 if(use_default && is_first) {
2075 Gdk::Cairo::set_source_rgba(cr, fg_color);
2076 cr->set_dash(std::vector<double> {1.0, 1.0}, 0);
2077 } else {
2078 Gdk::Cairo::set_source_rgba(cr, fg_color);
2079 }
2080
2081 // draw a half-circle touching destination band
2082 cr->move_to(x1, y1);
2083 cr->line_to(end_x, y1);
2084 cr->stroke();
2085 cr->arc(end_x, y1, arc_radius, M_PI / 2, M_PI * 1.5);
2086 cr->fill();
2087 }
2088 else {
2089 // Draw an 'L'-shaped connection to another filter primitive
2090 // If no connection is specified, draw a light connection to the previous primitive
2091 if(use_default) {
2092 res = input;
2093 --res;
2094 }
2095
2096 if(res) {
2097 Gdk::Rectangle rct;
2098
2099 get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct);
2100 static constexpr int fheight = CellRendererConnection::size_h;
2101 static constexpr int fwidth = CellRendererConnection::size_w;
2102
2103 get_cell_area(get_model()->get_path(res), *get_column(1), rct);
2104 const int row_index = find_index(res);
2105 const int x2 = rct.get_x() + fwidth * (row_count - row_index) - fwidth / 2;
2106 const int y2 = rct.get_y() + rct.get_height();
2107
2108 // Draw a bevelled 'L'-shaped connection
2109 Gdk::Cairo::set_source_rgba(cr, fg_color);
2110 cr->move_to(x1, y1);
2111 cr->line_to(x2 - fwidth/4, y1);
2112 cr->line_to(x2, y1 - fheight/4);
2113 cr->line_to(x2, y2);
2114 cr->stroke();
2115 }
2116 }
2117 cr->restore();
2118}
2119
2120// Draw the triangular outline of the connection node, and fill it
2121// if desired
2122void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr,
2123 std::vector<Geom::Point> const &points,
2124 Gdk::RGBA const &fill, Gdk::RGBA const &stroke)
2125{
2126 cr->save();
2127 cr->move_to(points[0].x() + 0.5, points[0].y() + 0.5);
2128 cr->line_to(points[1].x() + 0.5, points[1].y() + 0.5);
2129 cr->line_to(points[2].x() + 0.5, points[2].y() + 0.5);
2130 cr->line_to(points[0].x() + 0.5, points[0].y() + 0.5);
2131 cr->close_path();
2132
2133 Gdk::Cairo::set_source_rgba(cr, fill);
2134 cr->fill_preserve();
2135 cr->set_line_width(1);
2136 Gdk::Cairo::set_source_rgba(cr, stroke);
2137 cr->stroke();
2138
2139 cr->restore();
2140}
2141
2142// Creates a triangle outline of the connection node and returns true if (x,y) is inside the node
2143bool FilterEffectsDialog::PrimitiveList::do_connection_node(const Gtk::TreeModel::iterator& row, const int input,
2144 std::vector<Geom::Point> &points,
2145 const int ix, const int iy)
2146{
2147 Gdk::Rectangle rct;
2148 const int icnt = input_count((*row)[_columns.primitive]);
2149
2150 get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct);
2151 static constexpr int fheight = CellRendererConnection::size_h;
2152 static constexpr int fwidth = CellRendererConnection::size_w;
2153
2154 get_cell_area(_model->get_path(row), *get_column(1), rct);
2155 const float h = rct.get_height() / icnt;
2156
2157 const int x = rct.get_x() + fwidth * (_model->children().size() - find_index(row));
2158 // this is how big arrowhead appears:
2159 const int con_w = (int)(fwidth * 0.70f);
2160 const int con_h = (int)(fheight * 0.35f);
2161 const int con_y = (int)(rct.get_y() + (h / 2) - con_h + (input * h));
2162 points.clear();
2163 points.emplace_back(x, con_y);
2164 points.emplace_back(x, con_y + con_h * 2);
2165 points.emplace_back(x - con_w, con_y + con_h);
2166
2167 return ix >= x - h && iy >= con_y && ix <= x && iy <= points[1].y();
2168}
2169
2170const Gtk::TreeModel::iterator FilterEffectsDialog::PrimitiveList::find_result(const Gtk::TreeModel::iterator& start,
2171 const SPAttr attr, int& src_id,
2172 const int pos)
2173{
2174 SPFilterPrimitive* prim = (*start)[_columns.primitive];
2175 Gtk::TreeModel::iterator target = _model->children().end();
2176 int image = 0;
2177
2178 if(is<SPFeMerge>(prim)) {
2179 int c = 0;
2180 bool found = false;
2181 for (auto& o: prim->children) {
2182 if(c == pos && is<SPFeMergeNode>(&o)) {
2183 image = cast<SPFeMergeNode>(&o)->get_in();
2184 found = true;
2185 }
2186 ++c;
2187 }
2188 if(!found)
2189 return target;
2190 }
2191 else {
2192 if(attr == SPAttr::IN_)
2193 image = prim->get_in();
2194 else if(attr == SPAttr::IN2) {
2195 if(is<SPFeBlend>(prim))
2196 image = cast<SPFeBlend>(prim)->get_in2();
2197 else if(is<SPFeComposite>(prim))
2198 image = cast<SPFeComposite>(prim)->get_in2();
2199 else if(is<SPFeDisplacementMap>(prim))
2200 image = cast<SPFeDisplacementMap>(prim)->get_in2();
2201 else
2202 return target;
2203 }
2204 else
2205 return target;
2206 }
2207
2208 if(image >= 0) {
2209 for(Gtk::TreeModel::iterator i = _model->children().begin();
2210 i != start; ++i) {
2211 if(((SPFilterPrimitive*)(*i)[_columns.primitive])->get_out() == image)
2212 target = i;
2213 }
2214 return target;
2215 }
2216 else if(image < -1) {
2217 src_id = -(image + 2);
2218 return start;
2219 }
2220
2221 return target;
2222}
2223
2224int FilterEffectsDialog::PrimitiveList::find_index(const Gtk::TreeModel::iterator& target)
2225{
2226 int i = 0;
2227 for (auto iter = _model->children().begin(); iter != target; ++iter, ++i) {}
2228 return i;
2229}
2230
2231static std::pair<int, int> widget_to_bin_window(Gtk::TreeView const &tree_view, int const wx, int const wy)
2232{
2233 int bx, by;
2234 tree_view.convert_widget_to_bin_window_coords(wx, wy, bx, by);
2235 return {bx, by};
2236}
2237
2238Gtk::EventSequenceState
2240 int /*n_press*/,
2241 double const wx, double const wy)
2242{
2243 Gtk::TreePath path;
2244 Gtk::TreeViewColumn* col;
2245 auto const [x, y] = widget_to_bin_window(*this, wx, wy);
2246 int cx, cy;
2247
2248 _drag_prim = nullptr;
2249
2250 if(get_path_at_pos(x, y, path, col, cx, cy)) {
2251 Gtk::TreeModel::iterator iter = _model->get_iter(path);
2252 std::vector<Geom::Point> points;
2253
2254 _drag_prim = (*iter)[_columns.primitive];
2255 const int icnt = input_count(_drag_prim);
2256
2257 for(int i = 0; i < icnt; ++i) {
2258 if(do_connection_node(_model->get_iter(path), i, points, x, y)) {
2259 _in_drag = i + 1;
2260 break;
2261 }
2262 }
2263
2264 queue_draw();
2265 }
2266
2267 if(_in_drag) {
2268 _scroll_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &PrimitiveList::on_scroll_timeout), 150);
2269 _autoscroll_x = 0;
2270 _autoscroll_y = 0;
2271 get_selection()->select(path);
2272 return Gtk::EventSequenceState::CLAIMED;
2273 }
2274
2275 return Gtk::EventSequenceState::NONE;
2276}
2277
2279{
2280 const int speed = 10;
2281 const int limit = 15;
2282
2283 auto const [x, y] = widget_to_bin_window(*this, wx, wy);
2284
2285 Gdk::Rectangle vis;
2286 get_visible_rect(vis);
2287 int vis_x, vis_y;
2288 int vis_x2, vis_y2;
2289 convert_widget_to_tree_coords(vis.get_x(), vis.get_y(), vis_x2, vis_y2);
2290 convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y);
2291 const int top = vis_y + vis.get_height();
2292 const int right_edge = vis_x + vis.get_width();
2293
2294 // When autoscrolling during a connection drag, set the speed based on
2295 // where the mouse is in relation to the edges.
2296 if (y < vis_y)
2297 _autoscroll_y = -(int)(speed + (vis_y - y) / 5);
2298 else if (y < vis_y + limit)
2299 _autoscroll_y = -speed;
2300 else if (y > top)
2301 _autoscroll_y = (int)(speed + (y - top) / 5);
2302 else if (y > top - limit)
2303 _autoscroll_y = speed;
2304 else
2305 _autoscroll_y = 0;
2306
2307 double const e2 = x - vis_x2 / 2;
2308 // horizontal scrolling
2309 if(e2 < vis_x)
2310 _autoscroll_x = -(int)(speed + (vis_x - e2) / 5);
2311 else if(e2 < vis_x + limit)
2312 _autoscroll_x = -speed;
2313 else if(e2 > right_edge)
2314 _autoscroll_x = (int)(speed + (e2 - right_edge) / 5);
2315 else if(e2 > right_edge - limit)
2316 _autoscroll_x = speed;
2317 else
2318 _autoscroll_x = 0;
2319
2320 queue_draw();
2321}
2322
2323Gtk::EventSequenceState
2325 int /*n_press*/,
2326 double const wx, double const wy)
2327{
2328 _scroll_connection.disconnect();
2329
2330 auto const prim = get_selected();
2331 if(_in_drag && prim) {
2332 auto const [x, y] = widget_to_bin_window(*this, wx, wy);
2333 Gtk::TreePath path;
2334 Gtk::TreeViewColumn* col;
2335 int cx, cy;
2336 if (get_path_at_pos(x, y, path, col, cx, cy)) {
2337 auto const selected_iter = get_selection()->get_selected();
2338 g_assert(selected_iter);
2339
2340 auto const target_iter = _model->get_iter(path);
2341 g_assert(target_iter);
2342
2343 auto const target = target_iter->get_value(_columns.primitive);
2344 g_assert(target);
2345
2346 col = get_column(1);
2347 g_assert(col);
2348
2349 char const *in_val = nullptr;
2350 Glib::ustring result;
2351
2352 Gdk::Rectangle rct;
2353 get_cell_area(path, *col, rct);
2354 const int twidth = get_input_type_width();
2355 const int sources_x = rct.get_width() - twidth * _inputs_count;
2356 if(cx > sources_x) {
2357 int src = (cx - sources_x) / twidth;
2358 if (src < 0) {
2359 src = 0;
2360 } else if(src >= static_cast<int>(_inputs_count)) {
2361 src = _inputs_count - 1;
2362 }
2364 in_val = result.c_str();
2365 } else {
2366 // Ensure that the target comes before the selected primitive
2367 for (auto iter = _model->children().begin(); iter != selected_iter; ++iter) {
2368 if(iter == target_iter) {
2369 Inkscape::XML::Node *repr = target->getRepr();
2370 // Make sure the target has a result
2371 const gchar *gres = repr->attribute("result");
2372 if(!gres) {
2373 result = cast<SPFilter>(prim->parent)->get_new_result_name();
2374 repr->setAttributeOrRemoveIfEmpty("result", result);
2375 in_val = result.c_str();
2376 }
2377 else
2378 in_val = gres;
2379 break;
2380 }
2381 }
2382 }
2383
2384 if(is<SPFeMerge>(prim)) {
2385 int c = 1;
2386 bool handled = false;
2387 for (auto& o: prim->children) {
2388 if(c == _in_drag && is<SPFeMergeNode>(&o)) {
2389 // If input is null, delete it
2390 if(!in_val) {
2391 //XML Tree being used directly here while it shouldn't be.
2393 DocumentUndo::done(prim->document, _("Remove merge node"), INKSCAPE_ICON("dialog-filters"));
2394 selected_iter->set_value(_columns.primitive, prim);
2395 } else {
2396 _dialog.set_attr(&o, SPAttr::IN_, in_val);
2397 }
2398 handled = true;
2399 break;
2400 }
2401 ++c;
2402 }
2403
2404 // Add new input?
2405 if(!handled && c == _in_drag && in_val) {
2406 Inkscape::XML::Document *xml_doc = prim->document->getReprDoc();
2407 Inkscape::XML::Node *repr = xml_doc->createElement("svg:feMergeNode");
2408 repr->setAttribute("inkscape:collect", "always");
2409
2410 //XML Tree being used directly here while it shouldn't be.
2411 prim->getRepr()->appendChild(repr);
2412 auto node = cast<SPFeMergeNode>(prim->document->getObjectByRepr(repr));
2414 _dialog.set_attr(node, SPAttr::IN_, in_val);
2415 selected_iter->set_value(_columns.primitive, prim);
2416 }
2417 }
2418 else {
2419 if(_in_drag == 1)
2420 _dialog.set_attr(prim, SPAttr::IN_, in_val);
2421 else if(_in_drag == 2)
2422 _dialog.set_attr(prim, SPAttr::IN2, in_val);
2423 }
2424 }
2425
2426 _in_drag = 0;
2427 queue_draw();
2428
2429 _dialog.update_settings_view();
2430 }
2431
2432 if (click.get_current_button() == 3) {
2433 bool const sensitive = prim != nullptr;
2434 _primitive_menu->set_sensitive(sensitive);
2435 _dialog._popoverbin.setPopover(_primitive_menu.get());
2436 _primitive_menu->popup_at(*this, wx + 4, wy);
2437 return Gtk::EventSequenceState::CLAIMED;
2438 }
2439
2440 return Gtk::EventSequenceState::NONE;
2441}
2442
2443// Checks all of prim's inputs, removes any that use result
2445{
2446 if (prim && (result >= 0)) {
2447 if (prim->get_in() == result) {
2448 prim->removeAttribute("in");
2449 }
2450
2451 if (auto blend = cast<SPFeBlend>(prim)) {
2452 if (blend->get_in2() == result) {
2453 prim->removeAttribute("in2");
2454 }
2455 } else if (auto comp = cast<SPFeComposite>(prim)) {
2456 if (comp->get_in2() == result) {
2457 prim->removeAttribute("in2");
2458 }
2459 } else if (auto disp = cast<SPFeDisplacementMap>(prim)) {
2460 if (disp->get_in2() == result) {
2461 prim->removeAttribute("in2");
2462 }
2463 }
2464 }
2465}
2466
2467// Remove any connections going to/from prim_iter that forward-reference other primitives
2468void FilterEffectsDialog::PrimitiveList::sanitize_connections(const Gtk::TreeModel::iterator& prim_iter)
2469{
2470 SPFilterPrimitive *prim = (*prim_iter)[_columns.primitive];
2471 bool before = true;
2472
2473 for (auto iter = _model->children().begin(); iter != _model->children().end(); ++iter) {
2474 if(iter == prim_iter)
2475 before = false;
2476 else {
2477 SPFilterPrimitive* cur_prim = (*iter)[_columns.primitive];
2478 if(before)
2479 check_single_connection(cur_prim, prim->get_out());
2480 else
2481 check_single_connection(prim, cur_prim->get_out());
2482 }
2483 }
2484}
2485
2486// Reorder the filter primitives to match the list order
2487void FilterEffectsDialog::PrimitiveList::on_drag_end(Glib::RefPtr<Gdk::Drag> const &/*&drag*/,
2488 bool /*delete_data*/)
2489{
2490 SPFilter* filter = _dialog._filter_modifier.get_selected_filter();
2491 g_assert(filter);
2492
2493 int ndx = 0;
2494 for (auto iter = _model->children().begin(); iter != _model->children().end(); ++iter, ++ndx) {
2495 SPFilterPrimitive* prim = (*iter)[_columns.primitive];
2496 if (prim && prim == _drag_prim) {
2497 prim->getRepr()->setPosition(ndx);
2498 break;
2499 }
2500 }
2501
2502 for (auto iter = _model->children().begin(); iter != _model->children().end(); ++iter) {
2503 SPFilterPrimitive* prim = (*iter)[_columns.primitive];
2504 if (prim && prim == _drag_prim) {
2505 sanitize_connections(iter);
2506 get_selection()->select(iter);
2507 break;
2508 }
2509 }
2510
2511 filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
2512 DocumentUndo::done(filter->document, _("Reorder filter primitive"), INKSCAPE_ICON("dialog-filters"));
2513}
2514
2515static void autoscroll(Glib::RefPtr<Gtk::Adjustment> const &a, double const delta)
2516{
2517 auto v = a->get_value() + delta;
2518 v = std::clamp(v, 0.0, a->get_upper() - a->get_page_size());
2519 a->set_value(v);
2520}
2521
2522// If a connection is dragged towards the top or bottom of the list, the list should scroll to follow.
2524{
2525 if (!(_autoscroll_y || _autoscroll_x)) return true;
2526
2527 auto &scrolled_window = dynamic_cast<Gtk::ScrolledWindow &>(*get_parent());
2528
2529 if(_autoscroll_y) {
2530 autoscroll(scrolled_window.get_vadjustment(), _autoscroll_y);
2531 }
2532
2533 if(_autoscroll_x) {
2534 autoscroll(scrolled_window.get_hadjustment(), _autoscroll_x);
2535 }
2536
2537 queue_draw();
2538 return true;
2539}
2540
2542{
2543 return _model->children().size();
2544}
2545
2547{
2548 // Maximum font height calculated in initText() and stored in _input_type_width.
2549 // Add 2 to font height to account for rectangle around text.
2550 return _input_type_width + 2;
2551}
2552
2554 return _inputs_count;
2555}
2556
2558 _inputs_count = count;
2559 queue_allocate();
2560 queue_draw();
2561}
2562
2564
2565const Glib::ustring& get_category_name(EffectCategory category) {
2566 static const std::map<EffectCategory, Glib::ustring> category_names = {
2567 { EffectCategory::Effect, _("Effect") },
2568 { EffectCategory::Compose, _("Compositing") },
2569 { EffectCategory::Colors, _("Color editing") },
2570 { EffectCategory::Generation, _("Generating") },
2571 };
2572 return category_names.at(category);
2573}
2574
2575struct EffectMetadata {
2576 EffectCategory category;
2577 Glib::ustring icon_name;
2578 Glib::ustring tooltip;
2579};
2580
2581static const std::map<Inkscape::Filters::FilterPrimitiveType, EffectMetadata>& get_effects() {
2582 static std::map<Inkscape::Filters::FilterPrimitiveType, EffectMetadata> effects = {
2583 { NR_FILTER_GAUSSIANBLUR, { EffectCategory::Effect, "feGaussianBlur-icon",
2584 _("Uniformly blurs its input. Commonly used together with Offset to create a drop shadow effect.") }},
2585 { NR_FILTER_MORPHOLOGY, { EffectCategory::Effect, "feMorphology-icon",
2586 _("Provides erode and dilate effects. For single-color objects erode makes the object thinner and dilate makes it thicker.") }},
2587 { NR_FILTER_OFFSET, { EffectCategory::Effect, "feOffset-icon",
2588 _("Offsets the input by an user-defined amount. Commonly used for drop shadow effects.") }},
2589 { NR_FILTER_CONVOLVEMATRIX, { EffectCategory::Effect, "feConvolveMatrix-icon",
2590 _("Performs a convolution on the input image enabling effects like blur, sharpening, embossing and edge detection.") }},
2591 { NR_FILTER_DISPLACEMENTMAP, { EffectCategory::Effect, "feDisplacementMap-icon",
2592 _("Displaces pixels from the first input using the second as a map of displacement intensity. Classical examples are whirl and pinch effects.") }},
2593 { NR_FILTER_TILE, { EffectCategory::Effect, "feTile-icon",
2594 _("Tiles a region with an input graphic. The source tile is defined by the filter primitive subregion of the input.") }},
2595 { NR_FILTER_COMPOSITE, { EffectCategory::Compose, "feComposite-icon",
2596 _("Composites two images using one of the Porter-Duff blending modes or the arithmetic mode described in SVG standard.") }},
2597 { NR_FILTER_BLEND, { EffectCategory::Compose, "feBlend-icon",
2598 _("Provides image blending modes, such as screen, multiply, darken and lighten.") }},
2599 { NR_FILTER_MERGE, { EffectCategory::Compose, "feMerge-icon",
2600 _("Merges multiple inputs using normal alpha compositing. Equivalent to using several Blend primitives in 'normal' mode or several Composite primitives in 'over' mode.") }},
2601 { NR_FILTER_COLORMATRIX, { EffectCategory::Colors, "feColorMatrix-icon",
2602 _("Modifies pixel colors based on a transformation matrix. Useful for adjusting color hue and saturation.") }},
2603 { NR_FILTER_COMPONENTTRANSFER, { EffectCategory::Colors, "feComponentTransfer-icon",
2604 _("Manipulates color components according to particular transfer functions. Useful for brightness and contrast adjustment, color balance, and thresholding.") }},
2605 { NR_FILTER_DIFFUSELIGHTING, { EffectCategory::Colors, "feDiffuseLighting-icon",
2606 _("Creates \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.") }},
2607 { NR_FILTER_SPECULARLIGHTING, { EffectCategory::Colors, "feSpecularLighting-icon",
2608 _("Creates \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.") }},
2609 { NR_FILTER_FLOOD, { EffectCategory::Generation, "feFlood-icon",
2610 _("Fills the region with a given color and opacity. Often used as input to other filters to apply color to a graphic.") }},
2611 { NR_FILTER_IMAGE, { EffectCategory::Generation, "feImage-icon",
2612 _("Fills the region with graphics from an external file or from another portion of the document.") }},
2613 { NR_FILTER_TURBULENCE, { EffectCategory::Generation, "feTurbulence-icon",
2614 _("Renders Perlin noise, which is useful to generate textures such as clouds, fire, smoke, marble or granite.") }},
2615 };
2616 return effects;
2617}
2618
2619// populate popup with filter effects and completion list for a search box
2621 auto& menu = popup.get_menu();
2622
2623 struct Effect {
2625 Glib::ustring label;
2626 EffectCategory category;
2627 Glib::ustring icon_name;
2628 Glib::ustring tooltip;
2629 };
2630 std::vector<Effect> effects;
2631 effects.reserve(get_effects().size());
2632 for (auto&& effect : get_effects()) {
2633 effects.push_back({
2634 effect.first,
2635 _(FPConverter.get_label(effect.first).c_str()),
2636 effect.second.category,
2637 effect.second.icon_name,
2638 effect.second.tooltip
2639 });
2640 }
2641 std::sort(begin(effects), end(effects), [=](auto&& a, auto&& b) {
2642 if (a.category != b.category) {
2643 return a.category < b.category;
2644 }
2645 return a.label < b.label;
2646 });
2647
2648 popup.clear_completion_list();
2649
2650 // 2-column menu
2651 Inkscape::UI::ColumnMenuBuilder<EffectCategory> builder{menu, 2, Gtk::IconSize::LARGE};
2652 for (auto const &effect : effects) {
2653 // build popup menu
2654 auto const &type = effect.type;
2655 auto const menuitem = builder.add_item(effect.label, effect.category, effect.tooltip,
2656 effect.icon_name, true, true,
2657 [=, this]{ add_filter_primitive(type); });
2658 auto const id = static_cast<int>(type);
2659 menuitem->signal_query_tooltip().connect([=, this] (int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tooltipw) {
2660 return sp_query_custom_tooltip(this, x, y, kbd, tooltipw, id, effect.tooltip, effect.icon_name);
2661 }, false); // before
2662 if (builder.new_section()) {
2663 builder.set_section(get_category_name(effect.category));
2664 }
2665 // build completion list
2666 popup.add_to_completion_list(id, effect.label, effect.icon_name + (symbolic ? "-symbolic" : ""));
2667 }
2668 if (symbolic) {
2669 menu.add_css_class("symbolic");
2670 }
2671}
2672
2673/*** FilterEffectsDialog ***/
2674
2676 : DialogBase("/dialogs/filtereffects", "FilterEffects"),
2677 _builder(create_builder("dialog-filter-editor.glade")),
2678 _paned(get_widget<Gtk::Paned>(_builder, "paned")),
2679 _main_grid(get_widget<Gtk::Grid>(_builder, "main")),
2680 _params_box(get_widget<Gtk::Box>(_builder, "params")),
2681 _search_box(get_widget<Gtk::Box>(_builder, "search")),
2682 _search_wide_box(get_widget<Gtk::Box>(_builder, "search-wide")),
2683 _filter_wnd(get_widget<Gtk::ScrolledWindow>(_builder, "filter")),
2684 _cur_filter_btn(get_widget<Gtk::CheckButton>(_builder, "label"))
2685 , _add_primitive_type(FPConverter)
2686 , _add_primitive(_("Add Effect:"))
2687 , _empty_settings("", Gtk::Align::CENTER)
2688 , _no_filter_selected(_("No filter selected"), Gtk::Align::START)
2689 , _settings_initialized(false)
2690 , _locked(false)
2691 , _attr_lock(false)
2692 , _filter_modifier(*this, _builder)
2693 , _primitive_list(*this)
2694 , _settings_effect(Gtk::Orientation::VERTICAL)
2695 , _settings_filter(Gtk::Orientation::VERTICAL)
2696{
2697 _settings = std::make_unique<Settings>(*this, _settings_effect,
2698 [this](auto const a){ set_attr_direct(a); },
2700 _cur_effect_name = &get_widget<Gtk::Label>(_builder, "cur-effect");
2701 _settings->_size_group->add_widget(*_cur_effect_name);
2702 _filter_general_settings = std::make_unique<Settings>(*this, _settings_filter,
2703 [this](auto const a){ set_filternode_attr(a); }, 1);
2704
2705 // Initialize widget hierarchy
2706 _primitive_box = &get_widget<Gtk::ScrolledWindow>(_builder, "filter");
2707 _primitive_list.set_enable_search(false);
2708 _primitive_box->set_child(_primitive_list);
2709
2710 auto symbolic = Inkscape::Preferences::get()->getBool("/theme/symbolicIcons", true);
2711 add_effects(_effects_popup, symbolic);
2712 _effects_popup.get_entry().set_placeholder_text(_("Add effect"));
2714 [this](int const id){ add_filter_primitive(static_cast<FilterPrimitiveType>(id)); });
2716
2717 _settings_effect.set_valign(Gtk::Align::FILL);
2719
2720 _settings_filter.set_margin(5);
2721 get_widget<Gtk::Popover>(_builder, "gen-settings").set_child(_settings_filter);
2722
2723 get_widget<Gtk::Popover>(_builder, "info-popover").signal_show().connect([this]{
2724 if (auto prim = _primitive_list.get_selected()) {
2725 if (prim->getRepr()) {
2726 auto id = FPConverter.get_id_from_key(prim->getRepr()->name());
2727 const auto& effect = get_effects().at(id);
2728 get_widget<Gtk::Image>(_builder, "effect-icon").set_from_icon_name(effect.icon_name);
2729 auto buffer = get_widget<Gtk::TextView>(_builder, "effect-info").get_buffer();
2730 buffer->set_text("");
2731 buffer->insert_markup(buffer->begin(), effect.tooltip);
2732 get_widget<Gtk::TextView>(_builder, "effect-desc").get_buffer()->set_text("");
2733 }
2734 }
2735 });
2736
2737 _primitive_list.signal_primitive_changed().connect([this]{ update_settings_view(); });
2738
2739 _cur_filter_toggle = _cur_filter_btn.signal_toggled().connect([this]{
2740 _filter_modifier.toggle_current_filter();
2741 });
2742
2743 auto update_checkbox = [this]{
2744 auto active = _filter_modifier.is_selected_filter_active();
2745 _cur_filter_toggle.block();
2746 _cur_filter_btn.set_active(active);
2747 _cur_filter_toggle.unblock();
2748 };
2749
2750 auto update_widgets = [=, this]{
2751 auto& opt = get_widget<Gtk::MenuButton>(_builder, "filter-opt");
2752 _primitive_list.update();
2753 Glib::ustring name;
2754 if (auto filter = _filter_modifier.get_selected_filter()) {
2755 name = get_filter_name(filter);
2756 _effects_popup.set_sensitive();
2757 _cur_filter_btn.set_sensitive(); // ideally this should also be selection-dependent
2758 opt.set_sensitive();
2759 } else {
2760 name = "-";
2761 _effects_popup.set_sensitive(false);
2762 _cur_filter_btn.set_sensitive(false);
2763 opt.set_sensitive(false);
2764 }
2765 get_widget<Gtk::Label>(_builder, "filter-name").set_label(name);
2766 update_checkbox();
2767 update_settings_view();
2768 };
2769
2770 //TODO: adding animated GIFs to the info popup once they are ready:
2771 // auto a = Gdk::PixbufAnimation::create_from_file("/Users/mike/blur-effect.gif");
2772 // get_widget<Gtk::Image>(_builder, "effect-image").property_pixbuf_animation().set_value(a);
2773
2774 init_settings_widgets();
2775
2776 _filter_modifier.signal_filter_changed().connect([=](){
2777 update_widgets();
2778 });
2779
2780 _filter_modifier.signal_filters_updated().connect([=](){
2781 update_checkbox();
2782 });
2783
2784 _add_primitive.signal_clicked().connect(sigc::mem_fun(*this, &FilterEffectsDialog::add_primitive));
2785 _primitive_list.set_menu(sigc::mem_fun(*this, &FilterEffectsDialog::duplicate_primitive),
2786 sigc::mem_fun(_primitive_list, &PrimitiveList::remove_selected));
2787
2788 get_widget<Gtk::Button>(_builder, "new-filter").signal_clicked().connect([this]{ _filter_modifier.add_filter(); });
2789 append(_bin);
2790 _bin.set_expand(true);
2791 _bin.set_child(_popoverbin);
2792 _popoverbin.setChild(&_main_grid);
2793
2794 get_widget<Gtk::Button>(_builder, "dup-btn").signal_clicked().connect([this]{ duplicate_primitive(); });
2795 get_widget<Gtk::Button>(_builder, "del-btn").signal_clicked().connect([this]{ _primitive_list.remove_selected(); });
2796 // get_widget<Gtk::Button>(_builder, "info-btn").signal_clicked().connect([this]{ /* todo */ });
2797
2798 _show_sources = &get_widget<Gtk::ToggleButton>(_builder, "btn-connect");
2799 auto set_inputs = [this](bool const all){
2800 int count = all ? FPInputConverter._length : 2;
2801 _primitive_list.set_inputs_count(count);
2802 // full rebuild: this is what it takes to make cell renderer new min width into account to adjust scrollbar
2803 _primitive_list.update();
2804 };
2805 auto show_all_sources = Inkscape::Preferences::get()->getBool(prefs_path + "/dialogs/filters/showAllSources", false);
2806 _show_sources->set_active(show_all_sources);
2807 set_inputs(show_all_sources);
2808 _show_sources->signal_toggled().connect([=, this]{
2809 bool const show_all = _show_sources->get_active();
2810 set_inputs(show_all);
2811 Inkscape::Preferences::get()->setBool(prefs_path + "/dialogs/filters/showAllSources", show_all);
2812 });
2813
2814 _paned.set_position(Inkscape::Preferences::get()->getIntLimited(prefs_path + "/handlePos", 200, 10, 9999));
2815 _paned.property_position().signal_changed().connect([this]{
2816 Inkscape::Preferences::get()->setInt(prefs_path + "/handlePos", _paned.get_position());
2817 });
2818
2819 _primitive_list.update();
2820
2821 // reading minimal width at this point should reflect space needed for fitting effect parameters panel
2822 Gtk::Requisition minimum_size, natural_size;
2823 get_preferred_size(minimum_size, natural_size);
2824 int min_width = minimum_size.get_width();
2825 _effects_popup.get_preferred_size(minimum_size, natural_size);
2826 auto const min_effects = minimum_size.get_width();
2827 // calculate threshold/minimum width of filters dialog in horizontal layout;
2828 // use this size to decide where transition from vertical to horizontal layout is;
2829 // if this size is too small dialog can get stuck in horizontal layout - users won't be able
2830 // to make it narrow again, due to min dialog size enforced by GTK
2831 int threshold_width = min_width + min_effects * 3;
2832
2833 // two alternative layout arrangements depending on the dialog size;
2834 // one is tall and narrow with widgets in one column, while the other
2835 // is for wide dialogs with filter parameters and effects side by side
2836 _bin.connectBeforeResize([=, this] (int width, int height, int baseline) {
2837 if (width < 10 || height < 10) return;
2838
2839 double const ratio = width / static_cast<double>(height);
2840
2841 constexpr double hysteresis = 0.01;
2842 if (ratio < 1 - hysteresis || width <= threshold_width) {
2843 // make narrow/tall
2844 if (!_narrow_dialog) {
2845 _main_grid.remove(_filter_wnd);
2846 _search_wide_box.remove(_effects_popup);
2847 _paned.set_start_child(_filter_wnd);
2848 UI::pack_start(_search_box, _effects_popup);
2849 _paned.set_size_request();
2850 get_widget<Gtk::Box>(_builder, "connect-box-wide").remove(*_show_sources);
2851 get_widget<Gtk::Box>(_builder, "connect-box").append(*_show_sources);
2852 _narrow_dialog = true;
2853 }
2854 } else if (ratio > 1 + hysteresis && width > threshold_width) {
2855 // make wide/short
2856 if (_narrow_dialog) {
2857 _paned.property_start_child().set_value(nullptr);
2858 _search_box.remove(_effects_popup);
2859 _main_grid.attach(_filter_wnd, 2, 1, 1, 2);
2860 UI::pack_start(_search_wide_box, _effects_popup);
2861 _paned.set_size_request(min_width);
2862 get_widget<Gtk::Box>(_builder, "connect-box").remove(*_show_sources);
2863 get_widget<Gtk::Box>(_builder, "connect-box-wide").append(*_show_sources);
2864 _narrow_dialog = false;
2865 }
2866 }
2867 });
2868
2869 update_widgets();
2870 update();
2871 update_settings_view();
2872}
2873
2875
2877{
2878 _resource_changed.disconnect();
2879 if (auto document = getDocument()) {
2880 auto const update_filters = [this]{ _filter_modifier.update_filters(); };
2881 _resource_changed = document->connectResourcesChanged("filter", update_filters);
2882 update_filters();
2883 }
2884}
2885
2892
2894{
2895 if (flags & (SP_OBJECT_MODIFIED_FLAG |
2896 SP_OBJECT_PARENT_MODIFIED_FLAG |
2897 SP_OBJECT_STYLE_MODIFIED_FLAG)) {
2899 }
2900}
2901
2903{
2904 _locked = l;
2905}
2906
2908{
2909 // TODO: Find better range/climb-rate/digits values for the SpinScales,
2910 // most of the current values are complete guesses!
2911
2912 _empty_settings.set_sensitive(false);
2914
2915 _no_filter_selected.set_sensitive(false);
2917 _settings_initialized = true;
2918
2919 _filter_general_settings->type(0);
2920 auto _region_auto = _filter_general_settings->add_checkbutton(true, SPAttr::AUTO_REGION, _("Automatic Region"), "true", "false", _("If unset, the coordinates and dimensions won't be updated automatically."));
2921 _region_pos = _filter_general_settings->add_multispinbutton(/*default x:*/ (double) -0.1, /*default y:*/ (double) -0.1, SPAttr::X, SPAttr::Y, _("Coordinates:"), -100, 100, 0.01, 0.1, 2, _("X coordinate of the left corners of filter effects region"), _("Y coordinate of the upper corners of filter effects region"));
2922 _region_size = _filter_general_settings->add_multispinbutton(/*default width:*/ (double) 1.2, /*default height:*/ (double) 1.2, SPAttr::WIDTH, SPAttr::HEIGHT, _("Dimensions:"), 0, 1000, 0.01, 0.1, 2, _("Width of filter effects region"), _("Height of filter effects region"));
2923 _region_auto->signal_attr_changed().connect( sigc::bind(sigc::mem_fun(*this, &FilterEffectsDialog::update_automatic_region), _region_auto));
2924
2927
2929 ComboBoxEnum<FilterColorMatrixType>* colmat = _settings->add_combo(COLORMATRIX_MATRIX, SPAttr::TYPE, _("Type:"), ColorMatrixTypeConverter, _("Indicates the type of matrix operation. The keyword 'matrix' indicates that a full 5x4 matrix of values will be provided. The other keywords represent convenience shortcuts to allow commonly used color operations to be performed without specifying a complete matrix."));
2930 _color_matrix_values = _settings->add_colormatrixvalues(_("Value(s):"));
2931 colmat->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::update_color_matrix));
2932
2934 // TRANSLATORS: Abbreviation for red color channel in RGBA
2935 _settings->add_componenttransfervalues(C_("color", "R:"), SPFeFuncNode::R);
2936 // TRANSLATORS: Abbreviation for green color channel in RGBA
2937 _settings->add_componenttransfervalues(C_("color", "G:"), SPFeFuncNode::G);
2938 // TRANSLATORS: Abbreviation for blue color channel in RGBA
2939 _settings->add_componenttransfervalues(C_("color", "B:"), SPFeFuncNode::B);
2940 // TRANSLATORS: Abbreviation for alpha channel in RGBA
2941 _settings->add_componenttransfervalues(C_("color", "A:"), SPFeFuncNode::A);
2942
2945 _k1 = _settings->add_spinscale(0, SPAttr::K1, _("K1:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively."));
2946 _k2 = _settings->add_spinscale(0, SPAttr::K2, _("K2:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively."));
2947 _k3 = _settings->add_spinscale(0, SPAttr::K3, _("K3:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively."));
2948 _k4 = _settings->add_spinscale(0, SPAttr::K4, _("K4:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively."));
2949
2951 _convolve_order = _settings->add_dualspinbutton((char*)"3", SPAttr::ORDER, _("Size:"), 1, max_convolution_kernel_size, 1, 1, 0, _("width of the convolve matrix"), _("height of the convolve matrix"));
2952 _convolve_target = _settings->add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, SPAttr::TARGETX, SPAttr::TARGETY, _("Target:"), 0, max_convolution_kernel_size - 1, 1, 1, 0, _("X coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."), _("Y coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."));
2953 //TRANSLATORS: for info on "Kernel", see http://en.wikipedia.org/wiki/Kernel_(matrix)
2954 _convolve_matrix = _settings->add_matrix(SPAttr::KERNELMATRIX, _("Kernel:"), _("This matrix describes the convolve operation that is applied to the input image in order to calculate the pixel colors at the output. Different arrangements of values in this matrix result in various possible visual effects. An identity matrix would lead to a motion blur effect (parallel to the matrix diagonal) while a matrix filled with a constant non-zero value would lead to a common blur effect."));
2955 _convolve_order->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::convolve_order_changed));
2956 _settings->add_spinscale(0, SPAttr::DIVISOR, _("Divisor:"), 0, 1000, 1, 0.1, 2, _("After applying the kernelMatrix to the input image to yield a number, that number is divided by divisor to yield the final destination color value. A divisor that is the sum of all the matrix values tends to have an evening effect on the overall color intensity of the result."));
2957 _settings->add_spinscale(0, SPAttr::BIAS, _("Bias:"), -10, 10, 0.1, 0.5, 2, _("This value is added to each component. This is useful to define a constant value as the zero response of the filter."));
2958 _settings->add_combo(CONVOLVEMATRIX_EDGEMODE_NONE, SPAttr::EDGEMODE, _("Edge Mode:"), ConvolveMatrixEdgeModeConverter, _("Determines how to extend the input image as necessary with color values so that the matrix operations can be applied when the kernel is positioned at or near the edge of the input image."));
2959 _settings->add_checkbutton(false, SPAttr::PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive."));
2960
2962 _settings->add_color(/*default: white*/ 0xffffffff, SPAttr::LIGHTING_COLOR, _("Diffuse Color:"), _("Defines the color of the light source"));
2963 _settings->add_spinscale(1, SPAttr::SURFACESCALE, _("Surface Scale:"), -5, 5, 0.01, 0.001, 3, _("This value amplifies the heights of the bump map defined by the input alpha channel"));
2964 _settings->add_spinscale(1, SPAttr::DIFFUSECONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model."));
2965 // deprecated (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/kernelUnitLength)
2966 // _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1);
2967 _settings->add_lightsource();
2968
2970 _settings->add_spinscale(0, SPAttr::SCALE, _("Scale:"), 0, 100, 1, 0.01, 1, _("This defines the intensity of the displacement effect."));
2971 _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SPAttr::XCHANNELSELECTOR, _("X displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the X direction"));
2972 _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SPAttr::YCHANNELSELECTOR, _("Y displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the Y direction"));
2973
2975 _settings->add_color(/*default: black*/ 0, SPAttr::FLOOD_COLOR, _("Color:"), _("The whole filter region will be filled with this color."));
2976 _settings->add_spinscale(1, SPAttr::FLOOD_OPACITY, _("Opacity:"), 0, 1, 0.1, 0.01, 2);
2977
2979 _settings->add_dualspinscale(SPAttr::STDDEVIATION, _("Size:"), 0, 100, 1, 0.01, 2, _("The standard deviation for the blur operation."));
2980
2982 _settings->add_no_params();
2983
2985 _settings->add_combo(MORPHOLOGY_OPERATOR_ERODE, SPAttr::OPERATOR, _("Operator:"), MorphologyOperatorConverter, _("Erode: performs \"thinning\" of input image.\nDilate: performs \"fattening\" of input image."));
2986 _settings->add_dualspinscale(SPAttr::RADIUS, _("Radius:"), 0, 100, 1, 0.01, 1);
2987
2989 _settings->add_fileorelement(SPAttr::XLINK_HREF, _("Source of Image:"));
2990 _image_x = _settings->add_entry(SPAttr::X, _("Position X:"), _("Position X"));
2991 _image_x->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_x_changed));
2992 //This is commented out because we want the default empty value of X or Y and couldn't get it from SpinButton
2993 //_image_y = _settings->add_spinbutton(0, SPAttr::Y, _("Y:"), -DBL_MAX, DBL_MAX, 1, 1, 5, _("Y"));
2994 _image_y = _settings->add_entry(SPAttr::Y, _("Position Y:"), _("Position Y"));
2995 _image_y->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_y_changed));
2996 _settings->add_entry(SPAttr::WIDTH, _("Width:"), _("Width"));
2997 _settings->add_entry(SPAttr::HEIGHT, _("Height:"), _("Height"));
2998
3000 _settings->add_checkbutton(false, SPAttr::PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive."));
3001 _settings->add_spinscale(0, SPAttr::DX, _("Delta X:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted to the right"));
3002 _settings->add_spinscale(0, SPAttr::DY, _("Delta Y:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted downwards"));
3003
3005 _settings->add_color(/*default: white*/ 0xffffffff, SPAttr::LIGHTING_COLOR, _("Specular Color:"), _("Defines the color of the light source"));
3006 _settings->add_spinscale(1, SPAttr::SURFACESCALE, _("Surface Scale:"), -5, 5, 0.1, 0.01, 2, _("This value amplifies the heights of the bump map defined by the input alpha channel"));
3007 _settings->add_spinscale(1, SPAttr::SPECULARCONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model."));
3008 _settings->add_spinscale(1, SPAttr::SPECULAREXPONENT, _("Exponent:"), 1, 50, 1, 0.01, 1, _("Exponent for specular term, larger is more \"shiny\"."));
3009 // deprecated (https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/kernelUnitLength)
3010 // _settings->add_dualspinscale(SPAttr::KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1);
3011 _settings->add_lightsource();
3012
3014 // add some filter primitive attributes: https://drafts.fxtf.org/filter-effects/#feTileElement
3015 // issue: https://gitlab.com/inkscape/inkscape/-/issues/1417
3016 _settings->add_entry(SPAttr::X, _("Position X:"), _("Position X"));
3017 _settings->add_entry(SPAttr::Y, _("Position Y:"), _("Position Y"));
3018 _settings->add_entry(SPAttr::WIDTH, _("Width:"), _("Width"));
3019 _settings->add_entry(SPAttr::HEIGHT, _("Height:"), _("Height"));
3020
3022// _settings->add_checkbutton(false, SPAttr::STITCHTILES, _("Stitch Tiles"), "stitch", "noStitch");
3023 _settings->add_combo(TURBULENCE_TURBULENCE, SPAttr::TYPE, _("Type:"), TurbulenceTypeConverter, _("Indicates whether the filter primitive should perform a noise or turbulence function."));
3024 _settings->add_dualspinscale(SPAttr::BASEFREQUENCY, _("Size:"), 0.001, 10, 0.001, 0.1, 3);
3025 _settings->add_spinscale(1, SPAttr::NUMOCTAVES, _("Detail:"), 1, 10, 1, 1, 0);
3026 _settings->add_spinscale(0, SPAttr::SEED, _("Seed:"), 0, 1000, 1, 1, 0, _("The starting number for the pseudo random number generator."));
3027}
3028
3030 if (auto filter = _filter_modifier.get_selected_filter()) {
3031 SPFilterPrimitive* prim = filter_add_primitive(filter, type);
3032 _primitive_list.select(prim);
3033 DocumentUndo::done(filter->document, _("Add filter primitive"), INKSCAPE_ICON("dialog-filters"));
3034 }
3035}
3036
3038{
3040 if (id.has_value()) {
3042 }
3043}
3044
3046{
3049
3050 if (filter && origprim) {
3051 Inkscape::XML::Node *repr;
3052 repr = origprim->getRepr()->duplicate(origprim->getRepr()->document());
3053 filter->getRepr()->appendChild(repr);
3054
3055 DocumentUndo::done(filter->document, _("Duplicate filter primitive"), INKSCAPE_ICON("dialog-filters"));
3056
3058 }
3059}
3060
3062{
3063 _convolve_matrix->set_from_attribute(_primitive_list.get_selected());
3064 // MultiSpinButtons orders widgets backwards: so use index 1 and 0
3065 _convolve_target->get_spinbuttons()[1]->get_adjustment()->set_upper(_convolve_order->get_spinbutton1().get_value() - 1);
3066 _convolve_target->get_spinbuttons()[0]->get_adjustment()->set_upper(_convolve_order->get_spinbutton2().get_value() - 1);
3067}
3068
3069bool number_or_empy(const Glib::ustring& text) {
3070 if (text.empty()) {
3071 return true;
3072 }
3073 double n = g_strtod(text.c_str(), nullptr);
3074 if (n == 0.0 && strcmp(text.c_str(), "0") != 0 && strcmp(text.c_str(), "0.0") != 0) {
3075 return false;
3076 }
3077 else {
3078 return true;
3079 }
3080}
3081
3083{
3084 if (number_or_empy(_image_x->get_text())) {
3085 _image_x->set_from_attribute(_primitive_list.get_selected());
3086 }
3087}
3088
3090{
3091 if (number_or_empy(_image_y->get_text())) {
3092 _image_y->set_from_attribute(_primitive_list.get_selected());
3093 }
3094}
3095
3100
3102{
3103 if(!_locked) {
3104 _attr_lock = true;
3106 const gchar* name = (const gchar*)sp_attribute_name(input->get_attribute());
3107 if (filter && name && filter->getRepr()){
3109 filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
3110 }
3111 _attr_lock = false;
3112 }
3113}
3114
3119
3120void FilterEffectsDialog::set_attr(SPObject* o, const SPAttr attr, const gchar* val)
3121{
3122 if(!_locked) {
3123 _attr_lock = true;
3124
3126 const gchar* name = (const gchar*)sp_attribute_name(attr);
3127 if(filter && name && o) {
3129
3130 o->setAttribute(name, val);
3131 filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
3132
3133 Glib::ustring undokey = "filtereffects:";
3134 undokey += name;
3135 DocumentUndo::maybeDone(filter->document, undokey.c_str(), _("Set filter primitive attribute"), INKSCAPE_ICON("dialog-filters"));
3136 }
3137
3138 _attr_lock = false;
3139 }
3140}
3141
3143{
3144 if(_settings_initialized != true) return;
3145
3146 if(!_locked) {
3147 _attr_lock = true;
3148
3150
3151 if(filter) {
3152 _filter_general_settings->show_and_update(0, filter);
3153 _no_filter_selected.set_visible(false);
3154 } else {
3155 UI::get_children(_settings_filter).at(0)->set_visible(false);
3156 _no_filter_selected.set_visible(true);
3157 }
3158
3159 _attr_lock = false;
3160 }
3161}
3162
3164{
3166
3167 if (_attr_lock)
3168 return;
3169
3170 // selected effect parameters
3171
3172 for (auto const i : UI::get_children(_settings_effect)) {
3173 i->set_visible(false);
3174 }
3175
3177 auto& header = get_widget<Gtk::Box>(_builder, "effect-header");
3179 bool present = _filter_modifier.filters_present();
3180
3181 if (prim && prim->getRepr()) {
3182 //XML Tree being used directly here while it shouldn't be.
3183 auto id = FPConverter.get_id_from_key(prim->getRepr()->name());
3184 _settings->show_and_update(id, prim);
3185 _empty_settings.set_visible(false);
3186 _cur_effect_name->set_text(_(FPConverter.get_label(id).c_str()));
3187 header.set_visible(true);
3188 }
3189 else {
3190 if (filter) {
3191 _empty_settings.set_text(_("Add effect from the search bar"));
3192 }
3193 else if (present) {
3194 _empty_settings.set_text(_("Select a filter"));
3195 }
3196 else {
3197 _empty_settings.set_text(_("No filters in the document"));
3198 }
3199 _empty_settings.set_visible(true);
3200 _cur_effect_name->set_text(Glib::ustring());
3201 header.set_visible(false);
3202 }
3203
3204 // current filter parameters (area size)
3205
3206 UI::get_children(_settings_filter).at(0)->set_visible(false);
3207 _no_filter_selected.set_visible(true);
3208
3209 if (filter) {
3210 _filter_general_settings->show_and_update(0, filter);
3211 _no_filter_selected.set_visible(false);
3212 }
3213}
3214
3216{
3218 const bool use_k = is<SPFeComposite>(prim) && cast<SPFeComposite>(prim)->get_composite_operator() == COMPOSITE_ARITHMETIC;
3219 _k1->set_sensitive(use_k);
3220 _k2->set_sensitive(use_k);
3221 _k3->set_sensitive(use_k);
3222 _k4->set_sensitive(use_k);
3223}
3224
3229
3231{
3232 bool automatic = btn->get_active();
3233 _region_pos->set_sensitive(!automatic);
3234 _region_size->set_sensitive(!automatic);
3235}
3236
3237} // namespace Inkscape::UI::Dialog
3238
3239/*
3240 Local Variables:
3241 mode:c++
3242 c-file-style:"stroustrup"
3243 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
3244 indent-tabs-mode:nil
3245 fill-column:99
3246 End:
3247*/
3248// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
gchar const * sp_attribute_name(SPAttr id)
Get attribute name by id.
SPAttr
Definition attributes.h:27
@ KERNELMATRIX
@ FLOOD_COLOR
@ TABLEVALUES
@ DIFFUSECONSTANT
@ XCHANNELSELECTOR
@ XLINK_HREF
@ STDDEVIATION
@ SPECULARCONSTANT
@ FLOOD_OPACITY
@ YCHANNELSELECTOR
@ LIMITINGCONEANGLE
@ SURFACESCALE
@ SPECULAREXPONENT
@ PRESERVEALPHA
@ AUTO_REGION
@ INVALID
Must have value 0.
@ LIGHTING_COLOR
@ BASEFREQUENCY
@ NUMOCTAVES
uint32_t Color
SVG blend filter effect.
Gtk builder utilities.
Cairo::RefPtr< Cairo::ImageSurface > surface
Definition canvas.cpp:137
std::string toString(bool opacity=true) const
Format the color as a css string and return it.
Definition color.cpp:106
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
static void maybeDone(SPDocument *document, const gchar *keyconst, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
SPGroup * currentRoot() const
Returns current root (=bottom) layer.
SPItemRange items()
Returns a range of selected SPItems.
Definition object-set.h:255
boost::enable_if< boost::is_base_of< SPObject, T >, void >::type setList(const std::vector< T * > &objs)
Selects exactly the specified objects.
Definition object-set.h:299
XMLNodeRange xmlNodes()
Returns a range of the xml nodes of all selected objects.
Definition object-set.h:274
bool isEmpty()
Returns true if no items are selected.
Preference storage class.
Definition preferences.h:66
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
void setString(Glib::ustring const &pref_path, Glib::ustring const &value)
Set an UTF-8 string value.
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.
std::string str() const
The set of selected SPObjects for a given document and layer model.
Definition selection.h:80
DialogBase is the base class for the dialog system.
Definition dialog-base.h:42
virtual void update()
The update() method is essential to Gtk state management.
Definition dialog-base.h:57
SPDocument * getDocument() const
Definition dialog-base.h:83
void get_preferred_width_vfunc(Gtk::Widget &widget, int &minimum_width, int &natural_width) const override
void get_preferred_height_for_width_vfunc(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const override
void get_preferred_width_for_height_vfunc(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const override
void get_preferred_height_vfunc(Gtk::Widget &widget, int &minimum_height, int &natural_height) const override
void on_name_edited(const Glib::ustring &, const Glib::ustring &)
FilterModifier(FilterEffectsDialog &d, Glib::RefPtr< Gtk::Builder > builder)
std::unique_ptr< Inkscape::XML::SignalObserver > _observer
void selection_toggled(Gtk::TreeModel::iterator iter, bool toggle)
Gtk::EventSequenceState filter_list_click_released(Gtk::GestureClick const &click, int n_press, double x, double y)
std::unique_ptr< UI::Widget::PopoverMenu > create_menu()
void css_changed(GtkCssStyleChange *change) override
Called after gtk_widget_css_changed(): when a CSS widget node is validated & style changed.
void set_menu(sigc::slot< void()> dup, sigc::slot< void()> rem)
bool do_connection_node(const Gtk::TreeModel::iterator &row, const int input, std::vector< Geom::Point > &points, const int ix, const int iy)
void on_drag_end(Glib::RefPtr< Gdk::Drag > const &drag, bool delete_data)
void sanitize_connections(const Gtk::TreeModel::iterator &prim_iter)
std::unique_ptr< Inkscape::XML::SignalObserver > _observer
Gtk::EventSequenceState on_click_released(Gtk::GestureClick const &click, int n_press, double x, double y)
Gtk::EventSequenceState on_click_pressed(Gtk::GestureClick const &click, int n_press, double x, double y)
const Gtk::TreeModel::iterator find_result(const Gtk::TreeModel::iterator &start, const SPAttr attr, int &src_id, const int pos)
void draw_connection(const Cairo::RefPtr< Cairo::Context > &cr, const Gtk::TreeModel::iterator &, const SPAttr attr, const int text_start_x, const int x1, const int y1, const int row_count, const int pos, const Gdk::RGBA fg_color, const Gdk::RGBA mid_color)
void snapshot_vfunc(Glib::RefPtr< Gtk::Snapshot > const &snapshot) override
void set_attr(SPObject *, const SPAttr, const gchar *val)
std::unique_ptr< Settings > _filter_general_settings
void selectionModified(Inkscape::Selection *selection, guint flags) override
void set_filternode_attr(const UI::Widget::AttrWidget *)
void set_attr_direct(const UI::Widget::AttrWidget *)
void add_filter_primitive(Filters::FilterPrimitiveType type)
void set_child_attr_direct(const UI::Widget::AttrWidget *)
UI::Widget::ComboBoxEnum< Inkscape::Filters::FilterPrimitiveType > _add_primitive_type
void selectionChanged(Inkscape::Selection *selection) override
void add_effects(Inkscape::UI::Widget::CompletionPopup &popup, bool symbolic)
Inkscape::UI::Widget::CompletionPopup _effects_popup
virtual Glib::ustring get_as_attribute() const =0
DefaultValueHolder * get_default()
AttrWidget(const SPAttr a, unsigned int value)
const gchar * attribute_value(SPObject *o) const
sigc::signal< void()> & signal_attr_changed()
ColorPicker(Glib::ustring title, Glib::ustring const &tip, Colors::Color const &initial, bool undo, bool use_transparency=true)
Colors::Color get_current_color() const
sigc::connection connectChanged(sigc::slot< void(Colors::Color const &)> slot)
void setColor(Colors::Color const &)
Simplified management of enumerations in the UI as combobox.
Definition combo-enums.h:37
std::optional< E > get_selected_id() const
ComboBoxEnum(T const default_value, const Util::EnumDataConverter< T > &c, SPAttr const a=SPAttr::INVALID, bool const sort=true, const char *translation_context=nullptr)
Definition combo-enums.h:39
void add_to_completion_list(int id, Glib::ustring name, Glib::ustring icon_name, Glib::ustring search_text={})
sigc::signal< void(int)> & on_match_selected()
Contains two SpinScales for controlling number-opt-number attributes.
Definition spin-scale.h:68
SpinButton widget, that allows entry of simple math expressions (also units, when linked with UnitMen...
Definition spinbutton.h:52
Wrap the InkSpinScale class and attach an attribute.
Definition spin-scale.h:33
A class you can inherit to access GTK4ʼs Widget.css_changed & .focus vfuncs, missing in gtkmm4.
Simplified management of enumerations of svg items with UI labels.
Definition enums.h:42
E get_id_from_key(const Glib::ustring &key) const
Definition enums.h:60
const Glib::ustring & get_label(const E id) const
Definition enums.h:89
Interface for refcounted XML nodes.
Definition node.h:80
virtual Node * parent()=0
Get the parent of this node.
virtual void setPosition(int pos)=0
Set the position of this node in parent's child order.
virtual void appendChild(Node *child)=0
Append a node as the last child of this node.
virtual char const * name() const =0
Get the name of the element node.
void setAttributeOrRemoveIfEmpty(Inkscape::Util::const_char_ptr key, Inkscape::Util::const_char_ptr value)
Change an attribute of this node.
Definition node.cpp:167
void setAttribute(Util::const_char_ptr key, Util::const_char_ptr value)
Change an attribute of this node.
Definition node.cpp:25
virtual Node * duplicate(Document *doc) const =0
Create a duplicate of this node.
virtual char const * attribute(char const *key) const =0
Get the string representation of a node's attribute.
virtual bool matchAttributeName(char const *partial_name) const =0
Check whether this node has any attribute that matches a string.
virtual Document * document()=0
Get the node's associated document.
To do: update description of desktop.
Definition desktop.h:149
SPDocument * getDocument() const
Definition desktop.h:189
Inkscape::Selection * getSelection() const
Definition desktop.h:188
Inkscape::LayerManager & layerManager()
Definition desktop.h:287
Typed SVG document implementation.
Definition document.h:101
sigc::connection connectResourcesChanged(char const *key, SPDocument::ResourcesChangedSignal::slot_type slot)
std::vector< SPObject * > const getResourceList(char const *key)
Inkscape::XML::Document * getReprDoc()
Our Inkscape::XML::Document.
Definition document.h:211
SPObject * getObjectByRepr(Inkscape::XML::Node *repr) const
void set(SPAttr key, char const *value) override
SPFilter * getObject() const
unsigned getRefCount()
Returns the number of references to the filter.
bool valid_for(SPObject const *obj) const
Checks each filter primitive to make sure the object won't cause issues.
Filter type internal to SPStyle.
SPFilterReference * href
Base class for visual SVG elements.
Definition sp-item.h:109
SPObject is an abstract base class of all of the document nodes at the SVG document level.
Definition sp-object.h:160
char const * label() const
Gets the author-visible label property for the object or a default if no label is defined.
void setAttributeOrRemoveIfEmpty(Inkscape::Util::const_char_ptr key, Inkscape::Util::const_char_ptr value)
Definition sp-object.h:785
void setAttribute(Inkscape::Util::const_char_ptr key, Inkscape::Util::const_char_ptr value)
void requestModified(unsigned int flags)
Requests that a modification notification signal be emitted later (e.g.
void removeAttribute(char const *key)
SPDocument * document
Definition sp-object.h:188
SPObject * firstChild()
Definition sp-object.h:315
char const * getId() const
Returns the objects current ID string.
SPStyle * style
Represents the style properties, whether from presentation attributes, the style attribute,...
Definition sp-object.h:248
SPObject * parent
Definition sp-object.h:189
void setLabel(char const *label)
Sets the author-visible label for this object.
Inkscape::XML::Node * getRepr()
Returns the XML representation of tree.
ChildrenList children
Definition sp-object.h:907
void requestDisplayUpdate(unsigned int flags)
Queues an deferred update of this object's display.
An SVG style object.
Definition style.h:45
SPFilter * getFilter()
Definition style.h:335
T< SPAttr::FILTER, SPIFilter > filter
Filter effect.
Definition style.h:275
Color picker button and window.
SVG color matrix filter effect.
SVG <filter> implementation, see sp-filter.cpp.
SVG component transferfilter effect.
@ COMPOSITE_ARITHMETIC
Definition composite.h:28
@ COMPOSITE_OVER
Definition composite.h:23
const double w
Definition conic-4.cpp:19
Utilities to more easily use Gtk::EventController & subclasses like Gesture.
SVG matrix convolution filter effect.
Css & result
bool sp_query_custom_tooltip(Gtk::Widget *widg, int x, int y, bool keyboard_tooltip, const Glib::RefPtr< Gtk::Tooltip > &tooltipw, gint id, Glib::ustring tooltip, Glib::ustring icon, Gtk::IconSize iconsize, int delaytime)
double c[8][4]
Editable view implementation.
static char const *const parent
Definition dir-util.cpp:70
@ DISPLACEMENTMAP_CHANNEL_ALPHA
SVG <filter> implementation, see sp-filter.cpp.
TODO: insert short description here.
void remove_filter(SPObject *item, bool recursive)
SPFilterPrimitive * filter_add_primitive(SPFilter *filter, const Inkscape::Filters::FilterPrimitiveType type)
SPFilter * new_filter(SPDocument *document)
Filter Effects dialog.
const EnumDataConverter< FeCompositeOperator > CompositeOperatorConverter(CompositeOperatorData, COMPOSITE_ENDOPERATOR)
const EnumDataConverter< LightSource > LightSourceConverter(LightSourceData, LIGHT_ENDSOURCE)
const EnumDataConverter< Inkscape::Filters::FilterComponentTransferType > ComponentTransferTypeConverter(ComponentTransferTypeData, Inkscape::Filters::COMPONENTTRANSFER_TYPE_ERROR)
const EnumDataConverter< FilterDisplacementMapChannelSelector > DisplacementMapChannelConverter(DisplacementMapChannelData, DISPLACEMENTMAP_CHANNEL_ENDTYPE)
const EnumDataConverter< Inkscape::Filters::FilterConvolveMatrixEdgeMode > ConvolveMatrixEdgeModeConverter(ConvolveMatrixEdgeModeData, Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_ENDTYPE)
const EnumDataConverter< FilterPrimitiveInput > FPInputConverter(FPInputData, FPINPUT_END)
const EnumDataConverter< Inkscape::Filters::FilterColorMatrixType > ColorMatrixTypeConverter(ColorMatrixTypeData, Inkscape::Filters::COLORMATRIX_ENDTYPE)
const EnumDataConverter< Inkscape::Filters::FilterTurbulenceType > TurbulenceTypeConverter(TurbulenceTypeData, Inkscape::Filters::TURBULENCE_ENDTYPE)
const EnumDataConverter< Inkscape::Filters::FilterPrimitiveType > FPConverter(FPData, Inkscape::Filters::NR_FILTER_ENDPRIMITIVETYPE)
const EnumDataConverter< Inkscape::Filters::FilterMorphologyOperator > MorphologyOperatorConverter(MorphologyOperatorData, Inkscape::Filters::MORPHOLOGY_OPERATOR_END)
FilterPrimitiveInput
@ LIGHT_ENDSOURCE
@ LIGHT_DISTANT
@ LIGHT_SPOT
@ LIGHT_POINT
Util::TreeifyResult const & _tree
Macro for icon names used in Inkscape.
std::unique_ptr< Magick::Image > image
SPItem * item
Inkscape::XML::Node * node
Inkscape - An SVG editor.
Glib::ustring label
SVG merge filter effect.
feMergeNode implementation.
size_t v
Align
Values for the <align> parameter of preserveAspectRatio.
Definition rect.h:51
Definition desktop.h:50
static R & release(R &r)
Decrements the reference count of a anchored object.
auto use_state(Slot &&slot)
Definition controller.h:43
Dialog code.
Definition desktop.h:117
const Glib::ustring & get_category_name(EffectCategory category)
static auto get_cell_area(Gtk::TreeView const &tree_view, Gtk::TreeModel::Path const &path, Gtk::TreeViewColumn &column)
Definition objects.cpp:1602
void get_start_directory(std::string &start_path, Glib::ustring const &prefs_path, bool try_document_dir)
Find the start directory for a file dialog.
Glib::RefPtr< Gio::ListStore< Gtk::FileFilter > > create_open_filters()
Create a Gtk::FileFilter for all image file types.
constexpr int max_convolution_kernel_size
static Glib::ustring get_filter_name(SPFilter *filter)
static std::pair< int, int > widget_to_bin_window(Gtk::TreeView const &tree_view, int const wx, int const wy)
void draw_connection_node(const Cairo::RefPtr< Cairo::Context > &cr, std::vector< Geom::Point > const &points, Gdk::RGBA const &fill, Gdk::RGBA const &stroke)
static std::unique_ptr< UI::Widget::PopoverMenu > create_popup_menu(Gtk::Widget &parent, sigc::slot< void()> dup, sigc::slot< void()> rem)
static int input_count(const SPFilterPrimitive *prim)
static void check_single_connection(SPFilterPrimitive *prim, const int result)
static const std::map< Inkscape::Filters::FilterPrimitiveType, EffectMetadata > & get_effects()
static Glib::ustring const prefs_path
static void autoscroll(Glib::RefPtr< Gtk::Adjustment > const &a, double const delta)
bool number_or_empy(const Glib::ustring &text)
static constexpr int height
static char const * get_text(Gtk::Editable const &editable)
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
std::vector< Gtk::Widget * > get_children(Gtk::Widget &widget)
Get a vector of the widgetʼs children, from get_first_child() through each get_next_sibling().
Definition util.cpp:156
W & get_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id)
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
Glib::RefPtr< Gtk::Builder > create_builder(const char *filename)
GType type()
Returns the type used for storing an object of type T inside a value.
Definition value-utils.h:29
Helper class to stream background task notifications as a series of messages.
static void append(std::vector< T > &target, std::vector< T > &&source)
const Util::EnumDataConverter< SPBlendMode > SPBlendModeConverter
Glib::RefPtr< Gio::File > choose_file_open(Glib::ustring const &title, Gtk::Window *parent, Glib::RefPtr< Gio::ListStore< Gtk::FileFilter > > const &filters_model, std::string &current_folder, Glib::ustring const &accept)
Synchronously run a Gtk::FileDialog to open a single file for reading data.
int n
Definition spiro.cpp:66
STL namespace.
TODO: insert short description here.
Helpers for using Gtk::Boxes, encapsulating large changes between GTK3 & GTK4.
bool used
SVG <filter> implementation, see sp-filter.cpp.
A replacement for GTK3ʼs Gtk::MenuItem, as removed in GTK4.
A replacement for GTK3ʼs Gtk::Menu, as removed in GTK4.
Singleton class to access the preferences file in a convenient way.
unsigned long mi
Definition quantize.cpp:40
Ocnode * child[8]
Definition quantize.cpp:33
void sp_repr_unparent(Inkscape::XML::Node *repr)
Remove repr from children of its parent node.
Definition repr.h:107
GList * items
std::vector< SPItem * > get_all_items(SPObject *from, SPDesktop *desktop, bool onlyvisible, bool onlysensitive, bool ingroups, std::vector< SPItem * > const &exclude)
Derived from and replaces SpinSlider.
SVG <filter> implementation, see sp-filter.cpp.
Interface for XML documents.
Definition document.h:43
virtual Node * createElement(char const *name)=0
@ SP_CSS_BLEND_NORMAL
void sp_style_set_property_url(SPObject *item, gchar const *property, SPObject *linked, bool recursive)
Definition style.cpp:1380
SPStyle - a style object for SPItem objects.
int delta
int index
double width
Glib::ustring name
Definition toolbars.cpp:55
Glib::RefPtr< Gtk::Builder > builder
Gdk::RGBA get_color_with_class(Gtk::Widget &widget, Glib::ustring const &css_class)
Definition util.cpp:303
Gdk::RGBA change_alpha(const Gdk::RGBA &color, double new_alpha)
Definition util.cpp:417
Gdk::RGBA mix_colors(const Gdk::RGBA &a, const Gdk::RGBA &b, float ratio)
Definition util.cpp:283