"""Assemble GUI controllers and own the lifetime of the application window.""" from pathlib import Path from PyQt6.QtWidgets import QMainWindow from python_app.gui.controllers.config_controller import ConfigController from python_app.gui.controllers.control_button_controller import ControlButtonController from python_app.gui.controllers.history_controller import HistoryController from python_app.gui.controllers.live_settings_controller import LiveSettingsController from python_app.gui.controllers.log_controller import LogController from python_app.gui.controllers.pipeline_controller import PipelineController from python_app.gui.controllers.plot_controller import PlotController from python_app.gui.controllers.preprocess_controller import PreprocessController from python_app.gui.controllers.profile_controller import ProfileController, load_startup from python_app.gui.controllers.radar_settings_controller import RadarSettingsController from python_app.gui.controllers.recording_controller import RecordingController from python_app.gui.controllers.snapshot_controller import SnapshotController from python_app.gui.controllers.web_controller import WebBridge from python_app.gui.settings_defaults import cpp_bscan_replay_window_for_config from python_app.gui.window_view import WindowView from python_app.orchestration.config_writer import ConfigWriter from python_app.orchestration.live_processing_config import ProcessingLiveConfigWriter from python_app.storage.npz.store import NpzStore class AppWindow(QMainWindow): def __init__(self, project_root: Path) -> None: super().__init__() self._closed = False self._log = LogController(self, project_root, lambda text: self._view.status.setText(text)) state = load_startup(project_root, self._log) runtime = project_root / "python_app/runtime" store = NpzStore(project_root / "python_app/data") published: dict[int, dict] = {} self._history = HistoryController() self._plots = PlotController( self._history, lambda: self._config.processing_settings(), lambda: self._config.geometry(), lambda: self._pipeline.running, self._log.warning, ) self._plots.live_window = cpp_bscan_replay_window_for_config(state.config) self._view = WindowView(self, state.config, state.gui, self._plots.stack, self._log.panel, project_root=project_root) fields = {key: widget for section in self._view.sections for key, widget in section.fields.items()} fields["_log_level_combo"] = self._log.level self._config = ConfigController(fields, self._view.switches.select_single.isChecked) self._plots.bind_display_controls(self._view.switches.display_combo, self._view.switches.next_combo) def write_profile(path: Path, *, allow_overwrite: bool = True, dataset: bool = False): return self._profile.write(path, allow_overwrite=allow_overwrite, dataset=dataset) self._recording = RecordingController( store, published, write_profile, self._log.info, self._log.show_error, ) self._pipeline = PipelineController( project_root, store, ConfigWriter(runtime), self._history, self._plots, self._recording, build_config=lambda: self._profile.build_config(), live_config=lambda: self._live.build(), publish_live=lambda: self._live.publish(), capture_guard=lambda: self._preprocess.busy_reason(), refresh_radar_limits=lambda: self._radar.refresh_limits(), status=self._view.status.setText, history_changed=lambda: self._view.update_history(self._history), poll_ui=lambda: self._web.refresh_snapshot(), show_exception=self._log.show_exception, deferred_close=self.close, event_scope=self._log.suspend_error_capture, ) self._preprocess = PreprocessController( self, state.config, state.gui.preprocess_dialog, store, self._pipeline, self._plots, build_config=lambda: self._profile.build_config(), current_radar_key=lambda: self._profile.radar_key(), geometry=self._config.geometry, summary=self._view.update_preprocess, error=self._log.show_exception, ) self._snapshots = SnapshotController( self._pipeline, store, write_profile, destination=self._config.dataset_destination, build_config=lambda: self._profile.build_config(), processing_settings=published, allowed=lambda deletion: self._preprocess.data_action_allowed(deletion=deletion), log=self._log.info, error=self._log.show_exception, ) self._radar = RadarSettingsController( self._view.radar, self._view.switches, self._view.processing, lambda: state.config, self._log, reset_preprocess=self._preprocess.reset_selection, live_changed=lambda: self._live.changed(), ) self._live = LiveSettingsController( self._view.processing, self._config, self._pipeline, self._preprocess, self._radar, ProcessingLiveConfigWriter(runtime / "processing_live.json"), published, self._log, build_config=lambda: self._profile.build_config(), ) self._profile = ProfileController(self, project_root, state, self._config, self._preprocess, self._pipeline, self._radar, self._live, self._log) self._control_button = ControlButtonController(self, lambda: state.config.control_button, self._preprocess.capture_tmp_reference, self._log) self._web = WebBridge( self, project_root / "run_configs", self._pipeline, self._preprocess, self._snapshots, self._profile, self._live, self._log, save_path=self._view.data.save_path, save_name=self._view.data.save_name, record_count=self._view.data.record_count, ) self._pipeline.bind_actions(self._view.primary) self._snapshots.bind_actions(self._view.primary, self._view.data) self._preprocess.bind_actions(self._view.primary, self._view.data) self._profile.bind_actions(self._view.primary) self._radar.sync_profile(state.gui.switches.combo_mode) self._live.sync_controls() self._view.update_preprocess(self._preprocess.selected_sets) self._radar.initial_limits() self._live.mode_changed(state.gui.processing.selected_mode) self._live.publish() self._pipeline.timer.start() self._pipeline.schedule_startup() self._control_button.start() self._web.start_server() self._log.info(f"Active config profile: {state.path}") def closeEvent(self, event) -> None: # noqa: N802 if self._closed: super().closeEvent(event) return if not self._pipeline.begin_close(): event.ignore() return self._closed = True self._log.info("Window closing; shutting down runtime") try: for close in (self._web.shutdown, self._control_button.close, self._preprocess.shutdown, self._pipeline.close, self._recording.finish): try: close() except Exception as exc: self._log.exception("Shutdown operation failed", exc) finally: self._log.close() super().closeEvent(event)