Fix whitespace inconsistencies.
[herrie-working.git] / herrie / src / gui_input.c
blob11e8e18b79a1f55d2495c900fb5dabaf43c3f8ef
1 /*
2 * Copyright (c) 2006-2011 Ed Schouten <ed@80386.nl>
3 * All rights reserved.
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 "dbus.h"
36 #include "gui.h"
37 #include "gui_internal.h"
38 #include "playq.h"
39 #include "scrobbler.h"
40 #include "vfs.h"
42 /**
43 * @brief The focus is on the browser.
45 #define GUI_FOCUS_BROWSER 0
46 /**
47 * @brief The focus is on the playlist.
49 #define GUI_FOCUS_PLAYQ 1
50 /**
51 * @brief The number of focusable windows.
53 #define GUI_FOCUS_COUNT 2
54 /**
55 * @brief Window that is currently focused.
57 static int gui_input_curfocus = GUI_FOCUS_BROWSER;
58 /**
59 * @brief Indicator of the current search string.
61 static struct vfsmatch *cursearch = NULL;
62 /**
63 * @brief The last seek string that has been entered.
65 static char *curseek = NULL;
66 /**
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;
71 /**
72 * @brief Add the Ctrl modifier to a character.
74 #define CTRL(x) (((x) - 'A' + 1) & 0x7f)
76 /**
77 * @brief Properly shutdown the application by stopping playback and
78 * destroying the GUI.
80 static void
81 gui_input_quit(void)
83 shutting_down = 1;
85 playq_shutdown();
86 #ifdef BUILD_SCROBBLER
87 scrobbler_shutdown();
88 #endif /* BUILD_SCROBBLER */
89 audio_output_close();
90 gui_draw_destroy();
91 exit(0);
94 /**
95 * @brief Prompt the user with a message to confirm termination of the
96 * application.
98 static void
99 gui_input_askquit(void)
101 int ret;
102 char *msg;
104 if (!config_getopt_bool("gui.input.may_quit")) {
105 gui_msgbar_warn(_("Use kill(1) to quit."));
106 return;
109 msg = g_strdup_printf(_("Quit %s?"), APP_NAME);
110 ret = gui_input_askyesno(msg);
111 g_free(msg);
113 if (ret == 0)
114 gui_input_quit();
118 * @brief Fetch a character from the keyboard, already processing
119 * terminal resizes and awkward bugs.
121 static int
122 gui_input_getch(void)
124 int ch;
126 for (;;) {
127 ch = getch();
129 switch (ch) {
130 /* Redraw everything when resizing */
131 case KEY_RESIZE:
132 case CTRL('L'):
133 gui_draw_resize();
134 continue;
136 /* Catch backspace button */
137 case CTRL('H'):
138 case CTRL('?'):
139 return (KEY_BACKSPACE);
141 /* FreeBSD returns KEY_SELECT instead of KEY_END */
142 case KEY_SELECT:
143 return (KEY_END);
145 /* Error condition */
146 case ERR:
147 switch (errno) {
148 case 0:
149 case EINTR:
150 /* Signal delivery */
151 continue;
152 default:
153 /* We've lost stdin */
154 gui_input_quit();
158 break;
161 return (ch);
165 * @brief Switch the focus to the next window.
167 static void
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.
181 static int
182 gui_input_asksearch(void)
184 char *str;
185 const char *old = NULL;
186 struct vfsmatch *vm;
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);
194 if (str == NULL)
195 return (-1);
197 vm = vfs_match_new(str);
198 if (vm == NULL) {
199 gui_msgbar_warn(_("Bad pattern."));
200 g_free(str);
201 return (-1);
204 /* Replace our search string */
205 if (cursearch != NULL)
206 vfs_match_free(cursearch);
207 cursearch = vm;
209 str = g_strdup_printf(_("Searching for \"%s\"..."),
210 vfs_match_value(vm));
211 gui_msgbar_warn(str);
212 g_free(str);
214 return (0);
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.
221 static void
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)
229 return;
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
237 * last two.
239 if (gui_input_curfocus == GUI_FOCUS_PLAYQ &&
240 gui_playq_searchnext(cursearch) == 0) {
241 goto found;
242 } else if (gui_browser_searchnext(cursearch) == 0) {
243 nfocus = GUI_FOCUS_BROWSER;
244 goto found;
245 } else if (gui_input_curfocus != GUI_FOCUS_PLAYQ &&
246 gui_playq_searchnext(cursearch) == 0) {
247 goto found;
250 /* Bad luck. */
251 gui_msgbar_warn(_("Not found."));
252 return;
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
262 * first search.
264 static void
265 gui_input_search(void)
267 /* Always ask for a search string */
268 if (gui_input_asksearch() != 0)
269 return;
271 /* Just simulate a 'n' button */
272 gui_input_searchnext();
276 * @brief Ask for a search string and filter matching files in the file
277 * browser.
279 static void
280 gui_input_locate(void)
282 /* Always ask for a search string */
283 if (gui_input_asksearch() != 0)
284 return;
286 /* Perform the serach */
287 if (gui_browser_locate(cursearch) != 0)
288 gui_msgbar_warn(_("Not found."));
289 else
290 gui_msgbar_flush();
294 * @brief Instruct the playlist to seek the current song 5 seconds
295 * backward.
297 static void
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
305 * forward.
307 static void
308 gui_input_cursong_seek_forward(void)
310 playq_cursong_seek(5, 1);
314 * @brief Approve input string format for the seeking option.
316 static int
317 gui_input_cursong_seek_validator(const char *str, char c)
319 const char *s;
321 if (c == '+' || c == '-') {
322 /* Only allow + and - at the beginning of the statement */
323 if (str[0] != '\0')
324 return (-1);
325 } else if (c == ':') {
326 /* Don't allow : before a digit has been inserted */
327 if (strpbrk(str, "0123456789") == NULL)
328 return (-1);
330 s = strrchr(str, ':');
331 if (s != NULL) {
332 /* Only allow colon after two decimals */
333 if (strlen(s + 1) != 2)
334 return (-1);
335 /* Only allow two : characters in the string */
336 if (s - strchr(str, ':') == 3)
337 return (-1);
339 } else if (g_ascii_isdigit(c)) {
340 s = strrchr(str, ':');
341 if (s != NULL) {
342 /* Only allow up to two decimals after a colon */
343 if (strlen(s + 1) == 2)
344 return (-1);
345 /* Only allow '0' to '5' as the first digit */
346 if (s[1] == '\0' && c > '5')
347 return (-1);
349 } else {
350 /* Don't allow any other characters */
351 return (-1);
354 return (0);
358 * @brief Ask the user to specify a position to seek the current song to.
360 static void
361 gui_input_cursong_seek_jump(void)
363 char *str, *t;
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);
368 if (str == NULL)
369 return;
371 for (t = str; *t != '\0'; t++) {
372 switch (*t) {
373 case ':':
375 * Only allow two :'s, not without a prepending
376 * digit. :'s must be interleaved with two
377 * digits.
379 g_assert(split <= 1 && digit != 0);
380 g_assert(split == 0 || digit == 2);
381 split++;
382 digit = 0;
383 break;
384 case '+':
385 /* Must be at the beginning */
386 g_assert(t == str);
387 relative = 1;
388 break;
389 case '-':
390 /* Must be at the beginning */
391 g_assert(t == str);
392 relative = -1;
393 break;
394 default:
395 /* Regular digit */
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;
401 total += value;
402 digit++;
406 /* Not enough trailing digits */
407 if (split > 0 && digit != 2)
408 goto bad;
410 if (relative != 0) {
411 total *= relative;
412 if (total == 0)
413 goto bad;
415 playq_cursong_seek(total, relative);
417 /* Show the string the next time as well */
418 g_free(curseek);
419 curseek = str;
420 return;
422 bad: gui_msgbar_warn(_("Bad time format."));
423 g_free(str);
427 * @brief A simple binding from a keyboard character input to a function.
429 struct gui_binding {
431 * @brief The window that should be focussed.
433 int focus;
435 * @brief The character that should be pressed.
437 int input;
439 * @brief The function that will be run.
441 void (*func)(void);
445 * @brief List of keybindings available in the GUI.
447 static struct gui_binding kbdbindings[] = {
448 /* Application-wide keyboard bindings */
449 #ifdef BUILD_VOLUME
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))
527 void
528 gui_input_sigmask(void)
530 #ifdef G_THREADS_IMPL_POSIX
531 sigset_t sset;
533 sigemptyset(&sset);
534 sigaddset(&sset, SIGHUP);
535 sigaddset(&sset, SIGINT);
536 sigaddset(&sset, SIGPIPE);
537 sigaddset(&sset, SIGQUIT);
538 sigaddset(&sset, SIGTERM);
539 #ifdef SIGWINCH
540 sigaddset(&sset, SIGWINCH);
541 #endif /* SIGWINCH */
542 pthread_sigmask(SIG_BLOCK, &sset, NULL);
543 #endif /* G_THREADS_IMPL_POSIX */
546 #ifdef G_OS_UNIX
548 * @brief Handler of all incoming signals with a custom action.
550 static void
551 gui_input_sighandler(int signal)
553 if (shutting_down)
554 return;
556 switch (signal) {
557 case SIGHUP:
558 case SIGINT:
559 case SIGPIPE:
560 case SIGQUIT:
561 case SIGTERM:
562 gui_input_quit();
563 g_assert_not_reached();
566 #endif /* G_OS_UNIX */
568 void
569 gui_input_loop(void)
571 int ch;
572 unsigned int i;
574 #ifdef 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 */
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 #ifdef BUILD_DBUS
594 dbus_lock();
595 kbdbindings[i].func();
596 dbus_unlock();
597 #else /* !BUILD_DBUS */
598 kbdbindings[i].func();
599 #endif /* BUILD_DBUS */
600 break;
603 gui_draw_done();
608 gui_input_askyesno(const char *question)
610 char *msg, input;
611 const char *yes, *no;
612 int ret;
614 /* Skip the question if the user really wants so */
615 if (!config_getopt_bool("gui.input.confirm"))
616 return (0);
618 yes = _("yes");
619 no = _("no");
621 /* Print the question on screen */
622 msg = g_strdup_printf("%s ([%s]/%s): ", question, yes, no);
623 gui_msgbar_ask(msg);
624 g_free(msg);
626 for (;;) {
627 input = gui_input_getch();
629 #ifdef BUILD_NLS
630 /* Localized yes/no buttons */
631 if (input == yes[0]) {
632 ret = 0;
633 goto done;
634 } else if (input == no[0]) {
635 ret = -1;
636 goto done;
638 #endif /* BUILD_NLS */
640 /* Default y/n behaviour */
641 switch (input) {
642 case 'y':
643 case 'Y':
644 case '\r':
645 ret = 0;
646 goto done;
647 case CTRL('['):
648 case 'n':
649 case 'N':
650 case CTRL('C'):
651 ret = -1;
652 goto done;
655 done:
656 gui_msgbar_flush();
657 return (ret);
661 * @brief Find the offset to where a string should be trimmed to remove
662 * one word or special character sequence, including trailing
663 * whitespace.
665 static int
666 gui_input_trimword(GString *gs)
668 const char *end;
670 /* Last character */
671 end = (gs->str + gs->len) - 1;
673 /* Trim as much trailing whitespace as possible */
674 for (;;) {
675 if (end < gs->str) return (0);
676 if (!isspace(*end)) break;
677 end--;
680 if (isalnum(*end)) {
681 /* Trim alphanumerics */
682 do {
683 if (--end < gs->str) return (0);
684 } while (isalnum(*end));
685 } else {
686 /* Trim special characters */
687 do {
688 if (--end < gs->str) return (0);
689 } while (!isalnum(*end) && !isspace(*end));
692 return (end - gs->str) + 1;
695 char *
696 gui_input_askstring(const char *question, const char *defstr,
697 int (*validator)(const char *str, char c))
699 GString *msg;
700 unsigned int origlen, newlen;
701 int c, clearfirst = 0;
702 char *ret = NULL;
703 const char *vstr;
705 msg = g_string_new(question);
706 g_string_append(msg, ": ");
707 origlen = msg->len;
708 if (defstr != NULL) {
709 g_string_append(msg, defstr);
710 clearfirst = 1;
713 for(;;) {
714 gui_msgbar_ask(msg->str);
716 switch (c = gui_input_getch()) {
717 case '\r':
718 goto done;
719 case KEY_BACKSPACE:
720 clearfirst = 0;
721 if (msg->len > origlen) {
722 /* Prompt has contents */
723 g_string_truncate(msg, msg->len - 1);
725 break;
726 case CTRL('C'):
727 case CTRL('['):
728 /* Just empty the return */
729 g_string_truncate(msg, origlen);
730 goto done;
731 case CTRL('U'):
732 g_string_truncate(msg, origlen);
733 break;
734 case CTRL('W'):
735 clearfirst = 0;
736 newlen = gui_input_trimword(msg);
737 g_string_truncate(msg, MAX(newlen, origlen));
738 break;
739 default:
740 /* Control characters are not allowed */
741 if (g_ascii_iscntrl(c))
742 break;
744 if (validator != NULL) {
745 vstr = clearfirst ? "" : msg->str + origlen;
746 if (validator(vstr, c) != 0)
747 break;
749 if (clearfirst) {
750 g_string_truncate(msg, origlen);
751 clearfirst = 0;
753 g_string_append_c(msg, c);
757 done:
758 gui_msgbar_flush();
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);
765 return ret;