Repository navigation
Add keyboard input from the focused visualizer window #8158
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
8fc077b
Add a keyboard input API to visualizers
rwiltz d4fb5e8
Report keys from the Kit and Newton visualizer windows
rwiltz 14428a5
Bind keyboard input to visualizer window lifetime
ooctipus 5119499
Consolidate visualizer keyboard ownership
ooctipus 840f67d
Keep the visualizers starting when keyboard input fails
rwiltz 291b16f
Merge main and resolve retired visualizer test helper conflict
ooctipus d80d8c2
Handle unavailable Kit keyboard imports during initialization
ooctipus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
source/isaaclab/changelog.d/minimal-window-keyboard.changed.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| * Simplified keyboard sources to listener registration and event forwarding; native window hooks | ||
| were initialized and closed by visualizers instead of changing with listener counts. | ||
| * Centralized the optional keyboard source and teardown in the base visualizer and removed | ||
| redundant subscription state. |
8 changes: 8 additions & 0 deletions
8
source/isaaclab/changelog.d/rwiltz-visualizer-key-events.added.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| * Added :attr:`~isaaclab.visualizers.BaseVisualizer.key_event_source` and | ||
| :class:`~isaaclab.visualizers.KeyEventSource`, keyboard input from a focused visualizer window. | ||
| :meth:`~isaaclab.visualizers.KeyEventSource.add_key_listener` returns a | ||
| :class:`~isaaclab.visualizers.KeyboardSubscription` that receives each key press and release as a | ||
| W3C ``KeyboardEvent.code`` string, and a focus loss when the window stops delivering keys. | ||
| Visualizers without a local window that can report keys return ``None``, the default. | ||
| * Added :meth:`~isaaclab.visualizers.BaseVisualizer.set_camera_keys_enabled`, which turns a | ||
| visualizer's keyboard camera movement off while a keyboard consumer uses those keys. |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
source/isaaclab/isaaclab/visualizers/key_event_source.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| # Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). | ||
| # All rights reserved. | ||
| # | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| """Keyboard input from a focused visualizer window, shared by the visualizer backends.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from collections.abc import Callable | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| KeyCallback = Callable[[str, bool], None] | ||
| """Called with a W3C ``KeyboardEvent.code`` string (e.g. ``"KeyW"``) and ``True`` on press, ``False`` on release.""" | ||
|
|
||
| FocusLostCallback = Callable[[], None] | ||
| """Called when the window stops delivering keys: treat every held key as released.""" | ||
|
|
||
|
|
||
| class KeyboardSubscription: | ||
| """A listener subscribed with :meth:`KeyEventSource.add_key_listener`. | ||
|
|
||
| Close it (or use it as a context manager) to unsubscribe. Closing is idempotent and delivers no | ||
| final focus loss: the listener chose to leave. | ||
| """ | ||
|
|
||
| def __init__(self, source: KeyEventSource, on_key: KeyCallback, on_focus_lost: FocusLostCallback) -> None: | ||
| self._source: KeyEventSource | None = source | ||
| self._on_key = on_key | ||
| self._on_focus_lost = on_focus_lost | ||
|
|
||
| @property | ||
| def closed(self) -> bool: | ||
| """Whether the subscription no longer receives callbacks.""" | ||
| return self._source is None | ||
|
|
||
| def close(self) -> None: | ||
| """Unsubscribe.""" | ||
| source, self._source = self._source, None | ||
| if source is not None and not source.closed: | ||
| source._subscriptions.remove(self) | ||
|
|
||
| def __enter__(self) -> KeyboardSubscription: | ||
| return self | ||
|
|
||
| def __exit__(self, *exc) -> None: | ||
| self.close() | ||
|
|
||
|
|
||
| class KeyEventSource: | ||
| """Keyboard input from a focused visualizer window. | ||
|
|
||
| Obtained from :attr:`~isaaclab.visualizers.BaseVisualizer.key_event_source`. For every backend: | ||
|
|
||
| - Subscribing, closing and every callback happen on the thread that steps the visualizer. | ||
| - ``on_key`` receives the window's presses and releases as W3C ``KeyboardEvent.code`` strings. | ||
| Key repeat can arrive as further presses, and a release can arrive without its press (e.g. a | ||
| key pressed in the visualizer's UI), so a listener tracks the keys it holds. | ||
| - ``on_focus_lost`` runs when the window stops delivering keys: it lost focus, the visualizer's | ||
| UI took the keyboard, or the source closed. The listener treats every held key as released. | ||
| - A listener that raises is logged and does not affect other listeners. | ||
| - Closing the source delivers a final focus loss to every listener; afterwards | ||
| :meth:`add_key_listener` raises :class:`RuntimeError`. | ||
|
|
||
| The visualizer connects native events when its window initializes and closes the source before | ||
| the window closes. Backend bridges report events through :meth:`_emit_key` and | ||
| :meth:`_emit_focus_lost`; adding or removing a listener never changes native window resources. | ||
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
| self._subscriptions: list[KeyboardSubscription] = [] | ||
| self._closed = False | ||
|
|
||
| @property | ||
| def closed(self) -> bool: | ||
| """Whether the source was closed.""" | ||
| return self._closed | ||
|
|
||
| def add_key_listener(self, on_key: KeyCallback, on_focus_lost: FocusLostCallback) -> KeyboardSubscription: | ||
| """Subscribe to key presses and releases from the initialized window. | ||
|
|
||
| Args: | ||
| on_key: Called with the key's W3C ``KeyboardEvent.code`` and whether it was pressed. | ||
| on_focus_lost: Called when the window stops delivering keys. | ||
|
|
||
| Returns: | ||
| The subscription. Close it to unsubscribe. | ||
|
|
||
| Raises: | ||
| RuntimeError: If the source is closed. | ||
| """ | ||
| if self._closed: | ||
| raise RuntimeError(f"{type(self).__name__} is closed") | ||
| subscription = KeyboardSubscription(self, on_key, on_focus_lost) | ||
| self._subscriptions.append(subscription) | ||
| return subscription | ||
|
|
||
| def close(self) -> None: | ||
| """Close every subscription and deliver a final focus loss to its listener.""" | ||
| if self._closed: | ||
| return | ||
| self._closed = True | ||
| subscriptions, self._subscriptions = self._subscriptions, [] | ||
| for subscription in subscriptions: | ||
| if not subscription.closed: | ||
| subscription.close() | ||
| _call(subscription._on_focus_lost) | ||
|
|
||
| def _emit_key(self, code: str, pressed: bool) -> None: | ||
| """Report a key press or release to every listener.""" | ||
| for subscription in list(self._subscriptions): | ||
| if not subscription.closed: | ||
| _call(subscription._on_key, code, pressed) | ||
|
|
||
| def _emit_focus_lost(self) -> None: | ||
| """Tell every listener that the window stopped delivering keys.""" | ||
| for subscription in list(self._subscriptions): | ||
| if not subscription.closed: | ||
| _call(subscription._on_focus_lost) | ||
|
|
||
|
|
||
| def _call(callback: Callable, *args) -> None: | ||
| try: | ||
| callback(*args) | ||
| except Exception: | ||
| logger.exception("Keyboard listener %r failed", callback) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.