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

[WIP] feat: new widget to manage Groups and Presets #338

Closed
wants to merge 22 commits into from
Closed
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
14 changes: 14 additions & 0 deletions examples/group_preset_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pymmcore_plus import CMMCorePlus
from qtpy.QtWidgets import QApplication

from pymmcore_widgets import GroupPresetDialog

app = QApplication([])

mmc = CMMCorePlus.instance()
mmc.loadSystemConfiguration()

gp = GroupPresetDialog(mmcore=mmc)
gp.show()

app.exec()
2 changes: 2 additions & 0 deletions src/pymmcore_widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"DeviceWidget",
"ExposureWidget",
"GridPlanWidget",
"GroupPresetDialog",
"GroupPresetTableWidget",
"HCSWizard",
"ImagePreview",
Expand Down Expand Up @@ -64,6 +65,7 @@
StageWidget,
)
from .device_properties import PropertiesWidget, PropertyBrowser, PropertyWidget
from .group_preset_widget import GroupPresetDialog
from .hcs import HCSWizard
from .hcwizard import ConfigWizard
from .mda import MDAWidget
Expand Down
16 changes: 6 additions & 10 deletions src/pymmcore_widgets/control/_objective_widget.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from pymmcore_plus import CMMCorePlus
from qtpy.QtWidgets import QComboBox, QHBoxLayout, QLabel, QSizePolicy, QWidget
from qtpy.QtWidgets import QComboBox, QHBoxLayout, QLabel, QWidget

from pymmcore_widgets._deprecated._device_widget import StateDeviceWidget
from pymmcore_widgets._util import guess_objective_or_prompt
Expand Down Expand Up @@ -45,12 +45,11 @@ def __init__(
self._combo = self._create_objective_combo(objective_device)

lbl = QLabel("Objectives:")
lbl.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)

self.setLayout(QHBoxLayout())
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().addWidget(lbl)
self.layout().addWidget(self._combo)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(lbl, 0)
layout.addWidget(self._combo, 1)

self._mmc.events.systemConfigurationLoaded.connect(self._on_sys_cfg_loaded)
self.destroyed.connect(self._disconnect)
Expand All @@ -69,16 +68,13 @@ def _on_sys_cfg_loaded(self) -> None:
self._objective_device = guess_objective_or_prompt(parent=self)
self._combo.setParent(QWidget())
self._combo = self._create_objective_combo(self._objective_device)
self.layout().addWidget(self._combo)
self.layout().addWidget(self._combo, 1)

def _create_objective_combo(
self, device_label: str | None
) -> _ObjectiveStateWidget | QComboBox:
if device_label:
combo = _ObjectiveStateWidget(device_label, parent=self, mmcore=self._mmc)
combo.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
self.setMinimumWidth(0)
combo.adjustSize()
combo._combo.currentIndexChanged.connect(self._on_obj_changed)
else:
combo = QComboBox(parent=self)
Expand Down
85 changes: 76 additions & 9 deletions src/pymmcore_widgets/device_properties/_device_property_table.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

from logging import getLogger
from typing import Iterable, cast
from re import Pattern
from typing import Callable, Iterable, Sequence, cast

