1 /*
2     GTK hotkeys configuration for Deadbeef player
3     Copyright (C) 2009-2013 Alexey Yakovenko and other contributors
4 
5     This software is provided 'as-is', without any express or implied
6     warranty.  In no event will the authors be held liable for any damages
7     arising from the use of this software.
8 
9     Permission is granted to anyone to use this software for any purpose,
10     including commercial applications, and to alter it and redistribute it
11     freely, subject to the following restrictions:
12 
13     1. The origin of this software must not be misrepresented; you must not
14      claim that you wrote the original software. If you use this software
15      in a product, an acknowledgment in the product documentation would be
16      appreciated but is not required.
17 
18     2. Altered source versions must be plainly marked as such, and must not be
19      misrepresented as being the original software.
20 
21     3. This notice may not be removed or altered from any source distribution.
22 */
23 
24 
25 // deadbeef core doesn't have any special hotkeys code,
26 // but we need some common hotkey definition to share between plugins
27 // so here is the example structure to use when implementing hotkeys support
28 /*
29 // corresponding line in the config file:
30 // hotkey.keyX "key combination" CONTEXT IS_GLOBAL ACTION_ID
31 // action contexts are defined in deadbeef.h
32 //
33 // example:
34 // hotkey.key1 "Super+n" 0 1 playback_random
35 // this would mean "execute playback_random action when Super+n is pressed globally"
36 //
37 // context can be main, selection, playlist or nowplaying
38 // TODO: do we need anything else, like widget contexts?..
39 typedef struct
40 {
41     char *key_combination;
42     int context; // NULL, selection, playlist, nowplaying
43     DB_plugin_action_t *action;
44     unsigned is_global : 1;
45 } ddb_hotkey_t;
46 */
47 
48 #ifdef HAVE_CONFIG_H
49 #  include <config.h>
50 #endif
51 #include <gtk/gtk.h>
52 #include <string.h>
53 #include <stdlib.h>
54 #include "../../gettext.h"
55 #include "support.h"
56 #include "gtkui.h"
57 #include "interface.h"
58 #include "../libparser/parser.h"
59 #include "../hotkeys/hotkeys.h"
60 #ifndef __APPLE__
61 #include <X11/Xlib.h> // only for the KeySym type
62 #endif
63 #include "hotkeys.h"
64 #include "../../strdupa.h"
65 
66 int gtkui_hotkeys_changed = 0;
67 
68 void
69 on_hotkeys_actions_cursor_changed      (GtkTreeView     *treeview,
70                                         gpointer         user_data);
71 
72 static GtkWidget *prefwin;
73 static guint last_accel_key = 0;
74 static guint last_accel_mask = 0;
75 static const char *ctx_names[DDB_ACTION_CTX_COUNT];
76 
77 static void
unescape_forward_slash(const char * src,char * dst,int size)78 unescape_forward_slash (const char *src, char *dst, int size) {
79     char *start = dst;
80     while (*src) {
81         if (dst - start >= size - 1) {
82             break;
83         }
84         if (*src == '\\' && *(src+1) == '/') {
85             src++;
86         }
87         *dst++ = *src++;
88     }
89     *dst = 0;
90 }
91 
92 static void
prettify_forward_slash(const char * src,char * dst,int size)93 prettify_forward_slash (const char *src, char *dst, int size) {
94     char *start = dst;
95     const char arrow[] = " → ";
96     int larrow = strlen (arrow);
97     while (*src && size > 1) {
98         if (*src == '\\' && *(src+1) == '/') {
99             src++;
100         }
101         else if (*src == '/' && size > larrow) {
102             memcpy (dst, arrow, larrow);
103             src++;
104             dst += larrow;
105             size -= larrow;
106             continue;
107         }
108         *dst++ = *src++;
109         size--;
110     }
111     *dst = 0;
112 }
113 
114 static const char *
get_display_action_title(const char * title)115 get_display_action_title (const char *title) {
116     const char *t = title + strlen (title) - 1;
117     while (t > title) {
118         if (*t != '/' || *(t-1) == '\\') {
119             t--;
120             continue;
121         }
122         t++;
123         break;
124     }
125     return t;
126 }
127 
128 DB_plugin_action_t *
find_action_by_name(const char * command)129 find_action_by_name (const char *command) {
130     // find action with this name, and add to list
131     DB_plugin_action_t *actions = NULL;
132     DB_plugin_t **plugins = deadbeef->plug_get_list ();
133     for (int i = 0; plugins[i]; i++) {
134         DB_plugin_t *p = plugins[i];
135         if (p->get_actions) {
136             actions = p->get_actions (NULL);
137             while (actions) {
138                 if (actions->name && actions->title && !strcasecmp (actions->name, command)) {
139                     break; // found
140                 }
141                 actions = actions->next;
142             }
143             if (actions) {
144                 break;
145             }
146         }
147     }
148     return actions;
149 }
150 
151 static int
hotkeys_load(void)152 hotkeys_load (void) {
153     GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
154     GtkListStore *hkstore = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (hotkeys)));
155     gtk_list_store_clear (hkstore);
156     int n_items = 0;
157     DB_conf_item_t *item = deadbeef->conf_find ("hotkey.", NULL);
158     while (item) {
159         char token[MAX_TOKEN];
160         char keycombo[MAX_TOKEN];
161         int ctx;
162         int isglobal;
163         DB_plugin_action_t *action;
164         const char *script = item->value;
165         if ((script = gettoken (script, keycombo)) == 0) {
166             goto out;
167         }
168         if ((script = gettoken (script, token)) == 0) {
169             goto out;
170         }
171         ctx = atoi (token);
172         if (ctx < 0 || ctx >= DDB_ACTION_CTX_COUNT) {
173             goto out;
174         }
175         if ((script = gettoken (script, token)) == 0) {
176             goto out;
177         }
178         isglobal = atoi (token);
179         if ((script = gettoken (script, token)) == 0) {
180             goto out;
181         }
182         action = find_action_by_name (token);
183         if (!action) {
184             goto out;
185         }
186 
187         GtkTreeIter iter;
188         gtk_list_store_append (hkstore, &iter);
189 
190         const char *t = get_display_action_title (action->title);
191         char title[100];
192         unescape_forward_slash (t, title, sizeof (title));
193         gtk_list_store_set (hkstore, &iter, 0, keycombo, 1, title, 2, ctx_names[ctx], 3, isglobal, 4, action->name, 5, ctx, -1);
194         n_items++;
195 
196 out:
197         item = deadbeef->conf_find ("hotkey.", item);
198     }
199     return n_items;
200 }
201 
202 static void
hotkeys_save(void)203 hotkeys_save (void) {
204     GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
205     GtkListStore *hkstore = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (hotkeys)));
206     deadbeef->conf_remove_items ("hotkey.key");
207 
208     GtkTreePath *path = gtk_tree_path_new_first ();
209     GtkTreeIter iter;
210     gboolean res = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (hkstore), &iter);
211     int i = 1;
212     while (res) {
213         GValue keycombo = {0,}, action = {0,}, context = {0,}, global = {0,};
214         gtk_tree_model_get_value (GTK_TREE_MODEL (hkstore), &iter, 0, &keycombo);
215         gtk_tree_model_get_value (GTK_TREE_MODEL (hkstore), &iter, 4, &action);
216         gtk_tree_model_get_value (GTK_TREE_MODEL (hkstore), &iter, 5, &context);
217         gtk_tree_model_get_value (GTK_TREE_MODEL (hkstore), &iter, 3, &global);
218         char key[100];
219         snprintf (key, sizeof (key), "hotkey.key%02d", i);
220         char value[1000];
221         snprintf (value, sizeof (value), "\"%s\" %d %d %s", g_value_get_string (&keycombo), g_value_get_int (&context), g_value_get_boolean (&global), g_value_get_string (&action));
222         deadbeef->conf_set_str (key, value);
223 
224         res = gtk_tree_model_iter_next (GTK_TREE_MODEL (hkstore), &iter);
225         i++;
226     }
227     // FIXME: should be done in a more generic sendmessage
228     DB_plugin_t *hkplug = deadbeef->plug_get_for_id ("hotkeys");
229     if (hkplug) {
230         ((DB_hotkeys_plugin_t *)hkplug)->reset ();
231     }
232 }
233 
234 const char *
action_tree_append(const char * title,GtkTreeStore * store,GtkTreeIter * root_iter,GtkTreeIter * iter)235 action_tree_append (const char *title, GtkTreeStore *store, GtkTreeIter *root_iter, GtkTreeIter *iter) {
236     char *t = strdupa (title);
237     char *p = t;
238     GtkTreeIter i;
239     GtkTreeIter newroot;
240     int got_iter = 0;
241     for (;;) {
242         char *s = strchr (p, '/');
243         // find unescaped forward slash
244         if (s == p) {
245             p++;
246             continue;
247         }
248         if (s && s > p && *(s-1) == '\\') {
249             p = s + 1;
250             continue;
251         }
252         if (!s) {
253             break;
254         }
255         *s = 0;
256         // find iter in the current root with name==p
257         gboolean res = gtk_tree_model_iter_children (GTK_TREE_MODEL (store), &i, root_iter);
258         if (!res) {
259             gtk_tree_store_append (store, &i, root_iter);
260             gtk_tree_store_set (store, &i, 0, p, 1, NULL, 2, -1, -1);
261             memcpy (&newroot, &i, sizeof (GtkTreeIter));
262             root_iter = &newroot;
263         }
264         else {
265             int found = 0;
266             do {
267                 GValue val = {0,};
268                 gtk_tree_model_get_value (GTK_TREE_MODEL (store), &i, 0, &val);
269                 const char *n = g_value_get_string (&val);
270                 if (n && !strcmp (n, p)) {
271                     memcpy (&newroot, &i, sizeof (GtkTreeIter));
272                     root_iter = &newroot;
273                     found = 1;
274                     break;
275                 }
276             } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &i));
277             if (!found) {
278                 gtk_tree_store_append (store, &i, root_iter);
279                 gtk_tree_store_set (store, &i, 0, p, 1, NULL, 2, -1, -1);
280                 root_iter = &i;
281             }
282         }
283 
284         p = s+1;
285     }
286     gtk_tree_store_append (store, iter, root_iter);
287     return get_display_action_title (title);
288 }
289 
290 typedef struct {
291     const char *name;
292     int ctx;
293     GtkWidget *treeview;
294 } actionbinding_t;
295 
296 static gboolean
set_current_action(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)297 set_current_action (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) {
298     GValue val = {0,}, ctx_val = {0,};
299     gtk_tree_model_get_value (model, iter, 1, &val);
300     gtk_tree_model_get_value (model, iter, 2, &ctx_val);
301     actionbinding_t *binding = data;
302     const char *name = g_value_get_string (&val);
303     if (name && binding->name && !strcmp (binding->name, name) && binding->ctx == g_value_get_int (&ctx_val)) {
304         gtk_tree_view_expand_to_path (GTK_TREE_VIEW (binding->treeview), path);
305         gtk_tree_view_set_cursor (GTK_TREE_VIEW (binding->treeview), path, NULL, FALSE);
306         return TRUE;
307     }
308     return FALSE;
309 }
310 
311 void
init_action_tree(GtkWidget * actions,const char * act,int ctx)312 init_action_tree (GtkWidget *actions, const char *act, int ctx) {
313     GtkTreeViewColumn *hk_act_col1 = gtk_tree_view_column_new_with_attributes (_("Action"), gtk_cell_renderer_text_new (), "text", 0, NULL);
314     gtk_tree_view_append_column (GTK_TREE_VIEW (actions), hk_act_col1);
315 
316     // traverse all plugins and collect all exported actions to treeview
317     // column0: title
318     // column1: ID (invisible)
319     // column2: ctx (invisible
320     GtkTreeStore *actions_store = gtk_tree_store_new (3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);
321     GtkTreeIter action_main_iter;
322     gtk_tree_store_append (actions_store, &action_main_iter, NULL);
323     gtk_tree_store_set (actions_store, &action_main_iter, 0, _("Main"), -1);
324 
325     GtkTreeIter action_selection_iter;
326     gtk_tree_store_append (actions_store, &action_selection_iter, NULL);
327     gtk_tree_store_set (actions_store, &action_selection_iter, 0, _("Selected track(s)"), -1);
328     GtkTreeIter action_playlist_iter;
329     gtk_tree_store_append (actions_store, &action_playlist_iter, NULL);
330     gtk_tree_store_set (actions_store, &action_playlist_iter, 0, _("Current playlist"), -1);
331     GtkTreeIter action_nowplaying_iter;
332     gtk_tree_store_append (actions_store, &action_nowplaying_iter, NULL);
333     gtk_tree_store_set (actions_store, &action_nowplaying_iter, 0, _("Now playing"), -1);
334 
335     DB_plugin_t **plugins = deadbeef->plug_get_list ();
336     for (int i = 0; plugins[i]; i++) {
337         DB_plugin_t *p = plugins[i];
338         if (p->get_actions) {
339             DB_plugin_action_t *actions = p->get_actions (NULL);
340             while (actions) {
341                 if (actions->name && actions->title) { // only add actions with both the name and the title
342                     char title[100];
343 
344                     GtkTreeIter iter;
345                     const char *t;
346                     if (actions->flags & DB_ACTION_COMMON) {
347                         t = action_tree_append (actions->title, actions_store, &action_main_iter, &iter);
348                         unescape_forward_slash (t, title, sizeof (title));
349                         gtk_tree_store_set (actions_store, &iter, 0, title, 1, actions->name, 2, DDB_ACTION_CTX_MAIN, -1);
350                     }
351                     if (actions->flags & (DB_ACTION_SINGLE_TRACK | DB_ACTION_MULTIPLE_TRACKS | DB_ACTION_CAN_MULTIPLE_TRACKS)) {
352                         t = action_tree_append (actions->title, actions_store, &action_selection_iter, &iter);
353                         unescape_forward_slash (t, title, sizeof (title));
354                         gtk_tree_store_set (actions_store, &iter, 0, title, 1, actions->name, 2, DDB_ACTION_CTX_SELECTION, -1);
355                         t = action_tree_append (actions->title, actions_store, &action_playlist_iter, &iter);
356                         unescape_forward_slash (t, title, sizeof (title));
357                         gtk_tree_store_set (actions_store, &iter, 0, title, 1, actions->name, 2, DDB_ACTION_CTX_PLAYLIST, -1);
358                         t = action_tree_append (actions->title, actions_store, &action_nowplaying_iter, &iter);
359                         unescape_forward_slash (t, title, sizeof (title));
360                         gtk_tree_store_set (actions_store, &iter, 0, title, 1, actions->name, 2, DDB_ACTION_CTX_NOWPLAYING, -1);
361                     }
362                 }
363                 else {
364 //                    fprintf (stderr, "WARNING: action %s/%s from plugin %s is missing name and/or title\n", actions->name, actions->title, p->name);
365                 }
366                 actions = actions->next;
367             }
368         }
369     }
370 
371     gtk_tree_view_set_model (GTK_TREE_VIEW (actions), GTK_TREE_MODEL (actions_store));
372 
373     if (act && ctx != -1) {
374         actionbinding_t binding = {
375             .name = act,
376             .ctx = ctx,
377             .treeview = actions
378         };
379         gtk_tree_model_foreach (GTK_TREE_MODEL (actions_store), set_current_action, (void*)&binding);
380     }
381 }
382 
383 void
on_hotkeys_actions_clicked(GtkButton * button,gpointer user_data)384 on_hotkeys_actions_clicked             (GtkButton       *button,
385                                         gpointer         user_data)
386 {
387     GtkTreePath *path;
388     GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
389     gtk_tree_view_get_cursor (GTK_TREE_VIEW (hotkeys), &path, NULL);
390     GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (hotkeys));
391     GtkTreeIter iter;
392     if (!path || !gtk_tree_model_get_iter (model, &iter, path)) {
393         return;
394     }
395     // get action name from iter
396     GValue val_name = {0,}, val_ctx = {0,};
397     gtk_tree_model_get_value (model, &iter, 4, &val_name);
398     gtk_tree_model_get_value (model, &iter, 5, &val_ctx);
399     const char *act = g_value_get_string (&val_name);
400     int ctx = g_value_get_int (&val_ctx);
401 
402     GtkWidget *dlg = create_select_action ();
403     GtkWidget *treeview = lookup_widget (dlg, "actions");
404     init_action_tree (treeview, act, ctx);
405     int response = gtk_dialog_run (GTK_DIALOG (dlg));
406     if (response == GTK_RESPONSE_OK) {
407         on_hotkeys_actions_cursor_changed (GTK_TREE_VIEW (treeview), NULL);
408 
409         GtkTreePath *path;
410         gtk_tree_view_get_cursor (GTK_TREE_VIEW (treeview), &path, NULL);
411         GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
412         GtkTreeIter iter;
413         const char *name = NULL;
414         int ctx = -1;
415         if (path && gtk_tree_model_get_iter (model, &iter, path)) {
416             GValue val = {0,};
417             gtk_tree_model_get_value (model, &iter, 1, &val);
418             name = g_value_get_string (&val);
419             GValue val_ctx = {0,};
420             gtk_tree_model_get_value (model, &iter, 2, &val_ctx);
421             ctx = g_value_get_int (&val_ctx);
422         }
423         set_button_action_label (name, ctx, lookup_widget (prefwin, "hotkeys_actions"));
424     }
425     gtk_widget_destroy (dlg);
426 }
427 
428 void
prefwin_init_hotkeys(GtkWidget * _prefwin)429 prefwin_init_hotkeys (GtkWidget *_prefwin) {
430     DB_plugin_t *hkplug = deadbeef->plug_get_for_id ("hotkeys");
431     if (!hkplug) {
432         // remove hotkeys tab
433         gtk_notebook_remove_page (GTK_NOTEBOOK (lookup_widget (_prefwin, "notebook")), 6);
434         return;
435     }
436 
437     gtkui_hotkeys_changed = 0;
438     ctx_names[DDB_ACTION_CTX_MAIN] = _("Main");
439     ctx_names[DDB_ACTION_CTX_SELECTION] = _("Selection");
440     ctx_names[DDB_ACTION_CTX_PLAYLIST] = _("Playlist");
441     ctx_names[DDB_ACTION_CTX_NOWPLAYING] = _("Now playing");
442 
443     prefwin = _prefwin;
444     GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
445 
446     // setup hotkeys list
447     GtkTreeViewColumn *hk_col1 = gtk_tree_view_column_new_with_attributes (_("Key combination"), gtk_cell_renderer_text_new (), "text", 0, NULL);
448     gtk_tree_view_column_set_resizable (hk_col1, TRUE);
449     GtkTreeViewColumn *hk_col2 = gtk_tree_view_column_new_with_attributes (_("Action"), gtk_cell_renderer_text_new (), "text", 1, NULL);
450     gtk_tree_view_column_set_resizable (hk_col2, TRUE);
451     GtkTreeViewColumn *hk_col3 = gtk_tree_view_column_new_with_attributes (_("Context"), gtk_cell_renderer_text_new (), "text", 2, NULL);
452     gtk_tree_view_column_set_resizable (hk_col3, TRUE);
453     GtkTreeViewColumn *hk_col4 = gtk_tree_view_column_new_with_attributes (_("Is global"), gtk_cell_renderer_text_new (), "text", 3, NULL);
454     gtk_tree_view_column_set_resizable (hk_col4, TRUE);
455     gtk_tree_view_append_column (GTK_TREE_VIEW (hotkeys), hk_col1);
456     gtk_tree_view_append_column (GTK_TREE_VIEW (hotkeys), hk_col2);
457     gtk_tree_view_append_column (GTK_TREE_VIEW (hotkeys), hk_col3);
458     gtk_tree_view_append_column (GTK_TREE_VIEW (hotkeys), hk_col4);
459     // column0: keycombo string
460     // column1: action title
461     // column2: context title
462     // column3: is_global
463     // column4: action title id (hidden)
464     // column5: context id (hidden)
465     GtkListStore *hkstore = gtk_list_store_new (6, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_INT);
466 
467     gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkeys_actions"), FALSE);
468     gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkey_is_global"), FALSE);
469     gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkeys_set_key"), FALSE);
470 
471     gtk_tree_view_set_model (GTK_TREE_VIEW (hotkeys), GTK_TREE_MODEL (hkstore));
472 
473     int n_hotkeys = hotkeys_load ();
474 }
475 
476 void
set_button_action_label(const char * act,int action_ctx,GtkWidget * button)477 set_button_action_label (const char *act, int action_ctx, GtkWidget *button) {
478     if (act && action_ctx >= 0) {
479         DB_plugin_action_t *action = find_action_by_name (act);
480         if (action) {
481             const char *ctx_str = NULL;
482             switch (action_ctx) {
483             case DDB_ACTION_CTX_MAIN:
484                 break;
485             case DDB_ACTION_CTX_SELECTION:
486                 ctx_str = _("Selected tracks");
487                 break;
488             case DDB_ACTION_CTX_PLAYLIST:
489                 ctx_str = _("Tracks in current playlist");
490                 break;
491             case DDB_ACTION_CTX_NOWPLAYING:
492                 ctx_str = _("Currently playing track");
493                 break;
494             }
495             char s[200];
496             snprintf (s, sizeof (s), "%s%s%s", ctx_str ? ctx_str : "", ctx_str ? " ⇒ ": "", action->title);
497             char s_fixed[200];
498             prettify_forward_slash (s, s_fixed, sizeof (s_fixed));
499 
500             gtk_button_set_label (GTK_BUTTON (button), s_fixed);
501             return;
502         }
503     }
504 
505     gtk_button_set_label (GTK_BUTTON (button), _("<Not set>"));
506 }
507 
508 
509 void
on_hotkeys_list_cursor_changed(GtkTreeView * treeview,gpointer user_data)510 on_hotkeys_list_cursor_changed         (GtkTreeView     *treeview,
511                                         gpointer         user_data)
512 {
513     GtkTreePath *path;
514     gtk_tree_view_get_cursor (treeview, &path, NULL);
515     GtkTreeModel *model = gtk_tree_view_get_model (treeview);
516     GtkTreeIter iter;
517     int changed = gtkui_hotkeys_changed;
518     if (path && gtk_tree_model_get_iter (model, &iter, path)) {
519         GtkWidget *actions = lookup_widget (prefwin, "hotkeys_actions");
520         gtk_widget_set_sensitive (actions, TRUE);
521         // get action name from iter
522         GValue val_name = {0,}, val_ctx = {0,};
523         gtk_tree_model_get_value (model, &iter, 4, &val_name);
524         gtk_tree_model_get_value (model, &iter, 5, &val_ctx);
525         const char *name = g_value_get_string (&val_name);
526 
527         set_button_action_label (name, g_value_get_int (&val_ctx), actions);
528 
529         gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkey_is_global"), TRUE);
530         GValue val_isglobal = {0,};
531         gtk_tree_model_get_value (model, &iter, 3, &val_isglobal);
532         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (lookup_widget (prefwin, "hotkey_is_global")), g_value_get_boolean (&val_isglobal));
533         gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkeys_set_key"), TRUE);
534         GValue val_keycombo = {0,};
535         gtk_tree_model_get_value (model, &iter, 0, &val_keycombo);
536         const char *keycombo = g_value_get_string (&val_keycombo);
537         gtk_button_set_label (GTK_BUTTON (lookup_widget (prefwin, "hotkeys_set_key")), keycombo ? keycombo : "");
538     }
539     else {
540         gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkeys_actions"), FALSE);
541         gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkey_is_global"), FALSE);
542         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (lookup_widget (prefwin, "hotkey_is_global")), FALSE);
543         gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkeys_set_key"), FALSE);
544         gtk_button_set_label (GTK_BUTTON (lookup_widget (prefwin, "hotkeys_set_key")), _("<Not set>"));
545     }
546     if (path) {
547         gtk_tree_path_free (path);
548     }
549     gtkui_hotkeys_changed = changed;
550 }
551 
552 
553 void
on_hotkey_add_clicked(GtkButton * button,gpointer user_data)554 on_hotkey_add_clicked                  (GtkButton       *button,
555                                         gpointer         user_data)
556 {
557     GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
558     GtkListStore *hkstore = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (hotkeys)));
559     GtkTreeIter iter;
560     gtk_list_store_append (hkstore, &iter);
561     gtk_list_store_set (hkstore, &iter, 0, _("<Not set>"), 1, _("<Not set>"), 2, _("<Not set>"), 3, 0, 4, NULL, 5, -1, -1);
562     GtkTreePath *path = gtk_tree_model_get_path (GTK_TREE_MODEL (hkstore), &iter);
563     gtk_tree_view_set_cursor (GTK_TREE_VIEW (hotkeys), path, NULL, FALSE);
564     gtk_tree_path_free (path);
565     gtk_widget_grab_focus (hotkeys);
566     gtkui_hotkeys_changed = 1;
567 }
568 
569 
570 void
on_hotkey_remove_clicked(GtkButton * button,gpointer user_data)571 on_hotkey_remove_clicked               (GtkButton       *button,
572                                         gpointer         user_data)
573 {
574     GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
575     GtkTreePath *path;
576     gtk_tree_view_get_cursor (GTK_TREE_VIEW (hotkeys), &path, NULL);
577     GtkListStore *hkstore = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (hotkeys)));
578     GtkTreeIter iter;
579     gtk_tree_model_get_iter (GTK_TREE_MODEL (hkstore), &iter, path);
580     gtk_list_store_remove (hkstore, &iter);
581     set_button_action_label (NULL, 0, lookup_widget (prefwin, "hotkeys_actions"));
582     gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (lookup_widget (prefwin, "hotkey_is_global")), FALSE);
583     gtk_button_set_label (GTK_BUTTON (lookup_widget (prefwin, "hotkeys_set_key")), _("<Not set>"));
584     gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkeys_actions"), FALSE);
585     gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkey_is_global"), FALSE);
586     gtk_widget_set_sensitive (lookup_widget (prefwin, "hotkeys_set_key"), FALSE);
587     gtkui_hotkeys_changed = 1;
588 }
589 
590 
591 void
on_hotkeys_actions_cursor_changed(GtkTreeView * treeview,gpointer user_data)592 on_hotkeys_actions_cursor_changed      (GtkTreeView     *treeview,
593                                         gpointer         user_data)
594 {
595     GtkTreePath *path;
596     gtk_tree_view_get_cursor (treeview, &path, NULL);
597     GtkTreeModel *model = gtk_tree_view_get_model (treeview);
598     GtkTreeIter iter;
599     if (path && gtk_tree_model_get_iter (model, &iter, path)) {
600         GValue val = {0,};
601         gtk_tree_model_get_value (model, &iter, 1, &val);
602         const gchar *name = g_value_get_string (&val);
603         DB_plugin_action_t *action = NULL;
604         int ctx = 0;
605         if (name) {
606             action = find_action_by_name (name);
607             GValue val_ctx = {0,};
608             gtk_tree_model_get_value (model, &iter, 2, &val_ctx);
609             ctx = g_value_get_int (&val_ctx);
610         }
611         // update the tree
612         {
613             GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
614             GtkTreePath *path;
615             gtk_tree_view_get_cursor (GTK_TREE_VIEW (hotkeys), &path, NULL);
616             GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (hotkeys));
617             GtkTreeIter iter;
618             if (path && gtk_tree_model_get_iter (model, &iter, path)) {
619                 if (action) {
620                     const char *t = get_display_action_title (action->title);
621                     char title[100];
622                     unescape_forward_slash (t, title, sizeof (title));
623                     gtk_list_store_set (GTK_LIST_STORE (model), &iter, 1, title, 4, action->name, 5, ctx, 2, ctx_names[ctx], -1);
624                 }
625                 else {
626                     gtk_list_store_set (GTK_LIST_STORE (model), &iter, 1, _("<Not set>"), 4, NULL, 2, _("<Not set>"), -1);
627                 }
628             }
629         }
630     }
631 }
632 
633 
634 void
on_hotkey_is_global_toggled(GtkToggleButton * togglebutton,gpointer user_data)635 on_hotkey_is_global_toggled            (GtkToggleButton *togglebutton,
636                                         gpointer         user_data)
637 {
638     // update the tree
639     GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
640     GtkTreePath *path;
641     gtk_tree_view_get_cursor (GTK_TREE_VIEW (hotkeys), &path, NULL);
642     GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (hotkeys));
643     GtkTreeIter iter;
644     if (path && gtk_tree_model_get_iter (model, &iter, path)) {
645         gtk_list_store_set (GTK_LIST_STORE (model), &iter, 3, gtk_toggle_button_get_active (togglebutton), -1);
646     }
647     gtkui_hotkeys_changed = 1;
648 }
649 
650 typedef struct {
651     const char *name;
652     int keysym;
653 } xkey_t;
654 
655 #define KEY(kname, kcode) { .name=kname, .keysym=kcode },
656 
657 static const xkey_t keys[] = {
658     #include "../hotkeys/keysyms.inc"
659 };
660 
661 static const char *
get_name_for_keycode(int keycode)662 get_name_for_keycode (int keycode) {
663     for (int i = 0; keys[i].name; i++) {
664         if (keycode == keys[i].keysym) {
665             return keys[i].name;
666         }
667     }
668     return NULL;
669 }
670 
671 
672 int gtkui_hotkey_grabbing = 0;
673 
674 static void
get_keycombo_string(guint accel_key,GdkModifierType accel_mods,char * new_value)675 get_keycombo_string (guint accel_key, GdkModifierType accel_mods, char *new_value) {
676     // build value
677     new_value[0] = 0;
678     if (!accel_key) {
679         strcpy (new_value, _("<Not set>"));
680         return;
681     }
682     if (accel_mods & GDK_SHIFT_MASK) {
683         strcat (new_value, "Shift ");
684     }
685     if (accel_mods & GDK_CONTROL_MASK) {
686         strcat (new_value, "Ctrl ");
687     }
688     if (accel_mods & GDK_SUPER_MASK) {
689         strcat (new_value, "Super ");
690     }
691     if (accel_mods & GDK_MOD1_MASK) {
692         strcat (new_value, "Alt ");
693     }
694 
695     // translate numlock keycodes into non-numlock codes
696     switch (accel_key) {
697     case GDK_KP_0:
698         accel_key = GDK_KP_Insert;
699         break;
700     case GDK_KP_1:
701         accel_key = GDK_KP_End;
702         break;
703     case GDK_KP_2:
704         accel_key = GDK_KP_Down;
705         break;
706     case GDK_KP_3:
707         accel_key = GDK_KP_Page_Down;
708         break;
709     case GDK_KP_4:
710         accel_key = GDK_KP_Left;
711         break;
712     case GDK_KP_6:
713         accel_key = GDK_KP_Right;
714         break;
715     case GDK_KP_7:
716         accel_key = GDK_KP_Home;
717         break;
718     case GDK_KP_8:
719         accel_key = GDK_KP_Up;
720         break;
721     case GDK_KP_9:
722         accel_key = GDK_KP_Page_Up;
723         break;
724     }
725 
726     const char *name = get_name_for_keycode (accel_key);
727     if (!name) {
728         strcpy (new_value, _("<Not set>"));
729         return;
730     }
731     strcat (new_value, name);
732 }
733 
734 static GtkWidget *hotkey_grabber_button;
735 gboolean
on_hotkeys_set_key_key_press_event(GtkWidget * widget,GdkEventKey * event,gpointer user_data)736 on_hotkeys_set_key_key_press_event     (GtkWidget       *widget,
737                                         GdkEventKey     *event,
738                                         gpointer         user_data)
739 {
740     widget = hotkey_grabber_button;
741     GdkModifierType accel_mods = 0;
742     guint accel_key;
743     gchar *path;
744     gboolean edited;
745     gboolean cleared;
746     GdkModifierType consumed_modifiers;
747     GdkDisplay *display;
748     GtkTreePath *curpath;
749     GtkTreeIter iter;
750 
751     if (!gtkui_hotkey_grabbing) {
752         return FALSE;
753     }
754 
755     display = gtk_widget_get_display (widget);
756 
757     if (event->is_modifier)
758         return TRUE;
759 
760     edited = FALSE;
761     cleared = FALSE;
762 
763     gdk_keymap_translate_keyboard_state (gdk_keymap_get_for_display (display),
764             event->hardware_keycode, event->state,
765             0, &accel_key, NULL, NULL, &consumed_modifiers);
766 
767     if (accel_key == GDK_ISO_Left_Tab)
768         accel_key = GDK_Tab;
769 
770     accel_mods = event->state & gtk_accelerator_get_default_mod_mask ();
771 
772     /* Filter consumed modifiers
773     */
774     accel_mods &= ~(consumed_modifiers&~GDK_SHIFT_MASK);
775 
776     /* Put shift back if it changed the case of the key, not otherwise.
777     */
778     int lower = gdk_keyval_to_lower (accel_key);
779     if (lower != accel_key) {
780         accel_key = lower;
781     }
782 
783     char name[1000];
784     gtk_button_set_label (GTK_BUTTON (widget), _(""));
785 
786     GtkWidget *hotkeys = lookup_widget (prefwin, "hotkeys_list");
787     GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (hotkeys));
788 
789     // check if this key already registered
790     get_keycombo_string (accel_key, accel_mods, name);
791 
792     gtk_tree_view_get_cursor (GTK_TREE_VIEW (hotkeys), &curpath, NULL);
793     gboolean res = gtk_tree_model_get_iter_first (model, &iter);
794     while (res) {
795         GtkTreePath *iterpath = gtk_tree_model_get_path (model, &iter);
796 
797         if (!curpath || gtk_tree_path_compare (iterpath, curpath)) {
798             GValue keycombo = {0,};
799             gtk_tree_model_get_value (model, &iter, 0, &keycombo);
800             const char *val = g_value_get_string (&keycombo);
801             if (val && !strcmp (val, name)) {
802                 gtk_tree_path_free (iterpath);
803                 break;
804             }
805         }
806         gtk_tree_path_free (iterpath);
807 
808         res = gtk_tree_model_iter_next (model, &iter);
809     }
810 
811     if (res) {
812         // duplicate
813         gtk_button_set_label (GTK_BUTTON (widget), _("Duplicate key combination!"));
814         gtk_widget_error_bell (widget);
815         goto out;
816     }
817 
818     last_accel_key = accel_key;
819     last_accel_mask = accel_mods;
820     get_keycombo_string (last_accel_key, last_accel_mask, name);
821     gtk_button_set_label (GTK_BUTTON (widget), name);
822 
823     // update the tree
824     if (curpath && gtk_tree_model_get_iter (model, &iter, curpath)) {
825         gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, name, -1);
826     }
827 
828 out:
829     if (curpath) {
830         gtk_tree_path_free (curpath);
831     }
832     gdk_display_keyboard_ungrab (display, GDK_CURRENT_TIME);
833     gdk_display_pointer_ungrab (display, GDK_CURRENT_TIME);
834     gtkui_hotkey_grabbing = 0;
835     gtkui_hotkeys_changed = 1;
836     return TRUE;
837 }
838 
839 static void
hotkey_grab_focus(GtkWidget * widget)840 hotkey_grab_focus (GtkWidget *widget) {
841     GdkDisplay *display = gtk_widget_get_display (widget);
842     if (gtkui_hotkey_grabbing) {
843         return;
844     }
845     gtkui_hotkey_grabbing = 0;
846     if (GDK_GRAB_SUCCESS != gdk_keyboard_grab (gtk_widget_get_window (widget), FALSE, GDK_CURRENT_TIME)) {
847         return;
848     }
849 
850     if (gdk_pointer_grab (gtk_widget_get_window (widget), FALSE,
851                 GDK_BUTTON_PRESS_MASK,
852                 NULL, NULL,
853                 GDK_CURRENT_TIME) != GDK_GRAB_SUCCESS)
854     {
855         gdk_display_keyboard_ungrab (display, GDK_CURRENT_TIME);
856         return;
857     }
858     gtk_button_set_label (GTK_BUTTON (widget), _("New key combination..."));
859     gtkui_hotkey_grabbing = 1;
860 
861     // disable the window accelerators temporarily
862     hotkey_grabber_button = widget;
863 }
864 
865 void
on_hotkeys_set_key_clicked(GtkButton * button,gpointer user_data)866 on_hotkeys_set_key_clicked             (GtkButton       *button,
867                                         gpointer         user_data)
868 {
869     hotkey_grab_focus (GTK_WIDGET (button));
870 }
871 
872 void
on_hotkeys_apply_clicked(GtkButton * button,gpointer user_data)873 on_hotkeys_apply_clicked               (GtkButton       *button,
874                                         gpointer         user_data)
875 {
876     hotkeys_save ();
877     gtkui_hotkeys_changed = 0;
878 }
879 
880 
881 void
on_hotkeys_revert_clicked(GtkButton * button,gpointer user_data)882 on_hotkeys_revert_clicked              (GtkButton       *button,
883                                         gpointer         user_data)
884 {
885     hotkeys_load ();
886     gtkui_hotkeys_changed = 0;
887 }
888 
889 void
on_hotkeys_defaults_clicked(GtkButton * button,gpointer user_data)890 on_hotkeys_defaults_clicked            (GtkButton       *button,
891                                         gpointer         user_data)
892 {
893     GtkWidget *dlg = gtk_message_dialog_new (GTK_WINDOW (prefwin), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, _("All your custom-defined hotkeys will be lost."));
894     gtk_window_set_transient_for (GTK_WINDOW (dlg), GTK_WINDOW (prefwin));
895     gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dlg), _("This operation cannot be undone. Proceed?"));
896     gtk_window_set_title (GTK_WINDOW (dlg), _("Warning"));
897     int response = gtk_dialog_run (GTK_DIALOG (dlg));
898     gtk_widget_destroy (dlg);
899     if (response != GTK_RESPONSE_YES) {
900         return;
901     }
902     gtkui_set_default_hotkeys ();
903     hotkeys_load ();
904     gtkui_hotkeys_changed = 0;
905 }
906 
907 void
gtkui_set_default_hotkeys(void)908 gtkui_set_default_hotkeys (void) {
909     deadbeef->conf_remove_items ("hotkey.key");
910     deadbeef->conf_set_str ("hotkey.key01", "\"Ctrl f\" 0 0 find");
911     deadbeef->conf_set_str ("hotkey.key02", "\"Ctrl o\" 0 0 open_files");
912     deadbeef->conf_set_str ("hotkey.key03", "\"Ctrl q\" 0 0 quit");
913     deadbeef->conf_set_str ("hotkey.key04", "\"Ctrl n\" 0 0 new_playlist");
914     deadbeef->conf_set_str ("hotkey.key05", "\"Ctrl a\" 0 0 select_all");
915     deadbeef->conf_set_str ("hotkey.key06", "\"Escape\" 0 0 deselect_all");
916     deadbeef->conf_set_str ("hotkey.key07", "\"Ctrl m\" 0 0 toggle_stop_after_current");
917     deadbeef->conf_set_str ("hotkey.key08", "\"Ctrl j\" 0 0 jump_to_current_track");
918     deadbeef->conf_set_str ("hotkey.key09", "\"F1\" 0 0 help");
919     deadbeef->conf_set_str ("hotkey.key10", "\"Delete\" 1 0 remove_from_playlist");
920     deadbeef->conf_set_str ("hotkey.key11", "\"Ctrl w\" 0 0 remove_current_playlist");
921     deadbeef->conf_set_str ("hotkey.key11", "\"Ctrl w\" 0 0 remove_current_playlist");
922     deadbeef->conf_set_str ("hotkey.key11", "\"Ctrl w\" 0 0 remove_current_playlist");
923     deadbeef->conf_set_str ("hotkey.key14", "\"Return\" 0 0 play");
924     deadbeef->conf_set_str ("hotkey.key15", "\"Ctrl p\" 0 0 toggle_pause");
925     deadbeef->conf_set_str ("hotkey.key16", "\"Alt 1\" 0 0 playlist1");
926     deadbeef->conf_set_str ("hotkey.key17", "\"Alt 2\" 0 0 playlist2");
927     deadbeef->conf_set_str ("hotkey.key18", "\"Alt 3\" 0 0 playlist3");
928     deadbeef->conf_set_str ("hotkey.key19", "\"Alt 4\" 0 0 playlist4");
929     deadbeef->conf_set_str ("hotkey.key20", "\"Alt 5\" 0 0 playlist5");
930     deadbeef->conf_set_str ("hotkey.key21", "\"Alt 6\" 0 0 playlist6");
931     deadbeef->conf_set_str ("hotkey.key22", "\"Alt 7\" 0 0 playlist7");
932     deadbeef->conf_set_str ("hotkey.key23", "\"Alt 8\" 0 0 playlist8");
933     deadbeef->conf_set_str ("hotkey.key24", "\"Alt 9\" 0 0 playlist9");
934     deadbeef->conf_set_str ("hotkey.key25", "\"Alt 0\" 0 0 playlist10");
935     deadbeef->conf_set_str ("hotkey.key26", "z 0 0 prev");
936     deadbeef->conf_set_str ("hotkey.key27", "x 0 0 play");
937     deadbeef->conf_set_str ("hotkey.key28", "c 0 0 toggle_pause");
938     deadbeef->conf_set_str ("hotkey.key29", "v 0 0 stop");
939     deadbeef->conf_set_str ("hotkey.key30", "b 0 0 next");
940     deadbeef->conf_set_str ("hotkey.key31", "n 0 0 playback_random");
941     deadbeef->conf_set_str ("hotkey.key32", "\"Ctrl k\" 0 0 toggle_stop_after_album");
942     deadbeef->conf_save ();
943 }
944 
945 void
gtkui_import_0_5_global_hotkeys(void)946 gtkui_import_0_5_global_hotkeys (void) {
947     int n = 40;
948     deadbeef->conf_lock ();
949     DB_conf_item_t *item = deadbeef->conf_find ("hotkeys.key", NULL);
950     while (item) {
951         char *val = strdupa (item->value);
952         char *colon = strchr (val, ':');
953         if (colon) {
954             *colon++ = 0;
955             while (*colon && *colon == ' ') {
956                 colon++;
957             }
958             if (*colon) {
959                 char newkey[100];
960                 char newval[100];
961                 snprintf (newkey, sizeof (newkey), "hotkey.key%02d", n);
962                 snprintf (newval, sizeof (newval), "\"%s\" 0 1 %s", val, colon);
963                 deadbeef->conf_set_str (newkey, newval);
964                 n++;
965             }
966         }
967         item = deadbeef->conf_find ("hotkeys.", item);
968     }
969     deadbeef->conf_unlock ();
970 }
971