From 0e32cc52f10d6734958c7d8e6a0398eda1481f21 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Fri, 27 Feb 2026 06:39:42 -0800 Subject: [PATCH] improved keyboard input handling for jupyter notebooks --- rtxpy/notebook.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/rtxpy/notebook.py b/rtxpy/notebook.py index c19cb1b..c4f8bf1 100644 --- a/rtxpy/notebook.py +++ b/rtxpy/notebook.py @@ -120,19 +120,25 @@ def _map_browser_key(event: dict) -> Tuple[str, str]: el.style.outline = 'none'; el.style.cursor = 'crosshair'; + // Tell Lumino (JupyterLab) to skip shortcut processing for + // keyboard events originating from this element. ipyevents + // already captures keyboard via its own document-level listener, + // so we just need Lumino to not interfere. + el.setAttribute('data-lm-suppress-shortcuts', 'true'); + // Visual focus indicator el.addEventListener('focus', function() { el.style.outline = '2px solid #4a9eff'; + // Classic Notebook: disable its keyboard manager + if (window.IPython && IPython.keyboard_manager) { + IPython.keyboard_manager.disable(); + } }); el.addEventListener('blur', function() { el.style.outline = 'none'; - }); - - // Stop keyboard events from bubbling to notebook handlers - ['keydown', 'keyup', 'keypress'].forEach(function(type) { - el.addEventListener(type, function(e) { - e.stopPropagation(); - }); + if (window.IPython && IPython.keyboard_manager) { + IPython.keyboard_manager.enable(); + } }); // Stop wheel from scrolling the notebook page @@ -182,6 +188,16 @@ def _render_help_text(self): img = img.resize((new_w, new_h), Image.LANCZOS) self._help_text_rgba = np.array(img, dtype=np.float32) / 255.0 + def _handle_key_press(self, raw_key, key): + """Override to suppress exit keys in Jupyter. + + In Jupyter, 'x' and 'escape' shouldn't kill the viewer — too easy + to hit accidentally. Use ``widget.stop()`` instead. + """ + if key in ('escape', 'x'): + return + super()._handle_key_press(raw_key, key) + def run(self, start_position: Optional[Tuple[float, float, float]] = None, look_at: Optional[Tuple[float, float, float]] = None): """Start the viewer and return an interactive widget. @@ -258,7 +274,7 @@ def run(self, start_position: Optional[Tuple[float, float, float]] = None, 'wheel', ], prevent_default_action=True, - wait=8, + wait=0, ) event_handler.on_dom_event(self._handle_dom_event) self._event_handler = event_handler @@ -284,9 +300,7 @@ def run(self, start_position: Optional[Tuple[float, float, float]] = None, ) self._render_thread.start() - print(f"rtxpy Jupyter viewer started ({self.width}x{self.height})") - print("Click the image to focus (blue border), then use keyboard/mouse.") - print("Press H for controls. Call widget.stop() to exit.") + print(f"rtxpy viewer ({self.width}x{self.height}) — click image to focus, H for help, widget.stop() to exit") # Display widget + keyboard isolation JavaScript display(self._widget)