Page: add `noexcept`
[ncmpc-debian.git] / src / SearchPage.cxx
1 /* ncmpc (Ncurses MPD Client)
2  * (c) 2004-2018 The Music Player Daemon Project
3  * Project homepage: http://musicpd.org
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 along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19
20 #include "SearchPage.hxx"
21 #include "PageMeta.hxx"
22 #include "screen_status.hxx"
23 #include "TextListRenderer.hxx"
24 #include "i18n.h"
25 #include "Options.hxx"
26 #include "Bindings.hxx"
27 #include "GlobalBindings.hxx"
28 #include "charset.hxx"
29 #include "mpdclient.hxx"
30 #include "strfsong.hxx"
31 #include "screen_utils.hxx"
32 #include "FileListPage.hxx"
33 #include "filelist.hxx"
34 #include "util/Macros.hxx"
35 #include "util/ScopeExit.hxx"
36
37 #include <glib.h>
38
39 #include <string.h>
40
41 enum {
42         SEARCH_URI = MPD_TAG_COUNT + 100,
43         SEARCH_ARTIST_TITLE
44 };
45
46 static constexpr struct {
47         enum mpd_tag_type tag_type;
48         const char *name;
49         const char *localname;
50 } search_tag[MPD_TAG_COUNT] = {
51         { MPD_TAG_ARTIST, "artist", N_("artist") },
52         { MPD_TAG_ALBUM, "album", N_("album") },
53         { MPD_TAG_TITLE, "title", N_("title") },
54         { MPD_TAG_TRACK, "track", N_("track") },
55         { MPD_TAG_NAME, "name", N_("name") },
56         { MPD_TAG_GENRE, "genre", N_("genre") },
57         { MPD_TAG_DATE, "date", N_("date") },
58         { MPD_TAG_COMPOSER, "composer", N_("composer") },
59         { MPD_TAG_PERFORMER, "performer", N_("performer") },
60         { MPD_TAG_COMMENT, "comment", N_("comment") },
61         { MPD_TAG_COUNT, nullptr, nullptr }
62 };
63
64 static int
65 search_get_tag_id(const char *name)
66 {
67         if (strcasecmp(name, "file") == 0 ||
68             strcasecmp(name, _("file")) == 0)
69                 return SEARCH_URI;
70
71         for (unsigned i = 0; search_tag[i].name != nullptr; ++i)
72                 if (strcasecmp(search_tag[i].name, name) == 0 ||
73                     strcasecmp(search_tag[i].localname, name) == 0)
74                         return search_tag[i].tag_type;
75
76         return -1;
77 }
78
79 typedef struct {
80         enum mpd_tag_type table;
81         const char *label;
82 } search_type_t;
83
84 static constexpr search_type_t mode[] = {
85         { MPD_TAG_TITLE, N_("Title") },
86         { MPD_TAG_ARTIST, N_("Artist") },
87         { MPD_TAG_ALBUM, N_("Album") },
88         { (enum mpd_tag_type)SEARCH_URI, N_("Filename") },
89         { (enum mpd_tag_type)SEARCH_ARTIST_TITLE, N_("Artist + Title") },
90         { MPD_TAG_COUNT, nullptr }
91 };
92
93 static const char *const help_text[] = {
94         "",
95         "",
96         "",
97         "Quick     -  Enter a string and ncmpc will search according",
98         "             to the current search mode (displayed above).",
99         "",
100         "Advanced  -  <tag>:<search term> [<tag>:<search term>...]",
101         "               Example: artist:radiohead album:pablo honey",
102         "",
103         "               Available tags: artist, album, title, track,",
104         "               name, genre, date composer, performer, comment, file",
105         "",
106 };
107
108 static bool advanced_search_mode = false;
109
110 class SearchPage final : public FileListPage {
111         History search_history;
112         std::string pattern;
113
114 public:
115         SearchPage(ScreenManager &_screen, WINDOW *_w, Size size)
116                 :FileListPage(_screen, _w, size,
117                               !options.search_format.empty()
118                               ? options.search_format.c_str()
119                               : options.list_format.c_str()) {
120                 lw.SetLength(ARRAY_SIZE(help_text));
121                 lw.hide_cursor = true;
122         }
123
124 private:
125         void Clear(bool clear_pattern);
126         void Reload(struct mpdclient &c);
127         void Start(struct mpdclient &c);
128
129 public:
130         /* virtual methods from class Page */
131         void Paint() const noexcept override;
132         void Update(struct mpdclient &c, unsigned events) noexcept override;
133         bool OnCommand(struct mpdclient &c, Command cmd) override;
134         const char *GetTitle(char *s, size_t size) const noexcept override;
135 };
136
137 /* search info */
138 class SearchHelpText final : public ListText {
139 public:
140         /* virtual methods from class ListText */
141         const char *GetListItemText(char *buffer, size_t size,
142                                     unsigned idx) const override {
143                 assert(idx < ARRAY_SIZE(help_text));
144
145                 if (idx == 0) {
146                         snprintf(buffer, size,
147                                  " %s : %s",
148                                  GetGlobalKeyBindings().GetKeyNames(Command::SCREEN_SEARCH,
149                                                                     true),
150                                  "New search");
151                         return buffer;
152                 }
153
154                 if (idx == 1) {
155                         snprintf(buffer, size,
156                                  " %s : %s [%s]",
157                                  GetGlobalKeyBindings().GetKeyNames(Command::SEARCH_MODE,
158                                                                     true),
159                                  get_key_description(Command::SEARCH_MODE),
160                                  gettext(mode[options.search_mode].label));
161                         return buffer;
162                 }
163
164                 return help_text[idx];
165         }
166 };
167
168 void
169 SearchPage::Clear(bool clear_pattern)
170 {
171         if (filelist) {
172                 delete filelist;
173                 filelist = new FileList();
174                 lw.SetLength(0);
175         }
176         if (clear_pattern)
177                 pattern.clear();
178
179         SetDirty();
180 }
181
182 static FileList *
183 search_simple_query(struct mpd_connection *connection, bool exact_match,
184                     int table, const char *local_pattern)
185 {
186         FileList *list;
187         const LocaleToUtf8 filter_utf8(local_pattern);
188
189         if (table == SEARCH_ARTIST_TITLE) {
190                 mpd_command_list_begin(connection, false);
191
192                 mpd_search_db_songs(connection, exact_match);
193                 mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
194                                               MPD_TAG_ARTIST,
195                                               filter_utf8.c_str());
196                 mpd_search_commit(connection);
197
198                 mpd_search_db_songs(connection, exact_match);
199                 mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
200                                               MPD_TAG_TITLE,
201                                               filter_utf8.c_str());
202                 mpd_search_commit(connection);
203
204                 mpd_command_list_end(connection);
205
206                 list = filelist_new_recv(connection);
207                 list->RemoveDuplicateSongs();
208         } else if (table == SEARCH_URI) {
209                 mpd_search_db_songs(connection, exact_match);
210                 mpd_search_add_uri_constraint(connection, MPD_OPERATOR_DEFAULT,
211                                               filter_utf8.c_str());
212                 mpd_search_commit(connection);
213
214                 list = filelist_new_recv(connection);
215         } else {
216                 mpd_search_db_songs(connection, exact_match);
217                 mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
218                                               (enum mpd_tag_type)table,
219                                               filter_utf8.c_str());
220                 mpd_search_commit(connection);
221
222                 list = filelist_new_recv(connection);
223         }
224
225         return list;
226 }
227
228 /*-----------------------------------------------------------------------
229  * NOTE: This code exists to test a new search ui,
230  *       Its ugly and MUST be redesigned before the next release!
231  *-----------------------------------------------------------------------
232  */
233 static FileList *
234 search_advanced_query(struct mpd_connection *connection, const char *query)
235 {
236         advanced_search_mode = false;
237         if (strchr(query, ':') == nullptr)
238                 return nullptr;
239
240         char *str = g_strdup(query);
241         AtScopeExit(str) { g_free(str); };
242
243         static constexpr size_t N = 10;
244
245         char *tabv[N];
246         char *matchv[N];
247         int table[N];
248
249         /*
250          * Replace every : with a '\0' and every space character
251          * before it unless spi = -1, link the resulting strings
252          * to their proper vector.
253          */
254         int spi = -1;
255         size_t n = 0;
256         for (size_t i = 0; str[i] != '\0' && n < N; i++) {
257                 switch(str[i]) {
258                 case ' ':
259                         spi = i;
260                         continue;
261                 case ':':
262                         str[i] = '\0';
263                         if (spi != -1)
264                                 str[spi] = '\0';
265
266                         matchv[n] = str + i + 1;
267                         tabv[n] = str + spi + 1;
268                         table[n] = search_get_tag_id(tabv[n]);
269                         if (table[n] < 0) {
270                                 screen_status_printf(_("Bad search tag %s"),
271                                                      tabv[n]);
272                                 return nullptr;
273                         }
274
275                         ++n;
276                         /* FALLTHROUGH */
277                 default:
278                         continue;
279                 }
280         }
281
282         /* Get rid of obvious failure case */
283         if (matchv[n - 1][0] == '\0') {
284                 screen_status_printf(_("No argument for search tag %s"), tabv[n - 1]);
285                 return nullptr;
286         }
287
288         advanced_search_mode = true;
289
290         /*-----------------------------------------------------------------------
291          * NOTE (again): This code exists to test a new search ui,
292          *               Its ugly and MUST be redesigned before the next release!
293          *             + the code below should live in mpdclient.c
294          *-----------------------------------------------------------------------
295          */
296         /** stupid - but this is just a test...... (fulhack)  */
297         mpd_search_db_songs(connection, false);
298
299         for (size_t i = 0; i < n; i++) {
300                 const LocaleToUtf8 value(matchv[i]);
301
302                 if (table[i] == SEARCH_URI)
303                         mpd_search_add_uri_constraint(connection,
304                                                       MPD_OPERATOR_DEFAULT,
305                                                       value.c_str());
306                 else
307                         mpd_search_add_tag_constraint(connection,
308                                                       MPD_OPERATOR_DEFAULT,
309                                                       (enum mpd_tag_type)table[i],
310                                                       value.c_str());
311         }
312
313         mpd_search_commit(connection);
314         auto *fl = filelist_new_recv(connection);
315         if (!mpd_response_finish(connection)) {
316                 delete fl;
317                 fl = nullptr;
318         }
319
320         return fl;
321 }
322
323 static FileList *
324 do_search(struct mpdclient *c, const char *query)
325 {
326         auto *connection = c->GetConnection();
327         if (connection == nullptr)
328                 return nullptr;
329
330         auto *fl = search_advanced_query(connection, query);
331         if (fl != nullptr)
332                 return fl;
333
334         if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) {
335                 c->HandleError();
336                 return nullptr;
337         }
338
339         fl = search_simple_query(connection, false,
340                                  mode[options.search_mode].table,
341                                  query);
342         if (fl == nullptr)
343                 c->HandleError();
344         return fl;
345 }
346
347 void
348 SearchPage::Reload(struct mpdclient &c)
349 {
350         if (pattern.empty())
351                 return;
352
353         lw.hide_cursor = false;
354         delete filelist;
355         filelist = do_search(&c, pattern.c_str());
356         if (filelist == nullptr)
357                 filelist = new FileList();
358         lw.SetLength(filelist->size());
359
360         screen_browser_sync_highlights(filelist, &c.playlist);
361
362         SetDirty();
363 }
364
365 void
366 SearchPage::Start(struct mpdclient &c)
367 {
368         if (!c.IsConnected())
369                 return;
370
371         Clear(true);
372
373         pattern = screen_readln(_("Search"),
374                                 nullptr,
375                                 &search_history,
376                                 nullptr);
377
378         if (pattern.empty()) {
379                 lw.Reset();
380                 return;
381         }
382
383         Reload(c);
384 }
385
386 static std::unique_ptr<Page>
387 screen_search_init(ScreenManager &_screen, WINDOW *w, Size size)
388 {
389         return std::make_unique<SearchPage>(_screen, w, size);
390 }
391
392 void
393 SearchPage::Paint() const noexcept
394 {
395         if (filelist) {
396                 FileListPage::Paint();
397         } else {
398                 lw.Paint(TextListRenderer(SearchHelpText()));
399         }
400 }
401
402 const char *
403 SearchPage::GetTitle(char *str, size_t size) const noexcept
404 {
405         if (advanced_search_mode && !pattern.empty())
406                 snprintf(str, size, "%s '%s'", _("Search"), pattern.c_str());
407         else if (!pattern.empty())
408                 snprintf(str, size,
409                          "%s '%s' [%s]",
410                          _("Search"),
411                          pattern.c_str(),
412                          gettext(mode[options.search_mode].label));
413         else
414                 return _("Search");
415
416         return str;
417 }
418
419 void
420 SearchPage::Update(struct mpdclient &c, unsigned events) noexcept
421 {
422         if (filelist != nullptr && events & MPD_IDLE_QUEUE) {
423                 screen_browser_sync_highlights(filelist, &c.playlist);
424                 SetDirty();
425         }
426 }
427
428 bool
429 SearchPage::OnCommand(struct mpdclient &c, Command cmd)
430 {
431         switch (cmd) {
432         case Command::SEARCH_MODE:
433                 options.search_mode++;
434                 if (mode[options.search_mode].label == nullptr)
435                         options.search_mode = 0;
436                 screen_status_printf(_("Search mode: %s"),
437                                      gettext(mode[options.search_mode].label));
438
439                 if (pattern.empty())
440                         /* show the new mode in the help text */
441                         SetDirty();
442                 else if (!advanced_search_mode)
443                         /* reload only if the new search mode is going
444                            to be considered */
445                         Reload(c);
446                 return true;
447
448         case Command::SCREEN_UPDATE:
449                 Reload(c);
450                 return true;
451
452         case Command::SCREEN_SEARCH:
453                 Start(c);
454                 return true;
455
456         case Command::CLEAR:
457                 Clear(true);
458                 lw.Reset();
459                 return true;
460
461         default:
462                 break;
463         }
464
465         if (FileListPage::OnCommand(c, cmd))
466                 return true;
467
468         return false;
469 }
470
471 const PageMeta screen_search = {
472         "search",
473         N_("Search"),
474         Command::SCREEN_SEARCH,
475         screen_search_init,
476 };