Lagrange [work/v1.9]

Keyboard navigation mode for home row keys

=> a6314e152b2d2f306bcbb880356d3890efbfc89e

diff --git a/res/about/version.gmi b/res/about/version.gmi
index 1b8e502a..48b4cbd1 100644
--- a/res/about/version.gmi
+++ b/res/about/version.gmi
@@ -7,9 +7,10 @@
 # Release notes
 
 ## 0.10
-* Added option to load inline images when pressing Space or ↓ for a more focused reading experience — just keep tapping on a single key to proceed. If an image link is visible, it will be loaded instead of scrolling. This option is disabled by default.
+* Added option to load inline images when pressing Space or ↓ for a more focused reading experience — just keep tapping a single key to proceed. If an image link is visible, it will be loaded instead of scrolling. This option is disabled by default.
 * Added an option to use a proxy server for Gemini requests.
-* Added a keybinding to activate keyboard link navigation mode (default is "F").
+* Added a new keyboard link navigation mode focusing on the home row keys. The default keybinding for this is "F".
+* Added a keybinding to activate keyboard link modifier mode. The keyboard link keys are active while the modifier is held down. The default is ${ALT}.
 * Clearing and resetting keybindings via a context menu.
 * Added a Window tab in the Preferences dialog; moved some of the settings around for better organization.
 * Improved page search visualization: if the match is inside a link URL, the link icon is now highlighted. Previously these matches were not visualized in any way.
diff --git a/src/ui/bindingswidget.c b/src/ui/bindingswidget.c
index ff68ea7b..dee844db 100644
--- a/src/ui/bindingswidget.c
+++ b/src/ui/bindingswidget.c
@@ -195,6 +195,12 @@ static iBool processEvent_BindingsWidget_(iBindingsWidget *d, const SDL_Event *e
             postCommand_App("bindings.changed");
             return iTrue;
         }
+        else if (ev->type == SDL_KEYUP && isMod_Sym(ev->key.keysym.sym)) {
+            setKey_BindingItem_(item_ListWidget(d->list, d->activePos), ev->key.keysym.sym, 0);
+            setActiveItem_BindingsWidget_(d, iInvalidPos);
+            postCommand_App("bindings.changed");
+            return iTrue;
+        }
     }
     return processEvent_Widget(w, ev);
 }
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index b1c166aa..3cf564ac 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -125,11 +125,17 @@ enum iDocumentWidgetFlag {
     showLinkNumbers_DocumentWidgetFlag       = iBit(3),
 };
 
+enum iDocumentLinkOrdinalMode {
+    numbersAndAlphabet_DocumentLinkOrdinalMode,
+    homeRow_DocumentLinkOrdinalMode,
+};
+
 struct Impl_DocumentWidget {
     iWidget        widget;
     enum iRequestState state;
     iPersistentDocumentState mod;
     int            flags;
+    enum iDocumentLinkOrdinalMode ordinalMode;
     iString *      titleUser;
     iGmRequest *   request;
     iAtomicInt     isRequestUpdated; /* request has new content, need to parse it */
@@ -228,7 +234,6 @@ void init_DocumentWidget(iDocumentWidget *d) {
     addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward");
     addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent");
     addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");
-    addAction_Widget(w, 'f', 0, "document.linkkeys");
 }
 
 void deinit_DocumentWidget(iDocumentWidget *d) {
@@ -371,6 +376,15 @@ static void invalidateLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) {
     }
 }
 
+static void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) {
+    iConstForEach(PtrArray, i, &d->visibleLinks) {
+        const iGmRun *run = i.ptr;
+        if (run->linkId) {
+            insert_PtrSet(d->invalidRuns, run);
+        }
+    }
+}
+
 static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
     const iWidget *w            = constAs_Widget(d);
     const iRect    docBounds    = documentBounds_DocumentWidget_(d);
@@ -969,7 +983,7 @@ static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int dur
     /* Get rid of link numbers when scrolling. */
     if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) {
         d->flags &= ~showLinkNumbers_DocumentWidgetFlag;
-        invalidate_DocumentWidget_(d);
+        invalidateVisibleLinks_DocumentWidget_(d);
     }
     if (!prefs_App()->smoothScrolling) {
         duration = 0; /* always instant */
@@ -1542,8 +1556,14 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
         return iTrue;
     }
     else if (equal_Command(cmd, "document.linkkeys") && document_App() == d) {
-        iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue);
-        invalidate_DocumentWidget_(d);
+        if (argLabel_Command(cmd, "release")) {
+            iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
+        }
+        else {
+            d->ordinalMode = arg_Command(cmd);
+            iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue);
+        }
+        invalidateVisibleLinks_DocumentWidget_(d);
         refresh_Widget(d);
         return iTrue;
     }
@@ -1815,45 +1835,75 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E
     return iFalse;
 }
 
