copick-shared-ui 2.0.0a2__tar.gz → 2.0.0a3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/workflows/release-please.yml +1 -3
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/workflows/tests.yml +0 -3
- copick_shared_ui-2.0.0a3/.release-please.manifest.json +3 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/CHANGELOG.md +7 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/PKG-INFO +2 -2
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/pyproject.toml +1 -1
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/__init__.py +1 -1
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/click_schema.py +1 -1
- copick_shared_ui-2.0.0a3/src/copick_shared_ui/core/types.py +132 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/ui/edit_object_types_dialog.py +115 -3
- copick_shared_ui-2.0.0a3/src/copick_shared_ui/util/filament_session.py +567 -0
- copick_shared_ui-2.0.0a3/src/copick_shared_ui/util/filaments.py +341 -0
- copick_shared_ui-2.0.0a3/src/copick_shared_ui/util/instances.py +332 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/actions_bar.py +3 -14
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/command_form.py +3 -12
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/uri_widget.py +26 -4
- copick_shared_ui-2.0.0a3/src/copick_shared_ui/widgets/compact_delegate.py +466 -0
- copick_shared_ui-2.0.0a3/src/copick_shared_ui/widgets/flow_layout.py +105 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/info/info_widget.py +44 -4
- copick_shared_ui-2.0.0a3/src/copick_shared_ui/widgets/instances/__init__.py +5 -0
- copick_shared_ui-2.0.0a3/src/copick_shared_ui/widgets/instances/instance_browser.py +490 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/data_worker.py +12 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/conftest.py +16 -0
- copick_shared_ui-2.0.0a3/tests/test_compact_rows.py +203 -0
- copick_shared_ui-2.0.0a3/tests/test_filament_session.py +302 -0
- copick_shared_ui-2.0.0a3/tests/test_filaments.py +131 -0
- copick_shared_ui-2.0.0a3/tests/test_instances.py +99 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_package_metadata.py +1 -1
- copick_shared_ui-2.0.0a3/tests/test_types.py +67 -0
- copick_shared_ui-2.0.0a3/tests/test_uri_and_dialog.py +110 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/uv.lock +52 -52
- copick_shared_ui-2.0.0a2/.release-please.manifest.json +0 -3
- copick_shared_ui-2.0.0a2/tests/inspect_wheel_metadata.py +0 -30
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/dependabot.yml +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/workflows/conventional-commits.yml +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.github/workflows/py-formatting.yml +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.gitignore +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/.pre-commit-config.yaml +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/LICENSE +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/README.md +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/SECURITY.md +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/benchmarks/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/benchmarks/results/2026-08-18-local-alpha-baseline.md +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/benchmarks/thumbnail_io.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/release-please.config.json +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/release-please.prerelease.config.json +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/image_interface.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/models.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/thumbnail_cache.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/icons/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/platform/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/platform/chimerax_integration.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/platform/napari_integration.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/storage.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/theming/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/theming/colors.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/theming/styles.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/theming/theme_detection.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/ui/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/util/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/util/validation.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/command_browser.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/command_runner.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/cli/param_widgets.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/gallery/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/gallery/gallery_widget.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/gallery/run_card.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/widgets/info/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/base.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/base_manager.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/chimerax.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/napari.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/workers/unified_workers.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/__init__.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/docker-compose.yml +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/helpers.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/ssh_mount/.gitignore +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_core_direct_reader_fixture.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_core_entity_backends.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_imports.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_live_portal_integration.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_ssh_backend_integration.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_storage.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_thumbnail_baseline.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_thumbnail_benchmark.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_thumbnail_errors.py +0 -0
- {copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/tests/test_worker_callbacks.py +0 -0
|
@@ -78,9 +78,7 @@ jobs:
|
|
|
78
78
|
run: uv lock --check
|
|
79
79
|
|
|
80
80
|
- name: build
|
|
81
|
-
run:
|
|
82
|
-
uv build
|
|
83
|
-
uv run --no-project --with packaging python tests/inspect_wheel_metadata.py dist/*.whl
|
|
81
|
+
run: uv build
|
|
84
82
|
|
|
85
83
|
# release-please's own Python updater misses a version with a dotted prerelease suffix (2.0.0-alpha.1), so
|
|
86
84
|
# src/copick_shared_ui/__init__.py carries an x-release-please-version marker; refuse to upload if it was still missed.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.0-alpha.3](https://github.com/copick/copick-shared-ui/compare/copick-shared-ui-v2.0.0-alpha.2...copick-shared-ui-v2.0.0-alpha.3) (2026-10-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ✨ Features
|
|
7
|
+
|
|
8
|
+
* filaments, instance segmentations and compact instance browsing (v2.0) ([#74](https://github.com/copick/copick-shared-ui/issues/74)) ([763861b](https://github.com/copick/copick-shared-ui/commit/763861bc00651eb5db6a68d4605df0671ec76317))
|
|
9
|
+
|
|
3
10
|
## [2.0.0-alpha.2](https://github.com/copick/copick-shared-ui/compare/copick-shared-ui-v2.0.0-alpha.1...copick-shared-ui-v2.0.0-alpha.2) (2026-10-06)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: copick-shared-ui
|
|
3
|
-
Version: 2.0.0-alpha.
|
|
3
|
+
Version: 2.0.0-alpha.3
|
|
4
4
|
Summary: Shared UI components for copick visualization plugins
|
|
5
5
|
Project-URL: Repository, https://github.com/copick/copick-shared-ui
|
|
6
6
|
Project-URL: Issues, https://github.com/copick/copick-shared-ui/issues
|
|
@@ -46,7 +46,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
46
46
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
47
47
|
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
48
48
|
Requires-Python: >=3.11
|
|
49
|
-
Requires-Dist: copick<3,>=2.0.
|
|
49
|
+
Requires-Dist: copick<3,>=2.0.0a3
|
|
50
50
|
Requires-Dist: numpy>=2.0.2
|
|
51
51
|
Requires-Dist: pydantic>=2
|
|
52
52
|
Requires-Dist: qtpy
|
{copick_shared_ui-2.0.0a2 → copick_shared_ui-2.0.0a3}/src/copick_shared_ui/core/click_schema.py
RENAMED
|
@@ -40,7 +40,7 @@ UI_EXCLUDED_COMMANDS = {"browse", "info", "config", "stats", "deposit"}
|
|
|
40
40
|
class URIParamMeta:
|
|
41
41
|
"""Metadata for a URI parameter, extracted from CopickURI type."""
|
|
42
42
|
|
|
43
|
-
object_type: str # "picks", "mesh", "segmentation", "tomogram", "feature", "any"
|
|
43
|
+
object_type: str # "picks", "filaments", "mesh", "segmentation", "tomogram", "feature", "any"
|
|
44
44
|
role: str # "input", "output", "reference"
|
|
45
45
|
|
|
46
46
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Copick entity types as the UI sees them, tolerant of copick versions that predate filaments and the newer
|
|
2
|
+
segmentation types. Qt-free."""
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
SEGMENTATION_TYPES = ("binary", "multilabel", "instance", "panoptic")
|
|
7
|
+
|
|
8
|
+
SEGMENTATION_TYPE_LABELS = {
|
|
9
|
+
"binary": "Binary",
|
|
10
|
+
"multilabel": "Multilabel",
|
|
11
|
+
"instance": "Instance",
|
|
12
|
+
"panoptic": "Panoptic",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Short tags for compact tables and badges (the chips copick-web shows).
|
|
16
|
+
SEGMENTATION_TYPE_TAGS = {
|
|
17
|
+
"binary": "binary",
|
|
18
|
+
"multilabel": "multi",
|
|
19
|
+
"instance": "inst",
|
|
20
|
+
"panoptic": "pan",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
FILAMENT_METADATA_NAMESPACE = "copick"
|
|
24
|
+
FILAMENT_METADATA_KEY = "filament"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _class_names(obj: Any):
|
|
28
|
+
return [cls.__name__ for cls in type(obj).__mro__]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def copick_object_type(obj: Any) -> Optional[str]:
|
|
32
|
+
"""``"filaments"``, ``"picks"``, ``"mesh"``, ``"segmentation"``, ``"tomogram"``, ``"feature"``, ``"voxel_spacing"``,
|
|
33
|
+
``"run"`` or None, by class (works for every backend's subclasses)."""
|
|
34
|
+
names = _class_names(obj)
|
|
35
|
+
# Order matters: check the more specific names first.
|
|
36
|
+
for cls_name, kind in (
|
|
37
|
+
("CopickFilaments", "filaments"),
|
|
38
|
+
("CopickPicks", "picks"),
|
|
39
|
+
("CopickMesh", "mesh"),
|
|
40
|
+
("CopickSegmentation", "segmentation"),
|
|
41
|
+
("CopickTomogram", "tomogram"),
|
|
42
|
+
("CopickFeatures", "feature"),
|
|
43
|
+
("CopickVoxelSpacing", "voxel_spacing"),
|
|
44
|
+
("CopickRun", "run"),
|
|
45
|
+
):
|
|
46
|
+
if cls_name in names:
|
|
47
|
+
return kind
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
URI_OBJECT_TYPES = ("picks", "filaments", "mesh", "segmentation", "tomogram", "feature")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def uri_object_type(obj: Any) -> Optional[str]:
|
|
55
|
+
"""The copick URI object type of an entity (``copick.util.uri``), or None if it has no URI form."""
|
|
56
|
+
if obj is None:
|
|
57
|
+
return None
|
|
58
|
+
kind = copick_object_type(obj)
|
|
59
|
+
return kind if kind in URI_OBJECT_TYPES else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def segmentation_type_of(seg: Any) -> str:
|
|
63
|
+
"""``"binary"``, ``"multilabel"``, ``"instance"`` or ``"panoptic"`` (falls back to ``is_multilabel`` for copick
|
|
64
|
+
versions without instance and panoptic segmentations)."""
|
|
65
|
+
kind = getattr(seg, "segmentation_type", None)
|
|
66
|
+
if isinstance(kind, str) and kind in SEGMENTATION_TYPES:
|
|
67
|
+
return kind
|
|
68
|
+
if getattr(seg, "is_panoptic", False) is True:
|
|
69
|
+
return "panoptic"
|
|
70
|
+
if getattr(seg, "is_instance", False) is True:
|
|
71
|
+
return "instance"
|
|
72
|
+
return "multilabel" if getattr(seg, "is_multilabel", False) else "binary"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def segmentation_type_flags(kind: str, explicit: Optional[bool] = None) -> dict:
|
|
76
|
+
"""Keyword flags for ``new_segmentation`` / ``get_segmentations`` / ``delete_segmentations`` selecting exactly one
|
|
77
|
+
type.
|
|
78
|
+
|
|
79
|
+
With a copick that has instance and panoptic segmentations (``explicit``, detected by default), all three flags are
|
|
80
|
+
given, because an omitted flag means "any": ``delete_segmentations(..., is_multilabel=False)`` alone would also
|
|
81
|
+
delete an instance segmentation with the same name, user, session and voxel size. Older copick versions only get
|
|
82
|
+
``is_multilabel``.
|
|
83
|
+
"""
|
|
84
|
+
if kind not in SEGMENTATION_TYPES:
|
|
85
|
+
raise ValueError(f"Unknown segmentation type {kind!r}")
|
|
86
|
+
explicit = supports_segmentation_types() if explicit is None else explicit
|
|
87
|
+
if not explicit:
|
|
88
|
+
if kind in ("instance", "panoptic"):
|
|
89
|
+
raise ValueError(f"This copick version has no {kind} segmentations.")
|
|
90
|
+
return {"is_multilabel": kind == "multilabel"}
|
|
91
|
+
return {"is_multilabel": kind == "multilabel", "is_instance": kind == "instance", "is_panoptic": kind == "panoptic"}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def filament_spec_of(obj: Any) -> Optional[dict]:
|
|
95
|
+
"""The filament declaration of a pickable object (``PickableObject`` or ``CopickObject``) as a dict, or None."""
|
|
96
|
+
spec = getattr(obj, "filament", None)
|
|
97
|
+
if spec is not None and not callable(spec):
|
|
98
|
+
return spec.model_dump(exclude_none=True) if hasattr(spec, "model_dump") else dict(spec)
|
|
99
|
+
meta = getattr(obj, "metadata", None)
|
|
100
|
+
if meta is None and hasattr(obj, "_meta"):
|
|
101
|
+
meta = getattr(obj._meta, "metadata", None)
|
|
102
|
+
if isinstance(meta, dict):
|
|
103
|
+
ns = meta.get(FILAMENT_METADATA_NAMESPACE)
|
|
104
|
+
if isinstance(ns, dict) and isinstance(ns.get(FILAMENT_METADATA_KEY), dict):
|
|
105
|
+
return dict(ns[FILAMENT_METADATA_KEY])
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_filament_object(obj: Any) -> bool:
|
|
110
|
+
"""Whether a pickable object is declared a filament."""
|
|
111
|
+
value = getattr(obj, "is_filament", None)
|
|
112
|
+
if isinstance(value, bool):
|
|
113
|
+
return value
|
|
114
|
+
return filament_spec_of(obj) is not None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def supports_filaments() -> bool:
|
|
118
|
+
"""Whether the installed copick has the Filaments entity."""
|
|
119
|
+
try:
|
|
120
|
+
from copick.models import CopickRun
|
|
121
|
+
except ImportError: # pragma: no cover
|
|
122
|
+
return False
|
|
123
|
+
return hasattr(CopickRun, "get_filaments")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def supports_segmentation_types() -> bool:
|
|
127
|
+
"""Whether the installed copick has instance and panoptic segmentations."""
|
|
128
|
+
try:
|
|
129
|
+
from copick.models import CopickSegmentation
|
|
130
|
+
except ImportError: # pragma: no cover
|
|
131
|
+
return False
|
|
132
|
+
return hasattr(CopickSegmentation, "segmentation_type")
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Dialog for editing and managing PickableObject types in the copick configuration
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import copy
|
|
6
|
+
from typing import List, Optional, Tuple
|
|
6
7
|
|
|
7
8
|
from copick.models import PickableObject
|
|
8
9
|
from qtpy.QtCore import Qt
|
|
@@ -10,6 +11,7 @@ from qtpy.QtGui import QFont
|
|
|
10
11
|
from qtpy.QtWidgets import (
|
|
11
12
|
QCheckBox,
|
|
12
13
|
QColorDialog,
|
|
14
|
+
QComboBox,
|
|
13
15
|
QDialog,
|
|
14
16
|
QDialogButtonBox,
|
|
15
17
|
QDoubleSpinBox,
|
|
@@ -29,8 +31,11 @@ from qtpy.QtWidgets import (
|
|
|
29
31
|
QWidget,
|
|
30
32
|
)
|
|
31
33
|
|
|
34
|
+
from copick_shared_ui.core.types import FILAMENT_METADATA_KEY, FILAMENT_METADATA_NAMESPACE, filament_spec_of
|
|
32
35
|
from copick_shared_ui.util.validation import validate_copick_name
|
|
33
36
|
|
|
37
|
+
POLARITY_CHOICES = [("Not stated", None), ("Polar", True), ("Apolar", False)]
|
|
38
|
+
|
|
34
39
|
|
|
35
40
|
class ColorButton(QPushButton):
|
|
36
41
|
"""Button that displays and allows selection of a color"""
|
|
@@ -261,6 +266,15 @@ class EditObjectTypesDialog(QDialog):
|
|
|
261
266
|
)
|
|
262
267
|
left_layout.addRow("Type:", self._is_particle_cb)
|
|
263
268
|
|
|
269
|
+
# Is filament checkbox (a filament is a particle annotated with ordered points along its axis)
|
|
270
|
+
self._is_filament_cb = QCheckBox("Is Filament")
|
|
271
|
+
self._is_filament_cb.setChecked(False)
|
|
272
|
+
self._is_filament_cb.setToolTip(
|
|
273
|
+
"Check for continuous assemblies (e.g. microtubules, actin) annotated with ordered points along their "
|
|
274
|
+
"axis.\nPicks then carry the filament ID as instance ID. Requires 'Is Particle'.",
|
|
275
|
+
)
|
|
276
|
+
left_layout.addRow("", self._is_filament_cb)
|
|
277
|
+
|
|
264
278
|
# Label (numeric ID)
|
|
265
279
|
self._label_spin = QSpinBox()
|
|
266
280
|
self._label_spin.setRange(1, 9999)
|
|
@@ -346,9 +360,39 @@ class EditObjectTypesDialog(QDialog):
|
|
|
346
360
|
|
|
347
361
|
right_group.setLayout(right_layout)
|
|
348
362
|
|
|
363
|
+
# Filament properties (stored as metadata["copick"]["filament"])
|
|
364
|
+
self._filament_group = QGroupBox("Filament Properties")
|
|
365
|
+
filament_layout = QFormLayout()
|
|
366
|
+
|
|
367
|
+
self._polarity_combo = QComboBox()
|
|
368
|
+
for text, _value in POLARITY_CHOICES:
|
|
369
|
+
self._polarity_combo.addItem(text)
|
|
370
|
+
self._polarity_combo.setToolTip("Whether the structure has a polarity (microtubules and actin are polar)")
|
|
371
|
+
filament_layout.addRow("Polarity:", self._polarity_combo)
|
|
372
|
+
|
|
373
|
+
self._rise_spin = QDoubleSpinBox()
|
|
374
|
+
self._rise_spin.setRange(0.0, 10000.0)
|
|
375
|
+
self._rise_spin.setDecimals(2)
|
|
376
|
+
self._rise_spin.setValue(0.0)
|
|
377
|
+
self._rise_spin.setSpecialValueText("Not set")
|
|
378
|
+
self._rise_spin.setToolTip("Axial rise per subunit (descriptive only; never used as a sampling distance)")
|
|
379
|
+
filament_layout.addRow("Helical rise (Å):", self._rise_spin)
|
|
380
|
+
|
|
381
|
+
self._twist_spin = QDoubleSpinBox()
|
|
382
|
+
self._twist_spin.setRange(-361.0, 360.0)
|
|
383
|
+
self._twist_spin.setDecimals(2)
|
|
384
|
+
self._twist_spin.setValue(-361.0)
|
|
385
|
+
self._twist_spin.setSpecialValueText("Not set")
|
|
386
|
+
self._twist_spin.setToolTip("Twist per subunit (descriptive only)")
|
|
387
|
+
filament_layout.addRow("Helical twist (°):", self._twist_spin)
|
|
388
|
+
|
|
389
|
+
self._filament_group.setLayout(filament_layout)
|
|
390
|
+
self._filament_group.setEnabled(False)
|
|
391
|
+
|
|
349
392
|
# Add groups to grid
|
|
350
393
|
grid_layout.addWidget(left_group, 0, 0)
|
|
351
394
|
grid_layout.addWidget(right_group, 0, 1)
|
|
395
|
+
grid_layout.addWidget(self._filament_group, 1, 0, 1, 2)
|
|
352
396
|
|
|
353
397
|
# Form action buttons
|
|
354
398
|
form_buttons_layout = QHBoxLayout()
|
|
@@ -393,6 +437,18 @@ class EditObjectTypesDialog(QDialog):
|
|
|
393
437
|
self._name_edit.textChanged.connect(self._validate_form)
|
|
394
438
|
self._label_spin.valueChanged.connect(self._validate_form)
|
|
395
439
|
|
|
440
|
+
# A filament must be a particle
|
|
441
|
+
self._is_particle_cb.toggled.connect(self._on_particle_toggled)
|
|
442
|
+
self._is_filament_cb.toggled.connect(self._on_filament_toggled)
|
|
443
|
+
|
|
444
|
+
def _on_particle_toggled(self, checked: bool):
|
|
445
|
+
self._is_filament_cb.setEnabled(checked)
|
|
446
|
+
if not checked:
|
|
447
|
+
self._is_filament_cb.setChecked(False)
|
|
448
|
+
|
|
449
|
+
def _on_filament_toggled(self, checked: bool):
|
|
450
|
+
self._filament_group.setEnabled(checked)
|
|
451
|
+
|
|
396
452
|
def _populate_objects_table(self):
|
|
397
453
|
"""Populate the objects table"""
|
|
398
454
|
self._objects_table.setRowCount(len(self._existing_objects))
|
|
@@ -404,7 +460,10 @@ class EditObjectTypesDialog(QDialog):
|
|
|
404
460
|
self._objects_table.setItem(row, 0, name_item)
|
|
405
461
|
|
|
406
462
|
# Type
|
|
407
|
-
|
|
463
|
+
if filament_spec_of(obj) is not None:
|
|
464
|
+
type_text = "Filament"
|
|
465
|
+
else:
|
|
466
|
+
type_text = "Particle" if obj.is_particle else "Segmentation"
|
|
408
467
|
type_item = QTableWidgetItem(type_text)
|
|
409
468
|
type_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
410
469
|
self._objects_table.setItem(row, 1, type_item)
|
|
@@ -626,6 +685,11 @@ class EditObjectTypesDialog(QDialog):
|
|
|
626
685
|
self._identifier_edit.clear()
|
|
627
686
|
self._threshold_spin.setValue(self._threshold_spin.minimum())
|
|
628
687
|
self._radius_spin.setValue(self._radius_spin.minimum())
|
|
688
|
+
self._is_filament_cb.setEnabled(True)
|
|
689
|
+
self._is_filament_cb.setChecked(False)
|
|
690
|
+
self._polarity_combo.setCurrentIndex(0)
|
|
691
|
+
self._rise_spin.setValue(self._rise_spin.minimum())
|
|
692
|
+
self._twist_spin.setValue(self._twist_spin.minimum())
|
|
629
693
|
|
|
630
694
|
# Hide validation messages
|
|
631
695
|
self._name_validation.hide()
|
|
@@ -658,6 +722,31 @@ class EditObjectTypesDialog(QDialog):
|
|
|
658
722
|
else:
|
|
659
723
|
self._radius_spin.setValue(self._radius_spin.minimum())
|
|
660
724
|
|
|
725
|
+
spec = filament_spec_of(obj)
|
|
726
|
+
self._is_filament_cb.setChecked(spec is not None)
|
|
727
|
+
spec = spec or {}
|
|
728
|
+
polarity = spec.get("polar")
|
|
729
|
+
self._polarity_combo.setCurrentIndex(next(i for i, (_t, v) in enumerate(POLARITY_CHOICES) if v == polarity))
|
|
730
|
+
rise = spec.get("helical_rise_a")
|
|
731
|
+
self._rise_spin.setValue(rise if rise is not None else self._rise_spin.minimum())
|
|
732
|
+
twist = spec.get("helical_twist_deg")
|
|
733
|
+
self._twist_spin.setValue(twist if twist is not None else self._twist_spin.minimum())
|
|
734
|
+
|
|
735
|
+
def _filament_spec_from_form(self, previous: Optional[dict]) -> Optional[dict]:
|
|
736
|
+
"""The filament spec in the form, keeping any extra keys of the previous spec; None if not a filament."""
|
|
737
|
+
if not (self._is_particle_cb.isChecked() and self._is_filament_cb.isChecked()):
|
|
738
|
+
return None
|
|
739
|
+
spec = dict(previous or {})
|
|
740
|
+
polar = POLARITY_CHOICES[self._polarity_combo.currentIndex()][1]
|
|
741
|
+
rise = self._rise_spin.value() if self._rise_spin.value() != self._rise_spin.minimum() else None
|
|
742
|
+
twist = self._twist_spin.value() if self._twist_spin.value() != self._twist_spin.minimum() else None
|
|
743
|
+
for key, value in (("polar", polar), ("helical_rise_a", rise), ("helical_twist_deg", twist)):
|
|
744
|
+
if value is None:
|
|
745
|
+
spec.pop(key, None)
|
|
746
|
+
else:
|
|
747
|
+
spec[key] = value
|
|
748
|
+
return spec
|
|
749
|
+
|
|
661
750
|
def _populate_initial_data(self):
|
|
662
751
|
"""Populate initial data for new object"""
|
|
663
752
|
existing_labels = {obj.label for obj in self._existing_objects if obj.label is not None}
|
|
@@ -686,7 +775,10 @@ class EditObjectTypesDialog(QDialog):
|
|
|
686
775
|
if self._radius_spin.value() != self._radius_spin.minimum():
|
|
687
776
|
radius = self._radius_spin.value()
|
|
688
777
|
|
|
689
|
-
|
|
778
|
+
# Start from the edited object so fields the form does not show (metadata, and anything newer copick
|
|
779
|
+
# versions add) survive the edit.
|
|
780
|
+
data = copy.deepcopy(self._selected_object.model_dump()) if self._selected_object is not None else {}
|
|
781
|
+
data.update(
|
|
690
782
|
name=name,
|
|
691
783
|
is_particle=is_particle,
|
|
692
784
|
label=label,
|
|
@@ -698,6 +790,25 @@ class EditObjectTypesDialog(QDialog):
|
|
|
698
790
|
radius=radius,
|
|
699
791
|
)
|
|
700
792
|
|
|
793
|
+
# Filament declaration: metadata["copick"]["filament"]
|
|
794
|
+
previous = filament_spec_of(self._selected_object) if self._selected_object is not None else None
|
|
795
|
+
spec = self._filament_spec_from_form(previous)
|
|
796
|
+
metadata = dict(data.get("metadata") or {})
|
|
797
|
+
namespace = metadata.get(FILAMENT_METADATA_NAMESPACE)
|
|
798
|
+
namespace = dict(namespace) if isinstance(namespace, dict) else {}
|
|
799
|
+
if spec is None:
|
|
800
|
+
namespace.pop(FILAMENT_METADATA_KEY, None)
|
|
801
|
+
else:
|
|
802
|
+
namespace[FILAMENT_METADATA_KEY] = spec
|
|
803
|
+
if namespace:
|
|
804
|
+
metadata[FILAMENT_METADATA_NAMESPACE] = namespace
|
|
805
|
+
else:
|
|
806
|
+
metadata.pop(FILAMENT_METADATA_NAMESPACE, None)
|
|
807
|
+
if metadata or "metadata" in data:
|
|
808
|
+
data["metadata"] = metadata
|
|
809
|
+
|
|
810
|
+
return PickableObject(**data)
|
|
811
|
+
|
|
701
812
|
def _validate_form(self):
|
|
702
813
|
"""Validate the current form state"""
|
|
703
814
|
name_text = self._name_edit.text().strip()
|
|
@@ -794,6 +905,7 @@ class EditObjectTypesDialog(QDialog):
|
|
|
794
905
|
or current.identifier != original.identifier
|
|
795
906
|
or current.map_threshold != original.map_threshold
|
|
796
907
|
or current.radius != original.radius
|
|
908
|
+
or getattr(current, "metadata", None) != getattr(original, "metadata", None)
|
|
797
909
|
):
|
|
798
910
|
return True
|
|
799
911
|
|