263 lines
12 KiB
Python
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() |