# File: pyinstallerguiwrapper/gui/main_window.py # -*- coding: utf-8 -*- """ Main GUI module for the PyInstaller Wrapper. Orchestrates the application flow and integrates various UI components. """ import tkinter as tk from tkinter import ttk, messagebox import tkinter.scrolledtext import queue import pathlib import sys import traceback import os import subprocess from typing import Optional, List, Any, Dict, Callable # Package imports from .project_selector import ProjectManager from .options_manager import OptionsManager from .output_logger import OutputLogger from .data_files_manager import DataFilesManager from .spec_editor_panel import SpecEditorPanel from .simple_script_builder import SimpleScriptBuilder from pyinstallerguiwrapper.build.version_manager import VersionManager from pyinstallerguiwrapper.build.build_orchestrator import BuildOrchestrator from pyinstallerguiwrapper import config from pyinstallerguiwrapper import spec_parser from pyinstallerguiwrapper import dependency_detector try: from pyinstallerguiwrapper import _version as wrapper_version WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})" WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}" except ImportError: WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" WRAPPER_BUILD_INFO = "Wrapper build time unknown" class PyInstallerGUI(tk.Tk): def __init__(self): super().__init__() self.title(f"PyInstaller GUI Wrapper - {WRAPPER_APP_VERSION_STRING}") self.project_directory_path = tk.StringVar() self.icon_path = tk.StringVar() self.app_name = tk.StringVar() self.is_onefile = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['onefile']) self.is_windowed = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['windowed']) self.log_level = tk.StringVar(value=config.DEFAULT_SPEC_OPTIONS['log_level']) self.clean_output_dir = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['clean_build']) self.use_upx_var = tk.BooleanVar(value=config.DEFAULT_SPEC_OPTIONS['use_upx']) self.build_wexpect_helper_var = tk.BooleanVar(value=False) self.auto_include_hiddenimports_var = tk.BooleanVar(value=False) self.build_queue: queue.Queue = queue.Queue() self._last_parsed_spec_options: Optional[Dict[str, Any]] = None self.original_cursor: str = "" # Variabile per salvare il cursore self.main_frame = ttk.Frame(self, padding="10") self.main_frame.grid(row=0, column=0, sticky="nsew") self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) # Create main mode notebook (tabs for different build modes) self.mode_notebook = ttk.Notebook(self.main_frame) # Create frames for each mode self.project_mode_frame = ttk.Frame(self.mode_notebook, padding="10") self.simple_mode_frame = ttk.Frame(self.mode_notebook, padding="10") # Add tabs self.mode_notebook.add(self.project_mode_frame, text="Project Mode") self.mode_notebook.add(self.simple_mode_frame, text="Simple Script Mode") self._create_widgets_frames_and_output_log_area() self.output_logger = OutputLogger( output_text_widget=self.output_text_widget, build_queue=self.build_queue, parent_window=self ) self.project_manager = ProjectManager( parent_gui=self, project_dir_var=self.project_directory_path, derived_script_label_val=self.derived_script_label_val, derived_spec_label_val=self.derived_spec_label_val, derived_icon_label_val=self.derived_icon_label_val, icon_path_var=self.icon_path, logger_func=self.output_logger.log_message, on_project_selected_callback=self._on_project_selected ) self.options_manager = OptionsManager( parent_gui=self, options_frame=self.basic_options_frame, icon_path_var=self.icon_path, app_name_var=self.app_name, is_onefile_var=self.is_onefile, is_windowed_var=self.is_windowed, use_upx_var=self.use_upx_var, clean_output_dir_var=self.clean_output_dir, build_wexpect_helper_var=self.build_wexpect_helper_var, derived_icon_path=self.project_manager.get_derived_paths().get("derived_icon_path"), auto_include_hiddenimports_var=self.auto_include_hiddenimports_var, logger_func=self.output_logger.log_message ) self.data_files_manager = DataFilesManager( parent_gui=self, data_listbox_widget=self.data_listbox, logger_func=self.output_logger.log_message ) # Instantiate the SpecEditorPanel (advanced structured editor) try: self.spec_editor_panel = SpecEditorPanel( parent=self.spec_editor_frame, get_spec_file_path_func=lambda: self.project_manager.get_derived_paths().get("spec_file_path"), logger_func=self.output_logger.log_message ) self.spec_editor_panel.pack(fill=tk.BOTH, expand=True) except Exception as e: self.output_logger.log_message(f"Failed to initialize SpecEditorPanel: {e}", level="ERROR") self.version_manager = VersionManager( logger_func=self.output_logger.log_message ) self.build_orchestrator = BuildOrchestrator( parent_gui_ref=self, output_logger_log_message_func=self.output_logger.log_message, build_queue_ref=self.build_queue, project_manager_ref=self.project_manager, version_manager_ref=self.version_manager, data_files_manager_ref=self.data_files_manager, get_project_dir_func=self.project_directory_path.get, get_main_script_path_func=lambda: self.project_manager.get_derived_paths().get("main_script_path"), get_derived_source_dir_path_func=lambda: self.project_manager.get_derived_paths().get("derived_source_dir_path"), get_spec_file_path_func=lambda: self.project_manager.get_derived_paths().get("spec_file_path"), get_project_root_name_func=lambda: self.project_manager.get_derived_paths().get("project_root_name"), get_log_level_func=self.log_level.get, get_clean_output_dir_func=self.clean_output_dir.get, get_app_name_func=self.app_name.get, get_icon_path_func=self.icon_path.get, get_is_onefile_func=self.is_onefile.get, get_is_windowed_func=self.is_windowed.get, get_use_upx_func=self.use_upx_var.get, get_build_wexpect_helper_func=self.build_wexpect_helper_var.get, get_auto_include_hiddenimports_func=self.auto_include_hiddenimports_var.get, get_last_parsed_spec_options_func=lambda: self._last_parsed_spec_options, update_build_button_state_callback=self._update_build_button_state_gui, update_derived_spec_label_callback=self._update_derived_spec_label_gui ) self.simple_script_builder = SimpleScriptBuilder( parent_frame=self.simple_mode_frame, logger_func=self.output_logger.log_message ) self._layout_widgets() self.after(100, self._check_main_gui_action_queue) self.output_logger.log_message(f"Running {WRAPPER_APP_VERSION_STRING}", level="INFO") self.output_logger.log_message(f"{WRAPPER_BUILD_INFO}", level="DEBUG") self.output_logger.log_message("Application initialized. Select the project directory.", level="INFO") def _update_build_button_state_gui(self, state: str) -> None: if hasattr(self, 'build_button') and self.build_button.winfo_exists(): self.build_button.config(state=state) def _update_derived_spec_label_gui(self, text: str, color: str) -> None: if hasattr(self, 'derived_spec_label_val') and self.derived_spec_label_val.winfo_exists(): self.derived_spec_label_val.config(text=text, foreground=color) def _check_main_gui_action_queue(self) -> None: try: for _ in range(100): raw_item = self.build_queue.get_nowait() if isinstance(raw_item, tuple) and len(raw_item) == 3: source, action_type, data = raw_item if source == "LOG_STREAM" and action_type == "LOG": # Process log messages directly here self.output_logger._update_output_log_widget(str(data)) self.build_queue.task_done() elif source == "MAIN_GUI_ACTION": if action_type == "BUILD_SUCCESS": messagebox.showinfo("Build Successful", data, parent=self) self.build_orchestrator.handle_config_restore_after_build() elif action_type == "BUILD_ERROR": messagebox.showerror("Build Failed", data, parent=self) self.build_orchestrator._cleanup_backup_dir_if_exists() elif action_type == "BUILD_FINISHED": self.build_orchestrator.on_build_attempt_finished() # Ripristina il cursore qui, alla vera fine del processo if self.original_cursor: self.config(cursor=self.original_cursor) self.build_queue.task_done() else: self.output_logger.log_message(f"Unknown queue item source: {source}", "WARNING") self.build_queue.task_done() else: self.output_logger.log_message(f"Discarding malformed queue item: {raw_item}", "WARNING") self.build_queue.task_done() except queue.Empty: pass except Exception as e: error_msg = f"Critical error in GUI queue processing: {e}" self.output_logger.log_message(error_msg, "CRITICAL") traceback.print_exc() if self.build_orchestrator: self.build_orchestrator.on_build_attempt_finished() finally: if self.winfo_exists(): self.after(100, self._check_main_gui_action_queue) def _create_widgets_frames_and_output_log_area(self) -> None: # === PROJECT MODE WIDGETS (in project_mode_frame) === self.project_frame = ttk.LabelFrame(self.project_mode_frame, text="Project Directory") self.project_dir_label = ttk.Label(self.project_frame, text="Main Project Directory:") self.project_dir_entry = ttk.Entry(self.project_frame, textvariable=self.project_directory_path, state="readonly", width=80) self.project_dir_button_browse = ttk.Button(self.project_frame, text="Browse...") self.derived_paths_frame = ttk.LabelFrame(self.project_mode_frame, text="Derived Paths (Automatic)") self.derived_script_label_info = ttk.Label(self.derived_paths_frame, text="Script Found:") self.derived_script_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w") self.derived_spec_label_info = ttk.Label(self.derived_paths_frame, text="Spec File Found:") self.derived_spec_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w") self.derived_icon_label_info = ttk.Label(self.derived_paths_frame, text="Icon Found:") self.derived_icon_label_val = ttk.Label(self.derived_paths_frame, text="N/A", foreground="grey", anchor="w") self.options_notebook = ttk.Notebook(self.project_mode_frame) self.basic_options_frame = ttk.Frame(self.options_notebook, padding="10") self.options_notebook.add(self.basic_options_frame, text="Basic Options (from Spec/GUI)") self.data_files_frame = ttk.Frame(self.options_notebook, padding="10") self.options_notebook.add(self.data_files_frame, text="Additional Files (from Spec/GUI)") # Spec editor frame/tab (structured editor for Analysis/EXE/COLLECT) self.spec_editor_frame = ttk.Frame(self.options_notebook, padding="10") self.options_notebook.add(self.spec_editor_frame, text="Spec Editor (Advanced)") self.data_list_label = ttk.Label(self.data_files_frame, text="Files/Folders to Include (Source Rel to Proj Dir -> Dest in bundle):") self.data_list_scrollbar_y = ttk.Scrollbar(self.data_files_frame, orient=tk.VERTICAL) self.data_list_scrollbar_x = ttk.Scrollbar(self.data_files_frame, orient=tk.HORIZONTAL) self.data_listbox = tk.Listbox( self.data_files_frame, selectmode=tk.SINGLE, yscrollcommand=self.data_list_scrollbar_y.set, xscrollcommand=self.data_list_scrollbar_x.set, height=6, width=80 ) self.data_list_scrollbar_y.config(command=self.data_listbox.yview) self.data_list_scrollbar_x.config(command=self.data_listbox.xview) self.data_buttons_frame = ttk.Frame(self.data_files_frame) self.data_button_add_file = ttk.Button(self.data_buttons_frame, text="Add File...") self.data_button_add_dir = ttk.Button(self.data_buttons_frame, text="Add Folder...") self.data_button_remove = ttk.Button(self.data_buttons_frame, text="Remove Selected") self.project_action_frame = ttk.Frame(self.project_mode_frame) self.build_button = ttk.Button(self.project_action_frame, text="Build Executable", command=self._trigger_build_process, state="disabled") self.open_output_folder_button = ttk.Button(self.project_action_frame, text="Open Output Folder", command=self._open_output_folder, state="normal") # === SHARED OUTPUT LOG (in main_frame, outside tabs) === self.output_frame = ttk.LabelFrame(self.main_frame, text="Build Log") self.output_text_widget = tkinter.scrolledtext.ScrolledText(self.output_frame, wrap=tk.WORD, height=15, state="disabled") def _layout_widgets(self) -> None: # === LAYOUT PROJECT MODE TAB === self.project_dir_button_browse.config(command=self.project_manager.select_project_directory) self.project_mode_frame.rowconfigure(0, weight=0) self.project_mode_frame.columnconfigure(0, weight=1) # Project frame self.project_frame.grid(row=0, column=0, padx=5, pady=(0, 5), sticky="ew") self.project_frame.columnconfigure(1, weight=1) self.project_dir_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") self.project_dir_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.project_dir_button_browse.grid(row=0, column=2, padx=5, pady=5) # Derived paths frame row_idx = 1 self.project_mode_frame.rowconfigure(row_idx, weight=0) self.derived_paths_frame.grid(row=row_idx, column=0, padx=5, pady=5, sticky="ew") self.derived_paths_frame.columnconfigure(1, weight=1) self.derived_script_label_info.grid(row=0, column=0, padx=5, pady=2, sticky="w") self.derived_script_label_val.grid(row=0, column=1, padx=5, pady=2, sticky="ew") self.derived_spec_label_info.grid(row=1, column=0, padx=5, pady=2, sticky="w") self.derived_spec_label_val.grid(row=1, column=1, padx=5, pady=2, sticky="ew") self.derived_icon_label_info.grid(row=2, column=0, padx=5, pady=2, sticky="w") self.derived_icon_label_val.grid(row=2, column=1, padx=5, pady=2, sticky="ew") # Options notebook row_idx += 1 self.project_mode_frame.rowconfigure(row_idx, weight=1) self.options_notebook.grid(row=row_idx, column=0, padx=5, pady=5, sticky="nsew") self.data_files_frame.columnconfigure(0, weight=1) self.data_files_frame.rowconfigure(1, weight=1) self.data_list_label.grid(row=0, column=0, columnspan=2, padx=5, pady=(0,5), sticky="w") self.data_listbox.grid(row=1, column=0, padx=(5,0), pady=5, sticky="nsew") self.data_list_scrollbar_y.grid(row=1, column=1, padx=(0,5), pady=5, sticky="ns") self.data_list_scrollbar_x.grid(row=2, column=0, padx=5, pady=(0,5), sticky="ew") self.data_buttons_frame.grid(row=1, column=2, rowspan=2, padx=5, pady=5, sticky="ns") self.data_button_add_file.pack(fill=tk.X, pady=2) self.data_button_add_dir.pack(fill=tk.X, pady=2) self.data_button_remove.pack(fill=tk.X, pady=2) self.data_button_add_file.config(command=self.data_files_manager.add_file) self.data_button_add_dir.config(command=self.data_files_manager.add_directory) self.data_button_remove.config(command=self.data_files_manager.remove_selected) # Action frame row_idx += 1 self.project_mode_frame.rowconfigure(row_idx, weight=0) self.project_action_frame.grid(row=row_idx, column=0, padx=5, pady=(5,0), sticky="e") # Place the build and open-folder buttons side by side self.build_button.pack(side=tk.LEFT, padx=(0,5), pady=5) self.open_output_folder_button.pack(side=tk.LEFT, padx=(0,5), pady=5) # === LAYOUT MAIN FRAME (MODE NOTEBOOK + OUTPUT LOG) === self.main_frame.rowconfigure(0, weight=1) self.main_frame.rowconfigure(1, weight=1) self.main_frame.columnconfigure(0, weight=1) # Mode notebook (tabs) self.mode_notebook.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") # Shared output log self.output_frame.grid(row=1, column=0, padx=5, pady=5, sticky="nsew") self.output_frame.rowconfigure(0, weight=1) self.output_frame.columnconfigure(0, weight=1) self.output_text_widget.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") def _on_project_selected(self, project_root_str: str, derived_paths_info: Dict[str, Any]) -> None: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità self.output_logger.log_message(f"Project selection complete. Derived: {derived_paths_info.get('project_root_name')}", level="INFO") self.options_manager.update_derived_icon_path(derived_paths_info.get("derived_icon_path")) self._reset_options_to_defaults( derive_name=True, project_root_name=derived_paths_info.get("project_root_name"), derived_icon_path=derived_paths_info.get("derived_icon_path") ) spec_file_path = derived_paths_info.get("spec_file_path") if spec_file_path and pathlib.Path(spec_file_path).is_file(): self._handle_existing_spec_file(spec_file_path) # Refresh structured spec editor (if present) so it shows current spec try: if hasattr(self, 'spec_editor_panel') and self.spec_editor_panel: self.spec_editor_panel.refresh_calls() except Exception: self.output_logger.log_message('Failed to refresh SpecEditorPanel after project selection.', level='DEBUG') else: self.output_logger.log_message(f"Spec file not found or path invalid. New one will be generated.", level="WARNING") self.derived_spec_label_val.config(text="NOT FOUND (will be generated)", foreground="orange") main_script_path = derived_paths_info.get("main_script_path") if main_script_path and pathlib.Path(main_script_path).is_file(): self._update_build_button_state_gui("normal") self.output_logger.log_message("Project valid. Review options or click 'Build Executable'.", level="INFO") else: self._update_build_button_state_gui("disabled") self.output_logger.log_message("Build button disabled: Main script not found.", level="ERROR") def _handle_existing_spec_file(self, spec_path_str: str) -> None: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità self.output_logger.log_message("-" * 15 + " Parsing Existing Spec File " + "-" * 15, level="INFO") parsed_options = spec_parser.parse_spec_file(spec_path_str, logger_func=self.output_logger.log_message) self._last_parsed_spec_options = parsed_options if parsed_options: self.options_manager.load_options_from_parsed_spec(parsed_options, self.project_directory_path.get()) self.data_files_manager.update_gui_from_spec_options(parsed_options) self.output_logger.log_message(f"GUI fields updated from '{spec_path_str}'.", level="INFO") self.derived_spec_label_val.config(text=spec_path_str, foreground="black") elif parsed_options is None: self.output_logger.log_message(f"Failed to parse '{spec_path_str}'. GUI uses defaults.", level="ERROR") self.derived_spec_label_val.config(text="ERROR PARSING", foreground="red") else: self.output_logger.log_message(f"Parsed '{spec_path_str}' but no common options. GUI uses defaults.", level="WARNING") self.derived_spec_label_val.config(text=spec_path_str, foreground="black") def _reset_options_to_defaults(self, derive_name: bool = True, project_root_name: Optional[str] = None, derived_icon_path: Optional[str] = None) -> None: # Questo metodo non ha bisogno di modifiche, lo ometto per brevità self.output_logger.log_message("Resetting options to default values.", level="INFO") self.options_manager.reset_to_defaults( derive_name=derive_name, project_root_name=project_root_name, derived_icon_path=derived_icon_path ) self.data_files_manager.reset_data_list() self.log_level.set(config.DEFAULT_SPEC_OPTIONS['log_level']) self._last_parsed_spec_options = None def _trigger_build_process(self) -> None: """ Command for the 'Build Executable' button. Delegates the entire build process to the BuildOrchestrator. """ # Salva il cursore corrente e imposta quello di attesa self.original_cursor = self.cget("cursor") self.config(cursor="watch") self.update_idletasks() try: if not self.build_orchestrator: self.output_logger.log_message("Build orchestrator not initialized!", level="CRITICAL") messagebox.showerror("Internal Error", "Build orchestrator not available.", parent=self) self.config(cursor=self.original_cursor) return # If auto-include is enabled, run the static detector first and ask the # user to confirm/edit the detected modules before proceeding. try: if self.auto_include_hiddenimports_var.get(): project_dir = self.project_directory_path.get() if project_dir and os.path.isdir(project_dir): detected_details = dependency_detector.detect_external_components(project_dir, logger=self.output_logger.log_message) # detected_details: {'modules': [...], 'packages': [{'name','path','datas','deps'}, ...]} if detected_details and (detected_details.get('modules') or detected_details.get('packages')): confirmed_modules, selected_package_datas = self._prompt_user_to_confirm_hiddenimports(detected_details) if confirmed_modules is None: # User cancelled the confirmation dialog; abort build. self.output_logger.log_message("Build cancelled by user during hiddenimports confirmation.", level="INFO") self.config(cursor=self.original_cursor) return # Pass confirmed modules to orchestrator for spec generation try: self.build_orchestrator.set_confirmed_hiddenimports(confirmed_modules) except Exception as e_set: self.output_logger.log_message(f"Failed to set confirmed hiddenimports: {e_set}", level="WARNING") # Add selected package datas to Additional Files list (programmatically) for src_abs, dest in (selected_package_datas or []): try: project_root = self.project_directory_path.get() rel = os.path.relpath(src_abs, project_root) except Exception: rel = src_abs try: self.data_files_manager.add_data_entry(rel, dest) except Exception as e_add: self.output_logger.log_message(f"Failed to add detected external package to Additional Files: {e_add}", level="WARNING") else: self.output_logger.log_message("Dependency detector found no external modules or packages.", level="DEBUG") else: self.output_logger.log_message("Auto-include enabled but project directory invalid; skipping detector.", level="WARNING") except Exception as e_det: self.output_logger.log_message(f"Dependency detection/confirmation failed: {e_det}", level="WARNING") # Delegate to orchestrator to start the build (it will use the confirmed list if set) self.build_orchestrator.start_build_process() except Exception as e: # Cattura qualsiasi altro errore imprevisto all'avvio self.output_logger.log_message(f"Failed to trigger build process: {e}", "CRITICAL") self.config(cursor=self.original_cursor) def _prompt_user_to_confirm_hiddenimports(self, detected_details: dict) -> Optional[tuple]: """Show a modal dialog listing detected packages and modules. `detected_details` should be a dict with keys: - 'packages': list of {'name','path','datas','deps'} - 'modules': list of module names Returns a tuple `(confirmed_modules_list, selected_package_datas)` where `selected_package_datas` is a list of `(src_abs_path, dest_in_bundle)` for any external packages the user chose to include. Returns `None` if the user cancelled. """ packages = detected_details.get('packages', []) if isinstance(detected_details, dict) else [] modules = detected_details.get('modules', []) if isinstance(detected_details, dict) else (detected_details if detected_details else []) dlg = tk.Toplevel(self) dlg.transient(self) dlg.title("Confirm Detected External Components") dlg.grab_set() info_lbl = ttk.Label(dlg, text="Detected external packages and modules.\nSelect package folders to include as Additional Files and modules to add to hiddenimports.") info_lbl.pack(padx=10, pady=(10, 5)) # PACKAGES SECTION pkg_frame = ttk.LabelFrame(dlg, text="External Packages (from 'external' folder)") pkg_frame.pack(fill=tk.BOTH, expand=False, padx=10, pady=(5,0)) pkg_canvas = tk.Canvas(pkg_frame, height=120) pkg_scroll = ttk.Scrollbar(pkg_frame, orient=tk.VERTICAL, command=pkg_canvas.yview) pkg_inner = ttk.Frame(pkg_canvas) pkg_inner.bind("", lambda e: pkg_canvas.configure(scrollregion=pkg_canvas.bbox("all"))) pkg_canvas.create_window((0,0), window=pkg_inner, anchor='nw') pkg_canvas.configure(yscrollcommand=pkg_scroll.set) pkg_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) pkg_scroll.pack(side=tk.RIGHT, fill=tk.Y) pkg_var_map: Dict[str, tk.BooleanVar] = {} pkg_deps_map: Dict[str, List[str]] = {} for pkg in packages: name = pkg.get('name') path = pkg.get('path') deps = pkg.get('deps', []) var = tk.BooleanVar(value=True) cb = ttk.Checkbutton(pkg_inner, text=f"{name} ({path})", variable=var) cb.pack(anchor='w', padx=2, pady=1) pkg_var_map[path] = var pkg_deps_map[path] = deps if deps: dep_lbl = ttk.Label(pkg_inner, text=f" deps: {', '.join(deps)}", foreground='grey') dep_lbl.pack(anchor='w', padx=18) # MODULES SECTION mod_frame = ttk.LabelFrame(dlg, text="Detected Modules (candidates for hiddenimports)") mod_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(8,0)) canvas = tk.Canvas(mod_frame, height=140) scrollbar = ttk.Scrollbar(mod_frame, orient=tk.VERTICAL, command=canvas.yview) inner = ttk.Frame(canvas) inner.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=inner, anchor='nw') canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) var_map: Dict[str, tk.BooleanVar] = {} for mod in modules: v = tk.BooleanVar(value=True) cb = ttk.Checkbutton(inner, text=mod, variable=v) cb.pack(anchor='w', padx=2, pady=1) var_map[mod] = v # Entry to add manual modules (comma or newline separated) add_frame = ttk.Frame(dlg) add_frame.pack(fill=tk.X, padx=10, pady=(5, 0)) add_entry = ttk.Entry(add_frame) add_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) def _add_manual(): text = add_entry.get().strip() if not text: return parts = [p.strip() for p in text.replace(',', ' ').split() if p.strip()] for p in parts: if p in var_map: var_map[p].set(True) else: v = tk.BooleanVar(value=True) cb = ttk.Checkbutton(inner, text=p, variable=v) cb.pack(anchor='w', padx=2, pady=1) var_map[p] = v add_entry.delete(0, tk.END) add_button = ttk.Button(add_frame, text="Add", command=_add_manual) add_button.pack(side=tk.RIGHT, padx=(5,0)) # Select/Deselect all for modules ctrl_frame = ttk.Frame(dlg) ctrl_frame.pack(fill=tk.X, padx=10, pady=(5,0)) def _select_all(): for v in var_map.values(): v.set(True) for v in pkg_var_map.values(): v.set(True) def _deselect_all(): for v in var_map.values(): v.set(False) for v in pkg_var_map.values(): v.set(False) ttk.Button(ctrl_frame, text="Select All", command=_select_all).pack(side=tk.LEFT) ttk.Button(ctrl_frame, text="Deselect All", command=_deselect_all).pack(side=tk.LEFT, padx=(5,0)) # Action buttons btn_frame = ttk.Frame(dlg) btn_frame.pack(fill=tk.X, padx=10, pady=10) result_holder: Dict[str, Optional[tuple]] = {"result": None} def _on_ok(): chosen_modules = [name for name, v in var_map.items() if v.get()] selected_datas: List[tuple] = [] for path, v in pkg_var_map.items(): if v.get(): # find package entry by path for pkg in packages: if pkg.get('path') == path: for src, dest in pkg.get('datas', []): selected_datas.append((src, dest)) # also include package deps into chosen_modules for d in pkg.get('deps', []): if d not in chosen_modules: chosen_modules.append(d) break result_holder['result'] = (chosen_modules, selected_datas) dlg.grab_release() dlg.destroy() def _on_cancel(): result_holder['result'] = None dlg.grab_release() dlg.destroy() ttk.Button(btn_frame, text="OK", command=_on_ok).pack(side=tk.RIGHT, padx=(5,0)) ttk.Button(btn_frame, text="Cancel", command=_on_cancel).pack(side=tk.RIGHT) # Center dialog over parent self.update_idletasks() dlg.update_idletasks() x = self.winfo_rootx() + (self.winfo_width() // 2) - (dlg.winfo_width() // 2) y = self.winfo_rooty() + (self.winfo_height() // 2) - (dlg.winfo_height() // 2) try: dlg.geometry(f"+{max(0,x)}+{max(0,y)}") except Exception: pass self.wait_window(dlg) return result_holder['result'] def _open_output_folder(self) -> None: """Open the project's output/dist folder in the OS file manager.""" try: project_dir = self.project_directory_path.get() if not project_dir: messagebox.showwarning("Open Output Folder", "No project directory selected.", parent=self) return dist_folder = pathlib.Path(project_dir) / config.DEFAULT_SPEC_OPTIONS['output_dir_name'] if not dist_folder.exists(): messagebox.showinfo("Open Output Folder", f"Output folder does not exist:\n{dist_folder}\n\nBuild first or create the folder manually.", parent=self) return # Platform-specific open if sys.platform.startswith('win'): os.startfile(str(dist_folder)) elif sys.platform == 'darwin': subprocess.run(['open', str(dist_folder)]) else: subprocess.run(['xdg-open', str(dist_folder)]) except Exception as e: self.output_logger.log_message(f"Failed to open output folder: {e}", level="ERROR") messagebox.showerror("Open Output Folder", f"Could not open output folder: {e}", parent=self)