"""
Key press interaction
---------------------
This module provides a :class:`KeyPress` observable to
enable server side Python functions to subscribe to
key events.
In addtion there is also a middleware function, :meth:`navigate`,
that maps key actions to navigation actions, e.g.
`next_valid_time()` etc.
.. autoclass:: KeyPress
:members:
.. autofunction:: navigate
.. autofunction:: press
"""
import bokeh.models
import forest.db.control
from forest.observe import Observable
from forest.export import export
__all__ = []
KEY_PRESS = "KEY_PRESS"
[docs]def press(code):
"""Key press action creator
:param code: str representing browser event.code
:returns: dict representing action
"""
return {"kind": KEY_PRESS, "payload": {"code": code}}
[docs]@export
class KeyPress(Observable):
"""Key press server-side observable
To add this to an existing `document`, add the
hidden_button to the document and update `templates/index.html`
to click the button on page load
>>> key_press = KeyPress()
>>> document.add_root(key_press.hidden_button)
To observe the stream of actions generated by user key press
events simply register a function via the `subscribe` method
>>> key_press.subscribe(print)
.. note:: KeyPress.hidden_button must be added to the
document to allow JS hack to initialise callbacks
"""
def __init__(self):
self.source = bokeh.models.ColumnDataSource({"keys": []})
self.source.on_change("data", self._on_change)
custom_js = bokeh.models.CustomJS(
args=dict(source=self.source),
code="""
if (typeof window.keyPressOn === 'undefined') {
let interval = 150 // Key hold is about 30ms, double-click is about 200ms
let throttle = function(callback, miliseconds) {
var waiting = false
return function() {
if (!waiting) {
callback.apply(null, arguments)
waiting = true
setTimeout(function() { waiting = false } , miliseconds)
}
}
}
let onkeydown = function(e) {
let keys = source.data['keys']
keys.push(e.code)
source.data = {
'keys': keys
}
source.change.emit()
}
document.onkeydown = throttle(onkeydown, interval)
// Global to prevent multiple onkeydown callbacks
window.keyPressOn = true
}
""",
)
self.hidden_button = bokeh.models.Button(
css_classes=["keypress-hidden-btn"]
)
self.hidden_button.js_on_click(custom_js)
super().__init__()
def _on_change(self, attr, old, new):
code = self.source.data["keys"][-1]
self.notify(press(code))
[docs]def navigate(store, action):
"""Middleware to interpret key press events
It implements the following mapping of
actions to allow other parts of the
system to interpret the action
========== =====================
From To
========== =====================
ArrowRight Next valid time
ArrowLeft Previous valid time
ArrowUp Next initial time
ArrowDown Previous initial time
========== =====================
.. note:: Non key press actions are passed on unaltered
:param store: :class:`forest.redux.Store` instance
:param action: incoming action
"""
kind = action["kind"]
if kind != KEY_PRESS:
yield action
return
code = action["payload"]["code"].lower()
if code == "arrowright":
yield forest.db.control.next_valid_time()
elif code == "arrowleft":
yield forest.db.control.previous_valid_time()
elif code == "arrowup":
yield forest.db.control.next_initial_time()
elif code == "arrowdown":
yield forest.db.control.previous_initial_time()