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 #ifndef NCMPCPP_HELPERS_H
22 #define NCMPCPP_HELPERS_H
23 
24 #include "interfaces.h"
25 #include "mpdpp.h"
26 #include "screens/playlist.h"
27 #include "screens/screen.h"
28 #include "settings.h"
29 #include "song_list.h"
30 #include "status.h"
31 #include "utility/string.h"
32 #include "utility/type_conversions.h"
33 #include "utility/wide_string.h"
34 
35 enum ReapplyFilter { Yes, No };
36 
37 template <typename ItemT>
38 struct ScopedUnfilteredMenu
39 {
ScopedUnfilteredMenuScopedUnfilteredMenu40 	ScopedUnfilteredMenu(ReapplyFilter reapply_filter, NC::Menu<ItemT> &menu)
41 		: m_refresh(false), m_reapply_filter(reapply_filter), m_menu(menu)
42 	{
43 		m_is_filtered = m_menu.isFiltered();
44 		if (m_is_filtered)
45 			m_menu.showAllItems();
46 	}
47 
~ScopedUnfilteredMenuScopedUnfilteredMenu48 	~ScopedUnfilteredMenu()
49 	{
50 		if (m_is_filtered)
51 		{
52 			switch (m_reapply_filter)
53 			{
54 			case ReapplyFilter::Yes:
55 				m_menu.reapplyFilter();
56 				break;
57 			case ReapplyFilter::No:
58 				m_menu.showFilteredItems();
59 				break;
60 			}
61 		}
62 		if (m_refresh)
63 			m_menu.refresh();
64 	}
65 
setScopedUnfilteredMenu66 	void set(ReapplyFilter reapply_filter, bool refresh)
67 	{
68 		m_reapply_filter = reapply_filter;
69 		m_refresh = refresh;
70 	}
71 
72 private:
73 	bool m_is_filtered;
74 	bool m_refresh;
75 	ReapplyFilter m_reapply_filter;
76 	NC::Menu<ItemT> &m_menu;
77 };
78 
79 template <typename Iterator, typename PredicateT>
wrappedSearch(Iterator begin,Iterator current,Iterator end,const PredicateT & pred,bool wrap,bool skip_current)80 Iterator wrappedSearch(Iterator begin, Iterator current, Iterator end,
81                        const PredicateT &pred, bool wrap, bool skip_current)
82 {
83 	if (begin == end)
84 	{
85 		assert(current == end);
86 		return begin;
87 	}
88 	if (skip_current)
89 		++current;
90 	auto it = std::find_if(current, end, pred);
91 	if (it == end && wrap)
92 	{
93 		it = std::find_if(begin, current, pred);
94 		if (it == current)
95 			it = end;
96 	}
97 	return it;
98 }
99 
100 template <typename ItemT, typename PredicateT>
search(NC::Menu<ItemT> & m,const PredicateT & pred,SearchDirection direction,bool wrap,bool skip_current)101 bool search(NC::Menu<ItemT> &m, const PredicateT &pred,
102                   SearchDirection direction, bool wrap, bool skip_current)
103 {
104 	bool result = false;
105 	if (pred.defined())
106 	{
107 		switch (direction)
108 		{
109 			case SearchDirection::Backward:
110 			{
111 				auto it = wrappedSearch(m.rbegin(), m.rcurrent(), m.rend(),
112 					pred, wrap, skip_current
113 				);
114 				if (it != m.rend())
115 				{
116 					m.highlight(it.base()-m.begin()-1);
117 					result = true;
118 				}
119 				break;
120 			}
121 			case SearchDirection::Forward:
122 			{
123 				auto it = wrappedSearch(m.begin(), m.current(), m.end(),
124 					pred, wrap, skip_current
125 				);
126 				if (it != m.end())
127 				{
128 					m.highlight(it-m.begin());
129 					result = true;
130 				}
131 			}
132 		}
133 	}
134 	return result;
135 }
136 
137 template <typename Iterator>
hasSelected(Iterator first,Iterator last)138 bool hasSelected(Iterator first, Iterator last)
139 {
140 	for (; first != last; ++first)
141 		if (first->isSelected())
142 			return true;
143 	return false;
144 }
145 
146 template <typename Iterator>
getSelected(Iterator first,Iterator last)147 std::vector<Iterator> getSelected(Iterator first, Iterator last)
148 {
149 	std::vector<Iterator> result;
150 	for (; first != last; ++first)
151 		if (first->isSelected())
152 			result.push_back(first);
153 	return result;
154 }
155 
156 /// @return true if range that begins and ends with selected items was
157 /// found, false when there is no selected items (in which case first
158 /// == last).
159 template <typename Iterator>
findRange(Iterator & first,Iterator & last)160 bool findRange(Iterator &first, Iterator &last)
161 {
162 	for (; first != last; ++first)
163 	{
164 		if (first->isSelected())
165 			break;
166 	}
167 	if (first == last)
168 		return false;
169 	--last;
170 	for (; first != last; --last)
171 	{
172 		if (last->isSelected())
173 			break;
174 	}
175 	++last;
176 	return true;
177 }
178 
179 /// @return true if fully selected range was found or no selected
180 /// items were found, false otherwise.
181 template <typename Iterator>
findSelectedRange(Iterator & first,Iterator & last)182 bool findSelectedRange(Iterator &first, Iterator &last)
183 {
184 	auto orig_first = first;
185 	if (!findRange(first, last))
186 	{
187 		// If no selected items were found, return original range.
188 		if (first == last)
189 		{
190 			first = orig_first;
191 			return true;
192 		}
193 		else
194 			return false;
195 	}
196 	// We have range, now check if it's filled with selected items.
197 	for (auto it = first; it != last; ++it)
198 	{
199 		if (!it->isSelected())
200 			return false;
201 	}
202 	return true;
203 }
204 
205 template <typename T>
selectCurrentIfNoneSelected(NC::Menu<T> & m)206 void selectCurrentIfNoneSelected(NC::Menu<T> &m)
207 {
208 	if (!hasSelected(m.begin(), m.end()))
209 		m.current()->setSelected(true);
210 }
211 
212 template <typename Iterator>
getSelectedOrCurrent(Iterator first,Iterator last,Iterator current)213 std::vector<Iterator> getSelectedOrCurrent(Iterator first, Iterator last, Iterator current)
214 {
215 	std::vector<Iterator> result = getSelected(first, last);
216 	if (result.empty())
217 		result.push_back(current);
218 	return result;
219 }
220 
221 template <typename Iterator>
reverseSelectionHelper(Iterator first,Iterator last)222 void reverseSelectionHelper(Iterator first, Iterator last)
223 {
224 	for (; first != last; ++first)
225 		first->setSelected(!first->isSelected());
226 }
227 
228 template <typename F>
moveSelectedItemsUp(NC::Menu<MPD::Song> & m,F swap_fun)229 void moveSelectedItemsUp(NC::Menu<MPD::Song> &m, F swap_fun)
230 {
231 	if (m.choice() > 0)
232 		selectCurrentIfNoneSelected(m);
233 	auto list = getSelected(m.begin(), m.end());
234 	auto begin = m.begin();
235 	if (!list.empty() && list.front() != m.begin())
236 	{
237 		Mpd.StartCommandsList();
238 		for (auto it = list.begin(); it != list.end(); ++it)
239 			swap_fun(&Mpd, *it - begin, *it - begin - 1);
240 		Mpd.CommitCommandsList();
241 		if (list.size() > 1)
242 		{
243 			for (auto it = list.begin(); it != list.end(); ++it)
244 			{
245 				(*it)->setSelected(false);
246 				(*it-1)->setSelected(true);
247 			}
248 			m.highlight(list[(list.size())/2] - begin - 1);
249 		}
250 		else
251 		{
252 			// if we move only one item, do not select it. however, if single item
253 			// was selected prior to move, it'll deselect it. oh well.
254 			list[0]->setSelected(false);
255 			m.scroll(NC::Scroll::Up);
256 		}
257 	}
258 }
259 
260 template <typename F>
moveSelectedItemsDown(NC::Menu<MPD::Song> & m,F swap_fun)261 void moveSelectedItemsDown(NC::Menu<MPD::Song> &m, F swap_fun)
262 {
263 	if (m.choice() < m.size()-1)
264 		selectCurrentIfNoneSelected(m);
265 	auto list = getSelected(m.rbegin(), m.rend());
266 	auto begin = m.begin() + 1; // reverse iterators add 1, so we need to cancel it
267 	if (!list.empty() && list.front() != m.rbegin())
268 	{
269 		Mpd.StartCommandsList();
270 		for (auto it = list.begin(); it != list.end(); ++it)
271 			swap_fun(&Mpd, it->base() - begin, it->base() - begin + 1);
272 		Mpd.CommitCommandsList();
273 		if (list.size() > 1)
274 		{
275 			for (auto it = list.begin(); it != list.end(); ++it)
276 			{
277 				(*it)->setSelected(false);
278 				(*it-1)->setSelected(true);
279 			}
280 			m.highlight(list[(list.size())/2].base() - begin + 1);
281 		}
282 		else
283 		{
284 			// if we move only one item, do not select it. however, if single item
285 			// was selected prior to move, it'll deselect it. oh well.
286 			list[0]->setSelected(false);
287 			m.scroll(NC::Scroll::Down);
288 		}
289 	}
290 }
291 
292 template <typename F>
moveSelectedItemsTo(NC::Menu<MPD::Song> & menu,F && move_fun)293 void moveSelectedItemsTo(NC::Menu<MPD::Song> &menu, F &&move_fun)
294 {
295 	auto cur_ptr = &menu.current()->value();
296 	ScopedUnfilteredMenu<MPD::Song> sunfilter(ReapplyFilter::No, menu);
297 	// this is kinda shitty, but there is no other way to know
298 	// what position current item has in unfiltered menu.
299 	ptrdiff_t pos = 0;
300 	for (auto it = menu.begin(); it != menu.end(); ++it, ++pos)
301 		if (&it->value() == cur_ptr)
302 			break;
303 	auto begin = menu.begin();
304 	auto list = getSelected(menu.begin(), menu.end());
305 	// we move only truly selected items
306 	if (list.empty())
307 		return;
308 	// we can't move to the middle of selected items
309 	//(this also handles case when list.size() == 1)
310 	if (pos >= (list.front() - begin) && pos <= (list.back() - begin))
311 		return;
312 	int diff = pos - (list.front() - begin);
313 	Mpd.StartCommandsList();
314 	if (diff > 0) // move down
315 	{
316 		pos -= list.size();
317 		size_t i = list.size()-1;
318 		for (auto it = list.rbegin(); it != list.rend(); ++it, --i)
319 			move_fun(&Mpd, *it - begin, pos+i);
320 		Mpd.CommitCommandsList();
321 		i = list.size()-1;
322 		for (auto it = list.rbegin(); it != list.rend(); ++it, --i)
323 		{
324 			(*it)->setSelected(false);
325 			menu[pos+i].setSelected(true);
326 		}
327 	}
328 	else if (diff < 0) // move up
329 	{
330 		size_t i = 0;
331 		for (auto it = list.begin(); it != list.end(); ++it, ++i)
332 			move_fun(&Mpd, *it - begin, pos+i);
333 		Mpd.CommitCommandsList();
334 		i = 0;
335 		for (auto it = list.begin(); it != list.end(); ++it, ++i)
336 		{
337 			(*it)->setSelected(false);
338 			menu[pos+i].setSelected(true);
339 		}
340 	}
341 }
342 
343 template <typename F>
deleteSelectedSongs(NC::Menu<MPD::Song> & menu,F && delete_fun)344 void deleteSelectedSongs(NC::Menu<MPD::Song> &menu, F &&delete_fun)
345 {
346 	selectCurrentIfNoneSelected(menu);
347 	// We need to operate on the whole playlist to get positions right, but at the
348 	// same time we need to ignore all songs that are not filtered. We abuse the
349 	// fact that both ranges share the same values, i.e. we can compare addresses
350 	// of item values to check whether an item belongs to filtered range. TODO: do
351 	// something more sane here.
352 	NC::Menu<MPD::Song>::Iterator begin;
353 	NC::Menu<MPD::Song>::ReverseIterator real_begin, real_end;
354 	{
355 		ScopedUnfilteredMenu<MPD::Song> sunfilter(ReapplyFilter::No, menu);
356 		// obtain iterators for unfiltered range
357 		begin = menu.begin() + 1; // cancel reverse iterator's offset
358 		real_begin = menu.rbegin();
359 		real_end = menu.rend();
360 	};
361 	// get iterator to filtered range
362 	auto cur_filtered = menu.rbegin();
363 	Mpd.StartCommandsList();
364 	for (auto it = real_begin; it != real_end; ++it)
365 	{
366 		// current iterator belongs to filtered range, proceed
367 		if (&it->value() == &cur_filtered->value())
368 		{
369 			if (it->isSelected())
370 			{
371 				it->setSelected(false);
372 				delete_fun(Mpd, it.base() - begin);
373 			}
374 			++cur_filtered;
375 		}
376 	}
377 	Mpd.CommitCommandsList();
378 }
379 
380 template <typename F>
cropPlaylist(NC::Menu<MPD::Song> & m,F delete_fun)381 void cropPlaylist(NC::Menu<MPD::Song> &m, F delete_fun)
382 {
383 	reverseSelectionHelper(m.begin(), m.end());
384 	deleteSelectedSongs(m, delete_fun);
385 }
386 
387 template <typename Iterator>
getSharedDirectory(Iterator first,Iterator last)388 std::string getSharedDirectory(Iterator first, Iterator last)
389 {
390 	assert(first != last);
391 	std::string result = first->getDirectory();
392 	while (++first != last)
393 	{
394 		result = getSharedDirectory(result, first->getDirectory());
395 		if (result == "/")
396 			break;
397 	}
398 	return result;
399 }
400 
401 template <typename Iterator>
addSongsToPlaylist(Iterator first,Iterator last,bool play,int position)402 bool addSongsToPlaylist(Iterator first, Iterator last, bool play, int position)
403 {
404 	bool result = true;
405 	auto addSongNoError = [&](Iterator it) -> int {
406 		try
407 		{
408 			return Mpd.AddSong(*it, position);
409 		}
410 		catch (MPD::ServerError &e)
411 		{
412 			Status::handleServerError(e);
413 			result = false;
414 			return -1;
415 		}
416 	};
417 
418 	if (last-first >= 1)
419 	{
420 		int id;
421 		while (true)
422 		{
423 			id = addSongNoError(first);
424 			if (id >= 0)
425 				break;
426 			++first;
427 			if (first == last)
428 				return result;
429 		}
430 
431 		if (position == -1)
432 		{
433 			++first;
434 			for(; first != last; ++first)
435 				addSongNoError(first);
436 		}
437 		else
438 		{
439 			++position;
440 			--last;
441 			for (; first != last; --last)
442 				addSongNoError(last);
443 		}
444 		if (play)
445 			Mpd.PlayID(id);
446 	}
447 
448 	return result;
449 }
450 
ShowTime(T & buf,size_t length,bool short_names)451 template <typename T> void ShowTime(T &buf, size_t length, bool short_names)
452 {
453 	const unsigned MINUTE = 60;
454 	const unsigned HOUR = 60*MINUTE;
455 	const unsigned DAY = 24*HOUR;
456 	const unsigned YEAR = 365*DAY;
457 
458 	unsigned years = length/YEAR;
459 	if (years)
460 	{
461 		buf << years << (short_names ? "y" : (years == 1 ? " year" : " years"));
462 		length -= years*YEAR;
463 		if (length)
464 			buf << ", ";
465 	}
466 	unsigned days = length/DAY;
467 	if (days)
468 	{
469 		buf << days << (short_names ? "d" : (days == 1 ? " day" : " days"));
470 		length -= days*DAY;
471 		if (length)
472 			buf << ", ";
473 	}
474 	unsigned hours = length/HOUR;
475 	if (hours)
476 	{
477 		buf << hours << (short_names ? "h" : (hours == 1 ? " hour" : " hours"));
478 		length -= hours*HOUR;
479 		if (length)
480 			buf << ", ";
481 	}
482 	unsigned minutes = length/MINUTE;
483 	if (minutes)
484 	{
485 		buf << minutes << (short_names ? "m" : (minutes == 1 ? " minute" : " minutes"));
486 		length -= minutes*MINUTE;
487 		if (length)
488 			buf << ", ";
489 	}
490 	if (length)
491 		buf << length << (short_names ? "s" : (length == 1 ? " second" : " seconds"));
492 }
493 
494 template <typename BufferT>
ShowTag(BufferT & buf,const std::string & tag)495 void ShowTag(BufferT &buf, const std::string &tag)
496 {
497 	if (tag.empty())
498 		buf << Config.empty_tags_color
499 		    << Config.empty_tag
500 		    << NC::FormattedColor::End<>(Config.empty_tags_color);
501 	else
502 		buf << tag;
503 }
504 
ShowTag(const std::string & tag)505 inline NC::Buffer ShowTag(const std::string &tag)
506 {
507 	NC::Buffer result;
508 	ShowTag(result, tag);
509 	return result;
510 }
511 
512 template <typename T>
setHighlightFixes(NC::Menu<T> & m)513 void setHighlightFixes(NC::Menu<T> &m)
514 {
515 	m.setHighlightPrefix(Config.current_item_prefix);
516 	m.setHighlightSuffix(Config.current_item_suffix);
517 }
518 
519 template <typename T>
setHighlightInactiveColumnFixes(NC::Menu<T> & m)520 void setHighlightInactiveColumnFixes(NC::Menu<T> &m)
521 {
522 	m.setHighlightPrefix(Config.current_item_inactive_column_prefix);
523 	m.setHighlightSuffix(Config.current_item_inactive_column_suffix);
524 }
525 
withErrors(bool success)526 inline const char *withErrors(bool success)
527 {
528 	return success ? "" : " " "(with errors)";
529 }
530 
531 void deleteSelectedSongsFromPlaylist(NC::Menu<MPD::Song> &playlist);
532 
533 bool addSongToPlaylist(const MPD::Song &s, bool play, int position = -1);
534 
535 const MPD::Song *currentSong(const BaseScreen *screen);
536 
537 std::string timeFormat(const char *format, time_t t);
538 
539 std::string Timestamp(time_t t);
540 
541 std::wstring Scroller(const std::wstring &str, size_t &pos, size_t width);
542 void writeCyclicBuffer(const NC::WBuffer &buf, NC::Window &w, size_t &start_pos,
543                        size_t width, const std::wstring &separator);
544 
545 #endif // NCMPCPP_HELPERS_H
546