Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
accelerators.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2011-2013, 2015 Jérémy Rosen.
4 Copyright (C) 2011 Robert Bieber.
5 Copyright (C) 2012 Henrik Andersson.
6 Copyright (C) 2012 johannes hanika.
7 Copyright (C) 2012 Moritz Lipp.
8 Copyright (C) 2012 Richard Wonka.
9 Copyright (C) 2012, 2014-2017, 2020 Tobias Ellinghaus.
10 Copyright (C) 2012-2013 Ulrich Pegelow.
11 Copyright (C) 2013, 2016, 2020-2022 Pascal Obry.
12 Copyright (C) 2013-2016 Roman Lebedev.
13 Copyright (C) 2013 Yari Adan.
14 Copyright (C) 2019-2020, 2022 Aldric Renaudin.
15 Copyright (C) 2019 Diederik ter Rahe.
16 Copyright (C) 2019 Philippe Weyland.
17 Copyright (C) 2020-2021 Chris Elston.
18 Copyright (C) 2020-2022 Diederik Ter Rahe.
19 Copyright (C) 2020 Heiko Bauke.
20 Copyright (C) 2020-2021 Hubert Kowalski.
21 Copyright (C) 2020 Marco.
22 Copyright (C) 2021 Marco Carrarini.
23 Copyright (C) 2021 Mark-64.
24 Copyright (C) 2021 Ralf Brown.
25 Copyright (C) 2021 Victor Forsiuk.
26 Copyright (C) 2022-2023, 2025 Aurélien PIERRE.
27 Copyright (C) 2022 Martin Bařinka.
28 Copyright (C) 2022 Miloš Komarčević.
29 Copyright (C) 2023 Luca Zulberti.
30
31 darktable is free software: you can redistribute it and/or modify
32 it under the terms of the GNU General Public License as published by
33 the Free Software Foundation, either version 3 of the License, or
34 (at your option) any later version.
35
36 darktable is distributed in the hope that it will be useful,
37 but WITHOUT ANY WARRANTY; without even the implied warranty of
38 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39 GNU General Public License for more details.
40
41 You should have received a copy of the GNU General Public License
42 along with darktable. If not, see <http://www.gnu.org/licenses/>.
43*/
44#include "accelerators.h"
45#include "common/darktable.h" // lots of garbage to include, only to get debug prints & flags
46#include "control/control.h"
47#include "control/conf.h"
48#include "gui/gtk.h"
49#include "gui/gtkentry.h"
50#include "gui/gdkkeys.h"
52
53#ifdef GDK_WINDOWING_QUARTZ
54#include "osx/osx.h"
55#endif
56
57#include <assert.h>
58#include <glib.h>
59
60// Separator used to space between query and command in accels search
61#define DT_ACCEL_SEARCH_INLINE_SEPARATOR " > "
62#define DT_ACCEL_SEARCH_DISPATCH_RETRY_DELAY_MS 50
63
64typedef struct {
65 GClosure *base;
66 gpointer parent_data; // Reference to the closure->data of the parent shortcut instance, if any
68
69typedef struct _accel_removal_t
70{
71 const char *path;
72 gpointer data;
74
75
76static void _g_list_closure_unref(gpointer data)
77{
78 PayloadClosure *pc = (PayloadClosure *)data;
79 if(pc->base) g_closure_unref(pc->base);
80 dt_free(pc);
81}
82
83static inline void _shortcut_set_widget_data(GtkWidget *widget, dt_shortcut_t *shortcut)
84{
85 if(IS_NULL_PTR(widget)) return;
86 g_object_set_data(G_OBJECT(widget), DT_ACCELS_WIDGET_SHORTCUT_KEY, shortcut);
87 if(!g_object_get_data(G_OBJECT(widget), DT_ACCELS_WIDGET_TOOLTIP_DISABLED_KEY))
88 gtk_widget_set_has_tooltip(widget, TRUE);
89}
90
91static gboolean _accels_tooltip_query_hook(GSignalInvocationHint *hint, guint n_param_values,
92 const GValue *param_values, gpointer data)
93{
94 (void)hint;
95 (void)data;
96 if(n_param_values < 5) return TRUE;
97
98 GtkWidget *widget = g_value_get_object(&param_values[0]);
99 if(IS_NULL_PTR(widget)) return TRUE;
100
101 if(!gtk_widget_get_has_tooltip(widget)) return TRUE;
102 if(g_object_get_data(G_OBJECT(widget), DT_ACCELS_WIDGET_TOOLTIP_DISABLED_KEY)) return TRUE;
103
104 const char *base_markup = g_object_get_data(G_OBJECT(widget), "dt-accel-tooltip-base-markup");
105 const char *base_text = base_markup ? NULL : g_object_get_data(G_OBJECT(widget), "dt-accel-tooltip-base-text");
106 const gboolean base_none = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "dt-accel-tooltip-base-none"));
107
108 if(IS_NULL_PTR(base_markup) && IS_NULL_PTR(base_text) && !base_none)
109 {
110 gchar *current_markup = gtk_widget_get_tooltip_markup(widget);
111 if(current_markup && current_markup[0])
112 {
113 g_object_set_data_full(G_OBJECT(widget), "dt-accel-tooltip-base-markup", current_markup, g_free);
114 base_markup = current_markup;
115 }
116 else
117 {
118 dt_free(current_markup);
119 gchar *current_text = gtk_widget_get_tooltip_text(widget);
120 if(current_text && current_text[0])
121 {
122 g_object_set_data_full(G_OBJECT(widget), "dt-accel-tooltip-base-text", current_text, g_free);
123 base_text = current_text;
124 }
125 else
126 {
127 dt_free(current_text);
128 g_object_set_data(G_OBJECT(widget), "dt-accel-tooltip-base-none", GINT_TO_POINTER(1));
129 }
130 }
131 }
132
133 dt_shortcut_t *shortcut = g_object_get_data(G_OBJECT(widget), DT_ACCELS_WIDGET_SHORTCUT_KEY);
134 if(IS_NULL_PTR(shortcut))
135 {
136 const char *accel_path = g_object_get_data(G_OBJECT(widget), "accel-path");
137 if(accel_path && darktable.gui && darktable.gui->accels)
138 {
139 dt_accels_t *accels = darktable.gui->accels;
140 dt_pthread_mutex_lock(&accels->lock);
141 shortcut = (dt_shortcut_t *)g_hash_table_lookup(accels->acceleratables, accel_path);
143 }
144 }
145
146 if(IS_NULL_PTR(shortcut) || shortcut->key == 0)
147 {
148 if(base_markup)
149 gtk_widget_set_tooltip_markup(widget, base_markup);
150 else if(base_text)
151 gtk_widget_set_tooltip_text(widget, base_text);
152 return TRUE;
153 }
154
155 gchar *shortcut_label = gtk_accelerator_get_label(shortcut->key, shortcut->mods);
156 if(IS_NULL_PTR(shortcut_label) || !shortcut_label[0])
157 {
158 dt_free(shortcut_label);
159 return TRUE;
160 }
161
162 const char *shortcut_desc = (shortcut->description && shortcut->description[0]) ? shortcut->description : _("Shortcut");
163 if(base_markup && base_markup[0])
164 {
165 gchar *esc_label = g_markup_escape_text(shortcut_label, -1);
166 gchar *esc_desc = g_markup_escape_text(shortcut_desc, -1);
167 gchar *new_markup = g_strdup_printf("%s\n<small>%s: %s</small>", base_markup, esc_desc, esc_label);
168 gtk_widget_set_tooltip_markup(widget, new_markup);
169 dt_free(new_markup);
170 dt_free(esc_label);
171 dt_free(esc_desc);
172 }
173 else if(base_text && base_text[0])
174 {
175 gchar *new_text = g_strdup_printf("%s\n%s: %s", base_text, shortcut_desc, shortcut_label);
176 gtk_widget_set_tooltip_text(widget, new_text);
177 dt_free(new_text);
178 }
179 else
180 {
181 gchar *new_text = g_strdup_printf("%s: %s", shortcut_desc, shortcut_label);
182 gtk_widget_set_tooltip_text(widget, new_text);
183 dt_free(new_text);
184 }
185
186 dt_free(shortcut_label);
187
188 return TRUE;
189}
190
192{
193 static gulong hook_id = 0;
194 if(hook_id != 0) return;
195
196 const guint signal_id = g_signal_lookup("query-tooltip", GTK_TYPE_WIDGET);
197 if(signal_id == 0) return;
198
199 hook_id = g_signal_add_emission_hook(signal_id, 0, _accels_tooltip_query_hook, NULL, NULL);
200}
201
202
203static void _clean_shortcut(gpointer data)
204{
205 dt_shortcut_t *shortcut = (dt_shortcut_t *)data;
206 dt_free(shortcut->path);
207 g_list_free_full(shortcut->closure, _g_list_closure_unref);
208 shortcut->closure = NULL;
209 dt_free(shortcut);
210}
211
212
213// Return the last closure in the list
215{
216 GList *link = g_list_last(shortcut->closure);
217 if(link)
218 return (PayloadClosure *)link->data;
219 else
220 return NULL;
221}
222
224{
226 if(pc)
227 return pc->base;
228 else
229 return NULL;
230}
231
232
233// Remove the accel closure instance from shortcut that references data
234// as its input or as its parent. Useful when module instances are destroyed,
235// so we destroy the shortcut attached to the parent module, and the shortcut
236// attached to all its children.
237void dt_shortcut_remove_closure(dt_shortcut_t *shortcut, gpointer data)
238{
239 if(IS_NULL_PTR(shortcut->closure)) return;
240
241 PayloadClosure *cl = NULL;
242 GList *link = NULL;
243
244 if(data)
245 {
246 // Look for closures referencing data in their direct closure args
247 for(link = g_list_first(shortcut->closure); link; link = g_list_next(link))
248 {
249 PayloadClosure *closure = (PayloadClosure *)link->data;
250 if(closure->base->data == data || closure->parent_data == data)
251 {
252 cl = closure;
253 break;
254 }
255 }
256 }
257 else
258 {
259 link = g_list_last(shortcut->closure);
260 if(link) cl = (PayloadClosure *)link->data;
261 }
262
263 if(cl)
264 {
265 g_closure_unref(cl->base);
266 shortcut->closure = g_list_delete_link(shortcut->closure, link);
267 dt_free(cl);
268 // fprintf(stdout, "removing: %s at %p - %i entries remaining\n", shortcut->path, data, g_list_length(shortcut->closure));
269 }
270}
271
272
273static void _find_parent_hashtable(gpointer _key, gpointer value, gpointer user_data)
274{
275 dt_shortcut_t *parent_shortcut = (dt_shortcut_t *)value;
276 dt_shortcut_t *child_shortcut = (dt_shortcut_t *)user_data;
277
278 // Remove the last branch of the path to build the path of the immediate ancestor
279 gchar **child_parts = g_strsplit(child_shortcut->path, "/", -1);
280 guint n = g_strv_length(child_parts);
281 dt_free(child_parts[n - 1]);
282 gchar *parent_path = g_strjoinv ("/", child_parts);
283 g_strfreev(child_parts);
284
285 // This should technically match only once in the HashTable for_each()
286 if(!g_strcmp0(parent_shortcut->path, parent_path))
287 {
288 GClosure *parent_closure = dt_shortcut_get_closure(parent_shortcut);
289 PayloadClosure *child_closure = dt_shortcut_get_payload_closure(child_shortcut);
290 if(parent_closure && child_closure)
291 child_closure->parent_data = parent_closure->data;
292
293 /*
294 fprintf(stdout, "%s is the parent of %s - pointer %p\n", parent_shortcut->path, child_shortcut->path,
295 parent_closure->data);
296 */
297 }
298
299 dt_free(parent_path);
300}
301
302
303// Lookup all existing shortcuts that share their path root with this one,
304// Consider them as parent of this one,
305// write this one into their (dt_shortcut_t *)->children table.
306// This assumes that parents are declared before children, which makes sense for widgets.
308{
309 g_hash_table_foreach(shortcut->accels->acceleratables, _find_parent_hashtable, (gpointer)shortcut);
310}
311
312
313// Append a new closure in the list
315 gboolean (*action_callback)(GtkAccelGroup *group, GObject *acceleratable,
316 guint keyval, GdkModifierType mods, gpointer user_data),
317 gpointer data)
318{
319 PayloadClosure *pc = malloc(sizeof(PayloadClosure));
320 pc->base = g_cclosure_new(G_CALLBACK(action_callback), data, NULL);
321 pc->parent_data = NULL;
322
323 g_closure_set_marshal(pc->base, g_cclosure_marshal_generic);
324 g_closure_ref(pc->base);
325 g_closure_sink(pc->base);
326 shortcut->closure = g_list_append(shortcut->closure, pc);
327 // fprintf(stdout, "appending closure for %s - %i entries\n", shortcut->path, g_list_length(shortcut->closure));
329}
330
331
332dt_accels_t * dt_accels_init(char *config_file, GtkAccelFlags flags)
333{
334 dt_accels_t *accels = malloc(sizeof(dt_accels_t));
335 accels->config_file = g_strdup(config_file);
336 accels->global_accels = gtk_accel_group_new();
337 accels->darkroom_accels = gtk_accel_group_new();
338 accels->lighttable_accels = gtk_accel_group_new();
339 accels->map_accels = gtk_accel_group_new();
340 accels->print_accels = gtk_accel_group_new();
341 accels->slideshow_accels = gtk_accel_group_new();
342 accels->acceleratables = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, _clean_shortcut);
343 accels->active_group = NULL;
344 accels->reset = 1;
345 accels->keymap = gdk_keymap_get_for_display(gdk_display_get_default());
346 accels->default_mod_mask = gtk_accelerator_get_default_mod_mask();
347 accels->init = !g_file_test(accels->config_file, G_FILE_TEST_EXISTS);
348 accels->active_key.accel_flags = 0;
349 accels->active_key.accel_key = 0;
350 accels->active_key.accel_mods = 0;
351 accels->scroll.callback = NULL;
352 accels->scroll.data = NULL;
353 accels->disable_accels = FALSE;
354 accels->flags = flags;
355 dt_pthread_mutex_init(&accels->lock, NULL);
357 return accels;
358}
359
360
362{
363 gtk_accel_map_save(accels->config_file);
364
365 accels->active_group = NULL;
366
367 g_object_unref(accels->global_accels);
368 g_object_unref(accels->darkroom_accels);
369 g_object_unref(accels->lighttable_accels);
370 g_object_unref(accels->map_accels);
371 g_object_unref(accels->print_accels);
372 g_object_unref(accels->slideshow_accels);
373 accels->global_accels = NULL;
374 accels->darkroom_accels = NULL;
375 accels->lighttable_accels = NULL;
376 accels->map_accels = NULL;
377 accels->print_accels = NULL;
378 accels->slideshow_accels = NULL;
379
380 dt_pthread_mutex_lock(&accels->lock);
381 g_hash_table_unref(accels->acceleratables);
383
385
386 dt_free(accels->config_file);
387 dt_free(accels);
388}
389
390
391void dt_accels_connect_active_group(dt_accels_t *accels, const gchar *group)
392{
393 if(IS_NULL_PTR(accels)) return;
394
395 if(!g_strcmp0(group, "lighttable") && accels->lighttable_accels)
396 {
397 accels->reset--;
398 accels->active_group = accels->lighttable_accels;
399 }
400 else if(!g_strcmp0(group, "darkroom") && accels->darkroom_accels)
401 {
402 accels->reset--;
403 accels->active_group = accels->darkroom_accels;
404 }
405 else if(!g_strcmp0(group, "map") && accels->map_accels)
406 {
407 accels->reset--;
408 accels->active_group = accels->map_accels;
409 }
410 else if(!g_strcmp0(group, "print") && accels->print_accels)
411 {
412 accels->reset--;
413 accels->active_group = accels->print_accels;
414 }
415 else if(!g_strcmp0(group, "slideshow") && accels->slideshow_accels)
416 {
417 accels->reset--;
418 accels->active_group = accels->slideshow_accels;
419 }
420 else
421 {
422 fprintf(stderr, "[dt_accels_connect_active_group] INFO: unknown value: `%s'\n", group);
423 }
424}
425
426
428{
429 if(IS_NULL_PTR(accels)) return;
430 accels->active_group = NULL;
431 accels->reset++;
432}
433
434
435static gboolean _update_shortcut_state(dt_shortcut_t *shortcut, GtkAccelKey *key, gboolean init)
436{
437 gboolean changed = FALSE;
438 if(shortcut->type == DT_SHORTCUT_UNSET)
439 {
440 // accel_map table is initially populated with shortcut->type = DT_SHORTCUT_UNSET
441 // so that means the entry is new
442 if(init || shortcut->locked)
443 {
444 // We have no user config file, or the shortcut is locked by the app.
445 // Both ways, init shortcuts with defaults,
446 // then a brand new config will be saved on exiting the app.
447 // Note: they might still be zero, not all shortcuts are assigned.
448 key->accel_key = shortcut->key;
449 key->accel_mods = shortcut->mods;
450 gtk_accel_map_change_entry(shortcut->path, shortcut->key, shortcut->mods, TRUE);
451 shortcut->type = DT_SHORTCUT_DEFAULT;
452 }
453 else if(key->accel_key == shortcut->key && key->accel_mods == shortcut->mods)
454 {
455 // We loaded user config file and found our defaults in it. Nothing to do.
456 shortcut->type = DT_SHORTCUT_DEFAULT;
457 }
458 else
459 {
460 // We loaded user config file, and user made changes in there.
461 // We will need to update our "defaults", which now become rather a memory of previous state
462 shortcut->key = key->accel_key;
463 shortcut->mods = key->accel_mods;
464 shortcut->type = DT_SHORTCUT_USER;
465 }
466
467 // UNSET state always needs update, it means it's the first time we connect accels
468 changed = TRUE;
469 }
470 else if(shortcut->locked && (key->accel_key != shortcut->key || key->accel_mods != shortcut->mods))
471 {
472 // Something changed a locked shortcut. Revert to defaults.
473 key->accel_key = shortcut->key;
474 key->accel_mods = shortcut->mods;
475 gtk_accel_map_change_entry(shortcut->path, shortcut->key, shortcut->mods, TRUE);
476 shortcut->type = DT_SHORTCUT_DEFAULT;
477 changed = TRUE;
478 }
479 else if(key->accel_key != shortcut->key || key->accel_mods != shortcut->mods)
480 {
481 shortcut->key = key->accel_key;
482 shortcut->mods = key->accel_mods;
483 shortcut->type = DT_SHORTCUT_USER;
484 changed = TRUE;
485 }
486
487 return changed;
488}
489
495static void _add_widget_accel(dt_shortcut_t *shortcut, GtkAccelFlags flags)
496{
497 gtk_widget_add_accelerator(shortcut->widget, shortcut->signal, shortcut->accel_group, shortcut->key,
498 shortcut->mods, flags);
499
500 // Numpad numbers register as different keys. Find the numpad equivalent key here, if any.
501 guint alt_char = dt_keys_numpad_alternatives(shortcut->key);
502 if(shortcut->key != alt_char)
503 gtk_widget_add_accelerator(shortcut->widget, shortcut->signal, shortcut->accel_group, alt_char, shortcut->mods,
504 flags);
505}
506
507
508static void _remove_widget_accel(dt_shortcut_t *shortcut, const GtkAccelKey *old_key)
509{
510 gtk_widget_remove_accelerator(shortcut->widget, shortcut->accel_group, old_key->accel_key, old_key->accel_mods);
511
512 // Numpad numbers register as different keys. Find the numpad equivalent key here, if any.
513 guint alt_char = dt_keys_numpad_alternatives(old_key->accel_key);
514 if(old_key->accel_key != alt_char)
515 gtk_widget_remove_accelerator(shortcut->widget, shortcut->accel_group, alt_char, old_key->accel_mods);
516}
517
518
520{
521 // Need to increase the number of references to avoid loosing the closure just yet.
522 GClosure *cl = dt_shortcut_get_closure(shortcut);
523 if(IS_NULL_PTR(cl)) return;
524 g_closure_ref(cl);
525 g_closure_sink(cl);
526 gtk_accel_group_disconnect(shortcut->accel_group, cl);
527 g_closure_unref(cl);
528}
529
530
531static void _add_generic_accel(dt_shortcut_t *shortcut, GtkAccelFlags flags)
532{
533 GClosure *closure = dt_shortcut_get_closure(shortcut);
534 if(closure)
535 gtk_accel_group_connect(shortcut->accel_group, shortcut->key, shortcut->mods, flags | GTK_ACCEL_VISIBLE, closure);
536}
537
538
539static void _insert_accel(dt_accels_t *accels, dt_shortcut_t *shortcut)
540{
541 // init an accel_map entry with no keys so Gtk collects them from user config later.
542 gtk_accel_map_add_entry(shortcut->path, 0, 0);
543 dt_pthread_mutex_lock(&accels->lock);
544 g_hash_table_insert(accels->acceleratables, shortcut->path, shortcut);
546}
547
548
549// Since accel groups are no longer attached to any GtkWindow (see dt_accels_dispatch,
550// which handles everything internally to avoid the crashes from issue #484), a plain
551// widget+signal shortcut needs its own closure too, or the internal dispatcher has
552// nothing to invoke: gtk_widget_add_accelerator() alone is a dead end here.
553static gboolean _widget_shortcut_callback(GtkAccelGroup *group, GObject *acceleratable, guint keyval,
554 GdkModifierType mods, gpointer user_data)
555{
556 dt_shortcut_t *shortcut = (dt_shortcut_t *)user_data;
557 if(IS_NULL_PTR(shortcut->widget) || IS_NULL_PTR(shortcut->signal)) return FALSE;
558 g_signal_emit_by_name(shortcut->widget, shortcut->signal);
559 return TRUE;
560}
561
562
563static gboolean _virtual_shortcut_callback(GtkAccelGroup *group, GObject *acceleratable, guint keyval,
564 GdkModifierType mods, gpointer user_data)
565{
566 dt_shortcut_t *shortcut = (dt_shortcut_t *)user_data;
567 if(IS_NULL_PTR(shortcut->widget)) return FALSE;
568
569 // Focus the target widget
570 gtk_widget_grab_focus(shortcut->widget);
571
572 // Hardware-decode the shortcut key
573 guint keycode = 0;
574 GdkKeymapKey *keys = NULL;
575 gint n = 0;
576 GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
577 if(gdk_keymap_get_entries_for_keyval(keymap, shortcut->key, &keys, &n))
578 {
579 if(n > 0) keycode = keys[0].keycode;
580 dt_free(keys);
581 }
582
583 // Create a virtual key stroke using our shortcut keys
584 GdkEvent *ev = gdk_event_new(GDK_KEY_PRESS);
585 ev->key.window = g_object_ref(gtk_widget_get_window(shortcut->widget));
586 ev->key.send_event = TRUE;
587 ev->key.time = GDK_CURRENT_TIME;
588 ev->key.state = shortcut->mods;
589 ev->key.keyval = shortcut->key;
590 ev->key.hardware_keycode = keycode;
591 ev->key.group = 0;
592 ev->key.is_modifier = FALSE;
593
594 // Fire the virtual keystroke to the target widget
595 gtk_widget_event(shortcut->widget, ev);
596 gdk_event_free(ev);
597
598 return TRUE;
599}
600
601
602void dt_accels_new_virtual_shortcut(dt_accels_t *accels, GtkAccelGroup *accel_group, const gchar *accel_path,
603 GtkWidget *widget, guint key_val, GdkModifierType accel_mods)
604{
605 // Our own circuitery to keep track of things after user-defined shortcuts are updated
606 dt_pthread_mutex_lock(&accels->lock);
607 dt_shortcut_t *shortcut = (dt_shortcut_t *)g_hash_table_lookup(accels->acceleratables, accel_path);
609
610 if(shortcut && shortcut->widget == widget)
611 {
612 _shortcut_set_widget_data(widget, shortcut);
613 return;
614 }
615
616 if(IS_NULL_PTR(shortcut))
617 {
618 shortcut = malloc(sizeof(dt_shortcut_t));
619 shortcut->accel_group = accel_group;
620 shortcut->widget = widget;
621 shortcut->closure = NULL;
622 shortcut->path = g_strdup(accel_path);
623 shortcut->signal = NULL;
624 shortcut->key = key_val;
625 shortcut->mods = accel_mods;
626 shortcut->type = DT_SHORTCUT_UNSET;
627 shortcut->locked = TRUE;
628 shortcut->virtual_shortcut = TRUE;
629 shortcut->description = _("Contextual interaction on focus");
630 shortcut->accels = accels;
632 _insert_accel(accels, shortcut);
633 _shortcut_set_widget_data(widget, shortcut);
634 }
635}
636
638 gboolean (*action_callback)(GtkAccelGroup *group,
639 GObject *acceleratable, guint keyval,
640 GdkModifierType mods, gpointer user_data),
641 gpointer data, GtkAccelGroup *accel_group, const gchar *action_scope,
642 const gchar *action_name)
643{
644 gchar *accel_path = dt_accels_build_path(action_scope, action_name);
645
646 // Our own circuitery to keep track of things after user-defined shortcuts are updated
647 dt_pthread_mutex_lock(&accels->lock);
648 dt_shortcut_t *shortcut = (dt_shortcut_t *)g_hash_table_lookup(accels->acceleratables, accel_path);
650
651 if(IS_NULL_PTR(shortcut))
652 {
653 shortcut = malloc(sizeof(dt_shortcut_t));
654 shortcut->accel_group = accel_group;
655 shortcut->widget = NULL;
656 shortcut->closure = NULL;
657 shortcut->path = g_strdup(accel_path);
658 shortcut->signal = NULL;
659 shortcut->key = 0;
660 shortcut->mods = 0;
661 shortcut->type = DT_SHORTCUT_DEFAULT;
662 shortcut->locked = TRUE;
663 shortcut->virtual_shortcut = TRUE;
664 shortcut->description = _("Focuses the instance");
665 shortcut->accels = accels;
666 dt_shortcut_set_closure(shortcut, action_callback, data);
667
668 dt_pthread_mutex_lock(&accels->lock);
669 g_hash_table_insert(accels->acceleratables, shortcut->path, shortcut);
671 }
672
673 dt_free(accel_path);
674}
675
676
677void dt_accels_new_widget_shortcut(dt_accels_t *accels, GtkWidget *widget, const gchar *signal,
678 GtkAccelGroup *accel_group, const gchar *accel_path, guint key_val,
679 GdkModifierType accel_mods, const gboolean lock)
680{
681 // Our own circuitery to keep track of things after user-defined shortcuts are updated
682 dt_pthread_mutex_lock(&accels->lock);
683 dt_shortcut_t *shortcut = (dt_shortcut_t *)g_hash_table_lookup(accels->acceleratables, accel_path);
685
686 if(shortcut && shortcut->widget == widget)
687 {
688 // reference is still up-to-date. Nothing to do.
689 _shortcut_set_widget_data(widget, shortcut);
690 return;
691 }
692 else if(shortcut && shortcut->type != DT_SHORTCUT_UNSET)
693 {
694 // If we already have a shortcut object wired to Gtk for this accel path, just update it
695 GtkAccelKey key = { .accel_key = shortcut->key, .accel_mods = shortcut->mods, .accel_flags = 0 };
696 if(shortcut->key > 0) _remove_widget_accel(shortcut, &key);
697 shortcut->widget = widget;
698 if(shortcut->key > 0) _add_widget_accel(shortcut, accels->flags);
699 _shortcut_set_widget_data(widget, shortcut);
700 }
701 // else if shortcut && shortcut->type == DT_SHORTCUT_UNSET, we need to wait for the next call to dt_accels_connect_accels()
702 else if(!shortcut)
703 {
704 shortcut = malloc(sizeof(dt_shortcut_t));
705 shortcut->accel_group = accel_group;
706 shortcut->widget = widget;
707 shortcut->closure = NULL;
708 shortcut->path = g_strdup(accel_path);
709 shortcut->signal = signal;
710 shortcut->key = key_val;
711 shortcut->mods = accel_mods;
712 shortcut->type = DT_SHORTCUT_UNSET;
713 shortcut->locked = lock;
714 shortcut->virtual_shortcut = FALSE;
715 shortcut->description = _("Trigger the action");
716 shortcut->accels = accels;
718 _insert_accel(accels, shortcut);
719 _shortcut_set_widget_data(widget, shortcut);
720 // accel is inited with empty keys so user config may set it.
721 // dt_accels_load_config needs to run next
722 // then dt_accels_connect_accels will update keys and possibly wire the widgets in Gtk
723 }
724}
725
726
727// Multiple instances of modules will have the same path for the same control
728// meaning they all share the same shortcut object, which is not possible
729// because they are referenced by pathes and those are unique.
730// We handle this here by overriding any pre-existing closure
731// with a reference to the current widget, meaning
732// the last module in the order of GUI inits wins the shortcut.
734 gboolean (*action_callback)(GtkAccelGroup *group, GObject *acceleratable,
735 guint keyval, GdkModifierType mods,
736 gpointer user_data),
737 gpointer data, GtkAccelGroup *accel_group, const gchar *action_scope,
738 const gchar *action_name, guint key_val, GdkModifierType accel_mods,
739 const gboolean lock, const char *description)
740{
741 // Our own circuitery to keep track of things after user-defined shortcuts are updated
742 gchar *accel_path = dt_accels_build_path(action_scope, action_name);
743
744 dt_pthread_mutex_lock(&accels->lock);
745 dt_shortcut_t *shortcut = (dt_shortcut_t *)g_hash_table_lookup(accels->acceleratables, accel_path);
747
748 GClosure *closure = shortcut ? dt_shortcut_get_closure(shortcut) : NULL;
749
750 if(closure && closure->data == data)
751 {
752 // reference is still up-to-date: nothing to do.
753 dt_free(accel_path);
754 return;
755 }
756 else if(shortcut && shortcut->type != DT_SHORTCUT_UNSET)
757 {
758 // If we already have a shortcut object wired to Gtk for this accel path, just update it
759 if(shortcut->key > 0 && closure) _remove_generic_accel(shortcut);
760 dt_shortcut_set_closure(shortcut, action_callback, data);
761 if(shortcut->key > 0) _add_generic_accel(shortcut, accels->flags);
762 }
763 // else if shortcut && shortcut->type == DT_SHORTCUT_UNSET, we need to wait for the next call to dt_accels_connect_accels()
764 else if(!shortcut)
765 {
766 // Create a new object.
767 shortcut = malloc(sizeof(dt_shortcut_t));
768 shortcut->accel_group = accel_group;
769 shortcut->widget = NULL;
770 shortcut->closure = NULL;
771 shortcut->path = g_strdup(accel_path);
772 shortcut->signal = "";
773 shortcut->key = key_val;
774 shortcut->mods = accel_mods;
775 shortcut->type = DT_SHORTCUT_UNSET;
776 shortcut->locked = lock;
777 shortcut->virtual_shortcut = FALSE;
778 shortcut->description = description;
779 shortcut->accels = accels;
780 dt_shortcut_set_closure(shortcut, action_callback, data);
781 _insert_accel(accels, shortcut);
782 // accel is inited with empty keys so user config may set it.
783 // dt_accels_load_config needs to run next
784 // then dt_accels_connect_accels will update keys and possibly wire the widgets in Gtk
785 }
786
787 dt_free(accel_path);
788}
789
790
792{
793 gtk_accel_map_load(accels->config_file);
794}
795
796// Resync the GtkAccelMap with our shortcut, meaning key changes should happen in GtkAccelMap before
797static void _connect_accel(dt_shortcut_t *shortcut)
798{
799 GtkAccelKey key = { 0 };
800
801 // All shortcuts should be known, they are added to accel_map at init time.
802 const gboolean is_known = gtk_accel_map_lookup_entry(shortcut->path, &key);
803 if(!is_known) return;
804
805 // Remember previous values
806 const GtkAccelKey oldkey = { .accel_key = shortcut->key, .accel_mods = shortcut->mods, .accel_flags = 0 };
807 const dt_shortcut_type_t oldtype = shortcut->type;
808
809 // Resync our shortcut object key/mods with what is currently defined in the GtkAccelMap
810 const gboolean changed = _update_shortcut_state(shortcut, &key, shortcut->accels->init);
811
812 // if old_key was non zero, we already had an accel on the stack.
813 // then, if the new shortcut is different, that means we need to remove the old accel.
814 const gboolean needs_cleanup = changed && oldkey.accel_key > 0 && oldtype != DT_SHORTCUT_UNSET;
815
816 // if key is non zero and new, or updated, we need to add a new accel
817 const gboolean needs_init = changed && key.accel_key > 0;
818
819 if(dt_shortcut_get_closure(shortcut))
820 {
821 if(needs_cleanup) _remove_generic_accel(shortcut);
822 if(needs_init) _add_generic_accel(shortcut, shortcut->accels->flags);
823 // closures can be connected only at one accel at a time, so we don't handle keypad duplicates
824 }
825 else if(shortcut->widget)
826 {
827 if(needs_cleanup) _remove_widget_accel(shortcut, &oldkey);
828 if(needs_init) _add_widget_accel(shortcut, shortcut->accels->flags);
829 }
830 else
831 {
832 // Nothing
833 }
834}
835
836static void _connect_accel_hashtable(gpointer _key, gpointer value, gpointer user_data)
837{
838 dt_shortcut_t *shortcut = (dt_shortcut_t *)value;
839 _connect_accel(shortcut);
840}
841
842
844{
845 dt_pthread_mutex_lock(&accels->lock);
846 g_hash_table_foreach(accels->acceleratables, _connect_accel_hashtable, NULL);
848}
849
850static void
851_remove_accel_hashtable(gpointer _key, gpointer value, gpointer user_data)
852{
853 dt_shortcut_t *shortcut = (dt_shortcut_t *)value;
854 _accel_removal_t *params = (_accel_removal_t *)user_data;
855 if(g_strrstr(shortcut->path, params->path) != NULL)
856 {
857 //fprintf(stdout, "removing %s\n", shortcut->path);
858 if(dt_shortcut_get_closure(shortcut))
859 {
860 // Detach the accel from the accel group
861 if(shortcut->key > 0) _remove_generic_accel(shortcut);
862
863 // Remove the closure matching user_data, or the last one
864 dt_shortcut_remove_closure(shortcut, params->data);
865
866 // Reattach the accel to the accel group using the last closure in the list
867 if(shortcut->key > 0) _add_generic_accel(shortcut, shortcut->accels->flags);
868 }
869 /* Should we handle that too ?
870 else if(shortcut->widget)
871 {
872 GtkAccelKey key = { 0 };
873 if(gtk_accel_map_lookup_entry(shortcut->path, &key))
874 _remove_widget_accel(shortcut, &key);
875 }
876 */
877 }
878}
879
880// For all shortcuts matching path (fully or partially), remove the closure instance referencing data
881void dt_accels_remove_accel(dt_accels_t *accels, const char *path, gpointer data)
882{
883 if(IS_NULL_PTR(accels) || IS_NULL_PTR(accels->acceleratables)) return;
884
885 _accel_removal_t *params = malloc(sizeof(_accel_removal_t));
886 params->path = path;
887 params->data = data;
888
889 dt_pthread_mutex_lock(&accels->lock);
890 g_hash_table_foreach(accels->acceleratables, _remove_accel_hashtable, (gpointer)params);
892
893 dt_free(params);
894}
895
896void dt_accels_remove_shortcut(dt_accels_t *accels, const char *path)
897{
898 dt_pthread_mutex_lock(&accels->lock);
899 g_hash_table_remove(accels->acceleratables, path);
901}
902
903
904gchar *dt_accels_build_path(const gchar *scope, const gchar *feature)
905{
906 if(strncmp(scope, "<Ansel>/", strlen("<Ansel>/")) == 0)
907 return g_strdup_printf("%s/%s", scope, feature);
908 else
909 return g_strdup_printf("<Ansel>/%s/%s", scope, feature);
910}
911
912static void _accels_keys_decode(dt_accels_t *accels, GdkEvent *event, guint *keyval, GdkModifierType *mods)
913{
914 if(IS_NULL_PTR(accels)) return;
915
916 // Get modifiers
917 gdk_event_get_state(event, mods);
918
919 // Remove all modifiers that are irrelevant to key strokes
920 *mods &= accels->default_mod_mask;
921
922 // Get the canonical key code, that is without the modifiers
923 GdkModifierType consumed;
924 gdk_keymap_translate_keyboard_state(accels->keymap, event->key.hardware_keycode, event->key.state,
925 event->key.group, // this ensures that numlock or shift are properly decoded
926 keyval, NULL, NULL, &consumed);
927
929 {
930 gchar *accel_name = gtk_accelerator_name(*keyval, *mods);
931 dt_print(DT_DEBUG_SHORTCUTS, "[shortcuts] %s : %s\n",
932 (event->type == GDK_KEY_PRESS) ? "Key pressed" : "Key released", accel_name);
933 dt_free(accel_name);
934 }
935
936 // Remove the consumed Shift modifier for numbers.
937 // For French keyboards, numbers are accessed through Shift, e.g Shift + & = 1.
938 // Keeping Shift here would be meaningless and gets in the way.
939 if(gdk_keyval_to_lower(*keyval) == gdk_keyval_to_upper(*keyval))
940 {
941 *mods &= ~consumed;
942 }
943
944 // Shift + Tab gets decoded as ISO_Left_Tab and shift is consumed,
945 // so it gets absorbed by the previous correction.
946 // We need Ctrl+Shift+Tab to work as expected, so correct it.
947 if(*keyval == GDK_KEY_ISO_Left_Tab)
948 {
949 *keyval = GDK_KEY_Tab;
950 *mods |= GDK_SHIFT_MASK;
951 }
952
953 // Convert numpad keys to usual ones, because we care about WHAT is typed,
954 // not WHERE it is typed.
955 *keyval = dt_keys_mainpad_alternatives(*keyval);
956
957 // Hopefully no more heuristics required...
958}
959
960typedef struct _accel_lookup_t
961{
962 GList *results;
963 guint key;
964 GdkModifierType modifier;
965 GtkAccelGroup *group;
967
968static inline guint _normalize_keyval(const guint keyval)
969{
970 return gdk_keyval_to_lower(keyval);
971}
972
973
974static inline void _for_each_accel(gpointer key, gpointer value, gpointer user_data)
975{
976 dt_shortcut_t *shortcut = (dt_shortcut_t *)value;
977 const gchar *path = (const gchar *)key;
978 _accel_lookup_t *results = (_accel_lookup_t *)user_data;
979
980 // gtk_accel_group_activate() maps uppercase and lowercase to the same key,
981 // for compatibility we need to do the same.
982 const guint shortcut_key = _normalize_keyval(shortcut->key);
983 const guint result_key = _normalize_keyval(results->key);
984
985 if(shortcut->accel_group == results->group
986 && shortcut_key == result_key
987 && shortcut->mods == results->modifier)
988 {
989 if(!g_strcmp0(path, shortcut->path))
990 {
991 results->results = g_list_prepend(results->results, shortcut->path);
992 dt_print(DT_DEBUG_SHORTCUTS, "[shortcuts] Found accel %s for typed keys\n", path);
993 }
994 else
995 {
996 fprintf(stderr, "[shortcuts] ERROR: the shortcut path '%s' is known under the key '%s' in hashtable\n", shortcut->path, path);
997 }
998 }
999}
1000
1001
1002// Find the accel path for the matching key & modifier within the specified accel group.
1003// Return the path of the first accel found
1004static const char * _find_path_for_keys(dt_accels_t *accels, guint key, GdkModifierType modifier, GtkAccelGroup *group)
1005{
1006 _accel_lookup_t result = { .results = NULL, .key = key, .modifier = modifier, .group = group };
1007
1008 dt_pthread_mutex_lock(&accels->lock);
1009 g_hash_table_foreach(accels->acceleratables, _for_each_accel, &result);
1010 dt_pthread_mutex_unlock(&accels->lock);
1011
1012 char *path = NULL;
1013 GList *item = g_list_first(result.results);
1014 if(item) path = (char *)item->data;
1015
1016 g_list_free(result.results);
1017 result.results = NULL;
1018 return path;
1019}
1020
1021static inline void _for_each_non_virtual_accel(gpointer key, gpointer value, gpointer user_data)
1022{
1023 dt_shortcut_t *shortcut = (dt_shortcut_t *)value;
1024 const gchar *path = (const gchar *)key;
1025 _accel_lookup_t *results = (_accel_lookup_t *)user_data;
1026
1027 // gtk_accel_group_activate() maps uppercase and lowercase to the same key,
1028 // for compatibility we need to do the same.
1029 const guint shortcut_key = _normalize_keyval(shortcut->key);
1030 const guint result_key = _normalize_keyval(results->key);
1031
1032 if(shortcut->accel_group == results->group
1033 && shortcut_key == result_key
1034 && shortcut->mods == results->modifier
1035 && !shortcut->virtual_shortcut)
1036 {
1037 if(!g_strcmp0(path, shortcut->path))
1038 {
1039 results->results = g_list_prepend(results->results, shortcut);
1040 dt_print(DT_DEBUG_SHORTCUTS, "[shortcuts] Found accel %s for typed keys\n", path);
1041 }
1042 else
1043 {
1044 fprintf(stderr, "[shortcuts] ERROR: the shortcut path '%s' is known under the key '%s' in hashtable\n", shortcut->path, path);
1045 }
1046 }
1047}
1048
1049static dt_shortcut_t *_find_non_virtual_shortcut(dt_accels_t *accels, GtkAccelGroup *group, guint keyval,
1050 GdkModifierType mods)
1051{
1052 _accel_lookup_t result = { .results = NULL, .key = keyval, .modifier = mods, .group = group };
1053
1054 dt_pthread_mutex_lock(&accels->lock);
1055 g_hash_table_foreach(accels->acceleratables, _for_each_non_virtual_accel, &result);
1056 dt_pthread_mutex_unlock(&accels->lock);
1057
1058 dt_shortcut_t *shortcut = NULL;
1059 GList *item = g_list_first(result.results);
1060 if(!IS_NULL_PTR(item)) shortcut = (dt_shortcut_t *)item->data;
1061
1062 g_list_free(result.results);
1063 result.results = NULL;
1064 return shortcut;
1065}
1066
1067static gboolean _call_shortcut_cclosure(dt_shortcut_t *shortcut, GtkWindow *main_window, GClosure *closure);
1068
1069static gboolean _key_pressed(GtkWidget *w, GdkEvent *event, dt_accels_t *accels, guint keyval, GdkModifierType mods)
1070{
1071 // Get the accelerator entry from the accel group
1072 gchar *accel_name = gtk_accelerator_name(keyval, mods);
1073 dt_print(DT_DEBUG_SHORTCUTS, "[shortcuts] Combination of keys decoded: %s\n", accel_name);
1074 dt_free(accel_name);
1075
1076 // Look into the active group first, aka darkroom, lighttable, etc.
1077 dt_shortcut_t *shortcut = _find_non_virtual_shortcut(accels, accels->active_group, keyval, mods);
1078 if(!IS_NULL_PTR(shortcut) && _call_shortcut_cclosure(shortcut, GTK_WINDOW(w), NULL))
1079 {
1080 dt_print(DT_DEBUG_SHORTCUTS, "[shortcuts] Active group action executed\n");
1081 return TRUE;
1082 }
1083
1084 // If nothing found, try again with global accels.
1085 shortcut = _find_non_virtual_shortcut(accels, accels->global_accels, keyval, mods);
1086 if(!IS_NULL_PTR(shortcut) && _call_shortcut_cclosure(shortcut, GTK_WINDOW(w), NULL))
1087 {
1088 dt_print(DT_DEBUG_SHORTCUTS, "[shortcuts] Global group action executed\n");
1089 return TRUE;
1090 }
1091
1092 return FALSE;
1093}
1094
1095
1096gboolean dt_accels_dispatch(GtkWidget *w, GdkEvent *event, gpointer user_data)
1097{
1098 dt_accels_t *accels = (dt_accels_t *)user_data;
1099
1100 // Ditch everything that is not a key stroke or key strokes that are modifiers alone
1101 // Abort early for performance.
1102 if(event->key.is_modifier || IS_NULL_PTR(accels->active_group) || accels->reset > 0 || !gtk_window_is_active(GTK_WINDOW(w)))
1103 return FALSE;
1104
1105 if(!(event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE || event->type == GDK_SCROLL))
1106 return FALSE;
1107
1108 // Scroll event: dispatch and return
1109 if(event->type == GDK_SCROLL)
1110 {
1111 if(accels->scroll.callback)
1112 return accels->scroll.callback(event->scroll, accels->scroll.data);
1113 else
1114 return FALSE;
1115 }
1116
1117 // Key events: decode and dispatch
1118 GdkModifierType mods;
1119 guint keyval;
1120 _accels_keys_decode(accels, event, &keyval, &mods);
1121
1122 // Ugly design : global shortcuts are supposed to have a key modifier.
1123 // To allow single-key shortcuts, we have to work around that and impose our own shortcut handler.
1124 // But then, that breaks regular text input on GtkEntry, GtkSearchEntry, etc.
1125 // because letters are then captured as shortcuts.
1126 // Which was the whole purpose of forcing global shortcuts to use modifiers.
1127 // So, to avoid that, when text entries get focused, we manually set accels->disable_accels,
1128 // and unset it when they loose focus.
1129 // When "disabled", we reset typical Gtk behaviour : capture global shortcuts only if there is a modifier.
1130 // NOTE: this nasty workaround should not be taken as an incentive to extend it further.
1131 // It's bad design, it should not be turned into a rule.
1132 if(accels->disable_accels && !mods) return FALSE;
1133
1134 // When a text editor has keyboard focus, bypass accelerators so typing keeps
1135 // native widget behavior (letters, spaces, modifiers and editing keys).
1136 if(event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)
1137 {
1138 GtkWidget *focused = gtk_window_get_focus(GTK_WINDOW(w));
1139 if(!IS_NULL_PTR(focused) && (GTK_IS_EDITABLE(focused) || GTK_IS_TEXT_VIEW(focused)))
1140 {
1141 accels->active_key.accel_key = 0;
1142 accels->active_key.accel_mods = 0;
1143 return FALSE;
1144 }
1145 }
1146
1147 if(event->type == GDK_KEY_PRESS &&
1148 !(keyval == accels->active_key.accel_key && mods == accels->active_key.accel_mods))
1149 {
1150 // Store active keys until release
1151 accels->active_key.accel_key = keyval;
1152 accels->active_key.accel_mods = mods;
1153 return _key_pressed(w, event, accels, keyval, mods);
1154 }
1155 else if(event->type == GDK_KEY_RELEASE)
1156 {
1157 // Reset active keys
1158 accels->active_key.accel_key = 0;
1159 accels->active_key.accel_mods = 0;
1160 return FALSE;
1161 }
1162
1163 return FALSE;
1164}
1165
1166
1167void dt_accels_attach_scroll_handler(dt_accels_t *accels, gboolean (*callback)(GdkEventScroll event, void *data), void *data)
1168{
1169 accels->scroll.callback = callback;
1170 accels->scroll.data = data;
1171}
1172
1174{
1175 accels->scroll.callback = NULL;
1176 accels->scroll.data = NULL;
1177}
1178
1179// Ugly. Use that only for callbacks of the shortcuts GUI popup
1180// when the user_data pointer is already used for something else.
1181// This will be inited when opening the popup, so there is only
1182// one place/thread accessing it and the reference is up to date
1183// within the scope where it's used.
1185
1186enum
1187{
1198
1199typedef struct _accel_treeview_t
1200{
1201 GtkTreeStore *store;
1202 GHashTable *node_cache;
1204
1205
1206static void _make_column_editable(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
1207 GtkTreeIter *iter, gpointer data)
1208{
1209 dt_shortcut_t *shortcut;
1210 gtk_tree_model_get(model, iter, COL_SHORTCUT, &shortcut, -1);
1211 g_object_set(renderer,
1212 "visible", (!IS_NULL_PTR(shortcut)),
1213 "editable", (!IS_NULL_PTR(shortcut) && !shortcut->locked),
1214 "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_OTHER,
1215 NULL);
1216}
1217
1218static void _make_column_clearable(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
1219 GtkTreeIter *iter, gpointer data)
1220{
1221 dt_shortcut_t *shortcut;
1222 gtk_tree_model_get(model, iter, COL_SHORTCUT, &shortcut, -1);
1223 g_object_set (renderer,
1224 "icon-name", (!IS_NULL_PTR(shortcut) && !shortcut->locked) ? "edit-delete-symbolic" : "lock",
1225 "visible", (!IS_NULL_PTR(shortcut)),
1226 "sensitive", (!IS_NULL_PTR(shortcut) && !shortcut->locked && shortcut->key),
1227 NULL);
1228}
1229
1230
1231static int guess_key_group(dt_accels_t *accels, guint keyval, guint hardware_keycode)
1232{
1233 GdkKeymapKey *keys;
1234 guint *keyvals;
1235 gint n_keys;
1236
1237 if(!gdk_keymap_get_entries_for_keycode(accels->keymap, hardware_keycode, &keys, &keyvals, &n_keys))
1238 return 0;
1239
1240 for(int i = 0; i < n_keys; ++i)
1241 {
1242 if(keyvals[i] == keyval)
1243 {
1244 int group = keys[i].group;
1245 dt_free(keys);
1246 dt_free(keyvals);
1247 return group; // found matching group
1248 }
1249 }
1250
1251 dt_free(keys);
1252 dt_free(keyvals);
1253 return 0; // not found, default
1254}
1255
1256static void _shortcut_edited(GtkCellRenderer *cell, const gchar *path_string, guint key, GdkModifierType mods,
1257 guint hardware_key, gpointer user_data)
1258{
1259 // The tree model passed as arg is the filtered proxy.
1260 // We will need to access its underlying store (full, unfiltered)
1261 GtkTreeModel *filter = GTK_TREE_MODEL(user_data);
1262 GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(filter));
1263 if(IS_NULL_PTR(store)) return;
1264
1265 GtkTreePath *path = gtk_tree_path_new_from_string(path_string);
1266 dt_shortcut_t *shortcut = NULL;
1267
1268 // f_iter is the row coordinates relative to the filtered model
1269 // That's what we need to READ data
1270 GtkTreeIter f_iter;
1271 if(gtk_tree_model_get_iter(GTK_TREE_MODEL(filter), &f_iter, path))
1272 gtk_tree_model_get(GTK_TREE_MODEL(filter), &f_iter, COL_SHORTCUT, &shortcut, -1);
1273
1274 const char *shortcut_path = NULL;
1275 guint keyval = dt_keys_mainpad_alternatives(key);
1276
1277 // In GTK "OTHER" accel mode, clearing from the editor may come through either as
1278 // VoidSymbol or as an unmodified Delete/BackSpace key press. Normalize all those
1279 // cases to an empty shortcut so the model and the GtkAccelMap stay in sync.
1280 if(keyval == GDK_KEY_VoidSymbol
1281 || (mods == 0 && (keyval == GDK_KEY_Delete || keyval == GDK_KEY_BackSpace)))
1282 {
1283 keyval = 0;
1284 mods = 0;
1285 hardware_key = 0;
1286 }
1287
1288 // mods input arg doesn't record states (numlock, capslock), so we need to fetch it
1289 // directly before decoding full key combinations
1290 if(keyval != 0 || mods != 0)
1291 {
1292 GdkDisplay *display = gdk_display_get_default();
1293 GdkSeat *seat = gdk_display_get_default_seat(display);
1294 GdkDevice *pointer = gdk_seat_get_pointer(seat);
1295 GdkModifierType state;
1296 gdk_device_get_state(pointer, gdk_get_default_root_window(), NULL, &state);
1297
1298 // We only decode actual key strokes here. Clearing shortcuts bypasses this path
1299 // because there is no hardware key or modifier state to preserve.
1300 GdkEventKey event = { 0 };
1301 event.type = GDK_KEY_PRESS;
1302 event.state = mods | state;
1303 event.keyval = keyval;
1304 event.hardware_keycode = hardware_key;
1305 event.group = guess_key_group(accels_global_ref, keyval, hardware_key);
1306 _accels_keys_decode(accels_global_ref, (GdkEvent *)&event, &keyval, &mods);
1307 }
1308
1309 if(shortcut)
1310 {
1311 // Lookup this keys combination in the current accel_group (only if key is not empty)
1312 if(!(keyval == 0 && mods == 0))
1313 shortcut_path = _find_path_for_keys(shortcut->accels, keyval, mods, shortcut->accel_group);
1314
1315 // Try to update the GtkAccelMap with new keys
1316 if(IS_NULL_PTR(shortcut_path) && gtk_accel_map_change_entry(shortcut->path, keyval, mods, FALSE))
1317 {
1318 // Success:
1319 // Resync our internal shortcut object and its GtkAccelGroup to GtkAccelMap
1320 _connect_accel(shortcut);
1321
1322 // s_iter is the row coordinates relative to the child/source model (unfiltered)
1323 // That's what we need to WRITE data
1324 // And write new keys into the source model
1325 GtkTreeIter s_iter;
1326 gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(filter), &s_iter, &f_iter);
1327 gtk_tree_store_set(GTK_TREE_STORE(store), &s_iter, COL_KEYVAL, keyval, COL_MODS, mods, -1);
1328 }
1329 }
1330
1331 if(shortcut_path)
1332 {
1333 // The GtkAccelMap could not be updated because another accel uses the same keys
1334 // That also happens if we try to unset a shortcut more than once, but then it's no issue.
1335 char *new_text = gtk_accelerator_name(keyval, mods);
1336 GtkWidget *dlg
1337 = gtk_message_dialog_new_with_markup(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s <tt>%s</tt>\n%s <tt>%s</tt>.\n%s",
1338 _("The shortcut for"), shortcut_path,
1339 _("is already using the key combination"), new_text,
1340 _("Delete it first."));
1341 gtk_dialog_run(GTK_DIALOG(dlg));
1342 gtk_widget_destroy(dlg);
1343 dt_free(new_text);
1344 }
1345
1346 gtk_tree_path_free(path);
1347}
1348
1349static void _shortcut_cleared(GtkCellRendererAccel *renderer, const gchar *path_string, gpointer user_data)
1350{
1351 _shortcut_edited(GTK_CELL_RENDERER(renderer), path_string, 0, 0, 0, user_data);
1352}
1353
1354
1355static gboolean _icon_activate(GtkCellRenderer *cell, GdkEvent *event, GtkWidget *treeview, const gchar *path_str,
1356 GdkRectangle *background, GdkRectangle *cell_area, GtkCellRendererState flags,
1357 gpointer user_data)
1358{
1359 // Reset accel at current path
1360 _shortcut_edited(cell, path_str, 0, 0, 0, user_data);
1361 return TRUE;
1362}
1363
1364
1365static void _create_main_row(GtkTreeStore *store, GtkTreeIter *iter, const char *label, const char *path,
1366 dt_shortcut_t *shortcut)
1367{
1368 gtk_tree_store_set(store, iter,
1369 COL_NAME, label,
1370 COL_DESCRIPTION, shortcut->description,
1371 COL_PATH, path,
1372 COL_KEYVAL, shortcut->key,
1373 COL_MODS, shortcut->mods,
1374 COL_SHORTCUT, shortcut, -1);
1375}
1376
1377void _for_each_accel_create_treeview_row(gpointer key, gpointer value, gpointer user_data)
1378{
1379 // Extract HashTable key/value
1380 dt_shortcut_t *shortcut = (dt_shortcut_t *)value;
1381 if(IS_NULL_PTR(shortcut)) return;
1382 const gchar *path = (const gchar *)key;
1383
1384 // Extract user_data
1385 _accel_treeview_t *_data = (_accel_treeview_t *)user_data;
1386 GHashTable *node_cache = _data->node_cache;
1387 GtkTreeStore *store = _data->store;
1388
1389 GtkTreeIter *parent = NULL;
1390 GtkTreeIter *iter = NULL;
1391
1392 // Split the shortcut accel path on /.
1393 // Then we reconstruct it piece by piece and add a tree node fore each piece,
1394 // which lets us manage parents/children.
1395 // Note 1: parts[0] is always "<Ansel>"
1396 // Note 2: that fails if widget labels contain /
1397 gchar **parts = g_strsplit(path, "/", -1);
1398 gchar *accum = g_strdup("<Ansel>");
1399
1400 // We will copy pathes after <Ansel> string because that makes
1401 // treeview markup parsers fail since it looks like markup
1402 const size_t len_ansel = strlen(accum);
1403 for(int i = 1; parts[i]; ++i)
1404 {
1405 // Build the partial path so far
1406 gchar *tmp = g_strconcat(accum, "/", parts[i], NULL);
1407 dt_free(accum);
1408 accum = tmp;
1409
1410 // Find out if current node exists.
1411 // If it does, it will be our parent for the next step.
1412 iter = g_hash_table_lookup(node_cache, accum);
1413
1414 // If current node is not already in tree, add it.
1415 if(IS_NULL_PTR(iter))
1416 {
1417 // We need a heap-allocated iter to pass it along to the hashtable.
1418 // This will be freed when cleaning up the hashtable.
1419 GtkTreeIter new_iter;
1420 gtk_tree_store_append(store, &new_iter, parent);
1421
1422 // heap‑copy the struct to pass it along to the HashTable
1423 iter = g_new(GtkTreeIter, 1);
1424 *iter = new_iter;
1425 g_hash_table_insert(node_cache, g_strdup(accum), iter);
1426 }
1427
1428 // Capitalize first letter for GUI purposes
1429 gchar *label = g_strdup(parts[i]);
1430 dt_capitalize_label(label);
1431
1432 // Write the shortcut only if we are at the terminating point of the path
1433 if(!g_strcmp0(accum, path))
1434 _create_main_row(store, iter, label, path + len_ansel, shortcut);
1435 else
1436 gtk_tree_store_set(store, iter, COL_NAME, parts[i], COL_KEYS, "", COL_PATH, accum + len_ansel, -1);
1437
1438 dt_free(label);
1439
1440 parent = iter;
1441 }
1442
1443 dt_free(accum);
1444 g_strfreev(parts);
1445}
1446
1447static gchar *_shortcut_search_trim_display_path(const gchar *path)
1448{
1449 if(IS_NULL_PTR(path)) return g_strdup("");
1450
1451 gchar **parts = g_strsplit(path, "/", -1);
1452 const gint len = g_strv_length(parts);
1453 gchar *tail = NULL;
1454 if(len >= 3)
1455 tail = g_strjoinv("/", parts + 2);
1456 else if(len == 2)
1457 tail = g_strdup(parts[1]);
1458 else
1459 tail = g_strdup(parts[0]);
1460 g_strfreev(parts);
1461 return tail;
1462}
1463
1464void _for_each_path_create_treeview_row(gpointer key, gpointer value, gpointer user_data)
1465{
1466 // Extract HashTable key/value
1467 dt_shortcut_t *shortcut = (dt_shortcut_t *)value;
1468 if(IS_NULL_PTR(shortcut)) return;
1469 const gchar *path = (const gchar *)key;
1470
1471 GtkListStore *store = (GtkListStore *)user_data;
1472 if(IS_NULL_PTR(store)) return;
1473
1474 dt_accels_t *accels = shortcut->accels;
1475 //g_print("My object is a <%s>\n", G_OBJECT_TYPE_NAME(store));
1476
1477 // Append the shortcut path, minus initial <Ansel> root, to a flat list
1478 // only if the shortcut belongs to one currently-active accel group
1479 if(shortcut->accel_group == accels->global_accels ||
1480 shortcut->accel_group == accels->active_group)
1481 {
1482 gchar *tail = _shortcut_search_trim_display_path(path);
1483 gchar **tail_parts = g_strsplit(tail, "/", -1);
1484 const gint tail_len = g_strv_length(tail_parts);
1485 const gchar *leaf = tail;
1486 if(tail_len > 0 && !IS_NULL_PTR(tail_parts[tail_len - 1]) && tail_parts[tail_len - 1][0] != '\0')
1487 leaf = tail_parts[tail_len - 1];
1488
1489 GtkTreeIter iter;
1490 gtk_list_store_append(store, &iter);
1491 gtk_list_store_set(store, &iter,
1492 0, tail, // shortcut path
1493 1, shortcut, // shortcut object
1494 2, 0, // init relevance
1495 3, shortcut->description, // description
1496 4, leaf, // leaf label used for inline completion
1497 5, shortcut->key,
1498 6, shortcut->mods,
1499 -1);
1500 g_strfreev(tail_parts);
1501 dt_free(tail);
1502 }
1503}
1504
1505// Relevance coeff stored in column index 2
1506static gint _sort_model_by_relevance_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
1507{
1508 int ka, kb;
1509 gchar *pa = NULL, *pb = NULL;
1510 gtk_tree_model_get(model, a, 2, &ka, 0, &pa, -1);
1511 gtk_tree_model_get(model, b, 2, &kb, 0, &pb, -1);
1512
1513 if(ka != kb)
1514 {
1515 dt_free(pa);
1516 dt_free(pb);
1517 return ka - kb;
1518 }
1519
1520 gint ret = 0;
1521 if(!IS_NULL_PTR(pa) && !IS_NULL_PTR(pb))
1522 {
1523 gchar *pa_ci = g_utf8_casefold(pa, -1);
1524 gchar *pb_ci = g_utf8_casefold(pb, -1);
1525 gchar **pa_parts = g_strsplit(pa_ci, "/", -1);
1526 gchar **pb_parts = g_strsplit(pb_ci, "/", -1);
1527
1528 for(gint i = 0;; i++)
1529 {
1530 const gchar *pa_part = pa_parts[i];
1531 const gchar *pb_part = pb_parts[i];
1532 if(IS_NULL_PTR(pa_part) && IS_NULL_PTR(pb_part))
1533 {
1534 ret = 0;
1535 break;
1536 }
1537 if(IS_NULL_PTR(pa_part))
1538 {
1539 ret = -1;
1540 break;
1541 }
1542 if(IS_NULL_PTR(pb_part))
1543 {
1544 ret = 1;
1545 break;
1546 }
1547
1548 ret = g_utf8_collate(pa_part, pb_part);
1549 if(ret != 0) break;
1550 }
1551
1552 g_strfreev(pb_parts);
1553 g_strfreev(pa_parts);
1554 dt_free(pb_ci);
1555 dt_free(pa_ci);
1556 }
1557
1558 dt_free(pa);
1559 dt_free(pb);
1560 return ret;
1561}
1562
1563static gint _sort_model_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
1564{
1565 gchar *ka, *kb;
1566 gtk_tree_model_get(model, a, GPOINTER_TO_INT(data), &ka, -1);
1567 gtk_tree_model_get(model, b, GPOINTER_TO_INT(data), &kb, -1);
1568
1569 gint res = 0;
1570 if(ka && kb)
1571 {
1572 // Make strings case-insensitive
1573 gchar *ka_ci = g_utf8_casefold(ka, -1);
1574 gchar *kb_ci = g_utf8_casefold(kb, -1);
1575
1576 // Compare strings
1577 res = g_utf8_collate(ka_ci, kb_ci);
1578
1579 dt_free(ka_ci);
1580 dt_free(kb_ci);
1581 }
1582
1583 dt_free(ka);
1584 dt_free(kb);
1585 return res;
1586}
1587
1594
1595
1596static gboolean filter_callback(GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
1597{
1598 _accel_window_params_t *params = (_accel_window_params_t *)user_data;
1599
1600 // Everything visible if needle is empty or NULL, aka no active search
1601 const gchar *needle_path = gtk_entry_get_text(GTK_ENTRY(params->path_search));
1602 const gchar *needle_keys = gtk_entry_get_text(GTK_ENTRY(params->keys_search));
1603
1604 if((IS_NULL_PTR(needle_path) || needle_path[0] == '\0') &&
1605 (IS_NULL_PTR(needle_keys) || needle_keys[0] == '\0'))
1606 return TRUE;
1607
1608 gboolean show = TRUE;
1609
1610 // Check if path matches
1611 gchar *path = NULL;
1612 gtk_tree_model_get(model, iter, COL_PATH, &path, -1);
1613 if(needle_path && needle_path[0])
1614 {
1615 if(path && path[0] != '\0')
1616 {
1617 gchar *needle_ci = g_utf8_casefold(needle_path, -1);
1618 gchar *haystack_ci = g_utf8_casefold(path, -1);
1619 show &= (g_strrstr(haystack_ci, needle_ci) != NULL);
1620 dt_free(needle_ci);
1621 dt_free(haystack_ci);
1622 dt_free(path);
1623 }
1624 else
1625 {
1626 show &= FALSE;
1627 }
1628 }
1629
1630 // Check if keys match
1631 if(needle_keys && needle_keys[0] != '\0')
1632 {
1633 guint search_keyval = 0;
1634 GdkModifierType search_mods = 0;
1635 gtk_accelerator_parse(needle_keys, &search_keyval, &search_mods);
1636 if(search_keyval || search_mods)
1637 {
1638 guint keyval = 0;
1639 GdkModifierType mods = 0;
1640 gtk_tree_model_get(model, iter, COL_KEYVAL, &keyval, COL_MODS, &mods, -1);
1641 keyval = _normalize_keyval(keyval);
1642 search_keyval = _normalize_keyval(search_keyval);
1643
1644 // If both keyval and mods are searched, use strict mode.
1645 // Else use fuzzy mode
1646 if(search_keyval && search_mods)
1647 show &= (keyval == search_keyval && mods == search_mods);
1648 else
1649 show &= ((keyval && keyval == search_keyval) || (mods && mods == search_mods));
1650 }
1651 else
1652 {
1653 // Parsing failed, keys/modifiers syntax is wrong: let user know
1654 show &= FALSE;
1655 }
1656 }
1657
1658 if(show) return TRUE;
1659
1660 // Check again recursively if any of the current item's children has an accel path matching
1661 if(gtk_tree_model_iter_has_child(model, iter))
1662 {
1663 GtkTreeIter child;
1664 if(gtk_tree_model_iter_children(model, &child, iter))
1665 {
1666 do
1667 {
1668 if(filter_callback(model, &child, user_data))
1669 return TRUE;
1670 } while(gtk_tree_model_iter_next(model, &child));
1671 }
1672 }
1673
1674 return FALSE;
1675}
1676
1677static void search_changed(GtkEntry *entry, gpointer user_data)
1678{
1679 _accel_window_params_t *params = (_accel_window_params_t *)user_data;
1680 GtkTreeView *tree_view = GTK_TREE_VIEW(params->tree_view);
1681 gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(gtk_tree_view_get_model(tree_view)));
1682
1683 // Everything visible if needle is empty or NULL, aka no active search
1684 const gchar *needle_path = gtk_entry_get_text(GTK_ENTRY(params->path_search));
1685 const gchar *needle_keys = gtk_entry_get_text(GTK_ENTRY(params->keys_search));
1686
1687 if((IS_NULL_PTR(needle_path) || needle_path[0] == '\0') &&
1688 (IS_NULL_PTR(needle_keys) || needle_keys[0] == '\0'))
1689 gtk_tree_view_collapse_all(GTK_TREE_VIEW(params->tree_view));
1690 else
1691 gtk_tree_view_expand_all(GTK_TREE_VIEW(params->tree_view));
1692}
1693
1694
1695void dt_accels_window(dt_accels_t *accels, GtkWindow *main_window)
1696{
1697 // Update the ugly global variable referencing accels
1698 accels_global_ref = accels;
1699
1700 _accel_window_params_t *params = malloc(sizeof(_accel_window_params_t));
1701 params->keys_search = gtk_search_entry_new();
1702 params->path_search = gtk_search_entry_new();
1703 GtkWidget *tree_view = params->tree_view = gtk_tree_view_new();
1704
1705 // Setup auto-completion on key modifiers because they are annoying
1706 // Note: omit the initial < character in modifier names as it is used to trigger matching
1707 // and won't be appended
1708 static dt_gtkentry_completion_spec default_path_compl_list[]
1709 = { { "Primary>", N_("<Primary> - Decoded as <Control> on Windows/Linux or <Meta> on Mac OS") },
1710 { "Control>", N_("<Control>") },
1711 { "Shift>", N_("<Shift>") },
1712 { "Alt>", N_("<Alt>") },
1713 { "Super>", N_("<Super> - The Windows key on PC") },
1714 { "Hyper>", N_("<Hyper>") },
1715 { "Meta>", N_("<Meta> - Decoded as <Command> on Mac OS") },
1716 { NULL, NULL } };
1717 dt_gtkentry_setup_completion(GTK_ENTRY(params->keys_search), default_path_compl_list, "<");
1718 gtk_widget_set_tooltip_text(params->keys_search, _("Look for keys and modifiers codes, as `<Modifier>Key`.\n"
1719 "Type `<` to start the auto-completion"));
1720
1721 gtk_widget_set_tooltip_text(params->path_search, _("Case-insensitive search for keywords of full pathes.\n"
1722 "Ex: `darkroom/controls/sliders`"));
1723
1724 // Set dialog window properties
1725 GtkWidget *dialog = gtk_dialog_new();
1726 gtk_window_set_title(GTK_WINDOW(dialog), _("Ansel - Keyboard shortcuts"));
1727
1728#ifdef GDK_WINDOWING_QUARTZ
1730 gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER_ON_PARENT);
1731#endif
1732
1733 gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
1734 gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
1735 gtk_window_set_transient_for(GTK_WINDOW(dialog), main_window);
1736 gtk_window_set_default_size(GTK_WINDOW(dialog), 1100, 900);
1737
1738 // Create the full (non-filtered) tree view model
1739 GtkTreeStore *store = gtk_tree_store_new(NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, G_TYPE_STRING,
1740 G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_UINT, G_TYPE_UINT);
1741
1742 // Add a tree view row for each accel
1743 GHashTable *node_cache = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
1744 _accel_treeview_t _data = { .store = store , .node_cache = node_cache};
1745 g_hash_table_foreach(accels->acceleratables, _for_each_accel_create_treeview_row, &_data);
1746 g_hash_table_destroy(node_cache);
1747
1748 // Sort rows alphabetically by path
1749 for(int i = COL_NAME; i < COL_KEYS; i++)
1750 {
1751 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(store), i, (GtkTreeIterCompareFunc)_sort_model_func,
1752 GINT_TO_POINTER(i), NULL);
1753 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store), i, GTK_SORT_ASCENDING);
1754 }
1755
1756 // Set the search feature, aka wire the Gtk search entry to a GtkTreeModelFilter
1757 GtkTreeModel *filter_model = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL);
1758 gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter_model), filter_callback, params, NULL);
1759
1760 // So the content of the treeview is NOT the original (full) model, but the filtered one
1761 gtk_tree_view_set_model(GTK_TREE_VIEW(tree_view), filter_model);
1762 gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(tree_view), COL_PATH);
1763 gtk_widget_set_hexpand(tree_view, TRUE);
1764 gtk_widget_set_vexpand(tree_view, TRUE);
1765 gtk_widget_set_halign(tree_view, GTK_ALIGN_FILL);
1766 gtk_widget_set_valign(tree_view, GTK_ALIGN_FILL);
1767
1768 g_signal_connect(G_OBJECT(params->path_search), "changed", G_CALLBACK(search_changed), params);
1769 g_signal_connect(G_OBJECT(params->keys_search), "changed", G_CALLBACK(search_changed), params);
1770
1771 // Add tree view columns
1772 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_("View / Scope / Feature / Control"), gtk_cell_renderer_text_new(), "text", COL_NAME, NULL);
1773 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
1774
1775 GtkCellRenderer *renderer = gtk_cell_renderer_accel_new();
1776 column = gtk_tree_view_column_new_with_attributes(_("Keys"), renderer, "accel-key", COL_KEYVAL, "accel-mods",
1777 COL_MODS, NULL);
1778 gtk_tree_view_column_set_cell_data_func(column, renderer, _make_column_editable, NULL, NULL);
1779 g_signal_connect(renderer, "accel-edited", G_CALLBACK(_shortcut_edited), filter_model);
1780 g_signal_connect(renderer, "accel-cleared", G_CALLBACK(_shortcut_cleared), filter_model);
1781 gtk_tree_view_column_set_min_width(column, 100);
1782 gtk_tree_view_column_set_resizable(column, TRUE);
1783 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
1784
1785 renderer = dtgtk_cell_renderer_button_new();
1786 g_object_set(renderer, "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL);
1787 column = gtk_tree_view_column_new_with_attributes(_("Clear"), renderer, "pixbuf", COL_CLEAR, NULL);
1788 gtk_tree_view_column_set_cell_data_func(column, renderer, _make_column_clearable, NULL, NULL);
1789 g_signal_connect(renderer, "activate", G_CALLBACK(_icon_activate), filter_model);
1790 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
1791
1792 column = gtk_tree_view_column_new_with_attributes(_("Description"), gtk_cell_renderer_text_new(), "text", COL_DESCRIPTION, NULL);
1793 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
1794
1795 // Pack and show widgets
1796 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
1797 gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), box, TRUE, TRUE, 0);
1798
1799 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
1800 gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(_("Search by feature : ")), FALSE, FALSE, 0);
1801 gtk_box_pack_start(GTK_BOX(hbox), params->path_search, TRUE, TRUE, 0);
1802 gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(_("Search by keys : ")), FALSE, FALSE, 0);
1803 gtk_box_pack_start(GTK_BOX(hbox), params->keys_search, TRUE, TRUE, 0);
1804 gtk_box_pack_start(GTK_BOX(box), hbox, FALSE, FALSE, 0);
1805
1806 GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL);
1807 dt_gui_add_class(scrolled_window, "dt_recessed_scroll");
1808 gtk_container_add(GTK_CONTAINER(scrolled_window), tree_view);
1809 gtk_box_pack_start(GTK_BOX(box), scrolled_window, TRUE, TRUE, 0);
1810
1811 gtk_widget_set_visible(tree_view, TRUE);
1812 gtk_widget_show_all(dialog);
1813
1814 gtk_dialog_run(GTK_DIALOG(dialog));
1815 gtk_widget_destroy(dialog);
1816 g_object_unref(filter_model);
1817 g_object_unref(store);
1818 dt_free(params);
1819}
1820
1821// Case-insensitive partial matching
1822// Return:
1823// - 0: perfect match
1824// - > 0: matches increasingly worse (rank)
1825// - -1: no match
1826static int _match_text(GtkTreeModel *model, GtkTreeIter *iter, const char *needle)
1827{
1828 int ret = -1;
1829 if(IS_NULL_PTR(needle) || needle[0] == '\0') return 0;
1830
1831 // Get row entry
1832 gchar *label;
1833 gtk_tree_model_get(model, iter, 0, &label, -1);
1834 if(IS_NULL_PTR(label) || label[0] == '\0')
1835 {
1836 dt_free(label);
1837 return -1;
1838 }
1839
1840 // Convert to lowercase
1841 gchar *label_ci = g_utf8_casefold(label, -1);
1842
1843 gchar **parts = g_strsplit(label_ci, "/", -1);
1844 gchar *needle_copy = g_strdup(needle);
1845 gchar **tokens = g_strsplit_set(needle_copy, " \t\r\n", -1);
1846 int rank_sum = 0;
1847 gboolean has_token = FALSE;
1848 gboolean all_matched = TRUE;
1849 for(gint t = 0; !IS_NULL_PTR(tokens[t]); t++)
1850 {
1851 if(tokens[t][0] == '\0') continue;
1852 has_token = TRUE;
1853 int best_token_rank = INT_MAX;
1854 for(gint i = 0; !IS_NULL_PTR(parts[i]); i++)
1855 {
1856 // Rank by path cell index first (left cells first), then by position inside the cell.
1857 // This keeps generic scopes before deeper scopes for each search token.
1858 const char *match = g_strstr_len(parts[i], -1, tokens[t]);
1859 if(IS_NULL_PTR(match)) continue;
1860
1861 const int cell_rank = i * 10000;
1862 const int in_cell_rank = match - parts[i];
1863 const int token_rank = cell_rank + in_cell_rank;
1864 if(token_rank < best_token_rank) best_token_rank = token_rank;
1865 }
1866
1867 if(best_token_rank == INT_MAX)
1868 {
1869 all_matched = FALSE;
1870 break;
1871 }
1872 rank_sum += best_token_rank;
1873 }
1874
1875 if(all_matched) ret = has_token ? rank_sum : 0;
1876
1877 g_strfreev(tokens);
1878 dt_free(needle_copy);
1879 g_strfreev(parts);
1880
1881 dt_free(label);
1882 dt_free(label_ci);
1883
1884 return ret;
1885}
1886
1888{
1889 const gchar *needle = gtk_entry_get_text(GTK_ENTRY(search_entry));
1890 gchar *needle_query = g_strdup(!IS_NULL_PTR(needle) ? needle : "");
1891 gchar *needle_sep = g_strstr_len(needle_query, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
1892 if(!IS_NULL_PTR(needle_sep)) *needle_sep = '\0';
1893 g_strstrip(needle_query);
1894 gchar *needle_ci = g_utf8_casefold(needle_query, -1);
1895
1896 // Block sorting while we update the content of the column used to sort rows
1897 // otherwise that makes updating iterations recurse and ultimately fail
1898 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
1899 GTK_SORT_ASCENDING);
1900
1901 GtkTreeIter iter;
1902 if(gtk_tree_model_get_iter_first(model, &iter))
1903 {
1904 do
1905 {
1906 int rank = _match_text(model, &iter, needle_ci);
1907 gtk_list_store_set(GTK_LIST_STORE(model), &iter, 2, rank, -1);
1908
1909 } while(gtk_tree_model_iter_next(model, &iter));
1910 }
1911
1912 dt_free(needle_ci);
1913 dt_free(needle_query);
1914
1915 // Restore sorting
1916 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(model), 2, GTK_SORT_ASCENDING);
1917 gtk_tree_sortable_sort_column_changed(GTK_TREE_SORTABLE(model));
1918}
1919
1937
1938#define DT_ACCEL_SEARCH_RECENT_KEY "plugins/accel_search/recent_entries"
1939#define DT_ACCEL_SEARCH_RECENT_MAX 20
1940
1942{
1943 if(IS_NULL_PTR(store)) return;
1944 gtk_list_store_clear(store);
1945
1946 for(gint i = 0; i < DT_ACCEL_SEARCH_RECENT_MAX; i++)
1947 {
1948 gchar *entry_key = g_strdup_printf("%s/%d", DT_ACCEL_SEARCH_RECENT_KEY, i);
1949 if(!dt_conf_key_exists(entry_key))
1950 {
1951 dt_free(entry_key);
1952 continue;
1953 }
1954
1955 gchar *entry = dt_conf_get_string(entry_key);
1956 dt_free(entry_key);
1957 if(IS_NULL_PTR(entry) || entry[0] == '\0')
1958 {
1959 dt_free(entry);
1960 continue;
1961 }
1962 g_strstrip(entry);
1963 if(entry[0] == '\0')
1964 {
1965 dt_free(entry);
1966 continue;
1967 }
1968
1969 // Keep split limit to 3 for backward compatibility with older persisted
1970 // values using "query<TAB>command<TAB>description".
1971 gchar **parts = g_strsplit(entry, "\t", 3);
1972 if(IS_NULL_PTR(parts[0]) || IS_NULL_PTR(parts[1])
1973 || parts[0][0] == '\0' || parts[1][0] == '\0')
1974 {
1975 g_strfreev(parts);
1976 dt_free(entry);
1977 continue;
1978 }
1979
1980 const gchar *query = parts[0];
1981 const gchar *command = parts[1];
1982 gchar *display_command = _shortcut_search_trim_display_path(command);
1983 gchar *display = g_strdup_printf("%s%s%s", query, DT_ACCEL_SEARCH_INLINE_SEPARATOR, display_command);
1984
1985 GtkTreeIter iter;
1986 gtk_list_store_append(store, &iter);
1987 gtk_list_store_set(store, &iter,
1988 0, query,
1989 1, command,
1990 2, "",
1991 3, display,
1992 4, i,
1993 -1);
1994 dt_free(display_command);
1995 dt_free(display);
1996 g_strfreev(parts);
1997 dt_free(entry);
1998 }
1999}
2000
2001static gint _shortcut_search_recent_sort_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
2002{
2003 const dt_accels_search_state_t *state = (const dt_accels_search_state_t *)user_data;
2004 const gchar *search_text = "";
2005 if(!IS_NULL_PTR(state) && !IS_NULL_PTR(state->search_entry))
2006 {
2007 const gchar *entry_text = gtk_entry_get_text(GTK_ENTRY(state->search_entry));
2008 if(!IS_NULL_PTR(entry_text)) search_text = entry_text;
2009 }
2010
2011 gchar *query_text = g_strdup(search_text);
2012 gchar *query_sep = g_strstr_len(query_text, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
2013 if(!IS_NULL_PTR(query_sep)) *query_sep = '\0';
2014 g_strstrip(query_text);
2015 gchar *query_ci = g_utf8_casefold(query_text, -1);
2016 const gboolean has_query = query_ci[0] != '\0';
2017
2018 gchar *a_query = NULL, *a_command = NULL;
2019 gchar *b_query = NULL, *b_command = NULL;
2020 gint a_recent = G_MAXINT, b_recent = G_MAXINT;
2021 gtk_tree_model_get(model, a, 0, &a_query, 1, &a_command, 4, &a_recent, -1);
2022 gtk_tree_model_get(model, b, 0, &b_query, 1, &b_command, 4, &b_recent, -1);
2023
2024 gchar *a_query_ci = g_utf8_casefold(!IS_NULL_PTR(a_query) ? a_query : "", -1);
2025 gchar *a_command_ci = g_utf8_casefold(!IS_NULL_PTR(a_command) ? a_command : "", -1);
2026 gchar *b_query_ci = g_utf8_casefold(!IS_NULL_PTR(b_query) ? b_query : "", -1);
2027 gchar *b_command_ci = g_utf8_casefold(!IS_NULL_PTR(b_command) ? b_command : "", -1);
2028
2029 gint a_rank = 400000, b_rank = 400000;
2030 if(has_query)
2031 {
2032 if(g_str_has_prefix(a_query_ci, query_ci))
2033 a_rank = 0;
2034 else if(g_str_has_prefix(a_command_ci, query_ci))
2035 a_rank = 100000;
2036 else
2037 {
2038 const gchar *a_match_query = g_strstr_len(a_query_ci, -1, query_ci);
2039 if(!IS_NULL_PTR(a_match_query))
2040 a_rank = 200000 + (a_match_query - a_query_ci);
2041 else
2042 {
2043 const gchar *a_match_command = g_strstr_len(a_command_ci, -1, query_ci);
2044 if(!IS_NULL_PTR(a_match_command))
2045 a_rank = 300000 + (a_match_command - a_command_ci);
2046 }
2047 }
2048
2049 if(g_str_has_prefix(b_query_ci, query_ci))
2050 b_rank = 0;
2051 else if(g_str_has_prefix(b_command_ci, query_ci))
2052 b_rank = 100000;
2053 else
2054 {
2055 const gchar *b_match_query = g_strstr_len(b_query_ci, -1, query_ci);
2056 if(!IS_NULL_PTR(b_match_query))
2057 b_rank = 200000 + (b_match_query - b_query_ci);
2058 else
2059 {
2060 const gchar *b_match_command = g_strstr_len(b_command_ci, -1, query_ci);
2061 if(!IS_NULL_PTR(b_match_command))
2062 b_rank = 300000 + (b_match_command - b_command_ci);
2063 }
2064 }
2065 }
2066
2067 gint ret = a_rank - b_rank;
2068 if(ret == 0) ret = a_recent - b_recent;
2069 if(ret == 0) ret = g_utf8_collate(a_query_ci, b_query_ci);
2070 if(ret == 0) ret = g_utf8_collate(a_command_ci, b_command_ci);
2071
2072 dt_free(b_command_ci);
2073 dt_free(b_query_ci);
2074 dt_free(a_command_ci);
2075 dt_free(a_query_ci);
2076 dt_free(b_command);
2077 dt_free(b_query);
2078 dt_free(a_command);
2079 dt_free(a_query);
2080 dt_free(query_ci);
2081 dt_free(query_text);
2082 return ret;
2083}
2084
2085static void _shortcut_search_save_recent_entry(const char *query, const dt_shortcut_t *shortcut)
2086{
2087 if(IS_NULL_PTR(query)) return;
2088 if(IS_NULL_PTR(shortcut) || IS_NULL_PTR(shortcut->path) || shortcut->path[0] == '\0') return;
2089
2090 gchar *trimmed = g_strdup(query);
2091 g_strstrip(trimmed);
2092 if(trimmed[0] == '\0')
2093 {
2094 dt_free(trimmed);
2095 return;
2096 }
2097
2098 gchar *command = g_strdup(shortcut->path);
2099 g_strdelimit(trimmed, "\t\r\n", ' ');
2100 g_strdelimit(command, "\t\r\n", ' ');
2101
2102 GPtrArray *entries = g_ptr_array_new_with_free_func(g_free);
2103 GPtrArray *entries_ci = g_ptr_array_new_with_free_func(g_free);
2104 g_ptr_array_add(entries, g_strdup_printf("%s\t%s", trimmed, command));
2105 g_ptr_array_add(entries_ci, g_utf8_casefold(trimmed, -1));
2106
2108 {
2109 gchar *entry_key = g_strdup_printf("%s/%d", DT_ACCEL_SEARCH_RECENT_KEY, i);
2110 if(!dt_conf_key_exists(entry_key))
2111 {
2112 dt_free(entry_key);
2113 continue;
2114 }
2115
2116 gchar *candidate = dt_conf_get_string(entry_key);
2117 dt_free(entry_key);
2118 if(IS_NULL_PTR(candidate) || candidate[0] == '\0')
2119 {
2120 dt_free(candidate);
2121 continue;
2122 }
2123 g_strstrip(candidate);
2124 if(candidate[0] == '\0')
2125 {
2126 dt_free(candidate);
2127 continue;
2128 }
2129
2130 gchar **parts = g_strsplit(candidate, "\t", 3);
2131 if(IS_NULL_PTR(parts[0]) || IS_NULL_PTR(parts[1])
2132 || parts[0][0] == '\0' || parts[1][0] == '\0')
2133 {
2134 g_strfreev(parts);
2135 dt_free(candidate);
2136 continue;
2137 }
2138
2139 const gchar *candidate_query = parts[0];
2140 const gchar *candidate_command = parts[1];
2141
2142 gchar *candidate_ci = g_utf8_casefold(candidate_query, -1);
2143 gboolean found = FALSE;
2144 for(guint k = 0; k < entries_ci->len; k++)
2145 {
2146 const char *kept_ci = g_ptr_array_index(entries_ci, k);
2147 if(!g_strcmp0(kept_ci, candidate_ci))
2148 {
2149 found = TRUE;
2150 break;
2151 }
2152 }
2153 if(found)
2154 {
2155 dt_free(candidate_ci);
2156 g_strfreev(parts);
2157 dt_free(candidate);
2158 continue;
2159 }
2160 g_ptr_array_add(entries, g_strdup_printf("%s\t%s", candidate_query, candidate_command));
2161 g_ptr_array_add(entries_ci, candidate_ci);
2162 g_strfreev(parts);
2163 dt_free(candidate);
2164 }
2165
2166 for(gint i = 0; i < DT_ACCEL_SEARCH_RECENT_MAX; i++)
2167 {
2168 gchar *entry_key = g_strdup_printf("%s/%d", DT_ACCEL_SEARCH_RECENT_KEY, i);
2169 const gchar *entry_value = (i < entries->len) ? (const gchar *)g_ptr_array_index(entries, i) : "";
2170 dt_conf_set_string(entry_key, entry_value);
2171 dt_free(entry_key);
2172 }
2174
2175 g_ptr_array_free(entries_ci, TRUE);
2176 g_ptr_array_free(entries, TRUE);
2177 dt_free(command);
2178 dt_free(trimmed);
2179}
2180
2182{
2183 // Identify the shortcut by path + owning table rather than by raw pointer:
2184 // dispatch is deferred (idle/timeout) and the dt_shortcut_t may be freed and
2185 // rebuilt in between (view/module switches), so we re-resolve it when we fire.
2186 gchar *path; // owned copy of the selected shortcut's path
2187 dt_accels_t *accels; // table to re-resolve the shortcut from
2188 GtkWindow *main_window;
2189 guint retries;
2191
2192static gboolean _dispatch_selected_shortcut_idle(gpointer data);
2193
2194// redo the suggestion list on each entry change
2195static void _search_entry_changed(GtkWidget *widget, gpointer user_data)
2196{
2198 state->selected = NULL;
2199 if(!IS_NULL_PTR(state->recent_entries))
2200 gtk_tree_sortable_sort_column_changed(GTK_TREE_SORTABLE(state->recent_entries));
2201 _find_and_rank_matches(GTK_TREE_MODEL(state->store), widget);
2202 gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(state->filter_model));
2203
2204 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(state->tree_view));
2205 gtk_tree_selection_unselect_all(selection);
2206
2207 GtkTreeIter iter;
2208 gboolean has_iter = gtk_tree_model_get_iter_first(state->filter_model, &iter);
2209 GtkTreeIter fallback_iter = iter;
2210 GtkTreeIter selected_iter = iter;
2211 gboolean found_preferred = FALSE;
2212 if(has_iter && !IS_NULL_PTR(state->preferred_command) && state->preferred_command[0] != '\0')
2213 {
2214 do
2215 {
2216 dt_shortcut_t *shortcut = NULL;
2217 gtk_tree_model_get(state->filter_model, &iter, 1, &shortcut, -1);
2218 if(!IS_NULL_PTR(shortcut) && !IS_NULL_PTR(shortcut->path)
2219 && !g_strcmp0(shortcut->path, state->preferred_command))
2220 {
2221 selected_iter = iter;
2222 found_preferred = TRUE;
2223 break;
2224 }
2225 } while(gtk_tree_model_iter_next(state->filter_model, &iter));
2226 }
2227
2228 if(has_iter)
2229 {
2230 GtkTreeIter *target_iter = found_preferred ? &selected_iter : &fallback_iter;
2231 GtkTreePath *path = gtk_tree_model_get_path(state->filter_model, target_iter);
2232 if(IS_NULL_PTR(path)) return;
2233 gtk_tree_selection_select_iter(selection, target_iter);
2234 gtk_tree_view_set_cursor(GTK_TREE_VIEW(state->tree_view), path, NULL, FALSE);
2235 gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(state->tree_view), path, NULL, FALSE, 0.f, 0.f);
2236 gtk_tree_path_free(path);
2237
2238 gtk_tree_model_get(state->filter_model, target_iter, 1, &state->selected, -1);
2239 }
2240}
2241
2242static gboolean _shortcut_search_recent_match_selected(GtkEntryCompletion *completion, GtkTreeModel *model,
2243 GtkTreeIter *iter, gpointer user_data)
2244{
2246 gchar *query = NULL;
2247 gchar *command = NULL;
2248 gtk_tree_model_get(model, iter, 0, &query, 1, &command, -1);
2249
2250 if(!IS_NULL_PTR(state->preferred_command))
2251 {
2252 dt_free(state->preferred_command);
2253 state->preferred_command = NULL;
2254 }
2255 if(!IS_NULL_PTR(command) && command[0] != '\0')
2256 state->preferred_command = g_strdup(command);
2257
2258 if(!IS_NULL_PTR(query))
2259 {
2260 gtk_entry_set_text(GTK_ENTRY(state->search_entry), query);
2261 gtk_editable_set_position(GTK_EDITABLE(state->search_entry), -1);
2262 }
2263
2264 dt_free(command);
2265 dt_free(query);
2266 return TRUE;
2267}
2268
2269static gboolean _shortcut_search_recent_insert_prefix(GtkEntryCompletion *completion, gchar *prefix, gpointer user_data)
2270{
2272 if(IS_NULL_PTR(state) || IS_NULL_PTR(state->search_entry)) return FALSE;
2273 const gchar *entry_text = gtk_entry_get_text(GTK_ENTRY(state->search_entry));
2274 if(state->suppress_inline_once)
2275 {
2276 state->suppress_inline_once = FALSE;
2277 return TRUE;
2278 }
2279
2280 GtkTreeModel *model = gtk_entry_completion_get_model(completion);
2281 if(IS_NULL_PTR(model)) return FALSE;
2282 if(!IS_NULL_PTR(entry_text) && entry_text[0] != '\0')
2283 {
2284 const gchar *last = g_utf8_find_prev_char(entry_text, entry_text + strlen(entry_text));
2285 const gunichar last_char = !IS_NULL_PTR(last) ? g_utf8_get_char(last) : 0;
2286 if(last_char != 0 && !g_unichar_isalnum(last_char))
2287 return TRUE;
2288 }
2289 gchar *query = g_strdup(!IS_NULL_PTR(entry_text) ? entry_text : "");
2290 gchar *sep = g_strstr_len(query, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
2291 if(!IS_NULL_PTR(sep)) *sep = '\0';
2292 g_strstrip(query);
2293 if(query[0] == '\0')
2294 {
2295 dt_free(query);
2296 return TRUE;
2297 }
2298
2299 gchar *query_ci = g_utf8_casefold(query, -1);
2300 if(query_ci[0] == '\0')
2301 {
2302 dt_free(query_ci);
2303 dt_free(query);
2304 return TRUE;
2305 }
2306
2307 gint best_rank = G_MAXINT;
2308 gint best_recent = G_MAXINT;
2309 gchar *best_command = NULL;
2310 gchar *best_display = NULL;
2311 gchar *best_query_ci = NULL;
2312 gchar *best_command_ci = NULL;
2313 const glong query_len = g_utf8_strlen(query_ci, -1);
2314
2315 GtkTreeIter iter;
2316 if(gtk_tree_model_get_iter_first(model, &iter))
2317 {
2318 do
2319 {
2320 gchar *row_query = NULL;
2321 gchar *row_command = NULL;
2322 gchar *row_display = NULL;
2323 gint row_recent = G_MAXINT;
2324 gtk_tree_model_get(model, &iter, 0, &row_query, 1, &row_command, 3, &row_display, 4, &row_recent, -1);
2325 if(IS_NULL_PTR(row_query))
2326 {
2327 dt_free(row_display);
2328 dt_free(row_command);
2329 dt_free(row_query);
2330 continue;
2331 }
2332
2333 gchar *row_query_ci = g_utf8_casefold(row_query, -1);
2334 gchar *row_command_ci = g_utf8_casefold(!IS_NULL_PTR(row_command) ? row_command : "", -1);
2335 gint row_rank = G_MAXINT;
2336 if(g_str_has_prefix(row_query_ci, query_ci))
2337 {
2338 // Prefer the shortest matching query for inline completion (ex: "exp" before "expo" for "ex").
2339 const glong row_query_len = g_utf8_strlen(row_query_ci, -1);
2340 row_rank = MAX((gint)(row_query_len - query_len), 0);
2341 }
2342 else if(g_str_has_prefix(row_command_ci, query_ci))
2343 {
2344 const glong row_command_len = g_utf8_strlen(row_command_ci, -1);
2345 row_rank = 100000 + MAX((gint)(row_command_len - query_len), 0);
2346 }
2347 else
2348 {
2349 const gchar *match_query = g_strstr_len(row_query_ci, -1, query_ci);
2350 if(!IS_NULL_PTR(match_query))
2351 row_rank = 200000 + (match_query - row_query_ci);
2352 else
2353 {
2354 const gchar *match_command = g_strstr_len(row_command_ci, -1, query_ci);
2355 if(!IS_NULL_PTR(match_command))
2356 row_rank = 300000 + (match_command - row_command_ci);
2357 }
2358 }
2359
2360 if(row_rank < best_rank
2361 || (row_rank == best_rank && row_recent < best_recent)
2362 || (row_rank == best_rank && row_recent == best_recent
2363 && (!IS_NULL_PTR(best_query_ci) && g_utf8_collate(row_query_ci, best_query_ci) < 0))
2364 || (row_rank == best_rank && row_recent == best_recent
2365 && !IS_NULL_PTR(best_query_ci) && g_utf8_collate(row_query_ci, best_query_ci) == 0
2366 && (!IS_NULL_PTR(best_command_ci) && g_utf8_collate(row_command_ci, best_command_ci) < 0)))
2367 {
2368 best_rank = row_rank;
2369 best_recent = row_recent;
2370 if(!IS_NULL_PTR(best_command)) dt_free(best_command);
2371 if(!IS_NULL_PTR(best_display)) dt_free(best_display);
2372 if(!IS_NULL_PTR(best_query_ci)) dt_free(best_query_ci);
2373 if(!IS_NULL_PTR(best_command_ci)) dt_free(best_command_ci);
2374 best_command = g_strdup(!IS_NULL_PTR(row_command) ? row_command : "");
2375 best_display = g_strdup(!IS_NULL_PTR(row_display) ? row_display : row_query);
2376 best_query_ci = g_strdup(row_query_ci);
2377 best_command_ci = g_strdup(row_command_ci);
2378 }
2379
2380 dt_free(row_command_ci);
2381 dt_free(row_query_ci);
2382 dt_free(row_display);
2383 dt_free(row_command);
2384 dt_free(row_query);
2385 } while(gtk_tree_model_iter_next(model, &iter));
2386 }
2387
2388 if(!IS_NULL_PTR(best_command) && best_command[0] != '\0' && best_rank < G_MAXINT)
2389 {
2390 if(!IS_NULL_PTR(state->preferred_command))
2391 {
2392 dt_free(state->preferred_command);
2393 state->preferred_command = NULL;
2394 }
2395 state->preferred_command = g_strdup(best_command);
2396
2397 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(state->tree_view));
2398 GtkTreeIter filter_iter;
2399 if(gtk_tree_model_get_iter_first(state->filter_model, &filter_iter))
2400 {
2401 do
2402 {
2403 dt_shortcut_t *shortcut = NULL;
2404 gchar *shortcut_path_display = NULL;
2405 gtk_tree_model_get(state->filter_model, &filter_iter, 1, &shortcut, 0, &shortcut_path_display, -1);
2406 gchar *shortcut_trimmed = NULL;
2407 if(!IS_NULL_PTR(shortcut) && !IS_NULL_PTR(shortcut->path))
2408 shortcut_trimmed = _shortcut_search_trim_display_path(shortcut->path);
2409 if(!IS_NULL_PTR(shortcut) && !IS_NULL_PTR(shortcut->path)
2410 && (!g_strcmp0(shortcut->path, best_command)
2411 || (!IS_NULL_PTR(shortcut_trimmed) && !g_strcmp0(shortcut_trimmed, best_command))
2412 || (!IS_NULL_PTR(shortcut_path_display) && !g_strcmp0(shortcut_path_display, best_command))))
2413 {
2414 GtkTreePath *path = gtk_tree_model_get_path(state->filter_model, &filter_iter);
2415 if(IS_NULL_PTR(path))
2416 {
2417 dt_free(shortcut_trimmed);
2418 dt_free(shortcut_path_display);
2419 break;
2420 }
2421 gtk_tree_selection_select_iter(selection, &filter_iter);
2422 gtk_tree_view_set_cursor(GTK_TREE_VIEW(state->tree_view), path, NULL, FALSE);
2423 gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(state->tree_view), path, NULL, FALSE, 0.f, 0.f);
2424 gtk_tree_path_free(path);
2425 gtk_tree_model_get(state->filter_model, &filter_iter, 1, &state->selected, -1);
2426 dt_free(shortcut_trimmed);
2427 dt_free(shortcut_path_display);
2428 break;
2429 }
2430 dt_free(shortcut_trimmed);
2431 dt_free(shortcut_path_display);
2432 } while(gtk_tree_model_iter_next(state->filter_model, &filter_iter));
2433 }
2434
2435 if(!IS_NULL_PTR(best_display) && best_display[0] != '\0')
2436 {
2437 const gchar *current = gtk_entry_get_text(GTK_ENTRY(state->search_entry));
2438 if(g_strcmp0(current, best_display))
2439 {
2440 gtk_entry_set_text(GTK_ENTRY(state->search_entry), best_display);
2441 }
2442 gtk_editable_set_position(GTK_EDITABLE(state->search_entry), g_utf8_strlen(query, -1));
2443 gtk_editable_select_region(GTK_EDITABLE(state->search_entry), g_utf8_strlen(query, -1), -1);
2444 dt_free(best_display);
2445 dt_free(best_command);
2446 dt_free(best_command_ci);
2447 dt_free(best_query_ci);
2448 dt_free(query_ci);
2449 dt_free(query);
2450 return TRUE;
2451 }
2452 }
2453
2454 dt_free(best_display);
2455 dt_free(best_command);
2456 dt_free(best_command_ci);
2457 dt_free(best_query_ci);
2458 dt_free(query_ci);
2459 dt_free(query);
2460 return FALSE;
2461}
2462
2463// fire action callbacks even when they don't have a keyboard shortcut defined
2464static gboolean _call_shortcut_cclosure(dt_shortcut_t *shortcut, GtkWindow *main_window, GClosure *closure)
2465{
2466 /*
2467 Accel callback signature is:
2468 `GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data`
2469 but `user_data` is handled in the closure already
2470 */
2471 GValue params[4] = { G_VALUE_INIT };
2472
2473 g_value_init(&params[0], G_TYPE_POINTER);
2474 g_value_set_pointer(&params[0], shortcut->accel_group);
2475
2476 g_value_init(&params[1], G_TYPE_POINTER);
2477 g_value_set_pointer(&params[1], G_OBJECT(main_window));
2478
2479 g_value_init(&params[2], G_TYPE_UINT);
2480 g_value_set_uint(&params[2], shortcut->key);
2481
2482 g_value_init(&params[3], G_TYPE_UINT);
2483 g_value_set_uint(&params[3], shortcut->mods);
2484
2485 GValue ret = G_VALUE_INIT;
2486 g_value_init (&ret, G_TYPE_BOOLEAN);
2487
2488 GClosure *active_closure = !IS_NULL_PTR(closure) ? closure : dt_shortcut_get_closure(shortcut);
2489 if(IS_NULL_PTR(active_closure))
2490 {
2491 for(int k = 0; k < 4; k++) g_value_unset(&params[k]);
2492 g_value_unset(&ret);
2493 return FALSE;
2494 }
2495
2496 g_closure_invoke(active_closure, &ret, 4, params, NULL);
2497 const gboolean handled = g_value_get_boolean(&ret);
2498
2499 for(int k = 0; k < 4; k++) g_value_unset(&params[k]);
2500 g_value_unset(&ret);
2501
2502 return handled;
2503}
2504
2506{
2507 // Re-resolve the shortcut from the live hashtable by path. The dispatch was
2508 // deferred, and between selection and now the shortcut may have been rebuilt or
2509 // freed (e.g. switching darkroom modules/views rebuilds the accel table). Using
2510 // a stored raw pointer here caused a use-after-free crash (reading a freed
2511 // GClosure while walking shortcut->closure).
2512 dt_shortcut_t *shortcut = NULL;
2513 if(!IS_NULL_PTR(state->accels) && !IS_NULL_PTR(state->accels->acceleratables) && !IS_NULL_PTR(state->path))
2514 shortcut = (dt_shortcut_t *)g_hash_table_lookup(state->accels->acceleratables, state->path);
2515
2516 if(IS_NULL_PTR(shortcut))
2517 {
2518 dt_print(DT_DEBUG_SHORTCUTS, "[accel_search] dispatch skipped: shortcut '%s' no longer exists\n",
2519 !IS_NULL_PTR(state->path) ? state->path : "<null>");
2520 return;
2521 }
2522
2523 PayloadClosure *payload = NULL;
2524 PayloadClosure *payload_in_main_window = NULL;
2525 if(!IS_NULL_PTR(shortcut->closure))
2526 {
2527 for(GList *item = g_list_last(shortcut->closure); item; item = g_list_previous(item))
2528 {
2529 PayloadClosure *candidate = (PayloadClosure *)item->data;
2530 if(IS_NULL_PTR(candidate) || IS_NULL_PTR(candidate->base)) continue;
2531 if(GTK_IS_WIDGET(candidate->base->data))
2532 {
2533 GtkWidget *candidate_widget = GTK_WIDGET(candidate->base->data);
2534 const gboolean in_main_window = IS_NULL_PTR(state->main_window)
2535 || gtk_widget_is_ancestor(candidate_widget, GTK_WIDGET(state->main_window));
2536 if(in_main_window && IS_NULL_PTR(payload_in_main_window))
2537 payload_in_main_window = candidate;
2538 if(in_main_window && gtk_widget_get_visible(candidate_widget) && gtk_widget_get_mapped(candidate_widget))
2539 {
2540 payload = candidate;
2541 break;
2542 }
2543 }
2544 }
2545 }
2546 if(IS_NULL_PTR(payload)) payload = payload_in_main_window;
2547 if(IS_NULL_PTR(payload)) payload = dt_shortcut_get_payload_closure(shortcut);
2548
2549 GtkWidget *target_widget = NULL;
2550 if(!IS_NULL_PTR(payload) && !IS_NULL_PTR(payload->base) && GTK_IS_WIDGET(payload->base->data))
2551 target_widget = GTK_WIDGET(payload->base->data);
2552 else if(!IS_NULL_PTR(shortcut->widget))
2553 target_widget = shortcut->widget;
2554
2555 // Keep module/control focus actions in their UI context.
2556 // Refocusing center here would move focus to the main view (thumbtable/center)
2557 // and can race with deferred control focus in action callbacks.
2558 if(IS_NULL_PTR(target_widget))
2560
2561 // The action we are about to invoke can destroy modules and free this very
2562 // shortcut and/or its target widget. Capture everything we still need afterwards
2563 // BEFORE invoking: a stable path (state->path is owned by the dispatch state and
2564 // outlives this call) and description for logging, plus weak pointers so a
2565 // destroyed widget reads back as NULL. After the invoke `shortcut` must be
2566 // treated as potentially dangling and never dereferenced again.
2567 const char *path = !IS_NULL_PTR(state->path) ? state->path : "<null>";
2568 gchar *desc = g_strdup(!IS_NULL_PTR(shortcut->description) ? shortcut->description : "<null>");
2569 GtkWidget *shortcut_widget = shortcut->widget;
2570 if(!IS_NULL_PTR(target_widget))
2571 g_object_add_weak_pointer(G_OBJECT(target_widget), (gpointer *)&target_widget);
2572 if(!IS_NULL_PTR(shortcut_widget))
2573 g_object_add_weak_pointer(G_OBJECT(shortcut_widget), (gpointer *)&shortcut_widget);
2574
2575 GClosure *closure = !IS_NULL_PTR(payload) ? payload->base : dt_shortcut_get_closure(shortcut);
2576 if(!IS_NULL_PTR(closure))
2577 {
2578 const gboolean handled = _call_shortcut_cclosure(shortcut, state->main_window, closure);
2580 "[accel_search] dispatch closure target='%s' description='%s' handled=%d\n",
2581 path, desc, handled);
2582 }
2583 else if(!IS_NULL_PTR(shortcut_widget))
2584 {
2585 const gboolean activated = gtk_widget_activate(shortcut_widget);
2587 "[accel_search] dispatch widget target='%s' description='%s' activated=%d widget=%s\n",
2588 path, desc, activated,
2589 !IS_NULL_PTR(shortcut_widget) ? gtk_widget_get_name(shortcut_widget) : "<destroyed>");
2590 }
2591 else
2592 {
2594 "[accel_search] dispatch failed: no callable target for '%s' description='%s'\n",
2595 path, desc);
2596 }
2597
2598 // From here on, do not dereference `shortcut`: it may have been freed by the
2599 // action above. target_widget / shortcut_widget are NULL if they were destroyed.
2600
2601 GtkWidget *focused_widget = NULL;
2602 if(!IS_NULL_PTR(state->main_window))
2603 focused_widget = gtk_window_get_focus(state->main_window);
2604 GtkWidget *scroll_focused_widget = !IS_NULL_PTR(darktable.gui) ? darktable.gui->has_scroll_focus : NULL;
2605
2606 gboolean target_focused_gtk = FALSE;
2607 gboolean target_focused_scroll = FALSE;
2608 if(!IS_NULL_PTR(target_widget))
2609 {
2610 if(!IS_NULL_PTR(focused_widget))
2611 {
2612 target_focused_gtk = focused_widget == target_widget
2613 || gtk_widget_is_ancestor(focused_widget, target_widget)
2614 || gtk_widget_is_ancestor(target_widget, focused_widget);
2615 }
2616 if(!IS_NULL_PTR(scroll_focused_widget))
2617 {
2618 target_focused_scroll = scroll_focused_widget == target_widget
2619 || gtk_widget_is_ancestor(scroll_focused_widget, target_widget)
2620 || gtk_widget_is_ancestor(target_widget, scroll_focused_widget);
2621 }
2622 }
2623 const gboolean target_focused = target_focused_gtk || target_focused_scroll;
2624
2626 "[accel_search] focus check (pre-idle) target='%s' target_widget=%s(%p) gtk_focus=%s(%p)"
2627 " scroll_focus=%s(%p) target_focused_gtk=%d target_focused_scroll=%d target_focused=%d\n",
2628 path,
2629 !IS_NULL_PTR(target_widget) ? gtk_widget_get_name(target_widget) : "<null>",
2630 (void *)target_widget,
2631 !IS_NULL_PTR(focused_widget) ? gtk_widget_get_name(focused_widget) : "<null>",
2632 (void *)focused_widget,
2633 !IS_NULL_PTR(scroll_focused_widget) ? gtk_widget_get_name(scroll_focused_widget) : "<null>",
2634 (void *)scroll_focused_widget,
2635 target_focused_gtk, target_focused_scroll, target_focused);
2636
2637 // First dispatch can still hit an outdated control instance while module tabs
2638 // are being switched/rebuilt. Retry once shortly after for Bauhaus controls.
2639 if(!target_focused && !IS_NULL_PTR(target_widget) && state->retries < 1
2640 && !g_strcmp0(G_OBJECT_TYPE_NAME(target_widget), "DtBauhausWidget"))
2641 {
2642 dt_accels_dispatch_state_t *retry = g_malloc0(sizeof(*retry));
2643 retry->path = g_strdup(path);
2644 retry->accels = state->accels;
2645 retry->main_window = state->main_window;
2646 retry->retries = state->retries + 1;
2648 "[accel_search] dispatch retry scheduled target='%s' retry=%u\n",
2649 path, retry->retries);
2650 g_timeout_add_full(G_PRIORITY_DEFAULT, DT_ACCEL_SEARCH_DISPATCH_RETRY_DELAY_MS,
2652 }
2653
2654 // Release the weak pointers (no-op if the widget was already destroyed and the
2655 // pointer NULLed) and the captured description.
2656 if(!IS_NULL_PTR(target_widget))
2657 g_object_remove_weak_pointer(G_OBJECT(target_widget), (gpointer *)&target_widget);
2658 if(!IS_NULL_PTR(shortcut_widget))
2659 g_object_remove_weak_pointer(G_OBJECT(shortcut_widget), (gpointer *)&shortcut_widget);
2660 g_free(desc);
2661}
2662
2663static gboolean _dispatch_selected_shortcut_idle(gpointer data)
2664{
2667 g_free(state->path);
2668 dt_free(state);
2669 return G_SOURCE_REMOVE;
2670}
2671
2672static gboolean _shortcut_search_visible(GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
2673{
2674 int rank = -1;
2675 gtk_tree_model_get(model, iter, 2, &rank, -1);
2676 return rank >= 0;
2677}
2678
2679static gboolean _shortcut_search_recent_completion_match(GtkEntryCompletion *completion, const gchar *key,
2680 GtkTreeIter *iter, gpointer user_data)
2681{
2682 if(IS_NULL_PTR(key) || key[0] == '\0') return FALSE;
2683
2684 gchar *key_query = g_strdup(key);
2685 gchar *key_sep = g_strstr_len(key_query, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
2686 if(!IS_NULL_PTR(key_sep)) *key_sep = '\0';
2687 g_strstrip(key_query);
2688 if(key_query[0] == '\0')
2689 {
2690 dt_free(key_query);
2691 return FALSE;
2692 }
2693
2694 GtkTreeModel *model = gtk_entry_completion_get_model(completion);
2695 if(IS_NULL_PTR(model))
2696 {
2697 dt_free(key_query);
2698 return FALSE;
2699 }
2700
2701 gchar *query = NULL;
2702 gtk_tree_model_get(model, iter, 0, &query, -1);
2703 if(IS_NULL_PTR(query))
2704 {
2705 dt_free(key_query);
2706 return FALSE;
2707 }
2708
2709 gchar *key_ci = g_utf8_casefold(key_query, -1);
2710 gchar *query_ci = g_utf8_casefold(query, -1);
2711 const gboolean match = !IS_NULL_PTR(g_strrstr(query_ci, key_ci));
2712
2713 dt_free(query_ci);
2714 dt_free(key_ci);
2715 dt_free(key_query);
2716 dt_free(query);
2717 return match;
2718}
2719
2722{
2723 const gchar *query_text = gtk_entry_get_text(GTK_ENTRY(state->search_entry));
2724 gchar *query = g_strdup(!IS_NULL_PTR(query_text) ? query_text : "");
2725 gchar *query_sep = g_strstr_len(query, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
2726 if(!IS_NULL_PTR(query_sep)) *query_sep = '\0';
2727 g_strstrip(query);
2729 "[accel_search] validate query='%s' shortcut='%s' description='%s'\n",
2730 query,
2731 !IS_NULL_PTR(shortcut) && !IS_NULL_PTR(shortcut->path) ? shortcut->path : "<null>",
2732 !IS_NULL_PTR(shortcut) && !IS_NULL_PTR(shortcut->description) ? shortcut->description : "<null>");
2733 _shortcut_search_save_recent_entry(query, shortcut);
2734 dt_free(query);
2735 state->selected = shortcut;
2736 state->response = GTK_RESPONSE_ACCEPT;
2737 gtk_widget_destroy(window);
2738 return TRUE;
2739}
2740
2741static void _shortcut_search_selection_changed(GtkTreeSelection *selection, gpointer user_data)
2742{
2744 GtkTreeIter iter;
2745 if(gtk_tree_selection_get_selected(selection, NULL, &iter))
2746 {
2747 gtk_tree_model_get(state->filter_model, &iter, 1, &state->selected, -1);
2748 }
2749 else
2750 {
2751 state->selected = NULL;
2752 }
2753}
2754
2755static gboolean _shortcut_search_row_activated(GtkTreeView *tree_view, GtkTreePath *path,
2756 GtkTreeViewColumn *column, gpointer user_data)
2757{
2759 GtkTreeIter iter;
2760 if(!gtk_tree_model_get_iter(state->filter_model, &iter, path)) return FALSE;
2761
2762 dt_shortcut_t *shortcut = NULL;
2763 gtk_tree_model_get(state->filter_model, &iter, 1, &shortcut, -1);
2764 return _queue_action_from_shortcut(shortcut, state->window, state);
2765}
2766
2767static gboolean _shortcut_search_move_selection(dt_accels_search_state_t *state, const gboolean forward)
2768{
2769 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(state->tree_view));
2770 GtkTreeIter iter;
2771 if(!gtk_tree_selection_get_selected(selection, NULL, &iter))
2772 {
2773 if(!gtk_tree_model_get_iter_first(state->filter_model, &iter)) return TRUE;
2774 }
2775 else if(forward)
2776 {
2777 if(!gtk_tree_model_iter_next(state->filter_model, &iter)) return TRUE;
2778 }
2779 else
2780 {
2781 GtkTreePath *path = gtk_tree_model_get_path(state->filter_model, &iter);
2782 if(IS_NULL_PTR(path)) return TRUE;
2783 if(!gtk_tree_path_prev(path))
2784 {
2785 gtk_tree_path_free(path);
2786 return TRUE;
2787 }
2788
2789 if(!gtk_tree_model_get_iter(state->filter_model, &iter, path))
2790 {
2791 gtk_tree_path_free(path);
2792 return TRUE;
2793 }
2794 gtk_tree_path_free(path);
2795 }
2796
2797 GtkTreePath *path = gtk_tree_model_get_path(state->filter_model, &iter);
2798 if(IS_NULL_PTR(path)) return TRUE;
2799 gtk_tree_selection_select_iter(selection, &iter);
2800 gtk_tree_view_set_cursor(GTK_TREE_VIEW(state->tree_view), path, NULL, FALSE);
2801 gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(state->tree_view), path, NULL, FALSE, 0.f, 0.f);
2802 gtk_tree_path_free(path);
2803 gtk_tree_model_get(state->filter_model, &iter, 1, &state->selected, -1);
2804 return TRUE;
2805}
2806
2807static gboolean _search_entry_restore_space_idle(gpointer user_data)
2808{
2809 // Run after GTK key processing/completion so we can enforce "<typed query> + space".
2812 if(IS_NULL_PTR(state->search_entry) || IS_NULL_PTR(state->pending_space_query))
2813 return G_SOURCE_REMOVE;
2814
2815 gchar *with_space = g_strconcat(state->pending_space_query, " ", NULL);
2816 gtk_entry_set_text(GTK_ENTRY(state->search_entry), with_space);
2817 gtk_editable_set_position(GTK_EDITABLE(state->search_entry), -1);
2818 dt_free(with_space);
2819 dt_free(state->pending_space_query);
2820 state->pending_space_query = NULL;
2821 return G_SOURCE_REMOVE;
2822}
2823
2824static gboolean _search_entry_key_pressed(GtkWidget *widget __attribute__((unused)),
2825 GdkEventKey *event, gpointer user_data)
2826{
2828 guint key = dt_keys_mainpad_alternatives(event->keyval);
2829
2830 if(key == GDK_KEY_Escape)
2831 {
2832 state->response = GTK_RESPONSE_CANCEL;
2833 gtk_widget_destroy(state->window);
2834 return TRUE;
2835 }
2836
2837 if(key == GDK_KEY_Down)
2839 if(key == GDK_KEY_Up)
2841 if(key == GDK_KEY_Return)
2842 {
2843 dt_shortcut_t *shortcut = state->selected;
2844 gchar *command = NULL;
2845 if(IS_NULL_PTR(shortcut))
2846 {
2847 const gchar *entry_text = gtk_entry_get_text(GTK_ENTRY(state->search_entry));
2848 if(!IS_NULL_PTR(entry_text) && entry_text[0] != '\0')
2849 {
2850 const gchar *sep = g_strstr_len(entry_text, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
2851 command = !IS_NULL_PTR(sep)
2852 ? g_strdup(sep + strlen(DT_ACCEL_SEARCH_INLINE_SEPARATOR))
2853 : g_strdup(entry_text);
2854 }
2855
2856 if(!IS_NULL_PTR(command))
2857 {
2858 g_strstrip(command);
2859 if(command[0] != '\0')
2860 {
2861 GtkTreeIter iter;
2862 if(gtk_tree_model_get_iter_first(GTK_TREE_MODEL(state->store), &iter))
2863 {
2864 do
2865 {
2866 dt_shortcut_t *candidate = NULL;
2867 gchar *candidate_path = NULL;
2868 gtk_tree_model_get(GTK_TREE_MODEL(state->store), &iter, 1, &candidate, 0, &candidate_path, -1);
2869 gchar *candidate_display_path = NULL;
2870 if(!IS_NULL_PTR(candidate) && !IS_NULL_PTR(candidate->path))
2871 candidate_display_path = _shortcut_search_trim_display_path(candidate->path);
2872 if(!IS_NULL_PTR(candidate) && !IS_NULL_PTR(candidate->path)
2873 && (!g_strcmp0(command, candidate->path)
2874 || !g_strcmp0(command, candidate_path)
2875 || (!IS_NULL_PTR(candidate_display_path)
2876 && !g_strcmp0(command, candidate_display_path))))
2877 {
2878 shortcut = candidate;
2879 dt_free(candidate_path);
2880 dt_free(candidate_display_path);
2881 break;
2882 }
2883 dt_free(candidate_path);
2884 dt_free(candidate_display_path);
2885 } while(gtk_tree_model_iter_next(GTK_TREE_MODEL(state->store), &iter));
2886 }
2887 }
2888 dt_free(command);
2889 }
2890 }
2891
2892 if(!IS_NULL_PTR(shortcut))
2893 return _queue_action_from_shortcut(shortcut, state->window, state);
2894 return TRUE;
2895 }
2896
2897 if(key == GDK_KEY_space)
2898 {
2899 // Hack: GTK entry completion can be too aggressive here and may treat Space as
2900 // suggestion acceptance. Restore "<typed query> + space" in idle to preserve
2901 // user input semantics for multi-term search.
2902
2903 const gchar *entry_text = gtk_entry_get_text(GTK_ENTRY(state->search_entry));
2904 if(!IS_NULL_PTR(entry_text))
2905 {
2906 gchar *query_only = NULL;
2907 gint sel_start = 0, sel_end = 0;
2908 const gboolean has_selection = gtk_editable_get_selection_bounds(GTK_EDITABLE(state->search_entry),
2909 &sel_start, &sel_end);
2910 const gint cursor_chars = has_selection ? sel_start
2911 : gtk_editable_get_position(GTK_EDITABLE(state->search_entry));
2912 const gint text_chars = g_utf8_strlen(entry_text, -1);
2913 if(cursor_chars >= 0 && cursor_chars <= text_chars)
2914 query_only = g_utf8_substring(entry_text, 0, cursor_chars);
2915 else
2916 query_only = g_strdup(entry_text);
2917
2918 const gchar *sep = g_strstr_len(query_only, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
2919 if(!IS_NULL_PTR(sep))
2920 *((gchar *)sep) = '\0';
2921
2922 if(!IS_NULL_PTR(state->pending_space_query)) dt_free(state->pending_space_query);
2923 state->pending_space_query = query_only;
2924 }
2925
2926 if(state->pending_space_idle_id != 0) g_source_remove(state->pending_space_idle_id);
2927 state->pending_space_idle_id = g_idle_add(_search_entry_restore_space_idle, state);
2928 return TRUE;
2929 }
2930
2931 gunichar key_char = gdk_keyval_to_unicode(event->keyval);
2932 const gboolean is_alnum = key_char != 0 && g_unichar_isalnum(key_char);
2933 if(!is_alnum)
2934 {
2935 // Non-alphanumeric keys (space, punctuation, etc.) should not trigger inline completion insertion.
2936 // Mark one completion cycle as suppressed so GTK inserts the typed key in the entry instead.
2937 state->suppress_inline_once = TRUE;
2938
2939 const gchar *entry_text = gtk_entry_get_text(GTK_ENTRY(state->search_entry));
2940 if(!IS_NULL_PTR(entry_text))
2941 {
2942 const gchar *sep = g_strstr_len(entry_text, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
2943 if(!IS_NULL_PTR(sep))
2944 {
2945 const gint position = gtk_editable_get_position(GTK_EDITABLE(state->search_entry));
2946 const gsize query_len = sep - entry_text;
2947 gchar *query_only = g_strndup(entry_text, query_len);
2948
2949 gtk_entry_set_text(GTK_ENTRY(state->search_entry), query_only);
2950 gtk_editable_set_position(GTK_EDITABLE(state->search_entry), MIN(position, (gint)query_len));
2951 dt_free(query_only);
2952 return FALSE;
2953 }
2954 }
2955 }
2956 return FALSE;
2957}
2958
2959static gboolean _search_entry_button_pressed(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2960{
2962 if(event->button != 1) return FALSE;
2963 if(IS_NULL_PTR(state) || IS_NULL_PTR(state->search_entry)) return FALSE;
2964
2965 const gchar *entry_text = gtk_entry_get_text(GTK_ENTRY(state->search_entry));
2966 if(IS_NULL_PTR(entry_text)) return FALSE;
2967
2968 const gchar *sep = g_strstr_len(entry_text, -1, DT_ACCEL_SEARCH_INLINE_SEPARATOR);
2969 if(IS_NULL_PTR(sep)) return FALSE;
2970
2971 const gsize query_len = sep - entry_text;
2972 gchar *query = g_strndup(entry_text, query_len);
2973 state->suppress_inline_once = TRUE;
2974 gtk_entry_set_text(GTK_ENTRY(state->search_entry), query);
2975 gtk_editable_set_position(GTK_EDITABLE(state->search_entry), -1);
2976 dt_free(query);
2977 return FALSE;
2978}
2979
2980static gboolean _shortcut_search_window_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
2981{
2982 return _search_entry_key_pressed(widget, event, user_data);
2983}
2984
2985static gboolean _shortcut_search_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2986{
2988 GtkAllocation allocation = { 0 };
2989 gtk_widget_get_allocation(widget, &allocation);
2990
2991 gboolean click_inside = FALSE;
2992 GdkWindow *win = gtk_widget_get_window(widget);
2993 if(!IS_NULL_PTR(win))
2994 {
2995 gint wx = 0, wy = 0;
2996 gdk_window_get_origin(win, &wx, &wy);
2997 const gdouble x0 = (gdouble)wx;
2998 const gdouble y0 = (gdouble)wy;
2999 const gdouble x1 = x0 + (gdouble)allocation.width;
3000 const gdouble y1 = y0 + (gdouble)allocation.height;
3001 click_inside = (event->x_root >= x0 && event->x_root < x1
3002 && event->y_root >= y0 && event->y_root < y1);
3003 }
3004 else
3005 {
3006 click_inside = (event->x >= 0.0 && event->x < allocation.width
3007 && event->y >= 0.0 && event->y < allocation.height);
3008 }
3009
3010 if(click_inside) return FALSE;
3011
3012 state->response = GTK_RESPONSE_CANCEL;
3013 gtk_widget_destroy(widget);
3014 return TRUE;
3015}
3016
3017static void _shortcut_search_destroy(GtkWidget *widget, gpointer user_data)
3018{
3020 if(state->pending_space_idle_id != 0)
3021 {
3022 g_source_remove(state->pending_space_idle_id);
3023 state->pending_space_idle_id = 0;
3024 }
3025 if(!IS_NULL_PTR(state->pending_space_query))
3026 {
3027 dt_free(state->pending_space_query);
3028 state->pending_space_query = NULL;
3029 }
3030 gtk_grab_remove(widget);
3031 if(state->window == widget) state->window = NULL;
3032 if(!IS_NULL_PTR(state->loop)) g_main_loop_quit(state->loop);
3033}
3034
3035void dt_accels_search(dt_accels_t *accels, GtkWindow *main_window, GtkWidget *anchor)
3036{
3037 GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
3038 gtk_window_set_title(GTK_WINDOW(window), _("Ansel - Search accelerators"));
3039
3040#ifdef GDK_WINDOWING_QUARTZ
3042#endif
3043
3044 const int dialog_width = 800;
3045 const int dialog_height = 0;
3046
3047 gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
3048 gtk_window_set_modal(GTK_WINDOW(window), FALSE);
3049 gtk_window_set_transient_for(GTK_WINDOW(window), main_window);
3050 gtk_window_set_attached_to(GTK_WINDOW(window), GTK_WIDGET(main_window));
3051 gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
3052 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(window), TRUE);
3053 gtk_window_set_skip_pager_hint(GTK_WINDOW(window), TRUE);
3054 gtk_window_set_accept_focus(GTK_WINDOW(window), TRUE);
3055 gtk_window_set_focus_on_map(GTK_WINDOW(window), TRUE);
3056 gtk_window_set_default_size(GTK_WINDOW(window), dialog_width, dialog_height);
3057 gtk_widget_set_name(window, "shortcut-search-dialog");
3058 gtk_widget_add_events(window, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
3059
3060 // Build the list of currently-relevant shortcut pathes
3061 GtkListStore *store = gtk_list_store_new(7, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT);
3062 g_hash_table_foreach(accels->acceleratables, _for_each_path_create_treeview_row, store);
3063
3064 GMainLoop *loop = g_main_loop_new(NULL, FALSE);
3066 .store = store,
3067 .filter_model = NULL,
3068 .recent_entries = NULL,
3069 .main_window = main_window,
3070 .search_entry = NULL,
3071 .tree_view = NULL,
3072 .window = window,
3073 .loop = loop,
3074 .response = GTK_RESPONSE_CANCEL,
3075 .selected = NULL,
3076 .preferred_command = NULL,
3077 .suppress_inline_once = FALSE,
3078 .pending_space_query = NULL,
3079 .pending_space_idle_id = 0
3080 };
3081
3082 // Sort the filtered model by relevance
3083 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(store), 2,
3084 (GtkTreeIterCompareFunc)_sort_model_by_relevance_func, NULL, NULL);
3085 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store), 2, GTK_SORT_ASCENDING);
3086
3087 // Build the search entry
3088 GtkWidget *search_entry = gtk_search_entry_new();
3089 state.search_entry = search_entry;
3090 GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
3091 gtk_container_add(GTK_CONTAINER(window), box);
3092 GtkWidget *search_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
3093 gtk_box_pack_start(GTK_BOX(box), search_row, TRUE, TRUE, 0);
3094 gtk_box_pack_start(GTK_BOX(search_row), search_entry, TRUE, TRUE, 0);
3095
3096 GtkTreeModel *filter_model = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL);
3097 state.filter_model = filter_model;
3098 gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filter_model),
3099 _shortcut_search_visible, NULL, NULL);
3100
3101 GtkEntryCompletion *completion = gtk_entry_completion_new();
3102 GtkListStore *recent_entries = gtk_list_store_new(5, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
3103 G_TYPE_INT);
3104 state.recent_entries = recent_entries;
3106 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(recent_entries), 4,
3107 (GtkTreeIterCompareFunc)_shortcut_search_recent_sort_func, &state, NULL);
3108 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(recent_entries), 4, GTK_SORT_ASCENDING);
3109 gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(recent_entries));
3110 gtk_entry_completion_set_text_column(completion, 3);
3111 gtk_entry_completion_set_inline_completion(completion, TRUE);
3112 gtk_entry_completion_set_inline_selection(completion, TRUE);
3113 gtk_entry_completion_set_popup_completion(completion, FALSE);
3114 gtk_entry_completion_set_match_func(completion, _shortcut_search_recent_completion_match, NULL, NULL);
3115 gtk_entry_set_completion(GTK_ENTRY(search_entry), completion);
3116 g_object_unref(completion);
3117
3118 GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL);
3119 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
3120 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
3121 gtk_widget_set_size_request(scrolled, dialog_width, 320);
3122 dt_gui_add_class(scrolled, "dt_recessed_scroll");
3123 gtk_box_pack_start(GTK_BOX(box), scrolled, TRUE, TRUE, 0);
3124
3125 GtkWidget *tree_view = gtk_tree_view_new_with_model(filter_model);
3126 state.tree_view = tree_view;
3127 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), FALSE);
3128 gtk_tree_view_set_enable_search(GTK_TREE_VIEW(tree_view), FALSE);
3129 gtk_tree_view_set_hover_selection(GTK_TREE_VIEW(tree_view), FALSE);
3130 gtk_tree_view_set_activate_on_single_click(GTK_TREE_VIEW(tree_view), TRUE);
3131 gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(tree_view), 0);
3132 gtk_widget_set_hexpand(tree_view, TRUE);
3133 gtk_widget_set_vexpand(tree_view, TRUE);
3134 gtk_container_add(GTK_CONTAINER(scrolled), tree_view);
3135
3136 GtkCellRenderer *txt = gtk_cell_renderer_text_new();
3137 g_object_set(txt, "ellipsize", PANGO_ELLIPSIZE_END, "ellipsize-set", TRUE, "max-width-chars", 70, NULL);
3138 GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(NULL, txt, "text", 0, NULL);
3139 gtk_tree_view_column_set_expand(column, FALSE);
3140 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
3141 gtk_tree_view_column_set_min_width(column, 360);
3142 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
3143
3144 GtkCellRenderer *accel = gtk_cell_renderer_accel_new();
3145 g_object_set(accel, "editable", FALSE, "accel-mode", GTK_CELL_RENDERER_ACCEL_MODE_OTHER, NULL);
3146 column = gtk_tree_view_column_new_with_attributes(NULL, accel, "accel-key", 5, "accel-mods", 6, NULL);
3147 gtk_tree_view_column_set_min_width(column, 140);
3148 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
3149
3150 GtkCellRenderer *description = gtk_cell_renderer_text_new();
3151 g_object_set(description, "ellipsize", PANGO_ELLIPSIZE_END, "ellipsize-set", TRUE, NULL);
3152 column = gtk_tree_view_column_new_with_attributes(NULL, description, "text", 3, NULL);
3153 gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
3154 gtk_tree_view_column_set_min_width(column, 280);
3155 gtk_tree_view_column_set_expand(column, TRUE);
3156 gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), column);
3157
3158 GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view));
3159 gtk_tree_selection_set_mode(selection, GTK_SELECTION_BROWSE);
3160
3161 // Wire callbacks
3162 g_signal_connect(G_OBJECT(search_entry), "changed", G_CALLBACK(_search_entry_changed), &state);
3163 g_signal_connect(G_OBJECT(completion), "match-selected", G_CALLBACK(_shortcut_search_recent_match_selected), &state);
3164 g_signal_connect(G_OBJECT(completion), "insert-prefix", G_CALLBACK(_shortcut_search_recent_insert_prefix), &state);
3165 g_signal_connect(G_OBJECT(search_entry), "button-press-event", G_CALLBACK(_search_entry_button_pressed), &state);
3166 g_signal_connect(G_OBJECT(search_entry), "key-press-event", G_CALLBACK(_search_entry_key_pressed), &state);
3167 g_signal_connect(G_OBJECT(selection), "changed", G_CALLBACK(_shortcut_search_selection_changed), &state);
3168 g_signal_connect(G_OBJECT(tree_view), "row-activated", G_CALLBACK(_shortcut_search_row_activated), &state);
3169 g_signal_connect(G_OBJECT(window), "key-press-event", G_CALLBACK(_shortcut_search_window_key_pressed), &state);
3170 g_signal_connect(G_OBJECT(window), "button-press-event", G_CALLBACK(_shortcut_search_button_press), &state);
3171 g_signal_connect(G_OBJECT(window), "button-release-event", G_CALLBACK(_shortcut_search_button_press), &state);
3172 g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(_shortcut_search_destroy), &state);
3173
3175
3176 // Center horizontally against the current Ansel window position.
3177 GtkAllocation main_alloc = { 0 };
3178 gtk_widget_get_allocation(GTK_WIDGET(main_window), &main_alloc);
3179 gint main_x = 0, main_y = 0;
3180 GdkWindow *main_gdk_window = gtk_widget_get_window(GTK_WIDGET(main_window));
3181 if(!IS_NULL_PTR(main_gdk_window))
3182 gdk_window_get_origin(main_gdk_window, &main_x, &main_y);
3183 else
3184 gtk_window_get_position(main_window, &main_x, &main_y);
3185
3186 gint top_panel_height = 0;
3188 {
3190 if(!IS_NULL_PTR(top_panel) && gtk_widget_get_visible(top_panel))
3191 top_panel_height = gtk_widget_get_allocated_height(top_panel);
3192 }
3193
3194 const gint window_x = main_x + MAX((main_alloc.width - dialog_width) / 2, 0);
3195 const gint window_y = main_y + MAX(top_panel_height, 0);
3196 gtk_window_move(GTK_WINDOW(window), window_x, window_y);
3197
3198 gtk_widget_realize(window);
3199 gtk_widget_show_all(window);
3200 gtk_grab_add(window);
3201 gdk_window_focus(gtk_widget_get_window(window), GDK_CURRENT_TIME);
3202 gtk_window_set_focus(GTK_WINDOW(window), search_entry);
3203 gtk_widget_grab_focus(search_entry);
3204
3205 g_main_loop_run(loop);
3206 g_main_loop_unref(loop);
3207 if(state.response == GTK_RESPONSE_ACCEPT && !IS_NULL_PTR(state.selected))
3208 {
3209 dt_accels_dispatch_state_t *dispatch = g_malloc0(sizeof(*dispatch));
3210 dispatch->path = g_strdup(state.selected->path);
3211 dispatch->accels = !IS_NULL_PTR(state.selected->accels)
3212 ? state.selected->accels
3213 : (!IS_NULL_PTR(darktable.gui) ? darktable.gui->accels : NULL);
3214 dispatch->main_window = main_window;
3215 g_idle_add(_dispatch_selected_shortcut_idle, dispatch);
3216 }
3217 if(!IS_NULL_PTR(state.window)) gtk_widget_destroy(state.window);
3218 if(!IS_NULL_PTR(state.preferred_command)) dt_free(state.preferred_command);
3219 if(!IS_NULL_PTR(state.recent_entries)) g_object_unref(state.recent_entries);
3220 g_object_unref(store);
3221}
static dt_shortcut_t * _find_non_virtual_shortcut(dt_accels_t *accels, GtkAccelGroup *group, guint keyval, GdkModifierType mods)
static dt_accels_t * accels_global_ref
void dt_shortcut_remove_closure(dt_shortcut_t *shortcut, gpointer data)
static void _shortcut_search_load_recent_entries(GtkListStore *store)
static gboolean _search_entry_restore_space_idle(gpointer user_data)
static void _remove_generic_accel(dt_shortcut_t *shortcut)
void dt_accels_connect_accels(dt_accels_t *accels)
Actually enable accelerators after having loaded user config.
void dt_shortcut_set_closure(dt_shortcut_t *shortcut, gboolean(*action_callback)(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data), gpointer data)
static void _shortcut_set_widget_data(GtkWidget *widget, dt_shortcut_t *shortcut)
@ COL_KEYS
@ COL_SHORTCUT
@ COL_CLEAR
@ COL_NAME
@ COL_MODS
@ NUM_COLUMNS
@ COL_DESCRIPTION
@ COL_KEYVAL
@ COL_PATH
static gchar * _shortcut_search_trim_display_path(const gchar *path)
static void _add_generic_accel(dt_shortcut_t *shortcut, GtkAccelFlags flags)
static void _for_each_non_virtual_accel(gpointer key, gpointer value, gpointer user_data)
void dt_accels_connect_active_group(dt_accels_t *accels, const gchar *group)
Connect the contextual active accels group to the window. Views can declare their own set of contextu...
static void _make_column_editable(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
static gboolean _shortcut_search_visible(GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
void dt_accels_disconnect_active_group(dt_accels_t *accels)
Disconnect the contextual active accels group from the window.
void _for_each_path_create_treeview_row(gpointer key, gpointer value, gpointer user_data)
static gboolean _virtual_shortcut_callback(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data)
gboolean dt_accels_dispatch(GtkWidget *w, GdkEvent *event, gpointer user_data)
Force our listener for all key strokes to bypass reserved Gtk keys.
static void _shortcut_search_selection_changed(GtkTreeSelection *selection, gpointer user_data)
#define DT_ACCEL_SEARCH_RECENT_MAX
static void _insert_accel(dt_accels_t *accels, dt_shortcut_t *shortcut)
static gboolean _shortcut_search_recent_insert_prefix(GtkEntryCompletion *completion, gchar *prefix, gpointer user_data)
void dt_accels_remove_shortcut(dt_accels_t *accels, const char *path)
Remove the shortcut object identified by path and all its accels.
PayloadClosure * dt_shortcut_get_payload_closure(dt_shortcut_t *shortcut)
static gboolean _shortcut_search_move_selection(dt_accels_search_state_t *state, const gboolean forward)
static void _insert_parent_data_into_children(dt_shortcut_t *shortcut)
static void _shortcut_cleared(GtkCellRendererAccel *renderer, const gchar *path_string, gpointer user_data)
dt_accels_t * dt_accels_init(char *config_file, GtkAccelFlags flags)
static gint _sort_model_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
static void _dispatch_selected_shortcut(dt_accels_dispatch_state_t *state)
static gint _shortcut_search_recent_sort_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
static guint _normalize_keyval(const guint keyval)
static gboolean _shortcut_search_recent_match_selected(GtkEntryCompletion *completion, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
static void _accels_keys_decode(dt_accels_t *accels, GdkEvent *event, guint *keyval, GdkModifierType *mods)
static gboolean _search_entry_button_pressed(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
static void _find_and_rank_matches(GtkTreeModel *model, GtkWidget *search_entry)
static const char * _find_path_for_keys(dt_accels_t *accels, guint key, GdkModifierType modifier, GtkAccelGroup *group)
static gboolean _update_shortcut_state(dt_shortcut_t *shortcut, GtkAccelKey *key, gboolean init)
static void _remove_widget_accel(dt_shortcut_t *shortcut, const GtkAccelKey *old_key)
static void _connect_accel(dt_shortcut_t *shortcut)
static gboolean _icon_activate(GtkCellRenderer *cell, GdkEvent *event, GtkWidget *treeview, const gchar *path_str, GdkRectangle *background, GdkRectangle *cell_area, GtkCellRendererState flags, gpointer user_data)
static void _g_list_closure_unref(gpointer data)
static void _create_main_row(GtkTreeStore *store, GtkTreeIter *iter, const char *label, const char *path, dt_shortcut_t *shortcut)
static gboolean _shortcut_search_recent_completion_match(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter, gpointer user_data)
static gboolean _call_shortcut_cclosure(dt_shortcut_t *shortcut, GtkWindow *main_window, GClosure *closure)
static void _shortcut_search_save_recent_entry(const char *query, const dt_shortcut_t *shortcut)
static int _match_text(GtkTreeModel *model, GtkTreeIter *iter, const char *needle)
static void _make_column_clearable(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
static void _remove_accel_hashtable(gpointer _key, gpointer value, gpointer user_data)
static void _find_parent_hashtable(gpointer _key, gpointer value, gpointer user_data)
void _for_each_accel_create_treeview_row(gpointer key, gpointer value, gpointer user_data)
static void _shortcut_search_destroy(GtkWidget *widget, gpointer user_data)
#define DT_ACCEL_SEARCH_INLINE_SEPARATOR
void dt_accels_cleanup(dt_accels_t *accels)
#define DT_ACCEL_SEARCH_RECENT_KEY
static void search_changed(GtkEntry *entry, gpointer user_data)
static gboolean _dispatch_selected_shortcut_idle(gpointer data)
static void _for_each_accel(gpointer key, gpointer value, gpointer user_data)
static gint _sort_model_by_relevance_func(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer data)
static gboolean _accels_tooltip_query_hook(GSignalInvocationHint *hint, guint n_param_values, const GValue *param_values, gpointer data)
void dt_accels_search(dt_accels_t *accels, GtkWindow *main_window, GtkWidget *anchor)
static gboolean _search_entry_key_pressed(GtkWidget *widget __attribute__((unused)), GdkEventKey *event, gpointer user_data)
static int guess_key_group(dt_accels_t *accels, guint keyval, guint hardware_keycode)
void dt_accels_window(dt_accels_t *accels, GtkWindow *main_window)
Show the modal dialog listing all available keyboard shortcuts and letting user to set them.
static gboolean filter_callback(GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
static gboolean _widget_shortcut_callback(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data)
void dt_accels_new_virtual_shortcut(dt_accels_t *accels, GtkAccelGroup *accel_group, const gchar *accel_path, GtkWidget *widget, guint key_val, GdkModifierType accel_mods)
Add a new virtual shortcut. Virtual shortcuts are immutable, read-only and don't trigger any action....
#define DT_ACCEL_SEARCH_DISPATCH_RETRY_DELAY_MS
static void _shortcut_edited(GtkCellRenderer *cell, const gchar *path_string, guint key, GdkModifierType mods, guint hardware_key, gpointer user_data)
static void _accels_install_tooltip_hook(void)
void dt_accels_new_virtual_instance_shortcut(dt_accels_t *accels, gboolean(*action_callback)(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data), gpointer data, GtkAccelGroup *accel_group, const gchar *action_scope, const gchar *action_name)
gchar * dt_accels_build_path(const gchar *scope, const gchar *feature)
GClosure * dt_shortcut_get_closure(dt_shortcut_t *shortcut)
static gboolean _shortcut_search_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data)
void dt_accels_remove_accel(dt_accels_t *accels, const char *path, gpointer data)
Recursively remove all accels for all shortcuts containing path. This is unneeded for accels attached...
void dt_accels_load_user_config(dt_accels_t *accels)
Loads keyboardrc.lang from config dir. This needs to run after we inited the accel map from widgets c...
static gboolean _shortcut_search_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
void dt_accels_new_widget_shortcut(dt_accels_t *accels, GtkWidget *widget, const gchar *signal, GtkAccelGroup *accel_group, const gchar *accel_path, guint key_val, GdkModifierType accel_mods, const gboolean lock)
Register a new shortcut for a widget, setting up its path, default keys and accel group....
void dt_accels_new_action_shortcut(dt_accels_t *accels, gboolean(*action_callback)(GtkAccelGroup *group, GObject *acceleratable, guint keyval, GdkModifierType mods, gpointer user_data), gpointer data, GtkAccelGroup *accel_group, const gchar *action_scope, const gchar *action_name, guint key_val, GdkModifierType accel_mods, const gboolean lock, const char *description)
Register a new shortcut for a generic action, setting up its path, default keys and accel group....
static gboolean _shortcut_search_window_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
static void _connect_accel_hashtable(gpointer _key, gpointer value, gpointer user_data)
static void _clean_shortcut(gpointer data)
static void _search_entry_changed(GtkWidget *widget, gpointer user_data)
void dt_accels_attach_scroll_handler(dt_accels_t *accels, gboolean(*callback)(GdkEventScroll event, void *data), void *data)
Attach a new global scroll event callback. So far this is used in darkroom to redirect scroll events ...
static gboolean _queue_action_from_shortcut(dt_shortcut_t *shortcut, GtkWidget *window, dt_accels_search_state_t *state)
static void _add_widget_accel(dt_shortcut_t *shortcut, GtkAccelFlags flags)
static gboolean _key_pressed(GtkWidget *w, GdkEvent *event, dt_accels_t *accels, guint keyval, GdkModifierType mods)
void dt_accels_detach_scroll_handler(dt_accels_t *accels)
Handle default and user-set shortcuts (accelerators)
#define DT_ACCELS_WIDGET_SHORTCUT_KEY
dt_shortcut_type_t
@ DT_SHORTCUT_UNSET
@ DT_SHORTCUT_USER
@ DT_SHORTCUT_DEFAULT
#define DT_ACCELS_WIDGET_TOOLTIP_DISABLED_KEY
const char ** description(struct dt_iop_module_t *self)
Definition ashift.c:160
int scrolled(struct dt_iop_module_t *self, double x, double y, int up, uint32_t state)
Definition ashift.c:4898
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void init(dt_imageio_module_format_t *self)
Definition avif.c:151
int position()
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
char * key
int dt_conf_key_exists(const char *key)
gchar * dt_conf_get_string(const char *name)
void dt_conf_set_string(const char *name, const char *val)
darktable_t darktable
Definition darktable.c:183
void dt_print(dt_debug_thread_t thread, const char *msg,...)
Definition darktable.c:1600
@ DT_DEBUG_SHORTCUTS
Definition darktable.h:760
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:485
float dt_aligned_pixel_simd_t __attribute__((vector_size(16), aligned(16)))
Enable aggressive floating-point arithmetic optimizations, in denormals handling. Set through user pr...
Definition darktable.h:546
#define dt_free(ptr)
Definition darktable.h:478
static const dt_aligned_pixel_simd_t value
Definition darktable.h:599
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition darktable.h:293
int store(dt_imageio_module_storage_t *self, dt_imageio_module_data_t *sdata, const int32_t imgid, dt_imageio_module_format_t *format, dt_imageio_module_data_t *fdata, const int num, const int total, const gboolean high_quality, const gboolean export_masks, dt_colorspaces_color_profile_type_t icc_type, const gchar *icc_filename, dt_iop_color_intent_t icc_intent, dt_export_metadata_t *metadata)
Definition disk.c:252
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:384
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Definition dtpthread.h:369
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:389
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:374
static guint dt_keys_mainpad_alternatives(const guint key_val)
Remap keypad keys to usual mainpad ones.
Definition gdkkeys.h:113
static guint dt_keys_numpad_alternatives(const guint key_val)
Find the numpad equivalent key of any given key. Use this to define/handle alternative shortcuts.
Definition gdkkeys.h:29
void dt_capitalize_label(gchar *text)
Definition gtk.c:3382
void dt_gui_refocus_center()
Definition gtk.c:3482
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
Definition gtk.c:133
#define DT_GUI_BOX_SPACING
Definition gtk.h:109
void dt_gtkentry_setup_completion(GtkEntry *entry, const dt_gtkentry_completion_spec *compl_list, const char *trigger_char)
Definition gtkentry.c:173
GtkCellRenderer * dtgtk_cell_renderer_button_new(void)
const char * model
const int t
float *const restrict const size_t k
gboolean has_selection()
Definition menu.c:629
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
void dt_osx_disallow_fullscreen(GtkWidget *widget)
Definition osx.mm:104
struct _GtkWidget GtkWidget
Definition splash.h:29
const float uint32_t state[4]
gpointer parent_data
GClosure * base
GtkAccelGroup * group
GdkModifierType modifier
const char * path
GtkTreeStore * store
GHashTable * node_cache
struct dt_gui_gtk_t * gui
Definition darktable.h:803
int32_t unmuted
Definition darktable.h:788
GtkTreeModel * filter_model
GtkListStore * recent_entries
gboolean(* callback)(GdkEventScroll event, void *data)
GtkAccelFlags flags
gboolean disable_accels
GdkKeymap * keymap
struct dt_accels_t::scroll scroll
gboolean init
GtkAccelGroup * slideshow_accels
GtkAccelKey active_key
GtkAccelGroup * map_accels
GtkAccelGroup * global_accels
GtkAccelGroup * print_accels
GtkAccelGroup * lighttable_accels
GdkModifierType default_mod_mask
char * config_file
dt_pthread_mutex_t lock
GHashTable * acceleratables
GtkAccelGroup * active_group
GtkAccelGroup * darkroom_accels
dt_accels_t * accels
Definition gtk.h:199
GtkWidget * has_scroll_focus
Definition gtk.h:234
dt_ui_t * ui
Definition gtk.h:165
GdkModifierType mods
GtkAccelGroup * accel_group
gboolean locked
dt_shortcut_type_t type
dt_accels_t * accels
GtkWidget * widget
gboolean virtual_shortcut
const char * signal
const char * description
GtkWidget * panels[DT_UI_PANEL_SIZE]
GHashTable * entries
Definition supervisor.c:116
dt_pthread_mutex_t lock
Definition supervisor.c:119
GtkWidget * window
GtkWidget * search_entry
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
@ DT_UI_PANEL_TOP