From fd0188b76e502d0d41c3c6b9063288fc07f74b39 Mon Sep 17 00:00:00 2001 From: AnniekStok Date: Fri, 15 Aug 2025 14:41:37 +0200 Subject: [PATCH 1/7] implement orthoview logic --- finn/track_application_menus/main_app.py | 17 ++ .../views/layers/track_labels.py | 86 ++++-- .../views/layers/track_points.py | 71 ++++- .../views/layers/tracks_layer_group.py | 4 + finn/track_data_views/views/ortho_views.py | 285 ++++++++++++++++++ pyproject.toml | 2 + 6 files changed, 421 insertions(+), 44 deletions(-) create mode 100644 finn/track_data_views/views/ortho_views.py diff --git a/finn/track_application_menus/main_app.py b/finn/track_application_menus/main_app.py index 6355db9d..b47f510d 100644 --- a/finn/track_application_menus/main_app.py +++ b/finn/track_application_menus/main_app.py @@ -5,6 +5,15 @@ import finn from finn.track_application_menus.menu_widget import MenuWidget +from finn.track_data_views.views.layers.track_labels import TrackLabels +from finn.track_data_views.views.layers.track_points import TrackPoints +from finn.track_data_views.views.ortho_views import ( + _get_manager, + paint_event_hook, + point_data_hook, + sync_filters, + track_layers_hook, +) from finn.track_data_views.views.tree_view.tree_widget import TreeWidget @@ -22,4 +31,12 @@ def __init__(self, viewer: finn.Viewer): layout = QVBoxLayout() layout.addWidget(self.menu_widget) + orth_view_manager = _get_manager(viewer) + orth_view_manager.register_layer_hook( + (TrackLabels, TrackPoints), track_layers_hook + ) + orth_view_manager.register_layer_hook((TrackLabels), paint_event_hook) + orth_view_manager.register_layer_hook((TrackPoints), point_data_hook) + orth_view_manager.set_sync_filters(sync_filters) + self.setLayout(layout) diff --git a/finn/track_data_views/views/layers/track_labels.py b/finn/track_data_views/views/layers/track_labels.py index 48925b39..ed18290b 100644 --- a/finn/track_data_views/views/layers/track_labels.py +++ b/finn/track_data_views/views/layers/track_labels.py @@ -1,11 +1,13 @@ from __future__ import annotations import random +import time from typing import TYPE_CHECKING import numpy as np import finn +from finn.layers import Labels from finn.utils import DirectLabelColormap from finn.utils.action_manager import action_manager from finn.utils.notifications import show_info, show_warning @@ -99,34 +101,56 @@ def __init__( self.bind_key("z")(self.tracks_viewer.undo) self.bind_key("r")(self.tracks_viewer.redo) - # Connect click events to node selection - @self.mouse_drag_callbacks.append - def click(_, event): - if ( - event.type == "mouse_press" - and self.mode == "pan_zoom" - and not ( - self.tracks_viewer.mode == "lineage" - and self.viewer.dims.ndisplay == 3 - ) - ): # disable selecting in lineage mode in 3D + # Listen to click, paint events and changing the selected label + self.mouse_drag_callbacks.append(self.click) + self.events.paint.connect(self._on_paint) + self.tracks_viewer.selected_nodes.list_updated.connect(self.update_selected_label) + self.events.selected_label.connect(self._ensure_valid_label) + self.events.mode.connect(self._check_mode) + self.viewer.dims.events.current_step.connect(self._ensure_valid_label) + + # Connect click events to node selection + def click(self, _, event): + if ( + event.type == "mouse_press" + and self.mode == "pan_zoom" + and not ( + self.tracks_viewer.mode == "lineage" and self.viewer.dims.ndisplay == 3 + ) + ): # disable selecting in lineage mode in 3D + # differentiate between click and drag + mouse_press_time = time.time() + dragged = False + yield + # on move + while event.type == "mouse_move": + dragged = True + yield + if dragged and time.time() - mouse_press_time < 0.5: + dragged = False # suppress micro drag events and treat them as click + # on release + if not dragged: label = self.get_value( event.position, view_direction=event.view_direction, dims_displayed=event.dims_displayed, world=True, ) - # check opacity (=visibility) in the colormap - if label is not None and label != 0 and self.colormap.map(label)[-1] != 0: - append = "Shift" in event.modifiers - self.tracks_viewer.selected_nodes.add(label, append) + self.process_click(event, label) - # Listen to paint events and changing the selected label - self.events.paint.connect(self._on_paint) - self.tracks_viewer.selected_nodes.list_updated.connect(self.update_selected_label) - self.events.selected_label.connect(self._ensure_valid_label) - self.events.mode.connect(self._check_mode) - self.viewer.dims.events.current_step.connect(self._ensure_valid_label) + def assign_new_label(self, event): + """Function for orthoviews to connect to so the 'm' event can be processed here""" + + new_label(self) + + def process_click(self, event: Event, label: int): + """Process the click event to update the selected nodes""" + + if ( + label is not None and label != 0 and self.colormap.map(label)[-1] != 0 + ): # check opacity (=visibility) in the colormap + append = "Shift" in event.modifiers + self.tracks_viewer.selected_nodes.add(label, append) def _get_colormap(self) -> DirectLabelColormap: """Get a DirectLabelColormap that maps node ids to their track ids, and then @@ -208,20 +232,26 @@ def _parse_paint_event(self, event_val): mask = concatenated_values == old_value indices = tuple(concatenated_indices[dim][mask] for dim in range(ndim)) time_points = np.unique(indices[0]) - for time in time_points: - time_mask = indices[0] == time + for time_point in time_points: + time_mask = indices[0] == time_point actions.append( (tuple(indices[dim][time_mask] for dim in range(ndim)), old_value) ) return new_value, actions - def _revert_paint(self, event): + def _revert_paint(self, _, source_layer: Labels | None = None): """Revert a paint event after it fails validation (no motile tracker Actions have - been created). This keeps the view synced with the backend data. + been created). If a source_layer is provided, the paint event will be reverted on + this layer (this is necessary for orthoviews). This keeps the view synced with + the backend data. """ - super().undo() - def _on_paint(self, event): + if source_layer is not None: + source_layer.undo() # revert on the orthoview + else: + super().undo() + + def _on_paint(self, event, source_layer: Labels | None = None): """Listen to the paint event and check which track_ids have changed""" with self.events.selected_label.blocker(): @@ -267,7 +297,7 @@ def _on_paint(self, event): " If you want to update the track id of the node, please edit the " "edges directly instead." ) - self._revert_paint(event) + self._revert_paint(event, source_layer) self.refresh() return self.tracks_viewer.tracks_controller.update_segmentations( diff --git a/finn/track_data_views/views/layers/track_points.py b/finn/track_data_views/views/layers/track_points.py index 411a17e9..50e181c3 100644 --- a/finn/track_data_views/views/layers/track_points.py +++ b/finn/track_data_views/views/layers/track_points.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +import time from typing import TYPE_CHECKING import numpy as np @@ -13,12 +14,18 @@ if TYPE_CHECKING: from finn.track_data_views.views_coordinator.tracks_viewer import TracksViewer +from psygnal import Signal + +from finn.utils.events import Event + class TrackPoints(finn.layers.Points): """Extended points layer that holds the track information and emits and responds to dynamics visualization signals """ + data_updated = Signal() + @property def _type_string(self) -> str: return ( @@ -77,17 +84,25 @@ def __init__( @self.mouse_drag_callbacks.append def click(layer, event): if event.type == "mouse_press": - # is the value passed from the click event? - point_index = layer.get_value( - event.position, - view_direction=event.view_direction, - dims_displayed=event.dims_displayed, - world=True, - ) - if point_index is not None: - node_id = self.nodes[point_index] - append = "Shift" in event.modifiers - self.tracks_viewer.selected_nodes.add(node_id, append) + # differentiate between click and drag + mouse_press_time = time.time() + dragged = False + yield + # on move + while event.type == "mouse_move": + dragged = True + yield + if dragged and time.time() - mouse_press_time < 0.5: + dragged = False # suppress micro drag events and treat them as click + if not dragged: + # is the value passed from the click event? + point_index = layer.get_value( + event.position, + view_direction=event.view_direction, + dims_displayed=event.dims_displayed, + world=True, + ) + self.process_point_click(point_index, event) # listen to updates of the data self.events.data.connect(self._update_data) @@ -101,11 +116,22 @@ def click(layer, event): # to update the nodes in self.tracks_viewer.selected_nodes self.selected_data.events.items_changed.connect(self._update_selection) + def process_point_click(self, point_index: int | None, event: Event): + """Select the clicked point(s)""" + + if point_index is None: + self.tracks_viewer.selected_nodes.reset() + else: + node_id = self.nodes[point_index] + append = "Shift" in event.modifiers + self.tracks_viewer.selected_nodes.add(node_id, append) + def set_point_size(self, size: int) -> None: """Sets a new default point size""" self.default_size = size - self._refresh() + self.size = self.default_size + self.border_color = self.border_color # emits border color event which triggers updating the sizes as well (size does not have its own event) def _refresh(self): """Refresh the data in the points layer""" @@ -121,7 +147,10 @@ def _refresh(self): self.tracks_viewer.tracks.graph.nodes[node][NodeAttr.TRACK_ID.value] for node in self.nodes ] + # this submits two events one where the action is 'ongoing' and one when it is finished self.data = self.tracks_viewer.tracks.get_positions(self.nodes, incl_time=True) + self.data_updated.emit() # emit update signal for the orthogonal views to connect to + self.symbol = self.get_symbols( self.tracks_viewer.tracks, self.tracks_viewer.symbolmap ) @@ -129,8 +158,10 @@ def _refresh(self): self.tracks_viewer.colormap.map(track_id) for track_id in track_ids ] self.properties = {"node_id": self.nodes, "track_id": track_ids} - self.size = self.default_size - self.border_color = [1, 1, 1, 1] + + with self.events.border_color.blocker(): # no need to submit events for this + self.size = self.default_size + self.border_color = [1, 1, 1, 1] self.events.data.connect( self._update_data @@ -227,8 +258,12 @@ def update_point_outline(self, visible: list[int] | str) -> None: self.shown[indices] = True # set border color for selected item - self.border_color = [1, 1, 1, 1] - self.size = self.default_size + with ( + self.events.border_color.blocker() + ): # block the event emitter here to not trigger update in orthogonal views + self.border_color = [1, 1, 1, 1] + with self.events.size.blocker(): + self.size = self.default_size for node in self.tracks_viewer.selected_nodes: index = self.node_index_dict[node] self.border_color[index] = ( @@ -238,4 +273,8 @@ def update_point_outline(self, visible: list[int] | str) -> None: 1, ) self.size[index] = math.ceil(self.default_size + 0.3 * self.default_size) + + # emit the event to trigger update in orthogonal views + self.border_color = self.border_color + self.size = self.size self.refresh() diff --git a/finn/track_data_views/views/layers/tracks_layer_group.py b/finn/track_data_views/views/layers/tracks_layer_group.py index 02c17065..2da92e43 100644 --- a/finn/track_data_views/views/layers/tracks_layer_group.py +++ b/finn/track_data_views/views/layers/tracks_layer_group.py @@ -148,6 +148,10 @@ def center_view(self, node): location[dim] + 0.5 ) # use the world location, since the 'step' in viewer.dims.range # already in world units + # Also update the step for the dims that are displayed, in order to sync with + # the orthogonal views + for dim in self.viewer.dims.displayed: + step[dim] = int(location[dim] + 0.5) self.viewer.dims.current_step = step # check whether the new coordinates are inside or outside the field of view, diff --git a/finn/track_data_views/views/ortho_views.py b/finn/track_data_views/views/ortho_views.py new file mode 100644 index 00000000..d3ff7633 --- /dev/null +++ b/finn/track_data_views/views/ortho_views.py @@ -0,0 +1,285 @@ +import importlib +import inspect +import sys +import time + +from finn.layers import Labels, Layer, Points, Shapes +from finn.track_data_views.views.layers.track_graph import TrackGraph +from finn.track_data_views.views.layers.track_labels import TrackLabels +from finn.track_data_views.views.layers.track_points import TrackPoints +from finn.utils.events import Event + + +def convert_napari_to_finn(napari_name: str, finn_name: str): + """Convert `napari_name` to `finn_name`, ensuring parent packages are linked too.""" + module = importlib.import_module(finn_name) + sys.modules[napari_name] = module + + # Attach to parent package if there is one + if "." in napari_name: + parent_name, child_name = napari_name.rsplit(".", 1) + parent_module = sys.modules.get(parent_name) + if parent_module is None: + # If parent wasn't imported yet, create a dummy package + import types + + parent_module = types.ModuleType(parent_name) + sys.modules[parent_name] = parent_module + setattr(parent_module, child_name, module) + + +# Map napari imports to finn equivalents +convert_napari_to_finn("napari", "finn") +convert_napari_to_finn("napari.components", "finn.components") +convert_napari_to_finn("napari.components.viewer_model", "finn.components.viewer_model") +convert_napari_to_finn("napari.layers", "finn.layers") +convert_napari_to_finn("napari.qt", "finn.qt") +convert_napari_to_finn("napari.utils", "finn.utils") +convert_napari_to_finn("napari.utils.events", "finn.utils.events") +convert_napari_to_finn("napari.utils.events.event", "finn.utils.events.event") + + +# redefinition of copy_layer function +def copy_layer(layer: Layer, name: str = ""): + if isinstance( + layer, TrackGraph + ): # instead of showing the tracks (not very useful on 3D data because they are + # collapsed to a single frame), use an empty shapes layer as substitute to ensure + # that the layer indices in the orthogonal viewer models match with those in the + # main viewer + res_layer = Shapes( + name=layer.name, + data=[], + ) + + elif isinstance(layer, TrackLabels): + res_layer = Labels( + data=layer.data, + name=layer.name, + opacity=layer.opacity, + scale=layer.scale, + colormap=layer.colormap, + ) + elif isinstance(layer, TrackPoints): + res_layer = Points( + data=layer.data, + name=layer.name, + symbol=layer.symbol, + face_color=layer.face_color, + size=layer.size, + properties=layer.properties, + border_color=layer.border_color, + scale=layer.scale, + blending="translucent_no_depth", + ) + else: + res_layer = Layer.create(*layer.as_layer_data_tuple()) + + res_layer.metadata["viewer_name"] = name + return res_layer + +# do not let ruff move these import statements! +from napari_orthogonal_views.ortho_view_manager import _get_manager +import napari_orthogonal_views.ortho_view_widget as ov_widget + +ov_widget.copy_layer = copy_layer # replace the copy layer with the customized version +# defined here + +# Define custom sync_filters. By default, all properties are synced forwards and backwards +# between the original layer and its derived copy. However, for Tracks Layers we need +# finer control over some syncing events, because they may have additional attached events +# that shouldn't be triggered due to reverse syncing, or because we need to capture the +# event and process it separately before it gets synced. A dictionary can be defined here +# to disable specific Layer properties for forward or reverse syncing. + + +def get_property_names_from_class(layer_cls): + """Return all property names for a Layer class.""" + res = [] + for name, obj in inspect.getmembers(layer_cls): + # must be a property with a setter + if isinstance(obj, property) and obj.fset is not None: + # skip special or non-sync properties + if name in ("thumbnail", "name"): + continue + res.append(name) + return res + + +sync_filters = { + TrackGraph: { + "forward_exclude": "*", # disable all forward sync (layer is not shown) + "reverse_exclude": "*", # disable all reverse sync + }, + TrackPoints: { + "forward_exclude": {"data"}, # we will sync data separately on TrackPoints as we + # need finer control + "reverse_exclude": set(get_property_names_from_class(Points)) - {"mode"}, + }, + TrackLabels: { + "reverse_exclude": set(get_property_names_from_class(Labels)) + - { + "mode", + "selected_label", + "n_edit_dimensions", + "brush_size", + } # Let TrackLabels handle these properties on its own because it is listening to + # them and we do not want to overwrite through reverse syncing. + }, +} + + +# Define special functions to allow specific behavior on special layer types (TrackLabels, +# and TrackPoints) +# +def point_data_hook(orig_layer: TrackPoints, copied_layer: Points) -> None: + """Hook to connect to sync points data and visualization between original and copied + Points layers. + + Args: + orig_layer (TrackPoints): TracksLabels layer from which the copied layer is + derived. + copied_layer (Points): Points equivalent of the TracksPoints layer. + """ + + # Sync the shown points and their size, as it is not synced by default. We bind to the + # border_color event as this this is emitted when we modify shown points and point + # size on the TrackPoints layer. + def sync_shown_points(orig_layer: TrackPoints, copied_layer: Points) -> None: + """Sync the visible points between original TrackPoints layer and Points layers + in ViewerModel instances (this is not a synced property)""" + + with copied_layer.events.blocker_all(): + copied_layer.size = orig_layer.size + copied_layer.shown = orig_layer.shown + + copied_layer.refresh() + + def shown_points_wrapper(event): + return sync_shown_points(orig_layer, copied_layer) + + orig_layer.events.border_color.connect(shown_points_wrapper) + + # Receive data updates from the original layer + def receive_data(orig_layer: TrackPoints, copied_layer: Points) -> None: + """Respond to signal from the original layer, to update the data""" + + copied_layer.events.data.disconnect(copied_layer._sync_data_wrapper) + copied_layer.data = orig_layer.data + copied_layer.events.data.connect(copied_layer._sync_data_wrapper) + + def receive_data_wrapper(): + return receive_data(orig_layer, copied_layer) + + orig_layer.data_updated.connect(receive_data_wrapper) + + # Sync the event that is emitted when a point is moved or deleted. We need to capture + # it on the original layer to process it there, and potentially undo it if it was an + # invalid action (we have no way to judge that on a normal Points layer). + def sync_data_event( + orig_layer: TrackPoints, copied_layer: Points, event: Event + ) -> None: + """Send the event that is emitted when a point is moved or deleted to the original + layer""" + + if hasattr(event, "action") and event.action in ("added", "changed", "removed"): + with orig_layer.events.blocker_all(): # try to suppress updating visibility + orig_layer.selected_data = ( + copied_layer.selected_data + ) # make sure the same data is selected + orig_layer._update_data(event) + + def sync_data_wrapper(event): + return sync_data_event(orig_layer, copied_layer, event) + + copied_layer._sync_data_wrapper = sync_data_wrapper + copied_layer.events.data.connect(sync_data_wrapper) + + +def paint_event_hook(orig_layer: TrackLabels, copied_layer: Labels) -> None: + """Hook to connect to paint events and process them on the original TracksLabels + layer. + + Args: + orig_layer (TrackLabels): TracksLabels layer from which the copied layer is + derived. + copied_layer (Labels): Labels equivalent of the TracksLabels layer. Instead of + processing paint actions on this copy, we want to send them to the original + layer and process them there. + """ + + def sync_paint(orig_layer: TrackLabels, copied_layer: Labels, event: Event): + """Process paint event on original TrackLabels instance.""" + + orig_layer._on_paint(event, copied_layer) + + def paint_wrapper(event: Event): + """Wrap paint event and send to original layer.""" + + return sync_paint(orig_layer, copied_layer, event) + + copied_layer.events.paint.connect(paint_wrapper) + + +def track_layers_hook( + orig_layer: TrackLabels | TrackPoints, copied_layer: Labels | Points +) -> None: + """Hook to capture click events on TrackLabels and TrackPoints derived Labels and + Points layers, and forward them to their original layer. Also, register key binds + for view mode, undo & redo to copied layer, that call functions on the original layer. + + Args: + orig_layer (TrackLabels | TrackPoints): TracksLabels or TrackPoints layer from + which the copied layer is derived. + copied_layer (Labels | Points): Labels or Points equivalent of the TracksLabels + or TrackPoints layer. + """ + + # define the click behavior the layer should respond to + def click( + orig_layer: TrackLabels | TrackPoints, layer: Labels | Points, event: Event + ): + if layer.mode == "pan_zoom": + mouse_press_time = time.time() + dragged = False + yield # start + while event.type == "mouse_move": + dragged = True + yield + if dragged and time.time() - mouse_press_time < 0.5: + dragged = False # treat micro drags as clicks + + if not dragged: + if isinstance(layer, Labels): + label = layer.get_value( + event.position, + view_direction=event.view_direction, + dims_displayed=event.dims_displayed, + world=True, + ) + orig_layer.process_click(event, label) + + if isinstance(layer, Points): + point_index = layer.get_value( + event.position, + view_direction=event.view_direction, + dims_displayed=event.dims_displayed, + world=True, + ) + orig_layer.process_point_click(point_index, event) + + # Wrap and attach click callback + def click_wrapper(layer, event): + return click(orig_layer, layer, event) + + copied_layer.mouse_drag_callbacks.append(click_wrapper) + + # Bind keys to original layer TracksViewer + copied_layer.bind_key("q")(orig_layer.tracks_viewer.toggle_display_mode) + copied_layer.bind_key("z")(orig_layer.tracks_viewer.undo) + copied_layer.bind_key("r")(orig_layer.tracks_viewer.redo) + copied_layer.bind_key("d")(orig_layer.tracks_viewer.delete_node) + copied_layer.bind_key("a")(orig_layer.tracks_viewer.create_edge) + copied_layer.bind_key("b")(orig_layer.tracks_viewer.delete_edge) + if isinstance(orig_layer, TrackLabels): + copied_layer.bind_key("m")(orig_layer.assign_new_label) diff --git a/pyproject.toml b/pyproject.toml index d057a77c..568082c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,8 @@ dependencies = [ "fonticon-fontawesome6", "zarr<3", "imagecodecs", # for unzipping sample data + "napari-orthogonal-views @ git+https://github.com/AnniekStok/napari-orthogonal-views.git@main" + ] dynamic = [ "version", From 92cac0fcbf1f12e327c7757d28c1ee57f6d89d99 Mon Sep 17 00:00:00 2001 From: AnniekStok Date: Mon, 8 Sep 2025 10:01:18 +0200 Subject: [PATCH 2/7] skip center_cross_on_mouse in the viewer test --- finn/_tests/test_viewer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/finn/_tests/test_viewer.py b/finn/_tests/test_viewer.py index 46fa1e87..738b4007 100644 --- a/finn/_tests/test_viewer.py +++ b/finn/_tests/test_viewer.py @@ -33,6 +33,15 @@ def _assert_shortcuts_exist_for_each_action(type_): actions = _get_provider_actions(type_) shortcuts = {name.partition(":")[-1] for name in get_settings().shortcuts.shortcuts} shortcuts.update(func.__name__ for func in type_.class_keymap.values()) + + actions_to_skip = { + "center_cross_on_mouse" + } # skip this action as we only bind it if the cross widget is imported + + for action in actions: + if action.__name__ in actions_to_skip: + continue # Skip this action + for action in actions: assert action.__name__ in shortcuts, ( f"missing shortcut for action '{action.__name__}' on '{type_.__name__}' is missing" From 429e40f8db1e197f0de4dd101114fc2cb4a0fed7 Mon Sep 17 00:00:00 2001 From: AnniekStok Date: Mon, 8 Sep 2025 11:06:27 +0200 Subject: [PATCH 3/7] undo action manager skip --- finn/_tests/test_viewer.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/finn/_tests/test_viewer.py b/finn/_tests/test_viewer.py index 738b4007..46fa1e87 100644 --- a/finn/_tests/test_viewer.py +++ b/finn/_tests/test_viewer.py @@ -33,15 +33,6 @@ def _assert_shortcuts_exist_for_each_action(type_): actions = _get_provider_actions(type_) shortcuts = {name.partition(":")[-1] for name in get_settings().shortcuts.shortcuts} shortcuts.update(func.__name__ for func in type_.class_keymap.values()) - - actions_to_skip = { - "center_cross_on_mouse" - } # skip this action as we only bind it if the cross widget is imported - - for action in actions: - if action.__name__ in actions_to_skip: - continue # Skip this action - for action in actions: assert action.__name__ in shortcuts, ( f"missing shortcut for action '{action.__name__}' on '{type_.__name__}' is missing" From 245ca7622476d85bbe7db569ee47c4d7f5f46dfe Mon Sep 17 00:00:00 2001 From: AnniekStok Date: Mon, 8 Sep 2025 14:24:32 +0200 Subject: [PATCH 4/7] try adding -p no:napari to the test setup --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 568082c9..2d1e0a20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -391,7 +391,7 @@ xarray = "xr" [tool.pytest.ini_options] # These follow standard library warnings filters syntax. See more here: # https://docs.python.org/3/library/warnings.html#describing-warning-filters -addopts = ["--maxfail=5", "--durations=10", "-ra", "--strict-markers", "--strict-config"] +addopts = ["--maxfail=5", "--durations=10", "-ra", "--strict-markers", "--strict-config", "-p no:napari"] console_output_style = "count" minversion = "8" # log_cli_level = "INFO" From 9bf19abae3aae1b0a2ef557038095ab7097dc47a Mon Sep 17 00:00:00 2001 From: AnniekStok Date: Thu, 11 Sep 2025 17:02:57 +0200 Subject: [PATCH 5/7] tag specific orthoview version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2d1e0a20..1345640d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dependencies = [ "fonticon-fontawesome6", "zarr<3", "imagecodecs", # for unzipping sample data - "napari-orthogonal-views @ git+https://github.com/AnniekStok/napari-orthogonal-views.git@main" + "napari-orthogonal-views @ git+https://github.com/AnniekStok/napari-orthogonal-views.git@v0.0.1" ] dynamic = [ From 3ef2485b637c8f38e4a3938f7a1b7883b880d99a Mon Sep 17 00:00:00 2001 From: AnniekStok Date: Thu, 11 Sep 2025 17:05:45 +0200 Subject: [PATCH 6/7] disable painting in the time dimension in the orthoviews --- finn/track_data_views/views/ortho_views.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/finn/track_data_views/views/ortho_views.py b/finn/track_data_views/views/ortho_views.py index d3ff7633..49615694 100644 --- a/finn/track_data_views/views/ortho_views.py +++ b/finn/track_data_views/views/ortho_views.py @@ -8,6 +8,7 @@ from finn.track_data_views.views.layers.track_labels import TrackLabels from finn.track_data_views.views.layers.track_points import TrackPoints from finn.utils.events import Event +from finn.utils.notifications import show_info def convert_napari_to_finn(napari_name: str, finn_name: str): @@ -32,11 +33,14 @@ def convert_napari_to_finn(napari_name: str, finn_name: str): convert_napari_to_finn("napari", "finn") convert_napari_to_finn("napari.components", "finn.components") convert_napari_to_finn("napari.components.viewer_model", "finn.components.viewer_model") +convert_napari_to_finn("napari.components.overlays.base", "finn.components.overlays.base") convert_napari_to_finn("napari.layers", "finn.layers") convert_napari_to_finn("napari.qt", "finn.qt") convert_napari_to_finn("napari.utils", "finn.utils") convert_napari_to_finn("napari.utils.events", "finn.utils.events") convert_napari_to_finn("napari.utils.events.event", "finn.utils.events.event") +convert_napari_to_finn("napari._vispy.overlays.base", "finn._vispy.overlays.base") +convert_napari_to_finn("napari._vispy.utils.visual", "finn._vispy.utils.visual") # redefinition of copy_layer function @@ -78,9 +82,10 @@ def copy_layer(layer: Layer, name: str = ""): res_layer.metadata["viewer_name"] = name return res_layer + # do not let ruff move these import statements! -from napari_orthogonal_views.ortho_view_manager import _get_manager -import napari_orthogonal_views.ortho_view_widget as ov_widget +from napari_orthogonal_views.ortho_view_manager import _get_manager # noqa +import napari_orthogonal_views.ortho_view_widget as ov_widget # noqa ov_widget.copy_layer = copy_layer # replace the copy layer with the customized version # defined here @@ -211,7 +216,12 @@ def paint_event_hook(orig_layer: TrackLabels, copied_layer: Labels) -> None: def sync_paint(orig_layer: TrackLabels, copied_layer: Labels, event: Event): """Process paint event on original TrackLabels instance.""" - orig_layer._on_paint(event, copied_layer) + if copied_layer.data.ndim > 3: + orig_layer._on_paint(event, copied_layer) + else: + show_info("Painting in the time dimension is not supported") + orig_layer._revert_paint(event, copied_layer) + orig_layer.refresh() def paint_wrapper(event: Event): """Wrap paint event and send to original layer.""" From c4123632717d00bf4867c91ead5d76848fbbbe45 Mon Sep 17 00:00:00 2001 From: AnniekStok Date: Fri, 12 Sep 2025 13:08:38 +0200 Subject: [PATCH 7/7] tag specific commit on napari-orthogonal-views that auto-activates the canvas the mouse hovers over --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1345640d..4ca05043 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dependencies = [ "fonticon-fontawesome6", "zarr<3", "imagecodecs", # for unzipping sample data - "napari-orthogonal-views @ git+https://github.com/AnniekStok/napari-orthogonal-views.git@v0.0.1" + "napari-orthogonal-views @ git+https://github.com/AnniekStok/napari-orthogonal-views.git@db1ac78ec240bfd6657a319828d7d0c7be712ebd" ] dynamic = [