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