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