1 // SPDX-License-Identifier: GPL-2.0-or-later
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.
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>
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>
30 #include <glibmm/miscutils.h>
31 #include <gtkmm/filterlistmodel.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"
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
{
51 Glib::ustring tooltip
;
52 Glib::RefPtr
<Gdk::Texture
> icon
;
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());
63 item
->tooltip
= tooltip
;
66 item
->priority
= priority
;
67 item
->category
= category
;
71 TemplateItem() = default;
75 TemplateList::TemplateList(BaseObjectType
*cobject
, const Glib::RefPtr
<Gtk::Builder
> &refGlade
)
80 static Glib::ustring all_templates
= "All templates";
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
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());
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
);
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
)
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());
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
);
158 refilter(_search_term
);
160 if (allow_unselect
) {
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
;
176 if (cache
.contains(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
;
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;
203 auto sorted_model
= Gtk::SortListModel::create(store
, sorter
);
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
));
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-";
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
)) {
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
);
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
));
289 // Cancel pressed in options box.
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
;
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
); });
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());
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
)) {
366 return dynamic_cast<Gtk::GridView
*>(widget
);
369 } // namespace Inkscape::UI::Widget
374 c-file-style:"stroustrup"
375 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
380 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :