Question

I have a media application that allows the user to Play, Pause, step frame by frame, FastForward, etc. I am attempting to use Rx to get the following behavior for stepping and FastForward.

  1. If the user clicks the right arrow less than 2 times/300ms I want to frame step.
  2. If the user holds down the right arrow I want to fast forward until the right arrow button is released.

I think I have the fast forward part correct, but am not sure how to craft this to get the step functionality. I am also open to "better" ways to do the fast forward.

//start FF when we get 2 key presses within the threshold time
Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
            .Where(k => k.EventArgs.Key == Key.Right)
            .Timestamp()
            .Buffer(2)
            .Where(x => (x[1].Timestamp - x[0].Timestamp).Milliseconds < 300)
            .Subscribe(x =>
                { 
                    Console.WriteLine("FastForward GO");
                    _viewModel.FastForward();
                });

//stop ff on the key up
Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp")
            .Where(k => k.EventArgs.Key == Key.Right)
            .Subscribe(x => { 
                Console.WriteLine("FastForward STOP");
                _viewModel.StopFastForward();
            });
Was it helpful?

Solution

Solution

var up   = Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp")
                     .Where(x => x.EventArgs.KeyCode == Keys.Right);

// Take, Concat, and Repeat work together to prevent repeated KeyDown events.
var down = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
                     .Where(x => x.EventArgs.KeyCode == Keys.Right)
                     .Take(1)
                     .Concat(up.Take(1).IgnoreElements())
                     .Repeat();

var t = TimeSpan.FromMilliseconds(300);

var tap = down.SelectMany(x =>
    Observable.Amb(
        Observable.Empty<EventPattern<KeyEventArgs>>().Delay(t),
        up.Take(1)
    ))
    .Publish()
    .RefCount();

var longPress = down.SelectMany(x =>
    Observable.Return(x).Delay(t).TakeUntil(tap)
    );

There's multiple ways to do this, but this works at getting the "longPress" you need, as well as the "tap". You can use longPress to start fast-fowarding, up to stop fast-forwarding, and tap for frame-stepping.

tap yields when a key has been pressed and released within a timespan of t.

longPress yields when the key has been held down for longer than t.

up yields when the key has been released.

Explaination

A problem exists because the KeyDown event is repeated multiple times for each physical press of a key.

var down = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown");

In this case, we need a way to filter out the repeated KeyDown events. We can do that by using a combination of operators. First, we'll use Take(1). This will yield the first event and ignore the rest.

var first = down.Take(1);

If we only needed to get a single actual key press, this would be great. But, alas, we need to get all of the actual key presses. We need to wait for the KeyUp event to occur and start the whole thing over. To do this, we can use a combination of Concat and Repeat. For the concat observable, we need to make sure we're only taking 1 up event, and that we're ignore the elements of the up observable, otherwise we end up feeding all of the up events into our new observable.

var down = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
                     .Take(1)
                     .Contact(up.Take(1).IgnoreElements())
                     .Repeat();

This gives us the actual down events, without the in-between repeated events.

Now that we've cleaned up our source observables, we can start composing them in useful ways. What we're looking for is a "tap" event and a "long press" event. To get the tap event, we need to take a single actual down event, and make sure that it isn't held down too long... One way to do this is using the Amb operator.

var tap = down.SelectMany(x =>
    Observable.Amb(
        Observable.Empty<EventPattern<KeyEventArgs>>().Delay(t),
        up.Take(1)
    ))

The Amb operator stands for "ambiguous". It takes a number of Observables, listens to each one, and waits for them to yield something. Once one of them yields an event, the Amb operator ignores (disposes the subscriptions of) the other observables.

In our case, for each down event that occurs, we use the SelectMany and Amb operator to check to see which yields or completes first... a single up event, or an empty observable that completes after a timespan of t. If the up event occurs before the the empty observable completes, its a tap. Otherwise, we ignore it.

Now we can do a similar thing for "long press", except this time we want to delay the KeyDown event until we know that it's not a tap. We can use a combination of the Delay and TakeUntil operators to do this. Delay makes sure the long press doesn't occur before a tap can be registered, and TakeUntil makes sure we ignore the KeyPress if it turned out to be a tap after all.

var longPress = down.SelectMany(x =>
    Observable.Return(x).Delay(t).TakeUntil(tap)
    );

