A key that could not go on with the pending romaji, when that was not a syllable either, committed the whole reading to the application and passed the key on: one slip dumped かんj into the text with no way back. The letters typed now stay in the reading as they are, like any other IME shows them, and Backspace mends them; only a key that starts no syllable at all still ends the composition.
882 lines
16 KiB
C
882 lines
16 KiB
C
#include "dat.h"
|
|
#include "fn.h"
|
|
|
|
static Im im;
|
|
static void *activeowner;
|
|
static int activecap;
|
|
static Caret caret;
|
|
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 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 in pre and an ambiguous romaji syllable 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(!(activecap & Cclientpreedit))
|
|
dc->pre = pre;
|
|
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);
|
|
}
|
|
|
|
/* Kanji candidates for a complete reading; none is chosen yet. */
|
|
static void
|
|
dictqjp(void)
|
|
{
|
|
Str reading;
|
|
|
|
clearkouho();
|
|
if(!jpreading(im.l->map, &im.pre, &im.raw, &reading) || reading.n == 0)
|
|
return;
|
|
im.nkouho = dictlookup(im.l, &reading, im.kouho, Maxkouho);
|
|
}
|
|
|
|
static int
|
|
setlang(int c)
|
|
{
|
|
Lang *l;
|
|
|
|
l = getlang(c);
|
|
if(l == nil)
|
|
return 0;
|
|
im.l = l;
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
commitim(Im *p, Str *com)
|
|
{
|
|
Str val;
|
|
|
|
if(isjp(p))
|
|
jpreading(p->l->map, &p->pre, &p->raw, &val);
|
|
else if(!mapget(p->l->map, &p->pre, &val))
|
|
val = p->pre;
|
|
sappend(com, &val);
|
|
sclear(&p->pre);
|
|
sclear(&p->raw);
|
|
}
|
|
|
|
static void
|
|
backjp(Im *p)
|
|
{
|
|
if(p->raw.n != 0)
|
|
spopr(&p->raw);
|
|
else
|
|
spopr(&p->pre);
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
if(e.eat && isjp(&im))
|
|
dictqjp();
|
|
else
|
|
clearkouho();
|
|
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;
|
|
}
|
|
|
|
static Rune
|
|
ctrlkey(Rune c)
|
|
{
|
|
if(c >= 'A' && c <= 'Z')
|
|
c += 'a' - 'A';
|
|
if(c >= 'a' && c <= 'z')
|
|
return c - 'a' + 1;
|
|
return c;
|
|
}
|
|
|
|
static Rune
|
|
kokey(Rune c, u32int mod)
|
|
{
|
|
if(c >= 'A' && c <= 'Z')
|
|
c += 'a' - 'A';
|
|
if((mod & Mshift) && c >= 'a' && c <= 'z')
|
|
c -= 'a' - 'A';
|
|
return c;
|
|
}
|
|
|
|
static int
|
|
ismodkey(u32int ks)
|
|
{
|
|
return ks >= Kmodfirst && ks <= Kmodlast;
|
|
}
|
|
|
|
/* Modifier presses and unmapped keys never engage the engine. */
|
|
int
|
|
keymeaningful(u32int ks)
|
|
{
|
|
return ks != 0 && !ismodkey(ks);
|
|
}
|
|
|
|
static void
|
|
foldascii(Str *s)
|
|
{
|
|
int i;
|
|
|
|
for(i = 0; i < s->n; i++)
|
|
if(s->r[i] >= 'A' && s->r[i] <= 'Z')
|
|
s->r[i] += 'a' - 'A';
|
|
}
|
|
|
|
/* What typing raw in language l would produce, composition included. */
|
|
void
|
|
transstr(Lang *l, Str *raw, Str *out)
|
|
{
|
|
Im q;
|
|
Emit e;
|
|
int i;
|
|
|
|
sclear(out);
|
|
if(l->trans == nil){
|
|
*out = *raw;
|
|
return;
|
|
}
|
|
memset(&q, 0, sizeof q);
|
|
q.l = l;
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* The typed keys and their transliteration are both emoji queries. */
|
|
static void
|
|
emojiquery(void)
|
|
{
|
|
Str kouho[Maxkouho], raw, local;
|
|
int i, n, rawhit, localhit;
|
|
|
|
raw = search.raw;
|
|
foldascii(&raw);
|
|
transstr(im.l, &search.raw, &local);
|
|
foldascii(&local);
|
|
clearkouho();
|
|
n = dictlookup(getlang(LangEMOJI), &raw, kouho, Maxkouho);
|
|
rawhit = n != 0;
|
|
for(i = 0; i < n; i++)
|
|
addkouho(&kouho[i]);
|
|
localhit = 0;
|
|
if(scmp(&raw, &local) != 0){
|
|
n = dictlookup(getlang(LangEMOJI), &local, kouho, Maxkouho);
|
|
localhit = n != 0;
|
|
for(i = 0; i < n; i++)
|
|
addkouho(&kouho[i]);
|
|
}
|
|
search.text = rawhit || !localhit ? raw : local;
|
|
selectfirst();
|
|
}
|
|
|
|
static void
|
|
hanjaquery(void)
|
|
{
|
|
Str key;
|
|
|
|
transstr(im.l, &search.raw, &key);
|
|
search.text = search.seed;
|
|
sappend(&search.text, &key);
|
|
clearkouho();
|
|
im.nkouho = dictlookup(getlang(LangHANJA), &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;
|
|
}
|
|
|
|
static void
|
|
endsearch(void)
|
|
{
|
|
memset(&search, 0, sizeof search);
|
|
clearkouho();
|
|
}
|
|
|
|
static void
|
|
reset(void)
|
|
{
|
|
sclear(&im.pre);
|
|
sclear(&im.raw);
|
|
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();
|
|
}
|
|
|
|
/*
|
|
* 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){
|
|
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){
|
|
endsearch();
|
|
return 1;
|
|
}
|
|
if(ks >= Kspec || (ks < ' ' && !(mod & Mctrl)) ||
|
|
(mod & (Malt|Msuper))){
|
|
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)
|
|
{
|
|
int n;
|
|
Rune c;
|
|
|
|
if(ismodkey(ks))
|
|
return 0;
|
|
if(im.l->lang == LangKO)
|
|
ks = kokey(ks, mod);
|
|
else if(isjp(&im) && ks >= 'A' && ks <= 'Z')
|
|
ks += 'a' - 'A'; /* romaji is case-blind, Caps Lock included */
|
|
/* The Korean keyboard's 한/영 and 한자 keys are Ctrl+S or Ctrl+T, and Ctrl+H. */
|
|
if(ks == Khangul){
|
|
ks = im.l->lang == LangKO ? 't' : 's';
|
|
mod = Mctrl;
|
|
}else if(ks == Khanja){
|
|
ks = 'h';
|
|
mod = Mctrl;
|
|
}
|
|
if(search.lang)
|
|
return searchkey(ks, mod, com);
|
|
n = movedelta(ks);
|
|
if(n != 0 && im.nkouho != 0){
|
|
movekouho(n);
|
|
return 1;
|
|
}
|
|
n = numberedkouho(ks, mod);
|
|
if(n >= 0){
|
|
sappend(com, &im.kouho[n]);
|
|
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 || (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){
|
|
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();
|
|
return 1;
|
|
}
|
|
if(isjp(&im))
|
|
dictqjp();
|
|
else
|
|
clearkouho();
|
|
return 1;
|
|
}
|
|
if(ks == Kesc){
|
|
if(!haspre(&im))
|
|
return 0;
|
|
if(im.sel >= 0)
|
|
im.sel = -1;
|
|
else
|
|
reset();
|
|
return 1;
|
|
}
|
|
if(ks >= Kspec || (mod & (Malt|Msuper))){
|
|
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' && im.nkouho > 0){
|
|
im.sel = -1;
|
|
flush(com);
|
|
return 1;
|
|
}
|
|
if(ks > 0x7f || ks == ' '){
|
|
if(!haspre(&im))
|
|
return 0;
|
|
flush(com);
|
|
if(com->n >= Maxrunes)
|
|
return 0;
|
|
sputr(com, ks);
|
|
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);
|
|
activeowner = nil;
|
|
activecap = 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:
|
|
if(kr->owner == activeowner){
|
|
flush(&res.commit);
|
|
activeowner = nil;
|
|
activecap = 0;
|
|
}
|
|
break;
|
|
case Keyreset:
|
|
if(kr->owner == activeowner)
|
|
flush(&res.commit);
|
|
break;
|
|
case Keycaret:
|
|
if(kr->owner == activeowner)
|
|
caret = kr->caret;
|
|
break;
|
|
case Keycap:
|
|
res.eaten = activeowner != nil && kr->owner == activeowner;
|
|
if(res.eaten)
|
|
activecap = kr->cap & Cclientpreedit;
|
|
break;
|
|
case Keypress:
|
|
res.eaten = 0;
|
|
if(activeowner != nil && kr->owner == activeowner)
|
|
activecap = kr->cap & Cclientpreedit;
|
|
if(keymeaningful(kr->ks)){
|
|
if(kr->owner != activeowner){
|
|
reset();
|
|
activeowner = kr->owner;
|
|
activecap = kr->cap & Cclientpreedit;
|
|
caret = kr->caret;
|
|
}else if(kr->caret.valid)
|
|
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);
|
|
}
|
|
}
|
|
|