1 /***************************************************************************
2 * Copyright (C) 2008-2017 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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 *
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 ***************************************************************************/
22 #include <boost/optional.hpp>
23 #include <boost/date_time/posix_time/posix_time.hpp>
26 #include "curses/menu_impl.h"
31 #include "screens/playlist.h"
32 #include "screens/playlist_editor.h"
35 #include "statusbar.h"
36 #include "screens/tag_editor.h"
37 #include "format_impl.h"
38 #include "helpers/song_iterator_maker.h"
39 #include "utility/functional.h"
40 #include "utility/comparators.h"
42 #include "screens/screen_switcher.h"
44 using Global::MainHeight
;
45 using Global::MainStartY
;
47 namespace ph
= std::placeholders
;
49 PlaylistEditor
*myPlaylistEditor
;
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 setHighlightFixes(Playlists
);
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());
83 Content
= NC::Menu
<MPD::Song
>(RightColumnStartX
, MainStartY
, RightColumnWidth
, MainHeight
, Config
.titles_visibility
? "Content" : "", Config
.main_color
, NC::Border());
84 setHighlightInactiveColumnFixes(Content
);
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
)
96 case DisplayMode::Columns
:
97 Content
.setItemDisplayer(std::bind(
98 Display::SongsInColumns
, ph::_1
, std::cref(Content
)
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
);
125 std::wstring
PlaylistEditor::title()
127 return L
"Playlist editor";
130 void PlaylistEditor::refresh()
133 drawSeparator(RightColumnStartX
-1);
137 void PlaylistEditor::switchTo()
139 SwitchTo::execute(this);
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);
155 for (MPD::PlaylistIterator it
= Mpd
.GetPlaylists(), end
; it
!= end
; ++it
, ++idx
)
157 if (idx
< Playlists
.size())
158 Playlists
[idx
].value() = std::move(*it
);
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());
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);
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
);
192 Content
.addItem(std::move(*s
));
194 if (idx
< Content
.size())
195 Content
.resizeList(idx
);
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
);
214 return m_window_timeout
;
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())
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);
237 Screen
<WindowType
>::mouseButtonPressed(me
);
240 else if (Content
.hasCoords(me
.x
, me
.y
))
242 if (!isActiveWindow(Content
))
244 if (nextColumnAvailable())
249 if (size_t(me
.y
) < Content
.size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
252 bool play
= me
.bstate
& BUTTON3_PRESSED
;
253 addItemToPlaylist(play
);
256 Screen
<WindowType
>::mouseButtonPressed(me
);
260 /***********************************************************************/
262 bool PlaylistEditor::allowsSearching()
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
>(
283 PlaylistEntryMatcher
);
285 else if (isActiveWindow(Content
))
287 m_content_search_predicate
= Regex::Filter
<MPD::Song
>(
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
)
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
);
312 /***********************************************************************/
314 bool PlaylistEditor::allowsFiltering()
316 return allowsSearching();
319 std::string
PlaylistEditor::currentFilter()
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();
335 void PlaylistEditor::applyFilter(const std::string
&constraint
)
337 if (isActiveWindow(Playlists
))
339 if (!constraint
.empty())
341 Playlists
.applyFilter(Regex::Filter
<MPD::Playlist
>(
344 PlaylistEntryMatcher
));
347 Playlists
.clearFilter();
349 else if (isActiveWindow(Content
))
351 if (!constraint
.empty())
353 Content
.applyFilter(Regex::Filter
<MPD::Song
>(
359 Content
.clearFilter();
364 /***********************************************************************/
366 bool PlaylistEditor::itemAvailable()
368 if (isActiveWindow(Playlists
))
369 return !Playlists
.empty();
370 if (isActiveWindow(Content
))
371 return !Content
.empty();
375 bool PlaylistEditor::addItemToPlaylist(bool play
)
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
);
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
)
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();
417 /***********************************************************************/
419 bool PlaylistEditor::previousColumnAvailable()
421 if (isActiveWindow(Content
))
423 ScopedUnfilteredMenu
<MPD::Playlist
> sunfilter_playlists(ReapplyFilter::No
, Playlists
);
424 if (!Playlists
.empty())
430 void PlaylistEditor::previousColumn()
432 if (isActiveWindow(Content
))
434 setHighlightInactiveColumnFixes(Content
);
437 setHighlightFixes(Playlists
);
441 bool PlaylistEditor::nextColumnAvailable()
443 if (isActiveWindow(Playlists
))
445 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_content(ReapplyFilter::No
, Content
);
446 if (!Content
.empty())
452 void PlaylistEditor::nextColumn()
454 if (isActiveWindow(Playlists
))
456 setHighlightInactiveColumnFixes(Playlists
);
459 setHighlightFixes(Content
);
463 /***********************************************************************/
465 void PlaylistEditor::updateTimer()
467 m_timer
= Global::Timer
;
470 void PlaylistEditor::locatePlaylist(const MPD::Playlist
&playlist
)
473 Playlists
.clearFilter();
474 auto first
= Playlists
.beginV(), last
= Playlists
.endV();
475 auto it
= std::find(first
, last
, playlist
);
478 Playlists
.highlight(it
- first
);
480 Content
.clearFilter();
485 void PlaylistEditor::locateSong(const MPD::Song
&s
)
487 if (Playlists
.empty())
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
);
499 Content
.highlight(it
- Content
.beginV());
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());
514 requestContentUpdate();
516 Content
.highlight(*song_index
);
526 if (locate_song_in_current_playlist(Content
.currentV() + 1, Content
.endV()))
528 Statusbar::print("Jumping to song...");
529 if (locate_song_in_playlists(Playlists
.currentV() + 1, Playlists
.endV()))
531 if (locate_song_in_playlists(Playlists
.beginV(), Playlists
.currentV()))
533 if (locate_song_in_current_playlist(Content
.beginV(), Content
.currentV()))
536 // Highlighted song was skipped, so if that's the one we're looking for, we're
538 if (Content
.empty() || *Content
.currentV() != s
)
539 Statusbar::print("Song was not found in playlists");
544 std::string
SongToString(const MPD::Song
&s
)
547 switch (Config
.playlist_display_mode
)
549 case DisplayMode::Classic
:
550 result
= Format::stringify
<char>(Config
.song_list_format
, &s
);
552 case DisplayMode::Columns
:
553 result
= Format::stringify
<char>(Config
.song_columns_mode_format
, &s
);
559 bool PlaylistEntryMatcher(const Regex::Regex
&rx
, const MPD::Playlist
&playlist
)
561 return Regex::search(playlist
.path(), rx
, Config
.ignore_diacritics
);
564 bool SongEntryMatcher(const Regex::Regex
&rx
, const MPD::Song
&s
)
566 return Regex::search(SongToString(s
), rx
, Config
.ignore_diacritics
);
569 boost::optional
<size_t> GetSongIndexInPlaylist(MPD::Playlist playlist
, const MPD::Song
&song
)
572 MPD::SongIterator it
= Mpd
.GetPlaylistContentNoInfo(playlist
.path()), end
;