diff --git a/README.md b/README.md index c2d4d46..a0c4418 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,18 @@ 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 and `PageUp`/`PageDown` to move by -nine without wrapping. An unmodified `1`-`9` selects that row on the current -page. `Enter` and `Tab` commit a candidate you have moved to; otherwise they -commit the reading, as `0` always does. Japanese consumes the confirming key; -Korean and Vietnamese pass it on to the application. In a temporary Emoji or -Hanja search, `Enter` commits the highlighted result and `Tab`/`Shift-Tab` -wrap through the results. Leaving the field or clicking elsewhere commits -what is pending; `Esc` cancels it. +A complete Japanese reading shows its Kanji candidates with none chosen. +`Space` chooses the first and then steps on, wrapping (`Shift+Space` steps +back); `Up`/`Down` and `PageUp`/`PageDown` move without wrapping. An +unmodified `1`-`9` commits that row of the current page. `Enter` and `Tab` +commit the chosen candidate, or the reading when none is chosen, as `0` +always does; so does typing on, or any key that ends the composition. +Japanese consumes the confirming key; Korean and Vietnamese pass it on to +the application. `Space` on a reading without candidates commits it and +types nothing. `Backspace` and `Esc` on a chosen candidate go back to the +reading. In a temporary Emoji or Hanja search, `Enter` commits the +highlighted result and `Tab`/`Shift-Tab` wrap through the results. Leaving +the field or clicking elsewhere commits what is pending; `Esc` cancels it. Hiragana mode composes a complete reading before offering Kanji candidates. Katakana mode does not perform Kanji conversion. Emoji and Hanja searches diff --git a/strans.c b/strans.c index 8dc7fb9..950ccb5 100644 --- a/strans.c +++ b/strans.c @@ -5,7 +5,6 @@ static Im im; static void *activeowner; static int activecap; static Caret caret; -static int candidatechosen; static Drawcmd lastdraw; static Emit transjp(Im*, Rune); static void backjp(Im*); @@ -54,12 +53,12 @@ mapget(Trie *t, Str *key, Str *out) return mapmatch(t, key, out) == TrieExact; } +/* sel is the chosen candidate; -1 while the user has not moved to one. */ static void clearkouho(void) { im.nkouho = 0; im.sel = -1; - candidatechosen = 0; } static void @@ -99,7 +98,16 @@ movekouho(int delta) im.sel = 0; if(im.sel >= im.nkouho) im.sel = im.nkouho - 1; - candidatechosen = 1; +} + +/* Space and Tab step through the candidates and wrap around. */ +static void +cyclekouho(int delta) +{ + if(im.sel < 0) + im.sel = delta > 0 ? 0 : im.nkouho - 1; + else + im.sel = (im.sel + delta + im.nkouho) % im.nkouho; } static int @@ -230,7 +238,7 @@ redraw(void) channbsend(drawc, &dc); } -/* Kanji candidates for a complete reading. */ +/* Kanji candidates for a complete reading; none is chosen yet. */ static void dictqjp(void) { @@ -240,7 +248,6 @@ dictqjp(void) if(!jpreading(im.l->map, &im.pre, &im.raw, &reading) || reading.n == 0) return; im.nkouho = dictlookup(im.l, &reading, im.kouho, Maxkouho); - selectfirst(); } static int @@ -269,12 +276,6 @@ commitim(Im *p, Str *com) sclear(&p->raw); } -static void -commit(Str *com) -{ - commitim(&im, com); -} - static void backjp(Im *p) { @@ -542,10 +543,7 @@ searchlang(Rune c) static void endsearch(void) { - search.lang = 0; - sclear(&search.seed); - sclear(&search.raw); - sclear(&search.text); + memset(&search, 0, sizeof search); clearkouho(); } @@ -555,14 +553,23 @@ reset(void) sclear(&im.pre); sclear(&im.raw); endsearch(); - memset(&caret, 0, sizeof caret); } +/* + * Whatever is pending becomes committed text: the search query, a chosen + * candidate, else the reading. Enter does this, and so do a reset and a + * lost focus. + */ static void -commitsearch(Str *com) +flush(Str *com) { - sappend(com, &search.text); - endsearch(); + if(search.lang) + sappend(com, &search.text); + else if(im.sel >= 0) + sappend(com, &im.kouho[im.sel]); + else + commitim(&im, com); + reset(); } /* @@ -572,36 +579,18 @@ commitsearch(Str *com) static void startsearch(int lang, Str *com) { - Str pending; + Str seed; - impre(&im, &pending); - if(lang == LangHANJA){ - sclear(&im.pre); - sclear(&im.raw); - }else - commit(com); - commitsearch(com); - search.lang = lang; - if(lang == LangHANJA){ - search.seed = pending; - searchquery(); - } -} - -/* - * Whatever is pending becomes committed text, a moved-to candidate - * first: Enter does this, and so do a reset and a lost focus. - */ -static void -flush(Str *com) -{ - if(search.lang) - commitsearch(com); - else if(candidatechosen && im.sel >= 0 && im.sel < im.nkouho) - sappend(com, &im.kouho[im.sel]); + sclear(&seed); + if(lang == LangHANJA && !search.lang && im.sel < 0) + impre(&im, &seed); else - commit(com); + flush(com); reset(); + search.lang = lang; + search.seed = seed; + if(lang == LangHANJA) + searchquery(); } static void @@ -630,24 +619,15 @@ searchkey(u32int ks, u32int mod, Str *com) return 1; } if(ks == Ktab){ - if(im.nkouho == 0) - return 1; - if(mod & Mshift){ - if(im.sel <= 0) - im.sel = im.nkouho - 1; - else - im.sel--; - }else if(im.sel >= im.nkouho - 1) - im.sel = 0; - else - im.sel++; + if(im.nkouho != 0) + cyclekouho(mod & Mshift ? -1 : 1); return 1; } if(ks == Kret){ if(im.nkouho > 0) picksearch(max(im.sel, 0), com); else - commitsearch(com); + flush(com); return 1; } if(ks == Kback){ @@ -668,7 +648,7 @@ searchkey(u32int ks, u32int mod, Str *com) } if(ks >= Kspec || (ks < ' ' && !(mod & Mctrl)) || (mod & (Malt|Msuper))){ - commitsearch(com); + flush(com); return 0; } if(mod & Mctrl){ @@ -678,11 +658,11 @@ searchkey(u32int ks, u32int mod, Str *com) startsearch(n, com); return 1; } - commitsearch(com); + flush(com); return n != 0 || setlang(c); } if(search.raw.n >= Maxrunes){ - commitsearch(com); + flush(com); return 0; } sputr(&search.raw, ks); @@ -715,6 +695,15 @@ transition(u32int ks, u32int mod, Str *com) reset(); return 1; } + if(ks == ' ' && isjp(&im) && haspre(&im) && !(mod & ~Mshift)){ + /* Space converts: it steps through the candidates, or + * commits a reading that has none. */ + if(im.nkouho != 0) + cyclekouho(mod & Mshift ? -1 : 1); + else + flush(com); + return 1; + } if(ks == Ktab || ks == Kret){ if(!haspre(&im)) return 0; @@ -725,6 +714,10 @@ transition(u32int ks, u32int mod, Str *com) if(ks == Kback){ if(!haspre(&im)) return 0; + if(im.sel >= 0){ + im.sel = -1; /* back from the candidate to the reading */ + return 1; + } im.l->back(&im); if(!haspre(&im)){ reset(); @@ -739,12 +732,14 @@ transition(u32int ks, u32int mod, Str *com) if(ks == Kesc){ if(!haspre(&im)) return 0; - reset(); + if(im.sel >= 0) + im.sel = -1; + else + reset(); return 1; } if(ks >= Kspec || (mod & (Malt|Msuper))){ - commit(com); - reset(); + flush(com); return 0; } if(mod & Mctrl){ @@ -754,20 +749,18 @@ transition(u32int ks, u32int mod, Str *com) startsearch(n, com); return 1; } - commit(com); - reset(); + flush(com); return setlang(c); } if(ks == '0' && im.nkouho > 0){ - commit(com); - reset(); + im.sel = -1; + flush(com); return 1; } if(ks > 0x7f || ks == ' '){ if(!haspre(&im)) return 0; - commit(com); - reset(); + flush(com); if(com->n >= Maxrunes) return 0; sputr(com, ks); @@ -775,13 +768,15 @@ transition(u32int ks, u32int mod, Str *com) } if(im.l->trans == nil) return 0; + if(im.sel >= 0) + flush(com); /* typing on takes the chosen candidate */ if(im.pre.n >= Maxrunes || (isjp(&im) && im.pre.n + im.raw.n >= Maxrunes)){ /* A full reading commits; a pending romaji syllable stays. */ if(isjp(&im)) sappend(com, &im.pre); else - commit(com); + commitim(&im, com); sclear(&im.pre); clearkouho(); } @@ -798,7 +793,6 @@ init(void) memset(&caret, 0, sizeof caret); activeowner = nil; activecap = 0; - candidatechosen = 0; memset(&lastdraw, 0, sizeof lastdraw); } diff --git a/tests/engine_test.c b/tests/engine_test.c index af88be5..e0a3560 100644 --- a/tests/engine_test.c +++ b/tests/engine_test.c @@ -116,6 +116,9 @@ engine_backspace_clears_candidates(struct ct *t) im.nkouho = 1; im.sel = 0; CT_CHECK(t, keystroke(Kback, 0, &com)); + CT_EQ_INT(t, -1, im.sel); + checkstr(t, "chosen candidate backspace", "na", &im.raw); + CT_CHECK(t, keystroke(Kback, 0, &com)); checkstr(t, "backspace commit", "", &com); checkstr(t, "pending romaji after backspace", "n", &im.raw); impre(&im, &shown); @@ -162,7 +165,7 @@ engine_candidate_shortcut_modifiers(struct ct *t) init(); im.kouho[9] = mkstr("must-not-select"); im.nkouho = 10; - im.sel = 9; + im.sel = -1; selected = im.kouho[9]; sclear(&com); CT_CHECK(t, !keystroke('1', mods[i], &com)); @@ -277,7 +280,6 @@ engine_active_owner_reset(struct ct *t) CT_EQ_PTR(t, &owner, activeowner); checkstr(t, "reset hands the reading back", "か", &res.commit); CT_EQ_INT(t, 0, res.preedit.n); - CT_CHECK(t, !caret.valid); res = ownerrequest(&owner, Keypress, 'n', 0); checkstr(t, "same owner after reset", "ん", &res.preedit); } @@ -633,7 +635,7 @@ engine_language_switch_state(struct ct *t) if(steps[i].stale){ im.kouho[0] = mkstr("stale"); im.nkouho = 1; - im.sel = 0; + im.sel = -1; } sclear(&com); eaten = keystroke(steps[i].key, steps[i].mod, &com); @@ -693,7 +695,6 @@ struct Searchfix Trie *dict; int activecap; Drawcmd lastdraw; - int candidatechosen; }; static int @@ -723,7 +724,6 @@ setcandidates(int n) im.kouho[i] = mkstr(name); } im.nkouho = n; - selectfirst(); } static void @@ -873,6 +873,7 @@ engine_candidate_completion(struct ct *t) im.pre = mkstr("reading"); sclear(&com); CT_CHECK(t, keystroke(Kdown, 0, &com)); + CT_CHECK(t, keystroke(Kdown, 0, &com)); CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "explicit candidate", "c2", &com); @@ -888,13 +889,14 @@ engine_candidate_completion(struct ct *t) CT_CHECK(t, keystroke(Kret, 0, &com)); checkstr(t, "Enter without candidates", "reading", &com); - /* A digit with no such row on the page is an ordinary key. */ + /* A digit with no such row on the page is an ordinary key: it + * confirms the candidate paged to and passes on. */ candidatebegin(32); im.pre = mkstr("reading"); im.sel = 27; sclear(&com); CT_CHECK(t, !keystroke('9', 0, &com)); - checkstr(t, "unavailable short-page digit", "reading", &com); + checkstr(t, "unavailable short-page digit", "c28", &com); CT_EQ_INT(t, 0, im.nkouho); } @@ -913,7 +915,6 @@ searchsave(Searchfix *f, int lang) f->dict = f->dictlang->dict; f->activecap = activecap; f->lastdraw = lastdraw; - f->candidatechosen = candidatechosen; draindraw(nil); } @@ -956,7 +957,6 @@ searchend(Searchfix *f) search = f->search; activecap = f->activecap; lastdraw = f->lastdraw; - candidatechosen = f->candidatechosen; } void @@ -982,7 +982,7 @@ engine_popup_preedit_capability(struct ct *t) if(CT_CHECK(t, draindraw(&dc) > 0)){ CT_EQ_INT(t, 0, dc.pre.n); CT_EQ_INT(t, 1, dc.nkouho); - CT_EQ_INT(t, 0, dc.sel); + CT_EQ_INT(t, -1, dc.sel); checkstr(t, "client-preedit candidate", "家", &dc.kouho[0]); } @@ -992,11 +992,11 @@ engine_popup_preedit_capability(struct ct *t) if(CT_CHECK(t, draindraw(&dc) > 0)){ checkstr(t, "popup preedit", "か", &dc.pre); CT_EQ_INT(t, 1, dc.nkouho); - CT_EQ_INT(t, 0, dc.sel); + CT_EQ_INT(t, -1, dc.sel); checkstr(t, "popup candidate", "家", &dc.kouho[0]); } CT_EQ_INT(t, 1, im.nkouho); - CT_EQ_INT(t, 0, im.sel); + CT_EQ_INT(t, -1, im.sel); checkstr(t, "retained popup candidate", "家", &im.kouho[0]); kres = ownerrequestcap(&client, 0, Keycap, 0, 0); @@ -1016,11 +1016,11 @@ engine_popup_preedit_capability(struct ct *t) if(CT_CHECK(t, draindraw(&dc) > 0)){ CT_EQ_INT(t, 0, dc.pre.n); CT_EQ_INT(t, 1, dc.nkouho); - CT_EQ_INT(t, 0, dc.sel); + CT_EQ_INT(t, -1, dc.sel); checkstr(t, "restored client candidate", "家", &dc.kouho[0]); } CT_EQ_INT(t, 1, im.nkouho); - CT_EQ_INT(t, 0, im.sel); + CT_EQ_INT(t, -1, im.sel); checkstr(t, "retained client candidate", "家", &im.kouho[0]); kres = ownerrequestcap(&client, Cclientpreedit, Keycap, 0, 0); CT_CHECK(t, kres.eaten); @@ -1116,14 +1116,64 @@ engine_japanese_candidates(struct ct *t) checkstr(t, "complete Japanese reading", "かんじ", &shown); if(!CT_EQ_INT(t, 2, im.nkouho)) goto cleanup; - CT_EQ_INT(t, 0, im.sel); + CT_EQ_INT(t, -1, 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); CT_EQ_INT(t, 0, im.nkouho); + /* Space converts: it steps through the candidates and wraps; + * typing on takes the chosen one; Backspace goes back. */ + if(!typekeys(t, "kanji", &com)) + goto cleanup; + sclear(&com); + CT_CHECK(t, keystroke(' ', 0, &com)); + CT_EQ_INT(t, 0, im.sel); + CT_CHECK(t, keystroke(' ', 0, &com)); + CT_EQ_INT(t, 1, im.sel); + CT_CHECK(t, keystroke(' ', 0, &com)); + CT_EQ_INT(t, 0, im.sel); + CT_CHECK(t, keystroke(' ', Mshift, &com)); + CT_EQ_INT(t, 1, im.sel); + CT_CHECK(t, keystroke(Kback, 0, &com)); + CT_EQ_INT(t, -1, im.sel); + CT_EQ_INT(t, 2, im.nkouho); + CT_CHECK(t, keystroke(' ', 0, &com)); + CT_CHECK(t, keystroke('w', 0, &com)); + CT_CHECK(t, keystroke('o', 0, &com)); + checkstr(t, "typing on takes the candidate", "漢字", &com); + impre(&im, &shown); + checkstr(t, "reading after the candidate", "を", &shown); + sclear(&com); + CT_CHECK(t, keystroke(' ', 0, &com)); + checkstr(t, "Space commits a reading without candidates", "を", &com); + CT_EQ_INT(t, 0, im.pre.n); + CT_EQ_INT(t, 0, im.raw.n); + CT_CHECK(t, !keystroke(' ', 0, &com)); + sclear(&com); + if(!typekeys(t, "kanji", &com)) + goto cleanup; + CT_CHECK(t, keystroke(' ', 0, &com)); + CT_CHECK(t, keystroke(Kesc, 0, &com)); + CT_EQ_INT(t, -1, im.sel); + impre(&im, &shown); + checkstr(t, "Escape goes back to the reading", "かんじ", &shown); + CT_CHECK(t, keystroke(Kesc, 0, &com)); + CT_EQ_INT(t, 0, im.pre.n); + CT_EQ_INT(t, 0, im.raw.n); + CT_EQ_INT(t, 0, im.nkouho); + sclear(&com); + if(!typekeys(t, "kanji", &com)) + goto cleanup; + CT_CHECK(t, keystroke(Kdown, 0, &com)); + CT_CHECK(t, keystroke('s', Mctrl, &com)); + checkstr(t, "language switch takes the candidate", "漢字", &com); + CT_EQ_INT(t, LangKO, im.l->lang); + im.l = jp; /* Backspace looks a complete shorter reading up again. */ setdict(jp->dict, "かん", "缶"); if(!typekeys(t, "kanji", &com)) @@ -1179,8 +1229,8 @@ engine_japanese_backspace_and_boundaries(struct ct *t) im.l = getlang(LangJP); sclear(&com); if(typekeys(t, "kanji", &com)){ - CT_CHECK(t, keystroke(' ', 0, &com)); - checkstr(t, "real input boundary", "かんじ ", &com); + CT_CHECK(t, keystroke(L'日', 0, &com)); + checkstr(t, "real input boundary", "かんじ日", &com); } init(); @@ -1960,7 +2010,7 @@ engine_full_boundary_passthrough(struct ct *t) im.pre.r[i] = L'あ'; im.pre.n = Maxrunes; sclear(&com); - CT_CHECK(t, !keystroke(' ', 0, &com)); + CT_CHECK(t, !keystroke(L'日', 0, &com)); CT_EQ_INT(t, Maxrunes, com.n); for(i = 0; i < Maxrunes; i++) CT_EQ_UINT(t, L'あ', com.r[i]); @@ -1973,9 +2023,9 @@ engine_full_boundary_passthrough(struct ct *t) im.pre.r[i] = L'あ'; im.pre.n = Maxrunes-1; sclear(&com); - CT_CHECK(t, keystroke(' ', 0, &com)); + CT_CHECK(t, keystroke(L'日', 0, &com)); CT_EQ_INT(t, Maxrunes, com.n); - CT_EQ_UINT(t, ' ', com.r[Maxrunes-1]); + CT_EQ_UINT(t, L'日', com.r[Maxrunes-1]); /* Typing on at the limit commits the kana and keeps the syllable. */ init();