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