/* Hotkeys plugin for DeaDBeeF Copyright (C) 2009-2011 Viktor Semykin Copyright (C) 2012-2013 Alexey Yakovenko This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */ #include #include #include #include #ifndef __APPLE__ #include #endif #include #ifdef __linux__ #include #endif #include "../libparser/parser.h" #include "hotkeys.h" #include "../../deadbeef.h" #include "actionhandlers.h" //#define trace(...) { fprintf(stderr, __VA_ARGS__); } #define trace(fmt,...) static DB_hotkeys_plugin_t plugin; DB_functions_t *deadbeef; #ifndef __APPLE__ static int finished; static Display *disp; static intptr_t loop_tid; static int need_reset = 0; #endif #define MAX_COMMAND_COUNT 256 typedef struct { const char *name; int keysym; #ifndef __APPLE__ int keycode; // after mapping #endif } xkey_t; #define KEY(kname, kcode) { .name=kname, .keysym=kcode}, static xkey_t keys[] = { #include "keysyms.inc" }; typedef struct command_s { int keycode; #ifndef __APPLE__ int x11_keycode; #endif int modifier; int ctx; int isglobal; DB_plugin_action_t *action; } command_t; static command_t commands [MAX_COMMAND_COUNT]; static int command_count = 0; #ifndef __APPLE__ static void init_mapped_keycodes (Display *disp, Atom *syms, int first_kk, int last_kk, int ks_per_kk) { int i, ks; for (i = 0; i < last_kk-first_kk; i++) { int sym = * (syms + i*ks_per_kk); for (ks = 0; keys[ks].name; ks++) { if (keys[ ks ].keysym == sym) { keys[ks].keycode = i+first_kk; } } } } #endif static int get_keycode (const char* name) { for (int i = 0; keys[i].name; i++) { if (!strcmp (name, keys[i].name)) { trace ("init: key %s code %x\n", name, keys[i].keysym); return keys[i].keysym; } } return 0; } static char* trim (char* s) { char *h, *t; for (h = s; *h == ' ' || *h == '\t'; h++); for (t = s + strlen (s); *t == ' ' || *t == '\t'; t--); * (t+1) = 0; return h; } static void cmd_invoke_plugin_command (DB_plugin_action_t *action, int ctx) { if (action->callback) { if (ctx == DDB_ACTION_CTX_MAIN) { // collect stuff for 1.4 user data // common action if (action->flags & DB_ACTION_COMMON) { action->callback (action, NULL); return; } // playlist action if (action->flags & DB_ACTION_PLAYLIST) { ddb_playlist_t *plt = deadbeef->plt_get_curr (); if (plt) { action->callback (action, plt); deadbeef->plt_unref (plt); } return; } int selected_count = 0; DB_playItem_t *pit = deadbeef->pl_get_first (PL_MAIN); DB_playItem_t *selected = NULL; while (pit) { if (deadbeef->pl_is_selected (pit)) { if (!selected) selected = pit; selected_count++; } DB_playItem_t *next = deadbeef->pl_get_next (pit, PL_MAIN); deadbeef->pl_item_unref (pit); pit = next; } //Now we're checking if action is applicable: if (selected_count == 0) { trace ("No tracks selected\n"); return; } if ((selected_count == 1) && (!(action->flags & DB_ACTION_SINGLE_TRACK))) { trace ("Hotkeys: action %s not allowed for single track\n", action->name); return; } if ((selected_count > 1) && (!(action->flags & DB_ACTION_MULTIPLE_TRACKS))) { trace ("Hotkeys: action %s not allowed for multiple tracks\n", action->name); return; } //So, action is allowed, do it. if (action->flags & DB_ACTION_CAN_MULTIPLE_TRACKS) { action->callback (action, NULL); } else { pit = deadbeef->pl_get_first (PL_MAIN); while (pit) { if (deadbeef->pl_is_selected (pit)) { action->callback (action, pit); } DB_playItem_t *next = deadbeef->pl_get_next (pit, PL_MAIN); deadbeef->pl_item_unref (pit); pit = next; } } } } else { action->callback2 (action, ctx); } } static DB_plugin_action_t * find_action_by_name (const char *command) { // find action with this name, and add to list DB_plugin_action_t *actions = NULL; DB_plugin_t **plugins = deadbeef->plug_get_list (); for (int i = 0; plugins[i]; i++) { DB_plugin_t *p = plugins[i]; if (p->get_actions) { actions = p->get_actions (NULL); while (actions) { if (actions->name && actions->title && !strcasecmp (actions->name, command)) { break; // found } actions = actions->next; } if (actions) { break; } } } return actions; } #ifndef __APPLE__ static int get_x11_keycode (const char *name, Atom *syms, int first_kk, int last_kk, int ks_per_kk) { int i, ks; for (i = 0; i < last_kk-first_kk; i++) { int sym = * (syms + i*ks_per_kk); for (ks = 0; keys[ks].name; ks++) { if ( (keys[ ks ].keysym == sym) && (0 == strcmp (name, keys[ ks ].name))) { return i+first_kk; } } } return 0; } static int read_config (Display *disp) { int ks_per_kk; int first_kk, last_kk; Atom* syms; XDisplayKeycodes (disp, &first_kk, &last_kk); syms = XGetKeyboardMapping (disp, first_kk, last_kk - first_kk, &ks_per_kk); #else #define ShiftMask (1<<0) #define LockMask (1<<1) #define ControlMask (1<<2) #define Mod1Mask (1<<3) #define Mod2Mask (1<<4) #define Mod3Mask (1<<5) #define Mod4Mask (1<<6) #define Mod5Mask (1<<7) int ks_per_kk = -1; int first_kk = -1, last_kk = -1; int* syms = NULL; static int read_config (void) { #endif DB_conf_item_t *item = deadbeef->conf_find ("hotkey.", NULL); while (item) { if (command_count == MAX_COMMAND_COUNT) { fprintf (stderr, "hotkeys: maximum number (%d) of commands exceeded\n", MAX_COMMAND_COUNT); break; } command_t *cmd_entry = &commands[ command_count ]; memset (cmd_entry, 0, sizeof (command_t)); char token[MAX_TOKEN]; char keycombo[MAX_TOKEN]; const char *script = item->value; if ((script = gettoken (script, keycombo)) == 0) { trace ("hotkeys: unexpected eol (keycombo)\n"); goto out; } if ((script = gettoken (script, token)) == 0) { trace ("hotkeys: unexpected eol (ctx)\n"); goto out; } cmd_entry->ctx = atoi (token); if (cmd_entry->ctx < 0 || cmd_entry->ctx >= DDB_ACTION_CTX_COUNT) { trace ("hotkeys: invalid ctx %d\n", cmd_entry->ctx); goto out; } if ((script = gettoken (script, token)) == 0) { trace ("hotkeys: unexpected eol (isglobal)\n"); goto out; } cmd_entry->isglobal = atoi (token); if ((script = gettoken (script, token)) == 0) { trace ("hotkeys: unexpected eol (action)\n"); goto out; } cmd_entry->action = find_action_by_name (token); if (!cmd_entry->action) { trace ("hotkeys: action not found %s\n", token); goto out; } // parse key combo int done = 0; char* p; char* space = keycombo; do { p = space; space = strchr (p, ' '); if (space) { *space = 0; space++; } else done = 1; if (0 == strcasecmp (p, "Ctrl")) cmd_entry->modifier |= ControlMask; else if (0 == strcasecmp (p, "Alt")) cmd_entry->modifier |= Mod1Mask; else if (0 == strcasecmp (p, "Shift")) cmd_entry->modifier |= ShiftMask; else if (0 == strcasecmp (p, "Super")) { cmd_entry->modifier |= Mod4Mask; } else { if (p[0] == '0' && p[1] == 'x') { // parse hex keycode int r = sscanf (p, "0x%x", &cmd_entry->keycode); if (!r) { cmd_entry->keycode = 0; } } else { // lookup name table cmd_entry->keycode = get_keycode (p); #ifndef __APPLE__ cmd_entry->x11_keycode = get_x11_keycode (p, syms, first_kk, last_kk, ks_per_kk); trace ("%s: kc=%d, xkc=%d\n", p, cmd_entry->keycode, cmd_entry->x11_keycode); #endif } if (!cmd_entry->keycode) { trace ("hotkeys: got 0 from get_keycode while adding hotkey: %s %s\n", item->key, item->value); break; } } } while (!done); if (done) { if (cmd_entry->keycode == 0) { trace ("hotkeys: Key not found while parsing %s %s\n", item->key, item->value); } else { command_count++; } } out: item = deadbeef->conf_find ("hotkey.", item); } #ifndef __APPLE__ XFree (syms); int i; // need to grab it here to prevent gdk_x_error from being called while we're // doing it on other thread for (i = 0; i < command_count; i++) { if (!commands[i].isglobal) { continue; } for (int f = 0; f < 16; f++) { uint32_t flags = 0; if (f & 1) { flags |= LockMask; } if (f & 2) { flags |= Mod2Mask; } if (f & 4) { flags |= Mod3Mask; } if (f & 8) { flags |= Mod5Mask; } trace ("XGrabKey %d %x\n", commands[i].keycode, commands[i].modifier | flags); XGrabKey (disp, commands[i].x11_keycode, commands[i].modifier | flags, DefaultRootWindow (disp), False, GrabModeAsync, GrabModeAsync); } } #endif return 0; } DB_plugin_t * hotkeys_load (DB_functions_t *api) { deadbeef = api; return DB_PLUGIN (&plugin); } static void cleanup () { command_count = 0; #ifndef __APPLE__ if (disp) { XCloseDisplay (disp); disp = NULL; } #endif } #ifndef __APPLE__ static int x_err_handler (Display *d, XErrorEvent *evt) { #if 0 // this code crashes if gtk plugin is active char buffer[1024]; XGetErrorText (d, evt->error_code, buffer, sizeof (buffer)); trace ("hotkeys: xlib error: %s\n", buffer); #endif return 0; } static void hotkeys_event_loop (void *unused) { int i; #ifdef __linux__ prctl (PR_SET_NAME, "deadbeef-hotkeys", 0, 0, 0, 0); #endif while (!finished) { if (need_reset) { trace ("hotkeys: reinitializing\n"); XSetErrorHandler (x_err_handler); for (int i = 0; i < command_count; i++) { for (int f = 0; f < 16; f++) { uint32_t flags = 0; if (f & 1) { flags |= LockMask; } if (f & 2) { flags |= Mod2Mask; } if (f & 4) { flags |= Mod3Mask; } if (f & 8) { flags |= Mod5Mask; } XUngrabKey (disp, commands[i].x11_keycode, commands[i].modifier | flags, DefaultRootWindow (disp)); } } memset (commands, 0, sizeof (commands)); command_count = 0; read_config (disp); need_reset = 0; } XEvent event; while (XPending (disp)) { XNextEvent (disp, &event); if (event.xkey.type == KeyPress) { int state = event.xkey.state; // ignore caps/scroll/numlock state &= (ShiftMask|ControlMask|Mod1Mask|Mod4Mask); trace ("hotkeys: key %d mods %X (%X)\n", event.xkey.keycode, state, event.xkey.state); trace ("filtered state=%X\n", state); for (i = 0; i < command_count; i++) { if ( (event.xkey.keycode == commands[ i ].x11_keycode) && (state == commands[ i ].modifier)) { trace ("matches to commands[%d]!\n", i); cmd_invoke_plugin_command (commands[i].action, commands[i].ctx); break; } } if (i == command_count) { trace ("keypress doesn't match to any global hotkey\n"); } } } usleep (200 * 1000); } } #endif static int hotkeys_connect (void) { #ifndef __APPLE__ finished = 0; loop_tid = 0; disp = XOpenDisplay (NULL); if (!disp) { fprintf (stderr, "hotkeys: could not open display\n"); return -1; } XSetErrorHandler (x_err_handler); read_config (disp); int ks_per_kk; int first_kk, last_kk; Atom* syms; XDisplayKeycodes (disp, &first_kk, &last_kk); syms = XGetKeyboardMapping (disp, first_kk, last_kk - first_kk, &ks_per_kk); init_mapped_keycodes (disp, syms, first_kk, last_kk, ks_per_kk); XFree (syms); XSync (disp, 0); loop_tid = deadbeef->thread_start (hotkeys_event_loop, 0); #else read_config (); #endif return 0; } static int hotkeys_disconnect (void) { #ifndef __APPLE__ if (loop_tid) { finished = 1; deadbeef->thread_join (loop_tid); } #endif cleanup (); return 0; } const char * hotkeys_get_name_for_keycode (int keycode) { for (int i = 0; keys[i].name; i++) { if (keycode == keys[i].keysym) { return keys[i].name; } } return NULL; } DB_plugin_action_t* hotkeys_get_action_for_keycombo (int key, int mods, int isglobal, int *ctx) { int i; // find mapped keycode if (key < 0x7f && isupper (key)) { key = tolower (key); } int keycode = key; trace ("hotkeys: keysym 0x%X mapped to 0x%X\n", key, keycode); for (i = 0; i < command_count; i++) { trace ("hotkeys: command %s keycode %x mods %x\n", commands[i].action->name, commands[i].keycode, commands[i].modifier); if (commands[i].keycode == keycode && commands[i].modifier == mods && commands[i].isglobal == isglobal) { *ctx = commands[i].ctx; return commands[i].action; } } return NULL; } void hotkeys_reset (void) { #ifndef __APPLE__ need_reset = 1; trace ("hotkeys: reset flagged\n"); #endif } int action_play_cb (struct DB_plugin_action_s *action, int ctx) { // NOTE: this function is copied as on_playbtn_clicked in gtkui DB_output_t *output = deadbeef->get_output (); if (output->state () == OUTPUT_STATE_PAUSED) { ddb_playlist_t *plt = deadbeef->plt_get_curr (); int cur = deadbeef->plt_get_cursor (plt, PL_MAIN); if (cur != -1) { ddb_playItem_t *it = deadbeef->plt_get_item_for_idx (plt, cur, PL_MAIN); ddb_playItem_t *it_playing = deadbeef->streamer_get_playing_track (); if (it) { deadbeef->pl_item_unref (it); } if (it_playing) { deadbeef->pl_item_unref (it_playing); } if (it != it_playing) { deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, cur, 0); } else { deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0); } } else { deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0); } deadbeef->plt_unref (plt); } else { ddb_playlist_t *plt = deadbeef->plt_get_curr (); int cur = -1; if (plt) { cur = deadbeef->plt_get_cursor (plt, PL_MAIN); deadbeef->plt_unref (plt); } if (cur == -1) { cur = 0; } deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, cur, 0); } return 0; } int action_prev_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->sendmessage (DB_EV_PREV, 0, 0, 0); return 0; } int action_next_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->sendmessage (DB_EV_NEXT, 0, 0, 0); return 0; } int action_stop_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->sendmessage (DB_EV_STOP, 0, 0, 0); return 0; } int action_toggle_pause_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->sendmessage (DB_EV_TOGGLE_PAUSE, 0, 0, 0); return 0; } int action_play_pause_cb (struct DB_plugin_action_s *action, int ctx) { int state = deadbeef->get_output ()->state (); if (state == OUTPUT_STATE_PLAYING) { deadbeef->sendmessage (DB_EV_PAUSE, 0, 0, 0); } else { deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0); } return 0; } int action_play_random_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->sendmessage (DB_EV_PLAY_RANDOM, 0, 0, 0); return 0; } int action_seek_5p_forward_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->pl_lock (); DB_playItem_t *it = deadbeef->streamer_get_playing_track (); if (it) { float dur = deadbeef->pl_get_item_duration (it); if (dur > 0) { float pos = deadbeef->streamer_get_playpos (); deadbeef->sendmessage (DB_EV_SEEK, 0, (pos + dur * 0.05f) * 1000, 0); } deadbeef->pl_item_unref (it); } deadbeef->pl_unlock (); return 0; } int action_seek_5p_backward_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->pl_lock (); DB_playItem_t *it = deadbeef->streamer_get_playing_track (); if (it) { float dur = deadbeef->pl_get_item_duration (it); if (dur > 0) { float pos = deadbeef->streamer_get_playpos (); pos = (pos - dur * 0.05f) * 1000; if (pos < 0) { pos = 0; } deadbeef->sendmessage (DB_EV_SEEK, 0, pos, 0); } deadbeef->pl_item_unref (it); } deadbeef->pl_unlock (); return 0; } int action_seek_1p_forward_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->pl_lock (); DB_playItem_t *it = deadbeef->streamer_get_playing_track (); if (it) { float dur = deadbeef->pl_get_item_duration (it); if (dur > 0) { float pos = deadbeef->streamer_get_playpos (); deadbeef->sendmessage (DB_EV_SEEK, 0, (pos + dur * 0.01f) * 1000, 0); } deadbeef->pl_item_unref (it); } deadbeef->pl_unlock (); return 0; } int action_seek_1p_backward_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->pl_lock (); DB_playItem_t *it = deadbeef->streamer_get_playing_track (); if (it) { float dur = deadbeef->pl_get_item_duration (it); if (dur > 0) { float pos = deadbeef->streamer_get_playpos (); pos = (pos - dur * 0.01f) * 1000; if (pos < 0) { pos = 0; } deadbeef->sendmessage (DB_EV_SEEK, 0, pos, 0); } deadbeef->pl_item_unref (it); } deadbeef->pl_unlock (); return 0; } int action_volume_up_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->volume_set_db (deadbeef->volume_get_db () + 2); return 0; } int action_volume_down_cb (struct DB_plugin_action_s *action, int ctx) { deadbeef->volume_set_db (deadbeef->volume_get_db () - 2); return 0; } int action_toggle_stop_after_current_cb (struct DB_plugin_action_s *action, int ctx) { int var = deadbeef->conf_get_int ("playlist.stop_after_current", 0); var = 1 - var; deadbeef->conf_set_int ("playlist.stop_after_current", var); deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0); return 0; } int action_toggle_stop_after_album_cb (struct DB_plugin_action_s *action, int ctx) { int var = deadbeef->conf_get_int ("playlist.stop_after_album", 0); var = 1 - var; deadbeef->conf_set_int ("playlist.stop_after_album", var); deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0); return 0; } static DB_plugin_action_t action_reload_metadata = { .title = "Reload Metadata", .name = "reload_metadata", .flags = DB_ACTION_MULTIPLE_TRACKS, .callback2 = action_reload_metadata_handler, .next = NULL }; static DB_plugin_action_t action_jump_to_current = { .title = "Playback/Jump To Currently Playing Track", .name = "jump_to_current_track", .flags = DB_ACTION_COMMON, .callback2 = action_jump_to_current_handler, .next = &action_reload_metadata }; static DB_plugin_action_t action_next_playlist = { .title = "Next Playlist", .name = "next_playlist", .flags = DB_ACTION_COMMON, .callback2 = action_next_playlist_handler, .next = &action_jump_to_current }; static DB_plugin_action_t action_prev_playlist = { .title = "Prev Playlist", .name = "prev_playlist", .flags = DB_ACTION_COMMON, .callback2 = action_prev_playlist_handler, .next = &action_next_playlist }; static DB_plugin_action_t action_playlist10 = { .title = "Switch To Playlist 10", .name = "playlist10", .flags = DB_ACTION_COMMON, .callback2 = action_playlist10_handler, .next = &action_prev_playlist }; static DB_plugin_action_t action_playlist9 = { .title = "Switch To Playlist 9", .name = "playlist9", .flags = DB_ACTION_COMMON, .callback2 = action_playlist9_handler, .next = &action_playlist10 }; static DB_plugin_action_t action_playlist8 = { .title = "Switch To Playlist 8", .name = "playlist8", .flags = DB_ACTION_COMMON, .callback2 = action_playlist8_handler, .next = &action_playlist9 }; static DB_plugin_action_t action_playlist7 = { .title = "Switch To Playlist 7", .name = "playlist7", .flags = DB_ACTION_COMMON, .callback2 = action_playlist7_handler, .next = &action_playlist8 }; static DB_plugin_action_t action_playlist6 = { .title = "Switch To Playlist 6", .name = "playlist6", .flags = DB_ACTION_COMMON, .callback2 = action_playlist6_handler, .next = &action_playlist7 }; static DB_plugin_action_t action_playlist5 = { .title = "Switch To Playlist 5", .name = "playlist5", .flags = DB_ACTION_COMMON, .callback2 = action_playlist5_handler, .next = &action_playlist6 }; static DB_plugin_action_t action_playlist4 = { .title = "Switch To Playlist 4", .name = "playlist4", .flags = DB_ACTION_COMMON, .callback2 = action_playlist4_handler, .next = &action_playlist5 }; static DB_plugin_action_t action_playlist3 = { .title = "Switch To Playlist 3", .name = "playlist3", .flags = DB_ACTION_COMMON, .callback2 = action_playlist3_handler, .next = &action_playlist4 }; static DB_plugin_action_t action_playlist2 = { .title = "Switch To Playlist 2", .name = "playlist2", .flags = DB_ACTION_COMMON, .callback2 = action_playlist2_handler, .next = &action_playlist3 }; static DB_plugin_action_t action_playlist1 = { .title = "Switch To Playlist 1", .name = "playlist1", .flags = DB_ACTION_COMMON, .callback2 = action_playlist1_handler, .next = &action_playlist2 }; static DB_plugin_action_t action_sort_randomize = { .title = "Edit/Sort Randomize", .name = "sort_randomize", .flags = DB_ACTION_COMMON, .callback2 = action_sort_randomize_handler, .next = &action_playlist1 }; static DB_plugin_action_t action_sort_by_date = { .title = "Edit/Sort By Date", .name = "sort_date", .flags = DB_ACTION_COMMON, .callback2 = action_sort_by_date_handler, .next = &action_sort_randomize }; static DB_plugin_action_t action_sort_by_artist = { .title = "Edit/Sort By Artist", .name = "sort_artist", .flags = DB_ACTION_COMMON, .callback2 = action_sort_by_artist_handler, .next = &action_sort_by_date }; static DB_plugin_action_t action_sort_by_album = { .title = "Edit/Sort By Album", .name = "sort_album", .flags = DB_ACTION_COMMON, .callback2 = action_sort_by_album_handler, .next = &action_sort_by_artist }; static DB_plugin_action_t action_sort_by_tracknr = { .title = "Edit/Sort By Track Number", .name = "sort_tracknr", .flags = DB_ACTION_COMMON, .callback2 = action_sort_by_tracknr_handler, .next = &action_sort_by_album }; static DB_plugin_action_t action_sort_by_title = { .title = "Edit/Sort By Title", .name = "sort_title", .flags = DB_ACTION_COMMON, .callback2 = action_sort_by_title_handler, .next = &action_sort_by_tracknr }; static DB_plugin_action_t action_invert_selection = { .title = "Edit/Invert Selection", .name = "invert_selection", .flags = DB_ACTION_COMMON, .callback2 = action_invert_selection_handler, .next = &action_sort_by_tracknr }; static DB_plugin_action_t action_clear_playlist = { .title = "Edit/Clear Playlist", .name = "clear_playlist", .flags = DB_ACTION_COMMON, .callback2 = action_clear_playlist_handler, .next = &action_invert_selection }; static DB_plugin_action_t action_remove_from_playqueue = { .title = "Playback/Remove From Playback Queue", .name = "remove_from_playback_queue", .flags = DB_ACTION_MULTIPLE_TRACKS, .callback2 = action_remove_from_playqueue_handler, .next = &action_clear_playlist }; static DB_plugin_action_t action_add_to_playqueue = { .title = "Playback/Add To Playback Queue", .name = "add_to_playback_queue", .flags = DB_ACTION_MULTIPLE_TRACKS, .callback2 = action_add_to_playqueue_handler, .next = &action_remove_from_playqueue }; static DB_plugin_action_t action_toggle_mute = { .title = "Playback/Toggle Mute", .name = "toggle_mute", .flags = DB_ACTION_COMMON, .callback2 = action_toggle_mute_handler, .next = &action_add_to_playqueue }; static DB_plugin_action_t action_play = { .title = "Playback/Play", .name = "play", .flags = DB_ACTION_COMMON, .callback2 = action_play_cb, .next = &action_toggle_mute }; static DB_plugin_action_t action_stop = { .title = "Playback/Stop", .name = "stop", .flags = DB_ACTION_COMMON, .callback2 = action_stop_cb, .next = &action_play }; static DB_plugin_action_t action_prev = { .title = "Playback/Previous", .name = "prev", .flags = DB_ACTION_COMMON, .callback2 = action_prev_cb, .next = &action_stop }; static DB_plugin_action_t action_next = { .title = "Playback/Next", .name = "next", .flags = DB_ACTION_COMMON, .callback2 = action_next_cb, .next = &action_prev }; static DB_plugin_action_t action_toggle_pause = { .title = "Playback/Toggle Pause", .name = "toggle_pause", .flags = DB_ACTION_COMMON, .callback2 = action_toggle_pause_cb, .next = &action_next }; static DB_plugin_action_t action_play_pause = { .title = "Playback/Play\\/Pause", .name = "play_pause", .flags = DB_ACTION_COMMON, .callback2 = action_play_pause_cb, .next = &action_toggle_pause }; static DB_plugin_action_t action_play_random = { .title = "Playback/Play Random", .name = "playback_random", .flags = DB_ACTION_COMMON, .callback2 = action_play_random_cb, .next = &action_play_pause }; static DB_plugin_action_t action_seek_1p_forward = { .title = "Playback/Seek 1% Forward", .name = "seek_1p_fwd", .flags = DB_ACTION_COMMON, .callback2 = action_seek_1p_forward_cb, .next = &action_play_random }; static DB_plugin_action_t action_seek_1p_backward = { .title = "Playback/Seek 1% Backward", .name = "seek_1p_back", .flags = DB_ACTION_COMMON, .callback2 = action_seek_1p_backward_cb, .next = &action_seek_1p_forward }; static DB_plugin_action_t action_seek_5p_forward = { .title = "Playback/Seek 5% Forward", .name = "seek_5p_fwd", .flags = DB_ACTION_COMMON, .callback2 = action_seek_5p_forward_cb, .next = &action_seek_1p_backward }; static DB_plugin_action_t action_seek_5p_backward = { .title = "Playback/Seek 5% Backward", .name = "seek_5p_back", .flags = DB_ACTION_COMMON, .callback2 = action_seek_5p_backward_cb, .next = &action_seek_5p_forward }; static DB_plugin_action_t action_volume_up = { .title = "Playback/Volume Up", .name = "volume_up", .flags = DB_ACTION_COMMON, .callback2 = action_volume_up_cb, .next = &action_seek_5p_backward }; static DB_plugin_action_t action_volume_down = { .title = "Playback/Volume Down", .name = "volume_down", .flags = DB_ACTION_COMMON, .callback2 = action_volume_down_cb, .next = &action_volume_up }; static DB_plugin_action_t action_toggle_stop_after_current = { .title = "Playback/Toggle Stop After Current Track", .name = "toggle_stop_after_current", .flags = DB_ACTION_COMMON, .callback2 = action_toggle_stop_after_current_cb, .next = &action_volume_down }; static DB_plugin_action_t action_toggle_stop_after_album = { .title = "Playback/Toggle Stop After Current Album", .name = "toggle_stop_after_album", .flags = DB_ACTION_COMMON, .callback2 = action_toggle_stop_after_album_cb, .next = &action_toggle_stop_after_current }; static DB_plugin_action_t * hotkeys_get_actions (DB_playItem_t *it) { return &action_toggle_stop_after_album; } // define plugin interface static DB_hotkeys_plugin_t plugin = { .misc.plugin.api_vmajor = 1, .misc.plugin.api_vminor = 5, .misc.plugin.version_major = 1, .misc.plugin.version_minor = 1, .misc.plugin.type = DB_PLUGIN_MISC, .misc.plugin.id = "hotkeys", .misc.plugin.name = "Hotkey manager", .misc.plugin.descr = "Manages local and global hotkeys, and executes actions when the assigned key combinations are pressed\n\n" "This plugin has its own API, to allow 3rd party GUI plugins to reuse the code.\n" "Check the plugins/hotkeys/hotkeys.h in the source tree if you need this.\n\n" "Changes in version 1.1\n" " * adaptation to new deadbeef 0.6 plugin API\n" " * added local hotkeys support\n" , .misc.plugin.copyright = "Copyright (C) 2012-2013 Alexey Yakovenko \n" "Copyright (C) 2009-2011 Viktor Semykin \n" "\n" "This program is free software; you can redistribute it and/or\n" "modify it under the terms of the GNU General Public License\n" "as published by the Free Software Foundation; either version 2\n" "of the License, or (at your option) any later version.\n" "\n" "This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" "GNU General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License\n" "along with this program; if not, write to the Free Software\n" "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n" , .misc.plugin.website = "http://deadbeef.sf.net", .misc.plugin.get_actions = hotkeys_get_actions, .misc.plugin.start = hotkeys_connect, .misc.plugin.stop = hotkeys_disconnect, .get_name_for_keycode = hotkeys_get_name_for_keycode, .get_action_for_keycombo = hotkeys_get_action_for_keycombo, .reset = hotkeys_reset, };