Lagrange [release]

Preferences: Basic key bindings UI

=> f809b80a688b8cd0a5a8bcb578d4cce1562fff5e

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7d73956d..cfd42044 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -123,6 +123,8 @@ set (SOURCES
     src/audio/player.h
     src/audio/stb_vorbis.c
     # User interface:
+    src/ui/bindingswidget.c
+    src/ui/bindingswidget.h
     src/ui/color.c
     src/ui/color.h
     src/ui/command.c
diff --git a/src/app.c b/src/app.c
index 10f5f0d2..311a88bb 100644
--- a/src/app.c
+++ b/src/app.c
@@ -430,6 +430,7 @@ static void init_App_(iApp *d, int argc, char **argv) {
 static void deinit_App(iApp *d) {
     saveState_App_(d);
     save_Keys(dataDir_App_);
+    deinit_Keys();
     savePrefs_App_(d);
     deinit_Prefs(&d->prefs);
     save_Bookmarks(d->bookmarks, dataDir_App_);
diff --git a/src/ui/bindingswidget.c b/src/ui/bindingswidget.c
new file mode 100644
index 00000000..4ce6ea4d
--- /dev/null
+++ b/src/ui/bindingswidget.c
@@ -0,0 +1,193 @@
+/* Copyright 2020 Jaakko Keränen 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+#include "bindingswidget.h"
+#include "listwidget.h"
+#include "keys.h"
+#include "command.h"
+#include "util.h"
+#include "app.h"
+
+iDeclareType(BindingItem)
+typedef iListItemClass iBindingItemClass;
+
+struct Impl_BindingItem {
+    iListItem listItem;
+    iString   label;
+    iString   key;
+    int       id;
+    iBool     isWaitingForEvent;
+};
+
+void init_BindingItem(iBindingItem *d) {
+    init_ListItem(&d->listItem);
+    init_String(&d->label);
+    init_String(&d->key);
+    d->id = 0;
+    d->isWaitingForEvent = iFalse;
+}
+
+void deinit_BindingItem(iBindingItem *d) {
+    deinit_String(&d->key);
+    deinit_String(&d->label);
+}
+
+static void setKey_BindingItem_(iBindingItem *d, int key, int mods) {
+    setKey_Binding(d->id, key, mods);
+    clear_String(&d->key);
+    toString_Sym(key, mods, &d->key);
+}
+
+static void draw_BindingItem_(const iBindingItem *d, iPaint *p, iRect itemRect,
+                              const iListWidget *list);
+
+iBeginDefineSubclass(BindingItem, ListItem)
+    .draw = (iAny *) draw_BindingItem_,
+iEndDefineSubclass(BindingItem)
+
+iDefineObjectConstruction(BindingItem)
+
+/*----------------------------------------------------------------------------------------------*/
+
+struct Impl_BindingsWidget {
+    iWidget widget;
+    iListWidget *list;
+    size_t activePos;
+};
+
+iDefineObjectConstruction(BindingsWidget)
+
+static int cmpId_BindingItem_(const iListItem **item1, const iListItem **item2) {
+    const iBindingItem *d = (const iBindingItem *) *item1, *other = (const iBindingItem *) *item2;
+    return iCmp(d->id, other->id);
+}
+
+static void updateItems_BindingsWidget_(iBindingsWidget *d) {
+    clear_ListWidget(d->list);
+    iConstForEach(PtrArray, i, list_Keys()) {
+        const iBinding *bind = i.ptr;
+        if (isEmpty_String(&bind->label)) {
+            /* Only the ones with label are user-changeable. */
+            continue;
+        }
+        iBindingItem *item = new_BindingItem();
+        item->id = bind->id;
+        set_String(&item->label, &bind->label);
+        toString_Sym(bind->key, bind->mods, &item->key);
+        addItem_ListWidget(d->list, item);
+    }
+    sort_ListWidget(d->list, cmpId_BindingItem_);
+    updateVisible_ListWidget(d->list);
+    invalidate_ListWidget(d->list);
+}
+
+void init_BindingsWidget(iBindingsWidget *d) {
+    iWidget *w = as_Widget(d);
+    init_Widget(w);
+    setFlags_Widget(w, resizeChildren_WidgetFlag, iTrue);
+    d->activePos = iInvalidPos;
+    d->list = new_ListWidget();
+    setItemHeight_ListWidget(d->list, lineHeight_Text(uiLabel_FontId) * 1.5f);
+    addChild_Widget(w, iClob(d->list));
+    updateItems_BindingsWidget_(d);
+}
+
+void deinit_BindingsWidget(iBindingsWidget *d) {
+    /* nothing to do */
+    iUnused(d);
+}
+
+static void setActiveItem_BindingsWidget_(iBindingsWidget *d, size_t pos) {
+    if (d->activePos != iInvalidPos) {
+        iBindingItem *item = item_ListWidget(d->list, d->activePos);
+        item->isWaitingForEvent = iFalse;
+        invalidateItem_ListWidget(d->list, d->activePos);
+    }
+    d->activePos = pos;
+    if (d->activePos != iInvalidPos) {
+        iBindingItem *item = item_ListWidget(d->list, d->activePos);
+        item->isWaitingForEvent = iTrue;
+        invalidateItem_ListWidget(d->list, d->activePos);
+    }
+}
+
+static iBool processEvent_BindingsWidget_(iBindingsWidget *d, const SDL_Event *ev) {
+    iWidget *   w   = as_Widget(d);
+    const char *cmd = command_UserEvent(ev);
+    if (isCommand_Widget(w, ev, "list.clicked")) {
+        setActiveItem_BindingsWidget_(d, arg_Command(cmd));
+        return iTrue;
+    }
+    /* Waiting for a keypress? */
+    if (d->activePos != iInvalidPos) {
+        if (ev->type == SDL_KEYDOWN && !isMod_Sym(ev->key.keysym.sym)) {
+            setKey_BindingItem_(item_ListWidget(d->list, d->activePos),
+                                ev->key.keysym.sym,
+                                keyMods_Sym(ev->key.keysym.mod));
+            setActiveItem_BindingsWidget_(d, iInvalidPos);
+            postCommand_App("bindings.changed");
+            return iTrue;
+        }
+    }
+    return processEvent_Widget(w, ev);
+}
+
+static void draw_BindingsWidget_(const iBindingsWidget *d) {
+    const iWidget *w = constAs_Widget(d);
+    drawChildren_Widget(w);
+    drawBackground_Widget(w); /* kludge to allow drawing a top border over the list */
+}
+
+static void draw_BindingItem_(const iBindingItem *d, iPaint *p, iRect itemRect,
+                              const iListWidget *list) {
+    const int   font       = uiLabel_FontId;
+    const int   itemHeight = height_Rect(itemRect);
+    const int   line       = lineHeight_Text(font);
+    int         fg         = uiText_ColorId;
+    const iBool isPressing = isMouseDown_ListWidget(list) || d->isWaitingForEvent;
+    const iBool isHover    = (isHover_Widget(constAs_Widget(list)) &&
+                              constHoverItem_ListWidget(list) == d);
+    if (isHover || isPressing) {
+        fg = isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId;
+        fillRect_Paint(p,
+                       itemRect,
+                       isPressing ? uiBackgroundPressed_ColorId
+                                  : uiBackgroundFramelessHover_ColorId);
+    }
+    const int y = top_Rect(itemRect) + (itemHeight - line) / 2;
+    drawRange_Text(font,
+                   init_I2(left_Rect(itemRect) + 3 * gap_UI, y),
+                   fg,
+                   range_String(&d->label));
+    drawAlign_Text(d->isWaitingForEvent ? uiContent_FontId : font,
+                   init_I2(right_Rect(itemRect) - 3 * gap_UI,
+                           y - (lineHeight_Text(uiContent_FontId) - line) / 2),
+                   fg,
+                   right_Alignment,
+                   "%s",
+                   d->isWaitingForEvent ? "\U0001F449 \u2328" : cstr_String(&d->key));
+}
+
+iBeginDefineSubclass(BindingsWidget, Widget)
+    .processEvent = (iAny *) processEvent_BindingsWidget_,
+    .draw         = (iAny *) draw_BindingsWidget_,
+iEndDefineSubclass(BindingsWidget)
diff --git a/src/ui/bindingswidget.h b/src/ui/bindingswidget.h
new file mode 100644
index 00000000..e1ed402c
--- /dev/null
+++ b/src/ui/bindingswidget.h
@@ -0,0 +1,28 @@
+/* Copyright 2020 Jaakko Keränen 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
+
+#pragma once
+
+#include "widget.h"
+
+iDeclareWidgetClass(BindingsWidget)
+iDeclareObjectConstruction(BindingsWidget)
diff --git a/src/ui/keys.c b/src/ui/keys.c
index 85304ef7..cc7dabef 100644
--- a/src/ui/keys.c
+++ b/src/ui/keys.c
@@ -24,12 +24,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "util.h"
 #include "app.h"
 
-#include 
+#include 
 
 iDeclareType(Keys)
 
-static int cmp_Binding_(const void *a, const void *b) {
-    const iBinding *d = a, *other = b;
+static int cmpPtr_Binding_(const void *a, const void *b) {
+    const iBinding *d = *(const void **) a, *other = *(const void **) b;
     const int cmp = iCmp(d->key, other->key);
     if (cmp == 0) {
         return iCmp(d->mods, other->mods);
@@ -37,14 +37,17 @@ static int cmp_Binding_(const void *a, const void *b) {
     return cmp;
 }
 
+/*----------------------------------------------------------------------------------------------*/
+
 struct Impl_Keys {
-    iSortedArray bindings;
+    iArray  bindings;
+    iPtrSet lookup; /* quick key/mods lookup */
 };
 
 static iKeys keys_;
 
 static void clear_Keys_(iKeys *d) {
-    iForEach(Array, i, &d->bindings.values) {
+    iForEach(Array, i, &d->bindings) {
         iBinding *bind = i.value;
         deinit_String(&bind->command);
         deinit_String(&bind->label);
@@ -52,27 +55,41 @@ static void clear_Keys_(iKeys *d) {
 }
 
 static void bindDefaults_(void) {
-    bind_Keys("scroll.top", SDLK_HOME, 0);
-    bind_Keys("scroll.bottom", SDLK_END, 0);
-    bind_Keys("scroll.step arg:-1", SDLK_UP, 0);
-    bind_Keys("scroll.step arg:1", SDLK_DOWN, 0);
-    bind_Keys("scroll.page arg:-1", SDLK_PAGEUP, 0);
-    bind_Keys("scroll.page arg:1", SDLK_PAGEDOWN, 0);
-    bind_Keys("scroll.page arg:-1", SDLK_SPACE, KMOD_SHIFT);
-    bind_Keys("scroll.page arg:1", SDLK_SPACE, 0);
+    /* TODO: This indirection could be used for localization, although all UI strings
+       would need to be similarly handled. */
+    bindLabel_Keys(1, "scroll.top", SDLK_HOME, 0, "Jump to top");
+    bindLabel_Keys(2, "scroll.bottom", SDLK_END, 0, "Jump to bottom");
+    bindLabel_Keys(10, "scroll.step arg:-1", SDLK_UP, 0, "Scroll up");
+    bindLabel_Keys(11, "scroll.step arg:1", SDLK_DOWN, 0, "Scroll down");
+    bindLabel_Keys(20, "scroll.page arg:-1", SDLK_PAGEUP, 0, "Scroll up half a page");
+    bindLabel_Keys(21, "scroll.page arg:1", SDLK_PAGEDOWN, 0, "Scroll down half a page");
+    /* The following cannot currently be changed (built-in duplicates). */
+    bind_Keys(1000, "scroll.page arg:-1", SDLK_SPACE, KMOD_SHIFT);
+    bind_Keys(1001, "scroll.page arg:1", SDLK_SPACE, 0);
 }
 
 static iBinding *find_Keys_(iKeys *d, int key, int mods) {
     size_t pos;
-    if (locate_SortedArray(&d->bindings, &(iBinding){ .key = key, .mods = mods }, &pos)) {
-        return at_SortedArray(&d->bindings, pos);
+    const iBinding elem = { .key = key, .mods = mods };
+    if (locate_PtrSet(&d->lookup, &elem, &pos)) {
+        return at_PtrSet(&d->lookup, pos);
+    }
+    return NULL;
+}
+
+static iBinding *findId_Keys_(iKeys *d, int id) {
+    iForEach(Array, i, &d->bindings) {
+        iBinding *bind = i.value;
+        if (bind->id == id) {
+            return bind;
+        }
     }
     return NULL;
 }
 
 static iBinding *findCommand_Keys_(iKeys *d, const char *command) {
     /* Note: O(n) */
-    iForEach(Array, i, &d->bindings.values) {
+    iForEach(Array, i, &d->bindings) {
         iBinding *bind = i.value;
         if (!cmp_String(&bind->command, command)) {
             return bind;
@@ -81,18 +98,37 @@ static iBinding *findCommand_Keys_(iKeys *d, const char *command) {
     return NULL;
 }
 
+static void updateLookup_Keys_(iKeys *d) {
+    clear_PtrSet(&d->lookup);
+    iConstForEach(Array, i, &d->bindings) {
+        insert_PtrSet(&d->lookup, i.value);
+    }
+}
+
+void setKey_Binding(int id, int key, int mods) {
+    iBinding *bind = findId_Keys_(&keys_, id);
+    if (bind) {
+        bind->key = key;
+        bind->mods = mods;
+        updateLookup_Keys_(&keys_);
+    }
+}
+
 /*----------------------------------------------------------------------------------------------*/
 
 void init_Keys(void) {
     iKeys *d = &keys_;
-    init_SortedArray(&d->bindings, sizeof(iBinding), cmp_Binding_);
+    init_Array(&d->bindings, sizeof(iBinding));
+    initCmp_PtrSet(&d->lookup, cmpPtr_Binding_);
     bindDefaults_();
+    updateLookup_Keys_(d);
 }
 
 void deinit_Keys(void) {
     iKeys *d = &keys_;
     clear_Keys_(d);
-    deinit_SortedArray(&d->bindings);
+    deinit_PtrSet(&d->lookup);
+    deinit_Array(&d->bindings);
 }
 
 void load_Keys(const char *saveDir) {
@@ -103,34 +139,42 @@ void save_Keys(const char *saveDir) {
 
 }
 
-void bind_Keys(const char *command, int key, int mods) {
+void bind_Keys(int id, const char *command, int key, int mods) {
     iKeys *d = &keys_;
-    iBinding *bind = find_Keys_(d, key, mods);
-    if (bind) {
-        setCStr_String(&bind->command, command);
+    iBinding *bind = findId_Keys_(d, id);
+    if (!bind) {
+        iBinding elem = { .id = id, .key = key, .mods = mods };
+        initCStr_String(&elem.command, command);
+        init_String(&elem.label);
+        pushBack_Array(&d->bindings, &elem);
     }
     else {
-        iBinding bind;
-        bind.key = key;
-        bind.mods = mods;
-        initCStr_String(&bind.command, command);
-        init_String(&bind.label);
-        insert_SortedArray(&d->bindings, &bind);
+        setCStr_String(&bind->command, command);
+        bind->key  = key;
+        bind->mods = mods;
     }
 }
 
-void setLabel_Keys(const char *command, const char *label) {
-    iBinding *bind = findCommand_Keys_(&keys_, command);
+void setLabel_Keys(int id, const char *label) {
+    iBinding *bind = findId_Keys_(&keys_, id);
     if (bind) {
         setCStr_String(&bind->label, label);
     }
 }
 
-//const iString *label_Keys(const char *command) {
-
-//}
-
-//const char *shortcutLabel_Keys(const char *command) {}
+#if 0
+const iString *label_Keys(const char *command) {
+    iKeys *d = &keys_;
+    /* TODO: A hash wouldn't hurt here. */
+    iConstForEach(PtrSet, i, &d->bindings) {
+        const iBinding *bind = *i.value;
+        if (!cmp_String(&bind->command, command) && !isEmpty_String(&bind->label)) {
+            return &bind->label;
+        }
+    }
+    return collectNew_String();
+}
+#endif
 
 iBool processEvent_Keys(const SDL_Event *ev) {
     iKeys *d = &keys_;
@@ -147,3 +191,12 @@ iBool processEvent_Keys(const SDL_Event *ev) {
 const iBinding *findCommand_Keys(const char *command) {
     return findCommand_Keys_(&keys_, command);
 }
+
+const iPtrArray *list_Keys(void) {
+    iKeys *d = &keys_;
+    iPtrArray *list = collectNew_PtrArray();
+    iConstForEach(Array, i, &d->bindings) {
+        pushBack_PtrArray(list, i.value);
+    }
+    return list;
+}
diff --git a/src/ui/keys.h b/src/ui/keys.h
index 0892bd81..a4c8f348 100644
--- a/src/ui/keys.h
+++ b/src/ui/keys.h
@@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #pragma once
 
 #include 
+#include 
 #include 
 
 #if defined (iPlatformApple)
@@ -46,23 +47,32 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 iDeclareType(Binding)
 
 struct Impl_Binding {
+    int id;
     int key;
     int mods;
     iString command;
     iString label;
 };
 
+void            setKey_Binding      (int id, int key, int mods);
+
+/*----------------------------------------------------------------------------------------------*/
+
 void            init_Keys           (void);
 void            deinit_Keys         (void);
 
 void            load_Keys           (const char *saveDir);
 void            save_Keys           (const char *saveDir);
 
-void            bind_Keys           (const char *command, int key, int mods);
-void            setLabel_Keys       (const char *command, const char *label);
-const iBinding *findCommand_Keys    (const char *command);
+void            bind_Keys           (int id, const char *command, int key, int mods);
+void            setLabel_Keys       (int id, const char *label);
 
-//const iString * label_Keys          (const char *command);
-//const char *    shortcutLabel_Keys  (const char *command);
+iLocalDef void bindLabel_Keys(int id, const char *command, int key, int mods, const char *label) {
+    bind_Keys(id, command, key, mods);
+    setLabel_Keys(id, label);
+}
+
+const iBinding *findCommand_Keys    (const char *command);
 
 iBool           processEvent_Keys   (const SDL_Event *);
+const iPtrArray *list_Keys          (void);
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c
index fb328c2f..02e1c728 100644
--- a/src/ui/listwidget.c
+++ b/src/ui/listwidget.c
@@ -120,6 +120,9 @@ void updateVisible_ListWidget(iListWidget *d) {
     const int   contentSize = size_PtrArray(&d->items) * d->itemHeight;
     const iRect bounds      = innerBounds_Widget(as_Widget(d));
     const iBool wasVisible  = isVisible_Widget(d->scroll);
+    if (area_Rect(bounds) == 0) {
+        return;
+    }
     setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax_ListWidget_(d) });
     setThumb_ScrollWidget(d->scroll,
                           d->scrollY,
@@ -245,11 +248,21 @@ void updateMouseHover_ListWidget(iListWidget *d) {
     setHoverItem_ListWidget_(d, itemIndex_ListWidget(d, mouse));
 }
 
+void sort_ListWidget(iListWidget *d, int (*cmp)(const iListItem **item1, const iListItem **item2)) {
+    sort_Array(&d->items, (iSortedArrayCompareElemFunc) cmp);
+}
+
 static void redrawHoverItem_ListWidget_(iListWidget *d) {
     insert_IntSet(&d->invalidItems, d->hoverItem);
     refresh_Widget(as_Widget(d));
 }
 
+static void sizeChanged_ListWidget_(iListWidget *d) {
+    printf("ListWidget %p size changed: %d x %d\n", d, d->widget.rect.size.x, d->widget.rect.size.y); fflush(stdout);
+    updateVisible_ListWidget(d);
+    invalidate_ListWidget(d);
+}
+
 static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
     iWidget *w = as_Widget(d);
     if (isCommand_SDLEvent(ev)) {
@@ -391,4 +404,5 @@ iBool isMouseDown_ListWidget(const iListWidget *d) {
 iBeginDefineSubclass(ListWidget, Widget)
     .processEvent = (iAny *) processEvent_ListWidget_,
     .draw         = (iAny *) draw_ListWidget_,
+    .sizeChanged  = (iAny *) sizeChanged_ListWidget_,
 iEndDefineSubclass(ListWidget)
diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h
index da6303e9..11f1672e 100644
--- a/src/ui/listwidget.h
+++ b/src/ui/listwidget.h
@@ -64,6 +64,8 @@ void    scrollOffset_ListWidget     (iListWidget *, int offset);
 void    updateVisible_ListWidget    (iListWidget *);
 void    updateMouseHover_ListWidget (iListWidget *);
 
+void                sort_ListWidget             (iListWidget *, int (*cmp)(const iListItem **item1, const iListItem **item2));
+
 iAnyObject *        item_ListWidget             (iListWidget *, size_t index);
 iAnyObject *        hoverItem_ListWidget        (iListWidget *);
 
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index d6292f5b..c8571589 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -485,8 +485,6 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
     /* Handle commands. */
     if (isResize_UserEvent(ev)) {
         checkModeButtonLayout_SidebarWidget_(d);
-        updateVisible_ListWidget(d->list);
-        invalidate_ListWidget(d->list);
     }
     else if (ev->type == SDL_USEREVENT && ev->user.code == command_UserEventCode) {
         const char *cmd = command_UserEvent(ev);
diff --git a/src/ui/util.c b/src/ui/util.c
index 44f7e089..ceab01b8 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include "gmutil.h"
 #include "labelwidget.h"
 #include "inputwidget.h"
+#include "bindingswidget.h"
 #include "keys.h"
 #include "widget.h"
 #include "text.h"
@@ -105,6 +106,11 @@ void toString_Sym(int key, int kmods, iString *str) {
     }
 }
 
+iBool isMod_Sym(int key) {
+    return key == SDLK_LALT || key == SDLK_RALT || key == SDLK_LCTRL || key == SDLK_RCTRL ||
+           key == SDLK_LGUI || key == SDLK_RGUI || key == SDLK_LSHIFT || key == SDLK_RSHIFT;
+}
+
 int keyMods_Sym(int kmods) {
     kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI);
     /* Don't treat left/right modifiers differently. */
@@ -920,12 +926,22 @@ iWidget *makeToggle_Widget(const char *id) {
     return toggle;
 }
 
+static void appendFramelessTabPage_(iWidget *tabs, iWidget *page, const char *title, int shortcut,
+                                    int kmods) {
+    appendTabPage_Widget(tabs, page, title, shortcut, kmods);
+    setFlags_Widget(
+        (iWidget *) back_ObjectList(children_Widget(findChild_Widget(tabs, "tabs.buttons"))),
+        frameless_WidgetFlag,
+        iTrue);
+}
+
 static iWidget *appendTwoColumnPage_(iWidget *tabs, const char *title, int shortcut, iWidget **headings,
                                      iWidget **values) {
     iWidget *page = new_Widget();
     setFlags_Widget(page, arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag |
                     resizeHeightOfChildren_WidgetFlag | borderTop_WidgetFlag, iTrue);
     addChildFlags_Widget(page, iClob(new_Widget()), expand_WidgetFlag);
+    setPadding_Widget(page, 0, gap_UI, 0, gap_UI);
     iWidget *columns = new_Widget();
     addChildFlags_Widget(page, iClob(columns), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
     *headings = addChildFlags_Widget(
@@ -933,11 +949,7 @@ static iWidget *appendTwoColumnPage_(iWidget *tabs, const char *title, int short
     *values = addChildFlags_Widget(
         columns, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
     addChildFlags_Widget(page, iClob(new_Widget()), expand_WidgetFlag);
-    appendTabPage_Widget(tabs, page, title, shortcut, shortcut ? KMOD_PRIMARY : 0);
-    setFlags_Widget(
-        (iWidget *) back_ObjectList(children_Widget(findChild_Widget(tabs, "tabs.buttons"))),
-        frameless_WidgetFlag,
-        iTrue);
+    appendFramelessTabPage_(tabs, page, title, shortcut, shortcut ? KMOD_PRIMARY : 0);
     return page;
 }
 
@@ -1080,6 +1092,11 @@ iWidget *makePreferences_Widget(void) {
         addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:")));
         setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http");
     }
+    /* Keybindings. */ {
+        iBindingsWidget *bind = new_BindingsWidget();
+        setFlags_Widget(as_Widget(bind), borderTop_WidgetFlag, iTrue);
+        appendFramelessTabPage_(tabs, iClob(bind), "Bindings", '5', KMOD_PRIMARY);
+    }
     resizeToLargestPage_Widget(tabs);
     arrange_Widget(dlg);
     /* Set input field sizes. */ {
diff --git a/src/ui/util.h b/src/ui/util.h
index 9796b387..c0e3a04c 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -48,6 +48,7 @@ iLocalDef iBool isResize_UserEvent(const SDL_Event *d) {
 #   define KMOD_SECONDARY   KMOD_GUI
 #endif
 
+iBool       isMod_Sym           (int key);
 int         keyMods_Sym         (int kmods); /* shift, alt, control, or gui */
 void        toString_Sym        (int key, int kmods, iString *str);
 
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/release/cdiff/f809b80a688b8cd0a5a8bcb578d4cce1562fff5e
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
71.354325 milliseconds
Gemini-to-HTML Time
1.287666 milliseconds

This content has been proxied by September (ba2dc).