// SPDX-License-Identifier: GPL-2.0-or-later #include "document-resources.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "color.h" #include "display/cairo-utils.h" #include "document.h" #include "extension/system.h" #include "helper/choose-file.h" #include "helper/save-image.h" #include "inkscape.h" #include "object/sp-filter.h" #include "object/filters/sp-filter-primitive.h" #include "object/color-profile.h" #include "object/sp-gradient.h" #include "object/sp-font.h" #include "object/sp-image.h" #include "object/sp-item-group.h" #include "object/sp-marker.h" #include "object/sp-mesh-gradient.h" #include "object/sp-object.h" #include "object/sp-offset.h" #include "object/sp-path.h" #include "object/sp-pattern.h" #include "object/tags.h" #include "pattern-manipulation.h" #include "rdf.h" #include "selection.h" #include "ui/builder-utils.h" #include "object/sp-defs.h" #include "object/sp-root.h" #include "object/sp-symbol.h" #include "object/sp-use.h" #include "style.h" #include "ui/dialog/filedialog.h" #include "ui/icon-names.h" #include "ui/themes.h" #include "ui/util.h" #include "ui/widget/shapeicon.h" #include "util/object-renderer.h" #include "util/trim.h" #include "xml/href-attribute-helper.h" namespace Inkscape { namespace UI { namespace Dialog { struct ItemColumns : public Gtk::TreeModel::ColumnRecord { Gtk::TreeModelColumn id; Gtk::TreeModelColumn label; Gtk::TreeModelColumn> image; Gtk::TreeModelColumn editable; Gtk::TreeModelColumn object; Gtk::TreeModelColumn color; ItemColumns() { add(id); add(label); add(image); add(editable); add(object); add(color); } } g_item_columns; struct InfoColumns : public Gtk::TreeModel::ColumnRecord { Gtk::TreeModelColumn item; Gtk::TreeModelColumn value; Gtk::TreeModelColumn count; Gtk::TreeModelColumn object; InfoColumns() { add(item); add(value); add(count); add(object); } } g_info_columns; enum Resources : int { Stats, Colors, Fonts, Styles, Patterns, Symbols, Markers, Gradients, Swatches, Images, Filters, External, Metadata }; const std::unordered_map g_id_to_resource = { {"colors", Colors}, {"swatches", Swatches}, {"fonts", Fonts}, {"stats", Stats}, {"styles", Styles}, {"patterns", Patterns}, {"symbols", Symbols}, {"markers", Markers}, {"gradients", Gradients}, {"images", Images}, {"filters", Filters}, {"external", External}, {"metadata", Metadata}, // to do: SVG fonts // other resources }; size_t get_resource_count(const details::Statistics& stats, Resources rsrc) { switch (rsrc) { case Colors: return stats.colors; case Swatches: return stats.swatches; case Fonts: return stats.fonts; case Symbols: return stats.symbols; case Gradients: return stats.gradients; case Patterns: return stats.patterns; case Images: return stats.images; case Filters: return stats.filters; case Markers: return stats.markers; case Metadata: return stats.metadata; case Styles: return stats.styles; case External: return stats.external_uris; case Stats: return 1; default: break; } return 0; } Resources id_to_resource(const std::string& id) { auto it = g_id_to_resource.find(id); if (it == end(g_id_to_resource)) return Stats; return it->second; } size_t get_resource_count(const std::string& id, const details::Statistics& stats) { auto it = g_id_to_resource.find(id); if (it == end(g_id_to_resource)) return 0; return get_resource_count(stats, it->second); } bool is_resource_present(const std::string& id, const details::Statistics& stats) { return get_resource_count(id, stats) > 0; } std::string choose_file(Glib::ustring title, Gtk::Window* parent, Glib::ustring mime_type, Glib::ustring file_name) { static std::string current_folder; return Inkscape::choose_file_save(title, parent, mime_type, file_name, current_folder); } void save_gimp_palette(std::string fname, const std::vector& colors, const char* name) { try { std::ostringstream ost; ost << "GIMP Palette\n"; if (name && *name) { ost << "Name: " << name << "\n"; } ost << "#\n"; for (auto c : colors) { auto r = (c >> 16) & 0xff; auto g = (c >> 8) & 0xff; auto b = c & 0xff; ost << r << ' ' << g << ' ' << b << '\n'; } Glib::file_set_contents(fname, ost.str()); } catch (Glib::Exception& ex) { g_warning("Error saving color palette: %s", ex.what().c_str()); } catch (...) { g_warning("Error saving color palette."); } } void extract_colors(Gtk::Window* parent, const std::vector& colors, const char* name) { if (colors.empty() || !parent) return; auto fname = choose_file(_("Export Color Palette"), parent, "application/color-palette", "color-palette.gpl"); if (fname.empty()) return; // export palette save_gimp_palette(fname, colors, name); } void delete_object(SPObject* object, Inkscape::Selection* selection) { if (!object || !selection) return; auto document = object->document; if (auto pattern = cast(object)) { // delete action fails for patterns; remove them by deleting their nodes sp_repr_unparent(pattern->getRepr()); DocumentUndo::done(document, _("Delete pattern"), INKSCAPE_ICON("document-resources")); } else if (auto gradient = cast(object)) { // delete action fails for gradients; remove them by deleting their nodes sp_repr_unparent(gradient->getRepr()); DocumentUndo::done(document, _("Delete gradient"), INKSCAPE_ICON("document-resources")); } else { selection->set(object); selection->deleteItems(); } } namespace details { // editing "inkscape:label" Glib::ustring get_inkscape_label(const SPObject& object) { auto label = object.getAttribute("inkscape:label"); return Glib::ustring(label ? label : ""); } void set_inkscape_label(SPObject& object, const Glib::ustring& label) { object.setAttribute("inkscape:label", label.c_str()); } // editing title element Glib::ustring get_title(const SPObject& object) { auto title = object.title(); Glib::ustring str(title ? title : ""); g_free(title); return str; } void set_title(SPObject& object, const Glib::ustring& title) { object.setTitle(title.c_str()); } } // label editing: get/set functions for various object types; // by default "inkscape:label" will be used (expressed as SPObject); // if some types need exceptions to this ruke, they can provide their own edit functions; // note: all most-derived types need to be listed to specify overrides std::map> g_get_label = { // default: editing "inkscape:label" as a description; // patterns use Inkscape-specific "inkscape:label" attribute; // gradients can also use labels instead of IDs; // filters; to do - editing in a tree view; // images can use both, label & title; defaulting to label for consistency {typeid(SPObject), details::get_inkscape_label}, // exception: symbols use element for description {typeid(SPSymbol), details::get_title}, // markers use stockid for some reason - label: to do {typeid(SPMarker), details::get_inkscape_label}, }; std::map<std::type_index, std::function<void (SPObject&, const Glib::ustring&)>> g_set_label = { {typeid(SPObject), details::set_inkscape_label}, {typeid(SPSymbol), details::set_title}, {typeid(SPMarker), details::set_inkscape_label}, }; // liststore columns from glade file constexpr int COL_ID = 1; constexpr int COL_ICON = 2; constexpr int COL_COUNT = 3; DocumentResources::DocumentResources() : DialogBase("/dialogs/document-resources", "DocumentResources"), _builder(create_builder("dialog-document-resources.glade")), _iconview(get_widget<Gtk::IconView>(_builder, "iconview")), _treeview(get_widget<Gtk::TreeView>(_builder, "treeview")), _selector(get_widget<Gtk::TreeView>(_builder, "tree")), _edit(get_widget<Gtk::Button>(_builder, "edit")), _select(get_widget<Gtk::Button>(_builder, "select")), _delete(get_widget<Gtk::Button>(_builder, "delete")), _extract(get_widget<Gtk::Button>(_builder, "extract")), _search(get_widget<Gtk::SearchEntry>(_builder, "search")) { _info_store = Gtk::ListStore::create(g_info_columns); _item_store = Gtk::ListStore::create(g_item_columns); auto filtered_info = Gtk::TreeModelFilter::create(_info_store); auto filtered_items = Gtk::TreeModelFilter::create(_item_store); auto model = Gtk::TreeModelSort::create(filtered_items); model->set_sort_column(g_item_columns.label.index(), Gtk::SORT_ASCENDING); add(get_widget<Gtk::Box>(_builder, "main")); _iconview.set_model(model); _iconview.set_text_column(g_item_columns.label); _label_renderer = dynamic_cast<Gtk::CellRendererText*>(_iconview.get_first_cell()); assert(_label_renderer); _label_renderer->property_editable() = true; _label_renderer->signal_editing_started().connect([=](Gtk::CellEditable* cell, const Glib::ustring& path){ start_editing(cell, path); }); _label_renderer->signal_edited().connect([=](const Glib::ustring& path, const Glib::ustring& new_text){ end_editing(path, new_text); }); _iconview.pack_start(_image_renderer); _iconview.add_attribute(_image_renderer, "surface", g_item_columns.image); _treeview.set_model(filtered_info); auto treestore = get_object<Gtk::ListStore>(_builder, "liststore"); _selector.set_row_separator_func([=](const Glib::RefPtr<Gtk::TreeModel>&, const Gtk::TreeModel::iterator& it){ Glib::ustring id; it->get_value(COL_ID, id); return id == "-"; }); _categories = Gtk::TreeModelFilter::create(treestore); _categories->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){ Glib::ustring id; it->get_value(COL_ID, id); return id == "-" || is_resource_present(id, _stats); }); _selector.set_model(_categories); auto icon_renderer = manage(new Inkscape::UI::Widget::CellRendererItemIcon()); _selector.insert_column("", *icon_renderer, 0); auto column = _selector.get_column(0); column->add_attribute(*icon_renderer, icon_renderer->property_shape_type().get_name(), COL_ICON); auto count_renderer = Gtk::make_managed<Gtk::CellRendererText>(); auto count_column = _selector.get_column(_selector.append_column("", *count_renderer) - 1); count_column->add_attribute(*count_renderer, "text", COL_COUNT); count_column->set_cell_data_func(*count_renderer, [=](Gtk::CellRenderer* r, const Gtk::TreeModel::iterator& it){ uint64_t count; it->get_value(COL_COUNT, count); count_renderer->property_text().set_value(count > 0 ? std::to_string(count) : ""); }); count_renderer->set_padding(3, 4); _wr.setUpdating(true); // set permanently for (auto entity = rdf_work_entities; entity && entity->name; ++entity) { if (entity->editable != RDF_EDIT_GENERIC) continue; auto w = Inkscape::UI::Widget::EntityEntry::create(entity, _wr); _rdf_list.push_back(w); } _page_selection = _selector.get_selection(); _selection_change = _page_selection->signal_changed().connect([=](){ if (auto it = _page_selection->get_selected()) { Glib::ustring id; it->get_value(COL_ID, id); select_page(id); } }); auto paned = &get_widget<Gtk::Paned>(_builder, "paned"); auto move = [=](){ auto pos = paned->get_position(); get_widget<Gtk::Label>(_builder, "spacer").set_size_request(pos); }; paned->property_position().signal_changed().connect([=](){ move(); }); move(); _edit.signal_clicked().connect([=](){ auto sel = _iconview.get_selected_items(); if (sel.size() == 1) { // todo: investigate why this doesn't work initially: _iconview.set_cursor(sel.front(), true); } else { // treeview todo if needed } }); // selectable elements can be selected on the canvas; // even elements in <defs> can be selected (same as in XML dialog) _select.signal_clicked().connect([=](){ auto document = getDocument(); auto desktop = getDesktop(); if (!document || !desktop) return; if (auto row = selected_item()) { Glib::ustring id = row[g_item_columns.id]; if (auto object = document->getObjectById(id)) { // select object desktop->getSelection()->set(object); } } else { // to do: select from treeview if needed } }); _search.signal_search_changed().connect([=](){ filtered_items->freeze_notify(); filtered_items->refilter(); filtered_items->thaw_notify(); filtered_info->freeze_notify(); filtered_info->refilter(); filtered_info->thaw_notify(); }); // filter gridview filtered_items->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){ if (_search.get_text_length() == 0) return true; auto str = _search.get_text().lowercase(); Glib::ustring label = (*it)[g_item_columns.label]; return label.lowercase().find(str) != Glib::ustring::npos; }); // filter treeview too filtered_info->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){ if (_search.get_text_length() == 0) return true; auto str = _search.get_text().lowercase(); Glib::ustring value = (*it)[g_info_columns.value]; return value.lowercase().find(str) != Glib::ustring::npos; }); _delete.signal_clicked().connect([=](){ // delete selected object if (auto row = selected_item()) { SPObject* object = row[g_item_columns.object]; delete_object(object, getDesktop()->getSelection()); } }); _extract.signal_clicked().connect([=](){ auto window = dynamic_cast<Gtk::Window*>(get_toplevel()); switch (_showing_resource) { case Images: // extract selected image if (auto row = selected_item()) { SPObject* object = row[g_item_columns.object]; extract_image(window, cast<SPImage>(object)); } break; case Colors: // export colors into a GIMP palette if (_document) { std::vector<int> colors; _item_store->foreach_iter([&](const Gtk::TreeModel::iterator& it){ int c; it->get_value(g_item_columns.color.index(), c); colors.push_back(c); return false; // false means continue }); extract_colors(window, colors, _document->getDocumentName()); } break; default: // nothing else so far break; } }); _iconview.signal_selection_changed().connect([=](){ update_buttons(); }); } Gtk::TreeModel::Row DocumentResources::selected_item() { auto sel = _iconview.get_selected_items(); auto model = _iconview.get_model(); Gtk::TreeModel::Row row; if (sel.size() == 1 && model) { row = *model->get_iter(sel.front()); } return row; } void DocumentResources::update_buttons() { if (!_iconview.get_visible()) return; auto single_sel = !!selected_item(); _edit.set_sensitive(single_sel); _extract.set_sensitive(single_sel || _showing_resource == Colors); _delete.set_sensitive(single_sel); _select.set_sensitive(single_sel); } Cairo::RefPtr<Cairo::Surface> render_color(uint32_t rgb, double size, double radius, int device_scale) { Cairo::RefPtr<Cairo::Surface> nul; return add_background_to_image(nul, rgb, size / 2, radius, device_scale, 0x7f7f7f00); } void collect_object_colors(SPObject& obj, std::map<std::string, SPColor>& colors) { auto style = obj.style; if (style->stroke.set && style->stroke.colorSet) { const auto& c = style->stroke.value.color; colors[c.toString()] = c; } if (style->color.set) { const auto& c = style->color.value.color; colors[c.toString()] = c; } if (style->fill.set) { const auto& c = style->fill.value.color; colors[c.toString()] = c; } if (style->solid_color.set) { const auto& c = style->solid_color.value.color; colors[c.toString()] = c; } } // traverse all nodes starting from given 'object' template<typename V> void apply_visitor(SPObject& object, V&& visitor) { visitor(object); // SPUse inserts referenced object as a child; skip it if (is<SPUse>(&object)) return; for (auto&& child : object.children) { apply_visitor(child, visitor); } } std::map<std::string, SPColor> collect_colors(SPObject* object) { std::map<std::string, SPColor> colors; if (object) { apply_visitor(*object, [&](SPObject& obj){ collect_object_colors(obj, colors); }); } return colors; } void collect_used_fonts(SPObject& object, std::set<std::string>& fonts) { auto style = object.style; if (style->font_specification.set) { auto fspec = style->font_specification.value(); if (fspec && *fspec) { fonts.insert(fspec); } } else if (style->font.set) { // some SVG files won't have Inkscape-specific fontspec; read font settings instead auto font = style->font.get_value(); if (style->font_style.set) { font += ' ' + style->font_style.get_value(); } fonts.insert(font); } } std::set<std::string> collect_fontspecs(SPObject* object) { std::set<std::string> fonts; if (object) { apply_visitor(*object, [&](SPObject& obj){ collect_used_fonts(obj, fonts); }); } return fonts; } template<typename T> bool filter_element(T& object) { return true; } template<> bool filter_element<SPPattern>(SPPattern& object) { return object.hasChildren(); } template<> bool filter_element<SPGradient>(SPGradient& object) { return object.hasStops(); } template<typename T> std::vector<T*> collect_items(SPObject* object, bool (*filter)(T&) = filter_element<T>) { std::vector<T*> items; if (object) { apply_visitor(*object, [&](SPObject& obj){ if (auto t = cast<T>(&obj)) { if (filter(*t)) items.push_back(t); } }); } return items; } std::unordered_map<std::string, size_t> collect_styles(SPObject* root) { std::unordered_map<std::string, size_t> map; if (!root) return map; apply_visitor(*root, [&](SPObject& obj){ if (auto style = obj.getAttribute("style")) { map[style]++; } }); return map; } bool has_external_ref(SPObject& obj) { bool present = false; if (auto href = Inkscape::getHrefAttribute(*obj.getRepr()).second) { if (*href && *href != '#' && *href != '?') { auto scheme = Glib::uri_parse_scheme(href); // There are tens of schemes: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml // TODO: Which ones to collect as external resources? if (scheme == "file" || scheme == "http" || scheme == "https" || scheme.empty()) { present = true; } } } return present; } details::Statistics collect_statistics(SPObject* root) { details::Statistics stats; if (!root) { return stats; } std::map<std::string, SPColor> colors; std::set<std::string> fonts; apply_visitor(*root, [&](SPObject& obj){ // order of tests is important; derived classes first, before base, // so meshgradient first, gradient next if (auto pattern = cast<SPPattern>(&obj)) { if (filter_element(*pattern)) { stats.patterns++; } } else if (is<SPMeshGradient>(&obj)) { stats.meshgradients++; } else if (auto gradient = cast<SPGradient>(&obj)) { if (filter_element(*gradient)) { if (gradient->isSwatch()) { stats.swatches++; } else { stats.gradients++; } } } else if (auto marker = cast<SPMarker>(&obj)) { if (filter_element(*marker)) { stats.markers++; } } else if (auto symbol = cast<SPSymbol>(&obj)) { if (filter_element(*symbol)) { stats.symbols++; } } else if (is<SPFont>(&obj)) { // SVG font stats.svg_fonts++; } else if (is<SPImage>(&obj)) { stats.images++; } else if (auto group = cast<SPGroup>(&obj)) { if (strcmp(group->getRepr()->name(), "svg:g") == 0) { switch (group->layerMode()) { case SPGroup::GROUP: stats.groups++; break; case SPGroup::LAYER: stats.layers++; break; } } } else if (is<SPPath>(&obj)) { stats.paths++; } else if (is<SPFilter>(&obj)) { stats.filters++; } else if (is<ColorProfile>(&obj)) { stats.colorprofiles++; } if (auto style = obj.getAttribute("style")) { if (*style) stats.styles++; } if (has_external_ref(obj)) { stats.external_uris++; } collect_object_colors(obj, colors); collect_used_fonts(obj, fonts); // verify: stats.nodes++; }); stats.colors = colors.size(); stats.fonts = fonts.size(); return stats; } details::Statistics DocumentResources::collect_statistics() { auto root = _document ? _document->getRoot() : nullptr; auto stats = ::Inkscape::UI::Dialog::collect_statistics(root); if (_document) { for (auto& el : _rdf_list) { bool read_only = true; el.update(_document, read_only); if (!el.content().empty()) stats.metadata++; } } return stats; } void DocumentResources::rebuild_stats() { _stats = collect_statistics(); if (auto desktop = getDesktop()) { _wr.setDesktop(desktop); } _categories->refilter(); _categories->foreach_iter([=](const Gtk::TreeModel::iterator& it){ Glib::ustring id; it->get_value(COL_ID, id); auto count = get_resource_count(id, _stats); if (id == "stats") count = 0; // don't show count 1 for "overview" it->set_value(COL_COUNT, count); return false; // false means continue }); _selector.columns_autosize(); } void DocumentResources::documentReplaced() { _document = getDocument(); if (_document) { _document_modified = _document->connectModified([=](unsigned){ // brute force refresh, but throttled _idle_refresh = Glib::signal_timeout().connect([=](){ rebuild_stats(); refresh_current_page(); return false; }, 200); }); } else { _document_modified.disconnect(); } rebuild_stats(); refresh_current_page(); } void DocumentResources::refresh_current_page() { auto page = _cur_page_id; if (!is_resource_present(page, _stats)) { page = "stats"; } auto model = _selector.get_model(); model->foreach([=](const Gtk::TreeModel::Path& path, const Gtk::TreeModel::iterator& it) { Glib::ustring id; it->get_value(COL_ID, id); if (id == page) { _page_selection->select(path); refresh_page(id); return true; } return false; }); } void DocumentResources::selectionModified(Inkscape::Selection* selection, guint flags) { // no op so far } auto get_id = [](const SPObject* object) { auto id = object->getId(); return id ? id : ""; }; auto label_fmt = [](const char* label, const Glib::ustring& id) { return label && *label ? label : '#' + id; }; void add_colors(Glib::RefPtr<Gtk::ListStore> item_store, const std::map<std::string, SPColor>& colors, int device_scale) { for (auto&& it : colors) { const auto& color = it.second; auto row = *item_store->append(); auto name = color.toString(); auto rgba32 = color.toRGBA32(0xff); auto rgb24 = rgba32 >> 8; row[g_item_columns.id] = name; row[g_item_columns.label] = name; row[g_item_columns.color] = rgb24; int size = 20; double radius = 2.0; row[g_item_columns.image] = render_color(rgba32, size, radius, device_scale); row[g_item_columns.object] = nullptr; } } void _add_items_with_images(Glib::RefPtr<Gtk::ListStore> item_store, const std::vector<SPObject*>& items, double width, double height, int device_scale, bool use_title, object_renderer::options opt) { object_renderer renderer; item_store->freeze_notify(); for (auto item : items) { auto row = *item_store->append(); auto id = get_id(item); row[g_item_columns.id] = id; if (use_title) { auto title = item->title(); row[g_item_columns.label] = label_fmt(title, id); g_free(title); } else { auto label = item->getAttribute("inkscape:label"); row[g_item_columns.label] = label_fmt(label, id); } row[g_item_columns.image] = renderer.render(*item, width, height, device_scale, opt); row[g_item_columns.object] = item; } item_store->thaw_notify(); } template<typename T> void add_items_with_images(Glib::RefPtr<Gtk::ListStore> item_store, const std::vector<T*>& items, double width, double height, int device_scale, bool use_title = false, object_renderer::options opt = {}) { static_assert(std::is_base_of<SPObject, T>::value); _add_items_with_images(item_store, reinterpret_cast<const std::vector<SPObject*>&>(items), width, height, device_scale, use_title, opt); } void add_fonts(Glib::RefPtr<Gtk::ListStore> store, const std::set<std::string>& fontspecs) { size_t i = 1; for (auto&& fs : fontspecs) { auto row = *store->append(); row[g_info_columns.item] = Glib::ustring::compose("%1 %2", _("Font"), i++); auto name = Glib::Markup::escape_text(fs); row[g_info_columns.value] = Glib::ustring::format( "<span allow_breaks='false' size='xx-large' font='", fs, "'>", name, "</span>\n", "<span allow_breaks='false' size='small' alpha='60%'>", name, "</span>" ); } } void add_stats(Glib::RefPtr<Gtk::ListStore> info_store, SPDocument* document, const details::Statistics& stats) { auto read_only = true; auto license = document ? rdf_get_license(document, read_only) : nullptr; std::pair<const char*, std::string> str[] = { {_("Document"), document && document->getDocumentFilename() ? document->getDocumentFilename() : "-"}, {_("License"), license && license->name ? license->name : "-"}, {_("Metadata"), stats.metadata > 0 ? C_("Adjective for Metadata status", "Present") : "-"}, }; for (auto& pair : str) { auto row = *info_store->append(); row[g_info_columns.item] = pair.first; row[g_info_columns.value] = Glib::Markup::escape_text(pair.second); } std::pair<const char*, size_t> kv[] = { {_("Colors"), stats.colors}, {_("Color profiles"), stats.colorprofiles}, {_("Swatches"), stats.swatches}, {_("Fonts"), stats.fonts}, {_("Gradients"), stats.gradients}, {_("Mesh gradients"), stats.meshgradients}, {_("Patterns"), stats.patterns}, {_("Symbols"), stats.symbols}, {_("Markers"), stats.markers}, {_("Filters"), stats.filters}, {_("Images"), stats.images}, {_("SVG fonts"), stats.svg_fonts}, {_("Layers"), stats.layers}, {_("Total elements"), stats.nodes}, {_("Groups"), stats.groups}, {_("Paths"), stats.paths}, {_("External URIs"), stats.external_uris}, }; for (auto& pair : kv) { auto row = *info_store->append(); row[g_info_columns.item] = pair.first; row[g_info_columns.value] = pair.second ? std::to_string(pair.second) : "-"; } } void add_metadata(Glib::RefPtr<Gtk::ListStore> info_store, SPDocument* document, const boost::ptr_vector<Inkscape::UI::Widget::EntityEntry>& rdf_list) { for (auto& entry : rdf_list) { auto row = *info_store->append(); auto label = entry._label.get_label(); Util::trim(label, ":"); row[g_info_columns.item] = label; row[g_info_columns.value] = Glib::Markup::escape_text(entry.content()); } } void add_filters(Glib::RefPtr<Gtk::ListStore> info_store, const std::vector<SPFilter*>& filters) { for (auto& filter : filters) { auto row = *info_store->append(); auto label = filter->getAttribute("inkscape:label"); auto name = Glib::ustring(label ? label : filter->getId()); row[g_info_columns.item] = name; std::ostringstream ost; bool first = true; for (auto& obj : filter->children) { if (auto primitive = cast<SPFilterPrimitive>(&obj)) { if (!first) ost << ", "; Glib::ustring name = primitive->getRepr()->name(); if (name.find("svg:") != std::string::npos) { name.erase(name.find("svg:"), 4); } ost << name; first = false; } } row[g_info_columns.value] = Glib::Markup::escape_text(ost.str()); } } void add_styles(Glib::RefPtr<Gtk::ListStore> info_store, const std::unordered_map<std::string, size_t>& map) { std::vector<std::string> vect; vect.reserve(map.size()); for (auto style : map) { vect.emplace_back(style.first); } std::sort(vect.begin(), vect.end()); info_store->freeze_notify(); int n = 1; for (auto& style : vect) { auto row = *info_store->append(); row[g_info_columns.item] = _("Style ") + std::to_string(n++); row[g_info_columns.count] = map.find(style)->second; row[g_info_columns.value] = Glib::Markup::escape_text(style); } info_store->thaw_notify(); } void add_refs(Glib::RefPtr<Gtk::ListStore> info_store, const std::vector<SPObject*>& objects) { info_store->freeze_notify(); for (auto& obj : objects) { auto href = Inkscape::getHrefAttribute(*obj->getRepr()).second; if (!href) continue; auto row = *info_store->append(); row[g_info_columns.item] = label_fmt(nullptr, get_id(obj)); row[g_info_columns.value] = href; row[g_info_columns.object] = obj; } info_store->thaw_notify(); } void DocumentResources::select_page(const Glib::ustring& id) { if (_cur_page_id == id) return; _cur_page_id = id; refresh_page(id); } void DocumentResources::clear_stores() { _item_store->freeze_notify(); _item_store->clear(); _item_store->thaw_notify(); _info_store->freeze_notify(); _info_store->clear(); _info_store->thaw_notify(); } void DocumentResources::refresh_page(const Glib::ustring& id) { auto rsrc = id_to_resource(id); // GTK spits out a lot of warnings and errors from filtered model. // I don't know how to fix them. // https://gitlab.gnome.org/GNOME/gtk/-/issues/1150 // Clear sorting? Remove filtering? // GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID clear_stores(); auto root = _document ? _document->getRoot() : nullptr; auto defs = _document ? _document->getDefs() : nullptr; int device_scale = get_scale_factor(); auto tab = "iconview"; auto has_count = false; auto item_width = 90; auto context = get_style_context(); Gdk::RGBA color = context->get_color(get_state_flags()); auto label_editable = false; auto items_selectable = true; auto can_delete = false; // enable where supported auto can_extract = false; switch (rsrc) { case Colors: add_colors(_item_store, collect_colors(root), device_scale); item_width = 70; items_selectable = false; // to do: make selectable? can_extract = true; break; case Symbols: { auto opt = object_renderer::options(); if (INKSCAPE.themecontext->isCurrentThemeDark(dynamic_cast<Gtk::Container*>(this))) { // white background for typically black symbols, so they don't disappear in a dark theme opt.solid_background(0xf0f0f0ff, 3, 3); } opt.symbol_style_from_use(); add_items_with_images(_item_store, collect_items<SPSymbol>(defs), 70, 60, device_scale, true, opt); } label_editable = true; can_delete = true; break; case Patterns: add_items_with_images(_item_store, collect_items<SPPattern>(defs), 80, 70, device_scale); label_editable = true; can_delete = true; break; case Markers: add_items_with_images(_item_store, collect_items<SPMarker>(defs), 70, 60, device_scale, false, object_renderer::options().foreground(color)); label_editable = true; can_delete = true; break; case Gradients: add_items_with_images(_item_store, collect_items<SPGradient>(defs, [](auto& g){ return filter_element(g) && !g.isSwatch(); }), 180, 22, device_scale); label_editable = true; can_delete = true; break; case Swatches: add_items_with_images(_item_store, collect_items<SPGradient>(defs, [](auto& g){ return filter_element(g) && g.isSwatch(); }), 100, 22, device_scale); label_editable = true; can_delete = true; break; case Fonts: add_fonts(_info_store, collect_fontspecs(root)); tab = "treeview"; items_selectable = false; break; case Filters: add_filters(_info_store, collect_items<SPFilter>(defs)); label_editable = true; tab = "treeview"; items_selectable = false; // to do: make selectable break; case Styles: add_styles(_info_store, collect_styles(root)); tab = "treeview"; has_count = true; items_selectable = false; // to do: make selectable? break; case Images: add_items_with_images(_item_store, collect_items<SPImage>(root), 110, 110, device_scale); label_editable = true; can_extract = true; can_delete = true; break; case External: add_refs(_info_store, collect_items<SPObject>(root, [](auto& obj){ return has_external_ref(obj); })); tab = "treeview"; items_selectable = false; // to do: make selectable break; case Stats: add_stats(_info_store, _document, _stats); tab = "treeview"; items_selectable = false; break; case Metadata: add_metadata(_info_store, _document, _rdf_list); tab = "treeview"; items_selectable = false; break; } _showing_resource = rsrc; _treeview.get_column(1)->set_visible(has_count); _label_renderer->property_editable() = label_editable; widget_show(_edit, label_editable); widget_show(_select, items_selectable); widget_show(_delete, can_delete); widget_show(_extract, can_extract); _iconview.set_item_width(item_width); get_widget<Gtk::Stack>(_builder, "stack").set_visible_child(tab); update_buttons(); } void DocumentResources::start_editing(Gtk::CellEditable* cell, const Glib::ustring& path) { auto entry = dynamic_cast<Gtk::Entry*>(cell); entry->set_has_frame(); } void DocumentResources::end_editing(const Glib::ustring& path, const Glib::ustring& new_text) { auto model = _iconview.get_model(); Gtk::TreeModel::Row row = *model->get_iter(path); if (!row) return; SPObject* object = row[g_item_columns.object]; if (!object) { g_warning("Missing object ptr, cannot edit object's name."); return; } // try object-specific edit functions first; if not present fall back to generic auto getter = g_get_label[typeid(*object)]; auto setter = g_set_label[typeid(*object)]; if (!getter || !setter) { getter = g_get_label[typeid(SPObject)]; setter = g_set_label[typeid(SPObject)]; } auto name = getter(*object); if (new_text == name) return; setter(*object, new_text); auto id = get_id(object); row[g_item_columns.label] = label_fmt(new_text.c_str(), id); if (auto document = object->document) { DocumentUndo::done(document, _("Edit object title"), INKSCAPE_ICON("document-resources")); } } } } } // namespaces