Fix warnings when compiling with GCC 7
[ncmpcpp.git] / src / screens / browser.cpp
blob5fd6d0cfa42ee291fe9e427a2541b208f4aca92b
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/algorithm/string/predicate.hpp>
23 #include <boost/filesystem.hpp>
24 #include <boost/locale/conversion.hpp>
25 #include <time.h>
27 #include "screens/browser.h"
28 #include "charset.h"
29 #include "display.h"
30 #include "global.h"
31 #include "helpers.h"
32 #include "screens/playlist.h"
33 #include "curses/menu_impl.h"
34 #include "screens/screen_switcher.h"
35 #include "settings.h"
36 #include "status.h"
37 #include "statusbar.h"
38 #include "screens/tag_editor.h"
39 #include "title.h"
40 #include "tags.h"
41 #include "format_impl.h"
42 #include "helpers/song_iterator_maker.h"
43 #include "utility/comparators.h"
44 #include "utility/string.h"
45 #include "configuration.h"
47 using Global::MainHeight;
48 using Global::MainStartY;
49 using Global::myScreen;
51 namespace fs = boost::filesystem;
52 namespace ph = std::placeholders;
54 Browser *myBrowser;
56 namespace {
58 std::set<std::string> lm_supported_extensions;
60 std::string realPath(bool local_browser, std::string path);
61 bool isStringParentDirectory(const std::string &directory);
62 bool isItemParentDirectory(const MPD::Item &item);
63 bool isRootDirectory(const std::string &directory);
64 bool isHidden(const fs::directory_iterator &entry);
65 bool hasSupportedExtension(const fs::directory_entry &entry);
66 MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags);
67 void getLocalDirectory(NC::Menu<MPD::Item> &menu, const std::string &directory);
68 void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs,
69 const std::string &directory);
70 void clearDirectory(const std::string &directory);
72 std::string itemToString(const MPD::Item &item);
73 bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter);
77 template <>
78 struct SongPropertiesExtractor<MPD::Item>
80 template <typename ItemT>
81 auto &operator()(ItemT &item) const
83 auto s = item.value().type() == MPD::Item::Type::Song
84 ? &item.value().song()
85 : nullptr;
86 m_cache.assign(&item.properties(), s);
87 return m_cache;
90 private:
91 mutable SongProperties m_cache;
94 SongIterator BrowserWindow::currentS()
96 return makeSongIterator(current());
99 ConstSongIterator BrowserWindow::currentS() const
101 return makeConstSongIterator(current());
104 SongIterator BrowserWindow::beginS()
106 return makeSongIterator(begin());
109 ConstSongIterator BrowserWindow::beginS() const
111 return makeConstSongIterator(begin());
114 SongIterator BrowserWindow::endS()
116 return makeSongIterator(end());
119 ConstSongIterator BrowserWindow::endS() const
121 return makeConstSongIterator(end());
124 std::vector<MPD::Song> BrowserWindow::getSelectedSongs()
126 return {}; // TODO
129 /**********************************************************************/
131 Browser::Browser()
132 : m_update_request(true)
133 , m_local_browser(false)
134 , m_scroll_beginning(0)
135 , m_current_directory("/")
137 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());
138 setHighlightFixes(w);
139 w.cyclicScrolling(Config.use_cyclic_scrolling);
140 w.centeredCursor(Config.centered_cursor);
141 w.setSelectedPrefix(Config.selected_item_prefix);
142 w.setSelectedSuffix(Config.selected_item_suffix);
143 w.setItemDisplayer(std::bind(Display::Items, ph::_1, std::cref(w)));
146 void Browser::resize()
148 size_t x_offset, width;
149 getWindowResizeParams(x_offset, width);
150 w.resize(width, MainHeight);
151 w.moveTo(x_offset, MainStartY);
152 switch (Config.browser_display_mode)
154 case DisplayMode::Columns:
155 if (Config.titles_visibility)
156 w.setTitle(Display::Columns(w.getWidth()));
157 break;
158 case DisplayMode::Classic:
159 w.setTitle("");
160 break;
162 hasToBeResized = 0;
165 void Browser::switchTo()
167 SwitchTo::execute(this);
168 drawHeader();
171 std::wstring Browser::title()
173 std::wstring result = L"Browse: ";
174 result += Scroller(ToWString(m_current_directory), m_scroll_beginning, COLS-result.length()-(Config.design == Design::Alternative ? 2 : Global::VolumeState.length()));
175 return result;
178 void Browser::update()
180 if (m_update_request)
182 m_update_request = false;
183 bool directory_changed = false;
188 getDirectory(m_current_directory);
189 w.refresh();
191 catch (MPD::ServerError &err)
193 // If current directory doesn't exist, try getting its
194 // parent until we either succeed or reach the root.
195 if (err.code() == MPD_SERVER_ERROR_NO_EXIST)
197 m_current_directory = getParentDirectory(m_current_directory);
198 directory_changed = true;
200 else
201 throw;
204 while (w.empty() && !inRootDirectory());
205 if (directory_changed)
206 drawHeader();
210 void Browser::mouseButtonPressed(MEVENT me)
212 if (w.empty() || !w.hasCoords(me.x, me.y) || size_t(me.y) >= w.size())
213 return;
214 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
216 w.Goto(me.y);
217 switch (w.current()->value().type())
219 case MPD::Item::Type::Directory:
220 if (me.bstate & BUTTON1_PRESSED)
221 enterDirectory();
222 else
223 addItemToPlaylist(false);
224 break;
225 case MPD::Item::Type::Playlist:
226 case MPD::Item::Type::Song:
228 bool play = me.bstate & BUTTON3_PRESSED;
229 addItemToPlaylist(play);
230 break;
234 else
235 Screen<WindowType>::mouseButtonPressed(me);
238 /***********************************************************************/
240 bool Browser::allowsSearching()
242 return true;
245 const std::string &Browser::searchConstraint()
247 return m_search_predicate.constraint();
250 void Browser::setSearchConstraint(const std::string &constraint)
252 m_search_predicate = Regex::Filter<MPD::Item>(
253 constraint,
254 Config.regex_type,
255 std::bind(browserEntryMatcher, ph::_1, ph::_2, false));
258 void Browser::clearSearchConstraint()
260 m_search_predicate.clear();
263 bool Browser::search(SearchDirection direction, bool wrap, bool skip_current)
265 return ::search(w, m_search_predicate, direction, wrap, skip_current);
268 /***********************************************************************/
270 bool Browser::allowsFiltering()
272 return allowsSearching();
275 std::string Browser::currentFilter()
277 std::string result;
278 if (auto pred = w.filterPredicate<Regex::Filter<MPD::Item>>())
279 result = pred->constraint();
280 return result;
283 void Browser::applyFilter(const std::string &constraint)
285 if (!constraint.empty())
287 w.applyFilter(Regex::Filter<MPD::Item>(
288 constraint,
289 Config.regex_type,
290 std::bind(browserEntryMatcher, ph::_1, ph::_2, true)));
292 else
293 w.clearFilter();
297 /***********************************************************************/
299 bool Browser::itemAvailable()
301 return !w.empty()
302 // ignore parent directory
303 && !isParentDirectory(w.current()->value());
306 bool Browser::addItemToPlaylist(bool play)
308 bool result = false;
310 auto tryToPlay = [] {
311 // Cheap trick that might fail in presence of multiple
312 // clients modifying the playlist at the same time, but
313 // oh well, this approach correctly loads cue playlists
314 // and is much faster in general as it doesn't require
315 // fetching song data.
318 Mpd.Play(Status::State::playlistLength());
320 catch (MPD::ServerError &e)
322 // If not bad index, rethrow.
323 if (e.code() != MPD_SERVER_ERROR_ARG)
324 throw;
328 const MPD::Item &item = w.current()->value();
329 switch (item.type())
331 case MPD::Item::Type::Directory:
333 if (m_local_browser)
335 std::vector<MPD::Song> songs;
336 getLocalDirectoryRecursively(songs, item.directory().path());
337 result = addSongsToPlaylist(songs.begin(), songs.end(), play, -1);
339 else
341 Mpd.Add(item.directory().path());
342 if (play)
343 tryToPlay();
344 result = true;
346 Statusbar::printf("Directory \"%1%\" added%2%",
347 item.directory().path(), withErrors(result));
348 break;
350 case MPD::Item::Type::Song:
351 result = addSongToPlaylist(item.song(), play);
352 break;
353 case MPD::Item::Type::Playlist:
354 Mpd.LoadPlaylist(item.playlist().path());
355 if (play)
356 tryToPlay();
357 Statusbar::printf("Playlist \"%1%\" loaded", item.playlist().path());
358 result = true;
359 break;
361 return result;
364 std::vector<MPD::Song> Browser::getSelectedSongs()
366 std::vector<MPD::Song> songs;
367 auto item_handler = [this, &songs](const MPD::Item &item) {
368 switch (item.type())
370 case MPD::Item::Type::Directory:
371 if (m_local_browser)
372 getLocalDirectoryRecursively(songs, item.directory().path());
373 else
375 std::copy(
376 std::make_move_iterator(Mpd.GetDirectoryRecursive(item.directory().path())),
377 std::make_move_iterator(MPD::SongIterator()),
378 std::back_inserter(songs)
381 break;
382 case MPD::Item::Type::Song:
383 songs.push_back(item.song());
384 break;
385 case MPD::Item::Type::Playlist:
386 std::copy(
387 std::make_move_iterator(Mpd.GetPlaylistContent(item.playlist().path())),
388 std::make_move_iterator(MPD::SongIterator()),
389 std::back_inserter(songs)
391 break;
394 for (const auto &item : w)
395 if (item.isSelected())
396 item_handler(item.value());
397 // if no item is selected, add current one
398 if (songs.empty() && !w.empty())
399 item_handler(w.current()->value());
400 return songs;
403 /***********************************************************************/
405 bool Browser::inRootDirectory()
407 return isRootDirectory(m_current_directory);
410 bool Browser::isParentDirectory(const MPD::Item &item)
412 return isItemParentDirectory(item);
415 const std::string& Browser::currentDirectory()
417 return m_current_directory;
420 void Browser::locateSong(const MPD::Song &s)
422 if (s.getDirectory().empty())
423 throw std::runtime_error("Song's directory is empty");
425 m_local_browser = !s.isFromDatabase();
427 if (myScreen != this)
428 switchTo();
430 w.clearFilter();
432 // change to relevant directory
433 if (m_current_directory != s.getDirectory())
435 getDirectory(s.getDirectory());
436 drawHeader();
439 // highlight the item
440 auto begin = w.beginV(), end = w.endV();
441 auto it = std::find(begin, end, MPD::Item(s));
442 if (it != end)
443 w.highlight(it-begin);
446 bool Browser::enterDirectory()
448 bool result = false;
449 if (!w.empty())
451 const auto &item = w.current()->value();
452 if (item.type() == MPD::Item::Type::Directory)
454 getDirectory(item.directory().path());
455 drawHeader();
456 result = true;
459 return result;
462 void Browser::getDirectory(std::string directory)
465 ScopedUnfilteredMenu<MPD::Item> sunfilter(ReapplyFilter::Yes, w);
467 m_scroll_beginning = 0;
468 w.clear();
470 // Reset the position if we change directories.
471 if (m_current_directory != directory)
472 w.reset();
474 // Check if it's a parent directory.
475 if (isStringParentDirectory(directory))
477 directory.resize(directory.length()-3);
478 directory = getParentDirectory(directory);
480 // When we go down to root, it can be empty.
481 if (directory.empty())
482 directory = "/";
484 bool is_root = isRootDirectory(directory);
485 // If the requested directory is not root, add parent directory.
486 if (!is_root)
488 // Make it so that display function doesn't have to handle special cases.
489 w.addItem(MPD::Directory(directory + "/.."), NC::List::Properties::None);
492 if (m_local_browser)
493 getLocalDirectory(w, directory);
494 else
496 MPD::ItemIterator end;
497 for (auto dir = Mpd.GetDirectory(directory); dir != end; ++dir)
498 w.addItem(std::move(*dir));
501 if (Config.browser_sort_mode != SortMode::NoOp)
503 std::sort(w.begin() + (is_root ? 0 : 1), w.end(),
504 LocaleBasedItemSorting(std::locale(), Config.ignore_leading_the, Config.browser_sort_mode));
508 for (size_t i = 0; i < w.size(); ++i)
510 if (w[i].value().type() == MPD::Item::Type::Directory
511 && w[i].value().directory().path() == m_current_directory)
513 w.highlight(i);
514 break;
517 m_current_directory = directory;
520 void Browser::changeBrowseMode()
522 if (Mpd.GetHostname()[0] != '/')
524 Statusbar::print("For browsing local filesystem connection to MPD via UNIX Socket is required");
525 return;
528 m_local_browser = !m_local_browser;
529 Statusbar::printf("Browse mode: %1%",
530 m_local_browser ? "local filesystem" : "MPD database"
532 if (m_local_browser)
534 m_current_directory = "~";
535 expand_home(m_current_directory);
537 else
538 m_current_directory = "/";
539 w.reset();
540 getDirectory(m_current_directory);
541 drawHeader();
544 void Browser::remove(const MPD::Item &item)
546 if (!Config.allow_for_physical_item_deletion)
547 throw std::runtime_error("physical deletion is forbidden");
548 if (isParentDirectory((item)))
549 throw std::runtime_error("deletion of parent directory is forbidden");
551 std::string path;
552 switch (item.type())
554 case MPD::Item::Type::Directory:
555 path = realPath(m_local_browser, item.directory().path());
556 clearDirectory(path);
557 fs::remove(path);
558 break;
559 case MPD::Item::Type::Song:
560 path = realPath(m_local_browser, item.song().getURI());
561 fs::remove(path);
562 break;
563 case MPD::Item::Type::Playlist:
564 path = item.playlist().path();
565 try {
566 Mpd.DeletePlaylist(path);
567 } catch (MPD::ServerError &e) {
568 // if there is no such mpd playlist, it's a local one
569 if (e.code() == MPD_SERVER_ERROR_NO_EXIST)
571 path = realPath(m_local_browser, std::move(path));
572 fs::remove(path);
574 else
575 throw;
577 break;
581 /***********************************************************************/
583 void Browser::fetchSupportedExtensions()
585 lm_supported_extensions.clear();
586 MPD::StringIterator extension = Mpd.GetSupportedExtensions(), end;
587 for (; extension != end; ++extension)
588 lm_supported_extensions.insert("." + std::move(*extension));
591 /***********************************************************************/
593 namespace {
595 std::string realPath(bool local_browser, std::string path)
597 if (!local_browser)
598 path = Config.mpd_music_dir + path;
599 return path;
602 bool isStringParentDirectory(const std::string &directory)
604 return boost::algorithm::ends_with(directory, "/..");
607 bool isItemParentDirectory(const MPD::Item &item)
609 return item.type() == MPD::Item::Type::Directory
610 && isStringParentDirectory(item.directory().path());
613 bool isRootDirectory(const std::string &directory)
615 return directory == "/";
618 bool isHidden(const fs::directory_iterator &entry)
620 return entry->path().filename().native()[0] == '.';
623 bool hasSupportedExtension(const fs::directory_entry &entry)
625 return lm_supported_extensions.find(entry.path().extension().native())
626 != lm_supported_extensions.end();
629 MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags)
631 mpd_pair pair = { "file", entry.path().c_str() };
632 mpd_song *s = mpd_song_begin(&pair);
633 if (s == nullptr)
634 throw std::runtime_error("invalid path: " + entry.path().native());
635 if (read_tags)
637 #ifdef HAVE_TAGLIB_H
638 Tags::setAttribute(s, "Last-Modified",
639 timeFormat("%Y-%m-%dT%H:%M:%SZ", fs::last_write_time(entry.path()))
641 // read tags
642 Tags::read(s);
643 #endif // HAVE_TAGLIB_H
645 return s;
648 void getLocalDirectory(NC::Menu<MPD::Item> &menu, const std::string &directory)
650 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
652 if (!Config.local_browser_show_hidden_files && isHidden(entry))
653 continue;
655 if (fs::is_directory(*entry))
657 menu.addItem(MPD::Directory(entry->path().native(),
658 fs::last_write_time(entry->path())));
660 else if (hasSupportedExtension(*entry))
661 menu.addItem(getLocalSong(*entry, true));
665 void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs, const std::string &directory)
667 size_t sort_offset = songs.size();
668 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
670 if (!Config.local_browser_show_hidden_files && isHidden(entry))
671 continue;
673 if (fs::is_directory(*entry))
675 getLocalDirectoryRecursively(songs, entry->path().native());
676 sort_offset = songs.size();
678 else if (hasSupportedExtension(*entry))
679 songs.push_back(getLocalSong(*entry, false));
682 if (Config.browser_sort_mode != SortMode::NoOp)
684 std::sort(songs.begin()+sort_offset, songs.end(),
685 LocaleBasedSorting(std::locale(), Config.ignore_leading_the)
690 void clearDirectory(const std::string &directory)
692 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
694 if (!fs::is_symlink(*entry) && fs::is_directory(*entry))
695 clearDirectory(entry->path().native());
696 const char msg[] = "Deleting \"%1%\"...";
697 Statusbar::printf(msg, wideShorten(entry->path().native(), COLS-const_strlen(msg)));
698 fs::remove(entry->path());
702 /***********************************************************************/
704 std::string itemToString(const MPD::Item &item)
706 std::string result;
707 switch (item.type())
709 case MPD::Item::Type::Directory:
710 result = "[" + getBasename(item.directory().path()) + "]";
711 break;
712 case MPD::Item::Type::Song:
713 switch (Config.browser_display_mode)
715 case DisplayMode::Classic:
716 result = Format::stringify<char>(Config.song_list_format, &item.song());
717 break;
718 case DisplayMode::Columns:
719 result = Format::stringify<char>(Config.song_columns_mode_format, &item.song());
720 break;
722 break;
723 case MPD::Item::Type::Playlist:
724 result = Config.browser_playlist_prefix.str();
725 result += getBasename(item.playlist().path());
726 break;
728 return result;
731 bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter)
733 if (isItemParentDirectory(item))
734 return filter;
735 return Regex::search(itemToString(item), rx, Config.ignore_diacritics);