itasc 0.2.0.dev0__py3-none-any.whl

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 (162) hide show
  1. itasc/__init__.py +16 -0
  2. itasc/cellpose/__init__.py +31 -0
  3. itasc/cellpose/cellpose_runner.py +334 -0
  4. itasc/cellpose/divergence_maps.py +380 -0
  5. itasc/cellpose/flow_following.py +283 -0
  6. itasc/cellpose/joint.py +98 -0
  7. itasc/cellpose/napari.yaml +10 -0
  8. itasc/cellpose/native_masks.py +313 -0
  9. itasc/cellpose/retrack.py +232 -0
  10. itasc/cellpose/shape.py +51 -0
  11. itasc/cellpose/track_laptrack.py +279 -0
  12. itasc/contact_analysis/__init__.py +61 -0
  13. itasc/contact_analysis/_provenance.py +29 -0
  14. itasc/contact_analysis/catalog.py +393 -0
  15. itasc/contact_analysis/config.py +155 -0
  16. itasc/contact_analysis/contacts/__init__.py +24 -0
  17. itasc/contact_analysis/contacts/batch.py +158 -0
  18. itasc/contact_analysis/contacts/build.py +837 -0
  19. itasc/contact_analysis/contacts/contact_labels.py +136 -0
  20. itasc/contact_analysis/contacts/neighborhood.py +146 -0
  21. itasc/contact_analysis/contacts/reader.py +58 -0
  22. itasc/contact_analysis/contacts/signed_contact_length.py +143 -0
  23. itasc/contact_analysis/curation.py +185 -0
  24. itasc/contact_analysis/dynamics/__init__.py +43 -0
  25. itasc/contact_analysis/dynamics/collective.py +206 -0
  26. itasc/contact_analysis/dynamics/kinematics.py +278 -0
  27. itasc/contact_analysis/dynamics/msd.py +165 -0
  28. itasc/contact_analysis/dynamics/store.py +251 -0
  29. itasc/contact_analysis/dynamics/trajectories.py +100 -0
  30. itasc/contact_analysis/frame_interval.py +85 -0
  31. itasc/contact_analysis/napari.yaml +10 -0
  32. itasc/contact_analysis/pipeline.py +313 -0
  33. itasc/contact_analysis/pixel_size.py +166 -0
  34. itasc/contact_analysis/quantifier.py +204 -0
  35. itasc/contact_analysis/quantifiers/__init__.py +7 -0
  36. itasc/contact_analysis/quantifiers/_contacts_derived.py +42 -0
  37. itasc/contact_analysis/quantifiers/cell_density.py +65 -0
  38. itasc/contact_analysis/quantifiers/cell_dynamics.py +81 -0
  39. itasc/contact_analysis/quantifiers/cell_shape.py +32 -0
  40. itasc/contact_analysis/quantifiers/contacts.py +72 -0
  41. itasc/contact_analysis/quantifiers/neighbor_count.py +26 -0
  42. itasc/contact_analysis/quantifiers/nucleus_dynamics.py +77 -0
  43. itasc/contact_analysis/quantifiers/nucleus_shape.py +35 -0
  44. itasc/contact_analysis/quantifiers/shape_relational.py +34 -0
  45. itasc/contact_analysis/quantifiers/signed_contact_length.py +44 -0
  46. itasc/contact_analysis/records.py +129 -0
  47. itasc/contact_analysis/shape/__init__.py +19 -0
  48. itasc/contact_analysis/shape/core.py +141 -0
  49. itasc/contact_analysis/shape/relational.py +134 -0
  50. itasc/contact_analysis/shape_tables.py +387 -0
  51. itasc/core/__init__.py +0 -0
  52. itasc/core/cancellation.py +15 -0
  53. itasc/core/commit.py +61 -0
  54. itasc/core/imageops.py +34 -0
  55. itasc/core/label_store.py +53 -0
  56. itasc/core/lineage.py +113 -0
  57. itasc/core/logging.py +51 -0
  58. itasc/core/paths.py +34 -0
  59. itasc/core/tiff.py +13 -0
  60. itasc/correction/__init__.py +25 -0
  61. itasc/correction/labels.py +983 -0
  62. itasc/napari/__init__.py +7 -0
  63. itasc/napari/_experiments_panel.py +1091 -0
  64. itasc/napari/_flow_layout.py +98 -0
  65. itasc/napari/_icons.py +126 -0
  66. itasc/napari/_napari_compat.py +118 -0
  67. itasc/napari/_paths.py +231 -0
  68. itasc/napari/_preview_cache.py +60 -0
  69. itasc/napari/_spotlight.py +46 -0
  70. itasc/napari/_stage_loader.py +146 -0
  71. itasc/napari/_stage_status.py +110 -0
  72. itasc/napari/_standalone_paths.py +89 -0
  73. itasc/napari/_state.py +146 -0
  74. itasc/napari/_status_rail.py +128 -0
  75. itasc/napari/_track_render.py +114 -0
  76. itasc/napari/_widget_helpers.py +236 -0
  77. itasc/napari/aggregate_widget.py +339 -0
  78. itasc/napari/candidate_gallery_controller.py +285 -0
  79. itasc/napari/cell_workflow_widget.py +1405 -0
  80. itasc/napari/cellpose_segment_track_widget.py +1253 -0
  81. itasc/napari/cellpose_widget.py +1034 -0
  82. itasc/napari/contact_analysis/__init__.py +6 -0
  83. itasc/napari/contact_analysis/curation_controller.py +94 -0
  84. itasc/napari/contact_analysis/plugins/__init__.py +102 -0
  85. itasc/napari/contact_analysis/plugins/catalog_summary.py +56 -0
  86. itasc/napari/contact_analysis/plugins/curation.py +247 -0
  87. itasc/napari/contact_analysis/plugins/visualize_contacts.py +64 -0
  88. itasc/napari/contact_analysis_widget.py +951 -0
  89. itasc/napari/contact_visualization.py +895 -0
  90. itasc/napari/correction/__init__.py +8 -0
  91. itasc/napari/correction/_correction_anchor.py +54 -0
  92. itasc/napari/correction/_correction_candidate_panel.py +259 -0
  93. itasc/napari/correction/_correction_candidates.py +169 -0
  94. itasc/napari/correction/_correction_centroids.py +132 -0
  95. itasc/napari/correction/_correction_commit.py +71 -0
  96. itasc/napari/correction/_correction_events.py +39 -0
  97. itasc/napari/correction/_correction_keymap.py +80 -0
  98. itasc/napari/correction/_correction_layer_lifecycle.py +137 -0
  99. itasc/napari/correction/_correction_layer_loader.py +86 -0
  100. itasc/napari/correction/_correction_navigation.py +97 -0
  101. itasc/napari/correction/_correction_paint.py +46 -0
  102. itasc/napari/correction/_correction_playback.py +68 -0
  103. itasc/napari/correction/_correction_protection.py +44 -0
  104. itasc/napari/correction/_correction_takeover.py +64 -0
  105. itasc/napari/correction/_correction_track_accordion.py +566 -0
  106. itasc/napari/correction/_correction_track_path.py +388 -0
  107. itasc/napari/correction/_correction_ui.py +367 -0
  108. itasc/napari/correction/_correction_ui_nucleus.py +393 -0
  109. itasc/napari/correction/_correction_utils.py +204 -0
  110. itasc/napari/correction/_correction_validation.py +83 -0
  111. itasc/napari/correction/cell_correction_widget.py +1190 -0
  112. itasc/napari/correction/correction_widget.py +1363 -0
  113. itasc/napari/correction/nucleus_correction_widget.py +2185 -0
  114. itasc/napari/divergence_maps_widget.py +795 -0
  115. itasc/napari/lineage_canvas_controller.py +351 -0
  116. itasc/napari/main_widget.py +1184 -0
  117. itasc/napari/nucleus_atom_extraction_widget.py +648 -0
  118. itasc/napari/nucleus_db_browser_widget.py +1442 -0
  119. itasc/napari/nucleus_pipeline_widget.py +579 -0
  120. itasc/napari/nucleus_segmentation_inputs_widget.py +21 -0
  121. itasc/napari/nucleus_tracking_inputs_widget.py +248 -0
  122. itasc/napari/nucleus_workflow_widget.py +963 -0
  123. itasc/napari/track_path_controller.py +387 -0
  124. itasc/napari/ui_gate.py +266 -0
  125. itasc/napari/ui_style.py +503 -0
  126. itasc/napari/validated_overlay_controller.py +254 -0
  127. itasc/napari/widgets.py +616 -0
  128. itasc/napari.yaml +45 -0
  129. itasc/segmentation/__init__.py +60 -0
  130. itasc/segmentation/cell_divergence_segmentation.py +433 -0
  131. itasc/segmentation/cell_label_icm.py +637 -0
  132. itasc/segmentation/contour_filtering.py +128 -0
  133. itasc/segmentation/lineage.py +18 -0
  134. itasc/segmentation/nucleus_segmentation.py +35 -0
  135. itasc/tracking_ultrack/__init__.py +12 -0
  136. itasc/tracking_ultrack/_node_geometry.py +191 -0
  137. itasc/tracking_ultrack/atoms.py +470 -0
  138. itasc/tracking_ultrack/config.py +76 -0
  139. itasc/tracking_ultrack/corrections.py +1213 -0
  140. itasc/tracking_ultrack/db_build.py +492 -0
  141. itasc/tracking_ultrack/db_query.py +952 -0
  142. itasc/tracking_ultrack/export.py +133 -0
  143. itasc/tracking_ultrack/extend.py +243 -0
  144. itasc/tracking_ultrack/ingest.py +83 -0
  145. itasc/tracking_ultrack/linking.py +286 -0
  146. itasc/tracking_ultrack/multi_threshold.py +46 -0
  147. itasc/tracking_ultrack/napari.yaml +10 -0
  148. itasc/tracking_ultrack/reseed.py +456 -0
  149. itasc/tracking_ultrack/retracker.py +153 -0
  150. itasc/tracking_ultrack/scoring.py +74 -0
  151. itasc/tracking_ultrack/seed_prior.py +175 -0
  152. itasc/tracking_ultrack/solve.py +52 -0
  153. itasc/tracking_ultrack/swap_candidate.py +171 -0
  154. itasc/tracking_ultrack/track_quality.py +142 -0
  155. itasc/tracking_ultrack/validation_nodes.py +315 -0
  156. itasc/tracking_ultrack/validation_state.py +430 -0
  157. itasc-0.2.0.dev0.data/data/itasc/napari.yaml +45 -0
  158. itasc-0.2.0.dev0.dist-info/METADATA +186 -0
  159. itasc-0.2.0.dev0.dist-info/RECORD +162 -0
  160. itasc-0.2.0.dev0.dist-info/WHEEL +4 -0
  161. itasc-0.2.0.dev0.dist-info/entry_points.txt +2 -0
  162. itasc-0.2.0.dev0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,503 @@
