diff options
Diffstat (limited to 'src/ui/dialog/paint-servers.cpp')
-rw-r--r-- | src/ui/dialog/paint-servers.cpp | 657 |
1 files changed, 657 insertions, 0 deletions
diff --git a/src/ui/dialog/paint-servers.cpp b/src/ui/dialog/paint-servers.cpp new file mode 100644 index 0000000..42f437c --- /dev/null +++ b/src/ui/dialog/paint-servers.cpp @@ -0,0 +1,657 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * Rafael Siejakowski <rs@rs-math.net> + * + * Copyright (C) 2019 Valentin Ionita + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <map> + +#include <giomm/listmodel.h> +#include <glibmm/regex.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/iconview.h> +#include <gtkmm/liststore.h> +#include <gtkmm/stockid.h> +#include <gtkmm/switch.h> + +#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"=====( +<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> + <defs id="Defs"/> + <rect id="Back" x="0" y="0" width="100px" height="100px" fill="lightgray"/> + <rect id="Rect" x="0" y="0" width="100px" height="100px" stroke="black"/> +</svg> +)====="; + +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<Gtk::Builder> 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<Glib::RefPtr<Gtk::TreeModel>>(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<Glib::ustring>, std::optional<Glib::ustring>> +PaintServersDialog::_findCommonFillAndStroke(std::vector<SPObject *> 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<Glib::ustring> &list) +{ + g_return_if_fail(in != nullptr); + + // Add paint servers in <defs> section. + if (is<SPPaintServer>(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<SPShape>(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<PaintDescription> 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>(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 <defs> of the current document */ +void PaintServersDialog::_loadFromCurrentDocument() +{ + auto document = getDocument(); + if (!document) { + return; + } + + std::vector<PaintDescription> 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<PaintDescription> &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<PaintDescription> 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<Gdk::Pixbuf>(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<Glib::Regex> 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<SPObject *> 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<SPObject *> 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<SPItem *>(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<PaintDescription> &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<Glib::ustring> 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<SPObject *> 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<Gdk::Pixbuf> 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<SPObject *> &output) const +{ + std::vector<SPObject *> 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<SPObject *> PaintServersDialog::_unpackSelection(Selection *selection) const +{ + std::vector<SPObject *> result; + if (!selection) { + return result; + } + + auto const &selected_range = selection->items(); + for (auto const item : selected_range) { + _unpackGroups(static_cast<SPObject *>(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 : |