# --- FILE: diff_viewer.py --- import tkinter as tk from tkinter import ttk, scrolledtext, Canvas, messagebox import difflib import os import locale # Per fallback encoding import log_handler from git_commands import GitCommands, GitCommandError # Importa anche i livelli di logging se usati nei messaggi fallback import logging from typing import List, Optional # Per type hints # --- Tooltip Class Definition (Copiata da gui.py per standalone, o importata) --- # (Assumiamo sia definita qui o importata correttamente se messa in un file separato) 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(): # Usiamo add='+' per non sovrascrivere altri binding Enter/Leave self.widget.bind("", self.enter, add="+") self.widget.bind("", self.leave, add="+") self.widget.bind("", self.leave, add="+") # Nasconde su click def enter(self, event=None): self.unschedule() # Pianifica la visualizzazione dopo un ritardo (es. 500ms) self.id = self.widget.after(500, self.showtip) 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: # Necessario controllare che widget esista prima di chiamare after_cancel if self.widget and self.widget.winfo_exists(): self.widget.after_cancel(id_to_cancel) except Exception: # Ignora errori se il widget è già distrutto pass def showtip(self): # Non mostrare se il widget non esiste più if not self.widget or not self.widget.winfo_exists(): return # Nascondi tooltip precedente se esiste self.hidetip() # Calcola posizione tooltip vicino al cursore o al widget x_cursor, y_cursor = 0, 0 try: # Prova a ottenere posizione cursore x_cursor = self.widget.winfo_pointerx() + 15 y_cursor = self.widget.winfo_pointery() + 10 except Exception: # Fallback: posizione relativa al widget 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: # Impossibile determinare posizione return # Crea la finestra Toplevel per il tooltip self.tooltip_window = tw = tk.Toplevel(self.widget) # Rimuovi decorazioni finestra (titolo, bordi standard) tw.wm_overrideredirect(True) try: # Posiziona la finestra tw.wm_geometry(f"+{int(x_cursor)}+{int(y_cursor)}") except tk.TclError: # Può fallire se le coordinate sono fuori schermo tw.destroy() self.tooltip_window = None return # Crea etichetta dentro la finestra Toplevel label = tk.Label( tw, text=self.text, justify=tk.LEFT, background="#ffffe0", # Sfondo giallo chiaro relief=tk.SOLID, borderwidth=1, font=("tahoma", "8", "normal"), # Font piccolo standard wraplength=350 # Larghezza massima prima di andare a capo ) label.pack(ipadx=3, ipady=3) # Piccolo padding interno def hidetip(self): tw = self.tooltip_window self.tooltip_window = None if tw: try: # Controlla se la finestra esiste prima di distruggerla if tw.winfo_exists(): tw.destroy() except Exception: # Ignora errori se la finestra è già distrutta pass class DiffViewerWindow(tk.Toplevel): """ Toplevel window to display a side-by-side diff of a file. Can show differences between working dir vs HEAD/Index, or between two Git references. Includes synchronized scrolling via clicking on the overview minimap. Uses log_handler for logging. """ def __init__( self, master, git_commands: GitCommands, repo_path: str, relative_file_path: str, # Path relativo del file ref1: str, # Riferimento Sinistra (es. 'WORKING_DIR', 'HEAD', 'master', 'origin/dev') ref2: str # Riferimento Destra (es. 'HEAD', 'origin/main', commit_hash) ): """ Initializes the Diff Viewer window. Args: master: The parent widget. git_commands (GitCommands): Instance for Git interaction. repo_path (str): Absolute path to the Git repository. relative_file_path (str): Relative path of the file within the repo. ref1 (str): Reference for the left pane (e.g., 'WORKING_DIR', branch, tag, commit). ref2 (str): Reference for the right pane (e.g., 'HEAD', branch, tag, commit). """ super().__init__(master) func_name = "__init__ (DiffViewer)" # Nome per logging # --- Validazione Input Base --- if not isinstance(git_commands, GitCommands): # Logga errore critico e solleva eccezione se git_commands non è valido msg = "DiffViewerWindow requires a valid GitCommands instance." log_handler.log_critical(msg, func_name=func_name) # Potremmo mostrare messagebox qui, ma ValueError è più standard per argomenti raise ValueError(msg) if not repo_path or not os.path.isdir(repo_path): msg = f"Invalid repository path provided: '{repo_path}'" log_handler.log_error(msg, func_name=func_name) raise ValueError(msg) if not relative_file_path: msg = "Relative file path cannot be empty." log_handler.log_error(msg, func_name=func_name) raise ValueError(msg) if not ref1 or not ref2: msg = "Both ref1 and ref2 must be specified for comparison." log_handler.log_error(msg, func_name=func_name) raise ValueError(msg) # Memorizza argomenti self.git_commands = git_commands self.repo_path = repo_path self.relative_file_path = relative_file_path self.ref1 = ref1 self.ref2 = ref2 # Nomi brevi per visualizzazione nelle etichette e messaggi self.ref1_display_name = "Working Dir" if self.ref1 == 'WORKING_DIR' else self.ref1 self.ref2_display_name = "Working Dir" if self.ref2 == 'WORKING_DIR' else self.ref2 log_handler.log_info(f"Opening diff for '{self.relative_file_path}': Comparing '{self.ref1_display_name}' (Left) vs '{self.ref2_display_name}' (Right)", func_name=func_name) # --- Configurazione Finestra Toplevel --- self.title(f"Diff - {os.path.basename(self.relative_file_path)}") self.geometry("920x650") # Dimensioni iniziali suggerite self.minsize(550, 400) # Dimensioni minime self.grab_set() # Rende modale rispetto al parent self.transient(master) # Appare sopra il parent # --- Stato Interno --- self.pane1_content_lines: List[str] = [] # Contenuto sinistra self.pane2_content_lines: List[str] = [] # Contenuto destra self.diff_map: List[int] = [] # Mappa per minimap self._scrolling_active: bool = False # Flag per scroll sincronizzato self._configure_timer_id = None # ID per debounce resize minimap self.loading_label: Optional[ttk.Label] = None # --- Costruzione Interfaccia Grafica --- log_handler.log_debug("Creating diff viewer widgets...", func_name=func_name) self._create_widgets(self.ref1_display_name, self.ref2_display_name) # --- Caricamento Contenuti e Calcolo Diff --- log_handler.log_debug("Loading content and computing diff...", func_name=func_name) load_ok = False try: self._show_loading_message() # Forza l'aggiornamento della GUI per mostrare il messaggio prima del blocco self.update_idletasks() load_ok = self._load_content() # Usa self.ref1 e self.ref2 internamente if load_ok: # Se il caricamento (anche parziale, es. file non trovato) è ok, calcola diff self._compute_and_display_diff() # Imposta lo scrolling sincronizzato (solo minimap click) self._setup_scrolling() else: # Se _load_content ritorna False (errore critico lettura/git), mostra errore log_handler.log_warning("Content loading failed critically, populating text widgets with error.", func_name=func_name) self._populate_text(self.text_pane1, [("error", f"")]) self._populate_text(self.text_pane2, [("error", f"")]) # Disegna comunque minimappa vuota self.minimap_canvas.after(50, self._draw_minimap) self._hide_loading_message() except Exception as load_err: # Errore imprevisto durante caricamento o calcolo diff self._hide_loading_message() log_handler.log_exception(f"Unexpected error during diff setup for '{self.relative_file_path}': {load_err}", func_name=func_name) messagebox.showerror("Fatal Error", f"Failed to display diff:\n{load_err}", parent=self) # Chiudi la finestra se l'inizializzazione fallisce gravemente self.after_idle(self.destroy) return # Esce da __init__ # Centra la finestra dopo che i widget sono stati creati self._center_window(master) log_handler.log_debug("DiffViewerWindow initialized successfully.", func_name=func_name) def _center_window(self, parent): """Centers the Toplevel window relative to its parent.""" func_name = "_center_window (DiffViewer)" try: self.update_idletasks() # Assicura dimensioni widget aggiornate # Calcola coordinate (come prima) parent_x = parent.winfo_rootx(); parent_y = parent.winfo_rooty() parent_w = parent.winfo_width(); parent_h = parent.winfo_height() win_w = self.winfo_width(); win_h = self.winfo_height() pos_x = parent_x + (parent_w // 2) - (win_w // 2) pos_y = parent_y + (parent_h // 2) - (win_h // 2) # Assicura visibilità screen_w = self.winfo_screenwidth(); screen_h = self.winfo_screenheight() pos_x = max(0, min(pos_x, screen_w - win_w)) pos_y = max(0, min(pos_y, screen_h - win_h)) # Imposta geometria self.geometry(f"+{int(pos_x)}+{int(pos_y)}") except Exception as e: log_handler.log_error(f"Could not center DiffViewerWindow: {e}", func_name=func_name) def _create_widgets(self, label1: str, label2: str): """Creates the main widgets for the diff view, using provided labels.""" main_frame = ttk.Frame(self, padding=5) main_frame.pack(fill=tk.BOTH, expand=True) main_frame.rowconfigure(1, weight=1) # Riga text widget si espande main_frame.columnconfigure(0, weight=1) # Colonna Sinistra (Pane 1) main_frame.columnconfigure(1, weight=1) # Colonna Destra (Pane 2) main_frame.columnconfigure(2, weight=0, minsize=40) # Minimap # Etichette Titoli (usano i parametri passati) ttk.Label(main_frame, text=label1).grid(row=0, column=0, sticky="w", padx=(5, 2), pady=(0, 2)) ttk.Label(main_frame, text=label2).grid(row=0, column=1, sticky="w", padx=(2, 5), pady=(0, 2)) ttk.Label(main_frame, text="Overview").grid(row=0, column=2, sticky="w", padx=(5, 0), pady=(0, 2)) # Widget Testo (rinominati pane1/pane2) text_font = ("Consolas", 10) # Font Monospace common_text_opts = { "wrap": tk.NONE, "font": text_font, "padx": 5, "pady": 5, "undo": False, "state": tk.DISABLED, "borderwidth": 1, "relief": tk.SUNKEN, } # Pane Sinistra self.text_pane1 = tk.Text(main_frame, name="text_pane1", **common_text_opts) self.text_pane1.grid(row=1, column=0, sticky="nsew", padx=(5, 2)) # Pane Destra self.text_pane2 = tk.Text(main_frame, name="text_pane2", **common_text_opts) self.text_pane2.grid(row=1, column=1, sticky="nsew", padx=(2, 5)) # Minimap Canvas self.minimap_canvas = Canvas(main_frame, width=40, bg="#F0F0F0", relief=tk.SUNKEN, borderwidth=1, highlightthickness=0) self.minimap_canvas.grid(row=1, column=2, sticky="ns", padx=(5, 0)) # Ridisegna su resize self.minimap_canvas.bind("", self._on_minimap_resize) self.loading_label = ttk.Label( main_frame, # Appoggia al main_frame per essere sopra gli altri text="Loading file versions, please wait...", # Testo in inglese font=("Segoe UI", 12, "italic"), background="#FFFACD", # Giallo chiaro foreground="#555555", # Grigio scuro relief=tk.SOLID, borderwidth=1, padding=(10, 5) ) # Configurazione Tag Highlighting (aggiornato nomi widget) self.text_pane1.tag_config("removed", background="#FFE0E0") # Righe solo a sinistra self.text_pane1.tag_config("empty", background="#F5F5F5", foreground="#A0A0A0") # Filler self.text_pane2.tag_config("added", background="#E0FFE0") # Righe solo a destra self.text_pane2.tag_config("empty", background="#F5F5F5", foreground="#A0A0A0") # Filler # Tag per errori (comune) self.text_pane1.tag_config("error", background="yellow", foreground="red") self.text_pane2.tag_config("error", background="yellow", foreground="red") def _load_content(self) -> bool: """Loads file content based on self.ref1 and self.ref2.""" func_name = "_load_content (DiffViewer)" log_handler.log_info(f"Loading content for diff: '{self.relative_file_path}' ({self.ref1} vs {self.ref2})", func_name=func_name) load_success = True # Traccia successo globale # --- Carica Contenuto PANE 1 (Sinistra) --- ref1_source = self.ref1 log_handler.log_debug(f"Loading content for Pane 1 from: {ref1_source}", func_name=func_name) content_pane1_raw = "" # Inizializza if ref1_source == 'WORKING_DIR': # Leggi dal filesystem locale try: workdir_full_path = os.path.join(self.repo_path, self.relative_file_path) if os.path.exists(workdir_full_path) and os.path.isfile(workdir_full_path): try: # Prova UTF-8 with open(workdir_full_path, "r", encoding="utf-8") as f: content_pane1_raw = f.read() except UnicodeDecodeError: # Fallback log_handler.log_warning(f"UTF-8 decode failed for WD '{workdir_full_path}', trying fallback.", func_name=func_name) try: fallback_encoding = locale.getpreferredencoding(False) or "latin-1" log_handler.log_debug(f"Using fallback encoding: {fallback_encoding}", func_name=func_name) with open(workdir_full_path, "r", encoding=fallback_encoding, errors="replace") as f: content_pane1_raw = f.read() except Exception as fb_e: # Errore anche nel fallback log_handler.log_error(f"WD fallback read failed for '{workdir_full_path}': {fb_e}", func_name=func_name) content_pane1_raw = f"" load_success = False # Errore grave lettura else: # File non trovato sul disco log_handler.log_info(f"Working directory file not found for Pane 1: {workdir_full_path}", func_name=func_name) content_pane1_raw = "" # Consideriamo questo un successo parziale (l'altra parte potrebbe caricare) except Exception as e_wd: # Errore generico lettura WD log_handler.log_exception(f"Error loading WD content for Pane 1 ('{self.relative_file_path}'): {e_wd}", func_name=func_name) content_pane1_raw = f"" load_success = False # Errore grave self.pane1_content_lines = content_pane1_raw.splitlines() else: # Leggi da riferimento Git try: # Chiama il metodo GitCommands che gestisce 'git show ref:path' content_pane1_raw = self.git_commands.get_file_content_from_ref( self.repo_path, self.relative_file_path, ref=ref1_source ) if content_pane1_raw is None: # File non trovato nel ref Git log_handler.log_info(f"File '{self.relative_file_path}' not found in ref '{ref1_source}' for Pane 1.", func_name=func_name) content_pane1_raw = f"" # Successo parziale self.pane1_content_lines = content_pane1_raw.splitlines() except Exception as e_ref1: # Errore durante 'git show' log_handler.log_exception(f"Error loading Git ref '{ref1_source}' for Pane 1 ('{self.relative_file_path}'): {e_ref1}", func_name=func_name) self.pane1_content_lines = [f""] load_success = False # Errore grave # --- Carica Contenuto PANE 2 (Destra) --- # (Logica speculare a Pane 1, usando ref2_source) ref2_source = self.ref2 log_handler.log_debug(f"Loading content for Pane 2 from: {ref2_source}", func_name=func_name) content_pane2_raw = "" # Inizializza if ref2_source == 'WORKING_DIR': try: workdir_full_path = os.path.join(self.repo_path, self.relative_file_path) if os.path.exists(workdir_full_path) and os.path.isfile(workdir_full_path): try: with open(workdir_full_path, "r", encoding="utf-8") as f: content_pane2_raw = f.read() except UnicodeDecodeError: log_handler.log_warning(f"UTF-8 decode failed for WD '{workdir_full_path}' (Pane 2), trying fallback.", func_name=func_name) try: fallback_encoding = locale.getpreferredencoding(False) or "latin-1" with open(workdir_full_path, "r", encoding=fallback_encoding, errors="replace") as f: content_pane2_raw = f.read() except Exception as fb_e: log_handler.log_error(f"WD fallback read failed for '{workdir_full_path}' (Pane 2): {fb_e}", func_name=func_name) content_pane2_raw = f""; load_success = False else: log_handler.log_info(f"Working directory file not found for Pane 2: {workdir_full_path}", func_name=func_name) content_pane2_raw = "" except Exception as e_wd2: log_handler.log_exception(f"Error loading WD content for Pane 2 ('{self.relative_file_path}'): {e_wd2}", func_name=func_name) content_pane2_raw = f""; load_success = False self.pane2_content_lines = content_pane2_raw.splitlines() else: # Leggi da riferimento Git try: content_pane2_raw = self.git_commands.get_file_content_from_ref( self.repo_path, self.relative_file_path, ref=ref2_source ) if content_pane2_raw is None: log_handler.log_info(f"File '{self.relative_file_path}' not found in ref '{ref2_source}' for Pane 2.", func_name=func_name) content_pane2_raw = f"" self.pane2_content_lines = content_pane2_raw.splitlines() except Exception as e_ref2: log_handler.log_exception(f"Error loading Git ref '{ref2_source}' for Pane 2 ('{self.relative_file_path}'): {e_ref2}", func_name=func_name) self.pane2_content_lines = [f""] load_success = False # Ritorna True se non ci sono stati errori gravi di caricamento return load_success def _compute_and_display_diff(self): """Calculates diff using SequenceMatcher and populates text widgets.""" func_name = "_compute_and_display_diff (DiffViewer)" log_handler.log_debug("Computing and displaying diff...", func_name=func_name) # Usa le variabili membro aggiornate lines1 = self.pane1_content_lines lines2 = self.pane2_content_lines # Caso base: nessun contenuto if not lines1 and not lines2: log_handler.log_warning("Both panes have empty content for diff.", func_name=func_name) self._populate_text(self.text_pane1, [("empty", f"(No content in '{self.ref1_display_name}')")]) self._populate_text(self.text_pane2, [("empty", f"(No content in '{self.ref2_display_name}')")]) self.diff_map = [] self.minimap_canvas.after(50, self._draw_minimap) # Aggiorna minimappa vuota return # Calcola differenze matcher = difflib.SequenceMatcher(None, lines1, lines2, autojunk=False) # Prepara liste per visualizzazione allineata pane1_lines_display = [] # Righe per PANE 1 (Sinistra) pane2_lines_display = [] # Righe per PANE 2 (Destra) diff_map_for_minimap = [] # Mappa colori per minimappa log_handler.log_debug("Generating aligned display lines using opcodes...", func_name=func_name) # Itera sugli opcodes del SequenceMatcher for tag, i1, i2, j1, j2 in matcher.get_opcodes(): chunk1 = lines1[i1:i2] # Chunk da lista sinistra chunk2 = lines2[j1:j2] # Chunk da lista destra if tag == "equal": # Righe uguali for k in range(len(chunk1)): pane1_lines_display.append(("equal", chunk1[k])) # Nessun tag speciale o 'equal' pane2_lines_display.append(("equal", chunk2[k])) diff_map_for_minimap.append(0) # 0 = uguale (grigio) elif tag == "delete": # Presente solo in Pane 1 (sinistra) -> Rimosso for k in range(len(chunk1)): pane1_lines_display.append(("removed", chunk1[k])) # Tag 'removed' per sinistra pane2_lines_display.append(("empty", "")) # Filler per destra diff_map_for_minimap.append(1) # 1 = rimosso/modificato (rosso) elif tag == "insert": # Presente solo in Pane 2 (destra) -> Aggiunto for k in range(len(chunk2)): pane1_lines_display.append(("empty", "")) # Filler per sinistra pane2_lines_display.append(("added", chunk2[k])) # Tag 'added' per destra diff_map_for_minimap.append(2) # 2 = aggiunto/modificato (verde) elif tag == "replace": # Modificato (diverso tra sinistra e destra) len1 = len(chunk1); len2 = len(chunk2) max_len = max(len1, len2) for k in range(max_len): line1 = chunk1[k] if k < len1 else "" line2 = chunk2[k] if k < len2 else "" tag1 = "removed" if k < len1 else "empty" # Sinistra usa 'removed' o filler tag2 = "added" if k < len2 else "empty" # Destra usa 'added' o filler pane1_lines_display.append((tag1, line1)) pane2_lines_display.append((tag2, line2)) # Minimap: rosso se c'è contenuto a sinistra, verde altrimenti minimap_code = 1 if tag1 != "empty" else 2 diff_map_for_minimap.append(minimap_code) # Controllo coerenza (debug) if len(pane1_lines_display) != len(pane2_lines_display) or len(pane1_lines_display) != len(diff_map_for_minimap): log_handler.log_error("Internal Diff Error: Mismatch in aligned line counts!", func_name=func_name) # Potremmo mostrare errore all'utente o popolare con messaggio di errore # Aggiorna mappa per minimap self.diff_map = diff_map_for_minimap # Popola i widget di testo log_handler.log_debug("Populating text widgets with aligned lines...", func_name=func_name) self._populate_text(self.text_pane1, pane1_lines_display) # Popola PANE 1 self._populate_text(self.text_pane2, pane2_lines_display) # Popola PANE 2 # Disegna/Aggiorna minimappa (con leggero ritardo) log_handler.log_debug("Scheduling minimap draw.", func_name=func_name) self.minimap_canvas.after(100, self._draw_minimap) def _populate_text(self, text_widget: tk.Text, lines_data: List[tuple[str, str]]): """Populates a text widget with lines data, applying tags.""" func_name = "_populate_text" widget_name = "unknown_widget" try: widget_name = text_widget.winfo_name() except Exception: pass log_handler.log_debug(f"Populating widget '{widget_name}' with {len(lines_data)} lines.", func_name=func_name) try: # Abilita, pulisci, inserisci, applica tag, disabilita original_state = text_widget.cget("state") text_widget.config(state=tk.NORMAL) text_widget.delete("1.0", tk.END) if not lines_data: text_widget.insert("1.0", "(No content)\n", ("empty",)) else: content_string = "" tags_to_apply = [] current_line_num = 1 for tag_code, content in lines_data: line_content = content + "\n" content_string += line_content start_index = f"{current_line_num}.0" end_index = f"{current_line_num}.end" # Mappa codice tag a nome tag effettivo if tag_code in ["removed", "added", "empty", "error"]: tags_to_apply.append((tag_code, start_index, end_index)) current_line_num += 1 # Inserisci tutto il testo text_widget.insert("1.0", content_string) # Applica i tag for tag_name, start, end in tags_to_apply: try: text_widget.tag_add(tag_name, start, end) except tk.TclError as tag_err: log_handler.log_error(f"Error applying tag '{tag_name}' from {start} to {end}: {tag_err}", func_name=func_name) # Ripristina stato e posizione vista text_widget.config(state=tk.DISABLED) text_widget.yview_moveto(0.0) log_handler.log_debug(f"Finished populating widget '{widget_name}'.", func_name=func_name) except Exception as e: log_handler.log_exception(f"Error populating text widget '{widget_name}': {e}", func_name=func_name) # Prova a mostrare errore nel widget stesso try: if text_widget.winfo_exists(): text_widget.config(state=tk.NORMAL); text_widget.delete("1.0", tk.END) text_widget.insert("1.0", f"", ("error",)) text_widget.config(state=tk.DISABLED) except Exception: pass def _draw_minimap(self): """Draws the minimap overview based on self.diff_map.""" func_name = "_draw_minimap" canvas = self.minimap_canvas # Pulisci elementi precedenti canvas.delete("diff_line") canvas.delete("viewport_indicator") num_lines = len(self.diff_map) if num_lines == 0: return # Assicura dimensioni valide canvas.update_idletasks() canvas_width = canvas.winfo_width(); canvas_height = canvas.winfo_height() if canvas_width <= 1 or canvas_height <= 1: return log_handler.log_debug(f"Drawing minimap ({canvas_width}x{canvas_height}) for {num_lines} lines.", func_name=func_name) # Calcola altezza linea e disegna rettangoli exact_line_height = float(canvas_height) / num_lines accumulated_height = 0.0 for i, diff_type in enumerate(self.diff_map): y_start = round(accumulated_height) accumulated_height += exact_line_height y_end = round(accumulated_height) if y_end <= y_start: y_end = y_start + 1 if i == num_lines - 1: y_end = canvas_height # Forza ultimo pixel # Colori: 0=uguale(grigio), 1=rimosso/mod(rosso), 2=aggiunto/mod(verde) color = "#F0F0F0" # Default grigio if diff_type == 1: color = "#FFD0D0" # Rosso chiaro elif diff_type == 2: color = "#D0FFD0" # Verde chiaro canvas.create_rectangle(0, y_start, canvas_width, y_end, fill=color, outline="", tags="diff_line") # Disegna indicatore viewport self._update_minimap_viewport() log_handler.log_debug("Minimap drawing complete.", func_name=func_name) def _hide_loading_message(self): """Removes the loading label from view.""" if self.loading_label and self.loading_label.winfo_exists(): try: self.loading_label.place_forget() log_handler.log_debug("Loading message hidden.", func_name="_hide_loading_message") except Exception as e: log_handler.log_error(f"Failed to hide loading label: {e}", func_name="_hide_loading_message") def _show_loading_message(self): """Places and lifts the loading label to make it visible.""" if self.loading_label and self.loading_label.winfo_exists(): try: # Place al centro del main_frame (primo figlio di self) container = self.winfo_children()[0] if self.winfo_children() else self self.loading_label.place(in_=container, relx=0.5, rely=0.5, anchor=tk.CENTER) self.loading_label.lift() log_handler.log_debug("Loading message shown.", func_name="_show_loading_message") except Exception as e: log_handler.log_error(f"Failed to place/lift loading label: {e}", func_name="_show_loading_message") # --- Scrolling Logic --- def _setup_scrolling(self): """Configures minimap click for navigation.""" # Associa evento click sinistro sul canvas alla funzione di scroll self.minimap_canvas.bind("", self._on_minimap_click) def _reset_scroll_flag(self): """Resets the scrolling flag.""" self._scrolling_active = False def _update_minimap_viewport(self): """Updates the indicator rectangle on the minimap showing current view.""" func_name = "_update_minimap_viewport" canvas = self.minimap_canvas canvas.delete("viewport_indicator") # Rimuovi indicatore precedente try: # Ottieni frazioni y visibili da un widget testo (pane1 va bene) first_visible_fraction, last_visible_fraction = self.text_pane1.yview() except tk.TclError: log_handler.log_warning("TclError getting text yview, cannot update minimap viewport.", func_name=func_name) return # Assicura dimensioni canvas valide canvas.update_idletasks() canvas_height = canvas.winfo_height(); canvas_width = canvas.winfo_width() if canvas_height <= 1 or canvas_width <= 1: return # Canvas non pronto # Calcola coordinate y del rettangolo indicatore y_start = first_visible_fraction * canvas_height y_end = last_visible_fraction * canvas_height if y_end <= y_start: y_end = y_start + 1 # Altezza minima 1px # Disegna nuovo rettangolo indicatore canvas.create_rectangle( 1, y_start, canvas_width - 1, y_end, # Margine laterale 1px outline="black", width=1, tags="viewport_indicator" ) # Assicura che sia sopra le linee colorate canvas.tag_raise("viewport_indicator") def _on_minimap_click(self, event): """Handles clicks on the minimap to scroll the text views.""" func_name = "_on_minimap_click" if self._scrolling_active: return # Ignora click se già in scroll self._scrolling_active = True try: canvas = self.minimap_canvas canvas_height = canvas.winfo_height() if canvas_height <= 1: self._scrolling_active = False; return # Calcola frazione verticale del click target_fraction = max(0.0, min(event.y / canvas_height, 1.0)) log_handler.log_debug(f"Minimap clicked at y={event.y}, fraction={target_fraction:.3f}", func_name=func_name) # Muovi entrambi i widget di testo alla stessa frazione self.text_pane1.yview_moveto(target_fraction) self.text_pane2.yview_moveto(target_fraction) # Aggiorna indicatore self._update_minimap_viewport() finally: # Resetta flag dopo breve ritardo self.after(50, self._reset_scroll_flag) # --- Minimap Resize Handling --- def _on_minimap_resize(self, event): """Called when the minimap canvas is resized. Schedules redraw (debounce).""" func_name = "_on_minimap_resize" if self._configure_timer_id: self.after_cancel(self._configure_timer_id) self._configure_timer_id = self.after(150, self._redraw_minimap_on_resize) # log_handler.log_debug(f"Scheduled minimap redraw timer: {self._configure_timer_id}", func_name=func_name) # Log opzionale def _redraw_minimap_on_resize(self): """Actually redraws the minimap. Called by the debounced timer.""" func_name = "_redraw_minimap_on_resize" log_handler.log_debug("Executing debounced minimap redraw due to resize.", func_name=func_name) self._configure_timer_id = None # Resetta ID timer self._draw_minimap() # Chiama funzione di disegno # --- END OF FILE diff_viewer.py ---