# 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_csv_use_tab_var = tk.BooleanVar(value=False) # New variable for tab separator 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) formats_frame.columnconfigure(2, weight=1) # Add weight to third column # CSV Options csv_options_frame = ttk.Frame(formats_frame) csv_options_frame.grid(row=0, column=0, columnspan=3, sticky="ew") ttk.Checkbutton(csv_options_frame, text="Generate .csv file", variable=self.out_output_csv_var).pack(side=tk.LEFT, padx=(5,10)) self.out_csv_profile_combobox = ttk.Combobox(csv_options_frame, textvariable=self.out_csv_profile_var, state="readonly", width=20) self.out_csv_profile_combobox.pack(side=tk.LEFT, padx=5) ttk.Checkbutton(csv_options_frame, text="Use Tab Separator", variable=self.out_csv_use_tab_var).pack(side=tk.LEFT, padx=(10,5)) # JSON Options json_options_frame = ttk.Frame(formats_frame) json_options_frame.grid(row=1, column=0, columnspan=3, sticky="ew") ttk.Checkbutton(json_options_frame, text="Generate .json file", variable=self.out_output_json_var).pack(side=tk.LEFT, padx=(5,10)) self.out_json_profile_combobox = ttk.Combobox(json_options_frame, textvariable=self.out_json_profile_var, state="readonly", width=20) self.out_json_profile_combobox.pack(side=tk.LEFT, 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()