WListbox: optimize mouse event processing.
[midnight-commander.git] / lib / widget / listbox.c
blobc68fd1d2f74da1ff261c561d035070e4fdb83386
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, 2016
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/skin.h"
43 #include "lib/strutil.h"
44 #include "lib/util.h" /* Q_() */
45 #include "lib/keybind.h" /* global_keymap_t */
46 #include "lib/widget.h"
48 /*** global variables ****************************************************************************/
50 const global_keymap_t *listbox_map = NULL;
52 /*** file scope macro definitions ****************************************************************/
54 /* Gives the position of the last item. */
55 #define LISTBOX_LAST(l) (g_queue_is_empty ((l)->list) ? 0 : (int) g_queue_get_length ((l)->list) - 1)
57 /*** file scope type declarations ****************************************************************/
59 /*** file scope variables ************************************************************************/
61 /*** file scope functions ************************************************************************/
63 static int
64 listbox_entry_cmp (const void *a, const void *b, void *user_data)
66 const WLEntry *ea = (const WLEntry *) a;
67 const WLEntry *eb = (const WLEntry *) b;
69 (void) user_data;
71 return strcmp (ea->text, eb->text);
74 /* --------------------------------------------------------------------------------------------- */
76 static void
77 listbox_entry_free (void *data)
79 WLEntry *e = data;
81 g_free (e->text);
82 if (e->free_data)
83 g_free (e->data);
84 g_free (e);
87 /* --------------------------------------------------------------------------------------------- */
89 static void
90 listbox_drawscroll (WListbox * l)
92 Widget *w = WIDGET (l);
93 int max_line = w->lines - 1;
94 int line = 0;
95 int i;
96 int length;
98 /* Are we at the top? */
99 widget_move (w, 0, w->cols);
100 if (l->top == 0)
101 tty_print_one_vline (TRUE);
102 else
103 tty_print_char ('^');
105 length = g_queue_get_length (l->list);
107 /* Are we at the bottom? */
108 widget_move (w, max_line, w->cols);
109 if (l->top + w->lines == length || w->lines >= length)
110 tty_print_one_vline (TRUE);
111 else
112 tty_print_char ('v');
114 /* Now draw the nice relative pointer */
115 if (!g_queue_is_empty (l->list))
116 line = 1 + ((l->pos * (w->lines - 2)) / length);
118 for (i = 1; i < max_line; i++)
120 widget_move (w, i, w->cols);
121 if (i != line)
122 tty_print_one_vline (TRUE);
123 else
124 tty_print_char ('*');
128 /* --------------------------------------------------------------------------------------------- */
130 static void
131 listbox_draw (WListbox * l, gboolean focused)
133 Widget *w = WIDGET (l);
134 const WDialog *h = w->owner;
135 const gboolean disabled = (w->options & W_DISABLED) != 0;
136 const int normalc = disabled ? DISABLED_COLOR : h->color[DLG_COLOR_NORMAL];
137 /* *INDENT-OFF* */
138 int selc = disabled
139 ? DISABLED_COLOR
140 : focused
141 ? h->color[DLG_COLOR_HOT_FOCUS]
142 : h->color[DLG_COLOR_FOCUS];
143 /* *INDENT-ON* */
145 int length = 0;
146 GList *le = NULL;
147 int pos;
148 int i;
149 int sel_line = -1;
151 if (l->list != NULL)
153 length = g_queue_get_length (l->list);
154 le = g_queue_peek_nth_link (l->list, (guint) l->top);
157 /* pos = (le == NULL) ? 0 : g_list_position (l->list, le); */
158 pos = (le == NULL) ? 0 : l->top;
160 for (i = 0; i < w->lines; i++)
162 const char *text = "";
164 /* Display the entry */
165 if (pos == l->pos && sel_line == -1)
167 sel_line = i;
168 tty_setcolor (selc);
170 else
171 tty_setcolor (normalc);
173 widget_move (l, i, 1);
175 if (l->list != NULL && le != NULL && (i == 0 || pos < length))
177 WLEntry *e = LENTRY (le->data);
179 text = e->text;
180 le = g_list_next (le);
181 pos++;
184 tty_print_string (str_fit_to_term (text, w->cols - 2, J_LEFT_FIT));
187 l->cursor_y = sel_line;
189 if (l->scrollbar && length > w->lines)
191 tty_setcolor (normalc);
192 listbox_drawscroll (l);
196 /* --------------------------------------------------------------------------------------------- */
198 static int
199 listbox_check_hotkey (WListbox * l, int key)
201 if (!listbox_is_empty (l))
203 int i;
204 GList *le;
206 for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
208 WLEntry *e = LENTRY (le->data);
210 if (e->hotkey == key)
211 return i;
215 return (-1);
218 /* --------------------------------------------------------------------------------------------- */
220 /* Calculates the item displayed at screen row 'y' (y==0 being the widget's 1st row). */
221 static int
222 listbox_y_pos (WListbox * l, int y)
224 return min (l->top + y, LISTBOX_LAST (l));
227 /* --------------------------------------------------------------------------------------------- */
229 static void
230 listbox_fwd (WListbox * l, gboolean wrap)
232 if ((guint) l->pos + 1 < g_queue_get_length (l->list))
233 listbox_select_entry (l, l->pos + 1);
234 else if (wrap)
235 listbox_select_first (l);
238 /* --------------------------------------------------------------------------------------------- */
240 static void
241 listbox_fwd_n (WListbox * l, int n)
243 listbox_select_entry (l, min (l->pos + n, LISTBOX_LAST (l)));
246 /* --------------------------------------------------------------------------------------------- */
248 static void
249 listbox_back (WListbox * l, gboolean wrap)
251 if (l->pos > 0)
252 listbox_select_entry (l, l->pos - 1);
253 else if (wrap)
254 listbox_select_last (l);
257 /* --------------------------------------------------------------------------------------------- */
259 static void
260 listbox_back_n (WListbox * l, int n)
262 listbox_select_entry (l, max (l->pos - n, 0));
265 /* --------------------------------------------------------------------------------------------- */
267 static cb_ret_t
268 listbox_execute_cmd (WListbox * l, long command)
270 cb_ret_t ret = MSG_HANDLED;
271 Widget *w = WIDGET (l);
273 if (l->list == NULL || g_queue_is_empty (l->list))
274 return MSG_NOT_HANDLED;
276 switch (command)
278 case CK_Up:
279 listbox_back (l, TRUE);
280 break;
281 case CK_Down:
282 listbox_fwd (l, TRUE);
283 break;
284 case CK_Top:
285 listbox_select_first (l);
286 break;
287 case CK_Bottom:
288 listbox_select_last (l);
289 break;
290 case CK_PageUp:
291 listbox_back_n (l, w->lines - 1);
292 break;
293 case CK_PageDown:
294 listbox_fwd_n (l, w->lines - 1);
295 break;
296 case CK_Delete:
297 if (l->deletable)
299 gboolean is_last, is_more;
300 int length;
302 length = g_queue_get_length (l->list);
304 is_last = (l->pos + 1 >= length);
305 is_more = (l->top + w->lines >= length);
307 listbox_remove_current (l);
308 if ((l->top > 0) && (is_last || is_more))
309 l->top--;
311 break;
312 case CK_Clear:
313 if (l->deletable && mc_global.widget.confirm_history_cleanup
314 /* TRANSLATORS: no need to translate 'DialogTitle', it's just a context prefix */
315 && (query_dialog (Q_ ("DialogTitle|History cleanup"),
316 _("Do you want clean this history?"),
317 D_ERROR, 2, _("&Yes"), _("&No")) == 0))
318 listbox_remove_list (l);
319 break;
320 default:
321 ret = MSG_NOT_HANDLED;
324 return ret;
327 /* --------------------------------------------------------------------------------------------- */
329 /* Return MSG_HANDLED if we want a redraw */
330 static cb_ret_t
331 listbox_key (WListbox * l, int key)
333 long command;
335 if (l->list == NULL)
336 return MSG_NOT_HANDLED;
338 /* focus on listbox item N by '0'..'9' keys */
339 if (key >= '0' && key <= '9')
341 listbox_select_entry (l, key - '0');
342 return MSG_HANDLED;
345 command = keybind_lookup_keymap_command (listbox_map, key);
346 if (command == CK_IgnoreKey)
347 return MSG_NOT_HANDLED;
348 return listbox_execute_cmd (l, command);
351 /* --------------------------------------------------------------------------------------------- */
353 /* Listbox item adding function */
354 static inline void
355 listbox_append_item (WListbox * l, WLEntry * e, listbox_append_t pos)
357 if (l->list == NULL)
359 l->list = g_queue_new ();
360 pos = LISTBOX_APPEND_AT_END;
363 switch (pos)
365 case LISTBOX_APPEND_AT_END:
366 g_queue_push_tail (l->list, e);
367 break;
369 case LISTBOX_APPEND_BEFORE:
370 g_queue_insert_before (l->list, g_queue_peek_nth_link (l->list, (guint) l->pos), e);
371 break;
373 case LISTBOX_APPEND_AFTER:
374 g_queue_insert_after (l->list, g_queue_peek_nth_link (l->list, (guint) l->pos), e);
375 break;
377 case LISTBOX_APPEND_SORTED:
378 g_queue_insert_sorted (l->list, e, (GCompareDataFunc) listbox_entry_cmp, NULL);
379 break;
381 default:
382 break;
386 /* --------------------------------------------------------------------------------------------- */
388 /* Call this whenever the user changes the selected item. */
389 static void
390 listbox_on_change (WListbox * l)
392 listbox_draw (l, TRUE);
393 send_message (WIDGET (l)->owner, l, MSG_NOTIFY, l->pos, NULL);
396 /* --------------------------------------------------------------------------------------------- */
398 static void
399 listbox_do_action (WListbox * l)
401 int action;
403 if (l->callback != NULL)
404 action = l->callback (l);
405 else
406 action = LISTBOX_DONE;
408 if (action == LISTBOX_DONE)
410 WDialog *h = WIDGET (l)->owner;
412 h->ret_value = B_ENTER;
413 dlg_stop (h);
417 /* --------------------------------------------------------------------------------------------- */
419 static void
420 listbox_run_hotkey (WListbox * l, int pos)
422 listbox_select_entry (l, pos);
423 listbox_on_change (l);
424 listbox_do_action (l);
427 /* --------------------------------------------------------------------------------------------- */
429 static inline void
430 listbox_destroy (WListbox * l)
432 listbox_remove_list (l);
435 /* --------------------------------------------------------------------------------------------- */
437 static cb_ret_t
438 listbox_callback (Widget * w, Widget * sender, widget_msg_t msg, int parm, void *data)
440 WListbox *l = LISTBOX (w);
441 cb_ret_t ret_code;
443 switch (msg)
445 case MSG_INIT:
446 return MSG_HANDLED;
448 case MSG_HOTKEY:
450 int pos;
452 pos = listbox_check_hotkey (l, parm);
453 if (pos < 0)
454 return MSG_NOT_HANDLED;
456 listbox_run_hotkey (l, pos);
458 return MSG_HANDLED;
461 case MSG_KEY:
462 ret_code = listbox_key (l, parm);
463 if (ret_code != MSG_NOT_HANDLED)
464 listbox_on_change (l);
465 return ret_code;
467 case MSG_ACTION:
468 return listbox_execute_cmd (l, parm);
470 case MSG_CURSOR:
471 widget_move (l, l->cursor_y, 0);
472 return MSG_HANDLED;
474 case MSG_FOCUS:
475 case MSG_UNFOCUS:
476 l->focused = msg == MSG_FOCUS;
477 /* fall through */
478 case MSG_DRAW:
479 listbox_draw (l, l->focused);
480 return MSG_HANDLED;
482 case MSG_DESTROY:
483 listbox_destroy (l);
484 return MSG_HANDLED;
486 case MSG_RESIZE:
487 return MSG_HANDLED;
489 default:
490 return widget_default_callback (w, sender, msg, parm, data);
494 /* --------------------------------------------------------------------------------------------- */
496 static void
497 listbox_mouse_callback (Widget * w, mouse_msg_t msg, mouse_event_t * event)
499 WListbox *l = LISTBOX (w);
500 int old_pos;
502 old_pos = l->pos;
504 switch (msg)
506 case MSG_MOUSE_DOWN:
507 dlg_select_widget (l);
508 listbox_select_entry (l, listbox_y_pos (l, event->y));
509 break;
511 case MSG_MOUSE_SCROLL_UP:
512 listbox_back (l, FALSE);
513 break;
515 case MSG_MOUSE_SCROLL_DOWN:
516 listbox_fwd (l, FALSE);
517 break;
519 case MSG_MOUSE_DRAG:
520 event->result.repeat = TRUE; /* It'd be functional even without this. */
521 listbox_select_entry (l, listbox_y_pos (l, event->y));
522 break;
524 case MSG_MOUSE_CLICK:
525 /* We don't call listbox_select_entry() here: MSG_MOUSE_DOWN/DRAG did this already. */
526 if (event->count == GPM_DOUBLE) /* Double click */
527 listbox_do_action (l);
528 break;
530 default:
531 break;
534 /* If the selection has changed, we redraw the widget and notify the dialog. */
535 if (l->pos != old_pos)
536 listbox_on_change (l);
539 /* --------------------------------------------------------------------------------------------- */
540 /*** public functions ****************************************************************************/
541 /* --------------------------------------------------------------------------------------------- */
543 WListbox *
544 listbox_new (int y, int x, int height, int width, gboolean deletable, lcback_fn callback)
546 WListbox *l;
547 Widget *w;
549 if (height <= 0)
550 height = 1;
552 l = g_new (WListbox, 1);
553 w = WIDGET (l);
554 widget_init (w, y, x, height, width, listbox_callback, NULL);
555 set_easy_mouse_callback (w, listbox_mouse_callback);
557 l->list = NULL;
558 l->top = l->pos = 0;
559 l->deletable = deletable;
560 l->callback = callback;
561 l->allow_duplicates = TRUE;
562 l->scrollbar = !mc_global.tty.slow_terminal;
563 l->focused = FALSE;
564 widget_want_hotkey (w, TRUE);
565 widget_want_cursor (w, FALSE);
567 return l;
570 /* --------------------------------------------------------------------------------------------- */
573 listbox_search_text (WListbox * l, const char *text)
575 if (!listbox_is_empty (l))
577 int i;
578 GList *le;
580 for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
582 WLEntry *e = LENTRY (le->data);
584 if (strcmp (e->text, text) == 0)
585 return i;
589 return (-1);
592 /* --------------------------------------------------------------------------------------------- */
594 /* Selects the first entry and scrolls the list to the top */
595 void
596 listbox_select_first (WListbox * l)
598 l->pos = l->top = 0;
601 /* --------------------------------------------------------------------------------------------- */
603 /* Selects the last entry and scrolls the list to the bottom */
604 void
605 listbox_select_last (WListbox * l)
607 int lines = WIDGET (l)->lines;
608 int length = 0;
610 if (!listbox_is_empty (l))
611 length = g_queue_get_length (l->list);
613 l->pos = length > 0 ? length - 1 : 0;
614 l->top = length > lines ? length - lines : 0;
617 /* --------------------------------------------------------------------------------------------- */
619 void
620 listbox_select_entry (WListbox * l, int dest)
622 GList *le;
623 int pos;
624 gboolean top_seen = FALSE;
626 if (listbox_is_empty (l) || dest < 0)
627 return;
629 /* Special case */
630 for (pos = 0, le = g_queue_peek_head_link (l->list); le != NULL; pos++, le = g_list_next (le))
632 if (pos == l->top)
633 top_seen = TRUE;
635 if (pos == dest)
637 l->pos = dest;
638 if (!top_seen)
639 l->top = l->pos;
640 else
642 int lines = WIDGET (l)->lines;
644 if (l->pos - l->top >= lines)
645 l->top = l->pos - lines + 1;
647 return;
651 /* If we are unable to find it, set decent values */
652 l->pos = l->top = 0;
655 /* --------------------------------------------------------------------------------------------- */
657 /* Returns the current string text as well as the associated extra data */
658 void
659 listbox_get_current (WListbox * l, char **string, void **extra)
661 WLEntry *e = NULL;
662 gboolean ok;
664 if (l != NULL)
665 e = listbox_get_nth_item (l, l->pos);
667 ok = (e != NULL);
669 if (string != NULL)
670 *string = ok ? e->text : NULL;
672 if (extra != NULL)
673 *extra = ok ? e->data : NULL;
676 /* --------------------------------------------------------------------------------------------- */
678 WLEntry *
679 listbox_get_nth_item (const WListbox * l, int pos)
681 if (!listbox_is_empty (l) && pos >= 0)
683 GList *item;
685 item = g_queue_peek_nth_link (l->list, (guint) pos);
686 if (item != NULL)
687 return LENTRY (item->data);
690 return NULL;
693 /* --------------------------------------------------------------------------------------------- */
695 GList *
696 listbox_get_first_link (const WListbox * l)
698 return (l == NULL || l->list == NULL) ? NULL : g_queue_peek_head_link (l->list);
701 /* --------------------------------------------------------------------------------------------- */
703 void
704 listbox_remove_current (WListbox * l)
706 if (!listbox_is_empty (l))
708 GList *current;
709 int length;
711 current = g_queue_peek_nth_link (l->list, (guint) l->pos);
712 listbox_entry_free (LENTRY (current->data));
713 g_queue_delete_link (l->list, current);
715 length = g_queue_get_length (l->list);
717 if (length == 0)
718 l->top = l->pos = 0;
719 else if (l->pos >= length)
720 l->pos = length - 1;
724 /* --------------------------------------------------------------------------------------------- */
726 gboolean
727 listbox_is_empty (const WListbox * l)
729 return (l == NULL || l->list == NULL || g_queue_is_empty (l->list));
732 /* --------------------------------------------------------------------------------------------- */
734 void
735 listbox_set_list (WListbox * l, GList * list)
737 listbox_remove_list (l);
739 if (l != NULL)
741 GList *ll;
743 l->list = g_queue_new ();
745 for (ll = list; ll != NULL; ll = g_list_next (ll))
746 g_queue_push_tail (l->list, ll->data);
748 g_list_free (list);
752 /* --------------------------------------------------------------------------------------------- */
754 void
755 listbox_remove_list (WListbox * l)
757 if (l != NULL)
759 if (l->list != NULL)
761 g_queue_foreach (l->list, (GFunc) listbox_entry_free, NULL);
762 g_queue_free (l->list);
765 l->list = NULL;
766 l->pos = l->top = 0;
770 /* --------------------------------------------------------------------------------------------- */
772 char *
773 listbox_add_item (WListbox * l, listbox_append_t pos, int hotkey, const char *text, void *data,
774 gboolean free_data)
776 WLEntry *entry;
778 if (l == NULL)
779 return NULL;
781 if (!l->allow_duplicates && (listbox_search_text (l, text) >= 0))
782 return NULL;
784 entry = g_new (WLEntry, 1);
785 entry->text = g_strdup (text);
786 entry->data = data;
787 entry->free_data = free_data;
788 entry->hotkey = hotkey;
790 listbox_append_item (l, entry, pos);
792 return entry->text;
795 /* --------------------------------------------------------------------------------------------- */