mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
* ReaderDictionary: Port delay computations to TimeVal
* ReaderHighlight: Port delay computations to TimeVal
* ReaderView: Port delay computations to TimeVal
* Android: Reset gesture detection state on APP_CMD_TERM_WINDOW.
This prevents potentially being stuck in bogus gesture states when switching apps.
* GestureDetector:
* Port delay computations to TimeVal
* Fixed delay computations to handle time warps (large and negative deltas).
* Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture.
* Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1.
* Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy.
* The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events.
The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use
a clock source that is *NOT* MONOTONIC.
AFAICT, that's pretty much... PocketBook, and that's it?
* Input:
* Use the <linux/input.h> FFI module instead of re-declaring every constant
* Fixed (verbose) debug logging of input events to actually translate said constants properly.
* Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume.
* Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient.
Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts,
as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector.
* reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary.
* TimeVal:
* Refactored and simplified, especially as far as metamethods are concerned (based on <bsd/sys/time.h>).
* Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC.
* Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence.
* New methods:
* Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime
* Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero
* UIManager:
* Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base.
This ensures reliable and consistent scheduling, as time is ensured never to go backwards.
* Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame.
It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value,
because very few time has passed.
The only code left that does live syscalls does it because it's actually necessary for accuracy,
and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics).
* DictQuickLookup: Port delay computations to TimeVal
* FootNoteWidget: Port delay computations to TimeVal
* HTMLBoxWidget: Port delay computations to TimeVal
* Notification: Port delay computations to TimeVal
* TextBoxWidget: Port delay computations to TimeVal
* AutoSuspend: Port to TimeVal
* AutoTurn:
* Fix it so that settings are actually honored.
* Port to TimeVal
* BackgroundRunner: Port to TimeVal
* Calibre: Port benchmarking code to TimeVal
* BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly.
* All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries..
1074 lines
42 KiB
Lua
1074 lines
42 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 TimeVal = require("ui/timeval")
|
|
local framebuffer = require("ffi/framebuffer")
|
|
local input = require("ffi/input")
|
|
local logger = require("logger")
|
|
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
|
|
|
|
-- For the events of the Forma accelerometer (MSC.value)
|
|
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
|
|
|
|
-- 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_abs_code_map = {
|
|
[C.ABS_X] = "ABS_X",
|
|
[C.ABS_Y] = "ABS_Y",
|
|
[C.ABS_PRESSURE] = "ABS_PRESSURE",
|
|
[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_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",
|
|
}
|
|
-- luacheck: pop
|
|
|
|
local _internal_clipboard_text = nil -- holds the last copied text
|
|
|
|
local Input = {
|
|
-- 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 = {},
|
|
|
|
group = {
|
|
Cursor = { "Up", "Down", "Left", "Right" },
|
|
PgFwd = { "RPgFwd", "LPgFwd" },
|
|
PgBack = { "RPgBack", "LPgBack" },
|
|
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 = {},
|
|
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:
|
|
cur_slot = 0,
|
|
MTSlots = {},
|
|
ev_slots = {
|
|
[0] = {
|
|
slot = 0,
|
|
}
|
|
},
|
|
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()
|
|
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"
|
|
|
|
-- 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
|
|
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)
|
|
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 = TimeVal:now() + delay
|
|
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.
|
|
timerfd = input.setTimer(clock_id, deadline.sec, deadline.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 = TimeVal: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)
|
|
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" 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
|
|
|
|
-- 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 Device = require("frontend/device")
|
|
local UIManager = require("ui/uimanager")
|
|
|
|
local save_quit = function()
|
|
Device:saveSettings()
|
|
UIManager:quit()
|
|
end
|
|
UIManager:broadcastEvent(Event:new("Exit", save_quit))
|
|
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 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
|
|
|
|
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
|
|
if #self.MTSlots == 0 then
|
|
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
|
|
end
|
|
if ev.code == C.ABS_MT_SLOT then
|
|
self:addSlotIfChanged(ev.value)
|
|
elseif ev.code == C.ABS_MT_TRACKING_ID then
|
|
if self.snow_protocol then
|
|
self:addSlotIfChanged(ev.value)
|
|
end
|
|
self:setCurrentMtSlot("id", ev.value)
|
|
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)
|
|
|
|
-- code to emulate mt protocol on kobos
|
|
-- we "confirm" abs_x, abs_y only when pressure ~= 0
|
|
elseif ev.code == C.ABS_X then
|
|
self:setCurrentMtSlot("abs_x", ev.value)
|
|
elseif ev.code == C.ABS_Y then
|
|
self:setCurrentMtSlot("abs_y", ev.value)
|
|
elseif ev.code == C.ABS_PRESSURE then
|
|
if ev.value ~= 0 then
|
|
self:setCurrentMtSlot("id", 1)
|
|
self:confirmAbsxy()
|
|
else
|
|
self:cleanAbsxy()
|
|
self:setCurrentMtSlot("id", -1)
|
|
end
|
|
end
|
|
elseif ev.type == C.EV_SYN then
|
|
if ev.code == C.SYN_REPORT then
|
|
for _, MTSlot in pairs(self.MTSlots) do
|
|
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
|
|
if self.snow_protocol then
|
|
-- if a slot appears in the current touch event, set "used"
|
|
self:setMtSlot(MTSlot.slot, "used", true)
|
|
end
|
|
end
|
|
if self.snow_protocol then
|
|
-- reset every slot that doesn't appear in the current touch event
|
|
-- (because on the H2O2, this is the only way we detect finger-up)
|
|
self.MTSlots = {}
|
|
for _, slot in pairs(self.ev_slots) do
|
|
table.insert(self.MTSlots, slot)
|
|
if not slot.used then
|
|
slot.id = -1
|
|
slot.timev = TimeVal:new(ev.time)
|
|
end
|
|
end
|
|
end
|
|
-- feed ev in all slots to state machine
|
|
local touch_ges = self.gesture_detector:feedEvent(self.MTSlots)
|
|
self.MTSlots = {}
|
|
if self.snow_protocol then
|
|
-- go through all the ev_slots and clear used
|
|
for _, slot in pairs(self.ev_slots) do
|
|
slot.used = nil
|
|
end
|
|
end
|
|
if touch_ges then
|
|
self:gestureAdjustHook(touch_ges)
|
|
return Event:new("Gesture",
|
|
self.gesture_detector:adjustGesCoordinate(touch_ges)
|
|
)
|
|
end
|
|
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 #self.MTSlots == 0 then
|
|
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
|
|
end
|
|
if ev.code == C.ABS_MT_TRACKING_ID then
|
|
self:addSlotIfChanged(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 pairs(self.MTSlots) do
|
|
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
|
|
end
|
|
-- feed ev in all slots to state machine
|
|
local touch_ges = self.gesture_detector:feedEvent(self.MTSlots)
|
|
self.MTSlots = {}
|
|
if touch_ges then
|
|
self:gestureAdjustHook(touch_ges)
|
|
return Event:new("Gesture",
|
|
self.gesture_detector:adjustGesCoordinate(touch_ges)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
function Input:handleTouchEvLegacy(ev)
|
|
-- Single Touch Protocol. Some devices emit both singletouch and multitouch events.
|
|
-- In those devices the 'handleTouchEv' function doesn't work as expected. Use this function instead.
|
|
if ev.type == C.EV_ABS then
|
|
if #self.MTSlots == 0 then
|
|
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
|
|
end
|
|
if ev.code == C.ABS_X then
|
|
self:setCurrentMtSlot("x", ev.value)
|
|
elseif ev.code == C.ABS_Y then
|
|
self:setCurrentMtSlot("y", ev.value)
|
|
elseif ev.code == C.ABS_PRESSURE then
|
|
if ev.value ~= 0 then
|
|
self:setCurrentMtSlot("id", 1)
|
|
else
|
|
self:setCurrentMtSlot("id", -1)
|
|
end
|
|
end
|
|
elseif ev.type == C.EV_SYN then
|
|
if ev.code == C.SYN_REPORT then
|
|
for _, MTSlot in pairs(self.MTSlots) do
|
|
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
|
|
end
|
|
|
|
-- feed ev in all slots to state machine
|
|
local touch_ges = self.gesture_detector:feedEvent(self.MTSlots)
|
|
self.MTSlots = {}
|
|
if touch_ges then
|
|
self:gestureAdjustHook(touch_ges)
|
|
return Event:new("Gesture",
|
|
self.gesture_detector:adjustGesCoordinate(touch_ges)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:handleOasisOrientationEv(ev)
|
|
local rotation_mode, screen_mode
|
|
if ev.value == DEVICE_ORIENTATION_PORTRAIT
|
|
or ev.value == DEVICE_ORIENTATION_PORTRAIT_LEFT
|
|
or ev.value == DEVICE_ORIENTATION_PORTRAIT_RIGHT then
|
|
rotation_mode = framebuffer.ORIENTATION_PORTRAIT
|
|
screen_mode = 'portrait'
|
|
elseif ev.value == DEVICE_ORIENTATION_LANDSCAPE then
|
|
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
|
|
rotation_mode = framebuffer.ORIENTATION_PORTRAIT_ROTATED
|
|
screen_mode = 'portrait'
|
|
elseif ev.value == DEVICE_ORIENTATION_LANDSCAPE_ROTATED then
|
|
rotation_mode = framebuffer.ORIENTATION_LANDSCAPE_ROTATED
|
|
screen_mode = 'landscape'
|
|
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, c.f., drivers/hwmon/mma8x5x.c
|
|
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 = function() end
|
|
self.isNTXAccelHooked = false
|
|
end
|
|
else
|
|
-- Toggle it
|
|
if self.isNTXAccelHooked then
|
|
self.handleMiscEv = function() end
|
|
else
|
|
self.handleMiscEv = self.handleMiscEvNTX
|
|
end
|
|
|
|
self.isNTXAccelHooked = not self.isNTXAccelHooked
|
|
end
|
|
end
|
|
|
|
-- helpers for touch event data management:
|
|
|
|
function Input:setMtSlot(slot, key, val)
|
|
if not self.ev_slots[slot] then
|
|
self.ev_slots[slot] = {
|
|
slot = slot
|
|
}
|
|
end
|
|
|
|
self.ev_slots[slot][key] = val
|
|
end
|
|
|
|
function Input:setCurrentMtSlot(key, val)
|
|
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:addSlotIfChanged(value)
|
|
if self.cur_slot ~= value then
|
|
table.insert(self.MTSlots, self:getMtSlot(value))
|
|
end
|
|
self.cur_slot = value
|
|
end
|
|
|
|
function Input:confirmAbsxy()
|
|
self:setCurrentMtSlot("x", self.ev_slots[self.cur_slot]["abs_x"])
|
|
self:setCurrentMtSlot("y", self.ev_slots[self.cur_slot]["abs_y"])
|
|
end
|
|
|
|
function Input:cleanAbsxy()
|
|
self:setCurrentMtSlot("abs_x", nil)
|
|
self:setCurrentMtSlot("abs_y", nil)
|
|
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() (a TimeVal), and it's just been updated by UIManager.
|
|
-- `deadline` (a TimeVal) 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).
|
|
-- Speaking of input.waitForEvent, it can return:
|
|
-- * true, ev: When an input event was read. ev is a table 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 > 0 then
|
|
-- If we have timers set, we need to honor them once we're done draining the input events.
|
|
while #self.timer_callbacks > 0 do
|
|
-- Choose the earliest deadline between the next timer deadline, and our full timeout deadline.
|
|
local deadline_is_timer = 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
|
|
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 TimeVal: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 = TimeVal:new{ sec = 0 }
|
|
end
|
|
end
|
|
|
|
local timerfd
|
|
ok, ev, timerfd = input.waitForEvent(poll_timeout and poll_timeout.sec, poll_timeout and poll_timeout.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.
|
|
-- If we were woken up by a timerfd, or if our actual select deadline was the timer itself,
|
|
-- we're guaranteed to have reached it.
|
|
-- But if it was a task deadline instead, we to have to check it against the current time.
|
|
if timerfd or (deadline_is_timer or TimeVal:now() >= self.timer_callbacks[1].deadline) then
|
|
local touch_ges = self.timer_callbacks[1].callback()
|
|
table.remove(self.timer_callbacks, 1)
|
|
-- If it was a timerfd, we also need to close the fd.
|
|
-- NOTE: The fact that deadlines are sorted *should* ensure that the timerfd that expired
|
|
-- is actually the first of the list without us having to double-check that...
|
|
if timerfd then
|
|
input.clearTimer(timerfd)
|
|
end
|
|
if touch_ges then
|
|
-- The timers we'll encounter are for finalizing a hold or (if enabled) double tap gesture,
|
|
-- as such, it makes no sense to try to detect *multiple* subsequent gestures.
|
|
-- This is why we clear the full list of timers on the first match ;).
|
|
self:clearTimeouts()
|
|
self:gestureAdjustHook(touch_ges)
|
|
return Event:new("Gesture",
|
|
self.gesture_detector:adjustGesCoordinate(touch_ges)
|
|
)
|
|
end -- if touch_ges
|
|
end -- if poll_deadline reached
|
|
end -- if poll returned ETIME
|
|
|
|
-- Refresh now on the next iteration (e.g., when we have multiple timers to check)
|
|
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 TimeVal: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 = TimeVal:new{ sec = 0 }
|
|
end
|
|
end
|
|
|
|
ok, ev = input.waitForEvent(poll_timeout and poll_timeout.sec, poll_timeout and poll_timeout.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)
|
|
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
|
|
if DEBUG.is_on then
|
|
DEBUG:logEv(ev)
|
|
if ev.type == C.EV_KEY then
|
|
logger.dbg(string.format(
|
|
"key event => code: %d (%s), value: %s, time: %d.%d",
|
|
ev.code, self.event_map[ev.code], ev.value,
|
|
ev.time.sec, ev.time.usec))
|
|
elseif ev.type == C.EV_SYN then
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d",
|
|
ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_syn_code_map[ev.code], ev.value,
|
|
ev.time.sec, ev.time.usec))
|
|
elseif ev.type == C.EV_ABS then
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d",
|
|
ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_abs_code_map[ev.code], ev.value,
|
|
ev.time.sec, ev.time.usec))
|
|
elseif ev.type == C.EV_MSC then
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d",
|
|
ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_msc_code_map[ev.code], ev.value,
|
|
ev.time.sec, ev.time.usec))
|
|
else
|
|
logger.dbg(string.format(
|
|
"input event => type: %d (%s), code: %d, value: %s, time: %d.%d",
|
|
ev.type, linux_evdev_type_map[ev.type], ev.code, ev.value,
|
|
ev.time.sec, ev.time.usec))
|
|
end
|
|
end
|
|
self:eventAdjustHook(ev)
|
|
if ev.type == C.EV_KEY then
|
|
return self:handleKeyBoardEv(ev)
|
|
elseif ev.type == C.EV_ABS and ev.code == ABS_OASIS_ORIENTATION then
|
|
return self:handleOasisOrientationEv(ev)
|
|
elseif ev.type == C.EV_ABS or ev.type == C.EV_SYN then
|
|
return self:handleTouchEv(ev)
|
|
elseif ev.type == C.EV_MSC then
|
|
return self:handleMiscEv(ev)
|
|
elseif ev.type == C.EV_SDL then
|
|
return self:handleSdlEv(ev)
|
|
else
|
|
-- Received some other kind of event that we do not know how to specifically handle yet
|
|
return Event:new("GenericInput", ev)
|
|
end
|
|
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
|
|
|
|
return Input
|