1 /***************************************************************************
2 * Copyright (C) 2008-2021 by Andrzej Rybczak *
3 * andrzej@rybczak.net *
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 ***************************************************************************/
20
21 #include <algorithm>
22 #include <boost/optional.hpp>
23 #include <boost/date_time/posix_time/posix_time.hpp>
24 #include <cassert>
25
26 #include "curses/menu_impl.h"
27 #include "charset.h"
28 #include "display.h"
29 #include "global.h"
30 #include "helpers.h"
31 #include "screens/playlist.h"
32 #include "screens/playlist_editor.h"
33 #include "mpdpp.h"
34 #include "status.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"
41 #include "title.h"
42 #include "screens/screen_switcher.h"
43
44 using Global::MainHeight;
45 using Global::MainStartY;
46
47 namespace ph = std::placeholders;
48
49 PlaylistEditor *myPlaylistEditor;
50
51 namespace {
52
53 size_t LeftColumnStartX;
54 size_t LeftColumnWidth;
55 size_t RightColumnStartX;
56 size_t RightColumnWidth;
57
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);
62 }
63
PlaylistEditor()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))
68 {
69 LeftColumnWidth = COLS/3-1;
70 RightColumnStartX = LeftColumnWidth+1;
71 RightColumnWidth = COLS-LeftColumnWidth-1;
72
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());
81 });
82
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)
90 {
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;
101 }
102
103 w = &Playlists;
104 }
105
resize()106 void PlaylistEditor::resize()
107 {
108 size_t x_offset, width;
109 getWindowResizeParams(x_offset, width);
110
111 LeftColumnStartX = x_offset;
112 LeftColumnWidth = width/3-1;
113 RightColumnStartX = LeftColumnStartX+LeftColumnWidth+1;
114 RightColumnWidth = width-LeftColumnWidth-1;
115
116 Playlists.resize(LeftColumnWidth, MainHeight);
117 Content.resize(RightColumnWidth, MainHeight);
118
119 Playlists.moveTo(LeftColumnStartX, MainStartY);
120 Content.moveTo(RightColumnStartX, MainStartY);
121
122 hasToBeResized = 0;
123 }
124
title()125 std::wstring PlaylistEditor::title()
126 {
127 return L"Playlist editor";
128 }
129
refresh()130 void PlaylistEditor::refresh()
131 {
132 Playlists.display();
133 drawSeparator(RightColumnStartX-1);
134 Content.display();
135 }
136
switchTo()137 void PlaylistEditor::switchTo()
138 {
139 SwitchTo::execute(this);
140 drawHeader();
141 refresh();
142 }
143
update()144 void PlaylistEditor::update()
145 {
146 {
147 ScopedUnfilteredMenu<MPD::Playlist> sunfilter_playlists(ReapplyFilter::No, Playlists);
148 if (Playlists.empty() || m_playlists_update_requested)
149 {
150 m_playlists_update_requested = false;
151 sunfilter_playlists.set(ReapplyFilter::Yes, true);
152 size_t idx = 0;
153 try
154 {
155 for (MPD::PlaylistIterator it = Mpd.GetPlaylists(), end; it != end; ++it, ++idx)
156 {
157 if (idx < Playlists.size())
158 Playlists[idx].value() = std::move(*it);
159 else
160 Playlists.addItem(std::move(*it));
161 };
162 }
163 catch (MPD::ServerError &e)
164 {
165 if (e.code() == MPD_SERVER_ERROR_SYSTEM) // no playlists directory
166 Statusbar::print(e.what());
167 else
168 throw;
169 }
170 if (idx < Playlists.size())
171 Playlists.resizeList(idx);
172 std::sort(Playlists.beginV(), Playlists.endV(),
173 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
174 }
175 }
176
177 {
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))
182 {
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)
188 {
189 if (idx < Content.size())
190 Content[idx].value() = std::move(*s);
191 else
192 Content.addItem(std::move(*s));
193 }
194 if (idx < Content.size())
195 Content.resizeList(idx);
196 std::string wtitle;
197 if (Config.titles_visibility)
198 {
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());
203 }
204 Content.setTitle(wtitle);
205 Content.refreshBorder();
206 }
207 }
208 }
209
windowTimeout()210 int PlaylistEditor::windowTimeout()
211 {
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();
217 }
218
mouseButtonPressed(MEVENT me)219 void PlaylistEditor::mouseButtonPressed(MEVENT me)
220 {
221 if (Playlists.hasCoords(me.x, me.y))
222 {
223 if (!isActiveWindow(Playlists))
224 {
225 if (previousColumnAvailable())
226 previousColumn();
227 else
228 return;
229 }
230 if (size_t(me.y) < Playlists.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
231 {
232 Playlists.Goto(me.y);
233 if (me.bstate & BUTTON3_PRESSED)
234 addItemToPlaylist(false);
235 }
236 else
237 Screen<WindowType>::mouseButtonPressed(me);
238 Content.clear();
239 }
240 else if (Content.hasCoords(me.x, me.y))
241 {
242 if (!isActiveWindow(Content))
243 {
244 if (nextColumnAvailable())
245 nextColumn();
246 else
247 return;
248 }
249 if (size_t(me.y) < Content.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
250 {
251 Content.Goto(me.y);
252 bool play = me.bstate & BUTTON3_PRESSED;
253 addItemToPlaylist(play);
254 }
255 else
256 Screen<WindowType>::mouseButtonPressed(me);
257 }
258 }
259
260 /***********************************************************************/
261
allowsSearching()262 bool PlaylistEditor::allowsSearching()
263 {
264 return true;
265 }
266
searchConstraint()267 const std::string &PlaylistEditor::searchConstraint()
268 {
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");
274 }
275
setSearchConstraint(const std::string & constraint)276 void PlaylistEditor::setSearchConstraint(const std::string &constraint)
277 {
278 if (isActiveWindow(Playlists))
279 {
280 m_playlists_search_predicate = Regex::Filter<MPD::Playlist>(
281 constraint,
282 Config.regex_type,
283 PlaylistEntryMatcher);
284 }
285 else if (isActiveWindow(Content))
286 {
287 m_content_search_predicate = Regex::Filter<MPD::Song>(
288 constraint,
289 Config.regex_type,
290 SongEntryMatcher);
291 }
292 }
293
clearSearchConstraint()294 void PlaylistEditor::clearSearchConstraint()
295 {
296 if (isActiveWindow(Playlists))
297 m_playlists_search_predicate.clear();
298 else if (isActiveWindow(Content))
299 m_content_search_predicate.clear();
300 }
301
search(SearchDirection direction,bool wrap,bool skip_current)302 bool PlaylistEditor::search(SearchDirection direction, bool wrap, bool skip_current)
303 {
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;
310 }
311
312 /***********************************************************************/
313
allowsFiltering()314 bool PlaylistEditor::allowsFiltering()
315 {
316 return allowsSearching();
317 }
318
currentFilter()319 std::string PlaylistEditor::currentFilter()
320 {
321 std::string result;
322 if (isActiveWindow(Playlists))
323 {
324 if (auto pred = Playlists.filterPredicate<Regex::Filter<MPD::Playlist>>())
325 result = pred->constraint();
326 }
327 else if (isActiveWindow(Content))
328 {
329 if (auto pred = Content.filterPredicate<Regex::Filter<MPD::Song>>())
330 result = pred->constraint();
331 }
332 return result;
333 }
334
applyFilter(const std::string & constraint)335 void PlaylistEditor::applyFilter(const std::string &constraint)
336 {
337 if (isActiveWindow(Playlists))
338 {
339 if (!constraint.empty())
340 {
341 Playlists.applyFilter(Regex::Filter<MPD::Playlist>(
342 constraint,
343 Config.regex_type,
344 PlaylistEntryMatcher));
345 }
346 else
347 Playlists.clearFilter();
348 }
349 else if (isActiveWindow(Content))
350 {
351 if (!constraint.empty())
352 {
353 Content.applyFilter(Regex::Filter<MPD::Song>(
354 constraint,
355 Config.regex_type,
356 SongEntryMatcher));
357 }
358 else
359 Content.clearFilter();
360 }
361 }
362
363
364 /***********************************************************************/
365
itemAvailable()366 bool PlaylistEditor::itemAvailable()
367 {
368 if (isActiveWindow(Playlists))
369 return !Playlists.empty();
370 if (isActiveWindow(Content))
371 return !Content.empty();
372 return false;
373 }
374
addItemToPlaylist(bool play)375 bool PlaylistEditor::addItemToPlaylist(bool play)
376 {
377 bool success = false;
378 if (isActiveWindow(Playlists))
379 {
380 const auto &playlist = Playlists.current()->value();
381 success = Mpd.LoadPlaylist(playlist.path());
382 if (play)
383 {
384 // Cheap trick that might fail in presence of multiple clients modifying the
385 // playlist at the same time, but oh well, this approach correctly loads cue
386 // playlists and is much faster in general as it doesn't require fetching
387 // song data.
388 try
389 {
390 Mpd.Play(Status::State::playlistLength());
391 }
392 catch (MPD::ServerError &e)
393 {
394 // If not bad index, rethrow.
395 if (e.code() != MPD_SERVER_ERROR_ARG)
396 throw;
397 }
398 }
399 if (success)
400 Statusbar::printf("Playlist \"%1%\" loaded", playlist.path());
401 }
402 else if (isActiveWindow(Content))
403 success = addSongToPlaylist(Content.current()->value(), play);
404 return success;
405 }
406
getSelectedSongs()407 std::vector<MPD::Song> PlaylistEditor::getSelectedSongs()
408 {
409 std::vector<MPD::Song> result;
410 if (isActiveWindow(Playlists))
411 {
412 bool any_selected = false;
413 for (auto &e : Playlists)
414 {
415 if (e.isSelected())
416 {
417 any_selected = true;
418 std::copy(
419 std::make_move_iterator(Mpd.GetPlaylistContent(e.value().path())),
420 std::make_move_iterator(MPD::SongIterator()),
421 std::back_inserter(result));
422 }
423 }
424 // if no item is selected, add songs from right column
425 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
426 if (!any_selected && !Playlists.empty())
427 std::copy(Content.beginV(), Content.endV(), std::back_inserter(result));
428 }
429 else if (isActiveWindow(Content))
430 result = Content.getSelectedSongs();
431 return result;
432 }
433
434 /***********************************************************************/
435
previousColumnAvailable()436 bool PlaylistEditor::previousColumnAvailable()
437 {
438 if (isActiveWindow(Content))
439 {
440 ScopedUnfilteredMenu<MPD::Playlist> sunfilter_playlists(ReapplyFilter::No, Playlists);
441 if (!Playlists.empty())
442 return true;
443 }
444 return false;
445 }
446
previousColumn()447 void PlaylistEditor::previousColumn()
448 {
449 if (isActiveWindow(Content))
450 {
451 setHighlightInactiveColumnFixes(Content);
452 w->refresh();
453 w = &Playlists;
454 setHighlightFixes(Playlists);
455 }
456 }
457
nextColumnAvailable()458 bool PlaylistEditor::nextColumnAvailable()
459 {
460 if (isActiveWindow(Playlists))
461 {
462 ScopedUnfilteredMenu<MPD::Song> sunfilter_content(ReapplyFilter::No, Content);
463 if (!Content.empty())
464 return true;
465 }
466 return false;
467 }
468
nextColumn()469 void PlaylistEditor::nextColumn()
470 {
471 if (isActiveWindow(Playlists))
472 {
473 setHighlightInactiveColumnFixes(Playlists);
474 w->refresh();
475 w = &Content;
476 setHighlightFixes(Content);
477 }
478 }
479
480 /***********************************************************************/
481
updateTimer()482 void PlaylistEditor::updateTimer()
483 {
484 m_timer = Global::Timer;
485 }
486
locatePlaylist(const MPD::Playlist & playlist)487 void PlaylistEditor::locatePlaylist(const MPD::Playlist &playlist)
488 {
489 update();
490 Playlists.clearFilter();
491 auto first = Playlists.beginV(), last = Playlists.endV();
492 auto it = std::find(first, last, playlist);
493 if (it != last)
494 {
495 Playlists.highlight(it - first);
496 Content.clear();
497 Content.clearFilter();
498 switchTo();
499 }
500 }
501
locateSong(const MPD::Song & s)502 void PlaylistEditor::locateSong(const MPD::Song &s)
503 {
504 if (Playlists.empty())
505 return;
506
507 Content.clearFilter();
508 Playlists.clearFilter();
509
510 auto locate_song_in_current_playlist = [this, &s](auto front, auto back) {
511 if (!Content.empty())
512 {
513 auto it = std::find(front, back, s);
514 if (it != back)
515 {
516 Content.highlight(it - Content.beginV());
517 nextColumn();
518 return true;
519 }
520 }
521 return false;
522 };
523 auto locate_song_in_playlists = [this, &s](auto front, auto back) {
524 for (auto it = front; it != back; ++it)
525 {
526 if (auto song_index = GetSongIndexInPlaylist(*it, s))
527 {
528 Playlists.highlight(it - Playlists.beginV());
529 Playlists.refresh();
530
531 requestContentUpdate();
532 update();
533 Content.highlight(*song_index);
534 nextColumn();
535
536 return true;
537 }
538 }
539 return false;
540 };
541
542
543 if (locate_song_in_current_playlist(Content.currentV() + 1, Content.endV()))
544 return;
545 Statusbar::print("Jumping to song...");
546 if (locate_song_in_playlists(Playlists.currentV() + 1, Playlists.endV()))
547 return;
548 if (locate_song_in_playlists(Playlists.beginV(), Playlists.currentV()))
549 return;
550 if (locate_song_in_current_playlist(Content.beginV(), Content.currentV()))
551 return;
552
553 // Highlighted song was skipped, so if that's the one we're looking for, we're
554 // good.
555 if (Content.empty() || *Content.currentV() != s)
556 Statusbar::print("Song was not found in playlists");
557 }
558
559 namespace {
560
SongToString(const MPD::Song & s)561 std::string SongToString(const MPD::Song &s)
562 {
563 std::string result;
564 switch (Config.playlist_display_mode)
565 {
566 case DisplayMode::Classic:
567 result = Format::stringify<char>(Config.song_list_format, &s);
568 break;
569 case DisplayMode::Columns:
570 result = Format::stringify<char>(Config.song_columns_mode_format, &s);
571 break;
572 }
573 return result;
574 }
575
PlaylistEntryMatcher(const Regex::Regex & rx,const MPD::Playlist & playlist)576 bool PlaylistEntryMatcher(const Regex::Regex &rx, const MPD::Playlist &playlist)
577 {
578 return Regex::search(playlist.path(), rx, Config.ignore_diacritics);
579 }
580
SongEntryMatcher(const Regex::Regex & rx,const MPD::Song & s)581 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::Song &s)
582 {
583 return Regex::search(SongToString(s), rx, Config.ignore_diacritics);
584 }
585
GetSongIndexInPlaylist(MPD::Playlist playlist,const MPD::Song & song)586 boost::optional<size_t> GetSongIndexInPlaylist(MPD::Playlist playlist, const MPD::Song &song)
587 {
588 size_t index = 0;
589 MPD::SongIterator it = Mpd.GetPlaylistContentNoInfo(playlist.path()), end;
590
591 for (;;)
592 {
593 if (it == end)
594 return boost::none;
595 if (*it == song)
596 return index;
597
598 ++it, ++index;
599 }
600 }
601
602 }
603