karcytics-sdk 3.2.0__tar.gz → 3.3.0__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.
- {karcytics_sdk-3.2.0/src/karcytics_sdk.egg-info → karcytics_sdk-3.3.0}/PKG-INFO +1 -1
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/pyproject.toml +1 -1
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/contrib/image_utils.py +3 -3
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/__init__.py +38 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/analysis.py +11 -1
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/components.py +21 -5
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/image_view.py +1 -1
- karcytics_sdk-3.3.0/src/karcytics_sdk/plugin/params.py +58 -0
- karcytics_sdk-3.3.0/src/karcytics_sdk/plugin/params_form.py +319 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/mpl_canvas.py +8 -3
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/runtime_services.py +22 -10
- karcytics_sdk-3.3.0/src/karcytics_sdk/plugin/stage_rail.py +139 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/tasks.py +1 -1
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/ui_daemon_runtime.py +36 -4
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/version.py +1 -1
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/testing/ui_daemon.py +10 -1
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0/src/karcytics_sdk.egg-info}/PKG-INFO +1 -1
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/SOURCES.txt +3 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/LICENSE +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/README.md +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/setup.cfg +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/compliance.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/diagnostics.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/migrate.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/scaffold.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/security.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/main.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/contrib/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/contrib/imaging.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/baseline.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/catalog.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/design_rules.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/functional_rules.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/model.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/org_rules.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/registry.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/repo.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/report.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/runner.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/source.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/test_rules.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/ai.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/core_services.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/docs.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/marketplace_cache.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/module_status_widget.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/qt_bridge.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/sign_plugin.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/trust_manager.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/trust_overrides.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/trust_path.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/trust_storage.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/i_crash_reporter.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/i_event_bus.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/i_logger.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/i_task_scheduler.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/academy.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/academy_driver.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/academy_lock_card.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/academy_window.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/autosave.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/base.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/citation_widgets.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/citations.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/context.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/course_complete_overlay.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/cyto_character.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/cyto_costumes.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/daemon.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/dialogs.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/draggable.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/effects.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/events.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/galactic_loader.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/galactic_loader.qml +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/history.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/interfaces.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/io.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/logging.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/managed_task.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/manifest.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/manifest_parser.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/menu_builder.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/preferences.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/question_panel.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/axes.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/graphics_scene.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/lock.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/pipeline.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/ribbon.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/security_parser.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/signals.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/state.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/theme_fallback.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/theme_overlay.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/timing.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/toast.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/tutorial_highlight.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/tutorial_models.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/tutorial_overlay.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/ui_preferences.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/update_check.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/validation.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/wizard.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/worker_thread.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/workflow.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/zoom_controls.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/py.typed +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/sdk_cli.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/testing/__init__.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/testing/contract.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/dependency_links.txt +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/entry_points.txt +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/requires.txt +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/top_level.txt +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_host_ai.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_host_security.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_marketplace_cache.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_namespaces.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_plugin_architecture.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_plugin_ribbon.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_plugin_state.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_plugin_ui.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_sdk_cli.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_sign_plugin.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_testing_contract.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_theme_fallback.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_ui_daemon_runtime.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_workflow_context.py +0 -0
- {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_workflow_dialogs.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Image loading, format conversion, and preprocessing utilities (deprecated).
|
|
2
2
|
|
|
3
|
-
Deprecated: this is
|
|
4
|
-
|
|
3
|
+
Deprecated: this is band-image logic from the Gel & Blot plugin (formerly western-blot),
|
|
4
|
+
which now owns it. It will be removed in karcytics-sdk 4.0.
|
|
5
5
|
|
|
6
6
|
This module provides low-level image I/O and transformation functions
|
|
7
7
|
used throughout the Karcytics analysis pipeline. All functions operate on
|
|
@@ -36,7 +36,7 @@ from skimage.util import img_as_float64
|
|
|
36
36
|
|
|
37
37
|
warnings.warn(
|
|
38
38
|
"karcytics_sdk.contrib.image_utils is deprecated and will be removed in karcytics-sdk 4.0: it is "
|
|
39
|
-
"
|
|
39
|
+
"band-image logic that now lives in the Gel & Blot plugin (formerly western-blot). Copy what you need into your plugin.",
|
|
40
40
|
DeprecationWarning,
|
|
41
41
|
stacklevel=2,
|
|
42
42
|
)
|
|
@@ -19,12 +19,16 @@ if TYPE_CHECKING:
|
|
|
19
19
|
BioCaptionLabel,
|
|
20
20
|
BioComboBox,
|
|
21
21
|
BioDoubleSpinBox,
|
|
22
|
+
BioFooter,
|
|
22
23
|
BioHeaderLabel,
|
|
23
24
|
BioHelpButton,
|
|
24
25
|
BioLineEdit,
|
|
25
26
|
BioListWidget,
|
|
27
|
+
BioMenu,
|
|
28
|
+
BioProgressDialog,
|
|
26
29
|
BioRunButton,
|
|
27
30
|
BioScrollArea,
|
|
31
|
+
BioSectionLabel,
|
|
28
32
|
BioSpinBox,
|
|
29
33
|
BioSplitter,
|
|
30
34
|
BioStatusLabel,
|
|
@@ -36,6 +40,7 @@ if TYPE_CHECKING:
|
|
|
36
40
|
PrimaryButton,
|
|
37
41
|
SecondaryButton,
|
|
38
42
|
SubtitleLabel,
|
|
43
|
+
WorkspaceSaveButton,
|
|
39
44
|
apply_component_style,
|
|
40
45
|
repopulate_combo,
|
|
41
46
|
)
|
|
@@ -65,6 +70,8 @@ if TYPE_CHECKING:
|
|
|
65
70
|
from .logging import get_logger
|
|
66
71
|
from .managed_task import FunctionalTask
|
|
67
72
|
from .manifest import PluginManifest
|
|
73
|
+
from .params import Param
|
|
74
|
+
from .params_form import ParamsForm
|
|
68
75
|
from .preferences import PreferenceManagerProtocol
|
|
69
76
|
from .rendering import (
|
|
70
77
|
MPL_RASTER_LOCK,
|
|
@@ -91,6 +98,7 @@ if TYPE_CHECKING:
|
|
|
91
98
|
tutorial_manager,
|
|
92
99
|
)
|
|
93
100
|
from .signals import PluginSignals, move_to_gui_thread, safe_disconnect
|
|
101
|
+
from .stage_rail import StageRail, StageStatus
|
|
94
102
|
from .state import PluginState
|
|
95
103
|
from .tasks import TaskHandle, submit_task
|
|
96
104
|
from .timing import Debouncer
|
|
@@ -153,12 +161,16 @@ _EXPORTS: dict[str, tuple[str, ...]] = {
|
|
|
153
161
|
"BioCaptionLabel",
|
|
154
162
|
"BioComboBox",
|
|
155
163
|
"BioDoubleSpinBox",
|
|
164
|
+
"BioFooter",
|
|
156
165
|
"BioHeaderLabel",
|
|
157
166
|
"BioHelpButton",
|
|
158
167
|
"BioLineEdit",
|
|
159
168
|
"BioListWidget",
|
|
169
|
+
"BioMenu",
|
|
170
|
+
"BioProgressDialog",
|
|
160
171
|
"BioRunButton",
|
|
161
172
|
"BioScrollArea",
|
|
173
|
+
"BioSectionLabel",
|
|
162
174
|
"BioSpinBox",
|
|
163
175
|
"BioSplitter",
|
|
164
176
|
"BioStatusLabel",
|
|
@@ -170,6 +182,7 @@ _EXPORTS: dict[str, tuple[str, ...]] = {
|
|
|
170
182
|
"PrimaryButton",
|
|
171
183
|
"SecondaryButton",
|
|
172
184
|
"SubtitleLabel",
|
|
185
|
+
"WorkspaceSaveButton",
|
|
173
186
|
"apply_component_style",
|
|
174
187
|
"repopulate_combo",
|
|
175
188
|
),
|
|
@@ -199,6 +212,8 @@ _EXPORTS: dict[str, tuple[str, ...]] = {
|
|
|
199
212
|
".logging": ("get_logger",),
|
|
200
213
|
".managed_task": ("FunctionalTask",),
|
|
201
214
|
".manifest": ("PluginManifest",),
|
|
215
|
+
".params": ("Param",),
|
|
216
|
+
".params_form": ("ParamsForm",),
|
|
202
217
|
".preferences": ("PreferenceManagerProtocol",),
|
|
203
218
|
".rendering": (
|
|
204
219
|
"MPL_RASTER_LOCK",
|
|
@@ -224,6 +239,7 @@ _EXPORTS: dict[str, tuple[str, ...]] = {
|
|
|
224
239
|
"task_scheduler",
|
|
225
240
|
"tutorial_manager",
|
|
226
241
|
),
|
|
242
|
+
".stage_rail": ("StageRail", "StageStatus"),
|
|
227
243
|
".signals": ("PluginSignals", "move_to_gui_thread", "safe_disconnect"),
|
|
228
244
|
".state": ("PluginState",),
|
|
229
245
|
".tasks": ("TaskHandle", "submit_task"),
|
|
@@ -345,13 +361,17 @@ __all__ = [
|
|
|
345
361
|
"AxesColors",
|
|
346
362
|
"BioComboBox",
|
|
347
363
|
"BioDoubleSpinBox",
|
|
364
|
+
"BioFooter",
|
|
348
365
|
"BioHeaderLabel",
|
|
349
366
|
"BioHelpButton",
|
|
350
367
|
"BioLineEdit",
|
|
351
368
|
"BioListWidget",
|
|
369
|
+
"BioMenu",
|
|
370
|
+
"BioProgressDialog",
|
|
352
371
|
"BioRibbon",
|
|
353
372
|
"BioRunButton",
|
|
354
373
|
"BioScrollArea",
|
|
374
|
+
"BioSectionLabel",
|
|
355
375
|
"BioSpinBox",
|
|
356
376
|
"BioSplitter",
|
|
357
377
|
"BioStatusLabel",
|
|
@@ -365,6 +385,7 @@ __all__ = [
|
|
|
365
385
|
"StepIndicator",
|
|
366
386
|
"SubtitleLabel",
|
|
367
387
|
"WizardPanel",
|
|
388
|
+
"WorkspaceSaveButton",
|
|
368
389
|
"WizardStep",
|
|
369
390
|
"apply_component_style",
|
|
370
391
|
# Dialogs
|
|
@@ -408,6 +429,10 @@ __all__ = [
|
|
|
408
429
|
"array_to_pixmap",
|
|
409
430
|
"FunctionalTask",
|
|
410
431
|
"TaskHandle",
|
|
432
|
+
"Param",
|
|
433
|
+
"ParamsForm",
|
|
434
|
+
"StageRail",
|
|
435
|
+
"StageStatus",
|
|
411
436
|
]
|
|
412
437
|
|
|
413
438
|
|
|
@@ -426,5 +451,18 @@ def __getattr__(name: str) -> object:
|
|
|
426
451
|
return value
|
|
427
452
|
|
|
428
453
|
|
|
454
|
+
def _preload_exports() -> None:
|
|
455
|
+
"""Import every eagerly-exported name now, as the old barrel did.
|
|
456
|
+
|
|
457
|
+
The UI daemon calls this before its stdin-reader thread starts: on
|
|
458
|
+
Windows, importing native-extension-heavy modules on the main thread
|
|
459
|
+
while that thread is blocked reading stdin can deadlock (see
|
|
460
|
+
``ui_daemon_runtime.run``). ``LayeredMatplotlibCanvas`` stays lazy, as it
|
|
461
|
+
always was.
|
|
462
|
+
"""
|
|
463
|
+
for name in (*(n for names in _EXPORTS.values() for n in names), *_ALIASES):
|
|
464
|
+
__getattr__(name)
|
|
465
|
+
|
|
466
|
+
|
|
429
467
|
def __dir__() -> list[str]:
|
|
430
468
|
return sorted(set(globals()) | set(_LOCATIONS))
|
|
@@ -7,6 +7,7 @@ the UI.
|
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
9
|
from abc import ABC, abstractmethod
|
|
10
|
+
from collections.abc import Callable
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
from PySide6.QtCore import QObject, QRunnable, Signal
|
|
@@ -177,14 +178,17 @@ class AnalysisRunnable(QRunnable):
|
|
|
177
178
|
standard QThreadPool management system.
|
|
178
179
|
"""
|
|
179
180
|
|
|
180
|
-
def __init__(self, worker: AnalysisWorker):
|
|
181
|
+
def __init__(self, worker: AnalysisWorker, on_done: Callable[[], None] | None = None):
|
|
181
182
|
"""Initialize the runnable wrapper with a target worker.
|
|
182
183
|
|
|
183
184
|
Args:
|
|
184
185
|
worker: The background AnalysisWorker controller containing the job.
|
|
186
|
+
on_done: Called on the pool thread once this runnable has let go of
|
|
187
|
+
the worker, so its owner can release it on its own thread.
|
|
185
188
|
"""
|
|
186
189
|
super().__init__()
|
|
187
190
|
self.worker: AnalysisWorker | None = worker
|
|
191
|
+
self._on_done = on_done
|
|
188
192
|
# QThreadPool will take ownership and delete this runnable after run()
|
|
189
193
|
self.setAutoDelete(True)
|
|
190
194
|
|
|
@@ -204,4 +208,10 @@ class AnalysisRunnable(QRunnable):
|
|
|
204
208
|
# Fallback for truly catastrophic failures inside the worker itself
|
|
205
209
|
logger.exception(f"Critical failure in background runnable: {e}")
|
|
206
210
|
finally:
|
|
211
|
+
# Let go first: whoever releases the worker after on_done then
|
|
212
|
+
# holds the last reference, so it is never destroyed here, on the
|
|
213
|
+
# pool thread, while the GUI thread may still be delivering its
|
|
214
|
+
# signals.
|
|
207
215
|
self.worker = None
|
|
216
|
+
if self._on_done is not None:
|
|
217
|
+
self._on_done()
|
|
@@ -540,6 +540,17 @@ class BioCaptionLabel(QLabel):
|
|
|
540
540
|
self.setStyleSheet(f"font-size: {size}px; color: {Colors.FG_SECONDARY};")
|
|
541
541
|
|
|
542
542
|
|
|
543
|
+
class BioSectionLabel(QLabel):
|
|
544
|
+
"""A small bold heading over a group of controls, e.g. in a tab's sidebar."""
|
|
545
|
+
|
|
546
|
+
def __init__(self, text: str, parent=None):
|
|
547
|
+
super().__init__(text, parent)
|
|
548
|
+
_connect_theme_signal(self._apply_theme_styles)
|
|
549
|
+
|
|
550
|
+
def _apply_theme_styles(self) -> None:
|
|
551
|
+
self.setStyleSheet(f"color: {Colors.FG_SECONDARY}; font-weight: bold; font-size: 11px;")
|
|
552
|
+
|
|
553
|
+
|
|
543
554
|
class BioStatusLabel(QLabel):
|
|
544
555
|
"""Standardized italicized status text."""
|
|
545
556
|
|
|
@@ -637,11 +648,14 @@ class BioComboBox(QComboBox):
|
|
|
637
648
|
""")
|
|
638
649
|
|
|
639
650
|
|
|
651
|
+
_KEEP_SELECTION = object()
|
|
652
|
+
|
|
653
|
+
|
|
640
654
|
def repopulate_combo(
|
|
641
655
|
combo: QComboBox,
|
|
642
656
|
items: Sequence[tuple[str, Any]],
|
|
643
657
|
*,
|
|
644
|
-
restore_data: Any =
|
|
658
|
+
restore_data: Any = _KEEP_SELECTION,
|
|
645
659
|
) -> None:
|
|
646
660
|
"""Rebuild a combo box's items and restore a selection, without spurious signals.
|
|
647
661
|
|
|
@@ -658,8 +672,9 @@ def repopulate_combo(
|
|
|
658
672
|
items: Ordered (label, data) pairs for every item to show.
|
|
659
673
|
restore_data: The `data` value to reselect if still present in
|
|
660
674
|
`items` after rebuilding — typically the combo's own
|
|
661
|
-
`currentData()` captured before calling this. `None`
|
|
662
|
-
|
|
675
|
+
`currentData()` captured before calling this. `None` is a value
|
|
676
|
+
like any other (an "All events" item often carries it). Leave it
|
|
677
|
+
out to keep the index wherever `clear()`/`addItem()` land it.
|
|
663
678
|
|
|
664
679
|
Returns:
|
|
665
680
|
None.
|
|
@@ -667,7 +682,8 @@ def repopulate_combo(
|
|
|
667
682
|
items = list(items)
|
|
668
683
|
current = [(combo.itemText(i), combo.itemData(i)) for i in range(combo.count())]
|
|
669
684
|
items_unchanged = current == items
|
|
670
|
-
|
|
685
|
+
keep = restore_data is _KEEP_SELECTION
|
|
686
|
+
selection_already_right = keep or combo.currentData() == restore_data
|
|
671
687
|
|
|
672
688
|
if items_unchanged and selection_already_right:
|
|
673
689
|
return
|
|
@@ -677,7 +693,7 @@ def repopulate_combo(
|
|
|
677
693
|
combo.clear()
|
|
678
694
|
for label, data in items:
|
|
679
695
|
combo.addItem(label, data)
|
|
680
|
-
if
|
|
696
|
+
if not keep:
|
|
681
697
|
idx = combo.findData(restore_data)
|
|
682
698
|
if idx >= 0:
|
|
683
699
|
combo.setCurrentIndex(idx)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""A zoomable, pannable image viewer for image-analysis plugins.
|
|
2
2
|
|
|
3
3
|
`ImageView` is the part every image plugin's canvas had re-implemented
|
|
4
|
-
(
|
|
4
|
+
(gel-blot's `ImageCanvas`, cytometrics' `MultiChannelCanvas`): showing
|
|
5
5
|
an image, wheel zoom around the cursor with limits, panning, fit-to-view, an
|
|
6
6
|
empty state with a load button and drag-and-drop, a calibration line, a
|
|
7
7
|
scale bar, and named overlay layers. Plugins subclass it and add their own
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""User-settable parameters, declared once and turned into controls by ``ParamsForm``.
|
|
2
|
+
|
|
3
|
+
An algorithm (a segmenter, a background method, a lane finder…) declares
|
|
4
|
+
what it can be told as a list of :class:`Param`. The form builds the
|
|
5
|
+
controls from that list, so adding a parameter never means editing UI code.
|
|
6
|
+
This module imports no Qt, so analysis code can declare parameters freely.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
#: The kinds a ``Param`` can be. ``options`` is a pick-list whose entries
|
|
15
|
+
#: arrive at runtime (channel names, image names…) through
|
|
16
|
+
#: ``ParamsForm.set_options(param.source, names)``.
|
|
17
|
+
KINDS = ("float", "int", "bool", "choice", "options")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Param:
|
|
22
|
+
"""One parameter: enough to build its control, label it and explain it.
|
|
23
|
+
|
|
24
|
+
``unit`` is shown after the value. A form can map a unit to another
|
|
25
|
+
display (µm instead of px, say) with ``ParamsForm.set_unit_display``;
|
|
26
|
+
the algorithm always receives the value in the unit it declared.
|
|
27
|
+
``advanced`` parameters sit behind the form's "Advanced settings" toggle.
|
|
28
|
+
``enabled_by`` names a ``bool`` parameter that switches this one on.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
key: str
|
|
32
|
+
label: str
|
|
33
|
+
kind: str
|
|
34
|
+
default: Any = None
|
|
35
|
+
minimum: float | None = None
|
|
36
|
+
maximum: float | None = None
|
|
37
|
+
step: float | None = None
|
|
38
|
+
decimals: int = 1
|
|
39
|
+
unit: str = ""
|
|
40
|
+
choices: tuple[tuple[str, Any], ...] = ()
|
|
41
|
+
source: str = ""
|
|
42
|
+
advanced: bool = False
|
|
43
|
+
help: str = ""
|
|
44
|
+
special_value_text: str = ""
|
|
45
|
+
enabled_by: str = ""
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if self.kind not in KINDS:
|
|
49
|
+
raise ValueError(f"Param {self.key!r}: kind must be one of {KINDS}, not {self.kind!r}")
|
|
50
|
+
if self.kind == "options" and not self.source:
|
|
51
|
+
raise ValueError(f"Param {self.key!r}: an 'options' parameter needs a source")
|
|
52
|
+
if self.kind == "choice" and not self.choices:
|
|
53
|
+
raise ValueError(f"Param {self.key!r}: a 'choice' parameter needs choices")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def defaults(params: list[Param]) -> dict[str, Any]:
|
|
57
|
+
"""Every parameter's default, keyed like ``ParamsForm.values()``."""
|
|
58
|
+
return {p.key: p.default for p in params}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Controls generated from a list of :class:`~karcytics_sdk.plugin.params.Param`.
|
|
2
|
+
|
|
3
|
+
Basic parameters are shown; advanced ones sit behind an "Advanced settings"
|
|
4
|
+
toggle. Values the user typed are remembered by key, so swapping one
|
|
5
|
+
algorithm's parameters for another's keeps the shared ones. A form can
|
|
6
|
+
show units differently from how the algorithm takes them (µm on screen,
|
|
7
|
+
pixels to the algorithm) through ``set_unit_display``, and can keep a
|
|
8
|
+
user's preferred values as "my defaults" in any preference store.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import Qt, Signal
|
|
17
|
+
from PySide6.QtWidgets import QCheckBox, QGridLayout, QLabel, QVBoxLayout, QWidget
|
|
18
|
+
|
|
19
|
+
from .components import (
|
|
20
|
+
BioComboBox,
|
|
21
|
+
BioDoubleSpinBox,
|
|
22
|
+
BioHelpButton,
|
|
23
|
+
BioToggleButton,
|
|
24
|
+
_connect_theme_signal,
|
|
25
|
+
_get_theme_tokens,
|
|
26
|
+
)
|
|
27
|
+
from .params import Param
|
|
28
|
+
from .preferences import PreferenceManagerProtocol
|
|
29
|
+
|
|
30
|
+
_ADVANCED_CLOSED = "Advanced settings ▸"
|
|
31
|
+
_ADVANCED_OPEN = "Advanced settings ▾"
|
|
32
|
+
|
|
33
|
+
#: Maps a parameter's declared unit to (suffix shown, display units per declared unit).
|
|
34
|
+
UnitDisplay = Callable[[str], tuple[str, float]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _unit_as_declared(unit: str) -> tuple[str, float]:
|
|
38
|
+
return unit, 1.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ParamsForm(QWidget):
|
|
42
|
+
"""A form for a list of parameters; ``changed`` fires on every edit."""
|
|
43
|
+
|
|
44
|
+
changed = Signal()
|
|
45
|
+
|
|
46
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
47
|
+
super().__init__(parent)
|
|
48
|
+
self._params: list[Param] = []
|
|
49
|
+
self._widgets: dict[str, QWidget] = {}
|
|
50
|
+
self._labels: dict[str, QLabel] = {}
|
|
51
|
+
self._helps: dict[str, BioHelpButton] = {}
|
|
52
|
+
self._memory: dict[str, Any] = {}
|
|
53
|
+
# The default of the parameter each remembered value was set on: a value only
|
|
54
|
+
# carries over to a parameter with the same default (another algorithm's "Max
|
|
55
|
+
# Area" of 1e6 is not this one's 5000). Values set explicitly carry over anywhere.
|
|
56
|
+
self._memory_defaults: dict[str, Any] = {}
|
|
57
|
+
self._hidden: set[str] = set()
|
|
58
|
+
self._options: dict[str, list[str]] = {}
|
|
59
|
+
self._unit_display: UnitDisplay = _unit_as_declared
|
|
60
|
+
|
|
61
|
+
layout = QVBoxLayout(self)
|
|
62
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
63
|
+
self._basic = QWidget()
|
|
64
|
+
self._basic_grid = _grid(self._basic)
|
|
65
|
+
layout.addWidget(self._basic)
|
|
66
|
+
|
|
67
|
+
self.btn_advanced = BioToggleButton(_ADVANCED_CLOSED)
|
|
68
|
+
self.btn_advanced.toggled.connect(self._on_advanced_toggled)
|
|
69
|
+
layout.addWidget(self.btn_advanced)
|
|
70
|
+
self._advanced = QWidget()
|
|
71
|
+
self._advanced_grid = _grid(self._advanced)
|
|
72
|
+
self._advanced.setVisible(False)
|
|
73
|
+
layout.addWidget(self._advanced)
|
|
74
|
+
self.btn_advanced.setVisible(False)
|
|
75
|
+
|
|
76
|
+
_connect_theme_signal(self.apply_theme)
|
|
77
|
+
|
|
78
|
+
# ── Building ─────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
def set_params(self, params: list[Param]) -> None:
|
|
81
|
+
"""Rebuild the controls for `params`, keeping values the user already set."""
|
|
82
|
+
self._memory.update(self.display_values())
|
|
83
|
+
self._memory_defaults.update({p.key: p.default for p in self._params})
|
|
84
|
+
for w in [*self._widgets.values(), *self._labels.values(), *self._helps.values()]:
|
|
85
|
+
w.setParent(None) # type: ignore[call-overload] # PySide6 stub omits Optional
|
|
86
|
+
w.deleteLater()
|
|
87
|
+
self._widgets.clear()
|
|
88
|
+
self._labels.clear()
|
|
89
|
+
self._helps.clear()
|
|
90
|
+
self._params = list(params)
|
|
91
|
+
|
|
92
|
+
rows = {False: 0, True: 0}
|
|
93
|
+
self.setVisible(bool(self._params))
|
|
94
|
+
for p in self._params:
|
|
95
|
+
grid = self._advanced_grid if p.advanced else self._basic_grid
|
|
96
|
+
widget = self._make_widget(p)
|
|
97
|
+
self._widgets[p.key] = widget
|
|
98
|
+
r = rows[p.advanced]
|
|
99
|
+
if p.kind == "bool":
|
|
100
|
+
grid.addWidget(widget, r, 1)
|
|
101
|
+
else:
|
|
102
|
+
label = QLabel(f"{p.label}:")
|
|
103
|
+
label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
104
|
+
label.setToolTip(p.help)
|
|
105
|
+
self._labels[p.key] = label
|
|
106
|
+
grid.addWidget(label, r, 0)
|
|
107
|
+
grid.addWidget(widget, r, 1)
|
|
108
|
+
if p.help:
|
|
109
|
+
button = BioHelpButton()
|
|
110
|
+
button.setHelpText(p.help, p.label)
|
|
111
|
+
self._helps[p.key] = button
|
|
112
|
+
grid.addWidget(button, r, 2)
|
|
113
|
+
rows[p.advanced] = r + 1
|
|
114
|
+
|
|
115
|
+
self._apply_hidden()
|
|
116
|
+
has_advanced = rows[True] > 0
|
|
117
|
+
self.btn_advanced.setVisible(has_advanced)
|
|
118
|
+
self._advanced.setVisible(has_advanced and self.btn_advanced.isChecked())
|
|
119
|
+
self._apply_units()
|
|
120
|
+
self._apply_dependencies()
|
|
121
|
+
self.apply_theme()
|
|
122
|
+
|
|
123
|
+
def set_options(self, source: str, names: list[str]) -> None:
|
|
124
|
+
"""Offer `names` in every ``options`` picker fed by `source`, keeping each choice that survives."""
|
|
125
|
+
self._options[source] = list(names)
|
|
126
|
+
for p in self._params:
|
|
127
|
+
if p.kind == "options" and p.source == source:
|
|
128
|
+
combo = self._widgets[p.key]
|
|
129
|
+
assert isinstance(combo, BioComboBox)
|
|
130
|
+
current = combo.currentText() or self._memory.get(p.key, "")
|
|
131
|
+
combo.blockSignals(True)
|
|
132
|
+
combo.clear()
|
|
133
|
+
combo.addItems(names)
|
|
134
|
+
if current in names:
|
|
135
|
+
combo.setCurrentText(current)
|
|
136
|
+
combo.blockSignals(False)
|
|
137
|
+
|
|
138
|
+
def set_unit_display(self, unit_display: UnitDisplay) -> None:
|
|
139
|
+
"""Show units through `unit_display`. The numbers the user typed are kept."""
|
|
140
|
+
self._unit_display = unit_display
|
|
141
|
+
self._apply_units()
|
|
142
|
+
|
|
143
|
+
def set_hidden(self, keys: set[str]) -> None:
|
|
144
|
+
"""Hide the controls of `keys`; their values are still read and written."""
|
|
145
|
+
self._hidden = set(keys)
|
|
146
|
+
self._apply_hidden()
|
|
147
|
+
|
|
148
|
+
# ── Values ───────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
def display_values(self) -> dict[str, Any]:
|
|
151
|
+
"""Every control's value as shown (in the displayed unit)."""
|
|
152
|
+
return {p.key: self._read(p) for p in self._params}
|
|
153
|
+
|
|
154
|
+
def set_display_values(self, values: dict[str, Any]) -> None:
|
|
155
|
+
"""Set controls from values in the displayed unit; unknown keys are remembered for later."""
|
|
156
|
+
self._memory.update(values)
|
|
157
|
+
for key in values:
|
|
158
|
+
self._memory_defaults.pop(key, None)
|
|
159
|
+
for p in self._params:
|
|
160
|
+
if p.key in values:
|
|
161
|
+
self._write(p, values[p.key])
|
|
162
|
+
self._apply_dependencies()
|
|
163
|
+
|
|
164
|
+
def values(self) -> dict[str, Any]:
|
|
165
|
+
"""The algorithm's input: every value converted back to its declared unit."""
|
|
166
|
+
out: dict[str, Any] = {}
|
|
167
|
+
for p in self._params:
|
|
168
|
+
value = self._read(p)
|
|
169
|
+
if p.kind in ("float", "int") and p.unit:
|
|
170
|
+
value = value / self._unit_display(p.unit)[1]
|
|
171
|
+
if p.kind == "int":
|
|
172
|
+
value = int(round(value))
|
|
173
|
+
out[p.key] = value
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
def reset_to_defaults(self) -> None:
|
|
177
|
+
"""Put every control back to its parameter's declared default."""
|
|
178
|
+
self.set_display_values({p.key: p.default for p in self._params})
|
|
179
|
+
self.changed.emit()
|
|
180
|
+
|
|
181
|
+
def save_defaults(self, store: PreferenceManagerProtocol, namespace: str) -> None:
|
|
182
|
+
"""Keep the current values as this user's defaults for `namespace` in `store`."""
|
|
183
|
+
saved = dict(store.get(namespace, {}) or {})
|
|
184
|
+
saved.update(self.display_values())
|
|
185
|
+
store.set(namespace, saved)
|
|
186
|
+
store.save()
|
|
187
|
+
|
|
188
|
+
def load_defaults(self, store: PreferenceManagerProtocol, namespace: str) -> bool:
|
|
189
|
+
"""Apply this user's saved defaults for `namespace`, if any. Returns whether there were any."""
|
|
190
|
+
saved = store.get(namespace, {}) or {}
|
|
191
|
+
if not saved:
|
|
192
|
+
return False
|
|
193
|
+
self.set_display_values(dict(saved))
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
# ── Access for tests and hosts ───────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
def widget(self, key: str) -> QWidget | None:
|
|
199
|
+
return self._widgets.get(key)
|
|
200
|
+
|
|
201
|
+
def label(self, key: str) -> QLabel | None:
|
|
202
|
+
return self._labels.get(key)
|
|
203
|
+
|
|
204
|
+
def apply_theme(self) -> None:
|
|
205
|
+
"""Re-colour the plain Qt widgets (the Bio* ones restyle themselves)."""
|
|
206
|
+
colors, _, _ = _get_theme_tokens()
|
|
207
|
+
for p in self._params:
|
|
208
|
+
if p.kind == "bool":
|
|
209
|
+
self._widgets[p.key].setStyleSheet(f"color: {colors.FG_PRIMARY};")
|
|
210
|
+
for label in self._labels.values():
|
|
211
|
+
label.setStyleSheet(f"color: {colors.FG_PRIMARY};")
|
|
212
|
+
|
|
213
|
+
# ── Internals ────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
def _remembered(self, p: Param) -> Any:
|
|
216
|
+
if p.key in self._memory and self._memory_defaults.get(p.key, p.default) == p.default:
|
|
217
|
+
return self._memory[p.key]
|
|
218
|
+
return p.default
|
|
219
|
+
|
|
220
|
+
def _make_widget(self, p: Param) -> QWidget:
|
|
221
|
+
remembered = self._remembered(p)
|
|
222
|
+
if p.kind == "bool":
|
|
223
|
+
box = QCheckBox(p.label)
|
|
224
|
+
box.setChecked(bool(remembered))
|
|
225
|
+
box.setToolTip(p.help)
|
|
226
|
+
box.toggled.connect(self._on_changed)
|
|
227
|
+
return box
|
|
228
|
+
if p.kind in ("options", "choice"):
|
|
229
|
+
combo = BioComboBox()
|
|
230
|
+
combo.setMinimumWidth(180)
|
|
231
|
+
combo.view().setMinimumWidth(300)
|
|
232
|
+
combo.setToolTip(p.help)
|
|
233
|
+
if p.kind == "options":
|
|
234
|
+
names = self._options.get(p.source, [])
|
|
235
|
+
combo.addItems(names)
|
|
236
|
+
if remembered in names:
|
|
237
|
+
combo.setCurrentText(remembered)
|
|
238
|
+
else:
|
|
239
|
+
for text, data in p.choices:
|
|
240
|
+
combo.addItem(text, data)
|
|
241
|
+
combo.setCurrentIndex(max(combo.findData(remembered), 0))
|
|
242
|
+
combo.currentIndexChanged.connect(self._on_changed)
|
|
243
|
+
return combo
|
|
244
|
+
spin = BioDoubleSpinBox()
|
|
245
|
+
spin.setDecimals(0 if p.kind == "int" else p.decimals)
|
|
246
|
+
spin.setRange(
|
|
247
|
+
p.minimum if p.minimum is not None else -1e9,
|
|
248
|
+
p.maximum if p.maximum is not None else 1e9,
|
|
249
|
+
)
|
|
250
|
+
if p.step is not None:
|
|
251
|
+
spin.setSingleStep(p.step)
|
|
252
|
+
if p.special_value_text:
|
|
253
|
+
spin.setSpecialValueText(p.special_value_text)
|
|
254
|
+
spin.setValue(float(remembered or 0))
|
|
255
|
+
spin.setToolTip(p.help)
|
|
256
|
+
spin.valueChanged.connect(self._on_changed)
|
|
257
|
+
return spin
|
|
258
|
+
|
|
259
|
+
def _read(self, p: Param) -> Any:
|
|
260
|
+
w = self._widgets[p.key]
|
|
261
|
+
if isinstance(w, QCheckBox):
|
|
262
|
+
return w.isChecked()
|
|
263
|
+
if isinstance(w, BioComboBox):
|
|
264
|
+
return w.currentText() if p.kind == "options" else w.currentData()
|
|
265
|
+
assert isinstance(w, BioDoubleSpinBox)
|
|
266
|
+
return int(w.value()) if p.kind == "int" else w.value()
|
|
267
|
+
|
|
268
|
+
def _write(self, p: Param, value: Any) -> None:
|
|
269
|
+
w = self._widgets[p.key]
|
|
270
|
+
w.blockSignals(True)
|
|
271
|
+
if isinstance(w, QCheckBox):
|
|
272
|
+
w.setChecked(bool(value))
|
|
273
|
+
elif isinstance(w, BioComboBox):
|
|
274
|
+
if p.kind == "options":
|
|
275
|
+
w.setCurrentText(str(value))
|
|
276
|
+
else:
|
|
277
|
+
w.setCurrentIndex(max(w.findData(value), 0))
|
|
278
|
+
elif isinstance(w, BioDoubleSpinBox):
|
|
279
|
+
w.setValue(float(value or 0))
|
|
280
|
+
w.blockSignals(False)
|
|
281
|
+
|
|
282
|
+
def _apply_hidden(self) -> None:
|
|
283
|
+
for key, widget in self._widgets.items():
|
|
284
|
+
widget.setVisible(key not in self._hidden)
|
|
285
|
+
for extra in (self._labels, self._helps):
|
|
286
|
+
if key in extra:
|
|
287
|
+
extra[key].setVisible(key not in self._hidden)
|
|
288
|
+
|
|
289
|
+
def _apply_units(self) -> None:
|
|
290
|
+
for p in self._params:
|
|
291
|
+
w = self._widgets[p.key]
|
|
292
|
+
if isinstance(w, BioDoubleSpinBox) and p.unit:
|
|
293
|
+
w.setSuffix(f" {self._unit_display(p.unit)[0]}")
|
|
294
|
+
|
|
295
|
+
def _apply_dependencies(self) -> None:
|
|
296
|
+
for p in self._params:
|
|
297
|
+
if not p.enabled_by or p.enabled_by not in self._widgets:
|
|
298
|
+
continue
|
|
299
|
+
switch = self._widgets[p.enabled_by]
|
|
300
|
+
on = isinstance(switch, QCheckBox) and switch.isChecked()
|
|
301
|
+
self._widgets[p.key].setEnabled(on)
|
|
302
|
+
if p.key in self._labels:
|
|
303
|
+
self._labels[p.key].setEnabled(on)
|
|
304
|
+
|
|
305
|
+
def _on_changed(self, *_args: object) -> None:
|
|
306
|
+
self._apply_dependencies()
|
|
307
|
+
self.changed.emit()
|
|
308
|
+
|
|
309
|
+
def _on_advanced_toggled(self, checked: bool) -> None:
|
|
310
|
+
self.btn_advanced.setText(_ADVANCED_OPEN if checked else _ADVANCED_CLOSED)
|
|
311
|
+
self._advanced.setVisible(checked)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _grid(parent: QWidget) -> QGridLayout:
|
|
315
|
+
grid = QGridLayout(parent)
|
|
316
|
+
grid.setContentsMargins(0, 0, 0, 0)
|
|
317
|
+
grid.setSpacing(8)
|
|
318
|
+
grid.setColumnStretch(1, 1)
|
|
319
|
+
return grid
|