AMS-BP 0.3.0__py3-none-any.whl → 0.4.0__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 (61) hide show
  1. AMS_BP/__init__.py +1 -1
  2. AMS_BP/configio/configmodels.py +32 -18
  3. AMS_BP/configio/convertconfig.py +508 -632
  4. AMS_BP/gui/README.md +77 -0
  5. AMS_BP/gui/__init__.py +0 -0
  6. AMS_BP/gui/assets/__init__.py +0 -0
  7. AMS_BP/gui/assets/drawing.svg +107 -0
  8. AMS_BP/gui/configuration_window.py +333 -0
  9. AMS_BP/gui/help_docs/__init__.py +0 -0
  10. AMS_BP/gui/help_docs/cell_help.md +45 -0
  11. AMS_BP/gui/help_docs/channels_help.md +78 -0
  12. AMS_BP/gui/help_docs/condensate_help.md +59 -0
  13. AMS_BP/gui/help_docs/detector_help.md +57 -0
  14. AMS_BP/gui/help_docs/experiment_help.md +92 -0
  15. AMS_BP/gui/help_docs/fluorophore_help.md +128 -0
  16. AMS_BP/gui/help_docs/general_help.md +43 -0
  17. AMS_BP/gui/help_docs/global_help.md +47 -0
  18. AMS_BP/gui/help_docs/laser_help.md +76 -0
  19. AMS_BP/gui/help_docs/molecule_help.md +78 -0
  20. AMS_BP/gui/help_docs/output_help.md +5 -0
  21. AMS_BP/gui/help_docs/psf_help.md +51 -0
  22. AMS_BP/gui/help_window.py +26 -0
  23. AMS_BP/gui/logging_window.py +93 -0
  24. AMS_BP/gui/main.py +255 -0
  25. AMS_BP/gui/sim_worker.py +58 -0
  26. AMS_BP/gui/template_window_selection.py +100 -0
  27. AMS_BP/gui/widgets/__init__.py +0 -0
  28. AMS_BP/gui/widgets/camera_config_widget.py +213 -0
  29. AMS_BP/gui/widgets/cell_config_widget.py +225 -0
  30. AMS_BP/gui/widgets/channel_config_widget.py +307 -0
  31. AMS_BP/gui/widgets/condensate_config_widget.py +341 -0
  32. AMS_BP/gui/widgets/experiment_config_widget.py +259 -0
  33. AMS_BP/gui/widgets/flurophore_config_widget.py +513 -0
  34. AMS_BP/gui/widgets/general_config_widget.py +47 -0
  35. AMS_BP/gui/widgets/global_config_widget.py +142 -0
  36. AMS_BP/gui/widgets/laser_config_widget.py +255 -0
  37. AMS_BP/gui/widgets/molecule_config_widget.py +714 -0
  38. AMS_BP/gui/widgets/output_config_widget.py +61 -0
  39. AMS_BP/gui/widgets/psf_config_widget.py +128 -0
  40. AMS_BP/gui/widgets/utility_widgets/__init__.py +0 -0
  41. AMS_BP/gui/widgets/utility_widgets/scinotation_widget.py +21 -0
  42. AMS_BP/gui/widgets/utility_widgets/spectrum_widget.py +115 -0
  43. AMS_BP/logging/__init__.py +0 -0
  44. AMS_BP/logging/logutil.py +83 -0
  45. AMS_BP/logging/setup_run_directory.py +35 -0
  46. AMS_BP/{run_cell_simulation.py → main_cli.py} +27 -72
  47. AMS_BP/optics/filters/filters.py +2 -0
  48. AMS_BP/resources/template_configs/metadata_configs.json +20 -0
  49. AMS_BP/resources/template_configs/sim_config.toml +408 -0
  50. AMS_BP/resources/template_configs/twocolor_widefield_timeseries_live.toml +399 -0
  51. AMS_BP/resources/template_configs/twocolor_widefield_zstack_fixed.toml +406 -0
  52. AMS_BP/resources/template_configs/twocolor_widefield_zstack_live.toml +408 -0
  53. AMS_BP/run_sim_util.py +76 -0
  54. AMS_BP/sim_microscopy.py +2 -2
  55. {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/METADATA +59 -34
  56. ams_bp-0.4.0.dist-info/RECORD +103 -0
  57. ams_bp-0.4.0.dist-info/entry_points.txt +2 -0
  58. ams_bp-0.3.0.dist-info/RECORD +0 -55
  59. ams_bp-0.3.0.dist-info/entry_points.txt +0 -2
  60. {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/WHEEL +0 -0
  61. {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,51 @@
1
+ # Point Spread Function (PSF) Configuration Help
2
+
3
+ The PSF (Point Spread Function) models how light emitted from a point source is blurred by the microscope optics. This section defines the parameters of the optical model used for image formation.
4
+
5
+ ---
6
+
7
+ ## `type`
8
+ - **Type**: String
9
+ - **Allowed**: `"gaussian"` *(currently the only supported type)*
10
+ - **Description**: Specifies the PSF model. A Gaussian PSF approximates diffraction-limited optics and is sufficient for many simulation scenarios.
11
+
12
+ ---
13
+
14
+ ## `custom_path`
15
+ - **Type**: String
16
+ - **Default**: Empty
17
+ - **Description**: Placeholder for loading a custom PSF file. Currently not supported.
18
+
19
+ ---
20
+
21
+ ## `confocal`
22
+ - **Type**: Boolean
23
+ - **Description**: Enables confocal mode by introducing a pinhole before the detector to reject out-of-focus light. When enabled, additional parameters become relevant (e.g., pinhole diameter).
24
+
25
+ ---
26
+
27
+ ## `parameters`
28
+
29
+ ### `numerical_aperture`
30
+ - **Type**: Float
31
+ - **Typical Range**: 0.1 – 1.5
32
+ - **Description**: The numerical aperture of the objective lens, which affects the resolution and depth of field of the microscope.
33
+
34
+ ### `refractive_index`
35
+ - **Type**: Float
36
+ - **Typical Range**: 1.0 – 2.0
37
+ - **Description**: The refractive index of the imaging medium (e.g., air: 1.0, water: 1.33, immersion oil: ~1.5).
38
+
39
+ ### `pinhole_diameter`
40
+ - **Type**: Float (optional)
41
+ - **Units**: Micrometers (μm)
42
+ - **Visible only when confocal mode is enabled**
43
+ - **Description**: The diameter of the confocal pinhole that restricts the depth of detection, improving axial resolution by eliminating out-of-focus light.
44
+
45
+ ---
46
+
47
+ ## Notes
48
+
49
+ - Confocal simulation is optional and intended for advanced users modeling high-resolution 3D optical sectioning.
50
+ - You can adjust the `numerical_aperture` and `refractive_index` even in widefield mode to simulate different objective lens setups.
51
+
@@ -0,0 +1,26 @@
1
+ from pathlib import Path
2
+
3
+ from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QTextBrowser, QVBoxLayout
4
+
5
+
6
+ class HelpWindow(QDialog):
7
+ def __init__(self, help_path: Path, parent=None):
8
+ super().__init__(parent)
9
+ self.setWindowTitle("Help")
10
+ self.resize(500, 400)
11
+
12
+ layout = QVBoxLayout()
13
+ browser = QTextBrowser()
14
+
15
+ try:
16
+ with open(help_path, "r", encoding="utf-8") as f:
17
+ browser.setMarkdown(f.read())
18
+ except Exception as e:
19
+ browser.setPlainText(f"Failed to load help: {e}")
20
+
21
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
22
+ buttons.rejected.connect(self.reject)
23
+
24
+ layout.addWidget(browser)
25
+ layout.addWidget(buttons)
26
+ self.setLayout(layout)
@@ -0,0 +1,93 @@
1
+ from PyQt6.QtCore import Qt, QTimer
2
+ from PyQt6.QtWidgets import (
3
+ QDialog,
4
+ QHBoxLayout,
5
+ QPlainTextEdit,
6
+ QProgressBar,
7
+ QPushButton,
8
+ QVBoxLayout,
9
+ )
10
+
11
+
12
+ class LogWindow(QDialog):
13
+ cancel_requested = False # Flag to check cancel request
14
+
15
+ def __init__(self, parent=None):
16
+ super().__init__(parent)
17
+ self.setWindowTitle("Simulation Log")
18
+ self.setMinimumSize(600, 300)
19
+
20
+ self.log_output = QPlainTextEdit()
21
+ self.log_output.setReadOnly(True)
22
+
23
+ # Progress bar (indeterminate)
24
+ self.progress = QProgressBar()
25
+ self.progress.setRange(0, 0) # Infinite/indeterminate animation
26
+
27
+ # Cancel button
28
+ self.cancel_button = QPushButton("Cancel")
29
+ self.cancel_button.clicked.connect(self.on_cancel_clicked)
30
+
31
+ button_layout = QHBoxLayout()
32
+ button_layout.addStretch()
33
+ button_layout.addWidget(self.cancel_button)
34
+
35
+ layout = QVBoxLayout()
36
+ layout.addWidget(self.log_output)
37
+ layout.addWidget(self.progress)
38
+ layout.addLayout(button_layout)
39
+
40
+ self.setLayout(layout)
41
+
42
+ def append_text(self, text: str):
43
+ self.log_output.appendPlainText(text)
44
+ QTimer.singleShot(0, self.scroll_to_bottom)
45
+
46
+ def scroll_to_bottom(self):
47
+ self.log_output.verticalScrollBar().setValue(
48
+ self.log_output.verticalScrollBar().maximum()
49
+ )
50
+
51
+ def on_cancel_clicked(self):
52
+ self.cancel_requested = True
53
+ self.append_text("Cancellation requested by user...")
54
+
55
+ def mark_success(self):
56
+ self.progress.setRange(0, 1)
57
+ self.progress.setValue(1)
58
+ self.progress.setStyleSheet("""
59
+ QProgressBar {
60
+ border: 1px solid #555;
61
+ border-radius: 5px;
62
+ background-color: #2d2d2d;
63
+ text-align: center;
64
+ color: white;
65
+ font-weight: bold;
66
+ font-size: 14px;
67
+ }
68
+ QProgressBar::chunk {
69
+ background-color: #4CAF50;
70
+ }
71
+ """)
72
+ self.append_text("Simulation completed successfully.")
73
+
74
+ def mark_failure(self):
75
+ self.progress.setRange(0, 1)
76
+ self.progress.setValue(0) # No fill
77
+ self.progress.setFormat("Failed, share logs with developer.") # Custom text
78
+ self.progress.setAlignment(Qt.AlignmentFlag.AlignCenter)
79
+ self.progress.setStyleSheet("""
80
+ QProgressBar {
81
+ border: 1px solid #800000;
82
+ border-radius: 5px;
83
+ background-color: #2d2d2d;
84
+ color: #e53935;
85
+ font-weight: bold;
86
+ font-size: 13px;
87
+ text-align: center;
88
+ }
89
+ QProgressBar::chunk {
90
+ background-color: transparent; /* no fill */
91
+ }
92
+ """)
93
+ self.append_text("Simulation failed. Please send logs to the developer.")
AMS_BP/gui/main.py ADDED
@@ -0,0 +1,255 @@
1
+ import webbrowser
2
+ from pathlib import Path
3
+ from zipfile import ZipFile
4
+
5
+ import napari
6
+ import tifffile
7
+ from PyQt6.QtCore import Qt, QThread
8
+ from PyQt6.QtGui import QPainter, QPixmap
9
+ from PyQt6.QtSvg import QSvgRenderer
10
+ from PyQt6.QtWidgets import (
11
+ QFileDialog,
12
+ QLabel,
13
+ QMainWindow,
14
+ QMessageBox,
15
+ QPushButton,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from ..logging.logutil import LoggerManager
21
+ from ..logging.setup_run_directory import setup_run_directory
22
+ from .logging_window import LogWindow
23
+ from .sim_worker import SimulationWorker
24
+ from .template_window_selection import TemplateSelectionWindow
25
+
26
+ LOGO_PATH = str(Path(__file__).parent / "assets" / "drawing.svg")
27
+
28
+
29
+ class MainWindow(QMainWindow):
30
+ def __init__(self):
31
+ super().__init__()
32
+ self.setWindowTitle("Welcome to AMS!")
33
+
34
+ # Set up the main layout
35
+ layout = QVBoxLayout()
36
+
37
+ # Add logo as a placeholder (SVG format)
38
+ self.logo_label = QLabel() # Label to hold the logo
39
+ self.set_svg_logo(LOGO_PATH) # Set the SVG logo
40
+ layout.addWidget(self.logo_label, alignment=Qt.AlignmentFlag.AlignCenter)
41
+
42
+ # Add the maintainer's name under the image
43
+ self.maintainer_label = QLabel(
44
+ "Maintainer: Baljyot Parmar \n baljyotparmar@hotmail.com"
45
+ )
46
+ self.maintainer_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
47
+ layout.addWidget(self.maintainer_label)
48
+ self.lab_label = QLabel(
49
+ "Brought to you by: " + '<a href="https://weberlab.ca">The WeberLab</a>'
50
+ )
51
+ self.lab_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
52
+ self.lab_label.setOpenExternalLinks(True) # Enable external links
53
+ self.lab_label.linkActivated.connect(self.on_link_activated)
54
+ layout.addWidget(self.lab_label)
55
+
56
+ # Button to open the Configuration Creation window
57
+ self.config_button = QPushButton("Create Configuration File")
58
+ self.config_button.clicked.connect(self.open_config_editor)
59
+ layout.addWidget(self.config_button)
60
+
61
+ # Create a central widget and set the layout
62
+ central_widget = QWidget()
63
+ central_widget.setLayout(layout)
64
+ self.setCentralWidget(central_widget)
65
+
66
+ # Button to run simulation with a TOML config file
67
+ self.run_sim_button = QPushButton("Run Simulation from Config")
68
+ self.run_sim_button.clicked.connect(self.run_simulation_from_config)
69
+ layout.addWidget(self.run_sim_button)
70
+
71
+ # Button to open Napari viewer
72
+ self.view_button = QPushButton("Visualize Microscopy Data (Napari)")
73
+ self.view_button.clicked.connect(self.open_napari_viewer)
74
+ layout.addWidget(self.view_button)
75
+
76
+ self.package_logs_button = QPushButton("Package Logs for Sharing")
77
+ self.package_logs_button.clicked.connect(self.package_logs)
78
+ layout.addWidget(self.package_logs_button)
79
+
80
+ def package_logs(self):
81
+ log_dir = Path.home() / "AMS_runs"
82
+
83
+ # Step 1: Open dialog to select folders
84
+ folder_paths = QFileDialog.getExistingDirectory(
85
+ self,
86
+ "Select Folder Containing Run Logs",
87
+ str(log_dir),
88
+ QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks,
89
+ )
90
+
91
+ if not folder_paths:
92
+ return
93
+
94
+ # QFileDialog.getExistingDirectory() returns a single path.
95
+ # For now, let's treat this as a single run_* folder being selected.
96
+
97
+ run_dir = Path(folder_paths)
98
+ if not run_dir.is_dir() or not run_dir.name.startswith("run_"):
99
+ QMessageBox.warning(
100
+ self, "Invalid Selection", "Please select a valid run_* folder."
101
+ )
102
+ return
103
+
104
+ # Step 2: Ask for destination .zip archive
105
+ zip_path_str, _ = QFileDialog.getSaveFileName(
106
+ self,
107
+ "Save Zipped Folder As",
108
+ str(log_dir / f"{run_dir.name}.zip"),
109
+ "Zip Archive (*.zip)",
110
+ )
111
+
112
+ if not zip_path_str:
113
+ return
114
+
115
+ zip_path = Path(zip_path_str)
116
+ if not zip_path.suffix == ".zip":
117
+ zip_path = zip_path.with_suffix(".zip")
118
+
119
+ try:
120
+ with ZipFile(zip_path, "w") as archive:
121
+ for path in run_dir.rglob("*"):
122
+ archive.write(path, arcname=path.relative_to(run_dir.parent))
123
+
124
+ QMessageBox.information(
125
+ self,
126
+ "Logs Packaged",
127
+ f"Folder '{run_dir.name}' successfully packaged to:\n{zip_path}",
128
+ )
129
+
130
+ except Exception as e:
131
+ QMessageBox.critical(self, "Error", f"Failed to package folder:\n{e}")
132
+
133
+ def run_simulation_from_config(self):
134
+ config_path_str, _ = QFileDialog.getOpenFileName(
135
+ self,
136
+ "Select Configuration File",
137
+ "",
138
+ "TOML Files (*.toml);;All Files (*)",
139
+ )
140
+
141
+ if config_path_str:
142
+ config_path = Path(config_path_str)
143
+
144
+ # Structured run dir setup
145
+ run_info = setup_run_directory(config_path)
146
+ log_path = run_info["log_file"]
147
+
148
+ # Create logger
149
+ self.logger_manager = LoggerManager(log_path)
150
+ self.logger_manager.start()
151
+
152
+ # Log window
153
+ self.log_window = LogWindow(self)
154
+ self.log_window.show()
155
+
156
+ # Connect logger to GUI window
157
+ # Get emitter from logger
158
+ emitter = self.logger_manager.get_emitter()
159
+
160
+ # Hook up emitter to the GUI
161
+ emitter.message.connect(self.log_window.append_text)
162
+
163
+ self.sim_worker = SimulationWorker(
164
+ config_path,
165
+ emitter,
166
+ cancel_callback=lambda: self.log_window.cancel_requested,
167
+ )
168
+
169
+ self.sim_thread = QThread()
170
+ self.sim_worker.moveToThread(self.sim_thread)
171
+
172
+ self.sim_thread.started.connect(self.sim_worker.run)
173
+ self.sim_worker.finished.connect(self.sim_thread.quit)
174
+ self.sim_worker.finished.connect(self.sim_worker.deleteLater)
175
+ self.sim_thread.finished.connect(self.sim_thread.deleteLater)
176
+
177
+ self.sim_worker.finished.connect(lambda: print("Simulation finished."))
178
+ self.sim_worker.finished.connect(self.logger_manager.stop)
179
+
180
+ def handle_finished():
181
+ if self.sim_worker.failed:
182
+ self.log_window.mark_failure()
183
+ else:
184
+ self.log_window.mark_success()
185
+
186
+ self.log_window.cancel_button.setText("Close")
187
+ self.log_window.cancel_button.clicked.disconnect()
188
+ self.log_window.cancel_button.clicked.connect(self.log_window.close)
189
+
190
+ self.sim_worker.finished.connect(handle_finished)
191
+ self.sim_worker.error_occurred.connect(emitter.message.emit)
192
+
193
+ self.sim_thread.start()
194
+
195
+ def open_napari_viewer(self):
196
+ """Open a file dialog to select a microscopy image and visualize it with Napari."""
197
+ # Allow user to select an image file
198
+ file_path, _ = QFileDialog.getOpenFileName(
199
+ self,
200
+ "Open Microscopy Image",
201
+ "",
202
+ "Image Files (*.tif *.tiff *.nd2 *.png *.jpg *.zarr);;All Files (*)",
203
+ )
204
+
205
+ if file_path:
206
+ try:
207
+ # Load the image (expand here if you want ND2 or Zarr support)
208
+ image = tifffile.imread(file_path)
209
+
210
+ # Open Napari viewer and display the image
211
+ viewer = napari.Viewer()
212
+ viewer.add_image(image, name=Path(file_path).stem)
213
+ napari.run()
214
+
215
+ except Exception as e:
216
+ print(f"Failed to open image: {e}")
217
+
218
+ def set_svg_logo(self, svg_path):
219
+ """Set an SVG logo to the QLabel, maintaining the aspect ratio."""
220
+ # Create a QSvgRenderer to render the SVG
221
+ renderer = QSvgRenderer(svg_path)
222
+ if renderer.isValid():
223
+ # Get the size of the SVG image
224
+ image_size = renderer.defaultSize()
225
+
226
+ # Create a QPixmap to hold the rendered SVG with the same size as the SVG image
227
+ pixmap = QPixmap(image_size * 2)
228
+ pixmap.fill(Qt.GlobalColor.transparent) # Fill the pixmap with transparency
229
+
230
+ # Use QPainter to paint the SVG onto the pixmap
231
+ painter = QPainter(pixmap)
232
+ renderer.render(painter)
233
+ painter.end()
234
+
235
+ # Scale the pixmap to fit the desired size while maintaining the aspect ratio
236
+ scaled_pixmap = pixmap.scaled(
237
+ 200,
238
+ 200,
239
+ Qt.AspectRatioMode.KeepAspectRatio,
240
+ Qt.TransformationMode.SmoothTransformation,
241
+ )
242
+
243
+ # Set the scaled pixmap as the QLabel content
244
+ self.logo_label.setPixmap(scaled_pixmap)
245
+ else:
246
+ print("Failed to load SVG file.")
247
+
248
+ def open_config_editor(self):
249
+ """Launch template selection first, then open ConfigEditor."""
250
+ self.template_window = TemplateSelectionWindow()
251
+ self.template_window.show()
252
+
253
+ def on_link_activated(self, url):
254
+ """Handle the link activation (clicking the hyperlink)."""
255
+ webbrowser.open(url) # Open the URL in the default web browser
@@ -0,0 +1,58 @@
1
+ from pathlib import Path
2
+
3
+ from PyQt6.QtCore import QObject, pyqtSignal
4
+
5
+ from ..logging.logutil import LogEmitter
6
+
7
+
8
+ class SimulationWorker(QObject):
9
+ finished = pyqtSignal()
10
+ error_occurred = pyqtSignal(str)
11
+
12
+ def __init__(self, config_path: Path, emitter: LogEmitter, cancel_callback=None):
13
+ super().__init__()
14
+ self.config_path = config_path
15
+ self.emitter = emitter
16
+ self.cancel_callback = cancel_callback or (lambda: False)
17
+ self.failed = False
18
+
19
+ def run(self):
20
+ try:
21
+ self.emitter.message.emit(f"Starting simulation for {self.config_path}")
22
+
23
+ from ..configio.convertconfig import load_config, setup_microscope
24
+ from ..configio.saving import save_config_frames
25
+
26
+ loadedconfig = load_config(self.config_path)
27
+
28
+ if "version" in loadedconfig:
29
+ version = loadedconfig["version"]
30
+ self.emitter.message.emit(f"Using config version: {version}")
31
+
32
+ setup_config = setup_microscope(loadedconfig)
33
+ microscope = setup_config["microscope"]
34
+ configEXP = setup_config["experiment_config"]
35
+ functionEXP = setup_config["experiment_func"]
36
+
37
+ if self.cancel_callback():
38
+ self.emitter.message.emit("Simulation canceled before execution.")
39
+ return
40
+
41
+ frames, metadata = functionEXP(microscope=microscope, config=configEXP)
42
+
43
+ if self.cancel_callback():
44
+ self.emitter.message.emit("Simulation canceled after run.")
45
+ return
46
+
47
+ save_config_frames(
48
+ metadata, frames, setup_config["base_config"].OutputParameter
49
+ )
50
+
51
+ self.emitter.message.emit("Simulation data saved successfully.")
52
+ self.finished.emit()
53
+
54
+ except Exception as e:
55
+ self.failed = True
56
+ self.error_occurred.emit(f"Simulation failed: {e}")
57
+ finally:
58
+ self.finished.emit()
@@ -0,0 +1,100 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import tomli
5
+ from PyQt6.QtCore import Qt
6
+ from PyQt6.QtGui import QPixmap
7
+ from PyQt6.QtWidgets import (
8
+ QGroupBox,
9
+ QHBoxLayout,
10
+ QLabel,
11
+ QMessageBox,
12
+ QPushButton,
13
+ QScrollArea,
14
+ QVBoxLayout,
15
+ QWidget,
16
+ )
17
+
18
+ from .configuration_window import ConfigEditor
19
+
20
+ TEMPLATE_DIR = Path(__file__).parent.parent / "resources" / "template_configs"
21
+ METADATA_PATH = TEMPLATE_DIR / "metadata_configs.json"
22
+
23
+
24
+ class TemplateSelectionWindow(QWidget):
25
+ def __init__(self):
26
+ super().__init__()
27
+ self.setWindowTitle("Choose a Simulation Template")
28
+ self.setMinimumSize(700, 500)
29
+ layout = QVBoxLayout()
30
+ layout.addWidget(QLabel("<h2>Select a Template to Begin</h2>"))
31
+
32
+ scroll_area = QScrollArea()
33
+ scroll_area.setWidgetResizable(True)
34
+
35
+ content_widget = QWidget()
36
+ content_layout = QVBoxLayout(content_widget)
37
+
38
+ try:
39
+ with open(METADATA_PATH, "r", encoding="utf-8") as f:
40
+ metadata = json.load(f)
41
+
42
+ for key, entry in metadata.items():
43
+ card = self.create_template_card(entry)
44
+ content_layout.addWidget(card)
45
+ except Exception as e:
46
+ error_label = QLabel(f"Failed to load templates: {e}")
47
+ layout.addWidget(error_label)
48
+
49
+ scroll_area.setWidget(content_widget)
50
+ layout.addWidget(scroll_area)
51
+ self.setLayout(layout)
52
+
53
+ def create_template_card(self, entry: dict) -> QWidget:
54
+ group = QGroupBox(entry["label"])
55
+ layout = QHBoxLayout()
56
+
57
+ # Image
58
+ img_label = QLabel()
59
+ img_path = TEMPLATE_DIR / entry["image"]
60
+ if img_path.exists():
61
+ pixmap = QPixmap(str(img_path)).scaled(
62
+ 200, 200, Qt.AspectRatioMode.KeepAspectRatio
63
+ )
64
+ img_label.setPixmap(pixmap)
65
+ else:
66
+ img_label.setText("[Missing image assets]")
67
+ layout.addWidget(img_label)
68
+
69
+ # Description + Button
70
+ vbox = QVBoxLayout()
71
+
72
+ description_label = QLabel(entry.get("description", ""))
73
+ description_label.setWordWrap(True) # <--- this is the key!
74
+ vbox.addWidget(description_label)
75
+
76
+ btn = QPushButton("Use This Template")
77
+ btn.clicked.connect(
78
+ lambda _, config=entry["config"]: self.load_template(config)
79
+ )
80
+ vbox.addWidget(btn)
81
+ layout.addLayout(vbox)
82
+
83
+ group.setLayout(layout)
84
+ return group
85
+
86
+ def load_template(self, config_filename: str):
87
+ config_path = TEMPLATE_DIR / config_filename
88
+ try:
89
+ with open(config_path, "rb") as f:
90
+ config = tomli.load(f)
91
+
92
+ self.launch_config_editor(config)
93
+ except Exception as e:
94
+ QMessageBox.critical(self, "Error", f"Failed to load template:\n{e}")
95
+
96
+ def launch_config_editor(self, config_dict: dict):
97
+ self.editor = ConfigEditor()
98
+ self.editor.set_data(config_dict)
99
+ self.editor.show()
100
+ self.close()
File without changes