SXXXXXXX_GUI_g_reconverter/gui_g_reconverter/gui/application_gui.py
VALLONGOL 1224daa929 Chore: Stop tracking files based on .gitignore update.
Untracked files matching the following rules:
- Rule "!.vscode/launch.json": 1 file
2025-05-12 14:56:09 +02:00

1138 lines
61 KiB
Python

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