Repository navigation
Expand file tree
/
Copy pathinput_adapter.lua
More file actions
453 lines (417 loc) · 16.1 KB
/
Copy pathinput_adapter.lua
File metadata and controls
453 lines (417 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
local logger = require("logger")
local util = require("util")
local SchemaSwitches = require("schema_switches")
local InputAdapter = {}
InputAdapter.__index = InputAdapter
function InputAdapter:new(options)
return setmetatable({
ime = assert(options.ime),
candidate_bar = assert(options.candidate_bar),
debug = options.debug == true,
show_schema_dialog = options.show_schema_dialog,
switch_prefs = options.switch_prefs,
active_inputbox = nil,
inputboxes = setmetatable({}, { __mode = "k" }),
installed = false,
}, self)
end
local function rawAddChars(inputbox, chars)
local method = inputbox.addChars
if type(method) == "table" and method.raw_method_call then
return method:raw_method_call(chars)
end
return method(inputbox, chars)
end
local function rawDelChar(inputbox)
local method = inputbox.delChar
if type(method) == "table" and method.raw_method_call then
return method:raw_method_call()
end
return method(inputbox)
end
local function rawMethod(inputbox, name, ...)
local method = inputbox[name]
if type(method) == "table" and method.raw_method_call then
return method:raw_method_call(...)
end
if method then return method(inputbox, ...) end
end
function InputAdapter:_removeDisplayed(inputbox)
local displayed = inputbox._rime_displayed_input or ""
for _ = 1, #displayed do rawDelChar(inputbox) end
inputbox._rime_displayed_input = ""
end
function InputAdapter:_sync(inputbox)
local commit, commit_error = self.ime:takeCommit(inputbox._rime_session)
if commit == nil then
logger.warn("[RIME] take commit failed", commit_error)
return nil, commit_error
end
if commit ~= "" then rawAddChars(inputbox, commit) end
local state, state_error = self.ime:getState(inputbox._rime_session)
if not state then
logger.warn("[RIME] get state failed", state_error)
return nil, state_error
end
inputbox._rime_state = state
inputbox._rime_displayed_input = state.input
if state.input ~= "" then rawAddChars(inputbox, state.input) end
self.candidate_bar:update(state)
return state
end
function InputAdapter:_nativeAction(inputbox, method, argument)
local started = self.debug and os.clock()
local displayed = inputbox._rime_displayed_input or ""
self:_removeDisplayed(inputbox)
local ok, err = self.ime[method](self.ime, inputbox._rime_session, argument)
if not ok then
logger.warn("[RIME] input action failed", method, err)
if displayed ~= "" then rawAddChars(inputbox, displayed) end
inputbox._rime_displayed_input = ""
inputbox._rime_state = { input = "", composing = false, candidates = {} }
inputbox._rime_failed = true
self.candidate_bar:clear()
return nil, err
end
local state, sync_error = self:_sync(inputbox)
if self.debug and state then
logger.info("[RIME][debug] action", method, "input", state.input,
"candidates", #(state.candidates or {}), "latency_ms",
string.format("%.2f", (os.clock() - started) * 1000))
end
return state, sync_error
end
-- Unlike the action helpers above, a key that Rime does not consume is a
-- normal result, not a native failure. Keep the original spelling and case
-- intact and let the active schema decide whether it belongs to composition.
function InputAdapter:_processKey(inputbox, char)
local started = self.debug and os.clock()
local displayed = inputbox._rime_displayed_input or ""
self:_removeDisplayed(inputbox)
local handled, err = self.ime:processKey(
inputbox._rime_session, char, 0)
if handled == nil then
logger.warn("[RIME] input action failed", "processKey", err)
if displayed ~= "" then rawAddChars(inputbox, displayed) end
inputbox._rime_displayed_input = ""
inputbox._rime_state = { input = "", composing = false, candidates = {} }
inputbox._rime_failed = true
self.candidate_bar:clear()
return nil, err
end
if not handled then
if displayed ~= "" then rawAddChars(inputbox, displayed) end
inputbox._rime_displayed_input = displayed
return false
end
local state, sync_error = self:_sync(inputbox)
if self.debug and state then
logger.info("[RIME][debug] key", char, "input", state.input,
"handled", true, "latency_ms",
string.format("%.2f", (os.clock() - started) * 1000))
end
if not state then return nil, sync_error end
return true
end
local function isPrintableAscii(text)
for index = 1, #text do
local byte = text:byte(index)
if byte < 0x20 or byte > 0x7e then return false end
end
return true
end
function InputAdapter:_commitComposition(inputbox)
if inputbox._rime_failed then return inputbox._rime_state end
local state = inputbox._rime_state or self.ime:getState(inputbox._rime_session)
if state and state.composing then
return self:_nativeAction(inputbox, "commit")
end
return state
end
function InputAdapter:_clearComposition(inputbox)
if inputbox._rime_failed then
self.candidate_bar:clear()
return inputbox._rime_state
end
local state = inputbox._rime_state or self.ime:getState(inputbox._rime_session)
if state and state.composing then
return self:_nativeAction(inputbox, "clear")
end
self:_removeDisplayed(inputbox)
self.candidate_bar:clear()
return state
end
function InputAdapter:_addChars(inputbox, chars)
if type(chars) ~= "string" or chars == "" then
return rawAddChars(inputbox, chars)
end
if inputbox._rime_failed then return rawAddChars(inputbox, chars) end
self.active_inputbox = inputbox
local state = inputbox._rime_state or self.ime:getState(inputbox._rime_session)
local composing = state and state.composing
local shifted_input = type(self.candidate_bar.isShiftActive) == "function"
and self.candidate_bar:isShiftActive()
if composing and chars == " " then
local candidate = self.candidate_bar:getVisibleCandidate(1)
self:_nativeAction(inputbox,
candidate and "selectCandidate" or "space",
candidate and candidate.index or nil)
return
end
if composing and chars:match("^[1-9]$") then
local candidate = self.candidate_bar:getVisibleCandidate(tonumber(chars))
if candidate then
self:_nativeAction(inputbox, "selectCandidate", candidate.index)
end
return
end
if isPrintableAscii(chars) then
for index = 1, #chars do
local handled = self:_processKey(inputbox, chars:sub(index, index))
if handled == nil then
return rawAddChars(inputbox, chars:sub(index))
end
if not handled then
local current = inputbox._rime_state
if current and current.composing then
self:_commitComposition(inputbox)
end
return rawAddChars(inputbox, chars:sub(index))
end
if shifted_input then
-- Rime's uppercase recognizer deliberately keeps strings such
-- as "Ani" in an English composition. A key from KOReader's
-- shifted character layer is a standalone literal, whether
-- Shift is one-shot or locked, so finalize it before later
-- lowercase input starts a new composition.
self:_commitComposition(inputbox)
shifted_input = false
end
end
return
end
if composing then
self:_commitComposition(inputbox)
end
return rawAddChars(inputbox, chars)
end
function InputAdapter:_delChar(inputbox)
self.active_inputbox = inputbox
if inputbox._rime_failed then return rawDelChar(inputbox) end
local state = inputbox._rime_state or self.ime:getState(inputbox._rime_session)
if state and state.composing then
self:_nativeAction(inputbox, "backspace")
return
end
return rawDelChar(inputbox)
end
function InputAdapter:_onKeyPress(inputbox, key)
if inputbox._rime_failed then return rawMethod(inputbox, "onKeyPress", key) end
local state = inputbox._rime_state or self.ime:getState(inputbox._rime_session)
if state and state.composing and type(key) == "table"
and (key.Escape or key.Esc) then
self:_clearComposition(inputbox)
return true
end
return rawMethod(inputbox, "onKeyPress", key)
end
function InputAdapter:_finish(inputbox)
local state = inputbox._rime_state
if state and state.composing then
self:_commitComposition(inputbox)
else
self:_removeDisplayed(inputbox)
end
if inputbox._rime_session then
self.ime:destroySession(inputbox._rime_session)
end
inputbox._rime_session = nil
inputbox._rime_failed = nil
inputbox._rime_state = nil
inputbox._rime_displayed_input = nil
self.inputboxes[inputbox] = nil
if self.active_inputbox == inputbox then
self.active_inputbox = nil
self.candidate_bar:clear()
end
end
function InputAdapter:wrapInputBox(inputbox)
if inputbox._rime_wrapped then return end
local session, err = self.ime:createSession()
if not session then
logger.warn("[RIME] cannot create InputBox session", err)
if self.original_wrap then return self.original_wrap(inputbox) end
return
end
inputbox._rime_wrapped = true
inputbox._rime_session = session
inputbox._rime_displayed_input = ""
self:_applySessionSwitches(session)
local initial_state, state_error = self.ime:getState(session)
if not initial_state then
self.ime:destroySession(session)
inputbox._rime_wrapped = nil
inputbox._rime_session = nil
logger.warn("[RIME] cannot read initial InputBox state", state_error)
if self.original_wrap then return self.original_wrap(inputbox) end
return
end
inputbox._rime_state = initial_state
self.inputboxes[inputbox] = true
self.active_inputbox = inputbox
self.candidate_bar:clear()
local wrappers = {
util.wrapMethod(inputbox, "addChars", function(box, chars)
return self:_addChars(box, chars)
end, nil),
util.wrapMethod(inputbox, "delChar", function(box)
return self:_delChar(box)
end, nil),
util.wrapMethod(inputbox, "onKeyPress", function(box, key)
return self:_onKeyPress(box, key)
end, nil),
}
local commit_before = {
"leftChar", "rightChar", "upLine", "downLine",
"goToStartOfLine", "goToEndOfLine", "goToHome", "goToEnd",
"moveCursorToCharPos", "delNextChar", "delWord", "unfocus",
"onCloseKeyboard", "onSwitchingKeyboardLayout",
"onTapTextBox", "onHoldTextBox", "onSwipeTextBox",
}
for _, method in ipairs(commit_before) do
wrappers[#wrappers + 1] = util.wrapMethod(inputbox, method, nil, function(box)
self:_commitComposition(box)
end)
end
for _, method in ipairs({ "clear", "delToStartOfLine" }) do
wrappers[#wrappers + 1] = util.wrapMethod(inputbox, method, nil, function(box)
self:_clearComposition(box)
end)
end
return function()
if not inputbox._rime_wrapped then return end
self:_finish(inputbox)
for _, wrapper in ipairs(wrappers) do wrapper:revert() end
inputbox._rime_wrapped = nil
end
end
function InputAdapter:nextPage()
local inputbox = self.active_inputbox
if not inputbox or inputbox._rime_failed then return end
local ok, err = self.ime:nextPage(inputbox._rime_session)
if not ok then logger.warn("[RIME] next page failed", err); return end
local state = self.ime:getState(inputbox._rime_session)
inputbox._rime_state = state
return state
end
function InputAdapter:previousPage()
local inputbox = self.active_inputbox
if not inputbox or inputbox._rime_failed then return end
local ok, err = self.ime:previousPage(inputbox._rime_session)
if not ok then logger.warn("[RIME] previous page failed", err); return end
local state = self.ime:getState(inputbox._rime_session)
inputbox._rime_state = state
return state
end
function InputAdapter:getCandidateWindow(offset)
local inputbox = self.active_inputbox
if not inputbox or inputbox._rime_failed then return end
return self.ime:getCandidateWindow(inputbox._rime_session, offset)
end
function InputAdapter:selectCandidate(index)
local inputbox = self.active_inputbox
if not inputbox or inputbox._rime_failed then return end
return self:_nativeAction(inputbox, "selectCandidate", index)
end
function InputAdapter:escape()
local inputbox = self.active_inputbox
if not inputbox then return end
return self:_clearComposition(inputbox)
end
function InputAdapter:showSchemaDialog()
if self.show_schema_dialog then return self.show_schema_dialog() end
end
function InputAdapter:switchSchema(schema)
local switched = {}
for inputbox in pairs(self.inputboxes) do
if inputbox._rime_failed then
self:_removeDisplayed(inputbox)
else
self:_clearComposition(inputbox)
end
local ok, err = self.ime:selectSchema(inputbox._rime_session, schema)
if not ok then
logger.warn("[RIME] schema switch failed", schema, err)
return nil, err
end
local state, state_error = self.ime:getState(inputbox._rime_session)
if not state then return nil, state_error end
inputbox._rime_state = state
inputbox._rime_displayed_input = state.input or ""
inputbox._rime_failed = nil
switched[inputbox] = state
end
local ok, err = self.ime:setSchema(schema)
if not ok then return nil, err end
local active_state = self.active_inputbox and switched[self.active_inputbox]
if active_state then
self.candidate_bar:update(active_state)
else
self.candidate_bar:clear()
end
logger.info("[RIME] schema switched", schema)
return true
end
-- Re-apply the persisted per-schema switch selections whenever a session is
-- created, since librime options are session state that otherwise resets to
-- the schema's defaults.
function InputAdapter:_applySessionSwitches(session)
if not self.ime or not self.ime:hasSwitchesSupport() then return end
local prefs = self.switch_prefs and self.switch_prefs[self.ime.schema]
if not prefs then return end
local defaults, err = self.ime:getSchemaSwitches(self.ime.schema)
if not defaults then
logger.warn("[RIME] cannot read schema switches", err)
return
end
local switches = SchemaSwitches:applyPrefs(defaults, prefs)
local ok, apply_error = self.ime:setSchemaSwitches(session, switches)
if not ok then
logger.warn("[RIME] cannot apply schema switch preferences", apply_error)
end
end
function InputAdapter:setSwitches(schema, switches)
if not self.ime or not self.ime:hasSwitchesSupport() then
return nil, "native switches API is unavailable"
end
for inputbox in pairs(self.inputboxes) do
local ok, err =
self.ime:setSchemaSwitches(inputbox._rime_session, switches)
if not ok then
logger.warn("[RIME] cannot apply switches to InputBox session", err)
return nil, err
end
end
return true
end
function InputAdapter:install()
if self.installed then return true end
local keyboard = require("ui/data/keyboardlayouts/rime_keyboard")
if not keyboard then return nil, "RIME keyboard layout is unavailable" end
self.original_wrap = keyboard.wrapInputBox
keyboard.wrapInputBox = function(inputbox)
return self:wrapInputBox(inputbox)
end
self.candidate_bar:setController(self)
self.installed = true
logger.info("[RIME] InputAdapter installed")
return true
end
function InputAdapter:uninstall()
if not self.installed then return end
local keyboard = require("ui/data/keyboardlayouts/rime_keyboard")
keyboard.wrapInputBox = self.original_wrap
self.candidate_bar:setController(nil)
self.installed = false
end
return InputAdapter