3f1667cbf5c8108b6f758aebd83db9368a1026f4
[ncmpc-debian.git] / src / screen_browser.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 "config.h"
21 #include "screen_browser.hxx"
22 #include "screen_file.hxx"
23 #include "screen_song.hxx"
24 #include "screen_lyrics.hxx"
25 #include "screen_status.hxx"
26 #include "screen_find.hxx"
27 #include "screen.hxx"
28 #include "i18n.h"
29 #include "options.hxx"
30 #include "charset.hxx"
31 #include "strfsong.hxx"
32 #include "mpdclient.hxx"
33 #include "filelist.hxx"
34 #include "colors.hxx"
35 #include "paint.hxx"
36 #include "song_paint.hxx"
37
38 #include <mpd/client.h>
39
40 #include <string.h>
41
42 #define BUFSIZE 1024
43
44 #ifndef NCMPC_MINI
45 #define HIGHLIGHT  (0x01)
46 #endif
47
48 FileListPage::~FileListPage()
49 {
50         delete filelist;
51 }
52
53 #ifndef NCMPC_MINI
54
55 /* sync highlight flags with playlist */
56 void
57 screen_browser_sync_highlights(FileList *fl, const MpdQueue *playlist)
58 {
59         for (unsigned i = 0; i < fl->size(); ++i) {
60                 auto &entry = (*fl)[i];
61                 const auto *entity = entry.entity;
62
63                 if (entity != nullptr && mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG) {
64                         const auto *song = mpd_entity_get_song(entity);
65
66                         if (playlist->FindUri(*song) >= 0)
67                                 entry.flags |= HIGHLIGHT;
68                         else
69                                 entry.flags &= ~HIGHLIGHT;
70                 }
71         }
72 }
73
74 #endif
75
76 /* list_window callback */
77 static const char *
78 browser_lw_callback(unsigned idx, void *data)
79 {
80         const auto *fl = (const FileList *) data;
81         static char buf[BUFSIZE];
82
83         assert(fl != nullptr);
84         assert(idx < fl->size());
85
86         const auto &entry = (*fl)[idx];
87         const auto *entity = entry.entity;
88
89         if( entity == nullptr )
90                 return "..";
91
92         if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_DIRECTORY) {
93                 const auto *dir = mpd_entity_get_directory(entity);
94                 const char *name = g_basename(mpd_directory_get_path(dir));
95                 g_strlcpy(buf, Utf8ToLocale(name).c_str(), sizeof(buf));
96                 return buf;
97         } else if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG) {
98                 const auto *song = mpd_entity_get_song(entity);
99
100                 strfsong(buf, BUFSIZE, options.list_format, song);
101                 return buf;
102         } else if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_PLAYLIST) {
103                 const auto *playlist = mpd_entity_get_playlist(entity);
104                 const char *name = g_basename(mpd_playlist_get_path(playlist));
105                 g_strlcpy(buf, Utf8ToLocale(name).c_str(), sizeof(buf));
106                 return buf;
107         }
108
109         return "Error: Unknown entry!";
110 }
111
112 static bool
113 load_playlist(struct mpdclient *c, const struct mpd_playlist *playlist)
114 {
115         auto *connection = mpdclient_get_connection(c);
116
117         if (connection == nullptr)
118                 return false;
119
120         if (mpd_run_load(connection, mpd_playlist_get_path(playlist))) {
121                 const char *name = g_basename(mpd_playlist_get_path(playlist));
122                 screen_status_printf(_("Loading playlist %s..."),
123                                      Utf8ToLocale(name).c_str());
124
125                 c->events |= MPD_IDLE_QUEUE;
126         } else
127                 mpdclient_handle_error(c);
128
129         return true;
130 }
131
132 static bool
133 enqueue_and_play(struct mpdclient *c, FileListEntry *entry)
134 {
135         auto *connection = mpdclient_get_connection(c);
136         if (connection == nullptr)
137                 return false;
138
139         const auto *song = mpd_entity_get_song(entry->entity);
140         int id;
141
142 #ifndef NCMPC_MINI
143         if (!(entry->flags & HIGHLIGHT))
144                 id = -1;
145         else
146 #endif
147                 id = c->playlist.FindUri(*song);
148
149         if (id < 0) {
150                 char buf[BUFSIZE];
151
152                 id = mpd_run_add_id(connection, mpd_song_get_uri(song));
153                 if (id < 0) {
154                         mpdclient_handle_error(c);
155                         return false;
156                 }
157
158 #ifndef NCMPC_MINI
159                 entry->flags |= HIGHLIGHT;
160 #endif
161                 strfsong(buf, BUFSIZE, options.list_format, song);
162                 screen_status_printf(_("Adding \'%s\' to queue"), buf);
163         }
164
165         if (!mpd_run_play_id(connection, id)) {
166                 mpdclient_handle_error(c);
167                 return false;
168         }
169
170         return true;
171 }
172
173 FileListEntry *
174 FileListPage::GetSelectedEntry() const
175 {
176         const auto range = lw.GetRange();
177
178         if (filelist == nullptr ||
179             range.empty() ||
180             range.end_index > range.start_index + 1 ||
181             range.start_index >= filelist->size())
182                 return nullptr;
183
184         return &(*filelist)[range.start_index];
185 }
186
187 const struct mpd_entity *
188 FileListPage::GetSelectedEntity() const
189 {
190         const auto *entry = GetSelectedEntry();
191
192         return entry != nullptr
193                 ? entry->entity
194                 : nullptr;
195 }
196
197 const struct mpd_song *
198 FileListPage::GetSelectedSong() const
199 {
200         const auto *entity = GetSelectedEntity();
201
202         return entity != nullptr &&
203                 mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG
204                 ? mpd_entity_get_song(entity)
205                 : nullptr;
206 }
207
208 FileListEntry *
209 FileListPage::GetIndex(unsigned i) const
210 {
211         if (filelist == nullptr || i >= filelist->size())
212                 return nullptr;
213
214         return &(*filelist)[i];
215 }
216
217 bool
218 FileListPage::HandleEnter(struct mpdclient &c)
219 {
220         auto *entry = GetSelectedEntry();
221         if (entry == nullptr)
222                 return false;
223
224         auto *entity = entry->entity;
225         if (entity == nullptr)
226                 return false;
227
228         if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_PLAYLIST)
229                 return load_playlist(&c, mpd_entity_get_playlist(entity));
230         else if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG)
231                 return enqueue_and_play(&c, entry);
232         return false;
233 }
234
235 static bool
236 browser_select_entry(struct mpdclient *c, FileListEntry *entry,
237                      gcc_unused bool toggle)
238 {
239         assert(entry != nullptr);
240         assert(entry->entity != nullptr);
241
242         if (mpd_entity_get_type(entry->entity) == MPD_ENTITY_TYPE_PLAYLIST)
243                 return load_playlist(c, mpd_entity_get_playlist(entry->entity));
244
245         if (mpd_entity_get_type(entry->entity) == MPD_ENTITY_TYPE_DIRECTORY) {
246                 const auto *dir = mpd_entity_get_directory(entry->entity);
247
248                 if (mpdclient_cmd_add_path(c, mpd_directory_get_path(dir))) {
249                         screen_status_printf(_("Adding \'%s\' to queue"),
250                                              Utf8ToLocale(mpd_directory_get_path(dir)).c_str());
251                 }
252
253                 return true;
254         }
255
256         if (mpd_entity_get_type(entry->entity) != MPD_ENTITY_TYPE_SONG)
257                 return false;
258
259 #ifndef NCMPC_MINI
260         if (!toggle || (entry->flags & HIGHLIGHT) == 0)
261 #endif
262         {
263                 const auto *song = mpd_entity_get_song(entry->entity);
264
265 #ifndef NCMPC_MINI
266                 entry->flags |= HIGHLIGHT;
267 #endif
268
269                 if (mpdclient_cmd_add(c, song)) {
270                         char buf[BUFSIZE];
271
272                         strfsong(buf, BUFSIZE, options.list_format, song);
273                         screen_status_printf(_("Adding \'%s\' to queue"), buf);
274                 }
275 #ifndef NCMPC_MINI
276         } else {
277                 /* remove song from playlist */
278                 const auto *song = mpd_entity_get_song(entry->entity);
279                 int idx;
280
281                 entry->flags &= ~HIGHLIGHT;
282
283                 while ((idx = c->playlist.FindUri(*song)) >= 0)
284                         mpdclient_cmd_delete(c, idx);
285 #endif
286         }
287
288         return true;
289 }
290
291 bool
292 FileListPage::HandleSelect(struct mpdclient &c)
293 {
294         bool success = false;
295
296         const auto range = lw.GetRange();
297         for (const unsigned i : range) {
298                 auto *entry = GetIndex(i);
299                 if (entry != nullptr && entry->entity != nullptr)
300                         success = browser_select_entry(&c, entry, true);
301         }
302
303         SetDirty();
304
305         return range.end_index == range.start_index + 1 && success;
306 }
307
308 bool
309 FileListPage::HandleAdd(struct mpdclient &c)
310 {
311         bool success = false;
312
313         const auto range = lw.GetRange();
314         for (const unsigned i : range) {
315                 auto *entry = GetIndex(i);
316                 if (entry != nullptr && entry->entity != nullptr)
317                         success = browser_select_entry(&c, entry, false) ||
318                                 success;
319         }
320
321         return range.end_index == range.start_index + 1 && success;
322 }
323
324 void
325 FileListPage::HandleSelectAll(struct mpdclient &c)
326 {
327         if (filelist == nullptr)
328                 return;
329
330         for (unsigned i = 0; i < filelist->size(); ++i) {
331                 auto &entry = (*filelist)[i];
332
333                 if (entry.entity != nullptr)
334                         browser_select_entry(&c, &entry, false);
335         }
336
337         SetDirty();
338 }
339
340 #ifdef HAVE_GETMOUSE
341
342 bool
343 FileListPage::OnMouse(struct mpdclient &c, Point p,
344                       mmask_t bstate)
345 {
346         unsigned prev_selected = lw.selected;
347
348         if (ListPage::OnMouse(c, p, bstate))
349                 return true;
350
351         lw.SetCursor(lw.start + p.y);
352
353         if( bstate & BUTTON1_CLICKED ) {
354                 if (prev_selected == lw.selected)
355                         HandleEnter(c);
356         } else if (bstate & BUTTON3_CLICKED) {
357                 if (prev_selected == lw.selected)
358                         HandleSelect(c);
359         }
360
361         return true;
362 }
363
364 #endif
365
366 bool
367 FileListPage::OnCommand(struct mpdclient &c, command_t cmd)
368 {
369         if (filelist == nullptr)
370                 return false;
371
372         if (ListPage::OnCommand(c, cmd))
373                 return true;
374
375         switch (cmd) {
376 #if defined(ENABLE_SONG_SCREEN) || defined(ENABLE_LYRICS_SCREEN)
377                 const struct mpd_song *song;
378 #endif
379
380         case CMD_LIST_FIND:
381         case CMD_LIST_RFIND:
382         case CMD_LIST_FIND_NEXT:
383         case CMD_LIST_RFIND_NEXT:
384                 screen_find(screen, &lw, cmd, browser_lw_callback, filelist);
385                 SetDirty();
386                 return true;
387         case CMD_LIST_JUMP:
388                 screen_jump(screen, &lw,
389                             browser_lw_callback, filelist,
390                             PaintRow, this);
391                 SetDirty();
392                 return true;
393
394 #ifdef ENABLE_SONG_SCREEN
395         case CMD_SCREEN_SONG:
396                 song = GetSelectedSong();
397                 if (song == nullptr)
398                         return false;
399
400                 screen_song_switch(screen, c, *song);
401                 return true;
402 #endif
403
404 #ifdef ENABLE_LYRICS_SCREEN
405         case CMD_SCREEN_LYRICS:
406                 song = GetSelectedSong();
407                 if (song == nullptr)
408                         return false;
409
410                 screen_lyrics_switch(screen, c, *song, false);
411                 return true;
412 #endif
413         case CMD_SCREEN_SWAP:
414                 screen.Swap(c, GetSelectedSong());
415                 return true;
416
417         default:
418                 break;
419         }
420
421         if (!c.IsConnected())
422                 return false;
423
424         switch (cmd) {
425                 const struct mpd_song *song;
426
427         case CMD_PLAY:
428                 HandleEnter(c);
429                 return true;
430
431         case CMD_SELECT:
432                 if (HandleSelect(c))
433                         lw.HandleCommand(CMD_LIST_NEXT);
434                 SetDirty();
435                 return true;
436
437         case CMD_ADD:
438                 if (HandleAdd(c))
439                         lw.HandleCommand(CMD_LIST_NEXT);
440                 SetDirty();
441                 return true;
442
443         case CMD_SELECT_ALL:
444                 HandleSelectAll(c);
445                 return true;
446
447         case CMD_LOCATE:
448                 song = GetSelectedSong();
449                 if (song == nullptr)
450                         return false;
451
452                 screen_file_goto_song(screen, c, *song);
453                 return true;
454
455         default:
456                 break;
457         }
458
459         return false;
460 }
461
462 void
463 screen_browser_paint_directory(WINDOW *w, unsigned width,
464                                bool selected, const char *name)
465 {
466         row_color(w, COLOR_DIRECTORY, selected);
467
468         waddch(w, '[');
469         waddstr(w, name);
470         waddch(w, ']');
471
472         /* erase the unused space after the text */
473         row_clear_to_eol(w, width, selected);
474 }
475
476 static void
477 screen_browser_paint_playlist(WINDOW *w, unsigned width,
478                               bool selected, const char *name)
479 {
480         row_paint_text(w, width, COLOR_PLAYLIST, selected, name);
481 }
482
483 void
484 FileListPage::PaintRow(WINDOW *w, unsigned i,
485                        unsigned y, unsigned width,
486                        bool selected, const void *data)
487 {
488         const auto &page = *(const FileListPage *) data;
489
490         assert(page.filelist != nullptr);
491         assert(i < page.filelist->size());
492
493         const auto &entry = (*page.filelist)[i];
494         const struct mpd_entity *entity = entry.entity;
495         if (entity == nullptr) {
496                 screen_browser_paint_directory(w, width, selected, "..");
497                 return;
498         }
499
500 #ifndef NCMPC_MINI
501         const bool highlight = (entry.flags & HIGHLIGHT) != 0;
502 #else
503         const bool highlight = false;
504 #endif
505
506         switch (mpd_entity_get_type(entity)) {
507                 const struct mpd_directory *directory;
508                 const struct mpd_playlist *playlist;
509                 const char *name;
510
511         case MPD_ENTITY_TYPE_DIRECTORY:
512                 directory = mpd_entity_get_directory(entity);
513                 name = g_basename(mpd_directory_get_path(directory));
514                 screen_browser_paint_directory(w, width, selected,
515                                                Utf8ToLocale(name).c_str());
516                 break;
517
518         case MPD_ENTITY_TYPE_SONG:
519                 paint_song_row(w, y, width, selected, highlight,
520                                mpd_entity_get_song(entity), nullptr,
521                                page.song_format);
522                 break;
523
524         case MPD_ENTITY_TYPE_PLAYLIST:
525                 playlist = mpd_entity_get_playlist(entity);
526                 name = g_basename(mpd_playlist_get_path(playlist));
527                 screen_browser_paint_playlist(w, width, selected,
528                                               Utf8ToLocale(name).c_str());
529                 break;
530
531         default:
532                 row_paint_text(w, width, highlight ? COLOR_LIST_BOLD : COLOR_LIST,
533                                selected, "<unknown>");
534         }
535 }
536
537 void
538 FileListPage::Paint() const
539 {
540         lw.Paint(PaintRow, this);
541 }