Skip to content
Draft
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 6 additions & 108 deletions examples/notebook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"id": "aa695816",
"metadata": {
"jupyter": {
Expand All @@ -25,116 +25,14 @@
},
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 2,
"id": "461399b0-e02d-43d9-9ede-c1aa6c180338",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "ab365ae7b1ff463dab87b8d77a3e09a5",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"RFBOutputContext()"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/javascript": [
"\n",
" (function() {\n",
" function setup_rightclick() {\n",
" // Get all buttons with the right-click-button class\n",
" var button = document.getElementsByClassName(\n",
" 'right-click-buttonNone'\n",
" )[0];\n",
" if (!button) {\n",
" return;\n",
" }\n",
"\n",
" // For each button, add a contextmenu listener\n",
" button.addEventListener('contextmenu', function(e) {\n",
" // Prevent default context menu\n",
" e.preventDefault();\n",
" e.stopPropagation();\n",
"\n",
" // Get button position\n",
" var rect = this.getBoundingClientRect();\n",
" var scrollLeft = window.pageXOffset ||\n",
" document.documentElement.scrollLeft;\n",
" var scrollTop = window.pageYOffset ||\n",
" document.documentElement.scrollTop;\n",
"\n",
" // Position the popup above the button\n",
" var popup = document.getElementsByClassName(\n",
" 'ipywidget-popupNone'\n",
" )[0];\n",
" popup.style.display = '';\n",
" popup.style.position = 'absolute';\n",
" popup.style.top = (rect.bottom + scrollTop) + 'px';\n",
" popup.style.left = (rect.left + scrollLeft) + 'px';\n",
"\n",
" // Style the popup\n",
" popup.style.background = 'white';\n",
" popup.style.border = '1px solid #ccc';\n",
" popup.style.borderRadius = '3px';\n",
" popup.style.padding = '8px';\n",
" popup.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';\n",
" popup.style.zIndex = '1000';\n",
"\n",
" // Add to body\n",
" document.body.appendChild(popup);\n",
"\n",
" // Close popup when clicking elsewhere\n",
" document.addEventListener('click', function closePopup(event) {\n",
" var popup = document.getElementsByClassName(\n",
" 'ipywidget-popupNone'\n",
" )[0];\n",
" if (popup && !popup.contains(event.target)) {\n",
" popup.style.display = 'none';\n",
" document.removeEventListener('click', closePopup);\n",
" }\n",
" });\n",
"\n",
" return false;\n",
" });\n",
" }\n",
"\n",
" // Make sure it works even after widget is redrawn/updated\n",
" setTimeout(setup_rightclick, 1000);\n",
" })();\n",
" "
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "1e07c07e8f5b4a6784b1f3fc1743e505",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(BoundedFloatText(value=0.0, description='Ignore Lower Tail:', step=0.1, style=DescriptionStyle(…"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "6dee7c200c7843c1b6643ff256cd60ae",
"model_id": "1833857155da4b69878bfb6d83523d67",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -222,7 +120,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "efade88644164099b6f47c3f97d53cfa",
"model_id": "1b98cbfd0d69426db4b715190733d729",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -310,7 +208,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "2c80fd0c49104d2195a3fc3875f19b0e",
"model_id": "437387a825bd46cf8adee59f18cc577b",
"version_major": 2,
"version_minor": 0
},
Expand Down Expand Up @@ -368,7 +266,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.12"
"version": "3.14.4"
}
},
"nbformat": 4,
Expand Down
12 changes: 8 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"

[tool.hatch.metadata]
allow-direct-references = true

# https://peps.python.org/pep-0621/
[project]
name = "ndv"
Expand Down Expand Up @@ -34,7 +37,8 @@ dependencies = [
"cmap >=0.3",
"numpy >=2.1.0; python_version >= '3.13'",
"numpy >=1.26.0; python_version >= '3.12'",
"psygnal >=0.12",
# Gives us SignalGroupDescriptor.connect_child_events
"psygnal >=0.14",
"pydantic >=2.10",
"typing_extensions >= 4.10",
"ihist>=0.1.3",
Expand All @@ -59,9 +63,9 @@ pyside = [
]
wxpython = ["pyconify>=0.2.1", "wxpython >=4.2.2"]

# Supported Canavs backends
vispy = ["vispy>=0.16", "pyopengl >=3.1"]
pygfx = ["pygfx>=0.16.0", "rendercanvas>=2.6.2", "wgpu>=0.31.0"]
# Supported Canvas backends
vispy = ["scenex[vispy] @ git+https://github.com/pyapp-kit/scenex.git"]
pygfx = ["scenex[pygfx] @ git+https://github.com/pyapp-kit/scenex.git"]

# ready to go bundles with pygfx
qt = ["ndv[pygfx,pyqt]", "imageio[tifffile] >=2.20"]
Expand Down
4 changes: 2 additions & 2 deletions scripts/profile_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
from unittest.mock import MagicMock, patch

import numpy as np
from ndv.views.bases._graphics._canvas import ArrayCanvas
from ndv.views.bases._graphics._canvas_elements import ImageHandle

from ndv.controllers import ArrayViewer
from ndv.models._array_display_model import ChannelMode
from ndv.views import _app
from ndv.views.bases import ArrayView, LUTView
from ndv.views.bases._graphics._canvas import ArrayCanvas
from ndv.views.bases._graphics._canvas_elements import ImageHandle

# --------------- mock setup (same pattern as test_controller) ---------------

Expand Down
37 changes: 22 additions & 15 deletions src/ndv/_keybindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
from enum import Enum, auto
from typing import TYPE_CHECKING, cast

from ndv._types import KeyCode, KeyMod, KeyPressEvent
from app_model.types import KeyBinding, KeyCode, SimpleKeyBinding
from scenex.app.events import MouseButton, WheelEvent

if TYPE_CHECKING:
from scenex.app.events import KeyPressEvent

from ndv._types import AxisKey
from ndv.controllers._array_viewer import ArrayViewer

Expand All @@ -21,21 +24,22 @@ class Action(Enum):
ZOOM_OUT = auto()


_DEFAULT_KEYBINDINGS: dict[tuple[KeyCode | str, KeyMod], Action] = {
(KeyCode.RIGHT, KeyMod.NONE): Action.STEP_FORWARD,
(KeyCode.LEFT, KeyMod.NONE): Action.STEP_BACKWARD,
(KeyCode.UP, KeyMod.NONE): Action.FOCUS_PREV_AXIS,
(KeyCode.DOWN, KeyMod.NONE): Action.FOCUS_NEXT_AXIS,
("+", KeyMod.SHIFT): Action.ZOOM_IN,
("=", KeyMod.NONE): Action.ZOOM_IN,
("-", KeyMod.NONE): Action.ZOOM_OUT,
("_", KeyMod.SHIFT): Action.ZOOM_OUT,
_DEFAULT_KEYBINDINGS: dict[KeyBinding, Action] = {
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.RightArrow)]): Action.STEP_FORWARD,
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.LeftArrow)]): Action.STEP_BACKWARD,
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.UpArrow)]): Action.FOCUS_PREV_AXIS,
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.DownArrow)]): Action.FOCUS_NEXT_AXIS,
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.Equal)]): Action.ZOOM_IN,
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.Equal, shift=True)]): Action.ZOOM_IN,
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.NumpadAdd)]): Action.ZOOM_IN,
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.Minus)]): Action.ZOOM_OUT,
KeyBinding(parts=[SimpleKeyBinding(key=KeyCode.NumpadSubtract)]): Action.ZOOM_OUT,
}


