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