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