Comment out the URL to the Doxygen page.
[herrie-working.git] / herrie / src / gui_input.c
blobdbfbcecebe0e981d15c9398c75858e6ed2143e03
1 /*
2 * Copyright (c) 2006-2008 Ed Schouten <ed@80386.nl>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
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
24 * SUCH DAMAGE.
26 /**
27 * @file gui_input.c
28 * @brief Keyboard and signal input for user interface.
31 #include "stdinc.h"
33 #include "audio_output.h"
34 #include "config.h"
35 #include "gui.h"
36 #include "gui_internal.h"
37 #include "playq.h"
38 #include "scrobbler.h"
39 #include "vfs.h"
41 /**
42 * @brief The focus is on the browser.
44 #define GUI_FOCUS_BROWSER 0
45 /**
46 * @brief The focus is on the playlist.
48 #define GUI_FOCUS_PLAYQ 1
49 /**
50 * @brief The number of focusable windows.
52 #define GUI_FOCUS_COUNT 2
53 /**
54 * @brief Window that is currently focused.
56 static int gui_input_curfocus = GUI_FOCUS_BROWSER;
57 /**
58 * @brief Indicator of the current search string.
60 static struct vfsmatch *cursearch = NULL;
61 /**
62 * @brief The last seek string that has been entered.
64 static char *curseek = NULL;
65 /**
66 * @brief Determine if we're already trying to shut down, to prevent
67 * multiple signals being handled while quitting.
69 static int shutting_down = 0;
70 /**
71 * @brief Add the Ctrl modifier to a character.
73 #define CTRL(x) (((x) - 'A' + 1) & 0x7f)
75 /**
76 * @brief Properly shutdown the application by stopping playback and
77 * destroying the GUI.
79 static void
80 gui_input_quit(void)
82 shutting_down = 1;
84 playq_shutdown();
85 #ifdef BUILD_SCROBBLER
86 scrobbler_shutdown();
87 #endif /* BUILD_SCROBBLER */
88 audio_output_close();
89 gui_draw_destroy();
90 exit(0);
93 /**
94 * @brief Prompt the user with a message to confirm termination of the
95 * application.
97 static void
98 gui_input_askquit(void)
100 int ret;
101 char *msg;
103 if (!config_getopt_bool("gui.input.may_quit")) {
104 gui_msgbar_warn(_("Use kill(1) to quit."));
105 return;
108 msg = g_strdup_printf(_("Quit %s?"), APP_NAME);
109 ret = gui_input_askyesno(msg);
110 g_free(msg);
112 if (ret == 0)
113 gui_input_quit();
117 * @brief Fetch a character from the keyboard, already processing
118 * terminal resizes and awkward bugs.
120 static int
121 gui_input_getch(void)
123 int ch;
125 for (;;) {
126 ch = getch();
128 switch (ch) {
129 /* Redraw everything when resizing */
130 case KEY_RESIZE:
131 case CTRL('L'):
132 gui_draw_resize();
133 continue;
135 /* Catch backspace button */
136 case CTRL('H'):
137 case CTRL('?'):
138 return (KEY_BACKSPACE);
140 /* FreeBSD returns KEY_SELECT instead of KEY_END */
141 case KEY_SELECT:
142 return (KEY_END);
144 /* Error condition */
145 case ERR:
146 switch (errno) {
147 case 0:
148 case EINTR:
149 /* Signal delivery */
150 continue;
151 default:
152 /* We've lost stdin */
153 gui_input_quit();
157 break;
160 return (ch);
164 * @brief Switch the focus to the next window.
166 static void
167 gui_input_switchfocus(void)
169 gui_input_curfocus++;
170 gui_input_curfocus %= GUI_FOCUS_COUNT;
172 /* Update the selection colors */
173 gui_playq_setfocus(gui_input_curfocus == GUI_FOCUS_PLAYQ);
174 gui_browser_setfocus(gui_input_curfocus == GUI_FOCUS_BROWSER);
178 * @brief Ask the user to enter a new search string.
180 static int
181 gui_input_asksearch(void)
183 char *str;
184 const char *old = NULL;
185 struct vfsmatch *vm;
187 /* Show the previous value if we have one */
188 if (cursearch != NULL)
189 old = vfs_match_value(cursearch);
191 /* Allow the user to enter a search string */
192 str = gui_input_askstring(_("Search for"), old, NULL);
193 if (str == NULL)
194 return (-1);
196 vm = vfs_match_new(str);
197 if (vm == NULL) {
198 gui_msgbar_warn(_("Bad pattern."));
199 g_free(str);
200 return (-1);
203 /* Replace our search string */
204 if (cursearch != NULL)
205 vfs_match_free(cursearch);
206 cursearch = vm;
208 return (0);
212 * @brief Ask the user to enter a search string when none was given and
213 * search for the next item matching the search string.
215 static void
216 gui_input_searchnext(void)
218 int nfocus = GUI_FOCUS_PLAYQ;
220 if (cursearch == NULL) {
221 /* No search string yet */
222 if (gui_input_asksearch() != 0)
223 return;
227 * We want to change our search order depending on which dialog
228 * is currently focused. This code is quite awful, but does the
229 * thing. When the playq is focused, it only performs the first
230 * two searches. If the browser is focused, it only performs the
231 * last two.
233 if (gui_input_curfocus == GUI_FOCUS_PLAYQ &&
234 gui_playq_searchnext(cursearch) == 0) {
235 goto found;
236 } else if (gui_browser_searchnext(cursearch) == 0) {
237 nfocus = GUI_FOCUS_BROWSER;
238 goto found;
239 } else if (gui_input_curfocus != GUI_FOCUS_PLAYQ &&
240 gui_playq_searchnext(cursearch) == 0) {
241 goto found;
244 /* Bad luck. */
245 gui_msgbar_warn(_("Not found."));
246 return;
248 found: /* Focus the window with the match and redraw them. */
249 gui_input_curfocus = nfocus;
250 gui_playq_setfocus(gui_input_curfocus == GUI_FOCUS_PLAYQ);
251 gui_browser_setfocus(gui_input_curfocus == GUI_FOCUS_BROWSER);
255 * @brief Ask the user to enter a new search string and perform the
256 * first search.
258 static void
259 gui_input_search(void)
261 /* Always ask for a search string */
262 if (gui_input_asksearch() != 0)
263 return;
265 /* Just simulate a 'n' button */
266 gui_input_searchnext();
270 * @brief Ask for a search string and filter matching files in the file
271 * browser.
273 static void
274 gui_input_locate(void)
276 /* Always ask for a search string */
277 if (gui_input_asksearch() != 0)
278 return;
280 /* Perform the serach */
281 if (gui_browser_locate(cursearch) != 0)
282 gui_msgbar_warn(_("Not found."));
286 * @brief Instruct the playlist to seek the current song 5 seconds
287 * backward.
289 static void
290 gui_input_cursong_seek_backward(void)
292 playq_cursong_seek(-5, 1);
296 * @brief Instruct the playlist to seek the current song 5 seconds
297 * forward.
299 static void
300 gui_input_cursong_seek_forward(void)
302 playq_cursong_seek(5, 1);
306 * @brief Approve input string format for the seeking option.
308 static int
309 gui_input_cursong_seek_validator(const char *str, char c)
311 const char *s;
313 if (c == '+' || c == '-') {
314 /* Only allow + and - at the beginning of the statement */
315 if (str[0] != '\0')
316 return (-1);
317 } else if (c == ':') {
318 /* Don't allow : before a digit has been inserted */
319 if (strpbrk(str, "0123456789") == NULL)
320 return (-1);
322 s = strrchr(str, ':');
323 if (s != NULL) {
324 /* Only allow colon after two decimals */
325 if (strlen(s + 1) != 2)
326 return (-1);
327 /* Only allow two : characters in the string */
328 if (s - strchr(str, ':') == 3)
329 return (-1);
331 } else if (g_ascii_isdigit(c)) {
332 s = strrchr(str, ':');
333 if (s != NULL) {
334 /* Only allow up to two decimals after a colon */
335 if (strlen(s + 1) == 2)
336 return (-1);
337 /* Only allow '0' to '5' as the first digit */
338 if (s[1] == '\0' && c > '5')
339 return (-1);
341 } else {
342 /* Don't allow any other characters */
343 return (-1);
346 return (0);
350 * @brief Ask the user to specify a position to seek the current song to.
352 static void
353 gui_input_cursong_seek_jump(void)
355 char *str, *t;
356 int total = 0, split = 0, digit = 0, value, relative = 0;
358 t = str = gui_input_askstring(_("Jump to position"),
359 curseek, gui_input_cursong_seek_validator);
360 if (str == NULL)
361 return;
363 for (t = str; *t != '\0'; t++) {
364 switch (*t) {
365 case ':':
367 * Only allow two :'s, not without a prepending
368 * digit. :'s must be interleaved with two
369 * digits.
371 g_assert(split <= 1 && digit != 0);
372 g_assert(split == 0 || digit == 2);
373 split++;
374 digit = 0;
375 break;
376 case '+':
377 /* Must be at the beginning */
378 g_assert(t == str);
379 relative = 1;
380 break;
381 case '-':
382 /* Must be at the beginning */
383 g_assert(t == str);
384 relative = -1;
385 break;
386 default:
387 /* Regular digit */
388 value = g_ascii_digit_value(*t);
389 g_assert(value != -1);
390 g_assert(split == 0 || digit != 0 || value <= 5);
392 total *= (digit == 0) ? 6 : 10;
393 total += value;
394 digit++;
398 /* Not enough trailing digits */
399 if (split > 0 && digit != 2)
400 goto bad;
402 if (relative != 0) {
403 total *= relative;
404 if (total == 0)
405 goto bad;
407 playq_cursong_seek(total, relative);
409 /* Show the string the next time as well */
410 g_free(curseek);
411 curseek = str;
412 return;
414 bad: gui_msgbar_warn(_("Bad time format."));
415 g_free(str);
419 * @brief A simple binding from a keyboard character input to a function.
421 struct gui_binding {
423 * @brief The window that should be focussed.
425 int focus;
427 * @brief The character that should be pressed.
429 int input;
431 * @brief The function that will be run.
433 void (*func)(void);
437 * @brief List of keybindings available in the GUI.
439 static struct gui_binding kbdbindings[] = {
440 /* Application-wide keyboard bindings */
441 #ifdef BUILD_VOLUME
442 { -1, '(', gui_playq_volume_down },
443 { -1, ')', gui_playq_volume_up },
444 #endif /* BUILD_VOLUME */
445 { -1, '<', gui_input_cursong_seek_backward },
446 { -1, '>', gui_input_cursong_seek_forward },
447 { -1, 'a', gui_browser_playq_add_after },
448 { -1, 'A', gui_browser_playq_add_tail },
449 { -1, 'b', playq_cursong_next },
450 { -1, 'c', playq_cursong_pause },
451 { -1, 'C', gui_browser_chdir },
452 { -1, 'd', gui_playq_song_remove },
453 { -1, 'D', gui_playq_song_remove_all },
454 { -1, 'h', gui_browser_dir_parent },
455 { -1, 'i', gui_browser_playq_add_before },
456 { -1, 'I', gui_browser_playq_add_head },
457 { -1, 'J', gui_input_cursong_seek_jump },
458 { -1, 'l', gui_browser_dir_enter },
459 { -1, 'L', gui_input_locate },
460 { -1, 'q', gui_input_askquit },
461 { -1, 'r', playq_repeat_toggle },
462 { -1, 'R', gui_playq_song_randomize },
463 { -1, 'v', playq_cursong_stop },
464 { -1, 'w', gui_browser_write_playlist },
465 { -1, 'x', gui_playq_song_select },
466 { -1, 'z', playq_cursong_prev },
467 { -1, '[', gui_playq_song_move_up },
468 { -1, ']', gui_playq_song_move_down },
469 { -1, '{', gui_playq_song_move_head },
470 { -1, '}', gui_playq_song_move_tail },
471 { -1, '\t', gui_input_switchfocus },
472 { -1, CTRL('W'), gui_input_switchfocus },
473 { -1, '/', gui_input_search },
474 { -1, 'n', gui_input_searchnext },
475 { -1, KEY_LEFT, gui_browser_dir_parent },
476 { -1, KEY_RIGHT, gui_browser_dir_enter },
478 /* Keyboard bindings for the file browser */
479 { GUI_FOCUS_BROWSER, ' ', gui_browser_cursor_pagedown },
480 { GUI_FOCUS_BROWSER, 'F', gui_browser_gotofolder },
481 { GUI_FOCUS_BROWSER, 'f', gui_browser_fullpath },
482 { GUI_FOCUS_BROWSER, 'G', gui_browser_cursor_tail },
483 { GUI_FOCUS_BROWSER, 'g', gui_browser_cursor_head },
484 { GUI_FOCUS_BROWSER, 'j', gui_browser_cursor_down },
485 { GUI_FOCUS_BROWSER, 'k', gui_browser_cursor_up },
486 { GUI_FOCUS_BROWSER, CTRL('B'), gui_browser_cursor_pageup },
487 { GUI_FOCUS_BROWSER, CTRL('F'), gui_browser_cursor_pagedown },
488 { GUI_FOCUS_BROWSER, KEY_DOWN, gui_browser_cursor_down },
489 { GUI_FOCUS_BROWSER, KEY_END, gui_browser_cursor_tail },
490 { GUI_FOCUS_BROWSER, KEY_HOME, gui_browser_cursor_head },
491 { GUI_FOCUS_BROWSER, KEY_NPAGE, gui_browser_cursor_pagedown },
492 { GUI_FOCUS_BROWSER, KEY_PPAGE, gui_browser_cursor_pageup },
493 { GUI_FOCUS_BROWSER, KEY_UP, gui_browser_cursor_up },
495 /* Keyboard bindings for the playlist */
496 { GUI_FOCUS_PLAYQ, ' ', gui_playq_cursor_pagedown },
497 { GUI_FOCUS_PLAYQ, 'F', gui_playq_gotofolder },
498 { GUI_FOCUS_PLAYQ, 'f', gui_playq_fullpath },
499 { GUI_FOCUS_PLAYQ, 'G', gui_playq_cursor_tail },
500 { GUI_FOCUS_PLAYQ, 'g', gui_playq_cursor_head },
501 { GUI_FOCUS_PLAYQ, 'j', gui_playq_cursor_down },
502 { GUI_FOCUS_PLAYQ, 'k', gui_playq_cursor_up },
503 { GUI_FOCUS_PLAYQ, CTRL('B'), gui_playq_cursor_pageup },
504 { GUI_FOCUS_PLAYQ, CTRL('F'), gui_playq_cursor_pagedown },
505 { GUI_FOCUS_PLAYQ, KEY_DOWN, gui_playq_cursor_down },
506 { GUI_FOCUS_PLAYQ, KEY_END, gui_playq_cursor_tail },
507 { GUI_FOCUS_PLAYQ, KEY_HOME, gui_playq_cursor_head },
508 { GUI_FOCUS_PLAYQ, KEY_NPAGE, gui_playq_cursor_pagedown },
509 { GUI_FOCUS_PLAYQ, KEY_PPAGE, gui_playq_cursor_pageup },
510 { GUI_FOCUS_PLAYQ, KEY_UP, gui_playq_cursor_up },
513 * @brief Amount of keybindings.
515 #define NUM_BINDINGS (sizeof kbdbindings / sizeof(struct gui_binding))
517 void
518 gui_input_sigmask(void)
520 #ifdef G_THREADS_IMPL_POSIX
521 sigset_t sset;
523 sigemptyset(&sset);
524 sigaddset(&sset, SIGUSR1);
525 sigaddset(&sset, SIGUSR2);
526 sigaddset(&sset, SIGHUP);
527 sigaddset(&sset, SIGINT);
528 sigaddset(&sset, SIGPIPE);
529 sigaddset(&sset, SIGQUIT);
530 sigaddset(&sset, SIGTERM);
531 #ifdef SIGWINCH
532 sigaddset(&sset, SIGWINCH);
533 #endif /* SIGWINCH */
534 pthread_sigmask(SIG_BLOCK, &sset, NULL);
535 #endif /* G_THREADS_IMPL_POSIX */
538 #ifdef G_OS_UNIX
540 * @brief Handler of all incoming signals with a custom action.
542 static void
543 gui_input_sighandler(int signal)
545 if (shutting_down)
546 return;
548 switch (signal) {
549 case SIGUSR1:
550 playq_cursong_pause();
551 break;
552 case SIGUSR2:
553 playq_cursong_next();
554 break;
555 case SIGHUP:
556 case SIGINT:
557 case SIGPIPE:
558 case SIGQUIT:
559 case SIGTERM:
560 gui_input_quit();
561 g_assert_not_reached();
564 #endif /* G_OS_UNIX */
566 void
567 gui_input_loop(void)
569 int ch;
570 unsigned int i;
572 #ifdef G_OS_UNIX
573 signal(SIGUSR1, gui_input_sighandler);
574 signal(SIGUSR2, gui_input_sighandler);
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 */
582 for (;;) {
583 ch = gui_input_getch();
584 gui_msgbar_flush();
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))
591 continue;
593 kbdbindings[i].func();
594 break;
597 gui_draw_done();
602 gui_input_askyesno(const char *question)
604 char *msg, input;
605 const char *yes, *no;
606 int ret;
608 /* Skip the question if the user really wants so */
609 if (!config_getopt_bool("gui.input.confirm"))
610 return (0);
612 yes = _("yes");
613 no = _("no");
615 /* Print the question on screen */
616 msg = g_strdup_printf("%s ([%s]/%s): ", question, yes, no);
617 gui_msgbar_ask(msg);
618 g_free(msg);
620 for (;;) {
621 input = gui_input_getch();
623 #ifdef BUILD_NLS
624 /* Localized yes/no buttons */
625 if (input == yes[0]) {
626 ret = 0;
627 goto done;
628 } else if (input == no[0]) {
629 ret = -1;
630 goto done;
632 #endif /* BUILD_NLS */
634 /* Default y/n behaviour */
635 switch (input) {
636 case 'y':
637 case 'Y':
638 case '\r':
639 ret = 0;
640 goto done;
641 case CTRL('['):
642 case 'n':
643 case 'N':
644 case CTRL('C'):
645 ret = -1;
646 goto done;
649 done:
650 gui_msgbar_flush();
651 return (ret);
655 * @brief Find the offset to where a string should be trimmed to remove
656 * one word or special character sequence, including trailing
657 * whitespace.
659 static int
660 gui_input_trimword(GString *gs)
662 const char *end;
664 /* Last character */
665 end = (gs->str + gs->len) - 1;
667 /* Trim as much trailing whitespace as possible */
668 for (;;) {
669 if (end < gs->str) return (0);
670 if (!isspace(*end)) break;
671 end--;
674 if (isalnum(*end)) {
675 /* Trim alphanumerics */
676 do {
677 if (--end < gs->str) return (0);
678 } while (isalnum(*end));
679 } else {
680 /* Trim special characters */
681 do {
682 if (--end < gs->str) return (0);
683 } while (!isalnum(*end) && !isspace(*end));
686 return (end - gs->str) + 1;
689 char *
690 gui_input_askstring(const char *question, const char *defstr,
691 int (*validator)(const char *str, char c))
693 GString *msg;
694 unsigned int origlen, newlen;
695 int c, clearfirst = 0;
696 char *ret = NULL, *vstr;
698 msg = g_string_new(question);
699 g_string_append(msg, ": ");
700 origlen = msg->len;
701 if (defstr != NULL) {
702 g_string_append(msg, defstr);
703 clearfirst = 1;
706 for(;;) {
707 gui_msgbar_ask(msg->str);
709 switch (c = gui_input_getch()) {
710 case '\r':
711 goto done;
712 case KEY_BACKSPACE:
713 clearfirst = 0;
714 if (msg->len > origlen) {
715 /* Prompt has contents */
716 g_string_truncate(msg, msg->len - 1);
718 break;
719 case CTRL('C'):
720 case CTRL('['):
721 /* Just empty the return */
722 g_string_truncate(msg, origlen);
723 goto done;
724 case CTRL('U'):
725 g_string_truncate(msg, origlen);
726 break;
727 case CTRL('W'):
728 clearfirst = 0;
729 newlen = gui_input_trimword(msg);
730 g_string_truncate(msg, MAX(newlen, origlen));
731 break;
732 default:
733 /* Control characters are not allowed */
734 if (g_ascii_iscntrl(c))
735 break;
737 if (validator != NULL) {
738 vstr = clearfirst ? "" : msg->str + origlen;
739 if (validator(vstr, c) != 0)
740 break;
742 if (clearfirst) {
743 g_string_truncate(msg, origlen);
744 clearfirst = 0;
746 g_string_append_c(msg, c);
750 done:
751 gui_msgbar_flush();
753 /* Only return a string when there are contents */
754 if (msg->len > origlen)
755 ret = g_strdup(msg->str + origlen);
757 g_string_free(msg, TRUE);
758 return ret;