# --- FILE: gitsync_tool/gui/tabs/commit_tab.py --- import tkinter as tk from tkinter import ttk, scrolledtext from typing import Callable, List, Optional from gitutility.gui.tooltip import Tooltip from gitutility.logging_setup import log_handler class CommitTab(ttk.Frame): """ The 'Commit / Changes' tab in the main application notebook. This tab provides widgets for writing commit messages, viewing changed files, and performing commit actions. """ def __init__(self, master: tk.Misc, **kwargs): """ Initializes the Commit / Changes 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.commit_changes_callback = kwargs.get('commit_changes_cb') self.refresh_changed_files_callback = kwargs.get('refresh_changed_files_cb') self.open_diff_viewer_callback = kwargs.get('open_diff_viewer_cb') self.add_selected_file_callback = kwargs.get('add_selected_file_cb') # Get a reference to the main frame for shared components like menus self.main_frame = self.master.master # --- Tkinter Variables specific to this tab --- self.autocommit_var = tk.BooleanVar() # Configure layout self.rowconfigure(3, weight=1) self.columnconfigure(0, weight=1) # Create widgets self._create_widgets() def _create_widgets(self) -> None: """Creates and arranges all widgets for this tab.""" # --- Autocommit Checkbox --- self.autocommit_checkbox = ttk.Checkbutton( self, 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)) Tooltip( self.autocommit_checkbox, "If enabled, automatically stage all changes and commit them using the message below before creating a bundle." ) # --- Commit Message Area --- ttk.Label(self, text="Commit Message:").grid(row=1, column=0, columnspan=3, sticky="w", padx=5) self.commit_message_text = scrolledtext.ScrolledText( self, 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)) Tooltip(self.commit_message_text, "Enter the commit message here for manual commits or autocommits.") # --- Changed Files Frame --- changes_frame = ttk.LabelFrame(self, 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) Tooltip(self.changed_files_listbox, "List of changed, added, or deleted files. Double-click to view diff (vs HEAD). Right-click for actions.") 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)) Tooltip(self.refresh_changes_button, "Refresh the list of changed files in the working directory.") # --- Commit Button --- self.commit_button = ttk.Button( self, text="Commit Staged Changes", command=self.commit_changes_callback, state=tk.DISABLED ) self.commit_button.grid(row=4, column=0, columnspan=3, sticky="se", padx=5, pady=5) Tooltip(self.commit_button, "Stage all current changes (git add .) and commit them with the provided message.") def set_action_widgets_state(self, state: str) -> None: """Sets the state of all action widgets in this tab.""" list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED widgets = [ self.autocommit_checkbox, self.commit_message_text, self.refresh_changes_button, self.commit_button, self.changed_files_listbox, ] for widget in widgets: if widget and widget.winfo_exists(): widget.config(state=list_state) def update_changed_files_list(self, files_status_list: List[str]): """Populates the listbox with the status of changed files.""" listbox = self.changed_files_listbox if not listbox or not listbox.winfo_exists(): return listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) if files_status_list: listbox.config(fg="black") for status_line in files_status_list: sanitized_line = str(status_line).replace("\x00", "").strip() if sanitized_line: listbox.insert(tk.END, sanitized_line) else: listbox.insert(tk.END, "(No changes detected)") listbox.config(fg="grey") listbox.yview_moveto(0.0) def get_commit_message(self) -> str: """Returns the content of the commit message text widget.""" return self.commit_message_text.get("1.0", "end-1c").strip() def clear_commit_message(self): """Clears the commit message text widget.""" if self.commit_message_text.winfo_exists(): 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() def _on_changed_file_double_click(self, event: tk.Event) -> None: """Handles double-click on a file in the changed files list.""" selection = self.changed_files_listbox.curselection() if selection: line = self.changed_files_listbox.get(selection[0]) if line and callable(self.open_diff_viewer_callback): self.open_diff_viewer_callback(line) def _show_changed_files_context_menu(self, event: tk.Event) -> None: """Shows a context menu for the selected file in the changed files list.""" menu = self.main_frame.changed_files_context_menu try: idx = self.changed_files_listbox.nearest(event.y) if idx < 0: return self.changed_files_listbox.selection_clear(0, tk.END) self.changed_files_listbox.selection_set(idx) self.changed_files_listbox.activate(idx) line = self.changed_files_listbox.get(idx) except tk.TclError: return if not line: return menu.delete(0, tk.END) cleaned = line.strip() is_untracked = cleaned.startswith("??") # Add "Add to Staging" option add_state = tk.NORMAL if is_untracked and callable(self.add_selected_file_callback) else tk.DISABLED menu.add_command( label="Add to Staging Area", state=add_state, command=lambda: self.add_selected_file_callback(line) ) # Add "View Changes (Diff)" option can_diff = not is_untracked and not cleaned.startswith("!!") and not cleaned.startswith(" D") and callable(self.open_diff_viewer_callback) diff_state = tk.NORMAL if can_diff else tk.DISABLED menu.add_command( label="View Changes (Diff)", state=diff_state, command=lambda: self.open_diff_viewer_callback(line) ) menu.tk_popup(event.x_root, event.y_root)