# --- FILE: gitsync_tool/gui/main_frame.py --- import tkinter as tk from tkinter import ttk from tkinter import messagebox, simpledialog, scrolledtext import os import sys from typing import Tuple, Dict, List, Callable, Optional, Any from gitutility.logging_setup import log_handler from gitutility.gui.tooltip import Tooltip from gitutility.config.config_manager import DEFAULT_PROFILE # --- Importazioni dai moduli dei tab --- from .tabs.repo_tab import RepoTab from .tabs.submodules_tab import SubmodulesTab from .tabs.remote_tab import RemoteTab from .tabs.backup_tab import BackupTab from .tabs.commit_tab import CommitTab from .tabs.tags_tab import TagsTab from .tabs.branch_tab import BranchTab from .tabs.automation_tab import AutomationTab from .tabs.history_tab import HistoryTab class MainFrame(ttk.Frame): """ The main application frame, which acts as a container for all UI components. It assembles the profile bar, the tabbed notebook, the log area, and the status bar. It delegates tab-specific logic and UI updates to individual tab classes. """ GREEN: str = "#90EE90" RED: str = "#F08080" STATUS_YELLOW: str = "#FFFACD" STATUS_RED: str = "#FFA07A" STATUS_GREEN: str = "#98FB98" STATUS_DEFAULT_BG: Optional[str] = None def __init__(self, master: tk.Misc, **kwargs): super().__init__(master) self.master: tk.Misc = master self.initial_profile_sections = kwargs.get("profile_sections_list", []) self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5) # --- Store Callbacks --- self.load_profile_settings_callback = kwargs.get("load_profile_settings_cb") # ... (gli altri callback non sono necessari qui, vengono passati ai tab) # --- Tkinter Variables (ONLY global/shared ones) --- self.profile_var = tk.StringVar() self.status_bar_var = tk.StringVar() self.current_local_branch: Optional[str] = None # --- Context Menus (owned by the main frame) --- self.remote_branch_context_menu = tk.Menu(self.master, tearoff=0) self.local_branch_context_menu = tk.Menu(self.master, tearoff=0) self.changed_files_context_menu = tk.Menu(self.master, tearoff=0) self.submodule_context_menu = tk.Menu(self.master, tearoff=0) # --- Create Main UI Structure --- self._create_profile_frame( kwargs.get("save_profile_cb"), kwargs.get("add_profile_cb"), kwargs.get("clone_remote_repo_cb"), kwargs.get("remove_profile_cb"), ) self.notebook = ttk.Notebook(self, padding=(0, 5, 0, 0)) self.notebook.pack(pady=(5, 0), padx=0, fill="both", expand=True) # --- Instantiate and Add Tabs --- self.repo_tab = RepoTab(self.notebook, **kwargs) self.notebook.add(self.repo_tab, text=" Repository / Bundle ") self.submodules_tab = SubmodulesTab(self.notebook, **kwargs) self.notebook.add(self.submodules_tab, text=" Submodules ") self.remote_tab = RemoteTab( self.notebook, **kwargs ) # <-- Questo probabilmente era giĆ  giusto self.notebook.add(self.remote_tab, text=" Remote Repository ") self.backup_tab = BackupTab(self.notebook, **kwargs) self.notebook.add(self.backup_tab, text=" Backup Settings ") self.commit_tab = CommitTab(self.notebook, **kwargs) self.notebook.add(self.commit_tab, text=" Commit / Changes ") self.tags_tab = TagsTab(self.notebook, **kwargs) self.notebook.add(self.tags_tab, text=" Tags ") self.branch_tab = BranchTab(self.notebook, **kwargs) self.notebook.add(self.branch_tab, text=" Branches (Local Ops) ") self.automation_tab = AutomationTab(self.notebook, **kwargs) self.notebook.add(self.automation_tab, text=" Automation ") self.history_tab = HistoryTab(self.notebook, **kwargs) self.notebook.add(self.history_tab, text=" History ") # --- Log and Status Bar --- log_frame_container = ttk.Frame(self) log_frame_container.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=(5, 0) ) self._create_log_area(log_frame_container) self.status_bar = ttk.Label( self, textvariable=self.status_bar_var, relief=tk.SUNKEN, anchor=tk.W, padding=(5, 2), ) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X, pady=(2, 0), padx=0) self._status_reset_timer: Optional[str] = None self._initialize_profile_selection() def _create_profile_frame(self, save_cb, add_cb, clone_cb, remove_cb): profile_outer_frame = ttk.Frame(self, padding=(0, 0, 0, 5)) profile_outer_frame.pack(fill="x", side=tk.TOP) frame = ttk.LabelFrame( profile_outer_frame, text="Profile Management", padding=(10, 5) ) frame.pack(fill="x") frame.columnconfigure(1, weight=1) ttk.Label(frame, text="Select Profile:").grid( row=0, column=0, sticky=tk.W, padx=5, pady=5 ) self.profile_dropdown = ttk.Combobox( frame, textvariable=self.profile_var, state="readonly", width=35, values=self.initial_profile_sections, ) self.profile_dropdown.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=5) # MODIFICA: Bind all'handler intermediario self.profile_dropdown.bind("<>", self._on_profile_change) self.profile_var.trace_add("write", self._on_profile_change) button_subframe = ttk.Frame(frame) button_subframe.grid(row=0, column=2, sticky=tk.E, padx=(10, 0)) self.save_settings_button = ttk.Button( button_subframe, text="Save Profile", command=save_cb ) self.save_settings_button.pack(side=tk.LEFT, padx=(0, 2), pady=5) self.add_profile_button = ttk.Button( button_subframe, text="Add New", width=8, command=add_cb ) self.add_profile_button.pack(side=tk.LEFT, padx=(2, 2), pady=5) self.clone_profile_button = ttk.Button( button_subframe, text="Clone from Remote", width=18, command=clone_cb ) self.clone_profile_button.pack(side=tk.LEFT, padx=5, pady=5) self.remove_profile_button = ttk.Button( button_subframe, text="Remove", width=8, command=remove_cb ) self.remove_profile_button.pack(side=tk.LEFT, padx=(2, 0), pady=5) def _on_profile_change(self, *args): """Intermediate handler for profile selection changes.""" if callable(self.load_profile_settings_callback): self.load_profile_settings_callback(self.profile_var.get()) def _create_log_area(self, parent_frame): log_frame = ttk.LabelFrame( parent_frame, text="Application Log", padding=(10, 5) ) log_frame.pack(fill=tk.BOTH, expand=True) log_frame.rowconfigure(0, weight=1) log_frame.columnconfigure(0, weight=1) self.log_text = scrolledtext.ScrolledText( log_frame, height=8, width=100, font=("Consolas", 9), wrap=tk.WORD, state=tk.DISABLED, padx=5, pady=5, borderwidth=1, relief=tk.SUNKEN, ) self.log_text.grid(row=0, column=0, sticky="nsew") self.log_text.tag_config("INFO", foreground="black") self.log_text.tag_config("DEBUG", foreground="grey") self.log_text.tag_config("WARNING", foreground="orange") self.log_text.tag_config("ERROR", foreground="red") self.log_text.tag_config( "CRITICAL", foreground="red", font=("Consolas", 9, "bold") ) def _initialize_profile_selection(self): current_profiles = self.profile_dropdown.cget("values") if not isinstance(current_profiles, (list, tuple)): current_profiles = [] target_profile = "" if DEFAULT_PROFILE in current_profiles: target_profile = DEFAULT_PROFILE elif current_profiles: target_profile = current_profiles[0] if target_profile: self.profile_var.set(target_profile) else: self.profile_var.set("") self.update_status_bar("No profiles found. Please add or clone a profile.") # --- Delegated GUI Update Methods --- def update_profile_dropdown(self, sections: List[str]): if hasattr(self, "profile_dropdown") and self.profile_dropdown.winfo_exists(): curr = self.profile_var.get() self.profile_dropdown["values"] = sections if sections: if curr in sections: self.profile_var.set(curr) else: self.profile_var.set(sections[0] if sections else "") else: self.profile_var.set("") def update_tag_list(self, tags_data: List[Tuple[str, str]]): if hasattr(self, "tags_tab"): self.tags_tab.update_tag_list(tags_data) def update_branch_list(self, branches: List[str], current_branch: Optional[str]): self.current_local_branch = current_branch if hasattr(self, "branch_tab"): self.branch_tab.update_branch_list(branches, current_branch) if hasattr(self, "remote_tab"): self.remote_tab.update_local_branch_list(branches, current_branch) def update_history_display(self, log_lines: List[str]): if hasattr(self, "history_tab"): self.history_tab.update_history_display(log_lines) def update_history_branch_filter(self, branches_tags: List[str]): if hasattr(self, "history_tab"): self.history_tab.update_history_branch_filter(branches_tags) def update_changed_files_list(self, files_status_list: List[str]): if hasattr(self, "commit_tab"): self.commit_tab.update_changed_files_list(files_status_list) def update_remote_branches_list(self, remote_branch_list: List[str]): if hasattr(self, "remote_tab"): self.remote_tab.update_remote_branches_list(remote_branch_list) def update_submodules_list(self, submodules_data: Optional[List[Dict[str, str]]]): if hasattr(self, "submodules_tab"): self.submodules_tab.update_submodules_list(submodules_data) # --- Delegated Getter/Action Methods --- def get_commit_message(self) -> str: return ( self.commit_tab.get_commit_message() if hasattr(self, "commit_tab") else "" ) def clear_commit_message(self): if hasattr(self, "commit_tab"): self.commit_tab.clear_commit_message() def get_selected_tag(self) -> Optional[str]: return self.tags_tab.get_selected_tag() if hasattr(self, "tags_tab") else None def get_selected_branch(self) -> Optional[str]: active_tab_index = self.notebook.index(self.notebook.select()) # Safely determine the index of the branch_tab inside the notebook. try: branch_tab_index = self.notebook.index(self.branch_tab) except Exception: # Fallback: find by comparing widget objects for each tab id branch_tab_index = None for i, tab_id in enumerate(self.notebook.tabs()): try: w = self.notebook.nametowidget(tab_id) except Exception: w = None if w is self.branch_tab: branch_tab_index = i break if branch_tab_index is not None and active_tab_index == branch_tab_index: return ( self.branch_tab.get_selected_branch() if hasattr(self, "branch_tab") else None ) elif active_tab_index == self.notebook.tabs().index(self.remote_tab): return ( self.remote_tab.get_selected_local_branch() if hasattr(self, "remote_tab") else None ) return None # --- Global State Management --- def set_action_widgets_state(self, state: str): for btn_name in ["save_settings_button", "remove_profile_button"]: widget = getattr(self, btn_name, None) if widget and widget.winfo_exists(): widget.config(state=state) for tab_name in [ "repo_tab", "submodules_tab", "remote_tab", "backup_tab", "commit_tab", "tags_tab", "branch_tab", "automation_tab", "history_tab", ]: tab_instance = getattr(self, tab_name, None) if tab_instance and hasattr(tab_instance, "set_action_widgets_state"): tab_instance.set_action_widgets_state(state) def update_status_bar( self, message: str, bg_color: Optional[str] = None, duration_ms: Optional[int] = None, ): if ( hasattr(self, "status_bar_var") and hasattr(self, "status_bar") and self.status_bar.winfo_exists() ): if self._status_reset_timer: self.master.after_cancel(self._status_reset_timer) self._status_reset_timer = None actual_bg = bg_color if bg_color else self.STATUS_DEFAULT_BG self.status_bar_var.set(message) self.status_bar.config(background=actual_bg) if bg_color and duration_ms and duration_ms > 0: self._status_reset_timer = self.master.after( duration_ms, self.reset_status_bar_color ) def reset_status_bar_color(self): self._status_reset_timer = None if hasattr(self, "status_bar") and self.status_bar.winfo_exists(): if MainFrame.STATUS_DEFAULT_BG is None: MainFrame.STATUS_DEFAULT_BG = self.status_bar.cget("background") self.status_bar.config(background=MainFrame.STATUS_DEFAULT_BG) # --- Passthrough for Dialogs --- def ask_new_profile_name(self) -> Optional[str]: return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master) def ask_branch_name(self, title: str, prompt: str, default: Optional[str] = None) -> Optional[str]: """Ask the user for a branch name with an optional default value.""" if default: return simpledialog.askstring(title, prompt, initialvalue=default, parent=self.master) return simpledialog.askstring(title, prompt, parent=self.master) def ask_new_submodule_details(self) -> Optional[Tuple[str, str]]: url = simpledialog.askstring("Add Submodule", "Repository URL:", parent=self) if not url: return None path = simpledialog.askstring("Add Submodule", "Local Path:", parent=self) if not path: return None return url.strip(), path.strip() def show_error(self, title: str, message: str): messagebox.showerror(title, message, parent=self.master) def show_info(self, title: str, message: str): messagebox.showinfo(title, message, parent=self.master) def show_warning(self, title: str, message: str): messagebox.showwarning(title, message, parent=self.master) def ask_yes_no(self, title: str, message: str) -> bool: return messagebox.askyesno(title, message, parent=self.master)