def handle_key_press(event: KeyPressEvent, viewer: ArrayViewer) -> None:
"""Dispatch a key press event to the appropriate action."""
action = _DEFAULT_KEYBINDINGS.get((event.key, event.mods))
action = _DEFAULT_KEYBINDINGS.get(event.key)
if action is Action.STEP_FORWARD:
_step_focused_slider(viewer, 1)
elif action is Action.STEP_BACKWARD:
Expand All @@ -44,10 +48,13 @@ def handle_key_press(event: KeyPressEvent, viewer: ArrayViewer) -> None:
_cycle_focused_axis(viewer, 1)
elif action is Action.FOCUS_PREV_AXIS:
_cycle_focused_axis(viewer, -1)
elif action is Action.ZOOM_IN and viewer._highlight_pos is not None:
viewer._canvas.zoom(factor=0.667, center=viewer._highlight_pos)
elif action is Action.ZOOM_OUT and viewer._highlight_pos is not None:
viewer._canvas.zoom(factor=1.5, center=viewer._highlight_pos)
elif action in [Action.ZOOM_IN, Action.ZOOM_OUT]:
size = viewer._canvas._canvas.size
canvas_pos = (size[0] / 2, size[1] / 2)
angle_delta = (0, 120) if action is Action.ZOOM_IN else (0, -120)
mouse_event = WheelEvent(canvas_pos, MouseButton.NONE, angle_delta=angle_delta)
view = viewer._canvas.view
view.camera.controller.handle_event(mouse_event, view)


def _steppable_axes(viewer: ArrayViewer) -> list[AxisKey]:
Expand Down
Loading
Loading