diff --git a/demo/Views/DialogsView.vala b/demo/Views/DialogsView.vala index fd3f279d3..70b0123f2 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,54 @@ 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.set_response_enabled ("accept", false); + + dialog.response.connect ((response_id) => { + if (response_id == "accept") { + toast.send_notification (); + } + + dialog.destroy (); + }); + + dialog.present (); + + Timeout.add_once (3000, () => { + dialog.set_response_enabled ("accept", true); + }); + } + 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..d5c4927fd --- /dev/null +++ b/lib/Widgets/AlertDialog.vala @@ -0,0 +1,166 @@ +/* + * 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; + private SimpleActionGroup action_group; + + /** + * 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); + + action_group = new SimpleActionGroup (); + + 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 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_id, + use_underline = true + }; + + if (button_style != DEFAULT) { + button.add_css_class (button_style.to_string ()); + } + + 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); + } +} 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