1 /***************************************************************************
2 * Copyright (C) 2008-2014 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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. *
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. *
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 ***************************************************************************/
22 #include <boost/bind.hpp>
29 #include "regex_filter.h"
30 #include "search_engine.h"
33 #include "statusbar.h"
34 #include "utility/comparators.h"
36 #include "screen_switcher.h"
38 using Global::MainHeight
;
39 using Global::MainStartY
;
41 SearchEngine
*mySearcher
;
45 /*const std::array<const std::string, 11> constraintsNames = {{
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"
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;
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
[] =
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()));
130 case DisplayMode::Classic
:
136 void SearchEngine::switchTo()
138 SwitchTo::execute(this);
141 markSongsInPlaylist(proxySongList());
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
)
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)
176 SearchMode
= &SearchModes
[0];
177 w
.current().value().buffer() << NC::Format::Bold
<< "Search mode:" << NC::Format::NoBold
<< ' ' << *SearchMode
;
179 else if (option
== SearchButton
)
182 Statusbar::print("Searching...");
183 if (w
.size() > StaticOptions
)
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
);
205 Statusbar::print("No results found");
207 else if (option
== ResetButton
)
212 addSongToPlaylist(w
.current().value().song(), true);
214 if (option
< SearchButton
)
218 void SearchEngine::spacePressed()
220 if (!w
.current().value().isSong())
223 if (Config
.space_selects
)
225 w
.current().setSelected(!w
.current().isSelected());
226 w
.scroll(NC::Scroll::Down
);
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())
238 if (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
))
243 if ((me
.bstate
& BUTTON3_PRESSED
|| w
.choice() > ConstraintsNumber
) && w
.choice() < StaticOptions
)
245 else if (w
.choice() >= StaticOptions
)
247 if (me
.bstate
& BUTTON1_PRESSED
)
249 size_t pos
= w
.choice();
251 if (pos
< w
.size()-1)
252 w
.scroll(NC::Scroll::Up
);
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
)
279 w
.clearFilterResults();
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();
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
&)
320 void SearchEngine::nextFound(bool wrap
)
325 void SearchEngine::prevFound(bool wrap
)
330 /***********************************************************************/
332 ProxySongList
SearchEngine::proxySongList()
334 return ProxySongList(w
, [](NC::Menu
<SEItem
>::Item
&item
) -> MPD::Song
* {
336 if (!item
.isSeparator() && item
.value().isSong())
337 ptr
= &item
.value().song();
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());
372 /***********************************************************************/
374 void SearchEngine::Prepare()
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();
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;
418 if (constraints_empty
)
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
) {
453 if (Config
.search_in_db
)
454 Mpd
.GetDirectoryRecursive("/", vectorMoveInserter(list
));
456 list
.insert(list
.end(), myPlaylist
->main().beginV(), myPlaylist
->main().endV());
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
467 if (!itsConstraints
[0].empty())
471 rx
.assign(itsConstraints
[0], Config
.regex_type
);
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())
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
)
624 std::string
SEItemToString(const SEItem
&ei
)
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
);
634 case DisplayMode::Columns
:
635 result
= ei
.song().toString(Config
.song_in_columns_to_string_format
, Config
.tags_separator
);
640 result
= ei
.buffer().str();
644 bool SEItemEntryMatcher(const boost::regex
&rx
, const NC::Menu
<SEItem
>::Item
&item
, bool filter
)
646 if (item
.isSeparator() || !item
.value().isSong())
648 return boost::regex_search(SEItemToString(item
.value()), rx
);