-static size_t linkOrdinalFromKey_(int key) {
-    if (key >= '1' && key <= '9') {
-        return key - '1';
-    }
-    if (key < 'a' || key > 'z') {
-        return iInvalidPos;
-    }
-    int ord = key - 'a' + 9;
+/* Sorted by proximity to F and J. */
+static const int homeRowKeys_[] = {
+    'f', 'd', 's', 'a',
+    'j', 'k', 'l',
+    'r', 'e', 'w', 'q',
+    'u', 'i', 'o', 'p',
+    'v', 'c', 'x', 'z',
+    'm', 'n',
+    'g', 'h',
+    'b',
+    't', 'y', 'u',
+};
+
+static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) {
+    size_t ord = iInvalidPos;
+    if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
+        if (key >= '1' && key <= '9') {
+            return key - '1';
+        }
+        if (key < 'a' || key > 'z') {
+            return iInvalidPos;
+        }
+        ord = key - 'a' + 9;
 #if defined (iPlatformApple)
-    /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */
-    if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {
-        return iInvalidPos;
-    }
-    if (key > 'h') ord--;
-    if (key > 'm') ord--;
-    if (key > 'q') ord--;
-    if (key > 'w') ord--;
+        /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */
+        if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {
+            return iInvalidPos;
+        }
+        if (key > 'h') ord--;
+        if (key > 'm') ord--;
+        if (key > 'q') ord--;
+        if (key > 'w') ord--;
 #endif
+    }
+    else {
+        iForIndices(i, homeRowKeys_) {
+            if (homeRowKeys_[i] == key) {
+                return i;
+            }
+        }
+    }
     return ord;
 }
 
-static iChar linkOrdinalChar_(size_t ord) {
-    if (ord < 9) {
-        return 0x278a + ord;
-    }
+static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
+    if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
+        if (ord < 9) {
+            return 0x278a + ord;
+        }
 #if defined (iPlatformApple)
-    if (ord < 9 + 22) {
-        int key = 'a' + ord - 9;
-        if (key >= 'h') key++;
-        if (key >= 'm') key++;
-        if (key >= 'q') key++;
-        if (key >= 'w') key++;
-        return 0x24b6 + key - 'a';
-    }
+        if (ord < 9 + 22) {
+            int key = 'a' + ord - 9;
+            if (key >= 'h') key++;
+            if (key >= 'm') key++;
+            if (key >= 'q') key++;
+            if (key >= 'w') key++;
+            return 0x24b6 + key - 'a';
+        }
 #else
-    if (ord < 9 + 26) {
-        return 0x24b6 + ord - 9;
-    }
+        if (ord < 9 + 26) {
+            return 0x24b6 + ord - 9;
+        }
 #endif
+    }
+    else {
+        if (ord < iElemCount(homeRowKeys_)) {
+            return 0x24b6 + homeRowKeys_[ord] - 'a';
+        }
+    }
     return 0;
 }
 
@@ -1866,31 +1916,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
         }
         return iTrue;
     }
-    if (ev->type == SDL_KEYUP) {
-        const int key = ev->key.keysym.sym;
-        switch (key) {
-            case SDLK_LALT:
-            case SDLK_RALT:
-                if (document_App() == d) {
-                    iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
-                    invalidate_DocumentWidget_(d);
-                    refresh_Widget(w);
-                }
-                break;
-            case SDLK_PAGEUP:
-            case SDLK_PAGEDOWN:
-            case SDLK_SPACE:
-            case SDLK_UP:
-            case SDLK_DOWN:
-//                d->smoothContinue = iFalse;
-                break;
-        }
-    }
     if (ev->type == SDL_KEYDOWN) {
         const int key = ev->key.keysym.sym;
         if ((d->flags & showLinkNumbers_DocumentWidgetFlag) &&
             ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) {
-            const size_t ord = linkOrdinalFromKey_(key);
+            const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key);
             iConstForEach(PtrArray, i, &d->visibleLinks) {
                 if (ord == iInvalidPos) break;
                 const iGmRun *run = i.ptr;
@@ -1904,6 +1934,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
                                      cstr_String(absoluteUrl_String(
                                      d->mod.url, linkUrl_GmDocument(d->doc, run->linkId))));
                     iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
+                    invalidateVisibleLinks_DocumentWidget_(d);
+                    refresh_Widget(d);
                     return iTrue;
                 }
             }
@@ -1912,19 +1944,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
             case SDLK_ESCAPE:
                 if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) {
                     iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
-                    invalidate_DocumentWidget_(d);
+                    invalidateVisibleLinks_DocumentWidget_(d);
                     refresh_Widget(d);
                     return iTrue;
                 }
                 break;
