mirror of
https://github.com/koreader/koreader
synced 2024-10-31 21:20:20 +00:00
1431 lines
59 KiB
Lua
1431 lines
59 KiB
Lua
--[[--
|
|
This module detects gestures.
|
|
|
|
Current detectable gestures:
|
|
|
|
* `touch` (emitted once on first contact down)
|
|
* `tap` (touch action detected as single tap)
|
|
* `pan`
|
|
* `hold`
|
|
* `swipe`
|
|
* `pinch`
|
|
* `spread`
|
|
* `rotate`
|
|
* `hold_pan` (will emit `hold_release` on contact lift, unlike its two-finger variant)
|
|
* `double_tap`
|
|
* `inward_pan`
|
|
* `outward_pan`
|
|
* `pan_release`
|
|
* `hold_release`
|
|
* `two_finger_hold`
|
|
* `two_finger_hold_release`
|
|
* `two_finger_tap`
|
|
* `two_finger_pan`
|
|
* `two_finger_hold_pan`
|
|
* `two_finger_swipe`
|
|
* `two_finger_pan_release`
|
|
* `two_finger_hold_pan_release`
|
|
|
|
You change the state machine by feeding it touch events, i.e. calling
|
|
@{GestureDetector:feedEvent|GestureDetector:feedEvent(tev)}.
|
|
|
|
|
|
a touch event should have following format:
|
|
|
|
tev = {
|
|
slot = 1,
|
|
id = 46,
|
|
x = 0,
|
|
y = 1,
|
|
timev = time.s(123.23),
|
|
}
|
|
|
|
Don't confuse `tev` with raw evs from kernel, `tev` is built according to ev.
|
|
|
|
@{GestureDetector:feedEvent|GestureDetector:feedEvent(tev)} will return a
|
|
detection result when you feed a touch release event to it.
|
|
--]]
|
|
|
|
local Geom = require("ui/geometry")
|
|
local logger = require("logger")
|
|
local time = require("ui/time")
|
|
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 (time parameters are in milliseconds (ms))
|
|
local TAP_INTERVAL_MS = 0
|
|
local DOUBLE_TAP_INTERVAL_MS = 300
|
|
local TWO_FINGER_TAP_DURATION_MS = 300
|
|
local HOLD_INTERVAL_MS = 500
|
|
local SWIPE_INTERVAL_MS = 900
|
|
|
|
-- This is used as a singleton by Input (itself used as a singleton).
|
|
local GestureDetector = {
|
|
-- must be initialized with the Input singleton class
|
|
input = nil,
|
|
-- default values (accessed for display by plugins/gestures.koplugin)
|
|
TAP_INTERVAL_MS = TAP_INTERVAL_MS,
|
|
DOUBLE_TAP_INTERVAL_MS = DOUBLE_TAP_INTERVAL_MS,
|
|
TWO_FINGER_TAP_DURATION_MS = TWO_FINGER_TAP_DURATION_MS,
|
|
HOLD_INTERVAL_MS = HOLD_INTERVAL_MS,
|
|
SWIPE_INTERVAL_MS = SWIPE_INTERVAL_MS,
|
|
-- pinch/spread direction table
|
|
DIRECTION_TABLE = { -- const
|
|
east = "horizontal",
|
|
west = "horizontal",
|
|
north = "vertical",
|
|
south = "vertical",
|
|
northeast = "diagonal",
|
|
northwest = "diagonal",
|
|
southeast = "diagonal",
|
|
southwest = "diagonal",
|
|
},
|
|
-- Hash of our currently active contacts
|
|
active_contacts = {},
|
|
contact_count = 0,
|
|
-- Used for double tap and bounce detection (this is outside a Contact object because it requires minimal persistance).
|
|
previous_tap = {},
|
|
-- for timestamp clocksource detection
|
|
clock_id = nil,
|
|
-- current values
|
|
ges_tap_interval = time.ms(G_reader_settings:readSetting("ges_tap_interval_ms") or TAP_INTERVAL_MS),
|
|
ges_double_tap_interval = time.ms(G_reader_settings:readSetting("ges_double_tap_interval_ms")
|
|
or DOUBLE_TAP_INTERVAL_MS),
|
|
ges_two_finger_tap_duration = time.ms(G_reader_settings:readSetting("ges_two_finger_tap_duration_ms")
|
|
or TWO_FINGER_TAP_DURATION_MS),
|
|
ges_hold_interval = time.ms(G_reader_settings:readSetting("ges_hold_interval_ms") or HOLD_INTERVAL_MS),
|
|
ges_swipe_interval = time.ms(G_reader_settings:readSetting("ges_swipe_interval_ms") or SWIPE_INTERVAL_MS),
|
|
}
|
|
|
|
function GestureDetector:new(o)
|
|
o = o or {}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
if o.init then o:init() end
|
|
return o
|
|
end
|
|
|
|
function GestureDetector:init()
|
|
-- distance parameters
|
|
self.TWO_FINGER_TAP_REGION = self.screen:scaleByDPI(20)
|
|
self.DOUBLE_TAP_DISTANCE = self.screen:scaleByDPI(50)
|
|
self.SINGLE_TAP_BOUNCE_DISTANCE = self.DOUBLE_TAP_DISTANCE
|
|
self.PAN_THRESHOLD = self.screen:scaleByDPI(35)
|
|
self.MULTISWIPE_THRESHOLD = self.DOUBLE_TAP_DISTANCE
|
|
end
|
|
|
|
-- Contact object, it'll keep track of everything we need for a single contact across its lifetime
|
|
-- i.e., from this contact's down to up (or its *effective* up for double-taps, e.g., when the tap or double_tap is emitted).
|
|
-- We'll identify contacts by their slot numbers, and store 'em in GestureDetector's active_contacts table (hash).
|
|
local Contact = {} -- Class object is empty, as we do *NOT* want inheritance outside of methods.
|
|
function Contact:new(o)
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
function GestureDetector:newContact(slot)
|
|
-- Check if this new contact might be part of a two finger gesture,
|
|
-- by checking if the current slot is one of the two main slots, and the other is active.
|
|
local buddy_slot = slot == self.input.main_finger_slot and self.input.main_finger_slot + 1 or
|
|
slot == self.input.main_finger_slot + 1 and self.input.main_finger_slot
|
|
local buddy_contact = buddy_slot and self:getContact(buddy_slot)
|
|
|
|
self.active_contacts[slot] = Contact:new{
|
|
state = Contact.initialState, -- Current state function
|
|
slot = slot, -- Current ABS_MT_SLOT value (also its key in the active_contacts hash)
|
|
id = -1, -- Current ABS_MT_TRACKING_ID value
|
|
initial_tev = nil, -- Copy of the input event table at first contact (i.e., at contact down)
|
|
current_tev = nil, -- Pointer to the current input event table, ref is *stable*, c.f., NOTE in feedEvent below
|
|
down = false, -- Contact is down (as opposed to up, i.e., lifted). Only really happens for double-tap handling, in every other case the Contact object is destroyed on lift.
|
|
pending_double_tap_timer = false, -- Contact is pending a double_tap timer
|
|
pending_hold_timer = false, -- Contact is pending a hold timer
|
|
mt_gesture = nil, -- Contact is part of a MT gesture (string, gesture name)
|
|
mt_immobile = true, -- Contact is part of a MT gesture, and hasn't moved (i.e., would be in holdState if it weren't in voidState)
|
|
multiswipe_directions = {}, -- Accumulated multiswipe chain for this contact
|
|
multiswipe_type = nil, -- Current multiswipe type for this contact
|
|
buddy_contact = buddy_contact, -- Ref to the paired contact in a MT gesture (if any)
|
|
ges_dec = self, -- Ref to the current GestureDetector instance
|
|
}
|
|
self.contact_count = self.contact_count + 1
|
|
--logger.dbg("New contact for slot", slot, "#contacts =", self.contact_count)
|
|
|
|
-- If we have a buddy contact, point its own buddy ref to us
|
|
if buddy_contact then
|
|
buddy_contact.buddy_contact = self.active_contacts[slot]
|
|
end
|
|
|
|
return self.active_contacts[slot]
|
|
end
|
|
|
|
function GestureDetector:getContact(slot)
|
|
return self.active_contacts[slot]
|
|
end
|
|
|
|
function GestureDetector:dropContact(contact)
|
|
local slot = contact.slot
|
|
|
|
-- Guard against double drops
|
|
if not self.active_contacts[slot] then
|
|
logger.warn("Contact for slot", slot, "has already been dropped! #contacts =", self.contact_count)
|
|
return
|
|
end
|
|
|
|
-- Also clear any pending callbacks on that slot.
|
|
if contact.pending_double_tap_timer then
|
|
self.input:clearTimeout(slot, "double_tap")
|
|
contact.pending_double_tap_timer = nil
|
|
end
|
|
if contact.pending_hold_timer then
|
|
self.input:clearTimeout(slot, "hold")
|
|
contact.pending_hold_timer = nil
|
|
end
|
|
|
|
-- If we have a buddy contact, drop its buddy ref to us
|
|
if contact.buddy_contact then
|
|
contact.buddy_contact.buddy_contact = nil
|
|
end
|
|
|
|
self.active_contacts[slot] = nil
|
|
self.contact_count = self.contact_count - 1
|
|
--logger.dbg("Dropped contact for slot", slot, "#contacts =", self.contact_count)
|
|
end
|
|
|
|
function GestureDetector:dropContacts()
|
|
for _, contact in pairs(self.active_contacts) do
|
|
self:dropContact(contact)
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Feeds touch events to state machine.
|
|
|
|
Note that, in a single input frame, if the same slot gets multiple events, only the last one is kept.
|
|
Every slot in the input frame is consumed, and that in FIFO order (slot order based on appearance in the frame).
|
|
--]]
|
|
function GestureDetector:feedEvent(tevs)
|
|
local gestures = {}
|
|
for _, tev in ipairs(tevs) do
|
|
local slot = tev.slot
|
|
local contact = self:getContact(slot)
|
|
if not contact then
|
|
contact = self:newContact(slot)
|
|
-- NOTE: tev is actually a simple reference to Input's self.ev_slots[slot],
|
|
-- which means a Contact's current_tev doesn't actually point to the *previous*
|
|
-- input frame for a given slot, but always points to the *current* input frame for that slot!
|
|
-- Meaning the tev we feed the state function *always* matches that Contact's current_tev.
|
|
-- Compare to initial_tev below, which does create a copy...
|
|
-- This is what allows us to only do this once on contact creation ;).
|
|
contact.current_tev = tev
|
|
end
|
|
local ges = contact.state(contact)
|
|
if ges then
|
|
table.insert(gestures, ges)
|
|
end
|
|
end
|
|
return gestures
|
|
end
|
|
|
|
local function deepCopyEv(tev)
|
|
return {
|
|
x = tev.x,
|
|
y = tev.y,
|
|
id = tev.id,
|
|
slot = tev.slot,
|
|
timev = tev.timev, -- A ref is enough for this table, it's re-assigned to a new object on every SYN_REPORT
|
|
}
|
|
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 time_diff = tap2.timev - tap1.timev
|
|
if time_diff < 0 then
|
|
time_diff = time.huge
|
|
end
|
|
if time_diff < interval then
|
|
local x_diff = math.abs(tap1.x - tap2.x)
|
|
local y_diff = math.abs(tap1.y - tap2.y)
|
|
|
|
return (
|
|
x_diff < self.SINGLE_TAP_BOUNCE_DISTANCE and
|
|
y_diff < self.SINGLE_TAP_BOUNCE_DISTANCE
|
|
)
|
|
end
|
|
end
|
|
|
|
function GestureDetector:isDoubleTap(tap1, tap2)
|
|
local time_diff = tap2.timev - tap1.timev
|
|
if time_diff < 0 then
|
|
time_diff = time.huge
|
|
end
|
|
if time_diff < self.ges_double_tap_interval then
|
|
local x_diff = math.abs(tap1.x - tap2.x)
|
|
local y_diff = math.abs(tap1.y - tap2.y)
|
|
|
|
return (
|
|
x_diff < self.DOUBLE_TAP_DISTANCE and
|
|
y_diff < self.DOUBLE_TAP_DISTANCE
|
|
)
|
|
end
|
|
end
|
|
|
|
function Contact:isTwoFingerTap(buddy_contact)
|
|
local gesture_detector = self.ges_dec
|
|
|
|
local time_diff0 = self.current_tev.timev - self.initial_tev.timev
|
|
if time_diff0 < 0 then
|
|
time_diff0 = time.huge
|
|
end
|
|
local time_diff1 = buddy_contact.current_tev.timev - buddy_contact.initial_tev.timev
|
|
if time_diff1 < 0 then
|
|
time_diff1 = time.huge
|
|
end
|
|
if time_diff0 < gesture_detector.ges_two_finger_tap_duration and
|
|
time_diff1 < gesture_detector.ges_two_finger_tap_duration then
|
|
local x_diff0 = math.abs(self.current_tev.x - self.initial_tev.x)
|
|
local x_diff1 = math.abs(buddy_contact.current_tev.x - buddy_contact.initial_tev.x)
|
|
local y_diff0 = math.abs(self.current_tev.y - self.initial_tev.y)
|
|
local y_diff1 = math.abs(buddy_contact.current_tev.y - buddy_contact.initial_tev.y)
|
|
|
|
return (
|
|
x_diff0 < gesture_detector.TWO_FINGER_TAP_REGION and
|
|
x_diff1 < gesture_detector.TWO_FINGER_TAP_REGION and
|
|
y_diff0 < gesture_detector.TWO_FINGER_TAP_REGION and
|
|
y_diff1 < gesture_detector.TWO_FINGER_TAP_REGION
|
|
)
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Compares `current_tev` with `initial_tev`.
|
|
|
|
The first boolean argument `simple` results in only four directions if true.
|
|
|
|
@return (direction, distance) pan direction and distance
|
|
--]]
|
|
function Contact:getPath(simple, diagonal, initial_tev)
|
|
initial_tev = initial_tev or self.initial_tev
|
|
|
|
local x_diff = self.current_tev.x - initial_tev.x
|
|
local y_diff = self.current_tev.y - initial_tev.y
|
|
local direction = nil
|
|
local distance = math.sqrt(x_diff*x_diff + y_diff*y_diff)
|
|
if x_diff ~= 0 or y_diff ~= 0 then
|
|
local v_direction = y_diff < 0 and "north" or "south"
|
|
local h_direction = x_diff < 0 and "west" or "east"
|
|
if (not simple
|
|
and math.abs(y_diff) > 0.577*math.abs(x_diff)
|
|
and math.abs(y_diff) < 1.732*math.abs(x_diff))
|
|
or (simple and diagonal)
|
|
then
|
|
direction = v_direction .. h_direction
|
|
elseif (math.abs(x_diff) > math.abs(y_diff)) then
|
|
direction = h_direction
|
|
else
|
|
direction = v_direction
|
|
end
|
|
end
|
|
return direction, distance
|
|
end
|
|
|
|
function Contact:isSwipe()
|
|
local gesture_detector = self.ges_dec
|
|
|
|
local time_diff = self.current_tev.timev - self.initial_tev.timev
|
|
if time_diff < 0 then
|
|
time_diff = time.huge
|
|
end
|
|
if time_diff < gesture_detector.ges_swipe_interval then
|
|
local x_diff = self.current_tev.x - self.initial_tev.x
|
|
local y_diff = self.current_tev.y - self.initial_tev.y
|
|
if x_diff ~= 0 or y_diff ~= 0 then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
function GestureDetector:getRotate(orig_point, start_point, end_point)
|
|
--[[
|
|
local a = orig_point:distance(start_point)
|
|
local b = orig_point:distance(end_point)
|
|
local c = start_point:distance(end_point)
|
|
return math.acos((a*a + b*b - c*c)/(2*a*b))*180/math.pi
|
|
--]]
|
|
|
|
-- NOTE: I am severely maths impaired, and I just wanted something that preserved rotation direction (CCW if < 0),
|
|
-- so this is shamelessly stolen from https://stackoverflow.com/a/31334882
|
|
-- & https://stackoverflow.com/a/21484228
|
|
local rad = math.atan2(end_point.y - orig_point.y, end_point.x - orig_point.x) -
|
|
math.atan2(start_point.y - orig_point.y, start_point.x - orig_point.x)
|
|
-- Normalize to [-180, 180]
|
|
if rad < -math.pi then
|
|
rad = rad + 2 * math.pi
|
|
elseif rad > math.pi then
|
|
rad = rad - 2 * math.pi
|
|
end
|
|
return rad * 180/math.pi
|
|
end
|
|
|
|
function Contact:switchState(state_func, func_arg)
|
|
self.state = state_func
|
|
return state_func(self, func_arg)
|
|
end
|
|
|
|
function Contact:initialState()
|
|
local tev = self.current_tev
|
|
|
|
if tev.id then
|
|
-- Contact lift
|
|
if tev.id == -1 then
|
|
-- If this slot was a buddy slot that happened to be dropped by a MT gesture in the *same* input frame,
|
|
-- a lift might be the first thing we process here... We can safely drop it again.
|
|
-- Hover pen events are also good candidates for this.
|
|
logger.dbg("Contact:initialState Cancelled a gesture in slot", self.slot)
|
|
self.ges_dec:dropContact(self)
|
|
else
|
|
self.id = tev.id
|
|
if tev.x and tev.y then
|
|
-- Contact down, user started a new touch motion
|
|
if not self.down then
|
|
self.down = true
|
|
-- NOTE: We can't use a simple reference, because tev is actually Input's self.ev_slots[slot],
|
|
-- and *that* is a fixed reference for a given slot!
|
|
-- Here, we really want to remember the *first* tev, so, make a copy of it.
|
|
self.initial_tev = deepCopyEv(tev)
|
|
-- Default to tap state, indicating that this is a new contact
|
|
return self:switchState(Contact.tapState, true)
|
|
end
|
|
end
|
|
end
|
|
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 = time.s(2) + time.ms(500)
|
|
|
|
-- Start w/ REALTIME, because it's the easiest to detect ;).
|
|
local realtime = time.realtime_coarse()
|
|
-- clock-threshold <= timev <= clock+threshold
|
|
if timev >= realtime - threshold and timev <= realtime + threshold then
|
|
self.clock_id = C.CLOCK_REALTIME
|
|
logger.dbg("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 = time.monotonic_coarse()
|
|
if timev >= monotonic - threshold and timev <= monotonic + threshold then
|
|
self.clock_id = C.CLOCK_MONOTONIC
|
|
logger.dbg("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_MONOTONIC")
|
|
return
|
|
end
|
|
|
|
-- Finally, BOOTTIME
|
|
local boottime = time.boottime()
|
|
-- NOTE: It was implemented in Linux 2.6.39, so, reject 0, which would mean it's unsupported...
|
|
if not boottime == 0 and timev >= boottime - threshold and timev <= boottime + threshold then
|
|
self.clock_id = C.CLOCK_BOOTTIME
|
|
logger.dbg("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.dbg("GestureDetector:probeClockSource: Touch event clock source detection was inconclusive")
|
|
-- Print all all the gory details in debug mode when this happens...
|
|
logger.dbg("Input frame :", time.format_time(timev))
|
|
logger.dbg("CLOCK_REALTIME :", time.format_time(realtime))
|
|
logger.dbg("CLOCK_MONOTONIC:", time.format_time(monotonic))
|
|
logger.dbg("CLOCK_BOOTTIME :", time.format_time(boottime))
|
|
end
|
|
|
|
function GestureDetector:getClockSource()
|
|
return self.clock_id
|
|
end
|
|
|
|
function GestureDetector:resetClockSource()
|
|
self.clock_id = nil
|
|
end
|
|
|
|
--[[--
|
|
Handles both single and double tap. `new_tap` is true for the initial contact down event.
|
|
--]]
|
|
function Contact:tapState(new_tap)
|
|
local slot = self.slot
|
|
local tev = self.current_tev
|
|
local buddy_contact = self.buddy_contact
|
|
local gesture_detector = self.ges_dec
|
|
|
|
-- Attempt to detect the clock source for these events (we reset it on suspend to discriminate MONOTONIC from BOOTTIME).
|
|
if not gesture_detector.clock_id then
|
|
gesture_detector:probeClockSource(tev.timev)
|
|
end
|
|
|
|
logger.dbg("slot", slot, "in tap state...")
|
|
-- Contact lift
|
|
if tev.id == -1 then
|
|
if buddy_contact and self.down then
|
|
-- Both main contacts are actives and we are down
|
|
if self:isTwoFingerTap(buddy_contact) then
|
|
-- Mark that slot
|
|
self.mt_gesture = "tap"
|
|
-- Neuter its buddy
|
|
buddy_contact.state = Contact.voidState
|
|
buddy_contact.mt_gesture = "tap"
|
|
|
|
local pos0 = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local pos1 = Geom:new{
|
|
x = buddy_contact.current_tev.x,
|
|
y = buddy_contact.current_tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local tap_span = pos0:distance(pos1)
|
|
local tap_pos = pos0:midpoint(pos1)
|
|
logger.dbg("two_finger_tap detected @", tap_pos.x, tap_pos.y, "with span", tap_span)
|
|
-- Don't drop buddy, voidState will handle it
|
|
gesture_detector:dropContact(self)
|
|
return {
|
|
ges = "two_finger_tap",
|
|
pos = tap_pos,
|
|
span = tap_span,
|
|
time = tev.timev,
|
|
}
|
|
else
|
|
logger.dbg("Contact:tapState: Two-contact tap failed to pass the two_finger_tap constraints -> single tap @", tev.x, tev.y)
|
|
-- We blew the gesture position/time constraints,
|
|
-- neuter buddy and send a single tap on this slot.
|
|
buddy_contact.state = Contact.voidState
|
|
gesture_detector:dropContact(self)
|
|
|
|
return {
|
|
ges = "tap",
|
|
pos = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
},
|
|
time = tev.timev,
|
|
}
|
|
end
|
|
elseif self.down or self.pending_double_tap_timer then
|
|
-- Hand over to the double tap handler, it's responsible for downgrading to single tap
|
|
return self:handleDoubleTap()
|
|
else
|
|
-- Huh, caught a *second* contact lift for this contact? (should never happen).
|
|
logger.warn("Contact:tapState Cancelled a gesture")
|
|
gesture_detector:dropContact(self)
|
|
end
|
|
else
|
|
-- If we're pending a double_tap timer, flag the contact as down again.
|
|
if self.pending_double_tap_timer and self.down == false then
|
|
self.down = true
|
|
logger.dbg("Contact:tapState: Contact down")
|
|
end
|
|
-- See if we need to do something with the move/hold
|
|
return self:handleNonTap(new_tap)
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Emits both tap & double_tap gestures. Contact is up (but down is still true) or pending a double_tap timer.
|
|
--]]
|
|
function Contact:handleDoubleTap()
|
|
local slot = self.slot
|
|
--logger.dbg("Contact:handleDoubleTap for slot", slot)
|
|
local tev = self.current_tev
|
|
local gesture_detector = self.ges_dec
|
|
|
|
-- If we don't actually detect two distinct taps (i.e., down -> up -> down -> up), then it's a hover, ignore it.
|
|
-- (Without a double tap timer involved, these get dropped in initialState).
|
|
if self.pending_double_tap_timer and self.down == false then
|
|
logger.dbg("Contact:handleDoubleTap Ignored a hover event")
|
|
return
|
|
end
|
|
|
|
-- cur_tap is used for double tap and bounce detection
|
|
local cur_tap = {
|
|
x = tev.x,
|
|
y = tev.y,
|
|
timev = tev.timev,
|
|
}
|
|
|
|
-- Tap interval / bounce detection may be tweaked by a widget (i.e., VirtualKeyboard)
|
|
local tap_interval = gesture_detector.input.tap_interval_override or gesture_detector.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 gesture_detector.previous_tap[slot] ~= nil and
|
|
gesture_detector:isTapBounce(gesture_detector.previous_tap[slot], cur_tap, tap_interval) then
|
|
logger.dbg("Contact:handleDoubleTap Stopped a tap bounce")
|
|
-- Simply ignore it, and drop this slot as this is a contact lift.
|
|
gesture_detector:dropContact(self)
|
|
return
|
|
end
|
|
|
|
local ges_ev = {
|
|
-- Default to single tap
|
|
ges = "tap",
|
|
pos = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
},
|
|
time = tev.timev,
|
|
}
|
|
|
|
if not gesture_detector.input.disable_double_tap and self.pending_double_tap_timer and
|
|
gesture_detector:isDoubleTap(gesture_detector.previous_tap[slot], cur_tap) then
|
|
-- It is a double tap
|
|
ges_ev.ges = "double_tap"
|
|
logger.dbg("Contact:handleDoubleTap: double_tap detected @", ges_ev.pos.x, ges_ev.pos.y)
|
|
gesture_detector:dropContact(self)
|
|
return ges_ev
|
|
end
|
|
|
|
-- Remember this tap, now that we're out of the bounce & double_tap windows
|
|
gesture_detector.previous_tap[slot] = cur_tap
|
|
|
|
if gesture_detector.input.disable_double_tap then
|
|
-- We can send the event immediately (no need for the timer stuff needed for double tap support)
|
|
logger.dbg("Contact:handleDoubleTap: single tap detected @", ges_ev.pos.x, ges_ev.pos.y)
|
|
gesture_detector:dropContact(self)
|
|
return ges_ev
|
|
end
|
|
|
|
-- Double tap enabled: we can't send this single tap immediately as it 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.
|
|
if not self.pending_double_tap_timer then
|
|
logger.dbg("set up double_tap timer")
|
|
self.pending_double_tap_timer = true
|
|
-- setTimeout will handle computing the deadline in the least lossy way possible given the platform.
|
|
gesture_detector.input:setTimeout(slot, "double_tap", function()
|
|
if self == gesture_detector:getContact(slot) and self.pending_double_tap_timer then
|
|
self.pending_double_tap_timer = false
|
|
if self.state == Contact.tapState then
|
|
-- A single or double tap will yield a different contact object, by virtue of dropContact and closure magic ;).
|
|
-- Speaking of closures, this is the original ges_ev from the timer setup.
|
|
logger.dbg("double_tap timer detected a single tap in slot", slot, "@", ges_ev.pos.x, ges_ev.pos.y)
|
|
gesture_detector:dropContact(self)
|
|
return ges_ev
|
|
end
|
|
end
|
|
end, tev.timev, gesture_detector.ges_double_tap_interval)
|
|
end
|
|
-- Regardless of the timer shenanigans, it's at the very least a contact lift,
|
|
-- but we can't quite call dropContact yet, as it would cancel the timer.
|
|
self.down = false
|
|
logger.dbg("Contact:handleDoubleTap: Contact lift")
|
|
end
|
|
|
|
--[[--
|
|
Handles move (switch to panState) & hold (switch to holdState). Contact is down.
|
|
`new_tap` is true for the initial contact down event.
|
|
--]]
|
|
function Contact:handleNonTap(new_tap)
|
|
local slot = self.slot
|
|
--logger.dbg("Contact:handleNonTap for slot", slot)
|
|
local tev = self.current_tev
|
|
local gesture_detector = self.ges_dec
|
|
|
|
-- If we haven't yet fired the hold timer, do so first and foremost, as hold_pan handling *requires* a hold.
|
|
-- We only do this on the first contact down.
|
|
if new_tap and not self.pending_hold_timer then
|
|
logger.dbg("set up hold timer")
|
|
self.pending_hold_timer = true
|
|
gesture_detector.input:setTimeout(slot, "hold", function()
|
|
-- If this contact is still active & alive and its timer hasn't been cancelled,
|
|
-- (e.g., it hasn't gone through dropContact because of a contact lift yet),
|
|
-- then check that we're still in a stationary contact down state (i.e., tapState).
|
|
-- NOTE: We need to check that the current contact in this slot is *still* the same object first, because closure ;).
|
|
if self == gesture_detector:getContact(slot) and self.pending_hold_timer then
|
|
self.pending_hold_timer = nil
|
|
if self.state == Contact.tapState and self.down then
|
|
-- NOTE: If we happened to have moved enough, holdState will generate a hold_pan on the *next* event,
|
|
-- but for now, the initial hold is mandatory.
|
|
-- On the other hand, if we're *already* in pan state, we stay there and *never* switch to hold.
|
|
logger.dbg("hold timer tripped a switch to hold state in slot", slot)
|
|
return self:switchState(Contact.holdState, true)
|
|
end
|
|
end
|
|
end, tev.timev, gesture_detector.ges_hold_interval)
|
|
|
|
-- NOTE: We only generate touch *once*, on first contact down (at which point there's not enough history to trip a pan).
|
|
return {
|
|
ges = "touch",
|
|
pos = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
},
|
|
time = tev.timev,
|
|
}
|
|
else
|
|
-- Once the hold timer has been fired, we're free to see if we can switch to pan,
|
|
-- if the contact moved far enough on the X or Y axes...
|
|
if (math.abs(tev.x - self.initial_tev.x) >= gesture_detector.PAN_THRESHOLD) or
|
|
(math.abs(tev.y - self.initial_tev.y) >= gesture_detector.PAN_THRESHOLD) then
|
|
return self:switchState(Contact.panState)
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Handles the full panel of pans & swipes, including their two-finger variants.
|
|
--]]
|
|
function Contact:panState(keep_contact)
|
|
local slot = self.slot
|
|
local tev = self.current_tev
|
|
local buddy_contact = self.buddy_contact
|
|
local gesture_detector = self.ges_dec
|
|
|
|
logger.dbg("slot", slot, "in pan state...")
|
|
if tev.id == -1 then
|
|
-- End of pan, emit swipe and swipe-like gestures if necessary
|
|
if self:isSwipe() then
|
|
if buddy_contact and self.down then
|
|
-- Both main contacts are actives and we are down, mark that slot
|
|
self.mt_gesture = "swipe"
|
|
-- Neuter its buddy
|
|
-- NOTE: Similar trickery as in handlePan to deal with rotate,
|
|
-- without the panState check for self, because we're obviously in panState...
|
|
if buddy_contact.state ~= Contact.panState and buddy_contact.mt_immobile then
|
|
buddy_contact.mt_gesture = "rotate"
|
|
else
|
|
buddy_contact.mt_gesture = "swipe"
|
|
end
|
|
buddy_contact.state = Contact.voidState
|
|
|
|
local ges_ev = self:handleTwoFingerPan(buddy_contact)
|
|
if ges_ev then
|
|
if buddy_contact.mt_gesture == "swipe" then
|
|
-- Only accept gestures that require both contacts to have been lifted
|
|
if ges_ev.ges == "two_finger_pan" then
|
|
ges_ev.ges = "two_finger_swipe"
|
|
elseif ges_ev.ges == "inward_pan" then
|
|
ges_ev.ges = "pinch"
|
|
elseif ges_ev.ges == "outward_pan" then
|
|
ges_ev.ges = "spread"
|
|
else
|
|
ges_ev = nil
|
|
end
|
|
else
|
|
-- Only accept the rotate gesture
|
|
if ges_ev.ges ~= "rotate" then
|
|
ges_ev = nil
|
|
end
|
|
end
|
|
|
|
if ges_ev then
|
|
logger.dbg(ges_ev.ges, ges_ev.direction, ges_ev.distance or math.abs(ges_ev.angle), "detected")
|
|
end
|
|
end
|
|
|
|
-- Don't drop buddy, voidState will handle it.
|
|
-- NOTE: This is a hack for out of order rotate lifts when we have to fake a lift from voidState:
|
|
-- when `keep_contact` is true, this isn't an actual contact lift,
|
|
-- so we can't destroy the contact just yet...
|
|
if not keep_contact then
|
|
gesture_detector:dropContact(self)
|
|
end
|
|
return ges_ev
|
|
elseif self.down then
|
|
return self:handleSwipe()
|
|
end
|
|
elseif self.down then
|
|
-- If the contact lift is not a swipe, then it's a pan.
|
|
return self:handlePanRelease(keep_contact)
|
|
else
|
|
-- Huh, caught a *second* contact lift for this contact? (should never happen).
|
|
logger.warn("Contact:panState Cancelled a gesture")
|
|
gesture_detector:dropContact(self)
|
|
end
|
|
else
|
|
return self:handlePan()
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Used to ignore a buddy slot part of a MT gesture, so that we don't send duplicate events.
|
|
--]]
|
|
function Contact:voidState()
|
|
local slot = self.slot
|
|
local tev = self.current_tev
|
|
local buddy_contact = self.buddy_contact
|
|
local buddy_slot = buddy_contact and self.buddy_contact.slot
|
|
local gesture_detector = self.ges_dec
|
|
|
|
logger.dbg("slot", slot, "in void state...")
|
|
-- We basically don't do anything but drop the slot on contact lift,
|
|
-- if need be deferring to the right state when we're part of a MT gesture.
|
|
if tev.id == -1 then
|
|
if self.down and buddy_contact and buddy_contact.down and self.mt_gesture then
|
|
-- If we were lifted before our buddy, and we're part of a MT gesture,
|
|
-- defer to the proper state (wthout switching state ourselves).
|
|
if self.mt_gesture == "tap" then
|
|
return self:tapState()
|
|
elseif self.mt_gesture == "swipe" or self.mt_gesture == "pan" or self.mt_gesture == "pan_release" then
|
|
return self:panState()
|
|
elseif self.mt_gesture == "rotate" then
|
|
-- NOTE: As usual, rotate requires some trickery,
|
|
-- because it's the only gesture that requires both slots to be in *different* states...
|
|
-- (The trigger contact *has* to be the panning one; while we're the held one in this scenario).
|
|
logger.dbg("Contact:voidState Deferring to panState via buddy slot", buddy_slot, "to handle MT contact lift for a rotate")
|
|
local ges_ev
|
|
local buddy_tid = buddy_contact.current_tev.id
|
|
if buddy_tid == -1 then
|
|
-- It's an actual lift for buddy, so we can just send it along, panState will drop the contact.
|
|
ges_ev = buddy_contact:panState()
|
|
else
|
|
-- But *this* means the lifts are staggered, and we, the hold pivot, were lifted *first*.
|
|
-- To avoid further issues, we'll forcibly lift buddy for this single call,
|
|
-- to make sure panState tries for the rotate gesture *now*,
|
|
-- while asking it *not* to drop itself just now (as it's not an actual contact lift just yet) so that...
|
|
buddy_contact.current_tev.id = -1
|
|
ges_ev = buddy_contact:panState(true)
|
|
-- ...we can then send it to the void.
|
|
-- Whether the gesture fails or not, it'll be in voidState and only dropped on actual contact lift,
|
|
-- regardless of whether the driver repeats ABS_MT_TRACKING_ID values or not.
|
|
-- Otherwise, if it only lifts on the next input frame,
|
|
-- it won't go through MT codepaths at all, and you'll end up with a single swipe,
|
|
-- and if it lifts even later, we'd have to deal with spurious moves first, probably leading into a tap...
|
|
-- If the gesture *succeeds*, the buddy contact will be dropped whenever it's actually lifted,
|
|
-- thanks to the temporary tracking id switcheroo & voidState...
|
|
buddy_contact.state = Contact.voidState
|
|
buddy_contact.current_tev.id = buddy_tid
|
|
end
|
|
-- Regardless of whether we detected a gesture, this is a contact lift, so it's curtains for us!
|
|
gesture_detector:dropContact(self)
|
|
return ges_ev
|
|
elseif self.mt_gesture == "hold" or self.mt_gesture == "hold_pan" or
|
|
self.mt_gesture == "hold_release" or self.mt_gesture == "hold_pan_release" then
|
|
return self:holdState()
|
|
else
|
|
-- Should absolutely never happen (and, at the time of writing, is technically guaranteed to be unreachable).
|
|
logger.warn("Contact:voidState Unknown MT gesture", self.mt_gesture, "cannot handle contact lift properly")
|
|
-- We're still gone, though.
|
|
gesture_detector:dropContact(self)
|
|
end
|
|
elseif self.down then
|
|
-- We were lifted *after* our buddy, the gesture already went through, we can silently slink away into the night.
|
|
logger.dbg("Contact:voidState Contact lift detected")
|
|
gesture_detector:dropContact(self)
|
|
else
|
|
-- Huh, caught a *second* contact lift for this contact? (should never happen).
|
|
logger.warn("Contact:voidState Cancelled a gesture")
|
|
gesture_detector:dropContact(self)
|
|
end
|
|
else
|
|
-- We need to be able to discriminate between a moving and unmoving contact for rotate/pan discrimination.
|
|
if self.mt_immobile then
|
|
if (math.abs(tev.x - self.initial_tev.x) >= gesture_detector.PAN_THRESHOLD) or
|
|
(math.abs(tev.y - self.initial_tev.y) >= gesture_detector.PAN_THRESHOLD) then
|
|
self.mt_immobile = false
|
|
-- NOTE: We've just moved: if we were flagged for a hold gesture (meaning our buddy is still in holdState),
|
|
-- that won't do anymore, switch to a setup for a rotate gesture.
|
|
-- (This happens when attempting a rotate, and the hold timer for slot 0 expires
|
|
-- before slot 1 has the chance to switch to panState).
|
|
-- A.K.A., "rotate dirty hack #42" ;).
|
|
if buddy_contact and buddy_contact.mt_gesture == "hold" and self.mt_gesture == "hold" then
|
|
self.mt_gesture = "pan"
|
|
buddy_contact.mt_gesture = "rotate"
|
|
logger.dbg("Contact:voidState We moved while pending a hold gesture, swap to a rotate setup")
|
|
end
|
|
else
|
|
self.mt_immobile = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Emits the swipe & multiswipe gestures. Contact is up. ST only (i.e., there isn't any buddy contact active).
|
|
--]]
|
|
function Contact:handleSwipe()
|
|
--logger.dbg("Contact:handleSwipe for slot", self.slot)
|
|
local tev = self.current_tev
|
|
local gesture_detector = self.ges_dec
|
|
|
|
local swipe_direction, swipe_distance = self:getPath()
|
|
local start_pos = Geom:new{
|
|
x = self.initial_tev.x,
|
|
y = self.initial_tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local ges = "swipe"
|
|
local multiswipe_directions
|
|
|
|
if #self.multiswipe_directions > 1 then
|
|
ges = "multiswipe"
|
|
multiswipe_directions = ""
|
|
for k, v in ipairs(self.multiswipe_directions) do
|
|
local sep = ""
|
|
if k > 1 then
|
|
sep = " "
|
|
end
|
|
multiswipe_directions = multiswipe_directions .. sep .. v[1]
|
|
end
|
|
logger.dbg("multiswipe", multiswipe_directions)
|
|
end
|
|
|
|
logger.dbg("Contact:handleSwipe: swipe", swipe_direction, swipe_distance, "detected")
|
|
gesture_detector:dropContact(self)
|
|
return {
|
|
ges = ges,
|
|
-- use first pan tev coordination as swipe start point
|
|
pos = start_pos,
|
|
direction = swipe_direction,
|
|
multiswipe_directions = multiswipe_directions,
|
|
distance = swipe_distance,
|
|
time = tev.timev,
|
|
}
|
|
end
|
|
|
|
--[[--
|
|
Emits the pan gestures and handles their two finger variants. Contact is down (and either in holdState or panState).
|
|
--]]
|
|
function Contact:handlePan()
|
|
--logger.dbg("Contact:handlePan for slot", self.slot)
|
|
local tev = self.current_tev
|
|
local buddy_contact = self.buddy_contact
|
|
local gesture_detector = self.ges_dec
|
|
|
|
if buddy_contact and self.down then
|
|
-- Both main contacts are actives and we are down, mark that slot
|
|
self.mt_gesture = "pan"
|
|
-- Neuter its buddy
|
|
-- NOTE: Small trickery for rotate, which requires both contacts to be in very specific states.
|
|
-- We merge tapState with holdState because it's likely that the hold hasn't taken yet,
|
|
-- and it never will after that because we switch to voidState ;).
|
|
if buddy_contact.state ~= Contact.panState and buddy_contact.mt_immobile and
|
|
self.state == Contact.panState then
|
|
buddy_contact.mt_gesture = "rotate"
|
|
else
|
|
buddy_contact.mt_gesture = "pan"
|
|
end
|
|
buddy_contact.state = Contact.voidState
|
|
|
|
return self:handleTwoFingerPan(buddy_contact)
|
|
elseif self.down then
|
|
local pan_direction, pan_distance = self:getPath()
|
|
local pan_ev = {
|
|
ges = "pan",
|
|
relative = {
|
|
-- default to pan 0
|
|
x = 0,
|
|
y = 0,
|
|
},
|
|
pos = nil,
|
|
direction = pan_direction,
|
|
distance = pan_distance,
|
|
time = tev.timev,
|
|
}
|
|
|
|
-- Regular pan
|
|
pan_ev.relative.x = tev.x - self.initial_tev.x
|
|
pan_ev.relative.y = tev.y - self.initial_tev.y
|
|
|
|
pan_ev.pos = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
|
|
local msd_cnt = #self.multiswipe_directions
|
|
local msd_direction_prev = (msd_cnt > 0) and self.multiswipe_directions[msd_cnt][1] or ""
|
|
local prev_ms_ev, fake_initial_tev
|
|
|
|
if msd_cnt == 0 then
|
|
-- determine whether to initiate a straight or diagonal multiswipe
|
|
self.multiswipe_type = "straight"
|
|
if pan_direction ~= "north" and pan_direction ~= "south" and
|
|
pan_direction ~= "east" and pan_direction ~= "west" then
|
|
self.multiswipe_type = "diagonal"
|
|
end
|
|
elseif msd_cnt > 0 then
|
|
-- recompute a more accurate direction and distance in a multiswipe context
|
|
prev_ms_ev = self.multiswipe_directions[msd_cnt][2]
|
|
fake_initial_tev = {
|
|
x = prev_ms_ev.pos.x,
|
|
y = prev_ms_ev.pos.y,
|
|
}
|
|
end
|
|
|
|
-- the first time, fake_initial_tev is nil, so the contact's initial_tev is automatically used instead
|
|
local msd_direction, msd_distance
|
|
if self.multiswipe_type == "straight" then
|
|
msd_direction, msd_distance = self:getPath(true, false, fake_initial_tev)
|
|
else
|
|
msd_direction, msd_distance = self:getPath(true, true, fake_initial_tev)
|
|
end
|
|
|
|
if msd_distance > gesture_detector.MULTISWIPE_THRESHOLD then
|
|
local pan_ev_multiswipe = pan_ev
|
|
-- store a copy of pan_ev without rotation adjustment for multiswipe calculations when rotated
|
|
if gesture_detector.screen:getTouchRotation() > gesture_detector.screen.ORIENTATION_PORTRAIT then
|
|
pan_ev_multiswipe = util.tableDeepCopy(pan_ev)
|
|
end
|
|
if msd_direction ~= msd_direction_prev then
|
|
self.multiswipe_directions[msd_cnt+1] = {
|
|
[1] = msd_direction,
|
|
[2] = pan_ev_multiswipe,
|
|
}
|
|
else
|
|
-- update ongoing swipe direction to the new maximum
|
|
self.multiswipe_directions[msd_cnt] = {
|
|
[1] = msd_direction,
|
|
[2] = pan_ev_multiswipe,
|
|
}
|
|
end
|
|
end
|
|
|
|
return pan_ev
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Emits the pan, two_finger_pan, two_finger_hold_pan, inward_pan, outward_pan & rotate gestures.
|
|
Contact is down in panState or holdState, or up in panState if it was lifted below the swipe interval.
|
|
--]]
|
|
function Contact:handleTwoFingerPan(buddy_contact)
|
|
--logger.dbg("Contact:handleTwoFingerPan for slot", self.slot)
|
|
local gesture_detector = self.ges_dec
|
|
|
|
-- triggering contact is self
|
|
-- reference contact is buddy_contact
|
|
local tpan_dir, tpan_dis = self:getPath()
|
|
local tstart_pos = Geom:new{
|
|
x = self.initial_tev.x,
|
|
y = self.initial_tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local tend_pos = Geom:new{
|
|
x = self.current_tev.x,
|
|
y = self.current_tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local rstart_pos = Geom:new{
|
|
x = buddy_contact.initial_tev.x,
|
|
y = buddy_contact.initial_tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
if self.current_tev.id == -1 and buddy_contact.mt_gesture == "rotate" then
|
|
-- NOTE: We only handle the rotate gesture when triggered by the just lifted pan finger
|
|
-- (actually, it needs to pass the swipe interval check, but it is in panState),
|
|
-- because this gesture would be too difficult to discriminate from a pinch/spread the other way around ;).
|
|
-- TL;DR: Both fingers need to move for a pinch/spread, while a finger needs to stay still for a rotate.
|
|
-- NOTE: FWIW, on an Elipsa, if we misdetect a pinch (i.e., both fingers moved) for a rotate
|
|
-- because the buddy slot failed to pass the pan threshold, we get a very shallow angle (often < 1°, at most ~2°).
|
|
-- If, on the other hand, we misdetect a rotate that *looked* like a pinch,
|
|
-- (i.e., a pinch with only one finger moving), we get slightly larger angles (~5°).
|
|
-- Things get wildly more difficult on an Android phone, where you can easily add ~10° of noise to those results.
|
|
-- TL;DR: We just chuck those as misdetections instead of adding brittle heuristics to correct course ;).
|
|
local angle = gesture_detector:getRotate(rstart_pos, tstart_pos, tend_pos)
|
|
logger.dbg("Contact:handleTwoFingerPan: rotate", angle, "detected")
|
|
return {
|
|
ges = "rotate",
|
|
pos = rstart_pos,
|
|
angle = angle,
|
|
direction = angle >= 0 and "cw" or "ccw",
|
|
time = self.current_tev.timev,
|
|
}
|
|
else
|
|
local rpan_dir, rpan_dis = buddy_contact:getPath()
|
|
local rend_pos = Geom:new{
|
|
x = buddy_contact.current_tev.x,
|
|
y = buddy_contact.current_tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
-- Use midpoint of tstart and rstart as swipe start point
|
|
local start_point = tstart_pos:midpoint(rstart_pos)
|
|
local end_point = tend_pos:midpoint(rend_pos)
|
|
-- Compute the distance based on the start & end midpoints
|
|
local avg_distance = start_point:distance(end_point)
|
|
-- We'll also want to remember the span between both contacts on start & end for some gestures
|
|
local start_distance = tstart_pos:distance(rstart_pos)
|
|
local end_distance = tend_pos:distance(rend_pos)
|
|
local ges_ev = {
|
|
ges = "two_finger_pan",
|
|
pos = start_point,
|
|
distance = avg_distance,
|
|
direction = tpan_dir,
|
|
time = self.current_tev.timev,
|
|
}
|
|
if tpan_dir ~= rpan_dir then
|
|
if start_distance > end_distance then
|
|
ges_ev.ges = "inward_pan"
|
|
else
|
|
ges_ev.ges = "outward_pan"
|
|
end
|
|
ges_ev.direction = gesture_detector.DIRECTION_TABLE[tpan_dir]
|
|
-- Use the the sum of both contacts' travel for the distance
|
|
ges_ev.distance = tpan_dis + rpan_dis
|
|
-- Some handlers might also want to know the distance between the two contacts on lift & down.
|
|
ges_ev.span = end_distance
|
|
ges_ev.start_span = start_distance
|
|
elseif self.state == Contact.holdState then
|
|
ges_ev.ges = "two_finger_hold_pan"
|
|
-- Flag 'em for holdState to discriminate with two_finger_hold_release
|
|
self.mt_gesture = "hold_pan"
|
|
buddy_contact.mt_gesture = "hold_pan"
|
|
end
|
|
|
|
logger.dbg("Contact:handleTwoFingerPan:", ges_ev.ges, ges_ev.direction, ges_ev.distance, "detected")
|
|
return ges_ev
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Emits the pan_release & two_finger_pan_release gestures. Contact is up (but down is still true) and in panState.
|
|
--]]
|
|
function Contact:handlePanRelease(keep_contact)
|
|
--logger.dbg("Contact:handlePanRelease for slot", self.slot)
|
|
local tev = self.current_tev
|
|
local buddy_contact = self.buddy_contact
|
|
local gesture_detector = self.ges_dec
|
|
|
|
local release_pos = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local pan_ev = {
|
|
ges = "pan_release",
|
|
pos = release_pos,
|
|
time = tev.timev,
|
|
}
|
|
if buddy_contact and self.down then
|
|
-- Both main contacts are actives and we are down, mark that slot
|
|
self.mt_gesture = "pan_release"
|
|
-- Neuter its buddy
|
|
buddy_contact.state = Contact.voidState
|
|
buddy_contact.mt_gesture = "pan_release"
|
|
|
|
logger.dbg("Contact:handlePanRelease: two_finger_pan_release detected")
|
|
pan_ev.ges = "two_finger_pan_release"
|
|
-- Don't drop buddy, voidState will handle it
|
|
-- NOTE: This is yet another rotate hack, emanating from voidState into panState.
|
|
if not keep_contact then
|
|
gesture_detector:dropContact(self)
|
|
end
|
|
return pan_ev
|
|
elseif self.down then
|
|
logger.dbg("Contact:handlePanRelease: pan release detected")
|
|
gesture_detector:dropContact(self)
|
|
return pan_ev
|
|
else
|
|
-- Huh, caught a *second* contact lift for this contact? (should never happen).
|
|
logger.warn("Contact:handlePanRelease Cancelled a gesture")
|
|
gesture_detector:dropContact(self)
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Emits the hold, hold_release & hold_pan gestures and their two_finger variants.
|
|
--]]
|
|
function Contact:holdState(new_hold)
|
|
local slot = self.slot
|
|
local tev = self.current_tev
|
|
local buddy_contact = self.buddy_contact
|
|
local gesture_detector = self.ges_dec
|
|
|
|
logger.dbg("slot", slot, "in hold state...")
|
|
-- When we switch to hold state, we pass an additional boolean param "new_hold".
|
|
if new_hold and tev.id ~= -1 then
|
|
if buddy_contact and self.down then
|
|
-- Both main contacts are actives and we are down, mark that slot
|
|
self.mt_gesture = "hold"
|
|
-- Neuter its buddy
|
|
buddy_contact.state = Contact.voidState
|
|
buddy_contact.mt_gesture = "hold"
|
|
|
|
local pos0 = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local pos1 = Geom:new{
|
|
x = buddy_contact.current_tev.x,
|
|
y = buddy_contact.current_tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local tap_span = pos0:distance(pos1)
|
|
local tap_pos = pos0:midpoint(pos1)
|
|
logger.dbg("two_finger_hold detected @", tap_pos.x, tap_pos.y, "with span", tap_span)
|
|
return {
|
|
ges = "two_finger_hold",
|
|
pos = tap_pos,
|
|
span = tap_span,
|
|
time = tev.timev,
|
|
}
|
|
elseif self.down then
|
|
logger.dbg("hold detected @", tev.x, tev.y)
|
|
return {
|
|
ges = "hold",
|
|
pos = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
},
|
|
time = tev.timev,
|
|
}
|
|
end
|
|
elseif tev.id == -1 then
|
|
if buddy_contact and self.down then
|
|
-- Both main contacts are actives and we are down, mark that slot
|
|
if self.mt_gesture == "rotate" and buddy_contact.mt_gesture == "pan" then
|
|
-- NOTE: We're setup as the hold in a rotate gesture, and we were lifted *before* our pan buddy,
|
|
-- do a bit of gymnastics, because the trigger contact for a rotate *needs* to be the pan...
|
|
-- This is a snow protocol special :/.
|
|
-- NOTE: This is simpler than the elaborate trickery this case involves when dealt with via voidState,
|
|
-- because it is specifically aimed at the snow protocol, so we *know* both contacts are lifted
|
|
-- in the same input frame.
|
|
logger.dbg("Contact:holdState: Early lift as a rotate pivot, trying for a rotate...")
|
|
local ges_ev = buddy_contact:handleTwoFingerPan(self)
|
|
if ges_ev then
|
|
if ges_ev.ges ~= "rotate" then
|
|
ges_ev = nil
|
|
else
|
|
logger.dbg(ges_ev.ges, ges_ev.direction, math.abs(ges_ev.angle), "detected")
|
|
end
|
|
end
|
|
-- Regardless of whether this panned out (pun intended), this is a lift, so we'll defer to voidState next.
|
|
buddy_contact.state = Contact.voidState
|
|
gesture_detector:dropContact(self)
|
|
return ges_ev
|
|
elseif self.mt_gesture == "hold_pan" or self.mt_gesture == "pan" then
|
|
self.mt_gesture = "hold_pan_release"
|
|
buddy_contact.mt_gesture = "hold_pan_release"
|
|
else
|
|
self.mt_gesture = "hold_release"
|
|
buddy_contact.mt_gesture = "hold_release"
|
|
end
|
|
-- Neuter its buddy
|
|
buddy_contact.state = Contact.voidState
|
|
|
|
-- Don't drop buddy, voidState will handle it
|
|
gesture_detector:dropContact(self)
|
|
|
|
local pos0 = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local pos1 = Geom:new{
|
|
x = buddy_contact.current_tev.x,
|
|
y = buddy_contact.current_tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
}
|
|
local ges_type = self.mt_gesture == "hold_pan_release" and "two_finger_hold_pan_release" or "two_finger_hold_release"
|
|
local tap_span = pos0:distance(pos1)
|
|
local tap_pos = pos0:midpoint(pos1)
|
|
logger.dbg(ges_type, "detected @", tap_pos.x, tap_pos.y, "with span", tap_span)
|
|
return {
|
|
ges = ges_type,
|
|
pos = tap_pos,
|
|
span = tap_span,
|
|
time = tev.timev,
|
|
}
|
|
elseif self.down then
|
|
-- Contact lift, emit a hold_release
|
|
logger.dbg("hold_release detected @", tev.x, tev.y)
|
|
gesture_detector:dropContact(self)
|
|
return {
|
|
ges = "hold_release",
|
|
pos = Geom:new{
|
|
x = tev.x,
|
|
y = tev.y,
|
|
w = 0,
|
|
h = 0,
|
|
},
|
|
time = tev.timev,
|
|
}
|
|
else
|
|
-- Huh, caught a *second* contact lift for this contact? (should never happen).
|
|
logger.warn("Contact:holdState Cancelled a gesture")
|
|
gesture_detector:dropContact(self)
|
|
end
|
|
elseif tev.id ~= -1 and ((math.abs(tev.x - self.initial_tev.x) >= gesture_detector.PAN_THRESHOLD) or
|
|
(math.abs(tev.y - self.initial_tev.y) >= gesture_detector.PAN_THRESHOLD)) then
|
|
-- We've moved enough to count as a pan, defer to the pan handler, but stay in holdState
|
|
local ges_ev = self:handlePan()
|
|
if ges_ev ~= nil then
|
|
if ges_ev.ges ~= "two_finger_hold_pan" then
|
|
ges_ev.ges = "hold_pan"
|
|
end
|
|
end
|
|
return ges_ev
|
|
end
|
|
end
|
|
|
|
local ges_coordinate_translation_270 = {
|
|
north = "west",
|
|
south = "east",
|
|
east = "north",
|
|
west = "south",
|
|
northeast = "northwest",
|
|
northwest = "southwest",
|
|
southeast = "northeast",
|
|
southwest = "southeast",
|
|
}
|
|
local ges_coordinate_translation_180 = {
|
|
north = "south",
|
|
south = "north",
|
|
east = "west",
|
|
west = "east",
|
|
northeast = "southwest",
|
|
northwest = "southeast",
|
|
southeast = "northwest",
|
|
southwest = "northeast",
|
|
}
|
|
local ges_coordinate_translation_90 = {
|
|
north = "east",
|
|
south = "west",
|
|
east = "south",
|
|
west = "north",
|
|
northeast = "southeast",
|
|
northwest = "northeast",
|
|
southeast = "southwest",
|
|
southwest = "northwest",
|
|
}
|
|
local function translateGesDirCoordinate(direction, translation_table)
|
|
return translation_table[direction]
|
|
end
|
|
local function translateMultiswipeGesDirCoordinate(multiswipe_directions, translation_table)
|
|
return multiswipe_directions:gsub("%S+", translation_table)
|
|
end
|
|
|
|
--[[--
|
|
Changes gesture's `x` and `y` coordinates according to screen view mode.
|
|
|
|
@param ges gesture that you want to adjust
|
|
@return adjusted gesture.
|
|
--]]
|
|
function GestureDetector:adjustGesCoordinate(ges)
|
|
local mode = self.screen:getTouchRotation()
|
|
if mode == self.screen.ORIENTATION_LANDSCAPE then
|
|
-- in landscape mode rotated 90
|
|
if ges.pos then
|
|
ges.pos.x, ges.pos.y = (self.screen:getWidth() - ges.pos.y), (ges.pos.x)
|
|
end
|
|
if ges.ges == "swipe" or ges.ges == "pan"
|
|
or ges.ges == "hold_pan"
|
|
or ges.ges == "multiswipe"
|
|
or ges.ges == "two_finger_swipe"
|
|
or ges.ges == "two_finger_pan"
|
|
or ges.ges == "two_finger_hold_pan"
|
|
then
|
|
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_90)
|
|
if ges.ges == "multiswipe" then
|
|
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_90)
|
|
logger.dbg("GestureDetector: Landscape translation for multiswipe:", ges.multiswipe_directions)
|
|
else
|
|
logger.dbg("GestureDetector: Landscape translation for ges:", ges.ges, ges.direction)
|
|
end
|
|
if ges.relative then
|
|
ges.relative.x, ges.relative.y = -ges.relative.y, ges.relative.x
|
|
end
|
|
elseif ges.ges == "pinch" or ges.ges == "spread"
|
|
or ges.ges == "inward_pan"
|
|
or ges.ges == "outward_pan" then
|
|
if ges.direction == "horizontal" then
|
|
ges.direction = "vertical"
|
|
elseif ges.direction == "vertical" then
|
|
ges.direction = "horizontal"
|
|
end
|
|
logger.dbg("GestureDetector: Landscape translation for ges:", ges.ges, ges.direction)
|
|
end
|
|
elseif mode == self.screen.ORIENTATION_LANDSCAPE_ROTATED then
|
|
-- in landscape mode rotated 270
|
|
if ges.pos then
|
|
ges.pos.x, ges.pos.y = (ges.pos.y), (self.screen:getHeight() - ges.pos.x)
|
|
end
|
|
if ges.ges == "swipe" or ges.ges == "pan"
|
|
or ges.ges == "hold_pan"
|
|
or ges.ges == "multiswipe"
|
|
or ges.ges == "two_finger_swipe"
|
|
or ges.ges == "two_finger_pan"
|
|
or ges.ges == "two_finger_hold_pan"
|
|
then
|
|
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_270)
|
|
if ges.ges == "multiswipe" then
|
|
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_270)
|
|
logger.dbg("GestureDetector: Inverted landscape translation for multiswipe:", ges.multiswipe_directions)
|
|
else
|
|
logger.dbg("GestureDetector: Inverted landscape translation for ges:", ges.ges, ges.direction)
|
|
end
|
|
if ges.relative then
|
|
ges.relative.x, ges.relative.y = ges.relative.y, -ges.relative.x
|
|
end
|
|
elseif ges.ges == "pinch" or ges.ges == "spread"
|
|
or ges.ges == "inward_pan"
|
|
or ges.ges == "outward_pan" then
|
|
if ges.direction == "horizontal" then
|
|
ges.direction = "vertical"
|
|
elseif ges.direction == "vertical" then
|
|
ges.direction = "horizontal"
|
|
end
|
|
logger.dbg("GestureDetector: Inverted landscape translation for ges:", ges.ges, ges.direction)
|
|
end
|
|
elseif mode == self.screen.ORIENTATION_PORTRAIT_ROTATED then
|
|
-- in portrait mode rotated 180
|
|
if ges.pos then
|
|
ges.pos.x, ges.pos.y = (self.screen:getWidth() - ges.pos.x), (self.screen:getHeight() - ges.pos.y)
|
|
end
|
|
if ges.ges == "swipe" or ges.ges == "pan"
|
|
or ges.ges == "hold_pan"
|
|
or ges.ges == "multiswipe"
|
|
or ges.ges == "two_finger_swipe"
|
|
or ges.ges == "two_finger_pan"
|
|
or ges.ges == "two_finger_hold_pan"
|
|
then
|
|
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_180)
|
|
if ges.ges == "multiswipe" then
|
|
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_180)
|
|
logger.dbg("GestureDetector: Inverted portrait translation for multiswipe:", ges.multiswipe_directions)
|
|
else
|
|
logger.dbg("GestureDetector: Inverted portrait translation for ges:", ges.ges, ges.direction)
|
|
end
|
|
if ges.relative then
|
|
ges.relative.x, ges.relative.y = -ges.relative.x, -ges.relative.y
|
|
end
|
|
end
|
|
-- pinch/spread are unaffected
|
|
end
|
|
return ges
|
|
end
|
|
|
|
return GestureDetector
|