1/*
2 * Copyright 2017–2019 elementary, Inc. (https://elementary.io)
3 * SPDX-License-Identifier: GPL-2.0-or-later
4 */
5
6/**
7 * MessageDialog is an elementary OS styled dialog used to display a message to the user.
8 *
9 * The class itself is similar to it's Gtk equivalent {@link Gtk.MessageDialog}
10 * but follows elementary OS design conventions.
11 *
12 * See [[https://elementary.io/docs/human-interface-guidelines#dialogs|The Human Interface Guidelines for dialogs]]
13 * for more detailed disscussion on the dialog wording and design.
14 *
15 * ''Example''<<BR>>
16 * {{{
17 *   var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (
18 *      "This is a primary text",
19 *      "This is a secondary, multiline, long text. This text usually extends the primary text and prints e.g: the details of an error.",
20 *      "applications-development",
21 *      Gtk.ButtonsType.CLOSE
22 *   );
23 *
24 *   var custom_widget = new Gtk.CheckButton.with_label ("Custom widget");
25 *   custom_widget.show ();
26 *
27 *   message_dialog.custom_bin.add (custom_widget);
28 *   message_dialog.run ();
29 *   message_dialog.destroy ();
30 * }}}
31 *
32 * {{../doc/images/MessageDialog.png}}
33 */
34public class Granite.MessageDialog : Granite.Dialog {
35    /**
36     * The primary text, title of the dialog.
37     */
38    public string primary_text {
39        get {
40            return primary_label.label;
41        }
42
43        set {
44            primary_label.label = value;
45        }
46    }
47
48    /**
49     * The secondary text, body of the dialog.
50     */
51    public string secondary_text {
52        get {
53            return secondary_label.label;
54        }
55
56        set {
57            secondary_label.label = value;
58        }
59    }
60
61    /**
62     * The {@link GLib.Icon} that is used to display the image
63     * on the left side of the dialog.
64     */
65    public GLib.Icon image_icon {
66        owned get {
67            return image.gicon;
68        }
69
70        set {
71            image.set_from_gicon (value, Gtk.IconSize.DIALOG);
72        }
73    }
74
75    /**
76     * The {@link GLib.Icon} that is used to display a badge, bottom-end aligned,
77     * over the image on the left side of the dialog.
78     */
79    public GLib.Icon badge_icon {
80        owned get {
81            return badge.gicon;
82        }
83
84        set {
85            badge.set_from_gicon (value, Gtk.IconSize.LARGE_TOOLBAR);
86        }
87    }
88
89    /**
90     * The {@link Gtk.Label} that displays the {@link Granite.MessageDialog.primary_text}.
91     *
92     * Most of the times, you will only want to modify the {@link Granite.MessageDialog.primary_text} string,
93     * this is available to set additional properites like {@link Gtk.Label.use_markup} if you wish to do so.
94     */
95    public Gtk.Label primary_label { get; construct; }
96
97    /**
98     * The {@link Gtk.Label} that displays the {@link Granite.MessageDialog.secondary_text}.
99     *
100     * Most of the times, you will only want to modify the {@link Granite.MessageDialog.secondary_text} string,
101     * this is available to set additional properites like {@link Gtk.Label.use_markup} if you wish to do so.
102     */
103    public Gtk.Label secondary_label { get; construct; }
104
105    /**
106     * The {@link Gtk.ButtonsType} value to display a set of buttons
107     * in the dialog.
108     *
109     * By design, some actions are not acceptable and such action values will not be added to the dialog, these include:
110     *
111     *  * {@link Gtk.ButtonsType.OK}
112     *  * {@link Gtk.ButtonsType.YES_NO}
113     *  * {@link Gtk.ButtonsType.OK_CANCEL}
114     *
115     * If you wish to provide more specific actions for your dialog
116     * pass a {@link Gtk.ButtonsType.NONE} to {@link Granite.MessageDialog.MessageDialog} and manually
117     * add those actions with {@link Gtk.Dialog.add_buttons} or {@link Gtk.Dialog.add_action_widget}.
118     */
119    public Gtk.ButtonsType buttons {
120        construct {
121            switch (value) {
122                case Gtk.ButtonsType.NONE:
123                    break;
124                case Gtk.ButtonsType.CLOSE:
125                    add_button (_("_Close"), Gtk.ResponseType.CLOSE);
126                    break;
127                case Gtk.ButtonsType.CANCEL:
128                    add_button (_("_Cancel"), Gtk.ResponseType.CANCEL);
129                    break;
130                case Gtk.ButtonsType.OK:
131                case Gtk.ButtonsType.YES_NO:
132                case Gtk.ButtonsType.OK_CANCEL:
133                    warning ("Unsupported GtkButtonsType value");
134                    break;
135                default:
136                    warning ("Unknown GtkButtonsType value");
137                    break;
138            }
139        }
140    }
141
142    /**
143     * The custom area to add custom widgets.
144     *
145     * This bin can be used to add any custom widget to the message area such as a {@link Gtk.ComboBox} or {@link Gtk.CheckButton}.
146     * Note that after adding such widget you will need to call {@link Gtk.Widget.show} or {@link Gtk.Widget.show_all} on the widget itself for it to appear in the dialog.
147     *
148     * If you want to add multiple widgets to this area, create a new container such as {@link Gtk.Grid} or {@link Gtk.Box} and then add it to the custom bin.
149     *
150     * When adding a custom widget to the custom bin, the {@link Granite.MessageDialog.secondary_label}'s bottom margin will be expanded automatically
151     * to compensate for the additional widget in the dialog.
152     * Removing the previously added widget will remove the bottom margin.
153     *
154     * If you don't want to have any margin between your custom widget and the {@link Granite.MessageDialog.secondary_label}, simply add your custom widget
155     * and then set the {@link Gtk.Label.margin_bottom} of {@link Granite.MessageDialog.secondary_label} to 0.
156     */
157    public Gtk.Bin custom_bin { get; construct; }
158
159    /**
160     * The image that's displayed in the dialog.
161     */
162    private Gtk.Image image;
163
164    /**
165     * The badge that's displayed in the dialog.
166     */
167    private Gtk.Image badge;
168
169    /**
170     * The main grid that's used to contain all dialog widgets.
171     */
172    private Gtk.Grid message_grid;
173
174    /**
175     * The {@link Gtk.TextView} used to display an additional error message.
176     */
177    private Gtk.TextView? details_view;
178
179    /**
180     * The {@link Gtk.Expander} used to hold the error details view.
181     */
182    private Gtk.Expander? expander;
183
184    /**
185     * SingleWidgetBin is only used within this class for creating a Bin that
186     * holds only one widget.
187     */
188    private class SingleWidgetBin : Gtk.Bin {}
189
190    /**
191     * Constructs a new {@link Granite.MessageDialog}.
192     * See {@link Granite.Dialog} for more details.
193     *
194     * @param primary_text the title of the dialog
195     * @param secondary_text the body of the dialog
196     * @param image_icon the {@link GLib.Icon} that is displayed on the left side of the dialog
197     * @param buttons the {@link Gtk.ButtonsType} value that decides what buttons to use, defaults to {@link Gtk.ButtonsType.CLOSE},
198     *        see {@link Granite.MessageDialog.buttons} on details and what values are accepted
199     */
200    public MessageDialog (
201        string primary_text,
202        string secondary_text,
203        GLib.Icon image_icon,
204        Gtk.ButtonsType buttons = Gtk.ButtonsType.CLOSE
205    ) {
206        Object (
207            primary_text: primary_text,
208            secondary_text: secondary_text,
209            image_icon: image_icon,
210            buttons: buttons
211        );
212    }
213
214    /**
215     * Constructs a new {@link Granite.MessageDialog} with an icon name as it's icon displayed in the image.
216     * This constructor is same as the main one but with a difference that
217     * you can pass an icon name string instead of manually creating the {@link GLib.Icon}.
218     *
219     * The {@link Granite.MessageDialog.image_icon} will store the created icon
220     * so you can retrieve it later with {@link GLib.Icon.to_string}.
221     *
222     * See {@link Gtk.Dialog} for more details.
223     *
224     * @param primary_text the title of the dialog
225     * @param secondary_text the body of the dialog
226     * @param image_icon_name the icon name to create the dialog image with
227     * @param buttons the {@link Gtk.ButtonsType} value that decides what buttons to use, defaults to {@link Gtk.ButtonsType.CLOSE},
228     *        see {@link Granite.MessageDialog.buttons} on details and what values are accepted
229     */
230    public MessageDialog.with_image_from_icon_name (
231        string primary_text,
232        string secondary_text,
233        string image_icon_name = "dialog-information",
234        Gtk.ButtonsType buttons = Gtk.ButtonsType.CLOSE
235    ) {
236        Object (
237            primary_text: primary_text,
238            secondary_text: secondary_text,
239            image_icon: new ThemedIcon (image_icon_name),
240            buttons: buttons
241        );
242    }
243
244    static construct {
245        Granite.init ();
246    }
247
248    class construct {
249        set_css_name (Gtk.STYLE_CLASS_MESSAGE_DIALOG);
250    }
251
252    construct {
253        resizable = false;
254        deletable = false;
255        skip_taskbar_hint = true;
256
257        image = new Gtk.Image ();
258
259        badge = new Gtk.Image ();
260        badge.halign = badge.valign = Gtk.Align.END;
261        badge.pixel_size = 24;
262
263        var overlay = new Gtk.Overlay ();
264        overlay.valign = Gtk.Align.START;
265        overlay.add (image);
266        overlay.add_overlay (badge);
267
268        primary_label = new Gtk.Label (null);
269        primary_label.get_style_context ().add_class (Granite.STYLE_CLASS_PRIMARY_LABEL);
270        primary_label.selectable = true;
271        primary_label.max_width_chars = 50;
272        primary_label.wrap = true;
273        primary_label.xalign = 0;
274
275        secondary_label = new Gtk.Label (null);
276        secondary_label.use_markup = true;
277        secondary_label.selectable = true;
278        secondary_label.max_width_chars = 50;
279        secondary_label.wrap = true;
280        secondary_label.xalign = 0;
281
282        custom_bin = new SingleWidgetBin ();
283        custom_bin.add.connect (() => {
284            secondary_label.margin_bottom = 18;
285            if (expander != null) {
286                custom_bin.margin_top = 6;
287            }
288        });
289
290        custom_bin.remove.connect (() => {
291            secondary_label.margin_bottom = 0;
292
293            if (expander != null) {
294                custom_bin.margin_top = 0;
295            }
296        });
297
298        message_grid = new Gtk.Grid ();
299        message_grid.column_spacing = 12;
300        message_grid.row_spacing = 6;
301        message_grid.margin_start = message_grid.margin_end = 12;
302        message_grid.attach (overlay, 0, 0, 1, 2);
303        message_grid.attach (primary_label, 1, 0, 1, 1);
304        message_grid.attach (secondary_label, 1, 1, 1, 1);
305        message_grid.attach (custom_bin, 1, 3, 1, 1);
306        message_grid.show_all ();
307
308        get_content_area ().add (message_grid);
309    }
310
311    /**
312     * Shows a terminal-like widget for error details that can be expanded by the user.
313     *
314     * This method can be useful to provide the user extended error details in a
315     * terminal-like text view. Calling this method will not add any widgets to the
316     * {@link Granite.MessageDialog.custom_bin}.
317     *
318     * Subsequent calls to this method will change the error message to a new one.
319     *
320     * @param error_message the detailed error message to display
321     */
322    public void show_error_details (string error_message) {
323        if (details_view == null) {
324            secondary_label.margin_bottom = 18;
325
326            details_view = new Gtk.TextView ();
327            details_view.border_width = 6;
328            details_view.editable = false;
329            details_view.pixels_below_lines = 3;
330            details_view.wrap_mode = Gtk.WrapMode.WORD;
331            details_view.get_style_context ().add_class (Granite.STYLE_CLASS_TERMINAL);
332
333            var scroll_box = new Gtk.ScrolledWindow (null, null);
334            scroll_box.margin_top = 12;
335            scroll_box.min_content_height = 70;
336            scroll_box.add (details_view);
337
338            expander = new Gtk.Expander (_("Details"));
339            expander.add (scroll_box);
340
341            message_grid.attach (expander, 1, 2, 1, 1);
342            message_grid.show_all ();
343
344            if (custom_bin.get_child () != null) {
345                custom_bin.margin_top = 12;
346            }
347        }
348
349        details_view.buffer.text = error_message;
350    }
351}
352