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