From 48a86a9c0353f87444fd6956f087af933ecad599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20PIERRE?= Date: Fri, 24 Nov 2023 18:31:54 +0100 Subject: [PATCH] =?UTF-8?q?Bauhaus:=C2=A0major=20refactoring/partial=20rew?= =?UTF-8?q?rite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. simplify the control flow, avoid recomputing everything all the time, limit redrawings 2. have cursor coordinates computed only in motion_notify callbacks 3. dispatch value-changed events only on button_released events (instead of everywhyre, button_pressed and motion_notify), so we don't start a pipeline recompute on every mouse interaction. 4. be sure to redraw widgets on every interaction BEFORE sending value-changed signal and dispatching pipeline params commits. That makes the GUI more responsive and less frustrating, giving immediate feedback about value changing, while the pipeline recomputes may take some time 5. have a timeout on all scrolling events (comboboxes and sliders) so intermediate scroll steps don't lead to pipeline recomputes until scrolling is finished. This may lead to a small lag in GUI response when scrolling on comboboxes, but it can prevents many intermediate useless pipeline recomputes, so overall it's better. 6. dispatch value-changed event ONLY if the value actually changed. 7. Remove duplicated (copy-pasted) code computing coordinates and translations. All pixel computations are done in getters/setters functions in one place. Fix many inconsistencies in coordinates recomputings because of isolated changes not propagated in all the right places. 8. Handle text drawing using uniform bounding-box adjustment. 9. Remove useless features needlessly complexifying code, like the curve and center alignment 10. Do not shift the combobox popup in the viewport when scrolling. This was meant to keep the overlayed item in front of the (non-popup) label, but there is no guaranty that the shifted popup stays entirely visible in viewport. We rely on native Gtk popup positionning and that's it. 11. Dispatch hovered and active CSS rules on pseudo-elements in comboboxes, allowing to style them. 12. Replace expensive calls to powf by optimized integer ipow. 13. Handle clicks on comboboxes chevrons "quad" too, as in any select field in any GUI toolkit. 14. Remove the ability to add shortcuts on sliders and comboboxes because that things needs to be removed entirely. Will be replaced with native Gtk accels that perfectly did the job until 2021. 15. Add shitloads of Doxygen docstrings for future dev doc. 16. Fix a couple of silent bugs : doing boolean operations on floats, comparing floats with integers or double, etc. --- src/bauhaus/bauhaus.c | 2322 +++++++++++++++++------------------------ src/bauhaus/bauhaus.h | 21 +- src/develop/imageop.c | 23 + src/iop/ashift.c | 2 - src/libs/collect.c | 2 - 5 files changed, 1007 insertions(+), 1363 deletions(-) diff --git a/src/bauhaus/bauhaus.c b/src/bauhaus/bauhaus.c index 0d024086b48b..1d7dd71158ad 100644 --- a/src/bauhaus/bauhaus.c +++ b/src/bauhaus/bauhaus.c @@ -1,6 +1,7 @@ /* - This file is part of darktable, - Copyright (C) 2012-2022 darktable developers. + This file is part of Ansel, + Copyright (C) 2012-2021 darktable developers. + Copyright (C) 2022-2023 Aurélien Pierre. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,6 +20,7 @@ #include "bauhaus/bauhaus.h" #include "common/calculator.h" #include "common/darktable.h" +#include "common/math.h" #include "control/conf.h" #include "develop/develop.h" #include "develop/imageop.h" @@ -41,17 +43,27 @@ enum // Sliders DT_ACTION_ELEMENT_VALUE = 0, DT_ACTION_ELEMENT_BUTTON = 1, - DT_ACTION_ELEMENT_FORCE = 2, + DT_ACTION_ELEMENT_FORCE = 2, // set slider value beyond visible range if soft min/max DT_ACTION_ELEMENT_ZOOM = 3, // Combos DT_ACTION_ELEMENT_SELECTION = 0, -//DT_ACTION_ELEMENT_BUTTON = 1, }; // INNER_PADDING is the horizontal space between slider and quad // and vertical space between labels and slider baseline -static const double INNER_PADDING = 4.0; +#define INNER_PADDING DT_PIXEL_APPLY_DPI(4) +#define INTERNAL_PADDING 2. * INNER_PADDING + +// WARNING +// A lot of GUI setters/getters functions used to have type checking on input widgets +// and silently returned early if the types were not ok (like trying to set a combobox using slider methods). +// This only hides programmers mistakes in a way that prevents the soft to crash, +// but it still leads to faulty GUI interactions and inconsistent widgets values that might be hard to spot. +// In november 2023, they got removed in order to fail explicitly and possibly crash. +// If that's not enough, we can always add assertions in the code. + +#define DEBUG 0 // fwd declare static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data); @@ -63,26 +75,262 @@ static void _get_preferred_width(GtkWidget *widget, gint *minimum_size, gint *na static void _style_updated(GtkWidget *widget); static void dt_bauhaus_widget_accept(dt_bauhaus_widget_t *w); static void dt_bauhaus_widget_reject(dt_bauhaus_widget_t *w); -static void _bauhaus_combobox_set(dt_bauhaus_widget_t *w, const int pos, const gboolean mute); -void bauhaus_request_focus(dt_bauhaus_widget_t *w) +// !!! EXECUTIVE NOTE !!! +// Sizing and spacing need to be declared once only in getters/setters functions below. +// The rest of the code accesses those values only through the getters. +// Doxygen docstrings need to be added to explain what is computed, based on what. +// Code changes that recompute sizing or coordinates outside of getters/setters will be refused. + +/** + * @brief Update the box margin and padding properties of the widget w + * by reading CSS context. + * + * @param w The widget to update and from which the CSS context is read. + */ +static void _margins_retrieve(dt_bauhaus_widget_t *w) { - if(w->module && w->module->type == DT_ACTION_TYPE_IOP_INSTANCE) - dt_iop_request_focus((dt_iop_module_t *)w->module); - gtk_widget_grab_focus(GTK_WIDGET(w)); - gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE); - darktable.gui->has_scroll_focus = GTK_WIDGET(w); + if(!w->margin) w->margin = gtk_border_new(); + if(!w->padding) w->padding = gtk_border_new(); + GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(w)); + const GtkStateFlags state = gtk_widget_get_state_flags(GTK_WIDGET(w)); + gtk_style_context_get_margin(context, state, w->margin); + gtk_style_context_get_padding(context, state, w->padding); +} + +/** + * @brief Get the total height of a GUI row containing a line of text + top and bottom padding. + * + * This applies to comboboxes list elements only. Sliders text lines have only bottom padding. + * + * @return float + */ +static float _bh_get_row_height() +{ + return darktable.bauhaus->line_height + INTERNAL_PADDING; } -static float _widget_get_quad_width(dt_bauhaus_widget_t *w) +/** + * @brief Get the width of the quad without padding + * + * @param w + * @return double + */ +static double _widget_get_quad_width(dt_bauhaus_widget_t *w) { if(w->show_quad) - return darktable.bauhaus->quad_width + INNER_PADDING; + return darktable.bauhaus->quad_width; else - return .0f; + return 0.; +} + +/** + * @brief Get the total width of the main Bauhaus widget area, accounting for padding and margins. + * + * @param w Pointer to the structure holding the widget attributes, aka the dt_bauhaus_widget_t + * @param widget Actual GtkWidget to get allocation from. Can be NULL if it's the same as the Bauhaus widget. + * @return double + */ +static double _widget_get_total_width(dt_bauhaus_widget_t *w, GtkWidget *widget) +{ + GtkWidget *box_reference = (widget) ? widget : GTK_WIDGET(w); + GtkAllocation allocation; + gtk_widget_get_allocation(box_reference, &allocation); + return allocation.width - w->margin->left - w->margin->right - w->padding->left - w->padding->right; +} + +/** + * @brief Get the width of the main Bauhaus widget area (slider scale or combobox), accounting for quad space, padding and margins + * + * @param w Pointer to the structure holding the widget attributes, aka the dt_bauhaus_widget_t + * @param widget Actual GtkWidget to get allocation from. Can be NULL if it's the same as the Bauhaus widget. + * @param total_width Pointer where to write the total width of the widget, which is used in intermediate computations. + * This will spare another call to `gtk_widget_get_allocation()` if both are needed. Can be NULL. + * @return double + */ +static double _widget_get_main_width(dt_bauhaus_widget_t *w, GtkWidget *widget, double *total_width) +{ + const double tot_width = _widget_get_total_width(w, widget); + if(total_width) *total_width = tot_width; + return tot_width - _widget_get_quad_width(w) - 2. * INNER_PADDING; +} + +/** + * @brief Get the height of the main Bauhaus widget area (slider scale or combobox), that is the box allocation minus padding and margins. + * + * @param w Pointer to the structure holding the widget attributes, aka the dt_bauhaus_widget_t + * @param widget Actual GtkWidget to get allocation from. Can be NULL if it's the same as the Bauhaus widget. + * @return double + */ +static double _widget_get_main_height(dt_bauhaus_widget_t *w, GtkWidget *widget) +{ + GtkWidget *box_reference = (widget) ? widget : GTK_WIDGET(w); + GtkAllocation allocation; + gtk_widget_get_allocation(box_reference, &allocation); + return allocation.height - w->margin->top - w->margin->bottom - w->padding->top - w->padding->bottom; +} + +static double _get_combobox_height(GtkWidget *widget) +{ + dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; + return w->margin->top + w->padding->top + w->margin->bottom + w->padding->bottom + + darktable.bauhaus->line_height; +} + +static double _get_slider_height(GtkWidget *widget) +{ + dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; + return w->margin->top + w->padding->top + w->margin->bottom + w->padding->bottom + INNER_PADDING / 2. + + 2. * darktable.bauhaus->border_width + darktable.bauhaus->line_height + darktable.bauhaus->marker_size; +} + +static double _get_indicator_y_position() +{ + return darktable.bauhaus->line_height + INNER_PADDING + darktable.bauhaus->baseline_size / 2.0f; +} + +static double _get_slider_bar_height() +{ + // Total height of the text label + slider baseline, discarding padding + return darktable.bauhaus->line_height + INNER_PADDING + darktable.bauhaus->baseline_size; + +} + +static double _get_combobox_popup_height(dt_bauhaus_widget_t *w) +{ + dt_iop_module_t *module = (dt_iop_module_t *)(w->module); + const dt_bauhaus_combobox_data_t *d = &w->data.combobox; + + // Need to run the populating callback first for dynamically-populated ones. + if(d->populate) d->populate(GTK_WIDGET(w), &module); + if(!d->entries->len) return 0.; + + int num_lines = d->entries->len; + + // Add an extra sit for user keyboard input if any + if(darktable.bauhaus->keys_cnt > 0) num_lines += 1; + + return num_lines * _bh_get_row_height(); +} + + +/** + * @brief Translate in-place the cursor coordinates within the widget or popup according to padding and margin, so + * x = 0 is mapped to the starting point of the slider. + * + * @param x Cursor coordinate x + * @param y Cursor coordinate y + * @param w Widget + */ +static void _translate_cursor(double *x, double *y, dt_bauhaus_widget_t *w) +{ + *x -= w->margin->left + w->padding->left; + *y -= w->margin->top + w->padding->top; +} + +// Convenience state for cursor position over widget +typedef enum _bh_active_region_t +{ + BH_REGION_OUT = 0, // we are outside the padding box + BH_REGION_MAIN, // we are on the slider scale or combobox label/value, aka out of the quad button + BH_REGION_QUAD, // we are on the quad button +} _bh_active_region_t; + +/** + * @brief Check if we have user cursor over quad area or over the slider/main area, then correct cursor coordinates for widget padding and margin. + * For sliders, it means that x = 0 is mapped to the origin of the scale. + * + * @param widget Bauhaus widget + * @param event User event + * @param x Initial coordinate x of the cursor. Will be corrected in-place for margin and padding. + * @param y Initial coordinate y of the cursor. Will be corrected in-place for margin and padding. + * @param width Return pointer for the main width. Can be NULL. Caller owns the memory and is responsible for freeing it. + * @param popup Pointer to the Gtk window for the popup if any. Can be NULL. Height is computed from there if defined. + * @return _bh_active_region_t the region being selected, or BH_REGION_OUT (aka 0) if the cursor is outside the padding box (inactive region). + */ +static _bh_active_region_t _bh_get_active_region(GtkWidget *widget, double *x, double *y, double *width, GtkWidget *popup) +{ + dt_bauhaus_widget_t *const w = DT_BAUHAUS_WIDGET(widget); + + // The widget to use as a reference to fetch allocation and compute sizes + GtkWidget *box_reference = (popup) ? popup : widget; + const double quad_width = _widget_get_quad_width(w); + const double main_width = _widget_get_main_width(w, box_reference, NULL); + const double main_height = _widget_get_main_height(w, box_reference); + + if(width) *width = main_width; + _translate_cursor(x, y, w); + + // Check if we are within vertical bounds + if(!(*y >= 0. && *y <= main_height)) return BH_REGION_OUT; + + // Check where we are horizontally + if(*x >= 0. && *x <= main_width + INTERNAL_PADDING) + return BH_REGION_MAIN; + else if (*x >= main_width + INTERNAL_PADDING && + *x <= main_width + INTERNAL_PADDING + quad_width) + return BH_REGION_QUAD; + + return BH_REGION_OUT; +} + +/** + * @brief Round a slider numeric value to the number of digits specified in the widget `w`. + * + * @param w + * @param x Value to round. + * @return float + */ +static float _bh_round_to_n_digits(dt_bauhaus_widget_t *w, float x) +{ + const dt_bauhaus_slider_data_t *const d = &w->data.slider; + const float factor = (float)ipow(10, d->digits); + return roundf(x * factor) / factor; +} + +/** + * @brief Return the minimum representable value step, for current UI scaling factor and number of digits. + * + * @param w + * @return float + */ +static float _bh_slider_get_min_step(dt_bauhaus_widget_t *w) +{ + const dt_bauhaus_slider_data_t *d = &w->data.slider; + return d->factor * (float)ipow(10, d->digits); +} + +static float _bh_slider_get_scale(dt_bauhaus_widget_t *w) +{ + const dt_bauhaus_slider_data_t *d = &w->data.slider; + return 5.0f / (_bh_slider_get_min_step(w) * (d->max - d->min)); +} + +static void _bh_combobox_get_hovered_entry() +{ + if(darktable.bauhaus->current->type == DT_BAUHAUS_COMBOBOX) + { + // Mark which combobox entry is active + dt_bauhaus_combobox_data_t *d = &darktable.bauhaus->current->data.combobox; + d->hovered = (int)floorf(darktable.bauhaus->mouse_y / _bh_get_row_height()); + } +} + +void bauhaus_request_focus(dt_bauhaus_widget_t *w) +{ + if(w->module && w->module->type == DT_ACTION_TYPE_IOP_INSTANCE) + dt_iop_request_focus((dt_iop_module_t *)w->module); + + gtk_widget_grab_focus(GTK_WIDGET(w)); + gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE); + + // Scroll focus needs to be managed separately from Gtk focus + // because of Gtk notebooks (tabs): Gtk gives focus automatically to the first + // notebook child, which is not what we want for scroll event capture. + darktable.gui->has_scroll_focus = GTK_WIDGET(w); } -static void _combobox_next_sensitive(dt_bauhaus_widget_t *w, int delta, const gboolean mute) +static void _combobox_next_sensitive(dt_bauhaus_widget_t *w, int delta) { dt_bauhaus_combobox_data_t *d = &w->data.combobox; @@ -99,8 +347,8 @@ static void _combobox_next_sensitive(dt_bauhaus_widget_t *w, int delta, const gb } cur += step; } - - _bauhaus_combobox_set(w, new_pos, mute); + d->hovered = new_pos; + dt_bauhaus_combobox_set(GTK_WIDGET(w), d->hovered); } static dt_bauhaus_combobox_entry_t *new_combobox_entry(const char *label, dt_bauhaus_combobox_alignment_t alignment, @@ -124,203 +372,161 @@ static void free_combobox_entry(gpointer data) free(entry); } -static GdkRGBA * default_color_assign() +static GdkRGBA *default_color_assign() { // helper to initialize a color pointer with red color as a default GdkRGBA color = {.red = 1.0f, .green = 0.0f, .blue = 0.0f, .alpha = 1.0f}; return gdk_rgba_copy(&color); } -static void _margins_retrieve(dt_bauhaus_widget_t *w) -{ - if(!w->margin) w->margin = gtk_border_new(); - if(!w->padding) w->padding = gtk_border_new(); - GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(w)); - const GtkStateFlags state = gtk_widget_get_state_flags(GTK_WIDGET(w)); - gtk_style_context_get_margin(context, state, w->margin); - gtk_style_context_get_padding(context, state, w->padding); -} - void dt_bauhaus_widget_set_section(GtkWidget *widget, const gboolean is_section) { dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); w->is_section = is_section; } +// Vertical alignment of text in its bounding box +typedef enum _bh_valign_t +{ + BH_ALIGN_TOP = 0, + BH_ALIGN_BOTTOM = 1, + BH_ALIGN_MIDDLE = 2 +} _bh_valign_t; + +// Horizontal alignment of text in its bounding box +typedef enum _bh_halign_t +{ + BH_ALIGN_LEFT = 0, + BH_ALIGN_RIGHT = 1, + BH_ALIGN_CENTER = 2 +} _bh_halign_t; + /** - * @brief Draw some text into a Cairo object. TODO: unfuck that, too many unrelated parameters here. + * @brief Display text aligned in a bounding box, with pseudo-classes properties handled, and optional background color. * - * is_markup and is_label should be elements of an enum type. - * calc_only has no business here. - * Context should probably be fetched directly from Gtk and not cached/copied. - * - * @param w Bauhaus widget where to add the text (combobox or slider) - * @param context Gtk style context (holding CSS properties) - * @param cr Cairo object to draw in - * @param text Text context to render - * @param x_pos - * @param y_pos - * @param max_width - * @param right_aligned - * @param calc_only If TRUE, don't actually draw the text but only compute the size to allocate (WTF ? Seriously ?) - * @param ellipsize - * @param is_markup - * @param is_label - * @param width - * @param height - * @param ignore_font_style If TRUE, discard font weight and style that might be set on pseudo-classes. - * @return int + * @param w The current widget + * @param context Gtk CSS context + * @param cr Cairo drawing object + * @param bounding_box The bounding box in which the text should fit. + * @param text The text content to display. + * @param halign Horizontal alignment within the bounding box + * @param valign Vertical alignment within the bounding box + * @param ellipsize Pango ellipsization strategy, used only if text overflows its bounding box. + * @param bg_color Background color to paint in the bounding box. Can be NULL. + * @param width Pointer where text spanning width will be returned in Cairo units. Can be NULL. + * @param height Pointer where text spanning height will be returned in Cairo units. Can be NULL. + * @param ignore_pseudo_classes Disregard styling done in pseudo-classes and use the normal style. */ -static int show_pango_text(dt_bauhaus_widget_t *w, GtkStyleContext *context, cairo_t *cr, const char *text, - float x_pos, float y_pos, float max_width, gboolean right_aligned, gboolean calc_only, - PangoEllipsizeMode ellipsize, gboolean is_markup, gboolean is_label, float *width, - float *height, gboolean ignore_font_style) +static void show_pango_text(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, + gboolean ignore_pseudo_classes) { + if(text == NULL) return; + + // Prepare context and font properties PangoLayout *layout = pango_cairo_create_layout(cr); + PangoFontDescription *font_desc = NULL; + GtkStateFlags state = (ignore_pseudo_classes) ? GTK_STATE_NORMAL + : gtk_widget_get_state_flags(GTK_WIDGET(w)); + gtk_style_context_get(context, state, "font", &font_desc, NULL); + pango_layout_set_font_description(layout, font_desc); - if(max_width > 0) - { - pango_layout_set_ellipsize(layout, ellipsize); - pango_layout_set_width(layout, (int)(PANGO_SCALE * max_width + 0.5f)); - } + // FIXME: Is that still needed in 2023 ? + pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi); - if(text) - { - if(is_markup) - pango_layout_set_markup(layout, text, -1); - else - pango_layout_set_text(layout, text, -1); - } - else - { - // length of -1 is not allowed with NULL string (wtf) - pango_layout_set_text(layout, NULL, 0); - } + // Set the actual text + pango_layout_set_text(layout, text, -1); - PangoFontDescription *font_desc = - w->is_section && is_label - ? pango_font_description_copy_static(darktable.bauhaus->pango_sec_font_desc) - : pango_font_description_copy_static(darktable.bauhaus->pango_font_desc); + // Record Pango sizes, convert them to Cairo units and return them + int pango_width; + int pango_height; + pango_layout_get_size(layout, &pango_width, &pango_height); + double text_width = (double)pango_width / PANGO_SCALE; + double text_height = (double)pango_height / PANGO_SCALE; + if(width) *width = text_width; + if(height) *height = text_height; - // Update font properties for current text depending on :hover, :focused, etc. states - if(!ignore_font_style) + // Handle bounding box overflow if any + if(text_width > bounding_box->width) { - PangoWeight weight; - PangoStyle style; - gtk_style_context_get(context, gtk_widget_get_state_flags(GTK_WIDGET(w)), - "font-weight", &weight, - "font-style", &style, - NULL); - pango_font_description_set_weight(font_desc, weight); - pango_font_description_set_style(font_desc, style); + pango_layout_set_ellipsize(layout, ellipsize); + pango_layout_set_width(layout, (int)(PANGO_SCALE * bounding_box->width)); + text_width = bounding_box->width; + if(width) *width = text_width; } - pango_layout_set_font_description(layout, font_desc); - - PangoAttrList *attrlist = pango_attr_list_new(); - PangoAttribute *attr = pango_attr_font_features_new("tnum"); - pango_attr_list_insert(attrlist, attr); - pango_layout_set_attributes(layout, attrlist); - pango_attr_list_unref(attrlist); - - pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi); - - int pango_width, pango_height; - pango_layout_get_size(layout, &pango_width, &pango_height); - const float text_width = ((double)pango_width/PANGO_SCALE); - if(calc_only && width && height) + // Paint background color if any - useful to highlight elements in popup list + if(bg_color) { - *width = text_width; - *height = ((double)pango_height / PANGO_SCALE); + cairo_save(cr); + cairo_rectangle(cr, bounding_box->x, bounding_box->y, bounding_box->width, bounding_box->height); + cairo_set_source_rgba(cr, bg_color->red, bg_color->green, bg_color->blue, bg_color->alpha); + cairo_fill(cr); + cairo_restore(cr); } - if(right_aligned) x_pos -= text_width; - - if(!calc_only) + // Compute the coordinates of the top-left corner as to ensure proper alignment + // in bounding box given the dimensions of the label. + double x = 0; + switch(halign) { - cairo_move_to(cr, x_pos, y_pos); - pango_cairo_show_layout(cr, layout); + case BH_ALIGN_CENTER: + x = bounding_box->x + bounding_box->width / 2. - text_width / 2.; + break; + case BH_ALIGN_RIGHT: + x = bounding_box->x + bounding_box->width - text_width; + break; + case BH_ALIGN_LEFT: + default: + x = bounding_box->x; + break; } - pango_font_description_free(font_desc); - g_object_unref(layout); - - return text_width; -} - -// ------------------------------- -static gboolean _cursor_timeout_callback(gpointer user_data) -{ - if(darktable.bauhaus->cursor_blink_counter > 0) darktable.bauhaus->cursor_blink_counter--; - - darktable.bauhaus->cursor_visible = !darktable.bauhaus->cursor_visible; - gtk_widget_queue_draw(darktable.bauhaus->popup_area); - // this can be >0 when we haven't reached the desired number or -1 when blinking forever - if(darktable.bauhaus->cursor_blink_counter != 0) - return TRUE; - - darktable.bauhaus->cursor_timeout = 0; // otherwise the cursor won't come up when starting to type - return FALSE; -} - -static void _start_cursor(int max_blinks) -{ - darktable.bauhaus->cursor_blink_counter = max_blinks; - darktable.bauhaus->cursor_visible = FALSE; - if(darktable.bauhaus->cursor_timeout == 0) - darktable.bauhaus->cursor_timeout = g_timeout_add(500, _cursor_timeout_callback, NULL); -} - -static void _stop_cursor() -{ - if(darktable.bauhaus->cursor_timeout > 0) + double y = 0; + switch(valign) { - g_source_remove(darktable.bauhaus->cursor_timeout); - darktable.bauhaus->cursor_timeout = 0; - darktable.bauhaus->cursor_visible = FALSE; + case BH_ALIGN_MIDDLE: + y = bounding_box->y + bounding_box->height / 2. - text_height / 2.; + break; + case BH_ALIGN_BOTTOM: + y = bounding_box->y + bounding_box->height - text_height; + break; + case BH_ALIGN_TOP: + default: + y = bounding_box->y; + break; } -} -// ------------------------------- - -static void dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t *w, float pos); + // Actually (finally) draw everything in place + cairo_move_to(cr, x, y); + pango_cairo_show_layout(cr, layout); -static float slider_position_from_right(float width, dt_bauhaus_widget_t *w) -{ - // relative position (in widget) of the right bound of the slider corrected with the inner padding - return 1.0f - _widget_get_quad_width(w) / width; -} - -static float slider_coordinate(const float abs_position, const float width, dt_bauhaus_widget_t *w) -{ - // Translates an horizontal position relative to the slider - // in an horizontal position relative to the widget - const float left_bound = 0.0f; - const float right_bound = slider_position_from_right(width, w); // exclude the quad area on the right - return (left_bound + abs_position * (right_bound - left_bound)) * width; + // Cleanup + pango_font_description_free(font_desc); + g_object_unref(layout); } +static void dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t *w, float pos, gboolean raise); -static float get_slider_line_offset(float pos, float scale, float x, float y, float ht, const int width, - dt_bauhaus_widget_t *w) +static double get_slider_line_offset(const float pos, float scale, double x, double y, const double line_height) { - // ht is in [0,1] scale here - const float l = 0.0f; - const float r = slider_position_from_right(width, w); - - float offset = 0.0f; // handle linear startup and rescale y to fit the whole range again - if(y < ht) + // Note : all inputs are in relative coordinates, except pos + float offset = 0.f; + if(y < line_height) { - offset = (x - l) / (r - l) - pos; + offset = x - pos; } else { - y -= ht; - y /= (1.0f - ht); - - offset = (x - y * y * .5f - (1.0f - y * y) * (l + pos * (r - l))) - / (.5f * y * y / scale + (1.0f - y * y) * (r - l)); + // Renormalize y coordinates below the baseline + y = (y - line_height) / (1.0f - line_height); + offset = (x - sqf(y) * .5f - (1.0f - sqf(y)) * pos) + / (.5f * sqf(y) / scale + (1.0f - sqf(y))); } // clamp to result in a [0,1] range: if(pos + offset > 1.0f) offset = 1.0f - pos; @@ -329,51 +535,33 @@ static float get_slider_line_offset(float pos, float scale, float x, float y, fl } // draw a loupe guideline for the quadratic zoom in in the slider interface: -static void draw_slider_line(cairo_t *cr, float pos, float off, float scale, const int width, const int height, - const int ht, dt_bauhaus_widget_t *w) +static void draw_slider_line(cairo_t *cr, float pos, float off, float scale, const double width, const double height, + const double line_height, double line_width) { // pos is normalized position [0,1], offset is on that scale. // ht is in pixels here - const float r = slider_position_from_right(width, w); - const int steps = 64; - cairo_move_to(cr, width * (pos + off) * r, ht * .7f); - cairo_line_to(cr, width * (pos + off) * r, ht); + const double corrected_height = (height - line_height); + + cairo_set_line_width(cr, line_width); + cairo_move_to(cr, width * (pos + off), line_height); + const double half_line_width = line_width / 2.; for(int j = 1; j < steps; j++) { - const float y = j / (steps - 1.0f); - const float x = y * y * .5f * (1.f + off / scale) + (1.0f - y * y) * (pos + off) * r; - cairo_line_to(cr, x * width, ht + y * (height - ht)); + const double y = (double)j / (double)(steps - 1); + const double x = sqf(y) * .5f * (1.f + off / scale) + (1.0f - sqf(y)) * (pos + off); + cairo_line_to(cr, x * width - half_line_width, line_height + y * corrected_height); } } // ------------------------------- -static void combobox_popup_scroll(int amt) -{ - const dt_bauhaus_combobox_data_t *d = &darktable.bauhaus->current->data.combobox; - int old_value = d->active; - - _combobox_next_sensitive(darktable.bauhaus->current, amt, d->mute_scrolling); - - gint wx = 0, wy = 0; - const int skip = darktable.bauhaus->line_height; - GdkWindow *w = gtk_widget_get_window(darktable.bauhaus->popup_window); - gdk_window_get_origin(w, &wx, &wy); - gdk_window_move(w, wx, wy - skip * (d->active - old_value)); - - // make sure highlighted entry is updated: - darktable.bauhaus->mouse_x = 0; - darktable.bauhaus->mouse_y = d->active * skip + skip / 2; - gtk_widget_queue_draw(darktable.bauhaus->popup_area); -} - static void _slider_zoom_range(dt_bauhaus_widget_t *w, float zoom) { dt_bauhaus_slider_data_t *d = &w->data.slider; const float value = dt_bauhaus_slider_get(GTK_WIDGET(w)); - if(!zoom) + if((int)roundf(zoom) == 0) { d->min = d->soft_min; d->max = d->soft_max; @@ -382,8 +570,8 @@ static void _slider_zoom_range(dt_bauhaus_widget_t *w, float zoom) } // make sure current value still in zoomed range - const float min_visible = powf(10.0f, -d->digits) / d->factor; - const float multiplier = powf(2.0f, zoom/2); + const float min_visible = _bh_slider_get_min_step(w); + const float multiplier = exp2f(zoom / 2.f); const float new_min = value - multiplier * (value - d->min); const float new_max = value + multiplier * (d->max - value); if(new_min >= d->hard_min @@ -416,7 +604,7 @@ static gboolean dt_bauhaus_popup_scroll(GtkWidget *widget, GdkEventScroll *event if(dt_gui_get_scroll_unit_deltas(event, NULL, &delta_y)) { if(darktable.bauhaus->current->type == DT_BAUHAUS_COMBOBOX) - combobox_popup_scroll(delta_y); + _combobox_next_sensitive(darktable.bauhaus->current, delta_y); else { _slider_zoom_range(darktable.bauhaus->current, delta_y); @@ -428,89 +616,28 @@ static gboolean dt_bauhaus_popup_scroll(GtkWidget *widget, GdkEventScroll *event static gboolean dt_bauhaus_popup_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data) { - gtk_widget_queue_draw(darktable.bauhaus->popup_area); - - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); - - const GtkBorder *padding = darktable.bauhaus->popup_padding; - const int width = allocation.width - padding->left - padding->right; - const int height = allocation.height - padding->top - padding->bottom; - const int ht = darktable.bauhaus->line_height + INNER_PADDING * 2.0f; + dt_bauhaus_widget_t *w = darktable.bauhaus->current; + double event_x = event->x; + double event_y = event->y; + _bh_active_region_t active = _bh_get_active_region(GTK_WIDGET(w), &event_x, &event_y, NULL, widget); - gint wx, wy; - GdkWindow *window = gtk_widget_get_window(darktable.bauhaus->popup_window); - gdk_window_get_origin(window, &wx, &wy); +#if DEBUG + fprintf(stdout, "x: %i, y: %i, active: %i\n", (int)event_x, (int)event_y, active); +#endif - const float tol = 50; - if(event->x_root > wx + allocation.width + tol || event->y_root > wy + allocation.height + tol - || event->x_root < (int)wx - tol || event->y_root < (int)wy - tol) + if(active == BH_REGION_OUT) { dt_bauhaus_widget_reject(darktable.bauhaus->current); dt_bauhaus_hide_popup(); return TRUE; } - const float event_x = event->x_root - wx - padding->left; - const float event_y = event->y_root - wy - padding->top; - - if(darktable.bauhaus->keys_cnt == 0) _stop_cursor(); - - dt_bauhaus_widget_t *w = darktable.bauhaus->current; - switch(w->type) - { - case DT_BAUHAUS_COMBOBOX: - { - GdkRectangle workarea; - gdk_monitor_get_workarea(gdk_display_get_monitor_at_window(gdk_window_get_display(window), window), &workarea); - const gint workarea_bottom = workarea.y + workarea.height; - - float dy = 0; - const float move = darktable.bauhaus->mouse_y - event_y; - if(move > 0 && wy < workarea.y) - { - dy = (workarea.y - wy); - if(event->y_root >= workarea.y) - dy *= move / (darktable.bauhaus->mouse_y + wy + padding->top - workarea.y); - } - if(move < 0 && wy + allocation.height > workarea_bottom) - { - dy = (workarea_bottom - wy - allocation.height); - if(event->y_root <= workarea_bottom) - dy *= move / (darktable.bauhaus->mouse_y + wy + padding->top - workarea_bottom); - } - - darktable.bauhaus->mouse_x = event_x; - darktable.bauhaus->mouse_y = event_y - dy; - gdk_window_move(window, wx, wy + dy); - - break; - } - case DT_BAUHAUS_SLIDER: - { - const dt_bauhaus_slider_data_t *d = &w->data.slider; - const float mouse_off - = get_slider_line_offset(d->oldpos, 5.0 * powf(10.0f, -d->digits) / (d->max - d->min) / d->factor, - event_x / width, event_y / height, ht / (float)height, allocation.width, w); - if(!darktable.bauhaus->change_active) - { - if((darktable.bauhaus->mouse_line_distance < 0 && mouse_off >= 0) - || (darktable.bauhaus->mouse_line_distance > 0 && mouse_off <= 0)) - darktable.bauhaus->change_active = 1; - darktable.bauhaus->mouse_line_distance = mouse_off; - } - if(darktable.bauhaus->change_active) - { - // remember mouse position for motion effects in draw - darktable.bauhaus->mouse_x = event_x; - darktable.bauhaus->mouse_y = event_y; - dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off); - } - break; - } - default: - break; - } + // Pass-on new cursor coordinates corrected for padding and margin + // and start a redraw. Nothing else. + darktable.bauhaus->mouse_x = event_x; + darktable.bauhaus->mouse_y = event_y; + _bh_combobox_get_hovered_entry(); + gtk_widget_queue_draw(darktable.bauhaus->popup_area); return TRUE; } @@ -529,18 +656,6 @@ static gboolean dt_bauhaus_popup_button_release(GtkWidget *widget, GdkEventButto && (event->button == 1) && (event->time >= darktable.bauhaus->opentime + delay) && !darktable.bauhaus->hiding) { gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_ACTIVE, TRUE); - - // event might be in wrong system, transform ourselves: - gint wx, wy, x, y; - gdk_window_get_origin(gtk_widget_get_window(darktable.bauhaus->popup_window), &wx, &wy); - - gdk_device_get_position( - gdk_seat_get_pointer(gdk_display_get_default_seat(gtk_widget_get_display(widget))), 0, &x, &y); - darktable.bauhaus->end_mouse_x = x - wx; - darktable.bauhaus->end_mouse_y = y - wy; - const dt_bauhaus_combobox_data_t *d = &darktable.bauhaus->current->data.combobox; - if(!d->mute_scrolling) - dt_bauhaus_widget_accept(darktable.bauhaus->current); dt_bauhaus_hide_popup(); } else if(darktable.bauhaus->hiding) @@ -552,13 +667,6 @@ static gboolean dt_bauhaus_popup_button_release(GtkWidget *widget, GdkEventButto static gboolean dt_bauhaus_popup_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { - if(event->window != gtk_widget_get_window(widget)) - { - dt_bauhaus_widget_reject(darktable.bauhaus->current); - dt_bauhaus_hide_popup(); - return TRUE; - } - int delay = 0; g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL); if(event->button == 1) @@ -573,10 +681,18 @@ static gboolean dt_bauhaus_popup_button_press(GtkWidget *widget, GdkEventButton } else { - // only accept left mouse click - const GtkBorder *padding = darktable.bauhaus->popup_padding; - darktable.bauhaus->end_mouse_x = event->x - padding->left; - darktable.bauhaus->end_mouse_y = event->y - padding->top; + // only accept left mouse click. + // coordinates are set in motion_notify, which also makes sure they are within the valid range. + // problems appear with the cornercase where user didn't move the cursor since opening the popup. + // aka we need to re-read coordinates here. + double event_x = event->x; + double event_y = event->y; + _translate_cursor(&event_x, &event_y, darktable.bauhaus->current); + + darktable.bauhaus->end_mouse_x = darktable.bauhaus->mouse_x = event_x; + darktable.bauhaus->end_mouse_y = darktable.bauhaus->mouse_y = event_y; + + _bh_combobox_get_hovered_entry(); dt_bauhaus_widget_accept(darktable.bauhaus->current); } darktable.bauhaus->hiding = TRUE; @@ -630,7 +746,6 @@ static void _widget_finalize(GObject *widget) { dt_bauhaus_slider_data_t *d = &w->data.slider; if(d->timeout_handle) g_source_remove(d->timeout_handle); - free(d->grad_col); free(d->grad_pos); } else @@ -675,7 +790,6 @@ void dt_bauhaus_load_theme() GtkStyleContext *ctx = gtk_style_context_new(); GtkWidgetPath *path = gtk_widget_path_new(); const int pos = gtk_widget_path_append_type(path, GTK_TYPE_WIDGET); - gtk_widget_path_iter_add_class(path, pos, "plugin-ui"); gtk_style_context_set_path(ctx, path); gtk_style_context_set_screen (ctx, gtk_widget_get_screen(root_window)); @@ -717,14 +831,9 @@ void dt_bauhaus_load_theme() darktable.bauhaus->pango_font_desc = pfont; - if(darktable.bauhaus->pango_sec_font_desc) - pango_font_description_free(darktable.bauhaus->pango_sec_font_desc); - // now get the font for the section labels gtk_widget_path_iter_add_class(path, pos, "dt_section_label"); gtk_style_context_set_path(ctx, path); - gtk_style_context_get(ctx, GTK_STATE_FLAG_NORMAL, "font", &pfont, NULL); - darktable.bauhaus->pango_sec_font_desc = pfont; gtk_widget_path_free(path); @@ -734,7 +843,8 @@ void dt_bauhaus_load_theme() pango_layout_set_text(layout, "M", -1); pango_layout_set_font_description(layout, darktable.bauhaus->pango_font_desc); pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi); - int pango_width, pango_height; + int pango_width; + int pango_height; pango_layout_get_size(layout, &pango_width, &pango_height); g_object_unref(layout); cairo_destroy(cr); @@ -773,11 +883,9 @@ void dt_bauhaus_init() gtk_window_set_resizable(GTK_WINDOW(darktable.bauhaus->popup_window), FALSE); gtk_window_set_default_size(GTK_WINDOW(darktable.bauhaus->popup_window), 260, 260); - // gtk_window_set_modal(GTK_WINDOW(c->popup_window), TRUE); - // gtk_window_set_decorated(GTK_WINDOW(c->popup_window), FALSE); - - // for pie menu: - // gtk_window_set_position(GTK_WINDOW(c->popup_window), GTK_WIN_POS_MOUSE);// | GTK_WIN_POS_CENTER); + gtk_window_set_modal(GTK_WINDOW(darktable.bauhaus->popup_window), TRUE); + gtk_window_set_decorated(GTK_WINDOW(darktable.bauhaus->popup_window), FALSE); + gtk_window_set_attached_to(GTK_WINDOW(darktable.bauhaus->popup_window), NULL); // needed on macOS to avoid fullscreening the popup with newer GTK gtk_window_set_type_hint(GTK_WINDOW(darktable.bauhaus->popup_window), GDK_WINDOW_TYPE_HINT_POPUP_MENU); @@ -785,7 +893,6 @@ void dt_bauhaus_init() gtk_container_add(GTK_CONTAINER(darktable.bauhaus->popup_window), darktable.bauhaus->popup_area); gtk_widget_set_hexpand(darktable.bauhaus->popup_area, TRUE); gtk_widget_set_vexpand(darktable.bauhaus->popup_area, TRUE); - // gtk_window_set_title(GTK_WINDOW(c->popup_window), _("dtgtk control popup")); gtk_window_set_keep_above(GTK_WINDOW(darktable.bauhaus->popup_window), TRUE); gtk_window_set_gravity(GTK_WINDOW(darktable.bauhaus->popup_window), GDK_GRAVITY_STATIC); @@ -817,10 +924,6 @@ static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotio static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data); -static gboolean dt_bauhaus_combobox_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data); - -// static gboolean -// dt_bauhaus_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data); // end static init/cleanup // ================================================= @@ -870,20 +973,17 @@ void dt_bauhaus_slider_set_hard_min(GtkWidget* widget, float val) { dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; - float pos = dt_bauhaus_slider_get(widget); - d->hard_min = val; + float current_position = dt_bauhaus_slider_get(widget); + float desired_position = _bh_round_to_n_digits(w, val); d->min = MAX(d->min, d->hard_min); d->soft_min = MAX(d->soft_min, d->hard_min); - if(val > d->hard_max) dt_bauhaus_slider_set_hard_max(widget,val); - if(pos < val) - { - dt_bauhaus_slider_set(widget,val); - } - else - { - dt_bauhaus_slider_set(widget,pos); - } + if(desired_position > d->hard_max) + dt_bauhaus_slider_set_hard_max(widget,val); + + if(current_position < desired_position) + dt_bauhaus_slider_set(widget, desired_position); + // else nothing : old position is the new position, just the bound changes } float dt_bauhaus_slider_get_hard_min(GtkWidget* widget) @@ -897,20 +997,18 @@ void dt_bauhaus_slider_set_hard_max(GtkWidget* widget, float val) { dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; - float pos = dt_bauhaus_slider_get(widget); - d->hard_max = val; + float current_position = dt_bauhaus_slider_get(widget); + float desired_position = _bh_round_to_n_digits(w, val); + d->hard_max = desired_position; d->max = MIN(d->max, d->hard_max); d->soft_max = MIN(d->soft_max, d->hard_max); - if(val < d->hard_min) dt_bauhaus_slider_set_hard_min(widget,val); - if(pos > val) - { - dt_bauhaus_slider_set(widget,val); - } - else - { - dt_bauhaus_slider_set(widget,pos); - } + if(desired_position < d->hard_min) + dt_bauhaus_slider_set_hard_min(widget, desired_position); + + if(current_position > desired_position) + dt_bauhaus_slider_set(widget, desired_position); + // else nothing : old position is the new position, just the bound changes } float dt_bauhaus_slider_get_hard_max(GtkWidget* widget) @@ -925,8 +1023,8 @@ void dt_bauhaus_slider_set_soft_min(GtkWidget* widget, float val) dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; float oldval = dt_bauhaus_slider_get(widget); - d->min = d->soft_min = CLAMP(val,d->hard_min,d->hard_max); - dt_bauhaus_slider_set(widget,oldval); + d->min = d->soft_min = CLAMP(val, d->hard_min, d->hard_max); + dt_bauhaus_slider_set(widget, oldval); } float dt_bauhaus_slider_get_soft_min(GtkWidget* widget) @@ -941,8 +1039,8 @@ void dt_bauhaus_slider_set_soft_max(GtkWidget* widget, float val) dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; float oldval = dt_bauhaus_slider_get(widget); - d->max = d->soft_max = CLAMP(val,d->hard_min,d->hard_max); - dt_bauhaus_slider_set(widget,oldval); + d->max = d->soft_max = CLAMP(val, d->hard_min, d->hard_max); + dt_bauhaus_slider_set(widget, oldval); } float dt_bauhaus_slider_get_soft_max(GtkWidget* widget) @@ -961,8 +1059,8 @@ void dt_bauhaus_slider_set_default(GtkWidget *widget, float def) void dt_bauhaus_slider_set_soft_range(GtkWidget *widget, float soft_min, float soft_max) { - dt_bauhaus_slider_set_soft_min(widget,soft_min); - dt_bauhaus_slider_set_soft_max(widget,soft_max); + dt_bauhaus_slider_set_soft_min(widget, soft_min); + dt_bauhaus_slider_set_soft_max(widget, soft_max); } float dt_bauhaus_slider_get_default(GtkWidget *widget) @@ -972,8 +1070,6 @@ float dt_bauhaus_slider_get_default(GtkWidget *widget) return d->defpos; } -extern const dt_action_def_t dt_action_def_slider; -extern const dt_action_def_t dt_action_def_combo; void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *section, const char *label) { @@ -991,8 +1087,7 @@ void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *section, const c { if(!darktable.bauhaus->skip_accel || w->module->type != DT_ACTION_TYPE_IOP_INSTANCE) { - dt_action_t *ac = dt_action_define(w->module, section, label, widget, - w->type == DT_BAUHAUS_SLIDER ? &dt_action_def_slider : &dt_action_def_combo); + dt_action_t *ac = dt_action_define(w->module, section, label, widget, NULL); if(w->module->type != DT_ACTION_TYPE_IOP_INSTANCE) w->module = ac; } @@ -1048,8 +1143,15 @@ void dt_bauhaus_widget_set_field(GtkWidget *widget, gpointer field, dt_introspec w->field_type = field_type; } +// FIXME: IOP modules are parents, they shouldn't be handled from down here +// Wrong scope, bad API. void dt_bauhaus_update_module(dt_iop_module_t *self) { + +#if DEBUG + fprintf(stdout, "bauhaus update called for %s. %i widgets.\n", self->name(), g_slist_length(self->widget_list_bh)); +#endif + for(GSList *w = self->widget_list_bh; w; w = w->next) { dt_action_target_t *at = w->data; @@ -1154,18 +1256,6 @@ void dt_bauhaus_widget_release_quad(GtkWidget *widget) } } -static float _default_linear_curve(float value, dt_bauhaus_curve_t dir) -{ - // regardless of dir: input <-> output - return value; -} - -static float _reverse_linear_curve(float value, dt_bauhaus_curve_t dir) -{ - // regardless of dir: input <-> output - return 1.0 - value; -} - GtkWidget *dt_bauhaus_slider_new(dt_iop_module_t *self) { return dt_bauhaus_slider_new_with_range(self, 0.0, 1.0, 0.1, 0.5, 3); @@ -1199,20 +1289,9 @@ static void _style_updated(GtkWidget *widget) // it NEEDS to be defined and will be contextually adapted, possibly overriden by CSS. // Thing is Gtk CSS min-width in combination with hexpand is wonky so this is how it should be done. if(w->type == DT_BAUHAUS_COMBOBOX) - { - gtk_widget_set_size_request(widget, DT_PIXEL_APPLY_DPI(50), - w->margin->top + w->padding->top + w->margin->bottom + w->padding->bottom - + darktable.bauhaus->line_height); - } + gtk_widget_set_size_request(widget, DT_PIXEL_APPLY_DPI(50), _get_combobox_height(widget)); else if(w->type == DT_BAUHAUS_SLIDER) - { - // the lower thing to draw is indicator. See dt_bauhaus_draw_baseline for compute details - gtk_widget_set_size_request(widget, DT_PIXEL_APPLY_DPI(180), - w->margin->top + w->padding->top + w->margin->bottom + w->padding->bottom - + INNER_PADDING + - + darktable.bauhaus->line_height - + darktable.bauhaus->marker_size); - } + gtk_widget_set_size_request(widget, DT_PIXEL_APPLY_DPI(180), _get_slider_height(widget)); } GtkWidget *dt_bauhaus_slider_from_widget(dt_bauhaus_widget_t* w,dt_iop_module_t *self, float min, float max, @@ -1240,15 +1319,14 @@ GtkWidget *dt_bauhaus_slider_from_widget(dt_bauhaus_widget_t* w,dt_iop_module_t d->fill_feedback = feedback; d->is_dragging = 0; - d->is_changed = 0; d->timeout_handle = 0; - d->curve = _default_linear_curve; - gtk_widget_set_name(GTK_WIDGET(w), "bauhaus-slider"); + dt_gui_add_class(GTK_WIDGET(w), "bauhaus_slider"); g_signal_connect(G_OBJECT(w), "button-press-event", G_CALLBACK(dt_bauhaus_slider_button_press), NULL); g_signal_connect(G_OBJECT(w), "button-release-event", G_CALLBACK(dt_bauhaus_slider_button_release), NULL); g_signal_connect(G_OBJECT(w), "motion-notify-event", G_CALLBACK(dt_bauhaus_slider_motion_notify), NULL); + return GTK_WIDGET(w); } @@ -1288,14 +1366,13 @@ void dt_bauhaus_combobox_from_widget(dt_bauhaus_widget_t* w,dt_iop_module_t *sel d->editable = 0; d->text_align = DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT; d->entries_ellipsis = PANGO_ELLIPSIZE_END; - d->mute_scrolling = FALSE; d->populate = NULL; d->text = NULL; + d->timeout_handle = 0; - gtk_widget_set_name(GTK_WIDGET(w), "bauhaus-combobox"); + dt_gui_add_class(GTK_WIDGET(w), "bauhaus_combobox"); g_signal_connect(G_OBJECT(w), "button-press-event", G_CALLBACK(dt_bauhaus_combobox_button_press), NULL); - g_signal_connect(G_OBJECT(w), "motion-notify-event", G_CALLBACK(dt_bauhaus_combobox_motion_notify), NULL); } static dt_bauhaus_combobox_data_t *_combobox_data(GtkWidget *widget) @@ -1495,51 +1572,93 @@ void dt_bauhaus_combobox_set_text(GtkWidget *widget, const char *text) g_strlcpy(d->text, text, DT_BAUHAUS_COMBO_MAX_TEXT); } -static void _bauhaus_combobox_set(dt_bauhaus_widget_t *w, const int pos, const gboolean mute) +static gint _delayed_combobox_commit(gpointer data) { + // Commit combobox value change to pipeline history, handling a safety timout + // so incremental scrollings don't trigger a recompute at every scroll step. + + // API-wise, this is fucked up because IOPs are the GUI parents of Bauhaus widgets. + // Parents can call children modules (for methods and data structure), + // but children should be independent from parents, so the dependency tree is linear and vertical, + // and each child is a self-enclosed and self-reliant module. + + // The following introduces a circular dependency of children from their parent, that + // violates the modularity principle, and any change in parent scope will need to be propagated into the children scope, + // which is unexpected and unreliable given the non-existence of a dev doc. + + // A mitigation would be to define `dt_iop_gui_changed()` as a callback in the parent, + // then handle the following as a call to an opaque callback defined at implementation time. + // This would advertise the existence of the callback in the parent scope + // AND make the following immune to changes in parent. + + dt_bauhaus_widget_t *w = data; dt_bauhaus_combobox_data_t *d = &w->data.combobox; - d->active = CLAMP(pos, -1, (int)d->entries->len - 1); - gtk_widget_queue_draw(GTK_WIDGET(w)); + d->timeout_handle = 0; + g_signal_emit_by_name(G_OBJECT(w), "value-changed"); - if(!darktable.gui->reset && !mute) - { - if(w->field) + // TODO: make this a callback in module scope + if(w->field && w->module) { + switch(w->field_type) { - switch(w->field_type) - { - case DT_INTROSPECTION_TYPE_ENUM:; - if(d->active >= 0) - { - const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, d->active); - int *e = w->field, preve = *e; *e = GPOINTER_TO_INT(entry->data); - if(*e != preve) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &preve); - } - break; - case DT_INTROSPECTION_TYPE_INT:; - int *i = w->field, previ = *i; *i = d->active; - if(*i != previ) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &previ); - break; - case DT_INTROSPECTION_TYPE_UINT:; - unsigned int *u = w->field, prevu = *u; *u = d->active; - if(*u != prevu) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &prevu); - break; - case DT_INTROSPECTION_TYPE_BOOL:; - gboolean *b = w->field, prevb = *b; *b = d->active; - if(*b != prevb) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &prevb); - break; - default: - fprintf(stderr, "[_bauhaus_combobox_set] unsupported combo data type\n"); - } + case DT_INTROSPECTION_TYPE_ENUM:; + if(d->active >= 0) + { + const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, d->active); + int *e = w->field, preve = *e; *e = GPOINTER_TO_INT(entry->data); + if(*e != preve) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &preve); + } + break; + case DT_INTROSPECTION_TYPE_INT:; + int *i = w->field, previ = *i; *i = d->active; + if(*i != previ) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &previ); + break; + case DT_INTROSPECTION_TYPE_UINT:; + unsigned int *u = w->field, prevu = *u; *u = d->active; + if(*u != prevu) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &prevu); + break; + case DT_INTROSPECTION_TYPE_BOOL:; + gboolean *b = w->field, prevb = *b; *b = d->active; + if(*b != prevb) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &prevb); + break; + default: + fprintf(stderr, "[_bauhaus_combobox_set] unsupported combo data type\n"); } - g_signal_emit_by_name(G_OBJECT(w), "value-changed"); } + + return G_SOURCE_REMOVE; } void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos) { dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); - if(w->type != DT_BAUHAUS_COMBOBOX) return; - _bauhaus_combobox_set(w, pos, FALSE); + dt_bauhaus_combobox_data_t *d = &w->data.combobox; + const int old_pos = d->active; + const int new_pos = CLAMP(pos, -1, (int)d->entries->len - 1); + + if(old_pos != new_pos) + { + d->active = new_pos; + gtk_widget_queue_draw(GTK_WIDGET(w)); + +#if DEBUG + fprintf(stdout, "%s | %s | %s | item : %i \n", + (w->module && w->module->owner) ? "has owner" : "orphan", + (w->module) ? w->module->label : "anon", + w->label, + d->active); +#endif + + if(darktable.bauhaus->current == w) + gtk_widget_queue_draw(darktable.bauhaus->popup_area); + + if(!darktable.gui->reset && !d->timeout_handle) + { + const int delay = CLAMP(darktable.develop->average_delay * 3 / 2, + DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MIN, + DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MAX); + d->timeout_handle = g_timeout_add(delay, _delayed_combobox_commit, w); + } + } } gboolean dt_bauhaus_combobox_set_from_text(GtkWidget *widget, const char *text) @@ -1645,17 +1764,16 @@ static void draw_equilateral_triangle(cairo_t *cr, float radius) static void dt_bauhaus_draw_indicator(dt_bauhaus_widget_t *w, float pos, cairo_t *cr, float wd, const GdkRGBA fg_color, const GdkRGBA border_color) { // draw scale indicator (the tiny triangle) - if(w->type != DT_BAUHAUS_SLIDER) return; - const float size = darktable.bauhaus->marker_size; + const float vertical_position = _get_indicator_y_position(); + const float horizontal_position = pos * wd; cairo_save(cr); + // Clip horizontally to the limit of the baseline background - cairo_rectangle(cr, 0, 0, slider_position_from_right(wd, w) * wd, 200); + cairo_rectangle(cr, 0, 0, wd, 200); cairo_clip(cr); - cairo_translate(cr, slider_coordinate(pos, wd, w), - darktable.bauhaus->line_height + INNER_PADDING + darktable.bauhaus->baseline_size / 2.0f); - cairo_scale(cr, 1.0f, -1.0f); + cairo_translate(cr, horizontal_position, vertical_position); const dt_bauhaus_slider_data_t *d = &w->data.slider; @@ -1677,7 +1795,7 @@ static void dt_bauhaus_draw_indicator(dt_bauhaus_widget_t *w, float pos, cairo_t cairo_restore(cr); } -static void dt_bauhaus_draw_quad(dt_bauhaus_widget_t *w, cairo_t *cr, const int width, const int height) +static void dt_bauhaus_draw_quad(dt_bauhaus_widget_t *w, cairo_t *cr, const double x, const double y) { if(!w->show_quad) return; @@ -1685,62 +1803,48 @@ static void dt_bauhaus_draw_quad(dt_bauhaus_widget_t *w, cairo_t *cr, const int if(w->quad_paint) { // draw color picker - w->quad_paint(cr, width - darktable.bauhaus->quad_width, // x - 0.0, // y + w->quad_paint(cr, x, y, darktable.bauhaus->quad_width, // width darktable.bauhaus->quad_width, // height w->quad_paint_flags, w->quad_paint_data); } - else + else if(w->type == DT_BAUHAUS_COMBOBOX) { - // draw active area square: - switch(w->type) - { - case DT_BAUHAUS_COMBOBOX: - cairo_translate(cr, width - darktable.bauhaus->quad_width * .5f, height * .5f); - const float r = darktable.bauhaus->quad_width * .2f; - cairo_move_to(cr, -r, -r * .5f); - cairo_line_to(cr, 0, r * .5f); - cairo_line_to(cr, r, -r * .5f); - cairo_stroke(cr); - break; - case DT_BAUHAUS_SLIDER: - break; - default: - cairo_set_source_rgb(cr, 1.0, 0.0, 0.0); - cairo_rectangle(cr, width - darktable.bauhaus->quad_width, 0.0, darktable.bauhaus->quad_width, darktable.bauhaus->quad_width); - cairo_fill(cr); - break; - } + // draw combobox chevron + cairo_translate(cr, x + darktable.bauhaus->quad_width / 2., y + darktable.bauhaus->quad_width / 2.); + const float r = darktable.bauhaus->quad_width * .2f; + cairo_move_to(cr, -r, -r * .5f); + cairo_line_to(cr, 0, r * .5f); + cairo_line_to(cr, r, -r * .5f); + cairo_stroke(cr); } cairo_restore(cr); } +/** + * @brief Draw the slider baseline, aka the backgronud bar. + * + * @param w Widget + * @param cr Cairo object + * @param width The width of the actual slider baseline (corrected for padding, margin and quad width if needed) + */ static void dt_bauhaus_draw_baseline(dt_bauhaus_widget_t *w, cairo_t *cr, float width) { // draw line for orientation in slider - if(w->type != DT_BAUHAUS_SLIDER) return; - - const float slider_width = width - _widget_get_quad_width(w); cairo_save(cr); dt_bauhaus_slider_data_t *d = &w->data.slider; - - // pos of baseline - const float htm = darktable.bauhaus->line_height + INNER_PADDING; - - // thickness of baseline - const float htM = darktable.bauhaus->baseline_size; + const float baseline_top = darktable.bauhaus->line_height + INNER_PADDING; + const float baseline_height = darktable.bauhaus->baseline_size; // the background of the line + cairo_rectangle(cr, 0, baseline_top, width, baseline_height); cairo_pattern_t *gradient = NULL; - cairo_rectangle(cr, 0, htm, slider_width, htM); - if(d->grad_cnt > 0) { - // gradient line as used in some modules + // gradient line as used in some modules for hue, saturation, lightness const double zoom = (d->max - d->min) / (d->hard_max - d->hard_min); const double offset = (d->min - d->hard_min) / (d->hard_max - d->hard_min); - gradient = cairo_pattern_create_linear(0, 0, slider_width, htM); + gradient = cairo_pattern_create_linear(0, 0, width, baseline_height); for(int k = 0; k < d->grad_cnt; k++) cairo_pattern_add_color_stop_rgba(gradient, (d->grad_pos[k] - offset) / zoom, d->grad_col[k][0], d->grad_col[k][1], d->grad_col[k][2], 0.4f); @@ -1751,44 +1855,41 @@ static void dt_bauhaus_draw_baseline(dt_bauhaus_widget_t *w, cairo_t *cr, float // regular baseline set_color(cr, darktable.bauhaus->color_bg); } - cairo_fill(cr); + if(gradient) cairo_pattern_destroy(gradient); // get the reference of the slider aka the position of the 0 value const float origin = fmaxf(fminf((d->factor > 0 ? -d->min - d->offset/d->factor : d->max + d->offset/d->factor) - / (d->max - d->min), 1.0f) * slider_width, 0.0f); - const float position = d->pos * slider_width; + / (d->max - d->min), 1.0f) * width, 0.0f); + const float position = d->pos * width; const float delta = position - origin; // have a `fill ratio feel' from zero to current position - // - but only if set if(d->fill_feedback) { // only brighten, useful for colored sliders to not get too faint: + cairo_save(cr); cairo_set_operator(cr, CAIRO_OPERATOR_SCREEN); set_color(cr, darktable.bauhaus->color_fill); - cairo_rectangle(cr, origin, htm, delta, htM); + cairo_rectangle(cr, origin, baseline_top, delta, baseline_height); cairo_fill(cr); - - // change back to default cairo operator: - cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + cairo_restore(cr); } // draw the 0 reference graduation if it's different than the bounds of the slider - const float graduation_top = htm + darktable.bauhaus->marker_size + darktable.bauhaus->border_width; - const float graduation_height = darktable.bauhaus->border_width / 2.0f; + const float graduation_top = baseline_top + darktable.bauhaus->marker_size + darktable.bauhaus->border_width; set_color(cr, darktable.bauhaus->color_fg); // If the max of the slider is 360, it is likely an absolute hue slider in degrees - // a zero in periodic stuff has not much meaning so we skip it + // a zero in periodic stuff has not much meaning so we skip it. if(d->hard_max != 360.0f) - cairo_arc(cr, origin, graduation_top, graduation_height, 0, 2 * M_PI); + { + cairo_arc(cr, origin, graduation_top, darktable.bauhaus->border_width / 2., 0, 2 * M_PI); + cairo_fill(cr); + } - cairo_fill(cr); cairo_restore(cr); - - if(d->grad_cnt > 0) cairo_pattern_destroy(gradient); } static void dt_bauhaus_widget_reject(dt_bauhaus_widget_t *w) @@ -1800,7 +1901,7 @@ static void dt_bauhaus_widget_reject(dt_bauhaus_widget_t *w) case DT_BAUHAUS_SLIDER: { dt_bauhaus_slider_data_t *d = &w->data.slider; - dt_bauhaus_slider_set_normalized(w, d->oldpos); + dt_bauhaus_slider_set_normalized(w, d->oldpos, FALSE); } break; default: @@ -1812,74 +1913,69 @@ static void dt_bauhaus_widget_accept(dt_bauhaus_widget_t *w) { GtkWidget *widget = GTK_WIDGET(w); - GtkAllocation allocation_popup_window; - gtk_widget_get_allocation(darktable.bauhaus->popup_window, &allocation_popup_window); - const GtkBorder *padding = darktable.bauhaus->popup_padding; - - const int width = allocation_popup_window.width - padding->left - padding->right; - const int height = allocation_popup_window.height - padding->top - padding->bottom; - const int base_height = darktable.bauhaus->line_height + INNER_PADDING * 2.0f; - switch(w->type) { case DT_BAUHAUS_COMBOBOX: { - // only set to what's in the filtered list. dt_bauhaus_combobox_data_t *d = &w->data.combobox; - const int top_gap = (w->detached_popup) ? w->top_gap + darktable.bauhaus->line_height : w->top_gap; - const int active = darktable.bauhaus->end_mouse_y >= 0 - ? ((darktable.bauhaus->end_mouse_y - top_gap) / darktable.bauhaus->line_height) - : d->active; - int k = 0, i = 0, kk = 0, match = 1; - gchar *keys = g_utf8_casefold(darktable.bauhaus->keys, -1); - for(int j = 0; j < d->entries->len; j++) + if(d->editable && darktable.bauhaus->keys > 0) { - const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, j); - gchar *text_cmp = g_utf8_casefold(entry->label, -1); - if(!strncmp(text_cmp, keys, darktable.bauhaus->keys_cnt)) + // combobox is editable and we have text, assume it is a custom input + memset(d->text, 0, DT_BAUHAUS_COMBO_MAX_TEXT); + g_strlcpy(d->text, darktable.bauhaus->keys, DT_BAUHAUS_COMBO_MAX_TEXT); + dt_bauhaus_combobox_set(widget, -1); // select custom entry + } + else + { + if(darktable.bauhaus->keys_cnt > 0) { - if(active == k) + // combobox is not editable, but we have text. Assume user wanted to init a selection from keyboard. + // find the closest match by looking for the entry having the maximum number + // of characters in common with the user input. + gchar *keys = g_utf8_casefold(darktable.bauhaus->keys, -1); + int match = -1; + int matches = 0; + + for(int j = 0; j < d->entries->len; j++) { - if(entry->sensitive) - dt_bauhaus_combobox_set(widget, i); - g_free(keys); + const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, j); + gchar *text_cmp = g_utf8_casefold(entry->label, -1); + if(entry->sensitive && !strncmp(text_cmp, keys, darktable.bauhaus->keys_cnt)) + { + matches++; + match = j; + } g_free(text_cmp); - return; } - kk = i; // remember for down there - // editable should only snap to perfect matches, not prefixes: - if(d->editable && strcmp(entry->label, darktable.bauhaus->keys)) match = 0; - k++; + g_free(keys); + + // Accept result only if exactly one match was found. Anything else is ambiguous + if(matches == 1) + dt_bauhaus_combobox_set(widget, match); + } + else { + // Active entry (below cursor or scrolled) + dt_bauhaus_combobox_set(widget, d->hovered); } - i++; - g_free(text_cmp); - } - // didn't find it, but had only one matching choice? - if(k == 1 && match) - dt_bauhaus_combobox_set(widget, kk); - else if(d->editable) - { - // otherwise, if combobox is editable, assume it is a custom input - memset(d->text, 0, DT_BAUHAUS_COMBO_MAX_TEXT); - g_strlcpy(d->text, darktable.bauhaus->keys, DT_BAUHAUS_COMBO_MAX_TEXT); - // select custom entry - dt_bauhaus_combobox_set(widget, -1); } - g_free(keys); + break; } case DT_BAUHAUS_SLIDER: { - if(darktable.bauhaus->end_mouse_y < darktable.bauhaus->line_height) break; - + // The slider popup uses the quadratic magnifier for accurate setting. + // We need extra conversions from cursor coordinates to set it right. + // This needs to be kept in sync with popup_draw() dt_bauhaus_slider_data_t *d = &w->data.slider; - const float mouse_off - = get_slider_line_offset(d->oldpos, 5.0 * powf(10.0f, -d->digits) / (d->max - d->min) / d->factor, - darktable.bauhaus->end_mouse_x / width, darktable.bauhaus->end_mouse_y / height, - base_height / (float)height, allocation_popup_window.width, w); - dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off); - d->oldpos = d->pos; + + // This is needed to accept the change. + // d->pos is soft-updated with corrected coordinates for drawing purposes only in popup_redraw(). + // We need to reset it to the original value temporarily, and request a proper setting + // with value-changed signal through dt_bauhaus_slider_set_normalized() + const float value = d->pos; + d->pos = d->oldpos; + dt_bauhaus_slider_set_normalized(w, value, TRUE); break; } default: @@ -1897,22 +1993,16 @@ static gchar *_build_label(const dt_bauhaus_widget_t *w) static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data) { + // Popups belong to the app, not to the bauhaus widget. That's confusing. + // Also the *widget param here is the popup container, still not the bauhaus widget. Even more confusing. + // This is the actual parent bauhaus widget : dt_bauhaus_widget_t *w = darktable.bauhaus->current; - // dimensions of the popup + // get area properties GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); - const GtkBorder *padding = darktable.bauhaus->popup_padding; - const int w2 = allocation.width - padding->left - padding->right; - const int h2 = allocation.height - padding->top - padding->bottom; - - // dimensions of the original line - int ht = darktable.bauhaus->line_height + INNER_PADDING * 2.0f; - - // get area properties cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height); cairo_t *cr = cairo_create(cst); - GtkStyleContext *context = gtk_widget_get_style_context(widget); // look up some colors once @@ -1934,24 +2024,25 @@ static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height); gtk_render_frame(context, cr, 0, 0, allocation.width, allocation.height); + const double main_height = _widget_get_main_height(w, widget); + double total_width = 0.f; + const double main_width = _widget_get_main_width(w, NULL, &total_width); + // translate to account for the widget spacing - cairo_translate(cr, padding->left, padding->top); + cairo_translate(cr, w->padding->left, w->padding->top); // switch on bauhaus widget type (so we only need one static window) switch(w->type) { case DT_BAUHAUS_SLIDER: { - const dt_bauhaus_slider_data_t *d = &w->data.slider; - - dt_bauhaus_draw_baseline(w, cr, w2); - + dt_bauhaus_slider_data_t *d = &w->data.slider; cairo_save(cr); - cairo_set_line_width(cr, 0.5); set_color(cr, *fg_color); - float scale = 5.0 * powf(10.0f, -d->digits)/(d->max - d->min) / d->factor; + float scale = _bh_slider_get_scale(w); const int num_scales = 1.f / scale; + const float bottom_baseline = _get_slider_bar_height(); for(int k = 0; k < num_scales; k++) { @@ -1959,31 +2050,37 @@ static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer GdkRGBA fg_copy = *fg_color; fg_copy.alpha = scale / fabsf(off); set_color(cr, fg_copy); - draw_slider_line(cr, d->oldpos, off, scale, w2, h2, ht, w); + draw_slider_line(cr, d->oldpos, off, scale, main_width, main_height, bottom_baseline, 1); cairo_stroke(cr); } cairo_restore(cr); - // draw mouse over indicator line - cairo_save(cr); - cairo_set_line_width(cr, 2.); + // Get the x offset compared to d->oldpos accounting for vertical position magnification const float mouse_off - = darktable.bauhaus->change_active - ? get_slider_line_offset(d->oldpos, scale, darktable.bauhaus->mouse_x / w2, - darktable.bauhaus->mouse_y / h2, ht / (float)h2, allocation.width, w) - : 0.0f; - draw_slider_line(cr, d->oldpos, mouse_off, scale, w2, h2, ht, w); + = get_slider_line_offset(d->oldpos, scale, + darktable.bauhaus->mouse_x / main_width, + darktable.bauhaus->mouse_y / main_height, + bottom_baseline / main_height); + + d->pos = d->oldpos + mouse_off; + + // Draw the baseline with fill feedback if any (needs the new d->pos set before) + dt_bauhaus_draw_baseline(w, cr, main_width); + + cairo_save(cr); + + // draw mouse over indicator line + set_color(cr, *fg_color); + draw_slider_line(cr, d->oldpos, mouse_off, scale, main_width, main_height, bottom_baseline, 2); cairo_stroke(cr); - cairo_restore(cr); // draw indicator - dt_bauhaus_draw_indicator(w, d->oldpos + mouse_off, cr, w2, *fg_color, *bg_color); + dt_bauhaus_draw_indicator(w, d->pos, cr, main_width, *fg_color, *bg_color); + + cairo_restore(cr); // draw numerical value: cairo_save(cr); - - char *text = dt_bauhaus_slider_get_text(GTK_WIDGET(w), dt_bauhaus_slider_get(GTK_WIDGET(w))); - if(state & GTK_STATE_FLAG_PRELIGHT) set_color(cr, text_color_hover); else if(state & GTK_STATE_FLAG_FOCUSED) @@ -1993,105 +2090,88 @@ static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer else set_color(cr, *fg_color); - const float value_width = show_pango_text(w, context, cr, text, w2 - _widget_get_quad_width(w), 0, 0, TRUE, - FALSE, PANGO_ELLIPSIZE_END, FALSE, FALSE, NULL, NULL, FALSE); + float value_width = 0.f; + char *text = dt_bauhaus_slider_get_text(GTK_WIDGET(w), dt_bauhaus_slider_get(GTK_WIDGET(w))); + GdkRectangle bounding_value = { .x = 0., + .y = 0., + .width = main_width, + .height = darktable.bauhaus->line_height }; + // Display user keyboard input if any, otherwise the current value + show_pango_text(w, context, cr, &bounding_value, + (darktable.bauhaus->keys_cnt) ? darktable.bauhaus->keys : text, BH_ALIGN_RIGHT, + BH_ALIGN_MIDDLE, PANGO_ELLIPSIZE_NONE, NULL, &value_width, NULL, FALSE); g_free(text); - set_color(cr, text_color_insensitive); - char *min = dt_bauhaus_slider_get_text(GTK_WIDGET(w), d->factor > 0 ? d->min : d->max); - show_pango_text(w, context, cr, min, 0, ht + INNER_PADDING, 0, FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE, - FALSE, NULL, NULL, TRUE); - g_free(min); - char *max = dt_bauhaus_slider_get_text(GTK_WIDGET(w), d->factor > 0 ? d->max : d->min); - show_pango_text(w, context, cr, max, w2 - _widget_get_quad_width(w), ht + INNER_PADDING, 0, TRUE, FALSE, - PANGO_ELLIPSIZE_END, FALSE, FALSE, NULL, NULL, TRUE); - g_free(max); - - const float label_width = w2 - _widget_get_quad_width(w) - INNER_PADDING - value_width; - if(label_width > 0) - { - gchar *lb = _build_label(w); - show_pango_text(w, context, cr, lb, 0, 0, label_width, FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE, FALSE, - NULL, NULL, FALSE); - g_free(lb); - } + + // label on top of marker: + gchar *label_text = _build_label(w); + const float label_width = main_width - value_width - INNER_PADDING; + GdkRectangle bounding_label = { .x = 0., + .y = 0., + .width = label_width, + .height = darktable.bauhaus->line_height }; + show_pango_text(w, context, cr, &bounding_label, label_text, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE, + PANGO_ELLIPSIZE_END, NULL, NULL, NULL, FALSE); + g_free(label_text); + cairo_restore(cr); } break; case DT_BAUHAUS_COMBOBOX: { + float cumulative_height = 0.f; const dt_bauhaus_combobox_data_t *d = &w->data.combobox; + + // User keyboard input goes first + if(darktable.bauhaus->keys_cnt > 0) + { + cairo_save(cr); + set_color(cr, text_color_focused); + GdkRectangle query_label = { .x = 0., + .y = cumulative_height, + .width = main_width, + .height = _bh_get_row_height() }; + show_pango_text(w, context, cr, &query_label, darktable.bauhaus->keys, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE, + PANGO_ELLIPSIZE_NONE, NULL, NULL, NULL, FALSE); + cumulative_height += query_label.height; + cairo_restore(cr); + } + cairo_save(cr); - float first_label_width = 0.0; - gboolean first_label = *w->label; - gboolean show_box_label = TRUE; - int k = 0, i = 0; - ht = darktable.bauhaus->line_height + INNER_PADDING; - // case where the popup is detcahed (label on its specific line) - const int top_gap = (w->detached_popup) ? w->top_gap + ht : w->top_gap; - if(w->detached_popup || !w->show_label) first_label = FALSE; - if(!w->detached_popup && !w->show_label) show_box_label = FALSE; - const int hovered = (darktable.bauhaus->mouse_y - top_gap) / ht; gchar *keys = g_utf8_casefold(darktable.bauhaus->keys, -1); - const PangoEllipsizeMode ellipsis = d->entries_ellipsis; - for(int j = 0; j < d->entries->len; j++) { const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, j); gchar *text_cmp = g_utf8_casefold(entry->label, -1); if(!strncmp(text_cmp, keys, darktable.bauhaus->keys_cnt)) { - float max_width = w2 - _widget_get_quad_width(w); - float label_width = 0.0f; + // If user typed some keys, display matching entries only + gboolean ignore_font_style = FALSE; if(!entry->sensitive) set_color(cr, text_color_insensitive); - else if(i == hovered) + else if(j == d->hovered) set_color(cr, text_color_hover); - else if(i == d->active) + else if(j == d->active) set_color(cr, text_color_selected); - else - set_color(cr, text_color); - - if(entry->alignment == DT_BAUHAUS_COMBOBOX_ALIGN_LEFT) - { - gchar *esc_label = g_markup_escape_text(entry->label, -1); - gchar *label = g_strdup_printf("%s", esc_label); - label_width = show_pango_text(w, context, cr, label, 0, ht * k + top_gap, max_width, FALSE, FALSE, - ellipsis, TRUE, FALSE, NULL, NULL, TRUE); - g_free(label); - g_free(esc_label); - } else { - if(first_label) max_width *= 0.8; // give the label at least some room - label_width - = show_pango_text(w, context, cr, entry->label, w2 - _widget_get_quad_width(w), ht * k + top_gap, - max_width, TRUE, FALSE, ellipsis, FALSE, FALSE, NULL, NULL, TRUE); - } - - // prefer the entry over the label wrt. ellipsization when expanded - if(first_label) - { - show_box_label = entry->alignment == DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT; - first_label_width = label_width; - first_label = FALSE; + set_color(cr, text_color); + // GTK_STATE flag is applied to the whole widget. + // We need to selectively dispatch here to the right internal elements. + ignore_font_style = TRUE; } - k++; + GdkRectangle bounding_label = { .x = 0., + .y = cumulative_height, + .width = main_width, + .height = _bh_get_row_height() }; + show_pango_text(w, context, cr, &bounding_label, entry->label, BH_ALIGN_RIGHT, BH_ALIGN_TOP, + d->entries_ellipsis, bg_color, NULL, NULL, ignore_font_style); + cumulative_height += bounding_label.height; } - i++; + g_free(text_cmp); } cairo_restore(cr); - - // left aligned box label. add it to the gui after the entries so we can ellipsize it if needed - if(show_box_label) - { - set_color(cr, text_color); - gchar *lb = _build_label(w); - show_pango_text(w, context, cr, lb, 0, w->top_gap, w2 - _widget_get_quad_width(w) - first_label_width, - FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE, TRUE, NULL, NULL, FALSE); - g_free(lb); - } g_free(keys); } break; @@ -2100,45 +2180,6 @@ static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer break; } - // draw currently typed text. if a type doesn't want this, it should not - // allow stuff to be written here in the key callback. - const int line_height = darktable.bauhaus->line_height + INNER_PADDING; - const int size = MIN(3 * line_height, .2 * h2); - if(darktable.bauhaus->keys_cnt) - { - cairo_save(cr); - PangoLayout *layout = pango_cairo_create_layout(cr); - PangoRectangle ink; - pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi); - set_color(cr, text_color); - - // make extra large, but without dependency on popup window height - // (that might differ for comboboxes for example). only fall back - // to height dependency if the popup is really small. - PangoFontDescription *desc = pango_font_description_copy_static(darktable.bauhaus->pango_font_desc); - pango_font_description_set_absolute_size(desc, size * PANGO_SCALE); - pango_layout_set_font_description(layout, desc); - - pango_layout_set_text(layout, darktable.bauhaus->keys, -1); - pango_layout_get_pixel_extents(layout, &ink, NULL); - cairo_move_to(cr, w2 - _widget_get_quad_width(w) - ink.width, h2 * 0.5 - size); - pango_cairo_show_layout(cr, layout); - cairo_restore(cr); - pango_font_description_free(desc); - g_object_unref(layout); - } - if(darktable.bauhaus->cursor_visible) - { - // show the blinking cursor - cairo_save(cr); - set_color(cr, text_color); - cairo_move_to(cr, w2 - darktable.bauhaus->quad_width + 3, h2 * 0.5 + size / 3); - cairo_line_to(cr, w2 - darktable.bauhaus->quad_width + 3, h2 * 0.5 - size); - cairo_set_line_width(cr, 2.); - cairo_stroke(cr); - cairo_restore(cr); - } - cairo_destroy(cr); cairo_set_source_surface(crf, cst, 0, 0); cairo_paint(crf); @@ -2152,11 +2193,10 @@ static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf) { + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); - dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); - const double width = allocation.width, height = allocation.height; - cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height); cairo_t *cr = cairo_create(cst); GtkStyleContext *context = gtk_widget_get_style_context(widget); @@ -2167,12 +2207,12 @@ static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf) gtk_style_context_get(context, state, "background-color", &bg_color, NULL); _margins_retrieve(w); - // translate to account for the widget spacing - const double padded_h = height - w->margin->top - w->margin->bottom; - const double padded_w = width - w->margin->left - w->margin->right; - const double inner_h = padded_h - w->padding->top - w->padding->bottom; - const double inner_w = padded_w - w->padding->left - w->padding->right; - gtk_render_background(context, cr, w->margin->left, w->margin->top, padded_w, padded_h); + // Paint background first + gtk_render_background(context, cr, allocation.x, allocation.y, allocation.width, allocation.height); + + // Translate Cairo coordinates to account for the widget spacing + const float available_width = _widget_get_main_width(w, NULL, NULL); + const float inner_height = _widget_get_main_height(w, NULL); cairo_translate(cr, w->margin->left + w->padding->left, w->margin->top + w->padding->top); // draw type specific content: @@ -2184,7 +2224,8 @@ static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf) case DT_BAUHAUS_COMBOBOX: { // draw label and quad area at right end - if(w->show_quad) dt_bauhaus_draw_quad(w, cr, inner_w, inner_h); + if(w->show_quad) + dt_bauhaus_draw_quad(w, cr, available_width + 2. * INNER_PADDING, 0.); dt_bauhaus_combobox_data_t *d = &w->data.combobox; const PangoEllipsizeMode combo_ellipsis = d->entries_ellipsis; @@ -2195,87 +2236,68 @@ static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf) text = entry->label; } - const float available_width = inner_w - _widget_get_quad_width(w); - - //calculate total widths of label and combobox gchar *label_text = _build_label(w); - float label_width = 0; - float label_height = 0; - // we only show the label if the text is aligned on the right - if(label_text && w->show_label && d->text_align == DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT) - show_pango_text(w, context, cr, label_text, 0, 0, 0, FALSE, TRUE, PANGO_ELLIPSIZE_END, FALSE, TRUE, - &label_width, &label_height, FALSE); - float combo_width = 0; - float combo_height = 0; - show_pango_text(w, context, cr, text, available_width, 0, 0, TRUE, TRUE, combo_ellipsis, FALSE, FALSE, - &combo_width, &combo_height, FALSE); - // we want to center the text verticaly - w->top_gap = floor((inner_h - fmaxf(label_height, combo_height)) / 2.0f); - //check if they fit - if((label_width + combo_width) > available_width) - { - if(d->text_align == DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT) - { - // they don't fit: evenly divide the available width between the two in proportion - const float ratio = label_width / (label_width + combo_width); - if(w->show_label) - show_pango_text(w, context, cr, label_text, 0, w->top_gap, available_width * ratio - INNER_PADDING * 2, - FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE, TRUE, NULL, NULL, FALSE); - show_pango_text(w, context, cr, text, available_width, w->top_gap, available_width * (1.0f - ratio), - TRUE, FALSE, combo_ellipsis, FALSE, FALSE, NULL, NULL, FALSE); - } - else - show_pango_text(w, context, cr, text, 0, w->top_gap, available_width, FALSE, FALSE, combo_ellipsis, - FALSE, FALSE, NULL, NULL, FALSE); - } - else - { - if(d->text_align == DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT) - { - if(w->show_label) - show_pango_text(w, context, cr, label_text, 0, w->top_gap, 0, FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE, - TRUE, NULL, NULL, FALSE); - show_pango_text(w, context, cr, text, available_width, w->top_gap, 0, TRUE, FALSE, combo_ellipsis, FALSE, - FALSE, NULL, NULL, FALSE); - } - else - show_pango_text(w, context, cr, text, 0, w->top_gap, 0, FALSE, FALSE, combo_ellipsis, FALSE, FALSE, NULL, - NULL, FALSE); - } + float label_width = 0.f; + float label_height = 0.f; + + GdkRectangle bounding_label = { .x = 0., + .y = 0., + .width = available_width, + .height = inner_height }; + if(label_text && w->show_label) + show_pango_text(w, context, cr, &bounding_label, label_text, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE, + combo_ellipsis, NULL, &label_width, &label_height, FALSE); + + // The value is shown right-aligned, ellipsized if needed. + GdkRectangle bounding_value = { .x = label_width + INNER_PADDING, + .y = 0., + .width = available_width - label_width - INNER_PADDING, + .height = inner_height }; + show_pango_text(w, context, cr, &bounding_value, text, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE, combo_ellipsis, NULL, + NULL, NULL, FALSE); + g_free(label_text); break; } case DT_BAUHAUS_SLIDER: { // line for orientation - dt_bauhaus_draw_baseline(w, cr, inner_w); + dt_bauhaus_draw_baseline(w, cr, available_width); // Paint the non-active quad icon with some transparency, because // icons are bolder than the neighbouring text and appear brighter. cairo_save(cr); if(!(w->quad_paint_flags & CPF_ACTIVE)) cairo_set_source_rgba(cr, text_color->red, text_color->green, text_color->blue, text_color->alpha * 0.7); - if(w->show_quad) dt_bauhaus_draw_quad(w, cr, inner_w, inner_h); + dt_bauhaus_draw_quad(w, cr, available_width + 2. * INNER_PADDING, 0.); cairo_restore(cr); float value_width = 0; if(gtk_widget_is_sensitive(widget)) { cairo_save(cr); - dt_bauhaus_draw_indicator(w, w->data.slider.pos, cr, inner_w, *text_color, *bg_color); + dt_bauhaus_draw_indicator(w, w->data.slider.pos, cr, available_width, *text_color, *bg_color); cairo_restore(cr); char *text = dt_bauhaus_slider_get_text(widget, dt_bauhaus_slider_get(widget)); - value_width = show_pango_text(w, context, cr, text, inner_w - _widget_get_quad_width(w), 0, 0, TRUE, FALSE, - PANGO_ELLIPSIZE_END, FALSE, FALSE, NULL, NULL, FALSE); + GdkRectangle bounding_value = { .x = 0., + .y = 0., + .width = available_width, + .height = darktable.bauhaus->line_height }; + show_pango_text(w, context, cr, &bounding_value, text, BH_ALIGN_RIGHT, BH_ALIGN_MIDDLE, + PANGO_ELLIPSIZE_NONE, NULL, &value_width, NULL, FALSE); g_free(text); } + // label on top of marker: gchar *label_text = _build_label(w); - const float label_width = inner_w - _widget_get_quad_width(w) - value_width; - if(label_width > 0) - show_pango_text(w, context, cr, label_text, 0, 0, label_width, FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE, - TRUE, NULL, NULL, FALSE); + const float label_width = available_width - value_width - INNER_PADDING; + GdkRectangle bounding_label = { .x = 0., + .y = 0., + .width = label_width, + .height = darktable.bauhaus->line_height }; + show_pango_text(w, context, cr, &bounding_label, label_text, BH_ALIGN_LEFT, BH_ALIGN_MIDDLE, + PANGO_ELLIPSIZE_END, NULL, NULL, NULL, FALSE); g_free(label_text); } break; @@ -2288,60 +2310,22 @@ static gboolean _widget_draw(GtkWidget *widget, cairo_t *crf) cairo_paint(crf); cairo_surface_destroy(cst); - // render eventual css borders - gtk_render_frame(context, crf, w->margin->left, w->margin->top, padded_w, padded_h); gdk_rgba_free(text_color); gdk_rgba_free(bg_color); return TRUE; } -static gint _bauhaus_natural_width(GtkWidget *widget, gboolean popup) -{ - gint natural_size = 0; - - dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); - if(w->type == DT_BAUHAUS_COMBOBOX) - { - dt_bauhaus_combobox_data_t *d = &w->data.combobox; - - PangoLayout *layout = gtk_widget_create_pango_layout(widget, NULL); - pango_layout_set_font_description(layout, darktable.bauhaus->pango_font_desc); - gint label_width = 0, entry_width = 0; - - if(d->text_align == DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT && w->show_label) - { - pango_layout_set_text(layout, w->label, -1); - pango_layout_get_size(layout, &label_width, NULL); - label_width /= PANGO_SCALE; - if(label_width) label_width += 2 * INNER_PADDING; - } - - for(int i = 0; i < d->entries->len; i++) - { - const dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(d->entries, i); - - if(popup && (i || entry->alignment != DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT)) - label_width = 0; - - pango_layout_set_text(layout, entry->label, -1); - pango_layout_get_size(layout, &entry_width, NULL); - - natural_size = MAX(natural_size, label_width + entry_width / PANGO_SCALE); - } - - _margins_retrieve(w); - natural_size += _widget_get_quad_width(w) + w->margin->left + w->margin->right - + w->padding->left + w->padding->right; - g_object_unref(layout); - } - - return natural_size; -} - static void _get_preferred_width(GtkWidget *widget, gint *minimum_size, gint *natural_size) { - *natural_size = _bauhaus_natural_width(widget, FALSE); + // Nothing clever here : preferred size is the size of the container. + // If user is not happy with that, it's his responsibility to resize sidebars. + if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_RIGHT, widget)) + *natural_size = dt_ui_panel_get_size(darktable.gui->ui, DT_UI_PANEL_RIGHT); + else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_LEFT, widget)) + *natural_size = dt_ui_panel_get_size(darktable.gui->ui, DT_UI_PANEL_LEFT); + else + *natural_size = DT_PIXEL_APPLY_DPI(300); } void dt_bauhaus_hide_popup() @@ -2352,9 +2336,7 @@ void dt_bauhaus_hide_popup() gtk_widget_hide(darktable.bauhaus->popup_window); gtk_window_set_attached_to(GTK_WINDOW(darktable.bauhaus->popup_window), NULL); darktable.bauhaus->current = NULL; - // TODO: give focus to center view? do in accept() as well? } - _stop_cursor(); } void dt_bauhaus_show_popup(GtkWidget *widget) @@ -2367,8 +2349,8 @@ void dt_bauhaus_show_popup(GtkWidget *widget) darktable.bauhaus->change_active = 0; darktable.bauhaus->mouse_line_distance = 0.0f; darktable.bauhaus->hiding = FALSE; - _stop_cursor(); + // Should not be needed if we come from a click bauhaus_request_focus(w); gtk_widget_realize(darktable.bauhaus->popup_window); @@ -2376,124 +2358,48 @@ void dt_bauhaus_show_popup(GtkWidget *widget) GdkWindow *widget_window = gtk_widget_get_window(widget); gint wx = 0, wy = 0; - if(widget_window) - gdk_window_get_origin(widget_window, &wx, &wy); + if(widget_window) gdk_window_get_origin(widget_window, &wx, &wy); + + // From here, all positions are float so we need to handle them as such + // to avoid rounding errors resulting is pixel-shifting glitches. + float x = (float)wx; + float y = (float)wy; GtkAllocation tmp; gtk_widget_get_allocation(widget, &tmp); - gint natural_w = _bauhaus_natural_width(widget, TRUE); - if(tmp.width < natural_w) - tmp.width = natural_w; - else if(tmp.width == 1 || !w->margin) - { - if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_RIGHT, widget)) - tmp.width = dt_ui_panel_get_size(darktable.gui->ui, DT_UI_PANEL_RIGHT); - else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_LEFT, widget)) - tmp.width = dt_ui_panel_get_size(darktable.gui->ui, DT_UI_PANEL_LEFT); - else - tmp.width = 300; - tmp.width -= INNER_PADDING * 2; - } - else - { - // by default, we want the popup to be exactly the size of the widget content - tmp.width = MAX(1, tmp.width - (w->margin->left + w->margin->right + w->padding->left + w->padding->right)); - } - - gint px, py; - GdkDevice *pointer = gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_display_get_default())); - gdk_device_get_position(pointer, NULL, &px, &py); - - w->detached_popup = FALSE; - if(px < wx || px > wx + tmp.width) - { - w->detached_popup = TRUE; - wx = px - (tmp.width - _widget_get_quad_width(w)) / 2; - wy = py - (darktable.bauhaus->line_height + INNER_PADDING) / 2; - } - else if(py < wy || py > wy + tmp.height) - { - wy = py - (darktable.bauhaus->line_height + INNER_PADDING) / 2; - } switch(darktable.bauhaus->current->type) { case DT_BAUHAUS_SLIDER: { - dt_bauhaus_slider_data_t *d = &w->data.slider; - d->oldpos = d->pos; + // Slider popup: make it square tmp.height = tmp.width; - _start_cursor(6); break; } case DT_BAUHAUS_COMBOBOX: { - // we launch the dynamic populate fct if any - dt_iop_module_t *module = (dt_iop_module_t *)(w->module); - const dt_bauhaus_combobox_data_t *d = &w->data.combobox; - if(d->populate) d->populate(widget, &module); - // comboboxes change immediately - darktable.bauhaus->change_active = 1; - if(!d->entries->len) return; - tmp.height = (darktable.bauhaus->line_height + INNER_PADDING) * d->entries->len; - // if the popup is detached, we show the lable in any cases, in a special line - if(w->detached_popup) tmp.height += darktable.bauhaus->line_height + INNER_PADDING; - if(w->margin) tmp.height += w->margin->top + w->margin->bottom + w->top_gap; - - GtkAllocation allocation_w; - gtk_widget_get_allocation(widget, &allocation_w); - const int ht = allocation_w.height; - const int skip = darktable.bauhaus->line_height + INNER_PADDING; - wy -= d->active * (darktable.bauhaus->line_height + INNER_PADDING); - darktable.bauhaus->mouse_x = 0; - darktable.bauhaus->mouse_y = d->active * skip + ht / 2; + tmp.height = _get_combobox_popup_height(w); break; } default: + { + fprintf(stderr, "[dt_bauhaus_show_popup] The bauhaus widget has an unknown type\n"); break; + } } - // by default, we want the popup to be exactly at the position of the widget content - if(w->margin) - { - wx += w->margin->left + w->padding->left; - wy += w->margin->top + w->padding->top; - } - - // we update the popup padding defined in css - if(!darktable.bauhaus->popup_padding) darktable.bauhaus->popup_padding = gtk_border_new(); GtkStyleContext *context = gtk_widget_get_style_context(darktable.bauhaus->popup_area); - gtk_style_context_add_class(context, "dt_bauhaus_popup"); - // let's update the css class depending on the source widget type - // this allow to set different padding for example - if(w->show_quad) - gtk_style_context_remove_class(context, "bauhaus-popup-no-quad"); - else - gtk_style_context_add_class(context, "bauhaus-popup-no-quad"); - - const GtkStateFlags state = gtk_widget_get_state_flags(darktable.bauhaus->popup_area); - gtk_style_context_get_padding(context, state, darktable.bauhaus->popup_padding); - // and now we extent the popup to take account of its own padding - wx -= darktable.bauhaus->popup_padding->left; - wy -= darktable.bauhaus->popup_padding->top; - tmp.width += darktable.bauhaus->popup_padding->left + darktable.bauhaus->popup_padding->right; - tmp.height += darktable.bauhaus->popup_padding->top + darktable.bauhaus->popup_padding->bottom; - - if(widget_window) - { - GdkRectangle workarea; - gdk_monitor_get_workarea(gdk_display_get_monitor_at_window(gdk_window_get_display(widget_window), widget_window), &workarea); - wx = MAX(workarea.x, MIN(wx, workarea.x + workarea.width - tmp.width)); - } + gtk_style_context_add_class(context, "dt_bauhaus_popup"); // gtk_widget_get_window will return null if not shown yet. // it is needed for gdk_window_move, and gtk_window move will // sometimes be ignored. this is why we always call both... // we also don't want to show before move, as this results in noticeable flickering. GdkWindow *window = gtk_widget_get_window(darktable.bauhaus->popup_window); - if(window) gdk_window_move(window, wx, wy); - gtk_window_move(GTK_WINDOW(darktable.bauhaus->popup_window), wx, wy); + if(window) gdk_window_move(window, (gint)x, (gint)y); + gtk_window_move(GTK_WINDOW(darktable.bauhaus->popup_window), (gint)x, (gint)y); gtk_widget_set_size_request(darktable.bauhaus->popup_window, tmp.width, tmp.height); + // gtk_window_set_keep_above isn't enough on macOS gtk_window_set_attached_to(GTK_WINDOW(darktable.bauhaus->popup_window), GTK_WIDGET(darktable.bauhaus->current)); gtk_widget_show_all(darktable.bauhaus->popup_window); @@ -2502,14 +2408,13 @@ void dt_bauhaus_show_popup(GtkWidget *widget) static void _slider_add_step(GtkWidget *widget, float delta, guint state, gboolean force) { - if (delta == 0) return; - - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; + if(delta == 0) return; + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; delta *= dt_bauhaus_slider_get_step(widget) * dt_accel_get_speed_multiplier(widget, state); - const float min_visible = powf(10.0f, -d->digits) / fabsf(d->factor); + const float min_visible = 1.f / (fabsf(d->factor) * ipow(10, d->digits)); if(delta && fabsf(delta) < min_visible) delta = copysignf(min_visible, delta); @@ -2537,8 +2442,7 @@ static gboolean _widget_scroll(GtkWidget *widget, GdkEventScroll *event) if(dt_gui_get_scroll_unit_deltas(event, NULL, &delta_y)) { if(delta_y == 0) return TRUE; - - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); if(w->type == DT_BAUHAUS_SLIDER) { @@ -2552,14 +2456,14 @@ static gboolean _widget_scroll(GtkWidget *widget, GdkEventScroll *event) _slider_add_step(widget, - delta_y, event->state, force); } else - _combobox_next_sensitive(w, delta_y, FALSE); + _combobox_next_sensitive(w, delta_y); } return TRUE; // Ensure that scrolling the combobox cannot move side panel } static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); int delta = -1; switch(event->keyval) @@ -2578,7 +2482,7 @@ static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event) if(w->type == DT_BAUHAUS_SLIDER) _slider_add_step(widget, delta, event->state, FALSE); else - _combobox_next_sensitive(w, -delta, FALSE); + _combobox_next_sensitive(w, -delta); return TRUE; default: @@ -2588,47 +2492,55 @@ static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event) static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; - - if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE; bauhaus_request_focus(w); - GtkAllocation tmp; - gtk_widget_get_allocation(GTK_WIDGET(w), &tmp); - const dt_bauhaus_combobox_data_t *d = &w->data.combobox; - if(w->quad_paint && (event->x > allocation.width - _widget_get_quad_width(w))) + double event_x = event->x; + double event_y = event->y; + double width; + _bh_active_region_t activated = _bh_get_active_region(widget, &event_x, &event_y, &width, NULL); + + dt_bauhaus_combobox_data_t *d = &w->data.combobox; + if(d->timeout_handle) g_source_remove(d->timeout_handle); + d->timeout_handle = 0; + + if(activated == BH_REGION_OUT) return FALSE; + + if(activated == BH_REGION_QUAD && w->quad_toggle) { dt_bauhaus_widget_press_quad(widget); return TRUE; } - else if(event->button == 3) - { - darktable.bauhaus->mouse_x = event->x; - darktable.bauhaus->mouse_y = event->y; - dt_bauhaus_show_popup(widget); - return TRUE; - } - else if(event->button == 1) + else { - // reset to default. - if(event->type == GDK_2BUTTON_PRESS) + // If no quad toggle, treat the whole widget as unit pack. + if(event->button == 3) { - // never called, as we popup the other window under your cursor before. - // (except in weird corner cases where the popup is under the -1st entry - dt_bauhaus_combobox_set(widget, d->defpos); - dt_bauhaus_hide_popup(); + darktable.bauhaus->mouse_x = event_x; + darktable.bauhaus->mouse_y = event_y; + dt_bauhaus_show_popup(widget); + return TRUE; } - else + else if(event->button == 1) { - // single click, show options - darktable.bauhaus->opentime = event->time; - darktable.bauhaus->mouse_x = event->x; - darktable.bauhaus->mouse_y = event->y; - dt_bauhaus_show_popup(widget); + // reset to default. + if(event->type == GDK_2BUTTON_PRESS) + { + // never called, as we popup the other window under your cursor before. + // (except in weird corner cases where the popup is under the -1st entry + dt_bauhaus_combobox_set(widget, d->defpos); + dt_bauhaus_hide_popup(); + } + else + { + // single click, show options + darktable.bauhaus->opentime = event->time; + darktable.bauhaus->mouse_x = event_x; + darktable.bauhaus->mouse_y = event_y; + dt_bauhaus_show_popup(widget); + } + return TRUE; } - return TRUE; } return FALSE; } @@ -2636,15 +2548,9 @@ static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButt float dt_bauhaus_slider_get(GtkWidget *widget) { // first cast to bh widget, to check that type: - const dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - if(w->type != DT_BAUHAUS_SLIDER) return -1.0f; + const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); const dt_bauhaus_slider_data_t *d = &w->data.slider; - if(d->max == d->min) - { - return d->max; - } - const float rawval = d->curve(d->pos, DT_BAUHAUS_GET); - return d->min + rawval * (d->max - d->min); + return d->min + d->pos * (d->max - d->min); } float dt_bauhaus_slider_get_val(GtkWidget *widget) @@ -2665,14 +2571,12 @@ char *dt_bauhaus_slider_get_text(GtkWidget *w, float val) void dt_bauhaus_slider_set(GtkWidget *widget, float pos) { // this is the public interface function, translate by bounds and call set_normalized - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - if(w->type != DT_BAUHAUS_SLIDER) return; + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; const float rpos = CLAMP(pos, d->hard_min, d->hard_max); d->min = MIN(d->min, rpos); d->max = MAX(d->max, rpos); - const float rawval = (rpos - d->min) / (d->max - d->min); - dt_bauhaus_slider_set_normalized(w, d->curve(rawval, DT_BAUHAUS_SET)); + dt_bauhaus_slider_set_normalized(w, (rpos - d->min) / (d->max - d->min), TRUE); } void dt_bauhaus_slider_set_val(GtkWidget *widget, float val) @@ -2683,66 +2587,53 @@ void dt_bauhaus_slider_set_val(GtkWidget *widget, float val) void dt_bauhaus_slider_set_digits(GtkWidget *widget, int val) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - - if(w->type != DT_BAUHAUS_SLIDER) return; - + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; - d->digits = val; } int dt_bauhaus_slider_get_digits(GtkWidget *widget) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - - if(w->type != DT_BAUHAUS_SLIDER) return 0; - + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); const dt_bauhaus_slider_data_t *d = &w->data.slider; - return d->digits; } void dt_bauhaus_slider_set_step(GtkWidget *widget, float val) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - - if(w->type != DT_BAUHAUS_SLIDER) return; - + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; - d->step = val; } float dt_bauhaus_slider_get_step(GtkWidget *widget) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - - if(w->type != DT_BAUHAUS_SLIDER) return 0; - + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); const dt_bauhaus_slider_data_t *d = &w->data.slider; - float step = d->step; - if(!step) + if(step == 0.f) { gboolean zoom = dt_conf_get_bool("bauhaus/zoom_step"); const float min = zoom ? d->min : d->soft_min; const float max = zoom ? d->max : d->soft_max; const float top = fminf(max-min, fmaxf(fabsf(min), fabsf(max))); - if(top >= 100) + if(top >= 100.f) { step = 1.f; } else { - step = top * fabsf(d->factor) / 100; + step = top * fabsf(d->factor) / 100.f; const float log10step = log10f(step); - const float fdigits = floorf(log10step+.1); - step = powf(10.f,fdigits); - if(log10step - fdigits > .5) - step *= 5; + const float fdigits = floorf(log10step + .1f); + + // using ipow here makes the UI hang indefinitly. + // Why ? We have +/- inf in fdigits ? + step = powf(10.f, fdigits); + if(log10step - fdigits > .5f) + step *= 5.f; step /= fabsf(d->factor); } } @@ -2752,49 +2643,33 @@ float dt_bauhaus_slider_get_step(GtkWidget *widget) void dt_bauhaus_slider_set_feedback(GtkWidget *widget, int feedback) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - - if(w->type != DT_BAUHAUS_SLIDER) return; - + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; - d->fill_feedback = feedback; - gtk_widget_queue_draw(widget); } int dt_bauhaus_slider_get_feedback(GtkWidget *widget) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - - if(w->type != DT_BAUHAUS_SLIDER) return 0; - + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; - return d->fill_feedback; } void dt_bauhaus_slider_reset(GtkWidget *widget) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - - if(w->type != DT_BAUHAUS_SLIDER) return; + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; - d->min = d->soft_min; d->max = d->soft_max; - dt_bauhaus_slider_set(widget, d->defpos); - return; } void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - if(w->type != DT_BAUHAUS_SLIDER) return; + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; - d->format = g_intern_string(format); if(strstr(format,"%") && fabsf(d->hard_max) <= 10) @@ -2806,99 +2681,107 @@ void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format) void dt_bauhaus_slider_set_factor(GtkWidget *widget, float factor) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - if(w->type != DT_BAUHAUS_SLIDER) return; + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; d->factor = factor; - if(factor < 0) d->curve = _reverse_linear_curve; } void dt_bauhaus_slider_set_offset(GtkWidget *widget, float offset) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - if(w->type != DT_BAUHAUS_SLIDER) return; + dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); dt_bauhaus_slider_data_t *d = &w->data.slider; d->offset = offset; } -void dt_bauhaus_slider_set_curve(GtkWidget *widget, float (*curve)(float value, dt_bauhaus_curve_t dir)) +static gboolean _delayed_slider_commit(gpointer data) { - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget); - if(w->type != DT_BAUHAUS_SLIDER) return; - dt_bauhaus_slider_data_t *d = &w->data.slider; - if(curve == NULL) curve = _default_linear_curve; + // Commit slider value change to pipeline history, handling a safety timout + // so incremental scrolls don't trigger a recompute at every scroll step. - d->pos = curve(d->curve(d->pos, DT_BAUHAUS_GET), DT_BAUHAUS_SET); + // API-wise, this is fucked up because IOPs are the GUI parents of Bauhaus widgets. + // Parents can call children modules (for methods and data structure), + // but children should be independent from parents, so the dependency tree is linear and vertical, + // and each child is a self-enclosed and self-reliant module. - d->curve = curve; -} + // The following introduces a circular dependency of children from their parent, that + // violates the modularity principle, and any change in parent scope will need to be propagated into the children scope, + // which is unexpected and unreliable given the non-existence of a dev doc. -static gboolean _bauhaus_slider_value_change_dragging(gpointer data); + // A mitigation would be to define `dt_iop_gui_changed()` as a callback in the parent, + // then handle the following as a call to an opaque callback defined at implementation time. + // This would advertise the existence of the callback in the parent scope + // AND make the following immune to changes in parent. -static void _bauhaus_slider_value_change(dt_bauhaus_widget_t *w) -{ - if(!GTK_IS_WIDGET(w)) return; + dt_bauhaus_widget_t *w = data; + w->data.slider.timeout_handle = 0; + g_signal_emit_by_name(G_OBJECT(w), "value-changed"); - dt_bauhaus_slider_data_t *d = &w->data.slider; - if(d->is_changed && !d->timeout_handle && !darktable.gui->reset) + // TODO: rewrite that as callback. + if(w->field && w->module) { - if(w->field) + float val = dt_bauhaus_slider_get(GTK_WIDGET(w)); + switch(w->field_type) { - float val = dt_bauhaus_slider_get(GTK_WIDGET(w)); - switch(w->field_type) - { - case DT_INTROSPECTION_TYPE_FLOAT:; - float *f = w->field, prevf = *f; *f = val; - if(*f != prevf) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &prevf); - break; - case DT_INTROSPECTION_TYPE_INT:; - int *i = w->field, previ = *i; *i = val; - if(*i != previ) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &previ); - break; - case DT_INTROSPECTION_TYPE_USHORT:; - unsigned short *s = w->field, prevs = *s; *s = val; - if(*s != prevs) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &prevs); - break; - default: - fprintf(stderr, "[_bauhaus_slider_value_change] unsupported slider data type\n"); - } + case DT_INTROSPECTION_TYPE_FLOAT:; + float *f = w->field, prevf = *f; *f = val; + if(*f != prevf) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &prevf); + break; + case DT_INTROSPECTION_TYPE_INT:; + int *i = w->field, previ = *i; *i = val; + if(*i != previ) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &previ); + break; + case DT_INTROSPECTION_TYPE_USHORT:; + unsigned short *s = w->field, prevs = *s; *s = val; + if(*s != prevs) dt_iop_gui_changed(w->module, GTK_WIDGET(w), &prevs); + break; + default: + fprintf(stderr, "[_bauhaus_slider_value_change] unsupported slider data type\n"); } - darktable.gui->has_scroll_focus = GTK_WIDGET(w); - g_signal_emit_by_name(G_OBJECT(w), "value-changed"); - d->is_changed = 0; - } - - if(d->is_changed && d->is_dragging && !d->timeout_handle) - { - const int delay = CLAMP(darktable.develop->average_delay * 3 / 2, DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MIN, - DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MAX); - d->timeout_handle = g_timeout_add(delay, _bauhaus_slider_value_change_dragging, w); } -} -static gboolean _bauhaus_slider_value_change_dragging(gpointer data) -{ - dt_bauhaus_widget_t *w = data; - w->data.slider.timeout_handle = 0; - _bauhaus_slider_value_change(data); return G_SOURCE_REMOVE; } -static void dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t *w, float pos) +/** + * @brief Set the value of a slider as a ratio of the GUI slider width + * + * @param w Bauhaus widget + * @param pos Relative position over the slider bar (ratio between 0 and 1) + * @param raise Set to FALSE to redraw slider position without committing the actual value to pipeline + * nor sending the `value-changed` event (e.g. in motion-notify events, while dragging). + * Set to TRUE when the change is finished (e.g. in button-pressed events). + */ +static void dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t *w, float pos, gboolean raise) { dt_bauhaus_slider_data_t *d = &w->data.slider; - float rpos = CLAMP(pos, 0.0f, 1.0f); - rpos = d->curve(rpos, DT_BAUHAUS_GET); - rpos = d->min + (d->max - d->min) * rpos; - const float base = powf(10.0f, d->digits) * d->factor; - rpos = roundf(base * rpos) / base; - - rpos = (rpos - d->min) / (d->max - d->min); - d->pos = d->curve(rpos, DT_BAUHAUS_SET); - gtk_widget_queue_draw(GTK_WIDGET(w)); - d->is_changed = 1; + const float old_pos = d->pos; + const float new_pos = CLAMP(pos, 0.0f, 1.0f); + + if(old_pos != new_pos || raise) + { + const float new_value = new_pos * (d->max - d->min) + d->min; + const float precision = (float)ipow(10, d->digits); + const float rounded_value = roundf(new_value * precision) / precision; + d->pos = (rounded_value - d->min) / (d->max - d->min); + d->oldpos = d->pos; + gtk_widget_queue_draw(GTK_WIDGET(w)); + +#if DEBUG + fprintf(stdout, "%s | %s | %s | min: %f, max: %f, ratio: %f, base : %f, pos: %f\n", + (w->module && w->module->owner) ? "has owner" : "orphan", + (w->module) ? w->module->label : "anon", + w->label, + d->min, d->max, d->pos, d->factor, d->pos); +#endif - _bauhaus_slider_value_change(w); + if(!darktable.gui->reset && raise && !d->timeout_handle) + { + const int delay = CLAMP(darktable.develop->average_delay * 3 / 2, + DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MIN, + DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MAX); + d->timeout_handle = g_timeout_add(delay, _delayed_slider_commit, w); + } + } } static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data) @@ -2907,9 +2790,6 @@ static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event { case DT_BAUHAUS_SLIDER: { - // hack to do screenshots from popup: - // if(event->string[0] == 'p') return system("scrot"); - // else if(darktable.bauhaus->keys_cnt + 2 < 64 && (event->keyval == GDK_KEY_space || event->keyval == GDK_KEY_KP_Space || // SPACE event->keyval == GDK_KEY_percent || // % @@ -2951,7 +2831,7 @@ static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event } else return FALSE; - if(darktable.bauhaus->keys_cnt > 0) _start_cursor(-1); + return TRUE; } case DT_BAUHAUS_COMBOBOX: @@ -2959,8 +2839,6 @@ static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event if(!g_utf8_validate(event->string, -1, NULL)) return FALSE; const gunichar c = g_utf8_get_char(event->string); const long int char_width = g_utf8_next_char(event->string) - event->string; - // if(event->string[0] == 'p') return system("scrot"); - // else if(darktable.bauhaus->keys_cnt + 1 + char_width < 64 && g_unichar_isprint(c)) { // only accept key input if still valid or editable? @@ -3000,11 +2878,11 @@ static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event } else if(event->keyval == GDK_KEY_Up) { - combobox_popup_scroll(-1); + _combobox_next_sensitive(darktable.bauhaus->current, -1); } else if(event->keyval == GDK_KEY_Down) { - combobox_popup_scroll(1); + _combobox_next_sensitive(darktable.bauhaus->current, +1); } else if(event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) { @@ -3028,55 +2906,62 @@ static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event static gboolean dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data) { dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; + dt_bauhaus_slider_data_t *d = &w->data.slider; + bauhaus_request_focus(w); - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); - const double slider_width = allocation.width - w->margin->left - w->padding->left - w->margin->right - w->padding->right; - const double event_x = event->x - w->margin->left - w->padding->left; - const double event_y = event->y - w->margin->top - w->padding->top; - if(event->x > allocation.width - _widget_get_quad_width(w) - w->margin->right - w->padding->right) + double event_x = event->x; + double event_y = event->y; + double main_width = 0.; + _bh_active_region_t activated = _bh_get_active_region(widget, &event_x, &event_y, &main_width, NULL); + darktable.bauhaus->mouse_x = event_x; + darktable.bauhaus->mouse_y = event_y; + + if(activated == BH_REGION_OUT) return FALSE; + + if(activated == BH_REGION_QUAD) { dt_bauhaus_widget_press_quad(widget); return TRUE; } - else if(event->button == 3) - { - dt_bauhaus_show_popup(widget); - return TRUE; - } - else if(event->button == 2) + else if(activated == BH_REGION_MAIN) { - _slider_zoom_range(w, 0); // reset zoom range to soft min/max - _slider_zoom_toast(w); - } - else if(event->button == 1) - { - dt_bauhaus_slider_data_t *d = &w->data.slider; - // reset to default. - if(event->type == GDK_2BUTTON_PRESS) - { - d->is_dragging = 0; - dt_bauhaus_slider_reset(widget); - } - else if(event_y > darktable.bauhaus->line_height * 0.9) // allow some margin of inaccuracy when clicking + if(event->button == 1) { - d->is_dragging = 1; - - if(!dt_modifier_is(event->state, 0)) + if(event->type == GDK_2BUTTON_PRESS) { - darktable.bauhaus->mouse_x = event_x; + // double left click on the main region : reset value to default + dt_bauhaus_slider_reset(widget); + d->is_dragging = 0; } else { - const float x_from_right = slider_position_from_right(slider_width, w); - dt_bauhaus_slider_set_normalized(w, (event_x / slider_width) / x_from_right); - darktable.bauhaus->mouse_x = NAN; + // single left click on main region : redraw the slider immediately + // but without committing results to pipeline yet. + if(event_y < darktable.bauhaus->line_height) + { + // single left click on the header name : do nothing (only give focus) + d->is_dragging = 0; + } + else + { + // single left click on slider bar : set new value + d->is_dragging = 1; + dt_bauhaus_slider_set_normalized(w, event_x / main_width, FALSE); + } } } - else // we clicked on the header name : do nothing but give focus + else if(event->button == 3) + { + // right click : show accurate slider setting popup + d->oldpos = d->pos; + dt_bauhaus_show_popup(widget); + } + else if(event->button == 2) { - d->is_dragging = 0; + // middle click : reset zoom range to soft min/max + _slider_zoom_range(w, 0); + _slider_zoom_toast(w); } return TRUE; } @@ -3089,14 +2974,21 @@ static gboolean dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButt dt_bauhaus_slider_data_t *d = &w->data.slider; dt_bauhaus_widget_release_quad(widget); - if(event->button == 1 && d->is_dragging) + + // is_dragging is set TRUE no matter what on button_press, except if it's a double click. + // double click is the only event handled in button_press, otherwise we assume every event + // is drag and drop, and handle the final drag coordinate here. + if(d->is_dragging) { d->is_dragging = 0; if(d->timeout_handle) g_source_remove(d->timeout_handle); d->timeout_handle = 0; - dt_bauhaus_slider_set_normalized(w, d->pos); - return TRUE; + if(event->button == 1) + { + dt_bauhaus_slider_set_normalized(w, darktable.bauhaus->mouse_x / _widget_get_main_width(w, NULL, NULL), TRUE); + return TRUE; + } } return FALSE; } @@ -3105,275 +2997,23 @@ static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotio { dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; dt_bauhaus_slider_data_t *d = &w->data.slider; + _bh_active_region_t activated = BH_REGION_OUT; // = 0 - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); - const double slider_width = allocation.width - w->margin->left - w->padding->left - w->margin->right - w->padding->right; - const double event_x = event->x - w->margin->left - w->padding->left; if(d->is_dragging && event->state & GDK_BUTTON1_MASK) { - const float r = slider_position_from_right((float)slider_width, w); - - if(isnan(darktable.bauhaus->mouse_x)) - { - if(dt_modifier_is(event->state, 0)) - dt_bauhaus_slider_set_normalized(w, (event_x / slider_width) / r); - else - darktable.bauhaus->mouse_x = event_x; - } - else - { - const float scaled_step = slider_width * r * dt_bauhaus_slider_get_step(widget) / (d->max - d->min); - const float steps = floorf((event_x - darktable.bauhaus->mouse_x) / scaled_step); - _slider_add_step(widget, copysignf(1, d->factor) * steps, event->state, FALSE); - - darktable.bauhaus->mouse_x += steps * scaled_step; - } - } - - if(event_x <= slider_width - _widget_get_quad_width(w)) - { - darktable.control->element - = event_x > (0.1 * (slider_width - _widget_get_quad_width(w))) && event_x < (0.9 * (slider_width - _widget_get_quad_width(w))) - ? DT_ACTION_ELEMENT_VALUE - : DT_ACTION_ELEMENT_FORCE; - } - else - darktable.control->element = DT_ACTION_ELEMENT_BUTTON; - - return TRUE; -} - -static gboolean dt_bauhaus_combobox_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data) -{ - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; - GtkAllocation allocation; - gtk_widget_get_allocation(widget, &allocation); - - darktable.control->element = event->x <= allocation.width - _widget_get_quad_width(w) - ? DT_ACTION_ELEMENT_SELECTION - : DT_ACTION_ELEMENT_BUTTON; - - return TRUE; -} - -void dt_bauhaus_combobox_mute_scrolling(GtkWidget *widget) -{ - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; - dt_bauhaus_combobox_data_t *d = &w->data.combobox; - d->mute_scrolling = TRUE; -} - -static void _action_process_button(GtkWidget *widget, dt_action_effect_t effect) -{ - dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget); - if(effect != (w->quad_paint_flags & CPF_ACTIVE ? DT_ACTION_EFFECT_ON : DT_ACTION_EFFECT_OFF)) - dt_bauhaus_widget_press_quad(widget); - - gchar *text = w->quad_paint_flags & CPF_ACTIVE ? _("button on") : _("button off"); - dt_action_widget_toast(w->module, widget, text); - - gtk_widget_queue_draw(widget); -} - -static float _action_process_slider(gpointer target, dt_action_element_t element, dt_action_effect_t effect, float move_size) -{ - GtkWidget *widget = GTK_WIDGET(target); - dt_bauhaus_widget_t *bhw = DT_BAUHAUS_WIDGET(widget); - dt_bauhaus_slider_data_t *d = &bhw->data.slider; - - if(!isnan(move_size)) - { - switch(element) - { - case DT_ACTION_ELEMENT_VALUE: - case DT_ACTION_ELEMENT_FORCE: - switch(effect) - { - case DT_ACTION_EFFECT_POPUP: - dt_bauhaus_show_popup(widget); - break; - case DT_ACTION_EFFECT_DOWN: - move_size *= -1; - case DT_ACTION_EFFECT_UP: - ++d->is_dragging; - _slider_add_step(widget, move_size, GDK_MODIFIER_MASK, element == DT_ACTION_ELEMENT_FORCE); - --d->is_dragging; - break; - case DT_ACTION_EFFECT_RESET: - dt_bauhaus_slider_reset(widget); - break; - case DT_ACTION_EFFECT_TOP: - dt_bauhaus_slider_set(widget, element == DT_ACTION_ELEMENT_FORCE ? d->hard_max: d->max); - break; - case DT_ACTION_EFFECT_BOTTOM: - dt_bauhaus_slider_set(widget, element == DT_ACTION_ELEMENT_FORCE ? d->hard_min: d->min); - break; - case DT_ACTION_EFFECT_SET: - dt_bauhaus_slider_set(widget, move_size); - break; - default: - fprintf(stderr, "[_action_process_slider] unknown shortcut effect (%d) for slider\n", effect); - break; - } + double event_x = event->x; + double event_y = event->y; + double main_width; + activated = _bh_get_active_region(widget, &event_x, &event_y, &main_width, NULL); - gchar *text = dt_bauhaus_slider_get_text(widget, dt_bauhaus_slider_get(widget)); - dt_action_widget_toast(bhw->module, widget, text); - g_free(text); - - break; - case DT_ACTION_ELEMENT_BUTTON: - _action_process_button(widget, effect); - break; - case DT_ACTION_ELEMENT_ZOOM: - ; - switch(effect) - { - case DT_ACTION_EFFECT_POPUP: - dt_bauhaus_show_popup(widget); - break; - case DT_ACTION_EFFECT_RESET: - move_size = 0; - case DT_ACTION_EFFECT_DOWN: - move_size *= -1; - case DT_ACTION_EFFECT_UP: - _slider_zoom_range(bhw, move_size); - break; - case DT_ACTION_EFFECT_TOP: - case DT_ACTION_EFFECT_BOTTOM: - if((effect == DT_ACTION_EFFECT_TOP) ^ (d->factor < 0)) - d->max = d->hard_max; - else - d->min = d->hard_min; - gtk_widget_queue_draw(widget); - break; - default: - fprintf(stderr, "[_action_process_slider] unknown shortcut effect (%d) for slider\n", effect); - break; - } - - _slider_zoom_toast(bhw); - break; - default: - fprintf(stderr, "[_action_process_slider] unknown shortcut element (%d) for slider\n", element); - break; - } + darktable.bauhaus->mouse_x = event_x; + darktable.bauhaus->mouse_y = event_y; + dt_bauhaus_slider_set_normalized(w, event_x / main_width, FALSE); } - if(element == DT_ACTION_ELEMENT_BUTTON) - return dt_bauhaus_widget_get_quad_active(widget); - - if(effect == DT_ACTION_EFFECT_SET) - return dt_bauhaus_slider_get(widget); - - return d->pos + - ( d->min == -d->max ? DT_VALUE_PATTERN_PLUS_MINUS : - ( d->min == 0 && (d->max == 1 || d->max == 100) ? DT_VALUE_PATTERN_PERCENTAGE : 0 )); -} - -gboolean combobox_idle_value_changed(gpointer widget) -{ - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; - _bauhaus_combobox_set(w, w->data.combobox.active, FALSE); - - while(g_idle_remove_by_data(widget)); - - return G_SOURCE_REMOVE; + return activated; } -static float _action_process_combo(gpointer target, dt_action_element_t element, dt_action_effect_t effect, float move_size) -{ - GtkWidget *widget = GTK_WIDGET(target); - dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget; - int value = dt_bauhaus_combobox_get(widget); - - if(!isnan(move_size)) - { - if(element == DT_ACTION_ELEMENT_BUTTON) - { - _action_process_button(widget, effect); - return dt_bauhaus_widget_get_quad_active(widget); - } - else switch(effect) - { - case DT_ACTION_EFFECT_POPUP: - dt_bauhaus_show_popup(widget); - break; - case DT_ACTION_EFFECT_LAST: - move_size *= - 1; // reversed in effect_previous - case DT_ACTION_EFFECT_FIRST: - move_size *= 1e3; // reversed in effect_previous - case DT_ACTION_EFFECT_PREVIOUS: - move_size *= - 1; - case DT_ACTION_EFFECT_NEXT: - ++darktable.gui->reset; - _combobox_next_sensitive(w, move_size, FALSE); - --darktable.gui->reset; - - g_idle_add(combobox_idle_value_changed, widget); - break; - case DT_ACTION_EFFECT_RESET: - value = dt_bauhaus_combobox_get_default(widget); - dt_bauhaus_combobox_set(widget, value); - break; - default: - value = effect - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1; - dt_bauhaus_combobox_set(widget, value); - break; - } - - gchar *text = g_strdup_printf("\n%s", dt_bauhaus_combobox_get_text(widget)); - dt_action_widget_toast(w->module, widget, text); - g_free(text); - } - - if(element == DT_ACTION_ELEMENT_BUTTON) - return dt_bauhaus_widget_get_quad_active(widget); - - for(int i = value; i >= 0; i--) - { - dt_bauhaus_combobox_entry_t *entry = g_ptr_array_index(w->data.combobox.entries, i); - if(!entry->sensitive) value--; // don't count unselectable combo items in value - } - return - 1 - value + (value == effect - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1 ? DT_VALUE_PATTERN_ACTIVE : 0); -} - -const dt_action_element_def_t _action_elements_slider[] - = { { N_("value"), dt_action_effect_value }, - { N_("button"), dt_action_effect_toggle }, - { N_("force"), dt_action_effect_value }, - { N_("zoom"), dt_action_effect_value }, - { NULL } }; -const dt_action_element_def_t _action_elements_combo[] - = { { N_("selection"), dt_action_effect_selection }, - { N_("button"), dt_action_effect_toggle }, - { NULL } }; - -static const dt_shortcut_fallback_t _action_fallbacks_slider[] - = { { .element = DT_ACTION_ELEMENT_BUTTON, .button = DT_SHORTCUT_LEFT }, - { .element = DT_ACTION_ELEMENT_BUTTON, .effect = DT_ACTION_EFFECT_TOGGLE_CTRL, .button = DT_SHORTCUT_LEFT, .mods = GDK_CONTROL_MASK }, - { .element = DT_ACTION_ELEMENT_FORCE, .mods = GDK_CONTROL_MASK | GDK_SHIFT_MASK, .speed = 10.0 }, - { .element = DT_ACTION_ELEMENT_ZOOM, .effect = DT_ACTION_EFFECT_DEFAULT_MOVE, .button = DT_SHORTCUT_RIGHT, .move = DT_SHORTCUT_MOVE_VERTICAL }, - { } }; -static const dt_shortcut_fallback_t _action_fallbacks_combo[] - = { { .element = DT_ACTION_ELEMENT_SELECTION, .effect = DT_ACTION_EFFECT_RESET, .button = DT_SHORTCUT_LEFT, .click = DT_SHORTCUT_DOUBLE }, - { .element = DT_ACTION_ELEMENT_BUTTON, .button = DT_SHORTCUT_LEFT }, - { .element = DT_ACTION_ELEMENT_BUTTON, .effect = DT_ACTION_EFFECT_TOGGLE_CTRL, .button = DT_SHORTCUT_LEFT, .mods = GDK_CONTROL_MASK }, - { .move = DT_SHORTCUT_MOVE_SCROLL, .effect = DT_ACTION_EFFECT_DEFAULT_MOVE, .speed = -1 }, - { .move = DT_SHORTCUT_MOVE_VERTICAL, .effect = DT_ACTION_EFFECT_DEFAULT_MOVE, .speed = -1 }, - { } }; - -const dt_action_def_t dt_action_def_slider - = { N_("slider"), - _action_process_slider, - _action_elements_slider, - _action_fallbacks_slider }; -const dt_action_def_t dt_action_def_combo - = { N_("dropdown"), - _action_process_combo, - _action_elements_combo, - _action_fallbacks_combo }; - // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent diff --git a/src/bauhaus/bauhaus.h b/src/bauhaus/bauhaus.h index 2a192cd2a5eb..cdc46860844d 100644 --- a/src/bauhaus/bauhaus.h +++ b/src/bauhaus/bauhaus.h @@ -84,10 +84,8 @@ typedef struct dt_bauhaus_slider_data_t float factor; // multiplication factor before printing float offset; // addition before printing - int is_dragging : 1; // indicates is mouse is dragging slider - int is_changed : 1; // indicates new data + gboolean is_dragging; // indicates is mouse is dragging slider guint timeout_handle; // used to store id of timeout routine - float (*curve)(float, dt_bauhaus_curve_t); // callback function } dt_bauhaus_slider_data_t; typedef enum dt_bauhaus_combobox_alignment_t @@ -109,13 +107,14 @@ typedef struct dt_bauhaus_combobox_entry_t typedef struct dt_bauhaus_combobox_data_t { int active; // currently active element + int hovered; // currently hovered element, to be used by drawings until and if it is set to active int defpos; // default position int editable; // 1 if arbitrary text may be typed dt_bauhaus_combobox_alignment_t text_align; // if selected text in combo should be aligned to the left/right char *text; // to hold arbitrary text if editable PangoEllipsizeMode entries_ellipsis; GPtrArray *entries; - gboolean mute_scrolling; // if set, prevents to issue "data-changed" + guint timeout_handle; // used to store id of timeout routine void (*populate)(GtkWidget *w, struct dt_iop_module_t **module); // function to populate the combo list on the fly } dt_bauhaus_combobox_data_t; @@ -169,11 +168,6 @@ typedef struct dt_bauhaus_widget_t // margin and padding structure, defined in css, retrieve on each draw GtkBorder *margin, *padding; - // gap to add to the top padding due to the vertical centering - int top_gap; - - // is the popup not attached to the main widget (shortcuts) - gboolean detached_popup; // goes last, might extend past the end: dt_bauhaus_data_t data; @@ -227,13 +221,6 @@ typedef struct dt_bauhaus_t float border_width; // width of the border of the slider marker float quad_width; // width of the quad area to paint icons PangoFontDescription *pango_font_desc; // no need to recreate this for every string we want to print - PangoFontDescription *pango_sec_font_desc; // as above but for section labels - GtkBorder *popup_padding; // padding of the popup. updated in show function - - // the slider popup has a blinking cursor - guint cursor_timeout; - gboolean cursor_visible; - int cursor_blink_counter; // colors for sliders and comboboxes GdkRGBA color_fg, color_fg_insensitive, color_bg, color_border, indicator_border, color_fill; @@ -326,7 +313,6 @@ void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, void dt_bauhaus_slider_clear_stops(GtkWidget *widget); void dt_bauhaus_slider_set_default(GtkWidget *widget, float def); float dt_bauhaus_slider_get_default(GtkWidget *widget); -void dt_bauhaus_slider_set_curve(GtkWidget *widget, float (*curve)(float value, dt_bauhaus_curve_t dir)); // combobox: void dt_bauhaus_combobox_from_widget(struct dt_bauhaus_widget_t* widget,dt_iop_module_t *self); @@ -370,7 +356,6 @@ void dt_bauhaus_combobox_add_list(GtkWidget *widget, dt_action_t *action, const void dt_bauhaus_combobox_entry_set_sensitive(GtkWidget *widget, int pos, gboolean sensitive); void dt_bauhaus_combobox_set_entries_ellipsis(GtkWidget *widget, PangoEllipsizeMode ellipis); PangoEllipsizeMode dt_bauhaus_combobox_get_entries_ellipsis(GtkWidget *widget); -void dt_bauhaus_combobox_mute_scrolling(GtkWidget *widget); void bauhaus_request_focus(dt_bauhaus_widget_t *w); static inline void set_color(cairo_t *cr, GdkRGBA color) diff --git a/src/develop/imageop.c b/src/develop/imageop.c index 87a09688c6db..30186de3688b 100644 --- a/src/develop/imageop.c +++ b/src/develop/imageop.c @@ -1709,6 +1709,27 @@ void dt_iop_gui_update(dt_iop_module_t *module) if(module->params && module->gui_update) module->gui_update(module); + // Shitty dependency graph ahead !!! + // dt_bauhaus_update_module is in bauhaus.c + // 1. it calls dt_bauhaus_slider_set() and dt_bauhaus_combobox_set() + // 2. those call dt_iop_gui_changed() from here (imageop.c) + // 3. dt_iop_gui_changed() calls module->gui_changed() if available + // BUT only if darktable.gui->reset is 0. + // So we have to call module->gui_update() which may or may not call module->gui_changed(), + // depending on modules, to init and reset sliders and comboboxes not linked to module params. + // Because those widgets directly declared from params introspection are added to the module->bh_widget_list + // (GSList *), which is updated in dt_bauhaus_update_module(); + // TODO: fix this FUCKING mess: + // 1. the bauhaus lib should not change its behaviour based on the state of the global variable darktable.gui->reset, + // instead the global variable should be checked upstream before dispatching bauhaus events (like set, + // reset, update). Nesting dependencies across layers of libs to the state of a global variable is super unreliable and prone + // to race conditions. + // 2. the bauhaus lib should be unaware of imageop.c lib (modules). If modules need to perform operations, + // they should connect callbacks to the `value-changed` signal. The bauhaus lib should be treated as an + // an extension of Gtk, implementation-agnostic. + // 3. Ideally, all sliders and comboboxes should be updated through an unified method to prevent + // programmer errors in the future because, again, WE DON'T HAVE DEV DOC, so API need to be dummy-proof. + dt_iop_gui_update_blending(module); dt_iop_gui_update_expanded(module); } @@ -2953,6 +2974,8 @@ gboolean dt_iop_have_required_input_format(const int req_ch, struct dt_iop_modul } } +// WARNING: this is called in bauhaus.c too when slider & comboboxes values are changed. +// Mind that if any change is done here. void dt_iop_gui_changed(dt_action_t *action, GtkWidget *widget, gpointer data) { if(!action || action->type != DT_ACTION_TYPE_IOP_INSTANCE) return; diff --git a/src/iop/ashift.c b/src/iop/ashift.c index 61af8e245fb6..3d7c2150697f 100644 --- a/src/iop/ashift.c +++ b/src/iop/ashift.c @@ -5696,7 +5696,6 @@ void gui_init(struct dt_iop_module_t *self) g->f_length = dt_bauhaus_slider_from_params(self, "f_length"); dt_bauhaus_slider_set_soft_range(g->f_length, 10.0f, 1000.0f); - dt_bauhaus_slider_set_curve(g->f_length, log10_curve); dt_bauhaus_slider_set_digits(g->f_length, 0); dt_bauhaus_slider_set_format(g->f_length, " mm"); @@ -5712,7 +5711,6 @@ void gui_init(struct dt_iop_module_t *self) gtk_widget_set_visible(g->orthocorr, FALSE); g->aspect = dt_bauhaus_slider_from_params(self, "aspect"); - dt_bauhaus_slider_set_curve(g->aspect, log2_curve); gtk_box_pack_start(GTK_BOX(g->cs.container), g->specifics, TRUE, TRUE, 0); diff --git a/src/libs/collect.c b/src/libs/collect.c index 4056825a656a..7c7a010eb309 100644 --- a/src/libs/collect.c +++ b/src/libs/collect.c @@ -3239,8 +3239,6 @@ void gui_init(dt_lib_module_t *self) dt_bauhaus_combobox_set_selected_text_align(d->rule[i].combo, DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT); _populate_collect_combo(d->rule[i].combo); - dt_bauhaus_combobox_mute_scrolling(d->rule[i].combo); - g_signal_connect(G_OBJECT(d->rule[i].combo), "value-changed", G_CALLBACK(combo_changed), d->rule + i); gtk_box_pack_start(box, d->rule[i].combo, FALSE, FALSE, 0);