Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
11 changes: 4 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,16 @@ pyside = [
"superqt[iconify,pyside6] >=0.7.2",
# https://github.com/pyapp-kit/ndv/issues/59
"pyside6 ==6.6.3; sys_platform == 'win32'",
"numpy >=1.23,<2; sys_platform == 'win32'", # needed for pyside6.6
"numpy >=1.23,<2; sys_platform == 'win32'", # needed for pyside6.6
"pyside6 >=6.4",
"pyside6 >=6.6; python_version >= '3.12'",
"qtpy >=2",
]
wxpython = [
"pyconify>=0.2.1",
"wxpython >=4.2.2",
]
wxpython = ["pyconify>=0.2.1", "wxpython >=4.2.2"]

# Supported Canavs backends
vispy = ["vispy>=0.14.3", "pyopengl >=3.1"]
pygfx = ["pygfx>=0.8.0"]
pygfx = ["pygfx>=0.8.0", "rendercanvas>=2.1.0"]

# ready to go bundles with pygfx
qt = ["ndv[pygfx,pyqt]", "imageio[tifffile] >=2.20"]
Expand Down Expand Up @@ -110,7 +107,7 @@ dev = [
# omitting wxpython from dev env for now
# because `uv sync && pytest hangs` on a wx test in the "full" env
# use `make test extras=wx,[pygfx|vispy] isolated=1` to test
"ndv[vispy,pygfx,pyqt,jupyter]",
"ndv[vispy,pygfx,pyqt,jupyter]",
"imageio[tifffile] >=2.20",
"ipykernel>=6.29.5",
"ipython>=8.18.1",
Expand Down
12 changes: 6 additions & 6 deletions src/ndv/controllers/_array_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,15 @@ def __init__(
# mapping of channel keys to their respective controllers
# where None is the default channel
self._lut_controllers: dict[ChannelKey, ChannelController] = {}
self._histograms: dict[ChannelKey, HistogramCanvas] = {}

# get and create the front-end and canvas classes
frontend_cls = _app.get_array_view_class()
self._view = frontend_cls(self._viewer_model)
canvas_cls = _app.get_array_canvas_class()
self._canvas = canvas_cls(self._viewer_model)

# TODO: Is this necessary?
self._histograms: dict[ChannelKey, HistogramCanvas] = {}
self._view = frontend_cls(self._canvas.frontend_widget(), self._viewer_model)
parent = self._view.frontend_widget()
self._canvas = canvas_cls(self._viewer_model, parent=parent)
self._view.embed_canvas(self._canvas)

self._roi_view: RectangularROIHandle | None = None

Expand Down Expand Up @@ -254,7 +254,7 @@ def _default_display_model(

def _add_histogram(self, channel: ChannelKey = None) -> None:
histogram_cls = _app.get_histogram_canvas_class() # will raise if not supported
hist = histogram_cls()
hist = histogram_cls(parent=self._view.frontend_widget())
if ctrl := self._lut_controllers.get(channel, None):
# Add histogram to ArrayView for display
self._view.add_histogram(channel, hist)
Expand Down
27 changes: 16 additions & 11 deletions src/ndv/views/_jupyter/_array_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from vispy.app.backends import _jupyter_rfb

from ndv._types import AxisKey, ChannelKey
from ndv.views.bases._graphics._canvas import HistogramCanvas
from ndv.views.bases._graphics._canvas import ArrayCanvas, HistogramCanvas

# not entirely sure why it's necessary to specifically annotat signals as : PSignal
# i think it has to do with type variance?
Expand Down Expand Up @@ -382,13 +382,11 @@
class JupyterArrayView(ArrayView):
def __init__(
self,
canvas_widget: _jupyter_rfb.CanvasBackend,
viewer_model: ArrayViewerModel,
) -> None:
self._viewer_model = viewer_model
self._viewer_model.events.connect(self._on_viewer_model_event)
# WIDGETS
self._canvas_widget = canvas_widget
self._visible_axes: Sequence[AxisKey] = []
self._luts: dict[ChannelKey, JupyterLutView] = {}

Expand Down Expand Up @@ -454,12 +452,6 @@
),
)

try:
width = getattr(canvas_widget, "css_width", "600px").replace("px", "")
width = f"{int(width) + 4}px"
except Exception:
width = "604px"

self._btns_box = widgets.HBox(
[
self._channel_mode_combo,
Expand All @@ -472,19 +464,32 @@
self.layout = widgets.VBox(
[
top_row,
self._canvas_widget,
self._hover_info_label,
self._slider_box,
self._luts_box,
self._btns_box,
],
layout=widgets.Layout(width=width),
layout=widgets.Layout(width="604px"),
)

# CONNECTIONS

self._channel_mode_combo.observe(self._on_channel_mode_changed, names="value")

def embed_canvas(self, canvas: ArrayCanvas) -> None:
canvas_widget: _jupyter_rfb.CanvasBackend = canvas.frontend_widget()

try:
width = getattr(canvas_widget, "css_width", "600px").replace("px", "")
width = f"{int(width) + 4}px"
except Exception:
width = "604px"

Check warning on line 486 in src/ndv/views/_jupyter/_array_view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/views/_jupyter/_array_view.py#L485-L486

Added lines #L485 - L486 were not covered by tests

children = list(self.layout.children)
children.insert(1, canvas_widget)
self.layout.children = tuple(children)
self.layout.layout.width = width

def create_sliders(self, coords: Mapping[Hashable, Sequence]) -> None:
"""Update sliders with the given coordinate ranges."""
sliders = []
Expand Down
14 changes: 9 additions & 5 deletions src/ndv/views/_pygfx/_array_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,24 +365,21 @@ def remove(self) -> None:
class GfxArrayCanvas(ArrayCanvas):
"""pygfx-based canvas wrapper."""

def __init__(self, viewer_model: ArrayViewerModel) -> None:
def __init__(self, viewer_model: ArrayViewerModel, parent: Any = None) -> None:
self._viewer = viewer_model

self._current_shape: tuple[int, ...] = ()
self._last_state: dict[Literal[2, 3], Any] = {}

cls = rendercanvas_class()
self._canvas = cls(size=(600, 600))
self._canvas = cls(parent=parent)

# this filter needs to remain in scope for the lifetime of the canvas
# or mouse events will not be intercepted
# the returned function can be called to remove the filter, (and it also
# closes on the event filter and keeps it in scope).
self._disconnect_mouse_events = filter_mouse_events(self._canvas, self)

self._renderer = pygfx.renderers.WgpuRenderer(self._canvas)
self._renderer.blend_mode = "additive"

self._scene = pygfx.Scene()
self._camera: pygfx.Camera | None = None
self._ndim: Literal[2, 3] | None = None
Expand All @@ -392,6 +389,13 @@ def __init__(self, viewer_model: ArrayViewerModel) -> None:
# Maintain a weak reference to the last ROI created.
self._last_roi_created: ReferenceType[PyGFXRectangle] | None = None

@property
def _renderer(self) -> pygfx.Renderer:
if not hasattr(self, "_renderer_"):
self._renderer_ = pygfx.renderers.WgpuRenderer(self._canvas)
self._renderer_.blend_mode = "additive"
return self._renderer_

def frontend_widget(self) -> Any:
return self._canvas

Expand Down
6 changes: 3 additions & 3 deletions src/ndv/views/_pygfx/_histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def handle_event(
class PyGFXHistogramCanvas(HistogramCanvas):
"""A HistogramCanvas utilizing VisPy."""

def __init__(self, *, vertical: bool = False) -> None:
def __init__(self, parent: Any = None) -> None:
# ------------ data and state ------------ #

self._values: np.ndarray | None = None
Expand All @@ -101,7 +101,7 @@ def __init__(self, *, vertical: bool = False) -> None:
# whether the y-axis is logarithmic
self._log_base: float | None = None
# whether the histogram is vertical
self._vertical: bool = vertical
self._vertical: bool = False
# The values of the left and right edges on the canvas (respectively)
self._domain: tuple[float, float] | None = None
# The values of the bottom and top edges on the canvas (respectively)
Expand All @@ -116,7 +116,7 @@ def __init__(self, *, vertical: bool = False) -> None:
# ------------ PyGFX Canvas ------------ #
cls = rendercanvas_class()
self._size = (600, 600)
self._canvas = cls(size=self._size)
self._canvas = cls(size=self._size, parent=parent)

# this filter needs to remain in scope for the lifetime of the canvas
# or mouse events will not be intercepted
Expand Down
7 changes: 2 additions & 5 deletions src/ndv/views/_pygfx/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ def sizeHint(self) -> QSize:

return rendercanvas.jupyter.JupyterRenderCanvas
if frontend == GuiFrontend.WX:
# ...still not working
# import rendercanvas.wx
# return rendercanvas.wx.WxRenderWidget
from wgpu.gui.wx import WxWgpuCanvas
import rendercanvas.wx

return WxWgpuCanvas
return rendercanvas.wx.WxRenderWidget
42 changes: 27 additions & 15 deletions src/ndv/views/_qt/_array_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from qtpy.QtGui import QIcon

from ndv._types import AxisKey, ChannelKey
from ndv.views.bases._graphics._canvas import HistogramCanvas
from ndv.views.bases._graphics._canvas import ArrayCanvas, HistogramCanvas
from ndv.views.bases._graphics._canvas_elements import (
CanvasElement,
RectangularROIHandle,
Expand Down Expand Up @@ -679,10 +679,10 @@

# this is a PView ... but that would make a metaclass conflict
class _QArrayViewer(QWidget):
def __init__(self, canvas_widget: QWidget, parent: QWidget | None = None):
def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
self._canvas_widget: QWidget | None = None

self._canvas_widget = canvas_widget
self.dims_sliders = _QDimsSliders(self)

# place to display dataset summary
Expand All @@ -691,7 +691,7 @@
self.hover_info_label = QLabel("", self)

# spinner to indicate progress
self._progress_spinner = _QSpinner(canvas_widget)
self._progress_spinner = _QSpinner(self)
self._progress_spinner.hide()

# the button that controls the display mode of the channels
Expand Down Expand Up @@ -748,11 +748,10 @@
info.addWidget(self.hover_info_label)

left = QWidget()
left_layout = QVBoxLayout(left)
self._left_layout = left_layout = QVBoxLayout(left)
left_layout.setSpacing(2)
left_layout.setContentsMargins(0, 0, 0, 0)
left_layout.addWidget(self._info_widget)
left_layout.addWidget(canvas_widget, 1)
left_layout.addWidget(self.dims_sliders)
left_layout.addWidget(self.luts)
left_layout.addWidget(self._btns)
Expand All @@ -767,25 +766,35 @@

def resizeEvent(self, a0: Any) -> None:
# position at spinner the top right of the canvas_widget:
canv, spinner = self._canvas_widget, self._progress_spinner
pad = 4
spinner.move(canv.width() - spinner.width() - pad, pad)
if self._canvas_widget is not None:
canv, spinner = self._canvas_widget, self._progress_spinner
pad = 4
spinner.move(canv.width() - spinner.width() - pad, pad)
super().resizeEvent(a0)

def closeEvent(self, a0: Any) -> None:
with suppress(AttributeError):
del self._canvas_widget
super().closeEvent(a0)

def embed_canvas(self, canvas: QWidget) -> None:
"""Embed the canvas widget into the viewer."""
if self._canvas_widget is not None:
# remove the old canvas
self._progress_spinner.setParent(self)
self._left_layout.removeWidget(self._canvas_widget)
self._canvas_widget.setParent(None)
self._canvas_widget.deleteLater()

Check warning on line 787 in src/ndv/views/_qt/_array_view.py

View check run for this annotation

Codecov / codecov/patch

src/ndv/views/_qt/_array_view.py#L784-L787

Added lines #L784 - L787 were not covered by tests

self._canvas_widget = canvas
self._left_layout.insertWidget(1, canvas, 1)
self._progress_spinner.setParent(canvas)


class QtArrayView(ArrayView):
def __init__(
self,
canvas_widget: QWidget,
viewer_model: ArrayViewerModel,
) -> None:
def __init__(self, viewer_model: ArrayViewerModel) -> None:
self._viewer_model = viewer_model
self._qwidget = qwdg = _QArrayViewer(canvas_widget)
self._qwidget = qwdg = _QArrayViewer()
# Mapping of channel key to LutViews
self._luts: dict[ChannelKey, QLutView] = {}
qwdg.add_roi_btn.toggled.connect(self._on_add_roi_clicked)
Expand All @@ -802,6 +811,9 @@

self._visible_axes: Sequence[AxisKey] = []

def embed_canvas(self, canvas: ArrayCanvas) -> None:
return self._qwidget.embed_canvas(canvas.frontend_widget())

def add_lut_view(self, channel: ChannelKey) -> QLutView:
view = (
QRGBView(channel)
Expand Down
2 changes: 1 addition & 1 deletion src/ndv/views/_vispy/_array_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ class VispyArrayCanvas(ArrayCanvas):
could be swapped in if needed as long as they implement the same interface).
"""

def __init__(self, viewer_model: ArrayViewerModel) -> None:
def __init__(self, viewer_model: ArrayViewerModel, parent: Any = None) -> None:
self._viewer = viewer_model

self._canvas = scene.SceneCanvas(size=(600, 600))
Expand Down
6 changes: 3 additions & 3 deletions src/ndv/views/_vispy/_histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Grabbable(Enum):
class VispyHistogramCanvas(HistogramCanvas):
"""A HistogramCanvas utilizing VisPy."""

def __init__(self, *, vertical: bool = False) -> None:
def __init__(self, parent: Any = None) -> None:
# ------------ data and state ------------ #

self._values: np.ndarray | None = None
Expand All @@ -47,7 +47,7 @@ def __init__(self, *, vertical: bool = False) -> None:
# whether the y-axis is logarithmic
self._log_base: float | None = None
# whether the histogram is vertical
self._vertical: bool = vertical
self._vertical: bool = False
# The values of the left and right edges on the canvas (respectively)
self._domain: tuple[float, float] | None = None
# The values of the bottom and top edges on the canvas (respectively)
Expand Down Expand Up @@ -124,7 +124,7 @@ def __init__(self, *, vertical: bool = False) -> None:
self.plot._view.add(self._gamma_handle)
self.plot._view.add(self._highlight)

self.set_vertical(vertical)
self.set_vertical(False)

def refresh(self) -> None:
self._canvas.update()
Expand Down
Loading
Loading