SXXXXXXX_PyInstallerGUIWrapper/pyinstallerguiwrapper/gui/options_manager.py

422 lines
19 KiB
Python

# -*- 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
wexpect_check = ttk.Checkbutton(
self.options_frame,
text="Build and include 'wexpect' console helper (Windows only)",
variable=self.build_wexpect_helper_var,
command=self._on_wexpect_helper_option_changed,
)
wexpect_check.grid(row=current_row, column=0, columnspan=4, padx=5, pady=2, sticky="w")
if sys.platform != "win32":
wexpect_check.config(state="disabled")
self.build_wexpect_helper_var.set(False)
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")