SXXXXXXX_GitUtility/gitutility/gui/main_frame.py
2025-05-05 10:28:19 +02:00

2203 lines
93 KiB
Python

# --- 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],
refresh_history_cb: Callable[[], None],
refresh_branches_cb: Callable[[], None], # Callback unico per refresh locali
checkout_branch_cb: Callable[
[Optional[str], Optional[str]], None
], # Modificato per accettare override
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],
# Altre dipendenze
config_manager_instance: Any, # Evita import ConfigManager qui
profile_sections_list: List[str],
):
# ---<<< FINE MODIFICA >>>---
"""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.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
# 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
# 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.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(
"<<ComboboxSelected>>",
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(
"<FocusOut>",
lambda e: self.update_svn_status_callback(self.svn_path_entry.get()),
)
self.svn_path_entry.bind(
"<Return>",
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(
"<Double-Button-1>", self._on_changed_file_double_click
)
self.changed_files_listbox.bind(
"<Button-3>", 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_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).",
)
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("<Button-3>", 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(
"<Button-3>", 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(
"<Button-3>", 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(
"<<ComboboxSelected>>", 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("<Double-Button-1>", 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",
"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",
]
]
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("<Double-Button-1>")
else:
if hasattr(self, "_on_history_double_click_tree"):
history_tree.bind(
"<Double-Button-1>", 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 ---