-            case SDLK_LALT:
-            case SDLK_RALT:
-                if (document_App() == d) {
-                    iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue);
-                    invalidate_DocumentWidget_(d);
-                    refresh_Widget(w);
-                }
-                break;
 #if 1
             case SDLK_KP_1:
             case '`': {
@@ -2374,11 +2398,11 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
     else {
         if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) {
             const size_t ord = visibleLinkOrdinal_DocumentWidget_(d->widget, run->linkId);
-            const iChar ordChar = linkOrdinalChar_(ord);
+            const iChar ordChar = linkOrdinalChar_DocumentWidget_(d->widget, ord);
             if (ordChar) {
                 drawString_Text(run->font,
                                 init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
-                                fg,
+                                tmQuote_ColorId,
                                 collect_String(newUnicodeN_String(&ordChar, 1)));
                 goto runDrawn;
             }
diff --git a/src/ui/keys.c b/src/ui/keys.c
index ea874343..d42ecfba 100644
--- a/src/ui/keys.c
+++ b/src/ui/keys.c
@@ -57,7 +57,8 @@ static void clear_Keys_(iKeys *d) {
 }
 
 enum iBindFlag {
-    argRepeat_BindFlag = iBit(1),
+    argRepeat_BindFlag  = iBit(1),
+    argRelease_BindFlag = iBit(2),
 };
 
 /* TODO: This indirection could be used for localization, although all UI strings
@@ -73,7 +74,8 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =
     { 31, { "Go forward",                navigateForward_KeyShortcut,   "navigate.forward"   }, 0 },
     { 32, { "Go to parent directory",    navigateParent_KeyShortcut,    "navigate.parent"    }, 0 },
     { 33, { "Go to site root",           navigateRoot_KeyShortcut,      "navigate.root"      }, 0 },
-    { 40, { "Open link via keyboard",    'f', 0,                        "document.linkkeys"  }, 0 },
+    { 40, { "Open link via home row keys", 'f', 0,                      "document.linkkeys arg:1" }, 0 },
+    { 41, { "Open link via modifier key", SDLK_LALT, 0,                 "document.linkkeys arg:0" }, argRelease_BindFlag },
     /* The following cannot currently be changed (built-in duplicates). */
     { 1000, { NULL, SDLK_SPACE, KMOD_SHIFT, "scroll.page arg:-1" }, argRepeat_BindFlag },
     { 1001, { NULL, SDLK_SPACE, 0, "scroll.page arg:1" }, argRepeat_BindFlag },
@@ -110,6 +112,11 @@ static void bindDefaults_(void) {
 
 static iBinding *find_Keys_(iKeys *d, int key, int mods) {
     size_t pos;
+    /* Do not differentiate between left and right modifier keys. */
+    key = normalizedMod_Sym(key);
+    if (isMod_Sym(key)) {
+        mods = 0;
+    }
     const iBinding elem = { .key = key, .mods = mods };
     if (locate_PtrSet(&d->lookup, &elem, &pos)) {
         return at_PtrSet(&d->lookup, pos);
@@ -138,8 +145,8 @@ static void updateLookup_Keys_(iKeys *d) {
 void setKey_Binding(int id, int key, int mods) {
     iBinding *bind = findId_Keys_(&keys_, id);
     if (bind) {
-        bind->key = key;
-        bind->mods = mods;
+        bind->key  = normalizedMod_Sym(key);
+        bind->mods = isMod_Sym(key) ? 0 : mods;
         updateLookup_Keys_(&keys_);
     }
 }
@@ -252,25 +259,18 @@ void setLabel_Keys(int id, const char *label) {
     }
 }
 
-#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_;
-    if (ev->type == SDL_KEYDOWN) {
+    if (ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP) {
         const iBinding *bind = find_Keys_(d, ev->key.keysym.sym, keyMods_Sym(ev->key.keysym.mod));
         if (bind) {
+            if (ev->type == SDL_KEYUP) {
+                if (bind->flags & argRelease_BindFlag) {
+                    postCommandf_App("%s release:1", cstr_String(&bind->command));
+                    return iTrue;
+                }
+                return iFalse;
+            }
             if (ev->key.repeat && (bind->flags & argRepeat_BindFlag)) {
                 postCommandf_App("%s repeat:1", cstr_String(&bind->command));
             }
diff --git a/src/ui/util.c b/src/ui/util.c
index 559c5381..c1312062 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -117,6 +117,14 @@ iBool isMod_Sym(int key) {
            key == SDLK_LGUI || key == SDLK_RGUI || key == SDLK_LSHIFT || key == SDLK_RSHIFT;
 }
 
+int normalizedMod_Sym(int key) {
+    if (key == SDLK_RSHIFT) key = SDLK_LSHIFT;
+    if (key == SDLK_RCTRL) key = SDLK_LCTRL;
+    if (key == SDLK_RALT) key = SDLK_LALT;
+    if (key == SDLK_RGUI) key = SDLK_LGUI;
+    return key;
+}
+
 int keyMods_Sym(int kmods) {
     kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI);
     /* Don't treat left/right modifiers differently. */
diff --git a/src/ui/util.h b/src/ui/util.h
index c0e3a04c..f7a67f9a 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -49,6 +49,7 @@ iLocalDef iBool isResize_UserEvent(const SDL_Event *d) {
 #endif
 
 iBool       isMod_Sym           (int key);
+int         normalizedMod_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/work%2Fv1.9/cdiff/a6314e152b2d2f306bcbb880356d3890efbfc89e
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
73.53532 milliseconds
Gemini-to-HTML Time
0.755546 milliseconds

This content has been proxied by September (ba2dc).