1 /*
2 Hotkeys plugin for DeaDBeeF
3 Copyright (C) 2009-2011 Viktor Semykin <thesame.ml@gmail.com>
4 Copyright (C) 2012-2013 Alexey Yakovenko <waker@users.sourceforge.net>
5
6 This software is provided 'as-is', without any express or implied
7 warranty. In no event will the authors be held liable for any damages
8 arising from the use of this software.
9
10 Permission is granted to anyone to use this software for any purpose,
11 including commercial applications, and to alter it and redistribute it
12 freely, subject to the following restrictions:
13
14 1. The origin of this software must not be misrepresented; you must not
15 claim that you wrote the original software. If you use this software
16 in a product, an acknowledgment in the product documentation would be
17 appreciated but is not required.
18
19 2. Altered source versions must be plainly marked as such, and must not be
20 misrepresented as being the original software.
21
22 3. This notice may not be removed or altered from any source distribution.
23 */
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <unistd.h>
28 #ifndef __APPLE__
29 #include <X11/Xlib.h>
30 #endif
31 #include <ctype.h>
32 #ifdef __linux__
33 #include <sys/prctl.h>
34 #endif
35
36 #include "../libparser/parser.h"
37 #include "hotkeys.h"
38 #include "../../deadbeef.h"
39 #include "actionhandlers.h"
40
41 //#define trace(...) { fprintf(stderr, __VA_ARGS__); }
42 #define trace(fmt,...)
43
44 static DB_hotkeys_plugin_t plugin;
45 DB_functions_t *deadbeef;
46
47 #ifndef __APPLE__
48 static int finished;
49 static Display *disp;
50 static intptr_t loop_tid;
51 static int need_reset = 0;
52 #endif
53
54 #define MAX_COMMAND_COUNT 256
55
56 typedef struct {
57 const char *name;
58 int keysym;
59 #ifndef __APPLE__
60 int keycode; // after mapping
61 #endif
62 } xkey_t;
63
64 #define KEY(kname, kcode) { .name=kname, .keysym=kcode},
65
66 static xkey_t keys[] = {
67 #include "keysyms.inc"
68 };
69
70 typedef struct command_s {
71 int keycode;
72 #ifndef __APPLE__
73 int x11_keycode;
74 #endif
75 int modifier;
76 int ctx;
77 int isglobal;
78 DB_plugin_action_t *action;
79 } command_t;
80
81 static command_t commands [MAX_COMMAND_COUNT];
82 static int command_count = 0;
83
84 #ifndef __APPLE__
85 static void
init_mapped_keycodes(Display * disp,Atom * syms,int first_kk,int last_kk,int ks_per_kk)86 init_mapped_keycodes (Display *disp, Atom *syms, int first_kk, int last_kk, int ks_per_kk) {
87 int i, ks;
88 for (i = 0; i < last_kk-first_kk; i++)
89 {
90 int sym = * (syms + i*ks_per_kk);
91 for (ks = 0; keys[ks].name; ks++)
92 {
93 if (keys[ ks ].keysym == sym)
94 {
95 keys[ks].keycode = i+first_kk;
96 }
97 }
98 }
99 }
100 #endif
101
102 static int
get_keycode(const char * name)103 get_keycode (const char* name) {
104 for (int i = 0; keys[i].name; i++) {
105 if (!strcmp (name, keys[i].name)) {
106 trace ("init: key %s code %x\n", name, keys[i].keysym);
107 return keys[i].keysym;
108 }
109 }
110 return 0;
111 }
112
113 static char*
trim(char * s)114 trim (char* s)
115 {
116 char *h, *t;
117
118 for (h = s; *h == ' ' || *h == '\t'; h++);
119 for (t = s + strlen (s); *t == ' ' || *t == '\t'; t--);
120 * (t+1) = 0;
121 return h;
122 }
123
124 static void
cmd_invoke_plugin_command(DB_plugin_action_t * action,int ctx)125 cmd_invoke_plugin_command (DB_plugin_action_t *action, int ctx)
126 {
127 if (action->callback) {
128 if (ctx == DDB_ACTION_CTX_MAIN) {
129 // collect stuff for 1.4 user data
130
131 // common action
132 if (action->flags & DB_ACTION_COMMON)
133 {
134 action->callback (action, NULL);
135 return;
136 }
137
138 // playlist action
139 if (action->flags & DB_ACTION_PLAYLIST)
140 {
141 ddb_playlist_t *plt = deadbeef->plt_get_curr ();
142 if (plt) {
143 action->callback (action, plt);
144 deadbeef->plt_unref (plt);
145 }
146 return;
147 }
148
149 int selected_count = 0;
150 DB_playItem_t *pit = deadbeef->pl_get_first (PL_MAIN);
151 DB_playItem_t *selected = NULL;
152 while (pit) {
153 if (deadbeef->pl_is_selected (pit))
154 {
155 if (!selected)
156 selected = pit;
157 selected_count++;
158 }
159 DB_playItem_t *next = deadbeef->pl_get_next (pit, PL_MAIN);
160 deadbeef->pl_item_unref (pit);
161 pit = next;
162 }
163
164 //Now we're checking if action is applicable:
165
166 if (selected_count == 0)
167 {
168 trace ("No tracks selected\n");
169 return;
170 }
171 if ((selected_count == 1) && (!(action->flags & DB_ACTION_SINGLE_TRACK)))
172 {
173 trace ("Hotkeys: action %s not allowed for single track\n", action->name);
174 return;
175 }
176 if ((selected_count > 1) && (!(action->flags & DB_ACTION_MULTIPLE_TRACKS)))
177 {
178 trace ("Hotkeys: action %s not allowed for multiple tracks\n", action->name);
179 return;
180 }
181
182 //So, action is allowed, do it.
183
184 if (action->flags & DB_ACTION_CAN_MULTIPLE_TRACKS)
185 {
186 action->callback (action, NULL);
187 }
188 else {
189 pit = deadbeef->pl_get_first (PL_MAIN);
190 while (pit) {
191 if (deadbeef->pl_is_selected (pit))
192 {
193 action->callback (action, pit);
194 }
195 DB_playItem_t *next = deadbeef->pl_get_next (pit, PL_MAIN);
196 deadbeef->pl_item_unref (pit);
197 pit = next;
198 }
199 }
200 }
201 }
202 else {
203 action->callback2 (action, ctx);
204 }
205 }
206
207 static DB_plugin_action_t *
find_action_by_name(const char * command)208 find_action_by_name (const char *command) {
209 // find action with this name, and add to list
210 DB_plugin_action_t *actions = NULL;
211 DB_plugin_t **plugins = deadbeef->plug_get_list ();
212 for (int i = 0; plugins[i]; i++) {
213 DB_plugin_t *p = plugins[i];
214 if (p->get_actions) {
215 actions = p->get_actions (NULL);
216 while (actions) {
217 if (actions->name && actions->title && !strcasecmp (actions->name, command)) {
218 break; // found
219 }
220 actions = actions->next;
221 }
222 if (actions) {
223 break;
224 }
225 }
226 }
227 return actions;
228 }
229
230 #ifndef __APPLE__
231 static int
get_x11_keycode(const char * name,Atom * syms,int first_kk,int last_kk,int ks_per_kk)232 get_x11_keycode (const char *name, Atom *syms, int first_kk, int last_kk, int ks_per_kk) {
233 int i, ks;
234
235 for (i = 0; i < last_kk-first_kk; i++)
236 {
237 int sym = * (syms + i*ks_per_kk);
238 for (ks = 0; keys[ks].name; ks++)
239 {
240 if ( (keys[ ks ].keysym == sym) && (0 == strcmp (name, keys[ ks ].name)))
241 {
242 return i+first_kk;
243 }
244 }
245 }
246 return 0;
247 }
248
249 static int
read_config(Display * disp)250 read_config (Display *disp) {
251 int ks_per_kk;
252 int first_kk, last_kk;
253 Atom* syms;
254
255 XDisplayKeycodes (disp, &first_kk, &last_kk);
256 syms = XGetKeyboardMapping (disp, first_kk, last_kk - first_kk, &ks_per_kk);
257 #else
258 #define ShiftMask (1<<0)
259 #define LockMask (1<<1)
260 #define ControlMask (1<<2)
261 #define Mod1Mask (1<<3)
262 #define Mod2Mask (1<<4)
263 #define Mod3Mask (1<<5)
264 #define Mod4Mask (1<<6)
265 #define Mod5Mask (1<<7)
266 int ks_per_kk = -1;
267 int first_kk = -1, last_kk = -1;
268 int* syms = NULL;
269 static int
270 read_config (void) {
271 #endif
272 DB_conf_item_t *item = deadbeef->conf_find ("hotkey.", NULL);
273 while (item) {
274 if (command_count == MAX_COMMAND_COUNT)
275 {
276 fprintf (stderr, "hotkeys: maximum number (%d) of commands exceeded\n", MAX_COMMAND_COUNT);
277 break;
278 }
279
280 command_t *cmd_entry = &commands[ command_count ];
281 memset (cmd_entry, 0, sizeof (command_t));
282
283 char token[MAX_TOKEN];
284 char keycombo[MAX_TOKEN];
285 const char *script = item->value;
286 if ((script = gettoken (script, keycombo)) == 0) {
287 trace ("hotkeys: unexpected eol (keycombo)\n");
288 goto out;
289 }
290 if ((script = gettoken (script, token)) == 0) {
291 trace ("hotkeys: unexpected eol (ctx)\n");
292 goto out;
293 }
294 cmd_entry->ctx = atoi (token);
295 if (cmd_entry->ctx < 0 || cmd_entry->ctx >= DDB_ACTION_CTX_COUNT) {
296 trace ("hotkeys: invalid ctx %d\n", cmd_entry->ctx);
297 goto out;
298 }
299 if ((script = gettoken (script, token)) == 0) {
300 trace ("hotkeys: unexpected eol (isglobal)\n");
301 goto out;
302 }
303 cmd_entry->isglobal = atoi (token);
304 if ((script = gettoken (script, token)) == 0) {
305 trace ("hotkeys: unexpected eol (action)\n");
306 goto out;
307 }
308 cmd_entry->action = find_action_by_name (token);
309 if (!cmd_entry->action) {
310 trace ("hotkeys: action not found %s\n", token);
311 goto out;
312 }
313
314 // parse key combo
315 int done = 0;
316 char* p;
317 char* space = keycombo;
318 do {
319 p = space;
320 space = strchr (p, ' ');
321 if (space) {
322 *space = 0;
323 space++;
324 }
325 else
326 done = 1;
327
328 if (0 == strcasecmp (p, "Ctrl"))
329 cmd_entry->modifier |= ControlMask;
330
331 else if (0 == strcasecmp (p, "Alt"))
332 cmd_entry->modifier |= Mod1Mask;
333
334 else if (0 == strcasecmp (p, "Shift"))
335 cmd_entry->modifier |= ShiftMask;
336
337 else if (0 == strcasecmp (p, "Super")) {
338 cmd_entry->modifier |= Mod4Mask;
339 }
340
341 else {
342 if (p[0] == '0' && p[1] == 'x') {
343 // parse hex keycode
344 int r = sscanf (p, "0x%x", &cmd_entry->keycode);
345 if (!r) {
346 cmd_entry->keycode = 0;
347 }
348 }
349 else {
350 // lookup name table
351 cmd_entry->keycode = get_keycode (p);
352 #ifndef __APPLE__
353 cmd_entry->x11_keycode = get_x11_keycode (p, syms, first_kk, last_kk, ks_per_kk);
354 trace ("%s: kc=%d, xkc=%d\n", p, cmd_entry->keycode, cmd_entry->x11_keycode);
355 #endif
356 }
357 if (!cmd_entry->keycode)
358 {
359 trace ("hotkeys: got 0 from get_keycode while adding hotkey: %s %s\n", item->key, item->value);
360 break;
361 }
362 }
363 } while (!done);
364
365 if (done) {
366 if (cmd_entry->keycode == 0) {
367 trace ("hotkeys: Key not found while parsing %s %s\n", item->key, item->value);
368 }
369 else {
370 command_count++;
371 }
372 }
373 out:
374 item = deadbeef->conf_find ("hotkey.", item);
375 }
376 #ifndef __APPLE__
377 XFree (syms);
378 int i;
379 // need to grab it here to prevent gdk_x_error from being called while we're
380 // doing it on other thread
381 for (i = 0; i < command_count; i++) {
382 if (!commands[i].isglobal) {
383 continue;
384 }
385 for (int f = 0; f < 16; f++) {
386 uint32_t flags = 0;
387 if (f & 1) {
388 flags |= LockMask;
389 }
390 if (f & 2) {
391 flags |= Mod2Mask;
392 }
393 if (f & 4) {
394 flags |= Mod3Mask;
395 }
396 if (f & 8) {
397 flags |= Mod5Mask;
398 }
399 trace ("XGrabKey %d %x\n", commands[i].keycode, commands[i].modifier | flags);
400 XGrabKey (disp, commands[i].x11_keycode, commands[i].modifier | flags, DefaultRootWindow (disp), False, GrabModeAsync, GrabModeAsync);
401 }
402 }
403 #endif
404
405 return 0;
406 }
407
408 DB_plugin_t *
409 hotkeys_load (DB_functions_t *api) {
410 deadbeef = api;
411 return DB_PLUGIN (&plugin);
412 }
413
414 static void
415 cleanup () {
416 command_count = 0;
417 #ifndef __APPLE__
418 if (disp) {
419 XCloseDisplay (disp);
420 disp = NULL;
421 }
422 #endif
423 }
424
425 #ifndef __APPLE__
426 static int
427 x_err_handler (Display *d, XErrorEvent *evt) {
428 #if 0
429 // this code crashes if gtk plugin is active
430 char buffer[1024];
431 XGetErrorText (d, evt->error_code, buffer, sizeof (buffer));
432 trace ("hotkeys: xlib error: %s\n", buffer);
433 #endif
434 return 0;
435 }
436
437 static void
438 hotkeys_event_loop (void *unused) {
439 int i;
440 #ifdef __linux__
441 prctl (PR_SET_NAME, "deadbeef-hotkeys", 0, 0, 0, 0);
442 #endif
443
444 while (!finished) {
445 if (need_reset) {
446 trace ("hotkeys: reinitializing\n");
447 XSetErrorHandler (x_err_handler);
448 for (int i = 0; i < command_count; i++) {
449 for (int f = 0; f < 16; f++) {
450 uint32_t flags = 0;
451 if (f & 1) {
452 flags |= LockMask;
453 }
454 if (f & 2) {
455 flags |= Mod2Mask;
456 }
457 if (f & 4) {
458 flags |= Mod3Mask;
459 }
460 if (f & 8) {
461 flags |= Mod5Mask;
462 }
463 XUngrabKey (disp, commands[i].x11_keycode, commands[i].modifier | flags, DefaultRootWindow (disp));
464 }
465 }
466 memset (commands, 0, sizeof (commands));
467 command_count = 0;
468 read_config (disp);
469 need_reset = 0;
470 }
471
472 XEvent event;
473 while (XPending (disp))
474 {
475 XNextEvent (disp, &event);
476
477 if (event.xkey.type == KeyPress)
478 {
479 int state = event.xkey.state;
480 // ignore caps/scroll/numlock
481 state &= (ShiftMask|ControlMask|Mod1Mask|Mod4Mask);
482 trace ("hotkeys: key %d mods %X (%X)\n", event.xkey.keycode, state, event.xkey.state);
483 trace ("filtered state=%X\n", state);
484 for (i = 0; i < command_count; i++) {
485 if ( (event.xkey.keycode == commands[ i ].x11_keycode) &&
486 (state == commands[ i ].modifier))
487 {
488 trace ("matches to commands[%d]!\n", i);
489 cmd_invoke_plugin_command (commands[i].action, commands[i].ctx);
490 break;
491 }
492 }
493 if (i == command_count) {
494 trace ("keypress doesn't match to any global hotkey\n");
495 }
496 }
497 }
498 usleep (200 * 1000);
499 }
500 }
501 #endif
502
503 static int
504 hotkeys_connect (void) {
505 #ifndef __APPLE__
506 finished = 0;
507 loop_tid = 0;
508 disp = XOpenDisplay (NULL);
509 if (!disp)
510 {
511 fprintf (stderr, "hotkeys: could not open display\n");
512 return -1;
513 }
514 XSetErrorHandler (x_err_handler);
515
516 read_config (disp);
517
518 int ks_per_kk;
519 int first_kk, last_kk;
520 Atom* syms;
521 XDisplayKeycodes (disp, &first_kk, &last_kk);
522 syms = XGetKeyboardMapping (disp, first_kk, last_kk - first_kk, &ks_per_kk);
523 init_mapped_keycodes (disp, syms, first_kk, last_kk, ks_per_kk);
524 XFree (syms);
525 XSync (disp, 0);
526 loop_tid = deadbeef->thread_start (hotkeys_event_loop, 0);
527 #else
528 read_config ();
529 #endif
530 return 0;
531 }
532
533 static int
534 hotkeys_disconnect (void) {
535 #ifndef __APPLE__
536 if (loop_tid) {
537 finished = 1;
538 deadbeef->thread_join (loop_tid);
539 }
540 #endif
541 cleanup ();
542 return 0;
543 }
544
545 const char *
546 hotkeys_get_name_for_keycode (int keycode) {
547 for (int i = 0; keys[i].name; i++) {
548 if (keycode == keys[i].keysym) {
549 return keys[i].name;
550 }
551 }
552 return NULL;
553 }
554
555
556 DB_plugin_action_t*
557 hotkeys_get_action_for_keycombo (int key, int mods, int isglobal, int *ctx) {
558 int i;
559 // find mapped keycode
560
561 if (key < 0x7f && isupper (key)) {
562 key = tolower (key);
563 }
564
565 int keycode = key;
566
567 trace ("hotkeys: keysym 0x%X mapped to 0x%X\n", key, keycode);
568
569
570 for (i = 0; i < command_count; i++) {
571 trace ("hotkeys: command %s keycode %x mods %x\n", commands[i].action->name, commands[i].keycode, commands[i].modifier);
572 if (commands[i].keycode == keycode && commands[i].modifier == mods && commands[i].isglobal == isglobal) {
573 *ctx = commands[i].ctx;
574 return commands[i].action;
575 }
576 }
577 return NULL;
578 }
579
580 void
581 hotkeys_reset (void) {
582 #ifndef __APPLE__
583 need_reset = 1;
584 trace ("hotkeys: reset flagged\n");
585 #endif
586 }
587
588 int
589 action_play_cb (struct DB_plugin_action_s *action, int ctx) {
590 // NOTE: this function is copied as on_playbtn_clicked in gtkui
591 DB_output_t *output = deadbeef->get_output ();
592 if (output->state () == OUTPUT_STATE_PAUSED) {
593 ddb_playlist_t *plt = deadbeef->plt_get_curr ();
594 int cur = deadbeef->plt_get_cursor (plt, PL_MAIN);
595 if (cur != -1) {
596 ddb_playItem_t *it = deadbeef->plt_get_item_for_idx (plt, cur, PL_MAIN);
597 ddb_playItem_t *it_playing = deadbeef->streamer_get_playing_track ();
598 if (it) {
599 deadbeef->pl_item_unref (it);
600 }
601 if (it_playing) {
602 deadbeef->pl_item_unref (it_playing);
603 }
604 if (it != it_playing) {
605 deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, cur, 0);
606 }
607 else {
608 deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0);
609 }
610 }
611 else {
612 deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0);
613 }
614 deadbeef->plt_unref (plt);
615 }
616 else {
617 ddb_playlist_t *plt = deadbeef->plt_get_curr ();
618 int cur = -1;
619 if (plt) {
620 cur = deadbeef->plt_get_cursor (plt, PL_MAIN);
621 deadbeef->plt_unref (plt);
622 }
623 if (cur == -1) {
624 cur = 0;
625 }
626 deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, cur, 0);
627 }
628 return 0;
629 }
630
631 int
632 action_prev_cb (struct DB_plugin_action_s *action, int ctx) {
633 deadbeef->sendmessage (DB_EV_PREV, 0, 0, 0);
634 return 0;
635 }
636
637 int
638 action_next_cb (struct DB_plugin_action_s *action, int ctx) {
639 deadbeef->sendmessage (DB_EV_NEXT, 0, 0, 0);
640 return 0;
641 }
642
643 int
644 action_stop_cb (struct DB_plugin_action_s *action, int ctx) {
645 deadbeef->sendmessage (DB_EV_STOP, 0, 0, 0);
646 return 0;
647 }
648
649 int
650 action_toggle_pause_cb (struct DB_plugin_action_s *action, int ctx) {
651 deadbeef->sendmessage (DB_EV_TOGGLE_PAUSE, 0, 0, 0);
652 return 0;
653 }
654
655 int
656 action_play_pause_cb (struct DB_plugin_action_s *action, int ctx) {
657 int state = deadbeef->get_output ()->state ();
658 if (state == OUTPUT_STATE_PLAYING) {
659 deadbeef->sendmessage (DB_EV_PAUSE, 0, 0, 0);
660 }
661 else {
662 deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0);
663 }
664 return 0;
665 }
666
667 int
668 action_play_random_cb (struct DB_plugin_action_s *action, int ctx) {
669 deadbeef->sendmessage (DB_EV_PLAY_RANDOM, 0, 0, 0);
670 return 0;
671 }
672
673 int
674 action_seek_5p_forward_cb (struct DB_plugin_action_s *action, int ctx) {
675 deadbeef->pl_lock ();
676 DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
677 if (it) {
678 float dur = deadbeef->pl_get_item_duration (it);
679 if (dur > 0) {
680 float pos = deadbeef->streamer_get_playpos ();
681 deadbeef->sendmessage (DB_EV_SEEK, 0, (pos + dur * 0.05f) * 1000, 0);
682 }
683 deadbeef->pl_item_unref (it);
684 }
685 deadbeef->pl_unlock ();
686 return 0;
687 }
688
689 int
690 action_seek_5p_backward_cb (struct DB_plugin_action_s *action, int ctx) {
691 deadbeef->pl_lock ();
692 DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
693 if (it) {
694 float dur = deadbeef->pl_get_item_duration (it);
695 if (dur > 0) {
696 float pos = deadbeef->streamer_get_playpos ();
697 pos = (pos - dur * 0.05f) * 1000;
698 if (pos < 0) {
699 pos = 0;
700 }
701
702 deadbeef->sendmessage (DB_EV_SEEK, 0, pos, 0);
703 }
704 deadbeef->pl_item_unref (it);
705 }
706 deadbeef->pl_unlock ();
707 return 0;
708 }
709
710 int
711 action_seek_1p_forward_cb (struct DB_plugin_action_s *action, int ctx) {
712 deadbeef->pl_lock ();
713 DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
714 if (it) {
715 float dur = deadbeef->pl_get_item_duration (it);
716 if (dur > 0) {
717 float pos = deadbeef->streamer_get_playpos ();
718 deadbeef->sendmessage (DB_EV_SEEK, 0, (pos + dur * 0.01f) * 1000, 0);
719 }
720 deadbeef->pl_item_unref (it);
721 }
722 deadbeef->pl_unlock ();
723 return 0;
724 }
725
726 int
727 action_seek_1p_backward_cb (struct DB_plugin_action_s *action, int ctx) {
728 deadbeef->pl_lock ();
729 DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
730 if (it) {
731 float dur = deadbeef->pl_get_item_duration (it);
732 if (dur > 0) {
733 float pos = deadbeef->streamer_get_playpos ();
734 pos = (pos - dur * 0.01f) * 1000;
735 if (pos < 0) {
736 pos = 0;
737 }
738 deadbeef->sendmessage (DB_EV_SEEK, 0, pos, 0);
739 }
740 deadbeef->pl_item_unref (it);
741 }
742 deadbeef->pl_unlock ();
743 return 0;
744 }
745
746 static int
747 seek_sec (float sec) {
748 deadbeef->pl_lock ();
749 DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
750 if (it) {
751 float dur = deadbeef->pl_get_item_duration (it);
752 if (dur > 0) {
753 float pos = deadbeef->streamer_get_playpos ();
754 pos += sec;
755 if (pos < 0) {
756 pos = 0;
757 }
758 deadbeef->sendmessage (DB_EV_SEEK, 0, pos * 1000, 0);
759 }
760 deadbeef->pl_item_unref (it);
761 }
762 deadbeef->pl_unlock ();
763 return 0;
764 }
765
766 int
767 action_seek_1s_forward_cb (struct DB_plugin_action_s *action, int ctx) {
768 return seek_sec (1.f);
769 }
770
771 int
772 action_seek_1s_backward_cb (struct DB_plugin_action_s *action, int ctx) {
773 return seek_sec (-1.f);
774 }
775
776 int
777 action_seek_5s_forward_cb (struct DB_plugin_action_s *action, int ctx) {
778 return seek_sec (5.f);
779 }
780
781 int
782 action_seek_5s_backward_cb (struct DB_plugin_action_s *action, int ctx) {
783 return seek_sec (-5.f);
784 }
785
786 int
787 action_volume_up_cb (struct DB_plugin_action_s *action, int ctx) {
788 deadbeef->volume_set_db (deadbeef->volume_get_db () + 1);
789 return 0;
790 }
791
792 int
793 action_volume_down_cb (struct DB_plugin_action_s *action, int ctx) {
794 deadbeef->volume_set_db (deadbeef->volume_get_db () - 1);
795 return 0;
796 }
797
798 int
799 action_toggle_stop_after_current_cb (struct DB_plugin_action_s *action, int ctx) {
800 int var = deadbeef->conf_get_int ("playlist.stop_after_current", 0);
801 var = 1 - var;
802 deadbeef->conf_set_int ("playlist.stop_after_current", var);
803 deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
804 return 0;
805 }
806
807 int
808 action_toggle_stop_after_album_cb (struct DB_plugin_action_s *action, int ctx) {
809 int var = deadbeef->conf_get_int ("playlist.stop_after_album", 0);
810 var = 1 - var;
811 deadbeef->conf_set_int ("playlist.stop_after_album", var);
812 deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
813 return 0;
814 }
815
816 static DB_plugin_action_t action_reload_metadata = {
817 .title = "Reload Metadata",
818 .name = "reload_metadata",
819 .flags = DB_ACTION_MULTIPLE_TRACKS,
820 .callback2 = action_reload_metadata_handler,
821 .next = NULL
822 };
823
824 static DB_plugin_action_t action_jump_to_current = {
825 .title = "Playback/Jump To Currently Playing Track",
826 .name = "jump_to_current_track",
827 .flags = DB_ACTION_COMMON,
828 .callback2 = action_jump_to_current_handler,
829 .next = &action_reload_metadata
830 };
831
832 static DB_plugin_action_t action_skip_to_prev_genre = {
833 .title = "Playback/Skip to/Previous genre",
834 .name = "skip_to_prev_genre",
835 .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
836 .callback2 = action_skip_to_prev_genre_handler,
837 .next = &action_jump_to_current
838 };
839
840 static DB_plugin_action_t action_skip_to_prev_composer = {
841 .title = "Playback/Skip to/Previous composer",
842 .name = "skip_to_prev_composer",
843 .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
844 .callback2 = action_skip_to_prev_composer_handler,
845 .next = &action_skip_to_prev_genre
846 };
847
848 static DB_plugin_action_t action_skip_to_prev_artist = {
849 .title = "Playback/Skip to/Previous artist",
850 .name = "skip_to_prev_artist",
851 .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
852 .callback2 = action_skip_to_prev_artist_handler,
853 .next = &action_skip_to_prev_composer
854 };
855
856 static DB_plugin_action_t action_skip_to_prev_album = {
857 .title = "Playback/Skip to/Previous album",
858 .name = "skip_to_prev_album",
859 .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
860 .callback2 = action_skip_to_prev_album_handler,
861 .next = &action_skip_to_prev_artist
862 };
863
864 static DB_plugin_action_t action_skip_to_next_genre = {
865 .title = "Playback/Skip to/Next genre",
866 .name = "skip_to_next_genre",
867 .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
868 .callback2 = action_skip_to_next_genre_handler,
869 .next = &action_skip_to_prev_album
870 };
871
872 static DB_plugin_action_t action_skip_to_next_composer = {
873 .title = "Playback/Skip to/Next composer",
874 .name = "skip_to_next_composer",
875 .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
876 .callback2 = action_skip_to_next_composer_handler,
877 .next = &action_skip_to_next_genre
878 };
879
880 static DB_plugin_action_t action_skip_to_next_artist = {
881 .title = "Playback/Skip to/Next artist",
882 .name = "skip_to_next_artist",
883 .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
884 .callback2 = action_skip_to_next_artist_handler,
885 .next = &action_skip_to_next_composer
886 };
887
888 static DB_plugin_action_t action_skip_to_next_album = {
889 .title = "Playback/Skip to/Next album",
890 .name = "skip_to_next_album",
891 .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
892 .callback2 = action_skip_to_next_album_handler,
893 .next = &action_skip_to_next_artist
894 };
895
896 static DB_plugin_action_t action_next_playlist = {
897 .title = "Next Playlist",
898 .name = "next_playlist",
899 .flags = DB_ACTION_COMMON,
900 .callback2 = action_next_playlist_handler,
901 .next = &action_skip_to_next_album
902 };
903
904 static DB_plugin_action_t action_prev_playlist = {
905 .title = "Prev Playlist",
906 .name = "prev_playlist",
907 .flags = DB_ACTION_COMMON,
908 .callback2 = action_prev_playlist_handler,
909 .next = &action_next_playlist
910 };
911
912 static DB_plugin_action_t action_playlist10 = {
913 .title = "Switch To Playlist 10",
914 .name = "playlist10",
915 .flags = DB_ACTION_COMMON,
916 .callback2 = action_playlist10_handler,
917 .next = &action_prev_playlist
918 };
919
920 static DB_plugin_action_t action_playlist9 = {
921 .title = "Switch To Playlist 9",
922 .name = "playlist9",
923 .flags = DB_ACTION_COMMON,
924 .callback2 = action_playlist9_handler,
925 .next = &action_playlist10
926 };
927
928 static DB_plugin_action_t action_playlist8 = {
929 .title = "Switch To Playlist 8",
930 .name = "playlist8",
931 .flags = DB_ACTION_COMMON,
932 .callback2 = action_playlist8_handler,
933 .next = &action_playlist9
934 };
935
936 static DB_plugin_action_t action_playlist7 = {
937 .title = "Switch To Playlist 7",
938 .name = "playlist7",
939 .flags = DB_ACTION_COMMON,
940 .callback2 = action_playlist7_handler,
941 .next = &action_playlist8
942 };
943
944 static DB_plugin_action_t action_playlist6 = {
945 .title = "Switch To Playlist 6",
946 .name = "playlist6",
947 .flags = DB_ACTION_COMMON,
948 .callback2 = action_playlist6_handler,
949 .next = &action_playlist7
950 };
951
952 static DB_plugin_action_t action_playlist5 = {
953 .title = "Switch To Playlist 5",
954 .name = "playlist5",
955 .flags = DB_ACTION_COMMON,
956 .callback2 = action_playlist5_handler,
957 .next = &action_playlist6
958 };
959
960 static DB_plugin_action_t action_playlist4 = {
961 .title = "Switch To Playlist 4",
962 .name = "playlist4",
963 .flags = DB_ACTION_COMMON,
964 .callback2 = action_playlist4_handler,
965 .next = &action_playlist5
966 };
967
968 static DB_plugin_action_t action_playlist3 = {
969 .title = "Switch To Playlist 3",
970 .name = "playlist3",
971 .flags = DB_ACTION_COMMON,
972 .callback2 = action_playlist3_handler,
973 .next = &action_playlist4
974 };
975
976 static DB_plugin_action_t action_playlist2 = {
977 .title = "Switch To Playlist 2",
978 .name = "playlist2",
979 .flags = DB_ACTION_COMMON,
980 .callback2 = action_playlist2_handler,
981 .next = &action_playlist3
982 };
983
984 static DB_plugin_action_t action_playlist1 = {
985 .title = "Switch To Playlist 1",
986 .name = "playlist1",
987 .flags = DB_ACTION_COMMON,
988 .callback2 = action_playlist1_handler,
989 .next = &action_playlist2
990 };
991
992 static DB_plugin_action_t action_sort_randomize = {
993 .title = "Edit/Sort Randomize",
994 .name = "sort_randomize",
995 .flags = DB_ACTION_COMMON,
996 .callback2 = action_sort_randomize_handler,
997 .next = &action_playlist1
998 };
999
1000 static DB_plugin_action_t action_sort_by_date = {
1001 .title = "Edit/Sort By Date",
1002 .name = "sort_date",
1003 .flags = DB_ACTION_COMMON,
1004 .callback2 = action_sort_by_date_handler,
1005 .next = &action_sort_randomize
1006 };
1007
1008 static DB_plugin_action_t action_sort_by_artist = {
1009 .title = "Edit/Sort By Artist",
1010 .name = "sort_artist",
1011 .flags = DB_ACTION_COMMON,
1012 .callback2 = action_sort_by_artist_handler,
1013 .next = &action_sort_by_date
1014 };
1015
1016
1017 static DB_plugin_action_t action_sort_by_album = {
1018 .title = "Edit/Sort By Album",
1019 .name = "sort_album",
1020 .flags = DB_ACTION_COMMON,
1021 .callback2 = action_sort_by_album_handler,
1022 .next = &action_sort_by_artist
1023 };
1024
1025 static DB_plugin_action_t action_sort_by_tracknr = {
1026 .title = "Edit/Sort By Track Number",
1027 .name = "sort_tracknr",
1028 .flags = DB_ACTION_COMMON,
1029 .callback2 = action_sort_by_tracknr_handler,
1030 .next = &action_sort_by_album
1031 };
1032
1033 static DB_plugin_action_t action_sort_by_title = {
1034 .title = "Edit/Sort By Title",
1035 .name = "sort_title",
1036 .flags = DB_ACTION_COMMON,
1037 .callback2 = action_sort_by_title_handler,
1038 .next = &action_sort_by_tracknr
1039 };
1040
1041 static DB_plugin_action_t action_invert_selection = {
1042 .title = "Edit/Invert Selection",
1043 .name = "invert_selection",
1044 .flags = DB_ACTION_COMMON,
1045 .callback2 = action_invert_selection_handler,
1046 .next = &action_sort_by_tracknr
1047 };
1048
1049 static DB_plugin_action_t action_clear_playlist = {
1050 .title = "Edit/Clear Playlist",
1051 .name = "clear_playlist",
1052 .flags = DB_ACTION_COMMON,
1053 .callback2 = action_clear_playlist_handler,
1054 .next = &action_invert_selection
1055 };
1056
1057 static DB_plugin_action_t action_remove_from_playqueue = {
1058 .title = "Playback/Remove From Playback Queue",
1059 .name = "remove_from_playback_queue",
1060 .flags = DB_ACTION_MULTIPLE_TRACKS,
1061 .callback2 = action_remove_from_playqueue_handler,
1062 .next = &action_clear_playlist
1063 };
1064
1065 static DB_plugin_action_t action_add_to_playqueue = {
1066 .title = "Playback/Add To Playback Queue",
1067 .name = "add_to_playback_queue",
1068 .flags = DB_ACTION_MULTIPLE_TRACKS,
1069 .callback2 = action_add_to_playqueue_handler,
1070 .next = &action_remove_from_playqueue
1071 };
1072
1073 static DB_plugin_action_t action_toggle_mute = {
1074 .title = "Playback/Toggle Mute",
1075 .name = "toggle_mute",
1076 .flags = DB_ACTION_COMMON,
1077 .callback2 = action_toggle_mute_handler,
1078 .next = &action_add_to_playqueue
1079 };
1080
1081 static DB_plugin_action_t action_play = {
1082 .title = "Playback/Play",
1083 .name = "play",
1084 .flags = DB_ACTION_COMMON,
1085 .callback2 = action_play_cb,
1086 .next = &action_toggle_mute
1087 };
1088
1089 static DB_plugin_action_t action_stop = {
1090 .title = "Playback/Stop",
1091 .name = "stop",
1092 .flags = DB_ACTION_COMMON,
1093 .callback2 = action_stop_cb,
1094 .next = &action_play
1095 };
1096
1097 static DB_plugin_action_t action_prev = {
1098 .title = "Playback/Previous",
1099 .name = "prev",
1100 .flags = DB_ACTION_COMMON,
1101 .callback2 = action_prev_cb,
1102 .next = &action_stop
1103 };
1104
1105 static DB_plugin_action_t action_next = {
1106 .title = "Playback/Next",
1107 .name = "next",
1108 .flags = DB_ACTION_COMMON,
1109 .callback2 = action_next_cb,
1110 .next = &action_prev
1111 };
1112
1113 static DB_plugin_action_t action_toggle_pause = {
1114 .title = "Playback/Toggle Pause",
1115 .name = "toggle_pause",
1116 .flags = DB_ACTION_COMMON,
1117 .callback2 = action_toggle_pause_cb,
1118 .next = &action_next
1119 };
1120
1121 static DB_plugin_action_t action_play_pause = {
1122 .title = "Playback/Play\\/Pause",
1123 .name = "play_pause",
1124 .flags = DB_ACTION_COMMON,
1125 .callback2 = action_play_pause_cb,
1126 .next = &action_toggle_pause
1127 };
1128
1129 static DB_plugin_action_t action_play_random = {
1130 .title = "Playback/Play Random",
1131 .name = "playback_random",
1132 .flags = DB_ACTION_COMMON,
1133 .callback2 = action_play_random_cb,
1134 .next = &action_play_pause
1135 };
1136
1137 static DB_plugin_action_t action_seek_1s_forward = {
1138 .title = "Playback/Seek 1s Forward",
1139 .name = "seek_1s_fwd",
1140 .flags = DB_ACTION_COMMON,
1141 .callback2 = action_seek_1s_forward_cb,
1142 .next = &action_play_random
1143 };
1144
1145 static DB_plugin_action_t action_seek_1s_backward = {
1146 .title = "Playback/Seek 1s Backward",
1147 .name = "seek_1s_back",
1148 .flags = DB_ACTION_COMMON,
1149 .callback2 = action_seek_1s_backward_cb,
1150 .next = &action_seek_1s_forward
1151 };
1152
1153 static DB_plugin_action_t action_seek_5s_forward = {
1154 .title = "Playback/Seek 5s Forward",
1155 .name = "seek_5s_fwd",
1156 .flags = DB_ACTION_COMMON,
1157 .callback2 = action_seek_5s_forward_cb,
1158 .next = &action_seek_1s_backward
1159 };
1160
1161 static DB_plugin_action_t action_seek_5s_backward = {
1162 .title = "Playback/Seek 5s Backward",
1163 .name = "seek_5s_back",
1164 .flags = DB_ACTION_COMMON,
1165 .callback2 = action_seek_5s_backward_cb,
1166 .next = &action_seek_5s_forward
1167 };
1168
1169
1170 static DB_plugin_action_t action_seek_1p_forward = {
1171 .title = "Playback/Seek 1% Forward",
1172 .name = "seek_1p_fwd",
1173 .flags = DB_ACTION_COMMON,
1174 .callback2 = action_seek_1p_forward_cb,
1175 .next = &action_seek_5s_backward
1176 };
1177
1178 static DB_plugin_action_t action_seek_1p_backward = {
1179 .title = "Playback/Seek 1% Backward",
1180 .name = "seek_1p_back",
1181 .flags = DB_ACTION_COMMON,
1182 .callback2 = action_seek_1p_backward_cb,
1183 .next = &action_seek_1p_forward
1184 };
1185
1186 static DB_plugin_action_t action_seek_5p_forward = {
1187 .title = "Playback/Seek 5% Forward",
1188 .name = "seek_5p_fwd",
1189 .flags = DB_ACTION_COMMON,
1190 .callback2 = action_seek_5p_forward_cb,
1191 .next = &action_seek_1p_backward
1192 };
1193
1194 static DB_plugin_action_t action_seek_5p_backward = {
1195 .title = "Playback/Seek 5% Backward",
1196 .name = "seek_5p_back",
1197 .flags = DB_ACTION_COMMON,
1198 .callback2 = action_seek_5p_backward_cb,
1199 .next = &action_seek_5p_forward
1200 };
1201
1202 static DB_plugin_action_t action_volume_up = {
1203 .title = "Playback/Volume Up",
1204 .name = "volume_up",
1205 .flags = DB_ACTION_COMMON,
1206 .callback2 = action_volume_up_cb,
1207 .next = &action_seek_5p_backward
1208 };
1209
1210 static DB_plugin_action_t action_volume_down = {
1211 .title = "Playback/Volume Down",
1212 .name = "volume_down",
1213 .flags = DB_ACTION_COMMON,
1214 .callback2 = action_volume_down_cb,
1215 .next = &action_volume_up
1216 };
1217
1218 static DB_plugin_action_t action_toggle_stop_after_current = {
1219 .title = "Playback/Toggle Stop After Current Track",
1220 .name = "toggle_stop_after_current",
1221 .flags = DB_ACTION_COMMON,
1222 .callback2 = action_toggle_stop_after_current_cb,
1223 .next = &action_volume_down
1224 };
1225
1226 static DB_plugin_action_t action_toggle_stop_after_album = {
1227 .title = "Playback/Toggle Stop After Current Album",
1228 .name = "toggle_stop_after_album",
1229 .flags = DB_ACTION_COMMON,
1230 .callback2 = action_toggle_stop_after_album_cb,
1231 .next = &action_toggle_stop_after_current
1232 };
1233
1234 static DB_plugin_action_t *
1235 hotkeys_get_actions (DB_playItem_t *it)
1236 {
1237 return &action_toggle_stop_after_album;
1238 }
1239
1240 // define plugin interface
1241 static DB_hotkeys_plugin_t plugin = {
1242 .misc.plugin.api_vmajor = 1,
1243 .misc.plugin.api_vminor = 5,
1244 .misc.plugin.version_major = 1,
1245 .misc.plugin.version_minor = 1,
1246 .misc.plugin.type = DB_PLUGIN_MISC,
1247 .misc.plugin.id = "hotkeys",
1248 .misc.plugin.name = "Hotkey manager",
1249 .misc.plugin.descr =
1250 "Manages local and global hotkeys, and executes actions when the assigned key combinations are pressed\n\n"
1251 "This plugin has its own API, to allow 3rd party GUI plugins to reuse the code.\n"
1252 "Check the plugins/hotkeys/hotkeys.h in the source tree if you need this.\n\n"
1253 "Changes in version 1.1\n"
1254 " * adaptation to new deadbeef 0.6 plugin API\n"
1255 " * added local hotkeys support\n"
1256 ,
1257 .misc.plugin.copyright =
1258 "Copyright (C) 2012-2013 Alexey Yakovenko <waker@users.sourceforge.net>\n"
1259 "Copyright (C) 2009-2011 Viktor Semykin <thesame.ml@gmail.com>\n"
1260 "\n"
1261 "This program is free software; you can redistribute it and/or\n"
1262 "modify it under the terms of the GNU General Public License\n"
1263 "as published by the Free Software Foundation; either version 2\n"
1264 "of the License, or (at your option) any later version.\n"
1265 "\n"
1266 "This program is distributed in the hope that it will be useful,\n"
1267 "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
1268 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
1269 "GNU General Public License for more details.\n"
1270 "\n"
1271 "You should have received a copy of the GNU General Public License\n"
1272 "along with this program; if not, write to the Free Software\n"
1273 "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n"
1274 ,
1275 .misc.plugin.website = "http://deadbeef.sf.net",
1276 .misc.plugin.get_actions = hotkeys_get_actions,
1277 .misc.plugin.start = hotkeys_connect,
1278 .misc.plugin.stop = hotkeys_disconnect,
1279 .get_name_for_keycode = hotkeys_get_name_for_keycode,
1280 .get_action_for_keycombo = hotkeys_get_action_for_keycombo,
1281 .reset = hotkeys_reset,
1282 };
1283
1284