// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Combobox for selecting dash patterns - implementation. */ /* Author: * Lauris Kaplinski * bulia byak * Maximilian Albert * * Copyright (C) 2002 Lauris Kaplinski * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "stroke-marker-selector.h" #include #include #include "desktop-style.h" #include "path-prefix.h" #include "stroke-style.h" #include "helper/stock-items.h" #include "ui/icon-loader.h" #include "io/sys.h" #include "object/sp-defs.h" #include "object/sp-marker.h" #include "object/sp-root.h" #include "style.h" #include "ui/cache/svg_preview_cache.h" #include "ui/dialog-events.h" #include "ui/util.h" #include "ui/widget/spinbutton.h" static Inkscape::UI::Cache::SvgPreview svg_preview_cache; MarkerComboBox::MarkerComboBox(gchar const *id, int l) : Gtk::ComboBox(), combo_id(id), loc(l), updating(false), markerCount(0) { marker_store = Gtk::ListStore::create(marker_columns); set_model(marker_store); pack_start(image_renderer, false); set_cell_data_func(image_renderer, sigc::mem_fun(*this, &MarkerComboBox::prepareImageRenderer)); gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(gobj()), MarkerComboBox::separator_cb, nullptr, nullptr); empty_image = sp_get_icon_image("no-marker", Gtk::ICON_SIZE_SMALL_TOOLBAR); sandbox = ink_markers_preview_doc (); init_combo(); this->get_style_context()->add_class("combobright"); show(); } MarkerComboBox::~MarkerComboBox() { delete combo_id; delete sandbox; delete empty_image; if (doc) { modified_connection.disconnect(); } } void MarkerComboBox::setDocument(SPDocument *document) { if (doc != document) { if (doc) { modified_connection.disconnect(); } doc = document; if (doc) { modified_connection = doc->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&MarkerComboBox::handleDefsModified), this))) ); } refreshHistory(); } } void MarkerComboBox::handleDefsModified(MarkerComboBox *self) { self->refreshHistory(); } void MarkerComboBox::refreshHistory() { if (updating) return; updating = true; std::vector ml = get_marker_list(doc); /* * Seems to be no way to get notified of changes just to markers, * so listen to changes in all defs and check if the number of markers has changed here * to avoid unnecessary refreshes when things like gradients change */ if (markerCount != ml.size()) { const char *active = get_active()->get_value(marker_columns.marker); sp_marker_list_from_doc(doc, true); set_selected(active); markerCount = ml.size(); } updating = false; } /** * Init the combobox widget to display markers from markers.svg */ void MarkerComboBox::init_combo() { if (updating) return; static SPDocument *markers_doc = nullptr; // add separator Gtk::TreeModel::Row row_sep = *(marker_store->append()); row_sep[marker_columns.label] = "Separator"; row_sep[marker_columns.marker] = g_strdup("None"); row_sep[marker_columns.image] = NULL; row_sep[marker_columns.stock] = false; row_sep[marker_columns.history] = false; row_sep[marker_columns.separator] = true; // find and load markers.svg if (markers_doc == nullptr) { char *markers_source = g_build_filename(INKSCAPE_MARKERSDIR, "markers.svg", NULL); if (Inkscape::IO::file_test(markers_source, G_FILE_TEST_IS_REGULAR)) { markers_doc = SPDocument::createNewDoc(markers_source, FALSE); } g_free(markers_source); } // load markers from markers.svg if (markers_doc) { sp_marker_list_from_doc(markers_doc, false); } set_sensitive(true); } /** * Sets the current marker in the marker combobox. */ void MarkerComboBox::set_current(SPObject *marker) { updating = true; if (marker != nullptr) { gchar *markname = g_strdup(marker->getRepr()->attribute("id")); set_selected(markname); g_free (markname); } else { set_selected(nullptr); } updating = false; } /** * Return a uri string representing the current selected marker used for setting the marker style in the document */ const gchar * MarkerComboBox::get_active_marker_uri() { /* Get Marker */ const gchar *markid = get_active()->get_value(marker_columns.marker); if (!markid) { return nullptr; } gchar const *marker = ""; if (strcmp(markid, "none")) { bool stockid = get_active()->get_value(marker_columns.stock); gchar *markurn; if (stockid) { markurn = g_strconcat("urn:inkscape:marker:",markid,NULL); } else { markurn = g_strdup(markid); } SPObject *mark = get_stock_item(markurn, stockid); g_free(markurn); if (mark) { Inkscape::XML::Node *repr = mark->getRepr(); marker = g_strconcat("url(#", repr->attribute("id"), ")", NULL); } } else { marker = g_strdup(markid); } return marker; } void MarkerComboBox::set_active_history() { const gchar *markid = get_active()->get_value(marker_columns.marker); // If forked from a stockid, add the stockid SPObject const *marker = doc->getObjectById(markid); if (marker && marker->getRepr()->attribute("inkscape:stockid")) { markid = marker->getRepr()->attribute("inkscape:stockid"); } set_selected(markid); } void MarkerComboBox::set_selected(const gchar *name, gboolean retry/*=true*/) { if (!name) { set_active(0); return; } for(Gtk::TreeIter iter = marker_store->children().begin(); iter != marker_store->children().end(); ++iter) { Gtk::TreeModel::Row row = (*iter); if (row[marker_columns.marker] && !strcmp(row[marker_columns.marker], name)) { set_active(iter); return; } } // Didn't find it in the list, try refreshing from the doc if (retry) { sp_marker_list_from_doc(doc, true); set_selected(name, false); } } /** * Pick up all markers from source, except those that are in * current_doc (if non-NULL), and add items to the combo. */ void MarkerComboBox::sp_marker_list_from_doc(SPDocument *source, gboolean history) { std::vector ml = get_marker_list(source); remove_markers(history); // Seem to need to remove 2x remove_markers(history); add_markers(ml, source, history); } /** * Returns a list of markers in the defs of the given source document as a vector * Returns NULL if there are no markers in the document. */ std::vector MarkerComboBox::get_marker_list (SPDocument *source) { std::vector ml; if (source == nullptr) return ml; SPDefs *defs = source->getDefs(); if (!defs) { return ml; } for (auto& child: defs->children) { if (SP_IS_MARKER(&child)) { ml.push_back(SP_MARKER(&child)); } } return ml; } /** * Remove history or non-history markers from the combo */ void MarkerComboBox::remove_markers (gboolean history) { // Having the model set causes assertions when erasing rows, temporarily disconnect unset_model(); for(Gtk::TreeIter iter = marker_store->children().begin(); iter != marker_store->children().end(); ++iter) { Gtk::TreeModel::Row row = (*iter); if (row[marker_columns.history] == history && row[marker_columns.separator] == false) { marker_store->erase(iter); iter = marker_store->children().begin(); } } set_model(marker_store); } /** * Adds markers in marker_list to the combo */ void MarkerComboBox::add_markers (std::vector const& marker_list, SPDocument *source, gboolean history) { // Do this here, outside of loop, to speed up preview generation: Inkscape::Drawing drawing; unsigned const visionkey = SPItem::display_key_new(1); drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); // Find the separator, Gtk::TreeIter sep_iter; for(Gtk::TreeIter iter = marker_store->children().begin(); iter != marker_store->children().end(); ++iter) { Gtk::TreeModel::Row row = (*iter); if (row[marker_columns.separator]) { sep_iter = iter; } } if (history) { // add "None" Gtk::TreeModel::Row row = *(marker_store->prepend()); row[marker_columns.label] = C_("Marker", "None"); row[marker_columns.stock] = false; row[marker_columns.marker] = g_strdup("None"); row[marker_columns.image] = NULL; row[marker_columns.history] = true; row[marker_columns.separator] = false; } for (auto i:marker_list) { Inkscape::XML::Node *repr = i->getRepr(); gchar const *markid = repr->attribute("inkscape:stockid") ? repr->attribute("inkscape:stockid") : repr->attribute("id"); // generate preview Gtk::Image *prv = create_marker_image (24, repr->attribute("id"), source, drawing, visionkey); prv->show(); // Add history before separator, others after Gtk::TreeModel::Row row; if (history) row = *(marker_store->insert(sep_iter)); else row = *(marker_store->append()); row[marker_columns.label] = ink_ellipsize_text(markid, 20); // Non "stock" markers can also have "inkscape:stockid" (when using extension ColorMarkers), // So use !is_history instead to determine is it is "stock" (ie in the markers.svg file) row[marker_columns.stock] = !history; row[marker_columns.marker] = repr->attribute("id"); row[marker_columns.image] = prv; row[marker_columns.history] = history; row[marker_columns.separator] = false; } sandbox->getRoot()->invoke_hide(visionkey); } /* * Remove from the cache and recreate a marker image */ void MarkerComboBox::update_marker_image(gchar const *mname) { gchar *cache_name = g_strconcat(combo_id, mname, NULL); Glib::ustring key = svg_preview_cache.cache_key(doc->getDocumentURI(), cache_name, 24); g_free (cache_name); svg_preview_cache.remove_preview_from_cache(key); Inkscape::Drawing drawing; unsigned const visionkey = SPItem::display_key_new(1); drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); Gtk::Image *prv = create_marker_image(24, mname, doc, drawing, visionkey); if (prv) { prv->show(); } sandbox->getRoot()->invoke_hide(visionkey); for(const auto & iter : marker_store->children()) { Gtk::TreeModel::Row row = iter; if (row[marker_columns.marker] && row[marker_columns.history] && !strcmp(row[marker_columns.marker], mname)) { row[marker_columns.image] = prv; return; } } } /** * Creates a copy of the marker named mname, determines its visible and renderable * area in the bounding box, and then renders it. This allows us to fill in * preview images of each marker in the marker combobox. */ Gtk::Image * MarkerComboBox::create_marker_image(unsigned psize, gchar const *mname, SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/) { // Retrieve the marker named 'mname' from the source SVG document SPObject const *marker = source->getObjectById(mname); if (marker == nullptr) { return nullptr; } // Create a copy repr of the marker with id="sample" Inkscape::XML::Document *xml_doc = sandbox->getReprDoc(); Inkscape::XML::Node *mrepr = marker->getRepr()->duplicate(xml_doc); mrepr->setAttribute("id", "sample"); // Replace the old sample in the sandbox by the new one Inkscape::XML::Node *defsrepr = sandbox->getObjectById("defs")->getRepr(); SPObject *oldmarker = sandbox->getObjectById("sample"); if (oldmarker) { oldmarker->deleteObject(false); } // TODO - This causes a SIGTRAP on windows defsrepr->appendChild(mrepr); Inkscape::GC::release(mrepr); // If the marker color is a url link to a pattern or gradient copy that too SPObject *mk = source->getObjectById(mname); SPCSSAttr *css_marker = sp_css_attr_from_object(mk->firstChild(), SP_STYLE_FLAG_ALWAYS); //const char *mfill = sp_repr_css_property(css_marker, "fill", "none"); const char *mstroke = sp_repr_css_property(css_marker, "fill", "none"); if (!strncmp (mstroke, "url(", 4)) { SPObject *linkObj = getMarkerObj(mstroke, source); if (linkObj) { Inkscape::XML::Node *grepr = linkObj->getRepr()->duplicate(xml_doc); SPObject *oldmarker = sandbox->getObjectById(linkObj->getId()); if (oldmarker) { oldmarker->deleteObject(false); } defsrepr->appendChild(grepr); Inkscape::GC::release(grepr); if (SP_IS_GRADIENT(linkObj)) { SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (SP_GRADIENT(linkObj), false); if (vector) { Inkscape::XML::Node *grepr = vector->getRepr()->duplicate(xml_doc); SPObject *oldmarker = sandbox->getObjectById(vector->getId()); if (oldmarker) { oldmarker->deleteObject(false); } defsrepr->appendChild(grepr); Inkscape::GC::release(grepr); } } } } // Uncomment this to get the sandbox documents saved (useful for debugging) //FILE *fp = fopen (g_strconcat(combo_id, mname, ".svg", NULL), "w"); //sp_repr_save_stream(sandbox->getReprDoc(), fp); //fclose (fp); // object to render; note that the id is the same as that of the combo we're building SPObject *object = sandbox->getObjectById(combo_id); sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); sandbox->ensureUpToDate(); if (object == nullptr || !SP_IS_ITEM(object)) { return nullptr; // sandbox broken? } SPItem *item = SP_ITEM(object); // Find object's bbox in document Geom::OptRect dbox = item->documentVisualBounds(); if (!dbox) { return nullptr; } /* Update to renderable state */ gchar *cache_name = g_strconcat(combo_id, mname, NULL); Glib::ustring key = svg_preview_cache.cache_key(source->getDocumentURI(), cache_name, psize); g_free (cache_name); GdkPixbuf *pixbuf = svg_preview_cache.get_preview_from_cache(key); // no ref created if (!pixbuf) { pixbuf = render_pixbuf(drawing, 0.8, *dbox, psize); svg_preview_cache.set_preview_in_cache(key, pixbuf); g_object_unref(pixbuf); // reference is held by svg_preview_cache } // Create widget Gtk::Image *pb = Glib::wrap(GTK_IMAGE(gtk_image_new_from_pixbuf(pixbuf))); return pb; } void MarkerComboBox::prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ) { Gtk::Image *image = (*row)[marker_columns.image]; if (image) image_renderer.property_pixbuf() = image->get_pixbuf(); else image_renderer.property_pixbuf() = empty_image->get_pixbuf(); } gboolean MarkerComboBox::separator_cb (GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) { gboolean sep = FALSE; gtk_tree_model_get(model, iter, 4, &sep, -1); return sep; } /** * Returns a new document containing default start, mid, and end markers. */ SPDocument *MarkerComboBox::ink_markers_preview_doc () { gchar const *buffer = R"A( )A"; return SPDocument::createNewDocFromMem (buffer, strlen(buffer), FALSE); } /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :