Code

* implemented feImage Settings UI
[inkscape.git] / src / ui / dialog / filter-effects-dialog.cpp
1 /**
2  * \brief Filter Effects dialog
3  *
4  * Authors:
5  *   Nicholas Bishop <nicholasbishop@gmail.org>
6  *
7  * Copyright (C) 2007 Authors
8  *
9  * Released under GNU GPL.  Read the file 'COPYING' for more information.
10  */
12 #ifdef HAVE_CONFIG_H
13 # include <config.h>
14 #endif
16 #include <gtk/gtktreeview.h>
17 #include <gtkmm/cellrenderertext.h>
18 #include <gtkmm/colorbutton.h>
19 #include <gtkmm/messagedialog.h>
20 #include <gtkmm/paned.h>
21 #include <gtkmm/scale.h>
22 #include <gtkmm/scrolledwindow.h>
23 #include <gtkmm/spinbutton.h>
24 #include <gtkmm/stock.h>
25 #include <glibmm/i18n.h>
27 #include "application/application.h"
28 #include "application/editor.h"
29 #include "desktop.h"
30 #include "desktop-handles.h"
31 #include "dialog-manager.h"
32 #include "dir-util.h"
33 #include "document.h"
34 #include "filter-chemistry.h"
35 #include "filter-effects-dialog.h"
36 #include "filter-enums.h"
37 #include "inkscape.h"
38 #include "path-prefix.h"
39 #include "prefs-utils.h"
40 #include "selection.h"
41 #include "sp-feblend.h"
42 #include "sp-fecolormatrix.h"
43 #include "sp-fecomponenttransfer.h"
44 #include "sp-fecomposite.h"
45 #include "sp-feconvolvematrix.h"
46 #include "sp-fedisplacementmap.h"
47 #include "sp-fedistantlight.h"
48 #include "sp-femerge.h"
49 #include "sp-femergenode.h"
50 #include "sp-feoffset.h"
51 #include "sp-fepointlight.h"
52 #include "sp-fespotlight.h"
53 #include "sp-filter-primitive.h"
54 #include "sp-gaussian-blur.h"
56 #include "style.h"
57 #include "svg/svg-color.h"
58 #include "ui/dialog/filedialog.h"
59 #include "verbs.h"
60 #include "xml/node.h"
61 #include "xml/node-observer.h"
62 #include "xml/repr.h"
63 #include <sstream>
65 #include "io/sys.h"
66 #include <iostream>
68 using namespace NR;
70 namespace Inkscape {
71 namespace UI {
72 namespace Dialog {
74 // Returns the number of inputs available for the filter primitive type
75 int input_count(const SPFilterPrimitive* prim)
76 {
77     if(!prim)
78         return 0;
79     else if(SP_IS_FEBLEND(prim) || SP_IS_FECOMPOSITE(prim) || SP_IS_FEDISPLACEMENTMAP(prim))
80         return 2;
81     else if(SP_IS_FEMERGE(prim)) {
82         // Return the number of feMergeNode connections plus an extra
83         int count = 1;
84         for(const SPObject* o = prim->firstChild(); o; o = o->next, ++count);
85         return count;
86     }
87     else
88         return 1;
89 }
91 // Very simple observer that just emits a signal if anything happens to a node
92 class FilterEffectsDialog::SignalObserver : public XML::NodeObserver
93 {
94 public:
95     SignalObserver()
96         : _oldsel(0)
97     {}
99     // Add this observer to the SPObject and remove it from any previous object
100     void set(SPObject* o)
101     {
102         if(_oldsel && _oldsel->repr)
103             _oldsel->repr->removeObserver(*this);
104         if(o && o->repr)
105             o->repr->addObserver(*this);
106         _oldsel = o;
107     }
109     void notifyChildAdded(XML::Node&, XML::Node&, XML::Node*)
110     { signal_changed()(); }
112     void notifyChildRemoved(XML::Node&, XML::Node&, XML::Node*)
113     { signal_changed()(); }
115     void notifyChildOrderChanged(XML::Node&, XML::Node&, XML::Node*, XML::Node*)
116     { signal_changed()(); }
118     void notifyContentChanged(XML::Node&, Util::ptr_shared<char>, Util::ptr_shared<char>)
119     {}
121     void notifyAttributeChanged(XML::Node&, GQuark, Util::ptr_shared<char>, Util::ptr_shared<char>)
122     { signal_changed()(); }
124     sigc::signal<void>& signal_changed()
125     {
126         return _signal_changed;
127     }
128 private:
129     sigc::signal<void> _signal_changed;
130     SPObject* _oldsel;
131 };
133 class CheckButtonAttr : public Gtk::CheckButton, public AttrWidget
135 public:
136     CheckButtonAttr(const Glib::ustring& label,
137                     const Glib::ustring& tv, const Glib::ustring& fv,
138                     const SPAttributeEnum a)
139         : Gtk::CheckButton(label),
140           AttrWidget(a),
141           _true_val(tv), _false_val(fv)
142     {
143         signal_toggled().connect(signal_attr_changed().make_slot());
144     }
146     Glib::ustring get_as_attribute() const
147     {
148         return get_active() ? _true_val : _false_val;
149     }
151     void set_from_attribute(SPObject* o)
152     {
153         const gchar* val = attribute_value(o);
154         if(val) {
155             if(_true_val == val)
156                 set_active(true);
157             else if(_false_val == val)
158                 set_active(false);
159         }
160     }
161 private:
162     const Glib::ustring _true_val, _false_val;
163 };
165 class SpinButtonAttr : public Gtk::SpinButton, public AttrWidget
167 public:
168     SpinButtonAttr(double lower, double upper, double step_inc,
169                    double climb_rate, int digits, const SPAttributeEnum a)
170         : Gtk::SpinButton(climb_rate, digits),
171           AttrWidget(a)
172     {
173         set_range(lower, upper);
174         set_increments(step_inc, step_inc * 5);
176         signal_value_changed().connect(signal_attr_changed().make_slot());
177     }
179     Glib::ustring get_as_attribute() const
180     {
181         const double val = get_value();
182         
183         if(get_digits() == 0)
184             return Glib::Ascii::dtostr((int)val);
185         else
186             return Glib::Ascii::dtostr(val);
187     }
188     
189     void set_from_attribute(SPObject* o)
190     {
191         const gchar* val = attribute_value(o);
192         if(val)
193             set_value(Glib::Ascii::strtod(val));
194     }
195 };
197 // Contains an arbitrary number of spin buttons that use seperate attributes
198 class MultiSpinButton : public Gtk::HBox
200 public:
201     MultiSpinButton(double lower, double upper, double step_inc,
202                     double climb_rate, int digits, std::vector<SPAttributeEnum> attrs)
203     {
204         for(unsigned i = 0; i < attrs.size(); ++i) {
205             _spins.push_back(new SpinButtonAttr(lower, upper, step_inc, climb_rate, digits, attrs[i]));
206             pack_start(*_spins.back(), false, false);
207         }
208     }
210     ~MultiSpinButton()
211     {
212         for(unsigned i = 0; i < _spins.size(); ++i)
213             delete _spins[i];
214     }
216     std::vector<SpinButtonAttr*>& get_spinbuttons()
217     {
218         return _spins;
219     }
220 private:
221     std::vector<SpinButtonAttr*> _spins;
222 };
224 // Contains two spinbuttons that describe a NumberOptNumber
225 class DualSpinButton : public Gtk::HBox, public AttrWidget
227 public:
228     DualSpinButton(double lower, double upper, double step_inc,
229                    double climb_rate, int digits, const SPAttributeEnum a)
230         : AttrWidget(a),
231           _s1(climb_rate, digits), _s2(climb_rate, digits)
232     {
233         _s1.set_range(lower, upper);
234         _s2.set_range(lower, upper);
235         _s1.set_increments(step_inc, step_inc * 5);
236         _s2.set_increments(step_inc, step_inc * 5);
238         _s1.signal_value_changed().connect(signal_attr_changed().make_slot());
239         _s2.signal_value_changed().connect(signal_attr_changed().make_slot());
241         pack_start(_s1, false, false);
242         pack_start(_s2, false, false);
243     }
245     Gtk::SpinButton& get_spinbutton1()
246     {
247         return _s1;
248     }
250     Gtk::SpinButton& get_spinbutton2()
251     {
252         return _s2;
253     }
255     virtual Glib::ustring get_as_attribute() const
256     {
257         double v1 = _s1.get_value();
258         double v2 = _s2.get_value();
259         
260         if(_s1.get_digits() == 0) {
261             v1 = (int)v1;
262             v2 = (int)v2;
263         }
265         return Glib::Ascii::dtostr(v1) + " " + Glib::Ascii::dtostr(v2);
266     }
268     virtual void set_from_attribute(SPObject* o)
269     {
270         const gchar* val = attribute_value(o);
271         if(val) {
272             NumberOptNumber n;
273             n.set(val);
274             _s1.set_value(n.getNumber());
275             _s2.set_value(n.getOptNumber());
276         }
277     }
278 private:
279     Gtk::SpinButton _s1, _s2;
280 };
282 class ColorButton : public Gtk::ColorButton, public AttrWidget
284 public:
285     ColorButton(const SPAttributeEnum a)
286         : AttrWidget(a)
287     {
288         signal_color_set().connect(signal_attr_changed().make_slot());
290         Gdk::Color col;
291         col.set_rgb(65535, 65535, 65535);
292         set_color(col);
293     }
294     
295     // Returns the color in 'rgb(r,g,b)' form.
296     Glib::ustring get_as_attribute() const
297     {
298         std::ostringstream os;
299         const Gdk::Color c = get_color();
300         const int r = c.get_red() / 257, g = c.get_green() / 257, b = c.get_blue() / 257;
301         os << "rgb(" << r << "," << g << "," << b << ")";
302         return os.str();
303     }
306     void set_from_attribute(SPObject* o)
307     {
308         const gchar* val = attribute_value(o);
309         if(val) {
310             const guint32 i = sp_svg_read_color(val, 0xFFFFFFFF);
311             const int r = SP_RGBA32_R_U(i), g = SP_RGBA32_G_U(i), b = SP_RGBA32_B_U(i);
312             Gdk::Color col;
313             col.set_rgb(r * 257, g * 257, b * 257);
314             set_color(col);
315         }
316     }
317 };
319 /* Displays/Edits the matrix for feConvolveMatrix or feColorMatrix */
320 class FilterEffectsDialog::MatrixAttr : public Gtk::Frame, public AttrWidget
322 public:
323     MatrixAttr(const SPAttributeEnum a)
324         : AttrWidget(a), _locked(false)
325     {
326         _model = Gtk::ListStore::create(_columns);
327         _tree.set_model(_model);
328         _tree.set_headers_visible(false);
329         _tree.show();
330         add(_tree);
331         set_shadow_type(Gtk::SHADOW_IN);
332     }
334     std::vector<double> get_values() const
335     {
336         std::vector<double> vec;
337         for(Gtk::TreeIter iter = _model->children().begin();
338             iter != _model->children().end(); ++iter) {
339             for(unsigned c = 0; c < _tree.get_columns().size(); ++c)
340                 vec.push_back((*iter)[_columns.cols[c]]);
341         }
342         return vec;
343     }
345     void set_values(const std::vector<double>& v)
346     {
347         unsigned i = 0;
348         for(Gtk::TreeIter iter = _model->children().begin();
349             iter != _model->children().end(); ++iter) {
350             for(unsigned c = 0; c < _tree.get_columns().size(); ++c) {
351                 if(i >= v.size())
352                     return;
353                 (*iter)[_columns.cols[c]] = v[i];
354                 ++i;
355             }
356         }
357     }
359     Glib::ustring get_as_attribute() const
360     {
361         std::ostringstream os;
362         
363         for(Gtk::TreeIter iter = _model->children().begin();
364             iter != _model->children().end(); ++iter) {
365             for(unsigned c = 0; c < _tree.get_columns().size(); ++c) {
366                 os << (*iter)[_columns.cols[c]] << " ";
367             }
368         }
369         
370         return os.str();
371     }
372     
373     void set_from_attribute(SPObject* o)
374     {
375         if(o) {
376             if(SP_IS_FECONVOLVEMATRIX(o)) {
377                 SPFeConvolveMatrix* conv = SP_FECONVOLVEMATRIX(o);
378                 int cols, rows;
379                 cols = (int)conv->order.getNumber();
380                 if(cols > 5)
381                     cols = 5;
382                 rows = conv->order.optNumber_set ? (int)conv->order.getOptNumber() : cols;
383                 update(o, rows, cols);
384             }
385             else if(SP_IS_FECOLORMATRIX(o))
386                 update(o, 4, 5);
387         }
388     }
389 private:
390     class MatrixColumns : public Gtk::TreeModel::ColumnRecord
391     {
392     public:
393         MatrixColumns()
394         {
395             cols.resize(5);
396             for(unsigned i = 0; i < cols.size(); ++i)
397                 add(cols[i]);
398         }
399         std::vector<Gtk::TreeModelColumn<double> > cols;
400     };
402     void update(SPObject* o, const int rows, const int cols)
403     {
404         if(_locked)
405             return;
407         _model->clear();
409         _tree.remove_all_columns();
411         std::vector<gdouble>* values = NULL;
412         if(SP_IS_FECOLORMATRIX(o))
413             values = &SP_FECOLORMATRIX(o)->values;
414         else if(SP_IS_FECONVOLVEMATRIX(o))
415             values = &SP_FECONVOLVEMATRIX(o)->kernelMatrix;
416         else
417             return;
419         if(o) {
420             int ndx = 0;
422             for(int i = 0; i < cols; ++i) {
423                 _tree.append_column_numeric_editable("", _columns.cols[i], "%.2f");
424                 dynamic_cast<Gtk::CellRendererText*>(
425                     _tree.get_column_cell_renderer(i))->signal_edited().connect(
426                         sigc::mem_fun(*this, &MatrixAttr::rebind));
427             }
429             for(int r = 0; r < rows; ++r) {
430                 Gtk::TreeRow row = *(_model->append());
431                 // Default to identity matrix
432                 for(int c = 0; c < cols; ++c, ++ndx)
433                     row[_columns.cols[c]] = ndx < (int)values->size() ? (*values)[ndx] : (r == c ? 1 : 0);
434             }
435         }
436     }
438     void rebind(const Glib::ustring&, const Glib::ustring&)
439     {
440         _locked = true;
441         signal_attr_changed()();
442         _locked = false;
443     }
445     bool _locked;
446     Gtk::TreeView _tree;
447     Glib::RefPtr<Gtk::ListStore> _model;
448     MatrixColumns _columns;
449 };
451 // Displays a matrix or a slider for feColorMatrix
452 class FilterEffectsDialog::ColorMatrixValues : public Gtk::Frame, public AttrWidget
454 public:
455     ColorMatrixValues()
456         : AttrWidget(SP_ATTR_VALUES),
457           _matrix(SP_ATTR_VALUES),
458           _saturation(0, 0, 1, 0.1, 0.01, 2, SP_ATTR_VALUES),
459           _angle(0, 0, 360, 0.1, 0.01, 1, SP_ATTR_VALUES),
460           _label(_("None"), Gtk::ALIGN_LEFT),
461           _use_stored(false),
462           _saturation_store(0),
463           _angle_store(0)
464     {
465         _matrix.signal_attr_changed().connect(signal_attr_changed().make_slot());
466         _saturation.signal_attr_changed().connect(signal_attr_changed().make_slot());
467         _angle.signal_attr_changed().connect(signal_attr_changed().make_slot());
468         signal_attr_changed().connect(sigc::mem_fun(*this, &ColorMatrixValues::update_store));
470         _matrix.show();
471         _saturation.show();
472         _angle.show();
473         _label.show();
474         _label.set_sensitive(false);
476         set_shadow_type(Gtk::SHADOW_NONE);
477     }
479     virtual void set_from_attribute(SPObject* o)
480     {
481         if(SP_IS_FECOLORMATRIX(o)) {
482             SPFeColorMatrix* col = SP_FECOLORMATRIX(o);
483             remove();
484             switch(col->type) {
485                 case COLORMATRIX_SATURATE:
486                     add(_saturation);
487                     if(_use_stored)
488                         _saturation.set_value(_saturation_store);
489                     else
490                         _saturation.set_from_attribute(o);
491                     break;
492                 case COLORMATRIX_HUEROTATE:
493                     add(_angle);
494                     if(_use_stored)
495                         _angle.set_value(_angle_store);
496                     else
497                         _angle.set_from_attribute(o);
498                     break;
499                 case COLORMATRIX_LUMINANCETOALPHA:
500                     add(_label);
501                     break;
502                 case COLORMATRIX_MATRIX:
503                 default:
504                     add(_matrix);
505                     if(_use_stored)
506                         _matrix.set_values(_matrix_store);
507                     else
508                         _matrix.set_from_attribute(o);
509                     break;
510             }
511             _use_stored = true;
512         }
513     }
515     virtual Glib::ustring get_as_attribute() const
516     {
517         const Widget* w = get_child();
518         if(w == &_label)
519             return "";
520         else
521             return dynamic_cast<const AttrWidget*>(w)->get_as_attribute();
522     }
524     void clear_store()
525     {
526         _use_stored = false;
527     }
528 private:
529     void update_store()
530     {
531         const Widget* w = get_child();
532         if(w == &_matrix)
533             _matrix_store = _matrix.get_values();
534         else if(w == &_saturation)
535             _saturation_store = _saturation.get_value();
536         else if(w == &_angle)
537             _angle_store = _angle.get_value();
538     }
540     MatrixAttr _matrix;
541     SpinSlider _saturation;
542     SpinSlider _angle;
543     Gtk::Label _label;
545     // Store separate values for the different color modes
546     bool _use_stored;
547     std::vector<double> _matrix_store;
548     double _saturation_store;
549     double _angle_store;
550 };
552 static Inkscape::UI::Dialog::FileOpenDialog * selectFeImageFileInstance = NULL;
554 //Displays a chooser for feImage input
555 //It may be a filename or the id for an SVG Element
556 //described in xlink:href syntax
557 class FileOrElementChooser : public Gtk::HBox, public AttrWidget
559 public:
560     FileOrElementChooser(const SPAttributeEnum a)
561         : AttrWidget(a)
562     {
563         pack_start(_entry, false, false);
564         pack_start(_fromFile, false, false);
565         //pack_start(_fromSVGElement, false, false);
567         _fromFile.set_label(_("Image File"));
568         _fromFile.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_file));
570         _fromSVGElement.set_label(_("Selected SVG Element"));        
571         _fromSVGElement.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_svg_element));
573         _entry.signal_changed().connect(signal_attr_changed().make_slot());
575         show_all();
577     }
578     
579     // Returns the element in xlink:href form.
580     Glib::ustring get_as_attribute() const
581     {
582         return _entry.get_text();
583     }
586     void set_from_attribute(SPObject* o)
587     {
588         const gchar* val = attribute_value(o);
589         if(val) {
590             _entry.set_text(val);
591         }
592     }
594     void set_desktop(SPDesktop* d){
595         _desktop = d;
596     }
598 private:
599     void select_svg_element(){
600         Inkscape::Selection* sel = sp_desktop_selection(_desktop);
601         if (sel->isEmpty()) return;
602         Inkscape::XML::Node* node = (Inkscape::XML::Node*) g_slist_nth_data((GSList *)sel->reprList(), 0);
603         if (!node || !node->matchAttributeName("id")) return;
604         
605         std::ostringstream xlikhref;
606         xlikhref << "#(" << node->attribute("id") << ")";
607         _entry.set_text(xlikhref.str());
608     }
609     
610     void select_file(){
612         //# Get the current directory for finding files
613         Glib::ustring open_path;
614         char *attr = (char *)prefs_get_string_attribute("dialogs.open", "path");
615         if (attr)
616             open_path = attr;
617     
618         //# Test if the open_path directory exists
619         if (!Inkscape::IO::file_test(open_path.c_str(),
620                   (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)))
621             open_path = "";
622     
623         //# If no open path, default to our home directory
624         if (open_path.size() < 1)
625             {
626             open_path = g_get_home_dir();
627             open_path.append(G_DIR_SEPARATOR_S);
628             }
629     
630         //# Create a dialog if we don't already have one
631         if (!selectFeImageFileInstance) {
632             selectFeImageFileInstance =
633                   Inkscape::UI::Dialog::FileOpenDialog::create(
634                      *_desktop->getToplevel(),
635                      open_path,
636                      Inkscape::UI::Dialog::SVG_TYPES,/*TODO: any image, not justy svg*/
637                      (char const *)_("Select an image to be used as feImage input"));
638         }
639     
640         //# Show the dialog
641         bool const success = selectFeImageFileInstance->show();
642         if (!success)
643             return;
645         //# User selected something.  Get name and type
646         Glib::ustring fileName = selectFeImageFileInstance->getFilename();
648         if (fileName.size() > 0) {
650             Glib::ustring newFileName = Glib::filename_to_utf8(fileName);
652             if ( newFileName.size() > 0)
653                 fileName = newFileName;
654             else
655                 g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" );
657             open_path = fileName;
658             open_path.append(G_DIR_SEPARATOR_S);
659             prefs_set_string_attribute("dialogs.open", "path", open_path.c_str());
661             _entry.set_text(fileName);
662         }
663         return;
664     }
665     
666     Gtk::Entry _entry;
667     Gtk::Button _fromFile;
668     Gtk::Button _fromSVGElement;
669     SPDesktop* _desktop;
670 };
672 class FilterEffectsDialog::Settings
674 public:
675     typedef sigc::slot<void, const AttrWidget*> SetAttrSlot;
677     Settings(FilterEffectsDialog& d, Gtk::Box& b, SetAttrSlot slot, const int maxtypes)
678         : _dialog(d), _set_attr_slot(slot), _current_type(-1), _max_types(maxtypes)
679     {
680         _groups.resize(_max_types);
681         _attrwidgets.resize(_max_types);
683         for(int i = 0; i < _max_types; ++i) {
684             _groups[i] = new Gtk::VBox;
685             b.add(*_groups[i]);
686         }
687     }
689     ~Settings()
690     {
691         for(int i = 0; i < _max_types; ++i) {
692             delete _groups[i];
693             for(unsigned j = 0; j < _attrwidgets[i].size(); ++j)
694                 delete _attrwidgets[i][j];
695         }
696     }
698     // Show the active settings group and update all the AttrWidgets with new values
699     void show_and_update(const int t, SPObject* ob)
700     {
701         if(t != _current_type) {
702             type(t);
703             for(unsigned i = 0; i < _groups.size(); ++i)
704                 _groups[i]->hide();
705         }
706         if(t >= 0)
707             _groups[t]->show_all();
709         _dialog.set_attrs_locked(true);
710         for(unsigned i = 0; i < _attrwidgets[_current_type].size(); ++i)
711             _attrwidgets[_current_type][i]->set_from_attribute(ob);
712         _dialog.set_attrs_locked(false);
713     }
715     int get_current_type() const
716     {
717         return _current_type;
718     }
720     void type(const int t)
721     {
722         _current_type = t;
723     }
725     void add_notimplemented()
726     {
727         Gtk::Label* lbl = Gtk::manage(new Gtk::Label("This SVG filter effect is not yet implemented in Inkscape."));
729         add_widget(lbl, "");
730     }
732     // LightSource
733     LightSourceControl* add_lightsource();
735     // CheckBox
736     CheckButtonAttr* add_checkbutton(const SPAttributeEnum attr, const Glib::ustring& label,
737                                      const Glib::ustring& tv, const Glib::ustring& fv)
738     {
739         CheckButtonAttr* cb = new CheckButtonAttr(label, tv, fv, attr);
740         add_widget(cb, "");
741         add_attr_widget(cb);
742         return cb;
743     }
745     // ColorButton
746     ColorButton* add_color(const SPAttributeEnum attr, const Glib::ustring& label)
747     {
748         ColorButton* col = new ColorButton(attr);
749         add_widget(col, label);
750         add_attr_widget(col);
751         return col;
752     }
754     // Matrix
755     MatrixAttr* add_matrix(const SPAttributeEnum attr, const Glib::ustring& label)
756     {
757         MatrixAttr* conv = new MatrixAttr(attr);
758         add_widget(conv, label);
759         add_attr_widget(conv);
760         return conv;
761     }
763     // ColorMatrixValues
764     ColorMatrixValues* add_colormatrixvalues(const Glib::ustring& label)
765     {
766         ColorMatrixValues* cmv = new ColorMatrixValues;
767         add_widget(cmv, label);
768         add_attr_widget(cmv);
769         return cmv;
770     }
772     // SpinSlider
773     SpinSlider* add_spinslider(const SPAttributeEnum attr, const Glib::ustring& label,
774                          const double lo, const double hi, const double step_inc, const double climb, const int digits)
775     {
776         SpinSlider* spinslider = new SpinSlider(lo, lo, hi, step_inc, climb, digits, attr);
777         add_widget(spinslider, label);
778         add_attr_widget(spinslider);
779         return spinslider;
780     }
782     // DualSpinSlider
783     DualSpinSlider* add_dualspinslider(const SPAttributeEnum attr, const Glib::ustring& label,
784                                        const double lo, const double hi, const double step_inc,
785                                        const double climb, const int digits)
786     {
787         DualSpinSlider* dss = new DualSpinSlider(lo, lo, hi, step_inc, climb, digits, attr);
788         add_widget(dss, label);
789         add_attr_widget(dss);
790         return dss;
791     }
793     // DualSpinButton
794     DualSpinButton* add_dualspinbutton(const SPAttributeEnum attr, const Glib::ustring& label,
795                                        const double lo, const double hi, const double step_inc,
796                                        const double climb, const int digits)
797     {
798         DualSpinButton* dsb = new DualSpinButton(lo, hi, step_inc, climb, digits, attr);
799         add_widget(dsb, label);
800         add_attr_widget(dsb);
801         return dsb;
802     }
803     
804     // MultiSpinButton
805     MultiSpinButton* add_multispinbutton(const SPAttributeEnum attr1, const SPAttributeEnum attr2,
806                                          const Glib::ustring& label, const double lo, const double hi,
807                                          const double step_inc, const double climb, const int digits)
808     {
809         std::vector<SPAttributeEnum> attrs;
810         attrs.push_back(attr1);
811         attrs.push_back(attr2);
812         MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs);
813         add_widget(msb, label);
814         for(unsigned i = 0; i < msb->get_spinbuttons().size(); ++i)
815             add_attr_widget(msb->get_spinbuttons()[i]);
816         return msb;
817     }
818     MultiSpinButton* add_multispinbutton(const SPAttributeEnum attr1, const SPAttributeEnum attr2,
819                                          const SPAttributeEnum attr3, const Glib::ustring& label, const double lo,
820                                          const double hi, const double step_inc, const double climb, const int digits)
821     {
822         std::vector<SPAttributeEnum> attrs;
823         attrs.push_back(attr1);
824         attrs.push_back(attr2);
825         attrs.push_back(attr3);
826         MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs);
827         add_widget(msb, label);
828         for(unsigned i = 0; i < msb->get_spinbuttons().size(); ++i)
829             add_attr_widget(msb->get_spinbuttons()[i]);
830         return msb;
831     }
832     
833     // FileOrElementChooser
834     FileOrElementChooser* add_fileorelement(const SPAttributeEnum attr, const Glib::ustring& label)
835     {
836         FileOrElementChooser* foech = new FileOrElementChooser(attr);
837         foech->set_desktop(_dialog.getDesktop());
838         add_widget(foech, label);
839         add_attr_widget(foech);
840         return foech;
841     }
843     // ComboBoxEnum
844     template<typename T> ComboBoxEnum<T>* add_combo(const SPAttributeEnum attr,
845                                   const Glib::ustring& label,
846                                   const Util::EnumDataConverter<T>& conv)
847     {
848         ComboBoxEnum<T>* combo = new ComboBoxEnum<T>(conv, attr);
849         add_widget(combo, label);
850         add_attr_widget(combo);
851         return combo;
852     }
853 private:
854     void add_attr_widget(AttrWidget* a)
855     {    
856         _attrwidgets[_current_type].push_back(a);
857         a->signal_attr_changed().connect(sigc::bind(_set_attr_slot, a));
858     }
860     /* Adds a new settings widget using the specified label. The label will be formatted with a colon
861        and all widgets within the setting group are aligned automatically. */
862     void add_widget(Gtk::Widget* w, const Glib::ustring& label)
863     {
864         Gtk::Label *lbl = 0;
865         Gtk::HBox *hb = Gtk::manage(new Gtk::HBox);
866         hb->set_spacing(12);
867         
868         if(label != "") {
869             lbl = Gtk::manage(new Gtk::Label(label + (label == "" ? "" : ":"), Gtk::ALIGN_LEFT));
870             hb->pack_start(*lbl, false, false);
871             _dialog._sizegroup->add_widget(*lbl);
872             lbl->show();
873         }
874         
875         hb->pack_start(*w);
876         _groups[_current_type]->pack_start(*hb);
877         hb->show();
878         w->show();
879     }
881     std::vector<Gtk::VBox*> _groups;
883     FilterEffectsDialog& _dialog;
884     SetAttrSlot _set_attr_slot;
885     std::vector<std::vector<AttrWidget*> > _attrwidgets;
886     int _current_type, _max_types;
887 };
889 // Settings for the three light source objects
890 class FilterEffectsDialog::LightSourceControl : public AttrWidget
892 public:
893     LightSourceControl(FilterEffectsDialog& d)
894         : AttrWidget(SP_ATTR_INVALID),
895           _dialog(d),
896           _settings(d, _box, sigc::mem_fun(_dialog, &FilterEffectsDialog::set_child_attr_direct), LIGHT_ENDSOURCE),
897           _light_label(_("Light Source:"), Gtk::ALIGN_LEFT),
898           _light_source(LightSourceConverter),
899           _locked(false)
900     {
901         _light_box.pack_start(_light_label, false, false);
902         _light_box.pack_start(_light_source);
903         _light_box.show_all();
904         _light_box.set_spacing(12);
905         _dialog._sizegroup->add_widget(_light_label);
907         _box.add(_light_box);
908         _box.reorder_child(_light_box, 0);
909         _light_source.signal_changed().connect(sigc::mem_fun(*this, &LightSourceControl::on_source_changed));
911         // FIXME: these range values are complete crap
913         _settings.type(LIGHT_DISTANT);
914         _settings.add_spinslider(SP_ATTR_AZIMUTH, _("Azimuth"), 0, 360, 1, 1, 0);
915         _settings.add_spinslider(SP_ATTR_ELEVATION, _("Elevation"), 0, 360, 1, 1, 0);
917         _settings.type(LIGHT_POINT);
918         _settings.add_multispinbutton(SP_ATTR_X, SP_ATTR_Y, SP_ATTR_Z, _("Location"), -99999, 99999, 1, 100, 0);
920         _settings.type(LIGHT_SPOT);
921         _settings.add_multispinbutton(SP_ATTR_X, SP_ATTR_Y, SP_ATTR_Z, _("Location"), -99999, 99999, 1, 100, 0);
922         _settings.add_multispinbutton(SP_ATTR_POINTSATX, SP_ATTR_POINTSATY, SP_ATTR_POINTSATZ,
923                                       _("Points At"), -99999, 99999, 1, 100, 0);
924         _settings.add_spinslider(SP_ATTR_SPECULAREXPONENT, _("Specular Exponent"), 1, 100, 1, 1, 0);
925         _settings.add_spinslider(SP_ATTR_LIMITINGCONEANGLE, _("Cone Angle"), 1, 100, 1, 1, 0);
926     }
928     Gtk::VBox& get_box()
929     {
930         return _box;
931     }
932 protected:
933     Glib::ustring get_as_attribute() const
934     {
935         return "";
936     }
937     void set_from_attribute(SPObject* o)
938     {
939         if(_locked)
940             return;
942         _locked = true;
944         SPObject* child = o->children;
945         
946         if(SP_IS_FEDISTANTLIGHT(child))
947             _light_source.set_active(0);
948         else if(SP_IS_FEPOINTLIGHT(child))
949             _light_source.set_active(1);
950         else if(SP_IS_FESPOTLIGHT(child))
951             _light_source.set_active(2);
952         else
953             _light_source.set_active(-1);
955         update();
957         _locked = false;
958     }
959 private:
960     void on_source_changed()
961     {
962         if(_locked)
963             return;
965         SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
966         if(prim) {
967             _locked = true;
969             SPObject* child = prim->children;
970             const int ls = _light_source.get_active_row_number();
971             // Check if the light source type has changed
972             if(!(ls == -1 && !child) &&
973                !(ls == 0 && SP_IS_FEDISTANTLIGHT(child)) &&
974                !(ls == 1 && SP_IS_FEPOINTLIGHT(child)) &&
975                !(ls == 2 && SP_IS_FESPOTLIGHT(child))) {
976                 if(child)
977                     sp_repr_unparent(child->repr);
979                 if(ls != -1) {
980                     Inkscape::XML::Document *xml_doc = sp_document_repr_doc(prim->document);
981                     Inkscape::XML::Node *repr = xml_doc->createElement(_light_source.get_active_data()->key.c_str());
982                     prim->repr->appendChild(repr);
983                 }
985                 sp_document_done(prim->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("New light source"));
986                 update();
987             }
989             _locked = false;
990         }
991     }
993     void update()
994     {
995         _box.hide_all();
996         _box.show();
997         _light_box.show_all();
998         
999         SPFilterPrimitive* prim = _dialog._primitive_list.get_selected();
1000         if(prim && prim->children)
1001             _settings.show_and_update(_light_source.get_active_data()->id, prim->children);
1002     }
1004     FilterEffectsDialog& _dialog;
1005     Gtk::VBox _box;
1006     Settings _settings;
1007     Gtk::HBox _light_box;
1008     Gtk::Label _light_label;
1009     ComboBoxEnum<LightSource> _light_source;
1010     bool _locked;
1011 };
1013 FilterEffectsDialog::LightSourceControl* FilterEffectsDialog::Settings::add_lightsource()
1015     LightSourceControl* ls = new LightSourceControl(_dialog);
1016     add_attr_widget(ls);
1017     add_widget(&ls->get_box(), "");
1018     return ls;
1021 Glib::RefPtr<Gtk::Menu> create_popup_menu(Gtk::Widget& parent, sigc::slot<void> dup,
1022                                           sigc::slot<void> rem)
1024     Glib::RefPtr<Gtk::Menu> menu(new Gtk::Menu);
1026     menu->items().push_back(Gtk::Menu_Helpers::MenuElem(_("_Duplicate"), dup));
1027     Gtk::MenuItem* mi = Gtk::manage(new Gtk::ImageMenuItem(Gtk::Stock::REMOVE));
1028     menu->append(*mi);
1029     mi->signal_activate().connect(rem);
1030     mi->show();
1031     menu->accelerate(parent);
1033     return menu;
1036 /*** FilterModifier ***/
1037 FilterEffectsDialog::FilterModifier::FilterModifier(FilterEffectsDialog& d)
1038     : _dialog(d), _add(Gtk::Stock::NEW), _observer(new SignalObserver)
1040     Gtk::ScrolledWindow* sw = Gtk::manage(new Gtk::ScrolledWindow);
1041     pack_start(*sw);
1042     pack_start(_add, false, false);
1043     sw->add(_list);
1045     _model = Gtk::ListStore::create(_columns);
1046     _list.set_model(_model);
1047     _cell_toggle.set_active(true);
1048     const int selcol = _list.append_column("", _cell_toggle);
1049     Gtk::TreeViewColumn* col = _list.get_column(selcol - 1);
1050     if(col)
1051        col->add_attribute(_cell_toggle.property_active(), _columns.sel);
1052     _list.append_column_editable(_("_Filter"), _columns.label);
1053     ((Gtk::CellRendererText*)_list.get_column(1)->get_first_cell_renderer())->
1054         signal_edited().connect(sigc::mem_fun(*this, &FilterEffectsDialog::FilterModifier::on_name_edited));
1056     sw->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
1057     sw->set_shadow_type(Gtk::SHADOW_IN);
1058     show_all_children();
1059     _add.signal_clicked().connect(sigc::mem_fun(*this, &FilterModifier::add_filter));
1060     _cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &FilterModifier::on_selection_toggled));
1061     _list.signal_button_release_event().connect_notify(
1062         sigc::mem_fun(*this, &FilterModifier::filter_list_button_release));
1063     _menu = create_popup_menu(*this, sigc::mem_fun(*this, &FilterModifier::duplicate_filter),
1064                               sigc::mem_fun(*this, &FilterModifier::remove_filter));
1065     _menu->items().push_back(Gtk::Menu_Helpers::MenuElem(
1066                                  _("R_ename"), sigc::mem_fun(*this, &FilterModifier::rename_filter)));   
1067     _menu->accelerate(*this);
1069     _list.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FilterModifier::on_filter_selection_changed));
1070     _observer->signal_changed().connect(signal_filter_changed().make_slot());
1071     g_signal_connect(G_OBJECT(INKSCAPE), "change_selection",
1072                      G_CALLBACK(&FilterModifier::on_inkscape_change_selection), this);
1074     g_signal_connect(G_OBJECT(INKSCAPE), "activate_desktop",
1075                      G_CALLBACK(&FilterModifier::on_activate_desktop), this);
1077     on_activate_desktop(INKSCAPE, d.getDesktop(), this);
1078     update_filters();
1081 FilterEffectsDialog::FilterModifier::~FilterModifier()
1083    _resource_changed.disconnect();
1084    _doc_replaced.disconnect();
1087 void FilterEffectsDialog::FilterModifier::on_activate_desktop(Application*, SPDesktop* desktop, FilterModifier* me)
1089     me->_doc_replaced.disconnect();
1090     me->_doc_replaced = desktop->connectDocumentReplaced(
1091         sigc::mem_fun(me, &FilterModifier::on_document_replaced));
1093     me->_resource_changed.disconnect();
1094     me->_resource_changed =
1095         sp_document_resources_changed_connect(sp_desktop_document(desktop), "filter",
1096                                               sigc::mem_fun(me, &FilterModifier::update_filters));
1098     me->_dialog.setDesktop(desktop);
1100     me->update_filters();
1104 // When the selection changes, show the active filter(s) in the dialog
1105 void FilterEffectsDialog::FilterModifier::on_inkscape_change_selection(Application */*inkscape*/,
1106                                                                        Selection *sel,
1107                                                                        FilterModifier* fm)
1109     if(fm && sel)
1110         fm->update_selection(sel);
1113 // Update each filter's sel property based on the current object selection;
1114 //  If the filter is not used by any selected object, sel = 0,
1115 //  otherwise sel is set to the total number of filters in use by selected objects
1116 //  If only one filter is in use, it is selected
1117 void FilterEffectsDialog::FilterModifier::update_selection(Selection *sel)
1119     std::set<SPObject*> used;
1121     for(GSList const *i = sel->itemList(); i != NULL; i = i->next) {
1122         SPObject *obj = SP_OBJECT (i->data);
1123         SPStyle *style = SP_OBJECT_STYLE (obj);
1124         if(!style || !SP_IS_ITEM(obj)) continue;
1126         if(style->filter.set && style->getFilter())
1127             used.insert(style->getFilter());
1128         else
1129             used.insert(0);
1130     }
1132     const int size = used.size();
1134     for(Gtk::TreeIter iter = _model->children().begin();
1135         iter != _model->children().end(); ++iter) {
1136         if(used.find((*iter)[_columns.filter]) != used.end()) {
1137             // If only one filter is in use by the selection, select it
1138             if(size == 1)
1139                 _list.get_selection()->select(iter);
1140             (*iter)[_columns.sel] = size;
1141         }
1142         else
1143             (*iter)[_columns.sel] = 0;
1144     }
1147 void FilterEffectsDialog::FilterModifier::on_filter_selection_changed()
1149     _observer->set(get_selected_filter());
1150     signal_filter_changed()();
1153 void FilterEffectsDialog::FilterModifier::on_name_edited(const Glib::ustring& path, const Glib::ustring& text)
1155     Gtk::TreeModel::iterator iter = _model->get_iter(path);
1156     
1157     if(iter) {
1158         SPFilter* filter = (*iter)[_columns.filter];
1159         filter->setLabel(text.c_str());
1160         sp_document_done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Rename filter"));
1161         if(iter)
1162             (*iter)[_columns.label] = text;
1163     }
1166 void FilterEffectsDialog::FilterModifier::on_selection_toggled(const Glib::ustring& path)
1168     Gtk::TreeIter iter = _model->get_iter(path);
1170     if(iter) {
1171         SPDesktop *desktop = _dialog.getDesktop();
1172         SPDocument *doc = sp_desktop_document(desktop);
1173         SPFilter* filter = (*iter)[_columns.filter];
1174         Inkscape::Selection *sel = sp_desktop_selection(desktop);
1176         /* If this filter is the only one used in the selection, unset it */
1177         if((*iter)[_columns.sel] == 1)
1178             filter = 0;
1180         GSList const *items = sel->itemList();
1181             
1182         for (GSList const *i = items; i != NULL; i = i->next) {
1183             SPItem * item = SP_ITEM(i->data);
1184             SPStyle *style = SP_OBJECT_STYLE(item);
1185             g_assert(style != NULL);
1186                 
1187             if(filter)
1188                 sp_style_set_property_url(SP_OBJECT(item), "filter", SP_OBJECT(filter), false);
1189             else
1190                 ::remove_filter(item, false);
1192             SP_OBJECT(item)->requestDisplayUpdate((SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG ));
1193         }
1194     
1195         update_selection(sel);
1196         sp_document_done(doc, SP_VERB_DIALOG_FILTER_EFFECTS,  _("Apply filter"));
1197     }
1200 /* Add all filters in the document to the combobox.
1201    Keeps the same selection if possible, otherwise selects the first element */
1202 void FilterEffectsDialog::FilterModifier::update_filters()
1204     SPDesktop* desktop = _dialog.getDesktop();
1205     SPDocument* document = sp_desktop_document(desktop);
1206     const GSList* filters = sp_document_get_resource_list(document, "filter");
1208     _model->clear();
1210     for(const GSList *l = filters; l; l = l->next) {
1211         Gtk::TreeModel::Row row = *_model->append();
1212         SPFilter* f = (SPFilter*)l->data;
1213         row[_columns.filter] = f;
1214         const gchar* lbl = f->label();
1215         const gchar* id = SP_OBJECT_ID(f);
1216         row[_columns.label] = lbl ? lbl : (id ? id : "filter");
1217     }
1219     update_selection(desktop->selection);
1222 SPFilter* FilterEffectsDialog::FilterModifier::get_selected_filter()
1224     if(_list.get_selection()) {
1225         Gtk::TreeModel::iterator i = _list.get_selection()->get_selected();
1227         if(i)
1228             return (*i)[_columns.filter];
1229     }
1231     return 0;
1234 void FilterEffectsDialog::FilterModifier::select_filter(const SPFilter* filter)
1236     if(filter) {
1237         for(Gtk::TreeModel::iterator i = _model->children().begin();
1238             i != _model->children().end(); ++i) {
1239             if((*i)[_columns.filter] == filter) {
1240                 _list.get_selection()->select(i);
1241                 break;
1242             }
1243         }
1244     }
1247 void FilterEffectsDialog::FilterModifier::filter_list_button_release(GdkEventButton* event)
1249     if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) {
1250         const bool sensitive = get_selected_filter() != NULL;
1251         _menu->items()[0].set_sensitive(sensitive);
1252         _menu->items()[1].set_sensitive(sensitive);
1253         _menu->popup(event->button, event->time);
1254     }
1257 void FilterEffectsDialog::FilterModifier::add_filter()
1259     SPDocument* doc = sp_desktop_document(_dialog.getDesktop());
1260     SPFilter* filter = new_filter(doc);
1262     const int count = _model->children().size();
1263     std::ostringstream os;
1264     os << "filter" << count;
1265     filter->setLabel(os.str().c_str());
1267     update_filters();
1269     select_filter(filter);
1271     sp_document_done(doc, SP_VERB_DIALOG_FILTER_EFFECTS, _("Add filter"));
1274 void FilterEffectsDialog::FilterModifier::remove_filter()
1276     SPFilter *filter = get_selected_filter();
1278     if(filter) {
1279         SPDocument* doc = filter->document;
1280         sp_repr_unparent(filter->repr);
1282         sp_document_done(doc, SP_VERB_DIALOG_FILTER_EFFECTS, _("Remove filter"));
1284         update_filters();
1285     }
1288 void FilterEffectsDialog::FilterModifier::duplicate_filter()
1290     SPFilter* filter = get_selected_filter();
1292     if(filter) {
1293         Inkscape::XML::Node* repr = SP_OBJECT_REPR(filter), *parent = repr->parent();
1294         repr = repr->duplicate(repr->document());
1295         parent->appendChild(repr);
1297         sp_document_done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Duplicate filter"));
1299         update_filters();
1300     }
1303 void FilterEffectsDialog::FilterModifier::rename_filter()
1305     _list.set_cursor(_model->get_path(_list.get_selection()->get_selected()), *_list.get_column(1), true);
1308 FilterEffectsDialog::CellRendererConnection::CellRendererConnection()
1309     : Glib::ObjectBase(typeid(CellRendererConnection)),
1310       _primitive(*this, "primitive", 0)
1311 {}
1313 Glib::PropertyProxy<void*> FilterEffectsDialog::CellRendererConnection::property_primitive()
1315     return _primitive.get_proxy();
1318 void FilterEffectsDialog::CellRendererConnection::set_text_width(const int w)
1320     _text_width = w;
1323 int FilterEffectsDialog::CellRendererConnection::get_text_width() const
1325     return _text_width;
1328 void FilterEffectsDialog::CellRendererConnection::get_size_vfunc(
1329     Gtk::Widget& widget, const Gdk::Rectangle* /*cell_area*/,
1330     int* x_offset, int* y_offset, int* width, int* height) const
1332     PrimitiveList& primlist = dynamic_cast<PrimitiveList&>(widget);
1334     if(x_offset)
1335         (*x_offset) = 0;
1336     if(y_offset)
1337         (*y_offset) = 0;
1338     if(width)
1339         (*width) = size * primlist.primitive_count() + _text_width * 7;
1340     if(height) {
1341         // Scale the height depending on the number of inputs, unless it's
1342         // the first primitive, in which case there are no connections
1343         SPFilterPrimitive* prim = (SPFilterPrimitive*)_primitive.get_value();
1344         (*height) = size * input_count(prim);
1345     }
1348 /*** PrimitiveList ***/
1349 FilterEffectsDialog::PrimitiveList::PrimitiveList(FilterEffectsDialog& d)
1350     : _dialog(d),
1351       _in_drag(0),
1352       _observer(new SignalObserver)
1354     d.signal_expose_event().connect(sigc::mem_fun(*this, &PrimitiveList::on_expose_signal));
1355     
1356     add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK);
1357     signal_expose_event().connect(sigc::mem_fun(*this, &PrimitiveList::on_expose_signal));
1359     _model = Gtk::ListStore::create(_columns);
1361     set_reorderable(true);
1363     set_model(_model);
1364     append_column(_("_Effect"), _columns.type);
1366     _observer->signal_changed().connect(signal_primitive_changed().make_slot());
1367     get_selection()->signal_changed().connect(sigc::mem_fun(*this, &PrimitiveList::on_primitive_selection_changed));
1368     signal_primitive_changed().connect(sigc::mem_fun(*this, &PrimitiveList::queue_draw));
1370     _connection_cell.set_text_width(init_text());
1372     int cols_count = append_column(_("Connections"), _connection_cell);
1373     Gtk::TreeViewColumn* col = get_column(cols_count - 1);
1374     if(col)
1375        col->add_attribute(_connection_cell.property_primitive(), _columns.primitive);
1378 // Sets up a vertical Pango context/layout, and returns the largest
1379 // width needed to render the FilterPrimitiveInput labels.
1380 int FilterEffectsDialog::PrimitiveList::init_text()
1382     // Set up a vertical context+layout
1383     Glib::RefPtr<Pango::Context> context = create_pango_context();
1384     const Pango::Matrix matrix = {0, -1, 1, 0, 0, 0};
1385     context->set_matrix(matrix);
1386     _vertical_layout = Pango::Layout::create(context);
1388     int maxfont = 0;
1389     for(int i = 0; i < FPInputConverter.end; ++i) {
1390         _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
1391         int fontw, fonth;
1392         _vertical_layout->get_pixel_size(fontw, fonth);
1393         if(fonth > maxfont)
1394             maxfont = fonth;
1395     }
1397     return maxfont;
1400 sigc::signal<void>& FilterEffectsDialog::PrimitiveList::signal_primitive_changed()
1402     return _signal_primitive_changed;
1405 void FilterEffectsDialog::PrimitiveList::on_primitive_selection_changed()
1407     _observer->set(get_selected());
1408     signal_primitive_changed()();
1409     _dialog._color_matrix_values->clear_store();
1412 /* Add all filter primitives in the current to the list.
1413    Keeps the same selection if possible, otherwise selects the first element */
1414 void FilterEffectsDialog::PrimitiveList::update()
1416     SPFilter* f = _dialog._filter_modifier.get_selected_filter();
1417     const SPFilterPrimitive* active_prim = get_selected();
1418     bool active_found = false;
1420     _model->clear();
1422     if(f) {
1423         _dialog._primitive_box.set_sensitive(true);
1425         for(SPObject *prim_obj = f->children;
1426                 prim_obj && SP_IS_FILTER_PRIMITIVE(prim_obj);
1427                 prim_obj = prim_obj->next) {
1428             SPFilterPrimitive *prim = SP_FILTER_PRIMITIVE(prim_obj);
1429             if(prim) {
1430                 Gtk::TreeModel::Row row = *_model->append();
1431                 row[_columns.primitive] = prim;
1432                 row[_columns.type_id] = FPConverter.get_id_from_key(prim->repr->name());
1433                 row[_columns.type] = _(FPConverter.get_label(row[_columns.type_id]).c_str());
1434                 row[_columns.id] = SP_OBJECT_ID(prim);
1436                 if(prim == active_prim) {
1437                     get_selection()->select(row);
1438                     active_found = true;
1439                 }
1440             }
1441         }
1443         if(!active_found && _model->children().begin())
1444             get_selection()->select(_model->children().begin());
1446         columns_autosize();
1447     }
1448     else {
1449         _dialog._primitive_box.set_sensitive(false);
1450     }
1453 void FilterEffectsDialog::PrimitiveList::set_menu(Glib::RefPtr<Gtk::Menu> menu)
1455     _primitive_menu = menu;
1458 SPFilterPrimitive* FilterEffectsDialog::PrimitiveList::get_selected()
1460     if(_dialog._filter_modifier.get_selected_filter()) {
1461         Gtk::TreeModel::iterator i = get_selection()->get_selected();
1462         if(i)
1463             return (*i)[_columns.primitive];
1464     }
1466     return 0;
1469 void FilterEffectsDialog::PrimitiveList::select(SPFilterPrimitive* prim)
1471     for(Gtk::TreeIter i = _model->children().begin();
1472         i != _model->children().end(); ++i) {
1473         if((*i)[_columns.primitive] == prim)
1474             get_selection()->select(i);
1475     }
1478 void FilterEffectsDialog::PrimitiveList::remove_selected()
1480     SPFilterPrimitive* prim = get_selected();
1482     if(prim) {
1483         _observer->set(0);
1485         sp_repr_unparent(prim->repr);
1487         sp_document_done(sp_desktop_document(_dialog.getDesktop()), SP_VERB_DIALOG_FILTER_EFFECTS,
1488                          _("Remove filter primitive"));
1490         update();
1491     }
1494 bool FilterEffectsDialog::PrimitiveList::on_expose_signal(GdkEventExpose* e)
1496     Gdk::Rectangle clip(e->area.x, e->area.y, e->area.width, e->area.height);
1497     Glib::RefPtr<Gdk::Window> win = get_bin_window();
1498     Glib::RefPtr<Gdk::GC> darkgc = get_style()->get_dark_gc(Gtk::STATE_NORMAL);
1500     SPFilterPrimitive* prim = get_selected();
1501     int row_count = get_model()->children().size();
1503     int fheight = CellRendererConnection::size;
1504     Gdk::Rectangle rct, vis;
1505     Gtk::TreeIter row = get_model()->children().begin();
1506     int text_start_x = 0;
1507     if(row) {
1508         get_cell_area(get_model()->get_path(row), *get_column(1), rct);
1509         get_visible_rect(vis);
1510         int vis_x, vis_y;
1511         tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y);
1513         text_start_x = rct.get_x() + rct.get_width() - _connection_cell.get_text_width() * (FPInputConverter.end + 1) + 1;
1514         for(int i = 0; i < FPInputConverter.end; ++i) {
1515             _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str()));
1516             const int x = text_start_x + _connection_cell.get_text_width() * (i + 1);
1517             get_bin_window()->draw_rectangle(get_style()->get_bg_gc(Gtk::STATE_NORMAL), true, x, vis_y, _connection_cell.get_text_width(), vis.get_height());
1518             get_bin_window()->draw_layout(get_style()->get_text_gc(Gtk::STATE_NORMAL), x + 1, vis_y, _vertical_layout);
1519             get_bin_window()->draw_line(darkgc, x, vis_y, x, vis_y + vis.get_height());
1520         }
1521     }
1523     int row_index = 0;
1524     for(; row != get_model()->children().end(); ++row, ++row_index) {
1525         get_cell_area(get_model()->get_path(row), *get_column(1), rct);
1526         const int x = rct.get_x(), y = rct.get_y(), h = rct.get_height();
1528         // Check mouse state
1529         int mx, my;
1530         Gdk::ModifierType mask;
1531         get_bin_window()->get_pointer(mx, my, mask);
1533         // Outline the bottom of the connection area
1534         const int outline_x = x + fheight * (row_count - row_index);
1535         get_bin_window()->draw_line(darkgc, x, y + h, outline_x, y + h);
1537         // Side outline
1538         get_bin_window()->draw_line(darkgc, outline_x, y - 1, outline_x, y + h);
1540         std::vector<Gdk::Point> con_poly;
1541         int con_drag_y;
1542         bool inside;
1543         const SPFilterPrimitive* row_prim = (*row)[_columns.primitive];
1544         const int inputs = input_count(row_prim);
1546         if(SP_IS_FEMERGE(row_prim)) {
1547             for(int i = 0; i < inputs; ++i) {
1548                 inside = do_connection_node(row, i, con_poly, mx, my);
1549                 get_bin_window()->draw_polygon(inside && mask & GDK_BUTTON1_MASK ?
1550                                                darkgc : get_style()->get_dark_gc(Gtk::STATE_ACTIVE),
1551                                                inside, con_poly);
1553                 if(_in_drag == (i + 1))
1554                     con_drag_y = con_poly[2].get_y();
1556                 if(_in_drag != (i + 1) || row_prim != prim)
1557                     draw_connection(row, i, text_start_x, outline_x, con_poly[2].get_y(), row_count);
1558             }
1559         }
1560         else {
1561             // Draw "in" shape
1562             inside = do_connection_node(row, 0, con_poly, mx, my);
1563             con_drag_y = con_poly[2].get_y();
1564             get_bin_window()->draw_polygon(inside && mask & GDK_BUTTON1_MASK ?
1565                                            darkgc : get_style()->get_dark_gc(Gtk::STATE_ACTIVE),
1566                                            inside, con_poly);
1568             // Draw "in" connection
1569             if(_in_drag != 1 || row_prim != prim)
1570                 draw_connection(row, SP_ATTR_IN, text_start_x, outline_x, con_poly[2].get_y(), row_count);
1572             if(inputs == 2) {
1573                 // Draw "in2" shape
1574                 inside = do_connection_node(row, 1, con_poly, mx, my);
1575                 if(_in_drag == 2)
1576                     con_drag_y = con_poly[2].get_y();
1577                 get_bin_window()->draw_polygon(inside && mask & GDK_BUTTON1_MASK ?
1578                                                darkgc : get_style()->get_dark_gc(Gtk::STATE_ACTIVE),
1579                                                inside, con_poly);
1580                 // Draw "in2" connection
1581                 if(_in_drag != 2 || row_prim != prim)
1582                     draw_connection(row, SP_ATTR_IN2, text_start_x, outline_x, con_poly[2].get_y(), row_count);
1583             }
1584         }
1586         // Draw drag connection
1587         if(row_prim == prim && _in_drag) {
1588             get_bin_window()->draw_line(get_style()->get_black_gc(), outline_x, con_drag_y,
1589                                         mx, con_drag_y);
1590             get_bin_window()->draw_line(get_style()->get_black_gc(), mx, con_drag_y, mx, my);
1591         }
1592     }
1594     return true;
1597 void FilterEffectsDialog::PrimitiveList::draw_connection(const Gtk::TreeIter& input, const int attr,
1598                                                          const int text_start_x, const int x1, const int y1,
1599                                                          const int row_count)
1601     int src_id = 0;
1602     Gtk::TreeIter res = find_result(input, attr, src_id);
1603     Glib::RefPtr<Gdk::GC> darkgc = get_style()->get_black_gc();
1604     Glib::RefPtr<Gdk::GC> lightgc = get_style()->get_dark_gc(Gtk::STATE_NORMAL);
1605     Glib::RefPtr<Gdk::GC> gc;
1607     const bool is_first = input == get_model()->children().begin();
1608     const bool is_merge = SP_IS_FEMERGE((SPFilterPrimitive*)(*input)[_columns.primitive]);
1609     const bool use_default = !res && !is_merge;
1610     
1611     if(res == input || (use_default && is_first)) {
1612         // Draw straight connection to a standard input
1613         // Draw a lighter line for an implicit connection to a standard input
1614         const int tw = _connection_cell.get_text_width();
1615         gint end_x = text_start_x + tw * (src_id + 1) + (int)(tw * 0.5f) + 1;
1616         gc = (use_default && is_first) ? lightgc : darkgc;
1617         get_bin_window()->draw_rectangle(gc, true, end_x-2, y1-2, 5, 5);
1618         get_bin_window()->draw_line(gc, x1, y1, end_x, y1);
1619     }
1620     else {
1621         // Draw an 'L'-shaped connection to another filter primitive
1622         // If no connection is specified, draw a light connection to the previous primitive
1623         gc = use_default ? lightgc : darkgc;
1625         if(use_default) {
1626             res = input;
1627             --res;
1628         }
1630         if(res) {
1631             Gdk::Rectangle rct;
1632             
1633             get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct);
1634             const int fheight = CellRendererConnection::size;
1635             
1636             get_cell_area(get_model()->get_path(res), *get_column(1), rct);
1637             const int row_index = find_index(res);
1638             const int x2 = rct.get_x() + fheight * (row_count - row_index) - fheight / 2;
1639             const int y2 = rct.get_y() + rct.get_height();
1641             // Draw a bevelled 'L'-shaped connection
1642             get_bin_window()->draw_line(get_style()->get_black_gc(), x1, y1, x2-fheight/4, y1);
1643             get_bin_window()->draw_line(get_style()->get_black_gc(), x2-fheight/4, y1, x2, y1-fheight/4);
1644             get_bin_window()->draw_line(get_style()->get_black_gc(), x2, y1-fheight/4, x2, y2);            
1645         }
1646     }
1649 // Creates a triangle outline of the connection node and returns true if (x,y) is inside the node
1650 bool FilterEffectsDialog::PrimitiveList::do_connection_node(const Gtk::TreeIter& row, const int input,
1651                                                             std::vector<Gdk::Point>& points,
1652                                                             const int ix, const int iy)
1654     Gdk::Rectangle rct;
1655     const int icnt = input_count((*row)[_columns.primitive]);
1657     get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct);
1658     const int fheight = CellRendererConnection::size;
1660     get_cell_area(_model->get_path(row), *get_column(1), rct);
1661     const float h = rct.get_height() / icnt;
1663     const int x = rct.get_x() + fheight * (_model->children().size() - find_index(row));
1664     const int con_w = (int)(fheight * 0.35f);
1665     const int con_y = (int)(rct.get_y() + (h / 2) - con_w + (input * h));
1666     points.clear();
1667     points.push_back(Gdk::Point(x, con_y));
1668     points.push_back(Gdk::Point(x, con_y + con_w * 2));
1669     points.push_back(Gdk::Point(x - con_w, con_y + con_w));
1671     return ix >= x - h && iy >= con_y && ix <= x && iy <= points[1].get_y();
1674 const Gtk::TreeIter FilterEffectsDialog::PrimitiveList::find_result(const Gtk::TreeIter& start,
1675                                                                     const int attr, int& src_id)
1677     SPFilterPrimitive* prim = (*start)[_columns.primitive];
1678     Gtk::TreeIter target = _model->children().end();
1679     int image;
1681     if(SP_IS_FEMERGE(prim)) {
1682         int c = 0;
1683         bool found = false;
1684         for(const SPObject* o = prim->firstChild(); o; o = o->next, ++c) {
1685             if(c == attr && SP_IS_FEMERGENODE(o)) {
1686                 image = SP_FEMERGENODE(o)->input;
1687                 found = true;
1688             }
1689         }
1690         if(!found)
1691             return target;
1692     }
1693     else {
1694         if(attr == SP_ATTR_IN)
1695             image = prim->image_in;
1696         else if(attr == SP_ATTR_IN2) {
1697             if(SP_IS_FEBLEND(prim))
1698                 image = SP_FEBLEND(prim)->in2;
1699             else if(SP_IS_FECOMPOSITE(prim))
1700                 image = SP_FECOMPOSITE(prim)->in2;
1701             else if(SP_IS_FEDISPLACEMENTMAP(prim))
1702                 image = SP_FEDISPLACEMENTMAP(prim)->in2;
1703             else
1704                 return target;
1705         }
1706         else
1707             return target;
1708     }
1710     if(image >= 0) {
1711         for(Gtk::TreeIter i = _model->children().begin();
1712             i != start; ++i) {
1713             if(((SPFilterPrimitive*)(*i)[_columns.primitive])->image_out == image)
1714                 target = i;
1715         }
1716         return target;
1717     }
1718     else if(image < -1) {
1719         src_id = -(image + 2);
1720         return start;
1721     }
1723     return target;
1726 int FilterEffectsDialog::PrimitiveList::find_index(const Gtk::TreeIter& target)
1728     int i = 0;
1729     for(Gtk::TreeIter iter = _model->children().begin();
1730         iter != target; ++iter, ++i);
1731     return i;
1734 bool FilterEffectsDialog::PrimitiveList::on_button_press_event(GdkEventButton* e)
1736     Gtk::TreePath path;
1737     Gtk::TreeViewColumn* col;
1738     const int x = (int)e->x, y = (int)e->y;
1739     int cx, cy;
1741     _drag_prim = 0;
1742     
1743     if(get_path_at_pos(x, y, path, col, cx, cy)) {
1744         Gtk::TreeIter iter = _model->get_iter(path);
1745         std::vector<Gdk::Point> points;
1747         _drag_prim = (*iter)[_columns.primitive];
1748         const int icnt = input_count(_drag_prim);
1750         for(int i = 0; i < icnt; ++i) {
1751             if(do_connection_node(_model->get_iter(path), i, points, x, y)) {
1752                 _in_drag = i + 1;
1753                 break;
1754             }
1755         }
1756         
1757         queue_draw();
1758     }
1760     if(_in_drag) {
1761         _scroll_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &PrimitiveList::on_scroll_timeout), 150);
1762         _autoscroll = 0;
1763         get_selection()->select(path);
1764         return true;
1765     }
1766     else
1767         return Gtk::TreeView::on_button_press_event(e);
1770 bool FilterEffectsDialog::PrimitiveList::on_motion_notify_event(GdkEventMotion* e)
1772     const int speed = 10;
1773     const int limit = 15;
1775     Gdk::Rectangle vis;
1776     get_visible_rect(vis);
1777     int vis_x, vis_y;
1778     tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y);
1779     const int top = vis_y + vis.get_height();
1781     // When autoscrolling during a connection drag, set the speed based on
1782     // where the mouse is in relation to the edges.
1783     if(e->y < vis_y)
1784         _autoscroll = -(int)(speed + (vis_y - e->y) / 5);
1785     else if(e->y < vis_y + limit)
1786         _autoscroll = -speed;
1787     else if(e->y > top)
1788         _autoscroll = (int)(speed + (e->y - top) / 5);
1789     else if(e->y > top - limit)
1790         _autoscroll = speed;
1791     else
1792         _autoscroll = 0;
1794     queue_draw();
1796     return Gtk::TreeView::on_motion_notify_event(e);
1799 bool FilterEffectsDialog::PrimitiveList::on_button_release_event(GdkEventButton* e)
1801     SPFilterPrimitive *prim = get_selected(), *target;
1803     _scroll_connection.disconnect();
1805     if(_in_drag && prim) {
1806         Gtk::TreePath path;
1807         Gtk::TreeViewColumn* col;
1808         int cx, cy;
1809         
1810         if(get_path_at_pos((int)e->x, (int)e->y, path, col, cx, cy)) {
1811             const gchar *in_val = 0;
1812             Glib::ustring result;
1813             Gtk::TreeIter target_iter = _model->get_iter(path);
1814             target = (*target_iter)[_columns.primitive];
1816             Gdk::Rectangle rct;
1817             get_cell_area(path, *col, rct);
1818             const int twidth = _connection_cell.get_text_width();
1819             const int sources_x = rct.get_width() - twidth * FPInputConverter.end;
1820             if(cx > sources_x) {
1821                 int src = (cx - sources_x) / twidth;
1822                 if(src < 0)
1823                     src = 0;
1824                 else if(src >= FPInputConverter.end)
1825                     src = FPInputConverter.end - 1;
1826                 result = FPInputConverter.get_key((FilterPrimitiveInput)src);
1827                 in_val = result.c_str();
1828             }
1829             else {
1830                 // Ensure that the target comes before the selected primitive
1831                 for(Gtk::TreeIter iter = _model->children().begin();
1832                     iter != get_selection()->get_selected(); ++iter) {
1833                     if(iter == target_iter) {
1834                         Inkscape::XML::Node *repr = SP_OBJECT_REPR(target);
1835                         // Make sure the target has a result
1836                         const gchar *gres = repr->attribute("result");
1837                         if(!gres) {
1838                             result = "result" + Glib::Ascii::dtostr(SP_FILTER(prim->parent)->_image_number_next);
1839                             repr->setAttribute("result", result.c_str());
1840                             in_val = result.c_str();
1841                         }
1842                         else
1843                             in_val = gres;
1844                         break;
1845                     }
1846                 }
1847             }
1849             if(SP_IS_FEMERGE(prim)) {
1850                 int c = 1;
1851                 bool handled = false;
1852                 for(SPObject* o = prim->firstChild(); o && !handled; o = o->next, ++c) {
1853                     if(c == _in_drag && SP_IS_FEMERGENODE(o)) {
1854                         // If input is null, delete it
1855                         if(!in_val) {
1856                             sp_repr_unparent(o->repr);
1857                             sp_document_done(prim->document, SP_VERB_DIALOG_FILTER_EFFECTS,
1858                                              _("Remove merge node"));
1859                             (*get_selection()->get_selected())[_columns.primitive] = prim;
1860                         }
1861                         else
1862                             _dialog.set_attr(o, SP_ATTR_IN, in_val);
1863                         handled = true;
1864                     }
1865                 }
1866                 // Add new input?
1867                 if(!handled && c == _in_drag && in_val) {
1868                     Inkscape::XML::Document *xml_doc = sp_document_repr_doc(prim->document);
1869                     Inkscape::XML::Node *repr = xml_doc->createElement("svg:feMergeNode");
1870                     repr->setAttribute("inkscape:collect", "always");
1871                     prim->repr->appendChild(repr);
1872                     SPFeMergeNode *node = SP_FEMERGENODE(prim->document->getObjectByRepr(repr));
1873                     Inkscape::GC::release(repr);
1874                     _dialog.set_attr(node, SP_ATTR_IN, in_val);
1875                     (*get_selection()->get_selected())[_columns.primitive] = prim;
1876                 }
1877             }
1878             else {
1879                 if(_in_drag == 1)
1880                     _dialog.set_attr(prim, SP_ATTR_IN, in_val);
1881                 else if(_in_drag == 2)
1882                     _dialog.set_attr(prim, SP_ATTR_IN2, in_val);
1883             }
1884         }
1886         _in_drag = 0;
1887         queue_draw();
1889         _dialog.update_settings_view();
1890     }
1892     if((e->type == GDK_BUTTON_RELEASE) && (e->button == 3)) {
1893         const bool sensitive = get_selected() != NULL;
1894         _primitive_menu->items()[0].set_sensitive(sensitive);
1895         _primitive_menu->items()[1].set_sensitive(sensitive);
1896         _primitive_menu->popup(e->button, e->time);
1898         return true;
1899     }
1900     else
1901         return Gtk::TreeView::on_button_release_event(e);
1904 // Checks all of prim's inputs, removes any that use result
1905 void check_single_connection(SPFilterPrimitive* prim, const int result)
1907     if(prim && result >= 0) {
1909         if(prim->image_in == result)
1910             SP_OBJECT_REPR(prim)->setAttribute("in", 0);
1912         if(SP_IS_FEBLEND(prim)) {
1913             if(SP_FEBLEND(prim)->in2 == result)
1914                 SP_OBJECT_REPR(prim)->setAttribute("in2", 0);
1915         }
1916         else if(SP_IS_FECOMPOSITE(prim)) {
1917             if(SP_FECOMPOSITE(prim)->in2 == result)
1918                 SP_OBJECT_REPR(prim)->setAttribute("in2", 0);
1919         }
1920         else if(SP_IS_FEDISPLACEMENTMAP(prim)) {
1921             if(SP_FEDISPLACEMENTMAP(prim)->in2 == result)
1922                 SP_OBJECT_REPR(prim)->setAttribute("in2", 0);
1923         }
1924     }
1927 // Remove any connections going to/from prim_iter that forward-reference other primitives
1928 void FilterEffectsDialog::PrimitiveList::sanitize_connections(const Gtk::TreeIter& prim_iter)
1930     SPFilterPrimitive *prim = (*prim_iter)[_columns.primitive];
1931     bool before = true;
1933     for(Gtk::TreeIter iter = _model->children().begin();
1934         iter != _model->children().end(); ++iter) {
1935         if(iter == prim_iter)
1936             before = false;
1937         else {
1938             SPFilterPrimitive* cur_prim = (*iter)[_columns.primitive];
1939             if(before)
1940                 check_single_connection(cur_prim, prim->image_out);
1941             else
1942                 check_single_connection(prim, cur_prim->image_out);
1943         }
1944     }
1947 // Reorder the filter primitives to match the list order
1948 void FilterEffectsDialog::PrimitiveList::on_drag_end(const Glib::RefPtr<Gdk::DragContext>& /*dc*/)
1950     SPFilter* filter = _dialog._filter_modifier.get_selected_filter();
1951     int ndx = 0;
1953     for(Gtk::TreeModel::iterator iter = _model->children().begin();
1954         iter != _model->children().end(); ++iter, ++ndx) {
1955         SPFilterPrimitive* prim = (*iter)[_columns.primitive];
1956         if(prim && prim == _drag_prim) {
1957             SP_OBJECT_REPR(prim)->setPosition(ndx);
1958             break;
1959         }
1960     }
1962     for(Gtk::TreeModel::iterator iter = _model->children().begin();
1963         iter != _model->children().end(); ++iter, ++ndx) {
1964         SPFilterPrimitive* prim = (*iter)[_columns.primitive];
1965         if(prim && prim == _drag_prim) {
1966             sanitize_connections(iter);
1967             get_selection()->select(iter);
1968             break;
1969         }
1970     }
1972     filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
1974     sp_document_done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Reorder filter primitive"));
1977 // If a connection is dragged towards the top or bottom of the list, the list should scroll to follow.
1978 bool FilterEffectsDialog::PrimitiveList::on_scroll_timeout()
1980     if(_autoscroll) {
1981         Gtk::Adjustment& a = *dynamic_cast<Gtk::ScrolledWindow*>(get_parent())->get_vadjustment();
1982         double v;
1984         v = a.get_value() + _autoscroll;
1985         if(v < 0)
1986             v = 0;
1987         if(v > a.get_upper() - a.get_page_size())
1988             v = a.get_upper() - a.get_page_size();
1990         a.set_value(v);
1992         queue_draw();
1993     }
1995     return true;
1998 int FilterEffectsDialog::PrimitiveList::primitive_count() const
2000     return _model->children().size();
2003 /*** FilterEffectsDialog ***/
2005 FilterEffectsDialog::FilterEffectsDialog() 
2006     : UI::Widget::Panel("", "dialogs.filtereffects", SP_VERB_DIALOG_FILTER_EFFECTS),
2007       _filter_modifier(*this),
2008       _primitive_list(*this),
2009       _add_primitive_type(FPConverter),
2010       _add_primitive(_("Add Effect:")),
2011       _empty_settings(_("No effect selected"), Gtk::ALIGN_LEFT),
2012       _locked(false),
2013       _attr_lock(false)
2015     _settings = new Settings(*this, _settings_box, sigc::mem_fun(*this, &FilterEffectsDialog::set_attr_direct),
2016                              NR_FILTER_ENDPRIMITIVETYPE);
2017     _sizegroup = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL);
2018     _sizegroup->set_ignore_hidden();
2020     _add_primitive_type.remove_row(NR_FILTER_TILE);
2021     _add_primitive_type.remove_row(NR_FILTER_COMPONENTTRANSFER);
2022         
2023     // Initialize widget hierarchy
2024     Gtk::HPaned* hpaned = Gtk::manage(new Gtk::HPaned);
2025     Gtk::ScrolledWindow* sw_prims = Gtk::manage(new Gtk::ScrolledWindow);
2026     Gtk::HBox* infobox = Gtk::manage(new Gtk::HBox);
2027     Gtk::HBox* hb_prims = Gtk::manage(new Gtk::HBox);
2028     Gtk::Frame* fr_settings = Gtk::manage(new Gtk::Frame(_("<b>Effect parameters</b>")));
2029     Gtk::Alignment* al_settings = Gtk::manage(new Gtk::Alignment);
2030     _getContents()->add(*hpaned);
2031     hpaned->pack1(_filter_modifier);
2032     hpaned->pack2(_primitive_box);
2033     _primitive_box.pack_start(*sw_prims);
2034     _primitive_box.pack_start(*infobox,false, false);    
2035     _primitive_box.pack_start(*hb_prims, false, false);
2036     sw_prims->add(_primitive_list);
2037     infobox->pack_start(_infobox_icon, false, false);
2038     infobox->pack_end(_infobox_desc, false, false);
2039     _infobox_desc.set_line_wrap(true);
2040     
2041     hb_prims->pack_end(_add_primitive_type, false, false);
2042     hb_prims->pack_end(_add_primitive, false, false);
2043     _getContents()->pack_start(*fr_settings, false, false);
2044     fr_settings->add(*al_settings);
2045     al_settings->add(_settings_box);
2046     
2047     _primitive_list.signal_primitive_changed().connect(
2048         sigc::mem_fun(*this, &FilterEffectsDialog::update_settings_view));
2049     _filter_modifier.signal_filter_changed().connect(
2050         sigc::mem_fun(_primitive_list, &PrimitiveList::update));
2052     _add_primitive_type.signal_changed().connect(
2053         sigc::mem_fun(*this, &FilterEffectsDialog::update_primitive_infobox));
2054           
2055     sw_prims->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
2056     sw_prims->set_shadow_type(Gtk::SHADOW_IN);
2057     al_settings->set_padding(0, 0, 12, 0);
2058     fr_settings->set_shadow_type(Gtk::SHADOW_NONE);
2059     ((Gtk::Label*)fr_settings->get_label_widget())->set_use_markup();
2060     _add_primitive.signal_clicked().connect(sigc::mem_fun(*this, &FilterEffectsDialog::add_primitive));
2061     _primitive_list.set_menu(create_popup_menu(*this, sigc::mem_fun(*this, &FilterEffectsDialog::duplicate_primitive),
2062                                                sigc::mem_fun(_primitive_list, &PrimitiveList::remove_selected)));
2063     
2064     show_all_children();
2065     init_settings_widgets();
2066     _primitive_list.update();
2067     update_primitive_infobox();
2070 FilterEffectsDialog::~FilterEffectsDialog()
2072     delete _settings;
2075 void FilterEffectsDialog::set_attrs_locked(const bool l)
2077     _locked = l;
2080 void FilterEffectsDialog::show_all_vfunc()
2082     UI::Widget::Panel::show_all_vfunc();
2084     update_settings_view();
2087 void FilterEffectsDialog::init_settings_widgets()
2089     // TODO: Find better range/climb-rate/digits values for the SpinSliders,
2090     //       most of the current values are complete guesses!
2092     _empty_settings.set_sensitive(false);
2093     _settings_box.pack_start(_empty_settings);
2095     _settings->type(NR_FILTER_BLEND);
2096     _settings->add_combo(SP_ATTR_MODE, _("Mode"), BlendModeConverter);
2098     _settings->type(NR_FILTER_COLORMATRIX);
2099     ComboBoxEnum<FilterColorMatrixType>* colmat = _settings->add_combo(SP_ATTR_TYPE, _("Type"), ColorMatrixTypeConverter);
2100     _color_matrix_values = _settings->add_colormatrixvalues(_("Value(s)"));
2101     colmat->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::update_color_matrix));
2103     _settings->type(NR_FILTER_COMPONENTTRANSFER);
2104     _settings->add_notimplemented();
2105     /*_settings->add_combo(SP_ATTR_TYPE, _("Type"), ComponentTransferTypeConverter);
2106     _ct_slope = _settings->add_spinslider(SP_ATTR_SLOPE, _("Slope"), -100, 100, 1, 0.01, 1);
2107     _ct_intercept = _settings->add_spinslider(SP_ATTR_INTERCEPT, _("Intercept"), -100, 100, 1, 0.01, 1);
2108     _ct_amplitude = _settings->add_spinslider(SP_ATTR_AMPLITUDE, _("Amplitude"), 0, 100, 1, 0.01, 1);
2109     _ct_exponent = _settings->add_spinslider(SP_ATTR_EXPONENT, _("Exponent"), 0, 100, 1, 0.01, 1);
2110     _ct_offset = _settings->add_spinslider(SP_ATTR_OFFSET, _("Offset"), -100, 100, 1, 0.01, 1);*/
2112     _settings->type(NR_FILTER_COMPOSITE);
2113     _settings->add_combo(SP_ATTR_OPERATOR, _("Operator"), CompositeOperatorConverter);
2114     _k1 = _settings->add_spinslider(SP_ATTR_K1, _("K1"), -10, 10, 0.1, 0.01, 2);
2115     _k2 = _settings->add_spinslider(SP_ATTR_K2, _("K2"), -10, 10, 0.1, 0.01, 2);
2116     _k3 = _settings->add_spinslider(SP_ATTR_K3, _("K3"), -10, 10, 0.1, 0.01, 2);
2117     _k4 = _settings->add_spinslider(SP_ATTR_K4, _("K4"), -10, 10, 0.1, 0.01, 2);
2119     _settings->type(NR_FILTER_CONVOLVEMATRIX);
2120     _convolve_order = _settings->add_dualspinbutton(SP_ATTR_ORDER, _("Size"), 1, 5, 1, 1, 0);
2121     _convolve_target = _settings->add_multispinbutton(SP_ATTR_TARGETX, SP_ATTR_TARGETY, _("Target"), 0, 4, 1, 1, 0);
2122     _convolve_matrix = _settings->add_matrix(SP_ATTR_KERNELMATRIX, _("Kernel"));
2123     _convolve_order->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::convolve_order_changed));
2124     _settings->add_spinslider(SP_ATTR_DIVISOR, _("Divisor"), 1, 20, 1, 0.1, 2);
2125     _settings->add_spinslider(SP_ATTR_BIAS, _("Bias"), -10, 10, 1, 0.01, 1);
2126     _settings->add_combo(SP_ATTR_EDGEMODE, _("Edge Mode"), ConvolveMatrixEdgeModeConverter);
2127     _settings->add_checkbutton(SP_ATTR_PRESERVEALPHA, _("Preserve Alpha"), "true", "false");
2129     _settings->type(NR_FILTER_DIFFUSELIGHTING);
2130     _settings->add_color(SP_PROP_LIGHTING_COLOR, _("Diffuse Color"));
2131     _settings->add_spinslider(SP_ATTR_SURFACESCALE, _("Surface Scale"), -1000, 1000, 1, 0.01, 1);
2132     _settings->add_spinslider(SP_ATTR_DIFFUSECONSTANT, _("Constant"), 0, 100, 0.1, 0.01, 2);
2133     _settings->add_dualspinslider(SP_ATTR_KERNELUNITLENGTH, _("Kernel Unit Length"), 0.01, 10, 1, 0.01, 1);
2134     _settings->add_lightsource();
2136     _settings->type(NR_FILTER_DISPLACEMENTMAP);
2137     _settings->add_spinslider(SP_ATTR_SCALE, _("Scale"), 0, 100, 1, 0.01, 1);
2138     _settings->add_combo(SP_ATTR_XCHANNELSELECTOR, _("X Channel"), DisplacementMapChannelConverter);
2139     _settings->add_combo(SP_ATTR_YCHANNELSELECTOR, _("Y Channel"), DisplacementMapChannelConverter);
2141     _settings->type(NR_FILTER_FLOOD);
2142     _settings->add_color(SP_PROP_FLOOD_COLOR, _("Flood Color"));
2143     _settings->add_spinslider(SP_PROP_FLOOD_OPACITY, _("Opacity"), 0, 1, 0.1, 0.01, 2);
2144     
2145     _settings->type(NR_FILTER_GAUSSIANBLUR);
2146     _settings->add_dualspinslider(SP_ATTR_STDDEVIATION, _("Standard Deviation"), 0.01, 100, 1, 0.01, 1);
2148     _settings->type(NR_FILTER_MORPHOLOGY);
2149     _settings->add_combo(SP_ATTR_OPERATOR, _("Operator"), MorphologyOperatorConverter);
2150     _settings->add_dualspinslider(SP_ATTR_RADIUS, _("Radius"), 0, 100, 1, 0.01, 1);
2152     _settings->type(NR_FILTER_IMAGE);
2153     _settings->add_fileorelement(SP_ATTR_XLINK_HREF, _("Source of Image"));
2154     _settings->add_multispinbutton(SP_ATTR_X, SP_ATTR_Y, _("Coordinates"), -10000, 10000, 1, 1, 0);
2155     _settings->add_multispinbutton(SP_ATTR_WIDTH, SP_ATTR_HEIGHT, _("Dimensions"), 0, 10000, 1, 1, 0);
2156     
2157     _settings->type(NR_FILTER_OFFSET);
2158     _settings->add_spinslider(SP_ATTR_DX, _("Delta X"), -100, 100, 1, 0.01, 1);
2159     _settings->add_spinslider(SP_ATTR_DY, _("Delta Y"), -100, 100, 1, 0.01, 1);
2161     _settings->type(NR_FILTER_SPECULARLIGHTING);
2162     _settings->add_color(SP_PROP_LIGHTING_COLOR, _("Specular Color"));
2163     _settings->add_spinslider(SP_ATTR_SURFACESCALE, _("Surface Scale"), -1000, 1000, 1, 0.01, 1);
2164     _settings->add_spinslider(SP_ATTR_SPECULARCONSTANT, _("Constant"), 0, 100, 0.1, 0.01, 2);
2165     _settings->add_spinslider(SP_ATTR_SPECULAREXPONENT, _("Exponent"), 1, 128, 1, 0.01, 1);
2166     _settings->add_dualspinslider(SP_ATTR_KERNELUNITLENGTH, _("Kernel Unit Length"), 0.01, 10, 1, 0.01, 1);
2167     _settings->add_lightsource();
2168     
2169     _settings->type(NR_FILTER_TILE);
2170     _settings->add_notimplemented();
2172     _settings->type(NR_FILTER_TURBULENCE);
2173     _settings->add_checkbutton(SP_ATTR_STITCHTILES, _("Stitch Tiles"), "stitch", "noStitch");
2174     _settings->add_combo(SP_ATTR_TYPE, _("Type"), TurbulenceTypeConverter);
2175     _settings->add_dualspinslider(SP_ATTR_BASEFREQUENCY, _("Base Frequency"), 0, 1, 0.001, 0.01, 3);
2176     _settings->add_spinslider(SP_ATTR_NUMOCTAVES, _("Octaves"), 1, 10, 1, 1, 0);
2177     _settings->add_spinslider(SP_ATTR_SEED, _("Seed"), 0, 1000, 1, 1, 0);
2180 void FilterEffectsDialog::add_primitive()
2182     SPFilter* filter = _filter_modifier.get_selected_filter();
2184     if(filter) {
2185         SPFilterPrimitive* prim = filter_add_primitive(filter, _add_primitive_type.get_active_data()->id);
2187         _primitive_list.select(prim);
2189         sp_document_done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Add filter primitive"));
2190     }
2193 void FilterEffectsDialog::update_primitive_infobox()
2195     switch(_add_primitive_type.get_active_data()->id){
2196         case(NR::NR_FILTER_BLEND):
2197             _infobox_icon.set(g_strdup_printf("%s/feBlend-icon.png", INKSCAPE_PIXMAPDIR));
2198             _infobox_desc.set_markup(_("The <b>feBlend</b> filter primitive provides 4 image blending modes: screen, multiply, darken and lighten."));
2199             break;
2200         case(NR::NR_FILTER_COLORMATRIX):
2201             _infobox_icon.set(g_strdup_printf("%s/feColorMatrix-icon.png", INKSCAPE_PIXMAPDIR));
2202             _infobox_desc.set_markup(_("The <b>feColorMatrix</b> filter primitive applies a matrix transformation to colour of each rendered pixel. This allows for effects like turning object to grayscale, modifying colour saturation and changing colour hue."));
2203             break;
2204         case(NR::NR_FILTER_COMPONENTTRANSFER):
2205             _infobox_icon.set(g_strdup_printf("%s/feComponentTransfer-icon.png", INKSCAPE_PIXMAPDIR));
2206             _infobox_desc.set_markup(_("The <b>feComponentTransfer</b> filter primitive manipulates the input's color components (red, green, blue, and alpha) according to particular transfer functions, allowing operations like brightness and contrast adjustment, color balance, and thresholding."));
2207             break;
2208         case(NR::NR_FILTER_COMPOSITE):
2209             _infobox_icon.set(g_strdup_printf("%s/feComposite-icon.png", INKSCAPE_PIXMAPDIR));
2210             _infobox_desc.set_markup(_("The <b>feComposite</b> filter primitive composites two images using one of the Porter-Duff blending modes or the aritmetic mode described in SVG standard. Porter-Duff blending modes are essentially logical operations between the corresponding pixel values of the images."));
2211             break;
2212         case(NR::NR_FILTER_CONVOLVEMATRIX):
2213             _infobox_icon.set(g_strdup_printf("%s/feConvolveMatrix-icon.png", INKSCAPE_PIXMAPDIR));
2214             _infobox_desc.set_markup(_("The <b>feConvolveMatrix</b> lets you specify a Convolution to be applied on the image. Common effects created using convolution matrices are blur, sharpening, embossing and edge detection. Note that while gaussian blur can be created using this filter primitive, the special gaussian blur primitive is faster and resolution-independent."));
2215             break;
2216         case(NR::NR_FILTER_DIFFUSELIGHTING):
2217             _infobox_icon.set(g_strdup_printf("%s/feDiffuseLighting-icon.png", INKSCAPE_PIXMAPDIR));
2218             _infobox_desc.set_markup(_("The <b>feDiffuseLighting</b> and feSpecularLighting filter primitives create \"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."));
2219             break;
2220         case(NR::NR_FILTER_DISPLACEMENTMAP):
2221             _infobox_icon.set(g_strdup_printf("%s/feDisplacementMap-icon.png", INKSCAPE_PIXMAPDIR));
2222             _infobox_desc.set_markup(_("The <b>feDisplacementMap</b> filter primitive displaces the pixels in the first input using the second input as a displacement map, that shows from how far the pixel should come from. Classical examples are whirl and pinch effects."));
2223             break;
2224         case(NR::NR_FILTER_FLOOD):
2225             _infobox_icon.set(g_strdup_printf("%s/feFlood-icon.png", INKSCAPE_PIXMAPDIR));
2226             _infobox_desc.set_markup(_("The <b>feFlood</b> filter primitive fills the region with a given color and opacity.  It is usually used as an input to other filters to apply color to a graphic."));
2227             break;
2228         case(NR::NR_FILTER_GAUSSIANBLUR):
2229             _infobox_icon.set(g_strdup_printf("%s/feGaussianBlur-icon.png", INKSCAPE_PIXMAPDIR));
2230             _infobox_desc.set_markup(_("The <b>feGaussianBlur</b> filter primitive uniformly blurs its input.  It is commonly used together with feOffset to create a drop shadow effect."));
2231             break;
2232         case(NR::NR_FILTER_IMAGE):
2233             _infobox_icon.set(g_strdup_printf("%s/feImage-icon.png", INKSCAPE_PIXMAPDIR));
2234             _infobox_desc.set_markup(_("The <b>feImage</b> filter primitive fills the region with an external image or another part of the document."));
2235             break;
2236         case(NR::NR_FILTER_MERGE):
2237             _infobox_icon.set(g_strdup_printf("%s/feMerge-icon.png", INKSCAPE_PIXMAPDIR));
2238             _infobox_desc.set_markup(_("The <b>feMerge</b> filter primitive composites several temporary images inside the filter primitive to a single image. It uses normal alpha compositing for this. This is equivalent to using several feBlend primitives in 'normal' mode or several feComposite primitives in 'over' mode."));
2239             break;
2240         case(NR::NR_FILTER_MORPHOLOGY):
2241             _infobox_icon.set(g_strdup_printf("%s/feMorphology-icon.png", INKSCAPE_PIXMAPDIR));
2242             _infobox_desc.set_markup(_("The <b>feMorphology</b> filter primitive provides erode and dilate effects. For single-colour objects erode makes the object thinner and dilate makes it thicker."));
2243             break;
2244         case(NR::NR_FILTER_OFFSET):
2245             _infobox_icon.set(g_strdup_printf("%s/feOffset-icon.png", INKSCAPE_PIXMAPDIR));
2246             _infobox_desc.set_markup(_("The <b>feOffset</b> filter primitive offsets the image by an user-defined amount. For example, this is useful for drop shadows, where the shadow is in a slightly different position than the actual object."));
2247             break;
2248         case(NR::NR_FILTER_SPECULARLIGHTING):
2249             _infobox_icon.set(g_strdup_printf("%s/feSpecularLighting-icon.png", INKSCAPE_PIXMAPDIR));
2250             _infobox_desc.set_markup(_("The feDiffuseLighting and <b>feSpecularLighting</b> filter primitives create \"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."));
2251             break;
2252         case(NR::NR_FILTER_TILE):
2253             _infobox_icon.set(g_strdup_printf("%s/feTile-icon.png", INKSCAPE_PIXMAPDIR));
2254             _infobox_desc.set_markup(_("The <b>feTile</b> filter primitive tiles a region with its input graphic"));
2255             break;
2256         case(NR::NR_FILTER_TURBULENCE):
2257             _infobox_icon.set(g_strdup_printf("%s/feTurbulence-icon.png", INKSCAPE_PIXMAPDIR));
2258             _infobox_desc.set_markup(_("The <b>feTurbulence</b> filter primitive renders Perlin noise. This kind of noise is useful in simulating several nature phenomena like clouds, fire and smoke and in generating complex textures like marble or granite."));
2259             break;
2260     }
2263 void FilterEffectsDialog::duplicate_primitive()
2265     SPFilter* filter = _filter_modifier.get_selected_filter();
2266     SPFilterPrimitive* origprim = _primitive_list.get_selected();
2268     if(filter && origprim) {
2269         Inkscape::XML::Node *repr;
2270         repr = SP_OBJECT_REPR(origprim)->duplicate(SP_OBJECT_REPR(origprim)->document());
2271         SP_OBJECT_REPR(filter)->appendChild(repr);
2273         sp_document_done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Duplicate filter primitive"));
2275         _primitive_list.update();
2276     }
2279 void FilterEffectsDialog::convolve_order_changed()
2281     _convolve_matrix->set_from_attribute(SP_OBJECT(_primitive_list.get_selected()));
2282     _convolve_target->get_spinbuttons()[0]->get_adjustment()->set_upper(_convolve_order->get_spinbutton1().get_value() - 1);
2283     _convolve_target->get_spinbuttons()[1]->get_adjustment()->set_upper(_convolve_order->get_spinbutton2().get_value() - 1);
2286 void FilterEffectsDialog::set_attr_direct(const AttrWidget* input)
2288     set_attr(_primitive_list.get_selected(), input->get_attribute(), input->get_as_attribute().c_str());
2291 void FilterEffectsDialog::set_child_attr_direct(const AttrWidget* input)
2293     set_attr(_primitive_list.get_selected()->children, input->get_attribute(), input->get_as_attribute().c_str());
2296 void FilterEffectsDialog::set_attr(SPObject* o, const SPAttributeEnum attr, const gchar* val)
2298     if(!_locked) {
2299         _attr_lock = true;
2301         SPFilter *filter = _filter_modifier.get_selected_filter();
2302         const gchar* name = (const gchar*)sp_attribute_name(attr);
2303         if(filter && name && o) {
2304             update_settings_sensitivity();
2306             SP_OBJECT_REPR(o)->setAttribute(name, val);
2307             filter->requestModified(SP_OBJECT_MODIFIED_FLAG);
2309             Glib::ustring undokey = "filtereffects:";
2310             undokey += name;
2311             sp_document_maybe_done(filter->document, undokey.c_str(), SP_VERB_DIALOG_FILTER_EFFECTS,
2312                                    _("Set filter primitive attribute"));
2313         }
2315         _attr_lock = false;
2316     }
2319 void FilterEffectsDialog::update_settings_view()
2321     update_settings_sensitivity();
2323     if(_attr_lock)
2324         return;
2326     SPFilterPrimitive* prim = _primitive_list.get_selected();
2328     if(prim) {
2329         _settings->show_and_update(FPConverter.get_id_from_key(prim->repr->name()), prim);
2330         _empty_settings.hide();
2331     }
2332     else {
2333         _settings_box.hide_all();
2334         _settings_box.show();
2335         _empty_settings.show();
2336     }
2339 void FilterEffectsDialog::update_settings_sensitivity()
2341     SPFilterPrimitive* prim = _primitive_list.get_selected();
2342     const bool use_k = SP_IS_FECOMPOSITE(prim) && SP_FECOMPOSITE(prim)->composite_operator == COMPOSITE_ARITHMETIC;
2343     _k1->set_sensitive(use_k);
2344     _k2->set_sensitive(use_k);
2345     _k3->set_sensitive(use_k);
2346     _k4->set_sensitive(use_k);
2348     if(SP_IS_FECOMPONENTTRANSFER(prim)) {
2349         SPFeComponentTransfer* ct = SP_FECOMPONENTTRANSFER(prim);
2350         const bool linear = ct->type == COMPONENTTRANSFER_TYPE_LINEAR;
2351         const bool gamma = ct->type == COMPONENTTRANSFER_TYPE_GAMMA;
2353         // Component transfer not yet implemented
2354         /*_ct_table->set_sensitive(ct->type == COMPONENTTRANSFER_TYPE_TABLE || ct->type == COMPONENTTRANSFER_TYPE_DISCRETE);
2355         _ct_slope->set_sensitive(linear);
2356         _ct_intercept->set_sensitive(linear);
2357         _ct_amplitude->set_sensitive(gamma);
2358         _ct_exponent->set_sensitive(gamma);
2359         _ct_offset->set_sensitive(gamma);*/
2360     }
2363 void FilterEffectsDialog::update_color_matrix()
2365     _color_matrix_values->set_from_attribute(_primitive_list.get_selected());
2368 } // namespace Dialog
2369 } // namespace UI
2370 } // namespace Inkscape
2372 /*
2373   Local Variables:
2374   mode:c++
2375   c-file-style:"stroustrup"
2376   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
2377   indent-tabs-mode:nil
2378   fill-column:99
2379   End:
2380 */
2381 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :