# --- FILE: gitsync_tool/gui/tabs/branch_tab.py --- import tkinter as tk from tkinter import ttk from typing import Callable, List, Optional from gitutility.gui.tooltip import Tooltip from gitutility.logging_setup import log_handler class BranchTab(ttk.Frame): """ The 'Branches (Local Ops)' tab in the main application notebook. This tab provides widgets for listing, creating, checking out, and managing local Git branches. """ def __init__(self, master: tk.Misc, **kwargs): """ Initializes the Branches 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_branches_callback = kwargs.get("refresh_branches_cb") self.create_branch_callback = kwargs.get("create_branch_cb") self.checkout_branch_callback = kwargs.get("checkout_branch_cb") self.compare_branch_with_current_callback = kwargs.get( "compare_branch_with_current_cb" ) self.delete_local_branch_callback = kwargs.get("delete_local_branch_cb") self.merge_local_branch_callback = kwargs.get("merge_local_branch_cb") self.promote_to_main_callback = kwargs.get("promote_to_main_cb") # Get a reference to the main frame for shared components self.main_frame = self.master.master # Configure layout self.columnconfigure(0, weight=1) self.rowconfigure(1, weight=1) # Create widgets self._create_widgets() def _create_widgets(self) -> None: """Creates and arranges all widgets for this tab.""" ttk.Label(self, text="Local Branches (* = Current):").grid( row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2) ) # --- Listbox Frame --- list_frame = ttk.Frame(self) list_frame.grid(row=1, column=0, sticky="nsew", padx=(5, 0), pady=(0, 5)) list_frame.rowconfigure(0, weight=1) list_frame.columnconfigure(0, weight=1) self.branch_listbox = tk.Listbox( list_frame, height=10, exportselection=False, selectmode=tk.SINGLE, font=("Segoe UI", 9), borderwidth=1, relief=tk.SUNKEN, ) self.branch_listbox.grid(row=0, column=0, sticky="nsew") self.branch_listbox.bind("", self._show_local_branches_context_menu) scrollbar = ttk.Scrollbar( list_frame, orient=tk.VERTICAL, command=self.branch_listbox.yview ) scrollbar.grid(row=0, column=1, sticky="ns") self.branch_listbox.config(yscrollcommand=scrollbar.set) Tooltip( self.branch_listbox, "List of local branches. Right-click for actions (merge, delete, compare).", ) # --- Button Frame --- button_frame = ttk.Frame(self) button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5)) button_width = 20 self.refresh_branches_button = ttk.Button( button_frame, text="Refresh Branches", width=button_width, command=self.refresh_branches_callback, state=tk.DISABLED, ) self.refresh_branches_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 5)) Tooltip(self.refresh_branches_button, "Reload the list of local branches.") self.create_branch_button = ttk.Button( button_frame, text="Create New Branch...", width=button_width, command=self.create_branch_callback, state=tk.DISABLED, ) self.create_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5) Tooltip( self.create_branch_button, "Create a new local branch starting from the current commit.", ) self.checkout_branch_button = ttk.Button( button_frame, text="Checkout Selected Branch", width=button_width, command=lambda: self.checkout_branch_callback(None), state=tk.DISABLED, ) self.checkout_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5) Tooltip( self.checkout_branch_button, "Switch the working directory to the selected local branch.", ) # Promote to main button self.promote_to_main_button = ttk.Button( button_frame, text="Promote Selected to main", width=button_width, command=lambda: self.promote_to_main_callback( self.get_selected_branch() ) if callable(self.promote_to_main_callback) else None, state=tk.DISABLED, ) self.promote_to_main_button.pack(side=tk.TOP, fill=tk.X, pady=5) Tooltip( self.promote_to_main_button, "Force the selected branch into 'main' (creates backup and can push).", ) 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.refresh_branches_button, self.create_branch_button, self.checkout_branch_button, self.promote_to_main_button, self.branch_listbox, ] for widget in widgets: if widget and widget.winfo_exists(): widget.config(state=list_state) def update_branch_list(self, branches: List[str], current_branch: Optional[str]): """Populates the listbox with the local branches.""" if not self.branch_listbox.winfo_exists(): return self.branch_listbox.config(state=tk.NORMAL) self.branch_listbox.delete(0, tk.END) selected_index = -1 if branches: self.branch_listbox.config(fg="black") for i, branch in enumerate(branches): prefix = "* " if branch == current_branch else " " self.branch_listbox.insert(tk.END, f"{prefix}{branch}") if branch == current_branch: selected_index = i else: self.branch_listbox.insert(tk.END, "(No local branches found)") self.branch_listbox.config(fg="grey") if selected_index != -1: self.branch_listbox.selection_set(selected_index) self.branch_listbox.see(selected_index) self.branch_listbox.yview_moveto(0.0) def get_selected_branch(self) -> Optional[str]: """Returns the name of the currently selected local branch, or None.""" selection = self.branch_listbox.curselection() if not selection: return None item_text = self.branch_listbox.get(selection[0]) branch_name = item_text.lstrip("* ").strip() return branch_name if not branch_name.startswith("(") else None def _show_local_branches_context_menu(self, event: tk.Event) -> None: """Shows a context menu for the selected local branch.""" try: idx = self.branch_listbox.nearest(event.y) if idx < 0: return self.branch_listbox.selection_clear(0, tk.END) self.branch_listbox.selection_set(idx) self.branch_listbox.activate(idx) selected_item_text = self.branch_listbox.get(idx).strip() except tk.TclError: return menu = self.main_frame.local_branch_context_menu menu.delete(0, tk.END) is_current = selected_item_text.startswith("*") branch_name = selected_item_text.lstrip("* ").strip() current_branch = self.main_frame.current_local_branch is_valid = not branch_name.startswith("(") if is_valid: menu.add_command( label=f"Merge '{branch_name}' into '{current_branch}'...", state=tk.DISABLED if is_current or not current_branch else tk.NORMAL, command=lambda b=branch_name: self.merge_local_branch_callback(b), ) menu.add_command( label=f"Compare with current '{current_branch}'", state=tk.DISABLED if is_current or not current_branch else tk.NORMAL, command=lambda b=branch_name: self.compare_branch_with_current_callback( b ), ) menu.add_separator() menu.add_command( label=f"Delete Branch '{branch_name}'...", state=tk.DISABLED if is_current else tk.NORMAL, command=lambda b=branch_name: self.delete_local_branch_callback( b, False ), ) menu.add_command( label=f"Force Delete Branch '{branch_name}'...", state=tk.DISABLED if is_current else tk.NORMAL, command=lambda b=branch_name: self.delete_local_branch_callback( b, True ), ) # Promote to main (disabled if target is main or if it's the current branch) menu.add_separator() menu.add_command( label=f"Promote '{branch_name}' to 'main'...", state=tk.DISABLED if is_current or branch_name == 'main' else tk.NORMAL, command=lambda b=branch_name: self.promote_to_main_callback(b), ) menu.add_separator() menu.add_command(label="Cancel") menu.tk_popup(event.x_root, event.y_root)