diff --git a/share/icons/hicolor/symbolic/actions/sort-by-family-symbolic.svg b/share/icons/hicolor/symbolic/actions/sort-by-family-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..7441b9e6f1c14c8f0aab7b0e1eb1daf2f5b12c07 --- /dev/null +++ b/share/icons/hicolor/symbolic/actions/sort-by-family-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/share/ui/font-list.glade b/share/ui/font-list.glade index ec38dcf4ffe1f90a857ff6c51fd64e4c23e7798f..013963ec5441c3294fc4d1fcc4b5a487b1b7fc2c 100644 --- a/share/ui/font-list.glade +++ b/share/ui/font-list.glade @@ -1,6 +1,6 @@ - + 20 @@ -13,6 +13,12 @@ 10 20 + + 100 + 800.000000 + 10 + 20 + AbcdEfgh1234 @@ -37,6 +43,8 @@ + -1 + 200 1 1 1 @@ -83,7 +91,9 @@ - 180 + true + true + 200 240 True True @@ -140,43 +150,6 @@ True True 3 - - - True - True - True - never - - - True - False - False - False - horizontal - - - - - - - 0 - 4 - 2 - - - - - - - - 0 - 3 - - - 5 @@ -187,22 +160,46 @@ 0 0 + + + 4 + 0 + + + Show fonts in a list + false + layout-list + + + + + Show fonts in a grid + view-list + false + layout-grid + + + + - Font list sorting + Font sorting sort-icon - false + true True False True center False + Font filtering - false + true font_collections True False @@ -210,30 +207,37 @@ center False filter-popover + + true + true 0 - 12 - Character viewer + 0 + Open character viewer false charmap True False True - center + end - + + options-popover 4 0 true - true + false end Dialog options True + False True gear False @@ -243,7 +247,7 @@ fill true - 3 + 2 True true -1 @@ -251,212 +255,6 @@ - - - false - 5 - 5 - 5 - 3 - 3 - 3 - - - start - Sample - - 0 - 0 - - - - - - start - Preview size - - 0 - 2 - - - - - - Show font name - True - start - True - - 1 - 1 - - - - - - True - True - 10 - - 1 - 0 - - - - - - True - False - True - samples-menu - - 2 - 0 - - - - - - start - View - - 0 - 3 - - - - - - True - True - adjustment-size - True - 0 - 0 - right - - 1 - 2 - 2 - - - - - - - - - - True - layout-list-symbolic - view-grid - - - - - 0 - True - 0 - layout-grid-symbolic - - - - - - - - True - 5 - 10 - 6 - Aa - - - - 1 - 3 - - - - - - - - - - - - - - 0 - 1 - - - - - - 5 - 1 - 4 - - - Font size - 0 - - - - - True - False - center - 4 - 2 - True - - 6 - 7 - 8 - 9 - 10 - 12 - 14 - 16 - 20 - 24 - 32 - 48 - 72 - 144 - - - - - - True - True - adjustment-font-size - 1 - - - - - 0 - 8 - - - - - - - 0 - 7 - - - 4 @@ -537,6 +335,38 @@ + + + + 0 + 5 + + + + + + true + true + True + true + true + never + + + true + false + + + + + 0 + 6 + + + True @@ -545,25 +375,91 @@ never always - + + FontGrid + true + false + 10 + 1 + item True 4 4 0 0 - 0 - 0 - 0 + - - 0 - 6 - + + 0 + 7 + + + + + + + 0 + 9 + + + + + + 5 + 1 + 4 + + + Font size + 0 + + + + + True + False + center + 4 + 2 + True + + 6 + 7 + 8 + 9 + 10 + 12 + 14 + 16 + 20 + 24 + 32 + 48 + 72 + 144 + + + + + + True + True + adjustment-font-size + 1 + + + + + 0 + 10 + @@ -573,14 +469,191 @@ 2 vertical - + + false + false + never + true + + 0 - 9 + 11 - + + + + + + true + 2 + 2 + 2 + 2 + 3 + 3 + + + start + Sample + + 0 + 0 + + + + + + start + Preview size + + 0 + 2 + + + + + + Show font name + True + start + True + + 1 + 1 + 2 + + + + + + start + True + False + True + samples-menu + + 2 + 0 + + + + + + true + true + + + true + true + fill + True + True + adjustment-size + True + 0 + 0 + right + + + + + true + true + fill + True + True + adjustment-grid-size + True + 0 + 0 + right + + + + 1 + 2 + 2 + + + + + + + + fill + true + 15 + True + true + -1 + + + + + 15 + fill + true + true + true + True + 5 + 10 + -1 + Aa + + + + 1 + 0 + + + + + + start + Font size + + 0 + 4 + + + + + + start + + + Top + + + + + font-size-top + Bottom + + + + 1 + 4 + 2 + + + + + + + 0 + 3 + + + + + diff --git a/share/ui/style.css b/share/ui/style.css index 202b0e34329084bf706912cf5f9dbf4f48feebc7..133657f4a47480a63cf8149129aa5dfeca244be1 100644 --- a/share/ui/style.css +++ b/share/ui/style.css @@ -1787,6 +1787,37 @@ padding:0 1px; border: none; } +#FontList indent { + /* Set indent to align listview items with and without leaf nodes (i.e. with and without expander icon) */ + -gtk-icon-size: 16px; +} + #FontList .font-list { min-height: 8em; } + +/* Allow scrollwindow to become smaller than it normally is to let font variations take up as little space as they need */ +#FontList .collapse-scrollbar scrollbar slider { + min-height: 0; +} + +.compact-menu-button box { + border-spacing: 0; +} + +/* Style used by font grid items */ +.round-rect-shade { + border-radius: 3px; + background: mix(@theme_base_color, @theme_fg_color, 0.04); + color: @theme_fg_color; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.30); +} +#FontGrid > child { + padding: 4px; + border-radius: 6px; +} +#FontGrid > child box.item-box { + padding: 2px 0; + margin: 0; + border-spacing: 0; +} diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index e5f5e3ecf05ddffc01409e227381d7eace160325..b485fe27ffddc804a618516c9d08e5fbce364b7e 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2133,9 +2133,9 @@ void InkscapePreferences::initPageUI() _page_windows.add_group_header( _("Text and Font dialog")); std::vector lister = { { _("List fonts and styles"), 0, _("List fonts and styles separately"), true }, - { _("Unified font browser (experimental)"), 1, _("Show all font styles in a single list") } + { _("Unified font browser"), 1, _("Show all font styles in a single list or tree view") } }; - _page_windows.add_line(true, _("Font selector"), *Gtk::make_managed(lister, "/options/font/browser"), "", "", false, reset_icon()); + _page_windows.add_line(true, _("Font selector"), *Gtk::make_managed(lister, "/options/font/browser"), "", "", true, reset_icon()); _page_windows.add_group_header( _("Miscellaneous")); diff --git a/src/ui/dialog/text-edit.cpp b/src/ui/dialog/text-edit.cpp index dfd780ffc9099fd522205597fc43b16b6c6166e4..7f41d81a8d8c6c2283dfc1dfeeff972e4ee18751 100644 --- a/src/ui/dialog/text-edit.cpp +++ b/src/ui/dialog/text-edit.cpp @@ -98,7 +98,7 @@ TextEdit::TextEdit() , _redo{"doc.redo"} { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - _use_browser = prefs->getInt("/options/font/browser", 0) != 0; + _use_browser = prefs->getInt("/options/font/browser", 1) != 0; font_list = _use_browser ? Inkscape::UI::Widget::FontList::create_font_list("/font-selector") : diff --git a/src/ui/widget/font-list.cpp b/src/ui/widget/font-list.cpp index ad45df8d120b73d78af34c00952230f7e0382bc1..cfebd3de16e28dd3fd6b6c56728eee28dd5b791a 100644 --- a/src/ui/widget/font-list.cpp +++ b/src/ui/widget/font-list.cpp @@ -2,10 +2,15 @@ #include "font-list.h" +#include +#include #include #include #include #include +#include +#include +#include #include #include #include @@ -14,71 +19,26 @@ #include #include #include -#include +#include +#include #include "preferences.h" #include "svg/css-ostringstream.h" #include "ui/builder-utils.h" #include "ui/icon-loader.h" +#include "ui/text_filter.h" +#include "ui/dialog/xml-tree.h" +#include "ui/widget/drop-down-list.h" #include "ui/widget/generic/popover-menu.h" #include "util/font-collections.h" using Inkscape::UI::create_builder; -namespace Inkscape::UI::Widget { - -std::unique_ptr FontList::create_font_list(Glib::ustring path) { - return std::make_unique(path); -} - -struct FontListColumnModel : public Gtk::TreeModelColumnRecord { - // font metadata for installed fonts only - Gtk::TreeModelColumn font; - // font's class - // Gtk::TreeModelColumn font_class; - Gtk::TreeModelColumn injected; - // fontspec for fonts that are not installed, but used in a document - Gtk::TreeModelColumn alt_fontspec; - // icon to show next to a font name (if any) - Gtk::TreeModelColumn icon_name; - - FontListColumnModel() { - add(alt_fontspec); - add(injected); - add(icon_name); - add(font); - } -}; - -FontListColumnModel g_column_model; // model for font list - -// list of font sizes for a slider; combo box has its own list -static std::array g_font_sizes = { - 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 24, 28, 32, 36, - 44, 56, 64, 72, 80, 96, 112, 128, 144, 160, 192, 224, 256, - 300, 350, 400, 450, 500, 550, 600, 700, 800 -}; - -static int index_to_font_size(int index) { - if (index < 0) { - return g_font_sizes.front(); - } - else if (index >= g_font_sizes.size()) { - return g_font_sizes.back(); - } - else { - return g_font_sizes[index]; - } -} - -static int font_size_to_index(double size) { - auto it = std::lower_bound(begin(g_font_sizes), end(g_font_sizes), static_cast(size)); - return std::distance(begin(g_font_sizes), it); -} +namespace { // construct font name from Pango face and family; // return font name as it is recorded in the font itself, as far as Pango allows it -Glib::ustring get_full_name(const Inkscape::FontInfo& font_info) { +Glib::ustring get_full_name(const FontInfo& font_info) { return get_full_font_name(font_info.ff, font_info.face); } @@ -93,160 +53,486 @@ Glib::ustring get_alt_name(const Glib::ustring& fontspec) { return fontspec; // use font spec verbatim } -class CellFontRenderer : public Gtk::CellRendererText { +Glib::ustring get_font_icon(const FontInfo& font, bool missing_font = false) { + if (missing_font) { + return "missing-element-symbolic"; + } + else if (font.variable_font) { + return ""; // add icon for variable fonts? + } + else if (font.synthetic) { + return "generic-font-symbolic"; + } + return {}; +} + +// Gio models require Glib object-based elements, +// we use this class to keep track of all fonts. +class FontElement : public Glib::Object { + enum class Type { + Font, // all fonts when not sorting by family + Family, // when sorting by family, this is a font that represents a family (a node in a tree) + Style // when sorting by family, this is one of the "style" fonts in a family (a leaf in a tree) + } _type; public: - CellFontRenderer() { } + static Glib::RefPtr create_font(const FontInfo& font) { + return Glib::make_refptr_for_instance(new FontElement({}, font, {}, Type::Font)); + } + + static Glib::RefPtr create_style(const FontInfo& font) { + return Glib::make_refptr_for_instance(new FontElement({}, font, {}, Type::Style)); + } + + static Glib::RefPtr create_family(const FontInfo& font, std::vector family) { + return Glib::make_refptr_for_instance(new FontElement(std::move(family), font, {}, Type::Family)); + } + + static Glib::RefPtr create_injected_font(const FontInfo& font, Glib::ustring alt_spec, bool is_missing) { + auto element = Glib::make_refptr_for_instance(new FontElement({}, font, std::move(alt_spec), Type::Font)); + element->_missing_font = is_missing; + element->_injected = true; + return element; + } + + static Glib::RefPtr create_placeholder() { + auto element = Glib::make_refptr_for_instance(new FontElement({}, {}, {}, Type::Font)); + element->_placeholder = true; + return element; + } + + Glib::ustring icon_name() const { + return get_font_icon(_font, _missing_font); + } + Glib::ustring icon_tooltip() const { + if (_missing_font) { + // this font is not installed / not available + return _("This font is missing"); + } + else if (_font.synthetic) { + // this is an alias for some fallback font (ex: 'Serif', 'Sans') and/or faux style + return _("This is an alias or synthetic font"); + } + return {}; + } + const FontInfo& font() const { + return _font; + } + const Glib::ustring& get_alt_spec() const { + return _alt_fontspec; + } + bool is_present() const { + // true if this font is installed + return _font.ff != nullptr; + } + bool is_family() const { + return _type == Type::Family; + } + bool is_injected() const { + return _injected; + } + void clear_injected() { + _injected = false; + _placeholder = true; + } + bool is_placeholder() const { + return _placeholder; + } + const std::vector& family() const { + return _family; + } + // get markup for a full font name + Glib::ustring get_full_name_markup() const { + auto name = get_font_name(Type::Font); + return Glib::ustring::compose("%1", name); + } + // get markup for a font name + Glib::ustring get_name_markup() const { + auto name = get_font_name(_type); + return Glib::ustring::compose("%1", name); + } + Glib::ustring get_name_tooltip() const { + return get_font_name(_type); + } + // get markup for a font badge - number of styles in a family + Glib::ustring get_badge_markup() const { + if ( _family.size() > 1) { + // count + return Glib::ustring::compose(" %1 ", _family.size()); + } + return {}; + } + // get markup to render font preview + Glib::ustring get_sample_markup(int font_size_percent, Glib::ustring sample_text) { + // if no sample text given, then render font name + auto text = Glib::Markup::escape_text(sample_text.empty() ? get_font_name(_type == Type::Family ? _type : Type::Font) : sample_text); + + auto& alt = _alt_fontspec; + auto font_desc = Glib::Markup::escape_text( + is_present() ? get_font_description(_font.ff, _font.face).to_string() : (alt.empty() ? "sans-serif" : alt)); + auto alpha = _missing_font ? "60%" : "100%"; + return Glib::ustring::format( + "", text, ""); + } + +private: + FontElement(std::vector family, const FontInfo& font, Glib::ustring alt, Type type): + _font(font), _family(std::move(family)), _type(type), _alt_fontspec(std::move(alt)) { + } - ~CellFontRenderer() override = default; + Glib::ustring get_font_name(Type type) const { + auto present = is_present(); + Glib::ustring name; + Glib::RefPtr empty; - Gtk::Widget* _tree = nullptr; - bool _show_font_name = true; - int _font_size = 200; // size in %, where 100 is normal UI font size - Glib::ustring _sample_text; // text to render (font preview) - Glib::ustring _name; + switch (type) { + case Type::Font: + // full font name: family + style + name = Glib::Markup::escape_text(present ? get_full_font_name(_font.ff, _font.face) : get_alt_name(_alt_fontspec)); + break; + case Type::Family: + // font family name only + name = Glib::Markup::escape_text(present ? get_full_font_name(_font.ff, empty) : get_alt_name(_alt_fontspec)); + break; + case Type::Style: + // font style only + name = Glib::Markup::escape_text(_font.face->get_name()); + break; + default: + assert(false); + break; + } + return name; + } - void snapshot_vfunc(Glib::RefPtr const &snapshot, Gtk::Widget &widget, Gdk::Rectangle const &background_area, Gdk::Rectangle const &cell_area, Gtk::CellRendererState flags) override; + FontInfo _font; + std::vector _family; + // empty element to keep space in a store + bool _placeholder = false; + // if font is not present in a system, then show "missing font" icon + bool _missing_font = false; + // injected element - always show it first, regardless of filters or sorting order + bool _injected = false; + // this is a fontspec of the missing font + Glib::ustring _alt_fontspec; }; -CellFontRenderer& get_renderer(Gtk::CellRenderer& renderer) { - return dynamic_cast(renderer); +// This function constructs a widget to show font info in a list view. +// List view is capable of being transformed into a tree-like display too. +void on_set_up_listitem(const Glib::RefPtr& list_item) { + // Each ListItem contains a TreeExpander, which contains a box. + auto expander = Gtk::make_managed(); + auto vbox = Gtk::make_managed(Gtk::Orientation::VERTICAL, 1); + auto upper = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 3); + auto lower = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 4); + vbox->set_margin_top(2); + vbox->set_overflow(Gtk::Overflow::HIDDEN); + auto sample = Gtk::make_managed(); + sample->set_ellipsize(Pango::EllipsizeMode::END); + sample->set_halign(Gtk::Align::START); + sample->set_margin_start(2); // extra space for fonts extend past the bbox + auto name = Gtk::make_managed(); + name->set_ellipsize(Pango::EllipsizeMode::END); + name->set_halign(Gtk::Align::START); + name->set_margin_start(4); // indent more than label due to optical alignment + lower->append(*name); + auto badge = Gtk::make_managed(); + badge->set_halign(Gtk::Align::CENTER); + badge->add_css_class("tag-box"); + lower->append(*badge); + vbox->append(*upper); + vbox->append(*lower); + auto icon = Gtk::make_managed(); + icon->set_pixel_size(16); + icon->set_valign(Gtk::Align::CENTER); + upper->append(*icon); + upper->append(*sample); + expander->set_child(*vbox); + list_item->set_child(*expander); + list_item->set_activatable(); + expander->set_indent_for_icon(); + expander->set_indent_for_depth(); + // expander can be collapsed with an action, but it cannot be executed in setup function: + // expander->activate_action("listitem.collapse"); } -Glib::ustring get_font_name(Gtk::TreeIter& iter) { - if (!iter) return Glib::ustring(); - - const Inkscape::FontInfo& font = (*iter)[g_column_model.font]; - auto present = !!font.ff; - Glib::ustring&& alt = (*iter)[g_column_model.alt_fontspec]; - auto name = Glib::Markup::escape_text(present ? get_full_name(font) : get_alt_name(alt)); - return name; +void on_bind_listitem(int sample_font_size, bool show_name, const Glib::ustring& sample_text, const Glib::RefPtr& list_item) { + auto row = std::dynamic_pointer_cast(list_item->get_item()); + if (!row) return; + // if only leaves in the tree can be selected: + // list_item->set_selectable(!row->is_expandable()); + auto element = std::dynamic_pointer_cast(row->get_item()); + if (!element) return; + auto expander = dynamic_cast(list_item->get_child()); + if (!expander) return; + + expander->set_list_row(row); + + auto vbox = dynamic_cast(expander->get_child()); + auto& upper = dynamic_cast(*vbox->get_first_child()); + auto& lower = dynamic_cast(*upper.get_next_sibling()); + auto& icon = dynamic_cast(*upper.get_first_child()); + auto& sample = dynamic_cast(*icon.get_next_sibling()); + auto& name = dynamic_cast(*lower.get_first_child()); + auto& badge = dynamic_cast(*name.get_next_sibling()); + + sample.set_markup(element->get_sample_markup(sample_font_size, sample_text)); + if (show_name) { + name.set_markup(element->get_name_markup()); + badge.set_markup(element->get_badge_markup()); + } + name.set_visible(show_name); + badge.set_visible(show_name); + icon.set_from_icon_name(element->icon_name()); + icon.set_tooltip_text(element->icon_tooltip()); + icon.set_visible(!element->icon_name().empty()); } -void get_cell_data_func(Gtk::CellRenderer* cell_renderer, Gtk::TreeModel::ConstRow row) { - auto& renderer = get_renderer(*cell_renderer); +// helper function that constructs tree model +Glib::RefPtr> create_element_model(const Glib::RefPtr& item = {}) { + auto element = std::dynamic_pointer_cast(item); + if (!element || element && element->family().size() < 2) { + // An item without children, i.e. a leaf in the tree. + return {}; + } - const Inkscape::FontInfo& font = row[g_column_model.font]; - // auto font_class = row[g_column_model.font_class]; - auto present = !!font.ff; - Glib::ustring&& icon_name = row[g_column_model.icon_name]; - Glib::ustring&& alt = row[g_column_model.alt_fontspec]; + auto result = Gio::ListStore::create(); + for (auto& f : element->family()) { + result->append(FontElement::create_style(f)); + } - auto name = Glib::Markup::escape_text(present ? get_full_name(font) : get_alt_name(alt)); - // if no sample text given, then render font name - auto text = Glib::Markup::escape_text(renderer._sample_text.empty() ? name : renderer._sample_text); + // no visible children? + if (result->get_n_items() == 0) { + return {}; + } - auto font_desc = Glib::Markup::escape_text( - present ? Inkscape::get_font_description(font.ff, font.face).to_string() : (alt.empty() ? "sans-serif" : alt)); - auto markup = Glib::ustring::format("", text, ""); + return result; +} + +// Grid view items have a simple shape: just two labels, +// top one for a font preview and bottom one for a font name. +// Then we style a box hosting them to make items distinct. +void on_set_up_griditem(const Glib::RefPtr& list_item) { + auto box = Gtk::make_managed(Gtk::Orientation::VERTICAL, 1); + auto sample = Gtk::make_managed(); + auto name = Gtk::make_managed(); + sample->set_halign(Gtk::Align::CENTER); + sample->set_valign(Gtk::Align::CENTER); + sample->set_expand(); + name->set_halign(Gtk::Align::CENTER); + name->set_hexpand(); + name->set_margin_start(1); + name->set_margin_end(1); + name->set_ellipsize(Pango::EllipsizeMode::END); + box->add_css_class("item-box"); + box->add_css_class("round-rect-shade"); + box->append(*sample); + box->append(*name); + list_item->set_child(*box); +} - if (renderer._show_font_name) { - renderer._name = name; +void on_bind_griditem(int sample_font_size, bool show_name, const Glib::ustring& sample_text, const Glib::RefPtr& list_item) { + auto element = std::dynamic_pointer_cast(list_item->get_item()); + if (!element) return; + auto box = dynamic_cast(list_item->get_child()); + if (!box) return; + auto label = dynamic_cast(box->get_first_child()); + auto name = dynamic_cast(label->get_next_sibling()); + + label->set_markup(element->get_sample_markup(sample_font_size, sample_text.empty() ? "Aa" : sample_text)); + if (show_name) { + name->set_markup(element->get_full_name_markup()); } + name->set_visible(show_name); + box->set_tooltip_text(element->get_name_tooltip()); +} - renderer.set_property("markup", markup); +void refilter(Glib::RefPtr& filter) { + Gtk::Filter* f = filter.get(); + gtk_filter_changed(f->gobj(), GtkFilterChange::GTK_FILTER_CHANGE_DIFFERENT); } -void CellFontRenderer::snapshot_vfunc(Glib::RefPtr const &snapshot, Gtk::Widget &widget, Gdk::Rectangle const &background_area, Gdk::Rectangle const &cell_area, Gtk::CellRendererState flags) { - Gdk::Rectangle bgnd(background_area); - Gdk::Rectangle area(cell_area); - auto margin = 0; // extra space for icon? - bgnd.set_x(bgnd.get_x() + margin); - area.set_x(area.get_x() + margin); - const auto name_font_size = 10; // attempt to select text size - const auto bottom = area.get_y() + area.get_height(); // bottom where the info font name will be placed - Glib::RefPtr layout; - int text_height = 0; - - if (_show_font_name) { - layout = _tree->create_pango_layout(_name); - Pango::FontDescription font("Noto"); // wide range of character support - font.set_weight(Pango::Weight::NORMAL); - font.set_size(name_font_size * PANGO_SCALE); - layout->set_font_description(font); - int text_width = 0; - // get the text dimensions - layout->get_pixel_size(text_width, text_height); - // shrink area to prevent overlap - area.set_height(area.get_height() - text_height); - } - - CellRendererText::snapshot_vfunc(snapshot, widget, bgnd, area, flags); - - if (_show_font_name) { - auto context = _tree->get_style_context(); - Gtk::StateFlags sflags = _tree->get_state_flags(); - if ((bool)(flags & Gtk::CellRendererState::SELECTED)) { - sflags |= Gtk::StateFlags::SELECTED; - } - context->set_state(sflags); - Gdk::RGBA fg = context->get_color(); - snapshot->save(); - snapshot->translate({(float)(area.get_x() + 2), (float)bottom - text_height}); - snapshot->append_layout(layout, fg); - snapshot->restore(); - } +} // namespace + +namespace Inkscape::UI::Widget { + +std::unique_ptr FontList::create_font_list(Glib::ustring path) { + return std::make_unique(path); } -const char* get_sort_icon(Inkscape::FontOrder order) { - const char* icon = nullptr; +// list of font sizes for a slider; combo box has its own list +static std::array g_font_sizes = { + 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 24, 28, 32, 36, + 44, 56, 64, 72, 80, 96, 112, 128, 144, 160, 192, 224, 256, + 300, 350, 400, 450, 500, 550, 600, 700, 800 +}; - switch (order) { - case Inkscape::FontOrder::by_name: - icon = "sort-alphabetically-symbolic"; - break; +static int index_to_font_size(int index) { + if (index < 0) { + return g_font_sizes.front(); + } + else if (index >= g_font_sizes.size()) { + return g_font_sizes.back(); + } + else { + return g_font_sizes[index]; + } +} - case Inkscape::FontOrder::by_weight: - icon = "sort-by-weight-symbolic"; - break; +static int font_size_to_index(double size) { + auto it = std::lower_bound(begin(g_font_sizes), end(g_font_sizes), static_cast(size)); + return std::distance(begin(g_font_sizes), it); +} - case Inkscape::FontOrder::by_width: - icon = "sort-by-width-symbolic"; - break; +const char* get_sort_icon(FontOrder order) { + const char* icon = nullptr; - default: - g_warning("Missing case in get_sort_icon"); - break; + switch (order) { + case FontOrder::ByFamily: + icon = "sort-by-family-symbolic"; + break; + case FontOrder::ByName: + icon = "sort-alphabetically-symbolic"; + break; + case FontOrder::ByWeight: + icon = "sort-by-weight-symbolic"; + break; + case FontOrder::ByWidth: + icon = "sort-by-width-symbolic"; + break; + default: + g_warning("Missing case in get_sort_icon"); + break; } return icon; } -void set_grid_cell_size(Gtk::CellRendererText* renderer, int font_size_percent) { - // TODO: use pango layout to calc sizes - int size = 20 * font_size_percent / 100; - renderer->set_fixed_size(size * 4 / 3, size); -}; - FontList::FontList(Glib::ustring preferences_path) : _prefs(std::move(preferences_path)), _builder(create_builder("font-list.glade")), _main_grid(get_widget(_builder, "main-grid")), _tag_list(get_widget(_builder, "categories")), - _font_list(get_widget(_builder, "font-list")), - _font_grid(get_widget(_builder, "font-grid")), + _font_list(get_widget(_builder, "font-list")), + _font_grid(get_widget(_builder, "font-grid")), _font_size(get_widget(_builder, "font-size")), _font_size_scale(get_widget(_builder, "font-size-scale")), + _preview_size_scale(get_widget(_builder, "preview-font-size")), + _grid_size_scale(get_widget(_builder, "grid-font-size")), + _grid_sample_entry(get_widget(_builder, "grid-sample")), + _list_sample_entry(get_widget(_builder, "sample-text")), _tag_box(get_widget(_builder, "tag-box")), _info_box(get_widget(_builder, "info-box")), _progress_box(get_widget(_builder, "progress-box")), _search(get_widget(_builder, "font-search")), - _font_tags(Inkscape::FontTags::get()) + _var_axes(get_widget(_builder, "var-axes")), + _font_tags(FontTags::get()) { - _cell_renderer = std::make_unique(); - auto font_renderer = static_cast(_cell_renderer.get()); - font_renderer->_tree = &_font_list; - - _cell_icon_renderer = std::make_unique(); - auto ico = static_cast(_cell_icon_renderer.get()); - ico->set_fixed_size(16, 16); + _font_store = Gio::ListStore::create(); + + // common filtering action for placeholder and injected font: + // - placeholders: always hidden + // - injected fonts: always visible +#define HANDLE_SPECIAL_FONT \ + auto font = std::dynamic_pointer_cast(item); \ + if (!font || font->is_placeholder()) return false; \ + if (font->is_injected()) return true; + + // filter for: + // - grouping fonts by family + _family_filter = Gtk::BoolFilter::create(Gtk::ClosureExpression::create([this](auto& item) { + HANDLE_SPECIAL_FONT + + // if grouping by family is on filter out individual font styles leaving only "Regular" font + if (_order == FontOrder::ByFamily && !font->is_family()) return false; + + return true; + })); + + // filter for: + // - font collections + // - font categories + _font_filter = Gtk::BoolFilter::create(Gtk::ClosureExpression::create([this](auto& item){ + HANDLE_SPECIAL_FONT + + // apply categories, if any + auto& active_categories = _font_tags.get_selected_tags(); + if (!active_categories.empty()) { + bool filter_in = false; + auto&& set = _font_tags.get_font_tags(font->font().face); + for (auto&& ftag : active_categories) { + if (set.contains(ftag.tag)) { + filter_in = true; + break; + } + } + if (!filter_in) return false; + } - _grid_renderer = std::make_unique(); - auto grid_renderer = static_cast(_grid_renderer.get()); - grid_renderer->_show_font_name = false; + // check for selected font collections, if any + auto fc = FontCollections::get(); + auto& font_collections = fc->get_selected_collections(); + if (!font_collections.empty()) { + bool filter_in = false; + for (auto& col : font_collections) { + if (fc->is_font_in_collection(col, font->font().ff->get_name())) { + filter_in = true; + break; + } + } + if (!filter_in) return false; + } - _font_list_store = Gtk::ListStore::create(g_column_model); + return true; // filter in + })); + + // filter for matching search text + _text_filter = Gtk::BoolFilter::create(Gtk::ClosureExpression::create([this](auto& item) { + HANDLE_SPECIAL_FONT + + if (_search_term.empty()) return true; + + // TODO: vary name based on font '_type'? + auto text = get_full_name(font->font()).lowercase(); + return text.find(_search_term) != Glib::ustring::npos; + })); + +#undef HANDLE_SPECIAL_FONT + + // set up tree view + { + // cascade filters; currently family filter needs to be first to find the "Regular" font + auto filtered1 = Gtk::FilterListModel::create(_font_store, _family_filter); + auto filtered2 = Gtk::FilterListModel::create(filtered1, _font_filter); + auto filtered3 = Gtk::FilterListModel::create(filtered2, _text_filter); + auto tree_model = Gtk::TreeListModel::create(filtered3, [](auto& item){ return create_element_model(item); }, false, false); + _list_selection = Gtk::SingleSelection::create(tree_model); + // we need autoselect off, so collapsing nodes does not move selection + _list_selection->set_autoselect(false); + // manually unselecting current font doesn't seem to serve any purpose, so it is disabled + _list_selection->set_can_unselect(false); + auto factory = Gtk::SignalListItemFactory::create(); + factory->signal_setup().connect([](auto& item){ on_set_up_listitem(item); }); + factory->signal_bind().connect([this](auto& item){ on_bind_listitem(_sample_font_size, _show_font_names, _sample_text, item); }); + _font_list.set_show_separators(); + _font_list.set_model(_list_selection); + _font_list.set_factory(factory); + } + // set up grid view + { + // cascade filters; no family filter for a grid, there's no collapsing there + auto filtered1 = Gtk::FilterListModel::create(_font_store, _font_filter); + auto filtered2 = Gtk::FilterListModel::create(filtered1, _text_filter); + _grid_selection = Gtk::SingleSelection::create(filtered2); + _grid_selection->set_can_unselect(false); + _font_grid.set_model(_grid_selection); + auto factory = Gtk::SignalListItemFactory::create(); + factory->signal_setup().connect([](auto& item){ on_set_up_griditem(item); }); + factory->signal_bind().connect([this](auto& item){ on_bind_griditem(_grid_font_size, _show_font_names, _grid_sample_text, item); }); + _font_grid.set_factory(factory); + } - get_widget(_builder, "variants").append(_font_variations); + _var_axes.set_visible(false); + _var_axes.set_child(_font_variations); _font_variations.get_size_group(0)->add_widget(get_widget(_builder, "font-size-label")); _font_variations.get_size_group(1)->add_widget(_font_size); _font_variations.connectChanged([this]{ @@ -262,35 +548,40 @@ FontList::FontList(Glib::ustring preferences_path) : set_margin_top(5); set_margin_bottom(0); - auto options = &get_widget(_builder, "btn-options"); - auto options_grid = &get_widget(_builder, "options-grid"); - options->signal_toggled().connect([=](){ - options_grid->set_visible(options->get_active()); - }); + auto prefs = Preferences::get(); - std::pair sorting[3] = { - {N_("Sort alphabetically"), Inkscape::FontOrder::by_name}, - {N_("Light to heavy"), Inkscape::FontOrder::by_weight}, - {N_("Condensed to expanded"), Inkscape::FontOrder::by_width} + std::pair sorting[4] = { + {N_("Group by family"), FontOrder::ByFamily}, + {N_("Sort alphabetically"), FontOrder::ByName}, + {N_("Light to heavy"), FontOrder::ByWeight}, + {N_("Condensed to expanded"), FontOrder::ByWidth} }; auto sort_menu = Gtk::make_managed(Gtk::PositionType::BOTTOM); - for (auto&& el : sorting) { + for (auto&[label, order] : sorting) { auto item = Gtk::make_managed(); + if (order == FontOrder::ByFamily) { + _sort_by_family = item; + } auto hbox = Gtk::make_managed(); - hbox->append(*Gtk::manage(sp_get_icon_image(get_sort_icon(el.second), Gtk::IconSize::NORMAL))); - hbox->append(*Gtk::make_managed(_(el.first))); + hbox->append(*Gtk::manage(sp_get_icon_image(get_sort_icon(order), Gtk::IconSize::NORMAL))); + hbox->append(*Gtk::make_managed(_(label))); hbox->set_spacing(4); item->set_child(*hbox); - item->signal_activate().connect([=, this] { sort_fonts(el.second); }); + item->signal_activate().connect([=, this] { + _order = order; + set_sort_icon(); + sort_fonts(order); + prefs->setInt(_prefs + "/font-order", static_cast(_order)); + }); sort_menu->append(*item); } get_widget(_builder, "btn-sort").set_popover(*sort_menu); - get_widget(_builder, "id-reset-filter").signal_clicked().connect([this](){ + get_widget(_builder, "id-reset-filter").signal_clicked().connect([this] { bool modified = false; if (_font_tags.deselect_all()) { modified = true; } - auto fc = Inkscape::FontCollections::get(); + auto fc = FontCollections::get(); if (!fc->get_selected_collections().empty()) { fc->clear_selected_collections(); modified = true; @@ -309,8 +600,8 @@ FontList::FontList(Glib::ustring preferences_path) : _current_font_instance = FontFactory::get().FaceFromPangoString(spec.c_str()); Glib::ustring name; - if (auto iter = get_selected_font()) { - const auto& font = iter->get_value(g_column_model.font); + if (auto element = std::dynamic_pointer_cast(get_selected_font())) { + const auto& font = element->font(); name = get_full_name(font); } _charmap.set_font(_current_font_instance.get(), name); @@ -332,77 +623,72 @@ FontList::FontList(Glib::ustring preferences_path) : _signal_insert_text.emit(text); }); - _search.signal_changed().connect([this]{ filter(); }); - - auto set_row_height = [font_renderer, this](int font_size_percent) { - font_renderer->_font_size = font_size_percent; - // TODO: use pango layout to calc sizes - int hh = (font_renderer->_show_font_name ? 10 : 0) + 18 * font_renderer->_font_size / 100; - font_renderer->set_fixed_size(-1, hh); - // resize rows - _font_list.set_fixed_height_mode(false); - _font_list.set_fixed_height_mode(); - }; - auto set_grid_size = [=](int font_size_percent) { - grid_renderer->_font_size = font_size_percent; - set_grid_cell_size(grid_renderer, font_size_percent); - }; - - auto prefs = Inkscape::Preferences::get(); + _search.signal_changed().connect([this] { + apply_filters_keep_selection(true); + }); - font_renderer->_font_size = prefs->getIntLimited(_prefs + "/preview-size", 200, 100, 800); - auto size = &get_widget(_builder, "preview-font-size"); - size->set_format_value_func([](double val){ + _sample_font_size = prefs->getIntLimited(_prefs + "/preview-size", _sample_font_size, 100, 800); + _preview_size_scale.set_format_value_func([](double val){ return Glib::ustring::format(std::fixed, std::setprecision(0), val) + "%"; }); - size->set_value(font_renderer->_font_size); - size->signal_value_changed().connect([=, this]{ - auto font_size = size->get_value(); - set_row_height(font_size); - set_grid_size(font_size); - prefs->setInt(_prefs + "/preview-size", font_size); - // resize - filter(); + _preview_size_scale.set_value(_sample_font_size); + _preview_size_scale.signal_value_changed().connect([=, this]{ + _sample_font_size = _preview_size_scale.get_value(); + prefs->setInt(_prefs + "/preview-size", _sample_font_size); + rebuild_ui(); + }); + _grid_font_size = prefs->getIntLimited(_prefs + "/grid-size", _grid_font_size, 100, 800); + _grid_size_scale.set_format_value_func([](double val){ + return Glib::ustring::format(std::fixed, std::setprecision(0), val) + "%"; + }); + _grid_size_scale.set_value(_grid_font_size); + _grid_size_scale.signal_value_changed().connect([=, this]{ + _grid_font_size = _grid_size_scale.get_value(); + prefs->setInt(_prefs + "/grid-size", _grid_font_size); + rebuild_ui(); + }); + + auto to_top = prefs->getBool(_prefs + "/font-size-top", false); + set_font_size_layout(to_top); + auto size_top = &get_widget(_builder, "font-size-top"); + size_top->set_active(to_top); + get_widget(_builder, "font-size-bottom").set_active(!to_top); + size_top->signal_toggled().connect([=, this] { + bool top = size_top->get_active(); + set_font_size_layout(top); + prefs->setBool(_prefs + "/font-size-top", top); }); auto show_names = &get_widget(_builder, "show-font-name"); auto set_show_names = [=, this](bool show) { - font_renderer->_show_font_name = show; + _show_font_names = show; prefs->setBool(_prefs + "/show-font-names", show); - set_row_height(font_renderer->_font_size); - _font_list.set_grid_lines(show ? Gtk::TreeView::GridLines::HORIZONTAL : Gtk::TreeView::GridLines::NONE); - // resize - filter(); + rebuild_ui(); }; auto show = prefs->getBool(_prefs + "/show-font-names", true); set_show_names(show); show_names->set_active(show); - show_names->signal_toggled().connect([=](){ + show_names->signal_toggled().connect([=] { bool show = show_names->get_active(); set_show_names(show); }); // sample text to show for each font; empty to show font name - auto sample = &get_widget(_builder, "sample-text"); - auto sample_text = prefs->getString(_prefs + "/sample-text"); - sample->set_text(sample_text); - font_renderer->_sample_text = sample_text; - sample->signal_changed().connect([=, this](){ - auto text = sample->get_text(); - font_renderer->_sample_text = text; - prefs->setString(_prefs + "/sample-text", text); - _font_list.queue_draw(); + _sample_text = prefs->getString(_prefs + "/sample-text"); + _list_sample_entry.set_text(_sample_text); + _list_sample_entry.signal_changed().connect([=, this] { + _sample_text = _list_sample_entry.get_text(); + prefs->setString(_prefs + "/sample-text", _sample_text); + rebuild_ui(); }); + // sample text for grid - auto grid_sample = &get_widget(_builder, "grid-sample"); - auto sample_grid_text = prefs->getString(_prefs + "/grid-text", "Aa"); - grid_sample->set_text(sample_grid_text); - grid_renderer->_sample_text = sample_grid_text; - grid_sample->signal_changed().connect([=, this](){ - auto text = grid_sample->get_text(); - grid_renderer->_sample_text = text.empty() ? "?" : text; - prefs->setString(_prefs + "/grid-text", text); - _font_grid.queue_draw(); + _grid_sample_text = prefs->getString(_prefs + "/grid-text", "Aa"); + _grid_sample_entry.set_text(_grid_sample_text); + _grid_sample_entry.signal_changed().connect([=, this] { + _grid_sample_text = _grid_sample_entry.get_text(); + prefs->setString(_prefs + "/grid-text", _grid_sample_text); + rebuild_ui(); }); // Populate samples submenu from stringlist @@ -435,106 +721,68 @@ FontList::FontList(Glib::ustring preferences_path) : // Hook up action used by samples submenu auto action_group = Gio::SimpleActionGroup::create(); - action_group->add_action_with_parameter("set-sample", Glib::Variant::variant_type(), [=] (Glib::VariantBase const ¶m) { + action_group->add_action_with_parameter("set-sample", Glib::Variant::variant_type(), [=, this] (Glib::VariantBase const ¶m) { auto param_str = Glib::VariantBase::cast_dynamic>(param).get(); - sample->set_text(param_str); + _list_sample_entry.set_text(param_str); }); insert_action_group("win", action_group); - _text_column.set_sizing(Gtk::TreeViewColumn::Sizing::FIXED); - _text_column.pack_start(*_cell_icon_renderer, false); - _text_column.add_attribute(ico->property_icon_name(), g_column_model.icon_name); - _cell_renderer->property_ellipsize() = Pango::EllipsizeMode::END; - _text_column.pack_start(*_cell_renderer, true); - _text_column.set_fixed_width(100); // limit minimal width to keep entire dialog narrow; column can still grow - _text_column.set_cell_data_func(*_cell_renderer, [=](Gtk::CellRenderer* r, const Gtk::TreeModel::const_iterator& it) { - Gtk::TreeModel::ConstRow row = *it; - get_cell_data_func(r, row); - }); - _text_column.set_expand(); - _font_list.append_column(_text_column); - - _font_list.set_fixed_height_mode(); - set_row_height(font_renderer->_font_size); - //todo: restore grid size and view? - _font_list.set_search_column(-1); // disable, we have a separate search/filter - _font_list.set_enable_search(false); - _font_list.set_model(_font_list_store); - - _font_grid.pack_start(*_grid_renderer); - grid_renderer->set_fixed_height_from_font(-1); - set_grid_size(grid_renderer->_font_size); - // grid_renderer->_sample_text = "Aa"; - _font_grid.set_cell_data_func(*grid_renderer, [=](const Gtk::TreeModel::const_iterator& it) { - Gtk::TreeModel::ConstRow row = *it; - get_cell_data_func(grid_renderer, row); - }); - // show font name in a grid tooltip - _font_grid.signal_query_tooltip().connect([this](int x, int y, bool kbd, const Glib::RefPtr& tt){ - Gtk::TreeModel::iterator iter; - Glib::ustring name; - if (_font_grid.get_tooltip_context_iter(x, y, kbd, iter)) { - const auto& font = iter->get_value(g_column_model.font); - name = get_font_name(iter); - tt->set_text(name); - } - return !name.empty(); - }, true); - _font_grid.property_has_tooltip() = true; - auto font_selected = [this](const FontInfo& font) { if (_update.pending()) return; auto scoped = _update.block(); auto vars = font.variations; if (vars.empty() && font.variable_font) { - vars = Inkscape::get_inkscape_fontspec(font.ff, font.face, font.variations); + vars = get_inkscape_fontspec(font.ff, font.face, font.variations); } _font_variations.update(vars); + _var_axes.set_visible(_font_variations.variations_present()); _signal_changed.emit(); }; - _selection_changed = _font_grid.signal_selection_changed().connect([font_selected, this]{ - auto sel = _font_grid.get_selected_items(); - if (sel.size() == 1) { - auto it = _font_list_store->get_iter(sel.front()); - const Inkscape::FontInfo& font = (*it)[g_column_model.font]; - font_selected(font); + _list_selection->signal_selection_changed().connect([font_selected, this](auto index, auto n) { + if (auto element = std::dynamic_pointer_cast(get_selected_font())) { + font_selected(element->font()); } }); + _font_list.signal_activate().connect([this](auto index) { + if (_update.pending()) return; - auto show_grid = &get_widget(_builder, "view-grid"); - auto show_list = &get_widget(_builder, "view-list"); - auto set_list_view_mode = [prefs, this](bool show_list) { - auto& list = get_widget(_builder, "list"); - auto& grid = get_widget(_builder, "grid"); - // TODO: sync selection between font widgets - if (show_list) { - grid.set_visible(false); - _font_grid.unset_model(); - list.set_visible(); + if (auto element = std::dynamic_pointer_cast(get_nth_font(index))) { + auto scoped = _update.block(); + _signal_apply.emit(); } - else { - list.set_visible(false); - _font_grid.set_model(_font_list_store); - grid.set_visible(); + }); + + _grid_selection->signal_selection_changed().connect([font_selected, this](auto index, auto n) { + if (auto element = std::dynamic_pointer_cast(get_selected_font())) { + font_selected(element->font()); } - _view_mode_list = show_list; - get_widget(_builder, "grid-sample").set_visible(!show_list); - prefs->setBool(_prefs + "/list-view-mode", show_list); - }; + }); + _font_grid.signal_activate().connect([this](auto index) { + if (_update.pending()) return; + + if (auto element = std::dynamic_pointer_cast(get_nth_font(index))) { + auto scoped = _update.block(); + _signal_apply.emit(); + } + }); + + // set up list/tree view vs grid view mode switching auto list_mode = prefs->getBool(_prefs + "/list-view-mode", true); + switch_view_mode(list_mode); + auto show_grid = &get_widget(_builder, "view-grid"); + auto show_list = &get_widget(_builder, "view-list"); if (list_mode) show_list->set_active(); else show_grid->set_active(); - set_list_view_mode(list_mode); - show_list->signal_toggled().connect([=] { set_list_view_mode(true); }); - show_grid->signal_toggled().connect([=] { set_list_view_mode(false); }); + show_list->signal_toggled().connect([show_list, this] { + switch_view_mode(show_list->get_active()); + }); - _fonts.clear(); _initializing = 0; _info_box.set_visible(false); _progress_box.show(); - auto prepare_tags = [this](){ + auto prepare_tags = [this]{ // prepare dynamic tags for (auto&& f : _fonts) { auto kind = f.family_kind >> 8; @@ -565,10 +813,13 @@ FontList::FontList(Glib::ustring preferences_path) : _font_stream = FontDiscovery::get().connect_to_fonts([=, this](const FontDiscovery::MessageType& msg){ if (auto r = Async::Msg::get_result(msg)) { - _fonts = **r; + _font_families = **r; + _fonts.clear(); + for (auto&& family : _font_families) { + _fonts.insert(_fonts.end(), family.begin(), family.end()); + } sort_fonts(_order); prepare_tags(); - filter(); } else if (auto p = Async::Msg::get_progress(msg)) { // show progress @@ -579,12 +830,14 @@ FontList::FontList(Glib::ustring preferences_path) : progress.set_text(std::get(*p)); auto&& family = std::get>(*p); _fonts.insert(_fonts.end(), family.begin(), family.end()); + if (!family.empty()) { + _font_families.push_back(std::move(family)); + } auto delta = _fonts.size() - _initializing; // refresh fonts; at first more frequently, every new 100, but then more slowly, as it gets costly if (delta > 500 || (_fonts.size() < 500 && delta > 100)) { _initializing = _fonts.size(); sort_fonts(_order); - filter(); } } else if (Async::Msg::is_finished(msg)) { @@ -596,7 +849,7 @@ FontList::FontList(Glib::ustring preferences_path) : _font_size_scale.get_adjustment()->set_lower(0); _font_size_scale.get_adjustment()->set_upper(g_font_sizes.size() - 1); - _font_size_scale.signal_value_changed().connect([this](){ + _font_size_scale.signal_value_changed().connect([this] { if (_update.pending()) return; auto scoped = _update.block(); @@ -605,7 +858,7 @@ FontList::FontList(Glib::ustring preferences_path) : _signal_changed.emit(); }); - _font_size.signal_changed().connect([this](){ + _font_size.signal_changed().connect([this] { if (_update.pending()) return; auto scoped = _update.block(); @@ -621,199 +874,269 @@ FontList::FontList(Glib::ustring preferences_path) : _font_size.set_active_text("10"); _font_size.get_entry()->set_max_width_chars(6); - sort_fonts(Inkscape::FontOrder::by_name); - - _font_list.get_selection()->signal_changed().connect([=, this](){ - if (auto iter = _font_list.get_selection()->get_selected()) { - const Inkscape::FontInfo& font = (*iter)[g_column_model.font]; - font_selected(font); - } - }, false); - - // double-click: - _font_list.signal_row_activated().connect([this](const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn*){ - if (!_update.pending()) { - auto scoped = _update.block(); - _signal_apply.emit(); - } - }); + // restore sorting + _order = static_cast(prefs->getIntLimited(_prefs + "/font-order", static_cast(_order), static_cast(FontOrder::_First), static_cast(FontOrder::_Last))); + set_sort_icon(); + sort_fonts(_order); _font_tags.get_signal_tag_changed().connect([this](const FontTag* ftag, bool selected){ sync_font_tag(ftag, selected); }); auto& filter_popover = get_widget(_builder, "filter-popover"); - filter_popover.signal_show().connect([this](){ + filter_popover.signal_show().connect([this] { // update tag checkboxes add_categories(); update_filterbar(); }, false); - _font_collections_update = Inkscape::FontCollections::get()->connect_update([this]() { + _font_collections_update = FontCollections::get()->connect_update([this] { add_categories(); update_filterbar(); - filter(); + apply_filters_keep_selection(); }); - _font_collections_selection = Inkscape::FontCollections::get()->connect_selection_update([this](){ + _font_collections_selection = FontCollections::get()->connect_selection_update([this] { add_categories(); update_filterbar(); - filter(); + apply_filters_keep_selection(); }); } -void FontList::sort_fonts(Inkscape::FontOrder order) { - Inkscape::sort_fonts(_fonts, order, true); +void FontList::set_sort_icon() { + auto order = _order; + if (order == FontOrder::ByFamily && !_list_visible) { + order = FontOrder::ByName; + } + // this option only applies to a list/tree view + _sort_by_family->set_visible(_list_visible); if (const char* icon = get_sort_icon(order)) { auto& button = get_widget(_builder, "btn-sort"); button.set_icon_name(icon); } - - filter(); } -bool FontList::select_font(const Glib::ustring& fontspec) { - bool found = false; +void FontList::sort_fonts(FontOrder order) { + Inkscape::sort_fonts(_fonts, order, true); + + sort_font_families(_font_families, true); - _font_list_store->foreach([&](const Gtk::TreeModel::Path& path, const Gtk::TreeModel::iterator& iter){ - const auto& row = *iter; - const auto& font = row.get_value(g_column_model.font); - if (!font.ff) { - auto spec = row.get_value(g_column_model.alt_fontspec); + rebuild_store(); +} + +int FontList::find_font(const Glib::ustring& fontspec, int from, int count) const { + auto& selection = _list_visible ? _list_selection : _grid_selection; + auto total = selection->get_n_items(); + auto n = count > 0 ? std::min(count, static_cast(total)) : total; + for (int i = from; i < n; ++i) { + auto element = std::dynamic_pointer_cast(get_nth_font(i)); + if (!element) continue; + + if (element->is_present()) { + auto& font = element->font(); + auto spec = get_inkscape_fontspec(font.ff, font.face, font.variations); if (spec == fontspec) { - _font_list.get_selection()->select(row.get_iter()); - _font_grid.select_path(path); - scroll_to_row(path); - found = true; - return true; // stop + return i; } } else { - const auto& font = row.get_value(g_column_model.font); - auto spec = Inkscape::get_inkscape_fontspec(font.ff, font.face, font.variations); + auto& spec = element->get_alt_spec(); if (spec == fontspec) { - _font_list.get_selection()->select(row.get_iter()); - _font_grid.select_path(path); - scroll_to_row(path); - found = true; - return true; // stop + return i; } } - return false; // continue - }); + } + return -1; +} - return found; +void FontList::switch_view_mode(bool show_list) { + // get current selection to sync selection between font widgets + auto fontspec = get_fontspec(); + _list_visible = show_list; + auto& list = get_widget(_builder, "list"); + auto& grid = get_widget(_builder, "grid"); + if (show_list) { + grid.set_visible(false); + _font_grid.set_model({}); + _font_list.set_model(_list_selection); + list.set_visible(); + } + else { + list.set_visible(false); + _font_list.set_model({}); + _font_grid.set_model(_grid_selection); + grid.set_visible(); + } + // update sort icon: grid view does not support grouping by family + set_sort_icon(); + // update widgets in an option popup + get_widget(_builder, "sample-menu-btn").set_sensitive(show_list); + _list_sample_entry.set_visible(show_list); + _preview_size_scale.set_visible(show_list); + _grid_sample_entry.set_visible(!show_list); + _grid_size_scale.set_visible(!show_list); + Preferences::get()->setBool(_prefs + "/list-view-mode", show_list); + // try to reselect the same font in a new view + select_font(fontspec); } -void FontList::filter() { +bool FontList::select_font(const Glib::ustring& fontspec) { auto scoped = _update.block(); - // todo: save selection - Inkscape::FontInfo selected; - auto it = _font_list.get_selection()->get_selected(); - if (it) { - selected = it->get_value(g_column_model.font); - } - - // Not used: extra search terms; use collections instead - // auto& oblique = get_widget(_builder, "id-oblique"); - // auto& monospaced = get_widget(_builder, "id-monospaced"); - // auto& others = get_widget(_builder, "id-other"); - Show params; - // Not used - // params.monospaced = monospaced.get_active(); - // params.oblique = oblique.get_active(); - // params.others = others.get_active(); - populate_font_store(_search.get_text(), params); - if (!_current_fspec.empty()) { - add_font(_current_fspec, false); - } + bool found = false; - if (it) { - // reselect if that font is still available - //TODO + auto pos = find_font(fontspec); + if (pos >= 0) { + found = true; + scroll_to_row(pos); } -} -Glib::ustring get_font_icon(const FontInfo& font, bool missing_font) { - if (missing_font) { - return "missing-element-symbolic"; - } - else if (font.variable_font) { - return ""; // TODO: add icon for variable fonts? - } - else if (font.synthetic) { - return "generic-font-symbolic"; + if (!found && _list_visible && _order == FontOrder::ByFamily) { + // if some fonts in a tree are collapsed, their leaves are not visible to the selection model; + // we need to check all styles too: + + int pos = -1; + for (auto&& fam : _font_families) { + for (auto& font : fam) { + auto spec = get_inkscape_fontspec(font.ff, font.face, font.variations); + if (spec == fontspec) { + // find "family" node in the tree + auto& regular = get_family_font(fam); + spec = get_inkscape_fontspec(regular.ff, regular.face, regular.variations); + pos = find_font(spec); + break; + } + } + if (pos >= 0) { + if (auto row = std::dynamic_pointer_cast(_list_selection->get_object(pos))) { + // expand family row to populate styles + row->set_expanded(); + pos = find_font(fontspec, pos, pos + 1 + fam.size()); + if (pos >= 0) { + found = true; + scroll_to_row(pos); + } + } + break; + } + } } - return Glib::ustring(); + + return found; } -// add fonts to the font store taking filtring params into account -void FontList::populate_font_store(Glib::ustring text, const Show& params) { - auto filter = text.lowercase(); +void FontList::rebuild_store() { + // recreate content of the font store - auto active_categories = _font_tags.get_selected_tags(); - auto apply_categories = !active_categories.empty(); + auto scoped = _update.block(); - auto fc = Inkscape::FontCollections::get(); - auto font_collections = fc->get_selected_collections(); - auto apply_font_collections = !font_collections.empty(); + // save selection + auto fontspec = get_fontspec(); _font_list.set_visible(false); // hide tree view temporarily to speed up rebuild _font_grid.set_visible(false); - _font_list_store->freeze_notify(); - _font_list_store->clear(); - _extra_fonts = 0; + _font_list.set_model(nullptr); + _font_grid.set_model(nullptr); - for (auto&& f : _fonts) { - bool filter_in = false; + populate_font_store(_order == FontOrder::ByFamily); - if (!text.empty()) { - auto name1 = get_full_name(f); - if (name1.lowercase().find(filter) == Glib::ustring::npos) continue; - } + if (!_current_fspec.empty()) { + add_font(_current_fspec, false); + } - if (apply_categories || apply_font_collections) { - if (apply_categories) { - auto&& set = _font_tags.get_font_tags(f.face); - for (auto&& ftag : active_categories) { - if (set.contains(ftag.tag)) { - filter_in = true; - break; - } - } - } + apply_filters(); - if (!filter_in && apply_font_collections) { - for (auto& col : font_collections) { - if (fc->is_font_in_collection(col, f.ff->get_name())) { - filter_in = true; - break; - } - } - } + _font_list.set_visible(); + _font_grid.set_visible(); + rebuild_ui(); + + // reselect if that font is still available + select_font(fontspec); +} - if (!filter_in) continue; +void FontList::apply_filters_keep_selection(bool text_only) { + // save selection + auto fontspec = get_fontspec(); + + if (auto placeholder = std::dynamic_pointer_cast(_font_store->get_object(0))) { + if (placeholder->is_injected()) { + // now is the time to remove injected fonts; they will be reinserted as needed + placeholder->clear_injected(); + // + // text_only = false; } + } + + apply_filters(!text_only); + + // restore + select_font(fontspec); +} + +void FontList::apply_filters(bool all_filters) { + auto scoped = _update.block(); - Gtk::TreeModel::iterator treeModelIter = _font_list_store->append(); - auto& row = *treeModelIter; - row[g_column_model.font] = f; - row[g_column_model.alt_fontspec] = Glib::ustring(); - row[g_column_model.icon_name] = get_font_icon(f, false); + if (all_filters) { + refilter(_family_filter); + refilter(_font_filter); } - _font_list_store->thaw_notify(); - _font_list.set_visible(); // restore visibility - _font_grid.set_visible(); + _search_term = _search.get_text().lowercase(); + refilter(_text_filter); update_font_count(); } +void FontList::rebuild_ui() { + // force the UI to be recreated when all items are impacted + + _font_list.set_model(nullptr); + _font_grid.set_model(nullptr); + + if (_list_visible) { + _font_list.set_model(_list_selection); + } + else { + _font_grid.set_model(_grid_selection); + } +} + +// add fonts to the font store replacing its content +void FontList::populate_font_store(bool by_family) { + _font_store->freeze_notify(); + _font_store->remove_all(); + + // reserve first spot; it will be used for "missing" or "injected" fonts, as needed + _font_store->append(FontElement::create_placeholder()); + + if (by_family) { + for (auto&& fam : _font_families) { + auto& regular = get_family_font(fam); + for (auto& font : fam) { + if (font == regular) { + _font_store->append(FontElement::create_family(regular, fam)); + } + else { + _font_store->append(FontElement::create_font(font)); + } + } + } + } + else { + for (auto&& font : _fonts) { + _font_store->append(FontElement::create_font(font)); + } + } + + _font_store->thaw_notify(); +} + void FontList::update_font_count() { auto& font_count = get_widget(_builder, "font-count"); - auto count = _font_list_store->children().size(); - auto total = _fonts.size(); + // total number of fonts; subtract one, there's a placeholder or "missing" font entry always present + auto total = _font_store->get_n_items() - 1; + // use grid selection model, as it always reflects current number of fonts (list view can be partially collapsed) + auto count = _grid_selection->get_n_items(); // count could be larger than total if we insert "missing" font(s) 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")); font_count.set_text(label); @@ -828,31 +1151,43 @@ double FontList::get_fontsize() const { return _current_fsize; } -Gtk::TreeModel::iterator FontList::get_selected_font() const { - if (_view_mode_list) { - return _font_list.get_selection()->get_selected(); +Glib::RefPtr FontList::get_nth_font(int index) const { + if (_list_visible) { + if (auto row = std::dynamic_pointer_cast(_list_selection->get_object(index))) { + return row->get_item(); + } } else { - auto sel = _font_grid.get_selected_items(); - if (sel.size() == 1) return _font_list_store->get_iter(sel.front()); + return _grid_selection->get_object(index); } + + return {}; +} + +Glib::RefPtr FontList::get_selected_font() const { + if (_list_visible) { + if (auto row = std::dynamic_pointer_cast(_list_selection->get_selected_item())) { + return row->get_item(); + } + } + else { + return _grid_selection->get_selected_item(); + } + return {}; } Glib::ustring FontList::get_fontspec() const { - if (auto iter = get_selected_font()) { //_font_list.get_selection()->get_selected()) { - const auto& font = iter->get_value(g_column_model.font); - // auto font_class = iter->get_value(g_column_model.font_class); + if (auto element = std::dynamic_pointer_cast(get_selected_font())) { + auto& font = element->font(); if (font.ff) { auto variations = _font_variations.get_pango_string(true); - auto fspec = Inkscape::get_inkscape_fontspec(font.ff, font.face, variations); - return fspec; + return Inkscape::get_inkscape_fontspec(font.ff, font.face, variations); } else { // missing fonts don't have known variation that we could tweak, // so ignore _font_variations UI and simply return alt_fontspec - auto&& name = iter->get_value(g_column_model.alt_fontspec); - return name; + return element->get_alt_spec(); } } return "sans-serif"; // no selection @@ -864,6 +1199,7 @@ void FontList::set_current_font(const Glib::ustring& family, const Glib::ustring auto scoped = _update.block(); auto fontspec = Inkscape::get_fontspec(family, face); + if (fontspec == _current_fspec) { auto fspec = get_fontspec_without_variants(fontspec); select_font(fspec); @@ -871,8 +1207,9 @@ void FontList::set_current_font(const Glib::ustring& family, const Glib::ustring } _current_fspec = fontspec; - if (!fontspec.empty()) { + if (!fontspec.empty() && fontspec != get_fontspec()) { _font_variations.update(fontspec); + _var_axes.set_visible(_font_variations.variations_present()); add_font(fontspec, true); } } @@ -890,11 +1227,13 @@ void FontList::set_current_size(double size) { } void FontList::add_font(const Glib::ustring& fontspec, bool select) { + auto scoped = _update.block(); + bool found = select_font(fontspec); // found in the tree view? if (found) return; - auto it = std::find_if(begin(_fonts), end(_fonts), [&](const FontInfo& f){ - return Inkscape::get_inkscape_fontspec(f.ff, f.face, f.variations) == fontspec; + auto it = std::ranges::find_if(_fonts, [&](const FontInfo& f){ + return get_inkscape_fontspec(f.ff, f.face, f.variations) == fontspec; }); // fonts with variations will not be found, we need to remove " @ axis=value" part @@ -903,7 +1242,7 @@ void FontList::add_font(const Glib::ustring& fontspec, bool select) { if (it == end(_fonts) && fspec != fontspec) { // try to match existing font it = std::find_if(begin(_fonts), end(_fonts), [&](const FontInfo& f){ - return Inkscape::get_inkscape_fontspec(f.ff, f.face, f.variations) == fspec; + return get_inkscape_fontspec(f.ff, f.face, f.variations) == fspec; }); if (it != end(_fonts)) { bool found = select_font(fspec); // found in the tree view? @@ -911,36 +1250,25 @@ void FontList::add_font(const Glib::ustring& fontspec, bool select) { } } + Glib::RefPtr insert; + if (it != end(_fonts)) { // font found in the "all fonts" vector, but - // this font is filtered out; add it temporarily to the tree list - Gtk::TreeModel::iterator iter = _font_list_store->prepend(); - auto& row = *iter; - row[g_column_model.font] = *it; - row[g_column_model.injected] = true; - row[g_column_model.alt_fontspec] = Glib::ustring(); - row[g_column_model.icon_name] = get_font_icon(*it, false); - - if (select) { - _font_list.get_selection()->select(row.get_iter()); - auto path = _font_list_store->get_path(iter); - scroll_to_row(path); - } - - ++_extra_fonts; // extra font entry inserted + // this font is filtered out; boost it temporarily to let it pass filtering + insert = FontElement::create_injected_font(*it, {}, false); } else { bool missing_font = true; - Inkscape::FontInfo subst; + FontInfo subst; auto desc = Pango::FontDescription(fontspec); auto vars = desc.get_variations(); if (!vars.empty()) { - // font with variantions; check if we have matching family + // font with variations; check if we have matching family subst.variations = vars; auto family = desc.get_family(); - it = std::find_if(begin(_fonts), end(_fonts), [&](const FontInfo& f){ + it = std::ranges::find_if(_fonts, [&](const FontInfo& f){ return f.ff->get_name() == family; }); if (it != end(_fonts)) { @@ -949,32 +1277,15 @@ void FontList::add_font(const Glib::ustring& fontspec, bool select) { } } - auto&& children = _font_list_store->children(); - Gtk::TreeModel::iterator iter; - if (!children.empty() && children[0][g_column_model.injected]) { - // reuse "injected font" entry - iter = children[0].get_iter(); - } - else { - // font not found; insert a placeholder to show injected font: font is used in a document, - // but either not available in the system (it's missing) or it is a variant of the variable font - iter = _font_list_store->prepend(); - } - auto& row = *iter; - row[g_column_model.font] = subst; - row[g_column_model.injected] = true; - row[g_column_model.alt_fontspec] = fontspec;// TODO fname? - row[g_column_model.icon_name] = get_font_icon(subst, missing_font); - - if (select) { - _font_list.get_selection()->select(iter); - auto path = _font_list_store->get_path(iter); - scroll_to_row(path); - } - - ++_extra_fonts; // extra font entry for a missing font added + // set "missing" font entry + insert = FontElement::create_injected_font(subst, fontspec.raw(), missing_font); } - update_font_count(); + + // change placeholder font entry; it's a first one in a store + std::vector> v(1, insert); + _font_store->splice(0, 1, v); + apply_filters(); + scroll_to_row(0); } Gtk::Box* FontList::create_pill_box(const Glib::ustring& display_name, const Glib::ustring& tag, bool tags) { @@ -988,15 +1299,15 @@ Gtk::Box* FontList::create_pill_box(const Glib::ustring& display_name, const Gli close->set_image_from_icon_name("close-button-symbolic"); close->set_valign(Gtk::Align::CENTER); if (tags) { - close->signal_clicked().connect([=, this](){ + close->signal_clicked().connect([=, this] { // remove category from current filter update_categories(tag, false); }); } else { - close->signal_clicked().connect([=](){ + close->signal_clicked().connect([=] { // remove collection from current filter - Inkscape::FontCollections::get()->update_selected_collections(tag); + FontCollections::get()->update_selected_collections(tag); }); } box->get_style_context()->add_class("tag-box"); @@ -1018,7 +1329,7 @@ void FontList::update_filterbar() { _tag_box.append(*pill); } - for (auto&& collection : Inkscape::FontCollections::get()->get_selected_collections()) { + for (auto&& collection : FontCollections::get()->get_selected_collections()) { auto pill = create_pill_box(collection, collection, false); _tag_box.append(*pill); } @@ -1035,7 +1346,7 @@ void FontList::update_categories(const std::string& tag, bool select) { update_filterbar(); // apply filter - filter(); + apply_filters(); } void FontList::add_categories() { @@ -1058,7 +1369,7 @@ void FontList::add_categories() { label.set_markup("" + tag.display_name + ""); btn->set_child(label); btn->set_active(_font_tags.is_tag_selected(tag.tag)); - btn->signal_toggled().connect([=, this](){ + btn->signal_toggled().connect([=, this]{ // toggle font category update_categories(tag.tag, btn->get_active()); }); @@ -1076,9 +1387,12 @@ void FontList::add_categories() { add_row(sep); } for (auto& col : font_collections) { - auto btn = Gtk::make_managed(col); + auto btn = Gtk::make_managed(); + auto& label = *Gtk::make_managed(); + label.set_text(col); + btn->set_child(label); btn->set_active(fc->is_collection_selected(col)); - btn->signal_toggled().connect([=](){ + btn->signal_toggled().connect([=]{ // toggle font system collection fc->update_selected_collections(col); }); @@ -1095,20 +1409,44 @@ void FontList::sync_font_tag(const FontTag* ftag, bool selected) { //todo as needed } -void FontList::scroll_to_row(Gtk::TreePath path) { - if (_view_mode_list) { - // delay scroll request to let widget layout complete (due to hiding or showing variable font widgets); - // keep track of connection so we can disconnect in a destructor if it is still pending at that point - _scroll = Glib::signal_timeout().connect([=, this](){ - _font_list.scroll_to_row(path); - return false; // <- false means disconnect - }, 50, Glib::PRIORITY_LOW); - // fudge factor of 50ms; ideally wait for layout pass to complete before scrolling to the row +void FontList::scroll_to_row(int index) { + auto flags = Gtk::ListScrollFlags::SELECT; + + if (_list_visible) { + _font_list.scroll_to(index, flags); } else { - // scroll grid - //todo + _font_grid.scroll_to(index, flags); } } +// place "Font size" box at the top (true) or at the bottom (false) of the dialog +void FontList::set_font_size_layout(bool top) { + auto& size = get_widget(_builder, "size-box"); + auto layout1 = std::dynamic_pointer_cast(_main_grid.get_layout_manager()->get_layout_child(size)); + auto& variants = get_widget(_builder, "variants"); + auto layout2 = std::dynamic_pointer_cast(_main_grid.get_layout_manager()->get_layout_child(variants)); + auto& separator = get_widget(_builder, "btm-separator"); + if (top) { + layout1->set_row(3); + layout2->set_row(4); + separator.set_visible(false); + } + else { + layout1->set_row(10); + layout2->set_row(11); + separator.set_visible(); + } +} + +void FontList::on_map() { + Box::on_map(); + + // grow scrollwindow to accommodate up to 4 and a half axes; beyond that - scroll + static auto four = _font_variations.measure_height(4); + static auto five = _font_variations.measure_height(5); + // fractional size to expose fifth axis and let users know there's more content there + _var_axes.set_max_content_height((four + five) / 2); +} + } // namespaces diff --git a/src/ui/widget/font-list.h b/src/ui/widget/font-list.h index b85177ea3918d34a4df5918ece9a58851febb92d..1ca83ed4c38b1b50f886f165165826b0f23d71e9 100644 --- a/src/ui/widget/font-list.h +++ b/src/ui/widget/font-list.h @@ -2,25 +2,30 @@ /** @file * @brief Font browser and selector * - * Copyright (C) 2022-2023 Michael Kowalski + * Copyright (C) 2022-2025 Michael Kowalski */ #ifndef INKSCAPE_UI_WIDGET_FONT_LIST_H #define INKSCAPE_UI_WIDGET_FONT_LIST_H +#include #include #include #include -#include +#include #include -#include +#include #include +#include +#include #include "character-viewer.h" #include "ui/widget/font-variations.h" #include "util/font-discovery.h" #include "util/font-tags.h" #include "font-selector-interface.h" +#include "generic/popover-menu.h" +#include "ui/text_filter.h" namespace Inkscape::UI::Widget { @@ -52,58 +57,71 @@ public: void unset_model() override {}; private: - void sort_fonts(Inkscape::FontOrder order); - void filter(); - struct Show { - bool monospaced; - bool oblique; - bool others; - }; - void populate_font_store(Glib::ustring text, const Show& params); + void on_map() override; + void sort_fonts(FontOrder order); + void set_sort_icon(); + void apply_filters(bool all_filters = true); + void rebuild_ui(); + void rebuild_store(); + void populate_font_store(bool by_family); + void apply_filters_keep_selection(bool text_only = false); void add_font(const Glib::ustring& fontspec, bool select); bool select_font(const Glib::ustring& fontspec); void update_font_count(); void add_categories(); - void toggle_category(); void update_categories(const std::string& tag, bool select); void update_filterbar(); Gtk::Box* create_pill_box(const Glib::ustring& display_name, const Glib::ustring& tag, bool tags); void sync_font_tag(const FontTag* ftag, bool selected); - void scroll_to_row(Gtk::TreePath path); - Gtk::TreeModel::iterator get_selected_font() const; + void scroll_to_row(int index); + void set_font_size_layout(bool top); + Glib::RefPtr get_selected_font() const; + Glib::RefPtr get_nth_font(int index) const; + int find_font(const Glib::ustring& fontspec, int from = 0, int count = -1) const; + void switch_view_mode(bool show_list); sigc::signal _signal_changed; sigc::signal _signal_apply; sigc::signal _signal_insert_text; Glib::RefPtr _builder; Gtk::Grid& _main_grid; - Gtk::TreeView& _font_list; - Gtk::TreeViewColumn _text_column; - Gtk::IconView& _font_grid; + Gtk::GridView& _font_grid; + Gtk::ListView& _font_list; Gtk::SearchEntry2& _search; - sigc::scoped_connection _selection_changed; - Glib::RefPtr _font_list_store; Gtk::Box& _tag_box; Gtk::Box& _info_box; Gtk::Box& _progress_box; + Gtk::Entry& _grid_sample_entry; + Gtk::Entry& _list_sample_entry; + Gtk::Scale& _preview_size_scale; + Gtk::Scale& _grid_size_scale; + Gtk::ScrolledWindow& _var_axes; + Gtk::ListBox& _tag_list; + Inkscape::FontTags& _font_tags; std::vector _fonts; - Inkscape::FontOrder _order = Inkscape::FontOrder::by_name; + std::vector> _font_families; + Glib::RefPtr _font_store; + Glib::RefPtr _text_filter; + Glib::RefPtr _font_filter; + Glib::RefPtr _family_filter; + Glib::RefPtr _list_selection; + Glib::RefPtr _grid_selection; + bool _list_visible = true; + FontOrder _order = FontOrder::ByFamily; Glib::ustring _filter; Gtk::ComboBoxText& _font_size; Gtk::Scale& _font_size_scale; - std::unique_ptr _cell_renderer; - std::unique_ptr _cell_icon_renderer; - std::unique_ptr _grid_renderer; Glib::ustring _current_fspec; double _current_fsize = 0.0; + bool _show_font_names = true; + Glib::ustring _sample_text; + Glib::ustring _grid_sample_text; + int _sample_font_size = 200; + int _grid_font_size = 300; + Glib::ustring _search_term; OperationBlocker _update; - int _extra_fonts = 0; - Gtk::ListBox& _tag_list; - Inkscape::FontTags& _font_tags; FontVariations _font_variations; - sigc::scoped_connection _scroll; Glib::ustring _prefs; - bool _view_mode_list = true; sigc::scoped_connection _font_stream; std::size_t _initializing = 0; sigc::scoped_connection _font_collections_update; @@ -111,6 +129,7 @@ private: Gtk::Popover _charmap_popover; CharacterViewer _charmap; std::shared_ptr _current_font_instance; // for charmap only + PopoverMenuItem* _sort_by_family = nullptr; }; } // namespaces diff --git a/src/ui/widget/font-variations.cpp b/src/ui/widget/font-variations.cpp index d63100b630f8737499cd44b37ebc72fabf9b1508..c8b5e0480b79fe125a93feb5693cc7eb5be28a5b 100644 --- a/src/ui/widget/font-variations.cpp +++ b/src/ui/widget/font-variations.cpp @@ -48,6 +48,10 @@ std::pair get_axis_name(const std::string& tag, co // mainly from https://fonts.google.com/knowledge/using_type/introducing_parametric_axes // CC BY-SA 4.0 // Standard axes guide for reference: https://variationsguide.typenetwork.com + // Other references: + // - https://fonts.google.com/variablefonts#axis-definitions + // - https://canary.grida.co/docs/reference/open-type-variable-axes + static std::map> map = { // TRANSLATORS: “Grade” (GRAD in CSS) is an axis that can be used to alter stroke thicknesses (or other forms) // without affecting the type's overall width, inter-letter spacing, or kerning — unlike altering weight. @@ -97,8 +101,46 @@ std::pair get_axis_name(const std::string& tag, co {"WONK", std::make_pair(C_("Variable font axis", "Wonky"), _("Binary switch used to control substitution of “wonky” forms"))}, // Element shape {"ESHP", std::make_pair(C_("Variable font axis", "Element shape"), _("Selection of the base element glyphs are composed of"))}, + // Element shape + {"ELSH", std::make_pair(C_("Variable font axis", "Element shape"), _("Controls element shape characteristics"))}, + // Element grid + {"ELGR", std::make_pair(C_("Variable font axis", "Element grid"), _("Controls how many elements are used per one grid unit"))}, // Element grid {"EGRD", std::make_pair(C_("Variable font axis", "Element grid"), _("Controls how many elements are used per one grid unit"))}, + // Proposed axis "height" + {"HGHT", std::make_pair(C_("Variable font axis", "Height"), _("Controls the font file’s height parameter"))}, + // Non-standard Y-axis stem thickness + {"YAXS", std::make_pair(C_("Variable font axis", "Y-Axis"), _("Controls stem thickness in vertical direction"))}, + // Vertical Element Alignment + {"YELA", std::make_pair(C_("Variable font axis", "Vertical align"), _("Controls vertical element alignment"))}, + // Corner roundness + {"ROND", std::make_pair(C_("Variable font axis", "Roundness"), _("Controls corner roundness"))}, + // Bleed + {"BLED", std::make_pair(C_("Variable font axis", "Bleed"), _("Controls ink bleed effect"))}, + // Scanlines + {"SCAN", std::make_pair(C_("Variable font axis", "Scanlines"), _("Controls scanline effect"))}, + // Morph + {"MORF", std::make_pair(C_("Variable font axis", "Morph"), _("Controls morphing characteristics"))}, + // Extrusion + {"EDPT", std::make_pair(C_("Variable font axis", "Extrusion depth"), _("Controls depth of extrusion"))}, + // Edge highlight + {"EHLT", std::make_pair(C_("Variable font axis", "Edge highlight"), _("Controls edge highlighting"))}, + // Hyper expansion + {"HEXP", std::make_pair(C_("Variable font axis", "Hyper expansion"), _("Controls hyper expansion characteristics"))}, + // Bounce + {"BNCE", std::make_pair(C_("Variable font axis", "Bounce"), _("Controls bounce/spring effect"))}, + // Informal + {"INFM", std::make_pair(C_("Variable font axis", "Informality"), _("Controls informality characteristics"))}, + // Spacing + {"SPAC", std::make_pair(C_("Variable font axis", "Spacing"), _("Controls character spacing"))}, + // Negative space + {"NEGA", std::make_pair(C_("Variable font axis", "Negative space"), _("Controls negative spacing"))}, + // X-rotation + {"XROT", std::make_pair(C_("Variable font axis", "X rotation"), _("Controls character 3D horizontal rotation"))}, + // Y-rotation + {"YROT", std::make_pair(C_("Variable font axis", "Y rotation"), _("Controls character 3D vertical rotation"))}, + // Sharpness + {"SHRP", std::make_pair(C_("Variable font axis", "Sharpness"), _("Controls sharpness characteristics"))}, // TRANSLATORS: “Optical Size” // Optical sizes in a variable font are different versions of a typeface optimized for use at singular specific sizes, // such as 14 pt or 144 pt. Small (or body) optical sizes tend to have less stroke contrast, more open and wider spacing, @@ -371,6 +413,21 @@ Glib::RefPtr FontVariations::get_size_group(int index) { } } +int FontVariations::measure_height(int axis_count) { + std::map axes; + for (int i = 0; i < axis_count; ++i) { + auto name = std::to_string(i); + OTVarAxis axis; + axis.tag = name; + axes[name] = axis; + } + build_ui(axes); + int min=0,nat=0,b1,b2; + measure(Gtk::Orientation::VERTICAL, 9999, min, nat, b1, b2); + build_ui({}); + return nat; +} + } // namespace Widget } // namespace UI } // namespace Inkscape diff --git a/src/ui/widget/font-variations.h b/src/ui/widget/font-variations.h index f58c20f1313d9da5fdb7bc3fd7894f4195a82116..36b2b881e5b61490fe9114ae47e295308961eddc 100644 --- a/src/ui/widget/font-variations.h +++ b/src/ui/widget/font-variations.h @@ -102,6 +102,9 @@ public: // provide access to label and spin button size groups Glib::RefPtr get_size_group(int index); + // construct temp UI with N axes and report its height + int measure_height(int axis_count); + private: void build_ui(const std::map& axes); diff --git a/src/util/font-discovery.cpp b/src/util/font-discovery.cpp index e48145f5154c99bc5095877c65b91e3b7bc89d12..6dfe20203fd4106305fdeca13ae22d251a5c4161 100644 --- a/src/util/font-discovery.cpp +++ b/src/util/font-discovery.cpp @@ -133,17 +133,18 @@ void sort_fonts_by_name(std::vector& fonts, bool sans_first) { // sort fonts in requested order, in-place void sort_fonts(std::vector& fonts, FontOrder order, bool sans_first) { switch (order) { - case FontOrder::by_name: + case FontOrder::ByName: + case FontOrder::ByFamily: sort_fonts_by_name(fonts, sans_first); break; - case FontOrder::by_weight: + case FontOrder::ByWeight: // there are many repetitions for weight, due to font substitutions, so sort by name first sort_fonts_by_name(fonts, sans_first); std::stable_sort(begin(fonts), end(fonts), [](const FontInfo& a, const FontInfo& b) { return a.weight < b.weight; }); break; - case FontOrder::by_width: + case FontOrder::ByWidth: sort_fonts_by_name(fonts, sans_first); std::stable_sort(begin(fonts), end(fonts), [](const FontInfo& a, const FontInfo& b) { return a.width < b.width; }); break; @@ -154,6 +155,44 @@ void sort_fonts(std::vector& fonts, FontOrder order, bool sans_first) } } +const FontInfo& get_family_font(const std::vector& family) { + assert(!family.empty()); + auto it = std::ranges::find_if(family, [](auto& fam) { + return fam.face && + (fam.face->get_name().raw().find("Regular") != std::string::npos || + fam.face->get_name().raw().find("Normal") != std::string::npos); + }); + if (it != end(family)) { + return *it; + } + return family.front(); +} + +FontInfo& get_family_font(std::vector& family) { + return const_cast(get_family_font(const_cast&>(family))); +} + +void sort_font_families(std::vector>& fonts, bool sans_first) { + std::sort(begin(fonts), end(fonts), [=](const auto& a, const auto& b) { + auto& f1 = get_family_font(a); + auto& f2 = get_family_font(b); + + auto na = f1.ff->get_name(); + auto nb = f2.ff->get_name(); + if (sans_first) { + bool sans_a = f1.synthetic && f1.ff->get_name() == "Sans"; + bool sans_b = f2.synthetic && f2.ff->get_name() == "Sans"; + if (sans_a != sans_b) { + return sans_a; + } + } + // lexicographical order: + return na < nb; + // alphabetical order: + //return na.raw() < nb.raw(); + }); +} + Glib::ustring get_fontspec(const Glib::ustring& family, const Glib::ustring& face, const Glib::ustring& variations) { if (variations.empty()) { return face.empty() ? family : family + ", " + face; @@ -203,35 +242,37 @@ enum FontCacheFlags : int { Synthetic = 0x08, }; -void save_font_cache(const std::vector& fonts) { +void save_font_cache(const std::vector>& fonts) { auto keyfile = Glib::KeyFile::create(); keyfile->set_double(cache_header, "version", cache_version); Glib::ustring weight("weight"); Glib::ustring width("width"); - Glib::ustring family("family"); + Glib::ustring ffamily("family"); Glib::ustring fontflags("flags"); - for (auto&& font : fonts) { - auto desc = get_font_description(font.ff, font.face); - auto group = desc.to_string(); - int flags = FontCacheFlags::Normal; - if (font.monospaced) { - flags |= FontCacheFlags::Monospace; - } - if (font.oblique) { - flags |= FontCacheFlags::Oblique; - } - if (font.variable_font) { - flags |= FontCacheFlags::Variable; - } - if (font.synthetic) { - flags |= FontCacheFlags::Synthetic; + for (auto&& family : fonts) { + for (auto&& font : family) { + auto desc = get_font_description(font.ff, font.face); + auto group = desc.to_string(); + int flags = FontCacheFlags::Normal; + if (font.monospaced) { + flags |= FontCacheFlags::Monospace; + } + if (font.oblique) { + flags |= FontCacheFlags::Oblique; + } + if (font.variable_font) { + flags |= FontCacheFlags::Variable; + } + if (font.synthetic) { + flags |= FontCacheFlags::Synthetic; + } + keyfile->set_double(group, weight, font.weight); + keyfile->set_double(group, width, font.width); + keyfile->set_integer(group, ffamily, font.family_kind); + keyfile->set_integer(group, fontflags, flags); } - keyfile->set_double(group, weight, font.weight); - keyfile->set_double(group, width, font.width); - keyfile->set_integer(group, family, font.family_kind); - keyfile->set_integer(group, fontflags, flags); } std::string filename = Glib::build_filename(Inkscape::IO::Resource::profile_path(), font_cache); @@ -298,8 +339,8 @@ std::vector get_all_fonts() { return fonts; } -std::shared_ptr> get_all_fonts(Async::Progress>& progress) { - auto result = std::make_shared>(); +std::shared_ptr>> get_all_fonts(Async::Progress>& progress) { + auto result = std::make_shared>>(); auto& fonts = *result; auto cache = load_cached_font_info(); @@ -385,10 +426,12 @@ std::shared_ptr> get_all_fonts(Async::Progress #include #include -#include #include #include #include @@ -25,14 +24,23 @@ struct FontInfo { bool oblique = false; // italic or oblique font bool variable_font = false; // this is variable font bool synthetic = false; // this is an alias, like "Sans" or "Monospace" + + bool operator == (const FontInfo&) const = default; }; -enum class FontOrder { by_name, by_weight, by_width }; +enum class FontOrder { + _First = 0, + ByName = 0, + ByWeight, + ByWidth, + ByFamily, + _Last = ByFamily +}; class FontDiscovery : public Util::EnableSingleton> { public: - using FontsPayload = std::shared_ptr>; + using FontsPayload = std::shared_ptr>>; using MessageType = Async::Msg::Message>; sigc::scoped_connection connect_to_fonts(std::function fn); @@ -53,6 +61,13 @@ std::vector get_all_fonts(); // change order void sort_fonts(std::vector& fonts, FontOrder order, bool sans_first); +// sort font families +void sort_font_families(std::vector>& fonts, bool sans_first); + +// get regular font from the family +const FontInfo& get_family_font(const std::vector& family); +FontInfo& get_family_font(std::vector& family); + Pango::FontDescription get_font_description(const Glib::RefPtr& ff, const Glib::RefPtr& face); Glib::ustring get_fontspec(const Glib::ustring& family, const Glib::ustring& face); diff --git a/src/util/font-tags.cpp b/src/util/font-tags.cpp index 7900a8ea2f62719f69c3793ffb4f45bac0d3fb81..1488d74d180e78cd71fd30b637387e29e0b6821f 100644 --- a/src/util/font-tags.cpp +++ b/src/util/font-tags.cpp @@ -32,7 +32,7 @@ void FontTags::add_tag(const FontTag& tag) { _tags.push_back(tag); } -std::set FontTags::get_font_tags(Glib::RefPtr& face) const { +std::set FontTags::get_font_tags(const Glib::RefPtr& face) const { auto it = _map.find(face); if (it != _map.end()) { return it->second; diff --git a/src/util/font-tags.h b/src/util/font-tags.h index 0d2187eccffcf924ddd5e41d8b3135901fc6d315..a076580a48def8efe33cef9e3d41a0a9b87bb841 100644 --- a/src/util/font-tags.h +++ b/src/util/font-tags.h @@ -31,7 +31,7 @@ public: std::vector get_tags() const; void add_tag(const FontTag& tag); - std::set get_font_tags(Glib::RefPtr& face) const; + std::set get_font_tags(const Glib::RefPtr& face) const; void tag_font(Glib::RefPtr& face, std::string tag); const std::vector& get_selected_tags() const;