option parser: adjust functors so no object copies are made
[ncmpcpp.git] / src / search_engine.cpp
blob7325ed714d942f64e66793608cf3ecfca49a2e72
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 <array>
22 #include <boost/bind.hpp>
23 #include <iomanip>
25 #include "display.h"
26 #include "global.h"
27 #include "helpers.h"
28 #include "playlist.h"
29 #include "regex_filter.h"
30 #include "search_engine.h"
31 #include "settings.h"
32 #include "status.h"
33 #include "statusbar.h"
34 #include "utility/comparators.h"
35 #include "title.h"
36 #include "screen_switcher.h"
38 using Global::MainHeight;
39 using Global::MainStartY;
41 SearchEngine *mySearcher;
43 namespace {//
45 /*const std::array<const std::string, 11> constraintsNames = {{
46 "Any",
47 "Artist",
48 "Album Artist",
49 "Title",
50 "Album",
51 "Filename",
52 "Composer",
53 "Performer",
54 "Genre",
55 "Date",
56 "Comment"
57 }};
59 const std::array<const char *, 3> searchModes = {{
60 "Match if tag contains searched phrase (no regexes)",
61 "Match if tag contains searched phrase (regexes supported)",
62 "Match only if both values are the same"
63 }};
65 namespace pos {//
66 const size_t searchIn = constraintsNames.size()-1+1+1; // separated
67 const size_t searchMode = searchIn+1;
68 const size_t search = searchMode+1+1; // separated
69 const size_t reset = search+1;
70 }*/
72 std::string SEItemToString(const SEItem &ei);
73 bool SEItemEntryMatcher(const boost::regex &rx, const NC::Menu<SEItem>::Item &item, bool filter);
77 const char *SearchEngine::ConstraintsNames[] =
79 "Any",
80 "Artist",
81 "Album Artist",
82 "Title",
83 "Album",
84 "Filename",
85 "Composer",
86 "Performer",
87 "Genre",
88 "Date",
89 "Comment"
92 const char *SearchEngine::SearchModes[] =
94 "Match if tag contains searched phrase (no regexes)",
95 "Match if tag contains searched phrase (regexes supported)",
96 "Match only if both values are the same",
100 size_t SearchEngine::StaticOptions = 20;
101 size_t SearchEngine::ResetButton = 16;
102 size_t SearchEngine::SearchButton = 15;
104 SearchEngine::SearchEngine()
105 : Screen(NC::Menu<SEItem>(0, MainStartY, COLS, MainHeight, "", Config.main_color, NC::Border::None))
107 w.setHighlightColor(Config.main_highlight_color);
108 w.cyclicScrolling(Config.use_cyclic_scrolling);
109 w.centeredCursor(Config.centered_cursor);
110 w.setItemDisplayer(boost::bind(Display::SEItems, _1, proxySongList()));
111 w.setSelectedPrefix(Config.selected_item_prefix);
112 w.setSelectedSuffix(Config.selected_item_suffix);
113 SearchMode = &SearchModes[Config.search_engine_default_search_mode];
116 void SearchEngine::resize()
118 size_t x_offset, width;
119 getWindowResizeParams(x_offset, width);
120 w.resize(width, MainHeight);
121 w.moveTo(x_offset, MainStartY);
122 switch (Config.search_engine_display_mode)
124 case DisplayMode::Columns:
125 if (Config.titles_visibility)
127 w.setTitle(Display::Columns(w.getWidth()));
128 break;
130 case DisplayMode::Classic:
131 w.setTitle("");
133 hasToBeResized = 0;
136 void SearchEngine::switchTo()
138 SwitchTo::execute(this);
139 if (w.empty())
140 Prepare();
141 markSongsInPlaylist(proxySongList());
142 drawHeader();
145 std::wstring SearchEngine::title()
147 return L"Search engine";
150 void SearchEngine::enterPressed()
152 size_t option = w.choice();
153 if (option > ConstraintsNumber && option < SearchButton)
154 w.current().value().buffer().clear();
155 if (option < SearchButton)
156 Statusbar::lock();
158 if (option < ConstraintsNumber)
160 std::string constraint = ConstraintsNames[option];
161 Statusbar::put() << NC::Format::Bold << constraint << NC::Format::NoBold << ": ";
162 itsConstraints[option] = Global::wFooter->getString(itsConstraints[option]);
163 w.current().value().buffer().clear();
164 constraint.resize(13, ' ');
165 w.current().value().buffer() << NC::Format::Bold << constraint << NC::Format::NoBold << ": ";
166 ShowTag(w.current().value().buffer(), itsConstraints[option]);
168 else if (option == ConstraintsNumber+1)
170 Config.search_in_db = !Config.search_in_db;
171 w.current().value().buffer() << NC::Format::Bold << "Search in:" << NC::Format::NoBold << ' ' << (Config.search_in_db ? "Database" : "Current playlist");
173 else if (option == ConstraintsNumber+2)
175 if (!*++SearchMode)
176 SearchMode = &SearchModes[0];
177 w.current().value().buffer() << NC::Format::Bold << "Search mode:" << NC::Format::NoBold << ' ' << *SearchMode;
179 else if (option == SearchButton)
181 w.showAll();
182 Statusbar::print("Searching...");
183 if (w.size() > StaticOptions)
184 Prepare();
185 Search();
186 if (w.back().value().isSong())
188 if (Config.search_engine_display_mode == DisplayMode::Columns)
189 w.setTitle(Config.titles_visibility ? Display::Columns(w.getWidth()) : "");
190 size_t found = w.size()-SearchEngine::StaticOptions;
191 found += 3; // don't count options inserted below
192 w.insertSeparator(ResetButton+1);
193 w.insertItem(ResetButton+2, SEItem(), 1, 1);
194 w.at(ResetButton+2).value().mkBuffer() << Config.color1 << "Search results: " << Config.color2 << "Found " << found << (found > 1 ? " songs" : " song") << NC::Color::Default;
195 w.insertSeparator(ResetButton+3);
196 markSongsInPlaylist(proxySongList());
197 Statusbar::print("Searching finished");
198 if (Config.block_search_constraints_change)
199 for (size_t i = 0; i < StaticOptions-4; ++i)
200 w.at(i).setInactive(true);
201 w.scroll(NC::Scroll::Down);
202 w.scroll(NC::Scroll::Down);
204 else
205 Statusbar::print("No results found");
207 else if (option == ResetButton)
209 reset();
211 else
212 addSongToPlaylist(w.current().value().song(), true);
214 if (option < SearchButton)
215 Statusbar::unlock();
218 void SearchEngine::spacePressed()
220 if (!w.current().value().isSong())
221 return;
223 if (Config.space_selects)
225 w.current().setSelected(!w.current().isSelected());
226 w.scroll(NC::Scroll::Down);
227 return;
230 addSongToPlaylist(w.current().value().song(), false);
231 w.scroll(NC::Scroll::Down);
234 void SearchEngine::mouseButtonPressed(MEVENT me)
236 if (w.empty() || !w.hasCoords(me.x, me.y) || size_t(me.y) >= w.size())
237 return;
238 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
240 if (!w.Goto(me.y))
241 return;
242 w.refresh();
243 if ((me.bstate & BUTTON3_PRESSED || w.choice() > ConstraintsNumber) && w.choice() < StaticOptions)
244 enterPressed();
245 else if (w.choice() >= StaticOptions)
247 if (me.bstate & BUTTON1_PRESSED)
249 size_t pos = w.choice();
250 spacePressed();
251 if (pos < w.size()-1)
252 w.scroll(NC::Scroll::Up);
254 else
255 enterPressed();
258 else
259 Screen<WindowType>::mouseButtonPressed(me);
262 /***********************************************************************/
264 bool SearchEngine::allowsFiltering()
266 return w.back().value().isSong();
269 std::string SearchEngine::currentFilter()
271 return RegexItemFilter<SEItem>::currentFilter(w);
274 void SearchEngine::applyFilter(const std::string &filter)
276 if (filter.empty())
278 w.clearFilter();
279 w.clearFilterResults();
280 return;
284 w.showAll();
285 auto fun = boost::bind(SEItemEntryMatcher, _1, _2, true);
286 auto rx = RegexItemFilter<SEItem>(
287 boost::regex(filter, Config.regex_type), fun);
288 w.filter(w.begin(), w.end(), rx);
290 catch (boost::bad_expression &) { }
293 /***********************************************************************/
295 bool SearchEngine::allowsSearching()
297 return w.back().value().isSong();
300 bool SearchEngine::search(const std::string &constraint)
302 if (constraint.empty())
304 w.clearSearchResults();
305 return false;
309 auto fun = boost::bind(SEItemEntryMatcher, _1, _2, false);
310 auto rx = RegexItemFilter<SEItem>(
311 boost::regex(constraint, Config.regex_type), fun);
312 return w.search(w.begin(), w.end(), rx);
314 catch (boost::bad_expression &)
316 return false;
320 void SearchEngine::nextFound(bool wrap)
322 w.nextFound(wrap);
325 void SearchEngine::prevFound(bool wrap)
327 w.prevFound(wrap);
330 /***********************************************************************/
332 ProxySongList SearchEngine::proxySongList()
334 return ProxySongList(w, [](NC::Menu<SEItem>::Item &item) -> MPD::Song * {
335 MPD::Song *ptr = 0;
336 if (!item.isSeparator() && item.value().isSong())
337 ptr = &item.value().song();
338 return ptr;
342 bool SearchEngine::allowsSelection()
344 return w.current().value().isSong();
347 void SearchEngine::reverseSelection()
349 reverseSelectionHelper(w.begin()+std::min(StaticOptions, w.size()), w.end());
352 MPD::SongList SearchEngine::getSelectedSongs()
354 MPD::SongList result;
355 for (auto it = w.begin(); it != w.end(); ++it)
357 if (it->isSelected())
359 assert(it->value().isSong());
360 result.push_back(it->value().song());
363 // if no item is selected, add current one
364 if (result.empty() && !w.empty())
366 assert(w.current().value().isSong());
367 result.push_back(w.current().value().song());
369 return result;
372 /***********************************************************************/
374 void SearchEngine::Prepare()
376 w.setTitle("");
377 w.clear();
378 w.resizeList(StaticOptions-3);
380 w.at(ConstraintsNumber).setSeparator(true);
381 w.at(SearchButton-1).setSeparator(true);
383 for (size_t i = 0; i < ConstraintsNumber; ++i)
385 std::string constraint = ConstraintsNames[i];
386 constraint.resize(13, ' ');
387 w[i].value().mkBuffer() << NC::Format::Bold << constraint << NC::Format::NoBold << ": ";
388 ShowTag(w[i].value().buffer(), itsConstraints[i]);
391 w.at(ConstraintsNumber+1).value().mkBuffer() << NC::Format::Bold << "Search in:" << NC::Format::NoBold << ' ' << (Config.search_in_db ? "Database" : "Current playlist");
392 w.at(ConstraintsNumber+2).value().mkBuffer() << NC::Format::Bold << "Search mode:" << NC::Format::NoBold << ' ' << *SearchMode;
394 w.at(SearchButton).value().mkBuffer() << "Search";
395 w.at(ResetButton).value().mkBuffer() << "Reset";
398 void SearchEngine::reset()
400 for (size_t i = 0; i < ConstraintsNumber; ++i)
401 itsConstraints[i].clear();
402 w.reset();
403 Prepare();
404 Statusbar::print("Search state reset");
407 void SearchEngine::Search()
409 bool constraints_empty = 1;
410 for (size_t i = 0; i < ConstraintsNumber; ++i)
412 if (!itsConstraints[i].empty())
414 constraints_empty = 0;
415 break;
418 if (constraints_empty)
419 return;
421 if (Config.search_in_db && (SearchMode == &SearchModes[0] || SearchMode == &SearchModes[2])) // use built-in mpd searching
423 Mpd.StartSearch(SearchMode == &SearchModes[2]);
424 if (!itsConstraints[0].empty())
425 Mpd.AddSearchAny(itsConstraints[0]);
426 if (!itsConstraints[1].empty())
427 Mpd.AddSearch(MPD_TAG_ARTIST, itsConstraints[1]);
428 if (!itsConstraints[2].empty())
429 Mpd.AddSearch(MPD_TAG_ALBUM_ARTIST, itsConstraints[2]);
430 if (!itsConstraints[3].empty())
431 Mpd.AddSearch(MPD_TAG_TITLE, itsConstraints[3]);
432 if (!itsConstraints[4].empty())
433 Mpd.AddSearch(MPD_TAG_ALBUM, itsConstraints[4]);
434 if (!itsConstraints[5].empty())
435 Mpd.AddSearchURI(itsConstraints[5]);
436 if (!itsConstraints[6].empty())
437 Mpd.AddSearch(MPD_TAG_COMPOSER, itsConstraints[6]);
438 if (!itsConstraints[7].empty())
439 Mpd.AddSearch(MPD_TAG_PERFORMER, itsConstraints[7]);
440 if (!itsConstraints[8].empty())
441 Mpd.AddSearch(MPD_TAG_GENRE, itsConstraints[8]);
442 if (!itsConstraints[9].empty())
443 Mpd.AddSearch(MPD_TAG_DATE, itsConstraints[9]);
444 if (!itsConstraints[10].empty())
445 Mpd.AddSearch(MPD_TAG_COMMENT, itsConstraints[10]);
446 Mpd.CommitSearchSongs([this](MPD::Song s) {
447 w.addItem(s);
449 return;
452 MPD::SongList list;
453 if (Config.search_in_db)
454 Mpd.GetDirectoryRecursive("/", vectorMoveInserter(list));
455 else
456 list.insert(list.end(), myPlaylist->main().beginV(), myPlaylist->main().endV());
458 bool any_found = 1;
459 bool found = 1;
461 LocaleStringComparison cmp(std::locale(), Config.ignore_leading_the);
462 for (auto it = list.begin(); it != list.end(); ++it)
464 if (SearchMode != &SearchModes[2]) // match to pattern
466 boost::regex rx;
467 if (!itsConstraints[0].empty())
471 rx.assign(itsConstraints[0], Config.regex_type);
472 any_found =
473 boost::regex_search(it->getArtist(), rx)
474 || boost::regex_search(it->getAlbumArtist(), rx)
475 || boost::regex_search(it->getTitle(), rx)
476 || boost::regex_search(it->getAlbum(), rx)
477 || boost::regex_search(it->getName(), rx)
478 || boost::regex_search(it->getComposer(), rx)
479 || boost::regex_search(it->getPerformer(), rx)
480 || boost::regex_search(it->getGenre(), rx)
481 || boost::regex_search(it->getDate(), rx)
482 || boost::regex_search(it->getComment(), rx);
484 catch (boost::bad_expression &) { }
487 if (found && !itsConstraints[1].empty())
491 rx.assign(itsConstraints[1], Config.regex_type);
492 found = boost::regex_search(it->getArtist(), rx);
494 catch (boost::bad_expression &) { }
496 if (found && !itsConstraints[2].empty())
500 rx.assign(itsConstraints[2], Config.regex_type);
501 found = boost::regex_search(it->getAlbumArtist(), rx);
503 catch (boost::bad_expression &) { }
505 if (found && !itsConstraints[3].empty())
509 rx.assign(itsConstraints[3], Config.regex_type);
510 found = boost::regex_search(it->getTitle(), rx);
512 catch (boost::bad_expression &) { }
514 if (found && !itsConstraints[4].empty())
518 rx.assign(itsConstraints[4], Config.regex_type);
519 found = boost::regex_search(it->getAlbum(), rx);
521 catch (boost::bad_expression &) { }
523 if (found && !itsConstraints[5].empty())
527 rx.assign(itsConstraints[5], Config.regex_type);
528 found = boost::regex_search(it->getName(), rx);
530 catch (boost::bad_expression &) { }
532 if (found && !itsConstraints[6].empty())
536 rx.assign(itsConstraints[6], Config.regex_type);
537 found = boost::regex_search(it->getComposer(), rx);
539 catch (boost::bad_expression &) { }
541 if (found && !itsConstraints[7].empty())
545 rx.assign(itsConstraints[7], Config.regex_type);
546 found = boost::regex_search(it->getPerformer(), rx);
548 catch (boost::bad_expression &) { }
550 if (found && itsConstraints[8].empty())
554 rx.assign(itsConstraints[8], Config.regex_type);
555 found = boost::regex_search(it->getGenre(), rx);
557 catch (boost::bad_expression &) { }
559 if (found && !itsConstraints[9].empty())
563 rx.assign(itsConstraints[9], Config.regex_type);
564 found = boost::regex_search(it->getDate(), rx);
566 catch (boost::bad_expression &) { }
568 if (found && !itsConstraints[10].empty())
572 rx.assign(itsConstraints[10], Config.regex_type);
573 found = boost::regex_search(it->getComment(), rx);
575 catch (boost::bad_expression &) { }
578 else // match only if values are equal
580 if (!itsConstraints[0].empty())
581 any_found =
582 !cmp(it->getArtist(), itsConstraints[0])
583 || !cmp(it->getAlbumArtist(), itsConstraints[0])
584 || !cmp(it->getTitle(), itsConstraints[0])
585 || !cmp(it->getAlbum(), itsConstraints[0])
586 || !cmp(it->getName(), itsConstraints[0])
587 || !cmp(it->getComposer(), itsConstraints[0])
588 || !cmp(it->getPerformer(), itsConstraints[0])
589 || !cmp(it->getGenre(), itsConstraints[0])
590 || !cmp(it->getDate(), itsConstraints[0])
591 || !cmp(it->getComment(), itsConstraints[0]);
593 if (found && !itsConstraints[1].empty())
594 found = !cmp(it->getArtist(), itsConstraints[1]);
595 if (found && !itsConstraints[2].empty())
596 found = !cmp(it->getAlbumArtist(), itsConstraints[2]);
597 if (found && !itsConstraints[3].empty())
598 found = !cmp(it->getTitle(), itsConstraints[3]);
599 if (found && !itsConstraints[4].empty())
600 found = !cmp(it->getAlbum(), itsConstraints[4]);
601 if (found && !itsConstraints[5].empty())
602 found = !cmp(it->getName(), itsConstraints[5]);
603 if (found && !itsConstraints[6].empty())
604 found = !cmp(it->getComposer(), itsConstraints[6]);
605 if (found && !itsConstraints[7].empty())
606 found = !cmp(it->getPerformer(), itsConstraints[7]);
607 if (found && !itsConstraints[8].empty())
608 found = !cmp(it->getGenre(), itsConstraints[8]);
609 if (found && !itsConstraints[9].empty())
610 found = !cmp(it->getDate(), itsConstraints[9]);
611 if (found && !itsConstraints[10].empty())
612 found = !cmp(it->getComment(), itsConstraints[10]);
615 if (found && any_found)
616 w.addItem(*it);
617 found = 1;
618 any_found = 1;
622 namespace {//
624 std::string SEItemToString(const SEItem &ei)
626 std::string result;
627 if (ei.isSong())
629 switch (Config.search_engine_display_mode)
631 case DisplayMode::Classic:
632 result = ei.song().toString(Config.song_list_format_dollar_free, Config.tags_separator);
633 break;
634 case DisplayMode::Columns:
635 result = ei.song().toString(Config.song_in_columns_to_string_format, Config.tags_separator);
636 break;
639 else
640 result = ei.buffer().str();
641 return result;
644 bool SEItemEntryMatcher(const boost::regex &rx, const NC::Menu<SEItem>::Item &item, bool filter)
646 if (item.isSeparator() || !item.value().isSong())
647 return filter;
648 return boost::regex_search(SEItemToString(item.value()), rx);