f4142c1c8db206ae7369f2ce279ba4788d1867d2
[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 "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 "screen_utils.hxx"
30 #include "i18n.h"
31
32 #include <assert.h>
33 #include <stdlib.h>
34 #include <unistd.h>
35 #include <string.h>
36
37 void
38 ListWindow::Reset()
39 {
40         selected = 0;
41         range_selection = false;
42         range_base = 0;
43         start = 0;
44 }
45
46 unsigned
47 ListWindow::ValidateIndex(unsigned i) const
48 {
49         if (length == 0)
50                 return 0;
51         else if (i >= length)
52                 return length - 1;
53         else
54                 return i;
55 }
56
57 void
58 ListWindow::CheckSelected()
59 {
60         selected = ValidateIndex(selected);
61
62         if (range_selection)
63                 range_base = ValidateIndex(range_base);
64 }
65
66 void
67 ListWindow::Resize(Size _size)
68 {
69         size = _size;
70         CheckOrigin();
71 }
72
73 void
74 ListWindow::SetLength(unsigned _length)
75 {
76         if (_length == length)
77                 return;
78
79         length = _length;
80
81         CheckSelected();
82         CheckOrigin();
83 }
84
85 void
86 ListWindow::Center(unsigned n)
87 {
88         if (n > size.height / 2)
89                 start = n - size.height / 2;
90         else
91                 start = 0;
92
93         if (start + size.height > length) {
94                 if (size.height < length)
95                         start = length - size.height;
96                 else
97                         start = 0;
98         }
99 }
100
101 void
102 ListWindow::ScrollTo(unsigned n)
103 {
104         int new_start = start;
105
106         if ((unsigned) options.scroll_offset * 2 >= size.height)
107                 // Center if the offset is more than half the screen
108                 new_start = n - size.height / 2;
109         else {
110                 if (n < start + options.scroll_offset)
111                         new_start = n - options.scroll_offset;
112
113                 if (n >= start + size.height - options.scroll_offset)
114                         new_start = n - size.height + 1 + options.scroll_offset;
115         }
116
117         if (new_start + size.height > length)
118                 new_start = length - size.height;
119
120         if (new_start < 0 || length == 0)
121                 new_start = 0;
122
123         start = new_start;
124 }
125
126 void
127 ListWindow::SetCursor(unsigned i)
128 {
129         range_selection = false;
130         selected = i;
131
132         CheckSelected();
133         CheckOrigin();
134 }
135
136 void
137 ListWindow::MoveCursor(unsigned n)
138 {
139         selected = n;
140
141         CheckSelected();
142         CheckOrigin();
143 }
144
145 void
146 ListWindow::FetchCursor()
147 {
148         if (start > 0 &&
149             selected < start + options.scroll_offset)
150                 MoveCursor(start + options.scroll_offset);
151         else if (start + size.height < length &&
152                  selected > start + size.height - 1 - options.scroll_offset)
153                 MoveCursor(start + size.height - 1 - options.scroll_offset);
154 }
155
156 ListWindowRange
157 ListWindow::GetRange() const
158 {
159         if (length == 0) {
160                 /* empty list - no selection */
161                 return {0, 0};
162         } else if (range_selection) {
163                 /* a range selection */
164                 if (range_base < selected) {
165                         return {range_base, selected + 1};
166                 } else {
167                         return {selected, range_base + 1};
168                 }
169         } else {
170                 /* no range, just the cursor */
171                 return {selected, selected + 1};
172         }
173 }
174
175 void
176 ListWindow::MoveCursorNext()
177 {
178         if (selected + 1 < length)
179                 MoveCursor(selected + 1);
180         else if (options.list_wrap)
181                 MoveCursor(0);
182 }
183
184 void
185 ListWindow::MoveCursorPrevious()
186 {
187         if (selected > 0)
188                 MoveCursor(selected - 1);
189         else if (options.list_wrap)
190                 MoveCursor(length - 1);
191 }
192
193 void
194 ListWindow::MoveCursorTop()
195 {
196         if (start == 0)
197                 MoveCursor(start);
198         else
199                 if ((unsigned) options.scroll_offset * 2 >= size.height)
200                         MoveCursor(start + size.height / 2);
201                 else
202                         MoveCursor(start + options.scroll_offset);
203 }
204
205 void
206 ListWindow::MoveCursorMiddle()
207 {
208         if (length >= size.height)
209                 MoveCursor(start + size.height / 2);
210         else
211                 MoveCursor(length / 2);
212 }
213
214 void
215 ListWindow::MoveCursorBottom()
216 {
217         if (length >= size.height)
218                 if ((unsigned) options.scroll_offset * 2 >= size.height)
219                         MoveCursor(start + size.height / 2);
220                 else
221                         if (start + size.height == length)
222                                 MoveCursor(length - 1);
223                         else
224                                 MoveCursor(start + size.height - 1 - options.scroll_offset);
225         else
226                 MoveCursor(length - 1);
227 }
228
229 void
230 ListWindow::MoveCursorFirst()
231 {
232         MoveCursor(0);
233 }
234
235 void
236 ListWindow::MoveCursorLast()
237 {
238         if (length > 0)
239                 MoveCursor(length - 1);
240         else
241                 MoveCursor(0);
242 }
243
244 void
245 ListWindow::MoveCursorNextPage()
246 {
247         if (size.height < 2)
248                 return;
249         if (selected + size.height < length)
250                 MoveCursor(selected + size.height - 1);
251         else
252                 MoveCursorLast();
253 }
254
255 void
256 ListWindow::MoveCursorPreviousPage()
257 {
258         if (size.height < 2)
259                 return;
260         if (selected > size.height - 1)
261                 MoveCursor(selected - size.height + 1);
262         else
263                 MoveCursorFirst();
264 }
265
266 void
267 ListWindow::ScrollUp(unsigned n)
268 {
269         if (start > 0) {
270                 if (n > start)
271                         start = 0;
272                 else
273                         start -= n;
274
275                 FetchCursor();
276         }
277 }
278
279 void
280 ListWindow::ScrollDown(unsigned n)
281 {
282         if (start + size.height < length) {
283                 if (start + size.height + n > length - 1)
284                         start = length - size.height;
285                 else
286                         start += n;
287
288                 FetchCursor();
289         }
290 }
291
292 static void
293 list_window_paint_row(WINDOW *w, unsigned width, bool selected,
294                       const char *text)
295 {
296         row_paint_text(w, width, COLOR_LIST,
297                        selected, text);
298 }
299
300 void
301 ListWindow::Paint(list_window_callback_fn_t callback,
302                   void *callback_data) const
303 {
304         bool show_cursor = !hide_cursor &&
305                 (!options.hardware_cursor || range_selection);
306         ListWindowRange range;
307
308         if (show_cursor)
309                 range = GetRange();
310
311         for (unsigned i = 0; i < size.height; i++) {
312                 wmove(w, i, 0);
313
314                 if (start + i >= length) {
315                         wclrtobot(w);
316                         break;
317                 }
318
319                 const char *label = callback(start + i, callback_data);
320                 assert(label != nullptr);
321
322                 list_window_paint_row(w, size.width,
323                                       show_cursor &&
324                                       range.Contains(start + i),
325                                       label);
326         }
327
328         row_color_end(w);
329
330         if (options.hardware_cursor && selected >= start &&
331             selected < start + size.height) {
332                 curs_set(1);
333                 wmove(w, selected - start, 0);
334         }
335 }
336
337 void
338 ListWindow::Paint(list_window_paint_callback_t paint_callback,
339                   const void *callback_data) 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                 paint_callback(w, start + i, i, size.width,
360                                is_selected, callback_data);
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