actions: use unique_ptr for storing actions
[ncmpcpp.git] / src / search_engine.cpp
blobee808ea9cba175f293824c45cd866f2bc093751a
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 <array>
22 #include <boost/range/detail/any_iterator.hpp>
23 #include <iomanip>
25 #include "display.h"
26 #include "global.h"
27 #include "helpers.h"
28 #include "menu_impl.h"
29 #include "playlist.h"
30 #include "search_engine.h"
31 #include "settings.h"
32 #include "status.h"
33 #include "statusbar.h"
34 #include "helpers/song_iterator_maker.h"
35 #include "utility/comparators.h"
36 #include "title.h"
37 #include "screen_switcher.h"
39 using Global::MainHeight;
40 using Global::MainStartY;
42 namespace ph = std::placeholders;
44 SearchEngine *mySearcher;
46 namespace {
48 /*const std::array<const std::string, 11> constraintsNames = {{
49 "Any",
50 "Artist",
51 "Album Artist",
52 "Title",
53 "Album",
54 "Filename",
55 "Composer",
56 "Performer",
57 "Genre",
58 "Date",
59 "Comment"
60 }};
62 const std::array<const char *, 3> searchModes = {{
63 "Match if tag contains searched phrase (no regexes)",
64 "Match if tag contains searched phrase (regexes supported)",
65 "Match only if both values are the same"
66 }};
68 namespace pos {
69 const size_t searchIn = constraintsNames.size()-1+1+1; // separated
70 const size_t searchMode = searchIn+1;
71 const size_t search = searchMode+1+1; // separated
72 const size_t reset = search+1;
73 }*/
75 std::string SEItemToString(const SEItem &ei);
76 bool SEItemEntryMatcher(const Regex::Regex &rx, const NC::Menu<SEItem>::Item &item, bool filter);
78 template <bool Const>
79 struct SongExtractor
81 typedef SongExtractor type;
83 typedef typename NC::Menu<SEItem>::Item MenuItem;
84 typedef typename std::conditional<Const, const MenuItem, MenuItem>::type Item;
85 typedef typename std::conditional<Const, const MPD::Song, MPD::Song>::type Song;
87 Song *operator()(Item &item) const
89 Song *ptr = nullptr;
90 if (!item.isSeparator() && item.value().isSong())
91 ptr = &item.value().song();
92 return ptr;
98 SongIterator SearchEngineWindow::currentS()
100 return makeSongIterator_<SEItem>(current(), SongExtractor<false>());
103 ConstSongIterator SearchEngineWindow::currentS() const
105 return makeConstSongIterator_<SEItem>(current(), SongExtractor<true>());
108 SongIterator SearchEngineWindow::beginS()
110 return makeSongIterator_<SEItem>(begin(), SongExtractor<false>());
113 ConstSongIterator SearchEngineWindow::beginS() const
115 return makeConstSongIterator_<SEItem>(begin(), SongExtractor<true>());
118 SongIterator SearchEngineWindow::endS()
120 return makeSongIterator_<SEItem>(end(), SongExtractor<false>());
123 ConstSongIterator SearchEngineWindow::endS() const
125 return makeConstSongIterator_<SEItem>(end(), SongExtractor<true>());
128 std::vector<MPD::Song> SearchEngineWindow::getSelectedSongs()
130 std::vector<MPD::Song> result;
131 for (auto &item : *this)
133 if (item.isSelected())
135 assert(item.value().isSong());
136 result.push_back(item.value().song());
139 // If no item is selected, add the current one if it's a song.
140 if (result.empty() && !empty() && current()->value().isSong())
141 result.push_back(current()->value().song());
142 return result;
145 /**********************************************************************/
147 const char *SearchEngine::ConstraintsNames[] =
149 "Any",
150 "Artist",
151 "Album Artist",
152 "Title",
153 "Album",
154 "Filename",
155 "Composer",
156 "Performer",
157 "Genre",
158 "Date",
159 "Comment"
162 const char *SearchEngine::SearchModes[] =
164 "Match if tag contains searched phrase (no regexes)",
165 "Match if tag contains searched phrase (regexes supported)",
166 "Match only if both values are the same",
170 size_t SearchEngine::StaticOptions = 20;
171 size_t SearchEngine::ResetButton = 16;
172 size_t SearchEngine::SearchButton = 15;
174 SearchEngine::SearchEngine()
175 : Screen(NC::Menu<SEItem>(0, MainStartY, COLS, MainHeight, "", Config.main_color, NC::Border()))
177 w.setHighlightColor(Config.main_highlight_color);
178 w.cyclicScrolling(Config.use_cyclic_scrolling);
179 w.centeredCursor(Config.centered_cursor);
180 w.setItemDisplayer(std::bind(Display::SEItems, ph::_1, std::cref(w)));
181 w.setSelectedPrefix(Config.selected_item_prefix);
182 w.setSelectedSuffix(Config.selected_item_suffix);
183 SearchMode = &SearchModes[Config.search_engine_default_search_mode];
186 void SearchEngine::resize()
188 size_t x_offset, width;
189 getWindowResizeParams(x_offset, width);
190 w.resize(width, MainHeight);
191 w.moveTo(x_offset, MainStartY);
192 switch (Config.search_engine_display_mode)
194 case DisplayMode::Columns:
195 if (Config.titles_visibility)
197 w.setTitle(Display::Columns(w.getWidth()));
198 break;
200 case DisplayMode::Classic:
201 w.setTitle("");
203 hasToBeResized = 0;
206 void SearchEngine::switchTo()
208 SwitchTo::execute(this);
209 if (w.empty())
210 Prepare();
211 markSongsInPlaylist(w);
212 drawHeader();
215 std::wstring SearchEngine::title()
217 return L"Search engine";
220 void SearchEngine::mouseButtonPressed(MEVENT me)
222 if (w.empty() || !w.hasCoords(me.x, me.y) || size_t(me.y) >= w.size())
223 return;
224 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
226 if (!w.Goto(me.y))
227 return;
228 w.refresh();
229 if ((me.bstate & BUTTON3_PRESSED)
230 && w.choice() < StaticOptions)
231 runAction();
232 else if (w.choice() >= StaticOptions)
234 bool play = me.bstate & BUTTON3_PRESSED;
235 addItemToPlaylist(play);
238 else
239 Screen<WindowType>::mouseButtonPressed(me);
242 /***********************************************************************/
244 bool SearchEngine::allowsSearching()
246 return w.rbegin()->value().isSong();
249 const std::string &SearchEngine::searchConstraint()
251 return m_search_predicate.constraint();
254 void SearchEngine::setSearchConstraint(const std::string &constraint)
256 m_search_predicate = Regex::ItemFilter<SEItem>(
257 constraint,
258 Config.regex_type,
259 std::bind(SEItemEntryMatcher, ph::_1, ph::_2, false));
262 void SearchEngine::clearSearchConstraint()
264 m_search_predicate.clear();
267 bool SearchEngine::search(SearchDirection direction, bool wrap, bool skip_current)
269 return ::search(w, m_search_predicate, direction, wrap, skip_current);
272 /***********************************************************************/
274 bool SearchEngine::allowsFiltering()
276 return allowsSearching();
279 std::string SearchEngine::currentFilter()
281 std::string result;
282 if (auto pred = w.filterPredicate<Regex::ItemFilter<SEItem>>())
283 result = pred->constraint();
284 return result;
287 void SearchEngine::applyFilter(const std::string &constraint)
289 if (!constraint.empty())
291 w.applyFilter(Regex::ItemFilter<SEItem>(
292 constraint,
293 Config.regex_type,
294 std::bind(SEItemEntryMatcher, ph::_1, ph::_2, true)));
296 else
297 w.clearFilter();
300 /***********************************************************************/
302 bool SearchEngine::actionRunnable()
304 return !w.empty() && !w.current()->value().isSong();
307 void SearchEngine::runAction()
309 size_t option = w.choice();
310 if (option > ConstraintsNumber && option < SearchButton)
311 w.current()->value().buffer().clear();
313 if (option < ConstraintsNumber)
315 Statusbar::ScopedLock slock;
316 std::string constraint = ConstraintsNames[option];
317 Statusbar::put() << NC::Format::Bold << constraint << NC::Format::NoBold << ": ";
318 itsConstraints[option] = Global::wFooter->prompt(itsConstraints[option]);
319 w.current()->value().buffer().clear();
320 constraint.resize(13, ' ');
321 w.current()->value().buffer() << NC::Format::Bold << constraint << NC::Format::NoBold << ": ";
322 ShowTag(w.current()->value().buffer(), itsConstraints[option]);
324 else if (option == ConstraintsNumber+1)
326 Config.search_in_db = !Config.search_in_db;
327 w.current()->value().buffer() << NC::Format::Bold << "Search in:" << NC::Format::NoBold << ' ' << (Config.search_in_db ? "Database" : "Current playlist");
329 else if (option == ConstraintsNumber+2)
331 if (!*++SearchMode)
332 SearchMode = &SearchModes[0];
333 w.current()->value().buffer() << NC::Format::Bold << "Search mode:" << NC::Format::NoBold << ' ' << *SearchMode;
335 else if (option == SearchButton)
337 w.clearFilter();
338 Statusbar::print("Searching...");
339 if (w.size() > StaticOptions)
340 Prepare();
341 Search();
342 if (w.rbegin()->value().isSong())
344 if (Config.search_engine_display_mode == DisplayMode::Columns)
345 w.setTitle(Config.titles_visibility ? Display::Columns(w.getWidth()) : "");
346 size_t found = w.size()-SearchEngine::StaticOptions;
347 found += 3; // don't count options inserted below
348 w.insertSeparator(ResetButton+1);
349 w.insertItem(ResetButton+2, SEItem(), NC::List::Properties::Bold | NC::List::Properties::Inactive);
350 w.at(ResetButton+2).value().mkBuffer() << Config.color1 << "Search results: " << Config.color2 << "Found " << found << (found > 1 ? " songs" : " song") << NC::Color::Default;
351 w.insertSeparator(ResetButton+3);
352 markSongsInPlaylist(w);
353 Statusbar::print("Searching finished");
354 if (Config.block_search_constraints_change)
355 for (size_t i = 0; i < StaticOptions-4; ++i)
356 w.at(i).setInactive(true);
357 w.scroll(NC::Scroll::Down);
358 w.scroll(NC::Scroll::Down);
360 else
361 Statusbar::print("No results found");
363 else if (option == ResetButton)
365 reset();
367 else
368 addSongToPlaylist(w.current()->value().song(), true);
371 /***********************************************************************/
373 bool SearchEngine::itemAvailable()
375 return !w.empty() && w.current()->value().isSong();
378 bool SearchEngine::addItemToPlaylist(bool play)
380 return addSongToPlaylist(w.current()->value().song(), play);
383 std::vector<MPD::Song> SearchEngine::getSelectedSongs()
385 return w.getSelectedSongs();
388 /***********************************************************************/
390 void SearchEngine::Prepare()
392 w.setTitle("");
393 w.clear();
394 w.resizeList(StaticOptions-3);
396 for (auto &item : w)
397 item.setSelectable(false);
399 w.at(ConstraintsNumber).setSeparator(true);
400 w.at(SearchButton-1).setSeparator(true);
402 for (size_t i = 0; i < ConstraintsNumber; ++i)
404 std::string constraint = ConstraintsNames[i];
405 constraint.resize(13, ' ');
406 w[i].value().mkBuffer() << NC::Format::Bold << constraint << NC::Format::NoBold << ": ";
407 ShowTag(w[i].value().buffer(), itsConstraints[i]);
410 w.at(ConstraintsNumber+1).value().mkBuffer() << NC::Format::Bold << "Search in:" << NC::Format::NoBold << ' ' << (Config.search_in_db ? "Database" : "Current playlist");
411 w.at(ConstraintsNumber+2).value().mkBuffer() << NC::Format::Bold << "Search mode:" << NC::Format::NoBold << ' ' << *SearchMode;
413 w.at(SearchButton).value().mkBuffer() << "Search";
414 w.at(ResetButton).value().mkBuffer() << "Reset";
417 void SearchEngine::reset()
419 for (size_t i = 0; i < ConstraintsNumber; ++i)
420 itsConstraints[i].clear();
421 w.reset();
422 Prepare();
423 Statusbar::print("Search state reset");
426 void SearchEngine::Search()
428 bool constraints_empty = 1;
429 for (size_t i = 0; i < ConstraintsNumber; ++i)
431 if (!itsConstraints[i].empty())
433 constraints_empty = 0;
434 break;
437 if (constraints_empty)
438 return;
440 if (Config.search_in_db && (SearchMode == &SearchModes[0] || SearchMode == &SearchModes[2])) // use built-in mpd searching
442 Mpd.StartSearch(SearchMode == &SearchModes[2]);
443 if (!itsConstraints[0].empty())
444 Mpd.AddSearchAny(itsConstraints[0]);
445 if (!itsConstraints[1].empty())
446 Mpd.AddSearch(MPD_TAG_ARTIST, itsConstraints[1]);
447 if (!itsConstraints[2].empty())
448 Mpd.AddSearch(MPD_TAG_ALBUM_ARTIST, itsConstraints[2]);
449 if (!itsConstraints[3].empty())
450 Mpd.AddSearch(MPD_TAG_TITLE, itsConstraints[3]);
451 if (!itsConstraints[4].empty())
452 Mpd.AddSearch(MPD_TAG_ALBUM, itsConstraints[4]);
453 if (!itsConstraints[5].empty())
454 Mpd.AddSearchURI(itsConstraints[5]);
455 if (!itsConstraints[6].empty())
456 Mpd.AddSearch(MPD_TAG_COMPOSER, itsConstraints[6]);
457 if (!itsConstraints[7].empty())
458 Mpd.AddSearch(MPD_TAG_PERFORMER, itsConstraints[7]);
459 if (!itsConstraints[8].empty())
460 Mpd.AddSearch(MPD_TAG_GENRE, itsConstraints[8]);
461 if (!itsConstraints[9].empty())
462 Mpd.AddSearch(MPD_TAG_DATE, itsConstraints[9]);
463 if (!itsConstraints[10].empty())
464 Mpd.AddSearch(MPD_TAG_COMMENT, itsConstraints[10]);
465 for (MPD::SongIterator s = Mpd.CommitSearchSongs(), end; s != end; ++s)
466 w.addItem(std::move(*s));
467 return;
470 Regex::Regex rx[ConstraintsNumber];
471 if (SearchMode != &SearchModes[2]) // match to pattern
473 for (size_t i = 0; i < ConstraintsNumber; ++i)
475 if (!itsConstraints[i].empty())
479 rx[i] = Regex::make(itsConstraints[i], Config.regex_type);
481 catch (boost::bad_expression &) { }
486 typedef boost::range_detail::any_iterator<
487 const MPD::Song,
488 boost::single_pass_traversal_tag,
489 const MPD::Song &,
490 std::ptrdiff_t
491 > input_song_iterator;
492 input_song_iterator s, end;
493 if (Config.search_in_db)
495 s = input_song_iterator(getDatabaseIterator(Mpd));
496 end = input_song_iterator(MPD::SongIterator());
498 else
500 s = input_song_iterator(myPlaylist->main().beginV());
501 end = input_song_iterator(myPlaylist->main().endV());
504 LocaleStringComparison cmp(std::locale(), Config.ignore_leading_the);
505 for (; s != end; ++s)
507 bool any_found = true, found = true;
509 if (SearchMode != &SearchModes[2]) // match to pattern
511 if (!rx[0].empty())
512 any_found =
513 Regex::search(s->getArtist(), rx[0])
514 || Regex::search(s->getAlbumArtist(), rx[0])
515 || Regex::search(s->getTitle(), rx[0])
516 || Regex::search(s->getAlbum(), rx[0])
517 || Regex::search(s->getName(), rx[0])
518 || Regex::search(s->getComposer(), rx[0])
519 || Regex::search(s->getPerformer(), rx[0])
520 || Regex::search(s->getGenre(), rx[0])
521 || Regex::search(s->getDate(), rx[0])
522 || Regex::search(s->getComment(), rx[0]);
523 if (found && !rx[1].empty())
524 found = Regex::search(s->getArtist(), rx[1]);
525 if (found && !rx[2].empty())
526 found = Regex::search(s->getAlbumArtist(), rx[2]);
527 if (found && !rx[3].empty())
528 found = Regex::search(s->getTitle(), rx[3]);
529 if (found && !rx[4].empty())
530 found = Regex::search(s->getAlbum(), rx[4]);
531 if (found && !rx[5].empty())
532 found = Regex::search(s->getName(), rx[5]);
533 if (found && !rx[6].empty())
534 found = Regex::search(s->getComposer(), rx[6]);
535 if (found && !rx[7].empty())
536 found = Regex::search(s->getPerformer(), rx[7]);
537 if (found && !rx[8].empty())
538 found = Regex::search(s->getGenre(), rx[8]);
539 if (found && !rx[9].empty())
540 found = Regex::search(s->getDate(), rx[9]);
541 if (found && !rx[10].empty())
542 found = Regex::search(s->getComment(), rx[10]);
544 else // match only if values are equal
546 if (!itsConstraints[0].empty())
547 any_found =
548 !cmp(s->getArtist(), itsConstraints[0])
549 || !cmp(s->getAlbumArtist(), itsConstraints[0])
550 || !cmp(s->getTitle(), itsConstraints[0])
551 || !cmp(s->getAlbum(), itsConstraints[0])
552 || !cmp(s->getName(), itsConstraints[0])
553 || !cmp(s->getComposer(), itsConstraints[0])
554 || !cmp(s->getPerformer(), itsConstraints[0])
555 || !cmp(s->getGenre(), itsConstraints[0])
556 || !cmp(s->getDate(), itsConstraints[0])
557 || !cmp(s->getComment(), itsConstraints[0]);
559 if (found && !itsConstraints[1].empty())
560 found = !cmp(s->getArtist(), itsConstraints[1]);
561 if (found && !itsConstraints[2].empty())
562 found = !cmp(s->getAlbumArtist(), itsConstraints[2]);
563 if (found && !itsConstraints[3].empty())
564 found = !cmp(s->getTitle(), itsConstraints[3]);
565 if (found && !itsConstraints[4].empty())
566 found = !cmp(s->getAlbum(), itsConstraints[4]);
567 if (found && !itsConstraints[5].empty())
568 found = !cmp(s->getName(), itsConstraints[5]);
569 if (found && !itsConstraints[6].empty())
570 found = !cmp(s->getComposer(), itsConstraints[6]);
571 if (found && !itsConstraints[7].empty())
572 found = !cmp(s->getPerformer(), itsConstraints[7]);
573 if (found && !itsConstraints[8].empty())
574 found = !cmp(s->getGenre(), itsConstraints[8]);
575 if (found && !itsConstraints[9].empty())
576 found = !cmp(s->getDate(), itsConstraints[9]);
577 if (found && !itsConstraints[10].empty())
578 found = !cmp(s->getComment(), itsConstraints[10]);
581 if (any_found && found)
582 w.addItem(*s);
586 namespace {
588 std::string SEItemToString(const SEItem &ei)
590 std::string result;
591 if (ei.isSong())
593 switch (Config.search_engine_display_mode)
595 case DisplayMode::Classic:
596 result = Format::stringify<char>(Config.song_list_format, &ei.song());
597 break;
598 case DisplayMode::Columns:
599 result = Format::stringify<char>(Config.song_columns_mode_format, &ei.song());
600 break;
603 else
604 result = ei.buffer().str();
605 return result;
608 bool SEItemEntryMatcher(const Regex::Regex &rx, const NC::Menu<SEItem>::Item &item, bool filter)
610 if (item.isSeparator() || !item.value().isSong())
611 return filter;
612 return Regex::search(SEItemToString(item.value()), rx);