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