[libhandy] Add HdyStatusPage widget



commit 8d0fdb9b879e54763a75b3f7e0b6f5b744525ad5
Author: Yetizone <andreii lisita gmail com>
Date:   Mon Nov 2 13:49:20 2020 +0200

    Add HdyStatusPage widget
    
    Fixes https://gitlab.gnome.org/GNOME/libhandy/-/issues/138

 debian/libhandy-1-0.symbols        |   8 +
 doc/handy-docs.xml                 |   1 +
 glade/libhandy.xml                 |  14 ++
 src/handy.gresources.xml           |   1 +
 src/handy.h                        |   1 +
 src/hdy-status-page.c              | 397 +++++++++++++++++++++++++++++++++++++
 src/hdy-status-page.h              |  45 +++++
 src/hdy-status-page.ui             |  62 ++++++
 src/meson.build                    |   2 +
 src/themes/Adwaita-dark.css        |  14 +-
 src/themes/Adwaita.css             |  14 +-
 src/themes/HighContrast.css        |  14 +-
 src/themes/HighContrastInverse.css |  14 +-
 src/themes/_fallback-base.scss     |  23 +++
 src/themes/fallback.css            |   8 +
 tests/meson.build                  |   1 +
 tests/test-status-page.c           | 110 ++++++++++
 17 files changed, 717 insertions(+), 12 deletions(-)
---
diff --git a/debian/libhandy-1-0.symbols b/debian/libhandy-1-0.symbols
index 395860c9..ce6a22ad 100644
--- a/debian/libhandy-1-0.symbols
+++ b/debian/libhandy-1-0.symbols
@@ -102,6 +102,14 @@ libhandy-1.so.0 libhandy-1-0 #MINVER#
  hdy_deck_set_visible_child_name@LIBHANDY_1_0 0.80.0
  hdy_deck_transition_type_get_type@LIBHANDY_1_0 0.80.0
  hdy_ease_out_cubic@LIBHANDY_1_0 0.0.11
+ hdy_status_page_get_description@LIBHANDY_1_0 1.1.0
+ hdy_status_page_get_icon_name@LIBHANDY_1_0 1.1.0
+ hdy_status_page_get_title@LIBHANDY_1_0 1.1.0
+ hdy_status_page_get_type@LIBHANDY_1_0 1.1.0
+ hdy_status_page_new@LIBHANDY_1_0 1.1.0
+ hdy_status_page_set_description@LIBHANDY_1_0 1.1.0
+ hdy_status_page_set_icon_name@LIBHANDY_1_0 1.1.0
+ hdy_status_page_set_title@LIBHANDY_1_0 1.1.0
  hdy_enum_value_object_get_name@LIBHANDY_1_0 0.0.6
  hdy_enum_value_object_get_nick@LIBHANDY_1_0 0.0.6
  hdy_enum_value_object_get_type@LIBHANDY_1_0 0.0.6
diff --git a/doc/handy-docs.xml b/doc/handy-docs.xml
index 3d1531c8..310a9f0d 100644
--- a/doc/handy-docs.xml
+++ b/doc/handy-docs.xml
@@ -59,6 +59,7 @@
     <xi:include href="xml/hdy-preferences-window.xml"/>
     <xi:include href="xml/hdy-search-bar.xml"/>
     <xi:include href="xml/hdy-squeezer.xml"/>
+    <xi:include href="xml/hdy-status-page.xml"/>
     <xi:include href="xml/hdy-swipeable.xml"/>
     <xi:include href="xml/hdy-swipe-group.xml"/>
     <xi:include href="xml/hdy-swipe-tracker.xml"/>
diff --git a/glade/libhandy.xml b/glade/libhandy.xml
index 6069105d..122db125 100644
--- a/glade/libhandy.xml
+++ b/glade/libhandy.xml
@@ -370,6 +370,19 @@
       </properties>
     </glade-widget-class>
     <glade-widget-class name="HdySqueezer" generic-name="squeezer" title="Squeezer" since="0.0.10"/>
