summaryrefslogtreecommitdiffstats
path: root/lib/gs-category.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gs-category.c')
-rw-r--r--lib/gs-category.c724
1 files changed, 724 insertions, 0 deletions
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 <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+
+ */
+
+/**
+ * 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 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);
+}