2
0
mirror of https://github.com/koreader/koreader synced 2024-11-18 03:25:46 +00:00
koreader/frontend/ui/widget/virtualkeyboard.lua

1062 lines
36 KiB
Lua
Raw Normal View History

2017-04-29 08:38:09 +00:00
local Blitbuffer = require("ffi/blitbuffer")
2013-10-18 20:38:07 +00:00
local BottomContainer = require("ui/widget/container/bottomcontainer")
2017-04-29 08:38:09 +00:00
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local Event = require("ui/event")
local FocusManager = require("ui/widget/focusmanager")
2013-10-18 20:38:07 +00:00
local Font = require("ui/font")
2017-04-29 08:38:09 +00:00
local FrameContainer = require("ui/widget/container/framecontainer")
2013-10-18 20:38:07 +00:00
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
2017-04-29 08:38:09 +00:00
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")
2017-09-13 14:56:20 +00:00
local Size = require("ui/size")
2017-04-29 08:38:09 +00:00
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..
2021-03-30 00:57:59 +00:00
local TimeVal = require("ui/timeval")
2013-10-18 20:38:07 +00:00
local UIManager = require("ui/uimanager")
2017-04-29 08:38:09 +00:00
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")
2017-04-29 08:38:09 +00:00
local Screen = Device.screen
2013-07-30 15:07:33 +00:00
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
2013-10-18 20:38:07 +00:00
local VirtualKey = InputContainer:new{
2014-03-13 13:52:43 +00:00
key = nil,
icon = nil,
label = nil,
bold = nil,
2014-03-13 13:52:43 +00:00
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,
2014-03-13 13:52:43 +00:00
width = nil,
height = math.max(Screen:getWidth(), Screen:getHeight())*0.33,
bordersize = 0,
focused_bordersize = Size.border.default,
radius = 0,
2017-04-29 08:38:09 +00:00
face = Font:getFace("infont"),
2013-07-30 15:07:33 +00:00
}
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 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 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()
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)
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
2014-03-13 13:52:43 +00:00
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
2014-03-13 13:52:43 +00:00
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.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
2014-03-13 13:52:43 +00:00
end
2015-04-27 00:49:27 +00:00
local label_widget
2014-03-13 13:52:43 +00:00
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)
2014-03-13 13:52:43 +00:00
label_widget = ImageWidget:new{
file = self.icon,
scale_factor = 0, -- keep icon aspect ratio
height = icon_height,
width = self.width - 2*self.bordersize,
2014-03-13 13:52:43 +00:00
}
else
label_widget = TextWidget:new{
text = self.label,
face = self.face,
bold = self.bold or false,
2014-03-13 13:52:43 +00:00
}
-- 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,
},
}
2014-03-13 13:52:43 +00:00
end
self[1] = FrameContainer:new{
margin = 0,
bordersize = self.bordersize,
background = Blitbuffer.COLOR_WHITE,
radius = 0,
2014-03-13 13:52:43 +00:00
padding = 0,
allow_mirroring = false,
2014-03-13 13:52:43 +00:00
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,
},
},
2014-03-13 13:52:43 +00:00
}
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) 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")
2013-07-30 15:07:33 +00:00
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_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)
2019-02-07 00:14:37 +00:00
-- 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: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
2014-03-13 13:52:43 +00:00
end
return true
2013-07-30 15:07:33 +00:00
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 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, false)
2013-07-30 15:07:33 +00:00
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()
local item = self:getFocusItem()
if item then
item:handleEvent(Event:new("TapSelect"))
return true
end
return false
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 = 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 = G_reader_settings:readSetting("ges_tap_interval_on_keyboard", 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..
2021-03-30 00:57:59 +00:00
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
2022-01-23 17:40:37 +00:00
self.key_events.Close = { {Device.input.group.Back}, doc = "close keyboard" }
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)
UIManager:setDirty(self, function()
return "ui", keyboard_frame.dimen
end)
end
local VirtualKeyboard = FocusManager:new{
name = "VirtualKeyboard",
modal = true,
2014-03-13 13:52:43 +00:00
disable_double_tap = true,
inputbox = nil,
KEYS = {}, -- table to store layouts
shiftmode_keys = {},
symbolmode_keys = {},
utf8mode_keys = {},
umlautmode_keys = {},
keyboard_layer = 2,
2014-03-13 13:52:43 +00:00
shiftmode = false,
symbolmode = false,
2014-08-09 11:59:05 +00:00
umlautmode = false,
layout = {},
height = nil,
default_label_size = DEFAULT_LABEL_SIZE,
2017-09-13 14:56:20 +00:00
bordersize = Size.border.default,
padding = 0,
key_padding = Size.padding.small,
2013-07-30 15:07:33 +00:00
lang_to_keyboard_layout = {
ar_AA = "ar_AA_keyboard",
2021-02-28 18:59:02 +00:00
bg_BG = "bg_keyboard",
de = "de_keyboard",
el = "el_keyboard",
en = "en_keyboard",
es = "es_keyboard",
fa = "fa_keyboard",
fr = "fr_keyboard",
2019-09-22 20:21:00 +00:00
he = "he_keyboard",
ja = "ja_keyboard",
2021-02-01 21:35:15 +00:00
ka = "ka_keyboard",
pl = "pl_keyboard",
pt_BR = "pt_keyboard",
ro = "ro_keyboard",
ko_KR = "ko_KR_keyboard",
ru = "ru_keyboard",
2020-01-27 23:05:16 +00:00
tr = "tr_keyboard",
},
lang_has_submenu = {
ja = true,
},
}
2013-07-30 15:07:33 +00:00
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.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 = G_reader_settings:readSetting("ges_tap_interval_on_keyboard", 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..
2021-03-30 00:57:59 +00:00
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
if Device:hasDPad() and Device:hasKeyboard() and Device:isTouchDevice() 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
self.key_events.FocusLeft = nil
self.key_events.FocusRight = nil
self.key_events.FocusUp = nil
self.key_events.FocusDown = nil
self.key_events.PressKey = nil -- added above
end
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)
return true
end
function VirtualKeyboard:onPressKey()
local item = self:getFocusItem()
if item then
item:handleEvent(Event:new("TapSelect"))
return true
end
return false
2013-07-30 15:07:33 +00:00
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)
2014-03-13 13:52:43 +00:00
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)
2014-03-13 13:52:43 +00:00
end
self:addKeys()
2013-07-30 15:07:33 +00:00
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)
2014-03-13 13:52:43 +00:00
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 }
2014-03-13 13:52:43 +00:00
for i = 1, #self.KEYS do
local horizontal_group = HorizontalGroup:new{ allow_mirroring = false }
local layout_horizontal = {}
2014-03-13 13:52:43 +00:00
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)
2014-03-13 13:52:43 +00:00
- 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,
2014-03-13 13:52:43 +00:00
icon = self.KEYS[i][j].icon,
label = label,
alt_label = alt_label,
bold = self.KEYS[i][j].bold,
2014-03-13 13:52:43 +00:00
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
2014-03-13 13:52:43 +00:00
table.insert(horizontal_group, h_key_padding)
end
end
table.insert(vertical_group, horizontal_group)
table.insert(self.layout, layout_horizontal)
2014-03-13 13:52:43 +00:00
if i ~= #self.KEYS then
table.insert(vertical_group, v_key_padding)
end
end
2014-03-13 13:52:43 +00:00
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,
2014-03-13 13:52:43 +00:00
radius = 0,
padding = self.padding,
allow_mirroring = false,
2014-03-13 13:52:43 +00:00
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,
2014-03-13 13:52:43 +00:00
},
vertical_group,
}
}
self[1] = BottomContainer:new{
dimen = Screen:getSize(),
keyboard_frame,
}
self.dimen = keyboard_frame:getSize()
2013-07-30 15:07:33 +00:00
end
function VirtualKeyboard:setLayer(key)
2014-03-13 13:52:43 +00:00
if key == "Shift" then
self.shiftmode = not self.shiftmode
elseif key == "Sym" or key == "ABC" then
self.symbolmode = not self.symbolmode
2014-08-09 11:59:05 +00:00
elseif key == "Äéß" then
self.umlautmode = not self.umlautmode
2014-03-13 13:52:43 +00:00
end
self:initLayer()
self:_refresh(false)
2013-07-30 15:07:33 +00:00
end
function VirtualKeyboard:addChar(key)
logger.dbg("add char", key)
self.inputbox:addChars(key)
2013-07-30 15:07:33 +00:00
end
function VirtualKeyboard:delChar()
logger.dbg("delete char")
2014-03-13 13:52:43 +00:00
self.inputbox:delChar()
2013-07-30 15:07:33 +00:00
end
2013-10-18 20:38:07 +00:00
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: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
2013-10-18 20:38:07 +00:00
return VirtualKeyboard