[libadwaita/wip/exalm/message-dialog: 2/2] Add AdwMessageDialog
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/wip/exalm/message-dialog: 2/2] Add AdwMessageDialog
- Date: Mon, 27 Jun 2022 18:29:38 +0000 (UTC)
commit e42add2ea0bf33bbb993227670f36b2f3bd04bd0
Author: Alexander Mikhaylenko <alexm gnome org>
Date: Fri Jun 24 19:59:41 2022 +0400
Add AdwMessageDialog
Add a demo and a test, update docs.
demo/adw-demo-window.c | 2 +
demo/adw-demo-window.ui | 10 +
demo/adwaita-demo.gresources.xml | 2 +
.../scalable/actions/widget-dialog-symbolic.svg | 2 +
demo/meson.build | 1 +
demo/pages/dialogs/adw-demo-page-dialogs.c | 78 +
demo/pages/dialogs/adw-demo-page-dialogs.h | 11 +
demo/pages/dialogs/adw-demo-page-dialogs.ui | 24 +
doc/images/message-dialog-appearance-dark.png | Bin 0 -> 17434 bytes
doc/images/message-dialog-appearance.png | Bin 0 -> 17264 bytes
doc/images/message-dialog-dark.png | Bin 0 -> 14711 bytes
doc/images/message-dialog.png | Bin 0 -> 14581 bytes
doc/libadwaita.toml.in | 4 +
doc/named-colors.md | 30 +
doc/tools/data/message-dialog-appearance.ui | 16 +
doc/tools/data/message-dialog.ui | 15 +
doc/visual-index.md | 9 +
src/adw-gtkbuilder-utils-private.h | 50 +
src/adw-gtkbuilder-utils.c | 138 ++
src/adw-message-dialog.c | 2072 ++++++++++++++++++++
src/adw-message-dialog.h | 149 ++
src/adw-message-dialog.ui | 82 +
src/adwaita.gresources.xml | 1 +
src/adwaita.h | 1 +
src/meson.build | 4 +
src/stylesheet/_colors.scss | 3 +
src/stylesheet/_defaults.scss | 4 +
src/stylesheet/widgets/_message-dialog.scss | 79 +
tests/meson.build | 1 +
tests/test-message-dialog.c | 396 ++++
30 files changed, 3184 insertions(+)
---
diff --git a/demo/adw-demo-window.c b/demo/adw-demo-window.c
index befb473e..b15faf9c 100644
--- a/demo/adw-demo-window.c
+++ b/demo/adw-demo-window.c
@@ -7,6 +7,7 @@
#include "pages/buttons/adw-demo-page-buttons.h"
#include "pages/carousel/adw-demo-page-carousel.h"
#include "pages/clamp/adw-demo-page-clamp.h"
+#include "pages/dialogs/adw-demo-page-dialogs.h"
#include "pages/flap/adw-demo-page-flap.h"
#include "pages/leaflet/adw-demo-page-leaflet.h"
#include "pages/lists/adw-demo-page-lists.h"
@@ -120,6 +121,7 @@ adw_demo_window_init (AdwDemoWindow *self)
g_type_ensure (ADW_TYPE_DEMO_PAGE_BUTTONS);
g_type_ensure (ADW_TYPE_DEMO_PAGE_CAROUSEL);
g_type_ensure (ADW_TYPE_DEMO_PAGE_CLAMP);
+ g_type_ensure (ADW_TYPE_DEMO_PAGE_DIALOGS);
g_type_ensure (ADW_TYPE_DEMO_PAGE_FLAP);
g_type_ensure (ADW_TYPE_DEMO_PAGE_LEAFLET);
g_type_ensure (ADW_TYPE_DEMO_PAGE_LISTS);
diff --git a/demo/adw-demo-window.ui b/demo/adw-demo-window.ui
index 4ce61d67..7f3799bf 100644
--- a/demo/adw-demo-window.ui
+++ b/demo/adw-demo-window.ui
@@ -218,6 +218,16 @@
</property>
</object>
</child>
+ <child>
+ <object class="GtkStackPage">
+ <property name="title" translatable="yes">Dialogs</property>
+ <property name="child">
+ <object class="AdwDemoPageDialogs">
+ <signal name="add-toast" handler="adw_toast_overlay_add_toast"
object="toast_overlay" swapped="yes"/>
+ </object>
+ </property>
+ </object>
+ </child>
</object>
</child>
</object>
diff --git a/demo/adwaita-demo.gresources.xml b/demo/adwaita-demo.gresources.xml
index 80f528b9..b27a6b98 100644
--- a/demo/adwaita-demo.gresources.xml
+++ b/demo/adwaita-demo.gresources.xml
@@ -27,6 +27,7 @@
<file preprocess="xml-stripblanks">icons/scalable/actions/view-sidebar-end-symbolic-rtl.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/widget-carousel-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/widget-clamp-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/scalable/actions/widget-dialog-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/widget-flap-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/widget-leaflet-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/widget-list-symbolic.svg</file>
@@ -47,6 +48,7 @@
<file preprocess="xml-stripblanks">pages/buttons/adw-demo-page-buttons.ui</file>
<file preprocess="xml-stripblanks">pages/carousel/adw-demo-page-carousel.ui</file>
<file preprocess="xml-stripblanks">pages/clamp/adw-demo-page-clamp.ui</file>
+ <file preprocess="xml-stripblanks">pages/dialogs/adw-demo-page-dialogs.ui</file>
<file preprocess="xml-stripblanks">pages/flap/adw-demo-page-flap.ui</file>
<file preprocess="xml-stripblanks">pages/flap/adw-flap-demo-window.ui</file>
<file preprocess="xml-stripblanks">pages/leaflet/adw-demo-page-leaflet.ui</file>
diff --git a/demo/icons/scalable/actions/widget-dialog-symbolic.svg
b/demo/icons/scalable/actions/widget-dialog-symbolic.svg
new file mode 100644
index 00000000..3d31e026
--- /dev/null
+++ b/demo/icons/scalable/actions/widget-dialog-symbolic.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g
fill="#222222"><path d="m 2 1.007812 c -0.550781 0 -1 0.449219 -1 1 v 1.984376 h 2 v -0.984376 h 9 v 0.992188
h 2 v -1.992188 c 0 -0.550781 -0.449219 -1 -1 -1 z m 10 12 v 0.992188 h -9 v -0.984375 h -2 v 1.984375 c 0
0.550781 0.449219 1 1 1 h 11 c 0.550781 0 1 -0.449219 1 -1 v -1.992188 z m 0 0" fill-opacity="0.34902"/><path
d="m 4 4 c -0.550781 0 -1 0.449219 -1 1 v 7.007812 c 0 0.550782 0.449219 1 1 1 h 7 c 0.550781 0 1 -0.449218 1
-1 v -7.007812 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 2 h 7 v 3.992188 h -7 z m 0 4.992188 h 3 v 1.007812 h
-3 z m 4 0 h 3 v 1.007812 h -3 z m 0 0"/></g></svg>
diff --git a/demo/meson.build b/demo/meson.build
index 6af941fe..13280a64 100644
--- a/demo/meson.build
+++ b/demo/meson.build
@@ -17,6 +17,7 @@ adwaita_demo_sources = [
'pages/buttons/adw-demo-page-buttons.c',
'pages/carousel/adw-demo-page-carousel.c',
'pages/clamp/adw-demo-page-clamp.c',
+ 'pages/dialogs/adw-demo-page-dialogs.c',
'pages/flap/adw-demo-page-flap.c',
'pages/flap/adw-flap-demo-window.c',
'pages/leaflet/adw-demo-page-leaflet.c',
diff --git a/demo/pages/dialogs/adw-demo-page-dialogs.c b/demo/pages/dialogs/adw-demo-page-dialogs.c
new file mode 100644
index 00000000..132de2a6
--- /dev/null
+++ b/demo/pages/dialogs/adw-demo-page-dialogs.c
@@ -0,0 +1,78 @@
+#include "adw-demo-page-dialogs.h"
+
+#include <glib/gi18n.h>
+
+struct _AdwDemoPageDialogs
+{
+ AdwBin parent_instance;
+};
+
+G_DEFINE_TYPE (AdwDemoPageDialogs, adw_demo_page_dialogs, ADW_TYPE_BIN)
+
+enum {
+ SIGNAL_ADD_TOAST,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+message_response_cb (AdwDemoPageDialogs *self,
+ const char *response)
+{
+ AdwToast *toast = adw_toast_new_format (_("Dialog response: %s"), response);
+
+ g_signal_emit (self, signals[SIGNAL_ADD_TOAST], 0, toast);
+}
+
+static void
+demo_message_dialog_cb (AdwDemoPageDialogs *self)
+{
+ GtkWindow *parent = GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self)));
+ GtkWidget *dialog;
+
+ dialog = adw_message_dialog_new (parent,
+ _("Save Changes?"),
+ _("Open document contains unsaved changes. Changes which are not saved
will be permanently lost."));
+
+ adw_message_dialog_add_responses (ADW_MESSAGE_DIALOG (dialog),
+ "cancel", _("_Cancel"),
+ "discard", _("_Discard"),
+ "save", _("_Save"),
+ NULL);
+
+ adw_message_dialog_set_response_appearance (ADW_MESSAGE_DIALOG (dialog), "discard",
ADW_RESPONSE_DESTRUCTIVE);
+ adw_message_dialog_set_response_appearance (ADW_MESSAGE_DIALOG (dialog), "save", ADW_RESPONSE_SUGGESTED);
+
+ adw_message_dialog_set_default_response (ADW_MESSAGE_DIALOG (dialog), "save");
+ adw_message_dialog_set_close_response (ADW_MESSAGE_DIALOG (dialog), "cancel");
+
+ g_signal_connect_swapped (dialog, "response", G_CALLBACK (message_response_cb), self);
+
+ gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+adw_demo_page_dialogs_class_init (AdwDemoPageDialogsClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ signals[SIGNAL_ADD_TOAST] =
+ g_signal_new ("add-toast",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ ADW_TYPE_TOAST);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
"/org/gnome/Adwaita1/Demo/ui/pages/dialogs/adw-demo-page-dialogs.ui");
+
+ gtk_widget_class_install_action (widget_class, "demo.message-dialog", NULL, (GtkWidgetActionActivateFunc)
demo_message_dialog_cb);
+}
+
+static void
+adw_demo_page_dialogs_init (AdwDemoPageDialogs *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/demo/pages/dialogs/adw-demo-page-dialogs.h b/demo/pages/dialogs/adw-demo-page-dialogs.h
new file mode 100644
index 00000000..73190a8e
--- /dev/null
+++ b/demo/pages/dialogs/adw-demo-page-dialogs.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_DEMO_PAGE_DIALOGS (adw_demo_page_dialogs_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwDemoPageDialogs, adw_demo_page_dialogs, ADW, DEMO_PAGE_DIALOGS, AdwBin)
+
+G_END_DECLS
diff --git a/demo/pages/dialogs/adw-demo-page-dialogs.ui b/demo/pages/dialogs/adw-demo-page-dialogs.ui
new file mode 100644
index 00000000..eaccadcb
--- /dev/null
+++ b/demo/pages/dialogs/adw-demo-page-dialogs.ui
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk" version="4.0"/>
+ <requires lib="libadwaita" version="1.0"/>
+ <template class="AdwDemoPageDialogs" parent="AdwBin">
+ <property name="child">
+ <object class="AdwStatusPage">
+ <property name="icon-name">widget-dialog-symbolic</property>
+ <property name="title" translatable="yes">Dialogs</property>
+ <property name="description" translatable="yes">Adaptive dialog widgets.</property>
+ <property name="child">
+ <object class="GtkButton">
+ <property name="label" translatable="yes">Message Dialog</property>
+ <property name="halign">center</property>
+ <property name="action-name">demo.message-dialog</property>
+ <style>
+ <class name="pill"/>
+ </style>
+ </object>
+ </property>
+ </object>
+ </property>
+ </template>
+</interface>
diff --git a/doc/images/message-dialog-appearance-dark.png b/doc/images/message-dialog-appearance-dark.png
new file mode 100644
index 00000000..3715d3cf
Binary files /dev/null and b/doc/images/message-dialog-appearance-dark.png differ
diff --git a/doc/images/message-dialog-appearance.png b/doc/images/message-dialog-appearance.png
new file mode 100644
index 00000000..a1c16461
Binary files /dev/null and b/doc/images/message-dialog-appearance.png differ
diff --git a/doc/images/message-dialog-dark.png b/doc/images/message-dialog-dark.png
new file mode 100644
index 00000000..4e140caa
Binary files /dev/null and b/doc/images/message-dialog-dark.png differ
diff --git a/doc/images/message-dialog.png b/doc/images/message-dialog.png
new file mode 100644
index 00000000..70372e38
Binary files /dev/null and b/doc/images/message-dialog.png differ
diff --git a/doc/libadwaita.toml.in b/doc/libadwaita.toml.in
index 76ae6c2d..5be4004a 100644
--- a/doc/libadwaita.toml.in
+++ b/doc/libadwaita.toml.in
@@ -144,6 +144,10 @@ content_images = [
"images/leaflet-wide-dark.png",
"images/linked-controls.png",
"images/linked-controls-dark.png",
+ "images/message-dialog.png",
+ "images/message-dialog-dark.png",
+ "images/message-dialog-appearance.png",
+ "images/message-dialog-appearance-dark.png",
"images/navigation-sidebar.png",
"images/navigation-sidebar-dark.png",
"images/osd.png",
diff --git a/doc/named-colors.md b/doc/named-colors.md
index 0b71e86c..1ba61f6c 100644
--- a/doc/named-colors.md
+++ b/doc/named-colors.md
@@ -414,6 +414,36 @@ The card styles use a shadow to separate the card from the window background.
always be partially transparent black, with the opacity tuned to be well visible
on top of [<code>@window_bg_color</code>](#window-colors).
+### Dialog Colors
+
+These colors are used for [class@MessageDialog].
+
+<table>
+ <tr>
+ <th>Name</th>
+ <th/>
+ <th>Light</th>
+ <th/>
+ <th>Dark</th>
+ </tr>
+ <tr>
+ <td><tt>@dialog_bg_color</tt></td>
+ <td><div class="color-pill light" style="background-color: #fafafa"/></td>
+ <td><tt>#fafafa</tt></td>
+ <td><div class="color-pill" style="background-color: #383838"/></td>
+ <td><tt>#383838</tt></td>
+ </tr>
+ <tr>
+ <td><tt>@dialog_fg_color</tt></td>
+ <td><div class="color-pill dark" style="background-color: rgba(0, 0, 0, 0.8)"/></td>
+ <td><tt>rgba(0, 0, 0, 0.8)</tt></td>
+ <td><div class="color-pill light" style="background-color: #ffffff"/></td>
+ <td><tt>#ffffff</tt></td>
+ </tr>
+</table>
+
+Since: 1.2
+
### Popover Colors
These colors are used for [class@Gtk.Popover].
diff --git a/doc/tools/data/message-dialog-appearance.ui b/doc/tools/data/message-dialog-appearance.ui
new file mode 100644
index 00000000..e9f0d71e
--- /dev/null
+++ b/doc/tools/data/message-dialog-appearance.ui
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk" version="4.0"/>
+ <requires lib="libadwaita" version="1.0"/>
+ <object class="AdwMessageDialog" id="widget">
+ <property name="heading" translatable="yes">Save Changes?</property>
+ <property name="body" translatable="yes">Open document contains unsaved changes. Changes which are not
saved will be permanently lost.</property>
+ <property name="default-response">save</property>
+ <property name="close-response">cancel</property>
+ <responses>
+ <response id="cancel" translatable="yes">_Cancel</response>
+ <response id="discard" translatable="yes" appearance="destructive">_Discard</response>
+ <response id="save" translatable="yes" appearance="suggested">_Save</response>
+ </responses>
+ </object>
+</interface>
diff --git a/doc/tools/data/message-dialog.ui b/doc/tools/data/message-dialog.ui
new file mode 100644
index 00000000..b765c6a6
--- /dev/null
+++ b/doc/tools/data/message-dialog.ui
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk" version="4.0"/>
+ <requires lib="libadwaita" version="1.0"/>
+ <object class="AdwMessageDialog" id="widget">
+ <property name="heading" translatable="yes">Replace File?</property>
+ <property name="body" translatable="yes">A file named “example.png” already exists. Do you want to
replace it?</property>
+ <property name="default-response">cancel</property>
+ <property name="close-response">cancel</property>
+ <responses>
+ <response id="cancel" translatable="yes">_Cancel</response>
+ <response id="replace" translatable="yes" appearance="destructive">_Replace</response>
+ </responses>
+ </object>
+</interface>
diff --git a/doc/visual-index.md b/doc/visual-index.md
index b2597403..0a2f42d2 100644
--- a/doc/visual-index.md
+++ b/doc/visual-index.md
@@ -26,6 +26,15 @@ Slug: visual-index
<img src="avatar.png" alt="avatar">
</picture>](class.Avatar.html)
+## Dialogs
+
+### Message Dialog
+
+[<picture>
+ <source srcset="message-dialog-dark.png" media="(prefers-color-scheme: dark)">
+ <img src="message-dialog.png" alt="message-dialog">
+</picture>](class.MessageDialog.html)
+
## Boxed Lists
### Action Row
diff --git a/src/adw-gtkbuilder-utils-private.h b/src/adw-gtkbuilder-utils-private.h
new file mode 100644
index 00000000..b3a99fb3
--- /dev/null
+++ b/src/adw-gtkbuilder-utils-private.h
@@ -0,0 +1,50 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1998-2002 James Henstridge <james daa com au>
+ * Copyright (C) 2006-2007 Async Open Source,
+ * Johan Dahlin <jdahlin async com br>,
+ * Henrique Romano <henrique async com br>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+gboolean _gtk_builder_check_parent (GtkBuilder *builder,
+ GtkBuildableParseContext *context,
+ const gchar *parent_name,
+ GError **error);
+
+void _gtk_builder_prefix_error (GtkBuilder *builder,
+ GtkBuildableParseContext *context,
+ GError **error);
+
+void _gtk_builder_error_unhandled_tag (GtkBuilder *builder,
+ GtkBuildableParseContext *context,
+ const char *object,
+ const char *element_name,
+ GError **error);
+
+const char *_gtk_builder_parser_translate (const char *domain,
+ const char *context,
+ const char *text);
+
+G_END_DECLS
diff --git a/src/adw-gtkbuilder-utils.c b/src/adw-gtkbuilder-utils.c
new file mode 100644
index 00000000..32974743
--- /dev/null
+++ b/src/adw-gtkbuilder-utils.c
@@ -0,0 +1,138 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1998-2002 James Henstridge <james daa com au>
+ * Copyright (C) 2006-2007 Async Open Source,
+ * Johan Dahlin <jdahlin async com br>,
+ * Henrique Romano <henrique async com br>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Copied and modified from gtkbuilder.c. */
+
+#include "adw-gtkbuilder-utils-private.h"
+
+/*< private >
+ * @builder: a `GtkBuilder`
+ * @context: the `GtkBuildableParseContext`
+ * @parent_name: the name of the expected parent element
+ * @error: return location for an error
+ *
+ * Checks that the parent element of the currently handled
+ * start tag is @parent_name and set @error if it isn't.
+ *
+ * This is intended to be called in start_element vfuncs to
+ * ensure that element nesting is as intended.
+ *
+ * Returns: %TRUE if @parent_name is the parent element
+ */
+gboolean
+_gtk_builder_check_parent (GtkBuilder *builder,
+ GtkBuildableParseContext *context,
+ const gchar *parent_name,
+ GError **error)
+{
+ GPtrArray *stack;
+ int line, col;
+ const char *parent;
+ const char *element;
+
+ stack = gtk_buildable_parse_context_get_element_stack (context);
+
+ element = g_ptr_array_index (stack, stack->len - 1);
+ parent = stack->len > 1 ? g_ptr_array_index (stack, stack->len - 2) : "";
+
+ if (g_str_equal (parent_name, parent) ||
+ (g_str_equal (parent_name, "object") && g_str_equal (parent, "template")))
+ return TRUE;
+
+ gtk_buildable_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_INVALID_TAG,
+ "%d:%d Can't use <%s> here",
+ line, col, element);
+
+ return FALSE;
+}
+
+/*< private >
+ * _gtk_builder_prefix_error:
+ * @builder: a `GtkBuilder`
+ * @context: the `GtkBuildableParseContext`
+ * @error: an error
+ *
+ * Calls g_prefix_error() to prepend a filename:line:column marker
+ * to the given error. The filename is taken from @builder, and
+ * the line and column are obtained by calling
+ * g_markup_parse_context_get_position().
+ *
+ * This is intended to be called on errors returned by
+ * g_markup_collect_attributes() in a start_element vfunc.
+ */
+void
+_gtk_builder_prefix_error (GtkBuilder *builder,
+ GtkBuildableParseContext *context,
+ GError **error)
+{
+ int line, col;
+
+ gtk_buildable_parse_context_get_position (context, &line, &col);
+ g_prefix_error (error, ":%d:%d ", line, col);
+}
+
+/*< private >
+ * _gtk_builder_error_unhandled_tag:
+ * @builder: a `GtkBuilder`
+ * @context: the `GtkBuildableParseContext`
+ * @object: name of the object that is being handled
+ * @element_name: name of the element whose start tag is being handled
+ * @error: return location for the error
+ *
+ * Sets @error to a suitable error indicating that an @element_name
+ * tag is not expected in the custom markup for @object.
+ *
+ * This is intended to be called in a start_element vfunc.
+ */
+void
+_gtk_builder_error_unhandled_tag (GtkBuilder *builder,
+ GtkBuildableParseContext *context,
+ const char *object,
+ const char *element_name,
+ GError **error)
+{
+ int line, col;
+
+ gtk_buildable_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_UNHANDLED_TAG,
+ "%d:%d Unsupported tag for %s: <%s>",
+ line, col,
+ object, element_name);
+}
+
+const char *
+_gtk_builder_parser_translate (const char *domain,
+ const char *context,
+ const char *text)
+{
+ const char *s;
+
+ if (context)
+ s = g_dpgettext2 (domain, context, text);
+ else
+ s = g_dgettext (domain, text);
+
+ return s;
+}
diff --git a/src/adw-message-dialog.c b/src/adw-message-dialog.c
new file mode 100644
index 00000000..46d12741
--- /dev/null
+++ b/src/adw-message-dialog.c
@@ -0,0 +1,2072 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+#include "adw-message-dialog.h"
+
+#include "adw-gtkbuilder-utils-private.h"
+#include "adw-squeezer.h"
+
+/**
+ * AdwMessageDialog:
+ *
+ * A dialog presenting a message or a question.
+ *
+ * <picture>
+ * <source srcset="message-dialog-dark.png" media="(prefers-color-scheme: dark)">
+ * <img src="message-dialog.png" alt="message-dialog">
+ * </picture>
+ *
+ * Message dialogs have a heading, a body, an optional child widget, and one or
+ * multiple responses, each presented as a button.
+ *
+ * Each response has a unique string ID, and a button label. Additionally, each
+ * response can be enabled or disabled, and can have a suggested or destructive
+ * appearance.
+ *
+ * When one of the responses is activated, or the dialog is closed, the
+ * [signal@MessageDialog::response] signal will be emitted. This signal is
+ * detailed, and the detail, as well as the `response` parameter will be set to
+ * the ID of the activated response, or to the value of the
+ * [property@MessageDialog:close-response] property if the dialog had been
+ * closed without activating any of the responses.
+ *
+ * Response buttons can be presented horizontally or vertically depending on
+ * available space.
+ *
+ * An example of using a message dialog:
+ *
+ * ```c
+ * GtkWidget *dialog;
+ *
+ * dialog = adw_message_dialog_new (parent, _("Replace File?"), NULL);
+ *
+ * adw_message_dialog_format_body (ADW_MESSAGE_DIALOG (dialog),
+ * _("A file named “%s” already exists. Do you want to replace it?"),
+ * filename);
+ *
+ * adw_message_dialog_add_responses (ADW_MESSAGE_DIALOG (dialog),
+ * "cancel", _("_Cancel"),
+ * "replace", _("_Replace"),
+ * NULL);
+ *
+ * adw_message_dialog_set_response_appearance (ADW_MESSAGE_DIALOG (dialog), "replace",
ADW_RESPONSE_DESTRUCTIVE);
+ *
+ * adw_message_dialog_set_default_response (ADW_MESSAGE_DIALOG (dialog), "cancel");
+ * adw_message_dialog_set_close_response (ADW_MESSAGE_DIALOG (dialog), "cancel");
+ *
+ * g_signal_connect (dialog, "response", G_CALLBACK (response_cb), self);
+ *
+ * gtk_window_present (GTK_WINDOW (dialog));
+ * ```
+ *
+ * ## AdwMessageDialog as GtkBuildable
+ *
+ * `AdwMessageDialog` supports adding responses in UI definitions by via the
+ * `<responses>` element that may contain multiple `<response>` elements, each
+ * respresenting a response.
+ *
+ * Each of the `<response>` elements must have the `id` attribute specifying the
+ * response ID. The contents of the element are used as the response label.
+ *
+ * Response labels can be translated with the usual `translatable`, `context`
+ * and `comments` attributes.
+ *
+ * The `<response>` elements can also have `enabled` and/or `appearance`
+ * attributes. See [method@MessageDialog.set_response_enabled] and
+ * [method@MessageDialog.set_response_appearance] for details.
+ *
+ * Example of an `AdwMessageDialog` UI definition:
+ *
+ * ```xml
+ * <object class="AdwMessageDialog" id="dialog">
+ * <property name="heading" translatable="yes">Save Changes?</property>
+ * <property name="body" translatable="yes">Open documents contain unsaved changes. Changes which are not
saved will be permanently lost.</property>
+ * <property name="default-response">save</property>
+ * <property name="close-response">cancel</property>
+ * <signal name="response" handler="response_id"/>
+ * <responses>
+ * <response id="cancel" translatable="yes">_Cancel</response>
+ * <response id="discard" translatable="yes" appearance="destructive">_Discard</response>
+ * <response id="save" translatable="yes" appearance="suggested" enabled="false">_Save</response>
+ * </responses>
+ * </object>
+ * ```
+ *
+ * ## Accessibility
+ *
+ * `AdwMessageDialog` uses the `GTK_ACCESSIBLE_ROLE_DIALOG` role.
+ *
+ * Since: 1.2
+ */
+
+/**
+ * AdwResponseAppearance:
+ * @ADW_RESPONSE_DEFAULT: the default appearance.
+ * @ADW_RESPONSE_SUGGESTED: used to denote important responses such as the
+ * affirmative action.
+ * @ADW_RESPONSE_DESTRUCTIVE: used to draw attention to the potentially damaging
+ * consequences of using the response. This appearance acts as a warning to
+ * the user.
+ *
+ * Describes the possible styles of [class@MessageDialog] response buttons.
+ *
+ * See [method@MessageDialog.set_response_appearance].
+ *
+ * Since: 1.2
+ */
+
+#define DIALOG_MARGIN 30
+
+typedef struct {
+ AdwMessageDialog *dialog;
+ GQuark id;
+ char *label;
+ AdwResponseAppearance appearance;
+ gboolean enabled;
+
+ GtkWidget *wide_button;
+ GtkWidget *narrow_button;
+} ResponseInfo;
+
+typedef struct
+{
+ GtkWidget *heading_label;
+ GtkWidget *body_label;
+ GtkBox *message_area;
+ GtkBox *wide_response_box;
+ GtkBox *narrow_response_box;
+ GtkSizeGroup *wide_size_group;
+ GtkSizeGroup *narrow_size_group;
+ AdwSqueezer *squeezer;
+
+ char *heading;
+ gboolean heading_use_markup;
+ char *body;
+ gboolean body_use_markup;
+ GtkWidget *child;
+
+ GList *responses;
+ GQuark default_response;
+ GQuark close_response;
+
+ gboolean block_close_response;
+
+ GtkWindow *parent_window;
+ int parent_width;
+ int parent_height;
+} AdwMessageDialogPrivate;
+
+static void adw_message_dialog_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (AdwMessageDialog, adw_message_dialog, GTK_TYPE_WINDOW,
+ G_ADD_PRIVATE (AdwMessageDialog)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_message_dialog_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+ PROP_0,
+ PROP_HEADING,
+ PROP_HEADING_USE_MARKUP,
+ PROP_BODY,
+ PROP_BODY_USE_MARKUP,
+ PROP_EXTRA_CHILD,
+ PROP_DEFAULT_RESPONSE,
+ PROP_CLOSE_RESPONSE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_RESPONSE,
+ SIGNAL_LAST_SIGNAL,
+};
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+response_info_free (ResponseInfo *info)
+{
+ g_free (info->label);
+ g_free (info);
+}
+
+static ResponseInfo *
+find_response_by_quark (AdwMessageDialog *self,
+ GQuark id)
+{
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+ GList *l;
+
+ for (l = priv->responses; l; l = l->next) {
+ ResponseInfo *info = l->data;
+
+ if (info->id == id)
+ return info;
+ }
+
+ return NULL;
+}
+
+static inline ResponseInfo *
+find_response (AdwMessageDialog *self,
+ const char *id)
+{
+ return find_response_by_quark (self, g_quark_try_string (id));
+}
+
+static void
+parent_size_cb (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+ int w = gtk_widget_get_allocated_width (GTK_WIDGET (priv->parent_window));
+ int h = gtk_widget_get_allocated_height (GTK_WIDGET (priv->parent_window));
+
+ if (w == priv->parent_width && h == priv->parent_height)
+ return;
+
+ priv->parent_width = w;
+ priv->parent_height = h;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+parent_realize_cb (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+ GdkSurface *surface;
+
+ g_assert (GTK_IS_NATIVE (priv->parent_window));
+
+ surface = gtk_native_get_surface (GTK_NATIVE (priv->parent_window));
+
+ g_signal_connect_swapped (surface, "compute-size",
+ G_CALLBACK (parent_size_cb), self);
+ g_signal_connect_swapped (surface, "notify::width",
+ G_CALLBACK (parent_size_cb), self);
+ g_signal_connect_swapped (surface, "notify::height",
+ G_CALLBACK (parent_size_cb), self);
+
+ parent_size_cb (self);
+}
+
+static void
+parent_unrealize_cb (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+ GdkSurface *surface;
+
+ g_assert (GTK_IS_NATIVE (priv->parent_window));
+
+ surface = gtk_native_get_surface (GTK_NATIVE (priv->parent_window));
+
+ g_signal_handlers_disconnect_by_func (surface,
+ G_CALLBACK (parent_size_cb),
+ self);
+
+ priv->parent_width = -1;
+ priv->parent_height = -1;
+}
+
+static void
+set_parent (AdwMessageDialog *self,
+ GtkWindow *parent)
+{
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+
+ if (priv->parent_window == parent)
+ return;
+
+ if (priv->parent_window) {
+ g_signal_handlers_disconnect_by_func (priv->parent_window,
+ G_CALLBACK (parent_realize_cb),
+ self);
+ g_signal_handlers_disconnect_by_func (priv->parent_window,
+ G_CALLBACK (parent_unrealize_cb),
+ self);
+
+ if (gtk_widget_get_realized (GTK_WIDGET (priv->parent_window)))
+ parent_unrealize_cb (self);
+ }
+
+ priv->parent_window = parent;
+
+ if (priv->parent_window) {
+ if (gtk_widget_get_realized (GTK_WIDGET (priv->parent_window)))
+ parent_realize_cb (self);
+
+ g_signal_connect_swapped (priv->parent_window, "realize",
+ G_CALLBACK (parent_realize_cb), self);
+ g_signal_connect_swapped (priv->parent_window, "unrealize",
+ G_CALLBACK (parent_unrealize_cb), self);
+ }
+}
+
+static void
+parent_changed_cb (AdwMessageDialog *self)
+{
+ GtkWindow *transient_for = gtk_window_get_transient_for (GTK_WINDOW (self));
+
+ set_parent (self, transient_for);
+}
+
+static void
+button_clicked_cb (ResponseInfo *info)
+{
+ AdwMessageDialog *self = info->dialog;
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+
+ g_object_ref (self);
+ priv->block_close_response = TRUE;
+
+ gtk_window_close (GTK_WINDOW (self));
+ g_signal_emit (self, signals[SIGNAL_RESPONSE], info->id, g_quark_to_string (info->id));
+
+ priv->block_close_response = FALSE;
+ g_object_unref (self);
+}
+
+static GtkWidget *
+create_response_button (AdwMessageDialog *self,
+ ResponseInfo *info)
+{
+ GtkWidget *button = gtk_button_new_with_mnemonic (info->label);
+
+ gtk_widget_add_css_class (button, "flat");
+
+ switch (info->appearance) {
+ case ADW_RESPONSE_SUGGESTED:
+ gtk_widget_add_css_class (button, "suggested");
+ break;
+ case ADW_RESPONSE_DESTRUCTIVE:
+ gtk_widget_add_css_class (button, "destructive");
+ break;
+ case ADW_RESPONSE_DEFAULT:
+ default:
+ break;
+ }
+
+ gtk_widget_set_sensitive (button, info->enabled);
+
+ g_signal_connect_swapped (button, "clicked", G_CALLBACK (button_clicked_cb), info);
+
+ return button;
+}
+
+static void
+update_default_response (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+ ResponseInfo *info;
+
+ if (!priv->default_response)
+ return;
+
+ info = find_response_by_quark (self, priv->default_response);
+
+ if (!info)
+ return;
+
+ if (adw_squeezer_get_visible_child (priv->squeezer) == GTK_WIDGET (priv->narrow_response_box))
+ gtk_window_set_default_widget (GTK_WINDOW (self), info->narrow_button);
+ else
+ gtk_window_set_default_widget (GTK_WINDOW (self), info->wide_button);
+}
+
+static void
+update_window_title (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+
+ if (priv->heading_use_markup) {
+ char *heading = NULL;
+ GError *error = NULL;
+
+ pango_parse_markup (priv->heading, -1, 0, NULL, &heading, NULL, &error);
+
+ if (error) {
+ g_critical ("Couldn't parse markup: %s", error->message);
+ g_clear_error (&error);
+
+ heading = g_strdup (priv->heading);
+ }
+
+ gtk_window_set_title (GTK_WINDOW (self), heading);
+
+ g_free (heading);
+ } else {
+ gtk_window_set_title (GTK_WINDOW (self), priv->heading);
+ }
+}
+
+static gboolean
+adw_message_dialog_close_request (GtkWindow *window)
+{
+ AdwMessageDialog *self = ADW_MESSAGE_DIALOG (window);
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+
+ if (!priv->block_close_response)
+ g_signal_emit (self, signals[SIGNAL_RESPONSE],
+ priv->close_response,
+ g_quark_to_string (priv->close_response));
+
+ return GTK_WINDOW_CLASS (adw_message_dialog_parent_class)->close_request (window);
+}
+
+static void
+adw_message_dialog_map (GtkWidget *widget)
+{
+ AdwMessageDialog *self = ADW_MESSAGE_DIALOG (widget);
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+ GtkWidget *focus, *default_widget;
+
+ if (gtk_window_get_transient_for (GTK_WINDOW (self)) == NULL)
+ g_message ("AdwMessageDialog mapped without a transient parent. This is discouraged.");
+
+ GTK_WIDGET_CLASS (adw_message_dialog_parent_class)->map (widget);
+
+ update_default_response (self);
+
+ /* The rest of the function was copied from gtkdialog.c */
+ focus = gtk_window_get_focus (GTK_WINDOW (self));
+ if (!focus) {
+ GtkWidget *first_focus = NULL;
+ GList *l;
+
+ do {
+ g_signal_emit_by_name (self, "move_focus", GTK_DIR_TAB_FORWARD);
+
+ focus = gtk_window_get_focus (GTK_WINDOW (self));
+ if (GTK_IS_LABEL (focus) &&
+ !gtk_label_get_current_uri (GTK_LABEL (focus)))
+ gtk_label_select_region (GTK_LABEL (focus), 0, 0);
+
+ if (first_focus == NULL)
+ first_focus = focus;
+ else if (first_focus == focus)
+ break;
+
+ if (!GTK_IS_LABEL (focus))
+ break;
+ } while (TRUE);
+
+ default_widget = gtk_window_get_default_widget (GTK_WINDOW (self));
+ for (l = priv->responses; l; l = l->next) {
+ ResponseInfo *response = l->data;
+
+ if ((focus == NULL || response->wide_button == focus) &&
+ response->wide_button != default_widget &&
+ default_widget) {
+ gtk_widget_grab_focus (default_widget);
+ break;
+ }
+
+ if ((focus == NULL || response->narrow_button == focus) &&
+ response->narrow_button != default_widget &&
+ default_widget) {
+ gtk_widget_grab_focus (default_widget);
+ break;
+ }
+ }
+ }
+}
+
+static void
+adw_message_dialog_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *min,
+ int *nat,
+ int *min_baseline,
+ int *nat_baseline)
+{
+ AdwMessageDialog *self = ADW_MESSAGE_DIALOG (widget);
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+ int parent_size, max_size, base_min, base_nat;
+
+ if (min_baseline)
+ *min_baseline = -1;
+ if (nat_baseline)
+ *nat_baseline = -1;
+
+ GTK_WIDGET_CLASS (adw_message_dialog_parent_class)->measure (widget,
+ orientation,
+ for_size,
+ &base_min,
+ &base_nat,
+ NULL, NULL);
+
+ if (min)
+ *min = base_min;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ parent_size = priv->parent_width;
+ else
+ parent_size = priv->parent_height;
+
+ if (parent_size < 0) {
+ if (nat)
+ *nat = base_nat;
+
+ return;
+ }
+
+ max_size = parent_size - DIALOG_MARGIN * 2;
+ max_size = MAX (base_min, max_size);
+
+ if (nat)
+ *nat = CLAMP (base_nat, base_min, max_size);
+}
+
+static void
+adw_message_dialog_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwMessageDialog *self = ADW_MESSAGE_DIALOG (object);
+
+ switch (prop_id) {
+ case PROP_HEADING:
+ g_value_set_string (value, adw_message_dialog_get_heading (self));
+ break;
+ case PROP_HEADING_USE_MARKUP:
+ g_value_set_boolean (value, adw_message_dialog_get_heading_use_markup (self));
+ break;
+ case PROP_BODY:
+ g_value_set_string (value, adw_message_dialog_get_body (self));
+ break;
+ case PROP_BODY_USE_MARKUP:
+ g_value_set_boolean (value, adw_message_dialog_get_body_use_markup (self));
+ break;
+ case PROP_EXTRA_CHILD:
+ g_value_set_object (value, adw_message_dialog_get_extra_child (self));
+ break;
+ case PROP_DEFAULT_RESPONSE:
+ g_value_set_string (value, adw_message_dialog_get_default_response (self));
+ break;
+ case PROP_CLOSE_RESPONSE:
+ g_value_set_string (value, adw_message_dialog_get_close_response (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_message_dialog_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwMessageDialog *self = ADW_MESSAGE_DIALOG (object);
+
+ switch (prop_id) {
+ case PROP_HEADING:
+ adw_message_dialog_set_heading (self, g_value_get_string (value));
+ break;
+ case PROP_HEADING_USE_MARKUP:
+ adw_message_dialog_set_heading_use_markup (self, g_value_get_boolean (value));
+ break;
+ case PROP_BODY:
+ adw_message_dialog_set_body (self, g_value_get_string (value));
+ break;
+ case PROP_BODY_USE_MARKUP:
+ adw_message_dialog_set_body_use_markup (self, g_value_get_boolean (value));
+ break;
+ case PROP_EXTRA_CHILD:
+ adw_message_dialog_set_extra_child (self, g_value_get_object (value));
+ break;
+ case PROP_DEFAULT_RESPONSE:
+ adw_message_dialog_set_default_response (self, g_value_get_string (value));
+ break;
+ case PROP_CLOSE_RESPONSE:
+ adw_message_dialog_set_close_response (self, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_message_dialog_dispose (GObject *object)
+{
+ AdwMessageDialog *self = ADW_MESSAGE_DIALOG (object);
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+
+ set_parent (self, NULL);
+
+ if (priv->child) {
+ gtk_box_remove (priv->message_area, priv->child);
+ priv->child = NULL;
+ }
+
+ g_list_free_full (priv->responses, (GDestroyNotify) response_info_free);
+
+ G_OBJECT_CLASS (adw_message_dialog_parent_class)->dispose (object);
+}
+
+static void
+adw_message_dialog_finalize (GObject *object)
+{
+ AdwMessageDialog *self = ADW_MESSAGE_DIALOG (object);
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+
+ g_clear_pointer (&priv->heading, g_free);
+ g_clear_pointer (&priv->body, g_free);
+
+ G_OBJECT_CLASS (adw_message_dialog_parent_class)->finalize (object);
+}
+
+static void
+adw_message_dialog_class_init (AdwMessageDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass);
+
+ object_class->get_property = adw_message_dialog_get_property;
+ object_class->set_property = adw_message_dialog_set_property;
+ object_class->dispose = adw_message_dialog_dispose;
+ object_class->finalize = adw_message_dialog_finalize;
+
+ widget_class->map = adw_message_dialog_map;
+ widget_class->measure = adw_message_dialog_measure;
+
+ window_class->close_request = adw_message_dialog_close_request;
+
+ /**
+ * AdwMessageDialog:heading: (attributes org.gtk.Property.get=adw_message_dialog_get_heading
org.gtk.Property.set=adw_message_dialog_set_heading)
+ *
+ * The heading of the dialog.
+ *
+ * Since: 1.2
+ */
+ props[PROP_HEADING] =
+ g_param_spec_string ("heading", NULL, NULL,
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwMessageDialog:heading-use-markup: (attributes
org.gtk.Property.get=adw_message_dialog_get_heading_use_markup
org.gtk.Property.set=adw_message_dialog_set_heading_use_markup)
+ *
+ * Whether the heading includes Pango markup.
+ *
+ * See [func@Pango.parse_markup].
+ *
+ * Since: 1.2
+ */
+ props[PROP_HEADING_USE_MARKUP] =
+ g_param_spec_boolean ("heading-use-markup", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwMessageDialog:body: (attributes org.gtk.Property.get=adw_message_dialog_get_body
org.gtk.Property.set=adw_message_dialog_set_body)
+ *
+ * The body text of the dialog.
+ *
+ * Since: 1.2
+ */
+ props[PROP_BODY] =
+ g_param_spec_string ("body", NULL, NULL,
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwMessageDialog:body-use-markup: (attributes
org.gtk.Property.get=adw_message_dialog_get_body_use_markup
org.gtk.Property.set=adw_message_dialog_set_body_use_markup)
+ *
+ * Whether the body text includes Pango markup.
+ *
+ * See [func@Pango.parse_markup].
+ *
+ * Since: 1.2
+ */
+ props[PROP_BODY_USE_MARKUP] =
+ g_param_spec_boolean ("body-use-markup", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwMessageDialog:extra-child: (attributes org.gtk.Property.get=adw_message_dialog_get_extra_child
org.gtk.Property.set=adw_message_dialog_set_extra_child)
+ *
+ * The child widget.
+ *
+ * Displayed below the heading and body.
+ *
+ * Since: 1.2
+ */
+ props[PROP_EXTRA_CHILD] =
+ g_param_spec_object ("extra-child", NULL, NULL,
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwMessageDialog:default-response: (attributes
org.gtk.Property.get=adw_message_dialog_get_default_response
org.gtk.Property.set=adw_message_dialog_set_default_response)
+ *
+ * The response ID of the default response.
+ *
+ * If set, pressing <kbd>Enter</kbd> will activate the corresponding button.
+ *
+ * If set to `NULL` or a non-existent response ID, pressing <kbd>Enter</kbd>
+ * will do nothing.
+ *
+ * Since: 1.2
+ */
+ props[PROP_DEFAULT_RESPONSE] =
+ g_param_spec_string ("default-response", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwMessageDialog:close-response: (attributes org.gtk.Property.get=adw_message_dialog_get_close_response
org.gtk.Property.set=adw_message_dialog_set_close_response)
+ *
+ * The ID of the close response.
+ *
+ * It will be passed to [signal@MessageDialog::response] if the window is
+ * closed by pressing <kbd>Escape</kbd> or with a system action.
+ *
+ * It doesn't have to correspond to any of the responses in the dialog.
+ *
+ * The default close response is `close`.
+ *
+ * Since: 1.2
+ */
+ props[PROP_CLOSE_RESPONSE] =
+ g_param_spec_string ("close-response", NULL, NULL,
+ "close",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * AdwMessageDialog::response:
+ * @self: a message dialog
+ * @response: the response ID
+ *
+ * This signal is emitted when the dialog is closed.
+ *
+ * @response will be set to the response ID of the button that had been
+ * activated.
+ *
+ * if the dialog was closed by pressing <kbd>Escape</kbd> or with a system
+ * action, @response will be set to the value of
+ * [property@MessageDialog:close-response].
+ *
+ * Since: 1.2
+ */
+ signals[SIGNAL_RESPONSE] =
+ g_signal_new ("response",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+ G_STRUCT_OFFSET (AdwMessageDialogClass, response),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/Adwaita/ui/adw-message-dialog.ui");
+
+ gtk_widget_class_bind_template_child_private (widget_class, AdwMessageDialog, heading_label);
+ gtk_widget_class_bind_template_child_private (widget_class, AdwMessageDialog, body_label);
+ gtk_widget_class_bind_template_child_private (widget_class, AdwMessageDialog, message_area);
+ gtk_widget_class_bind_template_child_private (widget_class, AdwMessageDialog, squeezer);
+ gtk_widget_class_bind_template_child_private (widget_class, AdwMessageDialog, wide_response_box);
+ gtk_widget_class_bind_template_child_private (widget_class, AdwMessageDialog, narrow_response_box);
+ gtk_widget_class_bind_template_child_private (widget_class, AdwMessageDialog, wide_size_group);
+ gtk_widget_class_bind_template_child_private (widget_class, AdwMessageDialog, narrow_size_group);
+
+ gtk_widget_class_bind_template_callback (widget_class, update_default_response);
+
+ gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Escape, 0, "window.close", NULL);
+
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_DIALOG);
+}
+
+static void
+adw_message_dialog_init (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv = adw_message_dialog_get_instance_private (self);
+
+ gtk_window_set_resizable (GTK_WINDOW (self), FALSE);
+ gtk_window_set_modal (GTK_WINDOW (self), TRUE);
+ gtk_window_set_destroy_with_parent (GTK_WINDOW (self), TRUE);
+
+ priv->close_response = g_quark_from_string ("close");
+
+ priv->heading = g_strdup ("");
+ priv->body = g_strdup ("");
+ priv->parent_width = -1;
+ priv->parent_height = -1;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ parent_changed_cb (self);
+ g_signal_connect (self, "notify::transient-for",
+ G_CALLBACK (parent_changed_cb), self);
+}
+
+/* Custom tag handling was copied and modified
+ * from gtk-size-group.c and gtk-scale.c */
+
+typedef struct {
+ GObject *object;
+ GtkBuilder *builder;
+ GSList *responses;
+} ResponseParserData;
+
+typedef struct {
+ char *id;
+
+ GString *label;
+ char *context;
+ gboolean translatable;
+
+ AdwResponseAppearance appearance;
+ gboolean enabled;
+
+ int line;
+ int col;
+} ResponseData;
+
+static void
+response_data_free (gpointer data)
+{
+ ResponseData *response = data;
+
+ g_free (response->id);
+ g_string_free (response->label, TRUE);
+ g_free (response->context);
+ g_free (response);
+}
+
+static void
+response_start_element (GtkBuildableParseContext *context,
+ const char *element_name,
+ const char **names,
+ const char **values,
+ gpointer user_data,
+ GError **error)
+{
+ ResponseParserData *data = user_data;
+
+ if (strcmp (element_name, "response") == 0) {
+ const char *id;
+ const char *msg_context = NULL;
+ gboolean translatable = FALSE;
+ const char *appearance_str = NULL;
+ AdwResponseAppearance appearance = ADW_RESPONSE_DEFAULT;
+ gboolean enabled = TRUE;
+ ResponseData *response;
+
+ if (!_gtk_builder_check_parent (data->builder, context, "responses", error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_STRING, "id", &id,
+ G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "appearance",
&appearance_str,
+ G_MARKUP_COLLECT_TRISTATE | G_MARKUP_COLLECT_OPTIONAL, "enabled",
&enabled,
+ G_MARKUP_COLLECT_BOOLEAN | G_MARKUP_COLLECT_OPTIONAL, "translatable",
&translatable,
+ G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "comments", NULL,
+ G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "context",
&msg_context,
+ G_MARKUP_COLLECT_INVALID)) {
+ _gtk_builder_prefix_error (data->builder, context, error);
+ return;
+ }
+
+ if (appearance_str) {
+ GValue gvalue = G_VALUE_INIT;
+
+ if (!gtk_builder_value_from_string_type (data->builder, ADW_TYPE_RESPONSE_APPEARANCE, appearance_str,
&gvalue, error)) {
+ _gtk_builder_prefix_error (data->builder, context, error);
+ return;
+ }
+
+ appearance = g_value_get_enum (&gvalue);
+ }
+
+ /* Normalize a tri-state value */
+ enabled = enabled != FALSE;
+
+ response = g_new (ResponseData, 1);
+ response->id = g_strdup (id);
+ response->context = g_strdup (msg_context);
+ response->translatable = translatable;
+ response->label = g_string_new ("");
+ response->appearance = appearance;
+ response->enabled = enabled;
+
+ gtk_buildable_parse_context_get_position (context, &response->line, &response->col);
+ data->responses = g_slist_prepend (data->responses, response);
+ } else if (strcmp (element_name, "responses") == 0) {
+ if (!_gtk_builder_check_parent (data->builder, context, "object", error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_INVALID, NULL, NULL,
+ G_MARKUP_COLLECT_INVALID))
+ _gtk_builder_prefix_error (data->builder, context, error);
+ } else {
+ _gtk_builder_error_unhandled_tag (data->builder, context,
+ "AdwMessageDialog", element_name,
+ error);
+ }
+}
+
+static void
+response_text (GtkBuildableParseContext *context,
+ const char *text,
+ gsize text_len,
+ gpointer user_data,
+ GError **error)
+{
+ ResponseParserData *data = user_data;
+
+ if (strcmp (gtk_buildable_parse_context_get_element (context), "response") == 0) {
+ ResponseData *response = data->responses->data;
+
+ g_string_append_len (response->label, text, text_len);
+ }
+}
+
+static const GtkBuildableParser response_parser = {
+ response_start_element,
+ NULL,
+ response_text,
+ NULL
+};
+
+static gboolean
+adw_message_dialog_buildable_custom_tag_start (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const char *tagname,
+ GtkBuildableParser *parser,
+ gpointer *parser_data)
+{
+ ResponseParserData *data;
+
+ if (child)
+ return FALSE;
+
+ if (strcmp (tagname, "responses") == 0) {
+ data = g_new0 (ResponseParserData, 1);
+ data->responses = NULL;
+ data->object = G_OBJECT (buildable);
+ data->builder = builder;
+
+ *parser = response_parser;
+ *parser_data = data;
+
+ return TRUE;
+ }
+
+ return parent_buildable_iface->custom_tag_start (buildable, builder, child,
+ tagname, parser, parser_data);
+}
+
+static void
+adw_message_dialog_buildable_custom_finished (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const char *tagname,
+ gpointer user_data)
+{
+ GSList *l;
+ ResponseParserData *data;
+
+ if (strcmp (tagname, "responses") != 0) {
+ parent_buildable_iface->custom_finished (buildable, builder, child,
+ tagname, user_data);
+ return;
+ }
+
+ data = (ResponseParserData*)user_data;
+ data->responses = g_slist_reverse (data->responses);
+
+ for (l = data->responses; l; l = l->next) {
+ ResponseData *response = l->data;
+ const char *label;
+
+ if (response->translatable && response->label->len)
+ label = _gtk_builder_parser_translate (gtk_builder_get_translation_domain (builder),
+ response->context,
+ response->label->str);
+ else
+ label = response->label->str;
+
+ adw_message_dialog_add_response (ADW_MESSAGE_DIALOG (data->object),
+ response->id, label);
+
+ if (response->appearance != ADW_RESPONSE_DEFAULT)
+ adw_message_dialog_set_response_appearance (ADW_MESSAGE_DIALOG (data->object),
+ response->id, response->appearance);
+
+ if (!response->enabled)
+ adw_message_dialog_set_response_enabled (ADW_MESSAGE_DIALOG (data->object),
+ response->id, FALSE);
+ }
+
+ g_slist_free_full (data->responses, response_data_free);
+ g_free (data);
+}
+
+static void
+adw_message_dialog_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const char *type)
+{
+ AdwMessageDialog *self = ADW_MESSAGE_DIALOG (buildable);
+
+ if (GTK_IS_WIDGET (child))
+ adw_message_dialog_set_extra_child (self, GTK_WIDGET (child));
+ else
+ parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+adw_message_dialog_buildable_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+
+ iface->add_child = adw_message_dialog_buildable_add_child;
+ iface->custom_tag_start = adw_message_dialog_buildable_custom_tag_start;
+ iface->custom_finished = adw_message_dialog_buildable_custom_finished;
+}
+
+/**
+ * adw_message_dialog_new:
+ * @parent: (nullable): transient parent
+ * @heading: (nullable): the heading
+ * @body: (nullable): the body text
+ *
+ * Creates a new `AdwMessageDialog`.
+ *
+ * @heading and @body can be set to `NULL`. This can be useful if they need to
+ * be formatted or use markup. In that case, set them to `NULL` and call
+ * [method@MessageDialog.format_body] or similar methods afterwards:
+ *
+ * ```c
+ * GtkWidget *dialog;
+ *
+ * dialog = adw_message_dialog_new (parent, _("Replace File?"), NULL);
+ * adw_message_dialog_format_body (ADW_MESSAGE_DIALOG (dialog),
+ * _("A file named “%s” already exists. Do you want to replace it?"),
+ * filename);
+ * ```
+ *
+ * Returns: the newly created `AdwMessageDialog`
+ *
+ * Since: 1.2
+ */
+GtkWidget *
+adw_message_dialog_new (GtkWindow *parent,
+ const char *heading,
+ const char *body)
+{
+ GtkWidget *dialog;
+
+ g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), NULL);
+
+ dialog = g_object_new (ADW_TYPE_MESSAGE_DIALOG,
+ "transient-for", parent,
+ NULL);
+
+ if (heading)
+ adw_message_dialog_set_heading (ADW_MESSAGE_DIALOG (dialog), heading);
+
+ if (body)
+ adw_message_dialog_set_body (ADW_MESSAGE_DIALOG (dialog), body);
+
+ return dialog;
+}
+
+/**
+ * adw_message_dialog_get_heading: (attributes org.gtk.Method.get_property=heading)
+ * @self: a message dialog
+ *
+ * Gets the heading of @self.
+ *
+ * Returns: (nullable): the heading of @self.
+ *
+ * Since: 1.2
+ */
+const char *
+adw_message_dialog_get_heading (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ return priv->heading;
+}
+
+/**
+ * adw_message_dialog_set_heading: (attributes org.gtk.Method.set_property=heading)
+ * @self: a message dialog
+ * @heading: (nullable): the heading of @self
+ *
+ * Sets the heading of @self.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_heading (AdwMessageDialog *self,
+ const char *heading)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (heading != NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ if (heading == priv->heading)
+ return;
+
+ g_free (priv->heading);
+ priv->heading = g_strdup (heading);
+
+ gtk_label_set_label (GTK_LABEL (priv->heading_label), heading);
+ gtk_widget_set_visible (priv->heading_label, heading && *heading);
+
+ update_window_title (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HEADING]);
+}
+
+/**
+ * adw_message_dialog_get_heading_use_markup: (attributes org.gtk.Method.get_property=heading-use-markup)
+ * @self: a message dialog
+ *
+ * Gets whether the heading of @self includes Pango markup.
+ *
+ * Returns: whether @self uses markup for heading
+ *
+ * Since: 1.2
+ */
+gboolean
+adw_message_dialog_get_heading_use_markup (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), FALSE);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ return priv->heading_use_markup;
+}
+
+/**
+ * adw_message_dialog_set_heading_use_markup: (attributes org.gtk.Method.set_property=heading-use-markup)
+ * @self: a message dialog
+ * @use_markup: whether to use markup for heading
+ *
+ * Sets whether the heading of @self includes Pango markup.
+ *
+ * See [func@Pango.parse_markup].
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_heading_use_markup (AdwMessageDialog *self,
+ gboolean use_markup)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ use_markup = !!use_markup;
+
+ if (use_markup == priv->heading_use_markup)
+ return;
+
+ priv->heading_use_markup = use_markup;
+
+ gtk_label_set_use_markup (GTK_LABEL (priv->heading_label), use_markup);
+
+ update_window_title (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HEADING_USE_MARKUP]);
+}
+
+
+/**
+ * adw_message_dialog_format_heading:
+ * @self: a message dialog
+ * @format: the formatted string for the heading
+ * @...: the parameters to insert into @format
+ *
+ * Sets the formatted heading of @self.
+ *
+ * See [property@MessageDialog:heading].
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_format_heading (AdwMessageDialog *self,
+ const char *format,
+ ...)
+{
+ va_list args;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (format != NULL);
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ adw_message_dialog_set_heading_use_markup (self, FALSE);
+
+ if (format) {
+ char *heading;
+
+ va_start (args, format);
+ heading = g_strdup_vprintf (format, args);
+ va_end (args);
+
+ adw_message_dialog_set_heading (self, heading);
+
+ g_free (heading);
+ } else {
+ adw_message_dialog_set_heading (self, NULL);
+ }
+
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * adw_message_dialog_format_heading_markup:
+ * @self: a message dialog
+ * @format: the formatted string for the heading with Pango markup
+ * @...: the parameters to insert into @format
+ *
+ * Sets the formatted heading of @self with Pango markup.
+ *
+ * The @format is assumed to contain Pango markup.
+ *
+ * Special XML characters in the `printf()` arguments passed to this function
+ * will automatically be escaped as necessary, see
+ * [func@GLib.markup_printf_escaped].
+ *
+ * See [property@MessageDialog:heading].
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_format_heading_markup (AdwMessageDialog *self,
+ const char *format,
+ ...)
+{
+ va_list args;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (format != NULL);
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ adw_message_dialog_set_heading_use_markup (self, TRUE);
+
+ if (format) {
+ char *heading;
+
+ va_start (args, format);
+ heading = g_markup_vprintf_escaped (format, args);
+ va_end (args);
+
+ adw_message_dialog_set_heading (self, heading);
+
+ g_free (heading);
+ } else {
+ adw_message_dialog_set_heading (self, "");
+ }
+
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * adw_message_dialog_get_body: (attributes org.gtk.Method.get_property=body)
+ * @self: a message dialog
+ *
+ * Gets the body text of @self.
+ *
+ * Returns: the body of @self.
+ *
+ * Since: 1.2
+ */
+const char *
+adw_message_dialog_get_body (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ return priv->body;
+}
+
+/**
+ * adw_message_dialog_set_body: (attributes org.gtk.Method.set_property=body)
+ * @self: a message dialog
+ * @body: the body of @self
+ *
+ * Sets the body text of @self.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_body (AdwMessageDialog *self,
+ const char *body)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (body != NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ if (body == priv->body)
+ return;
+
+ g_free (priv->body);
+ priv->body = g_strdup (body);
+
+ gtk_label_set_label (GTK_LABEL (priv->body_label), body);
+ gtk_widget_set_visible (priv->body_label, body && *body);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BODY]);
+}
+
+/**
+ * adw_message_dialog_get_body_use_markup: (attributes org.gtk.Method.get_property=body-use-markup)
+ * @self: a message dialog
+ *
+ * Gets whether the body text of @self includes Pango markup.
+ *
+ * Returns: whether @self uses markup for body text
+ *
+ * Since: 1.2
+ */
+gboolean
+adw_message_dialog_get_body_use_markup (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), FALSE);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ return priv->body_use_markup;
+}
+
+/**
+ * adw_message_dialog_set_body_use_markup: (attributes org.gtk.Method.set_property=body-use-markup)
+ * @self: a message dialog
+ * @use_markup: whether to use markup for body text
+ *
+ * Sets whether the body text of @self includes Pango markup.
+ *
+ * See [func@Pango.parse_markup].
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_body_use_markup (AdwMessageDialog *self,
+ gboolean use_markup)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ use_markup = !!use_markup;
+
+ if (use_markup == priv->body_use_markup)
+ return;
+
+ priv->body_use_markup = use_markup;
+
+ gtk_label_set_use_markup (GTK_LABEL (priv->body_label), use_markup);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BODY_USE_MARKUP]);
+}
+
+/**
+ * adw_message_dialog_format_body:
+ * @self: a message dialog
+ * @format: the formatted string for the body text
+ * @...: the parameters to insert into @format
+ *
+ * Sets the formatted body text of @self.
+ *
+ * See [property@MessageDialog:body].
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_format_body (AdwMessageDialog *self,
+ const char *format,
+ ...)
+{
+ va_list args;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (format != NULL);
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ adw_message_dialog_set_body_use_markup (self, FALSE);
+
+ if (format) {
+ char *body;
+
+ va_start (args, format);
+ body = g_strdup_vprintf (format, args);
+ va_end (args);
+
+ adw_message_dialog_set_body (self, body);
+
+ g_free (body);
+ } else {
+ adw_message_dialog_set_body (self, "");
+ }
+
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * adw_message_dialog_format_body_markup:
+ * @self: a message dialog
+ * @format: the formatted string for the body text with Pango markup
+ * @...: the parameters to insert into @format
+ *
+ * Sets the formatted body text of @self with Pango markup.
+ *
+ * The @format is assumed to contain Pango markup.
+ *
+ * Special XML characters in the `printf()` arguments passed to this function
+ * will automatically be escaped as necessary, see
+ * [func@GLib.markup_printf_escaped].
+ *
+ * See [property@MessageDialog:body].
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_format_body_markup (AdwMessageDialog *self,
+ const char *format,
+ ...)
+{
+ va_list args;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (format != NULL);
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ adw_message_dialog_set_body_use_markup (self, TRUE);
+
+ if (format) {
+ char *body;
+
+ va_start (args, format);
+ body = g_markup_vprintf_escaped (format, args);
+ va_end (args);
+
+ adw_message_dialog_set_body (self, body);
+
+ g_free (body);
+ } else {
+ adw_message_dialog_set_body (self, NULL);
+ }
+
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * adw_message_dialog_get_extra_child: (attributes org.gtk.Method.get_property=extra-child)
+ * @self: a message dialog
+ *
+ * Gets the child widget of @self.
+ *
+ * Returns: (nullable) (transfer none): the child widget of @self.
+ *
+ * Since: 1.2
+ */
+GtkWidget *
+adw_message_dialog_get_extra_child (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ return priv->child;
+}
+
+/**
+ * adw_message_dialog_set_extra_child: (attributes org.gtk.Method.set_property=extra-child)
+ * @self: a message dialog
+ * @child: (nullable): the child widget
+ *
+ * Sets the child widget of @self.
+ *
+ * The child widget is displayed below the heading and body.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_extra_child (AdwMessageDialog *self,
+ GtkWidget *child)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ if (child == priv->child)
+ return;
+
+ if (priv->child)
+ gtk_box_remove (priv->message_area, priv->child);
+
+ priv->child = child;
+
+ if (priv->child)
+ gtk_box_append (priv->message_area, priv->child);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXTRA_CHILD]);
+}
+
+/**
+ * adw_message_dialog_add_response:
+ * @self: a message dialog
+ * @id: the response ID
+ * @label: the response label
+ *
+ * Adds a response with @id and @label to @self.
+ *
+ * Responses are represented as buttons in the dialog.
+ *
+ * Response ID must be unique. It will be used in
+ * [signal@MessageDialog::response] to tell which response had been activated,
+ * as well as to inspect and modify the response later.
+ *
+ * An embedded underline in @label indicates a mnemonic.
+ *
+ * [method@MessageDialog.set_response_label] can be used to change the response
+ * label after it had been added.
+ *
+ * [method@MessageDialog.set_response_enabled] and
+ * [method@MessageDialog.set_response_appearance] can be used to customize the
+ * responses further.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_add_response (AdwMessageDialog *self,
+ const char *id,
+ const char *label)
+{
+ AdwMessageDialogPrivate *priv;
+ ResponseInfo *info;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (id != NULL);
+ g_return_if_fail (label != NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ if (find_response (self, id)) {
+ g_critical ("Trying to add a response with id '%s' to an "
+ "AdwMessageDialog, but such a response already exists", id);
+ return;
+ }
+
+ info = g_new0 (ResponseInfo, 1);
+
+ info->dialog = self;
+ info->id = g_quark_from_string (id);
+ info->label = g_strdup (label);
+ info->appearance = ADW_RESPONSE_DEFAULT;
+ info->enabled = TRUE;
+
+ if (priv->responses) {
+ GtkWidget *separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
+ gtk_box_append (priv->wide_response_box, separator);
+ }
+
+ info->wide_button = create_response_button (self, info);
+ gtk_widget_set_hexpand (info->wide_button, TRUE);
+ gtk_box_append (priv->wide_response_box, info->wide_button);
+ gtk_size_group_add_widget (priv->wide_size_group, info->wide_button);
+
+ if (priv->responses) {
+ GtkWidget *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_box_prepend (priv->narrow_response_box, separator);
+ }
+
+ info->narrow_button = create_response_button (self, info);
+ gtk_box_prepend (priv->narrow_response_box, info->narrow_button);
+ gtk_size_group_add_widget (priv->narrow_size_group, info->narrow_button);
+
+ priv->responses = g_list_append (priv->responses, info);
+
+ if (priv->default_response == info->id &&
+ gtk_widget_get_mapped (GTK_WIDGET (self)))
+ update_default_response (self);
+}
+
+/**
+ * adw_message_dialog_add_responses: (skip)
+ * @self: a message dialog
+ * @first_id: response id
+ * @...: label for first response, then more id-label pairs
+ *
+ * Adds multiple responses to @self.
+ *
+ * This is the same as calling [[method@MessageDialog.add_response] repeatedly.
+ * The variable argument list should be `NULL`-terminated list of response IDs
+ * and labels.
+ *
+ * Example:
+ *
+ * ```c
+ * adw_message_dialog_add_responses (dialog,
+ * "cancel", _("_Cancel"),
+ * "discard", _("_Discard"),
+ * "save", _("_Save"),
+ * NULL);
+ * ```
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_add_responses (AdwMessageDialog *self,
+ const char *first_id,
+ ...)
+{
+ va_list args;
+ const char *id, *label;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+
+ va_start (args, first_id);
+
+ if (!first_id)
+ return;
+
+ id = first_id;
+ label = va_arg (args, const char *);
+
+ while (id) {
+ adw_message_dialog_add_response (self, id, label);
+
+ id = va_arg (args, const char *);
+ if (!id)
+ break;
+
+ label = va_arg (args, const char *);
+ }
+
+ va_end (args);
+}
+
+/**
+ * adw_message_dialog_get_response_label:
+ * @self: a message dialog
+ * @response: a response ID
+ *
+ * Gets the label of @response.
+ *
+ * See [method@MessageDialog.set_response_label].
+ *
+ * Returns: the label of @response
+ *
+ * Since: 1.2
+ */
+const char *
+adw_message_dialog_get_response_label (AdwMessageDialog *self,
+ const char *response)
+{
+ ResponseInfo *info;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), NULL);
+ g_return_val_if_fail (response != NULL, NULL);
+
+ info = find_response (self, response);
+ if (!info) {
+ g_critical ("AdwMessageDialog does not have a response with ID '%s'", response);
+ return NULL;
+ }
+
+ return info->label;
+}
+
+/**
+ * adw_message_dialog_set_response_label:
+ * @self: a message dialog
+ * @response: a response ID
+ * @label: the label of @response
+ *
+ * Sets the label of @response to @label.
+ *
+ * Labels are displayed on the dialog buttons. An embedded underline in @label
+ * indicates a mnemonic.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_response_label (AdwMessageDialog *self,
+ const char *response,
+ const char *label)
+{
+ ResponseInfo *info;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (response != NULL);
+ g_return_if_fail (label != NULL);
+
+ info = find_response (self, response);
+ if (!info) {
+ g_critical ("AdwMessageDialog does not have a response with ID '%s'", response);
+ return;
+ }
+
+ g_free (info->label);
+ info->label = g_strdup (label);
+
+ gtk_button_set_label (GTK_BUTTON (info->wide_button), label);
+ gtk_button_set_label (GTK_BUTTON (info->narrow_button), label);
+}
+
+/**
+ * adw_message_dialog_get_response_appearance:
+ * @self: a message dialog
+ * @response: a response ID
+ *
+ * Gets the appearance of @response.
+ *
+ * See [method@MessageDialog.set_response_appearance].
+ *
+ * Returns: the appearance of @response
+ *
+ * Since: 1.2
+ */
+AdwResponseAppearance
+adw_message_dialog_get_response_appearance (AdwMessageDialog *self,
+ const char *response)
+{
+ ResponseInfo *info;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), FALSE);
+ g_return_val_if_fail (response != NULL, FALSE);
+
+ info = find_response (self, response);
+ if (!info) {
+ g_critical ("AdwMessageDialog does not have a response with ID '%s'", response);
+ return FALSE;
+ }
+
+ return info->appearance;
+}
+
+/**
+ * adw_message_dialog_set_response_appearance:
+ * @self: a message dialog
+ * @response: a response ID
+ * @appearance: appearance for @response
+ *
+ * Sets the appearance for @response.
+ *
+ * <picture>
+ * <source srcset="message-dialog-appearance-dark.png" media="(prefers-color-scheme: dark)">
+ * <img src="message-dialog-appearance.png" alt="message-dialog-appearance">
+ * </picture>
+ *
+ * Use `ADW_RESPONSE_SUGGESTED` to mark important responses such as the
+ * affirmative action, like the Save button in the example.
+ *
+ * Use `ADW_RESPONSE_DESTRUCTIVE` to draw attention to the potentially damaging
+ * consequences of using @response. This appearance acts as a warning to the
+ * user. The Discard button in the example is using this appearance.
+ *
+ * The default appearance is `ADW_RESPONSE_DEFAULT`.
+ *
+ * Negative responses like Cancel or Close should use the default appearance.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_response_appearance (AdwMessageDialog *self,
+ const char *response,
+ AdwResponseAppearance appearance)
+{
+ ResponseInfo *info;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (response != NULL);
+ g_return_if_fail (appearance >= ADW_RESPONSE_DEFAULT &&
+ appearance <= ADW_RESPONSE_DESTRUCTIVE);
+
+ info = find_response (self, response);
+ if (!info) {
+ g_critical ("AdwMessageDialog does not have a response with ID '%s'", response);
+ return;
+ }
+
+ if (appearance == info->appearance)
+ return;
+
+ info->appearance = appearance;
+
+ if (info->appearance == ADW_RESPONSE_SUGGESTED) {
+ gtk_widget_add_css_class (info->wide_button, "suggested");
+ gtk_widget_add_css_class (info->narrow_button, "suggested");
+ } else {
+ gtk_widget_remove_css_class (info->wide_button, "suggested");
+ gtk_widget_remove_css_class (info->narrow_button, "suggested");
+ }
+
+ if (info->appearance == ADW_RESPONSE_DESTRUCTIVE) {
+ gtk_widget_add_css_class (info->wide_button, "destructive");
+ gtk_widget_add_css_class (info->narrow_button, "destructive");
+ } else {
+ gtk_widget_remove_css_class (info->wide_button, "destructive");
+ gtk_widget_remove_css_class (info->narrow_button, "destructive");
+ }
+}
+
+/**
+ * adw_message_dialog_get_response_enabled:
+ * @self: a message dialog
+ * @response: a response ID
+ *
+ * Gets whether @response is enabled.
+ *
+ * See [method@MessageDialog.set_response_enabled].
+ *
+ * Returns: whether @response is enabled
+ *
+ * Since: 1.2
+ */
+gboolean
+adw_message_dialog_get_response_enabled (AdwMessageDialog *self,
+ const char *response)
+{
+ ResponseInfo *info;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), FALSE);
+ g_return_val_if_fail (response != NULL, FALSE);
+
+ info = find_response (self, response);
+ if (!info) {
+ g_critical ("AdwMessageDialog does not have a response with ID '%s'", response);
+ return FALSE;
+ }
+
+ return info->enabled;
+}
+
+/**
+ * adw_message_dialog_set_response_enabled:
+ * @self: a message dialog
+ * @response: a response ID
+ * @enabled: whether to enable @response
+ *
+ * Sets whether @response is enabled.
+ *
+ * If @response is not enabled, the corresponding button will have
+ * [property@Gtk.Widget:sensitive] set to `FALSE` and it can't be activated as
+ * a default response.
+ *
+ * @response can still be used as [property@MessageDialog:close-response] while
+ * it's not enabled.
+ *
+ * Responses are enabled by default.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_response_enabled (AdwMessageDialog *self,
+ const char *response,
+ gboolean enabled)
+{
+ ResponseInfo *info;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (response != NULL);
+
+ info = find_response (self, response);
+ if (!info) {
+ g_critical ("AdwMessageDialog does not have a response with ID '%s'", response);
+ return;
+ }
+
+ enabled = !!enabled;
+
+ if (enabled == info->enabled)
+ return;
+
+ info->enabled = enabled;
+
+ gtk_widget_set_sensitive (info->wide_button, info->enabled);
+ gtk_widget_set_sensitive (info->narrow_button, info->enabled);
+}
+
+/**
+ * adw_message_dialog_get_default_response: (attributes org.gtk.Method.get_property=default-response)
+ * @self: a message dialog
+ *
+ * Gets the ID of the default response of @self.
+ *
+ * Returns: (nullable): the default response ID
+ *
+ * Since: 1.2
+ */
+const char *
+adw_message_dialog_get_default_response (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ if (!priv->default_response)
+ return NULL;
+
+ return g_quark_to_string (priv->default_response);
+}
+
+/**
+ * adw_message_dialog_set_default_response: (attributes org.gtk.Method.set_property=default-response)
+ * @self: a message dialog
+ * @response: (nullable): the default response ID
+ *
+ * Sets the ID of the default response of @self.
+ *
+ * If set, pressing <kbd>Enter</kbd> will activate the corresponding button.
+ *
+ * If set to `NULL` or to a non-existent response ID, pressing <kbd>Enter</kbd>
+ * will do nothing.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_default_response (AdwMessageDialog *self,
+ const char *response)
+{
+ AdwMessageDialogPrivate *priv;
+ GQuark quark;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+
+ priv = adw_message_dialog_get_instance_private (self);
+ quark = g_quark_from_string (response);
+
+ if (quark == priv->default_response)
+ return;
+
+ priv->default_response = quark;
+
+ if (gtk_widget_get_mapped (GTK_WIDGET (self)))
+ update_default_response (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEFAULT_RESPONSE]);
+}
+
+/**
+ * adw_message_dialog_get_close_response: (attributes org.gtk.Method.get_property=close-response)
+ * @self: a message dialog
+ *
+ * Gets the ID of the close response of @self.
+ *
+ * Returns: the close response ID
+ *
+ * Since: 1.2
+ */const char *
+adw_message_dialog_get_close_response (AdwMessageDialog *self)
+{
+ AdwMessageDialogPrivate *priv;
+
+ g_return_val_if_fail (ADW_IS_MESSAGE_DIALOG (self), NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+
+ return g_quark_to_string (priv->close_response);
+}
+
+/**
+ * adw_message_dialog_set_close_response: (attributes org.gtk.Method.set_property=close-response)
+ * @self: a message dialog
+ * @response: the close response ID
+ *
+ * Sets the ID of the close response of @self.
+ *
+ * It will be passed to [signal@MessageDialog::response] if the window is
+ * closed by pressing <kbd>Escape</kbd> or with a system action.
+ *
+ * It doesn't have to correspond to any of the responses in the dialog.
+ *
+ * The default close response is `close`.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_set_close_response (AdwMessageDialog *self,
+ const char *response)
+{
+ AdwMessageDialogPrivate *priv;
+ GQuark quark;
+
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (response != NULL);
+
+ priv = adw_message_dialog_get_instance_private (self);
+ quark = g_quark_from_string (response);
+
+ if (quark == priv->close_response)
+ return;
+
+ priv->close_response = quark;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CLOSE_RESPONSE]);
+}
+
+/**
+ * adw_message_dialog_response: (attributes org.gtk.Method.signal=response)
+ * @self: a message dialog
+ * @response: response ID
+ *
+ * Emits the [signal@MessageDialog::response] signal with the given response ID.
+ *
+ * Used to indicate that the user has responded to the dialog in some way.
+ *
+ * Since: 1.2
+ */
+void
+adw_message_dialog_response (AdwMessageDialog *self,
+ const char *response)
+{
+ g_return_if_fail (ADW_IS_MESSAGE_DIALOG (self));
+ g_return_if_fail (response != NULL);
+
+ g_signal_emit (self, signals[SIGNAL_RESPONSE],
+ g_quark_from_string (response), response);
+}
diff --git a/src/adw-message-dialog.h b/src/adw-message-dialog.h
new file mode 100644
index 00000000..19f96a6d
--- /dev/null
+++ b/src/adw-message-dialog.h
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include <gtk/gtk.h>
+#include "adw-enums.h"
+
+G_BEGIN_DECLS
+
+typedef enum {
+ ADW_RESPONSE_DEFAULT,
+ ADW_RESPONSE_SUGGESTED,
+ ADW_RESPONSE_DESTRUCTIVE,
+} AdwResponseAppearance;
+
+#define ADW_TYPE_MESSAGE_DIALOG (adw_message_dialog_get_type())
+
+ADW_AVAILABLE_IN_1_2
+G_DECLARE_DERIVABLE_TYPE (AdwMessageDialog, adw_message_dialog, ADW, MESSAGE_DIALOG, GtkWindow)
+
+struct _AdwMessageDialogClass
+{
+ GtkWindowClass parent_class;
+
+ void (* response) (AdwMessageDialog *self,
+ const char *response);
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_message_dialog_new (GtkWindow *parent,
+ const char *heading,
+ const char *body);
+
+ADW_AVAILABLE_IN_1_2
+const char *adw_message_dialog_get_heading (AdwMessageDialog *self);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_heading (AdwMessageDialog *self,
+ const char *heading);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_message_dialog_get_heading_use_markup (AdwMessageDialog *self);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_heading_use_markup (AdwMessageDialog *self,
+ gboolean use_markup);
+
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_format_heading (AdwMessageDialog *self,
+ const char *format,
+ ...) G_GNUC_PRINTF (2, 3);
+
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_format_heading_markup (AdwMessageDialog *self,
+ const char *format,
+ ...) G_GNUC_PRINTF (2, 3);
+
+ADW_AVAILABLE_IN_1_2
+const char *adw_message_dialog_get_body (AdwMessageDialog *self);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_body (AdwMessageDialog *self,
+ const char *body);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_message_dialog_get_body_use_markup (AdwMessageDialog *self);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_body_use_markup (AdwMessageDialog *self,
+ gboolean use_markup);
+
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_format_body (AdwMessageDialog *self,
+ const char *format,
+ ...) G_GNUC_PRINTF (2, 3);
+
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_format_body_markup (AdwMessageDialog *self,
+ const char *format,
+ ...) G_GNUC_PRINTF (2, 3);
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_message_dialog_get_extra_child (AdwMessageDialog *self);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_extra_child (AdwMessageDialog *self,
+ GtkWidget *child);
+
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_add_response (AdwMessageDialog *self,
+ const char *id,
+ const char *label);
+
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_add_responses (AdwMessageDialog *self,
+ const char *first_id,
+ ...) G_GNUC_NULL_TERMINATED;
+
+ADW_AVAILABLE_IN_1_2
+const char *adw_message_dialog_get_response_label (AdwMessageDialog *self,
+ const char *response);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_response_label (AdwMessageDialog *self,
+ const char *response,
+ const char *label);
+
+ADW_AVAILABLE_IN_1_2
+AdwResponseAppearance adw_message_dialog_get_response_appearance (AdwMessageDialog *self,
+ const char *response);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_response_appearance (AdwMessageDialog *self,
+ const char *response,
+ AdwResponseAppearance appearance);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_message_dialog_get_response_enabled (AdwMessageDialog *self,
+ const char *response);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_response_enabled (AdwMessageDialog *self,
+ const char *response,
+ gboolean enabled);
+
+ADW_AVAILABLE_IN_1_2
+const char *adw_message_dialog_get_default_response (AdwMessageDialog *self);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_default_response (AdwMessageDialog *self,
+ const char *response);
+
+ADW_AVAILABLE_IN_1_2
+const char *adw_message_dialog_get_close_response (AdwMessageDialog *self);
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_set_close_response (AdwMessageDialog *self,
+ const char *response);
+
+ADW_AVAILABLE_IN_1_2
+void adw_message_dialog_response (AdwMessageDialog *self,
+ const char *response);
+
+G_END_DECLS
diff --git a/src/adw-message-dialog.ui b/src/adw-message-dialog.ui
new file mode 100644
index 00000000..d040a91c
--- /dev/null
+++ b/src/adw-message-dialog.ui
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="libadwaita">
+ <requires lib="gtk" version="4.0"/>
+ <template class="AdwMessageDialog" parent="GtkWindow">
+ <style>
+ <class name="messagedialog"/>
+ </style>
+ <property name="titlebar">
+ <object class="GtkBox">
+ <property name="visible">False</property>
+ </object>
+ </property>
+ <property name="child">
+ <object class="GtkWindowHandle">
+ <property name="child">
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="message_area">
+ <property name="orientation">vertical</property>
+ <style>
+ <class name="message-area"/>
+ </style>
+ <child>
+ <object class="GtkLabel" id="heading_label">
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="max-width-chars">20</property>
+ <property name="justify">center</property>
+ <property name="xalign">0.5</property>
+ <property name="visible">False</property>
+ <style>
+ <class name="title-2"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="body_label">
+ <property name="vexpand">True</property>
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="max-width-chars">40</property>
+ <property name="justify">center</property>
+ <property name="xalign">0.5</property>
+ <property name="visible">False</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator"/>
+ </child>
+ <child>
+ <object class="AdwSqueezer" id="squeezer">
+ <property name="homogeneous">False</property>
+ <signal name="notify::visible-child" handler="update_default_response" swapped="yes"/>
+ <style>
+ <class name="response-area"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="wide_response_box">
+ <property name="hexpand">1</property>
+ <property name="orientation">horizontal</property>
+ <property name="valign">end</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="narrow_response_box">
+ <property name="hexpand">1</property>
+ <property name="orientation">vertical</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </property>
+ </object>
+ </property>
+ </template>
+ <object class="GtkSizeGroup" id="wide_size_group"/>
+ <object class="GtkSizeGroup" id="narrow_size_group"/>
+</interface>
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 7f4a4236..da724d86 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -14,6 +14,7 @@
<file preprocess="xml-stripblanks">adw-entry-row.ui</file>
<file preprocess="xml-stripblanks">adw-expander-row.ui</file>
<file preprocess="xml-stripblanks">adw-inspector-page.ui</file>
+ <file preprocess="xml-stripblanks">adw-message-dialog.ui</file>
<file preprocess="xml-stripblanks">adw-preferences-group.ui</file>
<file preprocess="xml-stripblanks">adw-preferences-page.ui</file>
<file preprocess="xml-stripblanks">adw-preferences-window.ui</file>
diff --git a/src/adwaita.h b/src/adwaita.h
index aa1ca1da..a2fda744 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -47,6 +47,7 @@ G_BEGIN_DECLS
#include "adw-header-bar.h"
#include "adw-leaflet.h"
#include "adw-main.h"
+#include "adw-message-dialog.h"
#include "adw-navigation-direction.h"
#include "adw-password-entry-row.h"
#include "adw-preferences-group.h"
diff --git a/src/meson.build b/src/meson.build
index 0f6c5217..eb2d9041 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -16,6 +16,7 @@ adw_public_enum_headers = [
'adw-easing.h',
'adw-header-bar.h',
'adw-leaflet.h',
+ 'adw-message-dialog.h',
'adw-navigation-direction.h',
'adw-style-manager.h',
'adw-squeezer.h',
@@ -106,6 +107,7 @@ src_headers = [
'adw-header-bar.h',
'adw-leaflet.h',
'adw-main.h',
+ 'adw-message-dialog.h',
'adw-navigation-direction.h',
'adw-password-entry-row.h',
'adw-preferences-group.h',
@@ -168,6 +170,7 @@ src_sources = [
'adw-header-bar.c',
'adw-leaflet.c',
'adw-main.c',
+ 'adw-message-dialog.c',
'adw-navigation-direction.c',
'adw-password-entry-row.c',
'adw-preferences-group.c',
@@ -201,6 +204,7 @@ libadwaita_private_sources += files([
'adw-bidi.c',
'adw-fading-label.c',
'adw-gizmo.c',
+ 'adw-gtkbuilder-utils.c',
'adw-indicator-bin.c',
'adw-inspector-page.c',
'adw-settings.c',
diff --git a/src/stylesheet/_colors.scss b/src/stylesheet/_colors.scss
index 2f4de113..f4b0cfbe 100644
--- a/src/stylesheet/_colors.scss
+++ b/src/stylesheet/_colors.scss
@@ -40,6 +40,9 @@ $card_bg_color: gtkcolor(card_bg_color);
$card_fg_color: gtkcolor(card_fg_color);
$card_shade_color: gtkcolor(card_shade_color);
+$dialog_bg_color: gtkcolor(dialog_bg_color);
+$dialog_fg_color: gtkcolor(dialog_fg_color);
+
$popover_bg_color: gtkcolor(popover_bg_color);
$popover_fg_color: gtkcolor(popover_fg_color);
diff --git a/src/stylesheet/_defaults.scss b/src/stylesheet/_defaults.scss
index 14355663..7761be5d 100644
--- a/src/stylesheet/_defaults.scss
+++ b/src/stylesheet/_defaults.scss
@@ -51,6 +51,10 @@
@define-color card_fg_color #{if($variant == 'light', transparentize(black, .2), white)};
@define-color card_shade_color #{if($variant == 'light', transparentize(black, .93), transparentize(black,
.64))};
+// Dialogs
+@define-color dialog_bg_color #{if($variant == 'light', #fafafa, #383838)};
+@define-color dialog_fg_color #{if($variant == 'light', transparentize(black, .2), white)};
+
// Popovers
@define-color popover_bg_color #{if($variant == 'light', #ffffff, #383838)};
@define-color popover_fg_color #{if($variant == 'light', transparentize(black, .2), white)};
diff --git a/src/stylesheet/widgets/_message-dialog.scss b/src/stylesheet/widgets/_message-dialog.scss
index 48be0c12..234bd915 100644
--- a/src/stylesheet/widgets/_message-dialog.scss
+++ b/src/stylesheet/widgets/_message-dialog.scss
@@ -1,3 +1,7 @@
+/********************
+ * GtkMessageDialog *
+ ********************/
+
window.dialog.message {
.titlebar {
min-height: 20px;
@@ -55,3 +59,78 @@ window.dialog.message {
}
}
}
+
+/********************
+ * AdwMessageDialog *
+ ********************/
+
+window.messagedialog {
+ background-color: $dialog_bg_color;
+ color: $dialog_fg_color;
+
+ @if $contrast != 'high' {
+ &.csd {
+ outline: none;
+ }
+ }
+
+ .message-area {
+ padding: 24px 30px;
+ border-spacing: 10px;
+ }
+
+ .response-area > box > button {
+ padding: 10px 14px;
+ border-radius: 0;
+
+ @if $contrast == 'high' {
+ &:hover,
+ &.keyboard-activating,
+ &:active,
+ &:checked {
+ box-shadow: none;
+ }
+ }
+
+ &.suggested {
+ color: $accent_color;
+ }
+
+ &.destructive {
+ color: $destructive_color;
+ }
+ }
+
+ &.csd:not(.solid-csd) {
+ border-radius: $window_radius+1;
+
+ .response-area {
+ > box.horizontal > button {
+ margin-top: -1px;
+ margin-right: -1px;
+ margin-left: -1px;
+
+ &:first-child {
+ border-bottom-left-radius: $window_radius+1;
+ margin-left: 0;
+ }
+
+ &:last-child {
+ border-bottom-right-radius: $window_radius+1;
+ margin-right: 0;
+ }
+ }
+
+ > box.vertical > button {
+ margin-top: -1px;
+ margin-bottom: -1px;
+
+ &:last-child {
+ border-bottom-left-radius: $window_radius+1;
+ border-bottom-right-radius: $window_radius+1;
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/meson.build b/tests/meson.build
index aa57172d..b2a660e5 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -41,6 +41,7 @@ test_names = [
'test-flap',
'test-header-bar',
'test-leaflet',
+ 'test-message-dialog',
'test-password-entry-row',
'test-preferences-group',
'test-preferences-page',
diff --git a/tests/test-message-dialog.c b/tests/test-message-dialog.c
new file mode 100644
index 00000000..c4953914
--- /dev/null
+++ b/tests/test-message-dialog.c
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include <adwaita.h>
+
+int notified;
+int responses;
+int responses_cancel;
+int responses_save;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+response_cb (AdwMessageDialog *dialog,
+ const char *response,
+ gpointer data)
+{
+ responses++;
+}
+
+static void
+response_cancel_cb (AdwMessageDialog *dialog,
+ const char *response,
+ gpointer data)
+{
+ responses_cancel++;
+}
+
+static void
+response_save_cb (AdwMessageDialog *dialog,
+ const char *response,
+ gpointer data)
+{
+ responses_save++;
+}
+
+static void
+test_adw_message_dialog_heading (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+ char *heading;
+
+ g_assert_nonnull (dialog);
+
+ notified = 0;
+ g_signal_connect (dialog, "notify::heading", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (dialog, "heading", &heading, NULL);
+ g_assert_cmpstr (heading, ==, "");
+
+ adw_message_dialog_set_heading (dialog, "Heading");
+ g_assert_cmpstr (adw_message_dialog_get_heading (dialog), ==, "Heading");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (dialog, "heading", "Heading 2", NULL);
+ g_assert_cmpstr (adw_message_dialog_get_heading (dialog), ==, "Heading 2");
+ g_assert_cmpint (notified, ==, 2);
+
+ g_free (heading);
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_heading_use_markup (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+ gboolean use_markup;
+
+ g_assert_nonnull (dialog);
+
+ notified = 0;
+ g_signal_connect (dialog, "notify::heading-use-markup", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (dialog, "heading-use-markup", &use_markup, NULL);
+ g_assert_false (use_markup);
+
+ adw_message_dialog_set_heading_use_markup (dialog, TRUE);
+ g_assert_true (adw_message_dialog_get_heading_use_markup (dialog));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (dialog, "heading-use-markup", FALSE, NULL);
+ g_assert_false (adw_message_dialog_get_heading_use_markup (dialog));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_body (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+ char *body;
+
+ g_assert_nonnull (dialog);
+
+ notified = 0;
+ g_signal_connect (dialog, "notify::body", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (dialog, "body", &body, NULL);
+ g_assert_cmpstr (body, ==, "");
+
+ adw_message_dialog_set_body (dialog, "Body");
+ g_assert_cmpstr (adw_message_dialog_get_body (dialog), ==, "Body");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (dialog, "body", "Body 2", NULL);
+ g_assert_cmpstr (adw_message_dialog_get_body (dialog), ==, "Body 2");
+ g_assert_cmpint (notified, ==, 2);
+
+ g_free (body);
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_body_use_markup (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+ gboolean use_markup;
+
+ g_assert_nonnull (dialog);
+
+ notified = 0;
+ g_signal_connect (dialog, "notify::body-use-markup", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (dialog, "body-use-markup", &use_markup, NULL);
+ g_assert_false (use_markup);
+
+ adw_message_dialog_set_body_use_markup (dialog, TRUE);
+ g_assert_true (adw_message_dialog_get_body_use_markup (dialog));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (dialog, "body-use-markup", FALSE, NULL);
+ g_assert_false (adw_message_dialog_get_body_use_markup (dialog));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_format (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+
+ g_assert_nonnull (dialog);
+
+ adw_message_dialog_format_heading_markup (dialog, "Heading <b>%d</b>", 42);
+ g_assert_cmpstr (adw_message_dialog_get_heading (dialog), ==, "Heading <b>42</b>");
+ g_assert_true (adw_message_dialog_get_heading_use_markup (dialog));
+
+ adw_message_dialog_format_heading (dialog, "Heading %d", 42);
+ g_assert_cmpstr (adw_message_dialog_get_heading (dialog), ==, "Heading 42");
+ g_assert_false (adw_message_dialog_get_heading_use_markup (dialog));
+
+ adw_message_dialog_format_body_markup (dialog, "Body <b>%d</b>", 42);
+ g_assert_cmpstr (adw_message_dialog_get_body (dialog), ==, "Body <b>42</b>");
+ g_assert_true (adw_message_dialog_get_body_use_markup (dialog));
+
+ adw_message_dialog_format_body (dialog, "Body %d", 42);
+ g_assert_cmpstr (adw_message_dialog_get_body (dialog), ==, "Body 42");
+ g_assert_false (adw_message_dialog_get_body_use_markup (dialog));
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_extra_child (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+ GtkWidget *widget = NULL;
+
+ g_assert_nonnull (dialog);
+
+ notified = 0;
+ g_signal_connect (dialog, "notify::extra-child", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (dialog, "extra-child", &widget, NULL);
+ g_assert_null (widget);
+
+ adw_message_dialog_set_extra_child (dialog, NULL);
+ g_assert_cmpint (notified, ==, 0);
+
+ widget = gtk_button_new ();
+ adw_message_dialog_set_extra_child (dialog, widget);
+ g_assert_true (adw_message_dialog_get_extra_child (dialog) == widget);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (dialog, "extra-child", NULL, NULL);
+ g_assert_null (adw_message_dialog_get_extra_child (dialog));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_add_response (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+
+ g_assert_nonnull (dialog);
+
+ adw_message_dialog_add_response (dialog, "response1", "Response 1");
+ adw_message_dialog_add_response (dialog, "response2", "Response 2");
+
+ g_assert_cmpstr (adw_message_dialog_get_response_label (dialog, "response1"), ==, "Response 1");
+ g_assert_true (adw_message_dialog_get_response_enabled (dialog, "response1"));
+ g_assert_cmpint (adw_message_dialog_get_response_appearance (dialog, "response1"), ==,
ADW_RESPONSE_DEFAULT);
+
+ g_assert_cmpstr (adw_message_dialog_get_response_label (dialog, "response2"), ==, "Response 2");
+ g_assert_true (adw_message_dialog_get_response_enabled (dialog, "response2"));
+ g_assert_cmpint (adw_message_dialog_get_response_appearance (dialog, "response2"), ==,
ADW_RESPONSE_DEFAULT);
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_add_responses (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+
+ g_assert_nonnull (dialog);
+
+ adw_message_dialog_add_responses (dialog,
+ "response1", "Response 1",
+ "response2", "Response 2",
+ NULL);
+
+ g_assert_cmpstr (adw_message_dialog_get_response_label (dialog, "response1"), ==, "Response 1");
+ g_assert_true (adw_message_dialog_get_response_enabled (dialog, "response1"));
+ g_assert_cmpint (adw_message_dialog_get_response_appearance (dialog, "response1"), ==,
ADW_RESPONSE_DEFAULT);
+
+ g_assert_cmpstr (adw_message_dialog_get_response_label (dialog, "response2"), ==, "Response 2");
+ g_assert_true (adw_message_dialog_get_response_enabled (dialog, "response2"));
+ g_assert_cmpint (adw_message_dialog_get_response_appearance (dialog, "response2"), ==,
ADW_RESPONSE_DEFAULT);
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_response_label (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+
+ g_assert_nonnull (dialog);
+
+ adw_message_dialog_add_response (dialog, "response", "Response");
+ g_assert_cmpstr (adw_message_dialog_get_response_label (dialog, "response"), ==, "Response");
+
+ adw_message_dialog_set_response_label (dialog, "response", "Label");
+ g_assert_cmpstr (adw_message_dialog_get_response_label (dialog, "response"), ==, "Label");
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_response_enabled (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+
+ g_assert_nonnull (dialog);
+
+ adw_message_dialog_add_response (dialog, "response", "Response");
+ g_assert_true (adw_message_dialog_get_response_enabled (dialog, "response"));
+
+ adw_message_dialog_set_response_enabled (dialog, "response", FALSE);
+ g_assert_false (adw_message_dialog_get_response_enabled (dialog, "response"));
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_response_appearance (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+
+ g_assert_nonnull (dialog);
+
+ adw_message_dialog_add_response (dialog, "response", "Response");
+ g_assert_cmpint (adw_message_dialog_get_response_appearance (dialog, "response"), ==,
ADW_RESPONSE_DEFAULT);
+
+ adw_message_dialog_set_response_appearance (dialog, "response", ADW_RESPONSE_DESTRUCTIVE);
+ g_assert_cmpint (adw_message_dialog_get_response_appearance (dialog, "response"), ==,
ADW_RESPONSE_DESTRUCTIVE);
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_response_signal (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+
+ responses = responses_cancel = responses_save = 0;
+ g_signal_connect (dialog, "response", G_CALLBACK (response_cb), NULL);
+ g_signal_connect (dialog, "response::cancel", G_CALLBACK (response_cancel_cb), NULL);
+ g_signal_connect (dialog, "response::save", G_CALLBACK (response_save_cb), NULL);
+
+ adw_message_dialog_add_response (dialog, "cancel", "Cancel");
+ adw_message_dialog_add_response (dialog, "save", "Save");
+
+ adw_message_dialog_response (dialog, "cancel");
+ g_assert_cmpint (responses, ==, 1);
+ g_assert_cmpint (responses_cancel, ==, 1);
+ g_assert_cmpint (responses_save, ==, 0);
+
+ adw_message_dialog_response (dialog, "save");
+ g_assert_cmpint (responses, ==, 2);
+ g_assert_cmpint (responses_cancel, ==, 1);
+ g_assert_cmpint (responses_save, ==, 1);
+
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_default_response (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+ char *response;
+
+ g_assert_nonnull (dialog);
+
+ notified = 0;
+ g_signal_connect (dialog, "notify::default-response", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (dialog, "default-response", &response, NULL);
+ g_assert_null (response);
+
+ adw_message_dialog_set_default_response (dialog, "save");
+ g_assert_cmpstr (adw_message_dialog_get_default_response (dialog), ==, "save");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (dialog, "default-response", "load", NULL);
+ g_assert_cmpstr (adw_message_dialog_get_default_response (dialog), ==, "load");
+ g_assert_cmpint (notified, ==, 2);
+
+ g_free (response);
+ g_assert_finalize_object (dialog);
+}
+
+static void
+test_adw_message_dialog_close_response (void)
+{
+ AdwMessageDialog *dialog = ADW_MESSAGE_DIALOG (adw_message_dialog_new (NULL, NULL, NULL));
+ char *response;
+
+ g_assert_nonnull (dialog);
+
+ notified = 0;
+ g_signal_connect (dialog, "notify::close-response", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (dialog, "close-response", &response, NULL);
+ g_assert_cmpstr (response, ==, "close");
+
+ adw_message_dialog_set_close_response (dialog, "save");
+ g_assert_cmpstr (adw_message_dialog_get_close_response (dialog), ==, "save");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (dialog, "close-response", "cancel", NULL);
+ g_assert_cmpstr (adw_message_dialog_get_close_response (dialog), ==, "cancel");
+ g_assert_cmpint (notified, ==, 2);
+
+ g_free (response);
+ g_assert_finalize_object (dialog);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ adw_init ();
+
+ g_test_add_func ("/Adwaita/MessageDialog/heading", test_adw_message_dialog_heading);
+ g_test_add_func ("/Adwaita/MessageDialog/heading_use_markup", test_adw_message_dialog_heading_use_markup);
+ g_test_add_func ("/Adwaita/MessageDialog/body", test_adw_message_dialog_body);
+ g_test_add_func ("/Adwaita/MessageDialog/body_use_markup", test_adw_message_dialog_body_use_markup);
+ g_test_add_func ("/Adwaita/MessageDialog/format", test_adw_message_dialog_format);
+ g_test_add_func ("/Adwaita/MessageDialog/extra_child", test_adw_message_dialog_extra_child);
+ g_test_add_func ("/Adwaita/MessageDialog/add_response", test_adw_message_dialog_add_response);
+ g_test_add_func ("/Adwaita/MessageDialog/add_responses", test_adw_message_dialog_add_responses);
+ g_test_add_func ("/Adwaita/MessageDialog/response_label", test_adw_message_dialog_response_label);
+ g_test_add_func ("/Adwaita/MessageDialog/response_enabled", test_adw_message_dialog_response_enabled);
+ g_test_add_func ("/Adwaita/MessageDialog/response_appearance",
test_adw_message_dialog_response_appearance);
+ g_test_add_func ("/Adwaita/MessageDialog/response_signal", test_adw_message_dialog_response_signal);
+ g_test_add_func ("/Adwaita/MessageDialog/default_response", test_adw_message_dialog_default_response);
+ g_test_add_func ("/Adwaita/MessageDialog/close_response", test_adw_message_dialog_close_response);
+
+ return g_test_run ();
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]