Source code for phenotypic.tools_.napari_._point_picker_widget
"""Blocking napari-based point picker widget."""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
if TYPE_CHECKING:
import napari
class _PointPickerPanel:
"""Dock widget providing point list management and confirmation controls.
Inherits from ``QWidget`` at runtime (via ``__new__``) so that ``qtpy``
is not imported at module level.
Args:
viewer: The napari viewer instance.
points_layer: The napari Points layer to manage.
max_points: Maximum number of points allowed. None means unlimited.
"""
def __new__(cls, *args, **kwargs): # noqa: ARG003
from qtpy.QtWidgets import QWidget
# Dynamically create a subclass that has QWidget in its MRO.
if not issubclass(cls, QWidget):
cls.__bases__ = (QWidget,)
instance = QWidget.__new__(cls)
return instance
def __init__(
self,
viewer: napari.Viewer,
points_layer,
*,
max_points: int | None = None,
) -> None:
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QHBoxLayout,
QLabel,
QListWidget,
QPushButton,
QVBoxLayout,
QWidget,
)
QWidget.__init__(self) # type: ignore[arg-type]
self._viewer = viewer
self._points_layer = points_layer
self._max_points = max_points
self._updating = False
self.confirmed_points: np.ndarray = np.empty((0, 2))
layout = QVBoxLayout(self) # type: ignore[call-overload]
layout.setContentsMargins(4, 4, 4, 4)
header = QLabel("Selected Points (y, x)")
header.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(header)
self._list_widget = QListWidget()
layout.addWidget(self._list_widget)
btn_row = QHBoxLayout()
self._delete_btn = QPushButton("Delete Selected")
self._clear_btn = QPushButton("Clear All")
btn_row.addWidget(self._delete_btn)
btn_row.addWidget(self._clear_btn)
layout.addLayout(btn_row)
self._confirm_btn = QPushButton("Confirm")
layout.addWidget(self._confirm_btn)
self._delete_btn.clicked.connect(self._delete_selected)
self._clear_btn.clicked.connect(self._clear_all)
self._confirm_btn.clicked.connect(self._confirm)
self._points_layer.events.data.connect(self._on_data_changed)
def _on_data_changed(self, event=None) -> None: # noqa: ARG001
"""Rebuild list widget from the current points layer data."""
if self._updating:
return
data = self._points_layer.data
if self._max_points is not None and len(data) > self._max_points:
self._updating = True
self._points_layer.data = data[-self._max_points :]
self._updating = False
data = self._points_layer.data
self._list_widget.clear()
for row in data:
self._list_widget.addItem(f"({row[0]:.1f}, {row[1]:.1f})")
def _delete_selected(self) -> None:
"""Remove the currently selected point from the layer."""
row = self._list_widget.currentRow()
if row < 0:
return
self._updating = True
self._points_layer.data = np.delete(self._points_layer.data, row, axis=0)
self._updating = False
self._on_data_changed()
def _clear_all(self) -> None:
"""Remove all points from the layer."""
self._updating = True
self._points_layer.data = np.empty((0, 2))
self._updating = False
self._on_data_changed()
def _confirm(self) -> None:
"""Store the current points and close the viewer."""
self.confirmed_points = self._points_layer.data.copy()
self._viewer.close()