You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
koreader/frontend/device/input.lua

884 lines
31 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,
-- keyboard state:
modifiers = {
Alt = false,
Ctrl = false,
Shift = 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 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 self.device.screen_saver_mode then
if ev.value == EVENT_VALUE_KEY_RELEASE then
return "Resume"
end
else
if ev.value == EVENT_VALUE_KEY_PRESS then
return "PowerPress"
elseif ev.value == EVENT_VALUE_KEY_RELEASE then
return "PowerRelease"
end
end
end
local FileChooser = self.file_chooser
if FileChooser and self:isEvKeyPress(ev)
and self.modifiers["Ctrl"] and keycode == "O" then
logger.dbg("Opening FileChooser:", FileChooser.type)
local file_path = FileChooser:open()
if file_path then
local ReaderUI = require("apps/reader/readerui")
ReaderUI:doShowReader(file_path)
end
return
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 savequit_caller = nil
local save_quit = function()
Device:saveSettings()
UIManager:quit()
end
local FileManager = require("apps/filemanager/filemanager")
if FileManager.instance then
savequit_caller = FileManager.instance.menu
end
local ReaderUI = require("apps/reader/readerui")
local readerui_instance = ReaderUI:_getRunningInstance()
if readerui_instance then
savequit_caller = readerui_instance.menu
end
savequit_caller:exitOrRestart(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.
--- @fixme 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()
local old_screen_mode = self.device.screen:getScreenMode()
if rotation_mode ~= old_rotation_mode and screen_mode == old_screen_mode then
self.device.screen:setRotationMode(rotation_mode)
local UIManager = require("ui/uimanager")
UIManager:onRotation()
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()
local old_screen_mode = self.device.screen:getScreenMode()
-- NOTE: Try to handle ScreenMode changes sanely, without wrecking the FM, which only supports Portrait/Inverted ;).
-- NOTE: See the Oasis version just above us for a variant that's locked to the current ScreenMode.
-- Might be nice to expose the two behaviors to the user, somehow?
if rotation_mode ~= old_rotation_mode then
if screen_mode ~= old_screen_mode then
return Event:new("SwapScreenMode", screen_mode, rotation_mode)
else
self.device.screen:setRotationMode(rotation_mode)
local UIManager = require("ui/uimanager")
UIManager:onRotation()
end
end
end
-- Allow toggling it at runtime
function Input:toggleMiscEvNTX(toggle)
if toggle and toggle == true then
-- Honor Gyro events
if not self.isNTXAccelHooked then
self.handleMiscEv = self.handleMiscEvNTX
self.isNTXAccelHooked = true
end
elseif toggle and 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