summaryrefslogtreecommitdiffstats
path: root/src/gs-app-context-bar.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-app-context-bar.c')
-rw-r--r--src/gs-app-context-bar.c988
1 files changed, 988 insertions, 0 deletions
diff --git a/src/gs-app-context-bar.c b/src/gs-app-context-bar.c
new file mode 100644
index 0000000..2a21c87
--- /dev/null
+++ b/src/gs-app-context-bar.c
@@ -0,0 +1,988 @@
+/* -*- 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 <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-app-context-bar
+ * @short_description: A bar containing context tiles describing an app
+ *
+ * #GsAppContextBar is a bar which contains ‘context tiles’ to describe some of
+ * the key features of an app. Each tile describes one aspect of the app, such
+ * as its download/installed size, hardware requirements, or content rating.
+ * Tiles are intended to convey the most pertinent information about aspects of
+ * the app, leaving further detail to be shown in a more detailed dialog.
+ *
+ * The widget has no special appearance if the app is unset, so callers will
+ * typically want to hide the bar in that case.
+ *
+ * Since: 41
+ */
+
+#include "config.h"
+
+#include <adwaita.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <locale.h>
+
+#include "gs-age-rating-context-dialog.h"
+#include "gs-app.h"
+#include "gs-app-context-bar.h"
+#include "gs-common.h"
+#include "gs-hardware-support-context-dialog.h"
+#include "gs-lozenge.h"
+#include "gs-safety-context-dialog.h"
+#include "gs-storage-context-dialog.h"
+
+typedef struct
+{
+ GtkWidget *tile;
+ GtkWidget *lozenge;
+ GtkLabel *title;
+ GtkLabel *description;
+} GsAppContextTile;
+
+typedef enum
+{
+ STORAGE_TILE,
+ SAFETY_TILE,
+ HARDWARE_SUPPORT_TILE,
+ AGE_RATING_TILE,
+} GsAppContextTileType;
+#define N_TILE_TYPES (AGE_RATING_TILE + 1)
+
+struct _GsAppContextBar
+{
+ GtkBox parent_instance;
+
+ GsApp *app; /* (nullable) (owned) */
+ gulong app_notify_handler;
+
+ GsAppContextTile tiles[N_TILE_TYPES];
+};
+
+G_DEFINE_TYPE (GsAppContextBar, gs_app_context_bar, GTK_TYPE_BOX)
+
+typedef enum {
+ PROP_APP = 1,
+} GsAppContextBarProperty;
+
+static GParamSpec *obj_props[PROP_APP + 1] = { NULL, };
+
+/* Certain tiles only make sense for applications which the user can run, and
+ * not for (say) fonts.
+ *
+ * Update the visibility of the tile’s parent box to hide it if both tiles
+ * are hidden. */
+static gboolean
+show_tile_for_non_applications (GsAppContextBar *self,
+ GsAppContextTileType tile_type)
+{
+ GtkWidget *sibling;
+ GtkBox *parent_box;
+ gboolean any_siblings_visible;
+ AsComponentKind app_kind = gs_app_get_kind (self->app);
+ gboolean is_application = (app_kind == AS_COMPONENT_KIND_DESKTOP_APP ||
+ app_kind == AS_COMPONENT_KIND_CONSOLE_APP ||
+ app_kind == AS_COMPONENT_KIND_WEB_APP);
+
+ gtk_widget_set_visible (self->tiles[tile_type].tile, is_application);
+
+ parent_box = GTK_BOX (gtk_widget_get_parent (self->tiles[tile_type].tile));
+ g_assert (GTK_IS_BOX (parent_box));
+
+ any_siblings_visible = FALSE;
+
+ for (sibling = gtk_widget_get_first_child (GTK_WIDGET (parent_box));
+ sibling != NULL;
+ sibling = gtk_widget_get_next_sibling (sibling)) {
+ g_assert (GTK_IS_BUTTON (sibling));
+ any_siblings_visible |= gtk_widget_get_visible (sibling);
+ }
+
+ gtk_widget_set_visible (GTK_WIDGET (parent_box), any_siblings_visible);
+
+ return is_application;
+}
+
+static void
+update_storage_tile (GsAppContextBar *self)
+{
+ g_autofree gchar *lozenge_text = NULL;
+ gboolean lozenge_text_is_markup = FALSE;
+ const gchar *title;
+ g_autofree gchar *description = NULL;
+ guint64 size_bytes;
+ GsSizeType size_type;
+
+ g_assert (self->app != NULL);
+
+ if (gs_app_is_installed (self->app)) {
+ guint64 size_installed, size_user_data, size_cache_data;
+ GsSizeType size_installed_type, size_user_data_type, size_cache_data_type;
+ g_autofree gchar *size_user_data_str = NULL;
+ g_autofree gchar *size_cache_data_str = NULL;
+
+ size_installed_type = gs_app_get_size_installed (self->app, &size_installed);
+ size_user_data_type = gs_app_get_size_user_data (self->app, &size_user_data);
+ size_cache_data_type = gs_app_get_size_cache_data (self->app, &size_cache_data);
+
+ /* Treat `0` sizes as `unknown`, to not show `0 bytes` in the text. */
+ if (size_user_data == 0)
+ size_user_data_type = GS_SIZE_TYPE_UNKNOWN;
+ if (size_cache_data == 0)
+ size_cache_data_type = GS_SIZE_TYPE_UNKNOWN;
+
+ /* If any installed sizes are unknowable, ignore them. This
+ * means the stated installed size is a lower bound on the
+ * actual installed size.
+ * Don’t include dependencies in the stated installed size,
+ * because uninstalling the app won’t reclaim that space unless
+ * it’s the last app using those dependencies. */
+ size_bytes = size_installed;
+ size_type = size_installed_type;
+ if (size_user_data_type == GS_SIZE_TYPE_VALID)
+ size_bytes += size_user_data;
+ if (size_cache_data_type == GS_SIZE_TYPE_VALID)
+ size_bytes += size_cache_data;
+
+ size_user_data_str = g_format_size (size_user_data);
+ size_cache_data_str = g_format_size (size_cache_data);
+
+ /* Translators: The disk usage of an application when installed.
+ * This is displayed in a context tile, so the string should be short. */
+ title = _("Installed Size");
+
+ if (size_user_data_type == GS_SIZE_TYPE_VALID && size_cache_data_type == GS_SIZE_TYPE_VALID)
+ description = g_strdup_printf (_("Includes %s of data and %s of cache"),
+ size_user_data_str, size_cache_data_str);
+ else if (size_user_data_type == GS_SIZE_TYPE_VALID)
+ description = g_strdup_printf (_("Includes %s of data"),
+ size_user_data_str);
+ else if (size_cache_data_type == GS_SIZE_TYPE_VALID)
+ description = g_strdup_printf (_("Includes %s of cache"),
+ size_cache_data_str);
+ else
+ description = g_strdup (_("Cache and data usage unknown"));
+ } else {
+ guint64 app_download_size_bytes, dependencies_download_size_bytes;
+ GsSizeType app_download_size_type, dependencies_download_size_type;
+
+ app_download_size_type = gs_app_get_size_download (self->app, &app_download_size_bytes);
+ dependencies_download_size_type = gs_app_get_size_download_dependencies (self->app, &dependencies_download_size_bytes);
+
+ size_bytes = app_download_size_bytes;
+ size_type = app_download_size_type;
+
+ /* Translators: The download size of an application.
+ * This is displayed in a context tile, so the string should be short. */
+ title = _("Download Size");
+
+ if (dependencies_download_size_type == GS_SIZE_TYPE_VALID &&
+ dependencies_download_size_bytes == 0) {
+ description = g_strdup (_("Needs no additional system downloads"));
+ } else if (dependencies_download_size_type != GS_SIZE_TYPE_VALID) {
+ description = g_strdup (_("Needs an unknown size of additional system downloads"));
+ } else {
+ g_autofree gchar *size = g_format_size (dependencies_download_size_bytes);
+ /* Translators: The placeholder is for a size string,
+ * such as ‘150 MB’ or ‘1.5 GB’. */
+ description = g_strdup_printf (_("Needs %s of additional system downloads"), size);
+ }
+ }
+
+ if (size_type != GS_SIZE_TYPE_VALID) {
+ /* Translators: This is displayed for the download size in an
+ * app’s context tile if the size is unknown. It should be short
+ * (at most a couple of characters wide). */
+ lozenge_text = g_strdup (_("?"));
+
+ g_free (description);
+ /* Translators: Displayed if the download or installed size of
+ * an app could not be determined.
+ * This is displayed in a context tile, so the string should be short. */
+ description = g_strdup (_("Size is unknown"));
+ } else {
+ lozenge_text = gs_utils_format_size (size_bytes, &lozenge_text_is_markup);
+ }
+
+ if (lozenge_text_is_markup)
+ gs_lozenge_set_markup (GS_LOZENGE (self->tiles[STORAGE_TILE].lozenge), lozenge_text);
+ else
+ gs_lozenge_set_text (GS_LOZENGE (self->tiles[STORAGE_TILE].lozenge), lozenge_text);
+ gtk_label_set_text (self->tiles[STORAGE_TILE].title, title);
+ gtk_label_set_text (self->tiles[STORAGE_TILE].description, description);
+}
+
+typedef enum
+{
+ /* The code in this file relies on the fact that these enum values
+ * numerically increase as they get more unsafe. */
+ SAFETY_SAFE,
+ SAFETY_POTENTIALLY_UNSAFE,
+ SAFETY_UNSAFE
+} SafetyRating;
+
+static void
+add_to_safety_rating (SafetyRating *chosen_rating,
+ GPtrArray *descriptions,
+ SafetyRating item_rating,
+ const gchar *item_description)
+{
+ /* Clear the existing descriptions and replace with @item_description if
+ * this item increases the @chosen_rating. This means the final list of
+ * @descriptions will only be the items which caused @chosen_rating to
+ * be so high. */
+ if (item_rating > *chosen_rating) {
+ g_ptr_array_set_size (descriptions, 0);
+ *chosen_rating = item_rating;
+ }
+
+ if (item_rating == *chosen_rating)
+ g_ptr_array_add (descriptions, (gpointer) item_description);
+}
+
+static void
+update_safety_tile (GsAppContextBar *self)
+{
+ const gchar *icon_name, *title, *css_class;
+ g_autofree gchar *description = NULL;
+ g_autoptr(GPtrArray) descriptions = g_ptr_array_new_with_free_func (NULL);
+ g_autoptr(GsAppPermissions) permissions = NULL;
+ GsAppPermissionsFlags perm_flags = GS_APP_PERMISSIONS_FLAGS_UNKNOWN;
+ GtkStyleContext *context;
+
+ /* Treat everything as safe to begin with, and downgrade its safety
+ * based on app properties. */
+ SafetyRating chosen_rating = SAFETY_SAFE;
+
+ g_assert (self->app != NULL);
+
+ permissions = gs_app_dup_permissions (self->app);
+ if (permissions != NULL)
+ perm_flags = gs_app_permissions_get_flags (permissions);
+ for (GsAppPermissionsFlags i = GS_APP_PERMISSIONS_FLAGS_NONE; i < GS_APP_PERMISSIONS_FLAGS_LAST; i <<= 1) {
+ if (!(perm_flags & i))
+ continue;
+
+ switch (i) {
+ case GS_APP_PERMISSIONS_FLAGS_NONE:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_SAFE,
+ /* Translators: This indicates an app requires no permissions to run.
+ * It’s used in a context tile, so should be short. */
+ _("No permissions"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_NETWORK:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ /* This isn’t actually safe (network access can expand a local
+ * vulnerability into a remotely exploitable one), but it’s
+ * needed commonly enough that marking it as
+ * %SAFETY_POTENTIALLY_UNSAFE is too noisy. */
+ SAFETY_SAFE,
+ /* Translators: This indicates an app uses the network.
+ * It’s used in a context tile, so should be short. */
+ _("Has network access"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_SYSTEM_BUS:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_POTENTIALLY_UNSAFE,
+ /* Translators: This indicates an app uses D-Bus system services.
+ * It’s used in a context tile, so should be short. */
+ _("Uses system services"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_SESSION_BUS:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_UNSAFE,
+ /* Translators: This indicates an app uses D-Bus session services.
+ * It’s used in a context tile, so should be short. */
+ _("Uses session services"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_DEVICES:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_POTENTIALLY_UNSAFE,
+ /* Translators: This indicates an app can access arbitrary hardware devices.
+ * It’s used in a context tile, so should be short. */
+ _("Can access hardware devices"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_HOME_FULL:
+ case GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL:
+ /* Don’t add twice. */
+ if (i == GS_APP_PERMISSIONS_FLAGS_HOME_FULL && (perm_flags & GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_FULL))
+ break;
+
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_UNSAFE,
+ /* Translators: This indicates an app can read/write to the user’s home or the entire filesystem.
+ * It’s used in a context tile, so should be short. */
+ _("Can read/write all your data"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_HOME_READ:
+ case GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_READ:
+ /* Don’t add twice. */
+ if (i == GS_APP_PERMISSIONS_FLAGS_HOME_READ && (perm_flags & GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_READ))
+ break;
+
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_UNSAFE,
+ /* Translators: This indicates an app can read (but not write) from the user’s home or the entire filesystem.
+ * It’s used in a context tile, so should be short. */
+ _("Can read all your data"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_FULL:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_POTENTIALLY_UNSAFE,
+ /* Translators: This indicates an app can read/write to the user’s Downloads directory.
+ * It’s used in a context tile, so should be short. */
+ _("Can read/write your downloads"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_DOWNLOADS_READ:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_POTENTIALLY_UNSAFE,
+ /* Translators: This indicates an app can read (but not write) from the user’s Downloads directory.
+ * It’s used in a context tile, so should be short. */
+ _("Can read your downloads"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_FILESYSTEM_OTHER:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_POTENTIALLY_UNSAFE,
+ /* Translators: This indicates an app can access data in the system unknown to the Software.
+ * It’s used in a context tile, so should be short. */
+ _("Can access arbitrary files"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_SETTINGS:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_POTENTIALLY_UNSAFE,
+ /* Translators: This indicates an app can access or change user settings.
+ * It’s used in a context tile, so should be short. */
+ _("Can access and change user settings"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_X11:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_UNSAFE,
+ /* Translators: This indicates an app uses the X11 windowing system.
+ * It’s used in a context tile, so should be short. */
+ _("Uses a legacy windowing system"));
+ break;
+ case GS_APP_PERMISSIONS_FLAGS_ESCAPE_SANDBOX:
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_UNSAFE,
+ /* Translators: This indicates an app can escape its sandbox.
+ * It’s used in a context tile, so should be short. */
+ _("Can acquire arbitrary permissions"));
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* Unknown permissions typically come from non-sandboxed packaging
+ * systems like RPM or DEB. Telling the user the software has unknown
+ * permissions is unhelpful; it’s more relevant to say it’s not
+ * sandboxed but is (or is not) packaged by a trusted vendor. They will
+ * have (at least) done some basic checks to make sure the software is
+ * not overtly malware. That doesn’t protect the user from exploitable
+ * bugs in the software, but it does mean they’re not accidentally
+ * installing something which is actively malicious.
+ *
+ * FIXME: We could do better by potentially adding a ‘trusted’ state
+ * to indicate that something is probably safe, but isn’t sandboxed.
+ * See https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1451 */
+ if (perm_flags == GS_APP_PERMISSIONS_FLAGS_UNKNOWN &&
+ gs_app_has_quirk (self->app, GS_APP_QUIRK_PROVENANCE))
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_SAFE,
+ /* Translators: This indicates that an application has been packaged
+ * by the user’s distribution and is safe.
+ * It’s used in a context tile, so should be short. */
+ _("Reviewed by your distribution"));
+ else if (perm_flags == GS_APP_PERMISSIONS_FLAGS_UNKNOWN)
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_POTENTIALLY_UNSAFE,
+ /* Translators: This indicates that an application has been packaged
+ * by someone other than the user’s distribution, so might not be safe.
+ * It’s used in a context tile, so should be short. */
+ _("Provided by a third party"));
+
+ /* Is the code FOSS and hence inspectable? This doesn’t distinguish
+ * between closed source and open-source-but-not-FOSS software, even
+ * though the code of the latter is technically publicly auditable. This
+ * is because I don’t want to get into the business of maintaining lists
+ * of ‘auditable’ source code licenses. */
+ if (!gs_app_get_license_is_free (self->app))
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_POTENTIALLY_UNSAFE,
+ /* Translators: This indicates an app is not licensed under a free software license.
+ * It’s used in a context tile, so should be short. */
+ _("Proprietary code"));
+ else
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_SAFE,
+ /* Translators: This indicates an app’s source code is freely available, so can be audited for security.
+ * It’s used in a context tile, so should be short. */
+ _("Auditable code"));
+
+ if (gs_app_has_quirk (self->app, GS_APP_QUIRK_DEVELOPER_VERIFIED))
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_SAFE,
+ /* Translators: This indicates an app was written and released by a developer who has been verified.
+ * It’s used in a context tile, so should be short. */
+ _("Software developer is verified"));
+
+ if (gs_app_get_metadata_item (self->app, "GnomeSoftware::EolReason") != NULL || (
+ gs_app_get_runtime (self->app) != NULL &&
+ gs_app_get_metadata_item (gs_app_get_runtime (self->app), "GnomeSoftware::EolReason") != NULL))
+ add_to_safety_rating (&chosen_rating, descriptions,
+ SAFETY_UNSAFE,
+ /* Translators: This indicates an app or its runtime reached its end of life.
+ * It’s used in a context tile, so should be short. */
+ _("Software no longer supported"));
+
+ g_assert (descriptions->len > 0);
+
+ g_ptr_array_add (descriptions, NULL);
+ /* Translators: This string is used to join various other translated
+ * strings into an inline list of reasons why an app has been marked as
+ * ‘safe’, ‘potentially safe’ or ‘unsafe’. For example:
+ * “App comes from a trusted source; Auditable code; No permissions”
+ * If concatenating strings as a list using a separator like this is not
+ * possible in your language, please file an issue against gnome-software:
+ * https://gitlab.gnome.org/GNOME/gnome-software/-/issues/new */
+ description = g_strjoinv (_("; "), (gchar **) descriptions->pdata);
+
+ /* Update the UI. */
+ switch (chosen_rating) {
+ case SAFETY_SAFE:
+ icon_name = "safety-symbolic";
+ /* Translators: The app is considered safe to install and run.
+ * This is displayed in a context tile, so the string should be short. */
+ title = _("Safe");
+ css_class = "green";
+ break;
+ case SAFETY_POTENTIALLY_UNSAFE:
+ icon_name = "dialog-question-symbolic";
+ /* Translators: The app is considered potentially unsafe to install and run.
+ * This is displayed in a context tile, so the string should be short. */
+ title = _("Potentially Unsafe");
+ css_class = "yellow";
+ break;
+ case SAFETY_UNSAFE:
+ icon_name = "dialog-warning-symbolic";
+ /* Translators: The app is considered unsafe to install and run.
+ * This is displayed in a context tile, so the string should be short. */
+ title = _("Unsafe");
+ css_class = "red";
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ gs_lozenge_set_icon_name (GS_LOZENGE (self->tiles[SAFETY_TILE].lozenge), icon_name);
+ gtk_label_set_text (self->tiles[SAFETY_TILE].title, title);
+ gtk_label_set_text (self->tiles[SAFETY_TILE].description, description);
+
+ context = gtk_widget_get_style_context (self->tiles[SAFETY_TILE].lozenge);
+
+ gtk_style_context_remove_class (context, "green");
+ gtk_style_context_remove_class (context, "yellow");
+ gtk_style_context_remove_class (context, "red");
+
+ gtk_style_context_add_class (context, css_class);
+}
+
+typedef struct {
+ guint min;
+ guint max;
+} Range;
+
+static void
+update_hardware_support_tile (GsAppContextBar *self)
+{
+ g_autoptr(GPtrArray) relations = NULL;
+ AsRelationKind control_relations[AS_CONTROL_KIND_LAST] = { AS_RELATION_KIND_UNKNOWN, };
+ GdkDisplay *display;
+ GdkMonitor *monitor = NULL;
+ gboolean any_control_relations_set;
+ const gchar *icon_name = NULL, *title = NULL, *description = NULL, *css_class = NULL;
+ gboolean has_touchscreen = FALSE, has_keyboard = FALSE, has_mouse = FALSE;
+ GtkStyleContext *context;
+
+ g_assert (self->app != NULL);
+
+ /* Don’t show the hardware support tile for non-desktop applications. */
+ if (!show_tile_for_non_applications (self, HARDWARE_SUPPORT_TILE))
+ return;
+
+ relations = gs_app_get_relations (self->app);
+
+ /* Extract the %AS_RELATION_ITEM_KIND_CONTROL relations and summarise
+ * them. */
+ display = gtk_widget_get_display (GTK_WIDGET (self));
+ gs_hardware_support_context_dialog_get_control_support (display, relations,
+ &any_control_relations_set,
+ control_relations,
+ &has_touchscreen,
+ &has_keyboard,
+ &has_mouse);
+
+ /* Warn about screen size mismatches. Compare against the largest
+ * monitor associated with this widget’s #GdkDisplay, defaulting to
+ * the primary monitor.
+ *
+ * See https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-requires-recommends-display_length
+ * for the semantics of the display length relations.*/
+ if (display != NULL)
+ monitor = gs_hardware_support_context_dialog_get_largest_monitor (display);
+
+ if (monitor != NULL) {
+ AsRelationKind desktop_relation_kind, mobile_relation_kind, current_relation_kind;
+ gboolean desktop_match, mobile_match, current_match;
+
+ gs_hardware_support_context_dialog_get_display_support (monitor, relations,
+ NULL,
+ &desktop_match, &desktop_relation_kind,
+ &mobile_match, &mobile_relation_kind,
+ &current_match, &current_relation_kind);
+
+ /* If the current screen size is not supported, try and
+ * summarise the restrictions into a single context tile. */
+ if (!current_match &&
+ !mobile_match && mobile_relation_kind == AS_RELATION_KIND_REQUIRES) {
+ icon_name = "phone-symbolic";
+ title = _("Mobile Only");
+ description = _("Only works on a small screen");
+ css_class = "red";
+ } else if (!current_match &&
+ !desktop_match && desktop_relation_kind == AS_RELATION_KIND_REQUIRES) {
+ icon_name = "desktop-symbolic";
+ title = _("Desktop Only");
+ description = _("Only works on a large screen");
+ css_class = "red";
+ } else if (!current_match && current_relation_kind == AS_RELATION_KIND_REQUIRES) {
+ icon_name = "desktop-symbolic";
+ title = _("Screen Size Mismatch");
+ description = _("Doesn’t support your current screen size");
+ css_class = "red";
+ }
+ }
+
+ /* Warn about missing touchscreen or keyboard support. There are some
+ * assumptions here that certain input devices are only available on
+ * certain platforms; they can change in future.
+ *
+ * As with the rest of the tile contents in this function, tile contents
+ * which are checked lower down in the function are only used if nothing
+ * more important has already been set earlier.
+ *
+ * The available information is being summarised to quite an extreme
+ * degree here, and it’s likely this code will have to evolve for
+ * corner cases in future. */
+ if (icon_name == NULL &&
+ control_relations[AS_CONTROL_KIND_TOUCH] == AS_RELATION_KIND_REQUIRES &&
+ !has_touchscreen) {
+ icon_name = "phone-symbolic";
+ title = _("Mobile Only");
+ description = _("Requires a touchscreen");
+ css_class = "red";
+ } else if (icon_name == NULL &&
+ control_relations[AS_CONTROL_KIND_KEYBOARD] == AS_RELATION_KIND_REQUIRES &&
+ !has_keyboard) {
+ icon_name = "input-keyboard-symbolic";
+ title = _("Desktop Only");
+ description = _("Requires a keyboard");
+ css_class = "red";
+ } else if (icon_name == NULL &&
+ control_relations[AS_CONTROL_KIND_POINTING] == AS_RELATION_KIND_REQUIRES &&
+ !has_mouse) {
+ icon_name = "input-mouse-symbolic";
+ title = _("Desktop Only");
+ description = _("Requires a mouse");
+ css_class = "red";
+ }
+
+ /* Say if the app requires a gamepad. We can’t reliably detect whether
+ * the computer has a gamepad, as it might be unplugged unless the user
+ * is currently playing a game. So this might be shown even if the user
+ * has a gamepad available. */
+ if (icon_name == NULL &&
+ control_relations[AS_CONTROL_KIND_GAMEPAD] == AS_RELATION_KIND_REQUIRES) {
+ icon_name = "input-gaming-symbolic";
+ title = _("Gamepad Needed");
+ description = _("Requires a gamepad to play");
+ css_class = "yellow";
+ }
+
+ /* Otherwise, is it adaptive? Note that %AS_RELATION_KIND_RECOMMENDS
+ * means more like ‘supports’ than ‘recommends’. */
+#if AS_CHECK_VERSION(0, 15, 0)
+ if (icon_name == NULL &&
+ (control_relations[AS_CONTROL_KIND_TOUCH] == AS_RELATION_KIND_RECOMMENDS ||
+ control_relations[AS_CONTROL_KIND_TOUCH] == AS_RELATION_KIND_SUPPORTS) &&
+ (control_relations[AS_CONTROL_KIND_KEYBOARD] == AS_RELATION_KIND_RECOMMENDS ||
+ control_relations[AS_CONTROL_KIND_KEYBOARD] == AS_RELATION_KIND_SUPPORTS) &&
+ (control_relations[AS_CONTROL_KIND_POINTING] == AS_RELATION_KIND_RECOMMENDS ||
+ control_relations[AS_CONTROL_KIND_POINTING] == AS_RELATION_KIND_SUPPORTS)) {
+#else
+ if (icon_name == NULL &&
+ control_relations[AS_CONTROL_KIND_TOUCH] == AS_RELATION_KIND_RECOMMENDS &&
+ control_relations[AS_CONTROL_KIND_KEYBOARD] == AS_RELATION_KIND_RECOMMENDS &&
+ control_relations[AS_CONTROL_KIND_POINTING] == AS_RELATION_KIND_RECOMMENDS) {
+#endif
+ icon_name = "adaptive-symbolic";
+ /* Translators: This is used in a context tile to indicate that
+ * an app works on phones, tablets *and* desktops. It should be
+ * short and in title case. */
+ title = _("Adaptive");
+ description = _("Works on phones, tablets and desktops");
+ css_class = "green";
+ }
+
+ /* Fallback. At the moment (June 2021) almost no apps have any metadata
+ * about hardware support, so this case will be hit most of the time.
+ *
+ * So in the absence of any other information, assume that all apps
+ * support desktop, and none support mobile. */
+ if (icon_name == NULL) {
+ if (!has_keyboard || !has_mouse) {
+ icon_name = "desktop-symbolic";
+ title = _("Desktop Only");
+ description = _("Probably requires a keyboard or mouse");
+ css_class = "yellow";
+ } else {
+ icon_name = "desktop-symbolic";
+ title = _("Desktop Only");
+ description = _("Works on desktops and laptops");
+ css_class = "grey";
+ }
+ }
+
+ /* Update the UI. The `adaptive-symbolic` icon needs a special size to
+ * be set, as it is wider than it is tall. Setting the size ensures it’s
+ * rendered at the right height. */
+ gs_lozenge_set_icon_name (GS_LOZENGE (self->tiles[HARDWARE_SUPPORT_TILE].lozenge), icon_name);
+ gs_lozenge_set_pixel_size (GS_LOZENGE (self->tiles[HARDWARE_SUPPORT_TILE].lozenge), g_str_equal (icon_name, "adaptive-symbolic") ? 56 : -1);
+
+ gtk_label_set_text (self->tiles[HARDWARE_SUPPORT_TILE].title, title);
+ gtk_label_set_text (self->tiles[HARDWARE_SUPPORT_TILE].description, description);
+
+ context = gtk_widget_get_style_context (self->tiles[HARDWARE_SUPPORT_TILE].lozenge);
+
+ gtk_style_context_remove_class (context, "green");
+ gtk_style_context_remove_class (context, "yellow");
+ gtk_style_context_remove_class (context, "red");
+
+ gtk_style_context_add_class (context, css_class);
+
+ if (g_str_equal (icon_name, "adaptive-symbolic"))
+ gtk_style_context_add_class (context, "wide-image");
+ else
+ gtk_style_context_remove_class (context, "wide-image");
+}
+
+static void
+build_age_rating_description_cb (const gchar *attribute,
+ AsContentRatingValue value,
+ gpointer user_data)
+{
+ GPtrArray *descriptions = user_data;
+ const gchar *description;
+
+ /* (attribute == NULL) is used by the caller to indicate that no
+ * attributes apply. This callback will be called at most once like
+ * that. */
+ if (attribute == NULL)
+ /* Translators: This indicates that the content rating for an
+ * app says it can be used by all ages of people, as it contains
+ * no objectionable content. */
+ description = _("Contains no age-inappropriate content");
+ else
+ description = as_content_rating_attribute_get_description (attribute, value);
+
+ g_ptr_array_add (descriptions, (gpointer) description);
+}
+
+static gchar *
+build_age_rating_description (AsContentRating *content_rating)
+{
+ g_autoptr(GPtrArray) descriptions = g_ptr_array_new_with_free_func (NULL);
+
+ gs_age_rating_context_dialog_process_attributes (content_rating,
+ TRUE,
+ build_age_rating_description_cb,
+ descriptions);
+
+ g_ptr_array_add (descriptions, NULL);
+ /* Translators: This string is used to join various other translated
+ * strings into an inline list of reasons why an app has been given a
+ * certain content rating. For example:
+ * “References to alcoholic beverages; Moderated chat functionality between users”
+ * If concatenating strings as a list using a separator like this is not
+ * possible in your language, please file an issue against gnome-software:
+ * https://gitlab.gnome.org/GNOME/gnome-software/-/issues/new */
+ return g_strjoinv (_("; "), (gchar **) descriptions->pdata);
+}
+
+static void
+update_age_rating_tile (GsAppContextBar *self)
+{
+ g_autoptr(AsContentRating) content_rating = NULL;
+ gboolean is_unknown;
+ g_autofree gchar *description = NULL;
+
+ g_assert (self->app != NULL);
+
+ /* Don’t show the age rating tile for non-desktop applications. */
+ if (!show_tile_for_non_applications (self, AGE_RATING_TILE))
+ return;
+
+ content_rating = gs_app_dup_content_rating (self->app);
+ gs_age_rating_context_dialog_update_lozenge (self->app,
+ GS_LOZENGE (self->tiles[AGE_RATING_TILE].lozenge),
+ &is_unknown);
+
+ /* Description */
+ if (content_rating == NULL || is_unknown) {
+ description = g_strdup (_("No age rating information available"));
+ } else {
+ description = build_age_rating_description (content_rating);
+ }
+
+ gtk_label_set_text (self->tiles[AGE_RATING_TILE].description, description);
+
+ /* Disable the button if no content rating information is available, as
+ * it would only show a dialogue full of rows saying ‘Unknown’ */
+ gtk_widget_set_sensitive (self->tiles[AGE_RATING_TILE].tile, (content_rating != NULL));
+}
+
+static void
+update_tiles (GsAppContextBar *self)
+{
+ if (self->app == NULL)
+ return;
+
+ update_storage_tile (self);
+ update_safety_tile (self);
+ update_hardware_support_tile (self);
+ update_age_rating_tile (self);
+}
+
+static void
+app_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsAppContextBar *self = GS_APP_CONTEXT_BAR (user_data);
+
+ update_tiles (self);
+}
+
+static void
+tile_clicked_cb (GtkWidget *widget,
+ gpointer user_data)
+{
+ GsAppContextBar *self = GS_APP_CONTEXT_BAR (user_data);
+ GtkWindow *dialog;
+ GtkRoot *root = gtk_widget_get_root (widget);
+
+ if (GTK_IS_WINDOW (root)) {
+ if (widget == self->tiles[STORAGE_TILE].tile)
+ dialog = GTK_WINDOW (gs_storage_context_dialog_new (self->app));
+ else if (widget == self->tiles[SAFETY_TILE].tile)
+ dialog = GTK_WINDOW (gs_safety_context_dialog_new (self->app));
+ else if (widget == self->tiles[HARDWARE_SUPPORT_TILE].tile)
+ dialog = GTK_WINDOW (gs_hardware_support_context_dialog_new (self->app));
+ else if (widget == self->tiles[AGE_RATING_TILE].tile)
+ dialog = GTK_WINDOW (gs_age_rating_context_dialog_new (self->app));
+ else
+ g_assert_not_reached ();
+
+ gtk_window_set_transient_for (dialog, GTK_WINDOW (root));
+ gtk_widget_show (GTK_WIDGET (dialog));
+ }
+}
+
+static void
+gs_app_context_bar_init (GsAppContextBar *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+gs_app_context_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsAppContextBar *self = GS_APP_CONTEXT_BAR (object);
+
+ switch ((GsAppContextBarProperty) prop_id) {
+ case PROP_APP:
+ g_value_set_object (value, gs_app_context_bar_get_app (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_context_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsAppContextBar *self = GS_APP_CONTEXT_BAR (object);
+
+ switch ((GsAppContextBarProperty) prop_id) {
+ case PROP_APP:
+ gs_app_context_bar_set_app (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_context_bar_dispose (GObject *object)
+{
+ GsAppContextBar *self = GS_APP_CONTEXT_BAR (object);
+
+ if (self->app_notify_handler != 0) {
+ g_signal_handler_disconnect (self->app, self->app_notify_handler);
+ self->app_notify_handler = 0;
+ }
+ g_clear_object (&self->app);
+
+ G_OBJECT_CLASS (gs_app_context_bar_parent_class)->dispose (object);
+}
+
+static void
+gs_app_context_bar_class_init (GsAppContextBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gs_app_context_bar_get_property;
+ object_class->set_property = gs_app_context_bar_set_property;
+ object_class->dispose = gs_app_context_bar_dispose;
+
+ /**
+ * GsAppContextBar:app: (nullable)
+ *
+ * The app to display the 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_css_name (widget_class, "app-context-bar");
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-app-context-bar.ui");
+
+ gtk_widget_class_bind_template_child_full (widget_class, "storage_tile", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[STORAGE_TILE].tile));
+ gtk_widget_class_bind_template_child_full (widget_class, "storage_tile_lozenge", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[STORAGE_TILE].lozenge));
+ gtk_widget_class_bind_template_child_full (widget_class, "storage_tile_title", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[STORAGE_TILE].title));
+ gtk_widget_class_bind_template_child_full (widget_class, "storage_tile_description", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[STORAGE_TILE].description));
+ gtk_widget_class_bind_template_child_full (widget_class, "safety_tile", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[SAFETY_TILE].tile));
+ gtk_widget_class_bind_template_child_full (widget_class, "safety_tile_lozenge", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[SAFETY_TILE].lozenge));
+ gtk_widget_class_bind_template_child_full (widget_class, "safety_tile_title", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[SAFETY_TILE].title));
+ gtk_widget_class_bind_template_child_full (widget_class, "safety_tile_description", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[SAFETY_TILE].description));
+ gtk_widget_class_bind_template_child_full (widget_class, "hardware_support_tile", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[HARDWARE_SUPPORT_TILE].tile));
+ gtk_widget_class_bind_template_child_full (widget_class, "hardware_support_tile_lozenge", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[HARDWARE_SUPPORT_TILE].lozenge));
+ gtk_widget_class_bind_template_child_full (widget_class, "hardware_support_tile_title", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[HARDWARE_SUPPORT_TILE].title));
+ gtk_widget_class_bind_template_child_full (widget_class, "hardware_support_tile_description", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[HARDWARE_SUPPORT_TILE].description));
+ gtk_widget_class_bind_template_child_full (widget_class, "age_rating_tile", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[AGE_RATING_TILE].tile));
+ gtk_widget_class_bind_template_child_full (widget_class, "age_rating_tile_lozenge", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[AGE_RATING_TILE].lozenge));
+ gtk_widget_class_bind_template_child_full (widget_class, "age_rating_tile_title", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[AGE_RATING_TILE].title));
+ gtk_widget_class_bind_template_child_full (widget_class, "age_rating_tile_description", FALSE, G_STRUCT_OFFSET (GsAppContextBar, tiles[AGE_RATING_TILE].description));
+ gtk_widget_class_bind_template_callback (widget_class, tile_clicked_cb);
+}
+
+/**
+ * gs_app_context_bar_new:
+ * @app: (nullable): the app to display context tiles for, or %NULL
+ *
+ * Create a new #GsAppContextBar and set its initial app to @app.
+ *
+ * Returns: (transfer full): a new #GsAppContextBar
+ * Since: 41
+ */
+GtkWidget *
+gs_app_context_bar_new (GsApp *app)
+{
+ g_return_val_if_fail (app == NULL || GS_IS_APP (app), NULL);
+
+ return g_object_new (GS_TYPE_APP_CONTEXT_BAR,
+ "app", app,
+ NULL);
+}
+
+/**
+ * gs_app_context_bar_get_app:
+ * @self: a #GsAppContextBar
+ *
+ * Gets the value of #GsAppContextBar:app.
+ *
+ * Returns: (nullable) (transfer none): app whose context tiles are being
+ * displayed, or %NULL if none is set
+ * Since: 41
+ */
+GsApp *
+gs_app_context_bar_get_app (GsAppContextBar *self)
+{
+ g_return_val_if_fail (GS_IS_APP_CONTEXT_BAR (self), NULL);
+
+ return self->app;
+}
+
+/**
+ * gs_app_context_bar_set_app:
+ * @self: a #GsAppContextBar
+ * @app: (nullable) (transfer none): the app to display context tiles for,
+ * or %NULL for none
+ *
+ * Set the value of #GsAppContextBar:app.
+ *
+ * Since: 41
+ */
+void
+gs_app_context_bar_set_app (GsAppContextBar *self,
+ GsApp *app)
+{
+ g_return_if_fail (GS_IS_APP_CONTEXT_BAR (self));
+ g_return_if_fail (app == NULL || GS_IS_APP (app));
+
+ if (app == self->app)
+ return;
+
+ if (self->app_notify_handler != 0) {
+ g_signal_handler_disconnect (self->app, self->app_notify_handler);
+ self->app_notify_handler = 0;
+ }
+
+ g_set_object (&self->app, app);
+
+ if (self->app != NULL)
+ self->app_notify_handler = g_signal_connect (self->app, "notify", G_CALLBACK (app_notify_cb), self);
+
+ /* Update the tiles. */
+ update_tiles (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_APP]);
+}