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