mirror of
https://github.com/koreader/koreader
synced 2024-10-31 21:20:20 +00:00
74c1813a82
Follow up to b90f6db8
: allow specifying an other
value for tap interval when the keyboard is shown
(a good value for tap interval on reader and UI
elements might be too long on the keyboard, and
prevent typing fast).
865 lines
30 KiB
Lua
865 lines
30 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")
|
|
|
|
-- luacheck: push
|
|
-- luacheck: ignore
|
|
-- constants from <linux/input.h>
|
|
local EV_SYN = 0
|
|
local EV_KEY = 1
|
|
local EV_ABS = 3
|
|
local EV_MSC = 4
|
|
-- for frontend SDL event handling
|
|
local EV_SDL = 53 -- ASCII code for S
|
|
|
|
-- key press event values (KEY.value)
|
|
local EVENT_VALUE_KEY_PRESS = 1
|
|
local EVENT_VALUE_KEY_REPEAT = 2
|
|
local EVENT_VALUE_KEY_RELEASE = 0
|
|
|
|
-- Synchronization events (SYN.code).
|
|
local SYN_REPORT = 0
|
|
local SYN_CONFIG = 1
|
|
local SYN_MT_REPORT = 2
|
|
|
|
-- For single-touch events (ABS.code).
|
|
local ABS_X = 00
|
|
local ABS_Y = 01
|
|
local ABS_PRESSURE = 24
|
|
|
|
-- For multi-touch events (ABS.code).
|
|
local ABS_MT_SLOT = 47
|
|
local ABS_MT_TOUCH_MAJOR = 48
|
|
local ABS_MT_WIDTH_MAJOR = 50
|
|
|
|
local ABS_MT_POSITION_X = 53
|
|
local ABS_MT_POSITION_Y = 54
|
|
local ABS_MT_TRACKING_ID = 57
|
|
local ABS_MT_PRESSURE = 58
|
|
|
|
-- For Kindle Oasis orientation events (ABS.code)
|
|
-- the ABS code of orientation event will be adjusted to -24 from 24(ABS_PRESSURE)
|
|
-- as 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.code)
|
|
local MSC_RAW = 0x03
|
|
|
|
-- 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
|
|
|
|
-- 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 == EV_ABS then
|
|
if ev.code == ABS_X then
|
|
ev.code = ABS_Y
|
|
elseif ev.code == ABS_Y then
|
|
ev.code = ABS_X
|
|
elseif ev.code == ABS_MT_POSITION_X then
|
|
ev.code = ABS_MT_POSITION_Y
|
|
elseif ev.code == ABS_MT_POSITION_Y then
|
|
ev.code = ABS_MT_POSITION_X
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchScale(ev, by)
|
|
if ev.type == EV_ABS then
|
|
if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then
|
|
ev.value = by.x * ev.value
|
|
end
|
|
if ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y then
|
|
ev.value = by.y * ev.value
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchMirrorX(ev, width)
|
|
if ev.type == EV_ABS
|
|
and (ev.code == ABS_X or ev.code == ABS_MT_POSITION_X) then
|
|
ev.value = width - ev.value
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchMirrorY(ev, height)
|
|
if ev.type == EV_ABS
|
|
and (ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y) then
|
|
ev.value = height - ev.value
|
|
end
|
|
end
|
|
|
|
function Input:adjustTouchTranslate(ev, by)
|
|
if ev.type == EV_ABS then
|
|
if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then
|
|
ev.value = by.x + ev.value
|
|
end
|
|
if ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y then
|
|
ev.value = by.y + ev.value
|
|
end
|
|
end
|
|
end
|
|
|
|
function Input:adjustKindleOasisOrientation(ev)
|
|
if ev.type == EV_ABS and ev.code == ABS_PRESSURE then
|
|
ev.code = ABS_OASIS_ORIENTATION
|
|
end
|
|
end
|
|
|
|
function Input:setTimeout(cb, tv_out)
|
|
local item = {
|
|
callback = cb,
|
|
deadline = tv_out,
|
|
}
|
|
table.insert(self.timer_callbacks, item)
|
|
table.sort(self.timer_callbacks, function(v1,v2)
|
|
return v1.deadline < v2.deadline
|
|
end)
|
|
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 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 == EV_ABS then
|
|
if #self.MTSlots == 0 then
|
|
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
|
|
end
|
|
if ev.code == ABS_MT_SLOT then
|
|
self:addSlotIfChanged(ev.value)
|
|
elseif ev.code == ABS_MT_TRACKING_ID then
|
|
if self.snow_protocol then
|
|
self:addSlotIfChanged(ev.value)
|
|
end
|
|
self:setCurrentMtSlot("id", ev.value)
|
|
elseif ev.code == ABS_MT_POSITION_X then
|
|
self:setCurrentMtSlot("x", ev.value)
|
|
elseif ev.code == 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 == ABS_X then
|
|
self:setCurrentMtSlot("abs_x", ev.value)
|
|
elseif ev.code == ABS_Y then
|
|
self:setCurrentMtSlot("abs_y", ev.value)
|
|
elseif ev.code == 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 == EV_SYN then
|
|
if ev.code == 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, ABS_MT_TRACKING_ID, 0);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 1);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 1);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, x1);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, y1);
|
|
-- input_mt_sync (elan_touch_data.input);
|
|
-- finger 1 down:
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 1);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 1);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 1);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, x2);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, y2);
|
|
-- input_mt_sync (elan_touch_data.input);
|
|
-- finger 0 up:
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 0);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 0);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 0);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, last_x);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, last_y);
|
|
-- input_mt_sync (elan_touch_data.input);
|
|
-- finger 1 up:
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 1);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 0);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 0);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, last_x2);
|
|
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, last_y2);
|
|
-- input_mt_sync (elan_touch_data.input);
|
|
if ev.type == EV_ABS then
|
|
if #self.MTSlots == 0 then
|
|
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
|
|
end
|
|
if ev.code == ABS_MT_TRACKING_ID then
|
|
self:addSlotIfChanged(ev.value)
|
|
self:setCurrentMtSlot("id", ev.value)
|
|
elseif ev.code == ABS_MT_TOUCH_MAJOR and ev.value == 0 then
|
|
self:setCurrentMtSlot("id", -1)
|
|
elseif ev.code == ABS_MT_POSITION_X then
|
|
self:setCurrentMtSlot("x", ev.value)
|
|
elseif ev.code == ABS_MT_POSITION_Y then
|
|
self:setCurrentMtSlot("y", ev.value)
|
|
end
|
|
elseif ev.type == EV_SYN then
|
|
if ev.code == 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 == EV_ABS then
|
|
if #self.MTSlots == 0 then
|
|
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
|
|
end
|
|
if ev.code == ABS_X then
|
|
self:setCurrentMtSlot("x", ev.value)
|
|
elseif ev.code == ABS_Y then
|
|
self:setCurrentMtSlot("y", ev.value)
|
|
elseif ev.code == ABS_PRESSURE then
|
|
if ev.value ~= 0 then
|
|
self:setCurrentMtSlot("id", 1)
|
|
else
|
|
self:setCurrentMtSlot("id", -1)
|
|
end
|
|
end
|
|
elseif ev.type == EV_SYN then
|
|
if ev.code == 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 == 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.
|
|
function Input:waitEvent(timeout_us)
|
|
local ok, ev
|
|
-- wrapper for input.waitForEvents that will retry for some cases
|
|
while true do
|
|
if #self.timer_callbacks > 0 then
|
|
local wait_deadline = TimeVal:now() + TimeVal:new{
|
|
usec = timeout_us
|
|
}
|
|
-- we don't block if there is any timer, set wait to 10us
|
|
while #self.timer_callbacks > 0 do
|
|
ok, ev = pcall(input.waitForEvent, 100)
|
|
if ok then break end
|
|
local tv_now = TimeVal:now()
|
|
if (not timeout_us or tv_now < wait_deadline) then
|
|
-- check whether timer is up
|
|
if tv_now >= self.timer_callbacks[1].deadline then
|
|
local touch_ges = self.timer_callbacks[1].callback()
|
|
table.remove(self.timer_callbacks, 1)
|
|
if touch_ges then
|
|
-- Do we really need to clear all setTimeout after
|
|
-- decided a gesture? FIXME
|
|
self.timer_callbacks = {}
|
|
self:gestureAdjustHook(touch_ges)
|
|
return Event:new("Gesture",
|
|
self.gesture_detector:adjustGesCoordinate(touch_ges)
|
|
)
|
|
end -- EOF if touch_ges
|
|
end -- EOF if deadline reached
|
|
else
|
|
break
|
|
end -- EOF if not exceed wait timeout
|
|
end -- while #timer_callbacks > 0
|
|
else
|
|
ok, ev = pcall(input.waitForEvent, timeout_us)
|
|
end -- EOF if #timer_callbacks > 0
|
|
if ok then
|
|
break
|
|
end
|
|
|
|
-- ev does contain an error message:
|
|
local timeout_err_msg = "Waiting for input failed: timeout\n"
|
|
-- ev may not be equal to timeout_err_msg, but it may ends with it
|
|
-- ("./ffi/SDL2_0.lua:110: Waiting for input failed: timeout" on the emulator)
|
|
if ev and ev.sub and ev:sub(-timeout_err_msg:len()) == timeout_err_msg then
|
|
-- don't report an error on timeout
|
|
ev = nil
|
|
break
|
|
elseif ev == "application forced to quit" then
|
|
--- @todo return an event that can be handled
|
|
os.exit(0)
|
|
end
|
|
logger.warn("got error waiting for events:", ev)
|
|
if ev ~= "Waiting for input failed: 4\n" then
|
|
-- we only abort if the error is not EINTR
|
|
break
|
|
end
|
|
end
|
|
|
|
if ok and ev then
|
|
if DEBUG.is_on and ev then
|
|
DEBUG:logEv(ev)
|
|
logger.dbg(string.format(
|
|
"%s event => type: %d, code: %d(%s), value: %s, time: %d.%d",
|
|
ev.type == EV_KEY and "key" or "input",
|
|
ev.type, ev.code, self.event_map[ev.code], tostring(ev.value),
|
|
ev.time.sec, ev.time.usec))
|
|
end
|
|
self:eventAdjustHook(ev)
|
|
if ev.type == EV_KEY then
|
|
return self:handleKeyBoardEv(ev)
|
|
elseif ev.type == EV_ABS and ev.code == ABS_OASIS_ORIENTATION then
|
|
return self:handleOasisOrientationEv(ev)
|
|
elseif ev.type == EV_ABS or ev.type == EV_SYN then
|
|
return self:handleTouchEv(ev)
|
|
elseif ev.type == EV_MSC then
|
|
return self:handleMiscEv(ev)
|
|
elseif ev.type == EV_SDL then
|
|
return self:handleSdlEv(ev)
|
|
else
|
|
-- some other kind of event that we do not know yet
|
|
return Event:new("GenericInput", ev)
|
|
end
|
|
elseif not ok and ev then
|
|
return Event:new("InputError", ev)
|
|
end
|
|
end
|
|
|
|
return Input
|