From 6f0f7d1b40a8fa8d46a2d6f4317600001cdbbb18 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:57:27 +0200 Subject: Adding upstream version 43.5. Signed-off-by: Daniel Baumann --- lib/gs-category.c | 724 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 724 insertions(+) create mode 100644 lib/gs-category.c (limited to 'lib/gs-category.c') diff --git a/lib/gs-category.c b/lib/gs-category.c new file mode 100644 index 0000000..befc167 --- /dev/null +++ b/lib/gs-category.c @@ -0,0 +1,724 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes + * Copyright (C) 2013 Matthias Clasen + * Copyright (C) 2015 Kalev Lember + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-category + * @short_description: An category that contains applications + * + * This object provides functionality that allows a plugin to create + * a tree structure of categories that each contain #GsApp's. + */ + +#include "config.h" + +#include + +#include "gs-category-private.h" +#include "gs-desktop-data.h" + +struct _GsCategory +{ + GObject parent_instance; + + const GsDesktopData *desktop_data; /* NULL for subcategories */ + const GsDesktopMap *desktop_map; /* NULL for parent categories */ + + GPtrArray *desktop_groups; /* potentially NULL if empty */ + GsCategory *parent; + guint size; /* (atomic) */ + GPtrArray *children; /* potentially NULL if empty */ +}; + +G_DEFINE_TYPE (GsCategory, gs_category, G_TYPE_OBJECT) + +typedef enum { + PROP_ID = 1, + PROP_NAME, + PROP_ICON_NAME, + PROP_SCORE, + PROP_PARENT, + PROP_SIZE, +} GsCategoryProperty; + +static GParamSpec *obj_props[PROP_SIZE + 1] = { NULL, }; + +/** + * gs_category_to_string: + * @category: a #GsCategory + * + * Returns a string representation of the category + * + * Returns: a string + * + * Since: 3.22 + **/ +gchar * +gs_category_to_string (GsCategory *category) +{ + guint i; + GString *str = g_string_new (NULL); + g_string_append_printf (str, "GsCategory[%p]:\n", category); + g_string_append_printf (str, " id: %s\n", + gs_category_get_id (category)); + if (gs_category_get_name (category) != NULL) { + g_string_append_printf (str, " name: %s\n", + gs_category_get_name (category)); + } + if (gs_category_get_icon_name (category) != NULL) { + g_string_append_printf (str, " icon-name: %s\n", + gs_category_get_icon_name (category)); + } + g_string_append_printf (str, " size: %u\n", + gs_category_get_size (category)); + g_string_append_printf (str, " desktop-groups: %u\n", + (category->desktop_groups != NULL) ? category->desktop_groups->len : 0); + if (category->parent != NULL) { + g_string_append_printf (str, " parent: %s\n", + gs_category_get_id (category->parent)); + } + g_string_append_printf (str, " score: %i\n", gs_category_get_score (category)); + if (category->children == NULL || category->children->len == 0) { + g_string_append_printf (str, " children: %u\n", 0u); + } else { + g_string_append_printf (str, " children: %u\n", category->children->len); + for (i = 0; i < category->children->len; i++) { + GsCategory *child = g_ptr_array_index (category->children, i); + g_string_append_printf (str, " - %s\n", + gs_category_get_id (child)); + } + } + return g_string_free (str, FALSE); +} + +/** + * gs_category_get_size: + * @category: a #GsCategory + * + * Returns how many applications the category could contain. + * + * NOTE: This may over-estimate the number if duplicate applications are + * filtered or core applications are not shown. + * + * Returns: the number of apps in the category + * + * Since: 3.22 + **/ +guint +gs_category_get_size (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), 0); + + /* The ‘all’ subcategory is a bit special. */ + if (category->parent != NULL && g_str_equal (gs_category_get_id (category), "all")) + return gs_category_get_size (category->parent); + + return g_atomic_int_get (&category->size); +} + +/** + * gs_category_set_size: + * @category: a #GsCategory + * @size: the number of applications + * + * Sets the number of applications in the category. + * Most plugins do not need to call this function. + * + * Since: 3.22 + **/ +void +gs_category_set_size (GsCategory *category, guint size) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + + g_atomic_int_set (&category->size, size); + g_object_notify_by_pspec (G_OBJECT (category), obj_props[PROP_SIZE]); +} + +/** + * gs_category_increment_size: + * @category: a #GsCategory + * @value: how many to add + * + * Adds @value to the size count. + * + * Since: 3.22 + **/ +void +gs_category_increment_size (GsCategory *category, + guint value) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + + g_atomic_int_add (&category->size, value); + if (value != 0) + g_object_notify_by_pspec (G_OBJECT (category), obj_props[PROP_SIZE]); +} + +/** + * gs_category_get_id: + * @category: a #GsCategory + * + * Gets the category ID. + * + * Returns: the string, e.g. "other" + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_id (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + if (category->desktop_data != NULL) + return category->desktop_data->id; + else if (category->desktop_map != NULL) + return category->desktop_map->id; + g_assert_not_reached (); +} + +/** + * gs_category_get_name: + * @category: a #GsCategory + * + * Gets the category name. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_name (GsCategory *category) +{ + const gchar *category_id; + + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + category_id = gs_category_get_id (category); + + /* special case, we don't want translations in the plugins */ + if (g_strcmp0 (category_id, "other") == 0) { + /* TRANSLATORS: this is where all applications that don't + * fit in other groups are put */ + return _("Other"); + } + if (g_strcmp0 (category_id, "all") == 0) { + /* TRANSLATORS: this is a subcategory matching all the + * different apps in the parent category, e.g. "Games" */ + return C_("Category", "All"); + } + if (g_strcmp0 (category_id, "featured") == 0) { + /* TRANSLATORS: this is a subcategory of featured apps */ + return _("Featured"); + } + + /* normal case */ + if (category->desktop_data != NULL) { + return gettext (category->desktop_data->name); + } else if (category->desktop_map != NULL) { + g_autofree gchar *msgctxt = g_strdup_printf ("Menu of %s", category->parent->desktop_data->name); + return g_dpgettext2 (GETTEXT_PACKAGE, msgctxt, category->desktop_map->name); + } + + g_assert_not_reached (); +} + +/** + * gs_category_get_icon_name: + * @category: a #GsCategory + * + * Gets the category icon name. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_icon_name (GsCategory *category) +{ + const gchar *category_id; + + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + category_id = gs_category_get_id (category); + + /* special case */ + if (g_strcmp0 (category_id, "other") == 0) + return "emblem-system-symbolic"; + if (g_strcmp0 (category_id, "all") == 0) + return "emblem-default-symbolic"; + if (g_strcmp0 (category_id, "featured") == 0) + return "emblem-favorite-symbolic"; + + if (category->desktop_data != NULL) + return category->desktop_data->icon; + else + return NULL; +} + +/** + * gs_category_get_score: + * @category: a #GsCategory + * + * Gets if the category score. + * Important categories may be shown before other categories, or tagged in a + * different way, for example with color or in a different section. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +gint +gs_category_get_score (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE); + + if (category->desktop_data != NULL) + return category->desktop_data->score; + else + return 0; +} + +/** + * gs_category_get_desktop_groups: + * @category: a #GsCategory + * + * Gets the list of AppStream groups for the category. + * + * Returns: (element-type utf8) (transfer none): An array + * + * Since: 3.22 + **/ +GPtrArray * +gs_category_get_desktop_groups (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + if (category->desktop_groups == NULL) + category->desktop_groups = g_ptr_array_new_with_free_func (g_free); + + return category->desktop_groups; +} + +/** + * gs_category_has_desktop_group: + * @category: a #GsCategory + * @desktop_group: a group of categories found in AppStream, e.g. "AudioVisual::Player" + * + * Finds out if the category has the specific AppStream desktop group. + * + * Returns: %TRUE if found, %FALSE otherwise + * + * Since: 3.22 + **/ +gboolean +gs_category_has_desktop_group (GsCategory *category, const gchar *desktop_group) +{ + guint i; + + g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE); + g_return_val_if_fail (desktop_group != NULL, FALSE); + + if (category->desktop_groups == NULL) + return FALSE; + + for (i = 0; i < category->desktop_groups->len; i++) { + const gchar *tmp = g_ptr_array_index (category->desktop_groups, i); + if (g_strcmp0 (tmp, desktop_group) == 0) + return TRUE; + } + return FALSE; +} + +/* + * gs_category_add_desktop_group: + * @category: a #GsCategory + * @desktop_group: a group of categories found in AppStream, e.g. "AudioVisual::Player" + * + * Adds a desktop group to the category. + * A desktop group is a set of category strings that all must exist. + * + * Since: 3.22 + */ +static void +gs_category_add_desktop_group (GsCategory *category, const gchar *desktop_group) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + g_return_if_fail (desktop_group != NULL); + + /* add if not already found, and lazily create the groups array + * (since it’s only needed in child categories) */ + if (gs_category_has_desktop_group (category, desktop_group)) + return; + if (category->desktop_groups == NULL) + category->desktop_groups = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (category->desktop_groups, g_strdup (desktop_group)); +} + +/** + * gs_category_find_child: + * @category: a #GsCategory + * @id: a category ID, e.g. "other" + * + * Find a child category with a specific ID. + * + * Returns: (transfer none): the #GsCategory, or %NULL + * + * Since: 3.22 + **/ +GsCategory * +gs_category_find_child (GsCategory *category, const gchar *id) +{ + GsCategory *tmp; + guint i; + + if (category->children == NULL) + return NULL; + + /* find the subcategory */ + for (i = 0; i < category->children->len; i++) { + tmp = GS_CATEGORY (g_ptr_array_index (category->children, i)); + if (g_strcmp0 (id, gs_category_get_id (tmp)) == 0) + return tmp; + } + return NULL; +} + +/** + * gs_category_get_parent: + * @category: a #GsCategory + * + * Gets the parent category. + * + * Returns: the #GsCategory or %NULL + * + * Since: 3.22 + **/ +GsCategory * +gs_category_get_parent (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + return category->parent; +} + +/** + * gs_category_get_children: + * @category: a #GsCategory + * + * Gets the list if children for a category. + * + * Return value: (element-type GsApp) (transfer none): A list of children + * + * Since: 3.22 + **/ +GPtrArray * +gs_category_get_children (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + if (category->children == NULL) + category->children = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + return category->children; +} + +/* + * gs_category_add_child: + * @category: a #GsCategory + * @subcategory: a #GsCategory + * + * Adds a child category to a parent category. + * + * Since: 3.22 + */ +static void +gs_category_add_child (GsCategory *category, GsCategory *subcategory) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + g_return_if_fail (GS_IS_CATEGORY (subcategory)); + + /* lazily create the array to save memory in subcategories, which don’t + * recursively have children */ + if (category->children == NULL) + category->children = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + + /* FIXME: do we need this? */ + subcategory->parent = category; + g_object_add_weak_pointer (G_OBJECT (subcategory->parent), + (gpointer *) &subcategory->parent); + + g_ptr_array_add (category->children, + g_object_ref (subcategory)); +} + +static gchar * +gs_category_get_sort_key (GsCategory *category) +{ + guint sort_order = 5; + if (g_strcmp0 (gs_category_get_id (category), "featured") == 0) + sort_order = 0; + else if (g_strcmp0 (gs_category_get_id (category), "all") == 0) + sort_order = 2; + else if (g_strcmp0 (gs_category_get_id (category), "other") == 0) + sort_order = 9; + return g_strdup_printf ("%u:%s", + sort_order, + gs_category_get_name (category)); +} + +static gint +gs_category_sort_children_cb (gconstpointer a, gconstpointer b) +{ + GsCategory *ca = GS_CATEGORY (*(GsCategory **) a); + GsCategory *cb = GS_CATEGORY (*(GsCategory **) b); + g_autofree gchar *id_a = gs_category_get_sort_key (ca); + g_autofree gchar *id_b = gs_category_get_sort_key (cb); + return g_strcmp0 (id_a, id_b); +} + +/** + * gs_category_sort_children: + * @category: a #GsCategory + * + * Sorts the list of children. + * + * Since: 3.22 + **/ +void +gs_category_sort_children (GsCategory *category) +{ + if (category->children == NULL) + return; + + g_ptr_array_sort (category->children, + gs_category_sort_children_cb); +} + +static void +gs_category_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsCategory *self = GS_CATEGORY (object); + + switch ((GsCategoryProperty) prop_id) { + case PROP_ID: + g_value_set_string (value, gs_category_get_id (self)); + break; + case PROP_NAME: + g_value_set_string (value, gs_category_get_name (self)); + break; + case PROP_ICON_NAME: + g_value_set_string (value, gs_category_get_icon_name (self)); + break; + case PROP_SCORE: + g_value_set_int (value, gs_category_get_score (self)); + break; + case PROP_PARENT: + g_value_set_object (value, self->parent); + break; + case PROP_SIZE: + g_value_set_uint (value, gs_category_get_size (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_category_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsCategory *self = GS_CATEGORY (object); + + switch ((GsCategoryProperty) prop_id) { + case PROP_ID: + case PROP_NAME: + case PROP_ICON_NAME: + case PROP_SCORE: + case PROP_PARENT: + /* Read only */ + g_assert_not_reached (); + break; + case PROP_SIZE: + gs_category_set_size (self, g_value_get_uint (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_category_finalize (GObject *object) +{ + GsCategory *category = GS_CATEGORY (object); + + if (category->parent != NULL) + g_object_remove_weak_pointer (G_OBJECT (category->parent), + (gpointer *) &category->parent); + g_clear_pointer (&category->children, g_ptr_array_unref); + g_clear_pointer (&category->desktop_groups, g_ptr_array_unref); + + G_OBJECT_CLASS (gs_category_parent_class)->finalize (object); +} + +static void +gs_category_class_init (GsCategoryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gs_category_get_property; + object_class->set_property = gs_category_set_property; + object_class->finalize = gs_category_finalize; + + /** + * GsCategory:id: + * + * A machine readable identifier for the category. Must be non-empty + * and in a valid format to be a + * [desktop category ID](https://specifications.freedesktop.org/menu-spec/latest/). + * + * Since: 40 + */ + obj_props[PROP_ID] = + g_param_spec_string ("id", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:name: + * + * Human readable name for the category. + * + * Since: 40 + */ + obj_props[PROP_NAME] = + g_param_spec_string ("name", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:icon-name: (nullable) + * + * Name of the icon to use for the category, or %NULL if none is set. + * + * Since: 40 + */ + obj_props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", NULL, NULL, + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:score: + * + * Score for sorting the category. Lower numeric values indicate more + * important categories. + * + * Since: 40 + */ + obj_props[PROP_SCORE] = + g_param_spec_int ("score", NULL, NULL, + G_MININT, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:parent: (nullable) + * + * The parent #GsCategory, or %NULL if this category is at the top + * level. + * + * Since: 40 + */ + obj_props[PROP_PARENT] = + g_param_spec_object ("parent", NULL, NULL, + GS_TYPE_CATEGORY, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GsCategory:size: + * + * Number of apps in this category, including apps in its subcategories. + * + * This has to be initialised externally to the #GsCategory by calling + * gs_category_increment_size(). + * + * Since: 40 + */ + obj_props[PROP_SIZE] = + g_param_spec_uint ("size", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); +} + +static void +gs_category_init (GsCategory *category) +{ +} + +/** + * gs_category_new_for_desktop_data: + * @data: data for the category, which must be static and constant + * + * Create a new #GsCategory instance which wraps the desktop category + * information in @data. Where possible, the static data will be reused, so + * @data must be static and constant across the lifetime of the process. + * + * Returns: (transfer full): a new #GsCategory wrapping @data + * Since: 40 + */ +GsCategory * +gs_category_new_for_desktop_data (const GsDesktopData *data) +{ + g_autoptr(GsCategory) category = NULL; + GsCategory *subcategory_all = NULL; + + /* parent category */ + category = g_object_new (GS_TYPE_CATEGORY, NULL); + category->desktop_data = data; + + /* add subcategories */ + for (gsize j = 0; data->mapping[j].id != NULL; j++) { + const GsDesktopMap *map = &data->mapping[j]; + g_autoptr(GsCategory) sub = g_object_new (GS_TYPE_CATEGORY, NULL); + sub->desktop_map = map; + for (gsize k = 0; map->fdo_cats[k] != NULL; k++) + gs_category_add_desktop_group (sub, map->fdo_cats[k]); + gs_category_add_child (category, sub); + + if (g_str_equal (map->id, "all")) + subcategory_all = sub; + } + + /* set up the ‘all’ subcategory specially, adding all the desktop groups + * from all other child categories to it */ + if (subcategory_all != NULL) { + g_assert (category->children != NULL); + + for (guint i = 0; i < category->children->len; i++) { + GPtrArray *desktop_groups; + GsCategory *child; + + /* ignore the all category */ + child = g_ptr_array_index (category->children, i); + if (child == subcategory_all) + continue; + + /* add all desktop groups */ + desktop_groups = gs_category_get_desktop_groups (child); + for (guint j = 0; j < desktop_groups->len; j++) { + const gchar *tmp = g_ptr_array_index (desktop_groups, j); + gs_category_add_desktop_group (subcategory_all, tmp); + } + } + } + + return g_steal_pointer (&category); +} -- cgit v1.2.3