Skip to content
Draft
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
4 changes: 2 additions & 2 deletions napari/_qt/widgets/qt_viewer_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ def __init__(self, viewer: 'ViewerModel') -> None:
)
self.gridViewButton = gvb
gvb.setCheckable(True)
gvb.setChecked(viewer.grid.enabled)
gvb.setChecked(viewer.canvases.grid_enabled)
gvb.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
gvb.customContextMenuRequested.connect(self._open_grid_popup)

@self.viewer.grid.events.enabled.connect
@self.viewer.canvases.events.grid_enabled.connect
def _set_grid_mode_checkstate(event):
gvb.setChecked(event.value)

Expand Down
49 changes: 48 additions & 1 deletion napari/_vispy/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,51 @@ def __init__(
self.viewer.camera.events.zoom.connect(self._on_cursor)
self.viewer.layers.events.reordered.connect(self._reorder_layers)
self.viewer.layers.events.removed.connect(self._remove_layer)
self.viewer.canvases.events.grid_enabled.connect(self._on_grid_change)
self.viewer.canvases.events.stride.connect(self._on_grid_change)
self.destroyed.connect(self._disconnect_theme)

def _on_grid_change(self):
"""Change grid view"""
if self.viewer.canvases.grid_enabled:
grid_shape, n_gridboxes = self.viewer.canvases.actual_shape(
len(self.layer_to_visual)
)

self.grid = self.central_widget.add_grid()
camera = self.camera._view.camera
self.grid_views = [
self.grid.add_view(
row=y, col=x, camera=camera if y == 0 and x == 0 else None
)
for y in range(grid_shape[0])
for x in range(grid_shape[1])
if x * y < n_gridboxes
]
self.camera._view = self.grid_views[0]
self.central_widget.remove_widget(self.view)
# del self.view
self.grid_cameras = [
VispyCamera(
self.grid_views[i], self.viewer.camera, self.viewer.dims
)
for i in range(len(self.grid_views[1:]))
]

for ind, layer in enumerate(self.layer_to_visual.values()):
if ind != 0:
self.grid_views[ind].camera = self.grid_cameras[
ind - 1
]._view.camera
self.grid_views[ind].camera.link(self.grid_views[0].camera)
layer.node.parent = self.grid_views[ind].scene
else:
for layer in self.layer_to_visual.values():
layer.node.parent = self.view.scene
self.camera._view = self.view
self.central_widget.remove_widget(self.grid)
del self.grid

@property
def destroyed(self) -> pyqtBoundSignal:
return self._scene_canvas._backend.destroyed
Expand Down Expand Up @@ -314,7 +357,11 @@ def _map_canvas2world(
of the viewer.
"""
nd = self.viewer.dims.ndisplay
transform = self.view.scene.transform
# TODO look into how to extend this to all grid boxes
if self.viewer.canvases.grid_enabled:
transform = self.grid_views[0].scene.transform
else:
transform = self.view.scene.transform
mapped_position = transform.imap(list(position))[:nd]
position_world_slice = mapped_position[::-1]

Expand Down
96 changes: 48 additions & 48 deletions napari/components/_tests/test_grid.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,84 @@
from napari.components.grid import GridCanvas
from napari.components.canvas import Canvas


def test_grid_creation():
"""Test creating grid object"""
grid = GridCanvas()
assert grid is not None
assert not grid.enabled
assert grid.shape == (-1, -1)
assert grid.stride == 1
canvas = Canvas()
assert canvas is not None
assert not canvas.grid_enabled
assert canvas.shape == (-1, -1)
assert canvas.stride == 1


def test_shape_stride_creation():
"""Test creating grid object"""
grid = GridCanvas(shape=(3, 4), stride=2)
assert grid.shape == (3, 4)
assert grid.stride == 2
canvas = Canvas(shape=(3, 4), stride=2)
assert canvas.shape == (3, 4)
assert canvas.stride == 2


def test_actual_shape_and_position():
"""Test actual shape"""
grid = GridCanvas(enabled=True)
assert grid.enabled
canvas = Canvas(grid_enabled=True)
assert canvas.grid_enabled

# 9 layers get put in a (3, 3) grid
assert grid.actual_shape(9) == (3, 3)
assert grid.position(0, 9) == (0, 0)
assert grid.position(2, 9) == (0, 2)
assert grid.position(3, 9) == (1, 0)
assert grid.position(8, 9) == (2, 2)
assert canvas.actual_shape(9) == ((3, 3), 9)
assert canvas.position(0, 9) == (0, 0)
assert canvas.position(2, 9) == (0, 2)
assert canvas.position(3, 9) == (1, 0)
assert canvas.position(8, 9) == (2, 2)

# 5 layers get put in a (2, 3) grid
assert grid.actual_shape(5) == (2, 3)
assert grid.position(0, 5) == (0, 0)
assert grid.position(2, 5) == (0, 2)
assert grid.position(3, 5) == (1, 0)
assert canvas.actual_shape(5) == ((2, 3), 5)
assert canvas.position(0, 5) == (0, 0)
assert canvas.position(2, 5) == (0, 2)
assert canvas.position(3, 5) == (1, 0)

# 10 layers get put in a (3, 4) grid
assert grid.actual_shape(10) == (3, 4)
assert grid.position(0, 10) == (0, 0)
assert grid.position(2, 10) == (0, 2)
assert grid.position(3, 10) == (0, 3)
assert grid.position(8, 10) == (2, 0)
assert canvas.actual_shape(10) == ((3, 4), 10)
assert canvas.position(0, 10) == (0, 0)
assert canvas.position(2, 10) == (0, 2)
assert canvas.position(3, 10) == (0, 3)
assert canvas.position(8, 10) == (2, 0)


def test_actual_shape_with_stride():
"""Test actual shape"""
grid = GridCanvas(enabled=True, stride=2)
assert grid.enabled
canvas = Canvas(grid_enabled=True, stride=2)
assert canvas.grid_enabled

# 7 layers get put in a (2, 2) grid
assert grid.actual_shape(7) == (2, 2)
assert grid.position(0, 7) == (0, 0)
assert grid.position(1, 7) == (0, 0)
assert grid.position(2, 7) == (0, 1)
assert grid.position(3, 7) == (0, 1)
assert grid.position(6, 7) == (1, 1)
assert canvas.actual_shape(7) == ((2, 2), 4)
assert canvas.position(0, 7) == (0, 0)
assert canvas.position(1, 7) == (0, 0)
assert canvas.position(2, 7) == (0, 1)
assert canvas.position(3, 7) == (0, 1)
assert canvas.position(6, 7) == (1, 1)

# 3 layers get put in a (1, 2) grid
assert grid.actual_shape(3) == (1, 2)
assert grid.position(0, 3) == (0, 0)
assert grid.position(1, 3) == (0, 0)
assert grid.position(2, 3) == (0, 1)
assert canvas.actual_shape(3) == ((1, 2), 2)
assert canvas.position(0, 3) == (0, 0)
assert canvas.position(1, 3) == (0, 0)
assert canvas.position(2, 3) == (0, 1)


def test_actual_shape_and_position_negative_stride():
"""Test actual shape"""
grid = GridCanvas(enabled=True, stride=-1)
assert grid.enabled
canvas = Canvas(grid_enabled=True, stride=-1)
assert canvas.grid_enabled

# 9 layers get put in a (3, 3) grid
assert grid.actual_shape(9) == (3, 3)
assert grid.position(0, 9) == (2, 2)
assert grid.position(2, 9) == (2, 0)
assert grid.position(3, 9) == (1, 2)
assert grid.position(8, 9) == (0, 0)
assert canvas.actual_shape(9) == ((3, 3), 9)
assert canvas.position(0, 9) == (2, 2)
assert canvas.position(2, 9) == (2, 0)
assert canvas.position(3, 9) == (1, 2)
assert canvas.position(8, 9) == (0, 0)


def test_actual_shape_grid_disabled():
"""Test actual shape with grid disabled"""
grid = GridCanvas()
assert not grid.enabled
assert grid.actual_shape(9) == (1, 1)
assert grid.position(3, 9) == (0, 0)
canvas = Canvas()
assert not canvas.grid_enabled
assert canvas.actual_shape(9) == ((1, 1), 0)
assert canvas.position(3, 9) == (0, 0)
2 changes: 1 addition & 1 deletion napari/components/_viewer_key_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def transpose_axes(viewer: Viewer):

@register_viewer_action(trans._("Toggle grid mode."))
def toggle_grid(viewer: Viewer):
viewer.grid.enabled = not viewer.grid.enabled
viewer.canvases.grid_enabled = not viewer.canvases.grid_enabled


@register_viewer_action(trans._("Toggle visibility of selected layers"))
Expand Down
47 changes: 12 additions & 35 deletions napari/components/grid.py → napari/components/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,12 @@
from napari.utils.events import EventedModel


class GridCanvas(EventedModel):
"""Grid for canvas.

Right now the only grid mode that is still inside one canvas with one
camera, but future grid modes could support multiple canvases.

Attributes
----------
enabled : bool
If grid is enabled or not.
stride : int
Number of layers to place in each grid square before moving on to
the next square. The default ordering is to place the most visible
layer in the top left corner of the grid. A negative stride will
cause the order in which the layers are placed in the grid to be
reversed.
shape : 2-tuple of int
Number of rows and columns in the grid. A value of -1 for either or
both of will be used the row and column numbers will trigger an
auto calculation of the necessary grid shape to appropriately fill
all the layers at the appropriate stride.
"""

# fields
class Canvas(EventedModel):
grid_enabled: bool = False
stride: GridStride = 1
shape: Tuple[GridHeight, GridWidth] = (-1, -1)
enabled: bool = False

def actual_shape(self, nlayers: int = 1) -> Tuple[int, int]:
def actual_shape(self, nlayers: int = 1) -> Tuple[Tuple[int, int], int]:
"""Return the actual shape of the grid.

This will return the shape parameter, unless one of the row
Expand All @@ -54,11 +31,11 @@ def actual_shape(self, nlayers: int = 1) -> Tuple[int, int]:
shape : 2-tuple of int
Number of rows and columns in the grid.
"""
if not self.enabled:
return (1, 1)
if not self.grid_enabled:
return (1, 1), 0

if nlayers == 0:
return (1, 1)
return (1, 1), 0

n_row, n_column = self.shape
n_grid_squares = np.ceil(nlayers / abs(self.stride)).astype(int)
Expand All @@ -74,7 +51,7 @@ def actual_shape(self, nlayers: int = 1) -> Tuple[int, int]:
n_row = max(1, n_row)
n_column = max(1, n_column)

return (n_row, n_column)
return (n_row, n_column), n_grid_squares

def position(self, index: int, nlayers: int) -> Tuple[int, int]:
"""Return the position of a given linear index in grid.
Expand All @@ -93,16 +70,16 @@ def position(self, index: int, nlayers: int) -> Tuple[int, int]:
position : 2-tuple of int
Row and column position of current index in the grid.
"""
if not self.enabled:
if not self.grid_enabled:
return (0, 0)

n_row, n_column = self.actual_shape(nlayers)
shape, n_gridboxes = self.actual_shape(nlayers)

# Adjust for forward or reverse ordering
adj_i = nlayers - index - 1 if self.stride < 0 else index

adj_i = adj_i // abs(self.stride)
adj_i = adj_i % (n_row * n_column)
i_row = adj_i // n_column
i_column = adj_i % n_column
adj_i = adj_i % (shape[0] * shape[1])
i_row = adj_i // shape[1]
i_column = adj_i % shape[1]
return (i_row, i_column)
Loading