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