Backspace, Enter, Tab, Escape and the arrow and page keys are matched by keysym alone -- strans.c:852, 875 and 882 name them, and searchkey names the same set at strans.c:746-786. Every other key above Kspec falls through to the catch-all at strans.c:898, `ks >= Kspec || chord(mod)`, and goes to the application. So the special keys strans knows by name are the ones it takes under a modifier, and the ones it does not know are the ones it hands over. That is backwards: a named special key under a modifier is exactly the one the application has a binding for. In Korean a syllable is pending for nearly all the time anyone is typing, ko.c holding one and no more, and the guards at strans.c:876 and 883 return 0 only when nothing is pending -- so the key is eaten precisely when it is wanted. Type 안녕하세요 and reach for Ctrl+Backspace to take the word back: 요 loses ㅛ, then ㅇ, and the word itself goes on the third press. Ctrl+Enter in a chat box, Ctrl+Tab in a browser and Ctrl+PageDown in either are the same key eaten by the same lines. before after Korean 가, Ctrl+Backspace eaten, pre ㄱ passed, commit 가 Korean 가, Alt+Backspace eaten, pre ㄱ passed, commit 가 Korean 가, Super+Backspace eaten, pre ㄱ passed, commit 가 Korean 가, Backspace eaten, pre ㄱ unchanged Korean 가, Shift+Backspace eaten, pre ㄱ unchanged かく, Ctrl+Enter eaten, commit かく passed, commit かく かく, Ctrl+Tab eaten, commit かく passed, commit かく かく Space, Ctrl+PageDown eaten, sel 0 -> 9 passed, commit 確 かく Space, PageDown eaten, sel 0 -> 9 unchanged かく Space, Shift+Tab eaten, sel 0 -> 30 unchanged Ctrl+E sm, Ctrl+Backspace eaten, query s passed, commit sm Ctrl+E sm, Backspace eaten, query s unchanged chord() cannot be reused here. It is `(mod & ~Mshift) != 0 && mod != Mctrl` and the exclusion is deliberate, since Ctrl+letter is strans's whole command set and chord() has to let plain Ctrl through. The rule this needs is the other one -- any modifier that is not Shift -- and Shift must stay in: Shift+Tab cycles the candidates backwards, pinned by engine/candidate-completion and engine/emoji-navigation at engine_test.c:914 and 1649, and Shift on a Korean key is what makes ㅃ. One line serves both paths because it sits above the searchkey dispatch, and it has to sit below the switch at strans.c:829: 한자, 한/영, 変換 and 無変換 arrive as keys above Kspec and are rewritten there into the Ctrl chords they stand for. Above the switch, 한자 would commit and pass instead of opening the Hanja list; below it, pressing it with Ctrl held still opens the list, because the switch sets the modifier itself. What this does not cover: Space, which is below Kspec, so searchkey:763 still picks a candidate on Ctrl+Space inside a search. transition tests !(mod & ~Mshift) for its own Space at strans.c:865, so the two disagree there. Left alone: what Ctrl+Space should mean wants its own argument, not a widened guard.
1029 lines
20 KiB
C
1029 lines
20 KiB
C
#include "dat.h"
|
|
#include "fn.h"
|
|
|
|
static Im im;
|
|
static void *activeowner;
|
|
static void *lostowner; /* the context the engine was taken from */
|
|
static Str lost;
|
|
static int clientpre; /* the owner draws its own preedit */
|
|
static Caret caret;
|
|
static Rune modemark; /* the mode the last key switched to */
|
|
static int okuriat = -1; /* where Shift marked the okurigana, or -1 */
|
|
static Drawcmd lastdraw;
|
|
static Emit transjp(Im*, Rune);
|
|
static void backjp(Im*);
|
|
|
|
/* seed is what Ctrl+H found pending; raw the keys typed since; text both. */
|
|
typedef struct Search Search;
|
|
struct Search
|
|
{
|
|
int lang;
|
|
Str seed;
|
|
Str raw;
|
|
Str text;
|
|
};
|
|
static Search search;
|
|
|
|
Lang langs[] = {
|
|
{LangEN, nil, nil, nil, nil, nil, nil},
|
|
{LangJP, "hira", "kanji", transjp, backjp, nil, nil},
|
|
{LangJPK, "kata", nil, transjp, backjp, nil, nil},
|
|
{LangKO, nil, nil, transko, backko, nil, nil},
|
|
{LangHANJA, nil, "hanja", nil, nil, nil, nil},
|
|
{LangEMOJI, nil, "emoji", nil, nil, nil, nil},
|
|
{LangVI, "telex", nil, transvi, backvi, nil, nil},
|
|
};
|
|
int nlang = nelem(langs);
|
|
|
|
/* Trie match kind for key; on an exact match out holds the value. */
|
|
static int
|
|
mapmatch(Trie *t, Str *key, Str *out)
|
|
{
|
|
char *v;
|
|
int match, vlen;
|
|
|
|
sclear(out);
|
|
if(key->n == 0)
|
|
return TrieMiss;
|
|
match = trielookup(t, key, &v, &vlen);
|
|
if(match == TrieExact && !sinit(out, v, vlen))
|
|
return TrieMiss;
|
|
return match;
|
|
}
|
|
|
|
int
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
switch(ks){
|
|
case Kup: return -1;
|
|
case Kdown: return 1;
|
|
case Kpgup: return -Maxdisp;
|
|
case Kpgdown: return Maxdisp;
|
|
}
|
|
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;
|
|
}
|
|
|
|
/* 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 void
|
|
addkouho(Str *s)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; i < im.nkouho; i++)
|
|
if(scmp(&im.kouho[i], s) == 0)
|
|
return;
|
|
if(im.nkouho < Maxkouho)
|
|
im.kouho[im.nkouho++] = *s;
|
|
}
|
|
|
|
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)
|
|
{
|
|
return p->l->lang == LangJP || p->l->lang == LangJPK;
|
|
}
|
|
|
|
/*
|
|
* Japanese keeps completed kana, and any romaji that never became kana,
|
|
* in pre, and the romaji still being typed in raw. A mapped raw suffix
|
|
* is part of the reading even while it remains pending (notably n,
|
|
* which may still grow into nya). Returns whether the reading is
|
|
* complete.
|
|
*/
|
|
static int
|
|
jpreading(Trie *map, Str *pre, Str *raw, Str *s)
|
|
{
|
|
Str mapped;
|
|
|
|
*s = *pre;
|
|
if(raw->n == 0)
|
|
return 1;
|
|
if(mapget(map, raw, &mapped)){
|
|
sappend(s, &mapped);
|
|
return 1;
|
|
}
|
|
sappend(s, raw);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
haspre(Im *p)
|
|
{
|
|
return p->pre.n != 0 || p->raw.n != 0;
|
|
}
|
|
|
|
/* The preedit as clients see it: Telex keys read back through the map. */
|
|
void
|
|
impre(Im *p, Str *s)
|
|
{
|
|
if(isjp(p))
|
|
jpreading(p->l->map, &p->pre, &p->raw, s);
|
|
else if(!mapget(p->l->map, &p->pre, s))
|
|
*s = p->pre;
|
|
}
|
|
|
|
static void
|
|
snapshot(Drawcmd *dc)
|
|
{
|
|
Str pre;
|
|
int i, first, n;
|
|
|
|
memset(dc, 0, sizeof *dc);
|
|
if(search.lang)
|
|
pre = search.text;
|
|
else
|
|
impre(&im, &pre);
|
|
if(!clientpre)
|
|
dc->pre = pre;
|
|
if(search.lang && pre.n == 0) /* a search just begun shows itself */
|
|
sputr(&dc->pre, search.lang == LangEMOJI ? L'☺' : L'漢');
|
|
else if(modemark != 0 && pre.n == 0 && im.nkouho == 0)
|
|
sputr(&dc->pre, modemark);
|
|
first = pagefirst();
|
|
n = im.nkouho - first;
|
|
if(n > Maxdisp) n = Maxdisp;
|
|
dc->nkouho = n;
|
|
dc->sel = im.sel >= 0 ? im.sel - first : -1;
|
|
dc->first = first;
|
|
dc->total = im.nkouho;
|
|
dc->caret = caret;
|
|
for(i = 0; i < n; i++)
|
|
dc->kouho[i] = im.kouho[first + i];
|
|
}
|
|
|
|
static int
|
|
careteq(Caret *a, Caret *b)
|
|
{
|
|
return a->valid == b->valid && a->x == b->x && a->y == b->y &&
|
|
a->h == b->h;
|
|
}
|
|
|
|
static int
|
|
shown(Drawcmd *dc)
|
|
{
|
|
return dc->pre.n != 0 || dc->nkouho != 0;
|
|
}
|
|
|
|
static int
|
|
samedraw(Drawcmd *a, Drawcmd *b)
|
|
{
|
|
int i;
|
|
|
|
if(!shown(a) && !shown(b))
|
|
return 1;
|
|
if(a->nkouho != b->nkouho || a->sel != b->sel ||
|
|
a->first != b->first || a->total != b->total ||
|
|
scmp(&a->pre, &b->pre) != 0 || !careteq(&a->caret, &b->caret))
|
|
return 0;
|
|
for(i = 0; i < a->nkouho; i++)
|
|
if(scmp(&a->kouho[i], &b->kouho[i]) != 0)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
/* Sends the popup a new picture only when it would look different. */
|
|
static void
|
|
redraw(void)
|
|
{
|
|
Drawcmd dc, junk;
|
|
|
|
snapshot(&dc);
|
|
if(samedraw(&dc, &lastdraw))
|
|
return;
|
|
lastdraw = dc;
|
|
/* imthread is the sole producer; keep only the newest pending draw. */
|
|
while(channbrecv(drawc, &junk) > 0)
|
|
;
|
|
channbsend(drawc, &dc);
|
|
}
|
|
|
|
/* SKK's letter for the okurigana a kana begins: its row, or its vowel. */
|
|
static Rune
|
|
okuriletter(Rune r)
|
|
{
|
|
static char *rows[] = {
|
|
"aあ", "iい", "uう", "eえ", "oお",
|
|
"kかきくけこ", "gがぎぐげご", "sさしすせそ", "zざじずぜぞ",
|
|
"tたちつてとっ", "dだぢづでど", "nなにぬねのん", "hはひふへほ",
|
|
"bばびぶべぼ", "pぱぴぷぺぽ", "mまみむめも", "yやゆよ",
|
|
"rらりるれろ", "wわ",
|
|
};
|
|
int i;
|
|
|
|
for(i = 0; i < nelem(rows); i++)
|
|
if(utfrune(rows[i]+1, r) != nil)
|
|
return rows[i][0];
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* SKK keys a verb or adjective by its stem and the letter of the
|
|
* okurigana that follows (かk: 書, 描, ...). The split the user marked
|
|
* is tried alone; otherwise every split is, longest stem first. The
|
|
* okurigana is put back, and a っ ending the stem (いっt: 言, 行) is
|
|
* written in kana too.
|
|
*/
|
|
static void
|
|
dictqokuri(Str *reading, int split)
|
|
{
|
|
Str key, okuri, kouho[Maxkouho];
|
|
Rune c;
|
|
int i, j, n;
|
|
|
|
for(i = split >= 0 ? split : reading->n - 1; i > 0; i--){
|
|
c = okuriletter(reading->r[i]);
|
|
if(c == 0)
|
|
break;
|
|
key = *reading;
|
|
key.n = i;
|
|
sputr(&key, c);
|
|
n = dictlookup(im.l->dict, &key, kouho, Maxkouho);
|
|
sclear(&okuri);
|
|
for(j = i - (reading->r[i-1] == L'っ'); j < reading->n; j++)
|
|
sputr(&okuri, reading->r[j]);
|
|
for(j = 0; j < n; j++){
|
|
sappend(&kouho[j], &okuri);
|
|
addkouho(&kouho[j]);
|
|
}
|
|
if(split >= 0)
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Kanji candidates for a complete Japanese reading; none is chosen yet. */
|
|
static void
|
|
dictqjp(void)
|
|
{
|
|
Str reading, exact[Maxkouho];
|
|
int i, n;
|
|
|
|
clearkouho();
|
|
if(!isjp(&im) || !jpreading(im.l->map, &im.pre, &im.raw, &reading) ||
|
|
reading.n == 0)
|
|
return;
|
|
/* What Shift marked comes first; the dictionary's own readings next. */
|
|
if(okuriat > 0 && okuriat < reading.n)
|
|
dictqokuri(&reading, okuriat);
|
|
n = dictlookup(im.l->dict, &reading, exact, Maxkouho);
|
|
for(i = 0; i < n; i++)
|
|
addkouho(&exact[i]);
|
|
dictqokuri(&reading, -1);
|
|
}
|
|
|
|
/* The reading in Katakana is the last candidate, as in every IME. */
|
|
static void
|
|
katakouho(void)
|
|
{
|
|
Str reading, k;
|
|
int i;
|
|
|
|
if(!jpreading(im.l->map, &im.pre, &im.raw, &reading))
|
|
return;
|
|
k = reading;
|
|
for(i = 0; i < k.n; i++)
|
|
if(k.r[i] >= L'ぁ' && k.r[i] <= L'ゖ')
|
|
k.r[i] += L'ァ' - L'ぁ';
|
|
if(scmp(&k, &reading) != 0)
|
|
addkouho(&k);
|
|
}
|
|
|
|
/* What the popup shows for a moment when the mode changes. */
|
|
static Rune
|
|
langmark(int lang)
|
|
{
|
|
switch(lang){
|
|
case LangJP: return L'あ';
|
|
case LangJPK: return L'ア';
|
|
case LangKO: return L'한';
|
|
case LangVI: return L'ă';
|
|
}
|
|
return L'A';
|
|
}
|
|
|
|
static int
|
|
setlang(int c)
|
|
{
|
|
Lang *l;
|
|
|
|
l = getlang(c);
|
|
if(l == nil)
|
|
return 0;
|
|
if(l != im.l)
|
|
modemark = langmark(l->lang);
|
|
im.l = l;
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
commitim(Im *p, Str *com)
|
|
{
|
|
Str val;
|
|
|
|
impre(p, &val);
|
|
sappend(com, &val);
|
|
sclear(&p->pre);
|
|
sclear(&p->raw);
|
|
}
|
|
|
|
/* Deletes the last kana as shown, not the last key: kya loses ゃ. */
|
|
static void
|
|
backjp(Im *p)
|
|
{
|
|
Str v;
|
|
|
|
if(p->raw.n == 0)
|
|
spopr(&p->pre);
|
|
else if(mapget(p->l->map, &p->raw, &v)){
|
|
spopr(&v);
|
|
sappend(&p->pre, &v);
|
|
sclear(&p->raw);
|
|
}else
|
|
spopr(&p->raw);
|
|
}
|
|
|
|
/*
|
|
* kk and the like are a sokuon: っ, and the consonant stays pending, as
|
|
* ch does after tch. xtu and ltu are っ itself.
|
|
*/
|
|
static int
|
|
issokuon(Str *key, Str *mapped)
|
|
{
|
|
return mapped->n == 1 && (mapped->r[0] == L'っ' || mapped->r[0] == L'ッ') &&
|
|
(key->r[0] == key->r[1] || (key->r[0] == 't' && key->r[1] == 'c'));
|
|
}
|
|
|
|
static void
|
|
keeptail(Str *s)
|
|
{
|
|
int i;
|
|
|
|
for(i = 1; i < s->n; i++)
|
|
s->r[i-1] = s->r[i];
|
|
if(s->n > 0)
|
|
s->n--;
|
|
}
|
|
|
|
static Emit
|
|
transjp(Im *p, Rune c)
|
|
{
|
|
Emit e;
|
|
Str key, mapped;
|
|
|
|
memset(&e, 0, sizeof e);
|
|
e.eat = 1;
|
|
e.next = p->pre;
|
|
e.raw = p->raw;
|
|
key = e.raw;
|
|
sputr(&key, c);
|
|
if(mapmatch(p->l->map, &key, &mapped) != TrieMiss){
|
|
e.raw = key;
|
|
if(issokuon(&key, &mapped)){
|
|
sappend(&e.next, &mapped);
|
|
keeptail(&e.raw);
|
|
}
|
|
return e;
|
|
}
|
|
/* The pending romaji is done: kana if it is complete, else the
|
|
* letters as typed, for Backspace to mend. */
|
|
if(!mapget(p->l->map, &e.raw, &mapped))
|
|
mapped = e.raw;
|
|
sappend(&e.next, &mapped);
|
|
sclear(&e.raw);
|
|
sclear(&key);
|
|
sputr(&key, c);
|
|
if(mapmatch(p->l->map, &key, &mapped) != TrieMiss){
|
|
e.raw = key;
|
|
return e;
|
|
}
|
|
/* The key starts no syllable: flush the reading and pass it on. */
|
|
e.s = e.next;
|
|
sclear(&e.next);
|
|
e.eat = 0;
|
|
return e;
|
|
}
|
|
|
|
static int
|
|
dotrans(Rune c, Str *com)
|
|
{
|
|
Emit e;
|
|
|
|
e = im.l->trans(&im, c);
|
|
sappend(com, &e.s);
|
|
im.pre = e.next;
|
|
im.raw = e.raw;
|
|
dictqjp();
|
|
return e.eat;
|
|
}
|
|
|
|
Lang*
|
|
getlang(int lang)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; i < nelem(langs); i++)
|
|
if(langs[i].lang == lang)
|
|
return &langs[i];
|
|
return nil;
|
|
}
|
|
|
|
/*
|
|
* The pending text is the key sequence itself while it is a prefix of some
|
|
* map entry; the map value is what gets shown and committed.
|
|
*/
|
|
Emit
|
|
transmap(Im *p, Rune c)
|
|
{
|
|
Emit e = {0};
|
|
Str key, mapped;
|
|
Trie *t;
|
|
|
|
t = p->l->map;
|
|
key = p->pre;
|
|
sputr(&key, c);
|
|
if(mapmatch(t, &key, &mapped) != TrieMiss){
|
|
e.eat = 1;
|
|
e.next = key;
|
|
return e;
|
|
}
|
|
if(!mapget(t, &p->pre, &e.s))
|
|
e.s = p->pre;
|
|
sclear(&key);
|
|
sputr(&key, c);
|
|
if(mapmatch(t, &key, &mapped) == TrieMiss)
|
|
return e;
|
|
e.eat = 1;
|
|
sputr(&e.next, c);
|
|
return e;
|
|
}
|
|
|
|
/* ASCII only: what the maps and dictionaries fold too. */
|
|
static Rune
|
|
lower(Rune c)
|
|
{
|
|
if(c >= 'A' && c <= 'Z')
|
|
c += 'a' - 'A';
|
|
return c;
|
|
}
|
|
|
|
static Rune
|
|
ctrlkey(Rune c)
|
|
{
|
|
c = lower(c);
|
|
if(c >= 'a' && c <= 'z')
|
|
return c - 'a' + 1;
|
|
return c;
|
|
}
|
|
|
|
/* Caps Lock does not shift a Korean key; Shift does. */
|
|
static Rune
|
|
kokey(Rune c, u32int mod)
|
|
{
|
|
c = lower(c);
|
|
if((mod & Mshift) && c >= 'a' && c <= 'z')
|
|
c -= 'a' - 'A';
|
|
return c;
|
|
}
|
|
|
|
/* A modifier press or an unmapped key is key 0: it engages nothing. */
|
|
int
|
|
keymeaningful(u32int ks)
|
|
{
|
|
return ks != 0;
|
|
}
|
|
|
|
static void
|
|
foldascii(Str *s)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; i < s->n; i++)
|
|
s->r[i] = lower(s->r[i]);
|
|
}
|
|
|
|
/*
|
|
* What typing raw in language l would produce, composition included,
|
|
* after pre, which may be nil, was already pending.
|
|
*/
|
|
void
|
|
transstr(Lang *l, Str *pre, Str *raw, Str *out)
|
|
{
|
|
Im q;
|
|
Emit e;
|
|
int i;
|
|
|
|
memset(&q, 0, sizeof q);
|
|
q.l = l;
|
|
if(pre != nil)
|
|
q.pre = *pre;
|
|
sclear(out);
|
|
if(l->trans == nil){
|
|
sappend(out, &q.pre);
|
|
sappend(out, raw);
|
|
return;
|
|
}
|
|
for(i = 0; i < raw->n; i++){
|
|
e = l->trans(&q, raw->r[i]);
|
|
sappend(out, &e.s);
|
|
q.pre = e.next;
|
|
q.raw = e.raw;
|
|
if(!e.eat)
|
|
sputr(out, raw->r[i]);
|
|
}
|
|
commitim(&q, out);
|
|
}
|
|
|
|
/*
|
|
* The typed keys and their transliteration are both emoji queries, folded
|
|
* as the dictionary is; the query shown is the keys while they match
|
|
* something, else the transliteration, as typed.
|
|
*/
|
|
static void
|
|
emojiquery(void)
|
|
{
|
|
Str kouho[Maxkouho], raw, local;
|
|
int i, n, rawhit;
|
|
|
|
raw = search.raw;
|
|
foldascii(&raw);
|
|
transstr(im.l, nil, &search.raw, &search.text);
|
|
local = search.text;
|
|
foldascii(&local);
|
|
clearkouho();
|
|
n = dictprefix(getlang(LangEMOJI)->dict, &raw, kouho, Maxkouho);
|
|
rawhit = n != 0;
|
|
for(i = 0; i < n; i++)
|
|
addkouho(&kouho[i]);
|
|
if(scmp(&raw, &local) != 0){
|
|
n = dictprefix(getlang(LangEMOJI)->dict, &local, kouho, Maxkouho);
|
|
for(i = 0; i < n; i++)
|
|
addkouho(&kouho[i]);
|
|
}
|
|
if(rawhit)
|
|
search.text = search.raw;
|
|
selectfirst();
|
|
}
|
|
|
|
/* The keys typed since Ctrl+H go on composing from the seed. */
|
|
static void
|
|
hanjaquery(void)
|
|
{
|
|
transstr(im.l, &search.seed, &search.raw, &search.text);
|
|
clearkouho();
|
|
im.nkouho = dictlookup(getlang(LangHANJA)->dict, &search.text,
|
|
im.kouho, Maxkouho);
|
|
selectfirst();
|
|
}
|
|
|
|
static void
|
|
searchquery(void)
|
|
{
|
|
if(search.lang == LangEMOJI)
|
|
emojiquery();
|
|
else if(search.lang == LangHANJA)
|
|
hanjaquery();
|
|
}
|
|
|
|
static int
|
|
searchlang(Rune c)
|
|
{
|
|
return c == LangEMOJI || c == LangHANJA ? c : 0;
|
|
}
|
|
|
|
/*
|
|
* A chord with Alt or Super, or Ctrl with Shift, is the application's
|
|
* (Ctrl+Shift+V pastes); only a plain Ctrl+letter is a strans command.
|
|
*/
|
|
static int
|
|
chord(u32int mod)
|
|
{
|
|
return (mod & ~Mshift) != 0 && mod != Mctrl;
|
|
}
|
|
|
|
static void
|
|
endsearch(void)
|
|
{
|
|
memset(&search, 0, sizeof search);
|
|
clearkouho();
|
|
}
|
|
|
|
static void
|
|
reset(void)
|
|
{
|
|
sclear(&im.pre);
|
|
sclear(&im.raw);
|
|
modemark = 0;
|
|
okuriat = -1;
|
|
endsearch();
|
|
}
|
|
|
|
/*
|
|
* 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
|
|
flush(Str *com)
|
|
{
|
|
if(search.lang)
|
|
sappend(com, &search.text);
|
|
else if(im.sel >= 0)
|
|
sappend(com, &im.kouho[im.sel]);
|
|
else
|
|
commitim(&im, com);
|
|
reset();
|
|
}
|
|
|
|
/*
|
|
* Focus can be lost after the next context's keys arrive, so the engine
|
|
* changes hands with text still pending. The context it was taken from
|
|
* gets that text back when it is next heard from.
|
|
*/
|
|
static void
|
|
takelost(void *owner, Str *com)
|
|
{
|
|
if(owner != lostowner)
|
|
return;
|
|
sappend(com, &lost);
|
|
lostowner = nil;
|
|
}
|
|
|
|
/*
|
|
* A search takes over the keys. Hanja converts the pending syllable, so
|
|
* it becomes the query; anything else pending is committed.
|
|
*/
|
|
static void
|
|
startsearch(int lang, Str *com)
|
|
{
|
|
Str seed;
|
|
|
|
sclear(&seed);
|
|
if(lang == LangHANJA && !search.lang && im.sel < 0)
|
|
impre(&im, &seed);
|
|
else
|
|
flush(com);
|
|
reset();
|
|
search.lang = lang;
|
|
search.seed = seed;
|
|
if(lang == LangHANJA)
|
|
searchquery();
|
|
}
|
|
|
|
static void
|
|
picksearch(int n, Str *com)
|
|
{
|
|
sappend(com, &im.kouho[n]);
|
|
endsearch();
|
|
}
|
|
|
|
static int
|
|
searchkey(u32int ks, u32int mod, Str *com)
|
|
{
|
|
int n;
|
|
Rune c;
|
|
|
|
n = movedelta(ks);
|
|
if(n != 0){
|
|
if(im.nkouho == 0)
|
|
return 1;
|
|
movekouho(n);
|
|
return 1;
|
|
}
|
|
n = numberedkouho(ks, mod);
|
|
if(n >= 0){
|
|
picksearch(n, com);
|
|
return 1;
|
|
}
|
|
if(ks == Ktab){
|
|
if(im.nkouho != 0)
|
|
cyclekouho(mod & Mshift ? -1 : 1);
|
|
return 1;
|
|
}
|
|
if(ks == Kret || ks == ' '){
|
|
if(im.nkouho > 0)
|
|
picksearch(max(im.sel, 0), com);
|
|
else
|
|
flush(com);
|
|
return 1;
|
|
}
|
|
if(ks == Kback){
|
|
if(search.raw.n != 0)
|
|
spopr(&search.raw);
|
|
else if(search.seed.n != 0)
|
|
spopr(&search.seed);
|
|
else{
|
|
endsearch();
|
|
return 1;
|
|
}
|
|
searchquery();
|
|
return 1;
|
|
}
|
|
if(ks == Kesc){
|
|
im.pre = search.seed; /* the syllable Ctrl+H took is pending again */
|
|
endsearch();
|
|
return 1;
|
|
}
|
|
if(ks >= Kspec || (ks < ' ' && !(mod & Mctrl)) || chord(mod)){
|
|
flush(com);
|
|
return 0;
|
|
}
|
|
if(mod == Mctrl){
|
|
c = ctrlkey(ks);
|
|
n = searchlang(c);
|
|
if(n != 0 && n != search.lang){
|
|
startsearch(n, com);
|
|
return 1;
|
|
}
|
|
flush(com);
|
|
return n != 0 || setlang(c);
|
|
}
|
|
if(search.raw.n >= Maxrunes){
|
|
flush(com);
|
|
return 0;
|
|
}
|
|
sputr(&search.raw, ks);
|
|
searchquery();
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
transition(u32int ks, u32int mod, Str *com)
|
|
{
|
|
Str mark;
|
|
int n;
|
|
Rune c;
|
|
|
|
modemark = 0; /* any key after the switch takes its mark away */
|
|
/* Shift on a romaji letter marks the okurigana, as SKK's capital does. */
|
|
if(isjp(&im) && (mod & Mshift) && ks >= 'A' && ks <= 'Z' && haspre(&im)){
|
|
impre(&im, &mark);
|
|
okuriat = mark.n;
|
|
}
|
|
if(im.l->lang == LangKO)
|
|
ks = kokey(ks, mod);
|
|
else if(isjp(&im))
|
|
ks = lower(ks); /* romaji is case-blind, Caps Lock included */
|
|
/* The Korean and Japanese keyboards' own keys, as the Ctrl chords
|
|
* and Space they stand for. */
|
|
switch(ks){
|
|
case Khangul: ks = im.l->lang == LangKO ? 't' : 's'; mod = Mctrl; break;
|
|
case Khanja: ks = 'h'; mod = Mctrl; break;
|
|
case Kzenkaku: ks = isjp(&im) ? 't' : 'n'; mod = Mctrl; break;
|
|
case Kkana: ks = im.l->lang == LangJP ? 'k' : 'n'; mod = Mctrl; break;
|
|
case Khenkan:
|
|
if(isjp(&im)){
|
|
ks = ' ';
|
|
mod &= Mshift;
|
|
}else{
|
|
ks = 'n';
|
|
mod = Mctrl;
|
|
}
|
|
break;
|
|
case Kmuhenkan: ks = 't'; mod = Mctrl; break;
|
|
}
|
|
/* Ctrl+Backspace deletes a word, Ctrl+Tab changes tab: not ours. */
|
|
if(ks >= Kspec && (mod & ~Mshift)){
|
|
flush(com);
|
|
return 0;
|
|
}
|
|
if(search.lang)
|
|
return searchkey(ks, mod, com);
|
|
n = movedelta(ks);
|
|
if(n != 0 && im.nkouho != 0){
|
|
movekouho(n);
|
|
return 1;
|
|
}
|
|
/* Digits pick a candidate only once the list has been engaged. */
|
|
n = im.sel >= 0 ? numberedkouho(ks, mod) : -1;
|
|
if(n >= 0){
|
|
sappend(com, &im.kouho[n]);
|
|
reset();
|
|
return 1;
|
|
}
|
|
if((ks == ' ' || ks == Ktab) && isjp(&im) && haspre(&im) &&
|
|
!(mod & ~Mshift)){
|
|
/* Space and Tab convert: they step through the candidates,
|
|
* Katakana last; a reading with none is committed. */
|
|
katakouho();
|
|
if(im.nkouho != 0)
|
|
cyclekouho(mod & Mshift ? -1 : 1);
|
|
else
|
|
flush(com);
|
|
return 1;
|
|
}
|
|
if(ks == Ktab || ks == Kret || (ks == Kesc && !isjp(&im))){
|
|
if(!haspre(&im))
|
|
return 0;
|
|
flush(com);
|
|
/* Japanese confirms with the key; Korean and Telex let it on. */
|
|
return isjp(&im);
|
|
}
|
|
if(ks == Kback || ks == Kesc){
|
|
if(!haspre(&im))
|
|
return 0;
|
|
if(im.sel >= 0)
|
|
im.sel = -1; /* back from the candidate to the reading */
|
|
else if(ks == Kesc)
|
|
reset();
|
|
else{
|
|
im.l->back(&im);
|
|
if(haspre(&im))
|
|
dictqjp();
|
|
else
|
|
reset();
|
|
}
|
|
return 1;
|
|
}
|
|
if(ks >= Kspec || chord(mod)){
|
|
flush(com);
|
|
return 0;
|
|
}
|
|
if(mod == Mctrl){
|
|
c = ctrlkey(ks);
|
|
n = searchlang(c);
|
|
if(n != 0){
|
|
startsearch(n, com);
|
|
return 1;
|
|
}
|
|
flush(com);
|
|
return setlang(c);
|
|
}
|
|
if(ks == '0' && isjp(&im) && haspre(&im)){
|
|
im.sel = -1; /* 0 commits the reading, as 1-9 pick candidates */
|
|
flush(com);
|
|
return 1;
|
|
}
|
|
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
|
|
commitim(&im, com);
|
|
sclear(&im.pre);
|
|
clearkouho();
|
|
}
|
|
return dotrans(ks, com);
|
|
}
|
|
|
|
static void
|
|
init(void)
|
|
{
|
|
memset(&im, 0, sizeof(im));
|
|
im.l = getlang(LangEN);
|
|
im.sel = -1;
|
|
memset(&search, 0, sizeof search);
|
|
memset(&caret, 0, sizeof caret);
|
|
modemark = 0;
|
|
okuriat = -1;
|
|
activeowner = nil;
|
|
lostowner = nil;
|
|
clientpre = 0;
|
|
memset(&lastdraw, 0, sizeof lastdraw);
|
|
}
|
|
|
|
/*
|
|
* The engine belongs to whichever context last typed a real key; only
|
|
* the owner's requests change state. A reset or release hands the
|
|
* owner its pending text back as commit. Every request ends in
|
|
* redraw(), which sends the popup a picture only if something visible
|
|
* changed.
|
|
*/
|
|
static void
|
|
imhandlekey(Keyreq *kr)
|
|
{
|
|
Keyres res;
|
|
|
|
sclear(&res.commit);
|
|
sclear(&res.preedit);
|
|
res.eaten = 1;
|
|
switch(kr->op){
|
|
case Keyrelease:
|
|
takelost(kr->owner, &res.commit);
|
|
if(kr->owner == activeowner){
|
|
flush(&res.commit);
|
|
activeowner = nil;
|
|
clientpre = 0;
|
|
}
|
|
break;
|
|
case Keyreset:
|
|
takelost(kr->owner, &res.commit);
|
|
if(kr->owner == activeowner)
|
|
flush(&res.commit);
|
|
break;
|
|
case Keycaret:
|
|
if(kr->owner == activeowner)
|
|
caret = kr->caret;
|
|
break;
|
|
case Keycap:
|
|
res.eaten = kr->owner == activeowner;
|
|
if(res.eaten)
|
|
clientpre = kr->clientpre;
|
|
break;
|
|
case Keypress:
|
|
res.eaten = 0;
|
|
takelost(kr->owner, &res.commit);
|
|
if(kr->owner == activeowner)
|
|
clientpre = kr->clientpre;
|
|
if(keymeaningful(kr->ks)){
|
|
if(kr->owner != activeowner){
|
|
sclear(&lost);
|
|
flush(&lost);
|
|
lostowner = lost.n > 0 ? activeowner : nil;
|
|
activeowner = kr->owner;
|
|
clientpre = kr->clientpre;
|
|
}
|
|
caret = kr->caret;
|
|
res.eaten = transition(kr->ks, kr->mod, &res.commit);
|
|
}
|
|
break;
|
|
}
|
|
redraw();
|
|
if(kr->owner == activeowner){
|
|
if(search.lang)
|
|
res.preedit = search.text;
|
|
else
|
|
impre(&im, &res.preedit);
|
|
}
|
|
chansend(kr->reply, &res);
|
|
}
|
|
|
|
void
|
|
imthread(void*)
|
|
{
|
|
Keyreq kr;
|
|
|
|
threadsetname("im");
|
|
init();
|
|
for(;;){
|
|
chanrecv(keyc, &kr);
|
|
imhandlekey(&kr);
|
|
}
|
|
}
|
|
|