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