Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
0b60237
wip
tlambert03 Jun 24, 2025
db619c8
use dev prop table
tlambert03 Jun 24, 2025
a94126c
feat: enhance ConfigTreeModel to support settings and update header data
tlambert03 Jun 24, 2025
b8b550e
fix init
tlambert03 Jun 24, 2025
c5a1e7d
fix bug
tlambert03 Jun 24, 2025
e735fad
two way updates
tlambert03 Jun 24, 2025
32ca961
feat: add custom delegate for Setting value editing in ConfigTreeModel
tlambert03 Jun 24, 2025
3b599d7
refactor: improve duplicate group handling and name validation in Con…
tlambert03 Jun 24, 2025
66d2d8b
move example
tlambert03 Jun 24, 2025
ba00699
move example
tlambert03 Jun 24, 2025
a3f03cb
updates
tlambert03 Jun 24, 2025
05ff08f
feat: enhance ConfigGroupsEditor with tree view and layout adjustments~
tlambert03 Jun 25, 2025
3692bb4
breaking into two
tlambert03 Jun 25, 2025
bced4e3
feat: restructure configuration editor and update device property han…
tlambert03 Jun 26, 2025
24eef51
future import
tlambert03 Jun 26, 2025
1ed09ec
docs
tlambert03 Jun 26, 2025
0423d05
feat: add methods to retrieve QModelIndex for groups and presets in Q…
tlambert03 Jun 27, 2025
8b21d73
wip: preset-table
tlambert03 Jun 27, 2025
eacc052
better table
tlambert03 Jun 28, 2025
6f25cb5
better table
tlambert03 Jun 28, 2025
6ed116b
more improvements
tlambert03 Jun 30, 2025
ae6940d
more improvements
tlambert03 Jun 30, 2025
0f71c16
starting tests
tlambert03 Jun 30, 2025
17f2450
Merge branch 'main' into q-config
tlambert03 Jun 30, 2025
443deb2
remove config props
tlambert03 Jun 30, 2025
8946e74
Merge branch 'main' into q-config
tlambert03 Jul 1, 2025
65088a7
move file
tlambert03 Jul 1, 2025
b951b88
remove file
tlambert03 Jul 1, 2025
b6985fd
feat: Introduce QConfigGroupsModel for managing configuration groups …
tlambert03 Jul 2, 2025
3d59072
feat: Add QConfigGroupsModel and related functionality for configurat…
tlambert03 Jul 2, 2025
08a3099
tmp
tlambert03 Jul 2, 2025
071eba3
wip
tlambert03 Jul 2, 2025
bbe779b
wip
tlambert03 Jul 2, 2025
0c5fae9
Refactor configuration model and tree structure
tlambert03 Jul 2, 2025
c22db0a
feat: Implement ConfigGroupPivotModel and update related references i…
tlambert03 Jul 2, 2025
3a1db3c
fix test
tlambert03 Jul 2, 2025
f0bb83a
refactor: Consolidate imports and move core functions to a new module
tlambert03 Jul 2, 2025
ab99162
fixes
tlambert03 Jul 2, 2025
f44b9c0
feat: Enhance device property management by adding QDevicePropertyMod…
tlambert03 Jul 3, 2025
0d16a3f
tmp models
tlambert03 Jul 3, 2025
1ea6994
tmp models
tlambert03 Jul 3, 2025
d76841a
add more example
tlambert03 Jul 3, 2025
8108c33
remove
tlambert03 Jul 3, 2025
e781e50
better resizing
tlambert03 Jul 4, 2025
c62ea6d
Refactor and enhance configuration presets management
tlambert03 Jul 4, 2025
39ef6a2
good selection sync
tlambert03 Jul 4, 2025
e05e024
activate buttons
tlambert03 Jul 4, 2025
8a80fc9
better icons
tlambert03 Jul 4, 2025
5d86439
fix delegate
tlambert03 Jul 4, 2025
3b19785
Enhance ConfigGroupsEditor layout and add persistent editor functiona…
tlambert03 Jul 5, 2025
211601d
good flat
tlambert03 Jul 5, 2025
de2738b
good flat model
tlambert03 Jul 5, 2025
1838b2b
wip
tlambert03 Jul 5, 2025
e1fecef
less code
tlambert03 Jul 5, 2025
3eeff59
moew
tlambert03 Jul 5, 2025
ad510d6
refactor: replace FlattenModel with TreeFlatteningProxy for improved …
tlambert03 Jul 5, 2025
49f0b36
Refactor configuration presets and device property selection
tlambert03 Jul 6, 2025
c654c3f
remove flattening
tlambert03 Jul 6, 2025
32f6002
feat: add help dialog and HTML documentation for configuration groups…
tlambert03 Jul 6, 2025
dda6f47
fix persistent
tlambert03 Jul 6, 2025
3148d47
remove example
tlambert03 Jul 6, 2025
388ff7d
fix visibility
tlambert03 Jul 6, 2025
7f55945
refactor: update icon sizes and layout margins in configuration editors
tlambert03 Jul 6, 2025
9f03dc8
feat: add preset property update functionality and enhance device pro…
tlambert03 Jul 6, 2025
119e005
refactor: streamline data change handling and improve rebuild logic i…
tlambert03 Jul 6, 2025
cbafdd4
feat: add system group and startup/shutdown preset icons; enhance pro…
tlambert03 Jul 6, 2025
2f0fbce
fix tests
tlambert03 Jul 6, 2025
a4bb373
undo redo 1
tlambert03 Jul 7, 2025
087e0c0
undo redo 2
tlambert03 Jul 7, 2025
2375230
undo redo 3
tlambert03 Jul 7, 2025
02f0dd5
feat: add name change validation for group and preset renaming; impro…
tlambert03 Jul 7, 2025
5b5a4d1
fix undo redo
tlambert03 Jul 7, 2025
e18f4a5
Merge branch 'main' into q-config-new-model
tlambert03 Jul 8, 2025
ec84cc1
Merge branch 'main' into q-config-new-model
tlambert03 Jul 12, 2025
752317b
Merge branch 'main' into q-config-new-model
tlambert03 Jul 15, 2025
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
16 changes: 16 additions & 0 deletions examples/config_groups_editor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pymmcore_plus import CMMCorePlus
from qtpy.QtWidgets import QApplication

