[libadwaita/wip/exalm/message-dialog] Add AdwMessageDialog



commit f8b7f6aa11138e9db00151f1f034e6c98583b1ac
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                           | 2074 ++++++++++++++++++++
 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, 3186 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>&#64;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>&#64;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>&#64;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..7cdee388
--- /dev/null
+++ b/src/adw-message-dialog.c
@@ -0,0 +1,2074 @@
+/*
+ * 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.
+ *
+ * When a response is activated, `AdwMessageDialog` is closed automatically.
+ *
+ * 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]