fb272e17df8324ae5f1d0695857e9337b1151f2b
[ncmpc-debian.git] / src / screen_queue.cxx
1 /* ncmpc (Ncurses MPD Client)
2  * (c) 2004-2018 The Music Player Daemon Project
3  * Project homepage: http://musicpd.org
4  *
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.
9  *
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.
14  *
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  */
19
20 #include "screen_queue.hxx"
21 #include "screen_interface.hxx"
22 #include "ListPage.hxx"
23 #include "screen_file.hxx"
24 #include "screen_status.hxx"
25 #include "screen_find.hxx"
26 #include "save_playlist.hxx"
27 #include "config.h"
28 #include "i18n.h"
29 #include "charset.hxx"
30 #include "options.hxx"
31 #include "mpdclient.hxx"
32 #include "strfsong.hxx"
33 #include "wreadln.hxx"
34 #include "Completion.hxx"
35 #include "song_paint.hxx"
36 #include "screen.hxx"
37 #include "screen_utils.hxx"
38 #include "screen_song.hxx"
39 #include "screen_lyrics.hxx"
40 #include "db_completion.hxx"
41 #include "Compiler.h"
42
43 #ifndef NCMPC_MINI
44 #include "hscroll.hxx"
45 #endif
46
47 #include <mpd/client.h>
48
49 #include <set>
50 #include <string>
51
52 #include <ctype.h>
53 #include <string.h>
54 #include <glib.h>
55
56 #define MAX_SONG_LENGTH 512
57
58 class QueuePage final : public ListPage {
59         ScreenManager &screen;
60
61 #ifndef NCMPC_MINI
62         mutable class hscroll hscroll;
63 #endif
64
65         MpdQueue *playlist = nullptr;
66         int current_song_id = -1;
67         int selected_song_id = -1;
68         guint timer_hide_cursor_id = 0;
69
70         unsigned last_connection_id = 0;
71         char *connection_name = nullptr;
72
73         bool playing = false;
74
75 public:
76         QueuePage(ScreenManager &_screen, WINDOW *w,
77                   Size size)
78                 :ListPage(w, size),
79                  screen(_screen)
80 #ifndef NCMPC_MINI
81                 , hscroll(w, options.scroll_sep)
82 #endif
83         {
84         }
85
86         ~QueuePage() override {
87                 g_free(connection_name);
88         }
89
90 private:
91         const struct mpd_song *GetSelectedSong() const;
92         void SaveSelection();
93         void RestoreSelection();
94
95         void Repaint() const {
96                 Paint();
97                 wrefresh(lw.w);
98         }
99
100         void CenterPlayingItem(const struct mpd_status *status,
101                                bool center_cursor);
102
103         bool OnSongChange(const struct mpd_status *status);
104
105         static gboolean OnHideCursorTimer(gpointer data);
106         static void PaintRow(WINDOW *w, unsigned i,
107                              unsigned y, unsigned width,
108                              bool selected, const void *data);
109
110 public:
111         /* virtual methods from class Page */
112         void OnOpen(struct mpdclient &c) override;
113         void OnClose() override;
114         void Paint() const override;
115         void Update(struct mpdclient &c, unsigned events) override;
116         bool OnCommand(struct mpdclient &c, command_t cmd) override;
117
118 #ifdef HAVE_GETMOUSE
119         bool OnMouse(struct mpdclient &c, Point p, mmask_t bstate) override;
120 #endif
121
122         const char *GetTitle(char *s, size_t size) const override;
123 };
124
125 const struct mpd_song *
126 QueuePage::GetSelectedSong() const
127 {
128         return !lw.range_selection &&
129                 lw.selected < playlist->size()
130                 ? &(*playlist)[lw.selected]
131                 : nullptr;
132 }
133
134 void
135 QueuePage::SaveSelection()
136 {
137         selected_song_id = GetSelectedSong() != nullptr
138                 ? (int)mpd_song_get_id(GetSelectedSong())
139                 : -1;
140 }
141
142 void
143 QueuePage::RestoreSelection()
144 {
145         lw.SetLength(playlist->size());
146
147         if (selected_song_id < 0)
148                 /* there was no selection */
149                 return;
150
151         const struct mpd_song *song = GetSelectedSong();
152         if (song != nullptr &&
153             mpd_song_get_id(song) == (unsigned)selected_song_id)
154                 /* selection is still valid */
155                 return;
156
157         int pos = playlist->FindId(selected_song_id);
158         if (pos >= 0)
159                 lw.SetCursor(pos);
160
161         SaveSelection();
162 }
163
164 static const char *
165 screen_queue_lw_callback(unsigned idx, void *data)
166 {
167         auto &playlist = *(MpdQueue *)data;
168         static char songname[MAX_SONG_LENGTH];
169
170         assert(idx < playlist.size());
171
172         const auto &song = playlist[idx];
173         strfsong(songname, MAX_SONG_LENGTH, options.list_format, &song);
174
175         return songname;
176 }
177
178 void
179 QueuePage::CenterPlayingItem(const struct mpd_status *status,
180                              bool center_cursor)
181 {
182         if (status == nullptr ||
183             (mpd_status_get_state(status) != MPD_STATE_PLAY &&
184              mpd_status_get_state(status) != MPD_STATE_PAUSE))
185                 return;
186
187         /* try to center the song that are playing */
188         int idx = mpd_status_get_song_pos(status);
189         if (idx < 0)
190                 return;
191
192         lw.Center(idx);
193
194         if (center_cursor) {
195                 lw.SetCursor(idx);
196                 return;
197         }
198
199         /* make sure the cursor is in the window */
200         lw.FetchCursor();
201 }
202
203 gcc_pure
204 static int
205 get_current_song_id(const struct mpd_status *status)
206 {
207         return status != nullptr &&
208                 (mpd_status_get_state(status) == MPD_STATE_PLAY ||
209                  mpd_status_get_state(status) == MPD_STATE_PAUSE)
210                 ? (int)mpd_status_get_song_id(status)
211                 : -1;
212 }
213
214 bool
215 QueuePage::OnSongChange(const struct mpd_status *status)
216 {
217         if (get_current_song_id(status) == current_song_id)
218                 return false;
219
220         current_song_id = get_current_song_id(status);
221
222         /* center the cursor */
223         if (options.auto_center && !lw.range_selection)
224                 CenterPlayingItem(status, false);
225
226         return true;
227 }
228
229 #ifndef NCMPC_MINI
230 static void
231 add_dir(Completion &completion, const char *dir,
232         struct mpdclient *c)
233 {
234         completion.clear();
235         gcmp_list_from_path(c, dir, completion, GCMP_TYPE_RFILE);
236 }
237
238 class DatabaseCompletion final : public Completion {
239         struct mpdclient &c;
240         std::set<std::string> dir_list;
241
242 public:
243         explicit DatabaseCompletion(struct mpdclient &_c)
244                 :c(_c) {}
245
246 protected:
247         /* virtual methods from class Completion */
248         void Pre(const char *value) override;
249         void Post(const char *value, Range range) override;
250 };
251
252 void
253 DatabaseCompletion::Pre(const char *line)
254 {
255         if (empty()) {
256                 /* create initial list */
257                 gcmp_list_from_path(&c, "", *this, GCMP_TYPE_RFILE);
258         } else if (line && line[0] && line[strlen(line) - 1] == '/') {
259                 auto i = dir_list.emplace(line);
260                 if (i.second)
261                         /* add directory content to list */
262                         add_dir(*this, line, &c);
263         }
264 }
265
266 void
267 DatabaseCompletion::Post(const char *line, Range range)
268 {
269         if (range.begin() != range.end() &&
270             std::next(range.begin()) != range.end())
271                 screen_display_completion_list(range);
272
273         if (line && line[0] && line[strlen(line) - 1] == '/') {
274                 /* add directory content to list */
275                 auto i = dir_list.emplace(line);
276                 if (i.second)
277                         add_dir(*this, line, &c);
278         }
279 }
280
281 #endif
282
283 static int
284 handle_add_to_playlist(struct mpdclient *c)
285 {
286 #ifndef NCMPC_MINI
287         /* initialize completion support */
288         DatabaseCompletion _completion(*c);
289         Completion *completion = &_completion;
290 #else
291         Completion *completion = nullptr;
292 #endif
293
294         /* get path */
295         auto path = screen_readln(_("Add"),
296                                   nullptr,
297                                   nullptr,
298                                   completion);
299
300         /* add the path to the playlist */
301         if (!path.empty()) {
302                 mpdclient_cmd_add_path(c, LocaleToUtf8(path.c_str()).c_str());
303         }
304
305         return 0;
306 }
307
308 static Page *
309 screen_queue_init(ScreenManager &_screen, WINDOW *w, Size size)
310 {
311         return new QueuePage(_screen, w, size);
312 }
313
314 gboolean
315 QueuePage::OnHideCursorTimer(gpointer data)
316 {
317         auto &q = *(QueuePage *)data;
318
319         assert(options.hide_cursor > 0);
320         assert(q.timer_hide_cursor_id != 0);
321
322         q.timer_hide_cursor_id = 0;
323
324         /* hide the cursor when mpd is playing and the user is inactive */
325
326         if (q.playing) {
327                 q.lw.hide_cursor = true;
328                 q.Repaint();
329         } else
330                 q.timer_hide_cursor_id = g_timeout_add_seconds(options.hide_cursor,
331                                                                OnHideCursorTimer, &q);
332
333         return false;
334 }
335
336 void
337 QueuePage::OnOpen(struct mpdclient &c)
338 {
339         playlist = &c.playlist;
340
341         assert(timer_hide_cursor_id == 0);
342         if (options.hide_cursor > 0) {
343                 lw.hide_cursor = false;
344                 timer_hide_cursor_id = g_timeout_add_seconds(options.hide_cursor,
345                                                              OnHideCursorTimer, this);
346         }
347
348         RestoreSelection();
349         OnSongChange(c.status);
350 }
351
352 void
353 QueuePage::OnClose()
354 {
355         if (timer_hide_cursor_id != 0) {
356                 g_source_remove(timer_hide_cursor_id);
357                 timer_hide_cursor_id = 0;
358         }
359
360 #ifndef NCMPC_MINI
361         if (options.scroll)
362                 hscroll.Clear();
363 #endif
364 }
365
366 const char *
367 QueuePage::GetTitle(char *str, size_t size) const
368 {
369        if (connection_name == nullptr)
370                return _("Queue");
371
372        g_snprintf(str, size, _("Queue on %s"), connection_name);
373        return str;
374 }
375
376 void
377 QueuePage::PaintRow(WINDOW *w, unsigned i, unsigned y, unsigned width,
378                     bool selected, const void *data)
379 {
380         const auto &q = *(const QueuePage *)data;
381         assert(q.playlist != nullptr);
382         assert(i < q.playlist->size());
383         const auto &song = (*q.playlist)[i];
384
385         class hscroll *row_hscroll = nullptr;
386 #ifndef NCMPC_MINI
387         row_hscroll = selected && options.scroll && q.lw.selected == i
388                 ? &q.hscroll : nullptr;
389 #endif
390
391         paint_song_row(w, y, width, selected,
392                        (int)mpd_song_get_id(&song) == q.current_song_id,
393                        &song, row_hscroll, options.list_format);
394 }
395
396 void
397 QueuePage::Paint() const
398 {
399 #ifndef NCMPC_MINI
400         if (options.scroll)
401                 hscroll.Clear();
402 #endif
403
404         lw.Paint(PaintRow, this);
405 }
406
407 void
408 QueuePage::Update(struct mpdclient &c, unsigned events)
409 {
410         playing = c.playing;
411
412         if (c.connection_id != last_connection_id) {
413                 last_connection_id = c.connection_id;
414                 g_free(connection_name);
415                 connection_name = mpdclient_settings_name(&c);
416         }
417
418         if (events & MPD_IDLE_QUEUE)
419                 RestoreSelection();
420         else
421                 /* the queue size may have changed, even if we havn't
422                    received the QUEUE idle event yet */
423                 lw.SetLength(playlist->size());
424
425         if (((events & MPD_IDLE_PLAYER) != 0 && OnSongChange(c.status)) ||
426             events & MPD_IDLE_QUEUE)
427                 /* the queue or the current song has changed, we must
428                    paint the new version */
429                 SetDirty();
430 }
431
432 #ifdef HAVE_GETMOUSE
433 bool
434 QueuePage::OnMouse(struct mpdclient &c, Point p, mmask_t bstate)
435 {
436         if (ListPage::OnMouse(c, p, bstate))
437                 return true;
438
439         if (bstate & BUTTON1_DOUBLE_CLICKED) {
440                 /* stop */
441                 screen.OnCommand(c, CMD_STOP);
442                 return true;
443         }
444
445         const unsigned old_selected = lw.selected;
446         lw.SetCursor(lw.start + p.y);
447
448         if (bstate & BUTTON1_CLICKED) {
449                 /* play */
450                 const struct mpd_song *song = GetSelectedSong();
451                 if (song != nullptr) {
452                         struct mpd_connection *connection =
453                                 mpdclient_get_connection(&c);
454
455                         if (connection != nullptr &&
456                             !mpd_run_play_id(connection,
457                                              mpd_song_get_id(song)))
458                                 mpdclient_handle_error(&c);
459                 }
460         } else if (bstate & BUTTON3_CLICKED) {
461                 /* delete */
462                 if (lw.selected == old_selected)
463                         mpdclient_cmd_delete(&c, lw.selected);
464
465                 lw.SetLength(playlist->size());
466         }
467
468         SaveSelection();
469         SetDirty();
470
471         return true;
472 }
473 #endif
474
475 bool
476 QueuePage::OnCommand(struct mpdclient &c, command_t cmd)
477 {
478         struct mpd_connection *connection;
479         static command_t cached_cmd = CMD_NONE;
480
481         const command_t prev_cmd = cached_cmd;
482         cached_cmd = cmd;
483
484         lw.hide_cursor = false;
485
486         if (options.hide_cursor > 0) {
487                 if (timer_hide_cursor_id != 0)
488                         g_source_remove(timer_hide_cursor_id);
489                 timer_hide_cursor_id = g_timeout_add_seconds(options.hide_cursor,
490                                                              OnHideCursorTimer, this);
491         }
492
493         if (ListPage::OnCommand(c, cmd)) {
494                 SaveSelection();
495                 return true;
496         }
497
498         switch(cmd) {
499         case CMD_SCREEN_UPDATE:
500                 CenterPlayingItem(c.status, prev_cmd == CMD_SCREEN_UPDATE);
501                 SetDirty();
502                 return false;
503         case CMD_SELECT_PLAYING:
504                 lw.SetCursor(c.playlist.Find(*c.song));
505                 SaveSelection();
506                 SetDirty();
507                 return true;
508
509         case CMD_LIST_FIND:
510         case CMD_LIST_RFIND:
511         case CMD_LIST_FIND_NEXT:
512         case CMD_LIST_RFIND_NEXT:
513                 screen_find(screen, &lw, cmd,
514                             screen_queue_lw_callback, &c.playlist);
515                 SaveSelection();
516                 SetDirty();
517                 return true;
518         case CMD_LIST_JUMP:
519                 screen_jump(screen, &lw, screen_queue_lw_callback, &c.playlist,
520                             nullptr, nullptr);
521                 SaveSelection();
522                 SetDirty();
523                 return true;
524
525 #ifdef ENABLE_SONG_SCREEN
526         case CMD_SCREEN_SONG:
527                 if (GetSelectedSong() != nullptr) {
528                         screen_song_switch(screen, c, *GetSelectedSong());
529                         return true;
530                 }
531
532                 break;
533 #endif
534
535 #ifdef ENABLE_LYRICS_SCREEN
536         case CMD_SCREEN_LYRICS:
537                 if (lw.selected < c.playlist.size()) {
538                         struct mpd_song &selected = c.playlist[lw.selected];
539                         bool follow = false;
540
541                         if (c.song &&
542                             !strcmp(mpd_song_get_uri(&selected),
543                                     mpd_song_get_uri(c.song)))
544                                 follow = true;
545
546                         screen_lyrics_switch(screen, c, selected, follow);
547                         return true;
548                 }
549
550                 break;
551 #endif
552         case CMD_SCREEN_SWAP:
553                 if (!c.playlist.empty())
554                         screen.Swap(c, &c.playlist[lw.selected]);
555                 else
556                         screen.Swap(c, nullptr);
557                 return true;
558
559         default:
560                 break;
561         }
562
563         if (!c.IsConnected())
564                 return false;
565
566         switch(cmd) {
567                 const struct mpd_song *song;
568                 ListWindowRange range;
569
570         case CMD_PLAY:
571                 song = GetSelectedSong();
572                 if (song == nullptr)
573                         return false;
574
575                 connection = mpdclient_get_connection(&c);
576                 if (connection != nullptr &&
577                     !mpd_run_play_id(connection, mpd_song_get_id(song)))
578                         mpdclient_handle_error(&c);
579
580                 return true;
581
582         case CMD_DELETE:
583                 range = lw.GetRange();
584                 mpdclient_cmd_delete_range(&c, range.start_index,
585                                            range.end_index);
586
587                 lw.SetCursor(range.start_index);
588                 return true;
589
590         case CMD_SAVE_PLAYLIST:
591                 playlist_save(&c, nullptr, nullptr);
592                 return true;
593
594         case CMD_ADD:
595                 handle_add_to_playlist(&c);
596                 return true;
597
598         case CMD_SHUFFLE:
599                 range = lw.GetRange();
600                 if (range.end_index <= range.start_index + 1)
601                         /* No range selection, shuffle all list. */
602                         break;
603
604                 connection = mpdclient_get_connection(&c);
605                 if (connection == nullptr)
606                         return true;
607
608                 if (mpd_run_shuffle_range(connection, range.start_index,
609                                           range.end_index))
610                         screen_status_message(_("Shuffled queue"));
611                 else
612                         mpdclient_handle_error(&c);
613                 return true;
614
615         case CMD_LIST_MOVE_UP:
616                 range = lw.GetRange();
617                 if (range.start_index == 0 || range.empty())
618                         return false;
619
620                 if (!mpdclient_cmd_move(&c, range.end_index - 1,
621                                         range.start_index - 1))
622                         return true;
623
624                 lw.selected--;
625                 lw.range_base--;
626
627                 if (lw.range_selection)
628                         lw.ScrollTo(lw.range_base);
629                 lw.ScrollTo(lw.selected);
630
631                 SaveSelection();
632                 return true;
633
634         case CMD_LIST_MOVE_DOWN:
635                 range = lw.GetRange();
636                 if (range.end_index >= c.playlist.size())
637                         return false;
638
639                 if (!mpdclient_cmd_move(&c, range.start_index,
640                                         range.end_index))
641                         return true;
642
643                 lw.selected++;
644                 lw.range_base++;
645
646                 if (lw.range_selection)
647                         lw.ScrollTo(lw.range_base);
648                 lw.ScrollTo(lw.selected);
649
650                 SaveSelection();
651                 return true;
652
653         case CMD_LOCATE:
654                 if (GetSelectedSong() != nullptr) {
655                         screen_file_goto_song(screen, c, *GetSelectedSong());
656                         return true;
657                 }
658
659                 break;
660
661         default:
662                 break;
663         }
664
665         return false;
666 }
667
668 const struct screen_functions screen_queue = {
669         "playlist",
670         screen_queue_init,
671 };