Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
closing.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/closing.h"
20
21#include "common/logging.h"
22#include "control/control.h"
23#include "control/jobs.h"
24#include "control/progress.h"
25#include "gui/application.h"
26#include "system/macros.h"
27#include "system/mem_alloc.h"
28#include "widgets/dialog.h"
31
32#include <glib/gi18n.h>
33#include <gtk/gtk.h>
34
35#ifdef GDK_WINDOWING_QUARTZ
36#include "osx/osx.h" // conditional-ok: its two calls below are under the same test
37#endif
38
39// A quit that is over within this long shows nothing: a window that flashes says less than none.
40#define DT_CLOSING_NOTICE_DELAY (G_USEC_PER_SEC)
41// How often the workers are counted, and the window brought up to date, in milliseconds.
42#define DT_CLOSING_POLL_INTERVAL 100
43// A job's kind, as the details name it, is the queue it was added to. The reserved worker, which
44// only the darkroom uses, comes after the queues.
45#define DT_CLOSING_KIND_DARKROOM DT_JOB_QUEUE_MAX
46
47// The same window serves twice. Before the quit, it asks whether to quit while background tasks
48// are running; once the quit is under way, it waits for the running jobs.
54
55enum
56{
57 DT_CLOSING_COL_KIND, // the job's kind
58 DT_CLOSING_COL_DESCRIPTION, // the description it was created with
60};
61
62typedef struct dt_closing_t
63{
65 gboolean quit; // the answer, in DT_CLOSING_CONFIRM
67 gint64 start;
69 GtkWidget *count; // how many jobs are still running
70 GtkWidget *names; // what the ones that publish a progress say they are doing
71 GtkWidget *details; // the expander that lists the running jobs, hidden while there is nothing in it
72 GtkListStore *store; // its rows, owned by the view inside it
73 gchar *listed; // what the list shows, so that it is rebuilt only when that changes
74 GtkWidget *dropped; // under the list, how many queued jobs the quit drops
75 int32_t queued; // what it says, -1 before it says anything
77
78typedef struct dt_closing_job_t
79{
80 int kind;
81 gchar *description; // translated when the catalog knows it
83
86 gpointer user_data __attribute__((unused)))
87{
88 return TRUE;
89}
90
91static void _closing_answer(dt_closing_t *closing, const gboolean quit)
92{
93 closing->quit = quit;
94 g_main_loop_quit(closing->loop);
95}
96
97static void _closing_quit_clicked(GtkButton *button __attribute__((unused)), gpointer user_data)
98{
99 _closing_answer((dt_closing_t *)user_data, TRUE);
100}
101
102static void _closing_back_clicked(GtkButton *button __attribute__((unused)), gpointer user_data)
103{
104 _closing_answer((dt_closing_t *)user_data, FALSE);
105}
106
107// Closing the question, by its title bar or by Escape, is going back.
109 GdkEvent *event __attribute__((unused)), gpointer user_data)
110{
111 _closing_answer((dt_closing_t *)user_data, FALSE);
112 return TRUE;
113}
114
115static gboolean _closing_confirm_key(GtkWidget *widget __attribute__((unused)), const GdkEventKey *event,
116 gpointer user_data)
117{
118 if(event->keyval != GDK_KEY_Escape) return FALSE;
119 _closing_answer((dt_closing_t *)user_data, FALSE);
120 return TRUE;
121}
122
124{
125 const gboolean confirm = closing->mode == DT_CLOSING_CONFIRM;
127#ifdef GDK_WINDOWING_QUARTZ
128 // Like every other window of ours: it must not open as a full-screen space of its own, and
129 // it must show over the one the main window may have left.
131#endif
132 gtk_window_set_icon_name(GTK_WINDOW(closing->window), "ansel");
133 gtk_window_set_title(GTK_WINDOW(closing->window), confirm ? _("Quit Ansel?") : _("Closing Ansel..."));
135 if(confirm)
136 {
137 // The main window is still up, and nothing in it may change while the question is open.
141 g_signal_connect(closing->window, "delete-event", G_CALLBACK(_closing_confirm_delete), closing);
142 g_signal_connect(closing->window, "key-press-event", G_CALLBACK(_closing_confirm_key), closing);
143 }
144 else
145 {
147 // Closing this window would stop nothing, so it offers no button to, and refuses a
148 // delete-event sent some other way (Alt+F4, a task bar).
150 g_signal_connect(closing->window, "delete-event", G_CALLBACK(_closing_refuse_delete), NULL);
151 // A window group of its own. The grab of dt_gui_closing_wait() is held in the default group,
152 // the one every other window of ours is in; in there, it would take this window's clicks
153 // too, and the details could not be unfolded.
156 g_object_unref(group);
157 }
158
161 gtk_container_add(GTK_CONTAINER(closing->window), box);
162
163 GtkWidget *icon = NULL;
164 if(confirm)
165 icon = gtk_image_new_from_icon_name("dialog-warning", GTK_ICON_SIZE_DIALOG);
166 else
167 {
168 icon = gtk_spinner_new();
170 }
172 gtk_box_pack_start(GTK_BOX(box), icon, FALSE, FALSE, 0);
173
175 gtk_box_pack_start(GTK_BOX(box), text, TRUE, TRUE, 0);
176
177 GtkWidget *title = gtk_label_new(NULL);
178 gchar *markup = g_markup_printf_escaped("<b>%s</b>", confirm ? _("Tasks are still running")
179 : _("Ansel is finishing its work before closing"));
182 gtk_label_set_xalign(GTK_LABEL(title), 0.0);
183 gtk_box_pack_start(GTK_BOX(text), title, FALSE, FALSE, 0);
184
185 closing->count = gtk_label_new(NULL);
186 gtk_label_set_xalign(GTK_LABEL(closing->count), 0.0);
187 gtk_box_pack_start(GTK_BOX(text), closing->count, FALSE, FALSE, 0);
188
189 closing->names = gtk_label_new(NULL);
190 gtk_label_set_xalign(GTK_LABEL(closing->names), 0.0);
192 gtk_box_pack_start(GTK_BOX(text), closing->names, FALSE, FALSE, 0);
193
194 // Nothing is cancelled by a quit: a running job is finished, a queued one is dropped.
195 GtkWidget *hint = gtk_label_new(confirm ? _("If you quit now, the tasks not started yet are dropped, and Ansel "
196 "closes once the running ones are done.")
197 : _("This window closes by itself as soon as it is done."));
201 gtk_box_pack_start(GTK_BOX(text), hint, FALSE, FALSE, 0);
202
203 closing->details = gtk_expander_new(_("Details"));
206 gtk_box_pack_start(GTK_BOX(text), closing->details, FALSE, FALSE, 0);
207
210 g_object_unref(closing->store);
213
215 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(C_("closing jobs", "Type"), renderer,
216 "text", DT_CLOSING_COL_KIND, NULL);
218
219 renderer = gtk_cell_renderer_text_new();
220 g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
221 column = gtk_tree_view_column_new_with_attributes(C_("closing jobs", "Description"), renderer, "text",
225
226 // As tall as its rows up to a point, then it scrolls: there is one row per worker at most. The
227 // tree view is transparent, and the recessed frame of our other lists puts it on a dark ground.
229 dt_gui_add_class(scroll, "dt_recessed_scroll");
235
236 closing->dropped = gtk_label_new(NULL);
237 gtk_label_set_xalign(GTK_LABEL(closing->dropped), 0.0);
239
242 gtk_box_pack_start(GTK_BOX(content), scroll, FALSE, FALSE, 0);
243 gtk_box_pack_start(GTK_BOX(content), closing->dropped, FALSE, FALSE, 0);
244 gtk_container_add(GTK_CONTAINER(closing->details), content);
245 // Shown by hand: the expander's no_show_all keeps show_all from reaching them. The count of
246 // dropped jobs keeps its own, and shows only when there are some.
247 gtk_widget_show_all(content);
248
249 if(confirm)
250 {
258 GtkWidget *quit = gtk_button_new_with_label(_("Quit anyway"));
259 g_signal_connect(quit, "clicked", G_CALLBACK(_closing_quit_clicked), closing);
260 gtk_container_add(GTK_CONTAINER(buttons), quit);
261 gtk_box_pack_start(GTK_BOX(text), buttons, FALSE, FALSE, 0);
262 // Going back is the default: Enter does not quit.
264 }
265
266 gtk_widget_show_all(closing->window);
267
268 // A quit does not always come from the application in front: the Dock's Quit, or Cmd+Q
269 // through the application switcher, leave another one active, and a window opened by an
270 // application in the background opens behind the windows of the one that is not.
272#ifdef GDK_WINDOWING_QUARTZ
274#endif
275}
276
277// Pango markup: the names are in italics, set apart from the sentences around them.
278static void _closing_append_name(const gchar *message, void *data)
279{
280 GString *names = (GString *)data;
281 if(names->len > 0) g_string_append_c(names, '\n');
282 gchar *name = g_markup_printf_escaped("<i>%s</i>", message);
284 dt_free(name);
285}
286
287static const char *_closing_kind_name(const int kind)
288{
289 switch(kind)
290 {
292 return C_("job type", "Image operation");
294 return C_("job type", "Thumbnail");
296 return C_("job type", "Background task");
297 case DT_JOB_QUEUE_USER_EXPORT: // prints share this queue: their description says so
298 return C_("job type", "Export");
300 return C_("job type", "Maintenance");
301 default:
302 return C_("job type", "Darkroom rendering");
303 }
304}
305
306static void _closing_job_clear(gpointer data)
307{
308 dt_closing_job_t *job = (dt_closing_job_t *)data;
309 dt_free(job->description);
310}
311
312static void _closing_collect_job(const char *description, const dt_job_queue_t queue, const gboolean reserved,
313 void *data)
314{
315 GArray *running = (GArray *)data;
316 // Descriptions are written for the debug log, in English. Some are also the catalog's
317 // strings, those the generic image jobs show on their progress bar.
318 const dt_closing_job_t job = { .kind = reserved ? DT_CLOSING_KIND_DARKROOM : (int)queue,
320 g_array_append_val(running, job);
321}
322
323// One row per running job. The queued ones are only counted, under the list: a quit drops them,
324// unrun.
326{
327 GArray *running = g_array_new(FALSE, FALSE, sizeof(dt_closing_job_t));
330
331 // Before the quit, the list says what the question counts: not the darkroom's service loop,
332 // which is not counted (see _closing_count_job()). Walk backwards so removals keep the indices.
333 if(closing->mode == DT_CLOSING_CONFIRM)
334 for(guint i = running->len; i > 0; i--)
336 g_array_remove_index(running, i - 1);
337
338 GString *listed = g_string_new(NULL);
339 for(guint i = 0; i < running->len; i++)
340 {
341 const dt_closing_job_t *job = &g_array_index(running, dt_closing_job_t, i);
342 g_string_append_printf(listed, "%d\t%s\n", job->kind, job->description);
343 }
344
345 if(g_strcmp0(listed->str, closing->listed) != 0)
346 {
347 gtk_list_store_clear(closing->store);
348 for(guint i = 0; i < running->len; i++)
349 {
350 const dt_closing_job_t *job = &g_array_index(running, dt_closing_job_t, i);
353 }
354 dt_free(closing->listed);
355 closing->listed = g_string_free(listed, FALSE);
356 }
357 else
358 g_string_free(listed, TRUE);
359
360 const int32_t queued = dt_control_queued_jobs_count(dt_control_get_global());
361 if(queued != closing->queued)
362 {
363 gchar *dropped = (closing->mode == DT_CLOSING_CONFIRM)
364 ? g_strdup_printf(ngettext("%d other task, not started, would be dropped",
365 "%d other tasks, not started, would be dropped", queued),
366 queued)
367 : g_strdup_printf(ngettext("%d other task, not started, is dropped",
368 "%d other tasks, not started, are dropped", queued),
369 queued);
370 gtk_label_set_text(GTK_LABEL(closing->dropped), dropped);
371 dt_free(dropped);
372 gtk_widget_set_visible(closing->dropped, queued > 0);
373 closing->queued = queued;
374 }
375
376 gtk_widget_set_visible(closing->details, running->len > 0 || queued > 0);
377 g_array_free(running, TRUE);
378}
379
381 const dt_job_queue_t queue __attribute__((unused)), const gboolean reserved,
382 void *data)
383{
384 // The reserved worker's job is the darkroom's service loop: it runs from entering the view to
385 // leaving it, idle or not, and leaving the view is part of the quit. Counted here, it would hold
386 // the question up for as long as the darkroom is open. It is the only reserved job there is
387 // (DT_CTL_WORKER_RESERVED is 1); a second one that does real work would need telling apart.
388 if(reserved) return;
389 (*(int32_t *)data)++;
390}
391
392// The jobs running now that the quit would wait for, thumbnails included, but not the darkroom's
393// service loop, which the quit ends itself. Only the question before the quit counts them so.
394static int32_t _closing_running_jobs(void)
395{
396 int32_t running = 0;
398 return running;
399}
400
401static void _closing_refresh(dt_closing_t *closing)
402{
403 // Before the quit, the workers are all alive, idle or not: the running jobs are counted one by
404 // one. During it, the workers still alive are those jobs.
405 const int32_t running
407 gchar *count = g_strdup_printf(ngettext("%d task is still running", "%d tasks are still running", running),
408 running);
409 gtk_label_set_text(GTK_LABEL(closing->count), count);
410 dt_free(count);
411
414 gtk_label_set_markup(GTK_LABEL(closing->names), names->str);
415 gtk_widget_set_visible(closing->names, names->len > 0);
417
419}
420
421static gboolean _closing_poll(gpointer user_data)
422{
423 dt_closing_t *closing = (dt_closing_t *)user_data;
424 if(dt_control_workers_alive() == 0)
425 {
426 g_main_loop_quit(closing->loop);
427 return G_SOURCE_REMOVE;
428 }
429
430 if(IS_NULL_PTR(closing->window))
431 {
433 _closing_window_new(closing);
434 }
435
436 _closing_refresh(closing);
437 return G_SOURCE_CONTINUE;
438}
439
440// Once nothing runs and nothing is queued, there is nothing left to ask about: the quit asked for
441// goes on, as if Quit anyway had been clicked.
442static gboolean _closing_nothing_pending(void)
443{
445}
446
447static gboolean _closing_confirm_poll(gpointer user_data)
448{
449 dt_closing_t *closing = (dt_closing_t *)user_data;
451 {
452 _closing_answer(closing, TRUE);
453 return G_SOURCE_CONTINUE; // removed by _closing_confirm()
454 }
455 _closing_refresh(closing);
456 return G_SOURCE_CONTINUE;
457}
458
459// Ask, while jobs are running or queued, whether to quit all the same. TRUE to quit. The running
460// ones are what the closing window would wait for; the queued ones, what the quit would drop.
461static gboolean _closing_confirm(void)
462{
463 if(_closing_nothing_pending()) return TRUE;
464
465 // A second request while the question is open -- the shortcut again, the dock -- does not ask
466 // it twice: the window that is up answers it.
467 static gboolean asking = FALSE;
468 if(asking) return FALSE;
469 asking = TRUE;
470
471 dt_closing_t closing = { .mode = DT_CLOSING_CONFIRM, .loop = g_main_loop_new(NULL, FALSE), .queued = -1 };
472 _closing_window_new(&closing);
473 _closing_refresh(&closing);
475 g_main_loop_run(closing.loop);
477 g_main_loop_unref(closing.loop);
478
479 gtk_widget_destroy(closing.window);
480 dt_free(closing.listed);
482
483 asking = FALSE;
484 return closing.quit;
485}
486
488{
490}
491
493{
494 if(dt_control_workers_alive() == 0) return;
495
496 // The main window is hidden and its view left, but the windows it may have left on screen are
497 // not, and nothing behind them is in a state to answer. The grab takes every input event
498 // away from them, close buttons included, for as long as this loop turns. It holds the
499 // default window group, theirs; the closing window has one of its own, and escapes it. Its
500 // widget is realised, since GTK delivers events to realised widgets only, but never shown:
501 // mapped, it would be a window on screen -- on macOS, an NSWindow of its own.
505
506 dt_closing_t closing = { .mode = DT_CLOSING_WAIT, .loop = g_main_loop_new(NULL, FALSE),
507 .start = g_get_monotonic_time(), .queued = -1 };
509 g_main_loop_run(closing.loop);
510 g_main_loop_unref(closing.loop);
511
514
515 dt_print(DT_DEBUG_CONTROL, "[closing] waited %.2f s for the running jobs\n",
516 (g_get_monotonic_time() - closing.start) / (double)G_USEC_PER_SEC);
517
518 // Off the screen at the drain of the main context that dt_cleanup() does right after
519 // dt_control_shutdown(), whose joins no longer wait for anything.
520 if(!IS_NULL_PTR(closing.window)) gtk_widget_destroy(closing.window);
521 dt_free(closing.listed);
522}
523
524// clang-format off
525// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
526// vim: shiftwidth=2 expandtab tabstop=2 cindent
527// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
528// clang-format on
GtkWidget * dt_gui_main_window(void)
const char ** description(struct dt_iop_module_t *self)
Definition ashift.c:168
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
static const dt_adaptation_t kind
static void _closing_details_update(dt_closing_t *closing)
Definition closing.c:325
dt_closing_mode_t
Definition closing.c:50
@ DT_CLOSING_CONFIRM
Definition closing.c:51
@ DT_CLOSING_WAIT
Definition closing.c:52
#define DT_CLOSING_KIND_DARKROOM
Definition closing.c:45
static gboolean _closing_nothing_pending(void)
Definition closing.c:442
static gboolean _closing_confirm_delete(GtkWidget *widget __attribute__((unused)), GdkEvent *event __attribute__((unused)), gpointer user_data)
Definition closing.c:108
static void _closing_refresh(dt_closing_t *closing)
Definition closing.c:401
static gboolean _closing_confirm_key(GtkWidget *widget __attribute__((unused)), const GdkEventKey *event, gpointer user_data)
Definition closing.c:115
void dt_gui_closing_quit(void)
Definition closing.c:487
static void _closing_job_clear(gpointer data)
Definition closing.c:306
@ DT_CLOSING_COL_KIND
Definition closing.c:57
@ DT_CLOSING_COLS
Definition closing.c:59
@ DT_CLOSING_COL_DESCRIPTION
Definition closing.c:58
static gboolean _closing_refuse_delete(GtkWidget *widget __attribute__((unused)), GdkEvent *event __attribute__((unused)), gpointer user_data __attribute__((unused)))
Definition closing.c:84
static void _closing_append_name(const gchar *message, void *data)
Definition closing.c:278
static gboolean _closing_confirm(void)
Definition closing.c:461
static void _closing_window_new(dt_closing_t *closing)
Definition closing.c:123
void dt_gui_closing_wait(void)
Definition closing.c:492
static gboolean _closing_poll(gpointer user_data)
Definition closing.c:421
static void _closing_count_job(const char *description __attribute__((unused)), const dt_job_queue_t queue __attribute__((unused)), const gboolean reserved, void *data)
Definition closing.c:380
#define DT_CLOSING_NOTICE_DELAY
Definition closing.c:40
static gboolean _closing_confirm_poll(gpointer user_data)
Definition closing.c:447
static void _closing_back_clicked(GtkButton *button __attribute__((unused)), gpointer user_data)
Definition closing.c:102
static void _closing_answer(dt_closing_t *closing, const gboolean quit)
Definition closing.c:91
static const char * _closing_kind_name(const int kind)
Definition closing.c:287
static int32_t _closing_running_jobs(void)
Definition closing.c:394
static void _closing_quit_clicked(GtkButton *button __attribute__((unused)), gpointer user_data)
Definition closing.c:97
static void _closing_collect_job(const char *description, const dt_job_queue_t queue, const gboolean reserved, void *data)
Definition closing.c:312
#define DT_CLOSING_POLL_INTERVAL
Definition closing.c:42
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
void dt_control_quit()
Definition control.c:452
struct dt_control_t * dt_control_get_global(void)
Definition darktable.c:651
void dt_gui_refocus_parent(GtkWindow *parent)
Definition dialog.c:64
GHashTable * names
GtkWidget* -> the name the application knows it by.
GtkWidget * view
the tree view
void dt_control_running_jobs_foreach(dt_control_t *control, dt_control_running_jobs_foreach_callback_t callback, void *data)
Definition jobs.c:549
int32_t dt_control_queued_jobs_count(dt_control_t *control)
Definition jobs.c:571
int32_t dt_control_workers_alive(void)
Definition jobs.c:544
dt_job_queue_t
Definition jobs.h:54
@ DT_JOB_QUEUE_USER_FG
Definition jobs.h:55
@ DT_JOB_QUEUE_USER_BG
Definition jobs.h:57
@ DT_JOB_QUEUE_USER_EXPORT
Definition jobs.h:58
@ DT_JOB_QUEUE_SYSTEM_BG
Definition jobs.h:59
@ DT_JOB_QUEUE_SYSTEM_FG
Definition jobs.h:56
@ DT_DEBUG_CONTROL
Definition logging.h:52
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
#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
void dt_osx_focus_window()
Definition osx.mm:289
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:105
const char * name
Definition pdf.h:90
void dt_control_progress_foreach(dt_control_t *control, dt_progress_foreach_callback_t callback, void *data)
Definition progress.c:411
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 * description
Definition closing.c:81
GMainLoop * loop
Definition closing.c:66
GtkWidget * window
Definition closing.c:68
gchar * listed
Definition closing.c:73
gint64 start
Definition closing.c:67
int32_t queued
Definition closing.c:75
GtkWidget * dropped
Definition closing.c:74
GtkWidget * names
Definition closing.c:70
GtkListStore * store
Definition closing.c:72
GtkWidget * count
Definition closing.c:69
dt_closing_mode_t mode
Definition closing.c:64
GtkWidget * details
Definition closing.c:71
gboolean quit
Definition closing.c:65
#define DT_PIXEL_APPLY_DPI(value)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)