Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
font-list.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2
3#include <algorithm>
4#include <array>
5#include <glibmm/main.h>
6#include <glibmm/priorities.h>
7#include <glibmm/ustring.h>
8#include <giomm/menu.h>
9#include <giomm/simpleactiongroup.h>
10#include <gtkmm/cellrenderertext.h>
11#include <gtkmm/enums.h>
12#include <gtkmm/label.h>
13#include <gtkmm/progressbar.h>
14#include <gtkmm/separator.h>
15#include <gtkmm/stringlist.h>
16#include <gtkmm/treeiter.h>
17#include <gtkmm/widget.h>
18#include <iostream>
19#include <iterator>
20#include <iomanip>
21#include <memory>
22#include <pangomm/fontdescription.h>
23#include <pangomm/fontfamily.h>
24#include <set>
25#include <glibmm/markup.h>
26#include <gtkmm/checkbutton.h>
27#include <gtkmm/menubutton.h>
28#include <gtkmm/scrolledwindow.h>
29#include <gtkmm/searchentry2.h>
30#include <gtkmm/snapshot.h>
31#include <gtkmm/scale.h>
32#include <gtkmm/togglebutton.h>
33#include <glibmm/i18n.h>
34#include <pangomm.h>
37#include <vector>
38#include "font-list.h"
39#include "preferences.h"
40#include "ui/builder-utils.h"
41#include "ui/icon-loader.h"
46
48
49namespace Inkscape::UI::Widget {
50
51std::unique_ptr<FontSelectorInterface> FontList::create_font_list(Glib::ustring path) {
52 return std::make_unique<FontList>(path);
53}
54
55struct FontListColumnModel : public Gtk::TreeModelColumnRecord {
56 // font metadata for installed fonts only
57 Gtk::TreeModelColumn<Inkscape::FontInfo> font;
58 // font's class
59 // Gtk::TreeModelColumn<FontClass> font_class;
60 Gtk::TreeModelColumn<bool> injected;
61 // fontspec for fonts that are not installed, but used in a document
62 Gtk::TreeModelColumn<Glib::ustring> alt_fontspec;
63 // icon to show next to a font name (if any)
64 Gtk::TreeModelColumn<Glib::ustring> icon_name;
65
66 FontListColumnModel() {
67 add(alt_fontspec);
68 add(injected);
69 add(icon_name);
70 add(font);
71 }
72};
73
74FontListColumnModel g_column_model; // model for font list
75
76// list of font sizes for a slider; combo box has its own list
77static std::array<int, 38> g_font_sizes = {
78 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 24, 28, 32, 36,
79 44, 56, 64, 72, 80, 96, 112, 128, 144, 160, 192, 224, 256,
80 300, 350, 400, 450, 500, 550, 600, 700, 800
81};
82
83static int index_to_font_size(int index) {
84 if (index < 0) {
85 return g_font_sizes.front();
86 }
87 else if (index >= g_font_sizes.size()) {
88 return g_font_sizes.back();
89 }
90 else {
91 return g_font_sizes[index];
92 }
93}
94
95static int font_size_to_index(double size) {
96 auto it = std::lower_bound(begin(g_font_sizes), end(g_font_sizes), static_cast<int>(size));
97 return std::distance(begin(g_font_sizes), it);
98}
99
100// construct font name from Pango face and family;
101// return font name as it is recorded in the font itself, as far as Pango allows it
102Glib::ustring get_full_name(const Inkscape::FontInfo& font_info) {
103 return get_full_font_name(font_info.ff, font_info.face);
104}
105
106Glib::ustring get_alt_name(const Glib::ustring& fontspec) {
107 static Glib::ustring sans = "sans-serif";
108 if (fontspec.find(sans) != Glib::ustring::npos) {
109 auto end = fontspec[sans.size()];
110 if (end == 0 || end == ' ' || end == ',') {
111 return _("Sans Serif") + fontspec.substr(sans.size());
112 }
113 }
114 return fontspec; // use font spec verbatim
115}
116
117class CellFontRenderer : public Gtk::CellRendererText {
118public:
119 CellFontRenderer() { }
120
121 ~CellFontRenderer() override = default;
122
123 Gtk::Widget* _tree = nullptr;
124 bool _show_font_name = true;
125 int _font_size = 200; // size in %, where 100 is normal UI font size
126 Glib::ustring _sample_text; // text to render (font preview)
127 Glib::ustring _name;
128
129 void snapshot_vfunc(Glib::RefPtr<Gtk::Snapshot> const &snapshot, Gtk::Widget &widget, Gdk::Rectangle const &background_area, Gdk::Rectangle const &cell_area, Gtk::CellRendererState flags) override;
130};
131
132CellFontRenderer& get_renderer(Gtk::CellRenderer& renderer) {
133 return dynamic_cast<CellFontRenderer&>(renderer);
134}
135
136Glib::ustring get_font_name(Gtk::TreeIter<Gtk::TreeRow>& iter) {
137 if (!iter) return Glib::ustring();
138
139 const Inkscape::FontInfo& font = (*iter)[g_column_model.font];
140 auto present = !!font.ff;
141 Glib::ustring&& alt = (*iter)[g_column_model.alt_fontspec];
142 auto name = Glib::Markup::escape_text(present ? get_full_name(font) : get_alt_name(alt));
143 return name;
144}
145
146void get_cell_data_func(Gtk::CellRenderer* cell_renderer, Gtk::TreeModel::ConstRow row) {
147 auto& renderer = get_renderer(*cell_renderer);
148
149 const Inkscape::FontInfo& font = row[g_column_model.font];
150 // auto font_class = row[g_column_model.font_class];
151 auto present = !!font.ff;
152 Glib::ustring&& icon_name = row[g_column_model.icon_name];
153 Glib::ustring&& alt = row[g_column_model.alt_fontspec];
154
155 auto name = Glib::Markup::escape_text(present ? get_full_name(font) : get_alt_name(alt));
156 // if no sample text given, then render font name
157 auto text = Glib::Markup::escape_text(renderer._sample_text.empty() ? name : renderer._sample_text);
158
159 auto font_desc = Glib::Markup::escape_text(
160 present ? Inkscape::get_font_description(font.ff, font.face).to_string() : (alt.empty() ? "sans-serif" : alt));
161 auto markup = Glib::ustring::format("<span allow_breaks='false' size='", renderer._font_size, "%' font='", font_desc, "'>", text, "</span>");
162
163 if (renderer._show_font_name) {
164 renderer._name = name;
165 }
166
167 renderer.set_property("markup", markup);
168}
169
170void CellFontRenderer::snapshot_vfunc(Glib::RefPtr<Gtk::Snapshot> const &snapshot, Gtk::Widget &widget, Gdk::Rectangle const &background_area, Gdk::Rectangle const &cell_area, Gtk::CellRendererState flags) {
171 Gdk::Rectangle bgnd(background_area);
172 Gdk::Rectangle area(cell_area);
173 auto margin = 0; // extra space for icon?
174 bgnd.set_x(bgnd.get_x() + margin);
175 area.set_x(area.get_x() + margin);
176 const auto name_font_size = 10; // attempt to select <small> text size
177 const auto bottom = area.get_y() + area.get_height(); // bottom where the info font name will be placed
178 Glib::RefPtr<Pango::Layout> layout;
179 int text_height = 0;
180
181 if (_show_font_name) {
182 layout = _tree->create_pango_layout(_name);
183 Pango::FontDescription font("Noto"); // wide range of character support
184 font.set_weight(Pango::Weight::NORMAL);
185 font.set_size(name_font_size * PANGO_SCALE);
186 layout->set_font_description(font);
187 int text_width = 0;
188 // get the text dimensions
189 layout->get_pixel_size(text_width, text_height);
190 // shrink area to prevent overlap
191 area.set_height(area.get_height() - text_height);
192 }
193
194 CellRendererText::snapshot_vfunc(snapshot, widget, bgnd, area, flags);
195
196 if (_show_font_name) {
197 auto context = _tree->get_style_context();
198 Gtk::StateFlags sflags = _tree->get_state_flags();
199 if ((bool)(flags & Gtk::CellRendererState::SELECTED)) {
200 sflags |= Gtk::StateFlags::SELECTED;
201 }
202 context->set_state(sflags);
203 Gdk::RGBA fg = context->get_color();
204 snapshot->save();
205 snapshot->translate({(float)(area.get_x() + 2), (float)bottom - text_height});
206 snapshot->append_layout(layout, fg);
207 snapshot->restore();
208 }
209}
210
212 const char* icon = nullptr;
213
214 switch (order) {
216 icon = "sort-alphabetically-symbolic";
217 break;
218
220 icon = "sort-by-weight-symbolic";
221 break;
222
224 icon = "sort-by-width-symbolic";
225 break;
226
227 default:
228 g_warning("Missing case in get_sort_icon");
229 break;
230 }
231
232 return icon;
233}
234
235void set_grid_cell_size(Gtk::CellRendererText* renderer, int font_size_percent) {
236 // TODO: use pango layout to calc sizes
237 int size = 20 * font_size_percent / 100;
238 renderer->set_fixed_size(size * 4 / 3, size);
239};
240
241FontList::FontList(Glib::ustring preferences_path) :
242 _prefs(std::move(preferences_path)),
243 _builder(create_builder("font-list.glade")),
244 _main_grid(get_widget<Gtk::Grid>(_builder, "main-grid")),
245 _tag_list(get_widget<Gtk::ListBox>(_builder, "categories")),
246 _font_list(get_widget<Gtk::TreeView>(_builder, "font-list")),
247 _font_grid(get_widget<Gtk::IconView>(_builder, "font-grid")),
248 _font_size(get_widget<Gtk::ComboBoxText>(_builder, "font-size")),
249 _font_size_scale(get_widget<Gtk::Scale>(_builder, "font-size-scale")),
250 _tag_box(get_widget<Gtk::Box>(_builder, "tag-box")),
251 _info_box(get_widget<Gtk::Box>(_builder, "info-box")),
252 _progress_box(get_widget<Gtk::Box>(_builder, "progress-box")),
253 _font_tags(Inkscape::FontTags::get())
254{
255 _cell_renderer = std::make_unique<CellFontRenderer>();
256 auto font_renderer = static_cast<CellFontRenderer*>(_cell_renderer.get());
257 font_renderer->_tree = &_font_list;
258
259 _cell_icon_renderer = std::make_unique<Gtk::CellRendererPixbuf>();
260 auto ico = static_cast<Gtk::CellRendererPixbuf*>(_cell_icon_renderer.get());
261 ico->set_fixed_size(16, 16);
262
263 _grid_renderer = std::make_unique<CellFontRenderer>();
264 auto grid_renderer = static_cast<CellFontRenderer*>(_grid_renderer.get());
265 grid_renderer->_show_font_name = false;
266
267 _font_list_store = Gtk::ListStore::create(g_column_model);
268
269 get_widget<Gtk::Box>(_builder, "variants").append(_font_variations);
270 _font_variations.get_size_group(0)->add_widget(get_widget<Gtk::Label>(_builder, "font-size-label"));
273 if (_update.pending()) return;
274 _signal_changed.emit();
275 });
276
277 set_hexpand();
278 set_vexpand();
280 set_margin_start(0);
281 set_margin_end(0);
282 set_margin_top(5);
283 set_margin_bottom(0);
284
285 auto options = &get_widget<Gtk::ToggleButton>(_builder, "btn-options");
286 auto options_grid = &get_widget<Gtk::Grid>(_builder, "options-grid");
287 options->signal_toggled().connect([=](){
288 options_grid->set_visible(options->get_active());
289 });
290
291 std::pair<const char*, Inkscape::FontOrder> sorting[3] = {
292 {N_("Sort alphabetically"), Inkscape::FontOrder::by_name},
293 {N_("Light to heavy"), Inkscape::FontOrder::by_weight},
294 {N_("Condensed to expanded"), Inkscape::FontOrder::by_width}
295 };
296 auto sort_menu = Gtk::make_managed<PopoverMenu>(Gtk::PositionType::BOTTOM);
297 for (auto&& el : sorting) {
298 auto item = Gtk::make_managed<PopoverMenuItem>();
299 auto hbox = Gtk::make_managed<Gtk::Box>();
300 hbox->append(*Gtk::manage(sp_get_icon_image(get_sort_icon(el.second), Gtk::IconSize::NORMAL)));
301 hbox->append(*Gtk::make_managed<Gtk::Label>(_(el.first)));
302 hbox->set_spacing(4);
303 item->set_child(*hbox);
304 item->signal_activate().connect([=, this] { sort_fonts(el.second); });
305 sort_menu->append(*item);
306 }
307 get_widget<Gtk::MenuButton>(_builder, "btn-sort").set_popover(*sort_menu);
308 get_widget<Gtk::Button>(_builder, "id-reset-filter").signal_clicked().connect([this](){
309 bool modified = false;
310 if (_font_tags.deselect_all()) {
311 modified = true;
312 }
314 if (!fc->get_selected_collections().empty()) {
315 fc->clear_selected_collections();
316 modified = true;
317 }
318 if (modified) {
321 }
322 });
323
324 auto search = &get_widget<Gtk::SearchEntry2>(_builder, "font-search");
325 search->signal_changed().connect([this](){ filter(); });
326
327 auto set_row_height = [font_renderer, this](int font_size_percent) {
328 font_renderer->_font_size = font_size_percent;
329 // TODO: use pango layout to calc sizes
330 int hh = (font_renderer->_show_font_name ? 10 : 0) + 18 * font_renderer->_font_size / 100;
331 font_renderer->set_fixed_size(-1, hh);
332 // resize rows
333 _font_list.set_fixed_height_mode(false);
334 _font_list.set_fixed_height_mode();
335 };
336 auto set_grid_size = [=](int font_size_percent) {
337 grid_renderer->_font_size = font_size_percent;
338 set_grid_cell_size(grid_renderer, font_size_percent);
339 };
340
341 auto prefs = Inkscape::Preferences::get();
342
343 font_renderer->_font_size = prefs->getIntLimited(_prefs + "/preview-size", 200, 100, 800);
344 auto size = &get_widget<Gtk::Scale>(_builder, "preview-font-size");
345 size->set_format_value_func([](double val){
346 return Glib::ustring::format(std::fixed, std::setprecision(0), val) + "%";
347 });
348 size->set_value(font_renderer->_font_size);
349 size->signal_value_changed().connect([=, this](){
350 auto font_size = size->get_value();
351 set_row_height(font_size);
352 set_grid_size(font_size);
353 prefs->setInt(_prefs + "/preview-size", font_size);
354 // resize
355 filter();
356 });
357
358 auto show_names = &get_widget<Gtk::CheckButton>(_builder, "show-font-name");
359 auto set_show_names = [=, this](bool show) {
360 font_renderer->_show_font_name = show;
361 prefs->setBool(_prefs + "/show-font-names", show);
362 set_row_height(font_renderer->_font_size);
363 _font_list.set_grid_lines(show ? Gtk::TreeView::GridLines::HORIZONTAL : Gtk::TreeView::GridLines::NONE);
364 // resize
365 filter();
366 };
367 auto show = prefs->getBool(_prefs + "/show-font-names", true);
368 set_show_names(show);
369 show_names->set_active(show);
370 show_names->signal_toggled().connect([=](){
371 bool show = show_names->get_active();
372 set_show_names(show);
373 });
374
375 // sample text to show for each font; empty to show font name
376 auto sample = &get_widget<Gtk::Entry>(_builder, "sample-text");
377 auto sample_text = prefs->getString(_prefs + "/sample-text");
378 sample->set_text(sample_text);
379 font_renderer->_sample_text = sample_text;
380 sample->signal_changed().connect([=, this](){
381 auto text = sample->get_text();
382 font_renderer->_sample_text = text;
383 prefs->setString(_prefs + "/sample-text", text);
384 _font_list.queue_draw();
385 });
386 // sample text for grid
387 auto grid_sample = &get_widget<Gtk::Entry>(_builder, "grid-sample");
388 auto sample_grid_text = prefs->getString(_prefs + "/grid-text", "Aa");
389 grid_sample->set_text(sample_grid_text);
390 grid_renderer->_sample_text = sample_grid_text;
391 grid_sample->signal_changed().connect([=, this](){
392 auto text = grid_sample->get_text();
393 grid_renderer->_sample_text = text.empty() ? "?" : text;
394 prefs->setString(_prefs + "/grid-text", text);
395 _font_grid.queue_draw();
396 });
397
398 // Populate samples submenu from stringlist
399 auto samples_submenu = get_object<Gio::Menu>(_builder, "samples-submenu");
400 auto samples_stringlist = get_object<Gtk::StringList>(_builder, "samples-stringlist");
401
402 auto truncate = [] (Glib::ustring const &text) {
403 constexpr int N = 30; // limit number of characters in label
404 if (text.length() <= N) {
405 return text;
406 }
407
408 auto substr = text.substr(0, N);
409 auto pos = substr.rfind(' ');
410 // do we have a space somewhere close to the limit of N chars?
411 if (pos != Glib::ustring::npos && pos > N - N / 4) {
412 // if so, truncate at space
413 substr = substr.substr(0, pos);
414 }
415 substr += "\u2026"; // add ellipsis to truncated content
416 return substr;
417 };
418
419 for (int i = 0, n_items = samples_stringlist->get_n_items(); i < n_items; i++) {
420 auto text = samples_stringlist->get_string(i);
421 auto menu_item = Gio::MenuItem::create(truncate(text), "");
422 menu_item->set_action_and_target("win.set-sample", Glib::Variant<Glib::ustring>::create(text));
423 samples_submenu->append_item(menu_item);
424 }
425
426 // Hook up action used by samples submenu
427 auto action_group = Gio::SimpleActionGroup::create();
428 action_group->add_action_with_parameter("set-sample", Glib::Variant<Glib::ustring>::variant_type(), [=] (Glib::VariantBase const &param) {
429 auto param_str = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(param).get();
430 sample->set_text(param_str);
431 });
432 insert_action_group("win", action_group);
433
434 _text_column.set_sizing(Gtk::TreeViewColumn::Sizing::FIXED);
435 _text_column.pack_start(*_cell_icon_renderer, false);
436 _text_column.add_attribute(ico->property_icon_name(), g_column_model.icon_name);
437 _cell_renderer->property_ellipsize() = Pango::EllipsizeMode::END;
438 _text_column.pack_start(*_cell_renderer, true);
439 _text_column.set_fixed_width(100); // limit minimal width to keep entire dialog narrow; column can still grow
440 _text_column.set_cell_data_func(*_cell_renderer, [=](Gtk::CellRenderer* r, const Gtk::TreeModel::const_iterator& it) {
441 Gtk::TreeModel::ConstRow row = *it;
442 get_cell_data_func(r, row);
443 });
444 _text_column.set_expand();
445 _font_list.append_column(_text_column);
446
447 _font_list.set_fixed_height_mode();
448 set_row_height(font_renderer->_font_size);
449 //todo: restore grid size and view?
450 _font_list.set_search_column(-1); // disable, we have a separate search/filter
451 _font_list.set_enable_search(false);
452 _font_list.set_model(_font_list_store);
453
454 _font_grid.pack_start(*_grid_renderer);
455 grid_renderer->set_fixed_height_from_font(-1);
456 set_grid_size(grid_renderer->_font_size);
457 // grid_renderer->_sample_text = "Aa";
458 _font_grid.set_cell_data_func(*grid_renderer, [=](const Gtk::TreeModel::const_iterator& it) {
459 Gtk::TreeModel::ConstRow row = *it;
460 get_cell_data_func(grid_renderer, row);
461 });
462 // show font name in a grid tooltip
463 _font_grid.signal_query_tooltip().connect([this](int x, int y, bool kbd, const Glib::RefPtr<Gtk::Tooltip>& tt){
464 Gtk::TreeModel::iterator iter;
465 Glib::ustring name;
466 if (_font_grid.get_tooltip_context_iter(x, y, kbd, iter)) {
467 const auto& font = iter->get_value(g_column_model.font);
468 name = get_font_name(iter);
469 tt->set_text(name);
470 }
471 return !name.empty();
472 }, true);
473 _font_grid.property_has_tooltip() = true;
474
475 auto font_selected = [this](const FontInfo& font) {
476 if (_update.pending()) return;
477
478 auto scoped = _update.block();
479 auto vars = font.variations;
480 if (vars.empty() && font.variable_font) {
481 vars = Inkscape::get_inkscape_fontspec(font.ff, font.face, font.variations);
482 }
484 _signal_changed.emit();
485 };
486
487 _selection_changed = _font_grid.signal_selection_changed().connect([font_selected, this](){
488 auto sel = _font_grid.get_selected_items();
489 if (sel.size() == 1) {
490 auto it = _font_list_store->get_iter(sel.front());
491 const Inkscape::FontInfo& font = (*it)[g_column_model.font];
492 font_selected(font);
493 }
494 });
495
496 auto show_grid = &get_widget<Gtk::ToggleButton>(_builder, "view-grid");
497 auto show_list = &get_widget<Gtk::ToggleButton>(_builder, "view-list");
498 auto set_list_view_mode = [prefs, this](bool show_list) {
499 auto& list = get_widget<Gtk::ScrolledWindow>(_builder, "list");
500 auto& grid = get_widget<Gtk::ScrolledWindow>(_builder, "grid");
501 // TODO: sync selection between font widgets
502 if (show_list) {
503 grid.set_visible(false);
504 _font_grid.unset_model();
505 list.set_visible();
506 }
507 else {
508 list.set_visible(false);
509 _font_grid.set_model(_font_list_store);
510 grid.set_visible();
511 }
512 _view_mode_list = show_list;
513 get_widget<Gtk::Entry>(_builder, "grid-sample").set_visible(!show_list);
514 prefs->setBool(_prefs + "/list-view-mode", show_list);
515 };
516 auto list_mode = prefs->getBool(_prefs + "/list-view-mode", true);
517 if (list_mode) show_list->set_active(); else show_grid->set_active();
518 set_list_view_mode(list_mode);
519 show_list->signal_toggled().connect([=] { set_list_view_mode(true); });
520 show_grid->signal_toggled().connect([=] { set_list_view_mode(false); });
521
522 _fonts.clear();
523 _initializing = 0;
524 _info_box.set_visible(false);
525 _progress_box.show();
526
527 auto prepare_tags = [this](){
528 // prepare dynamic tags
529 for (auto&& f : _fonts) {
530 auto kind = f.family_kind >> 8;
531 if (kind == 10) {
532 _font_tags.tag_font(f.face, "script");
533 }
534 else if (kind >= 1 && kind <= 5) {
535 _font_tags.tag_font(f.face, "serif");
536 }
537 else if (kind == 8) {
538 _font_tags.tag_font(f.face, "sans");
539 }
540 else if (kind == 12) {
541 _font_tags.tag_font(f.face, "symbols");
542 }
543
544 if (f.monospaced) {
545 _font_tags.tag_font(f.face, "monospace");
546 }
547 if (f.variable_font) {
548 _font_tags.tag_font(f.face, "variable");
549 }
550 if (f.oblique) {
551 _font_tags.tag_font(f.face, "oblique");
552 }
553 }
554 };
555
557 if (auto r = Async::Msg::get_result(msg)) {
558 _fonts = **r;
560 prepare_tags();
561 filter();
562 }
563 else if (auto p = Async::Msg::get_progress(msg)) {
564 // show progress
565 _info_box.set_visible(false);
566 _progress_box.set_visible();
567 auto& progress = get_widget<Gtk::ProgressBar>(_builder, "init-progress");
568 progress.set_fraction(std::get<double>(*p));
569 progress.set_text(std::get<Glib::ustring>(*p));
570 auto&& family = std::get<std::vector<FontInfo>>(*p);
571 _fonts.insert(_fonts.end(), family.begin(), family.end());
572 auto delta = _fonts.size() - _initializing;
573 // refresh fonts; at first more frequently, every new 100, but then more slowly, as it gets costly
574 if (delta > 500 || (_fonts.size() < 500 && delta > 100)) {
575 _initializing = _fonts.size();
577 filter();
578 }
579 }
580 else if (Async::Msg::is_finished(msg)) {
581 // hide progress
582 _progress_box.set_visible(false);
583 _info_box.set_visible();
584 }
585 });
586
587 _font_size_scale.get_adjustment()->set_lower(0);
588 _font_size_scale.get_adjustment()->set_upper(g_font_sizes.size() - 1);
589 _font_size_scale.signal_value_changed().connect([this](){
590 if (_update.pending()) return;
591
592 auto scoped = _update.block();
593 auto size = index_to_font_size(_font_size_scale.get_value());
594 _font_size.get_entry()->set_text(std::to_string(size));
595 _signal_changed.emit();
596 });
597
598 _font_size.signal_changed().connect([this](){
599 if (_update.pending()) return;
600
601 auto scoped = _update.block();
602 auto text = _font_size.get_active_text();
603 if (!text.empty()) {
604 auto size = ::atof(text.c_str());
605 if (size > 0) {
607 _signal_changed.emit();
608 }
609 }
610 });
611 _font_size.set_active_text("10");
612 _font_size.get_entry()->set_max_width_chars(6);
613
615
616 _font_list.get_selection()->signal_changed().connect([=, this](){
617 if (auto iter = _font_list.get_selection()->get_selected()) {
618 const Inkscape::FontInfo& font = (*iter)[g_column_model.font];
619 font_selected(font);
620 }
621 }, false);
622
623 // double-click:
624 _font_list.signal_row_activated().connect([this](const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn*){
625 if (!_update.pending()) {
626 auto scoped = _update.block();
627 _signal_apply.emit();
628 }
629 });
630
631 _font_tags.get_signal_tag_changed().connect([this](const FontTag* ftag, bool selected){
632 sync_font_tag(ftag, selected);
633 });
634
635 auto& filter_popover = get_widget<Gtk::Popover>(_builder, "filter-popover");
636 filter_popover.signal_show().connect([this](){
637 // update tag checkboxes
640 }, false);
641
645 filter();
646 });
650 filter();
651 });
652}
653
654void FontList::sort_fonts(Inkscape::FontOrder order) {
656
657 if (const char* icon = get_sort_icon(order)) {
658 auto& sort = get_widget<Gtk::Image>(_builder, "sort-icon");
659 sort.set_from_icon_name(icon);
660 }
661
662 filter();
663}
664
665bool FontList::select_font(const Glib::ustring& fontspec) {
666 bool found = false;
667
668 _font_list_store->foreach([&](const Gtk::TreeModel::Path& path, const Gtk::TreeModel::iterator& iter){
669 const auto& row = *iter;
670 const auto& font = row.get_value(g_column_model.font);
671 if (!font.ff) {
672 auto spec = row.get_value(g_column_model.alt_fontspec);
673 if (spec == fontspec) {
674 _font_list.get_selection()->select(row.get_iter());
675 _font_grid.select_path(path);
676 scroll_to_row(path);
677 found = true;
678 return true; // stop
679 }
680 }
681 else {
682 const auto& font = row.get_value(g_column_model.font);
683 auto spec = Inkscape::get_inkscape_fontspec(font.ff, font.face, font.variations);
684 if (spec == fontspec) {
685 _font_list.get_selection()->select(row.get_iter());
686 _font_grid.select_path(path);
687 scroll_to_row(path);
688 found = true;
689 return true; // stop
690 }
691 }
692 return false; // continue
693 });
694
695 return found;
696}
697
698void FontList::filter() {
699 auto scoped = _update.block();
700 // todo: save selection
701 Inkscape::FontInfo selected;
702 auto it = _font_list.get_selection()->get_selected();
703 if (it) {
704 selected = it->get_value(g_column_model.font);
705 }
706
707 auto& search = get_widget<Gtk::SearchEntry2>(_builder, "font-search");
708 // Not used: extra search terms; use collections instead
709 // auto& oblique = get_widget<Gtk::CheckButton>(_builder, "id-oblique");
710 // auto& monospaced = get_widget<Gtk::CheckButton>(_builder, "id-monospaced");
711 // auto& others = get_widget<Gtk::CheckButton>(_builder, "id-other");
712 Show params;
713 // Not used
714 // params.monospaced = monospaced.get_active();
715 // params.oblique = oblique.get_active();
716 // params.others = others.get_active();
717 populate_font_store(search.get_text(), params);
718
719 if (!_current_fspec.empty()) {
720 add_font(_current_fspec, false);
721 }
722
723 if (it) {
724 // reselect if that font is still available
725 //TODO
726 }
727}
728
729Glib::ustring get_font_icon(const FontInfo& font, bool missing_font) {
730 if (missing_font) {
731 return "missing-element-symbolic";
732 }
733 else if (font.variable_font) {
734 return ""; // TODO: add icon for variable fonts?
735 }
736 else if (font.synthetic) {
737 return "generic-font-symbolic";
738 }
739 return Glib::ustring();
740}
741
742// add fonts to the font store taking filtring params into account
743void FontList::populate_font_store(Glib::ustring text, const Show& params) {
744 auto filter = text.lowercase();
745
746 auto active_categories = _font_tags.get_selected_tags();
747 auto apply_categories = !active_categories.empty();
748
750 auto font_collections = fc->get_selected_collections();
751 auto apply_font_collections = !font_collections.empty();
752
753 _font_list.set_visible(false); // hide tree view temporarily to speed up rebuild
754 _font_grid.set_visible(false);
755 _font_list_store->freeze_notify();
756 _font_list_store->clear();
757 _extra_fonts = 0;
758
759 for (auto&& f : _fonts) {
760 bool filter_in = false;
761
762 if (!text.empty()) {
763 auto name1 = get_full_name(f);
764 if (name1.lowercase().find(filter) == Glib::ustring::npos) continue;
765 }
766
767 if (apply_categories || apply_font_collections) {
768 if (apply_categories) {
769 auto&& set = _font_tags.get_font_tags(f.face);
770 for (auto&& ftag : active_categories) {
771 if (set.contains(ftag.tag)) {
772 filter_in = true;
773 break;
774 }
775 }
776 }
777
778 if (!filter_in && apply_font_collections) {
779 for (auto& col : font_collections) {
780 if (fc->is_font_in_collection(col, f.ff->get_name())) {
781 filter_in = true;
782 break;
783 }
784 }
785 }
786
787 if (!filter_in) continue;
788 }
789
790 Gtk::TreeModel::iterator treeModelIter = _font_list_store->append();
791 auto& row = *treeModelIter;
792 row[g_column_model.font] = f;
793 row[g_column_model.alt_fontspec] = Glib::ustring();
794 row[g_column_model.icon_name] = get_font_icon(f, false);
795 }
796
797 _font_list_store->thaw_notify();
798 _font_list.set_visible(); // restore visibility
799 _font_grid.set_visible();
800
802}
803
804void FontList::update_font_count() {
805 auto& font_count = get_widget<Gtk::Label>(_builder, "font-count");
806 auto count = _font_list_store->children().size();
807 auto total = _fonts.size();
808 // count could be larger than total if we insert "missing" font(s)
809 auto label = count >= total ? C_("N-of-fonts", "All fonts") : Glib::ustring::format(count, ' ', C_("N-of-fonts", "of"), ' ', total, ' ', C_("N-of-fonts", "fonts"));
810 font_count.set_text(label);
811}
812
813double FontList::get_fontsize() const {
814 auto text = _font_size.get_entry()->get_text();
815 if (!text.empty()) {
816 auto size = ::atof(text.c_str());
817 if (size > 0) return size;
818 }
819 return _current_fsize;
820}
821
822Gtk::TreeModel::iterator FontList::get_selected_font() const {
823 if (_view_mode_list) {
824 return _font_list.get_selection()->get_selected();
825 }
826 else {
827 auto sel = _font_grid.get_selected_items();
828 if (sel.size() == 1) return _font_list_store->get_iter(sel.front());
829 }
830 return {};
831}
832
833Glib::ustring FontList::get_fontspec() const {
834 if (auto iter = get_selected_font()) { //_font_list.get_selection()->get_selected()) {
835 const auto& font = iter->get_value(g_column_model.font);
836 // auto font_class = iter->get_value(g_column_model.font_class);
837 if (font.ff) {
838 auto variations = _font_variations.get_pango_string(true);
839 auto fspec = Inkscape::get_inkscape_fontspec(font.ff, font.face, variations);
840 return fspec;
841 }
842 else {
843 // missing fonts don't have known variation that we could tweak,
844 // so ignore _font_variations UI and simply return alt_fontspec
845 auto&& name = iter->get_value(g_column_model.alt_fontspec);
846 return name;
847 }
848 }
849 return "sans-serif"; // no selection
850}
851
852void FontList::set_current_font(const Glib::ustring& family, const Glib::ustring& face) {
853 if (_update.pending()) return;
854
855 auto scoped = _update.block();
856
857 auto fontspec = Inkscape::get_fontspec(family, face);
858 if (fontspec == _current_fspec) {
859 auto fspec = get_fontspec_without_variants(fontspec);
860 select_font(fspec);
861 return;
862 }
863 _current_fspec = fontspec;
864
865 if (!fontspec.empty()) {
866 _font_variations.update(fontspec);
867 add_font(fontspec, true);
868 }
869}
870
871void FontList::set_current_size(double size) {
873 if (_update.pending()) return;
874
875 auto scoped = _update.block();
877 os.precision(3);
878 os << size;
880 _font_size.get_entry()->set_text(os.str());
881}
882
883void FontList::add_font(const Glib::ustring& fontspec, bool select) {
884 bool found = select_font(fontspec); // found in the tree view?
885 if (found) return;
886
887 auto it = std::find_if(begin(_fonts), end(_fonts), [&](const FontInfo& f){
888 return Inkscape::get_inkscape_fontspec(f.ff, f.face, f.variations) == fontspec;
889 });
890
891 // fonts with variations will not be found, we need to remove " @ axis=value" part
892 auto fspec = get_fontspec_without_variants(fontspec);
893 // auto at = fontspec.rfind('@');
894 if (it == end(_fonts) && fspec != fontspec) {
895 // try to match existing font
896 it = std::find_if(begin(_fonts), end(_fonts), [&](const FontInfo& f){
897 return Inkscape::get_inkscape_fontspec(f.ff, f.face, f.variations) == fspec;
898 });
899 if (it != end(_fonts)) {
900 bool found = select_font(fspec); // found in the tree view?
901 if (found) return;
902 }
903 }
904
905 if (it != end(_fonts)) {
906 // font found in the "all fonts" vector, but
907 // this font is filtered out; add it temporarily to the tree list
908 Gtk::TreeModel::iterator iter = _font_list_store->prepend();
909 auto& row = *iter;
910 row[g_column_model.font] = *it;
911 row[g_column_model.injected] = true;
912 row[g_column_model.alt_fontspec] = Glib::ustring();
913 row[g_column_model.icon_name] = get_font_icon(*it, false);
914
915 if (select) {
916 _font_list.get_selection()->select(row.get_iter());
917 auto path = _font_list_store->get_path(iter);
918 scroll_to_row(path);
919 }
920
921 ++_extra_fonts; // extra font entry inserted
922 }
923 else {
924 bool missing_font = true;
925 Inkscape::FontInfo subst;
926
927 auto desc = Pango::FontDescription(fontspec);
928 auto vars = desc.get_variations();
929 if (!vars.empty()) {
930 // font with variantions; check if we have matching family
931 subst.variations = vars;
932
933 auto family = desc.get_family();
934 it = std::find_if(begin(_fonts), end(_fonts), [&](const FontInfo& f){
935 return f.ff->get_name() == family;
936 });
937 if (it != end(_fonts)) {
938 missing_font = false;
939 subst.ff = it->ff;
940 }
941 }
942
943 auto&& children = _font_list_store->children();
944 Gtk::TreeModel::iterator iter;
945 if (!children.empty() && children[0][g_column_model.injected]) {
946 // reuse "injected font" entry
947 iter = children[0].get_iter();
948 }
949 else {
950 // font not found; insert a placeholder to show injected font: font is used in a document,
951 // but either not available in the system (it's missing) or it is a variant of the variable font
952 iter = _font_list_store->prepend();
953 }
954 auto& row = *iter;
955 row[g_column_model.font] = subst;
956 row[g_column_model.injected] = true;
957 row[g_column_model.alt_fontspec] = fontspec;// TODO fname?
958 row[g_column_model.icon_name] = get_font_icon(subst, missing_font);
959
960 if (select) {
961 _font_list.get_selection()->select(iter);
962 auto path = _font_list_store->get_path(iter);
963 scroll_to_row(path);
964 }
965
966 ++_extra_fonts; // extra font entry for a missing font added
967 }
969}
970
971Gtk::Box* FontList::create_pill_box(const Glib::ustring& display_name, const Glib::ustring& tag, bool tags) {
972 auto box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
973 auto text = Gtk::make_managed<Gtk::Label>(display_name);
974 text->set_ellipsize(Pango::EllipsizeMode::END);
975 text->set_max_width_chars(10);
976 text->set_tooltip_text(display_name);
977 auto close = Gtk::make_managed<Gtk::Button>();
978 close->set_has_frame(false);
979 close->set_image_from_icon_name("close-button-symbolic");
980 close->set_valign(Gtk::Align::CENTER);
981 if (tags) {
982 close->signal_clicked().connect([=, this](){
983 // remove category from current filter
984 update_categories(tag, false);
985 });
986 }
987 else {
988 close->signal_clicked().connect([=](){
989 // remove collection from current filter
991 });
992 }
993 box->get_style_context()->add_class("tag-box");
994 box->append(*text);
995 box->append(*close);
996 box->set_valign(Gtk::Align::CENTER);
997 return box;
998}
999
1000// show selected font categories in the filter bar
1001void FontList::update_filterbar() {
1002 // brute force approach at first
1003 for (auto&& btn : _tag_box.get_children()) {
1004 _tag_box.remove(*btn);
1005 }
1006
1007 for (auto&& ftag : _font_tags.get_selected_tags()) {
1008 auto pill = create_pill_box(ftag.display_name, ftag.tag, true);
1009 _tag_box.append(*pill);
1010 }
1011
1012 for (auto&& collection : Inkscape::FontCollections::get()->get_selected_collections()) {
1013 auto pill = create_pill_box(collection, collection, false);
1014 _tag_box.append(*pill);
1015 }
1016}
1017
1018void FontList::update_categories(const std::string& tag, bool select) {
1019 if (_update.pending()) return;
1020
1021 auto scoped = _update.block();
1022
1023 if (!_font_tags.select_tag(tag, select)) return;
1024
1025 // update UI
1027
1028 // apply filter
1029 filter();
1030}
1031
1032void FontList::add_categories() {
1033 for (auto row : _tag_list.get_children()) {
1034 if (row) _tag_list.remove(*row);
1035 }
1036
1037 auto add_row = [this](Gtk::Widget* w){
1038 auto row = Gtk::make_managed<Gtk::ListBoxRow>();
1039 row->set_can_focus(false);
1040 row->set_child(*w);
1041 row->set_sensitive(w->get_sensitive());
1042 _tag_list.append(*row);
1043 };
1044
1045 for (auto& tag : _font_tags.get_tags()) {
1046 auto btn = Gtk::make_managed<Gtk::CheckButton>("");
1047 // automatic collections in italic
1048 auto& label = *Gtk::make_managed<Gtk::Label>();
1049 label.set_markup("<i>" + tag.display_name + "</i>");
1050 btn->set_child(label);
1051 btn->set_active(_font_tags.is_tag_selected(tag.tag));
1052 btn->signal_toggled().connect([=, this](){
1053 // toggle font category
1054 update_categories(tag.tag, btn->get_active());
1055 });
1056 add_row(btn);
1057 }
1058
1059 // insert user collections
1061 auto font_collections = fc->get_collections();
1062 if (!font_collections.empty()) {
1063 auto sep = Gtk::make_managed<Gtk::Separator>();
1064 sep->set_margin_bottom(3);
1065 sep->set_margin_top(3);
1066 sep->set_sensitive(false);
1067 add_row(sep);
1068 }
1069 for (auto& col : font_collections) {
1070 auto btn = Gtk::make_managed<Gtk::CheckButton>(col);
1071 btn->set_active(fc->is_collection_selected(col));
1072 btn->signal_toggled().connect([=](){
1073 // toggle font system collection
1074 fc->update_selected_collections(col);
1075 });
1076 add_row(btn);
1077 }
1078}
1079
1080void FontList::sync_font_tag(const FontTag* ftag, bool selected) {
1081 if (!ftag) {
1082 // many/all tags changed
1085 }
1086 //todo as needed
1087}
1088
1089void FontList::scroll_to_row(Gtk::TreePath path) {
1090 if (_view_mode_list) {
1091 // delay scroll request to let widget layout complete (due to hiding or showing variable font widgets);
1092 // keep track of connection so we can disconnect in a destructor if it is still pending at that point
1093 _scroll = Glib::signal_timeout().connect([=, this](){
1094 _font_list.scroll_to_row(path);
1095 return false; // <- false means disconnect
1096 }, 50, Glib::PRIORITY_LOW);
1097 // fudge factor of 50ms; ideally wait for layout pass to complete before scrolling to the row
1098 }
1099 else {
1100 // scroll grid
1101 //todo
1102 }
1103}
1104
1105} // namespaces
Gtk builder utilities.
int margin
Definition canvas.cpp:166
A thin wrapper around std::ostringstream, but writing floating point numbers in the format required b...
std::streamsize precision() const
static FontCollections * get()
sigc::connection connect_update(sigc::slot< void()> slot)
const std::set< Glib::ustring > & get_selected_collections() const
sigc::connection connect_selection_update(sigc::slot< void()> slot)
void update_selected_collections(const Glib::ustring &collection_name)
Async::Msg::Message< FontsPayload, double, Glib::ustring, std::vector< FontInfo > > MessageType
sigc::scoped_connection connect_to_fonts(std::function< void(const MessageType &)> fn)
bool is_tag_selected(const std::string &tag_id) const
Definition font-tags.cpp:61
sigc::signal< void(const FontTag *, bool)> & get_signal_tag_changed()
bool select_tag(const std::string &tag_id, bool selected)
Definition font-tags.cpp:75
const std::vector< FontTag > & get_selected_tags() const
Definition font-tags.cpp:49
std::set< std::string > get_font_tags(Glib::RefPtr< Pango::FontFace > &face) const
Definition font-tags.cpp:35
void tag_font(Glib::RefPtr< Pango::FontFace > &face, std::string tag)
Definition font-tags.cpp:43
std::vector< FontTag > get_tags() const
Definition font-tags.cpp:27
static Preferences * get()
Access the singleton Preferences object.
void sync_font_tag(const FontTag *ftag, bool selected)
std::unique_ptr< Gtk::CellRenderer > _cell_icon_renderer
Definition font-list.h:101
sigc::scoped_connection _scroll
Definition font-list.h:110
sigc::scoped_connection _font_collections_selection
Definition font-list.h:116
Gtk::TreeViewColumn _text_column
Definition font-list.h:88
Gtk::Widget * box() override
Definition font-list.h:54
std::vector< FontInfo > _fonts
Definition font-list.h:95
bool select_font(const Glib::ustring &fontspec)
Gtk::Box * create_pill_box(const Glib::ustring &display_name, const Glib::ustring &tag, bool tags)
void update_categories(const std::string &tag, bool select)
Inkscape::FontOrder _order
Definition font-list.h:96
Gtk::TreeModel::iterator get_selected_font() const
Glib::RefPtr< Gtk::Builder > _builder
Definition font-list.h:85
void scroll_to_row(Gtk::TreePath path)
sigc::scoped_connection _selection_changed
Definition font-list.h:90
Glib::RefPtr< Gtk::ListStore > _font_list_store
Definition font-list.h:91
sigc::scoped_connection _font_collections_update
Definition font-list.h:115
Inkscape::FontTags & _font_tags
Definition font-list.h:108
sigc::signal< void()> _signal_changed
Definition font-list.h:83
std::unique_ptr< Gtk::CellRendererText > _cell_renderer
Definition font-list.h:100
void populate_font_store(Glib::ustring text, const Show &params)
void add_font(const Glib::ustring &fontspec, bool select)
std::unique_ptr< Gtk::CellRendererText > _grid_renderer
Definition font-list.h:102
sigc::scoped_connection _font_stream
Definition font-list.h:113
FontVariations _font_variations
Definition font-list.h:109
void sort_fonts(Inkscape::FontOrder order)
Gtk::ComboBoxText & _font_size
Definition font-list.h:98
Glib::RefPtr< Gtk::SizeGroup > get_size_group(int index)
Glib::ustring get_pango_string(bool include_defaults=false) const
void update(const Glib::ustring &font_spec)
Update GUI.
sigc::connection connectChanged(sigc::slot< void()> slot)
Let others know that user has changed GUI settings.
scoped_block block()
const double w
Definition conic-4.cpp:19
TODO: insert short description here.
Glib::ustring msg
const unsigned order
TODO: insert short description here.
The data describing a single loaded font.
Font browser and selector.
Util::TreeifyResult const & _tree
Gtk::Image * sp_get_icon_image(Glib::ustring const &icon_name, int size)
Icon Loader.
SPItem * item
Glib::ustring label
Definition desktop.h:50
const R * get_result(const Msg::Message< R, T... > &msg)
bool is_finished(const Msg::Message< R, T... > &msg)
const std::tuple< T... > * get_progress(const Msg::Message< R, T... > &msg)
Custom widgets.
Definition desktop.h:126
const char * get_sort_icon(Inkscape::FontOrder order)
Glib::ustring get_alt_name(const Glib::ustring &fontspec)
CellFontRenderer & get_renderer(Gtk::CellRenderer &renderer)
Glib::ustring get_font_icon(const FontInfo &font, bool missing_font)
void get_cell_data_func(Gtk::CellRenderer *cell_renderer, Gtk::TreeModel::ConstRow row)
static int font_size_to_index(double size)
Definition font-list.cpp:95
void set_grid_cell_size(Gtk::CellRendererText *renderer, int font_size_percent)
Glib::ustring get_full_name(const Inkscape::FontInfo &font_info)
Glib::ustring get_font_name(Gtk::TreeIter< Gtk::TreeRow > &iter)
static int index_to_font_size(int index)
Definition font-list.cpp:83
static std::array< int, 38 > g_font_sizes
Definition font-list.cpp:77
FontListColumnModel g_column_model
Definition font-list.cpp:74
W & get_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id)
Glib::RefPtr< Gtk::Builder > create_builder(const char *filename)
Helper class to stream background task notifications as a series of messages.
Glib::ustring get_inkscape_fontspec(const Glib::RefPtr< Pango::FontFamily > &ff, const Glib::RefPtr< Pango::FontFace > &face, const Glib::ustring &variations)
Glib::ustring get_fontspec_without_variants(const Glib::ustring &fontspec)
static void append(std::vector< T > &target, std::vector< T > &&source)
void sort_fonts(std::vector< FontInfo > &fonts, FontOrder order, bool sans_first)
Pango::FontDescription get_font_description(const Glib::RefPtr< Pango::FontFamily > &ff, const Glib::RefPtr< Pango::FontFace > &face)
Glib::ustring get_fontspec(const Glib::ustring &family, const Glib::ustring &face, const Glib::ustring &variations)
Glib::ustring get_full_font_name(Glib::RefPtr< Pango::FontFamily > ff, Glib::RefPtr< Pango::FontFace > face)
STL namespace.
A replacement for GTK3ʼs Gtk::MenuItem, as removed in GTK4.
A replacement for GTK3ʼs Gtk::Menu, as removed in GTK4.
Singleton class to access the preferences file in a convenient way.
size_t N
Glib::RefPtr< Pango::FontFace > face
Glib::ustring variations
Glib::RefPtr< Pango::FontFamily > ff
int delta
int index
Glib::ustring name
Definition toolbars.cpp:55