1
0
Fork 0
gnome-software/lib/gs-category.c
Daniel Baumann 68ee05b3fd
Adding upstream version 48.2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 21:00:23 +02:00

724 lines
18 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- 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 <richard@hughsie.com>
* Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
* Copyright (C) 2015 Kalev Lember <klember@redhat.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
/**
* 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 <glib/gi18n.h>
#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 apps 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 its 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 dont
* 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);
}