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.
Files changed (137) hide show
  1. {karcytics_sdk-3.2.0/src/karcytics_sdk.egg-info → karcytics_sdk-3.3.0}/PKG-INFO +1 -1
  2. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/pyproject.toml +1 -1
  3. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/contrib/image_utils.py +3 -3
  4. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/__init__.py +38 -0
  5. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/analysis.py +11 -1
  6. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/components.py +21 -5
  7. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/image_view.py +1 -1
  8. karcytics_sdk-3.3.0/src/karcytics_sdk/plugin/params.py +58 -0
  9. karcytics_sdk-3.3.0/src/karcytics_sdk/plugin/params_form.py +319 -0
  10. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/mpl_canvas.py +8 -3
  11. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/runtime_services.py +22 -10
  12. karcytics_sdk-3.3.0/src/karcytics_sdk/plugin/stage_rail.py +139 -0
  13. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/tasks.py +1 -1
  14. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/ui_daemon_runtime.py +36 -4
  15. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/version.py +1 -1
  16. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/testing/ui_daemon.py +10 -1
  17. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0/src/karcytics_sdk.egg-info}/PKG-INFO +1 -1
  18. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/SOURCES.txt +3 -0
  19. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/LICENSE +0 -0
  20. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/README.md +0 -0
  21. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/setup.cfg +0 -0
  22. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/__init__.py +0 -0
  23. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/__init__.py +0 -0
  24. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/__init__.py +0 -0
  25. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/compliance.py +0 -0
  26. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/diagnostics.py +0 -0
  27. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/migrate.py +0 -0
  28. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/scaffold.py +0 -0
  29. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/commands/security.py +0 -0
  30. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/cli/main.py +0 -0
  31. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/contrib/__init__.py +0 -0
  32. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/contrib/imaging.py +0 -0
  33. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/__init__.py +0 -0
  34. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/baseline.py +0 -0
  35. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/catalog.py +0 -0
  36. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/design_rules.py +0 -0
  37. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/functional_rules.py +0 -0
  38. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/model.py +0 -0
  39. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/org_rules.py +0 -0
  40. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/registry.py +0 -0
  41. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/repo.py +0 -0
  42. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/report.py +0 -0
  43. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/runner.py +0 -0
  44. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/source.py +0 -0
  45. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/evaluator/test_rules.py +0 -0
  46. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/__init__.py +0 -0
  47. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/ai.py +0 -0
  48. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/core_services.py +0 -0
  49. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/docs.py +0 -0
  50. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/marketplace_cache.py +0 -0
  51. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/module_status_widget.py +0 -0
  52. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/qt_bridge.py +0 -0
  53. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/sign_plugin.py +0 -0
  54. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/trust_manager.py +0 -0
  55. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/trust_overrides.py +0 -0
  56. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/trust_path.py +0 -0
  57. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/host/trust_storage.py +0 -0
  58. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/__init__.py +0 -0
  59. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/i_crash_reporter.py +0 -0
  60. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/i_event_bus.py +0 -0
  61. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/i_logger.py +0 -0
  62. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/interfaces/i_task_scheduler.py +0 -0
  63. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/academy.py +0 -0
  64. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/academy_driver.py +0 -0
  65. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/academy_lock_card.py +0 -0
  66. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/academy_window.py +0 -0
  67. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/autosave.py +0 -0
  68. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/base.py +0 -0
  69. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/citation_widgets.py +0 -0
  70. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/citations.py +0 -0
  71. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/context.py +0 -0
  72. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/course_complete_overlay.py +0 -0
  73. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/cyto_character.py +0 -0
  74. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/cyto_costumes.py +0 -0
  75. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/daemon.py +0 -0
  76. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/dialogs.py +0 -0
  77. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/draggable.py +0 -0
  78. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/effects.py +0 -0
  79. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/events.py +0 -0
  80. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/galactic_loader.py +0 -0
  81. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/galactic_loader.qml +0 -0
  82. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/history.py +0 -0
  83. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/interfaces.py +0 -0
  84. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/io.py +0 -0
  85. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/logging.py +0 -0
  86. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/managed_task.py +0 -0
  87. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/manifest.py +0 -0
  88. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/manifest_parser.py +0 -0
  89. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/menu_builder.py +0 -0
  90. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/preferences.py +0 -0
  91. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/question_panel.py +0 -0
  92. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/__init__.py +0 -0
  93. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/axes.py +0 -0
  94. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/graphics_scene.py +0 -0
  95. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/lock.py +0 -0
  96. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/rendering/pipeline.py +0 -0
  97. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/ribbon.py +0 -0
  98. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/security_parser.py +0 -0
  99. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/signals.py +0 -0
  100. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/state.py +0 -0
  101. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/theme_fallback.py +0 -0
  102. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/theme_overlay.py +0 -0
  103. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/timing.py +0 -0
  104. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/toast.py +0 -0
  105. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/tutorial_highlight.py +0 -0
  106. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/tutorial_models.py +0 -0
  107. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/tutorial_overlay.py +0 -0
  108. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/ui_preferences.py +0 -0
  109. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/update_check.py +0 -0
  110. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/validation.py +0 -0
  111. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/wizard.py +0 -0
  112. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/worker_thread.py +0 -0
  113. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/workflow.py +0 -0
  114. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/plugin/zoom_controls.py +0 -0
  115. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/py.typed +0 -0
  116. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/sdk_cli.py +0 -0
  117. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/testing/__init__.py +0 -0
  118. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk/testing/contract.py +0 -0
  119. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/dependency_links.txt +0 -0
  120. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/entry_points.txt +0 -0
  121. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/requires.txt +0 -0
  122. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/src/karcytics_sdk.egg-info/top_level.txt +0 -0
  123. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_host_ai.py +0 -0
  124. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_host_security.py +0 -0
  125. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_marketplace_cache.py +0 -0
  126. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_namespaces.py +0 -0
  127. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_plugin_architecture.py +0 -0
  128. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_plugin_ribbon.py +0 -0
  129. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_plugin_state.py +0 -0
  130. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_plugin_ui.py +0 -0
  131. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_sdk_cli.py +0 -0
  132. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_sign_plugin.py +0 -0
  133. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_testing_contract.py +0 -0
  134. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_theme_fallback.py +0 -0
  135. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_ui_daemon_runtime.py +0 -0
  136. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_workflow_context.py +0 -0
  137. {karcytics_sdk-3.2.0 → karcytics_sdk-3.3.0}/tests/test_workflow_dialogs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karcytics-sdk
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: Software Development Kit and CLI for Karcytics.
5
5
  Author: Kalaimaran Balasothy
6
6
  License: # PolyForm Noncommercial License 1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "karcytics-sdk"
7
- version = "3.2.0"
7
+ version = "3.3.0"
8
8
  description = "Software Development Kit and CLI for Karcytics."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -1,7 +1,7 @@
1
1
  """Image loading, format conversion, and preprocessing utilities (deprecated).
2
2
 
3
- Deprecated: this is western-blot band-image logic, which moved into that
4
- plugin. It will be removed in karcytics-sdk 4.0.
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
- "western-blot band-image logic, which now lives in that plugin. Copy what you need into your plugin.",
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 = None,
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` leaves
662
- the current index wherever `clear()`/`addItem()` land it.
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
- selection_already_right = restore_data is None or combo.currentData() == restore_data
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 restore_data is not None:
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
- (western-blot's `ImageCanvas`, cytometrics' `MultiChannelCanvas`): showing
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