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