ListWindow: replace callback function with abstract class
[ncmpc-debian.git] / src / ListWindow.cxx
1 /* ncmpc (Ncurses MPD Client)
2  * (c) 2004-2018 The Music Player Daemon Project
3  * Project homepage: http://musicpd.org
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 along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19
20 #include "ListWindow.hxx"
21 #include "ListRenderer.hxx"
22 #include "config.h"
23 #include "options.hxx"
24 #include "charset.hxx"
25 #include "match.hxx"
26 #include "command.hxx"
27 #include "colors.hxx"
28 #include "paint.hxx"
29 #include "screen_status.hxx"
30 #include "screen_utils.hxx"
31 #include "i18n.h"
32
33 #include <assert.h>
34 #include <stdlib.h>
35 #include <unistd.h>
36 #include <string.h>
37
38 void
39 ListWindow::Reset()
40 {
41         selected = 0;
42         range_selection = false;
43         range_base = 0;
44         start = 0;
45 }
46
47 unsigned
48 ListWindow::ValidateIndex(unsigned i) const
49 {
50         if (length == 0)
51                 return 0;
52         else if (i >= length)
53                 return length - 1;
54         else
55                 return i;
56 }
57
58 void
59 ListWindow::CheckSelected()
60 {
61         selected = ValidateIndex(selected);
62
63         if (range_selection)
64                 range_base = ValidateIndex(range_base);
65 }
66
67 void
68 ListWindow::Resize(Size _size)
69 {
70         size = _size;
71         CheckOrigin();
72 }
73
74 void
75 ListWindow::SetLength(unsigned _length)
76 {
77         if (_length == length)
78                 return;
79
80         length = _length;
81
82         CheckSelected();
83         CheckOrigin();
84 }
85
86 void
87 ListWindow::Center(unsigned n)
88 {
89         if (n > size.height / 2)
90                 start = n - size.height / 2;
91         else
92                 start = 0;
93
94         if (start + size.height > length) {
95                 if (size.height < length)
96                         start = length - size.height;
97                 else
98                         start = 0;
99         }
100 }
101
102 void
103 ListWindow::ScrollTo(unsigned n)
104 {
105         int new_start = start;
106
107         if ((unsigned) options.scroll_offset * 2 >= size.height)
108                 // Center if the offset is more than half the screen
109                 new_start = n - size.height / 2;
110         else {
111                 if (n < start + options.scroll_offset)
112                         new_start = n - options.scroll_offset;
113
114                 if (n >= start + size.height - options.scroll_offset)
115                         new_start = n - size.height + 1 + options.scroll_offset;
116         }
117
118         if (new_start + size.height > length)
119                 new_start = length - size.height;
120
121         if (new_start < 0 || length == 0)
122                 new_start = 0;
123
124         start = new_start;
125 }
126
127 void
128 ListWindow::SetCursor(unsigned i)
129 {
130         range_selection = false;
131         selected = i;
132
133         CheckSelected();
134         CheckOrigin();
135 }
136
137 void
138 ListWindow::MoveCursor(unsigned n)
139 {
140         selected = n;
141
142         CheckSelected();
143         CheckOrigin();
144 }
145
146 void
147 ListWindow::FetchCursor()
148 {
149         if (start > 0 &&
150             selected < start + options.scroll_offset)
151                 MoveCursor(start + options.scroll_offset);
152         else if (start + size.height < length &&
153                  selected > start + size.height - 1 - options.scroll_offset)
154                 MoveCursor(start + size.height - 1 - options.scroll_offset);
155 }
156
157 ListWindowRange
158 ListWindow::GetRange() const
159 {
160         if (length == 0) {
161                 /* empty list - no selection */
162                 return {0, 0};
163         } else if (range_selection) {
164                 /* a range selection */
165                 if (range_base < selected) {
166                         return {range_base, selected + 1};
167                 } else {
168                         return {selected, range_base + 1};
169                 }
170         } else {
171                 /* no range, just the cursor */
172                 return {selected, selected + 1};
173         }
174 }
175
176 void
177 ListWindow::MoveCursorNext()
178 {
179         if (selected + 1 < length)
180                 MoveCursor(selected + 1);
181         else if (options.list_wrap)
182                 MoveCursor(0);
183 }
184
185 void
186 ListWindow::MoveCursorPrevious()
187 {
188         if (selected > 0)
189                 MoveCursor(selected - 1);
190         else if (options.list_wrap)
191                 MoveCursor(length - 1);
192 }
193
194 void
195 ListWindow::MoveCursorTop()
196 {
197         if (start == 0)
198                 MoveCursor(start);
199         else
200                 if ((unsigned) options.scroll_offset * 2 >= size.height)
201                         MoveCursor(start + size.height / 2);
202                 else
203                         MoveCursor(start + options.scroll_offset);
204 }
205
206 void
207 ListWindow::MoveCursorMiddle()
208 {
209         if (length >= size.height)
210                 MoveCursor(start + size.height / 2);
211         else
212                 MoveCursor(length / 2);
213 }
214
215 void
216 ListWindow::MoveCursorBottom()
217 {
218         if (length >= size.height)
219                 if ((unsigned) options.scroll_offset * 2 >= size.height)
220                         MoveCursor(start + size.height / 2);
221                 else
222                         if (start + size.height == length)
223                                 MoveCursor(length - 1);
224                         else
225                                 MoveCursor(start + size.height - 1 - options.scroll_offset);
226         else
227                 MoveCursor(length - 1);
228 }
229
230 void
231 ListWindow::MoveCursorFirst()
232 {
233         MoveCursor(0);
234 }
235
236 void
237 ListWindow::MoveCursorLast()
238 {
239         if (length > 0)
240                 MoveCursor(length - 1);
241         else
242                 MoveCursor(0);
243 }
244
245 void
246 ListWindow::MoveCursorNextPage()
247 {
248         if (size.height < 2)
249                 return;
250         if (selected + size.height < length)
251                 MoveCursor(selected + size.height - 1);
252         else
253                 MoveCursorLast();
254 }
255
256 void
257 ListWindow::MoveCursorPreviousPage()
258 {
259         if (size.height < 2)
260                 return;
261         if (selected > size.height - 1)
262                 MoveCursor(selected - size.height + 1);
263         else
264                 MoveCursorFirst();
265 }
266
267 void
268 ListWindow::ScrollUp(unsigned n)
269 {
270         if (start > 0) {
271                 if (n > start)
272                         start = 0;
273                 else
274                         start -= n;
275
276                 FetchCursor();
277         }
278 }
279
280 void
281 ListWindow::ScrollDown(unsigned n)
282 {
283         if (start + size.height < length) {
284                 if (start + size.height + n > length - 1)
285                         start = length - size.height;
286                 else
287                         start += n;
288
289                 FetchCursor();
290         }
291 }
292
293 static void
294 list_window_paint_row(WINDOW *w, unsigned width, bool selected,
295                       const char *text)
296 {
297         row_paint_text(w, width, COLOR_LIST,
298                        selected, text);
299 }
300
301 void
302 ListWindow::Paint(list_window_callback_fn_t callback,
303                   void *callback_data) const
304 {
305         bool show_cursor = !hide_cursor &&
306                 (!options.hardware_cursor || range_selection);
307         ListWindowRange range;
308
309         if (show_cursor)
310                 range = GetRange();
311
312         for (unsigned i = 0; i < size.height; i++) {
313                 wmove(w, i, 0);
314
315                 if (start + i >= length) {
316                         wclrtobot(w);
317                         break;
318                 }
319
320                 const char *label = callback(start + i, callback_data);
321                 assert(label != nullptr);
322
323                 list_window_paint_row(w, size.width,
324                                       show_cursor &&
325                                       range.Contains(start + i),
326                                       label);
327         }
328
329         row_color_end(w);
330
331         if (options.hardware_cursor && selected >= start &&
332             selected < start + size.height) {
333                 curs_set(1);
334                 wmove(w, selected - start, 0);
335         }
336 }
337
338 void
339 ListWindow::Paint(const ListRenderer &renderer) const
340 {
341         bool show_cursor = !hide_cursor &&
342                 (!options.hardware_cursor || range_selection);
343         ListWindowRange range;
344
345         if (show_cursor)
346                 range = GetRange();
347
348         for (unsigned i = 0; i < size.height; i++) {
349                 wmove(w, i, 0);
350
351                 if (start + i >= length) {
352                         wclrtobot(w);
353                         break;
354                 }
355
356                 bool is_selected = show_cursor &&
357                         range.Contains(start + i);
358
359                 renderer.PaintListItem(w, start + i, i, size.width,
360                                        is_selected);
361         }
362
363         if (options.hardware_cursor && selected >= start &&
364             selected < start + size.height) {
365                 curs_set(1);
366                 wmove(w, selected - start, 0);
367         }
368 }
369
370 bool
371 ListWindow::Find(list_window_callback_fn_t callback,
372                  void *callback_data,
373                  const char *str,
374                  bool wrap,
375                  bool bell_on_wrap)
376 {
377         unsigned i = selected + 1;
378
379         assert(str != nullptr);
380
381         do {
382                 while (i < length) {
383                         const char *label = callback(i, callback_data);
384                         assert(label != nullptr);
385
386                         if (match_line(label, str)) {
387                                 MoveCursor(i);
388                                 return true;
389                         }
390                         if (wrap && i == selected)
391                                 return false;
392                         i++;
393                 }
394                 if (wrap) {
395                         if (i == 0) /* empty list */
396                                 return 1;
397                         i=0; /* first item */
398                         if (bell_on_wrap) {
399                                 screen_bell();
400                         }
401                 }
402         } while (wrap);
403
404         return false;
405 }
406
407 bool
408 ListWindow::ReverseFind(list_window_callback_fn_t callback,
409                         void *callback_data,
410                         const char *str,
411                         bool wrap,
412                         bool bell_on_wrap)
413 {
414         int i = selected - 1;
415
416         assert(str != nullptr);
417
418         if (length == 0)
419                 return false;
420
421         do {
422                 while (i >= 0) {
423                         const char *label = callback(i, callback_data);
424                         assert(label != nullptr);
425
426                         if (match_line(label, str)) {
427                                 MoveCursor(i);
428                                 return true;
429                         }
430                         if (wrap && i == (int)selected)
431                                 return false;
432                         i--;
433                 }
434                 if (wrap) {
435                         i = length - 1; /* last item */
436                         if (bell_on_wrap) {
437                                 screen_bell();
438                         }
439                 }
440         } while (wrap);
441
442         return false;
443 }
444
445 #ifdef NCMPC_MINI
446 bool
447 ListWindow::Jump(list_window_callback_fn_t callback,
448                  void *callback_data,
449                  const char *str)
450 {
451         assert(str != nullptr);
452
453         for (unsigned i = 0; i < length; i++) {
454                 const char *label = callback(i, callback_data);
455                 assert(label != nullptr);
456
457                 if (g_ascii_strncasecmp(label, str, strlen(str)) == 0) {
458                         MoveCursor(i);
459                         return true;
460                 }
461         }
462         return false;
463 }
464 #else
465 bool
466 ListWindow::Jump(list_window_callback_fn_t callback,
467                  void *callback_data,
468                  const char *str)
469 {
470         assert(str != nullptr);
471
472         GRegex *regex = compile_regex(str, options.jump_prefix_only);
473         if (regex == nullptr)
474                 return false;
475
476         for (unsigned i = 0; i < length; i++) {
477                 const char *label = callback(i, callback_data);
478                 assert(label != nullptr);
479
480                 if (match_regex(regex, label)) {
481                         g_regex_unref(regex);
482                         MoveCursor(i);
483                         return true;
484                 }
485         }
486         g_regex_unref(regex);
487         return false;
488 }
489 #endif
490
491 /* perform basic list window commands (movement) */
492 bool
493 ListWindow::HandleCommand(command_t cmd)
494 {
495         switch (cmd) {
496         case CMD_LIST_PREVIOUS:
497                 MoveCursorPrevious();
498                 break;
499         case CMD_LIST_NEXT:
500                 MoveCursorNext();
501                 break;
502         case CMD_LIST_TOP:
503                 MoveCursorTop();
504                 break;
505         case CMD_LIST_MIDDLE:
506                 MoveCursorMiddle();
507                 break;
508         case CMD_LIST_BOTTOM:
509                 MoveCursorBottom();
510                 break;
511         case CMD_LIST_FIRST:
512                 MoveCursorFirst();
513                 break;
514         case CMD_LIST_LAST:
515                 MoveCursorLast();
516                 break;
517         case CMD_LIST_NEXT_PAGE:
518                 MoveCursorNextPage();
519                 break;
520         case CMD_LIST_PREVIOUS_PAGE:
521                 MoveCursorPreviousPage();
522                 break;
523         case CMD_LIST_RANGE_SELECT:
524                 if(range_selection)
525                 {
526                         screen_status_printf(_("Range selection disabled"));
527                         SetCursor(selected);
528                 }
529                 else
530                 {
531                         screen_status_printf(_("Range selection enabled"));
532                         range_base = selected;
533                         range_selection = true;
534                 }
535                 break;
536         case CMD_LIST_SCROLL_UP_LINE:
537                 ScrollUp(1);
538                 break;
539         case CMD_LIST_SCROLL_DOWN_LINE:
540                 ScrollDown(1);
541                 break;
542         case CMD_LIST_SCROLL_UP_HALF:
543                 ScrollUp((size.height - 1) / 2);
544                 break;
545         case CMD_LIST_SCROLL_DOWN_HALF:
546                 ScrollDown((size.height - 1) / 2);
547                 break;
548         default:
549                 return false;
550         }
551
552         return true;
553 }
554
555 bool
556 ListWindow::HandleScrollCommand(command_t cmd)
557 {
558         switch (cmd) {
559         case CMD_LIST_SCROLL_UP_LINE:
560         case CMD_LIST_PREVIOUS:
561                 if (start > 0)
562                         start--;
563                 break;
564
565         case CMD_LIST_SCROLL_DOWN_LINE:
566         case CMD_LIST_NEXT:
567                 if (start + size.height < length)
568                         start++;
569                 break;
570
571         case CMD_LIST_FIRST:
572                 start = 0;
573                 break;
574
575         case CMD_LIST_LAST:
576                 if (length > size.height)
577                         start = length - size.height;
578                 else
579                         start = 0;
580                 break;
581
582         case CMD_LIST_NEXT_PAGE:
583                 start += size.height;
584                 if (start + size.height > length) {
585                         if (length > size.height)
586                                 start = length - size.height;
587                         else
588                                 start = 0;
589                 }
590                 break;
591
592         case CMD_LIST_PREVIOUS_PAGE:
593                 if (start > size.height)
594                         start -= size.height;
595                 else
596                         start = 0;
597                 break;
598
599         case CMD_LIST_SCROLL_UP_HALF:
600                 if (start > (size.height - 1) / 2)
601                         start -= (size.height - 1) / 2;
602                 else
603                         start = 0;
604                 break;
605
606         case CMD_LIST_SCROLL_DOWN_HALF:
607                 start += (size.height - 1) / 2;
608                 if (start + size.height > length) {
609                         if (length > size.height)
610                                 start = length - size.height;
611                         else
612                                 start = 0;
613                 }
614                 break;
615
616         default:
617                 return false;
618         }
619
620         return true;
621 }
622
623 #ifdef HAVE_GETMOUSE
624 bool
625 ListWindow::HandleMouse(mmask_t bstate, int y)
626 {
627         /* if the even occurred above the list window move up */
628         if (y < 0) {
629                 if (bstate & BUTTON3_CLICKED)
630                         MoveCursorFirst();
631                 else
632                         MoveCursorPreviousPage();
633                 return true;
634         }
635
636         /* if the even occurred below the list window move down */
637         if ((unsigned)y >= length) {
638                 if (bstate & BUTTON3_CLICKED)
639                         MoveCursorLast();
640                 else
641                         MoveCursorNextPage();
642                 return true;
643         }
644
645         return false;
646 }
647 #endif