actions: use unique_ptr for storing actions
[ncmpcpp.git] / src / browser.cpp
blob7544a6c893d41199dae3cf72dcd3b103710596ce
1 /***************************************************************************
2 * Copyright (C) 2008-2016 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/algorithm/string/predicate.hpp>
23 #include <boost/filesystem.hpp>
24 #include <boost/locale/conversion.hpp>
25 #include <time.h>
27 #include "browser.h"
28 #include "charset.h"
29 #include "display.h"
30 #include "global.h"
31 #include "helpers.h"
32 #include "playlist.h"
33 #include "menu_impl.h"
34 #include "screen_switcher.h"
35 #include "settings.h"
36 #include "status.h"
37 #include "statusbar.h"
38 #include "tag_editor.h"
39 #include "title.h"
40 #include "tags.h"
41 #include "helpers/song_iterator_maker.h"
42 #include "utility/comparators.h"
43 #include "utility/string.h"
44 #include "configuration.h"
46 using Global::MainHeight;
47 using Global::MainStartY;
48 using Global::myScreen;
50 namespace fs = boost::filesystem;
51 namespace ph = std::placeholders;
53 Browser *myBrowser;
55 namespace {
57 std::set<std::string> lm_supported_extensions;
59 std::string realPath(bool local_browser, std::string path);
60 bool isStringParentDirectory(const std::string &directory);
61 bool isItemParentDirectory(const MPD::Item &item);
62 bool isRootDirectory(const std::string &directory);
63 bool isHidden(const fs::directory_iterator &entry);
64 bool hasSupportedExtension(const fs::directory_entry &entry);
65 MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags);
66 void getLocalDirectory(std::vector<MPD::Item> &items, const std::string &directory);
67 void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs, const std::string &directory);
68 void clearDirectory(const std::string &directory);
70 std::string itemToString(const MPD::Item &item);
71 bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter);
73 template <bool Const>
74 struct SongExtractor
76 typedef SongExtractor type;
78 typedef typename NC::Menu<MPD::Item>::Item MenuItem;
79 typedef typename std::conditional<Const, const MenuItem, MenuItem>::type Item;
80 typedef typename std::conditional<Const, const MPD::Song, MPD::Song>::type Song;
82 Song *operator()(Item &item) const
84 Song *ptr = nullptr;
85 if (item.value().type() == MPD::Item::Type::Song)
86 ptr = const_cast<Song *>(&item.value().song());
87 return ptr;
93 SongIterator BrowserWindow::currentS()
95 return makeSongIterator_<MPD::Item>(current(), SongExtractor<false>());
98 ConstSongIterator BrowserWindow::currentS() const
100 return makeConstSongIterator_<MPD::Item>(current(), SongExtractor<true>());
103 SongIterator BrowserWindow::beginS()
105 return makeSongIterator_<MPD::Item>(begin(), SongExtractor<false>());
108 ConstSongIterator BrowserWindow::beginS() const
110 return makeConstSongIterator_<MPD::Item>(begin(), SongExtractor<true>());
113 SongIterator BrowserWindow::endS()
115 return makeSongIterator_<MPD::Item>(end(), SongExtractor<false>());
118 ConstSongIterator BrowserWindow::endS() const
120 return makeConstSongIterator_<MPD::Item>(end(), SongExtractor<true>());
123 std::vector<MPD::Song> BrowserWindow::getSelectedSongs()
125 return {}; // TODO
128 /**********************************************************************/
130 Browser::Browser()
131 : m_update_request(true)
132 , m_local_browser(false)
133 , m_scroll_beginning(0)
134 , m_current_directory("/")
136 w = NC::Menu<MPD::Item>(0, MainStartY, COLS, MainHeight, Config.browser_display_mode == DisplayMode::Columns && Config.titles_visibility ? Display::Columns(COLS) : "", Config.main_color, NC::Border());
137 w.setHighlightColor(Config.main_highlight_color);
138 w.cyclicScrolling(Config.use_cyclic_scrolling);
139 w.centeredCursor(Config.centered_cursor);
140 w.setSelectedPrefix(Config.selected_item_prefix);
141 w.setSelectedSuffix(Config.selected_item_suffix);
142 w.setItemDisplayer(std::bind(Display::Items, ph::_1, std::cref(w)));
145 void Browser::resize()
147 size_t x_offset, width;
148 getWindowResizeParams(x_offset, width);
149 w.resize(width, MainHeight);
150 w.moveTo(x_offset, MainStartY);
151 switch (Config.browser_display_mode)
153 case DisplayMode::Columns:
154 if (Config.titles_visibility)
156 w.setTitle(Display::Columns(w.getWidth()));
157 break;
159 case DisplayMode::Classic:
160 w.setTitle("");
161 break;
163 hasToBeResized = 0;
166 void Browser::switchTo()
168 SwitchTo::execute(this);
169 markSongsInPlaylist(w);
170 drawHeader();
173 std::wstring Browser::title()
175 std::wstring result = L"Browse: ";
176 result += Scroller(ToWString(m_current_directory), m_scroll_beginning, COLS-result.length()-(Config.design == Design::Alternative ? 2 : Global::VolumeState.length()));
177 return result;
180 void Browser::update()
182 if (m_update_request)
184 m_update_request = false;
185 bool directory_changed = false;
190 getDirectory(m_current_directory);
191 w.refresh();
193 catch (MPD::ServerError &err)
195 // If current directory doesn't exist, try getting its
196 // parent until we either succeed or reach the root.
197 if (err.code() == MPD_SERVER_ERROR_NO_EXIST)
199 m_current_directory = getParentDirectory(m_current_directory);
200 directory_changed = true;
202 else
203 throw;
206 while (w.empty() && !inRootDirectory());
207 if (directory_changed)
208 drawHeader();
212 void Browser::mouseButtonPressed(MEVENT me)
214 if (w.empty() || !w.hasCoords(me.x, me.y) || size_t(me.y) >= w.size())
215 return;
216 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
218 w.Goto(me.y);
219 switch (w.current()->value().type())
221 case MPD::Item::Type::Directory:
222 if (me.bstate & BUTTON1_PRESSED)
223 enterDirectory();
224 else
225 addItemToPlaylist(false);
226 break;
227 case MPD::Item::Type::Playlist:
228 case MPD::Item::Type::Song:
230 bool play = me.bstate & BUTTON3_PRESSED;
231 addItemToPlaylist(play);
232 break;
236 else
237 Screen<WindowType>::mouseButtonPressed(me);
240 /***********************************************************************/
242 bool Browser::allowsSearching()
244 return true;
247 const std::string &Browser::searchConstraint()
249 return m_search_predicate.constraint();
252 void Browser::setSearchConstraint(const std::string &constraint)
254 m_search_predicate = Regex::Filter<MPD::Item>(
255 constraint,
256 Config.regex_type,
257 std::bind(browserEntryMatcher, ph::_1, ph::_2, false));
260 void Browser::clearSearchConstraint()
262 m_search_predicate.clear();
265 bool Browser::search(SearchDirection direction, bool wrap, bool skip_current)
267 return ::search(w, m_search_predicate, direction, wrap, skip_current);
270 /***********************************************************************/
272 bool Browser::allowsFiltering()
274 return allowsSearching();
277 std::string Browser::currentFilter()
279 std::string result;
280 if (auto pred = w.filterPredicate<Regex::Filter<MPD::Item>>())
281 result = pred->constraint();
282 return result;
285 void Browser::applyFilter(const std::string &constraint)
287 if (!constraint.empty())
289 w.applyFilter(Regex::Filter<MPD::Item>(
290 constraint,
291 Config.regex_type,
292 std::bind(browserEntryMatcher, ph::_1, ph::_2, true)));
294 else
295 w.clearFilter();
299 /***********************************************************************/
301 bool Browser::itemAvailable()
303 return !w.empty()
304 // ignore parent directory
305 && !isParentDirectory(w.current()->value());
308 bool Browser::addItemToPlaylist(bool play)
310 bool result = false;
312 auto tryToPlay = [] {
313 // Cheap trick that might fail in presence of multiple
314 // clients modifying the playlist at the same time, but
315 // oh well, this approach correctly loads cue playlists
316 // and is much faster in general as it doesn't require
317 // fetching song data.
320 Mpd.Play(Status::State::playlistLength());
322 catch (MPD::ServerError &e)
324 // If not bad index, rethrow.
325 if (e.code() != MPD_SERVER_ERROR_ARG)
326 throw;
330 const MPD::Item &item = w.current()->value();
331 switch (item.type())
333 case MPD::Item::Type::Directory:
335 if (m_local_browser)
337 std::vector<MPD::Song> songs;
338 getLocalDirectoryRecursively(songs, item.directory().path());
339 result = addSongsToPlaylist(songs.begin(), songs.end(), play, -1);
341 else
343 Mpd.Add(item.directory().path());
344 if (play)
345 tryToPlay();
346 result = true;
348 Statusbar::printf("Directory \"%1%\" added%2%",
349 item.directory().path(), withErrors(result));
350 break;
352 case MPD::Item::Type::Song:
353 result = addSongToPlaylist(item.song(), play);
354 break;
355 case MPD::Item::Type::Playlist:
356 Mpd.LoadPlaylist(item.playlist().path());
357 if (play)
358 tryToPlay();
359 Statusbar::printf("Playlist \"%1%\" loaded", item.playlist().path());
360 result = true;
361 break;
363 return result;
366 std::vector<MPD::Song> Browser::getSelectedSongs()
368 std::vector<MPD::Song> songs;
369 auto item_handler = [this, &songs](const MPD::Item &item) {
370 switch (item.type())
372 case MPD::Item::Type::Directory:
373 if (m_local_browser)
374 getLocalDirectoryRecursively(songs, item.directory().path());
375 else
377 std::copy(
378 std::make_move_iterator(Mpd.GetDirectoryRecursive(item.directory().path())),
379 std::make_move_iterator(MPD::SongIterator()),
380 std::back_inserter(songs)
383 break;
384 case MPD::Item::Type::Song:
385 songs.push_back(item.song());
386 break;
387 case MPD::Item::Type::Playlist:
388 std::copy(
389 std::make_move_iterator(Mpd.GetPlaylistContent(item.playlist().path())),
390 std::make_move_iterator(MPD::SongIterator()),
391 std::back_inserter(songs)
393 break;
396 for (const auto &item : w)
397 if (item.isSelected())
398 item_handler(item.value());
399 // if no item is selected, add current one
400 if (songs.empty() && !w.empty())
401 item_handler(w.current()->value());
402 return songs;
405 /***********************************************************************/
407 bool Browser::inRootDirectory()
409 return isRootDirectory(m_current_directory);
412 bool Browser::isParentDirectory(const MPD::Item &item)
414 return isItemParentDirectory(item);
417 const std::string& Browser::currentDirectory()
419 return m_current_directory;
422 void Browser::locateSong(const MPD::Song &s)
424 if (s.getDirectory().empty())
425 throw std::runtime_error("Song's directory is empty");
427 m_local_browser = !s.isFromDatabase();
429 if (myScreen != this)
430 switchTo();
432 w.clearFilter();
434 // change to relevant directory
435 if (m_current_directory != s.getDirectory())
437 getDirectory(s.getDirectory());
438 drawHeader();
441 // highlight the item
442 auto begin = w.beginV(), end = w.endV();
443 auto it = std::find(begin, end, MPD::Item(s));
444 if (it != end)
445 w.highlight(it-begin);
448 bool Browser::enterDirectory()
450 bool result = false;
451 if (!w.empty())
453 const auto &item = w.current()->value();
454 if (item.type() == MPD::Item::Type::Directory)
456 getDirectory(item.directory().path());
457 drawHeader();
458 result = true;
461 return result;
464 void Browser::getDirectory(std::string directory)
466 ScopedUnfilteredMenu<MPD::Item> sunfilter(ReapplyFilter::Yes, w);
468 m_scroll_beginning = 0;
469 w.clear();
471 // reset the position if we change directories
472 if (m_current_directory != directory)
473 w.reset();
475 // check if it's a parent directory
476 if (isStringParentDirectory(directory))
478 directory.resize(directory.length()-3);
479 directory = getParentDirectory(directory);
481 // when we go down to root, it can be empty
482 if (directory.empty())
483 directory = "/";
485 std::vector<MPD::Item> items;
486 if (m_local_browser)
487 getLocalDirectory(items, directory);
488 else
490 std::copy(
491 std::make_move_iterator(Mpd.GetDirectory(directory)),
492 std::make_move_iterator(MPD::ItemIterator()),
493 std::back_inserter(items)
497 // sort items
498 if (Config.browser_sort_mode != SortMode::NoOp)
500 std::sort(items.begin(), items.end(),
501 LocaleBasedItemSorting(std::locale(), Config.ignore_leading_the, Config.browser_sort_mode)
505 // if the requested directory is not root, add parent directory
506 if (!isRootDirectory(directory))
508 // make it so that display function doesn't have to handle special cases
509 w.addItem(MPD::Directory(directory + "/.."), NC::List::Properties::None);
512 for (const auto &item : items)
514 switch (item.type())
516 case MPD::Item::Type::Playlist:
518 w.addItem(std::move(item));
519 break;
521 case MPD::Item::Type::Directory:
523 bool is_current = item.directory().path() == m_current_directory;
524 w.addItem(std::move(item));
525 if (is_current)
526 w.highlight(w.size()-1);
527 break;
529 case MPD::Item::Type::Song:
531 auto properties = NC::List::Properties::Selectable;
532 if (myPlaylist->checkForSong(item.song()))
533 properties |= NC::List::Properties::Bold;
534 w.addItem(std::move(item), properties);
535 break;
539 m_current_directory = directory;
542 void Browser::changeBrowseMode()
544 if (Mpd.GetHostname()[0] != '/')
546 Statusbar::print("For browsing local filesystem connection to MPD via UNIX Socket is required");
547 return;
550 m_local_browser = !m_local_browser;
551 Statusbar::printf("Browse mode: %1%",
552 m_local_browser ? "local filesystem" : "MPD database"
554 if (m_local_browser)
556 m_current_directory = "~";
557 expand_home(m_current_directory);
559 else
560 m_current_directory = "/";
561 w.reset();
562 getDirectory(m_current_directory);
563 drawHeader();
566 void Browser::remove(const MPD::Item &item)
568 if (!Config.allow_for_physical_item_deletion)
569 throw std::runtime_error("physical deletion is forbidden");
570 if (isParentDirectory((item)))
571 throw std::runtime_error("deletion of parent directory is forbidden");
573 std::string path;
574 switch (item.type())
576 case MPD::Item::Type::Directory:
577 path = realPath(m_local_browser, item.directory().path());
578 clearDirectory(path);
579 fs::remove(path);
580 break;
581 case MPD::Item::Type::Song:
582 path = realPath(m_local_browser, item.song().getURI());
583 fs::remove(path);
584 break;
585 case MPD::Item::Type::Playlist:
586 path = item.playlist().path();
587 try {
588 Mpd.DeletePlaylist(path);
589 } catch (MPD::ServerError &e) {
590 // if there is no such mpd playlist, it's a local one
591 if (e.code() == MPD_SERVER_ERROR_NO_EXIST)
593 path = realPath(m_local_browser, std::move(path));
594 fs::remove(path);
596 else
597 throw;
599 break;
603 /***********************************************************************/
605 void Browser::fetchSupportedExtensions()
607 lm_supported_extensions.clear();
608 MPD::StringIterator extension = Mpd.GetSupportedExtensions(), end;
609 for (; extension != end; ++extension)
610 lm_supported_extensions.insert("." + std::move(*extension));
613 /***********************************************************************/
615 namespace {
617 std::string realPath(bool local_browser, std::string path)
619 if (!local_browser)
620 path = Config.mpd_music_dir + path;
621 return path;
624 bool isStringParentDirectory(const std::string &directory)
626 return boost::algorithm::ends_with(directory, "/..");
629 bool isItemParentDirectory(const MPD::Item &item)
631 return item.type() == MPD::Item::Type::Directory
632 && isStringParentDirectory(item.directory().path());
635 bool isRootDirectory(const std::string &directory)
637 return directory == "/";
640 bool isHidden(const fs::directory_iterator &entry)
642 return entry->path().filename().native()[0] == '.';
645 bool hasSupportedExtension(const fs::directory_entry &entry)
647 return lm_supported_extensions.find(entry.path().extension().native())
648 != lm_supported_extensions.end();
651 MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags)
653 mpd_pair pair = { "file", entry.path().c_str() };
654 mpd_song *s = mpd_song_begin(&pair);
655 if (s == nullptr)
656 throw std::runtime_error("invalid path: " + entry.path().native());
657 if (read_tags)
659 #ifdef HAVE_TAGLIB_H
660 Tags::setAttribute(s, "Last-Modified",
661 timeFormat("%Y-%m-%dT%H:%M:%SZ", fs::last_write_time(entry.path()))
663 // read tags
664 Tags::read(s);
665 #endif // HAVE_TAGLIB_H
667 return s;
670 void getLocalDirectory(std::vector<MPD::Item> &items, const std::string &directory)
672 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
674 if (!Config.local_browser_show_hidden_files && isHidden(entry))
675 continue;
677 if (fs::is_directory(*entry))
679 items.push_back(MPD::Directory(
680 entry->path().native(),
681 fs::last_write_time(entry->path())
684 else if (hasSupportedExtension(*entry))
685 items.push_back(getLocalSong(*entry, true));
689 void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs, const std::string &directory)
691 size_t sort_offset = songs.size();
692 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
694 if (!Config.local_browser_show_hidden_files && isHidden(entry))
695 continue;
697 if (fs::is_directory(*entry))
699 getLocalDirectoryRecursively(songs, entry->path().native());
700 sort_offset = songs.size();
702 else if (hasSupportedExtension(*entry))
703 songs.push_back(getLocalSong(*entry, false));
706 if (Config.browser_sort_mode != SortMode::NoOp)
708 std::sort(songs.begin()+sort_offset, songs.end(),
709 LocaleBasedSorting(std::locale(), Config.ignore_leading_the)
714 void clearDirectory(const std::string &directory)
716 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
718 if (!fs::is_symlink(*entry) && fs::is_directory(*entry))
719 clearDirectory(entry->path().native());
720 const char msg[] = "Deleting \"%1%\"...";
721 Statusbar::printf(msg, wideShorten(entry->path().native(), COLS-const_strlen(msg)));
722 fs::remove(entry->path());
726 /***********************************************************************/
728 std::string itemToString(const MPD::Item &item)
730 std::string result;
731 switch (item.type())
733 case MPD::Item::Type::Directory:
734 result = "[" + getBasename(item.directory().path()) + "]";
735 break;
736 case MPD::Item::Type::Song:
737 switch (Config.browser_display_mode)
739 case DisplayMode::Classic:
740 result = Format::stringify<char>(Config.song_list_format, &item.song());
741 break;
742 case DisplayMode::Columns:
743 result = Format::stringify<char>(Config.song_columns_mode_format, &item.song());
744 break;
746 break;
747 case MPD::Item::Type::Playlist:
748 result = Config.browser_playlist_prefix.str();
749 result += getBasename(item.playlist().path());
750 break;
752 return result;
755 bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter)
757 if (isItemParentDirectory(item))
758 return filter;
759 return Regex::search(itemToString(item), rx);