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