mirror of
https://github.com/koreader/koreader
synced 2024-11-13 19:11:25 +00:00
b0d8919399
* ImageViewer: Minor code cleanups * GestureDetector: Fix the `distance` field of `two_finger_pan` & `two_finger_swipe` gestures so that it's no longer the double of the actual distance traveled. Get rid of existing workarounds throughout the codebase that had to deal with this quirk.
1427 lines
58 KiB
Lua
1427 lines
58 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
|
|
|
|
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 = {
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
end
|
|
if ges.relative then
|
|
ges.relative.x, ges.relative.y = -ges.relative.x, -ges.relative.y
|
|
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 = "horizontal"
|
|
elseif ges.direction == "vertical" then
|
|
ges.direction = "vertical"
|
|
end
|
|
end
|
|
end
|
|
logger.dbg("adjusted ges:", ges.ges, ges.multiswipe_directions or ges.direction)
|
|
return ges
|
|
end
|
|
|
|
return GestureDetector
|