[libadwaita] toast: Add custom-title property



commit d5e4e55dcf81ca032b09a3deadcd2d64f7801fad
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Sun Feb 27 10:21:04 2022 +0100

    toast: Add custom-title property
    
    Allow to change the title's GtkLabel to a custom widget.
    
    Fixes https://gitlab.gnome.org/GNOME/libadwaita/-/issues/339

 src/adw-toast-overlay.c           |  13 +++--
 src/adw-toast-widget.c            |  71 ++++++++++++++++++++++--
 src/adw-toast-widget.ui           |  14 +----
 src/adw-toast.c                   | 112 +++++++++++++++++++++++++++++++++++++-
 src/adw-toast.h                   |   6 ++
 src/stylesheet/widgets/_misc.scss |   2 +-
 tests/test-toast.c                |  62 +++++++++++++++++++++
 7 files changed, 255 insertions(+), 25 deletions(-)
---
diff --git a/src/adw-toast-overlay.c b/src/adw-toast-overlay.c
index 544caaa3..fcf11553 100644
--- a/src/adw-toast-overlay.c
+++ b/src/adw-toast-overlay.c
@@ -42,7 +42,9 @@
  * toastoverlay
  * ├── [child]
  * ├── toast
- * ┊   ├── label.heading
+ * ┊   ├── widget
+ * ┊   │   ├── [label.heading]
+ *     │   ╰── [custom title]
  *     ├── [button]
  *     ╰── button.circular.flat
  * ```
@@ -50,9 +52,12 @@
  * `AdwToastOverlay`'s CSS node is called `toastoverlay`. It contains the child,
  * as well as zero or more `toast` subnodes.
  *
- * Each of the `toast` nodes contains a `label` subnode with the `.heading`
- * style class, optionally a `button` subnode, and another `button` subnode with
- * `.circular` and `.flat` style classes.
+ * Each of the `toast` nodes contains a `widget` subnode, optionally a `button`
+ * subnode, and another `button` subnode with `.circular` and `.flat` style
+ * classes.
+ *
+ * The `widget` subnode contains a `label` subnode with the `.heading` style
+ * class, or a custom widget provided by the application.
  *
  * ## Accessibility
  *
diff --git a/src/adw-toast-widget.c b/src/adw-toast-widget.c
index b04f0d59..0b47724a 100644
--- a/src/adw-toast-widget.c
+++ b/src/adw-toast-widget.c
@@ -8,11 +8,14 @@
 
 #include "adw-toast-widget-private.h"
 
+#include "adw-bin.h"
 #include "adw-macros-private.h"
 
 struct _AdwToastWidget {
   GtkWidget parent_instance;
 
+  AdwBin *title_bin;
+
   AdwToast *toast;
 
   guint hide_timeout_id;
@@ -105,6 +108,64 @@ action_clicked_cb (AdwToastWidget *self)
   g_idle_add (G_SOURCE_FUNC (close_idle_cb), g_object_ref (self));
 }
 
+static void
+update_title_widget (AdwToastWidget *self)
+{
+  GtkWidget *custom_title;
+
+  if (!self->toast) {
+    adw_bin_set_child (self->title_bin, NULL);
+    return;
+  }
+
+  custom_title = adw_toast_get_custom_title (self->toast);
+
+  if (custom_title) {
+    adw_bin_set_child (self->title_bin, custom_title);
+  } else {
+    GtkWidget *title = gtk_label_new (NULL);
+
+    gtk_label_set_ellipsize (GTK_LABEL (title), PANGO_ELLIPSIZE_END);
+    gtk_label_set_xalign (GTK_LABEL (title), 0.0);
+    gtk_label_set_use_markup (GTK_LABEL (title), TRUE);
+    gtk_widget_add_css_class (title, "heading");
+
+    g_object_bind_property (self->toast, "title",
+                            title, "label",
+                            G_BINDING_SYNC_CREATE);
+
+    adw_bin_set_child (self->title_bin, title);
+  }
+}
+
+static void
+set_toast (AdwToastWidget *self,
+           AdwToast       *toast)
+{
+  g_assert (ADW_IS_TOAST_WIDGET (self));
+  g_assert (toast == NULL || ADW_IS_TOAST (toast));
+
+  if (self->toast) {
+    end_timeout (self);
+
+    g_signal_handlers_disconnect_by_func (self->toast,
+                                          update_title_widget,
+                                          self);
+  }
+
+  g_set_object (&self->toast, toast);
+  update_title_widget (self);
+
+  if (self->toast) {
+    g_signal_connect_swapped (toast,
+                              "notify::custom-title",
+                              G_CALLBACK (update_title_widget),
+                              self);
+
+    start_timeout (self);
+  }
+}
+
 static void
 adw_toast_widget_dispose (GObject *object)
 {
@@ -113,11 +174,11 @@ adw_toast_widget_dispose (GObject *object)
 
   end_timeout (self);
 
+  set_toast (self, NULL);
+
   while ((child = gtk_widget_get_first_child (GTK_WIDGET (self))))
     gtk_widget_unparent (child);
 
-  g_clear_pointer (&self->toast, g_object_unref);
-
   G_OBJECT_CLASS (adw_toast_widget_parent_class)->dispose (object);
 }
 
@@ -148,9 +209,7 @@ adw_toast_widget_set_property (GObject      *object,
 
   switch (prop_id) {
   case PROP_TOAST:
-    g_set_object (&self->toast, g_value_get_object (value));
-    end_timeout (self);
-    start_timeout (self);
+    set_toast (self, g_value_get_object (value));
     break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -179,6 +238,8 @@ adw_toast_widget_class_init (AdwToastWidgetClass *klass)
   gtk_widget_class_set_template_from_resource (widget_class,
                                                "/org/gnome/Adwaita/ui/adw-toast-widget.ui");
 
+  gtk_widget_class_bind_template_child (widget_class, AdwToastWidget, title_bin);
+
   gtk_widget_class_bind_template_callback (widget_class, string_is_not_empty);
   gtk_widget_class_bind_template_callback (widget_class, action_clicked_cb);
   gtk_widget_class_bind_template_callback (widget_class, dismiss);
diff --git a/src/adw-toast-widget.ui b/src/adw-toast-widget.ui
index 8a414492..e1d258bf 100644
--- a/src/adw-toast-widget.ui
+++ b/src/adw-toast-widget.ui
@@ -22,19 +22,9 @@
       </object>
     </child>
     <child>
-      <object class="GtkLabel" id="title">
-        <property name="ellipsize">end</property>
-        <property name="xalign">0</property>
+      <object class="AdwBin" id="title_bin">
         <property name="hexpand">True</property>
-        <property name="use-markup">True</property>
-        <binding name="label">
-          <lookup name="title" type="AdwToast">
-            <lookup name="toast">AdwToastWidget</lookup>
-          </lookup>
-        </binding>
-        <style>
-          <class name="heading"/>
-        </style>
+        <property name="halign">start</property>
       </object>
     </child>
     <child>
diff --git a/src/adw-toast.c b/src/adw-toast.c
index e0de2d93..12dd67f8 100644
--- a/src/adw-toast.c
+++ b/src/adw-toast.c
@@ -46,6 +46,9 @@
  * [property@Toast:priority] determines how it behaves if another toast is
  * already being displayed.
  *
+ * [property@Toast:custom-title] can be used to replace the title label with a
+ * custom widget.
+ *
  * ## Actions
  *
  * Toasts can have one button on them, with a label and an attached
@@ -152,6 +155,7 @@ struct _AdwToast {
   GVariant *action_target;
   AdwToastPriority priority;
   guint timeout;
+  GtkWidget *custom_title;
 
   gboolean added;
 };
@@ -164,6 +168,7 @@ enum {
   PROP_ACTION_TARGET,
   PROP_PRIORITY,
   PROP_TIMEOUT,
+  PROP_CUSTOM_TITLE,
   LAST_PROP,
 };
 
@@ -184,6 +189,16 @@ dismissed_cb (AdwToast *self)
   self->added = FALSE;
 }
 
+static void
+adw_toast_dispose (GObject *object)
+{
+  AdwToast *self = ADW_TOAST (object);
+
+  g_clear_object (&self->custom_title);
+
+  G_OBJECT_CLASS (adw_toast_parent_class)->dispose (object);
+}
+
 static void
 adw_toast_finalize (GObject *object)
 {
@@ -224,6 +239,9 @@ adw_toast_get_property (GObject    *object,
   case PROP_TIMEOUT:
     g_value_set_uint (value, adw_toast_get_timeout (self));
     break;
+  case PROP_CUSTOM_TITLE:
+    g_value_set_object (value, adw_toast_get_custom_title (self));
+    break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
   }
@@ -256,6 +274,9 @@ adw_toast_set_property (GObject      *object,
   case PROP_TIMEOUT:
     adw_toast_set_timeout (self, g_value_get_uint (value));
     break;
+  case PROP_CUSTOM_TITLE:
+    adw_toast_set_custom_title (self, g_value_get_object (value));
+    break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
   }
@@ -266,6 +287,7 @@ adw_toast_class_init (AdwToastClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
+  object_class->dispose = adw_toast_dispose;
   object_class->finalize = adw_toast_finalize;
   object_class->get_property = adw_toast_get_property;
   object_class->set_property = adw_toast_set_property;
@@ -277,6 +299,10 @@ adw_toast_class_init (AdwToastClass *klass)
    *
    * The title can be marked up with the Pango text markup language.
    *
+   * Setting a title will unset [property@Toast:custom-title].
+   *
+   * If [property@Toast:custom-title] is set, it will be used instead.
+   *
    * Since: 1.0
    */
   props[PROP_TITLE] =
@@ -384,6 +410,25 @@ adw_toast_class_init (AdwToastClass *klass)
                        0, G_MAXUINT, 5,
                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
 
+  /**
+   * AdwToast:custom-title: (attributes org.gtk.Property.get=adw_toast_get_custom_title 
org.gtk.Property.set=adw_toast_set_custom_title)
+   *
+   * The custom title widget.
+   *
+   * It will be displayed instead of the title if set. In this case,
+   * [property@Toast:title] is ignored.
+   *
+   * Setting a custom title will unset [property@Toast:title].
+   *
+   * Since: 1.2
+   */
+  props[PROP_CUSTOM_TITLE] =
+    g_param_spec_object ("custom-title",
+                         "Custom Title",
+                         "The custom title widget",
+                         GTK_TYPE_WIDGET,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
   g_object_class_install_properties (object_class, LAST_PROP, props);
 
   /**
@@ -408,6 +453,7 @@ adw_toast_init (AdwToast *self)
   self->title = g_strdup ("");
   self->priority = ADW_TOAST_PRIORITY_NORMAL;
   self->timeout = 5;
+  self->custom_title = NULL;
 
   g_signal_connect (self, "dismissed", G_CALLBACK (dismissed_cb), self);
 }
@@ -442,7 +488,10 @@ adw_toast_new (const char *title)
  *
  * Gets the title that will be displayed on the toast.
  *
- * Returns: the title
+ * If a custom title has been set with [method@Adw.Toast.set_custom_title]
+ * the return value will be %NULL.
+ *
+ * Returns: (nullable): the title
  *
  * Since: 1.0
  */
@@ -451,7 +500,10 @@ adw_toast_get_title (AdwToast *self)
 {
   g_return_val_if_fail (ADW_IS_TOAST (self), NULL);
 
-  return self->title;
+  if (self->custom_title == NULL)
+    return self->title;
+
+  return NULL;
 }
 
 /**
@@ -473,10 +525,16 @@ adw_toast_set_title (AdwToast   *self,
   if (!g_strcmp0 (self->title, title))
     return;
 
+  g_object_freeze_notify (G_OBJECT (self));
+
+  adw_toast_set_custom_title (self, NULL);
+
   g_clear_pointer (&self->title, g_free);
   self->title = g_strdup (title);
 
   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+
+  g_object_thaw_notify (G_OBJECT (self));
 }
 
 /**
@@ -801,7 +859,7 @@ adw_toast_dismiss (AdwToast *self)
 
   if (!self->added) {
     g_critical ("Trying to dismiss the toast '%s', but it isn't in an "
-                "AdwToastOverlay yet", self->title);
+                "AdwToastOverlay yet", adw_toast_get_title (self));
 
     return;
   }
@@ -825,3 +883,51 @@ adw_toast_set_added (AdwToast *self,
 
   self->added = !!added;
 }
+
+/**
+ * adw_toast_set_custom_title: (attributes org.gtk.Method.set_property=custom-title)
+ * @self: a toast
+ * @widget: (nullable): the custom title widget
+ *
+ * Sets the custom title widget of @self.
+ *
+ * Since: 1.2
+ */
+void
+adw_toast_set_custom_title (AdwToast  *self,
+                            GtkWidget *widget)
+{
+  g_return_if_fail (ADW_IS_TOAST (self));
+  g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget));
+
+  if (self->custom_title == widget)
+    return;
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  adw_toast_set_title (self, "");
+
+  g_set_object (&self->custom_title, widget);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CUSTOM_TITLE]);
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * adw_toast_get_custom_title: (attributes org.gtk.Method.get_property=custom-title)
+ * @self: a toast
+ *
+ * Gets the custom title widget of @self.
+ *
+ * Returns: (nullable) (transfer none): the custom title widget
+ *
+ * Since: 1.2
+ */
+GtkWidget *
+adw_toast_get_custom_title (AdwToast *self)
+{
+  g_return_val_if_fail (ADW_IS_TOAST (self), NULL);
+
+  return self->custom_title;
+}
diff --git a/src/adw-toast.h b/src/adw-toast.h
index a6adfe39..af67851e 100644
--- a/src/adw-toast.h
+++ b/src/adw-toast.h
@@ -73,6 +73,12 @@ ADW_AVAILABLE_IN_ALL
 void  adw_toast_set_timeout (AdwToast *self,
                              guint     timeout);
 
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_toast_get_custom_title (AdwToast *self);
+ADW_AVAILABLE_IN_ALL
+void       adw_toast_set_custom_title (AdwToast  *self,
+                                       GtkWidget *widget);
+
 ADW_AVAILABLE_IN_ALL
 void adw_toast_dismiss (AdwToast *self);
 
diff --git a/src/stylesheet/widgets/_misc.scss b/src/stylesheet/widgets/_misc.scss
index ed88a3ef..6c4ff1f7 100644
--- a/src/stylesheet/widgets/_misc.scss
+++ b/src/stylesheet/widgets/_misc.scss
@@ -66,7 +66,7 @@ toast {
   &:dir(ltr) { padding-left: 12px; }
   &:dir(rtl) { padding-right: 12px; }
 
-  > label {
+  > widget {
     margin: 0 6px;
   }
 
diff --git a/tests/test-toast.c b/tests/test-toast.c
index 26bdc59d..216959d3 100644
--- a/tests/test-toast.c
+++ b/tests/test-toast.c
@@ -216,6 +216,66 @@ test_adw_toast_dismiss (void)
   g_assert_finalize_object (toast);
 }
 
+static void
+test_adw_toast_custom_title (void)
+{
+  AdwToast *toast = adw_toast_new ("Title");
+  GtkWidget *widget = NULL;
+  char *title;
+
+  g_assert_nonnull (toast);
+
+  notified = 0;
+  g_signal_connect (toast, "notify::custom-title", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (toast, "title", &title, NULL);
+  g_assert_cmpstr (title, ==, "Title");
+  g_object_get (toast, "custom-title", &widget, NULL);
+  g_assert_null (widget);
+
+  adw_toast_set_title (toast, "Another title");
+  g_assert_cmpint (notified, ==, 0);
+
+  widget = g_object_ref_sink (gtk_label_new ("Custom title"));
+  adw_toast_set_custom_title (toast, widget);
+  g_assert_true (adw_toast_get_custom_title (toast) == widget);
+  g_assert_null (adw_toast_get_title (toast));
+  g_assert_cmpint (notified, ==, 1);
+
+  adw_toast_set_title (toast, "Final title");
+  g_assert_null (adw_toast_get_custom_title (toast));
+  g_assert_cmpstr (adw_toast_get_title (toast), ==, "Final title");
+  g_assert_cmpint (notified, ==, 2);
+
+  g_free (title);
+  g_assert_finalize_object (toast);
+  g_assert_finalize_object (widget);
+}
+
+static void
+test_adw_toast_custom_title_overlay (void)
+{
+  AdwToastOverlay *first_overlay = g_object_ref_sink (ADW_TOAST_OVERLAY (adw_toast_overlay_new ()));
+  AdwToastOverlay *second_overlay = g_object_ref_sink (ADW_TOAST_OVERLAY (adw_toast_overlay_new ()));
+  AdwToast *toast = adw_toast_new ("");
+  GtkWidget *widget = gtk_label_new ("Custom title");
+
+  g_assert_nonnull (first_overlay);
+  g_assert_nonnull (second_overlay);
+  g_assert_nonnull (toast);
+
+  adw_toast_set_custom_title (toast, g_object_ref (widget));
+
+  adw_toast_overlay_add_toast (first_overlay, g_object_ref (toast));
+  adw_toast_dismiss (toast);
+  adw_toast_overlay_add_toast (second_overlay, g_object_ref (toast));
+
+  g_assert_finalize_object (first_overlay);
+  g_assert_finalize_object (second_overlay);
+  g_assert_finalize_object (toast);
+  g_assert_finalize_object (widget);
+}
+
 int
 main (int   argc,
       char *argv[])
@@ -231,6 +291,8 @@ main (int   argc,
   g_test_add_func ("/Adwaita/Toast/priority", test_adw_toast_priority);
   g_test_add_func ("/Adwaita/Toast/timeout", test_adw_toast_timeout);
   g_test_add_func ("/Adwaita/Toast/dismiss", test_adw_toast_dismiss);
+  g_test_add_func ("/Adwaita/Toast/custom_title", test_adw_toast_custom_title);
+  g_test_add_func ("/Adwaita/Toast/custom_title_overlay", test_adw_toast_custom_title_overlay);
 
   return g_test_run ();
 }


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