Code

screen_play: Added the jump command to the playlist screen.
[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->visual_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 const char *
100 list_callback(unsigned idx, bool *highlight, G_GNUC_UNUSED void *data)
102         static char songname[MAX_SONG_LENGTH];
103 #ifndef NCMPC_MINI
104         static scroll_state_t st;
105 #endif
106         mpd_Song *song;
108         if (playlist == NULL || idx >= playlist_length(playlist))
109                 return NULL;
111         song = playlist_get(playlist, idx);
112         if (song->id == current_song_id)
113                 *highlight = true;
115         strfsong(songname, MAX_SONG_LENGTH, options.list_format, song);
117 #ifndef NCMPC_MINI
118         if (options.scroll && (unsigned)song->pos == lw->selected &&
119             utf8_width(songname) > (unsigned)COLS) {
120                 static unsigned current_song;
121                 char *tmp;
123                 if (current_song != lw->selected) {
124                         st.offset = 0;
125                         current_song = lw->selected;
126                 }
128                 tmp = strscroll(songname, options.scroll_sep,
129                                 MAX_SONG_LENGTH, &st);
130                 g_strlcpy(songname, tmp, MAX_SONG_LENGTH);
131                 g_free(tmp);
132         } else if ((unsigned)song->pos == lw->selected)
133                 st.offset = 0;
134 #endif
136         return songname;
139 static void
140 center_playing_item(mpdclient_t *c)
142         unsigned length = c->playlist.list->len;
143         unsigned offset = lw->selected - lw->start;
144         int idx;
146         if (!c->song || length < lw->rows ||
147             c->status == NULL || IS_STOPPED(c->status->state))
148                 return;
150         /* try to center the song that are playing */
151         idx = playlist_get_index(c, c->song);
152         if (idx < 0)
153                 return;
155         list_window_center(lw, length, idx);
157         /* make sure the cursor is in the window */
158         lw->selected = lw->start+offset;
159         lw->selected_start = lw->selected;
160         lw->selected_end = lw->selected;
161         list_window_check_selected(lw, length);
164 #ifndef NCMPC_MINI
165 static void
166 save_pre_completion_cb(GCompletion *gcmp, G_GNUC_UNUSED gchar *line,
167                        void *data)
169         completion_callback_data_t *tmp = (completion_callback_data_t *)data;
170         GList **list = tmp->list;
171         mpdclient_t *c = tmp->c;
173         if( *list == NULL ) {
174                 /* create completion list */
175                 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_PLAYLIST);
176                 g_completion_add_items(gcmp, *list);
177         }
180 static void
181 save_post_completion_cb(G_GNUC_UNUSED GCompletion *gcmp,
182                         G_GNUC_UNUSED gchar *line, GList *items,
183                         G_GNUC_UNUSED void *data)
185         if (g_list_length(items) >= 1)
186                 screen_display_completion_list(items);
188 #endif
190 #ifndef NCMPC_MINI
191 /**
192  * Wrapper for strncmp().  We are not allowed to pass &strncmp to
193  * g_completion_set_compare(), because strncmp() takes size_t where
194  * g_completion_set_compare passes a gsize value.
195  */
196 static gint
197 completion_strncmp(const gchar *s1, const gchar *s2, gsize n)
199         return strncmp(s1, s2, n);
201 #endif
203 int
204 playlist_save(mpdclient_t *c, char *name, char *defaultname)
206         gchar *filename, *filename_utf8;
207         gint error;
208 #ifndef NCMPC_MINI
209         GCompletion *gcmp;
210         GList *list = NULL;
211         completion_callback_data_t data;
212 #endif
214 #ifdef NCMPC_MINI
215         (void)defaultname;
216 #endif
218 #ifndef NCMPC_MINI
219         if (name == NULL) {
220                 /* initialize completion support */
221                 gcmp = g_completion_new(NULL);
222                 g_completion_set_compare(gcmp, completion_strncmp);
223                 data.list = &list;
224                 data.dir_list = NULL;
225                 data.c = c;
226                 wrln_completion_callback_data = &data;
227                 wrln_pre_completion_callback = save_pre_completion_cb;
228                 wrln_post_completion_callback = save_post_completion_cb;
231                 /* query the user for a filename */
232                 filename = screen_readln(screen.status_window.w,
233                                          _("Save playlist as: "),
234                                          defaultname,
235                                          NULL,
236                                          gcmp);
238                 /* destroy completion support */
239                 wrln_completion_callback_data = NULL;
240                 wrln_pre_completion_callback = NULL;
241                 wrln_post_completion_callback = NULL;
242                 g_completion_free(gcmp);
243                 list = string_list_free(list);
244                 if( filename )
245                         filename=g_strstrip(filename);
246         } else
247 #endif
248                         filename=g_strdup(name);
250         if (filename == NULL)
251                 return -1;
253         /* send save command to mpd */
255         filename_utf8 = locale_to_utf8(filename);
256         error = mpdclient_cmd_save_playlist(c, filename_utf8);
257         g_free(filename_utf8);
259         if (error) {
260                 gint code = GET_ACK_ERROR_CODE(error);
262                 if (code == MPD_ACK_ERROR_EXIST) {
263                         char *buf;
264                         int key;
266                         buf = g_strdup_printf(_("Replace %s [%s/%s] ? "),
267                                               filename, YES, NO);
268                         key = tolower(screen_getch(screen.status_window.w,
269                                                    buf));
270                         g_free(buf);
272                         if (key == YES[0]) {
273                                 filename_utf8 = locale_to_utf8(filename);
274                                 error = mpdclient_cmd_delete_playlist(c, filename_utf8);
275                                 g_free(filename_utf8);
277                                 if (error) {
278                                         g_free(filename);
279                                         return -1;
280                                 }
282                                 error = playlist_save(c, filename, NULL);
283                                 g_free(filename);
284                                 return error;
285                         }
287                         screen_status_printf(_("Aborted"));
288                 }
290                 g_free(filename);
291                 return -1;
292         }
294         /* success */
295         screen_status_printf(_("Saved %s"), filename);
296         g_free(filename);
297         return 0;
300 #ifndef NCMPC_MINI
301 static void add_dir(GCompletion *gcmp, gchar *dir, GList **dir_list,
302                     GList **list, mpdclient_t *c)
304         g_completion_remove_items(gcmp, *list);
305         *list = string_list_remove(*list, dir);
306         *list = gcmp_list_from_path(c, dir, *list, GCMP_TYPE_RFILE);
307         g_completion_add_items(gcmp, *list);
308         *dir_list = g_list_append(*dir_list, g_strdup(dir));
311 static void add_pre_completion_cb(GCompletion *gcmp, gchar *line, void *data)
313         completion_callback_data_t *tmp = (completion_callback_data_t *)data;
314         GList **dir_list = tmp->dir_list;
315         GList **list = tmp->list;
316         mpdclient_t *c = tmp->c;
318         if (*list == NULL) {
319                 /* create initial list */
320                 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_RFILE);
321                 g_completion_add_items(gcmp, *list);
322         } else if (line && line[0] && line[strlen(line)-1]=='/' &&
323                    string_list_find(*dir_list, line) == NULL) {
324                 /* add directory content to list */
325                 add_dir(gcmp, line, dir_list, list, c);
326         }
329 static void add_post_completion_cb(GCompletion *gcmp, gchar *line,
330                                    GList *items, void *data)
332         completion_callback_data_t *tmp = (completion_callback_data_t *)data;
333         GList **dir_list = tmp->dir_list;
334         GList **list = tmp->list;
335         mpdclient_t *c = tmp->c;
337         if (g_list_length(items) >= 1)
338                 screen_display_completion_list(items);
340         if (line && line[0] && line[strlen(line) - 1] == '/' &&
341             string_list_find(*dir_list, line) == NULL) {
342                 /* add directory content to list */
343                 add_dir(gcmp, line, dir_list, list, c);
344         }
346 #endif
348 static int
349 handle_add_to_playlist(mpdclient_t *c)
351         gchar *path;
352 #ifndef NCMPC_MINI
353         GCompletion *gcmp;
354         GList *list = NULL;
355         GList *dir_list = NULL;
356         completion_callback_data_t data;
358         /* initialize completion support */
359         gcmp = g_completion_new(NULL);
360         g_completion_set_compare(gcmp, completion_strncmp);
361         data.list = &list;
362         data.dir_list = &dir_list;
363         data.c = c;
364         wrln_completion_callback_data = &data;
365         wrln_pre_completion_callback = add_pre_completion_cb;
366         wrln_post_completion_callback = add_post_completion_cb;
367 #endif
369         /* get path */
370         path = screen_readln(screen.status_window.w,
371                              _("Add: "),
372                              NULL,
373                              NULL,
374 #ifdef NCMPC_MINI
375                              NULL
376 #else
377                              gcmp
378 #endif
379                              );
381         /* destroy completion data */
382 #ifndef NCMPC_MINI
383         wrln_completion_callback_data = NULL;
384         wrln_pre_completion_callback = NULL;
385         wrln_post_completion_callback = NULL;
386         g_completion_free(gcmp);
387         string_list_free(list);
388         string_list_free(dir_list);
389 #endif
391         /* add the path to the playlist */
392         if (path != NULL) {
393                 char *path_utf8 = locale_to_utf8(path);
394                 mpdclient_cmd_add_path(c, path_utf8);
395                 g_free(path_utf8);
396         }
398         g_free(path);
399         return 0;
402 static void
403 play_init(WINDOW *w, int cols, int rows)
405         lw = list_window_init(w, cols, rows);
408 static gboolean
409 timer_hide_cursor(gpointer data)
411         struct mpdclient *c = data;
413         assert(options.hide_cursor > 0);
414         assert(timer_hide_cursor_id != 0);
416         timer_hide_cursor_id = 0;
418         /* hide the cursor when mpd is playing and the user is inactive */
420         if (c->status != NULL && c->status->state == MPD_STATUS_STATE_PLAY) {
421                 lw->hide_cursor = true;
422                 playlist_repaint();
423         } else
424                 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
425                                                      timer_hide_cursor, c);
427         return FALSE;
430 static void
431 play_open(mpdclient_t *c)
433         static gboolean install_cb = TRUE;
435         playlist = &c->playlist;
437         assert(timer_hide_cursor_id == 0);
438         if (options.hide_cursor > 0) {
439                 lw->hide_cursor = false;
440                 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
441                                                      timer_hide_cursor, c);
442         }
444         if (install_cb) {
445                 mpdclient_install_playlist_callback(c, playlist_changed_callback);
446                 install_cb = FALSE;
447         }
450 static void
451 play_close(void)
453         if (timer_hide_cursor_id != 0) {
454                 g_source_remove(timer_hide_cursor_id);
455                 timer_hide_cursor_id = 0;
456         }
459 static void
460 play_resize(int cols, int rows)
462         lw->cols = cols;
463         lw->rows = rows;
467 static void
468 play_exit(void)
470         list_window_free(lw);
473 static const char *
474 play_title(char *str, size_t size)
476         if( strcmp(options.host, "localhost") == 0 )
477                 return _("Playlist");
479         g_snprintf(str, size, _("Playlist on %s"), options.host);
480         return str;
483 static void
484 play_paint(void)
486         list_window_paint(lw, list_callback, NULL);
489 static void
490 play_update(mpdclient_t *c)
492         static int prev_song_id = -1;
494         current_song_id = c->song != NULL && c->status != NULL &&
495                 !IS_STOPPED(c->status->state) ? c->song->id : -1;
497         if (current_song_id != prev_song_id) {
498                 prev_song_id = current_song_id;
500                 /* center the cursor */
501                 if (options.auto_center && current_song_id != -1)
502                         center_playing_item(c);
504                 playlist_repaint();
505 #ifndef NCMPC_MINI
506         } else if (options.scroll) {
507                 /* always repaint if horizontal scrolling is
508                    enabled */
509                 playlist_repaint();
510 #endif
511         }
514 #ifdef HAVE_GETMOUSE
515 static bool
516 handle_mouse_event(struct mpdclient *c)
518         int row;
519         unsigned selected;
520         unsigned long bstate;
522         if (screen_get_mouse_event(c, &bstate, &row) ||
523             list_window_mouse(lw, playlist_length(playlist), bstate, row)) {
524                 playlist_repaint();
525                 return true;
526         }
528         if (bstate & BUTTON1_DOUBLE_CLICKED) {
529                 /* stop */
530                 screen_cmd(c, CMD_STOP);
531                 return true;
532         }
534         selected = lw->start + row;
536         if (bstate & BUTTON1_CLICKED) {
537                 /* play */
538                 if (lw->start + row < playlist_length(playlist))
539                         mpdclient_cmd_play(c, lw->start + row);
540         } else if (bstate & BUTTON3_CLICKED) {
541                 /* delete */
542                 if (selected == lw->selected)
543                         mpdclient_cmd_delete(c, lw->selected);
544         }
546         lw->selected = selected;
547         list_window_check_selected(lw, playlist_length(playlist));
548         playlist_repaint();
550         return true;
552 #endif
554 static bool
555 play_cmd(mpdclient_t *c, command_t cmd)
557         lw->hide_cursor = false;
559         if (options.hide_cursor > 0) {
560                 if (timer_hide_cursor_id != 0)
561                         g_source_remove(timer_hide_cursor_id);
562                 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
563                                                      timer_hide_cursor, c);
564         }
566         if (list_window_cmd(lw, playlist_length(&c->playlist), cmd)) {
567                 playlist_repaint();
568                 return true;
569         }
571         switch(cmd) {
572         case CMD_PLAY:
573                 mpdclient_cmd_play(c, lw->selected);
574                 return true;
575         case CMD_DELETE:
576         {
577                 int i = lw->selected_end, start = lw->selected_start;
578                 for(; i >= start; --i)
579                         mpdclient_cmd_delete(c, i);
581                 i++;
582                 if(i >= (int)playlist_length(&c->playlist))
583                         i--;
584                 lw->selected = i;
585                 lw->selected_start = i;
586                 lw->selected_end = i;
587                 lw->visual_selection = false;
589                 return true;
590         }
591         case CMD_SAVE_PLAYLIST:
592                 playlist_save(c, NULL, NULL);
593                 return true;
594         case CMD_ADD:
595                 handle_add_to_playlist(c);
596                 return true;
597         case CMD_SCREEN_UPDATE:
598                 center_playing_item(c);
599                 playlist_repaint();
600                 return false;
601         case CMD_SHUFFLE:
602         {
603                 unsigned i = lw->selected_start + 1;
604                 unsigned last_selected = lw->selected;
605                 if(!lw->visual_selection)
606                         /* No visual selection, shuffle all list. */
607                         break;
608                 for(; i <= lw->selected_end; ++i)
609                         mpdclient_cmd_move(c, i, lw->selected_start + (rand() % ((i - lw->selected_start) + 1)));
611                 lw->selected = last_selected;
612                 screen_status_printf(_("Shuffled selection!"));
614                 return true;
615         }
616         case CMD_LIST_MOVE_UP:
617                 if(lw->selected_start == 0)
618                         return false;
619                 if(lw->visual_selection)
620                 {
621                         unsigned i = lw->selected_start;
622                         unsigned last_selected = lw->selected;
623                         for(; i <= lw->selected_end; ++i)
624                                 mpdclient_cmd_move(c, i, i-1);
625                         lw->selected_start--;
626                         lw->selected_end--;
627                         lw->selected = last_selected - 1;
628                         lw->visual_base--;
629                 }
630                 else
631                         mpdclient_cmd_move(c, lw->selected, lw->selected-1);
632                 return true;
633         case CMD_LIST_MOVE_DOWN:
634                 if(lw->selected_end+1 >= playlist_length(&c->playlist))
635                         return false;
636                 if(lw->visual_selection)
637                 {
638                         int i = lw->selected_end;
639                         unsigned last_selected = lw->selected;
640                         for(; i >= (int)lw->selected_start; --i)
641                                 mpdclient_cmd_move(c, i, i+1);
642                         lw->selected_start++;
643                         lw->selected_end++;
644                         lw->selected = last_selected + 1;
645                         lw->visual_base++;
646                 }
647                 else
648                         mpdclient_cmd_move(c, lw->selected, lw->selected+1);
649                 return true;
650         case CMD_LIST_FIND:
651         case CMD_LIST_RFIND:
652         case CMD_LIST_FIND_NEXT:
653         case CMD_LIST_RFIND_NEXT:
654                 screen_find(lw, playlist_length(&c->playlist),
655                             cmd, list_callback, NULL);
656                 playlist_repaint();
657                 return true;
658         case CMD_LIST_JUMP:
659                 screen_jump(lw, list_callback, NULL);
660                 playlist_repaint();
661                 return true;
663 #ifdef HAVE_GETMOUSE
664         case CMD_MOUSE_EVENT:
665                 return handle_mouse_event(c);
666 #endif
668 #ifdef ENABLE_SONG_SCREEN
669         case CMD_VIEW:
670                 if (lw->selected < playlist_length(&c->playlist)) {
671                         screen_song_switch(c, playlist_get(&c->playlist, lw->selected));
672                         return true;
673                 }
675                 break;
676 #endif
678         case CMD_LOCATE:
679                 if (lw->selected < playlist_length(&c->playlist)) {
680                         screen_file_goto_song(c, playlist_get(&c->playlist, lw->selected));
681                         return true;
682                 }
684                 break;
686 #ifdef ENABLE_LYRICS_SCREEN
687         case CMD_SCREEN_LYRICS:
688                 if (lw->selected < playlist_length(&c->playlist)) {
689                         screen_lyrics_switch(c, playlist_get(&c->playlist, lw->selected));
690                         return true;
691                 }
693                 break;
694 #endif
696         default:
697                 break;
698         }
700         return false;
703 const struct screen_functions screen_playlist = {
704         .init = play_init,
705         .exit = play_exit,
706         .open = play_open,
707         .close = play_close,
708         .resize = play_resize,
709         .paint = play_paint,
710         .update = play_update,
711         .cmd = play_cmd,
712         .get_title = play_title,
713 };