Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
bauhaus.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2011-2016 johannes hanika.
4 Copyright (C) 2012-2013 Henrik Andersson.
5 Copyright (C) 2012 Jean-Sébastien Pédron.
6 Copyright (C) 2012, 2014, 2017, 2019-2020 parafin.
7 Copyright (C) 2012 Pascal de Bruijn.
8 Copyright (C) 2012 Richard Wonka.
9 Copyright (C) 2012-2014, 2016-2020 Tobias Ellinghaus.
10 Copyright (C) 2012, 2016 Ulrich Pegelow.
11 Copyright (C) 2013, 2019, 2021-2022 Aldric Renaudin.
12 Copyright (C) 2013 Dennis Gnad.
13 Copyright (C) 2013-2016 Roman Lebedev.
14 Copyright (C) 2013 Simon Spannagel.
15 Copyright (C) 2013 Yari Adan.
16 Copyright (C) 2015 Jérémy Rosen.
17 Copyright (C) 2016 Asma.
18 Copyright (C) 2017 Christian Stussak.
19 Copyright (C) 2017-2018, 2020-2021 Dan Torop.
20 Copyright (C) 2017 luzpaz.
21 Copyright (C) 2018 Matthieu Moy.
22 Copyright (C) 2018-2022 Pascal Obry.
23 Copyright (C) 2018 Peter Budai.
24 Copyright (C) 2019-2020, 2022-2026 Aurélien PIERRE.
25 Copyright (C) 2019-2022 Diederik Ter Rahe.
26 Copyright (C) 2019 emeikei.
27 Copyright (C) 2019-2021 Heiko Bauke.
28 Copyright (C) 2019 Jakub Filipowicz.
29 Copyright (C) 2019 jakubfi.
30 Copyright (C) 2020-2022 Chris Elston.
31 Copyright (C) 2020 Hubert Kowalski.
32 Copyright (C) 2020 matt-maguire.
33 Copyright (C) 2020-2021 Philippe Weyland.
34 Copyright (C) 2020-2021 Ralf Brown.
35 Copyright (C) 2020 Érico Rolim.
36 Copyright (C) 2021 lhietal.
37 Copyright (C) 2021 Mark-64.
38 Copyright (C) 2021 Martin Straeten.
39 Copyright (C) 2021 Paolo DePetrillo.
40 Copyright (C) 2022 Martin Bařinka.
41 Copyright (C) 2022 Nicolas Auffray.
42 Copyright (C) 2023 Alynx Zhou.
43 Copyright (C) 2023 Luca Zulberti.
44 Copyright (C) 2025-2026 Guillaume Stutin.
45
46 darktable is free software: you can redistribute it and/or modify
47 it under the terms of the GNU General Public License as published by
48 the Free Software Foundation, either version 3 of the License, or
49 (at your option) any later version.
50
51 darktable is distributed in the hope that it will be useful,
52 but WITHOUT ANY WARRANTY; without even the implied warranty of
53 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
54 GNU General Public License for more details.
55
56 You should have received a copy of the GNU General Public License
57 along with darktable. If not, see <http://www.gnu.org/licenses/>.
58*/
59
60#include "common/darktable.h"
61#include "gui/gdkkeys.h"
62#include "bauhaus/bauhaus.h"
63#include "common/calculator.h"
64#include "common/math.h"
65#include "common/debug.h"
66#include "control/conf.h"
67#include "control/control.h"
68#include "develop/imageop.h"
69
70
71#include "gui/accelerators.h"
73#include "gui/gui_throttle.h"
74#include "gui/gtk.h"
75#ifdef GDK_WINDOWING_QUARTZ
76#include "osx/osx.h"
77#endif
78
79#include <math.h>
80#include <strings.h>
81
82#include <pango/pangocairo.h>
83#ifdef GDK_WINDOWING_WAYLAND
84#include <gdk/gdkwayland.h>
85#endif
86
87G_DEFINE_TYPE(DtBauhausWidget, dt_bh, GTK_TYPE_DRAWING_AREA)
88
89// WARNING
90// A lot of GUI setters/getters functions used to have type checking on input widgets
91// and silently returned early if the types were not ok (like trying to set a combobox using slider methods).
92// This only hides programmers mistakes in a way that prevents the soft to crash,
93// but it still leads to faulty GUI interactions and inconsistent widgets values that might be hard to spot.
94// In november 2023, they got removed in order to fail explicitly and possibly crash.
95// If that's not enough, we can always add assertions in the code.
96
97#define DEBUG 0
98
99// fwd declare
100static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data);
101static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data);
102static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf);
103static gboolean _widget_scroll(GtkWidget *widget, GdkEventScroll *event);
104static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event);
105static void _get_preferred_width(GtkWidget *widget, gint *minimum_size, gint *natural_size);
106static void _get_preferred_height(GtkWidget *widget, gint *minimum_size, gint *natural_size);
107static void _style_updated(GtkWidget *widget);
108static void dt_bauhaus_widget_accept(struct dt_bauhaus_widget_t *w, gboolean timeout);
109static void dt_bauhaus_widget_reject(struct dt_bauhaus_widget_t *w);
110static void _combobox_set(GtkWidget *widget, const int pos, gboolean timeout);
111
112// !!! EXECUTIVE NOTE !!!
113// Sizing and spacing need to be declared once only in getters/setters functions below.
114// The rest of the code accesses those values only through the getters.
115// Doxygen docstrings need to be added to explain what is computed, based on what.
116// Code changes that recompute sizing or coordinates outside of getters/setters will be refused.
117
125{
126 if(IS_NULL_PTR(w->margin)) w->margin = gtk_border_new();
127 if(IS_NULL_PTR(w->padding)) w->padding = gtk_border_new();
128 GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(w));
129 const GtkStateFlags state = gtk_widget_get_state_flags(GTK_WIDGET(w));
130 gtk_style_context_get_margin(context, state, w->margin);
131 gtk_style_context_get_padding(context, state, w->padding);
132
133 // Deal with borders by extending margins because we don't care
134 GtkBorder *borders = gtk_border_new();
135 gtk_style_context_get_border(context, state, borders);
136 w->margin->left += borders->left;
137 w->margin->right += borders->right;
138 w->margin->top += borders->top;
139 w->margin->bottom += borders->bottom;
140 gtk_border_free(borders);
141}
142
151{
152 return w->bauhaus->line_height * 1.4;
153}
154
162 const dt_bauhaus_combobox_entry_t *entry)
163{
164 const float row_height_factor = (entry->is_separator) ? MAX(entry->row_height_factor, 0.1f) : 1.f;
165 return _bh_get_row_height(w) * row_height_factor;
166}
167
175{
176 if(w->show_quad)
177 return w->bauhaus->quad_width;
178 else
179 return 0.;
180}
181
190{
191 GtkWidget *box_reference = (widget) ? widget : GTK_WIDGET(w);
192 GtkAllocation allocation;
193 gtk_widget_get_allocation(box_reference, &allocation);
194 return allocation.width - w->margin->left - w->margin->right - w->padding->left - w->padding->right;
195}
196
206static double _widget_get_main_width(struct dt_bauhaus_widget_t *w, GtkWidget *widget, double *total_width)
207{
208 const double tot_width = _widget_get_total_width(w, widget);
209 // Slider cursors are centered on the scale bounds, so the scale starts one
210 // cursor radius after the text origin while keeping its right edge flush.
211 const double slider_cursor_radius = (w->type == DT_BAUHAUS_SLIDER) ? 0.5 * w->bauhaus->marker_size : 0.0;
212 if(total_width) *total_width = tot_width;
213 return tot_width - _widget_get_quad_width(w) - 2. * INNER_PADDING - slider_cursor_radius;
214}
215
224{
225 GtkWidget *box_reference = (widget) ? widget : GTK_WIDGET(w);
226 GtkAllocation allocation;
227 gtk_widget_get_allocation(box_reference, &allocation);
228 return allocation.height - w->margin->top - w->margin->bottom - w->padding->top - w->padding->bottom;
229}
230
249static double _get_combobox_height(GtkWidget *widget)
250{
251 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
252 return w->margin->top + w->padding->top + w->margin->bottom + w->padding->bottom
253 + w->bauhaus->line_height;
254}
255
256static double _get_slider_height(GtkWidget *widget)
257{
258 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
259 return w->margin->top + w->padding->top + w->margin->bottom + w->padding->bottom + INNER_PADDING / 2.
261}
262
264{
265 return w->bauhaus->line_height + INNER_PADDING + w->bauhaus->baseline_size / 2.0f;
266}
267
269{
270 // Total height of the text label + slider baseline, discarding padding
272
273}
274
276{
277 dt_gui_module_t *module = (dt_gui_module_t *)(w->module);
279
280 // Need to run the populating callback first for dynamically-populated ones.
281 if(d->populate) d->populate(GTK_WIDGET(w), module);
282 if(!d->entries->len) return 0.;
283
284 double height = 0.;
285
286 // Add an extra sit for user keyboard input if any
287 if(w->bauhaus->keys_cnt > 0) height += _bh_get_row_height(w);
288
289 for(int i = 0; i < d->entries->len; i++)
290 {
291 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
293 }
294
295 return height;
296}
297
298
307static void _translate_cursor(double *x, double *y, struct dt_bauhaus_widget_t *const w)
308{
309 const double slider_cursor_radius = (w->type == DT_BAUHAUS_SLIDER) ? 0.5 * w->bauhaus->marker_size : 0.0;
310 const double left_offset = w->margin->left + w->padding->left + slider_cursor_radius;
311 *x -= left_offset;
312 *y -= w->margin->top + w->padding->top;
313}
314
315// Convenience state for cursor position over widget
317{
318 BH_REGION_OUT = 0, // we are outside the padding box
319 BH_REGION_MAIN, // we are on the slider scale or combobox label/value, aka out of the quad button
320 BH_REGION_QUAD, // we are on the quad button
322
335static _bh_active_region_t _bh_get_active_region(GtkWidget *widget, double *x, double *y, double *width, GtkWidget *popup)
336{
337 struct dt_bauhaus_widget_t *const w = DT_BAUHAUS_WIDGET(widget);
338
339 // The widget to use as a reference to fetch allocation and compute sizes
340 GtkWidget *box_reference = (popup) ? popup : widget;
341 double total_width;
342 const double main_width = _widget_get_main_width(w, box_reference, &total_width);
343 const double main_height = _widget_get_main_height(w, box_reference);
344
345 if(width) *width = main_width;
346 _translate_cursor(x, y, w);
347
348 // Check if we are within popup frame
349 if(*y < 0. || *y > main_height || *x < 0. || *x > total_width)
350 return BH_REGION_OUT;
351
352 // Check where we are horizontally
353 // The quad now begins exactly 2*INNER_PADDING past the main area for both
354 // sliders and comboboxes (see the quad draw sites), so use the same boundary.
355 if(*x <= main_width + 2. * INNER_PADDING)
356 return BH_REGION_MAIN;
357 else
358 return BH_REGION_QUAD;
359
360 return BH_REGION_OUT;
361}
362
370static float _bh_round_to_n_digits(const struct dt_bauhaus_widget_t *const w, float x)
371{
372 const dt_bauhaus_slider_data_t *const d = &w->data.slider;
373 const float factor = (float)ipow(10, d->digits);
374 return roundf(x * factor) / factor;
375}
376
383static float _bh_slider_get_min_step(const struct dt_bauhaus_widget_t *const w)
384{
386 return d->factor * (float)ipow(10, d->digits);
387}
388
390{
392 return 10.0 / (_bh_slider_get_min_step(w) * (d->max - d->min));
393}
394
396{
398 {
400 double y = w->bauhaus->mouse_y;
401 d->hovered = -1;
402 gtk_widget_set_tooltip_text(w->bauhaus->popup_area, NULL);
403
404 // If the combobox has a user input row, it is always the first one and has the regular row height.
405 if(w->bauhaus->keys_cnt > 0)
406 {
407 y -= _bh_get_row_height(w);
408 if(y < 0.) return;
409 }
410
411 // Separators may use less vertical space than regular entries.
412 // Accumulate actual row heights so mouse hit-testing stays aligned with drawing.
413 for(int i = 0; i < d->entries->len; i++)
414 {
415 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
416 const double entry_height = _bh_get_combobox_entry_height(w, entry);
417 if(y < entry_height)
418 {
419 d->hovered = i;
420 gtk_widget_set_tooltip_text(w->bauhaus->popup_area, entry->tooltip);
421 return;
422 }
423 y -= entry_height;
424 }
425 }
426}
427
428static _bh_active_region_t _popup_coordinates(dt_bauhaus_t *bh, const double x_root, const double y_root, double *event_x, double *event_y)
429{
430 // Because the popup widget is a floating window, it keeps capturing motion events even if they don't
431 // overlap it. In those events, (x, y) coordinates are expressed in the space of the hovered third-party widget,
432 // meaning their coordinates will seem ok from here (right range regarding height/width of widget) but will belong to something else.
433 // We need to grab absolute coordinates in the main window space to ensure we overlay the widget popup.
434 gint wx, wy;
435 GdkWindow *window = gtk_widget_get_window(bh->popup_window);
436 gdk_window_get_origin(window, &wx, &wy);
437 *event_x = x_root - (double)wx;
438 *event_y = y_root - (double)wy;
439 return _bh_get_active_region(GTK_WIDGET(bh->current), event_x, event_y, NULL, bh->popup_window);
440}
441
442// Ensure the programmatically-focused widget is visible,
443// and all its parents containers expose the right page before grabbing focus.
444#define DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY "dt-bauhaus-focus-idle-source"
445#define DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY "dt-bauhaus-focus-idle-tries"
446#define DT_BAUHAUS_FOCUS_IDLE_MAX_TRIES 20
447static gboolean ensure_focus_idle(gpointer data)
448{
449 GtkWidget *target = GTK_WIDGET(data);
450 if(!GTK_IS_WIDGET(target)) return G_SOURCE_REMOVE;
451
452 GtkWidget *child = target;
453
454 for(GtkWidget *w = child; w; w = gtk_widget_get_parent(w))
455 {
456 if(GTK_IS_NOTEBOOK(w))
457 {
458 GtkWidget *page = child;
459 while(!IS_NULL_PTR(page) && gtk_widget_get_parent(page) != w)
460 page = gtk_widget_get_parent(page);
461
462 GtkNotebook *nb = GTK_NOTEBOOK(w);
463 const gint page_num = !IS_NULL_PTR(page) ? gtk_notebook_page_num(nb, page) : -1;
464 if(page_num >= 0) gtk_notebook_set_current_page(nb, page_num);
465 }
466 else if(GTK_IS_STACK(w))
467 {
468 GtkWidget *page = child;
469 while(!IS_NULL_PTR(page) && gtk_widget_get_parent(page) != w)
470 page = gtk_widget_get_parent(page);
471
472 GtkWidget *visible_child = gtk_stack_get_visible_child(GTK_STACK(w));
473 if(!IS_NULL_PTR(page) && visible_child != page) gtk_stack_set_visible_child(GTK_STACK(w), page);
474 }
475 child = w;
476 }
477
478 if(gtk_widget_is_drawable(target))
479 {
480 gtk_widget_grab_focus(target);
482 GtkWidget *gtk_focus = NULL;
484 gtk_focus = gtk_window_get_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
486 "[bauhaus] ensure_focus_idle success target=%s(%p) gtk_focus=%s(%p) scroll_focus=%s(%p)\n",
487 gtk_widget_get_name(target), (void *)target,
488 !IS_NULL_PTR(gtk_focus) ? gtk_widget_get_name(gtk_focus) : "<null>", (void *)gtk_focus,
489 !IS_NULL_PTR(darktable.gui->has_scroll_focus) ? gtk_widget_get_name(darktable.gui->has_scroll_focus) : "<null>",
491 g_object_set_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY, NULL);
492 g_object_set_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY, NULL);
493 return G_SOURCE_REMOVE;
494 }
495
496 const int tries = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY)) + 1;
498 {
500 "[bauhaus] ensure_focus_idle abort target=%s(%p) tries=%d drawable=%d\n",
501 gtk_widget_get_name(target), (void *)target, tries, gtk_widget_is_drawable(target));
502 g_object_set_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY, NULL);
503 g_object_set_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY, NULL);
504 return G_SOURCE_REMOVE;
505 }
506
507 g_object_set_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY, GINT_TO_POINTER(tries));
508 return G_SOURCE_CONTINUE;
509}
510
511gboolean dt_bauhaus_focus_in_callback(GtkWidget *widget, GdkEventFocus event, gpointer user_data)
512{
513 // Scroll focus needs to be managed separately from Gtk focus
514 // because of Gtk notebooks (tabs): Gtk gives focus automatically to the first
515 // notebook child, which is not what we want for scroll event capture.
517 gtk_widget_queue_draw(widget);
518 return FALSE;
519}
520
521gboolean dt_bauhaus_focus_out_callback(GtkWidget *widget, GdkEventFocus event, gpointer user_data)
522{
523 // Scroll focus is released from leave-notify so wheel capture survives
524 // transient Gtk focus changes while the pointer is still over the widget.
525 gtk_widget_queue_draw(widget);
526 return FALSE;
527}
528
529
530gboolean dt_bauhaus_focus_callback(GtkWidget *widget, GtkDirectionType direction, gpointer data)
531{
532 // Let user focus on the next/previous widget on arrow up/down
533 if(direction == GTK_DIR_UP || direction == GTK_DIR_DOWN) return FALSE;
534
535 // Any other key stroke is captured
536 return TRUE;
537}
538
539gboolean _action_request_focus(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval,
540 GdkModifierType modifier, gpointer data)
541{
542 if(IS_NULL_PTR(data) || IS_NULL_PTR(accelerable))
543 {
544 dt_toast_log(_("The target widget of the action does not exist anymore"));
545 fprintf(stderr, "The target widget of the action does not exist anymore");
546 return FALSE;
547 }
548
550
551 // Make sure the parent module widget is visible, if we know it,
552 // because we can't grab focus on invisible widgets
553 if(!IS_NULL_PTR(w->module))
554 {
555 dt_iop_module_t *module = (dt_iop_module_t *)w->module;
556 if(!IS_NULL_PTR(module->expander))
557 {
558 g_object_set_data(G_OBJECT(module->expander), "dt-modulegroups-switch-from-active-once",
559 GINT_TO_POINTER(TRUE));
561 }
562
563 // If the target module is already marked as focused, modulegroups focus
564 // signal may not be emitted and tab visibility can stay stale. Drop focus
565 // once so the next focus request re-emits the full focus/update sequence.
568
569 w->module->focus(w->module, FALSE);
570 }
571
572 GtkWidget *target = GTK_WIDGET(data);
573 const guint previous_source = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY));
574 if(previous_source > 0)
575 g_source_remove(previous_source);
576
577 g_object_set_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY, GINT_TO_POINTER(0));
578 const guint source = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, ensure_focus_idle, g_object_ref(target),
579 (GDestroyNotify)g_object_unref);
580 g_object_set_data(G_OBJECT(target), DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY, GUINT_TO_POINTER(source));
581 return TRUE;
582}
583
585{
587
588 int new_pos = d->active;
589 int inc = (delta) > 0 ? 1 : -1;
590 int cur = new_pos + inc;
591 while(delta && cur >= 0 && cur < d->entries->len)
592 {
593 dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, cur);
594 if(entry->sensitive)
595 {
596 new_pos = cur;
597 delta -= inc;
598 }
599 cur += inc;
600 }
601 d->hovered = new_pos;
602 _combobox_set(GTK_WIDGET(w), new_pos, TRUE);
603}
604
606 dt_bauhaus_combobox_alignment_t alignment, gboolean sensitive,
607 void *data, void (*free_func)(void *))
608{
610 entry->label = g_strdup(label);
611 entry->tooltip = g_strdup(tooltip);
612 entry->alignment = alignment;
613 entry->sensitive = sensitive;
614 entry->row_height_factor = 1.f;
615 entry->data = data;
616 entry->free_func = free_func;
617 return entry;
618}
619
626static dt_bauhaus_combobox_entry_t *new_combobox_separator(const float row_height_factor)
627{
629 entry->label = g_strdup("");
630 entry->tooltip = NULL;
631 entry->sensitive = FALSE;
632 entry->is_separator = TRUE;
633 entry->row_height_factor = CLAMPF(row_height_factor, 0.1f, 1.f);
634 return entry;
635}
636
637static void free_combobox_entry(gpointer data)
638{
640 dt_free(entry->label);
641 dt_free(entry->tooltip);
642 if(entry->free_func)
643 entry->free_func(entry->data);
644 dt_free(entry);
645}
646
647static GdkRGBA *default_color_assign()
648{
649 // helper to initialize a color pointer with red color as a default
650 GdkRGBA color = {.red = 1.0f, .green = 0.0f, .blue = 0.0f, .alpha = 1.0f};
651 return gdk_rgba_copy(&color);
652}
653
654// Vertical alignment of text in its bounding box
661
662// Horizontal alignment of text in its bounding box
669
687 GtkStyleContext *context,
688 cairo_t *cr,
689 GdkRectangle *bounding_box,
690 const char *text,
691 _bh_halign_t halign,
692 _bh_valign_t valign,
693 PangoEllipsizeMode ellipsize,
694 GdkRGBA *bg_color,
695 float *width,
696 float *height,
697 GtkStateFlags state)
698{
699 if(IS_NULL_PTR(text)) return;
700
701 // Prepare context and font properties
702 PangoFontDescription *css_font = NULL;
703 gtk_style_context_get(context, state, GTK_STYLE_PROPERTY_FONT, &css_font, NULL);
704 PangoFontDescription *resolved = pango_font_description_copy(w->bauhaus->pango_font_desc);
705
706 if(css_font)
707 {
708 // Apply only style-related fields, NOT size
709 pango_font_description_set_weight(resolved, pango_font_description_get_weight(css_font));
710 pango_font_description_set_style(resolved, pango_font_description_get_style(css_font));
711 pango_font_description_set_variant(resolved, pango_font_description_get_variant(css_font));
712 pango_font_description_set_stretch(resolved, pango_font_description_get_stretch(css_font));
713 pango_font_description_set_family(resolved, pango_font_description_get_family(css_font));
714 pango_font_description_free(css_font);
715 }
716
717 // --- Propagate GTK's font rendering settings to the Cairo context ---
718 //
719 // gtk_widget_update_pango_context() (called by GTK on realise / screen
720 // change) already populated this context with the system's hinting,
721 // anti-aliasing, subpixel order and hint-metrics settings, sourced from
722 // GtkSettings / Xft / fontconfig. This is the identical source used by
723 // every native GTK widget for text rendering.
724 //
725 // pango_cairo_update_layout() copies font options FROM cr TO the layout's
726 // Pango context. When cr is an intermediate image surface (as used by the
727 // bauhaus off-screen compositing path) Cairo initialises it with its own
728 // defaults — anti-aliasing on, regardless of system settings. Calling
729 // pango_cairo_update_layout() without correcting cr first silently
730 // overwrites everything GTK configured.
731 //
732 // Solution: push the widget Pango context's options onto cr before
733 // pango_cairo_update_layout() runs. The round-trip then becomes a no-op
734 // on font options; only the CTM/DPI are synchronised.
735 //
736 // cairo_set_font_options() makes an internal copy, so the const pointer
737 // lifetime is not a concern.
738 PangoContext *pc = gtk_widget_get_pango_context(GTK_WIDGET(w));
739 dt_gui_cairo_set_font_options(cr, GTK_WIDGET(w));
740
741 // --- Build the layout ---
742 PangoLayout *layout = pango_layout_new(pc);
743 pango_layout_set_font_description(layout, resolved);
744 pango_font_description_free(resolved);
745
746 // Set the actual text
747 pango_layout_set_text(layout, text, -1);
748
749 // Sync layout with Cairo context (CTM, DPI).
750 // Font options are now consistent between cr and pc, so no overwrite occurs.
751 pango_cairo_update_layout(cr, layout);
752
753 // --- Measure ---
754 int pango_width, pango_height;
755 pango_layout_get_size(layout, &pango_width, &pango_height);
756 double text_width = (double)pango_width / PANGO_SCALE;
757 double text_height = fmax((double)pango_height / PANGO_SCALE, w->bauhaus->line_height);
758 if(width) *width = (float)text_width;
759 if(height) *height = (float)text_height;
760
761 // --- Ellipsise on overflow ---
762 if(text_width > bounding_box->width)
763 {
764 pango_layout_set_ellipsize(layout, ellipsize);
765 pango_layout_set_width(layout, (int)(PANGO_SCALE * bounding_box->width));
766 text_width = bounding_box->width;
767 if(width) *width = (float)text_width;
768 pango_cairo_update_layout(cr, layout); // re-sync after constraint change
769 }
770
771 // Paint background color if any - useful to highlight elements in popup list
772 if(!IS_NULL_PTR(bg_color))
773 {
774 cairo_save(cr);
775 cairo_rectangle(cr, bounding_box->x, bounding_box->y,
776 bounding_box->width, bounding_box->height);
777 cairo_set_source_rgba(cr, bg_color->red, bg_color->green,
778 bg_color->blue, bg_color->alpha);
779 cairo_fill(cr);
780 cairo_restore(cr);
781 }
782
783 // Compute the coordinates of the top-left corner as to ensure proper alignment
784 // in bounding box given the dimensions of the label.
785 double x = 0.;
786 switch(halign)
787 {
788 case BH_ALIGN_CENTER:
789 x = bounding_box->x + bounding_box->width / 2. - text_width / 2.;
790 break;
791 case BH_ALIGN_RIGHT:
792 x = bounding_box->x + bounding_box->width - text_width;
793 break;
794 case BH_ALIGN_LEFT:
795 default:
796 x = bounding_box->x;
797 break;
798 }
799
800 double y = 0.;
801 switch(valign)
802 {
803 case BH_ALIGN_MIDDLE:
804 y = bounding_box->y + bounding_box->height / 2. - text_height / 2.;
805 break;
806 case BH_ALIGN_BOTTOM:
807 y = bounding_box->y + bounding_box->height - text_height;
808 break;
809 case BH_ALIGN_TOP:
810 default:
811 y = bounding_box->y;
812 break;
813 }
814
815 // Actually (finally) draw everything in place
816 cairo_move_to(cr, x, y);
817 pango_cairo_show_layout(cr, layout);
818
819 // Cleanup
820 g_object_unref(layout);
821}
822
823static void dt_bauhaus_slider_set_normalized(struct dt_bauhaus_widget_t *w, float pos, gboolean raise, gboolean timeout);
824static void _dt_bauhaus_slider_set_with_raise(GtkWidget *widget, float pos, gboolean raise);
825
826static double get_slider_line_offset(const double pos, const double scale, const double x, double y, const double line_height)
827{
828 // handle linear startup and rescale y to fit the whole range again
829 // Note : all inputs are in relative coordinates, except pos
830 float offset = 0.f;
831 if(y < line_height)
832 {
833 offset = x - pos;
834 }
835 else
836 {
837 // Renormalize y coordinates below the baseline
838 y = (y - line_height) / (1.0 - line_height);
839 offset = (x - sqf(y) * .5 - (1.0 - sqf(y)) * pos)
840 / (.5 * sqf(y) / scale + (1.0 - sqf(y)));
841 }
842 // clamp to result in a [0,1] range:
843 if(pos + offset > 1.0) offset = 1.0 - pos;
844 if(pos + offset < 0.0) offset = -pos;
845 return offset;
846}
847
848// draw a loupe guideline for the quadratic zoom in in the slider interface:
849static void draw_slider_line(cairo_t *cr, const double pos, const double off, const double scale, const double width, const double height,
850 const double line_height, double line_width)
851{
852 // pos is normalized position [0,1], offset is on that scale.
853 // ht is in pixels here
854 const int steps = 128;
855 const double corrected_height = (height - line_height);
856
857 cairo_set_line_width(cr, line_width);
858 cairo_move_to(cr, width * (pos + off), line_height);
859 const double half_line_width = line_width / 2.;
860 for(int j = 1; j < steps; j++)
861 {
862 const double y = (double)j / (double)(steps - 1);
863 const double x = sqf(y) * .5f * (1.f + off / scale) + (1.0f - sqf(y)) * (pos + off);
864 cairo_line_to(cr, x * width - half_line_width, line_height + y * corrected_height);
865 }
866}
867// -------------------------------
868
869static void _slider_zoom_range(struct dt_bauhaus_widget_t *w, float zoom)
870{
872
873 const float value = dt_bauhaus_slider_get(GTK_WIDGET(w));
874
875 if(roundf(zoom) == 0.f)
876 {
877 d->min = d->soft_min;
878 d->max = d->soft_max;
879 dt_bauhaus_slider_set(GTK_WIDGET(w), value); // restore value (and move min/max again if needed)
880 return;
881 }
882
883 // make sure current value still in zoomed range
884 const float min_visible = _bh_slider_get_min_step(w);
885 const float multiplier = exp2f(zoom / 2.f);
886 const float new_min = value - multiplier * (value - d->min);
887 const float new_max = value + multiplier * (d->max - value);
888 if(new_min >= d->hard_min
889 && new_max <= d->hard_max
890 && new_max - new_min >= min_visible * 10)
891 {
892 d->min = new_min;
893 d->max = new_max;
894 }
895
896 gtk_widget_queue_draw(GTK_WIDGET(w));
897}
898
899static gboolean dt_bauhaus_popup_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
900{
901 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
903 darktable.gui->has_scroll_focus = GTK_WIDGET(w);
904 return _widget_scroll(GTK_WIDGET(w), event);
905}
906
907static gboolean dt_bauhaus_popup_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
908{
909 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
911
912 double event_x;
913 double event_y;
914 const _bh_active_region_t active = _popup_coordinates(bh, event->x_root, event->y_root, &event_x, &event_y);
915
916#if DEBUG
917 fprintf(stdout, "x: %i, y: %i, active: %i\n", (int)event_x, (int)event_y, active);
918#endif
919
920 if(active == BH_REGION_OUT) return FALSE;
921
922 // Pass-on new cursor coordinates corrected for padding and margin
923 // and start a redraw. Nothing else.
924 bh->mouse_x = event_x;
925 bh->mouse_y = event_y;
926
927 if(w->type == DT_BAUHAUS_COMBOBOX)
928 {
930 gtk_widget_queue_draw(bh->popup_area);
931 }
932 else
933 {
935 const double main_height = _widget_get_main_height(w, widget);
936 const float mouse_off = get_slider_line_offset(
937 d->oldpos, _bh_slider_get_scale(w), bh->mouse_x / _widget_get_main_width(w, NULL, NULL),
938 bh->mouse_y / main_height, _get_slider_bar_height(w) / main_height);
939
940 if(d->is_dragging)
941 {
942 // On dragging (when holding a click), we commit intermediate values to pipeline for "realtime" preview
943 dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off, TRUE, TRUE);
944 }
945 else
946 {
947 // If not dragging, assume user just wants to take his time to fine-tune the value.
948 d->pos = d->oldpos + mouse_off;
949 gtk_widget_queue_draw(bh->popup_area);
950 }
951 }
952
953 return TRUE;
954}
955
956static gboolean dt_bauhaus_popup_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
957{
958 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_NORMAL, TRUE);
959 return TRUE;
960}
961
962static gboolean dt_bauhaus_popup_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
963{
964 int delay = 0;
965 g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL);
966 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
968
969 if(w && (w->type == DT_BAUHAUS_COMBOBOX) && (event->button == 1)
970 && (event->time >= bh->opentime + delay) && !bh->hiding)
971 {
972 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_ACTIVE, TRUE);
973
975 }
976 else if(bh->hiding)
977 {
979 }
980 return TRUE;
981}
982
983static gboolean dt_bauhaus_popup_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
984{
985 int delay = 0;
986 g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL);
987
988 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
990
991 if(event->button == 1)
992 {
994 && event->time < bh->opentime + delay)
995 {
996 // counts as double click, reset:
998 dt_bauhaus_combobox_set(GTK_WIDGET(w), d->defpos);
1000 }
1001 else
1002 {
1003 // only accept left mouse click.
1004 // coordinates are set in motion_notify, which also makes sure they are within the valid range.
1005 // problems appear with the cornercase where user didn't move the cursor since opening the popup.
1006 // aka we need to re-read coordinates here.
1007 double event_x;
1008 double event_y;
1009 const _bh_active_region_t active = _popup_coordinates(bh, event->x_root, event->y_root, &event_x, &event_y);
1010
1011 if(active == BH_REGION_OUT)
1012 {
1015 return TRUE;
1016 }
1017
1018 bh->end_mouse_x = bh->mouse_x = event_x;
1019 bh->end_mouse_y = bh->mouse_y = event_y;
1020
1021 if(w->type == DT_BAUHAUS_SLIDER)
1022 {
1024 d->is_dragging = TRUE;
1025
1026 // Trick to ensure new value ≠ d->pos (so we commit to pipeline), since
1027 // d->pos is used for uncommitted drawings.
1028 const float value = d->pos;
1029 d->pos = d->oldpos;
1031 }
1032 else
1033 {
1036 }
1037 }
1038 bh->hiding = TRUE;
1039 }
1040 else if(event->button == 2 && w->type == DT_BAUHAUS_SLIDER)
1041 {
1042 _slider_zoom_range(w, 0);
1043 gtk_widget_queue_draw(widget);
1044 }
1045 else
1046 {
1048 bh->hiding = TRUE;
1049 }
1050 return TRUE;
1051}
1052
1053static void dt_bauhaus_window_show(GtkWidget *w, gpointer user_data)
1054{
1055 gtk_grab_add(GTK_WIDGET(user_data));
1056}
1057
1058static void dt_bh_init(DtBauhausWidget *class)
1059{
1060 // not sure if we want to use this instead of our code in *_new()
1061 // TODO: the common code from bauhaus_widget_init() could go here.
1062}
1063
1064static gboolean _enter_leave(GtkWidget *widget, GdkEventCrossing *event)
1065{
1066 if(event->type == GDK_ENTER_NOTIFY)
1067 {
1068 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
1069 }
1070 else
1071 {
1072 gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);
1073
1074 // GUI refreshes can emit synthetic crossing events while the pointer never
1075 // physically leaves the widget. Only drop wheel capture on a real pointer
1076 // leave from the widget itself.
1077 const gboolean real_leave = event->mode == GDK_CROSSING_NORMAL
1078 && event->detail != GDK_NOTIFY_INFERIOR
1080 if(real_leave && darktable.gui->has_scroll_focus == widget)
1082 }
1083
1084 gtk_widget_queue_draw(widget);
1085
1086 return FALSE;
1087}
1088
1101
1102static gboolean _resize_handle_cursor(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1103{
1105 if(event->type == GDK_ENTER_NOTIFY)
1106 {
1107 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
1108 dt_control_change_cursor(handle->orientation == GTK_ORIENTATION_VERTICAL
1109 ? GDK_SB_V_DOUBLE_ARROW
1110 : GDK_SB_H_DOUBLE_ARROW);
1111 }
1112 else if(!handle->dragging)
1113 {
1114 gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);
1115 dt_control_change_cursor(GDK_LEFT_PTR);
1116 }
1117
1118 gtk_widget_queue_draw(widget);
1119 return TRUE;
1120}
1121
1122static gboolean _resize_handle_button(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
1123{
1125 if(event->button != 1) return TRUE;
1126
1127 if(event->type == GDK_BUTTON_PRESS)
1128 {
1129 handle->dragging = TRUE;
1130 handle->start_root = (handle->orientation == GTK_ORIENTATION_VERTICAL) ? event->y_root : event->x_root;
1131 handle->start_size = handle->get_size(handle->user_data);
1132 handle->current_size = handle->start_size;
1133 gtk_grab_add(widget);
1134 dt_control_change_cursor(handle->orientation == GTK_ORIENTATION_VERTICAL
1135 ? GDK_SB_V_DOUBLE_ARROW
1136 : GDK_SB_H_DOUBLE_ARROW);
1137 }
1138 else if(event->type == GDK_BUTTON_RELEASE)
1139 {
1140 handle->dragging = FALSE;
1141 gtk_grab_remove(widget);
1142 handle->current_size = handle->resize(handle->current_size, TRUE, handle->user_data);
1143
1144 GtkAllocation allocation;
1145 gtk_widget_get_allocation(widget, &allocation);
1146 const gboolean pointer_on_handle = event->x >= 0. && event->x <= allocation.width
1147 && event->y >= 0. && event->y <= allocation.height;
1148 if(pointer_on_handle)
1149 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
1150 else
1151 gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);
1152
1153 dt_control_change_cursor(pointer_on_handle
1154 ? (handle->orientation == GTK_ORIENTATION_VERTICAL
1155 ? GDK_SB_V_DOUBLE_ARROW
1156 : GDK_SB_H_DOUBLE_ARROW)
1157 : GDK_LEFT_PTR);
1158 gtk_widget_queue_draw(widget);
1159 }
1160
1161 return TRUE;
1162}
1163
1164static gboolean _resize_handle_motion(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
1165{
1167 if(!handle->dragging) return TRUE;
1168
1169 /* Each motion event is one sample in the GTK pointer stream. Convert only the
1170 * axis matching the resize direction and leave clamping/application to the
1171 * owner callback, which is the code owning the resized widget. */
1172 const double root = (handle->orientation == GTK_ORIENTATION_VERTICAL) ? event->y_root : event->x_root;
1173 double delta = root - handle->start_root;
1174 if(handle->invert) delta = -delta;
1175 const int requested_size = handle->start_size + (int)round(delta);
1176 handle->current_size = handle->resize(requested_size, FALSE, handle->user_data);
1177 return TRUE;
1178}
1179
1180GtkWidget *dt_bauhaus_resize_handle_new(GtkOrientation orientation, gboolean invert, const char *tooltip,
1182 dt_bauhaus_resize_handle_resize_f resize, gpointer user_data)
1183{
1184 // A GtkEventBox is enough: it owns a GdkWindow for pointer events and renders its CSS
1185 // background/border, so the whole hover affordance lives in the stylesheet (.resize-handle)
1186 // with no custom drawing. It is meant to be added as an overlay child on the resized widget.
1187 GtkWidget *handle_widget = gtk_event_box_new();
1188 dt_bauhaus_resize_handle_t *handle = g_malloc0(sizeof(*handle));
1189 handle->orientation = orientation;
1190 handle->invert = invert;
1191 handle->get_size = get_size;
1192 handle->resize = resize;
1193 handle->user_data = user_data;
1194
1195 // Pin the grip to the edge that grows the target when dragged outward, filling the other axis.
1196 // `invert` means the target grows in the negative direction, so the grip sits on the start edge.
1197 // The grab thickness is set here (a CSS min-height/width isn't honoured for an empty overlay
1198 // child -- GTK allocates its ~0px natural size); an edge class is added for hover styling.
1199 GtkStyleContext *ctx = gtk_widget_get_style_context(handle_widget);
1200 gtk_style_context_add_class(ctx, "resize-handle");
1201 if(orientation == GTK_ORIENTATION_VERTICAL)
1202 {
1203 gtk_widget_set_size_request(handle_widget, -1, DT_PIXEL_APPLY_DPI(5));
1204 gtk_widget_set_halign(handle_widget, GTK_ALIGN_FILL);
1205 gtk_widget_set_valign(handle_widget, invert ? GTK_ALIGN_START : GTK_ALIGN_END);
1206 gtk_style_context_add_class(ctx, invert ? "resize-handle-top" : "resize-handle-bottom");
1207 }
1208 else
1209 {
1210 gtk_widget_set_size_request(handle_widget, DT_PIXEL_APPLY_DPI(5), -1);
1211 gtk_widget_set_valign(handle_widget, GTK_ALIGN_FILL);
1212 gtk_widget_set_halign(handle_widget, invert ? GTK_ALIGN_START : GTK_ALIGN_END);
1213 gtk_style_context_add_class(ctx, invert ? "resize-handle-left" : "resize-handle-right");
1214 }
1215
1216 gtk_widget_set_events(handle_widget, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
1217 | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
1218 | GDK_POINTER_MOTION_MASK);
1219 if(!IS_NULL_PTR(tooltip))
1220 gtk_widget_set_tooltip_text(handle_widget, tooltip);
1221
1222 g_object_set_data_full(G_OBJECT(handle_widget), "dt-bauhaus-resize-handle", handle, g_free);
1223 g_signal_connect(G_OBJECT(handle_widget), "button-press-event", G_CALLBACK(_resize_handle_button), handle);
1224 g_signal_connect(G_OBJECT(handle_widget), "button-release-event", G_CALLBACK(_resize_handle_button), handle);
1225 g_signal_connect(G_OBJECT(handle_widget), "motion-notify-event", G_CALLBACK(_resize_handle_motion), handle);
1226 g_signal_connect(G_OBJECT(handle_widget), "enter-notify-event", G_CALLBACK(_resize_handle_cursor), handle);
1227 g_signal_connect(G_OBJECT(handle_widget), "leave-notify-event", G_CALLBACK(_resize_handle_cursor), handle);
1228
1229 return handle_widget;
1230}
1231
1232static void _widget_finalize(GObject *widget)
1233{
1234 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1235
1236 const guint focus_source = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(widget), DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY));
1237 if(focus_source > 0)
1238 {
1239 g_source_remove(focus_source);
1240 g_object_set_data(G_OBJECT(widget), DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY, NULL);
1241 g_object_set_data(G_OBJECT(widget), DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY, NULL);
1242 }
1243
1244 const char *accel_path = g_object_get_data(G_OBJECT(widget), "accel-path");
1246 dt_accels_remove_accel(darktable.gui->accels, accel_path, widget);
1247
1248 if(darktable.gui && darktable.gui->has_scroll_focus == GTK_WIDGET(w))
1250 dt_gui_throttle_cancel(widget);
1251 if(w->type == DT_BAUHAUS_SLIDER)
1252 {
1254 dt_free(d->grad_col);
1255 dt_free(d->grad_pos);
1256 }
1257 else
1258 {
1260 g_ptr_array_free(d->entries, TRUE);
1261 dt_free(d->text);
1262 }
1263 gtk_border_free(w->margin);
1264 gtk_border_free(w->padding);
1265
1266 G_OBJECT_CLASS(dt_bh_parent_class)->finalize(widget);
1267}
1268
1270{
1271 class->signals[DT_BAUHAUS_VALUE_CHANGED_SIGNAL]
1272 = g_signal_new("value-changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1273 g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1274 class->signals[DT_BAUHAUS_QUAD_PRESSED_SIGNAL]
1275 = g_signal_new("quad-pressed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1276 g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1277
1278 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class);
1279 widget_class->draw = _widget_draw;
1280 widget_class->scroll_event = _widget_scroll;
1281 widget_class->key_press_event = _widget_key_press;
1282 widget_class->get_preferred_width = _get_preferred_width;
1283 widget_class->get_preferred_height = _get_preferred_height;
1284 widget_class->enter_notify_event = _enter_leave;
1285 widget_class->leave_notify_event = _enter_leave;
1286 widget_class->style_updated = _style_updated;
1287 G_OBJECT_CLASS(class)->finalize = _widget_finalize;
1288}
1289
1291{
1292 bauhaus->line_height = 16;
1293 bauhaus->marker_size = 0.25f;
1294
1295 GtkWidget *root_window = dt_ui_main_window(darktable.gui->ui);
1296 GtkStyleContext *ctx = gtk_widget_get_style_context(root_window);
1297 gtk_style_context_set_screen(ctx, gtk_widget_get_screen(root_window));
1298
1299 gtk_style_context_lookup_color(ctx, "bauhaus_fg", &bauhaus->color_fg);
1300 gtk_style_context_lookup_color(ctx, "bauhaus_fg_insensitive", &bauhaus->color_fg_insensitive);
1301 gtk_style_context_lookup_color(ctx, "bauhaus_bg", &bauhaus->color_bg);
1302 gtk_style_context_lookup_color(ctx, "bauhaus_border", &bauhaus->color_border);
1303 gtk_style_context_lookup_color(ctx, "bauhaus_fill", &bauhaus->color_fill);
1304 gtk_style_context_lookup_color(ctx, "bauhaus_indicator_border", &bauhaus->indicator_border);
1305 if(!gtk_style_context_lookup_color(ctx, "bauhaus_value", &bauhaus->color_value))
1307 if(!gtk_style_context_lookup_color(ctx, "bauhaus_value_insensitive", &bauhaus->color_value_insensitive))
1309 if(!gtk_style_context_lookup_color(ctx, "bauhaus_value_text", &bauhaus->color_value_text))
1311 if(!gtk_style_context_lookup_color(ctx, "bauhaus_value_text_insensitive", &bauhaus->color_value_text_insensitive))
1313
1314 gtk_style_context_lookup_color(ctx, "graph_bg", &bauhaus->graph_bg);
1315 gtk_style_context_lookup_color(ctx, "graph_exterior", &bauhaus->graph_exterior);
1316 gtk_style_context_lookup_color(ctx, "graph_border", &bauhaus->graph_border);
1317 gtk_style_context_lookup_color(ctx, "graph_grid", &bauhaus->graph_grid);
1318 gtk_style_context_lookup_color(ctx, "graph_fg", &bauhaus->graph_fg);
1319 gtk_style_context_lookup_color(ctx, "graph_fg_active", &bauhaus->graph_fg_active);
1320 gtk_style_context_lookup_color(ctx, "graph_overlay", &bauhaus->graph_overlay);
1321 if(!gtk_style_context_lookup_color(ctx, "graph_scope_restricted", &bauhaus->graph_scope_restricted))
1322 bauhaus->graph_scope_restricted = bauhaus->graph_fg; // For the "restricted" text in the scope
1323 gtk_style_context_lookup_color(ctx, "inset_histogram", &bauhaus->inset_histogram);
1324 gtk_style_context_lookup_color(ctx, "graph_red", &bauhaus->graph_colors[0]);
1325 gtk_style_context_lookup_color(ctx, "graph_green", &bauhaus->graph_colors[1]);
1326 gtk_style_context_lookup_color(ctx, "graph_blue", &bauhaus->graph_colors[2]);
1327 gtk_style_context_lookup_color(ctx, "colorlabel_red",
1329 gtk_style_context_lookup_color(ctx, "colorlabel_yellow",
1331 gtk_style_context_lookup_color(ctx, "colorlabel_green",
1333 gtk_style_context_lookup_color(ctx, "colorlabel_blue",
1335 gtk_style_context_lookup_color(ctx, "colorlabel_purple",
1337
1338
1339 // make sure we release previously loaded font
1341 pango_font_description_free(bauhaus->pango_font_desc);
1342
1343 // Create a scratch Cairo surface to draw on
1344 cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 128, 128);
1345 cairo_t *cr = cairo_create(cst);
1346
1347 // Let Gtk create a fake label and grab its font description
1348 GtkWidget *ref = gtk_label_new(NULL);
1349 ctx = gtk_widget_get_style_context(ref);
1350 gtk_style_context_get(ctx, GTK_STATE_FLAG_NORMAL, "font", &bauhaus->pango_font_desc, NULL);
1351
1352 PangoContext *context = gtk_widget_get_pango_context(ref);
1353 PangoLayout *layout = pango_layout_new(context);
1354 pango_layout_set_text(layout, "em", -1);
1355 pango_layout_set_font_description(layout, bauhaus->pango_font_desc);
1356 //double dpi = pango_cairo_context_get_resolution(context);
1357 //double scale = gtk_widget_get_scale_factor(ref);
1358 //pango_cairo_context_set_resolution(context, dpi * scale);
1359
1360 // Get advanced font options (system anti-aliasing/hinting/subpixel/kerning)
1362
1363 // Render
1364 pango_cairo_update_layout(cr, layout);
1365 pango_cairo_show_layout(cr, layout);
1366
1367 // Measure the text line height
1368 int pango_width;
1369 int pango_height;
1370 pango_layout_get_size(layout, &pango_width, &pango_height);
1371 g_object_unref(layout);
1372 cairo_destroy(cr);
1373 cairo_surface_destroy(cst);
1374
1375 bauhaus->line_height = pango_height / PANGO_SCALE;
1377
1378 bauhaus->baseline_size = 5; // absolute size in Cairo unit
1379 bauhaus->border_width = 1; // absolute size in Cairo unit
1380 bauhaus->marker_size = pango_height / PANGO_SCALE * 0.6;
1381}
1382
1384{
1385 dt_bauhaus_t * bauhaus = (dt_bauhaus_t *)calloc(1, sizeof(dt_bauhaus_t));
1386 bauhaus->keys_cnt = 0;
1387 bauhaus->current = NULL;
1388 bauhaus->popup_area = gtk_drawing_area_new();
1389 bauhaus->pango_font_desc = NULL;
1390
1392
1393 // this easily gets keyboard input:
1394 // bauhaus->popup_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1395 // but this doesn't flicker, and the above hack with key input seems to work well.
1396 bauhaus->popup_window = gtk_window_new(GTK_WINDOW_POPUP);
1397#ifdef GDK_WINDOWING_QUARTZ
1399#endif
1400 // this is needed for popup, not for toplevel.
1401 // since popup_area gets the focus if we show the window, this is all
1402 // we need.
1403
1404 gtk_window_set_resizable(GTK_WINDOW(bauhaus->popup_window), FALSE);
1405 gtk_window_set_default_size(GTK_WINDOW(bauhaus->popup_window), 30, 30);
1406 gtk_window_set_modal(GTK_WINDOW(bauhaus->popup_window), TRUE);
1407
1408 // Needed for Wayland and Sway :
1409 gtk_window_set_transient_for(GTK_WINDOW(bauhaus->popup_window),
1410 GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
1411
1412 gtk_window_set_decorated(GTK_WINDOW(bauhaus->popup_window), FALSE);
1413 gtk_window_set_attached_to(GTK_WINDOW(bauhaus->popup_window), NULL);
1414
1415 // needed on macOS to avoid fullscreening the popup with newer GTK
1416 gtk_window_set_type_hint(GTK_WINDOW(bauhaus->popup_window), GDK_WINDOW_TYPE_HINT_POPUP_MENU);
1417
1418 gtk_container_add(GTK_CONTAINER(bauhaus->popup_window), bauhaus->popup_area);
1419 gtk_widget_set_hexpand(bauhaus->popup_area, TRUE);
1420 gtk_widget_set_vexpand(bauhaus->popup_area, TRUE);
1421 gtk_window_set_keep_above(GTK_WINDOW(bauhaus->popup_window), TRUE);
1422 gtk_window_set_gravity(GTK_WINDOW(bauhaus->popup_window), GDK_GRAVITY_STATIC);
1423
1424 gtk_widget_set_can_focus(bauhaus->popup_area, TRUE);
1425 gtk_widget_add_events(bauhaus->popup_area, GDK_POINTER_MOTION_MASK
1426 | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
1427 | GDK_KEY_PRESS_MASK | GDK_LEAVE_NOTIFY_MASK
1429
1430 GObject *window = G_OBJECT(bauhaus->popup_window);
1431 GObject *area = G_OBJECT(bauhaus->popup_area);
1432 g_object_set_data(area, "bauhaus", bauhaus);
1433
1434 g_signal_connect(window, "show", G_CALLBACK(dt_bauhaus_window_show), area);
1435 g_signal_connect(area, "draw", G_CALLBACK(dt_bauhaus_popup_draw), NULL);
1436 g_signal_connect(area, "motion-notify-event", G_CALLBACK(dt_bauhaus_popup_motion_notify), NULL);
1437 g_signal_connect(area, "leave-notify-event", G_CALLBACK(dt_bauhaus_popup_leave_notify), NULL);
1438 g_signal_connect(area, "button-press-event", G_CALLBACK(dt_bauhaus_popup_button_press), NULL);
1439 g_signal_connect(area, "button-release-event", G_CALLBACK (dt_bauhaus_popup_button_release), NULL);
1440 g_signal_connect(area, "key-press-event", G_CALLBACK(dt_bauhaus_popup_key_press), NULL);
1441 g_signal_connect(area, "scroll-event", G_CALLBACK(dt_bauhaus_popup_scroll), NULL);
1442
1443 // Keys used by key-pressed event handler when the Bauhaus widget has the focus
1444 gchar *path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Increase value (normal step)"));
1446 path, NULL, GDK_KEY_Right, 0);
1447 dt_free(path);
1448 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Decrease value (normal step)"));
1450 path, NULL, GDK_KEY_Left, 0);
1451 dt_free(path);
1452 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Increase value (fine step)"));
1454 path, NULL, GDK_KEY_Right, GDK_CONTROL_MASK);
1455 dt_free(path);
1456 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Decrease value (fine step)"));
1458 path, NULL, GDK_KEY_Left, GDK_CONTROL_MASK);
1459 dt_free(path);
1460 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Increase value (coarse step)"));
1462 path, NULL, GDK_KEY_Right, GDK_SHIFT_MASK);
1463 dt_free(path);
1464 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Decrease value (coarse step)"));
1466 path, NULL, GDK_KEY_Left, GDK_SHIFT_MASK);
1467 dt_free(path);
1468 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Toggle color-picker"));
1470 path, NULL, GDK_KEY_Insert, 0);
1471 dt_free(path);
1472
1473 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Open editing mode"));
1475 path, NULL, GDK_KEY_Return, 0);
1476 dt_free(path);
1477 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Exit editing mode"));
1479 path, NULL, GDK_KEY_Escape, 0);
1480 dt_free(path);
1481 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Select previous (in editing mode)"));
1483 path, NULL, GDK_KEY_Up, 0);
1484 dt_free(path);
1485 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Select next (in editing mode)"));
1487 path, NULL, GDK_KEY_Down, 0);
1488 dt_free(path);
1489 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Validate result (in editing mode)"));
1491 path, NULL, GDK_KEY_Return, 0);
1492 dt_free(path);
1493 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Toggle color-picker"));
1495 path, NULL, GDK_KEY_Insert, 0);
1496 dt_free(path);
1497
1498 return bauhaus;
1499}
1500
1504
1505// fwd declare a few callbacks
1506static gboolean dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
1507static gboolean dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
1508static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data);
1509
1510
1511static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
1512
1513// end static init/cleanup
1514// =================================================
1515
1516
1517
1518// common initialization
1520{
1521 w->module = self;
1522 w->field = NULL;
1523
1524 w->no_accels = FALSE;
1525 w->no_module_list = FALSE;
1526 w->bauhaus = bauhaus;
1528
1529 // no quad icon and no toggle button:
1530 w->quad_paint = 0;
1531 w->quad_paint_data = NULL;
1532 w->quad_toggle = 0;
1533 w->show_quad = TRUE;
1534 w->expand = TRUE;
1535
1536 gtk_widget_add_events(GTK_WIDGET(w), GDK_POINTER_MOTION_MASK
1537 | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
1538 | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
1539 | GDK_FOCUS_CHANGE_MASK
1541
1542 gtk_widget_set_can_focus(GTK_WIDGET(w), TRUE);
1543 gtk_widget_set_halign(GTK_WIDGET(w), GTK_ALIGN_FILL);
1544 gtk_widget_set_hexpand(GTK_WIDGET(w), TRUE);
1545 g_signal_connect(G_OBJECT(w), "focus-in-event", G_CALLBACK(dt_bauhaus_focus_in_callback), NULL);
1546 g_signal_connect(G_OBJECT(w), "focus-out-event", G_CALLBACK(dt_bauhaus_focus_out_callback), NULL);
1547 g_signal_connect(G_OBJECT(w), "focus", G_CALLBACK(dt_bauhaus_focus_callback), NULL);
1548
1549 dt_gui_add_class(GTK_WIDGET(w), "dt_bauhaus");
1550}
1551
1553{
1554 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1556 d->defpos = def;
1557}
1558
1559
1561{
1562 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1564 float current_position = dt_bauhaus_slider_get(widget);
1565 float desired_position = _bh_round_to_n_digits(w, val);
1566 d->min = MAX(d->min, d->hard_min);
1567 d->soft_min = MAX(d->soft_min, d->hard_min);
1568
1569 if(desired_position > d->hard_max)
1571
1572 if(current_position < desired_position)
1573 dt_bauhaus_slider_set(widget, desired_position);
1574 // else nothing : old position is the new position, just the bound changes
1575}
1576
1578{
1579 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1581 return d->hard_min;
1582}
1583
1585{
1586 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1588 float current_position = dt_bauhaus_slider_get(widget);
1589 float desired_position = _bh_round_to_n_digits(w, val);
1590 d->hard_max = desired_position;
1591 d->max = MIN(d->max, d->hard_max);
1592 d->soft_max = MIN(d->soft_max, d->hard_max);
1593
1594 if(desired_position < d->hard_min)
1595 dt_bauhaus_slider_set_hard_min(widget, desired_position);
1596
1597 if(current_position > desired_position)
1598 dt_bauhaus_slider_set(widget, desired_position);
1599 // else nothing : old position is the new position, just the bound changes
1600}
1601
1603{
1604 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1606 return d->hard_max;
1607}
1608
1610{
1611 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1613 float oldval = dt_bauhaus_slider_get(widget);
1614 d->min = d->soft_min = CLAMP(val, d->hard_min, d->hard_max);
1615 // a bound change is not a user edit: re-apply the value without raising
1617}
1618
1620{
1621 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1623 return d->soft_min;
1624}
1625
1627{
1628 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1630 float oldval = dt_bauhaus_slider_get(widget);
1631 d->max = d->soft_max = CLAMP(val, d->hard_min, d->hard_max);
1632 // a bound change is not a user edit: re-apply the value without raising
1634}
1635
1637{
1638 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1640 return d->soft_max;
1641}
1642
1644{
1645 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1647 d->defpos = def;
1648}
1649
1650void dt_bauhaus_slider_set_soft_range(GtkWidget *widget, float soft_min, float soft_max)
1651{
1652 dt_bauhaus_slider_set_soft_min(widget, soft_min);
1653 dt_bauhaus_slider_set_soft_max(widget, soft_max);
1654}
1655
1657{
1658 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1659 if(label)
1660 {
1661 g_strlcpy(w->label, label, sizeof(w->label));
1663 }
1664
1665 if(w->module)
1666 {
1667 // Widgets auto-set by params introspection need to be added to the list of stuff to auto-update
1668 dt_gui_module_t *m = w->module;
1669 if(m && !w->no_module_list)
1670 m->widget_list = g_list_append(m->widget_list, w);
1671
1672 if(m && w->field && !w->no_module_list)
1673 m->widget_list_bh = g_list_append(m->widget_list_bh, w);
1674
1675 // Wire the focusing action
1676 // Note: once the focus is grabbed, interaction with the widget happens through arrow keys or mouse wheel.
1677 // No need to wire all possible events/interactions.
1678 if(m && !w->no_accels && !w->module->deprecated && label)
1679 {
1680 // slash is not allowed in control names because that makes accel pathes fail
1681 assert(g_strrstr(label, "/") == NULL);
1682
1683 gchar *plugin_name = g_strdup_printf("%s/%s", m->name, w->label);
1684 dt_capitalize_label(plugin_name);
1685
1686 gchar *scope = g_strdup_printf("%s/Modules", m->view);
1687 dt_accels_new_darkroom_action(_action_request_focus, widget, scope, plugin_name, 0, 0, _("Focuses the control"));
1688 g_object_set_data_full(G_OBJECT(widget), "accel-path",
1689 dt_accels_build_path("Darkroom/Modules", plugin_name), dt_free_gpointer);
1690 gtk_widget_set_has_tooltip(widget, TRUE);
1691 dt_free(scope);
1692 dt_free(plugin_name);
1693 }
1694
1695 gtk_widget_queue_draw(GTK_WIDGET(w));
1696 }
1697}
1698
1700{
1701 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1702 return w->label;
1703}
1704
1705void dt_bauhaus_widget_set_quad_paint(GtkWidget *widget, dt_bauhaus_quad_paint_f f, int paint_flags, void *paint_data)
1706{
1707 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1708 w->quad_paint = f;
1709 w->quad_paint_flags = paint_flags;
1710 w->quad_paint_data = paint_data;
1711}
1712
1714{
1715 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1716 if(*w->label)
1717 fprintf(stderr, "[dt_bauhaus_widget_set_field] bauhaus label '%s' set before field (needs to be after)\n", w->label);
1718 w->field = field;
1720}
1721
1722// make this quad a toggle button:
1724{
1725 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1726 w->quad_toggle = toggle;
1727}
1728
1730{
1731 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1732 if (active)
1734 else
1735 w->quad_paint_flags &= ~CPF_ACTIVE;
1736 gtk_widget_queue_draw(GTK_WIDGET(w));
1737}
1738
1739void dt_bauhaus_widget_set_quad_visibility(GtkWidget *widget, const gboolean visible)
1740{
1741 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1742 w->show_quad = visible;
1743 gtk_widget_queue_draw(GTK_WIDGET(w));
1744}
1745
1747{
1748 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1749 return (w->quad_paint_flags & CPF_ACTIVE) == CPF_ACTIVE;
1750}
1751
1753{
1754 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1755 if(w->quad_toggle)
1756 {
1758 }
1759 else
1761
1762 g_signal_emit_by_name(G_OBJECT(w), "quad-pressed");
1763}
1764
1766{
1767 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1768 gtk_widget_grab_focus(widget);
1769
1770 if(!w->quad_toggle)
1771 {
1773 w->quad_paint_flags &= ~CPF_ACTIVE;
1774 gtk_widget_queue_draw(GTK_WIDGET(w));
1775 }
1776}
1777
1779{
1780 return dt_bauhaus_slider_new_with_range(bh, self, 0.0, 1.0, 0.1, 0.5, 3);
1781}
1782
1784 float defval, int digits)
1785{
1786 return dt_bauhaus_slider_new_with_range_and_feedback(bh, self, min, max, step, defval, digits, 1);
1787}
1788
1790 float step, float defval, int digits, int feedback)
1791{
1792 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
1793 return dt_bauhaus_slider_from_widget(bh, w,self, min, max, step, defval, digits, feedback);
1794}
1795
1796static void _style_updated(GtkWidget *widget)
1797{
1798 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
1800
1801 // gtk_widget_set_size_request is the minimal preferred, size.
1802 // it NEEDS to be defined and will be contextually adapted, possibly overriden by CSS.
1803 // Thing is Gtk CSS min-width in combination with hexpand is wonky so this is how it should be done.
1804 if(w->type == DT_BAUHAUS_COMBOBOX)
1805 gtk_widget_set_size_request(widget, -1, _get_combobox_height(widget));
1806 else if(w->type == DT_BAUHAUS_SLIDER)
1807 gtk_widget_set_size_request(widget, -1, _get_slider_height(widget));
1808}
1809
1811 float step, float defval, int digits, int feedback)
1812{
1814 _bauhaus_widget_init(bh, w, self);
1816 d->min = d->soft_min = d->hard_min = min;
1817 d->max = d->soft_max = d->hard_max = max;
1818 d->step = step;
1819 // normalize default:
1820 d->defpos = defval;
1821 d->pos = (defval - min) / (max - min);
1822 d->oldpos = d->pos;
1823 d->digits = digits;
1824 d->format = "";
1825 d->factor = 1.0f;
1826 d->offset = 0.0f;
1827
1828 d->grad_cnt = 0;
1829 d->grad_col = NULL;
1830 d->grad_pos = NULL;
1831
1832 d->fill_feedback = feedback;
1833
1834 d->is_dragging = 0;
1835
1836 dt_gui_add_class(GTK_WIDGET(w), "bauhaus_slider");
1837
1838 g_signal_connect(G_OBJECT(w), "button-press-event", G_CALLBACK(dt_bauhaus_slider_button_press), NULL);
1839 g_signal_connect(G_OBJECT(w), "button-release-event", G_CALLBACK(dt_bauhaus_slider_button_release), NULL);
1840 g_signal_connect(G_OBJECT(w), "motion-notify-event", G_CALLBACK(dt_bauhaus_slider_motion_notify), NULL);
1841
1842 return GTK_WIDGET(w);
1843}
1844
1846{
1847 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
1849 return GTK_WIDGET(w);
1850}
1851
1853 int pos, GtkCallback callback, gpointer data, const char **texts)
1854{
1855 GtkWidget *combo = dt_bauhaus_combobox_new(bh, self);
1857 dt_bauhaus_combobox_add_list(combo, texts);
1858 dt_bauhaus_combobox_set(combo, pos);
1859 gtk_widget_set_tooltip_text(combo, tip ? tip : _(label));
1860 if(callback) g_signal_connect(G_OBJECT(combo), "value-changed", G_CALLBACK(callback), data);
1861
1862 return combo;
1863}
1864
1865static void _combobox_conf_value_changed(GtkWidget *widget, gpointer user_data)
1866{
1867 const char *value = (const char *)dt_bauhaus_combobox_get_data(widget);
1868 if(value) dt_conf_set_string((const char *)user_data, value);
1869}
1870
1872{
1873 if(dt_confgen_type(confkey) != DT_ENUM || !dt_confgen_value_exists(confkey, DT_VALUES))
1874 {
1875 fprintf(stderr, "[dt_bauhaus_combobox_from_conf] `%s` is not declared as an <enum> config entry\n", confkey);
1876 return NULL;
1877 }
1878
1879 GtkWidget *combo = dt_bauhaus_combobox_new(bh, self);
1881
1882 const char *tooltip = dt_confgen_get_tooltip(confkey);
1883 gtk_widget_set_tooltip_text(combo, (tooltip && *tooltip) ? _(tooltip) : _(dt_confgen_get_label(confkey)));
1884
1885 gchar *current = dt_conf_get_string(confkey);
1886 const char *values = dt_confgen_get(confkey, DT_VALUES);
1887 GList *options = dt_util_str_to_glist("][", values);
1888
1889 int pos = 0, active = 0;
1890 for(GList *opt = options; opt; opt = g_list_next(opt))
1891 {
1892 char *item = (char *)opt->data;
1893 // strip the leading '[' of the first entry and the trailing ']' of the last one
1894 if(item[0] == '[') item++;
1895 else if(item[strlen(item) - 1] == ']') item[strlen(item) - 1] = '\0';
1896
1897 dt_bauhaus_combobox_add_full(combo, g_dpgettext2(NULL, "preferences", item), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT,
1898 g_strdup(item), dt_free_gpointer, TRUE);
1899
1900 if(!g_strcmp0(current, item)) active = pos;
1901 pos++;
1902 }
1903 g_list_free_full(options, dt_free_gpointer);
1904 dt_free(current);
1905
1906 // Select the entry matching the current config value before connecting the signal,
1907 // so the initial sync doesn't bounce back through dt_conf_set_string().
1908 dt_bauhaus_combobox_set(combo, active);
1909
1910 g_signal_connect(G_OBJECT(combo), "value-changed", G_CALLBACK(_combobox_conf_value_changed), (gpointer)confkey);
1911
1912 return combo;
1913}
1914
1916{
1918 _bauhaus_widget_init(bh, w, self);
1920 d->entries = g_ptr_array_new_full(4, free_combobox_entry);
1921 d->defpos = 0;
1922 d->active = -1;
1923 d->editable = 0;
1924 d->text_align = DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT;
1925 d->entries_ellipsis = PANGO_ELLIPSIZE_END;
1926 d->populate = NULL;
1927 d->text = NULL;
1928
1929 dt_gui_add_class(GTK_WIDGET(w), "bauhaus_combobox");
1930
1931 g_signal_connect(G_OBJECT(w), "button-press-event", G_CALLBACK(dt_bauhaus_combobox_button_press), NULL);
1932}
1933
1935{
1936 if(IS_NULL_PTR(widget)) return NULL;
1937 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1938 if(w->type != DT_BAUHAUS_COMBOBOX) return NULL;
1940
1941 if(d->active >= d->entries->len) d->active = -1;
1942
1943 return d;
1944}
1945
1953{
1954 int count = 0;
1955 for(int i = 0; !IS_NULL_PTR(d) && i < d->entries->len; i++)
1956 {
1957 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
1958 if(!entry->is_separator) count++;
1959 }
1960 return count;
1961}
1962
1970 const gboolean allow_end)
1971{
1972 if(IS_NULL_PTR(d) || pos < 0) return -1;
1973
1974 int selectable_pos = 0;
1975 for(int i = 0; i < d->entries->len; i++)
1976 {
1977 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
1978 if(entry->is_separator) continue;
1979 if(selectable_pos == pos) return i;
1980 selectable_pos++;
1981 }
1982
1983 return (allow_end && selectable_pos == pos) ? (int)d->entries->len : -1;
1984}
1985
1989static int _combobox_entry_pos_to_public(const dt_bauhaus_combobox_data_t *d, const int entry_pos)
1990{
1991 if(IS_NULL_PTR(d) || entry_pos < 0 || entry_pos >= d->entries->len) return -1;
1992
1993 int selectable_pos = 0;
1994 for(int i = 0; i <= entry_pos; i++)
1995 {
1996 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
1997 if(entry->is_separator) continue;
1998 if(i == entry_pos) return selectable_pos;
1999 selectable_pos++;
2000 }
2001
2002 return -1;
2003}
2004
2005void dt_bauhaus_combobox_add_populate_fct(GtkWidget *widget, void (*fct)(GtkWidget *w, void *module))
2006{
2007 if(IS_NULL_PTR(widget)) return;
2008 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2009 if(w->type == DT_BAUHAUS_COMBOBOX)
2010 w->data.combobox.populate = fct;
2011}
2012
2013void dt_bauhaus_combobox_add_list(GtkWidget *widget, const char **texts)
2014{
2015 while(texts && *texts)
2016 dt_bauhaus_combobox_add_full(widget, _(*(texts++)), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT, NULL, NULL, TRUE);
2017}
2018
2019void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
2020{
2022}
2023
2024void dt_bauhaus_combobox_add_with_tooltip(GtkWidget *widget, const char *text, const char *tooltip)
2025{
2026 if(IS_NULL_PTR(widget)) return;
2027 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2028 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2032 g_ptr_array_add(d->entries, entry);
2033 if(d->active < 0) d->active = (int)d->entries->len - 1;
2034}
2035
2037{
2038 dt_bauhaus_combobox_add_full(widget, text, align, NULL, NULL, TRUE);
2039}
2040
2042 gpointer data, void (free_func)(void *data), gboolean sensitive)
2043{
2044 if(IS_NULL_PTR(widget)) return;
2045 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2046 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2048 dt_bauhaus_combobox_entry_t *entry = new_combobox_entry(text, NULL, align, sensitive, data, free_func);
2049 g_ptr_array_add(d->entries, entry);
2050 if(d->active < 0) d->active = (int)d->entries->len - 1;
2051}
2052
2057
2058void dt_bauhaus_combobox_add_separator_with_height(GtkWidget *widget, const float row_height_factor)
2059{
2060 if(IS_NULL_PTR(widget)) return;
2061 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2062 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2064 g_ptr_array_add(d->entries, new_combobox_separator(row_height_factor));
2065}
2066
2067void dt_bauhaus_combobox_set_entries_ellipsis(GtkWidget *widget, PangoEllipsizeMode ellipis)
2068{
2069 if(IS_NULL_PTR(widget)) return;
2070 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2071 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2073 d->entries_ellipsis = ellipis;
2074}
2075
2076
2078{
2079 if(IS_NULL_PTR(widget)) return;
2080 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2081 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2083 d->editable = editable ? 1 : 0;
2084 if(d->editable && IS_NULL_PTR(d->text))
2085 d->text = calloc(1, DT_BAUHAUS_COMBO_MAX_TEXT);
2086}
2087
2089{
2091
2092 return d ? d->editable : 0;
2093}
2094
2096{
2097 if(IS_NULL_PTR(widget)) return;
2098 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2099 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2101 d->text_align = text_align;
2102}
2103
2105{
2107 if(IS_NULL_PTR(d)) return;
2108 const int entry_pos = _combobox_public_to_entry_pos(d, pos, FALSE);
2109
2110 if(entry_pos < 0) return;
2111
2112 // move active position up if removing anything before it
2113 // or when removing last position that is currently active.
2114 // this also sets active to -1 when removing the last remaining entry in a combobox.
2115 if(d->active > entry_pos || d->active == d->entries->len-1)
2116 d->active--;
2117
2118 g_ptr_array_remove_index(d->entries, entry_pos);
2119}
2120
2121void dt_bauhaus_combobox_insert(GtkWidget *widget, const char *text,int pos)
2122{
2124}
2125
2127 gpointer data, void (*free_func)(void *), int pos)
2128{
2129 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2130 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2132 const int entry_pos = _combobox_public_to_entry_pos(d, pos, TRUE);
2133 if(entry_pos < 0) return;
2134 g_ptr_array_insert(d->entries, entry_pos, new_combobox_entry(text, NULL, align, TRUE, data, free_func));
2135 if(d->active < 0)
2136 d->active = entry_pos;
2137 else if(entry_pos <= d->active)
2138 d->active++;
2139}
2140
2145
2146void dt_bauhaus_combobox_insert_separator_with_height(GtkWidget *widget, int pos, const float row_height_factor)
2147{
2148 if(IS_NULL_PTR(widget)) return;
2149 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2150 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2152 const int entry_pos = _combobox_public_to_entry_pos(d, pos, TRUE);
2153 if(entry_pos < 0) return;
2154 g_ptr_array_insert(d->entries, entry_pos, new_combobox_separator(row_height_factor));
2155 if(entry_pos <= d->active) d->active++;
2156}
2157
2159{
2161
2163}
2164
2166{
2168 if(IS_NULL_PTR(d)) return NULL;
2169
2170 if(d->active < 0)
2171 {
2172 return d->editable ? d->text : NULL;
2173 }
2174 else
2175 {
2176 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, d->active);
2177 if(entry->is_separator) return NULL;
2178 return entry->label;
2179 }
2180}
2181
2183{
2185 if(IS_NULL_PTR(d) || d->active < 0) return NULL;
2186
2187 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, d->active);
2188 if(entry->is_separator) return NULL;
2189 return entry->data;
2190}
2191
2193{
2194 if(IS_NULL_PTR(widget)) return;
2195 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2196 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2198 d->active = -1;
2199 g_ptr_array_set_size(d->entries, 0);
2200}
2201
2202const char *dt_bauhaus_combobox_get_entry(GtkWidget *widget, int pos)
2203{
2205 const int entry_pos = _combobox_public_to_entry_pos(d, pos, FALSE);
2206 if(entry_pos < 0) return NULL;
2207
2208 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, entry_pos);
2209 return entry->label;
2210}
2211
2212void dt_bauhaus_combobox_set_text(GtkWidget *widget, const char *text)
2213{
2215 if(IS_NULL_PTR(d) || !d->editable) return;
2216
2217 g_strlcpy(d->text, text, DT_BAUHAUS_COMBO_MAX_TEXT);
2218}
2219
2220
2221static void _delayed_combobox_commit(gpointer data)
2222{
2223 // Commit combobox value change to pipeline history, handling a safety timout
2224 // so incremental scrollings don't trigger a recompute at every scroll step.
2225 struct dt_bauhaus_widget_t *w = data;
2226
2227 // If a reset started after the timeout was scheduled (e.g. while reloading history,
2228 // applying a style, etc.), don't commit anything to history from this stale callback.
2229 if(darktable.gui && dt_gui_widgets_suppressed()) return;
2230
2232 {
2234 w->bauhaus->default_value_changed_callback(GTK_WIDGET(w));
2235 else
2236 fprintf(stderr, "ERROR: %s - %s is set to use default callback but none is provided\n", w->module->name,
2237 w->label);
2238 }
2239 else
2240 {
2241 if(w->module)
2242 fprintf(stderr, "WARNING: %s - %s has an IOP module but doesn't use default callback\n", w->module->name,
2243 w->label);
2244 }
2245
2246 // We need te emit this signal inconditionnaly
2247 g_signal_emit_by_name(G_OBJECT(w), "value-changed");
2248}
2249
2259void _combobox_set(GtkWidget *widget, const int pos, gboolean timeout)
2260{
2261 if(IS_NULL_PTR(widget)) return;
2262 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2264 const int old_pos = d->active;
2265 const int new_pos = (d->entries) ? CLAMP(pos, -1, (int)d->entries->len - 1)
2266 : -1;
2267 if(new_pos >= 0)
2268 {
2269 // if the new position is a separator, don't change the active position but still redraw to update the visual selection
2270 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, new_pos);
2271 if(entry->is_separator) return;
2272 }
2273
2274 // When updating programmatically (GUI reset), ensure no delayed commit from a
2275 // previous user interaction survives, even if the value doesn't change.
2277
2278 if(old_pos != new_pos)
2279 {
2280 d->active = new_pos;
2281
2282 if(w->bauhaus->current == w)
2283 gtk_widget_queue_draw(w->bauhaus->popup_area);
2284
2285 gtk_widget_queue_draw(GTK_WIDGET(w));
2286
2287 // If a delayed commit is pending from a previous user interaction, cancel it.
2288 // This is especially important when updating widgets programmatically (during GUI reset),
2289 // as we don't want stale timeouts to later emit "value-changed" and commit to history.
2291 {
2292 if(timeout)
2294 else
2295 {
2296 dt_gui_throttle_cancel(widget);
2298 }
2299 }
2300 }
2301}
2302
2303// Public API function, called from GUI init and update
2304void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
2305{
2307 const int selectable_count = _combobox_selectable_count(d);
2308 const int public_pos = (selectable_count > 0) ? CLAMP(pos, -1, selectable_count - 1) : -1;
2309 const int entry_pos = _combobox_public_to_entry_pos(d, public_pos, FALSE);
2310 _combobox_set(widget, entry_pos, FALSE);
2311}
2312
2313
2314gboolean dt_bauhaus_combobox_set_from_text(GtkWidget *widget, const char *text)
2315{
2316 if(IS_NULL_PTR(text)) return FALSE;
2317
2319
2320 for(int i = 0; d && i < d->entries->len; i++)
2321 {
2322 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
2323 if(entry->is_separator) continue;
2324 if(!g_strcmp0(entry->label, text))
2325 {
2326 _combobox_set(widget, i, FALSE);
2327 return TRUE;
2328 }
2329 }
2330 return FALSE;
2331}
2332
2334{
2336
2337 for(int i = 0; d && i < d->entries->len; i++)
2338 {
2339 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
2340 if(entry->is_separator) continue;
2341 if(GPOINTER_TO_INT(entry->data) == value)
2342 {
2343 _combobox_set(widget, i, FALSE);
2344 return TRUE;
2345 }
2346 }
2347 return FALSE;
2348}
2349
2351{
2353
2354 return _combobox_entry_pos_to_public(d, d ? d->active : -1);
2355}
2356
2357void dt_bauhaus_combobox_entry_set_sensitive(GtkWidget *widget, int pos, gboolean sensitive)
2358{
2360 const int entry_pos = _combobox_public_to_entry_pos(d, pos, FALSE);
2361 if(entry_pos < 0) return;
2362
2363 dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)g_ptr_array_index(d->entries, entry_pos);
2364 entry->sensitive = sensitive;
2365}
2366
2368{
2369 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2370 if(w->type != DT_BAUHAUS_SLIDER) return;
2372 d->grad_cnt = 0;
2373}
2374
2375void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, float b)
2376{
2377 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2378 if(w->type != DT_BAUHAUS_SLIDER) return;
2380
2381 if(IS_NULL_PTR(d->grad_col))
2382 {
2383 d->grad_col = malloc(DT_BAUHAUS_SLIDER_MAX_STOPS * sizeof(*d->grad_col));
2384 d->grad_pos = malloc(DT_BAUHAUS_SLIDER_MAX_STOPS * sizeof(*d->grad_pos));
2385 }
2386 // need to replace stop?
2387 for(int k = 0; k < d->grad_cnt; k++)
2388 {
2389 if(d->grad_pos[k] == stop)
2390 {
2391 d->grad_col[k][0] = r;
2392 d->grad_col[k][1] = g;
2393 d->grad_col[k][2] = b;
2394 return;
2395 }
2396 }
2397 // new stop:
2398 if(d->grad_cnt < DT_BAUHAUS_SLIDER_MAX_STOPS)
2399 {
2400 int k = d->grad_cnt++;
2401 d->grad_pos[k] = stop;
2402 d->grad_col[k][0] = r;
2403 d->grad_col[k][1] = g;
2404 d->grad_col[k][2] = b;
2405 }
2406 else
2407 {
2408 fprintf(stderr, "[bauhaus_slider_set_stop] only %d stops allowed.\n", DT_BAUHAUS_SLIDER_MAX_STOPS);
2409 }
2410}
2411
2412static void dt_bauhaus_draw_indicator(struct dt_bauhaus_widget_t *w, float pos, cairo_t *cr, float wd)
2413{
2414 // draw scale indicator (the tiny triangle)
2415 const float size = w->bauhaus->marker_size;
2416 const float vertical_position = _get_indicator_y_position(w);
2417 const float horizontal_position = pos * wd;
2418
2419 cairo_save(cr);
2420 cairo_translate(cr, horizontal_position, vertical_position);
2421
2423 const gboolean has_colored_background = d->grad_cnt > 0;
2424
2425 if(!has_colored_background && d->fill_feedback)
2426 {
2427 // Plain indicator (regular sliders)
2428 cairo_arc(cr, 0, 0, size / 2., 0, M_PI * 2);
2429 set_color(cr, w->bauhaus->color_fg);
2430 cairo_set_line_width(cr, 0);
2431 cairo_fill(cr);
2432 }
2433 else
2434 {
2435 // Hollow indicator to keep the slider background visible through the marker.
2436 const float border = (size - w->bauhaus->baseline_size) / 2.;
2437 set_color(cr, w->bauhaus->color_fg);
2438 cairo_set_line_width(cr, border);
2439 const float radius = size / 2. - border / 2.;
2440 cairo_arc(cr, 0, 0, radius, 0, M_PI * 2);
2441 cairo_stroke(cr);
2442 }
2443 cairo_restore(cr);
2444}
2445
2446static void dt_bauhaus_draw_quad(struct dt_bauhaus_widget_t *w, cairo_t *cr, const double x, const double y)
2447{
2448 if(!w->show_quad) return;
2449
2450 cairo_save(cr);
2451 if(w->quad_paint)
2452 {
2453 // draw color picker
2454 w->quad_paint(cr, x, y,
2455 w->bauhaus->quad_width, // width
2456 w->bauhaus->quad_width, // height
2458 }
2459 else if(w->type == DT_BAUHAUS_COMBOBOX)
2460 {
2461 // draw combobox chevron
2462 // Inset the chevron from the right edge of the (now flush) quad box so it
2463 // doesn't crowd or overflow the widget border. The quad box itself is placed
2464 // identically to the slider's, so this keeps the chevron in line with slider
2465 // quad icons when widgets are stacked.
2466 // FIXME: Why do we need an extra half-pixel shift to the left for pixel-correctness ?
2467 cairo_translate(cr, x + w->bauhaus->quad_width / 2. - 0.5, y + w->bauhaus->line_height / 2.);
2468 const float r = w->bauhaus->quad_width * .2f;
2469 cairo_move_to(cr, -r, -r * .5f);
2470 cairo_line_to(cr, 0, r * .5f);
2471 cairo_line_to(cr, r, -r * .5f);
2472 cairo_stroke(cr);
2473 }
2474 cairo_restore(cr);
2475}
2476
2484static void dt_bauhaus_draw_baseline(struct dt_bauhaus_widget_t *w, cairo_t *cr, float width)
2485{
2486 // draw line for orientation in slider
2487 cairo_save(cr);
2489 const gboolean has_colored_background = d->grad_cnt > 0;
2490 const float baseline_top = w->bauhaus->line_height + INNER_PADDING;
2491 const float baseline_height = w->bauhaus->baseline_size;
2492 const gboolean is_sensitive = gtk_widget_is_sensitive(GTK_WIDGET(w));
2493
2494 // the background of the line
2495 // Note: to ensure the indicator is not clipped on the left side,
2496 // we push the whole slider of an indicator radius to the right.
2497 // Issue is that leaves a dent on the baseline, so the trick
2498 // is to make it overlap back by a radius on the left.
2499 const double x_origin = -w->bauhaus->marker_size / 3.;
2500 // The left edge was pushed by x_origin to overlap the indicator's leftmost
2501 // resting position. Extend the width by the same amount so the right edge
2502 // stays at `width` (flush with the right-aligned numeric value) instead of
2503 // retreating by |x_origin| and letting the value overhang the baseline.
2504 const double baseline_width = width - x_origin;
2505 // Make sure we use integer coordinates to limit anti-aliasing blur
2506 cairo_rectangle(cr, round(x_origin), round(baseline_top),
2507 round(baseline_width), round(baseline_height));
2508
2509 cairo_pattern_t *gradient = NULL;
2510 if(has_colored_background && is_sensitive)
2511 {
2512 // gradient line as used in some modules for hue, saturation, lightness
2513 const double zoom = (d->max - d->min) / (d->hard_max - d->hard_min);
2514 const double offset = (d->min - d->hard_min) / (d->hard_max - d->hard_min);
2515 gradient = cairo_pattern_create_linear(x_origin, 0, width, baseline_height);
2516 for(int k = 0; k < d->grad_cnt; k++)
2517 cairo_pattern_add_color_stop_rgba(gradient, (d->grad_pos[k] - offset) / zoom,
2518 d->grad_col[k][0], d->grad_col[k][1], d->grad_col[k][2], 0.4f);
2519 cairo_set_source(cr, gradient);
2520 }
2521 else
2522 {
2523 // regular baseline
2524 set_color(cr, w->bauhaus->color_bg);
2525 }
2526
2527 cairo_fill(cr);
2528
2529
2530 if(w->bauhaus->border_width > 0)
2531 {
2532 // Draw the border on top
2533 // We need to recess the coordinates by half a line-width for perfect border matching
2534 const double line_width = 1.;
2535 cairo_rectangle(cr, round(x_origin) + line_width / 2., round(baseline_top) + line_width / 2.,
2536 round(baseline_width) - line_width, round(baseline_height) - line_width);
2537 cairo_set_line_width(cr, line_width);
2539 cairo_stroke(cr);
2540 }
2541
2542 if(gradient) cairo_pattern_destroy(gradient);
2543
2544 // get the reference of the slider aka the position of the 0 value
2545 float origin = fmaxf(fminf((d->factor > 0 ? -d->min - d->offset/d->factor
2546 : d->max + d->offset/d->factor)
2547 / (d->max - d->min),
2548 1.0f) * width,
2549 0.f);
2550
2551 // have a `fill ratio feel' from zero to current position
2552 if(!has_colored_background && d->fill_feedback && is_sensitive)
2553 {
2554 set_color(cr, w->bauhaus->color_value);
2555
2556 const float position = d->pos * width;
2557 const float delta = position - origin;
2558
2559 // origin = 0 means start on left, but our left is shifted to the right,
2560 // so offset it. (see x_origin)
2561 if(origin == 0.f)
2562 cairo_rectangle(cr, x_origin, baseline_top, delta - x_origin, baseline_height);
2563 else
2564 cairo_rectangle(cr, origin, baseline_top, delta, baseline_height);
2565
2566 cairo_fill(cr);
2567 }
2568
2569 // draw the 0 reference graduation if it's different than the bounds of the slider
2570 // If the max of the slider is 360, it is likely an absolute hue slider in degrees
2571 // a zero in periodic stuff has not much meaning so we skip it.
2572 if(d->hard_max != 360.0f && is_sensitive)
2573 {
2574 set_color(cr, w->bauhaus->color_fg);
2575
2576 // If we are to put the 0 reference on the "left", this time we need to account for
2577 // actual leftmost resting position of the indicator, not the starting point of the baseline.
2578 cairo_arc(cr, origin, baseline_top + w->bauhaus->marker_size, w->bauhaus->border_width / 2., 0, 2 * M_PI);
2579 cairo_fill(cr);
2580 }
2581
2582 cairo_restore(cr);
2583}
2584
2586{
2587 switch(w->type)
2588 {
2590 break;
2591 case DT_BAUHAUS_SLIDER:
2592 {
2595 }
2596 break;
2597 default:
2598 break;
2599 }
2600}
2601
2602static void dt_bauhaus_widget_accept(struct dt_bauhaus_widget_t *w, gboolean timeout)
2603{
2604 GtkWidget *widget = GTK_WIDGET(w);
2605
2606 switch(w->type)
2607 {
2609 {
2611
2612 if(d->editable && w->bauhaus->keys_cnt > 0)
2613 {
2614 // combobox is editable and we have text, assume it is a custom input
2615 memset(d->text, 0, DT_BAUHAUS_COMBO_MAX_TEXT);
2616 g_strlcpy(d->text, w->bauhaus->keys, DT_BAUHAUS_COMBO_MAX_TEXT);
2617 _combobox_set(widget, -1, timeout); // select custom entry
2618
2619#if DEBUG
2620 fprintf(stdout, "combobox went the custom path\n");
2621#endif
2622 }
2623 else
2624 {
2625 if(w->bauhaus->keys_cnt > 0)
2626 {
2627 // combobox is not editable, but we have text. Assume user wanted to init a selection from keyboard.
2628 // find the closest match by looking for the entry having the maximum number
2629 // of characters in common with the user input.
2630 gchar *keys = g_utf8_casefold(w->bauhaus->keys, -1);
2631 int match = -1;
2632 int matches = 0;
2633
2634 for(int j = 0; j < d->entries->len; j++)
2635 {
2636 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, j);
2637 gchar *text_cmp = g_utf8_casefold(entry->label, -1);
2638 if(entry->sensitive && !strncmp(text_cmp, keys, w->bauhaus->keys_cnt))
2639 {
2640 matches++;
2641 match = j;
2642 }
2643 dt_free(text_cmp);
2644 }
2645 dt_free(keys);
2646
2647 // Accept result only if exactly one match was found. Anything else is ambiguous
2648 if(matches == 1)
2649 _combobox_set(widget, match, timeout);
2650 }
2651 else {
2652 // Active entry (below cursor or scrolled)
2653 _combobox_set(widget, d->hovered, timeout);
2654 }
2655 }
2656
2657 break;
2658 }
2659 case DT_BAUHAUS_SLIDER:
2660 {
2661 // The slider popup uses the quadratic magnifier for accurate setting.
2662 // We need extra conversions from cursor coordinates to set it right.
2663 // This needs to be kept in sync with popup_draw()
2665
2666 // This is needed to accept the change.
2667 // d->pos is soft-updated with corrected coordinates for drawing purposes only in popup_redraw().
2668 // We need to reset it to the original value temporarily, and request a proper setting
2669 // with value-changed signal through dt_bauhaus_slider_set_normalized()
2670 const float value = d->pos;
2671 d->pos = d->oldpos;
2673 break;
2674 }
2675 default:
2676 break;
2677 }
2678}
2679
2680static gchar *_build_label(const struct dt_bauhaus_widget_t *w)
2681{
2682 return g_strdup(w->label);
2683}
2684
2685static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
2686{
2687 // Popups belong to the app, not to the bauhaus widget. That's confusing.
2688 // Also the *widget param here is the popup container, still not the bauhaus widget. Even more confusing.
2689 // This is the actual parent bauhaus widget :
2690 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
2692
2693 // get area properties
2694 GtkAllocation allocation;
2695 gtk_widget_get_allocation(widget, &allocation);
2696 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
2697 cairo_t *cr = cairo_create(cst);
2698 GtkStyleContext *context = gtk_widget_get_style_context(widget);
2699
2700 // look up some colors once
2701 GdkRGBA text_color, text_color_selected, text_color_hover, text_color_insensitive, text_color_focused;
2702 gtk_style_context_get_color(context, GTK_STATE_FLAG_NORMAL, &text_color);
2703 gtk_style_context_get_color(context, GTK_STATE_FLAG_SELECTED, &text_color_selected);
2704 gtk_style_context_get_color(context, GTK_STATE_FLAG_PRELIGHT, &text_color_hover);
2705 gtk_style_context_get_color(context, GTK_STATE_FLAG_INSENSITIVE, &text_color_insensitive);
2706 gtk_style_context_get_color(context, GTK_STATE_FLAG_FOCUSED, &text_color_focused);
2707
2708 GdkRGBA *fg_color = default_color_assign();
2709 GdkRGBA *bg_color;
2710 GtkStateFlags state = gtk_widget_get_state_flags(widget);
2711
2712 gtk_style_context_get(context, state, "background-color", &bg_color, NULL);
2713 gtk_style_context_get_color(context, state, fg_color);
2714
2715 // draw background
2716 gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height);
2717 gtk_render_frame(context, cr, 0, 0, allocation.width, allocation.height);
2718
2719 const double main_height = _widget_get_main_height(w, widget);
2720 double total_width = 0.f;
2721 const double main_width = _widget_get_main_width(w, NULL, &total_width);
2722
2723 // translate to account for the widget spacing
2724 cairo_translate(cr, w->padding->left, w->padding->top);
2725
2726 // switch on bauhaus widget type (so we only need one static window)
2727 switch(w->type)
2728 {
2729 case DT_BAUHAUS_SLIDER:
2730 {
2732 const double slider_cursor_radius = 0.5 * w->bauhaus->marker_size;
2733 const double text_width = main_width + slider_cursor_radius;
2734 cairo_save(cr);
2735 cairo_translate(cr, slider_cursor_radius, 0.0);
2736 set_color(cr, *fg_color);
2737
2738 float scale = _bh_slider_get_scale(w);
2739 const int num_scales = 1.f / scale;
2740 const float bottom_baseline = _get_slider_bar_height(w);
2741
2742 for(int k = 0; k < num_scales; k++)
2743 {
2744 const float off = k * scale - d->oldpos;
2745 GdkRGBA fg_copy = *fg_color;
2746 fg_copy.alpha = scale / fabsf(off);
2747 set_color(cr, fg_copy);
2748 draw_slider_line(cr, d->oldpos, off, scale, main_width, main_height, bottom_baseline, 1);
2749 cairo_stroke(cr);
2750 }
2751 cairo_restore(cr);
2752
2753 // Get the x offset compared to d->oldpos accounting for vertical position magnification
2754 const double mouse_off = d->pos - d->oldpos;
2755
2756 cairo_save(cr);
2757 cairo_translate(cr, slider_cursor_radius, 0.0);
2758
2759 // Draw the baseline with fill feedback if any (needs the new d->pos set before)
2760 dt_bauhaus_draw_baseline(w, cr, main_width);
2761
2762 // draw mouse over indicator line
2764 draw_slider_line(cr, d->oldpos, mouse_off, scale, main_width, main_height, bottom_baseline, 2);
2765 cairo_stroke(cr);
2766
2767 // draw indicator
2768 dt_bauhaus_draw_indicator(w, d->pos, cr, main_width);
2769
2770 cairo_restore(cr);
2771
2772 // draw numerical value:
2773 cairo_save(cr);
2775
2776 float value_width = 0.f;
2777 char *text = dt_bauhaus_slider_get_text(GTK_WIDGET(w), dt_bauhaus_slider_get(GTK_WIDGET(w)));
2778 GdkRectangle bounding_value = { .x = 0.,
2779 .y = 0.,
2780 .width = text_width,
2781 .height = w->bauhaus->line_height };
2782 // Display user keyboard input if any, otherwise the current value
2783 show_pango_text(w, context, cr, &bounding_value,
2784 (w->bauhaus->keys_cnt) ? w->bauhaus->keys : text, BH_ALIGN_RIGHT,
2785 BH_ALIGN_MIDDLE, PANGO_ELLIPSIZE_NONE, NULL, &value_width, NULL, GTK_STATE_FLAG_NORMAL);
2786 dt_free(text);
2787
2788 // label on top of marker:
2789 gchar *label_text = _build_label(w);
2790 const float label_width = text_width - value_width - INNER_PADDING;
2791 GdkRectangle bounding_label = { .x = 0.,
2792 .y = 0.,
2793 .width = label_width,
2794 .height = w->bauhaus->line_height };
2795 set_color(cr, *fg_color);
2796 show_pango_text(w, context, cr, &bounding_label, label_text, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE,
2797 PANGO_ELLIPSIZE_END, NULL, NULL, NULL, GTK_STATE_FLAG_NORMAL);
2798 dt_free(label_text);
2799
2800 cairo_restore(cr);
2801 }
2802 break;
2804 {
2806
2807 // User keyboard input goes first
2808 double popup_y = 0.;
2809 if(w->bauhaus->keys_cnt > 0)
2810 {
2811 cairo_save(cr);
2812 set_color(cr, text_color_focused);
2813 const double query_height = _bh_get_row_height(w);
2814 GdkRectangle query_label = { .x = 0.,
2815 .y = popup_y,
2816 .width = main_width,
2817 .height = query_height };
2818 show_pango_text(w, context, cr, &query_label, w->bauhaus->keys, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE,
2819 PANGO_ELLIPSIZE_NONE, NULL, NULL, NULL, GTK_STATE_FLAG_NORMAL);
2820 popup_y += query_height;
2821 cairo_restore(cr);
2822 }
2823
2824 cairo_save(cr);
2825 gchar *keys = g_utf8_casefold(w->bauhaus->keys, -1);
2826 for(int j = 0; j < d->entries->len; j++)
2827 {
2828 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, j);
2829 const double entry_height = _bh_get_combobox_entry_height(w, entry);
2830 if(entry->is_separator)
2831 {
2832 if(w->bauhaus->keys_cnt == 0)
2833 {
2834 cairo_set_source_rgba(cr, 0.5, 0.5, 0.5, 1.);
2835 cairo_move_to(cr, 0., popup_y + 0.5 * entry_height);
2836 cairo_line_to(cr, main_width, popup_y + 0.5 * entry_height);
2837 cairo_set_line_width(cr, 1.);
2838 cairo_stroke(cr);
2839 }
2840 popup_y += entry_height;
2841 continue;
2842 }
2843
2844 gchar *text_cmp = g_utf8_casefold(entry->label, -1);
2845 if(!strncmp(text_cmp, keys, w->bauhaus->keys_cnt))
2846 {
2847 // If user typed some keys, display matching entries only
2848
2849 // The GTK state flag is applied to the whole widget,
2850 // we need to dispatch it individually to each entry
2851 if(!entry->sensitive)
2852 {
2853 set_color(cr, text_color_insensitive);
2854 state = GTK_STATE_FLAG_INSENSITIVE;
2855 }
2856 else if(j == d->active)
2857 {
2858 set_color(cr, text_color_selected);
2859 state = GTK_STATE_FLAG_SELECTED;
2860 }
2861 else if(j == d->hovered)
2862 {
2863 set_color(cr, text_color_hover);
2864 state = GTK_STATE_FLAG_PRELIGHT;
2865 }
2866 else
2867 {
2868 set_color(cr, text_color);
2869 state = GTK_STATE_FLAG_NORMAL;
2870 }
2871
2872 GdkRectangle bounding_label = { .x = 0.,
2873 .y = popup_y,
2874 .width = main_width,
2875 .height = entry_height };
2876#if DEBUG
2877 cairo_rectangle(cr, bounding_label.x, bounding_label.y, bounding_label.width, bounding_label.height);
2878 cairo_set_line_width(cr, 2);
2879 cairo_stroke(cr);
2880#endif
2881 show_pango_text(w, context, cr, &bounding_label, entry->label, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE,
2882 d->entries_ellipsis, bg_color, NULL, NULL, state);
2883 }
2884
2885 dt_free(text_cmp);
2886 popup_y += entry_height;
2887 }
2888 cairo_restore(cr);
2889 dt_free(keys);
2890 }
2891 break;
2892 default:
2893 // yell
2894 break;
2895 }
2896
2897 cairo_destroy(cr);
2898 cairo_set_source_surface(crf, cst, 0, 0);
2899 cairo_paint(crf);
2900 cairo_surface_destroy(cst);
2901
2902 gdk_rgba_free(bg_color);
2903 gdk_rgba_free(fg_color);
2904
2905 return TRUE;
2906}
2907
2908
2909// Get the maximum width of a full combobox without ellipsization
2911{
2912 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2914 GtkStyleContext *context = gtk_widget_get_style_context(widget);
2915 const GtkStateFlags state = gtk_widget_get_state_flags(widget);
2916
2917 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 999, 999);
2918 cairo_t *cr = cairo_create(cst);
2919
2920 float width = 0.f;
2921
2922 // Get chevron width + padding if any
2923 if(w->show_quad)
2925
2926 float label_width = 0.f;
2927 GdkRectangle bounding_label = { .x = 0.,
2928 .y = 0.,
2929 .width = 999,
2930 .height = 999 };
2931
2932 show_pango_text(w, context, cr, &bounding_label, w->label, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE,
2933 PANGO_ELLIPSIZE_NONE, NULL, &label_width, NULL, state);
2934
2935 if(label_width > 0.f) width += label_width + INNER_PADDING;
2936
2937 // Get width of the longest entry
2938 float max_entry = 0.f;
2939 for(int i = 0; i < d->entries->len; i++)
2940 {
2941 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
2942
2943 // The value is shown right-aligned, ellipsized if needed.
2944 GdkRectangle bounding_value = { .x = 0,
2945 .y = 0.,
2946 .width = 999,
2947 .height = 999 };
2948 float entry_label_width = 0.f;
2949
2950 show_pango_text(w, context, cr, &bounding_value, entry->label, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE, PANGO_ELLIPSIZE_NONE, NULL,
2951 &entry_label_width, NULL, state);
2952
2953 if(entry_label_width + INNER_PADDING > max_entry) max_entry = entry_label_width + INNER_PADDING;
2954 }
2955
2956 width += max_entry;
2957 width += w->margin->left + w->margin->right + w->padding->left + w->padding->right;
2958
2959 cairo_destroy(cr);
2960 cairo_surface_destroy(cst);
2961
2962 return width;
2963}
2964
2965static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf)
2966{
2967 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2968 GtkStyleContext *context = gtk_widget_get_style_context(widget);
2969
2970 // Get current Gtk allocation
2971 GtkAllocation allocation;
2972 gtk_widget_get_allocation(widget, &allocation);
2973
2974 if(w->type == DT_BAUHAUS_COMBOBOX)
2975 allocation.height = _get_combobox_height(widget);
2976 else if(w->type == DT_BAUHAUS_SLIDER)
2977 allocation.height = _get_slider_height(widget);
2978
2979 if(w->type == DT_BAUHAUS_COMBOBOX && !w->expand)
2980 {
2981 // For comboboxes that are not set to hexpand, limit the width span to what's needed
2982 // to display the internal text, aka prevent them to grow out of proportions
2983 float max_width = _get_combobox_max_width(widget);
2984 if(max_width < allocation.width)
2985 allocation.width = ceilf(max_width);
2986 }
2987
2988 // Force allocate to our requirements. Yes, it's ugly.
2989 gtk_widget_size_allocate(widget, &allocation);
2990
2991 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
2992 cairo_t *cr = cairo_create(cst);
2993
2994 GdkRGBA *text_color = default_color_assign();
2995 GdkRGBA *value_color = default_color_assign();
2996 GdkRGBA *value_text_color = default_color_assign();
2997 GtkStateFlags state = gtk_widget_get_state_flags(widget);
2998 if(gtk_widget_has_focus(widget))
2999 state |= GTK_STATE_FLAG_FOCUSED;
3000
3001 gtk_style_context_save(context);
3002 gtk_style_context_set_state(context, state);
3003 gtk_style_context_get_color(context, state, text_color);
3004
3005 *value_color = gtk_widget_is_sensitive(widget) ? w->bauhaus->color_value : w->bauhaus->color_value_insensitive;
3006 *value_text_color = gtk_widget_is_sensitive(widget) ? w->bauhaus->color_value_text : w->bauhaus->color_value_text_insensitive;
3007
3008 // Compute internal widget sizes, that is allocation minus margins
3010 // Note : box is the border box in CSS conventions
3011 const float box_width = allocation.width - w->margin->left - w->margin->right;
3012 const float box_height = allocation.height - w->margin->top - w->margin->bottom;
3013
3014 // Paint background and borders first
3015 gtk_render_background(context, crf, w->margin->left, w->margin->top, box_width, box_height);
3016 gtk_render_frame(context, crf, w->margin->left, w->margin->top, box_width, box_height);
3017
3018 // Translate Cairo coordinates to account for the widget spacing
3019 cairo_translate(cr,
3020 w->margin->left + w->padding->left,
3021 w->margin->top + w->padding->top);
3022
3023 const float available_width = _widget_get_main_width(w, NULL, NULL);
3024 const float inner_height = _widget_get_main_height(w, NULL);
3025
3026 // draw type specific content:
3027 cairo_save(cr);
3028 set_color(cr, *text_color);
3029 cairo_set_line_width(cr, 1.0);
3030 switch(w->type)
3031 {
3033 {
3034 // draw label and quad area at right end
3035 // Gap must match the one reserved by _widget_get_main_width() (2*INNER_PADDING)
3036 // and used by the slider, so combobox quads stay flush and vertically aligned
3037 // with slider quad icons when stacked.
3038 if(w->show_quad)
3039 dt_bauhaus_draw_quad(w, cr, available_width + 2 * INNER_PADDING, 0.);
3040
3042 const PangoEllipsizeMode combo_ellipsis = d->entries_ellipsis;
3043
3044 float label_width = 0.f;
3045 float label_height = 0.f;
3046
3047 GdkRectangle bounding_label = { .x = 0.,
3048 .y = 0.,
3049 .width = available_width,
3050 .height = inner_height };
3051 set_color(cr, *text_color);
3052 show_pango_text(w, context, cr, &bounding_label, w->label, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE,
3053 combo_ellipsis, NULL, &label_width, &label_height, state);
3054
3055 // The value is shown right-aligned, ellipsized if needed.
3056 gchar *text = d->text;
3057 if(d->active >= 0 && d->active < d->entries->len)
3058 {
3059 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, d->active);
3060 text = entry->label;
3061 }
3062 GdkRectangle bounding_value = { .x = label_width + INNER_PADDING,
3063 .y = 0.,
3064 .width = available_width - label_width - INNER_PADDING,
3065 .height = inner_height };
3066 set_color(cr, *value_text_color);
3067 show_pango_text(w, context, cr, &bounding_value, text, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE, combo_ellipsis, NULL,
3068 NULL, NULL, state);
3069
3070 break;
3071 }
3072 case DT_BAUHAUS_SLIDER:
3073 {
3074 const double slider_cursor_radius = 0.5 * w->bauhaus->marker_size;
3075 const double text_width = available_width + slider_cursor_radius;
3076
3077 // line for orientation
3078 cairo_save(cr);
3079 cairo_translate(cr, slider_cursor_radius, 0.0);
3080 dt_bauhaus_draw_baseline(w, cr, available_width);
3081 cairo_restore(cr);
3082
3083 // Paint the non-active quad icon with some transparency, because
3084 // icons are bolder than the neighbouring text and appear brighter.
3085 cairo_save(cr);
3086 if(!(w->quad_paint_flags & CPF_ACTIVE))
3087 cairo_set_source_rgba(cr, text_color->red, text_color->green, text_color->blue, text_color->alpha * 0.7);
3088
3089 dt_bauhaus_draw_quad(w, cr, text_width + 2. * INNER_PADDING, 0.);
3090 cairo_restore(cr);
3091
3092 float value_width = 0;
3093 if(gtk_widget_is_sensitive(widget))
3094 {
3095 cairo_save(cr);
3096 cairo_translate(cr, slider_cursor_radius, 0.0);
3097 dt_bauhaus_draw_indicator(w, w->data.slider.pos, cr, available_width);
3098 cairo_restore(cr);
3099
3100 char *text = dt_bauhaus_slider_get_text(widget, dt_bauhaus_slider_get(widget));
3101 GdkRectangle bounding_value = { .x = 0.,
3102 .y = 0.,
3103 .width = text_width,
3104 .height = w->bauhaus->line_height };
3105 set_color(cr, *value_text_color);
3106 show_pango_text(w, context, cr, &bounding_value, text, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE,
3107 PANGO_ELLIPSIZE_NONE, NULL, &value_width, NULL, state);
3108 dt_free(text);
3109 }
3110
3111 // label on top of marker:
3112 gchar *label_text = _build_label(w);
3113 const float label_width = text_width - value_width - INNER_PADDING;
3114 GdkRectangle bounding_label = { .x = 0.,
3115 .y = 0.,
3116 .width = label_width,
3117 .height = w->bauhaus->line_height };
3118 set_color(cr, *text_color);
3119 show_pango_text(w, context, cr, &bounding_label, label_text, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE,
3120 PANGO_ELLIPSIZE_END, NULL, NULL, NULL, state);
3121 dt_free(label_text);
3122 }
3123 break;
3124 default:
3125 break;
3126 }
3127 cairo_restore(cr);
3128 cairo_destroy(cr);
3129 cairo_set_source_surface(crf, cst, 0, 0);
3130 cairo_paint(crf);
3131 cairo_surface_destroy(cst);
3132 gtk_style_context_restore(context);
3133
3134 gdk_rgba_free(text_color);
3135 gdk_rgba_free(value_color);
3136 gdk_rgba_free(value_text_color);
3137
3138 return TRUE;
3139}
3140
3141static void _get_preferred_width(GtkWidget *widget, gint *minimum_size, gint *natural_size)
3142{
3143 // Nothing clever here : preferred size is the size of the container.
3144 // If user is not happy with that, it's his responsibility to resize sidebars.
3145 // Minimum size is left at 0 so the widget can shrink with its parent;
3146 // leaving it unset would read as uninitialized stack garbage and could
3147 // clamp natural_size up to a stale value, preventing the widget from
3148 // growing again when the parent grows.
3149 *minimum_size = 0;
3150
3155 else
3156 *natural_size = DT_PIXEL_APPLY_DPI(300);
3157}
3158
3159static void _get_preferred_height(GtkWidget *widget, gint *minimum_size, gint *natural_size)
3160{
3161 // Resolve the height from the same getters used everywhere else so it is
3162 // always correct at measure time, regardless of when "style-updated" fires.
3163 // Relying solely on the size-request set in _style_updated is not enough:
3164 // when the widget lives inside a freshly popped-up GtkMenu, the menu measures
3165 // its children before their style context is resolved, so it would size
3166 // itself from a stale/zero height and end up showing scroll arrows.
3167 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3169 const gint height = (w->type == DT_BAUHAUS_COMBOBOX) ? (gint)ceil(_get_combobox_height(widget))
3170 : (gint)ceil(_get_slider_height(widget));
3171 *minimum_size = height;
3172 *natural_size = height;
3173}
3174
3176{
3177 if(bh->current)
3178 {
3179 gtk_grab_remove(bh->popup_area);
3180 gtk_widget_hide(bh->popup_window);
3181 gtk_window_set_attached_to(GTK_WINDOW(bh->popup_window), NULL);
3182
3183 // Give back focus to the attached widget
3184 gtk_widget_grab_focus(GTK_WIDGET(bh->current));
3185 darktable.gui->has_scroll_focus = GTK_WIDGET(bh->current);
3186
3187 bh->current = NULL;
3188 }
3189}
3190
3192{
3193 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3195 w->bauhaus->current = w;
3196 w->bauhaus->keys_cnt = 0;
3197 memset(w->bauhaus->keys, 0, sizeof(w->bauhaus->keys));
3198 w->bauhaus->change_active = 0;
3199 w->bauhaus->mouse_line_distance = 0.0f;
3200 w->bauhaus->hiding = FALSE;
3201
3202 // Make sure all relevant widgets exist
3203 gtk_widget_realize(w->bauhaus->popup_window);
3204 gtk_widget_realize(widget);
3206
3207 GtkAllocation tmp;
3208 gtk_widget_get_allocation(widget, &tmp);
3209 int width = MAX(tmp.width, 1);
3210 int height = MAX(tmp.height, 1);
3211
3212 switch(w->bauhaus->current->type)
3213 {
3214 case DT_BAUHAUS_SLIDER:
3215 {
3216 // Slider popup: make it square
3218 d->oldpos = d->pos;
3219 d->is_dragging = FALSE;
3220 height = tmp.width;
3221 break;
3222 }
3224 {
3226 break;
3227 }
3228 default:
3229 {
3230 fprintf(stderr, "[dt_bauhaus_show_popup] The bauhaus widget has an unknown type\n");
3231 break;
3232 }
3233 }
3234
3235 /* Bind to CSS rules from parent widget */
3236 GtkStyleContext *context = gtk_widget_get_style_context(w->bauhaus->popup_area);
3237 gtk_style_context_add_class(context, "dt_bauhaus_popup");
3238 gtk_window_set_attached_to(GTK_WINDOW(w->bauhaus->popup_window), widget);
3239 #ifdef GDK_WINDOWING_WAYLAND
3240 if (GDK_IS_WAYLAND_DISPLAY(gdk_display_get_default())) {
3241 GtkWidget *toplevel = gtk_widget_get_toplevel(widget);
3242 gtk_window_set_transient_for(GTK_WINDOW(w->bauhaus->popup_window), GTK_WINDOW(toplevel));
3243 }
3244 #endif
3245
3246 // The popup window stays transient for the main application window, so the anchor
3247 // rectangle needs to remain expressed in that coordinate space even when the popup is
3248 // logically attached to an enclosing popover on Wayland.
3249 gint wx = 0, wy = 0;
3250 GdkWindow *widget_window = gtk_widget_get_window(widget);
3251 if(widget_window) gdk_window_get_origin(widget_window, &wx, &wy);
3252 wx += w->margin->left;
3253 wy += w->margin->top;
3254
3255 gint wwx = 0, wwy = 0;
3256 GdkWindow *main_window = gtk_widget_get_window(dt_ui_main_window(darktable.gui->ui));
3257 if(main_window) gdk_window_get_origin(main_window, &wwx, &wwy);
3258
3259 GdkRectangle anchor = {
3260 .x = wx - wwx,
3261 .y = wy - wwy,
3262 .width = MAX(tmp.width, 1),
3263 .height = MAX(tmp.height, 1)
3264 };
3265
3266 // Set desired size, but it's more a guide than a rule.
3267 gtk_widget_set_size_request(w->bauhaus->popup_area, width, height);
3268 gtk_widget_set_size_request(w->bauhaus->popup_window, width, height);
3269
3270 // Need to call resize to actually change something
3271 gtk_window_resize(GTK_WINDOW(w->bauhaus->popup_window), width, height);
3272
3273 GdkWindow *window = gtk_widget_get_window(w->bauhaus->popup_window);
3274 if(IS_NULL_PTR(window))
3275 {
3276 gtk_widget_show_all(w->bauhaus->popup_window);
3277 gtk_widget_grab_focus(w->bauhaus->popup_area);
3278 return;
3279 }
3280
3281 // For Wayland (and supposed to work on X11 too) and Gtk 3.24 this is how you do it
3282 gdk_window_move_to_rect(GDK_WINDOW(window), &anchor, GDK_GRAVITY_STATIC, GDK_GRAVITY_STATIC,
3283 GDK_ANCHOR_SLIDE, 0, 0);
3284
3285 gtk_widget_show_all(w->bauhaus->popup_window);
3286 gtk_widget_grab_focus(w->bauhaus->popup_area);
3287}
3288
3289static void _slider_add_step(GtkWidget *widget, float delta, guint state)
3290{
3291 if(delta == 0.f) return;
3292 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3294
3296 if(dt_modifier_is(state, GDK_CONTROL_MASK)) delta /= 5.f;
3297 else if(dt_modifier_is(state, GDK_SHIFT_MASK)) delta *= 5.f;
3298
3299 // Ensure the requested delta is at least visible given current number of digits in display
3300 const float min_visible = 1.f / (fabsf(d->factor) * ipow(10, d->digits));
3301 if(fabsf(delta) < min_visible)
3302 delta = copysignf(min_visible, delta);
3303
3304 const float value = dt_bauhaus_slider_get(widget);
3306}
3307
3308static gboolean _widget_scroll(GtkWidget *widget, GdkEventScroll *event)
3309{
3310 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3311 const gboolean popup_captured = (w->bauhaus->current == w);
3312 const gboolean scroll_captured = (darktable.gui->has_scroll_focus == widget);
3313 const gboolean key_captured = gtk_widget_has_focus(widget);
3314 const gboolean smooth = (event->direction == GDK_SCROLL_SMOOTH);
3315
3316 // - native Gtk focus (keyboard), that takes precedence and records all keyboard events,
3317 // - custom scroll focus (mouse wheel), that should not overlap with vertical scrolling.
3318 // Scroll focus is a subset of Gtk focus, aka we can lose scroll focus while we have Gtk focus,
3319 // but delayed history commits may transiently steal Gtk focus while the
3320 // pointer never leaves the widget. In that case, keep trusting the explicit
3321 // scroll focus until leave-notify releases it.
3322 // We also extend widget focus with the popup window focus if it is captured
3323 // by the current widget.
3324 if(!key_captured && !popup_captured && !scroll_captured)
3325 return FALSE;
3326
3327 // Keep ownership of the whole touchpad gesture sequence.
3328 darktable.gui->has_scroll_focus = widget;
3329
3330 int delta_x = 0;
3331 int delta_y = 0;
3332
3333 if(!dt_gui_get_scroll_unit_deltas(event, &delta_x, &delta_y))
3334 {
3335 // Important: for touchpad scroll, sub-unit deltas and stop events must not
3336 // propagate to the parent scrolled window, otherwise the same gesture gets
3337 // re-accumulated there.
3338 return smooth;
3339 }
3340
3341 const gboolean vscroll = delta_y != 0 && abs(delta_y) > abs(delta_x);
3342 const gboolean hscroll = delta_x != 0 && abs(delta_x) > abs(delta_y);
3343
3344 if(w->type == DT_BAUHAUS_SLIDER)
3345 {
3346 if(hscroll)
3347 {
3348 // inconditionnaly record horizontal scroll on slider
3349 _slider_add_step(widget, delta_x, event->state);
3350 return TRUE;
3351 }
3352 else if(vscroll && darktable.gui->has_scroll_focus)
3353 {
3354 // convert vertical scrolling to horizontal only if we have the scroll focus
3355 _slider_add_step(widget, -delta_y, event->state);
3356 return TRUE;
3357 }
3358
3359 // Diagonal smooth gestures: consume them if captured, so they do not leak.
3360 return smooth;
3361 }
3362 else
3363 {
3364 if(vscroll && darktable.gui->has_scroll_focus)
3365 {
3366 _combobox_next_sensitive(w, delta_y);
3367 return TRUE;
3368 }
3369
3370 return smooth;
3371 }
3372}
3373
3374static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event)
3375{
3376 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3377 guint key = dt_keys_mainpad_alternatives(event->keyval);
3378
3379 if(w->type == DT_BAUHAUS_SLIDER)
3380 {
3381 switch(key)
3382 {
3383 case GDK_KEY_Right:
3384 _slider_add_step(widget, 1, event->state);
3385 return TRUE;
3386
3387 case GDK_KEY_Left:
3388 _slider_add_step(widget, -1, event->state);
3389 return TRUE;
3390
3391 case GDK_KEY_Insert:
3392 if(w->quad_toggle)
3393 {
3396 return TRUE;
3397 }
3398
3399 default:
3400 return FALSE;
3401 }
3402 }
3403 else if(w->type == DT_BAUHAUS_COMBOBOX)
3404 {
3405 switch(key)
3406 {
3407 case GDK_KEY_Return:
3408 dt_bauhaus_show_popup(widget);
3409 return TRUE;
3410
3411 case GDK_KEY_Insert:
3412 if(w->quad_toggle)
3413 {
3416 return TRUE;
3417 }
3418
3419 default:
3420 return FALSE;
3421 }
3422 }
3423 return FALSE;
3424}
3425
3426static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3427{
3428 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3429
3430 double event_x = event->x;
3431 double event_y = event->y;
3432 double width;
3433 _bh_active_region_t activated = _bh_get_active_region(widget, &event_x, &event_y, &width, NULL);
3434
3436 dt_gui_throttle_cancel(widget);
3437
3438 if(activated == BH_REGION_OUT)
3439 {
3441 return FALSE;
3442 }
3443
3444 gtk_widget_grab_focus(widget);
3445 darktable.gui->has_scroll_focus = widget;
3446
3447 if(activated == BH_REGION_QUAD && w->quad_toggle)
3448 {
3450 return TRUE;
3451 }
3452 else
3453 {
3454 // If no quad toggle, treat the whole widget as unit pack.
3455 if(event->button == 3)
3456 {
3457 w->bauhaus->mouse_x = event_x;
3458 w->bauhaus->mouse_y = event_y;
3459 dt_bauhaus_show_popup(widget);
3460 return TRUE;
3461 }
3462 else if(event->button == 1)
3463 {
3464 // reset to default.
3465 if(event->type == GDK_2BUTTON_PRESS)
3466 {
3467 // never called, as we popup the other window under your cursor before.
3468 // (except in weird corner cases where the popup is under the -1st entry
3469 dt_bauhaus_combobox_set(widget, d->defpos);
3471 }
3472 else
3473 {
3474 // single click, show options
3475 w->bauhaus->opentime = event->time;
3476 w->bauhaus->mouse_x = event_x;
3477 w->bauhaus->mouse_y = event_y;
3478 dt_bauhaus_show_popup(widget);
3479 }
3480 return TRUE;
3481 }
3482 }
3483 return FALSE;
3484}
3485
3487{
3488 // first cast to bh widget, to check that type:
3489 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3491 return d->min + d->pos * (d->max - d->min);
3492}
3493
3495{
3496 const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(widget)->data.slider;
3497 return dt_bauhaus_slider_get(widget) * d->factor + d->offset;
3498}
3499
3501{
3502 const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(w)->data.slider;
3503 if((d->hard_max * d->factor + d->offset)*(d->hard_min * d->factor + d->offset) < 0)
3504 return g_strdup_printf("%+.*f%s", d->digits, val * d->factor + d->offset, d->format);
3505 else
3506 return g_strdup_printf( "%.*f%s", d->digits, val * d->factor + d->offset, d->format);
3507}
3508
3509static void _dt_bauhaus_slider_set_with_raise(GtkWidget *widget, float pos, gboolean raise)
3510{
3511 // translate by bounds and call set_normalized.
3512 // raise == FALSE re-applies the value (re-clamped to the current bounds) and
3513 // redraws without committing to history or emitting "value-changed". This is
3514 // what bound changes (soft min/max) need: re-clamping the current value to a
3515 // new display range is not a user edit, and emitting would re-enter the
3516 // module's gui_changed handler -- which during gui_init runs before all
3517 // widgets exist (NULL deref), and at runtime would spuriously commit.
3518 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3520 const float rpos = CLAMP(pos, d->hard_min, d->hard_max);
3521
3522 // Restore soft min/max if we are in its range
3523 float rrpos = (rpos - d->soft_min) / (d->soft_max - d->soft_min);
3524 if(rrpos > 0.f)
3525 d->min = d->soft_min;
3526 else
3527 d->min = rpos;
3528
3529 if(rrpos < 1.f)
3530 d->max = d->soft_max;
3531 else
3532 d->max = rpos;
3533
3534 dt_bauhaus_slider_set_normalized(w, (rpos - d->min) / (d->max - d->min), raise, FALSE);
3535}
3536
3537void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
3538{
3539 // this is the public interface function, it commits the value and emits "value-changed"
3541}
3542
3543void dt_bauhaus_slider_set_val(GtkWidget *widget, float val)
3544{
3545 const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(widget)->data.slider;
3546 dt_bauhaus_slider_set(widget, (val - d->offset) / d->factor);
3547}
3548
3550{
3551 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3553 d->digits = val;
3554}
3555
3557{
3558 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3560 return d->digits;
3561}
3562
3564{
3565 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3567 d->step = val;
3568}
3569
3571{
3572 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3574 float step = d->step;
3575
3576 if(step == 0.f)
3577 {
3578 const float min = d->soft_min;
3579 const float max = d->soft_max;
3580
3581 // Derive the automatic keyboard/wheel step from the internal slider domain.
3582 // Display formatting may rescale that domain (for example "%" multiplies by 100),
3583 // but interaction needs to stay in the unformatted value space to avoid jumping
3584 // across the whole range in a single step for small-percent sliders.
3585 const float top = fminf(max-min, fmaxf(fabsf(min), fabsf(max)));
3586 if(top >= 100.f)
3587 step = 1.f;
3588 else
3589 step = top / 100.f;
3590 }
3591
3592 return copysignf(step, d->factor);
3593}
3594
3596{
3597 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3599 d->fill_feedback = feedback;
3600 gtk_widget_queue_draw(widget);
3601}
3602
3604{
3605 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3607 d->min = d->soft_min;
3608 d->max = d->soft_max;
3609 dt_bauhaus_slider_set(widget, d->defpos);
3610 return;
3611}
3612
3613void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
3614{
3615 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3617 d->format = g_intern_string(format);
3618
3619 if(strstr(format,"%") && fabsf(d->hard_max) <= 10)
3620 {
3621 if(d->factor == 1.0f) d->factor = 100;
3622 d->digits -= 2;
3623 }
3624}
3625
3627{
3628 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3630 d->factor = factor;
3631}
3632
3633void dt_bauhaus_slider_set_offset(GtkWidget *widget, float offset)
3634{
3635 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3637 d->offset = offset;
3638}
3639
3640static void _delayed_slider_commit(gpointer data)
3641{
3642 // Commit slider value change to pipeline history, handling a safety timout
3643 // so incremental scrolls don't trigger a recompute at every scroll step.
3644 struct dt_bauhaus_widget_t *w = data;
3645
3646 // If a reset started after the timeout was scheduled (e.g. while reloading history,
3647 // applying a style, etc.), don't commit anything to history from this stale callback.
3648 if(darktable.gui && dt_gui_widgets_suppressed()) return;
3649
3651 {
3653 w->bauhaus->default_value_changed_callback(GTK_WIDGET(w));
3654 else
3655 fprintf(stderr, "ERROR: %s - %s is set to use default callback but none is provided\n", w->module->name,
3656 w->label);
3657 }
3658 else
3659 {
3660 if(w->module)
3661 fprintf(stderr, "WARNING: %s - %s has an IOP module but doesn't use default callback\n", w->module->name,
3662 w->label);
3663 }
3664
3665 // We need te emit this signal inconditionnaly
3666 g_signal_emit_by_name(G_OBJECT(w), "value-changed");
3667}
3668
3681static void dt_bauhaus_slider_set_normalized(struct dt_bauhaus_widget_t *w, float pos, gboolean raise, gboolean timeout)
3682{
3684 const float old_pos = d->pos;
3685 const float new_pos = CLAMP(pos, 0.0f, 1.0f);
3686
3687 if(old_pos != new_pos || raise)
3688 {
3689 const float new_value = new_pos * (d->max - d->min) + d->min;
3690 const float precision = (float)ipow(10, d->digits) * d->factor;
3691 const float rounded_value = roundf(new_value * precision) / precision;
3692 d->pos = (rounded_value - d->min) / (d->max - d->min);
3693
3694 if(w->bauhaus->current == w)
3695 gtk_widget_queue_draw(w->bauhaus->popup_area);
3696
3697 gtk_widget_queue_draw(GTK_WIDGET(w));
3698
3699 // If a delayed commit is pending from a previous user interaction, cancel it.
3700 // This prevents stale timers from firing after programmatic updates (GUI reset)
3701 // and unexpectedly committing module changes to history.
3702 if(!dt_gui_widgets_suppressed() && raise)
3703 {
3704 if(timeout)
3706 else
3707 {
3708 dt_gui_throttle_cancel(GTK_WIDGET(w));
3710 }
3711 }
3712 else if(!raise || dt_gui_widgets_suppressed())
3713 {
3714 dt_gui_throttle_cancel(GTK_WIDGET(w));
3715 }
3716 }
3717}
3718
3719static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
3720{
3721 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
3723
3724 guint key = dt_keys_mainpad_alternatives(event->keyval);
3725
3726 switch(w->type)
3727 {
3728 case DT_BAUHAUS_SLIDER:
3729 {
3730 if(bh->keys_cnt + 2 < 64
3731 && (key == GDK_KEY_space || // SPACE
3732 key == GDK_KEY_percent || // %
3733 (event->string[0] >= 40 && event->string[0] <= 57) || // ()+-*/.,0-9
3734 key == GDK_KEY_asciicircum || key == GDK_KEY_dead_circumflex || // ^
3735 key == GDK_KEY_X || key == GDK_KEY_x)) // Xx
3736 {
3737 if(key == GDK_KEY_dead_circumflex)
3738 bh->keys[bh->keys_cnt++] = '^';
3739 else
3740 bh->keys[bh->keys_cnt++] = event->string[0];
3741 gtk_widget_queue_draw(bh->popup_area);
3742 }
3743 else if(bh->keys_cnt > 0
3744 && (key == GDK_KEY_BackSpace || key == GDK_KEY_Delete))
3745 {
3746 bh->keys[--bh->keys_cnt] = 0;
3747 gtk_widget_queue_draw(bh->popup_area);
3748 }
3749 else if(bh->keys_cnt > 0 && bh->keys_cnt + 1 < 64
3750 && (key == GDK_KEY_Return))
3751 {
3752 // accept input
3753 bh->keys[bh->keys_cnt] = 0;
3754 // unnormalized input, user was typing this:
3755 const float old_value = dt_bauhaus_slider_get_val(GTK_WIDGET(w));
3756 const float new_value = dt_calculator_solve(old_value, bh->keys);
3757 if(isfinite(new_value)) dt_bauhaus_slider_set_val(GTK_WIDGET(w), new_value);
3758 bh->keys_cnt = 0;
3759 memset(bh->keys, 0, sizeof(bh->keys));
3761 }
3762 else if(key == GDK_KEY_Escape)
3763 {
3764 // discard input ands close popup
3765 bh->keys_cnt = 0;
3766 memset(bh->keys, 0, sizeof(bh->keys));
3768 }
3769 else
3770 return FALSE;
3771
3772 return TRUE;
3773 }
3775 {
3776 if(!g_utf8_validate(event->string, -1, NULL)) return FALSE;
3777 const gunichar c = g_utf8_get_char(event->string);
3778 const long int char_width = g_utf8_next_char(event->string) - event->string;
3779 if(bh->keys_cnt + 1 + char_width < 64 && g_unichar_isprint(c))
3780 {
3781 // only accept key input if still valid or editable?
3782 g_utf8_strncpy(bh->keys + bh->keys_cnt, event->string, 1);
3783 bh->keys_cnt += char_width;
3784 gtk_widget_queue_draw(bh->popup_area);
3785 }
3786 else if(bh->keys_cnt > 0
3787 && (key == GDK_KEY_BackSpace || key == GDK_KEY_Delete))
3788 {
3789 bh->keys_cnt
3790 -= (bh->keys + bh->keys_cnt)
3791 - g_utf8_prev_char(bh->keys + bh->keys_cnt);
3792 bh->keys[bh->keys_cnt] = 0;
3793 gtk_widget_queue_draw(bh->popup_area);
3794 }
3795 else if(bh->keys_cnt > 0 && bh->keys_cnt + 1 < 64
3796 && (key == GDK_KEY_Return))
3797 {
3798 // accept unique matches only for editable:
3799 if(w->data.combobox.editable)
3800 bh->end_mouse_y = FLT_MAX;
3801 else
3802 bh->end_mouse_y = 0;
3803 bh->keys[bh->keys_cnt] = 0;
3805 bh->keys_cnt = 0;
3806 memset(bh->keys, 0, sizeof(bh->keys));
3808 }
3809 else if(key == GDK_KEY_Escape)
3810 {
3811 // discard input and close popup
3812 bh->keys_cnt = 0;
3813 memset(bh->keys, 0, sizeof(bh->keys));
3815 }
3816 else if(key == GDK_KEY_Up)
3817 {
3819 }
3820 else if(key == GDK_KEY_Down)
3821 {
3823 }
3824 else if(key == GDK_KEY_Return)
3825 {
3826 // return pressed, but didn't type anything
3827 bh->end_mouse_y = -1; // negative will use currently highlighted instead.
3828 bh->keys[bh->keys_cnt] = 0;
3829 bh->keys_cnt = 0;
3830 memset(bh->keys, 0, sizeof(bh->keys));
3833 }
3834 else
3835 return FALSE;
3836 return TRUE;
3837 }
3838 default:
3839 return FALSE;
3840 }
3841}
3842
3843static gboolean dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3844{
3845 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3847
3848 double event_x = event->x;
3849 double event_y = event->y;
3850 double main_width = 0.;
3851 _bh_active_region_t activated = _bh_get_active_region(widget, &event_x, &event_y, &main_width, NULL);
3852 w->bauhaus->mouse_x = event_x;
3853 w->bauhaus->mouse_y = event_y;
3854
3855 if(activated == BH_REGION_OUT)
3856 {
3858 return FALSE;
3859 }
3860
3861 gtk_widget_grab_focus(widget);
3862 darktable.gui->has_scroll_focus = widget;
3863
3864 if(activated == BH_REGION_QUAD && w->quad_toggle)
3865 {
3867 return TRUE;
3868 }
3869 else if(activated == BH_REGION_MAIN)
3870 {
3871 if(event->button == 1)
3872 {
3873 if(event->type == GDK_2BUTTON_PRESS)
3874 {
3875 // double left click on the main region : reset value to default
3877 d->is_dragging = 0;
3878 }
3879 else
3880 {
3881 // single left click on main region : redraw the slider immediately
3882 // but without committing results to pipeline yet.
3883 if(event_y < w->bauhaus->line_height)
3884 {
3885 // single left click on the header name : do nothing (only give focus)
3886 d->is_dragging = 0;
3887 }
3888 else
3889 {
3890 // single left click on slider bar : set new value
3891 d->is_dragging = 1;
3892 dt_bauhaus_slider_set_normalized(w, event_x / main_width, FALSE, FALSE);
3893 }
3894 }
3895 }
3896 else if(event->button == 3)
3897 {
3898 // right click : show accurate slider setting popup
3899 d->oldpos = d->pos;
3900 dt_bauhaus_show_popup(widget);
3901 }
3902 else if(event->button == 2)
3903 {
3904 // middle click : reset zoom range to soft min/max
3905 _slider_zoom_range(w, 0);
3906 }
3907 return TRUE;
3908 }
3909 return FALSE;
3910}
3911
3912static gboolean dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3913{
3914 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3916
3918
3919 // is_dragging is set TRUE no matter what on button_press, except if it's a double click.
3920 // double click is the only event handled in button_press, otherwise we assume every event
3921 // is drag and drop, and handle the final drag coordinate here.
3922 if(d->is_dragging)
3923 {
3924 d->is_dragging = 0;
3925 dt_gui_throttle_cancel(widget);
3926
3927 if(event->button == 1)
3928 {
3930 return TRUE;
3931 }
3932 }
3933 return FALSE;
3934}
3935
3936static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
3937{
3938 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3940 _bh_active_region_t activated = BH_REGION_OUT; // = 0
3941
3942 if(d->is_dragging && event->state & GDK_BUTTON1_MASK)
3943 {
3944 double event_x = event->x;
3945 double event_y = event->y;
3946 double main_width;
3947 activated = _bh_get_active_region(widget, &event_x, &event_y, &main_width, NULL);
3948
3949 w->bauhaus->mouse_x = event_x;
3950 w->bauhaus->mouse_y = event_y;
3951 dt_bauhaus_slider_set_normalized(w, event_x / main_width, TRUE, TRUE);
3952 }
3953
3954 return activated;
3955}
3956
3958{
3959 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3960 w->no_accels = TRUE;
3961}
3962
3964{
3965 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3966 w->no_module_list = TRUE;
3967}
3968
3970{
3971 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3973}
3974
3975
3976// clang-format off
3977// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3978// vim: shiftwidth=2 expandtab tabstop=2 cindent
3979// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3980// clang-format on
void dt_accels_new_virtual_shortcut(dt_accels_t *accels, GtkAccelGroup *accel_group, const gchar *accel_path, GtkWidget *widget, guint key_val, GdkModifierType accel_mods)
Add a new virtual shortcut. Virtual shortcuts are immutable, read-only and don't trigger any action....
gchar * dt_accels_build_path(const gchar *scope, const gchar *feature)
void dt_accels_remove_accel(dt_accels_t *accels, const char *path, gpointer data)
Recursively remove all accels for all shortcuts containing path. This is unneeded for accels attached...
Handle default and user-set shortcuts (accelerators)
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int position()
#define m
Definition basecurve.c:278
GtkWidget * dt_bauhaus_slider_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1778
static gboolean dt_bauhaus_popup_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
Definition bauhaus.c:907
int dt_bauhaus_slider_get_digits(GtkWidget *widget)
Definition bauhaus.c:3556
GtkWidget * dt_bauhaus_combobox_new_full(dt_bauhaus_t *bh, dt_gui_module_t *self, const char *label, const char *tip, int pos, GtkCallback callback, gpointer data, const char **texts)
Definition bauhaus.c:1852
static float _bh_get_row_height(struct dt_bauhaus_widget_t *w)
Get the total height of a GUI row containing a line of text + top and bottom padding.
Definition bauhaus.c:150
void dt_bauhaus_slider_set_soft_range(GtkWidget *widget, float soft_min, float soft_max)
Definition bauhaus.c:1650
_bh_halign_t
Definition bauhaus.c:664
@ BH_ALIGN_RIGHT
Definition bauhaus.c:666
@ BH_ALIGN_CENTER
Definition bauhaus.c:667
@ BH_ALIGN_LEFT
Definition bauhaus.c:665
static gboolean ensure_focus_idle(gpointer data)
Definition bauhaus.c:447
static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
Definition bauhaus.c:2685
void dt_bauhaus_slider_set_digits(GtkWidget *widget, int val)
Definition bauhaus.c:3549
GtkWidget * dt_bauhaus_slider_from_widget(dt_bauhaus_t *bh, dt_bauhaus_widget_t *w, dt_gui_module_t *self, float min, float max, float step, float defval, int digits, int feedback)
Definition bauhaus.c:1810
void dt_bauhaus_combobox_insert(GtkWidget *widget, const char *text, int pos)
Definition bauhaus.c:2121
static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event)
Definition bauhaus.c:3374
void dt_bauhaus_slider_set_hard_max(GtkWidget *widget, float val)
Definition bauhaus.c:1584
static _bh_active_region_t _bh_get_active_region(GtkWidget *widget, double *x, double *y, double *width, GtkWidget *popup)
Check if we have user cursor over quad area or over the slider/main area, then correct cursor coordin...
Definition bauhaus.c:335
void dt_bauhaus_combobox_clear(GtkWidget *widget)
Definition bauhaus.c:2192
static gboolean dt_bauhaus_popup_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
Definition bauhaus.c:899
static gboolean _widget_scroll(GtkWidget *widget, GdkEventScroll *event)
Definition bauhaus.c:3308
void dt_bauhaus_slider_clear_stops(GtkWidget *widget)
Definition bauhaus.c:2367
static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
Definition bauhaus.c:3936
static void _delayed_slider_commit(gpointer data)
Definition bauhaus.c:3640
static gboolean dt_bauhaus_popup_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition bauhaus.c:956
static void dt_bauhaus_widget_reject(struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:2585
char * dt_bauhaus_slider_get_text(GtkWidget *w, float val)
Definition bauhaus.c:3500
void dt_bauhaus_combobox_set_selected_text_align(GtkWidget *widget, const dt_bauhaus_combobox_alignment_t text_align)
Definition bauhaus.c:2095
void dt_bauhaus_combobox_set_editable(GtkWidget *widget, int editable)
Definition bauhaus.c:2077
static double _widget_get_main_width(struct dt_bauhaus_widget_t *w, GtkWidget *widget, double *total_width)
Get the width of the main Bauhaus widget area (slider scale or combobox), accounting for quad space,...
Definition bauhaus.c:206
gboolean dt_bauhaus_focus_in_callback(GtkWidget *widget, GdkEventFocus event, gpointer user_data)
Definition bauhaus.c:511
void dt_bauhaus_hide_popup(dt_bauhaus_t *bh)
Definition bauhaus.c:3175
gboolean dt_bauhaus_focus_callback(GtkWidget *widget, GtkDirectionType direction, gpointer data)
Definition bauhaus.c:530
static double _get_slider_height(GtkWidget *widget)
Definition bauhaus.c:256
void dt_bauhaus_slider_reset(GtkWidget *widget)
Definition bauhaus.c:3603
static void dt_bauhaus_window_show(GtkWidget *w, gpointer user_data)
Definition bauhaus.c:1053
static void _style_updated(GtkWidget *widget)
Definition bauhaus.c:1796
void dt_bauhaus_cleanup(dt_bauhaus_t *bauhaus)
Definition bauhaus.c:1501
static float _bh_slider_get_min_step(const struct dt_bauhaus_widget_t *const w)
Return the minimum representable value step, for current UI scaling factor and number of digits.
Definition bauhaus.c:383
static gboolean _resize_handle_button(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:1122
void dt_bauhaus_slider_set_default(GtkWidget *widget, float def)
Definition bauhaus.c:1643
const char * dt_bauhaus_combobox_get_entry(GtkWidget *widget, int pos)
Definition bauhaus.c:2202
gboolean dt_bauhaus_focus_out_callback(GtkWidget *widget, GdkEventFocus event, gpointer user_data)
Definition bauhaus.c:521
static void dt_bauhaus_draw_baseline(struct dt_bauhaus_widget_t *w, cairo_t *cr, float width)
Draw the slider baseline, aka the backgronud bar.
Definition bauhaus.c:2484
void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, float b)
Definition bauhaus.c:2375
#define DT_BAUHAUS_FOCUS_IDLE_MAX_TRIES
Definition bauhaus.c:446
void dt_bauhaus_load_theme(dt_bauhaus_t *bauhaus)
Definition bauhaus.c:1290
float dt_bauhaus_slider_get(GtkWidget *widget)
Definition bauhaus.c:3486
void dt_bauhaus_combobox_set_text(GtkWidget *widget, const char *text)
Definition bauhaus.c:2212
static void dt_bauhaus_draw_indicator(struct dt_bauhaus_widget_t *w, float pos, cairo_t *cr, float wd)
Definition bauhaus.c:2412
void dt_bauhaus_combobox_entry_set_sensitive(GtkWidget *widget, int pos, gboolean sensitive)
Definition bauhaus.c:2357
gboolean dt_bauhaus_combobox_set_from_value(GtkWidget *widget, int value)
Definition bauhaus.c:2333
_bh_valign_t
Definition bauhaus.c:656
@ BH_ALIGN_TOP
Definition bauhaus.c:657
@ BH_ALIGN_MIDDLE
Definition bauhaus.c:659
@ BH_ALIGN_BOTTOM
Definition bauhaus.c:658
void dt_bauhaus_widget_release_quad(GtkWidget *widget)
Definition bauhaus.c:1765
GtkWidget * dt_bauhaus_slider_new_with_range_and_feedback(dt_bauhaus_t *bh, dt_gui_module_t *self, float min, float max, float step, float defval, int digits, int feedback)
Definition bauhaus.c:1789
void dt_bauhaus_combobox_from_widget(dt_bauhaus_t *bh, dt_bauhaus_widget_t *w, dt_gui_module_t *self)
Definition bauhaus.c:1915
static void _get_preferred_width(GtkWidget *widget, gint *minimum_size, gint *natural_size)
Definition bauhaus.c:3141
void dt_bauhaus_combobox_insert_separator_with_height(GtkWidget *widget, int pos, const float row_height_factor)
Definition bauhaus.c:2146
static void _delayed_combobox_commit(gpointer data)
Definition bauhaus.c:2221
static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
Definition bauhaus.c:3719
float dt_bauhaus_slider_get_soft_max(GtkWidget *widget)
Definition bauhaus.c:1636
static void free_combobox_entry(gpointer data)
Definition bauhaus.c:637
static double _get_combobox_popup_height(struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:275
void dt_bauhaus_widget_set_field(GtkWidget *widget, gpointer field, dt_introspection_type_t field_type)
Definition bauhaus.c:1713
static void dt_bauhaus_slider_set_normalized(struct dt_bauhaus_widget_t *w, float pos, gboolean raise, gboolean timeout)
Set the value of a slider as a ratio of the GUI slider width.
Definition bauhaus.c:3681
static void _bh_combobox_get_hovered_entry(struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:395
float dt_bauhaus_slider_get_soft_min(GtkWidget *widget)
Definition bauhaus.c:1619
GtkWidget * dt_bauhaus_combobox_from_conf(dt_bauhaus_t *bh, dt_gui_module_t *self, const char *confkey)
Definition bauhaus.c:1871
static void _combobox_set(GtkWidget *widget, const int pos, gboolean timeout)
Set a combobox to a given integer position. Private API function, called from user events.
Definition bauhaus.c:2259
void dt_bauhaus_slider_set_feedback(GtkWidget *widget, int feedback)
Definition bauhaus.c:3595
static double get_slider_line_offset(const double pos, const double scale, const double x, double y, const double line_height)
Definition bauhaus.c:826
static gboolean _resize_handle_cursor(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition bauhaus.c:1102
void dt_bauhaus_disable_module_list(GtkWidget *widget)
Definition bauhaus.c:3963
static void _widget_finalize(GObject *widget)
Definition bauhaus.c:1232
static float _bh_get_combobox_entry_height(struct dt_bauhaus_widget_t *w, const dt_bauhaus_combobox_entry_t *entry)
Get the vertical space used by a combobox popup entry.
Definition bauhaus.c:161
_bh_active_region_t
Definition bauhaus.c:317
@ BH_REGION_QUAD
Definition bauhaus.c:320
@ BH_REGION_OUT
Definition bauhaus.c:318
@ BH_REGION_MAIN
Definition bauhaus.c:319
static void _combobox_conf_value_changed(GtkWidget *widget, gpointer user_data)
Definition bauhaus.c:1865
float dt_bauhaus_slider_get_val(GtkWidget *widget)
Definition bauhaus.c:3494
static dt_bauhaus_combobox_data_t * _combobox_data(GtkWidget *widget)
Definition bauhaus.c:1934
void dt_bauhaus_combobox_insert_full(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align, gpointer data, void(*free_func)(void *), int pos)
Definition bauhaus.c:2126
int dt_bauhaus_combobox_get(GtkWidget *widget)
Definition bauhaus.c:2350
void dt_bauhaus_widget_set_quad_toggle(GtkWidget *widget, int toggle)
Definition bauhaus.c:1723
static int _combobox_selectable_count(const dt_bauhaus_combobox_data_t *d)
Count selectable combobox entries.
Definition bauhaus.c:1952
static gboolean dt_bauhaus_popup_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:983
static void show_pango_text(struct dt_bauhaus_widget_t *w, GtkStyleContext *context, cairo_t *cr, GdkRectangle *bounding_box, const char *text, _bh_halign_t halign, _bh_valign_t valign, PangoEllipsizeMode ellipsize, GdkRGBA *bg_color, float *width, float *height, GtkStateFlags state)
Display text aligned in a bounding box, with pseudo-classes properties handled, and optional backgrou...
Definition bauhaus.c:686
void dt_bauhaus_slider_set_offset(GtkWidget *widget, float offset)
Definition bauhaus.c:3633
static void _dt_bauhaus_slider_set_with_raise(GtkWidget *widget, float pos, gboolean raise)
Definition bauhaus.c:3509
#define DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY
Definition bauhaus.c:444
static void _combobox_next_sensitive(struct dt_bauhaus_widget_t *w, int delta)
Definition bauhaus.c:584
static gboolean dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:3843
void dt_bauhaus_combobox_set_entries_ellipsis(GtkWidget *widget, PangoEllipsizeMode ellipis)
Definition bauhaus.c:2067
void dt_bauhaus_slider_set_step(GtkWidget *widget, float val)
Definition bauhaus.c:3563
static dt_bauhaus_combobox_entry_t * new_combobox_entry(const char *label, const char *tooltip, dt_bauhaus_combobox_alignment_t alignment, gboolean sensitive, void *data, void(*free_func)(void *))
Definition bauhaus.c:605
static void _slider_zoom_range(struct dt_bauhaus_widget_t *w, float zoom)
Definition bauhaus.c:869
static GdkRGBA * default_color_assign()
Definition bauhaus.c:647
void dt_bauhaus_slider_set_soft_max(GtkWidget *widget, float val)
Definition bauhaus.c:1626
void dt_bauhaus_combobox_insert_separator(GtkWidget *widget, int pos)
Definition bauhaus.c:2141
void dt_bauhaus_show_popup(GtkWidget *widget)
Definition bauhaus.c:3191
void dt_bauhaus_set_use_default_callback(GtkWidget *widget)
Tell the widget to use the globally-defined default callback in the bauhaus structure This callback n...
Definition bauhaus.c:3969
int dt_bauhaus_combobox_length(GtkWidget *widget)
Definition bauhaus.c:2158
void dt_bauhaus_widget_set_quad_active(GtkWidget *widget, int active)
Definition bauhaus.c:1729
static _bh_active_region_t _popup_coordinates(dt_bauhaus_t *bh, const double x_root, const double y_root, double *event_x, double *event_y)
Definition bauhaus.c:428
void dt_bauhaus_combobox_set_default(GtkWidget *widget, int def)
Definition bauhaus.c:1552
static double _widget_get_quad_width(struct dt_bauhaus_widget_t *w)
Get the width of the quad without padding.
Definition bauhaus.c:174
float dt_bauhaus_slider_get_hard_min(GtkWidget *widget)
Definition bauhaus.c:1577
static double _widget_get_total_width(struct dt_bauhaus_widget_t *w, GtkWidget *widget)
Get the total width of the main Bauhaus widget area, accounting for padding and margins.
Definition bauhaus.c:189
static float _bh_round_to_n_digits(const struct dt_bauhaus_widget_t *const w, float x)
Round a slider numeric value to the number of digits specified in the widget w.
Definition bauhaus.c:370
void dt_bauhaus_combobox_add_full(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align, gpointer data, void(free_func)(void *data), gboolean sensitive)
Definition bauhaus.c:2041
gpointer dt_bauhaus_combobox_get_data(GtkWidget *widget)
Definition bauhaus.c:2182
void dt_bauhaus_combobox_add_populate_fct(GtkWidget *widget, void(*fct)(GtkWidget *w, void *module))
Definition bauhaus.c:2005
static void _bauhaus_widget_init(dt_bauhaus_t *bauhaus, dt_bauhaus_widget_t *w, dt_gui_module_t *self)
Definition bauhaus.c:1519
void dt_bauhaus_combobox_add_separator(GtkWidget *widget)
Definition bauhaus.c:2053
static float _get_combobox_max_width(GtkWidget *widget)
Definition bauhaus.c:2910
static dt_bauhaus_combobox_entry_t * new_combobox_separator(const float row_height_factor)
Create a new combobox separator entry.
Definition bauhaus.c:626
void dt_bauhaus_combobox_add_list(GtkWidget *widget, const char **texts)
Definition bauhaus.c:2013
gboolean _action_request_focus(GtkAccelGroup *accel_group, GObject *accelerable, guint keyval, GdkModifierType modifier, gpointer data)
Definition bauhaus.c:539
static double _get_indicator_y_position(struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:263
int dt_bauhaus_combobox_get_editable(GtkWidget *widget)
Definition bauhaus.c:2088
static void draw_slider_line(cairo_t *cr, const double pos, const double off, const double scale, const double width, const double height, const double line_height, double line_width)
Definition bauhaus.c:849
const char * dt_bauhaus_widget_get_label(GtkWidget *widget)
Definition bauhaus.c:1699
int dt_bauhaus_widget_get_quad_active(GtkWidget *widget)
Definition bauhaus.c:1746
void dt_bauhaus_slider_set_soft_min(GtkWidget *widget, float val)
Definition bauhaus.c:1609
void dt_bauhaus_disable_accels(GtkWidget *widget)
Definition bauhaus.c:3957
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
static void _margins_retrieve(struct dt_bauhaus_widget_t *w)
Update the box margin and padding properties of the widget w by reading CSS context.
Definition bauhaus.c:124
static gboolean dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:3912
static gchar * _build_label(const struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:2680
const char * dt_bauhaus_combobox_get_text(GtkWidget *widget)
Definition bauhaus.c:2165
void dt_bauhaus_slider_set_val(GtkWidget *widget, float val)
Definition bauhaus.c:3543
void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
Definition bauhaus.c:3537
static double _widget_get_main_height(struct dt_bauhaus_widget_t *w, GtkWidget *widget)
Get the height of the main Bauhaus widget area (slider scale or combobox), that is the box allocation...
Definition bauhaus.c:223
float dt_bauhaus_slider_get_step(GtkWidget *widget)
Definition bauhaus.c:3570
static void dt_bauhaus_widget_accept(struct dt_bauhaus_widget_t *w, gboolean timeout)
Definition bauhaus.c:2602
static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf)
Definition bauhaus.c:2965
static gboolean _enter_leave(GtkWidget *widget, GdkEventCrossing *event)
Definition bauhaus.c:1064
static void _slider_add_step(GtkWidget *widget, float delta, guint state)
Definition bauhaus.c:3289
void dt_bauhaus_widget_press_quad(GtkWidget *widget)
Definition bauhaus.c:1752
static int _combobox_entry_pos_to_public(const dt_bauhaus_combobox_data_t *d, const int entry_pos)
Convert an internal entry row to the public value index.
Definition bauhaus.c:1989
void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
Definition bauhaus.c:2304
#define DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY
Definition bauhaus.c:445
void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *label)
Definition bauhaus.c:1656
void dt_bauhaus_slider_set_hard_min(GtkWidget *widget, float val)
Definition bauhaus.c:1560
GtkWidget * dt_bauhaus_slider_new_with_range(dt_bauhaus_t *bh, dt_gui_module_t *self, float min, float max, float step, float defval, int digits)
Definition bauhaus.c:1783
void dt_bauhaus_combobox_remove_at(GtkWidget *widget, int pos)
Definition bauhaus.c:2104
GtkWidget * dt_bauhaus_combobox_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1845
void dt_bauhaus_widget_set_quad_visibility(GtkWidget *widget, const gboolean visible)
Definition bauhaus.c:1739
static double _get_slider_bar_height(struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:268
static void _get_preferred_height(GtkWidget *widget, gint *minimum_size, gint *natural_size)
Definition bauhaus.c:3159
gboolean dt_bauhaus_combobox_set_from_text(GtkWidget *widget, const char *text)
Definition bauhaus.c:2314
static gboolean _resize_handle_motion(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
Definition bauhaus.c:1164
void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
Definition bauhaus.c:3613
float dt_bauhaus_slider_get_hard_max(GtkWidget *widget)
Definition bauhaus.c:1602
static void dt_bauhaus_draw_quad(struct dt_bauhaus_widget_t *w, cairo_t *cr, const double x, const double y)
Definition bauhaus.c:2446
static int _combobox_public_to_entry_pos(const dt_bauhaus_combobox_data_t *d, const int pos, const gboolean allow_end)
Convert a public value index to the internal entry row.
Definition bauhaus.c:1969
static void _translate_cursor(double *x, double *y, struct dt_bauhaus_widget_t *const w)
Translate in-place the cursor coordinates within the widget or popup according to padding and margin,...
Definition bauhaus.c:307
void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
Definition bauhaus.c:2019
void dt_bauhaus_widget_set_quad_paint(GtkWidget *widget, dt_bauhaus_quad_paint_f f, int paint_flags, void *paint_data)
Definition bauhaus.c:1705
static gboolean dt_bauhaus_popup_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:962
void dt_bauhaus_combobox_add_with_tooltip(GtkWidget *widget, const char *text, const char *tooltip)
Definition bauhaus.c:2024
void dt_bauhaus_combobox_add_separator_with_height(GtkWidget *widget, const float row_height_factor)
Definition bauhaus.c:2058
dt_bauhaus_t * dt_bauhaus_init()
Definition bauhaus.c:1383
static void dt_bh_init(DtBauhausWidget *class)
Definition bauhaus.c:1058
void dt_bauhaus_slider_set_factor(GtkWidget *widget, float factor)
Definition bauhaus.c:3626
static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:3426
void dt_bauhaus_combobox_add_aligned(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align)
Definition bauhaus.c:2036
static double _bh_slider_get_scale(struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:389
static void dt_bh_class_init(DtBauhausWidgetClass *class)
Definition bauhaus.c:1269
static double _get_combobox_height(GtkWidget *widget)
Definition bauhaus.c:249
dt_bauhaus_combobox_alignment_t
Definition bauhaus.h:123
@ DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT
Definition bauhaus.h:125
@ DT_BAUHAUS_QUAD_PRESSED_SIGNAL
Definition bauhaus.h:227
@ DT_BAUHAUS_VALUE_CHANGED_SIGNAL
Definition bauhaus.h:226
#define DT_BAUHAUS_COMBO_MAX_TEXT
Definition bauhaus.h:72
@ DT_BAUHAUS_COMBOBOX
Definition bauhaus.h:87
@ DT_BAUHAUS_SLIDER
Definition bauhaus.h:86
#define DT_BAUHAUS_WIDGET(obj)
Definition bauhaus.h:60
#define DT_BAUHAUS_WIDGET_TYPE
Definition bauhaus.h:59
static void set_color(cairo_t *cr, GdkRGBA color)
Definition bauhaus.h:446
#define DT_BAUHAUS_COMBO_SEPARATOR_DEFAULT_HEIGHT_FACTOR
Definition bauhaus.h:73
int(* dt_bauhaus_resize_handle_get_size_f)(gpointer user_data)
Definition bauhaus.h:168
void(* dt_bauhaus_quad_paint_f)(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)
Definition bauhaus.h:167
#define DT_BAUHAUS_SLIDER_MAX_STOPS
Definition bauhaus.h:71
#define INNER_PADDING
Definition bauhaus.h:79
int(* dt_bauhaus_resize_handle_resize_f)(int requested_size, gboolean finished, gpointer user_data)
Definition bauhaus.h:169
int width
Definition bilateral.h:1
int height
Definition bilateral.h:1
float dt_calculator_solve(const float x, const char *formula)
Definition calculator.c:322
@ DT_COLORLABELS_PURPLE
Definition colorlabels.h:45
@ DT_COLORLABELS_GREEN
Definition colorlabels.h:43
@ DT_COLORLABELS_YELLOW
Definition colorlabels.h:42
@ DT_COLORLABELS_BLUE
Definition colorlabels.h:44
@ DT_COLORLABELS_RED
Definition colorlabels.h:41
const dt_aligned_pixel_t f
static const float const float const float min
const float inc
const float max
const float top
const float delta
char * key
@ DT_VALUES
Definition conf.h:99
@ DT_ENUM
Definition conf.h:65
const char * dt_confgen_get_tooltip(const char *name)
gchar * dt_conf_get_string(const char *name)
gboolean dt_confgen_value_exists(const char *name, dt_confgen_value_kind_t kind)
void dt_conf_set_string(const char *name, const char *val)
const char * dt_confgen_get(const char *name, dt_confgen_value_kind_t kind)
dt_confgen_type_t dt_confgen_type(const char *name)
const char * dt_confgen_get_label(const char *name)
void dt_toast_log(const char *msg,...)
Definition control.c:824
#define dt_control_change_cursor(cursor)
Definition control.h:116
darktable_t darktable
Definition darktable.c:183
void dt_print(dt_debug_thread_t thread, const char *msg,...)
Definition darktable.c:1600
@ DT_DEBUG_SHORTCUTS
Definition darktable.h:760
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:485
#define dt_free(ptr)
Definition darktable.h:478
void void void gboolean dt_gui_widgets_suppressed(void)
Definition gtk.c:194
static const dt_aligned_pixel_simd_t value
Definition darktable.h:599
static gboolean dt_modifier_is(const GdkModifierType state, const GdkModifierType desired_modifier_mask)
Definition darktable.h:955
#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
G_DEFINE_TYPE(GtkDarktableDrawingArea, dtgtk_drawing_area, GTK_TYPE_DRAWING_AREA)
@ CPF_ACTIVE
Definition dtgtk/paint.h:67
static guint dt_keys_mainpad_alternatives(const guint key_val)
Remap keypad keys to usual mainpad ones.
Definition gdkkeys.h:113
void dt_gui_cairo_set_font_options(cairo_t *cr, GtkWidget *widget)
Definition gtk.c:1556
gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, int *delta_x, int *delta_y)
Definition gtk.c:300
void dt_capitalize_label(gchar *text)
Definition gtk.c:3382
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
Definition gtk.c:133
static cairo_surface_t * dt_cairo_image_surface_create(cairo_format_t format, int width, int height)
Definition gtk.h:322
#define dt_accels_new_darkroom_action(a, b, c, d, e, f, g)
Definition gtk.h:448
int dt_ui_panel_get_size(dt_ui_t *ui, const dt_ui_panel_t p)
get width of right, left, or bottom panel
gboolean dt_ui_panel_ancestor(dt_ui_t *ui, const dt_ui_panel_t p, GtkWidget *w)
is the panel ancestor of widget
GtkWidget * dt_ui_main_window(dt_ui_t *ui)
get the main window widget
#define DT_PIXEL_APPLY_DPI(value)
Definition gtk.h:90
void dt_gui_throttle_cancel(gpointer source)
void dt_gui_throttle_queue(gpointer source, dt_gui_throttle_callback_t callback, gpointer user_data)
const char * tooltip
Definition image.h:251
void dt_iop_request_focus(dt_iop_module_t *module)
Definition imageop.c:2221
void dt_iop_gui_set_expanded(dt_iop_module_t *module, gboolean expanded, gboolean collapse_others)
Definition imageop.c:2381
dt_introspection_type_t
static const float x
static int precision(double x, double adj)
Definition lens.cc:1498
float *const restrict const size_t k
static int ipow(int base, int exp)
Fast integer power, computing base^exp.
Definition math.h:413
#define CLAMPF(a, mn, mx)
Definition math.h:89
#define M_PI
Definition math.h:45
size_t size
Definition mipmap_cache.c:3
static dt_mipmap_size_t get_size(const uint32_t key)
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:104
const float factor
Definition pdf.h:90
struct _GtkWidget GtkWidget
Definition splash.h:29
const float uint32_t state[4]
const float r
struct dt_gui_gtk_t * gui
Definition darktable.h:803
struct dt_develop_t * develop
Definition darktable.h:798
GtkAccelGroup * darkroom_accels
PangoEllipsizeMode entries_ellipsis
Definition bauhaus.h:149
void(* populate)(GtkWidget *w, void *module)
Definition bauhaus.h:151
dt_bauhaus_combobox_alignment_t text_align
Definition bauhaus.h:147
Definition bauhaus.h:130
gboolean sensitive
Definition bauhaus.h:134
char * tooltip
Definition bauhaus.h:132
char * label
Definition bauhaus.h:131
float row_height_factor
Definition bauhaus.h:136
void(* free_func)(gpointer)
Definition bauhaus.h:138
dt_bauhaus_combobox_alignment_t alignment
Definition bauhaus.h:133
gboolean is_separator
Definition bauhaus.h:135
gpointer data
Definition bauhaus.h:137
dt_bauhaus_resize_handle_get_size_f get_size
Definition bauhaus.c:1093
dt_bauhaus_resize_handle_resize_f resize
Definition bauhaus.c:1094
GtkOrientation orientation
Definition bauhaus.c:1091
const char * format
Definition bauhaus.h:115
GdkRGBA graph_bg
Definition bauhaus.h:281
guint32 opentime
Definition bauhaus.h:254
GdkRGBA color_value_text_insensitive
Definition bauhaus.h:278
float end_mouse_x
Definition bauhaus.h:256
float baseline_size
Definition bauhaus.h:271
GdkRGBA graph_scope_restricted
Definition bauhaus.h:282
GdkRGBA colorlabels[DT_COLORLABELS_LAST]
Definition bauhaus.h:284
void(* default_value_changed_callback)(GtkWidget *widget)
Definition bauhaus.h:288
GdkRGBA graph_overlay
Definition bauhaus.h:281
float mouse_line_distance
Definition bauhaus.h:259
GdkRGBA color_border
Definition bauhaus.h:277
GdkRGBA indicator_border
Definition bauhaus.h:277
GdkRGBA color_value_insensitive
Definition bauhaus.h:278
GdkRGBA graph_border
Definition bauhaus.h:281
GdkRGBA graph_fg_active
Definition bauhaus.h:281
GdkRGBA graph_grid
Definition bauhaus.h:281
PangoFontDescription * pango_font_desc
Definition bauhaus.h:274
GdkRGBA graph_exterior
Definition bauhaus.h:281
GdkRGBA color_fill
Definition bauhaus.h:277
gboolean hiding
Definition bauhaus.h:265
GtkWidget * popup_window
Definition bauhaus.h:246
GdkRGBA inset_histogram
Definition bauhaus.h:282
float quad_width
Definition bauhaus.h:273
float line_height
Definition bauhaus.h:269
float end_mouse_y
Definition bauhaus.h:256
GdkRGBA color_value_text
Definition bauhaus.h:278
float mouse_x
Definition bauhaus.h:250
GdkRGBA color_value
Definition bauhaus.h:278
char keys[64]
Definition bauhaus.h:261
float border_width
Definition bauhaus.h:272
GdkRGBA graph_fg
Definition bauhaus.h:281
GtkWidget * popup_area
Definition bauhaus.h:247
struct dt_bauhaus_widget_t * current
Definition bauhaus.h:245
GdkRGBA graph_colors[3]
Definition bauhaus.h:283
GdkRGBA color_fg
Definition bauhaus.h:277
int change_active
Definition bauhaus.h:258
GdkRGBA color_fg_insensitive
Definition bauhaus.h:277
float marker_size
Definition bauhaus.h:270
float mouse_y
Definition bauhaus.h:250
GdkRGBA color_bg
Definition bauhaus.h:277
char label[256]
Definition bauhaus.h:186
gboolean show_quad
Definition bauhaus.h:196
GtkBorder * padding
Definition bauhaus.h:203
dt_bauhaus_quad_paint_f quad_paint
Definition bauhaus.h:188
dt_bauhaus_data_t data
Definition bauhaus.h:218
gboolean no_module_list
Definition bauhaus.h:208
gboolean use_default_callback
Definition bauhaus.h:215
gboolean no_accels
Definition bauhaus.h:207
void * quad_paint_data
Definition bauhaus.h:192
GtkBorder * margin
Definition bauhaus.h:203
dt_gui_module_t *gpointer field
Definition bauhaus.h:181
dt_bauhaus_t * bauhaus
Definition bauhaus.h:211
dt_introspection_type_t field_type
Definition bauhaus.h:183
dt_bauhaus_type_t type
Definition bauhaus.h:177
struct dt_iop_module_t * gui_module
Definition develop.h:165
gint scroll_mask
Definition gtk.h:230
dt_accels_t * accels
Definition gtk.h:199
GtkWidget * has_scroll_focus
Definition gtk.h:234
dt_ui_t * ui
Definition gtk.h:165
The dt_gui_module_t type is the intersection between a dt_lib_module_t and a dt_iop_module_t structur...
GHashTable * entries
Definition supervisor.c:116
GtkWidget * window
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
dt_bauhaus_combobox_data_t combobox
Definition bauhaus.h:160
dt_bauhaus_slider_data_t slider
Definition bauhaus.h:159
GList * dt_util_str_to_glist(const gchar *separator, const gchar *text)
Definition utility.c:830
@ DT_UI_PANEL_LEFT
@ DT_UI_PANEL_RIGHT