mirror of
https://github.com/koreader/koreader
synced 2024-11-18 03:25:46 +00:00
2678d2c49b
* Kobo: Switch ST devices to a dedicated input handler Instead of shoehorning a hack into the standard handler. * Use setCurrentMtSlotChecked in handleTouchEvLegacy
1468 lines
59 KiB
Lua
1468 lines
59 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"
|
|
},
|
|
},
|
|
|
|
-- 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,
|
|
},
|
|
|
|
-- 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"
|
|
|
|
-- 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, width)
|
|
if ev.type == C.EV_ABS
|
|
and (ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X) then
|
|
ev.value = width - ev.value
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchMirrorY(ev, height)
|
|
if ev.type == C.EV_ABS
|
|
and (ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y) then
|
|
ev.value = height - 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:clearStates()
|
|
-- 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
|
|
|
|
-- fake events
|
|
if keycode == "IntoSS" or keycode == "OutOfSS"
|
|
or keycode == "UsbPlugIn" or keycode == "UsbPlugOut"
|
|
or keycode == "Charging" or keycode == "NotCharging"
|
|
or keycode == "WakeupFromSuspend" or keycode == "ReadyToSuspend" 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
|
|
|
|
-- Fake events
|
|
if keycode == "IntoSS" or keycode == "OutOfSS"
|
|
or keycode == "UsbPlugIn" or keycode == "UsbPlugOut"
|
|
or keycode == "Charging" or keycode == "NotCharging"
|
|
or keycode == "WakeupFromSuspend" or keycode == "ReadyToSuspend" 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)
|
|
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)
|
|
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
|
|
|
|
-- NOTE: If it was a timerfd, we *may* also need to close the fd.
|
|
-- GestureDetector only calls Input:setTimeout for "hold" & "double_tap" gestures.
|
|
-- For double taps, the callback itself doesn't interact with the timer_callbacks list,
|
|
-- but for holds, it *will* call GestureDetector:clearState on "hold_release" (and *only* then),
|
|
-- and *that* already takes care of pop'ping the (hold) timer and closing the fd,
|
|
-- via Input:clearTimeout(slot, "hold")...
|
|
if not touch_ges or touch_ges.ges ~= "hold_release" then
|
|
-- That leaves explicit cleanup to every other case (i.e., nil or every other gesture)
|
|
if timerfd then
|
|
input.clearTimer(timerfd)
|
|
end
|
|
table.remove(self.timer_callbacks, timer_idx)
|
|
end
|
|
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 ;).
|
|
DEBUG:logEv(event)
|
|
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
|