[1mdiff --git a/res/about/version.gmi b/res/about/version.gmi[m
[1mindex 1b8e502a..48b4cbd1 100644[m
[1m--- a/res/about/version.gmi[m
[1m+++ b/res/about/version.gmi[m
[36m@@ -7,9 +7,10 @@[m
[m
[31m-* 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.[m
[32m+[m[32m* 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.[m
[31m-* Added a keybinding to activate keyboard link navigation mode (default is "F").[m
[32m+[m[32m* Added a new keyboard link navigation mode focusing on the home row keys. The default keybinding for this is "F".[m
[32m+[m[32m* 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}.[m
[1mdiff --git a/src/ui/bindingswidget.c b/src/ui/bindingswidget.c[m
[1mindex ff68ea7b..dee844db 100644[m
[1m--- a/src/ui/bindingswidget.c[m
[1m+++ b/src/ui/bindingswidget.c[m
[36m@@ -195,6 +195,12 @@[m [mstatic iBool processEvent_BindingsWidget_(iBindingsWidget *d, const SDL_Event *e[m
postCommand_App("bindings.changed");[m
return iTrue;[m
}[m
[32m+[m[32m else if (ev->type == SDL_KEYUP && isMod_Sym(ev->key.keysym.sym)) {[m
[32m+[m[32m setKey_BindingItem_(item_ListWidget(d->list, d->activePos), ev->key.keysym.sym, 0);[m
[32m+[m[32m setActiveItem_BindingsWidget_(d, iInvalidPos);[m
[32m+[m[32m postCommand_App("bindings.changed");[m
[32m+[m[32m return iTrue;[m
[32m+[m[32m }[m
}[m
return processEvent_Widget(w, ev);[m
}[m
[1mdiff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c[m
[1mindex b1c166aa..3cf564ac 100644[m
[1m--- a/src/ui/documentwidget.c[m
[1m+++ b/src/ui/documentwidget.c[m
[36m@@ -125,11 +125,17 @@[m [menum iDocumentWidgetFlag {[m
showLinkNumbers_DocumentWidgetFlag = iBit(3),[m
};[m
[m
[32m+[m[32menum iDocumentLinkOrdinalMode {[m
[32m+[m[32m numbersAndAlphabet_DocumentLinkOrdinalMode,[m
[32m+[m[32m homeRow_DocumentLinkOrdinalMode,[m
[32m+[m[32m};[m
[32m+[m
struct Impl_DocumentWidget {[m
iWidget widget;[m
enum iRequestState state;[m
iPersistentDocumentState mod;[m
int flags;[m
[32m+[m[32m enum iDocumentLinkOrdinalMode ordinalMode;[m
iString * titleUser;[m
iGmRequest * request;[m
iAtomicInt isRequestUpdated; /* request has new content, need to parse it */[m
[36m@@ -228,7 +234,6 @@[m [mvoid init_DocumentWidget(iDocumentWidget *d) {[m
addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward");[m
addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent");[m
addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");[m
[31m- addAction_Widget(w, 'f', 0, "document.linkkeys");[m
}[m
[m
void deinit_DocumentWidget(iDocumentWidget *d) {[m
[36m@@ -371,6 +376,15 @@[m [mstatic void invalidateLink_DocumentWidget_(iDocumentWidget *d, iGmLinkId id) {[m
}[m
}[m
[m
[32m+[m[32mstatic void invalidateVisibleLinks_DocumentWidget_(iDocumentWidget *d) {[m
[32m+[m[32m iConstForEach(PtrArray, i, &d->visibleLinks) {[m
[32m+[m[32m const iGmRun *run = i.ptr;[m
[32m+[m[32m if (run->linkId) {[m
[32m+[m[32m insert_PtrSet(d->invalidRuns, run);[m
[32m+[m[32m }[m
[32m+[m[32m }[m
[32m+[m[32m}[m
[32m+[m
static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {[m
const iWidget *w = constAs_Widget(d);[m
const iRect docBounds = documentBounds_DocumentWidget_(d);[m
[36m@@ -969,7 +983,7 @@[m [mstatic void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int dur[m
/* Get rid of link numbers when scrolling. */[m
if (offset && d->flags & showLinkNumbers_DocumentWidgetFlag) {[m
d->flags &= ~showLinkNumbers_DocumentWidgetFlag;[m
[31m- invalidate_DocumentWidget_(d);[m
[32m+[m[32m invalidateVisibleLinks_DocumentWidget_(d);[m
}[m
if (!prefs_App()->smoothScrolling) {[m
duration = 0; /* always instant */[m
[36m@@ -1542,8 +1556,14 @@[m [mstatic iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)[m
return iTrue;[m
}[m
else if (equal_Command(cmd, "document.linkkeys") && document_App() == d) {[m
[31m- iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue);[m
[31m- invalidate_DocumentWidget_(d);[m
[32m+[m[32m if (argLabel_Command(cmd, "release")) {[m
[32m+[m[32m iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);[m
[32m+[m[32m }[m
[32m+[m[32m else {[m
[32m+[m[32m d->ordinalMode = arg_Command(cmd);[m
[32m+[m[32m iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue);[m
[32m+[m[32m }[m
[32m+[m[32m invalidateVisibleLinks_DocumentWidget_(d);[m
refresh_Widget(d);[m
return iTrue;[m
}[m
[36m@@ -1815,45 +1835,75 @@[m [mstatic iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E[m
return iFalse;[m
}[m
[m
[31m-static size_t linkOrdinalFromKey_(int key) {[m
[31m- if (key >= '1' && key <= '9') {[m
[31m- return key - '1';[m
[31m- }[m
[31m- if (key < 'a' || key > 'z') {[m
[31m- return iInvalidPos;[m
[31m- }[m
[31m- int ord = key - 'a' + 9;[m
[32m+[m[32m/* Sorted by proximity to F and J. */[m
[32m+[m[32mstatic const int homeRowKeys_[] = {[m
[32m+[m[32m 'f', 'd', 's', 'a',[m
[32m+[m[32m 'j', 'k', 'l',[m
[32m+[m[32m 'r', 'e', 'w', 'q',[m
[32m+[m[32m 'u', 'i', 'o', 'p',[m
[32m+[m[32m 'v', 'c', 'x', 'z',[m
[32m+[m[32m 'm', 'n',[m
[32m+[m[32m 'g', 'h',[m
[32m+[m[32m 'b',[m
[32m+[m[32m 't', 'y', 'u',[m
[32m+[m[32m};[m
[32m+[m
[32m+[m[32mstatic size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) {[m
[32m+[m[32m size_t ord = iInvalidPos;[m
[32m+[m[32m if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {[m
[32m+[m[32m if (key >= '1' && key <= '9') {[m
[32m+[m[32m return key - '1';[m
[32m+[m[32m }[m
[32m+[m[32m if (key < 'a' || key > 'z') {[m
[32m+[m[32m return iInvalidPos;[m
[32m+[m[32m }[m
[32m+[m[32m ord = key - 'a' + 9;[m
#if defined (iPlatformApple)[m
[31m- /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */[m
[31m- if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {[m
[31m- return iInvalidPos;[m
[31m- }[m
[31m- if (key > 'h') ord--;[m
[31m- if (key > 'm') ord--;[m
[31m- if (key > 'q') ord--;[m
[31m- if (key > 'w') ord--;[m
[32m+[m[32m /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */[m
[32m+[m[32m if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {[m
[32m+[m[32m return iInvalidPos;[m
[32m+[m[32m }[m
[32m+[m[32m if (key > 'h') ord--;[m
[32m+[m[32m if (key > 'm') ord--;[m
[32m+[m[32m if (key > 'q') ord--;[m
[32m+[m[32m if (key > 'w') ord--;[m
#endif[m
[32m+[m[32m }[m
[32m+[m[32m else {[m
[32m+[m[32m iForIndices(i, homeRowKeys_) {[m
[32m+[m[32m if (homeRowKeys_[i] == key) {[m
[32m+[m[32m return i;[m
[32m+[m[32m }[m
[32m+[m[32m }[m
[32m+[m[32m }[m
return ord;[m
}[m
[m
[31m-static iChar linkOrdinalChar_(size_t ord) {[m
[31m- if (ord < 9) {[m
[31m- return 0x278a + ord;[m
[31m- }[m
[32m+[m[32mstatic iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {[m
[32m+[m[32m if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {[m
[32m+[m[32m if (ord < 9) {[m
[32m+[m[32m return 0x278a + ord;[m
[32m+[m[32m }[m
#if defined (iPlatformApple)[m
[31m- if (ord < 9 + 22) {[m
[31m- int key = 'a' + ord - 9;[m
[31m- if (key >= 'h') key++;[m
[31m- if (key >= 'm') key++;[m
[31m- if (key >= 'q') key++;[m
[31m- if (key >= 'w') key++;[m
[31m- return 0x24b6 + key - 'a';[m
[31m- }[m
[32m+[m[32m if (ord < 9 + 22) {[m
[32m+[m[32m int key = 'a' + ord - 9;[m
[32m+[m[32m if (key >= 'h') key++;[m
[32m+[m[32m if (key >= 'm') key++;[m
[32m+[m[32m if (key >= 'q') key++;[m
[32m+[m[32m if (key >= 'w') key++;[m
[32m+[m[32m return 0x24b6 + key - 'a';[m
[32m+[m[32m }[m
#else[m
[31m- if (ord < 9 + 26) {[m
[31m- return 0x24b6 + ord - 9;[m
[31m- }[m
[32m+[m[32m if (ord < 9 + 26) {[m
[32m+[m[32m return 0x24b6 + ord - 9;[m
[32m+[m[32m }[m
#endif[m
[32m+[m[32m }[m
[32m+[m[32m else {[m
[32m+[m[32m if (ord < iElemCount(homeRowKeys_)) {[m
[32m+[m[32m return 0x24b6 + homeRowKeys_[ord] - 'a';[m
[32m+[m[32m }[m
[32m+[m[32m }[m
return 0;[m
}[m
[m
[36m@@ -1866,31 +1916,11 @@[m [mstatic iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e[m
}[m
return iTrue;[m
}[m
[31m- if (ev->type == SDL_KEYUP) {[m
[31m- const int key = ev->key.keysym.sym;[m
[31m- switch (key) {[m
[31m- case SDLK_LALT:[m
[31m- case SDLK_RALT:[m
[31m- if (document_App() == d) {[m
[31m- iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);[m
[31m- invalidate_DocumentWidget_(d);[m
[31m- refresh_Widget(w);[m
[31m- }[m
[31m- break;[m
[31m- case SDLK_PAGEUP:[m
[31m- case SDLK_PAGEDOWN:[m
[31m- case SDLK_SPACE:[m
[31m- case SDLK_UP:[m
[31m- case SDLK_DOWN:[m
[31m-// d->smoothContinue = iFalse;[m
[31m- break;[m
[31m- }[m
[31m- }[m
if (ev->type == SDL_KEYDOWN) {[m
const int key = ev->key.keysym.sym;[m
if ((d->flags & showLinkNumbers_DocumentWidgetFlag) &&[m
((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) {[m
[31m- const size_t ord = linkOrdinalFromKey_(key);[m
[32m+[m[32m const size_t ord = linkOrdinalFromKey_DocumentWidget_(d, key);[m
iConstForEach(PtrArray, i, &d->visibleLinks) {[m
if (ord == iInvalidPos) break;[m
const iGmRun *run = i.ptr;[m
[36m@@ -1904,6 +1934,8 @@[m [mstatic iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e[m
cstr_String(absoluteUrl_String([m
d->mod.url, linkUrl_GmDocument(d->doc, run->linkId))));[m
iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);[m
[32m+[m[32m invalidateVisibleLinks_DocumentWidget_(d);[m
[32m+[m[32m refresh_Widget(d);[m
return iTrue;[m
}[m
}[m
[36m@@ -1912,19 +1944,11 @@[m [mstatic iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e[m
case SDLK_ESCAPE:[m
if (d->flags & showLinkNumbers_DocumentWidgetFlag && document_App() == d) {[m
iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);[m
[31m- invalidate_DocumentWidget_(d);[m
[32m+[m[32m invalidateVisibleLinks_DocumentWidget_(d);[m
refresh_Widget(d);[m
return iTrue;[m
}[m
break;[m
[31m- case SDLK_LALT:[m
[31m- case SDLK_RALT:[m
[31m- if (document_App() == d) {[m
[31m- iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue);[m
[31m- invalidate_DocumentWidget_(d);[m
[31m- refresh_Widget(w);[m
[31m- }[m
[31m- break;[m
#if 1[m
case SDLK_KP_1:[m
case '`': {[m
[36m@@ -2374,11 +2398,11 @@[m [mstatic void drawRun_DrawContext_(void *context, const iGmRun *run) {[m
else {[m
if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) {[m
const size_t ord = visibleLinkOrdinal_DocumentWidget_(d->widget, run->linkId);[m
[31m- const iChar ordChar = linkOrdinalChar_(ord);[m
[32m+[m[32m const iChar ordChar = linkOrdinalChar_DocumentWidget_(d->widget, ord);[m
if (ordChar) {[m
drawString_Text(run->font,[m
init_I2(d->viewPos.x - gap_UI / 3, visPos.y),[m
[31m- fg,[m
[32m+[m[32m tmQuote_ColorId,[m
collect_String(newUnicodeN_String(&ordChar, 1)));[m
goto runDrawn;[m
}[m
[1mdiff --git a/src/ui/keys.c b/src/ui/keys.c[m
[1mindex ea874343..d42ecfba 100644[m
[1m--- a/src/ui/keys.c[m
[1m+++ b/src/ui/keys.c[m
[36m@@ -57,7 +57,8 @@[m [mstatic void clear_Keys_(iKeys *d) {[m
}[m
[m
enum iBindFlag {[m
[31m- argRepeat_BindFlag = iBit(1),[m
[32m+[m[32m argRepeat_BindFlag = iBit(1),[m
[32m+[m[32m argRelease_BindFlag = iBit(2),[m
};[m
[m
/* TODO: This indirection could be used for localization, although all UI strings[m
[36m@@ -73,7 +74,8 @@[m [mstatic const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =[m
{ 31, { "Go forward", navigateForward_KeyShortcut, "navigate.forward" }, 0 },[m
{ 32, { "Go to parent directory", navigateParent_KeyShortcut, "navigate.parent" }, 0 },[m
{ 33, { "Go to site root", navigateRoot_KeyShortcut, "navigate.root" }, 0 },[m
[31m- { 40, { "Open link via keyboard", 'f', 0, "document.linkkeys" }, 0 },[m
[32m+[m[32m { 40, { "Open link via home row keys", 'f', 0, "document.linkkeys arg:1" }, 0 },[m
[32m+[m[32m { 41, { "Open link via modifier key", SDLK_LALT, 0, "document.linkkeys arg:0" }, argRelease_BindFlag },[m
/* The following cannot currently be changed (built-in duplicates). */[m
{ 1000, { NULL, SDLK_SPACE, KMOD_SHIFT, "scroll.page arg:-1" }, argRepeat_BindFlag },[m
{ 1001, { NULL, SDLK_SPACE, 0, "scroll.page arg:1" }, argRepeat_BindFlag },[m
[36m@@ -110,6 +112,11 @@[m [mstatic void bindDefaults_(void) {[m
[m
static iBinding *find_Keys_(iKeys *d, int key, int mods) {[m
size_t pos;[m
[32m+[m[32m /* Do not differentiate between left and right modifier keys. */[m
[32m+[m[32m key = normalizedMod_Sym(key);[m
[32m+[m[32m if (isMod_Sym(key)) {[m
[32m+[m[32m mods = 0;[m
[32m+[m[32m }[m
const iBinding elem = { .key = key, .mods = mods };[m
if (locate_PtrSet(&d->lookup, &elem, &pos)) {[m
return at_PtrSet(&d->lookup, pos);[m
[36m@@ -138,8 +145,8 @@[m [mstatic void updateLookup_Keys_(iKeys *d) {[m
void setKey_Binding(int id, int key, int mods) {[m
iBinding *bind = findId_Keys_(&keys_, id);[m
if (bind) {[m
[31m- bind->key = key;[m
[31m- bind->mods = mods;[m
[32m+[m[32m bind->key = normalizedMod_Sym(key);[m
[32m+[m[32m bind->mods = isMod_Sym(key) ? 0 : mods;[m
updateLookup_Keys_(&keys_);[m
}[m
}[m
[36m@@ -252,25 +259,18 @@[m [mvoid setLabel_Keys(int id, const char *label) {[m
}[m
}[m
[m
[31m-#if 0[m
[31m-const iString *label_Keys(const char *command) {[m
[31m- iKeys *d = &keys_;[m
[31m- /* TODO: A hash wouldn't hurt here. */[m
[31m- iConstForEach(PtrSet, i, &d->bindings) {[m
[31m- const iBinding *bind = *i.value;[m
[31m- if (!cmp_String(&bind->command, command) && !isEmpty_String(&bind->label)) {[m
[31m- return &bind->label;[m
[31m- }[m
[31m- }[m
[31m- return collectNew_String();[m
[31m-}[m
[31m-#endif[m
[31m-[m
iBool processEvent_Keys(const SDL_Event *ev) {[m
iKeys *d = &keys_;[m
[31m- if (ev->type == SDL_KEYDOWN) {[m
[32m+[m[32m if (ev->type == SDL_KEYDOWN || ev->type == SDL_KEYUP) {[m
const iBinding *bind = find_Keys_(d, ev->key.keysym.sym, keyMods_Sym(ev->key.keysym.mod));[m
if (bind) {[m
[32m+[m[32m if (ev->type == SDL_KEYUP) {[m
[32m+[m[32m if (bind->flags & argRelease_BindFlag) {[m
[32m+[m[32m postCommandf_App("%s release:1", cstr_String(&bind->command));[m
[32m+[m[32m return iTrue;[m
[32m+[m[32m }[m
[32m+[m[32m return iFalse;[m
[32m+[m[32m }[m
if (ev->key.repeat && (bind->flags & argRepeat_BindFlag)) {[m
postCommandf_App("%s repeat:1", cstr_String(&bind->command));[m
}[m
[1mdiff --git a/src/ui/util.c b/src/ui/util.c[m
[1mindex 559c5381..c1312062 100644[m
[1m--- a/src/ui/util.c[m
[1m+++ b/src/ui/util.c[m
[36m@@ -117,6 +117,14 @@[m [miBool isMod_Sym(int key) {[m
key == SDLK_LGUI || key == SDLK_RGUI || key == SDLK_LSHIFT || key == SDLK_RSHIFT;[m
}[m
[m
[32m+[m[32mint normalizedMod_Sym(int key) {[m
[32m+[m[32m if (key == SDLK_RSHIFT) key = SDLK_LSHIFT;[m
[32m+[m[32m if (key == SDLK_RCTRL) key = SDLK_LCTRL;[m
[32m+[m[32m if (key == SDLK_RALT) key = SDLK_LALT;[m
[32m+[m[32m if (key == SDLK_RGUI) key = SDLK_LGUI;[m
[32m+[m[32m return key;[m
[32m+[m[32m}[m
[32m+[m
int keyMods_Sym(int kmods) {[m
kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI);[m
/* Don't treat left/right modifiers differently. */[m
[1mdiff --git a/src/ui/util.h b/src/ui/util.h[m
[1mindex c0e3a04c..f7a67f9a 100644[m
[1m--- a/src/ui/util.h[m
[1m+++ b/src/ui/util.h[m
[36m@@ -49,6 +49,7 @@[m [miLocalDef iBool isResize_UserEvent(const SDL_Event *d) {[m
#endif[m
[m
iBool isMod_Sym (int key);[m
[32m+[m[32mint normalizedMod_Sym (int key);[m
int keyMods_Sym (int kmods); /* shift, alt, control, or gui */[m
void toString_Sym (int key, int kmods, iString *str);[m
[m
text/plain
This content has been proxied by September (ba2dc).