/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2012-2024 Matthias Klumpp * Copyright (C) 2015-2016 Richard Hughes * * Licensed under the GNU Lesser General Public License Version 2.1 * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the license, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . */ #include "as-category.h" #include "as-category-gi.h" #include "config.h" #include #include #include "as-component.h" #include "as-utils-private.h" /** * SECTION:as-category * @short_description: Representation of a XDG category * @include: appstream.h * * This object represents a XDG category, as defined at: * https://specifications.freedesktop.org/menu-spec/latest/category-registry.html * * The #AsCategory object does not support all aspects of a menu. Its main purpose * is to be used in software-centers to group visual components (gui/web applications). * * You can use %as_get_default_categories() to get a set of supported default categories. * * See also: #AsComponent */ #ifdef __clang__ #pragma clang diagnostic ignored "-Wmissing-field-initializers" #endif typedef struct { const gchar *id; const gchar *name; const gchar *fdo_cats[16]; } AsCategoryMap; typedef struct { const gchar *id; const AsCategoryMap *mapping; const gchar *name; const gchar *icon; } AsCategoryData; /* clang-format off */ /* AudioVideo */ static const AsCategoryMap map_audiovideo[] = { { "featured", NC_("Category of AudioVideo", "Featured"), { "AudioVideo::Featured", NULL} }, { "creation-editing", NC_("Category of AudioVideo", "Audio Creation & Editing"), { "AudioVideo::AudioVideoEditing", "AudioVideo::Midi", "AudioVideo::DiscBurning", "AudioVideo::Sequencer", NULL} }, { "music-players", NC_("Category of AudioVideo", "Music Players"), { "AudioVideo::Music", "AudioVideo::Player", NULL} }, { NULL } }; /* Development */ static const AsCategoryMap map_developertools[] = { { "featured", NC_("Category of Development", "Featured"), { "Development::Featured", NULL} }, { "debuggers", NC_("Category of Development", "Debuggers"), { "Development:Debugger", NULL} }, { "ide", NC_("Category of Development", "IDEs"), { "Development::IDE", "Development::GUIDesigner", NULL} }, { NULL } }; /* Education */ static const AsCategoryMap map_education[] = { { "featured", NC_("Category of Education", "Featured"), { "Education::Featured", NULL} }, { "astronomy", NC_("Category of Education", "Astronomy"), { "Education::Astronomy", NULL} }, { "chemistry", NC_("Category of Education", "Chemistry"), { "Education::Chemistry", NULL} }, { "languages", NC_("Category of Education", "Languages"), { "Education::Languages", "Education::Literature", NULL} }, { "math", NC_("Category of Education", "Math"), { "Education::Math", "Education::NumericalAnalysis", NULL} }, { NULL } }; /* Games */ static const AsCategoryMap map_games[] = { { "featured", NC_("Category of Games", "Featured"), { "Game::Featured", NULL} }, { "action", NC_("Category of Games", "Action"), { "Game::ActionGame", NULL} }, { "adventure", NC_("Category of Games", "Adventure"), { "Game::AdventureGame", NULL} }, { "arcade", NC_("Category of Games", "Arcade"), { "Game::ArcadeGame", NULL} }, { "blocks", NC_("Category of Games", "Blocks"), { "Game::BlocksGame", NULL} }, { "board", NC_("Category of Games", "Board"), { "Game::BoardGame", NULL} }, { "card", NC_("Category of Games", "Card"), { "Game::CardGame", NULL} }, { "emulator", NC_("Category of Games", "Emulators"), { "Game::Emulator", NULL} }, { "kids", NC_("Category of Games", "Kids"), { "Game::KidsGame", NULL} }, { "logic", NC_("Category of Games", "Logic"), { "Game::LogicGame", NULL} }, { "role-playing", NC_("Category of Games", "Role Playing"), { "Game::RolePlaying", NULL} }, { "sports", NC_("Category of Games", "Sports"), { "Game::SportsGame", "Game::Simulation", NULL} }, { "strategy", NC_("Category of Games", "Strategy"), { "Game::StrategyGame", NULL} }, { NULL } }; /* Graphics */ static const AsCategoryMap map_graphics[] = { { "featured", NC_("Category of Graphics", "Featured"), { "Graphics::Featured", NULL} }, { "3d", NC_("Category of Graphics", "3D Graphics"), { "Graphics::3DGraphics", NULL} }, { "photography", NC_("Category of Graphics", "Photography"), { "Graphics::Photography", NULL} }, { "scanning", NC_("Category of Graphics", "Scanning"), { "Graphics::Scanning", NULL} }, { "vector", NC_("Category of Graphics", "Vector Graphics"), { "Graphics::VectorGraphics", NULL} }, { "viewers", NC_("Category of Graphics", "Viewers"), { "Graphics::Viewer", NULL} }, { NULL } }; /* Office */ static const AsCategoryMap map_office[] = { { "featured", NC_("Category of Office", "Featured"), { "Office::Featured", NULL} }, { "calendar", NC_("Category of Office", "Calendar"), { "Office::Calendar", "Office::ProjectManagement", NULL} }, { "database", NC_("Category of Office", "Database"), { "Office::Database", NULL} }, { "finance", NC_("Category of Office", "Finance"), { "Office::Finance", "Office::Spreadsheet", NULL} }, { "word-processor", NC_("Category of Office", "Word Processor"), { "Office::WordProcessor", "Office::Dictionary", NULL} }, { NULL } }; /* Addons */ static const AsCategoryMap map_addons[] = { { "fonts", NC_("Category of Addons", "Fonts"), { "Addons::Fonts", NULL} }, { "codecs", NC_("Category of Addons", "Codecs"), { "Addons::Codecs", NULL} }, { "input-sources", NC_("Category of Addons", "Input Sources"), { "Addons::InputSources", NULL} }, { "language-packs", NC_("Category of Addons", "Language Packs"), { "Addons::LanguagePacks", NULL} }, { "localization", NC_("Category of Addons", "Localization"), { "Addons::Localization", NULL} }, { NULL } }; /* Science */ static const AsCategoryMap map_science[] = { { "featured", NC_("Category of Science", "Featured"), { "Science::Featured", NULL} }, { "artificial-intelligence", NC_("Category of Science", "Artificial Intelligence"), { "Science::ArtificialIntelligence", NULL} }, { "astronomy", NC_("Category of Science", "Astronomy"), { "Science::Astronomy", NULL} }, { "chemistry", NC_("Category of Science", "Chemistry"), { "Science::Chemistry", NULL} }, { "math", NC_("Category of Science", "Math"), { "Science::Math", "Science::Physics", "Science::NumericalAnalysis", NULL} }, { "robotics", NC_("Category of Science", "Robotics"), { "Science::Robotics", NULL} }, { NULL } }; /* Communication */ static const AsCategoryMap map_communication[] = { { "featured", NC_("Category of Communication", "Featured"), { "Network::Featured", NULL} }, { "chat", NC_("Category of Communication", "Chat"), { "Network::Chat", "Network::IRCClient", "Network::Telephony", "Network::VideoConference", "Network::Email", NULL} }, { "news", NC_("Category of Communication", "News"), { "Network::Feed", "Network::News", NULL} }, { "web-browsers", NC_("Category of Communication", "Web Browsers"), { "Network::WebBrowser", NULL} }, { NULL } }; /* Utility */ static const AsCategoryMap map_utilities[] = { { "featured", NC_("Category of Utility", "Featured"), { "Utility::Featured", NULL} }, { "text-editors", NC_("Category of Utility", "Text Editors"), { "Utility::TextEditor", NULL} }, { "terminal-emulators", NC_("Category of Utility", "Terminal Emulators"), { "System::TerminalEmulator", NULL} }, { "filesystem", NC_("Category of Utility", "File System"), { "System::Filesystem", NULL} }, { "monitor", NC_("Category of Utility", "System Monitoring"), { "System::Monitor", NULL} }, { "security", NC_("Category of Utility", "Security"), { "System::Security", NULL} }, { NULL } }; /* main categories */ static const AsCategoryData msdata[] = { /* TRANSLATORS: this is the menu spec main category for Audio & Video */ { "audio-video", map_audiovideo, N_("Audio & Video"), "applications-multimedia" }, /* TRANSLATORS: this is the menu spec main category for Development */ { "developer-tools", map_developertools, N_("Developer Tools"), "applications-development" }, /* TRANSLATORS: this is the menu spec main category for Education */ { "education", map_education, N_("Education"), "applications-education" }, /* TRANSLATORS: this is the menu spec main category for Game */ { "games", map_games, N_("Games"), "applications-games" }, /* TRANSLATORS: this is the menu spec main category for Graphics */ { "graphics", map_graphics, N_("Graphics & Photography"), "applications-graphics" }, /* TRANSLATORS: this is the menu spec main category for Office */ { "office", map_office, N_("Office"), "applications-office" }, /* TRANSLATORS: this is the main category for Add-ons */ { "addons", map_addons, N_("Add-ons"), "applications-other" }, /* TRANSLATORS: this is the menu spec main category for Science */ { "science", map_science, N_("Science"), "applications-science" }, /* TRANSLATORS: this is the menu spec main category for Communication */ { "communication", map_communication, N_("Communication & News"), "applications-internet" }, /* TRANSLATORS: this is the menu spec main category for Utilities */ { "utilities", map_utilities, N_("Utilities"), "applications-utilities" }, { NULL } }; /* clang-format on */ typedef struct { gchar *id; gchar *name; gchar *summary; gchar *icon; GPtrArray *children; GPtrArray *desktop_groups; GPtrArray *components; } AsCategoryPrivate; G_DEFINE_TYPE_WITH_PRIVATE (AsCategory, as_category, G_TYPE_OBJECT) #define GET_PRIVATE(o) (as_category_get_instance_private (o)) enum { AS_CATEGORY_DUMMY, AS_CATEGORY_ID, AS_CATEGORY_NAME, AS_CATEGORY_SUMMARY, AS_CATEGORY_ICON, AS_CATEGORY_CHILDREN }; /** * as_category_init: **/ static void as_category_init (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); priv->children = g_ptr_array_new_with_free_func (g_object_unref); priv->desktop_groups = g_ptr_array_new_with_free_func (g_free); priv->components = g_ptr_array_new_with_free_func (g_object_unref); } /** * as_category_finalize: */ static void as_category_finalize (GObject *object) { AsCategory *category = AS_CATEGORY (object); AsCategoryPrivate *priv = GET_PRIVATE (category); g_free (priv->id); g_free (priv->name); g_free (priv->summary); g_free (priv->icon); g_ptr_array_unref (priv->children); g_ptr_array_unref (priv->desktop_groups); g_ptr_array_unref (priv->components); G_OBJECT_CLASS (as_category_parent_class)->finalize (object); } /** * as_category_get_id: * @category: An instance of #AsCategory. * * Get the ID of this category. */ const gchar * as_category_get_id (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); return priv->id; } /** * as_category_set_id: * @category: An instance of #AsCategory. * * Set the ID of this category. */ void as_category_set_id (AsCategory *category, const gchar *id) { AsCategoryPrivate *priv = GET_PRIVATE (category); as_assign_string_safe (priv->id, id); g_object_notify (G_OBJECT (category), "id"); } /** * as_category_get_name: * @category: An instance of #AsCategory. * * Get the name of this category. */ const gchar * as_category_get_name (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); return priv->name; } /** * as_category_set_name: * @category: An instance of #AsCategory. * * Set the name of this category. */ void as_category_set_name (AsCategory *category, const gchar *value) { AsCategoryPrivate *priv = GET_PRIVATE (category); as_assign_string_safe (priv->name, value); g_object_notify (G_OBJECT (category), "name"); } /** * as_category_get_children: * @category: An instance of #AsCategory. * * Returns: (element-type AsCategory) (transfer none): A list of subcategories. */ GPtrArray * as_category_get_children (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); return priv->children; } /** * as_category_add_child: * @category: An instance of #AsCategory. * @subcat: A subcategory to add. * * Add a subcategory to this category. */ void as_category_add_child (AsCategory *category, AsCategory *subcat) { AsCategoryPrivate *priv = GET_PRIVATE (category); g_ptr_array_add (priv->children, g_object_ref (subcat)); } /** * as_category_remove_child: * @category: An instance of #AsCategory. * @subcat: A subcategory to remove. * * Drop a subcategory from this #AsCategory. */ void as_category_remove_child (AsCategory *category, AsCategory *subcat) { AsCategoryPrivate *priv = GET_PRIVATE (category); g_ptr_array_remove (priv->children, subcat); } /** * as_category_has_children: * @category: An instance of #AsCategory. * * Test for sub-categories. * * Returns: %TRUE if this category has any subcategory */ gboolean as_category_has_children (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); return priv->children->len > 0; } /** * as_category_get_summary: * @category: An instance of #AsCategory. * * Get the summary (short description) of this category. */ const gchar * as_category_get_summary (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); return priv->summary; } /** * as_category_set_summary: * @category: An instance of #AsCategory. * @value: A new short summary of this category. * * Get the summary (short description) of this category. */ void as_category_set_summary (AsCategory *category, const gchar *value) { AsCategoryPrivate *priv = GET_PRIVATE (category); as_assign_string_safe (priv->summary, value); g_object_notify (G_OBJECT (category), "summary"); } /** * as_category_get_icon: * @category: An instance of #AsCategory. * * Get the stock icon name for this category. */ const gchar * as_category_get_icon (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); return priv->icon; } /** * as_category_set_icon: * @category: An instance of #AsCategory. * * Set the stock icon name for this category. */ void as_category_set_icon (AsCategory *category, const gchar *value) { AsCategoryPrivate *priv = GET_PRIVATE (category); as_assign_string_safe (priv->icon, value); g_object_notify (G_OBJECT (category), "icon"); } /** * as_category_get_desktop_groups: * @category: An instance of #AsCategory. * * Returns: (transfer none) (element-type utf8): A list of desktop-file categories. */ GPtrArray * as_category_get_desktop_groups (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); return priv->desktop_groups; } /** * as_category_add_desktop_group: * @category: An instance of #AsCategory. * @group_name: A subcategory to add. * * Add a desktop-file category to this #AsCategory. */ void as_category_add_desktop_group (AsCategory *category, const gchar *group_name) { AsCategoryPrivate *priv = GET_PRIVATE (category); g_ptr_array_add (priv->desktop_groups, g_strdup (group_name)); } /** * as_category_get_components: * @category: An instance of #AsCategory. * * Get list of components which have been sorted into this category. * * Returns: (transfer none) (element-type AsComponent): List of #AsCategory */ GPtrArray * as_category_get_components (AsCategory *category) { AsCategoryPrivate *priv = GET_PRIVATE (category); return priv->components; } /** * as_category_add_component: * @category: An instance of #AsCategory. * @cpt: The #AsComponent to add. * * Add a component to this category. */ void as_category_add_component (AsCategory *category, AsComponent *cpt) { AsCategoryPrivate *priv = GET_PRIVATE (category); g_ptr_array_add (priv->components, g_object_ref (cpt)); } /** * as_category_has_component: * @category: An instance of #AsCategory. * @cpt: The #AsComponent to look for. * * Check if the exact #AsComponent @cpt is a member of this * category already. * * returns: %TRUE if the component is present. */ gboolean as_category_has_component (AsCategory *category, AsComponent *cpt) { AsCategoryPrivate *priv = GET_PRIVATE (category); guint i; for (i = 0; i < priv->components->len; i++) { AsComponent *ecpt = AS_COMPONENT (g_ptr_array_index (priv->components, i)); if (ecpt == cpt) return TRUE; } return FALSE; } /** * as_category_get_property: */ static void as_category_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { AsCategory *category; category = G_TYPE_CHECK_INSTANCE_CAST (object, AS_TYPE_CATEGORY, AsCategory); switch (property_id) { case AS_CATEGORY_ID: g_value_set_string (value, as_category_get_id (category)); break; case AS_CATEGORY_NAME: g_value_set_string (value, as_category_get_name (category)); break; case AS_CATEGORY_SUMMARY: g_value_set_string (value, as_category_get_summary (category)); break; case AS_CATEGORY_ICON: g_value_set_string (value, as_category_get_icon (category)); break; case AS_CATEGORY_CHILDREN: g_value_set_pointer (value, as_category_get_children (category)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } /** * as_category_set_property: */ static void as_category_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { AsCategory *category; category = G_TYPE_CHECK_INSTANCE_CAST (object, AS_TYPE_CATEGORY, AsCategory); switch (property_id) { case AS_CATEGORY_ID: as_category_set_id (category, g_value_get_string (value)); break; case AS_CATEGORY_NAME: as_category_set_name (category, g_value_get_string (value)); break; case AS_CATEGORY_SUMMARY: as_category_set_summary (category, g_value_get_string (value)); break; case AS_CATEGORY_ICON: as_category_set_icon (category, g_value_get_string (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } /** * as_category_class_init: */ static void as_category_class_init (AsCategoryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = as_category_get_property; object_class->set_property = as_category_set_property; object_class->finalize = as_category_finalize; g_object_class_install_property ( object_class, AS_CATEGORY_ID, g_param_spec_string ("id", "id", "id", NULL, G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB | G_PARAM_READABLE | G_PARAM_WRITABLE)); g_object_class_install_property ( object_class, AS_CATEGORY_NAME, g_param_spec_string ("name", "name", "name", NULL, G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB | G_PARAM_READABLE | G_PARAM_WRITABLE)); g_object_class_install_property ( object_class, AS_CATEGORY_SUMMARY, g_param_spec_string ("summary", "summary", "summary", NULL, G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB | G_PARAM_READABLE)); g_object_class_install_property ( object_class, AS_CATEGORY_ICON, g_param_spec_string ("icon", "icon", "icon", NULL, G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB | G_PARAM_READABLE | G_PARAM_WRITABLE)); g_object_class_install_property ( object_class, AS_CATEGORY_CHILDREN, g_param_spec_pointer ("children", "children", "children", G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB | G_PARAM_READABLE)); } /** * as_category_new: * * Creates a new #AsCategory. * * Returns: (transfer full): a new #AsCategory **/ AsCategory * as_category_new (void) { AsCategory *category; category = g_object_new (AS_TYPE_CATEGORY, NULL); return AS_CATEGORY (category); } /** * as_get_default_categories: (skip) * @with_special: Include special categories (e.g. "addons", and "all"/"featured" in submenus) * * Get a list of the default Freedesktop and AppStream categories * that software components (especially GUI applications) can be sorted * into in software centers. * * Returns: (transfer container) (element-type AsCategory): a list of #AsCategory */ GPtrArray * as_get_default_categories (gboolean with_special) { guint i; gchar msgctxt[100]; GPtrArray *main_cats; main_cats = g_ptr_array_new_with_free_func (g_object_unref); for (i = 0; msdata[i].id != NULL; i++) { guint j; AsCategory *category; GHashTableIter iter; gpointer key; g_autoptr(GHashTable) root_fdocats = NULL; if ((!with_special) && (g_strcmp0 (msdata[i].id, "addons") == 0)) continue; category = as_category_new (); as_category_set_id (category, msdata[i].id); as_category_set_name (category, gettext (msdata[i].name)); as_category_set_icon (category, msdata[i].icon); g_ptr_array_add (main_cats, category); g_snprintf (msgctxt, sizeof (msgctxt), "Subcategory of %s", msdata[i].name); root_fdocats = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); /* add subcategories */ for (j = 0; msdata[i].mapping[j].id != NULL; j++) { guint k; const AsCategoryMap *map = &msdata[i].mapping[j]; g_autoptr(AsCategory) sub = NULL; if (!with_special) { if (g_strcmp0 (msdata[i].id, "featured") == 0) continue; } sub = as_category_new (); as_category_set_id (sub, map->id); for (k = 0; map->fdo_cats[k] != NULL; k++) { g_auto(GStrv) split = g_strsplit (map->fdo_cats[k], "::", -1); as_category_add_desktop_group (sub, map->fdo_cats[k]); g_hash_table_add (root_fdocats, g_strdup (split[0])); } as_category_set_name (sub, g_dpgettext2 (GETTEXT_PACKAGE, msgctxt, map->name)); as_category_add_child (category, sub); } /* ensure the root category has the right XDG group names set, which match the subcategories */ g_hash_table_iter_init (&iter, root_fdocats); while (g_hash_table_iter_next (&iter, &key, NULL)) { const gchar *desktop_group = (const gchar *) key; as_category_add_desktop_group (category, desktop_group); } } return main_cats; } /** * as_get_default_categories_gi: (rename-to as_get_default_categories) * @with_special: Include special categories (e.g. "addons", and "all"/"featured" in submenus) * * Get a list of the default Freedesktop and AppStream categories * that software components (especially GUI applications) can be sorted * into in software centers. * * This function fully transfers ownership of the returned container, * to be used in GIR bindings. * * Returns: (transfer full) (element-type AsCategory): a list of #AsCategory */ GPtrArray * as_get_default_categories_gi (gboolean with_special) { AS_PTR_ARRAY_RETURN_CLEAR_FREE_FUNC (as_get_default_categories (with_special)); }