/*************************************************************************** * Copyright (C) 2008-2012 by Andrzej Rybczak * * electricityispower@gmail.com * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include #include #include "display.h" #include "global.h" #include "helpers.h" #include "menu.h" #include "playlist.h" #include "regex_filter.h" #include "song.h" #include "status.h" #include "statusbar.h" #include "utility/comparators.h" #include "title.h" using namespace std::placeholders; using Global::MainHeight; using Global::MainStartY; Playlist *myPlaylist = new Playlist; bool Playlist::ReloadTotalLength = 0; bool Playlist::ReloadRemaining = false; namespace {// NC::Menu< std::pair > *SortDialog = 0; size_t SortDialogHeight; const size_t SortOptions = 10; const size_t SortDialogWidth = 30; std::string songToString(const MPD::Song &s); bool playlistEntryMatcher(const Regex &rx, const MPD::Song &s); } void Playlist::Init() { Items = new NC::Menu(0, MainStartY, COLS, MainHeight, Config.columns_in_playlist && Config.titles_visibility ? Display::Columns(COLS) : "", Config.main_color, NC::brNone); Items->cyclicScrolling(Config.use_cyclic_scrolling); Items->centeredCursor(Config.centered_cursor); Items->setHighlightColor(Config.main_highlight_color); Items->setSelectedPrefix(Config.selected_item_prefix); Items->setSelectedSuffix(Config.selected_item_suffix); if (Config.columns_in_playlist) Items->setItemDisplayer(std::bind(Display::SongsInColumns, _1, this)); else Items->setItemDisplayer(std::bind(Display::Songs, _1, this, Config.song_list_format)); if (!SortDialog) { SortDialogHeight = std::min(int(MainHeight), 17); SortDialog = new NC::Menu< std::pair >((COLS-SortDialogWidth)/2, (MainHeight-SortDialogHeight)/2+MainStartY, SortDialogWidth, SortDialogHeight, "Sort songs by...", Config.main_color, Config.window_border); SortDialog->cyclicScrolling(Config.use_cyclic_scrolling); SortDialog->centeredCursor(Config.centered_cursor); SortDialog->setItemDisplayer(Display::Pair); SortDialog->addItem(std::make_pair("Artist", &MPD::Song::getArtist)); SortDialog->addItem(std::make_pair("Album", &MPD::Song::getAlbum)); SortDialog->addItem(std::make_pair("Disc", &MPD::Song::getDisc)); SortDialog->addItem(std::make_pair("Track", &MPD::Song::getTrack)); SortDialog->addItem(std::make_pair("Genre", &MPD::Song::getGenre)); SortDialog->addItem(std::make_pair("Date", &MPD::Song::getDate)); SortDialog->addItem(std::make_pair("Composer", &MPD::Song::getComposer)); SortDialog->addItem(std::make_pair("Performer", &MPD::Song::getPerformer)); SortDialog->addItem(std::make_pair("Title", &MPD::Song::getTitle)); SortDialog->addItem(std::make_pair("Filename", &MPD::Song::getURI)); SortDialog->addSeparator(); SortDialog->addItem(std::make_pair("Sort", static_cast(0))); SortDialog->addItem(std::make_pair("Cancel", static_cast(0))); } w = Items; isInitialized = 1; } void Playlist::SwitchTo() { using Global::myScreen; using Global::myLockedScreen; using Global::myInactiveScreen; if (myScreen == this) return; if (!isInitialized) Init(); itsScrollBegin = 0; if (myLockedScreen) UpdateInactiveScreen(this); if (hasToBeResized || myLockedScreen) Resize(); if (myScreen != this && myScreen->isTabbable()) Global::myPrevScreen = myScreen; myScreen = this; EnableHighlighting(); if (w != Items) // even if sorting window is active, background has to be refreshed anyway Items->display(); drawHeader(); } void Playlist::Resize() { size_t x_offset, width; GetWindowResizeParams(x_offset, width); Items->resize(width, MainHeight); Items->moveTo(x_offset, MainStartY); Items->setTitle(Config.columns_in_playlist && Config.titles_visibility ? Display::Columns(Items->getWidth()) : ""); if (w == SortDialog) // if sorting window is active, playlist needs refreshing Items->display(); SortDialogHeight = std::min(int(MainHeight), 17); if (Items->getWidth() >= SortDialogWidth && MainHeight >= 5) { SortDialog->resize(SortDialogWidth, SortDialogHeight); SortDialog->moveTo(x_offset+(width-SortDialogWidth)/2, (MainHeight-SortDialogHeight)/2+MainStartY); } else // if screen is too low to display sorting window, fall back to items list w = Items; hasToBeResized = 0; } std::wstring Playlist::Title() { std::wstring result = L"Playlist "; if (ReloadTotalLength || ReloadRemaining) itsBufferedStats = TotalLength(); result += Scroller(ToWString(itsBufferedStats), itsScrollBegin, COLS-result.length()-(Config.new_design ? 2 : Global::VolumeState.length())); return result; } void Playlist::EnterPressed() { if (w == Items) { if (!Items->empty()) Mpd.PlayID(Items->current().value().getID()); } else if (w == SortDialog) { size_t pos = SortDialog->choice(); auto begin = Items->begin(), end = Items->end(); // if songs are selected, sort range from first selected to last selected if (Items->hasSelected()) { while (!begin->isSelected()) ++begin; while (!(end-1)->isSelected()) --end; } if (pos > SortOptions) { if (pos == SortOptions+2) // cancel { w = Items; return; } } else { Statusbar::msg("Move tag types up and down to adjust sort order"); return; } size_t start_pos = begin-Items->begin(); MPD::SongList playlist; playlist.reserve(end-begin); for (; begin != end; ++begin) playlist.push_back(begin->value()); LocaleStringComparison cmp(std::locale(), Config.ignore_leading_the); std::function iter_swap, quick_sort; auto song_cmp = [&cmp](const MPD::Song &a, const MPD::Song &b) -> bool { for (size_t i = 0; i < SortOptions; ++i) if (int ret = cmp(a.getTags((*SortDialog)[i].value().second), b.getTags((*SortDialog)[i].value().second))) return ret < 0; return a.getPosition() < b.getPosition(); }; iter_swap = [&playlist, &start_pos](MPD::SongList::iterator a, MPD::SongList::iterator b) { std::iter_swap(a, b); Mpd.Swap(start_pos+a-playlist.begin(), start_pos+b-playlist.begin()); }; quick_sort = [this, &song_cmp, &quick_sort, &iter_swap](MPD::SongList::iterator first, MPD::SongList::iterator last) { if (last-first > 1) { MPD::SongList::iterator pivot = first+rand()%(last-first); iter_swap(pivot, last-1); pivot = last-1; MPD::SongList::iterator tmp = first; for (MPD::SongList::iterator i = first; i != pivot; ++i) if (song_cmp(*i, *pivot)) iter_swap(i, tmp++); iter_swap(tmp, pivot); quick_sort(first, tmp); quick_sort(tmp+1, last); } }; Statusbar::msg("Sorting..."); Mpd.StartCommandsList(); quick_sort(playlist.begin(), playlist.end()); if (Mpd.CommitCommandsList()) Statusbar::msg("Playlist sorted"); w = Items; } } void Playlist::SpacePressed() { if (w == Items && !Items->empty()) { Items->current().setSelected(!Items->current().isSelected()); Items->scroll(NC::wDown); } } void Playlist::MouseButtonPressed(MEVENT me) { if (w == Items && !Items->empty() && Items->hasCoords(me.x, me.y)) { if (size_t(me.y) < Items->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))) { Items->Goto(me.y); if (me.bstate & BUTTON3_PRESSED) EnterPressed(); } else Screen::MouseButtonPressed(me); } else if (w == SortDialog && SortDialog->hasCoords(me.x, me.y)) { if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)) { SortDialog->Goto(me.y); if (me.bstate & BUTTON3_PRESSED) EnterPressed(); } else Screen::MouseButtonPressed(me); } } /***********************************************************************/ bool Playlist::allowsFiltering() { return true; } std::string Playlist::currentFilter() { std::string filter; if (w == Items) filter = RegexFilter::currentFilter(*Items); return filter; } void Playlist::applyFilter(const std::string &filter) { if (w == Items) { Items->showAll(); auto rx = RegexFilter(filter, Config.regex_type, playlistEntryMatcher); Items->filter(Items->begin(), Items->end(), rx); } } /***********************************************************************/ bool Playlist::allowsSearching() { return true; } bool Playlist::search(const std::string &constraint) { bool result = false; if (w == Items) { auto rx = RegexFilter(constraint, Config.regex_type, playlistEntryMatcher); result = Items->search(Items->begin(), Items->end(), rx); } return result; } void Playlist::nextFound(bool wrap) { if (w == Items) Items->nextFound(wrap); } void Playlist::prevFound(bool wrap) { if (w == Items) Items->prevFound(wrap); } /***********************************************************************/ std::shared_ptr Playlist::getProxySongList() { auto ptr = nullProxySongList(); if (w == Items) ptr = mkProxySongList(*Items, [](NC::Menu::Item &item) { return &item.value(); }); return ptr; } bool Playlist::allowsSelection() { return w == Items; } void Playlist::reverseSelection() { reverseSelectionHelper(Items->begin(), Items->end()); } MPD::SongList Playlist::getSelectedSongs() { MPD::SongList result; if (w == Items) { for (auto it = Items->begin(); it != Items->end(); ++it) if (it->isSelected()) result.push_back(it->value()); if (result.empty() && !Items->empty()) result.push_back(Items->current().value()); } return result; } /***********************************************************************/ MPD::Song Playlist::nowPlayingSong() { MPD::Song s; if (Mpd.isPlaying()) withUnfilteredMenu(*Items, [this, &s]() { s = Items->at(Mpd.GetCurrentSongPos()).value(); }); return s; } bool Playlist::isFiltered() { if (Items->isFiltered()) { Statusbar::msg("Function currently unavailable due to filtered playlist"); return true; } return false; } void Playlist::Sort() { if (isFiltered()) return; if (Items->getWidth() < SortDialogWidth || MainHeight < 5) Statusbar::msg("Screen is too small to display dialog window"); else { SortDialog->reset(); w = SortDialog; } } void Playlist::Reverse() { if (isFiltered()) return; Statusbar::msg("Reversing playlist order..."); size_t beginning = -1, end = -1; for (size_t i = 0; i < Items->size(); ++i) { if (Items->at(i).isSelected()) { if (beginning == size_t(-1)) beginning = i; end = i; } } if (beginning == size_t(-1)) // no selected items { beginning = 0; end = Items->size(); } Mpd.StartCommandsList(); for (size_t i = beginning, j = end-1; i < (beginning+end)/2; ++i, --j) Mpd.Swap(i, j); if (Mpd.CommitCommandsList()) Statusbar::msg("Playlist reversed"); } void Playlist::EnableHighlighting() { Items->setHighlighting(1); UpdateTimer(); } void Playlist::UpdateTimer() { itsTimer = Global::Timer; } bool Playlist::SortingInProgress() { return w == SortDialog; } std::string Playlist::TotalLength() { std::ostringstream result; if (ReloadTotalLength) { itsTotalLength = 0; for (size_t i = 0; i < Items->size(); ++i) itsTotalLength += (*Items)[i].value().getDuration(); ReloadTotalLength = 0; } if (Config.playlist_show_remaining_time && ReloadRemaining && !Items->isFiltered()) { itsRemainingTime = 0; for (size_t i = Mpd.GetCurrentlyPlayingSongPos(); i < Items->size(); ++i) itsRemainingTime += (*Items)[i].value().getDuration(); ReloadRemaining = false; } result << '(' << Items->size() << (Items->size() == 1 ? " item" : " items"); if (Items->isFiltered()) { Items->showAll(); size_t real_size = Items->size(); Items->showFiltered(); if (Items->size() != real_size) result << " (out of " << Mpd.GetPlaylistLength() << ")"; } if (itsTotalLength) { result << ", length: "; ShowTime(result, itsTotalLength, Config.playlist_shorten_total_times); } if (Config.playlist_show_remaining_time && itsRemainingTime && !Items->isFiltered() && Items->size() > 1) { result << " :: remaining: "; ShowTime(result, itsRemainingTime, Config.playlist_shorten_total_times); } result << ')'; return result.str(); } bool Playlist::Add(const MPD::Song &s, bool play, int position) { if (Config.ncmpc_like_songs_adding && checkForSong(s)) { size_t hash = s.getHash(); if (play) { for (size_t i = 0; i < Items->size(); ++i) { if (Items->at(i).value().getHash() == hash) { Mpd.Play(i); break; } } return true; } else { Mpd.StartCommandsList(); for (size_t i = 0; i < Items->size(); ++i) if ((*Items)[i].value().getHash() == hash) Mpd.Delete(i); Mpd.CommitCommandsList(); return false; } } else { int id = Mpd.AddSong(s, position); if (id >= 0) { Statusbar::msg("Added to playlist: %s", s.toString(Config.song_status_format_no_colors).c_str()); if (play) Mpd.PlayID(id); return true; } else return false; } } bool Playlist::Add(const MPD::SongList &l, bool play, int position) { if (l.empty()) return false; Mpd.StartCommandsList(); if (position < 0) { for (auto it = l.begin(); it != l.end(); ++it) if (Mpd.AddSong(*it) < 0) break; } else { for (auto j = l.rbegin(); j != l.rend(); ++j) if (Mpd.AddSong(*j, position) < 0) break; } if (!Mpd.CommitCommandsList()) return false; if (play) PlayNewlyAddedSongs(); return true; } void Playlist::PlayNewlyAddedSongs() { bool is_filtered = Items->isFiltered(); Items->showAll(); size_t old_size = Items->size(); Mpd.UpdateStatus(); if (old_size < Items->size()) Mpd.Play(old_size); if (is_filtered) Items->showFiltered(); } void Playlist::SetSelectedItemsPriority(int prio) { auto list = getSelectedOrCurrent(Items->begin(), Items->end(), Items->currentI()); Mpd.StartCommandsList(); for (auto it = list.begin(); it != list.end(); ++it) Mpd.SetPriority((*it)->value(), prio); if (Mpd.CommitCommandsList()) Statusbar::msg("Priority set"); } bool Playlist::checkForSong(const MPD::Song &s) { return itsSongHashes.find(s.getHash()) != itsSongHashes.end(); } void Playlist::moveSortOrderDown() { size_t pos = SortDialog->choice(); if (pos < SortOptions-1) { SortDialog->Swap(pos, pos+1); SortDialog->scroll(NC::wDown); } } void Playlist::moveSortOrderUp() { size_t pos = SortDialog->choice(); if (pos > 0 && pos < SortOptions) { SortDialog->Swap(pos, pos-1); SortDialog->scroll(NC::wUp); } } void Playlist::registerHash(size_t hash) { itsSongHashes[hash] += 1; } void Playlist::unregisterHash(size_t hash) { auto it = itsSongHashes.find(hash); assert(it != itsSongHashes.end()); if (it->second == 1) itsSongHashes.erase(it); else it->second -= 1; } namespace {// std::string songToString(const MPD::Song &s) { std::string result; if (Config.columns_in_playlist) result = s.toString(Config.song_in_columns_to_string_format); else result = s.toString(Config.song_list_format_dollar_free); return result; } bool playlistEntryMatcher(const Regex &rx, const MPD::Song &s) { return rx.match(songToString(s)); } }