diff --git a/README.md b/README.md index c3e2855..6db944e 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,12 @@ Vietnamese Telex is also available as a compatibility mode. | `Ctrl+E` | Emoji and symbol search | | `Ctrl+H` | One-shot Hanja search | -Use `Up`/`Down` to move through candidates, `Enter` or `1`-`9` to select, -`Tab` to cycle temporary search results, and `Esc` to cancel. `0` commits the -current reading without conversion. +Use `Up`/`Down` to move through candidates and `PageUp`/`PageDown` to move by +nine without wrapping. An unmodified `1`-`9` selects that row on the current +page. `Enter` confirms the selection, and `0` commits the current reading +without conversion. `Tab` confirms an ordinary candidate; in a temporary +Emoji or Hanja search, `Tab` and `Shift-Tab` wrap through the results. `Esc` +cancels composition. Hiragana mode composes a complete reading before offering Kanji candidates. Katakana mode does not perform Kanji conversion. Emoji and Hanja searches diff --git a/ipc.h b/ipc.h index e379c66..0b38023 100644 --- a/ipc.h +++ b/ipc.h @@ -33,6 +33,8 @@ enum Kesc = Kspec|0x1b, Kup = Kspec|0x52, Kdown = Kspec|0x54, + Kpgup = Kspec|0x55, + Kpgdown = Kspec|0x56, Kmodfirst = Kspec|0xe1, Kmodlast = Kspec|0xee, diff --git a/strans.c b/strans.c index 576003a..e75ee61 100644 --- a/strans.c +++ b/strans.c @@ -40,6 +40,64 @@ clearkouho(void) im.sel = -1; } +static void +selectfirst(void) +{ + im.sel = im.nkouho > 0 ? 0 : -1; +} + +static int +pagefirst(void) +{ + if(im.sel < 0) + return 0; + return im.sel / Maxdisp * Maxdisp; +} + +static int +movedelta(u32int ks) +{ + static struct { + u32int key; + int delta; + } move[] = { + {Kup, -1}, {Kdown, 1}, + {Kpgup, -Maxdisp}, {Kpgdown, Maxdisp}, + }; + int i; + + for(i = 0; i < nelem(move); i++) + if(move[i].key == ks) + return move[i].delta; + return 0; +} + +static void +movekouho(int delta) +{ + if(im.sel < 0) + im.sel = 0; + else + im.sel += delta; + if(im.sel < 0) + im.sel = 0; + if(im.sel >= im.nkouho) + im.sel = im.nkouho - 1; +} + +static int +numberedkouho(u32int ks, u32int mod) +{ + int n; + + if(mod != 0 || ks < '1' || ks > '9') + return -1; + n = pagefirst() + ks - '1'; + if(n >= im.nkouho) + return -1; + return n; +} + static int isjp(Im *p) { @@ -92,6 +150,7 @@ setkouho(Dictres *res) im.kouho[i] = res->kouho[i]; im.nkouho++; } + selectfirst(); } static void @@ -112,7 +171,7 @@ show(void) !mapget(im.l->map, &pre, &dc.pre)) dc.pre = pre; } - first = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0; + first = pagefirst(); n = im.nkouho - first; if(n > Maxdisp) n = Maxdisp; dc.nkouho = n; @@ -484,6 +543,7 @@ emojiquery(void) addkouho(&res.kouho[i]); } search.text = rawhit || !localhit ? raw : local; + selectfirst(); show(); } @@ -566,25 +626,22 @@ picksearch(int n, Str *com) static int searchkey(u32int ks, u32int mod, Str *com) { - int n, off; + int n; Rune c; if(ismodkey(ks)) return 0; - if(ks == Kdown || ks == Kup){ + n = movedelta(ks); + if(n != 0){ if(im.nkouho == 0) return 1; - if(ks == Kdown && im.sel < im.nkouho - 1) - im.sel++; - if(ks == Kup && im.sel > 0) - im.sel--; + movekouho(n); show(); return 1; } - n = ks - '1'; - off = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0; - if(n >= 0 && n < Maxdisp && off + n < im.nkouho && mod == 0){ - picksearch(off + n, com); + n = numberedkouho(ks, mod); + if(n >= 0){ + picksearch(n, com); return 1; } if(ks == Ktab){ @@ -655,29 +712,24 @@ searchkey(u32int ks, u32int mod, Str *com) static int keystroke(u32int ks, u32int mod, Str *com) { - int n, off; + int n; Rune c; if(ismodkey(ks)) return 0; if(search.lang) return searchkey(ks, mod, com); - if(ks == Kdown || ks == Kup){ + n = movedelta(ks); + if(n != 0){ if(im.nkouho == 0) return 0; - if(ks == Kdown && im.sel < im.nkouho - 1) - im.sel++; - if(ks == Kup && im.sel >= 0) - im.sel--; + movekouho(n); show(); return 1; } - n = ks - '1'; - off = 0; - if(im.sel >= Maxdisp) - off = im.sel - Maxdisp + 1; - if(n >= 0 && n < Maxdisp && off + n < im.nkouho && mod == 0){ - sappend(com, &im.kouho[off + n]); + n = numberedkouho(ks, mod); + if(n >= 0){ + sappend(com, &im.kouho[n]); reset(); return 1; } @@ -769,6 +821,7 @@ init(void) { memset(&im, 0, sizeof(im)); im.l = getlang(LangEN); + im.sel = -1; memset(&search, 0, sizeof search); memset(&caret, 0, sizeof caret); activeowner = nil; diff --git a/tests/engine_test.c b/tests/engine_test.c index a5303c3..6e5e4f3 100644 --- a/tests/engine_test.c +++ b/tests/engine_test.c @@ -122,8 +122,9 @@ void engine_selects_visible_candidate(struct ct *t) { static const struct { int n, sel; Rune key; char *want; } cases[] = { - { Maxdisp, -1, '9', "c9" }, - { Maxdisp+3, Maxdisp+1, '1', "c3" }, + { Maxdisp, 0, '9', "c9" }, + { Maxkouho, Maxdisp, '1', "c10" }, + { Maxkouho, 2*Maxdisp, '9', "c27" }, }; char name[8]; Str com; @@ -147,18 +148,19 @@ engine_selects_visible_candidate(struct ct *t) void engine_candidate_shortcut_modifiers(struct ct *t) { - static u32int mods[] = { Mctrl, Malt, Msuper }; - Str com; + static u32int mods[] = { Mshift, Mctrl, Malt, Msuper }; + Str com, selected; int i; for(i = 0; i < nelem(mods); i++){ init(); - im.kouho[0] = mkstr("must-not-select"); - im.nkouho = 1; - im.sel = -1; + im.kouho[9] = mkstr("must-not-select"); + im.nkouho = 10; + im.sel = 9; + selected = im.kouho[9]; sclear(&com); CT_CHECK(t, !keystroke('1', mods[i], &com)); - CT_CHECK(t, scmp(&com, &im.kouho[0]) != 0); + CT_CHECK(t, scmp(&com, &selected) != 0); } } @@ -397,8 +399,6 @@ engine_commit_contract(struct ct *t) init(); im.l = getlang(LangJP); - im.nkouho = 1; - im.sel = -1; sclear(&com); if(!typekeys(t, "ka", &com)) return; @@ -661,6 +661,147 @@ draindraw(Drawcmd *last) return n; } +static void +setcandidates(int n) +{ + Dictres res; + char name[8]; + int i; + + memset(&res, 0, sizeof res); + for(i = 0; i < n; i++){ + snprint(name, sizeof name, "c%d", i+1); + res.kouho[i] = mkstr(name); + } + res.nkouho = n; + setkouho(&res); +} + +static void +candidatebegin(int n) +{ + init(); + draindraw(nil); + setcandidates(n); +} + +static int +checkcandidatepage(struct ct *t, char *where, int first, int n, int sel) +{ + Drawcmd dc; + char want[8]; + + show(); + if(!CT_CHECK(t, draindraw(&dc) > 0)) + return 0; + if(!CT_EQ_INT(t, n, dc.nkouho) || !CT_EQ_INT(t, sel, dc.sel)) + return 0; + if(n == 0) + return 1; + snprint(want, sizeof want, "c%d", first+1); + if(!checkstr(t, where, want, &dc.kouho[0])) + return 0; + snprint(want, sizeof want, "c%d", first+n); + return checkstr(t, where, want, &dc.kouho[n-1]); +} + +void +engine_candidate_page_snapshots(struct ct *t) +{ + static int totals[] = { 0, 1, 9, 10, 18, 19, 32 }; + int i, n; + + for(i = 0; i < nelem(totals); i++){ + candidatebegin(totals[i]); + CT_EQ_INT(t, totals[i] == 0 ? -1 : 0, im.sel); + if(totals[i] == 0){ + show(); + CT_EQ_INT(t, 0, draindraw(nil)); + continue; + } + n = totals[i] < Maxdisp ? totals[i] : Maxdisp; + checkcandidatepage(t, "default candidate page", 0, n, 0); + } +} + +void +engine_candidate_page_movement(struct ct *t) +{ + static const struct { + int n; + int sel; + Rune key; + int want; + } cases[] = { + { 1, 0, Kup, 0 }, + { 9, 8, Kdown, 8 }, + { 10, 8, Kdown, 9 }, + { 10, 9, Kup, 8 }, + { 18, 8, Kpgdown, 17 }, + { 19, 17, Kdown, 18 }, + { 19, 18, Kup, 17 }, + { 32, 26, Kdown, 27 }, + { 32, 27, Kup, 26 }, + { 32, 0, Kpgup, 0 }, + { 32, 9, Kpgup, 0 }, + { 32, 22, Kpgdown, 31 }, + { 32, 31, Kpgup, 22 }, + { 32, 31, Kpgdown, 31 }, + }; + int first, i, n; + Str com; + + for(i = 0; i < nelem(cases); i++){ + candidatebegin(cases[i].n); + im.sel = cases[i].sel; + sclear(&com); + CT_CHECK(t, keystroke(cases[i].key, 0, &com)); + CT_EQ_INT(t, cases[i].want, im.sel); + first = cases[i].want / Maxdisp * Maxdisp; + n = cases[i].n - first; + if(n > Maxdisp) + n = Maxdisp; + checkcandidatepage(t, "moved candidate page", first, n, + cases[i].want-first); + } +} + +void +engine_candidate_completion(struct ct *t) +{ + Str com; + + candidatebegin(2); + sclear(&com); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "default candidate", "c1", &com); + + candidatebegin(2); + sclear(&com); + CT_CHECK(t, keystroke(Ktab, 0, &com)); + checkstr(t, "ordinary Tab candidate", "c1", &com); + + candidatebegin(2); + im.pre = mkstr("reading"); + sclear(&com); + CT_CHECK(t, keystroke('0', 0, &com)); + checkstr(t, "ordinary zero reading", "reading", &com); + + candidatebegin(0); + im.pre = mkstr("reading"); + sclear(&com); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "Enter without candidates", "reading", &com); + + candidatebegin(32); + im.sel = 27; + sclear(&com); + CT_CHECK(t, !keystroke('9', 0, &com)); + checkstr(t, "unavailable short-page digit", "", &com); + CT_EQ_INT(t, 32, im.nkouho); + CT_EQ_INT(t, 27, im.sel); +} + static void setdict(Hmap **dict, char *key, char *val) { @@ -888,11 +1029,10 @@ engine_japanese_candidates(struct ct *t) dictresult(&res); if(!CT_EQ_INT(t, 2, im.nkouho)) goto cleanup; + CT_EQ_INT(t, 0, im.sel); checkstr(t, "candidate one", "漢字", &im.kouho[0]); checkstr(t, "candidate two", "幹事", &im.kouho[1]); CT_CHECK(t, keystroke(Kdown, 0, &com)); - CT_EQ_INT(t, 0, im.sel); - CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_EQ_INT(t, 1, im.sel); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "selected Kanji", "幹事", &com); @@ -1187,40 +1327,89 @@ engine_emoji_navigation(struct ct *t) CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "many", &com)){ CT_EQ_INT(t, 12, im.nkouho); - for(i = 0; i < Maxdisp+1; i++) + CT_EQ_INT(t, 0, im.sel); + for(i = 0; i < Maxdisp; i++) CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_EQ_INT(t, Maxdisp, im.sel); CT_CHECK(t, draindraw(&dc) > 0); - checkstr(t, "scrolled first row", "c2", &dc.kouho[0]); - checkstr(t, "scrolled ninth row", "c10", &dc.kouho[8]); - CT_EQ_INT(t, 8, dc.sel); + checkstr(t, "second-page first row", "c10", &dc.kouho[0]); + checkstr(t, "second-page last row", "c12", &dc.kouho[2]); + CT_EQ_INT(t, 3, dc.nkouho); + CT_EQ_INT(t, 0, dc.sel); CT_CHECK(t, keystroke('1', 0, &com)); - checkstr(t, "scrolled row one", "c2", &com); + checkstr(t, "second-page row one", "c10", &com); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "many", &com)){ - for(i = 0; i < 12; i++) + for(i = 0; i < 11; i++) CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_EQ_INT(t, 11, im.sel); - CT_CHECK(t, keystroke('9', 0, &com)); - checkstr(t, "scrolled row nine", "c12", &com); + CT_CHECK(t, keystroke('3', 0, &com)); + checkstr(t, "short-page row three", "c12", &com); } sclear(&com); CT_CHECK(t, keystroke('e', Mctrl, &com)); if(typekeys(t, "smile", &com)){ + CT_EQ_INT(t, 0, im.sel); + CT_CHECK(t, keystroke(Ktab, Mshift, &com)); + CT_EQ_INT(t, 1, im.sel); CT_CHECK(t, keystroke(Ktab, 0, &com)); CT_EQ_INT(t, 0, im.sel); CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_EQ_INT(t, 1, im.sel); CT_CHECK(t, keystroke(Kup, 0, &com)); CT_EQ_INT(t, 0, im.sel); - CT_CHECK(t, keystroke(Ktab, Mshift, &com)); - CT_EQ_INT(t, 1, im.sel); CT_CHECK(t, keystroke(Kret, 0, &com)); - checkstr(t, "navigated candidate", "😄", &com); + checkstr(t, "navigated candidate", "😀", &com); + } + searchend(&f); +} + +void +engine_search_candidate_keys(struct ct *t) +{ + Searchfix f; + Str com; + + emojibegin(&f, 0); + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "smile", &com)){ + CT_EQ_INT(t, 0, im.sel); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "search default Enter", "😀", &com); + } + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "missing", &com)){ + CT_EQ_INT(t, 0, im.nkouho); + CT_EQ_INT(t, -1, im.sel); + CT_CHECK(t, keystroke(Kret, 0, &com)); + checkstr(t, "search query Enter", "missing", &com); + } + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + CT_CHECK(t, keystroke('^', Mshift, &com)); + CT_CHECK(t, keystroke('0', 0, &com)); + checkstr(t, "search zero query", "^0", &search.text); + checkstr(t, "search zero commit", "", &com); + CT_CHECK(t, search.lang); + CT_CHECK(t, keystroke(Kesc, 0, &com)); + + sclear(&com); + CT_CHECK(t, keystroke('e', Mctrl, &com)); + if(typekeys(t, "many", &com)){ + im.sel = Maxdisp; + CT_CHECK(t, keystroke('1', Mshift, &com)); + checkstr(t, "modified search digit commit", "", &com); + checkstr(t, "modified search digit query", "many1", &search.text); + CT_CHECK(t, search.lang); + CT_CHECK(t, keystroke(Kesc, 0, &com)); } searchend(&f); } @@ -1382,7 +1571,7 @@ engine_emoji_dictionary_identity(struct ct *t) res.kouho[0] = mkstr("right"); dictresult(&res); CT_EQ_INT(t, 1, im.nkouho); - CT_EQ_INT(t, -1, im.sel); + CT_EQ_INT(t, 0, im.sel); checkstr(t, "matching response accepted", "right", &im.kouho[0]); searchend(&f); } diff --git a/tests/test.h b/tests/test.h index f43fb98..6ce3928 100644 --- a/tests/test.h +++ b/tests/test.h @@ -32,6 +32,9 @@ void engine_clears_candidates(struct ct*); void engine_backspace_clears_candidates(struct ct*); void engine_selects_visible_candidate(struct ct*); void engine_candidate_shortcut_modifiers(struct ct*); +void engine_candidate_page_snapshots(struct ct*); +void engine_candidate_page_movement(struct ct*); +void engine_candidate_completion(struct ct*); void engine_dictionary_queue_latest_wins(struct ct*); void engine_active_owner_lifecycle(struct ct*); void engine_active_owner_reset(struct ct*); @@ -52,6 +55,7 @@ void engine_emoji_queries(struct ct*); void engine_emoji_japanese_and_multirune(struct ct*); void engine_emoji_digit_aliases(struct ct*); void engine_emoji_navigation(struct ct*); +void engine_search_candidate_keys(struct ct*); void engine_emoji_preedit_languages(struct ct*); void engine_emoji_start_and_unknown(struct ct*); void engine_emoji_dictionary_identity(struct ct*); diff --git a/tests/unit_test.c b/tests/unit_test.c index 97e35f0..d71d3a9 100644 --- a/tests/unit_test.c +++ b/tests/unit_test.c @@ -86,6 +86,9 @@ static const struct ct_test tests[] = { { "engine/backspace-clears-candidates", engine_backspace_clears_candidates }, { "engine/selects-visible-candidate", engine_selects_visible_candidate }, { "engine/candidate-shortcut-modifiers", engine_candidate_shortcut_modifiers }, + { "engine/candidate-page-snapshots", engine_candidate_page_snapshots }, + { "engine/candidate-page-movement", engine_candidate_page_movement }, + { "engine/candidate-completion", engine_candidate_completion }, { "engine/dictionary-queue-latest", engine_dictionary_queue_latest_wins }, { "engine/active-owner-lifecycle", engine_active_owner_lifecycle }, { "engine/active-owner-reset", engine_active_owner_reset }, @@ -106,6 +109,7 @@ static const struct ct_test tests[] = { { "engine/emoji-japanese-multirune", engine_emoji_japanese_and_multirune }, { "engine/emoji-digit-aliases", engine_emoji_digit_aliases }, { "engine/emoji-navigation", engine_emoji_navigation }, + { "engine/search-candidate-keys", engine_search_candidate_keys }, { "engine/emoji-preedit-languages", engine_emoji_preedit_languages }, { "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown }, { "engine/emoji-dictionary-identity", engine_emoji_dictionary_identity }, diff --git a/tests/xim_adapter_test.c b/tests/xim_adapter_test.c index 064b916..8479d90 100644 --- a/tests/xim_adapter_test.c +++ b/tests/xim_adapter_test.c @@ -474,7 +474,7 @@ xim_adapter_key_contract(struct ct *t) CT_CHECK(t, res.eaten); checkstr(t, "か", &res.preedit); - keypress(&state, Kspec|0x55, 0, &res); + keypress(&state, Kspec|0x57, 0, &res); nexttrace(t, &f, Keypress, &state); CT_CHECK(t, !res.eaten); checkstr(t, "か", &res.commit);