Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add minimal Points plan view #316

Merged
merged 48 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8c5cf47
feat: multi point plan useq widgets
fdrgsp Jul 7, 2024
8247b49
pull out pyproject
tlambert03 Jul 7, 2024
a249d64
pyproject
fdrgsp Jul 7, 2024
1cfd9c8
cleanup grid row
tlambert03 Jul 7, 2024
0256cca
cleanup
tlambert03 Jul 7, 2024
f6544a7
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Jul 7, 2024
252cb2b
tests
tlambert03 Jul 7, 2024
f36d29d
lint
tlambert03 Jul 7, 2024
9ed073e
bump
tlambert03 Jul 7, 2024
863e2f4
fov-widget
fdrgsp Jul 7, 2024
c0965e6
wip
tlambert03 Jul 7, 2024
99c037a
Merge branch 'main' into points-plan-selector
tlambert03 Jul 7, 2024
96d20d3
finish
tlambert03 Jul 7, 2024
2a04aa2
rm x
tlambert03 Jul 7, 2024
ed7b0f4
remove hcs
tlambert03 Jul 7, 2024
fc3f7f2
test
tlambert03 Jul 7, 2024
cae38e8
lint
tlambert03 Jul 7, 2024
a05ef2f
more test
tlambert03 Jul 7, 2024
a3f7023
style
tlambert03 Jul 7, 2024
60e288a
typing
tlambert03 Jul 7, 2024
4937860
rename
tlambert03 Jul 7, 2024
53ba0d8
lint
tlambert03 Jul 7, 2024
2c33634
fix again you dummy
tlambert03 Jul 7, 2024
9a1ba8d
fov-widget-selector
fdrgsp Jul 7, 2024
e346dbc
wip
tlambert03 Jul 7, 2024
596ae15
demo
tlambert03 Jul 8, 2024
2f81806
init files
tlambert03 Jul 8, 2024
a3d81fa
Merge branch 'main' into points-plan-view
tlambert03 Jul 8, 2024
6ebf75e
fix import
tlambert03 Jul 8, 2024
04673ce
chore: Update FOVSelectorWidget to disallow overlap in random points
tlambert03 Jul 8, 2024
09fddaa
minor
tlambert03 Jul 8, 2024
84bb4c2
add connecting line
tlambert03 Jul 8, 2024
052b8ff
Merge branch 'main' into points-plan-view
tlambert03 Jul 8, 2024
2c51f2a
click on point
tlambert03 Jul 9, 2024
5f06786
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Jul 9, 2024
aed76e0
lint
tlambert03 Jul 9, 2024
bcc0651
Merge branch 'points-plan-view' of https://github.com/tlambert03/pymm…
tlambert03 Jul 9, 2024
93c3662
fixup
tlambert03 Jul 9, 2024
7382ad8
deal with None
tlambert03 Jul 9, 2024
06846ba
cleanup
tlambert03 Jul 10, 2024
dc3707a
fix test
tlambert03 Jul 10, 2024
e76fd15
more fov fixes
tlambert03 Jul 10, 2024
5e9b6d6
move to useq
tlambert03 Jul 10, 2024
480cbdd
add tests
tlambert03 Jul 10, 2024
7e8a244
tighten
tlambert03 Jul 10, 2024
2718bdb
fix test
tlambert03 Jul 10, 2024
6d20fed
fix deprecation
tlambert03 Jul 10, 2024
e0336e3
docs
tlambert03 Jul 10, 2024
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: 11 additions & 0 deletions examples/hcs_temp/fov_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from qtpy.QtWidgets import QApplication
from useq import RandomPoints

from pymmcore_widgets.hcs._fov_widget._fov_widget import FOVSelectorWidget

app = QApplication([])

fs = FOVSelectorWidget(RandomPoints(num_points=80, allow_overlap=False))
fs.show()

