1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /* Authors:
3  *   Lauris Kaplinski <lauris@kaplinski.com>
4  *   Bryce Harrington <brycehar@bryceharrington.org>
5  *   bulia byak <buliabyak@users.sf.net>
6  *   Maximilian Albert <maximilian.albert@gmail.com>
7  *   Josh Andler <scislac@users.sf.net>
8  *   Jon A. Cruz <jon@joncruz.org>
9  *   Abhishek Sharma
10  *
11  * Copyright (C) 2001-2005 authors
12  * Copyright (C) 2001 Ximian, Inc.
13  * Copyright (C) 2004 John Cliff
14  * Copyright (C) 2008 Maximilian Albert (gtkmm-ification)
15  *
16  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
17  */
18 
19 #define noSP_SS_VERBOSE
20 
21 #include "stroke-style.h"
22 
23 #include "object/sp-marker.h"
24 #include "object/sp-namedview.h"
25 #include "object/sp-rect.h"
26 #include "object/sp-stop.h"
27 #include "object/sp-text.h"
28 
29 #include "svg/svg-color.h"
30 
31 #include "ui/icon-loader.h"
32 #include "ui/widget/dash-selector.h"
33 #include "ui/widget/marker-combo-box.h"
34 #include "ui/widget/unit-menu.h"
35 
36 #include "widgets/style-utils.h"
37 
38 using Inkscape::DocumentUndo;
39 using Inkscape::Util::unit_table;
40 
41 /**
42  * Extract the actual name of the link
43  * e.g. get mTriangle from url(#mTriangle).
44  * \return Buffer containing the actual name, allocated from GLib;
45  * the caller should free the buffer when they no longer need it.
46  */
getMarkerObj(gchar const * n,SPDocument * doc)47 SPObject* getMarkerObj(gchar const *n, SPDocument *doc)
48 {
49     gchar const *p = n;
50     while (*p != '\0' && *p != '#') {
51         p++;
52     }
53 
54     if (*p == '\0' || p[1] == '\0') {
55         return nullptr;
56     }
57 
58     p++;
59     int c = 0;
60     while (p[c] != '\0' && p[c] != ')') {
61         c++;
62     }
63 
64     if (p[c] == '\0') {
65         return nullptr;
66     }
67 
68     gchar* b = g_strdup(p);
69     b[c] = '\0';
70 
71     // FIXME: get the document from the object and let the caller pass it in
72     SPObject *marker = doc->getObjectById(b);
73 
74     g_free(b);
75     return marker;
76 }
77 
78 namespace Inkscape {
79 namespace UI {
80 namespace Widget {
81 
82 /**
83  * Construct a stroke-style radio button with a given icon
84  *
85  * \param[in] grp          The Gtk::RadioButtonGroup to which to add the new button
86  * \param[in] icon         The icon to use for the button
87  * \param[in] button_type  The type of stroke-style radio button (join/cap)
88  * \param[in] stroke_style The style attribute to associate with the button
89  */
StrokeStyleButton(Gtk::RadioButtonGroup & grp,char const * icon,StrokeStyleButtonType button_type,gchar const * stroke_style)90 StrokeStyle::StrokeStyleButton::StrokeStyleButton(Gtk::RadioButtonGroup &grp,
91                                                   char const            *icon,
92                                                   StrokeStyleButtonType  button_type,
93                                                   gchar const           *stroke_style)
94     :
95         Gtk::RadioButton(grp),
96         button_type(button_type),
97         stroke_style(stroke_style)
98 {
99     show();
100     set_mode(false);
101 
102     auto px = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR));
103     g_assert(px != nullptr);
104     px->show();
105     add(*px);
106 }
107 
StrokeStyle()108 StrokeStyle::StrokeStyle() :
109     Gtk::Box(),
110     miterLimitSpin(),
111     widthSpin(),
112     unitSelector(),
113     joinMiter(),
114     joinRound(),
115     joinBevel(),
116     capButt(),
117     capRound(),
118     capSquare(),
119     dashSelector(),
120     update(false),
121     desktop(nullptr),
122     selectChangedConn(),
123     selectModifiedConn(),
124     startMarkerConn(),
125     midMarkerConn(),
126     endMarkerConn(),
127     _old_unit(nullptr)
128 {
129     table = new Gtk::Grid();
130     table->set_border_width(4);
131     table->set_row_spacing(4);
132     table->set_hexpand(false);
133     table->set_halign(Gtk::ALIGN_CENTER);
134     table->show();
135     add(*table);
136 
137     Gtk::Box *hb;
138     gint i = 0;
139 
140     //spw_label(t, C_("Stroke width", "_Width:"), 0, i);
141 
142     hb = spw_hbox(table, 3, 1, i);
143 
144 // TODO: when this is gtkmmified, use an Inkscape::UI::Widget::ScalarUnit instead of the separate
145 // spinbutton and unit selector for stroke width. In sp_stroke_style_line_update, use
146 // setHundredPercent to remember the averaged width corresponding to 100%. Then the
147 // stroke_width_set_unit will be removed (because ScalarUnit takes care of conversions itself), and
148 // with it, the two remaining calls of stroke_average_width, allowing us to get rid of that
149 // function in desktop-style.
150     widthAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(1.0, 0.0, 1000.0, 0.1, 10.0, 0.0));
151     widthSpin = new Inkscape::UI::Widget::SpinButton(*widthAdj, 0.1, 3);
152     widthSpin->set_tooltip_text(_("Stroke width"));
153     widthSpin->show();
154     spw_label(table, C_("Stroke width", "_Width:"), 0, i, widthSpin);
155 
156     sp_dialog_defocus_on_enter_cpp(widthSpin);
157 
158     hb->pack_start(*widthSpin, false, false, 0);
159     unitSelector = new Inkscape::UI::Widget::UnitMenu();
160     unitSelector->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR);
161     Gtk::Widget *us = Gtk::manage(unitSelector);
162     SPDesktop *desktop = SP_ACTIVE_DESKTOP;
163 
164     unitSelector->addUnit(*unit_table.getUnit("%"));
165     unitSelector->append("hairline", _("Hairline"));
166     _old_unit = unitSelector->getUnit();
167     if (desktop) {
168         unitSelector->setUnit(desktop->getNamedView()->display_units->abbr);
169         _old_unit = desktop->getNamedView()->display_units;
170     }
171     widthSpin->setUnitMenu(unitSelector);
172     unitChangedConn = unitSelector->signal_changed().connect(sigc::mem_fun(*this, &StrokeStyle::unitChangedCB));
173 
174     us->show();
175 
176     hb->pack_start(*us, FALSE, FALSE, 0);
177     (*widthAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::widthChangedCB));
178 
179     i++;
180 
181     /* Dash */
182     spw_label(table, _("Dashes:"), 0, i, nullptr); //no mnemonic for now
183                                             //decide what to do:
184                                             //   implement a set_mnemonic_source function in the
185                                             //   Inkscape::UI::Widget::DashSelector class, so that we do not have to
186                                             //   expose any of the underlying widgets?
187     dashSelector = Gtk::manage(new Inkscape::UI::Widget::DashSelector);
188 
189     dashSelector->show();
190     dashSelector->set_hexpand();
191     dashSelector->set_halign(Gtk::ALIGN_FILL);
192     dashSelector->set_valign(Gtk::ALIGN_CENTER);
193     table->attach(*dashSelector, 1, i, 3, 1);
194     dashSelector->changed_signal.connect(sigc::mem_fun(*this, &StrokeStyle::lineDashChangedCB));
195 
196     i++;
197 
198     /* Drop down marker selectors*/
199     // TRANSLATORS: Path markers are an SVG feature that allows you to attach arbitrary shapes
200     // (arrowheads, bullets, faces, whatever) to the start, end, or middle nodes of a path.
201 
202     spw_label(table, _("Markers:"), 0, i, nullptr);
203 
204     hb = spw_hbox(table, 1, 1, i);
205     i++;
206 
207     startMarkerCombo = Gtk::manage(new MarkerComboBox("marker-start", SP_MARKER_LOC_START));
208     startMarkerCombo->set_tooltip_text(_("Start Markers are drawn on the first node of a path or shape"));
209     startMarkerConn = startMarkerCombo->signal_changed().connect(
210             sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>(
211                 sigc::ptr_fun(&StrokeStyle::markerSelectCB), startMarkerCombo, this, SP_MARKER_LOC_START));
212     startMarkerCombo->show();
213 
214     hb->pack_start(*startMarkerCombo, true, true, 0);
215 
216     midMarkerCombo = Gtk::manage(new MarkerComboBox("marker-mid", SP_MARKER_LOC_MID));
217     midMarkerCombo->set_tooltip_text(_("Mid Markers are drawn on every node of a path or shape except the first and last nodes"));
218     midMarkerConn = midMarkerCombo->signal_changed().connect(
219         sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>(
220             sigc::ptr_fun(&StrokeStyle::markerSelectCB), midMarkerCombo, this, SP_MARKER_LOC_MID));
221     midMarkerCombo->show();
222 
223     hb->pack_start(*midMarkerCombo, true, true, 0);
224 
225     endMarkerCombo = Gtk::manage(new MarkerComboBox("marker-end", SP_MARKER_LOC_END));
226     endMarkerCombo->set_tooltip_text(_("End Markers are drawn on the last node of a path or shape"));
227     endMarkerConn = endMarkerCombo->signal_changed().connect(
228         sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>(
229             sigc::ptr_fun(&StrokeStyle::markerSelectCB), endMarkerCombo, this, SP_MARKER_LOC_END));
230     endMarkerCombo->show();
231 
232     hb->pack_start(*endMarkerCombo, true, true, 0);
233 
234     i++;
235 
236     /* Join type */
237     // TRANSLATORS: The line join style specifies the shape to be used at the
238     //  corners of paths. It can be "miter", "round" or "bevel".
239     spw_label(table, _("Join:"), 0, i, nullptr);
240 
241     hb = spw_hbox(table, 3, 1, i);
242 
243     Gtk::RadioButtonGroup joinGrp;
244 
245     joinRound = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-round"),
246                                 hb, STROKE_STYLE_BUTTON_JOIN, "round");
247 
248     // TRANSLATORS: Round join: joining lines with a rounded corner.
249     //  For an example, draw a triangle with a large stroke width and modify the
250     //  "Join" option (in the Fill and Stroke dialog).
251     joinRound->set_tooltip_text(_("Round join"));
252 
253     joinBevel = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-bevel"),
254                                 hb, STROKE_STYLE_BUTTON_JOIN, "bevel");
255 
256     // TRANSLATORS: Bevel join: joining lines with a blunted (flattened) corner.
257     //  For an example, draw a triangle with a large stroke width and modify the
258     //  "Join" option (in the Fill and Stroke dialog).
259     joinBevel->set_tooltip_text(_("Bevel join"));
260 
261     joinMiter = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-miter"),
262                                 hb, STROKE_STYLE_BUTTON_JOIN, "miter");
263 
264     // TRANSLATORS: Miter join: joining lines with a sharp (pointed) corner.
265     //  For an example, draw a triangle with a large stroke width and modify the
266     //  "Join" option (in the Fill and Stroke dialog).
267     joinMiter->set_tooltip_text(_("Miter join"));
268 
269     /* Miterlimit  */
270     // TRANSLATORS: Miter limit: only for "miter join", this limits the length
271     //  of the sharp "spike" when the lines connect at too sharp an angle.
272     // When two line segments meet at a sharp angle, a miter join results in a
273     //  spike that extends well beyond the connection point. The purpose of the
274     //  miter limit is to cut off such spikes (i.e. convert them into bevels)
275     //  when they become too long.
276     //spw_label(t, _("Miter _limit:"), 0, i);
277     miterLimitAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(4.0, 0.0, 100000.0, 0.1, 10.0, 0.0));
278     miterLimitSpin = new Inkscape::UI::Widget::SpinButton(*miterLimitAdj, 0.1, 2);
279     miterLimitSpin->set_tooltip_text(_("Maximum length of the miter (in units of stroke width)"));
280     miterLimitSpin->show();
281     sp_dialog_defocus_on_enter_cpp(miterLimitSpin);
282 
283     hb->pack_start(*miterLimitSpin, false, false, 0);
284     (*miterLimitAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::miterLimitChangedCB));
285     i++;
286 
287     /* Cap type */
288     // TRANSLATORS: cap type specifies the shape for the ends of lines
289     //spw_label(t, _("_Cap:"), 0, i);
290     spw_label(table, _("Cap:"), 0, i, nullptr);
291 
292     hb = spw_hbox(table, 3, 1, i);
293 
294     Gtk::RadioButtonGroup capGrp;
295 
296     capButt = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-butt"),
297                                 hb, STROKE_STYLE_BUTTON_CAP, "butt");
298 
299     // TRANSLATORS: Butt cap: the line shape does not extend beyond the end point
300     //  of the line; the ends of the line are square
301     capButt->set_tooltip_text(_("Butt cap"));
302 
303     capRound = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-round"),
304                                 hb, STROKE_STYLE_BUTTON_CAP, "round");
305 
306     // TRANSLATORS: Round cap: the line shape extends beyond the end point of the
307     //  line; the ends of the line are rounded
308     capRound->set_tooltip_text(_("Round cap"));
309 
310     capSquare = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-square"),
311                                 hb, STROKE_STYLE_BUTTON_CAP, "square");
312 
313     // TRANSLATORS: Square cap: the line shape extends beyond the end point of the
314     //  line; the ends of the line are square
315     capSquare->set_tooltip_text(_("Square cap"));
316 
317     i++;
318 
319     /* Paint order */
320     // TRANSLATORS: Paint order determines the order the 'fill', 'stroke', and 'markers are painted.
321     spw_label(table, _("Order:"), 0, i, nullptr);
322 
323     hb = spw_hbox(table, 4, 1, i);
324 
325     Gtk::RadioButtonGroup paintOrderGrp;
326 
327     paintOrderFSM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fsm"),
328                                     hb, STROKE_STYLE_BUTTON_ORDER, "normal");
329     paintOrderFSM->set_tooltip_text(_("Fill, Stroke, Markers"));
330 
331     paintOrderSFM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-sfm"),
332                                     hb, STROKE_STYLE_BUTTON_ORDER, "stroke fill markers");
333     paintOrderSFM->set_tooltip_text(_("Stroke, Fill, Markers"));
334 
335     paintOrderFMS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fms"),
336                                     hb, STROKE_STYLE_BUTTON_ORDER, "fill markers stroke");
337     paintOrderFMS->set_tooltip_text(_("Fill, Markers, Stroke"));
338 
339     i++;
340 
341     hb = spw_hbox(table, 4, 1, i);
342 
343     paintOrderMFS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-mfs"),
344                                     hb, STROKE_STYLE_BUTTON_ORDER, "markers fill stroke");
345     paintOrderMFS->set_tooltip_text(_("Markers, Fill, Stroke"));
346 
347     paintOrderSMF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-smf"),
348                                     hb, STROKE_STYLE_BUTTON_ORDER, "stroke markers fill");
349     paintOrderSMF->set_tooltip_text(_("Stroke, Markers, Fill"));
350 
351     paintOrderMSF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-msf"),
352                                     hb, STROKE_STYLE_BUTTON_ORDER, "markers stroke fill");
353     paintOrderMSF->set_tooltip_text(_("Markers, Stroke, Fill"));
354 
355     i++;
356 }
357 
~StrokeStyle()358 StrokeStyle::~StrokeStyle()
359 {
360     selectModifiedConn.disconnect();
361     selectChangedConn.disconnect();
362 }
363 
setDesktop(SPDesktop * desktop)364 void StrokeStyle::setDesktop(SPDesktop *desktop)
365 {
366     if (this->desktop != desktop) {
367 
368         if (this->desktop) {
369             selectModifiedConn.disconnect();
370             selectChangedConn.disconnect();
371             _document_replaced_connection.disconnect();
372         }
373         this->desktop = desktop;
374 
375         if (!desktop) {
376             return;
377         }
378 
379         if (desktop->selection) {
380             selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &StrokeStyle::selectionChangedCB)));
381             selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &StrokeStyle::selectionModifiedCB)));
382         }
383 
384         _document_replaced_connection =
385             desktop->connectDocumentReplaced(sigc::mem_fun(this, &StrokeStyle::_handleDocumentReplaced));
386 
387         _handleDocumentReplaced(nullptr, desktop->getDocument());
388 
389         updateLine();
390     }
391 }
392 
_handleDocumentReplaced(SPDesktop *,SPDocument * document)393 void StrokeStyle::_handleDocumentReplaced(SPDesktop *, SPDocument *document)
394 {
395     for (MarkerComboBox *combo : { startMarkerCombo, midMarkerCombo, endMarkerCombo }) {
396         combo->setDocument(document);
397     }
398 }
399 
400 
401 /**
402  * Helper function for creating stroke-style radio buttons.
403  *
404  * \param[in] grp           The Gtk::RadioButtonGroup in which to add the button
405  * \param[in] icon          The icon for the button
406  * \param[in] hb            The Gtk::Box container in which to add the button
407  * \param[in] button_type   The type (join/cap) for the button
408  * \param[in] stroke_style  The style attribute to associate with the button
409  *
410  * \details After instantiating the button, it is added to a container box and
411  *          a handler for the toggle event is connected.
412  */
413 StrokeStyle::StrokeStyleButton *
makeRadioButton(Gtk::RadioButtonGroup & grp,char const * icon,Gtk::Box * hb,StrokeStyleButtonType button_type,gchar const * stroke_style)414 StrokeStyle::makeRadioButton(Gtk::RadioButtonGroup &grp,
415                              char const            *icon,
416                              Gtk::Box              *hb,
417                              StrokeStyleButtonType  button_type,
418                              gchar const           *stroke_style)
419 {
420     g_assert(icon != nullptr);
421     g_assert(hb  != nullptr);
422 
423     StrokeStyleButton *tb = new StrokeStyleButton(grp, icon, button_type, stroke_style);
424 
425     hb->pack_start(*tb, false, false, 0);
426 
427     tb->signal_toggled().connect(sigc::bind<StrokeStyleButton *, StrokeStyle *>(
428                                      sigc::ptr_fun(&StrokeStyle::buttonToggledCB), tb, this));
429 
430     return tb;
431 }
432 
shouldMarkersBeUpdated()433 bool StrokeStyle::shouldMarkersBeUpdated()
434 {
435     return startMarkerCombo->update() || midMarkerCombo->update() ||
436                           endMarkerCombo->update();
437 }
438 
439 /**
440  * Handles when user selects one of the markers from the marker combobox.
441  * Gets the marker uri string and applies it to all selected
442  * items in the current desktop.
443  */
markerSelectCB(MarkerComboBox * marker_combo,StrokeStyle * spw,SPMarkerLoc const)444 void StrokeStyle::markerSelectCB(MarkerComboBox *marker_combo, StrokeStyle *spw, SPMarkerLoc const /*which*/)
445 {
446     if (spw->update || spw->shouldMarkersBeUpdated()) {
447         return;
448     }
449 
450     spw->update = true;
451 
452     SPDocument *document = spw->desktop->getDocument();
453     if (!document) {
454         return;
455     }
456 
457     /* Get Marker */
458     gchar const *marker = marker_combo->get_active_marker_uri();
459 
460 
461     SPCSSAttr *css = sp_repr_css_attr_new();
462     gchar const *combo_id = marker_combo->get_id();
463     sp_repr_css_set_property(css, combo_id, marker);
464 
465     Inkscape::Selection *selection = spw->desktop->getSelection();
466     auto itemlist= selection->items();
467     for(auto i=itemlist.begin();i!=itemlist.end();++i){
468         SPItem *item = *i;
469         if (!SP_IS_SHAPE(item)) {
470             continue;
471         }
472         Inkscape::XML::Node *selrepr = item->getRepr();
473         if (selrepr) {
474             sp_repr_css_change_recursive(selrepr, css, "style");
475         }
476 
477         item->requestModified(SP_OBJECT_MODIFIED_FLAG);
478         item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG);
479 
480         DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, _("Set markers"));
481     }
482 
483     sp_repr_css_attr_unref(css);
484     css = nullptr;
485 
486     spw->update = false;
487 };
488 
489 /**
490  * Callback for when UnitMenu widget is modified.
491  * Triggers update action.
492  */
unitChangedCB()493 void StrokeStyle::unitChangedCB()
494 {
495     if (update) {
496         return;
497     }
498 
499     // If the unit selector is set to hairline, don't do the normal conversion.
500     if (isHairlineSelected()) {
501         scaleLine();
502         return;
503     }
504 
505 
506     Inkscape::Util::Unit const *new_unit = unitSelector->getUnit();
507     if (new_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) {
508         widthSpin->set_value(100);
509     } else {
510         // Remove the non-scaling-stroke effect and the hairline extensions
511         SPCSSAttr *css = sp_repr_css_attr_new();
512         sp_repr_css_unset_property(css, "vector-effect");
513         sp_repr_css_unset_property(css, "-inkscape-stroke");
514         sp_desktop_set_style(desktop, css);
515         sp_repr_css_attr_unref(css);
516         css = nullptr;
517     }
518     widthSpin->set_value(Inkscape::Util::Quantity::convert(widthSpin->get_value(), _old_unit, new_unit));
519     _old_unit = new_unit;
520 }
521 
522 /**
523  * Callback for when stroke style widget is modified.
524  * Triggers update action.
525  */
526 void
selectionModifiedCB(guint flags)527 StrokeStyle::selectionModifiedCB(guint flags)
528 {
529     // We care deeply about only updating when the style is updated
530     // if we update on other flags, we slow inkscape down when dragging
531     if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) {
532         updateLine();
533     }
534 }
535 
536 /**
537  * Callback for when stroke style widget is changed.
538  * Triggers update action.
539  */
540 void
selectionChangedCB()541 StrokeStyle::selectionChangedCB()
542 {
543     updateLine();
544 }
545 
546 /**
547  * Sets selector widgets' dash style from an SPStyle object.
548  */
549 void
setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector * dsel,SPStyle * style)550 StrokeStyle::setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style)
551 {
552     if (!style->stroke_dasharray.values.empty()) {
553         double d[64];
554         size_t len = MIN(style->stroke_dasharray.values.size(), 64);
555         /* Set dash */
556         Inkscape::Preferences *prefs = Inkscape::Preferences::get();
557         gboolean scale = prefs->getBool("/options/dash/scale", true);
558         double scaledash = 1.0;
559         if (scale) {
560             scaledash = style->stroke_width.computed;
561         }
562         for (unsigned i = 0; i < len; i++) {
563             if (style->stroke_width.computed != 0)
564                 d[i] = style->stroke_dasharray.values[i].value / scaledash;
565             else
566                 d[i] = style->stroke_dasharray.values[i].value; // is there a better thing to do for stroke_width==0?
567         }
568         dsel->set_dash(len, d,
569                        style->stroke_width.computed != 0 ? style->stroke_dashoffset.value / scaledash
570                                                          : style->stroke_dashoffset.value);
571     } else {
572         dsel->set_dash(0, nullptr, 0.0);
573     }
574 }
575 
576 /**
577  * Sets the join type for a line, and updates the stroke style widget's buttons
578  */
579 void
setJoinType(unsigned const jointype)580 StrokeStyle::setJoinType (unsigned const jointype)
581 {
582     Gtk::RadioButton *tb = nullptr;
583     switch (jointype) {
584         case SP_STROKE_LINEJOIN_MITER:
585             tb = joinMiter;
586             break;
587         case SP_STROKE_LINEJOIN_ROUND:
588             tb = joinRound;
589             break;
590         case SP_STROKE_LINEJOIN_BEVEL:
591             tb = joinBevel;
592             break;
593         default:
594             // Should not happen
595             std::cerr << "StrokeStyle::setJoinType(): Invalid value: " << jointype << std::endl;
596             tb = joinMiter;
597             break;
598     }
599     setJoinButtons(tb);
600 }
601 
602 /**
603  * Sets the cap type for a line, and updates the stroke style widget's buttons
604  */
605 void
setCapType(unsigned const captype)606 StrokeStyle::setCapType (unsigned const captype)
607 {
608     Gtk::RadioButton *tb = nullptr;
609     switch (captype) {
610         case SP_STROKE_LINECAP_BUTT:
611             tb = capButt;
612             break;
613         case SP_STROKE_LINECAP_ROUND:
614             tb = capRound;
615             break;
616         case SP_STROKE_LINECAP_SQUARE:
617             tb = capSquare;
618             break;
619         default:
620             // Should not happen
621             std::cerr << "StrokeStyle::setCapType(): Invalid value: " << captype << std::endl;
622             tb = capButt;
623             break;
624     }
625     setCapButtons(tb);
626 }
627 
628 /**
629  * Sets the cap type for a line, and updates the stroke style widget's buttons
630  */
631 void
setPaintOrder(gchar const * paint_order)632 StrokeStyle::setPaintOrder (gchar const *paint_order)
633 {
634     Gtk::RadioButton *tb = paintOrderFSM;
635 
636     SPIPaintOrder temp;
637     temp.read( paint_order );
638 
639     if (temp.layer[0] != SP_CSS_PAINT_ORDER_NORMAL) {
640 
641         if (temp.layer[0] == SP_CSS_PAINT_ORDER_FILL) {
642             if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) {
643                 tb = paintOrderFSM;
644             } else {
645                 tb = paintOrderFMS;
646             }
647         } else if (temp.layer[0] == SP_CSS_PAINT_ORDER_STROKE) {
648             if (temp.layer[1] == SP_CSS_PAINT_ORDER_FILL) {
649                 tb = paintOrderSFM;
650             } else {
651                 tb = paintOrderSMF;
652             }
653         } else {
654             if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) {
655                 tb = paintOrderMSF;
656             } else {
657                 tb = paintOrderMFS;
658             }
659         }
660 
661     }
662     setPaintOrderButtons(tb);
663 }
664 
665 /**
666  * Callback for when stroke style widget is updated, including markers, cap type,
667  * join type, etc.
668  */
669 void
updateLine()670 StrokeStyle::updateLine()
671 {
672     if (update) {
673         return;
674     }
675 
676     update = true;
677 
678     Inkscape::Selection *sel = desktop ? desktop->getSelection() : nullptr;
679 
680     if (!sel || sel->isEmpty()) {
681         // Nothing selected, grey-out all controls in the stroke-style dialog
682         table->set_sensitive(false);
683 
684         update = false;
685 
686         return;
687     }
688 
689     FillOrStroke kind = STROKE;
690 
691     // create temporary style
692     SPStyle query(SP_ACTIVE_DOCUMENT);
693     // query into it
694     int result_sw = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH);
695     int result_ml = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEMITERLIMIT);
696     int result_cap = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKECAP);
697     int result_join = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_STROKEJOIN);
698 
699     int result_order = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_PAINTORDER);
700 
701     SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL);
702 
703     {
704         table->set_sensitive(true);
705 
706         if (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED) {
707             unitSelector->setUnit("%");
708         } else if (query.stroke_extensions.hairline) {
709             unitSelector->set_active_id("hairline");
710         } else {
711             // same width, or only one object; no sense to keep percent, switch to absolute
712             Inkscape::Util::Unit const *tempunit = unitSelector->getUnit();
713             if (tempunit->type != Inkscape::Util::UNIT_TYPE_LINEAR) {
714                 unitSelector->setUnit(desktop->getNamedView()->display_units->abbr);
715             }
716         }
717 
718         Inkscape::Util::Unit const *unit = unitSelector->getUnit();
719 
720         if (query.stroke_extensions.hairline) {
721             (*widthAdj)->set_value(0);
722         } else if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
723             double avgwidth = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", unit);
724             (*widthAdj)->set_value(avgwidth);
725         } else {
726             (*widthAdj)->set_value(100);
727         }
728 
729         // if none of the selected objects has a stroke, than quite some controls should be disabled
730         // These options should also be disabled for hairlines, since they don't make sense for
731         // 0-width lines.
732         // The markers might still be shown though, so these will not be disabled
733         bool enabled = (result_sw != QUERY_STYLE_NOTHING) && !targPaint.isNoneSet();
734 
735         /* No objects stroked, set insensitive */
736         widthSpin->set_sensitive(enabled &&
737                 (!query.stroke_extensions.hairline || result_sw == QUERY_STYLE_MULTIPLE_AVERAGED));
738         unitSelector->set_sensitive(enabled);
739 
740         joinMiter->set_sensitive(enabled && !query.stroke_extensions.hairline);
741         joinRound->set_sensitive(enabled && !query.stroke_extensions.hairline);
742         joinBevel->set_sensitive(enabled && !query.stroke_extensions.hairline);
743 
744         miterLimitSpin->set_sensitive(enabled && !query.stroke_extensions.hairline);
745 
746         capButt->set_sensitive(enabled && !query.stroke_extensions.hairline);
747         capRound->set_sensitive(enabled && !query.stroke_extensions.hairline);
748         capSquare->set_sensitive(enabled && !query.stroke_extensions.hairline);
749 
750         dashSelector->set_sensitive(enabled && !query.stroke_extensions.hairline);
751     }
752 
753     if (result_ml != QUERY_STYLE_NOTHING)
754         (*miterLimitAdj)->set_value(query.stroke_miterlimit.value); // TODO: reflect averagedness?
755 
756     using Inkscape::is_query_style_updateable;
757     if (! is_query_style_updateable(result_join)) {
758         setJoinType(query.stroke_linejoin.value);
759     } else {
760         setJoinButtons(nullptr);
761     }
762 
763     if (! is_query_style_updateable(result_cap)) {
764         setCapType (query.stroke_linecap.value);
765     } else {
766         setCapButtons(nullptr);
767     }
768 
769     if (! is_query_style_updateable(result_order)) {
770         setPaintOrder (query.paint_order.value);
771     } else {
772         setPaintOrder (nullptr);
773     }
774 
775     std::vector<SPItem*> const objects(sel->items().begin(), sel->items().end());
776     if (objects.size()) {
777         SPObject *const object = objects[0];
778         SPStyle *const style = object->style;
779         /* Markers */
780         updateAllMarkers(objects, true); // FIXME: make this desktop query too
781 
782         /* Dash */
783         setDashSelectorFromStyle(dashSelector, style); // FIXME: make this desktop query too
784     }
785     table->set_sensitive(true);
786 
787     update = false;
788 }
789 
790 /**
791  * Sets a line's dash properties in a CSS style object.
792  */
793 void
setScaledDash(SPCSSAttr * css,int ndash,double * dash,double offset,double scale)794 StrokeStyle::setScaledDash(SPCSSAttr *css,
795                                 int ndash, double *dash, double offset,
796                                 double scale)
797 {
798     if (ndash > 0) {
799         Inkscape::CSSOStringStream osarray;
800         for (int i = 0; i < ndash; i++) {
801             osarray << dash[i] * scale;
802             if (i < (ndash - 1)) {
803                 osarray << ",";
804             }
805         }
806         sp_repr_css_set_property(css, "stroke-dasharray", osarray.str().c_str());
807 
808         Inkscape::CSSOStringStream osoffset;
809         osoffset << offset * scale;
810         sp_repr_css_set_property(css, "stroke-dashoffset", osoffset.str().c_str());
811     } else {
812         sp_repr_css_set_property(css, "stroke-dasharray", "none");
813         sp_repr_css_set_property(css, "stroke-dashoffset", nullptr);
814     }
815 }
816 
calcScaleLineWidth(const double width_typed,SPItem * const item,Inkscape::Util::Unit const * const unit)817 static inline double calcScaleLineWidth(const double width_typed, SPItem *const item, Inkscape::Util::Unit const *const unit)
818 {
819     if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) {
820         return Inkscape::Util::Quantity::convert(width_typed, unit, "px");
821     } else { // percentage
822         const gdouble old_w = item->style->stroke_width.computed;
823         return old_w * width_typed / 100;
824     }
825 }
826 
827 /**
828  * Sets line properties like width, dashes, markers, etc. on all currently selected items.
829  */
830 void
scaleLine()831 StrokeStyle::scaleLine()
832 {
833     if (!desktop) {
834         return;
835     }
836 
837     if (update) {
838         return;
839     }
840 
841     update = true;
842 
843     SPDocument *document = desktop->getDocument();
844     Inkscape::Selection *selection = desktop->getSelection();
845     auto items= selection->items();
846 
847     /* TODO: Create some standardized method */
848     SPCSSAttr *css = sp_repr_css_attr_new();
849 
850     if (!items.empty()) {
851         double width_typed = (*widthAdj)->get_value();
852         double const miterlimit = (*miterLimitAdj)->get_value();
853 
854         Inkscape::Util::Unit const *const unit = unitSelector->getUnit();
855 
856         double *dash, offset;
857         int ndash;
858         dashSelector->get_dash(&ndash, &dash, &offset);
859 
860         for(auto i=items.begin();i!=items.end();++i){
861             /* Set stroke width */
862             const double width = calcScaleLineWidth(width_typed, (*i), unit);
863 
864             /* For renderers that don't understand -inkscape-stroke:hairline, fall back to 1px non-scaling */
865             if (isHairlineSelected()) {
866                 const double width1px = calcScaleLineWidth(1, (*i), unit);
867                 Inkscape::CSSOStringStream os_width;
868                 os_width << width1px;
869                 sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str());
870                 sp_repr_css_set_property(css, "vector-effect", "non-scaling-stroke");
871                 sp_repr_css_set_property(css, "-inkscape-stroke", "hairline");
872             } else {
873                 Inkscape::CSSOStringStream os_width;
874                 os_width << width;
875                 sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str());
876                 sp_repr_css_unset_property(css, "vector-effect");
877                 sp_repr_css_unset_property(css, "-inkscape-stroke");
878             }
879 
880             {
881                 Inkscape::CSSOStringStream os_ml;
882                 os_ml << miterlimit;
883                 sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str());
884             }
885 
886             /* Set dash */
887             Inkscape::Preferences *prefs = Inkscape::Preferences::get();
888             gboolean scale = prefs->getBool("/options/dash/scale", true);
889             if (scale) {
890                 setScaledDash(css, ndash, dash, offset, width);
891             }
892             else {
893                 setScaledDash(css, ndash, dash, offset, document->getDocumentScale()[0]);
894             }
895             sp_desktop_apply_css_recursive ((*i), css, true);
896         }
897 
898         g_free(dash);
899 
900         if (unit->type != Inkscape::Util::UNIT_TYPE_LINEAR) {
901             // reset to 100 percent
902             (*widthAdj)->set_value(100.0);
903         }
904 
905     }
906 
907     // we have already changed the items, so set style without changing selection
908     // FIXME: move the above stroke-setting stuff, including percentages, to desktop-style
909     sp_desktop_set_style (desktop, css, false);
910 
911     sp_repr_css_attr_unref(css);
912     css = nullptr;
913 
914     DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE,
915                        _("Set stroke style"));
916 
917     update = false;
918 }
919 
920 /**
921  * Returns whether the currently selected stroke width is "hairline"
922  *
923  */
924 bool
isHairlineSelected() const925 StrokeStyle::isHairlineSelected() const
926 {
927     return unitSelector->get_active_id() == "hairline";
928 }
929 
930 /**
931  * Callback for when the stroke style's width changes.
932  * Causes all line styles to be applied to all selected items.
933  */
934 void
widthChangedCB()935 StrokeStyle::widthChangedCB()
936 {
937     if (update) {
938         return;
939     }
940 
941     scaleLine();
942 }
943 
944 /**
945  * Callback for when the stroke style's miterlimit changes.
946  * Causes all line styles to be applied to all selected items.
947  */
948 void
miterLimitChangedCB()949 StrokeStyle::miterLimitChangedCB()
950 {
951     if (update) {
952         return;
953     }
954 
955     scaleLine();
956 }
957 
958 /**
959  * Callback for when the stroke style's dash changes.
960  * Causes all line styles to be applied to all selected items.
961  */
962 
963 void
lineDashChangedCB()964 StrokeStyle::lineDashChangedCB()
965 {
966     if (update) {
967         return;
968     }
969 
970     scaleLine();
971 }
972 
973 /**
974  * This routine handles toggle events for buttons in the stroke style dialog.
975  *
976  * When activated, this routine gets the data for the various widgets, and then
977  * calls the respective routines to update css properties, etc.
978  *
979  */
buttonToggledCB(StrokeStyleButton * tb,StrokeStyle * spw)980 void StrokeStyle::buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw)
981 {
982     if (spw->update) {
983         return;
984     }
985 
986     if (tb->get_active()) {
987         if (tb->get_button_type() == STROKE_STYLE_BUTTON_JOIN) {
988             spw->miterLimitSpin->set_sensitive(!strcmp(tb->get_stroke_style(), "miter"));
989         }
990 
991         /* TODO: Create some standardized method */
992         SPCSSAttr *css = sp_repr_css_attr_new();
993 
994         switch (tb->get_button_type()) {
995             case STROKE_STYLE_BUTTON_JOIN:
996                 sp_repr_css_set_property(css, "stroke-linejoin", tb->get_stroke_style());
997                 sp_desktop_set_style (spw->desktop, css);
998                 spw->setJoinButtons(tb);
999                 break;
1000             case STROKE_STYLE_BUTTON_CAP:
1001                 sp_repr_css_set_property(css, "stroke-linecap", tb->get_stroke_style());
1002                 sp_desktop_set_style (spw->desktop, css);
1003                 spw->setCapButtons(tb);
1004                 break;
1005             case STROKE_STYLE_BUTTON_ORDER:
1006                 sp_repr_css_set_property(css, "paint-order", tb->get_stroke_style());
1007                 sp_desktop_set_style (spw->desktop, css);
1008                 //spw->setPaintButtons(tb);
1009         }
1010 
1011         sp_repr_css_attr_unref(css);
1012         css = nullptr;
1013 
1014         DocumentUndo::done(spw->desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, _("Set stroke style"));
1015     }
1016 }
1017 
1018 /**
1019  * Updates the join style toggle buttons
1020  */
1021 void
setJoinButtons(Gtk::ToggleButton * active)1022 StrokeStyle::setJoinButtons(Gtk::ToggleButton *active)
1023 {
1024     joinMiter->set_active(active == joinMiter);
1025     miterLimitSpin->set_sensitive(active == joinMiter && !isHairlineSelected());
1026     joinRound->set_active(active == joinRound);
1027     joinBevel->set_active(active == joinBevel);
1028 }
1029 
1030 /**
1031  * Updates the cap style toggle buttons
1032  */
1033 void
setCapButtons(Gtk::ToggleButton * active)1034 StrokeStyle::setCapButtons(Gtk::ToggleButton *active)
1035 {
1036     capButt->set_active(active == capButt);
1037     capRound->set_active(active == capRound);
1038     capSquare->set_active(active == capSquare);
1039 }
1040 
1041 
1042 /**
1043  * Updates the paint order style toggle buttons
1044  */
1045 void
setPaintOrderButtons(Gtk::ToggleButton * active)1046 StrokeStyle::setPaintOrderButtons(Gtk::ToggleButton *active)
1047 {
1048     paintOrderFSM->set_active(active == paintOrderFSM);
1049     paintOrderSFM->set_active(active == paintOrderSFM);
1050     paintOrderFMS->set_active(active == paintOrderFMS);
1051     paintOrderMFS->set_active(active == paintOrderMFS);
1052     paintOrderSMF->set_active(active == paintOrderSMF);
1053     paintOrderMSF->set_active(active == paintOrderMSF);
1054 }
1055 
1056 
1057 /**
1058  * Recursively builds a simple list from an arbitrarily complex selection
1059  * of items and grouped items
1060  */
buildGroupedItemList(SPObject * element,std::vector<SPObject * > & simple_list)1061 static void buildGroupedItemList(SPObject *element, std::vector<SPObject*> &simple_list)
1062 {
1063     if (SP_IS_GROUP(element)) {
1064         for (SPObject *i = element->firstChild(); i; i = i->getNext()) {
1065             buildGroupedItemList(i, simple_list);
1066         }
1067     } else {
1068         simple_list.push_back(element);
1069     }
1070 }
1071 
1072 
1073 /**
1074  * Updates the marker combobox to highlight the appropriate marker and scroll to
1075  * that marker.
1076  */
1077 void
updateAllMarkers(std::vector<SPItem * > const & objects,bool skip_undo)1078 StrokeStyle::updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo)
1079 {
1080     struct { MarkerComboBox *key; int loc; } const keyloc[] = {
1081             { startMarkerCombo, SP_MARKER_LOC_START },
1082             { midMarkerCombo, SP_MARKER_LOC_MID },
1083             { endMarkerCombo, SP_MARKER_LOC_END }
1084     };
1085 
1086     bool all_texts = true;
1087 
1088     auto simplified_list = std::vector<SPObject *>();
1089     for (SPItem *item : objects) {
1090         buildGroupedItemList(item, simplified_list);
1091     }
1092 
1093     for (SPObject *object : simplified_list) {
1094         if (!SP_IS_TEXT(object)) {
1095             all_texts = false;
1096             break;
1097         }
1098     }
1099 
1100     // We show markers of the last object in the list only
1101     // FIXME: use the first in the list that has the marker of each type, if any
1102 
1103     for (auto const &markertype : keyloc) {
1104         // For all three marker types,
1105 
1106         // find the corresponding combobox item
1107         MarkerComboBox *combo = markertype.key;
1108 
1109         // Quit if we're in update state
1110         if (combo->update()) {
1111             return;
1112         }
1113 
1114         // Per SVG spec, text objects cannot have markers; disable combobox if only texts are selected
1115         // They should also be disabled for hairlines, since scaling against a 0-width line doesn't
1116         // make sense.
1117         combo->set_sensitive(!all_texts && !isHairlineSelected());
1118 
1119         SPObject *marker = nullptr;
1120 
1121         if (!all_texts && !isHairlineSelected()) {
1122             for (SPObject *object : simplified_list) {
1123                 char const *value = object->style->marker_ptrs[markertype.loc]->value();
1124 
1125                 // If the object has this type of markers,
1126                 if (value == nullptr)
1127                     continue;
1128 
1129                 // Extract the name of the marker that the object uses
1130                 marker = getMarkerObj(value, object->document);
1131             }
1132         }
1133 
1134         // Scroll the combobox to that marker
1135         combo->set_current(marker);
1136     }
1137 
1138 }
1139 
1140 } // namespace Widget
1141 } // namespace UI
1142 } // namespace Inkscape
1143 
1144 
1145 /*
1146   Local Variables:
1147   mode:c++
1148   c-file-style:"stroustrup"
1149   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1150   indent-tabs-mode:nil
1151   fill-column:99
1152   End:
1153 */
1154 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
1155