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