# projectutility/gui/config_window.py import tkinter as tk from tkinter import ttk, messagebox import logging import os import threading import dataclasses # Needed for dataclasses.replace from typing import Dict, Any, Optional, Callable, List # --- Import Core Components --- GIT_ENABLED = False try: from projectutility.core.registry_models import ToolRegistryEntry # Try importing git_manager to determine availability and get constants from projectutility.core import git_manager GIT_ENABLED = git_manager.GITPYTHON_AVAILABLE # Import Git status constants (use real ones if Git is enabled) if GIT_ENABLED: # QUESTO BLOCCO IMPORTA LE COSTANTI REALI DA git_manager from projectutility.core.git_manager import ( GIT_STATUS_UP_TO_DATE, GIT_STATUS_BEHIND, GIT_STATUS_AHEAD, GIT_STATUS_DIVERGED, GIT_STATUS_NOT_CLONED, GIT_STATUS_ERROR, GIT_STATUS_CHECKING, GIT_STATUS_UPDATING, GIT_STATUS_GITPYTHON_MISSING, ) logging.getLogger(__name__).debug("Successfully imported core models and git_manager constants.") else: # QUESTO BLOCCO DEFINISCE LE COSTANTI FITTIZIE SE GIT_ENABLED È FALSE logging.getLogger(__name__).warning("GitPython not available via git_manager. Defining dummy Git status constants.") GIT_STATUS_UP_TO_DATE = "Up-to-date (N/A)" GIT_STATUS_BEHIND = "Behind (N/A)" GIT_STATUS_AHEAD = "Ahead (N/A)" GIT_STATUS_DIVERGED = "Diverged (N/A)" GIT_STATUS_NOT_CLONED = "Not Cloned (N/A)" GIT_STATUS_ERROR = "Error" GIT_STATUS_CHECKING = "Checking... (N/A)" GIT_STATUS_UPDATING = "Updating... (N/A)" GIT_STATUS_GITPYTHON_MISSING = "GitPython Missing" # This one is real except ImportError as e: # QUESTO BLOCCO DEFINISCE LE COSTANTI FITTIZIE SE L'IMPORT FALLISCE logging.getLogger(__name__).critical( f"Failed to import core modules (registry_models/git_manager): {e}. " f"ConfigWindow functionality will be limited.", exc_info=True ) CORE_AVAILABLE = False GIT_ENABLED = False # Define dummy model and constants if imports fail # ... (dummy ToolRegistryEntry) ... GIT_STATUS_UP_TO_DATE = "?" GIT_STATUS_BEHIND = "?" # ... (altre costanti fittizie) ... GIT_STATUS_GITPYTHON_MISSING = "N/A" # Use absolute imports from the 'projectutility' package CORE_AVAILABLE = True GIT_ENABLED = False try: from projectutility.core.registry_models import ToolRegistryEntry # Try importing git_manager to determine availability and get constants from projectutility.core import git_manager GIT_ENABLED = git_manager.GITPYTHON_AVAILABLE # Import Git status constants (use real ones if Git is enabled) if GIT_ENABLED: from projectutility.core.git_manager import ( GIT_STATUS_UP_TO_DATE, GIT_STATUS_BEHIND, GIT_STATUS_AHEAD, GIT_STATUS_DIVERGED, GIT_STATUS_NOT_CLONED, GIT_STATUS_ERROR, GIT_STATUS_CHECKING, GIT_STATUS_UPDATING, GIT_STATUS_GITPYTHON_MISSING, ) logging.getLogger(__name__).debug( "Successfully imported core models and git_manager constants." ) else: # GitPython not found via git_manager, define dummy constants logging.getLogger(__name__).warning( "GitPython not available via git_manager. Defining dummy Git status constants." ) GIT_STATUS_UP_TO_DATE = "Up-to-date (N/A)" GIT_STATUS_BEHIND = "Behind (N/A)" GIT_STATUS_AHEAD = "Ahead (N/A)" GIT_STATUS_DIVERGED = "Diverged (N/A)" GIT_STATUS_NOT_CLONED = "Not Cloned (N/A)" GIT_STATUS_ERROR = "Error" GIT_STATUS_CHECKING = "Checking... (N/A)" GIT_STATUS_UPDATING = "Updating... (N/A)" GIT_STATUS_GITPYTHON_MISSING = "GitPython Missing" # This one is real except ImportError as e: # Failed to import core components, this window will be severely limited logging.getLogger(__name__).critical( f"Failed to import core modules (registry_models/git_manager): {e}. " f"ConfigWindow functionality will be limited.", exc_info=True, ) CORE_AVAILABLE = False GIT_ENABLED = False # Define dummy model and constants if imports fail @dataclasses.dataclass class ToolRegistryEntry: id: str = "dummy" display_name: str = "Dummy (Import Failed)" type: str = "local" description: Optional[str] = None run_command: List[str] = dataclasses.field(default_factory=list) has_gui: bool = False enabled: bool = False git_url: Optional[str] = None git_ref: str = "main" parameters: Optional[List[Dict[str, Any]]] = None parameters_definition_file: Optional[str] = None GIT_STATUS_UP_TO_DATE = "?" GIT_STATUS_BEHIND = "?" GIT_STATUS_AHEAD = "?" GIT_STATUS_DIVERGED = "?" GIT_STATUS_NOT_CLONED = "?" GIT_STATUS_ERROR = "Error" GIT_STATUS_CHECKING = "?" GIT_STATUS_UPDATING = "?" GIT_STATUS_GITPYTHON_MISSING = "N/A" class ConfigWindow(tk.Toplevel): """ A modal dialog window for viewing and managing the configuration of a single tool entry from the registry. Allows editing common fields and provides controls for Git-based tools (status check, update trigger). """ def __init__( self, parent: tk.Tk, tool_id: str, tool_config: ToolRegistryEntry, git_status_func: Callable[ [str], Dict[str, Any] ], # Func from MainWindow to get Git status update_func: Callable[ [str], None ], # Func from MainWindow to trigger update thread save_registry_func: Callable[ [List[ToolRegistryEntry]], bool ], # Func to save registry get_full_registry_func: Callable[ [], Optional[List[ToolRegistryEntry]] ], # Func to get current registry ) -> None: """ Initializes the tool configuration window. Args: parent: The parent Tkinter window (usually the MainWindow root). tool_id: The unique ID of the tool being configured. tool_config: The ToolRegistryEntry object representing the current configuration of the tool to be displayed/edited. git_status_func: A callable (provided by MainWindow) that takes the tool_id and returns a dictionary with Git status information. update_func: A callable (provided by MainWindow) that takes the tool_id to trigger the background update process for that tool. save_registry_func: A callable (likely registry_manager.save_registry) that takes the full, modified list of registry entries and attempts to save it, returning True on success. get_full_registry_func: A callable (likely registry_manager.load_registry) that returns the current list of all ToolRegistryEntry objects, or None on failure. """ super().__init__(parent) self.parent = parent self.tool_id = tool_id # Keep a reference to the original config to compare changes if needed, # though currently we fetch the whole registry on save. self.original_config: ToolRegistryEntry = tool_config self.get_git_status = git_status_func self.trigger_update = update_func self.save_registry = save_registry_func self.get_full_registry = get_full_registry_func self.logger = logging.getLogger( f"{__name__}.{tool_id}" ) # Logger specific to this tool's config window self.logger.info(f"Opening configuration window for tool: {self.tool_id}") if not CORE_AVAILABLE: self.logger.error( "Core modules not available. Configuration options will be limited or non-functional." ) # Optionally show a message box here? # --- Window Setup --- self.title(f"Configure Tool: {self.original_config.display_name}") # Set initial size (adjust as needed) self.geometry("650x580") self.resizable(False, False) # Prevent resizing # Make the window modal (grab focus, block interaction with parent) self.transient(parent) # Associate with parent (e.g., stays on top) self.grab_set() # Direct all events to this window # self.focus_set() # Set focus to this window (usually automatic with grab_set) # --- Tkinter Variables for Editable Fields --- # Initialize variables with values from the passed tool_config self.display_name_var = tk.StringVar(value=self.original_config.display_name) self.description_var = tk.StringVar( value=self.original_config.description or "" ) # Use empty string if None self.enabled_var = tk.BooleanVar(value=self.original_config.enabled) # Git-specific editable field (only relevant if type is 'git') self.git_ref_var = tk.StringVar( value=self.original_config.git_ref or "main" ) # Default to 'main' if somehow None # --- Tkinter Variables for Displaying Git Status (Read-Only) --- self.git_status_var = tk.StringVar( value="-" if GIT_ENABLED else GIT_STATUS_GITPYTHON_MISSING ) self.git_local_hash_var = tk.StringVar(value="-") self.git_remote_hash_var = tk.StringVar(value="-") self.git_local_ref_var = tk.StringVar(value="-") # --- Build UI Widgets --- self._create_widgets() # --- Load Initial Git Status (if applicable) --- if self.original_config.type == "git" and GIT_ENABLED: self.logger.debug("Requesting initial Git status refresh.") self._refresh_git_status() elif not GIT_ENABLED and self.original_config.type == "git": self.logger.warning( "Cannot display Git status: GitPython library is missing." ) # Status var already set to GIT_STATUS_GITPYTHON_MISSING # --- Final Window Adjustments --- # Intercept window close button (top-right 'X') self.protocol("WM_DELETE_WINDOW", self._on_close) # Center the window relative to the parent self.update_idletasks() # Ensure window size is calculated self._center_window() # Wait for the window to be visible and events processed before releasing grab # self.wait_window(self) # This would block until destroyed - use grab_set instead. def _center_window(self) -> None: """Calculates and sets the window position to center it over the parent.""" try: self.update_idletasks() # Ensure winfo_* methods return correct values parent_x = self.parent.winfo_rootx() parent_y = self.parent.winfo_rooty() parent_width = self.parent.winfo_width() parent_height = self.parent.winfo_height() window_width = self.winfo_width() window_height = self.winfo_height() position_x = parent_x + (parent_width // 2) - (window_width // 2) position_y = parent_y + (parent_height // 2) - (window_height // 2) self.geometry(f"+{position_x}+{position_y}") self.logger.debug(f"Centered window at +{position_x}+{position_y}") except Exception as e: self.logger.warning(f"Could not center window: {e}") # Window will appear at default location def _create_widgets(self) -> None: """Creates and arranges all the widgets within the configuration window.""" # Main container frame with padding main_frame = ttk.Frame(self, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # Configure grid columns: Label column (0) fixed, Value column (1) expands main_frame.columnconfigure(0, weight=0) main_frame.columnconfigure(1, weight=1) row_idx = 0 # Keep track of the current grid row # --- Basic Info Section (Partially Editable) --- # Tool ID (Read-only) ttk.Label(main_frame, text="Tool ID:", font="-weight bold").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) ttk.Label(main_frame, text=self.tool_id).grid( row=row_idx, column=1, sticky="nw", padx=5, pady=3 ) row_idx += 1 # Display Name (Editable) ttk.Label(main_frame, text="Display Name:").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) ttk.Entry(main_frame, textvariable=self.display_name_var, width=60).grid( row=row_idx, column=1, sticky="ew", padx=5, pady=3 ) row_idx += 1 # Description (Editable) ttk.Label(main_frame, text="Description:").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) # Use a Text widget for potentially longer descriptions? For now, Entry. # Consider increasing width if needed. Max width limited by column 1 expansion. ttk.Entry(main_frame, textvariable=self.description_var, width=60).grid( row=row_idx, column=1, sticky="ew", padx=5, pady=3 ) # If using Text widget: # desc_text = tk.Text(main_frame, height=3, width=50, wrap=tk.WORD) # desc_text.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=3) # desc_text.insert(tk.END, self.description_var.get()) # # Need to retrieve value from Text widget on save: self.description_var.set(desc_text.get("1.0", tk.END).strip()) row_idx += 1 # Enabled Checkbox (Editable) ttk.Label(main_frame, text="Enabled:").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) ttk.Checkbutton( main_frame, variable=self.enabled_var, style="TCheckbutton" ).grid(row=row_idx, column=1, sticky="w", padx=5, pady=3) row_idx += 1 # Separator before type-specific section ttk.Separator(main_frame, orient="horizontal").grid( row=row_idx, column=0, columnspan=2, sticky="ew", pady=10 ) row_idx += 1 # --- Type-Specific Configuration Section --- if self.original_config.type == "git": self._create_git_config_widgets(main_frame, row_idx) elif self.original_config.type == "local": self._create_local_config_widgets(main_frame, row_idx) else: # Handle unknown/unsupported tool types ttk.Label( main_frame, text=f"Configuration for tool type '{self.original_config.type}' is not supported.", foreground="orange", font="-style italic", ).grid(row=row_idx, column=0, columnspan=2, sticky="w", padx=5, pady=5) # --- Action Buttons Frame --- # Place buttons at the bottom, aligned to the right button_frame = ttk.Frame( self, padding=(10, 5, 10, 10) ) # Padding: left, top, right, bottom button_frame.pack(fill=tk.X, side=tk.BOTTOM) # Make column 1 expand, pushing buttons (in col 1, 2) to the right button_frame.columnconfigure(0, weight=1) self.save_button = ttk.Button( button_frame, text="Save Changes", command=self._save_changes, style="Accent.TButton", # Optional style for emphasis ) self.save_button.grid(row=0, column=1, padx=5) self.close_button = ttk.Button( button_frame, text="Close", command=self._on_close, # Use the same close handler as WM_DELETE_WINDOW ) self.close_button.grid(row=0, column=2, padx=5) # Set initial state for save button (maybe disable until changes are made?) # self.save_button.config(state=tk.DISABLED) # Requires change tracking logic self.logger.debug("UI widgets created successfully.") def _create_git_config_widgets( self, parent_frame: ttk.Frame, start_row: int ) -> None: """Creates the widgets specific to Git tool configuration.""" self.logger.debug("Creating Git-specific configuration widgets.") row_idx = start_row # --- Git Repository Details (Read-Only and Editable) --- # Git URL (Read-Only) ttk.Label(parent_frame, text="Git URL:", font="-weight bold").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) # Wrap long URLs url_label = ttk.Label( parent_frame, text=self.original_config.git_url or "N/A", wraplength=450, anchor="w", ) url_label.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=3) row_idx += 1 # Git Ref (Branch/Tag/Commit) (Editable) ttk.Label(parent_frame, text="Git Ref:").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) ttk.Entry(parent_frame, textvariable=self.git_ref_var, width=40).grid( row=row_idx, column=1, sticky="ew", padx=5, pady=3 ) row_idx += 1 # --- Other Registry Info (Read-Only in this window) --- # Run Command (Read-Only) ttk.Label(parent_frame, text="Run Command:").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) run_cmd_text = ( " ".join(self.original_config.run_command) if self.original_config.run_command else "N/A" ) ttk.Label(parent_frame, text=run_cmd_text, wraplength=450).grid( row=row_idx, column=1, sticky="ew", padx=5, pady=3 ) row_idx += 1 # Has GUI (Read-Only) ttk.Label(parent_frame, text="Has GUI:").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) ttk.Label(parent_frame, text=str(self.original_config.has_gui)).grid( row=row_idx, column=1, sticky="w", padx=5, pady=3 ) row_idx += 1 # Parameter Definition File (Read-Only) ttk.Label(parent_frame, text="Params File:").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) params_file_text = self.original_config.parameters_definition_file or "None" ttk.Label(parent_frame, text=params_file_text).grid( row=row_idx, column=1, sticky="w", padx=5, pady=3 ) row_idx += 1 # --- Git Status Section --- # Separator before status section ttk.Separator(parent_frame, orient="horizontal").grid( row=row_idx, column=0, columnspan=2, sticky="ew", pady=10 ) row_idx += 1 # LabelFrame for Git Status git_status_frame = ttk.LabelFrame( parent_frame, text="Git Repository Status", padding="10" ) git_status_frame.grid( row=row_idx, column=0, columnspan=2, sticky="ew", padx=5, pady=5 ) # Configure status frame columns git_status_frame.columnconfigure(0, weight=0) git_status_frame.columnconfigure(1, weight=1) # Allow value labels to expand s_row = 0 # Row index within the status frame # Status Label ttk.Label(git_status_frame, text="Status:").grid( row=s_row, column=0, sticky="nw", padx=5, pady=2 ) status_value_label = ttk.Label( git_status_frame, textvariable=self.git_status_var, font="-weight bold", wraplength=400, ) status_value_label.grid(row=s_row, column=1, sticky="ew", padx=5, pady=2) s_row += 1 # Local Ref Label ttk.Label(git_status_frame, text="Local Ref:").grid( row=s_row, column=0, sticky="nw", padx=5, pady=2 ) ttk.Label(git_status_frame, textvariable=self.git_local_ref_var).grid( row=s_row, column=1, sticky="w", padx=5, pady=2 ) s_row += 1 # Local Hash Label ttk.Label(git_status_frame, text="Local Hash:").grid( row=s_row, column=0, sticky="nw", padx=5, pady=2 ) ttk.Label(git_status_frame, textvariable=self.git_local_hash_var).grid( row=s_row, column=1, sticky="w", padx=5, pady=2 ) s_row += 1 # Remote Ref Label (uses the *original* configured git_ref) ttk.Label(git_status_frame, text="Remote Ref:").grid( row=s_row, column=0, sticky="nw", padx=5, pady=2 ) ttk.Label(git_status_frame, text=f"origin/{self.original_config.git_ref}").grid( row=s_row, column=1, sticky="w", padx=5, pady=2 ) s_row += 1 # Remote Hash Label ttk.Label(git_status_frame, text="Remote Hash:").grid( row=s_row, column=0, sticky="nw", padx=5, pady=2 ) ttk.Label(git_status_frame, textvariable=self.git_remote_hash_var).grid( row=s_row, column=1, sticky="w", padx=5, pady=2 ) s_row += 1 # --- Git Action Buttons --- git_action_frame = ttk.Frame(git_status_frame) # Place below the status labels, span columns if needed, add top padding git_action_frame.grid( row=s_row, column=0, columnspan=2, sticky="ew", pady=(10, 0) ) # Align buttons within the action frame (e.g., to the right) git_action_frame.columnconfigure(0, weight=1) # Push buttons right self.refresh_status_button = ttk.Button( git_action_frame, text="Refresh Status", command=self._refresh_git_status, state=( tk.NORMAL if GIT_ENABLED else tk.DISABLED ), # Disable if GitPython missing ) self.refresh_status_button.grid(row=0, column=1, padx=5) self.update_button = ttk.Button( git_action_frame, text="Update This Tool", command=self._trigger_single_update, state=tk.DISABLED, # Initially disabled until status check confirms update needed ) self.update_button.grid(row=0, column=2, padx=5) # If GitPython is missing, add a message if not GIT_ENABLED: ttk.Label( git_status_frame, text="GitPython library not installed.", foreground="red", ).grid( row=s_row + 1, column=0, columnspan=2, sticky="w", padx=5, pady=(5, 0) ) def _create_local_config_widgets( self, parent_frame: ttk.Frame, start_row: int ) -> None: """Creates the widgets specific to Local tool configuration.""" self.logger.debug("Creating Local-specific configuration widgets.") row_idx = start_row # Run Command (Read-Only) ttk.Label(parent_frame, text="Run Command:", font="-weight bold").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) run_cmd_text = ( " ".join(self.original_config.run_command) if self.original_config.run_command else "N/A" ) ttk.Label(parent_frame, text=run_cmd_text, wraplength=450).grid( row=row_idx, column=1, sticky="ew", padx=5, pady=3 ) row_idx += 1 # Has GUI (Read-Only) ttk.Label(parent_frame, text="Has GUI:").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) ttk.Label(parent_frame, text=str(self.original_config.has_gui)).grid( row=row_idx, column=1, sticky="w", padx=5, pady=3 ) row_idx += 1 # Parameters Source Info (Read-Only) ttk.Label(parent_frame, text="Parameters:", font="-weight bold").grid( row=row_idx, column=0, sticky="nw", padx=5, pady=3 ) # Check if inline parameters are defined in the registry entry params_text = ( "Defined inline in registry." if self.original_config.parameters else "None defined." ) ttk.Label(parent_frame, text=params_text, style="Italic.TLabel").grid( row=row_idx, column=1, sticky="w", padx=5, pady=3 ) # Note: Local tools don't use parameters_definition_file currently. def _refresh_git_status(self) -> None: """ Requests and updates the display of the Git repository status. Uses the `git_status_func` passed during initialization, executing it in a separate thread to avoid blocking the GUI. Updates UI elements to indicate loading state. """ if self.original_config.type != "git" or not GIT_ENABLED: self.logger.debug( "Skipping Git status refresh: Not a Git tool or GitPython unavailable." ) return self.logger.info(f"Requesting Git status refresh for tool: {self.tool_id}") # Update UI to show "Checking..." state and disable buttons self.git_status_var.set(GIT_STATUS_CHECKING) self.git_local_hash_var.set("...") self.git_remote_hash_var.set("...") self.git_local_ref_var.set("...") # Disable buttons during check if hasattr(self, "update_button"): # Check attribute exists self.update_button.config(state=tk.DISABLED) if hasattr(self, "refresh_status_button"): self.refresh_status_button.config(state=tk.DISABLED) # Force GUI update to show changes immediately self.update_idletasks() # --- Execute status check in a background thread --- # This prevents the GUI from freezing during potentially slow network operations (fetch). thread = threading.Thread( target=self._get_status_background, daemon=True, # Allows program to exit even if thread is running name=f"{self.tool_id}_git_status_check", ) thread.start() self.logger.debug("Git status check thread started.") def _get_status_background(self) -> None: """ Background task executed by the status refresh thread. Calls the `get_git_status` function (provided by MainWindow) and schedules the `_update_status_display` method to run on the main GUI thread with the results. """ self.logger.debug("Background thread: Calling get_git_status function.") status_info: Dict[str, Any] = { "status": GIT_STATUS_ERROR, "message": "Failed to get status from background task.", # Ensure default keys exist "local_hash": None, "remote_hash": None, "local_ref": None, "remote_ref": None, } # Default error state try: # Call the function passed from MainWindow (which calls git_manager) status_info = self.get_git_status(self.tool_id) self.logger.debug(f"Background thread: Received status_info: {status_info}") except Exception as e: # Catch any error occurring within the passed get_git_status function self.logger.exception( f"Error occurred while calling the provided get_git_status function " f"for tool '{self.tool_id}' in background thread." ) status_info["status"] = GIT_STATUS_ERROR # Ensure status reflects error status_info["message"] = f"Error executing status function: {e}" # --- Schedule GUI update on the main thread --- # Use `self.after(0, ...)` to safely queue the GUI update function # to be executed by the Tkinter event loop from the main thread. # Pass the retrieved status_info dictionary to the update function. try: self.after(0, self._update_status_display, status_info) self.logger.debug("Scheduled _update_status_display on main thread.") except tk.TclError: self.logger.warning( "Could not schedule GUI update (window likely closing)." ) except Exception as e: self.logger.exception( "Unexpected error scheduling GUI update from background thread." ) def _update_status_display(self, status_info: Dict[str, Any]) -> None: """ Updates the GUI widgets with the Git status information received. This method is executed on the main GUI thread via `self.after()`. Args: status_info: The dictionary containing Git status details returned by the `get_git_status` function. """ if not self.winfo_exists(): # Check if window still exists self.logger.warning( "Cannot update status display: ConfigWindow no longer exists." ) return self.logger.info( f"Updating Git status display for '{self.tool_id}': {status_info}" ) # Extract status details safely using .get() with defaults status = status_info.get("status", GIT_STATUS_ERROR) message = status_info.get("message", "No message provided.") local_hash = status_info.get("local_hash") remote_hash = status_info.get("remote_hash") local_ref = status_info.get("local_ref") # Format values for display status_display = f"{status} - {message}" local_hash_display = ( str(local_hash)[:12] if local_hash else "-" ) # Show short hash or "-" remote_hash_display = str(remote_hash)[:12] if remote_hash else "-" local_ref_display = str(local_ref) if local_ref else "-" # Update Tkinter StringVars bound to the labels self.git_status_var.set(status_display) self.git_local_hash_var.set(local_hash_display) self.git_remote_hash_var.set(remote_hash_display) self.git_local_ref_var.set(local_ref_display) # Enable/disable action buttons based on the status can_update = status in [ GIT_STATUS_BEHIND, GIT_STATUS_DIVERGED, GIT_STATUS_NOT_CLONED, # Allow update attempt even if not cloned (it will clone first) ] update_button_state = tk.NORMAL if GIT_ENABLED and can_update else tk.DISABLED refresh_button_state = tk.NORMAL if GIT_ENABLED else tk.DISABLED if hasattr(self, "update_button"): self.update_button.config(state=update_button_state) if hasattr(self, "refresh_status_button"): self.refresh_status_button.config(state=refresh_button_state) self.logger.debug( f"Git status display updated. Update possible: {can_update}, Git enabled: {GIT_ENABLED}" ) def _trigger_single_update(self) -> None: """ Initiates the update process for this specific tool. Calls the `update_func` (provided by MainWindow) which is responsible for starting the actual update logic (likely in a background thread). Updates UI elements to show "Updating..." state. """ if not GIT_ENABLED: messagebox.showerror("Git Error", GIT_STATUS_GITPYTHON_MISSING, parent=self) return # Get the target Git reference from the editable field target_ref = self.git_ref_var.get() if not target_ref: messagebox.showerror( "Validation Error", "Git Ref cannot be empty.", parent=self ) return # Confirm with the user before starting the update # Include the target ref in the confirmation message confirm_msg = ( f"This will attempt to update the tool '{self.original_config.display_name}' " f"to match the remote reference '{target_ref}'.\n\n" f"Proceed with update?" ) if messagebox.askyesno("Confirm Update", confirm_msg, parent=self): self.logger.info( f"User confirmed. Triggering single update for tool '{self.tool_id}' to ref '{target_ref}'." ) # --- Update UI to 'Updating...' state --- self.git_status_var.set(GIT_STATUS_UPDATING) # Disable all action buttons during update if hasattr(self, "update_button"): self.update_button.config(state=tk.DISABLED) if hasattr(self, "refresh_status_button"): self.refresh_status_button.config(state=tk.DISABLED) if hasattr(self, "save_button"): # Also disable save during update self.save_button.config(state=tk.DISABLED) self.update_idletasks() # Show changes immediately try: # --- Call the update function passed from MainWindow --- # This function is expected to handle launching the background thread # in MainWindow's context. # Note: We might need to save the changed git_ref *before* triggering # the update if the update process itself relies on the latest registry state. # Current git_manager.update_repository takes ToolRegistryEntry, which should # reflect the desired state (including the potentially changed git_ref). # Let's ensure we save the ref change *first*. # --- Modification: Save ref change before triggering update --- config_changed = False if self.original_config.git_ref != target_ref: self.logger.info( f"Git ref changed from '{self.original_config.git_ref}' to '{target_ref}'. Saving before update." ) # We need to save just this change without validating/saving others yet. # Option 1: Modify the original_config temporarily (not ideal if immutable) # Option 2: Pass the target_ref explicitly to the update function? No, update needs registry state. # Option 3: Perform a limited save of just the ref change now. if self._save_specific_field("git_ref", target_ref): # Update the original_config reference in memory as well if save successful self.original_config = dataclasses.replace( self.original_config, git_ref=target_ref ) config_changed = True else: # If saving the ref change failed, abort the update. messagebox.showerror( "Save Error", "Could not save the updated Git Ref to the registry before updating. Aborting update.", parent=self, ) self._refresh_git_status() # Refresh status to reset UI if hasattr(self, "save_button"): self.save_button.config(state=tk.NORMAL) # Re-enable save return # Now, trigger the update using the (potentially updated) config self.trigger_update(self.tool_id) self.logger.debug(f"Update function called for tool {self.tool_id}.") # The status display will be updated later when MainWindow processes # the results from the update thread via the queue. # This window doesn't know directly when the update finishes, # but refreshing the status later will show the result. except Exception as e: self.logger.exception( f"Error occurred while triggering the update for tool '{self.tool_id}'." ) messagebox.showerror( "Update Error", f"Could not start update:\n{e}", parent=self ) # Re-enable buttons on error triggering update self._refresh_git_status() # Refresh status to reset UI if hasattr(self, "save_button"): self.save_button.config(state=tk.NORMAL) # Re-enable save def _save_changes(self) -> None: """ Gathers changes from the UI fields, validates them, updates the registry list in memory, and calls the save_registry function. """ self.logger.info( f"Attempting to save configuration changes for tool: {self.tool_id}" ) # --- Gather Current Values from Widgets --- try: new_display_name = self.display_name_var.get() new_description = self.description_var.get() # May be empty string new_enabled = self.enabled_var.get() # Get Git ref only if it's a Git tool, otherwise keep original new_git_ref = ( self.git_ref_var.get() if self.original_config.type == "git" else self.original_config.git_ref ) except tk.TclError as e: self.logger.error(f"Error reading value from Tkinter variable: {e}") messagebox.showerror( "Input Error", "Could not read input values.", parent=self ) return # --- Validate Input --- errors = [] if not new_display_name.strip(): errors.append("Display Name cannot be empty.") if self.original_config.type == "git" and not new_git_ref.strip(): errors.append("Git Ref cannot be empty for Git tools.") # Add more validation rules here if needed if errors: error_message = "Please correct the following errors:\n\n- " + "\n- ".join( errors ) messagebox.showerror("Validation Error", error_message, parent=self) return # --- Update Registry Data --- self.logger.debug("Loading current full registry to apply changes...") full_registry: Optional[List[ToolRegistryEntry]] = self.get_full_registry() if full_registry is None: # This means the get_full_registry_func (load_registry) failed messagebox.showerror( "Error", "Could not load the tool registry. Cannot save changes.", parent=self, ) return updated = False for i, entry in enumerate(full_registry): if entry.id == self.tool_id: self.logger.debug( f"Found registry entry for tool '{self.tool_id}' at index {i}." ) try: # Create a *new* ToolRegistryEntry object with the updated values # using dataclasses.replace() for immutability benefits if frozen=True was used. # If not frozen, direct attribute assignment on 'entry' would also work, # but 'replace' is cleaner and safer. updated_entry = dataclasses.replace( entry, # Start with the existing entry's data # Apply changes from the UI fields: display_name=new_display_name, description=( new_description if new_description else None ), # Store None if empty enabled=new_enabled, # Only update git_ref if it's a Git tool git_ref=new_git_ref if entry.type == "git" else entry.git_ref, ) # Replace the old entry with the updated one in the list full_registry[i] = updated_entry updated = True self.logger.info( f"Registry entry for '{self.tool_id}' updated in memory." ) break # Stop searching once the entry is found and updated except Exception as e: # Catch errors during dataclasses.replace or list modification self.logger.exception( f"Error creating updated registry entry object for '{self.tool_id}': {e}" ) messagebox.showerror( "Internal Error", f"Failed to prepare updated tool configuration object:\n{e}", parent=self, ) return # Abort save if not updated: # This should not happen if the window was opened correctly messagebox.showerror( "Error", f"Could not find tool '{self.tool_id}' in the loaded registry. " f"Cannot save changes.", parent=self, ) return # --- Save Updated Registry --- self.logger.debug("Calling save_registry function with the modified list...") save_successful = self.save_registry(full_registry) if save_successful: self.logger.info("Registry saved successfully via provided function.") messagebox.showinfo( "Success", "Tool configuration saved successfully.\n\n" "The tool list in the main window will be refreshed.", parent=self, ) # Close this configuration window after successful save self._on_close() # Use the close handler # MainWindow needs to be notified to reload tools. # This is currently handled implicitly because load_registry() will be called # again next time discover_tools() runs. If immediate refresh is needed, # MainWindow would need a specific method called here. else: # The save_registry function itself should have logged the specific error self.logger.error( "Failed to save the updated registry file (see previous logs)." ) messagebox.showerror( "Error Saving Registry", "Failed to save the updated configuration to the registry file.\n" "Please check the application logs for more details.", parent=self, ) # Keep the config window open on save failure? Yes. def _save_specific_field(self, field_name: str, new_value: Any) -> bool: """ Helper to save only a specific field change to the registry. Used internally before triggering an update if the Git ref changed. Args: field_name: The name of the attribute in ToolRegistryEntry to update. new_value: The new value for the field. Returns: True if the specific field was saved successfully, False otherwise. """ self.logger.info( f"Attempting to save specific field '{field_name}' = '{new_value}' for tool '{self.tool_id}'." ) full_registry = self.get_full_registry() if full_registry is None: self.logger.error("Could not load registry to save specific field.") return False updated = False for i, entry in enumerate(full_registry): if entry.id == self.tool_id: try: # Use replace to create the updated entry updated_entry = dataclasses.replace( entry, **{field_name: new_value} ) full_registry[i] = updated_entry updated = True break except Exception as e: self.logger.exception( f"Error using dataclasses.replace for field '{field_name}': {e}" ) return False if not updated: self.logger.error( f"Could not find tool '{self.tool_id}' in registry to save specific field." ) return False # Save the entire modified registry return self.save_registry(full_registry) def _on_close(self) -> None: """Handles window closing actions (via button or WM_DELETE_WINDOW).""" self.logger.info(f"Closing configuration window for tool: {self.tool_id}") # Perform any necessary cleanup here, though usually just destroying is enough. # Release the grab explicitly before destroying? Optional. # self.grab_release() self.destroy() # Close the Toplevel window