# -*- coding: utf-8 -*- """ Manages the core PyInstaller options and their associated GUI widgets. """ import os import sys import pathlib import tkinter as tk from tkinter import filedialog, ttk, messagebox from typing import Optional, Callable, Dict, Any, Tuple, List from pyinstallerguiwrapper import config # Importa la configurazione globale # Definizione di un logger di default per OptionsManager se non ne viene passato uno def _default_logger(message: str, level: str = "INFO") -> None: """Default logger for OptionsManager if none is provided.""" # print(f"[{level}] OptionsManager: {message}") pass class OptionsManager: """ Manages the creation, display, and retrieval of core PyInstaller build options (like app name, icon, onefile/onedir, windowed/console, UPX, clean build, and wexpect helper). """ def __init__( self, parent_gui: tk.Tk, # Riferimento alla finestra principale per i dialoghi options_frame: ttk.Frame, # Il frame (es. basic_options_frame) dove verranno posizionati i widget icon_path_var: tk.StringVar, # StringVar per il path dell'icona (esterno, gestito anche da ProjectManager) app_name_var: tk.StringVar, # StringVar per il nome dell'app is_onefile_var: tk.BooleanVar, # BooleanVar per onefile/onedir is_windowed_var: tk.BooleanVar, # BooleanVar per windowed/console use_upx_var: tk.BooleanVar, # BooleanVar per UPX clean_output_dir_var: tk.BooleanVar, # BooleanVar per clean output dir build_wexpect_helper_var: tk.BooleanVar, # BooleanVar per wexpect helper derived_icon_path: Optional[str], # Path dell'icona derivata da ProjectManager logger_func: Optional[Callable[..., None]] = None, ): """ Initializes the OptionsManager with Tkinter variables and widget references. """ self.parent_gui = parent_gui self.options_frame = options_frame self.icon_path_var = icon_path_var self.app_name_var = app_name_var self.is_onefile_var = is_onefile_var self.is_windowed_var = is_windowed_var self.use_upx_var = use_upx_var self.clean_output_dir_var = clean_output_dir_var self.build_wexpect_helper_var = ( build_wexpect_helper_var # Correzione errore di battitura ) self.derived_icon_path = derived_icon_path # Viene da ProjectManager self.logger = logger_func if logger_func else _default_logger # Widgets (references to be stored after creation) self.icon_entry: Optional[ttk.Entry] = None self.derived_icon_label_val: Optional[ttk.Label] = ( None # Per aggiornare il testo dell'icona derivata ) self._create_widgets() # Crea e posiziona i widget nel frame passato def _log(self, message: str, level: str = "INFO") -> None: """Helper for logging messages with a consistent prefix.""" try: self.logger(f"[OptionsManager] {message}", level=level) except TypeError: self.logger(f"[{level}][OptionsManager] {message}") def _create_widgets(self) -> None: """Creates and lays out the widgets for the core PyInstaller options.""" # Main layout for options_frame (basic_options_frame) self.options_frame.columnconfigure(1, weight=1) # Make entry expand current_row = 0 ttk.Label(self.options_frame, text="App Name (from Spec/GUI):").grid( row=current_row, column=0, padx=5, pady=5, sticky="w" ) ttk.Entry(self.options_frame, textvariable=self.app_name_var).grid( row=current_row, column=1, columnspan=3, padx=5, pady=5, sticky="ew" ) current_row += 1 ttk.Label(self.options_frame, text="Icon File (from Spec/GUI):").grid( row=current_row, column=0, padx=5, pady=5, sticky="w" ) self.icon_entry = ttk.Entry( self.options_frame, textvariable=self.icon_path_var, state="readonly", width=70, ) self.icon_entry.grid(row=current_row, column=1, padx=5, pady=5, sticky="ew") ttk.Button( self.options_frame, text="Browse...", command=self._select_icon_file ).grid(row=current_row, column=2, padx=(0, 2), pady=5) ttk.Button( self.options_frame, text="Clear", command=self._clear_icon_file ).grid(row=current_row, column=3, padx=(2, 5), pady=5) current_row += 1 ttk.Checkbutton( self.options_frame, text="Single File (One Executable)", variable=self.is_onefile_var, ).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w") current_row += 1 ttk.Checkbutton( self.options_frame, text="Windowed (No Console)", variable=self.is_windowed_var, ).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w") current_row += 1 ttk.Checkbutton( self.options_frame, text="Use UPX (if available in PATH)", variable=self.use_upx_var, ).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w") current_row += 1 ttk.Checkbutton( self.options_frame, text="Clean output directory before build (deletes existing '_dist' content)", variable=self.clean_output_dir_var, ).grid(row=current_row, column=0, columnspan=4, padx=5, pady=5, sticky="w") current_row += 1 # Wexpect Helper Option ttk.Checkbutton( self.options_frame, text="Build and include 'wexpect' console helper (for frozen apps using wexpect)", variable=self.build_wexpect_helper_var, command=self._on_wexpect_helper_option_changed, ).grid(row=current_row, column=0, columnspan=4, padx=5, pady=2, sticky="w") current_row += 1 ttk.Label( self.options_frame, text="Needed if the target app uses 'wexpect' and fails to start subprocesses when frozen.\n" "The main app should use an UNMODIFIED 'wexpect/host.py'.", foreground="grey", font=("TkDefaultFont", 8), ).grid(row=current_row, column=0, columnspan=4, padx=7, pady=(0, 5), sticky="w") current_row += 1 def _select_icon_file(self) -> None: """Opens a file dialog to select the application icon.""" # This needs the project directory path from ProjectManager. # We need a way to get this or pass it. For now, let's assume it's set in main_window. # Or, we can expose a method in main_window to get the project_dir. # Let's pass `project_directory_getter: Callable[[], str]` to init. # Per semplificare la referenza, ora `parent_gui` ha `project_manager` project_dir_str = self.parent_gui.project_manager.project_directory_path.get() initial_dir = ( project_dir_str if project_dir_str and os.path.isdir(project_dir_str) else None ) # Determine file types based on OS filetypes, default_ext = self._get_icon_file_types_by_platform() file_path = filedialog.askopenfilename( title="Select Application Icon (Overrides Spec)", filetypes=filetypes, defaultextension=default_ext, initialdir=initial_dir, parent=self.parent_gui, ) if file_path: self.icon_path_var.set(file_path) self._log(f"Icon selected manually: {file_path}", level="INFO") def _clear_icon_file(self) -> None: """Clears the selected icon file path.""" self.icon_path_var.set("") self._log("Icon selection cleared by user.", level="INFO") # Questo label `derived_icon_label_val` deve essere passato da main_window se vogliamo aggiornarlo qui. # Altrimenti, main_window dovrà riaggiornarlo dopo che la variabile è stata pulita. # Per ora, è meglio che main_window gestisca l'aggiornamento dei suoi label derivati. # Rimuovo l'accesso diretto a `self.derived_icon_label_val` da qui, perché non è un attributo di OptionsManager. # main_window si occuperà di chiamare ProjectManager per aggiornare i label. # self.derived_icon_label_val.config(text="Not found (optional)", foreground="grey") def _get_icon_file_types_by_platform(self) -> Tuple[List[Tuple[str, str]], str]: """Helper to get icon file types based on the current platform.""" if sys.platform == "win32": return config.ICON_FILE_TYPES_WINDOWS, config.ICON_DEFAULT_EXT_WINDOWS elif sys.platform == "darwin": return config.ICON_FILE_TYPES_MACOS, config.ICON_DEFAULT_EXT_MACOS else: # Linux, etc. return config.ICON_FILE_TYPES_LINUX, config.ICON_DEFAULT_EXT_LINUX def _on_wexpect_helper_option_changed(self) -> None: """Callback when the wexpect helper checkbox state changes.""" if self.build_wexpect_helper_var.get(): self._log( "Wexpect console helper build ENABLED. This will run an additional PyInstaller build for the helper.", level="INFO", ) messagebox.showinfo( "Wexpect Helper Info", "Building the 'wexpect console helper' is an advanced option.\n" "It's needed if your target application uses 'wexpect' and fails to start subprocesses when frozen by PyInstaller.\n" "Ensure the target application uses an UNMODIFIED 'wexpect/host.py'.\n" "The helper will be named 'wexpect.exe' and placed in a 'wexpect' subdirectory of your main application's output (_dist/wexpect/wexpect.exe for onedir, or _MEIPASS/wexpect/wexpect.exe for onefile).", parent=self.parent_gui, ) else: self._log("Wexpect console helper build DISABLED.", level="INFO") # MODIFICHE INIZIO: Nuovo metodo per aggiornare il percorso dell'icona derivata def update_derived_icon_path(self, new_path: Optional[str]) -> None: """Updates the internal derived icon path, typically from ProjectManager.""" self.derived_icon_path = new_path self._log(f"Internal derived icon path updated to: {new_path}", level="DEBUG") # MODIFICHE FINE # MODIFICHE INIZIO: Nuovo metodo per resettare le opzioni ai valori predefiniti def reset_to_defaults( self, derive_name: bool = True, project_root_name: Optional[str] = None, derived_icon_path: Optional[str] = None, ) -> None: """ Resets all Tkinter variables managed by this class to their default values. Args: derive_name (bool): If True, attempts to derive app name from project_root_name. project_root_name (str, optional): The name of the project root, used if derive_name is True. derived_icon_path (str, optional): The automatically derived icon path from ProjectManager. """ self._log("Resetting OptionsManager variables to defaults.", level="INFO") # Reset app name if derive_name and project_root_name: self.app_name_var.set(project_root_name) self._log( f" App Name reset to derived project name: {project_root_name}", level="DEBUG", ) else: self.app_name_var.set( config.DEFAULT_SPEC_OPTIONS.get("name", "") ) # Default empty string self._log( " App Name reset to empty string (no derivation).", level="DEBUG" ) # Reset icon path # Usiamo il derived_icon_path passato per decidere se auto-popolare l'icona if derived_icon_path and pathlib.Path(derived_icon_path).is_file(): self.icon_path_var.set(derived_icon_path) self._log( f" Icon path reset to derived: {derived_icon_path}", level="DEBUG" ) else: self.icon_path_var.set("") # Clear if no derived icon or it doesn't exist self._log(" Icon path cleared.", level="DEBUG") # Reset boolean options from config defaults self.is_onefile_var.set(config.DEFAULT_SPEC_OPTIONS["onefile"]) self.is_windowed_var.set(config.DEFAULT_SPEC_OPTIONS["windowed"]) self.use_upx_var.set(config.DEFAULT_SPEC_OPTIONS["use_upx"]) self.clean_output_dir_var.set(config.DEFAULT_SPEC_OPTIONS["clean_build"]) self.build_wexpect_helper_var.set( False ) # wexpect helper default should be False, as it's an advanced option self._log("OptionsManager variables reset.", level="INFO") # MODIFICHE FINE def get_options_for_spec( self, project_root_path_obj: pathlib.Path, derived_main_script_path: Optional[str], derived_source_dir_path: Optional[pathlib.Path], ) -> Dict[str, Any]: """ Collects the current GUI options from this manager into a dictionary suitable for .spec file generation. Converts paths to be relative to project root. """ # App Name app_name = self.app_name_var.get() if ( not app_name and self.parent_gui.project_manager.project_root_name ): # Fallback to project root name app_name = self.parent_gui.project_manager.project_root_name # Icon Path (relative to project root) icon_rel_path_for_spec: Optional[str] = None icon_gui_path_str = self.icon_path_var.get() if icon_gui_path_str and pathlib.Path(icon_gui_path_str).is_file(): try: icon_rel_path_for_spec = os.path.relpath( icon_gui_path_str, str(project_root_path_obj) ) # Usa project_root_path_obj passato except ValueError: self._log( f"Cannot make icon path '{icon_gui_path_str}' relative to project root. Using absolute.", level="WARNING", ) icon_rel_path_for_spec = ( icon_gui_path_str # Use absolute if relative fails ) # Script Path (relative to project root) - This is mostly handled by ProjectManager now # but needed for completeness here for the spec script_rel_path: Optional[str] = None if ( derived_main_script_path and pathlib.Path(derived_main_script_path).is_file() ): try: script_rel_path = os.path.relpath( derived_main_script_path, str(project_root_path_obj) ) # Usa project_root_path_obj passato except ValueError: self._log( f"Cannot make main script path '{derived_main_script_path}' relative to project root. Using absolute.", level="WARNING", ) script_rel_path = derived_main_script_path # Pathex (source directory relative to project root) source_dir_rel_path: str = "." # Default to project root for pathex if derived_source_dir_path and derived_source_dir_path.is_dir(): try: source_dir_rel_path = os.path.relpath( str(derived_source_dir_path), str(project_root_path_obj) ) # Usa project_root_path_obj passato except ValueError: self._log( f"Cannot make source dir path '{derived_source_dir_path}' relative. Using absolute.", level="WARNING", ) source_dir_rel_path = str(derived_source_dir_path) return { "app_name": app_name, "icon_rel_path": icon_rel_path_for_spec, "is_onefile": self.is_onefile_var.get(), "is_windowed": self.is_windowed_var.get(), "use_upx": self.use_upx_var.get(), "clean_output_dir": self.clean_output_dir_var.get(), # Not a spec option, but needed for build orchestration "build_wexect_helper": self.build_wexpect_helper_var.get(), # Not a spec option, but needed for build orchestration # These are for spec generation, might be redundant with SpecTransformer's defaults # but collected for clarity. SpecTransformer will use its own internal logic. "analysis_scripts": [script_rel_path] if script_rel_path else [], "analysis_pathex": [source_dir_rel_path] if source_dir_rel_path else ["."], "pyz_cipher_var_name": "block_cipher", "a_var_name": "a", "pyz_var_name": "pyz", } def load_options_from_parsed_spec( self, parsed_options: Dict[str, Any], project_root_dir_str: str ) -> None: """ Updates the Tkinter variables with values parsed from an existing .spec file. """ self._log("Updating GUI from loaded/parsed spec options.", level="INFO") if "name" in parsed_options and parsed_options["name"] is not None: self.app_name_var.set(parsed_options["name"]) self._log(f" App Name from spec: {parsed_options['name']}", level="DEBUG") if "icon" in parsed_options and parsed_options["icon"] is not None: icon_spec_path_str = parsed_options["icon"] abs_icon_path = pathlib.Path(project_root_dir_str) / icon_spec_path_str if abs_icon_path.exists(): self.icon_path_var.set(str(abs_icon_path)) self._log( f" Resolved relative spec icon path: '{icon_spec_path_str}' -> '{abs_icon_path}'", level="DEBUG", ) else: self._log( f" Icon from spec ('{abs_icon_path}') not found. GUI icon field unchanged.", level="WARNING", ) if "onefile" in parsed_options and isinstance( parsed_options.get("onefile"), bool ): self.is_onefile_var.set(parsed_options["onefile"]) self._log( f" OneFile from spec: {parsed_options['onefile']}", level="DEBUG" ) if "windowed" in parsed_options and isinstance( parsed_options.get("windowed"), bool ): self.is_windowed_var.set(parsed_options["windowed"]) self._log( f" Windowed (no console) from spec: {parsed_options['windowed']}", level="DEBUG", ) if "upx" in parsed_options and isinstance(parsed_options.get("upx"), bool): self.use_upx_var.set(parsed_options["upx"]) self._log(f" Use UPX from spec: {parsed_options['upx']}", level="DEBUG") # clean_output_dir and build_wexect_helper are GUI-only options, not loaded from spec. self._log("Core GUI options updated from spec.", level="INFO")