Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
gtk.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2009-2014, 2016 johannes hanika.
4 Copyright (C) 2010 Alexandre Prokoudine.
5 Copyright (C) 2010-2014 Henrik Andersson.
6 Copyright (C) 2010, 2013-2014, 2016 Pascal de Bruijn.
7 Copyright (C) 2010 Richard Hughes.
8 Copyright (C) 2010 Stuart Henderson.
9 Copyright (C) 2010-2019 Tobias Ellinghaus.
10 Copyright (C) 2011 Antony Dovgal.
11 Copyright (C) 2011-2012, 2015 Jérémy Rosen.
12 Copyright (C) 2011 Moritz Lipp.
13 Copyright (C) 2011 Olivier Tribout.
14 Copyright (C) 2011 Robert Bieber.
15 Copyright (C) 2011-2013 Simon Spannagel.
16 Copyright (C) 2012, 2014, 2019-2022 Aldric Renaudin.
17 Copyright (C) 2012-2017, 2019-2020 parafin.
18 Copyright (C) 2012 Richard Wonka.
19 Copyright (C) 2013 Jochem Kossen.
20 Copyright (C) 2013, 2015, 2017-2022 Pascal Obry.
21 Copyright (C) 2013-2016, 2019-2020 Roman Lebedev.
22 Copyright (C) 2014 Mikhail Trishchenkov.
23 Copyright (C) 2015 Edouard Gomez.
24 Copyright (C) 2015 Pedro Côrte-Real.
25 Copyright (C) 2015, 2019 Ulrich Pegelow.
26 Copyright (C) 2016-2017 Peter Budai.
27 Copyright (C) 2017-2018, 2021 Dan Torop.
28 Copyright (C) 2017-2018 Matthieu Moy.
29 Copyright (C) 2017-2018 Rikard Öxler.
30 Copyright (C) 2018-2019 Edgardo Hoszowski.
31 Copyright (C) 2018 Mario Lueder.
32 Copyright (C) 2019-2020, 2022-2023, 2025-2026 Aurélien PIERRE.
33 Copyright (C) 2019 emeikei.
34 Copyright (C) 2019 Heiko Bauke.
35 Copyright (C) 2019 jakubfi.
36 Copyright (C) 2019 vacaboja.
37 Copyright (C) 2020 Bill Ferguson.
38 Copyright (C) 2020-2022 Chris Elston.
39 Copyright (C) 2020-2021 David-Tillmann Schaefer.
40 Copyright (C) 2020-2022 Diederik Ter Rahe.
41 Copyright (C) 2020 Hanno Schwalm.
42 Copyright (C) 2020 Harold le Clément de Saint-Marcq.
43 Copyright (C) 2020-2021 Hubert Kowalski.
44 Copyright (C) 2020 Marco.
45 Copyright (C) 2020, 2022 Miloš Komarčević.
46 Copyright (C) 2020-2021 Philippe Weyland.
47 Copyright (C) 2020 quovadit.
48 Copyright (C) 2020-2021 Ralf Brown.
49 Copyright (C) 2021 darkelectron.
50 Copyright (C) 2021 lhietal.
51 Copyright (C) 2021 luzpaz.
52 Copyright (C) 2022 Martin Bařinka.
53 Copyright (C) 2022 Nicolas Auffray.
54 Copyright (C) 2022 Roman Neuhauser.
55 Copyright (C) 2022 Victor Forsiuk.
56 Copyright (C) 2023 Luca Zulberti.
57 Copyright (C) 2023 Maurizio Paglia.
58 Copyright (C) 2025 Alynx Zhou.
59 Copyright (C) 2025 Guillaume Stutin.
60
61 darktable is free software: you can redistribute it and/or modify
62 it under the terms of the GNU General Public License as published by
63 the Free Software Foundation, either version 3 of the License, or
64 (at your option) any later version.
65
66 darktable is distributed in the hope that it will be useful,
67 but WITHOUT ANY WARRANTY; without even the implied warranty of
68 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
69 GNU General Public License for more details.
70
71 You should have received a copy of the GNU General Public License
72 along with darktable. If not, see <http://www.gnu.org/licenses/>.
73*/
74#include "common/darktable.h"
75#include "common/collection.h"
76#include "common/colorspaces.h"
77#include "common/l10n.h"
79#include "common/ratings.h"
80#include "common/image.h"
81#include "common/image_cache.h"
82#include "gui/guides.h"
83#include "bauhaus/bauhaus.h"
84#include "develop/develop.h"
85#include "develop/imageop.h"
86#include "dtgtk/button.h"
87#include "dtgtk/expander.h"
88#include "dtgtk/sidepanel.h"
89
90#include "gui/gtk.h"
91#include "gui/splash.h"
92
93#include "common/styles.h"
94#include "control/conf.h"
95#include "control/control.h"
96#include "control/jobs.h"
97#include "control/signal.h"
98#include "gui/presets.h"
99#include "views/view.h"
100
101#include <gdk/gdkkeysyms.h>
102#ifdef GDK_WINDOWING_WAYLAND
103#include <gdk/gdkwayland.h>
104#endif
105#ifdef _WIN32
106#include <gdk/gdkwin32.h>
107#endif
108#include <gtk/gtk.h>
109#include <math.h>
110#include <stdlib.h>
111#include <stdio.h>
112#include <string.h>
113#include <unistd.h>
114#ifdef MAC_INTEGRATION
115#include <gtkosxapplication.h>
116#endif
117#ifdef GDK_WINDOWING_QUARTZ
118#include "osx/osx.h"
119#endif
120#include <pthread.h>
121
122/*
123 * NEW UI API
124 */
125
126/* generic callback for redraw widget signals */
127static void _ui_widget_redraw_callback(gpointer instance, GtkWidget *widget);
128/* callback for redraw log signals */
129static void _ui_log_redraw_callback(gpointer instance, GtkWidget *widget);
130static void _ui_toast_redraw_callback(gpointer instance, GtkWidget *widget);
131
132// set class function to add CSS classes with just a simple line call
133void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
134{
135 GtkStyleContext *context = gtk_widget_get_style_context(widget);
136 if(!gtk_style_context_has_class(context, class_name))
137 {
138 gtk_style_context_add_class(context, class_name);
139 gtk_widget_queue_draw(widget);
140 }
141}
142
143void dt_gui_remove_class(GtkWidget *widget, const gchar *class_name)
144{
145 GtkStyleContext *context = gtk_widget_get_style_context(widget);
146 if(gtk_style_context_has_class(context, class_name))
147 {
148 gtk_style_context_remove_class(context, class_name);
149 gtk_widget_queue_draw(widget);
150 }
151}
152
153void dt_gui_set_symbolic_icon(GtkWidget *image, const char *icon_name, GtkIconSize size, const GdkRGBA *color)
154{
155 gint width = 16, height = 16;
156 gtk_icon_size_lookup(size, &width, &height);
157 // Icon themes only look up square icons: request the larger dimension,
158 // then scale the result down to the exact width/height below if the two differ.
159 GtkIconInfo *info = gtk_icon_theme_lookup_icon(gtk_icon_theme_get_default(), icon_name, MAX(width, height),
160 GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
161 if(IS_NULL_PTR(info))
162 {
163 gtk_image_set_from_icon_name(GTK_IMAGE(image), icon_name, size);
164 return;
165 }
166
167 GdkPixbuf *pixbuf = IS_NULL_PTR(color)
168 ? gtk_icon_info_load_symbolic_for_context(info, gtk_widget_get_style_context(image), NULL, NULL)
169 : gtk_icon_info_load_symbolic(info, color, color, color, color, NULL, NULL);
170 g_object_unref(info);
171
172 if(!IS_NULL_PTR(pixbuf))
173 {
174 if(gdk_pixbuf_get_width(pixbuf) != width || gdk_pixbuf_get_height(pixbuf) != height)
175 {
176 GdkPixbuf *scaled = gdk_pixbuf_scale_simple(pixbuf, width, height, GDK_INTERP_BILINEAR);
177 g_object_unref(pixbuf);
178 pixbuf = scaled;
179 }
180 gtk_image_set_from_pixbuf(GTK_IMAGE(image), pixbuf);
181 g_object_unref(pixbuf);
182 }
183}
184
185/* ------------------------------------------------------------------------------------------
186 * Widget-callback suppression depth (see common/darktable.h for the rationale and API).
187 * ------------------------------------------------------------------------------------------ */
188static inline gboolean _dt_on_gui_thread(void)
189{
190 // Same idiom as dt_control_signal (control/signal.c): is the caller the GUI/main thread?
191 return darktable.control && pthread_equal(darktable.control->gui_thread, pthread_self());
192}
193
195{
197}
198
199void dt_gui_freeze_begin_(const char *file, int line)
200{
201 // Only the GUI thread owns widget state. Off-thread callers (notably worker-thread
202 // reload_defaults during thumbnail/export, which has no widgets to suppress) must not touch
203 // the shared depth, or concurrent non-atomic ++/-- drift it and break suppression for the
204 // GUI thread. For them this is a deliberate no-op.
205 if(!darktable.gui || !_dt_on_gui_thread()) return;
206 // MAX(.,0) heals any pre-existing negative drift so the depth is always genuinely suppressing.
208 (void)file;
209 (void)line;
210}
211
212void dt_gui_freeze_end_(const char *file, int line)
213{
214 if(!darktable.gui || !_dt_on_gui_thread()) return;
216 {
217 // A bare end with nothing to match: an unbalanced freeze bracket exists. Surface it (with
218 // the offending site) instead of letting the counter go negative and silently disable
219 // suppression for the rest of the session.
220 fprintf(stderr, "[dt_gui_freeze] unbalanced end at %s:%d (depth was %d); "
221 "look for a freeze begin without a matching end.\n",
224 return;
225 }
227}
228
233
234/*
235 * OLD UI API
236 */
237static void _init_widgets(dt_gui_gtk_t *gui);
238static gboolean _configure(GtkWidget *da, GdkEventConfigure *event, gpointer user_data);
239
240gboolean dt_gui_get_scroll_deltas(const GdkEventScroll *event, gdouble *delta_x, gdouble *delta_y)
241{
242 // avoid double counting real and emulated events when receiving smooth scrolls
243 if(gdk_event_get_pointer_emulated((GdkEvent*)event)) return FALSE;
244
245 gboolean handled = FALSE;
246 switch(event->direction)
247 {
248 // is one-unit cardinal, e.g. from a mouse scroll wheel
249 case GDK_SCROLL_LEFT:
250 if(delta_x)
251 {
252 *delta_x = dt_conf_get_bool("scroll/reverse_x") ? 1.0 : -1.0;
253 if(delta_y) *delta_y = 0.0;
254 handled = TRUE;
255 }
256 break;
257 case GDK_SCROLL_RIGHT:
258 if(delta_x)
259 {
260 *delta_x = dt_conf_get_bool("scroll/reverse_x") ? -1.0 : 1.0;
261 if(delta_y) *delta_y = 0.0;
262 handled = TRUE;
263 }
264 break;
265 case GDK_SCROLL_UP:
266 if(delta_y)
267 {
268 if(delta_x) *delta_x = 0.0;
269 *delta_y = dt_conf_get_bool("scroll/reverse_y") ? 1.0 : -1.0;
270 handled = TRUE;
271 }
272 break;
273 case GDK_SCROLL_DOWN:
274 if(delta_y)
275 {
276 if(delta_x) *delta_x = 0.0;
277 *delta_y = dt_conf_get_bool("scroll/reverse_y") ? -1.0 : 1.0;
278 handled = TRUE;
279 }
280 break;
281 // is trackpad (or touch) scroll
282 case GDK_SCROLL_SMOOTH:
283 if((delta_x && event->delta_x != 0) || (delta_y && event->delta_y != 0))
284 {
285#ifdef GDK_WINDOWING_QUARTZ // on macOS deltas need to be scaled
286 if(delta_x) *delta_x = dt_conf_get_bool("scroll/reverse_x") ? -event->delta_x / 50. : event->delta_x / 50.;
287 if(delta_y) *delta_y = dt_conf_get_bool("scroll/reverse_y") ? -event->delta_y / 50. : event->delta_y / 50.;
288#else
289 if(delta_x) *delta_x = dt_conf_get_bool("scroll/reverse_x") ? -event->delta_x : event->delta_x;
290 if(delta_y) *delta_y = dt_conf_get_bool("scroll/reverse_y") ? -event->delta_y : event->delta_y;
291#endif
292 handled = TRUE;
293 }
294 default:
295 break;
296 }
297 return handled;
298}
299
300gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, int *delta_x, int *delta_y)
301{
302 // avoid double counting real and emulated events when receiving smooth scrolls
303 if(gdk_event_get_pointer_emulated((GdkEvent*)event)) return FALSE;
304
305 // accumulates scrolling regardless of source or the widget being scrolled
306 static gdouble acc_x = 0.0, acc_y = 0.0;
307
308 gboolean handled = FALSE;
309
310 switch(event->direction)
311 {
312 // is one-unit cardinal, e.g. from a mouse scroll wheel
313 case GDK_SCROLL_LEFT:
314 if(delta_x)
315 {
316 *delta_x = dt_conf_get_bool("scroll/reverse_x") ? 1 : -1;
317 if(delta_y) *delta_y = 0;
318 handled = TRUE;
319 }
320 break;
321 case GDK_SCROLL_RIGHT:
322 if(delta_x)
323 {
324 *delta_x = dt_conf_get_bool("scroll/reverse_x") ? -1 : 1;
325 if(delta_y) *delta_y = 0;
326 handled = TRUE;
327 }
328 break;
329 case GDK_SCROLL_UP:
330 if(delta_y)
331 {
332 if(delta_x) *delta_x = 0;
333 *delta_y = dt_conf_get_bool("scroll/reverse_y") ? 1 : -1;
334 handled = TRUE;
335 }
336 break;
337 case GDK_SCROLL_DOWN:
338 if(delta_y)
339 {
340 if(delta_x) *delta_x = 0;
341 *delta_y = dt_conf_get_bool("scroll/reverse_y") ? -1 : 1;
342 handled = TRUE;
343 }
344 break;
345 // is trackpad (or touch) scroll
346 case GDK_SCROLL_SMOOTH:
347 // stop events reset accumulated delta
348 if(event->is_stop)
349 {
350 acc_x = acc_y = 0.0;
351 break;
352 }
353 // accumulate trackpad/touch scrolls until they make a unit
354 // scroll, and only then tell caller that there is a scroll to
355 // handle
356#ifdef GDK_WINDOWING_QUARTZ // on macOS deltas need to be scaled
357 acc_x += dt_conf_get_bool("scroll/reverse_x") ? -event->delta_x / 50. : event->delta_x / 50.;
358 acc_y += dt_conf_get_bool("scroll/reverse_y") ? -event->delta_y / 50. : event->delta_y / 50.;
359#else
360 acc_x += dt_conf_get_bool("scroll/reverse_x") ? -event->delta_x : event->delta_x;
361 acc_y += dt_conf_get_bool("scroll/reverse_y") ? -event->delta_y : event->delta_y;
362#endif
363 const gdouble amt_x = trunc(acc_x);
364 const gdouble amt_y = trunc(acc_y);
365 if(amt_x != 0 || amt_y != 0)
366 {
367 acc_x -= amt_x;
368 acc_y -= amt_y;
369 if((delta_x && amt_x != 0) || (delta_y && amt_y != 0))
370 {
371 if(delta_x) *delta_x = (int)amt_x;
372 if(delta_y) *delta_y = (int)amt_y;
373 handled = TRUE;
374 }
375 }
376 break;
377 default:
378 break;
379 }
380 return handled;
381}
382
383gboolean dt_gui_get_scroll_delta(const GdkEventScroll *event, gdouble *delta)
384{
385 gdouble delta_x, delta_y;
386 if(dt_gui_get_scroll_deltas(event, &delta_x, &delta_y))
387 {
388 *delta = delta_x + delta_y;
389 return TRUE;
390 }
391 return FALSE;
392}
393
394gboolean dt_gui_get_scroll_unit_delta(const GdkEventScroll *event, int *delta)
395{
396 int delta_x, delta_y;
397 if(dt_gui_get_scroll_unit_deltas(event, &delta_x, &delta_y))
398 {
399 *delta = delta_x + delta_y;
400 return TRUE;
401 }
402 return FALSE;
403}
404
405
406static gboolean _draw(GtkWidget *da, cairo_t *cr, gpointer user_data)
407{
408 dt_control_expose(NULL);
410 {
411 cairo_set_source_surface(cr, darktable.gui->surface, 0, 0);
412 cairo_paint(cr);
413 }
414 return TRUE;
415}
416
417#ifdef _DEBUG
418void dt_gtk_widget_queue_draw_ext(GtkWidget *widget, const char *name, const char *file, const int line)
419{
420 if(!GTK_IS_WIDGET(widget))
421 {
422 dt_print(DT_DEBUG_GTK, "gtk_widget_queue_draw(%s) called with a non-WIDGET or NULL widget at %s:%d (widget=%p)\n",
423 name, file, line, widget);
424 return;
425 }
426 else
427 dt_print(DT_DEBUG_GTK, "queueing redraw for `%s` (`%s`) at %s:%d\n",
428 name, gtk_widget_get_name(widget), file, line);
429
430
431 (gtk_widget_queue_draw)(widget);
432}
433
434void dt_gtk_toggle_button_set_active_ext(GtkToggleButton *toggle_button, const char *name, const gboolean active,
435 const char *file, const int line)
436{
437 if(!GTK_IS_TOGGLE_BUTTON(toggle_button))
438 {
439 dt_print(DT_DEBUG_GTK, "gtk_toggle_button_set_active(%s) called with a non-TOGGLE_BUTTON or NULL widget at %s:%d (toggle_button=%p)\n",
440 name, file, line, toggle_button);
441 return;
442 }
443 else
444 dt_print(DT_DEBUG_GTK, "setting toggle button `%s` (`%s`) to %s at %s:%d\n", name, gtk_widget_get_name(GTK_WIDGET(toggle_button)),
445 active ? "active" : "inactive", file, line);
446
447 (gtk_toggle_button_set_active)(toggle_button, active);
448}
449#endif
450
451static gboolean _scrolled(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
452{
453 if(!gtk_window_is_active(GTK_WINDOW(darktable.gui->ui->main_window))) return FALSE;
454
455 int delta_y;
456 if(dt_gui_get_scroll_unit_delta(event, &delta_y))
457 {
458 return dt_view_manager_scrolled(darktable.view_manager, event->x, event->y,
459 delta_y < 0,
460 event->state, delta_y);
461 }
462
463 return FALSE;
464}
465
466
468{
471 const GdkWindowState window_state = gdk_window_get_state(gtk_widget_get_window(widget));
472 dt_conf_set_bool("ui_last/maximized", (window_state & GDK_WINDOW_STATE_MAXIMIZED));
473 int width, height;
474 gtk_window_get_size(GTK_WINDOW(widget), &width, &height);
475 dt_conf_set_int("ui_last/window_width", width);
476 dt_conf_set_int("ui_last/window_height", height);
477
478 gboolean save_window_position = TRUE;
479#ifdef GDK_WINDOWING_WAYLAND
480 GdkDisplay *display = gtk_widget_get_display(widget);
481 if(GDK_IS_WAYLAND_DISPLAY(display))
482 save_window_position = FALSE;
483#endif
484
485 if(save_window_position)
486 {
487 GdkWindow *gdk_window = gtk_widget_get_window(widget);
488 GdkDisplay *window_display = gtk_widget_get_display(widget);
489 if(!IS_NULL_PTR(gdk_window) && !IS_NULL_PTR(window_display))
490 {
491 GdkMonitor *monitor = gdk_display_get_monitor_at_window(window_display, gdk_window);
492 if(!IS_NULL_PTR(monitor))
493 {
494 const int n_monitors = gdk_display_get_n_monitors(window_display);
495 int monitor_index = -1;
496 for(int i = 0; i < n_monitors; i++)
497 {
498 if(gdk_display_get_monitor(window_display, i) == monitor)
499 {
500 monitor_index = i;
501 break;
502 }
503 }
504 if(monitor_index >= 0)
505 dt_conf_set_int("ui_last/window_monitor", monitor_index);
506 }
507 }
508
509 if(!(window_state & GDK_WINDOW_STATE_MAXIMIZED))
510 {
511 int x, y;
512 gtk_window_get_position(GTK_WINDOW(widget), &x, &y);
513 dt_conf_set_int("ui_last/window_x", x);
514 dt_conf_set_int("ui_last/window_y", y);
515 }
516 }
517
519
520 return 0;
521}
522
524{
525 const GdkRGBA bc = darktable.gui->colors[color];
526 cairo_set_source_rgb(cr, bc.red, bc.green, bc.blue);
527}
528
529void dt_gui_gtk_set_source_rgba(cairo_t *cr, dt_gui_color_t color, float opacity_coef)
530{
531 GdkRGBA bc = darktable.gui->colors[color];
532 cairo_set_source_rgba(cr, bc.red, bc.green, bc.blue, bc.alpha * opacity_coef);
533}
534
536{
538 dt_gui_add_class(win, "dt_gui_quit");
539 gtk_window_set_title(GTK_WINDOW(win), _("closing Ansel..."));
540
542
543 // Write out windows dimension
545
546 // hide main window
547 gtk_widget_hide(dt_ui_main_window(darktable.gui->ui));
548}
549
550gboolean dt_gui_quit_callback(GtkWidget *widget, GdkEvent *event, gpointer user_data)
551{
553 return TRUE;
554}
555
557{
559 darktable.gui->last_preset = g_strdup(name);
560}
561
562#ifdef MAC_INTEGRATION
563#ifdef GTK_TYPE_OSX_APPLICATION
564static gboolean _osx_quit_callback(GtkOSXApplication *OSXapp, gpointer user_data)
565#else
566static gboolean _osx_quit_callback(GtkosxApplication *OSXapp, gpointer user_data)
567#endif
568{
569 GList *windows, *window;
570 windows = gtk_window_list_toplevels();
571 for(window = windows; !IS_NULL_PTR(window); window = g_list_next(window))
572 if(gtk_window_get_modal(GTK_WINDOW(window->data)) && gtk_widget_get_visible(GTK_WIDGET(window->data)))
573 break;
575 g_list_free(windows);
576 windows = NULL;
577 return TRUE;
578}
579
580#ifdef GTK_TYPE_OSX_APPLICATION
581static gboolean _osx_openfile_callback(GtkOSXApplication *OSXapp, gchar *path, gpointer user_data)
582#else
583static gboolean _osx_openfile_callback(GtkosxApplication *OSXapp, gchar *path, gpointer user_data)
584#endif
585{
586 return dt_load_from_string(path, TRUE, NULL) > 0;
587}
588#endif
589
590static gboolean _configure(GtkWidget *da, GdkEventConfigure *event, gpointer user_data)
591{
592 static int oldw = 0;
593 static int oldh = 0;
594 // make our selves a properly sized pixmap if our window has been resized
595 if(oldw != event->width || oldh != event->height)
596 {
597 // create our new pixmap with the correct size.
598 cairo_surface_t *tmpsurface
599 = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, event->width, event->height);
600 // copy the contents of the old pixmap to the new pixmap. This keeps ugly uninitialized
601 // pixmaps from being painted upon resize
602 // int minw = oldw, minh = oldh;
603 // if(event->width < minw) minw = event->width;
604 // if(event->height < minh) minh = event->height;
605
606 cairo_t *cr = cairo_create(tmpsurface);
607 cairo_set_source_surface(cr, darktable.gui->surface, 0, 0);
608 cairo_paint(cr);
609 cairo_destroy(cr);
610
611 // we're done with our old pixmap, so we can get rid of it and replace it with our properly-sized one.
612 cairo_surface_destroy(darktable.gui->surface);
613 darktable.gui->surface = tmpsurface;
615 DT_COLORSPACE_DISPLAY); // maybe we are on another screen now with > 50% of the area
616 }
617 oldw = event->width;
618 oldh = event->height;
619
620#ifndef GDK_WINDOWING_QUARTZ
621 dt_configure_ppd_dpi((dt_gui_gtk_t *) user_data);
622#endif
623
624 return dt_control_configure(da, event, user_data);
625}
626
627static gboolean _window_configure(GtkWidget *da, GdkEvent *event, gpointer user_data)
628{
629 static int oldx = 0;
630 static int oldy = 0;
631 if(oldx != event->configure.x || oldy != event->configure.y)
632 {
634 DT_COLORSPACE_DISPLAY); // maybe we are on another screen now with > 50% of the area
635 oldx = event->configure.x;
636 oldy = event->configure.y;
637 }
638 return FALSE;
639}
640
642{
643 gboolean valid;
644 double x;
645 double y;
646 guint32 time_ms;
649 double pressure;
650 gboolean have_tilt;
651 double tilt_x;
652 double tilt_y;
654
656
657static inline double _clamp01d(const double value)
658{
659 return MIN(1.0, MAX(0.0, value));
660}
661
662static const gdouble *_event_axes(const GdkEvent *event)
663{
664 if(IS_NULL_PTR(event)) return NULL;
665 switch(event->type)
666 {
667 case GDK_MOTION_NOTIFY:
668 return ((const GdkEventMotion *)event)->axes;
669 case GDK_BUTTON_PRESS:
670 case GDK_2BUTTON_PRESS:
671 case GDK_3BUTTON_PRESS:
672 case GDK_BUTTON_RELEASE:
673 return ((const GdkEventButton *)event)->axes;
674 default:
675 return NULL;
676 }
677}
678
679static gboolean _get_axis_value_for_source(const GdkEvent *event, GdkDevice *source_device,
680 const GdkAxisUse axis, double *value, gboolean *from_source_map)
681{
682 if(from_source_map) *from_source_map = FALSE;
683 if(IS_NULL_PTR(value)) return FALSE;
684
685 const gdouble *axes = _event_axes(event);
686 if(source_device && axes)
687 {
688 double source_value = 0.0;
689 if(gdk_device_get_axis(source_device, (gdouble *)axes, axis, &source_value))
690 {
691 *value = source_value;
692 if(from_source_map) *from_source_map = TRUE;
693 return TRUE;
694 }
695 }
696
697 return gdk_event_get_axis((GdkEvent *)event, axis, value);
698}
699
700static gboolean _sample_axis_from_device_state(GdkWindow *window, GdkDevice *device,
701 const GdkAxisUse axis, double *value)
702{
703 if(IS_NULL_PTR(window) || IS_NULL_PTR(device) || IS_NULL_PTR(value)) return FALSE;
704 if(gdk_device_get_source(device) == GDK_SOURCE_KEYBOARD) return FALSE;
705 if(gdk_device_get_device_type(device) == GDK_DEVICE_TYPE_SLAVE)
706 {
707 GdkDisplay *display = gdk_device_get_display(device);
708 if(!display || !gdk_display_device_is_grabbed(display, device)) return FALSE;
709 }
710
711 const int n_axes = gdk_device_get_n_axes(device);
712 if(n_axes <= 0) return FALSE;
713
714 double *axes = g_newa(double, n_axes);
715 memset(axes, 0, sizeof(double) * n_axes);
716 GdkModifierType modifiers = 0;
717 gdk_device_get_state(device, window, axes, &modifiers);
718 return gdk_device_get_axis(device, axes, axis, value);
719}
720
721static gboolean _sample_tablet_state_from_devices(const GdkEvent *event,
722 double *pressure, gboolean *have_pressure,
723 double *tilt_x, double *tilt_y, gboolean *have_tilt,
724 const char **picked_device_name)
725{
726 if(pressure) *pressure = 0.0;
727 if(have_pressure) *have_pressure = FALSE;
728 if(tilt_x) *tilt_x = 0.0;
729 if(tilt_y) *tilt_y = 0.0;
730 if(have_tilt) *have_tilt = FALSE;
731 if(picked_device_name) *picked_device_name = NULL;
732
733 if(IS_NULL_PTR(darktable.gui)) return FALSE;
734 GdkWindow *window = gdk_event_get_window((GdkEvent *)event);
735 if(IS_NULL_PTR(window)) window = gtk_widget_get_window(dt_ui_center(darktable.gui->ui));
736 if(IS_NULL_PTR(window)) return FALSE;
737
738 int best_score = -1;
739 double best_p = 0.0;
740 gboolean best_have_p = FALSE;
741 double best_tx = 0.0;
742 double best_ty = 0.0;
743 gboolean best_have_t = FALSE;
744 const char *best_name = NULL;
745
746 GdkSeat *seat = gdk_display_get_default() ? gdk_display_get_default_seat(gdk_display_get_default()) : NULL;
747 GList *runtime_devices = seat ? gdk_seat_get_slaves(seat, GDK_SEAT_CAPABILITY_ALL_POINTING) : NULL;
748
749 for(GList *l = runtime_devices; l; l = g_list_next(l))
750 {
751 GdkDevice *device = (GdkDevice *)l->data;
752 if(IS_NULL_PTR(device)) continue;
753 const GdkInputSource source = gdk_device_get_source(device);
754 if(source == GDK_SOURCE_KEYBOARD) continue;
755 if(gdk_device_get_device_type(device) == GDK_DEVICE_TYPE_SLAVE)
756 {
757 GdkDisplay *display = gdk_device_get_display(device);
758 /* gdk_device_get_state() requires slave devices to be grabbed.
759 * Skip non-grabbed slaves to avoid GTK/GDK criticals. */
760 if(!display || !gdk_display_device_is_grabbed(display, device)) continue;
761 }
762
763 const GdkAxisFlags axis_flags = gdk_device_get_axes(device);
764 const gboolean supports_pressure = (axis_flags & GDK_AXIS_FLAG_PRESSURE) != 0;
765 const gboolean supports_x_tilt = (axis_flags & GDK_AXIS_FLAG_XTILT) != 0;
766 const gboolean supports_y_tilt = (axis_flags & GDK_AXIS_FLAG_YTILT) != 0;
767 const gboolean pen_like = (source == GDK_SOURCE_PEN || source == GDK_SOURCE_ERASER || source == GDK_SOURCE_CURSOR);
768 if(!supports_pressure && !supports_x_tilt && !supports_y_tilt && !pen_like) continue;
769
770 const int n_axes = gdk_device_get_n_axes(device);
771 if(n_axes <= 0) continue;
772 double *axes = g_newa(double, n_axes);
773 memset(axes, 0, sizeof(double) * n_axes);
774 GdkModifierType modifiers = 0;
775 gdk_device_get_state(device, window, axes, &modifiers);
776
777 double p = 0.0;
778 gboolean have_p = FALSE;
779 double tx = 0.0;
780 double ty = 0.0;
781 gboolean have_tx = FALSE;
782 gboolean have_ty = FALSE;
783
784 for(int i = 0; i < n_axes; i++)
785 {
786 const GdkAxisUse use = gdk_device_get_axis_use(device, i);
787 if(use == GDK_AXIS_PRESSURE)
788 {
789 p = axes[i];
790 have_p = TRUE;
791 }
792 else if(use == GDK_AXIS_XTILT)
793 {
794 tx = axes[i];
795 have_tx = TRUE;
796 }
797 else if(use == GDK_AXIS_YTILT)
798 {
799 ty = axes[i];
800 have_ty = TRUE;
801 }
802 }
803
804 const gboolean have_t = have_tx || have_ty;
805 const int score = (have_p ? 2 : 0) + (have_t ? 2 : 0) + (p > 1e-4 ? 4 : 0)
806 + ((hypot(tx, ty) > 1e-4) ? 3 : 0) + (pen_like ? 1 : 0);
807 if(score <= best_score) continue;
808
809 best_score = score;
810 best_p = p;
811 best_have_p = have_p;
812 best_tx = tx;
813 best_ty = ty;
814 best_have_t = have_t;
815 best_name = gdk_device_get_name(device);
816 }
817 if(runtime_devices)
818 {
819 g_list_free(runtime_devices);
820 runtime_devices = NULL;
821 }
822
823 if(best_score < 0) return FALSE;
824 if(pressure) *pressure = best_p;
825 if(have_pressure) *have_pressure = best_have_p;
826 if(tilt_x) *tilt_x = best_tx;
827 if(tilt_y) *tilt_y = best_ty;
828 if(have_tilt) *have_tilt = best_have_t;
829 if(picked_device_name) *picked_device_name = best_name;
830 return TRUE;
831}
832
833static dt_control_pointer_input_t _extract_pointer_input(const GdkEvent *event, const double x, const double y,
834 const guint32 time_ms, const gboolean reset_kinematics,
835 const char *tag)
836{
837 dt_control_pointer_input_t input = { 0 };
838 input.x = x;
839 input.y = y;
840 input.time_ms = time_ms;
841 GdkDevice *source_device = gdk_event_get_source_device((GdkEvent *)event);
842 GdkDevice *event_device = gdk_event_get_device((GdkEvent *)event);
843 GdkDevice *device = source_device ? source_device : event_device;
844 const GdkInputSource source = device ? gdk_device_get_source(device) : GDK_SOURCE_MOUSE;
845 const GdkAxisFlags axis_flags = device ? gdk_device_get_axes(device) : 0;
846 const gboolean supports_pressure = (axis_flags & GDK_AXIS_FLAG_PRESSURE) != 0;
847 const gboolean supports_x_tilt = (axis_flags & GDK_AXIS_FLAG_XTILT) != 0;
848 const gboolean supports_y_tilt = (axis_flags & GDK_AXIS_FLAG_YTILT) != 0;
849 gboolean read_pressure = FALSE;
850 gboolean read_x_tilt = FALSE;
851 gboolean read_y_tilt = FALSE;
852 gboolean map_pressure_source = FALSE;
853 gboolean map_xtilt_source = FALSE;
854 gboolean map_ytilt_source = FALSE;
855 gboolean state_pressure_source = FALSE;
856 gboolean state_pressure_event = FALSE;
857 gboolean state_xtilt_source = FALSE;
858 gboolean state_ytilt_source = FALSE;
859 gboolean fallback_pressure = FALSE;
860 gboolean fallback_tilt = FALSE;
861 const char *fallback_device_name = NULL;
862 GdkDeviceTool *tool = gdk_event_get_device_tool((GdkEvent *)event);
863 const int tool_type = tool ? (int)gdk_device_tool_get_tool_type(tool) : -1;
864 const gboolean tool_is_stylus
865 = tool && (tool_type == GDK_DEVICE_TOOL_TYPE_PEN || tool_type == GDK_DEVICE_TOOL_TYPE_ERASER
866 || tool_type == GDK_DEVICE_TOOL_TYPE_BRUSH || tool_type == GDK_DEVICE_TOOL_TYPE_PENCIL
867 || tool_type == GDK_DEVICE_TOOL_TYPE_AIRBRUSH);
868 gboolean is_tablet_like = supports_pressure || supports_x_tilt || supports_y_tilt || tool_is_stylus;
869 GdkWindow *window = gdk_event_get_window((GdkEvent *)event);
870 if(IS_NULL_PTR(window) && darktable.gui) window = gtk_widget_get_window(dt_ui_center(darktable.gui->ui));
871
872 {
873 double pressure = 0.0;
874 if(_get_axis_value_for_source(event, source_device, GDK_AXIS_PRESSURE, &pressure, &map_pressure_source))
875 {
876 read_pressure = TRUE;
877 input.pressure = _clamp01d(pressure);
878 input.has_pressure = TRUE;
881 }
882 else if(is_tablet_like && _tablet_motion_state.have_pressure)
883 {
885 input.has_pressure = TRUE;
886 }
887 else if(!is_tablet_like)
888 {
890 }
891 }
892
893 if(input.has_pressure && input.pressure <= 0.0 && window)
894 {
895 double p_state = 0.0;
896 if(source_device && _sample_axis_from_device_state(window, source_device, GDK_AXIS_PRESSURE, &p_state))
897 {
898 input.pressure = _clamp01d(p_state);
899 state_pressure_source = TRUE;
900 }
901 else if(event_device
902 && _sample_axis_from_device_state(window, event_device, GDK_AXIS_PRESSURE, &p_state))
903 {
904 input.pressure = _clamp01d(p_state);
905 state_pressure_event = TRUE;
906 }
907 }
908
909 {
910 double x_tilt = 0.0;
911 double y_tilt = 0.0;
912 const gboolean has_x_tilt
913 = _get_axis_value_for_source(event, source_device, GDK_AXIS_XTILT, &x_tilt, &map_xtilt_source);
914 const gboolean has_y_tilt
915 = _get_axis_value_for_source(event, source_device, GDK_AXIS_YTILT, &y_tilt, &map_ytilt_source);
916 read_x_tilt = has_x_tilt;
917 read_y_tilt = has_y_tilt;
918 if(has_x_tilt || has_y_tilt)
919 {
920 input.tilt_x = CLAMP(x_tilt, -1.0, 1.0);
921 input.tilt_y = CLAMP(y_tilt, -1.0, 1.0);
922 input.has_tilt = TRUE;
926 }
927 else if(is_tablet_like && _tablet_motion_state.have_tilt)
928 {
931 input.has_tilt = TRUE;
932 }
933 else if(!is_tablet_like)
934 {
936 }
937 }
938
939 if(!input.has_tilt && window)
940 {
941 double tx_state = 0.0, ty_state = 0.0;
942 const gboolean has_tx = source_device && _sample_axis_from_device_state(window, source_device, GDK_AXIS_XTILT, &tx_state);
943 const gboolean has_ty = source_device && _sample_axis_from_device_state(window, source_device, GDK_AXIS_YTILT, &ty_state);
944 if(has_tx || has_ty)
945 {
946 input.tilt_x = CLAMP(tx_state, -1.0, 1.0);
947 input.tilt_y = CLAMP(ty_state, -1.0, 1.0);
948 input.has_tilt = TRUE;
952 state_xtilt_source = has_tx;
953 state_ytilt_source = has_ty;
954 }
955 }
956
957 if(input.has_tilt)
958 input.tilt = _clamp01d(hypot(input.tilt_x, input.tilt_y));
959 else
960 input.tilt = 0.0;
961
962 if(!input.has_pressure || !input.has_tilt)
963 {
964 double fb_pressure = 0.0;
965 gboolean fb_have_pressure = FALSE;
966 double fb_tilt_x = 0.0;
967 double fb_tilt_y = 0.0;
968 gboolean fb_have_tilt = FALSE;
969 if(_sample_tablet_state_from_devices(event, &fb_pressure, &fb_have_pressure,
970 &fb_tilt_x, &fb_tilt_y, &fb_have_tilt,
971 &fallback_device_name))
972 {
973 if(!input.has_pressure && fb_have_pressure)
974 {
975 input.pressure = _clamp01d(fb_pressure);
976 input.has_pressure = TRUE;
977 fallback_pressure = TRUE;
978 }
979 if(!input.has_tilt && fb_have_tilt)
980 {
981 input.tilt_x = CLAMP(fb_tilt_x, -1.0, 1.0);
982 input.tilt_y = CLAMP(fb_tilt_y, -1.0, 1.0);
983 input.tilt = _clamp01d(hypot(input.tilt_x, input.tilt_y));
984 input.has_tilt = TRUE;
985 fallback_tilt = TRUE;
986 }
987 }
988 }
989
990 if(input.has_pressure || input.has_tilt) is_tablet_like = TRUE;
991
992 if(reset_kinematics)
993 {
999 input.acceleration = 0.0;
1000 return input;
1001 }
1002
1004 {
1005 const double dt_s = MAX((double)(time_ms - _tablet_motion_state.time_ms), 1.0) * 1e-3;
1006 const double dx = x - _tablet_motion_state.x;
1007 const double dy = y - _tablet_motion_state.y;
1008 const double speed_px_s = hypot(dx, dy) / dt_s;
1009 const double accel_px_s2 = fabs(speed_px_s - _tablet_motion_state.speed_px_s) / dt_s;
1010 /* Normalize acceleration for stylus mapping. 25000 px/s² keeps a useful
1011 * dynamic range while clipping extreme event jitter. */
1012 input.acceleration = _clamp01d(accel_px_s2 / 25000.0);
1013 _tablet_motion_state.speed_px_s = speed_px_s;
1014 }
1015 else
1016 {
1017 input.acceleration = 0.0;
1019 }
1020
1024 _tablet_motion_state.time_ms = time_ms;
1025
1027 "[tablet] %s dev='%s' src_dev='%s' evt_dev='%s' src=%d tablet=%d tool=%d supports[p=%d xt=%d yt=%d] read[p=%d xt=%d yt=%d] map_src[p=%d xt=%d yt=%d] state[p_src=%d p_evt=%d xt_src=%d yt_src=%d] fallback[p=%d t=%d dev='%s'] values[p=%.4f tx=%.4f ty=%.4f t=%.4f a=%.4f] xy=(%.1f, %.1f) t_ms=%u reset=%d\n",
1028 tag ? tag : "event",
1029 device ? gdk_device_get_name(device) : "<none>",
1030 source_device ? gdk_device_get_name(source_device) : "<none>",
1031 event_device ? gdk_device_get_name(event_device) : "<none>",
1032 (int)source,
1033 is_tablet_like ? 1 : 0,
1034 tool_type,
1035 supports_pressure ? 1 : 0,
1036 supports_x_tilt ? 1 : 0,
1037 supports_y_tilt ? 1 : 0,
1038 read_pressure ? 1 : 0,
1039 read_x_tilt ? 1 : 0,
1040 read_y_tilt ? 1 : 0,
1041 map_pressure_source ? 1 : 0,
1042 map_xtilt_source ? 1 : 0,
1043 map_ytilt_source ? 1 : 0,
1044 state_pressure_source ? 1 : 0,
1045 state_pressure_event ? 1 : 0,
1046 state_xtilt_source ? 1 : 0,
1047 state_ytilt_source ? 1 : 0,
1048 fallback_pressure ? 1 : 0,
1049 fallback_tilt ? 1 : 0,
1050 fallback_device_name ? fallback_device_name : "<none>",
1051 input.has_pressure ? input.pressure : -1.0,
1052 input.has_tilt ? input.tilt_x : 0.0,
1053 input.has_tilt ? input.tilt_y : 0.0,
1054 input.has_tilt ? input.tilt : 0.0,
1055 input.acceleration,
1056 x, y,
1057 time_ms,
1058 reset_kinematics ? 1 : 0);
1059
1060 return input;
1061}
1062
1063static gboolean _button_pressed(GtkWidget *w, GdkEventButton *event, gpointer user_data)
1064{
1065 if(!gtk_window_is_active(GTK_WINDOW(darktable.gui->ui->main_window))) return FALSE;
1066
1067 /* Reset Gtk focus */
1069 gtk_widget_grab_focus(w);
1070
1071 const dt_control_pointer_input_t input = _extract_pointer_input((const GdkEvent *)event, event->x, event->y,
1072 event->time, TRUE, "button-press");
1074 const double pressure = input.has_pressure ? input.pressure : 1.0;
1075 dt_control_button_pressed(event->x, event->y, pressure, event->button, event->type, event->state & 0xf);
1076 return FALSE;
1077}
1078
1079static gboolean _button_released(GtkWidget *w, GdkEventButton *event, gpointer user_data)
1080{
1081 if(!gtk_window_is_active(GTK_WINDOW(darktable.gui->ui->main_window))) return FALSE;
1082 const dt_control_pointer_input_t input = _extract_pointer_input((const GdkEvent *)event, event->x, event->y,
1083 event->time, FALSE, "button-release");
1085 dt_control_button_released(event->x, event->y, event->button, event->state & 0xf);
1086
1087 return TRUE;
1088}
1089
1090static gboolean _mouse_moved(GtkWidget *w, GdkEventMotion *event, gpointer user_data)
1091{
1092 if(!gtk_window_is_active(GTK_WINDOW(darktable.gui->ui->main_window))) return FALSE;
1093
1094 const dt_control_pointer_input_t input = _extract_pointer_input((const GdkEvent *)event, event->x, event->y,
1095 event->time, FALSE, "motion");
1097 dt_control_mouse_moved(event->x, event->y, input.has_pressure ? input.pressure : 1.0, event->state & 0xf);
1098 return FALSE;
1099}
1100
1101#ifdef _WIN32
1102/* Arbitrary stable subclass identifier encoded as ASCII "ASNN".
1103 * It only needs to stay unique within this process for SetWindowSubclass(). */
1104#define DT_WIN32_CURSOR_SUBCLASS_CENTER ((UINT_PTR)0x41534e4e)
1105
1106static LRESULT CALLBACK _center_win32_cursor_proc(HWND hwnd, UINT message, WPARAM w_param, LPARAM l_param,
1107 UINT_PTR subclass_id, DWORD_PTR ref_data)
1108{
1109 /* On Win32, DefSubclassProc() answers WM_SETCURSOR for the drawing area in center view by
1110 * restoring the window-class default arrow on every mouse move. The center
1111 * view already selected the proper cursor through GDK, so swallow the
1112 * client-area reset and keep the current cursor unchanged until the view
1113 * requests another explicit cursor change. */
1114 if(subclass_id == DT_WIN32_CURSOR_SUBCLASS_CENTER && message == WM_SETCURSOR && LOWORD(l_param) == HTCLIENT)
1115 return TRUE;
1116
1117 return DefSubclassProc(hwnd, message, w_param, l_param);
1118}
1119
1120static void _center_realize(GtkWidget *widget, gpointer user_data)
1121{
1122 GdkWindow *center_window = gtk_widget_get_window(widget);
1123 HWND center_hwnd = center_window ? (HWND)gdk_win32_window_get_handle(center_window) : NULL;
1124 if(!IS_NULL_PTR(center_hwnd))
1125 SetWindowSubclass(center_hwnd, _center_win32_cursor_proc, DT_WIN32_CURSOR_SUBCLASS_CENTER, (DWORD_PTR)widget);
1126}
1127
1128static void _center_unrealize(GtkWidget *widget, gpointer user_data)
1129{
1130 GdkWindow *center_window = gtk_widget_get_window(widget);
1131 HWND center_hwnd = center_window ? (HWND)gdk_win32_window_get_handle(center_window) : NULL;
1132 if(!IS_NULL_PTR(center_hwnd))
1133 RemoveWindowSubclass(center_hwnd, _center_win32_cursor_proc, DT_WIN32_CURSOR_SUBCLASS_CENTER);
1134}
1135#endif
1136
1137static gboolean _key_pressed(GtkWidget *w, GdkEventKey *event)
1138{
1139 if(!gtk_window_is_active(GTK_WINDOW(darktable.gui->ui->main_window))) return FALSE;
1141 return TRUE;
1142}
1143
1144static gboolean _center_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1145{
1147 return TRUE;
1148}
1149
1150static gboolean _center_enter(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1151{
1153 return TRUE;
1154}
1155
1156static const char* _get_source_name(int pos)
1157{
1158 static const gchar *SOURCE_NAMES[]
1159 = { "GDK_SOURCE_MOUSE", "GDK_SOURCE_PEN", "GDK_SOURCE_ERASER", "GDK_SOURCE_CURSOR",
1160 "GDK_SOURCE_KEYBOARD", "GDK_SOURCE_TOUCHSCREEN", "GDK_SOURCE_TOUCHPAD", "GDK_SOURCE_TRACKPOINT",
1161 "GDK_SOURCE_TABLET_PAD" };
1162 if(pos >= G_N_ELEMENTS(SOURCE_NAMES)) return "<UNKNOWN>";
1163 return SOURCE_NAMES[pos];
1164}
1165
1166static const char* _get_mode_name(int pos)
1167{
1168 static const gchar *MODE_NAMES[] = { "GDK_MODE_DISABLED", "GDK_MODE_SCREEN", "GDK_MODE_WINDOW" };
1169 if(pos >= G_N_ELEMENTS(MODE_NAMES)) return "<UNKNOWN>";
1170 return MODE_NAMES[pos];
1171}
1172
1173static const char* _get_axis_name(int pos)
1174{
1175 static const gchar *AXIS_NAMES[]
1176 = { "GDK_AXIS_IGNORE", "GDK_AXIS_X", "GDK_AXIS_Y", "GDK_AXIS_PRESSURE",
1177 "GDK_AXIS_XTILT", "GDK_AXIS_YTILT", "GDK_AXIS_WHEEL", "GDK_AXIS_DISTANCE",
1178 "GDK_AXIS_ROTATION", "GDK_AXIS_SLIDER", "GDK_AXIS_LAST" };
1179 if(pos >= G_N_ELEMENTS(AXIS_NAMES)) return "<UNKNOWN>";
1180 return AXIS_NAMES[pos];
1181}
1182
1184{
1185 /* lets zero mem */
1186 memset(gui, 0, sizeof(dt_gui_gtk_t));
1187
1188 dt_pthread_mutex_init(&gui->mutex, NULL);
1189
1190 // force gtk3 to use normal scroll bars instead of the popup thing. they get in the way of controls
1191 // the alternative would be to gtk_scrolled_window_set_overlay_scrolling(..., FALSE); every single widget
1192 // that might have scroll bars
1193 g_setenv("GTK_OVERLAY_SCROLLING", "0", 0);
1194
1195 // same for ubuntus overlay-scrollbar-gtk3
1196 g_setenv("LIBOVERLAY_SCROLLBAR", "0", 0);
1197
1198 // unset gtk rc from kde:
1199 char path[PATH_MAX] = { 0 }, datadir[PATH_MAX] = { 0 }, sharedir[PATH_MAX] = { 0 }, configdir[PATH_MAX] = { 0 };
1200 dt_loc_get_datadir(datadir, sizeof(datadir));
1201 dt_loc_get_sharedir(sharedir, sizeof(sharedir));
1202 dt_loc_get_user_config_dir(configdir, sizeof(configdir));
1203
1204 const char *css_theme = dt_conf_get_string_const("ui_last/theme");
1205 if(css_theme)
1206 g_strlcpy(gui->gtkrc, css_theme, sizeof(gui->gtkrc));
1207 else
1208 g_snprintf(gui->gtkrc, sizeof(gui->gtkrc), "ansel");
1209
1210#ifdef MAC_INTEGRATION
1211#ifdef GTK_TYPE_OSX_APPLICATION
1212 GtkOSXApplication *OSXApp = g_object_new(GTK_TYPE_OSX_APPLICATION, NULL);
1213 gtk_osxapplication_set_menu_bar(
1214 OSXApp, GTK_MENU_SHELL(gtk_menu_bar_new())); // needed for default entries to show up
1215#else
1216 GtkosxApplication *OSXApp = g_object_new(GTKOSX_TYPE_APPLICATION, NULL);
1217 gtkosx_application_set_menu_bar(
1218 OSXApp, GTK_MENU_SHELL(gtk_menu_bar_new())); // needed for default entries to show up
1219#endif
1220 g_signal_connect(G_OBJECT(OSXApp), "NSApplicationBlockTermination", G_CALLBACK(_osx_quit_callback), NULL);
1221 g_signal_connect(G_OBJECT(OSXApp), "NSApplicationOpenFile", G_CALLBACK(_osx_openfile_callback), NULL);
1222#endif
1223
1224 GtkWidget *widget;
1225 gui->ui = g_malloc0(sizeof(dt_ui_t));
1226 gui->surface = NULL;
1227 gui->center_tooltip = 0;
1228 gui->culling_mode = FALSE;
1229 gui->selection_stacked = FALSE;
1230 gui->presets_popup_menu = NULL;
1231 gui->last_preset = NULL;
1232 gui->export_popup.window = NULL;
1233 gui->export_popup.module = NULL;
1234 gui->styles_popup.window = NULL;
1235 gui->styles_popup.module = NULL;
1236
1237 // smooth scrolling must be enabled to handle trackpad/touch events
1238 gui->scroll_mask = GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK;
1239
1240 // Emulates the same feature as Gtk focus but for scrolling events
1241 // The GtkWidget capturing scrolling events will write its address in this pointer
1242 gui->has_scroll_focus = NULL;
1243
1244 // Init global accels. We localize the config because accels pathes use translated GUI labels.
1245 // User switching between languages may loose their custom shortcuts if we didn't localize them.
1246 // NOTE: needs to be inited before widgets, more specifically before the global menu
1247 gchar *keyboardrc = g_strdup_printf("keyboardrc.%s", dt_l10n_get_current_lang(darktable.l10n));
1248 gchar *keyboardrc_path = g_build_filename(configdir, keyboardrc, NULL);
1249
1250 GtkAccelFlags flags = 0;
1251 if(dt_conf_get_bool("accels/mask")) flags |= GTK_ACCEL_MASK;
1252 gui->accels = dt_accels_init(keyboardrc_path, flags);
1253 dt_free(keyboardrc);
1254 dt_free(keyboardrc_path);
1255
1256 // Initializing widgets
1257 _init_widgets(gui);
1258
1259 //init overlay colors
1261
1262 dt_concat_path_file(path, datadir, "icons");
1263 gtk_icon_theme_append_search_path(gtk_icon_theme_get_default(), path);
1264 dt_concat_path_file(path, sharedir, "icons");
1265 gtk_icon_theme_append_search_path(gtk_icon_theme_get_default(), path);
1266
1268 widget = center;
1269
1270 gtk_widget_set_can_focus(widget, TRUE);
1271 gtk_widget_set_visible(widget, TRUE);
1272 gtk_widget_grab_focus(widget);
1273 gtk_widget_add_events(widget, GDK_PROXIMITY_IN_MASK | GDK_PROXIMITY_OUT_MASK | GDK_TABLET_PAD_MASK);
1274 g_signal_connect(G_OBJECT(widget), "configure-event", G_CALLBACK(_configure), gui);
1275 g_signal_connect(G_OBJECT(widget), "draw", G_CALLBACK(_draw), NULL);
1276 g_signal_connect(G_OBJECT(widget), "motion-notify-event", G_CALLBACK(_mouse_moved), NULL);
1277 g_signal_connect(G_OBJECT(widget), "key-press-event", G_CALLBACK(_key_pressed), NULL);
1278 g_signal_connect(G_OBJECT(widget), "leave-notify-event", G_CALLBACK(_center_leave), NULL);
1279 g_signal_connect(G_OBJECT(widget), "enter-notify-event", G_CALLBACK(_center_enter), NULL);
1280 g_signal_connect(G_OBJECT(widget), "button-press-event", G_CALLBACK(_button_pressed), NULL);
1281 g_signal_connect(G_OBJECT(widget), "button-release-event", G_CALLBACK(_button_released), NULL);
1282 g_signal_connect(G_OBJECT(widget), "scroll-event", G_CALLBACK(_scrolled), NULL);
1283#ifdef _WIN32
1284 g_signal_connect(G_OBJECT(widget), "realize", G_CALLBACK(_center_realize), NULL);
1285 g_signal_connect(G_OBJECT(widget), "unrealize", G_CALLBACK(_center_unrealize), NULL);
1286 if(gtk_widget_get_realized(widget))
1287 _center_realize(widget, NULL);
1288#endif
1289
1291
1293 // update the profile when the window is moved. resize is already handled in configure()
1294 widget = dt_ui_main_window(darktable.gui->ui);
1295 g_signal_connect(G_OBJECT(widget), "configure-event", G_CALLBACK(_window_configure), NULL);
1296
1298
1299 // load theme
1301
1302 // let's try to support pressure sensitive input devices like tablets for mask drawing
1303 dt_print(DT_DEBUG_INPUT, "[input device] Input devices found:\n\n");
1304
1305 GList *input_devices
1306 = gdk_seat_get_slaves(gdk_display_get_default_seat(gdk_display_get_default()), GDK_SEAT_CAPABILITY_ALL);
1307 const int manager_slave_count = 0;
1308 const int manager_floating_count = 0;
1309 GList *stylus_devices
1310 = gdk_seat_get_slaves(gdk_display_get_default_seat(gdk_display_get_default()), GDK_SEAT_CAPABILITY_TABLET_STYLUS);
1311 dt_print(DT_DEBUG_INPUT, "[input device] seat capabilities bitmask: %u\n",
1312 (unsigned int)gdk_seat_get_capabilities(gdk_display_get_default_seat(gdk_display_get_default())));
1313 dt_print(DT_DEBUG_INPUT, "[input device] stylus-capable devices reported by seat: %d\n", g_list_length(stylus_devices));
1314 dt_print(DT_DEBUG_INPUT, "[input device] manager fallback devices: slave=%d floating=%d merged_total=%d\n",
1315 manager_slave_count, manager_floating_count, g_list_length(input_devices));
1316 for(GList *l = stylus_devices; !IS_NULL_PTR(l); l = g_list_next(l))
1317 {
1318 GdkDevice *device = (GdkDevice *)l->data;
1319 if(IS_NULL_PTR(device)) continue;
1320 dt_print(DT_DEBUG_INPUT, " [tablet seat] %s source=%s axes_flags=%u n_axes=%d\n",
1321 gdk_device_get_name(device), _get_source_name(gdk_device_get_source(device)),
1322 (unsigned int)gdk_device_get_axes(device), gdk_device_get_n_axes(device));
1323 }
1324 if(stylus_devices)
1325 {
1326 g_list_free(stylus_devices);
1327 stylus_devices = NULL;
1328 }
1329 for(GList *l = input_devices; !IS_NULL_PTR(l); l = g_list_next(l))
1330 {
1331 GdkDevice *device = (GdkDevice *)l->data;
1332 if(IS_NULL_PTR(device)) continue;
1333 const GdkInputSource source = gdk_device_get_source(device);
1334 const gint n_axes = (source == GDK_SOURCE_KEYBOARD ? 0 : gdk_device_get_n_axes(device));
1335
1336 // force-enable everything we find in screen mode.
1337 // TODO: make that an user param ?
1338 gdk_device_set_mode(device, GDK_MODE_SCREEN);
1339
1340 dt_print(DT_DEBUG_INPUT, "%s (%s), source: %s, mode: %s, %d axes, %d keys\n", gdk_device_get_name(device),
1341 (source != GDK_SOURCE_KEYBOARD) && gdk_device_get_has_cursor(device) ? "with cursor" : "no cursor",
1342 _get_source_name(source),
1343 _get_mode_name(gdk_device_get_mode(device)), n_axes,
1344 source != GDK_SOURCE_KEYBOARD ? gdk_device_get_n_keys(device) : 0);
1345
1346 for(int i = 0; i < n_axes; i++)
1347 {
1348 dt_print(DT_DEBUG_INPUT, " %s\n", _get_axis_name(gdk_device_get_axis_use(device, i)));
1349 }
1350 dt_print(DT_DEBUG_INPUT, "\n");
1351 }
1352 if(input_devices)
1353 {
1354 g_list_free(input_devices);
1355 input_devices = NULL;
1356 }
1357
1358 // Gtk seems to capture some reserved shortcuts (Tab). We need to bypass it entirely
1359 // by hacking all events.
1360 gtk_widget_add_events(dt_ui_main_window(gui->ui), gui->scroll_mask);
1361 g_signal_connect(G_OBJECT(dt_ui_main_window(gui->ui)), "event", G_CALLBACK(dt_accels_dispatch), gui->accels);
1362
1363 // finally set the cursor to be the default.
1364 // for some reason this is needed on some systems to pick up the correctly themed cursor
1365 dt_control_change_cursor(GDK_LEFT_PTR);
1367
1368 return 0;
1369}
1370
1372{
1374 GtkAllocation allocation;
1375 gtk_widget_get_allocation(widget, &allocation);
1376
1377 if(darktable.gui->surface)
1378 {
1379 cairo_surface_destroy(darktable.gui->surface);
1380 darktable.gui->surface = NULL;
1381 }
1382
1384 = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
1385 // need to pre-configure views to avoid crash caused by draw coming before configure-event
1387 const int tb = darktable.control->tabborder;
1388 dt_view_manager_configure(darktable.view_manager, allocation.width - 2 * tb, allocation.height - 2 * tb);
1389#ifdef MAC_INTEGRATION
1390#ifdef GTK_TYPE_OSX_APPLICATION
1391 gtk_osxapplication_ready(g_object_new(GTK_TYPE_OSX_APPLICATION, NULL));
1392#else
1393 gtkosx_application_ready(g_object_new(GTKOSX_TYPE_APPLICATION, NULL));
1394#endif
1395#endif
1396#ifdef GDK_WINDOWING_QUARTZ
1398#endif
1399 /* start the event loop */
1400 gtk_main();
1401
1402 if (darktable.gui->surface)
1403 {
1404 cairo_surface_destroy(darktable.gui->surface);
1405 darktable.gui->surface = NULL;
1406 }
1407 dt_cleanup();
1408}
1409
1410// refactored function to read current ppd, because gtk for osx has been unreliable
1411// we use the specific function here. Anyway, if nothing meaningful is found we default back to 1.0
1413{
1414 double res = 0.0f;
1415#ifdef GDK_WINDOWING_QUARTZ
1416 res = dt_osx_get_ppd();
1417#else
1418 res = gtk_widget_get_scale_factor(widget);
1419#endif
1420 if((res < 1.0f) || (res > 4.0f))
1421 {
1422 dt_print(DT_DEBUG_CONTROL, "[dt_get_system_gui_ppd] can't detect system ppd\n");
1423 return 1.0f;
1424 }
1425 dt_print(DT_DEBUG_CONTROL, "[dt_get_system_gui_ppd] system ppd is %f\n", res);
1426 return res;
1427}
1428
1430{
1431 GtkWidget *widget = gui->ui->main_window;
1432
1433 gui->ppd = dt_get_system_gui_ppd(widget);
1434 gui->filter_image = CAIRO_FILTER_GOOD;
1435
1436 // get the screen resolution
1437 const float screen_dpi_overwrite = dt_conf_get_float("screen_dpi_overwrite");
1438 if(screen_dpi_overwrite > 0.0)
1439 {
1440 gui->dpi = screen_dpi_overwrite;
1441 gdk_screen_set_resolution(gtk_widget_get_screen(widget), screen_dpi_overwrite);
1442 dt_print(DT_DEBUG_CONTROL, "[screen resolution] setting the screen resolution to %f dpi as specified in "
1443 "the configuration file\n",
1444 screen_dpi_overwrite);
1445 }
1446 else
1447 {
1448#ifdef GDK_WINDOWING_QUARTZ
1449 dt_osx_autoset_dpi(widget);
1450#endif
1451 gui->dpi = gdk_screen_get_resolution(gtk_widget_get_screen(widget));
1452 if(gui->dpi < 0.0)
1453 {
1454 gui->dpi = 96.0;
1455 gdk_screen_set_resolution(gtk_widget_get_screen(widget), 96.0);
1456 dt_print(DT_DEBUG_CONTROL, "[screen resolution] setting the screen resolution to the default 96 dpi\n");
1457 }
1458 else
1459 dt_print(DT_DEBUG_CONTROL, "[screen resolution] setting the screen resolution to %f dpi\n", gui->dpi);
1460 }
1461 gui->dpi_factor
1462 = gui->dpi / 96; // according to man xrandr and the docs of gdk_screen_set_resolution 96 is the default
1463
1464 // em depends on the screen DPI (point -> px), so refresh it here too.
1466}
1467
1468// Last DT_GUI_BOX_SPACING value actually applied to containers. Used to retarget exactly
1469// the containers that carry the standard spacing when em changes, leaving deliberate 0-spacing
1470// and custom-spacing containers untouched. Seeded with the pre-em reference (10px).
1471static gint _last_box_spacing = 10;
1472
1473typedef struct _spacing_ctx_t { gint old_s, new_s; } _spacing_ctx_t;
1474
1475// Recursively retarget GtkBox/GtkGrid/GtkFlowBox children whose spacing still equals the
1476// previously-applied standard spacing. Setting spacing inside gtk_container_foreach() doesn't
1477// mutate the child list, so the walk is safe.
1478static void _refresh_container_spacing(GtkWidget *w, gpointer user_data)
1479{
1480 const _spacing_ctx_t *c = (const _spacing_ctx_t *)user_data;
1481
1482 if(GTK_IS_BOX(w))
1483 {
1484 if(gtk_box_get_spacing(GTK_BOX(w)) == c->old_s) gtk_box_set_spacing(GTK_BOX(w), c->new_s);
1485 }
1486 else if(GTK_IS_FLOW_BOX(w))
1487 {
1488 if((gint)gtk_flow_box_get_row_spacing(GTK_FLOW_BOX(w)) == c->old_s)
1489 gtk_flow_box_set_row_spacing(GTK_FLOW_BOX(w), c->new_s);
1490 if((gint)gtk_flow_box_get_column_spacing(GTK_FLOW_BOX(w)) == c->old_s)
1491 gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(w), c->new_s);
1492 }
1493 else if(GTK_IS_GRID(w))
1494 {
1495 if((gint)gtk_grid_get_row_spacing(GTK_GRID(w)) == c->old_s)
1496 gtk_grid_set_row_spacing(GTK_GRID(w), c->new_s);
1497 if((gint)gtk_grid_get_column_spacing(GTK_GRID(w)) == c->old_s)
1498 gtk_grid_set_column_spacing(GTK_GRID(w), c->new_s);
1499 }
1500
1501 if(GTK_IS_CONTAINER(w))
1502 gtk_container_foreach(GTK_CONTAINER(w), _refresh_container_spacing, user_data);
1503}
1504
1505// Propagate a new DT_GUI_BOX_SPACING to already-built containers across every toplevel, so a
1506// runtime font/DPI change updates the inner gutters live (gtk_*_set_spacing bakes the value into
1507// the widget at creation time, so reloading the CSS alone is not enough).
1509{
1510 const gint new_s = DT_GUI_BOX_SPACING;
1511 if(new_s == _last_box_spacing) return;
1512
1513 _spacing_ctx_t c = { _last_box_spacing, new_s };
1514 GList *toplevels = gtk_window_list_toplevels(); // list owned by us, elements not reffed
1515 for(GList *l = toplevels; l; l = l->next)
1516 _refresh_container_spacing(GTK_WIDGET(l->data), &c);
1517 g_list_free(toplevels);
1518
1519 _last_box_spacing = new_s;
1520}
1521
1523{
1524 dt_gui_gtk_t *gui = darktable.gui;
1525 if(!gui || !gui->ui || !gui->ui->main_window) return;
1526
1527 GtkStyleContext *ctx = gtk_widget_get_style_context(gui->ui->main_window);
1528 PangoFontDescription *desc = NULL;
1529 gtk_style_context_get(ctx, gtk_style_context_get_state(ctx), GTK_STYLE_PROPERTY_FONT, &desc, NULL);
1530 if(!desc) return;
1531
1532 const gint size = pango_font_description_get_size(desc);
1533 if(size > 0)
1534 {
1535 if(pango_font_description_get_size_is_absolute(desc))
1536 // already device-independent px
1537 gui->em = (double)size / PANGO_SCALE;
1538 else
1539 // points -> px at the screen DPI, matching how GTK renders point-sized fonts
1540 gui->em = (double)size / PANGO_SCALE * gui->dpi / 72.0;
1541 }
1542 pango_font_description_free(desc);
1543
1544 // The new em may change DT_GUI_BOX_SPACING; push it to existing containers so the change is live.
1546}
1547
1548void dt_gui_set_pango_resolution(PangoLayout *layout)
1549{
1550 if(IS_NULL_PTR(layout) || !darktable.gui) return;
1551 // Cairo-drawn text is laid out in points; the screen DPI converts those to device-independent px,
1552 // matching how GTK renders the rest of the UI. Centralized here so call sites never hand-write the DPI.
1553 pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
1554}
1555
1557{
1558 if(IS_NULL_PTR(cr)) return;
1559
1560 // Source GTK's resolved text-rendering options (anti-aliasing, hinting, subpixel order,
1561 // hint-metrics/kerning), which GTK populates from GtkSettings/Xft/fontconfig. The widget's
1562 // Pango context is the same source native widgets use; fall back to the main window, then to the
1563 // screen defaults, so an off-screen/scratch Cairo surface never silently reverts to Cairo's
1564 // AA-on defaults (which would make our cairo-drawn text look unlike the rest of the UI).
1565 const cairo_font_options_t *fo = NULL;
1566
1567 if(widget)
1568 {
1569 PangoContext *pc = gtk_widget_get_pango_context(widget);
1570 if(pc) fo = pango_cairo_context_get_font_options(pc);
1571 }
1572 if(!fo && darktable.gui && darktable.gui->ui && darktable.gui->ui->main_window)
1573 {
1574 PangoContext *pc = gtk_widget_get_pango_context(darktable.gui->ui->main_window);
1575 if(pc) fo = pango_cairo_context_get_font_options(pc);
1576 }
1577 if(!fo)
1578 {
1579 GdkScreen *screen = gdk_screen_get_default();
1580 if(screen) fo = gdk_screen_get_font_options(screen);
1581 }
1582
1583 // cairo_set_font_options() copies internally, so the const pointer's lifetime is not a concern.
1584 if(fo) cairo_set_font_options(cr, fo);
1585}
1586
1587static gboolean _focus_in_out_event(GtkWidget *widget, GdkEvent *event, gpointer user_data)
1588{
1589 gtk_window_set_urgency_hint(GTK_WINDOW(widget), FALSE);
1590 return FALSE;
1591}
1592
1593static gboolean _ui_log_button_press_event(GtkWidget *widget, GdkEvent *event, gpointer user_data)
1594{
1595 gtk_widget_hide(GTK_WIDGET(user_data));
1596 return TRUE;
1597}
1598
1599static gboolean _ui_toast_button_press_event(GtkWidget *widget, GdkEvent *event, gpointer user_data)
1600{
1601 gtk_widget_hide(GTK_WIDGET(user_data));
1602 return TRUE;
1603}
1604
1606{
1608 GtkWidget *widget;
1609
1610 // Creating the main window
1611 gui->ui->main_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1612 gtk_widget_set_name(gui->ui->main_window , "main_window");
1613 gtk_window_set_role(GTK_WINDOW(gui->ui->main_window ), "main-app");
1614 gtk_window_set_icon_name(GTK_WINDOW(gui->ui->main_window ), "ansel");
1615 gtk_window_set_title(GTK_WINDOW(gui->ui->main_window ), "Ansel");
1616
1617 // Init the titlebar ASAP because we replace the desktop titlebar & decoration with ours
1618 dt_ui_init_titlebar(gui->ui);
1619
1621
1622 gtk_window_set_default_size(GTK_WINDOW(gui->ui->main_window), DT_PIXEL_APPLY_DPI(1200), DT_PIXEL_APPLY_DPI(800));
1623
1624 // NOTE: allowing full-screen on startup shits the bed with MacOS
1625 if(dt_conf_get_bool("ui_last/maximized"))
1626 {
1627 gboolean restore_window_position = TRUE;
1628#ifdef GDK_WINDOWING_WAYLAND
1629 GdkDisplay *display = gtk_widget_get_display(gui->ui->main_window);
1630 if(GDK_IS_WAYLAND_DISPLAY(display))
1631 restore_window_position = FALSE;
1632#endif
1633
1634 if(restore_window_position)
1635 {
1636 GdkDisplay *window_display = gtk_widget_get_display(gui->ui->main_window);
1637 GdkMonitor *monitor = NULL;
1638
1639 if(!IS_NULL_PTR(window_display))
1640 {
1641 if(dt_conf_key_exists("ui_last/window_monitor"))
1642 {
1643 const int monitor_index = dt_conf_get_int("ui_last/window_monitor");
1644 if(monitor_index >= 0 && monitor_index < gdk_display_get_n_monitors(window_display))
1645 monitor = gdk_display_get_monitor(window_display, monitor_index);
1646 }
1647
1648 if(IS_NULL_PTR(monitor)
1649 && dt_conf_key_exists("ui_last/window_x")
1650 && dt_conf_key_exists("ui_last/window_y"))
1651 {
1652 const int x = dt_conf_get_int("ui_last/window_x");
1653 const int y = dt_conf_get_int("ui_last/window_y");
1654 monitor = gdk_display_get_monitor_at_point(window_display, x, y);
1655 }
1656
1657 if(IS_NULL_PTR(monitor))
1658 monitor = gdk_display_get_primary_monitor(window_display);
1659 if(IS_NULL_PTR(monitor) && gdk_display_get_n_monitors(window_display) > 0)
1660 monitor = gdk_display_get_monitor(window_display, 0);
1661 }
1662
1663 if(!IS_NULL_PTR(monitor))
1664 {
1665 GdkRectangle workarea = { 0 };
1666 gdk_monitor_get_workarea(monitor, &workarea);
1667 gtk_window_move(GTK_WINDOW(gui->ui->main_window), workarea.x, workarea.y);
1668 }
1669 }
1670
1671 gtk_window_maximize(GTK_WINDOW(gui->ui->main_window));
1672 }
1673 else
1674 {
1675 int width = dt_conf_get_int("ui_last/window_width");
1676 int height = dt_conf_get_int("ui_last/window_height");
1677 gtk_window_resize(GTK_WINDOW(gui->ui->main_window), width, height);
1678
1679 gboolean restore_window_position = TRUE;
1680#ifdef GDK_WINDOWING_WAYLAND
1681 GdkDisplay *display = gtk_widget_get_display(gui->ui->main_window);
1682 if(GDK_IS_WAYLAND_DISPLAY(display))
1683 restore_window_position = FALSE;
1684#endif
1685
1686 if(restore_window_position
1687 && dt_conf_key_exists("ui_last/window_x")
1688 && dt_conf_key_exists("ui_last/window_y"))
1689 {
1690 const int x = dt_conf_get_int("ui_last/window_x");
1691 const int y = dt_conf_get_int("ui_last/window_y");
1692
1693 int clamped_x = x;
1694 int clamped_y = y;
1695 GdkDisplay *window_display = gtk_widget_get_display(gui->ui->main_window);
1696 GdkMonitor *monitor = NULL;
1697
1698 if(!IS_NULL_PTR(window_display))
1699 {
1700 if(dt_conf_key_exists("ui_last/window_monitor"))
1701 {
1702 const int monitor_index = dt_conf_get_int("ui_last/window_monitor");
1703 if(monitor_index >= 0 && monitor_index < gdk_display_get_n_monitors(window_display))
1704 monitor = gdk_display_get_monitor(window_display, monitor_index);
1705 }
1706
1707 if(IS_NULL_PTR(monitor))
1708 monitor = gdk_display_get_monitor_at_point(window_display, x + width / 2, y + height / 2);
1709 if(IS_NULL_PTR(monitor))
1710 monitor = gdk_display_get_primary_monitor(window_display);
1711 if(IS_NULL_PTR(monitor) && gdk_display_get_n_monitors(window_display) > 0)
1712 monitor = gdk_display_get_monitor(window_display, 0);
1713 }
1714
1715 if(!IS_NULL_PTR(monitor))
1716 {
1717 GdkRectangle workarea = { 0 };
1718 gdk_monitor_get_workarea(monitor, &workarea);
1719
1720 const int max_x = workarea.x + MAX(0, workarea.width - width);
1721 const int max_y = workarea.y + MAX(0, workarea.height - height);
1722 clamped_x = CLAMP(x, workarea.x, max_x);
1723 clamped_y = CLAMP(y, workarea.y, max_y);
1724 }
1725
1726 gtk_window_move(GTK_WINDOW(gui->ui->main_window), clamped_x, clamped_y);
1727 }
1728 }
1729
1731
1732 g_signal_connect(G_OBJECT(gui->ui->main_window ), "delete_event", G_CALLBACK(dt_gui_quit_callback), NULL);
1733 g_signal_connect(G_OBJECT(gui->ui->main_window ), "focus-in-event", G_CALLBACK(_focus_in_out_event), NULL);
1734 g_signal_connect(G_OBJECT(gui->ui->main_window ), "focus-out-event", G_CALLBACK(_focus_in_out_event), NULL);
1735 g_signal_connect_after(G_OBJECT(gui->ui->main_window ), "key-press-event", G_CALLBACK(_key_pressed), NULL);
1736
1737 container = gui->ui->main_window;
1738
1739 // Adding the outermost vbox
1740 widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1741 gtk_container_add(GTK_CONTAINER(container), widget);
1742 gtk_widget_show(widget);
1743
1744 /* connect to signal redraw all */
1746 G_CALLBACK(_ui_widget_redraw_callback), gui->ui->main_window);
1747
1748 container = widget;
1749
1750 // Initializing the main table
1752
1753 /* the log message */
1754 GtkWidget *eb = gtk_event_box_new();
1755 darktable.gui->ui->log_msg = gtk_label_new("");
1756 g_signal_connect(G_OBJECT(eb), "button-press-event", G_CALLBACK(_ui_log_button_press_event),
1758 gtk_label_set_ellipsize(GTK_LABEL(darktable.gui->ui->log_msg), PANGO_ELLIPSIZE_MIDDLE);
1759 dt_gui_add_class(darktable.gui->ui->log_msg, "dt_messages");
1760 gtk_container_add(GTK_CONTAINER(eb), darktable.gui->ui->log_msg);
1761 gtk_widget_set_valign(eb, GTK_ALIGN_CENTER);
1762 gtk_widget_set_halign(eb, GTK_ALIGN_CENTER);
1763 gtk_overlay_add_overlay(GTK_OVERLAY(darktable.gui->ui->center_base), eb);
1764 //gtk_overlay_reorder_overlay(GTK_OVERLAY(darktable.gui->ui->center_base), eb, -1);
1765
1766 /* the toast message */
1767 eb = gtk_event_box_new();
1768 darktable.gui->ui->toast_msg = gtk_label_new("");
1769 g_signal_connect(G_OBJECT(eb), "button-press-event", G_CALLBACK(_ui_toast_button_press_event),
1771 gtk_widget_set_events(eb, GDK_BUTTON_PRESS_MASK | darktable.gui->scroll_mask);
1772 g_signal_connect(G_OBJECT(eb), "scroll-event", G_CALLBACK(_scrolled), NULL);
1773 gtk_label_set_ellipsize(GTK_LABEL(darktable.gui->ui->toast_msg), PANGO_ELLIPSIZE_MIDDLE);
1774
1775 PangoAttrList *attrlist = pango_attr_list_new();
1776 PangoAttribute *attr = pango_attr_font_features_new("tnum");
1777 pango_attr_list_insert(attrlist, attr);
1778 gtk_label_set_attributes(GTK_LABEL(darktable.gui->ui->toast_msg), attrlist);
1779 pango_attr_list_unref(attrlist);
1780
1781 dt_gui_add_class(darktable.gui->ui->toast_msg, "dt_messages");
1782 gtk_container_add(GTK_CONTAINER(eb), darktable.gui->ui->toast_msg);
1783 gtk_widget_set_valign(eb, GTK_ALIGN_START);
1784 gtk_widget_set_halign(eb, GTK_ALIGN_CENTER);
1785 gtk_overlay_add_overlay(GTK_OVERLAY(darktable.gui->ui->center_base), eb);
1786 //gtk_overlay_reorder_overlay(GTK_OVERLAY(darktable.gui->ui->center_base), eb, -1);
1787
1788 /* update log message label */
1791
1792 /* update toast message label */
1795
1796
1797 // Showing everything
1798 gtk_widget_show_all(dt_ui_main_window(gui->ui));
1799
1800 gtk_widget_set_visible(dt_ui_log_msg(gui->ui), FALSE);
1801 gtk_widget_set_visible(dt_ui_toast_msg(gui->ui), FALSE);
1802}
1803
1805{
1806 g_return_if_fail(GTK_IS_CONTAINER(ui->containers[c]));
1807
1808 if(GTK_WIDGET(ui->containers[c]) != gtk_widget_get_parent(w)) return;
1809
1810 gtk_container_set_focus_child(GTK_CONTAINER(ui->containers[c]), w);
1811 gtk_widget_queue_draw(ui->containers[c]);
1812}
1813
1814void dt_ui_container_foreach(dt_ui_t *ui, const dt_ui_container_t c, GtkCallback callback)
1815{
1816 g_return_if_fail(GTK_IS_CONTAINER(ui->containers[c]));
1817 gtk_container_foreach(GTK_CONTAINER(ui->containers[c]), callback, (gpointer)ui->containers[c]);
1818}
1819
1824
1826{
1827 if(darktable.gui && !gtk_window_is_active(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui))))
1828 {
1829 gtk_window_set_urgency_hint(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), TRUE);
1830#ifdef MAC_INTEGRATION
1831#ifdef GTK_TYPE_OSX_APPLICATION
1832 gtk_osxapplication_attention_request(g_object_new(GTK_TYPE_OSX_APPLICATION, NULL), INFO_REQUEST);
1833#else
1834 gtkosx_application_attention_request(g_object_new(GTKOSX_TYPE_APPLICATION, NULL), INFO_REQUEST);
1835#endif
1836#endif
1837 }
1838}
1839
1840/* this is called as a signal handler, the signal raising logic asserts the gdk lock. */
1841static void _ui_widget_redraw_callback(gpointer instance, GtkWidget *widget)
1842{
1843 gtk_widget_queue_draw(widget);
1844}
1845
1846static void _ui_log_redraw_callback(gpointer instance, GtkWidget *widget)
1847{
1848 // draw log message, if any
1850 if(!GTK_IS_LABEL(widget))
1851 {
1853 return;
1854 }
1856 {
1857 if(strcmp(darktable.control->log_message[darktable.control->log_ack], gtk_label_get_text(GTK_LABEL(widget))))
1858 gtk_label_set_markup(GTK_LABEL(widget), darktable.control->log_message[darktable.control->log_ack]);
1859 gtk_widget_show(widget);
1860 }
1861 else
1862 {
1863 gtk_widget_hide(widget);
1864 }
1866}
1867
1868static void _ui_toast_redraw_callback(gpointer instance, GtkWidget *widget)
1869{
1870 // draw toast message, if any
1872 if(!GTK_IS_LABEL(widget))
1873 {
1875 return;
1876 }
1878 {
1879 if(strcmp(darktable.control->toast_message[darktable.control->toast_ack], gtk_label_get_text(GTK_LABEL(widget))))
1880 gtk_label_set_markup(GTK_LABEL(widget), darktable.control->toast_message[darktable.control->toast_ack]);
1881 if(!gtk_widget_get_visible(widget))
1882 {
1883 const int h = gtk_widget_get_allocated_height(dt_ui_center_base(darktable.gui->ui));
1884 gtk_widget_set_margin_bottom(gtk_widget_get_parent(widget), 0.15 * h - DT_PIXEL_APPLY_DPI(10));
1885 gtk_widget_show(widget);
1886 }
1887 }
1888 else
1889 {
1890 if(gtk_widget_get_visible(widget)) gtk_widget_hide(widget);
1891 }
1893}
1894
1895void dt_ellipsize_combo(GtkComboBox *cbox)
1896{
1897 GList *renderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(cbox));
1898 for(const GList *it = renderers; it; it = g_list_next(it))
1899 {
1900 GtkCellRendererText *tr = GTK_CELL_RENDERER_TEXT(it->data);
1901 g_object_set(G_OBJECT(tr), "ellipsize", PANGO_ELLIPSIZE_MIDDLE, (gchar *)0);
1902 }
1903 g_list_free(renderers);
1904 renderers = NULL;
1905}
1906
1913
1914static void _gtk_main_quit_safe(GtkWidget *widget, gpointer data)
1915{
1916 (void)widget;
1917 (void)data;
1918 if(gtk_main_level() > 0) gtk_main_quit();
1919}
1920
1921static void _yes_no_button_handler(GtkButton *button, gpointer data)
1922{
1923 result_t *result = (result_t *)data;
1924
1925 if((void *)button == (void *)result->button_yes)
1926 result->result = RESULT_YES;
1927 else if((void *)button == (void *)result->button_no)
1928 result->result = RESULT_NO;
1929
1930 if(result->entry)
1931 result->entry_text = g_strdup(gtk_entry_get_text(GTK_ENTRY(result->entry)));
1932 gtk_widget_destroy(result->window);
1933 _gtk_main_quit_safe(NULL, NULL);
1934}
1935
1936void dt_gui_refocus_parent(GtkWindow *parent)
1937{
1938 if(!GTK_IS_WINDOW(parent) && !IS_NULL_PTR(darktable.gui) && !IS_NULL_PTR(darktable.gui->ui)
1940 {
1941 GtkWidget *main_window = dt_ui_main_window(darktable.gui->ui);
1942 if(GTK_IS_WINDOW(main_window)) parent = GTK_WINDOW(main_window);
1943 }
1944
1945 if(GTK_IS_WINDOW(parent)) gtk_window_present(parent);
1946
1947#ifdef GDK_WINDOWING_QUARTZ
1949#endif
1950}
1951
1952gboolean dt_gui_show_standalone_yes_no_dialog(const char *title, const char *markup, const char *no_text,
1953 const char *yes_text)
1954{
1955 GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1956#ifdef GDK_WINDOWING_QUARTZ
1958#endif
1959
1960 // themes not yet loaded, no CSS add some manual padding
1961 const int padding = darktable.themes ? 0 : 5;
1962
1963 gtk_window_set_icon_name(GTK_WINDOW(window), "ansel");
1964 gtk_window_set_title(GTK_WINDOW(window), title);
1965 g_signal_connect(window, "destroy", G_CALLBACK(_gtk_main_quit_safe), NULL);
1966
1967 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
1968
1971 {
1972 GtkWidget *main_window = dt_ui_main_window(darktable.gui->ui);
1973 if(GTK_IS_WINDOW(main_window))
1974 {
1975 GtkWindow *win = GTK_WINDOW(main_window);
1976 gtk_window_set_transient_for(GTK_WINDOW(window), win);
1977 gtk_window_set_modal(GTK_WINDOW(window), TRUE);
1978 if(gtk_widget_get_visible(GTK_WIDGET(win)))
1979 {
1980 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ON_PARENT);
1981 }
1982 }
1983 }
1984
1985 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1986 gtk_container_add(GTK_CONTAINER(window), vbox);
1987
1988 GtkWidget *mhbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1989 gtk_box_pack_start(GTK_BOX(vbox), mhbox, TRUE, TRUE, padding);
1990
1991 if(padding)
1992 {
1993 gtk_box_pack_start(GTK_BOX(mhbox),
1994 gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING), TRUE, TRUE, padding);
1995 }
1996
1997 GtkWidget *label = gtk_label_new(NULL);
1998 gtk_label_set_markup(GTK_LABEL(label), markup);
1999 gtk_box_pack_start(GTK_BOX(mhbox), label, TRUE, TRUE, padding);
2000
2001 if(padding)
2002 {
2003 gtk_box_pack_start(GTK_BOX(mhbox),
2004 gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING), TRUE, TRUE, padding);
2005 }
2006
2007 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
2008 gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
2009
2010 result_t result = {.result = RESULT_NONE, .window = window};
2011
2012 GtkWidget *button;
2013
2014 if(no_text)
2015 {
2016 button = gtk_button_new_with_label(no_text);
2017 result.button_no = button;
2018 g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(_yes_no_button_handler), &result);
2019 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
2020 }
2021
2022 if(yes_text)
2023 {
2024 button = gtk_button_new_with_label(yes_text);
2025 result.button_yes = button;
2026 g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(_yes_no_button_handler), &result);
2027 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
2028 }
2029
2030 gtk_widget_show_all(window);
2031 gtk_main();
2032
2033 return result.result == RESULT_YES;
2034}
2035
2041
2042static void _three_choice_button_handler(GtkButton *button, gpointer data)
2043{
2045
2046 if((void *)button == (void *)result->button_first)
2047 result->result = 0;
2048 else if((void *)button == (void *)result->button_second)
2049 result->result = 1;
2050 else if((void *)button == (void *)result->button_third)
2051 result->result = 2;
2052
2053 gtk_widget_destroy(result->window);
2054 _gtk_main_quit_safe(NULL, NULL);
2055}
2056
2057int dt_gui_show_standalone_three_choice_dialog(const char *title, const char *markup, const char *first_text,
2058 const char *second_text, const char *third_text)
2059{
2060 GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
2061#ifdef GDK_WINDOWING_QUARTZ
2063#endif
2064
2065 // themes not yet loaded, no CSS add some manual padding
2066 const int padding = darktable.themes ? 0 : 5;
2067
2068 gtk_window_set_icon_name(GTK_WINDOW(window), "ansel");
2069 gtk_window_set_title(GTK_WINDOW(window), title);
2070 g_signal_connect(window, "destroy", G_CALLBACK(_gtk_main_quit_safe), NULL);
2071
2072 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
2073
2076 {
2077 GtkWidget *main_window = dt_ui_main_window(darktable.gui->ui);
2078 if(GTK_IS_WINDOW(main_window))
2079 {
2080 GtkWindow *win = GTK_WINDOW(main_window);
2081 gtk_window_set_transient_for(GTK_WINDOW(window), win);
2082 gtk_window_set_modal(GTK_WINDOW(window), TRUE);
2083 if(gtk_widget_get_visible(GTK_WIDGET(win)))
2084 {
2085 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ON_PARENT);
2086 }
2087 }
2088 }
2089
2090 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
2091 gtk_container_add(GTK_CONTAINER(window), vbox);
2092
2093 GtkWidget *mhbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
2094 gtk_box_pack_start(GTK_BOX(vbox), mhbox, TRUE, TRUE, padding);
2095
2096 if(padding)
2097 {
2098 gtk_box_pack_start(GTK_BOX(mhbox),
2099 gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING), TRUE, TRUE, padding);
2100 }
2101
2102 GtkWidget *label = gtk_label_new(NULL);
2103 gtk_label_set_markup(GTK_LABEL(label), markup);
2104 gtk_box_pack_start(GTK_BOX(mhbox), label, TRUE, TRUE, padding);
2105
2106 if(padding)
2107 {
2108 gtk_box_pack_start(GTK_BOX(mhbox),
2109 gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING), TRUE, TRUE, padding);
2110 }
2111
2112 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
2113 gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
2114
2115 _three_choice_result_t result = { .result = -1, .window = window };
2116
2117 GtkWidget *button;
2118
2119 if(first_text)
2120 {
2121 button = gtk_button_new_with_label(first_text);
2122 result.button_first = button;
2123 g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(_three_choice_button_handler), &result);
2124 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
2125 }
2126
2127 if(second_text)
2128 {
2129 button = gtk_button_new_with_label(second_text);
2130 result.button_second = button;
2131 g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(_three_choice_button_handler), &result);
2132 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
2133 }
2134
2135 if(third_text)
2136 {
2137 button = gtk_button_new_with_label(third_text);
2138 result.button_third = button;
2139 g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(_three_choice_button_handler), &result);
2140 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
2141 }
2142
2143 gtk_widget_show_all(window);
2144 gtk_main();
2145
2146 return result.result;
2147}
2148
2149char *dt_gui_show_standalone_string_dialog(const char *title, const char *markup, const char *placeholder,
2150 const char *no_text, const char *yes_text)
2151{
2152 GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
2153#ifdef GDK_WINDOWING_QUARTZ
2155#endif
2156
2157 gtk_window_set_icon_name(GTK_WINDOW(window), "ansel");
2158 gtk_window_set_title(GTK_WINDOW(window), title);
2159 g_signal_connect(window, "destroy", G_CALLBACK(_gtk_main_quit_safe), NULL);
2160
2161 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_MOUSE);
2162
2165 {
2166 GtkWidget *main_window = dt_ui_main_window(darktable.gui->ui);
2167 if(GTK_IS_WINDOW(main_window))
2168 {
2169 GtkWindow *win = GTK_WINDOW(main_window);
2170 gtk_window_set_transient_for(GTK_WINDOW(window), win);
2171 if(gtk_widget_get_visible(GTK_WIDGET(win)))
2172 {
2173 gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ON_PARENT);
2174 }
2175 }
2176 }
2177
2178 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
2179 gtk_widget_set_margin_start(vbox, 10);
2180 gtk_widget_set_margin_end(vbox, 10);
2181 gtk_widget_set_margin_top(vbox, 7);
2182 gtk_widget_set_margin_bottom(vbox, 5);
2183 gtk_container_add(GTK_CONTAINER(window), vbox);
2184
2185 GtkWidget *label = gtk_label_new(NULL);
2186 gtk_label_set_markup(GTK_LABEL(label), markup);
2187 gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, TRUE, 0);
2188
2189 GtkWidget *entry = gtk_entry_new();
2191
2192 g_object_ref(entry);
2193 if(placeholder)
2194 gtk_entry_set_placeholder_text(GTK_ENTRY(entry), placeholder);
2195 gtk_box_pack_start(GTK_BOX(vbox), entry, TRUE, TRUE, 0);
2196
2197 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
2198 gtk_widget_set_margin_top(hbox, 10);
2199 gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
2200
2201 result_t result = {.result = RESULT_NONE, .window = window, .entry = entry};
2202
2203 GtkWidget *button;
2204
2205 if(no_text)
2206 {
2207 button = gtk_button_new_with_label(no_text);
2208 result.button_no = button;
2209 g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(_yes_no_button_handler), &result);
2210 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
2211 }
2212
2213 if(yes_text)
2214 {
2215 button = gtk_button_new_with_label(yes_text);
2216 result.button_yes = button;
2217 g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(_yes_no_button_handler), &result);
2218 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
2219 }
2220
2221 gtk_widget_show_all(window);
2222 gtk_main();
2223
2224 if(result.result == RESULT_YES)
2225 return result.entry_text;
2226
2227 dt_free(result.entry_text);
2228 return NULL;
2229}
2230
2231// TODO: should that go to another place than gtk.c?
2232void dt_gui_add_help_link(GtkWidget *widget, char *link)
2233{
2234 g_object_set_data_full(G_OBJECT(widget), "dt-help-url", link, g_free);
2235 gtk_widget_add_events(widget, GDK_BUTTON_PRESS_MASK);
2236}
2237
2238// load a CSS theme
2239void dt_gui_load_theme(const char *theme)
2240{
2241 char theme_css[PATH_MAX] = { 0 };
2242 g_snprintf(theme_css, sizeof(theme_css), "%s.css", theme);
2243
2244 if(!dt_conf_key_exists("use_system_font"))
2245 dt_conf_set_bool("use_system_font", TRUE);
2246
2247 //set font size
2248 if(dt_conf_get_bool("use_system_font"))
2249 gtk_settings_reset_property(gtk_settings_get_default(), "gtk-font-name");
2250 else
2251 {
2252 //font name can only use period as decimal separator
2253 //but printf format strings use comma for some locales, so replace comma with period
2254 gchar *font_size = g_strdup_printf(_("%.1f"), dt_conf_get_float("font_size"));
2255 gchar *font_size_updated = dt_util_str_replace(font_size, ",", ".");
2256 gchar *font_name = g_strdup_printf(_("Sans %s"), font_size_updated);
2257 g_object_set(gtk_settings_get_default(), "gtk-font-name", font_name, NULL);
2258 dt_free(font_size_updated);
2259 dt_free(font_size);
2260 dt_free(font_name);
2261 }
2262
2263 gchar *path, *usercsspath;
2264 char datadir[PATH_MAX] = { 0 }, configdir[PATH_MAX] = { 0 };
2265 dt_loc_get_datadir(datadir, sizeof(datadir));
2266 dt_loc_get_user_config_dir(configdir, sizeof(configdir));
2267
2268 // user dir theme
2269 path = g_build_filename(configdir, "themes", theme_css, NULL);
2270 if(!g_file_test(path, G_FILE_TEST_EXISTS))
2271 {
2272 // dt dir theme
2273 dt_free(path);
2274 path = g_build_filename(datadir, "themes", theme_css, NULL);
2275 if(!g_file_test(path, G_FILE_TEST_EXISTS))
2276 {
2277 // fallback to default theme
2278 dt_free(path);
2279 path = g_build_filename(datadir, "themes", "ansel.css", NULL);
2280 dt_conf_set_string("ui_last/theme", "ansel");
2281 }
2282 else
2283 dt_conf_set_string("ui_last/theme", theme);
2284 }
2285 else
2286 dt_conf_set_string("ui_last/theme", theme);
2287
2288 GError *error = NULL;
2289
2290 GtkStyleProvider *themes_style_provider = GTK_STYLE_PROVIDER(gtk_css_provider_new());
2291 gtk_style_context_add_provider_for_screen
2292 (gdk_screen_get_default(), themes_style_provider, GTK_STYLE_PROVIDER_PRIORITY_USER + 1);
2293
2294 usercsspath = g_build_filename(configdir, "user.css", NULL);
2295
2296 gchar *path_uri = g_filename_to_uri(path, NULL, &error);
2297 if(IS_NULL_PTR(path_uri))
2298 fprintf(stderr, "%s: could not convert path %s to URI. Error: %s\n", G_STRFUNC, path, error->message);
2299
2300 gchar *usercsspath_uri = g_filename_to_uri(usercsspath, NULL, &error);
2301 if(IS_NULL_PTR(usercsspath_uri))
2302 fprintf(stderr, "%s: could not convert path %s to URI. Error: %s\n", G_STRFUNC, usercsspath, error->message);
2303
2304 gchar *themecss = NULL;
2305 if(dt_conf_get_bool("themes/usercss") && g_file_test(usercsspath, G_FILE_TEST_EXISTS))
2306 {
2307 themecss = g_strjoin(NULL, "@import url('", path_uri,
2308 "'); @import url('", usercsspath_uri, "');", NULL);
2309 }
2310 else
2311 {
2312 themecss = g_strjoin(NULL, "@import url('", path_uri, "');", NULL);
2313 }
2314
2315 dt_free(path_uri);
2316 dt_free(usercsspath_uri);
2317 dt_free(path);
2318 dt_free(usercsspath);
2319
2320 if(dt_conf_get_bool("ui/hide_tooltips"))
2321 {
2322 gchar *newcss = g_strjoin(NULL, themecss, " tooltip {opacity: 0; background: transparent;}", NULL);
2323 dt_free(themecss);
2324 themecss = newcss;
2325 }
2326
2327 if(!gtk_css_provider_load_from_data(GTK_CSS_PROVIDER(themes_style_provider), themecss, -1, &error))
2328 {
2329 fprintf(stderr, "%s: error parsing combined CSS %s: %s\n", G_STRFUNC, themecss, error->message);
2330 g_clear_error(&error);
2331 }
2332
2333 dt_free(themecss);
2334
2335 g_object_unref(themes_style_provider);
2336
2337 // setup the colors
2338
2339 GdkRGBA *c = darktable.gui->colors;
2340 GtkWidget *main_window = dt_ui_main_window(darktable.gui->ui);
2341 GtkStyleContext *ctx = gtk_widget_get_style_context(main_window);
2342
2343 c[DT_GUI_COLOR_BG] = (GdkRGBA){ 0.1333, 0.1333, 0.1333, 1.0 };
2344
2345 struct color_init
2346 {
2347 const char *name;
2348 GdkRGBA default_col;
2349 } init[DT_GUI_COLOR_LAST] = {
2350 [DT_GUI_COLOR_DARKROOM_BG] = { "darkroom_bg_color", { .2, .2, .2, 1.0 } },
2351 [DT_GUI_COLOR_DARKROOM_PREVIEW_BG] = { "darkroom_preview_bg_color", { .1, .1, .1, 1.0 } },
2352 [DT_GUI_COLOR_LIGHTTABLE_BG] = { "lighttable_bg_color", { .2, .2, .2, 1.0 } },
2353 [DT_GUI_COLOR_LIGHTTABLE_PREVIEW_BG] = { "lighttable_preview_bg_color", { .1, .1, .1, 1.0 } },
2354 [DT_GUI_COLOR_LIGHTTABLE_FONT] = { "lighttable_bg_font_color", { .7, .7, .7, 1.0 } },
2355 [DT_GUI_COLOR_PRINT_BG] = { "print_bg_color", { .2, .2, .2, 1.0 } },
2356 [DT_GUI_COLOR_BRUSH_CURSOR] = { "brush_cursor", { 1., 1., 1., 0.9 } },
2357 [DT_GUI_COLOR_BRUSH_TRACE] = { "brush_trace", { 0., 0., 0., 0.8 } },
2358 [DT_GUI_COLOR_BUTTON_FG] = { "button_fg", { 0.7, 0.7, 0.7, 0.55 } },
2359 [DT_GUI_COLOR_THUMBNAIL_BG] = { "thumbnail_bg_color", { 0.4, 0.4, 0.4, 1.0 } },
2360 [DT_GUI_COLOR_THUMBNAIL_SELECTED_BG] = { "thumbnail_selected_bg_color", { 0.8, 0.8, 0.8, 1.0 } },
2361 [DT_GUI_COLOR_THUMBNAIL_HOVER_BG] = { "thumbnail_hover_bg_color", { 0.65, 0.65, 0.65, 1.0 } },
2362 [DT_GUI_COLOR_THUMBNAIL_OUTLINE] = { "thumbnail_outline_color", { 0.2, 0.2, 0.2, 1.0 } },
2363 [DT_GUI_COLOR_THUMBNAIL_SELECTED_OUTLINE] = { "thumbnail_selected_outline_color", { 0.4, 0.4, 0.4, 1.0 } },
2364 [DT_GUI_COLOR_THUMBNAIL_HOVER_OUTLINE] = { "thumbnail_hover_outline_color", { 0.6, 0.6, 0.6, 1.0 } },
2365 [DT_GUI_COLOR_THUMBNAIL_FONT] = { "thumbnail_font_color", { 0.425, 0.425, 0.425, 1.0 } },
2366 [DT_GUI_COLOR_THUMBNAIL_SELECTED_FONT] = { "thumbnail_selected_font_color", { 0.5, 0.5, 0.5, 1.0 } },
2367 [DT_GUI_COLOR_THUMBNAIL_HOVER_FONT] = { "thumbnail_hover_font_color", { 0.7, 0.7, 0.7, 1.0 } },
2368 [DT_GUI_COLOR_THUMBNAIL_BORDER] = { "thumbnail_border_color", { 0.1, 0.1, 0.1, 1.0 } },
2369 [DT_GUI_COLOR_THUMBNAIL_SELECTED_BORDER] = { "thumbnail_selected_border_color", { 0.9, 0.9, 0.9, 1.0 } },
2370 [DT_GUI_COLOR_FILMSTRIP_BG] = { "filmstrip_bg_color", { 0.2, 0.2, 0.2, 1.0 } },
2371 [DT_GUI_COLOR_PREVIEW_HOVER_BORDER] = { "preview_hover_border_color", { 0.9, 0.9, 0.9, 1.0 } },
2372 [DT_GUI_COLOR_LOG_BG] = { "log_bg_color", { 0.1, 0.1, 0.1, 1.0 } },
2373 [DT_GUI_COLOR_LOG_FG] = { "log_fg_color", { 0.6, 0.6, 0.6, 1.0 } },
2374 [DT_GUI_COLOR_MAP_COUNT_SAME_LOC] = { "map_count_same_loc_color", { 1.0, 1.0, 1.0, 1.0 } },
2375 [DT_GUI_COLOR_MAP_COUNT_DIFF_LOC] = { "map_count_diff_loc_color", { 1.0, 0.85, 0.0, 1.0 } },
2376 [DT_GUI_COLOR_MAP_COUNT_BG] = { "map_count_bg_color", { 0.0, 0.0, 0.0, 1.0 } },
2377 [DT_GUI_COLOR_MAP_LOC_SHAPE_HIGH] = { "map_count_circle_color_h", { 1.0, 1.0, 0.8, 1.0 } },
2378 [DT_GUI_COLOR_MAP_LOC_SHAPE_LOW] = { "map_count_circle_color_l", { 0.0, 0.0, 0.0, 1.0 } },
2379 [DT_GUI_COLOR_MAP_LOC_SHAPE_DEF] = { "map_count_circle_color_d", { 1.0, 0.0, 0.0, 1.0 } },
2380 [DT_GUI_COLOR_WARNING] = { "warning_color", { 1.0, 0.647, 0.0, 1.0 } },
2381 };
2382
2383 // starting from 1 as DT_GUI_COLOR_BG is not part of this table
2384 for(int i = 1; i < DT_GUI_COLOR_LAST; i++)
2385 {
2386 if(!gtk_style_context_lookup_color(ctx, init[i].name, &c[i]))
2387 {
2388 c[i] = init[i].default_col;
2389 }
2390 }
2391
2392 // The active theme/font may change the root font size, so refresh the cached em
2393 // that drives DT_GUI_BOX_SPACING.
2395}
2396
2397GdkModifierType dt_key_modifier_state()
2398{
2399 guint state = 0;
2400 GdkWindow *window = gtk_widget_get_window(dt_ui_main_window(darktable.gui->ui));
2401 gdk_device_get_state(gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_window_get_display(window))), window, NULL, &state);
2402 return state;
2403
2404/* FIXME double check correct way of doing this (merge conflict with Input System NG 20210319)
2405 GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
2406 return gdk_keymap_get_modifier_state(keymap) & gdk_keymap_get_modifier_mask(keymap, GDK_MODIFIER_INTENT_DEFAULT_MOD_MASK);
2407*/
2408}
2409
2410static void _notebook_size_callback(GtkNotebook *notebook, GdkRectangle *allocation, gpointer *data)
2411{
2412 const int n = gtk_notebook_get_n_pages(notebook);
2413 g_return_if_fail(n > 0);
2414
2415 GtkRequestedSize *sizes = g_malloc_n(n, sizeof(GtkRequestedSize));
2416
2417 for(int i = 0; i < n; i++)
2418 {
2419 sizes[i].data = gtk_notebook_get_tab_label(notebook, gtk_notebook_get_nth_page(notebook, i));
2420 sizes[i].minimum_size = 0;
2421 GtkRequisition natural_size;
2422 gtk_widget_get_preferred_size(sizes[i].data, NULL, &natural_size);
2423 sizes[i].natural_size = natural_size.width;
2424 }
2425
2426 GtkAllocation first, last;
2427 gtk_widget_get_allocation(sizes[0].data, &first);
2428 gtk_widget_get_allocation(sizes[n - 1].data, &last);
2429
2430 const gint total_space = last.x + last.width - first.x; // ignore tab padding; CSS sets padding for label
2431
2432 if(total_space > 0)
2433 {
2434 gtk_distribute_natural_allocation(total_space, n, sizes);
2435
2436 for(int i = 0; i < n; i++)
2437 gtk_widget_set_size_request(sizes[i].data, sizes[i].minimum_size, -1);
2438
2439 gtk_widget_size_allocate(GTK_WIDGET(notebook), allocation);
2440
2441 for(int i = 0; i < n; i++)
2442 gtk_widget_set_size_request(sizes[i].data, -1, -1);
2443 }
2444
2445 dt_free(sizes);
2446}
2447
2448// GTK_STATE_FLAG_PRELIGHT does not seem to get set on the label on hover so
2449// state-flags-changed cannot update darktable.control->element for shortcut mapping
2450static gboolean _notebook_motion_notify_callback(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
2451{
2452 GtkAllocation notebook_alloc, label_alloc;
2453 gtk_widget_get_allocation(widget, &notebook_alloc);
2454
2455 GtkNotebook *notebook = GTK_NOTEBOOK(widget);
2456 const int n = gtk_notebook_get_n_pages(notebook);
2457 for(int i = 0; i < n; i++)
2458 {
2459 gtk_widget_get_allocation(gtk_notebook_get_tab_label(notebook, gtk_notebook_get_nth_page(notebook, i)), &label_alloc);
2460 }
2461
2462 return FALSE;
2463}
2464
2466{
2467 return GTK_NOTEBOOK(gtk_notebook_new());
2468}
2469
2470GtkWidget *dt_ui_notebook_page(GtkNotebook *notebook, const char *text, const char *tooltip)
2471{
2472 gchar *text_cpy = g_strdup(_(text));
2473 dt_capitalize_label(text_cpy);
2474 GtkWidget *label = gtk_label_new(text_cpy);
2475 dt_free(text_cpy);
2476 GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
2477 if(strlen(text) > 2)
2478 gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_END);
2479 gtk_widget_set_tooltip_text(label, tooltip ? tooltip : _(text));
2480 gtk_widget_set_has_tooltip(GTK_WIDGET(notebook), FALSE);
2481
2482 gint page_num = gtk_notebook_append_page(notebook, page, label);
2483 gtk_container_child_set(GTK_CONTAINER(notebook), page, "tab-expand", TRUE, "tab-fill", TRUE, NULL);
2484 if(page_num == 1 &&
2485 !g_signal_handler_find(G_OBJECT(notebook), G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _notebook_size_callback, NULL))
2486 {
2487 g_signal_connect(G_OBJECT(notebook), "size-allocate", G_CALLBACK(_notebook_size_callback), NULL);
2488 g_signal_connect(G_OBJECT(notebook), "motion-notify-event", G_CALLBACK(_notebook_motion_notify_callback), NULL);
2489 }
2490
2491 return page;
2492}
2493
2494static void _notebook_switch_page_signal_relay(GtkNotebook *notebook, GtkWidget *page, guint page_num,
2495 gpointer user_data)
2496{
2498}
2499
2500void dt_ui_notebook_set_picker_owner(GtkNotebook *notebook, gpointer owner)
2501{
2502 if(IS_NULL_PTR(notebook)) return;
2503
2504 // Guard against connecting twice on the same notebook (e.g. a caller re-entering
2505 // gui_init after a GUI rebuild); user_data is set per-connection, so re-registering
2506 // a different owner on an already-relayed notebook would otherwise leak the old
2507 // connection instead of updating it.
2508 const gulong existing = g_signal_handler_find(G_OBJECT(notebook), G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
2509 G_CALLBACK(_notebook_switch_page_signal_relay), NULL);
2510 if(existing) g_signal_handler_disconnect(G_OBJECT(notebook), existing);
2511
2512 g_signal_connect(G_OBJECT(notebook), "switch_page", G_CALLBACK(_notebook_switch_page_signal_relay), owner);
2513}
2514
2516{
2517 gint height = DT_PIXEL_APPLY_DPI(10);
2518
2519 if(GTK_IS_TREE_VIEW(w))
2520 {
2521 gint row_height = 0;
2522
2523 const gint num_columns = gtk_tree_view_get_n_columns(GTK_TREE_VIEW(w));
2524 for(int c = 0; c < num_columns; c++)
2525 {
2526 gint cell_height = 0;
2527 gtk_tree_view_column_cell_get_size(gtk_tree_view_get_column(GTK_TREE_VIEW(w), c),
2528 NULL, NULL, NULL, NULL, &cell_height);
2529 if(cell_height > row_height) row_height = cell_height;
2530 }
2531 GValue separation = { G_TYPE_INT };
2532 gtk_widget_style_get_property(w, "vertical-separator", &separation);
2533
2534 if(row_height > 0) height = row_height + g_value_get_int(&separation);
2535 }
2536 else if(GTK_IS_TEXT_VIEW(w))
2537 {
2538 PangoLayout *layout = gtk_widget_create_pango_layout(w, "X");
2539 pango_layout_get_pixel_size(layout, NULL, &height);
2540 g_object_unref(layout);
2541 }
2542 else
2543 {
2544 GtkWidget *child = dt_gui_container_first_child(GTK_CONTAINER(w));
2545 if(child)
2546 {
2547 height = gtk_widget_get_allocated_height(child);
2548 }
2549 }
2550
2551 return height;
2552}
2553
2554static const char *const DT_GUI_WIDGET_AUTO_HEIGHT_KEY = "dt-gui-widget-auto-height";
2555
2556// find the scrolled window parent of a treeview, if any
2558{
2559 if(!GTK_IS_WIDGET(w)) return NULL;
2560
2561 GtkWidget *parent = w;
2562 while(parent)
2563 {
2564 if(GTK_IS_SCROLLED_WINDOW(parent)) break;
2565 parent = gtk_widget_get_parent(parent);
2566 }
2567
2568 return GTK_IS_SCROLLED_WINDOW(parent) ? parent : NULL;
2569}
2570
2571// Counts only visible items (those whose parents are expanded)
2572static int _treeview_count_visible_rows(GtkTreeView *treeview, GtkTreeModel *model, GtkTreeIter *parent)
2573{
2574 if(!GTK_IS_TREE_MODEL(model)) return 0;
2575
2576 GtkTreeIter iter;
2577 gboolean valid = parent ? gtk_tree_model_iter_children(model, &iter, parent)
2578 : gtk_tree_model_get_iter_first(model, &iter);
2579 int count = 0;
2580
2581 while(valid)
2582 {
2583 count++;
2584
2585 // If this item is expanded, recursively count its visible children
2586 if(gtk_tree_model_iter_has_child(model, &iter))
2587 {
2588 GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
2589 if(path)
2590 {
2591 if(gtk_tree_view_row_expanded(treeview, path))
2592 {
2593 count += _treeview_count_visible_rows(treeview, model, &iter);
2594 }
2595 gtk_tree_path_free(path);
2596 }
2597 }
2598
2599 valid = gtk_tree_model_iter_next(model, &iter);
2600 }
2601
2602 return count;
2603}
2604
2606{
2607 if(!GTK_IS_TEXT_VIEW(textview)) return 0;
2608
2609 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
2610 if(!GTK_IS_TEXT_BUFFER(buffer)) return 0;
2611
2612 // For text views, use the number of logical lines in the buffer.
2613 return MAX(0, gtk_text_buffer_get_line_count(buffer));
2614}
2615
2617{
2618 if(IS_NULL_PTR(state)) return;
2619
2620 GtkTreeModel *model = state->model;
2621 if(model)
2622 {
2623 if(state->model_row_inserted) g_signal_handler_disconnect(model, state->model_row_inserted);
2624 if(state->model_row_deleted) g_signal_handler_disconnect(model, state->model_row_deleted);
2625 if(state->model_row_changed) g_signal_handler_disconnect(model, state->model_row_changed);
2626 if(state->model_rows_reordered) g_signal_handler_disconnect(model, state->model_rows_reordered);
2627 g_object_remove_weak_pointer(G_OBJECT(model), (gpointer *)&state->model);
2628 }
2629
2630 if(GTK_IS_TREE_VIEW(treeview))
2631 {
2632 if(state->model_row_expanded) g_signal_handler_disconnect(treeview, state->model_row_expanded);
2633 if(state->model_row_collapsed) g_signal_handler_disconnect(treeview, state->model_row_collapsed);
2634 }
2635
2636 state->model = NULL;
2637 state->model_row_inserted = 0;
2638 state->model_row_deleted = 0;
2639 state->model_row_changed = 0;
2640 state->model_rows_reordered = 0;
2641 state->model_row_expanded = 0;
2642 state->model_row_collapsed = 0;
2643}
2644
2646{
2647 if(IS_NULL_PTR(state)) return;
2648
2649 GtkTextBuffer *buffer = state->buffer;
2650 if(buffer)
2651 {
2652 if(state->buffer_changed) g_signal_handler_disconnect(buffer, state->buffer_changed);
2653 g_object_remove_weak_pointer(G_OBJECT(buffer), (gpointer *)&state->buffer);
2654 }
2655
2656 state->buffer = NULL;
2657 state->buffer_changed = 0;
2658}
2659
2668{
2670 return win ? gtk_widget_get_allocated_height(win) : DT_PIXEL_APPLY_DPI(1000);
2671}
2672
2683{
2684 dt_gui_widget_auto_height_t *state = g_object_get_data(G_OBJECT(w), DT_GUI_WIDGET_AUTO_HEIGHT_KEY);
2685 if(IS_NULL_PTR(state)) return;
2686
2688 if(!GTK_IS_SCROLLED_WINDOW(sw)) return;
2689
2690 const gint max_height = _resizable_scroll_max_height();
2691 const gint min_size = MAX(1, state->min_size);
2692 const gboolean has_conf = state->config_str && dt_conf_key_exists(state->config_str);
2693
2694 gint height;
2695 gint increment = 0;
2696
2697 if(state->mode == DT_UI_RESIZE_STATIC)
2698 {
2699 // Fixed height: the user's persisted size, or min_size as the default. Independent of content,
2700 // so a content refresh (e.g. on hovering another thumbnail) never shifts the layout.
2701 height = CLAMP(has_conf ? dt_conf_get_int(state->config_str) : min_size, min_size, max_height);
2702 }
2703 else
2704 {
2705 // Dynamic: fit to content, capped by the user's persisted height (or the window ceiling).
2706 const gboolean row_based = GTK_IS_TREE_VIEW(w) || GTK_IS_TEXT_VIEW(w);
2707 increment = row_based ? _get_container_row_heigth(w) : 0;
2708
2709 gint content = 0;
2710 if(GTK_IS_TREE_VIEW(w))
2711 {
2712 const int rows = _treeview_count_visible_rows(GTK_TREE_VIEW(w), gtk_tree_view_get_model(GTK_TREE_VIEW(w)), NULL);
2713 content = MAX(1, rows) * increment;
2714 }
2715 else if(GTK_IS_TEXT_VIEW(w))
2716 {
2717 const int rows = _textview_count_visible_rows(w);
2718 content = MAX(1, rows) * increment;
2719 }
2720 else
2721 {
2722 gtk_widget_get_preferred_height(w, NULL, &content);
2723 }
2724
2725 const gint cap = has_conf ? CLAMP(dt_conf_get_int(state->config_str), min_size, max_height) : max_height;
2726 height = CLAMP(MIN(content, cap), min_size, max_height);
2727
2728 // snap to whole rows for lists/textviews to avoid clipped half-rows
2729 if(increment > 0)
2730 {
2731 height += increment - 1;
2732 height -= height % increment;
2733 }
2734 }
2735 state->last_height = height;
2736
2737 GtkBorder padding;
2738 gtk_style_context_get_padding(gtk_widget_get_style_context(sw),
2739 gtk_widget_get_state_flags(sw), &padding);
2740
2741 gint old_height = 0;
2742 gtk_widget_get_size_request(sw, NULL, &old_height);
2743 const gint new_height = height + padding.top + padding.bottom + (GTK_IS_TEXT_VIEW(w) ? 2 : 0);
2744 if(new_height != old_height)
2745 gtk_widget_set_size_request(sw, -1, new_height);
2746}
2747
2748static void _widget_auto_update(GtkWidget *widget)
2749{
2751}
2752
2753static gboolean _resizable_scroll_draw(GtkWidget *w, cairo_t *cr, gpointer user_data)
2754{
2756 return FALSE;
2757}
2758
2759static void _resizable_scroll_realize(GtkWidget *w, gpointer user_data)
2760{
2762}
2763
2764// Drag handle accessors: bare (pre-padding) target height, kept consistent with the sizing rule.
2765static int _resizable_scroll_handle_get_size(gpointer user_data)
2766{
2767 GtkWidget *w = GTK_WIDGET(user_data);
2768 const dt_gui_widget_auto_height_t *state = g_object_get_data(G_OBJECT(w), DT_GUI_WIDGET_AUTO_HEIGHT_KEY);
2769 return state ? state->last_height : 0;
2770}
2771
2772static int _resizable_scroll_handle_resize(int requested_size, gboolean finished, gpointer user_data)
2773{
2774 GtkWidget *w = GTK_WIDGET(user_data);
2775 dt_gui_widget_auto_height_t *state = g_object_get_data(G_OBJECT(w), DT_GUI_WIDGET_AUTO_HEIGHT_KEY);
2776 if(IS_NULL_PTR(state) || IS_NULL_PTR(state->config_str)) return requested_size;
2777
2778 const gint value = CLAMP(requested_size, MAX(1, state->min_size), _resizable_scroll_max_height());
2779 dt_conf_set_int(state->config_str, value);
2781 return state->last_height;
2782}
2783
2784static void _widget_auto_model_row_inserted(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
2785 gpointer user_data)
2786{
2787 (void)model;
2788 (void)path;
2789 (void)iter;
2790 _widget_auto_update(GTK_WIDGET(user_data));
2791}
2792
2793static void _widget_auto_model_row_deleted(GtkTreeModel *model, GtkTreePath *path, gpointer user_data)
2794{
2795 (void)model;
2796 (void)path;
2797 _widget_auto_update(GTK_WIDGET(user_data));
2798}
2799
2800static void _widget_auto_model_row_changed(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
2801 gpointer user_data)
2802{
2803 (void)model;
2804 (void)path;
2805 (void)iter;
2806 _widget_auto_update(GTK_WIDGET(user_data));
2807}
2808
2809static void _widget_auto_model_rows_reordered(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
2810 gint *new_order, gpointer user_data)
2811{
2812 (void)model;
2813 (void)path;
2814 (void)iter;
2815 (void)new_order;
2816 _widget_auto_update(GTK_WIDGET(user_data));
2817}
2818
2819static void _widget_auto_model_row_expanded(GtkTreeView *tree_view, GtkTreeIter *expanded_iter, GtkTreePath *path,
2820 gpointer user_data)
2821{
2822 (void)tree_view;
2823 (void)expanded_iter;
2824 (void)path;
2825 _widget_auto_update(GTK_WIDGET(user_data));
2826}
2827
2828static void _widget_auto_model_row_collapsed(GtkTreeView *tree_view, GtkTreeIter *collapsed_iter, GtkTreePath *path,
2829 gpointer user_data)
2830{
2831 (void)tree_view;
2832 (void)collapsed_iter;
2833 (void)path;
2834
2835 // Recalculate tree view height after loading the data
2836 _widget_auto_update(GTK_WIDGET(user_data));
2837}
2838
2839static void _widget_auto_text_buffer_changed(GtkTextBuffer *buffer, gpointer user_data)
2840{
2841 (void)buffer;
2842 _widget_auto_update(GTK_WIDGET(user_data));
2843}
2844
2846{
2847 if(!GTK_IS_TREE_VIEW(treeview)) return;
2848
2849 dt_gui_widget_auto_height_t *state = g_object_get_data(G_OBJECT(treeview), DT_GUI_WIDGET_AUTO_HEIGHT_KEY);
2850 if(IS_NULL_PTR(state)) return;
2851
2852 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
2853 if(model == state->model) return;
2854
2856 if(!GTK_IS_TREE_MODEL(model)) return;
2857
2858 state->model = model;
2859 g_object_add_weak_pointer(G_OBJECT(model), (gpointer *)&state->model);
2860 state->model_row_inserted = g_signal_connect(model, "row-inserted",
2861 G_CALLBACK(_widget_auto_model_row_inserted), treeview);
2862 state->model_row_deleted = g_signal_connect(model, "row-deleted",
2863 G_CALLBACK(_widget_auto_model_row_deleted), treeview);
2864 state->model_row_changed = g_signal_connect(model, "row-changed",
2865 G_CALLBACK(_widget_auto_model_row_changed), treeview);
2866 state->model_rows_reordered = g_signal_connect(model, "rows-reordered",
2867 G_CALLBACK(_widget_auto_model_rows_reordered), treeview);
2868 state->model_row_expanded = g_signal_connect(treeview, "row-expanded",
2869 G_CALLBACK(_widget_auto_model_row_expanded), treeview);
2870 state->model_row_collapsed = g_signal_connect(treeview, "row-collapsed",
2871 G_CALLBACK(_widget_auto_model_row_collapsed), treeview);
2872}
2873
2875{
2876 if(!GTK_IS_TEXT_VIEW(textview)) return;
2877
2878 dt_gui_widget_auto_height_t *state = g_object_get_data(G_OBJECT(textview), DT_GUI_WIDGET_AUTO_HEIGHT_KEY);
2879 if(IS_NULL_PTR(state)) return;
2880
2881 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
2882 if(buffer == state->buffer) return;
2883
2885 if(!GTK_IS_TEXT_BUFFER(buffer)) return;
2886
2887 state->buffer = buffer;
2888 g_object_add_weak_pointer(G_OBJECT(buffer), (gpointer *)&state->buffer);
2889 state->buffer_changed = g_signal_connect(buffer, "changed",
2890 G_CALLBACK(_widget_auto_text_buffer_changed), textview);
2891}
2892
2893static void _widget_auto_on_model_changed(GObject *treeview, GParamSpec *pspec, gpointer user_data)
2894{
2895 (void)pspec;
2896 (void)user_data;
2897 _widget_auto_connect_model(GTK_WIDGET(treeview));
2898 _widget_auto_update(GTK_WIDGET(treeview));
2899}
2900
2901static void _widget_auto_on_buffer_changed(GObject *textview, GParamSpec *pspec, gpointer user_data)
2902{
2903 (void)pspec;
2904 (void)user_data;
2905 _widget_auto_connect_buffer(GTK_WIDGET(textview));
2906 _widget_auto_update(GTK_WIDGET(textview));
2907}
2908
2909static void _widget_auto_height_free(gpointer data)
2910{
2912 if(IS_NULL_PTR(state)) return;
2915 g_free(state->config_str);
2916 dt_free(state);
2917}
2918
2919void dt_gui_textview_set_padding(GtkTextView *textview)
2920{
2921 if(!GTK_IS_TEXT_VIEW(textview)) return;
2922
2923 gtk_text_view_set_left_margin(textview, DT_PIXEL_APPLY_DPI(4));
2924 gtk_text_view_set_right_margin(textview, DT_PIXEL_APPLY_DPI(4));
2925 gtk_text_view_set_top_margin(textview, DT_PIXEL_APPLY_DPI(2));
2926 gtk_text_view_set_bottom_margin(textview, DT_PIXEL_APPLY_DPI(2));
2927}
2928
2945GtkWidget *dt_ui_scroll_wrap(GtkWidget *w, gint min_size, char *config_str, dt_ui_resize_mode_t mode)
2946{
2947 GtkWidget *sw = gtk_scrolled_window_new(NULL, NULL);
2948 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
2949 if(GTK_IS_TREE_VIEW(w)) dt_gui_add_class(sw, "dt_recessed_scroll");
2950 gtk_container_add(GTK_CONTAINER(sw), w);
2951
2952 // Per-widget sizing state, freed with w.
2953 dt_gui_widget_auto_height_t *state = calloc(1, sizeof(*state));
2954 state->config_str = g_strdup(config_str);
2955 state->min_size = MAX(1, DT_PIXEL_APPLY_DPI(min_size));
2956 state->mode = mode;
2957 g_object_set_data_full(G_OBJECT(w), DT_GUI_WIDGET_AUTO_HEIGHT_KEY, state, _widget_auto_height_free);
2958
2959 if(mode == DT_UI_RESIZE_DYNAMIC)
2960 {
2961 // Sizing triggers. Lists/textviews recompute from their model/buffer change signals (cheap and
2962 // exact) plus a one-shot on realize, when row heights become accurate -- we deliberately avoid a
2963 // per-draw recount, which would re-walk a large model on every redraw. Generic content has no
2964 // such signals, so it recomputes on draw using its (GTK-cached) preferred height.
2965 if(GTK_IS_TREE_VIEW(w))
2966 {
2967 g_signal_connect(w, "notify::model", G_CALLBACK(_widget_auto_on_model_changed), NULL);
2968 g_signal_connect(w, "realize", G_CALLBACK(_resizable_scroll_realize), NULL);
2969 }
2970 else if(GTK_IS_TEXT_VIEW(w))
2971 {
2972 g_signal_connect(w, "notify::buffer", G_CALLBACK(_widget_auto_on_buffer_changed), NULL);
2973 g_signal_connect(w, "realize", G_CALLBACK(_resizable_scroll_realize), NULL);
2974 }
2975 else
2976 {
2977 g_signal_connect(G_OBJECT(w), "draw", G_CALLBACK(_resizable_scroll_draw), NULL);
2978 }
2981 }
2982 else
2983 {
2984 // Static: height is content-independent, so we only size once after realization (and on drag).
2985 g_signal_connect(w, "realize", G_CALLBACK(_resizable_scroll_realize), NULL);
2986 }
2987
2988 // Drag grip floating on the scrolled window's bottom edge (overlay): it takes no layout space,
2989 // so the wrapper leaves no margin-like gap and stays aligned with neighbouring widgets. The grip
2990 // is centered on the bottom border via CSS and is invisible until hovered.
2991 GtkWidget *handle = dt_bauhaus_resize_handle_new(GTK_ORIENTATION_VERTICAL, FALSE,
2992 _("Drag to resize"),
2995
2996 GtkWidget *over = gtk_overlay_new();
2997 gtk_container_add(GTK_CONTAINER(over), sw);
2998 gtk_overlay_add_overlay(GTK_OVERLAY(over), handle);
2999
3001 return over;
3002}
3003
3008{
3009 if(!GTK_IS_CONTAINER(wrapper)) return NULL;
3010 GList *children = gtk_container_get_children(GTK_CONTAINER(wrapper));
3011 GtkWidget *sw = NULL;
3012 for(GList *l = children; l; l = g_list_next(l))
3013 {
3014 if(GTK_IS_SCROLLED_WINDOW(l->data))
3015 {
3016 sw = GTK_WIDGET(l->data);
3017 break;
3018 }
3019 }
3020 g_list_free(children);
3021 return sw;
3022}
3023
3024// ---- Resizable drawing area (the histogram-scope paradigm, made reusable) -------------------
3025// A fixed-pixel-height GtkDrawingArea (or any widget sized by height-request) made vertically
3026// resizable by the shared grip primitive, with the height persisted to config. Unlike the
3027// scroll-wrap helper above, the content draws itself and is not scrolled; we only manage its
3028// height-request, so the drawing code keeps reading the live allocation as usual.
3029
3030static const char *const DT_UI_RESIZABLE_AREA_KEY = "dt-ui-resizable-area";
3031
3033{
3034 char *config_str; // conf key persisting the user-chosen height (px); owned
3035 int min_height; // minimum height floor, device pixels
3036 int last_height; // last applied height, shared with the drag handle
3038
3039static void _resizable_area_free(gpointer data)
3040{
3042 if(IS_NULL_PTR(state)) return;
3043 g_free(state->config_str);
3044 dt_free(state);
3045}
3046
3047static int _resizable_area_get_size(gpointer user_data)
3048{
3049 GtkWidget *area = GTK_WIDGET(user_data);
3050 const dt_ui_resizable_area_t *state = g_object_get_data(G_OBJECT(area), DT_UI_RESIZABLE_AREA_KEY);
3051 if(state) return state->last_height;
3052 return gtk_widget_get_allocated_height(area);
3053}
3054
3055static int _resizable_area_resize(int requested_size, gboolean finished, gpointer user_data)
3056{
3057 GtkWidget *area = GTK_WIDGET(user_data);
3058 dt_ui_resizable_area_t *state = g_object_get_data(G_OBJECT(area), DT_UI_RESIZABLE_AREA_KEY);
3059 if(IS_NULL_PTR(state)) return requested_size;
3060
3061 const int height = CLAMP(requested_size, MAX(1, state->min_height), _resizable_scroll_max_height());
3062 state->last_height = height;
3063 gtk_widget_set_size_request(area, -1, height);
3064 if(finished && state->config_str) dt_conf_set_int(state->config_str, height);
3065 return height;
3066}
3067
3068GtkWidget *dt_ui_resizable_drawing_area(GtkWidget *area, char *config_str, int default_height, int min_height)
3069{
3070 dt_ui_resizable_area_t *state = calloc(1, sizeof(*state));
3071 state->config_str = g_strdup(config_str);
3072 state->min_height = MAX(1, DT_PIXEL_APPLY_DPI(min_height));
3073
3074 int height = (config_str && dt_conf_key_exists(config_str)) ? dt_conf_get_int(config_str)
3075 : DT_PIXEL_APPLY_DPI(default_height);
3076 height = CLAMP(height, state->min_height, _resizable_scroll_max_height());
3077 state->last_height = height;
3078 g_object_set_data_full(G_OBJECT(area), DT_UI_RESIZABLE_AREA_KEY, state, _resizable_area_free);
3079 gtk_widget_set_size_request(area, -1, height);
3080
3081 // Drag grip floating on the area's bottom edge (an overlay, not a packed sibling), so it takes
3082 // no layout space -- the area stays flush with neighbouring widgets, no margin-like gap. It sits
3083 // over the graph's bottom inset/axis margin (graphs reserve one), invisible until hovered.
3084 GtkWidget *handle = dt_bauhaus_resize_handle_new(GTK_ORIENTATION_VERTICAL, FALSE,
3085 _("Drag to resize"),
3087
3088 GtkWidget *over = gtk_overlay_new();
3089 gtk_container_add(GTK_CONTAINER(over), area);
3090 gtk_overlay_add_overlay(GTK_OVERLAY(over), handle);
3091 return over;
3092}
3093
3095{
3096 g_return_val_if_fail(GTK_IS_CONTAINER(container), FALSE);
3097 GList *children = gtk_container_get_children(container);
3098 gboolean has_children = !IS_NULL_PTR(children);
3099 g_list_free(children);
3100 children = NULL;
3101 return has_children;
3102}
3103
3105{
3106 g_return_val_if_fail(GTK_IS_CONTAINER(container), FALSE);
3107 GList *children = gtk_container_get_children(container);
3108 int num_children = g_list_length(children);
3109 g_list_free(children);
3110 children = NULL;
3111 return num_children;
3112}
3113
3115{
3116 g_return_val_if_fail(GTK_IS_CONTAINER(container), NULL);
3117 GList *children = gtk_container_get_children(container);
3118 GtkWidget *child = children ? (GtkWidget*)children->data : NULL;
3119 g_list_free(children);
3120 children = NULL;
3121 return child;
3122}
3123
3125{
3126 g_return_val_if_fail(GTK_IS_CONTAINER(container), NULL);
3127 GList *children = gtk_container_get_children(container);
3128 GtkWidget *child = (GtkWidget*)g_list_nth_data(children, which);
3129 g_list_free(children);
3130 children = NULL;
3131 return child;
3132}
3133
3134static void _remove_child(GtkWidget *widget, gpointer data)
3135{
3136 gtk_container_remove((GtkContainer*)data, widget);
3137}
3138
3140{
3141 g_return_if_fail(GTK_IS_CONTAINER(container));
3142 gtk_container_foreach(container, _remove_child, container);
3143}
3144
3145static void _delete_child(GtkWidget *widget, gpointer data)
3146{
3147 (void)data; // avoid unreferenced-parameter warning
3148 gtk_widget_destroy(widget);
3149}
3150
3152{
3153 g_return_if_fail(GTK_IS_CONTAINER(container));
3154 gtk_container_foreach(container, _delete_child, NULL);
3155}
3156
3158{
3159 if(IS_NULL_PTR(widget)) return NULL;
3160
3161 GtkWidget *relative = widget;
3162
3163 // Wayland only accepts the top-most enclosing popup as transient parent.
3164 for(GtkWidget *parent = gtk_widget_get_parent(widget); parent; parent = gtk_widget_get_parent(parent))
3165 if(GTK_IS_POPOVER(parent)) relative = parent;
3166
3167 if(rect)
3168 {
3169 rect->x = 0;
3170 rect->y = 0;
3171 rect->width = MAX(gtk_widget_get_allocated_width(widget), 1);
3172 rect->height = MAX(gtk_widget_get_allocated_height(widget), 1);
3173
3174 if(relative != widget
3175 && !gtk_widget_translate_coordinates(widget, relative, 0, 0, &rect->x, &rect->y))
3176 {
3177 rect->x = 0;
3178 rect->y = 0;
3179 }
3180 }
3181
3182 return relative;
3183}
3184
3185void dt_gui_menu_popup(GtkMenu *menu, GtkWidget *button, GdkGravity widget_anchor, GdkGravity menu_anchor)
3186{
3187 gtk_widget_show_all(GTK_WIDGET(menu));
3188
3189 GdkEvent *event = gtk_get_current_event();
3190 if(button)
3191 {
3192 GdkRectangle rect = { 0 };
3193 GtkWidget *relative = dt_gui_get_popup_relative_widget(button, &rect);
3194
3195 if(relative && relative != button && gtk_widget_get_window(relative))
3196 gtk_menu_popup_at_rect(menu, gtk_widget_get_window(relative), &rect, widget_anchor, menu_anchor, event);
3197 else
3198 gtk_menu_popup_at_widget(menu, button, widget_anchor, menu_anchor, event);
3199 }
3200 else
3201 {
3202 if(IS_NULL_PTR(event))
3203 {
3204 event = gdk_event_new(GDK_BUTTON_PRESS);
3205 event->button.device = gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_display_get_default()));
3206 event->button.window = gtk_widget_get_window(GTK_WIDGET(darktable.gui->ui->main_window));
3207 g_object_ref(event->button.window);
3208 }
3209
3210 gtk_menu_popup_at_pointer(menu, event);
3211 }
3212 gdk_event_free(event);
3213}
3214
3215static void _popover_set_relative_to_topmost_parent(GtkPopover *popover, GtkWidget *button)
3216{
3217 GdkRectangle rect = { 0 };
3218 GtkWidget *relative = dt_gui_get_popup_relative_widget(button, &rect);
3219 gtk_popover_set_relative_to(popover, relative ? relative : button);
3220 gtk_popover_set_pointing_to(popover, &rect);
3221}
3222
3223// draw rounded rectangle
3224void dt_gui_draw_rounded_rectangle(cairo_t *cr, float width, float height, float x, float y)
3225{
3226 const float radius = height / 5.0f;
3227 const float degrees = M_PI / 180.0;
3228 cairo_new_sub_path(cr);
3229 cairo_arc(cr, x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees);
3230 cairo_arc(cr, x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees);
3231 cairo_arc(cr, x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees);
3232 cairo_arc(cr, x + radius, y + radius, radius, 180 * degrees, 270 * degrees);
3233 cairo_close_path(cr);
3234 cairo_fill(cr);
3235}
3236
3237gboolean dt_gui_search_start(GtkWidget *widget, GdkEventKey *event, GtkSearchEntry *entry)
3238{
3239 if(gtk_search_entry_handle_event(entry, (GdkEvent *)event))
3240 {
3241 gtk_entry_grab_focus_without_selecting(GTK_ENTRY(entry));
3242 return TRUE;
3243 }
3244
3245 return FALSE;
3246}
3247
3248void dt_gui_search_stop(GtkSearchEntry *entry, GtkWidget *widget)
3249{
3250 gtk_widget_grab_focus(widget);
3251
3252 gtk_entry_set_text(GTK_ENTRY(entry), "");
3253
3254 if(GTK_IS_TREE_VIEW(widget))
3255 {
3256 GtkTreePath *path = NULL;
3257 gtk_tree_view_get_cursor(GTK_TREE_VIEW(widget), &path, NULL);
3258 gtk_tree_selection_select_path(gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)), path);
3259 gtk_tree_path_free(path);
3260 }
3261}
3262
3264{
3265 if(active)
3266 {
3267 // We don't apply the GTK_STATE_SELECTED flag to the container here because it would
3268 // be inherited by all children, which would mess up the state of checkboxes and togglebuttons.
3269 dt_gui_add_class(GTK_WIDGET(cs->expander), "active");
3270 }
3271 else
3272 {
3273 gtk_widget_set_state_flags(GTK_WIDGET(cs->expander), GTK_STATE_FLAG_NORMAL, TRUE);
3274 dt_gui_remove_class(GTK_WIDGET(cs->expander), "active");
3275 }
3276}
3277
3278static void _collapsible_container_show(GtkWidget *widget, gpointer user_data)
3279{
3280 /* Called whenever the container receives a "show" event, including from gtk_widget_show_all().
3281 * If the toggle is not active the section should remain collapsed, so we re-hide the container
3282 * immediately. By the time this fires, show_all has already recursed into the children and
3283 * set their visible flags, so a later expand will find all children ready to display. */
3285 if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cs->toggle)))
3286 gtk_widget_hide(widget);
3287}
3288
3289static void _coeffs_button_changed(GtkDarktableToggleButton *widget, gpointer user_data)
3290{
3292
3293 const gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cs->toggle));
3296 (active ? CPF_DIRECTION_DOWN : CPF_DIRECTION_LEFT), NULL);
3297 dt_conf_set_bool(cs->confname, active);
3298 _collapsible_set_states(cs, active);
3299}
3300
3301static void _coeffs_expander_click(GtkWidget *widget, GdkEventButton *e, gpointer user_data)
3302{
3303 if(e->type == GDK_2BUTTON_PRESS || e->type == GDK_3BUTTON_PRESS) return;
3304
3306
3307 const gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cs->toggle));
3308 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs->toggle), !active);
3309 _collapsible_set_states(cs, !active);
3310}
3311
3313{
3314 const gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cs->toggle));
3316 (active ? CPF_DIRECTION_DOWN : CPF_DIRECTION_LEFT), NULL);
3318
3319 if(active)
3320 gtk_widget_show(GTK_WIDGET(cs->container));
3321 else
3322 gtk_widget_hide(GTK_WIDGET(cs->container));
3323
3324 _collapsible_set_states(cs, active);
3325}
3326
3328{
3329 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs->toggle), FALSE);
3330 gtk_widget_hide(GTK_WIDGET(cs->container));
3332}
3333
3335 const char *confname, const char *label,
3336 GtkBox *parent, GtkPackType pack)
3337{
3338 const gboolean expanded = dt_conf_get_bool(confname);
3339
3340 cs->confname = confname;
3341 cs->parent = parent;
3342
3343 // collapsible section header
3344 GtkWidget *destdisp_head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
3345 GtkWidget *header_evb = gtk_event_box_new();
3346 cs->label = dt_ui_section_label_new(label);
3347 dt_gui_add_class(destdisp_head, "dt_section_expander");
3348 gtk_container_add(GTK_CONTAINER(header_evb), cs->label);
3349
3351 (expanded ? CPF_DIRECTION_DOWN : CPF_DIRECTION_LEFT), NULL);
3352 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs->toggle), expanded);
3353 dt_gui_add_class(cs->toggle, "dt_ignore_fg_state");
3354
3355 cs->container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING));
3356 gtk_widget_set_name(GTK_WIDGET(cs->container), "collapsible");
3357 gtk_box_pack_start(GTK_BOX(destdisp_head), header_evb, TRUE, TRUE, 0);
3358 gtk_box_pack_start(GTK_BOX(destdisp_head), cs->toggle, FALSE, FALSE, 0);
3359
3360 cs->expander = dtgtk_expander_new(destdisp_head, GTK_WIDGET(cs->container));
3361 /* gtk_widget_show_all() on the parent (called by lib modules after gui_init) recurses into
3362 * the container and would override the collapsed state set by dtgtk_expander_set_expanded.
3363 * Connect to "show" so we can re-apply the correct visibility right after show_all touches it. */
3364 g_signal_connect(G_OBJECT(cs->container), "show",
3365 G_CALLBACK(_collapsible_container_show), (gpointer)cs);
3366 // Pack at the requested side so callers control ordering at insertion time.
3367 if(pack == GTK_PACK_START)
3368 gtk_box_pack_start(GTK_BOX(cs->parent), cs->expander, FALSE, FALSE, 0);
3369 else
3370 gtk_box_pack_end(GTK_BOX(cs->parent), cs->expander, FALSE, FALSE, 0);
3372 gtk_widget_set_name(cs->expander, "collapse-block");
3373
3374 g_signal_connect(G_OBJECT(cs->toggle), "toggled",
3375 G_CALLBACK(_coeffs_button_changed), (gpointer)cs);
3376
3377 g_signal_connect(G_OBJECT(header_evb), "button-release-event",
3378 G_CALLBACK(_coeffs_expander_click),
3379 (gpointer)cs);
3380}
3381
3382void dt_capitalize_label(gchar *text)
3383{
3384 if(!text || !text[0]) return;
3385
3386 // Deal with strings beginning with a Mnemonics underscore
3387 gchar *p = text;
3388 if(*p == '_' && p[1]) p++;
3389
3390 // `p[0] = g_unichar_toupper(p[0])` used to write here directly, but `p[0]` is only
3391 // the first *byte* of `p`, not the first character: translated labels routinely
3392 // start with a multi-byte UTF-8 character (accented capitals, non-Latin scripts).
3393 // Mutating one byte of a multi-byte sequence produces a different, still
3394 // "valid-looking" sequence followed by an orphaned continuation byte - invalid
3395 // UTF-8 that crashes later collation (e.g. g_utf8_collate -> glibc wcscoll_l).
3396 gunichar c = g_utf8_get_char_validated(p, -1);
3397 if(c == (gunichar)-1 || c == (gunichar)-2) return; // invalid UTF-8, leave untouched
3398
3399 gunichar upper = g_unichar_toupper(c);
3400 if(upper == c) return;
3401
3402 gchar utf8_buf[6] = { 0 };
3403 gint n = g_unichar_to_utf8(upper, utf8_buf);
3404 gint orig_len = g_utf8_next_char(p) - p;
3405
3406 // Callers pass buffers sized exactly for the original string (often g_strdup'd),
3407 // not a longer one - only mutate in place if the uppercase form fits.
3408 if(n == orig_len) memcpy(p, utf8_buf, n);
3409}
3410
3411GtkBox * attach_popover(GtkWidget *widget, const char *icon, GtkWidget *content)
3412{
3413 // Create the wrapping box and add the original widget to it
3414 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
3415 gtk_box_pack_start(GTK_BOX(box), widget, FALSE, FALSE, 0);
3416
3417 // Create the info icon button that will trigger the popover
3418 GtkWidget *button = gtk_menu_button_new();
3419 dt_gui_add_class(button, "popover-button");
3420 GtkWidget *image = gtk_image_new_from_icon_name(icon, GTK_ICON_SIZE_BUTTON);
3421 gtk_button_set_image(GTK_BUTTON(button), image);
3422 gtk_widget_set_hexpand(button, FALSE);
3423 gtk_widget_set_vexpand(button, FALSE);
3424 gtk_widget_set_size_request(button, DT_PIXEL_APPLY_DPI(16), DT_PIXEL_APPLY_DPI(16));
3425 gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 0);
3426
3427 // Create the content of the popover
3428 GtkWidget *popover_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
3429 gtk_box_pack_start(GTK_BOX(popover_box), content, FALSE, FALSE, 0);
3430
3431 // Wrap the content into a popover and attach it to the button
3432 GtkWidget *popover = gtk_popover_new(button);
3433 gtk_container_add(GTK_CONTAINER(popover), popover_box);
3434 gtk_popover_set_modal(GTK_POPOVER(popover), FALSE);
3435 g_signal_connect(G_OBJECT(popover), "show", G_CALLBACK(_popover_set_relative_to_topmost_parent), button);
3436 gtk_menu_button_set_popover(GTK_MENU_BUTTON(button), popover);
3437 gtk_widget_show_all(popover_box);
3438
3439 return GTK_BOX(box);
3440}
3441
3442GtkBox * attach_help_popover(GtkWidget *widget, const char *label)
3443{
3444 // Create the content of the popover
3445 GtkWidget *popover_label = gtk_label_new(label);
3446 gtk_label_set_line_wrap(GTK_LABEL(popover_label), TRUE);
3447 gtk_label_set_max_width_chars(GTK_LABEL(popover_label), 60);
3448 return attach_popover(widget, "help-about", popover_label);
3449}
3450
3451static gboolean _text_entry_focus_in_event(GtkWidget *self, GdkEventFocus event, gpointer user_data)
3452{
3454 return FALSE;
3455}
3456
3457static gboolean _text_entry_focus_out_event(GtkWidget *self, GdkEventFocus event, gpointer user_data)
3458{
3460 return FALSE;
3461}
3462
3463static gboolean _text_entry_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
3464{
3465 if(event->keyval == GDK_KEY_Escape)
3466 {
3468 return TRUE;
3469 }
3470 return FALSE;
3471}
3472
3474{
3475 gtk_widget_add_events(widget, GDK_FOCUS_CHANGE_MASK);
3476 g_signal_connect(G_OBJECT(widget), "focus-in-event", G_CALLBACK(_text_entry_focus_in_event), NULL);
3477 g_signal_connect(G_OBJECT(widget), "focus-out-event", G_CALLBACK(_text_entry_focus_out_event), NULL);
3478 g_signal_connect(G_OBJECT(widget), "key-press-event", G_CALLBACK(_text_entry_key_pressed), NULL);
3479}
3480
3481
3483{
3484 // Refocus window, useful if we just closed a popup/modal/transient
3485 gtk_window_present_with_time(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), GDK_CURRENT_TIME);
3486
3487 // Desperate measure to refocus the window
3488 gtk_grab_add(dt_ui_main_window(darktable.gui->ui));
3489 gtk_grab_remove(dt_ui_main_window(darktable.gui->ui));
3490 gtk_widget_grab_focus(dt_ui_main_window(darktable.gui->ui));
3491
3492 // dt_view_manager_name() returns the translated display name (e.g. "Table lumineuse"), not the
3493 // internal id: compare against module_name, which is stable and untranslated.
3495 if(!IS_NULL_PTR(current_view) && !strcmp(current_view->module_name, "lighttable"))
3496 {
3497 gtk_widget_grab_focus(darktable.gui->ui->thumbtable_lighttable->grid);
3498 }
3499 else
3500 {
3501 gtk_widget_grab_focus(dt_ui_center(darktable.gui->ui));
3502 }
3503
3504 // Be sure to re-enable accelerators
3506}
3507
3508// clang-format off
3509// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3510// vim: shiftwidth=2 expandtab tabstop=2 cindent
3511// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3512// clang-format on
gboolean dt_accels_dispatch(GtkWidget *w, GdkEvent *event, gpointer user_data)
Force our listener for all key strokes to bypass reserved Gtk keys.
dt_accels_t * dt_accels_init(char *config_file, GtkAccelFlags flags)
static void dt_accels_disable(dt_accels_t *accels, gboolean state)
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void init(dt_imageio_module_format_t *self)
Definition avif.c:151
uint32_t container(dt_lib_module_t *self)
GtkWidget * dt_bauhaus_resize_handle_new(GtkOrientation orientation, gboolean invert, const char *tooltip, dt_bauhaus_resize_handle_get_size_f get_size, dt_bauhaus_resize_handle_resize_f resize, gpointer user_data)
Create a themed handle widget driving one-dimensional resize gestures.
Definition bauhaus.c:1180
int width
Definition bilateral.h:1
int height
Definition bilateral.h:1
static const dt_aligned_pixel_simd_t const dt_adaptation_t const float p
void dt_colorspaces_set_display_profile(const dt_colorspaces_color_profile_type_t profile_type)
@ DT_COLORSPACE_DISPLAY
Definition colorspaces.h:91
const float delta
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
char * name
void dt_conf_set_bool(const char *name, int val)
int dt_conf_get_bool(const char *name)
int dt_conf_key_exists(const char *key)
float dt_conf_get_float(const char *name)
void dt_conf_set_int(const char *name, int val)
int dt_conf_get_int(const char *name)
void dt_conf_set_string(const char *name, const char *val)
const char * dt_conf_get_string_const(const char *name)
void dt_control_key_pressed(GdkEventKey *event)
Definition control.c:635
void dt_control_button_pressed(double x, double y, double pressure, int which, int type, uint32_t state)
Definition control.c:722
void * dt_control_expose(void *voidptr)
Definition control.c:550
void dt_control_quit()
Definition control.c:443
void dt_control_mouse_leave()
Definition control.c:620
gboolean dt_control_configure(GtkWidget *da, GdkEventConfigure *event, gpointer user_data)
Definition control.c:506
void dt_control_button_released(double x, double y, int which, uint32_t state)
Definition control.c:640
void dt_control_set_pointer_input(const dt_control_pointer_input_t *input)
Definition control.c:83
void dt_control_mouse_moved(double x, double y, double pressure, int which)
Definition control.c:630
void dt_control_mouse_enter()
Definition control.c:625
#define dt_control_change_cursor(cursor)
Definition control.h:116
int dt_load_from_string(const gchar *input, gboolean open_image_in_dr, gboolean *single_image)
Definition darktable.c:366
void dt_cleanup()
Definition darktable.c:1366
void dt_concat_path_file(char destination[PATH_MAX], const char path[PATH_MAX], const char *const file)
Definition darktable.c:1947
darktable_t darktable
Definition darktable.c:183
void dt_print(dt_debug_thread_t thread, const char *msg,...)
Definition darktable.c:1600
@ DT_DEBUG_INPUT
Definition darktable.h:751
@ DT_DEBUG_GTK
Definition darktable.h:740
@ DT_DEBUG_CONTROL
Definition darktable.h:738
#define dt_free(ptr)
Definition darktable.h:478
static const dt_aligned_pixel_simd_t value
Definition darktable.h:599
#define PATH_MAX
Definition darktable.h:1189
#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 darktable.h:293
void dtgtk_cairo_paint_solid_arrow(cairo_t *cr, gint x, int y, gint w, gint h, gint flags, void *data)
@ CPF_DIRECTION_LEFT
Definition dtgtk/paint.h:63
@ CPF_DIRECTION_DOWN
Definition dtgtk/paint.h:62
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:384
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Definition dtpthread.h:369
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:374
void dtgtk_expander_set_expanded(GtkDarktableExpander *expander, gboolean expanded)
Definition expander.c:70
GtkWidget * dtgtk_expander_new(GtkWidget *header, GtkWidget *body)
Definition expander.c:101
#define DTGTK_EXPANDER(obj)
Definition expander.h:30
void dt_loc_get_sharedir(char *sharedir, size_t bufsize)
void dt_loc_get_datadir(char *datadir, size_t bufsize)
void dt_loc_get_user_config_dir(char *configdir, size_t bufsize)
static const char * _get_source_name(int pos)
Definition gtk.c:1156
gboolean dt_gui_get_scroll_deltas(const GdkEventScroll *event, gdouble *delta_x, gdouble *delta_y)
Definition gtk.c:240
static double _clamp01d(const double value)
Definition gtk.c:657
static void _widget_auto_model_rows_reordered(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gint *new_order, gpointer user_data)
Definition gtk.c:2809
static gboolean _sample_tablet_state_from_devices(const GdkEvent *event, double *pressure, gboolean *have_pressure, double *tilt_x, double *tilt_y, gboolean *have_tilt, const char **picked_device_name)
Definition gtk.c:721
static void _ui_widget_redraw_callback(gpointer instance, GtkWidget *widget)
Definition gtk.c:1841
GtkBox * attach_help_popover(GtkWidget *widget, const char *label)
Definition gtk.c:3442
static gboolean _button_released(GtkWidget *w, GdkEventButton *event, gpointer user_data)
Definition gtk.c:1079
void dt_gui_menu_popup(GtkMenu *menu, GtkWidget *button, GdkGravity widget_anchor, GdkGravity menu_anchor)
Definition gtk.c:3185
static dt_control_pointer_input_t _extract_pointer_input(const GdkEvent *event, const double x, const double y, const guint32 time_ms, const gboolean reset_kinematics, const char *tag)
Definition gtk.c:833
static gboolean _text_entry_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
Definition gtk.c:3463
void dt_gui_cairo_set_font_options(cairo_t *cr, GtkWidget *widget)
Definition gtk.c:1556
void dt_ui_container_foreach(dt_ui_t *ui, const dt_ui_container_t c, GtkCallback callback)
calls a callback on all children widgets from container
Definition gtk.c:1814
gboolean dt_gui_get_scroll_delta(const GdkEventScroll *event, gdouble *delta)
Definition gtk.c:383
void dt_gui_container_remove_children(GtkContainer *container)
Definition gtk.c:3139
static int _resizable_scroll_handle_resize(int requested_size, gboolean finished, gpointer user_data)
Definition gtk.c:2772
gboolean dt_gui_quit_callback(GtkWidget *widget, GdkEvent *event, gpointer user_data)
Definition gtk.c:550
gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, int *delta_x, int *delta_y)
Definition gtk.c:300
static void _widget_auto_on_buffer_changed(GObject *textview, GParamSpec *pspec, gpointer user_data)
Definition gtk.c:2901
static int _textview_count_visible_rows(GtkWidget *textview)
Definition gtk.c:2605
static void _three_choice_button_handler(GtkButton *button, gpointer data)
Definition gtk.c:2042
static gboolean _center_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition gtk.c:1144
static int _resizable_area_resize(int requested_size, gboolean finished, gpointer user_data)
Definition gtk.c:3055
static const gdouble * _event_axes(const GdkEvent *event)
Definition gtk.c:662
gboolean dt_gui_get_scroll_unit_delta(const GdkEventScroll *event, int *delta)
Definition gtk.c:394
static void _collapsible_container_show(GtkWidget *widget, gpointer user_data)
Definition gtk.c:3278
void dt_gui_search_stop(GtkSearchEntry *entry, GtkWidget *widget)
Definition gtk.c:3248
static void _widget_auto_connect_model(GtkWidget *treeview)
Definition gtk.c:2845
void dt_gui_hide_collapsible_section(dt_gui_collapsible_section_t *cs)
Definition gtk.c:3327
static void _widget_auto_model_row_expanded(GtkTreeView *tree_view, GtkTreeIter *expanded_iter, GtkTreePath *path, gpointer user_data)
Definition gtk.c:2819
static void _delete_child(GtkWidget *widget, gpointer data)
Definition gtk.c:3145
static const char * _get_mode_name(int pos)
Definition gtk.c:1166
void dt_gui_remove_class(GtkWidget *widget, const gchar *class_name)
Definition gtk.c:143
static void _widget_auto_model_row_changed(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition gtk.c:2800
GtkWidget * dt_ui_resizable_drawing_area(GtkWidget *area, char *config_str, int default_height, int min_height)
Make a self-drawing widget (typically a GtkDrawingArea graph or scope) vertically resizable.
Definition gtk.c:3068
static void _remove_child(GtkWidget *widget, gpointer data)
Definition gtk.c:3134
void dt_gui_new_collapsible_section(dt_gui_collapsible_section_t *cs, const char *confname, const char *label, GtkBox *parent, GtkPackType pack)
Create a collapsible section and pack it into the parent box.
Definition gtk.c:3334
static int _resizable_area_get_size(gpointer user_data)
Definition gtk.c:3047
static gboolean _key_pressed(GtkWidget *w, GdkEventKey *event)
Definition gtk.c:1137
static void _gtk_main_quit_safe(GtkWidget *widget, gpointer data)
Definition gtk.c:1914
static gint _get_container_row_heigth(GtkWidget *w)
Definition gtk.c:2515
static gboolean _notebook_motion_notify_callback(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
Definition gtk.c:2450
static void _widget_auto_connect_buffer(GtkWidget *textview)
Definition gtk.c:2874
GtkWidget * dt_ui_notebook_page(GtkNotebook *notebook, const char *text, const char *tooltip)
Definition gtk.c:2470
static gboolean _center_enter(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition gtk.c:1150
GtkNotebook * dt_ui_notebook_new()
Definition gtk.c:2465
static gboolean _ui_toast_button_press_event(GtkWidget *widget, GdkEvent *event, gpointer user_data)
Definition gtk.c:1599
static gboolean _button_pressed(GtkWidget *w, GdkEventButton *event, gpointer user_data)
Definition gtk.c:1063
static dt_tablet_motion_state_t _tablet_motion_state
Definition gtk.c:655
static void _resizable_scroll_realize(GtkWidget *w, gpointer user_data)
Definition gtk.c:2759
void dt_gui_freeze_reset(void)
Definition gtk.c:229
static gboolean _draw(GtkWidget *da, cairo_t *cr, gpointer user_data)
Definition gtk.c:406
static gboolean _ui_log_button_press_event(GtkWidget *widget, GdkEvent *event, gpointer user_data)
Definition gtk.c:1593
static void _coeffs_button_changed(GtkDarktableToggleButton *widget, gpointer user_data)
Definition gtk.c:3289
void dt_ui_container_destroy_children(dt_ui_t *ui, const dt_ui_container_t c)
destroy all child widgets from container
Definition gtk.c:1820
char * dt_gui_show_standalone_string_dialog(const char *title, const char *markup, const char *placeholder, const char *no_text, const char *yes_text)
Definition gtk.c:2149
static void _widget_auto_height_free(gpointer data)
Definition gtk.c:2909
static void _widget_auto_model_row_collapsed(GtkTreeView *tree_view, GtkTreeIter *collapsed_iter, GtkTreePath *path, gpointer user_data)
Definition gtk.c:2828
static int _resizable_scroll_handle_get_size(gpointer user_data)
Definition gtk.c:2765
static void _collapsible_set_states(dt_gui_collapsible_section_t *cs, gboolean active)
Definition gtk.c:3263
static void _widget_auto_model_row_deleted(GtkTreeModel *model, GtkTreePath *path, gpointer user_data)
Definition gtk.c:2793
int dt_gui_container_num_children(GtkContainer *container)
Definition gtk.c:3104
void dt_gui_freeze_end_(const char *file, int line)
Definition gtk.c:212
static void _widget_auto_disconnect_buffer(dt_gui_widget_auto_height_t *state)
Definition gtk.c:2645
void dt_gui_gtk_set_source_rgb(cairo_t *cr, dt_gui_color_t color)
Definition gtk.c:523
void dt_gui_draw_rounded_rectangle(cairo_t *cr, float width, float height, float x, float y)
Definition gtk.c:3224
GtkWidget * dt_gui_container_nth_child(GtkContainer *container, int which)
Definition gtk.c:3124
static GtkWidget * _search_parent_scrolled_window(GtkWidget *w)
Definition gtk.c:2557
static void _coeffs_expander_click(GtkWidget *widget, GdkEventButton *e, gpointer user_data)
Definition gtk.c:3301
void dt_gui_update_collapsible_section(dt_gui_collapsible_section_t *cs)
Definition gtk.c:3312
void dt_gui_textview_set_padding(GtkTextView *textview)
Apply the standard recessed-input text padding to a GtkTextView.
Definition gtk.c:2919
void dt_gui_load_theme(const char *theme)
Definition gtk.c:2239
GtkWidget * dt_ui_scroll_wrap_get_scrolled_window(GtkWidget *wrapper)
Return the inner scrolled window of a dt_ui_scroll_wrap() wrapper, or NULL.
Definition gtk.c:3007
int dt_gui_show_standalone_three_choice_dialog(const char *title, const char *markup, const char *first_text, const char *second_text, const char *third_text)
Definition gtk.c:2057
static void _widget_auto_text_buffer_changed(GtkTextBuffer *buffer, gpointer user_data)
Definition gtk.c:2839
void dt_capitalize_label(gchar *text)
Definition gtk.c:3382
int dt_gui_gtk_write_config()
Definition gtk.c:467
static void _yes_no_button_handler(GtkButton *button, gpointer data)
Definition gtk.c:1921
void dt_gui_store_last_preset(const char *name)
Definition gtk.c:556
void dt_ellipsize_combo(GtkComboBox *cbox)
Definition gtk.c:1895
GdkModifierType dt_key_modifier_state()
Definition gtk.c:2397
void dt_configure_ppd_dpi(dt_gui_gtk_t *gui)
Definition gtk.c:1429
static gint _resizable_scroll_max_height(void)
Window-height ceiling shared by the auto-size rule and the drag handle.
Definition gtk.c:2667
static void _resizable_area_free(gpointer data)
Definition gtk.c:3039
static gboolean _get_axis_value_for_source(const GdkEvent *event, GdkDevice *source_device, const GdkAxisUse axis, double *value, gboolean *from_source_map)
Definition gtk.c:679
static const char * _get_axis_name(int pos)
Definition gtk.c:1173
static gboolean _dt_on_gui_thread(void)
Definition gtk.c:188
void dt_gui_gtk_quit()
Definition gtk.c:535
static void _init_widgets(dt_gui_gtk_t *gui)
Definition gtk.c:1605
void dt_gui_set_symbolic_icon(GtkWidget *image, const char *icon_name, GtkIconSize size, const GdkRGBA *color)
Set a symbolic icon on an image widget, optionally forcing a specific color.
Definition gtk.c:153
gboolean dt_gui_search_start(GtkWidget *widget, GdkEventKey *event, GtkSearchEntry *entry)
Definition gtk.c:3237
static const char *const DT_GUI_WIDGET_AUTO_HEIGHT_KEY
Definition gtk.c:2554
static void _notebook_size_callback(GtkNotebook *notebook, GdkRectangle *allocation, gpointer *data)
Definition gtk.c:2410
void dt_gui_gtk_set_source_rgba(cairo_t *cr, dt_gui_color_t color, float opacity_coef)
Definition gtk.c:529
double dt_get_system_gui_ppd(GtkWidget *widget)
Definition gtk.c:1412
static gboolean _mouse_moved(GtkWidget *w, GdkEventMotion *event, gpointer user_data)
Definition gtk.c:1090
void dt_gui_container_destroy_children(GtkContainer *container)
Definition gtk.c:3151
static void _widget_auto_model_row_inserted(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
Definition gtk.c:2784
void dt_gui_add_help_link(GtkWidget *widget, char *link)
Definition gtk.c:2232
void dt_ui_notify_user()
draw user's attention
Definition gtk.c:1825
gboolean dt_gui_show_standalone_yes_no_dialog(const char *title, const char *markup, const char *no_text, const char *yes_text)
Definition gtk.c:1952
static gboolean _focus_in_out_event(GtkWidget *widget, GdkEvent *event, gpointer user_data)
Definition gtk.c:1587
static void _ui_log_redraw_callback(gpointer instance, GtkWidget *widget)
Definition gtk.c:1846
GtkWidget * dt_gui_get_popup_relative_widget(GtkWidget *widget, GdkRectangle *rect)
Resolve the widget used as parent for nested popups on Wayland.
Definition gtk.c:3157
static void _widget_auto_on_model_changed(GObject *treeview, GParamSpec *pspec, gpointer user_data)
Definition gtk.c:2893
int dt_gui_gtk_init(dt_gui_gtk_t *gui)
Definition gtk.c:1183
static void _resizable_scroll_apply(GtkWidget *w)
The single sizing rule for every dt_ui_scroll_wrap area.
Definition gtk.c:2682
static void _widget_auto_update(GtkWidget *widget)
Definition gtk.c:2748
gboolean dt_gui_container_has_children(GtkContainer *container)
Definition gtk.c:3094
void dt_gui_refocus_center()
Definition gtk.c:3482
static const char *const DT_UI_RESIZABLE_AREA_KEY
Definition gtk.c:3030
static void _refresh_all_container_spacing(void)
Definition gtk.c:1508
static gint _last_box_spacing
Definition gtk.c:1471
gboolean dt_gui_widgets_suppressed(void)
Definition gtk.c:194
void dt_gui_set_pango_resolution(PangoLayout *layout)
Definition gtk.c:1548
GtkBox * attach_popover(GtkWidget *widget, const char *icon, GtkWidget *content)
Definition gtk.c:3411
static void _popover_set_relative_to_topmost_parent(GtkPopover *popover, GtkWidget *button)
Definition gtk.c:3215
void dt_gui_refocus_parent(GtkWindow *parent)
Definition gtk.c:1936
static gboolean _configure(GtkWidget *da, GdkEventConfigure *event, gpointer user_data)
Definition gtk.c:590
static int _treeview_count_visible_rows(GtkTreeView *treeview, GtkTreeModel *model, GtkTreeIter *parent)
Definition gtk.c:2572
static gboolean _scrolled(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
Definition gtk.c:451
GtkWidget * dt_ui_scroll_wrap(GtkWidget *w, gint min_size, char *config_str, dt_ui_resize_mode_t mode)
Wrap a scrollable content widget in a recessed, vertically resizable scrolled window.
Definition gtk.c:2945
static void _ui_toast_redraw_callback(gpointer instance, GtkWidget *widget)
Definition gtk.c:1868
static void _notebook_switch_page_signal_relay(GtkNotebook *notebook, GtkWidget *page, guint page_num, gpointer user_data)
Definition gtk.c:2494
void dt_gui_gtk_run(dt_gui_gtk_t *gui)
Definition gtk.c:1371
static gboolean _text_entry_focus_out_event(GtkWidget *self, GdkEventFocus event, gpointer user_data)
Definition gtk.c:3457
GtkWidget * dt_gui_container_first_child(GtkContainer *container)
Definition gtk.c:3114
void dt_accels_disconnect_on_text_input(GtkWidget *widget)
Disconnects accels when a text or search entry gets the focus, and reconnects them when it looses it....
Definition gtk.c:3473
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
Definition gtk.c:133
static gboolean _text_entry_focus_in_event(GtkWidget *self, GdkEventFocus event, gpointer user_data)
Definition gtk.c:3451
static void _refresh_container_spacing(GtkWidget *w, gpointer user_data)
Definition gtk.c:1478
static gboolean _window_configure(GtkWidget *da, GdkEvent *event, gpointer user_data)
Definition gtk.c:627
void dt_ui_notebook_set_picker_owner(GtkNotebook *notebook, gpointer owner)
Register an opaque owner for a GtkNotebook's page switches, and relay every "switch_page" as DT_SIGNA...
Definition gtk.c:2500
void dt_ui_container_focus_widget(dt_ui_t *ui, const dt_ui_container_t c, GtkWidget *w)
gives a widget focus in the container
Definition gtk.c:1804
static gboolean _sample_axis_from_device_state(GdkWindow *window, GdkDevice *device, const GdkAxisUse axis, double *value)
Definition gtk.c:700
void dt_gui_update_em(void)
Definition gtk.c:1522
static void _widget_auto_disconnect_model(dt_gui_widget_auto_height_t *state, GtkWidget *treeview)
Definition gtk.c:2616
void dt_gui_freeze_begin_(const char *file, int line)
Definition gtk.c:199
static gboolean _resizable_scroll_draw(GtkWidget *w, cairo_t *cr, gpointer user_data)
Definition gtk.c:2753
GtkWidget * dt_ui_toast_msg(dt_ui_t *ui)
get the toast message widget
GtkWidget * dt_ui_center(dt_ui_t *ui)
get the center drawable widget
dt_gui_color_t
Definition gtk.h:126
@ DT_GUI_COLOR_BG
Definition gtk.h:127
@ DT_GUI_COLOR_DARKROOM_PREVIEW_BG
Definition gtk.h:129
@ DT_GUI_COLOR_LIGHTTABLE_FONT
Definition gtk.h:132
@ DT_GUI_COLOR_MAP_LOC_SHAPE_HIGH
Definition gtk.h:155
@ DT_GUI_COLOR_LOG_FG
Definition gtk.h:151
@ DT_GUI_COLOR_BRUSH_CURSOR
Definition gtk.h:134
@ DT_GUI_COLOR_LAST
Definition gtk.h:159
@ DT_GUI_COLOR_THUMBNAIL_FONT
Definition gtk.h:143
@ DT_GUI_COLOR_THUMBNAIL_SELECTED_BG
Definition gtk.h:138
@ DT_GUI_COLOR_MAP_COUNT_BG
Definition gtk.h:154
@ DT_GUI_COLOR_LOG_BG
Definition gtk.h:150
@ DT_GUI_COLOR_THUMBNAIL_SELECTED_BORDER
Definition gtk.h:147
@ DT_GUI_COLOR_THUMBNAIL_OUTLINE
Definition gtk.h:140
@ DT_GUI_COLOR_MAP_LOC_SHAPE_LOW
Definition gtk.h:156
@ DT_GUI_COLOR_LIGHTTABLE_BG
Definition gtk.h:130
@ DT_GUI_COLOR_BUTTON_FG
Definition gtk.h:136
@ DT_GUI_COLOR_DARKROOM_BG
Definition gtk.h:128
@ DT_GUI_COLOR_THUMBNAIL_BORDER
Definition gtk.h:146
@ DT_GUI_COLOR_LIGHTTABLE_PREVIEW_BG
Definition gtk.h:131
@ DT_GUI_COLOR_WARNING
Definition gtk.h:158
@ DT_GUI_COLOR_MAP_COUNT_SAME_LOC
Definition gtk.h:152
@ DT_GUI_COLOR_MAP_COUNT_DIFF_LOC
Definition gtk.h:153
@ DT_GUI_COLOR_THUMBNAIL_BG
Definition gtk.h:137
@ DT_GUI_COLOR_PREVIEW_HOVER_BORDER
Definition gtk.h:149
@ DT_GUI_COLOR_FILMSTRIP_BG
Definition gtk.h:148
@ DT_GUI_COLOR_BRUSH_TRACE
Definition gtk.h:135
@ DT_GUI_COLOR_THUMBNAIL_SELECTED_FONT
Definition gtk.h:144
@ DT_GUI_COLOR_THUMBNAIL_SELECTED_OUTLINE
Definition gtk.h:141
@ DT_GUI_COLOR_THUMBNAIL_HOVER_BG
Definition gtk.h:139
@ DT_GUI_COLOR_PRINT_BG
Definition gtk.h:133
@ DT_GUI_COLOR_MAP_LOC_SHAPE_DEF
Definition gtk.h:157
@ DT_GUI_COLOR_THUMBNAIL_HOVER_FONT
Definition gtk.h:145
@ DT_GUI_COLOR_THUMBNAIL_HOVER_OUTLINE
Definition gtk.h:142
static cairo_surface_t * dt_cairo_image_surface_create(cairo_format_t format, int width, int height)
Definition gtk.h:322
GtkWidget * dt_ui_log_msg(dt_ui_t *ui)
get the log message widget
GtkWidget * dt_ui_main_window(dt_ui_t *ui)
get the main window widget
static GtkWidget * dt_ui_section_label_new(const gchar *str)
Definition gtk.h:469
#define DT_UI_SCALE_DEVICE(value)
Definition gtk.h:86
dt_ui_resize_mode_t
Definition gtk.h:263
@ DT_UI_RESIZE_STATIC
Definition gtk.h:270
@ DT_UI_RESIZE_DYNAMIC
Definition gtk.h:266
#define DT_GUI_BOX_SPACING
Definition gtk.h:109
#define DT_PIXEL_APPLY_DPI(value)
Definition gtk.h:90
GtkWidget * dt_ui_center_base(dt_ui_t *ui)
void dt_gui_presets_init()
void dt_guides_set_overlay_colors()
Definition guides.c:666
const char * tooltip
Definition image.h:251
const char * model
static const float x
const char * dt_l10n_get_current_lang(dt_l10n_t *l10n)
Definition l10n.c:455
#define M_PI
Definition math.h:45
size_t size
Definition mipmap_cache.c:3
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
void dt_osx_focus_window()
Definition osx.mm:301
void dt_osx_autoset_dpi(GtkWidget *widget)
Definition osx.mm:54
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:104
float dt_osx_get_ppd()
Definition osx.mm:73
#define DT_DEBUG_CONTROL_SIGNAL_RAISE(ctlsig, signal,...)
Definition signal.h:366
@ DT_SIGNAL_CONTROL_REDRAW_ALL
This signal is raised when dt_control_queue_redraw() is called. no param, no returned value.
Definition signal.h:69
@ DT_SIGNAL_CONTROL_TOAST_REDRAW
This signal is raised when dt_control_toast_redraw() is called. no param, no returned value.
Definition signal.h:288
@ DT_SIGNAL_CONTROL_NOTEBOOK_TAB_CHANGED
This signal is raised when a GtkNotebook created/registered via dt_ui_notebook_set_picker_owner() swi...
Definition signal.h:302
@ DT_SIGNAL_CONTROL_LOG_REDRAW
This signal is raised when dt_control_log_redraw() is called. no param, no returned value.
Definition signal.h:283
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:376
void dt_gui_splash_set_transient_for(GtkWidget *parent)
Definition splash.c:79
struct _GtkWidget GtkWidget
Definition splash.h:29
const float uint32_t state[4]
const char * confname
Definition gtk.h:255
GtkWidget * toggle
Definition gtk.h:256
GtkWidget * expander
Definition gtk.h:257
GtkWidget * label
Definition gtk.h:259
gint new_s
Definition gtk.c:1473
gint old_s
Definition gtk.c:1473
GtkWidget * button_first
Definition gtk.c:2039
GtkWidget * button_third
Definition gtk.c:2039
GtkWidget * button_second
Definition gtk.c:2039
GtkWidget * window
Definition gtk.c:2039
struct dt_l10n_t * l10n
Definition darktable.h:817
struct dt_gui_gtk_t * gui
Definition darktable.h:803
struct dt_control_signal_t * signals
Definition darktable.h:802
GList * themes
Definition darktable.h:857
struct dt_view_manager_t * view_manager
Definition darktable.h:800
struct dt_control_t * control
Definition darktable.h:801
double tabborder
Definition control.h:217
int toast_ack
Definition control.h:247
char toast_message[10][300]
Definition control.h:248
dt_pthread_mutex_t log_mutex
Definition control.h:244
int toast_pos
Definition control.h:247
dt_pthread_mutex_t toast_mutex
Definition control.h:251
char log_message[10][1000]
Definition control.h:241
pthread_t gui_thread
Definition control.h:219
double dpi
Definition gtk.h:205
int32_t _widget_suppress_depth
Definition gtk.h:177
double ppd
Definition gtk.h:205
gint scroll_mask
Definition gtk.h:230
struct dt_gui_gtk_t::@50 export_popup
int32_t center_tooltip
Definition gtk.h:180
cairo_surface_t * surface
Definition gtk.h:169
dt_accels_t * accels
Definition gtk.h:199
struct dt_gui_gtk_t::@51 styles_popup
float effect_radius
Definition gtk.h:215
GtkMenu * presets_popup_menu
Definition gtk.h:170
GtkWidget * has_scroll_focus
Definition gtk.h:234
double dpi_factor
Definition gtk.h:205
dt_ui_t * ui
Definition gtk.h:165
dt_pthread_mutex_t mutex
Definition gtk.h:249
gboolean culling_mode
Definition gtk.h:192
gboolean selection_stacked
Definition gtk.h:196
struct dt_gui_gtk_t::@49 mouse
char gtkrc[PATH_MAX]
Definition gtk.h:225
GtkWidget * window
Definition gtk.h:241
cairo_filter_t filter_image
Definition gtk.h:236
char * last_preset
Definition gtk.h:171
double em
Definition gtk.h:210
GdkRGBA colors[DT_GUI_COLOR_LAST]
Definition gtk.h:178
gboolean have_pressure
Definition gtk.c:648
GtkWidget * grid
Definition thumbtable.h:109
dt_thumbtable_t * thumbtable_lighttable
GtkWidget * containers[DT_UI_CONTAINER_SIZE]
GtkWidget * main_window
GtkWidget * center_base
GtkWidget * log_msg
GtkWidget * toast_msg
char module_name[64]
Definition view.h:154
double x
double width
double y
enum result_t::@52 result
GtkWidget * button_yes
Definition gtk.c:1911
char * entry_text
Definition gtk.c:1910
GtkWidget * entry
Definition gtk.c:1911
GtkWidget * button_no
Definition gtk.c:1911
@ RESULT_NO
Definition gtk.c:1909
@ RESULT_YES
Definition gtk.c:1909
@ RESULT_NONE
Definition gtk.c:1909
GtkWidget * window
Definition gtk.c:1911
GtkWidget * window
GtkWidget * notebook
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
void dtgtk_togglebutton_set_paint(GtkDarktableToggleButton *button, DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata)
GtkWidget * dtgtk_togglebutton_new(DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata)
#define DTGTK_TOGGLEBUTTON(obj)
gchar * dt_util_str_replace(const gchar *string, const gchar *pattern, const gchar *substitute)
Definition utility.c:136
void dt_view_manager_configure(dt_view_manager_t *vm, int width, int height)
Definition view.c:629
int dt_view_manager_scrolled(dt_view_manager_t *vm, double x, double y, int up, int state, int delta_y)
Definition view.c:641
const dt_view_t * dt_view_manager_get_current_view(dt_view_manager_t *vm)
Definition view.c:141
void dt_ui_cleanup_titlebar(dt_ui_t *ui)
void dt_ui_init_titlebar(dt_ui_t *ui)
void dt_ui_init_main_table(GtkWidget *parent, dt_ui_t *ui)
dt_ui_container_t