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