1 /***************************************************************************
2 * Copyright (C) 2008-2021 by Andrzej Rybczak *
3 * andrzej@rybczak.net *
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 *
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 ***************************************************************************/
20
21 #include <cassert>
22
23 #include "curses/menu_impl.h"
24 #include "screens/browser.h"
25 #include "charset.h"
26 #include "display.h"
27 #include "format_impl.h"
28 #include "helpers.h"
29 #include "screens/song_info.h"
30 #include "screens/playlist.h"
31 #include "global.h"
32 #include "screens/tag_editor.h"
33 #include "utility/string.h"
34 #include "utility/type_conversions.h"
35
36 using Global::myScreen;
37
38 namespace {
39
toColumnName(char c)40 const wchar_t *toColumnName(char c)
41 {
42 switch (c)
43 {
44 case 'l':
45 return L"Time";
46 case 'f':
47 return L"Filename";
48 case 'D':
49 return L"Directory";
50 case 'a':
51 return L"Artist";
52 case 'A':
53 return L"Album Artist";
54 case 't':
55 return L"Title";
56 case 'b':
57 return L"Album";
58 case 'y':
59 return L"Date";
60 case 'n': case 'N':
61 return L"Track";
62 case 'g':
63 return L"Genre";
64 case 'c':
65 return L"Composer";
66 case 'p':
67 return L"Performer";
68 case 'd':
69 return L"Disc";
70 case 'C':
71 return L"Comment";
72 case 'P':
73 return L"Priority";
74 default:
75 return L"?";
76 }
77 }
78
79 template <typename T>
setProperties(NC::Menu<T> & menu,const MPD::Song & s,const SongList & list,bool & separate_albums,bool & is_now_playing,bool & is_selected,bool & is_in_playlist,bool & discard_colors)80 void setProperties(NC::Menu<T> &menu, const MPD::Song &s, const SongList &list,
81 bool &separate_albums, bool &is_now_playing, bool &is_selected,
82 bool &is_in_playlist, bool &discard_colors)
83 {
84 size_t drawn_pos = menu.drawn() - menu.begin();
85 separate_albums = false;
86 if (Config.playlist_separate_albums)
87 {
88 auto next = list.beginS() + drawn_pos + 1;
89 if (next != list.endS())
90 {
91 if (next->song() != nullptr)
92 {
93 // Draw a separator when the next album is different than the current
94 // one. In case there are two albums with the same name, but a different
95 // artist, compare also artists.
96 separate_albums = next->song()->getAlbum() != s.getAlbum()
97 || next->song()->getArtist() != s.getArtist();
98 }
99 }
100 }
101 if (separate_albums)
102 {
103 menu << NC::Format::Underline;
104 mvwhline(menu.raw(), menu.getY(), 0, NC::Key::Space, menu.getWidth());
105 }
106
107 int song_pos = s.getPosition();
108 is_now_playing = Status::State::player() != MPD::psStop
109 && myPlaylist->isActiveWindow(menu)
110 && song_pos == Status::State::currentSongPosition();
111 if (is_now_playing)
112 menu << Config.now_playing_prefix;
113
114 is_in_playlist = !myPlaylist->isActiveWindow(menu)
115 && myPlaylist->checkForSong(s);
116 if (is_in_playlist)
117 menu << NC::Format::Bold;
118
119 is_selected = menu.drawn()->isSelected();
120 discard_colors = Config.discard_colors_if_item_is_selected && is_selected;
121 }
122
123 template <typename T>
unsetProperties(NC::Menu<T> & menu,bool separate_albums,bool is_now_playing,bool is_in_playlist)124 void unsetProperties(NC::Menu<T> &menu, bool separate_albums, bool is_now_playing,
125 bool is_in_playlist)
126 {
127 if (is_in_playlist)
128 menu << NC::Format::NoBold;
129
130 if (is_now_playing)
131 menu << Config.now_playing_suffix;
132
133 if (separate_albums)
134 menu << NC::Format::NoUnderline;
135 }
136
137 template <typename T>
showSongs(NC::Menu<T> & menu,const MPD::Song & s,const SongList & list,const Format::AST<char> & ast)138 void showSongs(NC::Menu<T> &menu, const MPD::Song &s, const SongList &list, const Format::AST<char> &ast)
139 {
140 bool separate_albums, is_now_playing, is_selected, is_in_playlist, discard_colors;
141 setProperties(menu, s, list, separate_albums, is_now_playing, is_selected,
142 is_in_playlist, discard_colors);
143
144 const size_t y = menu.getY();
145 NC::Buffer right_aligned;
146 Format::print(ast, menu, &s, &right_aligned,
147 discard_colors ? Format::Flags::Tag | Format::Flags::OutputSwitch : Format::Flags::All
148 );
149 if (!right_aligned.str().empty())
150 {
151 size_t x_off = menu.getWidth() - wideLength(ToWString(right_aligned.str()));
152 if (menu.isHighlighted() && list.currentS()->song() == &s)
153 {
154 if (menu.highlightSuffix() == Config.current_item_suffix)
155 x_off -= Config.current_item_suffix_length;
156 else
157 x_off -= Config.current_item_inactive_column_suffix_length;
158 }
159 if (is_now_playing)
160 x_off -= Config.now_playing_suffix_length;
161 if (is_selected)
162 x_off -= Config.selected_item_suffix_length;
163 menu << NC::TermManip::ClearToEOL << NC::XY(x_off, y) << right_aligned;
164 }
165
166 unsetProperties(menu, separate_albums, is_now_playing, is_in_playlist);
167 }
168
169 template <typename T>
showSongsInColumns(NC::Menu<T> & menu,const MPD::Song & s,const SongList & list)170 void showSongsInColumns(NC::Menu<T> &menu, const MPD::Song &s, const SongList &list)
171 {
172 if (Config.columns.empty())
173 return;
174
175 bool separate_albums, is_now_playing, is_selected, is_in_playlist, discard_colors;
176 setProperties(menu, s, list, separate_albums, is_now_playing, is_selected,
177 is_in_playlist, discard_colors);
178
179 int menu_width = menu.getWidth();
180 if (menu.isHighlighted() && list.currentS()->song() == &s)
181 {
182 if (menu.highlightPrefix() == Config.current_item_prefix)
183 menu_width -= Config.current_item_prefix_length;
184 else
185 menu_width -= Config.current_item_inactive_column_prefix_length;
186
187 if (menu.highlightSuffix() == Config.current_item_suffix)
188 menu_width -= Config.current_item_suffix_length;
189 else
190 menu_width -= Config.current_item_inactive_column_suffix_length;
191 }
192 if (is_now_playing)
193 {
194 menu_width -= Config.now_playing_prefix_length;
195 menu_width -= Config.now_playing_suffix_length;
196 }
197 if (is_selected)
198 {
199 menu_width -= Config.selected_item_prefix_length;
200 menu_width -= Config.selected_item_suffix_length;
201 }
202
203 int width;
204 int y = menu.getY();
205 int remained_width = menu_width;
206
207 std::vector<Column>::const_iterator it, last = Config.columns.end() - 1;
208 for (it = Config.columns.begin(); it != Config.columns.end(); ++it)
209 {
210 // check current X coordinate
211 int x = menu.getX();
212 // column has relative width and all after it have fixed width,
213 // so stretch it so it fills whole screen along with these after.
214 if (it->stretch_limit >= 0) // (*)
215 width = remained_width - it->stretch_limit;
216 else
217 width = it->fixed ? it->width : it->width * menu_width * 0.01;
218 // columns with relative width may shrink to 0, omit them
219 if (width == 0)
220 continue;
221 // if column is not last, we need to have spacing between it
222 // and next column, so we substract it now and restore later.
223 if (it != last)
224 --width;
225
226 // if column doesn't fit into screen, discard it and any other after it.
227 if (remained_width-width < 0 || width < 0 /* this one may come from (*) */)
228 break;
229
230 std::wstring tag;
231 for (size_t i = 0; i < it->type.length(); ++i)
232 {
233 MPD::Song::GetFunction get = charToGetFunction(it->type[i]);
234 assert(get);
235 tag = ToWString(Charset::utf8ToLocale(s.getTags(get)));
236 if (!tag.empty())
237 break;
238 }
239 if (tag.empty() && it->display_empty_tag)
240 tag = ToWString(Config.empty_tag);
241 wideCut(tag, width);
242
243 if (!discard_colors && it->color != NC::Color::Default)
244 menu << it->color;
245
246 int x_off = 0;
247 // if column uses right alignment, calculate proper offset.
248 // otherwise just assume offset is 0, ie. we start from the left.
249 if (it->right_alignment)
250 x_off = std::max(0, width - int(wideLength(tag)));
251
252 whline(menu.raw(), NC::Key::Space, width);
253 menu.goToXY(x + x_off, y);
254 menu << tag;
255 menu.goToXY(x + width, y);
256 if (it != last)
257 {
258 // add missing width's part and restore the value.
259 menu << ' ';
260 remained_width -= width+1;
261 }
262
263 if (!discard_colors && it->color != NC::Color::Default)
264 menu << NC::Color::End;
265 }
266
267 unsetProperties(menu, separate_albums, is_now_playing, is_in_playlist);
268 }
269
270 }
271
Columns(size_t list_width)272 std::string Display::Columns(size_t list_width)
273 {
274 std::string result;
275 if (Config.columns.empty())
276 return result;
277
278 int width;
279 int remained_width = list_width;
280 std::vector<Column>::const_iterator it, last = Config.columns.end() - 1;
281 for (it = Config.columns.begin(); it != Config.columns.end(); ++it)
282 {
283 // column has relative width and all after it have fixed width,
284 // so stretch it so it fills whole screen along with these after.
285 if (it->stretch_limit >= 0) // (*)
286 width = remained_width - it->stretch_limit;
287 else
288 width = it->fixed ? it->width : it->width * list_width * 0.01;
289 // columns with relative width may shrink to 0, omit them
290 if (width == 0)
291 continue;
292 // if column is not last, we need to have spacing between it
293 // and next column, so we substract it now and restore later.
294 if (it != last)
295 --width;
296
297 // if column doesn't fit into screen, discard it and any other after it.
298 if (remained_width-width < 0 || width < 0 /* this one may come from (*) */)
299 break;
300
301 std::wstring name;
302 if (it->name.empty())
303 {
304 size_t j = 0;
305 while (true)
306 {
307 name += toColumnName(it->type[j]);
308 ++j;
309 if (j < it->type.length())
310 name += '/';
311 else
312 break;
313 }
314 }
315 else
316 name = it->name;
317 wideCut(name, width);
318
319 int x_off = std::max(0, width - int(wideLength(name)));
320 if (it->right_alignment)
321 {
322 result += std::string(x_off, NC::Key::Space);
323 result += Charset::utf8ToLocale(ToString(name));
324 }
325 else
326 {
327 result += Charset::utf8ToLocale(ToString(name));
328 result += std::string(x_off, NC::Key::Space);
329 }
330
331 if (it != last)
332 {
333 // add missing width's part and restore the value.
334 remained_width -= width+1;
335 result += ' ';
336 }
337 }
338
339 return result;
340 }
341
SongsInColumns(NC::Menu<MPD::Song> & menu,const SongList & list)342 void Display::SongsInColumns(NC::Menu<MPD::Song> &menu, const SongList &list)
343 {
344 showSongsInColumns(menu, menu.drawn()->value(), list);
345 }
346
Songs(NC::Menu<MPD::Song> & menu,const SongList & list,const Format::AST<char> & ast)347 void Display::Songs(NC::Menu<MPD::Song> &menu, const SongList &list, const Format::AST<char> &ast)
348 {
349 showSongs(menu, menu.drawn()->value(), list, ast);
350 }
351
352 #ifdef HAVE_TAGLIB_H
Tags(NC::Menu<MPD::MutableSong> & menu)353 void Display::Tags(NC::Menu<MPD::MutableSong> &menu)
354 {
355 const MPD::MutableSong &s = menu.drawn()->value();
356 if (s.isModified())
357 menu << Config.modified_item_prefix;
358 size_t i = myTagEditor->TagTypes->choice();
359 if (i < 11)
360 {
361 ShowTag(menu, Charset::utf8ToLocale(s.getTags(SongInfo::Tags[i].Get)));
362 }
363 else if (i == 12)
364 {
365 if (s.getNewName().empty())
366 menu << Charset::utf8ToLocale(s.getName());
367 else
368 menu << Charset::utf8ToLocale(s.getName())
369 << Config.color2
370 << " -> "
371 << NC::FormattedColor::End<>(Config.color2)
372 << Charset::utf8ToLocale(s.getNewName());
373 }
374 }
375 #endif // HAVE_TAGLIB_H
376
Items(NC::Menu<MPD::Item> & menu,const SongList & list)377 void Display::Items(NC::Menu<MPD::Item> &menu, const SongList &list)
378 {
379 const MPD::Item &item = menu.drawn()->value();
380 switch (item.type())
381 {
382 case MPD::Item::Type::Directory:
383 menu << "["
384 << Charset::utf8ToLocale(getBasename(item.directory().path()))
385 << "]";
386 break;
387 case MPD::Item::Type::Song:
388 switch (Config.browser_display_mode)
389 {
390 case DisplayMode::Classic:
391 showSongs(menu, item.song(), list, Config.song_list_format);
392 break;
393 case DisplayMode::Columns:
394 showSongsInColumns(menu, item.song(), list);
395 break;
396 }
397 break;
398 case MPD::Item::Type::Playlist:
399 menu << Config.browser_playlist_prefix
400 << Charset::utf8ToLocale(getBasename(item.playlist().path()));
401 break;
402 }
403 }
404
SEItems(NC::Menu<SEItem> & menu,const SongList & list)405 void Display::SEItems(NC::Menu<SEItem> &menu, const SongList &list)
406 {
407 const SEItem &si = menu.drawn()->value();
408 if (si.isSong())
409 {
410 switch (Config.search_engine_display_mode)
411 {
412 case DisplayMode::Classic:
413 showSongs(menu, si.song(), list, Config.song_list_format);
414 break;
415 case DisplayMode::Columns:
416 showSongsInColumns(menu, si.song(), list);
417 break;
418 }
419 }
420 else
421 menu << si.buffer();
422 }
423