summaryrefslogtreecommitdiff
path: root/lib/menu.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/menu.c')
-rw-r--r--lib/menu.c727
1 files changed, 727 insertions, 0 deletions
diff --git a/lib/menu.c b/lib/menu.c
new file mode 100644
index 0000000..b7ab021
--- /dev/null
+++ b/lib/menu.c
@@ -0,0 +1,727 @@
+#include "internal.h"
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+
+/**
+ * Filter function map.
+ */
+static bmItem** (*filterFunc[BM_FILTER_MODE_LAST])(bmMenu *menu, char addition, unsigned int *outNmemb) = {
+ _bmFilterDmenu, /* BM_FILTER_DMENU */
+ _bmFilterDmenuCaseInsensitive /* BM_FILTER_DMENU_CASE_INSENSITIVE */
+};
+
+int _bmMenuItemIsSelected(const bmMenu *menu, const bmItem *item)
+{
+ assert(menu);
+ assert(item);
+
+ unsigned int i, count;
+ bmItem **items = bmMenuGetSelectedItems(menu, &count);
+ for (i = 0; i < count && items[i] != item; ++i);
+ return (i < count);
+}
+
+/**
+ * Create new bmMenu instance.
+ *
+ * @param drawMode Render method to be used for this menu instance.
+ * @return bmMenu for new menu instance, **NULL** if creation failed.
+ */
+bmMenu* bmMenuNew(bmDrawMode drawMode)
+{
+ bmMenu *menu = calloc(1, sizeof(bmMenu));
+
+ menu->drawMode = drawMode;
+
+ if (!menu)
+ return NULL;
+
+ int status = 1;
+
+ switch (menu->drawMode) {
+ case BM_DRAW_MODE_CURSES:
+ status = _bmDrawCursesInit(&menu->renderApi);
+ break;
+
+ default: break;
+ }
+
+ if (status == 0) {
+ bmMenuFree(menu);
+ return NULL;
+ }
+
+ return menu;
+}
+
+/**
+ * Release bmMenu instance.
+ *
+ * @param menu bmMenu instance to be freed from memory.
+ */
+void bmMenuFree(bmMenu *menu)
+{
+ assert(menu);
+
+ if (menu->renderApi.free)
+ menu->renderApi.free();
+
+ if (menu->title)
+ free(menu->title);
+
+ if (menu->filter)
+ free(menu->filter);
+
+ if (menu->oldFilter)
+ free(menu->oldFilter);
+
+ bmMenuFreeItems(menu);
+ free(menu);
+}
+
+/**
+ * Release items inside bmMenu instance.
+ *
+ * @param menu bmMenu instance which items will be freed from memory.
+ */
+void bmMenuFreeItems(bmMenu *menu)
+{
+ assert(menu);
+ _bmItemListFreeList(&menu->selection);
+ _bmItemListFreeList(&menu->filtered);
+ _bmItemListFreeItems(&menu->items);
+}
+
+/**
+ * Set userdata pointer to bmMenu instance.
+ * Userdata will be carried unmodified by the instance.
+ *
+ * @param menu bmMenu instance where to set userdata pointer.
+ * @param userdata Pointer to userdata.
+ */
+void bmMenuSetUserdata(bmMenu *menu, void *userdata)
+{
+ assert(menu);
+ menu->userdata = userdata;
+}
+
+/**
+ * Get userdata pointer from bmMenu instance.
+ *
+ * @param menu bmMenu instance which userdata pointer to get.
+ * @return Pointer for unmodified userdata.
+ */
+void* bmMenuGetUserdata(bmMenu *menu)
+{
+ assert(menu);
+ return menu->userdata;
+}
+
+/**
+ * Set filter text to bmMenu instance.
+ *
+ * @param menu bmMenu instance where to set filter.
+ * @param filter Null terminated C "string" to act as filter.
+ */
+void bmMenuSetFilter(bmMenu *menu, const char *filter)
+{
+ assert(menu);
+
+ if (menu->filter)
+ free(menu->filter);
+
+ menu->filter = (filter ? _bmStrdup(filter) : NULL);
+ menu->filterSize = (filter ? strlen(filter) : 0);
+}
+
+/**
+ * Get filter text from bmMenu instance.
+ *
+ * @param menu bmMenu instance where to get filter.
+ * @return Const pointer to current filter text, may be **NULL** if empty.
+ */
+const char* bmMenuGetFilter(bmMenu *menu)
+{
+ assert(menu);
+ return menu->filter;
+}
+
+/**
+ * Set active filter mode to bmMenu instance.
+ *
+ * @param menu bmMenu instance where to set filter mode.
+ * @param mode bmFilterMode constant.
+ */
+void bmMenuSetFilterMode(bmMenu *menu, bmFilterMode mode)
+{
+ assert(menu);
+ menu->filterMode = (mode >= BM_FILTER_MODE_LAST ? BM_FILTER_MODE_DMENU : mode);
+}
+
+/**
+ * Get active filter mode from bmMenu instance.
+ *
+ * @param menu bmMenu instance where to get filter mode.
+ * @return bmFilterMode constant.
+ */
+bmFilterMode bmMenuGetFilterMode(const bmMenu *menu)
+{
+ assert(menu);
+ return menu->filterMode;
+}
+
+/**
+ * Set selection wrapping on/off.
+ *
+ * @param menu bmMenu instance where to toggle selection wrapping.
+ * @param int 1 == on, 0 == off.
+ */
+void bmMenuSetWrap(bmMenu *menu, int wrap)
+{
+ assert(menu);
+ menu->wrap = (wrap ? 1 : 0);
+}
+
+/**
+ * Get selection wrapping state.
+ *
+ * @param menu bmMenu instance where to get selection wrapping state.
+ * @return int for wrap state.
+ */
+int bmMenuGetWrap(const bmMenu *menu)
+{
+ assert(menu);
+ return menu->wrap;
+}
+
+/**
+ * Set title to bmMenu instance.
+ *
+ * @param menu bmMenu instance where to set title.
+ * @param title C "string" to set as title, can be **NULL** for empty title.
+ */
+int bmMenuSetTitle(bmMenu *menu, const char *title)
+{
+ assert(menu);
+
+ char *copy = NULL;
+ if (title && !(copy = _bmStrdup(title)))
+ return 0;
+
+ if (menu->title)
+ free(menu->title);
+
+ menu->title = copy;
+ return 1;
+}
+
+/**
+ * Get title from bmMenu instance.
+ *
+ * @param menu bmMenu instance where to get title from.
+ * @return Pointer to null terminated C "string", can be **NULL** for empty title.
+ */
+const char* bmMenuGetTitle(const bmMenu *menu)
+{
+ assert(menu);
+ return menu->title;
+}
+
+/**
+ * Add item to bmMenu instance at specific index.
+ *
+ * @param menu bmMenu instance where item will be added.
+ * @param item bmItem instance to add.
+ * @param index Index where item will be added.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuAddItemAt(bmMenu *menu, bmItem *item, unsigned int index)
+{
+ assert(menu);
+ return _bmItemListAddItemAt(&menu->items, item, index);
+}
+
+/**
+ * Add item to bmMenu instance.
+ *
+ * @param menu bmMenu instance where item will be added.
+ * @param item bmItem instance to add.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuAddItem(bmMenu *menu, bmItem *item)
+{
+ return _bmItemListAddItem(&menu->items, item);
+}
+
+/**
+ * Remove item from bmMenu instance at specific index.
+ *
+ * @warning The item won't be freed, use bmItemFree to do that.
+ *
+ * @param menu bmMenu instance from where item will be removed.
+ * @param index Index of item to remove.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuRemoveItemAt(bmMenu *menu, unsigned int index)
+{
+ assert(menu);
+
+ if (!menu->items.list || menu->items.count <= index)
+ return 0;
+
+ bmItem *item = menu->items.list[index];
+ int ret = _bmItemListRemoveItemAt(&menu->items, index);
+
+ if (ret) {
+ _bmItemListRemoveItem(&menu->selection, item);
+ _bmItemListRemoveItem(&menu->filtered, item);
+ }
+
+ return ret;
+}
+
+/**
+ * Remove item from bmMenu instance.
+ *
+ * @warning The item won't be freed, use bmItemFree to do that.
+ *
+ * @param menu bmMenu instance from where item will be removed.
+ * @param item bmItem instance to remove.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuRemoveItem(bmMenu *menu, bmItem *item)
+{
+ assert(menu);
+
+ int ret = _bmItemListRemoveItem(&menu->items, item);
+
+ if (ret) {
+ _bmItemListRemoveItem(&menu->selection, item);
+ _bmItemListRemoveItem(&menu->filtered, item);
+ }
+
+ return ret;
+}
+
+/**
+ * Highlight item in menu by index.
+ *
+ * @param menu bmMenu instance from where to highlight item.
+ * @param index Index of item to highlight.
+ * @return 1 on successful highlight, 0 on failure.
+ */
+int bmMenuSetHighlightedIndex(bmMenu *menu, unsigned int index)
+{
+ assert(menu);
+
+ unsigned int itemsCount;
+ bmMenuGetFilteredItems(menu, &itemsCount);
+
+ if (itemsCount <= index)
+ return 0;
+
+ return (menu->index = index);
+}
+
+/**
+ * Highlight item in menu.
+ *
+ * @param menu bmMenu instance from where to highlight item.
+ * @param item bmItem instance to highlight.
+ * @return 1 on successful highlight, 0 on failure.
+ */
+int bmMenuSetHighlighted(bmMenu *menu, bmItem *item)
+{
+ assert(menu);
+
+ unsigned int i, itemsCount;
+ bmItem **items = bmMenuGetFilteredItems(menu, &itemsCount);
+ for (i = 0; i < itemsCount && items[i] != item; ++i);
+
+ if (itemsCount <= i)
+ return 0;
+
+ return (menu->index = i);
+}
+
+/**
+ * Get highlighted item from bmMenu instance.
+ *
+ * @warning The pointer returned by this function may be invalid after items change.
+ *
+ * @param menu bmMenu instance from where to get highlighted item.
+ * @return Selected bmItem instance, **NULL** if none highlighted.
+ */
+bmItem* bmMenuGetHighlightedItem(const bmMenu *menu)
+{
+ assert(menu);
+
+ unsigned int count;
+ bmItem **items = bmMenuGetFilteredItems(menu, &count);
+
+ if (!items || count <= menu->index)
+ return NULL;
+
+ return items[menu->index];
+}
+
+/**
+ * Set selected items to bmMenu instance.
+ *
+ * @param menu bmMenu instance where items will be set.
+ * @param items Array of bmItem pointers to set.
+ * @param nmemb Total count of items in array.
+ * @return 1 on successful set, 0 on failure.
+ */
+int bmMenuSetSelectedItems(bmMenu *menu, bmItem **items, unsigned int nmemb)
+{
+ assert(menu);
+
+ bmItem **newItems;
+ if (!(newItems = calloc(sizeof(bmItem*), nmemb)))
+ return 0;
+
+ memcpy(newItems, items, sizeof(bmItem*) * nmemb);
+ return _bmItemListSetItemsNoCopy(&menu->selection, newItems, nmemb);
+}
+
+/**
+ * Get selected items from bmMenu instance.
+ *
+ * @warning The pointer returned by this function may be invalid after selection or items change.
+ *
+ * @param menu bmMenu instance from where to get selected items.
+ * @param outNmemb Reference to unsigned int where total count of returned items will be stored.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** bmMenuGetSelectedItems(const bmMenu *menu, unsigned int *outNmemb)
+{
+ assert(menu);
+ return _bmItemListGetItems(&menu->selection, outNmemb);
+}
+
+/**
+ * Set items to bmMenu instance.
+ * Will replace all the old items on bmMenu instance.
+ *
+ * If items is **NULL**, or nmemb is zero, all items will be freed from the menu.
+ *
+ * @param menu bmMenu instance where items will be set.
+ * @param items Array of bmItem pointers to set.
+ * @param nmemb Total count of items in array.
+ * @return 1 on successful set, 0 on failure.
+ */
+int bmMenuSetItems(bmMenu *menu, const bmItem **items, unsigned int nmemb)
+{
+ assert(menu);
+
+ int ret = _bmItemListSetItems(&menu->items, items, nmemb);
+
+ if (ret) {
+ _bmItemListFreeList(&menu->selection);
+ _bmItemListFreeList(&menu->filtered);
+ }
+
+ return ret;
+}
+
+/**
+ * Get items from bmMenu instance.
+ *
+ * @warning The pointer returned by this function may be invalid after removing or adding new items.
+ *
+ * @param menu bmMenu instance from where to get items.
+ * @param outNmemb Reference to unsigned int where total count of returned items will be stored.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** bmMenuGetItems(const bmMenu *menu, unsigned int *outNmemb)
+{
+ assert(menu);
+ return _bmItemListGetItems(&menu->items, outNmemb);
+}
+
+/**
+ * Get filtered (displayed) items from bmMenu instance.
+ *
+ * @warning The pointer returned by this function _will_ be invalid when menu internally filters its list again.
+ * Do not store this pointer.
+ *
+ * @param menu bmMenu instance from where to get filtered items.
+ * @param outNmemb Reference to unsigned int where total count of returned items will be stored.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** bmMenuGetFilteredItems(const bmMenu *menu, unsigned int *outNmemb)
+{
+ assert(menu);
+
+ if (menu->filter && strlen(menu->filter))
+ return _bmItemListGetItems(&menu->filtered, outNmemb);
+
+ return _bmItemListGetItems(&menu->items, outNmemb);
+}
+
+/**
+ * Render bmMenu instance using chosen draw method.
+ *
+ * @param menu bmMenu instance to be rendered.
+ */
+void bmMenuRender(const bmMenu *menu)
+{
+ assert(menu);
+
+ if (menu->renderApi.render)
+ menu->renderApi.render(menu);
+}
+
+/**
+ * Trigger filtering of menu manually.
+ * This is useful when adding new items and want to dynamically see them filtered.
+ *
+ * Do note that filtering might be heavy, so you should only call it after batch manipulation of items.
+ * Not after manipulation of each single item.
+ *
+ * @param menu bmMenu instance which to filter.
+ */
+void bmMenuFilter(bmMenu *menu)
+{
+ assert(menu);
+
+ char addition = 0;
+ size_t len = (menu->filter ? strlen(menu->filter) : 0);
+
+ if (!len || !menu->items.list || menu->items.count <= 0) {
+ _bmItemListFreeList(&menu->filtered);
+
+ if (menu->oldFilter)
+ free(menu->oldFilter);
+
+ menu->oldFilter = NULL;
+ return;
+ }
+
+ if (menu->oldFilter) {
+ size_t oldLen = strlen(menu->oldFilter);
+ addition = (oldLen < len && !memcmp(menu->oldFilter, menu->filter, oldLen));
+ }
+
+ if (menu->oldFilter && addition && menu->filtered.count <= 0)
+ return;
+
+ if (menu->oldFilter && !strcmp(menu->filter, menu->oldFilter))
+ return;
+
+ unsigned int count;
+ bmItem **filtered = filterFunc[menu->filterMode](menu, addition, &count);
+
+ _bmItemListSetItemsNoCopy(&menu->filtered, filtered, count);
+ menu->index = 0;
+
+ if (menu->oldFilter)
+ free(menu->oldFilter);
+
+ menu->oldFilter = _bmStrdup(menu->filter);
+}
+
+/**
+ * Poll key and unicode from underlying UI toolkit.
+ *
+ * This function will block on @link ::bmDrawMode BM_DRAW_MODE_CURSES @endlink draw mode.
+ *
+ * @param menu bmMenu instance from which to poll.
+ * @param outUnicode Reference to unsigned int.
+ * @return bmKey for polled key.
+ */
+bmKey bmMenuGetKey(bmMenu *menu, unsigned int *outUnicode)
+{
+ assert(menu);
+ assert(outUnicode);
+
+ *outUnicode = 0;
+ bmKey key = BM_KEY_NONE;
+
+ if (menu->renderApi.getKey)
+ key = menu->renderApi.getKey(outUnicode);
+
+ return key;
+}
+
+/**
+ * Advances menu logic with key and unicode as input.
+ *
+ * @param menu bmMenu instance to be advanced.
+ * @param key Key input that will advance menu logic.
+ * @param unicode Unicode input that will advance menu logic.
+ * @return bmRunResult for menu state.
+ */
+bmRunResult bmMenuRunWithKey(bmMenu *menu, bmKey key, unsigned int unicode)
+{
+ assert(menu);
+
+ unsigned int itemsCount;
+ bmMenuGetFilteredItems(menu, &itemsCount);
+
+ unsigned int displayed = 0;
+ if (menu->renderApi.displayedCount)
+ displayed = menu->renderApi.displayedCount(menu);
+
+ if (!displayed)
+ displayed = itemsCount;
+
+ switch (key) {
+ case BM_KEY_LEFT:
+ if (menu->filter) {
+ unsigned int oldCursor = menu->cursor;
+ menu->cursor -= _bmUtf8RunePrev(menu->filter, menu->cursor);
+ menu->cursesCursor -= _bmUtf8RuneWidth(menu->filter + menu->cursor, oldCursor - menu->cursor);
+ }
+ break;
+
+ case BM_KEY_RIGHT:
+ if (menu->filter) {
+ unsigned int oldCursor = menu->cursor;
+ menu->cursor += _bmUtf8RuneNext(menu->filter, menu->cursor);
+ menu->cursesCursor += _bmUtf8RuneWidth(menu->filter + oldCursor, menu->cursor - oldCursor);
+ }
+ break;
+
+ case BM_KEY_HOME:
+ menu->cursesCursor = menu->cursor = 0;
+ break;
+
+ case BM_KEY_END:
+ menu->cursor = (menu->filter ? strlen(menu->filter) : 0);
+ menu->cursesCursor = (menu->filter ? _bmUtf8StringScreenWidth(menu->filter) : 0);
+ break;
+
+ case BM_KEY_UP:
+ if (menu->index > 0) {
+ menu->index--;
+ } else if (menu->wrap) {
+ menu->index = itemsCount - 1;
+ }
+ break;
+
+ case BM_KEY_DOWN:
+ if (menu->index < itemsCount - 1) {
+ menu->index++;
+ } else if (menu->wrap) {
+ menu->index = 0;
+ }
+ break;
+
+ case BM_KEY_PAGE_UP:
+ menu->index = (menu->index < displayed ? 0 : menu->index - (displayed - 1));
+ break;
+
+ case BM_KEY_PAGE_DOWN:
+ menu->index = (menu->index + displayed >= itemsCount ? itemsCount - 1 : menu->index + (displayed - 1));
+ break;
+
+ case BM_KEY_SHIFT_PAGE_UP:
+ menu->index = 0;
+ break;
+
+ case BM_KEY_SHIFT_PAGE_DOWN:
+ menu->index = itemsCount - 1;
+ break;
+
+ case BM_KEY_BACKSPACE:
+ if (menu->filter) {
+ size_t width;
+ menu->cursor -= _bmUtf8RuneRemove(menu->filter, menu->cursor, &width);
+ menu->cursesCursor -= width;
+ }
+ break;
+
+ case BM_KEY_DELETE:
+ if (menu->filter)
+ _bmUtf8RuneRemove(menu->filter, menu->cursor + 1, NULL);
+ break;
+
+ case BM_KEY_LINE_DELETE_LEFT:
+ if (menu->filter) {
+ while (menu->cursor > 0) {
+ size_t width;
+ menu->cursor -= _bmUtf8RuneRemove(menu->filter, menu->cursor, &width);
+ menu->cursesCursor -= width;
+ }
+ }
+ break;
+
+ case BM_KEY_LINE_DELETE_RIGHT:
+ if (menu->filter)
+ menu->filter[menu->cursor] = 0;
+ break;
+
+ case BM_KEY_WORD_DELETE:
+ if (menu->filter) {
+ while (menu->cursor < strlen(menu->filter) && !isspace(menu->filter[menu->cursor])) {
+ unsigned int oldCursor = menu->cursor;
+ menu->cursor += _bmUtf8RuneNext(menu->filter, menu->cursor);
+ menu->cursesCursor += _bmUtf8RuneWidth(menu->filter + oldCursor, menu->cursor - oldCursor);
+ }
+ while (menu->cursor > 0 && isspace(menu->filter[menu->cursor - 1])) {
+ unsigned int oldCursor = menu->cursor;
+ menu->cursor -= _bmUtf8RunePrev(menu->filter, menu->cursor);
+ menu->cursesCursor -= _bmUtf8RuneWidth(menu->filter + menu->cursor, oldCursor - menu->cursor);
+ }
+ while (menu->cursor > 0 && !isspace(menu->filter[menu->cursor - 1])) {
+ size_t width;
+ menu->cursor -= _bmUtf8RuneRemove(menu->filter, menu->cursor, &width);
+ menu->cursesCursor -= width;
+ }
+ }
+ break;
+
+ case BM_KEY_UNICODE:
+ {
+ size_t width;
+ menu->cursor += _bmUnicodeInsert(&menu->filter, &menu->filterSize, menu->cursor, unicode, &width);
+ menu->cursesCursor += width;
+ }
+ break;
+
+ case BM_KEY_TAB:
+ {
+ const char *text;
+ bmItem *highlighted = bmMenuGetHighlightedItem(menu);
+ if (highlighted && (text = bmItemGetText(highlighted))) {
+ bmMenuSetFilter(menu, text);
+ menu->cursor = (menu->filter ? strlen(menu->filter) : 0);
+ menu->cursesCursor = (menu->filter ? _bmUtf8StringScreenWidth(menu->filter) : 0);
+ }
+ }
+ break;
+
+ case BM_KEY_CONTROL_RETURN:
+ case BM_KEY_RETURN:
+ {
+ bmItem *highlighted = bmMenuGetHighlightedItem(menu);
+ if (highlighted && !_bmMenuItemIsSelected(menu, highlighted))
+ _bmItemListAddItem(&menu->selection, highlighted);
+ }
+ break;
+
+ case BM_KEY_SHIFT_RETURN:
+ case BM_KEY_ESCAPE:
+ _bmItemListFreeList(&menu->selection);
+ break;
+
+ default: break;
+ }
+
+ bmMenuFilter(menu);
+
+ switch (key) {
+ case BM_KEY_SHIFT_RETURN:
+ case BM_KEY_RETURN: return BM_RUN_RESULT_SELECTED;
+ case BM_KEY_ESCAPE: return BM_RUN_RESULT_CANCEL;
+ default: break;
+ }
+
+ return BM_RUN_RESULT_RUNNING;
+}
+
+/* vim: set ts=8 sw=4 tw=0 :*/