Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logic to detect short and long (key/button) presses. #76

Merged
merged 2 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions StreamDeckSimHub.Plugin/Actions/ShortAndLongPressHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (C) 2023 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using System.Collections.Concurrent;
using SharpDeck.Events.Received;

namespace StreamDeckSimHub.Plugin.Actions;

/// <summary>
/// Handles the detection of short and long (key/button) presses.
/// <p/>
/// If the time between KeyDown and KeyUp is shorter than "longPressTimeSpan", the callback "OnShortPress" will be called.
/// If the time is larger, the callback "OnLongPress" will be called.
/// </summary>
public class ShortAndLongPressHandler
{
private ConcurrentStack<ActionEventArgs<KeyPayload>> KeyPressStack { get; } = new();
private TimeSpan LongPressTimeSpan { get; }
private Func<ActionEventArgs<KeyPayload>, Task> OnShortPress { get; }
private Func<ActionEventArgs<KeyPayload>, Task> OnLongPress { get; }
private readonly CancellationTokenSource _cancellationTokenSource = new();

public ShortAndLongPressHandler(
Func<ActionEventArgs<KeyPayload>, Task> onShortPress,
Func<ActionEventArgs<KeyPayload>, Task> onLongPress) : this(TimeSpan.FromMilliseconds(500), onShortPress, onLongPress)
{
}

public ShortAndLongPressHandler(
TimeSpan longPressTimeSpan,
Func<ActionEventArgs<KeyPayload>, Task> onShortPress,
Func<ActionEventArgs<KeyPayload>, Task> onLongPress)
{
LongPressTimeSpan = longPressTimeSpan;
OnShortPress = onShortPress;
OnLongPress = onLongPress;
}

public Task KeyDown(ActionEventArgs<KeyPayload> args)
{
KeyPressStack.Push(args);
if (LongPressTimeSpan > TimeSpan.Zero)
{
Task.Run(async () =>
{
try
{
var me = this;
await Task.Delay(LongPressTimeSpan, _cancellationTokenSource.Token);
await me.TryHandlePress(OnLongPress);
}
catch (TaskCanceledException)
{
// That is what we expect if a short press was faster
}
});
}

return Task.CompletedTask;
}

public async Task KeyUp()
{
await TryHandlePress(OnShortPress);
}

private async Task TryHandlePress(Func<ActionEventArgs<KeyPayload>, Task> handler)
{
if (KeyPressStack.TryPop(out var result))
{
_cancellationTokenSource.Cancel();
await handler(result);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (C) 2023 Martin Renner
// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER)

using SharpDeck.Events.Received;
using StreamDeckSimHub.Plugin.Actions;

namespace StreamDeckSimHub.PluginTests.Actions;

public class ShortAndLongPressHandlerTests
{
private readonly TimeSpan _timeSpan = TimeSpan.FromMilliseconds(100);
private readonly TimeSpan _timeSpanShorter = TimeSpan.FromMilliseconds(50);
private readonly TimeSpan _timeSpanLonger = TimeSpan.FromMilliseconds(150);

[Test]
public async Task TestShortPress()
{
var cb = new CallbackHolder();
var handler = new ShortAndLongPressHandler(_timeSpan, cb.OnShortPress, cb.OnLongPress);
await handler.KeyDown(new ActionEventArgs<KeyPayload>());
await Task.Delay(_timeSpanShorter);
await handler.KeyUp();

cb.AssertAndReset(true, false);
}

[Test]
public async Task TestLongPress()
{
var cb = new CallbackHolder();
var handler = new ShortAndLongPressHandler(_timeSpan, cb.OnShortPress, cb.OnLongPress);
await handler.KeyDown(new ActionEventArgs<KeyPayload>());
await Task.Delay(_timeSpanLonger);
await handler.KeyUp();

cb.AssertAndReset(false, true);
}

[Test]
public async Task TestShortShort()
{
// Short+Short must not trigger Short+Long! See https://github.com/GeekyEggo/SharpDeck/issues/18 for details.

var cb = new CallbackHolder();
var handler = new ShortAndLongPressHandler(_timeSpan, cb.OnShortPress, cb.OnLongPress);

await handler.KeyDown(new ActionEventArgs<KeyPayload>());
await Task.Delay(_timeSpanShorter);
await handler.KeyUp();

cb.AssertAndReset(true, false);

await handler.KeyDown(new ActionEventArgs<KeyPayload>());
await Task.Delay(_timeSpanShorter);
await handler.KeyUp();

cb.AssertAndReset(true, false);
}

private class CallbackHolder
{
private bool _shortWasCalled;
private bool _longWasCalled;

internal void AssertAndReset(bool expectedShort, bool expectedLong)
{
Assert.Multiple(() =>
{
Assert.That(_shortWasCalled, Is.EqualTo(expectedShort), "Short {0} be called", expectedShort ? "must" : "must not");
Assert.That(_longWasCalled, Is.EqualTo(expectedLong), "Long {0} be called", expectedLong ? "must" : "must not");
});
_shortWasCalled = false;
_longWasCalled = false;
}

internal Task OnShortPress(ActionEventArgs<KeyPayload> args)
{
_shortWasCalled = true;
return Task.CompletedTask;
}

internal Task OnLongPress(ActionEventArgs<KeyPayload> args)
{
_longWasCalled = true;
return Task.CompletedTask;
}
}
}