""" Main View for the Radar Data Reader application. """ import tkinter as tk from tkinter import scrolledtext, filedialog, ttk from pathlib import Path from typing import Dict, Any, List import queue from datetime import datetime, timezone from ..utils import logger from ..core.export_profiles import ExportProfile log = logger.get_logger(__name__) def _format_timetag(timetag: Any) -> str: """Safely formats a numeric timetag into a human-readable UTC string.""" if not isinstance(timetag, (int, float)) or timetag <= 0: return "N/A" try: dt_object = datetime.fromtimestamp(timetag, tz=timezone.utc) return dt_object.strftime("%Y-%m-%d %H:%M:%S UTC") except (ValueError, OSError): return f"Invalid timestamp ({timetag})" class MainWindow(tk.Frame): """The main application window (View).""" def __init__(self, master: tk.Tk, controller, logging_config: Dict[str, Any]): super().__init__(master) self.master = master self.controller = controller self.gui_update_queue = controller.result_queue self.total_blocks_for_progress = 0 self._init_vars() self.pack(fill=tk.BOTH, expand=True) self.master.title("Radar Data Reader") self.master.geometry("800x750") self._create_widgets() self._setup_gui_logging(logging_config) self.master.protocol("WM_DELETE_WINDOW", self.on_close) log.info("Main window View initialized.") def _init_vars(self): """Initialize all Tkinter variables.""" self.filepath_var = tk.StringVar() self.output_filepath_var = tk.StringVar() self.active_profile_var = tk.StringVar() self.progress_text_var = tk.StringVar(value="N/A") self.batches_found_var = tk.StringVar(value="N/A") self.file_batch_counter_var = tk.StringVar(value="N/A") self.timetag_var = tk.StringVar(value="N/A") self.progress_bar_var = tk.DoubleVar(value=0) self.status_bar_var = tk.StringVar(value="Ready") def _create_widgets(self): """Create all the widgets for the main window.""" menu_bar = tk.Menu(self.master) self.master.config(menu=menu_bar) file_menu = tk.Menu(menu_bar, tearoff=0) menu_bar.add_cascade(label="File", menu=file_menu) file_menu.add_command( label="Manage Export Profiles...", command=self.controller.open_profile_editor, ) file_menu.add_separator() file_menu.add_command(label="Exit", command=self.on_close) main_frame = tk.Frame(self) main_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) main_frame.rowconfigure(2, weight=1) main_frame.columnconfigure(0, weight=1) controls_frame = ttk.LabelFrame(main_frame, text="Controls") controls_frame.grid(row=0, column=0, sticky="ew", pady=(0, 5)) controls_frame.columnconfigure(1, weight=1) ttk.Label(controls_frame, text="Export Profile:").grid( row=0, column=0, padx=5, pady=5, sticky="w" ) self.profile_combobox = ttk.Combobox( controls_frame, textvariable=self.active_profile_var, state="readonly" ) self.profile_combobox.grid(row=0, column=1, padx=(0, 5), pady=5, sticky="ew") self.process_button = ttk.Button( controls_frame, text="Process File", command=self.on_process_click ) self.process_button.grid(row=0, column=2, padx=(0, 5), pady=5, sticky="ew") self.stop_button = ttk.Button( controls_frame, text="Stop", command=self.on_stop_click, state=tk.DISABLED ) self.stop_button.grid(row=0, column=3, padx=(5, 5), pady=5, sticky="ew") file_select_frame = ttk.Frame(controls_frame) file_select_frame.grid( row=1, column=0, columnspan=4, sticky="ew", padx=5, pady=(0, 5) ) file_select_frame.columnconfigure(1, weight=1) ttk.Label(file_select_frame, text="Radar File:").grid( row=0, column=0, padx=(0, 5) ) self.file_entry = ttk.Entry( file_select_frame, textvariable=self.filepath_var, state="readonly" ) self.file_entry.grid(row=0, column=1, sticky="ew") self.browse_button = ttk.Button( file_select_frame, text="Browse...", command=self.on_browse_click ) self.browse_button.grid(row=0, column=2, padx=(5, 0)) output_file_frame = ttk.Frame(controls_frame) output_file_frame.grid( row=2, column=0, columnspan=4, sticky="ew", padx=5, pady=(0, 5) ) output_file_frame.columnconfigure(1, weight=1) ttk.Label(output_file_frame, text="Output CSV File:").grid( row=0, column=0, padx=(0, 5) ) self.output_file_entry = ttk.Entry( output_file_frame, textvariable=self.output_filepath_var ) self.output_file_entry.grid(row=0, column=1, sticky="ew") self.save_as_button = ttk.Button( output_file_frame, text="Save As...", command=self.on_save_as_click ) self.save_as_button.grid(row=0, column=2, padx=(5, 0)) self.open_folder_button = ttk.Button( output_file_frame, text="Open Folder", command=self.on_open_folder_click, state=tk.DISABLED, ) self.open_folder_button.grid(row=0, column=3, padx=(5, 0)) status_frame = ttk.LabelFrame(main_frame, text="Live Data & Progress") status_frame.grid(row=1, column=0, sticky="ew", pady=5) status_frame.columnconfigure(1, weight=1) self.progress_bar = ttk.Progressbar( status_frame, variable=self.progress_bar_var, maximum=100 ) self.progress_bar.grid( row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5, 2) ) ttk.Label(status_frame, text="Progress:", anchor="e").grid( row=1, column=0, padx=(10, 5), pady=2, sticky="e" ) ttk.Label(status_frame, textvariable=self.progress_text_var, anchor="w").grid( row=1, column=1, padx=5, pady=2, sticky="w" ) ttk.Label(status_frame, text="Batches Found:", anchor="e").grid( row=2, column=0, padx=(10, 5), pady=2, sticky="e" ) ttk.Label(status_frame, textvariable=self.batches_found_var, anchor="w").grid( row=2, column=1, padx=5, pady=2, sticky="w" ) ttk.Label(status_frame, text="File Batch Counter:", anchor="e").grid( row=3, column=0, padx=(10, 5), pady=2, sticky="e" ) ttk.Label( status_frame, textvariable=self.file_batch_counter_var, anchor="w" ).grid(row=3, column=1, padx=5, pady=2, sticky="w") ttk.Label(status_frame, text="File TimeTag:", anchor="e").grid( row=4, column=0, padx=(10, 5), pady=(2, 5), sticky="e" ) ttk.Label(status_frame, textvariable=self.timetag_var, anchor="w").grid( row=4, column=1, padx=5, pady=(2, 5), sticky="w" ) log_frame = ttk.LabelFrame(main_frame, text="Log Console") log_frame.grid(row=2, column=0, sticky="nsew", pady=(5, 0)) log_frame.rowconfigure(0, weight=1) log_frame.columnconfigure(0, weight=1) self.log_widget = scrolledtext.ScrolledText( log_frame, state=tk.DISABLED, wrap=tk.WORD ) self.log_widget.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) self.status_bar = ttk.Label( self, textvariable=self.status_bar_var, relief=tk.SUNKEN, anchor=tk.W, padding=2, ) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) def _setup_gui_logging(self, logging_config): logger.add_tkinter_handler(self.log_widget, self.master, logging_config) def set_filepath(self, path: str): self.filepath_var.set(path) def get_filepath(self) -> str: return self.filepath_var.get() def set_output_filepath(self, path: str): self.output_filepath_var.set(path) if path and Path(path).parent.exists(): self.open_folder_button.config(state=tk.NORMAL) else: self.open_folder_button.config(state=tk.DISABLED) def get_output_filepath(self) -> str: return self.output_filepath_var.get() def on_open_folder_click(self): self.controller.open_output_folder() def ask_open_filename(self, current_path: str) -> str: initial_dir = ( Path(current_path).parent if current_path and Path(current_path).exists() else Path.cwd() ) return filedialog.askopenfilename( initialdir=initial_dir, filetypes=[("Radar Output", "*.out"), ("All files", "*.*")], ) def ask_save_as_filename(self, current_path: str) -> str: initial_dir = Path(current_path).parent if current_path else Path.cwd() initial_file = Path(current_path).name if current_path else "" return filedialog.asksaveasfilename( initialdir=initial_dir, initialfile=initial_file, defaultextension=".csv", filetypes=[("CSV files", "*.csv"), ("All files", "*.*")], ) def update_export_profiles( self, profiles: List[ExportProfile], active_profile_name: str ): profile_names = [p.name for p in profiles] self.profile_combobox["values"] = profile_names if active_profile_name in profile_names: self.active_profile_var.set(active_profile_name) elif profile_names: self.active_profile_var.set(profile_names[0]) else: self.active_profile_var.set("") def get_active_profile_name(self) -> str: return self.active_profile_var.get() def start_processing_ui(self): self.update_ui_for_processing_state(True) self.progress_text_var.set("Starting...") self.batches_found_var.set("N/A") self.file_batch_counter_var.set("N/A") self.timetag_var.set("N/A") self.progress_bar_var.set(0) self.after(100, self.poll_result_queue) def update_ui_for_processing_state(self, is_processing: bool): state = tk.DISABLED if is_processing else tk.NORMAL self.browse_button.config(state=state) self.save_as_button.config(state=state) self.process_button.config(state=state) self.profile_combobox.config(state=state) self.stop_button.config(state=tk.NORMAL if is_processing else tk.DISABLED) if is_processing: self.status_bar_var.set("Processing... Please wait.") self.master.config(cursor="watch") else: self.status_bar_var.set("Ready") self.progress_bar_var.set(0) self.progress_text_var.set("Done") self.master.config(cursor="") def poll_result_queue(self): try: while True: msg = self.gui_update_queue.get_nowait() msg_type = msg.get("type") if msg_type == "start": self.total_blocks_for_progress = msg.get("total", 0) elif msg_type == "progress": batch_id = msg.get("batch_id", "N/A") file_batch_counter = msg.get("file_batch_counter", "N/A") timetag = msg.get("timetag", "N/A") blocks_done = msg.get("blocks_done", 0) self.batches_found_var.set(str(batch_id)) self.progress_text_var.set( f"Block {blocks_done} / {self.total_blocks_for_progress}" ) self.file_batch_counter_var.set(str(file_batch_counter)) self.timetag_var.set(f"{timetag} ({_format_timetag(timetag)})") if self.total_blocks_for_progress > 0: progress = (blocks_done / self.total_blocks_for_progress) * 100 self.progress_bar_var.set(progress) elif msg_type == "data_batch": self.controller.handle_data_batch(msg.get("data")) elif msg_type == "complete": self.controller.handle_worker_completion(msg) self.update_ui_for_processing_state(False) return elif msg_type == "error": log.error(f"Received error from worker: {msg.get('message')}") self.controller.handle_worker_completion(msg) self.update_ui_for_processing_state(False) self.progress_text_var.set("Error!") return except queue.Empty: pass except Exception as e: log.error(f"Error in GUI polling loop: {e}") if self.controller.is_processing: self.after(100, self.poll_result_queue) def on_browse_click(self): self.controller.select_file() def on_process_click(self): self.controller.start_processing() def on_stop_click(self): self.controller.stop_processing() def on_close(self): self.controller.shutdown() self.master.destroy() def on_save_as_click(self): self.controller.select_output_file()