Add per-tool settings and gesture customization
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Recognize one touchpad contact that has paused before a quick second-finger
|
||||
-- tap. Hide the tapping contact from libinput and emit a middle click instead.
|
||||
-- Recognize one established touchpad contact followed by a quick second-finger
|
||||
-- tap. Hide the tapping contact from libinput and emit a configured action.
|
||||
|
||||
libinput:register({1})
|
||||
|
||||
local MINIMUM_STATIONARY_US = 100 * 1000
|
||||
local MAXIMUM_TAP_US = 150 * 1000
|
||||
local settings = TOUCHPAD_HOLD_TAP_SETTINGS or {}
|
||||
local MINIMUM_ANCHOR_AGE_US = (settings.minimum_anchor_age_ms or 100) * 1000
|
||||
local MINIMUM_STATIONARY_US = (settings.minimum_pause_ms or 100) * 1000
|
||||
local MINIMUM_TAP_US = (settings.minimum_tap_ms or 10) * 1000
|
||||
local MAXIMUM_TAP_US = (settings.maximum_tap_ms or 150) * 1000
|
||||
local MAXIMUM_ANCHOR_MOVEMENT_MM = 1.5
|
||||
local MAXIMUM_TAPPER_MOVEMENT_MM = 1.0
|
||||
local OUTPUT_EVENT_NAME = settings.output_event or "BTN_MIDDLE"
|
||||
local OUTPUT_USAGE = evdev[OUTPUT_EVENT_NAME]
|
||||
|
||||
local states = {}
|
||||
local button_devices = {}
|
||||
local button_device = nil
|
||||
local output_devices = {}
|
||||
local output_device = nil
|
||||
|
||||
local tool_usages = {
|
||||
{ usage = evdev.BTN_TOOL_FINGER, count = 1 },
|
||||
@@ -131,18 +136,19 @@ local function restore_candidate(device, state, reason)
|
||||
"touchpad-hold-tap: restored candidate (" .. reason .. ")")
|
||||
end
|
||||
|
||||
local function emit_middle_click()
|
||||
if not button_device then
|
||||
local function emit_action()
|
||||
if not output_device then
|
||||
libinput:log_error(
|
||||
"touchpad-hold-tap: no pointer device available for button output")
|
||||
"touchpad-hold-tap: no device available for " ..
|
||||
OUTPUT_EVENT_NAME .. " output")
|
||||
return false
|
||||
end
|
||||
|
||||
button_device:append_frame({
|
||||
{ usage = evdev.BTN_MIDDLE, value = 1 },
|
||||
output_device:append_frame({
|
||||
{ usage = OUTPUT_USAGE, value = 1 },
|
||||
})
|
||||
button_device:append_frame({
|
||||
{ usage = evdev.BTN_MIDDLE, value = 0 },
|
||||
output_device:append_frame({
|
||||
{ usage = OUTPUT_USAGE, value = 0 },
|
||||
})
|
||||
return true
|
||||
end
|
||||
@@ -185,8 +191,14 @@ local function handle_tracking_begin(device, state, tracking_id, timestamp)
|
||||
state.anchor_slot = contact.slot
|
||||
elseif state.mode == "anchor" and previous_count == 1 then
|
||||
local anchor = state.contacts[state.anchor_slot]
|
||||
if anchor and anchor.rest_x and anchor.rest_y and
|
||||
timestamp - anchor.stationary_since >= MINIMUM_STATIONARY_US then
|
||||
local old_enough = anchor and
|
||||
timestamp - anchor.down_time >=
|
||||
MINIMUM_ANCHOR_AGE_US
|
||||
local stationary = MINIMUM_STATIONARY_US == 0 or
|
||||
(anchor and anchor.rest_x and anchor.rest_y and
|
||||
timestamp - anchor.stationary_since >=
|
||||
MINIMUM_STATIONARY_US)
|
||||
if old_enough and stationary then
|
||||
contact.hidden = true
|
||||
state.mode = "candidate"
|
||||
state.candidate_slot = contact.slot
|
||||
@@ -211,7 +223,9 @@ local function handle_tracking_end(device, state, timestamp)
|
||||
if contact.hidden and state.source_slot == state.candidate_slot then
|
||||
local anchor = state.contacts[state.anchor_slot]
|
||||
local duration = timestamp - contact.down_time
|
||||
local accepted = anchor and duration <= MAXIMUM_TAP_US
|
||||
local accepted = anchor and
|
||||
duration >= MINIMUM_TAP_US and
|
||||
duration <= MAXIMUM_TAP_US
|
||||
|
||||
state.contacts[state.source_slot] = nil
|
||||
state.candidate_slot = nil
|
||||
@@ -224,11 +238,12 @@ local function handle_tracking_end(device, state, timestamp)
|
||||
end
|
||||
|
||||
if accepted then
|
||||
if emit_middle_click() then
|
||||
libinput:log_debug("touchpad-hold-tap: middle click")
|
||||
if emit_action() then
|
||||
libinput:log_debug(
|
||||
"touchpad-hold-tap: emitted " .. OUTPUT_EVENT_NAME)
|
||||
end
|
||||
else
|
||||
libinput:log_debug("touchpad-hold-tap: ignored long tap")
|
||||
libinput:log_debug("touchpad-hold-tap: ignored tap duration")
|
||||
end
|
||||
return false, true
|
||||
end
|
||||
@@ -243,6 +258,12 @@ local function handle_tracking_end(device, state, timestamp)
|
||||
if contact_count(state, false) > 0 then
|
||||
state.mode = "passthrough"
|
||||
end
|
||||
elseif state.mode == "passthrough" and state.anchor_slot and
|
||||
state.contacts[state.anchor_slot] and
|
||||
contact_count(state, false) == 1 then
|
||||
-- A rejected second contact must not poison the original anchor.
|
||||
-- It can become eligible again without first being lifted.
|
||||
state.mode = "anchor"
|
||||
end
|
||||
reset_if_clear(state)
|
||||
return true, false
|
||||
@@ -272,7 +293,7 @@ local function handle_mt_axis(device, state, event, timestamp)
|
||||
remember_axis(contact, event.usage, event.value)
|
||||
|
||||
if contact.slot == state.anchor_slot then
|
||||
if state.mode == "candidate" and
|
||||
if state.mode == "candidate" and MINIMUM_STATIONARY_US > 0 and
|
||||
movement_exceeded_from(
|
||||
state, contact, state.candidate_anchor_x,
|
||||
state.candidate_anchor_y, MAXIMUM_ANCHOR_MOVEMENT_MM) then
|
||||
@@ -373,31 +394,45 @@ end
|
||||
|
||||
local function device_removed(device)
|
||||
states[device] = nil
|
||||
button_devices[device] = nil
|
||||
if button_device == device then
|
||||
button_device = nil
|
||||
for candidate in pairs(button_devices) do
|
||||
button_device = candidate
|
||||
output_devices[device] = nil
|
||||
if output_device == device then
|
||||
output_device = nil
|
||||
for candidate in pairs(output_devices) do
|
||||
output_device = candidate
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function can_emit_output(properties, usages)
|
||||
if OUTPUT_EVENT_NAME:sub(1, 4) == "BTN_" then
|
||||
return (properties.ID_INPUT_MOUSE or
|
||||
properties.ID_INPUT_POINTINGSTICK) and
|
||||
not properties.ID_INPUT_TOUCHPAD and
|
||||
usages[evdev.REL_X] and usages[evdev.REL_Y]
|
||||
end
|
||||
return properties.ID_INPUT_KEYBOARD
|
||||
end
|
||||
|
||||
local function device_new(device)
|
||||
if not OUTPUT_USAGE then
|
||||
libinput:log_error(
|
||||
"touchpad-hold-tap: unknown output event " .. OUTPUT_EVENT_NAME)
|
||||
return
|
||||
end
|
||||
|
||||
local properties = device:udev_properties()
|
||||
local usages = device:usages()
|
||||
|
||||
-- Clickpads intentionally ignore BTN_MIDDLE from their own event node.
|
||||
-- Route synthesized buttons through an existing pointer device instead.
|
||||
if (properties.ID_INPUT_MOUSE or properties.ID_INPUT_POINTINGSTICK) and
|
||||
not properties.ID_INPUT_TOUCHPAD and
|
||||
usages[evdev.REL_X] and usages[evdev.REL_Y] then
|
||||
if not usages[evdev.BTN_MIDDLE] then
|
||||
device:enable_evdev_usage(evdev.BTN_MIDDLE)
|
||||
-- Clickpads ignore synthesized buttons on their own event node. Keyboard
|
||||
-- events likewise belong on a keyboard, so use a suitable existing device.
|
||||
if can_emit_output(properties, usages) then
|
||||
if not usages[OUTPUT_USAGE] then
|
||||
device:enable_evdev_usage(OUTPUT_USAGE)
|
||||
end
|
||||
button_devices[device] = true
|
||||
if not button_device then
|
||||
button_device = device
|
||||
output_devices[device] = true
|
||||
if not output_device then
|
||||
output_device = device
|
||||
end
|
||||
device:connect("device-removed", device_removed)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user