// SPDX-License-Identifier: GPL-2.0-or-later /** @file * @brief Paint Servers dialog */ /* Authors: * Valentin Ionita * Rafael Siejakowski * * Copyright (C) 2019 Valentin Ionita * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include #include #include #include #include #include #include #include "document.h" #include "inkscape.h" #include "paint-servers.h" #include "path-prefix.h" #include "style.h" #include "io/resource.h" #include "object/sp-defs.h" #include "object/sp-hatch.h" #include "object/sp-pattern.h" #include "object/sp-root.h" #include "ui/cache/svg_preview_cache.h" #include "ui/widget/scrollprotected.h" #include "xml/href-attribute-helper.h" namespace Inkscape { namespace UI { namespace Dialog { static Glib::ustring const wrapper = R"=====( )====="; const char *ALLDOCS = N_("All paint servers"); const char *CURRENTDOC = N_("Current document"); PaintServersDialog::PaintServersDialog() : DialogBase("/dialogs/paint", "PaintServers") , _targetting_fill(true) , columns() { current_store = ALLDOCS; store[ALLDOCS] = Gtk::ListStore::create(columns); // Get wrapper document (rectangle to fill with paint server). preview_document = SPDocument::createNewDocFromMem(wrapper.c_str(), wrapper.length(), true); SPObject *rect = preview_document->getObjectById("Rect"); SPObject *defs = preview_document->getObjectById("Defs"); if (!rect || !defs) { g_warn_message("Inkscape", __FILE__, __LINE__, __func__, "Failed to get wrapper defs or rectangle for preview document!"); } unsigned key = SPItem::display_key_new(1); preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); preview_document->ensureUpToDate(); renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY)); _buildDialogWindow("dialog-paint-servers.glade"); _loadStockPaints(); } PaintServersDialog::~PaintServersDialog() { _defs_changed.disconnect(); _document_closed.disconnect(); } /** Handles the replacement of the document that we edit */ void PaintServersDialog::documentReplaced() { _defs_changed.disconnect(); _document_closed.disconnect(); auto document = getDocument(); if (!document) { return; } document_map[CURRENTDOC] = document; _loadFromCurrentDocument(); _regenerateAll(); if (auto const defs = document->getDefs()) { _defs_changed = defs->connectModified([=](SPObject *, unsigned) -> void { _loadFromCurrentDocument(); _regenerateAll(); }); } _document_closed = document->connectDestroy([=]() { _documentClosed(); }); } /** Builds the dialog window from a Glade file and attaches event handlers */ void PaintServersDialog::_buildDialogWindow(char const *const glade_file) { // Load the dialog from the Glade file auto file_path = get_filename_string(IO::Resource::UIS, glade_file); Glib::RefPtr builder; try { builder = Gtk::Builder::create_from_file(file_path); } catch (Glib::Error const &e) { Glib::ustring message{"Could not load the Glade file for the Paint Servers dialog: "}; message += file_path + "\n" + e.what(); g_warn_message("Inkscape", __FILE__, __LINE__, __func__, message.c_str()); return; } // Place top-level grid container in the window Gtk::Grid *container = nullptr; builder->get_widget("PaintServersContainerGrid", container); if (container) { pack_start(*container, Gtk::PACK_EXPAND_WIDGET); } else { return; } builder->get_widget("ServersDropdown", dropdown); dropdown->append(ALLDOCS, _(ALLDOCS)); dropdown->set_active_id(ALLDOCS); dropdown->signal_changed().connect([=]() { onPaintSourceDocumentChanged(); }); builder->get_widget("PaintIcons", icon_view); icon_view->set_model(static_cast>(store[current_store])); icon_view->set_tooltip_column(columns.id.index()); icon_view->set_pixbuf_column(columns.pixbuf.index()); _item_activated = icon_view->signal_item_activated().connect([=](Gtk::TreeModel::Path const &p) { onPaintClicked(p); }); Gtk::RadioButton *fill_radio = nullptr; builder->get_widget("TargetRadioFill", fill_radio); fill_radio->signal_toggled().connect([=]() { _targetting_fill = fill_radio->get_active(); _updateActiveItem(); }); } /** Handles the destruction of the current document */ void PaintServersDialog::_documentClosed() { _defs_changed.disconnect(); _document_closed.disconnect(); document_map.erase(CURRENTDOC); store[CURRENTDOC]->clear(); _regenerateAll(); } /** * @brief Returns the Fill and Stroke, in this order, common to a list of objects * @param objects - a vector of pointers to objects whose fill and stroke will be analysed * @return a tuple of optionals which are empty when the vector of objects is empty or * when either fill or stroke are not common to all of them. Otherwise, the first * element of the tuple is the common fill, as a Glib::ustring, and the second one * is the common stroke. */ std::tuple, std::optional> PaintServersDialog::_findCommonFillAndStroke(std::vector const &objects) const { MaybeString common_fill, common_stroke; if (!objects.empty()) { Glib::ustring candidate_fill = objects[0]->style->fill.get_value(); Glib::ustring candidate_stroke = objects[0]->style->stroke.get_value(); bool fills_agree = true; bool strokes_agree = true; size_t const count = objects.size(); for (size_t i = 1; i < count; i++) { if (fills_agree && candidate_fill != objects[i]->style->fill.get_value()) { fills_agree = false; } if (strokes_agree && candidate_stroke != objects[i]->style->stroke.get_value()) { strokes_agree = false; } } if (fills_agree) { common_fill = candidate_fill; } if (strokes_agree) { common_stroke = candidate_stroke; } } return std::tuple(common_fill, common_stroke); } /** Finds paints used by an object and (recursively) by its descendants * @param in - the object whose paints to grab * @param list - the paints will be added to this vector as strings usable in the `fill` CSS property */ void PaintServersDialog::_findPaints(SPObject *in, std::vector &list) { g_return_if_fail(in != nullptr); // Add paint servers in section. if (is(in)) { if (in->getId()) { // Need to check as one can't construct Glib::ustring with nullptr. list.push_back(Glib::ustring("url(#") + in->getId() + ")"); } // Don't recurse into paint servers. return; } // Add paint servers referenced by shapes. if (is(in)) { auto const style = in->style; list.push_back(style->fill.get_value()); list.push_back(style->stroke.get_value()); } for (auto child: in->childList(false)) { PaintServersDialog::_findPaints(child, list); } } /** Load stock paints from files in share/paint */ void PaintServersDialog::_loadStockPaints() { std::vector paints; // Extract out paints from files in share/paint. for (auto const &path : get_filenames(Inkscape::IO::Resource::PAINT, {".svg"})) { try { // createNewDoc throws auto doc = std::unique_ptr(SPDocument::createNewDoc(path.c_str(), false)); if (!doc) { throw std::exception(); } _loadPaintsFromDocument(doc.get(), paints); _stock_documents.push_back(std::move(doc)); // Ensures eventual destruction in our dtor } catch (std::exception &e) { auto message = Glib::ustring{"Cannot open paint server resource file '"} + path + "'!"; g_warn_message("Inkscape", __FILE__, __LINE__, __func__, message.c_str()); continue; } } _createPaints(paints); } /** Load paint servers from the of the current document */ void PaintServersDialog::_loadFromCurrentDocument() { auto document = getDocument(); if (!document) { return; } std::vector paints; _loadPaintsFromDocument(document, paints); // There can only be one current document, so we clear the corresponding store store[CURRENTDOC]->clear(); _createPaints(paints); } /** Creates a collection of paints from the given vector of descriptions */ void PaintServersDialog::_createPaints(std::vector &collection) { // Sort and remove duplicates. auto paints_cmp = [](PaintDescription const &a, PaintDescription const &b) -> bool { return a.url < b.url; }; std::sort(collection.begin(), collection.end(), paints_cmp); collection.erase(std::unique(collection.begin(), collection.end()), collection.end()); for (auto &paint : collection) { _instantiatePaint(paint); } } /** Create a paint from a description and generate its bitmap preview */ void PaintServersDialog::_instantiatePaint(PaintDescription &paint) { if (!paint.has_preview()) { _generateBitmapPreview(paint); } if (paint.has_preview()) { // don't add the paint if preview generation failed. _addToStore(paint); } } /** Adds a paint to store */ void PaintServersDialog::_addToStore(PaintDescription &paint) { if (store.find(paint.doc_title) == store.end()) { store[paint.doc_title] = Gtk::ListStore::create(columns); } auto iter = store[paint.doc_title]->append(); paint.write_to_iterator(iter, &columns); if (document_map.find(paint.doc_title) == document_map.end()) { document_map[paint.doc_title] = paint.source_document; dropdown->append(paint.doc_title, _(paint.doc_title.c_str())); } } /** Returns a PaintDescription for a paint already present in the store */ PaintDescription PaintServersDialog::_descriptionFromIterator(Gtk::ListStore::iterator const &iter) const { Glib::ustring doc_title = (*iter)[columns.document]; SPDocument *doc_ptr; try { doc_ptr = document_map.at(doc_title); } catch (std::out_of_range &exception) { doc_ptr = nullptr; } Glib::ustring paint_url = (*iter)[columns.paint]; PaintDescription result(doc_ptr, doc_title, std::move(paint_url)); // Fill in fields that are set only on instantiation result.id = (*iter)[columns.id]; result.bitmap = (*iter)[columns.pixbuf]; return result; } /** Regenerates the list of all paint servers from the already loaded paints */ void PaintServersDialog::_regenerateAll() { bool showing_all = (current_store == ALLDOCS); std::vector all_paints; for (auto const &[doc, paint_list] : store) { if (doc == ALLDOCS) { continue; // ignore the target store } paint_list->foreach_iter([&](Gtk::ListStore::iterator const &paint) -> bool { all_paints.push_back(_descriptionFromIterator(paint)); return false; }); } // Sort and remove duplicates. When the duplicate entry is from the current document, // we remove it preferentially, keeping the stock paint if available. std::sort(all_paints.begin(), all_paints.end(), [=](PaintDescription const &a, PaintDescription const &b) -> bool { int cmp = a.url.compare(b.url); if (cmp < 0) return true; if (cmp > 0) return false; auto external = [=] (PaintDescription const &p) { return p.doc_title != CURRENTDOC; }; return external(a) && !external(b); }); all_paints.erase(std::unique(all_paints.begin(), all_paints.end()), all_paints.end()); store[ALLDOCS]->clear(); // Add paints from the cleaned up list to the store for (auto &&paint : all_paints) { auto iter = store[ALLDOCS]->append(); paint.write_to_iterator(iter, &columns); } if (showing_all) { selectionChanged(getSelection()); } } /** Generates the bitmap preview for the given paint */ void PaintServersDialog::_generateBitmapPreview(PaintDescription &paint) { SPObject *rect = preview_document->getObjectById("Rect"); SPObject *defs = preview_document->getObjectById("Defs"); paint.bitmap = Glib::RefPtr(nullptr); if (paint.url.empty()) { return; } // Set style on the preview rectangle SPCSSAttr *css = sp_repr_css_attr_new(); sp_repr_css_set_property(css, "fill", paint.url.c_str()); rect->changeCSS(css, "style"); sp_repr_css_attr_unref(css); // Insert paint into the defs of the preview document if required Glib::MatchInfo matchInfo; static Glib::RefPtr regex = Glib::Regex::create("url\\(#([A-Za-z0-9#._-]*)\\)"); regex->match(paint.url, matchInfo); if (!matchInfo.matches()) { // Currently we only show previews for hatches/patterns of the form url(#some-id) // TODO: handle colors, gradients, etc. // See https://wiki.inkscape.org/wiki/Google_Summer_of_Code#P11._Improvements_to_Paint_Server_Dialog return; } paint.id = matchInfo.fetch(1); // Delete old paints if necessary std::vector old_paints = preview_document->getObjectsBySelector("defs > *"); for (auto paint : old_paints) { paint->deleteObject(false); } // Find the new paint SPObject *new_paint = paint.source_document->getObjectById(paint.id); if (!new_paint) { Glib::ustring error_message = Glib::ustring{"Cannot find paint server: "} + paint.id; g_warn_message("Inkscape", __FILE__, __LINE__, __func__, error_message.c_str()); return; } // Add the new paint along with all paints it refers to XML::Document *xml_doc = preview_document->getReprDoc(); std::vector encountered{new_paint}; ///< For the prevention of cyclic refs while (new_paint) { auto const *new_repr = new_paint->getRepr(); if (!new_repr) { break; } // Create a copy repr of the paint defs->appendChild(new_repr->duplicate(xml_doc)); // Check for cross-references in the paint auto ref = Inkscape::getHrefAttribute(*new_repr).second; if (ref) { // Paint is cross-referencing another object (probably another paint); // we must copy the referenced object as well new_paint = paint.source_document->getObjectByHref(ref); using namespace std; if (find(begin(encountered), end(encountered), new_paint) == end(encountered)) { encountered.push_back(new_paint); } else { break; // Break reference cycle } } else { // No more hrefs break; } } preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); preview_document->ensureUpToDate(); if (Geom::OptRect dbox = static_cast(rect)->visualBounds()) { unsigned size = std::ceil(std::max(dbox->width(), dbox->height())); paint.bitmap = Glib::wrap(render_pixbuf(renderDrawing, 1, *dbox, size)); } } /** @brief Load paint servers from the given source document * @param document - the source document * @param output - the paint descriptions will be added to this vector */ void PaintServersDialog::_loadPaintsFromDocument(SPDocument *document, std::vector &output) { Glib::ustring document_title; if (!document->getRoot()->title()) { document_title = CURRENTDOC; } else { document_title = Glib::ustring(document->getRoot()->title()); } // Find all paints std::vector urls; _findPaints(document->getRoot(), urls); for (auto const &url : urls) { output.emplace_back(document, document_title, std::move(url)); } } /** Handles the change of the dropdown for selecting paint sources */ void PaintServersDialog::onPaintSourceDocumentChanged() { current_store = dropdown->get_active_id(); icon_view->set_model(store[current_store]); _updateActiveItem(); } /** Event handler for when a paint entry in the dialog has been activated */ void PaintServersDialog::onPaintClicked(Gtk::TreeModel::Path const &path) { // Get the current selected elements Selection *selection = getSelection(); std::vector items = _unpackSelection(selection); if (items.empty()) { return; } Gtk::ListStore::iterator iter = store[current_store]->get_iter(path); Glib::ustring id = (*iter)[columns.id]; Glib::ustring paint = (*iter)[columns.paint]; Glib::RefPtr pixbuf = (*iter)[columns.pixbuf]; Glib::ustring hatches_document_title = (*iter)[columns.document]; SPDocument *hatches_document = document_map[hatches_document_title]; SPObject *paint_server = hatches_document->getObjectById(id); bool paint_server_exists = false; for (auto const &server : store[CURRENTDOC]->children()) { if (server[columns.id] == id) { paint_server_exists = true; break; } } SPDocument *document = getDocument(); if (!paint_server_exists) { // Add the paint server to the current document definition Inkscape::XML::Document *xml_doc = document->getReprDoc(); Inkscape::XML::Node *repr = paint_server->getRepr()->duplicate(xml_doc); document->getDefs()->appendChild(repr); Inkscape::GC::release(repr); // Add the pixbuf to the current document store iter = store[CURRENTDOC]->append(); (*iter)[columns.id] = id; (*iter)[columns.paint] = paint; (*iter)[columns.pixbuf] = pixbuf; (*iter)[columns.document] = CURRENTDOC; } for (auto item : items) { item->style->getFillOrStroke(_targetting_fill)->read(paint.c_str()); item->updateRepr(); } document->collectOrphans(); } /** * @brief Handles the change in the selection, finding common fill or stroke for selected objects * @param selection - the new selection */ void PaintServersDialog::selectionChanged(Selection* selection) { if (!selection || selection->isEmpty()) { _common_fill.reset(); _common_stroke.reset(); } else { auto const selected_items = _unpackSelection(selection); auto const &[fill, stroke] = _findCommonFillAndStroke(selected_items); _common_fill = std::move(fill); _common_stroke = std::move(stroke); } _updateActiveItem(); } /** * Recursively extracts non-group elements from groups, if any * @param parent - the parent object which will be unpacked recursively * @param output - the resulting SPObject pointers will be added to this vector */ void PaintServersDialog::_unpackGroups(SPObject *parent, std::vector &output) const { std::vector children = parent->childList(false); if (children.empty()) { output.push_back(parent); } else { for (auto child : children) { _unpackGroups(child, output); } } } /** * @brief Recursively unpacks groups in the given selection * @param selection - a pointer to an Inkscape::Selection object to be unpacked * @return a vector of SPObject pointers to the unpacked selected items. */ std::vector PaintServersDialog::_unpackSelection(Selection *selection) const { std::vector result; if (!selection) { return result; } auto const &selected_range = selection->items(); for (auto const item : selected_range) { _unpackGroups(static_cast(item), result); } return result; } /** * @brief Updates the active item in the icon view to reflect the common paint of the * fill or stroke of selected objects */ void PaintServersDialog::_updateActiveItem() { _item_activated.block(); MaybeString &common = (_targetting_fill ? _common_fill : _common_stroke); if (common) { bool found = false; store[current_store]->foreach( [&](Gtk::ListStore::Path const &path, Gtk::ListStore::iterator const &icon) -> bool { if ((*icon)[columns.paint] == *common) { icon_view->select_path(path); found = true; return true; // Finish iterating } return false; }); if (!found) { icon_view->unselect_all(); } } else { icon_view->unselect_all(); } _item_activated.unblock(); } //---------------------------------------------------------------------------------------------------- PaintDescription::PaintDescription(SPDocument *source_doc, Glib::ustring title, Glib::ustring const &&paint_url) : source_document{source_doc} , doc_title{std::move(title)} , id{} // id will be filled in when generating the bitmap , url{paint_url} , bitmap{nullptr} {} /** Write the data stored in this struct to a list store * @param it - the iterator to the ListStore to write to */ void PaintDescription::write_to_iterator(Gtk::ListStore::iterator &it, PaintServersColumns const *cols) const { (*it)[cols->id] = id; (*it)[cols->paint] = url; (*it)[cols->pixbuf] = bitmap; (*it)[cols->document] = doc_title; } } // namespace Dialog } // namespace UI } // namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-basic-offset:2 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 :