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