316 lines
12 KiB
Python
316 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Manages the "Additional Files" (datas) section of the PyInstaller GUI Wrapper.
|
|
Handles adding, removing, and displaying files/folders to be included in the bundle.
|
|
"""
|
|
|
|
import os
|
|
import pathlib
|
|
import tkinter as tk
|
|
from tkinter import filedialog, messagebox
|
|
from tkinter import Listbox # Importazione esplicita per Listbox
|
|
import tkinter.simpledialog as simpledialog
|
|
from typing import Optional, Callable, List, Tuple, Any, Dict, Union, TYPE_CHECKING
|
|
|
|
# Per evitare un'importazione circolare, specialmente se DataFilesManager ha bisogno
|
|
# di interagire con la finestra principale (es. per ProjectManager.project_directory_path)
|
|
if TYPE_CHECKING:
|
|
from pyinstallerguiwrapper.gui.main_window import PyInstallerGUI
|
|
from pyinstallerguiwrapper.gui.project_selector import ProjectManager
|
|
|
|
|
|
def _default_logger_func(message: str, level: str = "INFO") -> None:
|
|
"""
|
|
A simple default logger function for modules that might need a logger
|
|
but don't necessarily have a proper OutputLogger instance passed to them.
|
|
"""
|
|
print(f"[{level}] (DefaultDataFilesLogger) {message}")
|
|
|
|
|
|
class DataFilesManager:
|
|
"""
|
|
Manages the list of additional files and directories to be included in the PyInstaller bundle.
|
|
Interacts with the Tkinter Listbox widget and handles file dialogs.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
parent_gui: "PyInstallerGUI", # Riferimento alla finestra principale per i dialoghi
|
|
data_listbox_widget: Listbox,
|
|
logger_func: Optional[Callable[..., None]] = None,
|
|
):
|
|
"""
|
|
Initializes the DataFilesManager.
|
|
|
|
Args:
|
|
parent_gui: The main Tkinter window (PyInstallerGUI instance) for dialogs.
|
|
data_listbox_widget: The Tkinter Listbox widget to display the added data.
|
|
logger_func: A function to use for logging messages.
|
|
"""
|
|
self.parent_gui = parent_gui
|
|
self.data_listbox = data_listbox_widget
|
|
self.logger = logger_func if logger_func else _default_logger_func
|
|
|
|
# La lista interna che memorizza i percorsi (source, destination)
|
|
self.added_data_list: List[Tuple[str, str]] = []
|
|
|
|
def _log(self, message: str, level: str = "INFO") -> None:
|
|
"""Helper for logging messages with a consistent prefix."""
|
|
try:
|
|
self.logger(f"[DataFilesManager] {message}", level=level)
|
|
except TypeError:
|
|
self.logger(f"[{level}][DataFilesManager] {message}")
|
|
|
|
def add_file(self) -> None:
|
|
"""
|
|
Opens a file dialog for the user to select a file to add to the bundle.
|
|
Calculates its relative path and adds it to the internal list and GUI Listbox.
|
|
"""
|
|
# Ottieni il riferimento a project_manager dalla parent_gui
|
|
project_manager: "ProjectManager" = self.parent_gui.project_manager
|
|
project_dir_str = project_manager.project_directory_path.get()
|
|
|
|
if not project_dir_str:
|
|
messagebox.showerror(
|
|
"Error", "Select the project directory first.", parent=self.parent_gui
|
|
)
|
|
self._log(
|
|
"Add file cancelled: project directory not selected.", level="ERROR"
|
|
)
|
|
return
|
|
|
|
initial_dir = (
|
|
project_dir_str
|
|
if project_dir_str and os.path.isdir(project_dir_str)
|
|
else os.getcwd()
|
|
)
|
|
|
|
source_path_abs_str = filedialog.askopenfilename(
|
|
title="Select Source File to Add",
|
|
initialdir=initial_dir,
|
|
parent=self.parent_gui,
|
|
)
|
|
if not source_path_abs_str:
|
|
self._log("Add file cancelled.", level="DEBUG")
|
|
return
|
|
|
|
try:
|
|
abs_source_path = pathlib.Path(source_path_abs_str).resolve()
|
|
project_root_path = pathlib.Path(project_dir_str).resolve()
|
|
relative_source_path_for_spec = os.path.relpath(
|
|
str(abs_source_path), str(project_root_path)
|
|
)
|
|
except ValueError as e:
|
|
messagebox.showerror(
|
|
"Path Error",
|
|
f"Cannot calculate relative path for the file.\n"
|
|
f"Ensure the file is on the same drive as the project directory or in a subfolder.\n"
|
|
f"Error: {e}",
|
|
parent=self.parent_gui,
|
|
)
|
|
self._log(
|
|
f"Error calculating relative path between '{source_path_abs_str}' and '{project_dir_str}': {e}",
|
|
level="ERROR",
|
|
)
|
|
return
|
|
except Exception as e:
|
|
messagebox.showerror(
|
|
"Error",
|
|
f"Unexpected error processing file path: {e}",
|
|
parent=self.parent_gui,
|
|
)
|
|
self._log(
|
|
f"Error processing path '{source_path_abs_str}': {e}", level="ERROR"
|
|
)
|
|
return
|
|
|
|
dest_path_in_bundle = simpledialog.askstring(
|
|
"Destination in Bundle",
|
|
f"Relative destination path for file:\n{abs_source_path.name}\n(e.g., '.', 'data', 'assets/{abs_source_path.name}')",
|
|
initialvalue=".",
|
|
parent=self.parent_gui,
|
|
)
|
|
if dest_path_in_bundle is None:
|
|
self._log("Add file cancelled (destination input).", level="DEBUG")
|
|
return
|
|
|
|
entry = (relative_source_path_for_spec, dest_path_in_bundle)
|
|
self.added_data_list.append(entry)
|
|
display_text = f"{relative_source_path_for_spec} -> {dest_path_in_bundle}"
|
|
self.data_listbox.insert(tk.END, display_text)
|
|
self._log(
|
|
f"Added data file: {display_text} (Source relative to project dir)",
|
|
level="INFO",
|
|
)
|
|
|
|
def add_directory(self) -> None:
|
|
"""
|
|
Opens a directory dialog for the user to select a folder to add to the bundle.
|
|
Calculates its relative path and adds it to the internal list and GUI Listbox.
|
|
"""
|
|
# Ottieni il riferimento a project_manager dalla parent_gui
|
|
project_manager: "ProjectManager" = self.parent_gui.project_manager
|
|
project_dir_str = project_manager.project_directory_path.get()
|
|
|
|
if not project_dir_str:
|
|
messagebox.showerror(
|
|
"Error", "Select the project directory first.", parent=self.parent_gui
|
|
)
|
|
self._log(
|
|
"Add folder cancelled: project directory not selected.", level="ERROR"
|
|
)
|
|
return
|
|
|
|
initial_dir = (
|
|
project_dir_str
|
|
if project_dir_str and os.path.isdir(project_dir_str)
|
|
else os.getcwd()
|
|
)
|
|
|
|
source_path_abs_str = filedialog.askdirectory(
|
|
title="Select Source Folder to Add",
|
|
initialdir=initial_dir,
|
|
parent=self.parent_gui,
|
|
)
|
|
if not source_path_abs_str:
|
|
self._log("Add folder cancelled.", level="DEBUG")
|
|
return
|
|
|
|
try:
|
|
abs_source_path = pathlib.Path(source_path_abs_str).resolve()
|
|
project_root_path = pathlib.Path(project_dir_str).resolve()
|
|
relative_source_path_for_spec = os.path.relpath(
|
|
str(abs_source_path), str(project_root_path)
|
|
)
|
|
except ValueError as e:
|
|
messagebox.showerror(
|
|
"Path Error",
|
|
f"Cannot calculate relative path for the folder.\n"
|
|
f"Ensure the folder is on the same drive as the project directory or in a subfolder.\n"
|
|
f"Error: {e}",
|
|
parent=self.parent_gui,
|
|
)
|
|
self._log(
|
|
f"Error calculating relative path between '{source_path_abs_str}' and '{project_dir_str}': {e}",
|
|
level="ERROR",
|
|
)
|
|
return
|
|
except Exception as e:
|
|
messagebox.showerror(
|
|
"Error",
|
|
f"Unexpected error processing folder path: {e}",
|
|
parent=self.parent_gui,
|
|
)
|
|
self._log(
|
|
f"Error processing path '{source_path_abs_str}': {e}", level="ERROR"
|
|
)
|
|
return
|
|
|
|
default_dest_in_bundle = abs_source_path.name
|
|
dest_path_in_bundle = simpledialog.askstring(
|
|
"Destination in Bundle",
|
|
f"Relative destination path for folder contents:\n{default_dest_in_bundle}\n(e.g., '{default_dest_in_bundle}', 'data/{default_dest_in_bundle}')",
|
|
initialvalue=default_dest_in_bundle,
|
|
parent=self.parent_gui,
|
|
)
|
|
if dest_path_in_bundle is None:
|
|
self._log("Add folder cancelled (destination input).", level="DEBUG")
|
|
return
|
|
|
|
entry = (relative_source_path_for_spec, dest_path_in_bundle)
|
|
self.added_data_list.append(entry)
|
|
display_text = f"{relative_source_path_for_spec} -> {dest_path_in_bundle}"
|
|
self.data_listbox.insert(tk.END, display_text)
|
|
self._log(
|
|
f"Added data folder: {display_text} (Source relative to project dir)",
|
|
level="INFO",
|
|
)
|
|
|
|
def remove_selected(self) -> None:
|
|
"""
|
|
Removes the currently selected item from the internal list and the GUI Listbox.
|
|
"""
|
|
selected_indices = self.data_listbox.curselection()
|
|
if not selected_indices:
|
|
messagebox.showwarning(
|
|
"No Selection",
|
|
"Select an item to remove from the 'Additional Files' list.",
|
|
parent=self.parent_gui,
|
|
)
|
|
return
|
|
|
|
index_to_remove = selected_indices[0]
|
|
item_text = self.data_listbox.get(index_to_remove)
|
|
self.data_listbox.delete(index_to_remove)
|
|
|
|
if 0 <= index_to_remove < len(self.added_data_list):
|
|
self.added_data_list.pop(index_to_remove)
|
|
self._log(f"Removed data entry: {item_text}", level="INFO")
|
|
else:
|
|
self._log(
|
|
f"Error: Index {index_to_remove} out of range for internal data list ({len(self.added_data_list)} entries). Listbox item was: {item_text}",
|
|
level="ERROR",
|
|
)
|
|
|
|
def update_gui_from_spec_options(self, options_from_spec: Dict[str, Any]) -> None:
|
|
"""
|
|
Updates the Additional Files listbox and internal list from parsed spec options.
|
|
"""
|
|
self.added_data_list.clear()
|
|
self.data_listbox.delete(0, tk.END)
|
|
parsed_datas = options_from_spec.get("datas", [])
|
|
if parsed_datas:
|
|
self._log(
|
|
f"Found 'datas' in spec: {len(parsed_datas)} entries.", level="INFO"
|
|
)
|
|
for item in parsed_datas:
|
|
if isinstance(item, (tuple, list)) and len(item) == 2:
|
|
source_rel_to_spec, destination_in_bundle = item
|
|
if isinstance(source_rel_to_spec, str) and isinstance(
|
|
destination_in_bundle, str
|
|
):
|
|
self.added_data_list.append(
|
|
(source_rel_to_spec, destination_in_bundle)
|
|
)
|
|
display_text = (
|
|
f"{source_rel_to_spec} -> {destination_in_bundle}"
|
|
)
|
|
self.data_listbox.insert(tk.END, display_text)
|
|
self._log(
|
|
f" Added data entry from spec: '{source_rel_to_spec}' -> '{destination_in_bundle}'",
|
|
level="DEBUG",
|
|
)
|
|
else:
|
|
self._log(
|
|
f" Ignored invalid 'datas' entry (non-string paths): {item}",
|
|
level="WARNING",
|
|
)
|
|
else:
|
|
self._log(
|
|
f" Ignored invalid 'datas' entry (format not tuple/list of 2): {item}",
|
|
level="WARNING",
|
|
)
|
|
if self.added_data_list:
|
|
self._log("Additional files list populated from spec.", level="DEBUG")
|
|
else:
|
|
self._log(
|
|
"No valid 'datas' entries found in spec or 'datas' was empty.",
|
|
level="DEBUG",
|
|
)
|
|
|
|
def get_current_datas(self) -> List[Tuple[str, str]]:
|
|
"""
|
|
Returns the current list of added data entries (source, destination).
|
|
"""
|
|
return list(
|
|
self.added_data_list
|
|
) # Return a copy to prevent external modification
|
|
|
|
def reset_data_list(self) -> None:
|
|
"""
|
|
Clears the internal list of added data and clears the GUI Listbox.
|
|
"""
|
|
self.added_data_list.clear()
|
|
self.data_listbox.delete(0, tk.END)
|
|
self._log("Additional files list reset.", level="INFO")
|