384 lines
21 KiB
Python
384 lines
21 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 .simple_script_builder import SimpleScriptBuilder
|
|
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)
|
|
|
|
# Create main mode notebook (tabs for different build modes)
|
|
self.mode_notebook = ttk.Notebook(self.main_frame)
|
|
|
|
# Create frames for each mode
|
|
self.project_mode_frame = ttk.Frame(self.mode_notebook, padding="10")
|
|
self.simple_mode_frame = ttk.Frame(self.mode_notebook, padding="10")
|
|
|
|
# Add tabs
|
|
self.mode_notebook.add(self.project_mode_frame, text="Project Mode")
|
|
self.mode_notebook.add(self.simple_mode_frame, text="Simple Script Mode")
|
|
|
|
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.simple_script_builder = SimpleScriptBuilder(
|
|
parent_frame=self.simple_mode_frame,
|
|
logger_func=self.output_logger.log_message
|
|
)
|
|
|
|
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:
|
|
# === PROJECT MODE WIDGETS (in project_mode_frame) ===
|
|
self.project_frame = ttk.LabelFrame(self.project_mode_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.project_mode_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.project_mode_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.project_action_frame = ttk.Frame(self.project_mode_frame)
|
|
self.build_button = ttk.Button(self.project_action_frame, text="Build Executable", command=self._trigger_build_process, state="disabled")
|
|
|
|
# === SHARED OUTPUT LOG (in main_frame, outside tabs) ===
|
|
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")
|
|
|
|
def _layout_widgets(self) -> None:
|
|
# === LAYOUT PROJECT MODE TAB ===
|
|
self.project_dir_button_browse.config(command=self.project_manager.select_project_directory)
|
|
self.project_mode_frame.rowconfigure(0, weight=0)
|
|
self.project_mode_frame.columnconfigure(0, weight=1)
|
|
|
|
# Project frame
|
|
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)
|
|
|
|
# Derived paths frame
|
|
row_idx = 1
|
|
self.project_mode_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")
|
|
|
|
# Options notebook
|
|
row_idx += 1
|
|
self.project_mode_frame.rowconfigure(row_idx, weight=1)
|
|
self.options_notebook.grid(row=row_idx, column=0, padx=5, pady=5, sticky="nsew")
|
|
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)
|
|
|
|
# Action frame
|
|
row_idx += 1
|
|
self.project_mode_frame.rowconfigure(row_idx, weight=0)
|
|
self.project_action_frame.grid(row=row_idx, column=0, padx=5, pady=(5,0), sticky="e")
|
|
self.build_button.pack(pady=5)
|
|
|
|
# === LAYOUT MAIN FRAME (MODE NOTEBOOK + OUTPUT LOG) ===
|
|
self.main_frame.rowconfigure(0, weight=1)
|
|
self.main_frame.rowconfigure(1, weight=1)
|
|
self.main_frame.columnconfigure(0, weight=1)
|
|
|
|
# Mode notebook (tabs)
|
|
self.mode_notebook.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
|
|
|
|
# Shared output log
|
|
self.output_frame.grid(row=1, 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")
|
|
|
|
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) |