# gui.py import tkinter as tk from tkinter import ttk # Import themed widgets, including Notebook from tkinter import scrolledtext, filedialog, messagebox, simpledialog import logging import os import re # Needed for validation in dialogs # 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): """Initialize tooltip.""" self.widget = widget self.text = text self.tooltip_window = None self.id = None self.x = 0.0 # Use float for position calculation if needed self.y = 0.0 def showtip(self): """Display text in a tooltip window.""" # Hide any existing tooltip first self.hidetip() # Avoid error if the parent widget has been destroyed if not self.widget.winfo_exists(): return try: # Get widget position relative to screen # bbox("insert") gets coordinates relative to widget's top-left x_rel, y_rel, _, _ = self.widget.bbox("insert") x_root = self.widget.winfo_rootx() # Widget's top-left corner X on screen y_root = self.widget.winfo_rooty() # Widget's top-left corner Y on screen # Calculate tooltip position (below and right of cursor/widget) x_pos = x_root + x_rel + 25 y_pos = y_root + y_rel + 25 except tk.TclError: # Fallback if bbox fails (e.g., widget not visible) # Position below the center of the widget x_root = self.widget.winfo_rootx() y_root = self.widget.winfo_rooty() widget_width = self.widget.winfo_width() widget_height = self.widget.winfo_height() x_pos = x_root + widget_width // 2 y_pos = y_root + widget_height + 5 # Create the tooltip as a Toplevel window self.tooltip_window = tw = tk.Toplevel(self.widget) # Remove window decorations (border, title bar) tw.wm_overrideredirect(True) # Position the window on screen (ensure integer coordinates) tw.wm_geometry(f"+{int(x_pos)}+{int(y_pos)}") # Create the label inside the tooltip window label = tk.Label( tw, text=self.text, justify=tk.LEFT, background="#ffffe0", # Light yellow background relief=tk.SOLID, # Give it a border borderwidth=1, font=("tahoma", "8", "normal"), # Small standard font ) label.pack(ipadx=1) # Small internal padding def hidetip(self): """Hide the tooltip window.""" tw = self.tooltip_window # Reset the reference self.tooltip_window = None # Destroy the window if it exists if tw: try: # Check if window still exists before trying to destroy if tw.winfo_exists(): tw.destroy() except tk.TclError: # Handle race condition where window might be destroyed already pass # --- End Tooltip Class --- # --- Gitignore Editor Window Class --- class GitignoreEditorWindow(tk.Toplevel): """Toplevel window for editing the .gitignore file.""" def __init__(self, master, gitignore_path, logger): """Initialize the editor window.""" super().__init__(master) self.gitignore_path = gitignore_path self.logger = logger self.original_content = "" # Store initial content # --- Window Config --- self.title(f"Edit {os.path.basename(gitignore_path)}") self.geometry("600x450") self.minsize(400, 300) self.grab_set() # Make modal self.transient(master) # Stay on top of parent self.protocol("WM_DELETE_WINDOW", self._on_close) # Handle X button # --- Widgets --- main_frame = ttk.Frame(self, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) main_frame.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) self.text_editor = scrolledtext.ScrolledText( main_frame, wrap=tk.WORD, font=("Consolas", 10), undo=True ) self.text_editor.grid(row=0, column=0, sticky="nsew", pady=(0, 10)) button_frame = ttk.Frame(main_frame) button_frame.grid(row=1, column=0, sticky="ew") button_frame.columnconfigure(0, weight=1) # Spacer left button_frame.columnconfigure(3, weight=1) # Spacer right 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) self.cancel_button = ttk.Button( button_frame, text="Cancel", command=self._on_close ) self.cancel_button.grid(row=0, column=1, padx=5) # --- Load Content & Position --- self._load_file() self._center_window(master) self.text_editor.focus_set() def _center_window(self, parent): """Centers the editor window relative to its parent.""" self.update_idletasks() # Ensure window size is calculated parent_x = parent.winfo_rootx() parent_y = parent.winfo_rooty() parent_w = parent.winfo_width() parent_h = parent.winfo_height() win_w = self.winfo_width() win_h = self.winfo_height() x = parent_x + (parent_w // 2) - (win_w // 2) y = parent_y + (parent_h // 2) - (win_h // 2) # Basic screen boundary check sw = self.winfo_screenwidth() sh = self.winfo_screenheight() x = max(0, min(x, sw - win_w)) y = max(0, min(y, sh - win_h)) self.geometry(f"+{int(x)}+{int(y)}") def _load_file(self): """Loads the .gitignore content into the editor.""" self.logger.info(f"Loading gitignore: {self.gitignore_path}") content = "" try: if os.path.exists(self.gitignore_path): with open( self.gitignore_path, "r", encoding="utf-8", errors="replace" ) as f: content = f.read() self.logger.debug(".gitignore loaded.") else: self.logger.info(".gitignore does not exist.") # Store original content and populate editor self.original_content = content self.text_editor.delete("1.0", tk.END) self.text_editor.insert(tk.END, self.original_content) self.text_editor.edit_reset() # Reset undo stack except IOError as e: self.logger.error(f"Read error: {e}", exc_info=True) messagebox.showerror("Error", f"Read error:\n{e}", parent=self) except Exception as e: self.logger.exception(f"Load error: {e}") messagebox.showerror("Error", f"Load error:\n{e}", parent=self) def _save_file(self): """Saves the editor content back to the .gitignore file.""" # Get content, normalize trailing newline current = self.text_editor.get("1.0", tk.END).rstrip() current += "\n" if current else "" # Normalize original for comparison original = self.original_content.rstrip() original += "\n" if original else "" if current == original: self.logger.info("No changes to save in .gitignore.") return True # Indicate success (no action needed) self.logger.info(f"Saving changes to: {self.gitignore_path}") try: with open(self.gitignore_path, "w", encoding="utf-8", newline="\n") as f: f.write(current) self.logger.info(".gitignore saved successfully.") self.original_content = current # Update baseline for change tracking self.text_editor.edit_reset() # Reset undo stack after save return True # Indicate success except IOError as e: self.logger.error(f"Write error: {e}", exc_info=True) messagebox.showerror("Error", f"Save error:\n{e}", parent=self) return False # Indicate failure except Exception as e: self.logger.exception(f"Save error: {e}") messagebox.showerror("Error", f"Save error:\n{e}", parent=self) return False def _save_and_close(self): """Saves the file then closes the window if save succeeds.""" if self._save_file(): self.destroy() def _on_close(self): """Handles window close: checks for unsaved changes.""" current = self.text_editor.get("1.0", tk.END).rstrip() current += "\n" if current else "" original = self.original_content.rstrip() original += "\n" if original else "" if current != original: # Ask user: Yes (Save), No (Discard), Cancel response = messagebox.askyesnocancel( "Unsaved Changes", "Save changes?", parent=self ) if response is True: # Yes -> Save and Close self._save_and_close() elif response is False: # No -> Discard and Close self.logger.warning("Discarding .gitignore changes.") self.destroy() # Else (Cancel): Do nothing, window stays open else: # No changes, simply close self.destroy() # --- End Gitignore Editor Window --- # --- Create Tag Dialog --- class CreateTagDialog(simpledialog.Dialog): """Dialog to get new tag name and message.""" def __init__(self, parent, title="Create New Tag"): """Initialize the dialog.""" self.tag_name_var = tk.StringVar() self.tag_message_var = tk.StringVar() self.result = None super().__init__(parent, title=title) def body(self, master): """Create dialog body with input fields.""" name_label = ttk.Label(master, text="Tag Name:") name_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") self.name_entry = ttk.Entry(master, textvariable=self.tag_name_var, width=40) self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") msg_label = ttk.Label(master, text="Tag Message:") msg_label.grid(row=1, column=0, padx=5, pady=5, sticky="w") self.message_entry = ttk.Entry( master, textvariable=self.tag_message_var, width=40 ) self.message_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") master.columnconfigure(1, weight=1) # Allow entries to expand return self.name_entry # Initial focus def validate(self): """Validate the input fields.""" name = self.tag_name_var.get().strip() message = self.tag_message_var.get().strip() if not name: messagebox.showwarning( "Input Error", "Tag name cannot be empty.", parent=self ) return 0 # Validation failed if not message: messagebox.showwarning( "Input Error", "Tag message cannot be empty.", parent=self ) return 0 # Validation failed # Validate tag name format using regex # Ensure 're' module is imported at the top of gui.py pattern = r"^(?![./]|.*([./]{2,}|[.]$|\.lock$))[^ \t\n\r\f\v~^:?*[\\]+(?>", lambda e: self.load_profile_settings_callback(self.profile_var.get()), ) self.profile_var.trace_add( "write", lambda *a: self.load_profile_settings_callback(self.profile_var.get()), ) # Buttons self.save_settings_button = ttk.Button( frame, text="Save Settings", command=self.save_profile_callback ) self.save_settings_button.grid( row=0, column=2, sticky=tk.W, padx=(5, 2), pady=5 ) self.create_tooltip(self.save_settings_button, "Save settings.") self.add_profile_button = ttk.Button( frame, text="Add", width=5, command=self.add_profile_callback ) self.add_profile_button.grid(row=0, column=3, sticky=tk.W, padx=(2, 0), pady=5) self.remove_profile_button = ttk.Button( frame, text="Remove", width=8, command=self.remove_profile_callback ) self.remove_profile_button.grid( row=0, column=4, sticky=tk.W, padx=(2, 5), pady=5 ) def _create_repo_tab(self): """Creates the frame for the 'Repository / Bundle' tab.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(1, weight=1) # Allow entries to expand # --- Paths and Bundle Names --- paths_frame = ttk.LabelFrame( frame, text="Paths & Bundle Names", padding=(10, 5) ) paths_frame.pack(pady=5, fill="x") paths_frame.columnconfigure(1, weight=1) # Entry column expands col_label = 0 col_entry = 1 col_button = 2 col_indicator = 3 # SVN Path lbl_svn = ttk.Label(paths_frame, text="SVN Working Copy:") lbl_svn.grid(row=0, column=col_label, sticky=tk.W, padx=5, pady=3) self.svn_path_entry = ttk.Entry(paths_frame, width=60) self.svn_path_entry.grid(row=0, column=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( paths_frame, text="Browse...", width=9, command=lambda: self.browse_folder_callback(self.svn_path_entry), ) self.svn_path_browse_button.grid( row=0, column=col_button, sticky=tk.W, padx=(0, 5), pady=3 ) self.svn_status_indicator = tk.Label( paths_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, "Git repo status (Green=Ready)") # Bundle Target Dir lbl_usb = ttk.Label(paths_frame, text="Bundle Target Dir:") lbl_usb.grid(row=1, column=col_label, sticky=tk.W, padx=5, pady=3) self.usb_path_entry = ttk.Entry(paths_frame, width=60) self.usb_path_entry.grid(row=1, column=col_entry, sticky=tk.EW, padx=5, pady=3) self.usb_path_browse_button = ttk.Button( paths_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_button, sticky=tk.W, padx=(0, 5), pady=3 ) # Create Bundle Name lbl_create_b = ttk.Label(paths_frame, text="Create Bundle Name:") lbl_create_b.grid(row=2, column=col_label, sticky=tk.W, padx=5, pady=3) self.bundle_name_entry = ttk.Entry(paths_frame, width=60) self.bundle_name_entry.grid( row=2, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) # Fetch Bundle Name lbl_fetch_b = ttk.Label(paths_frame, text="Fetch Bundle Name:") lbl_fetch_b.grid(row=3, column=col_label, sticky=tk.W, padx=5, pady=3) self.bundle_updated_name_entry = ttk.Entry(paths_frame, width=60) self.bundle_updated_name_entry.grid( row=3, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) # --- Actions Frame --- actions_frame = ttk.LabelFrame(frame, text="Actions", padding=(10, 5)) actions_frame.pack(pady=10, fill="x") # Prepare Button self.prepare_svn_button = ttk.Button( actions_frame, text="Prepare SVN Repo", command=self.prepare_svn_for_git_callback, state=tk.DISABLED, ) self.prepare_svn_button.pack(side=tk.LEFT, padx=(0, 5), pady=5) self.create_tooltip(self.prepare_svn_button, "Initialize Git & .gitignore") # Create Bundle Button self.create_bundle_button = ttk.Button( actions_frame, text="Create Bundle", command=self.create_git_bundle_callback, state=tk.DISABLED, ) self.create_bundle_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip(self.create_bundle_button, "Create Git bundle file") # Fetch Bundle Button self.fetch_bundle_button = ttk.Button( actions_frame, text="Fetch from Bundle", command=self.fetch_from_git_bundle_callback, state=tk.DISABLED, ) self.fetch_bundle_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip(self.fetch_bundle_button, "Fetch & merge from bundle") # Edit .gitignore Button (Moved here) self.edit_gitignore_button = ttk.Button( actions_frame, text="Edit .gitignore", width=12, command=self.open_gitignore_editor_callback, state=tk.DISABLED, ) self.edit_gitignore_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip(self.edit_gitignore_button, "Edit .gitignore file") return frame def _create_backup_tab(self): """Creates the frame for the 'Backup' tab.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(1, weight=1) # Entry expands # --- Configuration --- config_frame = ttk.LabelFrame(frame, text="Configuration", padding=(10, 5)) config_frame.pack(pady=5, fill="x") config_frame.columnconfigure(1, weight=1) col_label = 0 col_entry = 1 col_button = 2 self.autobackup_checkbox = ttk.Checkbutton( config_frame, text="Auto Backup before Create/Fetch", variable=self.autobackup_var, command=self.toggle_backup_dir, ) self.autobackup_checkbox.grid( row=0, column=0, columnspan=3, sticky=tk.W, padx=5, pady=(5, 0) ) backup_dir_label = ttk.Label(config_frame, text="Backup Directory:") backup_dir_label.grid(row=1, column=col_label, sticky=tk.W, padx=5, pady=5) self.backup_dir_entry = ttk.Entry( config_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( config_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 ) exclude_label = ttk.Label(config_frame, text="Exclude Extensions:") exclude_label.grid(row=2, column=col_label, sticky=tk.W, padx=5, pady=5) self.backup_exclude_entry = ttk.Entry( config_frame, textvariable=self.backup_exclude_extensions_var, width=60 ) self.backup_exclude_entry.grid( row=2, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=5 ) self.create_tooltip( self.backup_exclude_entry, "Comma-separated (e.g., .log,.tmp)" ) # --- Manual Backup Action --- action_frame = ttk.LabelFrame(frame, text="Manual Backup", padding=(10, 5)) action_frame.pack(pady=10, fill="x") self.manual_backup_button = ttk.Button( action_frame, text="Backup Now (ZIP)", command=self.manual_backup_callback, state=tk.DISABLED, ) self.manual_backup_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip(self.manual_backup_button, "Create a ZIP backup now.") return frame def _create_commit_tab(self): """Creates the frame for the 'Commit' tab.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.rowconfigure(2, weight=1) # Text area expands frame.columnconfigure(0, weight=1) # Text area expands # Row 0: Autocommit Checkbox (for Create Bundle action) self.autocommit_checkbox = ttk.Checkbutton( frame, text="Autocommit changes before 'Create Bundle' action", variable=self.autocommit_var, state=tk.DISABLED, ) self.autocommit_checkbox.grid( row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 5) ) self.create_tooltip( self.autocommit_checkbox, "If checked, uses message below to commit before Create Bundle.", ) # Row 1: Commit Message Label commit_msg_label = ttk.Label(frame, text="Commit Message:") commit_msg_label.grid(row=1, column=0, columnspan=2, sticky="w", padx=5) # Row 2: Commit Message Text Area self.commit_message_text = scrolledtext.ScrolledText( frame, height=7, width=60, wrap=tk.WORD, # Increased height font=("Segoe UI", 9), state=tk.DISABLED, # Start disabled ) self.commit_message_text.grid( row=2, column=0, columnspan=2, sticky="nsew", padx=5, pady=(0, 5) ) self.create_tooltip( self.commit_message_text, "Enter commit message here for manual commit or autocommit.", ) # Row 3: Commit Button self.commit_button = ttk.Button( frame, text="Commit Staged Changes", # Clarified label command=self.commit_changes_callback, # Link to controller method state=tk.DISABLED, ) self.commit_button.grid( row=3, column=0, columnspan=2, sticky="e", padx=5, pady=5 ) self.create_tooltip( self.commit_button, "Stage ALL changes and commit with the message above." ) return frame def _create_tags_tab(self): """Creates the frame for the 'Tags' tab.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(0, weight=1) # Listbox column expands frame.rowconfigure(1, weight=1) # Listbox row expands # Row 0: Label lbl = ttk.Label(frame, text="Existing Tags (Newest First):") lbl.grid(row=0, column=0, sticky="w", padx=5, pady=(0, 2)) # Row 1: Listbox + Scrollbar Frame list_frame = ttk.Frame(frame) list_frame.grid(row=1, column=0, sticky="nsew", padx=(5, 0), pady=(0, 5)) list_frame.rowconfigure(0, weight=1) list_frame.columnconfigure(0, weight=1) self.tag_listbox = tk.Listbox( list_frame, height=10, exportselection=False, selectmode=tk.SINGLE, font=("Consolas", 9), ) self.tag_listbox.grid(row=0, column=0, sticky="nsew") scrollbar = ttk.Scrollbar( list_frame, orient=tk.VERTICAL, command=self.tag_listbox.yview ) scrollbar.grid(row=0, column=1, sticky="ns") self.tag_listbox.config(yscrollcommand=scrollbar.set) self.create_tooltip(self.tag_listbox, "Select tag to checkout.") # Row 1, Column 1: Vertical Button Frame button_frame = ttk.Frame(frame) button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5)) button_width = 18 # Uniform width self.refresh_tags_button = ttk.Button( button_frame, text="Refresh Tags", width=button_width, command=self.refresh_tags_callback, state=tk.DISABLED, ) self.refresh_tags_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 5)) self.create_tooltip(self.refresh_tags_button, "Reload tag list.") self.create_tag_button = ttk.Button( button_frame, text="Create Tag...", width=button_width, command=self.create_tag_callback, state=tk.DISABLED, ) self.create_tag_button.pack(side=tk.TOP, fill=tk.X, pady=5) self.create_tooltip( self.create_tag_button, "Commit (if needed with msg) & create tag." ) self.checkout_tag_button = ttk.Button( button_frame, text="Checkout Selected Tag", width=button_width, command=self.checkout_tag_callback, state=tk.DISABLED, ) self.checkout_tag_button.pack(side=tk.TOP, fill=tk.X, pady=(5, 0)) self.create_tooltip(self.checkout_tag_button, "Switch to selected tag.") return frame def _create_branch_tab(self): """Creates the frame for the 'Branches' tab.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(0, weight=1) # Listbox expands frame.rowconfigure(1, weight=1) # Listbox expands # Row 0: Label lbl = ttk.Label(frame, text="Local Branches (* Current):") lbl.grid(row=0, column=0, sticky="w", padx=5, pady=(0, 2)) # Row 1: Listbox + Scrollbar Frame list_frame = ttk.Frame(frame) list_frame.grid(row=1, column=0, sticky="nsew", padx=(5, 0), pady=(0, 5)) list_frame.rowconfigure(0, weight=1) list_frame.columnconfigure(0, weight=1) self.branch_listbox = tk.Listbox( list_frame, height=10, exportselection=False, selectmode=tk.SINGLE ) self.branch_listbox.grid(row=0, column=0, sticky="nsew") scrollbar = ttk.Scrollbar( list_frame, orient=tk.VERTICAL, command=self.branch_listbox.yview ) scrollbar.grid(row=0, column=1, sticky="ns") self.branch_listbox.config(yscrollcommand=scrollbar.set) self.create_tooltip(self.branch_listbox, "Select branch to checkout.") # Row 1, Column 1: Vertical Button Frame button_frame = ttk.Frame(frame) button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5)) button_width = 18 # Uniform width self.refresh_branches_button = ttk.Button( button_frame, text="Refresh Branches", width=button_width, command=self.refresh_branches_callback, state=tk.DISABLED, ) self.refresh_branches_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 5)) self.create_tooltip(self.refresh_branches_button, "Reload branch list.") self.create_branch_button = ttk.Button( button_frame, text="Create Branch...", width=button_width, command=self.create_branch_callback, state=tk.DISABLED, ) self.create_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5) self.create_tooltip(self.create_branch_button, "Create a new local branch.") self.checkout_branch_button = ttk.Button( button_frame, text="Checkout Selected", width=button_width, # Shortened text command=self.checkout_branch_callback, state=tk.DISABLED, ) self.checkout_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5) self.create_tooltip(self.checkout_branch_button, "Switch to selected branch.") # Placeholder for Delete Branch if added later # self.delete_branch_button = ttk.Button(...) # self.delete_branch_button.pack(...) return frame def _create_history_tab(self): """Creates the frame for the 'History' tab.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.rowconfigure(2, weight=1) # Text area expands frame.columnconfigure(0, weight=1) # Text area expands # --- Filters --- (Row 0) filter_frame = ttk.Frame(frame) filter_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) filter_frame.columnconfigure(1, weight=1) # Allow combobox to expand filter_label = ttk.Label(filter_frame, text="Filter by Branch:") filter_label.pack(side=tk.LEFT, padx=(0, 5)) self.history_branch_filter_var = tk.StringVar() self.history_branch_filter_combo = ttk.Combobox( filter_frame, textvariable=self.history_branch_filter_var, state="readonly", width=30, ) self.history_branch_filter_combo.pack( side=tk.LEFT, expand=True, fill=tk.X, padx=5 ) # Apply filter when selection changes self.history_branch_filter_combo.bind( "<>", lambda e: self.refresh_history_callback() ) self.create_tooltip( self.history_branch_filter_combo, "Show history for selected branch (or all).", ) self.refresh_history_button = ttk.Button( filter_frame, text="Refresh History", command=self.refresh_history_callback, state=tk.DISABLED, ) self.refresh_history_button.pack(side=tk.LEFT, padx=5) self.create_tooltip(self.refresh_history_button, "Load commit history.") # --- History Display --- (Row 1, 2, 3) history_label = ttk.Label(frame, text="Recent Commits:") history_label.grid(row=1, column=0, sticky="w", padx=5, pady=(5, 0)) self.history_text = scrolledtext.ScrolledText( frame, height=15, width=100, font=("Consolas", 9), wrap=tk.NONE, state=tk.DISABLED, ) self.history_text.grid(row=2, column=0, sticky="nsew", padx=5, pady=(0, 5)) history_xscroll = ttk.Scrollbar( frame, orient=tk.HORIZONTAL, command=self.history_text.xview ) history_xscroll.grid(row=3, column=0, sticky="ew", padx=5) self.history_text.config(xscrollcommand=history_xscroll.set) return frame def _create_function_frame(self): """REMOVED - This frame is no longer used in the tabbed layout.""" pass # Return None or simply don't call this method def _create_log_area(self): """Creates the application log area at the bottom.""" log_frame = ttk.Frame(self, padding=(0, 5, 0, 0)) # Parent is MainFrame log_frame.pack( side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=(5, 0) ) # Pack below notebook log_label = ttk.Label(log_frame, text="Application Log:") log_label.pack(side=tk.TOP, anchor=tk.W, padx=5) self.log_text = scrolledtext.ScrolledText( log_frame, height=8, width=100, font=("Consolas", 9), wrap=tk.WORD, state=tk.DISABLED, ) self.log_text.pack( side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=5, pady=(0, 5) ) def _initialize_profile_selection(self): """Sets the initial value of the profile dropdown.""" 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: self.profile_var.set(self.initial_profile_sections[0]) # --- GUI Update Methods --- def toggle_backup_dir(self): """Toggles state of backup directory widgets.""" state = tk.NORMAL if self.autobackup_var.get() else tk.DISABLED if hasattr(self, "backup_dir_entry"): self.backup_dir_entry.config(state=state) if hasattr(self, "backup_dir_button"): self.backup_dir_button.config(state=state) def browse_backup_dir(self): """Opens directory dialog for backup path.""" initial = self.backup_dir_var.get() or DEFAULT_BACKUP_DIR dirname = filedialog.askdirectory( initialdir=initial, title="Select Backup Dir", parent=self.master ) if dirname: self.backup_dir_var.set(dirname) def update_svn_indicator(self, is_prepared): """Updates repo indicator color and Prepare button state.""" color = self.GREEN if is_prepared else self.RED state = tk.DISABLED if is_prepared else tk.NORMAL tooltip = "Prepared" if is_prepared else "Not prepared" if hasattr(self, "svn_status_indicator"): self.svn_status_indicator.config(background=color) self.update_tooltip(self.svn_status_indicator, tooltip) if hasattr(self, "prepare_svn_button"): # Prepare button is in Repo tab self.prepare_svn_button.config(state=state) def update_profile_dropdown(self, sections): """Updates profile dropdown list and selection.""" if hasattr(self, "profile_dropdown"): current = self.profile_var.get() self.profile_dropdown["values"] = sections if sections: if current in sections: self.profile_var.set(current) elif "default" in sections: self.profile_var.set("default") else: self.profile_var.set(sections[0]) else: self.profile_var.set("") def update_tag_list(self, tags_data): """Updates tag listbox with (name, subject) tuples.""" if not hasattr(self, "tag_listbox"): return try: self.tag_listbox.delete(0, tk.END) if tags_data: try: # Reset color if self.tag_listbox.cget("fg") == "grey": self.tag_listbox.config(fg="SystemWindowText") except tk.TclError: pass for name, subject in tags_data: self.tag_listbox.insert(tk.END, f"{name}\t({subject})") else: self.tag_listbox.insert(tk.END, "(No tags found)") try: self.tag_listbox.config(fg="grey") except tk.TclError: pass except Exception as e: logging.error(f"Error tags: {e}", exc_info=True) def get_selected_tag(self): """Returns the name only of the selected tag.""" if hasattr(self, "tag_listbox"): indices = self.tag_listbox.curselection() if indices: item = self.tag_listbox.get(indices[0]) if item != "(No tags found)": return item.split("\t", 1)[0].strip() return None def update_branch_list(self, branches, current_branch): """Updates branch listbox, highlighting current.""" if not hasattr(self, "branch_listbox"): return try: self.branch_listbox.delete(0, tk.END) sel_index = -1 if branches: for i, branch in enumerate(branches): prefix = "* " if branch == current_branch else " " self.branch_listbox.insert(tk.END, f"{prefix}{branch}") if branch == current_branch: sel_index = i else: self.branch_listbox.insert(tk.END, "(No local branches)") # Select current branch if found if sel_index >= 0: self.branch_listbox.selection_set(sel_index) self.branch_listbox.see(sel_index) except Exception as e: logging.error(f"Error branches: {e}", exc_info=True) def get_selected_branch(self): """Returns the name only of the selected branch.""" if hasattr(self, "branch_listbox"): indices = self.branch_listbox.curselection() if indices: item = self.branch_listbox.get(indices[0]) if item != "(No local branches)": return item.lstrip("* ").strip() return None def get_commit_message(self): """Gets commit message from ScrolledText widget.""" if hasattr(self, "commit_message_text"): return self.commit_message_text.get("1.0", tk.END).strip() return "" def clear_commit_message(self): """Clears the commit message ScrolledText widget.""" if hasattr(self, "commit_message_text"): # Check state before modifying - avoid error if disabled if self.commit_message_text.cget("state") == tk.NORMAL: self.commit_message_text.delete("1.0", tk.END) else: # If disabled, enable, clear, disable self.commit_message_text.config(state=tk.NORMAL) self.commit_message_text.delete("1.0", tk.END) self.commit_message_text.config(state=tk.DISABLED) def update_history_display(self, log_lines): """Updates the commit history text area.""" if not hasattr(self, "history_text"): return try: self.history_text.config(state=tk.NORMAL) self.history_text.delete("1.0", tk.END) if log_lines: self.history_text.insert(tk.END, "\n".join(log_lines)) else: self.history_text.insert(tk.END, "(No history found)") self.history_text.config(state=tk.DISABLED) self.history_text.yview_moveto(0.0) # Scroll top except Exception as e: logging.error(f"Error history: {e}", exc_info=True) def update_history_branch_filter(self, branches, current_branch=None): """Populates branch filter combobox in History tab.""" if not hasattr(self, "history_branch_filter_combo"): return filter_options = ["-- All History --"] + branches self.history_branch_filter_combo["values"] = filter_options # Set default selection if current_branch and current_branch in branches: self.history_branch_filter_var.set(current_branch) else: self.history_branch_filter_var.set(filter_options[0]) # --- Dialog Wrappers (Unchanged) --- def ask_new_profile_name(self): return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master) def show_error(self, title, message): messagebox.showerror(title, message, parent=self.master) def show_info(self, title, message): messagebox.showinfo(title, message, parent=self.master) def show_warning(self, title, message): messagebox.showwarning(title, message, parent=self.master) def ask_yes_no(self, title, message): return messagebox.askyesno(title, message, parent=self.master) # --- Tooltip Helpers (Unchanged) --- def create_tooltip(self, widget, text): tt = Tooltip(widget, text) widget.bind("", lambda e, t=tt: t.showtip(), add="+") widget.bind("", lambda e, t=tt: t.hidetip(), add="+") widget.bind("", lambda e, t=tt: t.hidetip(), add="+") def update_tooltip(self, widget, text): widget.unbind("") widget.unbind("") widget.unbind("") self.create_tooltip(widget, text)