1138 lines
61 KiB
Python
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 |