display: fix possible crash if song_columns_list_format was invalid
[ncmpcpp.git] / src / display.cpp
blobd10961de516d82d8e98d6c862b41d4990a08e2db
1 /***************************************************************************
2 * Copyright (C) 2008-2012 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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 ***************************************************************************/
21 #include <cassert>
23 #include "browser.h"
24 #include "display.h"
25 #include "helpers.h"
26 #include "song_info.h"
27 #include "playlist.h"
28 #include "global.h"
29 #include "tag_editor.h"
30 #include "utility/type_conversions.h"
32 using Global::myScreen;
34 namespace {//
36 const wchar_t *toColumnName(char c)
38 switch (c)
40 case 'l':
41 return L"Time";
42 case 'f':
43 return L"Filename";
44 case 'D':
45 return L"Directory";
46 case 'a':
47 return L"Artist";
48 case 'A':
49 return L"Album Artist";
50 case 't':
51 return L"Title";
52 case 'b':
53 return L"Album";
54 case 'y':
55 return L"Date";
56 case 'n': case 'N':
57 return L"Track";
58 case 'g':
59 return L"Genre";
60 case 'c':
61 return L"Composer";
62 case 'p':
63 return L"Performer";
64 case 'd':
65 return L"Disc";
66 case 'C':
67 return L"Comment";
68 case 'P':
69 return L"Priority";
70 default:
71 return L"?";
75 template <typename T>
76 void setProperties(NC::Menu<T> &menu, const MPD::Song &s, HasSongs &screen, bool &separate_albums,
77 bool &is_now_playing, bool &is_selected, bool &discard_colors)
79 size_t drawn_pos = menu.drawn() - menu.begin();
80 separate_albums = false;
81 if (Config.playlist_separate_albums)
83 auto pl = screen.getProxySongList();
84 assert(pl);
85 auto next = pl->getSong(drawn_pos+1);
86 if (next && next->getAlbum() != s.getAlbum())
87 separate_albums = true;
89 if (separate_albums)
91 menu << NC::fmtUnderline;
92 mvwhline(menu.raw(), menu.getY(), 0, KEY_SPACE, menu.getWidth());
95 is_selected = menu.drawn()->isSelected();
96 discard_colors = Config.discard_colors_if_item_is_selected && is_selected;
98 int song_pos = menu.isFiltered() ? s.getPosition() : drawn_pos;
99 is_now_playing = static_cast<void *>(&menu) == myPlaylist->Items
100 && song_pos == myPlaylist->NowPlaying;
101 if (is_now_playing)
102 menu << Config.now_playing_prefix;
105 template <typename T>
106 void showSongs(NC::Menu<T> &menu, const MPD::Song &s, HasSongs &screen, const std::string &format)
108 bool separate_albums, is_now_playing, is_selected, discard_colors;
109 setProperties(menu, s, screen, separate_albums, is_now_playing, is_selected, discard_colors);
111 std::string line = s.toString(format, "$");
112 for (auto it = line.begin(); it != line.end(); ++it)
114 if (*it == '$')
116 if (++it == line.end()) // end of format
118 menu << '$';
119 break;
121 else if (isdigit(*it)) // color
123 if (!discard_colors)
124 menu << NC::Color(*it-'0');
126 else if (*it == 'R') // right align
128 NC::WBuffer buf;
129 buf << L" ";
130 String2Buffer(ToWString(line.substr(it-line.begin()+1)), buf);
131 if (discard_colors)
132 buf.removeFormatting();
133 if (is_now_playing)
134 buf << Config.now_playing_suffix;
135 menu << NC::XY(menu.getWidth()-buf.str().length()-(is_selected ? Config.selected_item_suffix_length : 0), menu.getY()) << buf;
136 if (separate_albums)
137 menu << NC::fmtUnderlineEnd;
138 return;
140 else // not a color nor right align, just a random character
141 menu << *--it;
143 else if (*it == MPD::Song::FormatEscapeCharacter)
145 // treat '$' as a normal character if song format escape char is prepended to it
146 if (++it == line.end() || *it != '$')
147 --it;
148 menu << *it;
150 else
151 menu << *it;
153 if (is_now_playing)
154 menu << Config.now_playing_suffix;
155 if (separate_albums)
156 menu << NC::fmtUnderlineEnd;
159 template <typename T>
160 void showSongsInColumns(NC::Menu<T> &menu, const MPD::Song &s, HasSongs &screen)
162 if (Config.columns.empty())
163 return;
165 bool separate_albums, is_now_playing, is_selected, discard_colors;
166 setProperties(menu, s, screen, separate_albums, is_now_playing, is_selected, discard_colors);
168 int width;
169 int y = menu.getY();
170 int remained_width = menu.getWidth();
171 std::vector<Column>::const_iterator it, last = Config.columns.end() - 1;
172 for (it = Config.columns.begin(); it != Config.columns.end(); ++it)
174 // check current X coordinate
175 int x = menu.getX();
176 // column has relative width and all after it have fixed width,
177 // so stretch it so it fills whole screen along with these after.
178 if (it->stretch_limit >= 0) // (*)
179 width = remained_width - it->stretch_limit;
180 else
181 width = it->fixed ? it->width : it->width * menu.getWidth() * 0.01;
182 // columns with relative width may shrink to 0, omit them
183 if (width == 0)
184 continue;
185 // if column is not last, we need to have spacing between it
186 // and next column, so we substract it now and restore later.
187 if (it != last)
188 --width;
190 if (it == Config.columns.begin() && (is_now_playing || is_selected))
192 // here comes the shitty part. if we applied now playing or selected
193 // prefix, first column's width needs to be properly modified, so
194 // next column is not affected by them. if prefixes fit, we just
195 // subtract their width from allowed column's width. if they don't,
196 // then we pretend that they do, but we adjust current cursor position
197 // so part of them will be overwritten by next column.
198 int offset = 0;
199 if (is_now_playing)
200 offset += Config.now_playing_prefix_length;
201 if (is_selected)
202 offset += Config.selected_item_prefix_length;
203 if (width-offset < 0)
205 remained_width -= width + 1;
206 menu.goToXY(width, y);
207 menu << ' ';
208 continue;
210 width -= offset;
211 remained_width -= offset;
214 // if column doesn't fit into screen, discard it and any other after it.
215 if (remained_width-width < 0 || width < 0 /* this one may come from (*) */)
216 break;
218 std::wstring tag;
219 for (size_t i = 0; i < it->type.length(); ++i)
221 MPD::Song::GetFunction get = charToGetFunction(it->type[i]);
222 tag = ToWString(get ? s.getTags(get) : "");
223 if (!tag.empty())
224 break;
226 if (tag.empty() && it->display_empty_tag)
227 tag = ToWString(Config.empty_tag);
228 wideCut(tag, width);
230 if (!discard_colors && it->color != NC::clDefault)
231 menu << it->color;
233 int x_off = 0;
234 // if column uses right alignment, calculate proper offset.
235 // otherwise just assume offset is 0, ie. we start from the left.
236 if (it->right_alignment)
237 x_off = std::max(0, width - int(wideLength(tag)));
239 whline(menu.raw(), KEY_SPACE, width);
240 menu.goToXY(x + x_off, y);
241 menu << tag;
242 menu.goToXY(x + width, y);
243 if (it != last)
245 // add missing width's part and restore the value.
246 menu << ' ';
247 remained_width -= width+1;
250 if (!discard_colors && it->color != NC::clDefault)
251 menu << NC::clEnd;
254 // here comes the shitty part, second chapter. here we apply
255 // now playing suffix or/and make room for selected suffix
256 // (as it will be applied in Menu::Refresh when this function
257 // returns there).
258 if (is_now_playing)
260 int np_x = menu.getWidth() - Config.now_playing_suffix_length;
261 if (is_selected)
262 np_x -= Config.selected_item_suffix_length;
263 menu.goToXY(np_x, y);
264 menu << Config.now_playing_suffix;
266 if (is_selected)
267 menu.goToXY(menu.getWidth() - Config.selected_item_suffix_length, y);
269 if (separate_albums)
270 menu << NC::fmtUnderlineEnd;
275 std::string Display::Columns(size_t list_width)
277 std::string result;
278 if (Config.columns.empty())
279 return result;
281 int width;
282 int remained_width = list_width;
283 std::vector<Column>::const_iterator it, last = Config.columns.end() - 1;
284 for (it = Config.columns.begin(); it != Config.columns.end(); ++it)
286 // column has relative width and all after it have fixed width,
287 // so stretch it so it fills whole screen along with these after.
288 if (it->stretch_limit >= 0) // (*)
289 width = remained_width - it->stretch_limit;
290 else
291 width = it->fixed ? it->width : it->width * list_width * 0.01;
292 // columns with relative width may shrink to 0, omit them
293 if (width == 0)
294 continue;
295 // if column is not last, we need to have spacing between it
296 // and next column, so we substract it now and restore later.
297 if (it != last)
298 --width;
300 // if column doesn't fit into screen, discard it and any other after it.
301 if (remained_width-width < 0 || width < 0 /* this one may come from (*) */)
302 break;
304 std::wstring name;
305 if (it->name.empty())
307 size_t j = 0;
308 while (true)
310 name += toColumnName(it->type[j]);
311 ++j;
312 if (j < it->type.length())
313 name += '/';
314 else
315 break;
318 else
319 name = it->name;
320 wideCut(name, width);
322 int x_off = std::max(0, width - int(wideLength(name)));
323 if (it->right_alignment)
325 result += std::string(x_off, KEY_SPACE);
326 result += ToString(name);
328 else
330 result += ToString(name);
331 result += std::string(x_off, KEY_SPACE);
334 if (it != last)
336 // add missing width's part and restore the value.
337 remained_width -= width+1;
338 result += ' ';
342 return result;
345 void Display::SongsInColumns(NC::Menu<MPD::Song> &menu, HasSongs *screen)
347 showSongsInColumns(menu, menu.drawn()->value(), *screen);
350 void Display::Songs(NC::Menu<MPD::Song> &menu, HasSongs *screen, const std::string &format)
352 showSongs(menu, menu.drawn()->value(), *screen, format);
355 #ifdef HAVE_TAGLIB_H
356 void Display::Tags(NC::Menu<MPD::MutableSong> &menu)
358 const MPD::MutableSong &s = menu.drawn()->value();
359 size_t i = myTagEditor->TagTypes->choice();
360 if (i < 11)
362 ShowTag(menu, s.getTags(SongInfo::Tags[i].Get));
364 else if (i == 12)
366 if (s.getNewURI().empty())
367 menu << s.getName();
368 else
369 menu << s.getName() << Config.color2 << " -> " << NC::clEnd << s.getNewURI();
372 #endif // HAVE_TAGLIB_H
374 void Display::Outputs(NC::Menu<MPD::Output> &menu)
376 menu << menu.drawn()->value().name();
379 void Display::Items(NC::Menu<MPD::Item> &menu)
381 const MPD::Item &item = menu.drawn()->value();
382 switch (item.type)
384 case MPD::itDirectory:
385 menu << "[" << getBasename(item.name) << "]";
386 break;
387 case MPD::itSong:
388 if (!Config.columns_in_browser)
389 showSongs(menu, *item.song, *myBrowser, Config.song_list_format);
390 else
391 showSongsInColumns(menu, *item.song, *myBrowser);
392 break;
393 case MPD::itPlaylist:
394 menu << Config.browser_playlist_prefix << getBasename(item.name);
395 break;
399 void Display::SearchEngine(NC::Menu<SEItem> &menu)
401 const SEItem &si = menu.drawn()->value();
402 if (si.isSong())
404 if (!Config.columns_in_search_engine)
405 showSongs(menu, si.song(), *mySearcher, Config.song_list_format);
406 else
407 showSongsInColumns(menu, si.song(), *mySearcher);
409 else
410 menu << si.buffer();