+    <glade-widget-class name="HdyStatusPage" generic-name="statuspage" title="Status Page" since="1.1" 
use-placeholders="False">
+      <post-create-function>glade_hdy_bin_post_create</post-create-function>
+      <add-child-verify-function>glade_hdy_bin_add_verify</add-child-verify-function>
+      <add-child-function>glade_hdy_bin_add_child</add-child-function>
+      <remove-child-function>glade_hdy_bin_remove_child</remove-child-function>
+      <replace-child-function>glade_hdy_bin_replace_child</replace-child-function>
+      <get-children-function>glade_hdy_bin_get_children</get-children-function>
+      <properties>
+        <property id="title" translatable="True" />
+        <property id="description" translatable="True" />
+        <property id="icon-name" themed-icon="True" />
+      </properties>
+    </glade-widget-class>
     <glade-widget-class name="HdySwipeGroup" generic-name="swipegroup" title="Swipe Group" toplevel="True">
       <read-widget-function>glade_hdy_swipe_group_read_widget</read-widget-function>
       <write-widget-function>glade_hdy_swipe_group_write_widget</write-widget-function>
@@ -444,6 +457,7 @@
     <glade-widget-class-ref name="HdyPreferencesWindow"/>
     <glade-widget-class-ref name="HdySearchBar"/>
     <glade-widget-class-ref name="HdySqueezer"/>
+    <glade-widget-class-ref name="HdyStatusPage"/>
     <glade-widget-class-ref name="HdySwipeGroup"/>
     <glade-widget-class-ref name="HdyTitleBar"/>
     <glade-widget-class-ref name="HdyViewSwitcher"/>
diff --git a/src/handy.gresources.xml b/src/handy.gresources.xml
index b1d948ab..2d50728b 100644
--- a/src/handy.gresources.xml
+++ b/src/handy.gresources.xml
@@ -23,6 +23,7 @@
     <file preprocess="xml-stripblanks">hdy-preferences-page.ui</file>
     <file preprocess="xml-stripblanks">hdy-preferences-window.ui</file>
     <file preprocess="xml-stripblanks">hdy-search-bar.ui</file>
+    <file preprocess="xml-stripblanks">hdy-status-page.ui</file>
     <file preprocess="xml-stripblanks">hdy-view-switcher-bar.ui</file>
     <file preprocess="xml-stripblanks">hdy-view-switcher-button.ui</file>
     <file preprocess="xml-stripblanks">hdy-view-switcher-title.ui</file>
diff --git a/src/handy.h b/src/handy.h
index c02fa52a..d688e762 100644
--- a/src/handy.h
+++ b/src/handy.h
@@ -47,6 +47,7 @@ G_BEGIN_DECLS
 #include "hdy-preferences-window.h"
 #include "hdy-search-bar.h"
 #include "hdy-squeezer.h"
+#include "hdy-status-page.h"
 #include "hdy-swipe-group.h"
 #include "hdy-swipe-tracker.h"
 #include "hdy-swipeable.h"
