# --- FILE: commit_detail_window.py --- import tkinter as tk from tkinter import ttk from tkinter import scrolledtext # Per il corpo del messaggio import os from typing import Dict, Any, List, Tuple, Callable, Optional # Import moduli locali (se necessari, come Tooltip o log_handler) import log_handler # Importa la classe Tooltip (assumendo sia definita in gui.py o altrove) try: # Prova a importare da gui.py se non è in un modulo separato from gui import Tooltip except ImportError: # Fallback se Tooltip non è disponibile (potrebbe causare errori se usato) log_handler.log_warning("Tooltip class not found, tooltips may be disabled.", func_name="import") # Definisci una classe fittizia per evitare errori immediati class Tooltip: def __init__(self, widget, text): pass class CommitDetailWindow(tk.Toplevel): """ Toplevel window to display detailed information about a specific Git commit, including metadata and a list of changed files. Allows opening a diff viewer for selected files. """ # Mappa per tradurre gli stati dei file (simile a DiffSummaryWindow) STATUS_MAP: Dict[str, str] = { 'A': 'Added', 'M': 'Modified', 'D': 'Deleted', 'R': 'Renamed', 'C': 'Copied', 'T': 'Type Change', 'U': 'Unmerged', 'X': 'Unknown', 'B': 'Broken', # Aggiungi altri se necessario } def __init__( self, master: tk.Misc, # Parent window commit_data: Dict[str, Any], # Dati ricevuti dal worker open_diff_callback: Callable[[str, str, str, Optional[str]], None] # Callback per aprire il diff ): """ Initializes the Commit Detail window. Args: master: The parent widget. commit_data (Dict[str, Any]): Dictionary containing commit details: 'hash_full', 'author_name', 'author_email', 'author_date', 'subject', 'body', 'files_changed' (List[Tuple[status, path1, path2]]) open_diff_callback (Callable): Function to call when a file is selected for diffing. Expects args: (commit_hash, file_status, file_path, old_file_path) """ super().__init__(master) func_name: str = "__init__ (CommitDetail)" # Nome per logging # --- Validazione Input --- if not isinstance(commit_data, dict): raise TypeError("commit_data must be a dictionary.") if not callable(open_diff_callback): raise TypeError("open_diff_callback must be callable.") # Memorizza dati e callback self.commit_data: Dict[str, Any] = commit_data self.open_diff_callback: Callable[[str, str, str, Optional[str]], None] = open_diff_callback # Estrai hash per usarlo nel titolo e nel callback diff self.commit_hash: Optional[str] = commit_data.get('hash_full') self.short_hash: str = self.commit_hash[:7] if self.commit_hash else "N/A" log_handler.log_info(f"Opening Commit Detail window for {self.short_hash}", func_name=func_name) # --- Configurazione Finestra --- self.title(f"Commit Details - {self.short_hash}") self.geometry("800x600") # Dimensioni suggerite self.minsize(550, 400) # Rendi modale rispetto al parent self.grab_set() self.transient(master) # --- Creazione Widget --- self._create_widgets() # Popola i widget con i dati del commit self._populate_details() # Centra la finestra self._center_window(master) # Imposta focus iniziale sulla lista dei file if hasattr(self, "files_tree"): self.files_tree.focus_set() def _create_widgets(self): """ Creates the main widgets for the commit detail view. """ # Frame principale con padding main_frame = ttk.Frame(self, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # Configura espansione righe/colonne main_frame.columnconfigure(0, weight=1) main_frame.rowconfigure(1, weight=0) # Riga metadati non si espande molto main_frame.rowconfigure(3, weight=1) # Riga corpo messaggio si espande main_frame.rowconfigure(5, weight=2) # Riga lista file si espande di più # --- Frame Metadati --- meta_frame = ttk.LabelFrame(main_frame, text="Commit Info", padding=(10, 5)) meta_frame.grid(row=0, column=0, sticky="ew", pady=(0, 5)) meta_frame.columnconfigure(1, weight=1) # Colonna valori si espande row_idx: int = 0 # Hash Completo ttk.Label(meta_frame, text="Commit Hash:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=2) self.hash_label = ttk.Label(meta_frame, text="", anchor="w", font=("Consolas", 9)) self.hash_label.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2) Tooltip(self.hash_label, "Full commit SHA-1 hash.") row_idx += 1 # Autore ttk.Label(meta_frame, text="Author:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=2) self.author_label = ttk.Label(meta_frame, text="", anchor="w") self.author_label.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2) Tooltip(self.author_label, "Commit author name and email.") row_idx += 1 # Data ttk.Label(meta_frame, text="Date:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=2) self.date_label = ttk.Label(meta_frame, text="", anchor="w") self.date_label.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2) Tooltip(self.date_label, "Author date.") row_idx += 1 # --- Subject (Titolo Commit) --- ttk.Label(main_frame, text="Subject:").grid(row=1, column=0, sticky="w", padx=5, pady=(5, 0)) self.subject_label = ttk.Label( main_frame, text="", anchor="w", font=("Segoe UI", 10, "bold"), relief=tk.GROOVE, padding=5 ) self.subject_label.grid(row=2, column=0, sticky="ew", padx=5, pady=(0, 5)) Tooltip(self.subject_label, "Commit subject line.") # --- Corpo Messaggio Commit --- ttk.Label(main_frame, text="Message Body:").grid(row=3, column=0, sticky="w", padx=5, pady=(0, 0)) self.body_text = scrolledtext.ScrolledText( master=main_frame, height=6, # Altezza iniziale wrap=tk.WORD, font=("Segoe UI", 9), state=tk.DISABLED, # Read-only padx=5, pady=5, borderwidth=1, relief=tk.SUNKEN, ) self.body_text.grid(row=4, column=0, sticky="nsew", padx=5, pady=(0, 5)) Tooltip(self.body_text, "Full commit message body.") # --- Lista File Modificati --- files_frame = ttk.LabelFrame(main_frame, text="Changed Files", padding=(10, 5)) files_frame.grid(row=5, column=0, sticky="nsew", pady=(5, 0)) files_frame.rowconfigure(0, weight=1) files_frame.columnconfigure(0, weight=1) # Treeview per i file (migliore per colonne) columns: Tuple[str, str] = ('status', 'file') self.files_tree = ttk.Treeview( master=files_frame, columns=columns, show='headings', # Mostra solo headings, non colonna #0 selectmode='browse', # Seleziona solo una riga height=7 # Altezza iniziale ) # Definisci headings self.files_tree.heading('status', text='Status', anchor='w') self.files_tree.heading('file', text='File Path', anchor='w') # Definisci colonne self.files_tree.column('status', width=80, stretch=tk.NO, anchor='w') self.files_tree.column('file', width=600, stretch=tk.YES, anchor='w') # Scrollbar verticale tree_scrollbar = ttk.Scrollbar( master=files_frame, orient=tk.VERTICAL, command=self.files_tree.yview ) self.files_tree.configure(yscrollcommand=tree_scrollbar.set) # Layout Treeview e Scrollbar self.files_tree.grid(row=0, column=0, sticky="nsew") tree_scrollbar.grid(row=0, column=1, sticky="ns") # Binding per doppio click sulla lista file -> apre diff self.files_tree.bind("", self._on_file_double_click) Tooltip(self.files_tree, "Double-click a file to view changes in this commit.") def _populate_details(self): """ Populates the widgets with data from self.commit_data. """ func_name: str = "_populate_details" # --- Popola Metadati --- if hasattr(self, "hash_label"): self.hash_label.config(text=self.commit_data.get('hash_full', 'N/A')) if hasattr(self, "author_label"): name: str = self.commit_data.get('author_name', 'N/A') email: str = self.commit_data.get('author_email', '') author_text: str = f"{name} <{email}>" if email else name self.author_label.config(text=author_text) if hasattr(self, "date_label"): self.date_label.config(text=self.commit_data.get('author_date', 'N/A')) if hasattr(self, "subject_label"): self.subject_label.config(text=self.commit_data.get('subject', '(No Subject)')) # --- Popola Corpo Messaggio --- if hasattr(self, "body_text"): body_content: str = self.commit_data.get('body', '(No Message Body)') try: self.body_text.config(state=tk.NORMAL) # Abilita per modifica self.body_text.delete("1.0", tk.END) # Pulisci self.body_text.insert(tk.END, body_content) # Inserisci testo self.body_text.config(state=tk.DISABLED) # Disabilita di nuovo except Exception as e: log_handler.log_error(f"Error populating commit body text: {e}", func_name=func_name) # Mostra errore nel widget stesso try: self.body_text.config(state=tk.NORMAL) self.body_text.delete("1.0", tk.END) self.body_text.insert(tk.END, f"(Error displaying body: {e})") self.body_text.config(state=tk.DISABLED) except Exception: pass # Ignora errori nel fallback # --- Popola Lista File Modificati --- if hasattr(self, "files_tree"): # Pulisci treeview precedente for item in self.files_tree.get_children(): self.files_tree.delete(item) # Ottieni lista file dal dizionario dati files_changed: List[Tuple[str, str, Optional[str]]] = self.commit_data.get('files_changed', []) if files_changed: # Inserisci ogni file nella treeview for i, (status_char, path1, path2) in enumerate(files_changed): display_status: str = self.STATUS_MAP.get(status_char, status_char) # Traduci stato display_path: str = "" # Gestisci visualizzazione per rinominati/copiati if status_char in ('R', 'C') and path2: display_path = f"{path1} -> {path2}" else: display_path = path1 # Usa path1 per A, M, D, T # Memorizza dati completi per il callback nel tag dell'item # (Alternativa: usare un dizionario separato mappato per iid) item_data_tag = f"file_{i}" self.files_tree.insert( parent='', index=tk.END, iid=i, # Usa indice come ID interno values=(display_status, display_path), tags=(item_data_tag,) # Associa un tag univoco ) # Salva i dati necessari per il diff nel tag config (un po' hacky ma funziona) self.files_tree.tag_configure( item_data_tag, foreground='black' # Dummy config, usiamo solo per storage ) # Allega i dati veri al tag self.files_tree.tag_bind( item_data_tag, "", lambda e, sc=status_char, p1=path1, p2=path2: setattr(e.widget, f"_data_{e.widget.focus()}", (sc, p1, p2)) ) else: # Mostra messaggio se non ci sono file cambiati (raro per un commit!) self.files_tree.insert(parent='', index=tk.END, values=("", "(No files changed in commit data)")) def _on_file_double_click(self, event: tk.Event): """ Handles double-click on the files Treeview. """ func_name: str = "_on_file_double_click" if not hasattr(self, "files_tree"): return # Ottieni l'ID dell'item selezionato selected_iid = self.files_tree.focus() # focus() restituisce l'iid dell'elemento con focus if not selected_iid: return # Nessun item selezionato try: # Recupera i dati associati all'item (dalla tupla originale) item_index = int(selected_iid) files_list: list = self.commit_data.get('files_changed', []) if 0 <= item_index < len(files_list): status_char, path1, path2 = files_list[item_index] # Determina quale path passare e se c'era un path vecchio file_path_to_diff: str = path2 if status_char in ('R', 'C') and path2 else path1 old_file_path_for_diff: Optional[str] = path1 if status_char in ('R', 'C') else None # ---<<< MODIFICA: Non passare None come hash commit >>>--- commit_hash_full: Optional[str] = self.commit_data.get('hash_full') if commit_hash_full: log_handler.log_debug( f"Calling open_diff_callback for file: {file_path_to_diff}, commit: {commit_hash_full[:7]}", func_name=func_name ) # Chiama il callback passato da GitUtility self.open_diff_callback( commit_hash_full, # Hash completo del commit status_char, # Stato del file (A, M, D...) file_path_to_diff, # Path del file nel commit old_file_path_for_diff # Path vecchio (solo per R/C) ) else: log_handler.log_error("Cannot open diff: Commit hash is missing.", func_name=func_name) messagebox.showerror("Error", "Internal error: Commit hash not available.", parent=self) # ---<<< FINE MODIFICA >>>--- else: log_handler.log_error( f"Selected Treeview item IID '{selected_iid}' out of range for data.", func_name=func_name ) messagebox.showerror("Error", "Internal error: Selected item data not found.", parent=self) except ValueError: log_handler.log_error(f"Invalid Treeview item IID: '{selected_iid}'", func_name=func_name) messagebox.showerror("Error", "Internal error: Invalid item selected.", parent=self) except Exception as e: log_handler.log_exception(f"Error handling file double-click: {e}", func_name=func_name) messagebox.showerror("Error", f"Could not process file selection:\n{e}", parent=self) def _center_window(self, parent: tk.Misc): """ Centers the Toplevel window relative to its parent. """ func_name: str = "_center_window (CommitDetail)" try: self.update_idletasks() # Ensure window dimensions are calculated # Get parent geometry parent_x: int = parent.winfo_rootx() parent_y: int = parent.winfo_rooty() parent_w: int = parent.winfo_width() parent_h: int = parent.winfo_height() # Get self geometry win_w: int = self.winfo_width() win_h: int = self.winfo_height() # Calculate position pos_x: int = parent_x + (parent_w // 2) - (win_w // 2) pos_y: int = parent_y + (parent_h // 2) - (win_h // 2) # Keep window on screen screen_w: int = self.winfo_screenwidth() screen_h: int = 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)) # Set geometry self.geometry(f"+{pos_x}+{pos_y}") except Exception as e: # Log error if centering fails log_handler.log_error( f"Could not center CommitDetailWindow: {e}", func_name=func_name ) # --- FINE FILE commit_detail_window.py ---