from pymmcore_widgets.config_presets._views._config_groups_editor import (
ConfigGroupsEditor,
)

app = QApplication([])
core = CMMCorePlus()
core.loadSystemConfiguration()

cfg = ConfigGroupsEditor.create_from_core(core)
cfg.setCurrentPreset("Channel", "FITC")
cfg.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 @@ -14,6 +14,7 @@
"ChannelGroupWidget",
"ChannelTable",
"ChannelWidget",
"ConfigGroupsEditor",
"ConfigGroupsTree",
"ConfigWizard",
"ConfigurationWidget",
Expand Down Expand Up @@ -49,6 +50,7 @@
from ._install_widget import InstallWidget
from ._log import CoreLogWidget
from .config_presets import (
ConfigGroupsEditor,
ConfigGroupsTree,
GroupPresetTableWidget,
ObjectivesPixelConfigurationWidget,
Expand Down
Empty file.
42 changes: 42 additions & 0 deletions src/pymmcore_widgets/_help/_config_groups_help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pathlib import Path

from qtpy.QtWidgets import (
QDialog,
QDialogButtonBox,
QFrame,
QTextBrowser,
QVBoxLayout,
QWidget,
)

HELP_DOC = Path(__file__).parent / "config_groups_help.html"


class ConfigGroupsHelpDialog(QDialog):
"""Help dialog for the Config Groups Editor."""

def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
self.setWindowTitle("Config Groups and Presets")
self.setModal(True)

# Add help content here
help_text = QTextBrowser(self)
help_text.setReadOnly(True)
help_text.setAcceptRichText(True)
help_text.setHtml(HELP_DOC.read_text())
help_text.setFrameShape(QFrame.Shape.NoFrame)
# enable links
help_text.setOpenExternalLinks(True)

# make the background match the dialog
pal = self.palette()
pal.setColor(pal.ColorRole.Base, pal.color(pal.ColorRole.Window))
help_text.setPalette(pal)

btn_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
btn_box.accepted.connect(self.accept)

layout = QVBoxLayout(self)
layout.addWidget(help_text)
layout.addWidget(btn_box)
90 changes: 90 additions & 0 deletions src/pymmcore_widgets/_help/config_groups_help.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Micro-Manager Configuration Groups and Presets - Quick Guide</title>
</head>
<body>
<h1>Configuration Groups and Presets</h1>

<p>
Micro-Manager lets you bundle hardware settings into
<a
href="https://micro-manager.org/Micro-Manager_User%27s_Guide#controlling-devices"
><strong>Groups of Configuration Presets</strong></a
>.
</p>

<p>
Each <strong>Group</strong> contains named <strong>Presets</strong> that
define a set of <strong>Device Property values</strong> to be applied to
the hardware.
</p>

<h2>Typical Workflow</h2>

<ol>
<li>
<strong>Add a configuration group</strong> (e.g. "Channel") using the
<em>Add Group</em> button at the top.
</li>
<li>
<strong>Add a preset to the group</strong> (e.g. "DAPI"), by selecting
the group and clicking the <em>Add Preset</em> button.
</li>
<li>
<strong>Select device properties to include in the preset</strong> (e.g.
"Filter Wheel Position", "Light Source Power") using the device property
selector at the right.
</li>
<li>
<strong>Set the device properties to desired values</strong> for the
currently selected preset in the Presets Table at the bottom.
</li>
<li>
<strong>Repeat steps 2-4 to add more presets to the group</strong> (e.g.
"FITC", "Cy3", "Cy5"). You may <em>Duplicate</em> an existing preset to
copy its device property values if another preset is similar.
</li>
</ol>

<h2>Key Building Blocks</h2>

<h3>1. Devices</h3>
<p>
Physical hardware (camera, filter wheel, stage, light source). Each device
exposes one or more <strong>properties</strong> that can be changed or
read.
</p>

<h3>2. Device Properties</h3>
<p>
A single adjustable parameter on a device (e.g.
<code>Exposure</code>, <code>Position</code>).
</p>

<h3>3. Configuration Presets</h3>
<p>
A preset is a snapshot of <em>specific</em> values for a set of device
<strong>properties</strong>. Activating a preset sets each device property
to its stored value.
</p>
<p>
For example, you might have a <em>FITC</em> preset that sets the
appropriate filter wheel positions and light sources for imaging the FITC
channel.
</p>

<h3>4. Configuration Groups</h3>
<p>
A configuration group is a <strong>collection of presets</strong>. It is
customary (<em>but not mandatory</em>) for each preset in a group to share
the same set of device properties.
</p>
<p>
For example, you might have a <em>Channel</em> group that includes presets
for each optical configuration preset (<em>DAPI</em>, <em>FITC</em>,
<em>Cy3</em>, <em>Cy5</em>).
</p>
</body>
</html>
2 changes: 2 additions & 0 deletions src/pymmcore_widgets/_models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
PixelSizePreset,
)
from ._q_config_model import QConfigGroupsModel
from ._q_device_prop_model import QDevicePropertyModel