diff --git a/src/hdy-status-page.c b/src/hdy-status-page.c
new file mode 100644
index 00000000..a5b0e644
--- /dev/null
+++ b/src/hdy-status-page.c
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2020 Andrei Lișiță <andreii lisita gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-status-page.h"
+
+/**
+ * SECTION:hdy-status-page
+ * @short_description: A page used for empty/error states and similar use-cases.
+ * @Title: HdyStatusPage
+ *
+ * The #HdyStatusPage widget can have an icon, a title, a description and a
+ * custom widget which is displayed below them.
+ *
+ * # CSS nodes
+ *
+ * #HdyStatusPage has a main CSS node with name statuspage.
+ *
+ * Since: 1.1
+ */
+
+enum {
+  PROP_0,
+  PROP_ICON_NAME,
+  PROP_TITLE,
+  PROP_DESCRIPTION,
+  LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+struct _HdyStatusPage
+{
+  GtkBin parent_instance;
+
+  GtkWidget *scrolled_window;
+  GtkBox *toplevel_box;
+  GtkImage *image;
+  gchar *icon_name;
+  GtkLabel *title_label;
+  GtkLabel *description_label;
+
+  GtkWidget *user_widget;
+};
+
+G_DEFINE_TYPE (HdyStatusPage, hdy_status_page, GTK_TYPE_BIN)
+
+static void
+hdy_status_page_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  HdyStatusPage *self = HDY_STATUS_PAGE (object);
+
+  switch (prop_id) {
+  case PROP_ICON_NAME:
+    g_value_set_string (value, hdy_status_page_get_icon_name (self));
+    break;
+
+  case PROP_TITLE:
+    g_value_set_string (value, hdy_status_page_get_title (self));
+    break;
+
+  case PROP_DESCRIPTION:
+    g_value_set_string (value, hdy_status_page_get_description (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_status_page_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  HdyStatusPage *self = HDY_STATUS_PAGE (object);
+
+  switch (prop_id) {
+  case PROP_ICON_NAME:
+    hdy_status_page_set_icon_name (self, g_value_get_string (value));
+    break;
+
+  case PROP_TITLE:
+    hdy_status_page_set_title (self, g_value_get_string (value));
+    break;
+
+  case PROP_DESCRIPTION:
+    hdy_status_page_set_description (self, g_value_get_string (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_status_page_finalize (GObject *object)
+{
+  HdyStatusPage *self = HDY_STATUS_PAGE (object);
+
+  g_clear_pointer (&self->icon_name, g_free);
+
+  G_OBJECT_CLASS (hdy_status_page_parent_class)->finalize (object);
+}
+
+static void
+hdy_status_page_destroy (GtkWidget *widget)
+{
+  HdyStatusPage *self = HDY_STATUS_PAGE (widget);
+
+  if (self->scrolled_window) {
+    gtk_container_remove (GTK_CONTAINER (self), self->scrolled_window);
+    self->toplevel_box = NULL;
+    self->image = NULL;
+    self->title_label = NULL;
+    self->description_label = NULL;
+    self->user_widget = NULL;
+  }
+
+  GTK_WIDGET_CLASS (hdy_status_page_parent_class)->destroy (widget);
+}
+
+static void
+hdy_status_page_add (GtkContainer *container,
+                     GtkWidget    *child)
+{
+  HdyStatusPage *self = HDY_STATUS_PAGE (container);
+
+  if (!self->scrolled_window) {
+    GTK_CONTAINER_CLASS (hdy_status_page_parent_class)->add (container, child);
+  } else if (!self->user_widget) {
+    gtk_container_add (GTK_CONTAINER (self->toplevel_box), child);
+    self->user_widget = child;
+  } else {
+    g_warning ("Attempting to add a second child to a HdyStatusPage, but a HdyStatusPage can only have one 
child");
+  }
+}
+
+static void
+hdy_status_page_remove (GtkContainer *container,
+                        GtkWidget    *child)
+{
+  HdyStatusPage *self = HDY_STATUS_PAGE (container);
+
+  if (child == self->scrolled_window) {
+    GTK_CONTAINER_CLASS (hdy_status_page_parent_class)->remove (container, child);
+  } else if (child == self->user_widget) {
+    gtk_container_remove (GTK_CONTAINER (self->toplevel_box), child);
+    self->user_widget = NULL;
+  } else {
+    g_return_if_reached ();
+  }
+}
+
+static void
+hdy_status_page_forall (GtkContainer *container,
+                        gboolean      include_internals,
+                        GtkCallback   callback,
+                        gpointer      callback_data)
+{
+  HdyStatusPage *self = HDY_STATUS_PAGE (container);
+
+  if (include_internals)
+    GTK_CONTAINER_CLASS (hdy_status_page_parent_class)->forall (container,
+                                                                include_internals,
+                                                                callback,
+                                                                callback_data);
+  else if (self->user_widget)
+    callback (self->user_widget, callback_data);
+}
+
+static void
+hdy_status_page_class_init (HdyStatusPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->get_property = hdy_status_page_get_property;
+  object_class->set_property = hdy_status_page_set_property;
+  object_class->finalize = hdy_status_page_finalize;
+  widget_class->destroy = hdy_status_page_destroy;
+  container_class->add = hdy_status_page_add;
+  container_class->remove = hdy_status_page_remove;
+  container_class->forall = hdy_status_page_forall;
+
+  /**
+   * HdyStatusPage:icon-name:
+   *
+   * The name of the icon to be used.
+   *
+   * Since: 1.1
+   */
+  props[PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         _("Icon name"),
+                         _("The name of the icon to be used"),
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * HdyStatusPage:title:
+   *
+   * The title to be displayed below the icon.
+   *
+   * Since: 1.1
+   */
+  props[PROP_TITLE] =
+    g_param_spec_string ("title",
+                         _("Title"),
+                         _("The title to be displayed below the icon"),
+                         "",
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * HdyStatusPage:description:
+   *
+   * The description to be displayed below the title.
+   *
+   * Since: 1.1
+   */
+  props[PROP_DESCRIPTION] =
+    g_param_spec_string ("description",
+                         _("Description"),
+                         _("The description to be displayed below the title"),
+                         "",
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/sm/puri/handy/ui/hdy-status-page.ui");
+  gtk_widget_class_bind_template_child (widget_class, HdyStatusPage, scrolled_window);
+  gtk_widget_class_bind_template_child (widget_class, HdyStatusPage, toplevel_box);
+  gtk_widget_class_bind_template_child (widget_class, HdyStatusPage, image);
+  gtk_widget_class_bind_template_child (widget_class, HdyStatusPage, title_label);
+  gtk_widget_class_bind_template_child (widget_class, HdyStatusPage, description_label);
+
+  gtk_widget_class_set_css_name (widget_class, "statuspage");
+}
+
+static void
+hdy_status_page_init (HdyStatusPage *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_status_page_new:
+ *
+ * Creates a new #HdyStatusPage.
+ *
+ * Returns: a new #HdyStatusPage
+ *
+ * Since: 1.1
+ */
+GtkWidget *
+hdy_status_page_new (void)
+{
+  return g_object_new (HDY_TYPE_STATUS_PAGE, NULL);
+}
+
+/**
+ * hdy_status_page_get_icon_name:
+ * @self: a #HdyStatusPage
+ *
+ * Gets the icon name for @self.
+ *
+ * Returns: (transfer none) (nullable): the icon name for @self.
+ *
+ * Since: 1.1
+ */
+const gchar *
+hdy_status_page_get_icon_name (HdyStatusPage *self)
+{
+  return self->icon_name;
+}
+
+/**
+ * hdy_status_page_set_icon_name:
+ * @self: a #HdyStatusPage
+ * @icon_name: (nullable): the icon name
+ *
+ * Sets the icon name for @self.
+ *
+ * Since: 1.1
+ */
+void
+hdy_status_page_set_icon_name (HdyStatusPage *self,
+                               const gchar   *icon_name)
+{
+  g_return_if_fail (HDY_IS_STATUS_PAGE (self));
+
+  if (g_strcmp0 (self->icon_name, icon_name) == 0)
+    return;
+
+  g_free (self->icon_name);
+  self->icon_name = g_strdup (icon_name);
+
+  if (!icon_name)
+    g_object_set (G_OBJECT (self->image), "icon-name", "image-missing", NULL);
+  else
+    g_object_set (G_OBJECT (self->image), "icon-name", icon_name, NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_status_page_get_title:
+ * @self: a #HdyStatusPage
+ *
+ * Gets the title for @self.
+ *
+ * Returns: (transfer none) (nullable): the title for @self, or %NULL.
+ *
+ * Since: 1.1
+ */
+const gchar *
+hdy_status_page_get_title (HdyStatusPage *self)
+{
+  g_return_val_if_fail (HDY_IS_STATUS_PAGE (self), NULL);
+
+  return gtk_label_get_label (self->title_label);
+}
+
+/**
+ * hdy_status_page_set_title:
+ * @self: a #HdyStatusPage
+ * @title: (nullable): the title
+ *
+ * Sets the title for @self.
+ *
+ * Since: 1.1
+ */
+void
+hdy_status_page_set_title (HdyStatusPage *self,
+                           const gchar   *title)
+{
+  g_return_if_fail (HDY_IS_STATUS_PAGE (self));
+
+  if (g_strcmp0 (title, hdy_status_page_get_title (self)) == 0)
+    return;
+
+  gtk_label_set_label (self->title_label, title);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_status_page_get_description:
+ * @self: a #HdyStatusPage
+ *
+ * Gets the description for @self.
+ *
+ * Returns: (transfer none) (nullable): the description for @self, or %NULL.
+ *
+ * Since: 1.1
+ */
+const gchar *
+hdy_status_page_get_description (HdyStatusPage *self)
+{
+  g_return_val_if_fail (HDY_IS_STATUS_PAGE (self), NULL);
+
+  return gtk_label_get_label (self->description_label);
+}
+
+/**
+ * hdy_status_page_set_description:
+ * @self: a #HdyStatusPage
+ * @description: (nullable): the description
+ *
+ * Sets the description for @self.
+ *
+ * Since: 1.1
+ */
+void
+hdy_status_page_set_description (HdyStatusPage *self,
+                                 const gchar   *description)
+{
+  g_return_if_fail (HDY_IS_STATUS_PAGE (self));
+
+  if (g_strcmp0 (description, hdy_status_page_get_description (self)) == 0)
+    return;
+
+  gtk_label_set_label (self->description_label, description);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DESCRIPTION]);
+}
diff --git a/src/hdy-status-page.h b/src/hdy-status-page.h
new file mode 100644
index 00000000..62b45d8f
--- /dev/null
+++ b/src/hdy-status-page.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 Andrei Lișiță <andreii lisita gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_STATUS_PAGE (hdy_status_page_get_type())
+
+HDY_AVAILABLE_IN_1_1
+G_DECLARE_FINAL_TYPE (HdyStatusPage, hdy_status_page, HDY, STATUS_PAGE, GtkBin)
+
+HDY_AVAILABLE_IN_1_1
+GtkWidget       *hdy_status_page_new (void);
+
+HDY_AVAILABLE_IN_1_1
+const gchar     *hdy_status_page_get_icon_name (HdyStatusPage *self);
+HDY_AVAILABLE_IN_1_1
+void             hdy_status_page_set_icon_name (HdyStatusPage *self,
+                                                const gchar   *icon_name);
+
+HDY_AVAILABLE_IN_1_1
+const gchar     *hdy_status_page_get_title (HdyStatusPage *self);
+HDY_AVAILABLE_IN_1_1
+void             hdy_status_page_set_title (HdyStatusPage *self,
+                                            const gchar   *title);
+
+HDY_AVAILABLE_IN_1_1
+const gchar     *hdy_status_page_get_description (HdyStatusPage *self);
+HDY_AVAILABLE_IN_1_1
+void             hdy_status_page_set_description (HdyStatusPage *self,
+                                                  const gchar   *description);
+
+G_END_DECLS
diff --git a/src/hdy-status-page.ui b/src/hdy-status-page.ui
new file mode 100644
index 00000000..771ee80e
--- /dev/null
+++ b/src/hdy-status-page.ui
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="HdyStatusPage" parent="GtkBin">
+    <child>
+      <object class="GtkScrolledWindow" id="scrolled_window">
+        <property name="visible">True</property>
+        <property name="hscrollbar-policy">never</property>
+        <child>
+          <object class="GtkBox" id="toplevel_box">
+            <property name="visible">True</property>
+            <property name="orientation">vertical</property>
+            <property name="valign">center</property>
+            <child>
+              <object class="HdyClamp">
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="orientation">vertical</property>
+                    <property name="valign">center</property>
+                    <child>
+                      <object class="GtkImage" id="image">
+                        <property name="visible">True</property>
+                        <property name="pixel-size">128</property>
+                        <property name="icon-name">image-missing</property>
+                        <style>
+                          <class name="icon"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="title_label">
+                        <property name="visible">True</property>
+                        <property name="wrap">True</property>
+                        <style>
+                          <class name="title"/>
+                          <class name="large-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="description_label">
+                        <property name="visible">True</property>
+                        <property name="wrap">True</property>
+                        <property name="justify">center</property>
+                        <property name="use-markup">True</property>
+                        <style>
+                          <class name="body"/>
+                          <class name="description"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/meson.build b/src/meson.build
index af850b0d..dc164ca3 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -91,6 +91,7 @@ src_headers = [
   'hdy-preferences-window.h',
   'hdy-search-bar.h',
   'hdy-squeezer.h',
+  'hdy-status-page.h',
   'hdy-swipe-group.h',
   'hdy-swipe-tracker.h',
   'hdy-swipeable.h',
@@ -148,6 +149,7 @@ src_sources = [
   'hdy-shadow-helper.c',
   'hdy-squeezer.c',
   'hdy-stackable-box.c',
+  'hdy-status-page.c',
   'hdy-swipe-group.c',
   'hdy-swipe-tracker.c',
   'hdy-swipeable.c',
diff --git a/src/themes/Adwaita-dark.css b/src/themes/Adwaita-dark.css
index f4db9595..2577143f 100644
--- a/src/themes/Adwaita-dark.css
+++ b/src/themes/Adwaita-dark.css
@@ -75,6 +75,14 @@ avatar.image { background: none; }
 
 viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
 
+statuspage > scrolledwindow > viewport > box { margin: 36px 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .icon { margin-bottom: 36px; opacity: 0.5; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .title { margin-bottom: 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .description { margin-bottom: 36px; }
+
 /*************************** Check and Radio buttons * */
 popover.combo list { min-width: 200px; }
 
@@ -170,11 +178,11 @@ list.content, list.content list { background-color: transparent; }
 
 list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > 
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#353535, #2d2d2d, 0.5); }
 
-list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: 
mix(#eeeeec, #2d2d2d, 0.95); }
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: 
mix(#eeeeec, #2d2d2d, 0.95); }
 
 list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > 
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander 
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander 
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
 
-list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander 
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander 
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
 
 list.content > row, list.content > row list > row { border-color: alpha(#1b1b1b, 0.7); border-style: solid; 
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
 
@@ -184,7 +192,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
 
 list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked { border-width: 1px; }
 
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
 
 list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { 
margin-top: 6px; }
 
diff --git a/src/themes/Adwaita.css b/src/themes/Adwaita.css
index 2d8fe364..9824ca68 100644
--- a/src/themes/Adwaita.css
+++ b/src/themes/Adwaita.css
@@ -75,6 +75,14 @@ avatar.image { background: none; }
 
 viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
 
+statuspage > scrolledwindow > viewport > box { margin: 36px 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .icon { margin-bottom: 36px; opacity: 0.5; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .title { margin-bottom: 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .description { margin-bottom: 36px; }
+
 /*************************** Check and Radio buttons * */
 popover.combo list { min-width: 200px; }
 
@@ -170,11 +178,11 @@ list.content, list.content list { background-color: transparent; }
 
 list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > 
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#f6f5f4, #ffffff, 0.5); }
 
-list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: 
mix(#2e3436, #ffffff, 0.95); }
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: 
mix(#2e3436, #ffffff, 0.95); }
 
 list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > 
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander 
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander 
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
 
-list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander 
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander 
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
 
 list.content > row, list.content > row list > row { border-color: alpha(#cdc7c2, 0.7); border-style: solid; 
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
 
@@ -184,7 +192,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
 
 list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked { border-width: 1px; }
 
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
 
 list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { 
margin-top: 6px; }
 
diff --git a/src/themes/HighContrast.css b/src/themes/HighContrast.css
index 4d535ab3..7d295e26 100644
--- a/src/themes/HighContrast.css
+++ b/src/themes/HighContrast.css
@@ -75,6 +75,14 @@ avatar.image { background: none; }
 
 viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
 
+statuspage > scrolledwindow > viewport > box { margin: 36px 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .icon { margin-bottom: 36px; opacity: 0.5; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .title { margin-bottom: 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .description { margin-bottom: 36px; }
+
 /*************************** Check and Radio buttons * */
 popover.combo list { min-width: 200px; }
 
@@ -170,11 +178,11 @@ list.content, list.content list { background-color: transparent; }
 
 list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > 
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#fdfdfc, #ffffff, 0.5); }
 
-list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: 
mix(#272c2e, #ffffff, 0.95); }
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: 
mix(#272c2e, #ffffff, 0.95); }
 
 list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > 
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander 
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander 
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
 
-list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander 
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander 
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
 
 list.content > row, list.content > row list > row { border-color: alpha(#877b6e, 0.7); border-style: solid; 
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
 
@@ -184,7 +192,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
 
 list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked { border-width: 1px; }
 
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
 
 list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { 
margin-top: 6px; }
 
diff --git a/src/themes/HighContrastInverse.css b/src/themes/HighContrastInverse.css
index 554e5a9f..a1735558 100644
--- a/src/themes/HighContrastInverse.css
+++ b/src/themes/HighContrastInverse.css
@@ -75,6 +75,14 @@ avatar.image { background: none; }
 
 viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
 
+statuspage > scrolledwindow > viewport > box { margin: 36px 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .icon { margin-bottom: 36px; opacity: 0.5; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .title { margin-bottom: 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .description { margin-bottom: 36px; }
+
 /*************************** Check and Radio buttons * */
 popover.combo list { min-width: 200px; }
 
@@ -170,11 +178,11 @@ list.content, list.content list { background-color: transparent; }
 
 list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > 
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#303030, #2d2d2d, 0.5); }
 
-list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: 
mix(#f3f3f1, #2d2d2d, 0.95); }
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: 
mix(#f3f3f1, #2d2d2d, 0.95); }
 
 list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > 
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander 
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander 
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
 
-list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander 
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander 
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
 
 list.content > row, list.content > row list > row { border-color: alpha(#686868, 0.7); border-style: solid; 
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
 
@@ -184,7 +192,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
 
 list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked { border-width: 1px; }
 
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
 
 list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { 
margin-top: 6px; }
 
diff --git a/src/themes/_fallback-base.scss b/src/themes/_fallback-base.scss
index 66f977b0..0f5cb6cf 100644
--- a/src/themes/_fallback-base.scss
+++ b/src/themes/_fallback-base.scss
@@ -147,3 +147,26 @@ viewswitchertitle viewswitcher {
   margin-left: 12px;
   margin-right: 12px;
 }
+
+// HdyStatusPage
+
+statuspage > scrolledwindow > viewport > box {
+  margin: 36px 12px;
+
+  > clamp > box {
+    > .icon {
+      margin-bottom: 36px;
+      opacity: 0.5;
+    }
+
+    > .title {
+      margin-bottom: 12px;
+    }
+
+    > .description {
+      margin-bottom: 36px;
+    }
+  }
+}
+
+
diff --git a/src/themes/fallback.css b/src/themes/fallback.css
index 13d103ff..cffb9221 100644
--- a/src/themes/fallback.css
+++ b/src/themes/fallback.css
@@ -74,3 +74,11 @@ avatar.contrasted { color: #fff; }
 avatar.image { background: none; }
 
 viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+statuspage > scrolledwindow > viewport > box { margin: 36px 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .icon { margin-bottom: 36px; opacity: 0.5; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .title { margin-bottom: 12px; }
+
+statuspage > scrolledwindow > viewport > box > clamp > box > .description { margin-bottom: 36px; }
diff --git a/tests/meson.build b/tests/meson.build
index 83abefda..ebde011e 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -39,6 +39,7 @@ test_names = [
   'test-preferences-window',
   'test-search-bar',
   'test-squeezer',
+  'test-status-page',
   'test-swipe-group',
   'test-value-object',
   'test-view-switcher',
diff --git a/tests/test-status-page.c b/tests/test-status-page.c
new file mode 100644
index 00000000..a3874cef
--- /dev/null
+++ b/tests/test-status-page.c
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 Andrei Lișiță <andreii lisita gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+gint notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+  notified++;
+}
+
+static void
+test_hdy_status_page_icon_name (void)
+{
+  g_autoptr (HdyStatusPage) status_page = NULL;
+  const gchar *icon_name = NULL;
+
+  status_page = HDY_STATUS_PAGE (g_object_ref_sink (hdy_status_page_new ()));
+  g_assert_nonnull (status_page);
+
+  notified = 0;
+  g_signal_connect (status_page, "notify::icon-name", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (status_page, "icon-name", &icon_name, NULL);
+  g_assert_cmpstr (icon_name, ==, NULL);
+
+  hdy_status_page_set_icon_name (status_page, NULL);
+  g_assert_cmpint (notified, ==, 0);
+
+  hdy_status_page_set_icon_name (status_page, "some-icon-symbolic");
+  g_assert_cmpstr (hdy_status_page_get_icon_name (status_page), ==, "some-icon-symbolic");
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (status_page, "icon-name", "other-icon-symbolic", NULL);
+  g_assert_cmpstr (hdy_status_page_get_icon_name (status_page), ==, "other-icon-symbolic");
+  g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_hdy_status_page_title (void)
+{
+  g_autoptr (HdyStatusPage) status_page = NULL;
+  const gchar *title = NULL;
+
+  status_page = HDY_STATUS_PAGE (g_object_ref_sink (hdy_status_page_new ()));
+  g_assert_nonnull (status_page);
+
+  notified = 0;
+  g_signal_connect (status_page, "notify::title", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (status_page, "title", &title, NULL);
+  g_assert_cmpstr (title, ==, "");
+
+  hdy_status_page_set_title (status_page, "");
+  g_assert_cmpint (notified, ==, 0);
+
+  hdy_status_page_set_title (status_page, "Some Title");
+  g_assert_cmpstr (hdy_status_page_get_title (status_page), ==, "Some Title");
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (status_page, "title", "Other Title", NULL);
+  g_assert_cmpstr (hdy_status_page_get_title (status_page), ==, "Other Title");
+  g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_hdy_status_page_description (void)
+{
+  g_autoptr (HdyStatusPage) status_page = NULL;
+  const gchar *description = NULL;
+
+  status_page = HDY_STATUS_PAGE (g_object_ref_sink (hdy_status_page_new ()));
+  g_assert_nonnull (status_page);
+
+  notified = 0;
+  g_signal_connect (status_page, "notify::description", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (status_page, "description", &description, NULL);
+  g_assert_cmpstr (description, ==, "");
+
+  hdy_status_page_set_description (status_page, "");
+  g_assert_cmpint (notified, ==, 0);
+
+  hdy_status_page_set_description (status_page, "Some description");
+  g_assert_cmpstr (hdy_status_page_get_description (status_page), ==, "Some description");
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (status_page, "description", "Other description", NULL);
+  g_assert_cmpstr (hdy_status_page_get_description (status_page), ==, "Other description");
+  g_assert_cmpint (notified, ==, 2);
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  gtk_test_init (&argc, &argv, NULL);
+  hdy_init ();
+
+  g_test_add_func ("/Handy/StatusPage/icon_name", test_hdy_status_page_icon_name);
+  g_test_add_func ("/Handy/StatusPage/title", test_hdy_status_page_title);
+  g_test_add_func ("/Handy/StatusPage/description", test_hdy_status_page_description);
+
+  return g_test_run ();
+}


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