Code

update lyrics screen when a new song starts
[ncmpc.git] / src / screen_play.c
1 /* ncmpc (Ncurses MPD Client)
2  * (c) 2004-2009 The Music Player Daemon Project
3  * Project homepage: http://musicpd.org
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
20 #include "config.h"
21 #include "i18n.h"
22 #include "charset.h"
23 #include "options.h"
24 #include "mpdclient.h"
25 #include "utils.h"
26 #include "strfsong.h"
27 #include "wreadln.h"
28 #include "command.h"
29 #include "colors.h"
30 #include "screen.h"
31 #include "screen_utils.h"
32 #include "screen_play.h"
34 #ifndef NCMPC_MINI
35 #include "hscroll.h"
36 #endif
38 #include <ctype.h>
39 #include <stdlib.h>
40 #include <string.h>
41 #include <time.h>
42 #include <glib.h>
44 #define MAX_SONG_LENGTH 512
46 #ifndef NCMPC_MINI
47 typedef struct
48 {
49         GList **list;
50         GList **dir_list;
51         mpdclient_t *c;
52 } completion_callback_data_t;
53 #endif
55 static struct mpdclient_playlist *playlist;
56 static int current_song_id = -1;
57 static list_window_t *lw = NULL;
58 static guint timer_hide_cursor_id;
60 static void
61 play_paint(void);
63 static void
64 playlist_repaint(void)
65 {
66         play_paint();
67         wrefresh(lw->w);
68 }
70 static void
71 playlist_repaint_if_active(void)
72 {
73         if (screen_is_visible(&screen_playlist))
74                 playlist_repaint();
75 }
77 static void
78 playlist_changed_callback(mpdclient_t *c, int event, gpointer data)
79 {
80         switch (event) {
81         case PLAYLIST_EVENT_DELETE:
82                 break;
83         case PLAYLIST_EVENT_MOVE:
84                 if(lw->range_selection < 0)
85                 {
86                         lw->selected = *((int *) data);
87                         if (lw->selected < lw->start)
88                                 lw->start--;
89                 }
90                 break;
91         default:
92                 break;
93         }
95         list_window_check_selected(lw, c->playlist.list->len);
96         playlist_repaint_if_active();
97 }
99 static char *
100 format_duration(int duration)
102         if (duration == MPD_SONG_NO_TIME)
103                 return NULL;
105         return g_strdup_printf("%d:%02d", duration / 60, duration % 60);
108 static const char *
109 list_callback(unsigned idx, bool *highlight, char **second_column, G_GNUC_UNUSED void *data)
111         static char songname[MAX_SONG_LENGTH];
112 #ifndef NCMPC_MINI
113         static scroll_state_t st;
114 #endif
115         mpd_Song *song;
117         if (playlist == NULL || idx >= playlist_length(playlist))
118                 return NULL;
120         song = playlist_get(playlist, idx);
121         if (song->id == current_song_id)
122                 *highlight = true;
124         strfsong(songname, MAX_SONG_LENGTH, options.list_format, song);
126 #ifndef NCMPC_MINI
127         if(second_column)
128                 *second_column = format_duration(song->time);
130         if (idx == lw->selected)
131         {
132                 if (options.scroll && utf8_width(songname) > (unsigned)(COLS - strlen(*second_column) - 1) )
133                 {
134                         static unsigned current_song;
135                         char *tmp;
137                         if (current_song != lw->selected) {
138                                 st.offset = 0;
139                                 current_song = lw->selected;
140                         }
142                         tmp = strscroll(songname, options.scroll_sep,
143                                         MAX_SONG_LENGTH, &st);
144                         g_strlcpy(songname, tmp, MAX_SONG_LENGTH);
145                         g_free(tmp);
146                 }
147                 else
148                         st.offset = 0;
149         }
150 #else
151         (void)second_column;
152 #endif
154         return songname;
157 static void
158 center_playing_item(mpdclient_t *c, bool center_cursor)
160         unsigned length = c->playlist.list->len;
161         int idx;
163         if (!c->song || c->status == NULL ||
164                 IS_STOPPED(c->status->state))
165                 return;
167         /* try to center the song that are playing */
168         idx = playlist_get_index(c, c->song);
169         if (idx < 0)
170                 return;
172         if (length < lw->rows)
173         {
174                 if (center_cursor)
175                         list_window_set_selected(lw, idx);
176                 return;
177         }
179         list_window_center(lw, length, idx);
181         if (center_cursor) {
182                 list_window_set_selected(lw, idx);
183                 return;
184         }
186         /* make sure the cursor is in the window */
187         if (lw->selected < lw->start + options.scroll_offset) {
188                 if (lw->start > 0)
189                         lw->selected = lw->start + options.scroll_offset;
190                 if (lw->range_selection) {
191                         lw->selected_start = lw->range_base;
192                         lw->selected_end = lw->selected;
193                 } else {
194                         lw->selected_start = lw->selected;
195                         lw->selected_end = lw->selected;
196                 }
197         } else if (lw->selected > lw->start + lw->rows - 1 - options.scroll_offset) {
198                 if (lw->start + lw->rows < length)
199                         lw->selected = lw->start + lw->rows - 1 - options.scroll_offset;
200                 if (lw->range_selection) {
201                         lw->selected_start = lw->selected;
202                         lw->selected_end = lw->range_base;
203                 } else {
204                         lw->selected_start = lw->selected;
205                         lw->selected_end = lw->selected;
206                 }
207         }
210 #ifndef NCMPC_MINI
211 static void
212 save_pre_completion_cb(GCompletion *gcmp, G_GNUC_UNUSED gchar *line,
213                        void *data)
215         completion_callback_data_t *tmp = (completion_callback_data_t *)data;
216         GList **list = tmp->list;
217         mpdclient_t *c = tmp->c;
219         if( *list == NULL ) {
220                 /* create completion list */
221                 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_PLAYLIST);
222                 g_completion_add_items(gcmp, *list);
223         }
226 static void
227 save_post_completion_cb(G_GNUC_UNUSED GCompletion *gcmp,
228                         G_GNUC_UNUSED gchar *line, GList *items,
229                         G_GNUC_UNUSED void *data)
231         if (g_list_length(items) >= 1)
232                 screen_display_completion_list(items);
234 #endif
236 #ifndef NCMPC_MINI
237 /**
238  * Wrapper for strncmp().  We are not allowed to pass &strncmp to
239  * g_completion_set_compare(), because strncmp() takes size_t where
240  * g_completion_set_compare passes a gsize value.
241  */
242 static gint
243 completion_strncmp(const gchar *s1, const gchar *s2, gsize n)
245         return strncmp(s1, s2, n);
247 #endif
249 int
250 playlist_save(mpdclient_t *c, char *name, char *defaultname)
252         gchar *filename, *filename_utf8;
253         gint error;
254 #ifndef NCMPC_MINI
255         GCompletion *gcmp;
256         GList *list = NULL;
257         completion_callback_data_t data;
258 #endif
260 #ifdef NCMPC_MINI
261         (void)defaultname;
262 #endif
264 #ifndef NCMPC_MINI
265         if (name == NULL) {
266                 /* initialize completion support */
267                 gcmp = g_completion_new(NULL);
268                 g_completion_set_compare(gcmp, completion_strncmp);
269                 data.list = &list;
270                 data.dir_list = NULL;
271                 data.c = c;
272                 wrln_completion_callback_data = &data;
273                 wrln_pre_completion_callback = save_pre_completion_cb;
274                 wrln_post_completion_callback = save_post_completion_cb;
277                 /* query the user for a filename */
278                 filename = screen_readln(screen.status_window.w,
279                                          _("Save playlist as"),
280                                          defaultname,
281                                          NULL,
282                                          gcmp);
284                 /* destroy completion support */
285                 wrln_completion_callback_data = NULL;
286                 wrln_pre_completion_callback = NULL;
287                 wrln_post_completion_callback = NULL;
288                 g_completion_free(gcmp);
289                 list = string_list_free(list);
290                 if( filename )
291                         filename=g_strstrip(filename);
292         } else
293 #endif
294                         filename=g_strdup(name);
296         if (filename == NULL)
297                 return -1;
299         /* send save command to mpd */
301         filename_utf8 = locale_to_utf8(filename);
302         error = mpdclient_cmd_save_playlist(c, filename_utf8);
303         g_free(filename_utf8);
305         if (error) {
306                 gint code = GET_ACK_ERROR_CODE(error);
308                 if (code == MPD_ACK_ERROR_EXIST) {
309                         char *buf;
310                         int key;
312                         buf = g_strdup_printf(_("Replace %s [%s/%s] ? "),
313                                               filename, YES, NO);
314                         key = tolower(screen_getch(screen.status_window.w,
315                                                    buf));
316                         g_free(buf);
318                         if (key == YES[0]) {
319                                 filename_utf8 = locale_to_utf8(filename);
320                                 error = mpdclient_cmd_delete_playlist(c, filename_utf8);
321                                 g_free(filename_utf8);
323                                 if (error) {
324                                         g_free(filename);
325                                         return -1;
326                                 }
328                                 error = playlist_save(c, filename, NULL);
329                                 g_free(filename);
330                                 return error;
331                         }
333                         screen_status_printf(_("Aborted"));
334                 }
336                 g_free(filename);
337                 return -1;
338         }
340         /* success */
341         screen_status_printf(_("Saved %s"), filename);
342         g_free(filename);
343         return 0;
346 #ifndef NCMPC_MINI
347 static void add_dir(GCompletion *gcmp, gchar *dir, GList **dir_list,
348                     GList **list, mpdclient_t *c)
350         g_completion_remove_items(gcmp, *list);
351         *list = string_list_remove(*list, dir);
352         *list = gcmp_list_from_path(c, dir, *list, GCMP_TYPE_RFILE);
353         g_completion_add_items(gcmp, *list);
354         *dir_list = g_list_append(*dir_list, g_strdup(dir));
357 static void add_pre_completion_cb(GCompletion *gcmp, gchar *line, void *data)
359         completion_callback_data_t *tmp = (completion_callback_data_t *)data;
360         GList **dir_list = tmp->dir_list;
361         GList **list = tmp->list;
362         mpdclient_t *c = tmp->c;
364         if (*list == NULL) {
365                 /* create initial list */
366                 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_RFILE);
367                 g_completion_add_items(gcmp, *list);
368         } else if (line && line[0] && line[strlen(line)-1]=='/' &&
369                    string_list_find(*dir_list, line) == NULL) {
370                 /* add directory content to list */
371                 add_dir(gcmp, line, dir_list, list, c);
372         }
375 static void add_post_completion_cb(GCompletion *gcmp, gchar *line,
376                                    GList *items, void *data)
378         completion_callback_data_t *tmp = (completion_callback_data_t *)data;
379         GList **dir_list = tmp->dir_list;
380         GList **list = tmp->list;
381         mpdclient_t *c = tmp->c;
383         if (g_list_length(items) >= 1)
384                 screen_display_completion_list(items);
386         if (line && line[0] && line[strlen(line) - 1] == '/' &&
387             string_list_find(*dir_list, line) == NULL) {
388                 /* add directory content to list */
389                 add_dir(gcmp, line, dir_list, list, c);
390         }
392 #endif
394 static int
395 handle_add_to_playlist(mpdclient_t *c)
397         gchar *path;
398 #ifndef NCMPC_MINI
399         GCompletion *gcmp;
400         GList *list = NULL;
401         GList *dir_list = NULL;
402         completion_callback_data_t data;
404         /* initialize completion support */
405         gcmp = g_completion_new(NULL);
406         g_completion_set_compare(gcmp, completion_strncmp);
407         data.list = &list;
408         data.dir_list = &dir_list;
409         data.c = c;
410         wrln_completion_callback_data = &data;
411         wrln_pre_completion_callback = add_pre_completion_cb;
412         wrln_post_completion_callback = add_post_completion_cb;
413 #endif
415         /* get path */
416         path = screen_readln(screen.status_window.w,
417                              _("Add"),
418                              NULL,
419                              NULL,
420 #ifdef NCMPC_MINI
421                              NULL
422 #else
423                              gcmp
424 #endif
425                              );
427         /* destroy completion data */
428 #ifndef NCMPC_MINI
429         wrln_completion_callback_data = NULL;
430         wrln_pre_completion_callback = NULL;
431         wrln_post_completion_callback = NULL;
432         g_completion_free(gcmp);
433         string_list_free(list);
434         string_list_free(dir_list);
435 #endif
437         /* add the path to the playlist */
438         if (path != NULL) {
439                 char *path_utf8 = locale_to_utf8(path);
440                 mpdclient_cmd_add_path(c, path_utf8);
441                 g_free(path_utf8);
442         }
444         g_free(path);
445         return 0;
448 static void
449 play_init(WINDOW *w, int cols, int rows)
451         lw = list_window_init(w, cols, rows);
454 static gboolean
455 timer_hide_cursor(gpointer data)
457         struct mpdclient *c = data;
459         assert(options.hide_cursor > 0);
460         assert(timer_hide_cursor_id != 0);
462         timer_hide_cursor_id = 0;
464         /* hide the cursor when mpd is playing and the user is inactive */
466         if (c->status != NULL && c->status->state == MPD_STATUS_STATE_PLAY) {
467                 lw->hide_cursor = true;
468                 playlist_repaint();
469         } else
470                 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
471                                                      timer_hide_cursor, c);
473         return FALSE;
476 static void
477 play_open(mpdclient_t *c)
479         static gboolean install_cb = TRUE;
481         playlist = &c->playlist;
483         assert(timer_hide_cursor_id == 0);
484         if (options.hide_cursor > 0) {
485                 lw->hide_cursor = false;
486                 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
487                                                      timer_hide_cursor, c);
488         }
490         if (install_cb) {
491                 mpdclient_install_playlist_callback(c, playlist_changed_callback);
492                 install_cb = FALSE;
493         }
496 static void
497 play_close(void)
499         if (timer_hide_cursor_id != 0) {
500                 g_source_remove(timer_hide_cursor_id);
501                 timer_hide_cursor_id = 0;
502         }
505 static void
506 play_resize(int cols, int rows)
508         lw->cols = cols;
509         lw->rows = rows;
513 static void
514 play_exit(void)
516         list_window_free(lw);
519 static const char *
520 play_title(char *str, size_t size)
522         if( strcmp(options.host, "localhost") == 0 )
523                 return _("Playlist");
525         g_snprintf(str, size, _("Playlist on %s"), options.host);
526         return str;
529 static void
530 play_paint(void)
532         list_window_paint(lw, list_callback, NULL);
535 static void
536 play_update(mpdclient_t *c)
538         static int prev_song_id = -1;
540         current_song_id = c->song != NULL && c->status != NULL &&
541                 !IS_STOPPED(c->status->state) ? c->song->id : -1;
543         if (current_song_id != prev_song_id) {
544                 prev_song_id = current_song_id;
546                 /* center the cursor */
547                 if (options.auto_center && current_song_id != -1 && ! lw->range_selection)
548                         center_playing_item(c, false);
550                 playlist_repaint();
551 #ifndef NCMPC_MINI
552         } else if (options.scroll) {
553                 /* always repaint if horizontal scrolling is
554                    enabled */
555                 playlist_repaint();
556 #endif
557         }
560 #ifdef HAVE_GETMOUSE
561 static bool
562 handle_mouse_event(struct mpdclient *c)
564         int row;
565         unsigned selected;
566         unsigned long bstate;
568         if (screen_get_mouse_event(c, &bstate, &row) ||
569             list_window_mouse(lw, playlist_length(playlist), bstate, row)) {
570                 playlist_repaint();
571                 return true;
572         }
574         if (bstate & BUTTON1_DOUBLE_CLICKED) {
575                 /* stop */
576                 screen_cmd(c, CMD_STOP);
577                 return true;
578         }
580         selected = lw->start + row;
582         if (bstate & BUTTON1_CLICKED) {
583                 /* play */
584                 if (lw->start + row < playlist_length(playlist))
585                         mpdclient_cmd_play(c, lw->start + row);
586         } else if (bstate & BUTTON3_CLICKED) {
587                 /* delete */
588                 if (selected == lw->selected)
589                         mpdclient_cmd_delete(c, lw->selected);
590         }
592         lw->selected = selected;
593         list_window_check_selected(lw, playlist_length(playlist));
594         playlist_repaint();
596         return true;
598 #endif
600 static bool
601 play_cmd(mpdclient_t *c, command_t cmd)
603         static command_t cached_cmd = CMD_NONE;
604         command_t prev_cmd = cached_cmd;
605         cached_cmd = cmd;
607         lw->hide_cursor = false;
609         if (options.hide_cursor > 0) {
610                 if (timer_hide_cursor_id != 0)
611                         g_source_remove(timer_hide_cursor_id);
612                 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
613                                                      timer_hide_cursor, c);
614         }
616         if (list_window_cmd(lw, playlist_length(&c->playlist), cmd)) {
617                 playlist_repaint();
618                 return true;
619         }
621         switch(cmd) {
622         case CMD_PLAY:
623                 mpdclient_cmd_play(c, lw->selected);
624                 return true;
625         case CMD_DELETE:
626         {
627                 int i = lw->selected_end, start = lw->selected_start;
628                 for(; i >= start; --i)
629                         mpdclient_cmd_delete(c, i);
631                 i++;
632                 if(i >= (int)playlist_length(&c->playlist))
633                         i--;
634                 lw->selected = i;
635                 lw->selected_start = i;
636                 lw->selected_end = i;
637                 lw->range_selection = false;
639                 return true;
640         }
641         case CMD_SAVE_PLAYLIST:
642                 playlist_save(c, NULL, NULL);
643                 return true;
644         case CMD_ADD:
645                 handle_add_to_playlist(c);
646                 return true;
647         case CMD_SCREEN_UPDATE:
648                 center_playing_item(c, prev_cmd == CMD_SCREEN_UPDATE);
649                 playlist_repaint();
650                 return false;
651         case CMD_SELECT_PLAYING:
652                 list_window_set_selected(lw, playlist_get_index(c, c->song));
653                 return true;
654         case CMD_SHUFFLE:
655         {
656                 if(!lw->range_selection)
657                         /* No range selection, shuffle all list. */
658                         break;
660                 if (mpdclient_cmd_shuffle_range(c, lw->selected_start, lw->selected_end+1) == 0)
661                         screen_status_message(_("Shuffled playlist"));
663                 return true;
664         }
665         case CMD_LIST_MOVE_UP:
666                 if(lw->selected_start == 0)
667                         return false;
668                 if(lw->range_selection)
669                 {
670                         unsigned i = lw->selected_start;
671                         unsigned last_selected = lw->selected;
672                         for(; i <= lw->selected_end; ++i)
673                                 mpdclient_cmd_move(c, i, i-1);
674                         lw->selected_start--;
675                         lw->selected_end--;
676                         lw->selected = last_selected - 1;
677                         lw->range_base--;
678                 }
679                 else
680                 {
681                         mpdclient_cmd_move(c, lw->selected, lw->selected-1);
682                         lw->selected--;
683                         lw->selected_start--;
684                         lw->selected_end--;
685                 }
686                 return true;
687         case CMD_LIST_MOVE_DOWN:
688                 if(lw->selected_end+1 >= playlist_length(&c->playlist))
689                         return false;
690                 if(lw->range_selection)
691                 {
692                         int i = lw->selected_end;
693                         unsigned last_selected = lw->selected;
694                         for(; i >= (int)lw->selected_start; --i)
695                                 mpdclient_cmd_move(c, i, i+1);
696                         lw->selected_start++;
697                         lw->selected_end++;
698                         lw->selected = last_selected + 1;
699                         lw->range_base++;
700                 }
701                 else
702                 {
703                         mpdclient_cmd_move(c, lw->selected, lw->selected+1);
704                         lw->selected++;
705                         lw->selected_start++;
706                         lw->selected_end++;
707                 }
708                 return true;
709         case CMD_LIST_FIND:
710         case CMD_LIST_RFIND:
711         case CMD_LIST_FIND_NEXT:
712         case CMD_LIST_RFIND_NEXT:
713                 screen_find(lw, playlist_length(&c->playlist),
714                             cmd, list_callback, NULL);
715                 playlist_repaint();
716                 return true;
717         case CMD_LIST_JUMP:
718                 screen_jump(lw, list_callback, NULL);
719                 playlist_repaint();
720                 return true;
722 #ifdef HAVE_GETMOUSE
723         case CMD_MOUSE_EVENT:
724                 return handle_mouse_event(c);
725 #endif
727 #ifdef ENABLE_SONG_SCREEN
728         case CMD_SCREEN_SONG:
729                 if (lw->selected < playlist_length(&c->playlist)) {
730                         screen_song_switch(c, playlist_get(&c->playlist, lw->selected));
731                         return true;
732                 }
734                 break;
735 #endif
737         case CMD_LOCATE:
738                 if (lw->selected < playlist_length(&c->playlist)) {
739                         screen_file_goto_song(c, playlist_get(&c->playlist, lw->selected));
740                         return true;
741                 }
743                 break;
745 #ifdef ENABLE_LYRICS_SCREEN
746         case CMD_SCREEN_LYRICS:
747                 if (lw->selected < playlist_length(&c->playlist)) {
748                         struct mpd_song *selected = playlist_get(&c->playlist, lw->selected);
749                         bool follow = false;
751                         if (c->song && selected &&
752                             !strcmp(selected->file, c->song->file))
753                                 follow = true;
755                         screen_lyrics_switch(c, selected, follow);
756                         return true;
757                 }
759                 break;
760 #endif
761         case CMD_SCREEN_SWAP:
762                 screen_swap(c, playlist_get(&c->playlist, lw->selected));
763                 return true;
765         default:
766                 break;
767         }
769         return false;
772 const struct screen_functions screen_playlist = {
773         .init = play_init,
774         .exit = play_exit,
775         .open = play_open,
776         .close = play_close,
777         .resize = play_resize,
778         .paint = play_paint,
779         .update = play_update,
780         .cmd = play_cmd,
781         .get_title = play_title,
782 };