# --- FILE: gitsync_tool/gui/main_frame.py --- import tkinter as tk from tkinter import ttk from tkinter import ( messagebox, simpledialog, scrolledtext, filedialog, ) # Import moduli tk necessari import os import re import sys from typing import Tuple, Dict, List, Callable, Optional, Any # Importa moduli/classi dal pacchetto gitsync_tool from gitutility.logging_setup import log_handler # Usa il log handler from gitutility.gui.tooltip import Tooltip # Importa Tooltip dal suo file # Importa costanti dal modulo config try: from gitutility.config.config_manager import ( DEFAULT_BACKUP_DIR, DEFAULT_REMOTE_NAME, DEFAULT_PROFILE, ) except ImportError: DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "backup_fallback") DEFAULT_REMOTE_NAME = "origin" DEFAULT_PROFILE = "default" print( f"WARNING: main_frame.py could not import constants. Using fallbacks.", file=sys.stderr, ) # Non importiamo più classi di Dialog, Editor, Viewer qui perché sono in file separati # Non importiamo moduli di logica (ActionHandler, GitCommands, etc.) class MainFrame(ttk.Frame): """ The main application frame, containing the profile management bar, the tabbed notebook interface, the log area, and the status bar. Connects GUI elements to controller callbacks. """ # Definizioni costanti colori GREEN: str = "#90EE90" # LightGreen RED: str = "#F08080" # LightCoral (un rosso più tenue) STATUS_YELLOW: str = "#FFFACD" # Lemon Chiffon STATUS_RED: str = "#FFA07A" # Light Salmon STATUS_GREEN: str = "#98FB98" # Pale Green STATUS_DEFAULT_BG: Optional[str] = None # Verrà impostato in __init__ # ---<<< MODIFICA: Firma __init__ aggiornata >>>--- # Rimuovi type hints che si riferiscono a classi non più importate qui # Assicurati che tutti i parametri callback siano definiti e ricevuti def __init__( self, master: tk.Misc, # Callbacks load_profile_settings_cb: Callable[[str], None], browse_folder_cb: Callable[[tk.Entry], None], update_svn_status_cb: Callable[[str], None], prepare_svn_for_git_cb: Callable[[], None], create_git_bundle_cb: Callable[[], None], fetch_from_git_bundle_cb: Callable[[], None], add_profile_cb: Callable[[], None], remove_profile_cb: Callable[[], None], manual_backup_cb: Callable[[], None], open_gitignore_editor_cb: Callable[[], None], save_profile_cb: Callable[[], bool], commit_changes_cb: Callable[[], None], refresh_tags_cb: Callable[[], None], create_tag_cb: Callable[[], None], checkout_tag_cb: Callable[[], None], revert_to_tag_cb: Callable[[], None], refresh_history_cb: Callable[[], None], refresh_branches_cb: Callable[[], None], checkout_branch_cb: Callable[[Optional[str], Optional[str]], None], create_branch_cb: Callable[[], None], refresh_changed_files_cb: Callable[[], None], open_diff_viewer_cb: Callable[[str], None], add_selected_file_cb: Callable[[str], None], apply_remote_config_cb: Callable[[], None], check_connection_auth_cb: Callable[[], None], fetch_remote_cb: Callable[[], None], pull_remote_cb: Callable[[], None], push_remote_cb: Callable[[], None], push_tags_remote_cb: Callable[[], None], refresh_remote_status_cb: Callable[[], None], clone_remote_repo_cb: Callable[[], None], refresh_remote_branches_cb: Callable[[], None], checkout_remote_branch_cb: Callable[[str, str], None], delete_local_branch_cb: Callable[[str, bool], None], merge_local_branch_cb: Callable[[str], None], compare_branch_with_current_cb: Callable[[str], None], view_commit_details_cb: Callable[[str], None], # Automation Callbacks update_gitea_wiki_cb: Callable[[], None], analyze_and_clean_history_cb: Callable[[], None], # <<< NUOVO PARAMETRO AGGIUNTO QUI # Altre dipendenze config_manager_instance: Any, profile_sections_list: List[str], ): """Initializes the MainFrame.""" super().__init__(master) self.master: tk.Misc = master # Store callbacks 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.manual_backup_callback = manual_backup_cb self.add_profile_callback = add_profile_cb self.remove_profile_callback = remove_profile_cb self.save_profile_callback = save_profile_cb self.open_gitignore_editor_callback = open_gitignore_editor_cb self.commit_changes_callback = commit_changes_cb self.refresh_tags_callback = refresh_tags_cb self.create_tag_callback = create_tag_cb self.checkout_tag_callback = checkout_tag_cb self.revert_to_tag_callback = revert_to_tag_cb self.refresh_history_callback = refresh_history_cb self.refresh_branches_callback = refresh_branches_cb self.checkout_branch_callback = checkout_branch_cb self.create_branch_callback = create_branch_cb self.refresh_changed_files_callback = refresh_changed_files_cb self.open_diff_viewer_callback = open_diff_viewer_cb self.add_selected_file_callback = add_selected_file_cb self.apply_remote_config_callback = apply_remote_config_cb self.check_connection_auth_callback = check_connection_auth_cb self.fetch_remote_callback = fetch_remote_cb self.pull_remote_callback = pull_remote_cb self.push_remote_callback = push_remote_cb self.push_tags_remote_callback = push_tags_remote_cb self.refresh_remote_status_callback = refresh_remote_status_cb self.clone_remote_repo_callback = clone_remote_repo_cb self.refresh_remote_branches_callback = refresh_remote_branches_cb self.checkout_remote_branch_callback = checkout_remote_branch_cb self.delete_local_branch_callback = delete_local_branch_cb self.merge_local_branch_callback = merge_local_branch_cb self.compare_branch_with_current_callback = compare_branch_with_current_cb self.view_commit_details_callback = view_commit_details_cb self.update_gitea_wiki_callback = update_gitea_wiki_cb self.analyze_and_clean_history_cb = analyze_and_clean_history_cb # Store references needed internally self.config_manager = ( config_manager_instance # Necessario per accedere a metodi? Forse no. ) self.initial_profile_sections = profile_sections_list # Configurazione stile (richiede ttk) self.style = ttk.Style() # Se hai configurazioni di stile, mettile qui o in un metodo separato # Esempio: self.style.configure('Red.TLabel', foreground='red') # Configura il layout del frame principale self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) # --- Tkinter Variables --- self.profile_var = tk.StringVar() self.autobackup_var = tk.BooleanVar() self.backup_dir_var = tk.StringVar() self.backup_exclude_extensions_var = tk.StringVar() self.backup_exclude_dirs_var = tk.StringVar() self.autocommit_var = tk.BooleanVar() self.status_bar_var = tk.StringVar() self.remote_url_var = tk.StringVar() self.remote_name_var = tk.StringVar() self.remote_auth_status_var = tk.StringVar(value="Status: Unknown") self.remote_ahead_behind_var = tk.StringVar(value="Sync Status: Unknown") # Variabile per il filtro history (già presente nel metodo _create) self.history_branch_filter_var = tk.StringVar() # --- Creazione Menu Contestuali --- # Associali al master (root window) o a self (MainFrame) self.remote_branch_context_menu = tk.Menu(self.master, tearoff=0) self.local_branch_context_menu = tk.Menu(self.master, tearoff=0) self.changed_files_context_menu = tk.Menu(self.master, tearoff=0) # --- Create UI Elements --- self._create_profile_frame() self.notebook = ttk.Notebook(self, padding=(0, 5, 0, 0)) self.notebook.pack(pady=(5, 0), padx=0, fill="both", expand=True) # Creazione delle tab (l'ordine di chiamata influenza l'esistenza dei widget) self.repo_tab_frame = self._create_repo_tab() self.backup_tab_frame = self._create_backup_tab() self.commit_tab_frame = self._create_commit_tab() self.tags_tab_frame = self._create_tags_tab() self.branch_tab_frame = ( self._create_branch_tab() ) # Tab originale per branch locali self.remote_tab_frame = self._create_remote_tab() # Nuova tab remota self.history_tab_frame = ( self._create_history_tab_treeview() ) # Usa la versione TreeView self.automation_tab_frame = self._create_automation_tab() # Aggiunta delle tab al Notebook (ordine di visualizzazione) self.notebook.add(self.repo_tab_frame, text=" Repository / Bundle ") self.notebook.add(self.remote_tab_frame, text=" Remote Repository ") self.notebook.add(self.backup_tab_frame, text=" Backup Settings ") self.notebook.add(self.commit_tab_frame, text=" Commit / Changes ") self.notebook.add(self.tags_tab_frame, text=" Tags ") self.notebook.add(self.branch_tab_frame, text=" Branches (Local Ops) ") self.notebook.add(self.automation_tab_frame, text=" Automation ") self.notebook.add(self.history_tab_frame, text=" History ") # Creazione area log e status bar log_frame_container = ttk.Frame(self) log_frame_container.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=(5, 0) ) self._create_log_area(log_frame_container) # Chiama metodo di creazione log self.status_bar = ttk.Label( self, textvariable=self.status_bar_var, relief=tk.SUNKEN, anchor=tk.W, padding=(5, 2), ) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X, pady=(2, 0), padx=0) # Impostazione colore default status bar try: s = ttk.Style() # Salva il colore di background di default per ripristinarlo MainFrame.STATUS_DEFAULT_BG = s.lookup("TLabel", "background") except tk.TclError: # Fallback se lo stile non è pronto o dà errore MainFrame.STATUS_DEFAULT_BG = self.status_bar.cget("background") self.status_bar_var.set("Initializing...") # Messaggio iniziale self._status_reset_timer: Optional[str] = ( None # Timer per reset colore status bar ) self.current_local_branch: Optional[str] = None # Memorizza branch corrente # Initial State Setup self._initialize_profile_selection() # Imposta selezione iniziale profilo self.toggle_backup_dir() # Imposta stato iniziale widget backup dir # --- Frame Creation Methods --- # (Questi metodi dovrebbero ora essere definiti qui) def _create_profile_frame(self): # ... (Codice da gui.py originale, assicurati che Tooltip sia usabile) ... profile_outer_frame = ttk.Frame(self, padding=(0, 0, 0, 5)) profile_outer_frame.pack(fill="x", side=tk.TOP) frame = ttk.LabelFrame( profile_outer_frame, text="Profile Management", padding=(10, 5) ) frame.pack(fill="x") frame.columnconfigure(1, weight=1) ttk.Label(frame, text="Select Profile:").grid( row=0, column=0, sticky=tk.W, padx=5, pady=5 ) self.profile_dropdown = ttk.Combobox( frame, textvariable=self.profile_var, state="readonly", width=35, values=self.initial_profile_sections, ) self.profile_dropdown.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=5) self.profile_dropdown.bind( "<>", lambda e: self.load_profile_settings_callback(self.profile_var.get()), ) self.profile_var.trace_add( "write", lambda n, i, m: self.load_profile_settings_callback(self.profile_var.get()), ) self.create_tooltip( self.profile_dropdown, "Select the configuration profile for the repository.", ) button_subframe = ttk.Frame(frame) button_subframe.grid(row=0, column=2, sticky=tk.E, padx=(10, 0)) self.save_settings_button = ttk.Button( button_subframe, text="Save Profile", command=self.save_profile_callback ) self.save_settings_button.pack(side=tk.LEFT, padx=(0, 2), pady=5) self.create_tooltip( self.save_settings_button, "Save the current settings to the selected profile.", ) self.add_profile_button = ttk.Button( button_subframe, text="Add New", width=8, command=self.add_profile_callback ) self.add_profile_button.pack(side=tk.LEFT, padx=(2, 2), pady=5) self.create_tooltip(self.add_profile_button, "Add a new configuration profile.") self.clone_profile_button = ttk.Button( button_subframe, text="Clone from Remote", width=18, command=self.clone_remote_repo_callback, ) self.clone_profile_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.clone_profile_button, "Clone a remote repository into a new local directory and create a profile for it.", ) self.remove_profile_button = ttk.Button( button_subframe, text="Remove", width=8, command=self.remove_profile_callback, ) self.remove_profile_button.pack(side=tk.LEFT, padx=(2, 0), pady=5) self.create_tooltip( self.remove_profile_button, "Remove the selected profile (cannot remove default).", ) def _create_repo_tab(self): # ... (Codice da gui.py originale, usa self.browse_folder_callback, etc.) ... frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(1, weight=1) 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) cl, ce, cb, ci = 0, 1, 2, 3 ttk.Label(paths_frame, text="Working Directory Path:").grid( row=0, column=cl, 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=ce, 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.create_tooltip( self.svn_path_entry, "Path to the local Git repository working directory." ) 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=cb, 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=ci, sticky=tk.E, padx=(0, 5), pady=3 ) self.create_tooltip( self.svn_status_indicator, "Indicates if the path points to a valid Git repository (Green=Yes, Red=No).", ) ttk.Label(paths_frame, text="Bundle Target Directory:").grid( row=1, column=cl, 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=ce, sticky=tk.EW, padx=5, pady=3) self.create_tooltip( self.usb_path_entry, "Directory where bundle files will be created or read from (e.g., USB drive).", ) 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=cb, sticky=tk.W, padx=(0, 5), pady=3 ) ttk.Label(paths_frame, text="Create Bundle Filename:").grid( row=2, column=cl, 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=ce, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) self.create_tooltip( self.bundle_name_entry, "Filename to use when creating a new bundle file (e.g., my_project.bundle).", ) ttk.Label(paths_frame, text="Fetch Bundle Filename:").grid( row=3, column=cl, 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=ce, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) self.create_tooltip( self.bundle_updated_name_entry, "Filename of the bundle file to fetch updates from (e.g., update.bundle).", ) actions_frame = ttk.LabelFrame( frame, text="Repository Actions", padding=(10, 5) ) actions_frame.pack(pady=10, fill="x") self.prepare_svn_button = ttk.Button( actions_frame, text="Prepare Repository", 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 in the selected directory and configure .gitignore (if not already a Git repo).", ) 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 a Git bundle file containing the entire repository history and refs.", ) 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 updates from a bundle file and merge them, or clone if the directory is empty.", ) 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, "Open an editor for the .gitignore file." ) return frame def _create_backup_tab(self): # ... (Codice da gui.py originale) ... frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(1, weight=1) config_frame = ttk.LabelFrame( frame, text="Backup Configuration", padding=(10, 5) ) config_frame.pack(pady=5, fill="x", expand=False) config_frame.columnconfigure(1, weight=1) cl, ce, cb = 0, 1, 2 self.autobackup_checkbox = ttk.Checkbutton( config_frame, text="Enable Auto Backup before Actions (Create/Fetch Bundle)", 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, 5) ) self.create_tooltip( self.autobackup_checkbox, "Automatically create a ZIP backup of the working directory before creating or fetching from a bundle.", ) backup_dir_label = ttk.Label(config_frame, text="Backup Directory:") backup_dir_label.grid(row=1, column=cl, sticky=tk.W, padx=5, pady=3) 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=ce, sticky=tk.EW, padx=5, pady=3) self.create_tooltip( self.backup_dir_entry, "Directory where automatic and manual backup ZIP files will be stored.", ) 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=cb, sticky=tk.W, padx=(0, 5), pady=3) exclude_ext_label = ttk.Label(config_frame, text="Exclude File Exts:") exclude_ext_label.grid(row=2, column=cl, sticky=tk.W, padx=5, pady=3) self.backup_exclude_entry = ttk.Entry( config_frame, textvariable=self.backup_exclude_extensions_var, width=60 ) self.backup_exclude_entry.grid( row=2, column=ce, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) self.create_tooltip( self.backup_exclude_entry, "Comma-separated list of file extensions to exclude from backups (e.g., .log, .tmp, .bak).", ) exclude_dir_label = ttk.Label(config_frame, text="Exclude Dirs (Name):") exclude_dir_label.grid(row=3, column=cl, sticky=tk.W, padx=5, pady=3) self.backup_exclude_dirs_entry = ttk.Entry( config_frame, textvariable=self.backup_exclude_dirs_var, width=60 ) self.backup_exclude_dirs_entry.grid( row=3, column=ce, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) self.create_tooltip( self.backup_exclude_dirs_entry, "Comma-separated list of directory base names to exclude (e.g., __pycache__, build, node_modules). .git and .svn are always excluded.", ) action_frame = ttk.LabelFrame(frame, text="Manual Backup", padding=(10, 5)) action_frame.pack(pady=10, fill="x", expand=False) 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 manual ZIP backup of the working directory now using the configured exclusions.", ) return frame def _create_commit_tab(self): # ... (Codice da gui.py originale) ... frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.rowconfigure(3, weight=1) frame.columnconfigure(0, weight=1) self.autocommit_checkbox = ttk.Checkbutton( frame, text="Enable Autocommit before 'Create Bundle'", variable=self.autocommit_var, state=tk.DISABLED, ) self.autocommit_checkbox.grid( row=0, column=0, columnspan=3, sticky="w", padx=5, pady=(0, 5) ) self.create_tooltip( self.autocommit_checkbox, "If enabled, automatically stage all changes and commit them using the message below before creating a bundle.", ) ttk.Label(frame, text="Commit Message:").grid( row=1, column=0, columnspan=3, sticky="w", padx=5 ) self.commit_message_text = scrolledtext.ScrolledText( frame, height=3, width=60, wrap=tk.WORD, font=("Segoe UI", 9), state=tk.DISABLED, undo=True, padx=5, pady=5, borderwidth=1, relief=tk.SUNKEN, ) self.commit_message_text.grid( row=2, column=0, columnspan=3, sticky="ew", padx=5, pady=(0, 5) ) self.create_tooltip( self.commit_message_text, "Enter the commit message here for manual commits or autocommits.", ) changes_frame = ttk.LabelFrame( frame, text="Working Directory Changes", padding=(10, 5) ) changes_frame.grid( row=3, column=0, columnspan=3, sticky="nsew", padx=5, pady=(5, 5) ) changes_frame.rowconfigure(0, weight=1) changes_frame.columnconfigure(0, weight=1) list_sub_frame = ttk.Frame(changes_frame) list_sub_frame.grid(row=0, column=0, columnspan=2, sticky="nsew", pady=(0, 5)) list_sub_frame.rowconfigure(0, weight=1) list_sub_frame.columnconfigure(0, weight=1) self.changed_files_listbox = tk.Listbox( list_sub_frame, height=8, exportselection=False, selectmode=tk.SINGLE, font=("Consolas", 9), borderwidth=1, relief=tk.SUNKEN, ) self.changed_files_listbox.grid(row=0, column=0, sticky="nsew") self.changed_files_listbox.bind( "", self._on_changed_file_double_click ) self.changed_files_listbox.bind( "", self._show_changed_files_context_menu ) scrollbar_list = ttk.Scrollbar( list_sub_frame, orient=tk.VERTICAL, command=self.changed_files_listbox.yview ) scrollbar_list.grid(row=0, column=1, sticky="ns") self.changed_files_listbox.config(yscrollcommand=scrollbar_list.set) self.create_tooltip( self.changed_files_listbox, "List of changed, added, or deleted files. Double-click to view diff (vs HEAD). Right-click for actions.", ) # self.changed_files_context_menu = tk.Menu(self.changed_files_listbox, tearoff=0) # Menu viene creato in __init__ self.refresh_changes_button = ttk.Button( changes_frame, text="Refresh List", command=self.refresh_changed_files_callback, state=tk.DISABLED, ) self.refresh_changes_button.grid( row=1, column=0, sticky="w", padx=(0, 5), pady=(5, 0) ) self.create_tooltip( self.refresh_changes_button, "Refresh the list of changed files in the working directory.", ) self.commit_button = ttk.Button( frame, text="Commit Staged Changes", command=self.commit_changes_callback, state=tk.DISABLED, ) self.commit_button.grid( row=4, column=2, sticky="se", padx=5, pady=5 ) # Allinea a destra in basso self.create_tooltip( self.commit_button, "Stage all current changes (git add .) and commit them with the provided message.", ) return frame def _create_automation_tab(self): """Creates the Automation tab content.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(0, weight=1) # Permette ai LabelFrame di espandersi orizzontalmente # --- Wiki Synchronization Section --- wiki_frame = ttk.LabelFrame( frame, text="Gitea Wiki Synchronization", padding=(10, 5) ) wiki_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10)) ttk.Label( wiki_frame, text="Update Gitea Wiki pages using local files from the 'doc/' directory.", wraplength=450, justify=tk.LEFT ).pack(pady=(0, 10), fill="x", expand=True) self.update_wiki_button = ttk.Button( wiki_frame, text="Update Gitea Wiki Now", command=self.update_gitea_wiki_callback, state=tk.DISABLED ) self.update_wiki_button.pack(pady=5) self.create_tooltip( self.update_wiki_button, "Clones the associated Gitea Wiki repo, copies 'doc/Manual*.md' files,\n" "commits the changes, and pushes them to the remote wiki." ) # --- History Cleaning Section --- history_frame = ttk.LabelFrame( frame, text="Repository History Maintenance", padding=(10, 5) ) history_frame.grid(row=1, column=0, sticky="ew") ttk.Label( history_frame, text="Analyze repository for committed files that should be ignored and offer to purge them from history.", wraplength=450, justify=tk.LEFT ).pack(pady=(0, 10), fill="x", expand=True) # --- NUOVO PULSANTE --- self.analyze_history_button = ttk.Button( history_frame, text="Analyze & Clean History...", command=self.analyze_and_clean_history_cb, # Nuovo callback state=tk.DISABLED, style="Danger.TButton" # Stile per attirare l'attenzione ) self.analyze_history_button.pack(pady=5) self.create_tooltip( self.analyze_history_button, "DESTRUCTIVE: Analyze history for files to remove.\n" "This action can rewrite the entire repository history." ) return frame def _create_tags_tab(self): # ... (Codice da gui.py originale) ... frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) ttk.Label(frame, text="Existing Tags (Newest First):").grid( row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2) ) 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), borderwidth=1, relief=tk.SUNKEN, ) 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, "List of existing tags. Select a tag to perform actions." ) button_frame = ttk.Frame(frame) button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5)) bw = 18 self.refresh_tags_button = ttk.Button( button_frame, text="Refresh Tags", width=bw, 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 the list of tags from the repository." ) self.create_tag_button = ttk.Button( button_frame, text="Create New Tag...", width=bw, 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, "Create a new annotated tag pointing to the current commit.", ) self.checkout_tag_button = ttk.Button( button_frame, text="Checkout Selected Tag", width=bw, command=self.checkout_tag_callback, state=tk.DISABLED, ) self.checkout_tag_button.pack(side=tk.TOP, fill=tk.X, pady=5) self.create_tooltip( self.checkout_tag_button, "Switch the working directory to the state of the selected tag (Detached HEAD).", ) self.revert_to_tag_button = ttk.Button( button_frame, text="Revert to this Tag", width=bw, command=self.revert_to_tag_callback, # Nuovo callback state=tk.DISABLED, ) self.revert_to_tag_button.pack(side=tk.TOP, fill=tk.X, pady=5) self.create_tooltip( self.revert_to_tag_button, "DESTRUCTIVE: Resets the current branch to this tag.\nAll later commits and uncommitted changes will be lost.", ) return frame def _create_branch_tab(self): # ... (Codice da gui.py originale, usa self.refresh_branches_callback) ... frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) ttk.Label(frame, text="Local Branches (* = Current):").grid( row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2) ) 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, font=("Segoe UI", 9), borderwidth=1, relief=tk.SUNKEN, ) self.branch_listbox.grid(row=0, column=0, sticky="nsew") self.branch_listbox.bind("", self._show_local_branches_context_menu) 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, "List of local branches. Right-click for actions (merge, delete, compare).", ) button_frame = ttk.Frame(frame) button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5)) bw = 18 self.refresh_branches_button = ttk.Button( button_frame, text="Refresh Branches", width=bw, 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 the list of local branches." ) self.create_branch_button = ttk.Button( button_frame, text="Create New Branch...", width=bw, 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 starting from the current commit.", ) self.checkout_branch_button = ttk.Button( button_frame, text="Checkout Selected Branch", width=bw, 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 the working directory to the selected local branch.", ) return frame def _create_remote_tab(self): # ... (Codice da gui.py originale, usa self.refresh_branches_callback per il refresh locale) ... frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(0, weight=1) frame.columnconfigure(1, weight=1) frame.rowconfigure(3, weight=1) top_frame = ttk.LabelFrame( frame, text="Remote Configuration & Sync Status", padding=(10, 5) ) top_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5, 0)) top_frame.columnconfigure(1, weight=1) ttk.Label(top_frame, text="Remote URL:").grid( row=0, column=0, sticky=tk.W, padx=5, pady=3 ) self.remote_url_entry = ttk.Entry( top_frame, textvariable=self.remote_url_var, width=60 ) self.remote_url_entry.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=3) self.create_tooltip( self.remote_url_entry, "URL of the remote repository (e.g., https://... or ssh://...).", ) ttk.Label(top_frame, text="Local Name:").grid( row=1, column=0, sticky=tk.W, padx=5, pady=3 ) self.remote_name_entry = ttk.Entry( top_frame, textvariable=self.remote_name_var, width=20 ) self.remote_name_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=3) self.create_tooltip( self.remote_name_entry, f"Local alias for the remote (Default: '{DEFAULT_REMOTE_NAME}').", ) self.sync_status_label = ttk.Label( top_frame, textvariable=self.remote_ahead_behind_var, anchor=tk.W, padding=(5, 2), ) self.sync_status_label.grid(row=2, column=1, sticky="ew", padx=5, pady=(2, 5)) self.create_tooltip( self.sync_status_label, "Shows the current local branch and its sync status (ahead/behind) relative to its upstream.", ) config_action_frame = ttk.Frame(top_frame) config_action_frame.grid( row=0, column=2, rowspan=3, sticky="ne", padx=(15, 5), pady=3 ) self.apply_remote_config_button = ttk.Button( config_action_frame, text="Apply Config", command=self.apply_remote_config_callback, state=tk.DISABLED, width=18, ) self.apply_remote_config_button.pack(side=tk.TOP, fill=tk.X, pady=1) self.create_tooltip( self.apply_remote_config_button, "Add or update this remote configuration in the local .git/config file.", ) self.check_auth_button = ttk.Button( config_action_frame, text="Check Connection", command=self.check_connection_auth_callback, state=tk.DISABLED, width=18, ) self.check_auth_button.pack(side=tk.TOP, fill=tk.X, pady=1) self.create_tooltip( self.check_auth_button, "Verify connection and authentication status for the configured remote.", ) self.auth_status_indicator_label = ttk.Label( config_action_frame, textvariable=self.remote_auth_status_var, anchor=tk.CENTER, relief=tk.SUNKEN, padding=(5, 1), width=18, ) self.auth_status_indicator_label.pack(side=tk.TOP, fill=tk.X, pady=(1, 3)) self._update_auth_status_indicator("unknown") self.create_tooltip( self.auth_status_indicator_label, "Connection and authentication status (Unknown, Checking, Connected, Auth Required, Failed, Error).", ) self.refresh_sync_status_button = ttk.Button( config_action_frame, text="Refresh Sync Status", command=self.refresh_remote_status_callback, state=tk.DISABLED, width=18, ) self.refresh_sync_status_button.pack(side=tk.TOP, fill=tk.X, pady=(3, 1)) self.create_tooltip( self.refresh_sync_status_button, "Check how many commits the current local branch is ahead or behind its upstream remote branch.", ) actions_frame = ttk.LabelFrame( frame, text="Common Remote Actions", padding=(10, 5) ) actions_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5) action_buttons_inner_frame = ttk.Frame(actions_frame) action_buttons_inner_frame.pack() self.fetch_button = ttk.Button( action_buttons_inner_frame, text="Fetch", command=self.fetch_remote_callback, state=tk.DISABLED, ) self.fetch_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.fetch_button, "Download objects and references from the remote repository (does not modify local branches).", ) self.pull_button = ttk.Button( action_buttons_inner_frame, text="Pull", command=self.pull_remote_callback, state=tk.DISABLED, ) self.pull_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.pull_button, "Fetch changes from the remote and merge them into the current local branch.", ) self.push_button = ttk.Button( action_buttons_inner_frame, text="Push", command=self.push_remote_callback, state=tk.DISABLED, ) self.push_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.push_button, "Upload local commits from the current branch to the corresponding branch on the remote repository.", ) self.push_tags_button = ttk.Button( action_buttons_inner_frame, text="Push Tags", command=self.push_tags_remote_callback, state=tk.DISABLED, ) self.push_tags_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.push_tags_button, "Upload all local tags to the remote repository." ) branch_view_frame = ttk.Frame(frame) branch_view_frame.grid( row=3, column=0, columnspan=2, sticky="nsew", padx=0, pady=(5, 5) ) branch_view_frame.rowconfigure(0, weight=1) branch_view_frame.columnconfigure(0, weight=1) branch_view_frame.columnconfigure(1, weight=1) remote_list_frame = ttk.Frame(branch_view_frame, padding=(5, 0, 10, 0)) remote_list_frame.grid(row=0, column=0, sticky="nsew") remote_list_frame.rowconfigure(1, weight=1) remote_list_frame.columnconfigure(0, weight=1) ttk.Label(remote_list_frame, text="Remote Branches (from last Fetch):").grid( row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2) ) self.remote_branches_listbox = tk.Listbox( remote_list_frame, height=8, exportselection=False, selectmode=tk.SINGLE, font=("Consolas", 9), borderwidth=1, relief=tk.SUNKEN, state=tk.DISABLED, ) self.remote_branches_listbox.grid(row=1, column=0, sticky="nsew") self.remote_branches_listbox.bind( "", self._show_remote_branches_context_menu ) rb_scrollbar = ttk.Scrollbar( remote_list_frame, orient=tk.VERTICAL, command=self.remote_branches_listbox.yview, ) rb_scrollbar.grid(row=1, column=1, sticky="ns") self.remote_branches_listbox.config(yscrollcommand=rb_scrollbar.set) self.create_tooltip( self.remote_branches_listbox, "Branches existing on the remote repository. Right-click for actions (Compare, Checkout as local).", ) self.refresh_remote_branches_button = ttk.Button( remote_list_frame, text="Refresh Remote List", command=self.refresh_remote_branches_callback, state=tk.DISABLED, ) self.refresh_remote_branches_button.grid( row=2, column=0, columnspan=2, sticky="w", padx=5, pady=(5, 0) ) self.create_tooltip( self.refresh_remote_branches_button, "Update the list of remote branches (requires fetching from the remote).", ) local_list_frame = ttk.Frame(branch_view_frame, padding=(10, 0, 5, 0)) local_list_frame.grid(row=0, column=1, sticky="nsew") local_list_frame.rowconfigure(1, weight=1) local_list_frame.columnconfigure(0, weight=1) ttk.Label(local_list_frame, text="Local Branches (* = Current):").grid( row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2) ) self.local_branches_listbox_remote_tab = tk.Listbox( local_list_frame, height=8, exportselection=False, selectmode=tk.SINGLE, font=("Consolas", 9), borderwidth=1, relief=tk.SUNKEN, state=tk.DISABLED, ) self.local_branches_listbox_remote_tab.grid(row=1, column=0, sticky="nsew") self.local_branches_listbox_remote_tab.bind( "", self._show_local_branches_context_menu ) lb_scrollbar_remote_tab = ttk.Scrollbar( local_list_frame, orient=tk.VERTICAL, command=self.local_branches_listbox_remote_tab.yview, ) lb_scrollbar_remote_tab.grid(row=1, column=1, sticky="ns") self.local_branches_listbox_remote_tab.config( yscrollcommand=lb_scrollbar_remote_tab.set ) self.create_tooltip( self.local_branches_listbox_remote_tab, "Your local branches. Right-click for actions (Checkout, Merge, Delete, Compare).", ) self.refresh_local_branches_button_remote_tab = ttk.Button( local_list_frame, text="Refresh Local List", command=self.refresh_branches_callback, state=tk.DISABLED, ) # Usa refresh_branches_callback self.refresh_local_branches_button_remote_tab.grid( row=2, column=0, columnspan=2, sticky="w", padx=5, pady=(5, 0) ) self.create_tooltip( self.refresh_local_branches_button_remote_tab, "Update the list of local branches.", ) return frame def _create_history_tab_treeview(self): # ... (Codice della nuova versione con TreeView, come nella risposta precedente) ... frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.rowconfigure(1, weight=1) frame.columnconfigure(0, weight=1) controls_frame = ttk.Frame(frame) controls_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) controls_frame.columnconfigure(1, weight=1) ttk.Label(controls_frame, text="Filter History by Branch/Tag:").pack( side=tk.LEFT, padx=(0, 5) ) self.history_branch_filter_var = tk.StringVar() self.history_branch_filter_combo = ttk.Combobox( controls_frame, textvariable=self.history_branch_filter_var, state="readonly", width=40, ) self.history_branch_filter_combo.pack( side=tk.LEFT, expand=True, fill=tk.X, padx=5 ) if hasattr(self, "refresh_history_callback") and callable( self.refresh_history_callback ): self.history_branch_filter_combo.bind( "<>", lambda e: self.refresh_history_callback() ) self.create_tooltip( self.history_branch_filter_combo, "Select a branch or tag to filter the commit history.", ) self.refresh_history_button = ttk.Button( controls_frame, text="Refresh History", state=tk.DISABLED ) if hasattr(self, "refresh_history_callback") and callable( self.refresh_history_callback ): self.refresh_history_button.config(command=self.refresh_history_callback) self.refresh_history_button.pack(side=tk.LEFT, padx=5) self.create_tooltip( self.refresh_history_button, "Reload commit history based on the selected filter.", ) content_frame = ttk.Frame(frame) content_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5)) content_frame.rowconfigure(0, weight=1) content_frame.columnconfigure(0, weight=1) columns = ("hash", "datetime", "author", "details") self.history_tree = ttk.Treeview( content_frame, columns=columns, show="headings", selectmode="browse", height=15, ) self.history_tree.heading("hash", text="Hash", anchor="w") self.history_tree.heading("datetime", text="Date/Time", anchor="w") self.history_tree.heading("author", text="Author", anchor="w") self.history_tree.heading("details", text="Subject / Refs", anchor="w") self.history_tree.column("hash", width=80, stretch=tk.NO, anchor="w") self.history_tree.column("datetime", width=140, stretch=tk.NO, anchor="w") self.history_tree.column("author", width=150, stretch=tk.NO, anchor="w") self.history_tree.column("details", width=450, stretch=tk.YES, anchor="w") tree_scrollbar_y = ttk.Scrollbar( content_frame, orient=tk.VERTICAL, command=self.history_tree.yview ) tree_scrollbar_x = ttk.Scrollbar( content_frame, orient=tk.HORIZONTAL, command=self.history_tree.xview ) self.history_tree.configure( yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set ) self.history_tree.grid(row=0, column=0, sticky="nsew") tree_scrollbar_y.grid(row=0, column=1, sticky="ns") tree_scrollbar_x.grid(row=1, column=0, columnspan=2, sticky="ew") self.history_tree.bind("", self._on_history_double_click_tree) self.create_tooltip( self.history_tree, "Double-click a commit line to view details." ) return frame def _create_log_area(self, parent_frame): # ... (Codice da gui.py originale) ... log_frame = ttk.LabelFrame( parent_frame, text="Application Log", padding=(10, 5) ) log_frame.pack(fill=tk.BOTH, expand=True) log_frame.rowconfigure(0, weight=1) log_frame.columnconfigure(0, weight=1) self.log_text = scrolledtext.ScrolledText( log_frame, height=8, width=100, font=("Consolas", 9), wrap=tk.WORD, state=tk.DISABLED, padx=5, pady=5, borderwidth=1, relief=tk.SUNKEN, ) self.log_text.grid(row=0, column=0, sticky="nsew") # Configura tag per colori log self.log_text.tag_config("INFO", foreground="black") self.log_text.tag_config("DEBUG", foreground="grey") self.log_text.tag_config("WARNING", foreground="orange") self.log_text.tag_config("ERROR", foreground="red") self.log_text.tag_config( "CRITICAL", foreground="red", font=("Consolas", 9, "bold") ) def _initialize_profile_selection(self): # ... (Codice da gui.py originale) ... # Imposta il primo profilo o il default se esiste current_profiles = self.profile_dropdown.cget("values") if not isinstance(current_profiles, (list, tuple)): current_profiles = [] target_profile = "" if DEFAULT_PROFILE in current_profiles: target_profile = DEFAULT_PROFILE elif current_profiles: target_profile = current_profiles[0] if target_profile: self.profile_var.set(target_profile) else: self.profile_var.set("") self.update_status_bar("No profiles found. Please add or clone a profile.") # --- GUI Update Methods --- def toggle_backup_dir(self): # ... (Codice da gui.py originale) ... state = tk.NORMAL if self.autobackup_var.get() else tk.DISABLED if hasattr(self, "backup_dir_entry") and self.backup_dir_entry.winfo_exists(): self.backup_dir_entry.config(state=state) if hasattr(self, "backup_dir_button") and self.backup_dir_button.winfo_exists(): self.backup_dir_button.config(state=state) def browse_backup_dir(self): # ... (Codice da gui.py originale) ... curr = self.backup_dir_var.get() init = curr if os.path.isdir(curr) else DEFAULT_BACKUP_DIR if not os.path.isdir(init): init = os.path.expanduser("~") sel = filedialog.askdirectory( initialdir=init, title="Select Backup Directory", parent=self.master ) if sel: self.backup_dir_var.set(sel) def update_svn_indicator(self, is_prepared): # ... (Codice da gui.py originale) ... color = self.GREEN if is_prepared else self.RED tip = ( "Repository is valid and prepared (found .git)." if is_prepared else "Directory is not a valid/prepared Git repository." ) if ( hasattr(self, "svn_status_indicator") and self.svn_status_indicator.winfo_exists() ): self.svn_status_indicator.config(background=color) self.update_tooltip(self.svn_status_indicator, tip) # Usa metodo helper def update_profile_dropdown(self, sections: List[str]): # ... (Codice da gui.py originale) ... if hasattr(self, "profile_dropdown") and self.profile_dropdown.winfo_exists(): curr = self.profile_var.get() self.profile_dropdown["values"] = sections if sections: if curr in sections: self.profile_var.set(curr) else: if DEFAULT_PROFILE in sections: self.profile_var.set(DEFAULT_PROFILE) else: self.profile_var.set(sections[0]) else: self.profile_var.set("") def update_tag_list(self, tags_data: List[Tuple[str, str]]): # ... (Codice da gui.py originale) ... listbox = getattr(self, "tag_listbox", None) if not listbox or not listbox.winfo_exists(): return try: listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) if tags_data: try: default_fg = self.style.lookup("TListbox", "foreground") if listbox.cget("fg") == "grey": listbox.config(fg=default_fg) except tk.TclError: pass for name, subj in tags_data: listbox.insert(tk.END, f"{name}\t({subj})") else: listbox.insert(tk.END, "(No tags found)") listbox.config(fg="grey") listbox.config(state=tk.NORMAL) listbox.yview_moveto(0.0) except Exception as e: print(f"ERROR updating tag list GUI: {e}", file=sys.stderr) # Log fallback try: if listbox.winfo_exists(): listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) listbox.insert(tk.END, "(Error)") listbox.config(fg="red", state=tk.DISABLED) except Exception: pass def update_branch_list(self, branches: List[str], current_branch: Optional[str]): # ... (Codice da gui.py originale, aggiorna entrambe le listbox locali) ... func_name = "update_branch_list (GUI)" log_handler.log_debug( f"Updating local branches. Current: {repr(current_branch)}", func_name=func_name, ) self.current_local_branch = current_branch listboxes_to_update = [] lb1 = getattr(self, "branch_listbox", None) lb2 = getattr(self, "local_branches_listbox_remote_tab", None) if lb1 and lb1.winfo_exists(): listboxes_to_update.append(lb1) if lb2 and lb2.winfo_exists(): listboxes_to_update.append(lb2) if not listboxes_to_update: log_handler.log_warning( "No local branch listbox found for update.", func_name=func_name ) return for listbox in listboxes_to_update: widget_name = "unknown_listbox" try: widget_name = listbox.winfo_pathname(listbox.winfo_id()) except Exception: pass try: listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) sel_idx = -1 if isinstance(branches, list) and branches: try: default_fg = self.style.lookup("TListbox", "foreground") if listbox.cget("fg") != default_fg: listbox.config(fg=default_fg) except tk.TclError: pass for i, branch in enumerate(branches): prefix = "* " if branch == current_branch else " " listbox.insert(tk.END, f"{prefix}{str(branch)}") if branch == current_branch: sel_idx = i elif isinstance(branches, list) and not branches: listbox.insert(tk.END, "(No local branches)") listbox.config(fg="grey") else: listbox.insert(tk.END, "(Invalid data received)") listbox.config(fg="orange") if sel_idx >= 0: listbox.selection_set(sel_idx) listbox.see(sel_idx) listbox.config(state=tk.NORMAL) listbox.yview_moveto(0.0) except Exception as e: log_handler.log_exception( f"Error updating local branch list GUI ({widget_name}): {e}", func_name=func_name, ) try: if listbox.winfo_exists(): listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) listbox.insert(tk.END, "(Error)") listbox.config(fg="red", state=tk.DISABLED) except Exception: pass def get_selected_tag(self) -> Optional[str]: # ... (Codice da gui.py originale) ... listbox = getattr(self, "tag_listbox", None) item = None if listbox and listbox.winfo_exists(): idx = listbox.curselection() if idx: item = listbox.get(idx[0]) if item: if "\t" in item and not item.startswith("("): return item.split("\t", 1)[0].strip() elif not item.startswith("("): return item.strip() return None def get_selected_branch(self) -> Optional[str]: # ... (Codice da gui.py originale) ... listbox = getattr(self, "branch_listbox", None) item = None name = None if listbox and listbox.winfo_exists(): idx = listbox.curselection() if idx: item = listbox.get(idx[0]) if item: name = item.lstrip("* ").strip() if name and not name.startswith("("): return name return None def get_commit_message(self) -> str: # ... (Codice da gui.py originale) ... if ( hasattr(self, "commit_message_text") and self.commit_message_text.winfo_exists() ): return self.commit_message_text.get("1.0", "end-1c").strip() return "" def clear_commit_message(self): # ... (Codice da gui.py originale) ... if ( hasattr(self, "commit_message_text") and self.commit_message_text.winfo_exists() ): try: state = self.commit_message_text.cget("state") self.commit_message_text.config(state=tk.NORMAL) self.commit_message_text.delete("1.0", tk.END) self.commit_message_text.config(state=state) self.commit_message_text.edit_reset() except Exception: pass def _on_history_double_click_tree( self, event: tk.Event ) -> None: # Assicurati che questo sia il metodo corretto # ... (Codice aggiornato per Treeview, come nella risposta precedente) ... func_name = "_on_history_double_click_tree" tree = getattr(self, "history_tree", None) view_cb = getattr(self, "view_commit_details_callback", None) tree_exists = tree is not None and tree.winfo_exists() cb_callable = callable(view_cb) log_handler.log_debug( f"Tree exists: {tree_exists}. Callback callable: {cb_callable}.", func_name=func_name, ) if view_cb and not cb_callable: log_handler.log_warning( f"view_commit_details_callback is NOT callable. Type: {type(view_cb)}, Value: {repr(view_cb)}", func_name=func_name, ) if not tree_exists or not cb_callable: log_handler.log_warning( "History tree or view commit details callback not configured or available.", func_name=func_name, ) return try: selected_iid = tree.focus() if not selected_iid: log_handler.log_debug( "No item selected in history tree on double click.", func_name=func_name, ) return item_data = tree.item(selected_iid) item_values = item_data.get("values") if item_values and len(item_values) > 0: commit_hash_short = str(item_values[0]).strip() if commit_hash_short and not commit_hash_short.startswith("("): log_handler.log_info( f"History tree double-clicked. Requesting details for hash: '{commit_hash_short}'", func_name=func_name, ) view_cb(commit_hash_short) else: log_handler.log_debug( f"Ignoring double-click on placeholder/invalid history item: {item_values}", func_name=func_name, ) else: log_handler.log_warning( f"Could not get values for selected history item IID: {selected_iid}", func_name=func_name, ) except Exception as e: log_handler.log_exception( f"Error handling history tree double-click: {e}", func_name=func_name ) messagebox.showerror( "Error", f"Could not process history selection:\n{e}", parent=self ) def update_history_display( self, log_lines: List[str] ): # Assicurati che questo sia il metodo corretto # ... (Codice aggiornato per Treeview, come nella risposta precedente) ... func_name = "update_history_display_(Tree)" log_handler.log_debug( f"Updating history display (Treeview) with {len(log_lines) if isinstance(log_lines, list) else 'N/A'} lines.", func_name=func_name, ) tree = getattr(self, "history_tree", None) if not tree or not tree.winfo_exists(): log_handler.log_error( "history_tree widget not available for update.", func_name=func_name ) return try: for item_id in tree.get_children(): tree.delete(item_id) if isinstance(log_lines, list): if log_lines: for i, line in enumerate(log_lines): line_str = str(line).strip() if not line_str or line_str.startswith("("): continue commit_hash, commit_datetime, commit_author, commit_details = ( "", "", "", line_str, ) try: parts = line_str.split("|", 2) if len(parts) >= 3: part1 = parts[0].strip() part2 = parts[1].strip() part3 = parts[2].strip() first_space = part1.find(" ") if first_space != -1: commit_hash = part1[:first_space] commit_datetime = part1[first_space:].strip()[:16] else: commit_hash = part1 commit_datetime = "N/A" commit_author = part2 commit_details = part3 elif len(parts) == 2: part1 = parts[0].strip() part3 = parts[1].strip() first_space = part1.find(" ") if first_space != -1: commit_hash = part1[:first_space] commit_datetime = part1[first_space:].strip()[:16] else: commit_hash = part1 commit_datetime = "N/A" commit_author = "N/A" commit_details = part3 elif len(parts) == 1: commit_details = line_str commit_hash = "N/A" commit_datetime = "N/A" commit_author = "N/A" except Exception as parse_err: log_handler.log_warning( f"Could not parse history line '{line_str}': {parse_err}", func_name=func_name, ) commit_hash = "Error" commit_datetime = "" commit_author = "" commit_details = line_str tree.insert( parent="", index=tk.END, iid=i, values=( commit_hash, commit_datetime, commit_author, commit_details, ), ) else: tree.insert( parent="", index=tk.END, iid=0, values=("", "", "", "(No history found)"), ) else: log_handler.log_warning( f"Invalid data received for history (Treeview): {repr(log_lines)}", func_name=func_name, ) tree.insert( parent="", index=tk.END, iid=0, values=("", "", "", "(Error: Invalid data received)"), ) tree.yview_moveto(0.0) tree.xview_moveto(0.0) except Exception as e: log_handler.log_exception( f"Error updating history Treeview GUI: {e}", func_name=func_name ) try: if tree.winfo_exists(): for item_id in tree.get_children(): tree.delete(item_id) tree.insert( parent="", index=tk.END, iid=0, values=("", "", "", "(Error displaying history)"), ) except Exception as fallback_e: log_handler.log_error( f"Error displaying fallback error in history Treeview: {fallback_e}", func_name=func_name, ) def update_history_branch_filter( self, branches_tags: List[str], current_ref: Optional[str] = None ): # ... (Codice da gui.py originale) ... combo = getattr(self, "history_branch_filter_combo", None) var = getattr(self, "history_branch_filter_var", None) if not combo or not var or not combo.winfo_exists(): return opts = ["-- All History --"] + sorted(branches_tags if branches_tags else []) combo["values"] = opts # Set selection: use current_ref if valid, otherwise default to All History if current_ref and current_ref in opts: var.set(current_ref) else: var.set(opts[0]) def update_changed_files_list(self, files_status_list: List[str]): # ... (Codice da gui.py originale, con sanificazione) ... listbox = getattr(self, "changed_files_listbox", None) if not listbox or not listbox.winfo_exists(): print( "ERROR: changed_files_listbox not available for update.", file=sys.stderr, ) return try: listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) if files_status_list: try: default_fg = self.style.lookup("TListbox", "foreground") if listbox.cget("fg") == "grey": listbox.config(fg=default_fg) except tk.TclError: pass processed_lines = 0 for status_line in files_status_list: try: sanitized_line = str(status_line).replace("\x00", "").strip() if sanitized_line: listbox.insert(tk.END, sanitized_line) processed_lines += 1 else: print( f"Warning: Sanitized status line empty: {repr(status_line)}", file=sys.stderr, ) except Exception as insert_err: print( f"ERROR inserting line into listbox: {insert_err} - Line: {repr(status_line)}", file=sys.stderr, ) listbox.insert( tk.END, f"(Error processing line: {repr(status_line)})" ) listbox.itemconfig(tk.END, {"fg": "red"}) if processed_lines == 0 and files_status_list: listbox.insert(tk.END, "(Error processing all lines)") listbox.config(fg="red") else: listbox.insert(tk.END, "(No changes detected)") listbox.config(fg="grey") listbox.config(state=tk.NORMAL) listbox.yview_moveto(0.0) except Exception as e: print(f"ERROR updating changed files list GUI: {e}", file=sys.stderr) try: listbox.delete(0, tk.END) listbox.insert(tk.END, "(Error updating list)") listbox.config(fg="red") except: pass def _on_changed_file_double_click(self, event: tk.Event) -> None: # ... (Codice da gui.py originale) ... widget = event.widget sel = widget.curselection() line = None if sel: line = widget.get(sel[0]) if ( line and hasattr(self, "open_diff_viewer_callback") and callable(self.open_diff_viewer_callback) ): self.open_diff_viewer_callback(line) def _show_changed_files_context_menu(self, event: tk.Event) -> None: # ... (Codice da gui.py originale, con correzione recupero 'line') ... func_name = "_show_changed_files_context_menu" line = None listbox = getattr(self, "changed_files_listbox", None) if not listbox: return try: idx = listbox.nearest(event.y) listbox.selection_clear(0, tk.END) listbox.selection_set(idx) listbox.activate(idx) line = listbox.get(idx) except tk.TclError: log_handler.log_debug( f"TclError getting selected line for context menu.", func_name=func_name ) return except Exception as e: log_handler.log_error( f"Error getting selected line for context menu: {e}", func_name=func_name, ) return if line is None: log_handler.log_debug( f"Could not retrieve line content at index.", func_name=func_name ) return log_handler.log_debug(f"Context menu for line: '{line}'", func_name=func_name) self.changed_files_context_menu.delete(0, tk.END) cleaned = line.strip() is_untracked = cleaned.startswith("??") can_add = ( is_untracked and hasattr(self, "add_selected_file_callback") and callable(self.add_selected_file_callback) ) add_state = tk.NORMAL if can_add else tk.DISABLED self.changed_files_context_menu.add_command( label="Add to Staging Area", state=add_state, command=lambda current_line=line: ( self.add_selected_file_callback(current_line) if can_add else None ), ) can_diff = hasattr(self, "open_diff_viewer_callback") and callable( self.open_diff_viewer_callback ) diff_state = tk.DISABLED if ( not is_untracked and not cleaned.startswith("!!") and not cleaned.startswith(" D") and can_diff ): diff_state = tk.NORMAL self.changed_files_context_menu.add_command( label="View Changes (Diff)", state=diff_state, command=lambda current_line=line: ( self.open_diff_viewer_callback(current_line) if diff_state == tk.NORMAL else None ), ) try: self.changed_files_context_menu.tk_popup(event.x_root, event.y_root) finally: self.changed_files_context_menu.grab_release() def update_status_bar( self, message: str, bg_color: Optional[str] = None, duration_ms: Optional[int] = None, ): # ... (Codice da gui.py originale) ... if hasattr(self, "status_bar_var") and hasattr(self, "status_bar"): try: if self._status_reset_timer: self.master.after_cancel(self._status_reset_timer) self._status_reset_timer = None def _update(): if self.status_bar.winfo_exists(): self.status_bar_var.set(message) actual_bg = ( bg_color if bg_color else MainFrame.STATUS_DEFAULT_BG ) try: self.status_bar.config(background=actual_bg) except tk.TclError: self.status_bar.config( background=MainFrame.STATUS_DEFAULT_BG ) print( f"Warning: Invalid status bar color '{bg_color}', using default.", file=sys.stderr, ) if bg_color and duration_ms and duration_ms > 0: self._status_reset_timer = self.master.after( duration_ms, self.reset_status_bar_color ) self.master.after(0, _update) except Exception as e: print(f"ERROR updating status bar: {e}", file=sys.stderr) def reset_status_bar_color(self): # ... (Codice da gui.py originale) ... self._status_reset_timer = None if hasattr(self, "status_bar") and self.status_bar.winfo_exists(): try: self.status_bar.config(background=MainFrame.STATUS_DEFAULT_BG) except Exception as e: print(f"ERROR resetting status bar color: {e}", file=sys.stderr) def ask_new_profile_name(self) -> Optional[str]: # ... (Codice da gui.py originale) ... return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master) def show_error(self, title: str, message: str): # ... (Codice da gui.py originale) ... messagebox.showerror(title, message, parent=self.master) def show_info(self, title: str, message: str): # ... (Codice da gui.py originale) ... messagebox.showinfo(title, message, parent=self.master) def show_warning(self, title: str, message: str): # ... (Codice da gui.py originale) ... messagebox.showwarning(title, message, parent=self.master) def ask_yes_no(self, title: str, message: str) -> bool: # ... (Codice da gui.py originale) ... return messagebox.askyesno(title, message, parent=self.master) def create_tooltip(self, widget: tk.Widget, text: str): # ... (Codice da gui.py originale) ... # Assicurati che la classe Tooltip sia importata correttamente if widget and isinstance(widget, tk.Widget) and widget.winfo_exists(): Tooltip(widget, text) def update_tooltip(self, widget: tk.Widget, text: str): # ... (Codice da gui.py originale) ... self.create_tooltip(widget, text) def set_action_widgets_state(self, state: str): # ... (Codice da gui.py originale, aggiornato per TreeView) ... if state not in [tk.NORMAL, tk.DISABLED]: log_handler.log_warning( f"Invalid state requested for widgets: {state}", func_name="set_action_widgets_state", ) return widgets_with_state_option = [ getattr(self, name, None) for name in [ "save_settings_button", "remove_profile_button", "prepare_svn_button", "create_bundle_button", "fetch_bundle_button", "edit_gitignore_button", "manual_backup_button", "autobackup_checkbox", "backup_dir_entry", "backup_dir_button", "commit_button", "refresh_changes_button", "commit_message_text", "autocommit_checkbox", "refresh_tags_button", "create_tag_button", "checkout_tag_button", "revert_to_tag_button", "refresh_branches_button", "create_branch_button", "checkout_branch_button", "history_branch_filter_combo", "refresh_history_button", "apply_remote_config_button", "check_auth_button", "fetch_button", "pull_button", "push_button", "push_tags_button", "refresh_sync_status_button", "refresh_remote_branches_button", "refresh_local_branches_button_remote_tab", "update_wiki_button", ] ] log_handler.log_debug( f"Setting {len(widgets_with_state_option)} action widgets state to: {state}", func_name="set_action_widgets_state", ) failed_widgets = [] for widget in widgets_with_state_option: if not widget or not widget.winfo_exists(): continue widget_attr_name = None for attr, value in self.__dict__.items(): if value is widget: widget_attr_name = attr break try: w_state = state if isinstance(widget, ttk.Combobox): w_state = "readonly" if state == tk.NORMAL else tk.DISABLED elif isinstance(widget, (tk.Entry, tk.Text, scrolledtext.ScrolledText)): w_state = state elif isinstance(widget, (ttk.Button, ttk.Checkbutton)): w_state = state widget.config(state=w_state) except Exception as e: failed_widgets.append(f"{widget_attr_name or 'UnknownWidget'}: {e}") if failed_widgets: log_handler.log_error( f"Error setting state for some widgets: {'; '.join(failed_widgets)}", func_name="set_action_widgets_state", ) self.toggle_backup_dir() profile_dropdown = getattr(self, "profile_dropdown", None) if profile_dropdown and profile_dropdown.winfo_exists(): try: profile_dropdown.config(state="readonly") except Exception as e: log_handler.log_error( f"Error setting state for profile dropdown: {e}", func_name="set_action_widgets_state", ) list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED interactive_lists = [ getattr(self, name, None) for name in [ "tag_listbox", "branch_listbox", "changed_files_listbox", "remote_branches_listbox", "local_branches_listbox_remote_tab", ] ] for lb in interactive_lists: if lb and lb.winfo_exists() and isinstance(lb, tk.Listbox): try: lb.config(state=list_state) except Exception: pass history_tree = getattr(self, "history_tree", None) if history_tree and history_tree.winfo_exists(): try: if state == tk.DISABLED: history_tree.unbind("") else: if hasattr(self, "_on_history_double_click_tree"): history_tree.bind( "", self._on_history_double_click_tree ) except Exception as e: log_handler.log_error( f"Error configuring history tree bindings: {e}", func_name="set_action_widgets_state", ) def update_ahead_behind_status( self, current_branch: Optional[str] = None, status_text: Optional[str] = None, ahead: Optional[int] = None, behind: Optional[int] = None, ): # ... (Codice da gui.py originale) ... label = getattr(self, "sync_status_label", None) var = getattr(self, "remote_ahead_behind_var", None) if not label or not var or not label.winfo_exists(): return if current_branch: branch_part = f"Branch '{current_branch}': " else: branch_part = "Current Branch: " status_part = "Unknown" if status_text is not None: status_part = status_text if ( "Branch" in status_part or "Detached" in status_part or "Upstream" in status_part ): text_to_display = status_part else: text_to_display = branch_part + status_part elif ahead is not None and behind is not None: if ahead == 0 and behind == 0: status_part = "Up to date" else: parts = [] if ahead > 0: plural_a = "s" if ahead > 1 else "" parts.append(f"{ahead} commit{plural_a} ahead (Push needed)") if behind > 0: plural_b = "s" if behind > 1 else "" parts.append(f"{behind} commit{plural_b} behind (Pull needed)") status_part = ", ".join(parts) text_to_display = branch_part + status_part else: text_to_display = branch_part + "Unknown Status" try: var.set(text_to_display) except Exception as e: log_handler.log_error( f"Failed to update sync status variable: {e}", func_name="update_ahead_behind_status", ) def update_remote_branches_list(self, remote_branch_list: List[str]): # ... (Codice da gui.py originale) ... listbox = getattr(self, "remote_branches_listbox", None) if not listbox or not listbox.winfo_exists(): log_handler.log_error( "remote_branches_listbox not available for update.", func_name="update_remote_branches_list", ) return try: listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) if remote_branch_list and remote_branch_list != ["(Error)"]: try: default_fg = self.style.lookup("TListbox", "foreground") if listbox.cget("fg") != default_fg: listbox.config(fg=default_fg) except tk.TclError: pass for branch_name in remote_branch_list: listbox.insert(tk.END, f" {branch_name}") elif remote_branch_list == ["(Error)"]: listbox.insert(tk.END, "(Error retrieving list)") listbox.config(fg="red") else: listbox.insert(tk.END, "(No remote branches found)") listbox.config(fg="grey") listbox.config(state=tk.NORMAL) listbox.yview_moveto(0.0) except Exception as e: log_handler.log_exception( f"Error updating remote branches list GUI: {e}", func_name="update_remote_branches_list", ) try: if listbox.winfo_exists(): listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) listbox.insert(tk.END, "(Error updating list)") listbox.config(fg="red", state=tk.DISABLED) except Exception: pass def _update_auth_status_indicator(self, status: str): # ... (Codice da gui.py originale) ... label = getattr(self, "auth_status_indicator_label", None) if not label or not label.winfo_exists(): return text = "Status: Unknown" color = MainFrame.STATUS_DEFAULT_BG tooltip = "Connection and authentication status." if status == "ok": text = "Status: Connected" color = self.STATUS_GREEN tooltip = "Successfully connected and authenticated to the remote." elif status == "required": text = "Status: Auth Required" color = self.STATUS_YELLOW tooltip = "Authentication needed. Use 'Check Connection' to attempt interactive login." elif status == "failed": text = "Status: Auth Failed" color = self.STATUS_RED tooltip = "Authentication failed. Check credentials or use 'Check Connection' to retry." elif status == "connection_failed": text = "Status: Connection Failed" color = self.STATUS_RED tooltip = "Could not connect to the remote. Check URL and network." elif status == "unknown_error": text = "Status: Error" color = self.STATUS_RED tooltip = "An unknown error occurred while checking the remote." elif status == "checking": text = "Status: Checking..." color = self.STATUS_YELLOW tooltip = "Attempting to contact the remote repository..." # Aggiunto stato checking try: self.remote_auth_status_var.set(text) label.config(background=color) self.update_tooltip(label, tooltip) except Exception as e: log_handler.log_error( f"Failed to update auth status indicator GUI: {e}", func_name="_update_auth_status_indicator", ) def _show_remote_branches_context_menu(self, event: tk.Event) -> None: # ... (Codice da gui.py originale) ... func_name = "_show_remote_branches_context_menu" listbox = event.widget selected_index = None try: selected_index = listbox.nearest(event.y) if selected_index < 0: return listbox.selection_clear(0, tk.END) listbox.selection_set(selected_index) listbox.activate(selected_index) selected_item_text = listbox.get(selected_index).strip() self.remote_branch_context_menu.delete(0, tk.END) is_valid_branch = ( "/" in selected_item_text and not selected_item_text.startswith("(") ) if is_valid_branch: remote_branch_full_name = selected_item_text slash_index = remote_branch_full_name.find("/") local_branch_suggestion = ( remote_branch_full_name[slash_index + 1 :] if slash_index != -1 else "" ) current_branch_name_local = self.current_local_branch compare_state = tk.NORMAL compare_label = f"Compare '{remote_branch_full_name}' with current..." if current_branch_name_local: compare_label = f"Compare '{remote_branch_full_name}' with current '{current_branch_name_local}'" self.remote_branch_context_menu.add_command( label=compare_label, state=compare_state, command=lambda b_other=remote_branch_full_name: ( self.compare_branch_with_current_callback(b_other) if callable(self.compare_branch_with_current_callback) else None ), ) if local_branch_suggestion: self.remote_branch_context_menu.add_command( label=f"Checkout as new local branch '{local_branch_suggestion}'", command=lambda rb=remote_branch_full_name, lb=local_branch_suggestion: ( self.checkout_remote_branch_callback(rb, lb) if callable(self.checkout_remote_branch_callback) else None ), ) self.remote_branch_context_menu.add_separator() self.remote_branch_context_menu.add_command(label="Cancel") else: self.remote_branch_context_menu.add_command( label="(No actions available)", state=tk.DISABLED ) self.remote_branch_context_menu.tk_popup(event.x_root, event.y_root) except tk.TclError: log_handler.log_debug( "TclError during remote branch context menu display.", func_name=func_name, ) except Exception as e: log_handler.log_exception( f"Error showing remote branch context menu: {e}", func_name=func_name ) finally: if hasattr(self, "remote_branch_context_menu"): self.remote_branch_context_menu.grab_release() def _show_local_branches_context_menu(self, event: tk.Event) -> None: # ... (Codice da gui.py originale) ... func_name = "_show_local_branches_context_menu" listbox = event.widget selected_index = None current_branch_name_local = self.current_local_branch # Usa variabile membro try: selected_index = listbox.nearest(event.y) if selected_index < 0: return listbox.selection_clear(0, tk.END) listbox.selection_set(selected_index) listbox.activate(selected_index) selected_item_text = listbox.get(selected_index).strip() self.local_branch_context_menu.delete(0, tk.END) is_current_selected = selected_item_text.startswith("*") local_branch_name_selected = selected_item_text.lstrip("* ").strip() is_valid_branch = not local_branch_name_selected.startswith("(") if is_valid_branch: checkout_state = tk.DISABLED if is_current_selected else tk.NORMAL self.local_branch_context_menu.add_command( label=f"Checkout Branch '{local_branch_name_selected}'", state=checkout_state, command=lambda b=local_branch_name_selected: ( self.checkout_branch_callback(branch_to_checkout=b) if callable(self.checkout_branch_callback) else None ), ) merge_state = tk.DISABLED merge_label = f"Merge '{local_branch_name_selected}' into current..." if not is_current_selected and current_branch_name_local: merge_state = tk.NORMAL merge_label = f"Merge '{local_branch_name_selected}' into current '{current_branch_name_local}'" self.local_branch_context_menu.add_command( label=merge_label, state=merge_state, command=lambda b_source=local_branch_name_selected: ( self.merge_local_branch_callback(b_source) if callable(self.merge_local_branch_callback) else None ), ) delete_state = tk.DISABLED if is_current_selected else tk.NORMAL self.local_branch_context_menu.add_command( label=f"Delete Branch '{local_branch_name_selected}'...", state=delete_state, command=lambda b=local_branch_name_selected: ( self.delete_local_branch_callback(b, force=False) if callable(self.delete_local_branch_callback) else None ), ) self.local_branch_context_menu.add_command( label=f"Force Delete Branch '{local_branch_name_selected}'...", state=delete_state, command=lambda b=local_branch_name_selected: ( self.delete_local_branch_callback(b, force=True) if callable(self.delete_local_branch_callback) else None ), ) compare_state = tk.DISABLED if is_current_selected else tk.NORMAL compare_label = ( f"Compare '{local_branch_name_selected}' with current..." ) if current_branch_name_local and not is_current_selected: compare_label = f"Compare '{local_branch_name_selected}' with current '{current_branch_name_local}'" self.local_branch_context_menu.add_command( label=compare_label, state=compare_state, command=lambda b_other=local_branch_name_selected: ( self.compare_branch_with_current_callback(b_other) if callable(self.compare_branch_with_current_callback) else None ), ) self.local_branch_context_menu.add_separator() self.local_branch_context_menu.add_command(label="Cancel") else: self.local_branch_context_menu.add_command( label="(No actions available)", state=tk.DISABLED ) self.local_branch_context_menu.tk_popup(event.x_root, event.y_root) except tk.TclError: log_handler.log_debug( "TclError during local branch context menu display.", func_name=func_name, ) except Exception as e: log_handler.log_exception( f"Error showing local branch context menu: {e}", func_name=func_name ) finally: if hasattr(self, "local_branch_context_menu"): self.local_branch_context_menu.grab_release() # --- END OF FILE gitsync_tool/gui/main_frame.py ---