377 lines
16 KiB
Python
377 lines
16 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[pathlib.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 _find_source_folders_with_main(self, project_root: pathlib.Path) -> list[pathlib.Path]:
|
|
"""
|
|
Finds all direct subdirectories of the project root that contain a __main__.py file.
|
|
|
|
Args:
|
|
project_root: The root directory of the project.
|
|
|
|
Returns:
|
|
A list of Path objects representing subdirectories containing __main__.py.
|
|
"""
|
|
candidates = []
|
|
try:
|
|
for item in project_root.iterdir():
|
|
if item.is_dir() and not item.name.startswith('.') and not item.name.startswith('_'):
|
|
main_script = item / "__main__.py"
|
|
if main_script.is_file():
|
|
candidates.append(item)
|
|
self._log(f"Found candidate source folder: {item.name}", level="DEBUG")
|
|
except Exception as e:
|
|
self._log(f"Error scanning directory: {e}", level="ERROR")
|
|
|
|
return candidates
|
|
|
|
def _ask_user_for_source_folder(self, project_root: pathlib.Path, candidates: list[pathlib.Path]) -> Optional[pathlib.Path]:
|
|
"""
|
|
Asks the user to select the correct source folder either from candidates or by manual selection.
|
|
|
|
Args:
|
|
project_root: The root directory of the project.
|
|
candidates: List of candidate folders found automatically.
|
|
|
|
Returns:
|
|
The selected source folder Path, or None if cancelled.
|
|
"""
|
|
dialog = tk.Toplevel(self.parent_gui)
|
|
dialog.title("Select Source Folder")
|
|
dialog.geometry("600x400")
|
|
dialog.transient(self.parent_gui)
|
|
dialog.grab_set()
|
|
|
|
selected_folder = None
|
|
|
|
def on_select():
|
|
nonlocal selected_folder
|
|
selection = listbox.curselection()
|
|
if selection:
|
|
idx = selection[0]
|
|
if idx < len(candidates):
|
|
selected_folder = candidates[idx]
|
|
dialog.destroy()
|
|
|
|
def on_browse():
|
|
nonlocal selected_folder
|
|
folder = filedialog.askdirectory(
|
|
title="Select the source folder containing __main__.py",
|
|
initialdir=str(project_root),
|
|
parent=dialog
|
|
)
|
|
if folder:
|
|
folder_path = pathlib.Path(folder)
|
|
# Verify it contains __main__.py
|
|
if (folder_path / "__main__.py").is_file():
|
|
selected_folder = folder_path
|
|
dialog.destroy()
|
|
else:
|
|
messagebox.showerror(
|
|
"Invalid Selection",
|
|
f"The selected folder does not contain a __main__.py file.",
|
|
parent=dialog
|
|
)
|
|
|
|
def on_cancel():
|
|
dialog.destroy()
|
|
|
|
# Message label
|
|
msg_frame = ttk.Frame(dialog, padding=10)
|
|
msg_frame.pack(fill=tk.X)
|
|
|
|
if candidates:
|
|
msg_text = "Multiple source folders found. Please select the correct one:"
|
|
else:
|
|
msg_text = "No source folder found automatically. Please browse to select the folder containing __main__.py:"
|
|
|
|
ttk.Label(msg_frame, text=msg_text, wraplength=560).pack()
|
|
|
|
# Listbox with candidates
|
|
if candidates:
|
|
list_frame = ttk.Frame(dialog, padding=10)
|
|
list_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
scrollbar = ttk.Scrollbar(list_frame)
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set)
|
|
listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
scrollbar.config(command=listbox.yview)
|
|
|
|
for candidate in candidates:
|
|
listbox.insert(tk.END, candidate.name)
|
|
|
|
listbox.bind('<Double-Button-1>', lambda e: on_select())
|
|
else:
|
|
listbox = None
|
|
|
|
# Buttons
|
|
btn_frame = ttk.Frame(dialog, padding=10)
|
|
btn_frame.pack(fill=tk.X)
|
|
|
|
if candidates:
|
|
ttk.Button(btn_frame, text="Select", command=on_select).pack(side=tk.LEFT, padx=5)
|
|
|
|
ttk.Button(btn_frame, text="Browse...", command=on_browse).pack(side=tk.LEFT, padx=5)
|
|
ttk.Button(btn_frame, text="Cancel", command=on_cancel).pack(side=tk.RIGHT, padx=5)
|
|
|
|
dialog.wait_window()
|
|
return selected_folder
|
|
|
|
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.
|
|
If the expected source folder is not found, searches for alternatives and asks the user.
|
|
"""
|
|
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()
|
|
|
|
# Try to find the source directory - first check the expected location
|
|
expected_source_dir = project_root / project_name_lower
|
|
source_folder_name = project_name_lower # Default to expected name
|
|
|
|
if (expected_source_dir / "__main__.py").is_file():
|
|
# Expected location found - use it
|
|
self.derived_source_dir_path = expected_source_dir
|
|
self._log(f"Using expected source folder: {source_folder_name}", level="INFO")
|
|
else:
|
|
# Expected location not found - search for alternatives
|
|
self._log(f"Expected source folder '{project_name_lower}' not found or does not contain __main__.py", level="WARNING")
|
|
self._log("Searching for alternative source folders...", level="INFO")
|
|
|
|
candidates = self._find_source_folders_with_main(project_root)
|
|
|
|
if len(candidates) == 1:
|
|
# Only one candidate found - use it automatically
|
|
self.derived_source_dir_path = candidates[0]
|
|
source_folder_name = candidates[0].name
|
|
self._log(f"Automatically selected single candidate: {source_folder_name}", level="INFO")
|
|
elif len(candidates) > 1:
|
|
# Multiple candidates - ask user to choose
|
|
self._log(f"Found {len(candidates)} candidate folders, asking user to select...", level="INFO")
|
|
selected = self._ask_user_for_source_folder(project_root, candidates)
|
|
if selected:
|
|
self.derived_source_dir_path = selected
|
|
source_folder_name = selected.name
|
|
self._log(f"User selected source folder: {source_folder_name}", level="INFO")
|
|
else:
|
|
self._log("User cancelled source folder selection.", level="INFO")
|
|
return
|
|
else:
|
|
# No candidates found - ask user to browse
|
|
self._log("No source folders with __main__.py found, asking user to browse...", level="WARNING")
|
|
selected = self._ask_user_for_source_folder(project_root, [])
|
|
if selected:
|
|
self.derived_source_dir_path = selected
|
|
source_folder_name = selected.name
|
|
self._log(f"User selected source folder: {source_folder_name}", level="INFO")
|
|
else:
|
|
self._log("User cancelled source folder selection.", level="INFO")
|
|
return
|
|
|
|
# Now derive paths using the determined source folder name
|
|
self.derived_main_script_path = str(
|
|
self.derived_source_dir_path / "__main__.py"
|
|
)
|
|
self.derived_spec_path = str(project_root / f"{source_folder_name}.spec")
|
|
|
|
# Default icon filename based on platform
|
|
default_icon_filename = f"{source_folder_name}.ico"
|
|
if sys.platform == "darwin":
|
|
default_icon_filename = f"{source_folder_name}.icns"
|
|
elif sys.platform != "win32":
|
|
default_icon_filename = f"{source_folder_name}.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)
|
|
|
|
# At this point, main script should always exist since we verified it during folder selection
|
|
if main_script_path_obj.is_file():
|
|
self.derived_script_label_val.config(
|
|
text=str(main_script_path_obj), foreground="black"
|
|
)
|
|
self._log(f"Main script confirmed: {main_script_path_obj}", level="INFO")
|
|
else:
|
|
# This shouldn't happen anymore, but keep as safeguard
|
|
self.derived_script_label_val.config(text="NOT FOUND!", foreground="red")
|
|
self._log(
|
|
f"Error: Main script not found: {main_script_path_obj}",
|
|
level="ERROR",
|
|
)
|
|
messagebox.showerror(
|
|
"Invalid Project Structure",
|
|
f"Main script not found:\n{main_script_path_obj}",
|
|
parent=self.parent_gui,
|
|
)
|
|
return
|
|
|
|
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
|
|
|
|
# Source directory should always exist at this point
|
|
if (
|
|
isinstance(self.derived_source_dir_path, pathlib.Path)
|
|
and not self.derived_source_dir_path.is_dir()
|
|
):
|
|
self._log(
|
|
f"Warning: Source directory '{self.derived_source_dir_path.name}' does not exist.",
|
|
level="WARNING",
|
|
)
|
|
|
|
# Call the callback - main script existence is guaranteed at this point
|
|
if 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(),
|
|
}
|