Implement filtering in playlist editor
[ncmpcpp.git] / src / playlist_editor.cpp
blob0008d9c8fc2bca79a23a423e2b589660a7b95359
1 /***************************************************************************
2 * Copyright (C) 2008-2014 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/date_time/posix_time/posix_time.hpp>
24 #include <cassert>
26 #include "charset.h"
27 #include "display.h"
28 #include "global.h"
29 #include "helpers.h"
30 #include "playlist.h"
31 #include "playlist_editor.h"
32 #include "menu_impl.h"
33 #include "mpdpp.h"
34 #include "status.h"
35 #include "statusbar.h"
36 #include "tag_editor.h"
37 #include "helpers/song_iterator_maker.h"
38 #include "utility/functional.h"
39 #include "utility/comparators.h"
40 #include "title.h"
41 #include "screen_switcher.h"
43 using Global::MainHeight;
44 using Global::MainStartY;
46 namespace ph = std::placeholders;
48 PlaylistEditor *myPlaylistEditor;
50 namespace {
52 size_t LeftColumnStartX;
53 size_t LeftColumnWidth;
54 size_t RightColumnStartX;
55 size_t RightColumnWidth;
57 std::string SongToString(const MPD::Song &s);
58 bool PlaylistEntryMatcher(const Regex::Regex &rx, const MPD::Playlist &playlist);
59 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::Song &s);
63 PlaylistEditor::PlaylistEditor()
64 : m_timer(boost::posix_time::from_time_t(0))
65 , m_window_timeout(Config.data_fetching_delay ? 250 : BaseScreen::defaultWindowTimeout)
66 , m_fetching_delay(boost::posix_time::milliseconds(Config.data_fetching_delay ? 250 : -1))
68 LeftColumnWidth = COLS/3-1;
69 RightColumnStartX = LeftColumnWidth+1;
70 RightColumnWidth = COLS-LeftColumnWidth-1;
72 Playlists = NC::Menu<MPD::Playlist>(0, MainStartY, LeftColumnWidth, MainHeight, Config.titles_visibility ? "Playlists" : "", Config.main_color, NC::Border());
73 Playlists.setHighlightColor(Config.active_column_color);
74 Playlists.cyclicScrolling(Config.use_cyclic_scrolling);
75 Playlists.centeredCursor(Config.centered_cursor);
76 Playlists.setSelectedPrefix(Config.selected_item_prefix);
77 Playlists.setSelectedSuffix(Config.selected_item_suffix);
78 Playlists.setItemDisplayer([](NC::Menu<MPD::Playlist> &menu) {
79 menu << Charset::utf8ToLocale(menu.drawn()->value().path());
80 });
82 Content = NC::Menu<MPD::Song>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, Config.titles_visibility ? "Content" : "", Config.main_color, NC::Border());
83 Content.setHighlightColor(Config.main_highlight_color);
84 Content.cyclicScrolling(Config.use_cyclic_scrolling);
85 Content.centeredCursor(Config.centered_cursor);
86 Content.setSelectedPrefix(Config.selected_item_prefix);
87 Content.setSelectedSuffix(Config.selected_item_suffix);
88 switch (Config.playlist_editor_display_mode)
90 case DisplayMode::Classic:
91 Content.setItemDisplayer(std::bind(
92 Display::Songs, ph::_1, std::cref(Content), std::cref(Config.song_list_format)
93 ));
94 break;
95 case DisplayMode::Columns:
96 Content.setItemDisplayer(std::bind(
97 Display::SongsInColumns, ph::_1, std::cref(Content)
98 ));
99 break;
102 w = &Playlists;
105 void PlaylistEditor::resize()
107 size_t x_offset, width;
108 getWindowResizeParams(x_offset, width);
110 LeftColumnStartX = x_offset;
111 LeftColumnWidth = width/3-1;
112 RightColumnStartX = LeftColumnStartX+LeftColumnWidth+1;
113 RightColumnWidth = width-LeftColumnWidth-1;
115 Playlists.resize(LeftColumnWidth, MainHeight);
116 Content.resize(RightColumnWidth, MainHeight);
118 Playlists.moveTo(LeftColumnStartX, MainStartY);
119 Content.moveTo(RightColumnStartX, MainStartY);
121 hasToBeResized = 0;
124 std::wstring PlaylistEditor::title()
126 return L"Playlist editor";
129 void PlaylistEditor::refresh()
131 Playlists.display();
132 drawSeparator(RightColumnStartX-1);
133 Content.display();
136 void PlaylistEditor::switchTo()
138 SwitchTo::execute(this);
139 markSongsInPlaylist(Content);
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 bool in_playlist = myPlaylist->checkForSong(*s);
190 if (idx < Content.size())
192 Content[idx].setBold(in_playlist);
193 Content[idx].value() = std::move(*s);
195 else
197 auto properties = NC::List::Properties::Selectable;
198 if (in_playlist)
199 properties |= NC::List::Properties::Bold;
200 Content.addItem(std::move(*s), properties);
203 if (idx < Content.size())
204 Content.resizeList(idx);
205 std::string wtitle;
206 if (Config.titles_visibility)
208 wtitle = (boost::format("Content (%1% %2%)")
209 % boost::lexical_cast<std::string>(Content.size())
210 % (Content.size() == 1 ? "item" : "items")).str();
211 wtitle.resize(Content.getWidth());
213 Content.setTitle(wtitle);
214 Content.refreshBorder();
219 int PlaylistEditor::windowTimeout()
221 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
222 if (Content.empty())
223 return m_window_timeout;
224 else
225 return Screen<WindowType>::windowTimeout();
228 void PlaylistEditor::mouseButtonPressed(MEVENT me)
230 if (!Playlists.empty() && Playlists.hasCoords(me.x, me.y))
232 if (!isActiveWindow(Playlists))
234 if (previousColumnAvailable())
235 previousColumn();
236 else
237 return;
239 if (size_t(me.y) < Playlists.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
241 Playlists.Goto(me.y);
242 if (me.bstate & BUTTON3_PRESSED)
243 addItemToPlaylist(false);
245 else
246 Screen<WindowType>::mouseButtonPressed(me);
247 Content.clear();
249 else if (!Content.empty() && Content.hasCoords(me.x, me.y))
251 if (!isActiveWindow(Content))
253 if (nextColumnAvailable())
254 nextColumn();
255 else
256 return;
258 if (size_t(me.y) < Content.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
260 Content.Goto(me.y);
261 bool play = me.bstate & BUTTON3_PRESSED;
262 addItemToPlaylist(play);
264 else
265 Screen<WindowType>::mouseButtonPressed(me);
269 /***********************************************************************/
271 bool PlaylistEditor::allowsSearching()
273 return true;
276 const std::string &PlaylistEditor::searchConstraint()
278 if (isActiveWindow(Playlists))
279 return m_playlists_search_predicate.constraint();
280 else if (isActiveWindow(Content))
281 return m_content_search_predicate.constraint();
282 throw std::runtime_error("no active window");
285 void PlaylistEditor::setSearchConstraint(const std::string &constraint)
287 if (isActiveWindow(Playlists))
289 m_playlists_search_predicate = Regex::Filter<MPD::Playlist>(
290 constraint,
291 Config.regex_type,
292 PlaylistEntryMatcher);
294 else if (isActiveWindow(Content))
296 m_content_search_predicate = Regex::Filter<MPD::Song>(
297 constraint,
298 Config.regex_type,
299 SongEntryMatcher);
303 void PlaylistEditor::clearSearchConstraint()
305 if (isActiveWindow(Playlists))
306 m_playlists_search_predicate.clear();
307 else if (isActiveWindow(Content))
308 m_content_search_predicate.clear();
311 bool PlaylistEditor::search(SearchDirection direction, bool wrap, bool skip_current)
313 bool result = false;
314 if (isActiveWindow(Playlists))
315 result = ::search(Playlists, m_playlists_search_predicate, direction, wrap, skip_current);
316 else if (isActiveWindow(Content))
317 result = ::search(Content, m_content_search_predicate, direction, wrap, skip_current);
318 return result;
321 std::string PlaylistEditor::currentFilter()
323 std::string result;
324 if (isActiveWindow(Playlists))
326 if (auto pred = Playlists.filterPredicate<Regex::Filter<MPD::Playlist>>())
327 result = pred->constraint();
329 else if (isActiveWindow(Content))
331 if (auto pred = Content.filterPredicate<Regex::Filter<MPD::Song>>())
332 result = pred->constraint();
334 return result;
337 void PlaylistEditor::applyFilter(const std::string &constraint)
339 if (isActiveWindow(Playlists))
341 if (!constraint.empty())
343 Playlists.applyFilter(Regex::Filter<MPD::Playlist>(
344 constraint,
345 Config.regex_type,
346 PlaylistEntryMatcher));
348 else
349 Playlists.clearFilter();
351 else if (isActiveWindow(Content))
353 if (!constraint.empty())
355 Content.applyFilter(Regex::Filter<MPD::Song>(
356 constraint,
357 Config.regex_type,
358 SongEntryMatcher));
360 else
361 Content.clearFilter();
366 /***********************************************************************/
368 bool PlaylistEditor::itemAvailable()
370 if (isActiveWindow(Playlists))
371 return !Playlists.empty();
372 if (isActiveWindow(Content))
373 return !Content.empty();
374 return false;
377 bool PlaylistEditor::addItemToPlaylist(bool play)
379 bool result = false;
380 if (isActiveWindow(Playlists))
382 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
383 result = addSongsToPlaylist(Content.beginV(), Content.endV(), play, -1);
384 Statusbar::printf("Playlist \"%1%\" loaded%2%",
385 Playlists.current()->value().path(), withErrors(result));
387 else if (isActiveWindow(Content))
388 result = addSongToPlaylist(Content.current()->value(), play);
389 return result;
392 std::vector<MPD::Song> PlaylistEditor::getSelectedSongs()
394 std::vector<MPD::Song> result;
395 if (isActiveWindow(Playlists))
397 bool any_selected = false;
398 for (auto &e : Playlists)
400 if (e.isSelected())
402 any_selected = true;
403 std::copy(
404 std::make_move_iterator(Mpd.GetPlaylistContent(e.value().path())),
405 std::make_move_iterator(MPD::SongIterator()),
406 std::back_inserter(result));
409 // if no item is selected, add songs from right column
410 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
411 if (!any_selected && !Playlists.empty())
412 std::copy(Content.beginV(), Content.endV(), std::back_inserter(result));
414 else if (isActiveWindow(Content))
415 result = Content.getSelectedSongs();
416 return result;
419 /***********************************************************************/
421 bool PlaylistEditor::previousColumnAvailable()
423 if (isActiveWindow(Content))
425 ScopedUnfilteredMenu<MPD::Playlist> sunfilter_playlists(ReapplyFilter::No, Playlists);
426 if (!Playlists.empty())
427 return true;
429 return false;
432 void PlaylistEditor::previousColumn()
434 if (isActiveWindow(Content))
436 Content.setHighlightColor(Config.main_highlight_color);
437 w->refresh();
438 w = &Playlists;
439 Playlists.setHighlightColor(Config.active_column_color);
443 bool PlaylistEditor::nextColumnAvailable()
445 if (isActiveWindow(Playlists))
447 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
448 if (!Content.empty())
449 return true;
451 return false;
454 void PlaylistEditor::nextColumn()
456 if (isActiveWindow(Playlists))
458 Playlists.setHighlightColor(Config.main_highlight_color);
459 w->refresh();
460 w = &Content;
461 Content.setHighlightColor(Config.active_column_color);
465 /***********************************************************************/
467 void PlaylistEditor::updateTimer()
469 m_timer = Global::Timer;
472 void PlaylistEditor::locatePlaylist(const MPD::Playlist &playlist)
474 update();
475 Playlists.clearFilter();
476 auto first = Playlists.beginV(), last = Playlists.endV();
477 auto it = std::find(first, last, playlist);
478 if (it != last)
480 Playlists.highlight(it - first);
481 Content.clear();
482 Content.clearFilter();
483 switchTo();
487 namespace {
489 std::string SongToString(const MPD::Song &s)
491 std::string result;
492 switch (Config.playlist_display_mode)
494 case DisplayMode::Classic:
495 result = Format::stringify<char>(Config.song_list_format, &s);
496 break;
497 case DisplayMode::Columns:
498 result = Format::stringify<char>(Config.song_columns_mode_format, &s);
499 break;
501 return result;
504 bool PlaylistEntryMatcher(const Regex::Regex &rx, const MPD::Playlist &playlist)
506 return Regex::search(playlist.path(), rx);
509 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::Song &s)
511 return Regex::search(SongToString(s), rx);