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