SXXXXXXX_GitUtility/gitutility/gui/main_frame.py

391 lines
15 KiB
Python

# --- 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("<<ComboboxSelected>>", 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)