# --- 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 # 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, 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, checkout_branch_cb, create_branch_cb, refresh_changed_files_cb, open_diff_viewer_cb, add_selected_file_cb, apply_remote_config_cb, check_connection_auth_cb, fetch_remote_cb, pull_remote_cb, push_remote_cb, push_tags_remote_cb, ): """Initializes the MainFrame.""" super().__init__(master) self.master = master # Store callbacks provided by the controller 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.config_manager = config_manager_instance self.initial_profile_sections = profile_sections_list 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 # Configure style (invariato) self.style = ttk.Style() available = self.style.theme_names() preferred = ["vista", "xpnative", "clam"] theme = "clam" for t in preferred: if t in available: theme = t break try: self.style.theme_use(theme) except tk.TclError: print(f"Warning: Theme '{theme}' not found.", file=sys.stderr) theme = self.style.theme_use() # print(f"Using ttk theme: {theme}") # Rimosso log diretto 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") # --- 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) self.repo_tab_frame = self._create_repo_tab() self.remote_tab_frame = self._create_remote_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() self.history_tab_frame = self._create_history_tab() 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 ") self.notebook.add(self.history_tab_frame, text=" History ") log_frame_container = ttk.Frame(self) log_frame_container.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=(5, 0) ) # Pack sopra status bar 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) try: # Usa lookup per ottenere il colore di sfondo standard di un TTK Label s = ttk.Style() self.STATUS_DEFAULT_BG = s.lookup("TLabel", "background") except tk.TclError: # Fallback se il tema non è pronto o lookup fallisce self.STATUS_DEFAULT_BG = self.status_bar.cget( "background" ) # Usa colore attuale widget self.status_bar_var.set("Initializing...") self._status_reset_timer = None # --- Initial State --- self._initialize_profile_selection() self.toggle_backup_dir() self.update_status_bar("Ready.") # --- 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.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.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(1, weight=1) # --- Sezione Configurazione --- config_frame = ttk.LabelFrame( frame, text="Remote Configuration (Saved in Profile)", padding=(10, 5) ) config_frame.pack(pady=5, fill="x", expand=False) config_frame.columnconfigure(1, weight=1) # Remote URL ttk.Label(config_frame, text="Remote URL:").grid( row=0, column=0, sticky=tk.W, padx=5, pady=3 ) self.remote_url_entry = ttk.Entry( config_frame, textvariable=self.remote_url_var, width=70 ) 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://...).", ) # Remote Name ttk.Label(config_frame, text="Local Name:").grid( row=1, column=0, sticky=tk.W, padx=5, pady=3 ) self.remote_name_entry = ttk.Entry( config_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}').", ) # Mettiamo i bottoni di azione sulla configurazione in un frame separato a destra config_action_frame = ttk.Frame(config_frame) config_action_frame.grid( row=0, column=2, rowspan=2, sticky="ne", padx=(10, 5) ) # Allineato Nord-Est # Pulsante Applica Configurazione self.apply_remote_config_button = ttk.Button( config_action_frame, text="Apply Config to Local Repo", command=self.apply_remote_config_callback, state=tk.DISABLED, ) self.apply_remote_config_button.pack(side=tk.TOP, pady=2, fill=tk.X) # Sopra 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 / Auth", command=self.check_connection_auth_callback, # Nuovo callback state=tk.DISABLED, # Abilitato quando repo pronto ) self.check_auth_button.pack(side=tk.TOP, pady=(5, 2), fill=tk.X) # Sotto Apply self.create_tooltip( self.check_auth_button, "Verify connection and authentication status for the configured remote.", ) # Usiamo un Label che cambierà colore e testo self.auth_status_indicator_label = ttk.Label( config_action_frame, textvariable=self.remote_auth_status_var, # Collegato alla variabile anchor=tk.CENTER, relief=tk.SUNKEN, padding=(5, 2), width=25, # Larghezza fissa per consistenza ) self.auth_status_indicator_label.pack(side=tk.TOP, pady=(2, 2), fill=tk.X) # Tooltip iniziale (verrà aggiornato) self.create_tooltip( self.auth_status_indicator_label, "Connection and authentication status." ) # Imposta colore iniziale (es. grigio) self._update_auth_status_indicator("unknown") # --- Sezione Azioni Remote --- actions_frame = ttk.LabelFrame(frame, text="Remote Actions", padding=(10, 5)) actions_frame.pack(pady=10, fill="x", expand=False) self.fetch_button = ttk.Button( actions_frame, text="Fetch", command=self.fetch_remote_callback, # Usa il nuovo callback state=tk.DISABLED, # Abilitato quando repo pronto e remote configurato? (O solo repo pronto?) ) self.fetch_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.fetch_button, "Download objects and references from the configured remote repository.", ) self.pull_button = ttk.Button( actions_frame, text="Pull (Current Branch)", command=self.pull_remote_callback, # Usa il nuovo callback state=tk.DISABLED, # Abilitato quando repo pronto e connesso? ) self.pull_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.pull_button, "Fetch from and integrate with the remote branch corresponding to the current local branch (merge or rebase).", ) self.push_button = ttk.Button( actions_frame, text="Push (Current Branch)", command=self.push_remote_callback, # Nuovo callback state=tk.DISABLED, # Abilitato quando repo pronto e connesso? ) self.push_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.push_button, "Upload local branch commits to the corresponding remote branch. Will set upstream on first push.", ) # ---<<< FINE NUOVO PULSANTE PUSH BRANCH >>>--- # ---<<< NUOVO PULSANTE PUSH TAGS >>>--- self.push_tags_button = ttk.Button( actions_frame, text="Push Tags", command=self.push_tags_remote_callback, # Nuovo callback state=tk.DISABLED, # Abilitato quando repo pronto e connesso? ) self.push_tags_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.push_tags_button, "Upload all local tags (created via the Tags tab) to the remote repository.", ) return frame 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") 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, current_branch): """Clears and populates the branch listbox.""" func_name = "update_branch_list (GUI)" # Nome specifico per i log # ---<<< INIZIO MODIFICA DEBUG & ERRORE >>>--- log_handler.log_debug( f"Received branches type={type(branches)}, count={len(branches) if isinstance(branches, list) else 'N/A'}, " f"current={repr(current_branch)}", func_name=func_name, ) listbox = getattr(self, "branch_listbox", None) if not listbox or not listbox.winfo_exists(): log_handler.log_error( "branch_listbox not available for update.", func_name=func_name ) return try: listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) sel_idx = -1 # Assicurati che 'branches' sia una lista prima di iterare if isinstance(branches, list) and branches: # Resetta colore (se era grigio o rosso) try: default_fg = self.style.lookup("TListbox", "foreground") if listbox.cget("fg") != default_fg: listbox.config(fg=default_fg) except tk.TclError: pass # Ignora errori di stile # Popola la lista for i, branch in enumerate(branches): prefix = "* " if branch == current_branch else " " # Assicura che branch sia una stringa prima di inserire listbox.insert(tk.END, f"{prefix}{str(branch)}") if branch == current_branch: sel_idx = i elif isinstance(branches, list) and not branches: # Lista vuota valida listbox.insert(tk.END, "(No local branches)") listbox.config(fg="grey") else: # Caso in cui branches non è una lista (errore?) log_handler.log_warning( f"Invalid data received for branches: {repr(branches)}", func_name=func_name, ) listbox.insert(tk.END, "(Invalid data received)") listbox.config(fg="orange") # Imposta selezione e vista if sel_idx >= 0: listbox.selection_set(sel_idx) listbox.see(sel_idx) listbox.config(state=tk.NORMAL) # Mantieni selezionabile listbox.yview_moveto(0.0) except Exception as e: log_handler.log_exception( f"Error updating branch list GUI: {e}", func_name=func_name ) # Fallback: Mostra errore nella listbox 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) # Disabilita su errore except Exception as fallback_e: log_handler.log_error( f"Error displaying fallback error in branch listbox: {fallback_e}", func_name=func_name, ) # ---<<< 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): if state not in [tk.NORMAL, tk.DISABLED]: return # Invalid state widgets = [ self.save_settings_button, self.remove_profile_button, self.prepare_svn_button, self.create_bundle_button, self.fetch_bundle_button, self.edit_gitignore_button, self.manual_backup_button, self.commit_button, self.refresh_changes_button, self.refresh_tags_button, self.create_tag_button, self.checkout_tag_button, self.refresh_branches_button, self.create_branch_button, self.checkout_branch_button, self.refresh_history_button, self.history_branch_filter_combo, self.commit_message_text, self.autocommit_checkbox, self.apply_remote_config_button, self.check_auth_button, self.fetch_button, self.pull_button, self.push_button, self.push_tags_button, ] # log_handler.log_debug(f"Setting action widgets state to: {state}", func_name="set_action_widgets_state") # Usa log_handler for widget in widgets: # Check attribute exists before get/check widget widget_attr_name = None 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 = ( "readonly" if isinstance(widget, ttk.Combobox) and state == tk.NORMAL else state ) widget.config(state=w_state) except Exception as e: pass # log_handler.log_error(f"Error setting state for {widget_attr_name}: {e}", func_name="set_action_widgets_state") if hasattr(self, "profile_dropdown") and self.profile_dropdown.winfo_exists(): try: dd_state = "readonly" if state == tk.NORMAL else tk.DISABLED self.profile_dropdown.config(state=dd_state) except Exception as e: pass # log_handler.log_error(f"Error setting state for profile dropdown: {e}", func_name="set_action_widgets_state") # --- END OF FILE gui.py ---