# gui.py import tkinter as tk from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog import logging import os # Import os for path operations # Import constant from the central location from config_manager import DEFAULT_BACKUP_DIR # --- Tooltip Class Definition --- class Tooltip: """Simple tooltip implementation for Tkinter widgets.""" def __init__(self, widget, text): self.widget = widget self.text = text self.tooltip_window = None self.id = None self.x = self.y = 0 def showtip(self): """Display text in a tooltip window.""" self.hidetip() # Hide any existing tooltip first if not self.widget.winfo_exists(): return # Avoid error if widget destroyed try: # Get widget position relative to screen x, y, _, _ = self.widget.bbox("insert") # Get widget location relative to widget itself x += self.widget.winfo_rootx() + 25 # Add screen coordinates and offset y += self.widget.winfo_rooty() + 25 except tk.TclError: # Handle cases where bbox might fail (e.g., widget not visible) # Fallback position calculation x = self.widget.winfo_rootx() + self.widget.winfo_width() // 2 y = self.widget.winfo_rooty() + self.widget.winfo_height() + 5 self.tooltip_window = tw = tk.Toplevel(self.widget) # Create new top-level window tw.wm_overrideredirect(True) # Remove window decorations (border, title bar) tw.wm_geometry(f"+{int(x)}+{int(y)}") # Position the window (ensure integer coordinates) label = tk.Label(tw, text=self.text, justify=tk.LEFT, background="#ffffe0", relief=tk.SOLID, borderwidth=1, # Light yellow background font=("tahoma", "8", "normal")) label.pack(ipadx=1) def hidetip(self): """Hide the tooltip window.""" tw = self.tooltip_window self.tooltip_window = None if tw: try: if tw.winfo_exists(): tw.destroy() except tk.TclError: # Handle cases where window might already be destroyed pass # --- End Tooltip Class --- # --- Gitignore Editor Window Class --- class GitignoreEditorWindow(tk.Toplevel): """ A Toplevel window for editing the .gitignore file. """ def __init__(self, master, gitignore_path, logger): """ Initializes the editor window. Args: master (tk.Widget): The parent widget (usually the main app window). gitignore_path (str): The full path to the .gitignore file. logger (logging.Logger): Logger instance for logging actions. """ super().__init__(master) self.gitignore_path = gitignore_path self.logger = logger self.original_content = "" # Store original content to check for changes # --- Window Configuration --- self.title(f"Edit {os.path.basename(gitignore_path)}") self.geometry("600x450") # Set initial size, slightly larger height self.minsize(400, 300) # Set minimum size # Make window modal (grab focus) self.grab_set() # Make window appear on top of the master window self.transient(master) # Handle closing via window manager (X button) self.protocol("WM_DELETE_WINDOW", self._on_close) # --- Widgets --- # Main frame main_frame = ttk.Frame(self, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # Configure grid weights for resizing main_frame.rowconfigure(0, weight=1) # Text editor row expands main_frame.columnconfigure(0, weight=1) # Text editor column expands # ScrolledText widget for editing self.text_editor = scrolledtext.ScrolledText( main_frame, wrap=tk.WORD, font=("Consolas", 10), # Use a suitable font undo=True # Enable undo/redo ) self.text_editor.grid(row=0, column=0, sticky="nsew", pady=(0, 10)) # Use grid, expand in all directions # Button frame (using grid within main_frame) button_frame = ttk.Frame(main_frame) button_frame.grid(row=1, column=0, sticky="ew") # Place below text editor, stretch horizontally # Center buttons within the button frame button_frame.columnconfigure(0, weight=1) # Make space on the left button_frame.columnconfigure(3, weight=1) # Make space on the right # Save button self.save_button = ttk.Button(button_frame, text="Save and Close", command=self._save_and_close) self.save_button.grid(row=0, column=2, padx=5) # Place in middle-right column # Cancel button self.cancel_button = ttk.Button(button_frame, text="Cancel", command=self._on_close) self.cancel_button.grid(row=0, column=1, padx=5) # Place in middle-left column # --- Load File Content --- self._load_file() # Center window relative to parent (call after widgets are created) self._center_window(master) # Set focus to the text editor self.text_editor.focus_set() def _center_window(self, parent): """Centers the window relative to its parent.""" self.update_idletasks() # Ensure window size is calculated parent_x = parent.winfo_rootx() parent_y = parent.winfo_rooty() parent_width = parent.winfo_width() parent_height = parent.winfo_height() win_width = self.winfo_width() win_height = self.winfo_height() # Calculate position, ensuring it stays within screen bounds (basic check) x_pos = parent_x + (parent_width // 2) - (win_width // 2) y_pos = parent_y + (parent_height // 2) - (win_height // 2) # Adjust if going off-screen (simple version) screen_width = self.winfo_screenwidth() screen_height = self.winfo_screenheight() x_pos = max(0, min(x_pos, screen_width - win_width)) y_pos = max(0, min(y_pos, screen_height - win_height)) self.geometry(f"+{int(x_pos)}+{int(y_pos)}") def _load_file(self): """Loads the content of the .gitignore file into the editor.""" self.logger.info(f"Loading content for: {self.gitignore_path}") try: if os.path.exists(self.gitignore_path): with open(self.gitignore_path, 'r', encoding='utf-8', errors='replace') as f: self.original_content = f.read() self.text_editor.delete("1.0", tk.END) # Clear previous content self.text_editor.insert(tk.END, self.original_content) self.text_editor.edit_reset() # Reset undo stack after loading self.logger.debug(".gitignore content loaded successfully.") else: self.logger.info(f"'{self.gitignore_path}' does not exist. Editor is empty.") self.original_content = "" self.text_editor.delete("1.0", tk.END) self.text_editor.edit_reset() except IOError as e: self.logger.error(f"Error reading {self.gitignore_path}: {e}", exc_info=True) messagebox.showerror("Error Reading File", f"Could not read the .gitignore file:\n{e}", parent=self) except Exception as e: self.logger.exception(f"Unexpected error loading {self.gitignore_path}: {e}") messagebox.showerror("Unexpected Error", f"An unexpected error occurred while loading the file:\n{e}", parent=self) def _save_file(self): """Saves the current content of the editor to the .gitignore file.""" # Get content, ensure it ends with a single newline if not empty current_content = self.text_editor.get("1.0", tk.END).rstrip() if current_content: current_content += "\n" # Normalize original content similarly for comparison normalized_original = self.original_content.rstrip() if normalized_original: normalized_original += "\n" if current_content == normalized_original: self.logger.info("No changes detected in .gitignore content. Skipping save.") return True # Indicate success (no save needed) self.logger.info(f"Saving changes to: {self.gitignore_path}") try: # Ensure directory exists before writing (though unlikely needed for .gitignore) # os.makedirs(os.path.dirname(self.gitignore_path), exist_ok=True) with open(self.gitignore_path, 'w', encoding='utf-8', newline='\n') as f: # Use newline='\n' for consistency f.write(current_content) self.logger.info(".gitignore file saved successfully.") self.original_content = current_content # Update original content after save self.text_editor.edit_reset() # Reset undo stack after saving return True # Indicate success except IOError as e: self.logger.error(f"Error writing {self.gitignore_path}: {e}", exc_info=True) messagebox.showerror("Error Saving File", f"Could not save the .gitignore file:\n{e}", parent=self) return False # Indicate failure except Exception as e: self.logger.exception(f"Unexpected error saving {self.gitignore_path}: {e}") messagebox.showerror("Unexpected Error", f"An unexpected error occurred while saving the file:\n{e}", parent=self) return False def _save_and_close(self): """Saves the file and closes the window if save is successful.""" if self._save_file(): self.destroy() # Close window only if save succeeded or no changes def _on_close(self): """Handles closing the window (Cancel button or X button).""" # Check if content changed current_content = self.text_editor.get("1.0", tk.END).rstrip() if current_content: current_content += "\n" normalized_original = self.original_content.rstrip() if normalized_original: normalized_original += "\n" if current_content != normalized_original: # Use askyesnocancel for three options response = messagebox.askyesnocancel("Unsaved Changes", "You have unsaved changes.\nSave before closing?", parent=self) if response is True: # Yes, save and close self._save_and_close() # This handles save status and closes if successful elif response is False: # No, discard and close self.logger.warning("Discarding unsaved changes in .gitignore editor.") self.destroy() # Else (Cancel): Do nothing, keep window open else: # No changes, just close self.destroy() # --- End Gitignore Editor Window --- class MainFrame(ttk.Frame): """ The main frame containing all GUI elements for the Git SVN Sync Tool. Manages widget creation, layout, and basic GUI state updates. Callbacks for actions are provided by the main application controller. """ # Define constants for colors used in the GUI GREEN = "#90EE90" # Light Green for success/prepared status RED = "#F08080" # Light Coral for error/unprepared status def __init__(self, master, load_profile_settings_cb, browse_folder_cb, update_svn_status_cb, prepare_svn_for_git_cb, create_git_bundle_cb, fetch_from_git_bundle_cb, config_manager_instance, profile_sections_list, add_profile_cb, remove_profile_cb, manual_backup_cb, open_gitignore_editor_cb): # Added callback """ Initializes the MainFrame. Args: master (tk.Tk or ttk.Frame): The parent widget. load_profile_settings_cb (callable): Called when profile selection changes. browse_folder_cb (callable): Called by Browse buttons. update_svn_status_cb (callable): Called when SVN path might change. prepare_svn_for_git_cb (callable): Called by 'Prepare SVN' button. create_git_bundle_cb (callable): Called by 'Create Bundle' button. fetch_from_git_bundle_cb (callable): Called by 'Fetch Bundle' button. config_manager_instance (ConfigManager): Instance to access config data. profile_sections_list (list): Initial list of profile names. add_profile_cb (callable): Called by 'Add Profile' button. remove_profile_cb (callable): Called by 'Remove Profile' button. manual_backup_cb (callable): Called by 'Backup Now' button. open_gitignore_editor_cb (callable): Called by 'Edit .gitignore' button. """ super().__init__(master) self.master = master # Store callbacks provided by the controller (GitSvnSyncApp) self.load_profile_settings_callback = load_profile_settings_cb self.browse_folder_callback = browse_folder_cb self.update_svn_status_callback = update_svn_status_cb self.prepare_svn_for_git_callback = prepare_svn_for_git_cb self.create_git_bundle_callback = create_git_bundle_cb self.fetch_from_git_bundle_callback = fetch_from_git_bundle_cb self.add_profile_callback = add_profile_cb self.remove_profile_callback = remove_profile_cb self.manual_backup_callback = manual_backup_cb self.open_gitignore_editor_callback = open_gitignore_editor_cb # Store callback # Store config manager and initial profiles if needed locally self.config_manager = config_manager_instance self.initial_profile_sections = profile_sections_list # Style configuration self.style = ttk.Style() self.style.theme_use('clam') # Example theme # Pack the main frame self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10) # --- Tkinter Variables --- self.profile_var = tk.StringVar() self.autobackup_var = tk.BooleanVar() self.backup_dir_var = tk.StringVar() self.autocommit_var = tk.BooleanVar() self.commit_message_var = tk.StringVar() # For custom commit message self.backup_exclude_extensions_var = tk.StringVar() # For backup exclusions # --- Widget Creation --- self._create_profile_frame() self._create_repo_frame() # Modified self._create_backup_frame() self._create_function_frame() self._create_log_area() # --- Initial State Configuration --- self._initialize_profile_selection() self.toggle_backup_dir() # Initial status update is handled by the controller after loading profile def _create_profile_frame(self): """Creates the frame for profile selection and management.""" self.profile_frame = ttk.LabelFrame(self, text="Profile Configuration", padding=(10, 5)) self.profile_frame.pack(pady=5, fill="x") ttk.Label(self.profile_frame, text="Profile:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.profile_dropdown = ttk.Combobox( self.profile_frame, textvariable=self.profile_var, state="readonly", # Prevent typing custom values width=35, # Adjust width as needed values=self.initial_profile_sections # Set initial values ) self.profile_dropdown.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=5) # EW = stretch horizontally # When selection changes, call the controller's load function self.profile_dropdown.bind("<>", lambda event: self.load_profile_settings_callback(self.profile_var.get())) # Also trace the variable for programmatic changes self.profile_var.trace_add("write", lambda *args: self.load_profile_settings_callback(self.profile_var.get())) # Profile management buttons self.add_profile_button = ttk.Button(self.profile_frame, text="Add", width=5, command=self.add_profile_callback) self.add_profile_button.grid(row=0, column=2, sticky=tk.W, padx=(5, 0), pady=5) self.remove_profile_button = ttk.Button(self.profile_frame, text="Remove", width=8, command=self.remove_profile_callback) self.remove_profile_button.grid(row=0, column=3, sticky=tk.W, padx=(2, 5), pady=5) # Allow the dropdown column to expand horizontally self.profile_frame.columnconfigure(1, weight=1) def _create_repo_frame(self): """Creates the frame for repository paths, bundle names, and commit message.""" self.repo_frame = ttk.LabelFrame(self, text="Repository Configuration", padding=(10, 5)) self.repo_frame.pack(pady=5, fill="x") # Define column indices for clarity and easier adjustment col_label = 0 col_entry = 1 col_entry_span = 1 # Entry widgets usually span 1 logical column col_button1 = col_entry + col_entry_span # Column index for first button (e.g., Browse) col_button2 = col_button1 + 1 # Column index for second button (e.g., Edit .gitignore) col_indicator = col_button2 + 1 # Column index for status indicator (at the far right) # Configure grid columns weights self.repo_frame.columnconfigure(col_entry, weight=1) # Allow main entry fields to expand # Row 0: SVN Path ttk.Label(self.repo_frame, text="SVN Working Copy:").grid(row=0, column=col_label, sticky=tk.W, padx=5, pady=3) self.svn_path_entry = ttk.Entry(self.repo_frame, width=60) # Span entry up to the first button column self.svn_path_entry.grid(row=0, column=col_entry, columnspan=col_button1 - col_entry, sticky=tk.EW, padx=5, pady=3) self.svn_path_entry.bind("", lambda e: self.update_svn_status_callback(self.svn_path_entry.get())) self.svn_path_entry.bind("", lambda e: self.update_svn_status_callback(self.svn_path_entry.get())) self.svn_path_browse_button = ttk.Button( self.repo_frame, text="Browse...", width=9, command=lambda: self.browse_folder_callback(self.svn_path_entry) ) # Place browse button in its designated column self.svn_path_browse_button.grid(row=0, column=col_button1, sticky=tk.W, padx=(0, 5), pady=3) # Place indicator at the far right self.svn_status_indicator = tk.Label(self.repo_frame, text="", width=2, height=1, relief=tk.SUNKEN, background=self.RED, anchor=tk.CENTER) self.svn_status_indicator.grid(row=0, column=col_indicator, sticky=tk.E, padx=(0, 5), pady=3) self.create_tooltip(self.svn_status_indicator, "Indicates if '.git' folder exists (Green=Yes, Red=No)") # Row 1: USB/Bundle Target Path ttk.Label(self.repo_frame, text="Bundle Target Dir:").grid(row=1, column=col_label, sticky=tk.W, padx=5, pady=3) self.usb_path_entry = ttk.Entry(self.repo_frame, width=60) # Span entry up to the browse button self.usb_path_entry.grid(row=1, column=col_entry, columnspan=col_button1 - col_entry, sticky=tk.EW, padx=5, pady=3) self.usb_path_browse_button = ttk.Button( self.repo_frame, text="Browse...", width=9, command=lambda: self.browse_folder_callback(self.usb_path_entry) ) self.usb_path_browse_button.grid(row=1, column=col_button1, sticky=tk.W, padx=(0, 5), pady=3) # Row 2: Create Bundle Name ttk.Label(self.repo_frame, text="Create Bundle Name:").grid(row=2, column=col_label, sticky=tk.W, padx=5, pady=3) self.bundle_name_entry = ttk.Entry(self.repo_frame, width=60) # Span entry across its column and button columns if needed (up to indicator) self.bundle_name_entry.grid(row=2, column=col_entry, columnspan=(col_indicator - col_entry), sticky=tk.EW, padx=5, pady=3) # Row 3: Fetch Bundle Name ttk.Label(self.repo_frame, text="Fetch Bundle Name:").grid(row=3, column=col_label, sticky=tk.W, padx=5, pady=3) self.bundle_updated_name_entry = ttk.Entry(self.repo_frame, width=60) self.bundle_updated_name_entry.grid(row=3, column=col_entry, columnspan=(col_indicator - col_entry), sticky=tk.EW, padx=5, pady=3) # Row 4: Commit Message + Edit Button ttk.Label(self.repo_frame, text="Commit Message:").grid(row=4, column=col_label, sticky=tk.W, padx=5, pady=3) self.commit_message_entry = ttk.Entry( self.repo_frame, textvariable=self.commit_message_var, width=60 ) # Span entry up to the first button column self.commit_message_entry.grid(row=4, column=col_entry, columnspan=col_button1 - col_entry, sticky=tk.EW, padx=5, pady=3) self.create_tooltip(self.commit_message_entry, "Optional message for autocommit. If empty, a default message is used.") # Edit .gitignore Button self.edit_gitignore_button = ttk.Button( self.repo_frame, text="Edit .gitignore", width=12, # Adjust width as needed command=self.open_gitignore_editor_callback, # Use the new callback state=tk.DISABLED # Initially disabled, enabled by controller ) # Place button next to commit message entry, in the first button column self.edit_gitignore_button.grid(row=4, column=col_button1, sticky=tk.W, padx=(0, 5), pady=3) self.create_tooltip(self.edit_gitignore_button, "Open editor for the .gitignore file in the SVN Working Copy.") # Row 5: Autocommit Checkbox self.autocommit_checkbox = ttk.Checkbutton( self.repo_frame, text="Autocommit changes before 'Create Bundle'", variable=self.autocommit_var ) # Span across all columns used, up to and including indicator column self.autocommit_checkbox.grid(row=5, column=0, columnspan=col_indicator + 1, sticky=tk.W, padx=5, pady=(5, 3)) def _create_backup_frame(self): """Creates the frame for backup configuration including exclusions.""" self.backup_frame = ttk.LabelFrame(self, text="Backup Configuration (ZIP)", padding=(10, 5)) self.backup_frame.pack(pady=5, fill="x") # Define columns col_label = 0 col_entry = 1 col_button = 2 # Configure column weights self.backup_frame.columnconfigure(col_entry, weight=1) # Allow entry fields to expand # Row 0: Autobackup Checkbox self.autobackup_checkbox = ttk.Checkbutton( self.backup_frame, text="Automatic Backup before Create/Fetch", variable=self.autobackup_var, command=self.toggle_backup_dir ) self.autobackup_checkbox.grid(row=0, column=col_label, columnspan=col_button + 1, sticky=tk.W, padx=5, pady=(5, 0)) # Row 1: Backup Directory self.backup_dir_label = ttk.Label(self.backup_frame, text="Backup Directory:") self.backup_dir_label.grid(row=1, column=col_label, sticky=tk.W, padx=5, pady=5) self.backup_dir_entry = ttk.Entry( self.backup_frame, textvariable=self.backup_dir_var, width=60, state=tk.DISABLED ) self.backup_dir_entry.grid(row=1, column=col_entry, sticky=tk.EW, padx=5, pady=5) self.backup_dir_button = ttk.Button( self.backup_frame, text="Browse...", width=9, command=self.browse_backup_dir, state=tk.DISABLED ) self.backup_dir_button.grid(row=1, column=col_button, sticky=tk.W, padx=(0, 5), pady=5) # Row 2: Exclude Extensions self.backup_exclude_label = ttk.Label(self.backup_frame, text="Exclude Extensions:") self.backup_exclude_label.grid(row=2, column=col_label, sticky=tk.W, padx=5, pady=5) self.backup_exclude_entry = ttk.Entry( self.backup_frame, textvariable=self.backup_exclude_extensions_var, # Use Tkinter variable width=60 ) # Span across entry and button columns self.backup_exclude_entry.grid(row=2, column=col_entry, columnspan=col_button - col_entry + 1, sticky=tk.EW, padx=5, pady=5) self.create_tooltip(self.backup_exclude_entry, "Comma-separated extensions to exclude (e.g., .log, .tmp, .bak)") def _create_function_frame(self): """Creates the frame holding the main action buttons.""" self.function_frame = ttk.LabelFrame(self, text="Actions", padding=(10, 10)) self.function_frame.pack(pady=5, fill="x", anchor=tk.N) # Anchor North # Use a sub-frame to group buttons button_subframe = ttk.Frame(self.function_frame) button_subframe.pack(fill=tk.X) # Fill horizontally # Prepare SVN button self.prepare_svn_button = ttk.Button( button_subframe, text="Prepare SVN Repo", command=self.prepare_svn_for_git_callback ) self.prepare_svn_button.pack(side=tk.LEFT, padx=(0,5), pady=5) # Create Bundle button self.create_bundle_button = ttk.Button( button_subframe, text="Create Bundle", command=self.create_git_bundle_callback ) self.create_bundle_button.pack(side=tk.LEFT, padx=5, pady=5) # Fetch Bundle button self.fetch_bundle_button = ttk.Button( button_subframe, text="Fetch from Bundle", command=self.fetch_from_git_bundle_callback ) self.fetch_bundle_button.pack(side=tk.LEFT, padx=5, pady=5) # Manual Backup Button self.manual_backup_button = ttk.Button( button_subframe, text="Backup Now (ZIP)", # Indicate ZIP format command=self.manual_backup_callback # Use the new callback ) self.manual_backup_button.pack(side=tk.LEFT, padx=5, pady=5) def _create_log_area(self): """Creates the scrolled text area for logging output.""" log_frame = ttk.Frame(self.master) # Attach to master, below MainFrame content log_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=10, pady=(5, 10)) # ScrolledText widget for log messages self.log_text = scrolledtext.ScrolledText( log_frame, height=12, # Adjust height as desired width=100, # Adjust width as desired font=("Consolas", 9), # Use a monospaced font like Consolas or Courier New wrap=tk.WORD, # Wrap lines at word boundaries state=tk.DISABLED # Start in read-only state ) self.log_text.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) def _initialize_profile_selection(self): """Sets the initial value of the profile dropdown.""" # Use constant defined in config_manager if available, otherwise hardcode 'default' try: from config_manager import DEFAULT_PROFILE except ImportError: DEFAULT_PROFILE = "default" if DEFAULT_PROFILE in self.initial_profile_sections: self.profile_var.set(DEFAULT_PROFILE) elif self.initial_profile_sections: # If default not found, select the first available self.profile_var.set(self.initial_profile_sections[0]) # else: No profiles exist, variable remains empty # --- GUI Update Methods --- def toggle_backup_dir(self): """ Enables or disables the backup directory entry/button based on the autobackup checkbox. """ new_state = tk.NORMAL if self.autobackup_var.get() else tk.DISABLED if hasattr(self, 'backup_dir_entry'): self.backup_dir_entry.config(state=new_state) if hasattr(self, 'backup_dir_button'): self.backup_dir_button.config(state=new_state) # Exclude entry state is independent of autobackup checkbox def browse_backup_dir(self): """Opens a directory selection dialog for the backup directory entry.""" # Suggest initial directory based on current entry or the default initial_dir = self.backup_dir_var.get() or DEFAULT_BACKUP_DIR # Use IMPORTED constant dirname = filedialog.askdirectory( initialdir=initial_dir, title="Select Backup Directory", parent=self.master # Ensure dialog is modal to the main window ) if dirname: # Only update if a directory was actually selected self.backup_dir_var.set(dirname) def update_svn_indicator(self, is_prepared): """ Updates the visual indicator (color) for SVN preparation status and toggles the 'Prepare' button state accordingly. (Edit gitignore button state is handled separately) Args: is_prepared (bool): True if the SVN repo has a '.git' directory, False otherwise. """ if is_prepared: indicator_color = self.GREEN prepare_button_state = tk.DISABLED tooltip_text = "Repository is prepared ('.git' found)" else: indicator_color = self.RED prepare_button_state = tk.NORMAL tooltip_text = "Repository not prepared ('.git' not found)" # Update indicator color if hasattr(self, 'svn_status_indicator'): self.svn_status_indicator.config(background=indicator_color) self.update_tooltip(self.svn_status_indicator, tooltip_text) # Update prepare button state if hasattr(self, 'prepare_svn_button'): self.prepare_svn_button.config(state=prepare_button_state) def update_profile_dropdown(self, sections): """ Updates the list of profiles shown in the combobox. Args: sections (list): The new list of profile names. """ if hasattr(self, 'profile_dropdown'): # Check if dropdown exists current_profile = self.profile_var.get() self.profile_dropdown['values'] = sections # Maintain selection if possible, otherwise select default or first, or clear if sections: if current_profile in sections: self.profile_var.set(current_profile) # Keep current selection elif "default" in sections: self.profile_var.set("default") # Select default if available else: self.profile_var.set(sections[0]) # Select the first available else: self.profile_var.set("") # No profiles left, clear selection # Optionally trigger the callback if needed after programmatic change # self.load_profile_settings_callback(self.profile_var.get()) # --- Dialog Wrappers --- # These provide a consistent way to show standard dialogs via the GUI frame def ask_new_profile_name(self): """Asks the user for a new profile name using a simple dialog.""" # parent=self.master makes the dialog modal to the main window return simpledialog.askstring("Add Profile", "Enter new profile name:", parent=self.master) def show_error(self, title, message): """Displays an error message box.""" messagebox.showerror(title, message, parent=self.master) def show_info(self, title, message): """Displays an information message box.""" messagebox.showinfo(title, message, parent=self.master) def show_warning(self, title, message): """Displays a warning message box.""" messagebox.showwarning(title, message, parent=self.master) def ask_yes_no(self, title, message): """Displays a yes/no confirmation dialog.""" return messagebox.askyesno(title, message, parent=self.master) # --- Tooltip Helper Methods --- # Simple tooltip implementation for GUI elements def create_tooltip(self, widget, text): """Creates a tooltip for a given widget.""" tooltip = Tooltip(widget, text) # Use add='+' to avoid overwriting other bindings widget.bind("", lambda event, tt=tooltip: tt.showtip(), add='+') widget.bind("", lambda event, tt=tooltip: tt.hidetip(), add='+') widget.bind("", lambda event, tt=tooltip: tt.hidetip(), add='+') # Hide on click def update_tooltip(self, widget, text): """Updates the text of an existing tooltip (by re-creating it).""" # Simple approach: Remove old bindings and create new tooltip widget.unbind("") widget.unbind("") widget.unbind("") self.create_tooltip(widget, text)