SXXXXXXX_RadarDataReader/radar_data_reader/gui/main_window.py
2025-06-24 07:38:42 +02:00

342 lines
17 KiB
Python

# radar_data_reader/gui/main_window.py
"""
Main View for the Radar Data Reader application.
Features a tab for processing .out files and a tab for converting .rec files.
"""
import tkinter as tk
from tkinter import scrolledtext, ttk, messagebox
import logging
from typing import Dict, Any, List
import queue
from ..utils import logger
from ..core.export_profiles import ExportProfile
log = logger.get_logger(__name__)
# --- Import Version Info FOR THE WRAPPER ITSELF ---
try:
# Use absolute import based on package name
from radar_data_reader import _version as wrapper_version
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
except ImportError:
# This might happen if you run the wrapper directly from source
# without generating its _version.py first (if you use that approach for the wrapper itself)
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
# --- Constants for Version Generation ---
DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- End Constants ---
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.profile_editor_window = None
self.rec_config_window = None
self._init_vars()
self.pack(fill=tk.BOTH, expand=True)
self.master.title(f"Radar Data Reader & Processor - {WRAPPER_APP_VERSION_STRING}")
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.status_bar_var = tk.StringVar(value="Ready")
self.out_filepath_var = tk.StringVar()
self.out_output_dir_var = tk.StringVar()
self.out_basename_var = tk.StringVar()
self.out_output_csv_var = tk.BooleanVar(value=True)
self.out_output_json_var = tk.BooleanVar(value=False)
self.out_csv_profile_var = tk.StringVar()
self.out_json_profile_var = tk.StringVar()
self.rec_filepath_var = tk.StringVar()
self.rec_file_count_var = tk.IntVar(value=1)
self.rec_output_dir_var = tk.StringVar()
self.rec_basename_var = tk.StringVar()
self.progress_text_var = tk.StringVar(value="N/A")
self.batches_found_var = tk.StringVar(value="N/A")
self.progress_bar_var = tk.DoubleVar(value=0)
def _create_widgets(self):
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.columnconfigure(0, weight=1)
main_frame.rowconfigure(1, weight=1)
self.notebook = ttk.Notebook(main_frame)
self.notebook.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
self.out_processor_tab = ttk.Frame(self.notebook, padding="10")
self.rec_converter_tab = ttk.Frame(self.notebook, padding="10")
self.notebook.add(self.out_processor_tab, text="OUT Processor")
self.notebook.add(self.rec_converter_tab, text="REC to OUT Converter")
self._create_out_processor_tab(self.out_processor_tab)
self._create_rec_converter_tab(self.rec_converter_tab)
self._create_log_console_frame(main_frame)
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 _create_out_processor_tab(self, parent):
parent.columnconfigure(1, weight=1)
input_frame = ttk.LabelFrame(parent, text="Input .out File")
input_frame.grid(row=0, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
input_frame.columnconfigure(1, weight=1)
ttk.Label(input_frame, text="File Path:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
out_file_entry = ttk.Entry(input_frame, textvariable=self.out_filepath_var, state="readonly")
out_file_entry.grid(row=0, column=1, sticky="ew", padx=5)
self.out_browse_button = ttk.Button(input_frame, text="Browse...", command=self.controller.select_out_file)
self.out_browse_button.grid(row=0, column=2, padx=5, pady=5)
self.out_filepath_var.trace_add("write", self.controller.on_out_config_changed)
output_frame = ttk.LabelFrame(parent, text="CSV/JSON Output Configuration")
output_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
output_frame.columnconfigure(1, weight=1)
ttk.Label(output_frame, text="Output Directory:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
out_dir_entry = ttk.Entry(output_frame, textvariable=self.out_output_dir_var)
out_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
# --- Buttons Frame ---
out_dir_buttons_frame = ttk.Frame(output_frame)
out_dir_buttons_frame.grid(row=0, column=2, padx=5)
ttk.Button(out_dir_buttons_frame, text="Browse...", command=lambda: self.controller.select_output_dir(self.out_output_dir_var)).pack(side=tk.LEFT)
ttk.Button(out_dir_buttons_frame, text="Open...", command=lambda: self.controller.open_folder_from_path(self.out_output_dir_var.get())).pack(side=tk.LEFT, padx=(5, 0))
ttk.Label(output_frame, text="Base Filename:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
ttk.Entry(output_frame, textvariable=self.out_basename_var).grid(row=1, column=1, columnspan=2, sticky="ew", padx=5)
formats_frame = ttk.LabelFrame(parent, text="Output Formats & Profiles")
formats_frame.grid(row=2, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
formats_frame.columnconfigure(1, weight=1)
ttk.Checkbutton(formats_frame, text="Generate .csv file", variable=self.out_output_csv_var).grid(row=0, column=0, sticky="w", padx=5, pady=2)
self.out_csv_profile_combobox = ttk.Combobox(formats_frame, textvariable=self.out_csv_profile_var, state="readonly", width=20)
self.out_csv_profile_combobox.grid(row=0, column=1, sticky="w", padx=5)
ttk.Checkbutton(formats_frame, text="Generate .json file", variable=self.out_output_json_var).grid(row=1, column=0, sticky="w", padx=5, pady=2)
self.out_json_profile_combobox = ttk.Combobox(formats_frame, textvariable=self.out_json_profile_var, state="readonly", width=20)
self.out_json_profile_combobox.grid(row=1, column=1, sticky="w", padx=5)
action_frame = ttk.Frame(parent)
action_frame.grid(row=3, column=0, columnspan=3, pady=(10, 0))
self.out_process_button = ttk.Button(action_frame, text="Process .out File", command=self.controller.start_out_processing)
self.out_process_button.pack(side=tk.LEFT, padx=5)
self.out_stop_button = ttk.Button(action_frame, text="Stop", command=self.controller.stop_processing, state=tk.DISABLED)
self.out_stop_button.pack(side=tk.LEFT, padx=5)
self._create_live_data_frame(parent).grid(row=4, column=0, columnspan=3, sticky="ew", padx=5, pady=(10,0))
def _create_rec_converter_tab(self, parent):
parent.columnconfigure(1, weight=1)
input_frame = ttk.LabelFrame(parent, text="Input REC Sequence")
input_frame.grid(row=0, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
input_frame.columnconfigure(1, weight=1)
ttk.Label(input_frame, text="First .rec File:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
rec_file_entry = ttk.Entry(input_frame, textvariable=self.rec_filepath_var, state="readonly")
rec_file_entry.grid(row=0, column=1, sticky="ew", padx=5)
ttk.Button(input_frame, text="Browse...", command=self.controller.select_rec_file).grid(row=0, column=2, padx=5)
ttk.Label(input_frame, text="Number of Files (/n):").grid(row=1, column=0, padx=5, pady=5, sticky="w")
rec_file_count_spinbox = ttk.Spinbox(input_frame, from_=1, to=1000, textvariable=self.rec_file_count_var, width=10)
rec_file_count_spinbox.grid(row=1, column=1, padx=5, pady=5, sticky="w")
self.rec_filepath_var.trace_add("write", self.controller.on_rec_config_changed)
self.rec_file_count_var.trace_add("write", self.controller.on_rec_config_changed)
output_frame = ttk.LabelFrame(parent, text="Generated .out File")
output_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=5, pady=5)
output_frame.columnconfigure(1, weight=1)
ttk.Label(output_frame, text="Output Directory:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
rec_dir_entry = ttk.Entry(output_frame, textvariable=self.rec_output_dir_var)
rec_dir_entry.grid(row=0, column=1, sticky="ew", padx=5)
rec_dir_buttons_frame = ttk.Frame(output_frame)
rec_dir_buttons_frame.grid(row=0, column=2, padx=5)
ttk.Button(rec_dir_buttons_frame, text="Browse...", command=lambda: self.controller.select_output_dir(self.rec_output_dir_var)).pack(side=tk.LEFT)
ttk.Button(rec_dir_buttons_frame, text="Open...", command=lambda: self.controller.open_folder_from_path(self.rec_output_dir_var.get())).pack(side=tk.LEFT, padx=(5, 0))
ttk.Label(output_frame, text="Generated Filename:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
ttk.Label(output_frame, textvariable=self.rec_basename_var, relief="sunken", anchor="w").grid(row=1, column=1, columnspan=2, sticky="ew", padx=5)
action_frame = ttk.Frame(parent)
action_frame.grid(row=2, column=0, columnspan=3, pady=(10, 0))
self.rec_convert_button = ttk.Button(action_frame, text="Convert REC to OUT", command=self.controller.start_rec_conversion)
self.rec_convert_button.pack(side=tk.LEFT, padx=5)
self.rec_config_button = ttk.Button(action_frame, text="g_reconverter Advanced Config...", command=self.controller.open_rec_config_editor)
self.rec_config_button.pack(side=tk.LEFT, padx=5)
self.process_generated_out_button = ttk.Button(action_frame, text="Process Generated .out File ->", command=self.controller.process_last_generated_out, state=tk.DISABLED)
self.process_generated_out_button.pack(side=tk.LEFT, padx=5)
def _create_live_data_frame(self, parent):
status_frame = ttk.LabelFrame(parent, text="Live Data & Progress (.out Processor)")
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")
return status_frame
def _create_log_console_frame(self, parent):
log_frame = ttk.LabelFrame(parent, text="Log Console")
log_frame.grid(row=1, 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, bg="#f0f0f0")
self.log_widget.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
self.log_widget.tag_config("INFO", foreground="black")
self.log_widget.tag_config("ERROR", foreground="red", font=("", 0, "bold"))
self.log_widget.tag_config("SUCCESS", foreground="green")
self.log_widget.tag_config("WARNING", foreground="orange")
self.log_widget.tag_config("DEBUG", foreground="gray")
self.log_widget.tag_config("CMD", foreground="blue")
def _setup_gui_logging(self, logging_config):
logger.add_tkinter_handler(self.log_widget, self.master, logging_config)
def update_export_profiles(self, profiles: List[ExportProfile], **kwargs):
profile_names = [p.name for p in profiles] if profiles else []
active_out_profile = kwargs.get("active_out_profile", "")
for combo, var, active_name in [
(self.out_csv_profile_combobox, self.out_csv_profile_var, active_out_profile),
(self.out_json_profile_combobox, self.out_json_profile_var, active_out_profile),
]:
combo["values"] = profile_names
if active_name in profile_names: var.set(active_name)
elif profile_names: var.set(profile_names[0])
else: var.set("")
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.progress_bar_var.set(0)
self.process_generated_out_button.config(state=tk.DISABLED)
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.out_browse_button.config(state=state)
self.out_process_button.config(state=state)
self.rec_convert_button.config(state=state)
self.rec_config_button.config(state=state)
if is_processing:
self.process_generated_out_button.config(state=tk.DISABLED)
self.out_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 update_rec_tab_buttons_state(self, conversion_successful: bool):
"""Called by the controller to update button states after conversion."""
if conversion_successful:
self.process_generated_out_button.config(state=tk.NORMAL)
else:
self.process_generated_out_button.config(state=tk.DISABLED)
def poll_result_queue(self):
try:
while not self.gui_update_queue.empty():
msg = self.gui_update_queue.get_nowait()
msg_type = msg.get("type")
if msg_type == 'log':
level_str = msg.get('level', 'INFO').upper()
level_map = {'ERROR': logging.ERROR, 'WARNING': logging.WARNING, 'SUCCESS': logging.INFO, 'DEBUG': logging.DEBUG}
log_level = level_map.get(level_str, logging.INFO)
log.log(log_level, f"[C++ Runner] {msg.get('message')}")
elif msg_type == "start":
self.total_blocks_for_progress = msg.get("total", 0)
elif msg_type == "progress":
blocks_done = msg.get("blocks_done", 0)
self.batches_found_var.set(str(msg.get("batch_id", "N/A")))
self.progress_text_var.set(f"Block {blocks_done} / {self.total_blocks_for_progress}")
if self.total_blocks_for_progress > 0:
self.progress_bar_var.set((blocks_done / self.total_blocks_for_progress) * 100)
elif msg_type == "data_batch":
self.controller.handle_data_batch(msg.get("data"))
elif msg_type in ('success', 'complete', 'error'):
if msg_type == 'error': log.error(f"Received error from worker: {msg.get('message')}")
self.controller.handle_worker_completion(msg)
return
except queue.Empty:
pass
except Exception as e:
log.error(f"Error in GUI polling loop: {e}", exc_info=True)
if self.controller.is_processing:
self.controller.handle_worker_completion({'type': 'error', 'message': str(e)})
if self.controller.is_processing:
self.after(100, self.poll_result_queue)
def on_close(self):
if self.controller.is_processing:
if messagebox.askyesno("Confirm Exit", "A process is still running. Are you sure you want to exit?"):
self.controller.shutdown()
self.master.destroy()
else:
self.controller.shutdown()
self.master.destroy()