1 /***************************************************************************
2 * Copyright (C) 2008-2017 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/range/detail/any_iterator.hpp>
25 #include "curses/menu_impl.h"
29 #include "screens/playlist.h"
30 #include "screens/search_engine.h"
33 #include "statusbar.h"
34 #include "format_impl.h"
35 #include "helpers/song_iterator_maker.h"
36 #include "utility/comparators.h"
38 #include "screens/screen_switcher.h"
40 using Global::MainHeight
;
41 using Global::MainStartY
;
43 namespace ph
= std::placeholders
;
45 SearchEngine
*mySearcher
;
49 /*const std::array<const std::string, 11> constraintsNames = {{
63 const std::array<const char *, 3> searchModes = {{
64 "Match if tag contains searched phrase (no regexes)",
65 "Match if tag contains searched phrase (regexes supported)",
66 "Match only if both values are the same"
70 const size_t searchIn = constraintsNames.size()-1+1+1; // separated
71 const size_t searchMode = searchIn+1;
72 const size_t search = searchMode+1+1; // separated
73 const size_t reset = search+1;
76 std::string
SEItemToString(const SEItem
&ei
);
77 bool SEItemEntryMatcher(const Regex::Regex
&rx
,
78 const NC::Menu
<SEItem
>::Item
&item
,
84 struct SongPropertiesExtractor
<SEItem
>
86 template <typename ItemT
>
87 auto &operator()(ItemT
&item
) const
89 auto s
= !item
.isSeparator() && item
.value().isSong()
90 ? &item
.value().song()
92 return m_cache
.assign(&item
.properties(), s
);
96 mutable SongProperties m_cache
;
99 SongIterator
SearchEngineWindow::currentS()
101 return makeSongIterator(current());
104 ConstSongIterator
SearchEngineWindow::currentS() const
106 return makeConstSongIterator(current());
109 SongIterator
SearchEngineWindow::beginS()
111 return makeSongIterator(begin());
114 ConstSongIterator
SearchEngineWindow::beginS() const
116 return makeConstSongIterator(begin());
119 SongIterator
SearchEngineWindow::endS()
121 return makeSongIterator(end());
124 ConstSongIterator
SearchEngineWindow::endS() const
126 return makeConstSongIterator(end());
129 std::vector
<MPD::Song
> SearchEngineWindow::getSelectedSongs()
131 std::vector
<MPD::Song
> result
;
132 for (auto &item
: *this)
134 if (item
.isSelected())
136 assert(item
.value().isSong());
137 result
.push_back(item
.value().song());
140 // If no item is selected, add the current one if it's a song.
141 if (result
.empty() && !empty() && current()->value().isSong())
142 result
.push_back(current()->value().song());
146 /**********************************************************************/
148 const char *SearchEngine::ConstraintsNames
[] =
163 const char *SearchEngine::SearchModes
[] =
165 "Match if tag contains searched phrase (no regexes)",
166 "Match if tag contains searched phrase (regexes supported)",
167 "Match only if both values are the same",
171 size_t SearchEngine::StaticOptions
= 20;
172 size_t SearchEngine::ResetButton
= 16;
173 size_t SearchEngine::SearchButton
= 15;
175 SearchEngine::SearchEngine()
176 : Screen(NC::Menu
<SEItem
>(0, MainStartY
, COLS
, MainHeight
, "", Config
.main_color
, NC::Border()))
178 setHighlightFixes(w
);
179 w
.cyclicScrolling(Config
.use_cyclic_scrolling
);
180 w
.centeredCursor(Config
.centered_cursor
);
181 w
.setItemDisplayer(std::bind(Display::SEItems
, ph::_1
, std::cref(w
)));
182 w
.setSelectedPrefix(Config
.selected_item_prefix
);
183 w
.setSelectedSuffix(Config
.selected_item_suffix
);
184 SearchMode
= &SearchModes
[Config
.search_engine_default_search_mode
];
187 void SearchEngine::resize()
189 size_t x_offset
, width
;
190 getWindowResizeParams(x_offset
, width
);
191 w
.resize(width
, MainHeight
);
192 w
.moveTo(x_offset
, MainStartY
);
193 switch (Config
.search_engine_display_mode
)
195 case DisplayMode::Columns
:
196 if (Config
.titles_visibility
)
197 w
.setTitle(Display::Columns(w
.getWidth()));
199 case DisplayMode::Classic
:
206 void SearchEngine::switchTo()
208 SwitchTo::execute(this);
214 std::wstring
SearchEngine::title()
216 return L
"Search engine";
219 void SearchEngine::mouseButtonPressed(MEVENT me
)
221 if (w
.empty() || !w
.hasCoords(me
.x
, me
.y
) || size_t(me
.y
) >= w
.size())
223 if (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
))
228 if ((me
.bstate
& BUTTON3_PRESSED
)
229 && w
.choice() < StaticOptions
)
231 else if (w
.choice() >= StaticOptions
)
233 bool play
= me
.bstate
& BUTTON3_PRESSED
;
234 addItemToPlaylist(play
);
238 Screen
<WindowType
>::mouseButtonPressed(me
);
241 /***********************************************************************/
243 bool SearchEngine::allowsSearching()
245 ScopedUnfilteredMenu
<SEItem
> sunfilter(ReapplyFilter::Yes
, w
);
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
>(
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()
282 if (auto pred
= w
.filterPredicate
<Regex::ItemFilter
<SEItem
>>())
283 result
= pred
->constraint();
287 void SearchEngine::applyFilter(const std::string
&constraint
)
289 if (!constraint
.empty())
291 w
.applyFilter(Regex::ItemFilter
<SEItem
>(
294 std::bind(SEItemEntryMatcher
, ph::_1
, ph::_2
, true)));
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)
332 SearchMode
= &SearchModes
[0];
333 w
.current()->value().buffer() << NC::Format::Bold
<< "Search mode:" << NC::Format::NoBold
<< ' ' << *SearchMode
;
335 else if (option
== SearchButton
)
338 Statusbar::print("Searching...");
339 if (w
.size() > StaticOptions
)
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::Inactive
);
350 w
.at(ResetButton
+2).value().mkBuffer()
353 << "Search results: "
354 << NC::FormattedColor::End
<>(Config
.color1
)
356 << "Found " << found
<< (found
> 1 ? " songs" : " song")
357 << NC::FormattedColor::End
<>(Config
.color2
)
358 << NC::Format::NoBold
;
359 w
.insertSeparator(ResetButton
+3);
360 Statusbar::print("Searching finished");
361 if (Config
.block_search_constraints_change
)
362 for (size_t i
= 0; i
< StaticOptions
-4; ++i
)
363 w
.at(i
).setInactive(true);
364 w
.scroll(NC::Scroll::Down
);
365 w
.scroll(NC::Scroll::Down
);
368 Statusbar::print("No results found");
370 else if (option
== ResetButton
)
375 addSongToPlaylist(w
.current()->value().song(), true);
378 /***********************************************************************/
380 bool SearchEngine::itemAvailable()
382 return !w
.empty() && w
.current()->value().isSong();
385 bool SearchEngine::addItemToPlaylist(bool play
)
387 return addSongToPlaylist(w
.current()->value().song(), play
);
390 std::vector
<MPD::Song
> SearchEngine::getSelectedSongs()
392 return w
.getSelectedSongs();
395 /***********************************************************************/
397 void SearchEngine::Prepare()
401 w
.resizeList(StaticOptions
-3);
404 item
.setSelectable(false);
406 w
.at(ConstraintsNumber
).setSeparator(true);
407 w
.at(SearchButton
-1).setSeparator(true);
409 for (size_t i
= 0; i
< ConstraintsNumber
; ++i
)
411 std::string constraint
= ConstraintsNames
[i
];
412 constraint
.resize(13, ' ');
413 w
[i
].value().mkBuffer() << NC::Format::Bold
<< constraint
<< NC::Format::NoBold
<< ": ";
414 ShowTag(w
[i
].value().buffer(), itsConstraints
[i
]);
417 w
.at(ConstraintsNumber
+1).value().mkBuffer() << NC::Format::Bold
<< "Search in:" << NC::Format::NoBold
<< ' ' << (Config
.search_in_db
? "Database" : "Current playlist");
418 w
.at(ConstraintsNumber
+2).value().mkBuffer() << NC::Format::Bold
<< "Search mode:" << NC::Format::NoBold
<< ' ' << *SearchMode
;
420 w
.at(SearchButton
).value().mkBuffer() << "Search";
421 w
.at(ResetButton
).value().mkBuffer() << "Reset";
424 void SearchEngine::reset()
426 for (size_t i
= 0; i
< ConstraintsNumber
; ++i
)
427 itsConstraints
[i
].clear();
431 Statusbar::print("Search state reset");
434 void SearchEngine::Search()
436 bool constraints_empty
= 1;
437 for (size_t i
= 0; i
< ConstraintsNumber
; ++i
)
439 if (!itsConstraints
[i
].empty())
441 constraints_empty
= 0;
445 if (constraints_empty
)
448 if (Config
.search_in_db
&& (SearchMode
== &SearchModes
[0] || SearchMode
== &SearchModes
[2])) // use built-in mpd searching
450 Mpd
.StartSearch(SearchMode
== &SearchModes
[2]);
451 if (!itsConstraints
[0].empty())
452 Mpd
.AddSearchAny(itsConstraints
[0]);
453 if (!itsConstraints
[1].empty())
454 Mpd
.AddSearch(MPD_TAG_ARTIST
, itsConstraints
[1]);
455 if (!itsConstraints
[2].empty())
456 Mpd
.AddSearch(MPD_TAG_ALBUM_ARTIST
, itsConstraints
[2]);
457 if (!itsConstraints
[3].empty())
458 Mpd
.AddSearch(MPD_TAG_TITLE
, itsConstraints
[3]);
459 if (!itsConstraints
[4].empty())
460 Mpd
.AddSearch(MPD_TAG_ALBUM
, itsConstraints
[4]);
461 if (!itsConstraints
[5].empty())
462 Mpd
.AddSearchURI(itsConstraints
[5]);
463 if (!itsConstraints
[6].empty())
464 Mpd
.AddSearch(MPD_TAG_COMPOSER
, itsConstraints
[6]);
465 if (!itsConstraints
[7].empty())
466 Mpd
.AddSearch(MPD_TAG_PERFORMER
, itsConstraints
[7]);
467 if (!itsConstraints
[8].empty())
468 Mpd
.AddSearch(MPD_TAG_GENRE
, itsConstraints
[8]);
469 if (!itsConstraints
[9].empty())
470 Mpd
.AddSearch(MPD_TAG_DATE
, itsConstraints
[9]);
471 if (!itsConstraints
[10].empty())
472 Mpd
.AddSearch(MPD_TAG_COMMENT
, itsConstraints
[10]);
473 for (MPD::SongIterator s
= Mpd
.CommitSearchSongs(), end
; s
!= end
; ++s
)
474 w
.addItem(std::move(*s
));
478 Regex::Regex rx
[ConstraintsNumber
];
479 if (SearchMode
!= &SearchModes
[2]) // match to pattern
481 for (size_t i
= 0; i
< ConstraintsNumber
; ++i
)
483 if (!itsConstraints
[i
].empty())
487 rx
[i
] = Regex::make(itsConstraints
[i
], Config
.regex_type
);
489 catch (boost::bad_expression
&) { }
494 typedef boost::range_detail::any_iterator
<
496 boost::single_pass_traversal_tag
,
499 > input_song_iterator
;
500 input_song_iterator s
, end
;
501 if (Config
.search_in_db
)
503 s
= input_song_iterator(getDatabaseIterator(Mpd
));
504 end
= input_song_iterator(MPD::SongIterator());
508 s
= input_song_iterator(myPlaylist
->main().beginV());
509 end
= input_song_iterator(myPlaylist
->main().endV());
512 LocaleStringComparison
cmp(std::locale(), Config
.ignore_leading_the
);
513 for (; s
!= end
; ++s
)
515 bool any_found
= true, found
= true;
517 if (SearchMode
!= &SearchModes
[2]) // match to pattern
521 Regex::search(s
->getArtist(), rx
[0], Config
.ignore_diacritics
)
522 || Regex::search(s
->getAlbumArtist(), rx
[0], Config
.ignore_diacritics
)
523 || Regex::search(s
->getTitle(), rx
[0], Config
.ignore_diacritics
)
524 || Regex::search(s
->getAlbum(), rx
[0], Config
.ignore_diacritics
)
525 || Regex::search(s
->getName(), rx
[0], Config
.ignore_diacritics
)
526 || Regex::search(s
->getComposer(), rx
[0], Config
.ignore_diacritics
)
527 || Regex::search(s
->getPerformer(), rx
[0], Config
.ignore_diacritics
)
528 || Regex::search(s
->getGenre(), rx
[0], Config
.ignore_diacritics
)
529 || Regex::search(s
->getDate(), rx
[0], Config
.ignore_diacritics
)
530 || Regex::search(s
->getComment(), rx
[0], Config
.ignore_diacritics
);
531 if (found
&& !rx
[1].empty())
532 found
= Regex::search(s
->getArtist(), rx
[1], Config
.ignore_diacritics
);
533 if (found
&& !rx
[2].empty())
534 found
= Regex::search(s
->getAlbumArtist(), rx
[2], Config
.ignore_diacritics
);
535 if (found
&& !rx
[3].empty())
536 found
= Regex::search(s
->getTitle(), rx
[3], Config
.ignore_diacritics
);
537 if (found
&& !rx
[4].empty())
538 found
= Regex::search(s
->getAlbum(), rx
[4], Config
.ignore_diacritics
);
539 if (found
&& !rx
[5].empty())
540 found
= Regex::search(s
->getName(), rx
[5], Config
.ignore_diacritics
);
541 if (found
&& !rx
[6].empty())
542 found
= Regex::search(s
->getComposer(), rx
[6], Config
.ignore_diacritics
);
543 if (found
&& !rx
[7].empty())
544 found
= Regex::search(s
->getPerformer(), rx
[7], Config
.ignore_diacritics
);
545 if (found
&& !rx
[8].empty())
546 found
= Regex::search(s
->getGenre(), rx
[8], Config
.ignore_diacritics
);
547 if (found
&& !rx
[9].empty())
548 found
= Regex::search(s
->getDate(), rx
[9], Config
.ignore_diacritics
);
549 if (found
&& !rx
[10].empty())
550 found
= Regex::search(s
->getComment(), rx
[10], Config
.ignore_diacritics
);
552 else // match only if values are equal
554 if (!itsConstraints
[0].empty())
556 !cmp(s
->getArtist(), itsConstraints
[0])
557 || !cmp(s
->getAlbumArtist(), itsConstraints
[0])
558 || !cmp(s
->getTitle(), itsConstraints
[0])
559 || !cmp(s
->getAlbum(), itsConstraints
[0])
560 || !cmp(s
->getName(), itsConstraints
[0])
561 || !cmp(s
->getComposer(), itsConstraints
[0])
562 || !cmp(s
->getPerformer(), itsConstraints
[0])
563 || !cmp(s
->getGenre(), itsConstraints
[0])
564 || !cmp(s
->getDate(), itsConstraints
[0])
565 || !cmp(s
->getComment(), itsConstraints
[0]);
567 if (found
&& !itsConstraints
[1].empty())
568 found
= !cmp(s
->getArtist(), itsConstraints
[1]);
569 if (found
&& !itsConstraints
[2].empty())
570 found
= !cmp(s
->getAlbumArtist(), itsConstraints
[2]);
571 if (found
&& !itsConstraints
[3].empty())
572 found
= !cmp(s
->getTitle(), itsConstraints
[3]);
573 if (found
&& !itsConstraints
[4].empty())
574 found
= !cmp(s
->getAlbum(), itsConstraints
[4]);
575 if (found
&& !itsConstraints
[5].empty())
576 found
= !cmp(s
->getName(), itsConstraints
[5]);
577 if (found
&& !itsConstraints
[6].empty())
578 found
= !cmp(s
->getComposer(), itsConstraints
[6]);
579 if (found
&& !itsConstraints
[7].empty())
580 found
= !cmp(s
->getPerformer(), itsConstraints
[7]);
581 if (found
&& !itsConstraints
[8].empty())
582 found
= !cmp(s
->getGenre(), itsConstraints
[8]);
583 if (found
&& !itsConstraints
[9].empty())
584 found
= !cmp(s
->getDate(), itsConstraints
[9]);
585 if (found
&& !itsConstraints
[10].empty())
586 found
= !cmp(s
->getComment(), itsConstraints
[10]);
589 if (any_found
&& found
)
596 std::string
SEItemToString(const SEItem
&ei
)
601 switch (Config
.search_engine_display_mode
)
603 case DisplayMode::Classic
:
604 result
= Format::stringify
<char>(Config
.song_list_format
, &ei
.song());
606 case DisplayMode::Columns
:
607 result
= Format::stringify
<char>(Config
.song_columns_mode_format
, &ei
.song());
612 result
= ei
.buffer().str();
616 bool SEItemEntryMatcher(const Regex::Regex
&rx
, const NC::Menu
<SEItem
>::Item
&item
, bool filter
)
618 if (item
.isSeparator() || !item
.value().isSong())
620 return Regex::search(SEItemToString(item
.value()), rx
, Config
.ignore_diacritics
);