1 /*
2     This file is part of darktable,
3     Copyright (C) 2012-2021 darktable developers.
4 
5     darktable is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     darktable is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include "bauhaus/bauhaus.h"
20 #include "common/calculator.h"
21 #include "common/darktable.h"
22 #include "control/conf.h"
23 #include "develop/develop.h"
24 #include "develop/imageop.h"
25 #include "gui/accelerators.h"
26 #include "gui/gtk.h"
27 #ifdef GDK_WINDOWING_QUARTZ
28 #include "osx/osx.h"
29 #endif
30 
31 #include <math.h>
32 #include <strings.h>
33 
34 #include <pango/pangocairo.h>
35 
36 G_DEFINE_TYPE(DtBauhausWidget, dt_bh, GTK_TYPE_DRAWING_AREA)
37 
38 // INNER_PADDING is the horizontal space between slider and quad
39 // and vertical space between labels and slider baseline
40 static const double INNER_PADDING = 4.0;
41 
42 // fwd declare
43 static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data);
44 static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data);
45 static void dt_bauhaus_widget_accept(dt_bauhaus_widget_t *w);
46 static void dt_bauhaus_widget_reject(dt_bauhaus_widget_t *w);
47 static void _bauhaus_combobox_set(GtkWidget *widget, const int pos, const gboolean mute);
48 
_combobox_next_entry(GList * entries,int * new_pos,int delta_y)49 static gboolean _combobox_next_entry(GList *entries, int *new_pos, int delta_y)
50 {
51   dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)g_list_nth_data(entries, *new_pos);
52   while(entry && !entry->sensitive)
53   {
54     *new_pos += delta_y;
55     entry = (dt_bauhaus_combobox_entry_t *)g_list_nth_data(entries, *new_pos);
56   }
57   return entry != NULL;
58 }
59 
get_line_height()60 static inline int get_line_height()
61 {
62   return darktable.bauhaus->scale * darktable.bauhaus->line_height;
63 }
64 
new_combobox_entry(const char * label,dt_bauhaus_combobox_alignment_t alignment,gboolean sensitive,void * data,void (* free_func)(void *))65 static dt_bauhaus_combobox_entry_t *new_combobox_entry(const char *label, dt_bauhaus_combobox_alignment_t alignment,
66                                                        gboolean sensitive, void *data, void (*free_func)(void *))
67 {
68   dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)calloc(1, sizeof(dt_bauhaus_combobox_entry_t));
69   entry->label = g_strdup(label);
70   entry->alignment = alignment;
71   entry->sensitive = sensitive;
72   entry->data = data;
73   entry->free_func = free_func;
74   return entry;
75 }
76 
free_combobox_entry(gpointer data)77 static void free_combobox_entry(gpointer data)
78 {
79   dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)data;
80   g_free(entry->label);
81   if(entry->free_func)
82     entry->free_func(entry->data);
83   free(entry);
84 }
85 
inner_height(GtkAllocation allocation)86 static inline float inner_height(GtkAllocation allocation)
87 {
88   // retrieve the inner height of the widget (inside the top/bottom margin)
89   return allocation.height - 2.0f * darktable.bauhaus->widget_space;
90 }
91 
default_color_assign()92 static GdkRGBA * default_color_assign()
93 {
94   // helper to initialize a color pointer with red color as a default
95   GdkRGBA color;
96   color.red = 1.0f;
97   color.green = 0.0f;
98   color.blue = 0.0f;
99   color.alpha = 1.0f;
100   return gdk_rgba_copy(&color);
101 }
102 
dt_bauhaus_widget_set_section(GtkWidget * widget,const gboolean is_section)103 void dt_bauhaus_widget_set_section(GtkWidget *widget, const gboolean is_section)
104 {
105   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
106   w->is_section = is_section;
107 }
108 
show_pango_text(dt_bauhaus_widget_t * w,GtkStyleContext * context,cairo_t * cr,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)109 static int show_pango_text(dt_bauhaus_widget_t *w, GtkStyleContext *context, cairo_t *cr,
110                            char *text, float x_pos, float y_pos, float max_width,
111                            gboolean right_aligned, gboolean calc_only,
112                            PangoEllipsizeMode ellipsize, gboolean is_markup, gboolean is_label)
113 {
114   PangoLayout *layout = pango_cairo_create_layout(cr);
115 
116   if(max_width > 0)
117   {
118     pango_layout_set_ellipsize(layout, ellipsize);
119     pango_layout_set_width(layout, (int)(PANGO_SCALE * max_width + 0.5f));
120   }
121 
122   if(text)
123   {
124     if(is_markup)
125       pango_layout_set_markup(layout, text, -1);
126     else
127       pango_layout_set_text(layout, text, -1);
128   }
129   else
130   {
131     // length of -1 is not allowed with NULL string (wtf)
132     pango_layout_set_text(layout, NULL, 0);
133   }
134 
135   PangoFontDescription *font_desc =
136     w->is_section && is_label
137     ? pango_font_description_copy_static(darktable.bauhaus->pango_sec_font_desc)
138     : pango_font_description_copy_static(darktable.bauhaus->pango_font_desc);
139 
140   // This should be able to update the font style for current text depending on :hover, :focused, etc.
141   // CSS pseudo-classes, yet it defaults to system font.
142   // FIXME: get that working so we can put :active text in bold, for example.
143   //gtk_style_context_get(context, gtk_widget_get_state_flags(GTK_WIDGET(w)), "font", font_desc, NULL);
144 
145   pango_layout_set_font_description(layout, font_desc);
146 
147   PangoAttrList *attrlist = pango_attr_list_new();
148   PangoAttribute *attr = pango_attr_font_features_new("tnum");
149   pango_attr_list_insert(attrlist, attr);
150   pango_layout_set_attributes(layout, attrlist);
151   pango_attr_list_unref(attrlist);
152 
153   pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
154 
155   int pango_width, pango_height;
156   pango_layout_get_size(layout, &pango_width, &pango_height);
157   const float text_width = ((double)pango_width/PANGO_SCALE);
158 
159   if(right_aligned) x_pos -= text_width;
160 
161   if(!calc_only)
162   {
163     cairo_move_to(cr, x_pos, y_pos);
164     pango_cairo_show_layout(cr, layout);
165   }
166   pango_font_description_free(font_desc);
167   g_object_unref(layout);
168 
169   return text_width;
170 }
171 
172 // -------------------------------
_cursor_timeout_callback(gpointer user_data)173 static gboolean _cursor_timeout_callback(gpointer user_data)
174 {
175   if(darktable.bauhaus->cursor_blink_counter > 0) darktable.bauhaus->cursor_blink_counter--;
176 
177   darktable.bauhaus->cursor_visible = !darktable.bauhaus->cursor_visible;
178   gtk_widget_queue_draw(darktable.bauhaus->popup_area);
179 
180  // this can be >0 when we haven't reached the desired number or -1 when blinking forever
181   if(darktable.bauhaus->cursor_blink_counter != 0)
182     return TRUE;
183 
184   darktable.bauhaus->cursor_timeout = 0; // otherwise the cursor won't come up when starting to type
185   return FALSE;
186 }
187 
_start_cursor(int max_blinks)188 static void _start_cursor(int max_blinks)
189 {
190   darktable.bauhaus->cursor_blink_counter = max_blinks;
191   darktable.bauhaus->cursor_visible = FALSE;
192   if(darktable.bauhaus->cursor_timeout == 0)
193     darktable.bauhaus->cursor_timeout = g_timeout_add(500, _cursor_timeout_callback, NULL);
194 }
195 
_stop_cursor()196 static void _stop_cursor()
197 {
198   if(darktable.bauhaus->cursor_timeout > 0)
199   {
200     g_source_remove(darktable.bauhaus->cursor_timeout);
201     darktable.bauhaus->cursor_timeout = 0;
202     darktable.bauhaus->cursor_visible = FALSE;
203   }
204 }
205 // -------------------------------
206 
207 
208 static void dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t *w, float pos);
209 
slider_right_pos(float width)210 static float slider_right_pos(float width)
211 {
212   // relative position (in widget) of the right bound of the slider corrected with the inner padding
213   return 1.0f - (darktable.bauhaus->quad_width + INNER_PADDING) / width;
214 }
215 
slider_coordinate(const float abs_position,const float width)216 static float slider_coordinate(const float abs_position, const float width)
217 {
218   // Translates an horizontal position relative to the slider
219   // in an horizontal position relative to the widget
220   const float left_bound = 0.0f;
221   const float right_bound = slider_right_pos(width); // exclude the quad area on the right
222   return (left_bound + abs_position * (right_bound - left_bound)) * width;
223 }
224 
225 
get_slider_line_offset(float pos,float scale,float x,float y,float ht,const int width)226 static float get_slider_line_offset(float pos, float scale, float x, float y, float ht, const int width)
227 {
228   // ht is in [0,1] scale here
229   const float l = 0.0f;
230   const float r = slider_right_pos(width);
231 
232   float offset = 0.0f;
233   // handle linear startup and rescale y to fit the whole range again
234   if(y < ht)
235   {
236     offset = (x - l) / (r - l) - pos;
237   }
238   else
239   {
240     y -= ht;
241     y /= (1.0f - ht);
242 
243     offset = (x - y * y * .5f - (1.0f - y * y) * (l + pos * (r - l)))
244              / (.5f * y * y / scale + (1.0f - y * y) * (r - l));
245   }
246   // clamp to result in a [0,1] range:
247   if(pos + offset > 1.0f) offset = 1.0f - pos;
248   if(pos + offset < 0.0f) offset = -pos;
249   return offset;
250 }
251 
252 // draw a loupe guideline for the quadratic zoom in in the slider interface:
draw_slider_line(cairo_t * cr,float pos,float off,float scale,const int width,const int height,const int ht)253 static void draw_slider_line(cairo_t *cr, float pos, float off, float scale, const int width,
254                              const int height, const int ht)
255 {
256   // pos is normalized position [0,1], offset is on that scale.
257   // ht is in pixels here
258   const float l = 0.0f;
259   const float r = slider_right_pos(width);
260 
261   const int steps = 64;
262   cairo_move_to(cr, width * (l + (pos + off) * (r - l)), ht * .7f);
263   cairo_line_to(cr, width * (l + (pos + off) * (r - l)), ht);
264   for(int j = 1; j < steps; j++)
265   {
266     const float y = j / (steps - 1.0f);
267     const float x = y * y * .5f * (1.f + off / scale) + (1.0f - y * y) * (l + (pos + off) * (r - l));
268     cairo_line_to(cr, x * width, ht + y * (height - ht));
269   }
270 }
271 // -------------------------------
272 
273 // handlers on the popup window, to close popup:
dt_bauhaus_window_motion_notify(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)274 static gboolean dt_bauhaus_window_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
275 {
276   const float tol = 50;
277   gint wx, wy;
278   GtkAllocation allocation;
279   gtk_widget_get_allocation(widget, &allocation);
280   gdk_window_get_origin(gtk_widget_get_window(widget), &wx, &wy);
281 
282   if(event->x_root > wx + allocation.width + tol || event->y_root > wy + inner_height(allocation) + tol
283      || event->x_root < (int)wx - tol || event->y_root < (int)wy - tol)
284   {
285     dt_bauhaus_widget_reject(darktable.bauhaus->current);
286     gtk_widget_set_state_flags(GTK_WIDGET(darktable.bauhaus->current), GTK_STATE_FLAG_NORMAL, TRUE);
287     dt_bauhaus_hide_popup();
288     return TRUE;
289   }
290   // make sure to propagate the event further
291   return FALSE;
292 }
293 
dt_bauhaus_window_button_press(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)294 static gboolean dt_bauhaus_window_button_press(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
295 {
296   const float tol = 0;
297   gint wx, wy;
298   GtkAllocation allocation;
299   gtk_widget_get_allocation(widget, &allocation);
300   gdk_window_get_origin(gtk_widget_get_window(widget), &wx, &wy);
301 
302   if((event->x_root > wx + allocation.width + tol || event->y_root > wy + inner_height(allocation) + tol
303       || event->x_root < (int)wx - tol || event->y_root < (int)wy - tol))
304   {
305     dt_bauhaus_widget_reject(darktable.bauhaus->current);
306     gtk_widget_set_state_flags(GTK_WIDGET(darktable.bauhaus->current), GTK_STATE_FLAG_NORMAL, FALSE);
307     dt_bauhaus_hide_popup();
308     return TRUE;
309   }
310   // make sure to propagate the event further
311   return FALSE;
312 }
313 
combobox_popup_scroll(int amt)314 static void combobox_popup_scroll(int amt)
315 {
316   gint wx = 0, wy = 0;
317   GtkWidget *w = GTK_WIDGET(darktable.bauhaus->current);
318   GtkAllocation allocation_w;
319   gtk_widget_get_allocation(w, &allocation_w);
320   const int ht = allocation_w.height;
321   const int skip = darktable.bauhaus->line_height;
322   gdk_window_get_origin(gtk_widget_get_window(w), &wx, &wy);
323   const dt_bauhaus_combobox_data_t *d = &darktable.bauhaus->current->data.combobox;
324   int new_value = CLAMP(d->active + amt, 0, d->num_labels - 1);
325 
326   // skip insensitive ones
327   if(!_combobox_next_entry(d->entries, &new_value, amt))
328     return;
329 
330   // we move the popup up or down
331   if(new_value == d->active)
332   {
333     gdk_window_move(gtk_widget_get_window(darktable.bauhaus->popup_window), wx, wy - d->active * skip);
334   }
335   else
336   {
337     gint px, py;
338     gdk_window_get_origin(gtk_widget_get_window(darktable.bauhaus->popup_window), &px, &py);
339     gdk_window_move(gtk_widget_get_window(darktable.bauhaus->popup_window),
340                     wx, py - skip * (new_value - d->active));
341   }
342 
343   // make sure highlighted entry is updated:
344   darktable.bauhaus->mouse_x = 0;
345   darktable.bauhaus->mouse_y = new_value * skip + ht / 2;
346   gtk_widget_queue_draw(darktable.bauhaus->popup_area);
347 
348   // and we change the value
349   _bauhaus_combobox_set(w, new_value, d->mute_scrolling);
350 }
351 
352 
dt_bauhaus_popup_scroll(GtkWidget * widget,GdkEventScroll * event,gpointer user_data)353 static gboolean dt_bauhaus_popup_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
354 {
355   int delta_y = 0;
356   switch(darktable.bauhaus->current->type)
357   {
358     case DT_BAUHAUS_COMBOBOX:
359       if(dt_gui_get_scroll_unit_deltas(event, NULL, &delta_y))
360          combobox_popup_scroll(delta_y);
361       break;
362     case DT_BAUHAUS_SLIDER:
363       break;
364     default:
365       break;
366   }
367   return TRUE;
368 }
369 
dt_bauhaus_popup_motion_notify(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)370 static gboolean dt_bauhaus_popup_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
371 {
372   GtkAllocation allocation_popup_window;
373   gtk_widget_get_allocation(darktable.bauhaus->popup_window, &allocation_popup_window);
374   gtk_widget_queue_draw(darktable.bauhaus->popup_area);
375   dt_bauhaus_widget_t *w = darktable.bauhaus->current;
376   GtkAllocation allocation_w;
377   gtk_widget_get_allocation(GTK_WIDGET(w), &allocation_w);
378   const int width = allocation_popup_window.width, height = inner_height(allocation_popup_window);
379   // coordinate transform is in vain because we're only ever called after a button release.
380   // that means the system is always the one of the popup.
381   // that also means that we can't have hovering combobox entries while still holding the button. :(
382   const float ex = event->x;
383   const float ey = event->y;
384   GtkAllocation allocation;
385   gtk_widget_get_allocation(widget, &allocation);
386 
387   gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_PRELIGHT, TRUE);
388 
389   if(darktable.bauhaus->keys_cnt == 0) _stop_cursor();
390 
391   switch(w->type)
392   {
393     case DT_BAUHAUS_COMBOBOX:
394       darktable.bauhaus->mouse_x = ex;
395       darktable.bauhaus->mouse_y = ey;
396       break;
397     case DT_BAUHAUS_SLIDER:
398     {
399       const dt_bauhaus_slider_data_t *d = &w->data.slider;
400       const float mouse_off = get_slider_line_offset(d->oldpos, d->scale, ex / width, ey / height,
401                                                      allocation_w.height / (float)height, allocation.width);
402       if(!darktable.bauhaus->change_active)
403       {
404         if((darktable.bauhaus->mouse_line_distance < 0 && mouse_off >= 0)
405            || (darktable.bauhaus->mouse_line_distance > 0 && mouse_off <= 0))
406           darktable.bauhaus->change_active = 1;
407         darktable.bauhaus->mouse_line_distance = mouse_off;
408       }
409       if(darktable.bauhaus->change_active)
410       {
411         // remember mouse position for motion effects in draw
412         darktable.bauhaus->mouse_x = ex;
413         darktable.bauhaus->mouse_y = ey;
414         dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off);
415       }
416     }
417     break;
418     default:
419       break;
420   }
421   return TRUE;
422 }
423 
dt_bauhaus_popup_leave_notify(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)424 static gboolean dt_bauhaus_popup_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
425 {
426   gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_NORMAL, TRUE);
427   return TRUE;
428 }
429 
dt_bauhaus_popup_button_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)430 static gboolean dt_bauhaus_popup_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
431 {
432   if(darktable.bauhaus->current && (darktable.bauhaus->current->type == DT_BAUHAUS_COMBOBOX)
433      && (event->button == 1) &&                                // only accept left mouse click
434      (dt_get_wtime() - darktable.bauhaus->opentime >= 0.250f)) // default gtk timeout for double-clicks
435   {
436     gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_ACTIVE, TRUE);
437 
438     // event might be in wrong system, transform ourselves:
439     gint wx, wy, x, y;
440     gdk_window_get_origin(gtk_widget_get_window(darktable.bauhaus->popup_window), &wx, &wy);
441 
442     gdk_device_get_position(
443 #if GTK_CHECK_VERSION(3, 20, 0)
444         gdk_seat_get_pointer(gdk_display_get_default_seat(gtk_widget_get_display(widget))), 0, &x, &y);
445 #else
446         gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(gdk_display_get_default())), NULL, &x,
447         &y);
448 #endif
449     darktable.bauhaus->end_mouse_x = x - wx;
450     darktable.bauhaus->end_mouse_y = y - wy;
451     const dt_bauhaus_combobox_data_t *d = &darktable.bauhaus->current->data.combobox;
452     if(!d->mute_scrolling)
453       dt_bauhaus_widget_accept(darktable.bauhaus->current);
454     dt_bauhaus_hide_popup();
455   }
456   else if(darktable.bauhaus->hiding)
457   {
458     dt_bauhaus_hide_popup();
459   }
460   return TRUE;
461 }
462 
dt_bauhaus_popup_button_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)463 static gboolean dt_bauhaus_popup_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
464 {
465   if(event->button == 1)
466   {
467     if(darktable.bauhaus->current->type == DT_BAUHAUS_COMBOBOX
468        && dt_get_wtime() - darktable.bauhaus->opentime < 0.250f) // default gtk timeout for double-clicks
469     {
470       // counts as double click, reset:
471       const dt_bauhaus_combobox_data_t *d = &darktable.bauhaus->current->data.combobox;
472       dt_bauhaus_combobox_set(GTK_WIDGET(darktable.bauhaus->current), d->defpos);
473       dt_bauhaus_widget_reject(darktable.bauhaus->current);
474       gtk_widget_set_state_flags(GTK_WIDGET(darktable.bauhaus->current),
475                                  GTK_STATE_FLAG_FOCUSED, FALSE);
476     }
477     else
478     {
479       // only accept left mouse click
480       darktable.bauhaus->end_mouse_x = event->x;
481       darktable.bauhaus->end_mouse_y = event->y;
482       dt_bauhaus_widget_accept(darktable.bauhaus->current);
483       gtk_widget_set_state_flags(GTK_WIDGET(darktable.bauhaus->current),
484                                  GTK_STATE_FLAG_FOCUSED, FALSE);
485     }
486   }
487   else
488   {
489     dt_bauhaus_widget_reject(darktable.bauhaus->current);
490   }
491   darktable.bauhaus->hiding = TRUE;
492   return TRUE;
493 }
494 
dt_bauhaus_window_show(GtkWidget * w,gpointer user_data)495 static void dt_bauhaus_window_show(GtkWidget *w, gpointer user_data)
496 {
497   // Could grab the popup_area rather than popup_window, but if so
498   // then popup_area would get all motion events including those
499   // outside of the popup. This way the popup_area gets motion events
500   // related to updating the popup, and popup_window gets all others
501   // which would be the ones telling it to close the popup.
502   gtk_grab_add(w);
503 }
504 
dt_bh_init(DtBauhausWidget * class)505 static void dt_bh_init(DtBauhausWidget *class)
506 {
507   // not sure if we want to use this instead of our code in *_new()
508   // TODO: the common code from bauhaus_widget_init() could go here.
509 }
510 
dt_bh_class_init(DtBauhausWidgetClass * class)511 static void dt_bh_class_init(DtBauhausWidgetClass *class)
512 {
513   darktable.bauhaus->signals[DT_BAUHAUS_VALUE_CHANGED_SIGNAL]
514       = g_signal_new("value-changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
515                      g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
516   darktable.bauhaus->signals[DT_BAUHAUS_QUAD_PRESSED_SIGNAL]
517       = g_signal_new("quad-pressed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
518                      g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
519 
520   // TODO: could init callbacks once per class for more efficiency:
521   // GtkWidgetClass *widget_class;
522   // widget_class = GTK_WIDGET_CLASS (class);
523   // widget_class->draw = dt_bauhaus_draw;
524 }
525 
dt_bauhaus_load_theme()526 void dt_bauhaus_load_theme()
527 {
528   darktable.bauhaus->line_space = 1.5;
529   darktable.bauhaus->line_height = 9;
530   darktable.bauhaus->marker_size = 0.25f;
531 
532   GtkWidget *root_window = dt_ui_main_window(darktable.gui->ui);
533   GtkStyleContext *ctx = gtk_style_context_new();
534   GtkWidgetPath *path = gtk_widget_path_new();
535   const int pos = gtk_widget_path_append_type(path, GTK_TYPE_WIDGET);
536   gtk_widget_path_iter_set_name(path, pos, "iop-plugin-ui");
537   gtk_style_context_set_path(ctx, path);
538   gtk_style_context_set_screen (ctx, gtk_widget_get_screen(root_window));
539 
540   gtk_style_context_lookup_color(ctx, "bauhaus_fg", &darktable.bauhaus->color_fg);
541   gtk_style_context_lookup_color(ctx, "bauhaus_fg_insensitive", &darktable.bauhaus->color_fg_insensitive);
542   gtk_style_context_lookup_color(ctx, "bauhaus_bg", &darktable.bauhaus->color_bg);
543   gtk_style_context_lookup_color(ctx, "bauhaus_border", &darktable.bauhaus->color_border);
544   gtk_style_context_lookup_color(ctx, "bauhaus_fill", &darktable.bauhaus->color_fill);
545   gtk_style_context_lookup_color(ctx, "bauhaus_indicator_border", &darktable.bauhaus->indicator_border);
546 
547   gtk_style_context_lookup_color(ctx, "graph_bg", &darktable.bauhaus->graph_bg);
548   gtk_style_context_lookup_color(ctx, "graph_exterior", &darktable.bauhaus->graph_exterior);
549   gtk_style_context_lookup_color(ctx, "graph_border", &darktable.bauhaus->graph_border);
550   gtk_style_context_lookup_color(ctx, "graph_grid", &darktable.bauhaus->graph_grid);
551   gtk_style_context_lookup_color(ctx, "graph_fg", &darktable.bauhaus->graph_fg);
552   gtk_style_context_lookup_color(ctx, "graph_fg_active", &darktable.bauhaus->graph_fg_active);
553   gtk_style_context_lookup_color(ctx, "graph_overlay", &darktable.bauhaus->graph_overlay);
554   gtk_style_context_lookup_color(ctx, "inset_histogram", &darktable.bauhaus->inset_histogram);
555   gtk_style_context_lookup_color(ctx, "graph_red", &darktable.bauhaus->graph_colors[0]);
556   gtk_style_context_lookup_color(ctx, "graph_green", &darktable.bauhaus->graph_colors[1]);
557   gtk_style_context_lookup_color(ctx, "graph_blue", &darktable.bauhaus->graph_colors[2]);
558   gtk_style_context_lookup_color(ctx, "colorlabel_red",
559                                  &darktable.bauhaus->colorlabels[DT_COLORLABELS_RED]);
560   gtk_style_context_lookup_color(ctx, "colorlabel_yellow",
561                                  &darktable.bauhaus->colorlabels[DT_COLORLABELS_YELLOW]);
562   gtk_style_context_lookup_color(ctx, "colorlabel_green",
563                                  &darktable.bauhaus->colorlabels[DT_COLORLABELS_GREEN]);
564   gtk_style_context_lookup_color(ctx, "colorlabel_blue",
565                                  &darktable.bauhaus->colorlabels[DT_COLORLABELS_BLUE]);
566   gtk_style_context_lookup_color(ctx, "colorlabel_purple",
567                                  &darktable.bauhaus->colorlabels[DT_COLORLABELS_PURPLE]);
568 
569   PangoFontDescription *pfont = 0;
570   gtk_style_context_get(ctx, GTK_STATE_FLAG_NORMAL, "font", &pfont, NULL);
571 
572   // make sure we release previously loaded font
573   if(darktable.bauhaus->pango_font_desc)
574     pango_font_description_free(darktable.bauhaus->pango_font_desc);
575 
576   darktable.bauhaus->pango_font_desc = pfont;
577 
578   if(darktable.bauhaus->pango_sec_font_desc)
579     pango_font_description_free(darktable.bauhaus->pango_sec_font_desc);
580 
581   // now get the font for the section labels
582   gtk_widget_path_iter_set_name(path, pos, "section_label");
583   gtk_style_context_set_path(ctx, path);
584   gtk_style_context_get(ctx, GTK_STATE_FLAG_NORMAL, "font", &pfont, NULL);
585   darktable.bauhaus->pango_sec_font_desc = pfont;
586 
587   gtk_widget_path_free(path);
588 
589   cairo_surface_t *cst = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 128, 128);
590   cairo_t *cr = cairo_create(cst);
591   PangoLayout *layout = pango_cairo_create_layout(cr);
592   pango_layout_set_text(layout, "m", -1);
593   pango_layout_set_font_description(layout, darktable.bauhaus->pango_font_desc);
594   pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
595   int pango_width, pango_height;
596   pango_layout_get_size(layout, &pango_width, &pango_height);
597   g_object_unref(layout);
598   cairo_destroy(cr);
599   cairo_surface_destroy(cst);
600 
601   darktable.bauhaus->scale = 1.33f;
602   darktable.bauhaus->line_height = pango_height / PANGO_SCALE;
603   darktable.bauhaus->widget_space = INNER_PADDING / 4.0f; // used as a top/bottom margin for widgets
604   darktable.bauhaus->quad_width = darktable.bauhaus->line_height;
605 
606   darktable.bauhaus->baseline_size = darktable.bauhaus->line_height / 2.5f; // absolute size in Cairo unit
607   darktable.bauhaus->border_width = 2.0f; // absolute size in Cairo unit
608   darktable.bauhaus->marker_size = (darktable.bauhaus->baseline_size + darktable.bauhaus->border_width) * 0.9f;
609 }
610 
dt_bauhaus_init()611 void dt_bauhaus_init()
612 {
613   darktable.bauhaus = (dt_bauhaus_t *)calloc(1, sizeof(dt_bauhaus_t));
614   darktable.bauhaus->keys_cnt = 0;
615   darktable.bauhaus->current = NULL;
616   darktable.bauhaus->popup_area = gtk_drawing_area_new();
617   gtk_widget_set_name(darktable.bauhaus->popup_area, "bauhaus-popup");
618   darktable.bauhaus->pango_font_desc = NULL;
619 
620   dt_bauhaus_load_theme();
621 
622   // keys are freed with g_free, values are ptrs to the widgets, these don't need to be cleaned up.
623   darktable.bauhaus->keymap = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
624   darktable.bauhaus->key_mod = NULL;
625   darktable.bauhaus->key_val = NULL;
626   memset(darktable.bauhaus->key_history, 0, sizeof(darktable.bauhaus->key_history));
627 
628   darktable.bauhaus->skip_accel = 1;
629 
630   // this easily gets keyboard input:
631   // darktable.bauhaus->popup_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
632   // but this doesn't flicker, and the above hack with key input seems to work well.
633   darktable.bauhaus->popup_window = gtk_window_new(GTK_WINDOW_POPUP);
634 #ifdef GDK_WINDOWING_QUARTZ
635   dt_osx_disallow_fullscreen(darktable.bauhaus->popup_window);
636 #endif
637   // this is needed for popup, not for toplevel.
638   // since popup_area gets the focus if we show the window, this is all
639   // we need.
640   dt_gui_key_accel_block_on_focus_connect(darktable.bauhaus->popup_area);
641 
642   gtk_widget_set_size_request(darktable.bauhaus->popup_area, DT_PIXEL_APPLY_DPI(300), DT_PIXEL_APPLY_DPI(300));
643   gtk_window_set_resizable(GTK_WINDOW(darktable.bauhaus->popup_window), FALSE);
644   gtk_window_set_default_size(GTK_WINDOW(darktable.bauhaus->popup_window), 260, 260);
645   // gtk_window_set_modal(GTK_WINDOW(c->popup_window), TRUE);
646   // gtk_window_set_decorated(GTK_WINDOW(c->popup_window), FALSE);
647 
648   // for pie menu:
649   // gtk_window_set_position(GTK_WINDOW(c->popup_window), GTK_WIN_POS_MOUSE);// | GTK_WIN_POS_CENTER);
650 
651   // needed on macOS to avoid fullscreening the popup with newer GTK
652   gtk_window_set_type_hint(GTK_WINDOW(darktable.bauhaus->popup_window), GDK_WINDOW_TYPE_HINT_POPUP_MENU);
653 
654   gtk_container_add(GTK_CONTAINER(darktable.bauhaus->popup_window), darktable.bauhaus->popup_area);
655   // gtk_window_set_title(GTK_WINDOW(c->popup_window), _("dtgtk control popup"));
656   gtk_window_set_keep_above(GTK_WINDOW(darktable.bauhaus->popup_window), TRUE);
657   gtk_window_set_gravity(GTK_WINDOW(darktable.bauhaus->popup_window), GDK_GRAVITY_STATIC);
658 
659   gtk_widget_set_can_focus(darktable.bauhaus->popup_area, TRUE);
660   gtk_widget_add_events(darktable.bauhaus->popup_area, GDK_POINTER_MOTION_MASK
661                                                        | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
662                                                        | GDK_KEY_PRESS_MASK | GDK_LEAVE_NOTIFY_MASK
663                                                        | darktable.gui->scroll_mask);
664 
665   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_window), "show", G_CALLBACK(dt_bauhaus_window_show), (gpointer)NULL);
666   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_area), "draw", G_CALLBACK(dt_bauhaus_popup_draw),
667                    (gpointer)NULL);
668   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_window), "motion-notify-event",
669                    G_CALLBACK(dt_bauhaus_window_motion_notify), (gpointer)NULL);
670   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_window), "button-press-event", G_CALLBACK(dt_bauhaus_window_button_press),
671                    (gpointer)NULL);
672   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_area), "motion-notify-event",
673                    G_CALLBACK(dt_bauhaus_popup_motion_notify), (gpointer)NULL);
674   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_area), "leave-notify-event",
675                    G_CALLBACK(dt_bauhaus_popup_leave_notify), (gpointer)NULL);
676   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_area), "button-press-event",
677                    G_CALLBACK(dt_bauhaus_popup_button_press), (gpointer)NULL);
678   // this is connected to the widget itself, not the popup. we're only interested
679   // in mouse release events that are initiated by a press on the original widget.
680   g_signal_connect (G_OBJECT (darktable.bauhaus->popup_area), "button-release-event",
681                     G_CALLBACK (dt_bauhaus_popup_button_release), (gpointer)NULL);
682   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_area), "key-press-event",
683                    G_CALLBACK(dt_bauhaus_popup_key_press), (gpointer)NULL);
684   g_signal_connect(G_OBJECT(darktable.bauhaus->popup_area), "scroll-event",
685                    G_CALLBACK(dt_bauhaus_popup_scroll), (gpointer)NULL);
686 }
687 
dt_bauhaus_cleanup()688 void dt_bauhaus_cleanup()
689 {
690   // TODO: destroy popup window and resources
691   g_list_free_full(darktable.bauhaus->key_mod, (GDestroyNotify)g_free);
692   g_list_free_full(darktable.bauhaus->key_val, (GDestroyNotify)g_free);
693   if(darktable.bauhaus->keymap)
694   {
695     g_hash_table_destroy(darktable.bauhaus->keymap);
696     darktable.bauhaus->keymap = NULL;
697   }
698 }
699 
700 // fwd declare a few callbacks
701 static gboolean dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
702 static gboolean dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
703 static gboolean dt_bauhaus_slider_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data);
704 static gboolean dt_bauhaus_slider_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data);
705 static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data);
706 
707 
708 static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
709 static gboolean dt_bauhaus_combobox_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data);
710 static gboolean dt_bauhaus_combobox_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data);
711 
712 // static gboolean
713 // dt_bauhaus_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data);
714 static gboolean dt_bauhaus_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data);
715 
716 
717 // end static init/cleanup
718 // =================================================
719 
720 
721 
722 // common initialization
dt_bauhaus_widget_init(dt_bauhaus_widget_t * w,dt_iop_module_t * self)723 static void dt_bauhaus_widget_init(dt_bauhaus_widget_t *w, dt_iop_module_t *self)
724 {
725   w->module = self;
726 
727   w->section = NULL;
728 
729   // no quad icon and no toggle button:
730   w->quad_paint = 0;
731   w->quad_paint_data = NULL;
732   w->quad_toggle = 0;
733   w->combo_populate = NULL;
734 
735   switch(w->type)
736   {
737     case DT_BAUHAUS_SLIDER:
738     {
739       gtk_widget_set_name(GTK_WIDGET(w), "bauhaus-slider");
740       gtk_widget_set_size_request(GTK_WIDGET(w), -1, 2 * darktable.bauhaus->widget_space + INNER_PADDING + darktable.bauhaus->baseline_size + get_line_height() - darktable.bauhaus->border_width / 2.0f);
741       break;
742     }
743     case DT_BAUHAUS_COMBOBOX:
744     {
745       gtk_widget_set_name(GTK_WIDGET(w), "bauhaus-combobox");
746       gtk_widget_set_size_request(GTK_WIDGET(w), -1, 2 * darktable.bauhaus->widget_space + get_line_height());
747       break;
748     }
749   }
750 
751   gtk_widget_add_events(GTK_WIDGET(w), GDK_POINTER_MOTION_MASK
752                                        | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
753                                        | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
754                                        | GDK_FOCUS_CHANGE_MASK
755                                        | darktable.gui->scroll_mask);
756 
757   g_signal_connect(G_OBJECT(w), "draw", G_CALLBACK(dt_bauhaus_draw), NULL);
758 
759   // for combobox, where mouse-release triggers a selection, we need to catch this
760   // event where the mouse-press occurred, which will be this widget. we just pass
761   // it on though:
762   // g_signal_connect (G_OBJECT (w), "button-release-event",
763   //                   G_CALLBACK (dt_bauhaus_popup_button_release), (gpointer)NULL);
764 }
765 
dt_bauhaus_combobox_set_default(GtkWidget * widget,int def)766 void dt_bauhaus_combobox_set_default(GtkWidget *widget, int def)
767 {
768   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
769   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
770   d->defpos = def;
771 }
772 
dt_bauhaus_combobox_get_default(GtkWidget * widget)773 int dt_bauhaus_combobox_get_default(GtkWidget *widget)
774 {
775   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
776   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
777   return d->defpos;
778 }
779 
dt_bauhaus_slider_set_hard_min(GtkWidget * widget,float val)780 void dt_bauhaus_slider_set_hard_min(GtkWidget* widget, float val)
781 {
782   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
783   dt_bauhaus_slider_data_t *d = &w->data.slider;
784   float pos = dt_bauhaus_slider_get(widget);
785   d->hard_min = val;
786   d->min = MAX(d->min, d->hard_min);
787   d->soft_min = MAX(d->soft_min, d->hard_min);
788 
789   if(val > d->hard_max) dt_bauhaus_slider_set_hard_max(widget,val);
790   if(pos < val)
791   {
792     dt_bauhaus_slider_set_soft(widget,val);
793   }
794   else
795   {
796     dt_bauhaus_slider_set_soft(widget,pos);
797   }
798 }
799 
dt_bauhaus_slider_get_hard_min(GtkWidget * widget)800 float dt_bauhaus_slider_get_hard_min(GtkWidget* widget)
801 {
802   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
803   const dt_bauhaus_slider_data_t *d = &w->data.slider;
804   return d->hard_min;
805 }
806 
dt_bauhaus_slider_set_hard_max(GtkWidget * widget,float val)807 void dt_bauhaus_slider_set_hard_max(GtkWidget* widget, float val)
808 {
809   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
810   dt_bauhaus_slider_data_t *d = &w->data.slider;
811   float pos = dt_bauhaus_slider_get(widget);
812   d->hard_max = val;
813   d->max = MIN(d->max, d->hard_max);
814   d->soft_max = MIN(d->soft_max, d->hard_max);
815 
816   if(val < d->hard_min) dt_bauhaus_slider_set_hard_min(widget,val);
817   if(pos > val)
818   {
819     dt_bauhaus_slider_set_soft(widget,val);
820   }
821   else
822   {
823     dt_bauhaus_slider_set_soft(widget,pos);
824   }
825 }
826 
dt_bauhaus_slider_get_hard_max(GtkWidget * widget)827 float dt_bauhaus_slider_get_hard_max(GtkWidget* widget)
828 {
829   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
830   const dt_bauhaus_slider_data_t *d = &w->data.slider;
831   return d->hard_max;
832 }
833 
dt_bauhaus_slider_set_soft_min(GtkWidget * widget,float val)834 void dt_bauhaus_slider_set_soft_min(GtkWidget* widget, float val)
835 {
836   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
837   dt_bauhaus_slider_data_t *d = &w->data.slider;
838   float oldval = dt_bauhaus_slider_get(widget);
839   d->min = d->soft_min = CLAMP(val,d->hard_min,d->hard_max);
840   dt_bauhaus_slider_set_soft(widget,oldval);
841 }
842 
dt_bauhaus_slider_get_soft_min(GtkWidget * widget)843 float dt_bauhaus_slider_get_soft_min(GtkWidget* widget)
844 {
845   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
846   const dt_bauhaus_slider_data_t *d = &w->data.slider;
847   return d->soft_min;
848 }
849 
dt_bauhaus_slider_set_soft_max(GtkWidget * widget,float val)850 void dt_bauhaus_slider_set_soft_max(GtkWidget* widget, float val)
851 {
852   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
853   dt_bauhaus_slider_data_t *d = &w->data.slider;
854   float oldval = dt_bauhaus_slider_get(widget);
855   d->max = d->soft_max = CLAMP(val,d->hard_min,d->hard_max);
856   dt_bauhaus_slider_set_soft(widget,oldval);
857 }
858 
dt_bauhaus_slider_get_soft_max(GtkWidget * widget)859 float dt_bauhaus_slider_get_soft_max(GtkWidget* widget)
860 {
861   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
862   const dt_bauhaus_slider_data_t *d = &w->data.slider;
863   return d->soft_max;
864 }
865 
dt_bauhaus_slider_set_default(GtkWidget * widget,float def)866 void dt_bauhaus_slider_set_default(GtkWidget *widget, float def)
867 {
868   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
869   dt_bauhaus_slider_data_t *d = &w->data.slider;
870   d->defpos = def;
871 }
872 
dt_bauhaus_slider_set_soft_range(GtkWidget * widget,float soft_min,float soft_max)873 void dt_bauhaus_slider_set_soft_range(GtkWidget *widget, float soft_min, float soft_max)
874 {
875   dt_bauhaus_slider_set_soft_min(widget,soft_min);
876   dt_bauhaus_slider_set_soft_max(widget,soft_max);
877 }
878 
dt_bauhaus_slider_get_default(GtkWidget * widget)879 float dt_bauhaus_slider_get_default(GtkWidget *widget)
880 {
881   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
882   const dt_bauhaus_slider_data_t *d = &w->data.slider;
883   return d->defpos;
884 }
885 
dt_bauhaus_slider_enable_soft_boundaries(GtkWidget * widget,float hard_min,float hard_max)886 void dt_bauhaus_slider_enable_soft_boundaries(GtkWidget *widget, float hard_min, float hard_max)
887 {
888   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
889   dt_bauhaus_slider_data_t *d = &w->data.slider;
890   d->hard_min = hard_min;
891   d->hard_max = hard_max;
892 }
893 
dt_bauhaus_widget_set_label(GtkWidget * widget,const char * section_orig,const char * label_orig)894 void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *section_orig, const char *label_orig)
895 {
896   const char *section = section_orig ? _(section_orig) : NULL;
897   const char *label = _(label_orig);
898 
899   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
900   memset(w->label, 0, sizeof(w->label)); // keep valgrind happy
901   g_strlcpy(w->label, label, sizeof(w->label));
902   if(section) w->section = g_strdup(section);
903 
904   if(w->module)
905   {
906     if(!darktable.bauhaus->skip_accel)
907     {
908       gchar *combined_label = section_orig
909                             ? g_strdup_printf("%s`%s", section_orig, label_orig)
910                             : g_strdup(label_orig);
911       if(darktable.control->accel_initialising)
912       {
913         if(w->type == DT_BAUHAUS_SLIDER)
914         {
915           dt_accel_register_slider_iop(w->module->so, FALSE, combined_label);
916         }
917         else if(w->type == DT_BAUHAUS_COMBOBOX)
918         {
919           dt_accel_register_combobox_iop(w->module->so, FALSE, combined_label);
920         }
921       }
922       else
923       {
924         if(w->type == DT_BAUHAUS_SLIDER)
925         {
926           dt_accel_connect_slider_iop(w->module, combined_label, widget);
927         }
928         else if(w->type == DT_BAUHAUS_COMBOBOX)
929         {
930           dt_accel_connect_combobox_iop(w->module, combined_label, widget);
931         }
932       }
933 
934       g_free(combined_label);
935     }
936 
937     // construct control path name and insert into keymap:
938     gchar *path;
939     if(section && section[0] != '\0')
940     {
941       path = g_strdup_printf("%s.%s.%s", w->module->name(), section, w->label);
942       gchar *section_path = g_strdup_printf("%s.%s", w->module->name(), section);
943       if(!g_list_find_custom(darktable.bauhaus->key_val, section_path, (GCompareFunc)strcmp))
944         darktable.bauhaus->key_val
945             = g_list_insert_sorted(darktable.bauhaus->key_val, section_path, (GCompareFunc)strcmp);
946       else
947         g_free(section_path);
948     }
949     else
950       path = g_strdup_printf("%s.%s", w->module->name(), w->label);
951     if(!g_hash_table_lookup(darktable.bauhaus->keymap, path))
952     {
953       // also insert into sorted tab-complete list.
954       // (but only if this is the first time we insert this path)
955       gchar *mod = g_strdup(path);
956       gchar *val = g_strstr_len(mod, strlen(mod), ".");
957       if(val)
958       {
959         *val = 0;
960         if(!g_list_find_custom(darktable.bauhaus->key_mod, mod, (GCompareFunc)strcmp))
961           darktable.bauhaus->key_mod
962               = g_list_insert_sorted(darktable.bauhaus->key_mod, mod, (GCompareFunc)strcmp);
963         else
964           g_free(mod);
965 
966         // unfortunately need our own string, as replace in the hashtable below might destroy this pointer.
967         darktable.bauhaus->key_val
968             = g_list_insert_sorted(darktable.bauhaus->key_val, g_strdup(path), (GCompareFunc)strcmp);
969       }
970       else
971         g_free(mod);
972     }
973     // might free an old path
974     g_hash_table_replace(darktable.bauhaus->keymap, path, w);
975     gtk_widget_queue_draw(GTK_WIDGET(w));
976   }
977 }
978 
dt_bauhaus_widget_get_label(GtkWidget * widget)979 const char* dt_bauhaus_widget_get_label(GtkWidget *widget)
980 {
981   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
982   return w->label;
983 }
984 
dt_bauhaus_widget_set_quad_paint(GtkWidget * widget,dt_bauhaus_quad_paint_f f,int paint_flags,void * paint_data)985 void dt_bauhaus_widget_set_quad_paint(GtkWidget *widget, dt_bauhaus_quad_paint_f f, int paint_flags, void *paint_data)
986 {
987   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
988   w->quad_paint = f;
989   w->quad_paint_flags = paint_flags;
990   w->quad_paint_data = paint_data;
991 }
992 
993 // make this quad a toggle button:
dt_bauhaus_widget_set_quad_toggle(GtkWidget * widget,int toggle)994 void dt_bauhaus_widget_set_quad_toggle(GtkWidget *widget, int toggle)
995 {
996   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
997   w->quad_toggle = toggle;
998 }
999 
dt_bauhaus_widget_set_quad_active(GtkWidget * widget,int active)1000 void dt_bauhaus_widget_set_quad_active(GtkWidget *widget, int active)
1001 {
1002   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1003   if (active)
1004     w->quad_paint_flags |= CPF_ACTIVE;
1005   else
1006     w->quad_paint_flags &= ~CPF_ACTIVE;
1007   gtk_widget_queue_draw(GTK_WIDGET(w));
1008 }
1009 
dt_bauhaus_widget_get_quad_active(GtkWidget * widget)1010 int dt_bauhaus_widget_get_quad_active(GtkWidget *widget)
1011 {
1012   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1013   return (w->quad_paint_flags & CPF_ACTIVE) == CPF_ACTIVE;
1014 }
1015 
_default_linear_curve(GtkWidget * self,float value,dt_bauhaus_curve_t dir)1016 static float _default_linear_curve(GtkWidget *self, float value, dt_bauhaus_curve_t dir)
1017 {
1018   // regardless of dir: input <-> output
1019   return value;
1020 }
1021 
_reverse_linear_curve(GtkWidget * self,float value,dt_bauhaus_curve_t dir)1022 static float _reverse_linear_curve(GtkWidget *self, float value, dt_bauhaus_curve_t dir)
1023 {
1024   // regardless of dir: input <-> output
1025   return 1.0 - value;
1026 }
1027 
dt_bauhaus_slider_destroy(dt_bauhaus_widget_t * widget,gpointer user_data)1028 static void dt_bauhaus_slider_destroy(dt_bauhaus_widget_t *widget, gpointer user_data)
1029 {
1030   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1031   if(w->type != DT_BAUHAUS_SLIDER) return;
1032   if(w->section) g_free(w->section);
1033   dt_bauhaus_slider_data_t *d = &w->data.slider;
1034   if(d->timeout_handle) g_source_remove(d->timeout_handle);
1035   d->timeout_handle = 0;
1036 }
1037 
dt_bauhaus_slider_new(dt_iop_module_t * self)1038 GtkWidget *dt_bauhaus_slider_new(dt_iop_module_t *self)
1039 {
1040   return dt_bauhaus_slider_new_with_range(self, 0.0, 1.0, 0.1, 0.5, 3);
1041 }
1042 
dt_bauhaus_slider_new_with_range(dt_iop_module_t * self,float min,float max,float step,float defval,int digits)1043 GtkWidget *dt_bauhaus_slider_new_with_range(dt_iop_module_t *self, float min, float max, float step,
1044                                             float defval, int digits)
1045 {
1046   return dt_bauhaus_slider_new_with_range_and_feedback(self, min, max, step, defval, digits, 1);
1047 }
1048 
dt_bauhaus_slider_new_with_range_and_feedback(dt_iop_module_t * self,float min,float max,float step,float defval,int digits,int feedback)1049 GtkWidget *dt_bauhaus_slider_new_with_range_and_feedback(dt_iop_module_t *self, float min, float max,
1050                                                          float step, float defval, int digits, int feedback)
1051 {
1052   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
1053   return dt_bauhaus_slider_from_widget(w,self, min, max, step, defval, digits, feedback);
1054 }
1055 
dt_bauhaus_slider_from_widget(dt_bauhaus_widget_t * w,dt_iop_module_t * self,float min,float max,float step,float defval,int digits,int feedback)1056 GtkWidget *dt_bauhaus_slider_from_widget(dt_bauhaus_widget_t* w,dt_iop_module_t *self, float min, float max,
1057                                                          float step, float defval, int digits, int feedback)
1058 {
1059   w->type = DT_BAUHAUS_SLIDER;
1060   dt_bauhaus_widget_init(w, self);
1061   dt_bauhaus_slider_data_t *d = &w->data.slider;
1062   d->min = d->soft_min = d->hard_min = min;
1063   d->max = d->soft_max = d->hard_max = max;
1064   d->step = step;
1065   // normalize default:
1066   d->defpos = defval;
1067   d->pos = (defval - min) / (max - min);
1068   d->oldpos = d->pos;
1069   d->scale = 5.0f * step / (max - min);
1070   d->digits = digits;
1071   snprintf(d->format, sizeof(d->format), "%%.0%df", digits);
1072   d->factor = 1.0f;
1073   d->offset = 0.0f;
1074 
1075   d->grad_cnt = 0;
1076 
1077   d->fill_feedback = feedback;
1078 
1079   d->is_dragging = 0;
1080   d->is_changed = 0;
1081   d->timeout_handle = 0;
1082   d->curve = _default_linear_curve;
1083 
1084   gtk_widget_add_events(GTK_WIDGET(w), GDK_KEY_PRESS_MASK);
1085   gtk_widget_set_can_focus(GTK_WIDGET(w), TRUE);
1086 
1087   g_signal_connect(G_OBJECT(w), "button-press-event", G_CALLBACK(dt_bauhaus_slider_button_press),
1088                    (gpointer)NULL);
1089   g_signal_connect(G_OBJECT(w), "button-release-event", G_CALLBACK(dt_bauhaus_slider_button_release),
1090                    (gpointer)NULL);
1091   g_signal_connect(G_OBJECT(w), "scroll-event", G_CALLBACK(dt_bauhaus_slider_scroll), (gpointer)NULL);
1092   g_signal_connect(G_OBJECT(w), "key-press-event", G_CALLBACK(dt_bauhaus_slider_key_press), (gpointer)NULL);
1093   g_signal_connect(G_OBJECT(w), "motion-notify-event", G_CALLBACK(dt_bauhaus_slider_motion_notify),
1094                    (gpointer)NULL);
1095   g_signal_connect(G_OBJECT(w), "destroy", G_CALLBACK(dt_bauhaus_slider_destroy), (gpointer)NULL);
1096   return GTK_WIDGET(w);
1097 }
1098 
dt_bauhaus_combobox_destroy(dt_bauhaus_widget_t * widget,gpointer user_data)1099 static void dt_bauhaus_combobox_destroy(dt_bauhaus_widget_t *widget, gpointer user_data)
1100 {
1101   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1102   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1103   if(w->section) g_free(w->section);
1104   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1105   g_list_free_full(d->entries, free_combobox_entry);
1106   d->entries = NULL;
1107   d->num_labels = 0;
1108   d->active = -1;
1109 }
1110 
dt_bauhaus_combobox_new(dt_iop_module_t * self)1111 GtkWidget *dt_bauhaus_combobox_new(dt_iop_module_t *self)
1112 {
1113   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(g_object_new(DT_BAUHAUS_WIDGET_TYPE, NULL));
1114   dt_bauhaus_combobox_from_widget(w,self);
1115   return GTK_WIDGET(w);
1116 }
1117 
dt_bauhaus_combobox_from_widget(dt_bauhaus_widget_t * w,dt_iop_module_t * self)1118 void dt_bauhaus_combobox_from_widget(dt_bauhaus_widget_t* w,dt_iop_module_t *self)
1119 {
1120   w->type = DT_BAUHAUS_COMBOBOX;
1121   dt_bauhaus_widget_init(w, self);
1122   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1123   d->entries = NULL;
1124   d->num_labels = 0;
1125   d->defpos = 0;
1126   d->active = -1;
1127   d->editable = 0;
1128   d->scale = 1;
1129   d->text_align = DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT;
1130   d->entries_ellipsis = PANGO_ELLIPSIZE_END;
1131   d->mute_scrolling = FALSE;
1132   memset(d->text, 0, sizeof(d->text));
1133 
1134   gtk_widget_add_events(GTK_WIDGET(w), GDK_KEY_PRESS_MASK);
1135   gtk_widget_set_can_focus(GTK_WIDGET(w), TRUE);
1136 
1137   g_signal_connect(G_OBJECT(w), "button-press-event", G_CALLBACK(dt_bauhaus_combobox_button_press),
1138                    (gpointer)NULL);
1139   g_signal_connect(G_OBJECT(w), "button-release-event", G_CALLBACK(dt_bauhaus_popup_button_release),
1140                    (gpointer)NULL);
1141   g_signal_connect(G_OBJECT(w), "scroll-event", G_CALLBACK(dt_bauhaus_combobox_scroll), (gpointer)NULL);
1142   g_signal_connect(G_OBJECT(w), "key-press-event", G_CALLBACK(dt_bauhaus_combobox_key_press), (gpointer)NULL);
1143   g_signal_connect(G_OBJECT(w), "destroy", G_CALLBACK(dt_bauhaus_combobox_destroy), (gpointer)NULL);
1144 }
1145 
dt_bauhaus_combobox_add_populate_fct(GtkWidget * widget,void (* fct)(GtkWidget * w,struct dt_iop_module_t ** module))1146 void dt_bauhaus_combobox_add_populate_fct(GtkWidget *widget, void (*fct)(GtkWidget *w, struct dt_iop_module_t **module))
1147 {
1148   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1149   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1150   w->combo_populate = fct;
1151 }
1152 
dt_bauhaus_combobox_add(GtkWidget * widget,const char * text)1153 void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
1154 {
1155   dt_bauhaus_combobox_add_full(widget, text, DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT, NULL, NULL, TRUE);
1156 }
1157 
dt_bauhaus_combobox_add_section(GtkWidget * widget,const char * text)1158 void dt_bauhaus_combobox_add_section(GtkWidget *widget, const char *text)
1159 {
1160   dt_bauhaus_combobox_add_full(widget, text, DT_BAUHAUS_COMBOBOX_ALIGN_LEFT, NULL, NULL, FALSE);
1161 }
1162 
dt_bauhaus_combobox_add_aligned(GtkWidget * widget,const char * text,dt_bauhaus_combobox_alignment_t align)1163 void dt_bauhaus_combobox_add_aligned(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align)
1164 {
1165   dt_bauhaus_combobox_add_full(widget, text, align, NULL, NULL, TRUE);
1166 }
1167 
dt_bauhaus_combobox_add_full(GtkWidget * widget,const char * text,dt_bauhaus_combobox_alignment_t align,gpointer data,void (free_func)(void * data),gboolean sensitive)1168 void dt_bauhaus_combobox_add_full(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align,
1169                                   gpointer data, void (free_func)(void *data), gboolean sensitive)
1170 {
1171   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1172   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1173   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1174   d->num_labels++;
1175   dt_bauhaus_combobox_entry_t *entry = new_combobox_entry(text, align, sensitive, data, free_func);
1176   d->entries = g_list_append(d->entries, entry);
1177   if(d->active < 0) d->active = 0;
1178 }
1179 
dt_bauhaus_combobox_set_entries_ellipsis(GtkWidget * widget,PangoEllipsizeMode ellipis)1180 void dt_bauhaus_combobox_set_entries_ellipsis(GtkWidget *widget, PangoEllipsizeMode ellipis)
1181 {
1182   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1183   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1184   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1185   d->entries_ellipsis = ellipis;
1186 }
1187 
dt_bauhaus_combobox_get_entries_ellipsis(GtkWidget * widget)1188 PangoEllipsizeMode dt_bauhaus_combobox_get_entries_ellipsis(GtkWidget *widget)
1189 {
1190   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1191   if(w->type != DT_BAUHAUS_COMBOBOX) return PANGO_ELLIPSIZE_END;
1192   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1193   return d->entries_ellipsis;
1194 }
1195 
dt_bauhaus_combobox_set_editable(GtkWidget * widget,int editable)1196 void dt_bauhaus_combobox_set_editable(GtkWidget *widget, int editable)
1197 {
1198   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1199   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1200   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1201   d->editable = editable ? 1 : 0;
1202 }
1203 
dt_bauhaus_combobox_get_editable(GtkWidget * widget)1204 int dt_bauhaus_combobox_get_editable(GtkWidget *widget)
1205 {
1206   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1207   if(w->type != DT_BAUHAUS_COMBOBOX) return 0;
1208   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1209   return d->editable;
1210 }
1211 
dt_bauhaus_combobox_set_popup_scale(GtkWidget * widget,const int scale)1212 void dt_bauhaus_combobox_set_popup_scale(GtkWidget *widget, const int scale)
1213 {
1214   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1215   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1216   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1217   d->scale = scale;
1218 }
1219 
dt_bauhaus_combobox_set_selected_text_align(GtkWidget * widget,const dt_bauhaus_combobox_alignment_t text_align)1220 void dt_bauhaus_combobox_set_selected_text_align(GtkWidget *widget, const dt_bauhaus_combobox_alignment_t text_align)
1221 {
1222   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1223   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1224   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1225   d->text_align = text_align;
1226 }
1227 
dt_bauhaus_combobox_remove_at(GtkWidget * widget,int pos)1228 void dt_bauhaus_combobox_remove_at(GtkWidget *widget, int pos)
1229 {
1230   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1231   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1232   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1233 
1234   if(pos < 0 || pos >= d->num_labels) return;
1235 
1236   // move active position up if removing anything before it
1237   // or when removing last position that is currently active.
1238   // this also sets active to -1 when removing the last remaining entry in a combobox.
1239   if(d->active > pos)
1240     d->active--;
1241   else if((d->active == pos) && (d->active >= d->num_labels-1))
1242     d->active = d->num_labels-2;
1243 
1244   GList *rm = g_list_nth(d->entries, pos);
1245   free_combobox_entry(rm->data);
1246   d->entries = g_list_delete_link(d->entries, rm);
1247 
1248   d->num_labels--;
1249 }
1250 
dt_bauhaus_combobox_insert(GtkWidget * widget,const char * text,int pos)1251 void dt_bauhaus_combobox_insert(GtkWidget *widget, const char *text,int pos)
1252 {
1253   dt_bauhaus_combobox_insert_full(widget, text, DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT, NULL, NULL, pos);
1254 }
1255 
dt_bauhaus_combobox_insert_full(GtkWidget * widget,const char * text,dt_bauhaus_combobox_alignment_t align,gpointer data,void (* free_func)(void *),int pos)1256 void dt_bauhaus_combobox_insert_full(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align,
1257                                      gpointer data, void (*free_func)(void *), int pos)
1258 {
1259   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1260   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1261   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1262   d->num_labels++;
1263   d->entries = g_list_insert(d->entries, new_combobox_entry(text, align, TRUE, data, free_func), pos);
1264   if(d->active < 0) d->active = 0;
1265 }
1266 
dt_bauhaus_combobox_length(GtkWidget * widget)1267 int dt_bauhaus_combobox_length(GtkWidget *widget)
1268 {
1269   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1270   if(w->type != DT_BAUHAUS_COMBOBOX) return 0;
1271   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1272 
1273   return d->num_labels;
1274 }
1275 
dt_bauhaus_combobox_get_text(GtkWidget * widget)1276 const char *dt_bauhaus_combobox_get_text(GtkWidget *widget)
1277 {
1278   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1279   if(w->type != DT_BAUHAUS_COMBOBOX) return NULL;
1280   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1281 
1282   if(d->editable && d->active < 0)
1283   {
1284     return d->text;
1285   }
1286   else
1287   {
1288     if(d->active < 0 || d->active >= d->num_labels) return NULL;
1289     const dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)g_list_nth_data(d->entries, d->active);
1290     return entry->label;
1291   }
1292   return NULL;
1293 }
1294 
dt_bauhaus_combobox_get_data(GtkWidget * widget)1295 gpointer dt_bauhaus_combobox_get_data(GtkWidget *widget)
1296 {
1297   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1298   if(w->type != DT_BAUHAUS_COMBOBOX) return NULL;
1299   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1300   const dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)g_list_nth_data(d->entries, d->active);
1301   return entry ? entry->data : NULL;
1302 }
1303 
dt_bauhaus_combobox_clear(GtkWidget * widget)1304 void dt_bauhaus_combobox_clear(GtkWidget *widget)
1305 {
1306   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1307   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1308   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1309   d->active = -1;
1310   g_list_free_full(d->entries, free_combobox_entry);
1311   d->entries = NULL;
1312   d->num_labels = 0;
1313 }
1314 
dt_bauhaus_combobox_get_entries(GtkWidget * widget)1315 const GList *dt_bauhaus_combobox_get_entries(GtkWidget *widget)
1316 {
1317   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1318   if(w->type != DT_BAUHAUS_COMBOBOX) return 0;
1319   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1320   return d->entries;
1321 }
1322 
dt_bauhaus_combobox_set_text(GtkWidget * widget,const char * text)1323 void dt_bauhaus_combobox_set_text(GtkWidget *widget, const char *text)
1324 {
1325   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1326   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1327   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1328   if(!d->editable) return;
1329   g_strlcpy(d->text, text, sizeof(d->text));
1330 }
1331 
_bauhaus_combobox_set(GtkWidget * widget,const int pos,const gboolean mute)1332 static void _bauhaus_combobox_set(GtkWidget *widget, const int pos, const gboolean mute)
1333 {
1334   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1335   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1336   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1337   d->active = CLAMP(pos, -1, d->num_labels - 1);
1338   gtk_widget_queue_draw(GTK_WIDGET(w));
1339   if(!darktable.gui->reset && !mute) g_signal_emit_by_name(G_OBJECT(w), "value-changed");
1340 }
1341 
dt_bauhaus_combobox_set(GtkWidget * widget,const int pos)1342 void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
1343 {
1344   _bauhaus_combobox_set(widget, pos, FALSE);
1345 }
1346 
dt_bauhaus_combobox_set_from_text(GtkWidget * widget,const char * text)1347 gboolean dt_bauhaus_combobox_set_from_text(GtkWidget *widget, const char *text)
1348 {
1349   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1350   if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE;
1351   if(!text) return FALSE;
1352   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1353   int i = 0;
1354   for(GList *iter = d->entries; iter; iter = g_list_next(iter), i++)
1355   {
1356     const dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)iter->data;
1357     if(!g_strcmp0(entry->label, text))
1358     {
1359       dt_bauhaus_combobox_set(widget, i);
1360       return TRUE;
1361     }
1362   }
1363   return FALSE;
1364 }
1365 
dt_bauhaus_combobox_set_from_value(GtkWidget * widget,int value)1366 gboolean dt_bauhaus_combobox_set_from_value(GtkWidget *widget, int value)
1367 {
1368   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1369   if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE;
1370   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1371   int i = 0;
1372   for(GList *iter = d->entries; iter; iter = g_list_next(iter), i++)
1373   {
1374     const dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)iter->data;
1375     if(GPOINTER_TO_INT(entry->data) == value)
1376     {
1377       dt_bauhaus_combobox_set(widget, i);
1378       return TRUE;
1379     }
1380   }
1381   return FALSE;
1382 }
1383 
dt_bauhaus_combobox_get(GtkWidget * widget)1384 int dt_bauhaus_combobox_get(GtkWidget *widget)
1385 {
1386   const dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1387   if(w->type != DT_BAUHAUS_COMBOBOX) return -1;
1388   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1389   return d->active;
1390 }
1391 
dt_bauhaus_combobox_entry_set_sensitive(GtkWidget * widget,int pos,gboolean sensitive)1392 void dt_bauhaus_combobox_entry_set_sensitive(GtkWidget *widget, int pos, gboolean sensitive)
1393 {
1394   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1395   if(w->type != DT_BAUHAUS_COMBOBOX) return;
1396   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1397   dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)g_list_nth_data(d->entries, pos);
1398   if(entry)
1399     entry->sensitive = sensitive;
1400 }
1401 
dt_bauhaus_slider_clear_stops(GtkWidget * widget)1402 void dt_bauhaus_slider_clear_stops(GtkWidget *widget)
1403 {
1404   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1405   if(w->type != DT_BAUHAUS_SLIDER) return;
1406   dt_bauhaus_slider_data_t *d = &w->data.slider;
1407   d->grad_cnt = 0;
1408 }
1409 
dt_bauhaus_slider_set_stop(GtkWidget * widget,float stop,float r,float g,float b)1410 void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, float b)
1411 {
1412   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1413   if(w->type != DT_BAUHAUS_SLIDER) return;
1414   dt_bauhaus_slider_data_t *d = &w->data.slider;
1415   // need to replace stop?
1416   for(int k = 0; k < d->grad_cnt; k++)
1417   {
1418     if(d->grad_pos[k] == stop)
1419     {
1420       d->grad_col[k][0] = r;
1421       d->grad_col[k][1] = g;
1422       d->grad_col[k][2] = b;
1423       return;
1424     }
1425   }
1426   // new stop:
1427   if(d->grad_cnt < DT_BAUHAUS_SLIDER_MAX_STOPS)
1428   {
1429     int k = d->grad_cnt++;
1430     d->grad_pos[k] = stop;
1431     d->grad_col[k][0] = r;
1432     d->grad_col[k][1] = g;
1433     d->grad_col[k][2] = b;
1434   }
1435   else
1436   {
1437     fprintf(stderr, "[bauhaus_slider_set_stop] only %d stops allowed.\n", DT_BAUHAUS_SLIDER_MAX_STOPS);
1438   }
1439 }
1440 
1441 
draw_equilateral_triangle(cairo_t * cr,float radius)1442 static void draw_equilateral_triangle(cairo_t *cr, float radius)
1443 {
1444   const float sin = 0.866025404 * radius;
1445   const float cos = 0.5f * radius;
1446   cairo_move_to(cr, 0.0, radius);
1447   cairo_line_to(cr, -sin, -cos);
1448   cairo_line_to(cr, sin, -cos);
1449   cairo_line_to(cr, 0.0, radius);
1450 }
1451 
1452 
dt_bauhaus_draw_indicator(dt_bauhaus_widget_t * w,float pos,cairo_t * cr,const GdkRGBA fg_color,const GdkRGBA border_color)1453 static void dt_bauhaus_draw_indicator(dt_bauhaus_widget_t *w, float pos, cairo_t *cr, const GdkRGBA fg_color, const GdkRGBA border_color)
1454 {
1455   // draw scale indicator (the tiny triangle)
1456   GtkWidget *widget = GTK_WIDGET(w);
1457   GtkAllocation allocation;
1458   gtk_widget_get_allocation(widget, &allocation);
1459   if(w->type != DT_BAUHAUS_SLIDER) return;
1460   const float wd = allocation.width;
1461   const float border_width = darktable.bauhaus->border_width;
1462   const float size = darktable.bauhaus->marker_size;
1463 
1464   cairo_save(cr);
1465   cairo_translate(cr, slider_coordinate(pos, wd), get_line_height() + INNER_PADDING - border_width);
1466   cairo_scale(cr, 1.0f, -1.0f);
1467   cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
1468 
1469   // draw the outer triangle
1470   draw_equilateral_triangle(cr, size);
1471   cairo_set_line_width(cr, border_width);
1472   set_color(cr, border_color);
1473   cairo_stroke(cr);
1474 
1475   draw_equilateral_triangle(cr, size - border_width);
1476   cairo_clip(cr);
1477 
1478   // draw the inner triangle
1479   draw_equilateral_triangle(cr, size - border_width);
1480   set_color(cr, fg_color);
1481   cairo_set_line_width(cr, border_width);
1482 
1483   const dt_bauhaus_slider_data_t *d = &w->data.slider;
1484 
1485   if(d->fill_feedback)
1486     cairo_fill(cr); // Plain indicator (regular sliders)
1487   else
1488     cairo_stroke(cr);  // Hollow indicator to see a color through it (gradient sliders)
1489 
1490   cairo_restore(cr);
1491 }
1492 
dt_bauhaus_draw_quad(dt_bauhaus_widget_t * w,cairo_t * cr)1493 static void dt_bauhaus_draw_quad(dt_bauhaus_widget_t *w, cairo_t *cr)
1494 {
1495   GtkWidget *widget = GTK_WIDGET(w);
1496   const gboolean sensitive = gtk_widget_is_sensitive(GTK_WIDGET(w));
1497   GtkAllocation allocation;
1498   gtk_widget_get_allocation(widget, &allocation);
1499   const int width = allocation.width;
1500   const int height = inner_height(allocation);
1501 
1502   if(w->quad_paint)
1503   {
1504     cairo_save(cr);
1505 
1506     if(sensitive && (w->quad_paint_flags & CPF_ACTIVE))
1507       set_color(cr, darktable.bauhaus->color_fg);
1508     else
1509       set_color(cr, darktable.bauhaus->color_fg_insensitive);
1510 
1511     w->quad_paint(cr, width - darktable.bauhaus->quad_width,  // x
1512                       0.0,                                    // y
1513                       darktable.bauhaus->quad_width,          // width
1514                       darktable.bauhaus->quad_width,          // height
1515                       w->quad_paint_flags, w->quad_paint_data);
1516 
1517     cairo_restore(cr);
1518   }
1519   else
1520   {
1521     // draw active area square:
1522     cairo_save(cr);
1523     if(sensitive)
1524       set_color(cr, darktable.bauhaus->color_fg);
1525     else
1526       set_color(cr, darktable.bauhaus->color_fg_insensitive);
1527     switch(w->type)
1528     {
1529       case DT_BAUHAUS_COMBOBOX:
1530         cairo_translate(cr, width - darktable.bauhaus->quad_width * .5f, height * .33f);
1531         draw_equilateral_triangle(cr, darktable.bauhaus->quad_width * .25f);
1532         cairo_fill_preserve(cr);
1533         cairo_set_line_width(cr, 0.5);
1534         set_color(cr, darktable.bauhaus->color_border);
1535         cairo_stroke(cr);
1536         break;
1537       case DT_BAUHAUS_SLIDER:
1538         break;
1539       default:
1540         cairo_set_source_rgb(cr, 1.0, 0.0, 0.0);
1541         cairo_rectangle(cr, width - darktable.bauhaus->quad_width, 0.0, darktable.bauhaus->quad_width, darktable.bauhaus->quad_width);
1542         cairo_fill(cr);
1543         break;
1544     }
1545     cairo_restore(cr);
1546   }
1547 }
1548 
dt_bauhaus_draw_baseline(dt_bauhaus_widget_t * w,cairo_t * cr)1549 static void dt_bauhaus_draw_baseline(dt_bauhaus_widget_t *w, cairo_t *cr)
1550 {
1551   // draw line for orientation in slider
1552   GtkWidget *widget = GTK_WIDGET(w);
1553   GtkAllocation allocation;
1554   gtk_widget_get_allocation(widget, &allocation);
1555   if(w->type != DT_BAUHAUS_SLIDER) return;
1556   const int wd = allocation.width;
1557   const float slider_width = wd - darktable.bauhaus->quad_width - INNER_PADDING;
1558   cairo_save(cr);
1559   dt_bauhaus_slider_data_t *d = &w->data.slider;
1560 
1561   // pos of baseline
1562   const float htm = darktable.bauhaus->line_height + INNER_PADDING;
1563 
1564   // thickness of baseline
1565   const float htM = darktable.bauhaus->baseline_size - darktable.bauhaus->border_width;
1566 
1567   // the background of the line
1568   cairo_pattern_t *gradient = NULL;
1569   cairo_rectangle(cr, 0, htm, slider_width, htM);
1570 
1571   if(d->grad_cnt > 0)
1572   {
1573     // gradient line as used in some modules
1574     gradient = cairo_pattern_create_linear(0, 0, slider_width, htM);
1575     for(int k = 0; k < d->grad_cnt; k++)
1576       cairo_pattern_add_color_stop_rgba(gradient, d->grad_pos[k], d->grad_col[k][0], d->grad_col[k][1],
1577                                         d->grad_col[k][2], 0.4f);
1578     cairo_set_source(cr, gradient);
1579   }
1580   else
1581   {
1582     // regular baseline
1583     set_color(cr, darktable.bauhaus->color_bg);
1584   }
1585 
1586   cairo_fill(cr);
1587 
1588   // get the reference of the slider aka the position of the 0 value
1589   const float origin = fmaxf(fminf((d->factor > 0 ? -d->min - d->offset/d->factor
1590                                                   :  d->max + d->offset/d->factor)
1591                                                   / (d->max - d->min), 1.0f) * slider_width, 0.0f);
1592   const float position = d->pos * slider_width;
1593   const float delta = position - origin;
1594 
1595   // have a `fill ratio feel' from zero to current position
1596   // - but only if set
1597   if(d->fill_feedback)
1598   {
1599     // only brighten, useful for colored sliders to not get too faint:
1600     cairo_set_operator(cr, CAIRO_OPERATOR_SCREEN);
1601     set_color(cr, darktable.bauhaus->color_fill);
1602     cairo_rectangle(cr, origin, htm, delta, htM);
1603     cairo_fill(cr);
1604 
1605     // change back to default cairo operator:
1606     cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
1607   }
1608 
1609   // draw the 0 reference graduation if it's different than the bounds of the slider
1610   const float graduation_top = htm + htM + 2.0f * darktable.bauhaus->border_width;
1611   const float graduation_height = darktable.bauhaus->border_width / 2.0f;
1612   set_color(cr, darktable.bauhaus->color_fg);
1613 
1614   // If the max of the slider is 180 or 360, it is likely a hue slider in degrees
1615   // a zero in periodic stuff has not much meaning so we skip it
1616   if(d->hard_max != 180.0f && d->hard_max != 360.0f)
1617   {
1618     // translate the dot if it overflows the widget frame
1619     if(origin < graduation_height)
1620       cairo_arc(cr, graduation_height, graduation_top, graduation_height, 0, 2 * M_PI);
1621     else if(origin > slider_width - graduation_height)
1622       cairo_arc(cr, slider_width - graduation_height, graduation_top, graduation_height, 0, 2 * M_PI);
1623     else
1624       cairo_arc(cr, origin, graduation_top, graduation_height, 0, 2 * M_PI);
1625 }
1626 
1627   cairo_fill(cr);
1628   cairo_restore(cr);
1629 
1630   if(d->grad_cnt > 0) cairo_pattern_destroy(gradient);
1631 }
1632 
dt_bauhaus_widget_reject(dt_bauhaus_widget_t * w)1633 static void dt_bauhaus_widget_reject(dt_bauhaus_widget_t *w)
1634 {
1635   switch(w->type)
1636   {
1637     case DT_BAUHAUS_COMBOBOX:
1638       break;
1639     case DT_BAUHAUS_SLIDER:
1640     {
1641       dt_bauhaus_slider_data_t *d = &w->data.slider;
1642       dt_bauhaus_slider_set_normalized(w, d->oldpos);
1643     }
1644     break;
1645     default:
1646       break;
1647   }
1648 }
1649 
dt_bauhaus_widget_accept(dt_bauhaus_widget_t * w)1650 static void dt_bauhaus_widget_accept(dt_bauhaus_widget_t *w)
1651 {
1652   GtkWidget *widget = GTK_WIDGET(w);
1653   GtkAllocation allocation;
1654   gtk_widget_get_allocation(widget, &allocation);
1655   const int base_width = allocation.width, base_height = inner_height(allocation);
1656 
1657   GtkAllocation allocation_popup_window;
1658   gtk_widget_get_allocation(darktable.bauhaus->popup_window, &allocation_popup_window);
1659 
1660   const int width = allocation_popup_window.width, height = inner_height(allocation_popup_window);
1661 
1662   switch(w->type)
1663   {
1664     case DT_BAUHAUS_COMBOBOX:
1665     {
1666       // only set to what's in the filtered list.
1667       dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1668       const int active = darktable.bauhaus->end_mouse_y >= 0
1669                  ? ((darktable.bauhaus->end_mouse_y - INNER_PADDING) / (darktable.bauhaus->line_height))
1670                  : d->active;
1671 
1672       int k = 0, i = 0, kk = 0, match = 1;
1673 
1674       gchar *keys = g_utf8_casefold(darktable.bauhaus->keys, -1);
1675       for(GList *it = d->entries; it; it = g_list_next(it))
1676       {
1677         const dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)it->data;
1678         gchar *text_cmp = g_utf8_casefold(entry->label, -1);
1679         if(!strncmp(text_cmp, keys, darktable.bauhaus->keys_cnt))
1680         {
1681           if(active == k)
1682           {
1683             if(entry->sensitive)
1684               dt_bauhaus_combobox_set(widget, i);
1685             g_free(keys);
1686             g_free(text_cmp);
1687             return;
1688           }
1689           kk = i; // remember for down there
1690           // editable should only snap to perfect matches, not prefixes:
1691           if(d->editable && strcmp(entry->label, darktable.bauhaus->keys)) match = 0;
1692           k++;
1693         }
1694         i++;
1695         g_free(text_cmp);
1696       }
1697       // didn't find it, but had only one matching choice?
1698       if(k == 1 && match)
1699         dt_bauhaus_combobox_set(widget, kk);
1700       else if(d->editable)
1701       {
1702         // otherwise, if combobox is editable, assume it is a custom input
1703         memset(d->text, 0, sizeof(d->text));
1704         g_strlcpy(d->text, darktable.bauhaus->keys, sizeof(d->text));
1705         // select custom entry
1706         dt_bauhaus_combobox_set(widget, -1);
1707       }
1708       g_free(keys);
1709       break;
1710     }
1711     case DT_BAUHAUS_SLIDER:
1712     {
1713       dt_bauhaus_slider_data_t *d = &w->data.slider;
1714       const float mouse_off = get_slider_line_offset(
1715           d->oldpos, d->scale, darktable.bauhaus->end_mouse_x / width,
1716           darktable.bauhaus->end_mouse_y / height, base_height / (float)height, base_width);
1717       dt_bauhaus_slider_set_normalized(w, d->oldpos + mouse_off);
1718       d->oldpos = d->pos;
1719       break;
1720     }
1721     default:
1722       break;
1723   }
1724 }
1725 
dt_bauhaus_popup_draw(GtkWidget * widget,cairo_t * crf,gpointer user_data)1726 static gboolean dt_bauhaus_popup_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
1727 {
1728   dt_bauhaus_widget_t *w = darktable.bauhaus->current;
1729 
1730   // dimensions of the popup
1731   GtkAllocation allocation;
1732   gtk_widget_get_allocation(widget, &allocation);
1733   const int width = allocation.width - INNER_PADDING;
1734   const int height = inner_height(allocation);
1735 
1736   // dimensions of the original line
1737   GtkWidget *current = GTK_WIDGET(w);
1738   GtkAllocation allocation_current;
1739   gtk_widget_get_allocation(current, &allocation_current);
1740   int wd = allocation_current.width, ht = inner_height(allocation_current);
1741 
1742   const int popwin_wd = allocation.width + darktable.bauhaus->widget_space * 2.0f;
1743   const int popwin_ht = allocation.height + darktable.bauhaus->widget_space * 2.0f;
1744 
1745   // get area properties
1746   cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
1747                                                        popwin_wd, popwin_ht);
1748 
1749   cairo_t *cr = cairo_create(cst);
1750   GtkStyleContext *context = gtk_widget_get_style_context(widget);
1751 
1752   // look up some colors once
1753   GdkRGBA text_color, text_color_selected, text_color_hover, text_color_insensitive;
1754   gtk_style_context_get_color(context, GTK_STATE_FLAG_NORMAL, &text_color);
1755   gtk_style_context_get_color(context, GTK_STATE_FLAG_SELECTED, &text_color_selected);
1756   gtk_style_context_get_color(context, GTK_STATE_FLAG_PRELIGHT, &text_color_hover);
1757   gtk_style_context_get_color(context, GTK_STATE_FLAG_INSENSITIVE, &text_color_insensitive);
1758 
1759   GdkRGBA *fg_color = default_color_assign();
1760   GdkRGBA *bg_color;
1761   GtkStateFlags state = gtk_widget_get_state_flags(widget);
1762 
1763   gtk_style_context_get(context, state, "background-color", &bg_color, NULL);
1764   gtk_style_context_get_color(context, state, fg_color);
1765 
1766   // draw background
1767   gtk_render_background(context, cr, 0, 0, popwin_wd, popwin_ht);
1768 
1769   // draw border
1770   cairo_save(cr);
1771   set_color(cr, *fg_color);
1772   cairo_set_line_width(cr, darktable.bauhaus->widget_space);
1773   cairo_rectangle(cr, 0, 0, popwin_wd - 2, popwin_ht - 2);
1774   cairo_stroke(cr);
1775   cairo_restore(cr);
1776 
1777   // translate to account for the widget spacing
1778   cairo_translate(cr, darktable.bauhaus->widget_space, darktable.bauhaus->widget_space);
1779 
1780   // switch on bauhaus widget type (so we only need one static window)
1781   switch(w->type)
1782   {
1783     case DT_BAUHAUS_SLIDER:
1784     {
1785       const dt_bauhaus_slider_data_t *d = &w->data.slider;
1786 
1787       cairo_translate(cr, INNER_PADDING, 0);
1788 
1789       dt_bauhaus_draw_baseline(w, cr);
1790 
1791       cairo_save(cr);
1792       cairo_set_line_width(cr, 0.5);
1793       const int num_scales = 1.f / d->scale;
1794 
1795       cairo_rectangle(cr, - INNER_PADDING, ht, width + INNER_PADDING, height);
1796       cairo_clip(cr);
1797 
1798       for(int k = 0; k < num_scales; k++)
1799       {
1800         const float off = k * d->scale - d->oldpos;
1801         GdkRGBA fg_copy = *fg_color;
1802         fg_copy.alpha = d->scale / fabsf(off);
1803         set_color(cr, fg_copy);
1804         draw_slider_line(cr, d->oldpos, off, d->scale, width, height, ht);
1805         cairo_stroke(cr);
1806       }
1807       cairo_restore(cr);
1808       set_color(cr, *fg_color);
1809 
1810       // draw mouse over indicator line
1811       cairo_save(cr);
1812       cairo_set_line_width(cr, 2.);
1813       const float mouse_off
1814           = darktable.bauhaus->change_active
1815                 ? get_slider_line_offset(d->oldpos, d->scale, (darktable.bauhaus->mouse_x - INNER_PADDING)/ width,
1816                                          darktable.bauhaus->mouse_y / height, ht / (float)height, width)
1817                 : 0.0f;
1818       draw_slider_line(cr, d->oldpos, mouse_off, d->scale, width, height, ht);
1819       cairo_stroke(cr);
1820       cairo_restore(cr);
1821 
1822       // draw indicator
1823       dt_bauhaus_draw_indicator(w, d->oldpos + mouse_off, cr, *fg_color, *bg_color);
1824 
1825       // draw numerical value:
1826       cairo_save(cr);
1827 
1828       char *text = dt_bauhaus_slider_get_text(current);
1829       set_color(cr, *fg_color);
1830       float value_width = show_pango_text(w, context, cr, text, wd - darktable.bauhaus->quad_width - INNER_PADDING,
1831                                           0, 0, TRUE, FALSE, PANGO_ELLIPSIZE_END, FALSE, FALSE);
1832       g_free(text);
1833 
1834       float label_width = width - darktable.bauhaus->quad_width - INNER_PADDING * 2.0 - value_width;
1835       if(label_width > 0)
1836       {
1837         gchar *lb = NULL;
1838         if(w->show_extended_label && w->section)
1839           lb = dt_util_dstrcat(NULL, "%s - %s", w->section, w->label);
1840         else
1841           lb = g_strdup(w->label);
1842         show_pango_text(w, context, cr, lb, 0, 0, label_width, FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE, FALSE);
1843         g_free(lb);
1844       }
1845       cairo_restore(cr);
1846     }
1847     break;
1848     case DT_BAUHAUS_COMBOBOX:
1849     {
1850       const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
1851       cairo_save(cr);
1852       float first_label_width = 0.0;
1853       gboolean first_label = TRUE;
1854       gboolean show_box_label = TRUE;
1855       int k = 0, i = 0;
1856       const int hovered = (darktable.bauhaus->mouse_y - darktable.bauhaus->widget_space) / darktable.bauhaus->line_height;
1857       gchar *keys = g_utf8_casefold(darktable.bauhaus->keys, -1);
1858       const PangoEllipsizeMode ellipsis = d->entries_ellipsis;
1859       wd *= d->scale;
1860       ht = darktable.bauhaus->line_height;
1861 
1862       for(GList *it = d->entries; it; it = g_list_next(it))
1863       {
1864         const dt_bauhaus_combobox_entry_t *entry = (dt_bauhaus_combobox_entry_t *)it->data;
1865         gchar *text_cmp = g_utf8_casefold(entry->label, -1);
1866         if(!strncmp(text_cmp, keys, darktable.bauhaus->keys_cnt))
1867         {
1868           float max_width = wd - INNER_PADDING - darktable.bauhaus->quad_width;
1869           if(first_label) max_width *= 0.8; // give the label at least some room
1870           float label_width = 0.0f;
1871           if(!entry->sensitive)
1872             set_color(cr, text_color_insensitive);
1873           else if(i == hovered)
1874             set_color(cr, text_color_hover);
1875           else if(i == d->active)
1876             set_color(cr, text_color_selected);
1877           else
1878             set_color(cr, text_color);
1879 
1880           if(entry->alignment == DT_BAUHAUS_COMBOBOX_ALIGN_LEFT)
1881           {
1882             gchar *esc_label = g_markup_escape_text(entry->label, -1);
1883             gchar *label = g_strdup_printf("<b>%s</b>", esc_label);
1884             label_width = show_pango_text(w, context, cr, label, INNER_PADDING, ht * k + darktable.bauhaus->widget_space,
1885                                           max_width, FALSE, FALSE, ellipsis, TRUE, FALSE);
1886             g_free(label);
1887             g_free(esc_label);
1888           }
1889           else
1890             label_width
1891                 = show_pango_text(w, context, cr, entry->label, wd - darktable.bauhaus->quad_width,
1892                                   ht * k + darktable.bauhaus->widget_space, max_width, TRUE, FALSE, ellipsis, FALSE, FALSE);
1893 
1894           // prefer the entry over the label wrt. ellipsization when expanded
1895           if(first_label)
1896           {
1897             show_box_label = entry->alignment == DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT;
1898             first_label_width = label_width;
1899             first_label = FALSE;
1900           }
1901 
1902           k++;
1903         }
1904         i++;
1905         g_free(text_cmp);
1906       }
1907       cairo_restore(cr);
1908 
1909       // left aligned box label. add it to the gui after the entries so we can ellipsize it if needed
1910       if(show_box_label)
1911       {
1912         set_color(cr, text_color);
1913         gchar *lb = NULL;
1914         if(w->show_extended_label && w->section)
1915           lb = dt_util_dstrcat(NULL, "%s - %s", w->section, w->label);
1916         else
1917           lb = g_strdup(w->label);
1918         show_pango_text(w, context, cr, lb, INNER_PADDING, darktable.bauhaus->widget_space,
1919                         wd - INNER_PADDING - darktable.bauhaus->quad_width - first_label_width, FALSE, FALSE,
1920                         PANGO_ELLIPSIZE_END, FALSE, TRUE);
1921         g_free(lb);
1922       }
1923       g_free(keys);
1924     }
1925     break;
1926     default:
1927       // yell
1928       break;
1929   }
1930 
1931   // draw currently typed text. if a type doesn't want this, it should not
1932   // allow stuff to be written here in the key callback.
1933   const int line_height = get_line_height();
1934   const int size = MIN(3 * line_height, .2 * height);
1935   if(darktable.bauhaus->keys_cnt)
1936   {
1937     cairo_save(cr);
1938     PangoLayout *layout = pango_cairo_create_layout(cr);
1939     PangoRectangle ink;
1940     pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
1941     set_color(cr, text_color);
1942 
1943     // make extra large, but without dependency on popup window height
1944     // (that might differ for comboboxes for example). only fall back
1945     // to height dependency if the popup is really small.
1946     PangoFontDescription *desc = pango_font_description_copy_static(darktable.bauhaus->pango_font_desc);
1947     pango_font_description_set_absolute_size(desc, size * PANGO_SCALE);
1948     pango_layout_set_font_description(layout, desc);
1949 
1950     pango_layout_set_text(layout, darktable.bauhaus->keys, -1);
1951     pango_layout_get_pixel_extents(layout, &ink, NULL);
1952     cairo_move_to(cr, wd - INNER_PADDING - darktable.bauhaus->quad_width - ink.width, height * 0.5 - size);
1953     pango_cairo_show_layout(cr, layout);
1954     cairo_restore(cr);
1955     pango_font_description_free(desc);
1956     g_object_unref(layout);
1957   }
1958   if(darktable.bauhaus->cursor_visible)
1959   {
1960     // show the blinking cursor
1961     cairo_save(cr);
1962     set_color(cr, text_color);
1963     cairo_move_to(cr, wd - darktable.bauhaus->quad_width + 3, height * 0.5 + size/3);
1964     cairo_line_to(cr, wd - darktable.bauhaus->quad_width + 3, height * 0.5 - size);
1965     cairo_set_line_width(cr, 2.);
1966     cairo_stroke(cr);
1967     cairo_restore(cr);
1968   }
1969 
1970   cairo_destroy(cr);
1971   cairo_set_source_surface(crf, cst, 0, 0);
1972   cairo_paint(crf);
1973   cairo_surface_destroy(cst);
1974 
1975   gdk_rgba_free(bg_color);
1976   gdk_rgba_free(fg_color);
1977 
1978   return TRUE;
1979 }
1980 
dt_bauhaus_draw(GtkWidget * widget,cairo_t * crf,gpointer user_data)1981 static gboolean dt_bauhaus_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
1982 {
1983   GtkAllocation allocation;
1984   gtk_widget_get_allocation(widget, &allocation);
1985   dt_bauhaus_widget_t *w = DT_BAUHAUS_WIDGET(widget);
1986   const int width = allocation.width, height = allocation.height;
1987   cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
1988   cairo_t *cr = cairo_create(cst);
1989   GtkStyleContext *context = gtk_widget_get_style_context(widget);
1990 
1991   // translate to account for the widget spacing
1992   cairo_translate(cr, 0, darktable.bauhaus->widget_space);
1993 
1994   GdkRGBA *fg_color = default_color_assign();
1995   GdkRGBA *bg_color;
1996   GdkRGBA *text_color = default_color_assign();
1997   const GtkStateFlags state = gtk_widget_get_state_flags(widget);
1998   gtk_style_context_get_color(context, state, text_color);
1999   gtk_render_background(context, cr, 0, 0, width, height + INNER_PADDING);
2000   gtk_style_context_get_color(context, state, fg_color);
2001   gtk_style_context_get(context, state, "background-color", &bg_color, NULL);
2002 
2003   // draw type specific content:
2004   cairo_save(cr);
2005   cairo_set_line_width(cr, 1.0);
2006   switch(w->type)
2007   {
2008     case DT_BAUHAUS_COMBOBOX:
2009     {
2010       // draw label and quad area at right end
2011       set_color(cr, *text_color);
2012       dt_bauhaus_draw_quad(w, cr);
2013 
2014       dt_bauhaus_combobox_data_t *d = &w->data.combobox;
2015       const PangoEllipsizeMode combo_ellipsis = d->entries_ellipsis;
2016       gchar *text = d->text;
2017       if(d->active >= 0)
2018       {
2019         const dt_bauhaus_combobox_entry_t *entry = g_list_nth_data(d->entries, d->active);
2020         text = entry->label;
2021       }
2022       set_color(cr, *text_color);
2023 
2024       const float available_width = width - darktable.bauhaus->quad_width - INNER_PADDING;
2025 
2026       //calculate total widths of label and combobox
2027       gchar *label_text = NULL;
2028       if(w->show_extended_label && w->section)
2029         label_text = dt_util_dstrcat(NULL, "%s - %s", w->section, w->label);
2030       else
2031         label_text = g_strdup(w->label);
2032       const float label_width
2033           = show_pango_text(w, context, cr, label_text, 0, 0, 0, FALSE, TRUE, PANGO_ELLIPSIZE_END, FALSE, TRUE);
2034       const float combo_width
2035         = show_pango_text(w, context, cr, text, width - darktable.bauhaus->quad_width - INNER_PADDING, 0, 0,
2036                           TRUE, TRUE, combo_ellipsis, FALSE, FALSE);
2037 
2038       //check if they fit
2039       if((label_width + combo_width) > available_width)
2040       {
2041         //they don't fit: evenly divide the available width between the two in proportion
2042         const float ratio = label_width / (label_width + combo_width);
2043         show_pango_text(w, context, cr, label_text, 0, darktable.bauhaus->widget_space,
2044                         available_width * ratio - INNER_PADDING * 2, FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE,
2045                         TRUE);
2046         if(d->text_align == DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT)
2047           show_pango_text(w, context, cr, text, width - darktable.bauhaus->quad_width - INNER_PADDING, darktable.bauhaus->widget_space,
2048                           available_width * (1.0f - ratio),
2049                           TRUE, FALSE, combo_ellipsis, FALSE, FALSE);
2050         else
2051           show_pango_text(w, context, cr, text, INNER_PADDING, darktable.bauhaus->widget_space,
2052                           available_width * (1.0f - ratio),
2053                           FALSE, FALSE, combo_ellipsis, FALSE, FALSE);
2054       }
2055       else
2056       {
2057         show_pango_text(w, context, cr, label_text, 0, darktable.bauhaus->widget_space, 0, FALSE, FALSE,
2058                         PANGO_ELLIPSIZE_END, FALSE, TRUE);
2059         if(d->text_align == DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT)
2060           show_pango_text(w, context, cr, text, width - darktable.bauhaus->quad_width - INNER_PADDING, darktable.bauhaus->widget_space, 0,
2061                           TRUE, FALSE, combo_ellipsis, FALSE, FALSE);
2062         else
2063           show_pango_text(w, context, cr, text, INNER_PADDING, darktable.bauhaus->widget_space, 0,
2064                           FALSE, FALSE, combo_ellipsis, FALSE, FALSE);
2065       }
2066       g_free(label_text);
2067       break;
2068     }
2069     case DT_BAUHAUS_SLIDER:
2070     {
2071       const dt_bauhaus_slider_data_t *d = &w->data.slider;
2072 
2073       // line for orientation
2074       dt_bauhaus_draw_baseline(w, cr);
2075       dt_bauhaus_draw_quad(w, cr);
2076 
2077       float value_width = 0;
2078       if(gtk_widget_is_sensitive(widget))
2079       {
2080         cairo_save(cr);
2081         cairo_rectangle(cr, 0, 0, width - darktable.bauhaus->quad_width - INNER_PADDING, height + INNER_PADDING);
2082         cairo_clip(cr);
2083         dt_bauhaus_draw_indicator(w, d->pos, cr, *fg_color, *bg_color);
2084         cairo_restore(cr);
2085 
2086         // TODO: merge that text with combo
2087 
2088         char *text = dt_bauhaus_slider_get_text(widget);
2089         set_color(cr, *text_color);
2090         value_width = show_pango_text(w, context, cr, text, width - darktable.bauhaus->quad_width - INNER_PADDING,
2091                                       0, 0, TRUE, FALSE, PANGO_ELLIPSIZE_END, FALSE, FALSE);
2092         g_free(text);
2093       }
2094       // label on top of marker:
2095       gchar *label_text = NULL;
2096       if(w->show_extended_label && w->section)
2097         label_text = dt_util_dstrcat(NULL, "%s - %s", w->section, w->label);
2098       else
2099         label_text = g_strdup(w->label);
2100       set_color(cr, *text_color);
2101       float label_width = width - darktable.bauhaus->quad_width - INNER_PADDING - value_width;
2102       if(label_width > 0)
2103         show_pango_text(w, context, cr, label_text, 0, 0, label_width, FALSE, FALSE, PANGO_ELLIPSIZE_END, FALSE,
2104                         TRUE);
2105       g_free(label_text);
2106     }
2107     break;
2108     default:
2109       break;
2110   }
2111   cairo_restore(cr);
2112   cairo_destroy(cr);
2113   cairo_set_source_surface(crf, cst, 0, 0);
2114   cairo_paint(crf);
2115   cairo_surface_destroy(cst);
2116 
2117   gdk_rgba_free(text_color);
2118   gdk_rgba_free(fg_color);
2119   gdk_rgba_free(bg_color);
2120 
2121   return TRUE;
2122 }
2123 
dt_bauhaus_hide_popup()2124 void dt_bauhaus_hide_popup()
2125 {
2126   if(darktable.bauhaus->current)
2127   {
2128     gtk_grab_remove(darktable.bauhaus->popup_window);
2129     gtk_widget_hide(darktable.bauhaus->popup_window);
2130     gtk_window_set_attached_to(GTK_WINDOW(darktable.bauhaus->popup_window), NULL);
2131     darktable.bauhaus->current = NULL;
2132     // TODO: give focus to center view? do in accept() as well?
2133   }
2134   _stop_cursor();
2135 }
2136 
dt_bauhaus_show_popup(dt_bauhaus_widget_t * w)2137 void dt_bauhaus_show_popup(dt_bauhaus_widget_t *w)
2138 {
2139   if(darktable.bauhaus->current) dt_bauhaus_hide_popup();
2140   darktable.bauhaus->current = w;
2141   darktable.bauhaus->keys_cnt = 0;
2142   memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
2143   darktable.bauhaus->change_active = 0;
2144   darktable.bauhaus->mouse_line_distance = 0.0f;
2145   darktable.bauhaus->hiding = FALSE;
2146   _stop_cursor();
2147 
2148   if(w->module)
2149   {
2150     dt_iop_request_focus(w->module);
2151     gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2152   }
2153 
2154   int offset = 0;
2155   GtkAllocation tmp;
2156   gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
2157   if(tmp.width == 1) return;
2158 
2159   gtk_widget_realize(darktable.bauhaus->popup_window);
2160   switch(darktable.bauhaus->current->type)
2161   {
2162     case DT_BAUHAUS_SLIDER:
2163     {
2164       dt_bauhaus_slider_data_t *d = &w->data.slider;
2165       d->oldpos = d->pos;
2166       tmp.height = tmp.width;
2167       _start_cursor(6);
2168       break;
2169     }
2170     case DT_BAUHAUS_COMBOBOX:
2171     {
2172       // we launch the dynamic populate fct if any
2173       if(w->combo_populate) w->combo_populate(GTK_WIDGET(w), &w->module);
2174       // comboboxes change immediately
2175       darktable.bauhaus->change_active = 1;
2176       const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
2177       if(!d->num_labels) return;
2178       tmp.height = darktable.bauhaus->line_height * d->num_labels + 5 * darktable.bauhaus->widget_space;
2179       tmp.width *= d->scale;
2180 
2181       GtkAllocation allocation_w;
2182       gtk_widget_get_allocation(GTK_WIDGET(w), &allocation_w);
2183       const int ht = allocation_w.height;
2184       const int skip = darktable.bauhaus->line_height;
2185       offset = -d->active * darktable.bauhaus->line_height;
2186       darktable.bauhaus->mouse_x = 0;
2187       darktable.bauhaus->mouse_y = d->active * skip + ht / 2;
2188       break;
2189     }
2190     default:
2191       break;
2192   }
2193 
2194   gint wx, wy;
2195   gdk_window_get_origin(gtk_widget_get_window(GTK_WIDGET(w)), &wx, &wy);
2196 
2197   // move popup so mouse is over currently active item, to minimize confusion with scroll wheel:
2198   if(darktable.bauhaus->current->type == DT_BAUHAUS_COMBOBOX) wy += offset;
2199   wx -= darktable.bauhaus->widget_space + INNER_PADDING;
2200   tmp.width += darktable.bauhaus->widget_space + INNER_PADDING;
2201 
2202   // gtk_widget_get_window will return null if not shown yet.
2203   // it is needed for gdk_window_move, and gtk_window move will
2204   // sometimes be ignored. this is why we always call both...
2205   // we also don't want to show before move, as this results in noticeable flickering.
2206   GdkWindow *window = gtk_widget_get_window(darktable.bauhaus->popup_window);
2207   if(window) gdk_window_move(window, wx, wy);
2208   gtk_window_move(GTK_WINDOW(darktable.bauhaus->popup_window), wx, wy);
2209   gtk_widget_set_size_request(darktable.bauhaus->popup_area, tmp.width, tmp.height);
2210   gtk_widget_set_size_request(darktable.bauhaus->popup_window, tmp.width, tmp.height);
2211   // gtk_window_set_keep_above isn't enough on macOS
2212   gtk_window_set_attached_to(GTK_WINDOW(darktable.bauhaus->popup_window), GTK_WIDGET(darktable.bauhaus->current));
2213   gtk_widget_show_all(darktable.bauhaus->popup_window);
2214   gtk_widget_grab_focus(darktable.bauhaus->popup_area);
2215 }
2216 
dt_bauhaus_slider_add_delta_internal(GtkWidget * widget,float delta,guint state)2217 static gboolean dt_bauhaus_slider_add_delta_internal(GtkWidget *widget, float delta, guint state)
2218 {
2219   if (delta == 0) return TRUE;
2220 
2221   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2222   const dt_bauhaus_slider_data_t *d = &w->data.slider;
2223 
2224   float multiplier = 0.0f;
2225 
2226   if(dt_modifier_is(state, GDK_SHIFT_MASK))
2227   {
2228     multiplier = dt_conf_get_float("darkroom/ui/scale_rough_step_multiplier");
2229   }
2230   else if(dt_modifier_is(state, GDK_CONTROL_MASK))
2231   {
2232     multiplier = dt_conf_get_float("darkroom/ui/scale_precise_step_multiplier");
2233   }
2234   else
2235   {
2236     multiplier = dt_conf_get_float("darkroom/ui/scale_step_multiplier");
2237   }
2238 
2239   const float min_visible = powf(10.0f, -d->digits) / (d->max - d->min);
2240   if(fabsf(delta*multiplier) < min_visible)
2241     multiplier = min_visible / fabsf(delta);
2242 
2243   delta *= multiplier;
2244 
2245   if(w->module) dt_iop_request_focus(w->module);
2246 
2247   dt_bauhaus_slider_set_normalized(w, d->pos + delta);
2248 
2249   return TRUE;
2250 }
2251 
dt_bauhaus_slider_scroll(GtkWidget * widget,GdkEventScroll * event,gpointer user_data)2252 static gboolean dt_bauhaus_slider_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
2253 {
2254   const dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2255   if(w->type != DT_BAUHAUS_SLIDER) return FALSE;
2256 
2257   if(dt_gui_ignore_scroll(event)) return FALSE;
2258 
2259   gtk_widget_grab_focus(widget);
2260 
2261   int delta_y;
2262   if(dt_gui_get_scroll_unit_delta(event, &delta_y))
2263   {
2264     if(delta_y == 0) return TRUE;
2265     gdouble delta = delta_y * -w->data.slider.scale / 5.0;
2266     gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2267     return dt_bauhaus_slider_add_delta_internal(widget, delta, event->state);
2268   }
2269 
2270   return TRUE; // Ensure that scrolling the slider cannot move side panel
2271 }
2272 
dt_bauhaus_slider_key_press(GtkWidget * widget,GdkEventKey * event,gpointer user_data)2273 static gboolean dt_bauhaus_slider_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
2274 {
2275   const dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2276   if(w->type != DT_BAUHAUS_SLIDER) return FALSE;
2277   const dt_bauhaus_slider_data_t *d = &w->data.slider;
2278 
2279   int handled = 0;
2280   float delta = 0.0f;
2281   if(event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up || event->keyval == GDK_KEY_Right
2282      || event->keyval == GDK_KEY_KP_Right)
2283   {
2284     handled = 1;
2285     delta = d->scale / 5.0f;
2286   }
2287   else if(event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down || event->keyval == GDK_KEY_Left
2288           || event->keyval == GDK_KEY_KP_Left)
2289   {
2290     handled = 1;
2291     delta = -d->scale / 5.0f;
2292   }
2293 
2294   if(!handled) return dt_control_key_pressed(gdk_keyval_to_lower(event->keyval), dt_gui_translated_key_state(event));
2295 
2296   return dt_bauhaus_slider_add_delta_internal(widget, delta, event->state);
2297 }
2298 
2299 
dt_bauhaus_combobox_scroll(GtkWidget * widget,GdkEventScroll * event,gpointer user_data)2300 static gboolean dt_bauhaus_combobox_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
2301 {
2302   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2303   if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE;
2304 
2305   if(dt_gui_ignore_scroll(event)) return FALSE;
2306 
2307   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
2308   gtk_widget_grab_focus(widget);
2309 
2310   int delta_y = 0;
2311   if(dt_gui_get_scroll_unit_deltas(event, NULL, &delta_y))
2312   {
2313     if(w->module)
2314     {
2315       dt_iop_request_focus(w->module);
2316       gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2317     }
2318     // go to next sensitive one
2319     int new_pos = CLAMP(d->active + delta_y, 0, d->num_labels - 1);
2320     if(_combobox_next_entry(d->entries, &new_pos, delta_y))
2321       dt_bauhaus_combobox_set(widget, new_pos);
2322     return TRUE;
2323   }
2324   return TRUE; // Ensure that scrolling the combobox cannot move side panel
2325 }
2326 
dt_bauhaus_combobox_key_press(GtkWidget * widget,GdkEventKey * event,gpointer user_data)2327 static gboolean dt_bauhaus_combobox_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
2328 {
2329   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2330   if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE;
2331   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
2332   if(event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_KP_Up || event->keyval == GDK_KEY_Left
2333      || event->keyval == GDK_KEY_KP_Left)
2334   {
2335     if(w->module)
2336     {
2337       dt_iop_request_focus(w->module);
2338       gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2339     }
2340     // skip insensitive ones
2341     int new_pos = CLAMP(d->active - 1, 0, d->num_labels - 1);
2342     if(_combobox_next_entry(d->entries, &new_pos, -1))
2343       dt_bauhaus_combobox_set(widget, new_pos);
2344     return TRUE;
2345   }
2346   else if(event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down || event->keyval == GDK_KEY_Right
2347           || event->keyval == GDK_KEY_KP_Right)
2348   {
2349     if(w->module) dt_iop_request_focus(w->module);
2350     gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2351 
2352     // skip insensitive ones
2353     int new_pos = CLAMP(d->active + 1, 0, d->num_labels - 1);
2354     if(_combobox_next_entry(d->entries, &new_pos, 1))
2355       dt_bauhaus_combobox_set(widget, new_pos);
2356     return TRUE;
2357   }
2358   return dt_control_key_pressed(gdk_keyval_to_lower(event->keyval), dt_gui_translated_key_state(event));
2359 }
2360 
dt_bauhaus_combobox_button_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)2361 static gboolean dt_bauhaus_combobox_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2362 {
2363   GtkAllocation allocation;
2364   gtk_widget_get_allocation(widget, &allocation);
2365   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2366 
2367   if(w->type != DT_BAUHAUS_COMBOBOX) return FALSE;
2368   if(w->module) dt_iop_request_focus(w->module);
2369   gtk_widget_grab_focus(GTK_WIDGET(w));
2370   gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2371 
2372   GtkAllocation tmp;
2373   gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
2374   const dt_bauhaus_combobox_data_t *d = &w->data.combobox;
2375   if(w->quad_paint && (event->x > allocation.width - darktable.bauhaus->quad_width - INNER_PADDING))
2376   {
2377     if (w->quad_toggle)
2378     {
2379       if (w->quad_paint_flags & CPF_ACTIVE)
2380         w->quad_paint_flags &= ~CPF_ACTIVE;
2381       else
2382         w->quad_paint_flags |= CPF_ACTIVE;
2383     }
2384     g_signal_emit_by_name(G_OBJECT(w), "quad-pressed");
2385     return TRUE;
2386   }
2387   else if(event->button == 3)
2388   {
2389     darktable.bauhaus->mouse_x = event->x;
2390     darktable.bauhaus->mouse_y = event->y;
2391     dt_bauhaus_show_popup(w);
2392     return TRUE;
2393   }
2394   else if(event->button == 1)
2395   {
2396     // reset to default.
2397     if(event->type == GDK_2BUTTON_PRESS)
2398     {
2399       // never called, as we popup the other window under your cursor before.
2400       // (except in weird corner cases where the popup is under the -1st entry
2401       dt_bauhaus_combobox_set(widget, d->defpos);
2402       dt_bauhaus_hide_popup();
2403     }
2404     else
2405     {
2406       // single click, show options
2407       darktable.bauhaus->opentime = dt_get_wtime();
2408       darktable.bauhaus->mouse_x = event->x;
2409       darktable.bauhaus->mouse_y = event->y;
2410       dt_bauhaus_show_popup(w);
2411     }
2412     return TRUE;
2413   }
2414   return FALSE;
2415 }
2416 
dt_bauhaus_slider_get(GtkWidget * widget)2417 float dt_bauhaus_slider_get(GtkWidget *widget)
2418 {
2419   // first cast to bh widget, to check that type:
2420   const dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2421   if(w->type != DT_BAUHAUS_SLIDER) return -1.0f;
2422   const dt_bauhaus_slider_data_t *d = &w->data.slider;
2423   if(d->max == d->min)
2424   {
2425     return d->max;
2426   }
2427   const float rawval = d->curve(widget, d->pos, DT_BAUHAUS_GET);
2428   return d->min + rawval * (d->max - d->min);
2429 }
2430 
dt_bauhaus_slider_get_val(GtkWidget * widget)2431 float dt_bauhaus_slider_get_val(GtkWidget *widget)
2432 {
2433   const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(widget)->data.slider;
2434   return dt_bauhaus_slider_get(widget) * d->factor + d->offset;
2435 }
2436 
dt_bauhaus_slider_get_text(GtkWidget * w)2437 char *dt_bauhaus_slider_get_text(GtkWidget *w)
2438 {
2439   const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(w)->data.slider;
2440   return g_strdup_printf(d->format, dt_bauhaus_slider_get_val(w));
2441 }
2442 
dt_bauhaus_slider_set(GtkWidget * widget,float pos)2443 void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
2444 {
2445   // this is the public interface function, translate by bounds and call set_normalized
2446   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2447   if(w->type != DT_BAUHAUS_SLIDER) return;
2448   const dt_bauhaus_slider_data_t *d = &w->data.slider;
2449   const float rawval = (pos - d->min) / (d->max - d->min);
2450   dt_bauhaus_slider_set_normalized(w, d->curve(widget, rawval, DT_BAUHAUS_SET));
2451 }
2452 
dt_bauhaus_slider_set_val(GtkWidget * widget,float val)2453 void dt_bauhaus_slider_set_val(GtkWidget *widget, float val)
2454 {
2455   const dt_bauhaus_slider_data_t *d = &DT_BAUHAUS_WIDGET(widget)->data.slider;
2456   dt_bauhaus_slider_set_soft(widget, (val - d->offset) / d->factor);
2457 }
2458 
dt_bauhaus_slider_set_digits(GtkWidget * widget,int val)2459 void dt_bauhaus_slider_set_digits(GtkWidget *widget, int val)
2460 {
2461   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2462 
2463   if(w->type != DT_BAUHAUS_SLIDER) return;
2464 
2465   dt_bauhaus_slider_data_t *d = &w->data.slider;
2466 
2467   d->digits = val;
2468   snprintf(d->format, sizeof(d->format), "%%.0%df", val);
2469 }
2470 
dt_bauhaus_slider_get_digits(GtkWidget * widget)2471 int dt_bauhaus_slider_get_digits(GtkWidget *widget)
2472 {
2473   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2474 
2475   if(w->type != DT_BAUHAUS_SLIDER) return 0;
2476 
2477   const dt_bauhaus_slider_data_t *d = &w->data.slider;
2478 
2479   return d->digits;
2480 }
2481 
dt_bauhaus_slider_set_step(GtkWidget * widget,float val)2482 void dt_bauhaus_slider_set_step(GtkWidget *widget, float val)
2483 {
2484   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2485 
2486   if(w->type != DT_BAUHAUS_SLIDER) return;
2487 
2488   dt_bauhaus_slider_data_t *d = &w->data.slider;
2489 
2490   d->step = val;
2491   d->scale = 5.0f * d->step / (d->max - d->min);
2492 }
2493 
dt_bauhaus_slider_get_step(GtkWidget * widget)2494 float dt_bauhaus_slider_get_step(GtkWidget *widget)
2495 {
2496   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2497 
2498   if(w->type != DT_BAUHAUS_SLIDER) return 0;
2499 
2500   const dt_bauhaus_slider_data_t *d = &w->data.slider;
2501 
2502   return d->factor < 0 ? - d->step : d->step;
2503 }
2504 
dt_bauhaus_slider_set_feedback(GtkWidget * widget,int feedback)2505 void dt_bauhaus_slider_set_feedback(GtkWidget *widget, int feedback)
2506 {
2507   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2508 
2509   if(w->type != DT_BAUHAUS_SLIDER) return;
2510 
2511   dt_bauhaus_slider_data_t *d = &w->data.slider;
2512 
2513   d->fill_feedback = feedback;
2514 
2515   gtk_widget_queue_draw(widget);
2516 }
2517 
dt_bauhaus_slider_get_feedback(GtkWidget * widget)2518 int dt_bauhaus_slider_get_feedback(GtkWidget *widget)
2519 {
2520   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2521 
2522   if(w->type != DT_BAUHAUS_SLIDER) return 0;
2523 
2524   dt_bauhaus_slider_data_t *d = &w->data.slider;
2525 
2526   return d->fill_feedback;
2527 }
2528 
dt_bauhaus_slider_reset(GtkWidget * widget)2529 void dt_bauhaus_slider_reset(GtkWidget *widget)
2530 {
2531   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2532 
2533   if(w->type != DT_BAUHAUS_SLIDER) return;
2534   dt_bauhaus_slider_data_t *d = &w->data.slider;
2535 
2536   d->min = d->soft_min;
2537   d->max = d->soft_max;
2538   d->scale = 5.0f * d->step / (d->max - d->min);
2539 
2540   dt_bauhaus_slider_set_soft(widget, d->defpos);
2541 
2542   return;
2543 }
2544 
dt_bauhaus_slider_set_format(GtkWidget * widget,const char * format)2545 void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
2546 {
2547   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2548   if(w->type != DT_BAUHAUS_SLIDER) return;
2549   dt_bauhaus_slider_data_t *d = &w->data.slider;
2550   g_strlcpy(d->format, format, sizeof(d->format));
2551 }
2552 
dt_bauhaus_slider_set_factor(GtkWidget * widget,float factor)2553 void dt_bauhaus_slider_set_factor(GtkWidget *widget, float factor)
2554 {
2555   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2556   if(w->type != DT_BAUHAUS_SLIDER) return;
2557   dt_bauhaus_slider_data_t *d = &w->data.slider;
2558   d->factor = factor;
2559   if(factor < 0) d->curve = _reverse_linear_curve;
2560 }
2561 
dt_bauhaus_slider_set_offset(GtkWidget * widget,float offset)2562 void dt_bauhaus_slider_set_offset(GtkWidget *widget, float offset)
2563 {
2564   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2565   if(w->type != DT_BAUHAUS_SLIDER) return;
2566   dt_bauhaus_slider_data_t *d = &w->data.slider;
2567   d->offset = offset;
2568 }
2569 
dt_bauhaus_slider_set_curve(GtkWidget * widget,float (* curve)(GtkWidget * self,float value,dt_bauhaus_curve_t dir))2570 void dt_bauhaus_slider_set_curve(GtkWidget *widget, float (*curve)(GtkWidget *self, float value, dt_bauhaus_curve_t dir))
2571 {
2572   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2573   if(w->type != DT_BAUHAUS_SLIDER) return;
2574   dt_bauhaus_slider_data_t *d = &w->data.slider;
2575   if(curve == NULL) curve = _default_linear_curve;
2576 
2577   d->pos = curve(widget, d->curve(widget, d->pos  , DT_BAUHAUS_GET), DT_BAUHAUS_SET);
2578 
2579   d->curve = curve;
2580 }
2581 
dt_bauhaus_slider_set_soft(GtkWidget * widget,float pos)2582 void dt_bauhaus_slider_set_soft(GtkWidget *widget, float pos)
2583 {
2584   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)DT_BAUHAUS_WIDGET(widget);
2585   if(w->type != DT_BAUHAUS_SLIDER) return;
2586   dt_bauhaus_slider_data_t *d = &w->data.slider;
2587   const float rpos = CLAMP(pos, d->hard_min, d->hard_max);
2588   d->min = MIN(d->min, rpos);
2589   d->max = MAX(d->max, rpos);
2590   d->scale = 5.0f * d->step / (d->max - d->min);
2591   dt_bauhaus_slider_set(widget, rpos);
2592 }
2593 
dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t * w,float pos)2594 static void dt_bauhaus_slider_set_normalized(dt_bauhaus_widget_t *w, float pos)
2595 {
2596   dt_bauhaus_slider_data_t *d = &w->data.slider;
2597   float rpos = CLAMP(pos, 0.0f, 1.0f);
2598   rpos = d->curve(GTK_WIDGET(w), rpos, DT_BAUHAUS_GET);
2599   rpos = d->min + (d->max - d->min) * rpos;
2600   const float base = powf(10.0f, d->digits);
2601   rpos = roundf(base * rpos) / base;
2602   rpos = (rpos - d->min) / (d->max - d->min);
2603   d->pos = d->curve(GTK_WIDGET(w), rpos, DT_BAUHAUS_SET);
2604   gtk_widget_queue_draw(GTK_WIDGET(w));
2605   d->is_changed = 1;
2606   if(!darktable.gui->reset && !d->is_dragging)
2607   {
2608     g_signal_emit_by_name(G_OBJECT(w), "value-changed");
2609     d->is_changed = 0;
2610   }
2611 }
2612 
dt_bauhaus_slider_postponed_value_change(gpointer data)2613 static gboolean dt_bauhaus_slider_postponed_value_change(gpointer data)
2614 {
2615   if(!GTK_IS_WIDGET(data)) return 0;
2616 
2617   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)data;
2618   dt_bauhaus_slider_data_t *d = &w->data.slider;
2619   if(d->is_changed)
2620   {
2621     g_signal_emit_by_name(G_OBJECT(w), "value-changed");
2622     d->is_changed = 0;
2623   }
2624 
2625   if(!d->is_dragging) d->timeout_handle = 0;
2626   else
2627   {
2628     const int delay = CLAMP(darktable.develop->average_delay * 3 / 2,
2629                             DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MIN,
2630                             DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MAX);
2631     d->timeout_handle = g_timeout_add(delay, dt_bauhaus_slider_postponed_value_change, data);
2632   }
2633 
2634   return FALSE;
2635 }
2636 
dt_bauhaus_popup_key_press(GtkWidget * widget,GdkEventKey * event,gpointer user_data)2637 static gboolean dt_bauhaus_popup_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
2638 {
2639   switch(darktable.bauhaus->current->type)
2640   {
2641     case DT_BAUHAUS_SLIDER:
2642     {
2643       // hack to do screenshots from popup:
2644       // if(event->string[0] == 'p') return system("scrot");
2645       // else
2646       if(darktable.bauhaus->keys_cnt + 2 < 64
2647          && (event->keyval == GDK_KEY_space || event->keyval == GDK_KEY_KP_Space ||              // SPACE
2648              event->keyval == GDK_KEY_percent ||                                                 // %
2649              (event->string[0] >= 40 && event->string[0] <= 57) ||                               // ()+-*/.,0-9
2650              event->keyval == GDK_KEY_asciicircum || event->keyval == GDK_KEY_dead_circumflex || // ^
2651              event->keyval == GDK_KEY_X || event->keyval == GDK_KEY_x))                          // Xx
2652       {
2653         if(event->keyval == GDK_KEY_dead_circumflex)
2654           darktable.bauhaus->keys[darktable.bauhaus->keys_cnt++] = '^';
2655         else
2656           darktable.bauhaus->keys[darktable.bauhaus->keys_cnt++] = event->string[0];
2657         gtk_widget_queue_draw(darktable.bauhaus->popup_area);
2658       }
2659       else if(darktable.bauhaus->keys_cnt > 0
2660               && (event->keyval == GDK_KEY_BackSpace || event->keyval == GDK_KEY_Delete))
2661       {
2662         darktable.bauhaus->keys[--darktable.bauhaus->keys_cnt] = 0;
2663         gtk_widget_queue_draw(darktable.bauhaus->popup_area);
2664       }
2665       else if(darktable.bauhaus->keys_cnt > 0 && darktable.bauhaus->keys_cnt + 1 < 64
2666               && (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter))
2667       {
2668         // accept input
2669         darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
2670         // unnormalized input, user was typing this:
2671         const float old_value = dt_bauhaus_slider_get_val(GTK_WIDGET(darktable.bauhaus->current));
2672         const float new_value = dt_calculator_solve(old_value, darktable.bauhaus->keys);
2673         if(isfinite(new_value)) dt_bauhaus_slider_set_val(GTK_WIDGET(darktable.bauhaus->current), new_value);
2674         darktable.bauhaus->keys_cnt = 0;
2675         memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
2676         dt_bauhaus_hide_popup();
2677       }
2678       else if(event->keyval == GDK_KEY_Escape)
2679       {
2680         // discard input and close popup
2681         darktable.bauhaus->keys_cnt = 0;
2682         memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
2683         dt_bauhaus_hide_popup();
2684       }
2685       else
2686         return FALSE;
2687       if(darktable.bauhaus->keys_cnt > 0) _start_cursor(-1);
2688       return TRUE;
2689     }
2690     case DT_BAUHAUS_COMBOBOX:
2691     {
2692       if(!g_utf8_validate(event->string, -1, NULL)) return FALSE;
2693       const gunichar c = g_utf8_get_char(event->string);
2694       const long int char_width = g_utf8_next_char(event->string) - event->string;
2695       // if(event->string[0] == 'p') return system("scrot");
2696       // else
2697       if(darktable.bauhaus->keys_cnt + 1 + char_width < 64 && g_unichar_isprint(c))
2698       {
2699         // only accept key input if still valid or editable?
2700         g_utf8_strncpy(darktable.bauhaus->keys + darktable.bauhaus->keys_cnt, event->string, 1);
2701         darktable.bauhaus->keys_cnt += char_width;
2702         gtk_widget_queue_draw(darktable.bauhaus->popup_area);
2703       }
2704       else if(darktable.bauhaus->keys_cnt > 0
2705               && (event->keyval == GDK_KEY_BackSpace || event->keyval == GDK_KEY_Delete))
2706       {
2707         darktable.bauhaus->keys_cnt
2708             -= (darktable.bauhaus->keys + darktable.bauhaus->keys_cnt)
2709                - g_utf8_prev_char(darktable.bauhaus->keys + darktable.bauhaus->keys_cnt);
2710         darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
2711         gtk_widget_queue_draw(darktable.bauhaus->popup_area);
2712       }
2713       else if(darktable.bauhaus->keys_cnt > 0 && darktable.bauhaus->keys_cnt + 1 < 64
2714               && (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter))
2715       {
2716         // accept unique matches only for editable:
2717         if(darktable.bauhaus->current->data.combobox.editable)
2718           darktable.bauhaus->end_mouse_y = FLT_MAX;
2719         else
2720           darktable.bauhaus->end_mouse_y = 0;
2721         darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
2722         dt_bauhaus_widget_accept(darktable.bauhaus->current);
2723         darktable.bauhaus->keys_cnt = 0;
2724         memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
2725         dt_bauhaus_hide_popup();
2726       }
2727       else if(event->keyval == GDK_KEY_Escape)
2728       {
2729         // discard input and close popup
2730         darktable.bauhaus->keys_cnt = 0;
2731         memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
2732         dt_bauhaus_hide_popup();
2733       }
2734       else if(event->keyval == GDK_KEY_Up)
2735       {
2736         combobox_popup_scroll(-1);
2737       }
2738       else if(event->keyval == GDK_KEY_Down)
2739       {
2740         combobox_popup_scroll(1);
2741       }
2742       else if(event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter)
2743       {
2744         // return pressed, but didn't type anything
2745         darktable.bauhaus->end_mouse_y = -1; // negative will use currently highlighted instead.
2746         darktable.bauhaus->keys[darktable.bauhaus->keys_cnt] = 0;
2747         darktable.bauhaus->keys_cnt = 0;
2748         memset(darktable.bauhaus->keys, 0, sizeof(darktable.bauhaus->keys));
2749         dt_bauhaus_widget_accept(darktable.bauhaus->current);
2750         dt_bauhaus_hide_popup();
2751       }
2752       else
2753         return FALSE;
2754       return TRUE;
2755     }
2756     default:
2757       return FALSE;
2758   }
2759 }
2760 
dt_bauhaus_slider_button_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)2761 static gboolean dt_bauhaus_slider_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2762 {
2763   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2764   if(w->module) dt_iop_request_focus(w->module);
2765   gtk_widget_grab_focus(GTK_WIDGET(w));
2766   gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2767 
2768   GtkAllocation allocation;
2769   gtk_widget_get_allocation(widget, &allocation);
2770   if(event->x > allocation.width - darktable.bauhaus->quad_width - INNER_PADDING)
2771   {
2772     if (w->quad_paint && w->quad_toggle)
2773     {
2774       if (w->quad_paint_flags & CPF_ACTIVE)
2775         w->quad_paint_flags &= ~CPF_ACTIVE;
2776       else
2777         w->quad_paint_flags |= CPF_ACTIVE;
2778     }
2779     g_signal_emit_by_name(G_OBJECT(w), "quad-pressed");
2780     return TRUE;
2781   }
2782   else if(event->button == 3)
2783   {
2784     dt_bauhaus_show_popup(w);
2785     return TRUE;
2786   }
2787   else if(event->button == 1)
2788   {
2789     // reset to default.
2790     if(event->type == GDK_2BUTTON_PRESS)
2791     {
2792       dt_bauhaus_slider_data_t *d = &w->data.slider;
2793       d->is_dragging = 0;
2794       dt_bauhaus_slider_reset(GTK_WIDGET(w));
2795     }
2796     else
2797     {
2798       const float l = 0.0f;
2799       const float r = slider_right_pos((float)allocation.width);
2800       dt_bauhaus_slider_set_normalized(w, (event->x / allocation.width - l) / (r - l));
2801       dt_bauhaus_slider_data_t *d = &w->data.slider;
2802       d->is_dragging = 1;
2803       int delay = CLAMP(darktable.develop->average_delay * 3 / 2, DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MIN,
2804                         DT_BAUHAUS_SLIDER_VALUE_CHANGED_DELAY_MAX);
2805       // timeout_handle should always be zero here, but check just in case
2806       if(!d->timeout_handle)
2807         d->timeout_handle = g_timeout_add(delay, dt_bauhaus_slider_postponed_value_change, widget);
2808     }
2809     return TRUE;
2810   }
2811   return FALSE;
2812 }
2813 
dt_bauhaus_slider_button_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)2814 static gboolean dt_bauhaus_slider_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2815 {
2816   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2817   dt_bauhaus_slider_data_t *d = &w->data.slider;
2818 
2819   if((event->button == 1) && (d->is_dragging))
2820   {
2821     if(w->module) dt_iop_request_focus(w->module);
2822     gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2823 
2824     GtkAllocation tmp;
2825     gtk_widget_get_allocation(GTK_WIDGET(w), &tmp);
2826     d->is_dragging = 0;
2827     if(d->timeout_handle) g_source_remove(d->timeout_handle);
2828     d->timeout_handle = 0;
2829     const float l = 0.0f;
2830     const float r = slider_right_pos((float)tmp.width);
2831     dt_bauhaus_slider_set_normalized(w, (event->x / tmp.width - l) / (r - l));
2832 
2833     return TRUE;
2834   }
2835   return FALSE;
2836 }
2837 
dt_bauhaus_slider_motion_notify(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)2838 static gboolean dt_bauhaus_slider_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
2839 {
2840   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2841   dt_bauhaus_slider_data_t *d = &w->data.slider;
2842 
2843   GtkAllocation allocation;
2844   gtk_widget_get_allocation(widget, &allocation);
2845   if(d->is_dragging || event->x <= allocation.width - darktable.bauhaus->quad_width)
2846   {
2847     // remember mouse position for motion effects in draw
2848     if(event->state & GDK_BUTTON1_MASK && event->type != GDK_2BUTTON_PRESS)
2849     {
2850       if(w->module) dt_iop_request_focus(w->module);
2851       gtk_widget_set_state_flags(GTK_WIDGET(w), GTK_STATE_FLAG_FOCUSED, TRUE);
2852       const float l = 0.0f;
2853       const float r = slider_right_pos((float)allocation.width);
2854       dt_bauhaus_slider_set_normalized(w, (event->x / allocation.width - l) / (r - l));
2855     }
2856   }
2857   return TRUE;
2858 }
2859 
dt_bauhaus_vimkey_exec(const char * input)2860 void dt_bauhaus_vimkey_exec(const char *input)
2861 {
2862   char module[64], label[64], value[256], *key;
2863   float old_value, new_value = 0.0f;
2864 
2865   sscanf(input, ":set %63[^.].%63[^=]=%255s", module, label, value);
2866   fprintf(stderr, "[vimkey] setting module `%s', slider `%s' to `%s'", module, label, value);
2867   key = g_strjoin(".", module, label, NULL);
2868   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)g_hash_table_lookup(darktable.bauhaus->keymap, key);
2869   g_free(key);
2870   if(!w) return;
2871   switch(w->type)
2872   {
2873     case DT_BAUHAUS_SLIDER:
2874       old_value = dt_bauhaus_slider_get(GTK_WIDGET(w));
2875       new_value = dt_calculator_solve(old_value, value);
2876       fprintf(stderr, " = %f\n", new_value);
2877       if(isfinite(new_value)) dt_bauhaus_slider_set_soft(GTK_WIDGET(w), new_value);
2878       break;
2879     case DT_BAUHAUS_COMBOBOX:
2880       // TODO: what about text as entry?
2881       old_value = dt_bauhaus_combobox_get(GTK_WIDGET(w));
2882       new_value = dt_calculator_solve(old_value, value);
2883       fprintf(stderr, " = %f\n", new_value);
2884       if(isfinite(new_value)) dt_bauhaus_combobox_set(GTK_WIDGET(w), new_value);
2885       break;
2886     default:
2887       break;
2888   }
2889 }
2890 
2891 // give autocomplete suggestions
dt_bauhaus_vimkey_complete(const char * input)2892 GList *dt_bauhaus_vimkey_complete(const char *input)
2893 {
2894   char *point = strstr(input, ".");
2895   const int prefix = strlen(input);
2896   GList *cmp = point ? darktable.bauhaus->key_val : darktable.bauhaus->key_mod;
2897   GList *res = NULL;
2898   int after = 0;
2899   for( ; cmp ; cmp = g_list_next(cmp))
2900   {
2901     char *path = (char *)cmp->data;
2902     if(strncasecmp(path, input, prefix))
2903     {
2904       if(after) break; // sorted, so we're done
2905                        // else loop till we find the start of it
2906     }
2907     else
2908     {
2909       // append:
2910       res = g_list_insert_sorted(res, path, (GCompareFunc)strcmp);
2911       after = 1;
2912     }
2913   }
2914   return res;
2915 }
2916 
dt_bauhaus_combobox_mute_scrolling(GtkWidget * widget)2917 void dt_bauhaus_combobox_mute_scrolling(GtkWidget *widget)
2918 {
2919   dt_bauhaus_widget_t *w = (dt_bauhaus_widget_t *)widget;
2920   dt_bauhaus_combobox_data_t *d = &w->data.combobox;
2921   d->mute_scrolling = TRUE;
2922 }
2923 
2924 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
2925 // vim: shiftwidth=2 expandtab tabstop=2 cindent
2926 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
2927