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 --- src/gs-age-rating-context-dialog.c | 1253 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1253 insertions(+) create mode 100644 src/gs-age-rating-context-dialog.c (limited to 'src/gs-age-rating-context-dialog.c') diff --git a/src/gs-age-rating-context-dialog.c b/src/gs-age-rating-context-dialog.c new file mode 100644 index 0000000..4ecf9c5 --- /dev/null +++ b/src/gs-age-rating-context-dialog.c @@ -0,0 +1,1253 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2021 Endless OS Foundation LLC + * + * Author: Philip Withnall + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-age-rating-context-dialog + * @short_description: A dialog showing age rating information about an app + * + * #GsAgeRatingContextDialog is a dialog which shows detailed information + * about the suitability of the content in an app for different ages. It gives + * a breakdown of which content is more or less suitable for younger audiences. + * This information is derived from the `` element in the app’s + * appdata. + * + * It is designed to show a more detailed view of the information which the + * app’s age rating tile in #GsAppContextBar is derived from. + * + * The widget has no special appearance if the app is unset, so callers will + * typically want to hide the dialog in that case. + * + * Since: 41 + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include "gs-app.h" +#include "gs-common.h" +#include "gs-context-dialog-row.h" +#include "gs-age-rating-context-dialog.h" + +typedef enum { + GS_AGE_RATING_GROUP_TYPE_DRUGS, + GS_AGE_RATING_GROUP_TYPE_LANGUAGE, + GS_AGE_RATING_GROUP_TYPE_MONEY, + GS_AGE_RATING_GROUP_TYPE_SEX, + GS_AGE_RATING_GROUP_TYPE_SOCIAL, + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, +} GsAgeRatingGroupType; + +#define GS_AGE_RATING_GROUP_TYPE_COUNT (GS_AGE_RATING_GROUP_TYPE_VIOLENCE+1) + +typedef struct { + gchar *id; + gchar *icon_name; + GsContextDialogRowImportance importance; + gchar *title; + gchar *description; +} GsAgeRatingAttribute; + +struct _GsAgeRatingContextDialog +{ + GsInfoWindow parent_instance; + + GsApp *app; /* (nullable) (owned) */ + gulong app_notify_handler_content_rating; + gulong app_notify_handler_name; + GsContextDialogRow *rows[GS_AGE_RATING_GROUP_TYPE_COUNT]; /* (unowned) */ + GList *attributes[GS_AGE_RATING_GROUP_TYPE_COUNT]; /* (element-type GsAgeRatingAttribute) */ + + GsLozenge *lozenge; + GtkLabel *title; + GtkListBox *attributes_list; /* (element-type GsContextDialogRow) */ +}; + +G_DEFINE_TYPE (GsAgeRatingContextDialog, gs_age_rating_context_dialog, GS_TYPE_INFO_WINDOW) + +typedef enum { + PROP_APP = 1, +} GsAgeRatingContextDialogProperty; + +static GParamSpec *obj_props[PROP_APP + 1] = { NULL, }; + +static GsAgeRatingAttribute * +gs_age_rating_attribute_new (const gchar *id, + const gchar *icon_name, + GsContextDialogRowImportance importance, + const gchar *title, + const gchar *description) +{ + GsAgeRatingAttribute *attributes; + + g_assert (icon_name != NULL); + g_assert (title != NULL); + g_assert (description != NULL); + + attributes = g_new0 (GsAgeRatingAttribute, 1); + attributes->id = g_strdup (id); + attributes->icon_name = g_strdup (icon_name); + attributes->importance = importance; + attributes->title = g_strdup (title); + attributes->description = g_strdup (description); + + return attributes; +} + +static void +gs_age_rating_attribute_free (GsAgeRatingAttribute *attributes) +{ + g_free (attributes->id); + g_free (attributes->icon_name); + g_free (attributes->title); + g_free (attributes->description); + g_free (attributes); +} + +/* FIXME: Ideally this data would move into libappstream, to be next to the + * other per-attribute strings and data which it already stores. */ +static const struct { + const gchar *id; /* (not nullable) */ + GsAgeRatingGroupType group_type; + const gchar *title; /* (not nullable) */ + const gchar *unknown_description; /* (not nullable) */ + const gchar *icon_name; /* (not nullable) */ + const gchar *icon_name_negative; /* (nullable) */ +} attribute_details[] = { + /* v1.0 */ + { + "violence-cartoon", + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Cartoon Violence"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding cartoon violence"), + "violence-symbolic", + "violence-none-symbolic", + }, + { + "violence-fantasy", + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Fantasy Violence"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding fantasy violence"), + "violence-symbolic", + "violence-none-symbolic", + }, + { + "violence-realistic", + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Realistic Violence"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding realistic violence"), + "violence-symbolic", + "violence-none-symbolic", + }, + { + "violence-bloodshed", + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Violence Depicting Bloodshed"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding bloodshed"), + "violence-symbolic", + "violence-none-symbolic", + }, + { + "violence-sexual", + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Sexual Violence"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding sexual violence"), + "violence-symbolic", + "violence-none-symbolic", + }, + { + "drugs-alcohol", + GS_AGE_RATING_GROUP_TYPE_DRUGS, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Alcohol"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to alcohol"), + "pub-symbolic", + NULL, + }, + { + "drugs-narcotics", + GS_AGE_RATING_GROUP_TYPE_DRUGS, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Narcotics"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to illicit drugs"), + "cigarette-symbolic", + "cigarette-none-symbolic", + }, + { + "drugs-tobacco", + GS_AGE_RATING_GROUP_TYPE_DRUGS, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Tobacco"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to tobacco products"), + "cigarette-symbolic", + "cigarette-none-symbolic", + }, + { + "sex-nudity", + GS_AGE_RATING_GROUP_TYPE_SEX, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Nudity"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding nudity of any sort"), + "nudity-symbolic", + "nudity-none-symbolic", + }, + { + "sex-themes", + GS_AGE_RATING_GROUP_TYPE_SEX, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Sexual Themes"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to or depictions of sexual nature"), + "nudity-symbolic", + "nudity-none-symbolic", + }, + { + "language-profanity", + GS_AGE_RATING_GROUP_TYPE_LANGUAGE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Profanity"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding profanity of any kind"), + "strong-language-symbolic", + "strong-language-none-symbolic", + }, + { + "language-humor", + GS_AGE_RATING_GROUP_TYPE_LANGUAGE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Inappropriate Humor"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding inappropriate humor"), + "strong-language-symbolic", + "strong-language-none-symbolic", + }, + { + "language-discrimination", + GS_AGE_RATING_GROUP_TYPE_SOCIAL, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Discrimination"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding discriminatory language of any kind"), + "chat-symbolic", + "chat-none-symbolic", + }, + { + "money-advertising", + GS_AGE_RATING_GROUP_TYPE_MONEY, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Advertising"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding advertising of any kind"), + "money-symbolic", + "money-none-symbolic", + }, + { + "money-gambling", + GS_AGE_RATING_GROUP_TYPE_MONEY, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Gambling"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding gambling of any kind"), + "money-symbolic", + "money-none-symbolic", + }, + { + "money-purchasing", + GS_AGE_RATING_GROUP_TYPE_MONEY, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Purchasing"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding the ability to spend money"), + "money-symbolic", + "money-none-symbolic", + }, + { + "social-chat", + GS_AGE_RATING_GROUP_TYPE_SOCIAL, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Chat Between Users"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding ways to chat with other users"), + "chat-symbolic", + "chat-none-symbolic", + }, + { + "social-audio", + GS_AGE_RATING_GROUP_TYPE_SOCIAL, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Audio Chat Between Users"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding ways to talk with other users"), + "audio-headset-symbolic", + NULL, + }, + { + "social-contacts", + GS_AGE_RATING_GROUP_TYPE_SOCIAL, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Contact Details"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding sharing of social network usernames or email addresses"), + "contact-new-symbolic", + NULL, + }, + { + "social-info", + GS_AGE_RATING_GROUP_TYPE_SOCIAL, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Identifying Information"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding sharing of user information with third parties"), + "x-office-address-book-symbolic", + NULL, + }, + { + "social-location", + GS_AGE_RATING_GROUP_TYPE_SOCIAL, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Location Sharing"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding sharing of physical location with other users"), + "location-services-active-symbolic", + "location-services-disabled-symbolic", + }, + + /* v1.1 */ + { + /* Why is there an OARS category which discriminates based on sexual orientation? + * It’s because there are, very unfortunately, still countries in the world in + * which homosexuality, or software which refers to it, is illegal. In order to be + * able to ship FOSS in those countries, there needs to be a mechanism for apps to + * describe whether they refer to anything illegal, and for ratings mechanisms in + * those countries to filter out any apps which describe themselves as such. + * + * As a counterpoint, it’s illegal in many more countries to discriminate on the + * basis of sexual orientation, so this category is treated exactly the same as + * sex-themes (once the intensities of the ratings levels for both categories are + * normalised) in those countries. + * + * The differences between countries are handled through handling #AsContentRatingSystem + * values differently. */ + "sex-homosexuality", + GS_AGE_RATING_GROUP_TYPE_SEX, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Homosexuality"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to homosexuality"), + "nudity-symbolic", + "nudity-none-symbolic", + }, + { + "sex-prostitution", + GS_AGE_RATING_GROUP_TYPE_SEX, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Prostitution"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to prostitution"), + "nudity-symbolic", + "nudity-none-symbolic", + }, + { + "sex-adultery", + GS_AGE_RATING_GROUP_TYPE_SEX, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Adultery"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to adultery"), + "nudity-symbolic", + "nudity-none-symbolic", + }, + { + "sex-appearance", + GS_AGE_RATING_GROUP_TYPE_SEX, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Sexualized Characters"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding sexualized characters"), + "nudity-symbolic", + "nudity-none-symbolic", + }, + { + "violence-worship", + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Desecration"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to desecration"), + "violence-symbolic", + "violence-none-symbolic", + }, + { + "violence-desecration", + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Human Remains"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding visible dead human remains"), + "graveyard-symbolic", + NULL, + }, + { + "violence-slavery", + GS_AGE_RATING_GROUP_TYPE_VIOLENCE, + /* TRANSLATORS: content rating title, see https://hughsie.github.io/oars/ */ + N_("Slavery"), + /* TRANSLATORS: content rating description, see https://hughsie.github.io/oars/ */ + N_("No information regarding references to slavery"), + "violence-symbolic", + "violence-none-symbolic", + }, +}; + +/* Get the `icon_name` (or, if @negative_version is %TRUE, the + * `icon_name_negative`) from @attribute_details for the given @attribute. + * If `icon_name_negative` is %NULL, fall back to returning `icon_name`. */ +static const gchar * +content_rating_attribute_get_icon_name (const gchar *attribute, + gboolean negative_version) +{ + for (gsize i = 0; i < G_N_ELEMENTS (attribute_details); i++) { + if (g_str_equal (attribute, attribute_details[i].id)) { + if (negative_version && attribute_details[i].icon_name_negative != NULL) + return attribute_details[i].icon_name_negative; + return attribute_details[i].icon_name; + } + } + + /* Attribute not handled */ + g_assert_not_reached (); +} + +/* Get the `title` from @attribute_details for the given @attribute. */ +static const gchar * +content_rating_attribute_get_title (const gchar *attribute) +{ + for (gsize i = 0; i < G_N_ELEMENTS (attribute_details); i++) { + if (g_str_equal (attribute, attribute_details[i].id)) { + return _(attribute_details[i].title); + } + } + + /* Attribute not handled */ + g_assert_not_reached (); +} + +/* Get the `unknown_description` from @attribute_details for the given @attribute. */ +static const gchar * +content_rating_attribute_get_unknown_description (const gchar *attribute) +{ + for (gsize i = 0; i < G_N_ELEMENTS (attribute_details); i++) { + if (g_str_equal (attribute, attribute_details[i].id)) { + return _(attribute_details[i].unknown_description); + } + } + + /* Attribute not handled */ + g_assert_not_reached (); +} + +/* Get the `title` from @attribute_details for the given @attribute. */ +static GsAgeRatingGroupType +content_rating_attribute_get_group_type (const gchar *attribute) +{ + for (gsize i = 0; i < G_N_ELEMENTS (attribute_details); i++) { + if (g_str_equal (attribute, attribute_details[i].id)) { + return attribute_details[i].group_type; + } + } + + /* Attribute not handled */ + g_assert_not_reached (); +} + +static const gchar * +content_rating_group_get_description (GsAgeRatingGroupType group_type) +{ + switch (group_type) { + case GS_AGE_RATING_GROUP_TYPE_DRUGS: + return _("Does not include references to drugs"); + case GS_AGE_RATING_GROUP_TYPE_LANGUAGE: + return _("Does not include swearing, profanity, and other kinds of strong language"); + case GS_AGE_RATING_GROUP_TYPE_MONEY: + return _("Does not include ads or monetary transactions"); + case GS_AGE_RATING_GROUP_TYPE_SEX: + return _("Does not include sex or nudity"); + case GS_AGE_RATING_GROUP_TYPE_SOCIAL: + return _("Does not include uncontrolled chat functionality"); + case GS_AGE_RATING_GROUP_TYPE_VIOLENCE: + return _("Does not include violence"); + default: + g_assert_not_reached (); + } +} + +static const gchar * +content_rating_group_get_icon_name (GsAgeRatingGroupType group_type, + gboolean negative_version) +{ + switch (group_type) { + case GS_AGE_RATING_GROUP_TYPE_DRUGS: + return negative_version ? "cigarette-none-symbolic" : "cigarette-symbolic"; + case GS_AGE_RATING_GROUP_TYPE_LANGUAGE: + return negative_version ? "strong-language-none-symbolic" : "strong-language-symbolic"; + case GS_AGE_RATING_GROUP_TYPE_MONEY: + return negative_version ? "money-none-symbolic" : "money-symbolic"; + case GS_AGE_RATING_GROUP_TYPE_SEX: + return negative_version ? "nudity-none-symbolic" : "nudity-symbolic"; + case GS_AGE_RATING_GROUP_TYPE_SOCIAL: + return negative_version ? "chat-none-symbolic" : "chat-symbolic"; + case GS_AGE_RATING_GROUP_TYPE_VIOLENCE: + return negative_version ? "violence-none-symbolic" : "violence-symbolic"; + default: + g_assert_not_reached (); + } +} + +static const gchar * +content_rating_group_get_title (GsAgeRatingGroupType group_type) +{ + switch (group_type) { + case GS_AGE_RATING_GROUP_TYPE_DRUGS: + return _("Drugs"); + case GS_AGE_RATING_GROUP_TYPE_LANGUAGE: + return _("Strong Language"); + case GS_AGE_RATING_GROUP_TYPE_MONEY: + return _("Money"); + case GS_AGE_RATING_GROUP_TYPE_SEX: + return _("Nudity"); + case GS_AGE_RATING_GROUP_TYPE_SOCIAL: + return _("Social"); + case GS_AGE_RATING_GROUP_TYPE_VIOLENCE: + return _("Violence"); + default: + g_assert_not_reached (); + } +} + +static GsContextDialogRowImportance +content_rating_value_get_importance (AsContentRatingValue value) +{ + switch (value) { + case AS_CONTENT_RATING_VALUE_NONE: + return GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT; + case AS_CONTENT_RATING_VALUE_UNKNOWN: + return GS_CONTEXT_DIALOG_ROW_IMPORTANCE_NEUTRAL; + case AS_CONTENT_RATING_VALUE_MILD: + case AS_CONTENT_RATING_VALUE_MODERATE: + return GS_CONTEXT_DIALOG_ROW_IMPORTANCE_WARNING; + case AS_CONTENT_RATING_VALUE_INTENSE: + return GS_CONTEXT_DIALOG_ROW_IMPORTANCE_IMPORTANT; + default: + g_assert_not_reached (); + } +} + +static gint +attributes_compare (GsAgeRatingAttribute *attributes1, + GsAgeRatingAttribute *attributes2) +{ + if (attributes1->importance != attributes2->importance) { + /* Sort neutral attributes before unimportant ones. */ + if (attributes1->importance == GS_CONTEXT_DIALOG_ROW_IMPORTANCE_NEUTRAL && + attributes2->importance == GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT) + return -1; + if (attributes1->importance == GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT && + attributes2->importance == GS_CONTEXT_DIALOG_ROW_IMPORTANCE_NEUTRAL) + return 1; + + /* Important attributes come first */ + return attributes2->importance - attributes1->importance; + } else { + /* Sort by alphabetical ID order */ + return g_strcmp0 (attributes1->id, attributes2->id); + } +} + +static void +update_attribute_row (GsAgeRatingContextDialog *self, + GsAgeRatingGroupType group_type) +{ + const GsAgeRatingAttribute *first; + const gchar *group_icon_name; + const gchar *group_title; + const gchar *group_description; + g_autofree char *new_description = NULL; + + first = (GsAgeRatingAttribute *) self->attributes[group_type]->data; + + if (g_list_length (self->attributes[group_type]) == 1) { + g_object_set (self->rows[group_type], + "icon-name", first->icon_name, + "importance", first->importance, + "subtitle", first->description, + "title", first->title, + NULL); + + return; + } + + if (first->importance == GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT) { + gboolean only_unimportant = TRUE; + + for (GList *l = self->attributes[group_type]->next; l; l = l->next) { + GsAgeRatingAttribute *attribute = (GsAgeRatingAttribute *) l->data; + + if (attribute->importance != GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT) { + only_unimportant = FALSE; + break; + } + } + + if (only_unimportant) { + group_icon_name = content_rating_group_get_icon_name (group_type, first->importance == GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT); + group_title = content_rating_group_get_title (group_type); + group_description = content_rating_group_get_description (group_type); + + g_object_set (self->rows[group_type], + "icon-name", group_icon_name, + "importance", first->importance, + "subtitle", group_description, + "title", group_title, + NULL); + + return; + } + + } + + group_icon_name = content_rating_group_get_icon_name (group_type, FALSE); + group_title = content_rating_group_get_title (group_type); + new_description = g_strdup (first->description); + + for (GList *l = self->attributes[group_type]->next; l; l = l->next) { + GsAgeRatingAttribute *attribute = (GsAgeRatingAttribute *) l->data; + char *s; + + if (attribute->importance == GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT) + break; + + /* Translators: This is used to join two list items together in + * a compressed way of displaying a list of descriptions of age + * ratings for apps. The order of the items does not matter. */ + s = g_strdup_printf (_("%s • %s"), + new_description, + ((GsAgeRatingAttribute *) l->data)->description); + g_free (new_description); + new_description = s; + } + + g_object_set (self->rows[group_type], + "icon-name", group_icon_name, + "importance", first->importance, + "subtitle", new_description, + "title", group_title, + NULL); +} + +static void +add_attribute_row (GsAgeRatingContextDialog *self, + const gchar *attribute, + AsContentRatingValue value) +{ + GsAgeRatingGroupType group_type; + GsContextDialogRowImportance rating; + const gchar *icon_name, *title, *description; + GsAgeRatingAttribute *attributes; + + group_type = content_rating_attribute_get_group_type (attribute); + rating = content_rating_value_get_importance (value); + icon_name = content_rating_attribute_get_icon_name (attribute, value == AS_CONTENT_RATING_VALUE_NONE); + title = content_rating_attribute_get_title (attribute); + if (value == AS_CONTENT_RATING_VALUE_UNKNOWN) + description = content_rating_attribute_get_unknown_description (attribute); + else + description = as_content_rating_attribute_get_description (attribute, value); + + attributes = gs_age_rating_attribute_new (attribute, icon_name, rating, title, description); + + if (self->attributes[group_type] != NULL) { + self->attributes[group_type] = g_list_insert_sorted (self->attributes[group_type], + attributes, + (GCompareFunc) attributes_compare); + + update_attribute_row (self, group_type); + } else { + self->attributes[group_type] = g_list_prepend (self->attributes[group_type], attributes); + self->rows[group_type] = GS_CONTEXT_DIALOG_ROW (gs_context_dialog_row_new (icon_name, rating, title, description)); + gtk_list_box_append (self->attributes_list, GTK_WIDGET (self->rows[group_type])); + } +} + +/** + * gs_age_rating_context_dialog_process_attributes: + * @content_rating: content rating data from an app, retrieved using + * gs_app_dup_content_rating() + * @show_worst_only: %TRUE to only process the worst content rating attributes, + * %FALSE to process all of them + * @callback: callback to call for each attribute being processed + * @user_data: data to pass to @callback + * + * Loop through all the defined content rating attributes, and decide which ones + * are relevant to show to the user. For each of the relevant attributes, call + * @callback with the attribute name and value. + * + * If @show_worst_only is %TRUE, only the attributes which cause the overall + * rating of the app to be as high as it is are considered relevant. If it is + * %FALSE, all attributes are relevant. + * + * If the app has an overall age rating of 0, @callback is called exactly once, + * with the attribute name set to %NULL, to indicate that the app is suitable + * for all in every attribute. + * + * Since: 41 + */ +void +gs_age_rating_context_dialog_process_attributes (AsContentRating *content_rating, + gboolean show_worst_only, + GsAgeRatingContextDialogAttributeFunc callback, + gpointer user_data) +{ + g_autofree const gchar **rating_ids = as_content_rating_get_all_rating_ids (); + AsContentRatingValue value_bad = AS_CONTENT_RATING_VALUE_NONE; + guint age_bad = 0; + + /* Ordered from worst to best, these are all OARS 1.0/1.1 categories */ + const gchar * const violence_group[] = { + "violence-bloodshed", + "violence-realistic", + "violence-fantasy", + "violence-cartoon", + NULL + }; + const gchar * const social_group[] = { + "social-audio", + "social-chat", + "social-contacts", + "social-info", + NULL + }; + const gchar * const coalesce_groups[] = { + "sex-themes", + "sex-homosexuality", + NULL + }; + + /* Get the worst category. */ + for (gsize i = 0; rating_ids[i] != NULL; i++) { + guint rating_age; + AsContentRatingValue rating_value; + + rating_value = as_content_rating_get_value (content_rating, rating_ids[i]); + rating_age = as_content_rating_attribute_to_csm_age (rating_ids[i], rating_value); + + if (rating_age > age_bad) + age_bad = rating_age; + if (rating_value > value_bad) + value_bad = rating_value; + } + + /* If the worst category is nothing, great! Show a more specific message + * than a big listing of all the groups. */ + if (show_worst_only && (value_bad == AS_CONTENT_RATING_VALUE_NONE || age_bad == 0)) { + callback (NULL, AS_CONTENT_RATING_VALUE_UNKNOWN, user_data); + return; + } + + /* Add a description for each rating category which contributes to the + * @age_bad being as it is. Handle the groups separately. + * Intentionally coalesce some categories if they have the same values, + * to avoid confusion */ + for (gsize i = 0; rating_ids[i] != NULL; i++) { + guint rating_age; + AsContentRatingValue rating_value; + + if (g_strv_contains (violence_group, rating_ids[i]) || + g_strv_contains (social_group, rating_ids[i])) + continue; + + rating_value = as_content_rating_get_value (content_rating, rating_ids[i]); + rating_age = as_content_rating_attribute_to_csm_age (rating_ids[i], rating_value); + + if (show_worst_only && rating_age < age_bad) + continue; + + /* Coalesce down to the first element in @coalesce_groups, + * unless this group’s value differs. Currently only one + * coalesce group is supported. */ + if (g_strv_contains (coalesce_groups + 1, rating_ids[i]) && + as_content_rating_attribute_to_csm_age (coalesce_groups[0], + as_content_rating_get_value (content_rating, + coalesce_groups[0])) >= rating_age) + continue; + + callback (rating_ids[i], rating_value, user_data); + } + + for (gsize i = 0; violence_group[i] != NULL; i++) { + guint rating_age; + AsContentRatingValue rating_value; + + rating_value = as_content_rating_get_value (content_rating, violence_group[i]); + rating_age = as_content_rating_attribute_to_csm_age (violence_group[i], rating_value); + + if (show_worst_only && rating_age < age_bad) + continue; + + callback (violence_group[i], rating_value, user_data); + } + + for (gsize i = 0; social_group[i] != NULL; i++) { + guint rating_age; + AsContentRatingValue rating_value; + + rating_value = as_content_rating_get_value (content_rating, social_group[i]); + rating_age = as_content_rating_attribute_to_csm_age (social_group[i], rating_value); + + if (show_worst_only && rating_age < age_bad) + continue; + + callback (social_group[i], rating_value, user_data); + } +} + +static void +add_attribute_rows_cb (const gchar *attribute, + AsContentRatingValue value, + gpointer user_data) +{ + GsAgeRatingContextDialog *self = GS_AGE_RATING_CONTEXT_DIALOG (user_data); + + add_attribute_row (self, attribute, value); +} + +/* Wrapper around as_content_rating_system_format_age() which returns the short + * form of the content rating. This doesn’t make a difference for most ratings + * systems, but it does for ESRB which normally produces quite long strings. + * + * FIXME: This should probably be upstreamed into libappstream once it’s been in + * the GNOME 41 release and stabilised. */ +gchar * +gs_age_rating_context_dialog_format_age_short (AsContentRatingSystem system, + guint age) +{ + if (system == AS_CONTENT_RATING_SYSTEM_ESRB) { + if (age >= 18) + return g_strdup ("AO"); + if (age >= 17) + return g_strdup ("M"); + if (age >= 13) + return g_strdup ("T"); + if (age >= 10) + return g_strdup ("E10+"); + if (age >= 6) + return g_strdup ("E"); + + return g_strdup ("EC"); + } + + return as_content_rating_system_format_age (system, age); +} + +/** + * gs_age_rating_context_dialog_update_lozenge: + * @app: the #GsApp to rate + * @lozenge: a #GsLozenge widget + * @is_unknown_out: (out caller-allocates) (not optional): return location for + * a boolean indicating whether the age rating is unknown, rather than a + * specific age + * + * Update the @lozenge widget to indicate the overall age rating for @app. + * This involves changing its CSS class and label content. + * + * If the overall age rating for @app is unknown (because the app doesn’t + * provide a complete `` element in its appdata), the lozenge is + * set to show a question mark, and @is_unknown_out is set to %TRUE. + * + * Since: 41 + */ +void +gs_age_rating_context_dialog_update_lozenge (GsApp *app, + GsLozenge *lozenge, + gboolean *is_unknown_out) +{ + const gchar *css_class; + const gchar *locale; + AsContentRatingSystem system; + g_autoptr(AsContentRating) content_rating = NULL; + GtkStyleContext *context; + const gchar *css_age_classes[] = { + "details-rating-18", + "details-rating-15", + "details-rating-12", + "details-rating-5", + "details-rating-0", + }; + guint age = G_MAXUINT; + g_autofree gchar *age_text = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_LOZENGE (lozenge)); + g_return_if_fail (is_unknown_out != NULL); + + /* get the content rating system from the locale */ + locale = setlocale (LC_MESSAGES, NULL); + system = as_content_rating_system_from_locale (locale); + g_debug ("content rating system is guessed as %s from %s", + as_content_rating_system_to_string (system), + locale); + + content_rating = gs_app_dup_content_rating (app); + if (content_rating != NULL) + age = as_content_rating_get_minimum_age (content_rating); + + if (age != G_MAXUINT) + age_text = gs_age_rating_context_dialog_format_age_short (system, age); + + /* Some ratings systems (PEGI) don’t start at age 0 */ + if (content_rating != NULL && age_text == NULL && age == 0) + /* Translators: The app is considered suitable to be run by all ages of people. + * This is displayed in a context tile, so the string should be short. */ + age_text = g_strdup (C_("Age rating", "All")); + + /* We currently only support OARS-1.0 and OARS-1.1 */ + if (age_text == NULL || + (content_rating != NULL && + g_strcmp0 (as_content_rating_get_kind (content_rating), "oars-1.0") != 0 && + g_strcmp0 (as_content_rating_get_kind (content_rating), "oars-1.1") != 0)) { + /* Translators: This app has no age rating information available. + * This string is displayed like an icon. Please use any + * similarly short punctuation character, word or acronym which + * will be widely understood in your region, in this context. + * This is displayed in a context tile, so the string should be short. */ + g_free (age_text); + age_text = g_strdup (_("?")); + css_class = "grey"; + *is_unknown_out = TRUE; + } else { + /* Update the CSS */ + if (age >= 18) + css_class = css_age_classes[0]; + else if (age >= 15) + css_class = css_age_classes[1]; + else if (age >= 12) + css_class = css_age_classes[2]; + else if (age >= 5) + css_class = css_age_classes[3]; + else + css_class = css_age_classes[4]; + + *is_unknown_out = FALSE; + } + + /* Update the UI. */ + gs_lozenge_set_text (lozenge, age_text); + + context = gtk_widget_get_style_context (GTK_WIDGET (lozenge)); + + for (gsize i = 0; i < G_N_ELEMENTS (css_age_classes); i++) + gtk_style_context_remove_class (context, css_age_classes[i]); + gtk_style_context_remove_class (context, "grey"); + + gtk_style_context_add_class (context, css_class); +} + +static void +update_attributes_list (GsAgeRatingContextDialog *self) +{ + g_autoptr(AsContentRating) content_rating = NULL; + gboolean is_unknown; + g_autofree gchar *title = NULL; + + /* Clear existing state. */ + gs_widget_remove_all (GTK_WIDGET (self->attributes_list), (GsRemoveFunc) gtk_list_box_remove); + + for (GsAgeRatingGroupType group_type = 0; group_type < GS_AGE_RATING_GROUP_TYPE_COUNT; group_type++) { + g_list_free_full (self->attributes[group_type], + (GDestroyNotify) gs_age_rating_attribute_free); + self->attributes[group_type] = NULL; + + self->rows[group_type] = NULL; + } + + /* UI state is undefined if app is not set. */ + if (self->app == NULL) + return; + + /* Update lozenge and title */ + content_rating = gs_app_dup_content_rating (self->app); + gs_age_rating_context_dialog_update_lozenge (self->app, + self->lozenge, + &is_unknown); + + /* Title */ + if (is_unknown) { + /* Translators: It’s unknown what age rating this app has. The + * placeholder is the app name. */ + title = g_strdup_printf (("%s has an unknown age rating"), gs_app_get_name (self->app)); + } else { + guint age; + + /* if content_rating is NULL, is_unknown should be TRUE */ + g_assert (content_rating != NULL); + age = as_content_rating_get_minimum_age (content_rating); + + if (age == 0) + /* Translators: This is a dialogue title which indicates that an app is suitable + * for all ages. The placeholder is the app name. */ + title = g_strdup_printf (_("%s is suitable for everyone"), gs_app_get_name (self->app)); + else if (age <= 3) + /* Translators: This is a dialogue title which indicates that an app is suitable + * for children up to around age 3. The placeholder is the app name. */ + title = g_strdup_printf (_("%s is suitable for toddlers"), gs_app_get_name (self->app)); + else if (age <= 5) + /* Translators: This is a dialogue title which indicates that an app is suitable + * for children up to around age 5. The placeholder is the app name. */ + title = g_strdup_printf (_("%s is suitable for young children"), gs_app_get_name (self->app)); + else if (age <= 12) + /* Translators: This is a dialogue title which indicates that an app is suitable + * for children up to around age 12. The placeholder is the app name. */ + title = g_strdup_printf (("%s is suitable for children"), gs_app_get_name (self->app)); + else if (age <= 18) + /* Translators: This is a dialogue title which indicates that an app is suitable + * for people up to around age 18. The placeholder is the app name. */ + title = g_strdup_printf (_("%s is suitable for teenagers"), gs_app_get_name (self->app)); + else if (age < G_MAXUINT) + /* Translators: This is a dialogue title which indicates that an app is suitable + * for people aged up to and over 18. The placeholder is the app name. */ + title = g_strdup_printf (_("%s is suitable for adults"), gs_app_get_name (self->app)); + else + /* Translators: This is a dialogue title which indicates that an app is suitable + * for a specified age group. The first placeholder is the app name, the second + * is the age group. */ + title = g_strdup_printf (_("%s is suitable for %s"), gs_app_get_name (self->app), + gs_lozenge_get_text (self->lozenge)); + } + + gtk_label_set_text (self->title, title); + + /* Update the rows */ + gs_age_rating_context_dialog_process_attributes (content_rating, + FALSE, + add_attribute_rows_cb, + self); +} + +static void +app_notify_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + GsAgeRatingContextDialog *self = GS_AGE_RATING_CONTEXT_DIALOG (user_data); + + update_attributes_list (self); +} + +static gint +sort_cb (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer user_data) +{ + GsContextDialogRow *_row1 = GS_CONTEXT_DIALOG_ROW (row1); + GsContextDialogRow *_row2 = GS_CONTEXT_DIALOG_ROW (row2); + GsContextDialogRowImportance importance1, importance2; + const gchar *title1, *title2; + + importance1 = gs_context_dialog_row_get_importance (_row1); + importance2 = gs_context_dialog_row_get_importance (_row2); + + if (importance1 != importance2) + return importance2 - importance1; + + title1 = adw_preferences_row_get_title (ADW_PREFERENCES_ROW (_row1)); + title2 = adw_preferences_row_get_title (ADW_PREFERENCES_ROW (_row2)); + + return g_strcmp0 (title1, title2); +} + +static void +gs_age_rating_context_dialog_init (GsAgeRatingContextDialog *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + /* Sort the list so the most important rows are at the top. */ + gtk_list_box_set_sort_func (self->attributes_list, sort_cb, NULL, NULL); +} + +static void +gs_age_rating_context_dialog_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsAgeRatingContextDialog *self = GS_AGE_RATING_CONTEXT_DIALOG (object); + + switch ((GsAgeRatingContextDialogProperty) prop_id) { + case PROP_APP: + g_value_set_object (value, gs_age_rating_context_dialog_get_app (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_age_rating_context_dialog_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsAgeRatingContextDialog *self = GS_AGE_RATING_CONTEXT_DIALOG (object); + + switch ((GsAgeRatingContextDialogProperty) prop_id) { + case PROP_APP: + gs_age_rating_context_dialog_set_app (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_age_rating_context_dialog_dispose (GObject *object) +{ + GsAgeRatingContextDialog *self = GS_AGE_RATING_CONTEXT_DIALOG (object); + + gs_age_rating_context_dialog_set_app (self, NULL); + + G_OBJECT_CLASS (gs_age_rating_context_dialog_parent_class)->dispose (object); +} + +static void +gs_age_rating_context_dialog_class_init (GsAgeRatingContextDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gs_age_rating_context_dialog_get_property; + object_class->set_property = gs_age_rating_context_dialog_set_property; + object_class->dispose = gs_age_rating_context_dialog_dispose; + + /** + * GsAgeRatingContextDialog:app: (nullable) + * + * The app to display the age_rating context details for. + * + * This may be %NULL; if so, the content of the widget will be + * undefined. + * + * Since: 41 + */ + obj_props[PROP_APP] = + g_param_spec_object ("app", NULL, NULL, + GS_TYPE_APP, + 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); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-age-rating-context-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsAgeRatingContextDialog, lozenge); + gtk_widget_class_bind_template_child (widget_class, GsAgeRatingContextDialog, title); + gtk_widget_class_bind_template_child (widget_class, GsAgeRatingContextDialog, attributes_list); +} + +/** + * gs_age_rating_context_dialog_new: + * @app: (nullable): the app to display age_rating context information for, or %NULL + * + * Create a new #GsAgeRatingContextDialog and set its initial app to @app. + * + * Returns: (transfer full): a new #GsAgeRatingContextDialog + * Since: 41 + */ +GsAgeRatingContextDialog * +gs_age_rating_context_dialog_new (GsApp *app) +{ + g_return_val_if_fail (app == NULL || GS_IS_APP (app), NULL); + + return g_object_new (GS_TYPE_AGE_RATING_CONTEXT_DIALOG, + "app", app, + NULL); +} + +/** + * gs_age_rating_context_dialog_get_app: + * @self: a #GsAgeRatingContextDialog + * + * Gets the value of #GsAgeRatingContextDialog:app. + * + * Returns: (nullable) (transfer none): app whose age_rating context information is + * being displayed, or %NULL if none is set + * Since: 41 + */ +GsApp * +gs_age_rating_context_dialog_get_app (GsAgeRatingContextDialog *self) +{ + g_return_val_if_fail (GS_IS_AGE_RATING_CONTEXT_DIALOG (self), NULL); + + return self->app; +} + +/** + * gs_age_rating_context_dialog_set_app: + * @self: a #GsAgeRatingContextDialog + * @app: (nullable) (transfer none): the app to display age_rating context + * information for, or %NULL for none + * + * Set the value of #GsAgeRatingContextDialog:app. + * + * Since: 41 + */ +void +gs_age_rating_context_dialog_set_app (GsAgeRatingContextDialog *self, + GsApp *app) +{ + g_return_if_fail (GS_IS_AGE_RATING_CONTEXT_DIALOG (self)); + g_return_if_fail (app == NULL || GS_IS_APP (app)); + + if (app == self->app) + return; + + g_clear_signal_handler (&self->app_notify_handler_content_rating, self->app); + g_clear_signal_handler (&self->app_notify_handler_name, self->app); + + g_set_object (&self->app, app); + + if (self->app != NULL) { + self->app_notify_handler_content_rating = g_signal_connect (self->app, "notify::content-rating", G_CALLBACK (app_notify_cb), self); + self->app_notify_handler_name = g_signal_connect (self->app, "notify::name", G_CALLBACK (app_notify_cb), self); + } + + /* Update the UI. */ + update_attributes_list (self); + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_APP]); +} -- cgit v1.2.3