SXXXXXXX_RadarDataReader/radar_data_reader/gui/main_window.py
2025-06-18 15:04:13 +02:00

263 lines
12 KiB
Python

"""
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 ..utils import logger
from ..core.export_profiles import ExportProfile
log = logger.get_logger(__name__)
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_items_for_progress = 0
self._init_vars()
self.pack(fill=tk.BOTH, expand=True)
self.master.title("Radar Data Reader")
self.master.geometry("800x750") # Increased height slightly
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.batch_id_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_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)
# --- Row 0: Profile selection and main action buttons ---
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")
# --- Row 1: Input File selection ---
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))
# --- Row 2: Output File selection ---
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))
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)
status_frame.columnconfigure(3, weight=1)
status_frame.columnconfigure(5, weight=1)
ttk.Label(status_frame, text="Batch ID:").grid(row=0, column=0, padx=5, sticky="w")
ttk.Label(status_frame, textvariable=self.batch_id_var).grid(row=0, column=1, sticky="w")
ttk.Label(status_frame, text="File Batch Cntr:").grid(row=0, column=2, padx=5, sticky="w")
ttk.Label(status_frame, textvariable=self.file_batch_counter_var).grid(row=0, column=3, sticky="w")
ttk.Label(status_frame, text="TimeTag:").grid(row=0, column=4, padx=5, sticky="w")
ttk.Label(status_frame, textvariable=self.timetag_var).grid(row=0, column=5, sticky="w")
self.progress_bar = ttk.Progressbar(status_frame, variable=self.progress_var, maximum=100)
self.progress_bar.grid(row=1, column=0, columnspan=6, sticky="ew", padx=5, pady=5)
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)
def get_output_filepath(self) -> str:
return self.output_filepath_var.get()
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:
"""Opens a 'save as' dialog for the CSV file."""
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):
"""Updates the export profile combobox."""
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:
"""Returns the name of the currently selected export profile."""
return self.active_profile_var.get()
def start_processing_ui(self):
"""Prepares the UI for processing and starts the update loop."""
self.update_ui_for_processing_state(True)
self.batch_id_var.set("Starting...")
self.file_batch_counter_var.set("N/A")
self.timetag_var.set("N/A")
self.progress_var.set(0)
self.after(100, self.poll_result_queue)
def update_ui_for_processing_state(self, is_processing: bool):
"""Toggles the state of UI controls and status bar based on processing status."""
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_var.set(0)
self.batch_id_var.set("Done")
self.master.config(cursor="")
def poll_result_queue(self):
"""Polls the result queue from the worker process for updates."""
try:
while True:
msg = self.gui_update_queue.get_nowait()
msg_type = msg.get("type")
if msg_type == "start":
self.total_items_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")
log.info(f"Processed Batch ID: {batch_id} (File Counter: {file_batch_counter}, TimeTag: {timetag})")
self.batch_id_var.set(str(batch_id))
self.file_batch_counter_var.set(str(file_batch_counter))
self.timetag_var.set(str(timetag))
if self.total_items_for_progress > 0:
progress = (msg.get("blocks_done", 0) / self.total_items_for_progress) * 100
self.progress_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(
was_interrupted=msg.get("interrupted", False)
)
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(was_interrupted=True)
self.update_ui_for_processing_state(False)
self.batch_id_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()