Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
alert.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Guillaume Stutin.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19#include "gui/alert.h"
20
21#include "gui/application.h"
22#include "system/macros.h"
23#include "system/mem_alloc.h"
24#include "widgets/dialog.h"
27
28#include <glib/gi18n.h>
29#include <gtk/gtk.h>
30
31#ifdef GDK_WINDOWING_QUARTZ
32#include "osx/osx.h" // conditional-ok: its calls below are under the same test
33#endif
34
36{
39 GtkWidget *column; // right of the icon: the heading, then what the caller adds, then the buttons
40 GtkWidget *buttons; // NULL until the first button is added
41 GtkWidget *focus; // the first button, which has the focus when the window shows
42 dt_gui_alert_callback_t cancel; // a QUESTION's answer to Escape and the title bar
44};
45
46// One per button, freed with its "clicked" closure.
53
54// The window owns the alert: whatever destroys it -- dt_gui_alert_destroy(), or the user closing
55// a NOTICE -- frees it.
56static void _alert_window_destroyed(GtkWidget *window __attribute__((unused)), gpointer user_data)
57{
58 dt_gui_alert_t *alert = (dt_gui_alert_t *)user_data;
59 // A notice closed by the user hands the focus back to where they were working. A question or a
60 // progress leaves that to its owner, who knows what comes next.
62 dt_free(alert);
63}
64
65static void _alert_cancel(dt_gui_alert_t *alert)
66{
67 if(!IS_NULL_PTR(alert->cancel)) alert->cancel(alert, alert->cancel_data);
68}
69
70static gboolean _alert_delete(GtkWidget *window __attribute__((unused)), GdkEvent *event __attribute__((unused)),
71 gpointer user_data)
72{
73 dt_gui_alert_t *alert = (dt_gui_alert_t *)user_data;
74 switch(alert->kind)
75 {
77 return FALSE; // closed, as any window
79 _alert_cancel(alert);
80 return TRUE;
82 default:
83 // Closing it would stop nothing: it offers no button to, and refuses a delete-event sent
84 // some other way (Alt+F4, a task bar).
85 return TRUE;
86 }
87}
88
89static gboolean _alert_key(GtkWidget *window, const GdkEventKey *event, gpointer user_data)
90{
91 if(event->keyval != GDK_KEY_Escape) return FALSE;
92 dt_gui_alert_t *alert = (dt_gui_alert_t *)user_data;
93 switch(alert->kind)
94 {
96 gtk_widget_destroy(window);
97 return TRUE;
99 _alert_cancel(alert);
100 return TRUE;
102 default:
103 return FALSE;
104 }
105}
106
107dt_gui_alert_t *dt_gui_alert_new(const dt_gui_alert_kind_t kind, const char *title, const char *heading)
108{
109 dt_gui_alert_t *alert = g_new0(dt_gui_alert_t, 1);
110 alert->kind = kind;
111
112 GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
113 alert->window = window;
114#ifdef GDK_WINDOWING_QUARTZ
115 // Like every other window of ours: it must not open as a full-screen space of its own, and
116 // it must show over the one the main window may have left.
118#endif
119 gtk_window_set_icon_name(GTK_WINDOW(window), "ansel");
120 gtk_window_set_title(GTK_WINDOW(window), title);
121 gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
122
123 GtkWidget *main_window = dt_gui_main_window();
124 GtkWidget *icon = NULL;
125 switch(kind)
126 {
128 // Not modal: the user may go on working, and the window waits for them above the main window.
129 // Being transient for it is what keeps it there, and only there. Not gtk_window_set_keep_above():
130 // on Windows that is HWND_TOPMOST, above every application on the desktop.
131 if(GTK_IS_WINDOW(main_window)) gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(main_window));
132 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ON_PARENT);
133 icon = gtk_image_new_from_icon_name("dialog-warning", GTK_ICON_SIZE_DIALOG);
134 break;
136 // The main window is still up, and nothing in it may change while the question is open.
137 if(GTK_IS_WINDOW(main_window)) gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(main_window));
138 gtk_window_set_modal(GTK_WINDOW(window), TRUE);
139 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ON_PARENT);
140 icon = gtk_image_new_from_icon_name("dialog-warning", GTK_ICON_SIZE_DIALOG);
141 break;
143 default:
144 {
145 // The main window may be hidden: centred on the screen, not on it.
146 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
147 gtk_window_set_deletable(GTK_WINDOW(window), FALSE);
148 // A window group of its own. A grab held in the default group, the one every other window
149 // of ours is in -- as dt_gui_closing_wait() holds one -- would take this window's clicks
150 // too, and what the caller added could not be used.
151 GtkWindowGroup *group = gtk_window_group_new();
152 gtk_window_group_add_window(group, GTK_WINDOW(window));
153 g_object_unref(group);
154 icon = gtk_spinner_new();
155 gtk_spinner_start(GTK_SPINNER(icon));
156 break;
157 }
158 }
159
160 g_signal_connect(window, "delete-event", G_CALLBACK(_alert_delete), alert);
161 g_signal_connect(window, "key-press-event", G_CALLBACK(_alert_key), alert);
162 g_signal_connect(window, "destroy", G_CALLBACK(_alert_window_destroyed), alert);
163
164 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_PIXEL_APPLY_DPI(12));
165 gtk_container_set_border_width(GTK_CONTAINER(box), DT_PIXEL_APPLY_DPI(16));
166 gtk_container_add(GTK_CONTAINER(window), box);
167
168 gtk_widget_set_valign(icon, GTK_ALIGN_START);
169 gtk_box_pack_start(GTK_BOX(box), icon, FALSE, FALSE, 0);
170
171 alert->column = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_PIXEL_APPLY_DPI(8));
172 gtk_box_pack_start(GTK_BOX(box), alert->column, TRUE, TRUE, 0);
173
174 GtkWidget *label = gtk_label_new(NULL);
175 gchar *markup = g_markup_printf_escaped("<b>%s</b>", heading);
176 gtk_label_set_markup(GTK_LABEL(label), markup);
177 dt_free(markup);
178 gtk_label_set_xalign(GTK_LABEL(label), 0.0);
179 gtk_box_pack_start(GTK_BOX(alert->column), label, FALSE, FALSE, 0);
180
181 return alert;
182}
183
185{
186 GtkWidget *label = gtk_label_new(text);
187 gtk_label_set_xalign(GTK_LABEL(label), 0.0);
188 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
189 gtk_label_set_max_width_chars(GTK_LABEL(label), 60);
190 gtk_box_pack_start(GTK_BOX(alert->column), label, FALSE, FALSE, 0);
191 return label;
192}
193
195{
196 gtk_box_pack_start(GTK_BOX(alert->column), widget, FALSE, FALSE, 0);
197}
198
199static void _alert_button_clicked(GtkButton *button __attribute__((unused)), gpointer user_data)
200{
201 const dt_gui_alert_button_t *clicked = (const dt_gui_alert_button_t *)user_data;
202 // The callback may destroy the alert: nothing touches it after.
203 clicked->callback(clicked->alert, clicked->data);
204}
205
206void dt_gui_alert_add_button(dt_gui_alert_t *alert, const char *label, dt_gui_alert_callback_t callback,
207 void *data)
208{
209 // The row is packed from the end of the column, so that it stays at the bottom whatever is
210 // added after it.
211 if(IS_NULL_PTR(alert->buttons))
212 {
213 alert->buttons = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
214 gtk_button_box_set_layout(GTK_BUTTON_BOX(alert->buttons), GTK_BUTTONBOX_END);
215 gtk_box_set_spacing(GTK_BOX(alert->buttons), DT_PIXEL_APPLY_DPI(6));
216 gtk_widget_set_margin_top(alert->buttons, DT_PIXEL_APPLY_DPI(6));
217 gtk_box_pack_end(GTK_BOX(alert->column), alert->buttons, FALSE, FALSE, 0);
218 }
219
220 GtkWidget *button = gtk_button_new_with_label(label);
221 dt_gui_alert_button_t *clicked = g_new0(dt_gui_alert_button_t, 1);
222 clicked->alert = alert;
223 clicked->callback = callback;
224 clicked->data = data;
225 g_signal_connect_data(button, "clicked", G_CALLBACK(_alert_button_clicked), clicked, (GClosureNotify)g_free, 0);
226 gtk_container_add(GTK_CONTAINER(alert->buttons), button);
227 if(IS_NULL_PTR(alert->focus)) alert->focus = button;
228}
229
231{
232 alert->cancel = callback;
233 alert->cancel_data = data;
234}
235
237{
238 // The button takes the focus before the window shows. A window shown with no focus gives it to the
239 // first widget that takes it -- a selectable line of a dt_gui_alert() list -- and a selectable
240 // label selects all its text as it gets the focus, which a later grab does not unselect.
241 if(!IS_NULL_PTR(alert->focus)) gtk_widget_grab_focus(alert->focus);
242 gtk_widget_show_all(alert->window);
243 gtk_window_present(GTK_WINDOW(alert->window));
244#ifdef GDK_WINDOWING_QUARTZ
245 // A question or a progress answers the user, but not always from the application in front: the
246 // Dock's Quit, or Cmd+Q through the application switcher, leave another one active, and a window
247 // opened by an application in the background opens behind the windows of the one that is not.
248 // A notice comes unasked, and does not take the focus from another application.
250#endif
251}
252
254{
255 gtk_widget_destroy(alert->window); // frees the alert, see _alert_window_destroyed()
256}
257
258/* --- dt_gui_alert(): a NOTICE with one OK button, from any thread --------- */
259
260typedef struct dt_alert_message_t
261{
262 gchar *title;
263 gchar *message;
264 gchar *item;
265 gboolean once;
267
268// The dt_gui_alert() windows on screen, by title and message: the box that lists each one's items.
269// GUI thread only.
270static GHashTable *_alert_windows = NULL;
271// The titles and messages raised `once` whose window has been closed: they open no other. GUI
272// thread only.
273static GHashTable *_alert_closed = NULL;
274
275static void _alert_message_free(gpointer data)
276{
277 dt_alert_message_t *message = (dt_alert_message_t *)data;
278 dt_free(message->title);
279 dt_free(message->message);
280 dt_free(message->item);
281 dt_free(message);
282}
283
284// The list goes with its window, whatever closes it -- OK, its title bar, Escape: the message is
285// free for the next alert, unless it was raised `once`.
286static void _alert_window_items_destroyed(GtkWidget *block, gpointer user_data)
287{
288 if(g_object_get_data(G_OBJECT(block), "once"))
289 g_hash_table_add(_alert_closed, g_strdup((const gchar *)user_data));
290 g_hash_table_remove(_alert_windows, (const gchar *)user_data);
291}
292
293static void _alert_ok_clicked(dt_gui_alert_t *alert, void *data __attribute__((unused)))
294{
296}
297
298// The list, one item per line: each item's label ends with the line break of its empty line.
299static void _alert_items_copy(GtkButton *button __attribute__((unused)), gpointer user_data)
300{
301 GString *text = g_string_new(NULL);
302 GList *labels = gtk_container_get_children(GTK_CONTAINER(user_data));
303 for(GList *label = labels; label; label = g_list_next(label))
304 g_string_append(text, gtk_label_get_text(GTK_LABEL(label->data)));
305 g_list_free(labels);
306 // GDK_SELECTION_CLIPBOARD is the explicit-copy clipboard on all backends, not X11's PRIMARY.
307 GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
308 gtk_clipboard_set_text(clipboard, text->str, -1);
309 gtk_clipboard_store(clipboard);
310 g_string_free(text, TRUE);
311}
312
313static gboolean _alert_show(gpointer user_data)
314{
315 const dt_alert_message_t *message = (const dt_alert_message_t *)user_data;
317 {
318 _alert_windows = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
319 _alert_closed = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
320 }
321
322 // One window per kind of message: its title and its text, which say what went wrong, never where.
323 // The same pair again while its window is up only adds its item to that window's list. A title
324 // is one line: the first line break of the key ends it.
325 gchar *key = g_strconcat(message->title, "\n", message->message, NULL);
326 dt_gui_alert_t *alert = NULL;
327 GtkWidget *block = g_hash_table_lookup(_alert_windows, key);
328 if(IS_NULL_PTR(block))
329 {
330 // Raised `once` and closed already: the window is not opened again. One that is up -- opened
331 // by an alert that was not `once` -- still lists the item.
332 if(message->once && g_hash_table_contains(_alert_closed, key))
333 {
334 dt_free(key);
335 return G_SOURCE_REMOVE;
336 }
337 alert = dt_gui_alert_new(DT_GUI_ALERT_NOTICE, message->title, message->title);
338 dt_gui_alert_add_text(alert, message->message);
339 // Under the message, the list of its items, which appears with the first one.
340 block = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
341 dt_gui_alert_add_widget(alert, block);
342 dt_gui_alert_add_button(alert, _("OK"), _alert_ok_clicked, NULL);
343 // Each item once: a module failing on the same image at every change in the darkroom lists
344 // that image once.
345 g_object_set_data_full(G_OBJECT(block), "listed",
346 g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL),
347 (GDestroyNotify)g_hash_table_destroy);
348
349 // The table takes the key; the destroy handler gets a copy of its own, freed with the closure.
350 g_signal_connect_data(block, "destroy", G_CALLBACK(_alert_window_items_destroyed), g_strdup(key),
351 (GClosureNotify)g_free, 0);
352 g_hash_table_insert(_alert_windows, key, block);
353 }
354 else
355 dt_free(key);
356
357 // A window that a `once` message reached stays closed for the next ones; the others still reopen it.
358 if(message->once) g_object_set_data(G_OBJECT(block), "once", GINT_TO_POINTER(TRUE));
359
360 gboolean added = FALSE;
361 if(!IS_NULL_PTR(message->item)
362 && g_hash_table_add(g_object_get_data(G_OBJECT(block), "listed"), g_strdup(message->item)))
363 {
364 // The list, as the closing window's: as tall as its lines up to a point, then it scrolls -- a
365 // module failing on every thumbnail of a film roll lists hundreds of images.
366 GtkWidget *items = g_object_get_data(G_OBJECT(block), "items");
367 if(IS_NULL_PTR(items))
368 {
369 items = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
370 GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
371 dt_gui_add_class(scroll, "dt_recessed_scroll");
372 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
373 gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(scroll), DT_PIXEL_APPLY_DPI(150));
374 gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(scroll), TRUE);
375 gtk_container_add(GTK_CONTAINER(scroll), items);
376 // Above the list, on its right, a button that copies it whole, to paste in a bug report.
377 GtkWidget *copy = gtk_button_new_from_icon_name("edit-copy-symbolic", GTK_ICON_SIZE_SMALL_TOOLBAR);
378 gtk_widget_set_tooltip_text(copy, _("copy to clipboard"));
379 gtk_widget_set_halign(copy, GTK_ALIGN_END);
380 g_signal_connect(copy, "clicked", G_CALLBACK(_alert_items_copy), items);
381 gtk_box_pack_start(GTK_BOX(block), copy, FALSE, FALSE, 0);
382 gtk_box_pack_start(GTK_BOX(block), scroll, FALSE, FALSE, 0);
383 g_object_set_data(G_OBJECT(block), "items", items);
384 }
385 // A path has no spaces to break at: it wraps anywhere, whole, and can be selected to be copied.
386 // An empty line ends each item, so that one wrapped over several lines stays apart from the next.
387 gchar *text = g_strconcat(message->item, "\n", NULL);
388 GtkWidget *label = gtk_label_new(text);
389 dt_free(text);
390 gtk_label_set_xalign(GTK_LABEL(label), 0.0);
391 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
392 gtk_label_set_line_wrap_mode(GTK_LABEL(label), PANGO_WRAP_WORD_CHAR);
393 gtk_label_set_max_width_chars(GTK_LABEL(label), 60);
394 gtk_label_set_selectable(GTK_LABEL(label), TRUE);
395 gtk_box_pack_start(GTK_BOX(items), label, FALSE, FALSE, 0);
396 added = TRUE;
397 }
398
399 // A new window shows once it holds its first item, to be centred with it. A repeat that adds
400 // nothing leaves the window where it is: the darkroom runs its pipe again at every change, and
401 // the window would take the focus back each time.
402 if(!IS_NULL_PTR(alert))
403 dt_gui_alert_show(alert);
404 else if(added)
405 {
406 gtk_widget_show_all(block);
407 gtk_window_present(GTK_WINDOW(gtk_widget_get_toplevel(block)));
408 }
409 return G_SOURCE_REMOVE;
410}
411
412void dt_gui_alert(const char *title, const char *message, const char *item, const gboolean once)
413{
415 copy->title = g_strdup(title);
416 copy->message = g_strdup(message);
417 copy->item = g_strdup(item);
418 copy->once = once;
419 // Always deferred, even on the GUI thread: a caller may hold a lock -- the pixelpipe cache
420 // alerts from under its own -- and building a window must not run inside it.
421 g_idle_add_full(G_PRIORITY_DEFAULT, _alert_show, copy, _alert_message_free);
422}
423
424// clang-format off
425// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
426// vim: shiftwidth=2 expandtab tabstop=2 cindent
427// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
428// clang-format on
static GHashTable * _alert_closed
Definition alert.c:273
void dt_gui_alert_destroy(dt_gui_alert_t *alert)
Definition alert.c:253
static GHashTable * _alert_windows
Definition alert.c:270
dt_gui_alert_t * dt_gui_alert_new(const dt_gui_alert_kind_t kind, const char *title, const char *heading)
Definition alert.c:107
static void _alert_ok_clicked(dt_gui_alert_t *alert, void *data __attribute__((unused)))
Definition alert.c:293
void dt_gui_alert_add_widget(dt_gui_alert_t *alert, GtkWidget *widget)
Definition alert.c:194
static void _alert_button_clicked(GtkButton *button __attribute__((unused)), gpointer user_data)
Definition alert.c:199
void dt_gui_alert(const char *title, const char *message, const char *item, const gboolean once)
Definition alert.c:412
GtkWidget * dt_gui_alert_add_text(dt_gui_alert_t *alert, const char *text)
Definition alert.c:184
static void _alert_window_items_destroyed(GtkWidget *block, gpointer user_data)
Definition alert.c:286
static gboolean _alert_delete(GtkWidget *window __attribute__((unused)), GdkEvent *event __attribute__((unused)), gpointer user_data)
Definition alert.c:70
static void _alert_message_free(gpointer data)
Definition alert.c:275
static gboolean _alert_key(GtkWidget *window, const GdkEventKey *event, gpointer user_data)
Definition alert.c:89
void dt_gui_alert_set_cancel(dt_gui_alert_t *alert, dt_gui_alert_callback_t callback, void *data)
Definition alert.c:230
static void _alert_items_copy(GtkButton *button __attribute__((unused)), gpointer user_data)
Definition alert.c:299
void dt_gui_alert_add_button(dt_gui_alert_t *alert, const char *label, dt_gui_alert_callback_t callback, void *data)
Definition alert.c:206
void dt_gui_alert_show(dt_gui_alert_t *alert)
Definition alert.c:236
static void _alert_cancel(dt_gui_alert_t *alert)
Definition alert.c:65
static gboolean _alert_show(gpointer user_data)
Definition alert.c:313
static void _alert_window_destroyed(GtkWidget *window __attribute__((unused)), gpointer user_data)
Definition alert.c:56
dt_gui_alert_kind_t
Definition alert.h:36
@ DT_GUI_ALERT_PROGRESS
Definition alert.h:46
@ DT_GUI_ALERT_QUESTION
Definition alert.h:42
@ DT_GUI_ALERT_NOTICE
Definition alert.h:39
void(* dt_gui_alert_callback_t)(dt_gui_alert_t *alert, void *data)
Definition alert.h:50
GtkWidget * dt_gui_main_window(void)
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
static const dt_adaptation_t kind
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
void dt_gui_refocus_parent(GtkWindow *parent)
Definition dialog.c:64
GtkWidget * window
const dt_collection_sort_t items[]
Definition filter.c:102
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
char * key
void dt_osx_focus_window()
Definition osx.mm:289
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:105
void copy(TYPE *dest, TYPE *source, size_t num_el)
Copy a flat buffer used to initialize test matrices.
float dt_aligned_pixel_simd_t __attribute__((vector_size(16), aligned(16)))
Apply one channel's tone curve to each of the three colour channels, or pass the channel through unto...
Definition simd.h:55
gchar * title
Definition alert.c:262
gchar * message
Definition alert.c:263
gboolean once
Definition alert.c:265
dt_gui_alert_t * alert
Definition alert.c:49
dt_gui_alert_callback_t callback
Definition alert.c:50
GtkWidget * column
Definition alert.c:39
void * cancel_data
Definition alert.c:43
dt_gui_alert_callback_t cancel
Definition alert.c:42
GtkWidget * buttons
Definition alert.c:40
GtkWidget * focus
Definition alert.c:41
dt_gui_alert_kind_t kind
Definition alert.c:37
GtkWidget * window
Definition alert.c:38
#define DT_PIXEL_APPLY_DPI(value)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)