adds repeating tick jobs

This commit is contained in:
Harald Albrecht 2019-02-20 20:18:12 +01:00
parent aa4bf18e08
commit e08f9d916e
5 changed files with 157 additions and 43 deletions

View File

@ -67,6 +67,7 @@ function mb.tap_times(key, times, ...)
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
@ -78,31 +79,37 @@ 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 KeyJobMapper:new(func, ...)
function KeyJobMapper:new(func, ...) -- luacheck: ignore 212/self
-- 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
-- function which in fact is a table (as "busted" does with spies and
-- stubs).
-- stubs). Bottom line: we need much more space to explain than to do our
-- thing. Sigh.
if type(func) == "function" or (type(func) == "table" and #func == 0) then
func = {func}
end
local slf = {
local slf = setmetatable({
func=func, -- the function(s) to call for each element.
fidx=1,
flen=#func,
elements={...}, -- the elements to map onto function calls.
idx=1,
len=#{...}
}
return setmetatable(slf, KeyJobMapper)
}, KeyJobMapper)
slf:reset()
return slf
end
-- Reset a tick mapper so it can be repeated (using a key repeater job).
function KeyJobMapper:reset()
self.fidx = 1
self.idx = 1
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.
-- processed. Only then declare finish by returning -1 as the "done"
-- indication. Related, return 0 if there is still work to be done on the next
-- round, or a positive "afterms" value indicating to call back only later.
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.
@ -117,10 +124,49 @@ function KeyJobMapper:process()
-- Update index to next element and indicate back whether we'll need a
-- further round in the future.
self.idx = self.idx + 1
return self.idx <= self.len
return self.idx <= self.len and 0 or -1
end
-- There's always more to do, since there are more functions waiting to be called.
return true
-- There's always more to do, since there are more functions waiting to be
-- called; and we want to process the next function with the next tick.
return 0
end
-- Repeaters allow repeating tick mappers, but also repeaters; that is, we can
-- repeat repeaters.
local KeyJobRepeater = {}
KeyJobRepeater.__index = KeyJobRepeater
mb.KeyJobRepeater = KeyJobRepeater
-- Creates a new tick repeater that allows to repeat both tick mappers, as
-- well as repeaters.
function KeyJobRepeater:new(keyjob, times, pause) -- luacheck: ignore 212/self
pause = pause or 0
local slf = setmetatable({
keyjob=keyjob,
times=times,
pause=pause
}, KeyJobRepeater)
slf:reset()
return slf
end
-- Rest a repeater, so repeaters can repeat repeaters. (ARGH!!!)
function KeyJobRepeater:reset()
self.round = 1
end
function KeyJobRepeater:process()
local afterms = self.keyjob:process()
if afterms >= 0 then return afterms end
-- Start the next round ... or are we done now? Please note that the next
-- round might be delayed if so required in order to give the applications
-- on the USB host system some time to process the burst of key events.
-- Well, that's not meant with respect to USB and USB host, but instead
-- the applications working on key input.
self.round = self.round + 1
-- Always reset, so repeater cascades work ... greetings to M.C.E.
self.keyjob:reset()
return self.round <= self.times and self.pause or -1
end
-- Convenience function mainly for TDD: adds a set of functions to be called
@ -147,7 +193,7 @@ end
-- Adds a sequence of key presses to the queue of tick'ed key operations,
-- optionally enclosed by modifier presses and releases.
function mb.send_keys(after, keys, ...)
function mb.send_keys_repeatedly(after, times, pause, keys, ...)
local modsno = #{...}
-- First queue ticked modifier press(es) if necessary.
if modsno > 0 then
@ -178,3 +224,7 @@ function mb.send_keys(after, keys, ...)
mb.send_modifiers(0, keybow.KEY_UP, ...)
end
end
function mb.send_keys(after, keys, ...)
mb.send_keys_repeatedly(after, 1, 0, keys, ...)
end

View File

@ -27,9 +27,7 @@ local pq = {}
pq.__index = pq
-- Creates a new priority queue.
--
-- luacheck: ignore 212/self
function pq:new()
function pq:new() -- luacheck: ignore 212/self
return setmetatable({
heap={},
size=0

View File

@ -30,15 +30,18 @@ tq.__index = tq
-- 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)
function tq:new() -- luacheck: ignore 212/self
local slf = setmetatable({}, tq)
slf:clear()
return slf
end
-- Convenience for TDD
function tq:clear()
self.head = nil
self.tail = nil
self.at = nil
self.now = 0
end
-- Adds another element to this ticking queue, waiting to be processed when
@ -63,21 +66,32 @@ 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.
-- queue will be processed in the next turn. Finishing is indicated by return
-- -1 from a the process() method of the head queue element. 0 means: please
-- call me again the next time (erm, tick). And a positive value indicates to
-- call again, but this time after the amount of ms as indicated by the return
-- value.
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?
-- Did we already pass the time where the head element in this queue is
-- supposed to become active or to be processed further? If not, we don't
-- do anything yet.
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
local afterms = self.head:process(now)
if afterms > 0 then
-- More to process, but only after a certain amount of time, as
-- indicated by the return value of the process() method.
self.at = now + afterms
elseif afterms < 0 then
-- The foremost element finally 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
end

View File

@ -72,6 +72,10 @@ describe("asynchronous keys", function()
local tt = require("spec/snippets/ticktock")
before_each(function()
mb.keys:clear()
end)
it("map a function on a ticking element sequence", function()
local s = stub.new()
mb.send_mapped(20, s, 1, 2, 3)
@ -107,6 +111,33 @@ describe("asynchronous keys", function()
assert.stub(s).was.called(2*3)
end)
it("tick repeatedly", function()
local s = stub.new()
local kj = mb.KeyJobMapper:new(s, 42)
mb.keys:add(mb.KeyJobRepeater:new(kj, 2, 20))
tt.ticktock(10)
assert.stub(s).was.called(1)
s:clear()
tt.ticktock(10)
assert.stub(s).was.Not.called()
s:clear()
tt.ticktock(10)
assert.stub(s).was.called(1)
end)
it("#focus tick repeatedly**2", function()
local s = stub.new()
local kj = mb.KeyJobMapper:new(s, 42)
local rkj = mb.KeyJobRepeater:new(kj, 2)
mb.keys:add(mb.KeyJobRepeater:new(rkj, 2))
tt.ticktock(50)
assert.stub(s).was.called(4)
end)
it("tick modifiers", function()
local s = spy.on(keybow, "set_modifier")
mb.send_modifiers(0, keybow.KEY_DOWN, keybow.LEFT_CTRL, keybow.LEFT_SHIFT)
@ -117,7 +148,7 @@ describe("asynchronous keys", function()
assert.spy(s).was.called.With(keybow.LEFT_SHIFT, keybow.KEY_DOWN)
end)
it("ticks keys in a string", function()
it("tick keys in a string", function()
local sm = spy.on(keybow, "set_modifier")
local sk = spy.on(keybow, "set_key")
mb.send_keys(0, "abc", keybow.LEFT_CTRL, keybow.LEFT_SHIFT)
@ -140,7 +171,7 @@ describe("asynchronous keys", function()
assert.spy(sk).was.called.With("c", false)
end)
it("ticks keys in a table", function()
it("tick keys in a table", function()
local sk = spy.on(keybow, "set_key")
mb.send_keys(0, {"a", keybow.ENTER, "c"})

View File

@ -36,12 +36,13 @@ end
local El = {}
El.__index = El
-- luacheck: ignore 212/self
function El:new(stub, times)
function El:new(stub, times, delay) -- luacheck: ignore 212/self
times = times or 0
delay = delay or 0
return setmetatable({
stub=stub,
times=times
times=times,
delay=delay
}, El)
end
@ -49,7 +50,7 @@ end
function El:process(t)
if self.stub then self.stub() end
self.times = self.times - 1
return self.times > 0
return self.times > 0 and self.delay or -1
end
describe("ticking queue", function()
@ -66,6 +67,26 @@ describe("ticking queue", function()
assert.stub(s).was.called(2)
end)
it("processes a single entry with intermediate delays", function()
local q = tq:new()
local s = stub.new()
q:add(El:new(s, 2, 20), 0)
process(q, 10)
assert.stub(s).was.called()
assert.is.Not.Nil(q.head) -- element still in queue
s:clear()
process(q, 10)
assert.stub(s).was.Not.called()
assert.is.Not.Nil(q.head) -- element still in queue
s:clear()
process(q, 20)
assert.stub(s).was.called()
assert.is.Nil(q.head) -- element now gone for good...
end)
it("processes multiple entries with intermediate delay", function()
local q = tq:new()
local s = stub.new()