1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3  * Inkscape Preferences dialog.
4  *
5  * Authors:
6  *   Marco Scholten
7  *   Bruno Dilly <bruno.dilly@gmail.com>
8  *
9  * Copyright (C) 2004, 2006, 2007 Authors
10  *
11  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
12  */
13 
14 #include <glibmm/i18n.h>
15 #include <glibmm/convert.h>
16 #include <glibmm/regex.h>
17 
18 #include <gtkmm/box.h>
19 #include <gtkmm/frame.h>
20 #include <gtkmm/scale.h>
21 #include <gtkmm/table.h>
22 
23 
24 #include "desktop.h"
25 #include "inkscape.h"
26 #include "message-stack.h"
27 #include "preferences.h"
28 #include "selcue.h"
29 #include "selection-chemistry.h"
30 #include "verbs.h"
31 
32 #include "include/gtkmm_version.h"
33 
34 #include "io/sys.h"
35 
36 #include "ui/dialog/filedialog.h"
37 #include "ui/icon-loader.h"
38 #include "ui/widget/preferences-widget.h"
39 
40 
41 #ifdef _WIN32
42 #include <windows.h>
43 #endif
44 
45 using namespace Inkscape::UI::Widget;
46 
47 namespace Inkscape {
48 namespace UI {
49 namespace Widget {
50 
DialogPage()51 DialogPage::DialogPage()
52 {
53     set_border_width(12);
54 
55     set_orientation(Gtk::ORIENTATION_VERTICAL);
56     set_column_spacing(12);
57     set_row_spacing(6);
58 }
59 
60 /**
61  * Add a widget to the bottom row of the dialog page
62  *
63  * \param[in] indent         Whether the widget should be indented by one column
64  * \param[in] label          The label text for the widget
65  * \param[in] widget         The widget to add to the page
66  * \param[in] suffix         Text for an optional label at the right of the widget
67  * \param[in] tip            Tooltip text for the widget
68  * \param[in] expand_widget  Whether to expand the widget horizontally
69  * \param[in] other_widget   An optional additional widget to display at the right of the first one
70  */
add_line(bool indent,Glib::ustring const & label,Gtk::Widget & widget,Glib::ustring const & suffix,const Glib::ustring & tip,bool expand_widget,Gtk::Widget * other_widget)71 void DialogPage::add_line(bool                 indent,
72                           Glib::ustring const &label,
73                           Gtk::Widget         &widget,
74                           Glib::ustring const &suffix,
75                           const Glib::ustring &tip,
76                           bool                 expand_widget,
77                           Gtk::Widget         *other_widget)
78 {
79     if (tip != "")
80         widget.set_tooltip_text (tip);
81 
82     auto hb = Gtk::manage(new Gtk::Box());
83     hb->set_spacing(12);
84     hb->set_hexpand(true);
85     hb->pack_start(widget, expand_widget, expand_widget);
86 
87     // Pack an additional widget into a box with the widget if desired
88     if (other_widget)
89         hb->pack_start(*other_widget, expand_widget, expand_widget);
90 
91     hb->set_valign(Gtk::ALIGN_CENTER);
92 
93     // Add a label in the first column if provided
94     if (label != "")
95     {
96         Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(label, Gtk::ALIGN_START,
97                                                               Gtk::ALIGN_CENTER, true));
98         label_widget->set_mnemonic_widget(widget);
99         label_widget->set_markup(label_widget->get_text());
100 
101         if (indent) {
102             label_widget->set_margin_start(12);
103         }
104 
105         label_widget->set_valign(Gtk::ALIGN_CENTER);
106         add(*label_widget);
107         attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1);
108     }
109 
110     // Now add the widget to the bottom of the dialog
111     if (label == "")
112     {
113         if (indent) {
114             hb->set_margin_start(12);
115         }
116 
117         add(*hb);
118 
119         GValue width = G_VALUE_INIT;
120         g_value_init(&width, G_TYPE_INT);
121         g_value_set_int(&width, 2);
122         gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(hb->gobj()), "width", &width);
123     }
124 
125     // Add a label on the right of the widget if desired
126     if (suffix != "")
127     {
128         Gtk::Label* suffix_widget = Gtk::manage(new Gtk::Label(suffix , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
129         suffix_widget->set_markup(suffix_widget->get_text());
130         hb->pack_start(*suffix_widget,false,false);
131     }
132 
133 }
134 
add_group_header(Glib::ustring name)135 void DialogPage::add_group_header(Glib::ustring name)
136 {
137     if (name != "")
138     {
139         Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring(/*"<span size='large'>*/"<b>") + name +
140                                                Glib::ustring("</b>"/*</span>"*/) , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
141 
142         label_widget->set_use_markup(true);
143         label_widget->set_valign(Gtk::ALIGN_CENTER);
144         add(*label_widget);
145     }
146 }
147 
add_group_note(Glib::ustring name)148 void DialogPage::add_group_note(Glib::ustring name)
149 {
150     if (name != "")
151     {
152         Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring("<i>") + name +
153                                                Glib::ustring("</i>") , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true));
154         label_widget->set_use_markup(true);
155         label_widget->set_valign(Gtk::ALIGN_CENTER);
156         label_widget->set_line_wrap(true);
157         label_widget->set_line_wrap_mode(Pango::WRAP_WORD);
158 
159         add(*label_widget);
160         GValue width = G_VALUE_INIT;
161         g_value_init(&width, G_TYPE_INT);
162         g_value_set_int(&width, 2);
163         gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(label_widget->gobj()), "width", &width);
164     }
165 }
166 
set_tip(Gtk::Widget & widget,Glib::ustring const & tip)167 void DialogPage::set_tip(Gtk::Widget& widget, Glib::ustring const &tip)
168 {
169     widget.set_tooltip_text (tip);
170 }
171 
init(Glib::ustring const & label,Glib::ustring const & prefs_path,bool default_value)172 void PrefCheckButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
173     bool default_value)
174 {
175     _prefs_path = prefs_path;
176     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
177     this->set_label(label);
178     this->set_active( prefs->getBool(_prefs_path, default_value) );
179 }
180 
on_toggled()181 void PrefCheckButton::on_toggled()
182 {
183     if (this->get_visible()) //only take action if the user toggled it
184     {
185         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
186         prefs->setBool(_prefs_path, this->get_active());
187     }
188     this->changed_signal.emit(this->get_active());
189 }
190 
init(Glib::ustring const & label,Glib::ustring const & prefs_path,Glib::ustring const & string_value,bool default_value,PrefRadioButton * group_member)191 void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
192     Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member)
193 {
194     _prefs_path = prefs_path;
195     _value_type = VAL_STRING;
196     _string_value = string_value;
197     (void)default_value;
198     this->set_label(label);
199     if (group_member)
200     {
201         Gtk::RadioButtonGroup rbg = group_member->get_group();
202         this->set_group(rbg);
203     }
204     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
205     Glib::ustring val = prefs->getString(_prefs_path);
206     if ( !val.empty() )
207         this->set_active(val == _string_value);
208     else
209         this->set_active( false );
210 }
211 
init(Glib::ustring const & label,Glib::ustring const & prefs_path,int int_value,bool default_value,PrefRadioButton * group_member)212 void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
213     int int_value, bool default_value, PrefRadioButton* group_member)
214 {
215     _prefs_path = prefs_path;
216     _value_type = VAL_INT;
217     _int_value = int_value;
218     this->set_label(label);
219     if (group_member)
220     {
221         Gtk::RadioButtonGroup rbg = group_member->get_group();
222         this->set_group(rbg);
223     }
224     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
225     if (default_value)
226         this->set_active( prefs->getInt(_prefs_path, int_value) == _int_value );
227     else
228         this->set_active( prefs->getInt(_prefs_path, int_value + 1) == _int_value );
229 }
230 
on_toggled()231 void PrefRadioButton::on_toggled()
232 {
233     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
234 
235     if (this->get_visible() && this->get_active() ) //only take action if toggled by user (to active)
236     {
237         if ( _value_type == VAL_STRING )
238             prefs->setString(_prefs_path, _string_value);
239         else if ( _value_type == VAL_INT )
240             prefs->setInt(_prefs_path, _int_value);
241     }
242     this->changed_signal.emit(this->get_active());
243 }
244 
init(Glib::ustring const & prefs_path,double lower,double upper,double step_increment,double,double default_value,bool is_int,bool is_percent)245 void PrefSpinButton::init(Glib::ustring const &prefs_path,
246               double lower, double upper, double step_increment, double /*page_increment*/,
247               double default_value, bool is_int, bool is_percent)
248 {
249     _prefs_path = prefs_path;
250     _is_int = is_int;
251     _is_percent = is_percent;
252     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
253     double value;
254     if (is_int) {
255         if (is_percent) {
256             value = 100 * prefs->getDoubleLimited(prefs_path, default_value, lower/100.0, upper/100.0);
257         } else {
258             value = (double) prefs->getIntLimited(prefs_path, (int) default_value, (int) lower, (int) upper);
259         }
260     } else {
261         value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
262     }
263 
264     this->set_range (lower, upper);
265     this->set_increments (step_increment, 0);
266     this->set_value (value);
267     this->set_width_chars(6);
268     if (is_int)
269         this->set_digits(0);
270     else if (step_increment < 0.1)
271         this->set_digits(4);
272     else
273         this->set_digits(2);
274 
275 }
276 
on_value_changed()277 void PrefSpinButton::on_value_changed()
278 {
279     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
280     if (this->get_visible()) //only take action if user changed value
281     {
282         if (_is_int) {
283             if (_is_percent) {
284                 prefs->setDouble(_prefs_path, this->get_value()/100.0);
285             } else {
286                 prefs->setInt(_prefs_path, (int) this->get_value());
287             }
288         } else {
289             prefs->setDouble(_prefs_path, this->get_value());
290         }
291     }
292     this->changed_signal.emit(this->get_value());
293 }
294 
init(Glib::ustring const & prefs_path,double lower,double upper,double step_increment,double default_value,UnitType unit_type,Glib::ustring const & default_unit)295 void PrefSpinUnit::init(Glib::ustring const &prefs_path,
296               double lower, double upper, double step_increment,
297               double default_value, UnitType unit_type, Glib::ustring const &default_unit)
298 {
299     _prefs_path = prefs_path;
300     _is_percent = (unit_type == UNIT_TYPE_DIMENSIONLESS);
301 
302     resetUnitType(unit_type);
303     setUnit(default_unit);
304     setRange (lower, upper); /// @fixme  this disregards changes of units
305     setIncrements (step_increment, 0);
306     if (step_increment < 0.1) {
307         setDigits(4);
308     } else {
309         setDigits(2);
310     }
311 
312     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
313     double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
314     Glib::ustring unitstr = prefs->getUnit(prefs_path);
315     if (unitstr.length() == 0) {
316         unitstr = default_unit;
317         // write the assumed unit to preferences:
318         prefs->setDoubleUnit(_prefs_path, value, unitstr);
319     }
320     setValue(value, unitstr);
321 
322     signal_value_changed().connect_notify(sigc::mem_fun(*this, &PrefSpinUnit::on_my_value_changed));
323 }
324 
on_my_value_changed()325 void PrefSpinUnit::on_my_value_changed()
326 {
327     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
328     if (getWidget()->get_visible()) //only take action if user changed value
329     {
330         prefs->setDoubleUnit(_prefs_path, getValue(getUnit()->abbr), getUnit()->abbr);
331     }
332 }
333 
334 const double ZoomCorrRuler::textsize = 7;
335 const double ZoomCorrRuler::textpadding = 5;
336 
ZoomCorrRuler(int width,int height)337 ZoomCorrRuler::ZoomCorrRuler(int width, int height) :
338     _unitconv(1.0),
339     _border(5)
340 {
341     set_size(width, height);
342 }
343 
set_size(int x,int y)344 void ZoomCorrRuler::set_size(int x, int y)
345 {
346     _min_width = x;
347     _height = y;
348     set_size_request(x + _border*2, y + _border*2);
349 }
350 
351 // The following two functions are borrowed from 2geom's toy-framework-2; if they are useful in
352 // other locations, we should perhaps make them (or adapted versions of them) publicly available
353 static void
draw_text(cairo_t * cr,Geom::Point loc,const char * txt,bool bottom=false,double fontsize=ZoomCorrRuler::textsize,std::string fontdesc="Sans")354 draw_text(cairo_t *cr, Geom::Point loc, const char* txt, bool bottom = false,
355           double fontsize = ZoomCorrRuler::textsize, std::string fontdesc = "Sans") {
356     PangoLayout* layout = pango_cairo_create_layout (cr);
357     pango_layout_set_text(layout, txt, -1);
358 
359     // set font and size
360     std::ostringstream sizestr;
361     sizestr << fontsize;
362     fontdesc = fontdesc + " " + sizestr.str();
363     PangoFontDescription *font_desc = pango_font_description_from_string(fontdesc.c_str());
364     pango_layout_set_font_description(layout, font_desc);
365     pango_font_description_free (font_desc);
366 
367     PangoRectangle logical_extent;
368     pango_layout_get_pixel_extents(layout, nullptr, &logical_extent);
369     cairo_move_to(cr, loc[Geom::X], loc[Geom::Y] - (bottom ? logical_extent.height : 0));
370     pango_cairo_show_layout(cr, layout);
371 }
372 
373 static void
draw_number(cairo_t * cr,Geom::Point pos,double num)374 draw_number(cairo_t *cr, Geom::Point pos, double num) {
375     std::ostringstream number;
376     number << num;
377     draw_text(cr, pos, number.str().c_str(), true);
378 }
379 
380 /*
381  * \arg dist The distance between consecutive minor marks
382  * \arg major_interval Number of marks after which to draw a major mark
383  */
384 void
draw_marks(Cairo::RefPtr<Cairo::Context> cr,double dist,int major_interval)385 ZoomCorrRuler::draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval) {
386     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
387     const double zoomcorr = prefs->getDouble("/options/zoomcorrection/value", 1.0);
388     double mark = 0;
389     int i = 0;
390     while (mark <= _drawing_width) {
391         cr->move_to(mark, _height);
392         if ((i % major_interval) == 0) {
393             // major mark
394             cr->line_to(mark, 0);
395             Geom::Point textpos(mark + 3, ZoomCorrRuler::textsize + ZoomCorrRuler::textpadding);
396             draw_number(cr->cobj(), textpos, dist * i);
397         } else {
398             // minor mark
399             cr->line_to(mark, ZoomCorrRuler::textsize + 2 * ZoomCorrRuler::textpadding);
400         }
401         mark += dist * zoomcorr / _unitconv;
402         ++i;
403     }
404 }
405 
406 bool
on_draw(const Cairo::RefPtr<Cairo::Context> & cr)407 ZoomCorrRuler::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
408     Glib::RefPtr<Gdk::Window> window = get_window();
409 
410     int w = window->get_width();
411     _drawing_width = w - _border * 2;
412 
413     cr->set_source_rgb(1.0, 1.0, 1.0);
414     cr->set_fill_rule(Cairo::FILL_RULE_WINDING);
415     cr->rectangle(0, 0, w, _height + _border*2);
416     cr->fill();
417 
418     cr->set_source_rgb(0.0, 0.0, 0.0);
419     cr->set_line_width(0.5);
420 
421     cr->translate(_border, _border); // so that we have a small white border around the ruler
422     cr->move_to (0, _height);
423     cr->line_to (_drawing_width, _height);
424 
425     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
426     Glib::ustring abbr = prefs->getString("/options/zoomcorrection/unit");
427     if (abbr == "cm") {
428         draw_marks(cr, 0.1, 10);
429     } else if (abbr == "in") {
430         draw_marks(cr, 0.25, 4);
431     } else if (abbr == "mm") {
432         draw_marks(cr, 10, 10);
433     } else if (abbr == "pc") {
434         draw_marks(cr, 1, 10);
435     } else if (abbr == "pt") {
436         draw_marks(cr, 10, 10);
437     } else if (abbr == "px") {
438         draw_marks(cr, 10, 10);
439     } else {
440         draw_marks(cr, 1, 1);
441     }
442     cr->stroke();
443 
444     return true;
445 }
446 
447 
448 void
on_slider_value_changed()449 ZoomCorrRulerSlider::on_slider_value_changed()
450 {
451     if (this->get_visible() || freeze) //only take action if user changed value
452     {
453         freeze = true;
454         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
455         prefs->setDouble("/options/zoomcorrection/value", _slider->get_value() / 100.0);
456         _sb->set_value(_slider->get_value());
457         _ruler.queue_draw();
458         freeze = false;
459     }
460 }
461 
462 void
on_spinbutton_value_changed()463 ZoomCorrRulerSlider::on_spinbutton_value_changed()
464 {
465     if (this->get_visible() || freeze) //only take action if user changed value
466     {
467         freeze = true;
468         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
469         prefs->setDouble("/options/zoomcorrection/value", _sb->get_value() / 100.0);
470         _slider->set_value(_sb->get_value());
471         _ruler.queue_draw();
472         freeze = false;
473     }
474 }
475 
476 void
on_unit_changed()477 ZoomCorrRulerSlider::on_unit_changed() {
478     if (!_unit.get_sensitive()) {
479         // when the unit menu is initialized, the unit is set to the default but
480         // it needs to be reset later so we don't perform the change in this case
481         return;
482     }
483     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
484     prefs->setString("/options/zoomcorrection/unit", _unit.getUnitAbbr());
485     double conv = _unit.getConversion(_unit.getUnitAbbr(), "px");
486     _ruler.set_unit_conversion(conv);
487     if (_ruler.get_visible()) {
488         _ruler.queue_draw();
489     }
490 }
491 
on_mnemonic_activate(bool group_cycling)492 bool ZoomCorrRulerSlider::on_mnemonic_activate ( bool group_cycling )
493 {
494     return _sb->mnemonic_activate ( group_cycling );
495 }
496 
497 
498 void
init(int ruler_width,int ruler_height,double lower,double upper,double step_increment,double page_increment,double default_value)499 ZoomCorrRulerSlider::init(int ruler_width, int ruler_height, double lower, double upper,
500                       double step_increment, double page_increment, double default_value)
501 {
502     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
503     double value = prefs->getDoubleLimited("/options/zoomcorrection/value", default_value, lower, upper) * 100.0;
504 
505     freeze = false;
506 
507     _ruler.set_size(ruler_width, ruler_height);
508 
509     _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
510 
511     _slider->set_size_request(_ruler.width(), -1);
512     _slider->set_range (lower, upper);
513     _slider->set_increments (step_increment, page_increment);
514     _slider->set_value (value);
515     _slider->set_digits(2);
516 
517     _slider->signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_slider_value_changed));
518     _sb = Gtk::manage(new Inkscape::UI::Widget::SpinButton());
519     _sb->signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_spinbutton_value_changed));
520     _unit.signal_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_unit_changed));
521 
522     _sb->set_range (lower, upper);
523     _sb->set_increments (step_increment, 0);
524     _sb->set_value (value);
525     _sb->set_digits(2);
526     _sb->set_halign(Gtk::ALIGN_CENTER);
527     _sb->set_valign(Gtk::ALIGN_END);
528 
529     _unit.set_sensitive(false);
530     _unit.setUnitType(UNIT_TYPE_LINEAR);
531     _unit.set_sensitive(true);
532     _unit.setUnit(prefs->getString("/options/zoomcorrection/unit"));
533     _unit.set_halign(Gtk::ALIGN_CENTER);
534     _unit.set_valign(Gtk::ALIGN_END);
535 
536     _slider->set_hexpand(true);
537     _ruler.set_hexpand(true);
538     auto table = Gtk::manage(new Gtk::Grid());
539     table->attach(*_slider, 0, 0, 1, 1);
540     table->attach(*_sb,      1, 0, 1, 1);
541     table->attach(_ruler,   0, 1, 1, 1);
542     table->attach(_unit,    1, 1, 1, 1);
543 
544     pack_start(*table, Gtk::PACK_SHRINK);
545 }
546 
547 void
on_slider_value_changed()548 PrefSlider::on_slider_value_changed()
549 {
550     if (this->get_visible() || freeze) //only take action if user changed value
551     {
552         freeze = true;
553         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
554         prefs->setDouble(_prefs_path, _slider->get_value());
555         _sb->set_value(_slider->get_value());
556         freeze = false;
557     }
558 }
559 
560 void
on_spinbutton_value_changed()561 PrefSlider::on_spinbutton_value_changed()
562 {
563     if (this->get_visible() || freeze) //only take action if user changed value
564     {
565         freeze = true;
566         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
567         prefs->setDouble(_prefs_path, _sb->get_value());
568         _slider->set_value(_sb->get_value());
569         freeze = false;
570     }
571 }
572 
on_mnemonic_activate(bool group_cycling)573 bool PrefSlider::on_mnemonic_activate ( bool group_cycling )
574 {
575     return _sb->mnemonic_activate ( group_cycling );
576 }
577 
578 void
init(Glib::ustring const & prefs_path,double lower,double upper,double step_increment,double page_increment,double default_value,int digits)579 PrefSlider::init(Glib::ustring const &prefs_path,
580                  double lower, double upper, double step_increment, double page_increment, double default_value, int digits)
581 {
582     _prefs_path = prefs_path;
583 
584     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
585     double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper);
586 
587     freeze = false;
588 
589     _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL));
590 
591     _slider->set_range (lower, upper);
592     _slider->set_increments (step_increment, page_increment);
593     _slider->set_value (value);
594     _slider->set_digits(digits);
595     _slider->signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_slider_value_changed));
596     _sb = Gtk::manage(new Inkscape::UI::Widget::SpinButton());
597     _sb->signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_spinbutton_value_changed));
598     _sb->set_range (lower, upper);
599     _sb->set_increments (step_increment, 0);
600     _sb->set_value (value);
601     _sb->set_digits(digits);
602     _sb->set_halign(Gtk::ALIGN_CENTER);
603     _sb->set_valign(Gtk::ALIGN_END);
604 
605     auto table = Gtk::manage(new Gtk::Grid());
606     _slider->set_hexpand();
607     table->attach(*_slider, 0, 0, 1, 1);
608     table->attach(*_sb,     1, 0, 1, 1);
609 
610     this->pack_start(*table, Gtk::PACK_EXPAND_WIDGET);
611 }
612 
init(Glib::ustring const & prefs_path,Glib::ustring labels[],int values[],int num_items,int default_value)613 void PrefCombo::init(Glib::ustring const &prefs_path,
614                      Glib::ustring labels[], int values[], int num_items, int default_value)
615 {
616     _prefs_path = prefs_path;
617     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
618     int row = 0;
619     int value = prefs->getInt(_prefs_path, default_value);
620 
621     for (int i = 0 ; i < num_items; ++i)
622     {
623         this->append(labels[i]);
624         _values.push_back(values[i]);
625         if (value == values[i])
626             row = i;
627     }
628     this->set_active(row);
629 }
630 
init(Glib::ustring const & prefs_path,Glib::ustring labels[],Glib::ustring values[],int num_items,Glib::ustring default_value)631 void PrefCombo::init(Glib::ustring const &prefs_path,
632                      Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value)
633 {
634     _prefs_path = prefs_path;
635     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
636     int row = 0;
637     Glib::ustring value = prefs->getString(_prefs_path);
638     if(value.empty())
639     {
640         value = default_value;
641     }
642 
643     for (int i = 0 ; i < num_items; ++i)
644     {
645         this->append(labels[i]);
646         _ustr_values.push_back(values[i]);
647         if (value == values[i])
648             row = i;
649     }
650     this->set_active(row);
651 }
652 
init(Glib::ustring const & prefs_path,std::vector<Glib::ustring> labels,std::vector<int> values,int default_value)653 void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values,
654                      int default_value)
655 {
656     size_t labels_size = labels.size();
657     size_t values_size = values.size();
658     if (values_size != labels_size) {
659         std::cout << "PrefCombo::"
660                   << "Different number of values/labels in " << prefs_path << std::endl;
661         return;
662     }
663     _prefs_path = prefs_path;
664     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
665     int row = 0;
666     int value = prefs->getInt(_prefs_path, default_value);
667 
668     for (int i = 0; i < labels_size; ++i) {
669         this->append(labels[i]);
670         _values.push_back(values[i]);
671         if (value == values[i])
672             row = i;
673     }
674     this->set_active(row);
675 }
676 
init(Glib::ustring const & prefs_path,std::vector<Glib::ustring> labels,std::vector<Glib::ustring> values,Glib::ustring default_value)677 void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels,
678                      std::vector<Glib::ustring> values, Glib::ustring default_value)
679 {
680     size_t labels_size = labels.size();
681     size_t values_size = values.size();
682     if (values_size != labels_size) {
683         std::cout << "PrefCombo::"
684                   << "Different number of values/labels in " << prefs_path << std::endl;
685         return;
686     }
687     _prefs_path = prefs_path;
688     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
689     int row = 0;
690     Glib::ustring value = prefs->getString(_prefs_path);
691     if (value.empty()) {
692         value = default_value;
693     }
694 
695     for (int i = 0; i < labels_size; ++i) {
696         this->append(labels[i]);
697         _ustr_values.push_back(values[i]);
698         if (value == values[i])
699             row = i;
700     }
701     this->set_active(row);
702 }
703 
init(Glib::ustring const & prefs_path,std::vector<std::pair<Glib::ustring,Glib::ustring>> labels_and_values,Glib::ustring default_value)704 void PrefCombo::init(Glib::ustring const &prefs_path,
705                      std::vector<std::pair<Glib::ustring, Glib::ustring>> labels_and_values,
706                      Glib::ustring default_value)
707 {
708     _prefs_path = prefs_path;
709     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
710     Glib::ustring value = prefs->getString(_prefs_path);
711     if (value.empty()) {
712         value = default_value;
713     }
714 
715     int row = 0;
716     int i = 0;
717     for (auto entry : labels_and_values) {
718         this->append(entry.first);
719         _ustr_values.push_back(entry.second);
720         if (value == entry.second) {
721             row = i;
722         }
723         ++i;
724     }
725     this->set_active(row);
726 }
727 
on_changed()728 void PrefCombo::on_changed()
729 {
730     if (this->get_visible()) //only take action if user changed value
731     {
732         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
733         if(!_values.empty())
734         {
735             prefs->setInt(_prefs_path, _values[this->get_active_row_number()]);
736         }
737         else
738         {
739             prefs->setString(_prefs_path, _ustr_values[this->get_active_row_number()]);
740         }
741     }
742 }
743 
init(Glib::ustring const & prefs_path,bool visibility,Glib::ustring const & default_string)744 void PrefEntryButtonHBox::init(Glib::ustring const &prefs_path,
745             bool visibility, Glib::ustring const &default_string)
746 {
747     _prefs_path = prefs_path;
748     _default_string = default_string;
749     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
750     relatedEntry = new Gtk::Entry();
751     relatedButton = new Gtk::Button(_("Reset"));
752     relatedEntry->set_invisible_char('*');
753     relatedEntry->set_visibility(visibility);
754     relatedEntry->set_text(prefs->getString(_prefs_path));
755     this->pack_start(*relatedEntry);
756     this->pack_start(*relatedButton);
757     relatedButton->signal_clicked().connect(
758             sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedButtonClickedCallback));
759     relatedEntry->signal_changed().connect(
760             sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedEntryChangedCallback));
761 }
762 
onRelatedEntryChangedCallback()763 void PrefEntryButtonHBox::onRelatedEntryChangedCallback()
764 {
765     if (this->get_visible()) //only take action if user changed value
766     {
767         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
768         prefs->setString(_prefs_path, relatedEntry->get_text());
769     }
770 }
771 
onRelatedButtonClickedCallback()772 void PrefEntryButtonHBox::onRelatedButtonClickedCallback()
773 {
774     if (this->get_visible()) //only take action if user changed value
775     {
776         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
777         prefs->setString(_prefs_path, _default_string);
778         relatedEntry->set_text(_default_string);
779     }
780 }
781 
on_mnemonic_activate(bool group_cycling)782 bool PrefEntryButtonHBox::on_mnemonic_activate ( bool group_cycling )
783 {
784     return relatedEntry->mnemonic_activate ( group_cycling );
785 }
786 
init(Glib::ustring const & prefs_path,bool visibility)787 void PrefEntryFileButtonHBox::init(Glib::ustring const &prefs_path,
788             bool visibility)
789 {
790     _prefs_path = prefs_path;
791     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
792 
793     relatedEntry = new Gtk::Entry();
794     relatedEntry->set_invisible_char('*');
795     relatedEntry->set_visibility(visibility);
796     relatedEntry->set_text(prefs->getString(_prefs_path));
797 
798     relatedButton = new Gtk::Button();
799     Gtk::Box* pixlabel = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 3);
800     Gtk::Image *im = sp_get_icon_image("applications-graphics", Gtk::ICON_SIZE_BUTTON);
801     pixlabel->pack_start(*im);
802     Gtk::Label *l = new Gtk::Label();
803     l->set_markup_with_mnemonic(_("_Browse..."));
804     pixlabel->pack_start(*l);
805     relatedButton->add(*pixlabel);
806 
807     this->pack_end(*relatedButton, false, false, 4);
808     this->pack_start(*relatedEntry, true, true, 0);
809 
810     relatedButton->signal_clicked().connect(
811             sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedButtonClickedCallback));
812     relatedEntry->signal_changed().connect(
813             sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedEntryChangedCallback));
814 }
815 
onRelatedEntryChangedCallback()816 void PrefEntryFileButtonHBox::onRelatedEntryChangedCallback()
817 {
818     if (this->get_visible()) //only take action if user changed value
819     {
820         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
821         prefs->setString(_prefs_path, relatedEntry->get_text());
822     }
823 }
824 
825 static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr;
826 
onRelatedButtonClickedCallback()827 void PrefEntryFileButtonHBox::onRelatedButtonClickedCallback()
828 {
829     if (this->get_visible()) //only take action if user changed value
830     {
831         //# Get the current directory for finding files
832         static Glib::ustring open_path;
833         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
834 
835 
836         Glib::ustring attr = prefs->getString(_prefs_path);
837         if (!attr.empty()) open_path = attr;
838 
839         //# Test if the open_path directory exists
840         if (!Inkscape::IO::file_test(open_path.c_str(),
841                   (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)))
842             open_path = "";
843 
844 #ifdef _WIN32
845         //# If no open path, default to our win32 documents folder
846         if (open_path.empty())
847         {
848             // The path to the My Documents folder is read from the
849             // value "HKEY_CURRENT_USER\Software\Windows\CurrentVersion\Explorer\Shell Folders\Personal"
850             HKEY key = NULL;
851             if(RegOpenKeyExA(HKEY_CURRENT_USER,
852                 "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders",
853                 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS)
854             {
855                 WCHAR utf16path[_MAX_PATH];
856                 DWORD value_type;
857                 DWORD data_size = sizeof(utf16path);
858                 if(RegQueryValueExW(key, L"Personal", NULL, &value_type,
859                     (BYTE*)utf16path, &data_size) == ERROR_SUCCESS)
860                 {
861                     g_assert(value_type == REG_SZ);
862                     gchar *utf8path = g_utf16_to_utf8(
863                         (const gunichar2*)utf16path, -1, NULL, NULL, NULL);
864                     if(utf8path)
865                     {
866                         open_path = Glib::ustring(utf8path);
867                         g_free(utf8path);
868                     }
869                 }
870             }
871         }
872 #endif
873 
874         //# If no open path, default to our home directory
875         if (open_path.empty())
876         {
877             open_path = g_get_home_dir();
878             open_path.append(G_DIR_SEPARATOR_S);
879         }
880 
881         //# Create a dialog
882         SPDesktop *desktop = SP_ACTIVE_DESKTOP;
883         if (!selectPrefsFileInstance) {
884         selectPrefsFileInstance =
885               Inkscape::UI::Dialog::FileOpenDialog::create(
886                  *desktop->getToplevel(),
887                  open_path,
888                  Inkscape::UI::Dialog::EXE_TYPES,
889                  _("Select a bitmap editor"));
890         }
891 
892         //# Show the dialog
893         bool const success = selectPrefsFileInstance->show();
894 
895         if (!success) {
896             return;
897         }
898 
899         //# User selected something.  Get name and type
900         Glib::ustring fileName = selectPrefsFileInstance->getFilename();
901 
902         if (!fileName.empty())
903         {
904             Glib::ustring newFileName = Glib::filename_to_utf8(fileName);
905 
906             if ( newFileName.size() > 0)
907                 open_path = newFileName;
908             else
909                 g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" );
910 
911             prefs->setString(_prefs_path, open_path);
912         }
913 
914         relatedEntry->set_text(fileName);
915     }
916 }
917 
on_mnemonic_activate(bool group_cycling)918 bool PrefEntryFileButtonHBox::on_mnemonic_activate ( bool group_cycling )
919 {
920     return relatedEntry->mnemonic_activate ( group_cycling );
921 }
922 
init(Glib::ustring const & entry_string,Glib::ustring const & tooltip)923 void PrefOpenFolder::init(Glib::ustring const &entry_string, Glib::ustring const &tooltip)
924 {
925     relatedEntry = new Gtk::Entry();
926     relatedButton = new Gtk::Button();
927     Gtk::Box *pixlabel = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 3);
928     Gtk::Image *im = sp_get_icon_image("document-open", Gtk::ICON_SIZE_BUTTON);
929     pixlabel->pack_start(*im);
930     Gtk::Label *l = new Gtk::Label();
931     l->set_markup_with_mnemonic(_("Open"));
932     pixlabel->pack_start(*l);
933     relatedButton->add(*pixlabel);
934     relatedButton->set_tooltip_text(tooltip);
935     relatedEntry->set_text(entry_string);
936     relatedEntry->set_sensitive(false);
937     this->pack_end(*relatedButton, false, false, 4);
938     this->pack_start(*relatedEntry, true, true, 0);
939     relatedButton->signal_clicked().connect(sigc::mem_fun(*this, &PrefOpenFolder::onRelatedButtonClickedCallback));
940 }
941 
onRelatedButtonClickedCallback()942 void PrefOpenFolder::onRelatedButtonClickedCallback()
943 {
944     g_mkdir_with_parents(relatedEntry->get_text().c_str(), 0700);
945     // https://stackoverflow.com/questions/42442189/how-to-open-spawn-a-file-with-glib-gtkmm-in-windows
946 #ifdef _WIN32
947     ShellExecute(NULL, "open", relatedEntry->get_text().c_str(), NULL, NULL, SW_SHOWDEFAULT);
948 #elif defined(__APPLE__)
949     std::vector<std::string> argv = { "open", relatedEntry->get_text().raw() };
950     Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH);
951 #else
952     gchar *path = g_filename_to_uri(relatedEntry->get_text().c_str(), NULL, NULL);
953     std::vector<std::string> argv = { "xdg-open", path };
954     Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH);
955     g_free(path);
956 #endif
957 }
958 
init(Glib::ustring const & prefs_path)959 void PrefFileButton::init(Glib::ustring const &prefs_path)
960 {
961     _prefs_path = prefs_path;
962     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
963     select_filename(Glib::filename_from_utf8(prefs->getString(_prefs_path)));
964 
965     signal_selection_changed().connect(sigc::mem_fun(*this, &PrefFileButton::onFileChanged));
966 }
967 
onFileChanged()968 void PrefFileButton::onFileChanged()
969 {
970     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
971     prefs->setString(_prefs_path, Glib::filename_to_utf8(get_filename()));
972 }
973 
init(Glib::ustring const & prefs_path,bool visibility)974 void PrefEntry::init(Glib::ustring const &prefs_path, bool visibility)
975 {
976     _prefs_path = prefs_path;
977     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
978     this->set_invisible_char('*');
979     this->set_visibility(visibility);
980     this->set_text(prefs->getString(_prefs_path));
981 }
982 
on_changed()983 void PrefEntry::on_changed()
984 {
985     if (this->get_visible()) //only take action if user changed value
986     {
987         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
988         prefs->setString(_prefs_path, this->get_text());
989     }
990 }
991 
init(Glib::ustring const & prefs_path,int height)992 void PrefMultiEntry::init(Glib::ustring const &prefs_path, int height)
993 {
994     // TODO: Figure out if there's a way to specify height in lines instead of px
995     //       and how to obtain a reasonable default width if 'expand_widget' is not used
996     set_size_request(100, height);
997     set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
998     set_shadow_type(Gtk::SHADOW_IN);
999 
1000     add(_text);
1001 
1002     _prefs_path = prefs_path;
1003     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1004     Glib::ustring value = prefs->getString(_prefs_path);
1005     value = Glib::Regex::create("\\|")->replace_literal(value, 0, "\n", (Glib::RegexMatchFlags)0);
1006     _text.get_buffer()->set_text(value);
1007     _text.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &PrefMultiEntry::on_changed));
1008 }
1009 
on_changed()1010 void PrefMultiEntry::on_changed()
1011 {
1012     if (get_visible()) //only take action if user changed value
1013     {
1014         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1015         Glib::ustring value = _text.get_buffer()->get_text();
1016         value = Glib::Regex::create("\\n")->replace_literal(value, 0, "|", (Glib::RegexMatchFlags)0);
1017         prefs->setString(_prefs_path, value);
1018     }
1019 }
1020 
init(Glib::ustring const & label,Glib::ustring const & prefs_path,guint32 default_rgba)1021 void PrefColorPicker::init(Glib::ustring const &label, Glib::ustring const &prefs_path,
1022                            guint32 default_rgba)
1023 {
1024     _prefs_path = prefs_path;
1025     _title = label;
1026     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1027     this->setRgba32( prefs->getInt(_prefs_path, (int)default_rgba) );
1028 }
1029 
on_changed(guint32 rgba)1030 void PrefColorPicker::on_changed (guint32 rgba)
1031 {
1032     if (this->get_visible()) //only take action if the user toggled it
1033     {
1034         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1035         prefs->setInt(_prefs_path, (int) rgba);
1036     }
1037 }
1038 
init(Glib::ustring const & prefs_path)1039 void PrefUnit::init(Glib::ustring const &prefs_path)
1040 {
1041     _prefs_path = prefs_path;
1042     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1043     setUnitType(UNIT_TYPE_LINEAR);
1044     setUnit(prefs->getString(_prefs_path));
1045 }
1046 
on_changed()1047 void PrefUnit::on_changed()
1048 {
1049     if (this->get_visible()) //only take action if user changed value
1050     {
1051         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
1052         prefs->setString(_prefs_path, getUnitAbbr());
1053     }
1054 }
1055 
1056 } // namespace Widget
1057 } // namespace UI
1058 } // namespace Inkscape
1059 
1060 /*
1061   Local Variables:
1062   mode:c++
1063   c-file-style:"stroustrup"
1064   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1065   indent-tabs-mode:nil
1066   fill-column:99
1067   End:
1068 */
1069 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
1070