app.exec()
1 change: 1 addition & 0 deletions src/pymmcore_widgets/hcs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""High content screening widgets for PyMMCore."""
Empty file.
59 changes: 59 additions & 0 deletions src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

Check warning on line 1 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L1

Added line #L1 was not covered by tests

import useq
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QHBoxLayout, QWidget

Check warning on line 5 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L3-L5

Added lines #L3 - L5 were not covered by tests

from pymmcore_widgets.useq_widgets.points_plans import RelativePointPlanSelector

Check warning on line 7 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L7

Added line #L7 was not covered by tests

from ._well_graphics_view import WellView

Check warning on line 9 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L9

Added line #L9 was not covered by tests


class FOVSelectorWidget(QWidget):

Check warning on line 12 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L12

Added line #L12 was not covered by tests
"""Widget to select the FOVVs per well of the plate."""

valueChanged = Signal(object)

Check warning on line 15 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L15

Added line #L15 was not covered by tests

def __init__(

Check warning on line 17 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L17

Added line #L17 was not covered by tests
self,
plan: useq.RelativeMultiPointPlan | None = None,
parent: QWidget | None = None,
) -> None:
super().__init__(parent=parent)

Check warning on line 22 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L22

Added line #L22 was not covered by tests

self.selector = RelativePointPlanSelector()

Check warning on line 24 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L24

Added line #L24 was not covered by tests
# graphics scene to draw the well and the fovs
self.well_view = WellView()

Check warning on line 26 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L26

Added line #L26 was not covered by tests

# main
layout = QHBoxLayout(self)
layout.addWidget(self.selector)
layout.addWidget(self.well_view)

Check warning on line 31 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L29-L31

Added lines #L29 - L31 were not covered by tests

# connect
self.selector.valueChanged.connect(self._on_selector_value_changed)
self.well_view.maxPointsDetected.connect(self._on_view_max_points_detected)
self.well_view.positionClicked.connect(self._on_view_position_clicked)

Check warning on line 36 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L34-L36

Added lines #L34 - L36 were not covered by tests

if plan is not None:
self.setValue(plan)

Check warning on line 39 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L38-L39

Added lines #L38 - L39 were not covered by tests

def value(self) -> useq.RelativeMultiPointPlan:
return self.selector.value()

Check warning on line 42 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L41-L42

Added lines #L41 - L42 were not covered by tests

def setValue(self, plan: useq.RelativeMultiPointPlan) -> None:
self.selector.setValue(plan)

Check warning on line 45 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L44-L45

Added lines #L44 - L45 were not covered by tests

def _on_selector_value_changed(self, value: useq.RelativeMultiPointPlan) -> None:
self.well_view.setPointsPlan(value)
self.valueChanged.emit(value)

Check warning on line 49 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L47-L49

Added lines #L47 - L49 were not covered by tests

def _on_view_max_points_detected(self, value: int) -> None:
self.selector.random_points_wdg.num_points.setValue(value)

Check warning on line 52 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L51-L52

Added lines #L51 - L52 were not covered by tests

def _on_view_position_clicked(

Check warning on line 54 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L54

Added line #L54 was not covered by tests
self, index: int, position: useq.RelativePosition
) -> None:
if self.selector.active_plan_type is useq.RandomPoints:
print(position, type(position))
self.selector.random_points_wdg.start_at = position.replace(name=None)

Check warning on line 59 in src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_fov_widget.py#L57-L59

Added lines #L57 - L59 were not covered by tests
tlambert03 marked this conversation as resolved.
Show resolved Hide resolved
178 changes: 178 additions & 0 deletions src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
from __future__ import annotations

Check warning on line 1 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L1

Added line #L1 was not covered by tests

import warnings
from typing import TYPE_CHECKING

Check warning on line 4 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L3-L4

Added lines #L3 - L4 were not covered by tests

import useq
from qtpy.QtCore import QRectF, QSize, Qt, Signal
from qtpy.QtGui import QColor, QPainter, QPen
from qtpy.QtWidgets import QGraphicsItem, QGraphicsScene, QWidget
from useq import Shape

Check warning on line 10 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L6-L10

Added lines #L6 - L10 were not covered by tests

from pymmcore_widgets.hcs._util import ResizingGraphicsView

Check warning on line 12 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L12

Added line #L12 was not covered by tests

if TYPE_CHECKING:
from PyQt6.QtGui import QMouseEvent

DATA_POSITION = 1
DATA_POSITION_INDEX = 2

Check warning on line 18 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L17-L18

Added lines #L17 - L18 were not covered by tests


class WellView(ResizingGraphicsView):

Check warning on line 21 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L21

Added line #L21 was not covered by tests
"""Graphics view to draw a well and the FOVs."""

maxPointsDetected = Signal(int)
positionClicked = Signal(int, object)

Check warning on line 25 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L24-L25

Added lines #L24 - L25 were not covered by tests

def __init__(self, parent: QWidget | None = None) -> None:
self._scene = QGraphicsScene()

Check warning on line 28 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L27-L28

Added lines #L27 - L28 were not covered by tests

super().__init__(self._scene, parent)
self.setStyleSheet("background:grey; border-radius: 5px;")
self.setRenderHints(

Check warning on line 32 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L30-L32

Added lines #L30 - L32 were not covered by tests
QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform
)

# the scene coordinates are all real-world coordinates, in µm
# with the origin at the center of the view (0, 0)
self._well_width_um: float | None = 6000
self._well_height_um: float | None = 6000
self._fov_width_um: float = 400
self._fov_height_um: float = 340
self._is_circular: bool = False

Check warning on line 42 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L38-L42

Added lines #L38 - L42 were not covered by tests

# the item that draws the outline of the entire well area
self._outline_item: QGraphicsItem | None = None

Check warning on line 45 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L45

Added line #L45 was not covered by tests
# all of the rectangles representing the FOVs
self._fov_items: list[QGraphicsItem] = []

Check warning on line 47 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L47

Added line #L47 was not covered by tests

self.setMinimumSize(250, 250)

Check warning on line 49 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L49

Added line #L49 was not covered by tests

def sizeHint(self) -> QSize:
return QSize(500, 500)

Check warning on line 52 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L51-L52

Added lines #L51 - L52 were not covered by tests

def setPointsPlan(self, plan: useq.RelativeMultiPointPlan) -> None:

Check warning on line 54 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L54

Added line #L54 was not covered by tests
"""Set the plan to use to draw the FOVs."""
plandict = plan.model_dump(exclude_none=True)

Check warning on line 56 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L56

Added line #L56 was not covered by tests
# use our fov values if the plan doesn't have them
plandict.setdefault("fov_width", self._fov_width_um)
plandict.setdefault("fov_height", self._fov_height_um)
plan = plan.model_construct(**plandict)

Check warning on line 60 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L58-L60

Added lines #L58 - L60 were not covered by tests

self._fov_width_um = plan.fov_width
self._fov_height_um = plan.fov_height
if isinstance(plan, useq.RandomPoints):
self._is_circular = plan.shape == Shape.ELLIPSE

Check warning on line 65 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L62-L65

Added lines #L62 - L65 were not covered by tests

# WELL OUTLINE
self._draw_outline()

Check warning on line 68 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L68

Added line #L68 was not covered by tests

# DRAW FOVS
self._draw_fovs(plan)

Check warning on line 71 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L71

Added line #L71 was not covered by tests

def mousePressEvent(self, event: QMouseEvent | None) -> None:
if event is None:
return
scene_pos = self.mapToScene(event.pos())
items = self.scene().items(scene_pos)
for item in items:
if idx := item.data(DATA_POSITION_INDEX):
self.positionClicked.emit(idx, item.data(DATA_POSITION))
break

Check warning on line 81 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L73-L81

Added lines #L73 - L81 were not covered by tests

def setWellSize(self, width_mm: float | None, height_mm: float | None) -> None:

Check warning on line 83 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L83

Added line #L83 was not covered by tests
"""Set the well size width and height in mm."""
self._well_width_um = (width_mm * 1000) if width_mm else None
self._well_height_um = (height_mm * 1000) if height_mm else None

Check warning on line 86 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L85-L86

Added lines #L85 - L86 were not covered by tests

def _well_rect(self) -> QRectF:

Check warning on line 88 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L88

Added line #L88 was not covered by tests
"""Return the QRectF of the well area."""
if not self._well_width_um or not self._well_height_um:
return QRectF()
return QRectF(

Check warning on line 92 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L90-L92

Added lines #L90 - L92 were not covered by tests
-self._well_width_um / 2,
-self._well_height_um / 2,
self._well_width_um,
self._well_height_um,
)

def _draw_outline(self) -> None:

Check warning on line 99 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L99

Added line #L99 was not covered by tests
"""Draw the outline of the well area."""
if self._outline_item:
self._scene.removeItem(self._outline_item)

Check warning on line 102 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L101-L102

Added lines #L101 - L102 were not covered by tests

pen = QPen(QColor(Qt.GlobalColor.green))
pen.setWidth(self._scaled_pen_size())
if self._is_circular:
self._outline_item = self._scene.addEllipse(self._well_rect(), pen=pen)

Check warning on line 107 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L104-L107

Added lines #L104 - L107 were not covered by tests
else:
self._outline_item = self._scene.addRect(self._well_rect(), pen=pen)

Check warning on line 109 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L109

Added line #L109 was not covered by tests

def _draw_fovs(self, plan: useq.RelativeMultiPointPlan) -> None:

Check warning on line 111 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L111

Added line #L111 was not covered by tests
"""Draw the fovs in the scene as rectangles."""
# delete existing FOVs
while self._fov_items:
self._scene.removeItem(self._fov_items.pop())

Check warning on line 115 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L114-L115

Added lines #L114 - L115 were not covered by tests

half_fov_width = self._fov_width_um / 2
half_fov_height = self._fov_height_um / 2

Check warning on line 118 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L117-L118

Added lines #L117 - L118 were not covered by tests

# constrain random points to our own well size, regardless of the plan settings
# TODO: emit a warning here?
if isinstance(plan, useq.RandomPoints):
kwargs = {}
if self._well_width_um:
kwargs["max_width"] = self._well_width_um - half_fov_width * 1.4
if self._well_height_um:
kwargs["max_height"] = self._well_height_um - half_fov_height * 1.4
plan = plan.replace(**kwargs)

Check warning on line 128 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L122-L128

Added lines #L122 - L128 were not covered by tests

pen = QPen(Qt.GlobalColor.white)
pen.setWidth(self._scaled_pen_size())
line_pen = QPen(QColor(0, 0, 0, 100))
line_pen.setWidth(int(self._scaled_pen_size() // 1.5))

Check warning on line 133 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L130-L133

Added lines #L130 - L133 were not covered by tests

# iterate over the plan greedily, catching any warnings
# and then alert the model if we didn't get all the points
# XXX: I'm not sure about this pattern... feels like the model should be
# able to handle this itself, but this is perhaps the only place we actually
# iterate over the plan
with warnings.catch_warnings():
warnings.simplefilter("ignore")
points = list(plan)
if len(points) < getattr(plan, "num_points", 0):
self.maxPointsDetected.emit(len(points))

Check warning on line 144 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L140-L144

Added lines #L140 - L144 were not covered by tests
tlambert03 marked this conversation as resolved.
Show resolved Hide resolved

# draw the FOVs, and a connecting line
last_p: useq.RelativePosition | None = None
for i, pos in enumerate(points):
if i == 0:
pen.setColor(QColor(Qt.GlobalColor.black))

Check warning on line 150 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L147-L150

Added lines #L147 - L150 were not covered by tests
else:
pen.setColor(QColor(Qt.GlobalColor.white))
item = self._scene.addRect(

Check warning on line 153 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L152-L153

Added lines #L152 - L153 were not covered by tests
pos.x - half_fov_width,
pos.y - half_fov_height,
self._fov_width_um,
self._fov_height_um,
pen,
)
item.setData(DATA_POSITION, pos)
item.setData(DATA_POSITION_INDEX, i)
if item:
item.setZValue(100 if i == 0 else 0)
self._fov_items.append(item)

Check warning on line 164 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L160-L164

Added lines #L160 - L164 were not covered by tests
# draw a line from the last point to this one
if i > 0 and last_p:
self._fov_items.append(

Check warning on line 167 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L166-L167

Added lines #L166 - L167 were not covered by tests
self._scene.addLine(last_p.x, last_p.y, pos.x, pos.y, line_pen)
)
last_p = pos

Check warning on line 170 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L170

Added line #L170 was not covered by tests

def _scaled_pen_size(self) -> int:

Check warning on line 172 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L172

Added line #L172 was not covered by tests
# pick a pen size appropriate for the scene scale
# we might also want to scale this based on the sceneRect...
# and it's possible this needs to be rescaled on resize
if self._well_width_um:
return int(self._well_width_um / 150)
return int(self.sceneRect().width() / 150)

Check warning on line 178 in src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_fov_widget/_well_graphics_view.py#L176-L178

Added lines #L176 - L178 were not covered by tests
26 changes: 26 additions & 0 deletions src/pymmcore_widgets/hcs/_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

Check warning on line 1 in src/pymmcore_widgets/hcs/_util.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_util.py#L1

Added line #L1 was not covered by tests

from typing import TYPE_CHECKING

Check warning on line 3 in src/pymmcore_widgets/hcs/_util.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_util.py#L3

Added line #L3 was not covered by tests

from qtpy.QtCore import QMarginsF, Qt
from qtpy.QtWidgets import QGraphicsScene, QGraphicsView, QWidget

Check warning on line 6 in src/pymmcore_widgets/hcs/_util.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_util.py#L5-L6

Added lines #L5 - L6 were not covered by tests

if TYPE_CHECKING:
from qtpy.QtGui import QResizeEvent


class ResizingGraphicsView(QGraphicsView):

Check warning on line 12 in src/pymmcore_widgets/hcs/_util.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_util.py#L12

Added line #L12 was not covered by tests
"""A QGraphicsView that resizes the scene to fit the view."""

def __init__(self, scene: QGraphicsScene, parent: QWidget | None = None) -> None:
super().__init__(scene, parent)
self.padding = 0.05 # fraction of the bounding rect

Check warning on line 17 in src/pymmcore_widgets/hcs/_util.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_util.py#L15-L17

Added lines #L15 - L17 were not covered by tests

def resizeEvent(self, event: QResizeEvent) -> None:
if not (scene := self.scene()):
return
rect = scene.itemsBoundingRect()
xmargin = rect.width() * self.padding
ymargin = rect.height() * self.padding
margins = QMarginsF(xmargin, ymargin, xmargin, ymargin)
self.fitInView(rect.marginsAdded(margins), Qt.AspectRatioMode.KeepAspectRatio)

Check warning on line 26 in src/pymmcore_widgets/hcs/_util.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/hcs/_util.py#L19-L26

Added lines #L19 - L26 were not covered by tests
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __init__(self, parent: QWidget | None = None) -> None:
# overlap along y
self.overlap_y = QDoubleSpinBox()
self.overlap_y.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.overlap_x.setRange(-10000, 100)
self.overlap_y.setRange(-10000, 100)
# order combo
self.mode = QComboBox()
self.mode.addItems([mode.value for mode in OrderMode])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
)
from superqt.utils import signals_blocked

from pymmcore_widgets._util import SeparatorWidget

from ._grid_row_column_widget import GridRowColumnWidget
from ._random_points_widget import RandomPointWidget

Expand Down Expand Up @@ -72,6 +70,7 @@
self._active_plan_widget: (
RelativePositionWidget | RandomPointWidget | GridRowColumnWidget
) = self.single_pos_wdg
self._active_plan_type: type[RelativePointPlan] = useq.RelativePosition

# radio buttons selection

Expand All @@ -95,14 +94,16 @@

main_layout = QVBoxLayout(self)
main_layout.setSpacing(10)
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setContentsMargins(10, 0, 10, 0)
for btn, wdg in (
(self.single_radio_btn, self.single_pos_wdg),
(self.random_radio_btn, self.random_points_wdg),
(self.grid_radio_btn, self.grid_wdg),
):
wdg.setEnabled(btn.isChecked()) # type: ignore [attr-defined]
grpbx = QGroupBox()
# make a click on the groupbox act as a click on the button
grpbx.mousePressEvent = lambda _, b=btn: b.setChecked(True)
grpbx.setLayout(QVBoxLayout())
grpbx.layout().addWidget(wdg)

Expand All @@ -113,11 +114,15 @@
layout.addWidget(grpbx, 1)
main_layout.addLayout(layout)

for i in range(0, 7, 2):
main_layout.insertWidget(i, SeparatorWidget())
# for i in range(1, 5, 2):
# main_layout.insertWidget(i, SeparatorWidget())

# _________________________PUBLIC METHODS_________________________ #

@property
def active_plan_type(self) -> type[RelativePointPlan]:
return self._active_plan_type

Check warning on line 124 in src/pymmcore_widgets/useq_widgets/points_plans/_points_plan_selector.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/useq_widgets/points_plans/_points_plan_selector.py#L124

Added line #L124 was not covered by tests

def value(self) -> useq.RelativeMultiPointPlan:
return self._active_plan_widget.value()

Expand All @@ -129,20 +134,22 @@
plan : useq.RelativePosition | useq.RandomPoints | useq.GridRowsColumns
The point plan to set.
"""
if isinstance(plan, useq.RandomPoints):
with signals_blocked(self.random_points_wdg):
if plan == self.value():
return

