# --- FILE: gitsync_tool/gui/tabs/history_tab.py --- import tkinter as tk from tkinter import ttk, messagebox from typing import Callable, List, Optional from gitutility.gui.tooltip import Tooltip from gitutility.logging_setup import log_handler class HistoryTab(ttk.Frame): """ The 'History' tab in the main application notebook. This tab provides a treeview for browsing the Git commit history and widgets for filtering the view. """ def __init__(self, master: tk.Misc, **kwargs): """ Initializes the History tab. Args: master: The parent widget (the ttk.Notebook). **kwargs: Dictionary of callbacks from the main controller. """ super().__init__(master, padding=(10, 10)) # Store callbacks self.refresh_history_callback = kwargs.get("refresh_history_cb") self.view_commit_details_callback = kwargs.get("view_commit_details_cb") self.reset_to_commit_callback = kwargs.get("reset_to_commit_cb") # --- Get a reference to the main frame for shared components --- self.main_frame = self.master.master # --- Tkinter Variables specific to this tab --- self.history_branch_filter_var = tk.StringVar() # Configure layout self.rowconfigure(1, weight=1) self.columnconfigure(0, weight=1) # Create widgets self._create_widgets() # ... (il resto del file rimane identico) ... def _create_widgets(self) -> None: """Creates and arranges all widgets for this tab.""" # --- Controls Frame (Filter and Refresh) --- controls_frame = ttk.Frame(self) controls_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) controls_frame.columnconfigure(1, weight=1) ttk.Label(controls_frame, text="Filter History by Branch/Tag:").pack( side=tk.LEFT, padx=(0, 5) ) self.history_branch_filter_combo = ttk.Combobox( controls_frame, textvariable=self.history_branch_filter_var, state="readonly", width=40, ) 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() ) Tooltip( self.history_branch_filter_combo, "Select a branch or tag to filter the commit history.", ) self.refresh_history_button = ttk.Button( controls_frame, text="Refresh History", command=self.refresh_history_callback, state=tk.DISABLED, ) self.refresh_history_button.pack(side=tk.LEFT, padx=5) Tooltip( self.refresh_history_button, "Reload commit history based on the selected filter.", ) # --- Treeview Frame for History --- content_frame = ttk.Frame(self) content_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5)) content_frame.rowconfigure(0, weight=1) content_frame.columnconfigure(0, weight=1) columns = ("hash", "datetime", "author", "details") self.history_tree = ttk.Treeview( content_frame, columns=columns, show="headings", selectmode="browse", height=15, ) self.history_tree.heading("hash", text="Hash", anchor="w") self.history_tree.heading("datetime", text="Date/Time", anchor="w") self.history_tree.heading("author", text="Author", anchor="w") self.history_tree.heading("details", text="Subject / Refs", anchor="w") self.history_tree.column("hash", width=80, stretch=tk.NO, anchor="w") self.history_tree.column("datetime", width=140, stretch=tk.NO, anchor="w") self.history_tree.column("author", width=150, stretch=tk.NO, anchor="w") self.history_tree.column("details", width=450, stretch=tk.YES, anchor="w") tree_scrollbar_y = ttk.Scrollbar( content_frame, orient=tk.VERTICAL, command=self.history_tree.yview ) tree_scrollbar_x = ttk.Scrollbar( content_frame, orient=tk.HORIZONTAL, command=self.history_tree.xview ) self.history_tree.configure( yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set ) self.history_tree.grid(row=0, column=0, sticky="nsew") tree_scrollbar_y.grid(row=0, column=1, sticky="ns") tree_scrollbar_x.grid(row=1, column=0, columnspan=2, sticky="ew") self.history_tree.bind("", self._on_history_double_click) self.history_tree.bind("", self._show_context_menu) Tooltip( self.history_tree, "Double-click a commit line to view details.\nRight-click for more options.", ) def set_action_widgets_state(self, state: str) -> None: """Sets the state of all action widgets in this tab.""" combo_state = "readonly" if state == tk.NORMAL else tk.DISABLED widgets = { self.history_branch_filter_combo: combo_state, self.refresh_history_button: state, } for widget, widget_state in widgets.items(): if widget and widget.winfo_exists(): widget.config(state=widget_state) if self.history_tree and self.history_tree.winfo_exists(): if state == tk.DISABLED: self.history_tree.unbind("") self.history_tree.unbind("") else: self.history_tree.bind( "", self._on_history_double_click ) self.history_tree.bind("", self._show_context_menu) def update_history_display(self, log_lines: List[str]) -> None: """Populates the history treeview with parsed log data.""" func_name = "update_history_display (Tree)" if not self.history_tree.winfo_exists(): return for item_id in self.history_tree.get_children(): self.history_tree.delete(item_id) if not isinstance(log_lines, list): self.history_tree.insert( "", "end", values=("", "", "", "(Error: Invalid data received)") ) return if not log_lines: self.history_tree.insert( "", "end", values=("", "", "", "(No history found)") ) return for i, line in enumerate(log_lines): line_str = str(line).strip() if not line_str or line_str.startswith("("): continue try: parts = line_str.split("|", 2) part1 = parts[0].strip() commit_author = parts[1].strip() commit_details = parts[2].strip() first_space = part1.find(" ") commit_hash = part1[:first_space] commit_datetime = part1[first_space:].strip()[:16] except Exception: commit_hash, commit_datetime, commit_author, commit_details = ( "Error", "", "", line_str, ) self.history_tree.insert( "", "end", iid=i, values=(commit_hash, commit_datetime, commit_author, commit_details), ) self.history_tree.yview_moveto(0.0) self.history_tree.xview_moveto(0.0) def update_history_branch_filter(self, branches_tags: List[str]): """Updates the options in the branch/tag filter combobox.""" if not self.history_branch_filter_combo.winfo_exists(): return opts = ["-- All History --"] + sorted(branches_tags if branches_tags else []) self.history_branch_filter_combo["values"] = opts current_selection = self.history_branch_filter_var.get() if current_selection not in opts: self.history_branch_filter_var.set(opts[0]) def _on_history_double_click(self, event: tk.Event) -> None: """Handles double-click on a commit in the history tree.""" func_name = "_on_history_double_click" try: selected_iid = self.history_tree.focus() if not selected_iid: return item_data = self.history_tree.item(selected_iid) item_values = item_data.get("values") if item_values and len(item_values) > 0: commit_hash = str(item_values[0]).strip() if ( commit_hash and not commit_hash.startswith("(") and callable(self.view_commit_details_callback) ): log_handler.log_info( f"Requesting details for hash: '{commit_hash}'", func_name=func_name, ) self.view_commit_details_callback(commit_hash) except Exception as e: log_handler.log_exception( f"Error handling history double-click: {e}", func_name=func_name ) messagebox.showerror( "Error", f"Could not process history selection:\n{e}", parent=self ) def _show_context_menu(self, event: tk.Event) -> None: """Displays a context menu on right-click.""" # Select the item under the cursor iid = self.history_tree.identify_row(event.y) if not iid: # Clicked outside of any item return self.history_tree.selection_set(iid) self.history_tree.focus(iid) item_data = self.history_tree.item(iid) item_values = item_data.get("values") if not item_values or not str(item_values[0]).strip(): return commit_hash = str(item_values[0]).strip() context_menu = tk.Menu(self, tearoff=0) context_menu.add_command( label=f"View Details for {commit_hash[:7]}...", command=lambda: self.view_commit_details_callback(commit_hash), ) context_menu.add_separator() context_menu.add_command( label=f"Reset branch to this commit ({commit_hash[:7]}) ...", command=lambda: self._on_reset_to_commit(commit_hash), ) try: context_menu.tk_popup(event.x_root, event.y_root) finally: context_menu.grab_release() def _on_reset_to_commit(self, commit_hash: str) -> None: """Handles the 'Reset to commit' action from the context menu.""" func_name = "_on_reset_to_commit" if not callable(self.reset_to_commit_callback): log_handler.log_warning( "reset_to_commit_callback is not available.", func_name ) return title = "Confirm Destructive Action" message = ( f"Are you sure you want to reset your current branch to commit {commit_hash[:7]}?" f"WARNING: This is a destructive operation!" f"- All commits made after this one will be permanently lost." f"- All uncommitted changes in your working directory will be permanently lost." f"This action cannot be undone. Proceed with caution." ) if messagebox.askokcancel(title, message, icon=messagebox.WARNING, parent=self): log_handler.log_warning( f"User confirmed reset to commit: '{commit_hash}'", func_name ) self.reset_to_commit_callback(commit_hash) else: log_handler.log_info("User cancelled reset operation.", func_name)