1 /* ncmpc (Ncurses MPD Client)
2 * (c) 2004-2018 The Music Player Daemon Project
3 * Project homepage: http://musicpd.org
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.
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.
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.
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"
32 #include "charset.hxx"
33 #include "options.hxx"
34 #include "mpdclient.hxx"
35 #include "strfsong.hxx"
36 #include "wreadln.hxx"
37 #include "Completion.hxx"
39 #include "song_paint.hxx"
41 #include "screen_utils.hxx"
42 #include "SongPage.hxx"
43 #include "LyricsPage.hxx"
44 #include "db_completion.hxx"
45 #include "util/Compiler.h"
48 #include "hscroll.hxx"
51 #include <mpd/client.h>
58 #define MAX_SONG_LENGTH 512
60 class QueuePage final : public ListPage, ListRenderer, ListText {
61 ScreenManager &screen;
64 mutable class hscroll hscroll;
67 MpdQueue *playlist = nullptr;
68 int current_song_id = -1;
69 int selected_song_id = -1;
70 guint timer_hide_cursor_id = 0;
72 unsigned last_connection_id = 0;
73 std::string connection_name;
78 QueuePage(ScreenManager &_screen, WINDOW *w,
83 , hscroll(w, options.scroll_sep.c_str(), Style::LIST_BOLD)
90 const struct mpd_song *GetSelectedSong() const;
93 void RestoreSelection();
95 void Repaint() const {
100 void CenterPlayingItem(const struct mpd_status *status,
103 bool OnSongChange(const struct mpd_status *status);
105 bool OnHideCursorTimer();
107 void ScheduleHideCursor() {
108 assert(options.hide_cursor > 0);
109 assert(timer_hide_cursor_id == 0);
111 timer_hide_cursor_id = ScheduleTimeout<QueuePage,
112 &QueuePage::OnHideCursorTimer>(std::chrono::seconds(options.hide_cursor),
116 /* virtual methods from class ListRenderer */
117 void PaintListItem(WINDOW *w, unsigned i,
118 unsigned y, unsigned width,
119 bool selected) const override;
121 /* virtual methods from class ListText */
122 const char *GetListItemText(char *buffer, size_t size,
123 unsigned i) const override;
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;
134 bool OnMouse(struct mpdclient &c, Point p, mmask_t bstate) override;
137 const char *GetTitle(char *s, size_t size) const override;
140 const struct mpd_song *
141 QueuePage::GetSelectedSong() const
143 return !lw.range_selection &&
144 lw.selected < playlist->size()
145 ? &(*playlist)[lw.selected]
150 QueuePage::SaveSelection()
152 selected_song_id = GetSelectedSong() != nullptr
153 ? (int)mpd_song_get_id(GetSelectedSong())
158 QueuePage::RestoreSelection()
160 lw.SetLength(playlist->size());
162 if (selected_song_id < 0)
163 /* there was no selection */
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 */
172 int pos = playlist->FindById(selected_song_id);
180 QueuePage::GetListItemText(char *buffer, size_t size,
183 assert(idx < playlist->size());
185 const auto &song = (*playlist)[idx];
186 strfsong(buffer, size, options.list_format.c_str(), &song);
192 QueuePage::CenterPlayingItem(const struct mpd_status *status,
195 if (status == nullptr ||
196 (mpd_status_get_state(status) != MPD_STATE_PLAY &&
197 mpd_status_get_state(status) != MPD_STATE_PAUSE))
200 /* try to center the song that are playing */
201 int idx = mpd_status_get_song_pos(status);
212 /* make sure the cursor is in the window */
218 get_current_song_id(const struct mpd_status *status)
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)
228 QueuePage::OnSongChange(const struct mpd_status *status)
230 if (get_current_song_id(status) == current_song_id)
233 current_song_id = get_current_song_id(status);
235 /* center the cursor */
236 if (options.auto_center && !lw.range_selection)
237 CenterPlayingItem(status, false);
244 add_dir(Completion &completion, const char *dir,
248 gcmp_list_from_path(c, dir, completion, GCMP_TYPE_RFILE);
251 class DatabaseCompletion final : public Completion {
253 std::set<std::string> dir_list;
256 explicit DatabaseCompletion(struct mpdclient &_c)
260 /* virtual methods from class Completion */
261 void Pre(const char *value) override;
262 void Post(const char *value, Range range) override;
266 DatabaseCompletion::Pre(const char *line)
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);
274 /* add directory content to list */
275 add_dir(*this, line, &c);
280 DatabaseCompletion::Post(const char *line, Range range)
282 if (range.begin() != range.end() &&
283 std::next(range.begin()) != range.end())
284 screen_display_completion_list(range);
286 if (line && line[0] && line[strlen(line) - 1] == '/') {
287 /* add directory content to list */
288 auto i = dir_list.emplace(line);
290 add_dir(*this, line, &c);
297 handle_add_to_playlist(struct mpdclient *c)
300 /* initialize completion support */
301 DatabaseCompletion _completion(*c);
302 Completion *completion = &_completion;
304 Completion *completion = nullptr;
308 auto path = screen_readln(_("Add"),
313 /* add the path to the playlist */
315 mpdclient_cmd_add_path(c, LocaleToUtf8(path.c_str()).c_str());
322 screen_queue_init(ScreenManager &_screen, WINDOW *w, Size size)
324 return new QueuePage(_screen, w, size);
328 QueuePage::OnHideCursorTimer()
330 assert(options.hide_cursor > 0);
331 assert(timer_hide_cursor_id != 0);
333 timer_hide_cursor_id = 0;
335 /* hide the cursor when mpd is playing and the user is inactive */
338 lw.hide_cursor = true;
341 ScheduleHideCursor();
347 QueuePage::OnOpen(struct mpdclient &c)
349 playlist = &c.playlist;
351 assert(timer_hide_cursor_id == 0);
352 if (options.hide_cursor > 0) {
353 lw.hide_cursor = false;
354 ScheduleHideCursor();
358 OnSongChange(c.status);
364 if (timer_hide_cursor_id != 0) {
365 g_source_remove(timer_hide_cursor_id);
366 timer_hide_cursor_id = 0;
376 QueuePage::GetTitle(char *str, size_t size) const
378 if (connection_name.empty())
381 snprintf(str, size, _("Queue on %s"), connection_name.c_str());
386 QueuePage::PaintListItem(WINDOW *w, unsigned i, unsigned y, unsigned width,
389 assert(playlist != nullptr);
390 assert(i < playlist->size());
391 const auto &song = (*playlist)[i];
393 class hscroll *row_hscroll = nullptr;
395 row_hscroll = selected && options.scroll && lw.selected == i
396 ? &hscroll : nullptr;
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());
405 QueuePage::Paint() const
416 QueuePage::Update(struct mpdclient &c, unsigned events)
420 if (c.connection_id != last_connection_id) {
421 last_connection_id = c.connection_id;
422 connection_name = c.GetSettingsName();
425 if (events & MPD_IDLE_QUEUE)
428 /* the queue size may have changed, even if we havn't
429 received the QUEUE idle event yet */
430 lw.SetLength(playlist->size());
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 */
441 QueuePage::OnMouse(struct mpdclient &c, Point p, mmask_t bstate)
443 if (ListPage::OnMouse(c, p, bstate))
446 if (bstate & BUTTON1_DOUBLE_CLICKED) {
448 screen.OnCommand(c, Command::STOP);
452 const unsigned old_selected = lw.selected;
453 lw.SetCursor(lw.start + p.y);
455 if (bstate & BUTTON1_CLICKED) {
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)))
465 } else if (bstate & BUTTON3_CLICKED) {
467 if (lw.selected == old_selected)
468 mpdclient_cmd_delete(&c, lw.selected);
470 lw.SetLength(playlist->size());
481 QueuePage::OnCommand(struct mpdclient &c, Command cmd)
483 struct mpd_connection *connection;
484 static Command cached_cmd = Command::NONE;
486 const Command prev_cmd = cached_cmd;
489 lw.hide_cursor = false;
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;
497 ScheduleHideCursor();
500 if (ListPage::OnCommand(c, cmd)) {
506 case Command::SCREEN_UPDATE:
507 CenterPlayingItem(c.status, prev_cmd == Command::SCREEN_UPDATE);
510 case Command::SELECT_PLAYING:
511 lw.SetCursor(c.playlist.FindByReference(*c.song));
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);
524 case Command::LIST_JUMP:
525 screen_jump(screen, &lw, *this, *this);
530 #ifdef ENABLE_SONG_SCREEN
531 case Command::SCREEN_SONG:
532 if (GetSelectedSong() != nullptr) {
533 screen_song_switch(screen, c, *GetSelectedSong());
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];
547 !strcmp(mpd_song_get_uri(&selected),
548 mpd_song_get_uri(c.song)))
551 screen_lyrics_switch(screen, c, selected, follow);
557 case Command::SCREEN_SWAP:
558 if (!c.playlist.empty())
559 screen.Swap(c, &c.playlist[lw.selected]);
561 screen.Swap(c, nullptr);
568 if (!c.IsConnected())
572 const struct mpd_song *song;
573 ListWindowRange range;
576 song = GetSelectedSong();
580 connection = c.GetConnection();
581 if (connection != nullptr &&
582 !mpd_run_play_id(connection, mpd_song_get_id(song)))
587 case Command::DELETE:
588 range = lw.GetRange();
589 mpdclient_cmd_delete_range(&c, range.start_index,
592 lw.SetCursor(range.start_index);
595 case Command::SAVE_PLAYLIST:
596 playlist_save(&c, nullptr, nullptr);
600 handle_add_to_playlist(&c);
603 case Command::SHUFFLE:
604 range = lw.GetRange();
605 if (range.end_index <= range.start_index + 1)
606 /* No range selection, shuffle all list. */
609 connection = c.GetConnection();
610 if (connection == nullptr)
613 if (mpd_run_shuffle_range(connection, range.start_index,
615 screen_status_message(_("Shuffled queue"));
620 case Command::LIST_MOVE_UP:
621 range = lw.GetRange();
622 if (range.start_index == 0 || range.empty())
625 if (!mpdclient_cmd_move(&c, range.end_index - 1,
626 range.start_index - 1))
632 if (lw.range_selection)
633 lw.ScrollTo(lw.range_base);
634 lw.ScrollTo(lw.selected);
639 case Command::LIST_MOVE_DOWN:
640 range = lw.GetRange();
641 if (range.end_index >= c.playlist.size())
644 if (!mpdclient_cmd_move(&c, range.start_index,
651 if (lw.range_selection)
652 lw.ScrollTo(lw.range_base);
653 lw.ScrollTo(lw.selected);
658 case Command::LOCATE:
659 if (GetSelectedSong() != nullptr) {
660 screen_file_goto_song(screen, c, *GetSelectedSong());
673 const PageMeta screen_queue = {
676 Command::SCREEN_PLAY,