1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /**
3 * @file
4 * Combobox for selecting dash patterns - implementation.
5 */
6 /* Author:
7 * Lauris Kaplinski <lauris@kaplinski.com>
8 * bulia byak <buliabyak@users.sf.net>
9 * Maximilian Albert <maximilian.albert@gmail.com>
10 *
11 * Copyright (C) 2002 Lauris Kaplinski
12 *
13 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14 */
15
16 #include "marker-combo-box.h"
17
18 #include <glibmm/fileutils.h>
19 #include <glibmm/i18n.h>
20 #include <gtkmm/icontheme.h>
21
22 #include "desktop-style.h"
23 #include "path-prefix.h"
24
25 #include "helper/stock-items.h"
26 #include "ui/icon-loader.h"
27
28 #include "io/resource.h"
29 #include "io/sys.h"
30
31 #include "object/sp-defs.h"
32 #include "object/sp-marker.h"
33 #include "object/sp-root.h"
34 #include "style.h"
35
36 #include "ui/cache/svg_preview_cache.h"
37 #include "ui/dialog-events.h"
38 #include "ui/util.h"
39
40 #include "ui/widget/spinbutton.h"
41 #include "ui/widget/stroke-style.h"
42
43 static Inkscape::UI::Cache::SvgPreview svg_preview_cache;
44
45 namespace Inkscape {
46 namespace UI {
47 namespace Widget {
48
MarkerComboBox(gchar const * id,int l)49 MarkerComboBox::MarkerComboBox(gchar const *id, int l) :
50 combo_id(id),
51 loc(l),
52 updating(false),
53 markerCount(0)
54 {
55
56 marker_store = Gtk::ListStore::create(marker_columns);
57 set_model(marker_store);
58 pack_start(image_renderer, false);
59 add_attribute (image_renderer, "pixbuf", marker_columns.pixbuf);
60
61 gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(gobj()), MarkerComboBox::separator_cb, nullptr, nullptr);
62
63 sandbox = ink_markers_preview_doc ();
64
65 init_combo();
66 this->get_style_context()->add_class("combobright");
67
68 show();
69 }
70
~MarkerComboBox()71 MarkerComboBox::~MarkerComboBox() {
72 delete combo_id;
73 delete sandbox;
74
75 if (doc) {
76 modified_connection.disconnect();
77 }
78 }
79
setDocument(SPDocument * document)80 void MarkerComboBox::setDocument(SPDocument *document)
81 {
82 if (doc != document) {
83
84 if (doc) {
85 modified_connection.disconnect();
86 }
87
88 doc = document;
89
90 if (doc) {
91 modified_connection = doc->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&MarkerComboBox::handleDefsModified), this))) );
92 }
93
94 refreshHistory();
95 }
96 }
97
98 void
handleDefsModified(MarkerComboBox * self)99 MarkerComboBox::handleDefsModified(MarkerComboBox *self)
100 {
101 self->refreshHistory();
102 }
103
104 void
refreshHistory()105 MarkerComboBox::refreshHistory()
106 {
107 if (updating)
108 return;
109
110 updating = true;
111
112 std::vector<SPMarker *> ml = get_marker_list(doc);
113
114 /*
115 * Seems to be no way to get notified of changes just to markers,
116 * so listen to changes in all defs and check if the number of markers has changed here
117 * to avoid unnecessary refreshes when things like gradients change
118 */
119 if (markerCount != ml.size()) {
120 auto iter = get_active();
121 const char *active = iter ? iter->get_value(marker_columns.marker) : nullptr;
122 sp_marker_list_from_doc(doc, true);
123 set_selected(active);
124 markerCount = ml.size();
125 }
126
127 updating = false;
128 }
129
130 /**
131 * Init the combobox widget to display markers from markers.svg
132 */
133 void
init_combo()134 MarkerComboBox::init_combo()
135 {
136 if (updating)
137 return;
138
139 static SPDocument *markers_doc = nullptr;
140
141 // add separator
142 Gtk::TreeModel::Row row_sep = *(marker_store->append());
143 row_sep[marker_columns.label] = "Separator";
144 row_sep[marker_columns.marker] = g_strdup("None");
145 row_sep[marker_columns.stock] = false;
146 row_sep[marker_columns.history] = false;
147 row_sep[marker_columns.separator] = true;
148
149 // find and load markers.svg
150 if (markers_doc == nullptr) {
151 using namespace Inkscape::IO::Resource;
152 auto markers_source = get_path_string(SYSTEM, MARKERS, "markers.svg");
153 if (Glib::file_test(markers_source, Glib::FILE_TEST_IS_REGULAR)) {
154 markers_doc = SPDocument::createNewDoc(markers_source.c_str(), false);
155 }
156 }
157
158 // load markers from markers.svg
159 if (markers_doc) {
160 sp_marker_list_from_doc(markers_doc, false);
161 }
162
163 set_sensitive(true);
164 }
165
166 /**
167 * Sets the current marker in the marker combobox.
168 */
set_current(SPObject * marker)169 void MarkerComboBox::set_current(SPObject *marker)
170 {
171 updating = true;
172
173 if (marker != nullptr) {
174 gchar *markname = g_strdup(marker->getRepr()->attribute("id"));
175 set_selected(markname);
176 g_free (markname);
177 }
178 else {
179 set_selected(nullptr);
180 }
181
182 updating = false;
183
184 }
185 /**
186 * Return a uri string representing the current selected marker used for setting the marker style in the document
187 */
get_active_marker_uri()188 const gchar * MarkerComboBox::get_active_marker_uri()
189 {
190 /* Get Marker */
191 const gchar *markid = get_active()->get_value(marker_columns.marker);
192 if (!markid)
193 {
194 return nullptr;
195 }
196
197 gchar const *marker = "";
198 if (strcmp(markid, "none")) {
199 bool stockid = get_active()->get_value(marker_columns.stock);
200
201 gchar *markurn;
202 if (stockid)
203 {
204 markurn = g_strconcat("urn:inkscape:marker:",markid,NULL);
205 }
206 else
207 {
208 markurn = g_strdup(markid);
209 }
210 SPObject *mark = get_stock_item(markurn, stockid);
211 g_free(markurn);
212 if (mark) {
213 Inkscape::XML::Node *repr = mark->getRepr();
214 marker = g_strconcat("url(#", repr->attribute("id"), ")", NULL);
215 }
216 } else {
217 marker = g_strdup(markid);
218 }
219
220 return marker;
221 }
222
set_selected(const gchar * name,gboolean retry)223 void MarkerComboBox::set_selected(const gchar *name, gboolean retry/*=true*/) {
224
225 if (!name) {
226 set_active(0);
227 return;
228 }
229
230 for(Gtk::TreeIter iter = marker_store->children().begin();
231 iter != marker_store->children().end(); ++iter) {
232 Gtk::TreeModel::Row row = (*iter);
233 if (row[marker_columns.marker] &&
234 !strcmp(row[marker_columns.marker], name)) {
235 set_active(iter);
236 return;
237 }
238 }
239
240 // Didn't find it in the list, try refreshing from the doc
241 if (retry) {
242 sp_marker_list_from_doc(doc, true);
243 set_selected(name, false);
244 }
245 }
246
247
248 /**
249 * Pick up all markers from source, except those that are in
250 * current_doc (if non-NULL), and add items to the combo.
251 */
sp_marker_list_from_doc(SPDocument * source,gboolean history)252 void MarkerComboBox::sp_marker_list_from_doc(SPDocument *source, gboolean history)
253 {
254 std::vector<SPMarker *> ml = get_marker_list(source);
255
256 remove_markers(history); // Seem to need to remove 2x
257 remove_markers(history);
258 add_markers(ml, source, history);
259 }
260
261 /**
262 * Returns a list of markers in the defs of the given source document as a vector
263 * Returns NULL if there are no markers in the document.
264 */
get_marker_list(SPDocument * source)265 std::vector<SPMarker *> MarkerComboBox::get_marker_list (SPDocument *source)
266 {
267 std::vector<SPMarker *> ml;
268 if (source == nullptr)
269 return ml;
270
271 SPDefs *defs = source->getDefs();
272 if (!defs) {
273 return ml;
274 }
275
276 for (auto& child: defs->children)
277 {
278 if (SP_IS_MARKER(&child)) {
279 ml.push_back(SP_MARKER(&child));
280 }
281 }
282 return ml;
283 }
284
285 /**
286 * Remove history or non-history markers from the combo
287 */
remove_markers(gboolean history)288 void MarkerComboBox::remove_markers (gboolean history)
289 {
290 // Having the model set causes assertions when erasing rows, temporarily disconnect
291 unset_model();
292 for(Gtk::TreeIter iter = marker_store->children().begin();
293 iter != marker_store->children().end(); ++iter) {
294 Gtk::TreeModel::Row row = (*iter);
295 if (row[marker_columns.history] == history && row[marker_columns.separator] == false) {
296 marker_store->erase(iter);
297 iter = marker_store->children().begin();
298 }
299 }
300
301 set_model(marker_store);
302 }
303
304 /**
305 * Adds markers in marker_list to the combo
306 */
add_markers(std::vector<SPMarker * > const & marker_list,SPDocument * source,gboolean history)307 void MarkerComboBox::add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history)
308 {
309 // Do this here, outside of loop, to speed up preview generation:
310 Inkscape::Drawing drawing;
311 unsigned const visionkey = SPItem::display_key_new(1);
312 drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
313 // Find the separator,
314 Gtk::TreeIter sep_iter;
315 for(Gtk::TreeIter iter = marker_store->children().begin();
316 iter != marker_store->children().end(); ++iter) {
317 Gtk::TreeModel::Row row = (*iter);
318 if (row[marker_columns.separator]) {
319 sep_iter = iter;
320 }
321 }
322
323 if (history) {
324 // add "None"
325 Gtk::TreeModel::Row row = *(marker_store->prepend());
326 row[marker_columns.label] = C_("Marker", "None");
327 row[marker_columns.stock] = false;
328 row[marker_columns.marker] = g_strdup("None");
329 row[marker_columns.pixbuf] = sp_get_icon_pixbuf("no-marker", Gtk::ICON_SIZE_SMALL_TOOLBAR);
330 row[marker_columns.history] = true;
331 row[marker_columns.separator] = false;
332 }
333
334 for (auto i:marker_list) {
335
336 Inkscape::XML::Node *repr = i->getRepr();
337 gchar const *markid = repr->attribute("inkscape:stockid") ? repr->attribute("inkscape:stockid") : repr->attribute("id");
338
339 // generate preview
340 auto pixbuf = create_marker_image (24, repr->attribute("id"), source, drawing, visionkey);
341
342 // Add history before separator, others after
343 Gtk::TreeModel::Row row;
344 if (history)
345 row = *(marker_store->insert(sep_iter));
346 else
347 row = *(marker_store->append());
348
349 row[marker_columns.label] = ink_ellipsize_text(markid, 20);
350 // Non "stock" markers can also have "inkscape:stockid" (when using extension ColorMarkers),
351 // So use !is_history instead to determine is it is "stock" (ie in the markers.svg file)
352 row[marker_columns.stock] = !history;
353 row[marker_columns.marker] = repr->attribute("id");
354 row[marker_columns.pixbuf] = pixbuf;
355 row[marker_columns.history] = history;
356 row[marker_columns.separator] = false;
357
358 }
359
360 sandbox->getRoot()->invoke_hide(visionkey);
361 }
362
363 /*
364 * Remove from the cache and recreate a marker image
365 */
366 void
update_marker_image(gchar const * mname)367 MarkerComboBox::update_marker_image(gchar const *mname)
368 {
369 gchar *cache_name = g_strconcat(combo_id, mname, NULL);
370 Glib::ustring key = svg_preview_cache.cache_key(doc->getDocumentURI(), cache_name, 24);
371 g_free (cache_name);
372 svg_preview_cache.remove_preview_from_cache(key);
373
374 Inkscape::Drawing drawing;
375 unsigned const visionkey = SPItem::display_key_new(1);
376 drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY));
377 auto pixbuf = create_marker_image(24, mname, doc, drawing, visionkey);
378 sandbox->getRoot()->invoke_hide(visionkey);
379
380 for(const auto & iter : marker_store->children()) {
381 Gtk::TreeModel::Row row = iter;
382 if (row[marker_columns.marker] && row[marker_columns.history] &&
383 !strcmp(row[marker_columns.marker], mname)) {
384 row[marker_columns.pixbuf] = pixbuf;
385 return;
386 }
387 }
388
389 }
390 /**
391 * Creates a copy of the marker named mname, determines its visible and renderable
392 * area in the bounding box, and then renders it. This allows us to fill in
393 * preview images of each marker in the marker combobox.
394 */
395 Glib::RefPtr<Gdk::Pixbuf>
create_marker_image(unsigned psize,gchar const * mname,SPDocument * source,Inkscape::Drawing & drawing,unsigned)396 MarkerComboBox::create_marker_image(unsigned psize, gchar const *mname,
397 SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/)
398 {
399 // Retrieve the marker named 'mname' from the source SVG document
400 SPObject const *marker = source->getObjectById(mname);
401 if (marker == nullptr) {
402 return sp_get_icon_pixbuf("bad-marker", Gtk::ICON_SIZE_SMALL_TOOLBAR);
403 }
404
405 /* Get from cache right away */
406 gchar *cache_name = g_strconcat(combo_id, mname, NULL);
407 Glib::ustring key = svg_preview_cache.cache_key(source->getDocumentURI(), cache_name, psize);
408 g_free (cache_name);
409 GdkPixbuf *pixbuf = svg_preview_cache.get_preview_from_cache(key); // no ref created
410 if(pixbuf) {
411 return Glib::wrap(pixbuf, true);
412 }
413
414 // Create a copy repr of the marker with id="sample"
415 Inkscape::XML::Document *xml_doc = sandbox->getReprDoc();
416 Inkscape::XML::Node *mrepr = marker->getRepr()->duplicate(xml_doc);
417 mrepr->setAttribute("id", "sample");
418
419 // Replace the old sample in the sandbox by the new one
420 Inkscape::XML::Node *defsrepr = sandbox->getObjectById("defs")->getRepr();
421 SPObject *oldmarker = sandbox->getObjectById("sample");
422 if (oldmarker) {
423 oldmarker->deleteObject(false);
424 }
425
426 // TODO - This causes a SIGTRAP on windows
427 defsrepr->appendChild(mrepr);
428
429 Inkscape::GC::release(mrepr);
430
431 // If the marker color is a url link to a pattern or gradient copy that too
432 SPObject *mk = source->getObjectById(mname);
433 SPCSSAttr *css_marker = sp_css_attr_from_object(mk->firstChild(), SP_STYLE_FLAG_ALWAYS);
434 //const char *mfill = sp_repr_css_property(css_marker, "fill", "none");
435 const char *mstroke = sp_repr_css_property(css_marker, "fill", "none");
436
437 if (!strncmp (mstroke, "url(", 4)) {
438 SPObject *linkObj = getMarkerObj(mstroke, source);
439 if (linkObj) {
440 Inkscape::XML::Node *grepr = linkObj->getRepr()->duplicate(xml_doc);
441 SPObject *oldmarker = sandbox->getObjectById(linkObj->getId());
442 if (oldmarker) {
443 oldmarker->deleteObject(false);
444 }
445 defsrepr->appendChild(grepr);
446 Inkscape::GC::release(grepr);
447
448 if (SP_IS_GRADIENT(linkObj)) {
449 SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (SP_GRADIENT(linkObj), false);
450 if (vector) {
451 Inkscape::XML::Node *grepr = vector->getRepr()->duplicate(xml_doc);
452 SPObject *oldmarker = sandbox->getObjectById(vector->getId());
453 if (oldmarker) {
454 oldmarker->deleteObject(false);
455 }
456 defsrepr->appendChild(grepr);
457 Inkscape::GC::release(grepr);
458 }
459 }
460 }
461 }
462
463 // Uncomment this to get the sandbox documents saved (useful for debugging)
464 //FILE *fp = fopen (g_strconcat(combo_id, mname, ".svg", NULL), "w");
465 //sp_repr_save_stream(sandbox->getReprDoc(), fp);
466 //fclose (fp);
467
468 // object to render; note that the id is the same as that of the combo we're building
469 SPObject *object = sandbox->getObjectById(combo_id);
470 sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
471 sandbox->ensureUpToDate();
472
473 if (object == nullptr || !SP_IS_ITEM(object)) {
474 return sp_get_icon_pixbuf("bad-marker", Gtk::ICON_SIZE_SMALL_TOOLBAR); // sandbox broken?
475 }
476
477 SPItem *item = SP_ITEM(object);
478 // Find object's bbox in document
479 Geom::OptRect dbox = item->documentVisualBounds();
480
481 if (!dbox) {
482 return sp_get_icon_pixbuf("bad-marker", Gtk::ICON_SIZE_SMALL_TOOLBAR);
483 }
484
485 /* Update to renderable state */
486 pixbuf = render_pixbuf(drawing, 0.8, *dbox, psize);
487 svg_preview_cache.set_preview_in_cache(key, pixbuf);
488 return Glib::wrap(pixbuf);
489 }
490
separator_cb(GtkTreeModel * model,GtkTreeIter * iter,gpointer)491 gboolean MarkerComboBox::separator_cb (GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) {
492
493 gboolean sep = FALSE;
494 gtk_tree_model_get(model, iter, 4, &sep, -1);
495 return sep;
496 }
497
498 /**
499 * Returns a new document containing default start, mid, and end markers.
500 */
ink_markers_preview_doc()501 SPDocument *MarkerComboBox::ink_markers_preview_doc ()
502 {
503 gchar const *buffer = R"A(
504 <svg xmlns="http://www.w3.org/2000/svg"
505 xmlns:xlink="http://www.w3.org/1999/xlink"
506 id="MarkerSample">
507
508 <defs id="defs"/>
509
510 <g id="marker-start">
511 <path style="fill:white;stroke:black;stroke-width:1.7;stroke-opacity:0.2;marker-start:url(#sample)"
512 d="M 12.5,13 L 25,13"/>
513 <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/>
514 </g>
515
516 <g id="marker-mid">
517 <path style="fill:white;stroke:black;stroke-width:1.7;stroke-opacity:0.2;marker-mid:url(#sample)"
518 d="M 0,113 L 12.5,113 L 25,113"/>
519 <rect x="0" y="100" width="25" height="25" style="fill:none;stroke:none"/>
520 </g>
521
522 <g id="marker-end">
523 <path style="fill:white;stroke:black;stroke-width:1.7;stroke-opacity:0.2;marker-end:url(#sample)"
524 d="M 0,213 L 12.5,213"/>
525 <rect x="0" y="200" width="25" height="25" style="fill:none;stroke:none"/>
526 </g>
527
528 </svg>
529 )A";
530
531 return SPDocument::createNewDocFromMem (buffer, strlen(buffer), FALSE);
532 }
533
534 } // namespace Widget
535 } // namespace UI
536 } // namespace Inkscape
537
538 /*
539 Local Variables:
540 mode:c++
541 c-file-style:"stroustrup"
542 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
543 indent-tabs-mode:nil
544 fill-column:99
545 End:
546 */
547 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
548