Ticket #3569: various fixups in WListbox engine.
[midnight-commander.git] / lib / widget / listbox.c
blobde98046044e05de1fa1fc2dedef35270c7d72421
1 /*
2 Widgets for the Midnight Commander
4 Copyright (C) 1994-2016
5 Free Software Foundation, Inc.
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
15 This file is part of the Midnight Commander.
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.
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.
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/>.
31 /** \file listbox.c
32 * \brief Source: WListbox widget
35 #include <config.h>
37 #include <stdlib.h>
39 #include "lib/global.h"
41 #include "lib/tty/tty.h"
42 #include "lib/tty/mouse.h"
43 #include "lib/skin.h"
44 #include "lib/strutil.h"
45 #include "lib/util.h" /* Q_() */
46 #include "lib/keybind.h" /* global_keymap_t */
47 #include "lib/widget.h"
49 /*** global variables ****************************************************************************/
51 const global_keymap_t *listbox_map = NULL;
53 /*** file scope macro definitions ****************************************************************/
55 /* Gives the position of the last item. */
56 #define LISTBOX_LAST(l) (g_queue_is_empty ((l)->list) ? 0 : (int) g_queue_get_length ((l)->list) - 1)
58 /*** file scope type declarations ****************************************************************/
60 /*** file scope variables ************************************************************************/
62 /*** file scope functions ************************************************************************/
64 static int
65 listbox_entry_cmp (const void *a, const void *b, void *user_data)
67 const WLEntry *ea = (const WLEntry *) a;
68 const WLEntry *eb = (const WLEntry *) b;
70 (void) user_data;
72 return strcmp (ea->text, eb->text);
75 /* --------------------------------------------------------------------------------------------- */
77 static void
78 listbox_entry_free (void *data)
80 WLEntry *e = data;
82 g_free (e->text);
83 if (e->free_data)
84 g_free (e->data);
85 g_free (e);
88 /* --------------------------------------------------------------------------------------------- */
90 static void
91 listbox_drawscroll (WListbox * l)
93 Widget *w = WIDGET (l);
94 int max_line = w->lines - 1;
95 int line = 0;
96 int i;
97 int length;
99 /* Are we at the top? */
100 widget_move (w, 0, w->cols);
101 if (l->top == 0)
102 tty_print_one_vline (TRUE);
103 else
104 tty_print_char ('^');
106 length = g_queue_get_length (l->list);
108 /* Are we at the bottom? */
109 widget_move (w, max_line, w->cols);
110 if (l->top + w->lines == length || w->lines >= length)
111 tty_print_one_vline (TRUE);
112 else
113 tty_print_char ('v');
115 /* Now draw the nice relative pointer */
116 if (!g_queue_is_empty (l->list))
117 line = 1 + ((l->pos * (w->lines - 2)) / length);
119 for (i = 1; i < max_line; i++)
121 widget_move (w, i, w->cols);
122 if (i != line)
123 tty_print_one_vline (TRUE);
124 else
125 tty_print_char ('*');
129 /* --------------------------------------------------------------------------------------------- */
131 static void
132 listbox_draw (WListbox * l, gboolean focused)
134 Widget *w = WIDGET (l);
135 const WDialog *h = w->owner;
136 const gboolean disabled = (w->options & W_DISABLED) != 0;
137 const int normalc = disabled ? DISABLED_COLOR : h->color[DLG_COLOR_NORMAL];
138 /* *INDENT-OFF* */
139 int selc = disabled
140 ? DISABLED_COLOR
141 : focused
142 ? h->color[DLG_COLOR_HOT_FOCUS]
143 : h->color[DLG_COLOR_FOCUS];
144 /* *INDENT-ON* */
146 int length = 0;
147 GList *le = NULL;
148 int pos;
149 int i;
150 int sel_line = -1;
152 if (l->list != NULL)
154 length = g_queue_get_length (l->list);
155 le = g_queue_peek_nth_link (l->list, (guint) l->top);
158 /* pos = (le == NULL) ? 0 : g_list_position (l->list, le); */
159 pos = (le == NULL) ? 0 : l->top;
161 for (i = 0; i < w->lines; i++)
163 const char *text = "";
165 /* Display the entry */
166 if (pos == l->pos && sel_line == -1)
168 sel_line = i;
169 tty_setcolor (selc);
171 else
172 tty_setcolor (normalc);
174 widget_move (l, i, 1);
176 if (l->list != NULL && le != NULL && (i == 0 || pos < length))
178 WLEntry *e = LENTRY (le->data);
180 text = e->text;
181 le = g_list_next (le);
182 pos++;
185 tty_print_string (str_fit_to_term (text, w->cols - 2, J_LEFT_FIT));
188 l->cursor_y = sel_line;
190 if (l->scrollbar && length > w->lines)
192 tty_setcolor (normalc);
193 listbox_drawscroll (l);
197 /* --------------------------------------------------------------------------------------------- */
199 static int
200 listbox_check_hotkey (WListbox * l, int key)
202 if (!listbox_is_empty (l))
204 int i;
205 GList *le;
207 for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
209 WLEntry *e = LENTRY (le->data);
211 if (e->hotkey == key)
212 return i;
216 return (-1);
219 /* --------------------------------------------------------------------------------------------- */
221 /* Calculates the item displayed at screen row 'y' (y==0 being the widget's 1st row). */
222 static int
223 listbox_y_pos (WListbox * l, int y)
225 return min (l->top + y, LISTBOX_LAST (l));
228 /* --------------------------------------------------------------------------------------------- */
230 static void
231 listbox_fwd (WListbox * l, gboolean wrap)
233 if ((guint) l->pos + 1 < g_queue_get_length (l->list))
234 listbox_select_entry (l, l->pos + 1);
235 else if (wrap)
236 listbox_select_first (l);
239 /* --------------------------------------------------------------------------------------------- */
241 static void
242 listbox_fwd_n (WListbox * l, int n)
244 listbox_select_entry (l, min (l->pos + n, LISTBOX_LAST (l)));
247 /* --------------------------------------------------------------------------------------------- */
249 static void
250 listbox_back (WListbox * l, gboolean wrap)
252 if (l->pos > 0)
253 listbox_select_entry (l, l->pos - 1);
254 else if (wrap)
255 listbox_select_last (l);
258 /* --------------------------------------------------------------------------------------------- */
260 static void
261 listbox_back_n (WListbox * l, int n)
263 listbox_select_entry (l, max (l->pos - n, 0));
266 /* --------------------------------------------------------------------------------------------- */
268 static cb_ret_t
269 listbox_execute_cmd (WListbox * l, long command)
271 cb_ret_t ret = MSG_HANDLED;
272 Widget *w = WIDGET (l);
274 if (l->list == NULL || g_queue_is_empty (l->list))
275 return MSG_NOT_HANDLED;
277 switch (command)
279 case CK_Up:
280 listbox_back (l, TRUE);
281 break;
282 case CK_Down:
283 listbox_fwd (l, TRUE);
284 break;
285 case CK_Top:
286 listbox_select_first (l);
287 break;
288 case CK_Bottom:
289 listbox_select_last (l);
290 break;
291 case CK_PageUp:
292 listbox_back_n (l, w->lines - 1);
293 break;
294 case CK_PageDown:
295 listbox_fwd_n (l, w->lines - 1);
296 break;
297 case CK_Delete:
298 if (l->deletable)
300 gboolean is_last, is_more;
301 int length;
303 length = g_queue_get_length (l->list);
305 is_last = (l->pos + 1 >= length);
306 is_more = (l->top + w->lines >= length);
308 listbox_remove_current (l);
309 if ((l->top > 0) && (is_last || is_more))
310 l->top--;
312 break;
313 case CK_Clear:
314 if (l->deletable && mc_global.widget.confirm_history_cleanup
315 /* TRANSLATORS: no need to translate 'DialogTitle', it's just a context prefix */
316 && (query_dialog (Q_ ("DialogTitle|History cleanup"),
317 _("Do you want clean this history?"),
318 D_ERROR, 2, _("&Yes"), _("&No")) == 0))
319 listbox_remove_list (l);
320 break;
321 default:
322 ret = MSG_NOT_HANDLED;
325 return ret;
328 /* --------------------------------------------------------------------------------------------- */
330 /* Return MSG_HANDLED if we want a redraw */
331 static cb_ret_t
332 listbox_key (WListbox * l, int key)
334 long command;
336 if (l->list == NULL)
337 return MSG_NOT_HANDLED;
339 /* focus on listbox item N by '0'..'9' keys */
340 if (key >= '0' && key <= '9')
342 listbox_select_entry (l, key - '0');
343 return MSG_HANDLED;
346 command = keybind_lookup_keymap_command (listbox_map, key);
347 if (command == CK_IgnoreKey)
348 return MSG_NOT_HANDLED;
349 return listbox_execute_cmd (l, command);
352 /* --------------------------------------------------------------------------------------------- */
354 /* Listbox item adding function */
355 static inline void
356 listbox_append_item (WListbox * l, WLEntry * e, listbox_append_t pos)
358 if (l->list == NULL)
360 l->list = g_queue_new ();
361 pos = LISTBOX_APPEND_AT_END;
364 switch (pos)
366 case LISTBOX_APPEND_AT_END:
367 g_queue_push_tail (l->list, e);
368 break;
370 case LISTBOX_APPEND_BEFORE:
371 g_queue_insert_before (l->list, g_queue_peek_nth_link (l->list, (guint) l->pos), e);
372 break;
374 case LISTBOX_APPEND_AFTER:
375 g_queue_insert_after (l->list, g_queue_peek_nth_link (l->list, (guint) l->pos), e);
376 break;
378 case LISTBOX_APPEND_SORTED:
379 g_queue_insert_sorted (l->list, e, (GCompareDataFunc) listbox_entry_cmp, NULL);
380 break;
382 default:
383 break;
387 /* --------------------------------------------------------------------------------------------- */
389 /* Call this whenever the user changes the selected item. */
390 static void
391 listbox_on_change (WListbox * l)
393 listbox_draw (l, TRUE);
394 send_message (WIDGET (l)->owner, l, MSG_ACTION, l->pos, NULL);
397 /* --------------------------------------------------------------------------------------------- */
399 static void
400 listbox_run_hotkey (WListbox * l, int pos)
402 WDialog *h = WIDGET (l)->owner;
403 int action;
405 listbox_select_entry (l, pos);
406 listbox_on_change (l);
408 if (l->callback != NULL)
409 action = l->callback (l);
410 else
411 action = LISTBOX_DONE;
413 if (action == LISTBOX_DONE)
415 h->ret_value = B_ENTER;
416 dlg_stop (h);
420 /* --------------------------------------------------------------------------------------------- */
422 static inline void
423 listbox_destroy (WListbox * l)
425 listbox_remove_list (l);
428 /* --------------------------------------------------------------------------------------------- */
430 static cb_ret_t
431 listbox_callback (Widget * w, Widget * sender, widget_msg_t msg, int parm, void *data)
433 WListbox *l = LISTBOX (w);
434 cb_ret_t ret_code;
436 switch (msg)
438 case MSG_INIT:
439 return MSG_HANDLED;
441 case MSG_HOTKEY:
443 int pos;
445 pos = listbox_check_hotkey (l, parm);
446 if (pos < 0)
447 return MSG_NOT_HANDLED;
449 listbox_run_hotkey (l, pos);
451 return MSG_HANDLED;
454 case MSG_KEY:
455 ret_code = listbox_key (l, parm);
456 if (ret_code != MSG_NOT_HANDLED)
457 listbox_on_change (l);
458 return ret_code;
460 case MSG_ACTION:
461 return listbox_execute_cmd (l, parm);
463 case MSG_CURSOR:
464 widget_move (l, l->cursor_y, 0);
465 return MSG_HANDLED;
467 case MSG_FOCUS:
468 case MSG_UNFOCUS:
469 case MSG_DRAW:
470 listbox_draw (l, msg != MSG_UNFOCUS);
471 return MSG_HANDLED;
473 case MSG_DESTROY:
474 listbox_destroy (l);
475 return MSG_HANDLED;
477 case MSG_RESIZE:
478 return MSG_HANDLED;
480 default:
481 return widget_default_callback (w, sender, msg, parm, data);
485 /* --------------------------------------------------------------------------------------------- */
487 static int
488 listbox_event (Gpm_Event * event, void *data)
490 WListbox *l = LISTBOX (data);
491 Widget *w = WIDGET (data);
493 if (!mouse_global_in_widget (event, w))
494 return MOU_UNHANDLED;
496 /* Single click */
497 if ((event->type & GPM_DOWN) != 0)
498 dlg_select_widget (l);
500 if (listbox_is_empty (l))
501 return MOU_NORMAL;
503 if ((event->type & (GPM_DOWN | GPM_DRAG)) != 0)
505 int ret = MOU_REPEAT;
506 Gpm_Event local;
508 local = mouse_get_local (event, w);
509 if (local.y < 1)
510 listbox_back_n (l, -local.y + 1);
511 else if (local.y > w->lines)
512 listbox_fwd_n (l, local.y - w->lines);
513 else if ((local.buttons & GPM_B_UP) != 0)
515 listbox_back (l, FALSE);
516 ret = MOU_NORMAL;
518 else if ((local.buttons & GPM_B_DOWN) != 0)
520 listbox_fwd (l, FALSE);
521 ret = MOU_NORMAL;
523 else
524 listbox_select_entry (l, listbox_y_pos (l, local.y - 1));
526 listbox_on_change (l);
527 return ret;
530 /* Double click */
531 if ((event->type & (GPM_DOUBLE | GPM_UP)) == (GPM_UP | GPM_DOUBLE))
533 Gpm_Event local;
535 local = mouse_get_local (event, w);
536 dlg_select_widget (l);
537 listbox_run_hotkey (l, listbox_y_pos (l, local.y - 1));
540 return MOU_NORMAL;
543 /* --------------------------------------------------------------------------------------------- */
544 /*** public functions ****************************************************************************/
545 /* --------------------------------------------------------------------------------------------- */
547 WListbox *
548 listbox_new (int y, int x, int height, int width, gboolean deletable, lcback_fn callback)
550 WListbox *l;
551 Widget *w;
553 if (height <= 0)
554 height = 1;
556 l = g_new (WListbox, 1);
557 w = WIDGET (l);
558 widget_init (w, y, x, height, width, listbox_callback, listbox_event);
560 l->list = NULL;
561 l->top = l->pos = 0;
562 l->deletable = deletable;
563 l->callback = callback;
564 l->allow_duplicates = TRUE;
565 l->scrollbar = !mc_global.tty.slow_terminal;
566 widget_want_hotkey (w, TRUE);
567 widget_want_cursor (w, FALSE);
569 return l;
572 /* --------------------------------------------------------------------------------------------- */
575 listbox_search_text (WListbox * l, const char *text)
577 if (!listbox_is_empty (l))
579 int i;
580 GList *le;
582 for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
584 WLEntry *e = LENTRY (le->data);
586 if (strcmp (e->text, text) == 0)
587 return i;
591 return (-1);
594 /* --------------------------------------------------------------------------------------------- */
596 /* Selects the first entry and scrolls the list to the top */
597 void
598 listbox_select_first (WListbox * l)
600 l->pos = l->top = 0;
603 /* --------------------------------------------------------------------------------------------- */
605 /* Selects the last entry and scrolls the list to the bottom */
606 void
607 listbox_select_last (WListbox * l)
609 int lines = WIDGET (l)->lines;
610 int length = 0;
612 if (!listbox_is_empty (l))
613 length = g_queue_get_length (l->list);
615 l->pos = length > 0 ? length - 1 : 0;
616 l->top = length > lines ? length - lines : 0;
619 /* --------------------------------------------------------------------------------------------- */
621 void
622 listbox_select_entry (WListbox * l, int dest)
624 GList *le;
625 int pos;
626 gboolean top_seen = FALSE;
628 if (listbox_is_empty (l) || dest < 0)
629 return;
631 /* Special case */
632 for (pos = 0, le = g_queue_peek_head_link (l->list); le != NULL; pos++, le = g_list_next (le))
634 if (pos == l->top)
635 top_seen = TRUE;
637 if (pos == dest)
639 l->pos = dest;
640 if (!top_seen)
641 l->top = l->pos;
642 else
644 int lines = WIDGET (l)->lines;
646 if (l->pos - l->top >= lines)
647 l->top = l->pos - lines + 1;
649 return;
653 /* If we are unable to find it, set decent values */
654 l->pos = l->top = 0;
657 /* --------------------------------------------------------------------------------------------- */
659 /* Returns the current string text as well as the associated extra data */
660 void
661 listbox_get_current (WListbox * l, char **string, void **extra)
663 WLEntry *e = NULL;
664 gboolean ok;
666 if (l != NULL)
667 e = listbox_get_nth_item (l, l->pos);
669 ok = (e != NULL);
671 if (string != NULL)
672 *string = ok ? e->text : NULL;
674 if (extra != NULL)
675 *extra = ok ? e->data : NULL;
678 /* --------------------------------------------------------------------------------------------- */
680 WLEntry *
681 listbox_get_nth_item (const WListbox * l, int pos)
683 if (!listbox_is_empty (l) && pos >= 0)
685 GList *item;
687 item = g_queue_peek_nth_link (l->list, (guint) pos);
688 if (item != NULL)
689 return LENTRY (item->data);
692 return NULL;
695 /* --------------------------------------------------------------------------------------------- */
697 GList *
698 listbox_get_first_link (const WListbox * l)
700 return (l == NULL || l->list == NULL) ? NULL : g_queue_peek_head_link (l->list);
703 /* --------------------------------------------------------------------------------------------- */
705 void
706 listbox_remove_current (WListbox * l)
708 if (!listbox_is_empty (l))
710 GList *current;
711 int length;
713 current = g_queue_peek_nth_link (l->list, (guint) l->pos);
714 listbox_entry_free (LENTRY (current->data));
715 g_queue_delete_link (l->list, current);
717 length = g_queue_get_length (l->list);
719 if (length == 0)
720 l->top = l->pos = 0;
721 else if (l->pos >= length)
722 l->pos = length - 1;
726 /* --------------------------------------------------------------------------------------------- */
728 gboolean
729 listbox_is_empty (const WListbox * l)
731 return (l == NULL || l->list == NULL || g_queue_is_empty (l->list));
734 /* --------------------------------------------------------------------------------------------- */
736 void
737 listbox_set_list (WListbox * l, GList * list)
739 listbox_remove_list (l);
741 if (l != NULL)
743 GList *ll;
745 l->list = g_queue_new ();
747 for (ll = list; ll != NULL; ll = g_list_next (ll))
748 g_queue_push_tail (l->list, ll->data);
750 g_list_free (list);
754 /* --------------------------------------------------------------------------------------------- */
756 void
757 listbox_remove_list (WListbox * l)
759 if (l != NULL)
761 if (l->list != NULL)
763 g_queue_foreach (l->list, (GFunc) listbox_entry_free, NULL);
764 g_queue_free (l->list);
767 l->list = NULL;
768 l->pos = l->top = 0;
772 /* --------------------------------------------------------------------------------------------- */
774 char *
775 listbox_add_item (WListbox * l, listbox_append_t pos, int hotkey, const char *text, void *data,
776 gboolean free_data)
778 WLEntry *entry;
780 if (l == NULL)
781 return NULL;
783 if (!l->allow_duplicates && (listbox_search_text (l, text) >= 0))
784 return NULL;
786 entry = g_new (WLEntry, 1);
787 entry->text = g_strdup (text);
788 entry->data = data;
789 entry->free_data = free_data;
790 entry->hotkey = hotkey;
792 listbox_append_item (l, entry, pos);
794 return entry->text;
797 /* --------------------------------------------------------------------------------------------- */