mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
The great Input/GestureDetector/TimeVal spring cleanup (a.k.a., a saner main loop) (#7415)
* 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..
This commit is contained in:
2
base
2
base
Submodule base updated: fd30a65587...3a754c7288
@@ -13,6 +13,7 @@ local LuaData = require("luadata")
|
||||
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
|
||||
local NetworkMgr = require("ui/network/manager")
|
||||
local SortWidget = require("ui/widget/sortwidget")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local Trapper = require("ui/trapper")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local ffiUtil = require("ffi/util")
|
||||
@@ -109,7 +110,7 @@ function ReaderDictionary:init()
|
||||
-- Allow quick interruption or dismiss of search result window
|
||||
-- with tap if done before this delay. After this delay, the
|
||||
-- result window is shown and dismiss prevented for a few 100ms
|
||||
self.quick_dismiss_before_delay = 3
|
||||
self.quick_dismiss_before_delay = TimeVal:new{ sec = 3 }
|
||||
|
||||
-- Gather info about available dictionaries
|
||||
if not available_ifos then
|
||||
@@ -846,9 +847,9 @@ function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, box, li
|
||||
|
||||
self:showLookupInfo(word, self.lookup_msg_delay)
|
||||
|
||||
self._lookup_start_ts = ffiUtil.getTimestamp()
|
||||
self._lookup_start_tv = UIManager:getTime()
|
||||
local results = self:startSdcv(word, dict_names, fuzzy_search)
|
||||
if results and results.lookup_cancelled and ffiUtil.getDuration(self._lookup_start_ts) <= self.quick_dismiss_before_delay then
|
||||
if results and results.lookup_cancelled and TimeVal:now() - self._lookup_start_tv <= self.quick_dismiss_before_delay then
|
||||
-- If interrupted quickly just after launch, don't display anything
|
||||
-- (this might help avoiding refreshes and the need to dismiss
|
||||
-- after accidental long-press when holding a device).
|
||||
@@ -907,7 +908,7 @@ function ReaderDictionary:showDict(word, results, box, link)
|
||||
self:dismissLookupInfo()
|
||||
if results and results[1] then
|
||||
UIManager:show(self.dict_window)
|
||||
if not results.lookup_cancelled and self._lookup_start_ts and ffiUtil.getDuration(self._lookup_start_ts) > self.quick_dismiss_before_delay then
|
||||
if not results.lookup_cancelled and self._lookup_start_tv and TimeVal:now() - self._lookup_start_tv > self.quick_dismiss_before_delay then
|
||||
-- If the search took more than a few seconds to be done, discard
|
||||
-- queued and coming up events to avoid a voluntary dismissal
|
||||
-- (because the user felt the result would not come) to kill the
|
||||
|
||||
@@ -338,14 +338,14 @@ end
|
||||
-- to ensure current highlight has not already been cleared, and that we
|
||||
-- are not going to clear a new highlight
|
||||
function ReaderHighlight:getClearId()
|
||||
self.clear_id = TimeVal.now() -- can act as a unique id
|
||||
self.clear_id = UIManager:getTime() -- can act as a unique id
|
||||
return self.clear_id
|
||||
end
|
||||
|
||||
function ReaderHighlight:clear(clear_id)
|
||||
if clear_id then -- should be provided by delayed call to clear()
|
||||
if clear_id ~= self.clear_id then
|
||||
-- if clear_id is no more valid, highlight has already been
|
||||
-- if clear_id is no longer valid, highlight has already been
|
||||
-- cleared since this clear_id was given
|
||||
return
|
||||
end
|
||||
@@ -681,7 +681,7 @@ function ReaderHighlight:_resetHoldTimer(clear)
|
||||
if clear then
|
||||
self.hold_last_tv = nil
|
||||
else
|
||||
self.hold_last_tv = TimeVal.now()
|
||||
self.hold_last_tv = UIManager:getTime()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1124,9 +1124,8 @@ end
|
||||
function ReaderHighlight:onHoldRelease()
|
||||
local long_final_hold = false
|
||||
if self.hold_last_tv then
|
||||
local hold_duration = TimeVal.now() - self.hold_last_tv
|
||||
hold_duration = hold_duration.sec + hold_duration.usec/1000000
|
||||
if hold_duration > 3.0 then
|
||||
local hold_duration = UIManager:getTime() - self.hold_last_tv
|
||||
if hold_duration > TimeVal:new{ sec = 3 } then
|
||||
-- We stayed 3 seconds before release without updating selection
|
||||
long_final_hold = true
|
||||
end
|
||||
|
||||
@@ -13,6 +13,7 @@ local OverlapGroup = require("ui/widget/overlapgroup")
|
||||
local ReaderDogear = require("apps/reader/modules/readerdogear")
|
||||
local ReaderFlipping = require("apps/reader/modules/readerflipping")
|
||||
local ReaderFooter = require("apps/reader/modules/readerfooter")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local dbg = require("dbg")
|
||||
local logger = require("logger")
|
||||
@@ -71,7 +72,7 @@ local ReaderView = OverlapGroup:extend{
|
||||
-- in flipping state
|
||||
flipping_visible = false,
|
||||
-- to ensure periodic flush of settings
|
||||
settings_last_save_ts = nil,
|
||||
settings_last_save_tv = nil,
|
||||
}
|
||||
|
||||
function ReaderView:init()
|
||||
@@ -954,17 +955,17 @@ function ReaderView:onCloseDocument()
|
||||
end
|
||||
|
||||
function ReaderView:onReaderReady()
|
||||
self.settings_last_save_ts = os.time()
|
||||
self.settings_last_save_tv = UIManager:getTime()
|
||||
end
|
||||
|
||||
function ReaderView:onResume()
|
||||
-- As settings were saved on suspend, reset this on resume,
|
||||
-- as there's no need for a possibly immediate save.
|
||||
self.settings_last_save_ts = os.time()
|
||||
self.settings_last_save_tv = UIManager:getTime()
|
||||
end
|
||||
|
||||
function ReaderView:checkAutoSaveSettings()
|
||||
if not self.settings_last_save_ts then -- reader not yet ready
|
||||
if not self.settings_last_save_tv then -- reader not yet ready
|
||||
return
|
||||
end
|
||||
if G_reader_settings:nilOrFalse("auto_save_settings_interval_minutes") then
|
||||
@@ -973,9 +974,10 @@ function ReaderView:checkAutoSaveSettings()
|
||||
end
|
||||
|
||||
local interval = G_reader_settings:readSetting("auto_save_settings_interval_minutes")
|
||||
local now_ts = os.time()
|
||||
if now_ts - self.settings_last_save_ts >= interval*60 then
|
||||
self.settings_last_save_ts = now_ts
|
||||
interval = TimeVal:new{ sec = interval*60 }
|
||||
local now_tv = UIManager:getTime()
|
||||
if now_tv - self.settings_last_save_tv >= interval then
|
||||
self.settings_last_save_tv = now_tv
|
||||
-- I/O, delay until after the pageturn
|
||||
UIManager:tickAfterNext(function()
|
||||
self.ui:saveSettings()
|
||||
|
||||
@@ -143,6 +143,8 @@ function Device:init()
|
||||
or ev.code == C.APP_CMD_INIT_WINDOW
|
||||
or ev.code == C.APP_CMD_WINDOW_REDRAW_NEEDED then
|
||||
this.device.screen:_updateWindow()
|
||||
elseif ev.code == C.APP_CMD_TERM_WINDOW then
|
||||
this.device.input:resetState()
|
||||
elseif ev.code == C.APP_CMD_CONFIG_CHANGED then
|
||||
-- orientation and size changes
|
||||
if android.screen.width ~= android.getScreenWidth()
|
||||
|
||||
@@ -47,6 +47,11 @@ local TimeVal = require("ui/timeval")
|
||||
local logger = require("logger")
|
||||
local util = require("util")
|
||||
|
||||
-- We're going to need some clockid_t constants
|
||||
local ffi = require("ffi")
|
||||
local C = ffi.C
|
||||
require("ffi/posix_h")
|
||||
|
||||
-- default values (all the time parameters are in microseconds)
|
||||
local TAP_INTERVAL = 0 * 1000
|
||||
local DOUBLE_TAP_INTERVAL = 300 * 1000
|
||||
@@ -56,11 +61,17 @@ local PAN_DELAYED_INTERVAL = 500 * 1000
|
||||
local SWIPE_INTERVAL = 900 * 1000
|
||||
-- current values
|
||||
local ges_tap_interval = G_reader_settings:readSetting("ges_tap_interval") or TAP_INTERVAL
|
||||
ges_tap_interval = TimeVal:new{ usec = ges_tap_interval }
|
||||
local ges_double_tap_interval = G_reader_settings:readSetting("ges_double_tap_interval") or DOUBLE_TAP_INTERVAL
|
||||
ges_double_tap_interval = TimeVal:new{ usec = ges_double_tap_interval }
|
||||
local ges_two_finger_tap_duration = G_reader_settings:readSetting("ges_two_finger_tap_duration") or TWO_FINGER_TAP_DURATION
|
||||
ges_two_finger_tap_duration = TimeVal:new{ usec = ges_two_finger_tap_duration }
|
||||
local ges_hold_interval = G_reader_settings:readSetting("ges_hold_interval") or HOLD_INTERVAL
|
||||
ges_hold_interval = TimeVal:new{ usec = ges_hold_interval }
|
||||
local ges_pan_delayed_interval = G_reader_settings:readSetting("ges_pan_delayed_interval") or PAN_DELAYED_INTERVAL
|
||||
ges_pan_delayed_interval = TimeVal:new{ usec = ges_pan_delayed_interval }
|
||||
local ges_swipe_interval = G_reader_settings:readSetting("ges_swipe_interval") or SWIPE_INTERVAL
|
||||
ges_swipe_interval = TimeVal:new{ usec = ges_swipe_interval }
|
||||
|
||||
local GestureDetector = {
|
||||
-- must be initialized with the Input singleton class
|
||||
@@ -85,7 +96,7 @@ local GestureDetector = {
|
||||
},
|
||||
-- states are stored in separated slots
|
||||
states = {},
|
||||
hold_timer_id = {},
|
||||
pending_hold_timer = {},
|
||||
track_ids = {},
|
||||
tev_stacks = {},
|
||||
-- latest feeded touch event in each slots
|
||||
@@ -97,6 +108,8 @@ local GestureDetector = {
|
||||
detectings = {},
|
||||
-- for single/double tap
|
||||
last_taps = {},
|
||||
-- for timestamp clocksource detection
|
||||
clock_id = nil,
|
||||
}
|
||||
|
||||
function GestureDetector:new(o)
|
||||
@@ -155,23 +168,42 @@ end
|
||||
tap2 is the later tap
|
||||
--]]
|
||||
function GestureDetector:isTapBounce(tap1, tap2, interval)
|
||||
-- NOTE: If time went backwards, make the delta infinite to avoid misdetections,
|
||||
-- as we can no longer compute a sensible value...
|
||||
local tv_diff = tap2.timev - tap1.timev
|
||||
if not tv_diff:isPositive() then
|
||||
tv_diff = TimeVal:new{ sec = math.huge }
|
||||
end
|
||||
return (
|
||||
math.abs(tap1.x - tap2.x) < self.SINGLE_TAP_BOUNCE_DISTANCE and
|
||||
math.abs(tap1.y - tap2.y) < self.SINGLE_TAP_BOUNCE_DISTANCE and
|
||||
(tv_diff.sec == 0 and (tv_diff.usec) < interval)
|
||||
tv_diff < interval
|
||||
)
|
||||
end
|
||||
|
||||
function GestureDetector:isDoubleTap(tap1, tap2)
|
||||
local tv_diff = tap2.timev - tap1.timev
|
||||
if not tv_diff:isPositive() then
|
||||
tv_diff = TimeVal:new{ sec = math.huge }
|
||||
end
|
||||
return (
|
||||
math.abs(tap1.x - tap2.x) < self.DOUBLE_TAP_DISTANCE and
|
||||
math.abs(tap1.y - tap2.y) < self.DOUBLE_TAP_DISTANCE and
|
||||
(tv_diff.sec == 0 and (tv_diff.usec) < ges_double_tap_interval)
|
||||
tv_diff < ges_double_tap_interval
|
||||
)
|
||||
end
|
||||
|
||||
-- Takes TimeVals as input, not a tev
|
||||
function GestureDetector:isHold(t1, t2)
|
||||
local tv_diff = t2 - t1
|
||||
if not tv_diff:isPositive() then
|
||||
tv_diff = TimeVal:new{ sec = 0 }
|
||||
end
|
||||
-- NOTE: We cheat by not checking a distance because we're only checking that in tapState,
|
||||
-- which already ensures a stationary finger, by elimination ;).
|
||||
return tv_diff >= ges_hold_interval
|
||||
end
|
||||
|
||||
function GestureDetector:isTwoFingerTap()
|
||||
if self.last_tevs[0] == nil or self.last_tevs[1] == nil then
|
||||
return false
|
||||
@@ -181,14 +213,20 @@ function GestureDetector:isTwoFingerTap()
|
||||
local y_diff0 = math.abs(self.last_tevs[0].y - self.first_tevs[0].y)
|
||||
local y_diff1 = math.abs(self.last_tevs[1].y - self.first_tevs[1].y)
|
||||
local tv_diff0 = self.last_tevs[0].timev - self.first_tevs[0].timev
|
||||
if not tv_diff0:isPositive() then
|
||||
tv_diff0 = TimeVal:new{ sec = math.huge }
|
||||
end
|
||||
local tv_diff1 = self.last_tevs[1].timev - self.first_tevs[1].timev
|
||||
if not tv_diff1:isPositive() then
|
||||
tv_diff1 = TimeVal:new{ sec = math.huge }
|
||||
end
|
||||
return (
|
||||
x_diff0 < self.TWO_FINGER_TAP_REGION and
|
||||
x_diff1 < self.TWO_FINGER_TAP_REGION and
|
||||
y_diff0 < self.TWO_FINGER_TAP_REGION and
|
||||
y_diff1 < self.TWO_FINGER_TAP_REGION and
|
||||
tv_diff0.sec == 0 and tv_diff0.usec < ges_two_finger_tap_duration and
|
||||
tv_diff1.sec == 0 and tv_diff1.usec < ges_two_finger_tap_duration
|
||||
tv_diff0 < ges_two_finger_tap_duration and
|
||||
tv_diff1 < ges_two_finger_tap_duration
|
||||
)
|
||||
end
|
||||
|
||||
@@ -227,7 +265,10 @@ end
|
||||
function GestureDetector:isSwipe(slot)
|
||||
if not self.first_tevs[slot] or not self.last_tevs[slot] then return end
|
||||
local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev
|
||||
if (tv_diff.sec == 0) and (tv_diff.usec < ges_swipe_interval) then
|
||||
if not tv_diff:isPositive() then
|
||||
tv_diff = TimeVal:new{ sec = math.huge }
|
||||
end
|
||||
if tv_diff < ges_swipe_interval then
|
||||
local x_diff = self.last_tevs[slot].x - self.first_tevs[slot].x
|
||||
local y_diff = self.last_tevs[slot].y - self.first_tevs[slot].y
|
||||
if x_diff ~= 0 or y_diff ~= 0 then
|
||||
@@ -254,49 +295,54 @@ end
|
||||
|
||||
function GestureDetector:clearState(slot)
|
||||
self.states[slot] = self.initialState
|
||||
self.hold_timer_id[slot] = nil
|
||||
self.pending_hold_timer[slot] = nil
|
||||
self.detectings[slot] = false
|
||||
self.first_tevs[slot] = nil
|
||||
self.last_tevs[slot] = nil
|
||||
self.multiswipe_directions = {}
|
||||
self.multiswipe_type = nil
|
||||
|
||||
-- Also clear any pending hold callbacks on that slot.
|
||||
-- (single taps call this, so we can't clear double_tap callbacks without being caught in an obvious catch-22 ;)).
|
||||
self.input:clearTimeout(slot, "hold")
|
||||
end
|
||||
|
||||
function GestureDetector:setNewInterval(type, interval)
|
||||
if type == "ges_tap_interval" then
|
||||
ges_tap_interval = interval
|
||||
ges_tap_interval = TimeVal:new{ usec = interval }
|
||||
elseif type == "ges_double_tap_interval" then
|
||||
ges_double_tap_interval = interval
|
||||
ges_double_tap_interval = TimeVal:new{ usec = interval }
|
||||
elseif type == "ges_two_finger_tap_duration" then
|
||||
ges_two_finger_tap_duration = interval
|
||||
ges_two_finger_tap_duration = TimeVal:new{ usec = interval }
|
||||
elseif type == "ges_hold_interval" then
|
||||
ges_hold_interval = interval
|
||||
ges_hold_interval = TimeVal:new{ usec = interval }
|
||||
elseif type == "ges_pan_delayed_interval" then
|
||||
ges_pan_delayed_interval = interval
|
||||
ges_pan_delayed_interval = TimeVal:new{ usec = interval }
|
||||
elseif type == "ges_swipe_interval" then
|
||||
ges_swipe_interval = interval
|
||||
ges_swipe_interval = TimeVal:new{ usec = interval }
|
||||
end
|
||||
end
|
||||
|
||||
function GestureDetector:getInterval(type)
|
||||
if type == "ges_tap_interval" then
|
||||
return ges_tap_interval
|
||||
return ges_tap_interval:tousecs()
|
||||
elseif type == "ges_double_tap_interval" then
|
||||
return ges_double_tap_interval
|
||||
return ges_double_tap_interval:tousecs()
|
||||
elseif type == "ges_two_finger_tap_duration" then
|
||||
return ges_two_finger_tap_duration
|
||||
return ges_two_finger_tap_duration:tousecs()
|
||||
elseif type == "ges_hold_interval" then
|
||||
return ges_hold_interval
|
||||
return ges_hold_interval:tousecs()
|
||||
elseif type == "ges_pan_delayed_interval" then
|
||||
return ges_pan_delayed_interval
|
||||
return ges_pan_delayed_interval:tousecs()
|
||||
elseif type == "ges_swipe_interval" then
|
||||
return ges_swipe_interval
|
||||
return ges_swipe_interval:tousecs()
|
||||
end
|
||||
end
|
||||
|
||||
function GestureDetector:clearStates()
|
||||
self:clearState(0)
|
||||
self:clearState(1)
|
||||
for k, _ in pairs(self.states) do
|
||||
self:clearState(k)
|
||||
end
|
||||
end
|
||||
|
||||
function GestureDetector:initialState(tev)
|
||||
@@ -320,10 +366,61 @@ function GestureDetector:initialState(tev)
|
||||
end
|
||||
end
|
||||
|
||||
--[[--
|
||||
Attempts to figure out which clock source tap events are using...
|
||||
]]
|
||||
function GestureDetector:probeClockSource(timev)
|
||||
-- We'll check if that timestamp is +/- 2.5s away from the three potential clock sources supported by evdev.
|
||||
-- We have bigger issues than this if we're parsing events more than 3s late ;).
|
||||
local threshold = TimeVal:new{ sec = 2, usec = 500000 }
|
||||
|
||||
-- Start w/ REALTIME, because it's the easiest to detect ;).
|
||||
local realtime = TimeVal:realtime_coarse()
|
||||
-- clock-threshold <= timev <= clock+threshold
|
||||
if timev >= realtime - threshold and timev <= realtime + threshold then
|
||||
self.clock_id = C.CLOCK_REALTIME
|
||||
logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_REALTIME")
|
||||
return
|
||||
end
|
||||
|
||||
-- Then MONOTONIC, as it's (hopefully) more common than BOOTTIME (and also guaranteed to be an usable clock source)
|
||||
local monotonic = TimeVal:monotonic_coarse()
|
||||
if timev >= monotonic - threshold and timev <= monotonic + threshold then
|
||||
self.clock_id = C.CLOCK_MONOTONIC
|
||||
logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_MONOTONIC")
|
||||
return
|
||||
end
|
||||
|
||||
-- Finally, BOOTTIME
|
||||
local boottime = TimeVal:boottime()
|
||||
if timev >= boottime - threshold and timev <= boottime + threshold then
|
||||
self.clock_id = C.CLOCK_BOOTTIME
|
||||
logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_BOOTTIME")
|
||||
return
|
||||
end
|
||||
|
||||
-- If we're here, the detection was inconclusive :/
|
||||
self.clock_id = -1
|
||||
logger.info("GestureDetector:probeClockSource: Touch event clock source detection was inconclusive")
|
||||
end
|
||||
|
||||
function GestureDetector:getClockSource()
|
||||
return self.clock_id
|
||||
end
|
||||
|
||||
function GestureDetector:resetClockSource()
|
||||
self.clock_id = nil
|
||||
end
|
||||
|
||||
--[[--
|
||||
Handles both single and double tap.
|
||||
--]]
|
||||
function GestureDetector:tapState(tev)
|
||||
-- Attempt to detect the clock source for these events (we reset it on suspend to discriminate MONOTONIC from BOOTTIME).
|
||||
if not self.clock_id then
|
||||
self:probeClockSource(tev.timev)
|
||||
end
|
||||
|
||||
logger.dbg("in tap state...")
|
||||
local slot = tev.slot
|
||||
if tev.id == -1 then
|
||||
@@ -397,7 +494,7 @@ function GestureDetector:handleDoubleTap(tev)
|
||||
local tap_interval = self.input.tap_interval_override or ges_tap_interval
|
||||
-- We do tap bounce detection even when double tap is enabled (so, double tap
|
||||
-- is triggered when: ges_tap_interval <= delay < ges_double_tap_interval)
|
||||
if tap_interval > 0 and self.last_taps[slot] ~= nil and self:isTapBounce(self.last_taps[slot], cur_tap, tap_interval) then
|
||||
if not tap_interval:isZero() and self.last_taps[slot] ~= nil and self:isTapBounce(self.last_taps[slot], cur_tap, tap_interval) then
|
||||
logger.dbg("tap bounce detected in slot", slot, ": ignored")
|
||||
-- Simply ignore it, and clear state as this is the end of a touch event
|
||||
-- (this doesn't clear self.last_taps[slot], so a 3rd tap can be detected
|
||||
@@ -410,6 +507,7 @@ function GestureDetector:handleDoubleTap(tev)
|
||||
self:isDoubleTap(self.last_taps[slot], cur_tap) then
|
||||
-- it is a double tap
|
||||
self:clearState(slot)
|
||||
self.input:clearTimeout(slot, "double_tap")
|
||||
ges_ev.ges = "double_tap"
|
||||
self.last_taps[slot] = nil
|
||||
logger.dbg("double tap detected in slot", slot)
|
||||
@@ -431,16 +529,9 @@ function GestureDetector:handleDoubleTap(tev)
|
||||
-- may be the start of a double tap. We'll send it as a single tap after
|
||||
-- a timer if no second tap happened in the double tap delay.
|
||||
logger.dbg("set up single/double tap timer")
|
||||
-- deadline should be calculated by adding current tap time and the interval
|
||||
-- (No need to compute self._has_real_clock_time_ev_time here, we should always
|
||||
-- have been thru handleNonTap() where it is computed, before getting here)
|
||||
local ref_time = self._has_real_clock_time_ev_time and cur_tap.timev or TimeVal:now()
|
||||
local deadline = ref_time + TimeVal:new{
|
||||
sec = 0,
|
||||
usec = not self.input.disable_double_tap and ges_double_tap_interval or 0,
|
||||
}
|
||||
self.input:setTimeout(function()
|
||||
logger.dbg("in single/double tap timer", self.last_taps[slot] ~= nil)
|
||||
-- setTimeout will handle computing the deadline in the least lossy way possible given the platform.
|
||||
self.input:setTimeout(slot, "double_tap", function()
|
||||
logger.dbg("in single/double tap timer, single tap:", self.last_taps[slot] ~= nil)
|
||||
-- double tap will set last_tap to nil so if it is not, then
|
||||
-- user has not double-tap'ed: it's a single tap
|
||||
if self.last_taps[slot] ~= nil then
|
||||
@@ -449,7 +540,7 @@ function GestureDetector:handleDoubleTap(tev)
|
||||
logger.dbg("single tap detected in slot", slot, ges_ev.pos)
|
||||
return ges_ev
|
||||
end
|
||||
end, deadline)
|
||||
end, tev.timev, ges_double_tap_interval)
|
||||
-- we are already at the end of touch event
|
||||
-- so reset the state
|
||||
self:clearState(slot)
|
||||
@@ -461,34 +552,21 @@ function GestureDetector:handleNonTap(tev)
|
||||
-- switched from other state, probably from initialState
|
||||
-- we return nil in this case
|
||||
self.states[slot] = self.tapState
|
||||
if self._has_real_clock_time_ev_time == nil then
|
||||
if tev.timev.sec < TimeVal:now().sec - 600 then
|
||||
-- ev.timev is probably the uptime since device boot
|
||||
-- (which might pause on suspend) that we can't use
|
||||
-- with setTimeout(): we'll use TimeVal:now()
|
||||
self._has_real_clock_time_ev_time = false
|
||||
logger.info("event times are not real clock time: some adjustments will be made")
|
||||
else
|
||||
-- assume they are real clock time
|
||||
self._has_real_clock_time_ev_time = true
|
||||
logger.info("event times are real clock time: no adjustment needed")
|
||||
end
|
||||
end
|
||||
logger.dbg("set up hold timer")
|
||||
local ref_time = self._has_real_clock_time_ev_time and tev.timev or TimeVal:now()
|
||||
local deadline = ref_time + TimeVal:new{
|
||||
sec = 0, usec = ges_hold_interval
|
||||
}
|
||||
-- Be sure the following setTimeout only react to this tapState
|
||||
local hold_timer_id = tev.timev
|
||||
self.hold_timer_id[slot] = hold_timer_id
|
||||
self.input:setTimeout(function()
|
||||
if self.states[slot] == self.tapState and self.hold_timer_id[slot] == hold_timer_id then
|
||||
-- timer set in tapState, so we switch to hold
|
||||
-- Invalidate previous hold timers on that slot so that the following setTimeout will only react to *this* tapState.
|
||||
self.input:clearTimeout(slot, "hold")
|
||||
self.pending_hold_timer[slot] = true
|
||||
self.input:setTimeout(slot, "hold", function()
|
||||
-- If the pending_hold_timer we set on our first switch to tapState on this slot (e.g., first finger down event),
|
||||
-- back when the timer was setup, is still relevant (e.g., the slot wasn't run through clearState by a finger up gesture),
|
||||
-- then check that we're still in a stationary finger down state (e.g., tapState).
|
||||
if self.pending_hold_timer[slot] and self.states[slot] == self.tapState then
|
||||
-- That means we can switch to hold
|
||||
logger.dbg("hold gesture detected in slot", slot)
|
||||
self.pending_hold_timer[slot] = nil
|
||||
return self:switchState("holdState", tev, true)
|
||||
end
|
||||
end, deadline)
|
||||
end, tev.timev, ges_hold_interval)
|
||||
return {
|
||||
ges = "touch",
|
||||
pos = Geom:new{
|
||||
@@ -499,12 +577,10 @@ function GestureDetector:handleNonTap(tev)
|
||||
time = tev.timev,
|
||||
}
|
||||
else
|
||||
-- it is not end of touch event, see if we need to switch to
|
||||
-- other states
|
||||
-- We're still inside a stream of input events, see if we need to switch to other states.
|
||||
if (tev.x and math.abs(tev.x - self.first_tevs[slot].x) >= self.PAN_THRESHOLD) or
|
||||
(tev.y and math.abs(tev.y - self.first_tevs[slot].y) >= self.PAN_THRESHOLD) then
|
||||
-- if user's finger moved long enough in X or
|
||||
-- Y distance, we switch to pan state
|
||||
-- If user's finger moved far enough on the X or Y axes, switch to pan state.
|
||||
return self:switchState("panState", tev)
|
||||
end
|
||||
end
|
||||
@@ -594,6 +670,9 @@ function GestureDetector:handlePan(tev)
|
||||
else
|
||||
local pan_direction, pan_distance = self:getPath(slot)
|
||||
local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev
|
||||
if not tv_diff:isPositive() then
|
||||
tv_diff = TimeVal:new{ sec = math.huge }
|
||||
end
|
||||
|
||||
local pan_ev = {
|
||||
ges = "pan",
|
||||
@@ -620,7 +699,7 @@ function GestureDetector:handlePan(tev)
|
||||
|
||||
-- delayed pan, used where necessary to reduce potential activation of panning
|
||||
-- when swiping is intended (e.g., for the menu or for multiswipe)
|
||||
if not ((tv_diff.sec == 0) and (tv_diff.usec < ges_pan_delayed_interval)) then
|
||||
if not (tv_diff < ges_pan_delayed_interval) then
|
||||
pan_ev.relative_delayed.x = tev.x - self.first_tevs[slot].x
|
||||
pan_ev.relative_delayed.y = tev.y - self.first_tevs[slot].y
|
||||
pan_ev.distance_delayed = pan_distance
|
||||
@@ -774,7 +853,7 @@ end
|
||||
function GestureDetector:holdState(tev, hold)
|
||||
logger.dbg("in hold state...")
|
||||
local slot = tev.slot
|
||||
-- when we switch to hold state, we pass additional param "hold"
|
||||
-- When we switch to hold state, we pass an additional boolean param "hold".
|
||||
if tev.id ~= -1 and hold and self.last_tevs[slot].x and self.last_tevs[slot].y then
|
||||
self.states[slot] = self.holdState
|
||||
return {
|
||||
|
||||
@@ -13,44 +13,22 @@ 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
|
||||
-- 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.
|
||||
-- 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
|
||||
@@ -61,9 +39,6 @@ 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
|
||||
@@ -73,6 +48,56 @@ local MSC_RAW_GSENSOR_LANDSCAPE_LEFT = 0x1a
|
||||
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
|
||||
@@ -236,72 +261,148 @@ 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
|
||||
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 == EV_ABS then
|
||||
if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then
|
||||
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 == ABS_Y or ev.code == ABS_MT_POSITION_Y then
|
||||
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 == EV_ABS
|
||||
and (ev.code == ABS_X or ev.code == ABS_MT_POSITION_X) then
|
||||
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 == EV_ABS
|
||||
and (ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y) then
|
||||
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 == EV_ABS then
|
||||
if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then
|
||||
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 == ABS_Y or ev.code == ABS_MT_POSITION_Y then
|
||||
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 == EV_ABS and ev.code == ABS_PRESSURE then
|
||||
if ev.type == C.EV_ABS and ev.code == C.ABS_PRESSURE then
|
||||
ev.code = ABS_OASIS_ORIENTATION
|
||||
end
|
||||
end
|
||||
|
||||
function Input:setTimeout(cb, tv_out)
|
||||
function Input:setTimeout(slot, ges, cb, origin, delay)
|
||||
local item = {
|
||||
slot = slot,
|
||||
gesture = ges,
|
||||
callback = cb,
|
||||
deadline = tv_out,
|
||||
}
|
||||
|
||||
-- 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)
|
||||
table.sort(self.timer_callbacks, function(v1,v2)
|
||||
|
||||
-- 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
|
||||
@@ -434,7 +535,7 @@ 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
|
||||
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,
|
||||
@@ -443,29 +544,29 @@ 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 ev.type == C.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
|
||||
if ev.code == C.ABS_MT_SLOT then
|
||||
self:addSlotIfChanged(ev.value)
|
||||
elseif ev.code == ABS_MT_TRACKING_ID then
|
||||
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 == ABS_MT_POSITION_X then
|
||||
elseif ev.code == C.ABS_MT_POSITION_X then
|
||||
self:setCurrentMtSlot("x", ev.value)
|
||||
elseif ev.code == ABS_MT_POSITION_Y then
|
||||
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 == ABS_X then
|
||||
elseif ev.code == C.ABS_X then
|
||||
self:setCurrentMtSlot("abs_x", ev.value)
|
||||
elseif ev.code == ABS_Y then
|
||||
elseif ev.code == C.ABS_Y then
|
||||
self:setCurrentMtSlot("abs_y", ev.value)
|
||||
elseif ev.code == ABS_PRESSURE then
|
||||
elseif ev.code == C.ABS_PRESSURE then
|
||||
if ev.value ~= 0 then
|
||||
self:setCurrentMtSlot("id", 1)
|
||||
self:confirmAbsxy()
|
||||
@@ -474,8 +575,8 @@ function Input:handleTouchEv(ev)
|
||||
self:setCurrentMtSlot("id", -1)
|
||||
end
|
||||
end
|
||||
elseif ev.type == EV_SYN then
|
||||
if ev.code == SYN_REPORT then
|
||||
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
|
||||
@@ -517,49 +618,49 @@ 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_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, 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_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, 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_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, 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_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 == EV_ABS then
|
||||
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 == ABS_MT_TRACKING_ID then
|
||||
if ev.code == C.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
|
||||
elseif ev.code == C.ABS_MT_TOUCH_MAJOR and ev.value == 0 then
|
||||
self:setCurrentMtSlot("id", -1)
|
||||
elseif ev.code == ABS_MT_POSITION_X then
|
||||
elseif ev.code == C.ABS_MT_POSITION_X then
|
||||
self:setCurrentMtSlot("x", ev.value)
|
||||
elseif ev.code == ABS_MT_POSITION_Y then
|
||||
elseif ev.code == C.ABS_MT_POSITION_Y then
|
||||
self:setCurrentMtSlot("y", ev.value)
|
||||
end
|
||||
elseif ev.type == EV_SYN then
|
||||
if ev.code == SYN_REPORT then
|
||||
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
|
||||
@@ -578,23 +679,23 @@ 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 ev.type == C.EV_ABS then
|
||||
if #self.MTSlots == 0 then
|
||||
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
|
||||
end
|
||||
if ev.code == ABS_X then
|
||||
if ev.code == C.ABS_X then
|
||||
self:setCurrentMtSlot("x", ev.value)
|
||||
elseif ev.code == ABS_Y then
|
||||
elseif ev.code == C.ABS_Y then
|
||||
self:setCurrentMtSlot("y", ev.value)
|
||||
elseif ev.code == ABS_PRESSURE then
|
||||
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 == EV_SYN then
|
||||
if ev.code == SYN_REPORT then
|
||||
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
|
||||
@@ -651,7 +752,7 @@ 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.code == C.MSC_RAW then
|
||||
if ev.value == MSC_RAW_GSENSOR_PORTRAIT_UP then
|
||||
-- i.e., UR
|
||||
rotation_mode = framebuffer.ORIENTATION_PORTRAIT
|
||||
@@ -774,90 +875,198 @@ end
|
||||
|
||||
|
||||
--- Main event handling.
|
||||
function Input:waitEvent(timeout_us)
|
||||
-- `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 for input.waitForEvents that will retry for some cases
|
||||
-- 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
|
||||
local wait_deadline = TimeVal:now() + TimeVal:new{
|
||||
usec = timeout_us
|
||||
}
|
||||
-- we don't block if there is any timer, set wait to 10us
|
||||
-- If we have timers set, we need to honor them once we're done draining the input events.
|
||||
while #self.timer_callbacks > 0 do
|
||||
ok, ev = pcall(input.waitForEvent, 100)
|
||||
-- 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
|
||||
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
|
||||
|
||||
-- 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
|
||||
-- Do we really need to clear all setTimeout after
|
||||
-- decided a gesture? FIXME
|
||||
self.timer_callbacks = {}
|
||||
-- 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 -- EOF if touch_ges
|
||||
end -- EOF if deadline reached
|
||||
else
|
||||
break
|
||||
end -- EOF if not exceed wait timeout
|
||||
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
|
||||
ok, ev = pcall(input.waitForEvent, timeout_us)
|
||||
end -- EOF if #timer_callbacks > 0
|
||||
-- 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
|
||||
|
||||
-- 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, true)
|
||||
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
|
||||
-- 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 and ev then
|
||||
if DEBUG.is_on 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))
|
||||
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 == EV_KEY then
|
||||
if ev.type == C.EV_KEY then
|
||||
return self:handleKeyBoardEv(ev)
|
||||
elseif ev.type == EV_ABS and ev.code == ABS_OASIS_ORIENTATION then
|
||||
elseif ev.type == C.EV_ABS and ev.code == ABS_OASIS_ORIENTATION then
|
||||
return self:handleOasisOrientationEv(ev)
|
||||
elseif ev.type == EV_ABS or ev.type == EV_SYN then
|
||||
elseif ev.type == C.EV_ABS or ev.type == C.EV_SYN then
|
||||
return self:handleTouchEv(ev)
|
||||
elseif ev.type == EV_MSC then
|
||||
elseif ev.type == C.EV_MSC then
|
||||
return self:handleMiscEv(ev)
|
||||
elseif ev.type == EV_SDL then
|
||||
elseif ev.type == C.EV_SDL then
|
||||
return self:handleSdlEv(ev)
|
||||
else
|
||||
-- some other kind of event that we do not know yet
|
||||
-- Received some other kind of event that we do not know how to specifically handle yet
|
||||
return Event:new("GenericInput", ev)
|
||||
end
|
||||
elseif not ok and ev then
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
local Generic = require("device/generic/device") -- <= look at this file!
|
||||
local TimeVal = require("ui/timeval")
|
||||
local logger = require("logger")
|
||||
|
||||
local function yes() return true end
|
||||
@@ -58,7 +57,6 @@ local Remarkable1 = Remarkable:new{
|
||||
|
||||
function Remarkable1:adjustTouchEvent(ev, by)
|
||||
if ev.type == EV_ABS then
|
||||
ev.time = TimeVal:now()
|
||||
-- Mirror X and Y and scale up both X & Y as touch input is different res from
|
||||
-- display
|
||||
if ev.code == ABS_MT_POSITION_X then
|
||||
@@ -81,7 +79,6 @@ local Remarkable2 = Remarkable:new{
|
||||
}
|
||||
|
||||
function Remarkable2:adjustTouchEvent(ev, by)
|
||||
ev.time = TimeVal:now()
|
||||
if ev.type == EV_ABS then
|
||||
-- Mirror Y and scale up both X & Y as touch input is different res from
|
||||
-- display
|
||||
|
||||
@@ -3,9 +3,9 @@ local TimeVal = require("ui/timeval")
|
||||
local GestureRange = {
|
||||
-- gesture matching type
|
||||
ges = nil,
|
||||
-- spatial range limits the gesture emitting position
|
||||
-- spatial range, limits the gesture emitting position
|
||||
range = nil,
|
||||
-- temproal range limits the gesture emitting rate
|
||||
-- temporal range, limits the gesture emitting rate
|
||||
rate = nil,
|
||||
-- scale limits of this gesture
|
||||
scale = nil,
|
||||
@@ -23,12 +23,11 @@ function GestureRange:match(gs)
|
||||
return false
|
||||
end
|
||||
if self.range then
|
||||
-- sometimes widget dimenension is not available when creating a gesturerage
|
||||
-- for some action, now we accept a range function that will be later called
|
||||
-- and the result of which will be used to check gesture match
|
||||
-- Sometimes the widget's dimensions are not available when creating a GestureRange
|
||||
-- for some action, so we accept a range function that will only be called at match() time instead.
|
||||
-- e.g. range = function() return self.dimen end
|
||||
-- for inputcontainer given that the x and y field of `self.dimen` is only
|
||||
-- filled when the inputcontainer is painted into blitbuffer
|
||||
-- That's because most widgets' dimensions are only set at paintTo() time:
|
||||
-- e.g., with InputContainer, the x and y fields of `self.dimen`.
|
||||
local range
|
||||
if type(self.range) == "function" then
|
||||
range = self.range()
|
||||
@@ -41,10 +40,9 @@ function GestureRange:match(gs)
|
||||
end
|
||||
|
||||
if self.rate then
|
||||
-- This filed restraints the upper limit rate(matches per second).
|
||||
-- It's most useful for e-ink devices with less powerfull CPUs and
|
||||
-- screens that cannot handle gesture events that otherwise will be
|
||||
-- generated
|
||||
-- This field sets up rate-limiting (in matches per second).
|
||||
-- It's mostly useful for e-Ink devices with less powerful CPUs
|
||||
-- and screens that cannot handle the amount of gesture events that would otherwise be generated.
|
||||
local last_time = self.last_time or TimeVal:new{}
|
||||
if gs.time - last_time > TimeVal:new{usec = 1000000 / self.rate} then
|
||||
self.last_time = gs.time
|
||||
|
||||
@@ -8,20 +8,47 @@ A simple module to module to compare and do arithmetic with time values.
|
||||
-- Do some stuff.
|
||||
-- You can add and subtract `TimeVal` objects.
|
||||
local tv_duration = TimeVal:now() - tv_start
|
||||
-- If you need more precision (like 2.5 s),
|
||||
-- you can add the milliseconds to the seconds.
|
||||
local tv_duration_seconds_float = tv_duration.sec + tv_duration.usec/1000000
|
||||
-- And convert that object to various more human-readable formats, e.g.,
|
||||
print(string.format("Stuff took %.3fms", tv_duration:tomsecs()))
|
||||
]]
|
||||
|
||||
local dbg = require("dbg")
|
||||
local ffi = require("ffi")
|
||||
require("ffi/posix_h")
|
||||
local util = require("ffi/util")
|
||||
|
||||
local C = ffi.C
|
||||
|
||||
-- We prefer CLOCK_MONOTONIC_COARSE if it's available and has a decent resolution,
|
||||
-- as we generally don't need nano/micro second precision,
|
||||
-- and it can be more than twice as fast as CLOCK_MONOTONIC/CLOCK_REALTIME/gettimeofday...
|
||||
local PREFERRED_MONOTONIC_CLOCKID = C.CLOCK_MONOTONIC
|
||||
-- Ditto for REALTIME (for :realtime_coarse only, :realtime uses gettimeofday ;)).
|
||||
local PREFERRED_REALTIME_CLOCKID = C.CLOCK_REALTIME
|
||||
if ffi.os == "Linux" then
|
||||
-- Unfortunately, it was only implemented in Linux 2.6.32, and we may run on older kernels than that...
|
||||
-- So, just probe it to see if we can rely on it.
|
||||
local probe_ts = ffi.new("struct timespec")
|
||||
if C.clock_getres(C.CLOCK_MONOTONIC_COARSE, probe_ts) == 0 then
|
||||
-- Now, it usually has a 1ms resolution on modern x86_64 systems,
|
||||
-- but it only provides a 10ms resolution on all my armv7 devices :/.
|
||||
if probe_ts.tv_sec == 0 and probe_ts.tv_nsec <= 1000000 then
|
||||
PREFERRED_MONOTONIC_CLOCKID = C.CLOCK_MONOTONIC_COARSE
|
||||
end
|
||||
end
|
||||
if C.clock_getres(C.CLOCK_REALTIME_COARSE, probe_ts) == 0 then
|
||||
if probe_ts.tv_sec == 0 and probe_ts.tv_nsec <= 1000000 then
|
||||
PREFERRED_REALTIME_CLOCKID = C.CLOCK_REALTIME_COARSE
|
||||
end
|
||||
end
|
||||
probe_ts = nil --luacheck: ignore
|
||||
end
|
||||
|
||||
--[[--
|
||||
TimeVal object.
|
||||
TimeVal object. Maps to a POSIX struct timeval (<sys/time.h>).
|
||||
|
||||
@table TimeVal
|
||||
@int sec floored number of seconds
|
||||
@int usec remaining number of milliseconds
|
||||
@int usec number of microseconds past that second.
|
||||
]]
|
||||
local TimeVal = {
|
||||
sec = 0,
|
||||
@@ -47,7 +74,7 @@ function TimeVal:new(from_o)
|
||||
if o.usec == nil then
|
||||
o.usec = 0
|
||||
elseif o.usec > 1000000 then
|
||||
o.sec = o.sec + math.floor(o.usec/1000000)
|
||||
o.sec = o.sec + math.floor(o.usec / 1000000)
|
||||
o.usec = o.usec % 1000000
|
||||
end
|
||||
setmetatable(o, self)
|
||||
@@ -55,55 +82,39 @@ function TimeVal:new(from_o)
|
||||
return o
|
||||
end
|
||||
|
||||
|
||||
-- Based on <bsd/sys/time.h>
|
||||
function TimeVal:__lt(time_b)
|
||||
if self.sec < time_b.sec then
|
||||
return true
|
||||
elseif self.sec > time_b.sec then
|
||||
return false
|
||||
if self.sec == time_b.sec then
|
||||
return self.usec < time_b.usec
|
||||
else
|
||||
-- self.sec == time_b.sec
|
||||
if self.usec < time_b.usec then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
return self.sec < time_b.sec
|
||||
end
|
||||
end
|
||||
|
||||
function TimeVal:__le(time_b)
|
||||
if self.sec < time_b.sec then
|
||||
return true
|
||||
elseif self.sec > time_b.sec then
|
||||
return false
|
||||
if self.sec == time_b.sec then
|
||||
return self.usec <= time_b.usec
|
||||
else
|
||||
-- self.sec == time_b.sec
|
||||
if self.usec > time_b.usec then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
return self.sec <= time_b.sec
|
||||
end
|
||||
end
|
||||
|
||||
function TimeVal:__eq(time_b)
|
||||
if self.sec == time_b.sec and self.usec == time_b.usec then
|
||||
return true
|
||||
if self.sec == time_b.sec then
|
||||
return self.usec == time_b.usec
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- If sec is negative, time went backwards!
|
||||
function TimeVal:__sub(time_b)
|
||||
local diff = TimeVal:new{}
|
||||
|
||||
diff.sec = self.sec - time_b.sec
|
||||
diff.usec = self.usec - time_b.usec
|
||||
|
||||
if diff.sec < 0 and diff.usec > 0 then
|
||||
diff.sec = diff.sec + 1
|
||||
diff.usec = diff.usec - 1000000
|
||||
elseif diff.sec > 0 and diff.usec < 0 then
|
||||
if diff.usec < 0 then
|
||||
diff.sec = diff.sec - 1
|
||||
diff.usec = diff.usec + 1000000
|
||||
end
|
||||
@@ -111,48 +122,127 @@ function TimeVal:__sub(time_b)
|
||||
return diff
|
||||
end
|
||||
|
||||
dbg:guard(TimeVal, '__sub',
|
||||
function(self, time_b)
|
||||
assert(self.sec > time_b.sec or (self.sec == time_b.sec and self.usec >= time_b.usec),
|
||||
"Subtract the first timeval from the latest, not vice versa.")
|
||||
end)
|
||||
|
||||
function TimeVal:__add(time_b)
|
||||
local sum = TimeVal:new{}
|
||||
|
||||
sum.sec = self.sec + time_b.sec
|
||||
sum.usec = self.usec + time_b.usec
|
||||
if sum.usec > 1000000 then
|
||||
sum.usec = sum.usec - 1000000
|
||||
sum.sec = sum.sec + 1
|
||||
end
|
||||
|
||||
if sum.sec < 0 and sum.usec > 0 then
|
||||
if sum.usec >= 1000000 then
|
||||
sum.sec = sum.sec + 1
|
||||
sum.usec = sum.usec - 1000000
|
||||
elseif sum.sec > 0 and sum.usec < 0 then
|
||||
sum.sec = sum.sec - 1
|
||||
sum.usec = sum.usec + 1000000
|
||||
end
|
||||
|
||||
return sum
|
||||
end
|
||||
|
||||
--[[--
|
||||
Creates a new TimeVal object based on the current time.
|
||||
Creates a new TimeVal object based on the current wall clock time.
|
||||
(e.g., gettimeofday / clock_gettime(CLOCK_REALTIME).
|
||||
|
||||
This is a simple wrapper around util.gettime() to get all the niceties of a TimeVal object.
|
||||
If you don't need sub-second precision, prefer os.time().
|
||||
Which means that, yes, this is a fancier POSIX Epoch ;).
|
||||
|
||||
@usage
|
||||
local TimeVal = require("ui/timeval")
|
||||
local tv_start = TimeVal:now()
|
||||
local tv_start = TimeVal:realtime()
|
||||
-- Do some stuff.
|
||||
-- You can add and substract `TimeVal` objects.
|
||||
local tv_duration = TimeVal:now() - tv_start
|
||||
local tv_duration = TimeVal:realtime() - tv_start
|
||||
|
||||
@treturn TimeVal
|
||||
]]
|
||||
function TimeVal:now()
|
||||
function TimeVal:realtime()
|
||||
local sec, usec = util.gettime()
|
||||
return TimeVal:new{sec = sec, usec = usec}
|
||||
end
|
||||
|
||||
--[[--
|
||||
Creates a new TimeVal object based on the current value from the system's MONOTONIC clock source.
|
||||
(e.g., clock_gettime(CLOCK_MONOTONIC).)
|
||||
|
||||
POSIX guarantees that this clock source will *never* go backwards (but it *may* return the same value multiple times).
|
||||
On Linux, this will not account for time spent with the device in suspend (unlike CLOCK_BOOTTIME).
|
||||
|
||||
@treturn TimeVal
|
||||
]]
|
||||
function TimeVal:monotonic()
|
||||
local timespec = ffi.new("struct timespec")
|
||||
C.clock_gettime(C.CLOCK_MONOTONIC, timespec)
|
||||
|
||||
-- TIMESPEC_TO_TIMEVAL
|
||||
return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))}
|
||||
end
|
||||
|
||||
--- Ditto, but w/ CLOCK_MONOTONIC_COARSE if it's available and has a 1ms resolution or better (uses CLOCK_MONOTONIC otherwise).
|
||||
function TimeVal:monotonic_coarse()
|
||||
local timespec = ffi.new("struct timespec")
|
||||
C.clock_gettime(PREFERRED_MONOTONIC_CLOCKID, timespec)
|
||||
|
||||
-- TIMESPEC_TO_TIMEVAL
|
||||
return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))}
|
||||
end
|
||||
|
||||
--- Ditto, but w/ CLOCK_REALTIME_COARSE if it's available and has a 1ms resolution or better (uses CLOCK_REALTIME otherwise).
|
||||
function TimeVal:realtime_coarse()
|
||||
local timespec = ffi.new("struct timespec")
|
||||
C.clock_gettime(PREFERRED_REALTIME_CLOCKID, timespec)
|
||||
|
||||
-- TIMESPEC_TO_TIMEVAL
|
||||
return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))}
|
||||
end
|
||||
|
||||
--- Ditto, but w/ CLOCK_BOOTTIME (will return a TimeVal set to 0, 0 if the clock source is unsupported, as it's 2.6.39+)
|
||||
function TimeVal:boottime()
|
||||
local timespec = ffi.new("struct timespec")
|
||||
C.clock_gettime(C.CLOCK_BOOTTIME, timespec)
|
||||
|
||||
-- TIMESPEC_TO_TIMEVAL
|
||||
return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))}
|
||||
end
|
||||
|
||||
--[[-- Alias for `monotonic_coarse`.
|
||||
|
||||
The assumption being anything that requires accurate timestamps expects a monotonic clock source.
|
||||
This is certainly true for KOReader's UI scheduling.
|
||||
]]
|
||||
TimeVal.now = TimeVal.monotonic_coarse
|
||||
|
||||
--- Converts a TimeVal object to a Lua (decimal) number (sec.usecs) (accurate to the ms, rounded to 4 decimal places)
|
||||
function TimeVal:tonumber()
|
||||
-- Round to 4 decimal places
|
||||
return math.floor((self.sec + self.usec / 1000000) * 10000) / 10000
|
||||
end
|
||||
|
||||
--- Converts a TimeVal object to a Lua (int) number (resolution: 1µs)
|
||||
function TimeVal:tousecs()
|
||||
return math.floor(self.sec * 1000000 + self.usec + 0.5)
|
||||
end
|
||||
|
||||
--[[-- Converts a TimeVal object to a Lua (int) number (resolution: 1ms).
|
||||
|
||||
(Mainly useful when computing a time lapse for benchmarking purposes).
|
||||
]]
|
||||
function TimeVal:tomsecs()
|
||||
return self:tousecs() / 1000
|
||||
end
|
||||
|
||||
--- Converts a Lua (decimal) number (sec.usecs) to a TimeVal object
|
||||
function TimeVal:fromnumber(seconds)
|
||||
local sec = math.floor(seconds)
|
||||
local usec = math.floor((seconds - sec) * 1000000 + 0.5)
|
||||
return TimeVal:new{sec = sec, usec = usec}
|
||||
end
|
||||
|
||||
--- Checks if a TimeVal object is positive
|
||||
function TimeVal:isPositive()
|
||||
return self.sec >= 0
|
||||
end
|
||||
|
||||
--- Checks if a TimeVal object is zero
|
||||
function TimeVal:isZero()
|
||||
return self.sec == 0 and self.usec == 0
|
||||
end
|
||||
|
||||
return TimeVal
|
||||
|
||||
@@ -5,6 +5,7 @@ This module manages widgets.
|
||||
local Device = require("device")
|
||||
local Event = require("ui/event")
|
||||
local Geom = require("ui/geometry")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local dbg = require("dbg")
|
||||
local logger = require("logger")
|
||||
local ffiUtil = require("ffi/util")
|
||||
@@ -13,7 +14,6 @@ local _ = require("gettext")
|
||||
local Input = Device.input
|
||||
local Screen = Device.screen
|
||||
|
||||
local MILLION = 1000000
|
||||
local DEFAULT_FULL_REFRESH_COUNT = 6
|
||||
|
||||
-- there is only one instance of this
|
||||
@@ -29,6 +29,7 @@ local UIManager = {
|
||||
event_handlers = nil,
|
||||
|
||||
_running = true,
|
||||
_now = TimeVal:now(),
|
||||
_window_stack = {},
|
||||
_task_queue = {},
|
||||
_task_queue_dirty = false,
|
||||
@@ -512,13 +513,11 @@ end
|
||||
function UIManager:schedule(time, action, ...)
|
||||
local p, s, e = 1, 1, #self._task_queue
|
||||
if e ~= 0 then
|
||||
local us = time[1] * MILLION + time[2]
|
||||
-- do a binary insert
|
||||
repeat
|
||||
p = math.floor(s + (e - s) / 2)
|
||||
local ptime = self._task_queue[p].time
|
||||
local ptus = ptime[1] * MILLION + ptime[2]
|
||||
if us > ptus then
|
||||
local p_time = self._task_queue[p].time
|
||||
if time > p_time then
|
||||
if s == e then
|
||||
p = e + 1
|
||||
break
|
||||
@@ -527,7 +526,7 @@ function UIManager:schedule(time, action, ...)
|
||||
else
|
||||
s = p
|
||||
end
|
||||
elseif us < ptus then
|
||||
elseif time < p_time then
|
||||
e = p
|
||||
if s == e then
|
||||
break
|
||||
@@ -549,29 +548,23 @@ function UIManager:schedule(time, action, ...)
|
||||
end
|
||||
dbg:guard(UIManager, 'schedule',
|
||||
function(self, time, action)
|
||||
assert(time[1] >= 0 and time[2] >= 0, "Only positive time allowed")
|
||||
assert(time.sec >= 0, "Only positive time allowed")
|
||||
assert(action ~= nil)
|
||||
end)
|
||||
|
||||
--[[--
|
||||
Schedules a task to be run a certain amount of seconds from now.
|
||||
|
||||
@number seconds scheduling delay in seconds (supports decimal values)
|
||||
@number seconds scheduling delay in seconds (supports decimal values, 1ms resolution).
|
||||
@func action reference to the task to be scheduled (may be anonymous)
|
||||
@param ... optional arguments passed to action
|
||||
|
||||
@see unschedule
|
||||
]]
|
||||
function UIManager:scheduleIn(seconds, action, ...)
|
||||
local when = { ffiUtil.gettime() }
|
||||
local s = math.floor(seconds)
|
||||
local usecs = (seconds - s) * MILLION
|
||||
when[1] = when[1] + s
|
||||
when[2] = when[2] + usecs
|
||||
if when[2] >= MILLION then
|
||||
when[1] = when[1] + 1
|
||||
when[2] = when[2] - MILLION
|
||||
end
|
||||
-- We might run significantly late inside an UI frame, so we can't use the cached value here.
|
||||
-- It would also cause some bad interactions with the way nextTick & co behave.
|
||||
local when = TimeVal:now() + TimeVal:fromnumber(seconds)
|
||||
self:schedule(when, action, ...)
|
||||
end
|
||||
dbg:guard(UIManager, 'scheduleIn',
|
||||
@@ -1049,7 +1042,7 @@ function UIManager:discardEvents(set_or_seconds)
|
||||
self._discard_events_till = nil
|
||||
return
|
||||
end
|
||||
local usecs
|
||||
local delay
|
||||
if set_or_seconds == true then
|
||||
-- Use an adequate delay to account for device refresh duration
|
||||
-- so any events happening in this delay (ie. before a widget
|
||||
@@ -1059,17 +1052,15 @@ function UIManager:discardEvents(set_or_seconds)
|
||||
-- sometimes > 500ms on some devices/temperatures.
|
||||
-- So, block for 400ms (to have it displayed) + 400ms
|
||||
-- for user reaction to it
|
||||
usecs = 800000
|
||||
delay = TimeVal:new{ usec = 800000 }
|
||||
else
|
||||
-- On non-eInk screen, display is usually instantaneous
|
||||
usecs = 400000
|
||||
delay = TimeVal:new{ usec = 400000 }
|
||||
end
|
||||
else -- we expect a number
|
||||
usecs = set_or_seconds * MILLION
|
||||
delay = TimeVal:new{ sec = set_or_seconds }
|
||||
end
|
||||
local now = { ffiUtil.gettime() }
|
||||
local now_us = now[1] * MILLION + now[2]
|
||||
self._discard_events_till = now_us + usecs
|
||||
self._discard_events_till = self._now + delay
|
||||
end
|
||||
|
||||
--[[--
|
||||
@@ -1082,9 +1073,7 @@ function UIManager:sendEvent(event)
|
||||
|
||||
-- Ensure discardEvents
|
||||
if self._discard_events_till then
|
||||
local now = { ffiUtil.gettime() }
|
||||
local now_us = now[1] * MILLION + now[2]
|
||||
if now_us < self._discard_events_till then
|
||||
if TimeVal:now() < self._discard_events_till then
|
||||
return
|
||||
else
|
||||
self._discard_events_till = nil
|
||||
@@ -1159,8 +1148,7 @@ function UIManager:broadcastEvent(event)
|
||||
end
|
||||
|
||||
function UIManager:_checkTasks()
|
||||
local now = { ffiUtil.gettime() }
|
||||
local now_us = now[1] * MILLION + now[2]
|
||||
self._now = TimeVal:now()
|
||||
local wait_until = nil
|
||||
|
||||
-- task.action may schedule other events
|
||||
@@ -1172,11 +1160,8 @@ function UIManager:_checkTasks()
|
||||
break
|
||||
end
|
||||
local task = self._task_queue[1]
|
||||
local task_us = 0
|
||||
if task.time ~= nil then
|
||||
task_us = task.time[1] * MILLION + task.time[2]
|
||||
end
|
||||
if task_us <= now_us then
|
||||
local task_tv = task.time or TimeVal:new{}
|
||||
if task_tv <= self._now then
|
||||
-- remove from table
|
||||
table.remove(self._task_queue, 1)
|
||||
-- task is pending to be executed right now. do it.
|
||||
@@ -1191,7 +1176,26 @@ function UIManager:_checkTasks()
|
||||
end
|
||||
end
|
||||
|
||||
return wait_until, now
|
||||
return wait_until, self._now
|
||||
end
|
||||
|
||||
--[[--
|
||||
Returns a TimeVal object corresponding to the last UI tick.
|
||||
|
||||
This is essentially a cached TimeVal:now(), computed at the top of every iteration of the main UI loop,
|
||||
(right before checking/running scheduled tasks).
|
||||
This is mainly useful to compute/schedule stuff in the same time scale as the UI loop (i.e., MONOTONIC),
|
||||
without having to resort to a syscall.
|
||||
It should never be significantly stale (i.e., it should be precise enough),
|
||||
unless you're blocking the UI for a significant amount of time in the same UI tick.
|
||||
|
||||
Prefer the appropriate TimeVal method for your needs if you require perfect accuracy
|
||||
(e.g., when you're actually working on the event loop *itself* (UIManager, Input, GestureDetector)).
|
||||
|
||||
This is *NOT* wall clock time (REALTIME).
|
||||
]]
|
||||
function UIManager:getTime()
|
||||
return self._now
|
||||
end
|
||||
|
||||
-- precedence of refresh modes:
|
||||
@@ -1580,29 +1584,35 @@ function UIManager:handleInput()
|
||||
self:processZMQs()
|
||||
|
||||
-- Figure out how long to wait.
|
||||
-- Ultimately, that'll be the earliest of INPUT_TIMEOUT, ZMQ_TIMEOUT or the next earliest scheduled task.
|
||||
local deadline
|
||||
-- Default to INPUT_TIMEOUT (which may be nil, i.e. block until an event happens).
|
||||
local wait_us = self.INPUT_TIMEOUT
|
||||
|
||||
-- If there's a timed event pending, that puts an upper bound on how long to wait.
|
||||
if wait_until then
|
||||
wait_us = math.min(
|
||||
wait_us or math.huge,
|
||||
(wait_until[1] - now[1]) * MILLION
|
||||
+ (wait_until[2] - now[2]))
|
||||
end
|
||||
|
||||
-- If we have any ZMQs registered, ZMQ_TIMEOUT is another upper bound.
|
||||
if #self._zeromqs > 0 then
|
||||
wait_us = math.min(wait_us or math.huge, self.ZMQ_TIMEOUT)
|
||||
end
|
||||
|
||||
-- We pass that on as an absolute deadline, not a relative wait time.
|
||||
if wait_us then
|
||||
deadline = now + TimeVal:new{ usec = wait_us }
|
||||
end
|
||||
|
||||
-- If there's a scheduled task pending, that puts an upper bound on how long to wait.
|
||||
if wait_until and (not deadline or wait_until < deadline) then
|
||||
-- ^ We don't have a TIMEOUT induced deadline, making the choice easy.
|
||||
-- ^ We have a task scheduled for *before* our TIMEOUT induced deadline.
|
||||
deadline = wait_until
|
||||
end
|
||||
|
||||
-- If allowed, entering standby (from which we can wake by input) must trigger in response to event
|
||||
-- this function emits (plugin), or within waitEvent() right after (hardware).
|
||||
-- Anywhere else breaks preventStandby/allowStandby invariants used by background jobs while UI is left running.
|
||||
self:_standbyTransition()
|
||||
|
||||
-- wait for next event
|
||||
local input_event = Input:waitEvent(wait_us)
|
||||
local input_event = Input:waitEvent(now, deadline)
|
||||
|
||||
-- delegate input_event to handler
|
||||
if input_event then
|
||||
@@ -1672,6 +1682,9 @@ end
|
||||
function UIManager:_beforeSuspend()
|
||||
self:flushSettings()
|
||||
self:broadcastEvent(Event:new("Suspend"))
|
||||
|
||||
-- Reset gesture detection state to a blank slate (anything power-management related emits KEY events, which don't need gesture detection).
|
||||
Input:resetState()
|
||||
end
|
||||
|
||||
-- The common operations that should be performed after resuming the device.
|
||||
@@ -1772,5 +1785,11 @@ function UIManager:restartKOReader()
|
||||
self._exit_code = 85
|
||||
end
|
||||
|
||||
--- Sanely abort KOReader (e.g., exit sanely, but with a non-zero return code).
|
||||
function UIManager:abort()
|
||||
self:quit()
|
||||
self._exit_code = 1
|
||||
end
|
||||
|
||||
UIManager:init()
|
||||
return UIManager
|
||||
|
||||
@@ -20,6 +20,7 @@ local ScrollHtmlWidget = require("ui/widget/scrollhtmlwidget")
|
||||
local ScrollTextWidget = require("ui/widget/scrolltextwidget")
|
||||
local Size = require("ui/size")
|
||||
local TextWidget = require("ui/widget/textwidget")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
local VerticalSpan = require("ui/widget/verticalspan")
|
||||
@@ -153,7 +154,7 @@ function DictQuickLookup:init()
|
||||
-- callback function when HoldReleaseText is handled as args
|
||||
args = function(text, hold_duration)
|
||||
local lookup_target
|
||||
if hold_duration < 3.0 then
|
||||
if hold_duration < TimeVal:new{ sec = 3 } then
|
||||
-- do this lookup in the same domain (dict/wikipedia)
|
||||
lookup_target = self.is_wiki and "LookupWikipedia" or "LookupWord"
|
||||
else
|
||||
|
||||
@@ -13,6 +13,7 @@ local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local LineWidget = require("ui/widget/linewidget")
|
||||
local ScrollHtmlWidget = require("ui/widget/scrollhtmlwidget")
|
||||
local Size = require("ui/size")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
local VerticalSpan = require("ui/widget/verticalspan")
|
||||
@@ -194,7 +195,7 @@ function FootnoteWidget:init()
|
||||
-- callback function when HoldReleaseText is handled as args
|
||||
args = function(text, hold_duration)
|
||||
if self.dialog then
|
||||
local lookup_target = hold_duration < 3.0 and "LookupWord" or "LookupWikipedia"
|
||||
local lookup_target = hold_duration < TimeVal:new{ sec = 3 } and "LookupWord" or "LookupWikipedia"
|
||||
self.dialog:handleEvent(
|
||||
Event:new(lookup_target, text)
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ local GestureRange = require("ui/gesturerange")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local Mupdf = require("ffi/mupdf")
|
||||
local Screen = Device.screen
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local logger = require("logger")
|
||||
local util = require("util")
|
||||
|
||||
@@ -165,7 +165,7 @@ function HtmlBoxWidget:onHoldStartText(_, ges)
|
||||
return false -- let event be processed by other widgets
|
||||
end
|
||||
|
||||
self.hold_start_tv = TimeVal.now()
|
||||
self.hold_start_tv = UIManager:getTime()
|
||||
|
||||
return true
|
||||
end
|
||||
@@ -229,8 +229,7 @@ function HtmlBoxWidget:onHoldReleaseText(callback, ges)
|
||||
return false
|
||||
end
|
||||
|
||||
local hold_duration = TimeVal.now() - self.hold_start_tv
|
||||
hold_duration = hold_duration.sec + (hold_duration.usec/1000000)
|
||||
local hold_duration = UIManager:getTime() - self.hold_start_tv
|
||||
|
||||
local page = self.document:openPage(self.page_number)
|
||||
local lines = page:getPageText()
|
||||
|
||||
@@ -13,6 +13,7 @@ local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local RectSpan = require("ui/widget/rectspan")
|
||||
local Size = require("ui/size")
|
||||
local TextWidget = require("ui/widget/textwidget")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
local Input = Device.input
|
||||
@@ -73,7 +74,7 @@ function Notification:init()
|
||||
local notif_height = self.frame:getSize().h
|
||||
|
||||
self:_cleanShownStack()
|
||||
table.insert(Notification._nums_shown, os.time())
|
||||
table.insert(Notification._nums_shown, UIManager:getTime())
|
||||
self.num = #Notification._nums_shown
|
||||
|
||||
self[1] = VerticalGroup:new{
|
||||
@@ -101,9 +102,9 @@ function Notification:_cleanShownStack(num)
|
||||
-- to follow what is happening).
|
||||
-- As a sanity check, we also forget those shown for
|
||||
-- more than 30s in case no close event was received.
|
||||
local expire_ts = os.time() - 30
|
||||
local expire_tv = UIManager:getTime() - TimeVal:new{ sec = 30 }
|
||||
for i=#Notification._nums_shown, 1, -1 do
|
||||
if Notification._nums_shown[i] and Notification._nums_shown[i] > expire_ts then
|
||||
if Notification._nums_shown[i] and Notification._nums_shown[i] > expire_tv then
|
||||
break -- still shown (or not yet expired)
|
||||
end
|
||||
table.remove(Notification._nums_shown, i)
|
||||
|
||||
@@ -24,7 +24,6 @@ local RenderText = require("ui/rendertext")
|
||||
local RightContainer = require("ui/widget/container/rightcontainer")
|
||||
local Size = require("ui/size")
|
||||
local TextWidget = require("ui/widget/textwidget")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Math = require("optmath")
|
||||
local logger = require("logger")
|
||||
@@ -1790,7 +1789,7 @@ function TextBoxWidget:onHoldStartText(_, ges)
|
||||
return false -- let event be processed by other widgets
|
||||
end
|
||||
|
||||
self.hold_start_tv = TimeVal.now()
|
||||
self.hold_start_tv = UIManager:getTime()
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -1822,8 +1821,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges)
|
||||
return false
|
||||
end
|
||||
|
||||
local hold_duration = TimeVal.now() - self.hold_start_tv
|
||||
hold_duration = hold_duration.sec + hold_duration.usec/1000000
|
||||
local hold_duration = UIManager:getTime() - self.hold_start_tv
|
||||
|
||||
-- If page contains an image, check if Hold is on this image and deal
|
||||
-- with it directly
|
||||
@@ -1917,7 +1915,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges)
|
||||
-- to consider when looking for word boundaries)
|
||||
local selected_text = self._xtext:getSelectedWords(sel_start_idx, sel_end_idx, 50)
|
||||
|
||||
logger.dbg("onHoldReleaseText (duration:", hold_duration, ") :",
|
||||
logger.dbg("onHoldReleaseText (duration:", hold_duration:tonumber(), ") :",
|
||||
sel_start_idx, ">", sel_end_idx, "=", selected_text)
|
||||
callback(selected_text, hold_duration)
|
||||
return true
|
||||
@@ -1935,7 +1933,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges)
|
||||
end
|
||||
|
||||
local selected_text = table.concat(self.charlist, "", sel_start_idx, sel_end_idx)
|
||||
logger.dbg("onHoldReleaseText (duration:", hold_duration, ") :", sel_start_idx, ">", sel_end_idx, "=", selected_text)
|
||||
logger.dbg("onHoldReleaseText (duration:", hold_duration:tonumber(), ") :", sel_start_idx, ">", sel_end_idx, "=", selected_text)
|
||||
callback(selected_text, hold_duration)
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -16,6 +16,7 @@ local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local KeyboardLayoutDialog = require("ui/widget/keyboardlayoutdialog")
|
||||
local Size = require("ui/size")
|
||||
local TextWidget = require("ui/widget/textwidget")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
local VerticalSpan = require("ui/widget/verticalspan")
|
||||
@@ -588,6 +589,7 @@ function VirtualKeyPopup:init()
|
||||
},
|
||||
}
|
||||
self.tap_interval_override = G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0
|
||||
self.tap_interval_override = TimeVal:new{ usec = self.tap_interval_override }
|
||||
|
||||
if Device:hasDPad() then
|
||||
self.key_events.PressKey = { {"Press"}, doc = "select key" }
|
||||
@@ -699,6 +701,7 @@ function VirtualKeyboard:init()
|
||||
self.max_layer = keyboard.max_layer
|
||||
self:initLayer(self.keyboard_layer)
|
||||
self.tap_interval_override = G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0
|
||||
self.tap_interval_override = TimeVal:new{ usec = self.tap_interval_override }
|
||||
if Device:hasDPad() then
|
||||
self.key_events.PressKey = { {"Press"}, doc = "select key" }
|
||||
end
|
||||
|
||||
@@ -63,7 +63,7 @@ function AutoStandby:addToMainMenu(menu_items)
|
||||
}
|
||||
end
|
||||
|
||||
-- We've received touch/key event, so delay stadby accordingly
|
||||
-- We've received touch/key event, so delay standby accordingly
|
||||
function AutoStandby:onInputEvent()
|
||||
logger.dbg("AutoStandby:onInputevent() instance=", tostring(self))
|
||||
local config = self.settings.data
|
||||
|
||||
@@ -10,6 +10,7 @@ if not Device:isCervantes() and
|
||||
end
|
||||
|
||||
local PluginShare = require("pluginshare")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local logger = require("logger")
|
||||
@@ -24,7 +25,7 @@ local AutoSuspend = WidgetContainer:new{
|
||||
is_doc_only = false,
|
||||
autoshutdown_timeout_seconds = G_reader_settings:readSetting("autoshutdown_timeout_seconds") or default_autoshutdown_timeout_seconds,
|
||||
auto_suspend_timeout_seconds = G_reader_settings:readSetting("auto_suspend_timeout_seconds") or default_auto_suspend_timeout_seconds,
|
||||
last_action_sec = os.time(),
|
||||
last_action_tv = TimeVal:now(),
|
||||
standby_prevented = false,
|
||||
}
|
||||
|
||||
@@ -48,9 +49,11 @@ function AutoSuspend:_schedule(shutdown_only)
|
||||
delay_suspend = self.auto_suspend_timeout_seconds
|
||||
delay_shutdown = self.autoshutdown_timeout_seconds
|
||||
else
|
||||
local now_ts = os.time()
|
||||
delay_suspend = self.last_action_sec + self.auto_suspend_timeout_seconds - now_ts
|
||||
delay_shutdown = self.last_action_sec + self.autoshutdown_timeout_seconds - now_ts
|
||||
local now_tv = UIManager:getTime()
|
||||
delay_suspend = self.last_action_tv + TimeVal:new{ sec = self.auto_suspend_timeout_seconds } - now_tv
|
||||
delay_suspend = delay_suspend:tonumber()
|
||||
delay_shutdown = self.last_action_tv + TimeVal:new{ sec = self.autoshutdown_timeout_seconds } - now_tv
|
||||
delay_shutdown = delay_shutdown:tonumber()
|
||||
end
|
||||
|
||||
-- Try to shutdown first, as we may have been woken up from suspend just for the sole purpose of doing that.
|
||||
@@ -79,9 +82,9 @@ end
|
||||
|
||||
function AutoSuspend:_start()
|
||||
if self:_enabled() or self:_enabledShutdown() then
|
||||
local now_ts = os.time()
|
||||
logger.dbg("AutoSuspend: start at", now_ts)
|
||||
self.last_action_sec = now_ts
|
||||
local now_tv = UIManager:getTime()
|
||||
logger.dbg("AutoSuspend: start at", now_tv:tonumber())
|
||||
self.last_action_tv = now_tv
|
||||
self:_schedule()
|
||||
end
|
||||
end
|
||||
@@ -89,9 +92,9 @@ end
|
||||
-- Variant that only re-engages the shutdown timer for onUnexpectedWakeupLimit
|
||||
function AutoSuspend:_restart()
|
||||
if self:_enabledShutdown() then
|
||||
local now_ts = os.time()
|
||||
logger.dbg("AutoSuspend: restart at", now_ts)
|
||||
self.last_action_sec = now_ts
|
||||
local now_tv = UIManager:getTime()
|
||||
logger.dbg("AutoSuspend: restart at", now_tv:tonumber())
|
||||
self.last_action_tv = now_tv
|
||||
self:_schedule(true)
|
||||
end
|
||||
end
|
||||
@@ -108,7 +111,7 @@ end
|
||||
|
||||
function AutoSuspend:onInputEvent()
|
||||
logger.dbg("AutoSuspend: onInputEvent")
|
||||
self.last_action_sec = os.time()
|
||||
self.last_action_tv = UIManager:getTime()
|
||||
end
|
||||
|
||||
function AutoSuspend:onSuspend()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
local Device = require("device")
|
||||
local Event = require("ui/event")
|
||||
local PluginShare = require("pluginshare")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local logger = require("logger")
|
||||
@@ -10,11 +11,11 @@ local T = require("ffi/util").template
|
||||
local AutoTurn = WidgetContainer:new{
|
||||
name = "autoturn",
|
||||
is_doc_only = true,
|
||||
autoturn_sec = G_reader_settings:readSetting("autoturn_timeout_seconds") or 0,
|
||||
autoturn_distance = G_reader_settings:readSetting("autoturn_distance") or 1,
|
||||
enabled = G_reader_settings:isTrue("autoturn_enabled"),
|
||||
autoturn_sec = 0,
|
||||
autoturn_distance = 1,
|
||||
enabled = false,
|
||||
settings_id = 0,
|
||||
last_action_sec = os.time(),
|
||||
last_action_tv = TimeVal:now(),
|
||||
}
|
||||
|
||||
function AutoTurn:_enabled()
|
||||
@@ -34,7 +35,8 @@ function AutoTurn:_schedule(settings_id)
|
||||
return
|
||||
end
|
||||
|
||||
local delay = self.last_action_sec + self.autoturn_sec - os.time()
|
||||
local delay = self.last_action_tv + TimeVal:new{ sec = self.autoturn_sec } - UIManager:getTime()
|
||||
delay = delay:tonumber()
|
||||
|
||||
if delay <= 0 then
|
||||
if UIManager:getTopWidget() == "ReaderUI" then
|
||||
@@ -57,10 +59,10 @@ end
|
||||
|
||||
function AutoTurn:_start()
|
||||
if self:_enabled() then
|
||||
local now_ts = os.time()
|
||||
logger.dbg("AutoTurn: start at", now_ts)
|
||||
local now_tv = UIManager:getTime()
|
||||
logger.dbg("AutoTurn: start at", now_tv:tonumber())
|
||||
PluginShare.pause_auto_suspend = true
|
||||
self.last_action_sec = now_ts
|
||||
self.last_action_tv = now_tv
|
||||
self:_schedule(self.settings_id)
|
||||
|
||||
local text
|
||||
@@ -83,7 +85,10 @@ end
|
||||
|
||||
function AutoTurn:init()
|
||||
UIManager.event_hook:registerWidget("InputEvent", self)
|
||||
self.autoturn_sec = self.settings
|
||||
self.autoturn_sec = G_reader_settings:readSetting("autoturn_timeout_seconds") or 0
|
||||
self.autoturn_distance = G_reader_settings:readSetting("autoturn_distance") or 1
|
||||
self.enabled = G_reader_settings:isTrue("autoturn_enabled")
|
||||
self.settings_id = 0
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
self:_deprecateLastTask()
|
||||
self:_start()
|
||||
@@ -96,7 +101,7 @@ end
|
||||
|
||||
function AutoTurn:onInputEvent()
|
||||
logger.dbg("AutoTurn: onInputEvent")
|
||||
self.last_action_sec = os.time()
|
||||
self.last_action_tv = UIManager:getTime()
|
||||
end
|
||||
|
||||
-- We do not want autoturn to turn pages during the suspend process.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
local logger = require("logger")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
|
||||
local CommandRunner = {
|
||||
@@ -36,7 +37,7 @@ function CommandRunner:start(job)
|
||||
assert(self.pio == nil)
|
||||
assert(self.job == nil)
|
||||
self.job = job
|
||||
self.job.start_sec = os.time()
|
||||
self.job.start_tv = UIManager:getTime()
|
||||
assert(type(self.job.executable) == "string")
|
||||
local command = self:createEnvironment() .. " " ..
|
||||
"sh plugins/backgroundrunner.koplugin/luawrapper.sh " ..
|
||||
@@ -76,7 +77,7 @@ function CommandRunner:poll()
|
||||
UIManager:allowStandby()
|
||||
self.pio:close()
|
||||
self.pio = nil
|
||||
self.job.end_sec = os.time()
|
||||
self.job.end_tv = TimeVal:now()
|
||||
local job = self.job
|
||||
self.job = nil
|
||||
return job
|
||||
|
||||
@@ -9,13 +9,15 @@ end
|
||||
|
||||
local CommandRunner = require("commandrunner")
|
||||
local PluginShare = require("pluginshare")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local logger = require("logger")
|
||||
local _ = require("gettext")
|
||||
|
||||
-- BackgroundRunner is an experimental feature to execute non-critical jobs in
|
||||
-- background. A job is defined as a table in PluginShare.backgroundJobs table.
|
||||
-- the background.
|
||||
-- A job is defined as a table in PluginShare.backgroundJobs table.
|
||||
-- It contains at least following items:
|
||||
-- when: number, string or function
|
||||
-- number: the delay in seconds
|
||||
@@ -26,9 +28,9 @@ local _ = require("gettext")
|
||||
-- executed immediately.
|
||||
--
|
||||
-- repeated: boolean or function or nil or number
|
||||
-- boolean: true to repeated the job once it finished.
|
||||
-- function: if the return value of the function is true, repeated the job
|
||||
-- once it finished. If the function throws an error, it equals to
|
||||
-- boolean: true to repeat the job once it finished.
|
||||
-- function: if the return value of the function is true, repeat the job
|
||||
-- once it finishes. If the function throws an error, it equals to
|
||||
-- return false.
|
||||
-- nil: same as false.
|
||||
-- number: times to repeat.
|
||||
@@ -70,9 +72,10 @@ local _ = require("gettext")
|
||||
-- bad_command: boolean, whether the command is not found. Not available for
|
||||
-- function executable.
|
||||
-- blocked: boolean, whether the job is blocked.
|
||||
-- start_sec: number, the os.time() when the job was started.
|
||||
-- end_sec: number, the os.time() when the job was stopped.
|
||||
-- insert_sec: number, the os.time() when the job was inserted into queue.
|
||||
-- start_tv: number, the TimeVal when the job was started.
|
||||
-- end_tv: number, the TimeVal when the job was stopped.
|
||||
-- insert_tv: number, the TimeVal when the job was inserted into queue.
|
||||
-- (All of them in the monotonic time scale, like the main event loop & task queue).
|
||||
|
||||
local BackgroundRunner = {
|
||||
jobs = PluginShare.backgroundJobs,
|
||||
@@ -114,7 +117,9 @@ end
|
||||
function BackgroundRunner:_finishJob(job)
|
||||
assert(self ~= nil)
|
||||
if type(job.executable) == "function" then
|
||||
job.timeout = ((job.end_sec - job.start_sec) > 1)
|
||||
local tv_diff = job.end_tv - job.start_tv
|
||||
local threshold = TimeVal:new{ sec = 1 }
|
||||
job.timeout = (tv_diff > threshold)
|
||||
end
|
||||
job.blocked = job.timeout
|
||||
if not job.blocked and self:_shouldRepeat(job) then
|
||||
@@ -136,7 +141,7 @@ function BackgroundRunner:_executeJob(job)
|
||||
CommandRunner:start(job)
|
||||
return true
|
||||
elseif type(job.executable) == "function" then
|
||||
job.start_sec = os.time()
|
||||
job.start_tv = UIManager:getTime()
|
||||
local status, err = pcall(job.executable)
|
||||
if status then
|
||||
job.result = 0
|
||||
@@ -144,7 +149,7 @@ function BackgroundRunner:_executeJob(job)
|
||||
job.result = 1
|
||||
job.exception = err
|
||||
end
|
||||
job.end_sec = os.time()
|
||||
job.end_tv = TimeVal:now()
|
||||
self:_finishJob(job)
|
||||
return true
|
||||
else
|
||||
@@ -171,10 +176,10 @@ function BackgroundRunner:_execute()
|
||||
local round = 0
|
||||
while #self.jobs > 0 do
|
||||
local job = table.remove(self.jobs, 1)
|
||||
if job.insert_sec == nil then
|
||||
-- Jobs are first inserted to jobs table from external users. So
|
||||
-- they may not have insert_sec field.
|
||||
job.insert_sec = os.time()
|
||||
if job.insert_tv == nil then
|
||||
-- Jobs are first inserted to jobs table from external users.
|
||||
-- So they may not have an insert field.
|
||||
job.insert_tv = UIManager:getTime()
|
||||
end
|
||||
local should_execute = false
|
||||
local should_ignore = false
|
||||
@@ -187,7 +192,7 @@ function BackgroundRunner:_execute()
|
||||
end
|
||||
elseif type(job.when) == "number" then
|
||||
if job.when >= 0 then
|
||||
should_execute = ((os.time() - job.insert_sec) >= job.when)
|
||||
should_execute = ((UIManager:getTime() - job.insert_tv) >= TimeVal:fromnumber(job.when))
|
||||
else
|
||||
should_ignore = true
|
||||
end
|
||||
@@ -248,7 +253,7 @@ end
|
||||
|
||||
function BackgroundRunner:_insert(job)
|
||||
assert(self ~= nil)
|
||||
job.insert_sec = os.time()
|
||||
job.insert_tv = UIManager:getTime()
|
||||
table.insert(self.jobs, job)
|
||||
end
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ of storing it.
|
||||
@module koplugin.calibre.metadata
|
||||
--]]--
|
||||
|
||||
local TimeVal = require("ui/timeval")
|
||||
local lfs = require("libs/libkoreader-lfs")
|
||||
local rapidjson = require("rapidjson")
|
||||
local logger = require("logger")
|
||||
@@ -232,14 +233,13 @@ end
|
||||
-- in a given path. It will find calibre files if they're on disk and
|
||||
-- try to load info from them.
|
||||
|
||||
-- NOTE: you should care about the books table, because it could be huge.
|
||||
-- NOTE: Take special notice of the books table, because it could be huge.
|
||||
-- If you're not working with the metadata directly (ie: in wireless connections)
|
||||
-- you should copy relevant data to another table and free this one to keep things tidy.
|
||||
|
||||
function CalibreMetadata:init(dir, is_search)
|
||||
if not dir then return end
|
||||
local socket = require("socket")
|
||||
local start = socket.gettime()
|
||||
local start = TimeVal:now()
|
||||
self.path = dir
|
||||
local ok_meta, ok_drive, file_meta, file_drive = findCalibreFiles(dir)
|
||||
self.driveinfo = file_drive
|
||||
@@ -256,13 +256,13 @@ function CalibreMetadata:init(dir, is_search)
|
||||
|
||||
local msg
|
||||
if is_search then
|
||||
msg = string.format("(search) in %f milliseconds: %d books",
|
||||
(socket.gettime() - start) * 1000, #self.books)
|
||||
msg = string.format("(search) in %.3f milliseconds: %d books",
|
||||
(TimeVal:now() - start):tomsecs(), #self.books)
|
||||
else
|
||||
local deleted_count = self:prune()
|
||||
self:cleanUnused()
|
||||
msg = string.format("in %f milliseconds: %d books. %d pruned",
|
||||
(socket.gettime() - start) * 1000, #self.books, deleted_count)
|
||||
msg = string.format("in %.3f milliseconds: %d books. %d pruned",
|
||||
(TimeVal:now() - start):tomsecs(), #self.books, deleted_count)
|
||||
end
|
||||
logger.info(string.format("calibre info loaded from disk %s", msg))
|
||||
return true
|
||||
|
||||
@@ -16,9 +16,9 @@ local Menu = require("ui/widget/menu")
|
||||
local Persist = require("persist")
|
||||
local Screen = require("device").screen
|
||||
local Size = require("ui/size")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local logger = require("logger")
|
||||
local socket = require("socket")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
local T = require("ffi/util").template
|
||||
@@ -323,7 +323,7 @@ function CalibreSearch:find(option)
|
||||
end
|
||||
|
||||
-- measure time elapsed searching
|
||||
local start = socket.gettime()
|
||||
local start = TimeVal:now()
|
||||
if option == "find" then
|
||||
local books = self:findBooks(self.search_value)
|
||||
local result = self:bookCatalog(books)
|
||||
@@ -331,9 +331,8 @@ function CalibreSearch:find(option)
|
||||
else
|
||||
self:browse(option,1)
|
||||
end
|
||||
local elapsed = socket.gettime() - start
|
||||
logger.info(string.format("search done in %f milliseconds (%s, %s, %s, %s, %s)",
|
||||
elapsed * 1000,
|
||||
logger.info(string.format("search done in %.3f milliseconds (%s, %s, %s, %s, %s)",
|
||||
(TimeVal:now() - start):tomsecs(),
|
||||
option == "find" and "books" or option,
|
||||
"case sensitive: " .. tostring(not self.case_insensitive),
|
||||
"title: " .. tostring(self.find_by_title),
|
||||
@@ -556,8 +555,8 @@ end
|
||||
|
||||
-- get metadata from cache or calibre files
|
||||
function CalibreSearch:getMetadata()
|
||||
local start = socket.gettime()
|
||||
local template = "metadata: %d books imported from %s in %f milliseconds"
|
||||
local start = TimeVal:now()
|
||||
local template = "metadata: %d books imported from %s in %.3f milliseconds"
|
||||
|
||||
-- try to load metadata from cache
|
||||
if self.cache_metadata then
|
||||
@@ -581,8 +580,7 @@ function CalibreSearch:getMetadata()
|
||||
end
|
||||
end
|
||||
if is_newer then
|
||||
local elapsed = socket.gettime() - start
|
||||
logger.info(string.format(template, #cache, "cache", elapsed * 1000))
|
||||
logger.info(string.format(template, #cache, "cache", (TimeVal:now() - start):tomsecs()))
|
||||
return cache
|
||||
else
|
||||
logger.warn("cache is older than metadata, ignoring it")
|
||||
@@ -607,8 +605,7 @@ function CalibreSearch:getMetadata()
|
||||
end
|
||||
self.cache_books:save(serialized_table)
|
||||
end
|
||||
local elapsed = socket.gettime() - start
|
||||
logger.info(string.format(template, #books, "calibre", elapsed * 1000))
|
||||
logger.info(string.format(template, #books, "calibre", (TimeVal:now() - start):tomsecs()))
|
||||
return books
|
||||
end
|
||||
|
||||
|
||||
@@ -691,7 +691,6 @@ function BookInfoManager:extractInBackground(files)
|
||||
local cover_specs = files[idx].cover_specs
|
||||
logger.dbg(" BG extracting:", filepath)
|
||||
self:extractBookInfo(filepath, cover_specs)
|
||||
FFIUtil.usleep(100000) -- give main process 100ms of free cpu to do its processing
|
||||
end
|
||||
logger.dbg(" BG extraction done")
|
||||
end
|
||||
|
||||
@@ -46,8 +46,6 @@ if lang_locale then
|
||||
_.changeLang(lang_locale)
|
||||
end
|
||||
|
||||
local dummy = require("ffi/posix_h")
|
||||
|
||||
-- Try to turn the C blitter on/off, and synchronize setting so that UI config reflects real state
|
||||
local bb = require("ffi/blitbuffer")
|
||||
bb:setUseCBB(is_cbb_enabled)
|
||||
|
||||
@@ -8,7 +8,7 @@ package.cpath =
|
||||
|
||||
-- set search path for 'ffi.load()'
|
||||
local ffi = require("ffi")
|
||||
local dummy = require("ffi/posix_h")
|
||||
require("ffi/posix_h")
|
||||
local C = ffi.C
|
||||
if ffi.os == "Windows" then
|
||||
C._putenv("PATH=libs;common;")
|
||||
|
||||
@@ -16,6 +16,8 @@ describe("AutoSuspend", function()
|
||||
UIManager._run_forever = true
|
||||
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10)
|
||||
require("mock_time"):install()
|
||||
-- Reset UIManager:getTime()
|
||||
UIManager:handleInput()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
@@ -36,7 +38,6 @@ describe("AutoSuspend", function()
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
|
||||
it("should be able to deprecate last task", function()
|
||||
@@ -56,7 +57,6 @@ describe("AutoSuspend", function()
|
||||
mock_time:increase(5)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.suspend).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -74,6 +74,8 @@ describe("AutoSuspend", function()
|
||||
UIManager._run_forever = true
|
||||
G_reader_settings:saveSetting("autoshutdown_timeout_seconds", 10)
|
||||
require("mock_time"):install()
|
||||
-- Reset UIManager:getTime()
|
||||
UIManager:handleInput()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
@@ -94,7 +96,6 @@ describe("AutoSuspend", function()
|
||||
mock_time:increase(6)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.poweroff_action).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
|
||||
it("should be able to deprecate last task", function()
|
||||
@@ -114,7 +115,6 @@ describe("AutoSuspend", function()
|
||||
mock_time:increase(5)
|
||||
UIManager:handleInput()
|
||||
assert.stub(UIManager.poweroff_action).was.called(1)
|
||||
mock_time:uninstall()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -132,7 +132,7 @@ describe("BackgroundRunner widget tests", function()
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
notifyBackgroundJobsUpdated()
|
||||
|
||||
while job.end_sec == nil do
|
||||
while job.end_tv == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
@@ -157,7 +157,7 @@ describe("BackgroundRunner widget tests", function()
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
notifyBackgroundJobsUpdated()
|
||||
|
||||
while job.end_sec == nil do
|
||||
while job.end_tv == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
@@ -171,11 +171,11 @@ describe("BackgroundRunner widget tests", function()
|
||||
ENV1 = "yes",
|
||||
ENV2 = "no",
|
||||
}
|
||||
job.end_sec = nil
|
||||
job.end_tv = nil
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
notifyBackgroundJobsUpdated()
|
||||
|
||||
while job.end_sec == nil do
|
||||
while job.end_tv == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
@@ -206,7 +206,7 @@ describe("BackgroundRunner widget tests", function()
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
notifyBackgroundJobsUpdated()
|
||||
|
||||
while job.end_sec == nil do
|
||||
while job.end_tv == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
@@ -216,12 +216,12 @@ describe("BackgroundRunner widget tests", function()
|
||||
assert.is_false(job.timeout)
|
||||
assert.is_false(job.bad_command)
|
||||
|
||||
job.end_sec = nil
|
||||
job.end_tv = nil
|
||||
env2 = "no"
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
notifyBackgroundJobsUpdated()
|
||||
|
||||
while job.end_sec == nil do
|
||||
while job.end_tv == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
@@ -244,7 +244,7 @@ describe("BackgroundRunner widget tests", function()
|
||||
table.insert(PluginShare.backgroundJobs, job)
|
||||
notifyBackgroundJobsUpdated()
|
||||
|
||||
while job.end_sec == nil do
|
||||
while job.end_tv == nil do
|
||||
MockTime:increase(2)
|
||||
UIManager:handleInput()
|
||||
end
|
||||
|
||||
@@ -94,13 +94,13 @@ describe("device module", function()
|
||||
type = EV_ABS,
|
||||
code = ABS_X,
|
||||
value = y,
|
||||
time = TimeVal:now(),
|
||||
time = TimeVal:realtime(),
|
||||
}
|
||||
local ev_y = {
|
||||
type = EV_ABS,
|
||||
code = ABS_Y,
|
||||
value = Screen:getWidth()-x,
|
||||
time = TimeVal:now(),
|
||||
time = TimeVal:realtime(),
|
||||
}
|
||||
|
||||
kobo_dev.input:eventAdjustHook(ev_x)
|
||||
@@ -273,7 +273,7 @@ describe("device module", function()
|
||||
|
||||
mock_ffi_input = require('ffi/input')
|
||||
stub(mock_ffi_input, "waitForEvent")
|
||||
mock_ffi_input.waitForEvent.returns({
|
||||
mock_ffi_input.waitForEvent.returns(true, {
|
||||
type = 3,
|
||||
time = {
|
||||
usec = 450565,
|
||||
|
||||
@@ -1,36 +1,181 @@
|
||||
require("commonrequire")
|
||||
local TimeVal = require("ui/timeval")
|
||||
local ffi = require("ffi")
|
||||
local dummy = require("ffi/posix_h")
|
||||
local logger = require("logger")
|
||||
local util = require("ffi/util")
|
||||
|
||||
local C = ffi.C
|
||||
|
||||
local MockTime = {
|
||||
original_os_time = os.time,
|
||||
original_util_time = nil,
|
||||
value = os.time(),
|
||||
original_tv_realtime = nil,
|
||||
original_tv_realtime_coarse = nil,
|
||||
original_tv_monotonic = nil,
|
||||
original_tv_monotonic_coarse = nil,
|
||||
original_tv_boottime = nil,
|
||||
original_tv_now = nil,
|
||||
monotonic = 0,
|
||||
realtime = 0,
|
||||
boottime = 0,
|
||||
}
|
||||
|
||||
function MockTime:install()
|
||||
assert(self ~= nil)
|
||||
local util = require("ffi/util")
|
||||
if self.original_util_time == nil then
|
||||
self.original_util_time = util.gettime
|
||||
assert(self.original_util_time ~= nil)
|
||||
end
|
||||
if self.original_tv_realtime == nil then
|
||||
self.original_tv_realtime = TimeVal.realtime
|
||||
assert(self.original_tv_realtime ~= nil)
|
||||
end
|
||||
if self.original_tv_realtime_coarse == nil then
|
||||
self.original_tv_realtime_coarse = TimeVal.realtime_coarse
|
||||
assert(self.original_tv_realtime_coarse ~= nil)
|
||||
end
|
||||
if self.original_tv_monotonic == nil then
|
||||
self.original_tv_monotonic = TimeVal.monotonic
|
||||
assert(self.original_tv_monotonic ~= nil)
|
||||
end
|
||||
if self.original_tv_monotonic_coarse == nil then
|
||||
self.original_tv_monotonic_coarse = TimeVal.monotonic_coarse
|
||||
assert(self.original_tv_monotonic_coarse ~= nil)
|
||||
end
|
||||
if self.original_tv_boottime == nil then
|
||||
self.original_tv_boottime = TimeVal.boottime
|
||||
assert(self.original_tv_boottime ~= nil)
|
||||
end
|
||||
if self.original_tv_now == nil then
|
||||
self.original_tv_now = TimeVal.now
|
||||
assert(self.original_tv_now ~= nil)
|
||||
end
|
||||
|
||||
-- Store both REALTIME & MONOTONIC clocks
|
||||
self.realtime = os.time()
|
||||
local timespec = ffi.new("struct timespec")
|
||||
C.clock_gettime(C.CLOCK_MONOTONIC_COARSE, timespec)
|
||||
self.monotonic = tonumber(timespec.tv_sec)
|
||||
|
||||
os.time = function() --luacheck: ignore
|
||||
logger.dbg("MockTime:os.time: ", self.value)
|
||||
return self.value
|
||||
logger.dbg("MockTime:os.time: ", self.realtime)
|
||||
return self.realtime
|
||||
end
|
||||
util.gettime = function()
|
||||
logger.dbg("MockTime:util.gettime: ", self.value)
|
||||
return self.value, 0
|
||||
logger.dbg("MockTime:util.gettime: ", self.realtime)
|
||||
return self.realtime, 0
|
||||
end
|
||||
TimeVal.realtime = function()
|
||||
logger.dbg("MockTime:TimeVal.realtime: ", self.realtime)
|
||||
return TimeVal:new{ sec = self.realtime }
|
||||
end
|
||||
TimeVal.realtime_coarse = function()
|
||||
logger.dbg("MockTime:TimeVal.realtime_coarse: ", self.realtime)
|
||||
return TimeVal:new{ sec = self.realtime }
|
||||
end
|
||||
TimeVal.monotonic = function()
|
||||
logger.dbg("MockTime:TimeVal.monotonic: ", self.monotonic)
|
||||
return TimeVal:new{ sec = self.monotonic }
|
||||
end
|
||||
TimeVal.monotonic_coarse = function()
|
||||
logger.dbg("MockTime:TimeVal.monotonic_coarse: ", self.monotonic)
|
||||
return TimeVal:new{ sec = self.monotonic }
|
||||
end
|
||||
TimeVal.boottime = function()
|
||||
logger.dbg("MockTime:TimeVal.boottime: ", self.boottime)
|
||||
return TimeVal:new{ sec = self.boottime }
|
||||
end
|
||||
TimeVal.now = function()
|
||||
logger.dbg("MockTime:TimeVal.now: ", self.monotonic)
|
||||
return TimeVal:new{ sec = self.monotonic }
|
||||
end
|
||||
end
|
||||
|
||||
function MockTime:uninstall()
|
||||
assert(self ~= nil)
|
||||
local util = require("ffi/util")
|
||||
os.time = self.original_os_time --luacheck: ignore
|
||||
if self.original_util_time ~= nil then
|
||||
util.gettime = self.original_util_time
|
||||
end
|
||||
if self.original_tv_realtime ~= nil then
|
||||
TimeVal.realtime = self.original_tv_realtime
|
||||
end
|
||||
if self.original_tv_realtime_coarse ~= nil then
|
||||
TimeVal.realtime_coarse = self.original_tv_realtime_coarse
|
||||
end
|
||||
if self.original_tv_monotonic ~= nil then
|
||||
TimeVal.monotonic = self.original_tv_monotonic
|
||||
end
|
||||
if self.original_tv_monotonic_coarse ~= nil then
|
||||
TimeVal.monotonic_coarse = self.original_tv_monotonic_coarse
|
||||
end
|
||||
if self.original_tv_boottime ~= nil then
|
||||
TimeVal.boottime = self.original_tv_boottime
|
||||
end
|
||||
if self.original_tv_now ~= nil then
|
||||
TimeVal.now = self.original_tv_now
|
||||
end
|
||||
end
|
||||
|
||||
function MockTime:set_realtime(value)
|
||||
assert(self ~= nil)
|
||||
if type(value) ~= "number" then
|
||||
return false
|
||||
end
|
||||
self.realtime = math.floor(value)
|
||||
logger.dbg("MockTime:set_realtime ", self.realtime)
|
||||
return true
|
||||
end
|
||||
|
||||
function MockTime:increase_realtime(value)
|
||||
assert(self ~= nil)
|
||||
if type(value) ~= "number" then
|
||||
return false
|
||||
end
|
||||
self.realtime = math.floor(self.realtime + value)
|
||||
logger.dbg("MockTime:increase_realtime ", self.realtime)
|
||||
return true
|
||||
end
|
||||
|
||||
function MockTime:set_monotonic(value)
|
||||
assert(self ~= nil)
|
||||
if type(value) ~= "number" then
|
||||
return false
|
||||
end
|
||||
self.monotonic = math.floor(value)
|
||||
logger.dbg("MockTime:set_monotonic ", self.monotonic)
|
||||
return true
|
||||
end
|
||||
|
||||
function MockTime:increase_monotonic(value)
|
||||
assert(self ~= nil)
|
||||
if type(value) ~= "number" then
|
||||
return false
|
||||
end
|
||||
self.monotonic = math.floor(self.monotonic + value)
|
||||
logger.dbg("MockTime:increase_monotonic ", self.monotonic)
|
||||
return true
|
||||
end
|
||||
|
||||
function MockTime:set_boottime(value)
|
||||
assert(self ~= nil)
|
||||
if type(value) ~= "number" then
|
||||
return false
|
||||
end
|
||||
self.boottime = math.floor(value)
|
||||
logger.dbg("MockTime:set_boottime ", self.boottime)
|
||||
return true
|
||||
end
|
||||
|
||||
function MockTime:increase_boottime(value)
|
||||
assert(self ~= nil)
|
||||
if type(value) ~= "number" then
|
||||
return false
|
||||
end
|
||||
self.boottime = math.floor(self.boottime + value)
|
||||
logger.dbg("MockTime:increase_boottime ", self.boottime)
|
||||
return true
|
||||
end
|
||||
|
||||
function MockTime:set(value)
|
||||
@@ -38,8 +183,12 @@ function MockTime:set(value)
|
||||
if type(value) ~= "number" then
|
||||
return false
|
||||
end
|
||||
self.value = math.floor(value)
|
||||
logger.dbg("MockTime:set ", self.value)
|
||||
self.realtime = math.floor(value)
|
||||
logger.dbg("MockTime:set (realtime) ", self.realtime)
|
||||
self.monotonic = math.floor(value)
|
||||
logger.dbg("MockTime:set (monotonic) ", self.monotonic)
|
||||
self.boottime = math.floor(value)
|
||||
logger.dbg("MockTime:set (boottime) ", self.boottime)
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -48,8 +197,12 @@ function MockTime:increase(value)
|
||||
if type(value) ~= "number" then
|
||||
return false
|
||||
end
|
||||
self.value = math.floor(self.value + value)
|
||||
logger.dbg("MockTime:increase ", self.value)
|
||||
self.realtime = math.floor(self.realtime + value)
|
||||
logger.dbg("MockTime:increase (realtime) ", self.realtime)
|
||||
self.monotonic = math.floor(self.monotonic + value)
|
||||
logger.dbg("MockTime:increase (monotonic) ", self.monotonic)
|
||||
self.boottime = math.floor(self.boottime + value)
|
||||
logger.dbg("MockTime:increase (boottime) ", self.boottime)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
@@ -20,25 +20,42 @@ describe("TimeVal module", function()
|
||||
local timev2 = TimeVal:new{ sec = 10, usec = 6000}
|
||||
local timev3 = TimeVal:new{ sec = 10, usec = 50000000}
|
||||
|
||||
assert.is.same({sec = 15,usec = 11000}, timev1 + timev2)
|
||||
assert.is.same({sec = 65,usec = 5000}, timev1 + timev3)
|
||||
assert.is.same({sec = 15, usec = 11000}, timev1 + timev2)
|
||||
assert.is.same({sec = 65, usec = 5000}, timev1 + timev3)
|
||||
end)
|
||||
|
||||
it("should subtract", function()
|
||||
local timev1 = TimeVal:new{ sec = 5, usec = 5000}
|
||||
local timev2 = TimeVal:new{ sec = 10, usec = 6000}
|
||||
|
||||
assert.is.same({sec = 5,usec = 1000}, timev2 - timev1)
|
||||
assert.is.same({sec = -5,usec = -1000}, timev1 - timev2)
|
||||
end)
|
||||
assert.is.same({sec = 5, usec = 1000}, timev2 - timev1)
|
||||
local backwards_sub = timev1 - timev2
|
||||
assert.is.same({sec = -6, usec = 999000}, backwards_sub)
|
||||
|
||||
it("should guard against reverse subtraction logic", function()
|
||||
dbg:turnOn()
|
||||
TimeVal = package.reload("ui/timeval")
|
||||
local timev1 = TimeVal:new{ sec = 5, usec = 5000}
|
||||
local timev2 = TimeVal:new{ sec = 10, usec = 5000}
|
||||
-- Check that to/from float conversions behave, even for negative values.
|
||||
assert.is.same(-5.001, backwards_sub:tonumber())
|
||||
assert.is.same({sec = -6, usec = 999000}, TimeVal:fromnumber(-5.001))
|
||||
|
||||
assert.has.errors(function() return timev1 - timev2 end)
|
||||
local tv = TimeVal:new{ sec = -6, usec = 1000 }
|
||||
assert.is.same(-5.999, tv:tonumber())
|
||||
assert.is.same({sec = -6, usec = 1000}, TimeVal:fromnumber(-5.999))
|
||||
|
||||
-- We lose precision because of rounding if we go higher resolution than a ms...
|
||||
tv = TimeVal:new{ sec = -6, usec = 101 }
|
||||
assert.is.same(-5.9999, tv:tonumber())
|
||||
assert.is.same({sec = -6, usec = 100}, TimeVal:fromnumber(-5.9999))
|
||||
-- ^ precision loss
|
||||
|
||||
tv = TimeVal:new{ sec = -6, usec = 11 }
|
||||
assert.is.same(-6, tv:tonumber())
|
||||
-- ^ precision loss
|
||||
assert.is.same({sec = -6, usec = 10}, TimeVal:fromnumber(-5.99999))
|
||||
-- ^ precision loss
|
||||
|
||||
tv = TimeVal:new{ sec = -6, usec = 1 }
|
||||
assert.is.same(-6, tv:tonumber())
|
||||
-- ^ precision loss
|
||||
assert.is.same({sec = -6, usec = 1}, TimeVal:fromnumber(-5.999999))
|
||||
end)
|
||||
|
||||
it("should derive sec and usec from more than 1 sec worth of usec", function()
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
describe("UIManager spec", function()
|
||||
local UIManager, util
|
||||
local TimeVal, UIManager
|
||||
local now, wait_until
|
||||
local noop = function() end
|
||||
|
||||
setup(function()
|
||||
require("commonrequire")
|
||||
util = require("ffi/util")
|
||||
TimeVal = require("ui/timeval")
|
||||
UIManager = require("ui/uimanager")
|
||||
end)
|
||||
|
||||
it("should consume due tasks", function()
|
||||
now = { util.gettime() }
|
||||
local future = { now[1] + 60000, now[2] }
|
||||
local future2 = {future[1] + 5, future[2]}
|
||||
now = TimeVal:now()
|
||||
local future = TimeVal:new{ sec = now.sec + 60000, usec = now.usec }
|
||||
local future2 = TimeVal:new{ sec = future.sec + 5, usec = future.usec}
|
||||
UIManager:quit()
|
||||
UIManager._task_queue = {
|
||||
{ time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 },
|
||||
{ time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 },
|
||||
{ time = now, action = noop, args = {}, argc = 0 },
|
||||
{ time = future, action = noop, args = {}, argc = 0 },
|
||||
{ time = future2, action = noop, args = {}, argc = 0 },
|
||||
@@ -28,26 +28,26 @@ describe("UIManager spec", function()
|
||||
end)
|
||||
|
||||
it("should calcualte wait_until properly in checkTasks routine", function()
|
||||
now = { util.gettime() }
|
||||
local future = { now[1] + 60000, now[2] }
|
||||
now = TimeVal:now()
|
||||
local future = TimeVal:new{ sec = now.sec + 60000, usec = now.usec }
|
||||
UIManager:quit()
|
||||
UIManager._task_queue = {
|
||||
{ time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 },
|
||||
{ time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 },
|
||||
{ time = now, action = noop, args = {}, argc = 0 },
|
||||
{ time = future, action = noop, args = {}, argc = 0 },
|
||||
{ time = {future[1] + 5, future[2]}, action = noop, args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = future.sec + 5, usec = future.usec }, action = noop, args = {}, argc = 0 },
|
||||
}
|
||||
wait_until, now = UIManager:_checkTasks()
|
||||
assert.are.same(future, wait_until)
|
||||
end)
|
||||
|
||||
it("should return nil wait_until properly in checkTasks routine", function()
|
||||
now = { util.gettime() }
|
||||
now = TimeVal:now()
|
||||
UIManager:quit()
|
||||
UIManager._task_queue = {
|
||||
{ time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 },
|
||||
{ time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 },
|
||||
{ time = now, action = noop, args = {}, argc = 0 },
|
||||
}
|
||||
wait_until, now = UIManager:_checkTasks()
|
||||
@@ -55,7 +55,7 @@ describe("UIManager spec", function()
|
||||
end)
|
||||
|
||||
it("should insert new task properly in empty task queue", function()
|
||||
now = { util.gettime() }
|
||||
now = TimeVal:now()
|
||||
UIManager:quit()
|
||||
UIManager._task_queue = {}
|
||||
assert.are.same(0, #UIManager._task_queue)
|
||||
@@ -65,8 +65,8 @@ describe("UIManager spec", function()
|
||||
end)
|
||||
|
||||
it("should insert new task properly in single task queue", function()
|
||||
now = { util.gettime() }
|
||||
local future = { now[1]+10000, now[2] }
|
||||
now = TimeVal:now()
|
||||
local future = TimeVal:new{ sec = now.sec + 10000, usec = now.usec }
|
||||
UIManager:quit()
|
||||
UIManager._task_queue = {
|
||||
{ time = future, action = '1', args = {}, argc = 0 },
|
||||
@@ -90,59 +90,59 @@ describe("UIManager spec", function()
|
||||
end)
|
||||
|
||||
it("should insert new task in ascendant order", function()
|
||||
now = { util.gettime() }
|
||||
now = TimeVal:now()
|
||||
UIManager:quit()
|
||||
UIManager._task_queue = {
|
||||
{ time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 },
|
||||
{ time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 },
|
||||
{ time = now, action = '3', args = {}, argc = 0 },
|
||||
}
|
||||
-- insert into the tail slot
|
||||
UIManager:scheduleIn(10, 'foo')
|
||||
assert.are.same('foo', UIManager._task_queue[4].action)
|
||||
-- insert into the second slot
|
||||
UIManager:schedule({now[1]-5, now[2]}, 'bar')
|
||||
UIManager:schedule(TimeVal:new{ sec = now.sec - 5, usec = now.usec }, 'bar')
|
||||
assert.are.same('bar', UIManager._task_queue[2].action)
|
||||
-- insert into the head slot
|
||||
UIManager:schedule({now[1]-15, now[2]}, 'baz')
|
||||
UIManager:schedule(TimeVal:new{ sec = now.sec - 15, usec = now.usec }, 'baz')
|
||||
assert.are.same('baz', UIManager._task_queue[1].action)
|
||||
-- insert into the last second slot
|
||||
UIManager:scheduleIn(5, 'qux')
|
||||
assert.are.same('qux', UIManager._task_queue[6].action)
|
||||
-- insert into the middle slot
|
||||
UIManager:schedule({now[1], now[2]-1}, 'quux')
|
||||
UIManager:schedule(TimeVal:new{ sec = now.sec, usec = now.usec - 1 }, 'quux')
|
||||
assert.are.same('quux', UIManager._task_queue[5].action)
|
||||
end)
|
||||
|
||||
it("should unschedule all the tasks with the same action", function()
|
||||
now = { util.gettime() }
|
||||
now = TimeVal:now()
|
||||
UIManager:quit()
|
||||
UIManager._task_queue = {
|
||||
{ time = {now[1] - 15, now[2] }, action = '3', args = {}, argc = 0 },
|
||||
{ time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 },
|
||||
{ time = {now[1], now[2] - 6 }, action = '3', args = {}, argc = 0 },
|
||||
{ time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec - 15, usec = now.usec }, action = '3', args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 6 }, action = '3', args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 },
|
||||
{ time = now, action = '3', args = {}, argc = 0 },
|
||||
}
|
||||
-- insert into the tail slot
|
||||
UIManager:unschedule('3')
|
||||
assert.are.same({
|
||||
{ time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 },
|
||||
{ time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 },
|
||||
}, UIManager._task_queue)
|
||||
end)
|
||||
|
||||
it("should not have race between unschedule and _checkTasks", function()
|
||||
now = { util.gettime() }
|
||||
now = TimeVal:now()
|
||||
local run_count = 0
|
||||
local task_to_remove = function()
|
||||
run_count = run_count + 1
|
||||
end
|
||||
UIManager:quit()
|
||||
UIManager._task_queue = {
|
||||
{ time = { now[1], now[2]-5 }, action = task_to_remove, args = {}, argc = 0 },
|
||||
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = task_to_remove, args = {}, argc = 0 },
|
||||
{
|
||||
time = { now[1]-10, now[2] },
|
||||
time = TimeVal:new{ sec = now.sec - 10, usec = now.usec },
|
||||
action = function()
|
||||
run_count = run_count + 1
|
||||
UIManager:unschedule(task_to_remove)
|
||||
|
||||
Reference in New Issue
Block a user