# gui.py import tkinter as tk from tkinter import ttk # Import themed widgets, including Notebook from tkinter import scrolledtext, filedialog, messagebox, simpledialog import logging import os import re # Needed for validation in dialogs # Import constant from the central location if available try: from config_manager import DEFAULT_BACKUP_DIR except ImportError: # Fallback if the import fails DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "backup_fallback") print( "Warning: Could not import DEFAULT_BACKUP_DIR from config_manager. Using fallback." ) # --- Tooltip Class Definition --- # (Using the improved version from previous discussions for robustness) 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 self.x = 0.0 self.y = 0.0 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() if self.widget and self.widget.winfo_exists(): self.id = self.widget.after(500, self.showtip) # 500ms delay 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 # Ignore errors cancelling def showtip(self): if not self.widget or not self.widget.winfo_exists(): return self.hidetip() try: x_cursor = self.widget.winfo_pointerx() + 15 y_cursor = self.widget.winfo_pointery() + 10 except Exception: try: # Fallback position 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 # Cannot get position 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 # Ignore errors destroying # --- Gitignore Editor Window Class --- # (No changes needed here for directory exclusion - keeping original structure) class GitignoreEditorWindow(tk.Toplevel): """Toplevel window for editing the .gitignore file.""" def __init__(self, master, gitignore_path, logger, on_save_success_callback=None): super().__init__(master) self.gitignore_path = gitignore_path if not isinstance(logger, (logging.Logger, logging.LoggerAdapter)): raise TypeError( "GitignoreEditorWindow requires a valid Logger or LoggerAdapter." ) self.logger = 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 ) 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) button_frame.columnconfigure(3, weight=1) 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): try: self.update_idletasks() px, py = parent.winfo_rootx(), parent.winfo_rooty() pw, ph = parent.winfo_width(), parent.winfo_height() ww, wh = self.winfo_width(), self.winfo_height() x = px + (pw // 2) - (ww // 2) y = py + (ph // 2) - (wh // 2) sw, sh = self.winfo_screenwidth(), 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: self.logger.error( f"Could not center GitignoreEditorWindow: {e}", exc_info=True ) def _load_file(self): self.logger.info(f"Loading gitignore: {self.gitignore_path}") content = "" try: if os.path.exists(self.gitignore_path): with open( self.gitignore_path, "r", encoding="utf-8", errors="replace" ) as f: content = f.read() else: self.logger.info(".gitignore does not exist.") 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 Exception as e: self.logger.error(f"Error loading .gitignore: {e}", exc_info=True) messagebox.showerror("Load Error", f"Error loading:\n{e}", parent=self) def _has_changes(self): 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): # (Logica interna di _save_file invariata) if not self._has_changes(): self.logger.info("No changes to save.") return True # Indicate success even if no changes were made current_content = self.text_editor.get("1.0", "end-1c") self.logger.info(f"Saving changes to: {self.gitignore_path}") try: with open(self.gitignore_path, "w", encoding="utf-8", newline="\n") as f: f.write(current_content) self.logger.info(".gitignore saved.") self.original_content = current_content self.text_editor.edit_modified(False) return True # Return True on successful save except Exception as e: self.logger.error(f"Error saving .gitignore: {e}", exc_info=True) messagebox.showerror("Save Error", f"Error saving:\n{e}", parent=self) return False # Return False on error def _save_and_close(self): # --- MODIFICA: Chiama il callback dopo il salvataggio --- if self._save_file(): # Check if save succeeded (or no changes) self.logger.debug("Save successful, attempting to call success callback.") # Call the callback if it exists if self.on_save_success_callback: try: self.on_save_success_callback() except Exception as cb_e: self.logger.error(f"Error executing on_save_success_callback: {cb_e}", exc_info=True) # Show an error, maybe? Or just log it. messagebox.showwarning("Callback Error", "Saved .gitignore, but failed to run post-save action.\nCheck logs.", parent=self) # Proceed to destroy the window regardless of callback success/failure self.destroy() # --- FINE MODIFICA --- # else: If save failed, the error is already shown, do nothing more. def _on_close(self): # (Logica _on_close invariata, gestisce solo la chiusura/cancel) if self._has_changes(): res = messagebox.askyesnocancel( "Unsaved Changes", "Save changes?", parent=self ) if res is True: self._save_and_close() # This will now trigger the callback if save succeeds elif res is False: self.logger.warning("Discarding .gitignore changes.") self.destroy() # else: Cancel, do nothing else: self.destroy() # --- Create Tag Dialog --- # (No changes needed here) class CreateTagDialog(simpledialog.Dialog): 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 # --- MODIFICA: Memorizza il suggerimento --- self.suggested_tag_name = suggested_tag_name # --- FINE MODIFICA --- # Chiamare super() alla fine o dopo aver inizializzato le variabili usate in body/validate 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") # --- MODIFICA: Imposta valore iniziale --- if self.suggested_tag_name: self.tag_name_var.set(self.suggested_tag_name) # --- FINE MODIFICA --- 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") # Ritorna il widget che deve avere il focus iniziale return self.name_entry # O self.message_entry se si preferisce # La validazione del nome tag ora avviene in GitCommands.create_tag # Manteniamo solo i controlli per non vuoto. 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 cannot be empty.", parent=self) return 0 if not msg: messagebox.showwarning("Input Error", "Tag message cannot be empty.", parent=self) return 0 # Rimuovi il controllo regex da qui, verrà fatto da GitCommands # pattern = r"^(?![./]|.*([./]{2,}|[.]$|\.lock$))[^ \t\n\r\f\v~^:?*[\\]+(?>", lambda e: self.load_profile_settings_callback(self.profile_var.get()), ) self.profile_var.trace_add( "write", lambda n, i, m: self.load_profile_settings_callback(self.profile_var.get()), ) self.create_tooltip(self.profile_dropdown, "Select the configuration 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 current settings to selected profile." ) self.add_profile_button = ttk.Button( button_subframe, text="Add New", width=8, command=self.add_profile_callback ) self.add_profile_button.pack(side=tk.LEFT, padx=(2, 2), pady=5) self.create_tooltip(self.add_profile_button, "Add a new 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 (default cannot be removed).", ) def _create_repo_tab(self): """Creates the frame for the 'Repository / Bundle' tab.""" # (No changes needed here for this modification) frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(1, weight=1) paths_frame = ttk.LabelFrame( frame, text="Repository Paths & Bundle Names", padding=(10, 5) ) paths_frame.pack(pady=5, fill="x") paths_frame.columnconfigure(1, weight=1) col_label, col_entry, col_button, col_indicator = 0, 1, 2, 3 ttk.Label(paths_frame, text="SVN Working Copy Path:").grid( row=0, column=col_label, sticky=tk.W, padx=5, pady=3 ) self.svn_path_entry = ttk.Entry(paths_frame, width=60) self.svn_path_entry.grid(row=0, column=col_entry, sticky=tk.EW, padx=5, pady=3) self.svn_path_entry.bind( "", lambda e: self.update_svn_status_callback(self.svn_path_entry.get()), ) self.svn_path_entry.bind( "", lambda e: self.update_svn_status_callback(self.svn_path_entry.get()), ) self.create_tooltip( self.svn_path_entry, "Path to the local SVN working copy directory." ) self.svn_path_browse_button = ttk.Button( paths_frame, text="Browse...", width=9, command=lambda: self.browse_folder_callback(self.svn_path_entry), ) self.svn_path_browse_button.grid( row=0, column=col_button, sticky=tk.W, padx=(0, 5), pady=3 ) self.svn_status_indicator = tk.Label( paths_frame, text="", width=2, height=1, relief=tk.SUNKEN, background=self.RED, anchor=tk.CENTER, ) self.svn_status_indicator.grid( row=0, column=col_indicator, sticky=tk.E, padx=(0, 5), pady=3 ) self.create_tooltip( self.svn_status_indicator, "Git repository status (Red=Not prepared, Green=Prepared)", ) ttk.Label(paths_frame, text="Bundle Target Directory:").grid( row=1, column=col_label, sticky=tk.W, padx=5, pady=3 ) self.usb_path_entry = ttk.Entry(paths_frame, width=60) self.usb_path_entry.grid(row=1, column=col_entry, sticky=tk.EW, padx=5, pady=3) self.create_tooltip( self.usb_path_entry, "Directory for Git bundle files (e.g., USB drive)." ) self.usb_path_browse_button = ttk.Button( paths_frame, text="Browse...", width=9, command=lambda: self.browse_folder_callback(self.usb_path_entry), ) self.usb_path_browse_button.grid( row=1, column=col_button, sticky=tk.W, padx=(0, 5), pady=3 ) ttk.Label(paths_frame, text="Create Bundle Filename:").grid( row=2, column=col_label, sticky=tk.W, padx=5, pady=3 ) self.bundle_name_entry = ttk.Entry(paths_frame, width=60) self.bundle_name_entry.grid( row=2, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) self.create_tooltip( self.bundle_name_entry, "Filename for the bundle to be created (e.g., project.bundle).", ) ttk.Label(paths_frame, text="Fetch Bundle Filename:").grid( row=3, column=col_label, sticky=tk.W, padx=5, pady=3 ) self.bundle_updated_name_entry = ttk.Entry(paths_frame, width=60) self.bundle_updated_name_entry.grid( row=3, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) self.create_tooltip( self.bundle_updated_name_entry, "Filename of the bundle to fetch updates from.", ) 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): """Creates the frame for the 'Backup Settings' tab.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) frame.columnconfigure(1, weight=1) # Allow entry column to expand # --- Configuration Frame --- 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) # Entry column expands col_label = 0 col_entry = 1 col_button = 2 # Autobackup Checkbox (Row 0) self.autobackup_checkbox = ttk.Checkbutton( config_frame, text="Enable Auto Backup before 'Create Bundle' / 'Fetch from Bundle'", variable=self.autobackup_var, command=self.toggle_backup_dir, ) self.autobackup_checkbox.grid( row=0, column=0, columnspan=3, sticky=tk.W, padx=5, pady=(5, 5) ) self.create_tooltip( self.autobackup_checkbox, "If checked, automatically create a ZIP backup before bundle operations.", ) # Backup Directory (Row 1) backup_dir_label = ttk.Label(config_frame, text="Backup Directory:") backup_dir_label.grid(row=1, column=col_label, sticky=tk.W, padx=5, pady=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=col_entry, sticky=tk.EW, padx=5, pady=3 ) self.create_tooltip( self.backup_dir_entry, "Directory where backups will be stored (enabled if Auto Backup checked).", ) self.backup_dir_button = ttk.Button( config_frame, text="Browse...", width=9, command=self.browse_backup_dir, state=tk.DISABLED, ) self.backup_dir_button.grid( row=1, column=col_button, sticky=tk.W, padx=(0, 5), pady=3 ) # Exclude Extensions (Row 2) exclude_ext_label = ttk.Label(config_frame, text="Exclude File Extensions:") exclude_ext_label.grid(row=2, column=col_label, 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=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3 ) self.create_tooltip( self.backup_exclude_entry, "Comma-separated extensions to exclude (e.g., .log,.tmp). Case-insensitive.", ) # --- MODIFICA: Aggiunta Campo Esclusione Directory --- # Exclude Directories (Row 3) - NEW exclude_dir_label = ttk.Label(config_frame, text="Exclude Directories:") exclude_dir_label.grid(row=3, column=col_label, sticky=tk.W, padx=5, pady=3) # Create the new Entry widget and link it to the new StringVar self.backup_exclude_dirs_entry = ttk.Entry( config_frame, textvariable=self.backup_exclude_dirs_var, # Link to the new variable width=60, ) self.backup_exclude_dirs_entry.grid( row=3, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3, # Span entry and button columns ) # Add a tooltip for explanation self.create_tooltip( self.backup_exclude_dirs_entry, "Comma-separated directory names to exclude (e.g., __pycache__, .venv, build). Case-insensitive.", ) # --- FINE MODIFICA --- # --- Manual Backup Action Frame --- action_frame = ttk.LabelFrame( frame, text="Manual Backup Action", padding=(10, 5) ) action_frame.pack(pady=10, fill="x", expand=False) self.manual_backup_button = ttk.Button( action_frame, text="Create Manual Backup Now (ZIP)", command=self.manual_backup_callback, state=tk.DISABLED, ) self.manual_backup_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_tooltip( self.manual_backup_button, "Create a ZIP backup immediately using current exclusion settings.", ) return frame def _create_commit_tab(self): """Creates the frame for the 'Commit' tab with changed files list.""" frame = ttk.Frame(self.notebook, padding=(10, 10)) # Riduci peso riga messaggio, aumenta peso riga lista file frame.rowconfigure(2, weight=0) # Riga messaggio commit non si espande molto frame.rowconfigure(4, weight=1) # Riga lista file si espande frame.columnconfigure(0, weight=1) # Colonna principale si espande # --- Sezione Autocommit --- (Invariata) self.autocommit_checkbox = ttk.Checkbutton( frame, # <<< DEVE ESSERE 'frame' QUI text="Enable Autocommit before 'Create Bundle' action", 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, "...") # --- Sezione Messaggio Commit --- (Altezza ridotta) 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, # <<< Altezza ridotta width=60, wrap=tk.WORD, font=("Segoe UI", 9), state=tk.DISABLED, undo=True, padx=5, pady=5, ) self.commit_message_text.grid(row=2, column=0, columnspan=3, sticky="ew", padx=5, pady=(0, 5)) self.create_tooltip(self.commit_message_text, "Enter commit message...") # --- MODIFICA: Aggiunta Sezione Changed Files --- changes_frame = ttk.LabelFrame(frame, text="Changes to be Committed / Staged", 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) # Lista si espande changes_frame.columnconfigure(0, weight=1) # Lista si espande # Lista File Modificati list_sub_frame = ttk.Frame(changes_frame) # Frame per listbox e scrollbar list_sub_frame.grid(row=0, column=0, 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, # Altezza iniziale, ma si espanderà con la riga 4 di 'frame' exportselection=False, selectmode=tk.SINGLE, font=("Consolas", 9), # Font Monospace utile per status ) self.changed_files_listbox.grid(row=0, column=0, sticky="nsew") # Associa doppio click all'apertura del diff viewer (verrà collegato in GitUtility) self.changed_files_listbox.bind("", self._on_changed_file_double_click) 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, "Double-click a file to view changes (diff).") # Pulsante Refresh Lista File self.refresh_changes_button = ttk.Button( changes_frame, text="Refresh List", # Collegato a callback in GitUtility # command=self.refresh_changed_files_callback state=tk.DISABLED # Abilitato quando repo è pronto ) self.refresh_changes_button.grid(row=1, column=0, sticky="w", padx=(0, 5), pady=(5, 0)) self.create_tooltip(self.refresh_changes_button, "Refresh the list of changed files.") # --- FINE MODIFICA --- # --- Pulsante Commit Manuale --- (Spostato sotto) self.commit_button = ttk.Button( frame, # Ora nel frame principale text="Commit All Changes Manually", # command=self.commit_changes_callback state=tk.DISABLED, ) # Messo in basso a destra self.commit_button.grid(row=4, column=2, sticky="se", padx=5, pady=5) self.create_tooltip(self.commit_button, "Stage ALL changes and commit with the message above.") return frame def _create_tags_tab(self): """Creates the frame for the 'Tags' tab.""" # (No changes needed here for this modification) 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 - Name & Subject):").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), ) 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)) button_width = 18 self.refresh_tags_button = ttk.Button( button_frame, text="Refresh Tags", width=button_width, command=self.refresh_tags_callback, state=tk.DISABLED, ) self.refresh_tags_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 5)) self.create_tooltip(self.refresh_tags_button, "Reload tag list.") self.create_tag_button = ttk.Button( button_frame, text="Create New Tag...", width=button_width, command=self.create_tag_callback, state=tk.DISABLED, ) self.create_tag_button.pack(side=tk.TOP, fill=tk.X, pady=5) self.create_tooltip(self.create_tag_button, "Commit (if needed) & create tag.") self.checkout_tag_button = ttk.Button( button_frame, text="Checkout Selected Tag", width=button_width, command=self.checkout_tag_callback, state=tk.DISABLED, ) self.checkout_tag_button.pack(side=tk.TOP, fill=tk.X, pady=5) self.create_tooltip( self.checkout_tag_button, "Switch to selected tag (Detached HEAD)." ) return frame def _create_branch_tab(self): """Creates the frame for the 'Branches' tab.""" # (No changes needed here for this modification) 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 Branch):").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), ) 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)) button_width = 18 self.refresh_branches_button = ttk.Button( button_frame, text="Refresh Branches", width=button_width, command=self.refresh_branches_callback, state=tk.DISABLED, ) self.refresh_branches_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 5)) self.create_tooltip(self.refresh_branches_button, "Reload branch list.") self.create_branch_button = ttk.Button( button_frame, text="Create New Branch...", width=button_width, command=self.create_branch_callback, state=tk.DISABLED, ) self.create_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5) self.create_tooltip(self.create_branch_button, "Create a new local branch.") self.checkout_branch_button = ttk.Button( button_frame, text="Checkout Selected Branch", width=button_width, 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): """Creates the frame for the 'History' tab.""" # (No changes needed here for this modification) 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 by Branch/Tag:").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, "Show history for selected item or all." ) self.refresh_history_button = ttk.Button( filter_frame, text="Refresh History", command=self.refresh_history_callback, state=tk.DISABLED, ) self.refresh_history_button.pack(side=tk.LEFT, padx=5) self.create_tooltip(self.refresh_history_button, "Load commit history.") 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, ) 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): """Creates the application log area within the specified parent.""" log_frame = ttk.LabelFrame(parent_frame, text="Application Log", padding=(10, 5)) log_frame.pack(fill=tk.BOTH, expand=True) # Si espande nel suo container log_frame.rowconfigure(0, weight=1) log_frame.columnconfigure(0, weight=1) self.log_text = scrolledtext.ScrolledText( log_frame, # ... (configurazione log_text invariata) ... height=8, width=100, font=("Consolas", 9), wrap=tk.WORD, state=tk.DISABLED, padx=5, pady=5, ) self.log_text.grid(row=0, column=0, sticky="nsew") # (configurazione tag invariata) 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): """Sets the initial value of the profile dropdown.""" # (No changes needed here for this modification) try: from config_manager import DEFAULT_PROFILE except ImportError: DEFAULT_PROFILE = "default" current_profiles = self.profile_dropdown.cget("values") 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("") # --- GUI Update Methods --- # (Methods like toggle_backup_dir, browse_backup_dir, update_svn_indicator, etc. # do not need changes for adding the exclude_dirs field itself, only for # potentially using its value if logic required it, which is not the case here.) # Keep existing methods as they were in the original provided code or improved versions. def toggle_backup_dir(self): """Enables/disables backup directory widgets based on autobackup checkbox.""" new_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=new_state) if hasattr(self, "backup_dir_button") and self.backup_dir_button.winfo_exists(): self.backup_dir_button.config(state=new_state) def browse_backup_dir(self): """Opens directory dialog for selecting the backup path.""" initial_dir = self.backup_dir_var.get() or DEFAULT_BACKUP_DIR if not os.path.isdir(initial_dir): initial_dir = os.path.expanduser("~") selected_directory = filedialog.askdirectory( initialdir=initial_dir, title="Select Backup Directory", parent=self.master ) if selected_directory: self.backup_dir_var.set(selected_directory) def update_svn_indicator(self, is_prepared): """Updates repo indicator color and related button states.""" indicator_color = self.GREEN if is_prepared else self.RED tooltip = "Prepared" if is_prepared else "Not prepared" repo_ready_state = tk.NORMAL if is_prepared else tk.DISABLED prepare_state = tk.DISABLED if is_prepared else tk.NORMAL # Basic check for path validity for some buttons like edit gitignore path_valid = bool(self.svn_path_entry.get().strip()) edit_gitignore_state = tk.NORMAL if path_valid else tk.DISABLED if hasattr(self, "svn_status_indicator"): self.svn_status_indicator.config(background=indicator_color) self.update_tooltip(self.svn_status_indicator, tooltip) if hasattr(self, "prepare_svn_button"): self.prepare_svn_button.config(state=prepare_state) # Update state for other buttons based on repo readiness or path validity widgets_require_ready = [ self.create_bundle_button, self.fetch_bundle_button, self.manual_backup_button, self.autocommit_checkbox, self.commit_button, self.commit_message_text, 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.history_text, ] for widget in widgets_require_ready: if widget and widget.winfo_exists(): current_state = repo_ready_state if isinstance(widget, ttk.Combobox): widget.config( state="readonly" if current_state == tk.NORMAL else tk.DISABLED ) elif isinstance(widget, (tk.Text, scrolledtext.ScrolledText)): widget.config(state=current_state) else: widget.config(state=current_state) if hasattr(self, "edit_gitignore_button"): self.edit_gitignore_button.config( state=edit_gitignore_state ) # Depends only on path validity def update_profile_dropdown(self, sections): """Updates profile dropdown list and attempts to restore selection.""" # (No changes needed here for this modification) if hasattr(self, "profile_dropdown") and self.profile_dropdown.winfo_exists(): current = self.profile_var.get() self.profile_dropdown["values"] = sections if sections: if current in sections: self.profile_var.set(current) elif "default" in sections: self.profile_var.set("default") else: self.profile_var.set(sections[0]) else: self.profile_var.set("") # --- List Update Methods (Tags, Branches, History) --- # (No changes needed here for this modification, keep original or improved versions) def update_tag_list(self, tags_data): if not hasattr(self, "tag_listbox"): return try: self.tag_listbox.delete(0, tk.END) if tags_data: try: # Reset color if self.tag_listbox.cget("fg") == "grey": self.tag_listbox.config( fg=self.style.lookup("TListbox", "foreground") ) except tk.TclError: pass for name, subject in tags_data: self.tag_listbox.insert(tk.END, f"{name}\t({subject})") else: self.tag_listbox.insert(tk.END, "(No tags found)") self.tag_listbox.config(fg="grey") except Exception as e: logging.error(f"Error tags: {e}", exc_info=True) self.tag_listbox.insert(tk.END, "(Error)") def update_status_bar(self, message): """Updates the text displayed in the status bar.""" if hasattr(self, "status_bar_var"): try: # Usiamo after(0,..) per sicurezza, anche se probabilmente non strettamente necessario # nella maggior parte dei casi di questa app. Previene potenziali problemi se # una chiamata arrivasse da un thread diverso (improbabile qui ma buona pratica). self.master.after(0, self.status_bar_var.set, message) except Exception as e: # Logga se l'aggiornamento della status bar fallisce # Evita di usare self.logger qui per non creare dipendenze circolari potenziali # durante l'inizializzazione o la chiusura. Usa print o un logger di base. print(f"ERROR: Failed to update status bar: {e}") def get_selected_tag(self): if hasattr(self, "tag_listbox"): indices = self.tag_listbox.curselection() if indices: item = self.tag_listbox.get(indices[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 update_branch_list(self, branches, current_branch): if not hasattr(self, "branch_listbox"): return try: self.branch_listbox.delete(0, tk.END) sel_index = -1 if branches: try: # Reset color if self.branch_listbox.cget("fg") == "grey": self.branch_listbox.config( fg=self.style.lookup("TListbox", "foreground") ) except tk.TclError: pass for i, branch in enumerate(branches): prefix = "* " if branch == current_branch else " " self.branch_listbox.insert(tk.END, f"{prefix}{branch}") if branch == current_branch: sel_index = i else: self.branch_listbox.insert(tk.END, "(No local branches)") self.branch_listbox.config(fg="grey") if sel_index >= 0: self.branch_listbox.selection_set(sel_index) self.branch_listbox.see(sel_index) except Exception as e: logging.error(f"Error branches: {e}", exc_info=True) self.branch_listbox.insert(tk.END, "(Error)") def get_selected_branch(self): if hasattr(self, "branch_listbox"): indices = self.branch_listbox.curselection() if indices: item = self.branch_listbox.get(indices[0]) branch_name = item.lstrip("* ").strip() if not branch_name.startswith("("): return branch_name return None def get_commit_message(self): if hasattr(self, "commit_message_text"): return self.commit_message_text.get("1.0", "end-1c").strip() return "" def clear_commit_message(self): if hasattr(self, "commit_message_text"): 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() def update_history_display(self, log_lines): if not hasattr(self, "history_text"): return try: self.history_text.config(state=tk.NORMAL) self.history_text.delete("1.0", tk.END) if log_lines: self.history_text.insert(tk.END, "\n".join(log_lines)) else: self.history_text.insert(tk.END, "(No history found)") self.history_text.config(state=tk.DISABLED) self.history_text.yview_moveto(0.0) self.history_text.xview_moveto(0.0) except Exception as e: logging.error(f"Error history: {e}", exc_info=True) self.history_text.insert(tk.END, "(Error)") def update_history_branch_filter(self, branches_and_tags, current_ref=None): if not hasattr(self, "history_branch_filter_combo"): return filter_options = ["-- All History --"] + sorted(branches_and_tags) self.history_branch_filter_combo["values"] = filter_options if current_ref and current_ref in filter_options: self.history_branch_filter_var.set(current_ref) else: self.history_branch_filter_var.set(filter_options[0]) # --- Dialog Wrappers (Unchanged) --- def ask_new_profile_name(self): return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master) def show_error(self, title, message): messagebox.showerror(title, message, parent=self.master) def show_info(self, title, message): messagebox.showinfo(title, message, parent=self.master) def show_warning(self, title, message): messagebox.showwarning(title, message, parent=self.master) def ask_yes_no(self, title, message): return messagebox.askyesno(title, message, parent=self.master) # --- Tooltip Helpers (Unchanged) --- def create_tooltip(self, widget, text): 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) # Recreate is simplest def update_changed_files_list(self, files_status_list): """Clears and populates the changed files listbox.""" if not hasattr(self, "changed_files_listbox"): return self.changed_files_listbox.config(state=tk.NORMAL) self.changed_files_listbox.delete(0, tk.END) if files_status_list: # Potresti voler formattare meglio lo stato qui for status_line in files_status_list: self.changed_files_listbox.insert(tk.END, status_line) else: self.changed_files_listbox.insert(tk.END, "(No changes detected)") # Questo chiamerà la funzione vera in GitUtility def _on_changed_file_double_click(self, event): # Recupera l'indice selezionato dalla listbox widget = event.widget selection = widget.curselection() if selection: index = selection[0] file_status_line = widget.get(index) # Chiama il metodo del controller (verrà impostato in __init__ di MainFrame) if hasattr(self, 'open_diff_viewer_callback') and callable(self.open_diff_viewer_callback): self.open_diff_viewer_callback(file_status_line)