mc/lib/widget/listbox.c
Andrew Borodin 7df67031a2 Refactoring of WListbox widget.
Use GQueue instead of GList to store listbox entries.

g_list_append() function is slow because it uses g_list_last()
internally to traverse from the beginning to the end of a list, so
forming a list of results has O(n*n) complexity instead of O(n).
GQueue contains pointers to head and tail of list and list length.
So in this case we don't need seach end of list every time when we
want append listbox entry to the listbox.

Signed-off-by: Andrew Borodin <aborodin@vmail.ru>
2014-01-14 14:13:38 +04:00

791 lines
20 KiB
C

/*
Widgets for the Midnight Commander
Copyright (C) 1994, 1995, 1996, 1998, 1999, 2000, 2001, 2002, 2003,
2004, 2005, 2006, 2007, 2009, 2010, 2011, 2013
The Free Software Foundation, Inc.
Authors:
Radek Doulik, 1994, 1995
Miguel de Icaza, 1994, 1995
Jakub Jelinek, 1995
Andrej Borsenkow, 1996
Norbert Warmuth, 1997
Andrew Borodin <aborodin@vmail.ru>, 2009, 2010, 2013
This file is part of the Midnight Commander.
The Midnight Commander is free software: you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation, either version 3 of the License,
or (at your option) any later version.
The Midnight Commander is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/** \file listbox.c
* \brief Source: WListbox widget
*/
#include <config.h>
#include <stdlib.h>
#include "lib/global.h"
#include "lib/tty/tty.h"
#include "lib/tty/mouse.h"
#include "lib/skin.h"
#include "lib/strutil.h"
#include "lib/util.h" /* Q_() */
#include "lib/keybind.h" /* global_keymap_t */
#include "lib/widget.h"
/*** global variables ****************************************************************************/
const global_keymap_t *listbox_map = NULL;
/*** file scope macro definitions ****************************************************************/
/*** file scope type declarations ****************************************************************/
/*** file scope variables ************************************************************************/
/*** file scope functions ************************************************************************/
static int
listbox_entry_cmp (const void *a, const void *b, void *user_data)
{
const WLEntry *ea = (const WLEntry *) a;
const WLEntry *eb = (const WLEntry *) b;
(void) user_data;
return strcmp (ea->text, eb->text);
}
/* --------------------------------------------------------------------------------------------- */
static void
listbox_entry_free (void *data)
{
WLEntry *e = data;
g_free (e->text);
g_free (e);
}
/* --------------------------------------------------------------------------------------------- */
static void
listbox_drawscroll (WListbox * l)
{
Widget *w = WIDGET (l);
int max_line = w->lines - 1;
int line = 0;
int i;
int length;
/* Are we at the top? */
widget_move (w, 0, w->cols);
if (l->top == 0)
tty_print_one_vline (TRUE);
else
tty_print_char ('^');
length = g_queue_get_length (l->list);
/* Are we at the bottom? */
widget_move (w, max_line, w->cols);
if (l->top + w->lines == length || w->lines >= length)
tty_print_one_vline (TRUE);
else
tty_print_char ('v');
/* Now draw the nice relative pointer */
if (!g_queue_is_empty (l->list))
line = 1 + ((l->pos * (w->lines - 2)) / length);
for (i = 1; i < max_line; i++)
{
widget_move (w, i, w->cols);
if (i != line)
tty_print_one_vline (TRUE);
else
tty_print_char ('*');
}
}
/* --------------------------------------------------------------------------------------------- */
static void
listbox_draw (WListbox * l, gboolean focused)
{
Widget *w = WIDGET (l);
const WDialog *h = w->owner;
const gboolean disabled = (w->options & W_DISABLED) != 0;
const int normalc = disabled ? DISABLED_COLOR : h->color[DLG_COLOR_NORMAL];
/* *INDENT-OFF* */
int selc = disabled
? DISABLED_COLOR
: focused
? h->color[DLG_COLOR_HOT_FOCUS]
: h->color[DLG_COLOR_FOCUS];
/* *INDENT-ON* */
int length = 0;
GList *le = NULL;
int pos;
int i;
int sel_line = -1;
if (l->list != NULL)
{
length = g_queue_get_length (l->list);
le = g_queue_peek_nth_link (l->list, (guint) l->top);
}
/* pos = (le == NULL) ? 0 : g_list_position (l->list, le); */
pos = (le == NULL) ? 0 : l->top;
for (i = 0; i < w->lines; i++)
{
const char *text = "";
/* Display the entry */
if (pos == l->pos && sel_line == -1)
{
sel_line = i;
tty_setcolor (selc);
}
else
tty_setcolor (normalc);
widget_move (l, i, 1);
if (l->list != NULL && le != NULL && (i == 0 || pos < length))
{
WLEntry *e = LENTRY (le->data);
text = e->text;
le = g_list_next (le);
pos++;
}
tty_print_string (str_fit_to_term (text, w->cols - 2, J_LEFT_FIT));
}
l->cursor_y = sel_line;
if (l->scrollbar && length > w->lines)
{
tty_setcolor (normalc);
listbox_drawscroll (l);
}
}
/* --------------------------------------------------------------------------------------------- */
static int
listbox_check_hotkey (WListbox * l, int key)
{
if (!listbox_is_empty (l))
{
int i;
GList *le;
for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
{
WLEntry *e = LENTRY (le->data);
if (e->hotkey == key)
return i;
}
}
return (-1);
}
/* --------------------------------------------------------------------------------------------- */
/* Selects from base the pos element */
static int
listbox_select_pos (WListbox * l, int base, int pos)
{
int last = 0;
base += pos;
if (!listbox_is_empty (l))
last = g_queue_get_length (l->list) - 1;
base = min (base, last);
return base;
}
/* --------------------------------------------------------------------------------------------- */
static void
listbox_fwd (WListbox * l)
{
if ((guint) l->pos + 1 >= g_queue_get_length (l->list))
listbox_select_first (l);
else
listbox_select_entry (l, l->pos + 1);
}
/* --------------------------------------------------------------------------------------------- */
static void
listbox_back (WListbox * l)
{
if (l->pos <= 0)
listbox_select_last (l);
else
listbox_select_entry (l, l->pos - 1);
}
/* --------------------------------------------------------------------------------------------- */
static cb_ret_t
listbox_execute_cmd (WListbox * l, unsigned long command)
{
cb_ret_t ret = MSG_HANDLED;
int i;
Widget *w = WIDGET (l);
int length;
if (l->list == NULL || g_queue_is_empty (l->list))
return MSG_NOT_HANDLED;
switch (command)
{
case CK_Up:
listbox_back (l);
break;
case CK_Down:
listbox_fwd (l);
break;
case CK_Top:
listbox_select_first (l);
break;
case CK_Bottom:
listbox_select_last (l);
break;
case CK_PageUp:
for (i = 0; (i < w->lines - 1) && (l->pos > 0); i++)
listbox_back (l);
break;
case CK_PageDown:
length = g_queue_get_length (l->list);
for (i = 0; i < w->lines - 1 && l->pos < length - 1; i++)
listbox_fwd (l);
break;
case CK_Delete:
if (l->deletable)
{
gboolean is_last, is_more;
length = g_queue_get_length (l->list);
is_last = (l->pos + 1 >= length);
is_more = (l->top + w->lines >= length);
listbox_remove_current (l);
if ((l->top > 0) && (is_last || is_more))
l->top--;
}
break;
case CK_Clear:
if (l->deletable && mc_global.widget.confirm_history_cleanup
/* TRANSLATORS: no need to translate 'DialogTitle', it's just a context prefix */
&& (query_dialog (Q_ ("DialogTitle|History cleanup"),
_("Do you want clean this history?"),
D_ERROR, 2, _("&Yes"), _("&No")) == 0))
listbox_remove_list (l);
break;
default:
ret = MSG_NOT_HANDLED;
}
return ret;
}
/* --------------------------------------------------------------------------------------------- */
/* Return MSG_HANDLED if we want a redraw */
static cb_ret_t
listbox_key (WListbox * l, int key)
{
unsigned long command;
if (l->list == NULL)
return MSG_NOT_HANDLED;
/* focus on listbox item N by '0'..'9' keys */
if (key >= '0' && key <= '9')
{
int oldpos = l->pos;
listbox_select_entry (l, key - '0');
/* need scroll to item? */
if (abs (oldpos - l->pos) > WIDGET (l)->lines)
l->top = l->pos;
return MSG_HANDLED;
}
command = keybind_lookup_keymap_command (listbox_map, key);
if (command == CK_IgnoreKey)
return MSG_NOT_HANDLED;
return listbox_execute_cmd (l, command);
}
/* --------------------------------------------------------------------------------------------- */
/* Listbox item adding function */
static inline void
listbox_append_item (WListbox * l, WLEntry * e, listbox_append_t pos)
{
if (l->list == NULL)
l->list = g_queue_new ();
switch (pos)
{
case LISTBOX_APPEND_AT_END:
g_queue_push_tail (l->list, e);
break;
case LISTBOX_APPEND_BEFORE:
g_queue_insert_before (l->list, g_queue_peek_nth_link (l->list, (guint) l->pos), e);
break;
case LISTBOX_APPEND_AFTER:
g_queue_insert_after (l->list, g_queue_peek_nth_link (l->list, (guint) l->pos), e);
break;
case LISTBOX_APPEND_SORTED:
g_queue_insert_sorted (l->list, e, (GCompareDataFunc) listbox_entry_cmp, NULL);
break;
default:
break;
}
}
/* --------------------------------------------------------------------------------------------- */
static inline void
listbox_destroy (WListbox * l)
{
listbox_remove_list (l);
}
/* --------------------------------------------------------------------------------------------- */
static cb_ret_t
listbox_callback (Widget * w, Widget * sender, widget_msg_t msg, int parm, void *data)
{
WListbox *l = LISTBOX (w);
WDialog *h = w->owner;
cb_ret_t ret_code;
switch (msg)
{
case MSG_INIT:
return MSG_HANDLED;
case MSG_HOTKEY:
{
int pos, action;
pos = listbox_check_hotkey (l, parm);
if (pos < 0)
return MSG_NOT_HANDLED;
listbox_select_entry (l, pos);
send_message (h, w, MSG_ACTION, l->pos, NULL);
if (l->callback != NULL)
action = l->callback (l);
else
action = LISTBOX_DONE;
if (action == LISTBOX_DONE)
{
h->ret_value = B_ENTER;
dlg_stop (h);
}
return MSG_HANDLED;
}
case MSG_KEY:
ret_code = listbox_key (l, parm);
if (ret_code != MSG_NOT_HANDLED)
{
listbox_draw (l, TRUE);
send_message (h, w, MSG_ACTION, l->pos, NULL);
}
return ret_code;
case MSG_ACTION:
return listbox_execute_cmd (l, parm);
case MSG_CURSOR:
widget_move (l, l->cursor_y, 0);
send_message (h, w, MSG_ACTION, l->pos, NULL);
return MSG_HANDLED;
case MSG_FOCUS:
case MSG_UNFOCUS:
case MSG_DRAW:
listbox_draw (l, msg != MSG_UNFOCUS);
return MSG_HANDLED;
case MSG_DESTROY:
listbox_destroy (l);
return MSG_HANDLED;
case MSG_RESIZE:
return MSG_HANDLED;
default:
return widget_default_callback (w, sender, msg, parm, data);
}
}
/* --------------------------------------------------------------------------------------------- */
static int
listbox_event (Gpm_Event * event, void *data)
{
WListbox *l = LISTBOX (data);
Widget *w = WIDGET (data);
if (!mouse_global_in_widget (event, w))
return MOU_UNHANDLED;
/* Single click */
if ((event->type & GPM_DOWN) != 0)
dlg_select_widget (l);
if (g_queue_is_empty (l->list))
return MOU_NORMAL;
if ((event->type & (GPM_DOWN | GPM_DRAG)) != 0)
{
int ret = MOU_REPEAT;
Gpm_Event local;
int i;
local = mouse_get_local (event, w);
if (local.y < 1)
for (i = -local.y; i >= 0; i--)
listbox_back (l);
else if (local.y > w->lines)
for (i = local.y - w->lines; i > 0; i--)
listbox_fwd (l);
else if ((local.buttons & GPM_B_UP) != 0)
{
listbox_back (l);
ret = MOU_NORMAL;
}
else if ((local.buttons & GPM_B_DOWN) != 0)
{
listbox_fwd (l);
ret = MOU_NORMAL;
}
else
listbox_select_entry (l, listbox_select_pos (l, l->top, local.y - 1));
/* We need to refresh ourselves since the dialog manager doesn't */
/* know about this event */
listbox_draw (l, TRUE);
return ret;
}
/* Double click */
if ((event->type & (GPM_DOUBLE | GPM_UP)) == (GPM_UP | GPM_DOUBLE))
{
Gpm_Event local;
int action;
local = mouse_get_local (event, w);
dlg_select_widget (l);
listbox_select_entry (l, listbox_select_pos (l, l->top, local.y - 1));
if (l->callback != NULL)
action = l->callback (l);
else
action = LISTBOX_DONE;
if (action == LISTBOX_DONE)
{
w->owner->ret_value = B_ENTER;
dlg_stop (w->owner);
}
}
return MOU_NORMAL;
}
/* --------------------------------------------------------------------------------------------- */
/*** public functions ****************************************************************************/
/* --------------------------------------------------------------------------------------------- */
WListbox *
listbox_new (int y, int x, int height, int width, gboolean deletable, lcback_fn callback)
{
WListbox *l;
Widget *w;
if (height <= 0)
height = 1;
l = g_new (WListbox, 1);
w = WIDGET (l);
widget_init (w, y, x, height, width, listbox_callback, listbox_event);
l->list = NULL;
l->top = l->pos = 0;
l->deletable = deletable;
l->callback = callback;
l->allow_duplicates = TRUE;
l->scrollbar = !mc_global.tty.slow_terminal;
widget_want_hotkey (w, TRUE);
widget_want_cursor (w, FALSE);
return l;
}
/* --------------------------------------------------------------------------------------------- */
int
listbox_search_text (WListbox * l, const char *text)
{
if (!listbox_is_empty (l))
{
int i;
GList *le;
for (i = 0, le = g_queue_peek_head_link (l->list); le != NULL; i++, le = g_list_next (le))
{
WLEntry *e = LENTRY (le->data);
if (strcmp (e->text, text) == 0)
return i;
}
}
return (-1);
}
/* --------------------------------------------------------------------------------------------- */
/* Selects the first entry and scrolls the list to the top */
void
listbox_select_first (WListbox * l)
{
l->pos = l->top = 0;
}
/* --------------------------------------------------------------------------------------------- */
/* Selects the last entry and scrolls the list to the bottom */
void
listbox_select_last (WListbox * l)
{
int lines = WIDGET (l)->lines;
int length = 0;
if (!listbox_is_empty (l))
length = g_queue_get_length (l->list);
l->pos = length > 0 ? length - 1 : 0;
l->top = length > lines ? length - lines : 0;
}
/* --------------------------------------------------------------------------------------------- */
void
listbox_select_entry (WListbox * l, int dest)
{
GList *le;
int pos;
gboolean top_seen = FALSE;
if (listbox_is_empty (l) || dest < 0)
return;
/* Special case */
for (pos = 0, le = g_queue_peek_head_link (l->list); le != NULL; pos++, le = g_list_next (le))
{
if (pos == l->top)
top_seen = TRUE;
if (pos == dest)
{
l->pos = dest;
if (!top_seen)
l->top = l->pos;
else
{
int lines = WIDGET (l)->lines;
if (l->pos - l->top >= lines)
l->top = l->pos - lines + 1;
}
return;
}
}
/* If we are unable to find it, set decent values */
l->pos = l->top = 0;
}
/* --------------------------------------------------------------------------------------------- */
/* Returns the current string text as well as the associated extra data */
void
listbox_get_current (WListbox * l, char **string, void **extra)
{
WLEntry *e;
gboolean ok;
e = listbox_get_nth_item (l, l->pos);
ok = (e != NULL);
if (string != NULL)
*string = ok ? e->text : NULL;
if (extra != NULL)
*extra = ok ? e->data : NULL;
}
/* --------------------------------------------------------------------------------------------- */
WLEntry *
listbox_get_nth_item (const WListbox * l, int pos)
{
if (!listbox_is_empty (l) && pos >= 0)
{
GList *item;
item = g_queue_peek_nth_link (l->list, (guint) pos);
if (item != NULL)
return LENTRY (item->data);
}
return NULL;
}
/* --------------------------------------------------------------------------------------------- */
GList *
listbox_get_first_link (const WListbox * l)
{
return (l == NULL || l->list == NULL) ? NULL : g_queue_peek_head_link (l->list);
}
/* --------------------------------------------------------------------------------------------- */
void
listbox_remove_current (WListbox * l)
{
if (!listbox_is_empty (l))
{
GList *current;
int length;
current = g_queue_peek_nth_link (l->list, (guint) l->pos);
listbox_entry_free (LENTRY (current->data));
g_queue_delete_link (l->list, current);
length = g_queue_get_length (l->list);
if (length == 0)
l->top = l->pos = 0;
else if (l->pos >= length)
l->pos = length - 1;
}
}
/* --------------------------------------------------------------------------------------------- */
gboolean
listbox_is_empty (const WListbox * l)
{
return (l == NULL || l->list == NULL || g_queue_is_empty (l->list));
}
/* --------------------------------------------------------------------------------------------- */
void
listbox_set_list (WListbox * l, GList * list)
{
listbox_remove_list (l);
if (l != NULL)
{
GList *ll;
l->list = g_queue_new ();
for (ll = list; ll != NULL; ll = g_list_next (ll))
g_queue_push_tail (l->list, ll->data);
g_list_free (list);
}
}
/* --------------------------------------------------------------------------------------------- */
void
listbox_remove_list (WListbox * l)
{
if (l != NULL)
{
if (l->list != NULL)
{
g_queue_foreach (l->list, (GFunc) listbox_entry_free, NULL);
g_queue_free (l->list);
}
l->list = NULL;
l->pos = l->top = 0;
}
}
/* --------------------------------------------------------------------------------------------- */
char *
listbox_add_item (WListbox * l, listbox_append_t pos, int hotkey, const char *text, void *data)
{
WLEntry *entry;
if (l == NULL)
return NULL;
if (!l->allow_duplicates && (listbox_search_text (l, text) >= 0))
return NULL;
entry = g_new (WLEntry, 1);
entry->text = g_strdup (text);
entry->data = data;
entry->hotkey = hotkey;
listbox_append_item (l, entry, pos);
return entry->text;
}
/* --------------------------------------------------------------------------------------------- */