diff --git a/demo/adw-demo-window.c b/demo/adw-demo-window.c index 1337d26cd70b94e3571800402ecbf673e8136123..e1542a3f8f0aabd58205bc287ad44d5cc9f030b6 100644 --- a/demo/adw-demo-window.c +++ b/demo/adw-demo-window.c @@ -11,6 +11,7 @@ struct _AdwDemoWindow AdwApplicationWindow parent_instance; AdwLeaflet *content_box; + AdwToastOverlay *toast_overlay; GtkBox *right_box; GtkWidget *color_scheme_button; GtkStackSidebar *sidebar; @@ -26,6 +27,8 @@ struct _AdwDemoWindow GtkButton *avatar_remove_button; GtkFileChooserNative *avatar_file_chooser; GtkListBox *avatar_contacts; + int toast_undo_items; + AdwToast *undo_toast; }; G_DEFINE_TYPE (AdwDemoWindow, adw_demo_window, ADW_TYPE_APPLICATION_WINDOW) @@ -398,6 +401,87 @@ style_classes_demo_clicked_cb (GtkButton *btn, gtk_window_present (GTK_WINDOW (window)); } +static void +add_toast_cb (AdwDemoWindow *self) +{ + adw_toast_overlay_add_toast (self->toast_overlay, + adw_toast_new (_("Simple toast"))); +} + +static void +dismissed_cb (AdwDemoWindow *self) +{ + self->undo_toast = NULL; + self->toast_undo_items = 0; + + gtk_widget_action_set_enabled (GTK_WIDGET (self), "toast.dismiss", FALSE); +} + +static void +add_toast_with_button_cb (AdwDemoWindow *self) +{ + g_autofree char *title = NULL; + + self->toast_undo_items++; + + if (!self->undo_toast) { + title = g_strdup_printf (_("‘%s’ deleted"), "Lorem ipsum"); + + self->undo_toast = adw_toast_new (title); + + adw_toast_set_priority (self->undo_toast, ADW_TOAST_PRIORITY_HIGH); + adw_toast_set_button_label (self->undo_toast, _("Undo")); + adw_toast_set_action_name (self->undo_toast, "toast.undo"); + + g_signal_connect_swapped (self->undo_toast, "dismissed", G_CALLBACK (dismissed_cb), self); + + adw_toast_overlay_add_toast (self->toast_overlay, self->undo_toast); + + gtk_widget_action_set_enabled (GTK_WIDGET (self), "toast.dismiss", TRUE); + + return; + } + + title = + g_strdup_printf (ngettext ("%d item deleted", + "%d items deleted", + self->toast_undo_items), self->toast_undo_items); + + adw_toast_set_title (self->undo_toast, title); +} + +static void +add_toast_with_long_title_cb (AdwDemoWindow *self) +{ + adw_toast_overlay_add_toast (self->toast_overlay, + adw_toast_new (_("Lorem ipsum dolor sit amet, " + "consectetur adipiscing elit, " + "sed do eiusmod tempor incididunt " + "ut labore et dolore magnam aliquam " + "quaerat voluptatem."))); +} + +static void +toast_undo_cb (AdwDemoWindow *self) +{ + g_autofree char *title = + g_strdup_printf (ngettext ("Undoing deleting %d item…", + "Undoing deleting %d items…", + self->toast_undo_items), self->toast_undo_items); + AdwToast *toast = adw_toast_new (title); + + adw_toast_set_priority (toast, ADW_TOAST_PRIORITY_HIGH); + + adw_toast_overlay_add_toast (self->toast_overlay, toast); +} + +static void +toast_dismiss_cb (AdwDemoWindow *self) +{ + if (self->undo_toast) + adw_toast_dismiss (self->undo_toast); +} + static void adw_demo_window_class_init (AdwDemoWindowClass *klass) { @@ -405,6 +489,9 @@ adw_demo_window_class_init (AdwDemoWindowClass *klass) gtk_widget_class_add_binding_action (widget_class, GDK_KEY_q, GDK_CONTROL_MASK, "window.close", NULL); + gtk_widget_class_install_action (widget_class, "toast.undo", NULL, (GtkWidgetActionActivateFunc) toast_undo_cb); + gtk_widget_class_install_action (widget_class, "toast.dismiss", NULL, (GtkWidgetActionActivateFunc) toast_dismiss_cb); + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Adwaita1/Demo/ui/adw-demo-window.ui"); gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, content_box); gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, right_box); @@ -421,6 +508,7 @@ adw_demo_window_class_init (AdwDemoWindowClass *klass) gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, avatar_file_chooser_label); gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, avatar_remove_button); gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, avatar_contacts); + gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, toast_overlay); gtk_widget_class_bind_template_callback (widget_class, notify_visible_child_cb); gtk_widget_class_bind_template_callback (widget_class, back_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, leaflet_back_clicked_cb); @@ -441,6 +529,9 @@ adw_demo_window_class_init (AdwDemoWindowClass *klass) gtk_widget_class_bind_template_callback (widget_class, flap_demo_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, tab_view_demo_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, style_classes_demo_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, add_toast_cb); + gtk_widget_class_bind_template_callback (widget_class, add_toast_with_button_cb); + gtk_widget_class_bind_template_callback (widget_class, add_toast_with_long_title_cb); } static void @@ -485,4 +576,6 @@ adw_demo_window_init (AdwDemoWindow *self) avatar_page_init (self); adw_leaflet_set_visible_child (self->content_box, GTK_WIDGET (self->right_box)); + + gtk_widget_action_set_enabled (GTK_WIDGET (self), "toast.dismiss", FALSE); } diff --git a/demo/adw-demo-window.ui b/demo/adw-demo-window.ui index ebd0b867d8bfe0ec979630e242e3321b22af1411..aba5a73e50352109ce1efa15ca5319dbb748cb5b 100644 --- a/demo/adw-demo-window.ui +++ b/demo/adw-demo-window.ui @@ -39,944 +39,1017 @@ 800 576 - - True - 360 - False - - - + + + True + 360 + False + - - vertical + + True - - - - - - - - - AdwDemoWindow - - - - - + + vertical + + + + + + + + + + AdwDemoWindow + + + + + + + + + + primary_menu + open-menu-symbolic + + - - - primary_menu - open-menu-symbolic + + + 270 + True + stack - - 270 - True - stack - - - - - - - False - - - - - - - - vertical - True - - - - - + + False + + - - - center - Back - go-previous-symbolic - - - - - - True - False - + + vertical + True - - welcome - Welcome - - - org.gnome.Adwaita1.Demo-symbolic - Welcome to Adwaita Demo - This is a tour of the features the library has to offer. - + + + + + + + center + Back + go-previous-symbolic + + + + - - leaflet - Leaflet - - - widget-leaflet-symbolic - Leaflet - A widget showing either all its children or only one, depending on the available space. This window is using a leaflet, you can control it with the settings below. + + True + False + + + + welcome + Welcome - - - - - - The type of transition to use when the leaflet adapts its size or when changing the visible child - Transition type - - - - AdwLeafletTransitionType - - - - - - - - - - Go to the next page of the leaflet - True - True - - - - go-next-symbolic - - - - - - + + org.gnome.Adwaita1.Demo-symbolic + Welcome to Adwaita Demo + This is a tour of the features the library has to offer. - - - - - - clamp - Clamp - - - widget-clamp-symbolic - Clamp - This page is clamped to smoothly grow up to a maximum width. + + + + leaflet + Leaflet - - - + + widget-leaflet-symbolic + Leaflet + A widget showing either all its children or only one, depending on the available space. This window is using a leaflet, you can control it with the settings below. - - - - Maximum width + + + - - clamp_maximum_size_adjustment - center + + The type of transition to use when the leaflet adapts its size or when changing the visible child + Transition type + + + + AdwLeafletTransitionType + + + + + - - - - - Tightening threshold - - clamp_tightening_threshold_adjustment - center + + Go to the next page of the leaflet + True + True + + + + go-next-symbolic + + - + - - - - - - lists - Lists - - - widget-list-symbolic - Lists - Rows and helpers for GtkListBox. + + + + clamp + Clamp - - 400 - 300 + + widget-clamp-symbolic + Clamp + This page is clamped to smoothly grow up to a maximum width. - - vertical - 12 - + + + + - row-preferences-symbolic - They also have a subtitle and an icon - Rows have a title - - - - - frobnicate - Rows can have suffix widgets + Maximum width - - end - Frobnicate - center - - - - - - - - - - - - radio_button_1 - Rows can have prefix widgets - - + + clamp_maximum_size_adjustment center - True - radio_button_2 - Rows can have prefix widgets - - - radio_button_1 + Tightening threshold + + + clamp_tightening_threshold_adjustment center - - - - Combo Rows + + + + + + + + + + lists + Lists + + + widget-list-symbolic + Lists + Rows and helpers for GtkListBox. + + + 400 + 300 + + + vertical + 12 - - Combo row - - - - Foo - Bar - Baz - + + + + row-preferences-symbolic + They also have a subtitle and an icon + Rows have a title - - - - - - This combo row was created from an enumeration - Enumeration combo row - - - GtkLicense + + + + frobnicate + Rows can have suffix widgets + + + end + Frobnicate + center + + + - - - - + - - - - - Expander Rows - - Expander row + - A nested row + radio_button_1 + Rows can have prefix widgets + + + center + True + + - Another nested row + radio_button_2 + Rows can have prefix widgets + + + radio_button_1 + center + + - - Expander row with an action - - - center - row-copy-symbolic - - - + + Combo Rows - - A nested row + + Combo row + + + + Foo + Bar + Baz + + + - - Another nested row + + This combo row was created from an enumeration + Enumeration combo row + + + GtkLicense + + + + + - - True - Toggleable expander row + + Expander Rows - - A nested row + + Expander row + + + A nested row + + + + + Another nested row + + - - Another nested row + + Expander row with an action + + + center + row-copy-symbolic + + + + + + A nested row + + + + + Another nested row + + + + + + + True + Toggleable expander row + + + A nested row + + + + + Another nested row + + - + - - - - - - view-switcher - View Switcher - - - widget-view-switcher-symbolic + + + + view-switcher View Switcher - Widgets to switch the window's view. - - - Run the demo - center - - + + + widget-view-switcher-symbolic + View Switcher + Widgets to switch the window's view. + + + Run the demo + center + + + + - + - - - - - - carousel - Carousel - - - vertical - - - - - - True - True - - + + + + carousel + Carousel + + + vertical - - widget-carousel-symbolic - Carousel - A widget for paginated scrolling. - True - + - - 32 - 12 - 12 - 400 - 300 - center + + True + True + + - + + widget-carousel-symbolic + Carousel + A widget for paginated scrolling. + True + + + + + 32 + 12 + 12 + 400 + 300 + center - - Orientation - - - - GtkOrientation + + + + Orientation + + + + GtkOrientation + + + + + - - - - - - - - - Page Indicators - - - - - dots - lines - + + + + Page Indicators + + + + + dots + lines + + + + + + - - - - - - - - - Scroll wheel - carousel_scroll_wheel + - - center - True + + Scroll wheel + carousel_scroll_wheel + + + center + True + + - - - - - Long swipes - carousel_long_swipes - - center + + Long swipes + carousel_long_swipes + + + center + + - - - - - Another page - True - - - _Return to the first page - True - center - - + + + Another page + True + + + _Return to the first page + True + center + + + + - + - - - - - False - 6 - 6 - 6 - 6 - - dots - - - carousel - + + False + 6 + 6 + 6 + 6 + + + dots + + + carousel + + + - - - - - - lines - - - carousel - + + + + lines + + + carousel + + + - + - + - - - - - - avatar - Avatar - - - never + + + + avatar + Avatar - - True + + never - - vertical - start - - + + True + vertical + start + - - center - center - - - - - - - - Avatar - center - 0 - True - word-char - center - - - - - - A user avatar with generated fallback. - center - true - True - - - - - - - - 400 - 300 - - center vertical - 12 - + + center + center + + + + + + + + Avatar + center + 0 + True + word-char + center + + + + + + A user avatar with generated fallback. + center + true + True + + + + + + + + 400 + 300 + + + center + vertical + 12 - - Text + - - center + + Text + + + center + + - - - - - Show initials - avatar_show_initials - - center - True + + Show initials + avatar_show_initials + + + center + True + + - - - - - File - - center - + + File - - middle - 20 + + center + + + + middle + 20 + + + + + + + + center + avatar-delete-symbolic + + - - - center - avatar-delete-symbolic - - + + Size + + + center + True + avatar_adjustment + + - - - - - Size - - center - True - avatar_adjustment + + Export to file + + + center + avatar-save-symbolic + + + + - - Export to file - - - center - avatar-save-symbolic - - - - + + none + - - - - none - - - + - + - + - - - - - - flap - Flap - - - widget-flap-symbolic + + + + flap Flap - A widget showing a flap next to or above the content. - - Run the demo - center - - + + widget-flap-symbolic + Flap + A widget showing a flap next to or above the content. + + + Run the demo + center + + + + - - - - - - tab-view - Tab View - - - widget-tab-view-symbolic + + + + tab-view Tab View - A modern tab widget. - - Run the demo - center - - + + widget-tab-view-symbolic + Tab View + A modern tab widget. + + + Run the demo + center + + + + - - - - - - buttons - Buttons - - + + + + buttons Buttons - Button helper widgets. - - 400 - 300 + + Buttons + Button helper widgets. - - vertical - - - center - 12 - 12 - - - document-open-symbolic - sample_menu - - 0 - 0 - - - - - - document-open-symbolic - sample_menu - - - 0 - 1 - - - - - - _Open - True - sample_menu - - 1 - 0 - - - - - - _Open - True - sample_menu - - - 1 - 1 - - - + + 400 + 300 + + + vertical - - - + + center + 12 + 12 + + document-open-symbolic + sample_menu + + 0 + 0 + + + + + + document-open-symbolic + sample_menu + + + 0 + 1 + + + + + _Open True + sample_menu + + 1 + 0 + - - sample_menu - - 2 - 0 - - - - - - - - document-open-symbolic + + + _Open True + sample_menu + + + 1 + 1 + - - sample_menu - - - 2 - 1 - + + + + + + document-open-symbolic + _Open + True + + + sample_menu + + 2 + 0 + + + + + + + + document-open-symbolic + _Open + True + + + sample_menu + + + 2 + 1 + + + - + - - - - - - style-classes - Style Classes - - - style-classes-symbolic + + + + style-classes Style Classes - Various widget styles available for use. - - Run the demo - center - - + + style-classes-symbolic + Style Classes + Various widget styles available for use. + + + Run the demo + center + + + + - + + + + toast + Toast + + + preferences-system-notifications-symbolic + Toast + Transient in-app notifications. + + + 400 + 300 + + + + + Simple toast + + + center + Show + + + + + + + + Toast with an action + + + center + user-trash-symbolic + toast.dismiss + + + + + + center + Show + + + + + + + + Toast with a long title + + + center + Show + + + + + + + + + + + + + - - - - - vertical - - - - - - - center - Back - go-previous-symbolic - - - - - - - - True - Go back - - - vertical - center - 12 - - - gesture-touchscreen-swipe-back-symbolic - 128 - + + vertical + + + + + + + + center + Back + go-previous-symbolic + - - - gesture-touchpad-swipe-back-symbolic - 128 - + + + + + True + Go back + + + vertical + center + 12 + + + gesture-touchscreen-swipe-back-symbolic + 128 + + + + + + gesture-touchpad-swipe-back-symbolic + 128 + + + - + - + - + diff --git a/src/adw-enums.c.in b/src/adw-enums.c.in index b151b1edb56b623f4366970180681024df1f98ca..c36ff6892245f07eaa89047844ddd978a2dcf8b6 100644 --- a/src/adw-enums.c.in +++ b/src/adw-enums.c.in @@ -9,6 +9,7 @@ #include "adw-navigation-direction.h" #include "adw-squeezer.h" #include "adw-style-manager.h" +#include "adw-toast.h" #include "adw-view-switcher.h" /*** END file-header ***/ diff --git a/src/adw-toast-overlay.c b/src/adw-toast-overlay.c new file mode 100644 index 0000000000000000000000000000000000000000..9606d69d894777923f04d2253f7fac6ded53725b --- /dev/null +++ b/src/adw-toast-overlay.c @@ -0,0 +1,599 @@ +/* + * Copyright (C) 2021 Maximiliano Sandoval + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" + +#include "adw-toast-overlay.h" + +#include "adw-animation-private.h" +#include "adw-animation-util-private.h" +#include "adw-toast-private.h" +#include "adw-toast-widget-private.h" +#include "adw-widget-utils-private.h" + +#define SHOW_DURATION 300 +#define HIDE_DURATION 300 +#define REPLACE_DURATION 500 +#define FINAL_SIZE 0.7 +#define NATURAL_WIDTH 450 + +/** + * AdwToastOverlay: + * + * A widget showing toasts above its content. + * + * Toasts can be shown with [method@Adw.ToastOverlay.add_toast]. + * + * See [class@Adw.Toast] for details. + * + * ## CSS nodes + * + * ``` + * toastoverlay + * ├── [child] + * ├── toast + * ┊ ├── label.heading + * ├── [button] + * ╰── button.circular.flat + * ``` + * + * `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. + * + * ## Accessibility + * + * `AdwToastOverlay` uses the `GTK_ACCESSIBLE_ROLE_TAB_GROUP` role. + * + * Since: 1.0 + */ + +typedef struct { + AdwToastOverlay *overlay; + + AdwToast *toast; + GtkWidget *widget; + + AdwAnimation *show_animation; + AdwAnimation *hide_animation; + + gulong dismissed_id; + gboolean postponing; +} ToastInfo; + +struct _AdwToastOverlay { + GtkWidget parent_instance; + + GtkWidget *child; + + GQueue *queue; + ToastInfo *current_toast; + GList *hiding_toasts; +}; + +enum { + PROP_0, + PROP_CHILD, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +static void adw_toast_overlay_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (AdwToastOverlay, adw_toast_overlay, GTK_TYPE_WIDGET, + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + adw_toast_overlay_buildable_init)) + +static GtkBuildableIface *parent_buildable_iface; + +static void +free_toast_info (ToastInfo *info) +{ + if (info->dismissed_id && info->toast) + g_signal_handler_disconnect (info->toast, info->dismissed_id); + + g_clear_object (&info->show_animation); + g_clear_object (&info->hide_animation); + g_clear_pointer (&info->widget, gtk_widget_unparent); + g_clear_object (&info->toast); + + g_free (info); +} + +static void +dismiss_and_free_toast_info (ToastInfo *info) +{ + g_signal_handler_disconnect (info->toast, info->dismissed_id); + info->dismissed_id = 0; + + adw_toast_dismiss (info->toast); + + free_toast_info (info); +} + +static void +hide_value_cb (double value, + ToastInfo *info) +{ + gtk_widget_set_opacity (info->widget, adw_ease_out_cubic (value)); + + gtk_widget_queue_allocate (GTK_WIDGET (info->overlay)); +} + +static void +hide_done_cb (ToastInfo *info) +{ + AdwToastOverlay *self = info->overlay; + + self->hiding_toasts = g_list_remove (self->hiding_toasts, info); + + /* We don't want to free the toast just yet, just remove the widget as we'll + * make a new one later */ + if (info->postponing && info->dismissed_id) { + g_clear_object (&info->show_animation); + g_clear_object (&info->hide_animation); + g_clear_pointer (&info->widget, gtk_widget_unparent); + info->postponing = FALSE; + + return; + } + + free_toast_info (info); +} + +static void +show_value_cb (double value, + ToastInfo *info) +{ + gtk_widget_queue_allocate (GTK_WIDGET (info->overlay)); +} + +static void +show_done_cb (ToastInfo *info) +{ + g_clear_object (&info->show_animation); +} + +static void show_toast (AdwToastOverlay *self, + ToastInfo *info); + +static void +hide_current_toast (AdwToastOverlay *self) +{ + ToastInfo *info = self->current_toast; + + self->hiding_toasts = g_list_append (self->hiding_toasts, info); + self->current_toast = NULL; + + gtk_widget_set_can_target (GTK_WIDGET (info->widget), FALSE); + gtk_widget_set_can_focus (GTK_WIDGET (info->widget), FALSE); + + info->hide_animation = + adw_animation_new (GTK_WIDGET (self), 1, 0, HIDE_DURATION, + (AdwAnimationTargetFunc) hide_value_cb, info); + + g_signal_connect_swapped (info->hide_animation, "done", + G_CALLBACK (hide_done_cb), info); + + adw_animation_start (info->hide_animation); +} + +static void +dismissed_cb (ToastInfo *info) +{ + AdwToastOverlay *self = info->overlay; + + if (info->hide_animation && !info->postponing) + return; + + /* Protect against repeat emissions */ + if (info->dismissed_id) { + g_signal_handler_disconnect (info->toast, info->dismissed_id); + info->dismissed_id = 0; + } + + if (info == self->current_toast) { + ToastInfo *next_toast; + + hide_current_toast (self); + + next_toast = g_queue_pop_head (self->queue); + + if (next_toast) + show_toast (self, next_toast); + } else { + g_queue_remove (self->queue, info); + + if (!info->hide_animation) + free_toast_info (info); + } +} + +static void +show_toast (AdwToastOverlay *self, + ToastInfo *info) +{ + g_assert (!info->widget); + + self->current_toast = info; + + info->widget = adw_toast_widget_new (info->toast); + gtk_widget_insert_before (info->widget, GTK_WIDGET (self), NULL); + + info->show_animation = + adw_animation_new (GTK_WIDGET (self), 0, 1, + self->hiding_toasts ? REPLACE_DURATION : SHOW_DURATION, + (AdwAnimationTargetFunc) show_value_cb, info); + + g_signal_connect_swapped (info->show_animation, "done", + G_CALLBACK (show_done_cb), info); + + adw_animation_start (info->show_animation); +} + +static gboolean +dismiss_cb (AdwToastOverlay *self, + GVariant *args) +{ + if (self->current_toast) { + adw_toast_dismiss (self->current_toast->toast); + + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +static void +adw_toast_overlay_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + GtkWidget *child; + + for (child = gtk_widget_get_first_child (widget); + child; + child = gtk_widget_get_next_sibling (child)) { + int child_min = 0; + int child_nat = 0; + int child_min_baseline = -1; + int child_nat_baseline = -1; + + if (!gtk_widget_should_layout (child)) + continue; + + gtk_widget_measure (child, orientation, for_size, + &child_min, &child_nat, + &child_min_baseline, &child_nat_baseline); + + *minimum = MAX (*minimum, child_min); + *natural = MAX (*natural, child_nat); + + if (child_min_baseline > -1) + *minimum_baseline = MAX (*minimum_baseline, child_min_baseline); + if (child_nat_baseline > -1) + *natural_baseline = MAX (*natural_baseline, child_nat_baseline); + } +} + +static void +allocate_toast (AdwToastOverlay *self, + ToastInfo *info, + int width, + int height) +{ + GtkRequisition size; + GskTransform *transform; + float x, y; + + gtk_widget_get_preferred_size (info->widget, NULL, &size); + + size.width = MIN (MAX (size.width, NATURAL_WIDTH), width); + size.height = MIN (size.height, height); + + x = (width - size.width) / 2; + y = height - size.height; + transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (x, y)); + + if (info->show_animation) { + float value = adw_animation_get_value (info->show_animation); + float offset = adw_lerp (size.height, 0.0f, value); + + transform = gsk_transform_translate (transform, + &GRAPHENE_POINT_INIT (0, offset)); + } + + if (info->hide_animation) { + float value = adw_animation_get_value (info->hide_animation); + + x = size.width / 2.0f; + y = size.height / 2.0f; + + value = adw_lerp (FINAL_SIZE, 1.0f, value); + transform = gsk_transform_translate (transform, + &GRAPHENE_POINT_INIT (x, y)); + transform = gsk_transform_scale (transform, value, value); + transform = gsk_transform_translate (transform, + &GRAPHENE_POINT_INIT (-x, -y)); + } + + gtk_widget_allocate (info->widget, size.width, size.height, -1, transform); +} + +static void +adw_toast_overlay_size_allocate (GtkWidget *widget, + int width, + int height, + int baseline) +{ + AdwToastOverlay *self = ADW_TOAST_OVERLAY (widget); + GList *l; + + if (self->child && gtk_widget_should_layout (self->child)) + gtk_widget_allocate (self->child, width, height, baseline, NULL); + + for (l = self->hiding_toasts; l; l = l->next) + allocate_toast (self, l->data, width, height); + + if (self->current_toast) + allocate_toast (self, self->current_toast, width, height); +} + +static void +adw_toast_overlay_dispose (GObject *object) +{ + AdwToastOverlay *self = ADW_TOAST_OVERLAY (object); + + adw_toast_overlay_set_child (self, NULL); + + g_clear_list (&self->hiding_toasts, (GDestroyNotify) free_toast_info); + g_clear_pointer (&self->current_toast, dismiss_and_free_toast_info); + g_queue_foreach (self->queue, (GFunc) dismiss_and_free_toast_info, NULL); + + G_OBJECT_CLASS (adw_toast_overlay_parent_class)->dispose (object); +} + +static void +adw_toast_overlay_finalize (GObject *object) +{ + AdwToastOverlay *self = ADW_TOAST_OVERLAY (object); + + g_queue_free (self->queue); + + G_OBJECT_CLASS (adw_toast_overlay_parent_class)->finalize (object); +} + +static void +adw_toast_overlay_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + AdwToastOverlay *self = ADW_TOAST_OVERLAY (object); + + switch (prop_id) { + case PROP_CHILD: + g_value_set_object (value, adw_toast_overlay_get_child (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +adw_toast_overlay_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + AdwToastOverlay *self = ADW_TOAST_OVERLAY (object); + + switch (prop_id) { + case PROP_CHILD: + adw_toast_overlay_set_child (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +adw_toast_overlay_class_init (AdwToastOverlayClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = adw_toast_overlay_dispose; + object_class->finalize = adw_toast_overlay_finalize; + object_class->get_property = adw_toast_overlay_get_property; + object_class->set_property = adw_toast_overlay_set_property; + + widget_class->compute_expand = adw_widget_compute_expand; + widget_class->measure = adw_toast_overlay_measure; + widget_class->size_allocate = adw_toast_overlay_size_allocate; + + /** + * AdwToastOverlay:child: (attributes org.gtk.Property.get=adw_toast_overlay_get_child org.gtk.Property.set=adw_toast_overlay_set_child) + * + * The child widget. + * + * Since: 1.0 + */ + props[PROP_CHILD] = + g_param_spec_object ("child", + "Child", + "The child widget", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "toastoverlay"); + gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GROUP); + + gtk_widget_class_add_binding (widget_class, GDK_KEY_Escape, 0, (GtkShortcutFunc) dismiss_cb, NULL); +} + +static void +adw_toast_overlay_init (AdwToastOverlay *self) +{ + self->queue = g_queue_new (); +} + +static void +adw_toast_overlay_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const char *type) +{ + AdwToastOverlay *self = ADW_TOAST_OVERLAY (buildable); + + if (!type && GTK_IS_WIDGET (child)) + adw_toast_overlay_set_child (self, GTK_WIDGET (child)); + else + parent_buildable_iface->add_child (buildable, builder, child, type); +} + +static void +adw_toast_overlay_buildable_init (GtkBuildableIface *iface) +{ + parent_buildable_iface = g_type_interface_peek_parent (iface); + + iface->add_child = adw_toast_overlay_buildable_add_child; +} + +/** + * adw_toast_overlay_new: + * + * Creates a new `AdwToastOverlay`. + * + * Returns: the new created `AdwToastOverlay` + * + * Since: 1.0 + */ +GtkWidget * +adw_toast_overlay_new (void) +{ + return g_object_new (ADW_TYPE_TOAST_OVERLAY, NULL); +} + +/** + * adw_toast_overlay_get_child: (attributes org.gtk.Method.get_property=child) + * @self: a `AdwToastOverlay` + * + * Gets the child widget of @self. + * + * Returns: (nullable) (transfer none): the child widget of @self + * + * Since: 1.0 + */ +GtkWidget * +adw_toast_overlay_get_child (AdwToastOverlay *self) +{ + g_return_val_if_fail (ADW_IS_TOAST_OVERLAY (self), NULL); + + return self->child; +} + +/** + * adw_toast_overlay_set_child: (attributes org.gtk.Method.set_property=child) + * @self: a `AdwToastOverlay` + * @child: (nullable): the child widget + * + * Sets the child widget of @self. + * + * Since: 1.0 + */ +void +adw_toast_overlay_set_child (AdwToastOverlay *self, + GtkWidget *child) +{ + g_return_if_fail (ADW_IS_TOAST_OVERLAY (self)); + g_return_if_fail (child == NULL || GTK_IS_WIDGET (child)); + + if (self->child == child) + return; + + if (self->child) + gtk_widget_unparent (self->child); + + self->child = child; + + if (self->child) + gtk_widget_insert_after (self->child, GTK_WIDGET (self), NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]); +} + +/** + * adw_toast_overlay_add_toast: + * @self: a `AdwToastOverlay` + * @toast: (transfer full): a toast + * + * Displays @toast. + * + * Only one toast can be shown at a time; if a toast is already being displayed, + * either @toast or the original toast will be placed in a queue, depending on + * the priority of @toast. See [property@Adw.Toast:priority]. + * + * Since: 1.0 + */ +void +adw_toast_overlay_add_toast (AdwToastOverlay *self, + AdwToast *toast) +{ + ToastInfo *info; + + g_return_if_fail (ADW_IS_TOAST_OVERLAY (self)); + g_return_if_fail (ADW_IS_TOAST (toast)); + + if (adw_toast_get_added (toast)) { + g_critical ("Adding toast '%s', but it has already been added to an " + "AdwToastOverlay", adw_toast_get_title (toast)); + + return; + } + + adw_toast_set_added (toast, TRUE); + + info = g_new0 (ToastInfo, 1); + info->overlay = self; + info->toast = toast; + info-> dismissed_id = + g_signal_connect_swapped (info->toast, "dismissed", + G_CALLBACK (dismissed_cb), info); + + if (!self->current_toast) { + show_toast (self, info); + + return; + } + + switch (adw_toast_get_priority (toast)) { + case ADW_TOAST_PRIORITY_NORMAL: + g_queue_push_tail (self->queue, info); + break; + + case ADW_TOAST_PRIORITY_HIGH: + self->current_toast->postponing = TRUE; + g_queue_push_head (self->queue, self->current_toast); + + hide_current_toast (self); + show_toast (self, info); + break; + + default: + g_assert_not_reached (); + } +} diff --git a/src/adw-toast-overlay.h b/src/adw-toast-overlay.h new file mode 100644 index 0000000000000000000000000000000000000000..ca57310c22b78f9f804e01389be7ad3190c58bd2 --- /dev/null +++ b/src/adw-toast-overlay.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 Maximiliano Sandoval + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION) +#error "Only can be included directly." +#endif + +#include "adw-version.h" +#include "adw-toast.h" + +#include + +G_BEGIN_DECLS + +#define ADW_TYPE_TOAST_OVERLAY (adw_toast_overlay_get_type()) + +ADW_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (AdwToastOverlay, adw_toast_overlay, ADW, TOAST_OVERLAY, GtkWidget) + +ADW_AVAILABLE_IN_ALL +GtkWidget *adw_toast_overlay_new (void) G_GNUC_WARN_UNUSED_RESULT; + +ADW_AVAILABLE_IN_ALL +GtkWidget *adw_toast_overlay_get_child (AdwToastOverlay *self); +ADW_AVAILABLE_IN_ALL +void adw_toast_overlay_set_child (AdwToastOverlay *self, + GtkWidget *child); + +ADW_AVAILABLE_IN_ALL +void adw_toast_overlay_add_toast (AdwToastOverlay *self, + AdwToast *toast); + +G_END_DECLS diff --git a/src/adw-toast-private.h b/src/adw-toast-private.h new file mode 100644 index 0000000000000000000000000000000000000000..b9fa39076b4a25c4688aa01266cffd790a6cd0ec --- /dev/null +++ b/src/adw-toast-private.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Author: Alexander Mikhaylenko + */ + +#pragma once + +#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION) +#error "Only can be included directly." +#endif + +#include "adw-toast.h" + +G_BEGIN_DECLS + +gboolean adw_toast_get_added (AdwToast *self); +void adw_toast_set_added (AdwToast *self, + gboolean added); + +G_END_DECLS diff --git a/src/adw-toast-widget-private.h b/src/adw-toast-widget-private.h new file mode 100644 index 0000000000000000000000000000000000000000..ff523f716edfc01d30a92950c57e145270f46f82 --- /dev/null +++ b/src/adw-toast-widget-private.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION) +#error "Only can be included directly." +#endif + +#include "adw-version.h" +#include "adw-toast.h" + +#include + +G_BEGIN_DECLS + +#define ADW_TYPE_TOAST_WIDGET (adw_toast_widget_get_type()) + +G_DECLARE_FINAL_TYPE (AdwToastWidget, adw_toast_widget, ADW, TOAST_WIDGET, GtkWidget) + +GtkWidget *adw_toast_widget_new (AdwToast *toast) G_GNUC_WARN_UNUSED_RESULT; + +G_END_DECLS diff --git a/src/adw-toast-widget.c b/src/adw-toast-widget.c new file mode 100644 index 0000000000000000000000000000000000000000..a9fcb0184c7c68a64dd1d708fd2e2c27e7ef843d --- /dev/null +++ b/src/adw-toast-widget.c @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2021 Maximiliano Sandoval + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" + +#include "adw-toast-widget-private.h" + +#define TOAST_DURATION 5000 + +struct _AdwToastWidget { + GtkWidget parent_instance; + + AdwToast *toast; + + guint hide_timeout_id; + gint inhibit_count; +}; + +enum { + PROP_0, + PROP_TOAST, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +G_DEFINE_TYPE (AdwToastWidget, adw_toast_widget, GTK_TYPE_WIDGET) + +static gboolean +string_is_not_empty (gpointer user_data, + const char *string) +{ + return string && string[0]; +} + +static gboolean +timeout_cb (AdwToastWidget *self) +{ + self->hide_timeout_id = 0; + + adw_toast_dismiss (self->toast); + + return G_SOURCE_REMOVE; +} + +static void +start_timeout (AdwToastWidget *self) +{ + if (!self->hide_timeout_id) + self->hide_timeout_id = + g_timeout_add (TOAST_DURATION, + G_SOURCE_FUNC (timeout_cb), + self); +} + +static void +end_timeout (AdwToastWidget *self) +{ + g_clear_handle_id (&self->hide_timeout_id, g_source_remove); +} + +static void +inhibit_hide (AdwToastWidget *self) +{ + if (self->inhibit_count++ == 0) + end_timeout (self); +} + +static void +uninhibit_hide (AdwToastWidget *self) +{ + g_assert (self->inhibit_count); + + if (--self->inhibit_count == 0) + start_timeout (self); +} + +static void +dismiss (AdwToastWidget *self) +{ + end_timeout (self); + + adw_toast_dismiss (self->toast); +} + +static gboolean +close_idle_cb (AdwToastWidget *self) +{ + dismiss (self); + g_object_unref (self); + + return G_SOURCE_REMOVE; +} + +static void +action_clicked_cb (AdwToastWidget *self) +{ + end_timeout (self); + + /* Keep the widget alive through the idle. Otherwise it may be immediately + * destroyed if animations are disabled */ + g_idle_add (G_SOURCE_FUNC (close_idle_cb), g_object_ref (self)); +} + +static void +adw_toast_widget_dispose (GObject *object) +{ + AdwToastWidget *self = ADW_TOAST_WIDGET (object); + GtkWidget *child; + + end_timeout (self); + + 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); +} + +static void +adw_toast_widget_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + AdwToastWidget *self = ADW_TOAST_WIDGET (object); + + switch (prop_id) { + case PROP_TOAST: + g_value_set_object (value, self->toast); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +adw_toast_widget_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + AdwToastWidget *self = ADW_TOAST_WIDGET (object); + + switch (prop_id) { + case PROP_TOAST: + g_set_object (&self->toast, g_value_get_object (value)); + end_timeout (self); + start_timeout (self); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +adw_toast_widget_class_init (AdwToastWidgetClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = adw_toast_widget_dispose; + object_class->get_property = adw_toast_widget_get_property; + object_class->set_property = adw_toast_widget_set_property; + + props[PROP_TOAST] = + g_param_spec_object ("toast", + "Toast", + "The displayed toast", + ADW_TYPE_TOAST, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/Adwaita/ui/adw-toast-widget.ui"); + + 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); + gtk_widget_class_bind_template_callback (widget_class, inhibit_hide); + gtk_widget_class_bind_template_callback (widget_class, uninhibit_hide); + + gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT); + gtk_widget_class_set_css_name (widget_class, "toast"); +} + +static void +adw_toast_widget_init (AdwToastWidget *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget * +adw_toast_widget_new (AdwToast *toast) +{ + g_assert (ADW_IS_TOAST (toast)); + + return g_object_new (ADW_TYPE_TOAST_WIDGET, + "toast", toast, + NULL); +} diff --git a/src/adw-toast-widget.ui b/src/adw-toast-widget.ui new file mode 100644 index 0000000000000000000000000000000000000000..bb69ba3b69ac0f99b323a8d5fea8a695c394e918 --- /dev/null +++ b/src/adw-toast-widget.ui @@ -0,0 +1,80 @@ + + + + + diff --git a/src/adw-toast.c b/src/adw-toast.c new file mode 100644 index 0000000000000000000000000000000000000000..f8f3276aec333fe6ad4ae673724212f0a6d20b68 --- /dev/null +++ b/src/adw-toast.c @@ -0,0 +1,725 @@ +/* + * Copyright (C) 2021 Maximiliano Sandoval + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" + +#include "adw-toast-private.h" + +/** + * AdwToastPriority: + * @ADW_TOAST_PRIORITY_NORMAL: the toast will be queued if another toast is + * already displayed. + * @ADW_TOAST_PRIORITY_HIGH: the toast will be displayed immediately, pushing + * the previous toast into the queue instead. + * + * [class@Adw.Toast] behavior when another toast is already displayed. + * + * Since: 1.0 + */ + +/** + * AdwToast: + * + * A helper object for [class@Adw.ToastOverlay]. + * + * Toasts are meant to be passed into [method@Adw.ToastOverlay.add_toast] as + * follows: + * + * ```c + * adw_toast_overlay_add_toast (overlay, adw_toast_new (_("Simple Toast")); + * ``` + * + * Toasts always have a close button and a timeout. In both cases, they emit the + * [signal@Adw.Toast::dismissed] signal when disappearing. + * + * [property@Adw.Toast:priority] determines how the toast behaves if another + * toast is already being displayed. + * + * ## Actions + * + * Toasts can have one button on them, with a label and an attached + * [iface@Gio.Action]. + * + * ```c + * AdwToast *toast = adw_toast_new (_("Toast with Action")); + * + * adw_toast_set_button_label (toast, _("Example")); + * adw_toast_set_action_name (toast, "win.example"); + * + * adw_toast_overlay_add_toast (overlay, toast); + * ``` + * + * ## Modifying toasts + * + * Toasts can be modified after they have been shown. For this, an `AdwToast` + * reference must be kept around while the toast is visible. + * + * A common use case for this is using toasts as undo prompts that stack with + * each other, allowing to batch undo the last deleted items: + * + * ```c + * + * static void + * toast_undo_cb (GtkWidget *sender, + * const char *action, + * GVariant *param) + * { + * // Undo the deletion + * } + * + * static void + * dismissed_cb (MyWindow *self) + * { + * self->undo_toast = NULL; + * + * // Permanently delete the items + * } + * + * static void + * delete_item (MyWindow *self, + * MyItem *item) + * { + * g_autofree char *title = NULL; + * int n_items; + * + * // Mark the item as waiting for deletion + * n_items = ... // The number of waiting items + * + * if (!self->undo_toast) { + * title = g_strdup_printf (_("‘%s’ deleted"), ...); + * + * self->undo_toast = adw_toast_new (title); + * + * adw_toast_set_priority (self->undo_toast, ADW_TOAST_PRIORITY_HIGH); + * adw_toast_set_button_label (self->undo_toast, _("Undo")); + * adw_toast_set_action_name (self->undo_toast, "toast.undo"); + * + * g_signal_connect_swapped (self->undo_toast, "dismissed", + * G_CALLBACK (dismissed_cb), self); + * + * adw_toast_overlay_add_toast (self->toast_overlay, self->undo_toast); + * + * return; + * } + * + * title = + * g_strdup_printf (ngettext ("%d item deleted", + * "%d items deleted", + * n_items), n_items); + * + * adw_toast_set_title (self->undo_toast, title); + * } + * + * static void + * my_window_class_init (MyWindowClass *klass) + * { + * GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + * + * gtk_widget_class_install_action (widget_class, "toast.undo", NULL, toast_undo_cb); + * } + * ``` + * + * Since: 1.0 + */ + +struct _AdwToast { + GObject parent_instance; + + char *title; + char *button_label; + char *action_name; + GVariant *action_target; + AdwToastPriority priority; + + gboolean added; +}; + +enum { + PROP_0, + PROP_TITLE, + PROP_BUTTON_LABEL, + PROP_ACTION_NAME, + PROP_ACTION_TARGET, + PROP_PRIORITY, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_DISMISSED, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +G_DEFINE_TYPE (AdwToast, adw_toast, G_TYPE_OBJECT) + +static void +dismissed_cb (AdwToast *self) +{ + self->added = FALSE; +} + +static void +adw_toast_finalize (GObject *object) +{ + AdwToast *self = ADW_TOAST (object); + + g_clear_pointer (&self->title, g_free); + g_clear_pointer (&self->button_label, g_free); + g_clear_pointer (&self->action_name, g_free); + g_clear_pointer (&self->action_target, g_variant_unref); + + G_OBJECT_CLASS (adw_toast_parent_class)->finalize (object); +} + +static void +adw_toast_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + AdwToast *self = ADW_TOAST (object); + + switch (prop_id) { + case PROP_TITLE: + g_value_set_string (value, adw_toast_get_title (self)); + break; + case PROP_BUTTON_LABEL: + g_value_set_string (value, adw_toast_get_button_label (self)); + break; + case PROP_ACTION_NAME: + g_value_set_string (value, adw_toast_get_action_name (self)); + break; + case PROP_ACTION_TARGET: + g_value_set_variant (value, adw_toast_get_action_target_value (self)); + break; + case PROP_PRIORITY: + g_value_set_enum (value, adw_toast_get_priority (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +adw_toast_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + AdwToast *self = ADW_TOAST (object); + + switch (prop_id) { + case PROP_TITLE: + adw_toast_set_title (self, g_value_get_string (value)); + break; + case PROP_BUTTON_LABEL: + adw_toast_set_button_label (self, g_value_get_string (value)); + break; + case PROP_ACTION_NAME: + adw_toast_set_action_name (self, g_value_get_string (value)); + break; + case PROP_ACTION_TARGET: + adw_toast_set_action_target_value (self, g_value_get_variant (value)); + break; + case PROP_PRIORITY: + adw_toast_set_priority (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +adw_toast_class_init (AdwToastClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = adw_toast_finalize; + object_class->get_property = adw_toast_get_property; + object_class->set_property = adw_toast_set_property; + + /** + * AdwToast:title: (attributes org.gtk.Property.get=adw_toast_get_title org.gtk.Property.set=adw_toast_set_title) + * + * The title of the toast. + * + * The title can be marked up with the Pango text markup language. + * + * Since: 1.0 + */ + props[PROP_TITLE] = + g_param_spec_string ("title", + "Title", + "The title of the toast", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwToast:button-label: (attributes org.gtk.Property.get=adw_toast_get_button_label org.gtk.Property.set=adw_toast_set_button_label) + * + * The label to show on the button. + * + * If set to `NULL`, the button won't be shown. + * + * See [property@Adw.Toast:action-name]. + * + * Since: 1.0 + */ + props[PROP_BUTTON_LABEL] = + g_param_spec_string ("button-label", + "Button Label", + "The label to show on the button", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwToast:action-name: (attributes org.gtk.Property.get=adw_toast_get_action_name org.gtk.Property.set=adw_toast_set_action_name) + * + * The name of the associated action. + * + * It will be activated when clicking the button. + * + * See [property@Adw.Toast:action-target]. + * + * Since: 1.0 + */ + props[PROP_ACTION_NAME] = + g_param_spec_string ("action-name", + "Action Name", + "The name of the associated action", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwToast:action-target: (attributes org.gtk.Property.get=adw_toast_get_action_target_value org.gtk.Property.set=adw_toast_set_action_target_value) + * + * The parameter for action invocations. + * + * See [property@Adw.Toast:action-name]. + * + * Since: 1.0 + */ + props[PROP_ACTION_TARGET] = + g_param_spec_variant ("action-target", + "Action Target Value", + "The parameter for action invocations", + G_VARIANT_TYPE_ANY, + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * AdwToast:priority: (attributes org.gtk.Property.get=adw_toast_get_priority org.gtk.Property.set=adw_toast_set_priority) + * + * The priority of the toast. + * + * Priority controls how the toast behaves when another toast is already + * being displayed. + * + * If the priority is `ADW_TOAST_PRIORITY_NORMAL`, the toast will be queued. + * + * If the priority is `ADW_TOAST_PRIORITY_HIGH`, the toast will be displayed + * immediately, pushing the previous toast into the queue instead. + * + * Since: 1.0 + */ + props[PROP_PRIORITY] = + g_param_spec_enum ("priority", + "Priority", + "The priority of the toast", + ADW_TYPE_TOAST_PRIORITY, + ADW_TOAST_PRIORITY_NORMAL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * AdwToast::dismissed: + * + * Emitted when the toast has been dismissed. + * + * Since: 1.0 + */ + signals[SIGNAL_DISMISSED] = + g_signal_new ("dismissed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_FIRST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +adw_toast_init (AdwToast *self) +{ + self->title = g_strdup (""); + self->priority = ADW_TOAST_PRIORITY_NORMAL; + + g_signal_connect (self, "dismissed", G_CALLBACK (dismissed_cb), self); +} + +/** + * adw_toast_new: + * @title: the title to be displayed + * + * Creates a new `AdwToast`. + * + * The toast will use @title as its title. + * + * @title can be marked up with the Pango text markup language. + * + * Returns: the new created `AdwToast` + * + * Since: 1.0 + */ +AdwToast * +adw_toast_new (const char *title) +{ + g_return_val_if_fail (title != NULL, NULL); + + return g_object_new (ADW_TYPE_TOAST, + "title", title, + NULL); +} + +/** + * adw_toast_get_title: (attributes org.gtk.Method.get_property=title) + * @self: a `AdwToast` + * + * Gets the title that will be displayed on the toast. + * + * Returns: the title + * + * Since: 1.0 + */ +const char * +adw_toast_get_title (AdwToast *self) +{ + g_return_val_if_fail (ADW_IS_TOAST (self), NULL); + + return self->title; +} + +/** + * adw_toast_set_title: (attributes org.gtk.Method.set_property=title) + * @self: a `AdwToast` + * @title: a title + * + * Sets the title that will be displayed on the toast. + * + * Since: 1.0 + */ +void +adw_toast_set_title (AdwToast *self, + const char *title) +{ + g_return_if_fail (ADW_IS_TOAST (self)); + g_return_if_fail (title != NULL); + + if (!g_strcmp0 (self->title, title)) + return; + + g_clear_pointer (&self->title, g_free); + self->title = g_strdup (title); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + +/** + * adw_toast_get_button_label: (attributes org.gtk.Method.get_property=button-label) + * @self: a `AdwToast` + * + * Gets the label to show on the button. + * + * Returns: (nullable): the button label + * + * Since: 1.0 + */ +const char * +adw_toast_get_button_label (AdwToast *self) +{ + g_return_val_if_fail (ADW_IS_TOAST (self), NULL); + + return self->button_label; +} + +/** + * adw_toast_set_button_label: (attributes org.gtk.Method.set_property=button-label) + * @self: a `AdwToast` + * @button_label: (nullable): a button label + * + * Sets the label to show on the button. + * + * It set to `NULL`, the button won't be shown. + * + * Since: 1.0 + */ +void +adw_toast_set_button_label (AdwToast *self, + const char *button_label) +{ + g_return_if_fail (ADW_IS_TOAST (self)); + + if (!g_strcmp0 (self->button_label, button_label)) + return; + + g_clear_pointer (&self->button_label, g_free); + self->button_label = g_strdup (button_label); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUTTON_LABEL]); +} + +/** + * adw_toast_get_action_name: (attributes org.gtk.Method.get_property=action-name) + * @self: a `AdwToast` + * + * Gets the name of the associated action. + * + * Returns: (nullable): the action name + * + * Since: 1.0 + */ +const char * +adw_toast_get_action_name (AdwToast *self) +{ + g_return_val_if_fail (ADW_IS_TOAST (self), NULL); + + return self->action_name; +} + +/** + * adw_toast_set_action_name: (attributes org.gtk.Method.set_property=action-name) + * @self: a `AdwToast` + * @action_name: (nullable): the action name + * + * Sets the name of the associated action. + * + * Since: 1.0 + */ +void +adw_toast_set_action_name (AdwToast *self, + const char *action_name) +{ + g_return_if_fail (ADW_IS_TOAST (self)); + + if (!g_strcmp0 (self->action_name, action_name)) + return; + + g_clear_pointer (&self->action_name, g_free); + self->action_name = g_strdup (action_name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTION_NAME]); +} + +/** + * adw_toast_get_action_target_value: (attributes org.gtk.Method.get_property=action-target) + * @self: a `AdwToast` + * + * Gets the parameter for action invocations. + * + * Returns: (transfer none) (nullable): the action target + * + * Since: 1.0 + */ +GVariant * +adw_toast_get_action_target_value (AdwToast *self) +{ + g_return_val_if_fail (ADW_IS_TOAST (self), NULL); + + return self->action_target; +} + +/** + * adw_toast_set_action_target_value: (attributes org.gtk.Method.get_property=action-target) + * @self: a `AdwToast` + * @action_target: (nullable): the action target + * + * Sets the parameter for action invocations. + * + * Since: 1.0 + */ +void +adw_toast_set_action_target_value (AdwToast *self, + GVariant *action_target) +{ + g_return_if_fail (ADW_IS_TOAST (self)); + + if (action_target == self->action_target) + return; + + if (action_target && self->action_target && + g_variant_equal (action_target, self->action_target)) + return; + + g_clear_pointer (&self->action_target, g_variant_unref); + self->action_target = g_variant_ref (action_target); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTION_TARGET]); +} + +/** + * adw_toast_set_action_target: (skip) + * @self: a `AdwToast` + * @format_string: (nullable): a [struct@GLib.Variant] format string + * @...: arguments appropriate for @target_format + * + * Sets the parameter for action invocations. + * + * This is a convenience function that calls [ctor@GLib.Variant.new] for + * @format_string and uses the result to call + * [method@Adw.Toast.set_action_target_value]. + * + * If you are setting a string-valued target and want to set + * the action name at the same time, you can use + * [method@Adw.Toast.set_detailed_action_name]. + + * Since: 1.0 + */ +void +adw_toast_set_action_target (AdwToast *self, + const char *format_string, + ...) +{ + va_list args; + + va_start (args, format_string); + adw_toast_set_action_target_value (self, + g_variant_new_va (format_string, + NULL, &args)); + va_end (args); +} + +/** + * adw_toast_set_detailed_action_name: + * @self: a `AdwToast` + * @detailed_action_name: (nullable): the detailed action name + * + * Sets the action name and its parameter. + * + * @detailed_action_name is a string in the format accepted by + * [func@Gio.Action.parse_detailed_name]. + * + * Since: 1.0 + */ +void +adw_toast_set_detailed_action_name (AdwToast *self, + const char *detailed_action_name) +{ + g_autofree char *name = NULL; + g_autoptr (GVariant) target = NULL; + g_autoptr (GError) error = NULL; + + g_return_if_fail (ADW_IS_TOAST (self)); + + if (!detailed_action_name) { + adw_toast_set_action_name (self, NULL); + adw_toast_set_action_target_value (self, NULL); + + return; + } + + if (!g_action_parse_detailed_name (detailed_action_name, &name, &target, &error)) { + g_critical ("Couldn't parse detailed action name: %s", error->message); + + return; + } + + adw_toast_set_action_name (self, name); + adw_toast_set_action_target_value (self, target); +} + +/** + * adw_toast_get_priority: (attributes org.gtk.Method.get_property=priority) + * @self: a `AdwToast` + * + * Gets the toast priority. + * + * Returns: the priority + * + * Since: 1.0 + */ +AdwToastPriority +adw_toast_get_priority (AdwToast *self) +{ + g_return_val_if_fail (ADW_IS_TOAST (self), ADW_TOAST_PRIORITY_NORMAL); + + return self->priority; +} + +/** + * adw_toast_set_priority: (attributes org.gtk.Method.set_property=priority) + * @self: a `AdwToast` + * @priority: the priority + * + * Sets the toast priority. + * + * Priority controls how the toast behaves when another toast is already + * being displayed. + * + * If @priority is `ADW_TOAST_PRIORITY_NORMAL`, the toast will be queued. + * + * If @priority is `ADW_TOAST_PRIORITY_HIGH`, the toast will be displayed immediately, + * pushing the previous toast into the queue instead. + * + * Since: 1.0 + */ +void +adw_toast_set_priority (AdwToast *self, + AdwToastPriority priority) +{ + g_return_if_fail (ADW_IS_TOAST (self)); + g_return_if_fail (priority >= ADW_TOAST_PRIORITY_NORMAL && + priority <= ADW_TOAST_PRIORITY_HIGH); + + if (self->priority == priority) + return; + + self->priority = priority; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRIORITY]); +} + +/** + * adw_toast_dismiss: + * @self: a `AdwToast` + * + * Dismisses @self. + * + * Since: 1.0 + */ +void +adw_toast_dismiss (AdwToast *self) +{ + g_return_if_fail (ADW_IS_TOAST (self)); + + if (!self->added) { + g_critical ("Trying to dismiss the toast '%s', but it isn't in an " + "AdwToastOverlay yet", self->title); + + return; + } + + g_signal_emit (self, signals[SIGNAL_DISMISSED], 0, NULL); +} + +gboolean +adw_toast_get_added (AdwToast *self) +{ + g_return_val_if_fail (ADW_IS_TOAST (self), FALSE); + + return self->added; +} + +void +adw_toast_set_added (AdwToast *self, + gboolean added) +{ + g_return_if_fail (ADW_IS_TOAST (self)); + + self->added = !!added; +} diff --git a/src/adw-toast.h b/src/adw-toast.h new file mode 100644 index 0000000000000000000000000000000000000000..4111a088511a60e3e0eb93394e415cd13e5f37bb --- /dev/null +++ b/src/adw-toast.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2021 Maximiliano Sandoval + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION) +#error "Only can be included directly." +#endif + +#include "adw-version.h" + +#include +#include "adw-enums.h" + +G_BEGIN_DECLS + +typedef enum { + ADW_TOAST_PRIORITY_NORMAL, + ADW_TOAST_PRIORITY_HIGH, +} AdwToastPriority; + +#define ADW_TYPE_TOAST (adw_toast_get_type()) + +ADW_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (AdwToast, adw_toast, ADW, TOAST, GObject) + +ADW_AVAILABLE_IN_ALL +AdwToast *adw_toast_new (const char *title) G_GNUC_WARN_UNUSED_RESULT; + +ADW_AVAILABLE_IN_ALL +const char *adw_toast_get_title (AdwToast *self); +ADW_AVAILABLE_IN_ALL +void adw_toast_set_title (AdwToast *self, + const char *title); + +ADW_AVAILABLE_IN_ALL +const char *adw_toast_get_button_label (AdwToast *self); +ADW_AVAILABLE_IN_ALL +void adw_toast_set_button_label (AdwToast *self, + const char *button_label); + +ADW_AVAILABLE_IN_ALL +const char *adw_toast_get_action_name (AdwToast *self); +ADW_AVAILABLE_IN_ALL +void adw_toast_set_action_name (AdwToast *self, + const char *action_name); + +ADW_AVAILABLE_IN_ALL +GVariant *adw_toast_get_action_target_value (AdwToast *self); +ADW_AVAILABLE_IN_ALL +void adw_toast_set_action_target_value (AdwToast *self, + GVariant *action_target); +ADW_AVAILABLE_IN_ALL +void adw_toast_set_action_target (AdwToast *self, + const char *format_string, + ...); +ADW_AVAILABLE_IN_ALL +void adw_toast_set_detailed_action_name (AdwToast *self, + const char *detailed_action_name); + +ADW_AVAILABLE_IN_ALL +AdwToastPriority adw_toast_get_priority (AdwToast *self); +ADW_AVAILABLE_IN_ALL +void adw_toast_set_priority (AdwToast *self, + AdwToastPriority priority); + +ADW_AVAILABLE_IN_ALL +void adw_toast_dismiss (AdwToast *self); + +G_END_DECLS diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml index 69f5f48368f5403ed3cb6ba6951bcf569bda37ed..ea354366c34bae7deec9a1499d05f37e3abd9480 100644 --- a/src/adwaita.gresources.xml +++ b/src/adwaita.gresources.xml @@ -17,6 +17,7 @@ adw-status-page.ui adw-tab.ui adw-tab-bar.ui + adw-toast-widget.ui adw-view-switcher-bar.ui adw-view-switcher-button.ui adw-view-switcher-title.ui diff --git a/src/adwaita.h b/src/adwaita.h index 0fca7f8d727e8837c1ad9f4fd0fed9e8c528162b..c5aa01898997e820f2742dc4adbb9c9d8e9ec0f1 100644 --- a/src/adwaita.h +++ b/src/adwaita.h @@ -56,6 +56,8 @@ G_BEGIN_DECLS #include "adw-swipeable.h" #include "adw-tab-bar.h" #include "adw-tab-view.h" +#include "adw-toast-overlay.h" +#include "adw-toast.h" #include "adw-view-stack.h" #include "adw-view-switcher.h" #include "adw-view-switcher-bar.h" diff --git a/src/meson.build b/src/meson.build index 6d3b5e7a61d143ca3303d4859243c1bdeb1270ac..35f3d905434c333960026200b8a46def2fa51b0c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -17,7 +17,7 @@ adw_public_enum_headers = [ 'adw-navigation-direction.h', 'adw-style-manager.h', 'adw-squeezer.h', - 'adw-tab-bar.h', + 'adw-toast.h', 'adw-view-switcher.h', ] @@ -105,6 +105,8 @@ src_headers = [ 'adw-swipeable.h', 'adw-tab-bar.h', 'adw-tab-view.h', + 'adw-toast.h', + 'adw-toast-overlay.h', 'adw-view-stack.h', 'adw-view-switcher.h', 'adw-view-switcher-bar.h', @@ -168,6 +170,9 @@ src_sources = [ 'adw-tab-bar.c', 'adw-tab-box.c', 'adw-tab-view.c', + 'adw-toast.c', + 'adw-toast-overlay.c', + 'adw-toast-widget.c', 'adw-version.c', 'adw-view-stack.c', 'adw-view-switcher.c', diff --git a/src/stylesheet/widgets/_misc.scss b/src/stylesheet/widgets/_misc.scss index fbf5fdd6ac829764e91da79db921b87ff26dd4e1..51f10bb083f5e6a0e7bc21cbf11cd67e3534b2f8 100644 --- a/src/stylesheet/widgets/_misc.scss +++ b/src/stylesheet/widgets/_misc.scss @@ -49,6 +49,32 @@ separator { border { border: none; } } +/********** + * Toasts * + **********/ + +toast { + @extend %osd; + + margin: 12px; + margin-bottom: 24px; + + border-radius: 150px; + border-spacing: 6px; + padding: 6px; + + &:dir(ltr) { padding-left: 12px; } + &:dir(rtl) { padding-right: 12px; } + + > label { + margin: 0 6px; + } + + @if $contrast == 'high' { + box-shadow: 0 0 0 1px $border_color; + } +} + /************** * GtkVideo * **************/ diff --git a/tests/meson.build b/tests/meson.build index 83c40b69f863224983a6ad9dcc6466aabeeb0e17..2c9ffc4fc7c484aa55dd86b9e58ad0f1e7a702c8 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -43,6 +43,8 @@ test_names = [ 'test-style-manager', 'test-tab-bar', 'test-tab-view', + 'test-toast', + 'test-toast-overlay', 'test-view-switcher', 'test-view-switcher-bar', 'test-window', diff --git a/tests/test-toast-overlay.c b/tests/test-toast-overlay.c new file mode 100644 index 0000000000000000000000000000000000000000..1ea5c5fc3a5537c8cc2923e23cf482cdcde64beb --- /dev/null +++ b/tests/test-toast-overlay.c @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 Maximiliano Sandoval + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include + +int notified; + +static void +notify_cb (GtkWidget *widget, gpointer data) +{ + notified++; +} + +static void +test_adw_toast_overlay_child (void) +{ + AdwToastOverlay *toast_overlay = g_object_ref_sink (ADW_TOAST_OVERLAY (adw_toast_overlay_new ())); + GtkWidget *widget = NULL; + + g_assert_nonnull (toast_overlay); + + notified = 0; + g_signal_connect (toast_overlay, "notify::child", G_CALLBACK (notify_cb), NULL); + + g_object_get (toast_overlay, "child", &widget, NULL); + g_assert_null (widget); + + adw_toast_overlay_set_child (toast_overlay, NULL); + g_assert_cmpint (notified, ==, 0); + + widget = gtk_button_new (); + adw_toast_overlay_set_child (toast_overlay, widget); + g_assert_true (adw_toast_overlay_get_child (toast_overlay) == widget); + g_assert_cmpint (notified, ==, 1); + + g_object_set (toast_overlay, "child", NULL, NULL); + g_assert_null (adw_toast_overlay_get_child (toast_overlay)); + g_assert_cmpint (notified, ==, 2); + + g_assert_finalize_object (toast_overlay); +} + +static void +test_adw_toast_overlay_add_toast (void) +{ + AdwToastOverlay *toast_overlay = g_object_ref_sink (ADW_TOAST_OVERLAY (adw_toast_overlay_new ())); + AdwToast *toast = adw_toast_new ("Test notification"); + + g_assert_nonnull (toast_overlay); + g_assert_nonnull (toast); + + adw_toast_overlay_add_toast (toast_overlay, g_object_ref (toast)); + + g_assert_finalize_object (toast_overlay); + g_assert_finalize_object (toast); +} + +int +main (int argc, + char *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + adw_init (); + + g_test_add_func ("/Adwaita/ToastOverlay/child", test_adw_toast_overlay_child); + g_test_add_func ("/Adwaita/ToastOverlay/add_toast", test_adw_toast_overlay_add_toast); + + return g_test_run (); +} diff --git a/tests/test-toast.c b/tests/test-toast.c new file mode 100644 index 0000000000000000000000000000000000000000..0ce6364b90ced23fa047bc90a15b263f0b903170 --- /dev/null +++ b/tests/test-toast.c @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Author: Alexander Mikhaylenko + */ + +#include + +int notified; + +static void +notify_cb (GtkWidget *widget, gpointer data) +{ + notified++; +} + +static void +test_adw_toast_title (void) +{ + AdwToast *toast = adw_toast_new ("Title"); + g_autofree char *title = NULL; + + g_assert_nonnull (toast); + + notified = 0; + g_signal_connect (toast, "notify::title", G_CALLBACK (notify_cb), NULL); + + g_object_get (toast, "title", &title, NULL); + g_assert_cmpstr (title, ==, "Title"); + + adw_toast_set_title (toast, "Another title"); + g_assert_cmpstr (adw_toast_get_title (toast), ==, "Another title"); + g_assert_cmpint (notified, ==, 1); + + g_object_set (toast, "title", "Title", NULL); + g_assert_cmpstr (adw_toast_get_title (toast), ==, "Title"); + g_assert_cmpint (notified, ==, 2); + + g_assert_finalize_object (toast); +} + +static void +test_adw_toast_button_label (void) +{ + AdwToast *toast = adw_toast_new ("Title"); + char *button_label; + + g_assert_nonnull (toast); + + notified = 0; + g_signal_connect (toast, "notify::button-label", G_CALLBACK (notify_cb), NULL); + + g_object_get (toast, "button-label", &button_label, NULL); + g_assert_null (button_label); + + adw_toast_set_button_label (toast, "Button"); + g_assert_cmpstr (adw_toast_get_button_label (toast), ==, "Button"); + g_assert_cmpint (notified, ==, 1); + + g_object_set (toast, "button-label", "Button 2", NULL); + g_assert_cmpstr (adw_toast_get_button_label (toast), ==, "Button 2"); + g_assert_cmpint (notified, ==, 2); + + g_assert_finalize_object (toast); +} + +static void +test_adw_toast_action_name (void) +{ + AdwToast *toast = adw_toast_new ("Title"); + char *action_name; + + g_assert_nonnull (toast); + + notified = 0; + g_signal_connect (toast, "notify::action-name", G_CALLBACK (notify_cb), NULL); + + g_object_get (toast, "action-name", &action_name, NULL); + g_assert_null (action_name); + + adw_toast_set_action_name (toast, "win.something"); + g_assert_cmpstr (adw_toast_get_action_name (toast), ==, "win.something"); + g_assert_cmpint (notified, ==, 1); + + g_object_set (toast, "action-name", "win.something-else", NULL); + g_assert_cmpstr (adw_toast_get_action_name (toast), ==, "win.something-else"); + g_assert_cmpint (notified, ==, 2); + + g_assert_finalize_object (toast); +} + +static void +test_adw_toast_action_target (void) +{ + AdwToast *toast = adw_toast_new ("Title"); + GVariant *action_target; + g_autoptr (GVariant) variant1 = g_variant_ref_sink (g_variant_new_int32 (1)); + g_autoptr (GVariant) variant2 = g_variant_ref_sink (g_variant_new_int32 (2)); + g_autoptr (GVariant) variant3 = g_variant_ref_sink (g_variant_new_int32 (3)); + + g_assert_nonnull (toast); + + notified = 0; + g_signal_connect (toast, "notify::action-target", G_CALLBACK (notify_cb), NULL); + + g_object_get (toast, "action-target", &action_target, NULL); + g_assert_null (action_target); + + adw_toast_set_action_target_value (toast, g_variant_new_int32 (1)); + g_assert_cmpvariant (adw_toast_get_action_target_value (toast), variant1); + g_assert_cmpint (notified, ==, 1); + + g_object_set (toast, "action-target", g_variant_new_int32 (2), NULL); + g_assert_cmpvariant (adw_toast_get_action_target_value (toast), variant2); + g_assert_cmpint (notified, ==, 2); + + adw_toast_set_action_target (toast, "i", 3); + g_assert_cmpvariant (adw_toast_get_action_target_value (toast), variant3); + g_assert_cmpint (notified, ==, 3); + + g_assert_finalize_object (toast); +} + +static void +test_adw_toast_detailed_action_name (void) +{ + AdwToast *toast = adw_toast_new ("Title"); + g_autoptr (GVariant) variant = g_variant_ref_sink (g_variant_new_int32 (2)); + + g_assert_nonnull (toast); + + g_assert_null (adw_toast_get_action_name (toast)); + g_assert_null (adw_toast_get_action_target_value (toast)); + + adw_toast_set_detailed_action_name (toast, "win.something"); + g_assert_cmpstr (adw_toast_get_action_name (toast), ==, "win.something"); + g_assert_null (adw_toast_get_action_target_value (toast)); + + adw_toast_set_detailed_action_name (toast, "win.something(2)"); + g_assert_cmpstr (adw_toast_get_action_name (toast), ==, "win.something"); + g_assert_cmpvariant (adw_toast_get_action_target_value (toast), variant); + + g_assert_finalize_object (toast); +} + +static void +test_adw_toast_priority (void) +{ + AdwToast *toast = adw_toast_new ("Title"); + AdwToastPriority priority; + + g_assert_nonnull (toast); + + notified = 0; + g_signal_connect (toast, "notify::priority", G_CALLBACK (notify_cb), NULL); + + g_object_get (toast, "priority", &priority, NULL); + g_assert_cmpint (priority, ==, ADW_TOAST_PRIORITY_NORMAL); + + adw_toast_set_priority (toast, ADW_TOAST_PRIORITY_HIGH); + g_assert_cmpint (adw_toast_get_priority (toast), ==, ADW_TOAST_PRIORITY_HIGH); + g_assert_cmpint (notified, ==, 1); + + g_object_set (toast, "priority", ADW_TOAST_PRIORITY_NORMAL, NULL); + g_assert_cmpint (adw_toast_get_priority (toast), ==, ADW_TOAST_PRIORITY_NORMAL); + g_assert_cmpint (notified, ==, 2); + + g_assert_finalize_object (toast); +} + +static void +test_adw_toast_dismiss (void) +{ + AdwToast *toast = adw_toast_new ("Title"); + AdwToastOverlay *overlay = g_object_ref_sink (ADW_TOAST_OVERLAY (adw_toast_overlay_new ())); + + g_assert_nonnull (overlay); + g_assert_nonnull (toast); + + adw_toast_overlay_add_toast (overlay, g_object_ref (toast)); + adw_toast_dismiss (toast); + + g_assert_finalize_object (overlay); + g_assert_finalize_object (toast); +} + +int +main (int argc, + char *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + adw_init (); + + g_test_add_func ("/Adwaita/Toast/title", test_adw_toast_title); + g_test_add_func ("/Adwaita/Toast/button_label", test_adw_toast_button_label); + g_test_add_func ("/Adwaita/Toast/action_name", test_adw_toast_action_name); + g_test_add_func ("/Adwaita/Toast/action_target", test_adw_toast_action_target); + g_test_add_func ("/Adwaita/Toast/detailed_action_name", test_adw_toast_detailed_action_name); + g_test_add_func ("/Adwaita/Toast/priority", test_adw_toast_priority); + g_test_add_func ("/Adwaita/Toast/dismiss", test_adw_toast_dismiss); + + return g_test_run (); +}