2 * Copyright (c) 2006-2011 Ed Schouten <ed@80386.nl>
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 * @brief Keyboard and signal input for user interface.
33 #include "audio_output.h"
37 #include "gui_internal.h"
39 #include "scrobbler.h"
43 * @brief The focus is on the browser.
45 #define GUI_FOCUS_BROWSER 0
47 * @brief The focus is on the playlist.
49 #define GUI_FOCUS_PLAYQ 1
51 * @brief The number of focusable windows.
53 #define GUI_FOCUS_COUNT 2
55 * @brief Window that is currently focused.
57 static int gui_input_curfocus
= GUI_FOCUS_BROWSER
;
59 * @brief Indicator of the current search string.
61 static struct vfsmatch
*cursearch
= NULL
;
63 * @brief The last seek string that has been entered.
65 static char *curseek
= NULL
;
67 * @brief Determine if we're already trying to shut down, to prevent
68 * multiple signals being handled while quitting.
70 static int shutting_down
= 0;
72 * @brief Add the Ctrl modifier to a character.
74 #define CTRL(x) (((x) - 'A' + 1) & 0x7f)
77 * @brief Properly shutdown the application by stopping playback and
86 #ifdef BUILD_SCROBBLER
88 #endif /* BUILD_SCROBBLER */
95 * @brief Prompt the user with a message to confirm termination of the
99 gui_input_askquit(void)
104 if (!config_getopt_bool("gui.input.may_quit")) {
105 gui_msgbar_warn(_("Use kill(1) to quit."));
109 msg
= g_strdup_printf(_("Quit %s?"), APP_NAME
);
110 ret
= gui_input_askyesno(msg
);
118 * @brief Fetch a character from the keyboard, already processing
119 * terminal resizes and awkward bugs.
122 gui_input_getch(void)
130 /* Redraw everything when resizing */
136 /* Catch backspace button */
139 return (KEY_BACKSPACE
);
141 /* FreeBSD returns KEY_SELECT instead of KEY_END */
145 /* Error condition */
150 /* Signal delivery */
153 /* We've lost stdin */
165 * @brief Switch the focus to the next window.
168 gui_input_switchfocus(void)
170 gui_input_curfocus
++;
171 gui_input_curfocus
%= GUI_FOCUS_COUNT
;
173 /* Update the selection colors */
174 gui_playq_setfocus(gui_input_curfocus
== GUI_FOCUS_PLAYQ
);
175 gui_browser_setfocus(gui_input_curfocus
== GUI_FOCUS_BROWSER
);
179 * @brief Ask the user to enter a new search string.
182 gui_input_asksearch(void)
185 const char *old
= NULL
;
188 /* Show the previous value if we have one */
189 if (cursearch
!= NULL
)
190 old
= vfs_match_value(cursearch
);
192 /* Allow the user to enter a search string */
193 str
= gui_input_askstring(_("Search for"), old
, NULL
);
197 vm
= vfs_match_new(str
);
199 gui_msgbar_warn(_("Bad pattern."));
204 /* Replace our search string */
205 if (cursearch
!= NULL
)
206 vfs_match_free(cursearch
);
209 str
= g_strdup_printf(_("Searching for \"%s\"..."),
210 vfs_match_value(vm
));
211 gui_msgbar_warn(str
);
218 * @brief Ask the user to enter a search string when none was given and
219 * search for the next item matching the search string.
222 gui_input_searchnext(void)
224 int nfocus
= GUI_FOCUS_PLAYQ
;
226 if (cursearch
== NULL
) {
227 /* No search string yet */
228 if (gui_input_asksearch() != 0)
233 * We want to change our search order depending on which dialog
234 * is currently focused. This code is quite awful, but does the
235 * thing. When the playq is focused, it only performs the first
236 * two searches. If the browser is focused, it only performs the
239 if (gui_input_curfocus
== GUI_FOCUS_PLAYQ
&&
240 gui_playq_searchnext(cursearch
) == 0) {
242 } else if (gui_browser_searchnext(cursearch
) == 0) {
243 nfocus
= GUI_FOCUS_BROWSER
;
245 } else if (gui_input_curfocus
!= GUI_FOCUS_PLAYQ
&&
246 gui_playq_searchnext(cursearch
) == 0) {
251 gui_msgbar_warn(_("Not found."));
254 found
: /* Focus the window with the match and redraw them. */
255 gui_input_curfocus
= nfocus
;
256 gui_playq_setfocus(gui_input_curfocus
== GUI_FOCUS_PLAYQ
);
257 gui_browser_setfocus(gui_input_curfocus
== GUI_FOCUS_BROWSER
);
261 * @brief Ask the user to enter a new search string and perform the
265 gui_input_search(void)
267 /* Always ask for a search string */
268 if (gui_input_asksearch() != 0)
271 /* Just simulate a 'n' button */
272 gui_input_searchnext();
276 * @brief Ask for a search string and filter matching files in the file
280 gui_input_locate(void)
282 /* Always ask for a search string */
283 if (gui_input_asksearch() != 0)
286 /* Perform the serach */
287 if (gui_browser_locate(cursearch
) != 0)
288 gui_msgbar_warn(_("Not found."));
294 * @brief Instruct the playlist to seek the current song 5 seconds
298 gui_input_cursong_seek_backward(void)
300 playq_cursong_seek(-5, 1);
304 * @brief Instruct the playlist to seek the current song 5 seconds
308 gui_input_cursong_seek_forward(void)
310 playq_cursong_seek(5, 1);
314 * @brief Approve input string format for the seeking option.
317 gui_input_cursong_seek_validator(const char *str
, char c
)
321 if (c
== '+' || c
== '-') {
322 /* Only allow + and - at the beginning of the statement */
325 } else if (c
== ':') {
326 /* Don't allow : before a digit has been inserted */
327 if (strpbrk(str
, "0123456789") == NULL
)
330 s
= strrchr(str
, ':');
332 /* Only allow colon after two decimals */
333 if (strlen(s
+ 1) != 2)
335 /* Only allow two : characters in the string */
336 if (s
- strchr(str
, ':') == 3)
339 } else if (g_ascii_isdigit(c
)) {
340 s
= strrchr(str
, ':');
342 /* Only allow up to two decimals after a colon */
343 if (strlen(s
+ 1) == 2)
345 /* Only allow '0' to '5' as the first digit */
346 if (s
[1] == '\0' && c
> '5')
350 /* Don't allow any other characters */
358 * @brief Ask the user to specify a position to seek the current song to.
361 gui_input_cursong_seek_jump(void)
364 int total
= 0, split
= 0, digit
= 0, value
, relative
= 0;
366 str
= gui_input_askstring(_("Jump to position"), curseek
,
367 gui_input_cursong_seek_validator
);
371 for (t
= str
; *t
!= '\0'; t
++) {
375 * Only allow two :'s, not without a prepending
376 * digit. :'s must be interleaved with two
379 g_assert(split
<= 1 && digit
!= 0);
380 g_assert(split
== 0 || digit
== 2);
385 /* Must be at the beginning */
390 /* Must be at the beginning */
396 value
= g_ascii_digit_value(*t
);
397 g_assert(value
!= -1);
398 g_assert(split
== 0 || digit
!= 0 || value
<= 5);
400 total
*= (digit
== 0) ? 6 : 10;
406 /* Not enough trailing digits */
407 if (split
> 0 && digit
!= 2)
415 playq_cursong_seek(total
, relative
);
417 /* Show the string the next time as well */
422 bad
: gui_msgbar_warn(_("Bad time format."));
427 * @brief A simple binding from a keyboard character input to a function.
431 * @brief The window that should be focussed.
435 * @brief The character that should be pressed.
439 * @brief The function that will be run.
445 * @brief List of keybindings available in the GUI.
447 static struct gui_binding kbdbindings
[] = {
448 /* Application-wide keyboard bindings */
450 { -1, '(', gui_playq_volume_down
},
451 { -1, ')', gui_playq_volume_up
},
452 #endif /* BUILD_VOLUME */
453 { -1, '<', gui_input_cursong_seek_backward
},
454 { -1, '>', gui_input_cursong_seek_forward
},
455 { -1, 'a', gui_browser_playq_add_after
},
456 { -1, 'A', gui_browser_playq_add_tail
},
457 { -1, 'b', playq_cursong_next
},
458 { -1, 'c', playq_cursong_pause
},
459 { -1, 'C', gui_browser_chdir
},
460 { -1, 'd', gui_playq_song_remove
},
461 { -1, 'D', gui_playq_song_remove_all
},
462 { -1, 'h', gui_browser_dir_parent
},
463 { -1, 'i', gui_browser_playq_add_before
},
464 { -1, 'I', gui_browser_playq_add_head
},
465 { -1, 'J', gui_input_cursong_seek_jump
},
466 { -1, 'l', gui_browser_dir_enter
},
467 { -1, 'L', gui_input_locate
},
468 { -1, 'P', vfs_cache_purge
},
469 { -1, 'q', gui_input_askquit
},
470 { -1, 'r', playq_repeat_toggle
},
471 { -1, 'R', gui_playq_song_randomize
},
472 { -1, 'v', playq_cursong_stop
},
473 { -1, 'w', gui_browser_write_playlist
},
474 { -1, 'x', gui_playq_song_select
},
475 { -1, 'z', playq_cursong_prev
},
476 { -1, '[', gui_playq_song_move_up
},
477 { -1, ']', gui_playq_song_move_down
},
478 { -1, '{', gui_playq_song_move_head
},
479 { -1, '}', gui_playq_song_move_tail
},
480 { -1, '~', gui_browser_gotohome
},
481 { -1, '\t', gui_input_switchfocus
},
482 { -1, CTRL('W'), gui_input_switchfocus
},
483 { -1, '/', gui_input_search
},
484 { -1, 'n', gui_input_searchnext
},
485 { -1, KEY_LEFT
, gui_browser_dir_parent
},
486 { -1, KEY_RIGHT
, gui_browser_dir_enter
},
488 /* Keyboard bindings for the file browser */
489 { GUI_FOCUS_BROWSER
, ' ', gui_browser_cursor_pagedown
},
490 { GUI_FOCUS_BROWSER
, 'F', gui_browser_gotofolder
},
491 { GUI_FOCUS_BROWSER
, 'f', gui_browser_fullpath
},
492 { GUI_FOCUS_BROWSER
, 'G', gui_browser_cursor_tail
},
493 { GUI_FOCUS_BROWSER
, 'g', gui_browser_cursor_head
},
494 { GUI_FOCUS_BROWSER
, 'j', gui_browser_cursor_down
},
495 { GUI_FOCUS_BROWSER
, 'k', gui_browser_cursor_up
},
496 { GUI_FOCUS_BROWSER
, CTRL('B'), gui_browser_cursor_pageup
},
497 { GUI_FOCUS_BROWSER
, CTRL('F'), gui_browser_cursor_pagedown
},
498 { GUI_FOCUS_BROWSER
, KEY_DOWN
, gui_browser_cursor_down
},
499 { GUI_FOCUS_BROWSER
, KEY_END
, gui_browser_cursor_tail
},
500 { GUI_FOCUS_BROWSER
, KEY_HOME
, gui_browser_cursor_head
},
501 { GUI_FOCUS_BROWSER
, KEY_NPAGE
, gui_browser_cursor_pagedown
},
502 { GUI_FOCUS_BROWSER
, KEY_PPAGE
, gui_browser_cursor_pageup
},
503 { GUI_FOCUS_BROWSER
, KEY_UP
, gui_browser_cursor_up
},
505 /* Keyboard bindings for the playlist */
506 { GUI_FOCUS_PLAYQ
, ' ', gui_playq_cursor_pagedown
},
507 { GUI_FOCUS_PLAYQ
, 'F', gui_playq_gotofolder
},
508 { GUI_FOCUS_PLAYQ
, 'f', gui_playq_fullpath
},
509 { GUI_FOCUS_PLAYQ
, 'G', gui_playq_cursor_tail
},
510 { GUI_FOCUS_PLAYQ
, 'g', gui_playq_cursor_head
},
511 { GUI_FOCUS_PLAYQ
, 'j', gui_playq_cursor_down
},
512 { GUI_FOCUS_PLAYQ
, 'k', gui_playq_cursor_up
},
513 { GUI_FOCUS_PLAYQ
, CTRL('B'), gui_playq_cursor_pageup
},
514 { GUI_FOCUS_PLAYQ
, CTRL('F'), gui_playq_cursor_pagedown
},
515 { GUI_FOCUS_PLAYQ
, KEY_DOWN
, gui_playq_cursor_down
},
516 { GUI_FOCUS_PLAYQ
, KEY_END
, gui_playq_cursor_tail
},
517 { GUI_FOCUS_PLAYQ
, KEY_HOME
, gui_playq_cursor_head
},
518 { GUI_FOCUS_PLAYQ
, KEY_NPAGE
, gui_playq_cursor_pagedown
},
519 { GUI_FOCUS_PLAYQ
, KEY_PPAGE
, gui_playq_cursor_pageup
},
520 { GUI_FOCUS_PLAYQ
, KEY_UP
, gui_playq_cursor_up
},
523 * @brief Amount of keybindings.
525 #define NUM_BINDINGS (sizeof kbdbindings / sizeof(struct gui_binding))
528 gui_input_sigmask(void)
530 #ifdef G_THREADS_IMPL_POSIX
534 sigaddset(&sset
, SIGHUP
);
535 sigaddset(&sset
, SIGINT
);
536 sigaddset(&sset
, SIGPIPE
);
537 sigaddset(&sset
, SIGQUIT
);
538 sigaddset(&sset
, SIGTERM
);
540 sigaddset(&sset
, SIGWINCH
);
541 #endif /* SIGWINCH */
542 pthread_sigmask(SIG_BLOCK
, &sset
, NULL
);
543 #endif /* G_THREADS_IMPL_POSIX */
548 * @brief Handler of all incoming signals with a custom action.
551 gui_input_sighandler(int signal
)
563 g_assert_not_reached();
566 #endif /* G_OS_UNIX */
575 signal(SIGHUP
, gui_input_sighandler
);
576 signal(SIGINT
, gui_input_sighandler
);
577 signal(SIGPIPE
, gui_input_sighandler
);
578 signal(SIGQUIT
, gui_input_sighandler
);
579 signal(SIGTERM
, gui_input_sighandler
);
580 #endif /* G_OS_UNIX */
583 ch
= gui_input_getch();
586 for (i
= 0; i
< NUM_BINDINGS
; i
++) {
587 /* Let's see if the button matches */
588 if (kbdbindings
[i
].input
!= ch
||
589 (kbdbindings
[i
].focus
!= -1 &&
590 kbdbindings
[i
].focus
!= gui_input_curfocus
))
595 kbdbindings
[i
].func();
597 #else /* !BUILD_DBUS */
598 kbdbindings
[i
].func();
599 #endif /* BUILD_DBUS */
608 gui_input_askyesno(const char *question
)
611 const char *yes
, *no
;
614 /* Skip the question if the user really wants so */
615 if (!config_getopt_bool("gui.input.confirm"))
621 /* Print the question on screen */
622 msg
= g_strdup_printf("%s ([%s]/%s): ", question
, yes
, no
);
627 input
= gui_input_getch();
630 /* Localized yes/no buttons */
631 if (input
== yes
[0]) {
634 } else if (input
== no
[0]) {
638 #endif /* BUILD_NLS */
640 /* Default y/n behaviour */
661 * @brief Find the offset to where a string should be trimmed to remove
662 * one word or special character sequence, including trailing
666 gui_input_trimword(GString
*gs
)
671 end
= (gs
->str
+ gs
->len
) - 1;
673 /* Trim as much trailing whitespace as possible */
675 if (end
< gs
->str
) return (0);
676 if (!isspace(*end
)) break;
681 /* Trim alphanumerics */
683 if (--end
< gs
->str
) return (0);
684 } while (isalnum(*end
));
686 /* Trim special characters */
688 if (--end
< gs
->str
) return (0);
689 } while (!isalnum(*end
) && !isspace(*end
));
692 return (end
- gs
->str
) + 1;
696 gui_input_askstring(const char *question
, const char *defstr
,
697 int (*validator
)(const char *str
, char c
))
700 unsigned int origlen
, newlen
;
701 int c
, clearfirst
= 0;
705 msg
= g_string_new(question
);
706 g_string_append(msg
, ": ");
708 if (defstr
!= NULL
) {
709 g_string_append(msg
, defstr
);
714 gui_msgbar_ask(msg
->str
);
716 switch (c
= gui_input_getch()) {
721 if (msg
->len
> origlen
) {
722 /* Prompt has contents */
723 g_string_truncate(msg
, msg
->len
- 1);
728 /* Just empty the return */
729 g_string_truncate(msg
, origlen
);
732 g_string_truncate(msg
, origlen
);
736 newlen
= gui_input_trimword(msg
);
737 g_string_truncate(msg
, MAX(newlen
, origlen
));
740 /* Control characters are not allowed */
741 if (g_ascii_iscntrl(c
))
744 if (validator
!= NULL
) {
745 vstr
= clearfirst
? "" : msg
->str
+ origlen
;
746 if (validator(vstr
, c
) != 0)
750 g_string_truncate(msg
, origlen
);
753 g_string_append_c(msg
, c
);
760 /* Only return a string when there are contents */
761 if (msg
->len
> origlen
)
762 ret
= g_strdup(msg
->str
+ origlen
);
764 g_string_free(msg
, TRUE
);