SXXXXXXX_PyInstallerGUIWrapper/pyinstallerguiwrapper/gui/project_selector.py
2025-06-10 09:09:06 +02:00

213 lines
9.3 KiB
Python

# File: pyinstallerguiwrapper/gui/project_selector.py
# -*- coding: utf-8 -*-
"""
Manages project directory selection and derives key paths for the PyInstaller GUI Wrapper.
"""
import os
import sys
import pathlib
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from typing import Optional, Callable, Dict, Any, Tuple, Union
# Importamos config desde el paquete principal, no desde gui/
from pyinstallerguiwrapper import config
# Definizione di un logger di default per ProjectManager se non ne viene passato uno
def _default_logger(message: str, level: str = "INFO") -> None:
"""Default logger for ProjectManager if none is provided."""
# Sostituire con un vero logger di logging.Logger se necessario
# print(f"[{level}] ProjectManager: {message}")
pass
class ProjectManager:
"""
Handles the selection of the main project directory and derivation of key paths
(main script, .spec file, icon). Manages associated Tkinter variables and GUI labels.
"""
def __init__(
self,
parent_gui: tk.Tk, # Riferimento alla finestra principale per i dialoghi
project_dir_var: tk.StringVar,
derived_script_label_val: ttk.Label,
derived_spec_label_val: ttk.Label,
derived_icon_label_val: ttk.Label,
icon_path_var: tk.StringVar, # La variabile Tkinter per il path dell'icona (che viene auto-riempita)
logger_func: Optional[Callable[..., None]] = None,
on_project_selected_callback: Optional[
Callable[[str, Dict[str, Any]], None]
] = None,
):
"""
Initializes the ProjectManager.
Args:
parent_gui: The main Tkinter window (for dialogs).
project_dir_var: The StringVar for the selected project directory.
derived_script_label_val: The Label widget for displaying the main script path.
derived_spec_label_val: The Label widget for displaying the .spec file path.
derived_icon_label_val: The Label widget for displaying the icon path.
icon_path_var: The StringVar for the user-selected icon path (auto-filled on project select).
logger_func: A function to use for logging messages.
on_project_selected_callback: A callback function (project_root_str: str, derived_paths: Dict[str, Any]) -> None
to be called after a project is successfully selected and paths derived.
"""
self.parent_gui = parent_gui
self.project_directory_path = project_dir_var
self.derived_script_label_val = derived_script_label_val
self.derived_spec_label_val = derived_spec_label_val
self.derived_icon_label_val = derived_icon_label_val
self.icon_path_var = icon_path_var
self.logger = logger_func if logger_func else _default_logger
self.on_project_selected_callback = on_project_selected_callback
# Store derived paths as attributes for easy access within the class
self.derived_main_script_path: Optional[str] = None
self.derived_spec_path: Optional[str] = None
self.derived_icon_path: Optional[str] = None
self.derived_source_dir_path: Optional[path.Path] = None # pathlib.Path object
self.project_root_name: Optional[str] = None
def _log(self, message: str, level: str = "INFO") -> None:
"""Helper for logging messages with a consistent prefix."""
try:
self.logger(f"[ProjectManager] {message}", level=level)
except TypeError:
self.logger(f"[{level}][ProjectManager] {message}")
def select_project_directory(self) -> None:
"""
Opens a file dialog for the user to select the main project directory.
Derives and updates paths for the main script, .spec file, and icon.
"""
self._log(
"=" * 20 + " PROJECT DIRECTORY SELECTION STARTED " + "=" * 20, level="INFO"
)
dir_path = filedialog.askdirectory(
title="Select Main Project Directory", parent=self.parent_gui
)
if not dir_path:
self._log("Directory selection cancelled.", level="INFO")
return
self.project_directory_path.set(dir_path)
self._log(f"Selected project directory: {dir_path}", level="INFO")
project_root = pathlib.Path(dir_path)
self.project_root_name = project_root.name
project_name_lower = self.project_root_name.lower()
# Derived paths as strings for direct assignment to Tkinter variables/attributes
self.derived_source_dir_path = project_root / project_name_lower
self.derived_main_script_path = str(
self.derived_source_dir_path / "__main__.py"
)
self.derived_spec_path = str(project_root / f"{project_name_lower}.spec")
# Default icon filename based on platform
default_icon_filename = f"{project_name_lower}.ico"
if sys.platform == "darwin":
default_icon_filename = f"{project_name_lower}.icns"
elif sys.platform != "win32":
default_icon_filename = f"{project_name_lower}.png"
self.derived_icon_path = str(project_root / default_icon_filename)
self._log(f"Derived project name: {self.project_root_name}", level="DEBUG")
self._log(
f" Expected source path: {self.derived_source_dir_path}", level="DEBUG"
)
self._log(
f" Expected main script: {self.derived_main_script_path}", level="DEBUG"
)
self._log(f" Expected spec file: {self.derived_spec_path}", level="DEBUG")
self._log(
f" Expected icon file: {self.derived_icon_path} (platform default)",
level="DEBUG",
)
# Update GUI labels for derived paths (use pathlib for checks)
main_script_path_obj = pathlib.Path(self.derived_main_script_path)
spec_path_obj = pathlib.Path(self.derived_spec_path)
icon_path_obj = pathlib.Path(self.derived_icon_path)
if main_script_path_obj.is_file():
self.derived_script_label_val.config(
text=str(main_script_path_obj), foreground="black"
)
else:
self.derived_script_label_val.config(text="NOT FOUND!", foreground="red")
self._log(
f"Error: Main script not found in the expected location: {main_script_path_obj}",
level="ERROR",
)
messagebox.showerror(
"Invalid Project Structure",
f"Main script not found in expected location:\n{main_script_path_obj}",
parent=self.parent_gui,
)
if spec_path_obj.is_file():
self.derived_spec_label_val.config(
text=str(spec_path_obj), foreground="black"
)
else:
self.derived_spec_label_val.config(
text="NOT FOUND (will be generated)", foreground="orange"
)
if icon_path_obj.is_file():
self.derived_icon_label_val.config(
text=str(icon_path_obj), foreground="black"
)
self.icon_path_var.set(
str(icon_path_obj)
) # Auto-fill icon path if default found
else:
self.derived_icon_label_val.config(
text="Not found (optional)", foreground="grey"
)
self.icon_path_var.set("") # Clear icon path if default not found
# Check if derived source directory actually exists
if (
isinstance(self.derived_source_dir_path, pathlib.Path)
and not self.derived_source_dir_path.is_dir()
):
self._log(
f"Warning: Expected source directory '{self.derived_source_dir_path.name}' does not exist. This might be an issue if it's essential for the project structure.",
level="WARNING",
)
# Call the callback if project selection and initial path derivation are successful
if main_script_path_obj.is_file() and self.on_project_selected_callback:
# Prepare a dictionary of derived paths to pass to the callback
derived_paths_info: Dict[str, Any] = {
"project_root_path": dir_path,
"project_root_name": self.project_root_name,
"main_script_path": self.derived_main_script_path,
"derived_source_dir_path": self.derived_source_dir_path, # pathlib.Path object
"spec_file_path": self.derived_spec_path,
"derived_icon_path": self.derived_icon_path,
}
self.on_project_selected_callback(dir_path, derived_paths_info)
else:
self._log(
"Project selection failed or main script not found. Callback not invoked.",
level="INFO",
)
def get_derived_paths(self) -> Dict[str, Optional[Union[str, pathlib.Path]]]:
"""Returns a dictionary of currently derived paths."""
return {
"main_script_path": self.derived_main_script_path,
"derived_source_dir_path": self.derived_source_dir_path,
"spec_file_path": self.derived_spec_path,
"derived_icon_path": self.derived_icon_path,
"project_root_name": self.project_root_name,
"project_directory_path": self.project_directory_path.get(),
}