summaryrefslogtreecommitdiffstats
path: root/lib/widget/listbox.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/widget/listbox.c832
1 files changed, 832 insertions, 0 deletions
diff --git a/lib/widget/listbox.c b/lib/widget/listbox.c
new file mode 100644
index 0000000..9f25487
--- /dev/null
+++ b/lib/widget/listbox.c
@@ -0,0 +1,832 @@
+/*
+ Widgets for the Midnight Commander
+
+ Copyright (C) 1994-2023
+ 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-2022
+
+ 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/skin.h"
+#include "lib/strutil.h"
+#include "lib/util.h" /* Q_() */
+#include "lib/widget.h"
+
+/*** global variables ****************************************************************************/
+
+const global_keymap_t *listbox_map = NULL;
+
+/*** file scope macro definitions ****************************************************************/
+
+/* Gives the position of the last item. */
+#define LISTBOX_LAST(l) (listbox_is_empty (l) ? 0 : (int) g_queue_get_length ((l)->list) - 1)
+
+/*** file scope type declarations ****************************************************************/
+
+/*** forward declarations (file scope functions) *************************************************/
+
+/*** 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);
+ if (e->free_data)
+ g_free (e->data);
+ g_free (e);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_drawscroll (const WListbox * l)
+{
+ const WRect *w = &CONST_WIDGET (l)->rect;
+ int max_line = w->lines - 1;
+ int line = 0;
+ int i;
+ int length;
+
+ /* Are we at the top? */
+ widget_gotoyx (l, 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_gotoyx (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->current * (w->lines - 2)) / length);
+
+ for (i = 1; i < max_line; i++)
+ {
+ widget_gotoyx (l, i, w->cols);
+ if (i != line)
+ tty_print_one_vline (TRUE);
+ else
+ tty_print_char ('*');
+ }
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_draw (WListbox * l, gboolean focused)
+{
+ Widget *wl = WIDGET (l);
+ const WRect *w = &CONST_WIDGET (l)->rect;
+ const int *colors;
+ gboolean disabled;
+ int normalc, selc;
+ int length = 0;
+ GList *le = NULL;
+ int pos;
+ int i;
+ int sel_line = -1;
+
+ colors = widget_get_colors (wl);
+
+ disabled = widget_get_state (wl, WST_DISABLED);
+ normalc = disabled ? DISABLED_COLOR : colors[DLG_COLOR_NORMAL];
+ selc = disabled ? DISABLED_COLOR : colors[focused ? DLG_COLOR_HOT_FOCUS : DLG_COLOR_FOCUS];
+
+ 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->current && sel_line == -1)
+ {
+ sel_line = i;
+ tty_setcolor (selc);
+ }
+ else
+ tty_setcolor (normalc);
+
+ widget_gotoyx (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);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* Calculates the item displayed at screen row 'y' (y==0 being the widget's 1st row). */
+static int
+listbox_y_pos (WListbox * l, int y)
+{
+ return MIN (l->top + y, LISTBOX_LAST (l));
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_fwd (WListbox * l, gboolean wrap)
+{
+ if (!listbox_is_empty (l))
+ {
+ if ((guint) l->current + 1 < g_queue_get_length (l->list))
+ listbox_set_current (l, l->current + 1);
+ else if (wrap)
+ listbox_select_first (l);
+ }
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_fwd_n (WListbox * l, int n)
+{
+ listbox_set_current (l, MIN (l->current + n, LISTBOX_LAST (l)));
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_back (WListbox * l, gboolean wrap)
+{
+ if (!listbox_is_empty (l))
+ {
+ if (l->current > 0)
+ listbox_set_current (l, l->current - 1);
+ else if (wrap)
+ listbox_select_last (l);
+ }
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_back_n (WListbox * l, int n)
+{
+ listbox_set_current (l, MAX (l->current - n, 0));
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static cb_ret_t
+listbox_execute_cmd (WListbox * l, long command)
+{
+ cb_ret_t ret = MSG_HANDLED;
+ const WRect *w = &CONST_WIDGET (l)->rect;
+
+ if (l->list == NULL || g_queue_is_empty (l->list))
+ return MSG_NOT_HANDLED;
+
+ switch (command)
+ {
+ case CK_Up:
+ listbox_back (l, TRUE);
+ break;
+ case CK_Down:
+ listbox_fwd (l, TRUE);
+ break;
+ case CK_Top:
+ listbox_select_first (l);
+ break;
+ case CK_Bottom:
+ listbox_select_last (l);
+ break;
+ case CK_PageUp:
+ listbox_back_n (l, w->lines - 1);
+ break;
+ case CK_PageDown:
+ listbox_fwd_n (l, w->lines - 1);
+ break;
+ case CK_Delete:
+ if (l->deletable)
+ {
+ gboolean is_last, is_more;
+ int length;
+
+ length = g_queue_get_length (l->list);
+
+ is_last = (l->current + 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;
+ case CK_View:
+ case CK_Edit:
+ case CK_Enter:
+ ret = send_message (WIDGET (l)->owner, l, MSG_NOTIFY, command, NULL);
+ 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)
+{
+ long command;
+
+ if (l->list == NULL)
+ return MSG_NOT_HANDLED;
+
+ /* focus on listbox item N by '0'..'9' keys */
+ if (key >= '0' && key <= '9')
+ {
+ listbox_set_current (l, key - '0');
+ return MSG_HANDLED;
+ }
+
+ command = widget_lookup_key (WIDGET (l), key);
+ if (command == CK_IgnoreKey)
+ return MSG_NOT_HANDLED;
+ return listbox_execute_cmd (l, command);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* Listbox item adding function */
+static inline void
+listbox_add_entry (WListbox * l, WLEntry * e, listbox_append_t pos)
+{
+ if (l->list == NULL)
+ {
+ l->list = g_queue_new ();
+ pos = LISTBOX_APPEND_AT_END;
+ }
+
+ 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->current), e);
+ break;
+
+ case LISTBOX_APPEND_AFTER:
+ g_queue_insert_after (l->list, g_queue_peek_nth_link (l->list, (guint) l->current), e);
+ break;
+
+ case LISTBOX_APPEND_SORTED:
+ g_queue_insert_sorted (l->list, e, (GCompareDataFunc) listbox_entry_cmp, NULL);
+ break;
+
+ default:
+ break;
+ }
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* Call this whenever the user changes the selected item. */
+static void
+listbox_on_change (WListbox * l)
+{
+ listbox_draw (l, TRUE);
+ send_message (WIDGET (l)->owner, l, MSG_NOTIFY, 0, NULL);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_do_action (WListbox * l)
+{
+ int action;
+
+ if (listbox_is_empty (l))
+ return;
+
+ if (l->callback != NULL)
+ action = l->callback (l);
+ else
+ action = LISTBOX_DONE;
+
+ if (action == LISTBOX_DONE)
+ {
+ WDialog *h = DIALOG (WIDGET (l)->owner);
+
+ h->ret_value = B_ENTER;
+ dlg_close (h);
+ }
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_run_hotkey (WListbox * l, int pos)
+{
+ listbox_set_current (l, pos);
+ listbox_on_change (l);
+ listbox_do_action (l);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+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);
+
+ switch (msg)
+ {
+ case MSG_HOTKEY:
+ {
+ int pos;
+
+ pos = listbox_check_hotkey (l, parm);
+ if (pos < 0)
+ return MSG_NOT_HANDLED;
+
+ listbox_run_hotkey (l, pos);
+
+ return MSG_HANDLED;
+ }
+
+ case MSG_KEY:
+ {
+ cb_ret_t ret_code;
+
+ ret_code = listbox_key (l, parm);
+ if (ret_code != MSG_NOT_HANDLED)
+ listbox_on_change (l);
+ return ret_code;
+ }
+
+ case MSG_ACTION:
+ return listbox_execute_cmd (l, parm);
+
+ case MSG_CURSOR:
+ widget_gotoyx (l, l->cursor_y, 0);
+ return MSG_HANDLED;
+
+ case MSG_DRAW:
+ listbox_draw (l, widget_get_state (w, WST_FOCUSED));
+ return MSG_HANDLED;
+
+ case MSG_DESTROY:
+ listbox_destroy (l);
+ return MSG_HANDLED;
+
+ default:
+ return widget_default_callback (w, sender, msg, parm, data);
+ }
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static void
+listbox_mouse_callback (Widget * w, mouse_msg_t msg, mouse_event_t * event)
+{
+ WListbox *l = LISTBOX (w);
+ int old_current;
+
+ old_current = l->current;
+
+ switch (msg)
+ {
+ case MSG_MOUSE_DOWN:
+ widget_select (w);
+ listbox_set_current (l, listbox_y_pos (l, event->y));
+ break;
+
+ case MSG_MOUSE_SCROLL_UP:
+ listbox_back (l, FALSE);
+ break;
+
+ case MSG_MOUSE_SCROLL_DOWN:
+ listbox_fwd (l, FALSE);
+ break;
+
+ case MSG_MOUSE_DRAG:
+ event->result.repeat = TRUE; /* It'd be functional even without this. */
+ listbox_set_current (l, listbox_y_pos (l, event->y));
+ break;
+
+ case MSG_MOUSE_CLICK:
+ /* We don't call listbox_set_current() here: MSG_MOUSE_DOWN/DRAG did this already. */
+ if (event->count == GPM_DOUBLE) /* Double click */
+ listbox_do_action (l);
+ break;
+
+ default:
+ break;
+ }
+
+ /* If the selection has changed, we redraw the widget and notify the dialog. */
+ if (l->current != old_current)
+ listbox_on_change (l);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+/*** public functions ****************************************************************************/
+/* --------------------------------------------------------------------------------------------- */
+
+WListbox *
+listbox_new (int y, int x, int height, int width, gboolean deletable, lcback_fn callback)
+{
+ WRect r = { y, x, 1, width };
+ WListbox *l;
+ Widget *w;
+
+ l = g_new (WListbox, 1);
+ w = WIDGET (l);
+ r.lines = height > 0 ? height : 1;
+ widget_init (w, &r, listbox_callback, listbox_mouse_callback);
+ w->options |= WOP_SELECTABLE | WOP_WANT_HOTKEY;
+ w->keymap = listbox_map;
+
+ l->list = NULL;
+ l->top = l->current = 0;
+ l->deletable = deletable;
+ l->callback = callback;
+ l->allow_duplicates = TRUE;
+ l->scrollbar = !mc_global.tty.slow_terminal;
+
+ return l;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/**
+ * Finds item by its label.
+ */
+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);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/**
+ * Finds item by its 'data' slot.
+ */
+int
+listbox_search_data (WListbox * l, const void *data)
+{
+ 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->data == data)
+ return i;
+ }
+ }
+
+ return (-1);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* Select the first entry and scrolls the list to the top */
+void
+listbox_select_first (WListbox * l)
+{
+ l->current = l->top = 0;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* Selects the last entry and scrolls the list to the bottom */
+void
+listbox_select_last (WListbox * l)
+{
+ int lines = WIDGET (l)->rect.lines;
+ int length;
+
+ length = listbox_get_length (l);
+
+ l->current = DOZ (length, 1);
+ l->top = DOZ (length, lines);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+void
+listbox_set_current (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->current = dest;
+ if (!top_seen)
+ l->top = l->current;
+ else
+ {
+ int lines = WIDGET (l)->rect.lines;
+
+ if (l->current - l->top >= lines)
+ l->top = l->current - lines + 1;
+ }
+ return;
+ }
+ }
+
+ /* If we are unable to find it, set decent values */
+ l->current = l->top = 0;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+int
+listbox_get_length (const WListbox * l)
+{
+ return listbox_is_empty (l) ? 0 : (int) g_queue_get_length (l->list);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* Returns the current string text as well as the associated extra data */
+void
+listbox_get_current (WListbox * l, char **string, void **extra)
+{
+ WLEntry *e = NULL;
+ gboolean ok;
+
+ if (l != NULL)
+ e = listbox_get_nth_entry (l, l->current);
+
+ ok = (e != NULL);
+
+ if (string != NULL)
+ *string = ok ? e->text : NULL;
+
+ if (extra != NULL)
+ *extra = ok ? e->data : NULL;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+WLEntry *
+listbox_get_nth_entry (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->current);
+ listbox_entry_free (current->data);
+ g_queue_delete_link (l->list, current);
+
+ length = g_queue_get_length (l->list);
+
+ if (length == 0)
+ l->top = l->current = 0;
+ else if (l->current >= length)
+ l->current = length - 1;
+ }
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+gboolean
+listbox_is_empty (const WListbox * l)
+{
+ return (l == NULL || l->list == NULL || g_queue_is_empty (l->list));
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/**
+ * Set new listbox items list.
+ *
+ * @param l WListbox object
+ * @param list list of WLEntry objects
+ */
+void
+listbox_set_list (WListbox * l, GQueue * list)
+{
+ listbox_remove_list (l);
+
+ if (l != NULL)
+ l->list = list;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+void
+listbox_remove_list (WListbox * l)
+{
+ if (l != NULL)
+ {
+ if (l->list != NULL)
+ {
+ g_queue_free_full (l->list, (GDestroyNotify) listbox_entry_free);
+ l->list = NULL;
+ }
+
+ l->current = l->top = 0;
+ }
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+char *
+listbox_add_item (WListbox * l, listbox_append_t pos, int hotkey, const char *text, void *data,
+ gboolean free_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->free_data = free_data;
+ entry->hotkey = hotkey;
+
+ listbox_add_entry (l, entry, pos);
+
+ return entry->text;
+}
+
+/* --------------------------------------------------------------------------------------------- */