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);
824
825static double get_slider_line_offset(const double pos, const double scale, const double x, double y, const double line_height)
826{
827 // handle linear startup and rescale y to fit the whole range again
828 // Note : all inputs are in relative coordinates, except pos
829 float offset = 0.f;
830 if(y < line_height)
831 {
832 offset = x - pos;
833 }
834 else
835 {
836 // Renormalize y coordinates below the baseline
837 y = (y - line_height) / (1.0 - line_height);
838 offset = (x - sqf(y) * .5 - (1.0 - sqf(y)) * pos)
839 / (.5 * sqf(y) / scale + (1.0 - sqf(y)));
840 }
841 // clamp to result in a [0,1] range:
842 if(pos + offset > 1.0) offset = 1.0 - pos;
843 if(pos + offset < 0.0) offset = -pos;
844 return offset;
845}
846
847// draw a loupe guideline for the quadratic zoom in in the slider interface:
848static void draw_slider_line(cairo_t *cr, const double pos, const double off, const double scale, const double width, const double height,
849 const double line_height, double line_width)
850{
851 // pos is normalized position [0,1], offset is on that scale.
852 // ht is in pixels here
853 const int steps = 128;
854 const double corrected_height = (height - line_height);
855
856 cairo_set_line_width(cr, line_width);
857 cairo_move_to(cr, width * (pos + off), line_height);
858 const double half_line_width = line_width / 2.;
859 for(int j = 1; j < steps; j++)
860 {
861 const double y = (double)j / (double)(steps - 1);
862 const double x = sqf(y) * .5f * (1.f + off / scale) + (1.0f - sqf(y)) * (pos + off);
863 cairo_line_to(cr, x * width - half_line_width, line_height + y * corrected_height);
864 }
865}
866// -------------------------------
867
868static void _slider_zoom_range(struct dt_bauhaus_widget_t *w, float zoom)
869{
871
872 const float value = dt_bauhaus_slider_get(GTK_WIDGET(w));
873
874 if(roundf(zoom) == 0.f)
875 {
876 d->min = d->soft_min;
877 d->max = d->soft_max;
878 dt_bauhaus_slider_set(GTK_WIDGET(w), value); // restore value (and move min/max again if needed)
879 return;
880 }
881
882 // make sure current value still in zoomed range
883 const float min_visible = _bh_slider_get_min_step(w);
884 const float multiplier = exp2f(zoom / 2.f);
885 const float new_min = value - multiplier * (value - d->min);
886 const float new_max = value + multiplier * (d->max - value);
887 if(new_min >= d->hard_min
888 && new_max <= d->hard_max
889 && new_max - new_min >= min_visible * 10)
890 {
891 d->min = new_min;
892 d->max = new_max;
893 }
894
895 gtk_widget_queue_draw(GTK_WIDGET(w));
896}
897
898static gboolean dt_bauhaus_popup_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
899{
900 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
902 darktable.gui->has_scroll_focus = GTK_WIDGET(w);
903 return _widget_scroll(GTK_WIDGET(w), event);
904}
905
906static gboolean dt_bauhaus_popup_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
907{
908 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
910
911 double event_x;
912 double event_y;
913 const _bh_active_region_t active = _popup_coordinates(bh, event->x_root, event->y_root, &event_x, &event_y);
914
915#if DEBUG
916 fprintf(stdout, "x: %i, y: %i, active: %i\n", (int)event_x, (int)event_y, active);
917#endif
918
919 if(active == BH_REGION_OUT) return FALSE;
920
921 // Pass-on new cursor coordinates corrected for padding and margin
922 // and start a redraw. Nothing else.
923 bh->mouse_x = event_x;
924 bh->mouse_y = event_y;
925
926 if(w->type == DT_BAUHAUS_COMBOBOX)
927 {
929 gtk_widget_queue_draw(bh->popup_area);
930 }
931 else
932 {
934 const double main_height = _widget_get_main_height(w, widget);
935 const float mouse_off = get_slider_line_offset(
936 d->oldpos, _bh_slider_get_scale(w), bh->mouse_x / _widget_get_main_width(w, NULL, NULL),
937 bh->mouse_y / main_height, _get_slider_bar_height(w) / main_height);
938
939 if(d->is_dragging)
940 {
941 // On dragging (when holding a click), we commit intermediate values to pipeline for "realtime" preview
942 dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off, TRUE, TRUE);
943 }
944 else
945 {
946 // If not dragging, assume user just wants to take his time to fine-tune the value.
947 d->pos = d->oldpos + mouse_off;
948 gtk_widget_queue_draw(bh->popup_area);
949 }
950 }
951
952 return TRUE;
953}
954
955static gboolean dt_bauhaus_popup_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
956{
957 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_NORMAL, TRUE);
958 return TRUE;
959}
960
961static gboolean dt_bauhaus_popup_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
962{
963 int delay = 0;
964 g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL);
965 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
967
968 if(w && (w->type == DT_BAUHAUS_COMBOBOX) && (event->button == 1)
969 && (event->time >= bh->opentime + delay) && !bh->hiding)
970 {
971 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_ACTIVE, TRUE);
972
974 }
975 else if(bh->hiding)
976 {
978 }
979 return TRUE;
980}
981
982static gboolean dt_bauhaus_popup_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
983{
984 int delay = 0;
985 g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL);
986
987 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
989
990 if(event->button == 1)
991 {
993 && event->time < bh->opentime + delay)
994 {
995 // counts as double click, reset:
997 dt_bauhaus_combobox_set(GTK_WIDGET(w), d->defpos);
999 }
1000 else
1001 {
1002 // only accept left mouse click.
1003 // coordinates are set in motion_notify, which also makes sure they are within the valid range.
1004 // problems appear with the cornercase where user didn't move the cursor since opening the popup.
1005 // aka we need to re-read coordinates here.
1006 double event_x;
1007 double event_y;
1008 const _bh_active_region_t active = _popup_coordinates(bh, event->x_root, event->y_root, &event_x, &event_y);
1009
1010 if(active == BH_REGION_OUT)
1011 {
1014 return TRUE;
1015 }
1016
1017 bh->end_mouse_x = bh->mouse_x = event_x;
1018 bh->end_mouse_y = bh->mouse_y = event_y;
1019
1020 if(w->type == DT_BAUHAUS_SLIDER)
1021 {
1023 d->is_dragging = TRUE;
1024
1025 // Trick to ensure new value ≠ d->pos (so we commit to pipeline), since
1026 // d->pos is used for uncommitted drawings.
1027 const float value = d->pos;
1028 d->pos = d->oldpos;
1030 }
1031 else
1032 {
1035 }
1036 }
1037 bh->hiding = TRUE;
1038 }
1039 else if(event->button == 2 && w->type == DT_BAUHAUS_SLIDER)
1040 {
1041 _slider_zoom_range(w, 0);
1042 gtk_widget_queue_draw(widget);
1043 }
1044 else
1045 {
1047 bh->hiding = TRUE;
1048 }
1049 return TRUE;
1050}
1051
1052static void dt_bauhaus_window_show(GtkWidget *w, gpointer user_data)
1053{
1054 gtk_grab_add(GTK_WIDGET(user_data));
1055}
1056
1057static void dt_bh_init(DtBauhausWidget *class)
1058{
1059 // not sure if we want to use this instead of our code in *_new()
1060 // TODO: the common code from bauhaus_widget_init() could go here.
1061}
1062
1063static gboolean _enter_leave(GtkWidget *widget, GdkEventCrossing *event)
1064{
1065 if(event->type == GDK_ENTER_NOTIFY)
1066 {
1067 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
1068 }
1069 else
1070 {
1071 gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);
1072
1073 // GUI refreshes can emit synthetic crossing events while the pointer never
1074 // physically leaves the widget. Only drop wheel capture on a real pointer
1075 // leave from the widget itself.
1076 const gboolean real_leave = event->mode == GDK_CROSSING_NORMAL
1077 && event->detail != GDK_NOTIFY_INFERIOR
1078 && (!darktable.gui || !darktable.gui->reset);
1079 if(real_leave && darktable.gui->has_scroll_focus == widget)
1081 }
1082
1083 gtk_widget_queue_draw(widget);
1084
1085 return FALSE;
1086}
1087
1100
1101static gboolean _resize_handle_cursor(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
1102{
1104 if(event->type == GDK_ENTER_NOTIFY)
1105 {
1106 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
1107 dt_control_change_cursor(handle->orientation == GTK_ORIENTATION_VERTICAL
1108 ? GDK_SB_V_DOUBLE_ARROW
1109 : GDK_SB_H_DOUBLE_ARROW);
1110 }
1111 else if(!handle->dragging)
1112 {
1113 gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);
1114 dt_control_change_cursor(GDK_LEFT_PTR);
1115 }
1116
1117 gtk_widget_queue_draw(widget);
1118 return TRUE;
1119}
1120
1121static gboolean _resize_handle_button(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
1122{
1124 if(event->button != 1) return TRUE;
1125
1126 if(event->type == GDK_BUTTON_PRESS)
1127 {
1128 handle->dragging = TRUE;
1129 handle->start_root = (handle->orientation == GTK_ORIENTATION_VERTICAL) ? event->y_root : event->x_root;
1130 handle->start_size = handle->get_size(handle->user_data);
1131 handle->current_size = handle->start_size;
1132 gtk_grab_add(widget);
1133 dt_control_change_cursor(handle->orientation == GTK_ORIENTATION_VERTICAL
1134 ? GDK_SB_V_DOUBLE_ARROW
1135 : GDK_SB_H_DOUBLE_ARROW);
1136 }
1137 else if(event->type == GDK_BUTTON_RELEASE)
1138 {
1139 handle->dragging = FALSE;
1140 gtk_grab_remove(widget);
1141 handle->current_size = handle->resize(handle->current_size, TRUE, handle->user_data);
1142
1143 GtkAllocation allocation;
1144 gtk_widget_get_allocation(widget, &allocation);
1145 const gboolean pointer_on_handle = event->x >= 0. && event->x <= allocation.width
1146 && event->y >= 0. && event->y <= allocation.height;
1147 if(pointer_on_handle)
1148 gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
1149 else
1150 gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);
1151
1152 dt_control_change_cursor(pointer_on_handle
1153 ? (handle->orientation == GTK_ORIENTATION_VERTICAL
1154 ? GDK_SB_V_DOUBLE_ARROW
1155 : GDK_SB_H_DOUBLE_ARROW)
1156 : GDK_LEFT_PTR);
1157 gtk_widget_queue_draw(widget);
1158 }
1159
1160 return TRUE;
1161}
1162
1163static gboolean _resize_handle_motion(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
1164{
1166 if(!handle->dragging) return TRUE;
1167
1168 /* Each motion event is one sample in the GTK pointer stream. Convert only the
1169 * axis matching the resize direction and leave clamping/application to the
1170 * owner callback, which is the code owning the resized widget. */
1171 const double root = (handle->orientation == GTK_ORIENTATION_VERTICAL) ? event->y_root : event->x_root;
1172 double delta = root - handle->start_root;
1173 if(handle->invert) delta = -delta;
1174 const int requested_size = handle->start_size + (int)round(delta);
1175 handle->current_size = handle->resize(requested_size, FALSE, handle->user_data);
1176 return TRUE;
1177}
1178
1179GtkWidget *dt_bauhaus_resize_handle_new(GtkOrientation orientation, gboolean invert, const char *tooltip,
1181 dt_bauhaus_resize_handle_resize_f resize, gpointer user_data)
1182{
1183 // A GtkEventBox is enough: it owns a GdkWindow for pointer events and renders its CSS
1184 // background/border, so the whole hover affordance lives in the stylesheet (.resize-handle)
1185 // with no custom drawing. It is meant to be added as an overlay child on the resized widget.
1186 GtkWidget *handle_widget = gtk_event_box_new();
1187 dt_bauhaus_resize_handle_t *handle = g_malloc0(sizeof(*handle));
1188 handle->orientation = orientation;
1189 handle->invert = invert;
1190 handle->get_size = get_size;
1191 handle->resize = resize;
1192 handle->user_data = user_data;
1193
1194 // Pin the grip to the edge that grows the target when dragged outward, filling the other axis.
1195 // `invert` means the target grows in the negative direction, so the grip sits on the start edge.
1196 // The grab thickness is set here (a CSS min-height/width isn't honoured for an empty overlay
1197 // child -- GTK allocates its ~0px natural size); an edge class is added for hover styling.
1198 GtkStyleContext *ctx = gtk_widget_get_style_context(handle_widget);
1199 gtk_style_context_add_class(ctx, "resize-handle");
1200 if(orientation == GTK_ORIENTATION_VERTICAL)
1201 {
1202 gtk_widget_set_size_request(handle_widget, -1, DT_PIXEL_APPLY_DPI(5));
1203 gtk_widget_set_halign(handle_widget, GTK_ALIGN_FILL);
1204 gtk_widget_set_valign(handle_widget, invert ? GTK_ALIGN_START : GTK_ALIGN_END);
1205 gtk_style_context_add_class(ctx, invert ? "resize-handle-top" : "resize-handle-bottom");
1206 }
1207 else
1208 {
1209 gtk_widget_set_size_request(handle_widget, DT_PIXEL_APPLY_DPI(5), -1);
1210 gtk_widget_set_valign(handle_widget, GTK_ALIGN_FILL);
1211 gtk_widget_set_halign(handle_widget, invert ? GTK_ALIGN_START : GTK_ALIGN_END);
1212 gtk_style_context_add_class(ctx, invert ? "resize-handle-left" : "resize-handle-right");
1213 }
1214
1215 gtk_widget_set_events(handle_widget, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
1216 | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
1217 | GDK_POINTER_MOTION_MASK);
1218 if(!IS_NULL_PTR(tooltip))
1219 gtk_widget_set_tooltip_text(handle_widget, tooltip);
1220
1221 g_object_set_data_full(G_OBJECT(handle_widget), "dt-bauhaus-resize-handle", handle, g_free);
1222 g_signal_connect(G_OBJECT(handle_widget), "button-press-event", G_CALLBACK(_resize_handle_button), handle);
1223 g_signal_connect(G_OBJECT(handle_widget), "button-release-event", G_CALLBACK(_resize_handle_button), handle);
1224 g_signal_connect(G_OBJECT(handle_widget), "motion-notify-event", G_CALLBACK(_resize_handle_motion), handle);
1225 g_signal_connect(G_OBJECT(handle_widget), "enter-notify-event", G_CALLBACK(_resize_handle_cursor), handle);
1226 g_signal_connect(G_OBJECT(handle_widget), "leave-notify-event", G_CALLBACK(_resize_handle_cursor), handle);
1227
1228 return handle_widget;
1229}
1230
1231static void _widget_finalize(GObject *widget)
1232{
1233 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1234
1235 const guint focus_source = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(widget), DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY));
1236 if(focus_source > 0)
1237 {
1238 g_source_remove(focus_source);
1239 g_object_set_data(G_OBJECT(widget), DT_BAUHAUS_FOCUS_IDLE_SOURCE_KEY, NULL);
1240 g_object_set_data(G_OBJECT(widget), DT_BAUHAUS_FOCUS_IDLE_TRIES_KEY, NULL);
1241 }
1242
1243 const char *accel_path = g_object_get_data(G_OBJECT(widget), "accel-path");
1245 dt_accels_remove_accel(darktable.gui->accels, accel_path, widget);
1246
1247 if(darktable.gui && darktable.gui->has_scroll_focus == GTK_WIDGET(w))
1249 dt_gui_throttle_cancel(widget);
1250 if(w->type == DT_BAUHAUS_SLIDER)
1251 {
1253 dt_free(d->grad_col);
1254 dt_free(d->grad_pos);
1255 }
1256 else
1257 {
1259 g_ptr_array_free(d->entries, TRUE);
1260 dt_free(d->text);
1261 }
1262 gtk_border_free(w->margin);
1263 gtk_border_free(w->padding);
1264
1265 G_OBJECT_CLASS(dt_bh_parent_class)->finalize(widget);
1266}
1267
1269{
1270 class->signals[DT_BAUHAUS_VALUE_CHANGED_SIGNAL]
1271 = g_signal_new("value-changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1272 g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1273 class->signals[DT_BAUHAUS_QUAD_PRESSED_SIGNAL]
1274 = g_signal_new("quad-pressed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
1275 g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
1276
1277 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class);
1278 widget_class->draw = _widget_draw;
1279 widget_class->scroll_event = _widget_scroll;
1280 widget_class->key_press_event = _widget_key_press;
1281 widget_class->get_preferred_width = _get_preferred_width;
1282 widget_class->get_preferred_height = _get_preferred_height;
1283 widget_class->enter_notify_event = _enter_leave;
1284 widget_class->leave_notify_event = _enter_leave;
1285 widget_class->style_updated = _style_updated;
1286 G_OBJECT_CLASS(class)->finalize = _widget_finalize;
1287}
1288
1290{
1291 bauhaus->line_height = 16;
1292 bauhaus->marker_size = 0.25f;
1293
1294 GtkWidget *root_window = dt_ui_main_window(darktable.gui->ui);
1295 GtkStyleContext *ctx = gtk_widget_get_style_context(root_window);
1296 gtk_style_context_set_screen(ctx, gtk_widget_get_screen(root_window));
1297
1298 gtk_style_context_lookup_color(ctx, "bauhaus_fg", &bauhaus->color_fg);
1299 gtk_style_context_lookup_color(ctx, "bauhaus_fg_insensitive", &bauhaus->color_fg_insensitive);
1300 gtk_style_context_lookup_color(ctx, "bauhaus_bg", &bauhaus->color_bg);
1301 gtk_style_context_lookup_color(ctx, "bauhaus_border", &bauhaus->color_border);
1302 gtk_style_context_lookup_color(ctx, "bauhaus_fill", &bauhaus->color_fill);
1303 gtk_style_context_lookup_color(ctx, "bauhaus_indicator_border", &bauhaus->indicator_border);
1304 if(!gtk_style_context_lookup_color(ctx, "bauhaus_value", &bauhaus->color_value))
1306 if(!gtk_style_context_lookup_color(ctx, "bauhaus_value_insensitive", &bauhaus->color_value_insensitive))
1308 if(!gtk_style_context_lookup_color(ctx, "bauhaus_value_text", &bauhaus->color_value_text))
1310 if(!gtk_style_context_lookup_color(ctx, "bauhaus_value_text_insensitive", &bauhaus->color_value_text_insensitive))
1312
1313 gtk_style_context_lookup_color(ctx, "graph_bg", &bauhaus->graph_bg);
1314 gtk_style_context_lookup_color(ctx, "graph_exterior", &bauhaus->graph_exterior);
1315 gtk_style_context_lookup_color(ctx, "graph_border", &bauhaus->graph_border);
1316 gtk_style_context_lookup_color(ctx, "graph_grid", &bauhaus->graph_grid);
1317 gtk_style_context_lookup_color(ctx, "graph_fg", &bauhaus->graph_fg);
1318 gtk_style_context_lookup_color(ctx, "graph_fg_active", &bauhaus->graph_fg_active);
1319 gtk_style_context_lookup_color(ctx, "graph_overlay", &bauhaus->graph_overlay);
1320 if(!gtk_style_context_lookup_color(ctx, "graph_scope_restricted", &bauhaus->graph_scope_restricted))
1321 bauhaus->graph_scope_restricted = bauhaus->graph_fg; // For the "restricted" text in the scope
1322 gtk_style_context_lookup_color(ctx, "inset_histogram", &bauhaus->inset_histogram);
1323 gtk_style_context_lookup_color(ctx, "graph_red", &bauhaus->graph_colors[0]);
1324 gtk_style_context_lookup_color(ctx, "graph_green", &bauhaus->graph_colors[1]);
1325 gtk_style_context_lookup_color(ctx, "graph_blue", &bauhaus->graph_colors[2]);
1326 gtk_style_context_lookup_color(ctx, "colorlabel_red",
1328 gtk_style_context_lookup_color(ctx, "colorlabel_yellow",
1330 gtk_style_context_lookup_color(ctx, "colorlabel_green",
1332 gtk_style_context_lookup_color(ctx, "colorlabel_blue",
1334 gtk_style_context_lookup_color(ctx, "colorlabel_purple",
1336
1337
1338 // make sure we release previously loaded font
1340 pango_font_description_free(bauhaus->pango_font_desc);
1341
1342 // Create a scratch Cairo surface to draw on
1343 cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 128, 128);
1344 cairo_t *cr = cairo_create(cst);
1345
1346 // Let Gtk create a fake label and grab its font description
1347 GtkWidget *ref = gtk_label_new(NULL);
1348 ctx = gtk_widget_get_style_context(ref);
1349 gtk_style_context_get(ctx, GTK_STATE_FLAG_NORMAL, "font", &bauhaus->pango_font_desc, NULL);
1350
1351 PangoContext *context = gtk_widget_get_pango_context(ref);
1352 PangoLayout *layout = pango_layout_new(context);
1353 pango_layout_set_text(layout, "em", -1);
1354 pango_layout_set_font_description(layout, bauhaus->pango_font_desc);
1355 //double dpi = pango_cairo_context_get_resolution(context);
1356 //double scale = gtk_widget_get_scale_factor(ref);
1357 //pango_cairo_context_set_resolution(context, dpi * scale);
1358
1359 // Get advanced font options (system anti-aliasing/hinting/subpixel/kerning)
1361
1362 // Render
1363 pango_cairo_update_layout(cr, layout);
1364 pango_cairo_show_layout(cr, layout);
1365
1366 // Measure the text line height
1367 int pango_width;
1368 int pango_height;
1369 pango_layout_get_size(layout, &pango_width, &pango_height);
1370 g_object_unref(layout);
1371 cairo_destroy(cr);
1372 cairo_surface_destroy(cst);
1373
1374 bauhaus->line_height = pango_height / PANGO_SCALE;
1376
1377 bauhaus->baseline_size = 5; // absolute size in Cairo unit
1378 bauhaus->border_width = 1; // absolute size in Cairo unit
1379 bauhaus->marker_size = pango_height / PANGO_SCALE * 0.6;
1380}
1381
1383{
1384 dt_bauhaus_t * bauhaus = (dt_bauhaus_t *)calloc(1, sizeof(dt_bauhaus_t));
1385 bauhaus->keys_cnt = 0;
1386 bauhaus->current = NULL;
1387 bauhaus->popup_area = gtk_drawing_area_new();
1388 bauhaus->pango_font_desc = NULL;
1389
1391
1392 // this easily gets keyboard input:
1393 // bauhaus->popup_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
1394 // but this doesn't flicker, and the above hack with key input seems to work well.
1395 bauhaus->popup_window = gtk_window_new(GTK_WINDOW_POPUP);
1396#ifdef GDK_WINDOWING_QUARTZ
1398#endif
1399 // this is needed for popup, not for toplevel.
1400 // since popup_area gets the focus if we show the window, this is all
1401 // we need.
1402
1403 gtk_window_set_resizable(GTK_WINDOW(bauhaus->popup_window), FALSE);
1404 gtk_window_set_default_size(GTK_WINDOW(bauhaus->popup_window), 30, 30);
1405 gtk_window_set_modal(GTK_WINDOW(bauhaus->popup_window), TRUE);
1406
1407 // Needed for Wayland and Sway :
1408 gtk_window_set_transient_for(GTK_WINDOW(bauhaus->popup_window),
1409 GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
1410
1411 gtk_window_set_decorated(GTK_WINDOW(bauhaus->popup_window), FALSE);
1412 gtk_window_set_attached_to(GTK_WINDOW(bauhaus->popup_window), NULL);
1413
1414 // needed on macOS to avoid fullscreening the popup with newer GTK
1415 gtk_window_set_type_hint(GTK_WINDOW(bauhaus->popup_window), GDK_WINDOW_TYPE_HINT_POPUP_MENU);
1416
1417 gtk_container_add(GTK_CONTAINER(bauhaus->popup_window), bauhaus->popup_area);
1418 gtk_widget_set_hexpand(bauhaus->popup_area, TRUE);
1419 gtk_widget_set_vexpand(bauhaus->popup_area, TRUE);
1420 gtk_window_set_keep_above(GTK_WINDOW(bauhaus->popup_window), TRUE);
1421 gtk_window_set_gravity(GTK_WINDOW(bauhaus->popup_window), GDK_GRAVITY_STATIC);
1422
1423 gtk_widget_set_can_focus(bauhaus->popup_area, TRUE);
1424 gtk_widget_add_events(bauhaus->popup_area, GDK_POINTER_MOTION_MASK
1425 | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
1426 | GDK_KEY_PRESS_MASK | GDK_LEAVE_NOTIFY_MASK
1428
1429 GObject *window = G_OBJECT(bauhaus->popup_window);
1430 GObject *area = G_OBJECT(bauhaus->popup_area);
1431 g_object_set_data(area, "bauhaus", bauhaus);
1432
1433 g_signal_connect(window, "show", G_CALLBACK(dt_bauhaus_window_show), area);
1434 g_signal_connect(area, "draw", G_CALLBACK(dt_bauhaus_popup_draw), NULL);
1435 g_signal_connect(area, "motion-notify-event", G_CALLBACK(dt_bauhaus_popup_motion_notify), NULL);
1436 g_signal_connect(area, "leave-notify-event", G_CALLBACK(dt_bauhaus_popup_leave_notify), NULL);
1437 g_signal_connect(area, "button-press-event", G_CALLBACK(dt_bauhaus_popup_button_press), NULL);
1438 g_signal_connect(area, "button-release-event", G_CALLBACK (dt_bauhaus_popup_button_release), NULL);
1439 g_signal_connect(area, "key-press-event", G_CALLBACK(dt_bauhaus_popup_key_press), NULL);
1440 g_signal_connect(area, "scroll-event", G_CALLBACK(dt_bauhaus_popup_scroll), NULL);
1441
1442 // Keys used by key-pressed event handler when the Bauhaus widget has the focus
1443 gchar *path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Increase value (normal step)"));
1445 path, NULL, GDK_KEY_Right, 0);
1446 dt_free(path);
1447 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Decrease value (normal step)"));
1449 path, NULL, GDK_KEY_Left, 0);
1450 dt_free(path);
1451 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Increase value (fine step)"));
1453 path, NULL, GDK_KEY_Right, GDK_CONTROL_MASK);
1454 dt_free(path);
1455 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Decrease value (fine step)"));
1457 path, NULL, GDK_KEY_Left, GDK_CONTROL_MASK);
1458 dt_free(path);
1459 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Increase value (coarse step)"));
1461 path, NULL, GDK_KEY_Right, GDK_SHIFT_MASK);
1462 dt_free(path);
1463 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Decrease value (coarse step)"));
1465 path, NULL, GDK_KEY_Left, GDK_SHIFT_MASK);
1466 dt_free(path);
1467 path = dt_accels_build_path(_("Darkroom/Controls/Sliders"), _("Toggle color-picker"));
1469 path, NULL, GDK_KEY_Insert, 0);
1470 dt_free(path);
1471
1472 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Open editing mode"));
1474 path, NULL, GDK_KEY_Return, 0);
1475 dt_free(path);
1476 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Exit editing mode"));
1478 path, NULL, GDK_KEY_Escape, 0);
1479 dt_free(path);
1480 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Select previous (in editing mode)"));
1482 path, NULL, GDK_KEY_Up, 0);
1483 dt_free(path);
1484 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Select next (in editing mode)"));
1486 path, NULL, GDK_KEY_Down, 0);
1487 dt_free(path);
1488 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Validate result (in editing mode)"));
1490 path, NULL, GDK_KEY_Return, 0);
1491 dt_free(path);
1492 path = dt_accels_build_path(_("Darkroom/Controls/Comboboxes"), _("Toggle color-picker"));
1494 path, NULL, GDK_KEY_Insert, 0);
1495 dt_free(path);
1496
1497 return bauhaus;
1498}
1499
1503
1504// fwd declare a few callbacks
1505static gboolean dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
1506static gboolean dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
1507static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data);
1508
1509
1510static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
1511
1512// end static init/cleanup
1513// =================================================
1514
1515
1516
1517// common initialization
1519{
1520 w->module = self;
1521 w->field = NULL;
1522
1523 w->no_accels = FALSE;
1524 w->no_module_list = FALSE;
1525 w->bauhaus = bauhaus;
1527
1528 // no quad icon and no toggle button:
1529 w->quad_paint = 0;
1530 w->quad_paint_data = NULL;
1531 w->quad_toggle = 0;
1532 w->show_quad = TRUE;
1533 w->expand = TRUE;
1534
1535 gtk_widget_add_events(GTK_WIDGET(w), GDK_POINTER_MOTION_MASK
1536 | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
1537 | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
1538 | GDK_FOCUS_CHANGE_MASK
1540
1541 gtk_widget_set_can_focus(GTK_WIDGET(w), TRUE);
1542 gtk_widget_set_halign(GTK_WIDGET(w), GTK_ALIGN_FILL);
1543 gtk_widget_set_hexpand(GTK_WIDGET(w), TRUE);
1544 g_signal_connect(G_OBJECT(w), "focus-in-event", G_CALLBACK(dt_bauhaus_focus_in_callback), NULL);
1545 g_signal_connect(G_OBJECT(w), "focus-out-event", G_CALLBACK(dt_bauhaus_focus_out_callback), NULL);
1546 g_signal_connect(G_OBJECT(w), "focus", G_CALLBACK(dt_bauhaus_focus_callback), NULL);
1547
1548 dt_gui_add_class(GTK_WIDGET(w), "dt_bauhaus");
1549}
1550
1552{
1553 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1555 d->defpos = def;
1556}
1557
1558
1560{
1561 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1563 float current_position = dt_bauhaus_slider_get(widget);
1564 float desired_position = _bh_round_to_n_digits(w, val);
1565 d->min = MAX(d->min, d->hard_min);
1566 d->soft_min = MAX(d->soft_min, d->hard_min);
1567
1568 if(desired_position > d->hard_max)
1570
1571 if(current_position < desired_position)
1572 dt_bauhaus_slider_set(widget, desired_position);
1573 // else nothing : old position is the new position, just the bound changes
1574}
1575
1577{
1578 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1580 return d->hard_min;
1581}
1582
1584{
1585 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1587 float current_position = dt_bauhaus_slider_get(widget);
1588 float desired_position = _bh_round_to_n_digits(w, val);
1589 d->hard_max = desired_position;
1590 d->max = MIN(d->max, d->hard_max);
1591 d->soft_max = MIN(d->soft_max, d->hard_max);
1592
1593 if(desired_position < d->hard_min)
1594 dt_bauhaus_slider_set_hard_min(widget, desired_position);
1595
1596 if(current_position > desired_position)
1597 dt_bauhaus_slider_set(widget, desired_position);
1598 // else nothing : old position is the new position, just the bound changes
1599}
1600
1602{
1603 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1605 return d->hard_max;
1606}
1607
1609{
1610 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1612 float oldval = dt_bauhaus_slider_get(widget);
1613 d->min = d->soft_min = CLAMP(val, d->hard_min, d->hard_max);
1614 dt_bauhaus_slider_set(widget, oldval);
1615}
1616
1618{
1619 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1621 return d->soft_min;
1622}
1623
1625{
1626 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1628 float oldval = dt_bauhaus_slider_get(widget);
1629 d->max = d->soft_max = CLAMP(val, d->hard_min, d->hard_max);
1630 dt_bauhaus_slider_set(widget, oldval);
1631}
1632
1634{
1635 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1637 return d->soft_max;
1638}
1639
1641{
1642 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1644 d->defpos = def;
1645}
1646
1647void dt_bauhaus_slider_set_soft_range(GtkWidget *widget, float soft_min, float soft_max)
1648{
1649 dt_bauhaus_slider_set_soft_min(widget, soft_min);
1650 dt_bauhaus_slider_set_soft_max(widget, soft_max);
1651}
1652
1654{
1655 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1656 if(label)
1657 {
1658 g_strlcpy(w->label, label, sizeof(w->label));
1660 }
1661
1662 if(w->module)
1663 {
1664 // Widgets auto-set by params introspection need to be added to the list of stuff to auto-update
1665 dt_gui_module_t *m = w->module;
1666 if(m && !w->no_module_list)
1667 m->widget_list = g_list_append(m->widget_list, w);
1668
1669 if(m && w->field && !w->no_module_list)
1670 m->widget_list_bh = g_list_append(m->widget_list_bh, w);
1671
1672 // Wire the focusing action
1673 // Note: once the focus is grabbed, interaction with the widget happens through arrow keys or mouse wheel.
1674 // No need to wire all possible events/interactions.
1675 if(m && !w->no_accels && !w->module->deprecated && label)
1676 {
1677 // slash is not allowed in control names because that makes accel pathes fail
1678 assert(g_strrstr(label, "/") == NULL);
1679
1680 gchar *plugin_name = g_strdup_printf("%s/%s", m->name, w->label);
1681 dt_capitalize_label(plugin_name);
1682
1683 gchar *scope = g_strdup_printf("%s/Modules", m->view);
1684 dt_accels_new_darkroom_action(_action_request_focus, widget, scope, plugin_name, 0, 0, _("Focuses the control"));
1685 g_object_set_data_full(G_OBJECT(widget), "accel-path",
1686 dt_accels_build_path("Darkroom/Modules", plugin_name), dt_free_gpointer);
1687 gtk_widget_set_has_tooltip(widget, TRUE);
1688 dt_free(scope);
1689 dt_free(plugin_name);
1690 }
1691
1692 gtk_widget_queue_draw(GTK_WIDGET(w));
1693 }
1694}
1695
1697{
1698 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1699 return w->label;
1700}
1701
1702void dt_bauhaus_widget_set_quad_paint(GtkWidget *widget, dt_bauhaus_quad_paint_f f, int paint_flags, void *paint_data)
1703{
1704 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1705 w->quad_paint = f;
1706 w->quad_paint_flags = paint_flags;
1707 w->quad_paint_data = paint_data;
1708}
1709
1711{
1712 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1713 if(*w->label)
1714 fprintf(stderr, "[dt_bauhaus_widget_set_field] bauhaus label '%s' set before field (needs to be after)\n", w->label);
1715 w->field = field;
1717}
1718
1719// make this quad a toggle button:
1721{
1722 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1723 w->quad_toggle = toggle;
1724}
1725
1727{
1728 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1729 if (active)
1731 else
1732 w->quad_paint_flags &= ~CPF_ACTIVE;
1733 gtk_widget_queue_draw(GTK_WIDGET(w));
1734}
1735
1736void dt_bauhaus_widget_set_quad_visibility(GtkWidget *widget, const gboolean visible)
1737{
1738 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1739 w->show_quad = visible;
1740 gtk_widget_queue_draw(GTK_WIDGET(w));
1741}
1742
1744{
1745 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1746 return (w->quad_paint_flags & CPF_ACTIVE) == CPF_ACTIVE;
1747}
1748
1750{
1751 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1752 if(w->quad_toggle)
1753 {
1755 }
1756 else
1758
1759 g_signal_emit_by_name(G_OBJECT(w), "quad-pressed");
1760}
1761
1763{
1764 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1765 gtk_widget_grab_focus(widget);
1766
1767 if(!w->quad_toggle)
1768 {
1770 w->quad_paint_flags &= ~CPF_ACTIVE;
1771 gtk_widget_queue_draw(GTK_WIDGET(w));
1772 }
1773}
1774
1776{
1777 return dt_bauhaus_slider_new_with_range(bh, self, 0.0, 1.0, 0.1, 0.5, 3);
1778}
1779
1781 float defval, int digits)
1782{
1783 return dt_bauhaus_slider_new_with_range_and_feedback(bh, self, min, max, step, defval, digits, 1);
1784}
1785
1787 float step, float defval, int digits, int feedback)
1788{
1789 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
1790 return dt_bauhaus_slider_from_widget(bh, w,self, min, max, step, defval, digits, feedback);
1791}
1792
1793static void _style_updated(GtkWidget *widget)
1794{
1795 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
1797
1798 // gtk_widget_set_size_request is the minimal preferred, size.
1799 // it NEEDS to be defined and will be contextually adapted, possibly overriden by CSS.
1800 // Thing is Gtk CSS min-width in combination with hexpand is wonky so this is how it should be done.
1801 if(w->type == DT_BAUHAUS_COMBOBOX)
1802 gtk_widget_set_size_request(widget, -1, _get_combobox_height(widget));
1803 else if(w->type == DT_BAUHAUS_SLIDER)
1804 gtk_widget_set_size_request(widget, -1, _get_slider_height(widget));
1805}
1806
1808 float step, float defval, int digits, int feedback)
1809{
1811 _bauhaus_widget_init(bh, w, self);
1813 d->min = d->soft_min = d->hard_min = min;
1814 d->max = d->soft_max = d->hard_max = max;
1815 d->step = step;
1816 // normalize default:
1817 d->defpos = defval;
1818 d->pos = (defval - min) / (max - min);
1819 d->oldpos = d->pos;
1820 d->digits = digits;
1821 d->format = "";
1822 d->factor = 1.0f;
1823 d->offset = 0.0f;
1824
1825 d->grad_cnt = 0;
1826 d->grad_col = NULL;
1827 d->grad_pos = NULL;
1828
1829 d->fill_feedback = feedback;
1830
1831 d->is_dragging = 0;
1832
1833 dt_gui_add_class(GTK_WIDGET(w), "bauhaus_slider");
1834
1835 g_signal_connect(G_OBJECT(w), "button-press-event", G_CALLBACK(dt_bauhaus_slider_button_press), NULL);
1836 g_signal_connect(G_OBJECT(w), "button-release-event", G_CALLBACK(dt_bauhaus_slider_button_release), NULL);
1837 g_signal_connect(G_OBJECT(w), "motion-notify-event", G_CALLBACK(dt_bauhaus_slider_motion_notify), NULL);
1838
1839 return GTK_WIDGET(w);
1840}
1841
1843{
1844 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
1846 return GTK_WIDGET(w);
1847}
1848
1850 int pos, GtkCallback callback, gpointer data, const char **texts)
1851{
1852 GtkWidget *combo = dt_bauhaus_combobox_new(bh, self);
1854 dt_bauhaus_combobox_add_list(combo, texts);
1855 dt_bauhaus_combobox_set(combo, pos);
1856 gtk_widget_set_tooltip_text(combo, tip ? tip : _(label));
1857 if(callback) g_signal_connect(G_OBJECT(combo), "value-changed", G_CALLBACK(callback), data);
1858
1859 return combo;
1860}
1861
1862static void _combobox_conf_value_changed(GtkWidget *widget, gpointer user_data)
1863{
1864 const char *value = (const char *)dt_bauhaus_combobox_get_data(widget);
1865 if(value) dt_conf_set_string((const char *)user_data, value);
1866}
1867
1869{
1870 if(dt_confgen_type(confkey) != DT_ENUM || !dt_confgen_value_exists(confkey, DT_VALUES))
1871 {
1872 fprintf(stderr, "[dt_bauhaus_combobox_from_conf] `%s` is not declared as an <enum> config entry\n", confkey);
1873 return NULL;
1874 }
1875
1876 GtkWidget *combo = dt_bauhaus_combobox_new(bh, self);
1878
1879 const char *tooltip = dt_confgen_get_tooltip(confkey);
1880 gtk_widget_set_tooltip_text(combo, (tooltip && *tooltip) ? _(tooltip) : _(dt_confgen_get_label(confkey)));
1881
1882 gchar *current = dt_conf_get_string(confkey);
1883 const char *values = dt_confgen_get(confkey, DT_VALUES);
1884 GList *options = dt_util_str_to_glist("][", values);
1885
1886 int pos = 0, active = 0;
1887 for(GList *opt = options; opt; opt = g_list_next(opt))
1888 {
1889 char *item = (char *)opt->data;
1890 // strip the leading '[' of the first entry and the trailing ']' of the last one
1891 if(item[0] == '[') item++;
1892 else if(item[strlen(item) - 1] == ']') item[strlen(item) - 1] = '\0';
1893
1894 dt_bauhaus_combobox_add_full(combo, g_dpgettext2(NULL, "preferences", item), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT,
1895 g_strdup(item), dt_free_gpointer, TRUE);
1896
1897 if(!g_strcmp0(current, item)) active = pos;
1898 pos++;
1899 }
1900 g_list_free_full(options, dt_free_gpointer);
1901 dt_free(current);
1902
1903 // Select the entry matching the current config value before connecting the signal,
1904 // so the initial sync doesn't bounce back through dt_conf_set_string().
1905 dt_bauhaus_combobox_set(combo, active);
1906
1907 g_signal_connect(G_OBJECT(combo), "value-changed", G_CALLBACK(_combobox_conf_value_changed), (gpointer)confkey);
1908
1909 return combo;
1910}
1911
1913{
1915 _bauhaus_widget_init(bh, w, self);
1917 d->entries = g_ptr_array_new_full(4, free_combobox_entry);
1918 d->defpos = 0;
1919 d->active = -1;
1920 d->editable = 0;
1921 d->text_align = DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT;
1922 d->entries_ellipsis = PANGO_ELLIPSIZE_END;
1923 d->populate = NULL;
1924 d->text = NULL;
1925
1926 dt_gui_add_class(GTK_WIDGET(w), "bauhaus_combobox");
1927
1928 g_signal_connect(G_OBJECT(w), "button-press-event", G_CALLBACK(dt_bauhaus_combobox_button_press), NULL);
1929}
1930
1932{
1933 if(IS_NULL_PTR(widget)) return NULL;
1934 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1935 if(w->type != DT_BAUHAUS_COMBOBOX) return NULL;
1937
1938 if(d->active >= d->entries->len) d->active = -1;
1939
1940 return d;
1941}
1942
1950{
1951 int count = 0;
1952 for(int i = 0; !IS_NULL_PTR(d) && i < d->entries->len; i++)
1953 {
1954 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
1955 if(!entry->is_separator) count++;
1956 }
1957 return count;
1958}
1959
1967 const gboolean allow_end)
1968{
1969 if(IS_NULL_PTR(d) || pos < 0) return -1;
1970
1971 int selectable_pos = 0;
1972 for(int i = 0; i < d->entries->len; i++)
1973 {
1974 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
1975 if(entry->is_separator) continue;
1976 if(selectable_pos == pos) return i;
1977 selectable_pos++;
1978 }
1979
1980 return (allow_end && selectable_pos == pos) ? (int)d->entries->len : -1;
1981}
1982
1986static int _combobox_entry_pos_to_public(const dt_bauhaus_combobox_data_t *d, const int entry_pos)
1987{
1988 if(IS_NULL_PTR(d) || entry_pos < 0 || entry_pos >= d->entries->len) return -1;
1989
1990 int selectable_pos = 0;
1991 for(int i = 0; i <= entry_pos; i++)
1992 {
1993 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
1994 if(entry->is_separator) continue;
1995 if(i == entry_pos) return selectable_pos;
1996 selectable_pos++;
1997 }
1998
1999 return -1;
2000}
2001
2002void dt_bauhaus_combobox_add_populate_fct(GtkWidget *widget, void (*fct)(GtkWidget *w, void *module))
2003{
2004 if(IS_NULL_PTR(widget)) return;
2005 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2006 if(w->type == DT_BAUHAUS_COMBOBOX)
2007 w->data.combobox.populate = fct;
2008}
2009
2010void dt_bauhaus_combobox_add_list(GtkWidget *widget, const char **texts)
2011{
2012 while(texts && *texts)
2013 dt_bauhaus_combobox_add_full(widget, _(*(texts++)), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT, NULL, NULL, TRUE);
2014}
2015
2016void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
2017{
2019}
2020
2021void dt_bauhaus_combobox_add_with_tooltip(GtkWidget *widget, const char *text, const char *tooltip)
2022{
2023 if(IS_NULL_PTR(widget)) return;
2024 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2025 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2029 g_ptr_array_add(d->entries, entry);
2030 if(d->active < 0) d->active = (int)d->entries->len - 1;
2031}
2032
2034{
2035 dt_bauhaus_combobox_add_full(widget, text, align, NULL, NULL, TRUE);
2036}
2037
2039 gpointer data, void (free_func)(void *data), gboolean sensitive)
2040{
2041 if(IS_NULL_PTR(widget)) return;
2042 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2043 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2045 dt_bauhaus_combobox_entry_t *entry = new_combobox_entry(text, NULL, align, sensitive, data, free_func);
2046 g_ptr_array_add(d->entries, entry);
2047 if(d->active < 0) d->active = (int)d->entries->len - 1;
2048}
2049
2054
2055void dt_bauhaus_combobox_add_separator_with_height(GtkWidget *widget, const float row_height_factor)
2056{
2057 if(IS_NULL_PTR(widget)) return;
2058 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2059 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2061 g_ptr_array_add(d->entries, new_combobox_separator(row_height_factor));
2062}
2063
2064void dt_bauhaus_combobox_set_entries_ellipsis(GtkWidget *widget, PangoEllipsizeMode ellipis)
2065{
2066 if(IS_NULL_PTR(widget)) return;
2067 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2068 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2070 d->entries_ellipsis = ellipis;
2071}
2072
2073
2075{
2076 if(IS_NULL_PTR(widget)) return;
2077 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2078 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2080 d->editable = editable ? 1 : 0;
2081 if(d->editable && IS_NULL_PTR(d->text))
2082 d->text = calloc(1, DT_BAUHAUS_COMBO_MAX_TEXT);
2083}
2084
2086{
2088
2089 return d ? d->editable : 0;
2090}
2091
2093{
2094 if(IS_NULL_PTR(widget)) return;
2095 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2096 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2098 d->text_align = text_align;
2099}
2100
2102{
2104 if(IS_NULL_PTR(d)) return;
2105 const int entry_pos = _combobox_public_to_entry_pos(d, pos, FALSE);
2106
2107 if(entry_pos < 0) return;
2108
2109 // move active position up if removing anything before it
2110 // or when removing last position that is currently active.
2111 // this also sets active to -1 when removing the last remaining entry in a combobox.
2112 if(d->active > entry_pos || d->active == d->entries->len-1)
2113 d->active--;
2114
2115 g_ptr_array_remove_index(d->entries, entry_pos);
2116}
2117
2118void dt_bauhaus_combobox_insert(GtkWidget *widget, const char *text,int pos)
2119{
2121}
2122
2124 gpointer data, void (*free_func)(void *), int pos)
2125{
2126 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2127 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2129 const int entry_pos = _combobox_public_to_entry_pos(d, pos, TRUE);
2130 if(entry_pos < 0) return;
2131 g_ptr_array_insert(d->entries, entry_pos, new_combobox_entry(text, NULL, align, TRUE, data, free_func));
2132 if(d->active < 0)
2133 d->active = entry_pos;
2134 else if(entry_pos <= d->active)
2135 d->active++;
2136}
2137
2142
2143void dt_bauhaus_combobox_insert_separator_with_height(GtkWidget *widget, int pos, const float row_height_factor)
2144{
2145 if(IS_NULL_PTR(widget)) return;
2146 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2147 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2149 const int entry_pos = _combobox_public_to_entry_pos(d, pos, TRUE);
2150 if(entry_pos < 0) return;
2151 g_ptr_array_insert(d->entries, entry_pos, new_combobox_separator(row_height_factor));
2152 if(entry_pos <= d->active) d->active++;
2153}
2154
2156{
2158
2160}
2161
2163{
2165 if(IS_NULL_PTR(d)) return NULL;
2166
2167 if(d->active < 0)
2168 {
2169 return d->editable ? d->text : NULL;
2170 }
2171 else
2172 {
2173 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, d->active);
2174 if(entry->is_separator) return NULL;
2175 return entry->label;
2176 }
2177}
2178
2180{
2182 if(IS_NULL_PTR(d) || d->active < 0) return NULL;
2183
2184 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, d->active);
2185 if(entry->is_separator) return NULL;
2186 return entry->data;
2187}
2188
2190{
2191 if(IS_NULL_PTR(widget)) return;
2192 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2193 if(w->type != DT_BAUHAUS_COMBOBOX) return;
2195 d->active = -1;
2196 g_ptr_array_set_size(d->entries, 0);
2197}
2198
2199const char *dt_bauhaus_combobox_get_entry(GtkWidget *widget, int pos)
2200{
2202 const int entry_pos = _combobox_public_to_entry_pos(d, pos, FALSE);
2203 if(entry_pos < 0) return NULL;
2204
2205 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, entry_pos);
2206 return entry->label;
2207}
2208
2209void dt_bauhaus_combobox_set_text(GtkWidget *widget, const char *text)
2210{
2212 if(IS_NULL_PTR(d) || !d->editable) return;
2213
2214 g_strlcpy(d->text, text, DT_BAUHAUS_COMBO_MAX_TEXT);
2215}
2216
2217
2218static void _delayed_combobox_commit(gpointer data)
2219{
2220 // Commit combobox value change to pipeline history, handling a safety timout
2221 // so incremental scrollings don't trigger a recompute at every scroll step.
2222 struct dt_bauhaus_widget_t *w = data;
2223
2224 // If a reset started after the timeout was scheduled (e.g. while reloading history,
2225 // applying a style, etc.), don't commit anything to history from this stale callback.
2226 if(darktable.gui && darktable.gui->reset) return;
2227
2229 {
2231 w->bauhaus->default_value_changed_callback(GTK_WIDGET(w));
2232 else
2233 fprintf(stderr, "ERROR: %s - %s is set to use default callback but none is provided\n", w->module->name,
2234 w->label);
2235 }
2236 else
2237 {
2238 if(w->module)
2239 fprintf(stderr, "WARNING: %s - %s has an IOP module but doesn't use default callback\n", w->module->name,
2240 w->label);
2241 }
2242
2243 // We need te emit this signal inconditionnaly
2244 g_signal_emit_by_name(G_OBJECT(w), "value-changed");
2245}
2246
2256void _combobox_set(GtkWidget *widget, const int pos, gboolean timeout)
2257{
2258 if(IS_NULL_PTR(widget)) return;
2259 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2261 const int old_pos = d->active;
2262 const int new_pos = (d->entries) ? CLAMP(pos, -1, (int)d->entries->len - 1)
2263 : -1;
2264 if(new_pos >= 0)
2265 {
2266 // if the new position is a separator, don't change the active position but still redraw to update the visual selection
2267 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, new_pos);
2268 if(entry->is_separator) return;
2269 }
2270
2271 // When updating programmatically (GUI reset), ensure no delayed commit from a
2272 // previous user interaction survives, even if the value doesn't change.
2274
2275 if(old_pos != new_pos)
2276 {
2277 d->active = new_pos;
2278
2279 if(w->bauhaus->current == w)
2280 gtk_widget_queue_draw(w->bauhaus->popup_area);
2281
2282 gtk_widget_queue_draw(GTK_WIDGET(w));
2283
2284 // If a delayed commit is pending from a previous user interaction, cancel it.
2285 // This is especially important when updating widgets programmatically (during GUI reset),
2286 // as we don't want stale timeouts to later emit "value-changed" and commit to history.
2287 if(!darktable.gui->reset)
2288 {
2289 if(timeout)
2291 else
2292 {
2293 dt_gui_throttle_cancel(widget);
2295 }
2296 }
2297 }
2298}
2299
2300// Public API function, called from GUI init and update
2301void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
2302{
2304 const int selectable_count = _combobox_selectable_count(d);
2305 const int public_pos = (selectable_count > 0) ? CLAMP(pos, -1, selectable_count - 1) : -1;
2306 const int entry_pos = _combobox_public_to_entry_pos(d, public_pos, FALSE);
2307 _combobox_set(widget, entry_pos, FALSE);
2308}
2309
2310
2311gboolean dt_bauhaus_combobox_set_from_text(GtkWidget *widget, const char *text)
2312{
2313 if(IS_NULL_PTR(text)) return FALSE;
2314
2316
2317 for(int i = 0; d && i < d->entries->len; i++)
2318 {
2319 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
2320 if(entry->is_separator) continue;
2321 if(!g_strcmp0(entry->label, text))
2322 {
2323 _combobox_set(widget, i, FALSE);
2324 return TRUE;
2325 }
2326 }
2327 return FALSE;
2328}
2329
2331{
2333
2334 for(int i = 0; d && i < d->entries->len; i++)
2335 {
2336 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
2337 if(entry->is_separator) continue;
2338 if(GPOINTER_TO_INT(entry->data) == value)
2339 {
2340 _combobox_set(widget, i, FALSE);
2341 return TRUE;
2342 }
2343 }
2344 return FALSE;
2345}
2346
2348{
2350
2351 return _combobox_entry_pos_to_public(d, d ? d->active : -1);
2352}
2353
2354void dt_bauhaus_combobox_entry_set_sensitive(GtkWidget *widget, int pos, gboolean sensitive)
2355{
2357 const int entry_pos = _combobox_public_to_entry_pos(d, pos, FALSE);
2358 if(entry_pos < 0) return;
2359
2360 dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)g_ptr_array_index(d->entries, entry_pos);
2361 entry->sensitive = sensitive;
2362}
2363
2365{
2366 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2367 if(w->type != DT_BAUHAUS_SLIDER) return;
2369 d->grad_cnt = 0;
2370}
2371
2372void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, float b)
2373{
2374 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2375 if(w->type != DT_BAUHAUS_SLIDER) return;
2377
2378 if(IS_NULL_PTR(d->grad_col))
2379 {
2380 d->grad_col = malloc(DT_BAUHAUS_SLIDER_MAX_STOPS * sizeof(*d->grad_col));
2381 d->grad_pos = malloc(DT_BAUHAUS_SLIDER_MAX_STOPS * sizeof(*d->grad_pos));
2382 }
2383 // need to replace stop?
2384 for(int k = 0; k < d->grad_cnt; k++)
2385 {
2386 if(d->grad_pos[k] == stop)
2387 {
2388 d->grad_col[k][0] = r;
2389 d->grad_col[k][1] = g;
2390 d->grad_col[k][2] = b;
2391 return;
2392 }
2393 }
2394 // new stop:
2395 if(d->grad_cnt < DT_BAUHAUS_SLIDER_MAX_STOPS)
2396 {
2397 int k = d->grad_cnt++;
2398 d->grad_pos[k] = stop;
2399 d->grad_col[k][0] = r;
2400 d->grad_col[k][1] = g;
2401 d->grad_col[k][2] = b;
2402 }
2403 else
2404 {
2405 fprintf(stderr, "[bauhaus_slider_set_stop] only %d stops allowed.\n", DT_BAUHAUS_SLIDER_MAX_STOPS);
2406 }
2407}
2408
2409static void dt_bauhaus_draw_indicator(struct dt_bauhaus_widget_t *w, float pos, cairo_t *cr, float wd)
2410{
2411 // draw scale indicator (the tiny triangle)
2412 const float size = w->bauhaus->marker_size;
2413 const float vertical_position = _get_indicator_y_position(w);
2414 const float horizontal_position = pos * wd;
2415
2416 cairo_save(cr);
2417 cairo_translate(cr, horizontal_position, vertical_position);
2418
2420 const gboolean has_colored_background = d->grad_cnt > 0;
2421
2422 if(!has_colored_background && d->fill_feedback)
2423 {
2424 // Plain indicator (regular sliders)
2425 cairo_arc(cr, 0, 0, size / 2., 0, M_PI * 2);
2426 set_color(cr, w->bauhaus->color_fg);
2427 cairo_set_line_width(cr, 0);
2428 cairo_fill(cr);
2429 }
2430 else
2431 {
2432 // Hollow indicator to keep the slider background visible through the marker.
2433 const float border = (size - w->bauhaus->baseline_size) / 2.;
2434 set_color(cr, w->bauhaus->color_fg);
2435 cairo_set_line_width(cr, border);
2436 const float radius = size / 2. - border / 2.;
2437 cairo_arc(cr, 0, 0, radius, 0, M_PI * 2);
2438 cairo_stroke(cr);
2439 }
2440 cairo_restore(cr);
2441}
2442
2443static void dt_bauhaus_draw_quad(struct dt_bauhaus_widget_t *w, cairo_t *cr, const double x, const double y)
2444{
2445 if(!w->show_quad) return;
2446
2447 cairo_save(cr);
2448 if(w->quad_paint)
2449 {
2450 // draw color picker
2451 w->quad_paint(cr, x, y,
2452 w->bauhaus->quad_width, // width
2453 w->bauhaus->quad_width, // height
2455 }
2456 else if(w->type == DT_BAUHAUS_COMBOBOX)
2457 {
2458 // draw combobox chevron
2459 // Inset the chevron from the right edge of the (now flush) quad box so it
2460 // doesn't crowd or overflow the widget border. The quad box itself is placed
2461 // identically to the slider's, so this keeps the chevron in line with slider
2462 // quad icons when widgets are stacked.
2463 // FIXME: Why do we need an extra half-pixel shift to the left for pixel-correctness ?
2464 cairo_translate(cr, x + w->bauhaus->quad_width / 2. - 0.5, y + w->bauhaus->line_height / 2.);
2465 const float r = w->bauhaus->quad_width * .2f;
2466 cairo_move_to(cr, -r, -r * .5f);
2467 cairo_line_to(cr, 0, r * .5f);
2468 cairo_line_to(cr, r, -r * .5f);
2469 cairo_stroke(cr);
2470 }
2471 cairo_restore(cr);
2472}
2473
2481static void dt_bauhaus_draw_baseline(struct dt_bauhaus_widget_t *w, cairo_t *cr, float width)
2482{
2483 // draw line for orientation in slider
2484 cairo_save(cr);
2486 const gboolean has_colored_background = d->grad_cnt > 0;
2487 const float baseline_top = w->bauhaus->line_height + INNER_PADDING;
2488 const float baseline_height = w->bauhaus->baseline_size;
2489 const gboolean is_sensitive = gtk_widget_is_sensitive(GTK_WIDGET(w));
2490
2491 // the background of the line
2492 // Note: to ensure the indicator is not clipped on the left side,
2493 // we push the whole slider of an indicator radius to the right.
2494 // Issue is that leaves a dent on the baseline, so the trick
2495 // is to make it overlap back by a radius on the left.
2496 const double x_origin = -w->bauhaus->marker_size / 3.;
2497 // The left edge was pushed by x_origin to overlap the indicator's leftmost
2498 // resting position. Extend the width by the same amount so the right edge
2499 // stays at `width` (flush with the right-aligned numeric value) instead of
2500 // retreating by |x_origin| and letting the value overhang the baseline.
2501 const double baseline_width = width - x_origin;
2502 // Make sure we use integer coordinates to limit anti-aliasing blur
2503 cairo_rectangle(cr, round(x_origin), round(baseline_top),
2504 round(baseline_width), round(baseline_height));
2505
2506 cairo_pattern_t *gradient = NULL;
2507 if(has_colored_background && is_sensitive)
2508 {
2509 // gradient line as used in some modules for hue, saturation, lightness
2510 const double zoom = (d->max - d->min) / (d->hard_max - d->hard_min);
2511 const double offset = (d->min - d->hard_min) / (d->hard_max - d->hard_min);
2512 gradient = cairo_pattern_create_linear(x_origin, 0, width, baseline_height);
2513 for(int k = 0; k < d->grad_cnt; k++)
2514 cairo_pattern_add_color_stop_rgba(gradient, (d->grad_pos[k] - offset) / zoom,
2515 d->grad_col[k][0], d->grad_col[k][1], d->grad_col[k][2], 0.4f);
2516 cairo_set_source(cr, gradient);
2517 }
2518 else
2519 {
2520 // regular baseline
2521 set_color(cr, w->bauhaus->color_bg);
2522 }
2523
2524 cairo_fill(cr);
2525
2526
2527 if(w->bauhaus->border_width > 0)
2528 {
2529 // Draw the border on top
2530 // We need to recess the coordinates by half a line-width for perfect border matching
2531 const double line_width = 1.;
2532 cairo_rectangle(cr, round(x_origin) + line_width / 2., round(baseline_top) + line_width / 2.,
2533 round(baseline_width) - line_width, round(baseline_height) - line_width);
2534 cairo_set_line_width(cr, line_width);
2536 cairo_stroke(cr);
2537 }
2538
2539 if(gradient) cairo_pattern_destroy(gradient);
2540
2541 // get the reference of the slider aka the position of the 0 value
2542 float origin = fmaxf(fminf((d->factor > 0 ? -d->min - d->offset/d->factor
2543 : d->max + d->offset/d->factor)
2544 / (d->max - d->min),
2545 1.0f) * width,
2546 0.f);
2547
2548 // have a `fill ratio feel' from zero to current position
2549 if(!has_colored_background && d->fill_feedback && is_sensitive)
2550 {
2551 set_color(cr, w->bauhaus->color_value);
2552
2553 const float position = d->pos * width;
2554 const float delta = position - origin;
2555
2556 // origin = 0 means start on left, but our left is shifted to the right,
2557 // so offset it. (see x_origin)
2558 if(origin == 0.f)
2559 cairo_rectangle(cr, x_origin, baseline_top, delta - x_origin, baseline_height);
2560 else
2561 cairo_rectangle(cr, origin, baseline_top, delta, baseline_height);
2562
2563 cairo_fill(cr);
2564 }
2565
2566 // draw the 0 reference graduation if it's different than the bounds of the slider
2567 // If the max of the slider is 360, it is likely an absolute hue slider in degrees
2568 // a zero in periodic stuff has not much meaning so we skip it.
2569 if(d->hard_max != 360.0f && is_sensitive)
2570 {
2571 set_color(cr, w->bauhaus->color_fg);
2572
2573 // If we are to put the 0 reference on the "left", this time we need to account for
2574 // actual leftmost resting position of the indicator, not the starting point of the baseline.
2575 cairo_arc(cr, origin, baseline_top + w->bauhaus->marker_size, w->bauhaus->border_width / 2., 0, 2 * M_PI);
2576 cairo_fill(cr);
2577 }
2578
2579 cairo_restore(cr);
2580}
2581
2583{
2584 switch(w->type)
2585 {
2587 break;
2588 case DT_BAUHAUS_SLIDER:
2589 {
2592 }
2593 break;
2594 default:
2595 break;
2596 }
2597}
2598
2599static void dt_bauhaus_widget_accept(struct dt_bauhaus_widget_t *w, gboolean timeout)
2600{
2601 GtkWidget *widget = GTK_WIDGET(w);
2602
2603 switch(w->type)
2604 {
2606 {
2608
2609 if(d->editable && w->bauhaus->keys_cnt > 0)
2610 {
2611 // combobox is editable and we have text, assume it is a custom input
2612 memset(d->text, 0, DT_BAUHAUS_COMBO_MAX_TEXT);
2613 g_strlcpy(d->text, w->bauhaus->keys, DT_BAUHAUS_COMBO_MAX_TEXT);
2614 _combobox_set(widget, -1, timeout); // select custom entry
2615
2616#if DEBUG
2617 fprintf(stdout, "combobox went the custom path\n");
2618#endif
2619 }
2620 else
2621 {
2622 if(w->bauhaus->keys_cnt > 0)
2623 {
2624 // combobox is not editable, but we have text. Assume user wanted to init a selection from keyboard.
2625 // find the closest match by looking for the entry having the maximum number
2626 // of characters in common with the user input.
2627 gchar *keys = g_utf8_casefold(w->bauhaus->keys, -1);
2628 int match = -1;
2629 int matches = 0;
2630
2631 for(int j = 0; j < d->entries->len; j++)
2632 {
2633 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, j);
2634 gchar *text_cmp = g_utf8_casefold(entry->label, -1);
2635 if(entry->sensitive && !strncmp(text_cmp, keys, w->bauhaus->keys_cnt))
2636 {
2637 matches++;
2638 match = j;
2639 }
2640 dt_free(text_cmp);
2641 }
2642 dt_free(keys);
2643
2644 // Accept result only if exactly one match was found. Anything else is ambiguous
2645 if(matches == 1)
2646 _combobox_set(widget, match, timeout);
2647 }
2648 else {
2649 // Active entry (below cursor or scrolled)
2650 _combobox_set(widget, d->hovered, timeout);
2651 }
2652 }
2653
2654 break;
2655 }
2656 case DT_BAUHAUS_SLIDER:
2657 {
2658 // The slider popup uses the quadratic magnifier for accurate setting.
2659 // We need extra conversions from cursor coordinates to set it right.
2660 // This needs to be kept in sync with popup_draw()
2662
2663 // This is needed to accept the change.
2664 // d->pos is soft-updated with corrected coordinates for drawing purposes only in popup_redraw().
2665 // We need to reset it to the original value temporarily, and request a proper setting
2666 // with value-changed signal through dt_bauhaus_slider_set_normalized()
2667 const float value = d->pos;
2668 d->pos = d->oldpos;
2670 break;
2671 }
2672 default:
2673 break;
2674 }
2675}
2676
2677static gchar *_build_label(const struct dt_bauhaus_widget_t *w)
2678{
2679 return g_strdup(w->label);
2680}
2681
2682static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
2683{
2684 // Popups belong to the app, not to the bauhaus widget. That's confusing.
2685 // Also the *widget param here is the popup container, still not the bauhaus widget. Even more confusing.
2686 // This is the actual parent bauhaus widget :
2687 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
2689
2690 // get area properties
2691 GtkAllocation allocation;
2692 gtk_widget_get_allocation(widget, &allocation);
2693 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
2694 cairo_t *cr = cairo_create(cst);
2695 GtkStyleContext *context = gtk_widget_get_style_context(widget);
2696
2697 // look up some colors once
2698 GdkRGBA text_color, text_color_selected, text_color_hover, text_color_insensitive, text_color_focused;
2699 gtk_style_context_get_color(context, GTK_STATE_FLAG_NORMAL, &text_color);
2700 gtk_style_context_get_color(context, GTK_STATE_FLAG_SELECTED, &text_color_selected);
2701 gtk_style_context_get_color(context, GTK_STATE_FLAG_PRELIGHT, &text_color_hover);
2702 gtk_style_context_get_color(context, GTK_STATE_FLAG_INSENSITIVE, &text_color_insensitive);
2703 gtk_style_context_get_color(context, GTK_STATE_FLAG_FOCUSED, &text_color_focused);
2704
2705 GdkRGBA *fg_color = default_color_assign();
2706 GdkRGBA *bg_color;
2707 GtkStateFlags state = gtk_widget_get_state_flags(widget);
2708
2709 gtk_style_context_get(context, state, "background-color", &bg_color, NULL);
2710 gtk_style_context_get_color(context, state, fg_color);
2711
2712 // draw background
2713 gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height);
2714 gtk_render_frame(context, cr, 0, 0, allocation.width, allocation.height);
2715
2716 const double main_height = _widget_get_main_height(w, widget);
2717 double total_width = 0.f;
2718 const double main_width = _widget_get_main_width(w, NULL, &total_width);
2719
2720 // translate to account for the widget spacing
2721 cairo_translate(cr, w->padding->left, w->padding->top);
2722
2723 // switch on bauhaus widget type (so we only need one static window)
2724 switch(w->type)
2725 {
2726 case DT_BAUHAUS_SLIDER:
2727 {
2729 const double slider_cursor_radius = 0.5 * w->bauhaus->marker_size;
2730 const double text_width = main_width + slider_cursor_radius;
2731 cairo_save(cr);
2732 cairo_translate(cr, slider_cursor_radius, 0.0);
2733 set_color(cr, *fg_color);
2734
2735 float scale = _bh_slider_get_scale(w);
2736 const int num_scales = 1.f / scale;
2737 const float bottom_baseline = _get_slider_bar_height(w);
2738
2739 for(int k = 0; k < num_scales; k++)
2740 {
2741 const float off = k * scale - d->oldpos;
2742 GdkRGBA fg_copy = *fg_color;
2743 fg_copy.alpha = scale / fabsf(off);
2744 set_color(cr, fg_copy);
2745 draw_slider_line(cr, d->oldpos, off, scale, main_width, main_height, bottom_baseline, 1);
2746 cairo_stroke(cr);
2747 }
2748 cairo_restore(cr);
2749
2750 // Get the x offset compared to d->oldpos accounting for vertical position magnification
2751 const double mouse_off = d->pos - d->oldpos;
2752
2753 cairo_save(cr);
2754 cairo_translate(cr, slider_cursor_radius, 0.0);
2755
2756 // Draw the baseline with fill feedback if any (needs the new d->pos set before)
2757 dt_bauhaus_draw_baseline(w, cr, main_width);
2758
2759 // draw mouse over indicator line
2761 draw_slider_line(cr, d->oldpos, mouse_off, scale, main_width, main_height, bottom_baseline, 2);
2762 cairo_stroke(cr);
2763
2764 // draw indicator
2765 dt_bauhaus_draw_indicator(w, d->pos, cr, main_width);
2766
2767 cairo_restore(cr);
2768
2769 // draw numerical value:
2770 cairo_save(cr);
2772
2773 float value_width = 0.f;
2774 char *text = dt_bauhaus_slider_get_text(GTK_WIDGET(w), dt_bauhaus_slider_get(GTK_WIDGET(w)));
2775 GdkRectangle bounding_value = { .x = 0.,
2776 .y = 0.,
2777 .width = text_width,
2778 .height = w->bauhaus->line_height };
2779 // Display user keyboard input if any, otherwise the current value
2780 show_pango_text(w, context, cr, &bounding_value,
2781 (w->bauhaus->keys_cnt) ? w->bauhaus->keys : text, BH_ALIGN_RIGHT,
2782 BH_ALIGN_MIDDLE, PANGO_ELLIPSIZE_NONE, NULL, &value_width, NULL, GTK_STATE_FLAG_NORMAL);
2783 dt_free(text);
2784
2785 // label on top of marker:
2786 gchar *label_text = _build_label(w);
2787 const float label_width = text_width - value_width - INNER_PADDING;
2788 GdkRectangle bounding_label = { .x = 0.,
2789 .y = 0.,
2790 .width = label_width,
2791 .height = w->bauhaus->line_height };
2792 set_color(cr, *fg_color);
2793 show_pango_text(w, context, cr, &bounding_label, label_text, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE,
2794 PANGO_ELLIPSIZE_END, NULL, NULL, NULL, GTK_STATE_FLAG_NORMAL);
2795 dt_free(label_text);
2796
2797 cairo_restore(cr);
2798 }
2799 break;
2801 {
2803
2804 // User keyboard input goes first
2805 double popup_y = 0.;
2806 if(w->bauhaus->keys_cnt > 0)
2807 {
2808 cairo_save(cr);
2809 set_color(cr, text_color_focused);
2810 const double query_height = _bh_get_row_height(w);
2811 GdkRectangle query_label = { .x = 0.,
2812 .y = popup_y,
2813 .width = main_width,
2814 .height = query_height };
2815 show_pango_text(w, context, cr, &query_label, w->bauhaus->keys, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE,
2816 PANGO_ELLIPSIZE_NONE, NULL, NULL, NULL, GTK_STATE_FLAG_NORMAL);
2817 popup_y += query_height;
2818 cairo_restore(cr);
2819 }
2820
2821 cairo_save(cr);
2822 gchar *keys = g_utf8_casefold(w->bauhaus->keys, -1);
2823 for(int j = 0; j < d->entries->len; j++)
2824 {
2825 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, j);
2826 const double entry_height = _bh_get_combobox_entry_height(w, entry);
2827 if(entry->is_separator)
2828 {
2829 if(w->bauhaus->keys_cnt == 0)
2830 {
2831 cairo_set_source_rgba(cr, 0.5, 0.5, 0.5, 1.);
2832 cairo_move_to(cr, 0., popup_y + 0.5 * entry_height);
2833 cairo_line_to(cr, main_width, popup_y + 0.5 * entry_height);
2834 cairo_set_line_width(cr, 1.);
2835 cairo_stroke(cr);
2836 }
2837 popup_y += entry_height;
2838 continue;
2839 }
2840
2841 gchar *text_cmp = g_utf8_casefold(entry->label, -1);
2842 if(!strncmp(text_cmp, keys, w->bauhaus->keys_cnt))
2843 {
2844 // If user typed some keys, display matching entries only
2845
2846 // The GTK state flag is applied to the whole widget,
2847 // we need to dispatch it individually to each entry
2848 if(!entry->sensitive)
2849 {
2850 set_color(cr, text_color_insensitive);
2851 state = GTK_STATE_FLAG_INSENSITIVE;
2852 }
2853 else if(j == d->active)
2854 {
2855 set_color(cr, text_color_selected);
2856 state = GTK_STATE_FLAG_SELECTED;
2857 }
2858 else if(j == d->hovered)
2859 {
2860 set_color(cr, text_color_hover);
2861 state = GTK_STATE_FLAG_PRELIGHT;
2862 }
2863 else
2864 {
2865 set_color(cr, text_color);
2866 state = GTK_STATE_FLAG_NORMAL;
2867 }
2868
2869 GdkRectangle bounding_label = { .x = 0.,
2870 .y = popup_y,
2871 .width = main_width,
2872 .height = entry_height };
2873#if DEBUG
2874 cairo_rectangle(cr, bounding_label.x, bounding_label.y, bounding_label.width, bounding_label.height);
2875 cairo_set_line_width(cr, 2);
2876 cairo_stroke(cr);
2877#endif
2878 show_pango_text(w, context, cr, &bounding_label, entry->label, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE,
2879 d->entries_ellipsis, bg_color, NULL, NULL, state);
2880 }
2881
2882 dt_free(text_cmp);
2883 popup_y += entry_height;
2884 }
2885 cairo_restore(cr);
2886 dt_free(keys);
2887 }
2888 break;
2889 default:
2890 // yell
2891 break;
2892 }
2893
2894 cairo_destroy(cr);
2895 cairo_set_source_surface(crf, cst, 0, 0);
2896 cairo_paint(crf);
2897 cairo_surface_destroy(cst);
2898
2899 gdk_rgba_free(bg_color);
2900 gdk_rgba_free(fg_color);
2901
2902 return TRUE;
2903}
2904
2905
2906// Get the maximum width of a full combobox without ellipsization
2908{
2909 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2911 GtkStyleContext *context = gtk_widget_get_style_context(widget);
2912 const GtkStateFlags state = gtk_widget_get_state_flags(widget);
2913
2914 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 999, 999);
2915 cairo_t *cr = cairo_create(cst);
2916
2917 float width = 0.f;
2918
2919 // Get chevron width + padding if any
2920 if(w->show_quad)
2922
2923 float label_width = 0.f;
2924 GdkRectangle bounding_label = { .x = 0.,
2925 .y = 0.,
2926 .width = 999,
2927 .height = 999 };
2928
2929 show_pango_text(w, context, cr, &bounding_label, w->label, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE,
2930 PANGO_ELLIPSIZE_NONE, NULL, &label_width, NULL, state);
2931
2932 if(label_width > 0.f) width += label_width + INNER_PADDING;
2933
2934 // Get width of the longest entry
2935 float max_entry = 0.f;
2936 for(int i = 0; i < d->entries->len; i++)
2937 {
2938 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i);
2939
2940 // The value is shown right-aligned, ellipsized if needed.
2941 GdkRectangle bounding_value = { .x = 0,
2942 .y = 0.,
2943 .width = 999,
2944 .height = 999 };
2945 float entry_label_width = 0.f;
2946
2947 show_pango_text(w, context, cr, &bounding_value, entry->label, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE, PANGO_ELLIPSIZE_NONE, NULL,
2948 &entry_label_width, NULL, state);
2949
2950 if(entry_label_width + INNER_PADDING > max_entry) max_entry = entry_label_width + INNER_PADDING;
2951 }
2952
2953 width += max_entry;
2954 width += w->margin->left + w->margin->right + w->padding->left + w->padding->right;
2955
2956 cairo_destroy(cr);
2957 cairo_surface_destroy(cst);
2958
2959 return width;
2960}
2961
2962static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf)
2963{
2964 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
2965 GtkStyleContext *context = gtk_widget_get_style_context(widget);
2966
2967 // Get current Gtk allocation
2968 GtkAllocation allocation;
2969 gtk_widget_get_allocation(widget, &allocation);
2970
2971 if(w->type == DT_BAUHAUS_COMBOBOX)
2972 allocation.height = _get_combobox_height(widget);
2973 else if(w->type == DT_BAUHAUS_SLIDER)
2974 allocation.height = _get_slider_height(widget);
2975
2976 if(w->type == DT_BAUHAUS_COMBOBOX && !w->expand)
2977 {
2978 // For comboboxes that are not set to hexpand, limit the width span to what's needed
2979 // to display the internal text, aka prevent them to grow out of proportions
2980 float max_width = _get_combobox_max_width(widget);
2981 if(max_width < allocation.width)
2982 allocation.width = ceilf(max_width);
2983 }
2984
2985 // Force allocate to our requirements. Yes, it's ugly.
2986 gtk_widget_size_allocate(widget, &allocation);
2987
2988 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
2989 cairo_t *cr = cairo_create(cst);
2990
2991 GdkRGBA *text_color = default_color_assign();
2992 GdkRGBA *value_color = default_color_assign();
2993 GdkRGBA *value_text_color = default_color_assign();
2994 GtkStateFlags state = gtk_widget_get_state_flags(widget);
2995 if(gtk_widget_has_focus(widget))
2996 state |= GTK_STATE_FLAG_FOCUSED;
2997
2998 gtk_style_context_save(context);
2999 gtk_style_context_set_state(context, state);
3000 gtk_style_context_get_color(context, state, text_color);
3001
3002 *value_color = gtk_widget_is_sensitive(widget) ? w->bauhaus->color_value : w->bauhaus->color_value_insensitive;
3003 *value_text_color = gtk_widget_is_sensitive(widget) ? w->bauhaus->color_value_text : w->bauhaus->color_value_text_insensitive;
3004
3005 // Compute internal widget sizes, that is allocation minus margins
3007 // Note : box is the border box in CSS conventions
3008 const float box_width = allocation.width - w->margin->left - w->margin->right;
3009 const float box_height = allocation.height - w->margin->top - w->margin->bottom;
3010
3011 // Paint background and borders first
3012 gtk_render_background(context, crf, w->margin->left, w->margin->top, box_width, box_height);
3013 gtk_render_frame(context, crf, w->margin->left, w->margin->top, box_width, box_height);
3014
3015 // Translate Cairo coordinates to account for the widget spacing
3016 cairo_translate(cr,
3017 w->margin->left + w->padding->left,
3018 w->margin->top + w->padding->top);
3019
3020 const float available_width = _widget_get_main_width(w, NULL, NULL);
3021 const float inner_height = _widget_get_main_height(w, NULL);
3022
3023 // draw type specific content:
3024 cairo_save(cr);
3025 set_color(cr, *text_color);
3026 cairo_set_line_width(cr, 1.0);
3027 switch(w->type)
3028 {
3030 {
3031 // draw label and quad area at right end
3032 // Gap must match the one reserved by _widget_get_main_width() (2*INNER_PADDING)
3033 // and used by the slider, so combobox quads stay flush and vertically aligned
3034 // with slider quad icons when stacked.
3035 if(w->show_quad)
3036 dt_bauhaus_draw_quad(w, cr, available_width + 2 * INNER_PADDING, 0.);
3037
3039 const PangoEllipsizeMode combo_ellipsis = d->entries_ellipsis;
3040
3041 float label_width = 0.f;
3042 float label_height = 0.f;
3043
3044 GdkRectangle bounding_label = { .x = 0.,
3045 .y = 0.,
3046 .width = available_width,
3047 .height = inner_height };
3048 set_color(cr, *text_color);
3049 show_pango_text(w, context, cr, &bounding_label, w->label, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE,
3050 combo_ellipsis, NULL, &label_width, &label_height, state);
3051
3052 // The value is shown right-aligned, ellipsized if needed.
3053 gchar *text = d->text;
3054 if(d->active >= 0 && d->active < d->entries->len)
3055 {
3056 const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, d->active);
3057 text = entry->label;
3058 }
3059 GdkRectangle bounding_value = { .x = label_width + INNER_PADDING,
3060 .y = 0.,
3061 .width = available_width - label_width - INNER_PADDING,
3062 .height = inner_height };
3063 set_color(cr, *value_text_color);
3064 show_pango_text(w, context, cr, &bounding_value, text, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE, combo_ellipsis, NULL,
3065 NULL, NULL, state);
3066
3067 break;
3068 }
3069 case DT_BAUHAUS_SLIDER:
3070 {
3071 const double slider_cursor_radius = 0.5 * w->bauhaus->marker_size;
3072 const double text_width = available_width + slider_cursor_radius;
3073
3074 // line for orientation
3075 cairo_save(cr);
3076 cairo_translate(cr, slider_cursor_radius, 0.0);
3077 dt_bauhaus_draw_baseline(w, cr, available_width);
3078 cairo_restore(cr);
3079
3080 // Paint the non-active quad icon with some transparency, because
3081 // icons are bolder than the neighbouring text and appear brighter.
3082 cairo_save(cr);
3083 if(!(w->quad_paint_flags & CPF_ACTIVE))
3084 cairo_set_source_rgba(cr, text_color->red, text_color->green, text_color->blue, text_color->alpha * 0.7);
3085
3086 dt_bauhaus_draw_quad(w, cr, text_width + 2. * INNER_PADDING, 0.);
3087 cairo_restore(cr);
3088
3089 float value_width = 0;
3090 if(gtk_widget_is_sensitive(widget))
3091 {
3092 cairo_save(cr);
3093 cairo_translate(cr, slider_cursor_radius, 0.0);
3094 dt_bauhaus_draw_indicator(w, w->data.slider.pos, cr, available_width);
3095 cairo_restore(cr);
3096
3097 char *text = dt_bauhaus_slider_get_text(widget, dt_bauhaus_slider_get(widget));
3098 GdkRectangle bounding_value = { .x = 0.,
3099 .y = 0.,
3100 .width = text_width,
3101 .height = w->bauhaus->line_height };
3102 set_color(cr, *value_text_color);
3103 show_pango_text(w, context, cr, &bounding_value, text, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE,
3104 PANGO_ELLIPSIZE_NONE, NULL, &value_width, NULL, state);
3105 dt_free(text);
3106 }
3107
3108 // label on top of marker:
3109 gchar *label_text = _build_label(w);
3110 const float label_width = text_width - value_width - INNER_PADDING;
3111 GdkRectangle bounding_label = { .x = 0.,
3112 .y = 0.,
3113 .width = label_width,
3114 .height = w->bauhaus->line_height };
3115 set_color(cr, *text_color);
3116 show_pango_text(w, context, cr, &bounding_label, label_text, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE,
3117 PANGO_ELLIPSIZE_END, NULL, NULL, NULL, state);
3118 dt_free(label_text);
3119 }
3120 break;
3121 default:
3122 break;
3123 }
3124 cairo_restore(cr);
3125 cairo_destroy(cr);
3126 cairo_set_source_surface(crf, cst, 0, 0);
3127 cairo_paint(crf);
3128 cairo_surface_destroy(cst);
3129 gtk_style_context_restore(context);
3130
3131 gdk_rgba_free(text_color);
3132 gdk_rgba_free(value_color);
3133 gdk_rgba_free(value_text_color);
3134
3135 return TRUE;
3136}
3137
3138static void _get_preferred_width(GtkWidget *widget, gint *minimum_size, gint *natural_size)
3139{
3140 // Nothing clever here : preferred size is the size of the container.
3141 // If user is not happy with that, it's his responsibility to resize sidebars.
3142 // Minimum size is left at 0 so the widget can shrink with its parent;
3143 // leaving it unset would read as uninitialized stack garbage and could
3144 // clamp natural_size up to a stale value, preventing the widget from
3145 // growing again when the parent grows.
3146 *minimum_size = 0;
3147
3152 else
3153 *natural_size = DT_PIXEL_APPLY_DPI(300);
3154}
3155
3156static void _get_preferred_height(GtkWidget *widget, gint *minimum_size, gint *natural_size)
3157{
3158 // Resolve the height from the same getters used everywhere else so it is
3159 // always correct at measure time, regardless of when "style-updated" fires.
3160 // Relying solely on the size-request set in _style_updated is not enough:
3161 // when the widget lives inside a freshly popped-up GtkMenu, the menu measures
3162 // its children before their style context is resolved, so it would size
3163 // itself from a stale/zero height and end up showing scroll arrows.
3164 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3166 const gint height = (w->type == DT_BAUHAUS_COMBOBOX) ? (gint)ceil(_get_combobox_height(widget))
3167 : (gint)ceil(_get_slider_height(widget));
3168 *minimum_size = height;
3169 *natural_size = height;
3170}
3171
3173{
3174 if(bh->current)
3175 {
3176 gtk_grab_remove(bh->popup_area);
3177 gtk_widget_hide(bh->popup_window);
3178 gtk_window_set_attached_to(GTK_WINDOW(bh->popup_window), NULL);
3179
3180 // Give back focus to the attached widget
3181 gtk_widget_grab_focus(GTK_WIDGET(bh->current));
3182 darktable.gui->has_scroll_focus = GTK_WIDGET(bh->current);
3183
3184 bh->current = NULL;
3185 }
3186}
3187
3189{
3190 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3192 w->bauhaus->current = w;
3193 w->bauhaus->keys_cnt = 0;
3194 memset(w->bauhaus->keys, 0, sizeof(w->bauhaus->keys));
3195 w->bauhaus->change_active = 0;
3196 w->bauhaus->mouse_line_distance = 0.0f;
3197 w->bauhaus->hiding = FALSE;
3198
3199 // Make sure all relevant widgets exist
3200 gtk_widget_realize(w->bauhaus->popup_window);
3201 gtk_widget_realize(widget);
3203
3204 GtkAllocation tmp;
3205 gtk_widget_get_allocation(widget, &tmp);
3206 int width = MAX(tmp.width, 1);
3207 int height = MAX(tmp.height, 1);
3208
3209 switch(w->bauhaus->current->type)
3210 {
3211 case DT_BAUHAUS_SLIDER:
3212 {
3213 // Slider popup: make it square
3215 d->oldpos = d->pos;
3216 d->is_dragging = FALSE;
3217 height = tmp.width;
3218 break;
3219 }
3221 {
3223 break;
3224 }
3225 default:
3226 {
3227 fprintf(stderr, "[dt_bauhaus_show_popup] The bauhaus widget has an unknown type\n");
3228 break;
3229 }
3230 }
3231
3232 /* Bind to CSS rules from parent widget */
3233 GtkStyleContext *context = gtk_widget_get_style_context(w->bauhaus->popup_area);
3234 gtk_style_context_add_class(context, "dt_bauhaus_popup");
3235 gtk_window_set_attached_to(GTK_WINDOW(w->bauhaus->popup_window), widget);
3236 #ifdef GDK_WINDOWING_WAYLAND
3237 if (GDK_IS_WAYLAND_DISPLAY(gdk_display_get_default())) {
3238 GtkWidget *toplevel = gtk_widget_get_toplevel(widget);
3239 gtk_window_set_transient_for(GTK_WINDOW(w->bauhaus->popup_window), GTK_WINDOW(toplevel));
3240 }
3241 #endif
3242
3243 // The popup window stays transient for the main application window, so the anchor
3244 // rectangle needs to remain expressed in that coordinate space even when the popup is
3245 // logically attached to an enclosing popover on Wayland.
3246 gint wx = 0, wy = 0;
3247 GdkWindow *widget_window = gtk_widget_get_window(widget);
3248 if(widget_window) gdk_window_get_origin(widget_window, &wx, &wy);
3249 wx += w->margin->left;
3250 wy += w->margin->top;
3251
3252 gint wwx = 0, wwy = 0;
3253 GdkWindow *main_window = gtk_widget_get_window(dt_ui_main_window(darktable.gui->ui));
3254 if(main_window) gdk_window_get_origin(main_window, &wwx, &wwy);
3255
3256 GdkRectangle anchor = {
3257 .x = wx - wwx,
3258 .y = wy - wwy,
3259 .width = MAX(tmp.width, 1),
3260 .height = MAX(tmp.height, 1)
3261 };
3262
3263 // Set desired size, but it's more a guide than a rule.
3264 gtk_widget_set_size_request(w->bauhaus->popup_area, width, height);
3265 gtk_widget_set_size_request(w->bauhaus->popup_window, width, height);
3266
3267 // Need to call resize to actually change something
3268 gtk_window_resize(GTK_WINDOW(w->bauhaus->popup_window), width, height);
3269
3270 GdkWindow *window = gtk_widget_get_window(w->bauhaus->popup_window);
3271 if(IS_NULL_PTR(window))
3272 {
3273 gtk_widget_show_all(w->bauhaus->popup_window);
3274 gtk_widget_grab_focus(w->bauhaus->popup_area);
3275 return;
3276 }
3277
3278 // For Wayland (and supposed to work on X11 too) and Gtk 3.24 this is how you do it
3279 gdk_window_move_to_rect(GDK_WINDOW(window), &anchor, GDK_GRAVITY_STATIC, GDK_GRAVITY_STATIC,
3280 GDK_ANCHOR_SLIDE, 0, 0);
3281
3282 gtk_widget_show_all(w->bauhaus->popup_window);
3283 gtk_widget_grab_focus(w->bauhaus->popup_area);
3284}
3285
3286static void _slider_add_step(GtkWidget *widget, float delta, guint state)
3287{
3288 if(delta == 0.f) return;
3289 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3291
3293 if(dt_modifier_is(state, GDK_CONTROL_MASK)) delta /= 5.f;
3294 else if(dt_modifier_is(state, GDK_SHIFT_MASK)) delta *= 5.f;
3295
3296 // Ensure the requested delta is at least visible given current number of digits in display
3297 const float min_visible = 1.f / (fabsf(d->factor) * ipow(10, d->digits));
3298 if(fabsf(delta) < min_visible)
3299 delta = copysignf(min_visible, delta);
3300
3301 const float value = dt_bauhaus_slider_get(widget);
3303}
3304
3305static gboolean _widget_scroll(GtkWidget *widget, GdkEventScroll *event)
3306{
3307 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3308 const gboolean popup_captured = (w->bauhaus->current == w);
3309 const gboolean scroll_captured = (darktable.gui->has_scroll_focus == widget);
3310 const gboolean key_captured = gtk_widget_has_focus(widget);
3311 const gboolean smooth = (event->direction == GDK_SCROLL_SMOOTH);
3312
3313 // - native Gtk focus (keyboard), that takes precedence and records all keyboard events,
3314 // - custom scroll focus (mouse wheel), that should not overlap with vertical scrolling.
3315 // Scroll focus is a subset of Gtk focus, aka we can lose scroll focus while we have Gtk focus,
3316 // but delayed history commits may transiently steal Gtk focus while the
3317 // pointer never leaves the widget. In that case, keep trusting the explicit
3318 // scroll focus until leave-notify releases it.
3319 // We also extend widget focus with the popup window focus if it is captured
3320 // by the current widget.
3321 if(!key_captured && !popup_captured && !scroll_captured)
3322 return FALSE;
3323
3324 // Keep ownership of the whole touchpad gesture sequence.
3325 darktable.gui->has_scroll_focus = widget;
3326
3327 int delta_x = 0;
3328 int delta_y = 0;
3329
3330 if(!dt_gui_get_scroll_unit_deltas(event, &delta_x, &delta_y))
3331 {
3332 // Important: for touchpad scroll, sub-unit deltas and stop events must not
3333 // propagate to the parent scrolled window, otherwise the same gesture gets
3334 // re-accumulated there.
3335 return smooth;
3336 }
3337
3338 const gboolean vscroll = delta_y != 0 && abs(delta_y) > abs(delta_x);
3339 const gboolean hscroll = delta_x != 0 && abs(delta_x) > abs(delta_y);
3340
3341 if(w->type == DT_BAUHAUS_SLIDER)
3342 {
3343 if(hscroll)
3344 {
3345 // inconditionnaly record horizontal scroll on slider
3346 _slider_add_step(widget, delta_x, event->state);
3347 return TRUE;
3348 }
3349 else if(vscroll && darktable.gui->has_scroll_focus)
3350 {
3351 // convert vertical scrolling to horizontal only if we have the scroll focus
3352 _slider_add_step(widget, -delta_y, event->state);
3353 return TRUE;
3354 }
3355
3356 // Diagonal smooth gestures: consume them if captured, so they do not leak.
3357 return smooth;
3358 }
3359 else
3360 {
3361 if(vscroll && darktable.gui->has_scroll_focus)
3362 {
3363 _combobox_next_sensitive(w, delta_y);
3364 return TRUE;
3365 }
3366
3367 return smooth;
3368 }
3369}
3370
3371static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event)
3372{
3373 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3374 guint key = dt_keys_mainpad_alternatives(event->keyval);
3375
3376 if(w->type == DT_BAUHAUS_SLIDER)
3377 {
3378 switch(key)
3379 {
3380 case GDK_KEY_Right:
3381 _slider_add_step(widget, 1, event->state);
3382 return TRUE;
3383
3384 case GDK_KEY_Left:
3385 _slider_add_step(widget, -1, event->state);
3386 return TRUE;
3387
3388 case GDK_KEY_Insert:
3389 if(w->quad_toggle)
3390 {
3393 return TRUE;
3394 }
3395
3396 default:
3397 return FALSE;
3398 }
3399 }
3400 else if(w->type == DT_BAUHAUS_COMBOBOX)
3401 {
3402 switch(key)
3403 {
3404 case GDK_KEY_Return:
3405 dt_bauhaus_show_popup(widget);
3406 return TRUE;
3407
3408 case GDK_KEY_Insert:
3409 if(w->quad_toggle)
3410 {
3413 return TRUE;
3414 }
3415
3416 default:
3417 return FALSE;
3418 }
3419 }
3420 return FALSE;
3421}
3422
3423static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3424{
3425 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3426
3427 double event_x = event->x;
3428 double event_y = event->y;
3429 double width;
3430 _bh_active_region_t activated = _bh_get_active_region(widget, &event_x, &event_y, &width, NULL);
3431
3433 dt_gui_throttle_cancel(widget);
3434
3435 if(activated == BH_REGION_OUT)
3436 {
3438 return FALSE;
3439 }
3440
3441 gtk_widget_grab_focus(widget);
3442 darktable.gui->has_scroll_focus = widget;
3443
3444 if(activated == BH_REGION_QUAD && w->quad_toggle)
3445 {
3447 return TRUE;
3448 }
3449 else
3450 {
3451 // If no quad toggle, treat the whole widget as unit pack.
3452 if(event->button == 3)
3453 {
3454 w->bauhaus->mouse_x = event_x;
3455 w->bauhaus->mouse_y = event_y;
3456 dt_bauhaus_show_popup(widget);
3457 return TRUE;
3458 }
3459 else if(event->button == 1)
3460 {
3461 // reset to default.
3462 if(event->type == GDK_2BUTTON_PRESS)
3463 {
3464 // never called, as we popup the other window under your cursor before.
3465 // (except in weird corner cases where the popup is under the -1st entry
3466 dt_bauhaus_combobox_set(widget, d->defpos);
3468 }
3469 else
3470 {
3471 // single click, show options
3472 w->bauhaus->opentime = event->time;
3473 w->bauhaus->mouse_x = event_x;
3474 w->bauhaus->mouse_y = event_y;
3475 dt_bauhaus_show_popup(widget);
3476 }
3477 return TRUE;
3478 }
3479 }
3480 return FALSE;
3481}
3482
3484{
3485 // first cast to bh widget, to check that type:
3486 const struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3488 return d->min + d->pos * (d->max - d->min);
3489}
3490
3492{
3493 const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(widget)->data.slider;
3494 return dt_bauhaus_slider_get(widget) * d->factor + d->offset;
3495}
3496
3498{
3499 const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(w)->data.slider;
3500 if((d->hard_max * d->factor + d->offset)*(d->hard_min * d->factor + d->offset) < 0)
3501 return g_strdup_printf("%+.*f%s", d->digits, val * d->factor + d->offset, d->format);
3502 else
3503 return g_strdup_printf( "%.*f%s", d->digits, val * d->factor + d->offset, d->format);
3504}
3505
3506void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
3507{
3508 // this is the public interface function, translate by bounds and call set_normalized
3509 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3511 const float rpos = CLAMP(pos, d->hard_min, d->hard_max);
3512
3513 // Restore soft min/max if we are in its range
3514 float rrpos = (rpos - d->soft_min) / (d->soft_max - d->soft_min);
3515 if(rrpos > 0.f)
3516 d->min = d->soft_min;
3517 else
3518 d->min = rpos;
3519
3520 if(rrpos < 1.f)
3521 d->max = d->soft_max;
3522 else
3523 d->max = rpos;
3524
3525 dt_bauhaus_slider_set_normalized(w, (rpos - d->min) / (d->max - d->min), TRUE, FALSE);
3526}
3527
3528void dt_bauhaus_slider_set_val(GtkWidget *widget, float val)
3529{
3530 const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(widget)->data.slider;
3531 dt_bauhaus_slider_set(widget, (val - d->offset) / d->factor);
3532}
3533
3535{
3536 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3538 d->digits = val;
3539}
3540
3542{
3543 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3545 return d->digits;
3546}
3547
3549{
3550 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3552 d->step = val;
3553}
3554
3556{
3557 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3559 float step = d->step;
3560
3561 if(step == 0.f)
3562 {
3563 const float min = d->soft_min;
3564 const float max = d->soft_max;
3565
3566 // Derive the automatic keyboard/wheel step from the internal slider domain.
3567 // Display formatting may rescale that domain (for example "%" multiplies by 100),
3568 // but interaction needs to stay in the unformatted value space to avoid jumping
3569 // across the whole range in a single step for small-percent sliders.
3570 const float top = fminf(max-min, fmaxf(fabsf(min), fabsf(max)));
3571 if(top >= 100.f)
3572 step = 1.f;
3573 else
3574 step = top / 100.f;
3575 }
3576
3577 return copysignf(step, d->factor);
3578}
3579
3581{
3582 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3584 d->fill_feedback = feedback;
3585 gtk_widget_queue_draw(widget);
3586}
3587
3589{
3590 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3592 d->min = d->soft_min;
3593 d->max = d->soft_max;
3594 dt_bauhaus_slider_set(widget, d->defpos);
3595 return;
3596}
3597
3598void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
3599{
3600 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3602 d->format = g_intern_string(format);
3603
3604 if(strstr(format,"%") && fabsf(d->hard_max) <= 10)
3605 {
3606 if(d->factor == 1.0f) d->factor = 100;
3607 d->digits -= 2;
3608 }
3609}
3610
3612{
3613 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3615 d->factor = factor;
3616}
3617
3618void dt_bauhaus_slider_set_offset(GtkWidget *widget, float offset)
3619{
3620 struct dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
3622 d->offset = offset;
3623}
3624
3625static void _delayed_slider_commit(gpointer data)
3626{
3627 // Commit slider value change to pipeline history, handling a safety timout
3628 // so incremental scrolls don't trigger a recompute at every scroll step.
3629 struct dt_bauhaus_widget_t *w = data;
3630
3631 // If a reset started after the timeout was scheduled (e.g. while reloading history,
3632 // applying a style, etc.), don't commit anything to history from this stale callback.
3633 if(darktable.gui && darktable.gui->reset) return;
3634
3636 {
3638 w->bauhaus->default_value_changed_callback(GTK_WIDGET(w));
3639 else
3640 fprintf(stderr, "ERROR: %s - %s is set to use default callback but none is provided\n", w->module->name,
3641 w->label);
3642 }
3643 else
3644 {
3645 if(w->module)
3646 fprintf(stderr, "WARNING: %s - %s has an IOP module but doesn't use default callback\n", w->module->name,
3647 w->label);
3648 }
3649
3650 // We need te emit this signal inconditionnaly
3651 g_signal_emit_by_name(G_OBJECT(w), "value-changed");
3652}
3653
3666static void dt_bauhaus_slider_set_normalized(struct dt_bauhaus_widget_t *w, float pos, gboolean raise, gboolean timeout)
3667{
3669 const float old_pos = d->pos;
3670 const float new_pos = CLAMP(pos, 0.0f, 1.0f);
3671
3672 if(old_pos != new_pos || raise)
3673 {
3674 const float new_value = new_pos * (d->max - d->min) + d->min;
3675 const float precision = (float)ipow(10, d->digits) * d->factor;
3676 const float rounded_value = roundf(new_value * precision) / precision;
3677 d->pos = (rounded_value - d->min) / (d->max - d->min);
3678
3679 if(w->bauhaus->current == w)
3680 gtk_widget_queue_draw(w->bauhaus->popup_area);
3681
3682 gtk_widget_queue_draw(GTK_WIDGET(w));
3683
3684 // If a delayed commit is pending from a previous user interaction, cancel it.
3685 // This prevents stale timers from firing after programmatic updates (GUI reset)
3686 // and unexpectedly committing module changes to history.
3687 if(!darktable.gui->reset && raise)
3688 {
3689 if(timeout)
3691 else
3692 {
3693 dt_gui_throttle_cancel(GTK_WIDGET(w));
3695 }
3696 }
3697 else if(!raise || darktable.gui->reset)
3698 {
3699 dt_gui_throttle_cancel(GTK_WIDGET(w));
3700 }
3701 }
3702}
3703
3704static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
3705{
3706 dt_bauhaus_t *bh = g_object_get_data(G_OBJECT(widget), "bauhaus");
3708
3709 guint key = dt_keys_mainpad_alternatives(event->keyval);
3710
3711 switch(w->type)
3712 {
3713 case DT_BAUHAUS_SLIDER:
3714 {
3715 if(bh->keys_cnt + 2 < 64
3716 && (key == GDK_KEY_space || // SPACE
3717 key == GDK_KEY_percent || // %
3718 (event->string[0] >= 40 && event->string[0] <= 57) || // ()+-*/.,0-9
3719 key == GDK_KEY_asciicircum || key == GDK_KEY_dead_circumflex || // ^
3720 key == GDK_KEY_X || key == GDK_KEY_x)) // Xx
3721 {
3722 if(key == GDK_KEY_dead_circumflex)
3723 bh->keys[bh->keys_cnt++] = '^';
3724 else
3725 bh->keys[bh->keys_cnt++] = event->string[0];
3726 gtk_widget_queue_draw(bh->popup_area);
3727 }
3728 else if(bh->keys_cnt > 0
3729 && (key == GDK_KEY_BackSpace || key == GDK_KEY_Delete))
3730 {
3731 bh->keys[--bh->keys_cnt] = 0;
3732 gtk_widget_queue_draw(bh->popup_area);
3733 }
3734 else if(bh->keys_cnt > 0 && bh->keys_cnt + 1 < 64
3735 && (key == GDK_KEY_Return))
3736 {
3737 // accept input
3738 bh->keys[bh->keys_cnt] = 0;
3739 // unnormalized input, user was typing this:
3740 const float old_value = dt_bauhaus_slider_get_val(GTK_WIDGET(w));
3741 const float new_value = dt_calculator_solve(old_value, bh->keys);
3742 if(isfinite(new_value)) dt_bauhaus_slider_set_val(GTK_WIDGET(w), new_value);
3743 bh->keys_cnt = 0;
3744 memset(bh->keys, 0, sizeof(bh->keys));
3746 }
3747 else if(key == GDK_KEY_Escape)
3748 {
3749 // discard input ands close popup
3750 bh->keys_cnt = 0;
3751 memset(bh->keys, 0, sizeof(bh->keys));
3753 }
3754 else
3755 return FALSE;
3756
3757 return TRUE;
3758 }
3760 {
3761 if(!g_utf8_validate(event->string, -1, NULL)) return FALSE;
3762 const gunichar c = g_utf8_get_char(event->string);
3763 const long int char_width = g_utf8_next_char(event->string) - event->string;
3764 if(bh->keys_cnt + 1 + char_width < 64 && g_unichar_isprint(c))
3765 {
3766 // only accept key input if still valid or editable?
3767 g_utf8_strncpy(bh->keys + bh->keys_cnt, event->string, 1);
3768 bh->keys_cnt += char_width;
3769 gtk_widget_queue_draw(bh->popup_area);
3770 }
3771 else if(bh->keys_cnt > 0
3772 && (key == GDK_KEY_BackSpace || key == GDK_KEY_Delete))
3773 {
3774 bh->keys_cnt
3775 -= (bh->keys + bh->keys_cnt)
3776 - g_utf8_prev_char(bh->keys + bh->keys_cnt);
3777 bh->keys[bh->keys_cnt] = 0;
3778 gtk_widget_queue_draw(bh->popup_area);
3779 }
3780 else if(bh->keys_cnt > 0 && bh->keys_cnt + 1 < 64
3781 && (key == GDK_KEY_Return))
3782 {
3783 // accept unique matches only for editable:
3784 if(w->data.combobox.editable)
3785 bh->end_mouse_y = FLT_MAX;
3786 else
3787 bh->end_mouse_y = 0;
3788 bh->keys[bh->keys_cnt] = 0;
3790 bh->keys_cnt = 0;
3791 memset(bh->keys, 0, sizeof(bh->keys));
3793 }
3794 else if(key == GDK_KEY_Escape)
3795 {
3796 // discard input and close popup
3797 bh->keys_cnt = 0;
3798 memset(bh->keys, 0, sizeof(bh->keys));
3800 }
3801 else if(key == GDK_KEY_Up)
3802 {
3804 }
3805 else if(key == GDK_KEY_Down)
3806 {
3808 }
3809 else if(key == GDK_KEY_Return)
3810 {
3811 // return pressed, but didn't type anything
3812 bh->end_mouse_y = -1; // negative will use currently highlighted instead.
3813 bh->keys[bh->keys_cnt] = 0;
3814 bh->keys_cnt = 0;
3815 memset(bh->keys, 0, sizeof(bh->keys));
3818 }
3819 else
3820 return FALSE;
3821 return TRUE;
3822 }
3823 default:
3824 return FALSE;
3825 }
3826}
3827
3828static gboolean dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3829{
3830 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3832
3833 double event_x = event->x;
3834 double event_y = event->y;
3835 double main_width = 0.;
3836 _bh_active_region_t activated = _bh_get_active_region(widget, &event_x, &event_y, &main_width, NULL);
3837 w->bauhaus->mouse_x = event_x;
3838 w->bauhaus->mouse_y = event_y;
3839
3840 if(activated == BH_REGION_OUT)
3841 {
3843 return FALSE;
3844 }
3845
3846 gtk_widget_grab_focus(widget);
3847 darktable.gui->has_scroll_focus = widget;
3848
3849 if(activated == BH_REGION_QUAD && w->quad_toggle)
3850 {
3852 return TRUE;
3853 }
3854 else if(activated == BH_REGION_MAIN)
3855 {
3856 if(event->button == 1)
3857 {
3858 if(event->type == GDK_2BUTTON_PRESS)
3859 {
3860 // double left click on the main region : reset value to default
3862 d->is_dragging = 0;
3863 }
3864 else
3865 {
3866 // single left click on main region : redraw the slider immediately
3867 // but without committing results to pipeline yet.
3868 if(event_y < w->bauhaus->line_height)
3869 {
3870 // single left click on the header name : do nothing (only give focus)
3871 d->is_dragging = 0;
3872 }
3873 else
3874 {
3875 // single left click on slider bar : set new value
3876 d->is_dragging = 1;
3877 dt_bauhaus_slider_set_normalized(w, event_x / main_width, FALSE, FALSE);
3878 }
3879 }
3880 }
3881 else if(event->button == 3)
3882 {
3883 // right click : show accurate slider setting popup
3884 d->oldpos = d->pos;
3885 dt_bauhaus_show_popup(widget);
3886 }
3887 else if(event->button == 2)
3888 {
3889 // middle click : reset zoom range to soft min/max
3890 _slider_zoom_range(w, 0);
3891 }
3892 return TRUE;
3893 }
3894 return FALSE;
3895}
3896
3897static gboolean dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3898{
3899 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3901
3903
3904 // is_dragging is set TRUE no matter what on button_press, except if it's a double click.
3905 // double click is the only event handled in button_press, otherwise we assume every event
3906 // is drag and drop, and handle the final drag coordinate here.
3907 if(d->is_dragging)
3908 {
3909 d->is_dragging = 0;
3910 dt_gui_throttle_cancel(widget);
3911
3912 if(event->button == 1)
3913 {
3915 return TRUE;
3916 }
3917 }
3918 return FALSE;
3919}
3920
3921static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
3922{
3923 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3925 _bh_active_region_t activated = BH_REGION_OUT; // = 0
3926
3927 if(d->is_dragging && event->state & GDK_BUTTON1_MASK)
3928 {
3929 double event_x = event->x;
3930 double event_y = event->y;
3931 double main_width;
3932 activated = _bh_get_active_region(widget, &event_x, &event_y, &main_width, NULL);
3933
3934 w->bauhaus->mouse_x = event_x;
3935 w->bauhaus->mouse_y = event_y;
3936 dt_bauhaus_slider_set_normalized(w, event_x / main_width, TRUE, TRUE);
3937 }
3938
3939 return activated;
3940}
3941
3943{
3944 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3945 w->no_accels = TRUE;
3946}
3947
3949{
3950 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3951 w->no_module_list = TRUE;
3952}
3953
3955{
3956 struct dt_bauhaus_widget_t *w = (struct dt_bauhaus_widget_t *)widget;
3958}
3959
3960
3961// clang-format off
3962// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3963// vim: shiftwidth=2 expandtab tabstop=2 cindent
3964// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3965// 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:1775
static gboolean dt_bauhaus_popup_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
Definition bauhaus.c:906
int dt_bauhaus_slider_get_digits(GtkWidget *widget)
Definition bauhaus.c:3541
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:1849
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:1647
_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:2682
void dt_bauhaus_slider_set_digits(GtkWidget *widget, int val)
Definition bauhaus.c:3534
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:1807
void dt_bauhaus_combobox_insert(GtkWidget *widget, const char *text, int pos)
Definition bauhaus.c:2118
static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event)
Definition bauhaus.c:3371
void dt_bauhaus_slider_set_hard_max(GtkWidget *widget, float val)
Definition bauhaus.c:1583
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:2189
static gboolean dt_bauhaus_popup_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
Definition bauhaus.c:898
static gboolean _widget_scroll(GtkWidget *widget, GdkEventScroll *event)
Definition bauhaus.c:3305
void dt_bauhaus_slider_clear_stops(GtkWidget *widget)
Definition bauhaus.c:2364
static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
Definition bauhaus.c:3921
static void _delayed_slider_commit(gpointer data)
Definition bauhaus.c:3625
static gboolean dt_bauhaus_popup_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition bauhaus.c:955
static void dt_bauhaus_widget_reject(struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:2582
char * dt_bauhaus_slider_get_text(GtkWidget *w, float val)
Definition bauhaus.c:3497
void dt_bauhaus_combobox_set_selected_text_align(GtkWidget *widget, const dt_bauhaus_combobox_alignment_t text_align)
Definition bauhaus.c:2092
void dt_bauhaus_combobox_set_editable(GtkWidget *widget, int editable)
Definition bauhaus.c:2074
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:3172
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:3588
static void dt_bauhaus_window_show(GtkWidget *w, gpointer user_data)
Definition bauhaus.c:1052
static void _style_updated(GtkWidget *widget)
Definition bauhaus.c:1793
void dt_bauhaus_cleanup(dt_bauhaus_t *bauhaus)
Definition bauhaus.c:1500
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:1121
void dt_bauhaus_slider_set_default(GtkWidget *widget, float def)
Definition bauhaus.c:1640
const char * dt_bauhaus_combobox_get_entry(GtkWidget *widget, int pos)
Definition bauhaus.c:2199
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:2481
void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, float b)
Definition bauhaus.c:2372
#define DT_BAUHAUS_FOCUS_IDLE_MAX_TRIES
Definition bauhaus.c:446
void dt_bauhaus_load_theme(dt_bauhaus_t *bauhaus)
Definition bauhaus.c:1289
float dt_bauhaus_slider_get(GtkWidget *widget)
Definition bauhaus.c:3483
void dt_bauhaus_combobox_set_text(GtkWidget *widget, const char *text)
Definition bauhaus.c:2209
static void dt_bauhaus_draw_indicator(struct dt_bauhaus_widget_t *w, float pos, cairo_t *cr, float wd)
Definition bauhaus.c:2409
void dt_bauhaus_combobox_entry_set_sensitive(GtkWidget *widget, int pos, gboolean sensitive)
Definition bauhaus.c:2354
gboolean dt_bauhaus_combobox_set_from_value(GtkWidget *widget, int value)
Definition bauhaus.c:2330
_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:1762
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:1786
void dt_bauhaus_combobox_from_widget(dt_bauhaus_t *bh, dt_bauhaus_widget_t *w, dt_gui_module_t *self)
Definition bauhaus.c:1912
static void _get_preferred_width(GtkWidget *widget, gint *minimum_size, gint *natural_size)
Definition bauhaus.c:3138
void dt_bauhaus_combobox_insert_separator_with_height(GtkWidget *widget, int pos, const float row_height_factor)
Definition bauhaus.c:2143
static void _delayed_combobox_commit(gpointer data)
Definition bauhaus.c:2218
static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
Definition bauhaus.c:3704
float dt_bauhaus_slider_get_soft_max(GtkWidget *widget)
Definition bauhaus.c:1633
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:1710
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:3666
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:1617
GtkWidget * dt_bauhaus_combobox_from_conf(dt_bauhaus_t *bh, dt_gui_module_t *self, const char *confkey)
Definition bauhaus.c:1868
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:2256
void dt_bauhaus_slider_set_feedback(GtkWidget *widget, int feedback)
Definition bauhaus.c:3580
static double get_slider_line_offset(const double pos, const double scale, const double x, double y, const double line_height)
Definition bauhaus.c:825
static gboolean _resize_handle_cursor(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition bauhaus.c:1101
void dt_bauhaus_disable_module_list(GtkWidget *widget)
Definition bauhaus.c:3948
static void _widget_finalize(GObject *widget)
Definition bauhaus.c:1231
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:1862
float dt_bauhaus_slider_get_val(GtkWidget *widget)
Definition bauhaus.c:3491
static dt_bauhaus_combobox_data_t * _combobox_data(GtkWidget *widget)
Definition bauhaus.c:1931
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:2123
int dt_bauhaus_combobox_get(GtkWidget *widget)
Definition bauhaus.c:2347
void dt_bauhaus_widget_set_quad_toggle(GtkWidget *widget, int toggle)
Definition bauhaus.c:1720
static int _combobox_selectable_count(const dt_bauhaus_combobox_data_t *d)
Count selectable combobox entries.
Definition bauhaus.c:1949
static gboolean dt_bauhaus_popup_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:982
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:3618
#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:3828
void dt_bauhaus_combobox_set_entries_ellipsis(GtkWidget *widget, PangoEllipsizeMode ellipis)
Definition bauhaus.c:2064
void dt_bauhaus_slider_set_step(GtkWidget *widget, float val)
Definition bauhaus.c:3548
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:868
static GdkRGBA * default_color_assign()
Definition bauhaus.c:647
void dt_bauhaus_slider_set_soft_max(GtkWidget *widget, float val)
Definition bauhaus.c:1624
void dt_bauhaus_combobox_insert_separator(GtkWidget *widget, int pos)
Definition bauhaus.c:2138
void dt_bauhaus_show_popup(GtkWidget *widget)
Definition bauhaus.c:3188
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:3954
int dt_bauhaus_combobox_length(GtkWidget *widget)
Definition bauhaus.c:2155
void dt_bauhaus_widget_set_quad_active(GtkWidget *widget, int active)
Definition bauhaus.c:1726
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:1551
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:1576
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:2038
gpointer dt_bauhaus_combobox_get_data(GtkWidget *widget)
Definition bauhaus.c:2179
void dt_bauhaus_combobox_add_populate_fct(GtkWidget *widget, void(*fct)(GtkWidget *w, void *module))
Definition bauhaus.c:2002
static void _bauhaus_widget_init(dt_bauhaus_t *bauhaus, dt_bauhaus_widget_t *w, dt_gui_module_t *self)
Definition bauhaus.c:1518
void dt_bauhaus_combobox_add_separator(GtkWidget *widget)
Definition bauhaus.c:2050
static float _get_combobox_max_width(GtkWidget *widget)
Definition bauhaus.c:2907
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:2010
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:2085
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:848
const char * dt_bauhaus_widget_get_label(GtkWidget *widget)
Definition bauhaus.c:1696
int dt_bauhaus_widget_get_quad_active(GtkWidget *widget)
Definition bauhaus.c:1743
void dt_bauhaus_slider_set_soft_min(GtkWidget *widget, float val)
Definition bauhaus.c:1608
void dt_bauhaus_disable_accels(GtkWidget *widget)
Definition bauhaus.c:3942
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:1179
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:3897
static gchar * _build_label(const struct dt_bauhaus_widget_t *w)
Definition bauhaus.c:2677
const char * dt_bauhaus_combobox_get_text(GtkWidget *widget)
Definition bauhaus.c:2162
void dt_bauhaus_slider_set_val(GtkWidget *widget, float val)
Definition bauhaus.c:3528
void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
Definition bauhaus.c:3506
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:3555
static void dt_bauhaus_widget_accept(struct dt_bauhaus_widget_t *w, gboolean timeout)
Definition bauhaus.c:2599
static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf)
Definition bauhaus.c:2962
static gboolean _enter_leave(GtkWidget *widget, GdkEventCrossing *event)
Definition bauhaus.c:1063
static void _slider_add_step(GtkWidget *widget, float delta, guint state)
Definition bauhaus.c:3286
void dt_bauhaus_widget_press_quad(GtkWidget *widget)
Definition bauhaus.c:1749
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:1986
void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
Definition bauhaus.c:2301
#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:1653
void dt_bauhaus_slider_set_hard_min(GtkWidget *widget, float val)
Definition bauhaus.c:1559
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:1780
void dt_bauhaus_combobox_remove_at(GtkWidget *widget, int pos)
Definition bauhaus.c:2101
GtkWidget * dt_bauhaus_combobox_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1842
void dt_bauhaus_widget_set_quad_visibility(GtkWidget *widget, const gboolean visible)
Definition bauhaus.c:1736
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:3156
gboolean dt_bauhaus_combobox_set_from_text(GtkWidget *widget, const char *text)
Definition bauhaus.c:2311
static gboolean _resize_handle_motion(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
Definition bauhaus.c:1163
void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
Definition bauhaus.c:3598
float dt_bauhaus_slider_get_hard_max(GtkWidget *widget)
Definition bauhaus.c:1601
static void dt_bauhaus_draw_quad(struct dt_bauhaus_widget_t *w, cairo_t *cr, const double x, const double y)
Definition bauhaus.c:2443
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:1966
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:2016
void dt_bauhaus_widget_set_quad_paint(GtkWidget *widget, dt_bauhaus_quad_paint_f f, int paint_flags, void *paint_data)
Definition bauhaus.c:1702
static gboolean dt_bauhaus_popup_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:961
void dt_bauhaus_combobox_add_with_tooltip(GtkWidget *widget, const char *text, const char *tooltip)
Definition bauhaus.c:2021
void dt_bauhaus_combobox_add_separator_with_height(GtkWidget *widget, const float row_height_factor)
Definition bauhaus.c:2055
dt_bauhaus_t * dt_bauhaus_init()
Definition bauhaus.c:1382
static void dt_bh_init(DtBauhausWidget *class)
Definition bauhaus.c:1057
void dt_bauhaus_slider_set_factor(GtkWidget *widget, float factor)
Definition bauhaus.c:3611
static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition bauhaus.c:3423
void dt_bauhaus_combobox_add_aligned(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align)
Definition bauhaus.c:2033
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:1268
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:808
#define dt_control_change_cursor(cursor)
Definition control.h:116
darktable_t darktable
Definition darktable.c:181
void dt_print(dt_debug_thread_t thread, const char *msg,...)
Definition darktable.c:1542
@ DT_DEBUG_SHORTCUTS
Definition darktable.h:738
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:463
#define dt_free(ptr)
Definition darktable.h:456
static const dt_aligned_pixel_simd_t value
Definition darktable.h:577
static gboolean dt_modifier_is(const GdkModifierType state, const GdkModifierType desired_modifier_mask)
Definition darktable.h:893
#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:281
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:1475
gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, int *delta_x, int *delta_y)
Definition gtk.c:219
void dt_capitalize_label(gchar *text)
Definition gtk.c:3150
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:316
#define dt_accels_new_darkroom_action(a, b, c, d, e, f, g)
Definition gtk.h:430
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:2169
void dt_iop_gui_set_expanded(dt_iop_module_t *module, gboolean expanded, gboolean collapse_others)
Definition imageop.c:2321
dt_introspection_type_t
static const float x
static int precision(double x, double adj)
Definition lens.cc:1497
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:775
struct dt_develop_t * develop
Definition darktable.h:770
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:1092
dt_bauhaus_resize_handle_resize_f resize
Definition bauhaus.c:1093
GtkOrientation orientation
Definition bauhaus.c:1090
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:224
int32_t reset
Definition gtk.h:172
dt_accels_t * accels
Definition gtk.h:194
GtkWidget * has_scroll_focus
Definition gtk.h:228
dt_ui_t * ui
Definition gtk.h:164
The dt_gui_module_t type is the intersection between a dt_lib_module_t and a dt_iop_module_t structur...
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