change window timeout more transparently
[ncmpcpp.git] / src / browser.cpp
blobab4762670476cd0c8a643ceeb65e2a48c1560524
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 <boost/bind.hpp>
22 #include <boost/filesystem.hpp>
23 #include <boost/locale/conversion.hpp>
24 #include <algorithm>
26 #include "browser.h"
27 #include "charset.h"
28 #include "display.h"
29 #include "error.h"
30 #include "global.h"
31 #include "helpers.h"
32 #include "playlist.h"
33 #include "regex_filter.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 "utility/comparators.h"
42 #include "utility/string.h"
43 #include "configuration.h"
45 using Global::MainHeight;
46 using Global::MainStartY;
47 using Global::myScreen;
49 using MPD::itDirectory;
50 using MPD::itSong;
51 using MPD::itPlaylist;
53 namespace fs = boost::filesystem;
55 Browser *myBrowser;
57 namespace {//
59 std::set<std::string> SupportedExtensions;
60 bool hasSupportedExtension(const std::string &file);
62 std::string ItemToString(const MPD::Item &item);
63 bool BrowserEntryMatcher(const boost::regex &rx, const MPD::Item &item, bool filter);
67 Browser::Browser() : itsBrowseLocally(0), itsScrollBeginning(0), itsBrowsedDir("/")
69 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::None);
70 w.setHighlightColor(Config.main_highlight_color);
71 w.cyclicScrolling(Config.use_cyclic_scrolling);
72 w.centeredCursor(Config.centered_cursor);
73 w.setSelectedPrefix(Config.selected_item_prefix);
74 w.setSelectedSuffix(Config.selected_item_suffix);
75 w.setItemDisplayer(boost::bind(Display::Items, _1, proxySongList()));
78 void Browser::resize()
80 size_t x_offset, width;
81 getWindowResizeParams(x_offset, width);
82 w.resize(width, MainHeight);
83 w.moveTo(x_offset, MainStartY);
84 switch (Config.browser_display_mode)
86 case DisplayMode::Columns:
87 if (Config.titles_visibility)
89 w.setTitle(Display::Columns(w.getWidth()));
90 break;
92 case DisplayMode::Classic:
93 w.setTitle("");
94 break;
96 hasToBeResized = 0;
99 void Browser::switchTo()
101 SwitchTo::execute(this);
103 if (w.empty())
104 GetDirectory(itsBrowsedDir);
105 else
106 markSongsInPlaylist(proxySongList());
108 drawHeader();
111 std::wstring Browser::title()
113 std::wstring result = L"Browse: ";
114 result += Scroller(ToWString(itsBrowsedDir), itsScrollBeginning, COLS-result.length()-(Config.design == Design::Alternative ? 2 : Global::VolumeState.length()));
115 return result;
118 void Browser::enterPressed()
120 if (w.empty())
121 return;
123 const MPD::Item &item = w.current().value();
124 switch (item.type)
126 case itDirectory:
128 if (isParentDirectory(item))
129 GetDirectory(getParentDirectory(itsBrowsedDir), itsBrowsedDir);
130 else
131 GetDirectory(item.name, itsBrowsedDir);
132 drawHeader();
133 break;
135 case itSong:
137 addSongToPlaylist(*item.song, true, -1);
138 break;
140 case itPlaylist:
142 MPD::SongList list;
143 Mpd.GetPlaylistContentNoInfo(item.name, vectorMoveInserter(list));
144 bool success = addSongsToPlaylist(list.begin(), list.end(), true, -1);
145 Statusbar::printf("Playlist \"%1%\" loaded%2%",
146 item.name, withErrors(success)
152 void Browser::spacePressed()
154 if (w.empty())
155 return;
157 size_t i = itsBrowsedDir != "/" ? 1 : 0;
158 if (Config.space_selects && w.choice() >= i)
160 i = w.choice();
161 w.at(i).setSelected(!w.at(i).isSelected());
162 w.scroll(NC::Scroll::Down);
163 return;
166 const MPD::Item &item = w.current().value();
168 if (isParentDirectory(item))
169 return;
171 switch (item.type)
173 case itDirectory:
175 bool success;
176 # ifndef WIN32
177 if (isLocal())
179 MPD::SongList list;
180 MPD::ItemList items;
181 Statusbar::printf("Scanning directory \"%1%\"...", item.name);
182 myBrowser->GetLocalDirectory(items, item.name, 1);
183 list.reserve(items.size());
184 for (MPD::ItemList::const_iterator it = items.begin(); it != items.end(); ++it)
185 list.push_back(*it->song);
186 success = addSongsToPlaylist(list.begin(), list.end(), false, -1);
188 else
189 # endif // !WIN32
191 Mpd.Add(item.name);
192 success = true;
194 Statusbar::printf("Directory \"%1%\" added%2%",
195 item.name, withErrors(success)
197 break;
199 case itSong:
201 addSongToPlaylist(*item.song, false);
202 break;
204 case itPlaylist:
206 Mpd.LoadPlaylist(item.name);
207 Statusbar::printf("Playlist \"%1%\" loaded", item.name);
208 break;
211 w.scroll(NC::Scroll::Down);
214 void Browser::mouseButtonPressed(MEVENT me)
216 if (w.empty() || !w.hasCoords(me.x, me.y) || size_t(me.y) >= w.size())
217 return;
218 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
220 w.Goto(me.y);
221 switch (w.current().value().type)
223 case itDirectory:
224 if (me.bstate & BUTTON1_PRESSED)
226 GetDirectory(w.current().value().name);
227 drawHeader();
229 else
231 size_t pos = w.choice();
232 spacePressed();
233 if (pos < w.size()-1)
234 w.scroll(NC::Scroll::Up);
236 break;
237 case itPlaylist:
238 case itSong:
239 if (me.bstate & BUTTON1_PRESSED)
241 size_t pos = w.choice();
242 spacePressed();
243 if (pos < w.size()-1)
244 w.scroll(NC::Scroll::Up);
246 else
247 enterPressed();
248 break;
251 else
252 Screen<WindowType>::mouseButtonPressed(me);
255 /***********************************************************************/
257 bool Browser::allowsFiltering()
259 return true;
262 std::string Browser::currentFilter()
264 return RegexFilter<MPD::Item>::currentFilter(w);
267 void Browser::applyFilter(const std::string &filter)
269 if (filter.empty())
271 w.clearFilter();
272 w.clearFilterResults();
273 return;
277 w.showAll();
278 auto fun = boost::bind(BrowserEntryMatcher, _1, _2, true);
279 auto rx = RegexFilter<MPD::Item>(
280 boost::regex(filter, Config.regex_type), fun);
281 w.filter(w.begin(), w.end(), rx);
283 catch (boost::bad_expression &) { }
286 /***********************************************************************/
288 bool Browser::allowsSearching()
290 return true;
293 bool Browser::search(const std::string &constraint)
295 if (constraint.empty())
297 w.clearSearchResults();
298 return false;
302 auto fun = boost::bind(BrowserEntryMatcher, _1, _2, false);
303 auto rx = RegexFilter<MPD::Item>(
304 boost::regex(constraint, Config.regex_type), fun);
305 return w.search(w.begin(), w.end(), rx);
307 catch (boost::bad_expression &)
309 return false;
313 void Browser::nextFound(bool wrap)
315 w.nextFound(wrap);
318 void Browser::prevFound(bool wrap)
320 w.prevFound(wrap);
323 /***********************************************************************/
325 ProxySongList Browser::proxySongList()
327 return ProxySongList(w, [](NC::Menu<MPD::Item>::Item &item) -> MPD::Song * {
328 MPD::Song *ptr = 0;
329 if (item.value().type == itSong)
330 ptr = item.value().song.get();
331 return ptr;
335 bool Browser::allowsSelection()
337 return true;
340 void Browser::reverseSelection()
342 reverseSelectionHelper(w.begin()+(itsBrowsedDir == "/" ? 0 : 1), w.end());
345 MPD::SongList Browser::getSelectedSongs()
347 MPD::SongList result;
348 auto item_handler = [this, &result](const MPD::Item &item) {
349 if (item.type == itDirectory)
351 # ifndef WIN32
352 if (isLocal())
354 MPD::ItemList list;
355 GetLocalDirectory(list, item.name, true);
356 for (auto it = list.begin(); it != list.end(); ++it)
357 result.push_back(*it->song);
359 else
360 # endif // !WIN32
362 Mpd.GetDirectoryRecursive(item.name, vectorMoveInserter(result));
365 else if (item.type == itSong)
366 result.push_back(*item.song);
367 else if (item.type == itPlaylist)
369 Mpd.GetPlaylistContent(item.name, vectorMoveInserter(result));
372 for (auto it = w.begin(); it != w.end(); ++it)
373 if (it->isSelected())
374 item_handler(it->value());
375 // if no item is selected, add current one
376 if (result.empty() && !w.empty())
377 item_handler(w.current().value());
378 return result;
381 void Browser::fetchSupportedExtensions()
383 SupportedExtensions.clear();
384 Mpd.GetSupportedExtensions(SupportedExtensions);
387 void Browser::LocateSong(const MPD::Song &s)
389 if (s.getDirectory().empty())
390 return;
392 itsBrowseLocally = !s.isFromDatabase();
394 if (myScreen != this)
395 switchTo();
397 if (itsBrowsedDir != s.getDirectory())
398 GetDirectory(s.getDirectory());
399 for (size_t i = 0; i < w.size(); ++i)
401 if (w[i].value().type == itSong && s == *w[i].value().song)
403 w.highlight(i);
404 break;
407 drawHeader();
410 void Browser::GetDirectory(std::string dir, std::string subdir)
412 if (dir.empty())
413 dir = "/";
415 int highlightme = -1;
416 itsScrollBeginning = 0;
417 if (itsBrowsedDir != dir)
418 w.reset();
419 itsBrowsedDir = dir;
421 w.clear();
423 if (dir != "/")
425 MPD::Item parent;
426 parent.name = "..";
427 parent.type = itDirectory;
428 w.addItem(parent);
431 MPD::ItemList list;
432 # ifndef WIN32
433 if (isLocal())
434 GetLocalDirectory(list, itsBrowsedDir, false);
435 else
436 Mpd.GetDirectory(dir, vectorMoveInserter(list));
437 # else
438 list = Mpd.GetDirectory(dir);
439 # endif // !WIN32
440 if (Config.browser_sort_mode != SortMode::NoOp && !isLocal()) // local directory is already sorted
441 std::sort(list.begin(), list.end(),
442 LocaleBasedItemSorting(std::locale(), Config.ignore_leading_the, Config.browser_sort_mode)
445 for (MPD::ItemList::iterator it = list.begin(); it != list.end(); ++it)
447 switch (it->type)
449 case itPlaylist:
451 w.addItem(*it);
452 break;
454 case itDirectory:
456 if (it->name == subdir)
457 highlightme = w.size();
458 w.addItem(*it);
459 break;
461 case itSong:
463 w.addItem(*it, myPlaylist->checkForSong(*it->song));
464 break;
468 if (highlightme >= 0)
469 w.highlight(highlightme);
472 #ifndef WIN32
473 void Browser::GetLocalDirectory(MPD::ItemList &v, const std::string &directory, bool recursively) const
475 size_t start_size = v.size();
476 fs::path dir(directory);
477 std::for_each(fs::directory_iterator(dir), fs::directory_iterator(), [&](fs::directory_entry &e) {
478 if (!Config.local_browser_show_hidden_files && e.path().filename().native()[0] == '.')
479 return;
480 MPD::Item item;
481 if (fs::is_directory(e))
483 if (recursively)
485 GetLocalDirectory(v, e.path().native(), true);
486 start_size = v.size();
488 else
490 item.type = itDirectory;
491 item.name = e.path().native();
492 v.push_back(item);
495 else if (hasSupportedExtension(e.path().native()))
497 item.type = itSong;
498 mpd_pair file_pair = { "file", e.path().native().c_str() };
499 MPD::MutableSong *s = new MPD::MutableSong(mpd_song_begin(&file_pair));
500 item.song = std::shared_ptr<MPD::Song>(s);
501 # ifdef HAVE_TAGLIB_H
502 if (!recursively)
504 s->setMTime(fs::last_write_time(e.path()));
505 Tags::read(*s);
507 # endif // HAVE_TAGLIB_H
508 v.push_back(item);
512 if (Config.browser_sort_mode != SortMode::NoOp)
513 std::sort(v.begin()+start_size, v.end(),
514 LocaleBasedItemSorting(std::locale(), Config.ignore_leading_the, Config.browser_sort_mode)
518 void Browser::ClearDirectory(const std::string &path) const
520 fs::path dir(path);
521 std::for_each(fs::directory_iterator(dir), fs::directory_iterator(), [&](fs::directory_entry &e) {
522 if (!fs::is_symlink(e) && fs::is_directory(e))
523 ClearDirectory(e.path().native());
524 const char msg[] = "Deleting \"%1%\"...";
525 Statusbar::printf(msg, wideShorten(e.path().native(), COLS-const_strlen(msg)));
526 fs::remove(e.path());
530 void Browser::ChangeBrowseMode()
532 if (Mpd.GetHostname()[0] != '/')
534 Statusbar::print("For browsing local filesystem connection to MPD via UNIX Socket is required");
535 return;
538 itsBrowseLocally = !itsBrowseLocally;
539 Statusbar::printf("Browse mode: %1%",
540 itsBrowseLocally ? "local filesystem" : "MPD database"
542 if (itsBrowseLocally)
544 itsBrowsedDir = "~";
545 expand_home(itsBrowsedDir);
546 if (*itsBrowsedDir.rbegin() == '/')
547 itsBrowsedDir.resize(itsBrowsedDir.length()-1);
549 else
550 itsBrowsedDir = "/";
551 w.reset();
552 GetDirectory(itsBrowsedDir);
553 drawHeader();
556 bool Browser::deleteItem(const MPD::Item &item, std::string &errmsg)
558 if (!Config.allow_for_physical_item_deletion)
559 FatalError("Browser::deleteItem invoked with allow_for_physical_item_deletion = false");
560 if (isParentDirectory((item)))
561 FatalError("Parent directory passed to Browser::deleteItem");
563 // playlist created by mpd
564 if (!isLocal() && item.type == itPlaylist && CurrentDir() == "/")
565 Mpd.DeletePlaylist(item.name);
567 std::string path;
568 if (!isLocal())
569 path = Config.mpd_music_dir;
570 path += item.type == itSong ? item.song->getURI() : item.name;
572 bool rv;
575 if (item.type == itDirectory)
576 ClearDirectory(path);
577 if (!boost::filesystem::exists(path))
579 errmsg = "No such item: " + path;
580 rv = false;
582 else
584 boost::filesystem::remove(path);
585 rv = true;
588 catch (boost::filesystem::filesystem_error &err)
590 errmsg = err.what();
591 rv = false;
593 return rv;
595 #endif // !WIN32
597 namespace {//
599 bool hasSupportedExtension(const std::string &file)
601 size_t last_dot = file.rfind(".");
602 if (last_dot > file.length())
603 return false;
605 std::string ext = boost::locale::to_lower(file.substr(last_dot+1));
606 return SupportedExtensions.find(ext) != SupportedExtensions.end();
609 std::string ItemToString(const MPD::Item &item)
611 std::string result;
612 switch (item.type)
614 case MPD::itDirectory:
615 result = "[" + getBasename(item.name) + "]";
616 break;
617 case MPD::itSong:
618 switch (Config.browser_display_mode)
620 case DisplayMode::Classic:
621 result = item.song->toString(Config.song_list_format_dollar_free, Config.tags_separator);
622 break;
623 case DisplayMode::Columns:
624 result = item.song->toString(Config.song_in_columns_to_string_format, Config.tags_separator);
625 break;
627 break;
628 case MPD::itPlaylist:
629 result = Config.browser_playlist_prefix.str() + getBasename(item.name);
630 break;
632 return result;
635 bool BrowserEntryMatcher(const boost::regex &rx, const MPD::Item &item, bool filter)
637 if (Browser::isParentDirectory(item))
638 return filter;
639 return boost::regex_search(ItemToString(item), rx);