635 lines
34 KiB
Python
635 lines
34 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
|
|
import os
|
|
import subprocess
|
|
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 .spec_editor_panel import SpecEditorPanel
|
|
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
|
|
from pyinstallerguiwrapper import dependency_detector
|
|
|
|
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.auto_include_hiddenimports_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"),
|
|
auto_include_hiddenimports_var=self.auto_include_hiddenimports_var,
|
|
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
|
|
)
|
|
|
|
# Instantiate the SpecEditorPanel (advanced structured editor)
|
|
try:
|
|
self.spec_editor_panel = SpecEditorPanel(
|
|
parent=self.spec_editor_frame,
|
|
get_spec_file_path_func=lambda: self.project_manager.get_derived_paths().get("spec_file_path"),
|
|
logger_func=self.output_logger.log_message
|
|
)
|
|
self.spec_editor_panel.pack(fill=tk.BOTH, expand=True)
|
|
except Exception as e:
|
|
self.output_logger.log_message(f"Failed to initialize SpecEditorPanel: {e}", level="ERROR")
|
|
|
|
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_auto_include_hiddenimports_func=self.auto_include_hiddenimports_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)")
|
|
|
|
# Spec editor frame/tab (structured editor for Analysis/EXE/COLLECT)
|
|
self.spec_editor_frame = ttk.Frame(self.options_notebook, padding="10")
|
|
self.options_notebook.add(self.spec_editor_frame, text="Spec Editor (Advanced)")
|
|
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")
|
|
self.open_output_folder_button = ttk.Button(self.project_action_frame, text="Open Output Folder", command=self._open_output_folder, state="normal")
|
|
|
|
# === 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")
|
|
# Place the build and open-folder buttons side by side
|
|
self.build_button.pack(side=tk.LEFT, padx=(0,5), pady=5)
|
|
self.open_output_folder_button.pack(side=tk.LEFT, padx=(0,5), 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)
|
|
# Refresh structured spec editor (if present) so it shows current spec
|
|
try:
|
|
if hasattr(self, 'spec_editor_panel') and self.spec_editor_panel:
|
|
self.spec_editor_panel.refresh_calls()
|
|
except Exception:
|
|
self.output_logger.log_message('Failed to refresh SpecEditorPanel after project selection.', level='DEBUG')
|
|
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 not self.build_orchestrator:
|
|
self.output_logger.log_message("Build orchestrator not initialized!", level="CRITICAL")
|
|
messagebox.showerror("Internal Error", "Build orchestrator not available.", parent=self)
|
|
self.config(cursor=self.original_cursor)
|
|
return
|
|
|
|
# If auto-include is enabled, run the static detector first and ask the
|
|
# user to confirm/edit the detected modules before proceeding.
|
|
try:
|
|
if self.auto_include_hiddenimports_var.get():
|
|
project_dir = self.project_directory_path.get()
|
|
if project_dir and os.path.isdir(project_dir):
|
|
detected_details = dependency_detector.detect_external_components(project_dir, logger=self.output_logger.log_message)
|
|
# detected_details: {'modules': [...], 'packages': [{'name','path','datas','deps'}, ...]}
|
|
if detected_details and (detected_details.get('modules') or detected_details.get('packages')):
|
|
confirmed_modules, selected_package_datas = self._prompt_user_to_confirm_hiddenimports(detected_details)
|
|
if confirmed_modules is None:
|
|
# User cancelled the confirmation dialog; abort build.
|
|
self.output_logger.log_message("Build cancelled by user during hiddenimports confirmation.", level="INFO")
|
|
self.config(cursor=self.original_cursor)
|
|
return
|
|
|
|
# Pass confirmed modules to orchestrator for spec generation
|
|
try:
|
|
self.build_orchestrator.set_confirmed_hiddenimports(confirmed_modules)
|
|
except Exception as e_set:
|
|
self.output_logger.log_message(f"Failed to set confirmed hiddenimports: {e_set}", level="WARNING")
|
|
|
|
# Add selected package datas to Additional Files list (programmatically)
|
|
for src_abs, dest in (selected_package_datas or []):
|
|
try:
|
|
project_root = self.project_directory_path.get()
|
|
rel = os.path.relpath(src_abs, project_root)
|
|
except Exception:
|
|
rel = src_abs
|
|
try:
|
|
self.data_files_manager.add_data_entry(rel, dest)
|
|
except Exception as e_add:
|
|
self.output_logger.log_message(f"Failed to add detected external package to Additional Files: {e_add}", level="WARNING")
|
|
else:
|
|
self.output_logger.log_message("Dependency detector found no external modules or packages.", level="DEBUG")
|
|
else:
|
|
self.output_logger.log_message("Auto-include enabled but project directory invalid; skipping detector.", level="WARNING")
|
|
except Exception as e_det:
|
|
self.output_logger.log_message(f"Dependency detection/confirmation failed: {e_det}", level="WARNING")
|
|
|
|
# Delegate to orchestrator to start the build (it will use the confirmed list if set)
|
|
self.build_orchestrator.start_build_process()
|
|
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)
|
|
|
|
def _prompt_user_to_confirm_hiddenimports(self, detected_details: dict) -> Optional[tuple]:
|
|
"""Show a modal dialog listing detected packages and modules.
|
|
|
|
`detected_details` should be a dict with keys:
|
|
- 'packages': list of {'name','path','datas','deps'}
|
|
- 'modules': list of module names
|
|
|
|
Returns a tuple `(confirmed_modules_list, selected_package_datas)` where
|
|
`selected_package_datas` is a list of `(src_abs_path, dest_in_bundle)` for
|
|
any external packages the user chose to include. Returns `None` if the
|
|
user cancelled.
|
|
"""
|
|
packages = detected_details.get('packages', []) if isinstance(detected_details, dict) else []
|
|
modules = detected_details.get('modules', []) if isinstance(detected_details, dict) else (detected_details if detected_details else [])
|
|
|
|
dlg = tk.Toplevel(self)
|
|
dlg.transient(self)
|
|
dlg.title("Confirm Detected External Components")
|
|
dlg.grab_set()
|
|
|
|
info_lbl = ttk.Label(dlg, text="Detected external packages and modules.\nSelect package folders to include as Additional Files and modules to add to hiddenimports.")
|
|
info_lbl.pack(padx=10, pady=(10, 5))
|
|
|
|
# PACKAGES SECTION
|
|
pkg_frame = ttk.LabelFrame(dlg, text="External Packages (from 'external' folder)")
|
|
pkg_frame.pack(fill=tk.BOTH, expand=False, padx=10, pady=(5,0))
|
|
pkg_canvas = tk.Canvas(pkg_frame, height=120)
|
|
pkg_scroll = ttk.Scrollbar(pkg_frame, orient=tk.VERTICAL, command=pkg_canvas.yview)
|
|
pkg_inner = ttk.Frame(pkg_canvas)
|
|
pkg_inner.bind("<Configure>", lambda e: pkg_canvas.configure(scrollregion=pkg_canvas.bbox("all")))
|
|
pkg_canvas.create_window((0,0), window=pkg_inner, anchor='nw')
|
|
pkg_canvas.configure(yscrollcommand=pkg_scroll.set)
|
|
pkg_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
pkg_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
pkg_var_map: Dict[str, tk.BooleanVar] = {}
|
|
pkg_deps_map: Dict[str, List[str]] = {}
|
|
for pkg in packages:
|
|
name = pkg.get('name')
|
|
path = pkg.get('path')
|
|
deps = pkg.get('deps', [])
|
|
var = tk.BooleanVar(value=True)
|
|
cb = ttk.Checkbutton(pkg_inner, text=f"{name} ({path})", variable=var)
|
|
cb.pack(anchor='w', padx=2, pady=1)
|
|
pkg_var_map[path] = var
|
|
pkg_deps_map[path] = deps
|
|
if deps:
|
|
dep_lbl = ttk.Label(pkg_inner, text=f" deps: {', '.join(deps)}", foreground='grey')
|
|
dep_lbl.pack(anchor='w', padx=18)
|
|
|
|
# MODULES SECTION
|
|
mod_frame = ttk.LabelFrame(dlg, text="Detected Modules (candidates for hiddenimports)")
|
|
mod_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(8,0))
|
|
|
|
canvas = tk.Canvas(mod_frame, height=140)
|
|
scrollbar = ttk.Scrollbar(mod_frame, orient=tk.VERTICAL, command=canvas.yview)
|
|
inner = ttk.Frame(canvas)
|
|
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
canvas.create_window((0, 0), window=inner, anchor='nw')
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
var_map: Dict[str, tk.BooleanVar] = {}
|
|
for mod in modules:
|
|
v = tk.BooleanVar(value=True)
|
|
cb = ttk.Checkbutton(inner, text=mod, variable=v)
|
|
cb.pack(anchor='w', padx=2, pady=1)
|
|
var_map[mod] = v
|
|
|
|
# Entry to add manual modules (comma or newline separated)
|
|
add_frame = ttk.Frame(dlg)
|
|
add_frame.pack(fill=tk.X, padx=10, pady=(5, 0))
|
|
add_entry = ttk.Entry(add_frame)
|
|
add_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
def _add_manual():
|
|
text = add_entry.get().strip()
|
|
if not text:
|
|
return
|
|
parts = [p.strip() for p in text.replace(',', ' ').split() if p.strip()]
|
|
for p in parts:
|
|
if p in var_map:
|
|
var_map[p].set(True)
|
|
else:
|
|
v = tk.BooleanVar(value=True)
|
|
cb = ttk.Checkbutton(inner, text=p, variable=v)
|
|
cb.pack(anchor='w', padx=2, pady=1)
|
|
var_map[p] = v
|
|
add_entry.delete(0, tk.END)
|
|
|
|
add_button = ttk.Button(add_frame, text="Add", command=_add_manual)
|
|
add_button.pack(side=tk.RIGHT, padx=(5,0))
|
|
|
|
# Select/Deselect all for modules
|
|
ctrl_frame = ttk.Frame(dlg)
|
|
ctrl_frame.pack(fill=tk.X, padx=10, pady=(5,0))
|
|
def _select_all():
|
|
for v in var_map.values():
|
|
v.set(True)
|
|
for v in pkg_var_map.values():
|
|
v.set(True)
|
|
def _deselect_all():
|
|
for v in var_map.values():
|
|
v.set(False)
|
|
for v in pkg_var_map.values():
|
|
v.set(False)
|
|
ttk.Button(ctrl_frame, text="Select All", command=_select_all).pack(side=tk.LEFT)
|
|
ttk.Button(ctrl_frame, text="Deselect All", command=_deselect_all).pack(side=tk.LEFT, padx=(5,0))
|
|
|
|
# Action buttons
|
|
btn_frame = ttk.Frame(dlg)
|
|
btn_frame.pack(fill=tk.X, padx=10, pady=10)
|
|
|
|
result_holder: Dict[str, Optional[tuple]] = {"result": None}
|
|
|
|
def _on_ok():
|
|
chosen_modules = [name for name, v in var_map.items() if v.get()]
|
|
selected_datas: List[tuple] = []
|
|
for path, v in pkg_var_map.items():
|
|
if v.get():
|
|
# find package entry by path
|
|
for pkg in packages:
|
|
if pkg.get('path') == path:
|
|
for src, dest in pkg.get('datas', []):
|
|
selected_datas.append((src, dest))
|
|
# also include package deps into chosen_modules
|
|
for d in pkg.get('deps', []):
|
|
if d not in chosen_modules:
|
|
chosen_modules.append(d)
|
|
break
|
|
result_holder['result'] = (chosen_modules, selected_datas)
|
|
dlg.grab_release()
|
|
dlg.destroy()
|
|
|
|
def _on_cancel():
|
|
result_holder['result'] = None
|
|
dlg.grab_release()
|
|
dlg.destroy()
|
|
|
|
ttk.Button(btn_frame, text="OK", command=_on_ok).pack(side=tk.RIGHT, padx=(5,0))
|
|
ttk.Button(btn_frame, text="Cancel", command=_on_cancel).pack(side=tk.RIGHT)
|
|
|
|
# Center dialog over parent
|
|
self.update_idletasks()
|
|
dlg.update_idletasks()
|
|
x = self.winfo_rootx() + (self.winfo_width() // 2) - (dlg.winfo_width() // 2)
|
|
y = self.winfo_rooty() + (self.winfo_height() // 2) - (dlg.winfo_height() // 2)
|
|
try:
|
|
dlg.geometry(f"+{max(0,x)}+{max(0,y)}")
|
|
except Exception:
|
|
pass
|
|
|
|
self.wait_window(dlg)
|
|
return result_holder['result']
|
|
|
|
def _open_output_folder(self) -> None:
|
|
"""Open the project's output/dist folder in the OS file manager."""
|
|
try:
|
|
project_dir = self.project_directory_path.get()
|
|
if not project_dir:
|
|
messagebox.showwarning("Open Output Folder", "No project directory selected.", parent=self)
|
|
return
|
|
|
|
dist_folder = pathlib.Path(project_dir) / config.DEFAULT_SPEC_OPTIONS['output_dir_name']
|
|
if not dist_folder.exists():
|
|
messagebox.showinfo("Open Output Folder", f"Output folder does not exist:\n{dist_folder}\n\nBuild first or create the folder manually.", parent=self)
|
|
return
|
|
|
|
# Platform-specific open
|
|
if sys.platform.startswith('win'):
|
|
os.startfile(str(dist_folder))
|
|
elif sys.platform == 'darwin':
|
|
subprocess.run(['open', str(dist_folder)])
|
|
else:
|
|
subprocess.run(['xdg-open', str(dist_folder)])
|
|
except Exception as e:
|
|
self.output_logger.log_message(f"Failed to open output folder: {e}", level="ERROR")
|
|
messagebox.showerror("Open Output Folder", f"Could not open output folder: {e}", parent=self) |