11from __future__ import annotations
22
3- from typing import Any
3+ from contextlib import suppress
4+ from typing import TYPE_CHECKING , Any , Sequence
45
56from fonticon_mdi6 import MDI6
67from pymmcore_plus import CMMCorePlus
78from pymmcore_plus ._logger import logger
89from pymmcore_plus ._util import retry
9- from qtpy .QtWidgets import QCheckBox , QMessageBox , QWidget , QWidgetAction
10+ from qtpy .QtCore import Qt
11+ from qtpy .QtWidgets import (
12+ QCheckBox ,
13+ QMessageBox ,
14+ QPushButton ,
15+ QWidget ,
16+ QWidgetAction ,
17+ QWizard ,
18+ )
1019from superqt .utils import signals_blocked
20+ from useq import WellPlatePlan
1121
22+ from pymmcore_widgets import HCSWizard
1223from pymmcore_widgets .useq_widgets import PositionTable
1324from pymmcore_widgets .useq_widgets ._column_info import (
1425 ButtonColumn ,
1526)
1627from pymmcore_widgets .useq_widgets ._positions import AF_DEFAULT_TOOLTIP
1728
29+ if TYPE_CHECKING :
30+ from useq import Position
31+
32+ UPDATE_POSITIONS = "Update Positions List"
33+ ADD_POSITIONS = "Add to Positions List"
34+
1835
1936class CoreConnectedPositionTable (PositionTable ):
2037 """[PositionTable](../PositionTable#) connected to a Micro-Manager core instance.
@@ -45,6 +62,28 @@ def __init__(
4562 super ().__init__ (rows , parent )
4663 self ._mmc = mmcore or CMMCorePlus .instance ()
4764
65+ # -------------- HCS Wizard ----------------
66+ self ._hcs_wizard : HCSWizard | None = None
67+ self ._plate_plan : WellPlatePlan | None = None
68+
69+ self ._hcs_button = QPushButton ("Well Plate..." )
70+ # self._hcs_button.setIcon(icon(MDI6.view_comfy))
71+ self ._hcs_button .setFocusPolicy (Qt .FocusPolicy .NoFocus )
72+ self ._hcs_button .setToolTip ("Open the HCS wizard." )
73+ self ._hcs_button .clicked .connect (self ._show_hcs )
74+
75+ self ._edit_hcs_pos = QPushButton ("Make Editable" )
76+ self ._edit_hcs_pos .setToolTip (
77+ "Convert HCS positions to regular editable positions."
78+ )
79+ self ._edit_hcs_pos .setStyleSheet ("color: red" )
80+ self ._edit_hcs_pos .hide ()
81+ self ._edit_hcs_pos .clicked .connect (self ._show_pos_editing_dialog )
82+
83+ self ._btn_row .insertWidget (3 , self ._hcs_button )
84+ self ._btn_row .insertWidget (3 , self ._edit_hcs_pos )
85+ # ------------------------------------------
86+
4887 self .move_to_selection = QCheckBox ("Move Stage to Selected Point" )
4988 # add a button to update XY to the current position
5089 self ._xy_btn_col = ButtonColumn (
@@ -53,19 +92,20 @@ def __init__(
5392 self ._z_btn_col = ButtonColumn (
5493 key = "z_btn" , glyph = MDI6 .arrow_left , on_click = self ._set_z_from_core
5594 )
56- self .table ().addColumn (self ._xy_btn_col , self .table ().indexOf (self .X ))
57- self .table ().addColumn (self ._z_btn_col , self .table ().indexOf (self .Z ) + 1 )
58- self .table ().addColumn (self ._af_btn_col , self .table ().indexOf (self .AF ) + 1 )
95+ table = self .table ()
96+ table .addColumn (self ._xy_btn_col , table .indexOf (self .X ))
97+ table .addColumn (self ._z_btn_col , table .indexOf (self .Z ) + 1 )
98+ table .addColumn (self ._af_btn_col , table .indexOf (self .AF ) + 1 )
5999
60100 # when a new row is inserted, call _on_rows_inserted
61101 # to update the new values from the core position
62- self . table () .model ().rowsInserted .connect (self ._on_rows_inserted )
102+ table .model ().rowsInserted .connect (self ._on_rows_inserted )
63103
64104 # add move_to_selection to toolbar and link up callback
65105 toolbar = self .toolBar ()
66106 action0 = next (x for x in toolbar .children () if isinstance (x , QWidgetAction ))
67107 toolbar .insertWidget (action0 , self .move_to_selection )
68- self . table () .itemSelectionChanged .connect (self ._on_selection_change )
108+ table .itemSelectionChanged .connect (self ._on_selection_change )
69109
70110 # connect
71111 self ._mmc .events .systemConfigurationLoaded .connect (self ._on_sys_config_loaded )
@@ -77,8 +117,137 @@ def __init__(
77117 # hide the set-AF-offset button to begin with.
78118 self ._on_af_per_position_toggled (self .af_per_position .isChecked ())
79119
120+ # ---------------------- public methods -----------------------
121+
122+ def value (
123+ self , exclude_unchecked : bool = True , exclude_hidden_cols : bool = True
124+ ) -> tuple [Position , ...] | WellPlatePlan :
125+ """Return the current state of the positions table."""
126+ if self ._plate_plan is not None :
127+ return self ._plate_plan
128+ return super ().value (exclude_unchecked , exclude_hidden_cols )
129+
130+ def setValue (self , value : Sequence [Position ] | WellPlatePlan ) -> None :
131+ """Set the value of the positions table."""
132+ if isinstance (value , WellPlatePlan ):
133+ self ._plate_plan = value
134+ self ._hcs .setValue (value )
135+ self ._set_position_table_editable (False )
136+ value = tuple (value )
137+ super ().setValue (value )
138+
80139 # ----------------------- private methods -----------------------
81140
141+ def _show_hcs (self ) -> None :
142+ """Show or raise the HCS wizard."""
143+ self ._hcs .raise_ () if self ._hcs .isVisible () else self ._hcs .show ()
144+
145+ @property
146+ def _hcs (self ) -> HCSWizard :
147+ """Get the HCS wizard, initializing it if it doesn't exist."""
148+ if self ._hcs_wizard is None :
149+ self ._hcs_wizard = HCSWizard (self )
150+ self ._rename_hcs_position_button (ADD_POSITIONS )
151+ self ._hcs_wizard .accepted .connect (self ._on_hcs_accepted )
152+ return self ._hcs_wizard
153+
154+ def _on_hcs_accepted (self ) -> None :
155+ """Add the positions from the HCS wizard to the stage positions."""
156+ self ._plate_plan = self ._hcs .value ()
157+ if self ._plate_plan is not None :
158+ # show a ovwerwrite warning dialog if the table is not empty
159+ if self .table ().rowCount () > 0 :
160+ dialog = QMessageBox (
161+ QMessageBox .Icon .Warning ,
162+ "Overwrite Positions" ,
163+ "This will replace the positions currently stored in the table."
164+ "\n Would you like to proceed?" ,
165+ QMessageBox .StandardButton .Yes | QMessageBox .StandardButton .No ,
166+ self ,
167+ )
168+ dialog .setDefaultButton (QMessageBox .StandardButton .Yes )
169+ if dialog .exec () != QMessageBox .StandardButton .Yes :
170+ return
171+ self ._update_table_positions (self ._plate_plan )
172+
173+ def _update_table_positions (self , plan : WellPlatePlan ) -> None :
174+ """Update the table with the positions from the HCS wizard."""
175+ self .setValue (list (plan ))
176+ self ._set_position_table_editable (False )
177+
178+ def _rename_hcs_position_button (self , text : str ) -> None :
179+ if wiz := self ._hcs_wizard :
180+ wiz .points_plan_page .setButtonText (QWizard .WizardButton .FinishButton , text )
181+
182+ def _show_pos_editing_dialog (self ) -> None :
183+ dialog = QMessageBox (
184+ QMessageBox .Icon .Warning ,
185+ "Reset HCS" ,
186+ "Positions are currently autogenerated from the HCS Wizard."
187+ "\n \n Would you like to cast them to a list of stage positions?"
188+ "\n \n NOTE: you will no longer be able to edit them using the HCS Wizard "
189+ "widget." ,
190+ QMessageBox .StandardButton .Yes | QMessageBox .StandardButton .No ,
191+ self ,
192+ )
193+ dialog .setDefaultButton (QMessageBox .StandardButton .No )
194+ if dialog .exec () == QMessageBox .StandardButton .Yes :
195+ self ._plate_plan = None
196+ self ._set_position_table_editable (True )
197+
198+ def _set_position_table_editable (self , state : bool ) -> None :
199+ """Enable/disable the position table depending on the use of the HCS wizard."""
200+ self ._edit_hcs_pos .setVisible (not state )
201+ self .include_z .setVisible (state )
202+
203+ # Hide or show all columns that are irrelevant when using the HCS wizard
204+ table = self .table ()
205+ inc_z = self .include_z .isChecked ()
206+ table .setColumnHidden (table .indexOf (self ._xy_btn_col ), not state )
207+ table .setColumnHidden (table .indexOf (self ._z_btn_col ), not state or not inc_z )
208+ table .setColumnHidden (table .indexOf (self .Z ), not state or not inc_z )
209+ table .setColumnHidden (table .indexOf (self .SEQ ), not state )
210+
211+ # Enable or disable the toolbar
212+ for action in self .toolBar ().actions ()[1 :]:
213+ action .setEnabled (state )
214+
215+ self ._enable_table_items (state )
216+ # connect/disconnect the double click event and rename the button
217+ if state :
218+ self ._rename_hcs_position_button (ADD_POSITIONS )
219+ with suppress (RuntimeError ):
220+ self .table ().cellDoubleClicked .disconnect (self ._show_pos_editing_dialog )
221+ else :
222+ self ._rename_hcs_position_button (UPDATE_POSITIONS )
223+ # using UniqueConnection to avoid multiple connections
224+ # but catching the TypeError if the connection is already made
225+ with suppress (TypeError , RuntimeError ):
226+ self .table ().cellDoubleClicked .connect (
227+ self ._show_pos_editing_dialog , Qt .ConnectionType .UniqueConnection
228+ )
229+
230+ def _enable_table_items (self , state : bool ) -> None :
231+ """Enable or disable the table items depending on the use of the HCS wizard."""
232+ table = self .table ()
233+ name_col = table .indexOf (self .NAME )
234+ x_col = table .indexOf (self .X )
235+ y_col = table .indexOf (self .Y )
236+ with signals_blocked (table ):
237+ for row in range (table .rowCount ()):
238+ table .cellWidget (row , x_col ).setEnabled (state )
239+ table .cellWidget (row , y_col ).setEnabled (state )
240+ # enable/disable the name cells
241+ name_item = table .item (row , name_col )
242+ flags = name_item .flags () | Qt .ItemFlag .ItemIsEnabled
243+ if state :
244+ flags |= Qt .ItemFlag .ItemIsEditable
245+ else :
246+ # keep the name column enabled but NOT editable. We do not disable
247+ # to keep available the "Move Stage to Selected Point" option
248+ flags &= ~ Qt .ItemFlag .ItemIsEditable
249+ name_item .setFlags (flags )
250+
82251 def _on_sys_config_loaded (self ) -> None :
83252 """Update the table when the system configuration is loaded."""
84253 self ._update_xy_enablement ()
0 commit comments