Keyboard: add Chinese stroke-based layout (#9572)

Basically it uses 5 keys for 5 basic stroke types and
inputs characters by their stroke order.
See https://en.wikipedia.org/wiki/Stroke_count_method
reviewable/pr9582/r1
weijiuqiao 2 years ago committed by GitHub
parent 8fcc712c76
commit 05aba404b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,349 @@
-----------------------------------------
-- 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 == 0 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 = ""
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

@ -0,0 +1,202 @@
--[[--
Chinese stroke-based input method for Lua/KOReader.
Uses five basic strokes plus a wildcard stroke to input Chinese characters.
Supports both simplified and traditional.
Characters hardcoded on keys are uniform, no translation needed.
In-place candidates can be turned off in keyboard settings.
A Separation key is used to finish inputting a character.
A Switch key is used to iterate candidates.
Stroke-wise deletion (input not finished) mapped to the default Del key.
Character-wise deletion mapped to north of Separation key.
rf. https://en.wikipedia.org/wiki/Stroke_count_method
--]]
local IME = require("frontend/ui/data/keyboardlayouts/zh_ime")
local util = require("util")
local JA = require("ui/data/keyboardlayouts/ja_keyboard_keys")
local _ = require("gettext")
local SHOW_CANDI_KEY = "keyboard_chinese_stroke_show_candidates"
local s_3 = { alt_label = "%°#", "3", west = "%", north = "°", east = "#" }
local s_8 = { alt_label = "&-/", "8", west = "&", north = "-", east = "/" }
local comma_popup = { "",
north = "",
alt_label = "",
northeast = "",
northwest = "",
east = "",
west = "",
south = ",",
southeast = "",
southwest = "",
"{",
"[",
";",
}
local period_popup = { "",
north = "",
alt_label = "",
northeast = "",
northwest = "",
east = "",
west = "",
south = ".",
southeast = "",
southwest = "",
"}",
"]",
":",
}
local H = "H" -- stroke_h 横
local I = "I" -- stroke_s 竖
local J = "J" -- stroke_p 撇
local K = "K" -- stroke_n 捺
local L = "L" -- stroke_z 折
local W = "`" -- wildcard, * is not used because it can be input from symbols
local genMenuItems = function(self)
return {
{
text = _("Show character candidates"),
checked_func = function()
return G_reader_settings:nilOrTrue(SHOW_CANDI_KEY)
end,
callback = function()
G_reader_settings:flipNilOrTrue(SHOW_CANDI_KEY)
end,
},
}
end
local code_map = require("frontend/ui/data/keyboardlayouts/zh_stroke_data")
local ime = IME:new{
code_map = code_map,
key_map = {
[""] = H,
[""] = I,
[""] = J,
[""] = K,
[""] = L,
[W] = W, -- wildcard
},
iter_map = {
H = I,
I = J,
J = K,
K = L,
L = H,
},
iter_map_last_key = L,
show_candi_callback = function()
return G_reader_settings:nilOrTrue(SHOW_CANDI_KEY)
end,
W = W -- has wildcard function
}
local wrappedAddChars = function(inputbox, char)
ime:wrappedAddChars(inputbox, char)
end
local function wrappedSeparate(inputbox)
ime:wrappedSeparate(inputbox)
end
local function wrappedDelChar(inputbox)
ime:wrappedDelChar(inputbox)
end
local function clear_stack()
ime:clear_stack()
end
local wrapInputBox = function(inputbox)
if inputbox._zh_stroke_wrapped == nil then
inputbox._zh_stroke_wrapped = true
local wrappers = {}
-- Wrap all of the navigation and non-single-character-input keys with
-- a callback to clear the tap window, but pass through to the
-- original function.
-- -- Delete text.
table.insert(wrappers, util.wrapMethod(inputbox, "delChar", wrappedDelChar, nil))
table.insert(wrappers, util.wrapMethod(inputbox, "delToStartOfLine", nil, clear_stack))
table.insert(wrappers, util.wrapMethod(inputbox, "clear", nil, clear_stack))
-- -- Navigation.
table.insert(wrappers, util.wrapMethod(inputbox, "leftChar", nil, wrappedSeparate))
table.insert(wrappers, util.wrapMethod(inputbox, "rightChar", nil, wrappedSeparate))
table.insert(wrappers, util.wrapMethod(inputbox, "upLine", nil, wrappedSeparate))
table.insert(wrappers, util.wrapMethod(inputbox, "downLine", nil, wrappedSeparate))
-- -- Move to other input box.
table.insert(wrappers, util.wrapMethod(inputbox, "unfocus", nil, wrappedSeparate))
table.insert(wrappers, util.wrapMethod(inputbox, "onCloseKeyboard", nil, wrappedSeparate))
-- -- Gestures to move cursor.
table.insert(wrappers, util.wrapMethod(inputbox, "onTapTextBox", nil, wrappedSeparate))
table.insert(wrappers, util.wrapMethod(inputbox, "onHoldTextBox", nil, wrappedSeparate))
table.insert(wrappers, util.wrapMethod(inputbox, "onSwipeTextBox", nil, wrappedSeparate))
-- -- Others
table.insert(wrappers, util.wrapMethod(inputbox, "onSwitchingKeyboardLayout", nil, wrappedSeparate))
-- addChars is the only method we need a more complicated wrapper for.
table.insert(wrappers, util.wrapMethod(inputbox, "addChars", wrappedAddChars, nil))
return function()
if inputbox._zh_stroke_wrapped then
for _, wrapper in ipairs(wrappers) do
wrapper:revert()
end
inputbox._zh_stroke_wrapped = nil
end
end
end
end
return {
min_layer = 1,
max_layer = 2,
shiftmode_keys = {["123"] = false},
symbolmode_keys = {["Sym"] = false},
utf8mode_keys = {["🌐"] = true},
umlautmode_keys = {["Äéß"] = false}, -- Disabled 'umlaut' keys
keys = {
-- first row
{
{ label = "123" },
{ JA.s_1, { label = "", "", north="——"} },
{ JA.s_2, { label = "", ""} },
{ s_3, { label = "丿", ""} },
{ label = "", bold = false } -- backspace
},
-- second row
{
{ label = "" },
{ JA.s_4, { label = "", "", north="" } },
{ JA.s_5, { label = "𠃋", "" } },
{ JA.s_6, { ime.separator, north=ime.local_del, alt_label=ime.local_del } },
{ label = "" },
},
-- third row
{
{ label = "" },
{ JA.s_7, ime.switch_char },
{ s_8, comma_popup },
{ JA.s_9, period_popup },
{ label = "" },
},
-- fourth row
{
{ label = "🌐" },
{ label = "空格", " ", " ", width = 2.0 },
{ JA.s_0, { label = "", W } },
{ label = "", "\n", "\n", bold = true }, -- return
},
},
wrapInputBox = wrapInputBox,
genMenuItems = genMenuItems,
}

File diff suppressed because one or more lines are too long

@ -139,7 +139,9 @@ if Device:isTouchDevice() or Device:hasDPad() then
self.is_keyboard_hidden = false
end
end
if #self.charlist > 0 then -- Avoid cursor moving within a hint.
-- zh keyboard with candidates shown here has _frame_textwidget.dimen = nil.
-- Check to avoid crash.
if #self.charlist > 0 and self._frame_textwidget.dimen then -- Avoid cursor moving within a hint.
local textwidget_offset = self.margin + self.bordersize + self.padding
local x = ges.pos.x - self._frame_textwidget.dimen.x - textwidget_offset
local y = ges.pos.y - self._frame_textwidget.dimen.y - textwidget_offset

@ -64,6 +64,7 @@ function VirtualKey:init()
elseif self.keyboard.utf8mode_keys[self.label] ~= nil then
self.key_chars = self:genKeyboardLayoutKeyChars()
self.callback = function ()
self.keyboard:onSwitchingKeyboardLayout()
local current = G_reader_settings:readSetting("keyboard_layout")
local default = G_reader_settings:readSetting("keyboard_layout_default")
local keyboard_layouts = G_reader_settings:readSetting("keyboard_layouts", {})
@ -86,6 +87,7 @@ function VirtualKey:init()
self.keyboard:setKeyboardLayout(next_layout)
end
self.hold_callback = function()
self.keyboard:onSwitchingKeyboardLayout()
if util.tableSize(self.key_chars) > 5 then -- 2 or more layouts enabled
self.popup = VirtualKeyPopup:new{
parent_key = self,
@ -100,6 +102,7 @@ function VirtualKey:init()
end
self.hold_cb_is_popup = true
self.swipe_callback = function(ges)
self.keyboard:onSwitchingKeyboardLayout()
local key_function = self.key_chars[ges.direction.."_func"]
if key_function then
key_function()
@ -760,10 +763,12 @@ local VirtualKeyboard = FocusManager:new{
ko_KR = "ko_KR_keyboard",
ru = "ru_keyboard",
tr = "tr_keyboard",
zh = "zh_keyboard",
},
lang_has_submenu = {
ja = true,
zh = true,
},
}
@ -1035,6 +1040,11 @@ function VirtualKeyboard:goToStartOfLine()
self.inputbox:goToStartOfLine()
end
-- Some keyboard with intermediate state (ie. zh) may need to be notified
function VirtualKeyboard:onSwitchingKeyboardLayout()
if self.inputbox.onSwitchingKeyboardLayout then self.inputbox:onSwitchingKeyboardLayout() end
end
function VirtualKeyboard:goToEndOfLine()
self.inputbox:goToEndOfLine()
end

Loading…
Cancel
Save