1 /*
2 Widgets for the Midnight Commander
3
4 Copyright (C) 1994-2021
5 Free Software Foundation, Inc.
6
7 Authors:
8 Radek Doulik, 1994, 1995
9 Miguel de Icaza, 1994, 1995
10 Jakub Jelinek, 1995
11 Andrej Borsenkow, 1996
12 Norbert Warmuth, 1997
13 Andrew Borodin <aborodin@vmail.ru>, 2009, 2010, 2013, 2016
14
15 This file is part of the Midnight Commander.
16
17 The Midnight Commander is free software: you can redistribute it
18 and/or modify it under the terms of the GNU General Public License as
19 published by the Free Software Foundation, either version 3 of the License,
20 or (at your option) any later version.
21
22 The Midnight Commander is distributed in the hope that it will be useful,
23 but WITHOUT ANY WARRANTY; without even the implied warranty of
24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 GNU General Public License for more details.
26
27 You should have received a copy of the GNU General Public License
28 along with this program. If not, see <http://www.gnu.org/licenses/>.
29 */
30
31 /** \file listbox.c
32 * \brief Source: WListbox widget
33 */
34
35 #include <config.h>
36
37 #include <stdlib.h>
38
39 #include "lib/global.h"
40
41 #include "lib/tty/tty.h"
42 #include "lib/skin.h"
43 #include "lib/strutil.h"
44 #include "lib/util.h" /* Q_() */
45 #include "lib/widget.h"
46
47 /*** global variables ****************************************************************************/
48
49 const global_keymap_t *listbox_map = NULL;
50
51 /*** file scope macro definitions ****************************************************************/
52
53 /* Gives the position of the last item. */
54 #define LISTBOX_LAST(l) (listbox_is_empty (l) ? 0 : (int) g_queue_get_length ((l)->list) - 1)
55
56 /*** file scope type declarations ****************************************************************/
57
58 /*** file scope variables ************************************************************************/
59
60 /*** file scope functions ************************************************************************/
61
62 static int
listbox_entry_cmp(const void * a,const void * b,void * user_data)63 listbox_entry_cmp (const void *a, const void *b, void *user_data)
64 {
65 const WLEntry *ea = (const WLEntry *) a;
66 const WLEntry *eb = (const WLEntry *) b;
67
68 (void) user_data;
69
70 return strcmp (ea->text, eb->text);
71 }
72
73 /* --------------------------------------------------------------------------------------------- */
74
75 static void
listbox_entry_free(void * data)76 listbox_entry_free (void *data)
77 {
78 WLEntry *e = data;
79
80 g_free (e->text);
81 if (e->free_data)
82 g_free (e->data);
83 g_free (e);
84 }
85
86 /* --------------------------------------------------------------------------------------------- */
87
88 static void
listbox_drawscroll(WListbox * l)89 listbox_drawscroll (WListbox * l)
90 {
91 Widget *w = WIDGET (l);
92 int max_line = w->lines - 1;
93 int line = 0;
94 int i;
95 int length;
96
97 /* Are we at the top? */
98 widget_gotoyx (w, 0, w->cols);
99 if (l->top == 0)
100 tty_print_one_vline (TRUE);
101 else
102 tty_print_char ('^');
103
104 length = g_queue_get_length (l->list);
105
106 /* Are we at the bottom? */
107 widget_gotoyx (w, max_line, w->cols);
108 if (l->top + w->lines == length || w->lines >= length)
109 tty_print_one_vline (TRUE);
110 else
111 tty_print_char ('v');
112
113 /* Now draw the nice relative pointer */
114 if (!g_queue_is_empty (l->list))
115 line = 1 + ((l->pos * (w->lines - 2)) / length);
116
117 for (i = 1; i < max_line; i++)
118 {
119 widget_gotoyx (w, i, w->cols);
120 if (i != line)
121 tty_print_one_vline (TRUE);
122 else
123 tty_print_char ('*');
124 }
125 }
126
127 /* --------------------------------------------------------------------------------------------- */
128
129 static void
listbox_draw(WListbox * l,gboolean focused)130 listbox_draw (WListbox * l, gboolean focused)
131 {
132 Widget *w = WIDGET (l);
133 const int *colors;
134 gboolean disabled;
135 int normalc, selc;
136 int length = 0;
137 GList *le = NULL;
138 int pos;
139 int i;
140 int sel_line = -1;
141
142 colors = widget_get_colors (w);
143
144 disabled = widget_get_state (w, WST_DISABLED);
145 normalc = disabled ? DISABLED_COLOR : colors[DLG_COLOR_NORMAL];
146 selc = disabled ? DISABLED_COLOR : colors[focused ? DLG_COLOR_HOT_FOCUS : DLG_COLOR_FOCUS];
147
148 if (l->list != NULL)
149 {
150 length = g_queue_get_length (l->list);
151 le = g_queue_peek_nth_link (l->list, (guint) l->top);
152 }
153
154 /* pos = (le == NULL) ? 0 : g_list_position (l->list, le); */
155 pos = (le == NULL) ? 0 : l->top;
156
157 for (i = 0; i < w->lines; i++)
158 {
159 const char *text = "";
160
161 /* Display the entry */
162 if (pos == l->pos && sel_line == -1)
163 {
164 sel_line = i;
165 tty_setcolor (selc);
166 }
167 else
168 tty_setcolor (normalc);
169
170 widget_gotoyx (l, i, 1);
171
172 if (l->list != NULL && le != NULL && (i == 0 || pos < length))
173 {
174 WLEntry *e = LENTRY (le->data);
175
176 text = e->text;
177 le = g_list_next (le);
178 pos++;
179 }
180
181 tty_print_string (str_fit_to_term (text, w->cols - 2, J_LEFT_FIT));
182 }
183
184 l->cursor_y = sel_line;
185
186 if (l->scrollbar && length > w->lines)
187 {
188 tty_setcolor (normalc);
189 listbox_drawscroll (l);
190 }
191 }
192
193 /* --------------------------------------------------------------------------------------------- */
194
195 static int
listbox_check_hotkey(WListbox * l,int key)196 listbox_check_hotkey (WListbox * l, int key)
197 {
198 if (!listbox_is_empty (l))
199 {
200 int i;
201 GList *le;
202
203 for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
204 {
205 WLEntry *e = LENTRY (le->data);
206
207 if (e->hotkey == key)
208 return i;
209 }
210 }
211
212 return (-1);
213 }
214
215 /* --------------------------------------------------------------------------------------------- */
216
217 /* Calculates the item displayed at screen row 'y' (y==0 being the widget's 1st row). */
218 static int
listbox_y_pos(WListbox * l,int y)219 listbox_y_pos (WListbox * l, int y)
220 {
221 return MIN (l->top + y, LISTBOX_LAST (l));
222 }
223
224 /* --------------------------------------------------------------------------------------------- */
225
226 static void
listbox_fwd(WListbox * l,gboolean wrap)227 listbox_fwd (WListbox * l, gboolean wrap)
228 {
229 if (!listbox_is_empty (l))
230 {
231 if ((guint) l->pos + 1 < g_queue_get_length (l->list))
232 listbox_select_entry (l, l->pos + 1);
233 else if (wrap)
234 listbox_select_first (l);
235 }
236 }
237
238 /* --------------------------------------------------------------------------------------------- */
239
240 static void
listbox_fwd_n(WListbox * l,int n)241 listbox_fwd_n (WListbox * l, int n)
242 {
243 listbox_select_entry (l, MIN (l->pos + n, LISTBOX_LAST (l)));
244 }
245
246 /* --------------------------------------------------------------------------------------------- */
247
248 static void
listbox_back(WListbox * l,gboolean wrap)249 listbox_back (WListbox * l, gboolean wrap)
250 {
251 if (!listbox_is_empty (l))
252 {
253 if (l->pos > 0)
254 listbox_select_entry (l, l->pos - 1);
255 else if (wrap)
256 listbox_select_last (l);
257 }
258 }
259
260 /* --------------------------------------------------------------------------------------------- */
261
262 static void
listbox_back_n(WListbox * l,int n)263 listbox_back_n (WListbox * l, int n)
264 {
265 listbox_select_entry (l, MAX (l->pos - n, 0));
266 }
267
268 /* --------------------------------------------------------------------------------------------- */
269
270 static cb_ret_t
listbox_execute_cmd(WListbox * l,long command)271 listbox_execute_cmd (WListbox * l, long command)
272 {
273 cb_ret_t ret = MSG_HANDLED;
274 Widget *w = WIDGET (l);
275
276 if (l->list == NULL || g_queue_is_empty (l->list))
277 return MSG_NOT_HANDLED;
278
279 switch (command)
280 {
281 case CK_Up:
282 listbox_back (l, TRUE);
283 break;
284 case CK_Down:
285 listbox_fwd (l, TRUE);
286 break;
287 case CK_Top:
288 listbox_select_first (l);
289 break;
290 case CK_Bottom:
291 listbox_select_last (l);
292 break;
293 case CK_PageUp:
294 listbox_back_n (l, w->lines - 1);
295 break;
296 case CK_PageDown:
297 listbox_fwd_n (l, w->lines - 1);
298 break;
299 case CK_Delete:
300 if (l->deletable)
301 {
302 gboolean is_last, is_more;
303 int length;
304
305 length = g_queue_get_length (l->list);
306
307 is_last = (l->pos + 1 >= length);
308 is_more = (l->top + w->lines >= length);
309
310 listbox_remove_current (l);
311 if ((l->top > 0) && (is_last || is_more))
312 l->top--;
313 }
314 break;
315 case CK_Clear:
316 if (l->deletable && mc_global.widget.confirm_history_cleanup
317 /* TRANSLATORS: no need to translate 'DialogTitle', it's just a context prefix */
318 && (query_dialog (Q_ ("DialogTitle|History cleanup"),
319 _("Do you want clean this history?"),
320 D_ERROR, 2, _("&Yes"), _("&No")) == 0))
321 listbox_remove_list (l);
322 break;
323 case CK_View:
324 case CK_Edit:
325 case CK_Enter:
326 ret = send_message (WIDGET (l)->owner, l, MSG_NOTIFY, command, NULL);
327 break;
328 default:
329 ret = MSG_NOT_HANDLED;
330 }
331
332 return ret;
333 }
334
335 /* --------------------------------------------------------------------------------------------- */
336
337 /* Return MSG_HANDLED if we want a redraw */
338 static cb_ret_t
listbox_key(WListbox * l,int key)339 listbox_key (WListbox * l, int key)
340 {
341 long command;
342
343 if (l->list == NULL)
344 return MSG_NOT_HANDLED;
345
346 /* focus on listbox item N by '0'..'9' keys */
347 if (key >= '0' && key <= '9')
348 {
349 listbox_select_entry (l, key - '0');
350 return MSG_HANDLED;
351 }
352
353 command = widget_lookup_key (WIDGET (l), key);
354 if (command == CK_IgnoreKey)
355 return MSG_NOT_HANDLED;
356 return listbox_execute_cmd (l, command);
357 }
358
359 /* --------------------------------------------------------------------------------------------- */
360
361 /* Listbox item adding function */
362 static inline void
listbox_append_item(WListbox * l,WLEntry * e,listbox_append_t pos)363 listbox_append_item (WListbox * l, WLEntry * e, listbox_append_t pos)
364 {
365 if (l->list == NULL)
366 {
367 l->list = g_queue_new ();
368 pos = LISTBOX_APPEND_AT_END;
369 }
370
371 switch (pos)
372 {
373 case LISTBOX_APPEND_AT_END:
374 g_queue_push_tail (l->list, e);
375 break;
376
377 case LISTBOX_APPEND_BEFORE:
378 g_queue_insert_before (l->list, g_queue_peek_nth_link (l->list, (guint) l->pos), e);
379 break;
380
381 case LISTBOX_APPEND_AFTER:
382 g_queue_insert_after (l->list, g_queue_peek_nth_link (l->list, (guint) l->pos), e);
383 break;
384
385 case LISTBOX_APPEND_SORTED:
386 g_queue_insert_sorted (l->list, e, (GCompareDataFunc) listbox_entry_cmp, NULL);
387 break;
388
389 default:
390 break;
391 }
392 }
393
394 /* --------------------------------------------------------------------------------------------- */
395
396 /* Call this whenever the user changes the selected item. */
397 static void
listbox_on_change(WListbox * l)398 listbox_on_change (WListbox * l)
399 {
400 listbox_draw (l, TRUE);
401 send_message (WIDGET (l)->owner, l, MSG_NOTIFY, 0, NULL);
402 }
403
404 /* --------------------------------------------------------------------------------------------- */
405
406 static void
listbox_do_action(WListbox * l)407 listbox_do_action (WListbox * l)
408 {
409 int action;
410
411 if (listbox_is_empty (l))
412 return;
413
414 if (l->callback != NULL)
415 action = l->callback (l);
416 else
417 action = LISTBOX_DONE;
418
419 if (action == LISTBOX_DONE)
420 {
421 WDialog *h = DIALOG (WIDGET (l)->owner);
422
423 h->ret_value = B_ENTER;
424 dlg_stop (h);
425 }
426 }
427
428 /* --------------------------------------------------------------------------------------------- */
429
430 static void
listbox_run_hotkey(WListbox * l,int pos)431 listbox_run_hotkey (WListbox * l, int pos)
432 {
433 listbox_select_entry (l, pos);
434 listbox_on_change (l);
435 listbox_do_action (l);
436 }
437
438 /* --------------------------------------------------------------------------------------------- */
439
440 static inline void
listbox_destroy(WListbox * l)441 listbox_destroy (WListbox * l)
442 {
443 listbox_remove_list (l);
444 }
445
446 /* --------------------------------------------------------------------------------------------- */
447
448 static cb_ret_t
listbox_callback(Widget * w,Widget * sender,widget_msg_t msg,int parm,void * data)449 listbox_callback (Widget * w, Widget * sender, widget_msg_t msg, int parm, void *data)
450 {
451 WListbox *l = LISTBOX (w);
452
453 switch (msg)
454 {
455 case MSG_HOTKEY:
456 {
457 int pos;
458
459 pos = listbox_check_hotkey (l, parm);
460 if (pos < 0)
461 return MSG_NOT_HANDLED;
462
463 listbox_run_hotkey (l, pos);
464
465 return MSG_HANDLED;
466 }
467
468 case MSG_KEY:
469 {
470 cb_ret_t ret_code;
471
472 ret_code = listbox_key (l, parm);
473 if (ret_code != MSG_NOT_HANDLED)
474 listbox_on_change (l);
475 return ret_code;
476 }
477
478 case MSG_ACTION:
479 return listbox_execute_cmd (l, parm);
480
481 case MSG_CURSOR:
482 widget_gotoyx (l, l->cursor_y, 0);
483 return MSG_HANDLED;
484
485 case MSG_DRAW:
486 listbox_draw (l, widget_get_state (w, WST_FOCUSED));
487 return MSG_HANDLED;
488
489 case MSG_DESTROY:
490 listbox_destroy (l);
491 return MSG_HANDLED;
492
493 default:
494 return widget_default_callback (w, sender, msg, parm, data);
495 }
496 }
497
498 /* --------------------------------------------------------------------------------------------- */
499
500 static void
listbox_mouse_callback(Widget * w,mouse_msg_t msg,mouse_event_t * event)501 listbox_mouse_callback (Widget * w, mouse_msg_t msg, mouse_event_t * event)
502 {
503 WListbox *l = LISTBOX (w);
504 int old_pos;
505
506 old_pos = l->pos;
507
508 switch (msg)
509 {
510 case MSG_MOUSE_DOWN:
511 widget_select (w);
512 listbox_select_entry (l, listbox_y_pos (l, event->y));
513 break;
514
515 case MSG_MOUSE_SCROLL_UP:
516 listbox_back (l, FALSE);
517 break;
518
519 case MSG_MOUSE_SCROLL_DOWN:
520 listbox_fwd (l, FALSE);
521 break;
522
523 case MSG_MOUSE_DRAG:
524 event->result.repeat = TRUE; /* It'd be functional even without this. */
525 listbox_select_entry (l, listbox_y_pos (l, event->y));
526 break;
527
528 case MSG_MOUSE_CLICK:
529 /* We don't call listbox_select_entry() here: MSG_MOUSE_DOWN/DRAG did this already. */
530 if (event->count == GPM_DOUBLE) /* Double click */
531 listbox_do_action (l);
532 break;
533
534 default:
535 break;
536 }
537
538 /* If the selection has changed, we redraw the widget and notify the dialog. */
539 if (l->pos != old_pos)
540 listbox_on_change (l);
541 }
542
543 /* --------------------------------------------------------------------------------------------- */
544 /*** public functions ****************************************************************************/
545 /* --------------------------------------------------------------------------------------------- */
546
547 WListbox *
listbox_new(int y,int x,int height,int width,gboolean deletable,lcback_fn callback)548 listbox_new (int y, int x, int height, int width, gboolean deletable, lcback_fn callback)
549 {
550 WListbox *l;
551 Widget *w;
552
553 if (height <= 0)
554 height = 1;
555
556 l = g_new (WListbox, 1);
557 w = WIDGET (l);
558 widget_init (w, y, x, height, width, listbox_callback, listbox_mouse_callback);
559 w->options |= WOP_SELECTABLE | WOP_WANT_HOTKEY;
560 w->keymap = listbox_map;
561
562 l->list = NULL;
563 l->top = l->pos = 0;
564 l->deletable = deletable;
565 l->callback = callback;
566 l->allow_duplicates = TRUE;
567 l->scrollbar = !mc_global.tty.slow_terminal;
568
569 return l;
570 }
571
572 /* --------------------------------------------------------------------------------------------- */
573
574 /**
575 * Finds item by its label.
576 */
577 int
listbox_search_text(WListbox * l,const char * text)578 listbox_search_text (WListbox * l, const char *text)
579 {
580 if (!listbox_is_empty (l))
581 {
582 int i;
583 GList *le;
584
585 for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
586 {
587 WLEntry *e = LENTRY (le->data);
588
589 if (strcmp (e->text, text) == 0)
590 return i;
591 }
592 }
593
594 return (-1);
595 }
596
597 /* --------------------------------------------------------------------------------------------- */
598
599 /**
600 * Finds item by its 'data' slot.
601 */
602 int
listbox_search_data(WListbox * l,const void * data)603 listbox_search_data (WListbox * l, const void *data)
604 {
605 if (!listbox_is_empty (l))
606 {
607 int i;
608 GList *le;
609
610 for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
611 {
612 WLEntry *e = LENTRY (le->data);
613
614 if (e->data == data)
615 return i;
616 }
617 }
618
619 return (-1);
620 }
621
622 /* --------------------------------------------------------------------------------------------- */
623
624 /* Selects the first entry and scrolls the list to the top */
625 void
listbox_select_first(WListbox * l)626 listbox_select_first (WListbox * l)
627 {
628 l->pos = l->top = 0;
629 }
630
631 /* --------------------------------------------------------------------------------------------- */
632
633 /* Selects the last entry and scrolls the list to the bottom */
634 void
listbox_select_last(WListbox * l)635 listbox_select_last (WListbox * l)
636 {
637 int lines = WIDGET (l)->lines;
638 int length;
639
640 length = listbox_get_length (l);
641
642 l->pos = DOZ (length, 1);
643 l->top = DOZ (length, lines);
644 }
645
646 /* --------------------------------------------------------------------------------------------- */
647
648 void
listbox_select_entry(WListbox * l,int dest)649 listbox_select_entry (WListbox * l, int dest)
650 {
651 GList *le;
652 int pos;
653 gboolean top_seen = FALSE;
654
655 if (listbox_is_empty (l) || dest < 0)
656 return;
657
658 /* Special case */
659 for (pos = 0, le = g_queue_peek_head_link (l->list); le != NULL; pos++, le = g_list_next (le))
660 {
661 if (pos == l->top)
662 top_seen = TRUE;
663
664 if (pos == dest)
665 {
666 l->pos = dest;
667 if (!top_seen)
668 l->top = l->pos;
669 else
670 {
671 int lines = WIDGET (l)->lines;
672
673 if (l->pos - l->top >= lines)
674 l->top = l->pos - lines + 1;
675 }
676 return;
677 }
678 }
679
680 /* If we are unable to find it, set decent values */
681 l->pos = l->top = 0;
682 }
683
684 /* --------------------------------------------------------------------------------------------- */
685
686 int
listbox_get_length(const WListbox * l)687 listbox_get_length (const WListbox * l)
688 {
689 return listbox_is_empty (l) ? 0 : (int) g_queue_get_length (l->list);
690 }
691
692 /* --------------------------------------------------------------------------------------------- */
693
694 /* Returns the current string text as well as the associated extra data */
695 void
listbox_get_current(WListbox * l,char ** string,void ** extra)696 listbox_get_current (WListbox * l, char **string, void **extra)
697 {
698 WLEntry *e = NULL;
699 gboolean ok;
700
701 if (l != NULL)
702 e = listbox_get_nth_item (l, l->pos);
703
704 ok = (e != NULL);
705
706 if (string != NULL)
707 *string = ok ? e->text : NULL;
708
709 if (extra != NULL)
710 *extra = ok ? e->data : NULL;
711 }
712
713 /* --------------------------------------------------------------------------------------------- */
714
715 WLEntry *
listbox_get_nth_item(const WListbox * l,int pos)716 listbox_get_nth_item (const WListbox * l, int pos)
717 {
718 if (!listbox_is_empty (l) && pos >= 0)
719 {
720 GList *item;
721
722 item = g_queue_peek_nth_link (l->list, (guint) pos);
723 if (item != NULL)
724 return LENTRY (item->data);
725 }
726
727 return NULL;
728 }
729
730 /* --------------------------------------------------------------------------------------------- */
731
732 GList *
listbox_get_first_link(const WListbox * l)733 listbox_get_first_link (const WListbox * l)
734 {
735 return (l == NULL || l->list == NULL) ? NULL : g_queue_peek_head_link (l->list);
736 }
737
738 /* --------------------------------------------------------------------------------------------- */
739
740 void
listbox_remove_current(WListbox * l)741 listbox_remove_current (WListbox * l)
742 {
743 if (!listbox_is_empty (l))
744 {
745 GList *current;
746 int length;
747
748 current = g_queue_peek_nth_link (l->list, (guint) l->pos);
749 listbox_entry_free (current->data);
750 g_queue_delete_link (l->list, current);
751
752 length = g_queue_get_length (l->list);
753
754 if (length == 0)
755 l->top = l->pos = 0;
756 else if (l->pos >= length)
757 l->pos = length - 1;
758 }
759 }
760
761 /* --------------------------------------------------------------------------------------------- */
762
763 gboolean
listbox_is_empty(const WListbox * l)764 listbox_is_empty (const WListbox * l)
765 {
766 return (l == NULL || l->list == NULL || g_queue_is_empty (l->list));
767 }
768
769 /* --------------------------------------------------------------------------------------------- */
770
771 /**
772 * Set new listbox items list.
773 *
774 * @param l WListbox object
775 * @param list list of WLEntry objects
776 */
777 void
listbox_set_list(WListbox * l,GQueue * list)778 listbox_set_list (WListbox * l, GQueue * list)
779 {
780 listbox_remove_list (l);
781
782 if (l != NULL)
783 l->list = list;
784 }
785
786 /* --------------------------------------------------------------------------------------------- */
787
788 void
listbox_remove_list(WListbox * l)789 listbox_remove_list (WListbox * l)
790 {
791 if (l != NULL)
792 {
793 if (l->list != NULL)
794 {
795 g_queue_free_full (l->list, (GDestroyNotify) listbox_entry_free);
796 l->list = NULL;
797 }
798
799 l->pos = l->top = 0;
800 }
801 }
802
803 /* --------------------------------------------------------------------------------------------- */
804
805 char *
listbox_add_item(WListbox * l,listbox_append_t pos,int hotkey,const char * text,void * data,gboolean free_data)806 listbox_add_item (WListbox * l, listbox_append_t pos, int hotkey, const char *text, void *data,
807 gboolean free_data)
808 {
809 WLEntry *entry;
810
811 if (l == NULL)
812 return NULL;
813
814 if (!l->allow_duplicates && (listbox_search_text (l, text) >= 0))
815 return NULL;
816
817 entry = g_new (WLEntry, 1);
818 entry->text = g_strdup (text);
819 entry->data = data;
820 entry->free_data = free_data;
821 entry->hotkey = hotkey;
822
823 listbox_append_item (l, entry, pos);
824
825 return entry->text;
826 }
827
828 /* --------------------------------------------------------------------------------------------- */
829