4

I am wondering what is the canonical approach to solve the following problem in Rx: Say I have two observables, mouse_down and mouse_up, whose elements represent mouse button presses. In a very simplistic scenario, if I wanted to detect a long press, I could do it the following way (in this case using RxPy, but conceptually the same in any Rx implementation):

mouse_long_press = mouse_down.delay(1000).take_until(mouse_up).repeat()

However, problems arise when we need to hoist some information from the mouse_down observable to the mouse_up observable. For example, consider if the elements of the observable stored information about which mouse button was pressed. Obviously, we would only want to pair mouse_down with mouse_up of the corresponding button. One solution that I came up with is this:

mouse_long_press = mouse_down.select_many(lambda x:
    rx.Observable.just(x).delay(1000)\
        .take_until(mouse_up.where(lambda y: x.button == y.button))
)

If there is a more straight forward solution, I would love to hear it - but as far as I can tell this works. However, things get more complicated, if we also want to detect how far the mouse has moved between mouse_down and mouse_up. For this we need to introduce a new observable mouse_move, which carries information about the mouse position.

mouse_long_press = mouse_down.select_many(lambda x:
    mouse_move.select(lambda z: distance(x, z) > 100).delay(1000)\
        .take_until(mouse_up.where(lambda y: x.button == y.button))
)

However, this is pretty much where I get stuck. Whenever a button is held down longer than 1 second, I get a bunch of boolean values. However, I only want to detect a long press when all of them are false, which sounds like the perfect case for the all operator. It feels like there's only a small step missing, but I haven't been able to figure out how to make it work so far. Perhaps I am also doing things in a backwards way. Looking forward to any suggestions.

kloffy
  • 2,928
  • 2
  • 25
  • 34
  • Doesn't `mouse_up` event contain data about cursor position? – Eryk Napierała Apr 22 '15 at 19:55
  • @ErykNapierała Yes, however it can take an arbitrarily long time for the `mouse_up` to occur, but the long press should be triggered a fixed amount of time after the `mouse_down`. – kloffy Apr 22 '15 at 23:02

2 Answers2

1

Ok, I guess I found a possible answer. RxPy has a take_with_time operator, which works for this purpose. Not really as straight-forward as I was hoping for (not sure if the take_with_time is avaliable in other Rx implementations).

mouse_long_press = mouse_down.select_many(lambda x:
    mouse_moves.take_with_time(1000).all(lambda z: distance(x, z) < 100)\
        .take_until(mouse_up.where(lambda y: x.button == y.button))
)

I will leave the question open for now in case somebody has a better suggestion.

kloffy
  • 2,928
  • 2
  • 25
  • 34
  • It appears that RxPy is the [only implementation with takeWithTime](http://reactivex.io/documentation/operators/take.html), and the ReactiveX site doesn't have any docs on it yet. – Adam S Apr 22 '15 at 14:08
  • 1
    RxJava has an overload of `take` which works with time [take(long, TimeUnit)](http://reactivex.io/RxJava/javadoc/rx/Observable.html#take%28long,%20java.util.concurrent.TimeUnit%29) – Samuel Gruetter Apr 23 '15 at 08:29
  • @SamuelGruetter Interesting, thanks! I have looked for alternative ways of getting this behavior using more "standard" Rx operators, but so far this seems to be the most straight forward solution. – kloffy Apr 23 '15 at 12:30
0

I'd approach the problem differently, by creating a stream of mouse presses with length information, and filtering that for presses longer than 1s.


First let's assume that you only have one mouse button. Merge the mouse_up and mouse_down streams and assign time intervals between them with the time_interval() operator. You will get a stream of intervals since previous event, along with the event itself. Assuming your mouse-ups and mouse-downs alternate, this means your events now are:

(down + time since last up), (up + time since last down), (down + time since last up) ...

Now, simply filter for x.value.type == "up" and x.interval > datetime.timedelta(seconds=1)

(You can also validate this with pairwise(), which always gives you the current + previous event, so you can check that the previous one is down and the current one is up.)


Second, add the mouse movement information, using the window() operator.

(This part is untested, I'm going off the docs of how it's supposed to behave, but the docs aren't very clear. So YMMV. )

The idea is that you can collect sequences of events from an observable, separated into groups based on another observable. From the docs: window(window_openings) The window_openings observable is going to be the merged up/down stream, or the interval stream, whichever is more convenient. Then you can flat_map() (or select_many, which is the same thing) the result and work out the distance in whichever way you like.

Again, you should end up with a stream of distances between up/down events. Then you can zip() this stream with the interval stream, at which point you can filter for up events and get both time and distance until the previous down.


Third, what if you are getting events for multiple mouse buttons?

Simply use group_by() operator to split into per-button streams and proceed as above.

Full code below:

Event = collections.NamedTuple("Event", "event interval distance")

def sum_distance(move_stream):
    # put your distance calculation here; something like:
    return move_stream.pairwise().reduce(lambda acc, (a, b): acc + distance(a, b), 0)

def mouse_press(updown_stream):
    # shared stream for less duplication
    shared = updown_stream.share()
    intervals = shared.time_interval()  # element is: (interval=timedelta, value=original event)
    distances = mouse_move.window(shared).flat_map(sum_distance)
    zipped = intervals.zip(distances, lambda i, d: \
        Event(i.value, i.interval, d) )

mouse_long_press = (
    # merge the mouse streams
    rx.Observable.merge(mouse_up, mouse_down)
    # separate into streams for each button
    .group_by(lambda x: x.button)
    # create per-button event streams per above and merge results back
    .flat_map(mouse_press)
    # filter by event type and length
    .filter(lambda ev: ev.event.type == "up" and ev.interval >= datetime.timedelta(seconds=1)
)
matejcik
  • 1,912
  • 16
  • 26
  • 1
    ...and of course, after finishing the answer, I finally realized that you want the event to fire *after one second* regardless of when the mouse_up comes. Well. – matejcik Aug 15 '18 at 08:59