# File: pyinstallerguiwrapper/gui/main_window.py # -*- coding: utf-8 -*- """ Main GUI module for the PyInstaller Wrapper. Orchestrates the application flow and integrates various UI components. """ import tkinter as tk from tkinter import ttk, messagebox import tkinter.scrolledtext import queue import pathlib import sys import traceback from typing import Optional, List, Any, Dict, Callable # Package imports from .project_selector import ProjectManager from .options_manager import OptionsManager from .output_logger import OutputLogger from .data_files_manager import DataFilesManager from pyinstallerguiwrapper.build.version_manager import VersionManager from pyinstallerguiwrapper.build.build_orchestrator import BuildOrchestrator from pyinstallerguiwrapper import config from pyinstallerguiwrapper import spec_parser try: from pyinstallerguiwrapper 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: WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" WRAPPER_BUILD_INFO = "Wrapper build time unknown" class PyInstallerGUI(tk.Tk): def __init__(self): super().__init__() self.title(f"PyInstaller GUI Wrapper - {WRAPPER_APP_VERSION_STRING}") self.project_directory_path = tk.StringVar() self.icon_path = tk.StringVar() self.app_name = tk.StringVar() self.is_onefile = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['onefile']) self.is_windowed = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['windowed']) self.log_level = tk.StringVar(value=config.DEFAULT_SPEC_OPTIONS['log_level']) self.clean_output_dir = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['clean_build']) self.use_upx_var = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['use_upx']) self.build_wexpect_helper_var = tk.BooleanVar(value=False) self.build_queue: queue.Queue = queue.Queue() self._last_parsed_spec_options: Optional[Dict[str, Any]] = None self.original_cursor: str = "" # Variabile per salvare il cursore self.main_frame = ttk.Frame(self, padding="10") self.main_frame.grid(row=0, column=0, sticky="nsew") self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self._create_widgets_frames_and_output_log_area() self.output_logger = OutputLogger( output_text_widget=self.output_text_widget, build_queue=self.build_queue, parent_window=self ) self.project_manager = ProjectManager( parent_gui=self, project_dir_var=self.project_directory_path, derived_script_label_val=self.derived_script_label_val, derived_spec_label_val=self.derived_spec_label_val, derived_icon_label_val=self.derived_icon_label_val, icon_path_var=self.icon_path, logger_func=self.output_logger.log_message, on_project_selected_callback=self._on_project_selected ) self.options_manager = OptionsManager( parent_gui=self, options_frame=self.basic_options_frame, icon_path_var=self.icon_path, app_name_var=self.app_name, is_onefile_var=self.is_onefile, is_windowed_var=self.is_windowed, use_upx_var=self.use_upx_var, clean_output_dir_var=self.clean_output_dir, build_wexpect_helper_var=self.build_wexpect_helper_var, derived_icon_path=self.project_manager.get_derived_paths().get("derived_icon_path"), logger_func=self.output_logger.log_message ) self.data_files_manager = DataFilesManager( parent_gui=self, data_listbox_widget=self.data_listbox, logger_func=self.output_logger.log_message ) self.version_manager = VersionManager( logger_func=self.output_logger.log_message ) self.build_orchestrator = BuildOrchestrator( parent_gui_ref=self, output_logger_log_message_func=self.output_logger.log_message, build_queue_ref=self.build_queue, project_manager_ref=self.project_manager, version_manager_ref=self.version_manager, data_files_manager_ref=self.data_files_manager, get_project_dir_func=self.project_directory_path.get, get_main_script_path_func=lambda: self.project_manager.get_derived_paths().get("main_script_path"), get_derived_source_dir_path_func=lambda: self.project_manager.get_derived_paths().get("derived_source_dir_path"), get_spec_file_path_func=lambda: self.project_manager.get_derived_paths().get("spec_file_path"), get_project_root_name_func=lambda: self.project_manager.get_derived_paths().get("project_root_name"), get_log_level_func=self.log_level.get, get_clean_output_dir_func=self.clean_output_dir.get, get_app_name_func=self.app_name.get, get_icon_path_func=self.icon_path.get, get_is_onefile_func=self.is_onefile.get, get_is_windowed_func=self.is_windowed.get, get_use_upx_func=self.use_upx_var.get, get_build_wexpect_helper_func=self.build_wexpect_helper_var.get, get_last_parsed_spec_options_func=lambda: self._last_parsed_spec_options, update_build_button_state_callback=self._update_build_button_state_gui, update_derived_spec_label_callback=self._update_derived_spec_label_gui ) self._layout_widgets() self.after(100, self._check_main_gui_action_queue) self.output_logger.log_message(f"Running {WRAPPER_APP_VERSION_STRING}", level="INFO") self.output_logger.log_message(f"{WRAPPER_BUILD_INFO}", level="DEBUG") self.output_logger.log_message("Application initialized. Select the project directory.", level="INFO") def _update_build_button_state_gui(self, state: str) -> None: if hasattr(self, 'build_button') and self.build_button.winfo_exists(): self.build_button.config(state=state) def _update_derived_spec_label_gui(self, text: str, color: str) -> None: if hasattr(self, 'derived_spec_label_val') and self.derived_spec_label_val.winfo_exists(): self.derived_spec_label_val.config(text=text, foreground=color) def _check_main_gui_action_queue(self) -> None: try: for _ in range(100): raw_item = self.build_queue.get_nowait() if isinstance(raw_item, tuple) and len(raw_item) == 3: source, action_type, data = raw_item if source == "LOG_STREAM" and action_type == "LOG": # Process log messages directly here self.output_logger._update_output_log_widget(str(data)) self.build_queue.task_done() elif source == "MAIN_GUI_ACTION": if action_type == "BUILD_SUCCESS": messagebox.showinfo("Build Successful", data, parent=self) self.build_orchestrator.handle_config_restore_after_build() elif action_type == "BUILD_ERROR": messagebox.showerror("Build Failed", data, parent=self) self.build_orchestrator._cleanup_backup_dir_if_exists() elif action_type == "BUILD_FINISHED": self.build_orchestrator.on_build_attempt_finished() # Ripristina il cursore qui, alla vera fine del processo if self.original_cursor: self.config(cursor=self.original_cursor) self.build_queue.task_done() else: self.output_logger.log_message(f"Unknown queue item source: {source}", "WARNING") self.build_queue.task_done() else: self.output_logger.log_message(f"Discarding malformed queue item: {raw_item}", "WARNING") self.build_queue.task_done() except queue.Empty: pass except Exception as e: error_msg = f"Critical error in GUI queue processing: {e}" self.output_logger.log_message(error_msg, "CRITICAL") traceback.print_exc() if self.build_orchestrator: self.build_orchestrator.on_build_attempt_finished() finally: if self.winfo_exists(): self.after(100, self._check_main_gui_action_queue) def _create_widgets_frames_and_output_log_area(self) -> None: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità self.project_frame = ttk.LabelFrame(self.main_frame, text="Project Directory") self.project_dir_label = ttk.Label(self.project_frame, text="Main Project Directory:") self.project_dir_entry = ttk.Entry(self.project_frame, textvariable=self.project_directory_path, state="readonly", width=80) self.project_dir_button_browse = ttk.Button(self.project_frame, text="Browse...") self.derived_paths_frame = ttk.LabelFrame(self.main_frame, text="Derived Paths (Automatic)") self.derived_script_label_info = ttk.Label(self.derived_paths_frame, text="Script Found:") self.derived_script_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w") self.derived_spec_label_info = ttk.Label(self.derived_paths_frame, text="Spec File Found:") self.derived_spec_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w") self.derived_icon_label_info = ttk.Label(self.derived_paths_frame, text="Icon Found:") self.derived_icon_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w") self.options_notebook = ttk.Notebook(self.main_frame) self.basic_options_frame = ttk.Frame(self.options_notebook, padding="10") self.options_notebook.add(self.basic_options_frame, text="Basic Options (from Spec/GUI)") self.data_files_frame = ttk.Frame(self.options_notebook, padding="10") self.options_notebook.add(self.data_files_frame, text="Additional Files (from Spec/GUI)") self.data_list_label = ttk.Label(self.data_files_frame, text="Files/Folders to Include (Source Rel to Proj Dir -> Dest in bundle):") self.data_list_scrollbar_y = ttk.Scrollbar(self.data_files_frame, orient=tk.VERTICAL) self.data_list_scrollbar_x = ttk.Scrollbar(self.data_files_frame, orient=tk.HORIZONTAL) self.data_listbox = tk.Listbox( self.data_files_frame, selectmode=tk.SINGLE, yscrollcommand=self.data_list_scrollbar_y.set, xscrollcommand=self.data_list_scrollbar_x.set, height=6, width=80 ) self.data_list_scrollbar_y.config(command=self.data_listbox.yview) self.data_list_scrollbar_x.config(command=self.data_listbox.xview) self.data_buttons_frame = ttk.Frame(self.data_files_frame) self.data_button_add_file = ttk.Button(self.data_buttons_frame, text="Add File...") self.data_button_add_dir = ttk.Button(self.data_buttons_frame, text="Add Folder...") self.data_button_remove = ttk.Button(self.data_buttons_frame, text="Remove Selected") self.output_frame = ttk.LabelFrame(self.main_frame, text="Build Log") self.output_text_widget = tkinter.scrolledtext.ScrolledText(self.output_frame, wrap=tk.WORD, height=15, state="disabled") self.action_frame = ttk.Frame(self.main_frame) self.build_button = ttk.Button(self.action_frame, text="Build Executable", command=self._trigger_build_process, state="disabled") def _layout_widgets(self) -> None: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità self.project_dir_button_browse.config(command=self.project_manager.select_project_directory) self.main_frame.rowconfigure(0, weight=0) self.project_frame.grid(row=0, column=0, padx=5, pady=(0, 5), sticky="ew") self.project_frame.columnconfigure(1, weight=1) self.project_dir_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") self.project_dir_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.project_dir_button_browse.grid(row=0, column=2, padx=5, pady=5) row_idx = 1 self.main_frame.rowconfigure(row_idx, weight=0) self.derived_paths_frame.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew") self.derived_paths_frame.columnconfigure(1, weight=1) self.derived_script_label_info.grid(row=0, column=0, padx=5, pady=2, sticky="w") self.derived_script_label_val.grid(row=0, column=1, padx=5, pady=2, sticky="ew") self.derived_spec_label_info.grid(row=1, column=0, padx=5, pady=2, sticky="w") self.derived_spec_label_val.grid(row=1, column=1, padx=5, pady=2, sticky="ew") self.derived_icon_label_info.grid(row=2, column=0, padx=5, pady=2, sticky="w") self.derived_icon_label_val.grid(row=2, column=1, padx=5, pady=2, sticky="ew") row_idx += 1 self.main_frame.rowconfigure(row_idx, weight=0) self.options_notebook.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew") self.data_files_frame.columnconfigure(0, weight=1) self.data_files_frame.rowconfigure(1, weight=1) self.data_list_label.grid(row=0, column=0, columnspan=2, padx=5, pady=(0,5), sticky="w") self.data_listbox.grid(row=1, column=0, padx=(5,0), pady=5, sticky="nsew") self.data_list_scrollbar_y.grid(row=1, column=1, padx=(0,5), pady=5, sticky="ns") self.data_list_scrollbar_x.grid(row=2, column=0, padx=5, pady=(0,5), sticky="ew") self.data_buttons_frame.grid(row=1, column=2, rowspan=2, padx=5, pady=5, sticky="ns") self.data_button_add_file.pack(fill=tk.X, pady=2) self.data_button_add_dir.pack(fill=tk.X, pady=2) self.data_button_remove.pack(fill=tk.X, pady=2) self.data_button_add_file.config(command=self.data_files_manager.add_file) self.data_button_add_dir.config(command=self.data_files_manager.add_directory) self.data_button_remove.config(command=self.data_files_manager.remove_selected) row_idx += 1 self.main_frame.rowconfigure(row_idx, weight=1) self.output_frame.grid(row=row_idx, column=0, padx=5, pady=5, sticky="nsew") self.output_frame.rowconfigure(0, weight=1) self.output_frame.columnconfigure(0, weight=1) self.output_text_widget.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") row_idx += 1 self.main_frame.rowconfigure(row_idx, weight=0) self.action_frame.grid(row=row_idx, column=0, padx=5, pady=(5,0), sticky="e") self.build_button.pack(pady=5) def _on_project_selected(self, project_root_str: str, derived_paths_info: Dict[str, Any]) -> None: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità self.output_logger.log_message(f"Project selection complete. Derived: {derived_paths_info.get('project_root_name')}", level="INFO") self.options_manager.update_derived_icon_path(derived_paths_info.get("derived_icon_path")) self._reset_options_to_defaults( derive_name=True, project_root_name=derived_paths_info.get("project_root_name"), derived_icon_path=derived_paths_info.get("derived_icon_path") ) spec_file_path = derived_paths_info.get("spec_file_path") if spec_file_path and pathlib.Path(spec_file_path).is_file(): self._handle_existing_spec_file(spec_file_path) else: self.output_logger.log_message(f"Spec file not found or path invalid. New one will be generated.", level="WARNING") self.derived_spec_label_val.config(text="NOT FOUND (will be generated)", foreground="orange") main_script_path = derived_paths_info.get("main_script_path") if main_script_path and pathlib.Path(main_script_path).is_file(): self._update_build_button_state_gui("normal") self.output_logger.log_message("Project valid. Review options or click 'Build Executable'.", level="INFO") else: self._update_build_button_state_gui("disabled") self.output_logger.log_message("Build button disabled: Main script not found.", level="ERROR") def _handle_existing_spec_file(self, spec_path_str: str) -> None: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità self.output_logger.log_message("-" * 15 + " Parsing Existing Spec File " + "-" * 15, level="INFO") parsed_options = spec_parser.parse_spec_file(spec_path_str, logger_func=self.output_logger.log_message) self._last_parsed_spec_options = parsed_options if parsed_options: self.options_manager.load_options_from_parsed_spec(parsed_options, self.project_directory_path.get()) self.data_files_manager.update_gui_from_spec_options(parsed_options) self.output_logger.log_message(f"GUI fields updated from '{spec_path_str}'.", level="INFO") self.derived_spec_label_val.config(text=spec_path_str, foreground="black") elif parsed_options is None: self.output_logger.log_message(f"Failed to parse '{spec_path_str}'. GUI uses defaults.", level="ERROR") self.derived_spec_label_val.config(text="ERROR PARSING", foreground="red") else: self.output_logger.log_message(f"Parsed '{spec_path_str}' but no common options. GUI uses defaults.", level="WARNING") self.derived_spec_label_val.config(text=spec_path_str, foreground="black") def _reset_options_to_defaults(self, derive_name: bool = True, project_root_name: Optional[str] = None, derived_icon_path: Optional[str] = None) -> None: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità self.output_logger.log_message("Resetting options to default values.", level="INFO") self.options_manager.reset_to_defaults( derive_name=derive_name, project_root_name=project_root_name, derived_icon_path=derived_icon_path ) self.data_files_manager.reset_data_list() self.log_level.set(config.DEFAULT_SPEC_OPTIONS['log_level']) self._last_parsed_spec_options = None def _trigger_build_process(self) -> None: """ Command for the 'Build Executable' button. Delegates the entire build process to the BuildOrchestrator. """ # Salva il cursore corrente e imposta quello di attesa self.original_cursor = self.cget("cursor") self.config(cursor="watch") self.update_idletasks() try: if self.build_orchestrator: self.build_orchestrator.start_build_process() else: self.output_logger.log_message("Build orchestrator not initialized!", level="CRITICAL") messagebox.showerror("Internal Error", "Build orchestrator not available.", parent=self) # Ripristina il cursore se l'orchestratore non parte self.config(cursor=self.original_cursor) except Exception as e: # Cattura qualsiasi altro errore imprevisto all'avvio self.output_logger.log_message(f"Failed to trigger build process: {e}", "CRITICAL") self.config(cursor=self.original_cursor)