Enhance save as template and new from template dialogs
[inkscape.git] / src / ui / widget / template-list.cpp
blob203ac14f7cad91211dead97b16d0b11a7ddf5e9c
1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /* Authors:
3 * Martin Owens <doctormo@geek-2.com>
5 * Copyright (C) 2022 Authors
7 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
8 */
10 #include "template-list.h"
12 #include <cairomm/surface.h>
13 #include <giomm/liststore.h>
14 #include <glibmm/markup.h>
15 #include <glibmm/refptr.h>
16 #include <gtkmm/expression.h>
17 #include <gtkmm/gridview.h>
18 #include <map>
19 #include <glib/gi18n.h>
20 #include <gdkmm/pixbuf.h>
21 #include <gtkmm/builder.h>
22 #include <gtkmm/iconview.h>
23 #include <gtkmm/liststore.h>
24 #include <gtkmm/numericsorter.h>
25 #include <gtkmm/scrolledwindow.h>
26 #include <gtkmm/treemodel.h>
27 #include <gtkmm/sortlistmodel.h>
28 #include <gtkmm/singleselection.h>
29 #include <memory>
30 #include <glibmm/miscutils.h>
31 #include <gtkmm/filterlistmodel.h>
33 #include "document.h"
34 #include "extension/db.h"
35 #include "extension/template.h"
36 #include "inkscape-application.h"
37 #include "io/resource.h"
38 #include "ui/builder-utils.h"
39 #include "ui/iconview-item-factory.h"
40 #include "ui/util.h"
41 #include "ui/svg-renderer.h"
43 using namespace Inkscape::IO::Resource;
44 using Inkscape::Extension::TemplatePreset;
46 namespace Inkscape::UI::Widget {
48 struct TemplateList::TemplateItem : public Glib::Object {
49 Glib::ustring name;
50 Glib::ustring label;
51 Glib::ustring tooltip;
52 Glib::RefPtr<Gdk::Texture> icon;
53 Glib::ustring key;
54 int priority;
55 Glib::ustring category;
57 static Glib::RefPtr<TemplateItem> create(const Glib::ustring& name, const Glib::ustring& label, const Glib::ustring& tooltip,
58 Glib::RefPtr<Gdk::Texture> icon, Glib::ustring key, int priority, const Glib::ustring& category) {
60 auto item = Glib::make_refptr_for_instance<TemplateItem>(new TemplateItem());
61 item->name = name;
62 item->label = label;
63 item->tooltip = tooltip;
64 item->icon = icon;
65 item->key = key;
66 item->priority = priority;
67 item->category = category;
68 return item;
70 private:
71 TemplateItem() = default;
75 TemplateList::TemplateList(BaseObjectType *cobject, const Glib::RefPtr<Gtk::Builder> &refGlade)
76 : Gtk::Stack(cobject)
80 static Glib::ustring all_templates = "All templates";
82 /**
83 * Initialise this template list with categories and icons
85 void TemplateList::init(Inkscape::Extension::TemplateShow mode, AddPage add_page, bool allow_unselect)
87 // same width for all items
88 set_hhomogeneous();
89 // height can vary per row
90 set_vhomogeneous(false);
91 // track page switching
92 property_visible_child_name().signal_changed().connect([this]() {
93 _signal_switch_page.emit(get_visible_child_name());
94 });
96 std::map<std::string, Glib::RefPtr<Gio::ListStore<TemplateItem>>> stores;
98 Inkscape::Extension::DB::TemplateList extensions;
99 Inkscape::Extension::db.get_template_list(extensions);
101 Glib::RefPtr<Gio::ListStore<TemplateItem>> all;
102 if (add_page == All) {
103 all = generate_category(all_templates, allow_unselect);
106 int group = 0;
107 for (auto tmod : extensions) {
108 for (auto preset : tmod->get_presets(mode)) {
109 auto const &cat = preset->get_category();
110 if (add_page == Custom && cat != "Custom") continue;
112 if (auto it = stores.lower_bound(cat);
113 it == stores.end() || it->first != cat)
115 try {
116 group += 10000;
117 it = stores.emplace_hint(it, cat, generate_category(cat, allow_unselect));
118 it->second->remove_all();
119 } catch (UIBuilderError const& error) {
120 g_error("Error building templates %s\n", error.what());
121 return;
124 if (add_page == Custom) {
125 // add new template placeholder
126 auto const filepath = Glib::build_filename("icons", "custom.svg");
127 auto const fullpath = get_filename(TEMPLATES, filepath.c_str(), false, true);
128 auto icon = to_texture(icon_to_pixbuf(fullpath, get_scale_factor()));
129 auto templ = TemplateItem::create(
130 Glib::Markup::escape_text(_("<new template>")),
131 "", "", icon, "-new-template-", -1, cat
133 stores[cat]->append(templ);
137 auto& name = preset->get_name();
138 auto& desc = preset->get_description();
139 auto& label = preset->get_label();
140 auto tooltip = _(desc.empty() ? name.c_str() : desc.c_str());
141 auto trans_label = label.empty() ? "" : _(label.c_str());
142 auto icon = to_texture(icon_to_pixbuf(preset->get_icon_path(), get_scale_factor()));
144 auto templ = TemplateItem::create(
145 Glib::Markup::escape_text(name),
146 Glib::Markup::escape_text(trans_label),
147 Glib::Markup::escape_text(tooltip),
148 icon, preset->get_key(), group + preset->get_sort_priority(),
151 stores[cat]->append(templ);
152 if (all) {
153 all->append(templ);
158 refilter(_search_term);
160 if (allow_unselect) {
161 reset_selection();
166 * Turn the requested template icon name into a pixbuf
168 Cairo::RefPtr<Cairo::ImageSurface> TemplateList::icon_to_pixbuf(std::string const &path, int scale)
170 // TODO: cache to filesystem. This function is a major bottleneck for startup time (ca. 1 second)!
171 // The current memory-based caching only catches the case where multiple templates share the same icon.
172 static std::map<std::string, Cairo::RefPtr<Cairo::ImageSurface>> cache;
173 if (path.empty()) {
174 return {};
176 if (cache.contains(path)) {
177 return cache[path];
179 Inkscape::svg_renderer renderer(path.c_str());
180 auto result = renderer.render_surface(scale * 0.7); // reduced template icon size to fit more in a dialog
181 cache[path] = result;
182 return result;
185 sigc::signal<void (const Glib::ustring&)> TemplateList::signal_switch_page() {
186 return _signal_switch_page;
190 * Generate a new category with the given label and return it's list store.
192 Glib::RefPtr<Gio::ListStore<TemplateList::TemplateItem>> TemplateList::generate_category(std::string const &label, bool allow_unselect)
194 auto builder = create_builder("widget-new-from-template.ui");
195 auto& container = get_widget<Gtk::ScrolledWindow>(builder, "container");
196 auto& icons = get_widget<Gtk::GridView> (builder, "iconview");
198 auto store = Gio::ListStore<TemplateItem>::create();
199 auto sorter = Gtk::NumericSorter<int>::create(Gtk::ClosureExpression<int>::create([this](auto& item){
200 auto ptr = std::dynamic_pointer_cast<TemplateItem>(item);
201 return ptr ? ptr->priority : 0;
202 }));
203 auto sorted_model = Gtk::SortListModel::create(store, sorter);
204 if (!_filter) {
205 _filter = Gtk::BoolFilter::create({});
207 auto filtered_model = Gtk::FilterListModel::create(sorted_model, _filter);
208 auto selection_model = Gtk::SingleSelection::create(filtered_model);
209 if (allow_unselect) {
210 selection_model->set_can_unselect();
211 selection_model->set_autoselect(false);
213 auto factory = IconViewItemFactory::create([](auto& ptr) -> IconViewItemFactory::ItemData {
214 auto tmpl = std::dynamic_pointer_cast<TemplateItem>(ptr);
215 if (!tmpl) return {};
217 auto label = tmpl->label.empty() ? tmpl->name :
218 tmpl->name + "<small><span line_height='0.5'>\n\n</span><span alpha='60%'>" + tmpl->label + "</span></small>";
219 return { .label_markup = label, .image = tmpl->icon, .tooltip = tmpl->tooltip };
221 icons.set_max_columns(30);
222 icons.set_tab_behavior(Gtk::ListTabBehavior::ITEM); // don't steal the tab key
223 icons.set_factory(factory->get_factory());
224 icons.set_model(selection_model);
226 // This packing keeps the Gtk widget alive, beyond the builder's lifetime
227 add(container, label, g_dpgettext2(nullptr, "TemplateCategory", label.c_str()));
228 _categories.emplace_back(label);
230 selection_model->signal_selection_changed().connect([this](auto pos, auto count){
231 _item_selected_signal.emit(count > 0 ? static_cast<int>(pos) : -1);
233 icons.signal_activate().connect([this](auto pos){
234 _item_activated_signal.emit();
237 _factory.emplace_back(std::move(factory));
238 return store;
242 * Returns true if the template list has a visible, selected preset.
244 bool TemplateList::has_selected_preset()
246 return !!get_selected_preset();
249 bool TemplateList::has_selected_new_template() {
250 if (auto item = get_selected_item()) {
251 return item->key == "-new-template-";
253 return false;
256 Glib::RefPtr<TemplateList::TemplateItem> TemplateList::get_selected_item(Gtk::Widget* current_page) {
257 if (auto iconview = get_iconview(current_page ? current_page : get_visible_child())) {
258 auto sel = std::dynamic_pointer_cast<Gtk::SingleSelection>(iconview->get_model());
259 auto ptr = sel->get_selected_item();
260 if (auto item = std::dynamic_pointer_cast<TemplateList::TemplateItem>(ptr)) {
261 return item;
264 return nullptr;
268 * Returns the selected template preset, if one is not selected returns nullptr.
270 std::shared_ptr<TemplatePreset> TemplateList::get_selected_preset(Gtk::Widget* current_page)
272 if (auto item = get_selected_item(current_page)) {
273 return Extension::Template::get_any_preset(item->key);
275 return nullptr;
279 * Create a new document based on the selected item and return.
281 SPDocument *TemplateList::new_document(Gtk::Widget* current_page)
283 auto app = InkscapeApplication::instance();
284 if (auto preset = get_selected_preset(current_page)) {
285 if (auto doc = preset->new_from_template()) {
286 // TODO: Add memory to remember this preset for next time.
287 return app->document_add(std::move(doc));
288 } else {
289 // Cancel pressed in options box.
290 return nullptr;
293 // Fallback to the default template (already added)!
294 return app->document_new();
297 // Show page by its name
298 void TemplateList::show_page(const Glib::ustring& name) {
299 set_visible_child(name);
300 refilter(_search_term);
303 // callback to check if template should be visible
304 bool TemplateList::is_item_visible(const Glib::RefPtr<Glib::ObjectBase>& item, const Glib::ustring& search) const {
305 auto ptr = std::dynamic_pointer_cast<TemplateItem>(item);
306 if (!ptr) return false;
308 const auto& templ = *ptr;
310 if (search.empty()) return true;
312 // filter by name and label
313 return templ.label.lowercase().find(search) != Glib::ustring::npos ||
314 templ.name.lowercase().find(search) != Glib::ustring::npos;
317 // filter list of visible templates down to those that contain given search string in their name or label
318 void TemplateList::filter(Glib::ustring search) {
319 _search_term = search;
320 refilter(search);
323 // set keyboard focus on template list
324 void TemplateList::focus() {
325 if (auto iconview = get_iconview(get_visible_child())) {
326 iconview->grab_focus();
330 void TemplateList::refilter(Glib::ustring search) {
331 // When a new expression is set in the BoolFilter, it emits signal_changed(),
332 // and the FilterListModel re-evaluates the filter.
333 search = search.lowercase();
334 auto expression = Gtk::ClosureExpression<bool>::create([this, search](auto& item){ return is_item_visible(item, search); });
335 // filter results
336 _filter->set_expression(expression);
340 * Reset the selection, forcing the use of the default template.
342 void TemplateList::reset_selection(Gtk::Widget* current_page)
344 // TODO: Add memory here for the new document default (see new_document).
345 for (auto const widget : UI::get_children(current_page ? *current_page : *this)) {
346 if (auto iconview = get_iconview(widget)) {
347 auto sel = std::dynamic_pointer_cast<Gtk::SingleSelection>(iconview->get_model());
348 sel->unselect_all();
354 * Returns the internal iconview for the given widget.
356 Gtk::GridView *TemplateList::get_iconview(Gtk::Widget *widget)
358 if (!widget) return nullptr;
360 for (auto const child : UI::get_children(*widget)) {
361 if (auto iconview = get_iconview(child)) {
362 return iconview;
366 return dynamic_cast<Gtk::GridView *>(widget);
369 } // namespace Inkscape::UI::Widget
372 Local Variables:
373 mode:c++
374 c-file-style:"stroustrup"
375 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
376 indent-tabs-mode:nil
377 fill-column:99
378 End:
380 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :