engine: a takeover keeps the text it took for the context it took it from

Two applications' focus events cross — ibus-daemon documents the case —
so the first key of the new one can arrive before the old one says it
lost focus.  The engine dropped whatever that context was composing; it
now keeps it and hands it back with the reset or release that follows,
which every frontend already commits.  Normal ordering is unchanged.
This commit is contained in:
2026-08-17 12:07:19 +09:00
parent 34c0c501aa
commit a0e83f98c6
4 changed files with 30 additions and 4 deletions

View File

@@ -3,6 +3,8 @@
static Im im; static Im im;
static void *activeowner; 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 int clientpre; /* the owner draws its own preedit */
static Caret caret; static Caret caret;
static Drawcmd lastdraw; static Drawcmd lastdraw;
@@ -663,6 +665,20 @@ flush(Str *com)
reset(); 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 * A search takes over the keys. Hanja converts the pending syllable, so
* it becomes the query; anything else pending is committed. * it becomes the query; anything else pending is committed.
@@ -881,6 +897,7 @@ init(void)
memset(&search, 0, sizeof search); memset(&search, 0, sizeof search);
memset(&caret, 0, sizeof caret); memset(&caret, 0, sizeof caret);
activeowner = nil; activeowner = nil;
lostowner = nil;
clientpre = 0; clientpre = 0;
memset(&lastdraw, 0, sizeof lastdraw); memset(&lastdraw, 0, sizeof lastdraw);
} }
@@ -902,6 +919,7 @@ imhandlekey(Keyreq *kr)
res.eaten = 1; res.eaten = 1;
switch(kr->op){ switch(kr->op){
case Keyrelease: case Keyrelease:
takelost(kr->owner, &res.commit);
if(kr->owner == activeowner){ if(kr->owner == activeowner){
flush(&res.commit); flush(&res.commit);
activeowner = nil; activeowner = nil;
@@ -909,6 +927,7 @@ imhandlekey(Keyreq *kr)
} }
break; break;
case Keyreset: case Keyreset:
takelost(kr->owner, &res.commit);
if(kr->owner == activeowner) if(kr->owner == activeowner)
flush(&res.commit); flush(&res.commit);
break; break;
@@ -927,7 +946,9 @@ imhandlekey(Keyreq *kr)
clientpre = kr->clientpre; clientpre = kr->clientpre;
if(keymeaningful(kr->ks)){ if(keymeaningful(kr->ks)){
if(kr->owner != activeowner){ if(kr->owner != activeowner){
reset(); sclear(&lost);
flush(&lost);
lostowner = lost.n > 0 ? activeowner : nil;
activeowner = kr->owner; activeowner = kr->owner;
clientpre = kr->clientpre; clientpre = kr->clientpre;
} }

View File

@@ -234,6 +234,7 @@ engine_active_owner_lifecycle(struct ct *t)
checkstr(t, "takeover resets old preedit", "", &res.preedit); checkstr(t, "takeover resets old preedit", "", &res.preedit);
CT_EQ_PTR(t, &b, activeowner); CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&a, Keyreset, 0, 0); res = ownerrequest(&a, Keyreset, 0, 0);
checkstr(t, "the taken owner gets its reading back", "", &res.commit);
CT_EQ_INT(t, 0, res.preedit.n); CT_EQ_INT(t, 0, res.preedit.n);
impre(&im, &shown); impre(&im, &shown);
checkstr(t, "stale reset preserves owner", "", &shown); checkstr(t, "stale reset preserves owner", "", &shown);

View File

@@ -284,7 +284,8 @@ server_connection_ownership(struct ct *t)
req = nextrequest(t, &f.pump, Keyreset); req = nextrequest(t, &f.pump, Keyreset);
CT_EQ_PTR(t, aowner, req.owner); CT_EQ_PTR(t, aowner, req.owner);
allowrequest(&f.pump); allowrequest(&f.pump);
CT_CHECK(t, readreply(t, &a, 1, "", preedit)); /* The reset hands back what the takeover took from this client. */
CT_CHECK(t, readreply(t, &a, 1, "", preedit));
CT_EQ_STR(t, "", preedit); CT_EQ_STR(t, "", preedit);
CT_EQ_PTR(t, bowner, testengineowner()); CT_EQ_PTR(t, bowner, testengineowner());
if(!sendkey(t, &b, 1, 0, 0)) if(!sendkey(t, &b, 1, 0, 0))

View File

@@ -1059,9 +1059,12 @@ xim_adapter_callback_owner_loss(struct ct *t)
checkpreowner(); checkpreowner();
notrace(t, &f); notrace(t, &f);
/* Late XIM lifecycle messages remain idempotent after owner cleanup. */ /* The release hands back the reading the takeover took. */
release(&state); release(&state);
nexttrace(t, &f, Keyrelease, &state); nexttrace(t, &f, Keyrelease, &state);
CT_EQ_INT(t, 5, nwirecalls);
CT_EQ_INT(t, Wcommit, wirecalls[4].op);
/* Late XIM lifecycle messages remain idempotent after owner cleanup. */
release(&state); release(&state);
notrace(t, &f); notrace(t, &f);
memset(&hdr, 0, sizeof hdr); memset(&hdr, 0, sizeof hdr);
@@ -1071,7 +1074,7 @@ xim_adapter_callback_owner_loss(struct ct *t)
hdr.major_opcode = XCB_XIM_DESTROY_IC; hdr.major_opcode = XCB_XIM_DESTROY_IC;
callback(nil, nil, ic, &hdr, nil, nil, nil); callback(nil, nil, ic, &hdr, nil, nil, nil);
notrace(t, &f); notrace(t, &f);
CT_EQ_INT(t, 4, nwirecalls); CT_EQ_INT(t, 5, nwirecalls);
memset(&req, 0, sizeof req); memset(&req, 0, sizeof req);
req.owner = &foreign; req.owner = &foreign;