refactors key ticking
This commit is contained in:
parent
8d41590d09
commit
0cd4751687
|
@ -25,6 +25,13 @@ SOFTWARE.
|
|||
|
||||
-- luacheck: globals mb
|
||||
|
||||
-- Our key ticker queue: its elements are keys and modifiers to press and
|
||||
-- release, one after another, so that USB HID operations won't ever take too
|
||||
-- long as to minimize tick jitter.
|
||||
mb.keys = mb.tq:new()
|
||||
|
||||
|
||||
|
||||
-- Default delay between rapidly (repeated) key presses, can be overridden.
|
||||
mb.KEY_DELAY_MS = mb.KEY_DELAY_MS or 100
|
||||
|
||||
|
@ -59,20 +66,21 @@ function mb.tap_times(key, times, ...)
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
-- Tick mappers execute a series of tick'ered function calls, passing in each
|
||||
-- one of a sequence of things with each tick until done. So, this is like
|
||||
-- array mappers, but this time they're broken into discrete ticks. And since
|
||||
-- we sometimes need to do multiple ticked steps per element, our tick mappers
|
||||
-- can also work on sequences of functions to be called for each element.
|
||||
local TickMapper = {}
|
||||
TickMapper.__index = TickMapper
|
||||
mb.TickMapper = TickMapper
|
||||
local KeyJobMapper = {}
|
||||
KeyJobMapper.__index = KeyJobMapper
|
||||
mb.KeyJobMapper = KeyJobMapper
|
||||
|
||||
-- Creates a new tick mapper that calls a function (or a sequence of
|
||||
-- functions) on every element of an array, but only one call per tick.
|
||||
--
|
||||
-- luacheck: ignore 212/self
|
||||
function TickMapper:new(func, ...)
|
||||
function KeyJobMapper:new(func, ...)
|
||||
-- Since we allow for sequences of functions, simply turn a single
|
||||
-- function given to us into a single-element sequence. Additionally, we
|
||||
-- need to handle the special test case where someone is giving us a
|
||||
|
@ -89,13 +97,13 @@ function TickMapper:new(func, ...)
|
|||
idx=1,
|
||||
len=#{...}
|
||||
}
|
||||
return setmetatable(slf, TickMapper)
|
||||
return setmetatable(slf, KeyJobMapper)
|
||||
end
|
||||
|
||||
-- For each tick, process only the next element and next function in our list
|
||||
-- until all functions for this element, and then all elements have been
|
||||
-- processed. Only then indicate finish.
|
||||
function TickMapper:process()
|
||||
function KeyJobMapper:process()
|
||||
-- Don't wonder why we shield this, but this way we also correctly handle
|
||||
-- the border case of an empty list of elements to map without crashing.
|
||||
if self.idx <= self.len then
|
||||
|
@ -115,27 +123,35 @@ function TickMapper:process()
|
|||
return true
|
||||
end
|
||||
|
||||
-- Convenience function mainly for TDD: adds a set of functions to be called
|
||||
-- one after another in a ticked fashion; this allows testing KeyJobMapper
|
||||
-- objects.
|
||||
function mb.send_mapped(afterms, func, ...)
|
||||
mb.keys:add(
|
||||
KeyJobMapper:new(func, ...),
|
||||
afterms)
|
||||
end
|
||||
|
||||
-- Adds a set of modifiers to be either pressed or released to the queue of
|
||||
-- tick'ed key operations, so in each tick only one modifier will be pressed
|
||||
-- or released. The state parameter should be either keybow.KEY_DOWN or
|
||||
-- keybow.KEY_UP. The final variable args is/are the modifier(s) to be pressed
|
||||
-- or released.
|
||||
function mb.addmodifiers(after, state, ...)
|
||||
mb.addkeyticker(
|
||||
TickMapper:new(
|
||||
function mb.send_modifiers(afterms, state, ...)
|
||||
mb.keys:add(
|
||||
KeyJobMapper:new(
|
||||
function(mod) keybow.set_modifier(mod, state) end,
|
||||
...
|
||||
),
|
||||
after)
|
||||
...),
|
||||
afterms)
|
||||
end
|
||||
|
||||
-- Adds a sequence of key presses to the queue of tick'ed key operations,
|
||||
-- optionally enclosed by modifier presses and releases.
|
||||
function mb.addkeys(after, keys, ...)
|
||||
function mb.send_keys(after, keys, ...)
|
||||
local modsno = #{...}
|
||||
-- First queue ticked modifier press(es) if necessary.
|
||||
if modsno > 0 then
|
||||
mb.addmodifiers(after, keybow.KEY_DOWN, ...)
|
||||
mb.send_modifiers(after, keybow.KEY_DOWN, ...)
|
||||
after = 0
|
||||
end
|
||||
-- For convenience, explode a single keys string parameter into its
|
||||
|
@ -150,62 +166,15 @@ function mb.addkeys(after, keys, ...)
|
|||
-- Queue the keys to tap in a sequence of ticks. Please note that we
|
||||
-- expect things to be already broken up at this point, as the tick mapper
|
||||
-- will dutyfully tap each element on each tick.
|
||||
mb.addkeyticker(
|
||||
TickMapper:new(
|
||||
{
|
||||
mb.keys:add(
|
||||
KeyJobMapper:new({
|
||||
function(key) keybow.set_key(key, true) end,
|
||||
function(key) keybow.set_key(key, false) end
|
||||
},
|
||||
table.unpack(keys)
|
||||
),
|
||||
table.unpack(keys)),
|
||||
after)
|
||||
-- And finally queue to release the modifier keys if necessary.
|
||||
if modsno > 0 then
|
||||
mb.addmodifiers(0, keybow.KEY_UP, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- All key tickers are handled in a (singly linked) list, as only the first
|
||||
-- key ticker can be active and getting processed. Only after the first one
|
||||
-- has finished, it is removed, and the next (now new) key ticker gets
|
||||
-- processed. And so on...
|
||||
local firstkey = nil
|
||||
local lastkey = nil
|
||||
-- Key tickers can be initially delayed as necessary.
|
||||
local keyafter = 0
|
||||
|
||||
-- Adds another asynchronous USB HID key operation at the end of the key
|
||||
-- queue, waiting to be processed piecemeal-wise tick by tick.
|
||||
function mb.addkeyticker(keyop, afterms)
|
||||
keyop.afterms = afterms or 0
|
||||
if lastkey then
|
||||
lastkey.next = keyop
|
||||
end
|
||||
keyop.next = nil
|
||||
lastkey = keyop
|
||||
if not firstkey then
|
||||
-- Queue was empty before, so we need to kick it off...
|
||||
firstkey = keyop
|
||||
keyafter = mb.now + keyop.afterms
|
||||
end
|
||||
end
|
||||
|
||||
-- Key tick(ing) handler responsible to work on the current key operation as
|
||||
-- well as on the asynchronous key operations queue tick by tick. Since key
|
||||
-- operations are always in sequence as added, there is no point in using a
|
||||
-- (min) priority queue here. Instead, we roll our own very basic and
|
||||
-- low-profile single-linked list.
|
||||
function mb.tickkey(t)
|
||||
-- something in the queue (still) to be processed?
|
||||
if firstkey and t >= keyafter then
|
||||
if not firstkey:process() then
|
||||
-- as the current key operation has finished, so prepare the next
|
||||
-- key operation for the next round, optionally delaying it the
|
||||
-- span requested.
|
||||
firstkey = firstkey.next
|
||||
if firstkey then
|
||||
keyafter = mb.now + firstkey.afterms
|
||||
end
|
||||
end
|
||||
mb.send_modifiers(0, keybow.KEY_UP, ...)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,17 +24,16 @@ SOFTWARE.
|
|||
]]--
|
||||
|
||||
local pq = {}
|
||||
|
||||
pq.__index = pq
|
||||
|
||||
-- Creates a new priority queue.
|
||||
--
|
||||
-- luacheck: ignore 212/self
|
||||
function pq:new()
|
||||
local slf = {}
|
||||
setmetatable(slf, pq)
|
||||
slf.heap = {}
|
||||
slf.size = 0
|
||||
return slf
|
||||
return setmetatable({
|
||||
heap={},
|
||||
size=0
|
||||
}, pq)
|
||||
end
|
||||
|
||||
-- Returns the foremost (minimum) element (priority, value) from the priority
|
||||
|
@ -130,4 +129,4 @@ function pq:delete(priority, value)
|
|||
return priority, v
|
||||
end
|
||||
|
||||
return pq
|
||||
return pq -- module
|
||||
|
|
|
@ -29,17 +29,18 @@ SOFTWARE.
|
|||
-- have gone off by now, then call their timer user functions. Keybow's tick
|
||||
-- ms counter has its epoch set when the Keybow Lua scripting started (so it's
|
||||
-- not a *nix timestamp or such). In addition to timers, we also handle queued
|
||||
-- ticking objects here: these are always processed in sequence, and each such
|
||||
-- ticking object may consume multiple ticks until it has finished its job.
|
||||
function tick(t)
|
||||
mb.now = t
|
||||
-- ticking keys jobs here: these are always processed in sequence, and each
|
||||
-- such ticking key job may consume multiple ticks until it has finished its
|
||||
-- job.
|
||||
function tick(now)
|
||||
mb.now = now
|
||||
while true do
|
||||
local next, tim = mb.timers:peek()
|
||||
if next == nil or t < next then
|
||||
if next == nil or now < next then
|
||||
break
|
||||
end
|
||||
mb.timers:remove()
|
||||
tim:trigger()
|
||||
tim:trigger(now)
|
||||
end
|
||||
mb.tickkey(t)
|
||||
mb.keys:process(now)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
-- Multibow internal "module" implementing a "ticking" (key) queue.
|
||||
|
||||
--[[
|
||||
Copyright 2019 Harald Albrecht
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]--
|
||||
|
||||
local tq = {}
|
||||
tq.__index = tq
|
||||
|
||||
-- Creates a new ticking queue. All elements are stored in a singly linked
|
||||
-- list, with only the first (foremost) list element being active and
|
||||
-- processed when ticking along. Only after the first element has finished its
|
||||
-- processing it gets removed and the next element moves into first position,
|
||||
-- getting processed next.
|
||||
--
|
||||
-- luacheck: ignore 212/self
|
||||
function tq:new()
|
||||
return setmetatable({
|
||||
head=nil,
|
||||
tail=nil,
|
||||
at=nil,
|
||||
now=0
|
||||
}, tq)
|
||||
end
|
||||
|
||||
-- Adds another element to this ticking queue, waiting to be processed when
|
||||
-- its turn finally has come. Additionally, start of processing for this
|
||||
-- element can be delayed if necessary by the given timespan.
|
||||
function tq:add(element, afterms)
|
||||
element.afterms = afterms or 0
|
||||
-- Add the new element to the tail of this list, and move the tail
|
||||
-- accordingly to point to our new trailing element.
|
||||
if self.tail then
|
||||
self.tail.next = element
|
||||
end
|
||||
element.next = nil
|
||||
self.tail = element
|
||||
-- If queue has been empty, then our new element becomes the head of the
|
||||
-- list.
|
||||
if not self.head then
|
||||
self.head = element
|
||||
self.at = self.now + self.head.afterms
|
||||
end
|
||||
end
|
||||
|
||||
-- Processes the head element of this ticking queue for another tick. If the
|
||||
-- head element then signals that it has finished, then the next element in
|
||||
-- queue will be processed in the next turn.
|
||||
function tq:process(now)
|
||||
self.now = now
|
||||
if self.head == nil then return end
|
||||
-- Did we already passed the time where the head element in this
|
||||
-- queue is supposed to become active?
|
||||
if now < self.at then return end
|
||||
if self.head:process(now) then return end
|
||||
-- The foremost element finished its processing, so let's move on to the
|
||||
-- next element in our list ... if there is any. It will start its
|
||||
-- processing with the next tick; we're just setting up things here for
|
||||
-- this next round.
|
||||
self.head = self.head.next
|
||||
if self.head then
|
||||
self.at = self.now + self.head.afterms
|
||||
end
|
||||
end
|
||||
|
||||
return tq -- module
|
|
@ -32,6 +32,7 @@ require "keybow"
|
|||
mb.path = (...):match("^(.-)[^%/]+$")
|
||||
|
||||
mb.pq = require(mb.path .. "mb/prioqueue")
|
||||
mb.tq = require(mb.path .. "mb/tickqueue")
|
||||
require(mb.path .. "mb/ticker")
|
||||
require(mb.path .. "mb/timer")
|
||||
require(mb.path .. "mb/morekeys")
|
||||
|
|
|
@ -74,12 +74,9 @@ describe("asynchronous keys", function()
|
|||
|
||||
it("map a function on a ticking element sequence", function()
|
||||
local s = stub.new()
|
||||
local tm1 = mb.TickMapper:new(s, 1, 2, 3)
|
||||
local tm1 = mb.send_mapped(20, s, 1, 2, 3)
|
||||
local t = stub.new()
|
||||
local tm2 = mb.TickMapper:new(t, 42)
|
||||
|
||||
mb.addkeyticker(tm1, 20)
|
||||
mb.addkeyticker(tm2, 100)
|
||||
local tm2 = mb.send_mapped(100, t, 42)
|
||||
|
||||
-- "empty tick", as the tick mapper is yet delayed...
|
||||
tt.ticktock(10)
|
||||
|
@ -105,15 +102,14 @@ describe("asynchronous keys", function()
|
|||
|
||||
it("map two functions on ticking sequence", function()
|
||||
local s = stub.new()
|
||||
local tm = mb.TickMapper:new({s, s}, 1, 2, 3)
|
||||
mb.addkeyticker(tm)
|
||||
local tm = mb.send_mapped(0, {s, s}, 1, 2, 3)
|
||||
tt.ticktock(100)
|
||||
assert.stub(s).was.called(2*3)
|
||||
end)
|
||||
|
||||
it("tick modifiers", function()
|
||||
local s = spy.on(keybow, "set_modifier")
|
||||
mb.addmodifiers(0, keybow.KEY_DOWN, keybow.LEFT_CTRL, keybow.LEFT_SHIFT)
|
||||
mb.send_modifiers(0, keybow.KEY_DOWN, keybow.LEFT_CTRL, keybow.LEFT_SHIFT)
|
||||
|
||||
tt.ticktock(50)
|
||||
assert.spy(s).was.called(2)
|
||||
|
@ -124,7 +120,7 @@ describe("asynchronous keys", function()
|
|||
it("ticks keys in a string", function()
|
||||
local sm = spy.on(keybow, "set_modifier")
|
||||
local sk = spy.on(keybow, "set_key")
|
||||
mb.addkeys(0, "abc", keybow.LEFT_CTRL, keybow.LEFT_SHIFT)
|
||||
mb.send_keys(0, "abc", keybow.LEFT_CTRL, keybow.LEFT_SHIFT)
|
||||
|
||||
tt.ticktock(100)
|
||||
-- note that the modifiers were pressed AND released by now...
|
||||
|
@ -146,7 +142,7 @@ describe("asynchronous keys", function()
|
|||
|
||||
it("ticks keys in a table", function()
|
||||
local sk = spy.on(keybow, "set_key")
|
||||
mb.addkeys(0, {"a", keybow.ENTER, "c"})
|
||||
mb.send_keys(0, {"a", keybow.ENTER, "c"})
|
||||
|
||||
tt.ticktock(100)
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
--[[
|
||||
Copyright 2019 Harald Albrecht
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]--
|
||||
|
||||
require "mocked-keybow"
|
||||
local mb = require("snippets/multibow")
|
||||
local tq = require("snippets/mb/tickqueue")
|
||||
|
||||
local now = 0
|
||||
local process = function(q, ms)
|
||||
local delta = 10
|
||||
local start = now
|
||||
for passed = delta,ms,delta do
|
||||
now = start + passed
|
||||
q:process(now)
|
||||
end
|
||||
end
|
||||
|
||||
local El = {}
|
||||
El.__index = El
|
||||
|
||||
-- luacheck: ignore 212/self
|
||||
function El:new(stub, times)
|
||||
times = times or 0
|
||||
return setmetatable({
|
||||
stub=stub,
|
||||
times=times
|
||||
}, El)
|
||||
end
|
||||
|
||||
function El:process(t)
|
||||
if self.stub then self.stub() end
|
||||
self.times = self.times - 1
|
||||
return self.times > 0
|
||||
end
|
||||
|
||||
describe("ticking queue", function()
|
||||
|
||||
it("processes a single entry with initial delay the expected number of times", function()
|
||||
local q = tq:new()
|
||||
local s = stub.new()
|
||||
q:add(El:new(s, 2), 20)
|
||||
|
||||
process(q, 10)
|
||||
assert.stub(s).was.Not.called()
|
||||
|
||||
process(q, 50)
|
||||
assert.stub(s).was.called(2)
|
||||
end)
|
||||
|
||||
it("processes multiple entries with intermediate delay", function()
|
||||
local q = tq:new()
|
||||
local s = stub.new()
|
||||
q:add(El:new())
|
||||
q:add(El:new(s), 20)
|
||||
|
||||
process(q, 10)
|
||||
assert.stub(s).was.Not.called()
|
||||
process(q, 10)
|
||||
assert.stub(s).was.Not.called()
|
||||
process(q, 50)
|
||||
assert.stub(s).was.called()
|
||||
end)
|
||||
|
||||
end)
|
Loading…
Reference in New Issue