__all__ = [
"ConfigGroup",
Expand All @@ -26,6 +27,7 @@
"PixelSizeConfigs",
"PixelSizePreset",
"QConfigGroupsModel",
"QDevicePropertyModel",
"get_available_devices",
"get_config_groups",
"get_config_presets",
Expand Down
44 changes: 40 additions & 4 deletions src/pymmcore_widgets/_models/_q_config_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from qtpy.QtCore import QModelIndex, Qt
from qtpy.QtGui import QFont, QIcon
from qtpy.QtWidgets import QMessageBox, QWidget
from superqt import QIconifyIcon

from pymmcore_widgets._icons import StandardIcon
Expand Down Expand Up @@ -41,13 +42,22 @@ def create_from_core(cls, core: CMMCorePlus) -> Self:

def __init__(self, groups: Iterable[ConfigGroup] | None = None) -> None:
super().__init__()
self._in_undo_redo = False # Track when we're in undo/redo operation
if groups:
self.set_groups(groups)

# ------------------------------------------------------------------
# Required Qt model overrides
# ------------------------------------------------------------------

def set_undo_redo_mode(self, enabled: bool) -> None:
"""Set whether we're currently in an undo/redo operation.

When in undo/redo mode, name uniqueness checks are relaxed to allow
restoration of original names that may temporarily conflict.
"""
self._in_undo_redo = enabled

def columnCount(self, _parent: QModelIndex | None = None) -> int:
# In most subclasses, the number of columns is independent of the parent.
return len(Col)
Expand Down Expand Up @@ -142,12 +152,14 @@ def setData(
if new_name == node.name or not new_name:
return False

if self._name_exists(node.parent, new_name):
# During undo/redo, allow restoration of original names even if they
# temporarily conflict, as the conflict will be resolved by the operation
if not self._in_undo_redo and self._name_exists(node.parent, new_name):
warnings.warn(
f"Not adding duplicate name '{new_name}'. It already exists.",
stacklevel=2,
)
return False
return False # pragma: no cover

node.name = new_name
if isinstance(node.payload, (ConfigGroup, ConfigPreset)):
Expand Down Expand Up @@ -352,8 +364,28 @@ def removeRows(
self.endRemoveRows()
return True

def remove(self, idx: QModelIndex) -> None:
# TODO: probably remove the QWidget logic from here
def remove(
self,
idx: QModelIndex,
*,
ask_confirmation: bool = False,
parent: QWidget | None = None,
) -> None:
if idx.isValid():
if ask_confirmation:
item_name = idx.data(Qt.ItemDataRole.DisplayRole)
item_type = type(idx.data(Qt.ItemDataRole.UserRole))
type_name = item_type.__name__.replace(("Config"), "Config ")
msg = QMessageBox.question(
parent,
"Confirm Deletion",
f"Are you sure you want to delete {type_name} {item_name!r}?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes,
)
if msg != QMessageBox.StandardButton.Yes:
return
self.removeRows(idx.row(), 1, idx.parent())

# ------------------------------------------------------------------
Expand Down Expand Up @@ -539,9 +571,13 @@ def insertRows(

# ---------- modify the tree ----------
for i, payload in enumerate(_payloads):
# Only ensure uniqueness if there would be a conflict AND we're not
# in undo/redo mode
if isinstance(payload, (ConfigGroup, ConfigPreset)):
original_name = payload.name
if self._name_exists(parent_node, original_name):
if not self._in_undo_redo and self._name_exists(
parent_node, original_name
):
# Only modify the name if there's actually a conflict
unique_name = self._unique_child_name(
parent_node, original_name, suffix=""
Expand Down
Loading
Loading