1
+ from __future__ import annotations
2
+
3
+ from qtpy.QtCore import Qt
4
+ from qtpy.QtGui import QColor
5
+ from qtpy.QtWidgets import (
6
+ QGridLayout,
7
+ QLabel,
8
+ QPushButton,
9
+ QSizePolicy,
10
+ QToolButton,
11
+ QVBoxLayout,
12
+ QWidget,
13
+ )
14
+
15
+ TINY_MARGIN = 2
16
+ SECTION_MARGIN = 4
17
+ TIGHT_SPACING = 4
18
+ DEFAULT_FIELD_SPACING = 8
19
+ DEFAULT_ROW_SPACING = 4
20
+ BLOCK_GRID_COLUMNS = 4
21
+
22
+ # ── Theme palette ────────────────────────────────────────────────────────
23
+ # Accent palettes keyed by the same color names. THEME_PALETTES selects the
24
+ # active set; ACTIVE_THEME_NAME picks the default. Call sites reference accents
25
+ # via stage_accent() so theme changes propagate automatically.
26
+
27
+ # Nord — https://www.nordtheme.com/docs/colors-and-palettes
28
+ NORD = {
29
+ "rosewater": "#bf616a", "flamingo": "#bf616a", "pink": "#b48ead",
30
+ "mauve": "#b48ead", "red": "#bf616a", "maroon": "#bf616a",
31
+ "peach": "#d08770", "yellow": "#ebcb8b", "green": "#a3be8c",
32
+ "teal": "#8fbcbb", "sky": "#88c0d0", "sapphire": "#81a1c1",
33
+ "blue": "#5e81ac", "lavender": "#b48ead",
34
+ }
35
+
36
+ # Solarized Dark — https://ethanschoonover.com/solarized/
37
+ SOLARIZED_DARK = {
38
+ "rosewater": "#cb4b16", "flamingo": "#dc322f", "pink": "#d33682",
39
+ "mauve": "#6c71c4", "red": "#dc322f", "maroon": "#cb4b16",
40
+ "peach": "#cb4b16", "yellow": "#b58900", "green": "#859900",
41
+ "teal": "#2aa198", "sky": "#2aa198", "sapphire": "#268bd2",
42
+ "blue": "#268bd2", "lavender": "#6c71c4",
43
+ }
44
+
45
+ # ITASC Field Notes. Earlier second choice after Nord: a quiet custom
46
+ # palette with softer workbench-style accents for the visible stages.
47
+ FIELD_NOTES = {
48
+ "rosewater": "#c9b7a2", "flamingo": "#b98175", "pink": "#b4838f",
49
+ "mauve": "#6f9f95", "red": "#a05f55", "maroon": "#8a5d4f",
50
+ "peach": "#b88a63", "yellow": "#b7a56a", "green": "#8fa77a",
51
+ "teal": "#6f9f95", "sky": "#789ca8", "sapphire": "#7f8c8d",
52
+ "blue": "#6d8aa0", "lavender": "#9a879d",
53
+ }
54
+
55
+ # ITASC Museum Label. Muted, warmer, and more material than Nord while
56
+ # keeping enough separation between workflow stage accents.
57
+ MUSEUM_LABEL = {
58
+ "rosewater": "#d7c6b2", "flamingo": "#b98276", "pink": "#a87582",
59
+ "mauve": "#8f8a6f", "red": "#a65f4f", "maroon": "#7f5648",
60
+ "peach": "#b8794f", "yellow": "#b69b5e", "green": "#78906f",
61
+ "teal": "#6f9388", "sky": "#7d95a0", "sapphire": "#737a7c",
62
+ "blue": "#687f91", "lavender": "#8b7b8f",
63
+ }
64
+
65
+ # ITASC Dusk Gradient. Previous first choice: based on a compact
66
+ # teal-to-plum reference palette with five ordered workflow accents.
67
+ DUSK_GRADIENT = {
68
+ "rosewater": "#b48aa4", "flamingo": "#a3739c", "pink": "#8c5ca6",
69
+ "mauve": "#483a5e", "red": "#9b5d86", "maroon": "#6f4790",
70
+ "peach": "#98d2d4", "yellow": "#b9c5c8", "green": "#8c5ca6",
71
+ "teal": "#608a9a", "sky": "#7eabba", "sapphire": "#608a9a",
72
+ "blue": "#6f8eaa", "lavender": "#6f4790",
73
+ }
74
+
75
+ # ITASC Sunset. Current second choice: warm reference palette mapped
76
+ # left-to-right across the visible workflow accents.
77
+ SUNSET = {
78
+ "rosewater": "#fccc73", "flamingo": "#fd9d5d", "pink": "#d04e6c",
79
+ "mauve": "#8a5687", "red": "#fd6b5d", "maroon": "#d04e6c",
80
+ "peach": "#fd9d5d", "yellow": "#fccc73", "green": "#fd6b5d",
81
+ "teal": "#d98f71", "sky": "#c66d78", "sapphire": "#fccc73",
82
+ "blue": "#b66b83", "lavender": "#d04e6c",
83
+ }
84
+
85
+ # ITASC Signal Archive. Previous first choice: muted red, parchment, sage,
86
+ # teal, and charcoal reference palette mapped across the workflow accents.
87
+ SIGNAL_ARCHIVE = {
88
+ "rosewater": "#d7b2a7", "flamingo": "#c94b4b", "pink": "#a55b62",
89
+ "mauve": "#414643", "red": "#c94b4b", "maroon": "#8b4447",
90
+ "peach": "#eae3c3", "yellow": "#eae3c3", "green": "#9bb6a1",
91
+ "teal": "#3b7b7a", "sky": "#6f9a95", "sapphire": "#c94b4b",
92
+ "blue": "#3b7b7a", "lavender": "#3b7b7a",
93
+ }
94
+
95
+ # ITASC Viridis. Five-stop scientific colormap palette sampled from the
96
+ # inverted interior 15%-85% range to avoid the darkest and brightest endpoints.
97
+ VIRIDIS = {
98
+ "rosewater": "#9bd93c", "flamingo": "#38b977", "pink": "#31668e",
99
+ "mauve": "#463480", "red": "#31668e", "maroon": "#463480",
100
+ "peach": "#38b977", "yellow": "#9bd93c", "green": "#21918c",
101
+ "teal": "#21918c", "sky": "#31668e", "sapphire": "#9bd93c",
102
+ "blue": "#31668e", "lavender": "#31668e",
103
+ }
104
+
105
+ # ITASC Cividis. Current default: five-stop color-vision-friendly scientific
106
+ # colormap sampled from the inverted interior 15%-85% range.
107
+ CIVIDIS = {
108
+ "rosewater": "#d6c35d", "flamingo": "#a79d73", "pink": "#555c6d",
109
+ "mauve": "#243c6e", "red": "#555c6d", "maroon": "#243c6e",
110
+ "peach": "#a79d73", "yellow": "#d6c35d", "green": "#7d7c78",
111
+ "teal": "#7d7c78", "sky": "#555c6d", "sapphire": "#d6c35d",
112
+ "blue": "#555c6d", "lavender": "#555c6d",
113
+ }
114
+
115
+ THEME_PALETTES = {
116
+ "Cividis": CIVIDIS,
117
+ "Viridis": VIRIDIS,
118
+ "Signal Archive": SIGNAL_ARCHIVE,
119
+ "Dusk Gradient": DUSK_GRADIENT,
120
+ "Sunset": SUNSET,
121
+ "Nord": NORD,
122
+ "Field Notes": FIELD_NOTES,
123
+ "Museum Label": MUSEUM_LABEL,
124
+ }
125
+ ACTIVE_THEME_NAME = "Cividis"
126
+ ACTIVE_PALETTE = THEME_PALETTES[ACTIVE_THEME_NAME]
127
+
128
+
129
+ def theme_names() -> tuple[str, ...]:
130
+ return tuple(THEME_PALETTES)
131
+
132
+
133
+ def active_theme_name() -> str:
134
+ return ACTIVE_THEME_NAME
135
+
136
+
137
+ def set_active_theme(name: str) -> None:
138
+ global ACTIVE_PALETTE, ACTIVE_THEME_NAME
139
+
140
+ ACTIVE_THEME_NAME = name
141
+ ACTIVE_PALETTE = THEME_PALETTES[name]
142
+
143
+ STAGE_ACCENTS = {
144
+ "project_status": "sapphire",
145
+ "cellpose": "sapphire",
146
+ "nucleus": "peach",
147
+ "cell": "green",
148
+ "contact_analysis": "lavender",
149
+ "aggregate": "teal",
150
+ }
151
+
152
+
153
+ def stage_accent(stage_key: str) -> str:
154
+ """Resolve a stage key to its accent hex via the active palette."""
155
+ return ACTIVE_PALETTE[STAGE_ACCENTS[stage_key]]
156
+
157
+
158
+ def muted_accent(hex_str: str) -> str:
159
+ """Return a quieter variant of an accent color for nested or compact labels."""
160
+ c = QColor(hex_str)
161
+ h, s, l, a = c.getHslF()
162
+ new_s = max(0.0, s * 0.35)
163
+ new_l = 0.55 + (l - 0.55) * 0.3
164
+ new_l = max(0.0, min(1.0, new_l))
165
+ c.setHslF(h, new_s, new_l, a)
166
+ return c.name()
167
+
168
+
169
+ def muted_stage_accent(stage_key: str) -> str:
170
+ return muted_accent(stage_accent(stage_key))
171
+
172
+
173
+ def stage_header_pill_background(stage_key: str, alpha: int = 38) -> str:
174
+ color = QColor(muted_stage_accent(stage_key))
175
+ red, green, blue, _ = color.getRgb()
176
+ return f"rgba({red}, {green}, {blue}, {alpha})"
177
+
178
+
179
+ def stage_header_disabled_action_color(stage_key: str) -> str:
180
+ color = QColor(muted_stage_accent(stage_key))
181
+ h, s, l, a = color.getHslF()
182
+ color.setHslF(h, s * 0.55, max(0.0, l * 0.62), a)
183
+ return color.name()
184
+
185
+
186
+ # ── Designed-surface tokens (napariTFM ExperimentsList parity) ────────────
187
+ # Theme-agnostic so the experiments panel sits on any host background: selected
188
+ # rows are a translucent white "lift", text uses a grey ramp, inputs are pills.
189
+ COMPACT_SPACING = 4
190
+ TEXT_BRIGHT = "#e6edf3"
191
+ TEXT_MID = "#aeb6c0"
192
+ TEXT_DIM = "#6b7484"
193
+ ROW_LIFT_BG = "rgba(255, 255, 255, 13)" # a selected/raised row surface
194
+ HAIRLINE = "rgba(255, 255, 255, 18)"
195
+
196
+ # Experiment-row overall-status word -> color (amber running, green done, dim queued).
197
+ EXPERIMENT_STATUS_COLORS = {
198
+ "run": "#e3b341",
199
+ "done": "#3fb950",
200
+ "queued": TEXT_DIM,
201
+ }
202
+
203
+
204
+ def experiment_status_color(label: str) -> str:
205
+ """Color for an experiment row's overall-status word (run/done/queued)."""
206
+ return EXPERIMENT_STATUS_COLORS.get(label, TEXT_DIM)
207
+
208
+
209
+ def experiment_name_color(selected: bool) -> str:
210
+ """Brighten the active row's name; dim the rest."""
211
+ return TEXT_BRIGHT if selected else TEXT_MID
212
+
213
+
214
+ def experiment_row_style(selected: bool, accent: str) -> str:
215
+ """Row container style: a raised, accent-bordered surface when selected."""
216
+ if not selected:
217
+ return (
218
+ "QWidget#experiment_row { background: transparent; "
219
+ "border: 1px solid transparent; border-radius: 8px; }"
220
+ )
221
+ r, g, b, _ = QColor(accent).getRgb()
222
+ return (
223
+ "QWidget#experiment_row { "
224
+ f"background: {ROW_LIFT_BG}; "
225
+ f"border: 1px solid rgba({r}, {g}, {b}, 130); "
226
+ "border-radius: 8px; }"
227
+ )
228
+
229
+
230
+ def action_button_style() -> str:
231
+ """Give a text QToolButton real button chrome (fill, border, rounded,
232
+ hover/pressed feedback) so list actions don't read as flat status labels."""
233
+ return (
234
+ "QToolButton { "
235
+ "background: rgba(255, 255, 255, 10); "
236
+ f"border: 1px solid {HAIRLINE}; border-radius: 6px; "
237
+ f"padding: 4px 12px; color: {TEXT_BRIGHT}; }} "
238
+ "QToolButton:hover { background: rgba(255, 255, 255, 22); "
239
+ "border-color: rgba(255, 255, 255, 46); } "
240
+ "QToolButton:pressed { background: rgba(255, 255, 255, 32); } "
241
+ "QToolButton:disabled { "
242
+ f"color: {TEXT_DIM}; background: rgba(255, 255, 255, 4); "
243
+ "border-color: rgba(255, 255, 255, 10); }"
244
+ )
245
+
246
+
247
+ def mono_input_style() -> str:
248
+ """Themed pill style for a QLineEdit, so config fields aren't raw Qt."""
249
+ return (
250
+ "QLineEdit { "
251
+ "background: rgba(255, 255, 255, 8); "
252
+ f"border: 1px solid {HAIRLINE}; border-radius: 6px; "
253
+ f"padding: 3px 7px; color: {TEXT_BRIGHT}; }} "
254
+ "QLineEdit:focus { border-color: rgba(255, 255, 255, 38); }"
255
+ )
256
+
257
+
258
+ def _fixed_widget(widget, width=None):
259
+ if width is not None:
260
+ widget.setMaximumWidth(width)
261
+ widget.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
262
+ return widget
263
+
264
+
265
+ _DISABLED_PUSH_BUTTON_STYLE = (
266
+ "QPushButton:disabled { "
267
+ "color: palette(mid); "
268
+ "background-color: rgba(127, 124, 120, 42); "
269
+ "border: 1px solid rgba(127, 124, 120, 72); "
270
+ "border-radius: 4px; "
271
+ "}"
272
+ )
273
+
274
+
275
+ def _append_button_style(button, style: str):
276
+ current = button.styleSheet().strip()
277
+ if style in current:
278
+ return button
279
+ button.setStyleSheet(f"{current} {style}".strip())
280
+ return button
281
+
282
+
283
+ def _disabled_push_button(button):
284
+ if isinstance(button, QPushButton):
285
+ _append_button_style(button, _DISABLED_PUSH_BUTTON_STYLE)
286
+ return button
287
+
288
+
289
+ def action_button(button, expand=False):
290
+ horizontal_policy = (
291
+ QSizePolicy.Policy.Expanding if expand else QSizePolicy.Policy.Fixed
292
+ )
293
+ button.setSizePolicy(horizontal_policy, QSizePolicy.Policy.Fixed)
294
+ _disabled_push_button(button)
295
+ return button
296
+
297
+
298
+ def icon_button(button, width=24, height=None):
299
+ button.setFixedWidth(width)
300
+ if height is not None:
301
+ button.setFixedHeight(height)
302
+ _disabled_push_button(button)
303
+ return button
304
+
305
+
306
+ def muted_label(label, size_pt=8):
307
+ label.setStyleSheet(f"color: palette(mid); font-size: {size_pt}pt;")
308
+ return label
309
+
310
+
311
+ def status_label(label, size_pt=8, italic=False, muted=False):
312
+ style = f"font-size: {size_pt}pt;"
313
+ if muted:
314
+ style += " color: palette(mid);"
315
+ if italic:
316
+ style += " font-style: italic;"
317
+ label.setStyleSheet(style)
318
+ return label
319
+
320
+
321
+ def parameter_heading(label):
322
+ label.setStyleSheet("font-weight: 600;")
323
+ return label
324
+
325
+
326
+ def stage_header_label(label, stage_key: str, size_pt: int = 9):
327
+ label.setProperty("itasc_stage_key", stage_key)
328
+ label.setProperty("itasc_stage_header_size_pt", size_pt)
329
+ apply_stage_header_label_style(label)
330
+ return label
331
+
332
+
333
+ def stage_header_action_button(button: QToolButton, stage_key: str, size_px: int = 22):
334
+ button.setProperty("itasc_stage_key", stage_key)
335
+ button.setProperty("itasc_stage_header_action", True)
336
+ button.setFixedSize(size_px, size_px)
337
+ button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
338
+ color = muted_stage_accent(stage_key)
339
+ disabled_color = stage_header_disabled_action_color(stage_key)
340
+ background = stage_header_pill_background(stage_key)
341
+ button.setStyleSheet(
342
+ "QToolButton { "
343
+ "font-weight: bold; "
344
+ "font-size: 9pt; "
345
+ f"color: {color}; "
346
+ f"background-color: {background}; "
347
+ "border: none; "
348
+ "border-radius: 4px; "
349
+ "padding: 0; "
350
+ "margin: 0; "
351
+ "text-align: center; "
352
+ "} "
353
+ "QToolButton:hover { "
354
+ f"background-color: {stage_header_pill_background(stage_key, alpha=58)}; "
355
+ "} "
356
+ "QToolButton:checked { "
357
+ f"background-color: {stage_header_pill_background(stage_key, alpha=82)}; "
358
+ "} "
359
+ "QToolButton:disabled { "
360
+ f"color: {disabled_color}; "
361
+ f"background-color: {stage_header_pill_background(stage_key, alpha=28)}; "
362
+ "} "
363
+ "QToolButton:disabled:checked { "
364
+ f"color: {disabled_color}; "
365
+ f"background-color: {stage_header_pill_background(stage_key, alpha=44)}; "
366
+ "}"
367
+ )
368
+ return button
369
+
370
+
371
+ def apply_stage_header_label_style(label):
372
+ stage_key = label.property("itasc_stage_key")
373
+ if not stage_key:
374
+ return label
375
+ size_pt = label.property("itasc_stage_header_size_pt") or 9
376
+ label.setStyleSheet(
377
+ "font-weight: bold; "
378
+ f"font-size: {size_pt}pt; "
379
+ f"color: {muted_stage_accent(stage_key)}; "
380
+ f"background-color: {stage_header_pill_background(stage_key)}; "
381
+ "border-radius: 4px; "
382
+ "padding: 1px 6px;"
383
+ )
384
+ return label
385
+
386
+
387
+ def refresh_stage_header_labels(root) -> None:
388
+ for label in root.findChildren(QLabel):
389
+ if label.property("itasc_stage_key"):
390
+ apply_stage_header_label_style(label)
391
+
392
+
393
+ def danger_button(button):
394
+ _disabled_push_button(button)
395
+ return button
396
+
397
+
398
+ def checked_success_button(button):
399
+ _disabled_push_button(button)
400
+ return button
401
+
402
+
403
+ def block_grid(horizontal_spacing=8, vertical_spacing=4):
404
+ layout = QGridLayout()
405
+ layout.setHorizontalSpacing(horizontal_spacing)
406
+ layout.setVerticalSpacing(vertical_spacing)
407
+ for col in range(BLOCK_GRID_COLUMNS):
408
+ layout.setColumnStretch(col, 0)
409
+ return layout
410
+
411
+
412
+ def section_grid():
413
+ """A 4-column grid (label, field, label, field) where field columns
414
+ stretch — so sliders, combos, and labels fill the available width and
415
+ label columns stay aligned across all sections that share the grid."""
416
+ layout = QGridLayout()
417
+ layout.setHorizontalSpacing(DEFAULT_FIELD_SPACING)
418
+ layout.setVerticalSpacing(DEFAULT_ROW_SPACING)
419
+ layout.setColumnStretch(0, 0)
420
+ layout.setColumnStretch(1, 1)
421
+ layout.setColumnStretch(2, 0)
422
+ layout.setColumnStretch(3, 1)
423
+ return layout
424
+
425
+
426
+ def add_section_header(grid, row, widget):
427
+ """Add a heading widget spanning all 4 columns of a section_grid."""
428
+ grid.addWidget(widget, row, 0, 1, 4)
429
+ return widget
430
+
431
+
432
+ def add_section_full_row(grid, row, widget):
433
+ """Add a widget (separator, checkbox, …) spanning all 4 columns."""
434
+ grid.addWidget(widget, row, 0, 1, 4)
435
+ return widget
436
+
437
+
438
+ def add_section_pair_row(
439
+ grid, row,
440
+ left_label, left_widget,
441
+ right_label=None, right_widget=None,
442
+ ):
443
+ """Add a row with up to two [label][widget] pairs. Widgets keep their
444
+ natural size policy (no fixed-width wrap) so sliders/combos can stretch."""
445
+ left_label_widget = _block_label(left_label)
446
+ _add_section_pair_cell(grid, row, 0, left_label_widget, left_widget)
447
+
448
+ right_label_widget = None
449
+ if right_widget is not None:
450
+ right_label_widget = _block_label(right_label or "")
451
+ _add_section_pair_cell(grid, row, 2, right_label_widget, right_widget)
452
+ return left_label_widget, left_widget, right_label_widget, right_widget
453
+
454
+
455
+ def _add_section_pair_cell(grid, row, column, label_widget, widget):
456
+ container = QWidget()
457
+ layout = QVBoxLayout(container)
458
+ layout.setContentsMargins(0, 0, 0, 0)
459
+ layout.setSpacing(1)
460
+ label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBottom)
461
+ layout.addWidget(label_widget)
462
+ layout.addWidget(widget)
463
+ grid.addWidget(container, row, column, 1, 2)
464
+ return container
465
+
466
+
467
+ def _block_label(text):
468
+ label = QLabel(text)
469
+ label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
470
+ return label
471
+
472
+
473
+ def add_block_pair_row(
474
+ grid,
475
+ row,
476
+ left_label,
477
+ left_widget,
478
+ right_label=None,
479
+ right_widget=None,
480
+ field_width=70,
481
+ ):
482
+ left_label_widget = _block_label(left_label)
483
+ _add_block_pair_cell(grid, row, 0, left_label_widget, left_widget, field_width)
484
+
485
+ right_label_widget = None
486
+ if right_widget is not None:
487
+ right_label_widget = _block_label(right_label or "")
488
+ _add_block_pair_cell(grid, row, 2, right_label_widget, right_widget, field_width)
489
+
490
+ return left_label_widget, left_widget, right_label_widget, right_widget
491
+
492
+
493
+ def _add_block_pair_cell(grid, row, column, label_widget, widget, field_width):
494
+ container = QWidget()
495
+ layout = QVBoxLayout(container)
496
+ layout.setContentsMargins(0, 0, 0, 0)
497
+ layout.setSpacing(1)
498
+ label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBottom)
499
+ layout.addWidget(label_widget)
500
+ field = widget if widget.property("itasc_stack_section_label") else _fixed_widget(widget, field_width)
501
+ layout.addWidget(field)
502
+ grid.addWidget(container, row, column, 1, 2)
503
+ return container