|
|
|
-----------------------------------------
|
|
|
|
-- General Chinese input method engine --
|
|
|
|
-----------------------------------------
|
|
|
|
local logger = require("logger")
|
|
|
|
local util = require("util")
|
|
|
|
|
|
|
|
local function binarysearch( tbl, value, fcompval, reversed )
|
|
|
|
if not fcompval then return end
|
|
|
|
local iStart,iEnd = 1,#tbl
|
|
|
|
local iMid
|
|
|
|
while iStart <= iEnd do
|
|
|
|
iMid = math.floor( (iStart+iEnd)/2 )
|
|
|
|
local value2 = fcompval( tbl[iMid] )
|
|
|
|
if value == value2 then
|
|
|
|
if iMid == 1 or fcompval( tbl[iMid-1] ) ~= value then
|
|
|
|
return iMid
|
|
|
|
end
|
|
|
|
iEnd = iMid - 1
|
|
|
|
while iStart <= iEnd do
|
|
|
|
iMid = math.floor( (iStart+iEnd)/2 )
|
|
|
|
value2 = fcompval( tbl[iMid] )
|
|
|
|
if value2 == value then
|
|
|
|
if fcompval( tbl[iMid-1] ) ~= value then
|
|
|
|
return iMid
|
|
|
|
else
|
|
|
|
iEnd = iMid - 1
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if fcompval( tbl[iMid+1] ) == value then
|
|
|
|
return iMid + 1
|
|
|
|
else
|
|
|
|
iStart = iMid + 2
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return iMid
|
|
|
|
elseif ( reversed and value2 < value ) or ( not reversed and value2 > value ) then
|
|
|
|
iEnd = iMid - 1
|
|
|
|
else
|
|
|
|
iStart = iMid + 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function stringReplaceAt(str, pos, r)
|
|
|
|
return str:sub(1, pos-1) .. r .. str:sub(pos+1)
|
|
|
|
end
|
|
|
|
|
|
|
|
local _stack
|
|
|
|
local IME = {
|
|
|
|
code_map = {},
|
|
|
|
key_map = nil, -- input key to code map
|
|
|
|
keys_string = "abcdefghijklmnopqrstuvwxyz",
|
|
|
|
iter_map = nil, -- next code when using wildcard
|
|
|
|
iter_map_last_key = nil,
|
|
|
|
show_candi_callback = function() end,
|
|
|
|
switch_char = "下一字", -- default
|
|
|
|
separator = "分隔", -- default
|
|
|
|
use_space_as_separator = true,
|
|
|
|
local_del = "", -- default
|
|
|
|
W = nil -- default no wildcard
|
|
|
|
}
|
|
|
|
|
|
|
|
function IME:new(new_o)
|
|
|
|
local o = new_o or {}
|
|
|
|
setmetatable(o, self)
|
|
|
|
self.__index = self
|
|
|
|
o:init()
|
|
|
|
return o
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:init()
|
|
|
|
self:clear_stack()
|
|
|
|
|
|
|
|
self.sorted_codes = {}
|
|
|
|
for k,_ in pairs(self.code_map) do
|
|
|
|
table.insert(self.sorted_codes, k)
|
|
|
|
end
|
|
|
|
table.sort(self.sorted_codes)
|
|
|
|
|
|
|
|
if not self.key_map and self.keys_string then
|
|
|
|
self.key_map = {}
|
|
|
|
for i=0, #self.keys_string do
|
|
|
|
self.key_map[self.keys_string:sub(i, i)] = self.keys_string:sub(i, i)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not self.iter_map and self.W then
|
|
|
|
self.iter_map = {}
|
|
|
|
local keys = util.splitToChars(self.keys_string)
|
|
|
|
for i=1, #keys-1 do
|
|
|
|
self.iter_map[keys[i]] = keys[i+1]
|
|
|
|
end
|
|
|
|
if #keys > 1 then
|
|
|
|
self.iter_map[keys[#keys]] = keys[1]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:clear_stack()
|
|
|
|
_stack = { {code="", char="", index=1, candi={} } }
|
|
|
|
self.last_key = ""
|
|
|
|
self.last_index = 0
|
|
|
|
self.hint_char_count = 0
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:reset_status()
|
|
|
|
self.last_key = ""
|
|
|
|
self.last_index = 0
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:searchStartWith(code)
|
|
|
|
local result = binarysearch(self.sorted_codes, code, function(v) return string.sub(v or "", 1, #code) end)
|
|
|
|
if result then
|
|
|
|
local candi = self.code_map[self.sorted_codes[result]]
|
|
|
|
if candi then
|
|
|
|
logger.dbg("zh_kbd: got search result starting with", code, ":", candi)
|
|
|
|
end
|
|
|
|
if type(candi) == "string" then
|
|
|
|
return { candi }
|
|
|
|
end
|
|
|
|
return candi
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:getCandiFromMap(code)
|
|
|
|
local candi = self.code_map[code]
|
|
|
|
if candi then
|
|
|
|
logger.dbg("zh_kbd: got candi from map with", code, ":", candi)
|
|
|
|
end
|
|
|
|
if type(candi) == "string" then
|
|
|
|
return { candi }
|
|
|
|
end
|
|
|
|
return candi
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:getCandi(code)
|
|
|
|
return self:getCandiFromMap(code) or self:searchStartWith(code) or {}
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:getCandiWithWildcard(code, from_reset)
|
|
|
|
logger.dbg("zh_kdb: getCandiWithWildcard:", code, "lastKey:", self.last_key)
|
|
|
|
for i=#code, 1, -1 do
|
|
|
|
if code:sub(i, i) == self.W then
|
|
|
|
if self.last_key:sub(i, i) == self.iter_map_last_key then
|
|
|
|
local next = self.iter_map[self.iter_map_last_key]
|
|
|
|
self.last_key = stringReplaceAt(self.last_key, i, next)
|
|
|
|
else
|
|
|
|
self.last_key = stringReplaceAt(self.last_key, i, self.iter_map[self.last_key:sub(i, i)])
|
|
|
|
self.last_candi = self:getCandi(self.last_key)
|
|
|
|
if #self.last_candi > 0 then
|
|
|
|
logger.dbg("zh_kbd: got candi with wildchard for key", self.last_key, ":", self.last_candi)
|
|
|
|
return self.last_candi
|
|
|
|
end
|
|
|
|
return self:getCandiWithWildcard(code, from_reset)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- all wildcard reset
|
|
|
|
self.last_candi = self:getCandi(self.last_key)
|
|
|
|
if #self.last_candi > 0 then
|
|
|
|
logger.dbg("zh_kbd: got candi with wildchard for key", self.last_key, ":", self.last_candi)
|
|
|
|
return self.last_candi
|
|
|
|
elseif not from_reset then
|
|
|
|
return self:getCandiWithWildcard(code, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:getCandidates(code)
|
|
|
|
logger.dbg("zh_kbd: getCandidates", code)
|
|
|
|
if self.W then
|
|
|
|
local wildcard_count = select(2, string.gsub(code, self.W, ""))
|
|
|
|
if wildcard_count > 5 then
|
|
|
|
-- we limit the wildcard count to 5 due to performance conserns
|
|
|
|
return
|
|
|
|
elseif wildcard_count ~= 0 then
|
|
|
|
if #code == #self.last_key then -- only index change, no new stroke
|
|
|
|
local last_candi = _stack[#_stack].candi
|
|
|
|
return self:getCandiWithWildcard(code), last_candi
|
|
|
|
else
|
|
|
|
self:reset_status()
|
|
|
|
self.last_key = code:gsub(self.W, self.iter_map_last_key)
|
|
|
|
return self:getCandiWithWildcard(code)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- no wildcard
|
|
|
|
return self:getCandiFromMap(code) or self:searchStartWith(code)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
--- inputbox operation
|
|
|
|
function IME:delHintChars(inputbox)
|
|
|
|
logger.dbg("zh_kbd: delete hint chars of count", self.hint_char_count)
|
|
|
|
for i=1, self.hint_char_count do
|
|
|
|
inputbox.delChar:raw_method_call()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:getHintChars()
|
|
|
|
self.hint_char_count = 0
|
|
|
|
local hint_chars = ""
|
|
|
|
for i=1, #_stack do
|
|
|
|
hint_chars = hint_chars .. _stack[i].char
|
|
|
|
if _stack[i].char ~= "" then
|
|
|
|
self.hint_char_count = self.hint_char_count + #util.splitToChars(_stack[i].char)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
local imex = _stack[#_stack]
|
|
|
|
local has_wildcard = self.W and imex.code:find(self.W)
|
|
|
|
if self:show_candi_callback() and -- shows candidates
|
|
|
|
#imex.candi ~= 0 and -- has candidates
|
|
|
|
( #imex.code > 1 or imex.index > 1 ) and -- more than one key
|
|
|
|
( #imex.candi > 1 or has_wildcard and imex.candi[1] ~= (imex.last_candi or {})[1] ) then -- one candidate but use wildcard, or more candidates
|
|
|
|
hint_chars = hint_chars .. "["
|
|
|
|
if #imex.candi > 1 then
|
|
|
|
local remainder
|
|
|
|
if not has_wildcard then
|
|
|
|
remainder = (imex.index+1) % #imex.candi
|
|
|
|
else
|
|
|
|
remainder = (imex.index-self.last_index+1) % #imex.candi
|
|
|
|
end
|
|
|
|
local pos = remainder == 0 and #imex.candi or remainder
|
|
|
|
if not (has_wildcard and pos == 1) then
|
|
|
|
for i=1, math.min(#imex.candi-1, 5) do
|
|
|
|
hint_chars = hint_chars .. imex.candi[pos]
|
|
|
|
self.hint_char_count = self.hint_char_count + #util.splitToChars(imex.candi[pos])
|
|
|
|
pos = pos == #imex.candi and 1 or pos+1
|
|
|
|
if has_wildcard and pos == 1 then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if #imex.candi > 6 or has_wildcard then
|
|
|
|
hint_chars = hint_chars .. "…"
|
|
|
|
self.hint_char_count = self.hint_char_count + 1
|
|
|
|
end
|
|
|
|
hint_chars = hint_chars .. "]"
|
|
|
|
self.hint_char_count = self.hint_char_count + 2
|
|
|
|
end
|
|
|
|
logger.dbg("zh_kbd: got hint chars:", hint_chars, "with count", self.hint_char_count)
|
|
|
|
return hint_chars
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:refreshHintChars(inpuxbox)
|
|
|
|
self:delHintChars(inpuxbox)
|
|
|
|
inpuxbox.addChars:raw_method_call(self:getHintChars())
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:wrappedSeparate(inputbox)
|
|
|
|
local imex = _stack[#_stack]
|
|
|
|
if self:show_candi_callback() and ( #imex.candi > 1 or self.W and imex.code:find(self.W) ) then
|
|
|
|
imex.candi = {}
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
end
|
|
|
|
self:clear_stack()
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function IME:wrappedDelChar(inputbox)
|
|
|
|
local imex = _stack[#_stack]
|
|
|
|
-- stepped deletion
|
|
|
|
if #imex.code > 1 then
|
|
|
|
-- last char has over one input strokes
|
|
|
|
imex.code = string.sub(imex.code, 1, -2)
|
|
|
|
imex.index = 1
|
|
|
|
imex.candi, imex.last_candi = self:getCandidates(imex.code)
|
|
|
|
imex.char = imex.candi[1]
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
elseif #_stack > 1 then
|
|
|
|
-- over one chars, last char has only one stroke
|
|
|
|
_stack[#_stack] = nil
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
elseif #imex.code == 1 then
|
|
|
|
-- one char with one stroke
|
|
|
|
self:delHintChars(inputbox)
|
|
|
|
self:clear_stack()
|
|
|
|
else
|
|
|
|
inputbox.delChar:raw_method_call()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function IME:wrappedAddChars(inputbox, char)
|
|
|
|
local imex = _stack[#_stack]
|
|
|
|
if char == self.switch_char then
|
|
|
|
imex.index = imex.index + 1
|
|
|
|
if self.W and imex.code:find(self.W) then
|
|
|
|
if #imex.candi == 0 then
|
|
|
|
return
|
|
|
|
elseif imex.index - self.last_index > #imex.candi then
|
|
|
|
self.last_index = self.last_index + #imex.candi
|
|
|
|
imex.candi,imex.last_candi = self:getCandidates(imex.code)
|
|
|
|
imex.char = imex.candi[1]
|
|
|
|
else
|
|
|
|
imex.char = imex.candi[imex.index - self.last_index]
|
|
|
|
end
|
|
|
|
elseif #imex.candi > 1 then
|
|
|
|
local remainder = imex.index % #imex.candi
|
|
|
|
imex.char = imex.candi[remainder==0 and #imex.candi or remainder]
|
|
|
|
else
|
|
|
|
return
|
|
|
|
end
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
elseif char == self.separator or
|
|
|
|
self.use_space_as_separator and char == " " and _stack[1].code ~= "" then
|
|
|
|
imex.candi = {}
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
self:clear_stack()
|
|
|
|
return
|
|
|
|
elseif char == self.local_del then
|
|
|
|
if #imex.code > 0 then
|
|
|
|
imex.candi = {}
|
|
|
|
imex.char = ""
|
|
|
|
local previous_imex = _stack[#_stack-1]
|
|
|
|
if previous_imex then
|
|
|
|
previous_imex.candi = {}
|
|
|
|
previous_imex.char = ""
|
|
|
|
end
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
self:clear_stack()
|
|
|
|
else
|
|
|
|
inputbox.delChar:raw_method_call()
|
|
|
|
end
|
|
|
|
else
|
|
|
|
local key = self.key_map[char]
|
|
|
|
if key then
|
|
|
|
imex.index = 1
|
|
|
|
self:reset_status()
|
|
|
|
local new_candi
|
|
|
|
new_candi,imex.last_candi = self:getCandidates(imex.code..key)
|
|
|
|
if new_candi and #new_candi > 0 then
|
|
|
|
imex.code = imex.code .. key
|
|
|
|
imex.char = new_candi[1]
|
|
|
|
imex.candi = new_candi
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
else
|
|
|
|
new_candi,imex.last_candi = self:getCandidates(key) or {},nil -- single stroke
|
|
|
|
table.insert(_stack, {code=key, index=1, char=new_candi[1], candi=new_candi})
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
if #imex.candi > 1 then
|
|
|
|
imex.candi = {}
|
|
|
|
self:refreshHintChars(inputbox)
|
|
|
|
end
|
|
|
|
self:clear_stack()
|
|
|
|
inputbox.addChars:raw_method_call(char)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return IME
|