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