2
0
mirror of https://github.com/koreader/koreader synced 2024-11-10 01:10:34 +00:00
koreader/frontend/ui/widget/virtualkeyboard.lua
NiLuJe 4cc620b702
VirtualKeyboard: Revamp visibility handling (#10852)
Move as much of the state tracking as possible inside VirtualKeyboard itself.
InputDialog unfortunately needs an internal tracking of this state because it needs to know about it *before* the VK is shown, so we have to keep a bit of duplication in there, although we do try much harder to keep everything in sync (at least at function call edges), and to keep the damage contained to, essentially, the toggle button's handler.

(Followup to #10803 & #10850)
2023-09-01 22:51:41 +02:00

1162 lines
40 KiB
Lua

local Blitbuffer = require("ffi/blitbuffer")
local BottomContainer = require("ui/widget/container/bottomcontainer")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local FocusManager = require("ui/widget/focusmanager")
local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
local ImageWidget = require("ui/widget/imagewidget")
local InputContainer = require("ui/widget/container/inputcontainer")
local KeyboardLayoutDialog = require("ui/widget/keyboardlayoutdialog")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local time = require("ui/time")
local util = require("util")
local Screen = Device.screen
local keyboard_state = {
force_current_layout = false, -- Set to true to get/set current layout (instead of default layout)
}
local DEFAULT_LABEL_SIZE = 22
local VirtualKeyPopup
local VirtualKey = InputContainer:extend{
key = nil,
icon = nil,
label = nil,
bold = nil,
keyboard = nil,
callback = nil,
-- This is to inhibit the key's own refresh (useful to avoid conflicts on Layer changing keys)
skiptap = nil,
skiphold = nil,
width = nil,
height = math.max(Screen:getWidth(), Screen:getHeight())*0.33,
bordersize = 0,
focused_bordersize = Size.border.default,
radius = 0,
face = Font:getFace("infont"),
}
-- For caps lock, it's necessary because after setLayout, the new shift key is no longer the same virtual key
-- thus rendering its preset .ignore_key_release property useless
local ignore_key_release
function VirtualKey:init()
local label_font_size = G_reader_settings:readSetting("keyboard_key_font_size", DEFAULT_LABEL_SIZE)
self.face = Font:getFace("infont", label_font_size)
self.bold = G_reader_settings:isTrue("keyboard_key_bold")
if self.keyboard.symbolmode_keys[self.label] ~= nil then
self.callback = function () self.keyboard:setLayer("Sym") end
self.skiptap = true
elseif self.keyboard.shiftmode_keys[self.label] ~= nil or self.keyboard.shiftmode_keys[self.key] ~= nil then
-- self.key needed because the shift key's label could be the capslock instead of the shift
local key = self.key or self.label
local releasable = key == ""
self.callback = function()
self.keyboard.release_shift = releasable
self.keyboard:setLayer("Shift")
end
self.hold_callback = function()
ignore_key_release = true
if releasable then self.keyboard.release_shift = false end
self.keyboard:setLayer("Shift")
end
self.skiptap = true
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", {})
local next_layout = nil
local layout_index = util.arrayContains(keyboard_layouts, current)
if layout_index then
if layout_index == #keyboard_layouts then
layout_index = 1
else
layout_index = layout_index + 1
end
else
if default and current ~= default then
next_layout = default
else
layout_index = 1
end
end
next_layout = next_layout or keyboard_layouts[layout_index] or "en"
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,
}
else
self.keyboard_layout_dialog = KeyboardLayoutDialog:new{
parent = self,
keyboard_state = keyboard_state,
}
UIManager:show(self.keyboard_layout_dialog)
end
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()
end
end
self.skiptap = true
elseif self.keyboard.umlautmode_keys[self.label] ~= nil then
self.callback = function () self.keyboard:setLayer("Äéß") end
self.skiptap = true
elseif self.label == "" then
self.callback = function () self.keyboard:delChar() end
self.hold_callback = function ()
self.ignore_key_release = true -- don't have delChar called on release
self.keyboard:delToStartOfLine()
end
--self.skiphold = true
elseif self.label == "" then
self.callback = function() self.keyboard:leftChar() end
self.hold_callback = function()
self.ignore_key_release = true
self.keyboard:goToStartOfLine()
end
elseif self.label == "" then
self.callback = function() self.keyboard:rightChar() end
self.hold_callback = function()
self.ignore_key_release = true
self.keyboard:goToEndOfLine()
end
elseif self.label == "" then
self.callback = function() self.keyboard:upLine() end
self.hold_callback = function()
self.ignore_key_release = true
self.keyboard:scrollUp()
end
elseif self.label == "" then
self.callback = function() self.keyboard:downLine() end
self.hold_callback = function()
self.ignore_key_release = true
self.keyboard:scrollDown()
end
else
self.callback = function()
self.keyboard:addChar(self.key)
if self.close_after_callback_widget then
UIManager:close(self.close_after_callback_widget)
end
if self.keyboard.shiftmode and not self.keyboard.symbolmode and self.keyboard.release_shift then
self.keyboard:setLayer("Shift")
end
end
self.hold_callback = function()
if not self.key_chars then return end
VirtualKeyPopup:new{
parent_key = self,
}
end
self.hold_cb_is_popup = true
self.swipe_callback = function(ges)
local key_string = self.key_chars[ges.direction] or self.key
local key_function = self.key_chars[ges.direction.."_func"]
if not key_function and key_string then
if type(key_string) == "table" and key_string.key then
key_string = key_string.key
end
self.keyboard:addChar(key_string)
elseif key_function then
key_function()
end
end
end
local label_widget
if self.icon then
-- Scale icon to fit other characters height
-- (use *1.5 as our icons have a bit of white padding)
local icon_height = math.ceil(self.face.size * 1.5)
label_widget = ImageWidget:new{
file = self.icon,
scale_factor = 0, -- keep icon aspect ratio
height = icon_height,
width = self.width - 2*self.bordersize,
}
else
label_widget = TextWidget:new{
text = self.label,
face = self.face,
bold = self.bold or false,
}
-- Make long labels fit by decreasing font size
local max_width = self.width - 2*self.bordersize - 2*Size.padding.small
while label_widget:getWidth() > max_width do
local new_size = label_widget.face.orig_size - 1
label_widget:free()
if new_size < 8 then break end -- don't go too small
label_widget = TextWidget:new{
text = self.label,
face = Font:getFace(self.face.orig_font, new_size),
bold = self.bold or false,
}
end
end
if self.alt_label then
local OverlapGroup = require("ui/widget/overlapgroup")
local alt_label_widget = TextWidget:new{
text = self.alt_label,
face = Font:getFace(self.face.orig_font, label_font_size - 4),
bold = self.bold or false,
fgcolor = Blitbuffer.COLOR_DARK_GRAY,
padding = 0, -- no additional padding to font line height
}
local key_inner_dimen = Geom:new{
w = self.width - 2*self.bordersize - 2*Size.padding.default,
h = self.height - 2*self.bordersize - 2*Size.padding.small, -- already some padding via line height
}
label_widget = OverlapGroup:new{
CenterContainer:new{
dimen = key_inner_dimen,
label_widget,
},
WidgetContainer:new{
overlap_align = "right",
dimen = Geom:new{
w = alt_label_widget:getSize().w,
h = key_inner_dimen.h,
},
alt_label_widget,
},
}
end
self[1] = FrameContainer:new{
margin = 0,
bordersize = self.bordersize,
background = Blitbuffer.COLOR_WHITE,
radius = 0,
padding = 0,
allow_mirroring = false,
CenterContainer:new{
dimen = Geom:new{
w = self.width - 2*self.bordersize,
h = self.height - 2*self.bordersize,
},
label_widget,
},
}
self.dimen = Geom:new{
w = self.width,
h = self.height,
}
--self.dimen = self[1]:getSize()
self.ges_events = {
TapSelect = {
GestureRange:new{
ges = "tap",
range = self.dimen,
},
},
HoldSelect = {
GestureRange:new{
ges = "hold",
range = self.dimen,
},
},
HoldReleaseKey = {
GestureRange:new{
ges = "hold_release",
range = self.dimen,
},
},
PanReleaseKey = {
GestureRange:new{
ges = "pan_release",
range = self.dimen,
},
},
SwipeKey = {
GestureRange:new{
ges = "swipe",
range = self.dimen,
},
},
}
if ((self.keyboard.shiftmode_keys[self.label] ~= nil or self.keyboard.shiftmode_keys[self.key]) and self.keyboard.shiftmode) or
(self.keyboard.umlautmode_keys[self.label] ~= nil and self.keyboard.umlautmode) or
(self.keyboard.symbolmode_keys[self.label] ~= nil and self.keyboard.symbolmode) then
self[1].background = Blitbuffer.COLOR_LIGHT_GRAY
end
self.flash_keyboard = G_reader_settings:nilOrTrue("flash_keyboard")
end
function VirtualKey:genKeyboardLayoutKeyChars()
local positions = {
"northeast",
"north",
"northwest",
"west",
}
local keyboard_layouts = G_reader_settings:readSetting("keyboard_layouts", {})
local key_chars = {
{ label = "🌐",
},
east = { label = "", },
east_func = function ()
UIManager:close(self.popup)
self.keyboard_layout_dialog = KeyboardLayoutDialog:new{
parent = self,
keyboard_state = keyboard_state,
}
UIManager:show(self.keyboard_layout_dialog)
end,
}
for i = 1, #keyboard_layouts do
key_chars[positions[i]] = string.sub(keyboard_layouts[i], 1, 2)
key_chars[positions[i] .. "_func"] = function()
UIManager:close(self.popup)
self.keyboard:setKeyboardLayout(keyboard_layouts[i])
end
end
return key_chars
end
-- NOTE: We currently don't ever set want_flash to true (c.f., our invert method).
function VirtualKey:update_keyboard(want_flash, want_a2)
-- NOTE: We use "a2" for the highlights.
-- We flash the *full* keyboard when we release a hold.
if want_flash then
UIManager:setDirty(self.keyboard, function()
return "flashui", self.keyboard[1][1].dimen -- i.e., keyboard_frame
end)
else
local refresh_type = "ui"
if want_a2 then
refresh_type = "a2"
end
-- Only repaint the key itself, not the full board...
UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
logger.dbg("update key", self.key)
UIManager:setDirty(nil, refresh_type, self[1].dimen)
-- NOTE: On MTK, we'd have to forcibly stall a bit for the highlights to actually show.
--[[
UIManager:forceRePaint()
UIManager:yieldToEPDC(3000)
--]]
end
end
function VirtualKey:onFocus()
self[1].inner_bordersize = self.focused_bordersize
end
function VirtualKey:onUnfocus()
self[1].inner_bordersize = 0
end
function VirtualKey:onTapSelect(skip_flash)
Device:performHapticFeedback("KEYBOARD_TAP")
-- just in case it's not flipped to false on hold release where it's supposed to
self.keyboard.ignore_first_hold_release = false
if self.flash_keyboard and not skip_flash and not self.skiptap then
self:invert(true)
UIManager:forceRePaint()
UIManager:yieldToEPDC()
self:invert(false)
if self.callback then
self.callback()
end
UIManager:forceRePaint()
else
if self.callback then
self.callback()
end
end
return true
end
function VirtualKey:onHoldSelect()
Device:performHapticFeedback("LONG_PRESS")
-- No visual feedback necessary if we're going to show a popup on top of the key ;).
if self.flash_keyboard and not self.skiphold and not self.hold_cb_is_popup then
self:invert(true)
UIManager:forceRePaint()
UIManager:yieldToEPDC()
-- NOTE: We do *NOT* set hold to true here,
-- because some mxcfb drivers apparently like to merge the flash that it would request
-- with the following key redraw, leading to an unsightly double flash :/.
self:invert(false)
if self.hold_callback then
self.hold_callback()
end
UIManager:forceRePaint()
else
if self.hold_callback then
self.hold_callback()
end
end
return true
end
function VirtualKey:onSwipeKey(arg, ges)
Device:performHapticFeedback("KEYBOARD_TAP")
if self.flash_keyboard and not self.skipswipe then
self:invert(true)
UIManager:forceRePaint()
UIManager:yieldToEPDC()
self:invert(false)
if self.swipe_callback then
self.swipe_callback(ges)
end
UIManager:forceRePaint()
else
if self.swipe_callback then
self.swipe_callback(ges)
end
end
return true
end
function VirtualKey:onHoldReleaseKey()
if ignore_key_release then
ignore_key_release = nil
return true
end
if self.ignore_key_release then
self.ignore_key_release = nil
return true
end
Device:performHapticFeedback("LONG_PRESS")
if self.keyboard.ignore_first_hold_release then
self.keyboard.ignore_first_hold_release = false
return true
end
self:onTapSelect()
return true
end
function VirtualKey:onPanReleaseKey()
if self.ignore_key_release then
self.ignore_key_release = nil
return true
end
Device:performHapticFeedback("LONG_PRESS")
if self.keyboard.ignore_first_hold_release then
self.keyboard.ignore_first_hold_release = false
return true
end
self:onTapSelect()
return true
end
-- NOTE: We currently don't ever set hold to true (c.f., our onHoldSelect method)
function VirtualKey:invert(invert, hold)
if invert then
self[1].inner_bordersize = self.focused_bordersize
else
self[1].inner_bordersize = 0
end
self:update_keyboard(hold, true)
end
VirtualKeyPopup = FocusManager:extend{
modal = true,
disable_double_tap = true,
inputbox = nil,
layout = nil, -- array
}
function VirtualKeyPopup:onTapClose(arg, ges)
if ges.pos:notIntersectWith(self[1][1].dimen) then
UIManager:close(self)
return true
end
return false
end
function VirtualKeyPopup:onClose()
UIManager:close(self)
return true
end
function VirtualKeyPopup:onCloseWidget()
self:free()
UIManager:setDirty(nil, function()
return "ui", self[1][1].dimen -- i.e., keyboard_frame
end)
end
function VirtualKeyPopup:init()
local parent_key = self.parent_key
local key_chars = parent_key.key_chars
local key_char_orig = key_chars[1]
local key_char_orig_func = parent_key.callback
self.layout = {}
local rows = {
extra_key_chars = {
key_chars[2],
key_chars[3],
key_chars[4],
-- _func equivalent for unnamed extra keys
key_chars[5],
key_chars[6],
key_chars[7],
},
top_key_chars = {
key_chars.northwest,
key_chars.north,
key_chars.northeast,
key_chars.northwest_func,
key_chars.north_func,
key_chars.northeast_func,
},
middle_key_chars = {
key_chars.west,
key_char_orig,
key_chars.east,
key_chars.west_func,
key_char_orig_func,
key_chars.east_func,
},
bottom_key_chars = {
key_chars.southwest,
key_chars.south,
key_chars.southeast,
key_chars.southwest_func,
key_chars.south_func,
key_chars.southeast_func,
},
}
if util.tableSize(rows.extra_key_chars) == 0 then rows.extra_key_chars = nil end
if util.tableSize(rows.top_key_chars) == 0 then rows.top_key_chars = nil end
-- we should always have a middle
if util.tableSize(rows.bottom_key_chars) == 0 then rows.bottom_key_chars = nil end
-- to store if a column exists
local columns = {}
local blank = {
HorizontalSpan:new{width = 0},
HorizontalSpan:new{width = parent_key.width},
HorizontalSpan:new{width = 0},
}
local h_key_padding = {
HorizontalSpan:new{width = 0},
HorizontalSpan:new{width = parent_key.keyboard.key_padding},
HorizontalSpan:new{width = 0},
}
local v_key_padding = VerticalSpan:new{width = parent_key.keyboard.key_padding}
local vertical_group = VerticalGroup:new{ allow_mirroring = false }
local horizontal_group_extra = HorizontalGroup:new{ allow_mirroring = false }
local horizontal_group_top = HorizontalGroup:new{ allow_mirroring = false }
local horizontal_group_middle = HorizontalGroup:new{ allow_mirroring = false }
local horizontal_group_bottom = HorizontalGroup:new{ allow_mirroring = false }
local function horizontalRow(chars, group)
local layout_horizontal = {}
for i = 1,3 do
local v = chars[i]
local v_func = chars[i+3]
if v then
columns[i] = true
blank[i].width = blank[2].width
if i == 1 then
h_key_padding[i].width = h_key_padding[2].width
end
local key = type(v) == "table" and v.key or v
local label = type(v) == "table" and v.label or key
local icon = type(v) == "table" and v.icon
local bold = type(v) == "table" and v.bold
local virtual_key = VirtualKey:new{
key = key,
label = label,
icon = icon,
bold = bold,
keyboard = parent_key.keyboard,
key_chars = key_chars,
width = parent_key.width,
height = parent_key.height,
close_after_callback_widget = self,
}
-- Support any function as a callback.
if v_func then
virtual_key.callback = v_func
end
-- don't open another popup on hold
virtual_key.hold_callback = nil
-- close popup on hold release
virtual_key.onHoldReleaseKey = function()
-- NOTE: Check our *parent* key!
if parent_key.ignore_key_release then
parent_key.ignore_key_release = nil
return true
end
Device:performHapticFeedback("LONG_PRESS")
if virtual_key.keyboard.ignore_first_hold_release then
virtual_key.keyboard.ignore_first_hold_release = false
return true
end
virtual_key:onTapSelect(true)
UIManager:close(self)
return true
end
virtual_key.onPanReleaseKey = virtual_key.onHoldReleaseKey
if v == key_char_orig then
virtual_key[1].background = Blitbuffer.COLOR_LIGHT_GRAY
-- restore ability to hold/pan release on central key after opening popup
virtual_key._keyOrigHoldPanHandler = function()
virtual_key.onHoldReleaseKey = virtual_key._onHoldReleaseKey
virtual_key.onPanReleaseKey = virtual_key._onPanReleaseKey
end
virtual_key._onHoldReleaseKey = virtual_key.onHoldReleaseKey
virtual_key.onHoldReleaseKey = virtual_key._keyOrigHoldPanHandler
virtual_key._onPanReleaseKey = virtual_key.onPanReleaseKey
virtual_key.onPanReleaseKey = virtual_key._keyOrigHoldPanHandler
end
table.insert(group, virtual_key)
table.insert(layout_horizontal, virtual_key)
else
table.insert(group, blank[i])
end
table.insert(group, h_key_padding[i])
end
table.insert(vertical_group, group)
table.insert(self.layout, layout_horizontal)
end
if rows.extra_key_chars then
horizontalRow(rows.extra_key_chars, horizontal_group_extra)
table.insert(vertical_group, v_key_padding)
end
if rows.top_key_chars then
horizontalRow(rows.top_key_chars, horizontal_group_top)
table.insert(vertical_group, v_key_padding)
end
-- always middle row
horizontalRow(rows.middle_key_chars, horizontal_group_middle)
if rows.bottom_key_chars then
table.insert(vertical_group, v_key_padding)
horizontalRow(rows.bottom_key_chars, horizontal_group_bottom)
end
if not columns[3] then
h_key_padding[2].width = 0
end
local num_rows = util.tableSize(rows)
local num_columns = util.tableSize(columns)
local keyboard_frame = FrameContainer:new{
margin = 0,
bordersize = Size.border.default,
background = G_reader_settings:nilOrTrue("keyboard_key_border") and Blitbuffer.COLOR_LIGHT_GRAY or Blitbuffer.COLOR_WHITE,
radius = 0,
padding = parent_key.keyboard.padding,
allow_mirroring = false,
CenterContainer:new{
dimen = Geom:new{
w = parent_key.width*num_columns + 2*Size.border.default + (num_columns+1)*parent_key.keyboard.key_padding,
h = parent_key.height*num_rows + 2*Size.border.default + (num_rows+1)*parent_key.keyboard.key_padding,
},
vertical_group,
}
}
keyboard_frame.dimen = keyboard_frame:getSize()
self.ges_events.TapClose = {
GestureRange:new{
ges = "tap",
}
}
self.tap_interval_override = time.ms(G_reader_settings:readSetting("ges_tap_interval_on_keyboard_ms", 0))
if Device:hasKeys() then
self.key_events.Close = { { Device.input.group.Back } }
end
local offset_x = 2*keyboard_frame.bordersize + keyboard_frame.padding + parent_key.keyboard.key_padding
if columns[1] then
offset_x = offset_x + parent_key.width + parent_key.keyboard.key_padding
end
local offset_y = 2*keyboard_frame.bordersize + keyboard_frame.padding + parent_key.keyboard.key_padding
if rows.extra_key_chars then
offset_y = offset_y + parent_key.height + parent_key.keyboard.key_padding
end
if rows.top_key_chars then
offset_y = offset_y + parent_key.height + parent_key.keyboard.key_padding
end
local position_container = WidgetContainer:new{
dimen = {
x = parent_key.dimen.x - offset_x,
y = parent_key.dimen.y - offset_y,
h = Screen:getSize().h,
w = Screen:getSize().w,
},
keyboard_frame,
}
if position_container.dimen.x < 0 then
position_container.dimen.x = 0
-- We effectively move the popup, which means the key underneath our finger may no longer *exactly* be parent_key.
-- Make sure we won't close the popup right away, as that would risk being a *different* key, in order to make that less confusing.
parent_key.ignore_key_release = true
elseif position_container.dimen.x + keyboard_frame.dimen.w > Screen:getWidth() then
position_container.dimen.x = Screen:getWidth() - keyboard_frame.dimen.w
parent_key.ignore_key_release = true
end
if position_container.dimen.y < 0 then
position_container.dimen.y = 0
parent_key.ignore_key_release = true
elseif position_container.dimen.y + keyboard_frame.dimen.h > Screen:getHeight() then
position_container.dimen.y = Screen:getHeight() - keyboard_frame.dimen.h
parent_key.ignore_key_release = true
end
self[1] = position_container
UIManager:show(self)
-- Ensure the post-paint refresh will be able to grab updated coordinates from keyboard_frame by using a refresh function
UIManager:setDirty(self, function()
return "ui", keyboard_frame.dimen
end)
end
local VirtualKeyboard = FocusManager:extend{
name = "VirtualKeyboard",
visible = nil,
lock_visibility = false,
covers_footer = true,
modal = true,
disable_double_tap = true,
inputbox = nil,
KEYS = nil, -- table to store layouts
shiftmode_keys = nil, -- table
symbolmode_keys = nil, -- table
utf8mode_keys = nil, -- table
umlautmode_keys = nil, -- table
keyboard_layer = 2,
shiftmode = false,
symbolmode = false,
umlautmode = false,
layout = nil, -- array
height = nil,
default_label_size = DEFAULT_LABEL_SIZE,
bordersize = Size.border.default,
padding = 0,
key_padding = Size.padding.small,
lang_to_keyboard_layout = {
ar = "ar_keyboard",
bg_BG = "bg_keyboard",
bn = "bn_keyboard",
de = "de_keyboard",
el = "el_keyboard",
en = "en_keyboard",
es = "es_keyboard",
fa = "fa_keyboard",
fr = "fr_keyboard",
he = "he_keyboard",
ja = "ja_keyboard",
ka = "ka_keyboard",
ko_KR = "ko_KR_keyboard",
pl = "pl_keyboard",
pt_BR = "pt_keyboard",
ro = "ro_keyboard",
ru = "ru_keyboard",
sk = "sk_keyboard",
th = "th_keyboard",
tr = "tr_keyboard",
uk = "uk_keyboard",
vi = "vi_keyboard",
zh = "zh_keyboard",
zh_CN = "zh_CN_keyboard",
},
lang_has_submenu = {
ja = true,
zh = true,
zh_CN = true,
},
}
function VirtualKeyboard:init()
if self.uwrap_func then
self.uwrap_func()
self.uwrap_func = nil
end
local lang = self:getKeyboardLayout()
local keyboard_layout = self.lang_to_keyboard_layout[lang] or self.lang_to_keyboard_layout["en"]
local keyboard = require("ui/data/keyboardlayouts/" .. keyboard_layout)
self.KEYS = keyboard.keys or {}
self.shiftmode_keys = keyboard.shiftmode_keys or {}
self.symbolmode_keys = keyboard.symbolmode_keys or {}
self.utf8mode_keys = keyboard.utf8mode_keys or {}
self.umlautmode_keys = keyboard.umlautmode_keys or {}
self.width = Screen:getWidth()
local keys_height = G_reader_settings:isTrue("keyboard_key_compact") and 48 or 64
self.height = Screen:scaleBySize(keys_height * #self.KEYS)
self.min_layer = keyboard.min_layer
self.max_layer = keyboard.max_layer
self:initLayer(self.keyboard_layer)
self.tap_interval_override = time.ms(G_reader_settings:readSetting("ges_tap_interval_on_keyboard_ms", 0))
if Device:hasKeys() then
self.key_events.Close = { { "Back" } }
end
if keyboard.wrapInputBox then
self.uwrap_func = keyboard.wrapInputBox(self.inputbox) or self.uwrap_func
end
if Device:hasDPad() then
-- hadDPad() would have FocusManager handle arrow keys strokes to navigate
-- and activate this VirtualKeyboard's touch keys (needed on non-touch Kindle).
-- If we have a keyboard, we'd prefer arrow keys (and Enter, and Del) to be
-- handled by InputText to navigate the cursor inside the text box, and to
-- add newline and delete chars. And if we are a touch device, we don't
-- need focus manager to help us navigate keys and fields.
-- So, disable all key_event handled by FocusManager
if Device:isTouchDevice() then
-- Remove all FocusManager key event handlers.
for k, _ in pairs(self.builtin_key_events) do
self.key_events[k] = nil
end
for k, _ in pairs(self.extra_key_events) do
self.key_events[k] = nil
end
elseif Device:hasKeyboard() then
-- Use physical keyboard for most characters
-- For special characters not available in physical keyboard
-- Use arrow and Press keys to select in VirtualKeyboard
for k, seq in pairs(self.extra_key_events) do
if self:_isTextKeyWithoutModifier(seq) then
self.key_events[k] = nil
end
end
end
end
end
function VirtualKeyboard:_isTextKeyWithoutModifier(seq)
for _, oneseq in ipairs(seq) do
if #oneseq ~= 1 then -- has modifier key combination
return false
end
if #oneseq[1] ~= 1 then -- not simple text key, like Home, End
return false
end
end
return true
end
function VirtualKeyboard:getKeyboardLayout()
if G_reader_settings:isFalse("keyboard_remember_layout") and not keyboard_state.force_current_layout then
local lang = G_reader_settings:readSetting("keyboard_layout_default")
or G_reader_settings:readSetting("keyboard_layout", "en")
G_reader_settings:saveSetting("keyboard_layout", lang)
end
return G_reader_settings:readSetting("keyboard_layout") or G_reader_settings:readSetting("language")
end
function VirtualKeyboard:setKeyboardLayout(layout)
keyboard_state.force_current_layout = true
local prev_keyboard_height = self.dimen and self.dimen.h
G_reader_settings:saveSetting("keyboard_layout", layout)
self:init()
if prev_keyboard_height and self.dimen.h ~= prev_keyboard_height then
self:_refresh(true, true)
-- Keyboard height change: notify parent (InputDialog)
if self.inputbox and self.inputbox.parent and self.inputbox.parent.onKeyboardHeightChanged then
self.inputbox.parent:onKeyboardHeightChanged()
end
else
self:_refresh(true)
end
keyboard_state.force_current_layout = false
end
function VirtualKeyboard:onClose()
UIManager:close(self)
if self.inputbox and Device:hasDPad() then
-- let input text handle Back event to unfocus
-- otherwise, another extra Back event needed
return false
end
return true
end
function VirtualKeyboard:_refresh(want_flash, fullscreen)
local refresh_type = "ui"
if want_flash then
refresh_type = "flashui"
end
if fullscreen then
UIManager:setDirty("all", refresh_type)
return
end
UIManager:setDirty(self, function()
return refresh_type, self[1][1].dimen -- i.e., keyboard_frame
end)
end
function VirtualKeyboard:onShow()
self:_refresh(true)
self.visible = true
Device:startTextInput()
return true
end
function VirtualKeyboard:onCloseWidget()
self:_refresh(true)
self.visible = false
-- NOTE: This effectively stops SDL text input when a keyboard is hidden (... but navigational stuff still works).
-- If you instead wanted it to be enabled as long as an input dialog is displayed, regardless of VK's state,
-- this could be moved to InputDialog's onShow/onCloseWidget handlers (but, it would allow input on unfocused fields).
-- NOTE: But something more complex, possibly based on an in-class ref count would have to be implemented in order to be able to deal
-- with multiple InputDialogs being shown and closed in asymmetric fashion... Ugh.
Device:stopTextInput()
end
function VirtualKeyboard:lockVisibility(toggle)
self.lock_visibility = toggle
end
function VirtualKeyboard:setVisibility(toggle)
if self.lock_visibility then
return
end
if toggle then
UIManager:show(self)
else
self:onClose()
end
end
function VirtualKeyboard:isVisible()
return self.visible
end
function VirtualKeyboard:showKeyboard(ignore_first_hold_release)
if not self:isVisible() then
self.ignore_first_hold_release = ignore_first_hold_release
self:setVisibility(true)
end
end
function VirtualKeyboard:hideKeyboard()
if self:isVisible() then
self:setVisibility(false)
end
end
function VirtualKeyboard:initLayer(layer)
local function VKLayer(b1, b2, b3)
local function boolnum(bool)
return bool and 1 or 0
end
return 2 - boolnum(b1) + 2 * boolnum(b2) + 4 * boolnum(b3)
end
if layer then
-- to be sure layer is selected properly
layer = math.max(layer, self.min_layer)
layer = math.min(layer, self.max_layer)
self.keyboard_layer = layer
-- fill the layer modes
self.shiftmode = (layer == 1 or layer == 3 or layer == 5 or layer == 7 or layer == 9 or layer == 11)
self.symbolmode = (layer == 3 or layer == 4 or layer == 7 or layer == 8 or layer == 11 or layer == 12)
self.umlautmode = (layer == 5 or layer == 6 or layer == 7 or layer == 8)
else -- or, without input parameter, restore layer from current layer modes
self.keyboard_layer = VKLayer(self.shiftmode, self.symbolmode, self.umlautmode)
end
self:addKeys()
end
function VirtualKeyboard:addKeys()
self:free() -- free previous keys' TextWidgets
self.layout = {}
local base_key_width = math.floor((self.width - (#self.KEYS[1] + 1)*self.key_padding - 2*self.padding)/#self.KEYS[1])
local base_key_height = math.floor((self.height - (#self.KEYS + 1)*self.key_padding - 2*self.padding)/#self.KEYS)
local h_key_padding = HorizontalSpan:new{width = self.key_padding}
local v_key_padding = VerticalSpan:new{width = self.key_padding}
local vertical_group = VerticalGroup:new{ allow_mirroring = false }
for i = 1, #self.KEYS do
local horizontal_group = HorizontalGroup:new{ allow_mirroring = false }
local layout_horizontal = {}
for j = 1, #self.KEYS[i] do
local key
local key_chars = self.KEYS[i][j][self.keyboard_layer]
local label
local alt_label
local width_factor
if type(key_chars) == "table" then
key = key_chars[1]
label = key_chars.label
alt_label = key_chars.alt_label
width_factor = key_chars.width
else
key = key_chars
key_chars = nil
end
width_factor = width_factor or self.KEYS[i][j].width or self.KEYS[i].width or 1.0
local key_width = math.floor((base_key_width + self.key_padding) * width_factor)
- self.key_padding
local key_height = base_key_height
label = label or self.KEYS[i][j].label or key
if label == "" and self.shiftmode and (not self.release_shift or self.symbolmode) then
key = label
label = "" -- capslock symbol
end
local virtual_key = VirtualKey:new{
key = key,
key_chars = key_chars,
icon = self.KEYS[i][j].icon,
label = label,
alt_label = alt_label,
bold = self.KEYS[i][j].bold,
keyboard = self,
width = key_width,
height = key_height,
}
if not virtual_key.key_chars then
virtual_key.swipe_callback = nil
end
table.insert(horizontal_group, virtual_key)
table.insert(layout_horizontal, virtual_key)
if j ~= #self.KEYS[i] then
table.insert(horizontal_group, h_key_padding)
end
end
table.insert(vertical_group, horizontal_group)
table.insert(self.layout, layout_horizontal)
if i ~= #self.KEYS then
table.insert(vertical_group, v_key_padding)
end
end
local keyboard_frame = FrameContainer:new{
margin = 0,
bordersize = Size.border.default,
background = G_reader_settings:nilOrTrue("keyboard_key_border") and Blitbuffer.COLOR_LIGHT_GRAY or Blitbuffer.COLOR_WHITE,
radius = 0,
padding = self.padding,
allow_mirroring = false,
CenterContainer:new{
dimen = Geom:new{
w = self.width - 2*Size.border.default - 2*self.padding,
h = self.height - 2*Size.border.default - 2*self.padding,
},
vertical_group,
}
}
self[1] = BottomContainer:new{
dimen = Screen:getSize(),
keyboard_frame,
}
-- Beware, this won't be updated post-paint, so the coordinates will stay at (0, 0)
-- (i.e., only the size is accurate, not the position).
self.dimen = keyboard_frame:getSize()
end
function VirtualKeyboard:setLayer(key)
if key == "Shift" then
self.shiftmode = not self.shiftmode
elseif key == "Sym" or key == "ABC" then
self.symbolmode = not self.symbolmode
elseif key == "Äéß" then
self.umlautmode = not self.umlautmode
end
self:initLayer()
self:_refresh(false)
end
function VirtualKeyboard:addChar(key)
logger.dbg("add char", key)
self.inputbox:addChars(key)
end
function VirtualKeyboard:delChar()
logger.dbg("delete char")
self.inputbox:delChar()
end
function VirtualKeyboard:delToStartOfLine()
logger.dbg("delete to start of line")
self.inputbox:delToStartOfLine()
end
function VirtualKeyboard:leftChar()
self.inputbox:leftChar()
end
function VirtualKeyboard:rightChar()
self.inputbox:rightChar()
end
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
function VirtualKeyboard:upLine()
self.inputbox:upLine()
end
function VirtualKeyboard:scrollUp()
self.inputbox:scrollUp()
end
function VirtualKeyboard:downLine()
self.inputbox:downLine()
end
function VirtualKeyboard:scrollDown()
self.inputbox:scrollDown()
end
function VirtualKeyboard:clear()
logger.dbg("clear input")
self.inputbox:clear()
end
return VirtualKeyboard