fontchooser: Add user filtering

Add a popover that has filtering options. As a start,
allow filtering by monospace and by language coverage.
This commit is contained in:
Matthias Clasen 2020-09-08 19:51:12 -04:00
parent 0485a6c008
commit 09604eb3eb
2 changed files with 348 additions and 8 deletions

View File

@ -56,6 +56,12 @@
#include "gtkflattenlistmodel.h"
#include "gtkslicelistmodel.h"
#include "gtkmaplistmodel.h"
#include "gtklistitem.h"
#include "gtksignallistitemfactory.h"
#include "gtkstringlist.h"
#include "gtklistview.h"
#include "gtksortlistmodel.h"
#include "gtkstringsorter.h"
#include <hb-ot.h>
#if defined(HAVE_PANGOFT) && defined(HAVE_HARFBUZZ)
@ -104,6 +110,7 @@ struct _GtkFontChooserWidget
GtkWidget *list_stack;
GtkSingleSelection *selection;
GtkCustomFilter *custom_filter;
GtkCustomFilter *user_filter;
GtkFilterListModel *filter_model;
GtkWidget *preview;
@ -121,6 +128,14 @@ struct _GtkFontChooserWidget
GtkWidget *axis_grid;
GtkWidget *feature_box;
GtkWidget *language_list;
GtkStringList *languages;
GHashTable *language_table;
PangoLanguage *filter_language;
gboolean filter_by_language;
gboolean filter_by_monospace;
PangoFontMap *font_map;
PangoFontDescription *font_desc;
@ -325,6 +340,94 @@ output_cb (GtkSpinButton *spin,
return TRUE;
}
static gboolean
user_filter_cb (gpointer item,
gpointer data)
{
GtkFontChooserWidget *self = GTK_FONT_CHOOSER_WIDGET (data);
PangoFontFamily *family;
PangoFontFace *face;
if (PANGO_IS_FONT_FAMILY (item))
{
family = item;
face = pango_font_family_get_face (family, NULL);
}
else
{
face = PANGO_FONT_FACE (item);
family = pango_font_face_get_family (face);
}
if (self->filter_by_monospace &&
!pango_font_family_is_monospace (family))
return FALSE;
if (self->filter_by_language &&
self->filter_language)
{
PangoFontDescription *desc;
PangoContext *context;
PangoFont *font;
gboolean ret;
desc = pango_font_face_describe (face);
pango_font_description_set_size (desc, 20);
context = gtk_widget_get_pango_context (GTK_WIDGET (self));
font = pango_context_load_font (context, desc);
ret = TRUE;
if (PANGO_IS_FC_FONT (font))
{
PangoLanguage **langs;
int i;
ret = FALSE;
langs = pango_fc_font_get_languages (PANGO_FC_FONT (font));
for (i = 0; langs[i]; i++)
{
if (langs[i] == self->filter_language)
{
ret = TRUE;
break;
}
}
}
g_object_unref (font);
pango_font_description_free (desc);
return ret;
}
return TRUE;
}
static void
monospace_check_changed (GtkCheckButton *check,
GParamSpec *pspec,
GtkFontChooserWidget *self)
{
self->filter_by_monospace = gtk_check_button_get_active (check);
gtk_filter_changed (GTK_FILTER (self->user_filter),
self->filter_by_monospace ? GTK_FILTER_CHANGE_MORE_STRICT
: GTK_FILTER_CHANGE_LESS_STRICT);
}
static void
language_check_changed (GtkCheckButton *check,
GParamSpec *pspec,
GtkFontChooserWidget *self)
{
self->filter_by_language = gtk_check_button_get_active (check);
gtk_filter_changed (GTK_FILTER (self->user_filter),
self->filter_by_language ? GTK_FILTER_CHANGE_MORE_STRICT
: GTK_FILTER_CHANGE_LESS_STRICT);
}
static void
gtk_font_chooser_widget_update_marks (GtkFontChooserWidget *self)
{
@ -749,6 +852,7 @@ gtk_font_chooser_widget_dispose (GObject *object)
g_clear_pointer (&self->filter_data, self->filter_data_destroy);
g_clear_pointer (&self->stack, gtk_widget_unparent);
g_clear_pointer (&self->language_table, g_hash_table_unref);
G_OBJECT_CLASS (gtk_font_chooser_widget_parent_class)->dispose (object);
}
@ -801,6 +905,7 @@ gtk_font_chooser_widget_class_init (GtkFontChooserWidgetClass *klass)
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, filter_model);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, selection);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, custom_filter);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, user_filter);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, preview);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, preview2);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, size_label);
@ -812,6 +917,7 @@ gtk_font_chooser_widget_class_init (GtkFontChooserWidgetClass *klass)
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, font_name_label);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, feature_box);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, axis_grid);
gtk_widget_class_bind_template_child (widget_class, GtkFontChooserWidget, language_list);
gtk_widget_class_bind_template_callback (widget_class, get_font_name);
gtk_widget_class_bind_template_callback (widget_class, get_font_attributes);
@ -822,6 +928,8 @@ gtk_font_chooser_widget_class_init (GtkFontChooserWidgetClass *klass)
gtk_widget_class_bind_template_callback (widget_class, output_cb);
gtk_widget_class_bind_template_callback (widget_class, selection_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, resize_by_scroll_cb);
gtk_widget_class_bind_template_callback (widget_class, monospace_check_changed);
gtk_widget_class_bind_template_callback (widget_class, language_check_changed);
gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
gtk_widget_class_set_css_name (widget_class, I_("fontchooser"));
@ -896,6 +1004,76 @@ axis_free (gpointer v)
g_free (a);
}
static void
select_added (GListModel *model,
guint position,
guint removed,
guint added,
gpointer data)
{
GtkSingleSelection *selection = GTK_SINGLE_SELECTION (model);
g_assert (removed == 0);
g_assert (added == 1);
gtk_single_selection_set_selected (selection, position);
}
static void
add_languages_from_font (GtkFontChooserWidget *self,
gpointer item)
{
PangoFontFace *face;
PangoFontDescription *desc;
PangoFont *font;
PangoContext *context;
if (PANGO_IS_FONT_FAMILY (item))
face = pango_font_family_get_face (PANGO_FONT_FAMILY (item), NULL);
else
face = PANGO_FONT_FACE (item);
desc = pango_font_face_describe (face);
pango_font_description_set_size (desc, 20);
context = gtk_widget_get_pango_context (GTK_WIDGET (self));
font = pango_context_load_font (context, desc);
if (PANGO_IS_FC_FONT (font))
{
GtkSelectionModel *model = gtk_list_view_get_model (GTK_LIST_VIEW (self->language_list));
PangoLanguage *default_lang = pango_language_get_default ();
PangoLanguage **langs;
int i;
langs = pango_fc_font_get_languages (PANGO_FC_FONT (font));
for (i = 0; langs[i]; i++)
{
if (!g_hash_table_contains (self->language_table, langs[i]))
{
g_hash_table_add (self->language_table, langs[i]);
if (get_language_name (langs[i]))
{
const char *l = pango_language_to_string (langs[i]);
gulong id = 0;
/* Pre-select the default language */
if (pango_language_matches (default_lang, l))
id = g_signal_connect (model, "items-changed", G_CALLBACK (select_added), NULL);
gtk_string_list_append (self->languages, l);
if (id)
g_signal_handler_disconnect (model, id);
}
}
}
}
g_object_unref (font);
pango_font_description_free (desc);
}
/* We incrementally populate our fontlist to prevent blocking
* the font chooser for a long time with expensive FcFontSort
* calls in pango for every row in the list).
@ -908,7 +1086,7 @@ add_to_fontlist (GtkWidget *widget,
GtkFontChooserWidget *self = GTK_FONT_CHOOSER_WIDGET (widget);
GtkSliceListModel *model = user_data;
GListModel *child_model;
guint n;
guint i, n;
if (gtk_filter_list_model_get_model (self->filter_model) != G_LIST_MODEL (model))
return G_SOURCE_REMOVE;
@ -917,6 +1095,15 @@ add_to_fontlist (GtkWidget *widget,
n = gtk_slice_list_model_get_size (model);
for (i = n; i < n + 10; i++)
{
gpointer item = g_list_model_get_item (child_model, i);
if (!item)
break;
add_languages_from_font (self, item);
g_object_unref (item);
}
n += 10;
if (n >= g_list_model_get_n_items (child_model))
@ -952,6 +1139,93 @@ update_fontlist (GtkFontChooserWidget *self)
g_object_unref (model);
}
static void
setup_lang_item (GtkSignalListItemFactory *factory,
gpointer item,
gpointer data)
{
GtkWidget *label;
label = gtk_label_new (NULL);
gtk_label_set_xalign (GTK_LABEL (label), 0);
gtk_list_item_set_child (GTK_LIST_ITEM (item), label);
}
static void
bind_lang_item (GtkSignalListItemFactory *factory,
gpointer item,
gpointer data)
{
GtkWidget *label;
gpointer obj;
const char *str;
PangoLanguage *language;
const char *name;
obj = gtk_list_item_get_item (GTK_LIST_ITEM (item));
str = gtk_string_object_get_string (GTK_STRING_OBJECT (obj));
language = pango_language_from_string (str);
name = get_language_name (language);
label = gtk_list_item_get_child (GTK_LIST_ITEM (item));
gtk_label_set_label (GTK_LABEL (label), name);
}
static char *
get_lang_name (gpointer this,
const char *lang)
{
return g_strdup (get_language_name (pango_language_from_string (lang)));
}
static void
language_selection_changed (GtkSelectionModel *model,
guint position,
guint n_items,
GtkFontChooserWidget *self)
{
gpointer obj;
obj = gtk_single_selection_get_selected_item (GTK_SINGLE_SELECTION (model));
if (obj)
self->filter_language = pango_language_from_string (gtk_string_object_get_string (obj));
else
self->filter_language = NULL;
if (self->filter_by_language)
gtk_filter_changed (GTK_FILTER (self->user_filter), GTK_FILTER_CHANGE_DIFFERENT);
}
static void
setup_language_list (GtkFontChooserWidget *self)
{
GtkListItemFactory *factory;
GtkExpression *expression;
GListModel *model;
GtkSelectionModel *selection;
self->languages = gtk_string_list_new (NULL);
self->language_table = g_hash_table_new (NULL, NULL);
expression = gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string");
expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL, 1, &expression, (GCallback)get_lang_name, NULL, NULL);
model = G_LIST_MODEL (gtk_sort_list_model_new (G_LIST_MODEL (self->languages),
GTK_SORTER (gtk_string_sorter_new (expression))));
selection = GTK_SELECTION_MODEL (gtk_single_selection_new (model));
g_signal_connect (selection, "selection-changed", G_CALLBACK (language_selection_changed), self);
gtk_list_view_set_model (GTK_LIST_VIEW (self->language_list), selection);
g_object_unref (selection);
factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", G_CALLBACK (setup_lang_item), self);
g_signal_connect (factory, "bind", G_CALLBACK (bind_lang_item), self);
gtk_list_view_set_factory (GTK_LIST_VIEW (self->language_list), factory);
g_object_unref (factory);
}
static void
gtk_font_chooser_widget_init (GtkFontChooserWidget *self)
{
@ -988,6 +1262,9 @@ gtk_font_chooser_widget_init (GtkFontChooserWidget *self)
gtk_font_chooser_widget_populate_features (self);
gtk_font_chooser_widget_take_font_desc (self, NULL);
gtk_custom_filter_set_filter_func (self->user_filter, user_filter_cb, self, NULL);
setup_language_list (self);
}
/**

View File

@ -5,6 +5,7 @@
<signal name="items-changed" handler="rows_changed_cb" object="GtkFontChooserWidget" swapped="1" />
<property name="model">
<object class="GtkFilterListModel" id="filter_model">
<property name="incremental">1</property>
<property name="filter">
<object class="GtkEveryFilter">
<child>
@ -18,8 +19,10 @@
</object>
</child>
<child>
<object class="GtkCustomFilter" id="custom_filter">
</object>
<object class="GtkCustomFilter" id="custom_filter"/>
</child>
<child>
<object class="GtkCustomFilter" id="user_filter"/>
</child>
</object>
</property>
@ -49,11 +52,71 @@
<property name="row-spacing">6</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="hexpand">1</property>
<property name="activates-default">1</property>
<property name="placeholder-text" translatable="yes">Search font name</property>
<signal name="stop-search" handler="stop_search_cb" swapped="no"/>
<object class="GtkBox">
<style>
<class name="linked"/>
</style>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="hexpand">1</property>
<property name="activates-default">1</property>
<property name="placeholder-text" translatable="yes">Search font name</property>
<signal name="stop-search" handler="stop_search_cb" swapped="no"/>
</object>
</child>
<child>
<object class="GtkMenuButton">
<property name="icon-name">pan-down-symbolic</property>
<property name="popover">
<object class="GtkPopover">
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Filter by</property>
<property name="width-chars">20</property>
<property name="margin-bottom">10</property>
<style>
<class name="title-4"/>
</style>
</object>
</child>
<child>
<object class="GtkCheckButton" id="monospace_button">
<property name="label" translatable="yes">Monospace</property>
<signal name="notify::active" handler="monospace_check_changed"/>
</object>
</child>
<child>
<object class="GtkCheckButton" id="language_button">
<property name="label" translatable="yes">Language</property>
<signal name="notify::active" handler="language_check_changed"/>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="margin-start">12</property>
<child>
<object class="GtkScrolledWindow">
<property name="min-content-height">200</property>
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<child>
<object class="GtkListView" id="language_list">
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
<layout>
<property name="column">0</property>
<property name="row">0</property>