Print message only after song is not found in a current playlist
[ncmpcpp.git] / src / screens / playlist_editor.cpp
blob503b54bbac05de643bc885f331f08d98965930d4
1 /***************************************************************************
2 * Copyright (C) 2008-2017 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 <boost/lambda/bind.hpp>
23 #include <boost/optional.hpp>
24 #include <boost/date_time/posix_time/posix_time.hpp>
25 #include <cassert>
27 #include "curses/menu_impl.h"
28 #include "charset.h"
29 #include "display.h"
30 #include "global.h"
31 #include "helpers.h"
32 #include "screens/playlist.h"
33 #include "screens/playlist_editor.h"
34 #include "mpdpp.h"
35 #include "status.h"
36 #include "statusbar.h"
37 #include "screens/tag_editor.h"
38 #include "helpers/song_iterator_maker.h"
39 #include "utility/functional.h"
40 #include "utility/comparators.h"
41 #include "title.h"
42 #include "screens/screen_switcher.h"
44 using Global::MainHeight;
45 using Global::MainStartY;
47 namespace ph = std::placeholders;
49 PlaylistEditor *myPlaylistEditor;
51 namespace {
53 size_t LeftColumnStartX;
54 size_t LeftColumnWidth;
55 size_t RightColumnStartX;
56 size_t RightColumnWidth;
58 std::string SongToString(const MPD::Song &s);
59 bool PlaylistEntryMatcher(const Regex::Regex &rx, const MPD::Playlist &playlist);
60 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::Song &s);
61 boost::optional<size_t> GetSongIndexInPlaylist(MPD::Playlist playlist, const MPD::Song &song);
64 PlaylistEditor::PlaylistEditor()
65 : m_timer(boost::posix_time::from_time_t(0))
66 , m_window_timeout(Config.data_fetching_delay ? 250 : BaseScreen::defaultWindowTimeout)
67 , m_fetching_delay(boost::posix_time::milliseconds(Config.data_fetching_delay ? 250 : -1))
69 LeftColumnWidth = COLS/3-1;
70 RightColumnStartX = LeftColumnWidth+1;
71 RightColumnWidth = COLS-LeftColumnWidth-1;
73 Playlists = NC::Menu<MPD::Playlist>(0, MainStartY, LeftColumnWidth, MainHeight, Config.titles_visibility ? "Playlists" : "", Config.main_color, NC::Border());
74 Playlists.setHighlightColor(Config.active_column_color);
75 Playlists.cyclicScrolling(Config.use_cyclic_scrolling);
76 Playlists.centeredCursor(Config.centered_cursor);
77 Playlists.setSelectedPrefix(Config.selected_item_prefix);
78 Playlists.setSelectedSuffix(Config.selected_item_suffix);
79 Playlists.setItemDisplayer([](NC::Menu<MPD::Playlist> &menu) {
80 menu << Charset::utf8ToLocale(menu.drawn()->value().path());
81 });
83 Content = NC::Menu<MPD::Song>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, Config.titles_visibility ? "Content" : "", Config.main_color, NC::Border());
84 Content.setHighlightColor(Config.main_highlight_color);
85 Content.cyclicScrolling(Config.use_cyclic_scrolling);
86 Content.centeredCursor(Config.centered_cursor);
87 Content.setSelectedPrefix(Config.selected_item_prefix);
88 Content.setSelectedSuffix(Config.selected_item_suffix);
89 switch (Config.playlist_editor_display_mode)
91 case DisplayMode::Classic:
92 Content.setItemDisplayer(std::bind(
93 Display::Songs, ph::_1, std::cref(Content), std::cref(Config.song_list_format)
94 ));
95 break;
96 case DisplayMode::Columns:
97 Content.setItemDisplayer(std::bind(
98 Display::SongsInColumns, ph::_1, std::cref(Content)
99 ));
100 break;
103 w = &Playlists;
106 void PlaylistEditor::resize()
108 size_t x_offset, width;
109 getWindowResizeParams(x_offset, width);
111 LeftColumnStartX = x_offset;
112 LeftColumnWidth = width/3-1;
113 RightColumnStartX = LeftColumnStartX+LeftColumnWidth+1;
114 RightColumnWidth = width-LeftColumnWidth-1;
116 Playlists.resize(LeftColumnWidth, MainHeight);
117 Content.resize(RightColumnWidth, MainHeight);
119 Playlists.moveTo(LeftColumnStartX, MainStartY);
120 Content.moveTo(RightColumnStartX, MainStartY);
122 hasToBeResized = 0;
125 std::wstring PlaylistEditor::title()
127 return L"Playlist editor";
130 void PlaylistEditor::refresh()
132 Playlists.display();
133 drawSeparator(RightColumnStartX-1);
134 Content.display();
137 void PlaylistEditor::switchTo()
139 SwitchTo::execute(this);
140 drawHeader();
141 refresh();
144 void PlaylistEditor::update()
147 ScopedUnfilteredMenu<MPD::Playlist> sunfilter_playlists(ReapplyFilter::No, Playlists);
148 if (Playlists.empty() || m_playlists_update_requested)
150 m_playlists_update_requested = false;
151 sunfilter_playlists.set(ReapplyFilter::Yes, true);
152 size_t idx = 0;
155 for (MPD::PlaylistIterator it = Mpd.GetPlaylists(), end; it != end; ++it, ++idx)
157 if (idx < Playlists.size())
158 Playlists[idx].value() = std::move(*it);
159 else
160 Playlists.addItem(std::move(*it));
163 catch (MPD::ServerError &e)
165 if (e.code() == MPD_SERVER_ERROR_SYSTEM) // no playlists directory
166 Statusbar::print(e.what());
167 else
168 throw;
170 if (idx < Playlists.size())
171 Playlists.resizeList(idx);
172 std::sort(Playlists.beginV(), Playlists.endV(),
173 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
178 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
179 if (!Playlists.empty()
180 && ((Content.empty() && Global::Timer - m_timer > m_fetching_delay)
181 || m_content_update_requested))
183 m_content_update_requested = false;
184 sunfilter_content.set(ReapplyFilter::Yes, true);
185 size_t idx = 0;
186 MPD::SongIterator s = Mpd.GetPlaylistContent(Playlists.current()->value().path()), end;
187 for (; s != end; ++s, ++idx)
189 if (idx < Content.size())
190 Content[idx].value() = std::move(*s);
191 else
192 Content.addItem(std::move(*s));
194 if (idx < Content.size())
195 Content.resizeList(idx);
196 std::string wtitle;
197 if (Config.titles_visibility)
199 wtitle = (boost::format("Content (%1% %2%)")
200 % boost::lexical_cast<std::string>(Content.size())
201 % (Content.size() == 1 ? "item" : "items")).str();
202 wtitle.resize(Content.getWidth());
204 Content.setTitle(wtitle);
205 Content.refreshBorder();
210 int PlaylistEditor::windowTimeout()
212 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
213 if (Content.empty())
214 return m_window_timeout;
215 else
216 return Screen<WindowType>::windowTimeout();
219 void PlaylistEditor::mouseButtonPressed(MEVENT me)
221 if (Playlists.hasCoords(me.x, me.y))
223 if (!isActiveWindow(Playlists))
225 if (previousColumnAvailable())
226 previousColumn();
227 else
228 return;
230 if (size_t(me.y) < Playlists.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
232 Playlists.Goto(me.y);
233 if (me.bstate & BUTTON3_PRESSED)
234 addItemToPlaylist(false);
236 else
237 Screen<WindowType>::mouseButtonPressed(me);
238 Content.clear();
240 else if (Content.hasCoords(me.x, me.y))
242 if (!isActiveWindow(Content))
244 if (nextColumnAvailable())
245 nextColumn();
246 else
247 return;
249 if (size_t(me.y) < Content.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
251 Content.Goto(me.y);
252 bool play = me.bstate & BUTTON3_PRESSED;
253 addItemToPlaylist(play);
255 else
256 Screen<WindowType>::mouseButtonPressed(me);
260 /***********************************************************************/
262 bool PlaylistEditor::allowsSearching()
264 return true;
267 const std::string &PlaylistEditor::searchConstraint()
269 if (isActiveWindow(Playlists))
270 return m_playlists_search_predicate.constraint();
271 else if (isActiveWindow(Content))
272 return m_content_search_predicate.constraint();
273 throw std::runtime_error("no active window");
276 void PlaylistEditor::setSearchConstraint(const std::string &constraint)
278 if (isActiveWindow(Playlists))
280 m_playlists_search_predicate = Regex::Filter<MPD::Playlist>(
281 constraint,
282 Config.regex_type,
283 PlaylistEntryMatcher);
285 else if (isActiveWindow(Content))
287 m_content_search_predicate = Regex::Filter<MPD::Song>(
288 constraint,
289 Config.regex_type,
290 SongEntryMatcher);
294 void PlaylistEditor::clearSearchConstraint()
296 if (isActiveWindow(Playlists))
297 m_playlists_search_predicate.clear();
298 else if (isActiveWindow(Content))
299 m_content_search_predicate.clear();
302 bool PlaylistEditor::search(SearchDirection direction, bool wrap, bool skip_current)
304 bool result = false;
305 if (isActiveWindow(Playlists))
306 result = ::search(Playlists, m_playlists_search_predicate, direction, wrap, skip_current);
307 else if (isActiveWindow(Content))
308 result = ::search(Content, m_content_search_predicate, direction, wrap, skip_current);
309 return result;
312 /***********************************************************************/
314 bool PlaylistEditor::allowsFiltering()
316 return allowsSearching();
319 std::string PlaylistEditor::currentFilter()
321 std::string result;
322 if (isActiveWindow(Playlists))
324 if (auto pred = Playlists.filterPredicate<Regex::Filter<MPD::Playlist>>())
325 result = pred->constraint();
327 else if (isActiveWindow(Content))
329 if (auto pred = Content.filterPredicate<Regex::Filter<MPD::Song>>())
330 result = pred->constraint();
332 return result;
335 void PlaylistEditor::applyFilter(const std::string &constraint)
337 if (isActiveWindow(Playlists))
339 if (!constraint.empty())
341 Playlists.applyFilter(Regex::Filter<MPD::Playlist>(
342 constraint,
343 Config.regex_type,
344 PlaylistEntryMatcher));
346 else
347 Playlists.clearFilter();
349 else if (isActiveWindow(Content))
351 if (!constraint.empty())
353 Content.applyFilter(Regex::Filter<MPD::Song>(
354 constraint,
355 Config.regex_type,
356 SongEntryMatcher));
358 else
359 Content.clearFilter();
364 /***********************************************************************/
366 bool PlaylistEditor::itemAvailable()
368 if (isActiveWindow(Playlists))
369 return !Playlists.empty();
370 if (isActiveWindow(Content))
371 return !Content.empty();
372 return false;
375 bool PlaylistEditor::addItemToPlaylist(bool play)
377 bool result = false;
378 if (isActiveWindow(Playlists))
380 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
381 result = addSongsToPlaylist(Content.beginV(), Content.endV(), play, -1);
382 Statusbar::printf("Playlist \"%1%\" loaded%2%",
383 Playlists.current()->value().path(), withErrors(result));
385 else if (isActiveWindow(Content))
386 result = addSongToPlaylist(Content.current()->value(), play);
387 return result;
390 std::vector<MPD::Song> PlaylistEditor::getSelectedSongs()
392 std::vector<MPD::Song> result;
393 if (isActiveWindow(Playlists))
395 bool any_selected = false;
396 for (auto &e : Playlists)
398 if (e.isSelected())
400 any_selected = true;
401 std::copy(
402 std::make_move_iterator(Mpd.GetPlaylistContent(e.value().path())),
403 std::make_move_iterator(MPD::SongIterator()),
404 std::back_inserter(result));
407 // if no item is selected, add songs from right column
408 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
409 if (!any_selected && !Playlists.empty())
410 std::copy(Content.beginV(), Content.endV(), std::back_inserter(result));
412 else if (isActiveWindow(Content))
413 result = Content.getSelectedSongs();
414 return result;
417 /***********************************************************************/
419 bool PlaylistEditor::previousColumnAvailable()
421 if (isActiveWindow(Content))
423 ScopedUnfilteredMenu<MPD::Playlist> sunfilter_playlists(ReapplyFilter::No, Playlists);
424 if (!Playlists.empty())
425 return true;
427 return false;
430 void PlaylistEditor::previousColumn()
432 if (isActiveWindow(Content))
434 Content.setHighlightColor(Config.main_highlight_color);
435 w->refresh();
436 w = &Playlists;
437 Playlists.setHighlightColor(Config.active_column_color);
441 bool PlaylistEditor::nextColumnAvailable()
443 if (isActiveWindow(Playlists))
445 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
446 if (!Content.empty())
447 return true;
449 return false;
452 void PlaylistEditor::nextColumn()
454 if (isActiveWindow(Playlists))
456 Playlists.setHighlightColor(Config.main_highlight_color);
457 w->refresh();
458 w = &Content;
459 Content.setHighlightColor(Config.active_column_color);
463 /***********************************************************************/
465 void PlaylistEditor::updateTimer()
467 m_timer = Global::Timer;
470 void PlaylistEditor::locatePlaylist(const MPD::Playlist &playlist)
472 update();
473 Playlists.clearFilter();
474 auto first = Playlists.beginV(), last = Playlists.endV();
475 auto it = std::find(first, last, playlist);
476 if (it != last)
478 Playlists.highlight(it - first);
479 Content.clear();
480 Content.clearFilter();
481 switchTo();
485 void PlaylistEditor::locateSong(const MPD::Song &s)
487 if (Playlists.empty())
488 return;
490 Content.clearFilter();
491 Playlists.clearFilter();
493 auto locate_song_in_current_playlist = [this, &s](auto front, auto back) {
494 if (!Content.empty())
496 auto it = std::find(front, back, s);
497 if (it != back)
499 Content.highlight(it - Content.beginV());
500 nextColumn();
501 return true;
504 return false;
506 auto locate_song_in_playlists = [this, &s](auto front, auto back) {
507 for (auto it = front; it != back; ++it)
509 if (auto song_index = GetSongIndexInPlaylist(*it, s))
511 Playlists.highlight(it - Playlists.beginV());
512 Playlists.refresh();
514 requestContentUpdate();
515 update();
516 Content.highlight(*song_index);
517 nextColumn();
519 return true;
522 return false;
526 if (locate_song_in_current_playlist(Content.currentV() + 1, Content.endV()))
527 return;
528 Statusbar::print("Jumping to song...");
529 if (locate_song_in_playlists(Playlists.currentV() + 1, Playlists.endV()))
530 return;
531 if (locate_song_in_playlists(Playlists.beginV(), Playlists.currentV()))
532 return;
533 if (locate_song_in_current_playlist(Content.beginV(), Content.currentV()))
534 return;
536 // Highlighted song was skipped, so if that's the one we're looking for, we're
537 // good.
538 if (Content.empty() || *Content.currentV() != s)
539 Statusbar::print("Song was not found in playlists");
542 namespace {
544 std::string SongToString(const MPD::Song &s)
546 std::string result;
547 switch (Config.playlist_display_mode)
549 case DisplayMode::Classic:
550 result = Format::stringify<char>(Config.song_list_format, &s);
551 break;
552 case DisplayMode::Columns:
553 result = Format::stringify<char>(Config.song_columns_mode_format, &s);
554 break;
556 return result;
559 bool PlaylistEntryMatcher(const Regex::Regex &rx, const MPD::Playlist &playlist)
561 return Regex::search(playlist.path(), rx);
564 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::Song &s)
566 return Regex::search(SongToString(s), rx);
569 boost::optional<size_t> GetSongIndexInPlaylist(MPD::Playlist playlist, const MPD::Song &song)
571 size_t index = 0;
572 MPD::SongIterator it = Mpd.GetPlaylistContentNoInfo(playlist.path()), end;
574 for (;;)
576 if (it == end)
577 return boost::none;
578 if (*it == song)
579 return index;
581 ++it, ++index;