From a6f619f2be869e5c732d995c7015adfce4be87a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Mon, 23 Mar 2026 12:11:16 -0700 Subject: [PATCH 1/2] Create AlertDialog --- demo/Views/DialogsView.vala | 54 +++++++++- lib/Styles/Granite/AlertDialog.scss | 21 ++++ lib/Styles/Granite/Index.scss | 1 + lib/Widgets/AlertDialog.vala | 155 ++++++++++++++++++++++++++++ lib/meson.build | 1 + po/POTFILES | 1 + 6 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 lib/Styles/Granite/AlertDialog.scss create mode 100644 lib/Widgets/AlertDialog.vala diff --git a/demo/Views/DialogsView.vala b/demo/Views/DialogsView.vala index fd3f279d3..9b71fe400 100644 --- a/demo/Views/DialogsView.vala +++ b/demo/Views/DialogsView.vala @@ -15,6 +15,10 @@ public class DialogsView : DemoPage { construct { var dialog_button = new Gtk.Button.with_label ("Show Dialog"); + var alert_button = new Gtk.Button.with_label ("AlertDialog"); + + var alert_button_pro = new Gtk.Button.with_label ("AlertDialog+ Pro Ultra Max"); + var message_button = new Gtk.Button.with_label ("Show MessageDialog"); toast = new Granite.Toast ("Did something"); @@ -25,8 +29,10 @@ public class DialogsView : DemoPage { valign = Gtk.Align.CENTER, row_spacing = 12 }; - grid.attach (dialog_button, 0, 1); - grid.attach (message_button, 0, 2); + grid.attach (dialog_button, 0, 0); + grid.attach (alert_button, 0, 1); + grid.attach (alert_button_pro, 0, 2); + grid.attach (message_button, 0, 3); var overlay = new Gtk.Overlay () { child = grid @@ -36,6 +42,8 @@ public class DialogsView : DemoPage { child = overlay; dialog_button.clicked.connect (show_dialog); + alert_button.clicked.connect (show_alert_dialog); + alert_button_pro.clicked.connect (show_alert_dialog_pro); message_button.clicked.connect (show_message_dialog); } @@ -76,6 +84,48 @@ public class DialogsView : DemoPage { dialog.show (); } + private void show_alert_dialog () { + var dialog = new Granite.AlertDialog ( + "Say what happened", + "Provide reassurance. Explain why it happened. Provide a suggestion. Additional help info or links." + ) { + transient_for = (Gtk.Window) get_root () + }; + + dialog.add_button ("_Cancel", "cancel"); + + dialog.response.connect (() => { + dialog.destroy (); + }); + + dialog.present (); + } + + private void show_alert_dialog_pro () { + var dialog = new Granite.AlertDialog ( + "Say what happened", + "Provide reassurance. Explain why it happened. Provide a suggestion. Additional help info or links." + ) { + primary_icon = new ThemedIcon ("applications-development"), + secondary_icon = new ThemedIcon ("dialog-information"), + content = new Gtk.CheckButton.with_label ("Optional choices or content"), + transient_for = (Gtk.Window) get_root () + }; + + dialog.add_button ("_Cancel", "cancel"); + dialog.add_button ("_Take Action", "accept", SUGGESTED); + + dialog.response.connect ((response_id) => { + if (response_id == "accept") { + toast.send_notification (); + } + + dialog.destroy (); + }); + + dialog.present (); + } + private void show_message_dialog () { var message_dialog = new Granite.MessageDialog ( "Basic information and a suggestion", diff --git a/lib/Styles/Granite/AlertDialog.scss b/lib/Styles/Granite/AlertDialog.scss new file mode 100644 index 000000000..e3ad4d86b --- /dev/null +++ b/lib/Styles/Granite/AlertDialog.scss @@ -0,0 +1,21 @@ +window.granite-alert.dialog { + toolbox box.top, + toolbox box.bottom { + padding: 1rem; + } + + toolbox box.top { + overlay { + min-width: calc((48px * 2) - 1em); + } + + .large-icons { + -gtk-icon-size: 48px; + } + } + + toolbox box.bottom button { + min-width: 5em; + } +} + diff --git a/lib/Styles/Granite/Index.scss b/lib/Styles/Granite/Index.scss index 389118b48..9fe0e6f2b 100644 --- a/lib/Styles/Granite/Index.scss +++ b/lib/Styles/Granite/Index.scss @@ -1,4 +1,5 @@ @import '_classes.scss'; +@import 'AlertDialog.scss'; @import 'Box.scss'; @import 'Button.scss'; @import 'Dialog.scss'; diff --git a/lib/Widgets/AlertDialog.vala b/lib/Widgets/AlertDialog.vala new file mode 100644 index 000000000..5bafb3d64 --- /dev/null +++ b/lib/Widgets/AlertDialog.vala @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * A dialog presenting a message or a question + */ +[Version (since = "9.0.0")] +public class Granite.AlertDialog : Gtk.Window { + public signal void response (string response_id); + + /** + * Describes the possible styles of {@link Granite.AlertDialog} response buttons + */ + public enum ButtonStyle { + // Default button appearance. + DEFAULT, + // The primary suggested affirmative action/ + SUGGESTED, + // Used to draw attention to the potentially damaging consequences. This appearance acts as a warning. + DESTRUCTIVE; + + public string to_string () { + switch (this) { + case DESTRUCTIVE: + return Granite.CssClass.DESTRUCTIVE; + case SUGGESTED: + return Granite.CssClass.SUGGESTED; + default: + return ""; + } + } + } + + /** + * The secondary text, body of the dialog. + */ + public string secondary_text { get; construct set; } + + /** + * The {@link GLib.Icon} that is used to display the primary_icon representing the app making the request + */ + public GLib.Icon primary_icon { get; set; } + + /** + * The {@link GLib.Icon} that is used to display a secondary_icon representing the action to be performed + */ + public GLib.Icon secondary_icon { get; set; } + + /** + * The child widget for the content area + */ + public Gtk.Widget content { get; set; } + + private Granite.Box button_box; + + /** + * Constructs a new {@link Granite.AlertDialog}. + * See {@link Granite.AlertDialog} for more details. + * + * @param title the title of the dialog + * @param secondary_text the body of the dialog + */ + public AlertDialog (string title, string secondary_text) { + Object ( + title: title, + secondary_text: secondary_text + ); + } + + static construct { + Granite.init (); + } + + construct { + var primary_icon = new Gtk.Image.from_icon_name ("") { + halign = START, + icon_size = LARGE + }; + + var secondary_icon = new Gtk.Image.from_icon_name ("") { + halign = END, + icon_size = LARGE + }; + + var overlay = new Gtk.Overlay () { + child = secondary_icon, + halign = CENTER + }; + overlay.add_overlay (primary_icon); + + var header_label = new Granite.HeaderLabel ("") { + size = H3 + }; + + var header = new Granite.Box (VERTICAL); + header.append (overlay); + header.append (header_label); + + button_box = new Granite.Box (HORIZONTAL, HALF) { + homogeneous = true + }; + + var toolbarview = new Granite.ToolBox () { + vexpand = true + }; + toolbarview.add_bottom_bar (button_box); + toolbarview.add_top_bar (header); + + child = toolbarview; + default_width = 325; + modal = true; + + // We need to hide the title area + titlebar = new Gtk.Grid () { + visible = false + }; + + add_css_class ("dialog"); + add_css_class ("granite-alert"); + + bind_property ("primary-icon", primary_icon, "gicon"); + bind_property ("secondary-icon", secondary_icon, "gicon"); + bind_property ("content", toolbarview, "content"); + bind_property ("title", header_label, "label", SYNC_CREATE); + bind_property ("secondary-text", header_label, "secondary-text", SYNC_CREATE); + + var response_action = new SimpleAction ("response", VariantType.STRING); + response_action.activate.connect ((parameter) => { + response (parameter.get_string ()); + }); + + var action_group = new SimpleActionGroup (); + action_group.add_action (response_action); + + insert_action_group ("dialog", action_group); + + // close_request.connect (() => { response (DELETE_EVENT); }); + } + + public void add_button (string label, string response_id, ButtonStyle button_style = DEFAULT) { + var button = new Gtk.Button.with_label (label) { + action_name = "dialog.response", + action_target = new Variant.string (response_id), + use_underline = true + }; + + if (button_style != DEFAULT) { + button.add_css_class (button_style.to_string ()); + } + + button_box.append (button); + } +} diff --git a/lib/meson.build b/lib/meson.build index a3215c723..a9cc184b1 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -17,6 +17,7 @@ libgranite_sources = files( 'Widgets/AbstractSettingsPage.vala', 'Widgets/AbstractSimpleSettingsPage.vala', 'Widgets/AccelLabel.vala', + 'Widgets' / 'AlertDialog.vala', 'Widgets/BackButton.vala', 'Widgets/Bin.vala', 'Widgets/Box.vala', diff --git a/po/POTFILES b/po/POTFILES index 6188089a1..389f50025 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -6,6 +6,7 @@ lib/Services/System.vala lib/Widgets/AbstractSettingsPage.vala lib/Widgets/AbstractSimpleSettingsPage.vala +lib/Widgets/AlertDialog.vala lib/Widgets/DatePicker.vala lib/Widgets/HeaderLabel.vala lib/Widgets/MessageDialog.vala From 333ab0d6bb65eee254a3e4dae666bd2c1c721a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Mon, 23 Mar 2026 12:33:55 -0700 Subject: [PATCH 2/2] Allow setting response sensitivity --- demo/Views/DialogsView.vala | 6 ++++++ lib/Widgets/AlertDialog.vala | 29 ++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/demo/Views/DialogsView.vala b/demo/Views/DialogsView.vala index 9b71fe400..70b0123f2 100644 --- a/demo/Views/DialogsView.vala +++ b/demo/Views/DialogsView.vala @@ -115,6 +115,8 @@ public class DialogsView : DemoPage { dialog.add_button ("_Cancel", "cancel"); dialog.add_button ("_Take Action", "accept", SUGGESTED); + dialog.set_response_enabled ("accept", false); + dialog.response.connect ((response_id) => { if (response_id == "accept") { toast.send_notification (); @@ -124,6 +126,10 @@ public class DialogsView : DemoPage { }); dialog.present (); + + Timeout.add_once (3000, () => { + dialog.set_response_enabled ("accept", true); + }); } private void show_message_dialog () { diff --git a/lib/Widgets/AlertDialog.vala b/lib/Widgets/AlertDialog.vala index 5bafb3d64..d5c4927fd 100644 --- a/lib/Widgets/AlertDialog.vala +++ b/lib/Widgets/AlertDialog.vala @@ -54,6 +54,7 @@ public class Granite.AlertDialog : Gtk.Window { public Gtk.Widget content { get; set; } private Granite.Box button_box; + private SimpleActionGroup action_group; /** * Constructs a new {@link Granite.AlertDialog}. @@ -126,13 +127,7 @@ public class Granite.AlertDialog : Gtk.Window { bind_property ("title", header_label, "label", SYNC_CREATE); bind_property ("secondary-text", header_label, "secondary-text", SYNC_CREATE); - var response_action = new SimpleAction ("response", VariantType.STRING); - response_action.activate.connect ((parameter) => { - response (parameter.get_string ()); - }); - - var action_group = new SimpleActionGroup (); - action_group.add_action (response_action); + action_group = new SimpleActionGroup (); insert_action_group ("dialog", action_group); @@ -140,9 +135,15 @@ public class Granite.AlertDialog : Gtk.Window { } public void add_button (string label, string response_id, ButtonStyle button_style = DEFAULT) { + var response_action = new SimpleAction (response_id, null); + response_action.activate.connect (() => { + response (response_id); + }); + + action_group.add_action (response_action); + var button = new Gtk.Button.with_label (label) { - action_name = "dialog.response", - action_target = new Variant.string (response_id), + action_name = "dialog." + response_id, use_underline = true }; @@ -152,4 +153,14 @@ public class Granite.AlertDialog : Gtk.Window { button_box.append (button); } + + /** + * Set whether a response is enabled. The corresponding button will have {@link Gtk.Widget.sensitive} set accordingly. + * + * Responses are enabled by default + */ + public void set_response_enabled (string response_id, bool enabled) { + var action = (SimpleAction) action_group.lookup_action (response_id); + action.set_enabled (enabled); + } }