diff options
Diffstat (limited to '')
-rw-r--r-- | src/gs-hardware-support-context-dialog.c | 920 |
1 files changed, 920 insertions, 0 deletions
diff --git a/src/gs-hardware-support-context-dialog.c b/src/gs-hardware-support-context-dialog.c new file mode 100644 index 0000000..589ea78 --- /dev/null +++ b/src/gs-hardware-support-context-dialog.c @@ -0,0 +1,920 @@ +/* -*- 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-hardware-support-context-dialog + * @short_description: A dialog showing hardware support information about an app + * + * #GsHardwareSupportContextDialog is a dialog which shows detailed information + * about what hardware an app requires or recommends to be used when running it. + * For example, what input devices it requires, and what display sizes it + * supports. This information is derived from the `<requires>`, + * `<recommends>` and `<supports>` elements in the app’s appdata. + * + * Currently, `<supports>` is treated as a synonym of `<recommends>` as it’s + * only just been introduced into the appstream standard, and many apps which + * should be using `<supports>` are still using `<recommends>`. + * + * It is designed to show a more detailed view of the information which the + * app’s hardware support 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 <adwaita.h> +#include <glib.h> +#include <glib-object.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <locale.h> + +#include "gs-app.h" +#include "gs-common.h" +#include "gs-context-dialog-row.h" +#include "gs-hardware-support-context-dialog.h" +#include "gs-lozenge.h" + +struct _GsHardwareSupportContextDialog +{ + GsInfoWindow parent_instance; + + GsApp *app; /* (nullable) (owned) */ + gulong app_notify_handler_relations; + gulong app_notify_handler_name; + + GtkWidget *lozenge; + GtkLabel *title; + GtkListBox *relations_list; +}; + +G_DEFINE_TYPE (GsHardwareSupportContextDialog, gs_hardware_support_context_dialog, GS_TYPE_INFO_WINDOW) + +typedef enum { + PROP_APP = 1, +} GsHardwareSupportContextDialogProperty; + +static GParamSpec *obj_props[PROP_APP + 1] = { NULL, }; + +typedef enum { + MATCH_STATE_NO_MATCH = 0, + MATCH_STATE_MATCH = 1, + MATCH_STATE_UNKNOWN, +} MatchState; + +/* The `icon_name_*`, `title_*` and `description_*` arguments are all nullable. + * If a row would be added with %NULL values, it is not added. */ +static void +add_relation_row (GtkListBox *list_box, + GsContextDialogRowImportance *chosen_rating, + AsRelationKind control_relation_kind, + MatchState match_state, + gboolean any_control_relations_set, + const gchar *icon_name_required_matches, + const gchar *title_required_matches, + const gchar *description_required_matches, + const gchar *icon_name_no_relation, + const gchar *title_no_relation, + const gchar *description_no_relation, + const gchar *icon_name_required_no_match, + const gchar *title_required_no_match, + const gchar *description_required_no_match, + const gchar *icon_name_recommends, + const gchar *title_recommends, + const gchar *description_recommends, + const gchar *icon_name_unsupported, + const gchar *title_unsupported, + const gchar *description_unsupported) +{ + GtkListBoxRow *row; + GsContextDialogRowImportance rating; + const gchar *icon_name, *title, *description; + + g_assert (control_relation_kind == AS_RELATION_KIND_UNKNOWN || any_control_relations_set); + + switch (control_relation_kind) { + case AS_RELATION_KIND_UNKNOWN: + if (!any_control_relations_set) { + rating = GS_CONTEXT_DIALOG_ROW_IMPORTANCE_NEUTRAL; + icon_name = icon_name_no_relation; + title = title_no_relation; + description = description_no_relation; + } else { + rating = GS_CONTEXT_DIALOG_ROW_IMPORTANCE_WARNING; + icon_name = icon_name_unsupported; + title = title_unsupported; + description = description_unsupported; + } + break; + case AS_RELATION_KIND_REQUIRES: + if (match_state == MATCH_STATE_MATCH) { + rating = GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT; + icon_name = icon_name_required_matches; + title = title_required_matches; + description = description_required_matches; + } else { + rating = (match_state == MATCH_STATE_NO_MATCH) ? GS_CONTEXT_DIALOG_ROW_IMPORTANCE_IMPORTANT : GS_CONTEXT_DIALOG_ROW_IMPORTANCE_WARNING; + icon_name = icon_name_required_no_match; + title = title_required_no_match; + description = description_required_no_match; + } + break; + case AS_RELATION_KIND_RECOMMENDS: +#if AS_CHECK_VERSION(0, 15, 0) + case AS_RELATION_KIND_SUPPORTS: +#endif + rating = GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT; + icon_name = icon_name_recommends; + title = title_recommends; + description = description_recommends; + break; + default: + g_assert_not_reached (); + } + + if (icon_name == NULL) + return; + + if (rating > *chosen_rating) + *chosen_rating = rating; + + row = gs_context_dialog_row_new (icon_name, rating, title, description); + gtk_list_box_append (list_box, GTK_WIDGET (row)); +} + +/** + * gs_hardware_support_context_dialog_get_largest_monitor: + * @display: a #GdkDisplay + * + * Get the largest monitor associated with @display, comparing the larger of the + * monitor’s width and height, and breaking ties between equally-large monitors + * using gdk_monitor_is_primary(). + * + * Returns: (nullable) (transfer none): the largest monitor from @display, or + * %NULL if no monitor information is available + * Since: 41 + */ +GdkMonitor * +gs_hardware_support_context_dialog_get_largest_monitor (GdkDisplay *display) +{ + GListModel *monitors; /* (unowned) */ + GdkMonitor *monitor; /* (unowned) */ + int monitor_max_dimension; + guint n_monitors; + + g_return_val_if_fail (GDK_IS_DISPLAY (display), NULL); + + monitors = gdk_display_get_monitors (display); + n_monitors = g_list_model_get_n_items (monitors); + monitor_max_dimension = 0; + monitor = NULL; + + for (guint i = 0; i < n_monitors; i++) { + g_autoptr(GdkMonitor) monitor2 = g_list_model_get_item (monitors, i); + GdkRectangle monitor_geometry; + int monitor2_max_dimension; + + if (monitor2 == NULL) + continue; + + gdk_monitor_get_geometry (monitor2, &monitor_geometry); + monitor2_max_dimension = MAX (monitor_geometry.width, monitor_geometry.height); + + if (monitor2_max_dimension > monitor_max_dimension) { + monitor = monitor2; + monitor_max_dimension = monitor2_max_dimension; + continue; + } + } + + return monitor; +} + +/* Unfortunately the integer values of #AsRelationKind don’t have the same order + * as we want. */ +static AsRelationKind +max_relation_kind (AsRelationKind kind1, + AsRelationKind kind2) +{ + /* cases are ordered from maximum to minimum */ + if (kind1 == AS_RELATION_KIND_REQUIRES || kind2 == AS_RELATION_KIND_REQUIRES) + return AS_RELATION_KIND_REQUIRES; + if (kind1 == AS_RELATION_KIND_RECOMMENDS || kind2 == AS_RELATION_KIND_RECOMMENDS) + return AS_RELATION_KIND_RECOMMENDS; +#if AS_CHECK_VERSION(0, 15, 0) + if (kind1 == AS_RELATION_KIND_SUPPORTS || kind2 == AS_RELATION_KIND_SUPPORTS) + return AS_RELATION_KIND_SUPPORTS; +#endif + return AS_RELATION_KIND_UNKNOWN; +} + +typedef struct { + guint min; + guint max; +} Range; + +/* + * evaluate_display_comparison: + * @comparand1: + * @comparator: + * @comparand2: + * + * Evaluate `comparand1 comparator comparand2` and return the result. For + * example, `comparand1 EQ comparand2` or `comparand1 GT comparand2`. + * + * Comparisons are done as ranges, so depending on @comparator, sometimes the + * #Range.min value of a comparand is compared, sometimes #Range.max, and + * sometimes both. See the code for details. + * + * Returns: %TRUE if the comparison is true, %FALSE otherwise + * Since: 41 + */ +static gboolean +evaluate_display_comparison (Range comparand1, + AsRelationCompare comparator, + Range comparand2) +{ + switch (comparator) { + case AS_RELATION_COMPARE_EQ: + return (comparand1.min == comparand2.min && + comparand1.max == comparand2.max); + case AS_RELATION_COMPARE_NE: + return (comparand1.min != comparand2.min || + comparand1.max != comparand2.max); + case AS_RELATION_COMPARE_LT: + return (comparand1.max < comparand2.min); + case AS_RELATION_COMPARE_GT: + return (comparand1.min > comparand2.max); + case AS_RELATION_COMPARE_LE: + return (comparand1.max <= comparand2.max); + case AS_RELATION_COMPARE_GE: + return (comparand1.min >= comparand2.min); + case AS_RELATION_COMPARE_UNKNOWN: + case AS_RELATION_COMPARE_LAST: + default: + g_assert_not_reached (); + } +} + +/** + * gs_hardware_support_context_dialog_get_control_support: + * @display: a #GdkDisplay + * @relations: (element-type AsRelation): relations retrieved from a #GsApp + * using gs_app_get_relations() + * @any_control_relations_set_out: (out caller-allocates) (optional): return + * location for a boolean indicating whether any control relations are set + * in @relations + * @control_relations: (out caller-allocates) (array length=AS_CONTROL_KIND_LAST): + * array mapping #AsControlKind to #AsRelationKind; must be at least + * %AS_CONTROL_KIND_LAST elements long, doesn’t need to be initialised + * @has_touchscreen_out: (out caller-allocates) (optional): return location for + * a boolean indicating whether @display has a touchscreen + * @has_keyboard_out: (out caller-allocates) (optional): return location for + * a boolean indicating whether @display has a keyboard + * @has_mouse_out: (out caller-allocates) (optional): return location for + * a boolean indicating whether @display has a mouse + * + * Query @display and @relations and summarise the information in the output + * arguments. + * + * Each element of @control_relations will be set to the highest type of + * relation seen for that type of control. So if the appdata represented by + * @relations contains `<requires><control>keyboard</control></requires>`, + * `control_relations[AS_CONTROL_KIND_KEYBOARD]` will be set to + * %AS_RELATION_KIND_REQUIRES. All elements of @control_relations are set to + * %AS_RELATION_KIND_UNKNOWN by default. + * + * @any_control_relations_set_out is set to %TRUE if any elements of + * @control_relations are changed from %AS_RELATION_KIND_UNKNOWN. + * + * @has_touchscreen_out, @has_keyboard_out and @has_mouse_out are set to %TRUE + * if the default seat attached to @display has the relevant input device + * (%GDK_SEAT_CAPABILITY_TOUCH, %GDK_SEAT_CAPABILITY_KEYBOARD, + * %GDK_SEAT_CAPABILITY_POINTER respectively). + * + * Since: 41 + */ +void +gs_hardware_support_context_dialog_get_control_support (GdkDisplay *display, + GPtrArray *relations, + gboolean *any_control_relations_set_out, + AsRelationKind *control_relations, + gboolean *has_touchscreen_out, + gboolean *has_keyboard_out, + gboolean *has_mouse_out) +{ + gboolean any_control_relations_set; + gboolean has_touchscreen, has_keyboard, has_mouse; + + g_return_if_fail (display == NULL || GDK_IS_DISPLAY (display)); + g_return_if_fail (control_relations != NULL); + + any_control_relations_set = FALSE; + has_touchscreen = FALSE; + has_keyboard = FALSE; + has_mouse = FALSE; + + /* Initialise @control_relations */ + for (gint i = 0; i < AS_CONTROL_KIND_LAST; i++) + control_relations[i] = AS_RELATION_KIND_UNKNOWN; + + /* Set @control_relations to the maximum relation kind found for each control */ + for (guint i = 0; relations != NULL && i < relations->len; i++) { + AsRelation *relation = AS_RELATION (g_ptr_array_index (relations, i)); + AsRelationKind kind = as_relation_get_kind (relation); + + if (as_relation_get_item_kind (relation) == AS_RELATION_ITEM_KIND_CONTROL) { + AsControlKind control_kind = as_relation_get_value_control_kind (relation); + control_relations[control_kind] = MAX (control_relations[control_kind], kind); + + if (kind == AS_RELATION_KIND_REQUIRES || +#if AS_CHECK_VERSION(0, 15, 0) + kind == AS_RELATION_KIND_SUPPORTS || +#endif + kind == AS_RELATION_KIND_RECOMMENDS) + any_control_relations_set = TRUE; + } + } + + /* Work out what input devices are available. */ + if (display != NULL) { + GdkSeat *seat = gdk_display_get_default_seat (display); + GdkSeatCapabilities seat_capabilities = gdk_seat_get_capabilities (seat); + + has_touchscreen = (seat_capabilities & GDK_SEAT_CAPABILITY_TOUCH); + has_keyboard = (seat_capabilities & GDK_SEAT_CAPABILITY_KEYBOARD); + has_mouse = (seat_capabilities & GDK_SEAT_CAPABILITY_POINTER); + } + + if (any_control_relations_set_out != NULL) + *any_control_relations_set_out = any_control_relations_set; + if (has_touchscreen_out != NULL) + *has_touchscreen_out = has_touchscreen; + if (has_keyboard_out != NULL) + *has_keyboard_out = has_keyboard; + if (has_mouse_out != NULL) + *has_mouse_out = has_mouse; +} + +/** + * gs_hardware_support_context_dialog_get_display_support: + * @monitor: the largest #GdkMonitor currently connected + * @relations: (element-type AsRelation): (element-type AsRelation): relations retrieved from a #GsApp + * using gs_app_get_relations() + * @any_display_relations_set_out: (out caller-allocates) (optional): return + * location for a boolean indicating whether any display relations are set + * in @relations + * @desktop_match_out: (out caller-allocates) (not optional): return location + * for a boolean indicating whether @relations claims support for desktop + * displays + * @desktop_relation_kind_out: (out caller-allocates) (not optional): return + * location for an #AsRelationKind indicating what kind of support the app + * has for desktop displays + * @mobile_match_out: (out caller-allocates) (not optional): return location + * for a boolean indicating whether @relations claims support for mobile + * displays (phones) + * @mobile_relation_kind_out: (out caller-allocates) (not optional): return + * location for an #AsRelationKind indicating what kind of support the app + * has for mobile displays + * @current_match_out: (out caller-allocates) (not optional): return location + * for a boolean indicating whether @relations claims support for the + * currently connected @monitor + * @current_relation_kind_out: (out caller-allocates) (not optional): return + * location for an #AsRelationKind indicating what kind of support the app + * has for the currently connected monitor + * + * Query @monitor and @relations and summarise the information in the output + * arguments. + * + * @any_display_relations_set_out is set to %TRUE if any elements of @relations + * have type %AS_RELATION_ITEM_KIND_DISPLAY_LENGTH, i.e. if the app has provided + * any information about what displays it supports/requires. + * + * @desktop_match_out is set to %TRUE if the display relations in @relations + * indicate that the app supports desktop displays (currently, larger than + * 1024 pixels). + * + * @desktop_relation_kind_out is set to the type of support the app has for + * desktop displays: whether they’re required (%AS_RELATION_KIND_REQUIRES), + * supported but not required (%AS_RELATION_KIND_RECOMMENDS or + * %AS_RELATION_KIND_SUPPORTS) or whether there’s no information + * (%AS_RELATION_KIND_UNKNOWN). + * + * @mobile_match_out and @mobile_relation_kind_out behave similarly, but for + * mobile displays (smaller than 768 pixels). + * + * @current_match_out and @current_relation_kind_out behave similarly, but for + * the dimensions of @monitor. + * + * Since: 41 + */ +void +gs_hardware_support_context_dialog_get_display_support (GdkMonitor *monitor, + GPtrArray *relations, + gboolean *any_display_relations_set_out, + gboolean *desktop_match_out, + AsRelationKind *desktop_relation_kind_out, + gboolean *mobile_match_out, + AsRelationKind *mobile_relation_kind_out, + gboolean *current_match_out, + AsRelationKind *current_relation_kind_out) +{ + GdkRectangle current_screen_size; + gboolean any_display_relations_set; + + g_return_if_fail (GDK_IS_MONITOR (monitor)); + g_return_if_fail (desktop_match_out != NULL); + g_return_if_fail (desktop_relation_kind_out != NULL); + g_return_if_fail (mobile_match_out != NULL); + g_return_if_fail (mobile_relation_kind_out != NULL); + g_return_if_fail (current_match_out != NULL); + g_return_if_fail (current_relation_kind_out != NULL); + + gdk_monitor_get_geometry (monitor, ¤t_screen_size); + + /* Set default output */ + any_display_relations_set = FALSE; + *desktop_match_out = FALSE; + *desktop_relation_kind_out = AS_RELATION_KIND_UNKNOWN; + *mobile_match_out = FALSE; + *mobile_relation_kind_out = AS_RELATION_KIND_UNKNOWN; + *current_match_out = FALSE; + *current_relation_kind_out = AS_RELATION_KIND_UNKNOWN; + + for (guint i = 0; relations != NULL && i < relations->len; i++) { + AsRelation *relation = AS_RELATION (g_ptr_array_index (relations, i)); + + /* All lengths here are in logical/application pixels, + * not device pixels. */ + if (as_relation_get_item_kind (relation) == AS_RELATION_ITEM_KIND_DISPLAY_LENGTH) { + AsRelationCompare comparator = as_relation_get_compare (relation); + Range current_display_comparand, relation_comparand; + + /* From https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-requires-recommends-display_length */ + Range display_lengths[] = { + [AS_DISPLAY_LENGTH_KIND_XSMALL] = { 0, 360 }, + [AS_DISPLAY_LENGTH_KIND_SMALL] = { 360, 768 }, + [AS_DISPLAY_LENGTH_KIND_MEDIUM] = { 768, 1024 }, + [AS_DISPLAY_LENGTH_KIND_LARGE] = { 1024, 3840 }, + [AS_DISPLAY_LENGTH_KIND_XLARGE] = { 3840, G_MAXUINT }, + }; + + any_display_relations_set = TRUE; + + switch (as_relation_get_display_side_kind (relation)) { + case AS_DISPLAY_SIDE_KIND_SHORTEST: + current_display_comparand.min = current_display_comparand.max = MIN (current_screen_size.width, current_screen_size.height); + relation_comparand.min = relation_comparand.max = as_relation_get_value_px (relation); + break; + case AS_DISPLAY_SIDE_KIND_LONGEST: + current_display_comparand.min = current_display_comparand.max = MAX (current_screen_size.width, current_screen_size.height); + relation_comparand.min = relation_comparand.max = as_relation_get_value_px (relation); + break; + case AS_DISPLAY_SIDE_KIND_UNKNOWN: + case AS_DISPLAY_SIDE_KIND_LAST: + default: + current_display_comparand.min = current_display_comparand.max = MAX (current_screen_size.width, current_screen_size.height); + relation_comparand.min = display_lengths[as_relation_get_value_display_length_kind (relation)].min; + relation_comparand.max = display_lengths[as_relation_get_value_display_length_kind (relation)].max; + break; + } + + if (evaluate_display_comparison (display_lengths[AS_DISPLAY_LENGTH_KIND_SMALL], comparator, relation_comparand)) { + *mobile_relation_kind_out = max_relation_kind (*mobile_relation_kind_out, as_relation_get_kind (relation)); + *mobile_match_out = TRUE; + } + + if (evaluate_display_comparison (display_lengths[AS_DISPLAY_LENGTH_KIND_LARGE], comparator, relation_comparand)) { + *desktop_relation_kind_out = max_relation_kind (*desktop_relation_kind_out, as_relation_get_kind (relation)); + *desktop_match_out = TRUE; + } + + if (evaluate_display_comparison (current_display_comparand, comparator, relation_comparand)) { + *current_relation_kind_out = max_relation_kind (*current_relation_kind_out, as_relation_get_kind (relation)); + *current_match_out = TRUE; + } + } + } + + /* Output */ + if (any_display_relations_set_out != NULL) + *any_display_relations_set_out = any_display_relations_set; +} + +static void +update_relations_list (GsHardwareSupportContextDialog *self) +{ + const gchar *icon_name, *css_class; + g_autofree gchar *title = NULL; + g_autoptr(GPtrArray) relations = NULL; + AsRelationKind control_relations[AS_CONTROL_KIND_LAST] = { AS_RELATION_KIND_UNKNOWN, }; + GdkDisplay *display; + GdkMonitor *monitor = NULL; + GdkRectangle current_screen_size; + gboolean any_control_relations_set; + gboolean has_touchscreen = FALSE, has_keyboard = FALSE, has_mouse = FALSE; + GtkStyleContext *context; + GsContextDialogRowImportance chosen_rating; + + /* Treat everything as unknown to begin with, and downgrade its hardware + * support based on app properties. */ + chosen_rating = GS_CONTEXT_DIALOG_ROW_IMPORTANCE_NEUTRAL; + + gs_widget_remove_all (GTK_WIDGET (self->relations_list), (GsRemoveFunc) gtk_list_box_remove); + + /* UI state is undefined if app is not set. */ + if (self->app == NULL) + 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); + + if (display != NULL) + monitor = gs_hardware_support_context_dialog_get_largest_monitor (display); + + if (monitor != NULL) + gdk_monitor_get_geometry (monitor, ¤t_screen_size); + + /* For each of the screen sizes we understand, add a row to the dialogue. + * In the unlikely case that (monitor == NULL), don’t bother providing + * fallback rows. */ + if (monitor != NULL) { + AsRelationKind desktop_relation_kind, mobile_relation_kind, current_relation_kind; + gboolean desktop_match, mobile_match, current_match; + gboolean any_display_relations_set; + + gs_hardware_support_context_dialog_get_display_support (monitor, relations, + &any_display_relations_set, + &desktop_match, &desktop_relation_kind, + &mobile_match, &mobile_relation_kind, + ¤t_match, ¤t_relation_kind); + + add_relation_row (self->relations_list, &chosen_rating, + desktop_relation_kind, + desktop_match ? MATCH_STATE_MATCH : MATCH_STATE_NO_MATCH, + any_display_relations_set, + "desktop-symbolic", + _("Desktop Support"), + _("Supports being used on a large screen"), + "dialog-question-symbolic", + _("Desktop Support Unknown"), + _("Not enough information to know if large screens are supported"), + "desktop-symbolic", + _("Desktop Only"), + _("Requires a large screen"), + "desktop-symbolic", + _("Desktop Support"), + _("Supports being used on a large screen"), + "desktop-symbolic", + _("Desktop Not Supported"), + _("Cannot be used on a large screen")); + + add_relation_row (self->relations_list, &chosen_rating, + mobile_relation_kind, + mobile_match ? MATCH_STATE_MATCH : MATCH_STATE_NO_MATCH, + any_display_relations_set, + "phone-symbolic", + _("Mobile Support"), + _("Supports being used on a small screen"), + "dialog-question-symbolic", + _("Mobile Support Unknown"), + _("Not enough information to know if small screens are supported"), + "phone-symbolic", + _("Mobile Only"), + _("Requires a small screen"), + "phone-symbolic", + _("Mobile Support"), + _("Supports being used on a small screen"), + "phone-symbolic", + _("Mobile Not Supported"), + _("Cannot be used on a small screen")); + + /* Other display relations should only be listed if they are a + * requirement. They will typically be for special apps. */ + add_relation_row (self->relations_list, &chosen_rating, + current_relation_kind, + current_match ? MATCH_STATE_MATCH : MATCH_STATE_NO_MATCH, + any_display_relations_set, + NULL, NULL, NULL, + NULL, NULL, NULL, + "video-joined-displays-symbolic", + _("Screen Size Mismatch"), + _("Doesn’t support your current screen size"), + NULL, NULL, NULL, + NULL, NULL, NULL); + } + + /* For each of the control devices we understand, add a row to the dialogue. */ + add_relation_row (self->relations_list, &chosen_rating, + control_relations[AS_CONTROL_KIND_KEYBOARD], + has_keyboard ? MATCH_STATE_MATCH : MATCH_STATE_NO_MATCH, + any_control_relations_set, + "input-keyboard-symbolic", + _("Keyboard Support"), + _("Requires a keyboard"), + "dialog-question-symbolic", + _("Keyboard Support Unknown"), + _("Not enough information to know if keyboards are supported"), + "input-keyboard-symbolic", + _("Keyboard Required"), + _("Requires a keyboard"), + "input-keyboard-symbolic", + _("Keyboard Support"), + _("Supports keyboards"), + "input-keyboard-symbolic", + _("Keyboard Not Supported"), + _("Cannot be used with a keyboard")); + + add_relation_row (self->relations_list, &chosen_rating, + control_relations[AS_CONTROL_KIND_POINTING], + has_mouse ? MATCH_STATE_MATCH : MATCH_STATE_NO_MATCH, + any_control_relations_set, + "input-mouse-symbolic", + _("Mouse Support"), + _("Requires a mouse or pointing device"), + "dialog-question-symbolic", + _("Mouse Support Unknown"), + _("Not enough information to know if mice or pointing devices are supported"), + "input-mouse-symbolic", + _("Mouse Required"), + _("Requires a mouse or pointing device"), + "input-mouse-symbolic", + _("Mouse Support"), + _("Supports mice and pointing devices"), + "input-mouse-symbolic", + _("Mouse Not Supported"), + _("Cannot be used with a mouse or pointing device")); + + add_relation_row (self->relations_list, &chosen_rating, + control_relations[AS_CONTROL_KIND_TOUCH], + has_touchscreen ? MATCH_STATE_MATCH : MATCH_STATE_NO_MATCH, + any_control_relations_set, + "phone-symbolic", + _("Touchscreen Support"), + _("Requires a touchscreen"), + "dialog-question-symbolic", + _("Touchscreen Support Unknown"), + _("Not enough information to know if touchscreens are supported"), + "phone-symbolic", + _("Touchscreen Required"), + _("Requires a touchscreen"), + "phone-symbolic", + _("Touchscreen Support"), + _("Supports touchscreens"), + "phone-symbolic", + _("Touchscreen Not Supported"), + _("Cannot be used with a touchscreen")); + + /* Gamepads are a little different; only show the row if the appdata + * explicitly mentions gamepads, and don’t vary the row based on whether + * a gamepad is plugged in, since users often leave their gamepads + * unplugged until they’re actually needed. */ + add_relation_row (self->relations_list, &chosen_rating, + control_relations[AS_CONTROL_KIND_GAMEPAD], + MATCH_STATE_UNKNOWN, + any_control_relations_set, + NULL, NULL, NULL, + NULL, NULL, NULL, + "input-gaming-symbolic", + _("Gamepad Required"), + _("Requires a gamepad"), + "input-gaming-symbolic", + _("Gamepad Support"), + _("Supports gamepads"), + NULL, NULL, NULL); + + /* Update the UI. */ + switch (chosen_rating) { + case GS_CONTEXT_DIALOG_ROW_IMPORTANCE_NEUTRAL: + icon_name = "desktop-symbolic"; + /* Translators: It’s unknown whether this app is supported on + * the current hardware. The placeholder is the app name. */ + title = g_strdup_printf (_("%s probably works on this device"), gs_app_get_name (self->app)); + css_class = "grey"; + break; + case GS_CONTEXT_DIALOG_ROW_IMPORTANCE_UNIMPORTANT: + icon_name = "app-installed-symbolic"; + /* Translators: The app will work on the current hardware. + * The placeholder is the app name. */ + title = g_strdup_printf (_("%s works on this device"), gs_app_get_name (self->app)); + css_class = "green"; + break; + case GS_CONTEXT_DIALOG_ROW_IMPORTANCE_WARNING: + icon_name = "dialog-question-symbolic"; + /* Translators: The app may not work fully on the current hardware. + * The placeholder is the app name. */ + title = g_strdup_printf (_("%s will not work properly on this device"), gs_app_get_name (self->app)); + css_class = "yellow"; + break; + case GS_CONTEXT_DIALOG_ROW_IMPORTANCE_IMPORTANT: + icon_name = "dialog-warning-symbolic"; + /* Translators: The app will not work properly on the current hardware. + * The placeholder is the app name. */ + title = g_strdup_printf (_("%s will not work on this device"), gs_app_get_name (self->app)); + css_class = "red"; + break; + default: + g_assert_not_reached (); + } + + gs_lozenge_set_icon_name (GS_LOZENGE (self->lozenge), icon_name); + gtk_label_set_text (self->title, title); + + context = gtk_widget_get_style_context (self->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_remove_class (context, "grey"); + + gtk_style_context_add_class (context, css_class); +} + +static void +app_notify_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + GsHardwareSupportContextDialog *self = GS_HARDWARE_SUPPORT_CONTEXT_DIALOG (user_data); + + update_relations_list (self); +} + +static void +gs_hardware_support_context_dialog_init (GsHardwareSupportContextDialog *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +static void +gs_hardware_support_context_dialog_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GsHardwareSupportContextDialog *self = GS_HARDWARE_SUPPORT_CONTEXT_DIALOG (object); + + switch ((GsHardwareSupportContextDialogProperty) prop_id) { + case PROP_APP: + g_value_set_object (value, gs_hardware_support_context_dialog_get_app (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_hardware_support_context_dialog_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GsHardwareSupportContextDialog *self = GS_HARDWARE_SUPPORT_CONTEXT_DIALOG (object); + + switch ((GsHardwareSupportContextDialogProperty) prop_id) { + case PROP_APP: + gs_hardware_support_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_hardware_support_context_dialog_dispose (GObject *object) +{ + GsHardwareSupportContextDialog *self = GS_HARDWARE_SUPPORT_CONTEXT_DIALOG (object); + + gs_hardware_support_context_dialog_set_app (self, NULL); + + G_OBJECT_CLASS (gs_hardware_support_context_dialog_parent_class)->dispose (object); +} + +static void +gs_hardware_support_context_dialog_class_init (GsHardwareSupportContextDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gs_hardware_support_context_dialog_get_property; + object_class->set_property = gs_hardware_support_context_dialog_set_property; + object_class->dispose = gs_hardware_support_context_dialog_dispose; + + /** + * GsHardwareSupportContextDialog:app: (nullable) + * + * The app to display the hardware support 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-hardware-support-context-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, GsHardwareSupportContextDialog, lozenge); + gtk_widget_class_bind_template_child (widget_class, GsHardwareSupportContextDialog, title); + gtk_widget_class_bind_template_child (widget_class, GsHardwareSupportContextDialog, relations_list); +} + +/** + * gs_hardware_support_context_dialog_new: + * @app: (nullable): the app to display hardware support context information for, or %NULL + * + * Create a new #GsHardwareSupportContextDialog and set its initial app to @app. + * + * Returns: (transfer full): a new #GsHardwareSupportContextDialog + * Since: 41 + */ +GsHardwareSupportContextDialog * +gs_hardware_support_context_dialog_new (GsApp *app) +{ + g_return_val_if_fail (app == NULL || GS_IS_APP (app), NULL); + + return g_object_new (GS_TYPE_HARDWARE_SUPPORT_CONTEXT_DIALOG, + "app", app, + NULL); +} + +/** + * gs_hardware_support_context_dialog_get_app: + * @self: a #GsHardwareSupportContextDialog + * + * Gets the value of #GsHardwareSupportContextDialog:app. + * + * Returns: (nullable) (transfer none): app whose hardware support context information is + * being displayed, or %NULL if none is set + * Since: 41 + */ +GsApp * +gs_hardware_support_context_dialog_get_app (GsHardwareSupportContextDialog *self) +{ + g_return_val_if_fail (GS_IS_HARDWARE_SUPPORT_CONTEXT_DIALOG (self), NULL); + + return self->app; +} + +/** + * gs_hardware_support_context_dialog_set_app: + * @self: a #GsHardwareSupportContextDialog + * @app: (nullable) (transfer none): the app to display hardware support context + * information for, or %NULL for none + * + * Set the value of #GsHardwareSupportContextDialog:app. + * + * Since: 41 + */ +void +gs_hardware_support_context_dialog_set_app (GsHardwareSupportContextDialog *self, + GsApp *app) +{ + g_return_if_fail (GS_IS_HARDWARE_SUPPORT_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_relations, 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_relations = g_signal_connect (self->app, "notify::relations", 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_relations_list (self); + + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_APP]); +} |