# --- START OF FILE diff_viewer.py --- import tkinter as tk from tkinter import ttk, scrolledtext, Canvas, messagebox import difflib # Rimosso import logging import os import locale # Per fallback encoding # Importa il nuovo gestore della coda log import log_handler # Importa GitCommands per interagire con Git from git_commands import GitCommands, GitCommandError class DiffViewerWindow(tk.Toplevel): """ Toplevel window to display a side-by-side diff of a file. Shows differences between the working directory version and the HEAD version. Includes synchronized scrolling via clicking on the overview minimap. Uses log_handler for logging. """ def __init__( self, master, git_commands: GitCommands, repo_path: str, file_status_line: str ): """ Initializes the Diff Viewer window. Args: master: The parent widget (usually the main Tkinter root). git_commands (GitCommands): Instance of GitCommands for interacting with Git. repo_path (str): Absolute path to the Git repository. file_status_line (str): The full status line from 'git status --short' for the file to be diffed (e.g., " M path/to/file.py"). """ super().__init__(master) func_name = "__init__" # Per i log nell'init # Rimosso setup logger e fallback # self.logger = logger (Rimosso) # Setup GitCommands if not isinstance(git_commands, GitCommands): # Logga errore 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 anche mostrare un messagebox qui, ma l'eccezione è più standard raise ValueError(msg) self.git_commands = git_commands # Validazione repo_path (base) 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) messagebox.showerror("Initialization Error", msg, parent=master) self.after_idle(self.destroy) # Chiudi subito la finestra return self.repo_path = repo_path # Pulisci e valida il percorso relativo del file dalla riga di stato self.relative_file_path = self._clean_relative_path(file_status_line) if not self.relative_file_path: log_handler.log_error( f"Cannot show diff: Invalid path from '{file_status_line}'.", func_name=func_name, ) messagebox.showerror( "Path Error", "Invalid file path extracted for diff.", parent=master ) self.after_idle(self.destroy) # Chiudi se percorso non valido return # Esce dall'init # Configurazione Finestra Toplevel self.title(f"Diff - {os.path.basename(self.relative_file_path)}") self.geometry("920x650") # Dimensioni finestra iniziali self.minsize(550, 400) # Dimensioni minime consentite self.grab_set() # Rendi modale rispetto al parent self.transient(master) # Appare sopra il parent # Stato interno self.head_content_lines = [] self.workdir_content_lines = [] self.diff_map = ( [] ) # Mappa per minimappa: 0=uguale, 1=rimosso/mod, 2=aggiunto/mod self._scrolling_active = False # Flag per prevenire eventi ricorsivi di scroll self._configure_timer_id = None # ID per debounce resize minimap # Costruisci l'interfaccia grafica log_handler.log_debug("Creating diff viewer widgets...", func_name=func_name) self._create_widgets() # Carica il contenuto dei file e calcola/mostra il diff log_handler.log_debug( "Loading content and computing diff...", func_name=func_name ) load_ok = False try: load_ok = self._load_content() # Carica HEAD e Workdir if load_ok: # Calcola e visualizza le differenze se il caricamento è andato a buon fine self._compute_and_display_diff() # Configura solo il click sulla minimappa per lo scroll self._setup_scrolling() else: # Se _load_content fallisce, mostra errore nei widget di testo log_handler.log_warning( "Content loading failed, populating text widgets with error.", func_name=func_name, ) self._populate_text( self.text_head, [("error", "")] ) self._populate_text( self.text_workdir, [("error", "")] ) # Disegna comunque la minimappa (sarà vuota o con errore) self.minimap_canvas.after(50, self._draw_minimap) except Exception as load_err: # Errore imprevisto durante il caricamento/diff 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 ) self.after_idle(self.destroy) # Chiudi in caso di errore grave return # Esce dall'init # Centra la finestra rispetto al parent dopo aver creato i widget self._center_window(master) log_handler.log_debug( "DiffViewerWindow initialized successfully.", func_name=func_name ) def _clean_relative_path(self, file_status_line): """Extracts a clean relative path from a git status line.""" func_name = "_clean_relative_path" try: # Pulisci caratteri null e spazi esterni line = file_status_line.strip("\x00").strip() if not line: log_handler.log_warning( "Received empty status line for cleaning.", func_name=func_name ) return "" # Trova il primo spazio per separare lo stato dal percorso space_index = line.find(" ") # Se non c'è spazio o è all'inizio/fine, formato non valido if space_index <= 0 or space_index + 1 >= len(line): log_handler.log_warning( f"Could not find valid space separator in status line: '{line}'", func_name=func_name, ) return "" # Estrai la parte del percorso dopo il primo spazio relative_path_raw = line[space_index + 1 :].strip() # Gestisci rinominati "XY orig -> new" (prendi 'new') if "->" in relative_path_raw: # Prendi la parte dopo l'ultima occorrenza di "->" e puliscila relative_path = relative_path_raw.split("->")[-1].strip() else: relative_path = relative_path_raw # Gestisci le virgolette se presenti (tipico se non si usa -z) if ( len(relative_path) >= 2 and relative_path.startswith('"') and relative_path.endswith('"') ): relative_path = relative_path[1:-1] # Gestisci eventuali escape interni se necessario (più complesso, omesso per ora) # Controlla se il risultato è vuoto dopo la pulizia if not relative_path: log_handler.log_warning( f"Extracted path is empty from status line: '{line}'", func_name=func_name, ) return "" log_handler.log_debug( f"Cleaned path from '{file_status_line}' -> '{relative_path}'", func_name=func_name, ) return relative_path except Exception as e: log_handler.log_exception( f"Error cleaning path from status line '{file_status_line}': {e}", func_name=func_name, ) return "" # Ritorna vuoto in caso di errore def _center_window(self, parent): """Centers the Toplevel window relative to its parent.""" func_name = "_center_window" try: self.update_idletasks() # Assicura dimensioni widget aggiornate # Calcola coordinate 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à sullo schermo 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): """Creates the main widgets for the diff view (NO main scrollbar).""" 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 # Colonne: Sinistra (HEAD), Destra (Workdir), Minimap main_frame.columnconfigure(0, weight=1) # Colonna HEAD si espande main_frame.columnconfigure(1, weight=1) # Colonna Workdir si espande main_frame.columnconfigure(2, weight=0, minsize=40) # Minimap larghezza fissa # Etichette Titoli ttk.Label(main_frame, text="HEAD (Repository Version)").grid( row=0, column=0, sticky="w", padx=(5, 2), pady=(0, 2) ) ttk.Label(main_frame, text="Working Directory Version").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 (senza scrollbar verticali individuali) text_font = ("Consolas", 10) # Font Monospace suggerito common_text_opts = { "wrap": tk.NONE, # No a capo automatico "font": text_font, "padx": 5, "pady": 5, # Padding interno "undo": False, # Non serve undo in visualizzazione "state": tk.DISABLED, # Inizia non modificabile "borderwidth": 1, # Bordo "relief": tk.SUNKEN, } self.text_head = tk.Text(main_frame, name="text_head", **common_text_opts) self.text_workdir = tk.Text(main_frame, name="text_workdir", **common_text_opts) self.text_head.grid(row=1, column=0, sticky="nsew", padx=(5, 2)) self.text_workdir.grid(row=1, column=1, sticky="nsew", padx=(2, 5)) # Minimap Canvas self.minimap_canvas = Canvas( main_frame, width=40, bg="#F0F0F0", # Colore sfondo leggermente grigio relief=tk.SUNKEN, borderwidth=1, highlightthickness=0, # Aspetto ) self.minimap_canvas.grid(row=1, column=2, sticky="ns", padx=(5, 0)) # Ridisegna minimap quando il canvas viene ridimensionato self.minimap_canvas.bind("", self._on_minimap_resize) # Configurazione Tag per Highlighting Differenze # Usiamo nomi tag semplici e colori distinti self.text_head.tag_config( "removed", background="#FFE0E0" ) # Rosso chiaro per righe rimosse/modificate self.text_head.tag_config( "empty", background="#F5F5F5", foreground="#A0A0A0" ) # Grigio per filler self.text_workdir.tag_config( "added", background="#E0FFE0" ) # Verde chiaro per righe aggiunte/modificate self.text_workdir.tag_config( "empty", background="#F5F5F5", foreground="#A0A0A0" ) # Grigio per filler # Tag per errori (opzionale) self.text_head.tag_config("error", background="yellow", foreground="red") self.text_workdir.tag_config("error", background="yellow", foreground="red") def _load_content(self): """Loads file content from HEAD and working directory. Uses log_handler.""" func_name = "_load_content" log_handler.log_info( f"Loading content for diff: {self.relative_file_path}", func_name=func_name ) load_success = True # Flag per tracciare successo caricamento # --- Carica Contenuto HEAD --- try: # Usa GitCommands per ottenere il contenuto da HEAD head_content_raw = self.git_commands.get_file_content_from_ref( self.repo_path, self.relative_file_path, ref="HEAD" ) # Dividi in righe, gestisci caso None (file non in HEAD) self.head_content_lines = ( head_content_raw.splitlines() if head_content_raw is not None else [] ) if head_content_raw is None: # Non è un errore, ma un'informazione utile log_handler.log_info( f"File '{self.relative_file_path}' not found in HEAD (likely new).", func_name=func_name, ) except GitCommandError as e_git_head: log_handler.log_error( f"Git error loading HEAD content for '{self.relative_file_path}': {e_git_head}", func_name=func_name, ) self.head_content_lines = [f""] load_success = False except Exception as e_head: log_handler.log_exception( f"Unexpected error loading HEAD for '{self.relative_file_path}': {e_head}", func_name=func_name, ) self.head_content_lines = [f""] load_success = False # --- Carica Contenuto Working Directory --- try: # Costruisci percorso completo workdir_full_path = os.path.join(self.repo_path, self.relative_file_path) # Controlla se il file esiste sul disco if os.path.exists(workdir_full_path) and os.path.isfile(workdir_full_path): try: # Prova a leggere con UTF-8 (encoding più comune) with open(workdir_full_path, "r", encoding="utf-8") as f: self.workdir_content_lines = f.read().splitlines() except UnicodeDecodeError: # Fallback se UTF-8 fallisce: prova encoding preferito dal sistema o latin-1 log_handler.log_warning( f"UTF-8 decode failed for WD '{workdir_full_path}', trying fallback.", func_name=func_name, ) try: # Ottieni encoding preferito (o usa latin-1 come ultima risorsa) fallback_encoding = ( locale.getpreferredencoding(False) or "latin-1" ) log_handler.log_debug( f"Using fallback encoding: {fallback_encoding}", func_name=func_name, ) # Leggi con fallback encoding, rimpiazza caratteri non decodificabili with open( workdir_full_path, "r", encoding=fallback_encoding, errors="replace", ) as f: self.workdir_content_lines = f.read().splitlines() except Exception as fb_e: # Se anche il fallback fallisce log_handler.log_error( f"Working directory fallback read failed for '{workdir_full_path}': {fb_e}", func_name=func_name, ) self.workdir_content_lines = [ f"" ] load_success = False else: # File non trovato sul disco (es. cancellato localmente) log_handler.log_info( f"Working directory file not found: {workdir_full_path}", func_name=func_name, ) self.workdir_content_lines = [] # Lista vuota se file non esiste except Exception as e_wd: log_handler.log_exception( f"Error loading Workdir content for '{self.relative_file_path}': {e_wd}", func_name=func_name, ) self.workdir_content_lines = [f""] load_success = False return load_success def _compute_and_display_diff(self): """Calculates diff using SequenceMatcher and populates text widgets.""" func_name = "_compute_and_display_diff" log_handler.log_debug("Computing and displaying diff...", func_name=func_name) # Caso base: nessun contenuto da confrontare if not self.head_content_lines and not self.workdir_content_lines: log_handler.log_warning( "Both HEAD and Workdir content are empty or failed to load.", func_name=func_name, ) # Popola con messaggi appropriati self._populate_text(self.text_head, [("empty", "(No content in HEAD)")]) self._populate_text( self.text_workdir, [("empty", "(No content in Workdir)")] ) self.diff_map = [] # Mappa vuota # Richiama disegno minimappa (che sarà vuota) self.minimap_canvas.after(50, self._draw_minimap) return # Usa SequenceMatcher per trovare le differenze # autojunk=False considera tutte le righe, anche quelle "rumorose" matcher = difflib.SequenceMatcher( None, self.head_content_lines, self.workdir_content_lines, autojunk=False ) # Liste per contenere le righe da visualizzare, allineate head_lines_display = [] workdir_lines_display = [] # Mappa per la minimappa (0: uguale, 1: rimosso/modificato, 2: aggiunto/modificato) diff_map_for_minimap = [] log_handler.log_debug( "Generating aligned display lines using SequenceMatcher opcodes...", func_name=func_name, ) # Itera sugli opcodes generati da SequenceMatcher for tag, i1, i2, j1, j2 in matcher.get_opcodes(): # Estrai i chunk di righe corrispondenti dagli array originali head_chunk = self.head_content_lines[i1:i2] workdir_chunk = self.workdir_content_lines[j1:j2] if tag == "equal": # Righe uguali: aggiungi entrambe alle liste display senza tag specifici for k in range(len(head_chunk)): head_lines_display.append( ("equal", head_chunk[k]) ) # Usa 'equal' o '' per nessun tag speciale workdir_lines_display.append(("equal", workdir_chunk[k])) diff_map_for_minimap.append(0) # 0 = uguale elif tag == "delete": # Righe presenti solo in HEAD (rimosse in Workdir) for k in range(len(head_chunk)): head_lines_display.append( ("removed", head_chunk[k]) ) # Tag 'removed' per HEAD workdir_lines_display.append( ("empty", "") ) # Riga vuota filler per Workdir diff_map_for_minimap.append(1) # 1 = deleted/modified head side elif tag == "insert": # Righe presenti solo in Workdir (aggiunte rispetto a HEAD) for k in range(len(workdir_chunk)): head_lines_display.append( ("empty", "") ) # Riga vuota filler per HEAD workdir_lines_display.append( ("added", workdir_chunk[k]) ) # Tag 'added' per Workdir diff_map_for_minimap.append(2) # 2 = added/modified workdir side elif tag == "replace": # Righe modificate (diverse tra HEAD e Workdir) len_head = len(head_chunk) len_workdir = len(workdir_chunk) max_len = max( len_head, len_workdir ) # Lunghezza massima tra i due chunk # Itera per allineare le righe modificate, riempiendo con righe vuote se necessario for k in range(max_len): head_line = head_chunk[k] if k < len_head else "" workdir_line = workdir_chunk[k] if k < len_workdir else "" # Determina il tag ('removed'/'added' o 'empty' per filler) head_tag = "removed" if k < len_head else "empty" workdir_tag = "added" if k < len_workdir else "empty" # Aggiungi alle liste display head_lines_display.append((head_tag, head_line)) workdir_lines_display.append((workdir_tag, workdir_line)) # Segna come modificato sulla minimappa (rosso o verde) # Usa 1 (rosso) se la riga HEAD esiste, altrimenti 2 (verde) minimap_code = 1 if head_tag != "empty" else 2 diff_map_for_minimap.append(minimap_code) # Verifica coerenza interna (opzionale ma utile per debug) if len(head_lines_display) != len(workdir_lines_display) or len( head_lines_display ) != len(diff_map_for_minimap): log_handler.log_error( "Internal Diff Error: Mismatch in aligned line counts!", func_name=func_name, ) # Potrebbe essere utile mostrare un errore all'utente qui else: log_handler.log_debug( f"Aligned display generated with {len(head_lines_display)} lines.", func_name=func_name, ) # Aggiorna la mappa per la minimappa self.diff_map = diff_map_for_minimap # Popola i widget di testo con le righe allineate e i tag log_handler.log_debug("Populating text widgets...", func_name=func_name) self._populate_text(self.text_head, head_lines_display) self._populate_text(self.text_workdir, workdir_lines_display) # Disegna la minimappa (con un leggero ritardo per permettere rendering widget) log_handler.log_debug("Scheduling minimap draw.", func_name=func_name) self.minimap_canvas.after(100, self._draw_minimap) # Ritardo 100ms def _populate_text(self, text_widget, lines_data): """Populates a text widget with lines data, applying tags.""" func_name = "_populate_text" widget_name = text_widget.winfo_name() # Ottieni nome widget per log log_handler.log_debug( f"Populating widget '{widget_name}' with {len(lines_data)} lines.", func_name=func_name, ) # Abilita widget per modifica text_widget.config(state=tk.NORMAL) # Pulisci contenuto precedente text_widget.delete("1.0", tk.END) if not lines_data: # Caso senza dati log_handler.log_debug( f"No lines data provided for widget '{widget_name}'.", func_name=func_name, ) text_widget.insert( "1.0", "(No content)\n", ("empty",) ) # Mostra messaggio vuoto con tag 'empty' else: # Costruisci la stringa completa e raccogli i tag da applicare content_string = "" tags_to_apply = [] # Lista di tuple: (tag_name, start_index, end_index) current_line_num = 1 # Tkinter usa indice 1-based per le righe for data_tuple in lines_data: # Valida formato dati (deve essere tupla tag, contenuto) if not isinstance(data_tuple, tuple) or len(data_tuple) != 2: log_handler.log_warning( f"Skipping malformed data in '{widget_name}': {data_tuple}", func_name=func_name, ) tag_code = "error" # Tag per indicare errore interno line_content = "\n" else: tag_code, content = data_tuple # Assicura newline per il widget Text line_content = content + "\n" # Aggiungi il contenuto della riga alla stringa completa content_string += line_content # Calcola indici Tkinter per la riga corrente (es. "1.0", "1.end") start_index = f"{current_line_num}.0" end_index = f"{current_line_num}.end" # .end si riferisce alla fine logica della linea # Aggiungi tag alla lista se il codice corrisponde a un tag definito if tag_code == "removed": tags_to_apply.append(("removed", start_index, end_index)) elif tag_code == "added": tags_to_apply.append(("added", start_index, end_index)) elif tag_code == "empty": tags_to_apply.append(("empty", start_index, end_index)) elif tag_code == "error": tags_to_apply.append(("error", start_index, end_index)) # Ignora tag 'equal' o altri codici non mappati # Incrementa numero linea per la prossima iterazione current_line_num += 1 # Inserisci tutto il testo in una volta sola (più efficiente) text_widget.insert("1.0", content_string) # Applica tutti i tag raccolti in un secondo momento for tag_name, start, end in tags_to_apply: try: # Applica il tag agli indici specificati text_widget.tag_add(tag_name, start, end) except tk.TclError as tag_err: # Logga errore se l'applicazione del tag fallisce log_handler.log_error( f"Error applying tag '{tag_name}' from {start} to {end}: {tag_err}", func_name=func_name, ) # Disabilita widget dopo aver popolato (read-only) text_widget.config(state=tk.DISABLED) # Assicura che la vista sia all'inizio del testo text_widget.yview_moveto(0.0) log_handler.log_debug( f"Finished populating widget '{widget_name}'.", func_name=func_name ) def _draw_minimap(self): """Draws the minimap overview based on self.diff_map.""" func_name = "_draw_minimap" canvas = self.minimap_canvas # Pulisci elementi precedenti (linee diff e indicatore viewport) canvas.delete("diff_line") canvas.delete("viewport_indicator") num_lines = len(self.diff_map) if num_lines == 0: log_handler.log_debug( "No diff map data for minimap drawing.", func_name=func_name ) return # Niente da disegnare # Assicura che le dimensioni del canvas siano valide prima di disegnare canvas.update_idletasks() # Aggiorna dimensioni widget canvas_width = canvas.winfo_width() canvas_height = canvas.winfo_height() # Non disegnare se il canvas non ha dimensioni valide (es. finestra minimizzata) if canvas_width <= 1 or canvas_height <= 1: log_handler.log_debug( f"Skipping minimap draw due to invalid dimensions: {canvas_width}x{canvas_height}", func_name=func_name, ) return log_handler.log_debug( f"Drawing minimap ({canvas_width}x{canvas_height}) for {num_lines} lines.", func_name=func_name, ) # Calcola altezza per linea (può essere < 1 pixel) exact_line_height = float(canvas_height) / num_lines # Usa altezza accumulata per posizionamento preciso e arrotondamento accumulated_height = 0.0 for i, diff_type in enumerate(self.diff_map): # Arrotonda y iniziale e finale per disegno pixel y_start = round(accumulated_height) accumulated_height += exact_line_height y_end = round(accumulated_height) # Assicura altezza minima di 1 pixel per visibilità if y_end <= y_start: y_end = y_start + 1 # Forza ultima linea a raggiungere il bordo inferiore del canvas if i == num_lines - 1: y_end = canvas_height # Determina colore linea # 0 = uguale (grigio sfondo), 1 = rimosso/mod (rosso), 2 = aggiunto/mod (verde) color = "#F0F0F0" # Grigio default per uguale if diff_type == 1: color = "#FFD0D0" # Rosso chiaro elif diff_type == 2: color = "#D0FFD0" # Verde chiaro # Disegna rettangolo per la linea senza bordo canvas.create_rectangle( 0, y_start, canvas_width, y_end, fill=color, outline="", tags="diff_line", ) # Disegna/aggiorna l'indicatore della viewport self._update_minimap_viewport() log_handler.log_debug("Minimap drawing complete.", func_name=func_name) # --- Scrolling Logic (Minimap Click Only) --- def _setup_scrolling(self): """Configures ONLY minimap click for navigation.""" # Associa evento click sinistro sul canvas alla funzione di scroll self.minimap_canvas.bind("", self._on_minimap_click) # Nessun altro binding necessario (no scrollbar, no yscrollcommand) def _reset_scroll_flag(self): """Resets the scrolling flag to allow next scroll event.""" 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 # Rimuovi indicatore precedente canvas.delete("viewport_indicator") try: # Ottieni frazioni y visibili (inizio, fine) da un widget testo # Usiamo text_head, ma dovrebbero essere sincronizzati first_visible_fraction, last_visible_fraction = self.text_head.yview() except tk.TclError: # Gestisci errore se il widget testo è distrutto 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: log_handler.log_debug( "Canvas not ready for viewport indicator update.", func_name=func_name ) return # Calcola coordinate y del rettangolo indicatore y_start = first_visible_fraction * canvas_height y_end = last_visible_fraction * canvas_height # Assicura altezza minima di 1 pixel per visibilità if y_end <= y_start: y_end = y_start + 1 # Disegna il nuovo rettangolo indicatore con bordo nero canvas.create_rectangle( 1, y_start, canvas_width - 1, y_end, # Margine laterale 1px outline="black", width=1, # Bordo nero sottile tags="viewport_indicator", # Tag per poterlo cancellare ) # Assicura che l'indicatore sia disegnato 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" # Ignora click se uno scroll è già in corso (previene eventi multipli) if self._scrolling_active: return self._scrolling_active = True # Imposta flag blocco try: canvas = self.minimap_canvas canvas_height = canvas.winfo_height() # Non fare nulla se altezza non valida if canvas_height <= 1: self._scrolling_active = False # Resetta flag se esce subito return # Calcola la frazione verticale del click rispetto all'altezza canvas target_fraction = event.y / canvas_height # Limita la frazione tra 0.0 e 1.0 target_fraction = max(0.0, min(target_fraction, 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 a quella frazione # yview_moveto imposta la *prima* linea visibile a quella frazione self.text_head.yview_moveto(target_fraction) self.text_workdir.yview_moveto(target_fraction) # Aggiorna l'indicatore sulla minimappa per riflettere la nuova vista self._update_minimap_viewport() finally: # Resetta il flag dopo un breve ritardo per evitare loop o blocchi # Usiamo after per assicurarci che venga eseguito nell'event loop Tkinter self.after(50, self._reset_scroll_flag) # Ritardo 50ms # --- Minimap Resize Handling --- def _on_minimap_resize(self, event): """Called when the minimap canvas is resized. Schedules redraw (debounce).""" func_name = "_on_minimap_resize" # Cancella timer precedente se esiste per evitare ridisegni multipli if self._configure_timer_id: self.after_cancel(self._configure_timer_id) log_handler.log_debug( "Cancelled previous minimap resize timer.", func_name=func_name ) # Pianifica il ridisegno dopo un breve ritardo (es. 150ms) # Questo evita ridisegni continui durante il trascinamento della finestra 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, ) 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 # Chiama la funzione di disegno esistente self._draw_minimap() # --- END OF FILE diff_viewer.py ---