# --- START OF FILE gui.py --- # gui.py import tkinter as tk from tkinter import ttk from tkinter import scrolledtext, filedialog, messagebox, simpledialog # Rimosso import logging import os import re # Needed for validation in dialogs import sys # Per fallback print # Importa il gestore della coda log (anche se gui.py non logga molto direttamente) import log_handler from typing import Tuple, Dict, List, Callable # Import constant from the central location if available try: from config_manager import DEFAULT_BACKUP_DIR, DEFAULT_REMOTE_NAME except ImportError: DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "backup_fallback") DEFAULT_REMOTE_NAME = "origin" # Fallback per il nome del remote # Usa print qui perché il sistema di log potrebbe non essere inizializzato print( f"WARNING: gui.py could not import constants. Using fallbacks.", file=sys.stderr, ) # --- Tooltip Class Definition (invariata rispetto all'ultima versione, non logga) --- class Tooltip: """Simple tooltip implementation for Tkinter widgets.""" def __init__(self, widget, text): self.widget = widget self.text = text self.tooltip_window = None self.id = None if self.widget and self.widget.winfo_exists(): self.widget.bind("", self.enter, add="+") self.widget.bind("", self.leave, add="+") self.widget.bind("", self.leave, add="+") def enter(self, event=None): self.unschedule() id = None # PEP8 Fix: assignment on separate line def leave(self, event=None): self.unschedule() self.hidetip() def unschedule(self): id_to_cancel = self.id self.id = None if id_to_cancel: try: if self.widget and self.widget.winfo_exists(): self.widget.after_cancel(id_to_cancel) except Exception: pass def showtip(self): if not self.widget or not self.widget.winfo_exists(): return self.hidetip() x_cursor = 0 y_cursor = 0 # Init vars try: x_cursor = self.widget.winfo_pointerx() + 15 y_cursor = self.widget.winfo_pointery() + 10 except Exception: try: x_root = self.widget.winfo_rootx() y_root = self.widget.winfo_rooty() x_cursor = x_root + self.widget.winfo_width() // 2 y_cursor = y_root + self.widget.winfo_height() + 5 except Exception: return self.tooltip_window = tw = tk.Toplevel(self.widget) tw.wm_overrideredirect(True) try: tw.wm_geometry(f"+{int(x_cursor)}+{int(y_cursor)}") except tk.TclError: tw.destroy() self.tooltip_window = None return label = tk.Label( tw, text=self.text, justify=tk.LEFT, background="#ffffe0", relief=tk.SOLID, borderwidth=1, font=("tahoma", "8", "normal"), wraplength=350, ) label.pack(ipadx=3, ipady=3) def hidetip(self): tw = self.tooltip_window self.tooltip_window = None if tw: try: if tw.winfo_exists(): tw.destroy() except Exception: pass # --- Gitignore Editor Window Class --- class GitignoreEditorWindow(tk.Toplevel): """Toplevel window for editing the .gitignore file. Uses log_handler.""" # Rimosso logger da __init__ e fallback def __init__( self, master, gitignore_path, logger_ignored=None, on_save_success_callback=None ): """Initialize the Gitignore Editor window.""" super().__init__(master) self.gitignore_path = gitignore_path # Non c'è più self.logger self.original_content = "" self.on_save_success_callback = on_save_success_callback self.title(f"Edit {os.path.basename(gitignore_path)}") self.geometry("600x450") self.minsize(400, 300) self.grab_set() self.transient(master) self.protocol("WM_DELETE_WINDOW", self._on_close) main_frame = ttk.Frame(self, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) main_frame.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) self.text_editor = scrolledtext.ScrolledText( main_frame, wrap=tk.WORD, font=("Consolas", 10), undo=True, padx=5, pady=5, borderwidth=1, relief=tk.SUNKEN, ) self.text_editor.grid(row=0, column=0, sticky="nsew", pady=(0, 10)) button_frame = ttk.Frame(main_frame) button_frame.grid(row=1, column=0, sticky="ew") button_frame.columnconfigure(0, weight=1) # Push buttons right self.save_button = ttk.Button( button_frame, text="Save and Close", command=self._save_and_close ) self.save_button.grid(row=0, column=2, padx=5) self.cancel_button = ttk.Button( button_frame, text="Cancel", command=self._on_close ) self.cancel_button.grid(row=0, column=1, padx=5) self._load_file() self._center_window(master) self.wait_window() def _center_window(self, parent): """Centers the Toplevel window relative to its parent.""" func_name = "_center_window (Gitignore)" try: self.update_idletasks() px = parent.winfo_rootx() py = parent.winfo_rooty() pw = parent.winfo_width() ph = parent.winfo_height() ww = self.winfo_width() wh = self.winfo_height() x = px + (pw // 2) - (ww // 2) y = py + (ph // 2) - (wh // 2) sw = self.winfo_screenwidth() sh = self.winfo_screenheight() x = max(0, min(x, sw - ww)) y = max(0, min(y, sh - wh)) self.geometry(f"+{int(x)}+{int(y)}") except Exception as e: # Usa log_handler se possibile, altrimenti print try: log_handler.log_error( f"Could not center GitignoreEditor: {e}", func_name=func_name ) except NameError: print(f"ERROR: Could not center GitignoreEditor: {e}", file=sys.stderr) def _load_file(self): """Loads the content of the .gitignore file. Uses log_handler.""" func_name = "_load_file (Gitignore)" log_handler.log_info( f"Loading gitignore: {self.gitignore_path}", func_name=func_name ) content = "" try: if os.path.exists(self.gitignore_path): with open( self.gitignore_path, "r", encoding="utf-8", errors="replace" ) as f: content = f.read() else: log_handler.log_info( f".gitignore does not exist at: {self.gitignore_path}", func_name=func_name, ) self.original_content = content self.text_editor.config(state=tk.NORMAL) self.text_editor.delete("1.0", tk.END) self.text_editor.insert(tk.END, self.original_content) self.text_editor.edit_reset() self.text_editor.focus_set() except IOError as e: log_handler.log_error( f"I/O error loading .gitignore: {e}", func_name=func_name ) messagebox.showerror( "Load Error", f"Error reading .gitignore:\n{e}", parent=self ) except Exception as e: log_handler.log_exception( f"Unexpected error loading .gitignore: {e}", func_name=func_name ) messagebox.showerror( "Load Error", f"Unexpected error loading:\n{e}", parent=self ) self.text_editor.config(state=tk.DISABLED) def _has_changes(self): """Checks if the editor content differs from the original.""" try: return self.text_editor.get("1.0", "end-1c") != self.original_content except Exception: return True # Assume changes on error def _save_file(self): """Saves the current content to the .gitignore file. Uses log_handler.""" func_name = "_save_file (Gitignore)" if not self._has_changes(): log_handler.log_info( "No changes to save in .gitignore.", func_name=func_name ) return True current_content = self.text_editor.get("1.0", "end-1c") log_handler.log_info( f"Saving changes to: {self.gitignore_path}", func_name=func_name ) try: with open(self.gitignore_path, "w", encoding="utf-8", newline="\n") as f: f.write(current_content) log_handler.log_info(".gitignore saved successfully.", func_name=func_name) self.original_content = current_content self.text_editor.edit_modified(False) return True except IOError as e: log_handler.log_error( f"I/O error saving .gitignore: {e}", func_name=func_name ) messagebox.showerror( "Save Error", f"Error writing .gitignore:\n{e}", parent=self ) return False except Exception as e: log_handler.log_exception( f"Unexpected error saving .gitignore: {e}", func_name=func_name ) messagebox.showerror( "Save Error", f"Unexpected error saving:\n{e}", parent=self ) return False def _save_and_close(self): """Saves the file and closes the window, calling callback on success.""" func_name = "_save_and_close (Gitignore)" save_successful = self._save_file() if save_successful: log_handler.log_debug( "Save successful, attempting callback.", func_name=func_name ) if self.on_save_success_callback and callable( self.on_save_success_callback ): try: self.on_save_success_callback() except Exception as cb_e: log_handler.log_exception( f"Error in on_save_success_callback: {cb_e}", func_name=func_name, ) messagebox.showwarning( "Callback Error", "Saved, but post-save action failed.\nCheck logs.", parent=self, ) self.destroy() # Close window def _on_close(self): """Handles window close event (X or Cancel button).""" func_name = "_on_close (Gitignore)" if self._has_changes(): res = messagebox.askyesnocancel( "Unsaved Changes", "Save changes?", parent=self ) if res is True: self._save_and_close() # Yes - Save elif res is False: log_handler.log_warning( "Discarding .gitignore changes.", func_name=func_name ) self.destroy() # No - Discard # else: Cancel - do nothing else: self.destroy() # No changes - just close # --- Create Tag Dialog (invariata, non logga) --- class CreateTagDialog(simpledialog.Dialog): """Dialog to get tag name and message from the user.""" def __init__(self, parent, title="Create New Tag", suggested_tag_name=""): self.tag_name_var = tk.StringVar() self.tag_message_var = tk.StringVar() self.result = None self.suggested_tag_name = suggested_tag_name super().__init__(parent, title=title) def body(self, master): frame = ttk.Frame(master, padding="10") frame.pack(fill="x") frame.columnconfigure(1, weight=1) ttk.Label(frame, text="Tag Name:").grid( row=0, column=0, padx=5, pady=5, sticky="w" ) self.name_entry = ttk.Entry(frame, textvariable=self.tag_name_var, width=40) self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") if self.suggested_tag_name: self.tag_name_var.set(self.suggested_tag_name) ttk.Label(frame, text="Tag Message:").grid( row=1, column=0, padx=5, pady=5, sticky="w" ) self.message_entry = ttk.Entry( frame, textvariable=self.tag_message_var, width=40 ) self.message_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") return self.name_entry def validate(self): name = self.tag_name_var.get().strip() msg = self.tag_message_var.get().strip() if not name: messagebox.showwarning("Input Error", "Tag name empty.", parent=self) return 0 if not msg: messagebox.showwarning("Input Error", "Tag message empty.", parent=self) return 0 # GitCommands handles detailed name validation return 1 def apply(self): self.result = ( self.tag_name_var.get().strip(), self.tag_message_var.get().strip(), ) # --- Create Branch Dialog (invariata, non logga) --- class CreateBranchDialog(simpledialog.Dialog): """Dialog to get a new branch name from the user.""" def __init__(self, parent, title="Create New Branch"): self.branch_name_var = tk.StringVar() self.result = None super().__init__(parent, title=title) def body(self, master): frame = ttk.Frame(master, padding="10") frame.pack(fill="x") frame.columnconfigure(1, weight=1) ttk.Label(frame, text="New Branch Name:").grid( row=0, column=0, padx=5, pady=10, sticky="w" ) self.name_entry = ttk.Entry(frame, textvariable=self.branch_name_var, width=40) self.name_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew") return self.name_entry def validate(self): name = self.branch_name_var.get().strip() if not name: messagebox.showwarning("Input Error", "Branch name empty.", parent=self) return 0 pattern = ( r"^(?!\.| |.*[/.]\.|\.|.*\\|.*@\{|.*[/]$|.*\.\.)[^ \t\n\r\f\v~^:?*[\\]+$" ) if not re.match(pattern, name) or name.lower() == "head": messagebox.showwarning( "Input Error", "Invalid branch name format.", parent=self ) return 0 return 1 def apply(self): self.result = self.branch_name_var.get().strip() # --- Main Application Frame --- class MainFrame(ttk.Frame): """The main frame using ttk.Notebook. Does not log directly.""" GREEN = "#90EE90" RED = "#F08080" # Color constants # Aggiungi colori per status bar STATUS_YELLOW = "#FFFACD" # Lemon Chiffon (giallo chiaro) STATUS_RED = "#FFA07A" # Light Salmon (rosso/arancio chiaro) STATUS_GREEN = "#98FB98" # Pale Green (verde chiaro) STATUS_DEFAULT_BG = None # Per ripristinare il colore default del tema def __init__( self, master, # Callbacks Esistenti load_profile_settings_cb, browse_folder_cb, update_svn_status_cb, prepare_svn_for_git_cb, create_git_bundle_cb, fetch_from_git_bundle_cb, config_manager_instance, profile_sections_list, add_profile_cb, remove_profile_cb, manual_backup_cb, open_gitignore_editor_cb, save_profile_cb, commit_changes_cb, refresh_tags_cb, create_tag_cb, checkout_tag_cb, refresh_history_cb, refresh_branches_cb, # Per lista branch locali (entrambe le tab) checkout_branch_cb, create_branch_cb, refresh_changed_files_cb, open_diff_viewer_cb, add_selected_file_cb, # Callbacks Remoti apply_remote_config_cb, check_connection_auth_cb, fetch_remote_cb, pull_remote_cb, push_remote_cb, push_tags_remote_cb, refresh_remote_status_cb, clone_remote_repo_cb, refresh_remote_branches_cb, # Per lista branch remoti checkout_remote_branch_cb, # Azione da menu contestuale remoto delete_local_branch_cb: Callable[[str, bool], None], # Azione da menu contestuale locale merge_local_branch_cb: Callable[[str], None], # Azione da menu contestuale locale compare_branch_with_current_cb: Callable[[str], None], # Aggiungere qui futuri callback (es. delete remote branch, compare branches, etc.) ): """Initializes the MainFrame.""" super().__init__(master) self.master = 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 # Usato da entrambe le tab locali 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.config_manager = config_manager_instance self.initial_profile_sections = profile_sections_list # Configurazione stile self.style = ttk.Style() # (...) codice stile (...) 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") # --- Creazione Menu Contestuali --- 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) # Menu esistente # --- 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 qui può influenzare l'esistenza dei widget se riutilizzati) 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() # Crea la listbox/button originale self.remote_tab_frame = self._create_remote_tab() # Crea la nuova tab con le liste affiancate self.history_tab_frame = self._create_history_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) ") # Rinomina tab originale 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) 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(); self.STATUS_DEFAULT_BG = s.lookup('TLabel', 'background') except tk.TclError: self.STATUS_DEFAULT_BG = self.status_bar.cget('background') self.status_bar_var.set("Initializing...") self._status_reset_timer = None self.current_local_branch: str | None = None # Initial State self._initialize_profile_selection() self.toggle_backup_dir() # Lo stato iniziale dei widget verrà impostato da load_profile_settings -> update_svn_status_indicator # self.update_status_bar("Ready.") # Impostato da _perform_initial_load # --- Frame Creation Methods (_create_profile_frame, _create_repo_tab, etc.) --- # (Questi metodi rimangono invariati rispetto all'ultima versione valida, # non contengono chiamate dirette al logger. Li ometto per brevità qui.) # --- >> INCOLLA QUI I METODI _create_* DAL FILE GUI.PY PRECEDENTE << --- def _create_profile_frame(self): 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 config profile.") 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 settings to 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 new profile.") self.clone_profile_button = ttk.Button( button_subframe, text="Clone from Remote", # Testo aggiornato width=18, # Leggermente più largo command=self.clone_remote_repo_callback, # Chiama il nuovo 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 selected profile.") def _create_remote_tab(self): """Creates the widgets for the Remote Repository tab with new layout.""" # Frame principale per questa tab, usa padding standard frame = ttk.Frame(self.notebook, padding=(10, 10)) # Configura righe/colonne del frame principale per l'espansione frame.columnconfigure(0, weight=1) # Colonna principale (sinistra) si espande frame.columnconfigure(1, weight=1) # Colonna destra (per lista locali) si espande # La riga 3 (contenente le liste di branch) si espanderà verticalmente frame.rowconfigure(3, weight=1) # --- Frame Superiore: Configurazione e Stato Sync --- top_frame = ttk.LabelFrame(frame, text="Remote Configuration & Sync Status", padding=(10, 5)) # Posiziona usando grid sulla riga 0, occupando entrambe le colonne top_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5,0)) top_frame.columnconfigure(1, weight=1) # Colonna delle Entry si espande # Riga 0: URL 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://...).") # Riga 1: Nome Locale 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}').") # Riga 2: Label Sync Status (Ahead/Behind) 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.") # Colonna 2 (del top_frame): Pulsanti Azione Verticali e Indicatore Auth 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') # Imposta stato iniziale self.create_tooltip(self.auth_status_indicator_label, "Connection and authentication status.") 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 sync status (ahead/behind) for the current branch.") # --- Frame Azioni Standard --- 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() # Centra 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.") 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 and integrate changes into the current 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 remote.") 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.") # --- Frame Inferiore: Liste Branch Affiancate --- 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) # Le righe delle liste si espandono branch_view_frame.columnconfigure(0, weight=1) # Colonna sinistra si espande branch_view_frame.columnconfigure(1, weight=1) # Colonna destra si espande # Colonna Sinistra: Branch Remoti 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) # Riga della listbox si espande remote_list_frame.columnconfigure(0, weight=1) # Listbox si espande 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 on the remote. Right-click for actions.") 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 this list (requires fetch).") # Colonna Destra: Branch Locali (Nuovi Widget) 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) # Usa stesso menu dei locali 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, "Local branches. Right-click for actions.") self.refresh_local_branches_button_remote_tab = ttk.Button(local_list_frame, text="Refresh Local List", command=self.refresh_branches_callback, state=tk.DISABLED) # Chiama refresh locale 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 _show_remote_branches_context_menu(self, event): """Displays the context menu for the remote branches listbox.""" 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 # (...) Logica derivazione local_branch_suggestion (...) slash_index = remote_branch_full_name.find('/') local_branch_suggestion = remote_branch_full_name[slash_index + 1:] if slash_index != -1 else "" # --- Opzione Compare --- # ---<<< MODIFICA: Usa self.current_local_branch >>>--- current_branch_name_local = self.current_local_branch # Leggi dalla variabile membro compare_state = tk.NORMAL # Sempre abilitato per remoti (a meno che non sia l'upstream?) 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}'" # ---<<< FINE MODIFICA >>>--- 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 ) # (...) Altre opzioni (Checkout as local...), Separatore, Cancel (...) # Assicurati che anche checkout usi local_branch_suggestion 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) # Mostra il menu 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): """Displays the context menu for the local branches listbox.""" func_name = "_show_local_branches_context_menu" listbox = event.widget selected_index = None current_branch_name_local = None # Per sapere il nome del branch attivo # Ottieni il branch CORRENTE (per abilitare/disabilitare opzioni) # Potremmo prenderlo dalla listbox stessa cercando '*' o averlo da un refresh precedente # Per sicurezza, proviamo a leggerlo dalla lista attuale try: items = listbox.get(0, tk.END) for item in items: if item.strip().startswith("*"): current_branch_name_local = item.lstrip("* ").strip() break except Exception: pass # Ignora se non riusciamo a trovarlo qui 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() # Pulisci menu precedente self.local_branch_context_menu.delete(0, tk.END) # Estrai nome branch selezionato e verifica se è quello corrente 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: # --- Opzione Checkout (Invariata) --- 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 ) # ---<<< NUOVA OPZIONE: Merge >>>--- # Abilitata solo se NON è il branch corrente E conosciamo il branch corrente 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) # Chiama nuovo callback if callable(self.merge_local_branch_callback) else None ) # ---<<< FINE NUOVA OPZIONE >>>--- # --- Opzione Delete (Invariata) --- 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 ) # --- Opzione Force Delete (Invariata) --- 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 ) # --- Opzione Compare --- 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}'" # Assicurati che il command chiami il callback giusto self.local_branch_context_menu.add_command( label=compare_label, state=compare_state, command=lambda b_other=local_branch_name_selected: # Passa il branch selezionato self.compare_branch_with_current_callback(b_other) # <-- VERIFICA QUESTA CHIAMATA if callable(self.compare_branch_with_current_callback) else None ) # --- Separatore e Cancel --- self.local_branch_context_menu.add_separator() self.local_branch_context_menu.add_command(label="Cancel") else: # Elemento non valido selezionato self.local_branch_context_menu.add_command(label="(No actions available)", state=tk.DISABLED) # Mostra il menu 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() def update_remote_branches_list(self, remote_branch_list: List[str]): """Clears and populates the remote branches listbox.""" 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)"]: # Resetta colore se necessario try: default_fg = self.style.lookup("TListbox", "foreground") if listbox.cget("fg") != default_fg: listbox.config(fg=default_fg) except tk.TclError: pass # Popola la lista for branch_name in remote_branch_list: listbox.insert( tk.END, f" {branch_name}" ) # Aggiunge indentazione per leggibilità elif remote_branch_list == ["(Error)"]: listbox.insert(tk.END, "(Error retrieving list)") listbox.config(fg="red") else: # Lista vuota (o None) listbox.insert(tk.END, "(No remote branches found)") listbox.config(fg="grey") listbox.config(state=tk.NORMAL) # Lascia selezionabile listbox.yview_moveto(0.0) # Scrolla all'inizio except Exception as e: log_handler.log_exception( f"Error updating remote branches list GUI: {e}", func_name="update_remote_branches_list", ) try: # Fallback visualizzazione errore 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 # Ignora errori nel fallback def _update_auth_status_indicator(self, status: str): """Updates the text and background color of the auth status label.""" label = getattr(self, "auth_status_indicator_label", None) if not label or not label.winfo_exists(): return text = "Status: Unknown" color = self.STATUS_DEFAULT_BG # Grigio/Default 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." # else: status == 'unknown' -> usa i valori di default try: self.remote_auth_status_var.set(text) label.config(background=color) self.update_tooltip(label, tooltip) # Aggiorna anche il 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 _create_repo_tab(self): 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 # Column indices 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 Git repo.") 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, "Git repo status (Red=No, Green=Yes)" ) 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, "Dir for bundle files.") 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 for bundle creation.") 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 for bundle fetch." ) 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 & .gitignore.") self.create_bundle_button = ttk.Button( actions_frame, text="Create Bundle", command=self.create_git_bundle_callback, state=tk.DISABLED, ) self.create_bundle_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip(self.create_bundle_button, "Create Git bundle file.") self.fetch_bundle_button = ttk.Button( actions_frame, text="Fetch from Bundle", command=self.fetch_from_git_bundle_callback, state=tk.DISABLED, ) self.fetch_bundle_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip(self.fetch_bundle_button, "Fetch & merge from bundle.") self.edit_gitignore_button = ttk.Button( actions_frame, text="Edit .gitignore", width=12, command=self.open_gitignore_editor_callback, state=tk.DISABLED, ) self.edit_gitignore_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip(self.edit_gitignore_button, "Edit .gitignore file.") return frame def _create_backup_tab(self): 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 # Column indices self.autobackup_checkbox = ttk.Checkbutton( config_frame, text="Enable Auto Backup before Actions", 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, "Auto ZIP backup before bundle ops." ) 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, "Where backups are 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-sep extensions (.log,.tmp)" ) 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-sep dir names (__pycache__,build)" ) 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 ZIP backup now.") return frame def _create_commit_tab(self): frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.rowconfigure(3, weight=1) frame.columnconfigure(0, weight=1) # Changes frame (row 3) expands 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, "Auto commit before create 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, "Commit message.") 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, "Changed files list (Double-click to diff)." ) self.changed_files_context_menu = tk.Menu( self.changed_files_listbox, tearoff=0 ) # Context menu 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 file list.") 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) self.create_tooltip(self.commit_button, "Commit staged changes manually.") return frame def _create_tags_tab(self): 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, "Select tag to checkout.") button_frame = ttk.Frame(frame) button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5)) bw = 18 # Button width 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 tag list.") 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 annotated tag.") 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 to tag (Detached HEAD).") return frame def _create_branch_tab(self): 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, "Select branch to checkout.") button_frame = ttk.Frame(frame) button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5)) bw = 18 # Button width 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 branch list.") 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 new local branch.") 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 to selected branch.") return frame def _create_history_tab(self): frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.rowconfigure(2, weight=1) frame.columnconfigure(0, weight=1) filter_frame = ttk.Frame(frame) filter_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) filter_frame.columnconfigure(1, weight=1) ttk.Label(filter_frame, text="Filter History:").pack(side=tk.LEFT, padx=(0, 5)) self.history_branch_filter_var = tk.StringVar() self.history_branch_filter_combo = ttk.Combobox( filter_frame, textvariable=self.history_branch_filter_var, state="readonly", width=30, ) self.history_branch_filter_combo.pack( side=tk.LEFT, expand=True, fill=tk.X, padx=5 ) self.history_branch_filter_combo.bind( "<>", lambda e: self.refresh_history_callback() ) self.create_tooltip( self.history_branch_filter_combo, "Filter history by branch/tag." ) self.refresh_history_button = ttk.Button( filter_frame, text="Refresh History", command=self.refresh_history_callback, state=tk.DISABLED, ) self.refresh_history_button.pack(side=tk.LEFT, padx=5) self.create_tooltip(self.refresh_history_button, "Load commit history.") ttk.Label(frame, text="Commit History (Recent First):").grid( row=1, column=0, sticky="w", padx=5, pady=(5, 0) ) self.history_text = scrolledtext.ScrolledText( frame, height=15, width=100, font=("Consolas", 9), wrap=tk.NONE, state=tk.DISABLED, padx=5, pady=5, undo=False, borderwidth=1, relief=tk.SUNKEN, ) self.history_text.grid(row=2, column=0, sticky="nsew", padx=5, pady=(0, 5)) history_xscroll = ttk.Scrollbar( frame, orient=tk.HORIZONTAL, command=self.history_text.xview ) history_xscroll.grid(row=3, column=0, sticky="ew", padx=5) self.history_text.config(xscrollcommand=history_xscroll.set) return frame def _create_log_area(self, parent_frame): 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") 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): if not hasattr(self, "config_manager"): return try: from config_manager import DEFAULT_PROFILE except ImportError: DEFAULT_PROFILE = "default" current_profiles = self.profile_dropdown.cget("values") if not isinstance(current_profiles, (list, tuple)): current_profiles = [] if DEFAULT_PROFILE in current_profiles: self.profile_var.set(DEFAULT_PROFILE) elif current_profiles: self.profile_var.set(current_profiles[0]) else: self.profile_var.set("") self.update_status_bar("No profiles found.") # --- GUI Update Methods (non logganti) --- def toggle_backup_dir(self): 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): 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 Dir", parent=self.master ) if sel: self.backup_dir_var.set(sel) def update_svn_indicator(self, is_prepared): color = self.GREEN if is_prepared else self.RED tip = "Prepared" if is_prepared else "Not prepared" 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) def update_profile_dropdown(self, sections): 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: try: from config_manager import DEFAULT_PROFILE except ImportError: DEFAULT_PROFILE = "default" 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): if not hasattr(self, "tag_listbox") or not self.tag_listbox.winfo_exists(): return try: self.tag_listbox.config(state=tk.NORMAL) self.tag_listbox.delete(0, tk.END) if tags_data: try: if self.tag_listbox.cget("fg") == "grey": self.tag_listbox.config( fg=self.style.lookup("TListbox", "foreground") ) except tk.TclError: pass for name, subj in tags_data: self.tag_listbox.insert(tk.END, f"{name}\t({subj})") else: self.tag_listbox.insert(tk.END, "(No tags found)") self.tag_listbox.config(fg="grey") self.tag_listbox.config(state=tk.NORMAL) self.tag_listbox.yview_moveto(0.0) except Exception as e: # Log to console as log_handler might not be available print(f"ERROR updating tag list GUI: {e}", file=sys.stderr) try: self.tag_listbox.delete(0, tk.END) self.tag_listbox.insert(tk.END, "(Error)") self.tag_listbox.config(fg="red") except: pass def update_branch_list(self, branches: List[str], current_branch: str | None): """Clears and populates the local branch listbox(es).""" func_name = "update_branch_list (GUI)" log_handler.log_debug(f"Received branches type={type(branches)}, count={len(branches) if isinstance(branches, list) else 'N/A'}, current={repr(current_branch)}", func_name=func_name) self.current_local_branch = current_branch # Memorizza il branch corrente ricevuto log_handler.log_debug(f"Stored current_local_branch: {self.current_local_branch}", func_name=func_name) # Lista delle listbox dei branch LOCALI da aggiornare listboxes_to_update = [] # Listbox nella tab "Branches" originale if hasattr(self, "branch_listbox") and getattr(self, "branch_listbox") and self.branch_listbox.winfo_exists(): listboxes_to_update.append(self.branch_listbox) # Listbox nella tab "Remote Repository" if hasattr(self, "local_branches_listbox_remote_tab") and getattr(self, "local_branches_listbox_remote_tab") and self.local_branches_listbox_remote_tab.winfo_exists(): listboxes_to_update.append(self.local_branches_listbox_remote_tab) 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()) # Ottieni nome widget per log except Exception: pass try: listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) sel_idx = -1 if isinstance(branches, list) and branches: try: # Resetta colore default_fg = self.style.lookup("TListbox", "foreground") if listbox.cget("fg") != default_fg: listbox.config(fg=default_fg) except tk.TclError: pass # Popola lista 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) # Lascia selezionabile 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: # Fallback 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 # ---<<< FINE MODIFICA DEBUG & ERRORE >>>--- def get_selected_tag(self): if hasattr(self, "tag_listbox") and self.tag_listbox.winfo_exists(): idx = self.tag_listbox.curselection() if idx: item = self.tag_listbox.get(idx[0]) 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): if hasattr(self, "branch_listbox") and self.branch_listbox.winfo_exists(): idx = self.branch_listbox.curselection() if idx: item = self.branch_listbox.get(idx[0]) name = item.lstrip("* ").strip() if not name.startswith("("): return name return None def get_commit_message(self): 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): 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 update_history_display(self, log_lines): """Clears and populates the history ScrolledText widget.""" func_name = "update_history_display (GUI)" # Nome specifico per i log # ---<<< INIZIO MODIFICA DEBUG & ERRORE >>>--- log_handler.log_debug( f"Received log_lines type={type(log_lines)}, count={len(log_lines) if isinstance(log_lines, list) else 'N/A'}. " f"Sample: {repr(log_lines[:5]) if isinstance(log_lines, list) else repr(log_lines)}", func_name=func_name, ) history_widget = getattr(self, "history_text", None) if not history_widget or not history_widget.winfo_exists(): log_handler.log_error( "history_text widget not available for update.", func_name=func_name ) return try: history_widget.config(state=tk.NORMAL) history_widget.delete("1.0", tk.END) # Assicurati che log_lines sia una lista prima di fare join if isinstance(log_lines, list): if log_lines: # Resetta colore (se era rosso o altro) try: # Potrebbe non esserci un colore foreground specifico per ScrolledText nello stile # Potremmo impostare a nero o lasciare il default del widget default_fg = "black" # Assumiamo nero come default sicuro if history_widget.cget("fg") != default_fg: history_widget.config(fg=default_fg) except tk.TclError: pass # Unisci le linee (assicurati siano stringhe) text_to_insert = "\n".join(map(str, log_lines)) history_widget.insert(tk.END, text_to_insert) else: # Lista vuota valida history_widget.insert(tk.END, "(No history found)") history_widget.config(fg="grey") # Colore grigio per indicare vuoto else: # log_lines non è una lista (errore?) log_handler.log_warning( f"Invalid data received for history: {repr(log_lines)}", func_name=func_name, ) history_widget.insert( tk.END, f"(Invalid data received: {repr(log_lines)})" ) history_widget.config(fg="orange") # Arancione per dato inatteso history_widget.config(state=tk.DISABLED) # Rendi read-only history_widget.yview_moveto(0.0) history_widget.xview_moveto(0.0) except Exception as e: log_handler.log_exception( f"Error updating history GUI: {e}", func_name=func_name ) # Fallback: Mostra errore nel widget di testo try: if history_widget.winfo_exists(): history_widget.config(state=tk.NORMAL) history_widget.delete("1.0", tk.END) history_widget.insert(tk.END, "(Error displaying history)") history_widget.config( state=tk.DISABLED, fg="red" ) # Rosso e disabilitato except Exception as fallback_e: log_handler.log_error( f"Error displaying fallback error in history widget: {fallback_e}", func_name=func_name, ) # ---<<< FINE MODIFICA DEBUG & ERRORE >>>--- def update_history_branch_filter(self, branches_tags, current_ref=None): if ( not hasattr(self, "history_branch_filter_combo") or not self.history_branch_filter_combo.winfo_exists() ): return opts = ["-- All History --"] + sorted(branches_tags) self.history_branch_filter_combo["values"] = opts self.history_branch_filter_var.set( current_ref if current_ref and current_ref in opts else opts[0] ) def update_changed_files_list(self, files_status_list): """Clears and populates the changed files listbox, sanitizing input.""" # Usa la costante Listbox invece di hasattr ogni volta listbox = getattr(self, "changed_files_listbox", None) if not listbox or not listbox.winfo_exists(): # Logga se il widget non è disponibile (usa print come fallback qui) 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: # Reset color try: if listbox.cget("fg") == "grey": listbox.config(fg=self.style.lookup("TListbox", "foreground")) except tk.TclError: pass # <<< MODIFICA: Sanifica ogni riga prima di inserirla >>> processed_lines = 0 for status_line in files_status_list: try: # Assicura sia una stringa e rimuovi caratteri potenzialmente problematici # (es. NUL residuo, anche se split dovrebbe averlo rimosso) # Potremmo usare una regex più complessa per rimuovere tutti i controlli C0/C1 # ma iniziamo con una pulizia base. sanitized_line = str(status_line).replace("\x00", "").strip() if sanitized_line: # Inserisci solo se non vuota dopo pulizia listbox.insert(tk.END, sanitized_line) processed_lines += 1 else: # Logga se una riga viene scartata print( f"Warning: Sanitized status line resulted in empty string: {repr(status_line)}", file=sys.stderr, ) except Exception as insert_err: # Logga errore specifico per riga print( f"ERROR inserting line into listbox: {insert_err} - Line: {repr(status_line)}", file=sys.stderr, ) # Inserisci un placeholder di errore per quella riga listbox.insert( tk.END, f"(Error processing line: {repr(status_line)})" ) listbox.itemconfig(tk.END, {"fg": "red"}) # <<< FINE MODIFICA >>> # Se nessuna linea valida è stata processata, mostra placeholder 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) # Mantieni selezionabile 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 # Ignora errori durante la visualizzazione dell'errore def _on_changed_file_double_click(self, event): widget = event.widget sel = widget.curselection() if sel: line = widget.get(sel[0]) if 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): """Displays the context menu for the changed files listbox.""" func_name = "_show_changed_files_context_menu" line = None # Inizializza line a None try: # Trova l'indice dell'elemento più vicino al click idx = self.changed_files_listbox.nearest(event.y) # Seleziona programmaticamente quell'elemento self.changed_files_listbox.selection_clear(0, tk.END) self.changed_files_listbox.selection_set(idx) self.changed_files_listbox.activate(idx) # ---<<< MODIFICA: Ottieni 'line' QUI >>>--- # Ottieni il testo della riga selezionata ORA, dentro il try line = self.changed_files_listbox.get(idx) # ---<<< FINE MODIFICA >>>--- except tk.TclError: # Errore nel trovare/selezionare l'elemento (es. listbox vuota o click strano) log_handler.log_debug( f"TclError getting selected line for context menu.", func_name=func_name ) return # Esce se non si può selezionare nulla except Exception as e: # Altri errori imprevisti log_handler.log_error( f"Error getting selected line for context menu: {e}", func_name=func_name, ) return # Ora controlla se 'line' è stata ottenuta con successo if line is None: log_handler.log_debug( f"Could not retrieve line content at index {idx}.", func_name=func_name ) return # Esce se non siamo riusciti a ottenere il testo # Ora 'line' è sicuramente definita se siamo arrivati qui log_handler.log_debug(f"Context menu for line: '{line}'", func_name=func_name) # Pulisci e costruisci il menu contestuale self.changed_files_context_menu.delete(0, tk.END) cleaned = line.strip() is_untracked = cleaned.startswith("??") # Opzione "Add" 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, # Usa una lambda per passare la variabile 'line' correttamente definita command=lambda current_line=line: ( self.add_selected_file_callback(current_line) if can_add else None ), ) # Opzione "Diff" can_diff = hasattr(self, "open_diff_viewer_callback") and callable( self.open_diff_viewer_callback ) # Disabilita Diff per Untracked (??), Ignored (!!), Deleted ( D) 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 ), ) # Mostra il menu alla posizione del cursore try: self.changed_files_context_menu.tk_popup(event.x_root, event.y_root) finally: # Rilascia il grab del menu (importante!) self.changed_files_context_menu.grab_release() def update_status_bar(self, message, bg_color=None, duration_ms=None): """ Safely updates the status bar text and optionally its background color. Optionally resets the color after a duration. Args: message (str): The text to display. bg_color (str | None): Background color name (e.g., "#FFFACD") or None to use default. duration_ms (int | None): If set, reset color to default after this many ms. """ if hasattr(self, "status_bar_var") and hasattr(self, "status_bar"): try: # Cancella eventuale timer di reset precedente if self._status_reset_timer: self.master.after_cancel(self._status_reset_timer) self._status_reset_timer = None # Funzione interna per applicare aggiornamenti (via root.after) def _update(): if self.status_bar.winfo_exists(): # Controlla esistenza widget self.status_bar_var.set(message) actual_bg = bg_color if bg_color else self.STATUS_DEFAULT_BG try: self.status_bar.config(background=actual_bg) except tk.TclError: # Gestisci errore se colore non valido self.status_bar.config(background=self.STATUS_DEFAULT_BG) print( f"Warning: Invalid status bar color '{bg_color}', using default.", file=sys.stderr, ) # Pianifica reset colore se richiesto if bg_color and duration_ms and duration_ms > 0: self._status_reset_timer = self.master.after( duration_ms, self.reset_status_bar_color ) # Pianifica l'esecuzione nel main loop 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): """Resets the status bar background color to the default.""" self._status_reset_timer = None # Resetta timer ID if hasattr(self, "status_bar") and self.status_bar.winfo_exists(): try: self.status_bar.config(background=self.STATUS_DEFAULT_BG) except Exception as e: print(f"ERROR resetting status bar color: {e}", file=sys.stderr) def ask_new_profile_name(self): return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master) def show_error(self, title, message): messagebox.showerror(title, message, parent=self.master) def show_info(self, title, message): messagebox.showinfo(title, message, parent=self.master) def show_warning(self, title, message): messagebox.showwarning(title, message, parent=self.master) def ask_yes_no(self, title, message): return messagebox.askyesno(title, message, parent=self.master) def create_tooltip(self, widget, text): if widget and isinstance(widget, tk.Widget) and widget.winfo_exists(): Tooltip(widget, text) def update_tooltip(self, widget, text): self.create_tooltip(widget, text) def set_action_widgets_state(self, state): """Safely sets the state (NORMAL/DISABLED) for main action widgets.""" 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 # Lista aggiornata dei widget controllati dallo stato generale repo/app widgets = [ # Profile/Save self.save_settings_button, self.remove_profile_button, # Repo/Bundle self.prepare_svn_button, self.create_bundle_button, self.fetch_bundle_button, self.edit_gitignore_button, # Backup self.manual_backup_button, # Commit/Changes self.commit_button, self.refresh_changes_button, self.commit_message_text, self.autocommit_checkbox, # Tags self.refresh_tags_button, self.create_tag_button, self.checkout_tag_button, # Branches (Local Ops Tab) self.refresh_branches_button, self.create_branch_button, self.checkout_branch_button, # History self.refresh_history_button, self.history_branch_filter_combo, # Remote Tab self.apply_remote_config_button, self.check_auth_button, self.fetch_button, self.pull_button, self.push_button, self.push_tags_button, self.refresh_sync_status_button, self.refresh_remote_branches_button, # Refresh lista remota self.refresh_local_branches_button_remote_tab, # Refresh lista locale (in tab remota) ] log_handler.log_debug(f"Setting {len(widgets)} action widgets state to: {state}", func_name="set_action_widgets_state") failed_widgets = [] for widget in widgets: widget_attr_name = None; # Trova nome attributo per log errore for attr, value in self.__dict__.items(): if value is widget: widget_attr_name = attr; break if widget_attr_name and hasattr(self, widget_attr_name) and widget and widget.winfo_exists(): try: w_state = state if isinstance(widget, ttk.Combobox): w_state = "readonly" if state == tk.NORMAL else tk.DISABLED elif isinstance(widget, (tk.Text, scrolledtext.ScrolledText)): w_state = state # Già gestisce NORMAL/DISABLED widget.config(state=w_state) except Exception as e: failed_widgets.append(f"{widget_attr_name}: {e}") if failed_widgets: log_handler.log_error(f"Error setting state for some widgets: {'; '.join(failed_widgets)}", func_name="set_action_widgets_state") # Gestione profile dropdown (separata) if hasattr(self, "profile_dropdown") and self.profile_dropdown.winfo_exists(): try: self.profile_dropdown.config(state="readonly" if state == tk.NORMAL else tk.DISABLED) except Exception as e: log_handler.log_error(f"Error setting state for profile dropdown: {e}", func_name="set_action_widgets_state") # Gestione stato liste (devono essere selezionabili se abilitate) list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED listboxes = [ getattr(self, name, None) for name in ["tag_listbox", "branch_listbox", "history_text", "changed_files_listbox", "remote_branches_listbox", "local_branches_listbox_remote_tab"] ] for lb in listboxes: if lb and lb.winfo_exists(): try: lb.config(state=list_state) except Exception: pass # Ignora errori specifici widget (es. Text non ha 'readonly') def update_ahead_behind_status( self, current_branch: str | None = None, # Aggiunto parametro branch status_text: str | None = None, ahead: int | None = None, behind: int | None = None, ): """Updates the synchronization status label, including the current branch.""" label = getattr(self, "sync_status_label", None) # Usa nuovo nome label var = getattr(self, "remote_ahead_behind_var", None) if not label or not var or not label.winfo_exists(): return # Determina il testo da visualizzare if current_branch: branch_part = f"Branch '{current_branch}': " else: # Se non conosciamo il branch (es. detached head o errore iniziale) branch_part = "Current Branch: " status_part = "Unknown" # Default if status_text is not None: # Testo esplicito fornito (es. errore, no upstream, detached) status_part = ( status_text # Il testo dovrebbe già includere "Sync Status:" o simile ) # Sovrascrivi branch_part se il testo contiene già info sul branch o stato if ( "Branch" in status_part or "Detached" in status_part or "Upstream" in status_part ): text_to_display = status_part # Usa direttamente il testo fornito else: text_to_display = branch_part + status_part elif ahead is not None and behind is not None: # Costruisci messaggio da conteggi 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: # Caso di default o conteggi None senza testo esplicito 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", ) class CloneFromRemoteDialog(simpledialog.Dialog): """Dialog to get Remote URL and Local Parent Directory for cloning.""" def __init__(self, parent, title="Clone Remote Repository"): self.remote_url_var = tk.StringVar() self.local_parent_dir_var = tk.StringVar() self.profile_name_var = tk.StringVar() # Opzionale self.result = None # Conterrà la tupla (url, parent_dir, profile_name) # Imposta directory iniziale suggerita per la cartella locale self.local_parent_dir_var.set(os.path.expanduser("~")) # Default alla home super().__init__(parent, title=title) def body(self, master): """Creates the dialog body.""" main_frame = ttk.Frame(master, padding="10") main_frame.pack(fill="both", expand=True) main_frame.columnconfigure(1, weight=1) # Colonna delle entry si espande row_idx = 0 # Remote URL ttk.Label(main_frame, text="Remote Repository URL:").grid( row=row_idx, column=0, padx=5, pady=5, sticky="w" ) self.url_entry = ttk.Entry( main_frame, textvariable=self.remote_url_var, width=60 ) self.url_entry.grid( row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew" ) Tooltip( self.url_entry, "Enter the full URL (HTTPS or SSH) of the repository to clone.", ) row_idx += 1 # Local Parent Directory ttk.Label(main_frame, text="Clone into Directory:").grid( row=row_idx, column=0, padx=5, pady=5, sticky="w" ) self.dir_entry = ttk.Entry( main_frame, textvariable=self.local_parent_dir_var, width=60 ) self.dir_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew") Tooltip( self.dir_entry, "Select the PARENT directory where the new repository folder will be created.", ) self.browse_button = ttk.Button( main_frame, text="Browse...", width=9, command=self._browse_local_dir ) self.browse_button.grid(row=row_idx, column=2, padx=(0, 5), pady=5, sticky="w") row_idx += 1 # Info sulla cartella creata (Label esplicativo) ttk.Label( main_frame, text="(A new sub-folder named after the repository will be created inside this directory)", font=("Segoe UI", 8), foreground="grey", ).grid(row=row_idx, column=1, columnspan=2, padx=5, pady=(0, 5), sticky="w") row_idx += 1 # New Profile Name (Opzionale) ttk.Label(main_frame, text="New Profile Name (Optional):").grid( row=row_idx, column=0, padx=5, pady=5, sticky="w" ) self.profile_entry = ttk.Entry( main_frame, textvariable=self.profile_name_var, width=60 ) self.profile_entry.grid( row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew" ) Tooltip( self.profile_entry, "Enter a name for the new profile. If left empty, the repository name will be used.", ) row_idx += 1 return self.url_entry # initial focus def _browse_local_dir(self): """Callback for the local directory browse button.""" current_path = self.local_parent_dir_var.get() initial_dir = ( current_path if os.path.isdir(current_path) else os.path.expanduser("~") ) directory = filedialog.askdirectory( initialdir=initial_dir, title="Select Parent Directory for Clone", parent=self, # Rendi modale rispetto a questo dialogo ) if directory: self.local_parent_dir_var.set(directory) def validate(self): """Validates the input fields before closing.""" url = self.remote_url_var.get().strip() parent_dir = self.local_parent_dir_var.get().strip() profile_name = self.profile_name_var.get().strip() # Pulisce anche nome profilo if not url: messagebox.showwarning( "Input Error", "Remote Repository URL cannot be empty.", parent=self ) return 0 # Fallisce validazione # Verifica base URL (non una validazione completa, ma meglio di niente) if not ( url.startswith("http://") or url.startswith("https://") or url.startswith("ssh://") or "@" in url ): if not messagebox.askokcancel( "URL Format Warning", f"The URL '{url}' does not look like a standard HTTPS, HTTP, or SSH URL.\n\nProceed anyway?", icon="warning", parent=self, ): return 0 if not parent_dir: messagebox.showwarning( "Input Error", "Parent Local Directory cannot be empty.", parent=self ) return 0 if not os.path.isdir(parent_dir): messagebox.showwarning( "Input Error", f"The selected parent directory does not exist:\n{parent_dir}", parent=self, ) return 0 # Non validiamo qui se la sotto-cartella esiste, lo farà il controller principale # Validazione opzionale nome profilo (se fornito) if profile_name: # Applica regole base per nomi di sezione configparser (evita spazi/caratteri speciali?) # Per semplicità, controlliamo solo che non sia vuoto dopo strip (già fatto) # Potremmo aggiungere un check regex se necessario. pass return 1 # Validazione OK def apply(self): """Stores the validated result.""" # Restituisce una tupla con i valori puliti self.result = ( self.remote_url_var.get().strip(), self.local_parent_dir_var.get().strip(), self.profile_name_var.get().strip(), # Restituisce vuoto se non specificato ) # --- END OF FILE gui.py ---