# --- 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 except ImportError: DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "backup_fallback") # Usa print qui perché il sistema di log potrebbe non essere inizializzato print( f"WARNING: gui.py could not import DEFAULT_BACKUP_DIR. Using fallback: {DEFAULT_BACKUP_DIR}", 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 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, ): """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 # 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() # --- 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.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.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) # --- 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_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): if ( not hasattr(self, "branch_listbox") or not self.branch_listbox.winfo_exists() ): return try: self.branch_listbox.config(state=tk.NORMAL) self.branch_listbox.delete(0, tk.END) sel_idx = -1 if branches: try: 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_idx = i else: self.branch_listbox.insert(tk.END, "(No local branches)") self.branch_listbox.config(fg="grey") if sel_idx >= 0: self.branch_listbox.selection_set(sel_idx) self.branch_listbox.see(sel_idx) self.branch_listbox.config(state=tk.NORMAL) self.branch_listbox.yview_moveto(0.0) except Exception as e: print(f"ERROR updating branch list GUI: {e}", file=sys.stderr) try: self.branch_listbox.delete(0, tk.END) self.branch_listbox.insert(tk.END, "(Error)") self.branch_listbox.config(fg="red") except: pass 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): if not hasattr(self, "history_text") or not self.history_text.winfo_exists(): return try: self.history_text.config(state=tk.NORMAL) self.history_text.delete("1.0", tk.END) self.history_text.insert( tk.END, "\n".join(log_lines) if log_lines else "(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: print(f"ERROR updating history GUI: {e}", file=sys.stderr) try: self.history_text.config(state=tk.NORMAL) self.history_text.delete("1.0", tk.END) self.history_text.insert(tk.END, "(Error)") self.history_text.config(state=tk.DISABLED, fg="red") except: pass 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): if ( not hasattr(self, "changed_files_listbox") or not self.changed_files_listbox.winfo_exists() ): return try: self.changed_files_listbox.config(state=tk.NORMAL) self.changed_files_listbox.delete(0, tk.END) if files_status_list: try: if self.changed_files_listbox.cget("fg") == "grey": self.changed_files_listbox.config( fg=self.style.lookup("TListbox", "foreground") ) except tk.TclError: pass for line in files_status_list: self.changed_files_listbox.insert(tk.END, line) else: self.changed_files_listbox.insert(tk.END, "(No changes detected)") self.changed_files_listbox.config(fg="grey") self.changed_files_listbox.config(state=tk.NORMAL) self.changed_files_listbox.yview_moveto(0.0) except Exception as e: print(f"ERROR updating changes list GUI: {e}", file=sys.stderr) try: self.changed_files_listbox.delete(0, tk.END) self.changed_files_listbox.insert(tk.END, "(Error)") self.changed_files_listbox.config(fg="red") except: pass 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): try: idx = self.changed_files_listbox.nearest(event.y) self.changed_files_listbox.selection_clear(0, tk.END) self.changed_files_listbox.selection_set(idx) self.changed_files_listbox.activate(idx) except tk.TclError: return sel = self.changed_files_listbox.curselection() if not sel: return line = self.changed_files_listbox.get(sel[0]) self.changed_files_context_menu.delete(0, tk.END) cleaned = line.strip() is_untracked = cleaned.startswith("??") can_add = ( is_untracked and hasattr(self, "add_selected_file_callback") and callable(self.add_selected_file_callback) ) add_state = tk.NORMAL if can_add else tk.DISABLED self.changed_files_context_menu.add_command( label="Add to Staging Area", state=add_state, command=lambda l=line: ( self.add_selected_file_callback(l) if can_add else None ), ) can_diff = hasattr(self, "open_diff_viewer_callback") and callable( self.open_diff_viewer_callback ) diff_state = ( tk.DISABLED if cleaned.startswith("??") or cleaned.startswith(" D") or not can_diff else tk.NORMAL ) self.changed_files_context_menu.add_command( label="View Changes (Diff)", state=diff_state, command=lambda l=line: ( self.open_diff_viewer_callback(l) if diff_state == tk.NORMAL else None ), ) try: self.changed_files_context_menu.tk_popup(event.x_root, event.y_root) finally: self.changed_files_context_menu.grab_release() def update_status_bar(self, message): if hasattr(self, "status_bar_var"): try: self.master.after(0, self.status_bar_var.set, message) except Exception as e: print(f"ERROR updating status bar: {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, ] # 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 ---