add allowsFiltering / allowsSearching checks
[ncmpcpp.git] / src / playlist.cpp
blobe58074a0dee0a76458bafce0d484e90ff9d7f4f0
1 /***************************************************************************
2 * Copyright (C) 2008-2012 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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 *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
21 #include <algorithm>
22 #include <sstream>
24 #include "display.h"
25 #include "global.h"
26 #include "helpers.h"
27 #include "menu.h"
28 #include "playlist.h"
29 #include "regex_filter.h"
30 #include "song.h"
31 #include "status.h"
32 #include "utility/comparators.h"
34 using namespace std::placeholders;
36 using Global::MainHeight;
37 using Global::MainStartY;
39 Playlist *myPlaylist = new Playlist;
41 bool Playlist::ReloadTotalLength = 0;
42 bool Playlist::ReloadRemaining = false;
44 namespace {//
46 NC::Menu< std::pair<std::string, MPD::Song::GetFunction> > *SortDialog = 0;
47 size_t SortDialogHeight;
49 const size_t SortOptions = 10;
50 const size_t SortDialogWidth = 30;
52 std::string songToString(const MPD::Song &s);
53 bool playlistEntryMatcher(const Regex &rx, const MPD::Song &s);
57 void Playlist::Init()
59 Items = new NC::Menu<MPD::Song>(0, MainStartY, COLS, MainHeight, Config.columns_in_playlist && Config.titles_visibility ? Display::Columns(COLS) : "", Config.main_color, NC::brNone);
60 Items->cyclicScrolling(Config.use_cyclic_scrolling);
61 Items->centeredCursor(Config.centered_cursor);
62 Items->setHighlightColor(Config.main_highlight_color);
63 Items->setSelectedPrefix(Config.selected_item_prefix);
64 Items->setSelectedSuffix(Config.selected_item_suffix);
65 if (Config.columns_in_playlist)
66 Items->setItemDisplayer(std::bind(Display::SongsInColumns, _1, *this));
67 else
68 Items->setItemDisplayer(std::bind(Display::Songs, _1, *this, Config.song_list_format));
70 if (!SortDialog)
72 SortDialogHeight = std::min(int(MainHeight), 17);
74 SortDialog = new NC::Menu< std::pair<std::string, MPD::Song::GetFunction> >((COLS-SortDialogWidth)/2, (MainHeight-SortDialogHeight)/2+MainStartY, SortDialogWidth, SortDialogHeight, "Sort songs by...", Config.main_color, Config.window_border);
75 SortDialog->cyclicScrolling(Config.use_cyclic_scrolling);
76 SortDialog->centeredCursor(Config.centered_cursor);
77 SortDialog->setItemDisplayer(Display::Pair<std::string, MPD::Song::GetFunction>);
79 SortDialog->addItem(std::make_pair("Artist", &MPD::Song::getArtist));
80 SortDialog->addItem(std::make_pair("Album", &MPD::Song::getAlbum));
81 SortDialog->addItem(std::make_pair("Disc", &MPD::Song::getDisc));
82 SortDialog->addItem(std::make_pair("Track", &MPD::Song::getTrack));
83 SortDialog->addItem(std::make_pair("Genre", &MPD::Song::getGenre));
84 SortDialog->addItem(std::make_pair("Date", &MPD::Song::getDate));
85 SortDialog->addItem(std::make_pair("Composer", &MPD::Song::getComposer));
86 SortDialog->addItem(std::make_pair("Performer", &MPD::Song::getPerformer));
87 SortDialog->addItem(std::make_pair("Title", &MPD::Song::getTitle));
88 SortDialog->addItem(std::make_pair("Filename", &MPD::Song::getURI));
89 SortDialog->addSeparator();
90 SortDialog->addItem(std::make_pair("Sort", static_cast<MPD::Song::GetFunction>(0)));
91 SortDialog->addItem(std::make_pair("Cancel", static_cast<MPD::Song::GetFunction>(0)));
94 w = Items;
95 isInitialized = 1;
98 void Playlist::SwitchTo()
100 using Global::myScreen;
101 using Global::myLockedScreen;
102 using Global::myInactiveScreen;
104 if (myScreen == this)
105 return;
107 if (!isInitialized)
108 Init();
110 itsScrollBegin = 0;
112 if (myLockedScreen)
113 UpdateInactiveScreen(this);
115 if (hasToBeResized || myLockedScreen)
116 Resize();
118 if (myScreen != this && myScreen->isTabbable())
119 Global::myPrevScreen = myScreen;
120 myScreen = this;
121 EnableHighlighting();
122 if (w != Items) // even if sorting window is active, background has to be refreshed anyway
123 Items->display();
124 Global::RedrawHeader = true;
127 void Playlist::Resize()
129 size_t x_offset, width;
130 GetWindowResizeParams(x_offset, width);
131 Items->resize(width, MainHeight);
132 Items->moveTo(x_offset, MainStartY);
134 Items->setTitle(Config.columns_in_playlist && Config.titles_visibility ? Display::Columns(Items->getWidth()) : "");
135 if (w == SortDialog) // if sorting window is active, playlist needs refreshing
136 Items->display();
138 SortDialogHeight = std::min(int(MainHeight), 17);
139 if (Items->getWidth() >= SortDialogWidth && MainHeight >= 5)
141 SortDialog->resize(SortDialogWidth, SortDialogHeight);
142 SortDialog->moveTo(x_offset+(width-SortDialogWidth)/2, (MainHeight-SortDialogHeight)/2+MainStartY);
144 else // if screen is too low to display sorting window, fall back to items list
145 w = Items;
147 hasToBeResized = 0;
150 std::basic_string<my_char_t> Playlist::Title()
152 std::basic_string<my_char_t> result = U("Playlist ");
153 if (ReloadTotalLength || ReloadRemaining)
154 itsBufferedStats = TotalLength();
155 result += Scroller(TO_WSTRING(itsBufferedStats), itsScrollBegin, COLS-result.length()-(Config.new_design ? 2 : Global::VolumeState.length()));
156 return result;
159 void Playlist::EnterPressed()
161 if (w == Items)
163 if (!Items->empty())
164 Mpd.PlayID(Items->current().value().getID());
166 else if (w == SortDialog)
168 size_t pos = SortDialog->choice();
170 size_t beginning = 0;
171 size_t end = Items->size();
172 if (Items->hasSelected())
174 std::vector<size_t> list;
175 Items->getSelected(list);
176 beginning = *list.begin();
177 end = *list.rbegin()+1;
180 if (pos > SortOptions)
182 if (pos == SortOptions+2) // cancel
184 w = Items;
185 return;
188 else
190 ShowMessage("Move tag types up and down to adjust sort order");
191 return;
194 MPD::SongList playlist;
195 playlist.reserve(end-beginning);
196 for (size_t i = beginning; i < end; ++i)
197 playlist.push_back((*Items)[i].value());
199 std::function<void(MPD::SongList::iterator, MPD::SongList::iterator)> iter_swap, quick_sort;
200 auto song_cmp = [](const MPD::Song &a, const MPD::Song &b) -> bool {
201 CaseInsensitiveStringComparison cmp;
202 for (size_t i = 0; i < SortOptions; ++i)
203 if (int ret = cmp(a.getTags((*SortDialog)[i].value().second), b.getTags((*SortDialog)[i].value().second)))
204 return ret < 0;
205 return a.getPosition() < b.getPosition();
207 iter_swap = [&playlist](MPD::SongList::iterator a, MPD::SongList::iterator b) {
208 std::iter_swap(a, b);
209 Mpd.Swap(a-playlist.begin(), b-playlist.begin());
211 quick_sort = [this, &song_cmp, &quick_sort, &iter_swap](MPD::SongList::iterator first, MPD::SongList::iterator last) {
212 if (last-first > 1)
214 MPD::SongList::iterator pivot = first+rand()%(last-first);
215 iter_swap(pivot, last-1);
216 pivot = last-1;
218 MPD::SongList::iterator tmp = first;
219 for (MPD::SongList::iterator i = first; i != pivot; ++i)
220 if (song_cmp(*i, *pivot))
221 iter_swap(i, tmp++);
222 iter_swap(tmp, pivot);
224 quick_sort(first, tmp);
225 quick_sort(tmp+1, last);
229 ShowMessage("Sorting...");
230 Mpd.StartCommandsList();
231 quick_sort(playlist.begin(), playlist.end());
232 if (Mpd.CommitCommandsList())
233 ShowMessage("Playlist sorted");
234 w = Items;
238 void Playlist::SpacePressed()
240 if (w == Items && !Items->empty())
242 Items->current().setSelected(!Items->current().isSelected());
243 Items->scroll(NC::wDown);
247 void Playlist::MouseButtonPressed(MEVENT me)
249 if (w == Items && !Items->empty() && Items->hasCoords(me.x, me.y))
251 if (size_t(me.y) < Items->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
253 Items->Goto(me.y);
254 if (me.bstate & BUTTON3_PRESSED)
255 EnterPressed();
257 else
258 Screen<NC::Window>::MouseButtonPressed(me);
260 else if (w == SortDialog && SortDialog->hasCoords(me.x, me.y))
262 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
264 SortDialog->Goto(me.y);
265 if (me.bstate & BUTTON3_PRESSED)
266 EnterPressed();
268 else
269 Screen<NC::Window>::MouseButtonPressed(me);
273 /***********************************************************************/
275 bool Playlist::allowsFiltering()
277 return true;
280 std::string Playlist::currentFilter()
282 std::string filter;
283 if (w == Items)
284 filter = RegexFilter<MPD::Song>::currentFilter(*Items);
285 return filter;
288 void Playlist::applyFilter(const std::string &filter)
290 if (w == Items)
292 Items->showAll();
293 auto rx = RegexFilter<MPD::Song>(filter, Config.regex_type, playlistEntryMatcher);
294 Items->filter(Items->begin(), Items->end(), rx);
298 /***********************************************************************/
300 bool Playlist::allowsSearching()
302 return true;
305 bool Playlist::search(const std::string &constraint)
307 bool result = false;
308 if (w == Items)
310 auto rx = RegexFilter<MPD::Song>(constraint, Config.regex_type, playlistEntryMatcher);
311 result = Items->search(Items->begin(), Items->end(), rx);
313 return result;
316 void Playlist::nextFound(bool wrap)
318 if (w == Items)
319 Items->nextFound(wrap);
322 void Playlist::prevFound(bool wrap)
324 if (w == Items)
325 Items->prevFound(wrap);
328 /***********************************************************************/
330 std::shared_ptr<ProxySongList> Playlist::getProxySongList()
332 auto ptr = nullProxySongList();
333 if (w == Items)
334 ptr = mkProxySongList(*Items, [](NC::Menu<MPD::Song>::Item &item) {
335 return &item.value();
337 return ptr;
340 MPD::Song *Playlist::getSong(size_t pos)
342 MPD::Song *ptr = 0;
343 if (w == Items)
344 ptr = &(*Items)[pos].value();
345 return ptr;
348 MPD::Song *Playlist::currentSong()
350 if (Items->empty())
351 return 0;
352 else
353 return getSong(Items->choice());
356 bool Playlist::allowsSelection()
358 return w == Items;
361 void Playlist::reverseSelection()
363 reverseSelectionHelper(Items->begin(), Items->end());
366 MPD::SongList Playlist::getSelectedSongs()
368 MPD::SongList result;
369 if (w == Items)
371 for (auto it = Items->begin(); it != Items->end(); ++it)
372 if (it->isSelected())
373 result.push_back(it->value());
374 if (result.empty() && !Items->empty())
375 result.push_back(Items->current().value());
377 return result;
380 /***********************************************************************/
383 bool Playlist::isFiltered()
385 if (Items->isFiltered())
387 ShowMessage("Function currently unavailable due to filtered playlist");
388 return true;
390 return false;
393 void Playlist::MoveSelectedItems(Movement where)
395 if (Items->empty() || isFiltered())
396 return;
398 switch (where)
400 case mUp:
402 if (Items->hasSelected())
404 std::vector<size_t> list;
405 myPlaylist->Items->getSelected(list);
406 if (list.front() > 0)
408 Mpd.StartCommandsList();
409 std::vector<size_t>::const_iterator it = list.begin();
410 for (; it != list.end(); ++it)
411 Mpd.Move(*it-1, *it);
412 if (Mpd.CommitCommandsList())
414 Items->at(list.back()).setSelected(false);
415 Items->at(list.front()-1).setSelected(true);
416 Items->highlight(list[(list.size()-1)/2]-1);
420 else
422 size_t pos = myPlaylist->Items->choice();
423 if (pos > 0)
425 if (Mpd.Move(pos-1, pos))
426 Items->scroll(NC::wUp);
429 break;
431 case mDown:
433 if (Items->hasSelected())
435 std::vector<size_t> list;
436 Items->getSelected(list);
438 if (list.back() < Items->size()-1)
440 Mpd.StartCommandsList();
441 std::vector<size_t>::const_reverse_iterator it = list.rbegin();
442 for (; it != list.rend(); ++it)
443 Mpd.Move(*it, *it+1);
444 if (Mpd.CommitCommandsList())
446 Items->at(list.front()).setSelected(false);
447 Items->at(list.back()+1).setSelected(true);
448 Items->highlight(list[(list.size()-1)/2]+1);
452 else
454 size_t pos = Items->choice();
455 if (pos < Items->size()-1)
457 if (Mpd.Move(pos, pos+1))
458 Items->scroll(NC::wDown);
461 break;
466 void Playlist::Sort()
468 if (isFiltered())
469 return;
470 if (Items->getWidth() < SortDialogWidth || MainHeight < 5)
471 ShowMessage("Screen is too small to display dialog window");
472 else
474 SortDialog->reset();
475 w = SortDialog;
479 void Playlist::Reverse()
481 if (isFiltered())
482 return;
483 ShowMessage("Reversing playlist order...");
484 size_t beginning = -1, end = -1;
485 for (size_t i = 0; i < Items->size(); ++i)
487 if (Items->at(i).isSelected())
489 if (beginning == size_t(-1))
490 beginning = i;
491 end = i;
494 if (beginning == size_t(-1)) // no selected items
496 beginning = 0;
497 end = Items->size();
499 Mpd.StartCommandsList();
500 for (size_t i = beginning, j = end-1; i < (beginning+end)/2; ++i, --j)
501 Mpd.Swap(i, j);
502 if (Mpd.CommitCommandsList())
503 ShowMessage("Playlist reversed");
506 void Playlist::AdjustSortOrder(Movement where)
508 switch (where)
510 case mUp:
512 size_t pos = SortDialog->choice();
513 if (pos > 0 && pos < SortOptions)
515 SortDialog->Swap(pos, pos-1);
516 SortDialog->scroll(NC::wUp);
518 break;
520 case mDown:
522 size_t pos = SortDialog->choice();
523 if (pos < SortOptions-1)
525 SortDialog->Swap(pos, pos+1);
526 SortDialog->scroll(NC::wDown);
528 break;
533 void Playlist::EnableHighlighting()
535 Items->setHighlighting(1);
536 UpdateTimer();
539 bool Playlist::SortingInProgress()
541 return w == SortDialog;
544 std::string Playlist::TotalLength()
546 std::ostringstream result;
548 if (ReloadTotalLength)
550 itsTotalLength = 0;
551 for (size_t i = 0; i < Items->size(); ++i)
552 itsTotalLength += (*Items)[i].value().getDuration();
553 ReloadTotalLength = 0;
555 if (Config.playlist_show_remaining_time && ReloadRemaining && !Items->isFiltered())
557 itsRemainingTime = 0;
558 for (size_t i = NowPlaying; i < Items->size(); ++i)
559 itsRemainingTime += (*Items)[i].value().getDuration();
560 ReloadRemaining = false;
563 result << '(' << Items->size() << (Items->size() == 1 ? " item" : " items");
565 if (Items->isFiltered())
567 Items->showAll();
568 size_t real_size = Items->size();
569 Items->showFiltered();
570 if (Items->size() != real_size)
571 result << " (out of " << Mpd.GetPlaylistLength() << ")";
574 if (itsTotalLength)
576 result << ", length: ";
577 ShowTime(result, itsTotalLength, Config.playlist_shorten_total_times);
579 if (Config.playlist_show_remaining_time && itsRemainingTime && !Items->isFiltered() && Items->size() > 1)
581 result << " :: remaining: ";
582 ShowTime(result, itsRemainingTime, Config.playlist_shorten_total_times);
584 result << ')';
585 return result.str();
588 const MPD::Song *Playlist::NowPlayingSong()
590 bool was_filtered = Items->isFiltered();
591 Items->showAll();
592 const MPD::Song *s = isPlaying() ? &Items->at(NowPlaying).value() : 0;
593 if (was_filtered)
594 Items->showFiltered();
595 return s;
598 bool Playlist::Add(const MPD::Song &s, bool in_playlist, bool play, int position)
600 if (Config.ncmpc_like_songs_adding && in_playlist)
602 unsigned hash = s.getHash();
603 if (play)
605 for (size_t i = 0; i < Items->size(); ++i)
607 if (Items->at(i).value().getHash() == hash)
609 Mpd.Play(i);
610 break;
613 return true;
615 else
617 Mpd.StartCommandsList();
618 for (size_t i = 0; i < Items->size(); ++i)
620 if ((*Items)[i].value().getHash() == hash)
622 Mpd.Delete(i);
623 Items->deleteItem(i);
624 i--;
627 Mpd.CommitCommandsList();
628 return false;
631 else
633 int id = Mpd.AddSong(s, position);
634 if (id >= 0)
636 ShowMessage("Added to playlist: %s", s.toString(Config.song_status_format_no_colors).c_str());
637 if (play)
638 Mpd.PlayID(id);
639 return true;
641 else
642 return false;
646 bool Playlist::Add(const MPD::SongList &l, bool play, int position)
648 if (l.empty())
649 return false;
651 Mpd.StartCommandsList();
652 if (position < 0)
654 for (auto it = l.begin(); it != l.end(); ++it)
655 if (Mpd.AddSong(*it) < 0)
656 break;
658 else
660 for (auto j = l.rbegin(); j != l.rend(); ++j)
661 if (Mpd.AddSong(*j, position) < 0)
662 break;
664 if (!Mpd.CommitCommandsList())
665 return false;
666 if (play)
667 PlayNewlyAddedSongs();
668 return true;
671 void Playlist::PlayNewlyAddedSongs()
673 bool is_filtered = Items->isFiltered();
674 Items->showAll();
675 size_t old_size = Items->size();
676 Mpd.UpdateStatus();
677 if (old_size < Items->size())
678 Mpd.Play(old_size);
679 if (is_filtered)
680 Items->showFiltered();
683 void Playlist::SetSelectedItemsPriority(int prio)
685 std::vector<size_t> list;
686 myPlaylist->Items->getSelected(list);
687 if (list.empty())
688 list.push_back(Items->choice());
689 Mpd.StartCommandsList();
690 for (std::vector<size_t>::const_iterator it = list.begin(); it != list.end(); ++it)
691 Mpd.SetPriority((*Items)[*it].value(), prio);
692 if (Mpd.CommitCommandsList())
693 ShowMessage("Priority set");
696 bool Playlist::checkForSong (const MPD::Song &s)
698 for (size_t i = 0; i < Items->size(); ++i)
699 if (s.getHash() == (*Items)[i].value().getHash())
700 return true;
701 return false;
704 namespace {//
706 std::string songToString(const MPD::Song &s)
708 std::string result;
709 if (Config.columns_in_playlist)
710 result = s.toString(Config.song_in_columns_to_string_format);
711 else
712 result = s.toString(Config.song_list_format_dollar_free);
713 return result;
716 bool playlistEntryMatcher(const Regex &rx, const MPD::Song &s)
718 return rx.match(songToString(s));