Check warning on line 138 in src/pymmcore_widgets/useq_widgets/points_plans/_points_plan_selector.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/useq_widgets/points_plans/_points_plan_selector.py#L138

Added line #L138 was not covered by tests

with signals_blocked(self):
if isinstance(plan, useq.RandomPoints):
self.random_points_wdg.setValue(plan)
self.random_radio_btn.setChecked(True)
elif isinstance(plan, useq.GridRowsColumns):
with signals_blocked(self.grid_wdg):
self.random_radio_btn.setChecked(True)
elif isinstance(plan, useq.GridRowsColumns):
self.grid_wdg.setValue(plan)
self.grid_radio_btn.setChecked(True)
elif isinstance(plan, useq.RelativePosition):
with signals_blocked(self.single_pos_wdg):
self.grid_radio_btn.setChecked(True)
elif isinstance(plan, useq.RelativePosition):

Check warning on line 147 in src/pymmcore_widgets/useq_widgets/points_plans/_points_plan_selector.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/useq_widgets/points_plans/_points_plan_selector.py#L147

Added line #L147 was not covered by tests
self.single_pos_wdg.setValue(plan)
self.single_radio_btn.setChecked(True)
else: # pragma: no cover
raise ValueError(f"Invalid plan type: {type(plan)}")
self.single_radio_btn.setChecked(True)

Check warning on line 149 in src/pymmcore_widgets/useq_widgets/points_plans/_points_plan_selector.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/useq_widgets/points_plans/_points_plan_selector.py#L149

Added line #L149 was not covered by tests
else: # pragma: no cover
raise ValueError(f"Invalid plan type: {type(plan)}")
self._on_value_changed()

# _________________________PRIVATE METHODS_________________________ #

Expand All @@ -156,7 +163,12 @@
wdg.setEnabled(checked)
if checked:
self._active_plan_widget = wdg
self.valueChanged.emit(self.value())
self._active_plan_type = {
self.single_radio_btn: useq.RelativePosition,
self.random_radio_btn: useq.RandomPoints,
self.grid_radio_btn: useq.GridRowsColumns,
}[btn]
self._on_value_changed()

def _on_value_changed(self) -> None:
self.valueChanged.emit(self.value())
Loading