You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/frontend/ui/widget/virtualkeyboard.lua

959 lines
32 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 Event = require("ui/event")
local FFIUtil = require("ffi/util")
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")
The great Input/GestureDetector/TimeVal spring cleanup (a.k.a., a saner main loop) (#7415) * ReaderDictionary: Port delay computations to TimeVal * ReaderHighlight: Port delay computations to TimeVal * ReaderView: Port delay computations to TimeVal * Android: Reset gesture detection state on APP_CMD_TERM_WINDOW. This prevents potentially being stuck in bogus gesture states when switching apps. * GestureDetector: * Port delay computations to TimeVal * Fixed delay computations to handle time warps (large and negative deltas). * Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture. * Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1. * Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy. * The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events. The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use a clock source that is *NOT* MONOTONIC. AFAICT, that's pretty much... PocketBook, and that's it? * Input: * Use the <linux/input.h> FFI module instead of re-declaring every constant * Fixed (verbose) debug logging of input events to actually translate said constants properly. * Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume. * Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient. Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts, as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector. * reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary. * TimeVal: * Refactored and simplified, especially as far as metamethods are concerned (based on <bsd/sys/time.h>). * Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC. * Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence. * New methods: * Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime * Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero * UIManager: * Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base. This ensures reliable and consistent scheduling, as time is ensured never to go backwards. * Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame. It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value, because very few time has passed. The only code left that does live syscalls does it because it's actually necessary for accuracy, and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics). * DictQuickLookup: Port delay computations to TimeVal * FootNoteWidget: Port delay computations to TimeVal * HTMLBoxWidget: Port delay computations to TimeVal * Notification: Port delay computations to TimeVal * TextBoxWidget: Port delay computations to TimeVal * AutoSuspend: Port to TimeVal * AutoTurn: * Fix it so that settings are actually honored. * Port to TimeVal * BackgroundRunner: Port to TimeVal * Calibre: Port benchmarking code to TimeVal * BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly. * All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries..
3 years ago
local TimeVal = require("ui/timeval")
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 util = require("util")
local Screen = Device.screen
local VirtualKeyPopup
local VirtualKey = InputContainer:new{
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 = Size.border.thin,
focused_bordersize = Size.border.default * 5,
radius = 0,
face = Font:getFace("infont"),
}
function VirtualKey:init()
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 then
self.callback = function () 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 ()
local current = G_reader_settings:readSetting("keyboard_layout")
local keyboard_layouts = G_reader_settings:readSetting("keyboard_layouts") or {}
local enabled = false
local next_layout = nil
for k, v in FFIUtil.orderedPairs(keyboard_layouts) do
if enabled and v == true then
next_layout = k
break
end
if k == current then
enabled = true
end
end
if not next_layout then
for k, v in FFIUtil.orderedPairs(keyboard_layouts) do
if enabled and v == true then
next_layout = k
break
end
end
end
if next_layout then
self.keyboard:setKeyboardLayout(next_layout)
end
end
self.hold_callback = function()
if util.tableSize(self.key_chars) > 3 then
self.popup = VirtualKeyPopup:new{
parent_key = self,
}
else
self.keyboard_layout_dialog = KeyboardLayoutDialog:new{
parent = self,
}
UIManager:show(self.keyboard_layout_dialog)
end
end
self.swipe_callback = function(ges)
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
elseif self.label == "" then
self.callback = function() self.keyboard:downLine() end
self.hold_callback = function()
self.ignore_key_release = true
if not self.keyboard:onHideKeyboard() then
-- Keyboard was *not* actually hidden: refresh the key to clear the highlight
self:update_keyboard(false, true)
end
end
else
self.callback = function () self.keyboard:addChar(self.key) end
self.hold_callback = function()
if not self.key_chars then return end
VirtualKeyPopup:new{
parent_key = self,
}
end
self.swipe_callback = function(ges)
local key_string = self.key_chars[ges.direction]
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,
}
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()
if Device:isTouchDevice() then
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,
},
},
}
end
if (self.keyboard.shiftmode_keys[self.label] ~= nil and self.keyboard.shiftmode) or
(self.keyboard.umlautmode_keys[self.label] ~= nil and self.keyboard.umlautmode) 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") or {}
local key_chars = {
{ label = "🌐",
},
east = { label = "🌐", },
east_func = function ()
self.keyboard_layout_dialog = KeyboardLayoutDialog:new{
parent = self,
}
UIManager:show(self.keyboard_layout_dialog)
end,
}
local index = 1
for k, v in FFIUtil.orderedPairs(keyboard_layouts) do
if v == true then
key_chars[positions[index]] = string.sub(k, 1, 2)
key_chars[positions[index] .. "_func"] = function()
UIManager:tickAfterNext(function() UIManager:close(self.popup) end)
self.keyboard:setKeyboardLayout(k)
end
if index >= 4 then break end
index = index + 1
end
end
return key_chars
end
function VirtualKey:update_keyboard(want_flash, want_fast)
-- NOTE: We mainly use "fast" when inverted & "ui" when not, with a cherry on top:
-- we flash the *full* keyboard instead when we release a hold.
if want_flash then
UIManager:setDirty(self.keyboard, function()
return "flashui", self.keyboard[1][1].dimen
end)
else
local refresh_type = "ui"
if want_fast then
refresh_type = "fast"
end
Enable HW dithering in a few key places (#4541) * Enable HW dithering on supported devices (Clara HD, Forma; Oasis 2, PW4) * FileManager and co. (where appropriate, i.e., when covers are shown) * Book Status * Reader, where appropriate: * CRe: on pages whith image content (for over 7.5% of the screen area, should hopefully leave stuff like bullet points or small scene breaks alone). * Other engines: on user-request (in the gear tab of the bottom menu), via the new "Dithering" knob (will only appear on supported devices). * ScreenSaver * ImageViewer * Minimize repaints when flash_ui is enabled (by, almost everywhere, only repainting the flashing element, and not the toplevel window which hosts it). (The first pass of this involved fixing a few Button instances whose show_parent was wrong, in particular, chevrons in the FM & TopMenu). * Hunted down a few redundant repaints (unneeded setDirty("all") calls), either by switching the widget to nil when only a refresh was needed, and not a repaint, or by passing the appropritate widget to setDirty. (Note to self: Enable *verbose* debugging to catch broken setDirty calls via its post guard). There were also a few instances of 'em right behind a widget close. * Don't repaint the underlying widget when initially showing TopMenu & ConfigDialog. We unfortunately do need to do it when switching tabs, because of their variable heights. * On Kobo, disabled the extra and completely useless full refresh before suspend/reboot/poweroff, as well as on resume. No more double refreshes! * Fix another debug guard in Kobo sysfs_light * Switch ImageWidget & ImageViewer mostly to "ui" updates, which will be better suited to image content pretty much everywhere, REAGL or not. PS: (Almost :100: commits! :D)
5 years ago
-- Only repaint the key itself, not the full board...
UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y)
UIManager:setDirty(nil, function()
logger.dbg("update key region", self[1].dimen)
return refresh_type, self[1].dimen
end)
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[1].inner_bordersize = self.focused_bordersize
self:update_keyboard(false, true)
if self.callback then
self.callback()
end
UIManager:tickAfterNext(function() self:invert(false) end)
else
if self.callback then
self.callback()
end
end
return true
end
function VirtualKey:onHoldSelect()
Device:performHapticFeedback("LONG_PRESS")
if self.flash_keyboard and not self.skiphold then
self[1].inner_bordersize = self.focused_bordersize
self:update_keyboard(false, true)
-- Don't refresh the key region if we're going to show a popup on top of it ;).
if self.hold_callback then
self[1].inner_bordersize = 0
self.hold_callback()
else
UIManager:tickAfterNext(function() self:invert(false, true) end)
end
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[1].inner_bordersize = self.focused_bordersize
self:update_keyboard(false, true)
if self.swipe_callback then
self.swipe_callback(ges)
end
UIManager:tickAfterNext(function() self:invert(false, false) end)
else
if self.swipe_callback then
self.swipe_callback(ges)
end
end
return true
end
function VirtualKey:onHoldReleaseKey()
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
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, false)
end
VirtualKeyPopup = FocusManager:new{
modal = true,
disable_double_tap = true,
inputbox = nil,
layout = {},
}
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
end)
end
function VirtualKeyPopup:onPressKey()
self:getFocusItem():handleEvent(Event:new("TapSelect"))
return true
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
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,
}
-- 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 = 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)*parent_key.keyboard.key_padding,
h = parent_key.height*num_rows + 2*Size.border.default + num_rows*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 = G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0
The great Input/GestureDetector/TimeVal spring cleanup (a.k.a., a saner main loop) (#7415) * ReaderDictionary: Port delay computations to TimeVal * ReaderHighlight: Port delay computations to TimeVal * ReaderView: Port delay computations to TimeVal * Android: Reset gesture detection state on APP_CMD_TERM_WINDOW. This prevents potentially being stuck in bogus gesture states when switching apps. * GestureDetector: * Port delay computations to TimeVal * Fixed delay computations to handle time warps (large and negative deltas). * Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture. * Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1. * Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy. * The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events. The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use a clock source that is *NOT* MONOTONIC. AFAICT, that's pretty much... PocketBook, and that's it? * Input: * Use the <linux/input.h> FFI module instead of re-declaring every constant * Fixed (verbose) debug logging of input events to actually translate said constants properly. * Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume. * Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient. Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts, as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector. * reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary. * TimeVal: * Refactored and simplified, especially as far as metamethods are concerned (based on <bsd/sys/time.h>). * Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC. * Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence. * New methods: * Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime * Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero * UIManager: * Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base. This ensures reliable and consistent scheduling, as time is ensured never to go backwards. * Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame. It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value, because very few time has passed. The only code left that does live syscalls does it because it's actually necessary for accuracy, and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics). * DictQuickLookup: Port delay computations to TimeVal * FootNoteWidget: Port delay computations to TimeVal * HTMLBoxWidget: Port delay computations to TimeVal * Notification: Port delay computations to TimeVal * TextBoxWidget: Port delay computations to TimeVal * AutoSuspend: Port to TimeVal * AutoTurn: * Fix it so that settings are actually honored. * Port to TimeVal * BackgroundRunner: Port to TimeVal * Calibre: Port benchmarking code to TimeVal * BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly. * All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries..
3 years ago
self.tap_interval_override = TimeVal:new{ usec = self.tap_interval_override }
if Device:hasDPad() then
self.key_events.PressKey = { {"Press"}, doc = "select key" }
end
if Device:hasKeys() then
self.key_events.Close = { {"Back"}, doc = "close keyboard" }
end
local offset_x = 2*parent_key.keyboard.padding + 2*parent_key.keyboard.bordersize
if columns[1] then
offset_x = offset_x + parent_key.width + parent_key.keyboard.padding + 2*parent_key.keyboard.bordersize
end
local offset_y = parent_key.keyboard.padding + parent_key.keyboard.padding + 2*parent_key.keyboard.bordersize
if rows.extra_key_chars then
offset_y = offset_y + parent_key.height + parent_key.keyboard.padding + 2*parent_key.keyboard.bordersize
end
if rows.top_key_chars then
offset_y = offset_y + parent_key.height + parent_key.keyboard.padding + 2*parent_key.keyboard.bordersize
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)
UIManager:setDirty(self, function()
return "ui", keyboard_frame.dimen
end)
end
local VirtualKeyboard = FocusManager:new{
name = "VirtualKeyboard",
modal = true,
disable_double_tap = true,
inputbox = nil,
KEYS = {}, -- table to store layouts
shiftmode_keys = {},
symbolmode_keys = {},
utf8mode_keys = {},
umlautmode_keys = {},
keyboard_layer = 2,
shiftmode = false,
symbolmode = false,
umlautmode = false,
layout = {},
width = Screen:scaleBySize(600),
height = nil,
bordersize = Size.border.default,
padding = Size.padding.small,
key_padding = Size.padding.default,
lang_to_keyboard_layout = {
ar_AA = "ar_AA_keyboard",
bg_BG = "bg_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",
pl = "pl_keyboard",
pt_BR = "pt_keyboard",
ro = "ro_keyboard",
ko_KR = "ko_KR_keyboard",
ru = "ru_keyboard",
tr = "tr_keyboard",
},
}
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
self.shiftmode_keys = keyboard.shiftmode_keys
self.symbolmode_keys = keyboard.symbolmode_keys
self.utf8mode_keys = keyboard.utf8mode_keys
self.umlautmode_keys = keyboard.umlautmode_keys
self.height = Screen:scaleBySize(64 * #self.KEYS)
self.min_layer = keyboard.min_layer
self.max_layer = keyboard.max_layer
self:initLayer(self.keyboard_layer)
self.tap_interval_override = G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0
The great Input/GestureDetector/TimeVal spring cleanup (a.k.a., a saner main loop) (#7415) * ReaderDictionary: Port delay computations to TimeVal * ReaderHighlight: Port delay computations to TimeVal * ReaderView: Port delay computations to TimeVal * Android: Reset gesture detection state on APP_CMD_TERM_WINDOW. This prevents potentially being stuck in bogus gesture states when switching apps. * GestureDetector: * Port delay computations to TimeVal * Fixed delay computations to handle time warps (large and negative deltas). * Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture. * Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1. * Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy. * The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events. The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use a clock source that is *NOT* MONOTONIC. AFAICT, that's pretty much... PocketBook, and that's it? * Input: * Use the <linux/input.h> FFI module instead of re-declaring every constant * Fixed (verbose) debug logging of input events to actually translate said constants properly. * Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume. * Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient. Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts, as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector. * reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary. * TimeVal: * Refactored and simplified, especially as far as metamethods are concerned (based on <bsd/sys/time.h>). * Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC. * Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence. * New methods: * Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime * Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero * UIManager: * Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base. This ensures reliable and consistent scheduling, as time is ensured never to go backwards. * Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame. It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value, because very few time has passed. The only code left that does live syscalls does it because it's actually necessary for accuracy, and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics). * DictQuickLookup: Port delay computations to TimeVal * FootNoteWidget: Port delay computations to TimeVal * HTMLBoxWidget: Port delay computations to TimeVal * Notification: Port delay computations to TimeVal * TextBoxWidget: Port delay computations to TimeVal * AutoSuspend: Port to TimeVal * AutoTurn: * Fix it so that settings are actually honored. * Port to TimeVal * BackgroundRunner: Port to TimeVal * Calibre: Port benchmarking code to TimeVal * BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly. * All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries..
3 years ago
self.tap_interval_override = TimeVal:new{ usec = self.tap_interval_override }
if Device:hasDPad() then
self.key_events.PressKey = { {"Press"}, doc = "select key" }
end
if Device:hasKeys() then
self.key_events.Close = { {"Back"}, doc = "close keyboard" }
end
if keyboard.wrapInputBox then
self.uwrap_func = keyboard.wrapInputBox(self.inputbox) or self.uwrap_func
end
end
function VirtualKeyboard:getKeyboardLayout()
return G_reader_settings:readSetting("keyboard_layout") or G_reader_settings:readSetting("language")
end
function VirtualKeyboard:setKeyboardLayout(layout)
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
end
function VirtualKeyboard:onClose()
UIManager:close(self)
return true
end
function VirtualKeyboard:onHideKeyboard()
return self.inputbox:onHideKeyboard()
end
function VirtualKeyboard:onPressKey()
self:getFocusItem():handleEvent(Event:new("TapSelect"))
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
end)
end
function VirtualKeyboard:onShow()
self:_refresh(true)
return true
end
function VirtualKeyboard:onCloseWidget()
self:_refresh(false)
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
if type(key_chars) == "table" then
key = key_chars[1]
label = key_chars.label
else
key = key_chars
key_chars = nil
end
local width_factor = self.KEYS[i][j].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
local virtual_key = VirtualKey:new{
key = key,
key_chars = key_chars,
icon = self.KEYS[i][j].icon,
label = 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 = 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,
}
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(true)
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
function VirtualKeyboard:goToEndOfLine()
self.inputbox:goToEndOfLine()
end
function VirtualKeyboard:upLine()
self.inputbox:upLine()
end
function VirtualKeyboard:downLine()
self.inputbox:downLine()
end
function VirtualKeyboard:clear()
logger.dbg("clear input")
self.inputbox:clear()
end
return VirtualKeyboard