from pymmcore_plus import CMMCorePlus, DeviceProperty, DeviceType
from qtpy.QtCore import Qt
from qtpy.QtCore import Qt, Signal
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QAbstractScrollArea, QTableWidget, QTableWidgetItem, QWidget
from superqt.fonticon import icon
Expand Down Expand Up @@ -35,6 +36,7 @@
will not update the core. By default, True.
"""

valueChanged = Signal()
PROP_ROLE = QTableWidgetItem.ItemType.UserType + 1

def __init__(
Expand All @@ -48,13 +50,15 @@
rows = 0
cols = 2
super().__init__(rows, cols, parent)

self._rows_checkable: bool = False
self._prop_widgets_enabled: bool = enable_property_widgets
self._connect_core = connect_core

self._mmc = mmcore or CMMCorePlus.instance()
self._mmc.events.systemConfigurationLoaded.connect(self._rebuild_table)

self.itemChanged.connect(self._on_item_changed)
# If we enable these, then the edit group dialog will lose all of it's checks
# whenever modify group button is clicked. However, We don't want this widget
# to have to be aware of a current group (or do we?)
Expand All @@ -81,6 +85,20 @@
self.resize(500, 500)
self._rebuild_table()

def _on_item_changed(self, item: QTableWidgetItem) -> None:
if self._rows_checkable:
color = self.palette().color(self.foregroundRole())
font = item.font()
if item.checkState() == Qt.CheckState.Checked:
color.setAlpha(255)
font.setBold(True)
else:
color.setAlpha(175)
font.setBold(False)
item.setForeground(color)
item.setFont(font)
self.valueChanged.emit()

def _disconnect(self) -> None:
self._mmc.events.systemConfigurationLoaded.disconnect(self._rebuild_table)
# self._mmc.events.configGroupDeleted.disconnect(self._rebuild_table)
Expand Down Expand Up @@ -135,6 +153,9 @@
mmcore=self._mmc,
connect_core=self._connect_core,
)
# TODO: this is an over-emission. if this is a checkable table,
# and the property is not checked, we should not emit.
wdg.valueChanged.connect(self.valueChanged)
except Exception as e:
logger.error(
f"Error creating widget for {prop.device}-{prop.name}: {e}"
Expand Down Expand Up @@ -163,27 +184,51 @@

def filterDevices(
self,
query: str = "",
query: str | Pattern = "",
exclude_devices: Iterable[DeviceType] = (),
include_devices: Iterable[DeviceType] = (),
include_read_only: bool = True,
include_pre_init: bool = True,
init_props_only: bool = False,
selected_only: bool = False,
predicate: Callable[[DeviceProperty], bool | None] | None = None,
) -> None:
"""Update the table to only show devices that match the given query/filter."""
exclude_devices = set(exclude_devices)
include_devices = set(include_devices)
for row in range(self.rowCount()):
item = self.item(row, 0)
prop = cast(DeviceProperty, item.data(self.PROP_ROLE))
if include_devices and prop.deviceType() not in include_devices:
self.hideRow(row)
continue

Check warning on line 204 in src/pymmcore_widgets/device_properties/_device_property_table.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/device_properties/_device_property_table.py#L203-L204

Added lines #L203 - L204 were not covered by tests
if predicate:
result = predicate(prop)
if result is False:
self.hideRow(row)
continue
if result is True:
self.showRow(row)
continue

Check warning on line 212 in src/pymmcore_widgets/device_properties/_device_property_table.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/device_properties/_device_property_table.py#L206-L212

Added lines #L206 - L212 were not covered by tests
# for None: fall through to other filters
if (
(prop.isReadOnly() and not include_read_only)
or (prop.isPreInit() and not include_pre_init)
or (init_props_only and not prop.isPreInit())
or (prop.deviceType() in exclude_devices)
or (query and query.lower() not in item.text().lower())
or (selected_only and item.checkState() != Qt.CheckState.Checked)
):
self.hideRow(row)
else:
self.showRow(row)
continue
if query:
if isinstance(query, str) and query.lower() not in item.text().lower():
self.hideRow(row)
continue
elif isinstance(query, Pattern) and not query.search(item.text()):
self.hideRow(row)
continue

Check warning on line 229 in src/pymmcore_widgets/device_properties/_device_property_table.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/device_properties/_device_property_table.py#L224-L229

Added lines #L224 - L229 were not covered by tests

self.showRow(row)

def getCheckedProperties(self) -> list[tuple[str, str, str]]:
"""Return a list of checked properties.
Expand All @@ -194,12 +239,34 @@
# [(device, property, value_to_set), ...]
dev_prop_val_list: list[tuple[str, str, str]] = []
for row in range(self.rowCount()):
if self.item(row, 0) is None:
continue
if self.item(row, 0).checkState() == Qt.CheckState.Checked:
if (
item := self.item(row, 0)
) and item.checkState() == Qt.CheckState.Checked:
dev_prop_val_list.append(self.getRowData(row))
return dev_prop_val_list

def value(self) -> list[tuple[str, str, str]]:
return self.getCheckedProperties()

Check warning on line 249 in src/pymmcore_widgets/device_properties/_device_property_table.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/device_properties/_device_property_table.py#L249

Added line #L249 was not covered by tests

def setValue(self, value: Sequence[tuple[str, str, str]]) -> None:
self.setCheckedProperties(value, with_value=True)

Check warning on line 252 in src/pymmcore_widgets/device_properties/_device_property_table.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/device_properties/_device_property_table.py#L252

Added line #L252 was not covered by tests

def setCheckedProperties(
self,
value: Sequence[tuple[str, str, str]],
with_value: bool = True,
) -> None:
for row in range(self.rowCount()):
if self.item(row, 0) is None:
continue
self.item(row, 0).setCheckState(Qt.CheckState.Unchecked)
for device, prop, *val in value:
if self.item(row, 0).text() == f"{device}-{prop}":
self.item(row, 0).setCheckState(Qt.CheckState.Checked)
wdg = cast("PropertyWidget", self.cellWidget(row, 1))
if val and with_value:
wdg.setValue(val[0])

Check warning on line 268 in src/pymmcore_widgets/device_properties/_device_property_table.py

View check run for this annotation

Codecov / codecov/patch

src/pymmcore_widgets/device_properties/_device_property_table.py#L259-L268

Added lines #L259 - L268 were not covered by tests

def getRowData(self, row: int) -> tuple[str, str, str]:
item = self.item(row, 0)
prop: DeviceProperty = item.data(self.PROP_ROLE)
Expand Down
5 changes: 5 additions & 0 deletions src/pymmcore_widgets/group_preset_widget/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""GroupPresetDialog."""

from ._group_preset_dialog import GroupPresetDialog

__all__ = ["GroupPresetDialog"]
Loading