Generalized Solution

This version works for any key.

var up = Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp");
var downWithRepeats = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown");

var down =
    Observable.Merge(
        up.Select(x => new { e = x, type = "KeyUp" }),
        downWithRepeats.Select(x => new { e = x, type = "KeyDown" })
    )
    .GroupByUntil(
        x => x.e.EventArgs.KeyCode,
        g => g.Where(y => y.type == "KeyUp")
    )
    .SelectMany(x => x.FirstAsync())
    .Select(x => x.e);

var t = TimeSpan.FromMilliseconds(300);

var tap = down.SelectMany(x =>
    Observable.Amb(
        Observable.Empty<EventPattern<KeyEventArgs>>().Delay(t),
        up.Where(y => y.EventArgs.KeyCode == x.EventArgs.KeyCode).Take(1)
    ))
    .Publish()
    .RefCount();

var longPress = down.SelectMany(x =>
    Observable.Return(x).Delay(t).TakeUntil(
        tap.Where(y => y.EventArgs.KeyCode == x.EventArgs.KeyCode)
        )
    );

Usage

Observable.Merge(
    down     .Select(x => string.Format("{0} - press",      x.EventArgs.KeyCode)),
    tap      .Select(x => string.Format("{0} - tap",        x.EventArgs.KeyCode)),
    longPress.Select(x => string.Format("{0} - longPress",  x.EventArgs.KeyCode)),
    up       .Select(x => string.Format("{0} - up",         x.EventArgs.KeyCode))
)
.ObserveOn(SynchronizationContext.Current)
.Select(x => string.Format("{0} - {1}", x, DateTime.Now.ToLongTimeString()))
.Subscribe(text => this.myTextBox.Text = text);

OTHER TIPS

Here's an alternative to Chris's that gives three streams, one for clicks, one for begin holds and one for end holds. Makes use of TimeInterval for recording duration between events.

WinForms Version

We can capture KeyDown eliminating repeats by using GroupByUntil to group KeyDown until a KeyUp occurs:

TimeSpan limit = TimeSpan.FromMilliseconds(300);
var key = Keys.Right;

var keyUp = Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp")
                      .Where(i => i.EventArgs.KeyCode == key)
                      .Select(_ => true);

var keyDown = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
                        .Where(i => i.EventArgs.KeyCode == key)
                        .GroupByUntil(k => 0, _ => keyUp)
                        .SelectMany(x => x.FirstAsync());

var keyDownDuration = keyDown.Select(k => keyUp.TimeInterval()).Switch();

var clicks = keyDownDuration.Where(i => i.Interval < limit);

var beginHold = keyDown.Select(k => Observable.Timer(limit).TakeUntil(keyUp))
                       .Switch();

var endHold = keyDownDuration.Where(i => i.Interval > limit);

/* usage */
clicks.Subscribe(_ => Console.WriteLine("Click"));
beginHold.Subscribe(_ => Console.WriteLine("Hold Begin"));
endHold.Subscribe(_ => Console.WriteLine("Hold End"));

WPF Version

Originally, I had mistakenly assumed the WPF flavour of KevEventArgs as IsRepeat is not available in the WinForms version - which means this won't work for OP, but I'll leave it in as it may be of use for others.

TimeSpan limit = TimeSpan.FromMilliseconds(300);
var key = Key.Right;

var keyUp = Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp")
                        .Where(i => i.EventArgs.Key == key);

var keyDown = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
                        .Where(i => i.EventArgs.IsRepeat == false
                                && i.EventArgs.Key == key);

var keyDownDuration = keyDown.Select(k => keyUp.TimeInterval()).Switch();

var clicks = keyDownDuration.Where(i => i.Interval < limit);

var beginHold = keyDown.Select(k => Observable.Timer(limit).TakeUntil(keyUp))
                        .Switch();

var endHold = keyDownDuration.Where(i => i.Interval > limit);

/* usage */
clicks.Subscribe(_ => Console.WriteLine("Click"));
beginHold.Subscribe(_ => Console.WriteLine("Hold Begin"));
endHold.Subscribe(_ => Console.WriteLine("Hold End"));

To Test The Code

Include nuget package rx-main and paste the WinForms/WPF or code snippets as appropriate to the end of the Form contructor. Then run the code and press the right arrow key whilst observing the VS Output window to see the result.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top