# 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 .gui_utils import center_window from ..utils import logger from ..core.export_profiles import ExportProfile log = logger.get_logger(__name__) try: 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: WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" WRAPPER_BUILD_INFO = "Wrapper build time unknown" 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_csv_use_tab_var = tk.BooleanVar(value=False) self.out_output_json_var = tk.BooleanVar(value=False) self.out_use_full_path_var = tk.BooleanVar(value=False) # New variable 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="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) 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 ) # --- Reorganized Options Grid --- formats_frame = ttk.LabelFrame(parent, text="Output Formats & Options") formats_frame.grid(row=2, column=0, columnspan=3, sticky="ew", padx=5, pady=5) formats_frame.columnconfigure(1, weight=1) # CSV Options 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=25, ) self.out_csv_profile_combobox.grid(row=0, column=1, sticky="w", padx=5) # JSON Options 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=25, ) self.out_json_profile_combobox.grid(row=1, column=1, sticky="w", padx=5) # General Formatting Options in a separate sub-frame for alignment options_subframe = ttk.Frame(formats_frame) options_subframe.grid(row=0, column=2, rowspan=2, sticky="w", padx=(20, 5)) ttk.Checkbutton( options_subframe, text="Use Tab Separator (CSV)", variable=self.out_csv_use_tab_var, ).pack(anchor="w") ttk.Checkbutton( options_subframe, text="Use Full Path for Headers", variable=self.out_use_full_path_var, ).pack(anchor="w") 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" ) ttk.Spinbox( input_frame, from_=1, to=1000, textvariable=self.rec_file_count_var, width=10, ).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): self.process_generated_out_button.config( state=tk.NORMAL if conversion_successful else 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"))) if self.total_blocks_for_progress > 0: self.progress_text_var.set( f"Block {blocks_done} / {self.total_blocks_for_progress}" ) 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()