[gnome-software: 8/15] gs-hardware-support-context-dialog: Add hardware support context dialogue




commit 304cdc1e44d468be23c49d881948471b278dfb15
Author: Philip Withnall <pwithnall endlessos org>
Date:   Thu Jul 15 16:33:28 2021 +0100

    gs-hardware-support-context-dialog: Add hardware support context dialogue
    
    This presents information about what hardware the app supports/requires,
    to the user.
    
    A future commit will make it appear when the hardware tile in
    `GsAppContextBar` is clicked.
    
    Some of the code for working out the hardware support is copied from the
    `GsAppContextBar`. It will be refactored to remove the duplication in a
    future commit.
    
    Includes significant work by Adrien Plazas.
    
    Signed-off-by: Philip Withnall <pwithnall endlessos org>
    
    Helps: #1111

 po/POTFILES.in                            |   2 +
 src/gnome-software.gresource.xml          |   1 +
 src/gs-hardware-support-context-dialog.c  | 933 ++++++++++++++++++++++++++++++
 src/gs-hardware-support-context-dialog.h  |  50 ++
 src/gs-hardware-support-context-dialog.ui | 123 ++++
 src/meson.build                           |   1 +
 6 files changed, 1110 insertions(+)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index ede3e3290..5e60f482a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -35,6 +35,8 @@ src/gs-feature-tile.c
 src/gs-featured-carousel.c
 src/gs-featured-carousel.ui
 src/gs-first-run-dialog.ui
+src/gs-hardware-support-context-dialog.c
+src/gs-hardware-support-context-dialog.ui
 src/gs-history-dialog.c
 src/gs-history-dialog.ui
 src/gs-installed-page.c
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index 52157b29b..843d00db7 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -15,6 +15,7 @@
   <file preprocess="xml-stripblanks">gs-feature-tile.ui</file>
   <file preprocess="xml-stripblanks">gs-featured-carousel.ui</file>
   <file preprocess="xml-stripblanks">gs-first-run-dialog.ui</file>
+  <file preprocess="xml-stripblanks">gs-hardware-support-context-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-history-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-info-bar.ui</file>
   <file preprocess="xml-stripblanks">gs-installed-page.ui</file>
