# --- FILE: gitsync_tool/gui/tabs/submodules_tab.py --- import tkinter as tk from tkinter import ttk from typing import Callable, List, Dict, Optional, Any from gitutility.gui.tooltip import Tooltip from gitutility.logging_setup import log_handler class SubmodulesTab(ttk.Frame): """ The 'Submodules' tab in the main application notebook. This tab provides an interface for viewing, adding, updating, and removing Git submodules within the current repository. """ def __init__(self, master: tk.Misc, **kwargs): """ Initializes the Submodules 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 by getting them from kwargs self.refresh_submodules_callback = kwargs.get('refresh_submodules_cb') self.add_submodule_callback = kwargs.get('add_submodule_cb') self.init_missing_submodules_callback = kwargs.get('init_missing_submodules_cb') self.sync_all_submodules_callback = kwargs.get('sync_all_submodules_cb') self.remove_submodule_callback = kwargs.get('remove_submodule_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.""" # --- Actions Frame --- actions_frame = ttk.LabelFrame(self, text="Submodule Actions", padding=(10, 5)) actions_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) self.refresh_submodules_button = ttk.Button( actions_frame, text="Refresh List", command=self.refresh_submodules_callback, state=tk.DISABLED ) self.refresh_submodules_button.pack(side=tk.LEFT, padx=5, pady=5) Tooltip(self.refresh_submodules_button, "Reload the status of all submodules.") self.add_submodule_button = ttk.Button( actions_frame, text="Add Submodule...", command=self.add_submodule_callback, state=tk.DISABLED ) self.add_submodule_button.pack(side=tk.LEFT, padx=5, pady=5) Tooltip(self.add_submodule_button, "Add a new submodule to this repository.") self.init_submodules_button = ttk.Button( actions_frame, text="Initialize Missing", command=self.init_missing_submodules_callback, state=tk.DISABLED ) self.init_submodules_button.pack(side=tk.LEFT, padx=5, pady=5) Tooltip(self.init_submodules_button, "Clone any submodules that are registered but not yet downloaded.") self.sync_submodules_button = ttk.Button( actions_frame, text="Sync/Update All", command=self.sync_all_submodules_callback, state=tk.DISABLED ) self.sync_submodules_button.pack(side=tk.LEFT, padx=5, pady=5) Tooltip(self.sync_submodules_button, "Update all initialized submodules to the latest commit on their tracked branch and commit the change.") # --- List Frame --- list_frame = ttk.LabelFrame(self, text="Repository Submodules", padding=(10, 5)) list_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5) list_frame.rowconfigure(0, weight=1) list_frame.columnconfigure(0, weight=1) columns = ("path", "commit", "status") self.submodules_tree = ttk.Treeview( list_frame, columns=columns, show="headings", selectmode="browse" ) self.submodules_tree.heading("path", text="Path", anchor="w") self.submodules_tree.heading("commit", text="Current Commit", anchor="w") self.submodules_tree.heading("status", text="Status", anchor="w") self.submodules_tree.column("path", width=300, stretch=tk.YES, anchor="w") self.submodules_tree.column("commit", width=120, stretch=tk.NO, anchor="w") self.submodules_tree.column("status", width=220, stretch=tk.NO, anchor="w") scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.submodules_tree.yview) self.submodules_tree.configure(yscrollcommand=scrollbar.set) self.submodules_tree.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") self.submodules_tree.bind("", self._show_submodule_context_menu) Tooltip(self.submodules_tree, "List of registered submodules. Right-click on an item for more options.") # Tag configuration for status colors self.submodules_tree.tag_configure("status_ok", foreground="green") self.submodules_tree.tag_configure("status_warning", foreground="orange") self.submodules_tree.tag_configure("status_error", foreground="red") def update_submodules_list(self, submodules_data: Optional[List[Dict[str, str]]]) -> None: """Populates the submodules Treeview with the latest status.""" func_name = "update_submodules_list (GUI)" # Clear existing items for item in self.submodules_tree.get_children(): self.submodules_tree.delete(item) if submodules_data is None: # Error case self.submodules_tree.insert("", "end", values=("(Error)", "", "Could not retrieve submodule status"), tags=("status_error",)) return if not submodules_data: self.submodules_tree.insert("", "end", values=("(No submodules found)", "", "")) return for i, sub in enumerate(submodules_data): path = sub.get("path", "N/A") commit = sub.get("commit", "N/A") status_char = sub.get("status_char", "?") status_text = "OK (Initialized & Synced)" tag = "status_ok" if status_char == "-": status_text = "Not Initialized (Needs Clone)" tag = "status_warning" elif status_char == "+": status_text = "Commit Mismatch (Needs Sync/Update)" tag = "status_warning" elif status_char == "U": status_text = "Conflict (Needs Manual Resolution)" tag = "status_error" self.submodules_tree.insert( "", "end", iid=i, values=(path, commit[:10], status_text), tags=(tag,) ) def get_selected_submodule_path(self) -> Optional[str]: """Returns the path of the selected submodule in the treeview.""" selection = self.submodules_tree.selection() if not selection: return None item_data = self.submodules_tree.item(selection[0]) values = item_data.get("values") if values and len(values) > 0: return values[0] return None def _show_submodule_context_menu(self, event: tk.Event) -> None: """Shows a context menu for the selected submodule.""" # Select the item under the cursor iid = self.submodules_tree.identify_row(event.y) if iid: self.submodules_tree.selection_set(iid) else: return path = self.get_selected_submodule_path() if not path: return menu = self.main_frame.submodule_context_menu if not menu: return menu.delete(0, tk.END) menu.add_command( label=f"Remove Submodule '{path}'...", command=lambda p=path: self.remove_submodule_callback(p) if callable(self.remove_submodule_callback) else None ) menu.add_separator() menu.add_command(label="Cancel") try: menu.tk_popup(event.x_root, event.y_root) finally: menu.grab_release() def set_action_widgets_state(self, state: str) -> None: """Sets the state of all action widgets in this tab.""" widgets_to_toggle = [ self.refresh_submodules_button, self.add_submodule_button, self.init_submodules_button, self.sync_submodules_button, ] for widget in widgets_to_toggle: if widget and widget.winfo_exists(): widget.config(state=state)