mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
* Added a new plugin external-keyboard. It listens to USB events. When keyboard is plugged in or plugged out, it updates device and input configuration accordingly. * Added new fake events UsbDevicePlugIn and UsbDevicePlugOut that are emitted when a device is connected to a book reader that plays the role of USB host. The usage of the existing events UsbPlugIn and UsbPlugOut has not changed - they are used when a reader is connected to a host. The koreader-base has a related PR for those events. * Did a small refactoring of initialization for the modules FocusManager and InputText. They check device keyboard capabilities on their when the module is first loaded and store it. Some of the initialization code has been extracted into functions, so that we can re-initialize them when keyboard is (dis)connected. * Initial implementation centered around text input, and tested with USB keyboards on devices with OTG support. * Said OTG shenanigans are so far supported on devices with debugfs & the chipidea driver, or sunxi devices.
1480 lines
60 KiB
Lua
1480 lines
60 KiB
Lua
--[[--
|
|
An interface to get input events.
|
|
--]]
|
|
|
|
local DataStorage = require("datastorage")
|
|
local DEBUG = require("dbg")
|
|
local Event = require("ui/event")
|
|
local GestureDetector = require("device/gesturedetector")
|
|
local Key = require("device/key")
|
|
local framebuffer = require("ffi/framebuffer")
|
|
local input = require("ffi/input")
|
|
local logger = require("logger")
|
|
local time = require("ui/time")
|
|
local _ = require("gettext")
|
|
|
|
-- We're going to need a few <linux/input.h> constants...
|
|
local ffi = require("ffi")
|
|
local C = ffi.C
|
|
require("ffi/posix_h")
|
|
require("ffi/linux_input_h")
|
|
|
|
-- luacheck: push
|
|
-- luacheck: ignore
|
|
-- key press event values (KEY.value)
|
|
local EVENT_VALUE_KEY_PRESS = 1
|
|
local EVENT_VALUE_KEY_REPEAT = 2
|
|
local EVENT_VALUE_KEY_RELEASE = 0
|
|
|
|
-- For Kindle Oasis orientation events (ABS.code)
|
|
-- the ABS code of orientation event will be adjusted to -24 from 24 (C.ABS_PRESSURE)
|
|
-- as C.ABS_PRESSURE is also used to detect touch input in KOBO devices.
|
|
local ABS_OASIS_ORIENTATION = -24
|
|
local DEVICE_ORIENTATION_PORTRAIT_LEFT = 15
|
|
local DEVICE_ORIENTATION_PORTRAIT_RIGHT = 17
|
|
local DEVICE_ORIENTATION_PORTRAIT = 19
|
|
local DEVICE_ORIENTATION_PORTRAIT_ROTATED_LEFT = 16
|
|
local DEVICE_ORIENTATION_PORTRAIT_ROTATED_RIGHT = 18
|
|
local DEVICE_ORIENTATION_PORTRAIT_ROTATED = 20
|
|
local DEVICE_ORIENTATION_LANDSCAPE = 21
|
|
local DEVICE_ORIENTATION_LANDSCAPE_ROTATED = 22
|
|
|
|
-- Kindle Oasis 2 & 3 variant
|
|
-- c.f., drivers/input/misc/accel/bma2x2.c
|
|
local UPWARD_PORTRAIT_UP_INTERRUPT_HAPPENED = 15
|
|
local UPWARD_PORTRAIT_DOWN_INTERRUPT_HAPPENED = 16
|
|
local UPWARD_LANDSCAPE_LEFT_INTERRUPT_HAPPENED = 17
|
|
local UPWARD_LANDSCAPE_RIGHT_INTERRUPT_HAPPENED = 18
|
|
|
|
-- For the events of the Forma & Libra accelerometers (MSC.value)
|
|
-- c.f., drivers/hwmon/mma8x5x.c
|
|
local MSC_RAW_GSENSOR_PORTRAIT_DOWN = 0x17
|
|
local MSC_RAW_GSENSOR_PORTRAIT_UP = 0x18
|
|
local MSC_RAW_GSENSOR_LANDSCAPE_RIGHT = 0x19
|
|
local MSC_RAW_GSENSOR_LANDSCAPE_LEFT = 0x1a
|
|
-- Not that we care about those, but they are reported, and accurate ;).
|
|
local MSC_RAW_GSENSOR_BACK = 0x1b
|
|
local MSC_RAW_GSENSOR_FRONT = 0x1c
|
|
|
|
-- Based on ABS_MT_TOOL_TYPE values on Elan panels
|
|
local TOOL_TYPE_FINGER = 0
|
|
local TOOL_TYPE_PEN = 1
|
|
|
|
-- For debug logging of ev.type
|
|
local linux_evdev_type_map = {
|
|
[C.EV_SYN] = "EV_SYN",
|
|
[C.EV_KEY] = "EV_KEY",
|
|
[C.EV_REL] = "EV_REL",
|
|
[C.EV_ABS] = "EV_ABS",
|
|
[C.EV_MSC] = "EV_MSC",
|
|
[C.EV_SW] = "EV_SW",
|
|
[C.EV_LED] = "EV_LED",
|
|
[C.EV_SND] = "EV_SND",
|
|
[C.EV_REP] = "EV_REP",
|
|
[C.EV_FF] = "EV_FF",
|
|
[C.EV_PWR] = "EV_PWR",
|
|
[C.EV_FF_STATUS] = "EV_FF_STATUS",
|
|
[C.EV_MAX] = "EV_MAX",
|
|
[C.EV_SDL] = "EV_SDL",
|
|
}
|
|
|
|
-- For debug logging of ev.code
|
|
local linux_evdev_syn_code_map = {
|
|
[C.SYN_REPORT] = "SYN_REPORT",
|
|
[C.SYN_CONFIG] = "SYN_CONFIG",
|
|
[C.SYN_MT_REPORT] = "SYN_MT_REPORT",
|
|
[C.SYN_DROPPED] = "SYN_DROPPED",
|
|
}
|
|
|
|
local linux_evdev_key_code_map = {
|
|
[C.KEY_BATTERY] = "KEY_BATTERY",
|
|
[C.BTN_TOOL_PEN] = "BTN_TOOL_PEN",
|
|
[C.BTN_TOOL_FINGER] = "BTN_TOOL_FINGER",
|
|
[C.BTN_TOOL_RUBBER] = "BTN_TOOL_RUBBER",
|
|
[C.BTN_TOUCH] = "BTN_TOUCH",
|
|
[C.BTN_STYLUS] = "BTN_STYLUS",
|
|
[C.BTN_STYLUS2] = "BTN_STYLUS2",
|
|
}
|
|
|
|
local linux_evdev_abs_code_map = {
|
|
[C.ABS_X] = "ABS_X",
|
|
[C.ABS_Y] = "ABS_Y",
|
|
[C.ABS_PRESSURE] = "ABS_PRESSURE",
|
|
[C.ABS_DISTANCE] = "ABS_DISTANCE",
|
|
[C.ABS_MT_SLOT] = "ABS_MT_SLOT",
|
|
[C.ABS_MT_TOUCH_MAJOR] = "ABS_MT_TOUCH_MAJOR",
|
|
[C.ABS_MT_TOUCH_MINOR] = "ABS_MT_TOUCH_MINOR",
|
|
[C.ABS_MT_WIDTH_MAJOR] = "ABS_MT_WIDTH_MAJOR",
|
|
[C.ABS_MT_WIDTH_MINOR] = "ABS_MT_WIDTH_MINOR",
|
|
[C.ABS_MT_ORIENTATION] = "ABS_MT_ORIENTATION",
|
|
[C.ABS_MT_POSITION_X] = "ABS_MT_POSITION_X",
|
|
[C.ABS_MT_POSITION_Y] = "ABS_MT_POSITION_Y",
|
|
[C.ABS_MT_TOOL_TYPE] = "ABS_MT_TOOL_TYPE",
|
|
[C.ABS_MT_BLOB_ID] = "ABS_MT_BLOB_ID",
|
|
[C.ABS_MT_TRACKING_ID] = "ABS_MT_TRACKING_ID",
|
|
[C.ABS_MT_PRESSURE] = "ABS_MT_PRESSURE",
|
|
[C.ABS_TILT_X] = "ABS_TILT_X",
|
|
[C.ABS_TILT_Y] = "ABS_TILT_Y",
|
|
[C.ABS_MT_DISTANCE] = "ABS_MT_DISTANCE",
|
|
[C.ABS_MT_TOOL_X] = "ABS_MT_TOOL_X",
|
|
[C.ABS_MT_TOOL_Y] = "ABS_MT_TOOL_Y",
|
|
}
|
|
|
|
local linux_evdev_msc_code_map = {
|
|
[C.MSC_RAW] = "MSC_RAW",
|
|
}
|
|
|
|
local linux_evdev_rep_code_map = {
|
|
[C.REP_DELAY] = "REP_DELAY",
|
|
[C.REP_PERIOD] = "REP_PERIOD",
|
|
}
|
|
-- luacheck: pop
|
|
|
|
local _internal_clipboard_text = nil -- holds the last copied text
|
|
|
|
local Input = {
|
|
-- must point to the device implementation when instantiating
|
|
device = nil,
|
|
-- this depends on keyboard layout and should be overridden:
|
|
event_map = {},
|
|
-- adapters are post processing functions that transform a given event to another event
|
|
event_map_adapter = {},
|
|
-- EV_ABS event to honor for pressure event (if any)
|
|
pressure_event = nil,
|
|
|
|
group = {
|
|
Cursor = { "Up", "Down", "Left", "Right" },
|
|
PgFwd = { "RPgFwd", "LPgFwd" },
|
|
PgBack = { "RPgBack", "LPgBack" },
|
|
Back = { "Back" },
|
|
Alphabet = {
|
|
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
|
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
|
|
},
|
|
AlphaNumeric = {
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
|
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
|
|
},
|
|
Numeric = {
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
|
|
},
|
|
Text = {
|
|
" ", ".", "/",
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
|
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
|
|
},
|
|
Any = {
|
|
" ", ".", "/",
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
|
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
|
"Up", "Down", "Left", "Right", "Press", "Backspace", "End",
|
|
"Back", "Sym", "AA", "Menu", "Home", "Del",
|
|
"LPgBack", "RPgBack", "LPgFwd", "RPgFwd"
|
|
},
|
|
},
|
|
|
|
fake_event_set = {
|
|
IntoSS = true, OutOfSS = true,
|
|
UsbPlugIn = true, UsbPlugOut = true,
|
|
Charging = true, NotCharging = true,
|
|
WakeupFromSuspend = true, ReadyToSuspend = true,
|
|
UsbDevicePlugIn = true, UsbDevicePlugOut = true,
|
|
},
|
|
|
|
-- NOTE: When looking at the device in Portrait mode, that's assuming PgBack is on TOP, and PgFwd on the BOTTOM
|
|
rotation_map = {
|
|
[framebuffer.ORIENTATION_PORTRAIT] = {},
|
|
[framebuffer.ORIENTATION_LANDSCAPE] = { Up = "Right", Right = "Down", Down = "Left", Left = "Up", LPgBack = "LPgFwd", LPgFwd = "LPgBack", RPgBack = "RPgFwd", RPgFwd = "RPgBack" },
|
|
[framebuffer.ORIENTATION_PORTRAIT_ROTATED] = { Up = "Down", Right = "Left", Down = "Up", Left = "Right", LPgFwd = "LPgBack", LPgBack = "LPgFwd", RPgFwd = "RPgBack", RPgBack = "RPgFwd" },
|
|
[framebuffer.ORIENTATION_LANDSCAPE_ROTATED] = { Up = "Left", Right = "Up", Down = "Right", Left = "Down" }
|
|
},
|
|
|
|
timer_callbacks = nil, -- instance-specific table, because the object may get destroyed & recreated at runtime
|
|
disable_double_tap = true,
|
|
tap_interval_override = nil,
|
|
|
|
-- keyboard state:
|
|
modifiers = {
|
|
Alt = false,
|
|
Ctrl = false,
|
|
Shift = false,
|
|
Sym = false,
|
|
Meta = false,
|
|
},
|
|
|
|
-- repeat state:
|
|
repeat_count = 0,
|
|
|
|
-- touch state:
|
|
main_finger_slot = 0,
|
|
cur_slot = 0,
|
|
MTSlots = nil, -- table, object may be replaced at runtime
|
|
active_slots = nil, -- ditto
|
|
ev_slots = nil, -- table
|
|
gesture_detector = nil,
|
|
|
|
-- simple internal clipboard implementation, can be overidden to use system clipboard
|
|
hasClipboardText = function()
|
|
return _internal_clipboard_text ~= nil and _internal_clipboard_text ~= ""
|
|
end,
|
|
getClipboardText = function()
|
|
return _internal_clipboard_text
|
|
end,
|
|
setClipboardText = function(text)
|
|
_internal_clipboard_text = text
|
|
end,
|
|
}
|
|
|
|
function Input:new(o)
|
|
o = o or {}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
if o.init then o:init() end
|
|
return o
|
|
end
|
|
|
|
function Input:init()
|
|
-- Initialize instance-specific tables
|
|
-- NOTE: All of these arrays may be destroyed & recreated at runtime, so we don't want a parent/class object for those.
|
|
self.timer_callbacks = {}
|
|
self.MTSlots = {}
|
|
self.active_slots = {}
|
|
|
|
-- Handle default finger slot
|
|
self.cur_slot = self.main_finger_slot
|
|
self.ev_slots = {
|
|
[self.main_finger_slot] = {
|
|
slot = self.main_finger_slot,
|
|
},
|
|
}
|
|
|
|
self.gesture_detector = GestureDetector:new{
|
|
screen = self.device.screen,
|
|
input = self,
|
|
}
|
|
|
|
-- set up fake event map
|
|
self.event_map[10000] = "IntoSS" -- go into screen saver
|
|
self.event_map[10001] = "OutOfSS" -- go out of screen saver
|
|
self.event_map[10010] = "UsbPlugIn"
|
|
self.event_map[10011] = "UsbPlugOut"
|
|
self.event_map[10020] = "Charging"
|
|
self.event_map[10021] = "NotCharging"
|
|
self.event_map[10030] = "WakeupFromSuspend"
|
|
self.event_map[10031] = "ReadyToSuspend"
|
|
self.event_map[10040] = "UsbDevicePlugIn"
|
|
self.event_map[10041] = "UsbDevicePlugOut"
|
|
|
|
-- user custom event map
|
|
local custom_event_map_location = string.format(
|
|
"%s/%s", DataStorage:getSettingsDir(), "event_map.lua")
|
|
local ok, custom_event_map = pcall(dofile, custom_event_map_location)
|
|
if ok then
|
|
for key, value in pairs(custom_event_map) do
|
|
self.event_map[key] = value
|
|
end
|
|
logger.info("loaded custom event map", custom_event_map)
|
|
end
|
|
|
|
if G_reader_settings:isTrue("backspace_as_back") then
|
|
table.insert(self.group.Back, "Backspace")
|
|
end
|
|
|
|
-- setup inhibitInputUntil scheduling function
|
|
self._inhibitInputUntil_func = function() self:inhibitInputUntil() end
|
|
end
|
|
|
|
--[[--
|
|
Wrapper for FFI input open.
|
|
|
|
Note that we adhere to the "." syntax here for compatibility.
|
|
|
|
@todo Clean up separation FFI/this.
|
|
--]]
|
|
function Input.open(device, is_emu_events)
|
|
return input.open(device, is_emu_events and 1 or 0)
|
|
end
|
|
|
|
--[[--
|
|
Different device models can implement their own hooks
|
|
and register them.
|
|
--]]
|
|
function Input:registerEventAdjustHook(hook, hook_params)
|
|
local old = self.eventAdjustHook
|
|
self.eventAdjustHook = function(this, ev)
|
|
old(this, ev)
|
|
hook(this, ev, hook_params)
|
|
end
|
|
end
|
|
|
|
function Input:registerGestureAdjustHook(hook, hook_params)
|
|
local old = self.gestureAdjustHook
|
|
self.gestureAdjustHook = function(this, ges)
|
|
old(this, ges)
|
|
hook(this, ges, hook_params)
|
|
end
|
|
end
|
|
|
|
function Input:eventAdjustHook(ev)
|
|
-- do nothing by default
|
|
end
|
|
|
|
function Input:gestureAdjustHook(ges)
|
|
-- do nothing by default
|
|
end
|
|
|
|
--- Catalog of predefined hooks.
|
|
function Input:adjustTouchSwitchXY(ev)
|
|
if ev.type == C.EV_ABS then
|
|
if ev.code == C.ABS_X then
|
|
ev.code = C.ABS_Y
|
|
elseif ev.code == C.ABS_Y then
|
|
ev.code = C.ABS_X
|
|
elseif ev.code == C.ABS_MT_POSITION_X then
|
|
ev.code = C.ABS_MT_POSITION_Y
|
|
elseif ev.code == C.ABS_MT_POSITION_Y then
|
|
ev.code = C.ABS_MT_POSITION_X
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchScale(ev, by)
|
|
if ev.type == C.EV_ABS then
|
|
if ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X then
|
|
ev.value = by.x * ev.value
|
|
end
|
|
if ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y then
|
|
ev.value = by.y * ev.value
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchMirrorX(ev, max_x)
|
|
if ev.type == C.EV_ABS
|
|
and (ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X) then
|
|
ev.value = max_x - ev.value
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchMirrorY(ev, max_y)
|
|
if ev.type == C.EV_ABS
|
|
and (ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y) then
|
|
ev.value = max_y - ev.value
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchTranslate(ev, by)
|
|
if ev.type == C.EV_ABS then
|
|
if ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X then
|
|
ev.value = by.x + ev.value
|
|
end
|
|
if ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y then
|
|
ev.value = by.y + ev.value
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:adjustKindleOasisOrientation(ev)
|
|
if ev.type == C.EV_ABS and ev.code == C.ABS_PRESSURE then
|
|
ev.code = ABS_OASIS_ORIENTATION
|
|
end
|
|
end
|
|
|
|
function Input:setTimeout(slot, ges, cb, origin, delay)
|
|
local item = {
|
|
slot = slot,
|
|
gesture = ges,
|
|
callback = cb,
|
|
}
|
|
|
|
-- We're going to need the clock source id for these events from GestureDetector
|
|
local clock_id = self.gesture_detector:getClockSource()
|
|
local deadline
|
|
|
|
-- If we're on a platform with the timerfd backend, handle that
|
|
local timerfd
|
|
if input.setTimer then
|
|
-- If GestureDetector's clock source probing was inconclusive, do this on the UI timescale instead.
|
|
if clock_id == -1 then
|
|
deadline = time.now() + delay
|
|
clock_id = C.CLOCK_MONOTONIC
|
|
else
|
|
deadline = origin + delay
|
|
end
|
|
-- What this does is essentially to ask the kernel to wake us up when the timer expires,
|
|
-- instead of ensuring that ourselves via a polling timeout.
|
|
-- This ensures perfect accuracy, and allows it to be computed in the event's own timescale.
|
|
local sec, usec = time.split_s_us(deadline)
|
|
timerfd = input.setTimer(clock_id, sec, usec)
|
|
end
|
|
if timerfd then
|
|
-- It worked, tweak the table a bit to make it clear the deadline will be handled by the kernel
|
|
item.timerfd = timerfd
|
|
-- We basically only need this for the sorting ;).
|
|
item.deadline = deadline
|
|
else
|
|
-- No timerfd, we'll compute a poll timeout ourselves.
|
|
if clock_id == C.CLOCK_MONOTONIC then
|
|
-- If the event's clocksource is monotonic, we can use it directly.
|
|
deadline = origin + delay
|
|
else
|
|
-- Otherwise, fudge it by using a current timestamp in the UI's timescale (MONOTONIC).
|
|
-- This isn't the end of the world in practice (c.f., #7415).
|
|
deadline = time.now() + delay
|
|
end
|
|
item.deadline = deadline
|
|
end
|
|
table.insert(self.timer_callbacks, item)
|
|
|
|
-- NOTE: While the timescale is monotonic, we may interleave timers based on different delays, so we still need to sort...
|
|
table.sort(self.timer_callbacks, function(v1, v2)
|
|
return v1.deadline < v2.deadline
|
|
end)
|
|
end
|
|
|
|
-- Clear all timeouts for a specific slot (and a specific gesture, if ges is set)
|
|
function Input:clearTimeout(slot, ges)
|
|
for i = #self.timer_callbacks, 1, -1 do
|
|
local item = self.timer_callbacks[i]
|
|
if item.slot == slot and (not ges or item.gesture == ges) then
|
|
-- If the timerfd backend is in use, close the fd and free the list's node, too.
|
|
if item.timerfd then
|
|
input.clearTimer(item.timerfd)
|
|
end
|
|
table.remove(self.timer_callbacks, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:clearTimeouts()
|
|
-- If the timerfd backend is in use, close the fds, too
|
|
if input.setTimer then
|
|
for _, item in ipairs(self.timer_callbacks) do
|
|
if item.timerfd then
|
|
input.clearTimer(item.timerfd)
|
|
end
|
|
end
|
|
end
|
|
|
|
self.timer_callbacks = {}
|
|
end
|
|
|
|
-- Reset the gesture parsing state to a blank slate
|
|
function Input:resetState()
|
|
if self.gesture_detector then
|
|
self.gesture_detector:dropContacts()
|
|
-- Resets the clock source probe
|
|
self.gesture_detector:resetClockSource()
|
|
end
|
|
self:clearTimeouts()
|
|
end
|
|
|
|
function Input:handleKeyBoardEv(ev)
|
|
-- Detect loss of contact for the "snow" protocol...
|
|
-- NOTE: Some ST devices may also behave similarly, but we handle those via ABS_PRESSURE
|
|
if self.snow_protocol then
|
|
if ev.code == C.BTN_TOUCH and ev.value == 0 then
|
|
-- Kernel sends it after loss of contact for *all* slots,
|
|
-- only once the final contact point has been lifted.
|
|
if #self.MTSlots == 0 then
|
|
-- Likely, since this is usually in its own event stream,
|
|
-- meaning self.MTSlots has *just* been cleared by our last EV_SYN:SYN_REPORT handler...
|
|
-- So, poke at the actual data to find the slots that are currently active (i.e., in the down state),
|
|
-- and re-populate a minimal self.MTSlots array that simply switches them to the up state ;).
|
|
for _, slot in pairs(self.ev_slots) do
|
|
if slot.id ~= -1 then
|
|
table.insert(self.MTSlots, slot)
|
|
slot.id = -1
|
|
end
|
|
end
|
|
else
|
|
-- Unlikely, given what we mentioned above...
|
|
-- Note that, funnily enough, its EV_KEY:BTN_TOUCH:1 counterpart
|
|
-- *can* be in the same initial event stream as the EV_ABS batch...
|
|
for _, MTSlot in ipairs(self.MTSlots) do
|
|
self:setMtSlot(MTSlot.slot, "id", -1)
|
|
end
|
|
end
|
|
|
|
return
|
|
end
|
|
elseif self.wacom_protocol then
|
|
if ev.code == C.BTN_TOOL_PEN then
|
|
-- Always send pen data to slot 0
|
|
self:setupSlotData(0)
|
|
if ev.value == 1 then
|
|
self:setCurrentMtSlot("tool", TOOL_TYPE_PEN)
|
|
else
|
|
self:setCurrentMtSlot("tool", TOOL_TYPE_FINGER)
|
|
end
|
|
elseif ev.code == C.BTN_TOUCH then
|
|
-- Much like on snow, use this to detect contact down & lift,
|
|
-- as ABS_PRESSURE may be entirely omitted from hover events,
|
|
-- and ABS_DISTANCE is not very clear cut...
|
|
self:setupSlotData(0)
|
|
if ev.value == 1 then
|
|
self:setCurrentMtSlot("id", 0)
|
|
else
|
|
self:setCurrentMtSlot("id", -1)
|
|
end
|
|
end
|
|
end
|
|
|
|
local keycode = self.event_map[ev.code]
|
|
if not keycode then
|
|
-- do not handle keypress for keys we don't know
|
|
return
|
|
end
|
|
|
|
if self.event_map_adapter[keycode] then
|
|
return self.event_map_adapter[keycode](ev)
|
|
end
|
|
|
|
-- take device rotation into account
|
|
if self.rotation_map[self.device.screen:getRotationMode()][keycode] then
|
|
keycode = self.rotation_map[self.device.screen:getRotationMode()][keycode]
|
|
end
|
|
|
|
if self.fake_event_set[keycode] then
|
|
return keycode
|
|
end
|
|
|
|
-- The hardware camera button is used in Android to toggle the touchscreen
|
|
if keycode == "Camera" and ev.value == EVENT_VALUE_KEY_RELEASE
|
|
and G_reader_settings:isTrue("camera_key_toggles_touchscreen") then
|
|
local isAndroid, android = pcall(require, "android")
|
|
if isAndroid then
|
|
-- toggle touchscreen behaviour
|
|
android.toggleTouchscreenIgnored()
|
|
|
|
-- show a toast with the new behaviour
|
|
if android.isTouchscreenIgnored() then
|
|
android.notification(_("Touchscreen disabled"))
|
|
else
|
|
android.notification(_("Touchscreen enabled"))
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
if keycode == "Power" then
|
|
-- Kobo generates Power keycode only, we need to decide whether it's
|
|
-- power-on or power-off ourselves.
|
|
if ev.value == EVENT_VALUE_KEY_PRESS then
|
|
return "PowerPress"
|
|
elseif ev.value == EVENT_VALUE_KEY_RELEASE then
|
|
return "PowerRelease"
|
|
end
|
|
end
|
|
|
|
-- toggle fullscreen on F11
|
|
if self:isEvKeyPress(ev) and keycode == "F11" and not self.device:isAlwaysFullscreen() then
|
|
local UIManager = require("ui/uimanager")
|
|
UIManager:broadcastEvent(Event:new("ToggleFullscreen"))
|
|
end
|
|
|
|
-- quit on Alt + F4
|
|
-- this is also emitted by the close event in SDL
|
|
if self:isEvKeyPress(ev) and self.modifiers["Alt"] and keycode == "F4" then
|
|
local UIManager = require("ui/uimanager")
|
|
UIManager:broadcastEvent(Event:new("Close")) -- Tell all widgets to close.
|
|
UIManager:nextTick(function() UIManager:quit() end) -- Ensure the program closes in case of some lingering dialog.
|
|
end
|
|
|
|
-- handle modifier keys
|
|
if self.modifiers[keycode] ~= nil then
|
|
if ev.value == EVENT_VALUE_KEY_PRESS then
|
|
self.modifiers[keycode] = true
|
|
elseif ev.value == EVENT_VALUE_KEY_RELEASE then
|
|
self.modifiers[keycode] = false
|
|
end
|
|
return
|
|
end
|
|
|
|
local key = Key:new(keycode, self.modifiers)
|
|
|
|
if ev.value == EVENT_VALUE_KEY_PRESS then
|
|
return Event:new("KeyPress", key)
|
|
elseif ev.value == EVENT_VALUE_KEY_REPEAT then
|
|
-- NOTE: We only care about repeat events from the pageturn buttons...
|
|
-- And we *definitely* don't want to flood the Event queue with useless SleepCover repeats!
|
|
if keycode == "LPgBack"
|
|
or keycode == "RPgBack"
|
|
or keycode == "LPgFwd"
|
|
or keycode == "RPgFwd" then
|
|
--- @fixme Crappy event staggering!
|
|
--
|
|
-- The Forma & co repeats every 80ms after a 400ms delay, and 500ms roughly corresponds to a flashing update,
|
|
-- so stuff is usually in sync when you release the key.
|
|
-- Obvious downside is that this ends up slower than just mashing the key.
|
|
--
|
|
-- A better approach would be an onKeyRelease handler that flushes the Event queue...
|
|
self.repeat_count = self.repeat_count + 1
|
|
if self.repeat_count == 1 then
|
|
return Event:new("KeyRepeat", key)
|
|
elseif self.repeat_count >= 6 then
|
|
self.repeat_count = 0
|
|
end
|
|
end
|
|
elseif ev.value == EVENT_VALUE_KEY_RELEASE then
|
|
self.repeat_count = 0
|
|
return Event:new("KeyRelease", key)
|
|
end
|
|
end
|
|
|
|
-- Mangled variant of handleKeyBoardEv that will only handle power management related keys.
|
|
-- (Used when blocking input during suspend via sleep cover).
|
|
function Input:handlePowerManagementOnlyEv(ev)
|
|
local keycode = self.event_map[ev.code]
|
|
if not keycode then
|
|
-- Do not handle keypress for keys we don't know
|
|
return
|
|
end
|
|
|
|
-- We'll need to parse the synthetic event map, because SleepCover* events are synthetic.
|
|
if self.event_map_adapter[keycode] then
|
|
keycode = self.event_map_adapter[keycode](ev)
|
|
end
|
|
|
|
-- Power management synthetic events
|
|
if keycode == "SleepCoverClosed" or keycode == "SleepCoverOpened"
|
|
or keycode == "Suspend" or keycode == "Resume" then
|
|
return keycode
|
|
end
|
|
|
|
if self.fake_event_set[keycode] then
|
|
return keycode
|
|
end
|
|
|
|
if keycode == "Power" then
|
|
-- Kobo generates Power keycode only, we need to decide whether it's
|
|
-- power-on or power-off ourselves.
|
|
if ev.value == EVENT_VALUE_KEY_PRESS then
|
|
return "PowerPress"
|
|
elseif ev.value == EVENT_VALUE_KEY_RELEASE then
|
|
return "PowerRelease"
|
|
end
|
|
end
|
|
|
|
-- Nothing to see, move along!
|
|
return
|
|
end
|
|
|
|
-- Empty event handler used to send input to the void
|
|
function Input:voidEv(ev)
|
|
return
|
|
end
|
|
|
|
-- Generic event handler for unhandled input events
|
|
function Input:handleGenericEv(ev)
|
|
return Event:new("GenericInput", ev)
|
|
end
|
|
|
|
function Input:handleMiscEv(ev)
|
|
-- should be handled by a misc event protocol plugin
|
|
end
|
|
|
|
function Input:handleSdlEv(ev)
|
|
-- overwritten by device implementation
|
|
end
|
|
|
|
--[[--
|
|
Parse each touch ev from kernel and build up tev.
|
|
tev will be sent to GestureDetector:feedEvent
|
|
|
|
Events for a single tap motion from Linux kernel (MT protocol B):
|
|
|
|
MT_TRACK_ID: 0
|
|
MT_X: 222
|
|
MT_Y: 207
|
|
SYN REPORT
|
|
MT_TRACK_ID: -1
|
|
SYN REPORT
|
|
|
|
Notice that each line is a single event.
|
|
|
|
From kernel document:
|
|
For type B devices, the kernel driver should associate a slot with each
|
|
identified contact, and use that slot to propagate changes for the contact.
|
|
Creation, replacement and destruction of contacts is achieved by modifying
|
|
the C.ABS_MT_TRACKING_ID of the associated slot. A non-negative tracking id
|
|
is interpreted as a contact, and the value -1 denotes an unused slot. A
|
|
tracking id not previously present is considered new, and a tracking id no
|
|
longer present is considered removed. Since only changes are propagated,
|
|
the full state of each initiated contact has to reside in the receiving
|
|
end. Upon receiving an MT event, one simply updates the appropriate
|
|
attribute of the current slot.
|
|
--]]
|
|
function Input:handleTouchEv(ev)
|
|
if ev.type == C.EV_ABS then
|
|
-- NOTE: Ideally, an input frame starts with either ABS_MT_SLOT or ABS_MT_TRACKING_ID,
|
|
-- but they *both* may be omitted if the last contact point just moved without lift.
|
|
-- The use of setCurrentMtSlotChecked instead of setCurrentMtSlot ensures
|
|
-- we actually setup the slot data storage and/or reference for the current slot in this case,
|
|
-- as the reference list is empty at the beginning of an input frame (c.f., Input:newFrame).
|
|
-- The most common platforms where you'll see this happen are:
|
|
-- * PocketBook, because of our InkView EVT_POINTERMOVE translation
|
|
-- (c.f., translateEvent @ ffi/input_pocketbook.lua).
|
|
-- * SDL, because of our SDL_MOUSEMOTION/SDL_FINGERMOTION translation
|
|
-- (c.f., waitForEvent @ ffi/SDL2_0.lua).
|
|
if ev.code == C.ABS_MT_SLOT then
|
|
self:setupSlotData(ev.value)
|
|
elseif ev.code == C.ABS_MT_TRACKING_ID then
|
|
if self.snow_protocol then
|
|
-- NOTE: We'll never get an ABS_MT_SLOT event, instead we have a slot-like ABS_MT_TRACKING_ID value...
|
|
-- This also means this may never be set to -1 on contact lift,
|
|
-- which is why we instead rely on EV_KEY:BTN_TOUCH:0 for that (c.f., handleKeyBoardEv).
|
|
self:setupSlotData(ev.value)
|
|
else
|
|
-- The Elan driver needlessly repeats unchanged ABS_MT_TRACKING_ID values,
|
|
-- which allows us to do this here instead of relying more aggressively on setCurrentMtSlotChecked.
|
|
if #self.MTSlots == 0 then
|
|
self:addSlot(self.cur_slot)
|
|
end
|
|
end
|
|
self:setCurrentMtSlot("id", ev.value)
|
|
elseif ev.code == C.ABS_MT_TOOL_TYPE then
|
|
-- NOTE: On the Elipsa: Finger == 0; Pen == 1
|
|
self:setCurrentMtSlot("tool", ev.value)
|
|
elseif ev.code == C.ABS_MT_POSITION_X or ev.code == C.ABS_X then
|
|
self:setCurrentMtSlotChecked("x", ev.value)
|
|
elseif ev.code == C.ABS_MT_POSITION_Y or ev.code == C.ABS_Y then
|
|
self:setCurrentMtSlotChecked("y", ev.value)
|
|
elseif self.pressure_event and ev.code == self.pressure_event and ev.value == 0 then
|
|
-- Drop hovering *pen* events
|
|
local tool = self:getCurrentMtSlotData("tool")
|
|
if tool and tool == TOOL_TYPE_PEN then
|
|
self:setCurrentMtSlot("id", -1)
|
|
end
|
|
end
|
|
elseif ev.type == C.EV_SYN then
|
|
if ev.code == C.SYN_REPORT then
|
|
for _, MTSlot in ipairs(self.MTSlots) do
|
|
self:setMtSlot(MTSlot.slot, "timev", time.timeval(ev.time))
|
|
end
|
|
-- feed ev in all slots to state machine
|
|
local touch_gestures = self.gesture_detector:feedEvent(self.MTSlots)
|
|
self:newFrame()
|
|
local ges_evs = {}
|
|
for _, touch_ges in ipairs(touch_gestures) do
|
|
self:gestureAdjustHook(touch_ges)
|
|
table.insert(ges_evs, Event:new("Gesture", self.gesture_detector:adjustGesCoordinate(touch_ges)))
|
|
end
|
|
return ges_evs
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:handleTouchEvPhoenix(ev)
|
|
-- Hack on handleTouchEV for the Kobo Aura
|
|
-- It seems to be using a custom protocol:
|
|
-- finger 0 down:
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 0);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 1);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 1);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, x1);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, y1);
|
|
-- input_mt_sync (elan_touch_data.input);
|
|
-- finger 1 down:
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 1);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 1);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 1);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, x2);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, y2);
|
|
-- input_mt_sync (elan_touch_data.input);
|
|
-- finger 0 up:
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 0);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 0);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 0);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, last_x);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, last_y);
|
|
-- input_mt_sync (elan_touch_data.input);
|
|
-- finger 1 up:
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 1);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 0);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 0);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, last_x2);
|
|
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, last_y2);
|
|
-- input_mt_sync (elan_touch_data.input);
|
|
if ev.type == C.EV_ABS then
|
|
if ev.code == C.ABS_MT_TRACKING_ID then
|
|
self:setupSlotData(ev.value)
|
|
self:setCurrentMtSlot("id", ev.value)
|
|
elseif ev.code == C.ABS_MT_TOUCH_MAJOR and ev.value == 0 then
|
|
self:setCurrentMtSlot("id", -1)
|
|
elseif ev.code == C.ABS_MT_POSITION_X then
|
|
self:setCurrentMtSlot("x", ev.value)
|
|
elseif ev.code == C.ABS_MT_POSITION_Y then
|
|
self:setCurrentMtSlot("y", ev.value)
|
|
end
|
|
elseif ev.type == C.EV_SYN then
|
|
if ev.code == C.SYN_REPORT then
|
|
for _, MTSlot in ipairs(self.MTSlots) do
|
|
self:setMtSlot(MTSlot.slot, "timev", time.timeval(ev.time))
|
|
end
|
|
-- feed ev in all slots to state machine
|
|
local touch_gestures = self.gesture_detector:feedEvent(self.MTSlots)
|
|
self:newFrame()
|
|
local ges_evs = {}
|
|
for _, touch_ges in ipairs(touch_gestures) do
|
|
self:gestureAdjustHook(touch_ges)
|
|
table.insert(ges_evs, Event:new("Gesture", self.gesture_detector:adjustGesCoordinate(touch_ges)))
|
|
end
|
|
return ges_evs
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:handleTouchEvLegacy(ev)
|
|
-- Single Touch Protocol.
|
|
-- Some devices emit both singletouch and multitouch events,
|
|
-- on those devices, the 'handleTouchEv' function may not behave as expected. Use this one instead.
|
|
if ev.type == C.EV_ABS then
|
|
if ev.code == C.ABS_X then
|
|
self:setCurrentMtSlotChecked("x", ev.value)
|
|
elseif ev.code == C.ABS_Y then
|
|
self:setCurrentMtSlotChecked("y", ev.value)
|
|
elseif ev.code == C.ABS_PRESSURE then
|
|
if ev.value ~= 0 then
|
|
self:setCurrentMtSlotChecked("id", 1)
|
|
else
|
|
self:setCurrentMtSlotChecked("id", -1)
|
|
|
|
-- On Kobo Mk. 3 devices, the frame that reports a contact lift *actually* does the coordinates transform for us...
|
|
-- Unfortunately, our own transforms are not stateful, so, just revert 'em here,
|
|
-- since we can't simply avoid not doing 'em for that frame...
|
|
-- c.f., https://github.com/koreader/koreader/issues/2128#issuecomment-1236289909 for logs on a Touch B
|
|
-- NOTE: We can afford to do this here instead of on SYN_REPORT because the kernel *always*
|
|
-- reports ABS_PRESSURE after ABS_X/ABS_Y.
|
|
if self.touch_kobo_mk3_protocol then
|
|
local y = 599 - self:getCurrentMtSlotData("x") -- Mk. 3 devices are all 600x800, so just hard-code it here.
|
|
local x = self:getCurrentMtSlotData("y")
|
|
self:setCurrentMtSlot("x", x)
|
|
self:setCurrentMtSlot("y", y)
|
|
end
|
|
end
|
|
end
|
|
elseif ev.type == C.EV_SYN then
|
|
if ev.code == C.SYN_REPORT then
|
|
for _, MTSlot in ipairs(self.MTSlots) do
|
|
self:setMtSlot(MTSlot.slot, "timev", time.timeval(ev.time))
|
|
end
|
|
|
|
-- feed ev in all slots to state machine
|
|
local touch_gestures = self.gesture_detector:feedEvent(self.MTSlots)
|
|
self:newFrame()
|
|
local ges_evs = {}
|
|
for _, touch_ges in ipairs(touch_gestures) do
|
|
self:gestureAdjustHook(touch_ges)
|
|
table.insert(ges_evs, Event:new("Gesture", self.gesture_detector:adjustGesCoordinate(touch_ges)))
|
|
end
|
|
return ges_evs
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:handleOasisOrientationEv(ev)
|
|
local rotation_mode, screen_mode
|
|
if self.device:isZelda() then
|
|
if ev.value == UPWARD_PORTRAIT_UP_INTERRUPT_HAPPENED then
|
|
-- i.e., UR
|
|
rotation_mode = framebuffer.ORIENTATION_PORTRAIT
|
|
screen_mode = 'portrait'
|
|
elseif ev.value == UPWARD_LANDSCAPE_LEFT_INTERRUPT_HAPPENED then
|
|
-- i.e., CW
|
|
rotation_mode = framebuffer.ORIENTATION_LANDSCAPE
|
|
screen_mode = 'landscape'
|
|
elseif ev.value == UPWARD_PORTRAIT_DOWN_INTERRUPT_HAPPENED then
|
|
-- i.e., UD
|
|
rotation_mode = framebuffer.ORIENTATION_PORTRAIT_ROTATED
|
|
screen_mode = 'portrait'
|
|
elseif ev.value == UPWARD_LANDSCAPE_RIGHT_INTERRUPT_HAPPENED then
|
|
-- i.e., CCW
|
|
rotation_mode = framebuffer.ORIENTATION_LANDSCAPE_ROTATED
|
|
screen_mode = 'landscape'
|
|
end
|
|
else
|
|
if ev.value == DEVICE_ORIENTATION_PORTRAIT
|
|
or ev.value == DEVICE_ORIENTATION_PORTRAIT_LEFT
|
|
or ev.value == DEVICE_ORIENTATION_PORTRAIT_RIGHT then
|
|
-- i.e., UR
|
|
rotation_mode = framebuffer.ORIENTATION_PORTRAIT
|
|
screen_mode = 'portrait'
|
|
elseif ev.value == DEVICE_ORIENTATION_LANDSCAPE then
|
|
-- i.e., CW
|
|
rotation_mode = framebuffer.ORIENTATION_LANDSCAPE
|
|
screen_mode = 'landscape'
|
|
elseif ev.value == DEVICE_ORIENTATION_PORTRAIT_ROTATED
|
|
or ev.value == DEVICE_ORIENTATION_PORTRAIT_ROTATED_LEFT
|
|
or ev.value == DEVICE_ORIENTATION_PORTRAIT_ROTATED_RIGHT then
|
|
-- i.e., UD
|
|
rotation_mode = framebuffer.ORIENTATION_PORTRAIT_ROTATED
|
|
screen_mode = 'portrait'
|
|
elseif ev.value == DEVICE_ORIENTATION_LANDSCAPE_ROTATED then
|
|
-- i.e., CCW
|
|
rotation_mode = framebuffer.ORIENTATION_LANDSCAPE_ROTATED
|
|
screen_mode = 'landscape'
|
|
end
|
|
end
|
|
|
|
local old_rotation_mode = self.device.screen:getRotationMode()
|
|
if self.device:isGSensorLocked() then
|
|
local old_screen_mode = self.device.screen:getScreenMode()
|
|
if rotation_mode ~= old_rotation_mode and screen_mode == old_screen_mode then
|
|
-- Cheaper than a full SetRotationMode event, as we don't need to re-layout anything.
|
|
self.device.screen:setRotationMode(rotation_mode)
|
|
local UIManager = require("ui/uimanager")
|
|
UIManager:onRotation()
|
|
end
|
|
else
|
|
if rotation_mode ~= old_rotation_mode then
|
|
return Event:new("SetRotationMode", rotation_mode)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Accelerometer on the Forma/Libra
|
|
function Input:handleMiscEvNTX(ev)
|
|
local rotation_mode, screen_mode
|
|
if ev.code == C.MSC_RAW then
|
|
if ev.value == MSC_RAW_GSENSOR_PORTRAIT_UP then
|
|
-- i.e., UR
|
|
rotation_mode = framebuffer.ORIENTATION_PORTRAIT
|
|
screen_mode = 'portrait'
|
|
elseif ev.value == MSC_RAW_GSENSOR_LANDSCAPE_RIGHT then
|
|
-- i.e., CW
|
|
rotation_mode = framebuffer.ORIENTATION_LANDSCAPE
|
|
screen_mode = 'landscape'
|
|
elseif ev.value == MSC_RAW_GSENSOR_PORTRAIT_DOWN then
|
|
-- i.e., UD
|
|
rotation_mode = framebuffer.ORIENTATION_PORTRAIT_ROTATED
|
|
screen_mode = 'portrait'
|
|
elseif ev.value == MSC_RAW_GSENSOR_LANDSCAPE_LEFT then
|
|
-- i.e., CCW
|
|
rotation_mode = framebuffer.ORIENTATION_LANDSCAPE_ROTATED
|
|
screen_mode = 'landscape'
|
|
else
|
|
-- Discard FRONT/BACK
|
|
return
|
|
end
|
|
else
|
|
-- Discard unhandled event codes, just to future-proof this ;).
|
|
return
|
|
end
|
|
|
|
local old_rotation_mode = self.device.screen:getRotationMode()
|
|
if self.device:isGSensorLocked() then
|
|
local old_screen_mode = self.device.screen:getScreenMode()
|
|
if rotation_mode and rotation_mode ~= old_rotation_mode and screen_mode == old_screen_mode then
|
|
-- Cheaper than a full SetRotationMode event, as we don't need to re-layout anything.
|
|
self.device.screen:setRotationMode(rotation_mode)
|
|
local UIManager = require("ui/uimanager")
|
|
UIManager:onRotation()
|
|
end
|
|
else
|
|
if rotation_mode and rotation_mode ~= old_rotation_mode then
|
|
return Event:new("SetRotationMode", rotation_mode)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Allow toggling the accelerometer at runtime.
|
|
function Input:toggleMiscEvNTX(toggle)
|
|
if toggle == true then
|
|
-- Honor Gyro events
|
|
if not self.isNTXAccelHooked then
|
|
self.handleMiscEv = self.handleMiscEvNTX
|
|
self.isNTXAccelHooked = true
|
|
end
|
|
elseif toggle == false then
|
|
-- Ignore Gyro events
|
|
if self.isNTXAccelHooked then
|
|
self.handleMiscEv = self.voidEv
|
|
self.isNTXAccelHooked = false
|
|
end
|
|
else
|
|
-- Toggle it
|
|
if self.isNTXAccelHooked then
|
|
self.handleMiscEv = self.voidEv
|
|
else
|
|
self.handleMiscEv = self.handleMiscEvNTX
|
|
end
|
|
|
|
self.isNTXAccelHooked = not self.isNTXAccelHooked
|
|
end
|
|
end
|
|
|
|
-- helpers for touch event data management:
|
|
|
|
function Input:initMtSlot(slot)
|
|
if not self.ev_slots[slot] then
|
|
self.ev_slots[slot] = {
|
|
slot = slot
|
|
}
|
|
end
|
|
end
|
|
|
|
function Input:setMtSlot(slot, key, val)
|
|
self.ev_slots[slot][key] = val
|
|
end
|
|
|
|
function Input:setCurrentMtSlot(key, val)
|
|
self:setMtSlot(self.cur_slot, key, val)
|
|
end
|
|
|
|
-- Same as above, but ensures the current slot actually has a live ref first
|
|
function Input:setCurrentMtSlotChecked(key, val)
|
|
if #self.MTSlots == 0 then
|
|
self:addSlot(self.cur_slot)
|
|
end
|
|
|
|
self:setMtSlot(self.cur_slot, key, val)
|
|
end
|
|
|
|
function Input:getMtSlot(slot)
|
|
return self.ev_slots[slot]
|
|
end
|
|
|
|
function Input:getCurrentMtSlot()
|
|
return self:getMtSlot(self.cur_slot)
|
|
end
|
|
|
|
function Input:getCurrentMtSlotData(key)
|
|
local slot = self:getCurrentMtSlot()
|
|
if slot then
|
|
return slot[key]
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function Input:newFrame()
|
|
-- Array of references to the data for each slot seen in this input frame
|
|
-- (Points to self.ev_slots, c.f., getMtSlot)
|
|
self.MTSlots = {}
|
|
-- Simple hash to keep track of which references we've inserted into self.MTSlots (keys are slot numbers)
|
|
self.active_slots = {}
|
|
end
|
|
|
|
function Input:addSlot(value)
|
|
self:initMtSlot(value)
|
|
table.insert(self.MTSlots, self:getMtSlot(value))
|
|
self.active_slots[value] = true
|
|
self.cur_slot = value
|
|
end
|
|
|
|
function Input:addSlotIfChanged(value)
|
|
if self.cur_slot ~= value then
|
|
-- We've already seen that slot in this frame, don't insert a duplicate reference!
|
|
if self.active_slots[value] then
|
|
self.cur_slot = value
|
|
else
|
|
self:addSlot(value)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:setupSlotData(value)
|
|
if #self.MTSlots == 0 then
|
|
self:addSlot(value)
|
|
else
|
|
self:addSlotIfChanged(value)
|
|
end
|
|
end
|
|
|
|
function Input:isEvKeyPress(ev)
|
|
return ev.value == EVENT_VALUE_KEY_PRESS
|
|
end
|
|
|
|
function Input:isEvKeyRepeat(ev)
|
|
return ev.value == EVENT_VALUE_KEY_REPEAT
|
|
end
|
|
|
|
function Input:isEvKeyRelease(ev)
|
|
return ev.value == EVENT_VALUE_KEY_RELEASE
|
|
end
|
|
|
|
|
|
--- Main event handling.
|
|
-- `now` corresponds to UIManager:getTime() (an fts time), and it's just been updated by UIManager.
|
|
-- `deadline` (an fts time) is the absolute deadline imposed by UIManager:handleInput() (a.k.a., our main event loop ^^):
|
|
-- it's either nil (meaning block forever waiting for input), or the earliest UIManager deadline (in most cases, that's the next scheduled task,
|
|
-- in much less common cases, that's the earliest of UIManager.INPUT_TIMEOUT (currently, only KOSync ever sets it) or UIManager.ZMQ_TIMEOUT if there are pending ZMQs).
|
|
function Input:waitEvent(now, deadline)
|
|
-- On the first iteration of the loop, we don't need to update now, we're following closely (a couple ms at most) behind UIManager.
|
|
local ok, ev
|
|
-- Wrapper around the platform-specific input.waitForEvent (which itself is generally poll-like, and supposed to poll *once*).
|
|
-- Speaking of input.waitForEvent, it can return:
|
|
-- * true, ev: When a batch of input events was read.
|
|
-- ev is an array of event tables, themselves mapped after the input_event <linux/input.h> struct.
|
|
-- * false, errno, timerfd: When no input event was read, possibly for benign reasons.
|
|
-- One such common case is after a polling timeout, in which case errno is C.ETIME.
|
|
-- If the timerfd backend is in use, and the early return was caused by a timerfd expiring,
|
|
-- it returns false, C.ETIME, timerfd; where timerfd is a C pointer (i.e., light userdata)
|
|
-- to the timerfd node that expired (so as to be able to free it later, c.f., input/timerfd-callbacks.h).
|
|
-- Otherwise, errno is the actual error code from the backend (e.g., select's errno for the C backend).
|
|
-- * nil: When something terrible happened (e.g., fatal poll/read failure). We abort in such cases.
|
|
while true do
|
|
if self.timer_callbacks[1] then
|
|
-- If we have timers set, we need to honor them once we're done draining the input events.
|
|
while self.timer_callbacks[1] do
|
|
-- Choose the earliest deadline between the next timer deadline, and our full timeout deadline.
|
|
local deadline_is_timer = false
|
|
local with_timerfd = false
|
|
local poll_deadline
|
|
-- If the timer's deadline is handled via timerfd, that's easy
|
|
if self.timer_callbacks[1].timerfd then
|
|
-- We use the ultimate deadline, as the kernel will just signal us when the timer expires during polling.
|
|
poll_deadline = deadline
|
|
with_timerfd = true
|
|
else
|
|
if not deadline then
|
|
-- If we don't actually have a full timeout deadline, just honor the timer's.
|
|
poll_deadline = self.timer_callbacks[1].deadline
|
|
deadline_is_timer = true
|
|
else
|
|
if self.timer_callbacks[1].deadline < deadline then
|
|
poll_deadline = self.timer_callbacks[1].deadline
|
|
deadline_is_timer = true
|
|
else
|
|
poll_deadline = deadline
|
|
end
|
|
end
|
|
end
|
|
local poll_timeout
|
|
-- With the timerfd backend, poll_deadline is set to deadline, which might be nil, in which case,
|
|
-- we can happily block forever, like in the no timer_callbacks branch below ;).
|
|
if poll_deadline then
|
|
-- If we haven't hit that deadline yet, poll until it expires, otherwise,
|
|
-- have select return immediately so that we trip a timeout.
|
|
now = now or time.now()
|
|
if poll_deadline > now then
|
|
-- Deadline hasn't been blown yet, honor it.
|
|
poll_timeout = poll_deadline - now
|
|
else
|
|
-- We've already blown the deadline: make select return immediately (most likely straight to timeout).
|
|
-- NOTE: With the timerfd backend, this is sometimes a tad optimistic,
|
|
-- as we may in fact retry for a few iterations while waiting for the timerfd to actually expire.
|
|
poll_timeout = 0
|
|
end
|
|
end
|
|
|
|
local timerfd
|
|
local sec, usec = time.split_s_us(poll_timeout)
|
|
ok, ev, timerfd = input.waitForEvent(sec, usec)
|
|
-- We got an actual input event, go and process it
|
|
if ok then break end
|
|
|
|
-- If we've drained all pending input events, causing waitForEvent to time out, check our timers
|
|
if ok == false and ev == C.ETIME then
|
|
-- Check whether the earliest timer to finalize a Gesture detection is up.
|
|
local consume_callback = false
|
|
if timerfd then
|
|
-- If we were woken up by a timerfd, that means the timerfd backend is in use, of course,
|
|
-- and it also means that we're guaranteed to have reached its deadline.
|
|
consume_callback = true
|
|
elseif not with_timerfd then
|
|
-- On systems where the timerfd backend is *NOT* in use, we have a few more cases to handle...
|
|
if deadline_is_timer then
|
|
-- We're only guaranteed to have blown the timer's deadline
|
|
-- when our actual select deadline *was* the timer's!
|
|
consume_callback = true
|
|
elseif time.now() >= self.timer_callbacks[1].deadline then
|
|
-- But if it was a task deadline instead, we to have to check the timer's against the current time,
|
|
-- to double-check whether we blew it or not.
|
|
consume_callback = true
|
|
end
|
|
end
|
|
|
|
if consume_callback then
|
|
local touch_ges
|
|
local timer_idx = 1
|
|
if timerfd then
|
|
-- If there's a deadline collision, make sure we call the callback that matches the timerfd returned.
|
|
-- We'll handle the next one on the next iteration, as an expired timerfd will ensure
|
|
-- that select will return immediately.
|
|
for i, item in ipairs(self.timer_callbacks) do
|
|
if item.timerfd == timerfd then
|
|
-- In the vast majority of cases, we should find our match on the first entry ;).
|
|
timer_idx = i
|
|
touch_ges = item.callback()
|
|
break
|
|
end
|
|
end
|
|
else
|
|
-- If there's a deadline collision, we'll just handle the next one on the next iteration,
|
|
-- because the blown deadline means we'll have asked waitForEvent to return immediately.
|
|
touch_ges = self.timer_callbacks[1].callback()
|
|
end
|
|
|
|
-- Cleanup after the timer callback.
|
|
-- GestureDetector has guards in place to avoid double frees in case the callback itself
|
|
-- affected the timerfd or timer_callbacks list (e.g., by dropping a contact).
|
|
if timerfd then
|
|
input.clearTimer(timerfd)
|
|
end
|
|
table.remove(self.timer_callbacks, timer_idx)
|
|
|
|
if touch_ges then
|
|
self:gestureAdjustHook(touch_ges)
|
|
return {
|
|
Event:new("Gesture", self.gesture_detector:adjustGesCoordinate(touch_ges))
|
|
}
|
|
end -- if touch_ges
|
|
end -- if poll_deadline reached
|
|
else
|
|
-- Something went wrong, jump to error handling *now*
|
|
break
|
|
end -- if poll returned ETIME
|
|
|
|
-- Refresh now on the next iteration (e.g., when we have multiple timers to check, and we've just timed out)
|
|
now = nil
|
|
end -- while #timer_callbacks > 0
|
|
else
|
|
-- If there aren't any timers, just block for the requested amount of time.
|
|
-- deadline may be nil, in which case waitForEvent blocks indefinitely (i.e., until the next input event ;)).
|
|
local poll_timeout
|
|
-- If UIManager put us on deadline, enforce it, otherwise, block forever.
|
|
if deadline then
|
|
-- Convert that absolute deadline to value relative to *now*, as we may loop multiple times between UI ticks.
|
|
now = now or time.now()
|
|
if deadline > now then
|
|
-- Deadline hasn't been blown yet, honor it.
|
|
poll_timeout = deadline - now
|
|
else
|
|
-- Deadline has been blown: make select return immediately.
|
|
poll_timeout = 0
|
|
end
|
|
end
|
|
|
|
local sec, usec = time.split_s_us(poll_timeout)
|
|
ok, ev = input.waitForEvent(sec, usec)
|
|
end -- if #timer_callbacks > 0
|
|
|
|
-- Handle errors
|
|
if ok then
|
|
-- We're good, process the event and go back to UIManager.
|
|
break
|
|
elseif ok == false then
|
|
if ev == C.ETIME then
|
|
-- Don't report an error on ETIME, and go back to UIManager
|
|
ev = nil
|
|
break
|
|
elseif ev == C.EINTR then -- luacheck: ignore
|
|
-- Retry on EINTR
|
|
else
|
|
-- Warn, report, and go back to UIManager
|
|
logger.warn("Polling for input events returned an error:", ev, "->", ffi.string(C.strerror(ev)))
|
|
break
|
|
end
|
|
elseif ok == nil then
|
|
-- Something went horribly wrong, abort.
|
|
logger.err("Polling for input events failed catastrophically")
|
|
local UIManager = require("ui/uimanager")
|
|
UIManager:abort()
|
|
break
|
|
end
|
|
|
|
-- We'll need to refresh now on the next iteration, if there is one.
|
|
now = nil
|
|
end
|
|
|
|
if ok and ev then
|
|
local handled = {}
|
|
-- We're guaranteed that ev is an array of event tables. Might be an array of *one* event, but an array nonetheless ;).
|
|
for __, event in ipairs(ev) do
|
|
if DEBUG.is_on then
|
|
-- NOTE: This is rather spammy and computationally intensive,
|
|
-- and we can't conditionally prevent evalutation of function arguments,
|
|
-- so, just hide the whole thing behind a branch ;).
|
|
if event.type == C.EV_KEY then
|
|
logger.dbg(string.format(
|
|
"key event => code: %d (%s), value: %s, time: %d.%06d",
|
|
event.code, self.event_map[event.code] or linux_evdev_key_code_map[event.code], event.value,
|
|
event.time.sec, event.time.usec))
|
|
elseif event.type == C.EV_SYN then
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%06d",
|
|
event.type, linux_evdev_type_map[event.type], event.code, linux_evdev_syn_code_map[event.code], event.value,
|
|
event.time.sec, event.time.usec))
|
|
elseif event.type == C.EV_ABS then
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%06d",
|
|
event.type, linux_evdev_type_map[event.type], event.code, linux_evdev_abs_code_map[event.code], event.value,
|
|
event.time.sec, event.time.usec))
|
|
elseif event.type == C.EV_MSC then
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%06d",
|
|
event.type, linux_evdev_type_map[event.type], event.code, linux_evdev_msc_code_map[event.code], event.value,
|
|
event.time.sec, event.time.usec))
|
|
elseif event.type == C.EV_REP then
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%06d",
|
|
event.type, linux_evdev_type_map[event.type], event.code, linux_evdev_rep_code_map[event.code], event.value,
|
|
event.time.sec, event.time.usec))
|
|
else
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d, value: %s, time: %d.%06d",
|
|
event.type, linux_evdev_type_map[event.type], event.code, event.value,
|
|
event.time.sec, event.time.usec))
|
|
end
|
|
end
|
|
self:eventAdjustHook(event)
|
|
if event.type == C.EV_KEY then
|
|
local handled_ev = self:handleKeyBoardEv(event)
|
|
if handled_ev then
|
|
table.insert(handled, handled_ev)
|
|
end
|
|
elseif event.type == C.EV_ABS and event.code == ABS_OASIS_ORIENTATION then
|
|
local handled_ev = self:handleOasisOrientationEv(event)
|
|
if handled_ev then
|
|
table.insert(handled, handled_ev)
|
|
end
|
|
elseif event.type == C.EV_ABS or event.type == C.EV_SYN then
|
|
local handled_evs = self:handleTouchEv(event)
|
|
-- handleTouchEv only returns an array of Events once it gets a SYN_REPORT,
|
|
-- so more often than not, we just get a nil here ;).
|
|
if handled_evs then
|
|
for _, handled_ev in ipairs(handled_evs) do
|
|
table.insert(handled, handled_ev)
|
|
end
|
|
end
|
|
elseif event.type == C.EV_MSC then
|
|
local handled_ev = self:handleMiscEv(event)
|
|
if handled_ev then
|
|
table.insert(handled, handled_ev)
|
|
end
|
|
elseif event.type == C.EV_SDL then
|
|
local handled_ev = self:handleSdlEv(event)
|
|
if handled_ev then
|
|
table.insert(handled, handled_ev)
|
|
end
|
|
else
|
|
-- Received some other kind of event that we do not know how to specifically handle yet
|
|
local handled_ev = self:handleGenericEv(event)
|
|
if handled_ev then
|
|
table.insert(handled, handled_ev)
|
|
end
|
|
end
|
|
end
|
|
return handled
|
|
elseif ok == false and ev then
|
|
return {
|
|
Event:new("InputError", ev)
|
|
}
|
|
elseif ok == nil then
|
|
-- No ok and no ev? Hu oh...
|
|
return {
|
|
Event:new("InputError", "Catastrophic")
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Allow toggling the handling of most every kind of input, except for power management related events.
|
|
function Input:inhibitInput(toggle)
|
|
if toggle then
|
|
-- Only handle power management events
|
|
if not self._key_ev_handler then
|
|
logger.info("Inhibiting user input")
|
|
self._key_ev_handler = self.handleKeyBoardEv
|
|
self.handleKeyBoardEv = self.handlePowerManagementOnlyEv
|
|
end
|
|
-- And send everything else to the void
|
|
if not self._oasis_ev_handler then
|
|
self._oasis_ev_handler = self.handleOasisOrientationEv
|
|
self.handleOasisOrientationEv = self.voidEv
|
|
end
|
|
if not self._abs_ev_handler then
|
|
self._abs_ev_handler = self.handleTouchEv
|
|
self.handleTouchEv = self.voidEv
|
|
end
|
|
if not self._msc_ev_handler then
|
|
if not self.device:isPocketBook() and not self.device:isAndroid() then
|
|
-- NOTE: PocketBook is a special snowflake, synthetic Power events are sent as EV_MSC.
|
|
-- Thankfully, that's all that EV_MSC is used for on that platform.
|
|
-- NOTE: Android, on the other hand, handles a *lot* of critical stuff over EV_MSC,
|
|
-- as it's used to communicate between Android and Lua land ;).
|
|
self._msc_ev_handler = self.handleMiscEv
|
|
self.handleMiscEv = self.voidEv
|
|
end
|
|
end
|
|
if not self._sdl_ev_handler then
|
|
self._sdl_ev_handler = self.handleSdlEv
|
|
self.handleSdlEv = self.voidEv
|
|
end
|
|
if not self._generic_ev_handler then
|
|
self._generic_ev_handler = self.handleGenericEv
|
|
self.handleGenericEv = self.voidEv
|
|
end
|
|
|
|
-- Reset gesture detection state to a blank slate, to avoid bogus gesture detection on restore.
|
|
self:resetState()
|
|
else
|
|
-- Restore event handlers, if any
|
|
if self._key_ev_handler then
|
|
logger.info("Restoring user input handling")
|
|
self.handleKeyBoardEv = self._key_ev_handler
|
|
self._key_ev_handler = nil
|
|
end
|
|
if self._oasis_ev_handler then
|
|
self.handleOasisOrientationEv = self._oasis_ev_handler
|
|
self._oasis_ev_handler = nil
|
|
end
|
|
if self._abs_ev_handler then
|
|
self.handleTouchEv = self._abs_ev_handler
|
|
self._abs_ev_handler = nil
|
|
end
|
|
if self._msc_ev_handler then
|
|
self.handleMiscEv = self._msc_ev_handler
|
|
self._msc_ev_handler = nil
|
|
end
|
|
if self._sdl_ev_handler then
|
|
self.handleSdlEv = self._sdl_ev_handler
|
|
self._sdl_ev_handler = nil
|
|
end
|
|
if self._generic_ev_handler then
|
|
self.handleGenericEv = self._generic_ev_handler
|
|
self._generic_ev_handler = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Request all input events to be ignored for some duration.
|
|
|
|
@param set_or_seconds either `true`, in which case a platform-specific delay is chosen, or a duration in seconds (***int***).
|
|
]]
|
|
function Input:inhibitInputUntil(set_or_seconds)
|
|
local UIManager = require("ui/uimanager")
|
|
UIManager:unschedule(self._inhibitInputUntil_func)
|
|
if not set_or_seconds then -- remove any previously set
|
|
self:inhibitInput(false)
|
|
return
|
|
end
|
|
local delay_s
|
|
if set_or_seconds == true then
|
|
-- Use an adequate delay to account for device refresh duration
|
|
-- so any events happening in this delay (ie. before a widget
|
|
-- is really painted on screen) are discarded.
|
|
if self.device:hasEinkScreen() then
|
|
-- A screen refresh can take a few 100ms,
|
|
-- sometimes > 500ms on some devices/temperatures.
|
|
-- So, block for 400ms (to have it displayed) + 400ms
|
|
-- for user reaction to it
|
|
delay_s = 0.8
|
|
else
|
|
-- On non-eInk screen, display is usually instantaneous
|
|
delay_s = 0.4
|
|
end
|
|
else -- we expect a number
|
|
delay_s = set_or_seconds
|
|
end
|
|
UIManager:scheduleIn(delay_s, self._inhibitInputUntil_func)
|
|
self:inhibitInput(true)
|
|
end
|
|
|
|
return Input
|