diff --git a/.gitignore b/.gitignore index 587c1b5..a129a78 100644 --- a/.gitignore +++ b/.gitignore @@ -149,4 +149,7 @@ dmypy.json *.swp *~ *.txt -*.txt \ No newline at end of file + + +_dist/ +_build/ \ No newline at end of file diff --git a/GUI_g_reconverter.ico b/GUI_g_reconverter.ico index e69de29..327b4c4 100644 Binary files a/GUI_g_reconverter.ico and b/GUI_g_reconverter.ico differ diff --git a/gui_g_converter.spec b/gui_g_converter.spec deleted file mode 100644 index 9597beb..0000000 --- a/gui_g_converter.spec +++ /dev/null @@ -1,39 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - -a = Analysis( - ['gui_g_converter/__main__.py'], - pathex=['.'], - binaries=[], - # Usa project_icon_filename nella sezione datas - datas=[('GUI_g_converter.ico', '.')], - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False -) -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='GUI_g_converter', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - # Usa project_icon_filename per l'opzione icon - icon='GUI_g_converter.ico' -) diff --git a/gui_g_reconverter.spec b/gui_g_reconverter.spec new file mode 100644 index 0000000..206683e --- /dev/null +++ b/gui_g_reconverter.spec @@ -0,0 +1,46 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + +import os +a = Analysis(scripts=['gui_g_reconverter\\__main__.py'], + pathex=['gui_g_reconverter'], + binaries=[], + datas=[('GUI_g_reconverter.ico', '.')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=None, + noarchive=False) + +pyz = PYZ(a.pure, a.zipped_data, cipher=None) + +exe = EXE(pyz, + a.scripts, + [], # Binaries/Datas usually handled by Analysis/COLLECT + exclude_binaries=True, # Let COLLECT handle binaries in one-dir + name='GUI_g_converter', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, # Use UPX based on config + runtime_tmpdir=None, + console=False, # Set console based on GUI checkbox + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='GUI_g_reconverter.ico') + +coll = COLLECT(exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, # Match UPX setting + upx_exclude=[], + name='GUI_g_converter') diff --git a/gui_g_reconverter/__main__.py b/gui_g_reconverter/__main__.py index 992ec0a..eaf8611 100644 --- a/gui_g_reconverter/__main__.py +++ b/gui_g_reconverter/__main__.py @@ -10,7 +10,7 @@ import sys # Import the main GUI class from the gui subpackage -from .gui.application_gui import CppConverterGUI +from gui_g_reconverter.gui.application_gui import CppConverterGUI if __name__ == "__main__": root = tk.Tk() diff --git a/gui_g_reconverter/_version.py b/gui_g_reconverter/_version.py new file mode 100644 index 0000000..014cfea --- /dev/null +++ b/gui_g_reconverter/_version.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# File generated by PyInstaller GUI Wrapper. DO NOT EDIT MANUALLY. +# Contains build-time information scraped from Git (if available) +# and a helper function to format version strings. + + +import re + +# --- Version Data (Generated) --- +# This section is automatically generated by the build process. +__version__ = "v.0.0.0.1-2-g537930d-dirty" +GIT_COMMIT_HASH = "537930d65d7e7733d5b39df1e491b58cfeff5126" +GIT_BRANCH = "master" +BUILD_TIMESTAMP = "2025-05-12T12:53:06Z" +IS_GIT_REPO = True + +# --- Default Values (for comparison or fallback) --- +DEFAULT_VERSION = "0.0.0+unknown" +DEFAULT_COMMIT = "Unknown" +DEFAULT_BRANCH = "Unknown" + +# --- Helper Function --- +def get_version_string(format_string=None): + """ + Returns a formatted string based on the build version information. + + Args: + format_string (str, optional): A format string using placeholders. + Defaults to "{{version}} ({{branch}}/{{commit_short}})" if None. + Placeholders: + {{version}}: Full version string (e.g., 'v1.0.0-5-gabcdef-dirty') + {{tag}}: Clean tag part if exists (e.g., 'v1.0.0'), else DEFAULT_VERSION. + {{commit}}: Full Git commit hash. + {{commit_short}}: Short Git commit hash (7 chars). + {{branch}}: Git branch name. + {{dirty}}: '-dirty' if the repo was dirty, empty otherwise. + {{timestamp}}: Full build timestamp (ISO 8601 UTC). + {{timestamp_short}}: Build date only (YYYY-MM-DD). + {{is_git}}: 'Git' if IS_GIT_REPO is True, 'Unknown' otherwise. + + Returns: + str: The formatted version string, or an error message if formatting fails. + """ + if format_string is None: + format_string = "{version} ({branch}/{commit_short})" # Sensible default + + replacements = {} + try: + # Prepare data dictionary for substitution + replacements['version'] = __version__ if __version__ else DEFAULT_VERSION + replacements['commit'] = GIT_COMMIT_HASH if GIT_COMMIT_HASH else DEFAULT_COMMIT + replacements['commit_short'] = GIT_COMMIT_HASH[:7] if GIT_COMMIT_HASH and len(GIT_COMMIT_HASH) >= 7 else DEFAULT_COMMIT + replacements['branch'] = GIT_BRANCH if GIT_BRANCH else DEFAULT_BRANCH + replacements['timestamp'] = BUILD_TIMESTAMP if BUILD_TIMESTAMP else "Unknown" + replacements['timestamp_short'] = BUILD_TIMESTAMP.split('T')[0] if BUILD_TIMESTAMP and 'T' in BUILD_TIMESTAMP else "Unknown" + replacements['is_git'] = "Git" if IS_GIT_REPO else "Unknown" + replacements['dirty'] = "-dirty" if __version__ and __version__.endswith('-dirty') else "" + + # Extract clean tag using regex (handles versions like v1.0.0, 1.0.0) + tag = DEFAULT_VERSION + if __version__ and IS_GIT_REPO: + # Match optional 'v' prefix, then major.minor.patch + match = re.match(r'^(v?([0-9]+)\.([0-9]+)\.([0-9]+))', __version__) + if match: + tag = match.group(1) # Get the full tag (e.g., 'v1.0.0') + replacements['tag'] = tag + + # Perform substitution using regex to find placeholders {placeholder} + output_string = format_string + # Iterate through placeholders and replace them in the format string + for placeholder, value in replacements.items(): + # Compile regex pattern for {placeholder}, allowing for whitespace inside braces + pattern = re.compile(r'{\s*' + re.escape(placeholder) + r'\s*}') + # Substitute found patterns with the corresponding string value + output_string = pattern.sub(str(value), output_string) + + # Optional: Check if any placeholders remain unsubstituted (could indicate typo) + if re.search(r'{\s*[\w_]+\s*}', output_string): + # You might want to log this or handle it, for now, we return the string as is + # print(f"Warning: Unsubstituted placeholders remain in version string: {output_string}") + pass + + return output_string + + except Exception as e: + # Return a simple error message in case of unexpected formatting issues + # Avoid printing directly from this generated function + return f"[Formatting Error: {e}]" + + diff --git a/gui_g_reconverter/core/core.py b/gui_g_reconverter/core/core.py deleted file mode 100644 index e69de29..0000000 diff --git a/gui_g_reconverter/gui/application_gui.py b/gui_g_reconverter/gui/application_gui.py index 4076c4a..9710611 100644 --- a/gui_g_reconverter/gui/application_gui.py +++ b/gui_g_reconverter/gui/application_gui.py @@ -1,3 +1,4 @@ + import tkinter as tk from tkinter import scrolledtext, filedialog, messagebox, ttk import subprocess @@ -9,7 +10,7 @@ import datetime import sys # Needed for sys.platform in open_gui_log_file # Import the runner function from the core module -from ..core.g_reconvert_runner import run_g_reconvert +from gui_g_reconverter.core.g_reconvert_runner import run_g_reconvert # --- Constants --- DEFAULT_GUI_LOG_FILE = "gui_execution_log.txt" @@ -22,6 +23,28 @@ DEFAULT_PROFILE_FILE = "default_launch_profile.json" 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: """ @@ -37,7 +60,7 @@ class CppConverterGUI: master_window: The main Tkinter window. """ self.master_window = master_window - self.master_window.title("g_reconvert.exe Interface") + 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 @@ -129,6 +152,11 @@ class CppConverterGUI: # 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.""" @@ -507,6 +535,50 @@ class CppConverterGUI: 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.""" @@ -515,12 +587,23 @@ class CppConverterGUI: def _setup_control_buttons(self, parent_frame): """Create and arrange control buttons within a frame using grid.""" - # Use grid for centering the button within the frame - parent_frame.columnconfigure(0, weight=1) # Make the column expandable to center + # 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 centered + # 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')) - self.run_button.grid(row=0, column=0, padx=10, pady=10) + # 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): @@ -736,6 +819,7 @@ class CppConverterGUI: 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 @@ -750,7 +834,7 @@ class CppConverterGUI: # Start polling the output queue # Check queue every 100 milliseconds (adjust as needed) - self.master_window.after(100, self._process_output_queue) + self.master_window.after(OUTPUT_QUEUE_POLLING_INTERVAL, self._process_output_queue) def _process_output_queue(self): @@ -758,8 +842,13 @@ class CppConverterGUI: 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: - while True: # Process all messages currently in the queue + # 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() @@ -769,6 +858,8 @@ class CppConverterGUI: 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': ...} @@ -776,14 +867,18 @@ class CppConverterGUI: # 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(100, self._process_output_queue) + 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'):