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