diff --git a/src/gs-hardware-support-context-dialog.c b/src/gs-hardware-support-context-dialog.c
new file mode 100644
index 000000000..b2b050f83
--- /dev/null
+++ b/src/gs-hardware-support-context-dialog.c
@@ -0,0 +1,933 @@
+/* -*- 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>` and
+ * `<recommends>` elements in the app’s appdata.
+ *
+ * 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 <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <handy.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"
+
+struct _GsHardwareSupportContextDialog
+{
+       HdyWindow                parent_instance;
+
+       GsApp                   *app;  /* (nullable) (owned) */
+       gulong                   app_notify_handler_relations;
+       gulong                   app_notify_handler_name;
+
+       GtkImage                *icon;
+       GtkWidget               *lozenge;
+       GtkLabel                *title;
+       GtkListBox              *relations_list;
+};
+
+G_DEFINE_TYPE (GsHardwareSupportContextDialog, gs_hardware_support_context_dialog, HDY_TYPE_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:
+               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_insert (list_box, GTK_WIDGET (row), -1);
+}
+
+/**
+ * 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)
+{
+       GdkMonitor *monitor;  /* (unowned) */
+       int n_monitors, monitor_max_dimension;
+
+       g_return_val_if_fail (GDK_IS_DISPLAY (display), NULL);
+
+       n_monitors = gdk_display_get_n_monitors (display);
+       monitor_max_dimension = 0;
+       monitor = NULL;
+
+       for (int i = 0; i < n_monitors; i++) {
+               GdkMonitor *monitor2 = gdk_display_get_monitor (display, 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 ||
+                   (gdk_monitor_is_primary (monitor2) &&
+                    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;
+       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;
+
+       /* 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 ||
+                           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 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, &current_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_container_remove_all (GTK_CONTAINER (self->relations_list));
+
+       /* 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, &current_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,
+                                                                       &current_match, 
&current_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 = "test-pass-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 ();
+       }
+
+       gtk_image_set_from_icon_name (GTK_IMAGE (self->icon), icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR);
+       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 gboolean
+key_press_event_cb (GtkWidget            *sender,
+                    GdkEvent             *event,
+                    HdyPreferencesWindow *self)
+{
+       guint keyval;
+       GdkModifierType state;
+       GdkKeymap *keymap;
+       GdkEventKey *key_event = (GdkEventKey *) event;
+
+       gdk_event_get_state (event, &state);
+
+       keymap = gdk_keymap_get_for_display (gtk_widget_get_display (sender));
+
+       gdk_keymap_translate_keyboard_state (keymap,
+                                            key_event->hardware_keycode,
+                                            state,
+                                            key_event->group,
+                                            &keyval, NULL, NULL, NULL);
+
+       if (keyval == GDK_KEY_Escape) {
+               gtk_window_close (GTK_WINDOW (self));
+
+               return GDK_EVENT_STOP;
+       }
+
+       return GDK_EVENT_PROPAGATE;
+}
+
+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, icon);
+       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);
+
+       gtk_widget_class_bind_template_callback (widget_class, key_press_event_cb);
+}
+
+/**
+ * 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]);
+}
diff --git a/src/gs-hardware-support-context-dialog.h b/src/gs-hardware-support-context-dialog.h
new file mode 100644
index 000000000..1c9f6b99c
--- /dev/null
+++ b/src/gs-hardware-support-context-dialog.h
@@ -0,0 +1,50 @@
+/* -*- 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+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#include "gs-app.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_HARDWARE_SUPPORT_CONTEXT_DIALOG (gs_hardware_support_context_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsHardwareSupportContextDialog, gs_hardware_support_context_dialog, GS, 
HARDWARE_SUPPORT_CONTEXT_DIALOG, HdyWindow)
+
+GsHardwareSupportContextDialog *gs_hardware_support_context_dialog_new         (GsApp                        
  *app);
+
+GsApp                          *gs_hardware_support_context_dialog_get_app     
(GsHardwareSupportContextDialog *self);
+void                            gs_hardware_support_context_dialog_set_app     
(GsHardwareSupportContextDialog *self,
+                                                                                GsApp                        
  *app);
+
+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);
+
+GdkMonitor *gs_hardware_support_context_dialog_get_largest_monitor (GdkDisplay *display);
+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       *other_match_out,
+                                                             AsRelationKind *other_relation_kind_out);
+
+G_END_DECLS
diff --git a/src/gs-hardware-support-context-dialog.ui b/src/gs-hardware-support-context-dialog.ui
new file mode 100644
index 000000000..bf8b71e8c
--- /dev/null
+++ b/src/gs-hardware-support-context-dialog.ui
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="GsHardwareSupportContextDialog" parent="HdyWindow">
+    <property name="modal">True</property>
+    <property name="window_position">center</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="icon_name">dialog-information</property>
+    <property name="title" translatable="yes" comments="Translators: This is the title of the dialog which 
contains information about the hardware support/requirements of an app">Hardware Support</property>
+    <property name="type_hint">dialog</property>
+    <property name="default-width">640</property>
+    <property name="default-height">576</property>
+    <signal name="key-press-event" handler="key_press_event_cb" after="yes" swapped="no"/>
+    <style>
+      <class name="toolbox"/>
+    </style>
+
+    <child>
+      <object class="GtkOverlay">
+        <property name="visible">True</property>
+        <child type="overlay">
+          <object class="HdyHeaderBar">
+            <property name="show_close_button">True</property>
+            <property name="visible">True</property>
+            <property name="valign">start</property>
+          </object>
+        </child>
+        <child>
+          <object class="HdyPreferencesPage">
+            <property name="visible">True</property>
+            <child>
+              <object class="HdyPreferencesGroup">
+                <property name="visible">True</property>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">8</property>
+                    <property name="visible">True</property>
+
+                    <child>
+                      <object class="GtkBox">
+                        <property name="margin">20</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">12</property>
+                        <property name="visible">True</property>
+
+                        <child>
+                          <object class="GtkBox" id="lozenge">
+                            <property name="halign">center</property>
+                            <property name="visible">True</property>
+                            <style>
+                              <class name="context-tile-lozenge"/>
+                              <class name="large"/>
+                              <class name="grey"/>
+                            </style>
+                            <child>
+                              <object class="GtkImage" id="icon">
+                                <property name="halign">center</property>
+                                <!-- this is a placeholder: the icon is actually set in code -->
+                                <property name="icon-name">safety-symbolic</property>
+                                <property name="visible">True</property>
+                                <accessibility>
+                                  <relation target="title" type="labelled-by"/>
+                                </accessibility>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                              </packing>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="fill">False</property>
+                          </packing>
+                        </child>
+
+                        <child>
+                          <object class="GtkLabel" id="title">
+                            <!-- this is a placeholder: the text is actually set in code -->
+                            <property name="justify">center</property>
+                            <property name="label">Shortwave works on this device</property>
+                            <property name="visible">True</property>
+                            <property name="wrap">True</property>
+                            <property name="xalign">0.5</property>
+                            <style>
+                              <class name="heading"/>
+                              <class name="title-1"/>
+                            </style>
+                            <accessibility>
+                              <relation target="lozenge" type="label-for"/>
+                            </accessibility>
+                            <style>
+                              <class name="context-tile-title"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+
+                    <child>
+                      <object class="GtkListBox" id="relations_list">
+                        <property name="visible">True</property>
+                        <property name="selection_mode">none</property>
+                        <property name="halign">fill</property>
+                        <property name="valign">start</property>
+                        <style>
+                          <class name="content"/>
+                        </style>
+                        <!-- Rows are added in code -->
+                        <placeholder/>
+                      </object>
+                    </child>
+
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/meson.build b/src/meson.build
index 51af9845e..9fde1ffaf 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -44,6 +44,7 @@ gnome_software_sources = [
   'gs-first-run-dialog.c',
   'gs-fixed-size-bin.c',
   'gs-folders.c',
+  'gs-hardware-support-context-dialog.c',
   'gs-history-dialog.c',
   'gs-info-bar.c',
   'gs-installed-page.c',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]