SXXXXXXX_PyInstallerGUIWrapper/pyinstallerguiwrapper/gui/main_window.py
2025-06-10 09:09:06 +02:00

349 lines
20 KiB
Python

# 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)