import tkinter as tk from tkinter import scrolledtext, filedialog, messagebox, ttk import subprocess import threading import queue import json import os import datetime import sys # Needed for sys.platform in open_gui_log_file # Import the runner function from the core module from gui_g_reconverter.core.g_reconvert_runner import run_g_reconvert # --- Constants --- DEFAULT_GUI_LOG_FILE = "gui_execution_log.txt" PROFILE_EXTENSION = ".json" PROFILE_FILETYPES = [("JSON Profile", f"*{PROFILE_EXTENSION}"), ("All files", "*.*")] # Define a default profile file name DEFAULT_PROFILE_FILE = "default_launch_profile.json" # Specific file extension for input BIN_FILE_EXTENSION = ".rec" BIN_FILETYPES = [("REC files", f"*{BIN_FILE_EXTENSION}"), ("All files", "*.*")] # Polling interval for the output queue in milliseconds OUTPUT_QUEUE_POLLING_INTERVAL = 50 # Reduced from 100ms for potentially faster updates # --- Import Version Info FOR THE WRAPPER ITSELF --- try: # Use absolute import based on package name from gui_g_reconverter 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" # --- End Import Version Info --- # --- Constants for Version Generation --- DEFAULT_VERSION = "0.0.0+unknown" DEFAULT_COMMIT = "Unknown" DEFAULT_BRANCH = "Unknown" # --- End Constants --- class CppConverterGUI: """ A Tkinter GUI for setting parameters via tabs, running g_reconvert.exe via a core module, and capturing its console output. """ def __init__(self, master_window): """ Initialize the GUI application. Args: master_window: The main Tkinter window. """ self.master_window = master_window self.master_window.title(f"g_reconvert.exe Interface - {WRAPPER_APP_VERSION_STRING}") # Adjusted initial window size - Further reduced height for more compact layout self.master_window.geometry("950x650") # Adjusted height, might need tweaking based on content self.gui_log_file_path = DEFAULT_GUI_LOG_FILE # Use renamed log path # Queue for communication between worker thread and GUI thread self.output_queue = queue.Queue() self.process_running = False # --- Tkinter Variables for Parameters --- # This will now be loaded from the profile self.cpp_executable_path_var = tk.StringVar() # Input/Output self.bin_file_var = tk.StringVar() # output_file_var will now store the output *directory* self.output_dir_var = tk.StringVar() # Processing Options - Booleans self.analyze_only_var = tk.BooleanVar(value=False) # /a self.no_sign_var = tk.BooleanVar(value=False) # /nosign self.use_fw_header_var = tk.BooleanVar(value=False) # /hfw self.fix_dsp_bug_var = tk.BooleanVar(value=False) # /$pbug self.sync_signal_flows_var = tk.BooleanVar(value=False) # /h self.extract_sw_only_var = tk.BooleanVar(value=False) # /sw (positive form) self.extract_dsp_only_var = tk.BooleanVar(value=False) # /dsp (positive form) self.stop_on_discontinuity_var = tk.BooleanVar(value=False) # /ss self.dry_run_var = tk.BooleanVar(value=False) # /dryrun self.start_from_stby_var = tk.BooleanVar(value=False) # /z self.fix_sata_date_var = tk.BooleanVar(value=False) # /d (fix date) self.examine_data_var = tk.BooleanVar(value=False) # /x # Processing Options - With Values # Set default value for concatenation from batch file self.concatenate_n_var = tk.StringVar(value="3") # /n=VALUE # Set default value for post-process level from batch file, and check the box self.post_process_var = tk.BooleanVar(value=True) # /p (is active) self.post_process_level_var = tk.StringVar(value="1") # /p=VALUE self.output_format_var = tk.StringVar() # /f=VALUE # Advanced Options self.json_config_file_var = tk.StringVar() # /j=PATH # g_reconvert_log_file_var will be generated based on output_dir_var and bin_file_var # This variable will hold the generated path for display, not loaded/saved directly self.g_reconvert_log_file_var = tk.StringVar() # Internal variable to hold the full log file path for the command list self._generated_log_file_path = "" # Internal variable to hold the full output file path for the command list self._generated_output_file_path = "" self.max_batches_var = tk.StringVar() # /m=VALUE self.pri_number_var = tk.StringVar() # /r=VALUE self.rbin_number_var = tk.StringVar() # /c=VALUE # Video Options # Set default values from batch file self.video_out_to_rec_var = tk.BooleanVar(value=False) # /vout (positive form) self.video_show_var = tk.BooleanVar(value=True) # /vshow (positive form) self.video_save_var = tk.BooleanVar(value=True) # /vsave self.video_fix_framerate_var = tk.BooleanVar(value=False) # /vframe self.gps_save_track_var = tk.BooleanVar(value=True) # /gps self.fix_hr_debug_var = tk.BooleanVar(value=False) # /fixhr self.sar_save_only_var = tk.BooleanVar(value=False) # /sar self.video_add_subtitles_var = tk.BooleanVar(value=False) # /vst # Debug/Control Options (//flags) self.verbose_var = tk.BooleanVar(value=False) # //v self.debug_messages_var = tk.BooleanVar(value=False) # //d self.quiet_var = tk.BooleanVar(value=False) # //q self.mask_warnings_var = tk.BooleanVar(value=False) # //w self.mask_errors_var = tk.BooleanVar(value=False) # //e # Set default value from batch file self.silent_overwrite_var = tk.BooleanVar(value=True) # //o (g_reconvert overwrite) self._initialize_ui() self._create_menu() # --- Load default profile on startup --- self._load_default_profile() # --- End Load default profile --- # Handle window close event self.master_window.protocol("WM_DELETE_WINDOW", self._confirm_exit) self._log_message_to_gui_log("GUI Application Started.", include_timestamp=True) self._update_status_bar("Ready. Set g_reconvert.exe Path if not done.") self._apply_initial_widget_states() # Ensure dynamic states are set # Ensure generated paths are updated after potential profile load self._update_generated_file_paths() # Trace changes to output_dir_var to update button state self.output_dir_var.trace_add("write", lambda *args: self._update_go_to_output_button_state()) # Initial update of the button state self._update_go_to_output_button_state() def _load_default_profile(self): """Attempts to load the default profile file on application startup.""" default_profile_path = DEFAULT_PROFILE_FILE if os.path.exists(default_profile_path): self._insert_output_text(f"Attempting to load default profile: {default_profile_path}\n", 'INFO') self.load_profile(filepath=default_profile_path) # Pass the path to load_profile else: self._insert_output_text("No default profile found. Starting with default settings.\n", 'INFO') # Even without a profile, ensure generated paths are initialized based on empty fields self._update_generated_file_paths() def _apply_initial_widget_states(self): """Ensure dynamically enabled/disabled widgets are in correct initial state.""" self._on_post_process_toggle() # Enable/disable post-process level entry self._on_verbosity_change() # Ensure verbose/quiet consistency def _initialize_ui(self): """Set up the main UI layout and widgets.""" # --- Status Bar --- # Place the status bar at the bottom of the main window. self.status_bar = tk.Label(self.master_window, text="Status: Ready", bd=1, relief=tk.SUNKEN, anchor=tk.W) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) # --- End Status Bar --- # --- Main Content Area --- # Widgets are packed directly into the master window (above the status bar) # No main canvas and scrollbar needed. # --- Top Frame for Executable Path Display --- # This frame will be packed directly into the master window. exe_path_outer_frame = tk.Frame(self.master_window) exe_path_outer_frame.pack(fill=tk.X, padx=10, pady=(10, 0)) exe_path_frame = tk.Frame(exe_path_outer_frame) exe_path_frame.pack(fill=tk.X, pady=(0,5)) tk.Label(exe_path_frame, text="g_reconvert.exe Path:").pack(side=tk.LEFT, anchor=tk.W) # Make this display read-only and use a Label instead of Entry if we don't want manual typing self.exe_path_display = tk.Label(exe_path_frame, textvariable=self.cpp_executable_path_var, relief=tk.SUNKEN, anchor=tk.W, wraplength=800) self.exe_path_display.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5,0)) # No browse button here anymore - path is set via menu # --- Parameter Tabs using ttk.Notebook --- # The notebook will be packed directly into the master window. param_notebook = ttk.Notebook(self.master_window, padding="5 5 5 5") param_notebook.pack(fill=tk.X, padx=10, pady=5, expand=False) # Notebook fills horizontally # Create frames for each tab (these frames are children of the notebook) io_tab = ttk.Frame(param_notebook, padding="10 10 10 10") processing_tab = ttk.Frame(param_notebook, padding="10 10 10 10") advanced_tab = ttk.Frame(param_notebook, padding="10 10 10 10") video_tab = ttk.Frame(param_notebook, padding="10 10 10 10") debug_control_tab = ttk.Frame(param_notebook, padding="10 10 10 10") # Add frames as tabs to the notebook param_notebook.add(io_tab, text="Input/Output Files") param_notebook.add(processing_tab, text="Main Processing Options") param_notebook.add(advanced_tab, text="Advanced Config & Numerics") param_notebook.add(video_tab, text="Video Options") param_notebook.add(debug_control_tab, text="Execution Control (// flags)") # Populate tabs with widgets - Pass the correct tab frame as parent # These functions will now use the grid layout manager to spread items self._setup_io_widgets(io_tab) self._setup_processing_widgets(processing_tab) self._setup_advanced_widgets(advanced_tab) self._setup_video_widgets(video_tab) self._setup_debug_control_widgets(debug_control_tab) # --- End Notebook --- # --- Controls Frame (Run button) --- # This frame will be packed directly into the master window. controls_frame = tk.Frame(self.master_window, pady=10) controls_frame.pack(fill=tk.X, padx=10) # Fills horizontally within master_window self._setup_control_buttons(controls_frame) # Add the run button here # --- Output Area --- # This frame will be packed directly into the master window. # It should expand to fill the *remaining* vertical space. output_frame = tk.LabelFrame(self.master_window, text="Console Output", padx=10, pady=10) # fill=BOTH and expand=True ensure it uses the remaining space output_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5,0)) self._setup_output_area(output_frame) # Setup the ScrolledText inside def _create_menu(self): """Create the application menu bar.""" menubar = tk.Menu(self.master_window) self.master_window.config(menu=menubar) file_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="File", menu=file_menu) # This menu option is the only way to change the executable path file_menu.add_command(label="Set g_reconvert.exe Path", command=self.select_cpp_executable) file_menu.add_separator() file_menu.add_command(label="Load Profile", command=self.load_profile) file_menu.add_command(label="Save Profile", command=self.save_profile) # Add an option to save the current settings as the default profile file_menu.add_command(label="Save as Default Profile", command=self._save_as_default_profile) file_menu.add_separator() file_menu.add_command(label="Open GUI Log File", command=self.open_gui_log_file) file_menu.add_separator() file_menu.add_command(label="Exit", command=self._confirm_exit) def _save_as_default_profile(self): """Saves the current parameters to the default profile file.""" self.save_profile(filepath=DEFAULT_PROFILE_FILE) # Pass the default file path def _setup_io_widgets(self, parent_frame): """Create and arrange widgets for input/output files within a frame using grid.""" parent_frame.columnconfigure(1, weight=1) # Make the entry column expandable # Source Binary File (binfile) - Filter for .rec files tk.Label(parent_frame, text=f"Source Binary File ({BIN_FILE_EXTENSION}):*").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W) bin_file_entry = tk.Entry(parent_frame, textvariable=self.bin_file_var) bin_file_entry.grid(row=0, column=1, padx=5, pady=5, sticky=tk.EW) tk.Button(parent_frame, text="Browse...", command=self._select_bin_file).grid(row=0, column=2, padx=5, pady=5) # Output Directory - Select a directory instead of a file tk.Label(parent_frame, text="Output Directory:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W) output_dir_entry = tk.Entry(parent_frame, textvariable=self.output_dir_var) output_dir_entry.grid(row=1, column=1, padx=5, pady=5, sticky=tk.EW) tk.Button(parent_frame, text="Browse...", command=self._select_output_directory).grid(row=1, column=2, padx=5, pady=5) tk.Label(parent_frame, text=f"* A .{BIN_FILE_EXTENSION} file is required. Output file name and log file name will be generated based on input file name in the selected directory.", font=('Helvetica', 8, 'italic')) \ .grid(row=2, column=0, columnspan=3, padx=5, pady=2, sticky=tk.W) def _setup_processing_widgets(self, parent_frame): """Create and arrange widgets for main processing options using grid with multiple columns.""" # Configure columns to expand. More columns mean more horizontal space usage within the tab. parent_frame.columnconfigure(0, weight=1) parent_frame.columnconfigure(1, weight=1) parent_frame.columnconfigure(2, weight=1) # Added third column weight # Distribute widgets across columns. Each column is a conceptual group. # Column 0: Core processing flags # Column 1: Extraction and discontinuity flags # Column 2: Run control and date/data flags + options with values r0, r1, r2 = 0, 0, 0 # Row counters for each conceptual column group # Column 0 tk.Checkbutton(parent_frame, text="Analyze Only (/a)", variable=self.analyze_only_var, command=self._on_analyze_only_toggle).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 tk.Checkbutton(parent_frame, text="Don't Save Signal (/nosign)", variable=self.no_sign_var).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 tk.Checkbutton(parent_frame, text="Use FW Header (/hfw)", variable=self.use_fw_header_var).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 tk.Checkbutton(parent_frame, text="Fix DSP Geopos Bug (/$pbug)", variable=self.fix_dsp_bug_var).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 tk.Checkbutton(parent_frame, text="Sync Signal Flows w/ Header (/h)", variable=self.sync_signal_flows_var).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 # Column 1 tk.Checkbutton(parent_frame, text="Extract Only SW Data (/sw)", variable=self.extract_sw_only_var).grid(row=r1, column=1, sticky=tk.W, padx=2, pady=1); r1+=1 tk.Checkbutton(parent_frame, text="Extract Only DSP Data (/dsp)", variable=self.extract_dsp_only_var).grid(row=r1, column=1, sticky=tk.W, padx=2, pady=1); r1+=1 tk.Checkbutton(parent_frame, text="Stop on Discontinuity (/ss)", variable=self.stop_on_discontinuity_var).grid(row=r1, column=1, sticky=tk.W, padx=2, pady=1); r1+=1 # Column 2 tk.Checkbutton(parent_frame, text="Dry Run (/dryrun)", variable=self.dry_run_var).grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 tk.Checkbutton(parent_frame, text="Start Save from STBY (/z)", variable=self.start_from_stby_var).grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 tk.Checkbutton(parent_frame, text="Fix SATA File Date (/d)", variable=self.fix_sata_date_var).grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 tk.Checkbutton(parent_frame, text="Examine Some Data (/x)", variable=self.examine_data_var).grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 # Add options with values to the third column as well # Post Process (/p and /p=VALUE) post_process_frame = tk.Frame(parent_frame) # Use a frame to keep label and entry together post_process_frame.grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 tk.Checkbutton(post_process_frame, text="Post Process History (/p)", variable=self.post_process_var, command=self._on_post_process_toggle).pack(side=tk.LEFT, padx=2) self.post_process_level_label = tk.Label(post_process_frame, text="Level:") self.post_process_level_label.pack(side=tk.LEFT, padx=2) self.post_process_level_entry = tk.Entry(post_process_frame, textvariable=self.post_process_level_var, width=5, state=tk.DISABLED) self.post_process_level_entry.pack(side=tk.LEFT, padx=2) # Concatenate (/n=VALUE) concatenate_frame = tk.Frame(parent_frame) # Use a frame to keep label and entry together concatenate_frame.grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 tk.Label(concatenate_frame, text=f"Concatenate {BIN_FILE_EXTENSION} files (/n):").pack(side=tk.LEFT, padx=2) # Updated label tk.Entry(concatenate_frame, textvariable=self.concatenate_n_var, width=10).pack(side=tk.LEFT, padx=2) # Bind the concatenate_n_var to update the generated file paths when it changes self.concatenate_n_var.trace_add("write", lambda *args: self._update_generated_file_paths()) # Output Format (/f=VALUE) output_format_frame = tk.Frame(parent_frame) # Use a frame to keep label and entry together output_format_frame.grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 tk.Label(output_format_frame, text="Output Format (/f):").pack(side=tk.LEFT, padx=2) tk.Entry(output_format_frame, textvariable=self.output_format_var, width=15).pack(side=tk.LEFT, padx=2) def _setup_advanced_widgets(self, parent_frame): """Create and arrange widgets for advanced options using grid.""" parent_frame.columnconfigure(1, weight=1) # Make the entry column expandable r=0 # JSON Config File (/j) tk.Label(parent_frame, text="JSON Config File (/j):").grid(row=r, column=0, sticky=tk.W, padx=2, pady=1) tk.Entry(parent_frame, textvariable=self.json_config_file_var).grid(row=r, column=1, sticky=tk.EW, padx=2) # Removed fixed width tk.Button(parent_frame, text="...", command=lambda: self._browse_file_for_var(self.json_config_file_var, "Select JSON Config File")).grid(row=r, column=2, padx=2); r+=1 # g_reconvert Log File (/l) - This will be generated based on output dir and input file name tk.Label(parent_frame, text="g_reconvert Log File (/l):").grid(row=r, column=0, sticky=tk.W, padx=2, pady=1); r+=1 # Display the generated log file path, make it read-only self.g_reconvert_log_file_display = tk.Label(parent_frame, textvariable=self.g_reconvert_log_file_var, relief=tk.SUNKEN, anchor=tk.W, wraplength=600) self.g_reconvert_log_file_display.grid(row=r, column=1, sticky=tk.EW, padx=2, columnspan=2); # Span across remaining columns # Numeric params (kept in a single column for now, could split if needed) r=r+1 # Move to the next row after the log file display tk.Label(parent_frame, text="Max Batches (/m):").grid(row=r, column=0, sticky=tk.W, padx=2, pady=1) tk.Entry(parent_frame, textvariable=self.max_batches_var, width=10).grid(row=r, column=1, sticky=tk.W, padx=2); r+=1 tk.Label(parent_frame, text="PRI Number (/r):").grid(row=r, column=0, sticky=tk.W, padx=2, pady=1) tk.Entry(parent_frame, textvariable=self.pri_number_var, width=10).grid(row=r, column=1, sticky=tk.W, padx=2); r+=1 tk.Label(parent_frame, text="RBIN Number (/c):").grid(row=r, column=0, sticky=tk.W, padx=2, pady=1) tk.Entry(parent_frame, textvariable=self.rbin_number_var, width=10).grid(row=r, column=1, sticky=tk.W, padx=2); r+=1 def _setup_video_widgets(self, parent_frame): """Create and arrange widgets for video options using grid with multiple columns.""" parent_frame.columnconfigure(0, weight=1) parent_frame.columnconfigure(1, weight=1) parent_frame.columnconfigure(2, weight=1) # Added third column weight # Column 0: Video output/display options # Column 1: Video saving/framerate/subtitles # Column 2: GPS, HR, SAR related flags r0, r1, r2 = 0, 0, 0 # Row counters for each conceptual column group # Column 0 tk.Checkbutton(parent_frame, text="Output to Rec Results (/vout)", variable=self.video_out_to_rec_var).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 tk.Checkbutton(parent_frame, text="Show Video (/vshow)", variable=self.video_show_var).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 # Column 1 tk.Checkbutton(parent_frame, text="Save Video (/vsave)", variable=self.video_save_var).grid(row=r1, column=1, sticky=tk.W, padx=2, pady=1); r1+=1 tk.Checkbutton(parent_frame, text="Fix Frame Rate (/vframe)", variable=self.video_fix_framerate_var).grid(row=r1, column=1, sticky=tk.W, padx=2, pady=1); r1+=1 tk.Checkbutton(parent_frame, text="Add AVI Subtitles (/vst)", variable=self.video_add_subtitles_var).grid(row=r1, column=1, sticky=tk.W, padx=2, pady=1); r1+=1 # Column 2 tk.Checkbutton(parent_frame, text="GPS: Save Track (/gps)", variable=self.gps_save_track_var).grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 tk.Checkbutton(parent_frame, text="HR: Fix Debug Data (/fixhr)", variable=self.fix_hr_debug_var).grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 tk.Checkbutton(parent_frame, text="SAR: Save Only SAR (/sar)", variable=self.sar_save_only_var).grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 def _setup_debug_control_widgets(self, parent_frame): """Create and arrange widgets for debug/control options (// flags) using grid with multiple columns.""" parent_frame.columnconfigure(0, weight=1) parent_frame.columnconfigure(1, weight=1) parent_frame.columnconfigure(2, weight=1) # Added third column weight # Column 0: Verbosity flags # Column 1: Masking flags # Column 2: Overwrite flag r0, r1, r2 = 0, 0, 0 # Row counters for each conceptual column group # Column 0 tk.Checkbutton(parent_frame, text="Verbose Output (//v)", variable=self.verbose_var, command=lambda: self._on_verbosity_change("verbose")).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 tk.Checkbutton(parent_frame, text="Quiet Mode (//q)", variable=self.quiet_var, command=lambda: self._on_verbosity_change("quiet")).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 tk.Checkbutton(parent_frame, text="Debug Messages (//d)", variable=self.debug_messages_var).grid(row=r0, column=0, sticky=tk.W, padx=2, pady=1); r0+=1 # Column 1 tk.Checkbutton(parent_frame, text="Mask Warnings (//w)", variable=self.mask_warnings_var).grid(row=r1, column=1, sticky=tk.W, padx=2, pady=1); r1+=1 tk.Checkbutton(parent_frame, text="Mask Errors (//e)", variable=self.mask_errors_var).grid(row=r1, column=1, sticky=tk.W, padx=2, pady=1); r1+=1 # Column 2 tk.Checkbutton(parent_frame, text="Silently Overwrite Output (//o)", variable=self.silent_overwrite_var).grid(row=r2, column=2, sticky=tk.W, padx=2, pady=1); r2+=1 def _on_analyze_only_toggle(self): """Updates status bar based on analyze only checkbox state.""" if self.analyze_only_var.get(): self._update_status_bar("Analyze Only mode: Output file (/o) might not be required by g_reconvert.") else: self._update_status_bar("Output will be generated in the selected Output Directory.") # Update generated file paths when analyze only is toggled self._update_generated_file_paths() def _on_post_process_toggle(self): """Enables/disables the post process level entry based on checkbox state.""" state = tk.NORMAL if self.post_process_var.get() else tk.DISABLED # Check if widgets exist before trying to configure them if hasattr(self, 'post_process_level_entry') and self.post_process_level_entry is not None: self.post_process_level_entry.config(state=state) if hasattr(self, 'post_process_level_label') and self.post_process_level_label is not None: self.post_process_level_label.config(state=state) if state == tk.NORMAL: self._update_status_bar("Post-processing enabled. Specify level if needed (default 1).") else: self._update_status_bar("Post-processing disabled.") def _on_verbosity_change(self, changed_var=None): """Manages mutual exclusivity of verbose and quiet flags.""" # Only perform checks if a specific variable changed if changed_var == "verbose": if self.verbose_var.get() and self.quiet_var.get(): self.quiet_var.set(False) elif changed_var == "quiet": if self.quiet_var.get() and self.verbose_var.get(): self.verbose_var.set(False) # Update status or log based on final state if self.quiet_var.get(): self._update_status_bar("Quiet mode enabled (//q).") # If quiet is enabled, verbose, debug, warnings, and errors are typically suppressed # Based on g_reconvert's behavior, we might visually disable/uncheck others, # but the command list logic handles this. Just updating status is sufficient for now. elif self.verbose_var.get(): self._update_status_bar("Verbose output enabled (//v).") elif self.debug_messages_var.get(): # Check debug if neither quiet nor verbose is set self._update_status_bar("Debug messages enabled (//d).") elif self.mask_warnings_var.get() or self.mask_errors_var.get(): # Check masking if other verbosity flags are off status_parts = [] if self.mask_warnings_var.get(): status_parts.append("Warnings masked (//w).") if self.mask_errors_var.get(): status_parts.append("Errors masked (//e).") self._update_status_bar(" ".join(status_parts)) else: self._update_status_bar("Verbosity set to default.") def _browse_file_for_var(self, tk_var, title, filetypes=None): """Helper to open file dialog and set a StringVar.""" filepath = filedialog.askopenfilename(title=title, filetypes=filetypes) if filepath: tk_var.set(filepath) # Use os.path.basename for cleaner log message if path is long display_filepath = os.path.basename(filepath) if len(filepath) > 50 else filepath self._insert_output_text(f"{title.split(' ')[-2]} file selected: {display_filepath}\n", 'INFO') # Use last word before 'File' self._update_generated_file_paths() # Update generated paths if input file changes def _select_output_directory(self): """Open a dialog to select the output directory.""" # Use askdirectory to select a folder directory_path = filedialog.askdirectory( title="Select Output Directory" ) if directory_path: self.output_dir_var.set(directory_path) self._insert_output_text(f"Output directory set: {directory_path}\n", 'INFO') self._update_generated_file_paths() # Update generated file paths def _update_generated_file_paths(self): """Updates the display of the generated output file and log file paths.""" input_file = self.bin_file_var.get() output_dir = self.output_dir_var.get() if input_file and output_dir: base_name = os.path.splitext(os.path.basename(input_file))[0] # Use concatenate_n_var for the generated name concatenate_n_value = self.concatenate_n_var.get().strip() output_file_name = f"{base_name}-n{concatenate_n_value}.out" if concatenate_n_value else f"{base_name}.out" log_file_name = f"{base_name}-n{concatenate_n_value}.log" if concatenate_n_value else f"{base_name}.log" # Construct full paths self._generated_output_file_path = os.path.join(output_dir, output_file_name) self._generated_log_file_path = os.path.join(output_dir, log_file_name) # Update the StringVar for the log file path display label self.g_reconvert_log_file_var.set(self._generated_log_file_path) else: # Clear the displayed paths if input or output is missing self._generated_output_file_path = "" self._generated_log_file_path = "" self.g_reconvert_log_file_var.set("Generated log file path will appear here.") # Update the state of the "Go to Output Folder" button self._update_go_to_output_button_state() def _update_go_to_output_button_state(self): """Enables or disables the 'Go to Output Folder' button.""" output_dir = self.output_dir_var.get().strip() # Enable the button only if output_dir is not empty AND it's a valid directory path if output_dir and os.path.isdir(output_dir): if hasattr(self, 'go_to_output_button'): # Check if button exists self.go_to_output_button.config(state=tk.NORMAL) else: if hasattr(self, 'go_to_output_button'): # Check if button exists self.go_to_output_button.config(state=tk.DISABLED) def _open_output_folder(self): """Opens the selected output directory in the system's file explorer.""" output_dir = self.output_dir_var.get().strip() if not output_dir or not os.path.isdir(output_dir): messagebox.showwarning("Invalid Output Directory", "The specified output directory is empty or does not exist.") self._update_status_bar("Warning: Cannot open invalid output directory.") return try: # Use os.startfile on Windows if os.name == 'nt': os.startfile(output_dir) # Use subprocess for macOS and Linux elif sys.platform == 'darwin': # macOS subprocess.run(['open', output_dir], check=True) else: # Linux and other Unix-like subprocess.run(['xdg-open', output_dir], check=True) self._log_message_to_gui_log(f"Opened output directory: {output_dir}") self._update_status_bar(f"Opened output directory: {os.path.basename(output_dir)}") except FileNotFoundError: messagebox.showerror("Error", f"Could not find an application to open the directory.\nPlease open it manually: {output_dir}") self._log_message_to_gui_log(f"Error opening output directory with system default (app not found): {output_dir}") self._update_status_bar("Error opening output directory.") except Exception as e: messagebox.showerror("Error", f"Could not open output directory: {e}\nPath: {output_dir}") self._log_message_to_gui_log(f"Error opening output directory: {e}. Path: {output_dir}") self._update_status_bar("Error opening output directory.") def _select_bin_file(self): """Open a dialog to select the source binary file (binfile), filtered for .rec.""" self._browse_file_for_var(self.bin_file_var, f"Select Source Binary File ({BIN_FILE_EXTENSION})", filetypes=BIN_FILETYPES) def _setup_control_buttons(self, parent_frame): """Create and arrange control buttons within a frame using grid.""" # Configure grid for two columns to place buttons side-by-side parent_frame.columnconfigure(0, weight=1) # Column for Run button (will still center it) parent_frame.columnconfigure(1, weight=1) # Column for Go to Output button # Run button self.run_button = tk.Button(parent_frame, text="Run g_reconvert.exe", command=self.run_cpp_application, width=25, height=2, bg="#4CAF50", fg="white", font=('Helvetica', 10, 'bold')) # Use sticky='E' to push it to the right in its column (relative to the center if column weight > 0) # Or just use a fixed column and place it. Let's keep it simple and centered in its column for now. self.run_button.grid(row=0, column=0, padx=10, pady=10, sticky=tk.E) # Sticky East # Go to Output Folder button self.go_to_output_button = tk.Button(parent_frame, text="Go to Output Folder", command=self._open_output_folder, width=25, height=2, bg="#FFD700", fg="black", font=('Helvetica', 10, 'bold')) # Place it in the second column, sticky='W' to push it to the left self.go_to_output_button.grid(row=0, column=1, padx=10, pady=10, sticky=tk.W) # Ensure the button is initially disabled until a valid directory is selected self._update_go_to_output_button_state() def _setup_output_area(self, parent_frame): """Create the scrolled text widget for console output within a frame using grid.""" # Use grid for the scrolled text to make it fill the parent frame parent_frame.columnconfigure(0, weight=1) # Column should expand parent_frame.rowconfigure(0, weight=1) # Row should expand # Increased height for the output area, let it expand vertically # ScrolledText inherently provides its own scrollbars, so no need to link self.output_text = scrolledtext.ScrolledText(parent_frame, height=25, wrap=tk.WORD, state=tk.DISABLED, relief=tk.SUNKEN, font=('Consolas', 9)) # Stick to all sides (N, S, E, W) and expand self.output_text.grid(row=0, column=0, sticky=tk.NSEW) # Configure tags for colored output self.output_text.tag_config('INFO', foreground='black') self.output_text.tag_config('ERROR', foreground='red', font=('Consolas', 9, 'bold')) self.output_text.tag_config('SUCCESS', foreground='green') self.output_text.tag_config('CMD', foreground='blue', font=('Consolas', 9, 'italic')) self.output_text.tag_config('WARNING', foreground='orange') def _update_status_bar(self, message): """Updates the text in the status bar.""" self.status_bar.config(text=f"Status: {message}") # Force Tkinter to update the display immediately self.master_window.update_idletasks() def select_cpp_executable(self): """Open a dialog to select the g_reconvert.exe file and update the display.""" filepath = filedialog.askopenfilename( title="Select g_reconvert.exe", # Filter based on common executable extensions or no extension for Unix filetypes=(("Executable files", "*.exe"), ("All files", "*.*")) if os.name == 'nt' else (("Application", "*"),("All files", "*.*")) ) if filepath: self.cpp_executable_path_var.set(filepath) message = f"g_reconvert.exe path set to: {filepath}" self._insert_output_text(f"{message}\n", 'INFO') self._log_message_to_gui_log(message) self._update_status_bar("g_reconvert.exe path set.") # After setting the executable path manually, we should probably save it # to the default profile so it persists. self._save_as_default_profile() def run_cpp_application(self): """Prepare command list based on GUI state and run g_reconvert in a thread.""" if self.process_running: messagebox.showwarning("Busy", "g_reconvert.exe is already running.") return exe_path = self.cpp_executable_path_var.get() if not exe_path or not os.path.exists(exe_path): messagebox.showerror("Error", "g_reconvert.exe path is not set or invalid.\nPlease set it via File -> Set g_reconvert.exe Path.") self._update_status_bar("Error: g_reconvert.exe path not set.") return bin_file = self.bin_file_var.get() if not bin_file: messagebox.showerror("Error", f"Source Binary File ({BIN_FILE_EXTENSION}) is required.") self._update_status_bar(f"Error: Source Binary File ({BIN_FILE_EXTENSION}) required.") return if not os.path.exists(bin_file): messagebox.showerror("Error", f"Source Binary File not found: {bin_file}") self._update_status_bar(f"Error: Source Binary File not found: {os.path.basename(bin_file)}.") return output_dir = self.output_dir_var.get() is_analyze_only = self.analyze_only_var.get() # Check if output directory is specified when not in analyze only mode if not is_analyze_only and not output_dir: messagebox.showerror("Error", "Output Directory is required when not in Analyze Only mode.") self._update_status_bar("Error: Output Directory required.") return # Ensure generated paths are up to date before running self._update_generated_file_paths() # Get the generated paths full_output_file_path = self._generated_output_file_path full_log_file_path = self._generated_log_file_path # --- Assemble Command List --- command_list = [exe_path, bin_file] def add_if_true(var, flag): """Helper to add a flag if the BooleanVar is True.""" if var.get(): command_list.append(flag) def add_if_value(var, flag_template): """Helper to add a flag=value if StringVar is not empty.""" value = var.get().strip() # Use strip() to handle whitespace if value: # subprocess.Popen with list handles spaces in elements correctly. command_list.append(flag_template.format(value)) # --- Add parameters based on GUI state, matching batch file logic --- # Input file is already the second element in command_list # //b flag (assuming it means base output directory) if output_dir: # Add a trailing slash/backslash if not present, based on OS base_output_dir_arg = output_dir if not base_output_dir_arg.endswith(os.sep): base_output_dir_arg += os.sep command_list.append(f"//b={base_output_dir_arg}") # /l flag (log file) - Use the generated path if output_dir and full_log_file_path: # Only add if output directory is specified and path was generated command_list.append(f"/l={full_log_file_path}") # /n flag (concatenate) - Use the value from the GUI, check if it's not empty if self.concatenate_n_var.get().strip(): add_if_value(self.concatenate_n_var, "/n={}") # /p flag (post process) - Add if checked, with level if specified and enabled if self.post_process_var.get(): level = self.post_process_level_var.get().strip() if self.post_process_level_entry['state'] != tk.DISABLED and level: command_list.append(f"/p={level}") else: command_list.append("/p") # Video Options (matching batch file) add_if_true(self.video_show_var, "/vshow") add_if_true(self.video_save_var, "/vsave") add_if_true(self.gps_save_track_var, "/gps") # Add other video flags if their variables are True add_if_true(self.video_out_to_rec_var, "/vout") add_if_true(self.video_fix_framerate_var, "/vframe") add_if_true(self.fix_hr_debug_var, "/fixhr") add_if_true(self.sar_save_only_var, "/sar") add_if_true(self.video_add_subtitles_var, "/vst") # /o flag (output file) - Use the generated path if not analyze only and output dir specified if not is_analyze_only and output_dir and full_output_file_path: command_list.append(f"/o={full_output_file_path}") # //o flag (silent overwrite) - Matching batch file add_if_true(self.silent_overwrite_var, "//o") # Analyze only /a (often preferred last or near end by some CLIs) if is_analyze_only: command_list.append("/a") # Other Processing Options (from GUI, not explicitly in this batch example but keep) add_if_true(self.no_sign_var, "/nosign") add_if_true(self.use_fw_header_var, "/hfw") add_if_true(self.fix_dsp_bug_var, "/$pbug") add_if_true(self.sync_signal_flows_var, "/h") add_if_true(self.extract_sw_only_var, "/sw") add_if_true(self.extract_dsp_only_var, "/dsp") add_if_true(self.stop_on_discontinuity_var, "/ss") add_if_true(self.dry_run_var, "/dryrun") # Already added if matched batch, but keep for generality add_if_true(self.start_from_stby_var, "/z") add_if_true(self.fix_sata_date_var, "/d") add_if_true(self.examine_data_var, "/x") add_if_value(self.output_format_var, "/f={}") # Also from GUI, not batch # Advanced Options (from GUI, not explicitly in this batch example but keep) add_if_value(self.json_config_file_var, "/j={}") # /l already handled add_if_value(self.max_batches_var, "/m={}") add_if_value(self.pri_number_var, "/r={}") add_if_value(self.rbin_number_var, "/c={}") # Debug/Control Options (from GUI, not explicitly in this batch example but keep) # //q and //v handled with mutual exclusivity if self.quiet_var.get(): command_list.append("//q") elif self.verbose_var.get(): command_list.append("//v") add_if_true(self.debug_messages_var, "//d") add_if_true(self.mask_warnings_var, "//w") add_if_true(self.mask_errors_var, "//e") # //o already handled if matched batch # --- Execute --- self._insert_output_text("--- Starting g_reconvert.exe ---\n", 'INFO') # Create a string representation for display, quoting arguments with spaces cmd_str_parts = [] for c in command_list: # Quote if contains space and is not a simple flag or flag=value if ' ' in c and not c.startswith('/'): cmd_str_parts.append(f'"{c}"') # Handle cases like /p="value with spaces" or //b="path with spaces\" elif '=' in c: flag, value = c.split('=', 1) # Check if the value part itself contains spaces if ' ' in value: cmd_str_parts.append(f'{flag}="{value}"') else: cmd_str_parts.append(c) else: cmd_str_parts.append(c) cmd_str = ' '.join(cmd_str_parts) self._insert_output_text(f"Executing: {cmd_str}\n", 'CMD') self._log_message_to_gui_log(f"Executing: {cmd_str}") self._update_status_bar(f"Running g_reconvert.exe...") self.run_button.config(state=tk.DISABLED, bg="#cccccc") # Disable button while running self.go_to_output_button.config(state=tk.DISABLED) # Disable Go To button while running self.process_running = True # Run the core runner function in a separate thread # Pass the output_queue, masking flags, and the output directory for creation thread = threading.Thread(target=run_g_reconvert, args=(command_list, self.output_queue, self.mask_errors_var.get(), self.mask_warnings_var.get(), output_dir if not is_analyze_only else None)) # Pass output_dir if not analyze only thread.daemon = True # Allows main program to exit even if threads are still running thread.start() # Start polling the output queue # Check queue every 100 milliseconds (adjust as needed) self.master_window.after(OUTPUT_QUEUE_POLLING_INTERVAL, self._process_output_queue) def _process_output_queue(self): """ Checks the output queue for messages from the core runner thread and updates the GUI. This runs in the main GUI thread. """ # Process a limited number of messages at a time to avoid blocking the GUI MAX_MESSAGES_TO_PROCESS = 10 # Adjust this number as needed for responsiveness vs update frequency messages_processed = 0 try: # Process up to MAX_MESSAGES_TO_PROCESS from the queue while messages_processed < MAX_MESSAGES_TO_PROCESS: # Use get_nowait() to avoid blocking the GUI thread item = self.output_queue.get_nowait() if item is None: # End-of-process signal from the runner self._insert_output_text("--- g_reconvert.exe Finished ---\n", 'SUCCESS') self._log_message_to_gui_log("--- g_reconvert.exe Finished ---", include_timestamp=True) self.run_button.config(state=tk.NORMAL, bg="#4CAF50") # Re-enable button self.process_running = False self._update_status_bar("g_reconvert.exe finished. Ready.") # Re-enable Go To Output button if directory is valid self._update_go_to_output_button_state() return # Stop polling the `after` loop else: # It's a message dictionary {'text': ..., 'type': ...} self._insert_output_text(item['text'], item.get('type', 'INFO')) # Display in GUI # Decide whether to log the message to the GUI log based on its source or content # For now, log everything coming from the runner thread self._log_message_to_gui_log(item['text'].strip(), include_timestamp=False) # Log to file messages_processed += 1 # Increment counter except queue.Empty: # No new messages in queue, just continue polling if process is still running pass # If process is still marked as running, schedule the next check # Schedule the next check regardless of whether messages were processed this time if self.process_running: self.master_window.after(OUTPUT_QUEUE_POLLING_INTERVAL, self._process_output_queue) # If process_running is False, the 'None' signal would have caused the function to return def _insert_output_text(self, message, tag='INFO'): """Appends a message to the ScrolledText widget with an optional tag.""" self.output_text.config(state=tk.NORMAL) # Enable editing self.output_text.insert(tk.END, message, tag) # Insert message with tag self.output_text.see(tk.END) # Scroll to the end self.output_text.config(state=tk.DISABLED) # Disable editing def _log_message_to_gui_log(self, message, include_timestamp=True): """Appends a message to the GUI's internal log file.""" try: with open(self.gui_log_file_path, "a", encoding="utf-8") as log_file: if include_timestamp: timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_file.write(f"{timestamp} - {message}\n") else: log_file.write(f"{message}\n") # Message from process might already have timestamp or context except IOError as e: # If logging fails, display an error in the GUI console error_msg = f"GUI Log Error: Could not write to {self.gui_log_file_path}: {e}\n" # Avoid recursive calls to logging/output methods in case of file error # Temporarily enable, insert, then disable the text widget original_state = self.output_text.cget('state') self.output_text.config(state=tk.NORMAL) self.output_text.insert(tk.END, error_msg, 'ERROR') self.output_text.see(tk.END) self.output_text.config(state=original_state) # Modified save_profile to accept an optional filepath def save_profile(self, filepath=None): """Saves the current parameters as a launch profile.""" profile_data = { # Collect state from all Tkinter Variables, including cpp_executable_path "cpp_executable_path": self.cpp_executable_path_var.get(), "bin_file": self.bin_file_var.get(), "output_dir": self.output_dir_var.get(), # Save output directory "analyze_only": self.analyze_only_var.get(), "no_sign": self.no_sign_var.get(), "use_fw_header": self.use_fw_header_var.get(), "fix_dsp_bug": self.fix_dsp_bug_var.get(), "sync_signal_flows": self.sync_signal_flows_var.get(), "extract_sw_only": self.extract_sw_only_var.get(), "extract_dsp_only": self.extract_dsp_only_var.get(), "stop_on_discontinuity": self.stop_on_discontinuity_var.get(), "dry_run": self.dry_run_var.get(), "start_from_stby": self.start_from_stby_var.get(), "fix_sata_date": self.fix_sata_date_var.get(), "examine_data": self.examine_data_var.get(), "concatenate_n": self.concatenate_n_var.get(), "post_process": self.post_process_var.get(), "post_process_level": self.post_process_level_var.get(), "output_format": self.output_format_var.get(), "json_config_file": self.json_config_file_var.get(), # g_reconvert_log_file is generated, no need to save "max_batches": self.max_batches_var.get(), "pri_number": self.pri_number_var.get(), "rbin_number": self.rbin_number_var.get(), "video_out_to_rec": self.video_out_to_rec_var.get(), "video_show": self.video_show_var.get(), "video_save": self.video_save_var.get(), "video_fix_framerate": self.video_fix_framerate_var.get(), "gps_save_track": self.gps_save_track_var.get(), "fix_hr_debug": self.fix_hr_debug_var.get(), "sar_save_only": self.sar_save_only_var.get(), "video_add_subtitles": self.video_add_subtitles_var.get(), "verbose": self.verbose_var.get(), "debug_messages": self.debug_messages_var.get(), "quiet": self.quiet_var.get(), "mask_warnings": self.mask_warnings_var.get(), "mask_errors": self.mask_errors_var.get(), "silent_overwrite": self.silent_overwrite_var.get(), } # If filepath is not provided, open a save dialog if filepath is None: profile_path = filedialog.asksaveasfilename( defaultextension=PROFILE_EXTENSION, filetypes=PROFILE_FILETYPES, title="Save Launch Profile" ) else: # Use the provided filepath (for default profile) profile_path = filepath if profile_path: try: with open(profile_path, "w", encoding="utf-8") as f: json.dump(profile_data, f, indent=4) success_msg = f"Profile saved to {profile_path}" # Only show messagebox if not saving the default profile automatically if filepath is None: messagebox.showinfo("Profile Saved", success_msg) self._log_message_to_gui_log(success_msg) self._update_status_bar(f"Profile saved: {os.path.basename(profile_path)}") except IOError as e: error_msg = f"Failed to save profile: {e}" messagebox.showerror("Save Error", error_msg) self._log_message_to_gui_log(f"ERROR: {error_msg}") self._update_status_bar(f"Error saving profile.") # Modified load_profile to accept an optional filepath def load_profile(self, filepath=None): """Loads launch parameters from a profile file.""" # If filepath is not provided, open an open dialog if filepath is None: profile_path = filedialog.askopenfilename( filetypes=PROFILE_FILETYPES, title="Load Launch Profile" ) else: # Use the provided filepath (for default profile) profile_path = filepath if profile_path and os.path.exists(profile_path): try: with open(profile_path, "r", encoding="utf-8") as f: profile_data = json.load(f) # Helper to get value or default, works for all types def get_val(key, default): return profile_data.get(key, default) # Set state for all Tkinter Variables, including cpp_executable_path self.cpp_executable_path_var.set(get_val("cpp_executable_path", "")) self.bin_file_var.set(get_val("bin_file", "")) self.output_dir_var.set(get_val("output_dir", "")) # Load output directory self.analyze_only_var.set(get_val("analyze_only", False)) self.no_sign_var.set(get_val("no_sign", False)) self.use_fw_header_var.set(get_val("use_fw_header", False)) self.fix_dsp_bug_var.set(get_val("fix_dsp_bug", False)) self.sync_signal_flows_var.set(get_val("sync_signal_flows", False)) self.extract_sw_only_var.set(get_val("extract_sw_only", False)) self.extract_dsp_only_var.set(get_val("extract_dsp_only", False)) self.stop_on_discontinuity_var.set(get_val("stop_on_discontinuity", False)) self.dry_run_var.set(get_val("dry_run", False)) self.start_from_stby_var.set(get_val("start_from_stby", False)) self.fix_sata_date_var.set(get_val("fix_sata_date", False)) self.examine_data_var.set(get_val("examine_data", False)) self.concatenate_n_var.set(get_val("concatenate_n", "")) self.post_process_var.set(get_val("post_process", False)) self.post_process_level_var.set(get_val("post_process_level", "1")) self.output_format_var.set(get_val("output_format", "")) self.json_config_file_var.set(get_val("json_config_file", "")) # g_reconvert_log_file is generated, no need to load self.max_batches_var.set(get_val("max_batches", "")) self.pri_number_var.set(get_val("pri_number", "")) self.rbin_number_var.set(get_val("rbin_number", "")) self.video_out_to_rec_var.set(get_val("video_out_to_rec", False)) self.video_show_var.set(get_val("video_show", False)) self.video_save_var.set(get_val("video_save", False)) self.video_fix_framerate_var.set(get_val("video_fix_framerate", False)) self.gps_save_track_var.set(get_val("gps_save_track", False)) self.fix_hr_debug_var.set(get_val("fix_hr_debug", False)) self.sar_save_only_var.set(get_val("sar_save_only", False)) self.video_add_subtitles_var.set(get_val("video_add_subtitles", False)) self.verbose_var.set(get_val("verbose", False)) self.debug_messages_var.set(get_val("debug_messages", False)) self.quiet_var.set(get_val("quiet", False)) self.mask_warnings_var.set(get_val("mask_warnings", False)) self.mask_errors_var.set(get_val("mask_errors", False)) self.silent_overwrite_var.set(get_val("silent_overwrite", False)) # Trigger UI updates based on loaded state for dynamic widgets and generated paths self._on_analyze_only_toggle() self._on_post_process_toggle() self._on_verbosity_change(changed_var=None) # Update generated paths *after* setting bin_file and output_dir vars self._update_generated_file_paths() success_msg = f"Profile loaded from {profile_path}" # Avoid showing info message box on startup for default profile if it loaded successfully if filepath is None: # Only show messagebox if initiated by user via menu messagebox.showinfo("Profile Loaded", success_msg) self._insert_output_text(f"{success_msg}\n", 'INFO') if self.cpp_executable_path_var.get(): self._insert_output_text(f"g_reconvert.exe path from profile: {self.cpp_executable_path_var.get()}\n", 'INFO') if self.bin_file_var.get(): self._insert_output_text(f"Input file from profile: {self.bin_file_var.get()}\n", 'INFO') if self.output_dir_var.get(): self._insert_output_text(f"Output directory from profile: {self.output_dir_var.get()}\n", 'INFO') self._log_message_to_gui_log(success_msg) self._update_status_bar(f"Profile loaded: {os.path.basename(profile_path)}") except (IOError, json.JSONDecodeError, KeyError) as e: error_msg = f"Failed to load profile from {profile_path}: {e}" # Only show error messagebox if initiated by user via menu, not on startup for default if filepath is None: messagebox.showerror("Load Error", error_msg) else: # For default profile load errors on startup, log to GUI console self._insert_output_text(f"ERROR: {error_msg}\n", 'ERROR') self._log_message_to_gui_log(f"ERROR: {error_msg}") self._update_status_bar(f"Error loading profile.") elif filepath is None: # If user cancelled the load dialog self._update_status_bar("Profile load cancelled.") def open_gui_log_file(self): """Opens the GUI's internal log file with the default system application.""" log_path_to_open = os.path.abspath(self.gui_log_file_path) try: if not os.path.exists(log_path_to_open): # Log this event before showing the message box self._log_message_to_gui_log(f"GUI Log file does not exist yet: {log_path_to_open}") messagebox.showinfo("GUI Log File", "GUI Log file does not exist yet. It will be created upon first execution or GUI message.") return # Use os.startfile on Windows, subprocess for others if os.name == 'nt': os.startfile(log_path_to_open) elif sys.platform == 'darwin': # macOS subprocess.run(['open', log_path_to_open], check=True) else: # Linux and other Unix-like subprocess.run(['xdg-open', log_path_to_open], check=True) self._log_message_to_gui_log(f"Attempted to open GUI log file: {log_path_to_open}") except FileNotFoundError: messagebox.showerror("Error", f"Could not find an application to open the log file.\nPlease open it manually: {log_path_to_open}") self._log_message_to_gui_log(f"Error opening GUI log file with system default: {log_path_to_open}") except Exception as e: messagebox.showerror("Error", f"Could not open GUI log file: {e}\nPath: {log_path_to_open}") self._log_message_to_gui_log(f"Error opening GUI log file: {e}. Path: {log_path_to_open}") def _confirm_exit(self): """Confirms if the user wants to quit the application, checking for running process.""" if self.process_running: if messagebox.askyesno("Exit Confirmation", "g_reconvert.exe might still be running. Are you sure you want to exit? This may terminate the process abruptly." "\nConsider stopping the process first if possible."): # Added suggestion to stop self._log_message_to_gui_log("GUI Application Shutting Down (process was running).", include_timestamp=True) # In a real application, you might add logic here to try and terminate # the subprocess gracefully or forcefully before destroying the window. self.master_window.destroy() else: return # Do not exit elif messagebox.askyesno("Exit Confirmation", "Are you sure you want to exit?"): self._log_message_to_gui_log("GUI Application Shutting Down.", include_timestamp=True) self.master_window.destroy() # The if __name__ == "__main__": block is moved to gui_g_converter/__main__.py