1 /*
2     This file is part of darktable,
3     Copyright (C) 2011-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 "gui/accelerators.h"
20 #include "common/action.h"
21 #include "common/darktable.h"
22 #include "common/debug.h"
23 #include "common/file_location.h"
24 #include "common/utility.h"
25 #include "control/control.h"
26 #include "develop/blend.h"
27 #include "gui/presets.h"
28 #include "dtgtk/expander.h"
29 
30 #include <assert.h>
31 #include <gtk/gtk.h>
32 #ifdef GDK_WINDOWING_QUARTZ
33 #include "osx/osx.h"
34 #endif
35 
36 typedef struct dt_shortcut_t
37 {
38   dt_view_type_flags_t views;
39 
40   dt_input_device_t key_device;
41   guint key;
42   guint mods;
43   guint press     : 3;
44   guint button    : 3;
45   guint click     : 3;
46   guint direction : 2;
47   dt_input_device_t move_device;
48   dt_shortcut_move_t move;
49 
50   dt_action_t *action;
51 
52   dt_action_element_t element;
53   dt_action_effect_t effect;
54   float speed;
55   int instance; // 0 is from prefs, >0 counting from first, <0 counting from last
56 } dt_shortcut_t;
57 
58 
59 typedef struct dt_device_key_t
60 {
61   dt_input_device_t key_device;
62   guint key;
63   const dt_action_def_t *hold_def;
64   dt_action_element_t hold_element;
65 } dt_device_key_t;
66 
67 typedef struct dt_action_target_t
68 {
69   dt_action_t *action;
70   void *target;
71 } dt_action_target_t;
72 
73 #define DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE 0
74 
75 const char *move_string[]
76   = { "",
77       N_("scroll"),
78       N_("pan"),
79       N_("horizontal"),
80       N_("vertical"),
81       N_("diagonal"),
82       N_("skew"),
83       N_("leftright"),
84       N_("updown"),
85       N_("pgupdown"),
86       NULL };
87 
88 const struct _modifier_name
89 {
90   GdkModifierType modifier;
91   char           *name;
92 } modifier_string[]
93   = { { GDK_SHIFT_MASK  , N_("shift") },
94       { GDK_CONTROL_MASK, N_("ctrl" ) },
95       { GDK_MOD1_MASK   , N_("alt"  ) },
96       { GDK_MOD2_MASK   , N_("cmd"  ) },
97       { GDK_MOD5_MASK   , N_("altgr") },
98       { 0, NULL } };
99 
100 static dt_shortcut_t _sc = { 0 };  //  shortcut under construction
101 static guint _previous_move = DT_SHORTCUT_MOVE_NONE;
102 
103 const gchar *dt_action_effect_value[]
104   = { N_("edit"),
105       N_("up"),
106       N_("down"),
107       N_("reset"),
108       N_("top"),
109       N_("bottom"),
110       N_("set"),
111       NULL };
112 const gchar *dt_action_effect_selection[]
113   = { N_("popup"),
114       N_("next"),
115       N_("previous"),
116       N_("reset"),
117       N_("last"),
118       N_("first"),
119       NULL };
120 const gchar *dt_action_effect_toggle[]
121   = { N_("toggle"),
122       N_("on"),
123       N_("off"),
124       N_("ctrl-toggle"),
125       N_("ctrl-on"),
126       N_("right-toggle"),
127       N_("right-on"),
128       NULL };
129 const gchar *dt_action_effect_hold[]
130   = { N_("hold"),
131       N_("on"),
132       N_("off"),
133       N_("toggle"),
134       NULL };
135 const gchar *dt_action_effect_activate[]
136   = { N_("activate"),
137       N_("ctrl-activate"),
138       N_("right-activate"),
139       NULL };
140 const gchar *dt_action_effect_presets[]
141   = { N_("show"),
142       N_("previous"),
143       N_("next"),
144       N_("store"),
145       N_("delete"),
146       N_("edit"),
147       N_("update"),
148       N_("preferences"),
149       NULL };
150 const gchar *dt_action_effect_preset_iop[]
151   = { N_("apply"),
152       N_("apply on new instance"),
153       NULL };
154 
155 const dt_action_element_def_t dt_action_elements_hold[]
156   = { { NULL, dt_action_effect_hold } };
157 
158 const dt_action_element_def_t _action_elements_toggle[]
159   = { { NULL, dt_action_effect_toggle } };
160 const dt_action_element_def_t _action_elements_button[]
161   = { { NULL, dt_action_effect_activate } };
162 const dt_action_element_def_t _action_elements_value_fallback[]
163   = { { NULL, dt_action_effect_value } };
164 
_action_process_toggle(gpointer target,dt_action_element_t element,dt_action_effect_t effect,float move_size)165 static float _action_process_toggle(gpointer target, dt_action_element_t element, dt_action_effect_t effect, float move_size)
166 {
167   float value = gtk_toggle_button_get_active(target);
168 
169   if(!isnan(move_size) &&
170      !((effect == DT_ACTION_EFFECT_ON      ||
171         effect == DT_ACTION_EFFECT_ON_CTRL ||
172         effect == DT_ACTION_EFFECT_ON_RIGHT) && value) &&
173      (effect != DT_ACTION_EFFECT_OFF         || value))
174   {
175     GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS);
176     event->button.state = (effect == DT_ACTION_EFFECT_TOGGLE_CTRL ||
177                            effect == DT_ACTION_EFFECT_ON_CTRL)
178                         ? GDK_CONTROL_MASK : 0;
179     event->button.button = (effect == DT_ACTION_EFFECT_TOGGLE_RIGHT ||
180                             effect == DT_ACTION_EFFECT_ON_RIGHT)
181                          ? GDK_BUTTON_SECONDARY : GDK_BUTTON_PRIMARY;
182 
183     if(!gtk_widget_get_realized(target)) gtk_widget_realize(target);
184     event->button.window = gtk_widget_get_window(target);
185     g_object_ref(event->button.window);
186 
187     // some togglebuttons connect to the clicked signal, others to toggled or button-press-event
188     if(!gtk_widget_event(target, event))
189       gtk_button_clicked(GTK_BUTTON(target));
190     event->type = GDK_BUTTON_RELEASE;
191     gtk_widget_event(target, event);
192 
193     gdk_event_free(event);
194 
195     value = gtk_toggle_button_get_active(target);
196 
197     if(!gtk_widget_is_visible(target))
198       dt_action_widget_toast(NULL, target, value ? _("on") : _("off"));
199   }
200 
201   return value;
202 }
203 
_action_process_button(gpointer target,dt_action_element_t element,dt_action_effect_t effect,float move_size)204 static float _action_process_button(gpointer target, dt_action_element_t element, dt_action_effect_t effect, float move_size)
205 {
206   if(!gtk_widget_get_realized(target)) gtk_widget_realize(target);
207 
208   if(!isnan(move_size) && gtk_widget_is_sensitive(target))
209   {
210     if(effect != DT_ACTION_EFFECT_ACTIVATE || !gtk_widget_activate(GTK_WIDGET(target)))
211     {
212       GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS);
213       event->button.state = effect == DT_ACTION_EFFECT_ACTIVATE_CTRL
214                           ? GDK_CONTROL_MASK : 0;
215       event->button.button = effect == DT_ACTION_EFFECT_ACTIVATE_RIGHT
216                           ? GDK_BUTTON_SECONDARY : GDK_BUTTON_PRIMARY;
217 
218       event->button.window = gtk_widget_get_window(target);
219       g_object_ref(event->button.window);
220 
221       gtk_widget_event(target, event);
222       event->type = GDK_BUTTON_RELEASE;
223       gtk_widget_event(target, event);
224 
225       gdk_event_free(event);
226     }
227   }
228 
229   return NAN;
230 }
231 
232 static const dt_shortcut_fallback_t _action_fallbacks_toggle[]
233   = { { .mods = GDK_CONTROL_MASK    , .effect = DT_ACTION_EFFECT_TOGGLE_CTRL  },
234       { .button = DT_SHORTCUT_RIGHT , .effect = DT_ACTION_EFFECT_TOGGLE_RIGHT },
235       { .press = DT_SHORTCUT_LONG   , .effect = DT_ACTION_EFFECT_TOGGLE_RIGHT },
236       { } };
237 
238 const dt_action_def_t dt_action_def_toggle
239   = { N_("toggle"),
240       _action_process_toggle,
241       _action_elements_toggle,
242       _action_fallbacks_toggle };
243 
244 static const dt_shortcut_fallback_t _action_fallbacks_button[]
245   = { { .mods = GDK_CONTROL_MASK    , .effect = DT_ACTION_EFFECT_ACTIVATE_CTRL  },
246       { .button = DT_SHORTCUT_RIGHT , .effect = DT_ACTION_EFFECT_ACTIVATE_RIGHT },
247       { .press = DT_SHORTCUT_LONG   , .effect = DT_ACTION_EFFECT_ACTIVATE_RIGHT },
248       { } };
249 
250 const dt_action_def_t dt_action_def_button
251   = { N_("button"),
252       _action_process_button,
253       _action_elements_button,
254       _action_fallbacks_button };
255 
256 static const dt_shortcut_fallback_t _action_fallbacks_value[]
257   = { { .mods = GDK_CONTROL_MASK           , .effect = -1, .speed = 0.1 },
258       { .mods = GDK_SHIFT_MASK             , .effect = -1, .speed = 10. },
259       { .move = DT_SHORTCUT_MOVE_HORIZONTAL, .effect = -1, .speed = 0.1 },
260       { .move = DT_SHORTCUT_MOVE_VERTICAL  , .effect = -1, .speed = 10. },
261       { .effect = DT_ACTION_EFFECT_RESET   , .button = DT_SHORTCUT_LEFT, .click = DT_SHORTCUT_DOUBLE },
262       { .effect = DT_ACTION_EFFECT_TOP     , .button = DT_SHORTCUT_LEFT, .click = DT_SHORTCUT_DOUBLE, .move = DT_SHORTCUT_MOVE_VERTICAL, .direction = DT_SHORTCUT_UP },
263       { .effect = DT_ACTION_EFFECT_BOTTOM  , .button = DT_SHORTCUT_LEFT, .click = DT_SHORTCUT_DOUBLE, .move = DT_SHORTCUT_MOVE_VERTICAL, .direction = DT_SHORTCUT_DOWN },
264       { } };
265 
266 const dt_action_def_t dt_action_def_value
267   = { N_("value"),
268       NULL,
269       _action_elements_value_fallback,
270       _action_fallbacks_value };
271 
272 const dt_action_def_t _action_def_dummy
273   = { };
274 
_action_find_definition(dt_action_t * action)275 static const dt_action_def_t *_action_find_definition(dt_action_t *action)
276 {
277   if(!action) return NULL;
278 
279   dt_action_type_t type = action->type != DT_ACTION_TYPE_FALLBACK
280                         ? action->type : GPOINTER_TO_INT(action->target);
281   const int index = type - DT_ACTION_TYPE_WIDGET - 1;
282 
283   if(index >= 0 && index < darktable.control->widget_definitions->len)
284     return darktable.control->widget_definitions->pdata[index];
285   else if(type == DT_ACTION_TYPE_IOP)
286     return &dt_action_def_iop;
287   else if(type == DT_ACTION_TYPE_LIB)
288     return &dt_action_def_lib;
289   else if(type == DT_ACTION_TYPE_VALUE_FALLBACK)
290     return &dt_action_def_value;
291   else
292     return NULL;
293 }
294 
_action_find_elements(dt_action_t * action)295 static const dt_action_element_def_t *_action_find_elements(dt_action_t *action)
296 {
297   const dt_action_def_t *definition = _action_find_definition(action);
298 
299   if(!definition)
300     return NULL;
301   else
302     return definition->elements;
303 }
304 
_shortcut_compare_func(gconstpointer shortcut_a,gconstpointer shortcut_b,gpointer user_data)305 static gint _shortcut_compare_func(gconstpointer shortcut_a, gconstpointer shortcut_b, gpointer user_data)
306 {
307   const dt_shortcut_t *a = (const dt_shortcut_t *)shortcut_a;
308   const dt_shortcut_t *b = (const dt_shortcut_t *)shortcut_b;
309 
310   dt_view_type_flags_t active_view = GPOINTER_TO_INT(user_data);
311   const int a_in_view = a->views ? a->views & active_view : -1; // put fallbacks last
312   const int b_in_view = b->views ? b->views & active_view : -1; // put fallbacks last
313 
314   if(a_in_view != b_in_view)
315     // reverse order; in current view first, fallbacks last
316     return b_in_view - a_in_view;
317   if(!a->views && a->action && b->action && a->action->target != b->action->target)
318     // order fallbacks by referred type
319     return GPOINTER_TO_INT(a->action->target) - GPOINTER_TO_INT(b->action->target);
320   if(a->key_device != b->key_device)
321     return a->key_device - b->key_device;
322   if(a->key != b->key)
323     return a->key - b->key;
324   if(a->press != b->press)
325     return a->press - b->press;
326   if(a->move_device != b->move_device)
327     return a->move_device - b->move_device;
328   if(a->move != b->move)
329     return a->move - b->move;
330   if(a->mods != b->mods)
331     return a->mods - b->mods;
332   if(a->button != b->button)
333     return a->button - b->button;
334   if(a->click != b->click)
335     return a->click - b->click;
336   if((a->direction | b->direction) == (DT_SHORTCUT_UP | DT_SHORTCUT_DOWN))
337     return a->direction - b->direction;
338 
339   return 0;
340 };
341 
_action_full_id(dt_action_t * action)342 static gchar *_action_full_id(dt_action_t *action)
343 {
344   if(action->owner)
345   {
346     gchar *owner_id = _action_full_id(action->owner);
347     gchar *full_label = g_strdup_printf("%s/%s", owner_id, action->id);
348     g_free(owner_id);
349     return full_label;
350   }
351   else
352     return g_strdup(action->id);
353 }
354 
_action_full_label(dt_action_t * action)355 static gchar *_action_full_label(dt_action_t *action)
356 {
357   if(action->owner)
358   {
359     gchar *owner_label = _action_full_label(action->owner);
360     gchar *full_label = g_strdup_printf("%s/%s", owner_label, action->label);
361     g_free(owner_label);
362     return full_label;
363   }
364   else
365     return g_strdup(action->label);
366 }
367 
_action_distinct_label(gchar ** label,dt_action_t * action,gchar * instance)368 static void _action_distinct_label(gchar **label, dt_action_t *action, gchar *instance)
369 {
370   if(!action || action->type <= DT_ACTION_TYPE_GLOBAL)
371     return;
372 
373   gchar *instance_label = action->type == DT_ACTION_TYPE_IOP && *instance
374                         ? g_strdup_printf("%s %s", action->label, instance)
375                         : g_strdup(action->label);
376 
377   if(*label)
378   {
379     if(!strstr(action->label, *label) || *instance)
380     {
381       gchar *distinct_label = g_strdup_printf("%s / %s", instance_label, *label);
382       g_free(*label);
383       *label = distinct_label;
384     }
385 
386     g_free(instance_label);
387   }
388   else
389     *label = instance_label;
390 
391   _action_distinct_label(label, action->owner, instance);
392 }
393 
_dump_actions(FILE * f,dt_action_t * action)394 static void _dump_actions(FILE *f, dt_action_t *action)
395 {
396   while(action)
397   {
398     gchar *label = _action_full_id(action);
399     fprintf(f, "%s %s %d\n", label, !action->target ? "*" : "", action->type);
400     g_free(label);
401     if(action->type <= DT_ACTION_TYPE_SECTION)
402       _dump_actions(f, action->target);
403     action = action->next;
404   }
405 }
406 
dt_register_input_driver(dt_lib_module_t * module,const dt_input_driver_definition_t * callbacks)407 dt_input_device_t dt_register_input_driver(dt_lib_module_t *module, const dt_input_driver_definition_t *callbacks)
408 {
409   dt_input_device_t id = 10;
410 
411   for(GSList *d = darktable.control->input_drivers; d; d = d->next, id += 10)
412     if(((dt_input_driver_definition_t *)d->data)->module == module) return id;
413 
414   dt_input_driver_definition_t *new_driver = calloc(1, sizeof(dt_input_driver_definition_t));
415   *new_driver = *callbacks;
416   new_driver->module = module;
417   darktable.control->input_drivers = g_slist_append(darktable.control->input_drivers, (gpointer)new_driver);
418 
419   return id;
420 }
421 
422 #define DT_MOVE_NAME -1
_shortcut_key_move_name(dt_input_device_t id,guint key_or_move,guint mods,gboolean display)423 static gchar *_shortcut_key_move_name(dt_input_device_t id, guint key_or_move, guint mods, gboolean display)
424 {
425   gchar *name = NULL, *post_name = NULL;
426   if(id == DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE)
427   {
428     if(mods == DT_MOVE_NAME)
429       return g_strdup(display && key_or_move != 0 ? _(move_string[key_or_move]) : move_string[key_or_move]);
430     else
431     {
432       if(display)
433       {
434         gchar *key_name = gtk_accelerator_get_label(key_or_move, 0);
435         post_name = g_utf8_strdown(key_name, -1);
436         g_free(key_name);
437       }
438       else
439         name = key_or_move ? gtk_accelerator_name(key_or_move, 0) : g_strdup("None");
440     }
441   }
442   else
443   {
444     GSList *driver = darktable.control->input_drivers;
445     while(driver && (id -= 10) >= 10)
446       driver = driver->next;
447 
448     if(!driver)
449       name = g_strdup(_("unknown driver"));
450     else
451     {
452       dt_input_driver_definition_t *callbacks = driver->data;
453       gchar *without_device
454         = mods == DT_MOVE_NAME
455         ? callbacks->move_to_string(key_or_move, display)
456         : callbacks->key_to_string(key_or_move, display);
457 
458       if(display && id == 0)
459         post_name = without_device;
460       else
461       {
462         char id_str[2] = "\0\0";
463         if(id) id_str[0] = '0' + id;
464 
465         name = g_strdup_printf("%s%s:%s", display ? "" : callbacks->name, id_str, without_device);
466         g_free(without_device);
467       }
468     }
469   }
470   if(mods != DT_MOVE_NAME)
471   {
472     for(const struct _modifier_name *mod_str = modifier_string;
473         mod_str->modifier;
474         mod_str++)
475     {
476       if(mods & mod_str->modifier)
477       {
478         gchar *save_name = name;
479         name = display
480              ? g_strdup_printf("%s%s+", name ? name : "", _(mod_str->name))
481              : g_strdup_printf("%s;%s", name ? name : "",   mod_str->name);
482         g_free(save_name);
483       }
484     }
485   }
486 
487   if(post_name)
488   {
489     gchar *save_name = name;
490     name = g_strdup_printf("%s%s", name ? name : "", post_name);
491     g_free(save_name);
492     g_free(post_name);
493   }
494 
495   return name;
496 }
497 
_shortcut_is_move(dt_shortcut_t * s)498 static gboolean _shortcut_is_move(dt_shortcut_t *s)
499 {
500   return (s->move_device != DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE ||
501           s->move != DT_SHORTCUT_MOVE_NONE) && !s->direction;
502 }
503 
_shortcut_description(dt_shortcut_t * s)504 static gchar *_shortcut_description(dt_shortcut_t *s)
505 {
506   static gchar hint[1024];
507   int length = 0;
508 
509 #define add_hint(format, ...) length += length >= sizeof(hint) ? 0 : snprintf(hint + length, sizeof(hint) - length, format, ##__VA_ARGS__)
510 
511   gchar *key_name = _shortcut_key_move_name(s->key_device, s->key, s->mods, TRUE);
512   gchar *move_name = _shortcut_key_move_name(s->move_device, s->move, DT_MOVE_NAME, TRUE);
513 
514   add_hint("%s%s", key_name, s->key_device || s->key ? "" : move_name);
515 
516   if(s->press & DT_SHORTCUT_DOUBLE) add_hint(" %s", _("double"));
517   if(s->press & DT_SHORTCUT_TRIPLE) add_hint(" %s", _("triple"));
518   if(s->press & DT_SHORTCUT_LONG  ) add_hint(" %s", _("long"));
519   if(s->press) add_hint(" %s", _("press"));
520   if(s->button)
521   {
522     if(*key_name || *move_name) add_hint(",");
523     if(s->button & DT_SHORTCUT_LEFT  ) add_hint(" %s", C_("accel", "left"));
524     if(s->button & DT_SHORTCUT_RIGHT ) add_hint(" %s", C_("accel", "right"));
525     if(s->button & DT_SHORTCUT_MIDDLE) add_hint(" %s", C_("accel", "middle"));
526     if(s->click  & DT_SHORTCUT_DOUBLE) add_hint(" %s", C_("accel", "double"));
527     if(s->click  & DT_SHORTCUT_TRIPLE) add_hint(" %s", C_("accel", "triple"));
528     if(s->click  & DT_SHORTCUT_LONG  ) add_hint(" %s", C_("accel", "long"));
529     add_hint(" %s", _("click"));
530   }
531 
532   if(*move_name && (s->key_device || s->key))
533     add_hint(", %s", move_name);
534   if(s->direction)
535     add_hint(", %s", s->direction == DT_SHORTCUT_UP ? _("up") : _("down"));
536 
537   g_free(key_name);
538   g_free(move_name);
539 
540   return hint + (hint[0] == ' ' ? 1 : 0);
541 }
542 
_action_description(dt_shortcut_t * s,int components)543 static gchar *_action_description(dt_shortcut_t *s, int components)
544 {
545   static gchar hint[1024];
546   int length = 0;
547   hint[0] = 0;
548 
549   if(components == 2)
550   {
551     gchar *action_label = _action_full_label(s->action);
552     add_hint("%s", action_label);
553     g_free(action_label);
554   }
555 
556   if(s->instance == 1)
557     add_hint(", %s", _("first instance"));
558   else if(s->instance == -1)
559     add_hint(", %s", _("last instance"));
560   else if(s->instance != 0)
561     add_hint(", %s %+d", _("relative instance"), s->instance);
562 
563   const dt_action_def_t *def = _action_find_definition(s->action);
564 
565   if(def && def->elements)
566   {
567     if(components && (s->element || (!def->fallbacks && def->elements->name)))
568       add_hint(", %s", _(def->elements[s->element].name));
569     if(def->elements[s->element].effects == dt_action_effect_selection
570         && s->effect > DT_ACTION_EFFECT_COMBO_SEPARATOR)
571     {
572       dt_introspection_type_enum_tuple_t *values
573         = g_hash_table_lookup(darktable.control->combo_introspection, s->action);
574       if(values)
575         add_hint(", %s", _(values[s->effect - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1].description));
576       else
577       {
578         gchar **strings
579           = g_hash_table_lookup(darktable.control->combo_list, s->action);
580         if(strings)
581           add_hint(", %s", _(strings[s->effect - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1]));
582       }
583     }
584     else if(s->effect > 0) add_hint(", %s", _(def->elements[s->element].effects[s->effect]));
585   }
586 
587   if(s->speed != 1.0)
588     add_hint(", %s *%g", _("speed"), s->speed);
589 
590 #undef add_hint
591 
592   return hint;
593 }
594 
_insert_shortcut_in_list(GHashTable * ht,char * shortcut,dt_action_t * ac,char * label)595 static void _insert_shortcut_in_list(GHashTable *ht, char *shortcut, dt_action_t *ac, char *label)
596 {
597   if(ac->owner && ac->owner->owner)
598     _insert_shortcut_in_list(ht, shortcut, ac->owner, g_strdup_printf("%s/%s", ac->owner->label, label));
599   {
600     GtkListStore *list_store = g_hash_table_lookup(ht, ac->owner);
601     if(!list_store)
602     {
603       list_store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
604       g_hash_table_insert(ht, ac->owner, list_store);
605     }
606 
607     gtk_list_store_insert_with_values(list_store, NULL, -1, 0, shortcut, 1, label, -1);
608   }
609 
610   g_free(label);
611 }
612 
dt_shortcut_category_lists(dt_view_type_flags_t v)613 GHashTable *dt_shortcut_category_lists(dt_view_type_flags_t v)
614 {
615   GHashTable *ht = g_hash_table_new(NULL, NULL);
616 
617   for(GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
618       !g_sequence_iter_is_end(iter);
619       iter = g_sequence_iter_next(iter))
620   {
621     dt_shortcut_t *s = g_sequence_get(iter);
622     if(s && s->views & v)
623       _insert_shortcut_in_list(ht, _shortcut_description(s), s->action, g_strdup_printf("%s%s", s->action->label, _action_description(s, 1)));
624   }
625 
626   return ht;
627 }
628 
_shortcut_tooltip_callback(GtkWidget * widget,gint x,gint y,gboolean keyboard_mode,GtkTooltip * tooltip,gpointer user_data)629 static gboolean _shortcut_tooltip_callback(GtkWidget *widget, gint x, gint y, gboolean keyboard_mode,
630                                            GtkTooltip *tooltip, gpointer user_data)
631 {
632   gchar *markup_text = NULL;
633   gchar *description = NULL;
634   dt_action_t *action = NULL;
635 
636   if(GTK_IS_TREE_VIEW(widget))
637   {
638     if(!gtk_widget_is_sensitive(widget)) return FALSE;
639     if(user_data) // shortcuts treeview
640     {
641       gtk_tooltip_set_text(tooltip, _("press Del to delete selected shortcut\ndouble click to add new shortcut\nstart typing for incremental search"));
642       return TRUE;
643     }
644 
645     GtkTreePath *path = NULL;
646     GtkTreeModel *model;
647     GtkTreeIter iter;
648     if(!gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(widget), &x, &y, keyboard_mode, &model, &path, &iter))
649       return FALSE;
650 
651     gtk_tree_model_get(model, &iter, 0, &action, -1);
652     gtk_tree_view_set_tooltip_row(GTK_TREE_VIEW(widget), tooltip, path);
653     gtk_tree_path_free(path);
654 
655     markup_text = g_markup_escape_text(_("click to filter shortcut list\ndouble click to define new shortcut\nstart typing for incremental search"), -1);
656   }
657   else
658   {
659     action = g_hash_table_lookup(darktable.control->widgets, widget);
660 
661     if(darktable.control->mapping_widget == widget)
662     {
663       const int add_remove_qap = darktable.develop
664         ? dt_dev_modulegroups_basics_module_toggle(darktable.develop, widget, FALSE)
665         : 0;
666 
667       markup_text = g_markup_printf_escaped("%s\n%s\n%s%s",
668                                             _("press keys with mouse click and scroll or move combinations to create a shortcut"),
669                                             _("click to open shortcut configuration"),
670                                             add_remove_qap > 0 ? _("ctrl+click to add to quick access panel\n") :
671                                             add_remove_qap < 0 ? _("ctrl+click to remove from quick access panel\n")  : "",
672                                             _("right click to exit mapping mode"));
673     }
674   }
675 
676   const dt_action_def_t *def = _action_find_definition(action);
677   const gboolean has_fallbacks = def && def->fallbacks;
678 
679   if(def && (darktable.control->element || !has_fallbacks))
680   {
681     const gchar *element_name = NULL;
682     for(int i = 0; i <= darktable.control->element; i++)
683     {
684       element_name = def->elements[i].name;
685       if(!element_name) break;
686     }
687     if(element_name) description = g_markup_escape_text(_(element_name), -1);
688   }
689 
690   for(GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
691       !g_sequence_iter_is_end(iter);
692       iter = g_sequence_iter_next(iter))
693   {
694     dt_shortcut_t *s = g_sequence_get(iter);
695     if(s->action == action &&
696        (!def || darktable.control->element == -1 ||
697         s->element == darktable.control->element ||
698         (s->element == DT_ACTION_ELEMENT_DEFAULT && has_fallbacks)))
699     {
700       gchar *sc_escaped = g_markup_escape_text(_shortcut_description(s), -1);
701       gchar *ac_escaped = g_markup_escape_text(_action_description(s, 0), -1);
702       description = dt_util_dstrcat(description, "%s<b><big>%s</big></b><i>%s</i>",
703                                                  description ? "\n" : "",
704                                                  sc_escaped, ac_escaped);
705       g_free(sc_escaped);
706       g_free(ac_escaped);
707     }
708   }
709 
710   gchar *original_markup = gtk_widget_get_tooltip_markup(widget);
711   if(description || (original_markup && markup_text))
712   {
713     markup_text = dt_util_dstrcat(markup_text, "%s%s%s%s",
714                                   markup_text? "\n\n" : "",
715                                   original_markup ? original_markup : "",
716                                   original_markup && description ? "\n" : "",
717                                   description ? description : "");
718     g_free(description);
719   }
720   g_free(original_markup);
721 
722   if(markup_text)
723   {
724     gtk_tooltip_set_markup(tooltip, markup_text);
725     g_free(markup_text);
726 
727     return TRUE;
728   }
729 
730   return FALSE;
731 }
732 
_find_views(dt_action_t * action)733 static dt_view_type_flags_t _find_views(dt_action_t *action)
734 {
735   dt_view_type_flags_t vws = 0;
736 
737   dt_action_t *owner = action;
738   while(owner && owner->type >= DT_ACTION_TYPE_SECTION)
739     owner = owner->owner;
740 
741   if(owner)
742 
743   switch(owner->type)
744   {
745   case DT_ACTION_TYPE_IOP:
746     vws = DT_VIEW_DARKROOM;
747     break;
748   case DT_ACTION_TYPE_VIEW:
749     {
750       dt_view_t *view = (dt_view_t *)owner;
751 
752       vws = view->view(view);
753     }
754     break;
755   case DT_ACTION_TYPE_LIB:
756     {
757       dt_lib_module_t *lib = (dt_lib_module_t *)owner;
758 
759       const gchar **views = lib->views(lib);
760       while (*views)
761       {
762         if     (strcmp(*views, "lighttable") == 0)
763           vws |= DT_VIEW_LIGHTTABLE;
764         else if(strcmp(*views, "darkroom") == 0)
765           vws |= DT_VIEW_DARKROOM;
766         else if(strcmp(*views, "print") == 0)
767           vws |= DT_VIEW_PRINT;
768         else if(strcmp(*views, "slideshow") == 0)
769           vws |= DT_VIEW_SLIDESHOW;
770         else if(strcmp(*views, "map") == 0)
771           vws |= DT_VIEW_MAP;
772         else if(strcmp(*views, "tethering") == 0)
773           vws |= DT_VIEW_TETHERING;
774         else if(strcmp(*views, "*") == 0)
775           vws |= DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
776                  DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
777         views++;
778       }
779     }
780     break;
781   case DT_ACTION_TYPE_CATEGORY:
782     if(owner == &darktable.control->actions_blend)
783       vws = DT_VIEW_DARKROOM;
784     else if(owner == &darktable.control->actions_fallbacks)
785       vws = 0;
786     else if(owner == &darktable.control->actions_lua)
787       vws = DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
788             DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
789     else if(owner == &darktable.control->actions_thumb)
790     {
791       vws = DT_VIEW_DARKROOM | DT_VIEW_MAP | DT_VIEW_TETHERING | DT_VIEW_PRINT;
792       if(!strcmp(action->id,"rating") || !strcmp(action->id,"color label"))
793         vws |= DT_VIEW_LIGHTTABLE; // lighttable has copy/paste history shortcuts in separate lib
794     }
795     else
796       fprintf(stderr, "[find_views] views for category '%s' unknown\n", owner->id);
797     break;
798   case DT_ACTION_TYPE_GLOBAL:
799     vws = DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
800           DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
801     break;
802   default:
803     break;
804   }
805 
806   return vws;
807 }
808 
809 static GtkTreeStore *_shortcuts_store = NULL;
810 static GtkTreeStore *_actions_store = NULL;
811 static GtkWidget *_grab_widget = NULL, *_grab_window = NULL;
812 
813 #define NUM_CATEGORIES 3
814 const gchar *category_label[NUM_CATEGORIES]
815   = { N_("active view"),
816       N_("other views"),
817       N_("fallbacks") };
818 #define CATEGORY_FALLBACKS 2
819 
_shortcuts_store_category(GtkTreeIter * category,dt_shortcut_t * s,dt_view_type_flags_t view)820 static void _shortcuts_store_category(GtkTreeIter *category, dt_shortcut_t *s, dt_view_type_flags_t view)
821 {
822   gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(_shortcuts_store), category, NULL,
823                                 s && s->views ? s->views & view ? 0 : 1 : 2);
824 }
825 
_remove_shortcut_from_store(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)826 static gboolean _remove_shortcut_from_store(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
827 {
828   gpointer iter_data;
829   gtk_tree_model_get(model, iter, 0, &iter_data, -1);
830   if(iter_data == data)
831   {
832     gtk_tree_store_remove(GTK_TREE_STORE(model), iter);
833     return TRUE;
834   }
835 
836   return FALSE;
837 }
838 
_remove_shortcut(GSequenceIter * shortcut)839 static void _remove_shortcut(GSequenceIter *shortcut)
840 {
841   if(_shortcuts_store)
842     gtk_tree_model_foreach(GTK_TREE_MODEL(_shortcuts_store), _remove_shortcut_from_store, shortcut);
843 
844   dt_shortcut_t *s = g_sequence_get(shortcut);
845   if(s && s->direction) // was this a split move?
846   {
847     // unsplit the other half of the move
848     s->direction = 0;
849     dt_shortcut_t *o = g_sequence_get(g_sequence_iter_prev(shortcut));
850     if(g_sequence_iter_is_begin(shortcut)
851        || _shortcut_compare_func(s, o, GINT_TO_POINTER(s->views)))
852       o = g_sequence_get(g_sequence_iter_next(shortcut));
853     o->direction = 0;
854   }
855 
856   g_sequence_remove(shortcut);
857 }
858 
_add_shortcut(dt_shortcut_t * shortcut,dt_view_type_flags_t view)859 static void _add_shortcut(dt_shortcut_t *shortcut, dt_view_type_flags_t view)
860 {
861   GSequenceIter *new_shortcut = g_sequence_insert_sorted(darktable.control->shortcuts, shortcut,
862                                                          _shortcut_compare_func, GINT_TO_POINTER(view));
863 
864   GtkTreeModel *model = GTK_TREE_MODEL(_shortcuts_store);
865   if(model)
866   {
867     GSequenceIter *prev_shortcut = g_sequence_iter_prev(new_shortcut);
868     GSequenceIter *seq_iter = NULL;
869     GtkTreeIter category, child;
870     _shortcuts_store_category(&category, shortcut, view);
871 
872     gint position = 1, found = 0;
873     if(gtk_tree_model_iter_children(model, &child, &category))
874     do
875     {
876       gtk_tree_model_get(model, &child, 0, &seq_iter, -1);
877       if(seq_iter == prev_shortcut)
878       {
879         found = position;
880         break;
881       }
882       position++;
883     } while(gtk_tree_model_iter_next(model, &child));
884 
885     gtk_tree_store_insert_with_values(_shortcuts_store, NULL, &category, found, 0, new_shortcut, -1);
886   }
887 }
888 
_shortcut_row_inserted(GtkTreeModel * tree_model,GtkTreePath * path,GtkTreeIter * iter,gpointer view)889 static void _shortcut_row_inserted(GtkTreeModel *tree_model, GtkTreePath *path, GtkTreeIter *iter, gpointer view)
890 {
891   // connect to original store, not filtered one, because otherwise view not sufficiently updated to expand
892 
893   GtkTreePath *filter_path = gtk_tree_model_filter_convert_child_path_to_path
894                              (GTK_TREE_MODEL_FILTER(gtk_tree_view_get_model(view)), path);
895   if(!filter_path) return;
896 
897   gtk_tree_view_expand_to_path(view, filter_path);
898   gtk_tree_view_scroll_to_cell(view, filter_path, NULL, TRUE, 0.5, 0);
899   gtk_tree_view_set_cursor(view, filter_path, NULL, FALSE);
900   gtk_tree_path_free(filter_path);
901 }
902 
_yes_no_dialog(gchar * title,gchar * question)903 static gboolean _yes_no_dialog(gchar *title, gchar *question)
904 {
905   GtkWindow *win = NULL;
906   for(GList *wins = gtk_window_list_toplevels(); wins; wins = g_list_delete_link(wins, wins))
907     if(gtk_window_is_active(wins->data)) win = wins->data;
908 
909   GtkWidget *dialog = gtk_message_dialog_new(win, GTK_DIALOG_DESTROY_WITH_PARENT,
910                                              GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "%s", question);
911   gtk_window_set_title(GTK_WINDOW(dialog), title);
912 
913 #ifdef GDK_WINDOWING_QUARTZ
914     dt_osx_disallow_fullscreen(dialog);
915 #endif
916 
917   const int resp = gtk_dialog_run(GTK_DIALOG(dialog));
918 
919   gtk_widget_destroy(dialog);
920 
921   return resp == GTK_RESPONSE_YES;
922 }
923 
_insert_shortcut(dt_shortcut_t * shortcut,gboolean confirm)924 static gboolean _insert_shortcut(dt_shortcut_t *shortcut, gboolean confirm)
925 {
926   dt_shortcut_t *s = calloc(sizeof(dt_shortcut_t), 1);
927   *s = *shortcut;
928   s->views = _find_views(s->action);
929   dt_view_type_flags_t real_views = s->views;
930 
931   const dt_view_t *vw = NULL;
932   if(darktable.view_manager)
933     vw = dt_view_manager_get_current_view(darktable.view_manager);
934 
935   dt_view_type_flags_t view = vw && vw->view ? vw->view(vw) : DT_VIEW_LIGHTTABLE;
936 
937   // check (and remove if confirmed) clashes in current and other views
938   gboolean remove_existing = !confirm;
939   do
940   {
941     gchar *existing_labels = NULL;
942     int active_view = 1;
943     do
944     {
945       GSequenceIter *existing = g_sequence_lookup(darktable.control->shortcuts, s, _shortcut_compare_func, GINT_TO_POINTER(view));
946       if(existing) // at least one found
947       {
948         // go to first one that has same shortcut
949         while(!g_sequence_iter_is_begin(existing)
950               && !_shortcut_compare_func(s, g_sequence_get(g_sequence_iter_prev(existing)), GINT_TO_POINTER(view)))
951           existing = g_sequence_iter_prev(existing);
952 
953         do
954         {
955           GSequenceIter *saved_next = g_sequence_iter_next(existing);
956 
957           dt_shortcut_t *e = g_sequence_get(existing);
958 
959           if(e->action == s->action)
960           {
961             if(_shortcut_is_move(e) && e->effect != DT_ACTION_EFFECT_DEFAULT_MOVE)
962             {
963               if(!confirm ||
964                  _yes_no_dialog(_("shortcut for move exists with single effect"),
965                                 _("create separate shortcuts for up and down move?")))
966               {
967                 e->direction = (DT_SHORTCUT_UP | DT_SHORTCUT_DOWN) ^ s->direction;
968                 if(s->effect == DT_ACTION_EFFECT_DEFAULT_MOVE)
969                   s->effect = DT_ACTION_EFFECT_DEFAULT_KEY;
970                 _add_shortcut(s, view);
971                 return TRUE;
972               }
973             }
974             else if(e->element  != s->element ||
975                     e->effect   != s->effect  ||
976                     e->speed    != s->speed   ||
977                     e->instance != s->instance )
978             {
979               if(!confirm ||
980                  _yes_no_dialog(_("shortcut exists with different settings"),
981                                 _("reset the settings of the shortcut?")))
982               {
983                 e->element  = s->element;
984                 e->effect   = s->effect;
985                 e->speed    = s->speed;
986                 e->instance = s->instance;
987               }
988             }
989             else
990             {
991               // there should be no other clashes because same mapping already existed
992               if(confirm &&
993                  _yes_no_dialog(_("shortcut already exists"),
994                                 _("remove the shortcut?")))
995               {
996                 _remove_shortcut(existing);
997               }
998             }
999             g_free(s);
1000             return FALSE;
1001           }
1002 
1003           if(e->views & real_views) // overlap
1004           {
1005             if(remove_existing)
1006               _remove_shortcut(existing);
1007             else
1008             {
1009               gchar *old_labels = existing_labels;
1010               existing_labels = g_strdup_printf("%s\n%s",
1011                                                 existing_labels ? existing_labels : "",
1012                                                 _action_description(e, 2));
1013               g_free(old_labels);
1014             }
1015           }
1016 
1017           existing = saved_next;
1018         } while(!g_sequence_iter_is_end(existing) && !_shortcut_compare_func(s, g_sequence_get(existing), GINT_TO_POINTER(view)));
1019       }
1020 
1021       s->views ^= view; // look in the opposite selection
1022     } while(active_view--);
1023 
1024     if(existing_labels)
1025     {
1026       gchar *question = g_strdup_printf("%s\n%s",
1027                                         _("remove these existing shortcuts?"),
1028                                         existing_labels);
1029       remove_existing = _yes_no_dialog(_("clashing shortcuts exist"), question);
1030 
1031       g_free(existing_labels);
1032       g_free(question);
1033 
1034       if(!remove_existing)
1035       {
1036         g_free(s);
1037         return FALSE;
1038       }
1039     }
1040     else
1041     {
1042       remove_existing = FALSE;
1043     }
1044 
1045   } while(remove_existing);
1046 
1047   s->direction = shortcut->direction = 0;
1048   _add_shortcut(s, view);
1049 
1050   return TRUE;
1051 }
1052 
1053 typedef enum
1054 {
1055   SHORTCUT_VIEW_DESCRIPTION,
1056   SHORTCUT_VIEW_ACTION,
1057   SHORTCUT_VIEW_ELEMENT,
1058   SHORTCUT_VIEW_EFFECT,
1059   SHORTCUT_VIEW_SPEED,
1060   SHORTCUT_VIEW_INSTANCE,
1061   SHORTCUT_VIEW_COLUMNS
1062 } field_id;
1063 
1064 #define NUM_INSTANCES 5 // or 3, but change char relative[] = "-2" to "-1"
1065 const gchar *instance_label[/*NUM_INSTANCES*/]
1066   = { N_("preferred"),
1067       N_("first"),
1068       N_("last"),
1069       N_("second"),
1070       N_("last but one") };
1071 
_fill_shortcut_fields(GtkTreeViewColumn * column,GtkCellRenderer * cell,GtkTreeModel * model,GtkTreeIter * iter,gpointer data)1072 static void _fill_shortcut_fields(GtkTreeViewColumn *column, GtkCellRenderer *cell, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
1073 {
1074   void *data_ptr = NULL;
1075   gtk_tree_model_get(model, iter, 0, &data_ptr, -1);
1076   const field_id field = GPOINTER_TO_INT(data);
1077   gchar *field_text = NULL;
1078   gboolean editable = FALSE;
1079   PangoUnderline underline = PANGO_UNDERLINE_NONE;
1080   int weight = PANGO_WEIGHT_NORMAL;
1081 
1082   if(GPOINTER_TO_UINT(data_ptr) < NUM_CATEGORIES)
1083   {
1084     if(field == SHORTCUT_VIEW_DESCRIPTION)
1085       field_text = g_strdup(_(category_label[GPOINTER_TO_INT(data_ptr)]));
1086   }
1087   else
1088   {
1089     const dt_action_element_def_t *elements = NULL;
1090     dt_shortcut_t *s = g_sequence_get(data_ptr);
1091     switch(field)
1092     {
1093     case SHORTCUT_VIEW_DESCRIPTION:
1094       field_text = g_strdup(_shortcut_description(s));
1095       break;
1096     case SHORTCUT_VIEW_ACTION:
1097       if(s->action)
1098         field_text = _action_full_label(s->action);
1099       break;
1100     case SHORTCUT_VIEW_ELEMENT:
1101       elements = _action_find_elements(s->action);
1102       if(elements && elements->name)
1103       {
1104         if(s->element || s->action->type != DT_ACTION_TYPE_FALLBACK)
1105           field_text = g_strdup(_(elements[s->element].name));
1106         if(s->element == 0) weight = PANGO_WEIGHT_LIGHT;
1107         editable = TRUE;
1108       }
1109       break;
1110     case SHORTCUT_VIEW_EFFECT:
1111       elements = _action_find_elements(s->action);
1112       if(elements)
1113       {
1114         if(elements[s->element].effects == dt_action_effect_selection
1115            && s->effect > DT_ACTION_EFFECT_COMBO_SEPARATOR)
1116         {
1117           dt_introspection_type_enum_tuple_t *values
1118             = g_hash_table_lookup(darktable.control->combo_introspection, s->action);
1119           if(values)
1120             field_text = g_strdup(_(values[s->effect - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1].description));
1121           else
1122           {
1123             gchar **strings
1124               = g_hash_table_lookup(darktable.control->combo_list, s->action);
1125             if(strings)
1126               field_text = g_strdup(_(strings[s->effect - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1]));
1127           }
1128         }
1129         else if(s->effect > 0 || s->action->type != DT_ACTION_TYPE_FALLBACK)
1130           field_text = g_strdup(_(elements[s->element].effects[s->effect]));
1131         if(s->effect == 0) weight = PANGO_WEIGHT_LIGHT;
1132         editable = TRUE;
1133       }
1134       break;
1135     case SHORTCUT_VIEW_SPEED:
1136       elements = _action_find_elements(s->action);
1137       if(s->speed != 1.0
1138          || (elements && elements[s->element].effects == dt_action_effect_value
1139              && (s->effect == DT_ACTION_EFFECT_DEFAULT_MOVE
1140                  || s->effect == DT_ACTION_EFFECT_DEFAULT_KEY
1141                  || s->effect == DT_ACTION_EFFECT_DEFAULT_UP
1142                  || s->effect == DT_ACTION_EFFECT_DEFAULT_DOWN
1143                  || s->effect == DT_ACTION_EFFECT_SET
1144                  || (!s->effect && s->action->type == DT_ACTION_TYPE_FALLBACK))))
1145       {
1146         field_text = g_strdup_printf("%.3f", s->speed);
1147         if(s->speed == 1.0) weight = PANGO_WEIGHT_LIGHT;
1148       }
1149       editable = TRUE;
1150       break;
1151     case SHORTCUT_VIEW_INSTANCE:
1152       for(dt_action_t *owner = s->action; owner; owner = owner->owner)
1153       {
1154         if(owner->type == DT_ACTION_TYPE_IOP)
1155         {
1156           dt_iop_module_so_t *iop = (dt_iop_module_so_t *)owner;
1157 
1158           if(!(iop->flags() & IOP_FLAGS_ONE_INSTANCE))
1159           {
1160             field_text = abs(s->instance) <= (NUM_INSTANCES - 1) /2
1161                        ? g_strdup(_(instance_label[abs(s->instance)*2 - (s->instance > 0)]))
1162                        : g_strdup_printf("%+d", s->instance);
1163             if(s->instance == 0) weight = PANGO_WEIGHT_LIGHT;
1164             editable = TRUE;
1165           }
1166           break;
1167         }
1168       }
1169       break;
1170     default:
1171       break;
1172     }
1173   }
1174   g_object_set(cell, "text", field_text, "editable", editable, "underline", underline, "weight", weight, NULL);
1175   g_free(field_text);
1176 }
1177 
_add_prefs_column(GtkTreeView * tree,GtkCellRenderer * renderer,char * name,int position)1178 static void _add_prefs_column(GtkTreeView *tree, GtkCellRenderer *renderer, char *name, int position)
1179 {
1180   GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(name, renderer, NULL);
1181   gtk_tree_view_column_set_cell_data_func(column, renderer, _fill_shortcut_fields, GINT_TO_POINTER(position), NULL);
1182   gtk_tree_view_column_set_resizable(column, TRUE);
1183   gtk_tree_view_append_column(tree, column);
1184 }
1185 
_find_edited_shortcut(GtkTreeModel * model,const gchar * path_string)1186 static dt_shortcut_t *_find_edited_shortcut(GtkTreeModel *model, const gchar *path_string)
1187 {
1188   GtkTreePath *path = gtk_tree_path_new_from_string(path_string);
1189   GtkTreeIter iter;
1190   gtk_tree_model_get_iter(model, &iter, path);
1191   gtk_tree_path_free(path);
1192 
1193   void *data_ptr = NULL;
1194   gtk_tree_model_get(model, &iter, 0, &data_ptr, -1);
1195 
1196   return g_sequence_get(data_ptr);
1197 }
1198 
_element_editing_started(GtkCellRenderer * renderer,GtkCellEditable * editable,char * path,gpointer data)1199 static void _element_editing_started(GtkCellRenderer *renderer, GtkCellEditable *editable, char *path, gpointer data)
1200 {
1201   dt_shortcut_t *s = _find_edited_shortcut(data, path);
1202 
1203   GtkComboBox *combo_box = GTK_COMBO_BOX(editable);
1204   GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo_box));
1205   gtk_list_store_clear(store);
1206 
1207   int show_all = s->action->type != DT_ACTION_TYPE_FALLBACK;
1208   for(const dt_action_element_def_t *element = _action_find_elements(s->action); element && element->name ; element++)
1209     gtk_list_store_insert_with_values(store, NULL, -1, 0, show_all++ ? _(element->name) : _("(unchanged)"), -1);
1210 
1211   gtk_combo_box_set_active(combo_box, s->element);
1212 }
1213 
_element_changed(GtkCellRendererCombo * combo,char * path_string,GtkTreeIter * new_iter,gpointer data)1214 static void _element_changed(GtkCellRendererCombo *combo, char *path_string, GtkTreeIter *new_iter, gpointer data)
1215 {
1216   dt_shortcut_t *s = _find_edited_shortcut(data, path_string);
1217 
1218   GtkTreeModel *model = NULL;
1219   g_object_get(combo, "model", &model, NULL);
1220   GtkTreePath *path = gtk_tree_model_get_path(model, new_iter);
1221   const gint new_index = gtk_tree_path_get_indices(path)[0];
1222   gtk_tree_path_free(path);
1223 
1224   const dt_action_element_def_t *elements = _action_find_elements(s->action);
1225   if(elements[s->element].effects != elements[new_index].effects)
1226   {
1227     s->effect = _shortcut_is_move(s) ? DT_ACTION_EFFECT_DEFAULT_MOVE : DT_ACTION_EFFECT_DEFAULT_KEY;
1228   }
1229   s->element = new_index;
1230 
1231   dt_shortcuts_save(NULL, FALSE);
1232 }
1233 
1234 enum
1235 {
1236   DT_ACTION_EFFECT_COLUMN_NAME,
1237   DT_ACTION_EFFECT_COLUMN_SEPARATOR,
1238   DT_ACTION_EFFECT_COLUMN_WEIGHT,
1239 };
1240 
_effects_separator_func(GtkTreeModel * model,GtkTreeIter * iter,gpointer data)1241 static gboolean _effects_separator_func(GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
1242 {
1243   gboolean is_separator;
1244   gtk_tree_model_get(model, iter, DT_ACTION_EFFECT_COLUMN_SEPARATOR, &is_separator, -1);
1245   return is_separator;
1246 }
1247 
_effect_editing_started(GtkCellRenderer * renderer,GtkCellEditable * editable,char * path,gpointer data)1248 static void _effect_editing_started(GtkCellRenderer *renderer, GtkCellEditable *editable, char *path, gpointer data)
1249 {
1250   dt_shortcut_t *s = _find_edited_shortcut(data, path);
1251 
1252   GtkComboBox *combo_box = GTK_COMBO_BOX(editable);
1253   GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo_box));
1254   gtk_list_store_clear(store);
1255 
1256   const dt_action_element_def_t *elements = _action_find_elements(s->action);
1257   int show_all = s->action->type != DT_ACTION_TYPE_FALLBACK;
1258   int bold_move = _shortcut_is_move(s) ? DT_ACTION_EFFECT_DEFAULT_KEY : DT_ACTION_EFFECT_DEFAULT_DOWN + 1;
1259   if(elements)
1260     for(const gchar **effect = elements[s->element].effects; *effect ; effect++, bold_move++)
1261     {
1262       gtk_list_store_insert_with_values(store, NULL, -1,
1263                                         DT_ACTION_EFFECT_COLUMN_NAME, show_all++ ? _(*effect) : _("(unchanged)"),
1264                                         DT_ACTION_EFFECT_COLUMN_WEIGHT, bold_move >  DT_ACTION_EFFECT_DEFAULT_KEY
1265                                                                      && bold_move <= DT_ACTION_EFFECT_DEFAULT_DOWN
1266                                                                       ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
1267                                         -1);
1268     }
1269 
1270   GList *cell = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(combo_box));
1271   gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combo_box), cell->data, "weight", DT_ACTION_EFFECT_COLUMN_WEIGHT);
1272   g_list_free(cell);
1273 
1274   if(elements[s->element].effects == dt_action_effect_selection)
1275   {
1276     gtk_combo_box_set_row_separator_func(combo_box, _effects_separator_func, NULL, NULL);
1277 
1278     dt_introspection_type_enum_tuple_t *values
1279       = g_hash_table_lookup(darktable.control->combo_introspection, s->action);
1280     if(values)
1281     {
1282       // insert empty/separator row
1283       gtk_list_store_insert_with_values(store, NULL, -1, DT_ACTION_EFFECT_COLUMN_SEPARATOR, TRUE, -1);
1284 
1285       while(values->name)
1286       {
1287         gtk_list_store_insert_with_values(store, NULL, -1,
1288                                           DT_ACTION_EFFECT_COLUMN_NAME, _((values++)->description),
1289                                           DT_ACTION_EFFECT_COLUMN_WEIGHT, PANGO_WEIGHT_NORMAL,
1290                                           -1);
1291       }
1292     }
1293     else
1294     {
1295       gchar **strings
1296         = g_hash_table_lookup(darktable.control->combo_list, s->action);
1297       if(strings)
1298       {
1299         // insert empty/separator row
1300         gtk_list_store_insert_with_values(store, NULL, -1, DT_ACTION_EFFECT_COLUMN_SEPARATOR, TRUE, -1);
1301 
1302         while(*strings)
1303         {
1304           gtk_list_store_insert_with_values(store, NULL, -1,
1305                                             DT_ACTION_EFFECT_COLUMN_NAME, _(*(strings++)),
1306                                             DT_ACTION_EFFECT_COLUMN_WEIGHT, PANGO_WEIGHT_NORMAL,
1307                                             -1);
1308         }
1309       }
1310     }
1311   }
1312 
1313   gtk_combo_box_set_active(combo_box, s->effect == -1 ? 1 : s->effect);
1314 }
1315 
_effect_changed(GtkCellRendererCombo * combo,char * path_string,GtkTreeIter * new_iter,gpointer data)1316 static void _effect_changed(GtkCellRendererCombo *combo, char *path_string, GtkTreeIter *new_iter, gpointer data)
1317 {
1318   dt_shortcut_t *s = _find_edited_shortcut(data, path_string);
1319 
1320   GtkTreeModel *model = NULL;
1321   g_object_get(combo, "model", &model, NULL);
1322   GtkTreePath *path = gtk_tree_model_get_path(model, new_iter);
1323   const gint new_index = s->effect = gtk_tree_path_get_indices(path)[0];
1324   gtk_tree_path_free(path);
1325 
1326   if(_shortcut_is_move(s) &&
1327      (new_index == DT_ACTION_EFFECT_DEFAULT_UP || new_index == DT_ACTION_EFFECT_DEFAULT_DOWN))
1328     s->effect = DT_ACTION_EFFECT_DEFAULT_MOVE;
1329   else
1330     s->effect = new_index;
1331 
1332   dt_shortcuts_save(NULL, FALSE);
1333 }
1334 
_speed_edited(GtkCellRendererText * cell,const gchar * path_string,const gchar * new_text,gpointer data)1335 static void _speed_edited(GtkCellRendererText *cell, const gchar *path_string, const gchar *new_text, gpointer data)
1336 {
1337   _find_edited_shortcut(data, path_string)->speed = atof(new_text);
1338 
1339   dt_shortcuts_save(NULL, FALSE);
1340 }
1341 
_instance_edited(GtkCellRendererText * cell,const gchar * path_string,const gchar * new_text,gpointer data)1342 static void _instance_edited(GtkCellRendererText *cell, const gchar *path_string, const gchar *new_text, gpointer data)
1343 {
1344   dt_shortcut_t *s = _find_edited_shortcut(data, path_string);
1345 
1346   if(!(s->instance = atoi(new_text)))
1347     for(int i = 0; i < NUM_INSTANCES; i++)
1348       if(!strcmp(instance_label[i], new_text))
1349         s->instance = (i + 1) / 2 * (i % 2 ? 1 : -1);
1350 
1351   dt_shortcuts_save(NULL, FALSE);
1352 }
1353 
_grab_in_tree_view(GtkTreeView * tree_view)1354 static void _grab_in_tree_view(GtkTreeView *tree_view)
1355 {
1356   g_set_weak_pointer(&_grab_widget, gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(tree_view)))); // static
1357   gtk_widget_set_sensitive(_grab_widget, FALSE);
1358   gtk_widget_set_tooltip_text(_grab_widget, _("define a shortcut by pressing a key, optionally combined with modifier keys (ctrl/shift/alt)\n"
1359                                               "a key can be double or triple pressed, with a long last press\n"
1360                                               "while the key is held, a combination of mouse buttons can be (double/triple/long) clicked\n"
1361                                               "still holding the key (and modifiers and/or buttons) a scroll or mouse move can be added\n"
1362                                               "connected devices can send keys or moves using their physical controllers\n\n"
1363                                               "right-click to cancel"));
1364   g_set_weak_pointer(&_grab_window, gtk_widget_get_toplevel(_grab_widget));
1365   if(_sc.action && _sc.action->type == DT_ACTION_TYPE_FALLBACK)
1366     dt_shortcut_key_press(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, 0);
1367   g_signal_connect(_grab_window, "event", G_CALLBACK(dt_shortcut_dispatcher), NULL);
1368 }
1369 
_shortcut_row_activated(GtkTreeView * tree_view,GtkTreePath * path,GtkTreeViewColumn * column,gpointer user_data)1370 static void _shortcut_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data)
1371 {
1372   GtkTreeIter iter;
1373   gtk_tree_model_get_iter(GTK_TREE_MODEL(user_data), &iter, path);
1374 
1375   GSequenceIter  *shortcut_iter = NULL;
1376   gtk_tree_model_get(GTK_TREE_MODEL(user_data), &iter, 0, &shortcut_iter, -1);
1377 
1378   if(GPOINTER_TO_UINT(shortcut_iter) < NUM_CATEGORIES) return;
1379 
1380   dt_shortcut_t *s = g_sequence_get(shortcut_iter);
1381   _sc.action = s->action;
1382   _sc.element = s->element;
1383   _sc.instance = s->instance;
1384 
1385   _grab_in_tree_view(tree_view);
1386 }
1387 
_shortcut_key_pressed(GtkWidget * widget,GdkEventKey * event,gpointer user_data)1388 static gboolean _shortcut_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
1389 {
1390   // GDK_KEY_BackSpace moves to parent in tree
1391   if(event->keyval == GDK_KEY_Delete || event->keyval == GDK_KEY_KP_Delete)
1392   {
1393     GtkTreeView *view = GTK_TREE_VIEW(widget);
1394     GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
1395 
1396     GtkTreeIter iter;
1397     GtkTreeModel *model = NULL;
1398     if(gtk_tree_selection_get_selected(selection, &model, &iter))
1399     {
1400       GSequenceIter  *shortcut_iter = NULL;
1401       gtk_tree_model_get(model, &iter, 0, &shortcut_iter, -1);
1402 
1403       if(GPOINTER_TO_UINT(shortcut_iter) >= NUM_CATEGORIES)
1404       {
1405         if(_yes_no_dialog(_("removing shortcut"),
1406                           _("remove the selected shortcut?")))
1407         {
1408           _remove_shortcut(shortcut_iter);
1409 
1410           dt_shortcuts_save(NULL, FALSE);
1411         }
1412       }
1413     }
1414 
1415     return TRUE;
1416   }
1417 
1418   return FALSE;
1419 }
1420 
_add_shortcuts_to_tree()1421 static void _add_shortcuts_to_tree()
1422 {
1423   const dt_view_t *vw = dt_view_manager_get_current_view(darktable.view_manager);
1424   dt_view_type_flags_t view = vw && vw->view ? vw->view(vw) : DT_VIEW_LIGHTTABLE;
1425 
1426   for(gint i = 0; i < NUM_CATEGORIES; i++)
1427     gtk_tree_store_insert_with_values(_shortcuts_store, NULL, NULL, -1, 0, GINT_TO_POINTER(i), -1);
1428 
1429   for(GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
1430       !g_sequence_iter_is_end(iter);
1431       iter = g_sequence_iter_next(iter))
1432   {
1433     dt_shortcut_t *s = g_sequence_get(iter);
1434     GtkTreeIter category;
1435     _shortcuts_store_category(&category, s, view);
1436 
1437     gtk_tree_store_insert_with_values(_shortcuts_store, NULL, &category, -1, 0, iter, -1);
1438   }
1439 }
1440 
_add_actions_to_tree(GtkTreeIter * parent,dt_action_t * action,dt_action_t * find,GtkTreeIter * found)1441 static gboolean _add_actions_to_tree(GtkTreeIter *parent, dt_action_t *action,
1442                                      dt_action_t *find, GtkTreeIter *found)
1443 {
1444   gboolean any_leaves = FALSE;
1445 
1446   GtkTreeIter iter;
1447   while(action)
1448   {
1449     gtk_tree_store_insert_with_values(_actions_store, &iter, parent, -1, 0, action, -1);
1450 
1451     gboolean module_is_needed = FALSE;
1452     if(action->type == DT_ACTION_TYPE_IOP)
1453     {
1454       const dt_iop_module_so_t *module = (dt_iop_module_so_t *)action;
1455       module_is_needed = !(module->flags() & (IOP_FLAGS_HIDDEN | IOP_FLAGS_DEPRECATED));
1456     }
1457     else if(action->type == DT_ACTION_TYPE_LIB)
1458     {
1459       dt_lib_module_t *module = (dt_lib_module_t *)action;
1460       module_is_needed = module->gui_reset || module->get_params || module->expandable(module);
1461     }
1462 
1463     if(action->type <= DT_ACTION_TYPE_SECTION &&
1464        !_add_actions_to_tree(&iter, action->target, find, found) &&
1465        !module_is_needed)
1466       gtk_tree_store_remove(_actions_store, &iter);
1467     else
1468     {
1469       any_leaves = TRUE;
1470       if(action == find) *found = iter;
1471     }
1472 
1473     action = action->next;
1474   }
1475 
1476   return any_leaves;
1477 }
1478 
_fill_action_fields(GtkTreeViewColumn * column,GtkCellRenderer * cell,GtkTreeModel * model,GtkTreeIter * iter,gpointer data)1479 static void _fill_action_fields(GtkTreeViewColumn *column, GtkCellRenderer *cell, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
1480 {
1481   dt_action_t *action = NULL;
1482   gtk_tree_model_get(model, iter, 0, &action, -1);
1483   if(data)
1484     g_object_set(cell, "text", action->label, NULL);
1485   else
1486   {
1487     const dt_action_def_t *def = _action_find_definition(action);
1488     g_object_set(cell, "text", def ? _(def->name) : "", NULL);
1489   }
1490 }
1491 
_action_row_activated(GtkTreeView * tree_view,GtkTreePath * path,GtkTreeViewColumn * column,gpointer user_data)1492 static void _action_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data)
1493 {
1494   GtkTreeIter iter;
1495   gtk_tree_model_get_iter(GTK_TREE_MODEL(user_data), &iter, path);
1496 
1497   gtk_tree_model_get(GTK_TREE_MODEL(user_data), &iter, 0, &_sc.action, -1);
1498   _sc.element = DT_ACTION_ELEMENT_DEFAULT;
1499   _sc.instance = 0;
1500 
1501   if(_action_find_definition(_sc.action)
1502      || (_sc.action->type > DT_ACTION_TYPE_SECTION && _sc.action->type < DT_ACTION_TYPE_WIDGET))
1503 
1504     _grab_in_tree_view(tree_view);
1505   else
1506     _sc.action = NULL;
1507 }
1508 
_shortcut_selection_function(GtkTreeSelection * selection,GtkTreeModel * model,GtkTreePath * path,gboolean path_currently_selected,gpointer data)1509 static gboolean _shortcut_selection_function(GtkTreeSelection *selection,
1510                                              GtkTreeModel *model, GtkTreePath *path,
1511                                              gboolean path_currently_selected, gpointer data)
1512 {
1513   GtkTreeIter iter;
1514   gtk_tree_model_get_iter(model, &iter, path);
1515 
1516   void *data_ptr = NULL;
1517   gtk_tree_model_get(model, &iter, 0, &data_ptr, -1);
1518 
1519   if(GPOINTER_TO_UINT(data_ptr) < NUM_CATEGORIES)
1520   {
1521     GtkTreeView *view = gtk_tree_selection_get_tree_view(selection);
1522     if(gtk_tree_view_row_expanded(view, path))
1523       gtk_tree_view_collapse_row(view, path);
1524     else
1525       gtk_tree_view_expand_row(view, path, FALSE);
1526 
1527     return FALSE;
1528   }
1529   else
1530     return TRUE;
1531 }
1532 
1533 static dt_action_t *_selected_action = NULL;
1534 
_action_view_click(GtkWidget * widget,GdkEventButton * event,gpointer data)1535 static gboolean _action_view_click(GtkWidget *widget, GdkEventButton *event, gpointer data)
1536 {
1537   GtkTreeView *view = GTK_TREE_VIEW(widget);
1538 
1539   if(event->button == GDK_BUTTON_PRIMARY)
1540   {
1541     GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
1542 
1543     GtkTreePath *path = NULL;
1544     if(gtk_tree_view_get_path_at_pos(view, (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL))
1545     {
1546       if(event->type == GDK_DOUBLE_BUTTON_PRESS)
1547       {
1548         gtk_tree_selection_select_path(selection, path);
1549         _action_row_activated(view, path, NULL, gtk_tree_view_get_model(view));
1550       }
1551       else if(gtk_tree_selection_path_is_selected(selection, path))
1552       {
1553         gtk_tree_selection_unselect_path(selection, path);
1554         gtk_tree_view_collapse_row(view, path);
1555       }
1556       else
1557       {
1558         gtk_tree_selection_select_path(selection, path);
1559         gtk_tree_view_set_cursor(view, path, NULL, FALSE);
1560       }
1561 
1562       gtk_widget_grab_focus(widget);
1563     }
1564     else
1565       gtk_tree_selection_unselect_all(selection);
1566   }
1567 
1568   return TRUE;
1569 }
1570 
_action_view_map(GtkTreeView * view,GdkEvent * event,gpointer found_iter)1571 static gboolean _action_view_map(GtkTreeView *view, GdkEvent *event, gpointer found_iter)
1572 {
1573   GtkTreePath *path = gtk_tree_model_get_path(gtk_tree_view_get_model(view), found_iter);
1574   gtk_tree_view_expand_to_path(view, path);
1575   gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.5, 0);
1576   gtk_tree_view_set_cursor(view, path, NULL, FALSE);
1577   gtk_tree_path_free(path);
1578 
1579   gtk_tree_selection_select_iter(gtk_tree_view_get_selection(view), found_iter);
1580 
1581   return FALSE;
1582 }
1583 
_action_selection_changed(GtkTreeSelection * selection,gpointer data)1584 static void _action_selection_changed(GtkTreeSelection *selection, gpointer data)
1585 {
1586   GtkTreeIter iter;
1587   GtkTreeModel *model = NULL;
1588 
1589   if(!gtk_tree_selection_get_selected(selection, &model, &iter))
1590     _selected_action = NULL;
1591   else
1592   {
1593     gtk_tree_model_get(model, &iter, 0, &_selected_action, -1);
1594 
1595     GtkTreeView *view = gtk_tree_selection_get_tree_view(selection);
1596     GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
1597     gtk_tree_view_expand_row(view, path, FALSE);
1598     gtk_tree_path_free(path);
1599   }
1600 
1601   GtkTreeView *shortcuts_view = GTK_TREE_VIEW(data);
1602   gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(gtk_tree_view_get_model(shortcuts_view)));
1603   gtk_tree_view_expand_all(shortcuts_view);
1604 }
1605 
_search_func(GtkTreeModel * model,gint column,const gchar * key,GtkTreeIter * iter,gpointer search_data)1606 static gboolean _search_func(GtkTreeModel *model, gint column, const gchar *key, GtkTreeIter *iter, gpointer search_data)
1607 {
1608   gchar *key_case = g_utf8_casefold(key, -1), *label_case = NULL;
1609   if(column == 1)
1610   {
1611     dt_action_t *action = NULL;
1612     gtk_tree_model_get(model, iter, 0, &action, -1);
1613     label_case = g_utf8_casefold(action->label, -1);
1614   }
1615   else
1616   {
1617     GSequenceIter *seq_iter = NULL;
1618     gtk_tree_model_get(model, iter, 0, &seq_iter, -1);
1619     if(GPOINTER_TO_UINT(seq_iter) >= NUM_CATEGORIES)
1620     {
1621       dt_shortcut_t *s = g_sequence_get(seq_iter);
1622       if(s->action)
1623       {
1624         gchar *label = _action_full_label(s->action);
1625         label_case = g_utf8_casefold(label, -1);
1626         g_free(label);
1627       }
1628     }
1629   }
1630   const gboolean different = label_case ? !strstr(label_case, key_case) : TRUE;
1631   g_free(key_case);
1632   g_free(label_case);
1633   if(!different)
1634   {
1635     GtkTreePath *path = gtk_tree_model_get_path(model, iter);
1636     gtk_tree_view_expand_to_path(GTK_TREE_VIEW(search_data), path);
1637     gtk_tree_path_free(path);
1638 
1639     return FALSE;
1640   }
1641 
1642   GtkTreeIter child;
1643   if(gtk_tree_model_iter_children(model, &child, iter))
1644   {
1645     do
1646     {
1647       _search_func(model, column, key, &child, search_data);
1648     }
1649     while(gtk_tree_model_iter_next(model, &child));
1650   }
1651 
1652   return TRUE;
1653 }
1654 
_fallback_type_is_relevant(dt_action_t * ac,dt_action_type_t type)1655 static gboolean _fallback_type_is_relevant(dt_action_t *ac, dt_action_type_t type)
1656 {
1657   if(!ac) return FALSE;
1658 
1659   if(ac->type == type) return TRUE;
1660 
1661   if(ac->type >= DT_ACTION_TYPE_WIDGET)
1662   {
1663     if(type == DT_ACTION_TYPE_VALUE_FALLBACK)
1664     {
1665       const dt_action_def_t *def = _action_find_definition(ac);
1666       if(def && def->elements)
1667       {
1668         const dt_action_element_def_t *el = def->elements;
1669         do
1670         {
1671           if(el->effects == dt_action_effect_value) return TRUE;
1672           el++;
1673         } while (el->name);
1674       }
1675     }
1676   }
1677   else if(ac->type <= DT_ACTION_TYPE_SECTION)
1678     for(ac = ac->target; ac; ac = ac->next)
1679       if(_fallback_type_is_relevant(ac, type)) return TRUE;
1680 
1681   return FALSE;
1682 }
1683 
_visible_shortcuts(GtkTreeModel * model,GtkTreeIter * iter,gpointer data)1684 static gboolean _visible_shortcuts(GtkTreeModel *model, GtkTreeIter  *iter, gpointer data)
1685 {
1686   void *data_ptr = NULL;
1687   gtk_tree_model_get(model, iter, 0, &data_ptr, -1);
1688 
1689   if(GPOINTER_TO_UINT(data_ptr) == CATEGORY_FALLBACKS && !darktable.control->enable_fallbacks) return FALSE;
1690 
1691   if(!_selected_action || GPOINTER_TO_UINT(data_ptr) < NUM_CATEGORIES) return TRUE;
1692 
1693   dt_shortcut_t *s = g_sequence_get(data_ptr);
1694 
1695   if(_selected_action->type == DT_ACTION_TYPE_FALLBACK &&
1696      s->action->type == GPOINTER_TO_INT(_selected_action->target))
1697     return TRUE;
1698 
1699   for(dt_action_t *ac = s->action; ac; ac = ac->owner)
1700     if(ac == _selected_action)
1701       return TRUE;
1702 
1703   if(s->action->type == DT_ACTION_TYPE_FALLBACK)
1704     return _fallback_type_is_relevant(_selected_action, GPOINTER_TO_INT(s->action->target));
1705 
1706   return FALSE;
1707 }
1708 
_resize_shortcuts_view(GtkWidget * view,GdkRectangle * allocation,gpointer data)1709 static void _resize_shortcuts_view(GtkWidget *view, GdkRectangle *allocation, gpointer data)
1710 {
1711   dt_conf_set_int("shortcuts/window_split", gtk_paned_get_position(GTK_PANED(data)));
1712 }
1713 
1714 const dt_input_device_t DT_ALL_DEVICES = UINT8_MAX;
1715 static void _shortcuts_save(const gchar *shortcuts_file, const dt_input_device_t device);
1716 static void _shortcuts_load(const gchar *shortcuts_file, const dt_input_device_t file_dev, const dt_input_device_t load_dev, const gboolean clear);
1717 
_fallbacks_toggled(GtkToggleButton * button,gpointer data)1718 static void _fallbacks_toggled(GtkToggleButton *button, gpointer data)
1719 {
1720   dt_conf_set_bool("accel/enable_fallbacks",
1721                    (darktable.control->enable_fallbacks = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))));
1722 
1723   GtkTreeView *shortcuts_view = GTK_TREE_VIEW(data);
1724   gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(gtk_tree_view_get_model(shortcuts_view)));
1725 }
1726 
_restore_clicked(GtkButton * button,gpointer user_data)1727 static void _restore_clicked(GtkButton *button, gpointer user_data)
1728 {
1729   enum
1730   {
1731     _DEFAULTS = 1,
1732     _STARTUP,
1733     _EDITS,
1734   };
1735 
1736   GtkWidget *dialog = gtk_dialog_new_with_buttons(_("restore shortcuts"),
1737                                                   GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))),
1738                                                   GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
1739                                                   _("_cancel"), GTK_RESPONSE_REJECT,
1740                                                   _("_defaults"), _DEFAULTS,
1741                                                   _("_startup"), _STARTUP,
1742                                                   _("_edits"), _EDITS,
1743                                                   NULL);
1744   gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_REJECT);
1745 
1746   GtkContainer *content_area = GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG (dialog)));
1747   GtkWidget *label = gtk_label_new(_("restore default shortcuts\n  or as at startup\n  or when the configuration dialog was opened\n"));
1748   gtk_widget_set_halign(label, GTK_ALIGN_START);
1749   gtk_container_add(content_area, label);
1750   GtkWidget *clear = gtk_check_button_new_with_label(_("clear all newer shortcuts\n(instead of just restoring changed ones)"));
1751   gtk_container_add(content_area, clear);
1752 
1753   gtk_widget_show_all(GTK_WIDGET(content_area));
1754 
1755   const int resp = gtk_dialog_run(GTK_DIALOG(dialog));
1756   const gboolean wipe = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(clear));
1757 
1758   gtk_widget_destroy(dialog);
1759 
1760   switch(resp)
1761   {
1762   case _DEFAULTS:
1763     dt_shortcuts_load(".defaults", wipe);
1764     break;
1765   case _STARTUP:
1766     dt_shortcuts_load(".backup", wipe);
1767     break;
1768   case _EDITS:
1769     dt_shortcuts_load(".edit", wipe);
1770     break;
1771   }
1772 }
1773 
_import_export_dev_changed(GtkComboBox * widget,gpointer user_data)1774 static void _import_export_dev_changed(GtkComboBox *widget, gpointer user_data)
1775 {
1776   gint dev = gtk_combo_box_get_active(GTK_COMBO_BOX(widget));
1777   g_object_set_data(G_OBJECT(user_data), "device", GINT_TO_POINTER(dev));
1778   gtk_combo_box_set_active(GTK_COMBO_BOX(user_data), 1); // make sure changed triggered
1779   gtk_combo_box_set_active(GTK_COMBO_BOX(user_data), dev > 1 ? 0 : -1);
1780   gtk_widget_set_visible(gtk_widget_get_parent(GTK_WIDGET(user_data)), dev > 1);
1781 }
1782 
_export_id_changed(GtkComboBox * widget,gpointer user_data)1783 static void _export_id_changed(GtkComboBox *widget, gpointer user_data)
1784 {
1785   gint dev = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "device"));
1786   gint id = dev <= 1 ? 0 :
1787             gtk_combo_box_get_active(GTK_COMBO_BOX(widget)) + (dev-1) * 10;
1788 
1789   gint count = 0;
1790 
1791   for(GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
1792       !g_sequence_iter_is_end(iter);
1793       iter = g_sequence_iter_next(iter))
1794   {
1795     const dt_shortcut_t *s = g_sequence_get(iter);
1796     if(dev == 0 ||
1797        (id == 0 &&  s->key_device == id && s->move_device == id) ||
1798        (id != 0 && (s->key_device == id || s->move_device == id)))
1799       count++;
1800   }
1801 
1802   gchar *text = g_strdup_printf("%d %s", count, _("shortcuts"));
1803   gtk_label_set_text(GTK_LABEL(user_data), text);
1804   g_free(text);
1805 }
1806 
_export_clicked(GtkButton * button,gpointer user_data)1807 static void _export_clicked(GtkButton *button, gpointer user_data)
1808 {
1809   GtkWindow *win = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
1810 
1811   GtkWidget *dialog = gtk_dialog_new_with_buttons(_("export shortcuts"),
1812                                                   win, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
1813                                                   _("_cancel"), GTK_RESPONSE_REJECT,
1814                                                   _("_ok"), GTK_RESPONSE_OK,
1815                                                   NULL);
1816   gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_REJECT);
1817 
1818   GtkContainer *content_area = GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG (dialog)));
1819   GtkWidget *label = gtk_label_new(_("export all shortcuts to a file\nor just for one selected device\n"));
1820   gtk_widget_set_halign(label, GTK_ALIGN_START);
1821   gtk_container_add(content_area, label);
1822 
1823   GtkWidget *combo_dev = gtk_combo_box_text_new();
1824   gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev), _("all"));
1825   gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev), _("keyboard"));
1826   for(GSList *driver = darktable.control->input_drivers; driver; driver = driver->next)
1827     gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev),
1828                                    ((dt_input_driver_definition_t *)driver->data)->name);
1829   gtk_container_add(content_area, combo_dev);
1830 
1831   GtkWidget *device_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
1832 
1833   GtkWidget *combo_id = gtk_combo_box_text_new();
1834   for(gchar num[] = "0"; *num <= '9'; (*num)++)
1835     gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_id), num);
1836   gtk_container_add(GTK_CONTAINER(device_box), combo_id);
1837   gtk_container_add(GTK_CONTAINER(device_box), dt_ui_label_new(_("device id")));
1838 
1839   gtk_container_add(content_area, device_box);
1840 
1841   GtkWidget *count = gtk_label_new("");
1842   gtk_container_add(content_area, count);
1843 
1844   g_signal_connect(combo_dev, "changed", G_CALLBACK(_import_export_dev_changed), combo_id);
1845   g_signal_connect(combo_id, "changed", G_CALLBACK(_export_id_changed), count);
1846 
1847   gtk_widget_show_all(GTK_WIDGET(content_area));
1848 
1849   gtk_combo_box_set_active(GTK_COMBO_BOX(combo_dev), 0);
1850 
1851   const int resp = gtk_dialog_run(GTK_DIALOG(dialog));
1852 
1853   const gint dev = gtk_combo_box_get_active(GTK_COMBO_BOX(combo_dev));
1854   const gint id = dev == 0 ? DT_ALL_DEVICES :
1855                   dev == 1 ? 0 :
1856                   gtk_combo_box_get_active(GTK_COMBO_BOX(combo_id)) + (dev-1) * 10;
1857 
1858   gtk_widget_destroy(dialog);
1859 
1860   if(resp != GTK_RESPONSE_OK) return;
1861 
1862   GtkFileChooserNative *chooser = gtk_file_chooser_native_new(
1863         _("select file to export"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_SAVE,
1864         _("_export"), _("_cancel"));
1865 
1866   gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(chooser), TRUE);
1867   dt_conf_get_folder_to_file_chooser("ui_last/export_path", GTK_FILE_CHOOSER(chooser));
1868   gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(chooser), "shortcutsrc");
1869   if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(chooser)) == GTK_RESPONSE_ACCEPT)
1870   {
1871     gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(chooser));
1872 
1873     _shortcuts_save(filename, id);
1874     g_free(filename);
1875     dt_conf_set_folder_from_file_chooser("ui_last/export_path", GTK_FILE_CHOOSER(chooser));
1876   }
1877   g_object_unref(chooser);
1878 }
1879 
_import_id_changed(GtkComboBox * widget,gpointer user_data)1880 static void _import_id_changed(GtkComboBox *widget, gpointer user_data)
1881 {
1882   gint id = gtk_combo_box_get_active(widget);
1883   gtk_combo_box_set_active(GTK_COMBO_BOX(user_data), id);
1884 }
1885 
_import_clicked(GtkButton * button,gpointer user_data)1886 static void _import_clicked(GtkButton *button, gpointer user_data)
1887 {
1888   GtkWindow *win = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
1889 
1890   GtkWidget *dialog = gtk_dialog_new_with_buttons(_("import shortcuts"),
1891                                                   win, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
1892                                                   _("_cancel"), GTK_RESPONSE_REJECT,
1893                                                   _("_ok"), GTK_RESPONSE_OK,
1894                                                   NULL);
1895   gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_REJECT);
1896 
1897   GtkContainer *content_area = GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG (dialog)));
1898   GtkWidget *label = gtk_label_new(_("import all shortcuts from a file\nor just for one selected device\n"));
1899   gtk_widget_set_halign(label, GTK_ALIGN_START);
1900   gtk_container_add(content_area, label);
1901 
1902   GtkWidget *combo_dev = gtk_combo_box_text_new();
1903   gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev), _("all"));
1904   gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev), _("keyboard"));
1905   for(GSList *driver = darktable.control->input_drivers; driver; driver = driver->next)
1906     gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev),
1907                                    ((dt_input_driver_definition_t *)driver->data)->name);
1908   gtk_container_add(content_area, combo_dev);
1909 
1910   GtkWidget *device_grid = gtk_grid_new();
1911 
1912   GtkWidget *combo_from_id = gtk_combo_box_text_new();
1913   for(gchar num[] = "0"; *num <= '9'; (*num)++)
1914     gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_from_id), num);
1915   gtk_grid_attach(GTK_GRID(device_grid), combo_from_id, 0, 0, 1, 1);
1916   gtk_grid_attach(GTK_GRID(device_grid), dt_ui_label_new(_("id in file")), 1, 0, 1, 1);
1917 
1918   GtkWidget *combo_to_id = gtk_combo_box_text_new();
1919   for(gchar num[] = "0"; *num <= '9'; (*num)++)
1920     gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_to_id), num);
1921   gtk_grid_attach(GTK_GRID(device_grid), combo_to_id, 0, 1, 1, 1);
1922   gtk_grid_attach(GTK_GRID(device_grid), dt_ui_label_new(_("id when loaded")), 1, 1, 1, 1);
1923 
1924   gtk_container_add(content_area, device_grid);
1925 
1926   GtkWidget *clear = gtk_check_button_new_with_label(_("clear device first"));
1927   gtk_container_add(content_area, clear);
1928 
1929   g_signal_connect(combo_dev, "changed", G_CALLBACK(_import_export_dev_changed), combo_from_id);
1930   g_signal_connect(combo_from_id, "changed", G_CALLBACK(_import_id_changed), combo_to_id);
1931 
1932   gtk_widget_show_all(GTK_WIDGET(content_area));
1933 
1934   gtk_combo_box_set_active(GTK_COMBO_BOX(combo_dev), 0);
1935 
1936   const int resp = gtk_dialog_run(GTK_DIALOG(dialog));
1937   const gint dev = gtk_combo_box_get_active(GTK_COMBO_BOX(combo_dev));
1938   const gint from_id = dev == 0 ? DT_ALL_DEVICES :
1939                        dev == 1 ? 0 :
1940                        gtk_combo_box_get_active(GTK_COMBO_BOX(combo_from_id)) + (dev-1) * 10;
1941   const gint to_id = dev == 1 ? 0 :
1942                      gtk_combo_box_get_active(GTK_COMBO_BOX(combo_to_id)) + (dev-1) * 10;
1943   const gboolean wipe = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(clear));
1944 
1945   gtk_widget_destroy(dialog);
1946 
1947   if(resp != GTK_RESPONSE_OK) return;
1948 
1949   GtkFileChooserNative *chooser = gtk_file_chooser_native_new(
1950         _("select file to import"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_OPEN,
1951         _("_import"), _("_cancel"));
1952   gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(chooser), FALSE);
1953 
1954   dt_conf_get_folder_to_file_chooser("ui_last/import_path", GTK_FILE_CHOOSER(chooser));
1955   if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(chooser)) == GTK_RESPONSE_ACCEPT)
1956   {
1957     gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(chooser));
1958 
1959     if(wipe && from_id != DT_ALL_DEVICES)
1960     {
1961       GtkTreeModel *model = GTK_TREE_MODEL(_shortcuts_store);
1962       GtkTreeIter category;
1963       gboolean valid_category = gtk_tree_model_get_iter_first(model, &category);
1964       while(valid_category)
1965       {
1966         GtkTreeIter child;
1967         gboolean valid_child = gtk_tree_model_iter_children(model, &child, &category);
1968         while(valid_child)
1969         {
1970           gpointer child_data;
1971           gtk_tree_model_get(model, &child, 0, &child_data, -1);
1972 
1973           dt_shortcut_t *s = g_sequence_get(child_data);
1974           if((to_id == 0 &&  s->key_device == to_id && s->move_device == to_id) ||
1975              (to_id != 0 && (s->key_device == to_id || s->move_device == to_id)))
1976           {
1977             g_sequence_remove(child_data);
1978             valid_child = gtk_tree_store_remove(GTK_TREE_STORE(model), &child);
1979           }
1980           else
1981             valid_child = gtk_tree_model_iter_next(model, &child);
1982         }
1983         valid_category = gtk_tree_model_iter_next(model, &category);
1984       };
1985     }
1986 
1987     _shortcuts_load(filename, from_id, to_id, wipe && from_id == DT_ALL_DEVICES);
1988 
1989     g_free(filename);
1990     dt_conf_set_folder_from_file_chooser("ui_last/import_path", GTK_FILE_CHOOSER(chooser));
1991   }
1992   g_object_unref(chooser);
1993 
1994   dt_shortcuts_save(NULL, FALSE);
1995 }
1996 
dt_shortcuts_prefs(GtkWidget * widget)1997 GtkWidget *dt_shortcuts_prefs(GtkWidget *widget)
1998 {
1999   // Save the shortcuts before editing
2000   dt_shortcuts_save(".edit", FALSE);
2001 
2002   _selected_action = g_hash_table_lookup(darktable.control->widgets, widget);
2003   if(!_selected_action && widget)
2004     _selected_action = g_hash_table_lookup(darktable.control->widgets, gtk_widget_get_parent(widget));
2005   darktable.control->element = -1;
2006 
2007   GtkWidget *container = gtk_paned_new(GTK_ORIENTATION_VERTICAL);
2008 
2009   // Building the shortcut treeview
2010   g_set_weak_pointer(&_shortcuts_store, gtk_tree_store_new(1, G_TYPE_POINTER)); // static
2011 
2012   _add_shortcuts_to_tree();
2013 
2014   GtkTreeModel *filtered_shortcuts = gtk_tree_model_filter_new(GTK_TREE_MODEL(_shortcuts_store), NULL);
2015   g_object_unref(G_OBJECT(_shortcuts_store));
2016 
2017   gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filtered_shortcuts), _visible_shortcuts, NULL, NULL);
2018 
2019   GtkTreeView *shortcuts_view = GTK_TREE_VIEW(gtk_tree_view_new_with_model(filtered_shortcuts));
2020   g_object_unref(G_OBJECT(filtered_shortcuts));
2021   gtk_tree_view_set_search_column(shortcuts_view, 0); // fake column for _search_func
2022   gtk_tree_view_set_search_equal_func(shortcuts_view, _search_func, shortcuts_view, NULL);
2023   GtkWidget *search_shortcuts = gtk_search_entry_new();
2024   gtk_entry_set_placeholder_text(GTK_ENTRY(search_shortcuts), _("search shortcuts list"));
2025   gtk_widget_set_tooltip_text(GTK_WIDGET(search_shortcuts), _("incrementally search the list of shortcuts\npress up or down keys to cycle through matches"));
2026   g_signal_connect(G_OBJECT(search_shortcuts), "activate", G_CALLBACK(dt_gui_search_stop), shortcuts_view);
2027   g_signal_connect(G_OBJECT(search_shortcuts), "stop-search", G_CALLBACK(dt_gui_search_stop), shortcuts_view);
2028   gtk_tree_view_set_search_entry(shortcuts_view, GTK_ENTRY(search_shortcuts));
2029 
2030   gtk_tree_selection_set_select_function(gtk_tree_view_get_selection(shortcuts_view),
2031                                          _shortcut_selection_function, NULL, NULL);
2032   g_object_set(shortcuts_view, "has-tooltip", TRUE, NULL);
2033   g_signal_connect(G_OBJECT(shortcuts_view), "query-tooltip", G_CALLBACK(_shortcut_tooltip_callback), GINT_TO_POINTER(TRUE));
2034   g_signal_connect(G_OBJECT(shortcuts_view), "row-activated", G_CALLBACK(_shortcut_row_activated), filtered_shortcuts);
2035   g_signal_connect(G_OBJECT(shortcuts_view), "key-press-event", G_CALLBACK(_shortcut_key_pressed), NULL);
2036   g_signal_connect(G_OBJECT(shortcuts_view), "key-press-event", G_CALLBACK(dt_gui_search_start), search_shortcuts);
2037   g_signal_connect(G_OBJECT(_shortcuts_store), "row-inserted", G_CALLBACK(_shortcut_row_inserted), shortcuts_view);
2038 
2039   // Setting up the cell renderers
2040   _add_prefs_column(shortcuts_view, gtk_cell_renderer_text_new(), _("shortcut"), SHORTCUT_VIEW_DESCRIPTION);
2041 
2042   _add_prefs_column(shortcuts_view, gtk_cell_renderer_text_new(), _("action"), SHORTCUT_VIEW_ACTION);
2043 
2044   GtkCellRenderer *renderer = NULL;
2045 
2046   renderer = gtk_cell_renderer_combo_new();
2047   GtkListStore *elements = gtk_list_store_new(1, G_TYPE_STRING);
2048   g_object_set(renderer, "model", elements, "text-column", 0, "has-entry", FALSE, NULL);
2049   g_signal_connect(renderer, "editing-started" , G_CALLBACK(_element_editing_started), filtered_shortcuts);
2050   g_signal_connect(renderer, "changed", G_CALLBACK(_element_changed), filtered_shortcuts);
2051   _add_prefs_column(shortcuts_view, renderer, _("element"), SHORTCUT_VIEW_ELEMENT);
2052 
2053   renderer = gtk_cell_renderer_combo_new();
2054   GtkListStore *effects = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_INT);
2055   g_object_set(renderer, "model", effects, "text-column", 0, "has-entry", FALSE, NULL);
2056   g_signal_connect(renderer, "editing-started" , G_CALLBACK(_effect_editing_started), filtered_shortcuts);
2057   g_signal_connect(renderer, "changed", G_CALLBACK(_effect_changed), filtered_shortcuts);
2058   _add_prefs_column(shortcuts_view, renderer, _("effect"), SHORTCUT_VIEW_EFFECT);
2059 
2060   renderer = gtk_cell_renderer_spin_new();
2061   g_object_set(renderer, "adjustment", gtk_adjustment_new(1, -1000, 1000, .01, 1, 10),
2062                          "digits", 3, "xalign", 1.0, NULL);
2063   g_signal_connect(renderer, "edited", G_CALLBACK(_speed_edited), filtered_shortcuts);
2064   _add_prefs_column(shortcuts_view, renderer, _("speed"), SHORTCUT_VIEW_SPEED);
2065 
2066   renderer = gtk_cell_renderer_combo_new();
2067   GtkListStore *instances = gtk_list_store_new(1, G_TYPE_STRING);
2068   for(int i = 0; i < NUM_INSTANCES; i++)
2069     gtk_list_store_insert_with_values(instances, NULL, -1, 0, _(instance_label[i]), -1);
2070   for(char relative[] = "-2"; (relative[0] ^= '+' ^ '-') == '-' || ++relative[1] <= '9'; )
2071     gtk_list_store_insert_with_values(instances, NULL, -1, 0, relative, -1);
2072   g_object_set(renderer, "model", instances, "text-column", 0, "has-entry", FALSE, NULL);
2073   g_signal_connect(renderer, "edited", G_CALLBACK(_instance_edited), filtered_shortcuts);
2074   _add_prefs_column(shortcuts_view, renderer, _("instance"), SHORTCUT_VIEW_INSTANCE);
2075 
2076   // Adding the shortcuts treeview to its containers
2077   GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
2078   gtk_widget_set_size_request(scroll, -1, 100);
2079   gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2080   gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(shortcuts_view));
2081   gtk_paned_pack2(GTK_PANED(container), scroll, TRUE, FALSE);
2082 
2083   // Creating the action selection treeview
2084   g_set_weak_pointer(&_actions_store, gtk_tree_store_new(1, G_TYPE_POINTER)); // static
2085   GtkTreeIter found_iter = {};
2086   if(widget && !_selected_action)
2087   {
2088     const dt_view_t *active_view = dt_view_manager_get_current_view(darktable.view_manager);
2089     if(gtk_widget_is_ancestor(widget, dt_ui_center_base(darktable.gui->ui)) ||
2090        dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_CENTER_TOP, widget) ||
2091        dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_CENTER_BOTTOM, widget) ||
2092        gtk_widget_is_ancestor(widget, GTK_WIDGET(dt_ui_get_container(darktable.gui->ui,
2093                                                DT_UI_CONTAINER_PANEL_LEFT_TOP))) ||
2094        gtk_widget_is_ancestor(widget, GTK_WIDGET(dt_ui_get_container(darktable.gui->ui,
2095                                                DT_UI_CONTAINER_PANEL_RIGHT_TOP))))
2096       _selected_action = (dt_action_t*)active_view;
2097     else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_BOTTOM, widget))
2098       _selected_action = &darktable.control->actions_thumb;
2099     else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_RIGHT, widget))
2100       _selected_action = active_view->view(active_view) == DT_VIEW_DARKROOM
2101                        ? &darktable.control->actions_iops
2102                        : &darktable.control->actions_libs;
2103     else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_LEFT, widget))
2104       _selected_action = &darktable.control->actions_libs;
2105     else
2106       _selected_action = &darktable.control->actions_global;
2107   }
2108   _add_actions_to_tree(NULL, darktable.control->actions, _selected_action, &found_iter);
2109 
2110   GtkTreeView *actions_view = GTK_TREE_VIEW(gtk_tree_view_new_with_model(GTK_TREE_MODEL(_actions_store)));
2111   g_object_unref(_actions_store);
2112   gtk_tree_view_set_search_column(actions_view, 1); // fake column for _search_func
2113   gtk_tree_view_set_search_equal_func(actions_view, _search_func, actions_view, NULL);
2114   GtkWidget *search_actions = gtk_search_entry_new();
2115   gtk_entry_set_placeholder_text(GTK_ENTRY(search_actions), _("search actions list"));
2116   gtk_widget_set_tooltip_text(GTK_WIDGET(search_actions), _("incrementally search the list of actions\npress up or down keys to cycle through matches"));
2117   g_signal_connect(G_OBJECT(search_actions), "activate", G_CALLBACK(dt_gui_search_stop), actions_view);
2118   g_signal_connect(G_OBJECT(search_actions), "stop-search", G_CALLBACK(dt_gui_search_stop), actions_view);
2119   gtk_tree_view_set_search_entry(actions_view, GTK_ENTRY(search_actions));
2120 
2121   g_object_set(actions_view, "has-tooltip", TRUE, NULL);
2122   g_signal_connect(G_OBJECT(actions_view), "query-tooltip", G_CALLBACK(_shortcut_tooltip_callback), NULL);
2123   g_signal_connect(G_OBJECT(actions_view), "row-activated", G_CALLBACK(_action_row_activated), _actions_store);
2124   g_signal_connect(G_OBJECT(actions_view), "button-press-event", G_CALLBACK(_action_view_click), _actions_store);
2125   g_signal_connect(G_OBJECT(actions_view), "key-press-event", G_CALLBACK(dt_gui_search_start), search_actions);
2126   g_signal_connect(G_OBJECT(gtk_tree_view_get_selection(actions_view)), "changed",
2127                    G_CALLBACK(_action_selection_changed), shortcuts_view);
2128 
2129   renderer = gtk_cell_renderer_text_new();
2130   GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_("action"), renderer, NULL);
2131   gtk_tree_view_column_set_expand(column, TRUE);
2132   gtk_tree_view_column_set_cell_data_func(column, renderer, _fill_action_fields, GINT_TO_POINTER(TRUE), NULL);
2133   gtk_tree_view_append_column(GTK_TREE_VIEW(actions_view), column);
2134 
2135   renderer = gtk_cell_renderer_text_new();
2136   column = gtk_tree_view_column_new_with_attributes(_("type"), renderer, NULL);
2137   gtk_tree_view_column_set_alignment(column, 1.0);
2138   gtk_cell_renderer_set_alignment(renderer, 1.0, 0.0);
2139   gtk_tree_view_column_set_cell_data_func(column, renderer, _fill_action_fields, NULL, NULL);
2140   gtk_tree_view_append_column(GTK_TREE_VIEW(actions_view), column);
2141 
2142   // Adding the action treeview to its containers
2143   scroll = gtk_scrolled_window_new(NULL, NULL);
2144   gtk_widget_set_size_request(scroll, -1, 100);
2145   gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2146   gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(actions_view));
2147   gtk_paned_pack1(GTK_PANED(container), scroll, TRUE, FALSE);
2148 
2149   if(found_iter.user_data)
2150   {
2151     GtkTreeIter *send_iter = calloc(1, sizeof(GtkTreeIter));
2152     *send_iter = found_iter;
2153     gtk_widget_add_events(GTK_WIDGET(actions_view), GDK_STRUCTURE_MASK);
2154     g_signal_connect_data(G_OBJECT(actions_view), "map-event", G_CALLBACK(_action_view_map),
2155                           send_iter, (GClosureNotify)g_free, G_CONNECT_AFTER);
2156   }
2157 
2158   GtkTreePath *path = gtk_tree_path_new_first();
2159   gtk_tree_view_set_cursor(shortcuts_view, path, NULL, FALSE);
2160   gtk_tree_path_free(path);
2161 
2162   const int split_position = dt_conf_get_int("shortcuts/window_split");
2163   if(split_position) gtk_paned_set_position(GTK_PANED(container), split_position);
2164   g_signal_connect(G_OBJECT(shortcuts_view), "size-allocate", G_CALLBACK(_resize_shortcuts_view), container);
2165 
2166   GtkWidget *button_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0), *button = NULL;
2167   gtk_widget_set_name(button_bar, "shortcut_controls");
2168   gtk_box_pack_start(GTK_BOX(button_bar), search_actions, FALSE, FALSE, 0);
2169   gtk_box_pack_start(GTK_BOX(button_bar), search_shortcuts, FALSE, FALSE, 0);
2170 
2171   button = gtk_check_button_new_with_label(_("enable fallbacks"));
2172   gtk_widget_set_tooltip_text(button, _("enables default meanings for additional buttons, modifiers or moves\n"
2173                                         "when used in combination with a base shortcut"));
2174   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), darktable.control->enable_fallbacks);
2175   g_signal_connect(button, "toggled", G_CALLBACK(_fallbacks_toggled), shortcuts_view);
2176   gtk_box_pack_start(GTK_BOX(button_bar), button, TRUE, FALSE, 0);
2177 
2178   button = gtk_button_new_with_label(_("restore..."));
2179   gtk_widget_set_tooltip_text(button, _("restore default shortcuts or previous state"));
2180   g_signal_connect(button, "clicked", G_CALLBACK(_restore_clicked), NULL);
2181   gtk_box_pack_end(GTK_BOX(button_bar), button, FALSE, FALSE, 0);
2182 
2183   button = gtk_button_new_with_label(_("import..."));
2184   gtk_widget_set_tooltip_text(button, _("fully or partially import shortcuts from file"));
2185   g_signal_connect(button, "clicked", G_CALLBACK(_import_clicked), NULL);
2186   gtk_box_pack_end(GTK_BOX(button_bar), button, FALSE, FALSE, 0);
2187 
2188   button = gtk_button_new_with_label(_("export..."));
2189   gtk_widget_set_tooltip_text(button, _("fully or partially export shortcuts to file"));
2190   g_signal_connect(button, "clicked", G_CALLBACK(_export_clicked), NULL);
2191   gtk_box_pack_end(GTK_BOX(button_bar), button, FALSE, FALSE, 0);
2192 
2193   GtkWidget *top_level = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
2194   gtk_box_pack_start(GTK_BOX(top_level), container, TRUE, TRUE, 0);
2195   gtk_box_pack_start(GTK_BOX(top_level), button_bar, FALSE, FALSE, 0);
2196 
2197   return top_level;
2198 }
2199 
_shortcuts_save(const gchar * shortcuts_file,const dt_input_device_t device)2200 static void _shortcuts_save(const gchar *shortcuts_file, const dt_input_device_t device)
2201 {
2202   FILE *f = g_fopen(shortcuts_file, "wb");
2203   if(f)
2204   {
2205     for(GSequenceIter *i = g_sequence_get_begin_iter(darktable.control->shortcuts);
2206         !g_sequence_iter_is_end(i);
2207         i = g_sequence_iter_next(i))
2208     {
2209       dt_shortcut_t *s = g_sequence_get(i);
2210 
2211       if(device != DT_ALL_DEVICES &&
2212          (device != 0 ||  s->key_device != device || s->move_device != device) &&
2213          (device == 0 || (s->key_device != device && s->move_device != device)))
2214         continue;
2215 
2216       gchar *key_name = _shortcut_key_move_name(s->key_device, s->key, s->mods, FALSE);
2217       fprintf(f, "%s", key_name);
2218       g_free(key_name);
2219 
2220       if(s->move_device || s->move)
2221       {
2222         gchar *move_name = _shortcut_key_move_name(s->move_device, s->move, DT_MOVE_NAME, FALSE);
2223         fprintf(f, ";%s", move_name);
2224         g_free(move_name);
2225         if(s->direction)
2226           fprintf(f, ";%s", s->direction & DT_SHORTCUT_UP ? "up" : "down");
2227       }
2228 
2229       if(s->press  & DT_SHORTCUT_DOUBLE ) fprintf(f, ";%s", "double");
2230       if(s->press  & DT_SHORTCUT_TRIPLE ) fprintf(f, ";%s", "triple");
2231       if(s->press  & DT_SHORTCUT_LONG   ) fprintf(f, ";%s", "long");
2232       if(s->button & DT_SHORTCUT_LEFT   ) fprintf(f, ";%s", "left");
2233       if(s->button & DT_SHORTCUT_MIDDLE ) fprintf(f, ";%s", "middle");
2234       if(s->button & DT_SHORTCUT_RIGHT  ) fprintf(f, ";%s", "right");
2235       if(s->click  & DT_SHORTCUT_DOUBLE ) fprintf(f, ";%s", "double");
2236       if(s->click  & DT_SHORTCUT_TRIPLE ) fprintf(f, ";%s", "triple");
2237       if(s->click  & DT_SHORTCUT_LONG   ) fprintf(f, ";%s", "long");
2238 
2239       fprintf(f, "=");
2240 
2241       gchar *action_label = _action_full_id(s->action);
2242       fprintf(f, "%s", action_label);
2243       g_free(action_label);
2244 
2245       const dt_action_element_def_t *elements = _action_find_elements(s->action);
2246       if(s->element)
2247         fprintf(f, ";%s", elements[s->element].name);
2248       if(s->effect > (_shortcut_is_move(s) ? DT_ACTION_EFFECT_DEFAULT_MOVE
2249                                            : DT_ACTION_EFFECT_DEFAULT_KEY))
2250       {
2251         if(elements[s->element].effects == dt_action_effect_selection
2252            && s->effect > DT_ACTION_EFFECT_COMBO_SEPARATOR)
2253         {
2254           dt_introspection_type_enum_tuple_t *values
2255             = g_hash_table_lookup(darktable.control->combo_introspection, s->action);
2256           if(values)
2257             fprintf(f, ";item:%s", values[s->effect - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1].description);
2258           else
2259           {
2260             gchar **strings
2261               = g_hash_table_lookup(darktable.control->combo_list, s->action);
2262             if(strings)
2263               fprintf(f, ";item:%s", strings[s->effect - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1]);
2264           }
2265         }
2266         else
2267           fprintf(f, ";%s", elements[s->element].effects[s->effect]);
2268      }
2269 
2270       if(s->instance == -1) fprintf(f, ";last");
2271       if(s->instance == +1) fprintf(f, ";first");
2272       if(abs(s->instance) > 1) fprintf(f, ";%+d", s->instance);
2273       if(s->speed != 1.0) fprintf(f, ";*%g", s->speed);
2274 
2275       fprintf(f, "\n");
2276     }
2277 
2278     fclose(f);
2279   }
2280 }
2281 
dt_shortcuts_save(const gchar * ext,const gboolean backup)2282 void dt_shortcuts_save(const gchar *ext, const gboolean backup)
2283 {
2284   char shortcuts_file[PATH_MAX] = { 0 };
2285   dt_loc_get_user_config_dir(shortcuts_file, sizeof(shortcuts_file));
2286   g_strlcat(shortcuts_file, "/shortcutsrc", PATH_MAX);
2287   if(ext) g_strlcat(shortcuts_file, ext, PATH_MAX);
2288   if(backup)
2289   {
2290     gchar *backup_file = g_strdup_printf("%s.backup", shortcuts_file);
2291     g_rename(shortcuts_file, backup_file);
2292     g_free(backup_file);
2293   }
2294 
2295   _shortcuts_save(shortcuts_file, DT_ALL_DEVICES);
2296 }
2297 
_find_combo_effect(const gchar ** effects,const gchar * token,dt_action_t * ac,gint * ef)2298 static gboolean _find_combo_effect(const gchar **effects, const gchar *token, dt_action_t *ac, gint *ef)
2299 {
2300   if(effects == dt_action_effect_selection && g_strstr_len(token, 5, "item:"))
2301   {
2302     int effect = -1;
2303     const char *entry = NULL;
2304 
2305     dt_introspection_type_enum_tuple_t *values
2306       = g_hash_table_lookup(darktable.control->combo_introspection, ac);
2307     if(values)
2308     {
2309       while((entry = values[++effect].description))
2310         if(!strcmp(token + 5, entry)) break;
2311     }
2312     else
2313     {
2314       gchar **strings
2315         = g_hash_table_lookup(darktable.control->combo_list, ac);
2316       if(strings)
2317       {
2318         while((entry = strings[++effect]))
2319           if(!strcmp(token + 5, entry)) break;
2320       }
2321     }
2322     if(entry)
2323     {
2324       *ef = effect + DT_ACTION_EFFECT_COMBO_SEPARATOR + 1;
2325       return TRUE;
2326     }
2327   }
2328 
2329   return FALSE;
2330 }
2331 
_shortcuts_load(const gchar * shortcuts_file,dt_input_device_t file_dev,const dt_input_device_t load_dev,const gboolean clear)2332 static void _shortcuts_load(const gchar *shortcuts_file, dt_input_device_t file_dev, const dt_input_device_t load_dev, const gboolean clear)
2333 {
2334   // start with an empty shortcuts collection
2335   if(clear && darktable.control->shortcuts)
2336   {
2337     if(_shortcuts_store) gtk_tree_store_clear(_shortcuts_store);
2338 
2339     g_sequence_free(darktable.control->shortcuts);
2340     darktable.control->shortcuts = g_sequence_new(g_free);
2341 
2342     if(_shortcuts_store) _add_shortcuts_to_tree();
2343   }
2344 
2345   FILE *f = g_fopen(shortcuts_file, "rb");
2346   if(f)
2347   {
2348     while(!feof(f))
2349     {
2350       char line[1024];
2351       char *read = fgets(line, sizeof(line), f);
2352       if(read > 0)
2353       {
2354         line[strcspn(line, "\r\n")] = '\0';
2355 
2356         char *act_start = strchr(line, '=');
2357         if(!act_start)
2358         {
2359           fprintf(stderr, "[dt_shortcuts_load] line '%s' is not an assignment\n", line);
2360           continue;
2361         }
2362 
2363         dt_shortcut_t s = { .speed = 1 };
2364 
2365         char *token = strtok(line, "=;");
2366         if(strcmp(token, "None"))
2367         {
2368           char *colon = strchr(token, ':');
2369           if(!colon)
2370           {
2371             gtk_accelerator_parse(token, &s.key, &s.mods);
2372             if(s.mods) fprintf(stderr, "[dt_shortcuts_load] unexpected modifiers found in %s\n", token);
2373             if(!s.key) fprintf(stderr, "[dt_shortcuts_load] no key name found in %s\n", token);
2374           }
2375           else
2376           {
2377             char *key_start = colon + 1;
2378             *colon-- = 0;
2379             if(colon == token)
2380             {
2381               fprintf(stderr, "[dt_shortcuts_load] missing driver name in %s\n", token);
2382               continue;
2383             }
2384             dt_input_device_t id = *colon - '0';
2385             if(id > 9 )
2386               id = 0;
2387             else
2388               *colon-- = 0;
2389 
2390             GSList *driver = darktable.control->input_drivers;
2391             while(driver)
2392             {
2393               id += 10;
2394               dt_input_driver_definition_t *callbacks = driver->data;
2395               if(!strcmp(token, callbacks->name))
2396               {
2397                 if(!callbacks->string_to_key(key_start, &s.key))
2398                   fprintf(stderr, "[dt_shortcuts_load] key not recognised in %s\n", key_start);
2399 
2400                 s.key_device = id;
2401                 break;
2402               }
2403               driver = driver->next;
2404             }
2405             if(!driver)
2406             {
2407               fprintf(stderr, "[dt_shortcuts_load] '%s' is not a valid driver\n", token);
2408               continue;
2409             }
2410           }
2411         }
2412 
2413         while((token = strtok(NULL, "=;")) && token < act_start)
2414         {
2415           char *colon = strchr(token, ':');
2416           if(!colon)
2417           {
2418             int mod = -1;
2419             while(modifier_string[++mod].modifier)
2420               if(!strcmp(token, modifier_string[mod].name)) break;
2421             if(modifier_string[mod].modifier)
2422             {
2423               s.mods |= modifier_string[mod].modifier;
2424               continue;
2425             }
2426 
2427             if(!strcmp(token, "left"  )) { s.button |= DT_SHORTCUT_LEFT  ; continue; }
2428             if(!strcmp(token, "middle")) { s.button |= DT_SHORTCUT_MIDDLE; continue; }
2429             if(!strcmp(token, "right" )) { s.button |= DT_SHORTCUT_RIGHT ; continue; }
2430 
2431             if(s.button)
2432             {
2433               if(!strcmp(token, "double")) { s.click |= DT_SHORTCUT_DOUBLE; continue; }
2434               if(!strcmp(token, "triple")) { s.click |= DT_SHORTCUT_TRIPLE; continue; }
2435               if(!strcmp(token, "long"  )) { s.click |= DT_SHORTCUT_LONG  ; continue; }
2436             }
2437             else
2438             {
2439               if(!strcmp(token, "double")) { s.press |= DT_SHORTCUT_DOUBLE; continue; }
2440               if(!strcmp(token, "triple")) { s.press |= DT_SHORTCUT_TRIPLE; continue; }
2441               if(!strcmp(token, "long"  )) { s.press |= DT_SHORTCUT_LONG  ; continue; }
2442             }
2443 
2444             int move = 0;
2445             while(move_string[++move])
2446               if(!strcmp(token, move_string[move])) break;
2447             if(move_string[move])
2448             {
2449               s.move = move;
2450               continue;
2451             }
2452 
2453             if(!strcmp(token, "up"  )) { s.direction = DT_SHORTCUT_UP  ; continue; }
2454             if(!strcmp(token, "down")) { s.direction= DT_SHORTCUT_DOWN; continue; }
2455 
2456             fprintf(stderr, "[dt_shortcuts_load] token '%s' not recognised\n", token);
2457           }
2458           else
2459           {
2460             char *move_start = colon + 1;
2461             *colon-- = 0;
2462             if(colon == token)
2463             {
2464               fprintf(stderr, "[dt_shortcuts_load] missing driver name in %s\n", token);
2465               continue;
2466             }
2467             dt_input_device_t id = *colon - '0';
2468             if(id > 9 )
2469               id = 0;
2470             else
2471               *colon-- = 0;
2472 
2473             GSList *driver = darktable.control->input_drivers;
2474             while(driver)
2475             {
2476               id += 10;
2477               const dt_input_driver_definition_t *callbacks = driver->data;
2478               if(!strcmp(token, callbacks->name))
2479               {
2480                 if(!callbacks->string_to_move(move_start, &s.move))
2481                   fprintf(stderr, "[dt_shortcuts_load] move not recognised in %s\n", move_start);
2482 
2483                 s.move_device = id;
2484                 break;
2485               }
2486               driver = driver->next;
2487             }
2488             if(!driver)
2489             {
2490               fprintf(stderr, "[dt_shortcuts_load] '%s' is not a valid driver\n", token);
2491               continue;
2492             }
2493           }
2494         }
2495 
2496         // find action
2497         gchar **path = g_strsplit(token, "/", 0);
2498         s.action = dt_action_locate(NULL, path, FALSE);
2499         g_strfreev(path);
2500 
2501         if(!s.action)
2502         {
2503           fprintf(stderr, "[dt_shortcuts_load] action path '%s' not found\n", token);
2504           continue;
2505         }
2506 
2507         const dt_action_element_def_t *elements = _action_find_elements(s.action);
2508         const gchar **effects = NULL;
2509         const gint default_effect = s.effect = _shortcut_is_move(&s)
2510                                              ? DT_ACTION_EFFECT_DEFAULT_MOVE
2511                                              : DT_ACTION_EFFECT_DEFAULT_KEY;
2512 
2513         while((token = strtok(NULL, ";")))
2514         {
2515           if(elements)
2516           {
2517             int element = -1;
2518             while(elements[++element].name)
2519               if(!strcmp(token, elements[element].name)) break;
2520             if(elements[element].name)
2521             {
2522               s.element = element;
2523               s.effect = default_effect; // reset if an effect for a different element was found first
2524               continue;
2525             }
2526 
2527             effects = elements[s.element].effects;
2528 
2529             if(_find_combo_effect(effects, token, s.action, &s.effect)) continue;
2530 
2531             int effect = -1;
2532             while(effects[++effect])
2533               if(!strcmp(token, effects[effect])) break;
2534             if(effects[effect])
2535             {
2536               s.effect = effect;
2537               continue;
2538             }
2539           }
2540 
2541           if(!strcmp(token, "first")) s.instance =  1; else
2542           if(!strcmp(token, "last" )) s.instance = -1; else
2543           if(*token == '+' || *token == '-') sscanf(token, "%d", &s.instance); else
2544           if(*token == '*') sscanf(token, "*%g", &s.speed); else
2545           fprintf(stderr, "[dt_shortcuts_load] token '%s' not recognised\n", token);
2546         }
2547 
2548         if(file_dev == DT_ALL_DEVICES ||
2549            (file_dev == 0 &&  s.key_device == file_dev && s.move_device == file_dev) ||
2550            (file_dev != 0 && (s.key_device == file_dev || s.move_device == file_dev)))
2551         {
2552           if(file_dev != 0)
2553           {
2554             if(s.key_device  == file_dev) s.key_device  = load_dev;
2555             if(s.move_device == file_dev) s.move_device = load_dev;
2556           }
2557 
2558           _insert_shortcut(&s, FALSE);
2559         }
2560       }
2561     }
2562     fclose(f);
2563   }
2564 }
2565 
dt_shortcuts_load(const gchar * ext,const gboolean clear)2566 void dt_shortcuts_load(const gchar *ext, const gboolean clear)
2567 {
2568   char shortcuts_file[PATH_MAX] = { 0 };
2569   dt_loc_get_user_config_dir(shortcuts_file, sizeof(shortcuts_file));
2570   g_strlcat(shortcuts_file, "/shortcutsrc", PATH_MAX);
2571   if(ext) g_strlcat(shortcuts_file, ext, PATH_MAX);
2572   if(!g_file_test(shortcuts_file, G_FILE_TEST_EXISTS))
2573     return;
2574 
2575   _shortcuts_load(shortcuts_file, DT_ALL_DEVICES, DT_ALL_DEVICES, clear);
2576 }
2577 
dt_shortcuts_reinitialise()2578 void dt_shortcuts_reinitialise()
2579 {
2580   for(GSList *d = darktable.control->input_drivers; d; d = d->next)
2581   {
2582     const dt_input_driver_definition_t *driver = d->data;
2583     driver->module->gui_cleanup(driver->module);
2584     driver->module->gui_init(driver->module);
2585   }
2586 
2587   // reload shortcuts
2588   dt_shortcuts_load(NULL, TRUE);
2589 
2590   char actions_file[PATH_MAX] = { 0 };
2591   dt_loc_get_user_config_dir(actions_file, sizeof(actions_file));
2592   g_strlcat(actions_file, "/all_actions", PATH_MAX);
2593   FILE *f = g_fopen(actions_file, "wb");
2594   _dump_actions(f, darktable.control->actions);
2595   fclose(f);
2596 
2597   dt_control_log(_("input devices reinitialised"));
2598 }
2599 
dt_shortcuts_select_view(dt_view_type_flags_t view)2600 void dt_shortcuts_select_view(dt_view_type_flags_t view)
2601 {
2602   g_sequence_sort(darktable.control->shortcuts, _shortcut_compare_func, GINT_TO_POINTER(view));
2603 }
2604 
2605 static GSList *_pressed_keys = NULL, *_hold_keys = NULL; // lists of currently pressed and held keys
2606 static guint _pressed_button = 0;
2607 static guint _last_time = 0, _last_mapping_time = 0;
2608 static guint _timeout_source = 0;
2609 static guint _focus_loss_key = 0;
2610 static guint _focus_loss_press = 0;
2611 
_lookup_mapping_widget()2612 static void _lookup_mapping_widget()
2613 {
2614   if(_sc.action) return;
2615   _sc.action = g_hash_table_lookup(darktable.control->widgets, darktable.control->mapping_widget);
2616   if(!_sc.action) return;
2617 
2618   _sc.instance = 0;
2619   if(dt_conf_get_bool("accel/assign_instance") && _sc.action->target != darktable.control->mapping_widget)
2620   {
2621     // find relative module instance
2622     dt_action_t *owner = _sc.action;
2623     while(owner && owner->type != DT_ACTION_TYPE_IOP) owner = owner->owner;
2624     if(owner)
2625     {
2626       GtkWidget *expander = gtk_widget_get_ancestor(darktable.control->mapping_widget, DTGTK_TYPE_EXPANDER);
2627 
2628       dt_iop_module_so_t *module = (dt_iop_module_so_t *)owner;
2629 
2630       dt_iop_module_t *preferred = dt_iop_get_module_preferred_instance(module);
2631 
2632       if(expander != preferred->expander)
2633       {
2634         int current_instance = 0;
2635         for(GList *iop_mods = darktable.develop->iop;
2636             iop_mods;
2637             iop_mods = g_list_next(iop_mods))
2638         {
2639           const dt_iop_module_t *mod = (dt_iop_module_t *)iop_mods->data;
2640 
2641           if(mod->so == module && mod->iop_order != INT_MAX)
2642           {
2643             current_instance++;
2644 
2645             if(mod->expander == expander)
2646               _sc.instance = current_instance; // and continue counting
2647           }
2648         }
2649 
2650         if(current_instance + 1 - _sc.instance < _sc.instance) _sc.instance -= current_instance + 1;
2651       }
2652     }
2653   }
2654 
2655   _sc.element = 0;
2656   const dt_action_def_t *def = _action_find_definition(_sc.action);
2657   if(def && def->elements && def->elements[0].name)
2658     _sc.element = darktable.control->element;
2659 }
2660 
_widget_invisible(GtkWidget * w)2661 static gboolean _widget_invisible(GtkWidget *w)
2662 {
2663   GtkWidget *p = gtk_widget_get_parent(w);
2664   return (!GTK_IS_WIDGET(w) ||
2665           !gtk_widget_get_visible(w) ||
2666           (g_strcmp0(gtk_widget_get_name(p), "lib-plugin-ui-main") && !gtk_widget_get_visible(p)));
2667 }
2668 
_shortcut_closest_match(GSequenceIter ** current,dt_shortcut_t * s,gboolean * fully_matched,const dt_action_def_t * def,char ** fb_log)2669 gboolean _shortcut_closest_match(GSequenceIter **current, dt_shortcut_t *s, gboolean *fully_matched, const dt_action_def_t *def, char **fb_log)
2670 {
2671   *current = g_sequence_iter_prev(*current);
2672   dt_shortcut_t *c = g_sequence_get(*current);
2673 //dt_print(DT_DEBUG_INPUT, "  [_shortcut_closest_match] shortcut considered: %s\n", _shortcut_description(c));
2674 
2675   gboolean applicable;
2676   while((applicable =
2677            (c->key_device == s->key_device && c->key == s->key && c->press >= (s->press & ~DT_SHORTCUT_LONG) &&
2678            ((!c->move_device && !c->move) ||
2679              (c->move_device == s->move_device && c->move == s->move)) &&
2680            (!s->action || s->action->type != DT_ACTION_TYPE_FALLBACK ||
2681             s->action->target == c->action->target))) &&
2682         !g_sequence_iter_is_begin(*current) &&
2683         (((c->button || c->click) && (c->button != s->button || c->click != s->click)) ||
2684          (c->mods       && c->mods != s->mods ) ||
2685          (c->direction  & ~s->direction       ) ||
2686          (c->element    && s->element         ) ||
2687          (c->effect > 0 && s->effect > 0      ) ||
2688          (c->instance   && s->instance        ) ||
2689          (c->element    && s->effect > 0 && def &&
2690           def->elements[c->element].effects != def->elements[s->element].effects ) ))
2691   {
2692     *current = g_sequence_iter_prev(*current);
2693     c = g_sequence_get(*current);
2694 //dt_print(DT_DEBUG_INPUT, "  [_shortcut_closest_match] shortcut considered: %s\n", _shortcut_description(c));
2695   }
2696 
2697   if(applicable)
2698   {
2699     s->key_device   =  0;
2700     s->key          =  0;
2701     s->mods        &= ~c->mods;
2702     s->press       -=  c->press;
2703     s->button      &= ~c->button;
2704     s->click       -=  c->click;
2705     s->direction   &= ~c->direction;
2706     s->move_device -=  c->move_device;
2707     s->move        -=  c->move;
2708 
2709     if(c->element) s->element = c->element;
2710     if(c->effect > DT_ACTION_EFFECT_DEFAULT_KEY) s->effect = c->effect;
2711     if(c->instance) s->instance = c->instance;
2712 
2713     s->speed *= c->speed;
2714     s->action = c->action;
2715 
2716     *fully_matched = !(s->mods || s->press || s->button || s->click || s->move_device || s->move);
2717 
2718     if(*fb_log)
2719       *fb_log = dt_util_dstrcat(*fb_log, "\n%s \u2192 %s", _shortcut_description(c), _action_description(c, 2));
2720 
2721     return TRUE;
2722   }
2723   else
2724   {
2725     *fully_matched = FALSE;
2726     return FALSE;
2727   }
2728 }
2729 
_shortcut_match(dt_shortcut_t * f,gchar ** fb_log)2730 static gboolean _shortcut_match(dt_shortcut_t *f, gchar **fb_log)
2731 {
2732   f->views = darktable.view_manager->current_view->view(darktable.view_manager->current_view);
2733   gpointer v = GINT_TO_POINTER(f->views);
2734 
2735   GSequenceIter *existing = g_sequence_search(darktable.control->shortcuts, f, _shortcut_compare_func, v);
2736 
2737   gboolean matched = FALSE;
2738 
2739   if(!_shortcut_closest_match(&existing, f, &matched, NULL, fb_log))
2740   {
2741     // see if there is a fallback from midi knob press to knob turn
2742     if(!f->key_device || f->move_device || f->move)
2743       return FALSE;
2744 
2745     dt_input_device_t id = f->key_device;
2746     GSList *driver = darktable.control->input_drivers;
2747     while(driver && (id -= 10) >= 10)
2748       driver = driver->next;
2749 
2750     if(!driver)
2751       return FALSE;
2752     else
2753     {
2754       dt_input_driver_definition_t *callbacks = driver->data;
2755 
2756       if(callbacks->key_to_move &&
2757          callbacks->key_to_move(callbacks->module, f->key_device, f->key, &f->move))
2758       {
2759         f->move_device = f->key_device;
2760         f->key_device = 0;
2761         f->key = 0;
2762 
2763         existing = g_sequence_search(darktable.control->shortcuts, f, _shortcut_compare_func, v);
2764         if(!_shortcut_closest_match(&existing, f, &matched, NULL, fb_log) && !f->action)
2765           return FALSE;
2766         else
2767         {
2768           const dt_action_def_t *def = _action_find_definition(f->action);
2769 
2770           if(def && def->elements
2771              && (def->elements[f->element].effects == dt_action_effect_value
2772                  || def->elements[f->element].effects == dt_action_effect_selection))
2773             f->effect = DT_ACTION_EFFECT_RESET;
2774         }
2775       }
2776     }
2777   }
2778 
2779   if(!matched && f->action && darktable.control->enable_fallbacks)
2780   {
2781     // try to add fallbacks
2782     f->views = 0;
2783 
2784     dt_action_t *matched_action = f->action;
2785     dt_action_t fallback_action = { .type = DT_ACTION_TYPE_FALLBACK,
2786                                     .target = GINT_TO_POINTER(matched_action->type) };
2787     f->action = &fallback_action;
2788 
2789     const dt_action_def_t *def = _action_find_definition(matched_action);
2790 
2791     existing = g_sequence_search(darktable.control->shortcuts, f, _shortcut_compare_func, v);
2792     while(_shortcut_closest_match(&existing, f, &matched, def, fb_log) && !matched) {};
2793 
2794     if(!matched && def && def->elements[f->element].effects == dt_action_effect_value)
2795     {
2796       static dt_action_t value_action = { .type = DT_ACTION_TYPE_FALLBACK,
2797                                           .target = GINT_TO_POINTER(DT_ACTION_TYPE_VALUE_FALLBACK) };
2798       f->action = &value_action;
2799       existing = g_sequence_search(darktable.control->shortcuts, f, _shortcut_compare_func, v);
2800       while(_shortcut_closest_match(&existing, f, &matched, def, fb_log) && !matched) {};
2801     }
2802 
2803     if(f->move && !f->move_device && !(f->mods || f->press || f->button || f->click))
2804     {
2805       if(*fb_log)
2806         *fb_log = dt_util_dstrcat(*fb_log, "\n%s \u2192 %s", _shortcut_description(f), _("fallback to move"));
2807 
2808       f->effect = DT_ACTION_EFFECT_DEFAULT_MOVE;
2809       f->move = 0;
2810     }
2811 
2812     f->action = matched_action;
2813   }
2814 
2815   return f->action != NULL && !f->move;
2816 }
2817 
2818 
_process_action(dt_action_t * action,int instance,dt_action_element_t element,dt_action_effect_t effect,float move_size)2819 static float _process_action(dt_action_t *action, int instance,
2820                              dt_action_element_t element, dt_action_effect_t effect, float move_size)
2821 {
2822   float return_value = NAN;
2823 
2824   dt_action_t *owner = action;
2825   while(owner && owner->type >= DT_ACTION_TYPE_SECTION) owner = owner->owner;
2826 
2827   gpointer action_target = action->type == DT_ACTION_TYPE_LIB ? action : action->target;
2828 
2829   if(owner && owner->type == DT_ACTION_TYPE_IOP)
2830   {
2831     // find module instance
2832     dt_iop_module_so_t *module = (dt_iop_module_so_t *)owner;
2833 
2834     if(instance)
2835     {
2836       int current_instance = abs(instance);
2837 
2838       dt_iop_module_t *mod = NULL;
2839 
2840       for(GList *iop_mods = instance >= 0
2841                           ? darktable.develop->iop
2842                           : g_list_last(darktable.develop->iop);
2843           iop_mods;
2844           iop_mods = instance >= 0
2845                   ? g_list_next(iop_mods)
2846                   : g_list_previous(iop_mods))
2847       {
2848         mod = (dt_iop_module_t *)iop_mods->data;
2849 
2850         if(mod->so == module
2851             && mod->iop_order != INT_MAX
2852             && !--current_instance)
2853           break;
2854       }
2855 
2856       // find module instance widget
2857       if(mod && action->type >= DT_ACTION_TYPE_PER_INSTANCE)
2858       {
2859         for(GSList *w = mod->widget_list; w; w = w->next)
2860         {
2861           const dt_action_target_t *referral = w->data;
2862           if(referral->action == action)
2863           {
2864             action_target = referral->target;
2865             break;
2866           }
2867         }
2868       }
2869       else
2870         action_target = mod;
2871     }
2872     else if(action->type == DT_ACTION_TYPE_IOP
2873             || action->type == DT_ACTION_TYPE_PRESET)
2874     {
2875       action_target = dt_iop_get_module_preferred_instance((dt_iop_module_so_t *)owner);
2876     }
2877   }
2878 
2879   if(action->type == DT_ACTION_TYPE_CLOSURE && action->target && !isnan(move_size))
2880   {
2881     typedef gboolean (*accel_callback)(GtkAccelGroup *accel_group, GObject *acceleratable,
2882                                        guint keyval, GdkModifierType modifier, gpointer p);
2883     ((accel_callback)((GCClosure*)action_target)->callback)(NULL, NULL, _sc.key, _sc.mods,
2884                                                             ((GClosure*)action_target)->data);
2885   }
2886   else if(action->type == DT_ACTION_TYPE_PRESET && owner && !isnan(move_size))
2887   {
2888     if(owner->type == DT_ACTION_TYPE_LIB)
2889     {
2890       const dt_lib_module_t *lib = (dt_lib_module_t *)owner;
2891       dt_lib_presets_apply(action->label, lib->plugin_name, lib->version());
2892     }
2893     else if(owner->type == DT_ACTION_TYPE_IOP)
2894     {
2895       gchar *text = g_strdup_printf("\napplying preset '%s'", action->label);
2896       dt_action_widget_toast(action_target, NULL, text);
2897       g_free(text);
2898 
2899       dt_gui_presets_apply_preset(action->label, action_target);
2900     }
2901     else
2902       fprintf(stderr, "[process_action] preset '%s' has unsupported type\n", action->label);
2903   }
2904   else
2905   {
2906     const dt_action_def_t *definition = _action_find_definition(action);
2907 
2908     if(definition && definition->process
2909         && (action->type < DT_ACTION_TYPE_WIDGET
2910             || definition->no_widget
2911             || !_widget_invisible(action_target)))
2912       return_value = definition->process(action_target, element, effect, move_size);
2913     else if(!isnan(move_size))
2914       dt_action_widget_toast(action, action_target, "not active");
2915   }
2916 
2917   return return_value;
2918 }
2919 
_ungrab_grab_widget()2920 static void _ungrab_grab_widget()
2921 {
2922   gdk_seat_ungrab(gdk_display_get_default_seat(gdk_display_get_default()));
2923 
2924   g_slist_free_full(_pressed_keys, g_free);
2925   _pressed_keys = NULL;
2926 
2927   if(_grab_widget)
2928   {
2929     gtk_widget_set_sensitive(_grab_widget, TRUE);
2930     gtk_widget_set_tooltip_text(_grab_widget, NULL);
2931     g_signal_handlers_disconnect_by_func(gtk_widget_get_toplevel(_grab_widget), G_CALLBACK(dt_shortcut_dispatcher), NULL);
2932     _grab_widget = NULL;
2933   }
2934 }
2935 
_ungrab_at_focus_loss()2936 static void _ungrab_at_focus_loss()
2937 {
2938   _grab_window = NULL;
2939   _focus_loss_key = _sc.key;
2940   _focus_loss_press = _sc.press;
2941   _ungrab_grab_widget();
2942   _sc = (dt_shortcut_t) { 0 };
2943 }
2944 
_process_shortcut(float move_size)2945 static float _process_shortcut(float move_size)
2946 {
2947   float return_value = NAN;
2948 
2949   dt_shortcut_t fsc = _sc;
2950   fsc.action = NULL;
2951   fsc.element  = 0;
2952 
2953   static int consecutive_unmatched = 0;
2954 
2955   gchar *fb_log = darktable.control->mapping_widget && !isnan(move_size)
2956                 ? g_strdup_printf("[ %s ]", _shortcut_description(&fsc))
2957                 : NULL;
2958 
2959   if(_shortcut_match(&fsc, &fb_log))
2960   {
2961     consecutive_unmatched = 0;
2962 
2963     move_size *= fsc.speed;
2964 
2965     if(fsc.effect == DT_ACTION_EFFECT_DEFAULT_MOVE)
2966     {
2967       if(move_size < .0f)
2968       {
2969         fsc.effect = DT_ACTION_EFFECT_DEFAULT_DOWN;
2970         move_size *= -1;
2971       }
2972       else
2973         fsc.effect = DT_ACTION_EFFECT_DEFAULT_UP;
2974     }
2975 
2976     return_value =  _process_action(fsc.action, fsc.instance, fsc.element, fsc.effect, move_size);
2977   }
2978   else if(!isnan(move_size) && !fsc.action)
2979   {
2980     dt_toast_log(_("%s not assigned"), _shortcut_description(&_sc));
2981 
2982     if(++consecutive_unmatched >= 3)
2983     {
2984       _ungrab_at_focus_loss();
2985       consecutive_unmatched = 0;
2986     }
2987   }
2988 
2989   if(fb_log)
2990   {
2991     dt_control_log("%s", fb_log);
2992     g_free(fb_log);
2993   }
2994 
2995   return return_value;
2996 }
2997 
dt_action_process(const gchar * action,int instance,const gchar * element,const gchar * effect,float move_size)2998 float dt_action_process(const gchar *action, int instance, const gchar *element, const gchar *effect, float move_size)
2999 {
3000   gchar **path = g_strsplit(action, "/", 0);
3001   dt_action_t *ac = dt_action_locate(NULL, path, FALSE);
3002   g_strfreev(path);
3003 
3004   if(!ac)
3005   {
3006     fprintf(stderr, "[dt_action_process] action path '%s' not found\n", action);
3007     return NAN;;
3008   }
3009 
3010   const dt_view_type_flags_t vws = _find_views(ac);
3011   if(!(vws & darktable.view_manager->current_view->view(darktable.view_manager->current_view)))
3012   {
3013     fprintf(stderr, "[dt_action_process] action '%s' not valid for current view\n", action);
3014     return NAN;;
3015   }
3016 
3017   dt_action_element_t el = DT_ACTION_ELEMENT_DEFAULT;
3018   dt_action_effect_t ef = DT_ACTION_EFFECT_DEFAULT_KEY;
3019   if((element && *element) || (effect && *effect))
3020   {
3021     const dt_action_element_def_t *elements = _action_find_elements(ac);
3022     if(elements)
3023     {
3024       if(element && *element)
3025       {
3026         while(elements[el].name && strcmp(elements[el].name, element)) el++;
3027 
3028         if(!elements[el].name)
3029         {
3030           fprintf(stderr, "[dt_action_process] element '%s' not valid for action '%s'\n", element, action);
3031           return NAN;;
3032         }
3033       }
3034 
3035       const gchar **effects = elements[el].effects;
3036       if(effect && *effect && !_find_combo_effect(effects, effect, ac, &ef))
3037       {
3038         while(effects[ef] && strcmp(effects[ef], effect)) ef++;
3039 
3040         if(!effects[ef])
3041         {
3042           fprintf(stderr, "[dt_action_process] effect '%s' not valid for action '%s'\n", effect, action);
3043           return NAN;
3044         }
3045       }
3046     }
3047   }
3048 
3049   return _process_action(ac, instance, el, ef, move_size);
3050 }
3051 
_cmp_key(const gconstpointer a,const gconstpointer b)3052 static gint _cmp_key(const gconstpointer a, const gconstpointer b)
3053 {
3054   const dt_device_key_t *key_a = a;
3055   const dt_device_key_t *key_b = b;
3056   return key_a->key_device != key_b->key_device || key_a->key != key_b->key;
3057 }
3058 
_cancel_delayed_release(void)3059 static inline void _cancel_delayed_release(void)
3060 {
3061   if(_timeout_source)
3062   {
3063     g_source_remove(_timeout_source);
3064     _timeout_source = 0;
3065 
3066     _sc.button = _pressed_button;
3067     _sc.click = 0;
3068   }
3069 }
3070 
_key_modifiers_clean(guint mods)3071 static guint _key_modifiers_clean(guint mods)
3072 {
3073   GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
3074   mods &= GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_MOD5_MASK |
3075           gdk_keymap_get_modifier_mask(keymap, GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
3076   return mods | dt_modifier_shortcuts;
3077 }
3078 
dt_shortcut_move(dt_input_device_t id,guint time,guint move,double size)3079 float dt_shortcut_move(dt_input_device_t id, guint time, guint move, double size)
3080 {
3081   _sc.move_device = id;
3082   _sc.move = move;
3083   _sc.speed = 1.0;
3084   _sc.direction = 0;
3085 
3086   if(_shortcut_is_move(&_sc))
3087   {
3088     _sc.effect =  DT_ACTION_EFFECT_DEFAULT_MOVE;
3089     _sc.direction = size > 0 ? DT_SHORTCUT_UP : DT_SHORTCUT_DOWN;
3090   }
3091   else
3092     _sc.effect = DT_ACTION_EFFECT_DEFAULT_KEY;
3093 
3094   if(id) _sc.mods = _key_modifiers_clean(dt_key_modifier_state());
3095 
3096   float return_value = 0;
3097   if(isnan(size))
3098     return_value = _process_shortcut(size);
3099   else
3100   {
3101     _cancel_delayed_release();
3102     _last_time = 0;
3103     _previous_move = move;
3104 
3105     if(_grab_widget)
3106       _ungrab_grab_widget();
3107 
3108     dt_print(DT_DEBUG_INPUT,
3109              "  [dt_shortcut_move] shortcut received: %s\n",
3110              _shortcut_description(&_sc));
3111 
3112     _lookup_mapping_widget();
3113 
3114     if(_sc.action)
3115     {
3116       if(!time
3117          || time > _last_mapping_time + 1000
3118          || time < _last_mapping_time)
3119       {
3120         _last_mapping_time = time;
3121 
3122         // mapping_widget gets cleared by confirmation dialog focus loss
3123         GtkWidget *mapped_widget = darktable.control->mapping_widget;
3124 
3125         dt_shortcut_t s = _sc;
3126         if(_insert_shortcut(&s, TRUE))
3127         {
3128           dt_control_log(_("%s assigned to %s"),
3129                          _shortcut_description(&s), _action_description(&s, 2));
3130 
3131           if(mapped_widget)
3132             gtk_widget_trigger_tooltip_query(mapped_widget);
3133         }
3134 
3135         dt_shortcuts_save(NULL, FALSE);
3136       }
3137 
3138       _sc.action = NULL;
3139       _sc.instance = 0;
3140     }
3141     else
3142     {
3143       if(!_pressed_keys)
3144         return_value = _process_shortcut(size);
3145       else
3146       {
3147         // pressed_keys can be emptied if losing grab during processing
3148         for(GSList *k = _pressed_keys; k; k = _pressed_keys ? k->next : NULL)
3149         {
3150           const dt_device_key_t *device_key = k->data;
3151           _sc.key_device = device_key->key_device;
3152           _sc.key = device_key->key;
3153 
3154           return_value = _process_shortcut(size);
3155         }
3156       }
3157     }
3158   }
3159 
3160   _sc.move_device = 0;
3161   _sc.move = DT_SHORTCUT_MOVE_NONE;
3162   _sc.direction = 0;
3163 
3164   return return_value;
3165 }
3166 
_key_release_delayed(gpointer timed_out)3167 static gboolean _key_release_delayed(gpointer timed_out)
3168 {
3169   _timeout_source = 0;
3170   _last_time = 0;
3171 
3172   _ungrab_grab_widget();
3173 
3174   if(!timed_out)
3175     dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1);
3176 
3177   _sc = (dt_shortcut_t) { 0 };
3178 
3179   return FALSE;
3180 }
3181 
_button_release_delayed(gpointer timed_out)3182 static gboolean _button_release_delayed(gpointer timed_out)
3183 {
3184   _timeout_source = 0;
3185   _last_time = 0;
3186 
3187   if(!timed_out)
3188     dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1);
3189 
3190   _sc.button = _pressed_button;
3191   _sc.click = 0;
3192 
3193   return FALSE;
3194 }
3195 
dt_shortcut_key_press(dt_input_device_t id,guint time,guint key)3196 void dt_shortcut_key_press(dt_input_device_t id, guint time, guint key)
3197 {
3198   dt_device_key_t this_key = { id, key };
3199 
3200   if(g_slist_find_custom(_pressed_keys, &this_key, _cmp_key))
3201   {
3202     // if key is still repeating (after return from popup menu) then restore double/triple press state
3203     if(id == DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE && key == _focus_loss_key && time < _last_time + 50)
3204       _sc.press = _focus_loss_press;
3205     _focus_loss_key = 0;
3206   }
3207   else if(g_slist_find_custom(_hold_keys, &this_key, _cmp_key))
3208   {} // ignore repeating hold key
3209   else
3210   {
3211     if(id) _sc.mods = _key_modifiers_clean(dt_key_modifier_state());
3212 
3213     dt_shortcut_t just_key
3214       = { .key_device = id,
3215           .key = key,
3216           .mods = _sc.mods,
3217           .views = darktable.view_manager->current_view->view(darktable.view_manager->current_view) };
3218 
3219     dt_shortcut_t *s = NULL;
3220     GSequenceIter *existing = g_sequence_lookup(darktable.control->shortcuts, &just_key,
3221                                                 _shortcut_compare_func, GINT_TO_POINTER(just_key.views));
3222     if(existing)
3223       s = g_sequence_get(existing);
3224     else
3225     {
3226       just_key.mods = 0; // fall back to key without modifiers (for multiple emulated modifiers)
3227       existing = g_sequence_lookup(darktable.control->shortcuts, &just_key,
3228                                    _shortcut_compare_func, GINT_TO_POINTER(just_key.views));
3229       if(existing && (s = g_sequence_get(existing)) &&
3230          (s->action != darktable.control->actions_modifiers || s->effect != DT_ACTION_EFFECT_HOLD))
3231         s = NULL;
3232     }
3233     if(s
3234        && !_sc.action
3235        && s->effect == DT_ACTION_EFFECT_HOLD
3236        && s->action
3237        && s->action->type >= DT_ACTION_TYPE_WIDGET
3238        && !g_hash_table_lookup(darktable.control->widgets, darktable.control->mapping_widget))
3239     {
3240       const dt_action_def_t *definition = _action_find_definition(s->action);
3241       if(definition && definition->process
3242          && definition->elements[s->element].effects == dt_action_effect_hold)
3243       {
3244         if(darktable.control->mapping_widget)
3245         {
3246           dt_control_log("[ %s ]\n%s \u2192 %s", _shortcut_description(&_sc),
3247                          _shortcut_description(s), _action_description(s, 2));
3248         }
3249 
3250         definition->process(NULL, s->element, DT_ACTION_EFFECT_ON, 1);
3251 
3252         this_key.hold_def = definition;
3253         this_key.hold_element = s->element;
3254 
3255         dt_device_key_t *new_key = calloc(1, sizeof(dt_device_key_t));
3256         *new_key = this_key;
3257         _hold_keys = g_slist_prepend(_hold_keys, new_key);
3258 
3259         return;
3260       }
3261     }
3262 
3263     _cancel_delayed_release();
3264 
3265     int delay = 0;
3266     g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL);
3267 
3268     if(!_pressed_keys)
3269     {
3270       if((id || key)
3271          && id == _sc.key_device
3272          && key == _sc.key
3273          && time < _last_time + delay
3274          && !(_sc.press & DT_SHORTCUT_TRIPLE))
3275       {
3276         _sc.press += DT_SHORTCUT_DOUBLE;
3277       }
3278       else
3279       {
3280         _sc.press = 0;
3281 
3282         _lookup_mapping_widget();
3283       }
3284 
3285       gdk_seat_grab(gdk_display_get_default_seat(gdk_display_get_default()),
3286                     gtk_widget_get_window(_grab_window ? _grab_window
3287                                                        : dt_ui_main_window(darktable.gui->ui)),
3288                     GDK_SEAT_CAPABILITY_ALL, FALSE, NULL, NULL, NULL, NULL);
3289 
3290       _last_time = time;
3291     }
3292     else
3293     {
3294       if(_sc.action)
3295       {
3296         // only one key press allowed while defining shortcut
3297         _ungrab_grab_widget();
3298         _sc = (dt_shortcut_t) { 0 };
3299         return;
3300       }
3301     }
3302 
3303     _sc.key_device = id;
3304     _sc.key = key;
3305     _sc.button = _pressed_button = 0;
3306     _sc.click = 0;
3307     _sc.direction = 0;
3308 
3309     dt_device_key_t *new_key = calloc(1, sizeof(dt_device_key_t));
3310     *new_key = this_key;
3311     _pressed_keys = g_slist_prepend(_pressed_keys, new_key);
3312 
3313     // FIXME: make arrow keys repeat; eventually treat up/down and left/right as move
3314     if(key == GDK_KEY_Left
3315        || key == GDK_KEY_Right
3316        || key == GDK_KEY_Up
3317        || key == GDK_KEY_Down)
3318     {
3319       dt_shortcut_key_release(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, time, key);
3320     }
3321   }
3322 }
3323 
dt_shortcut_key_release(dt_input_device_t id,guint time,guint key)3324 void dt_shortcut_key_release(dt_input_device_t id, guint time, guint key)
3325 {
3326   dt_device_key_t this_key = { id, key };
3327 
3328   GSList *held_key = g_slist_find_custom(_hold_keys, &this_key, _cmp_key);
3329   if(held_key)
3330   {
3331     dt_device_key_t *held_data = held_key->data;
3332     held_data->hold_def->process(NULL, held_data->hold_element, DT_ACTION_EFFECT_OFF, 1);
3333     g_free(held_data);
3334     _hold_keys = g_slist_delete_link(_hold_keys, held_key);
3335     return;
3336   }
3337 
3338   GSList *stored_key = g_slist_find_custom(_pressed_keys, &this_key, _cmp_key);
3339   if(stored_key)
3340   {
3341     _cancel_delayed_release();
3342 
3343     g_free(stored_key->data);
3344     _pressed_keys = g_slist_delete_link(_pressed_keys, stored_key);
3345 
3346     if(!_pressed_keys)
3347     {
3348       // is this sequence of same key, without mouse buttons
3349       if(_sc.key_device == id
3350          && _sc.key == key
3351          && !_sc.button)
3352       {
3353         int delay = 0;
3354         g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL);
3355 
3356         guint passed_time = time - _last_time;
3357 
3358         if(passed_time > delay) _sc.press |= DT_SHORTCUT_LONG;
3359 
3360         if(!_sc.press && !_sc.action)
3361         {
3362           // detect if any double or triple press shortcuts exist for this key; otherwise skip delay
3363           dt_shortcut_t key_23press =
3364             { .key_device = id,
3365               .key   = key,
3366               .press = DT_SHORTCUT_DOUBLE,
3367               .views = darktable.view_manager->current_view->view(darktable.view_manager->current_view) };
3368           GSequenceIter *double_press = g_sequence_search(darktable.control->shortcuts, &key_23press, _shortcut_compare_func,
3369                                                           GINT_TO_POINTER(key_23press.views));
3370           if(double_press)
3371           {
3372             dt_shortcut_t *dp = g_sequence_get(double_press);
3373             if(!dp || dp->key_device != id || dp->key != key || dp->press <= DT_SHORTCUT_LONG)
3374             {
3375               dp = g_sequence_get(g_sequence_iter_prev(double_press));
3376               if(!dp || dp->key_device != id || dp->key != key || dp->press <= DT_SHORTCUT_LONG)
3377                 passed_time = delay;
3378             }
3379           }
3380         }
3381 
3382         if(passed_time < delay && !(_sc.press & DT_SHORTCUT_TRIPLE))
3383           _timeout_source = g_timeout_add(delay - passed_time, _key_release_delayed, NULL);
3384         else
3385           _key_release_delayed(GINT_TO_POINTER(passed_time > 2 * delay)); // call immediately
3386       }
3387       else
3388       {
3389         _key_release_delayed(GINT_TO_POINTER(TRUE)); // not sequence of same key
3390       }
3391     }
3392   }
3393 }
3394 
dt_shortcut_key_active(dt_input_device_t id,guint key)3395 gboolean dt_shortcut_key_active(dt_input_device_t id, guint key)
3396 {
3397   dt_shortcut_t base_key
3398     = { .key_device = id,
3399         .key   = key,
3400         .views = darktable.view_manager->current_view->view(darktable.view_manager->current_view) };
3401 
3402   GSequenceIter *existing = g_sequence_lookup(darktable.control->shortcuts, &base_key,
3403                                               _shortcut_compare_func, GINT_TO_POINTER(base_key.views));
3404   if(existing)
3405   {
3406     dt_shortcut_t *s = g_sequence_get(existing);
3407 
3408     if(s && s->action && s->action->type >= DT_ACTION_TYPE_WIDGET)
3409     {
3410       const dt_action_def_t *definition = _action_find_definition(s->action);
3411       if(definition && definition->process)
3412       {
3413         float value = definition->process(s->action->target, s->element, s->effect, NAN);
3414         return fmodf(value, 1) <= DT_VALUE_PATTERN_ACTIVE || fmodf(value, 2) > .5;
3415       }
3416     }
3417   }
3418 
3419   return FALSE;
3420 }
3421 
_fix_keyval(GdkEvent * event)3422 static guint _fix_keyval(GdkEvent *event)
3423 {
3424   guint keyval = 0;
3425   GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
3426   gdk_keymap_translate_keyboard_state(keymap, event->key.hardware_keycode, 0, 0,
3427                                       &keyval, NULL, NULL, NULL);
3428   return keyval;
3429 }
3430 
dt_shortcut_dispatcher(GtkWidget * w,GdkEvent * event,gpointer user_data)3431 gboolean dt_shortcut_dispatcher(GtkWidget *w, GdkEvent *event, gpointer user_data)
3432 {
3433 //  dt_print(DT_DEBUG_INPUT, "  [shortcut_dispatcher] %d\n", event->type);
3434 
3435   if(!darktable.control->key_accelerators_on) return FALSE; // FIXME should eventually no longer be needed
3436 
3437   if(_pressed_keys == NULL)
3438   {
3439     if(_grab_widget && event->type == GDK_BUTTON_PRESS)
3440     {
3441       _ungrab_grab_widget();
3442       _sc = (dt_shortcut_t) { 0 };
3443       return TRUE;
3444     }
3445 
3446     if(event->type != GDK_KEY_PRESS && event->type != GDK_KEY_RELEASE &&
3447        event->type != GDK_FOCUS_CHANGE)
3448       return FALSE;
3449 
3450     if(GTK_IS_WINDOW(w) &&
3451        (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE))
3452     {
3453       GtkWidget *focused_widget = gtk_window_get_focus(GTK_WINDOW(w));
3454       if(focused_widget)
3455       {
3456         if(gtk_widget_event(focused_widget, event))
3457           return TRUE;
3458 
3459         if(GTK_IS_ENTRY(focused_widget) &&
3460            (event->key.keyval == GDK_KEY_Tab ||
3461             event->key.keyval == GDK_KEY_KP_Tab ||
3462             event->key.keyval == GDK_KEY_ISO_Left_Tab))
3463           return FALSE;
3464       }
3465     }
3466   }
3467 
3468   int delay = 0;
3469   g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL);
3470 
3471   switch(event->type)
3472   {
3473   case GDK_KEY_PRESS:
3474     if(event->key.is_modifier
3475        || event->key.keyval == GDK_KEY_VoidSymbol
3476        || event->key.keyval == GDK_KEY_Meta_L
3477        || event->key.keyval == GDK_KEY_Meta_R
3478        || event->key.keyval == GDK_KEY_ISO_Level3_Shift)
3479       return FALSE;
3480 
3481     _sc.mods = _key_modifiers_clean(event->key.state);
3482 
3483     // FIXME: eventually clean up per-view and global key_pressed handlers
3484     if(!_grab_widget && !darktable.control->mapping_widget &&
3485        dt_control_key_pressed_override(event->key.keyval, dt_gui_translated_key_state(&event->key))) return TRUE;
3486 
3487     dt_shortcut_key_press(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->key.time, _fix_keyval(event));
3488     break;
3489   case GDK_KEY_RELEASE:
3490     if(event->key.is_modifier || event->key.keyval == GDK_KEY_ISO_Level3_Shift)
3491     {
3492       if(_sc.action)
3493       {
3494         // we may interrupt a delayed release, so do the ungrab here if needed
3495         if(!_pressed_keys) _ungrab_grab_widget();
3496 
3497         _sc.mods = _key_modifiers_clean(event->key.state);
3498         dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1);
3499       }
3500       return FALSE;
3501     }
3502 
3503     dt_shortcut_key_release(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->key.time, _fix_keyval(event));
3504     break;
3505   case GDK_GRAB_BROKEN:
3506     if(!event->grab_broken.implicit)
3507       _ungrab_at_focus_loss();
3508     return FALSE;
3509   case GDK_WINDOW_STATE:
3510     if(!(event->window_state.new_window_state & GDK_WINDOW_STATE_FOCUSED))
3511       _ungrab_at_focus_loss();
3512     return FALSE;
3513   case GDK_FOCUS_CHANGE: // dialog boxes and switch to other app release grab
3514     if(event->focus_change.in)
3515       g_set_weak_pointer(&_grab_window, w);
3516     else
3517       _ungrab_at_focus_loss();
3518     return FALSE;
3519   case GDK_SCROLL:
3520     _sc.mods = _key_modifiers_clean(event->scroll.state);
3521 
3522     int delta_x, delta_y;
3523     if(dt_gui_get_scroll_unit_deltas((GdkEventScroll *)event, &delta_x, &delta_y))
3524     {
3525       if(delta_x)
3526         dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->scroll.time, DT_SHORTCUT_MOVE_PAN, -delta_x);
3527       if(delta_y)
3528         dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->scroll.time, DT_SHORTCUT_MOVE_SCROLL, -delta_y);
3529     }
3530     break;
3531   case GDK_MOTION_NOTIFY:
3532     _sc.mods = _key_modifiers_clean(event->motion.state);
3533 
3534     static gdouble move_start_x = 0, move_start_y = 0, last_distance = 0;
3535 
3536     const gdouble x_move = event->motion.x - move_start_x;
3537     const gdouble y_move = event->motion.y - move_start_y;
3538     const gdouble new_distance = x_move * x_move + y_move * y_move;
3539 
3540     static int move_last_time = 0;
3541     if(move_last_time != _last_time || new_distance < last_distance)
3542     {
3543       move_start_x = event->motion.x;
3544       move_start_y = event->motion.y;
3545       move_last_time = _last_time;
3546       last_distance = 0;
3547       break;
3548     }
3549 
3550     // might just be an accidental move during a key press or button click
3551     // possibly different time sources from midi or other devices
3552     if(event->motion.time > _last_time && event->motion.time < _last_time + delay) break;
3553 
3554     const gdouble step_size = 10;
3555 
3556     const gdouble angle = x_move / (0.001 + y_move);
3557     gdouble size = trunc(x_move / step_size);
3558     gdouble y_size = - trunc(y_move / step_size);
3559 
3560     if(size != 0 || y_size != 0)
3561     {
3562       guint move = DT_SHORTCUT_MOVE_HORIZONTAL;
3563       if(fabs(angle) >= 2)
3564       {
3565         move_start_x += size * step_size;
3566         move_start_y = event->motion.y;
3567       }
3568       else
3569       {
3570         size = y_size;
3571         move_start_y -= size * step_size;
3572         if(fabs(angle) < .5)
3573         {
3574           move_start_x = event->motion.x;
3575           move = DT_SHORTCUT_MOVE_VERTICAL;
3576         }
3577         else
3578         {
3579           move_start_x -= size * step_size * angle;
3580           move = angle < 0 ? DT_SHORTCUT_MOVE_SKEW : DT_SHORTCUT_MOVE_DIAGONAL;
3581         }
3582       }
3583 
3584       if(_previous_move == move || _previous_move == DT_SHORTCUT_MOVE_NONE)
3585         dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->motion.time, move, size);
3586       else
3587         _previous_move = move;
3588     }
3589     break;
3590   case GDK_BUTTON_PRESS:
3591     _sc.mods = _key_modifiers_clean(event->button.state);
3592 
3593     _cancel_delayed_release();
3594     _pressed_button |= 1 << (event->button.button - 1);
3595     _sc.button = _pressed_button;
3596     _sc.click = 0;
3597     _last_time = event->button.time;
3598     break;
3599   case GDK_DOUBLE_BUTTON_PRESS:
3600     _sc.click |= DT_SHORTCUT_DOUBLE;
3601     break;
3602   case GDK_TRIPLE_BUTTON_PRESS:
3603     _sc.click |= DT_SHORTCUT_TRIPLE;
3604     break;
3605   case GDK_BUTTON_RELEASE:
3606     // FIXME; check if there's a shortcut defined for double/triple (could be fallback?); if not -> no delay
3607     // maybe even action on PRESS rather than RELEASE
3608     // FIXME be careful!!; we seem to be receiving presses and releases twice!?!
3609     _pressed_button &= ~(1 << (event->button.button - 1));
3610 
3611     const guint passed_time = event->button.time - _last_time;
3612     if(passed_time < delay
3613        && !(_sc.click & DT_SHORTCUT_TRIPLE))
3614     {
3615       if(!_timeout_source)
3616         _timeout_source = g_timeout_add(delay - passed_time, _button_release_delayed, NULL);
3617     }
3618     else
3619     {
3620       if(passed_time > delay)
3621         _sc.click |= DT_SHORTCUT_LONG;
3622       _button_release_delayed(GINT_TO_POINTER(passed_time > 2 * delay)); // call immediately
3623     }
3624     break;
3625   default:
3626     return FALSE;
3627   }
3628 
3629   return TRUE;
3630 }
3631 
_remove_widget_from_hashtable(GtkWidget * widget,gpointer user_data)3632 static void _remove_widget_from_hashtable(GtkWidget *widget, gpointer user_data)
3633 {
3634   dt_action_t *action = g_hash_table_lookup(darktable.control->widgets, widget);
3635   if(action)
3636   {
3637     if(action->target == widget) action->target = NULL;
3638 
3639     g_hash_table_remove(darktable.control->widgets, widget);
3640   }
3641 }
3642 
path_without_symbols(const gchar * path)3643 static inline gchar *path_without_symbols(const gchar *path)
3644 {
3645   return g_strdelimit(g_strdup(path), "=,/.", '-');
3646 }
3647 
dt_action_insert_sorted(dt_action_t * owner,dt_action_t * new_action)3648 void dt_action_insert_sorted(dt_action_t *owner, dt_action_t *new_action)
3649 {
3650   dt_action_t **insertion_point = (dt_action_t **)&owner->target;
3651 
3652   while(*insertion_point
3653         && strcmp(new_action->id, "preset")
3654         && (!strcmp((*insertion_point)->id, "preset")
3655             || g_utf8_collate((*insertion_point)->label, new_action->label) < 0))
3656   {
3657     insertion_point = &(*insertion_point)->next;
3658   }
3659   new_action->next = *insertion_point;
3660   *insertion_point = new_action;
3661 }
3662 
dt_action_locate(dt_action_t * owner,gchar ** path,gboolean create)3663 dt_action_t *dt_action_locate(dt_action_t *owner, gchar **path, gboolean create)
3664 {
3665   gchar *clean_path = NULL;
3666 
3667   dt_action_t *action = owner ? owner->target : darktable.control->actions;
3668   while(*path)
3669   {
3670     if(owner == &darktable.control->actions_lua) create = TRUE;
3671 
3672     if(!clean_path) clean_path = path_without_symbols(*path);
3673 
3674     if(!action)
3675     {
3676       if(!owner || !create)
3677       {
3678         fprintf(stderr, "[dt_action_locate] action '%s' %s\n", *path,
3679                 !owner ? "not valid base node" : "doesn't exist");
3680         g_free(clean_path);
3681         return NULL;
3682       }
3683 
3684       dt_action_t *new_action = calloc(1, sizeof(dt_action_t));
3685       new_action->id = clean_path;
3686       new_action->label = g_strdup(Q_(*path));
3687       new_action->type = DT_ACTION_TYPE_SECTION;
3688       new_action->owner = owner;
3689 
3690       dt_action_insert_sorted(owner, new_action);
3691 
3692       owner = new_action;
3693       action = NULL;
3694     }
3695     else if(!strcmp(action->id, clean_path))
3696     {
3697       g_free(clean_path);
3698       owner = action;
3699       action = action->target;
3700     }
3701     else
3702     {
3703       action = action->next;
3704       continue;
3705     }
3706     clean_path = NULL; // now owned by action or freed
3707     path++;
3708   }
3709 
3710   if(owner->type <= DT_ACTION_TYPE_VIEW)
3711   {
3712     fprintf(stderr, "[dt_action_locate] found action '%s' internal node\n", owner->id);
3713     return NULL;
3714   }
3715   else if(owner->type == DT_ACTION_TYPE_SECTION)
3716     owner->type = DT_ACTION_TYPE_CLOSURE; // mark newly created leaf as closure
3717 
3718   return owner;
3719 }
3720 
dt_action_define(dt_action_t * owner,const gchar * section,const gchar * label,GtkWidget * widget,const dt_action_def_t * action_def)3721 dt_action_t *dt_action_define(dt_action_t *owner, const gchar *section, const gchar *label, GtkWidget *widget, const dt_action_def_t *action_def)
3722 {
3723   if(owner->type == DT_ACTION_TYPE_IOP_INSTANCE)
3724   {
3725     dt_action_define_iop((dt_iop_module_t *)owner, section, label, widget, action_def);
3726     return owner;
3727   }
3728 
3729   dt_action_t *ac = owner;
3730 
3731   if(label)
3732   {
3733     const gchar *path[] = { section, label, NULL };
3734     ac = dt_action_locate(owner, (gchar**)&path[section ? 0 : 1], TRUE);
3735   }
3736 
3737   if(ac)
3738   {
3739     if(label)
3740     {
3741       if(ac->type == DT_ACTION_TYPE_CLOSURE && ac->target && action_def)
3742         g_closure_unref(ac->target);
3743 
3744       guint index = 0;
3745       if(g_ptr_array_find(darktable.control->widget_definitions, action_def, &index))
3746         ac->type = DT_ACTION_TYPE_WIDGET + index + 1;
3747       else if(action_def == &_action_def_dummy)
3748         ac->type = DT_ACTION_TYPE_WIDGET;
3749       else if(action_def)
3750       {
3751         ac->type = DT_ACTION_TYPE_WIDGET + darktable.control->widget_definitions->len + 1;
3752         g_ptr_array_add(darktable.control->widget_definitions, (gpointer)action_def);
3753 
3754         dt_action_define_fallback(ac->type, action_def);
3755       }
3756     }
3757 
3758     if(action_def && action_def->no_widget)
3759     {
3760       ac->target = widget;
3761     }
3762     else if(!darktable.control->accel_initialising)
3763     {
3764       if(label && action_def && !ac->target) ac->target = widget;
3765       g_hash_table_insert(darktable.control->widgets, widget, ac);
3766 
3767       gtk_widget_set_has_tooltip(widget, TRUE);
3768       g_signal_connect(G_OBJECT(widget), "query-tooltip", G_CALLBACK(_shortcut_tooltip_callback), NULL);
3769       g_signal_connect(G_OBJECT(widget), "destroy", G_CALLBACK(_remove_widget_from_hashtable), NULL);
3770     }
3771   }
3772 
3773   return ac;
3774 }
3775 
dt_action_define_iop(dt_iop_module_t * self,const gchar * section,const gchar * label,GtkWidget * widget,const dt_action_def_t * action_def)3776 void dt_action_define_iop(dt_iop_module_t *self, const gchar *section, const gchar *label, GtkWidget *widget, const dt_action_def_t *action_def)
3777 {
3778   // add to module_so or blending actions list
3779   dt_action_t *ac = NULL;
3780   if(section && g_str_has_prefix(section, "blend"))
3781   {
3782     const char *subsection = section[strlen("blend")] ? section + strlen("blend") + 1 : NULL;
3783     ac = dt_action_define(&darktable.control->actions_blend, subsection, label, widget, action_def);
3784   }
3785   else
3786   {
3787     ac = dt_action_define(&self->so->actions, section, label, widget, action_def ? action_def : &_action_def_dummy);
3788   }
3789 
3790   // to support multi-instance, also save in per instance widget list
3791   dt_action_target_t *referral = g_malloc0(sizeof(dt_action_target_t));
3792   referral->action = ac;
3793   referral->target = widget;
3794   self->widget_list = g_slist_prepend(self->widget_list, referral);
3795 }
3796 
_mods_fix_primary(GdkModifierType mods)3797 static GdkModifierType _mods_fix_primary(GdkModifierType mods)
3798 {
3799   // FIXME move to darktable.h (?) and use there too in dt_modifier_is and dt_modifiers_include
3800   // use global variable?
3801   GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
3802   if(mods & GDK_CONTROL_MASK)
3803     return (mods & ~GDK_CONTROL_MASK)
3804            | gdk_keymap_get_modifier_mask(keymap, GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
3805   else
3806     return mods;
3807 }
3808 
dt_action_define_fallback(dt_action_type_t type,const dt_action_def_t * action_def)3809 void dt_action_define_fallback(dt_action_type_t type, const dt_action_def_t *action_def)
3810 {
3811   const dt_shortcut_fallback_t *f = action_def->fallbacks;
3812   if(f)
3813   {
3814     const gchar *fallback_path[] = { action_def->name, NULL };
3815     dt_action_t *fb = dt_action_locate(&darktable.control->actions_fallbacks, (gchar**)fallback_path, TRUE);
3816     fb->type = DT_ACTION_TYPE_FALLBACK;
3817     fb->target = GINT_TO_POINTER(type);
3818 
3819     while(f->mods || f->press || f->button || f->click || f->direction || f->move)
3820     {
3821       dt_shortcut_t s = { .mods = _mods_fix_primary(f->mods),
3822                           .press = f->press,
3823                           .button = f->button,
3824                           .click = f->click,
3825                           .direction = f->direction,
3826                           .move = f->move,
3827                           .element = f->element,
3828                           .effect = f->effect,
3829                           .action = fb,
3830                           .speed = f->speed ? f->speed : 1.0 };
3831 
3832       _insert_shortcut(&s, FALSE);
3833 
3834       f++;
3835     }
3836   }
3837 }
3838 
dt_accel_register_shortcut(dt_action_t * owner,const gchar * path_string,guint element,guint effect,guint accel_key,GdkModifierType mods)3839 void dt_accel_register_shortcut(dt_action_t *owner, const gchar *path_string, guint element, guint effect, guint accel_key, GdkModifierType mods)
3840 {
3841   if(path_string)
3842   {
3843     gchar **split_path = g_strsplit(path_string, "/", 0);
3844     gchar **split_trans = g_strsplit(g_dpgettext2(NULL, "accel", path_string), "/", g_strv_length(split_path));
3845 
3846     gchar **path = split_path;
3847     gchar **trans = split_trans;
3848 
3849     gchar *clean_path = NULL;
3850 
3851     dt_action_t *action = owner->target;
3852     while(*path)
3853     {
3854       if(!clean_path) clean_path = path_without_symbols(*path);
3855 
3856       if(!action)
3857       {
3858         dt_action_t *new_action = calloc(1, sizeof(dt_action_t));
3859         new_action->id = clean_path;
3860         new_action->label = g_strdup(*trans ? *trans : *path);
3861         new_action->type = DT_ACTION_TYPE_SECTION;
3862         new_action->owner = owner;
3863 
3864         dt_action_insert_sorted(owner, new_action);
3865 
3866         owner = new_action;
3867         action = NULL;
3868       }
3869       else if(!strcmp(action->id, clean_path))
3870       {
3871         g_free(clean_path);
3872         owner = action;
3873         action = action->target;
3874       }
3875       else
3876       {
3877         action = action->next;
3878         continue;
3879       }
3880       clean_path = NULL; // now owned by action or freed
3881       path++;
3882       if(*trans) trans++;
3883     }
3884 
3885     g_strfreev(split_path);
3886     g_strfreev(split_trans);
3887   }
3888 
3889   if(accel_key != 0)
3890   {
3891     GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
3892 
3893     GdkKeymapKey *keys;
3894     gint n_keys, i = 0;
3895 
3896     if(!gdk_keymap_get_entries_for_keyval(keymap, accel_key, &keys, &n_keys)) return;
3897 
3898     // find the first key in group 0, if any
3899     while(i < n_keys - 1 && (keys[i].group > 0 || keys[i].level > 1)) i++;
3900 
3901     if(keys[i].level & 1) mods |= GDK_SHIFT_MASK;
3902     if(keys[i].level & 2) mods |= GDK_MOD5_MASK;
3903 
3904     mods = _mods_fix_primary(mods);
3905 
3906     dt_shortcut_t s = { .key_device = DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE,
3907                         .mods = mods,
3908                         .speed = 1.0,
3909                         .action = owner,
3910                         .element = element,
3911                         .effect = effect };
3912 
3913     gdk_keymap_translate_keyboard_state(keymap, keys[i].keycode, 0, 0, &s.key, NULL, NULL, NULL);
3914 
3915     _insert_shortcut(&s, FALSE);
3916 
3917     g_free(keys);
3918   }
3919 }
3920 
dt_accel_connect_shortcut(dt_action_t * owner,const gchar * path_string,GClosure * closure)3921 void dt_accel_connect_shortcut(dt_action_t *owner, const gchar *path_string, GClosure *closure)
3922 {
3923   gchar **split_path = g_strsplit(path_string, "/", 0);
3924   gchar **path = split_path;
3925 
3926   while(*path && (owner = owner->target))
3927   {
3928     gchar *clean_path = path_without_symbols(*path);
3929 
3930     while(owner)
3931     {
3932       if(!strcmp(owner->id, clean_path))
3933         break;
3934       else
3935         owner = owner->next;
3936     }
3937 
3938     g_free(clean_path);
3939     path++;
3940   }
3941 
3942   if(!*path && owner)
3943   {
3944     if(owner->type == DT_ACTION_TYPE_CLOSURE && owner->target)
3945       g_closure_unref(owner->target);
3946 
3947     owner->type = DT_ACTION_TYPE_CLOSURE;
3948     owner->target = closure;
3949     g_closure_ref(closure);
3950     g_closure_sink(closure);
3951   }
3952   else
3953   {
3954     fprintf(stderr, "[dt_accel_connect_shortcut] '%s' not found\n", path_string);
3955   }
3956 
3957   g_strfreev(split_path);
3958 }
3959 
dt_accel_register_global(const gchar * path,guint accel_key,GdkModifierType mods)3960 void dt_accel_register_global(const gchar *path, guint accel_key, GdkModifierType mods)
3961 {
3962   dt_accel_register_shortcut(&darktable.control->actions_global, path, 0, 0, accel_key, mods);
3963 }
3964 
dt_accel_register_view(dt_view_t * self,const gchar * path,guint accel_key,GdkModifierType mods)3965 void dt_accel_register_view(dt_view_t *self, const gchar *path, guint accel_key, GdkModifierType mods)
3966 {
3967   dt_accel_register_shortcut(&self->actions, path, 0, 0, accel_key, mods);
3968 }
3969 
dt_accel_register_iop(dt_iop_module_so_t * so,gboolean local,const gchar * path,guint accel_key,GdkModifierType mods)3970 void dt_accel_register_iop(dt_iop_module_so_t *so, gboolean local, const gchar *path, guint accel_key,
3971                            GdkModifierType mods)
3972 {
3973   dt_accel_register_shortcut(&so->actions, path, 0, 0, accel_key, mods);
3974 }
3975 
dt_action_define_preset(dt_action_t * action,const gchar * name)3976 void dt_action_define_preset(dt_action_t *action, const gchar *name)
3977 {
3978   gchar *path[3] = { "preset", (gchar *)name, NULL };
3979   dt_action_t *p = dt_action_locate(action, path, TRUE);
3980   if(p)
3981   {
3982     p->type = DT_ACTION_TYPE_PRESET;
3983     p->target = (gpointer)TRUE;
3984   }
3985 }
3986 
dt_action_rename(dt_action_t * action,const gchar * new_name)3987 void dt_action_rename(dt_action_t *action, const gchar *new_name)
3988 {
3989   g_free((char*)action->id);
3990   g_free((char*)action->label);
3991 
3992   dt_action_t **previous = (dt_action_t **)&action->owner->target;
3993   while(*previous)
3994   {
3995     if(*previous == action)
3996     {
3997       *previous = action->next;
3998       break;
3999     }
4000     previous = &(*previous)->next;
4001   }
4002 
4003   if(new_name)
4004   {
4005     action->id = path_without_symbols(new_name);
4006     action->label = g_strdup(_(new_name));
4007 
4008     dt_action_insert_sorted(action->owner, action);
4009   }
4010   else
4011   {
4012     GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
4013     while(!g_sequence_iter_is_end(iter))
4014     {
4015       GSequenceIter *current = iter;
4016       iter = g_sequence_iter_next(iter); // remove will invalidate
4017 
4018       dt_shortcut_t *s = g_sequence_get(current);
4019       if(s->action == action)
4020         _remove_shortcut(current);
4021     }
4022 
4023     if(action->type == DT_ACTION_TYPE_CLOSURE)
4024       g_closure_unref(action->target);
4025 
4026     g_free(action);
4027   }
4028 
4029   dt_shortcuts_save(NULL, FALSE);
4030 }
4031 
dt_action_rename_preset(dt_action_t * action,const gchar * old_name,const gchar * new_name)4032 void dt_action_rename_preset(dt_action_t *action, const gchar *old_name, const gchar *new_name)
4033 {
4034   gchar *path[3] = { "preset", (gchar *)old_name, NULL };
4035   dt_action_t *p = dt_action_locate(action, path, FALSE);
4036   if(p)
4037   {
4038     if(!new_name)
4039     {
4040       if(_actions_store)
4041         gtk_tree_model_foreach(GTK_TREE_MODEL(_actions_store), _remove_shortcut_from_store, p);
4042     }
4043 
4044     dt_action_rename(p, new_name);
4045   }
4046 }
4047 
dt_accel_register_lib_as_view(gchar * view_name,const gchar * path,guint accel_key,GdkModifierType mods)4048 void dt_accel_register_lib_as_view(gchar *view_name, const gchar *path, guint accel_key, GdkModifierType mods)
4049 {
4050   //register a lib shortcut but place it in the path of a view
4051   dt_action_t *a = darktable.control->actions_views.target;
4052   while(a)
4053   {
4054     if(!strcmp(a->id, view_name))
4055       break;
4056     else
4057       a = a->next;
4058   }
4059   if(a)
4060   {
4061     dt_accel_register_shortcut(a, path, 0, 0, accel_key, mods);
4062   }
4063   else
4064   {
4065     fprintf(stderr, "[dt_accel_register_lib_as_view] '%s' not found\n", view_name);
4066   }
4067 }
4068 
dt_accel_register_lib(dt_lib_module_t * self,const gchar * path,guint accel_key,GdkModifierType mods)4069 void dt_accel_register_lib(dt_lib_module_t *self, const gchar *path, guint accel_key, GdkModifierType mods)
4070 {
4071   dt_accel_register_shortcut(&self->actions, path, 0, 0, accel_key, mods);
4072 }
4073 
dt_accel_register_lua(const gchar * path,guint accel_key,GdkModifierType mods)4074 void dt_accel_register_lua(const gchar *path, guint accel_key, GdkModifierType mods)
4075 {
4076   dt_accel_register_shortcut(&darktable.control->actions_lua, path, 0, 0, accel_key, mods);
4077 }
4078 
dt_accel_connect_global(const gchar * path,GClosure * closure)4079 void dt_accel_connect_global(const gchar *path, GClosure *closure)
4080 {
4081   dt_accel_connect_shortcut(&darktable.control->actions_global, path, closure);
4082 }
4083 
dt_accel_connect_view(dt_view_t * self,const gchar * path,GClosure * closure)4084 void dt_accel_connect_view(dt_view_t *self, const gchar *path, GClosure *closure)
4085 {
4086   dt_accel_connect_shortcut(&self->actions, path, closure);
4087 }
4088 
dt_accel_connect_lib_as_view(dt_lib_module_t * module,gchar * view_name,const gchar * path,GClosure * closure)4089 void dt_accel_connect_lib_as_view(dt_lib_module_t *module, gchar *view_name, const gchar *path, GClosure *closure)
4090 {
4091   dt_action_t *a = darktable.control->actions_views.target;
4092   while(a)
4093   {
4094     if(!strcmp(a->id, view_name))
4095       break;
4096     else
4097       a = a->next;
4098   }
4099   if(a)
4100   {
4101     dt_accel_connect_shortcut(a, path, closure);
4102   }
4103   else
4104   {
4105     fprintf(stderr, "[dt_accel_register_lib_as_view] '%s' not found\n", view_name);
4106   }
4107 }
4108 
dt_accel_connect_lib_as_global(dt_lib_module_t * module,const gchar * path,GClosure * closure)4109 void dt_accel_connect_lib_as_global(dt_lib_module_t *module, const gchar *path, GClosure *closure)
4110 {
4111   dt_accel_connect_shortcut(&darktable.control->actions_global, path, closure);
4112 }
4113 
dt_accel_connect_iop(dt_iop_module_t * module,const gchar * path,GClosure * closure)4114 void dt_accel_connect_iop(dt_iop_module_t *module, const gchar *path, GClosure *closure)
4115 {
4116   gchar **split_path = g_strsplit(path, "`", 6);
4117   dt_action_t *ac = dt_action_locate(&module->so->actions, split_path, FALSE);
4118   g_strfreev(split_path);
4119 
4120   if(ac)
4121   {
4122     ac->type = DT_ACTION_TYPE_CLOSURE;
4123 
4124     // to support multi-instance, save in and own by per instance widget list
4125     dt_action_target_t *referral = g_malloc0(sizeof(dt_action_target_t));
4126     referral->action = ac;
4127     referral->target = closure;
4128     g_closure_ref(closure);
4129     g_closure_sink(closure);
4130     module->widget_list = g_slist_prepend(module->widget_list, referral);
4131   }
4132 }
4133 
dt_accel_connect_lib(dt_lib_module_t * module,const gchar * path,GClosure * closure)4134 void dt_accel_connect_lib(dt_lib_module_t *module, const gchar *path, GClosure *closure)
4135 {
4136   dt_accel_connect_shortcut(&module->actions, path, closure);
4137 }
4138 
dt_accel_connect_lua(const gchar * path,GClosure * closure)4139 void dt_accel_connect_lua(const gchar *path, GClosure *closure)
4140 {
4141   dt_accel_connect_shortcut(&darktable.control->actions_lua, path, closure);
4142 }
4143 
dt_accel_connect_button_iop(dt_iop_module_t * module,const gchar * path,GtkWidget * button)4144 void dt_accel_connect_button_iop(dt_iop_module_t *module, const gchar *path, GtkWidget *button)
4145 {
4146   dt_action_define_iop(module, NULL, path, button, &dt_action_def_button);
4147 }
4148 
dt_accel_connect_button_lib(dt_lib_module_t * module,const gchar * path,GtkWidget * button)4149 void dt_accel_connect_button_lib(dt_lib_module_t *module, const gchar *path, GtkWidget *button)
4150 {
4151   dt_action_define(DT_ACTION(module), NULL, path, button, &dt_action_def_button);
4152 }
4153 
dt_accel_connect_button_lib_as_global(dt_lib_module_t * module,const gchar * path,GtkWidget * button)4154 void dt_accel_connect_button_lib_as_global(dt_lib_module_t *module, const gchar *path, GtkWidget *button)
4155 {
4156   dt_action_define(&darktable.control->actions_global, NULL, path, button, &dt_action_def_button);
4157 }
4158 
dt_action_widget_toast(dt_action_t * action,GtkWidget * widget,const gchar * text)4159 void dt_action_widget_toast(dt_action_t *action, GtkWidget *widget, const gchar *text)
4160 {
4161   if(!darktable.gui->reset)
4162   {
4163     if(!action)
4164       action = g_hash_table_lookup(darktable.control->widgets, widget);
4165     if(action)
4166     {
4167       gchar *instance_name = "";
4168       gchar *label = NULL;
4169 
4170       if(action->type == DT_ACTION_TYPE_IOP_INSTANCE)
4171       {
4172         dt_iop_module_t *module = (dt_iop_module_t *)action;
4173 
4174         action = DT_ACTION(module->so);
4175         instance_name = module->multi_name;
4176 
4177         for(GSList *w = module->widget_list; w; w = w->next)
4178         {
4179           dt_action_target_t *referral = w->data;
4180           if(referral->target == widget)
4181           {
4182             if(referral->action->owner == &darktable.control->actions_blend)
4183             {
4184               _action_distinct_label(&label, referral->action, NULL);
4185             }
4186             else
4187               action = referral->action;
4188             break;
4189           }
4190         }
4191       }
4192 
4193       _action_distinct_label(&label, action, instance_name);
4194       dt_toast_log("%s : %s", label, text);
4195       g_free(label);
4196     }
4197     else
4198       dt_toast_log("%s", text);
4199   }
4200 }
4201 
dt_accel_get_slider_scale_multiplier()4202 float dt_accel_get_slider_scale_multiplier()
4203 {
4204   const int slider_precision = dt_conf_get_int("accel/slider_precision");
4205 
4206   if(slider_precision == DT_IOP_PRECISION_COARSE)
4207   {
4208     return dt_conf_get_float("darkroom/ui/scale_rough_step_multiplier");
4209   }
4210   else if(slider_precision == DT_IOP_PRECISION_FINE)
4211   {
4212     return dt_conf_get_float("darkroom/ui/scale_precise_step_multiplier");
4213   }
4214 
4215   return dt_conf_get_float("darkroom/ui/scale_step_multiplier");
4216 }
4217 
dt_accel_connect_instance_iop(dt_iop_module_t * module)4218 void dt_accel_connect_instance_iop(dt_iop_module_t *module)
4219 {
4220   for(GSList *w = module->widget_list; w; w = w->next)
4221   {
4222     dt_action_target_t *referral = w->data;
4223     referral->action->target = referral->target;
4224   }
4225 }
4226 
_destroy_referral(gpointer data)4227 static void _destroy_referral(gpointer data)
4228 {
4229   dt_action_target_t *referral = data;
4230   if(referral->action && referral->action->type == DT_ACTION_TYPE_CLOSURE)
4231   {
4232     if(referral->action->target == referral->target)
4233       referral->action->target = NULL;
4234     g_closure_unref(referral->target);
4235   }
4236 
4237   g_free(referral);
4238 }
4239 
dt_action_cleanup_instance_iop(dt_iop_module_t * module)4240 void dt_action_cleanup_instance_iop(dt_iop_module_t *module)
4241 {
4242   g_slist_free_full(module->widget_list, _destroy_referral);
4243 }
4244 
dt_accel_rename_global(const gchar * path,const gchar * new_name)4245 void dt_accel_rename_global(const gchar *path, const gchar *new_name)
4246 {
4247   gchar **split_path = g_strsplit(path, "/", 6);
4248   dt_action_t *p = dt_action_locate(&darktable.control->actions_global, split_path, FALSE);
4249   g_strfreev(split_path);
4250 
4251   if(p) dt_action_rename(p, new_name);
4252 }
4253 
dt_accel_rename_lua(const gchar * path,const gchar * new_name)4254 void dt_accel_rename_lua(const gchar *path, const gchar *new_name)
4255 {
4256   gchar **split_path = g_strsplit(path, "/", 6);
4257   dt_action_t *p = dt_action_locate(&darktable.control->actions_lua, split_path, FALSE);
4258   g_strfreev(split_path);
4259 
4260   if(p) dt_action_rename(p, new_name);
4261 }
4262 
4263 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
4264 // vim: shiftwidth=2 expandtab tabstop=2 cindent
4265 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
4266