// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Symbols dialog. */ /* Authors: * Copyright (C) 2012 Tavmjong Bah * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include <2geom/point.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 "preferences.h" #include "ui/builder-utils.h" #include "ui/dialog/messages.h" #ifdef HAVE_CONFIG_H # include "config.h" // only include where actually required! #endif #include "symbols.h" #include #include #include #include #include #include #include #include #include #include #include "document.h" #include "inkscape.h" #include "path-prefix.h" #include "selection.h" #include "display/cairo-utils.h" #include "include/gtkmm_version.h" #include "io/resource.h" #include "io/sys.h" #include "object/sp-defs.h" #include "object/sp-root.h" #include "object/sp-symbol.h" #include "object/sp-use.h" #include "ui/cache/svg_preview_cache.h" #include "ui/clipboard.h" #include "ui/icon-loader.h" #include "ui/icon-names.h" #include "ui/widget/scrollprotected.h" #include "xml/href-attribute-helper.h" #ifdef WITH_LIBVISIO #include #include using librevenge::RVNGFileStream; using librevenge::RVNGString; using librevenge::RVNGStringVector; using librevenge::RVNGPropertyList; using librevenge::RVNGSVGDrawingGenerator; #endif namespace Inkscape { namespace UI { namespace Dialog { constexpr int SIZES = 51; int SYMBOL_ICON_SIZES[SIZES]; struct SymbolSet { std::vector symbols; SPDocument* document = nullptr; Glib::ustring title; }; SPDocument* load_symbol_set(std::string filename); void scan_all_symbol_sets(std::map& symbol_sets); // key: symbol set full file name // value: symbol set static std::map symbol_sets; struct SymbolColumns : public Gtk::TreeModel::ColumnRecord { Gtk::TreeModelColumn cache_key; Gtk::TreeModelColumn symbol_id; Gtk::TreeModelColumn symbol_title; Gtk::TreeModelColumn symbol_short_title; Gtk::TreeModelColumn symbol_search_title; Gtk::TreeModelColumn> symbol_image; Gtk::TreeModelColumn doc_dimensions; Gtk::TreeModelColumn symbol_document; SymbolColumns() { add(cache_key); add(symbol_id); add(symbol_title); add(symbol_short_title); add(symbol_search_title); add(symbol_image); add(doc_dimensions); add(symbol_document); } } const g_columns; static Cairo::RefPtr g_dummy; struct SymbolSetsColumns : public Gtk::TreeModel::ColumnRecord { Gtk::TreeModelColumn set_id; Gtk::TreeModelColumn translated_title; Gtk::TreeModelColumn set_filename; Gtk::TreeModelColumn set_document; Gtk::TreeModelColumn> set_image; SymbolSetsColumns() { add(set_id); add(translated_title); add(set_filename); add(set_document); add(set_image); } } const g_set_columns; const Glib::ustring CURRENT_DOC_ID = "{?cur-doc?}"; const Glib::ustring ALL_SETS_ID = "{?all-sets?}"; const char *CURRENT_DOC = N_("Current document"); const char *ALL_SETS = N_("All symbol sets"); SymbolsDialog::SymbolsDialog(const char* prefsPath) : DialogBase(prefsPath, "Symbols"), _builder(create_builder("dialog-symbols.glade")), _zoom(get_widget(_builder, "zoom")), _symbols_popup(get_widget(_builder, "symbol-set-popup")), _set_search(get_widget(_builder, "set-search")), _search(get_widget(_builder, "search")), _symbol_sets_view(get_widget(_builder, "symbol-sets")), _cur_set_name(get_widget(_builder, "cur-set")), _store(Gtk::ListStore::create(g_columns)), _image_cache(1000) // arbitrary limit for how many rendered symbols to keep around { auto prefs = Inkscape::Preferences::get(); Glib::ustring path = prefsPath; path += '/'; _symbols._filtered = Gtk::TreeModelFilter::create(_store); _symbols._store = _store; _symbol_sets = Gtk::ListStore::create(g_set_columns); _sets._store = _symbol_sets; _sets._filtered = Gtk::TreeModelFilter::create(_symbol_sets); _sets._filtered->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){ if (_set_search.get_text_length() == 0) return true; Glib::ustring id = (*it)[g_set_columns.set_id]; if (id == CURRENT_DOC_ID || id == ALL_SETS_ID) return true; auto text = _set_search.get_text().lowercase(); Glib::ustring title = (*it)[g_set_columns.translated_title]; return title.lowercase().find(text) != Glib::ustring::npos; }); _sets._sorted = Gtk::TreeModelSort::create(_sets._filtered); _sets._sorted->set_sort_func(g_set_columns.translated_title, [=](const Gtk::TreeModel::iterator& a, const Gtk::TreeModel::iterator& b){ Glib::ustring ida = (*a)[g_set_columns.set_id]; Glib::ustring idb = (*b)[g_set_columns.set_id]; // current doc and all docs up front if (ida == idb) return 0; if (ida == CURRENT_DOC_ID) return -1; if (idb == CURRENT_DOC_ID) return 1; if (ida == ALL_SETS_ID) return -1; if (idb == ALL_SETS_ID) return 1; Glib::ustring ttl_a = (*a)[g_set_columns.translated_title]; Glib::ustring ttl_b = (*b)[g_set_columns.translated_title]; return ttl_a.compare(ttl_b); }); _symbol_sets_view.set_model(_sets._sorted); _symbol_sets_view.set_text_column(g_set_columns.translated_title.index()); _symbol_sets_view.pack_start(_renderer2); _symbol_sets_view.add_attribute(_renderer2, "surface", g_set_columns.set_image); auto row = _symbol_sets->append(); (*row)[g_set_columns.set_id] = CURRENT_DOC_ID; (*row)[g_set_columns.translated_title] = _(CURRENT_DOC); row = _symbol_sets->append(); (*row)[g_set_columns.set_id] = ALL_SETS_ID; (*row)[g_set_columns.translated_title] = _(ALL_SETS); _set_search.signal_search_changed().connect([=](){ auto scoped(_update.block()); _sets.refilter(); }); auto select_set = [=](const Gtk::TreeModel::Path& set_path) { if (!set_path.empty()) { // drive selection _symbol_sets_view.select_path(set_path); } else if (auto set = get_current_set()) { // populate icon view rebuild(*set); _cur_set_name.set_text((**set)[g_set_columns.translated_title]); update_tool_buttons(); Glib::ustring id = (**set)[g_set_columns.set_id]; prefs->setString(path + "current-set", id); return true; } return false; }; // _symbol_sets_view.signal_item_activated().connect([=](const Gtk::TreeModel::Path& path){ // select_set(path); // get_widget(_builder, "set-popover").hide(); // }); _symbol_sets_view.signal_selection_changed().connect([=](){ if (select_set({})) { get_widget(_builder, "set-popover").hide(); } }); const double factor = std::pow(2.0, 1.0 / 12.0); for (int i = 0; i < SIZES; ++i) { SYMBOL_ICON_SIZES[i] = std::round(std::pow(factor, i) * 16); } preview_document = symbolsPreviewDoc(); /* Template to render symbols in */ key = SPItem::display_key_new(1); renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY)); auto& main = get_widget(_builder, "main-box"); pack_start(main, Gtk::PACK_EXPAND_WIDGET); _builder->get_widget("tools", tools); icon_view = &get_widget(_builder, "icon-view"); _symbols._filtered->set_visible_func([=](const Gtk::TreeModel::const_iterator& it){ if (_search.get_text_length() == 0) return true; auto text = _search.get_text().lowercase(); Glib::ustring title = (*it)[g_columns.symbol_search_title]; return title.lowercase().find(text) != Glib::ustring::npos; }); icon_view->set_model(_symbols._filtered); icon_view->set_tooltip_column(g_columns.symbol_title.index()); _search.signal_search_changed().connect([=](){ int delay = _search.get_text_length() == 0 ? 0 : 300; _idle_search = Glib::signal_timeout().connect([=](){ auto scoped(_update.block()); _symbols.refilter(); set_info(); return false; // disconnect }, delay); }); auto show_names = &get_widget(_builder, "show-names"); auto names = prefs->getBool(path + "show-names", true); show_names->set_active(names); if (names) { icon_view->set_markup_column(g_columns.symbol_short_title); } show_names->signal_toggled().connect([=](){ bool show = show_names->get_active(); icon_view->set_markup_column(show ? g_columns.symbol_short_title.index() : -1); prefs->setBool(path + "show-names", show); }); std::vector targets; targets.emplace_back("application/x-inkscape-paste"); icon_view->enable_model_drag_source(targets, Gdk::BUTTON1_MASK, Gdk::ACTION_COPY); gtk_connections.emplace_back( icon_view->signal_drag_data_get().connect(sigc::mem_fun(*this, &SymbolsDialog::iconDragDataGet))); gtk_connections.emplace_back( icon_view->signal_selection_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::iconChanged))); gtk_connections.emplace_back(icon_view->signal_button_press_event().connect([=](GdkEventButton *ev) -> bool { _last_mousedown = {ev->x, ev->y - icon_view->get_vadjustment()->get_value()}; return false; }, false)); _builder->get_widget("scroller", scroller); // here we fix scoller to allow pass the scroll to parent scroll when reach upper or lower limit // this must be added to al scrolleing window in dialogs. We dont do auto because dialogs can be recreated // in the dialog code so think is safer call inside fix_inner_scroll(scroller); _builder->get_widget("overlay", overlay); /*************************Overlays******************************/ // No results overlay_icon = sp_get_icon_image("searching", Gtk::ICON_SIZE_DIALOG); overlay_icon->set_pixel_size(40); overlay_icon->set_halign(Gtk::ALIGN_CENTER); overlay_icon->set_valign(Gtk::ALIGN_START); overlay_icon->set_margin_top(90); overlay_icon->set_no_show_all(true); overlay_title = new Gtk::Label(); overlay_title->set_halign(Gtk::ALIGN_CENTER ); overlay_title->set_valign(Gtk::ALIGN_START ); overlay_title->set_justify(Gtk::JUSTIFY_CENTER); overlay_title->set_margin_top(135); overlay_title->set_no_show_all(true); overlay_desc = new Gtk::Label(); overlay_desc->set_halign(Gtk::ALIGN_CENTER); overlay_desc->set_valign(Gtk::ALIGN_START); overlay_desc->set_margin_top(160); overlay_desc->set_justify(Gtk::JUSTIFY_CENTER); overlay_desc->set_no_show_all(true); overlay->add_overlay(*overlay_icon); overlay->add_overlay(*overlay_title); overlay->add_overlay(*overlay_desc); previous_height = 0; previous_width = 0; /******************** Tools *******************************/ _builder->get_widget("add-symbol", add_symbol); add_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::insertSymbol)); _builder->get_widget("remove-symbol", remove_symbol); remove_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::revertSymbol)); // Pack size (controls display area) pack_size = prefs->getIntLimited(path + "tile-size", 12, 0, SIZES); auto scale = &get_widget(_builder, "symbol-size"); scale->set_value(pack_size); scale->signal_value_changed().connect([=](){ pack_size = scale->get_value(); assert(pack_size >= 0 && pack_size < SIZES); _image_cache.clear(); rebuild(); prefs->setInt(path + "tile-size", pack_size); }); scale_factor = prefs->getIntLimited(path + "scale-factor", 0, -10, +10); _zoom.set_value(scale_factor); _zoom.signal_value_changed().connect([=](){ scale_factor = _zoom.get_value(); rebuild(); prefs->setInt(path + "scale-factor", scale_factor); }); icon_view->set_columns(-1); icon_view->pack_start(_renderer); icon_view->add_attribute(_renderer, "surface", g_columns.symbol_image); icon_view->set_cell_data_func(_renderer, [=](const Gtk::TreeModel::const_iterator& it){ Gdk::Rectangle rect; Gtk::TreeModel::Path path(it); if (icon_view->get_cell_rect(path, rect)) { auto height = icon_view->get_allocated_height(); bool visible = !(rect.get_x() < 0 && rect.get_y() < 0); // cell rect coordinates are not affected by scrolling if (visible && (rect.get_y() + rect.get_height() < 0 || rect.get_y() > 0 + height)) { visible = false; } get_cell_data_func(&_renderer, *it, visible); } }); // Toggle scale to fit on/off _builder->get_widget("zoom-to-fit", fit_symbol); auto fit = prefs->getBool(path + "zoom-to-fit", true); fit_symbol->set_active(fit); fit_symbol->signal_clicked().connect([=](){ rebuild(); prefs->setBool(path + "zoom-to-fit", fit_symbol->get_active()); }); scan_all_symbol_sets(symbol_sets); for (auto&& it : symbol_sets) { auto row = _symbol_sets->append(); auto& set = it.second; (*row)[g_set_columns.set_id] = it.first; (*row)[g_set_columns.translated_title] = g_dpgettext2(nullptr, "Symbol", set.title.c_str()); (*row)[g_set_columns.set_document] = set.document; (*row)[g_set_columns.set_filename] = it.first; } // last selected set auto current = prefs->getString(path + "current-set", CURRENT_DOC_ID); // by default select current doc (first on the list) in case nothing else gets selected select_set(Gtk::TreeModel::Path("0")); sensitive = true; // restore set selection; check if it is still available first _sets._sorted->foreach_path([&](const Gtk::TreeModel::Path& path){ auto it = _sets.path_to_child_iter(path); if (current == (*it)[g_set_columns.set_id]) { select_set(path); return true; } return false; }); } void SymbolsDialog::on_unrealize() { for (auto &connection : gtk_connections) { connection.disconnect(); } gtk_connections.clear(); DialogBase::on_unrealize(); } SymbolsDialog::~SymbolsDialog() { Inkscape::GC::release(preview_document); assert(preview_document->_anchored_refcount() == 0); delete preview_document; } void collect_symbols(SPObject* object, std::vector& symbols) { if (!object) return; if (auto symbol = cast(object)) { symbols.push_back(symbol); } if (is(object)) return; for (auto& child : object->children) { collect_symbols(&child, symbols); } } void SymbolsDialog::load_all_symbols() { _sets._store->foreach_iter([=](const Gtk::TreeModel::iterator& it){ if (!(*it)[g_set_columns.set_document]) { std::string path = (*it)[g_set_columns.set_filename]; if (!path.empty()) { auto doc = load_symbol_set(path); (*it)[g_set_columns.set_document] = doc; } } return false; }); } std::map get_all_symbols(Glib::RefPtr& store) { std::map map; store->foreach_iter([&](const Gtk::TreeModel::iterator& it){ if (SPDocument* doc = (*it)[g_set_columns.set_document]) { SymbolSet vect; collect_symbols(doc->getRoot(), vect.symbols); vect.title = (*it)[g_set_columns.translated_title]; vect.document = doc; Glib::ustring id = (*it)[g_set_columns.set_id]; map[id.raw()] = vect; } return false; }); return map; } void SymbolsDialog::rebuild(Gtk::TreeIter current) { if (!sensitive || !current) { return; } auto pending = _update.block(); // remove model first, or else IconView will update N times as N rows get deleted... icon_view->unset_model(); _symbols._store->clear(); auto it = current; std::map symbols; SPDocument* document = (*it)[g_set_columns.set_document]; Glib::ustring set_id = (*it)[g_set_columns.set_id]; if (!document) { if (set_id == CURRENT_DOC_ID) { document = getDocument(); } else if (set_id == ALL_SETS_ID) { // load symbol sets, if not yet open load_all_symbols(); // get symbols from all symbol sets (apart from current document) symbols = get_all_symbols(_sets._store); } else { std::string path = (*it)[g_set_columns.set_filename]; // load symbol set document = load_symbol_set(path); (*it)[g_set_columns.set_document] = document; } } if (document) { auto& vect = symbols[set_id.raw()]; collect_symbols(document->getRoot(), vect.symbols); vect.document = set_id == CURRENT_DOC_ID ? nullptr : document; vect.title = (*it)[g_set_columns.translated_title]; } size_t n = 0; for (auto&& it : symbols) { auto& set = it.second; for (auto symbol : set.symbols) { addSymbol(symbol, set.title, set.document); } n += set.symbols.size(); } for (auto r : icon_view->get_cells()) { if (auto t = dynamic_cast(r)) { // sizable boost in layout speed at the cost of showing only part of the title... if (n > 1000) { t->set_fixed_height_from_font(1); t->property_ellipsize() = Pango::EllipsizeMode::ELLIPSIZE_END; } else { t->set_fixed_height_from_font(-1); t->property_ellipsize() = Pango::EllipsizeMode::ELLIPSIZE_NONE; // t->property_wrap_mode() = Pango::WrapMode::WRAP_CHAR; } } } // reattach the model, have IconView content rebuilt icon_view->set_model(_symbols._filtered); // layout speed test: // Gtk::Allocation alloc; // alloc.set_width(200); // alloc.set_height(500); // alloc.set_x(0); // alloc.set_y(0); // auto old_time = std::chrono::high_resolution_clock::now(); // icon_view->size_allocate(alloc); // auto current_time = std::chrono::high_resolution_clock::now(); // auto elapsed = std::chrono::duration_cast(current_time - old_time); // g_warning("size time: %d ms", static_cast(elapsed.count())); set_info(); } void SymbolsDialog::rebuild() { if (auto set = get_current_set()) { rebuild(*set); } } void SymbolsDialog::showOverlay() { auto search = _search.get_text_length() > 0; auto visible = visible_symbols(); auto current = get_current_set_id() == CURRENT_DOC_ID; auto small = [](const char* str){ return Glib::ustring::compose("%1", Glib::Markup::escape_text(str)); }; auto large = [](const char* str){ return Glib::ustring::compose("%1", Glib::Markup::escape_text(str)); }; if (!visible && search) { overlay_title->set_markup(large(_("No symbols found."))); overlay_desc->set_markup(small(_("Try a different search term,\nor switch to a different symbol set."))); } else if (!visible && current) { overlay_title->set_markup(large(_("No symbols found."))); overlay_desc->set_markup(small(_("No symbols in current document.\nChoose a different symbol set\nor add a new symbol."))); } /* if (current == ALLDOCS && !_l.size()) { overlay_icon->hide(); if (!all_docs_processed) { overlay_icon->show(); overlay_title->set_markup( Glib::ustring("") + _("Search in all symbol sets...") + ""); overlay_desc->set_markup( Glib::ustring("") + _("The first search can be slow.") + ""); } else if (!icons_found && !search_str.empty()) { overlay_title->set_markup( Glib::ustring("") + _("No symbols found.") + ""); overlay_desc->set_markup( Glib::ustring("") + _("Try a different search term.") + ""); } else { overlay_icon->show(); overlay_title->set_markup(Glib::ustring("") + Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("")); overlay_desc->set_markup(Glib::ustring("") + Glib::ustring("")); } } else if (!number_symbols && (current != CURRENTDOC || !search_str.empty())) { overlay_title->set_markup(Glib::ustring("") + Glib::ustring(_("No symbols found.")) + Glib::ustring("")); overlay_desc->set_markup(Glib::ustring("") + Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) + Glib::ustring("")); } else if (!number_symbols && current == CURRENTDOC) { overlay_title->set_markup(Glib::ustring("") + Glib::ustring(_("No symbols found.")) + Glib::ustring("")); overlay_desc->set_markup( Glib::ustring("") + Glib::ustring(_("No symbols in current document.\nChoose a different symbol set\nor add a new symbol.")) + Glib::ustring("")); } else if (!icons_found && !search_str.empty()) { overlay_title->set_markup(Glib::ustring("") + Glib::ustring(_("No symbols found.")) + Glib::ustring("")); overlay_desc->set_markup(Glib::ustring("") + Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) + Glib::ustring("")); } */ gint width = scroller->get_allocated_width(); gint height = scroller->get_allocated_height(); if (previous_height != height || previous_width != width) { previous_height = height; previous_width = width; } overlay_icon->show(); overlay_title->show(); overlay_desc->show(); } void SymbolsDialog::hideOverlay() { // overlay_opacity->hide(); overlay_icon->hide(); overlay_title->hide(); overlay_desc->hide(); } void SymbolsDialog::insertSymbol() { getDesktop()->getSelection()->toSymbol(); } void SymbolsDialog::revertSymbol() { if (auto document = getDocument()) { if (auto symbol = cast(document->getObjectById(getSymbolId(get_selected_symbol())))) { symbol->unSymbol(); } Inkscape::DocumentUndo::done(document, _("Group from symbol"), ""); } } void SymbolsDialog::iconDragDataGet(const Glib::RefPtr& /*context*/, Gtk::SelectionData& data, guint /*info*/, guint /*time*/) { auto selected = get_selected_symbol(); if (!selected) { return; } Glib::ustring symbol_id = (**selected)[g_columns.symbol_id]; GdkAtom dataAtom = gdk_atom_intern("application/x-inkscape-paste", false); gtk_selection_data_set(data.gobj(), dataAtom, 9, (guchar*)symbol_id.c_str(), symbol_id.length()); } void SymbolsDialog::selectionChanged(Inkscape::Selection *selection) { // what are we trying to do here? this code doesn't seem to accomplish anything in v1.2 /* auto selected = getSelected(); Glib::ustring symbol_id = getSymbolId(selected); Glib::ustring doc_title = get_active_base_text(getSymbolDocTitle(selected)); if (!doc_title.empty()) { SPDocument* symbol_document = symbol_sets[doc_title].second; if (!symbol_document) { //we are in global search so get the original symbol document by title symbol_document = selectedSymbols(); } if (symbol_document) { SPObject* symbol = symbol_document->getObjectById(symbol_id); if(symbol && !selection->includes(symbol)) { icon_view->unselect_all(); } } } */ } void SymbolsDialog::refresh_on_idle(int delay) { // if symbols from current document are presented... if (get_current_set_id() == CURRENT_DOC_ID) { // refresh them on idle; delay here helps to coalesce consecutive requests into one _idle_refresh = Glib::signal_timeout().connect([=](){ rebuild(*get_current_set()); return false; // disconnect }, delay, Glib::PRIORITY_DEFAULT_IDLE); } } void SymbolsDialog::documentReplaced() { _defs_modified.disconnect(); _doc_resource_changed.disconnect(); if (auto document = getDocument()) { _defs_modified = document->getDefs()->connectModified([=](SPObject* ob, guint flags) { refresh_on_idle(); }); _doc_resource_changed = document->connectResourcesChanged("symbol", [this](){ refresh_on_idle(); }); } // if symbol set is from current document, need to rebuild refresh_on_idle(0); update_tool_buttons(); } void SymbolsDialog::update_tool_buttons() { if (get_current_set_id() == CURRENT_DOC_ID) { add_symbol->set_sensitive(); remove_symbol->set_sensitive(); } else { add_symbol->set_sensitive(false); remove_symbol->set_sensitive(false); } } Glib::ustring SymbolsDialog::get_current_set_id() const { auto cur = get_current_set(); if (cur.has_value()) { return (*cur.value())[g_set_columns.set_id]; } return {}; } std::optional SymbolsDialog::get_current_set() const { auto selected = _symbol_sets_view.get_selected_items(); if (selected.empty()) { return std::nullopt; } return _sets.path_to_child_iter(selected.front()); } SPDocument* SymbolsDialog::get_symbol_document(const std::optional& it) const { if (!it) { return nullptr; } SPDocument* doc = (**it)[g_columns.symbol_document]; return doc; } /** Return the path to the selected symbol, or an empty optional if nothing is selected. */ std::optional SymbolsDialog::get_selected_symbol_path() const { auto selected = icon_view->get_selected_items(); if (selected.empty()) { return std::nullopt; } return selected.front(); } std::optional SymbolsDialog::get_selected_symbol() const { auto selected = get_selected_symbol_path(); if (!selected) { return std::nullopt; } return _symbols.path_to_child_iter(*selected); } /** Return the dimensions of the symbol at the given path, in document units. */ Geom::Point SymbolsDialog::getSymbolDimensions(const std::optional& it) const { if (!it) { return Geom::Point(); } return (**it)[g_columns.doc_dimensions]; } /** Return the ID of the symbol at the given path, with empty string fallback. */ Glib::ustring SymbolsDialog::getSymbolId(const std::optional& it) const { if (!it) { return ""; } return (**it)[g_columns.symbol_id]; } /** Store the symbol in the clipboard for further manipulation/insertion into document. * * @param symbol_path The path to the symbol in the tree model. * @param bbox The bounding box to set on the clipboard document's clipnode. */ void SymbolsDialog::sendToClipboard(const Gtk::TreeIter& symbol_iter, Geom::Rect const &bbox) { auto symbol_id = getSymbolId(symbol_iter); if (symbol_id.empty()) return; auto symbol_document = get_symbol_document(symbol_iter); if (!symbol_document) { //we are in global search so get the original symbol document by title symbol_document = getDocument(); } if (!symbol_document) { return; } if (SPObject* symbol = symbol_document->getObjectById(symbol_id)) { // Find style for use in // First look for default style stored in gchar const* style = symbol->getAttribute("inkscape:symbol-style"); if (!style) { // If no default style in , look in documents. if (symbol_document == getDocument()) { style = styleFromUse(symbol_id.c_str(), symbol_document); } else { style = symbol_document->getReprRoot()->attribute("style"); } } ClipboardManager::get()->copySymbol(symbol->getRepr(), style, symbol_document, bbox); } } void SymbolsDialog::iconChanged() { if (_update.pending()) return; if (auto selected = get_selected_symbol()) { auto const dims = getSymbolDimensions(selected); sendToClipboard(*selected, Geom::Rect(-0.5 * dims, 0.5 * dims)); } } #ifdef WITH_LIBVISIO // Extend libvisio's native RVNGSVGDrawingGenerator with support for extracting stencil names (to be used as ID/title) class REVENGE_API RVNGSVGDrawingGenerator_WithTitle : public RVNGSVGDrawingGenerator { public: RVNGSVGDrawingGenerator_WithTitle(RVNGStringVector &output, RVNGStringVector &titles, const RVNGString &nmSpace) : RVNGSVGDrawingGenerator(output, nmSpace) , _titles(titles) {} void startPage(const RVNGPropertyList &propList) override { RVNGSVGDrawingGenerator::startPage(propList); if (propList["draw:name"]) { _titles.append(propList["draw:name"]->getStr()); } else { _titles.append(""); } } private: RVNGStringVector &_titles; }; // Read Visio stencil files SPDocument* read_vss(std::string filename, std::string name) { gchar *fullname; #ifdef _WIN32 // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows // therefore attempt to convert uri to the system codepage // even if this is not possible the alternate short (8.3) file name will be used if available fullname = g_win32_locale_filename_from_utf8(filename.c_str()); #else fullname = strdup(filename.c_str()); #endif RVNGFileStream input(fullname); g_free(fullname); if (!libvisio::VisioDocument::isSupported(&input)) { return nullptr; } RVNGStringVector output; RVNGStringVector titles; RVNGSVGDrawingGenerator_WithTitle generator(output, titles, "svg"); if (!libvisio::VisioDocument::parseStencils(&input, &generator)) { return nullptr; } if (output.empty()) { return nullptr; } // prepare a valid title for the symbol file Glib::ustring title = Glib::Markup::escape_text(name); // prepare a valid id prefix for symbols libvisio doesn't give us a name for Glib::RefPtr regex1 = Glib::Regex::create("[^a-zA-Z0-9_-]"); Glib::ustring id = regex1->replace(name, 0, "_", Glib::REGEX_MATCH_PARTIAL); Glib::ustring tmpSVGOutput; tmpSVGOutput += "\n"; tmpSVGOutput += "\n"; tmpSVGOutput += " "; tmpSVGOutput += title; tmpSVGOutput += "\n"; tmpSVGOutput += " \n"; // Each "symbol" is in its own SVG file, we wrap with and merge into one file. for (unsigned i=0; ireplace(titles[i].cstr(), 0, "_", Glib::REGEX_MATCH_PARTIAL); } else { ss << id << "_" << i; } tmpSVGOutput += "\n"; if (titles.size() == output.size() && titles[i] != "") { tmpSVGOutput += "" + Glib::ustring(RVNGString::escapeXML(titles[i].cstr()).cstr()) + "\n"; } std::istringstream iss( output[i].cstr() ); std::string line; while( std::getline( iss, line ) ) { if( line.find( "svg:svg" ) == std::string::npos ) { tmpSVGOutput += line + "\n"; } } tmpSVGOutput += "\n"; } tmpSVGOutput += " \n"; tmpSVGOutput += "\n"; return SPDocument::createNewDocFromMem(tmpSVGOutput.c_str(), tmpSVGOutput.size(), false); } #endif /* Hunts preference directories for symbol files */ void scan_all_symbol_sets(std::map& symbol_sets) { using namespace Inkscape::IO::Resource; std::regex matchtitle(".*?(.*?)<(/| /)"); for (auto& filename : get_filenames(SYMBOLS, {".svg", ".vss", "vssx", "vsdx"})) { if (symbol_sets.count(filename)) continue; if (Glib::str_has_suffix(filename, ".vss") || Glib::str_has_suffix(filename, ".vssx") || Glib::str_has_suffix(filename, ".vsdx")) { std::size_t found = filename.find_last_of("/\\"); auto title = found != Glib::ustring::npos ? filename.substr(found + 1) : filename; title = title.erase(title.rfind('.')); if (title.empty()) { title = _("Unnamed Symbols"); } symbol_sets[filename].title = title; } else { std::ifstream infile(filename); std::string line; while (std::getline(infile, line)) { std::string title_res = std::regex_replace(line, matchtitle,"$1",std::regex_constants::format_no_copy); if (!title_res.empty()) { title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str()); symbol_sets[filename].title = title_res; break; } auto position_exit = line.find(" &l) { if (is(r) ) { l.push_back(cast(r)); } for (auto& child: r->children) { useInDoc( &child, l ); } } std::vector SymbolsDialog::useInDoc( SPDocument* useDocument) { std::vector l; useInDoc (useDocument->getRoot(), l); return l; } // Returns style from first element found that references id. // This is a last ditch effort to find a style. gchar const* SymbolsDialog::styleFromUse( gchar const* id, SPDocument* document) { gchar const* style = nullptr; std::vector l = useInDoc( document ); for( auto use:l ) { if ( use ) { gchar const *href = Inkscape::getHrefAttribute(*use->getRepr()).second; if( href ) { Glib::ustring href2(href); Glib::ustring id2(id); id2 = "#" + id2; if( !href2.compare(id2) ) { style = use->getRepr()->attribute("style"); break; } } } } return style; } size_t SymbolsDialog::total_symbols() const { return _symbols._store->children().size(); } size_t SymbolsDialog::visible_symbols() const { return _symbols._filtered->children().size(); } void SymbolsDialog::set_info() { auto total = total_symbols(); auto visible = visible_symbols(); if (!total) { set_info(""); } else if (total == visible) { set_info(Glib::ustring::compose("%1: %2", _("Symbols"), total).c_str()); } else if (visible == 0) { set_info(Glib::ustring::compose("%1: %2 / %3", _("Symbols"), _("none"), total).c_str()); } else { set_info(Glib::ustring::compose("%1: %2 / %3", _("Symbols"), visible, total).c_str()); } if (total == 0 || visible == 0) { showOverlay(); } else { hideOverlay(); } } void SymbolsDialog::set_info(const Glib::ustring& text) { auto info = "" + Glib::Markup::escape_text(text) + ""; get_widget(_builder, "info").set_markup(info); } Cairo::RefPtr add_background(Cairo::RefPtr image, uint32_t rgb, double margin, double radius, int device_scale, std::optional border = std::optional()) { auto w = image ? cairo_image_surface_get_width(image->cobj()) : 0; auto h = image ? cairo_image_surface_get_height(image->cobj()) : 0; auto width = w / device_scale + 2 * margin; auto height = h / device_scale + 2 * margin; auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, width * device_scale, height * device_scale); cairo_surface_set_device_scale(surface->cobj(), device_scale, device_scale); auto ctx = Cairo::Context::create(surface); auto x = 0; auto y = 0; if (border.has_value()) { x += 0.5 * device_scale; y += 0.5 * device_scale; width -= device_scale; height -= device_scale; } ctx->arc(x + width - radius, y + radius, radius, -M_PI_2, 0); ctx->arc(x + width - radius, y + height - radius, radius, 0, M_PI_2); ctx->arc(x + radius, y + height - radius, radius, M_PI_2, M_PI); ctx->arc(x + radius, y + radius, radius, M_PI, 3 * M_PI_2); ctx->close_path(); ctx->set_source_rgb(SP_RGBA32_R_F(rgb), SP_RGBA32_G_F(rgb), SP_RGBA32_B_F(rgb)); if (border.has_value()) { ctx->fill_preserve(); auto b = *border; ctx->set_source_rgb(SP_RGBA32_R_F(b), SP_RGBA32_G_F(b), SP_RGBA32_B_F(b)); ctx->set_line_width(1.0); ctx->stroke(); } else { ctx->fill(); } if (image) { ctx->set_source(image, margin, margin); ctx->paint(); } return surface; } void SymbolsDialog::addSymbol(SPSymbol* symbol, Glib::ustring doc_title, SPDocument* document) { auto id = symbol->getRepr()->attribute("id"); auto title = symbol->title(); // From title element Glib::ustring short_title = title ? g_dpgettext2(nullptr, "Symbol", title) : id; g_free(title); auto symbol_title = Glib::ustring::compose("%1 (%2)", short_title, doc_title); Geom::Point dimensions{64, 64}; // Default to 64x64 px if size not available. if (auto rect = symbol->documentVisualBounds()) { dimensions = rect->dimensions(); } auto set = symbol->document ? symbol->document->getDocumentFilename() : "null"; if (!set) set = "noname"; Gtk::ListStore::iterator row = _store->append(); std::ostringstream key; key << set << '\n' << id; (*row)[g_columns.cache_key] = key.str(); (*row)[g_columns.symbol_id] = Glib::ustring(id); // symbol title and document name - used in a tooltip (*row)[g_columns.symbol_title] = Glib::Markup::escape_text(symbol_title); // symbol title shown below image (*row)[g_columns.symbol_short_title] = "" + Glib::Markup::escape_text(short_title) + ""; // symbol title verbatim, used for searching/filtering (*row)[g_columns.symbol_search_title] = short_title; (*row)[g_columns.doc_dimensions] = dimensions; (*row)[g_columns.symbol_document] = document; } Cairo::RefPtr SymbolsDialog::draw_symbol(SPSymbol* symbol) { Cairo::RefPtr surface; Cairo::RefPtr image; int device_scale = get_scale_factor(); if (symbol) { image = drawSymbol(symbol); } else { unsigned psize = SYMBOL_ICON_SIZES[pack_size] * device_scale; image = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, psize, psize); cairo_surface_set_device_scale(image->cobj(), device_scale, device_scale); } // white background for typically black symbols, so they don't disappear in a dark theme if (image) { uint32_t background = 0xffffff00; double margin = 3.0; double radius = 3.0; surface = add_background(image, background, margin, radius, device_scale); } return surface; } /* * Returns image of symbol. * * Symbols normally are not visible. They must be referenced by a * element. A temporary document is created with a dummy * element and a element that references the symbol * element. Each real symbol is swapped in for the dummy symbol and * the temporary document is rendered. */ Cairo::RefPtr SymbolsDialog::drawSymbol(SPSymbol *symbol) { if (!symbol) return Cairo::RefPtr(); // Create a copy repr of the symbol with id="the_symbol" Inkscape::XML::Node *repr = symbol->getRepr()->duplicate(preview_document->getReprDoc()); repr->setAttribute("id", "the_symbol"); // First look for default style stored in gchar const* style = repr->attribute("inkscape:symbol-style"); if(!style) { // If no default style in , look in documents. if(symbol->document == getDocument()) { gchar const *id = symbol->getRepr()->attribute("id"); style = styleFromUse( id, symbol->document ); } else { style = symbol->document->getReprRoot()->attribute("style"); } } // This is for display in Symbols dialog only if( style ) repr->setAttribute( "style", style ); SPDocument::install_reference_document scoped(preview_document, symbol->document); preview_document->getDefs()->getRepr()->appendChild(repr); Inkscape::GC::release(repr); // Uncomment this to get the preview_document documents saved (useful for debugging) // FILE *fp = fopen (g_strconcat(id, ".svg", NULL), "w"); // sp_repr_save_stream(preview_document->getReprDoc(), fp); // fclose (fp); // Make sure preview_document is up-to-date. preview_document->ensureUpToDate(); // Make sure we have symbol in preview_document SPObject *object_temp = preview_document->getObjectById( "the_use" ); auto item = cast(object_temp); g_assert(item != nullptr); unsigned psize = SYMBOL_ICON_SIZES[pack_size]; cairo_surface_t* surface = 0; // We could use cache here, but it doesn't really work with the structure // of this user interface and we've already cached the pixbuf in the gtklist // Find object's bbox in document. // Note symbols can have own viewport... ignore for now. //Geom::OptRect dbox = item->geometricBounds(); Geom::OptRect dbox = item->documentVisualBounds(); if (dbox) { /* Scale symbols to fit */ double scale = 1.0; double width = dbox->width(); double height = dbox->height(); if( width == 0.0 ) width = 1.0; if( height == 0.0 ) height = 1.0; if (fit_symbol->get_active()) { scale = psize / ceil(std::max(width, height)); } else { scale = pow(2.0, scale_factor / 4.0) * psize / 32.0; } int device_scale = get_scale_factor(); surface = render_surface(renderDrawing, scale, *dbox, Geom::IntPoint(psize, psize), device_scale, nullptr, true); if (surface) { cairo_surface_set_device_scale(surface, device_scale, device_scale); } } preview_document->getObjectByRepr(repr)->deleteObject(false); return Cairo::RefPtr(new Cairo::Surface(surface, true)); } /* * Return empty doc to render symbols in. * Symbols are by default not rendered so a element is * provided. */ SPDocument* SymbolsDialog::symbolsPreviewDoc() { // BUG: must be inside const char buffer[] = "" " " ""; return SPDocument::createNewDocFromMem(buffer, strlen(buffer), false); } void SymbolsDialog::get_cell_data_func(Gtk::CellRenderer* cell_renderer, Gtk::TreeModel::Row row, bool visible) { std::string cache_key = (row)[g_columns.cache_key]; Glib::ustring id = (row)[g_columns.symbol_id]; Cairo::RefPtr surface; if (!visible) { // cell is not visible, so this is layout pass; return empty image of the right size int device_scale = get_scale_factor(); unsigned psize = SYMBOL_ICON_SIZES[pack_size] * device_scale; if (!g_dummy || g_dummy->get_width() != psize) { g_dummy = g_dummy.cast_static(draw_symbol(nullptr)); } surface = g_dummy; } else { // cell is visible, so we need to return correct symbol image and render it if it's missing if (auto image = _image_cache.get(cache_key)) { // cache hit surface = *image; } else { // render SPDocument* doc = row[g_columns.symbol_document]; if (!doc) doc = getDocument(); SPSymbol* symbol = doc ? cast(doc->getObjectById(id)) : nullptr; surface = draw_symbol(symbol); _image_cache.insert(cache_key, surface); } } cell_renderer->set_property("surface", surface); } } //namespace Dialogs } //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=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 :