SXXXXXXX_GitUtility/gitutility/gui/tabs/commit_tab.py
2025-07-29 08:41:44 +02:00

215 lines
8.9 KiB
Python

# --- 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("<Double-Button-1>", self._on_changed_file_double_click)
self.changed_files_listbox.bind("<Button-3>", 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)