SXXXXXXX_PyInstallerGUIWrapper/pyinstallerguiwrapper/gui/data_files_manager.py

332 lines
13 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")
def add_data_entry(self, source_rel: str, dest_in_bundle: str) -> None:
"""Programmatically add a data entry (source relative to project dir, dest in bundle).
This is used by programmatic detectors that want to suggest adding
entire folders/files to the Additional Files list without showing file
dialogs to the user.
"""
try:
entry = (source_rel, dest_in_bundle)
self.added_data_list.append(entry)
display_text = f"{source_rel} -> {dest_in_bundle}"
self.data_listbox.insert(tk.END, display_text)
self._log(f"Added data entry programmatically: {display_text}", level="INFO")
except Exception as e:
self._log(f"Failed to add programmatic data entry ({source_rel} -> {dest_in_bundle}): {e}", level="ERROR")