2203 lines
93 KiB
Python
2203 lines
93 KiB
Python
# --- FILE: gitsync_tool/gui/main_frame.py ---
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from tkinter import (
|
|
messagebox,
|
|
simpledialog,
|
|
scrolledtext,
|
|
filedialog,
|
|
) # Import moduli tk necessari
|
|
import os
|
|
import re
|
|
import sys
|
|
from typing import Tuple, Dict, List, Callable, Optional, Any
|
|
|
|
|
|
# Importa moduli/classi dal pacchetto gitsync_tool
|
|
from gitutility.logging_setup import log_handler # Usa il log handler
|
|
from gitutility.gui.tooltip import Tooltip # Importa Tooltip dal suo file
|
|
|
|
# Importa costanti dal modulo config
|
|
try:
|
|
from gitutility.config.config_manager import (
|
|
DEFAULT_BACKUP_DIR,
|
|
DEFAULT_REMOTE_NAME,
|
|
DEFAULT_PROFILE,
|
|
)
|
|
except ImportError:
|
|
DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "backup_fallback")
|
|
DEFAULT_REMOTE_NAME = "origin"
|
|
DEFAULT_PROFILE = "default"
|
|
print(
|
|
f"WARNING: main_frame.py could not import constants. Using fallbacks.",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
# Non importiamo più classi di Dialog, Editor, Viewer qui perché sono in file separati
|
|
# Non importiamo moduli di logica (ActionHandler, GitCommands, etc.)
|
|
|
|
|
|
class MainFrame(ttk.Frame):
|
|
"""
|
|
The main application frame, containing the profile management bar,
|
|
the tabbed notebook interface, the log area, and the status bar.
|
|
Connects GUI elements to controller callbacks.
|
|
"""
|
|
|
|
# Definizioni costanti colori
|
|
GREEN: str = "#90EE90" # LightGreen
|
|
RED: str = "#F08080" # LightCoral (un rosso più tenue)
|
|
STATUS_YELLOW: str = "#FFFACD" # Lemon Chiffon
|
|
STATUS_RED: str = "#FFA07A" # Light Salmon
|
|
STATUS_GREEN: str = "#98FB98" # Pale Green
|
|
STATUS_DEFAULT_BG: Optional[str] = None # Verrà impostato in __init__
|
|
|
|
# ---<<< MODIFICA: Firma __init__ aggiornata >>>---
|
|
# Rimuovi type hints che si riferiscono a classi non più importate qui
|
|
# Assicurati che tutti i parametri callback siano definiti e ricevuti
|
|
def __init__(
|
|
self,
|
|
master: tk.Misc,
|
|
# Callbacks
|
|
load_profile_settings_cb: Callable[[str], None],
|
|
browse_folder_cb: Callable[[tk.Entry], None],
|
|
update_svn_status_cb: Callable[[str], None],
|
|
prepare_svn_for_git_cb: Callable[[], None],
|
|
create_git_bundle_cb: Callable[[], None],
|
|
fetch_from_git_bundle_cb: Callable[[], None],
|
|
add_profile_cb: Callable[[], None],
|
|
remove_profile_cb: Callable[[], None],
|
|
manual_backup_cb: Callable[[], None],
|
|
open_gitignore_editor_cb: Callable[[], None],
|
|
save_profile_cb: Callable[[], bool],
|
|
commit_changes_cb: Callable[[], None],
|
|
refresh_tags_cb: Callable[[], None],
|
|
create_tag_cb: Callable[[], None],
|
|
checkout_tag_cb: Callable[[], None],
|
|
refresh_history_cb: Callable[[], None],
|
|
refresh_branches_cb: Callable[[], None], # Callback unico per refresh locali
|
|
checkout_branch_cb: Callable[
|
|
[Optional[str], Optional[str]], None
|
|
], # Modificato per accettare override
|
|
create_branch_cb: Callable[[], None],
|
|
refresh_changed_files_cb: Callable[[], None],
|
|
open_diff_viewer_cb: Callable[[str], None],
|
|
add_selected_file_cb: Callable[[str], None],
|
|
apply_remote_config_cb: Callable[[], None],
|
|
check_connection_auth_cb: Callable[[], None],
|
|
fetch_remote_cb: Callable[[], None],
|
|
pull_remote_cb: Callable[[], None],
|
|
push_remote_cb: Callable[[], None],
|
|
push_tags_remote_cb: Callable[[], None],
|
|
refresh_remote_status_cb: Callable[[], None],
|
|
clone_remote_repo_cb: Callable[[], None],
|
|
refresh_remote_branches_cb: Callable[[], None],
|
|
checkout_remote_branch_cb: Callable[[str, str], None],
|
|
delete_local_branch_cb: Callable[[str, bool], None],
|
|
merge_local_branch_cb: Callable[[str], None],
|
|
compare_branch_with_current_cb: Callable[[str], None],
|
|
view_commit_details_cb: Callable[[str], None],
|
|
# Altre dipendenze
|
|
config_manager_instance: Any, # Evita import ConfigManager qui
|
|
profile_sections_list: List[str],
|
|
):
|
|
# ---<<< FINE MODIFICA >>>---
|
|
"""Initializes the MainFrame."""
|
|
super().__init__(master)
|
|
self.master: tk.Misc = master
|
|
|
|
# Store callbacks
|
|
self.load_profile_settings_callback = load_profile_settings_cb
|
|
self.browse_folder_callback = browse_folder_cb
|
|
self.update_svn_status_callback = update_svn_status_cb
|
|
self.prepare_svn_for_git_callback = prepare_svn_for_git_cb
|
|
self.create_git_bundle_callback = create_git_bundle_cb
|
|
self.fetch_from_git_bundle_callback = fetch_from_git_bundle_cb
|
|
self.manual_backup_callback = manual_backup_cb
|
|
self.add_profile_callback = add_profile_cb
|
|
self.remove_profile_callback = remove_profile_cb
|
|
self.save_profile_callback = save_profile_cb
|
|
self.open_gitignore_editor_callback = open_gitignore_editor_cb
|
|
self.commit_changes_callback = commit_changes_cb
|
|
self.refresh_tags_callback = refresh_tags_cb
|
|
self.create_tag_callback = create_tag_cb
|
|
self.checkout_tag_callback = checkout_tag_cb
|
|
self.refresh_history_callback = refresh_history_cb
|
|
self.refresh_branches_callback = refresh_branches_cb
|
|
self.checkout_branch_callback = checkout_branch_cb
|
|
self.create_branch_callback = create_branch_cb
|
|
self.refresh_changed_files_callback = refresh_changed_files_cb
|
|
self.open_diff_viewer_callback = open_diff_viewer_cb
|
|
self.add_selected_file_callback = add_selected_file_cb
|
|
self.apply_remote_config_callback = apply_remote_config_cb
|
|
self.check_connection_auth_callback = check_connection_auth_cb
|
|
self.fetch_remote_callback = fetch_remote_cb
|
|
self.pull_remote_callback = pull_remote_cb
|
|
self.push_remote_callback = push_remote_cb
|
|
self.push_tags_remote_callback = push_tags_remote_cb
|
|
self.refresh_remote_status_callback = refresh_remote_status_cb
|
|
self.clone_remote_repo_callback = clone_remote_repo_cb
|
|
self.refresh_remote_branches_callback = refresh_remote_branches_cb
|
|
self.checkout_remote_branch_callback = checkout_remote_branch_cb
|
|
self.delete_local_branch_callback = delete_local_branch_cb
|
|
self.merge_local_branch_callback = merge_local_branch_cb
|
|
self.compare_branch_with_current_callback = compare_branch_with_current_cb
|
|
self.view_commit_details_callback = view_commit_details_cb
|
|
|
|
# Store references needed internally
|
|
self.config_manager = (
|
|
config_manager_instance # Necessario per accedere a metodi? Forse no.
|
|
)
|
|
self.initial_profile_sections = profile_sections_list
|
|
|
|
# Configurazione stile (richiede ttk)
|
|
self.style = ttk.Style()
|
|
# Se hai configurazioni di stile, mettile qui o in un metodo separato
|
|
# Esempio: self.style.configure('Red.TLabel', foreground='red')
|
|
|
|
# Configura il layout del frame principale
|
|
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
# --- Tkinter Variables ---
|
|
self.profile_var = tk.StringVar()
|
|
self.autobackup_var = tk.BooleanVar()
|
|
self.backup_dir_var = tk.StringVar()
|
|
self.backup_exclude_extensions_var = tk.StringVar()
|
|
self.backup_exclude_dirs_var = tk.StringVar()
|
|
self.autocommit_var = tk.BooleanVar()
|
|
self.status_bar_var = tk.StringVar()
|
|
self.remote_url_var = tk.StringVar()
|
|
self.remote_name_var = tk.StringVar()
|
|
self.remote_auth_status_var = tk.StringVar(value="Status: Unknown")
|
|
self.remote_ahead_behind_var = tk.StringVar(value="Sync Status: Unknown")
|
|
# Variabile per il filtro history (già presente nel metodo _create)
|
|
self.history_branch_filter_var = tk.StringVar()
|
|
|
|
# --- Creazione Menu Contestuali ---
|
|
# Associali al master (root window) o a self (MainFrame)
|
|
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)
|
|
|
|
# --- Create UI Elements ---
|
|
self._create_profile_frame()
|
|
self.notebook = ttk.Notebook(self, padding=(0, 5, 0, 0))
|
|
self.notebook.pack(pady=(5, 0), padx=0, fill="both", expand=True)
|
|
|
|
# Creazione delle tab (l'ordine di chiamata influenza l'esistenza dei widget)
|
|
self.repo_tab_frame = self._create_repo_tab()
|
|
self.backup_tab_frame = self._create_backup_tab()
|
|
self.commit_tab_frame = self._create_commit_tab()
|
|
self.tags_tab_frame = self._create_tags_tab()
|
|
self.branch_tab_frame = (
|
|
self._create_branch_tab()
|
|
) # Tab originale per branch locali
|
|
self.remote_tab_frame = self._create_remote_tab() # Nuova tab remota
|
|
self.history_tab_frame = (
|
|
self._create_history_tab_treeview()
|
|
) # Usa la versione TreeView
|
|
|
|
# Aggiunta delle tab al Notebook (ordine di visualizzazione)
|
|
self.notebook.add(self.repo_tab_frame, text=" Repository / Bundle ")
|
|
self.notebook.add(self.remote_tab_frame, text=" Remote Repository ")
|
|
self.notebook.add(self.backup_tab_frame, text=" Backup Settings ")
|
|
self.notebook.add(self.commit_tab_frame, text=" Commit / Changes ")
|
|
self.notebook.add(self.tags_tab_frame, text=" Tags ")
|
|
self.notebook.add(self.branch_tab_frame, text=" Branches (Local Ops) ")
|
|
self.notebook.add(self.history_tab_frame, text=" History ")
|
|
|
|
# Creazione area log e 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) # Chiama metodo di creazione log
|
|
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)
|
|
|
|
# Impostazione colore default status bar
|
|
try:
|
|
s = ttk.Style()
|
|
# Salva il colore di background di default per ripristinarlo
|
|
MainFrame.STATUS_DEFAULT_BG = s.lookup("TLabel", "background")
|
|
except tk.TclError: # Fallback se lo stile non è pronto o dà errore
|
|
MainFrame.STATUS_DEFAULT_BG = self.status_bar.cget("background")
|
|
|
|
self.status_bar_var.set("Initializing...") # Messaggio iniziale
|
|
self._status_reset_timer: Optional[str] = (
|
|
None # Timer per reset colore status bar
|
|
)
|
|
self.current_local_branch: Optional[str] = None # Memorizza branch corrente
|
|
|
|
# Initial State Setup
|
|
self._initialize_profile_selection() # Imposta selezione iniziale profilo
|
|
self.toggle_backup_dir() # Imposta stato iniziale widget backup dir
|
|
|
|
# --- Frame Creation Methods ---
|
|
# (Questi metodi dovrebbero ora essere definiti qui)
|
|
|
|
def _create_profile_frame(self):
|
|
# ... (Codice da gui.py originale, assicurati che Tooltip sia usabile) ...
|
|
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)
|
|
self.profile_dropdown.bind(
|
|
"<<ComboboxSelected>>",
|
|
lambda e: self.load_profile_settings_callback(self.profile_var.get()),
|
|
)
|
|
self.profile_var.trace_add(
|
|
"write",
|
|
lambda n, i, m: self.load_profile_settings_callback(self.profile_var.get()),
|
|
)
|
|
self.create_tooltip(
|
|
self.profile_dropdown,
|
|
"Select the configuration profile for the repository.",
|
|
)
|
|
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=self.save_profile_callback
|
|
)
|
|
self.save_settings_button.pack(side=tk.LEFT, padx=(0, 2), pady=5)
|
|
self.create_tooltip(
|
|
self.save_settings_button,
|
|
"Save the current settings to the selected profile.",
|
|
)
|
|
self.add_profile_button = ttk.Button(
|
|
button_subframe, text="Add New", width=8, command=self.add_profile_callback
|
|
)
|
|
self.add_profile_button.pack(side=tk.LEFT, padx=(2, 2), pady=5)
|
|
self.create_tooltip(self.add_profile_button, "Add a new configuration profile.")
|
|
self.clone_profile_button = ttk.Button(
|
|
button_subframe,
|
|
text="Clone from Remote",
|
|
width=18,
|
|
command=self.clone_remote_repo_callback,
|
|
)
|
|
self.clone_profile_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.clone_profile_button,
|
|
"Clone a remote repository into a new local directory and create a profile for it.",
|
|
)
|
|
self.remove_profile_button = ttk.Button(
|
|
button_subframe,
|
|
text="Remove",
|
|
width=8,
|
|
command=self.remove_profile_callback,
|
|
)
|
|
self.remove_profile_button.pack(side=tk.LEFT, padx=(2, 0), pady=5)
|
|
self.create_tooltip(
|
|
self.remove_profile_button,
|
|
"Remove the selected profile (cannot remove default).",
|
|
)
|
|
|
|
def _create_repo_tab(self):
|
|
# ... (Codice da gui.py originale, usa self.browse_folder_callback, etc.) ...
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.columnconfigure(1, weight=1)
|
|
paths_frame = ttk.LabelFrame(
|
|
frame, text="Paths & Bundle Names", padding=(10, 5)
|
|
)
|
|
paths_frame.pack(pady=5, fill="x")
|
|
paths_frame.columnconfigure(1, weight=1)
|
|
cl, ce, cb, ci = 0, 1, 2, 3
|
|
ttk.Label(paths_frame, text="Working Directory Path:").grid(
|
|
row=0, column=cl, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.svn_path_entry = ttk.Entry(paths_frame, width=60)
|
|
self.svn_path_entry.grid(row=0, column=ce, sticky=tk.EW, padx=5, pady=3)
|
|
self.svn_path_entry.bind(
|
|
"<FocusOut>",
|
|
lambda e: self.update_svn_status_callback(self.svn_path_entry.get()),
|
|
)
|
|
self.svn_path_entry.bind(
|
|
"<Return>",
|
|
lambda e: self.update_svn_status_callback(self.svn_path_entry.get()),
|
|
)
|
|
self.create_tooltip(
|
|
self.svn_path_entry, "Path to the local Git repository working directory."
|
|
)
|
|
self.svn_path_browse_button = ttk.Button(
|
|
paths_frame,
|
|
text="Browse...",
|
|
width=9,
|
|
command=lambda: self.browse_folder_callback(self.svn_path_entry),
|
|
)
|
|
self.svn_path_browse_button.grid(
|
|
row=0, column=cb, sticky=tk.W, padx=(0, 5), pady=3
|
|
)
|
|
self.svn_status_indicator = tk.Label(
|
|
paths_frame,
|
|
text="",
|
|
width=2,
|
|
height=1,
|
|
relief=tk.SUNKEN,
|
|
background=self.RED,
|
|
anchor=tk.CENTER,
|
|
)
|
|
self.svn_status_indicator.grid(
|
|
row=0, column=ci, sticky=tk.E, padx=(0, 5), pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.svn_status_indicator,
|
|
"Indicates if the path points to a valid Git repository (Green=Yes, Red=No).",
|
|
)
|
|
ttk.Label(paths_frame, text="Bundle Target Directory:").grid(
|
|
row=1, column=cl, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.usb_path_entry = ttk.Entry(paths_frame, width=60)
|
|
self.usb_path_entry.grid(row=1, column=ce, sticky=tk.EW, padx=5, pady=3)
|
|
self.create_tooltip(
|
|
self.usb_path_entry,
|
|
"Directory where bundle files will be created or read from (e.g., USB drive).",
|
|
)
|
|
self.usb_path_browse_button = ttk.Button(
|
|
paths_frame,
|
|
text="Browse...",
|
|
width=9,
|
|
command=lambda: self.browse_folder_callback(self.usb_path_entry),
|
|
)
|
|
self.usb_path_browse_button.grid(
|
|
row=1, column=cb, sticky=tk.W, padx=(0, 5), pady=3
|
|
)
|
|
ttk.Label(paths_frame, text="Create Bundle Filename:").grid(
|
|
row=2, column=cl, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.bundle_name_entry = ttk.Entry(paths_frame, width=60)
|
|
self.bundle_name_entry.grid(
|
|
row=2, column=ce, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.bundle_name_entry,
|
|
"Filename to use when creating a new bundle file (e.g., my_project.bundle).",
|
|
)
|
|
ttk.Label(paths_frame, text="Fetch Bundle Filename:").grid(
|
|
row=3, column=cl, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.bundle_updated_name_entry = ttk.Entry(paths_frame, width=60)
|
|
self.bundle_updated_name_entry.grid(
|
|
row=3, column=ce, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.bundle_updated_name_entry,
|
|
"Filename of the bundle file to fetch updates from (e.g., update.bundle).",
|
|
)
|
|
actions_frame = ttk.LabelFrame(
|
|
frame, text="Repository Actions", padding=(10, 5)
|
|
)
|
|
actions_frame.pack(pady=10, fill="x")
|
|
self.prepare_svn_button = ttk.Button(
|
|
actions_frame,
|
|
text="Prepare Repository",
|
|
command=self.prepare_svn_for_git_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.prepare_svn_button.pack(side=tk.LEFT, padx=(0, 5), pady=5)
|
|
self.create_tooltip(
|
|
self.prepare_svn_button,
|
|
"Initialize Git in the selected directory and configure .gitignore (if not already a Git repo).",
|
|
)
|
|
self.create_bundle_button = ttk.Button(
|
|
actions_frame,
|
|
text="Create Bundle",
|
|
command=self.create_git_bundle_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.create_bundle_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.create_bundle_button,
|
|
"Create a Git bundle file containing the entire repository history and refs.",
|
|
)
|
|
self.fetch_bundle_button = ttk.Button(
|
|
actions_frame,
|
|
text="Fetch from Bundle",
|
|
command=self.fetch_from_git_bundle_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.fetch_bundle_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.fetch_bundle_button,
|
|
"Fetch updates from a bundle file and merge them, or clone if the directory is empty.",
|
|
)
|
|
self.edit_gitignore_button = ttk.Button(
|
|
actions_frame,
|
|
text="Edit .gitignore",
|
|
width=12,
|
|
command=self.open_gitignore_editor_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.edit_gitignore_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.edit_gitignore_button, "Open an editor for the .gitignore file."
|
|
)
|
|
return frame
|
|
|
|
def _create_backup_tab(self):
|
|
# ... (Codice da gui.py originale) ...
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.columnconfigure(1, weight=1)
|
|
config_frame = ttk.LabelFrame(
|
|
frame, text="Backup Configuration", padding=(10, 5)
|
|
)
|
|
config_frame.pack(pady=5, fill="x", expand=False)
|
|
config_frame.columnconfigure(1, weight=1)
|
|
cl, ce, cb = 0, 1, 2
|
|
self.autobackup_checkbox = ttk.Checkbutton(
|
|
config_frame,
|
|
text="Enable Auto Backup before Actions (Create/Fetch Bundle)",
|
|
variable=self.autobackup_var,
|
|
command=self.toggle_backup_dir,
|
|
)
|
|
self.autobackup_checkbox.grid(
|
|
row=0, column=0, columnspan=3, sticky=tk.W, padx=5, pady=(5, 5)
|
|
)
|
|
self.create_tooltip(
|
|
self.autobackup_checkbox,
|
|
"Automatically create a ZIP backup of the working directory before creating or fetching from a bundle.",
|
|
)
|
|
backup_dir_label = ttk.Label(config_frame, text="Backup Directory:")
|
|
backup_dir_label.grid(row=1, column=cl, sticky=tk.W, padx=5, pady=3)
|
|
self.backup_dir_entry = ttk.Entry(
|
|
config_frame, textvariable=self.backup_dir_var, width=60, state=tk.DISABLED
|
|
)
|
|
self.backup_dir_entry.grid(row=1, column=ce, sticky=tk.EW, padx=5, pady=3)
|
|
self.create_tooltip(
|
|
self.backup_dir_entry,
|
|
"Directory where automatic and manual backup ZIP files will be stored.",
|
|
)
|
|
self.backup_dir_button = ttk.Button(
|
|
config_frame,
|
|
text="Browse...",
|
|
width=9,
|
|
command=self.browse_backup_dir,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.backup_dir_button.grid(row=1, column=cb, sticky=tk.W, padx=(0, 5), pady=3)
|
|
exclude_ext_label = ttk.Label(config_frame, text="Exclude File Exts:")
|
|
exclude_ext_label.grid(row=2, column=cl, sticky=tk.W, padx=5, pady=3)
|
|
self.backup_exclude_entry = ttk.Entry(
|
|
config_frame, textvariable=self.backup_exclude_extensions_var, width=60
|
|
)
|
|
self.backup_exclude_entry.grid(
|
|
row=2, column=ce, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.backup_exclude_entry,
|
|
"Comma-separated list of file extensions to exclude from backups (e.g., .log, .tmp, .bak).",
|
|
)
|
|
exclude_dir_label = ttk.Label(config_frame, text="Exclude Dirs (Name):")
|
|
exclude_dir_label.grid(row=3, column=cl, sticky=tk.W, padx=5, pady=3)
|
|
self.backup_exclude_dirs_entry = ttk.Entry(
|
|
config_frame, textvariable=self.backup_exclude_dirs_var, width=60
|
|
)
|
|
self.backup_exclude_dirs_entry.grid(
|
|
row=3, column=ce, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.backup_exclude_dirs_entry,
|
|
"Comma-separated list of directory base names to exclude (e.g., __pycache__, build, node_modules). .git and .svn are always excluded.",
|
|
)
|
|
action_frame = ttk.LabelFrame(frame, text="Manual Backup", padding=(10, 5))
|
|
action_frame.pack(pady=10, fill="x", expand=False)
|
|
self.manual_backup_button = ttk.Button(
|
|
action_frame,
|
|
text="Backup Now (ZIP)",
|
|
command=self.manual_backup_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.manual_backup_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.manual_backup_button,
|
|
"Create a manual ZIP backup of the working directory now using the configured exclusions.",
|
|
)
|
|
return frame
|
|
|
|
def _create_commit_tab(self):
|
|
# ... (Codice da gui.py originale) ...
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.rowconfigure(3, weight=1)
|
|
frame.columnconfigure(0, weight=1)
|
|
self.autocommit_checkbox = ttk.Checkbutton(
|
|
frame,
|
|
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)
|
|
)
|
|
self.create_tooltip(
|
|
self.autocommit_checkbox,
|
|
"If enabled, automatically stage all changes and commit them using the message below before creating a bundle.",
|
|
)
|
|
ttk.Label(frame, text="Commit Message:").grid(
|
|
row=1, column=0, columnspan=3, sticky="w", padx=5
|
|
)
|
|
self.commit_message_text = scrolledtext.ScrolledText(
|
|
frame,
|
|
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)
|
|
)
|
|
self.create_tooltip(
|
|
self.commit_message_text,
|
|
"Enter the commit message here for manual commits or autocommits.",
|
|
)
|
|
changes_frame = ttk.LabelFrame(
|
|
frame, 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)
|
|
self.create_tooltip(
|
|
self.changed_files_listbox,
|
|
"List of changed, added, or deleted files. Double-click to view diff (vs HEAD). Right-click for actions.",
|
|
)
|
|
# self.changed_files_context_menu = tk.Menu(self.changed_files_listbox, tearoff=0) # Menu viene creato in __init__
|
|
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)
|
|
)
|
|
self.create_tooltip(
|
|
self.refresh_changes_button,
|
|
"Refresh the list of changed files in the working directory.",
|
|
)
|
|
self.commit_button = ttk.Button(
|
|
frame,
|
|
text="Commit Staged Changes",
|
|
command=self.commit_changes_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.commit_button.grid(
|
|
row=4, column=2, sticky="se", padx=5, pady=5
|
|
) # Allinea a destra in basso
|
|
self.create_tooltip(
|
|
self.commit_button,
|
|
"Stage all current changes (git add .) and commit them with the provided message.",
|
|
)
|
|
return frame
|
|
|
|
def _create_tags_tab(self):
|
|
# ... (Codice da gui.py originale) ...
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.columnconfigure(0, weight=1)
|
|
frame.rowconfigure(1, weight=1)
|
|
ttk.Label(frame, text="Existing Tags (Newest First):").grid(
|
|
row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2)
|
|
)
|
|
list_frame = ttk.Frame(frame)
|
|
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.tag_listbox = tk.Listbox(
|
|
list_frame,
|
|
height=10,
|
|
exportselection=False,
|
|
selectmode=tk.SINGLE,
|
|
font=("Consolas", 9),
|
|
borderwidth=1,
|
|
relief=tk.SUNKEN,
|
|
)
|
|
self.tag_listbox.grid(row=0, column=0, sticky="nsew")
|
|
scrollbar = ttk.Scrollbar(
|
|
list_frame, orient=tk.VERTICAL, command=self.tag_listbox.yview
|
|
)
|
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
self.tag_listbox.config(yscrollcommand=scrollbar.set)
|
|
self.create_tooltip(
|
|
self.tag_listbox, "List of existing tags. Select a tag to perform actions."
|
|
)
|
|
button_frame = ttk.Frame(frame)
|
|
button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5))
|
|
bw = 18
|
|
self.refresh_tags_button = ttk.Button(
|
|
button_frame,
|
|
text="Refresh Tags",
|
|
width=bw,
|
|
command=self.refresh_tags_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.refresh_tags_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
|
|
self.create_tooltip(
|
|
self.refresh_tags_button, "Reload the list of tags from the repository."
|
|
)
|
|
self.create_tag_button = ttk.Button(
|
|
button_frame,
|
|
text="Create New Tag...",
|
|
width=bw,
|
|
command=self.create_tag_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.create_tag_button.pack(side=tk.TOP, fill=tk.X, pady=5)
|
|
self.create_tooltip(
|
|
self.create_tag_button,
|
|
"Create a new annotated tag pointing to the current commit.",
|
|
)
|
|
self.checkout_tag_button = ttk.Button(
|
|
button_frame,
|
|
text="Checkout Selected Tag",
|
|
width=bw,
|
|
command=self.checkout_tag_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.checkout_tag_button.pack(side=tk.TOP, fill=tk.X, pady=5)
|
|
self.create_tooltip(
|
|
self.checkout_tag_button,
|
|
"Switch the working directory to the state of the selected tag (Detached HEAD).",
|
|
)
|
|
return frame
|
|
|
|
def _create_branch_tab(self):
|
|
# ... (Codice da gui.py originale, usa self.refresh_branches_callback) ...
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.columnconfigure(0, weight=1)
|
|
frame.rowconfigure(1, weight=1)
|
|
ttk.Label(frame, text="Local Branches (* = Current):").grid(
|
|
row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2)
|
|
)
|
|
list_frame = ttk.Frame(frame)
|
|
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("<Button-3>", 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)
|
|
self.create_tooltip(
|
|
self.branch_listbox,
|
|
"List of local branches. Right-click for actions (merge, delete, compare).",
|
|
)
|
|
button_frame = ttk.Frame(frame)
|
|
button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5))
|
|
bw = 18
|
|
self.refresh_branches_button = ttk.Button(
|
|
button_frame,
|
|
text="Refresh Branches",
|
|
width=bw,
|
|
command=self.refresh_branches_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.refresh_branches_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
|
|
self.create_tooltip(
|
|
self.refresh_branches_button, "Reload the list of local branches."
|
|
)
|
|
self.create_branch_button = ttk.Button(
|
|
button_frame,
|
|
text="Create New Branch...",
|
|
width=bw,
|
|
command=self.create_branch_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.create_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5)
|
|
self.create_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=bw,
|
|
command=self.checkout_branch_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.checkout_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5)
|
|
self.create_tooltip(
|
|
self.checkout_branch_button,
|
|
"Switch the working directory to the selected local branch.",
|
|
)
|
|
return frame
|
|
|
|
def _create_remote_tab(self):
|
|
# ... (Codice da gui.py originale, usa self.refresh_branches_callback per il refresh locale) ...
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.columnconfigure(0, weight=1)
|
|
frame.columnconfigure(1, weight=1)
|
|
frame.rowconfigure(3, weight=1)
|
|
top_frame = ttk.LabelFrame(
|
|
frame, text="Remote Configuration & Sync Status", padding=(10, 5)
|
|
)
|
|
top_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5, 0))
|
|
top_frame.columnconfigure(1, weight=1)
|
|
ttk.Label(top_frame, text="Remote URL:").grid(
|
|
row=0, column=0, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.remote_url_entry = ttk.Entry(
|
|
top_frame, textvariable=self.remote_url_var, width=60
|
|
)
|
|
self.remote_url_entry.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=3)
|
|
self.create_tooltip(
|
|
self.remote_url_entry,
|
|
"URL of the remote repository (e.g., https://... or ssh://...).",
|
|
)
|
|
ttk.Label(top_frame, text="Local Name:").grid(
|
|
row=1, column=0, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.remote_name_entry = ttk.Entry(
|
|
top_frame, textvariable=self.remote_name_var, width=20
|
|
)
|
|
self.remote_name_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=3)
|
|
self.create_tooltip(
|
|
self.remote_name_entry,
|
|
f"Local alias for the remote (Default: '{DEFAULT_REMOTE_NAME}').",
|
|
)
|
|
self.sync_status_label = ttk.Label(
|
|
top_frame,
|
|
textvariable=self.remote_ahead_behind_var,
|
|
anchor=tk.W,
|
|
padding=(5, 2),
|
|
)
|
|
self.sync_status_label.grid(row=2, column=1, sticky="ew", padx=5, pady=(2, 5))
|
|
self.create_tooltip(
|
|
self.sync_status_label,
|
|
"Shows the current local branch and its sync status (ahead/behind) relative to its upstream.",
|
|
)
|
|
config_action_frame = ttk.Frame(top_frame)
|
|
config_action_frame.grid(
|
|
row=0, column=2, rowspan=3, sticky="ne", padx=(15, 5), pady=3
|
|
)
|
|
self.apply_remote_config_button = ttk.Button(
|
|
config_action_frame,
|
|
text="Apply Config",
|
|
command=self.apply_remote_config_callback,
|
|
state=tk.DISABLED,
|
|
width=18,
|
|
)
|
|
self.apply_remote_config_button.pack(side=tk.TOP, fill=tk.X, pady=1)
|
|
self.create_tooltip(
|
|
self.apply_remote_config_button,
|
|
"Add or update this remote configuration in the local .git/config file.",
|
|
)
|
|
self.check_auth_button = ttk.Button(
|
|
config_action_frame,
|
|
text="Check Connection",
|
|
command=self.check_connection_auth_callback,
|
|
state=tk.DISABLED,
|
|
width=18,
|
|
)
|
|
self.check_auth_button.pack(side=tk.TOP, fill=tk.X, pady=1)
|
|
self.create_tooltip(
|
|
self.check_auth_button,
|
|
"Verify connection and authentication status for the configured remote.",
|
|
)
|
|
self.auth_status_indicator_label = ttk.Label(
|
|
config_action_frame,
|
|
textvariable=self.remote_auth_status_var,
|
|
anchor=tk.CENTER,
|
|
relief=tk.SUNKEN,
|
|
padding=(5, 1),
|
|
width=18,
|
|
)
|
|
self.auth_status_indicator_label.pack(side=tk.TOP, fill=tk.X, pady=(1, 3))
|
|
self._update_auth_status_indicator("unknown")
|
|
self.create_tooltip(
|
|
self.auth_status_indicator_label,
|
|
"Connection and authentication status (Unknown, Checking, Connected, Auth Required, Failed, Error).",
|
|
)
|
|
self.refresh_sync_status_button = ttk.Button(
|
|
config_action_frame,
|
|
text="Refresh Sync Status",
|
|
command=self.refresh_remote_status_callback,
|
|
state=tk.DISABLED,
|
|
width=18,
|
|
)
|
|
self.refresh_sync_status_button.pack(side=tk.TOP, fill=tk.X, pady=(3, 1))
|
|
self.create_tooltip(
|
|
self.refresh_sync_status_button,
|
|
"Check how many commits the current local branch is ahead or behind its upstream remote branch.",
|
|
)
|
|
actions_frame = ttk.LabelFrame(
|
|
frame, text="Common Remote Actions", padding=(10, 5)
|
|
)
|
|
actions_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
|
|
action_buttons_inner_frame = ttk.Frame(actions_frame)
|
|
action_buttons_inner_frame.pack()
|
|
self.fetch_button = ttk.Button(
|
|
action_buttons_inner_frame,
|
|
text="Fetch",
|
|
command=self.fetch_remote_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.fetch_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.fetch_button,
|
|
"Download objects and references from the remote repository (does not modify local branches).",
|
|
)
|
|
self.pull_button = ttk.Button(
|
|
action_buttons_inner_frame,
|
|
text="Pull",
|
|
command=self.pull_remote_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.pull_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.pull_button,
|
|
"Fetch changes from the remote and merge them into the current local branch.",
|
|
)
|
|
self.push_button = ttk.Button(
|
|
action_buttons_inner_frame,
|
|
text="Push",
|
|
command=self.push_remote_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.push_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.push_button,
|
|
"Upload local commits from the current branch to the corresponding branch on the remote repository.",
|
|
)
|
|
self.push_tags_button = ttk.Button(
|
|
action_buttons_inner_frame,
|
|
text="Push Tags",
|
|
command=self.push_tags_remote_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.push_tags_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.create_tooltip(
|
|
self.push_tags_button, "Upload all local tags to the remote repository."
|
|
)
|
|
branch_view_frame = ttk.Frame(frame)
|
|
branch_view_frame.grid(
|
|
row=3, column=0, columnspan=2, sticky="nsew", padx=0, pady=(5, 5)
|
|
)
|
|
branch_view_frame.rowconfigure(0, weight=1)
|
|
branch_view_frame.columnconfigure(0, weight=1)
|
|
branch_view_frame.columnconfigure(1, weight=1)
|
|
remote_list_frame = ttk.Frame(branch_view_frame, padding=(5, 0, 10, 0))
|
|
remote_list_frame.grid(row=0, column=0, sticky="nsew")
|
|
remote_list_frame.rowconfigure(1, weight=1)
|
|
remote_list_frame.columnconfigure(0, weight=1)
|
|
ttk.Label(remote_list_frame, text="Remote Branches (from last Fetch):").grid(
|
|
row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2)
|
|
)
|
|
self.remote_branches_listbox = tk.Listbox(
|
|
remote_list_frame,
|
|
height=8,
|
|
exportselection=False,
|
|
selectmode=tk.SINGLE,
|
|
font=("Consolas", 9),
|
|
borderwidth=1,
|
|
relief=tk.SUNKEN,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.remote_branches_listbox.grid(row=1, column=0, sticky="nsew")
|
|
self.remote_branches_listbox.bind(
|
|
"<Button-3>", self._show_remote_branches_context_menu
|
|
)
|
|
rb_scrollbar = ttk.Scrollbar(
|
|
remote_list_frame,
|
|
orient=tk.VERTICAL,
|
|
command=self.remote_branches_listbox.yview,
|
|
)
|
|
rb_scrollbar.grid(row=1, column=1, sticky="ns")
|
|
self.remote_branches_listbox.config(yscrollcommand=rb_scrollbar.set)
|
|
self.create_tooltip(
|
|
self.remote_branches_listbox,
|
|
"Branches existing on the remote repository. Right-click for actions (Compare, Checkout as local).",
|
|
)
|
|
self.refresh_remote_branches_button = ttk.Button(
|
|
remote_list_frame,
|
|
text="Refresh Remote List",
|
|
command=self.refresh_remote_branches_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.refresh_remote_branches_button.grid(
|
|
row=2, column=0, columnspan=2, sticky="w", padx=5, pady=(5, 0)
|
|
)
|
|
self.create_tooltip(
|
|
self.refresh_remote_branches_button,
|
|
"Update the list of remote branches (requires fetching from the remote).",
|
|
)
|
|
local_list_frame = ttk.Frame(branch_view_frame, padding=(10, 0, 5, 0))
|
|
local_list_frame.grid(row=0, column=1, sticky="nsew")
|
|
local_list_frame.rowconfigure(1, weight=1)
|
|
local_list_frame.columnconfigure(0, weight=1)
|
|
ttk.Label(local_list_frame, text="Local Branches (* = Current):").grid(
|
|
row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2)
|
|
)
|
|
self.local_branches_listbox_remote_tab = tk.Listbox(
|
|
local_list_frame,
|
|
height=8,
|
|
exportselection=False,
|
|
selectmode=tk.SINGLE,
|
|
font=("Consolas", 9),
|
|
borderwidth=1,
|
|
relief=tk.SUNKEN,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.local_branches_listbox_remote_tab.grid(row=1, column=0, sticky="nsew")
|
|
self.local_branches_listbox_remote_tab.bind(
|
|
"<Button-3>", self._show_local_branches_context_menu
|
|
)
|
|
lb_scrollbar_remote_tab = ttk.Scrollbar(
|
|
local_list_frame,
|
|
orient=tk.VERTICAL,
|
|
command=self.local_branches_listbox_remote_tab.yview,
|
|
)
|
|
lb_scrollbar_remote_tab.grid(row=1, column=1, sticky="ns")
|
|
self.local_branches_listbox_remote_tab.config(
|
|
yscrollcommand=lb_scrollbar_remote_tab.set
|
|
)
|
|
self.create_tooltip(
|
|
self.local_branches_listbox_remote_tab,
|
|
"Your local branches. Right-click for actions (Checkout, Merge, Delete, Compare).",
|
|
)
|
|
self.refresh_local_branches_button_remote_tab = ttk.Button(
|
|
local_list_frame,
|
|
text="Refresh Local List",
|
|
command=self.refresh_branches_callback,
|
|
state=tk.DISABLED,
|
|
) # Usa refresh_branches_callback
|
|
self.refresh_local_branches_button_remote_tab.grid(
|
|
row=2, column=0, columnspan=2, sticky="w", padx=5, pady=(5, 0)
|
|
)
|
|
self.create_tooltip(
|
|
self.refresh_local_branches_button_remote_tab,
|
|
"Update the list of local branches.",
|
|
)
|
|
return frame
|
|
|
|
def _create_history_tab_treeview(self):
|
|
# ... (Codice della nuova versione con TreeView, come nella risposta precedente) ...
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.rowconfigure(1, weight=1)
|
|
frame.columnconfigure(0, weight=1)
|
|
controls_frame = ttk.Frame(frame)
|
|
controls_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
|
controls_frame.columnconfigure(1, weight=1)
|
|
ttk.Label(controls_frame, text="Filter History by Branch/Tag:").pack(
|
|
side=tk.LEFT, padx=(0, 5)
|
|
)
|
|
self.history_branch_filter_var = tk.StringVar()
|
|
self.history_branch_filter_combo = ttk.Combobox(
|
|
controls_frame,
|
|
textvariable=self.history_branch_filter_var,
|
|
state="readonly",
|
|
width=40,
|
|
)
|
|
self.history_branch_filter_combo.pack(
|
|
side=tk.LEFT, expand=True, fill=tk.X, padx=5
|
|
)
|
|
if hasattr(self, "refresh_history_callback") and callable(
|
|
self.refresh_history_callback
|
|
):
|
|
self.history_branch_filter_combo.bind(
|
|
"<<ComboboxSelected>>", lambda e: self.refresh_history_callback()
|
|
)
|
|
self.create_tooltip(
|
|
self.history_branch_filter_combo,
|
|
"Select a branch or tag to filter the commit history.",
|
|
)
|
|
self.refresh_history_button = ttk.Button(
|
|
controls_frame, text="Refresh History", state=tk.DISABLED
|
|
)
|
|
if hasattr(self, "refresh_history_callback") and callable(
|
|
self.refresh_history_callback
|
|
):
|
|
self.refresh_history_button.config(command=self.refresh_history_callback)
|
|
self.refresh_history_button.pack(side=tk.LEFT, padx=5)
|
|
self.create_tooltip(
|
|
self.refresh_history_button,
|
|
"Reload commit history based on the selected filter.",
|
|
)
|
|
content_frame = ttk.Frame(frame)
|
|
content_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
|
|
content_frame.rowconfigure(0, weight=1)
|
|
content_frame.columnconfigure(0, weight=1)
|
|
columns = ("hash", "datetime", "author", "details")
|
|
self.history_tree = ttk.Treeview(
|
|
content_frame,
|
|
columns=columns,
|
|
show="headings",
|
|
selectmode="browse",
|
|
height=15,
|
|
)
|
|
self.history_tree.heading("hash", text="Hash", anchor="w")
|
|
self.history_tree.heading("datetime", text="Date/Time", anchor="w")
|
|
self.history_tree.heading("author", text="Author", anchor="w")
|
|
self.history_tree.heading("details", text="Subject / Refs", anchor="w")
|
|
self.history_tree.column("hash", width=80, stretch=tk.NO, anchor="w")
|
|
self.history_tree.column("datetime", width=140, stretch=tk.NO, anchor="w")
|
|
self.history_tree.column("author", width=150, stretch=tk.NO, anchor="w")
|
|
self.history_tree.column("details", width=450, stretch=tk.YES, anchor="w")
|
|
tree_scrollbar_y = ttk.Scrollbar(
|
|
content_frame, orient=tk.VERTICAL, command=self.history_tree.yview
|
|
)
|
|
tree_scrollbar_x = ttk.Scrollbar(
|
|
content_frame, orient=tk.HORIZONTAL, command=self.history_tree.xview
|
|
)
|
|
self.history_tree.configure(
|
|
yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set
|
|
)
|
|
self.history_tree.grid(row=0, column=0, sticky="nsew")
|
|
tree_scrollbar_y.grid(row=0, column=1, sticky="ns")
|
|
tree_scrollbar_x.grid(row=1, column=0, columnspan=2, sticky="ew")
|
|
self.history_tree.bind("<Double-Button-1>", self._on_history_double_click_tree)
|
|
self.create_tooltip(
|
|
self.history_tree, "Double-click a commit line to view details."
|
|
)
|
|
return frame
|
|
|
|
def _create_log_area(self, parent_frame):
|
|
# ... (Codice da gui.py originale) ...
|
|
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")
|
|
# Configura tag per colori log
|
|
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):
|
|
# ... (Codice da gui.py originale) ...
|
|
# Imposta il primo profilo o il default se esiste
|
|
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.")
|
|
|
|
# --- GUI Update Methods ---
|
|
def toggle_backup_dir(self):
|
|
# ... (Codice da gui.py originale) ...
|
|
state = tk.NORMAL if self.autobackup_var.get() else tk.DISABLED
|
|
if hasattr(self, "backup_dir_entry") and self.backup_dir_entry.winfo_exists():
|
|
self.backup_dir_entry.config(state=state)
|
|
if hasattr(self, "backup_dir_button") and self.backup_dir_button.winfo_exists():
|
|
self.backup_dir_button.config(state=state)
|
|
|
|
def browse_backup_dir(self):
|
|
# ... (Codice da gui.py originale) ...
|
|
curr = self.backup_dir_var.get()
|
|
init = curr if os.path.isdir(curr) else DEFAULT_BACKUP_DIR
|
|
if not os.path.isdir(init):
|
|
init = os.path.expanduser("~")
|
|
sel = filedialog.askdirectory(
|
|
initialdir=init, title="Select Backup Directory", parent=self.master
|
|
)
|
|
if sel:
|
|
self.backup_dir_var.set(sel)
|
|
|
|
def update_svn_indicator(self, is_prepared):
|
|
# ... (Codice da gui.py originale) ...
|
|
color = self.GREEN if is_prepared else self.RED
|
|
tip = (
|
|
"Repository is valid and prepared (found .git)."
|
|
if is_prepared
|
|
else "Directory is not a valid/prepared Git repository."
|
|
)
|
|
if (
|
|
hasattr(self, "svn_status_indicator")
|
|
and self.svn_status_indicator.winfo_exists()
|
|
):
|
|
self.svn_status_indicator.config(background=color)
|
|
self.update_tooltip(self.svn_status_indicator, tip) # Usa metodo helper
|
|
|
|
def update_profile_dropdown(self, sections: List[str]):
|
|
# ... (Codice da gui.py originale) ...
|
|
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:
|
|
if DEFAULT_PROFILE in sections:
|
|
self.profile_var.set(DEFAULT_PROFILE)
|
|
else:
|
|
self.profile_var.set(sections[0])
|
|
else:
|
|
self.profile_var.set("")
|
|
|
|
def update_tag_list(self, tags_data: List[Tuple[str, str]]):
|
|
# ... (Codice da gui.py originale) ...
|
|
listbox = getattr(self, "tag_listbox", None)
|
|
if not listbox or not listbox.winfo_exists():
|
|
return
|
|
try:
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.delete(0, tk.END)
|
|
if tags_data:
|
|
try:
|
|
default_fg = self.style.lookup("TListbox", "foreground")
|
|
if listbox.cget("fg") == "grey":
|
|
listbox.config(fg=default_fg)
|
|
except tk.TclError:
|
|
pass
|
|
for name, subj in tags_data:
|
|
listbox.insert(tk.END, f"{name}\t({subj})")
|
|
else:
|
|
listbox.insert(tk.END, "(No tags found)")
|
|
listbox.config(fg="grey")
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.yview_moveto(0.0)
|
|
except Exception as e:
|
|
print(f"ERROR updating tag list GUI: {e}", file=sys.stderr) # Log fallback
|
|
try:
|
|
if listbox.winfo_exists():
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.delete(0, tk.END)
|
|
listbox.insert(tk.END, "(Error)")
|
|
listbox.config(fg="red", state=tk.DISABLED)
|
|
except Exception:
|
|
pass
|
|
|
|
def update_branch_list(self, branches: List[str], current_branch: Optional[str]):
|
|
# ... (Codice da gui.py originale, aggiorna entrambe le listbox locali) ...
|
|
func_name = "update_branch_list (GUI)"
|
|
log_handler.log_debug(
|
|
f"Updating local branches. Current: {repr(current_branch)}",
|
|
func_name=func_name,
|
|
)
|
|
self.current_local_branch = current_branch
|
|
listboxes_to_update = []
|
|
lb1 = getattr(self, "branch_listbox", None)
|
|
lb2 = getattr(self, "local_branches_listbox_remote_tab", None)
|
|
if lb1 and lb1.winfo_exists():
|
|
listboxes_to_update.append(lb1)
|
|
if lb2 and lb2.winfo_exists():
|
|
listboxes_to_update.append(lb2)
|
|
if not listboxes_to_update:
|
|
log_handler.log_warning(
|
|
"No local branch listbox found for update.", func_name=func_name
|
|
)
|
|
return
|
|
for listbox in listboxes_to_update:
|
|
widget_name = "unknown_listbox"
|
|
try:
|
|
widget_name = listbox.winfo_pathname(listbox.winfo_id())
|
|
except Exception:
|
|
pass
|
|
try:
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.delete(0, tk.END)
|
|
sel_idx = -1
|
|
if isinstance(branches, list) and branches:
|
|
try:
|
|
default_fg = self.style.lookup("TListbox", "foreground")
|
|
if listbox.cget("fg") != default_fg:
|
|
listbox.config(fg=default_fg)
|
|
except tk.TclError:
|
|
pass
|
|
for i, branch in enumerate(branches):
|
|
prefix = "* " if branch == current_branch else " "
|
|
listbox.insert(tk.END, f"{prefix}{str(branch)}")
|
|
if branch == current_branch:
|
|
sel_idx = i
|
|
elif isinstance(branches, list) and not branches:
|
|
listbox.insert(tk.END, "(No local branches)")
|
|
listbox.config(fg="grey")
|
|
else:
|
|
listbox.insert(tk.END, "(Invalid data received)")
|
|
listbox.config(fg="orange")
|
|
if sel_idx >= 0:
|
|
listbox.selection_set(sel_idx)
|
|
listbox.see(sel_idx)
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.yview_moveto(0.0)
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Error updating local branch list GUI ({widget_name}): {e}",
|
|
func_name=func_name,
|
|
)
|
|
try:
|
|
if listbox.winfo_exists():
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.delete(0, tk.END)
|
|
listbox.insert(tk.END, "(Error)")
|
|
listbox.config(fg="red", state=tk.DISABLED)
|
|
except Exception:
|
|
pass
|
|
|
|
def get_selected_tag(self) -> Optional[str]:
|
|
# ... (Codice da gui.py originale) ...
|
|
listbox = getattr(self, "tag_listbox", None)
|
|
item = None
|
|
if listbox and listbox.winfo_exists():
|
|
idx = listbox.curselection()
|
|
if idx:
|
|
item = listbox.get(idx[0])
|
|
if item:
|
|
if "\t" in item and not item.startswith("("):
|
|
return item.split("\t", 1)[0].strip()
|
|
elif not item.startswith("("):
|
|
return item.strip()
|
|
return None
|
|
|
|
def get_selected_branch(self) -> Optional[str]:
|
|
# ... (Codice da gui.py originale) ...
|
|
listbox = getattr(self, "branch_listbox", None)
|
|
item = None
|
|
name = None
|
|
if listbox and listbox.winfo_exists():
|
|
idx = listbox.curselection()
|
|
if idx:
|
|
item = listbox.get(idx[0])
|
|
if item:
|
|
name = item.lstrip("* ").strip()
|
|
if name and not name.startswith("("):
|
|
return name
|
|
return None
|
|
|
|
def get_commit_message(self) -> str:
|
|
# ... (Codice da gui.py originale) ...
|
|
if (
|
|
hasattr(self, "commit_message_text")
|
|
and self.commit_message_text.winfo_exists()
|
|
):
|
|
return self.commit_message_text.get("1.0", "end-1c").strip()
|
|
return ""
|
|
|
|
def clear_commit_message(self):
|
|
# ... (Codice da gui.py originale) ...
|
|
if (
|
|
hasattr(self, "commit_message_text")
|
|
and self.commit_message_text.winfo_exists()
|
|
):
|
|
try:
|
|
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()
|
|
except Exception:
|
|
pass
|
|
|
|
def _on_history_double_click_tree(
|
|
self, event: tk.Event
|
|
) -> None: # Assicurati che questo sia il metodo corretto
|
|
# ... (Codice aggiornato per Treeview, come nella risposta precedente) ...
|
|
func_name = "_on_history_double_click_tree"
|
|
tree = getattr(self, "history_tree", None)
|
|
view_cb = getattr(self, "view_commit_details_callback", None)
|
|
tree_exists = tree is not None and tree.winfo_exists()
|
|
cb_callable = callable(view_cb)
|
|
log_handler.log_debug(
|
|
f"Tree exists: {tree_exists}. Callback callable: {cb_callable}.",
|
|
func_name=func_name,
|
|
)
|
|
if view_cb and not cb_callable:
|
|
log_handler.log_warning(
|
|
f"view_commit_details_callback is NOT callable. Type: {type(view_cb)}, Value: {repr(view_cb)}",
|
|
func_name=func_name,
|
|
)
|
|
if not tree_exists or not cb_callable:
|
|
log_handler.log_warning(
|
|
"History tree or view commit details callback not configured or available.",
|
|
func_name=func_name,
|
|
)
|
|
return
|
|
try:
|
|
selected_iid = tree.focus()
|
|
if not selected_iid:
|
|
log_handler.log_debug(
|
|
"No item selected in history tree on double click.",
|
|
func_name=func_name,
|
|
)
|
|
return
|
|
item_data = tree.item(selected_iid)
|
|
item_values = item_data.get("values")
|
|
if item_values and len(item_values) > 0:
|
|
commit_hash_short = str(item_values[0]).strip()
|
|
if commit_hash_short and not commit_hash_short.startswith("("):
|
|
log_handler.log_info(
|
|
f"History tree double-clicked. Requesting details for hash: '{commit_hash_short}'",
|
|
func_name=func_name,
|
|
)
|
|
view_cb(commit_hash_short)
|
|
else:
|
|
log_handler.log_debug(
|
|
f"Ignoring double-click on placeholder/invalid history item: {item_values}",
|
|
func_name=func_name,
|
|
)
|
|
else:
|
|
log_handler.log_warning(
|
|
f"Could not get values for selected history item IID: {selected_iid}",
|
|
func_name=func_name,
|
|
)
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Error handling history tree double-click: {e}", func_name=func_name
|
|
)
|
|
messagebox.showerror(
|
|
"Error", f"Could not process history selection:\n{e}", parent=self
|
|
)
|
|
|
|
def update_history_display(
|
|
self, log_lines: List[str]
|
|
): # Assicurati che questo sia il metodo corretto
|
|
# ... (Codice aggiornato per Treeview, come nella risposta precedente) ...
|
|
func_name = "update_history_display_(Tree)"
|
|
log_handler.log_debug(
|
|
f"Updating history display (Treeview) with {len(log_lines) if isinstance(log_lines, list) else 'N/A'} lines.",
|
|
func_name=func_name,
|
|
)
|
|
tree = getattr(self, "history_tree", None)
|
|
if not tree or not tree.winfo_exists():
|
|
log_handler.log_error(
|
|
"history_tree widget not available for update.", func_name=func_name
|
|
)
|
|
return
|
|
try:
|
|
for item_id in tree.get_children():
|
|
tree.delete(item_id)
|
|
if isinstance(log_lines, list):
|
|
if log_lines:
|
|
for i, line in enumerate(log_lines):
|
|
line_str = str(line).strip()
|
|
if not line_str or line_str.startswith("("):
|
|
continue
|
|
commit_hash, commit_datetime, commit_author, commit_details = (
|
|
"",
|
|
"",
|
|
"",
|
|
line_str,
|
|
)
|
|
try:
|
|
parts = line_str.split("|", 2)
|
|
if len(parts) >= 3:
|
|
part1 = parts[0].strip()
|
|
part2 = parts[1].strip()
|
|
part3 = parts[2].strip()
|
|
first_space = part1.find(" ")
|
|
if first_space != -1:
|
|
commit_hash = part1[:first_space]
|
|
commit_datetime = part1[first_space:].strip()[:16]
|
|
else:
|
|
commit_hash = part1
|
|
commit_datetime = "N/A"
|
|
commit_author = part2
|
|
commit_details = part3
|
|
elif len(parts) == 2:
|
|
part1 = parts[0].strip()
|
|
part3 = parts[1].strip()
|
|
first_space = part1.find(" ")
|
|
if first_space != -1:
|
|
commit_hash = part1[:first_space]
|
|
commit_datetime = part1[first_space:].strip()[:16]
|
|
else:
|
|
commit_hash = part1
|
|
commit_datetime = "N/A"
|
|
commit_author = "N/A"
|
|
commit_details = part3
|
|
elif len(parts) == 1:
|
|
commit_details = line_str
|
|
commit_hash = "N/A"
|
|
commit_datetime = "N/A"
|
|
commit_author = "N/A"
|
|
except Exception as parse_err:
|
|
log_handler.log_warning(
|
|
f"Could not parse history line '{line_str}': {parse_err}",
|
|
func_name=func_name,
|
|
)
|
|
commit_hash = "Error"
|
|
commit_datetime = ""
|
|
commit_author = ""
|
|
commit_details = line_str
|
|
tree.insert(
|
|
parent="",
|
|
index=tk.END,
|
|
iid=i,
|
|
values=(
|
|
commit_hash,
|
|
commit_datetime,
|
|
commit_author,
|
|
commit_details,
|
|
),
|
|
)
|
|
else:
|
|
tree.insert(
|
|
parent="",
|
|
index=tk.END,
|
|
iid=0,
|
|
values=("", "", "", "(No history found)"),
|
|
)
|
|
else:
|
|
log_handler.log_warning(
|
|
f"Invalid data received for history (Treeview): {repr(log_lines)}",
|
|
func_name=func_name,
|
|
)
|
|
tree.insert(
|
|
parent="",
|
|
index=tk.END,
|
|
iid=0,
|
|
values=("", "", "", "(Error: Invalid data received)"),
|
|
)
|
|
tree.yview_moveto(0.0)
|
|
tree.xview_moveto(0.0)
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Error updating history Treeview GUI: {e}", func_name=func_name
|
|
)
|
|
try:
|
|
if tree.winfo_exists():
|
|
for item_id in tree.get_children():
|
|
tree.delete(item_id)
|
|
tree.insert(
|
|
parent="",
|
|
index=tk.END,
|
|
iid=0,
|
|
values=("", "", "", "(Error displaying history)"),
|
|
)
|
|
except Exception as fallback_e:
|
|
log_handler.log_error(
|
|
f"Error displaying fallback error in history Treeview: {fallback_e}",
|
|
func_name=func_name,
|
|
)
|
|
|
|
def update_history_branch_filter(
|
|
self, branches_tags: List[str], current_ref: Optional[str] = None
|
|
):
|
|
# ... (Codice da gui.py originale) ...
|
|
combo = getattr(self, "history_branch_filter_combo", None)
|
|
var = getattr(self, "history_branch_filter_var", None)
|
|
if not combo or not var or not combo.winfo_exists():
|
|
return
|
|
opts = ["-- All History --"] + sorted(branches_tags if branches_tags else [])
|
|
combo["values"] = opts
|
|
# Set selection: use current_ref if valid, otherwise default to All History
|
|
if current_ref and current_ref in opts:
|
|
var.set(current_ref)
|
|
else:
|
|
var.set(opts[0])
|
|
|
|
def update_changed_files_list(self, files_status_list: List[str]):
|
|
# ... (Codice da gui.py originale, con sanificazione) ...
|
|
listbox = getattr(self, "changed_files_listbox", None)
|
|
if not listbox or not listbox.winfo_exists():
|
|
print(
|
|
"ERROR: changed_files_listbox not available for update.",
|
|
file=sys.stderr,
|
|
)
|
|
return
|
|
try:
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.delete(0, tk.END)
|
|
if files_status_list:
|
|
try:
|
|
default_fg = self.style.lookup("TListbox", "foreground")
|
|
if listbox.cget("fg") == "grey":
|
|
listbox.config(fg=default_fg)
|
|
except tk.TclError:
|
|
pass
|
|
processed_lines = 0
|
|
for status_line in files_status_list:
|
|
try:
|
|
sanitized_line = str(status_line).replace("\x00", "").strip()
|
|
if sanitized_line:
|
|
listbox.insert(tk.END, sanitized_line)
|
|
processed_lines += 1
|
|
else:
|
|
print(
|
|
f"Warning: Sanitized status line empty: {repr(status_line)}",
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as insert_err:
|
|
print(
|
|
f"ERROR inserting line into listbox: {insert_err} - Line: {repr(status_line)}",
|
|
file=sys.stderr,
|
|
)
|
|
listbox.insert(
|
|
tk.END, f"(Error processing line: {repr(status_line)})"
|
|
)
|
|
listbox.itemconfig(tk.END, {"fg": "red"})
|
|
if processed_lines == 0 and files_status_list:
|
|
listbox.insert(tk.END, "(Error processing all lines)")
|
|
listbox.config(fg="red")
|
|
else:
|
|
listbox.insert(tk.END, "(No changes detected)")
|
|
listbox.config(fg="grey")
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.yview_moveto(0.0)
|
|
except Exception as e:
|
|
print(f"ERROR updating changed files list GUI: {e}", file=sys.stderr)
|
|
try:
|
|
listbox.delete(0, tk.END)
|
|
listbox.insert(tk.END, "(Error updating list)")
|
|
listbox.config(fg="red")
|
|
except:
|
|
pass
|
|
|
|
def _on_changed_file_double_click(self, event: tk.Event) -> None:
|
|
# ... (Codice da gui.py originale) ...
|
|
widget = event.widget
|
|
sel = widget.curselection()
|
|
line = None
|
|
if sel:
|
|
line = widget.get(sel[0])
|
|
if (
|
|
line
|
|
and hasattr(self, "open_diff_viewer_callback")
|
|
and callable(self.open_diff_viewer_callback)
|
|
):
|
|
self.open_diff_viewer_callback(line)
|
|
|
|
def _show_changed_files_context_menu(self, event: tk.Event) -> None:
|
|
# ... (Codice da gui.py originale, con correzione recupero 'line') ...
|
|
func_name = "_show_changed_files_context_menu"
|
|
line = None
|
|
listbox = getattr(self, "changed_files_listbox", None)
|
|
if not listbox:
|
|
return
|
|
try:
|
|
idx = listbox.nearest(event.y)
|
|
listbox.selection_clear(0, tk.END)
|
|
listbox.selection_set(idx)
|
|
listbox.activate(idx)
|
|
line = listbox.get(idx)
|
|
except tk.TclError:
|
|
log_handler.log_debug(
|
|
f"TclError getting selected line for context menu.", func_name=func_name
|
|
)
|
|
return
|
|
except Exception as e:
|
|
log_handler.log_error(
|
|
f"Error getting selected line for context menu: {e}",
|
|
func_name=func_name,
|
|
)
|
|
return
|
|
if line is None:
|
|
log_handler.log_debug(
|
|
f"Could not retrieve line content at index.", func_name=func_name
|
|
)
|
|
return
|
|
log_handler.log_debug(f"Context menu for line: '{line}'", func_name=func_name)
|
|
self.changed_files_context_menu.delete(0, tk.END)
|
|
cleaned = line.strip()
|
|
is_untracked = cleaned.startswith("??")
|
|
can_add = (
|
|
is_untracked
|
|
and hasattr(self, "add_selected_file_callback")
|
|
and callable(self.add_selected_file_callback)
|
|
)
|
|
add_state = tk.NORMAL if can_add else tk.DISABLED
|
|
self.changed_files_context_menu.add_command(
|
|
label="Add to Staging Area",
|
|
state=add_state,
|
|
command=lambda current_line=line: (
|
|
self.add_selected_file_callback(current_line) if can_add else None
|
|
),
|
|
)
|
|
can_diff = hasattr(self, "open_diff_viewer_callback") and callable(
|
|
self.open_diff_viewer_callback
|
|
)
|
|
diff_state = tk.DISABLED
|
|
if (
|
|
not is_untracked
|
|
and not cleaned.startswith("!!")
|
|
and not cleaned.startswith(" D")
|
|
and can_diff
|
|
):
|
|
diff_state = tk.NORMAL
|
|
self.changed_files_context_menu.add_command(
|
|
label="View Changes (Diff)",
|
|
state=diff_state,
|
|
command=lambda current_line=line: (
|
|
self.open_diff_viewer_callback(current_line)
|
|
if diff_state == tk.NORMAL
|
|
else None
|
|
),
|
|
)
|
|
try:
|
|
self.changed_files_context_menu.tk_popup(event.x_root, event.y_root)
|
|
finally:
|
|
self.changed_files_context_menu.grab_release()
|
|
|
|
def update_status_bar(
|
|
self,
|
|
message: str,
|
|
bg_color: Optional[str] = None,
|
|
duration_ms: Optional[int] = None,
|
|
):
|
|
# ... (Codice da gui.py originale) ...
|
|
if hasattr(self, "status_bar_var") and hasattr(self, "status_bar"):
|
|
try:
|
|
if self._status_reset_timer:
|
|
self.master.after_cancel(self._status_reset_timer)
|
|
self._status_reset_timer = None
|
|
|
|
def _update():
|
|
if self.status_bar.winfo_exists():
|
|
self.status_bar_var.set(message)
|
|
actual_bg = (
|
|
bg_color if bg_color else MainFrame.STATUS_DEFAULT_BG
|
|
)
|
|
try:
|
|
self.status_bar.config(background=actual_bg)
|
|
except tk.TclError:
|
|
self.status_bar.config(
|
|
background=MainFrame.STATUS_DEFAULT_BG
|
|
)
|
|
print(
|
|
f"Warning: Invalid status bar color '{bg_color}', using default.",
|
|
file=sys.stderr,
|
|
)
|
|
if bg_color and duration_ms and duration_ms > 0:
|
|
self._status_reset_timer = self.master.after(
|
|
duration_ms, self.reset_status_bar_color
|
|
)
|
|
|
|
self.master.after(0, _update)
|
|
except Exception as e:
|
|
print(f"ERROR updating status bar: {e}", file=sys.stderr)
|
|
|
|
def reset_status_bar_color(self):
|
|
# ... (Codice da gui.py originale) ...
|
|
self._status_reset_timer = None
|
|
if hasattr(self, "status_bar") and self.status_bar.winfo_exists():
|
|
try:
|
|
self.status_bar.config(background=MainFrame.STATUS_DEFAULT_BG)
|
|
except Exception as e:
|
|
print(f"ERROR resetting status bar color: {e}", file=sys.stderr)
|
|
|
|
def ask_new_profile_name(self) -> Optional[str]:
|
|
# ... (Codice da gui.py originale) ...
|
|
return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master)
|
|
|
|
def show_error(self, title: str, message: str):
|
|
# ... (Codice da gui.py originale) ...
|
|
messagebox.showerror(title, message, parent=self.master)
|
|
|
|
def show_info(self, title: str, message: str):
|
|
# ... (Codice da gui.py originale) ...
|
|
messagebox.showinfo(title, message, parent=self.master)
|
|
|
|
def show_warning(self, title: str, message: str):
|
|
# ... (Codice da gui.py originale) ...
|
|
messagebox.showwarning(title, message, parent=self.master)
|
|
|
|
def ask_yes_no(self, title: str, message: str) -> bool:
|
|
# ... (Codice da gui.py originale) ...
|
|
return messagebox.askyesno(title, message, parent=self.master)
|
|
|
|
def create_tooltip(self, widget: tk.Widget, text: str):
|
|
# ... (Codice da gui.py originale) ...
|
|
# Assicurati che la classe Tooltip sia importata correttamente
|
|
if widget and isinstance(widget, tk.Widget) and widget.winfo_exists():
|
|
Tooltip(widget, text)
|
|
|
|
def update_tooltip(self, widget: tk.Widget, text: str):
|
|
# ... (Codice da gui.py originale) ...
|
|
self.create_tooltip(widget, text)
|
|
|
|
def set_action_widgets_state(self, state: str):
|
|
# ... (Codice da gui.py originale, aggiornato per TreeView) ...
|
|
if state not in [tk.NORMAL, tk.DISABLED]:
|
|
log_handler.log_warning(
|
|
f"Invalid state requested for widgets: {state}",
|
|
func_name="set_action_widgets_state",
|
|
)
|
|
return
|
|
widgets_with_state_option = [
|
|
getattr(self, name, None)
|
|
for name in [
|
|
"save_settings_button",
|
|
"remove_profile_button",
|
|
"prepare_svn_button",
|
|
"create_bundle_button",
|
|
"fetch_bundle_button",
|
|
"edit_gitignore_button",
|
|
"manual_backup_button",
|
|
"autobackup_checkbox",
|
|
"backup_dir_entry",
|
|
"backup_dir_button",
|
|
"commit_button",
|
|
"refresh_changes_button",
|
|
"commit_message_text",
|
|
"autocommit_checkbox",
|
|
"refresh_tags_button",
|
|
"create_tag_button",
|
|
"checkout_tag_button",
|
|
"refresh_branches_button",
|
|
"create_branch_button",
|
|
"checkout_branch_button",
|
|
"history_branch_filter_combo",
|
|
"refresh_history_button",
|
|
"apply_remote_config_button",
|
|
"check_auth_button",
|
|
"fetch_button",
|
|
"pull_button",
|
|
"push_button",
|
|
"push_tags_button",
|
|
"refresh_sync_status_button",
|
|
"refresh_remote_branches_button",
|
|
"refresh_local_branches_button_remote_tab",
|
|
]
|
|
]
|
|
log_handler.log_debug(
|
|
f"Setting {len(widgets_with_state_option)} action widgets state to: {state}",
|
|
func_name="set_action_widgets_state",
|
|
)
|
|
failed_widgets = []
|
|
for widget in widgets_with_state_option:
|
|
if not widget or not widget.winfo_exists():
|
|
continue
|
|
widget_attr_name = None
|
|
for attr, value in self.__dict__.items():
|
|
if value is widget:
|
|
widget_attr_name = attr
|
|
break
|
|
try:
|
|
w_state = state
|
|
if isinstance(widget, ttk.Combobox):
|
|
w_state = "readonly" if state == tk.NORMAL else tk.DISABLED
|
|
elif isinstance(widget, (tk.Entry, tk.Text, scrolledtext.ScrolledText)):
|
|
w_state = state
|
|
elif isinstance(widget, (ttk.Button, ttk.Checkbutton)):
|
|
w_state = state
|
|
widget.config(state=w_state)
|
|
except Exception as e:
|
|
failed_widgets.append(f"{widget_attr_name or 'UnknownWidget'}: {e}")
|
|
if failed_widgets:
|
|
log_handler.log_error(
|
|
f"Error setting state for some widgets: {'; '.join(failed_widgets)}",
|
|
func_name="set_action_widgets_state",
|
|
)
|
|
self.toggle_backup_dir()
|
|
profile_dropdown = getattr(self, "profile_dropdown", None)
|
|
if profile_dropdown and profile_dropdown.winfo_exists():
|
|
try:
|
|
profile_dropdown.config(state="readonly")
|
|
except Exception as e:
|
|
log_handler.log_error(
|
|
f"Error setting state for profile dropdown: {e}",
|
|
func_name="set_action_widgets_state",
|
|
)
|
|
list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED
|
|
interactive_lists = [
|
|
getattr(self, name, None)
|
|
for name in [
|
|
"tag_listbox",
|
|
"branch_listbox",
|
|
"changed_files_listbox",
|
|
"remote_branches_listbox",
|
|
"local_branches_listbox_remote_tab",
|
|
]
|
|
]
|
|
for lb in interactive_lists:
|
|
if lb and lb.winfo_exists() and isinstance(lb, tk.Listbox):
|
|
try:
|
|
lb.config(state=list_state)
|
|
except Exception:
|
|
pass
|
|
history_tree = getattr(self, "history_tree", None)
|
|
if history_tree and history_tree.winfo_exists():
|
|
try:
|
|
if state == tk.DISABLED:
|
|
history_tree.unbind("<Double-Button-1>")
|
|
else:
|
|
if hasattr(self, "_on_history_double_click_tree"):
|
|
history_tree.bind(
|
|
"<Double-Button-1>", self._on_history_double_click_tree
|
|
)
|
|
except Exception as e:
|
|
log_handler.log_error(
|
|
f"Error configuring history tree bindings: {e}",
|
|
func_name="set_action_widgets_state",
|
|
)
|
|
|
|
def update_ahead_behind_status(
|
|
self,
|
|
current_branch: Optional[str] = None,
|
|
status_text: Optional[str] = None,
|
|
ahead: Optional[int] = None,
|
|
behind: Optional[int] = None,
|
|
):
|
|
# ... (Codice da gui.py originale) ...
|
|
label = getattr(self, "sync_status_label", None)
|
|
var = getattr(self, "remote_ahead_behind_var", None)
|
|
if not label or not var or not label.winfo_exists():
|
|
return
|
|
if current_branch:
|
|
branch_part = f"Branch '{current_branch}': "
|
|
else:
|
|
branch_part = "Current Branch: "
|
|
status_part = "Unknown"
|
|
if status_text is not None:
|
|
status_part = status_text
|
|
if (
|
|
"Branch" in status_part
|
|
or "Detached" in status_part
|
|
or "Upstream" in status_part
|
|
):
|
|
text_to_display = status_part
|
|
else:
|
|
text_to_display = branch_part + status_part
|
|
elif ahead is not None and behind is not None:
|
|
if ahead == 0 and behind == 0:
|
|
status_part = "Up to date"
|
|
else:
|
|
parts = []
|
|
if ahead > 0:
|
|
plural_a = "s" if ahead > 1 else ""
|
|
parts.append(f"{ahead} commit{plural_a} ahead (Push needed)")
|
|
if behind > 0:
|
|
plural_b = "s" if behind > 1 else ""
|
|
parts.append(f"{behind} commit{plural_b} behind (Pull needed)")
|
|
status_part = ", ".join(parts)
|
|
text_to_display = branch_part + status_part
|
|
else:
|
|
text_to_display = branch_part + "Unknown Status"
|
|
try:
|
|
var.set(text_to_display)
|
|
except Exception as e:
|
|
log_handler.log_error(
|
|
f"Failed to update sync status variable: {e}",
|
|
func_name="update_ahead_behind_status",
|
|
)
|
|
|
|
def update_remote_branches_list(self, remote_branch_list: List[str]):
|
|
# ... (Codice da gui.py originale) ...
|
|
listbox = getattr(self, "remote_branches_listbox", None)
|
|
if not listbox or not listbox.winfo_exists():
|
|
log_handler.log_error(
|
|
"remote_branches_listbox not available for update.",
|
|
func_name="update_remote_branches_list",
|
|
)
|
|
return
|
|
try:
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.delete(0, tk.END)
|
|
if remote_branch_list and remote_branch_list != ["(Error)"]:
|
|
try:
|
|
default_fg = self.style.lookup("TListbox", "foreground")
|
|
if listbox.cget("fg") != default_fg:
|
|
listbox.config(fg=default_fg)
|
|
except tk.TclError:
|
|
pass
|
|
for branch_name in remote_branch_list:
|
|
listbox.insert(tk.END, f" {branch_name}")
|
|
elif remote_branch_list == ["(Error)"]:
|
|
listbox.insert(tk.END, "(Error retrieving list)")
|
|
listbox.config(fg="red")
|
|
else:
|
|
listbox.insert(tk.END, "(No remote branches found)")
|
|
listbox.config(fg="grey")
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.yview_moveto(0.0)
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Error updating remote branches list GUI: {e}",
|
|
func_name="update_remote_branches_list",
|
|
)
|
|
try:
|
|
if listbox.winfo_exists():
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.delete(0, tk.END)
|
|
listbox.insert(tk.END, "(Error updating list)")
|
|
listbox.config(fg="red", state=tk.DISABLED)
|
|
except Exception:
|
|
pass
|
|
|
|
def _update_auth_status_indicator(self, status: str):
|
|
# ... (Codice da gui.py originale) ...
|
|
label = getattr(self, "auth_status_indicator_label", None)
|
|
if not label or not label.winfo_exists():
|
|
return
|
|
text = "Status: Unknown"
|
|
color = MainFrame.STATUS_DEFAULT_BG
|
|
tooltip = "Connection and authentication status."
|
|
if status == "ok":
|
|
text = "Status: Connected"
|
|
color = self.STATUS_GREEN
|
|
tooltip = "Successfully connected and authenticated to the remote."
|
|
elif status == "required":
|
|
text = "Status: Auth Required"
|
|
color = self.STATUS_YELLOW
|
|
tooltip = "Authentication needed. Use 'Check Connection' to attempt interactive login."
|
|
elif status == "failed":
|
|
text = "Status: Auth Failed"
|
|
color = self.STATUS_RED
|
|
tooltip = "Authentication failed. Check credentials or use 'Check Connection' to retry."
|
|
elif status == "connection_failed":
|
|
text = "Status: Connection Failed"
|
|
color = self.STATUS_RED
|
|
tooltip = "Could not connect to the remote. Check URL and network."
|
|
elif status == "unknown_error":
|
|
text = "Status: Error"
|
|
color = self.STATUS_RED
|
|
tooltip = "An unknown error occurred while checking the remote."
|
|
elif status == "checking":
|
|
text = "Status: Checking..."
|
|
color = self.STATUS_YELLOW
|
|
tooltip = "Attempting to contact the remote repository..." # Aggiunto stato checking
|
|
try:
|
|
self.remote_auth_status_var.set(text)
|
|
label.config(background=color)
|
|
self.update_tooltip(label, tooltip)
|
|
except Exception as e:
|
|
log_handler.log_error(
|
|
f"Failed to update auth status indicator GUI: {e}",
|
|
func_name="_update_auth_status_indicator",
|
|
)
|
|
|
|
def _show_remote_branches_context_menu(self, event: tk.Event) -> None:
|
|
# ... (Codice da gui.py originale) ...
|
|
func_name = "_show_remote_branches_context_menu"
|
|
listbox = event.widget
|
|
selected_index = None
|
|
try:
|
|
selected_index = listbox.nearest(event.y)
|
|
if selected_index < 0:
|
|
return
|
|
listbox.selection_clear(0, tk.END)
|
|
listbox.selection_set(selected_index)
|
|
listbox.activate(selected_index)
|
|
selected_item_text = listbox.get(selected_index).strip()
|
|
self.remote_branch_context_menu.delete(0, tk.END)
|
|
is_valid_branch = (
|
|
"/" in selected_item_text and not selected_item_text.startswith("(")
|
|
)
|
|
if is_valid_branch:
|
|
remote_branch_full_name = selected_item_text
|
|
slash_index = remote_branch_full_name.find("/")
|
|
local_branch_suggestion = (
|
|
remote_branch_full_name[slash_index + 1 :]
|
|
if slash_index != -1
|
|
else ""
|
|
)
|
|
current_branch_name_local = self.current_local_branch
|
|
compare_state = tk.NORMAL
|
|
compare_label = f"Compare '{remote_branch_full_name}' with current..."
|
|
if current_branch_name_local:
|
|
compare_label = f"Compare '{remote_branch_full_name}' with current '{current_branch_name_local}'"
|
|
self.remote_branch_context_menu.add_command(
|
|
label=compare_label,
|
|
state=compare_state,
|
|
command=lambda b_other=remote_branch_full_name: (
|
|
self.compare_branch_with_current_callback(b_other)
|
|
if callable(self.compare_branch_with_current_callback)
|
|
else None
|
|
),
|
|
)
|
|
if local_branch_suggestion:
|
|
self.remote_branch_context_menu.add_command(
|
|
label=f"Checkout as new local branch '{local_branch_suggestion}'",
|
|
command=lambda rb=remote_branch_full_name, lb=local_branch_suggestion: (
|
|
self.checkout_remote_branch_callback(rb, lb)
|
|
if callable(self.checkout_remote_branch_callback)
|
|
else None
|
|
),
|
|
)
|
|
self.remote_branch_context_menu.add_separator()
|
|
self.remote_branch_context_menu.add_command(label="Cancel")
|
|
else:
|
|
self.remote_branch_context_menu.add_command(
|
|
label="(No actions available)", state=tk.DISABLED
|
|
)
|
|
self.remote_branch_context_menu.tk_popup(event.x_root, event.y_root)
|
|
except tk.TclError:
|
|
log_handler.log_debug(
|
|
"TclError during remote branch context menu display.",
|
|
func_name=func_name,
|
|
)
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Error showing remote branch context menu: {e}", func_name=func_name
|
|
)
|
|
finally:
|
|
if hasattr(self, "remote_branch_context_menu"):
|
|
self.remote_branch_context_menu.grab_release()
|
|
|
|
def _show_local_branches_context_menu(self, event: tk.Event) -> None:
|
|
# ... (Codice da gui.py originale) ...
|
|
func_name = "_show_local_branches_context_menu"
|
|
listbox = event.widget
|
|
selected_index = None
|
|
current_branch_name_local = self.current_local_branch # Usa variabile membro
|
|
try:
|
|
selected_index = listbox.nearest(event.y)
|
|
if selected_index < 0:
|
|
return
|
|
listbox.selection_clear(0, tk.END)
|
|
listbox.selection_set(selected_index)
|
|
listbox.activate(selected_index)
|
|
selected_item_text = listbox.get(selected_index).strip()
|
|
self.local_branch_context_menu.delete(0, tk.END)
|
|
is_current_selected = selected_item_text.startswith("*")
|
|
local_branch_name_selected = selected_item_text.lstrip("* ").strip()
|
|
is_valid_branch = not local_branch_name_selected.startswith("(")
|
|
if is_valid_branch:
|
|
checkout_state = tk.DISABLED if is_current_selected else tk.NORMAL
|
|
self.local_branch_context_menu.add_command(
|
|
label=f"Checkout Branch '{local_branch_name_selected}'",
|
|
state=checkout_state,
|
|
command=lambda b=local_branch_name_selected: (
|
|
self.checkout_branch_callback(branch_to_checkout=b)
|
|
if callable(self.checkout_branch_callback)
|
|
else None
|
|
),
|
|
)
|
|
merge_state = tk.DISABLED
|
|
merge_label = f"Merge '{local_branch_name_selected}' into current..."
|
|
if not is_current_selected and current_branch_name_local:
|
|
merge_state = tk.NORMAL
|
|
merge_label = f"Merge '{local_branch_name_selected}' into current '{current_branch_name_local}'"
|
|
self.local_branch_context_menu.add_command(
|
|
label=merge_label,
|
|
state=merge_state,
|
|
command=lambda b_source=local_branch_name_selected: (
|
|
self.merge_local_branch_callback(b_source)
|
|
if callable(self.merge_local_branch_callback)
|
|
else None
|
|
),
|
|
)
|
|
delete_state = tk.DISABLED if is_current_selected else tk.NORMAL
|
|
self.local_branch_context_menu.add_command(
|
|
label=f"Delete Branch '{local_branch_name_selected}'...",
|
|
state=delete_state,
|
|
command=lambda b=local_branch_name_selected: (
|
|
self.delete_local_branch_callback(b, force=False)
|
|
if callable(self.delete_local_branch_callback)
|
|
else None
|
|
),
|
|
)
|
|
self.local_branch_context_menu.add_command(
|
|
label=f"Force Delete Branch '{local_branch_name_selected}'...",
|
|
state=delete_state,
|
|
command=lambda b=local_branch_name_selected: (
|
|
self.delete_local_branch_callback(b, force=True)
|
|
if callable(self.delete_local_branch_callback)
|
|
else None
|
|
),
|
|
)
|
|
compare_state = tk.DISABLED if is_current_selected else tk.NORMAL
|
|
compare_label = (
|
|
f"Compare '{local_branch_name_selected}' with current..."
|
|
)
|
|
if current_branch_name_local and not is_current_selected:
|
|
compare_label = f"Compare '{local_branch_name_selected}' with current '{current_branch_name_local}'"
|
|
self.local_branch_context_menu.add_command(
|
|
label=compare_label,
|
|
state=compare_state,
|
|
command=lambda b_other=local_branch_name_selected: (
|
|
self.compare_branch_with_current_callback(b_other)
|
|
if callable(self.compare_branch_with_current_callback)
|
|
else None
|
|
),
|
|
)
|
|
self.local_branch_context_menu.add_separator()
|
|
self.local_branch_context_menu.add_command(label="Cancel")
|
|
else:
|
|
self.local_branch_context_menu.add_command(
|
|
label="(No actions available)", state=tk.DISABLED
|
|
)
|
|
self.local_branch_context_menu.tk_popup(event.x_root, event.y_root)
|
|
except tk.TclError:
|
|
log_handler.log_debug(
|
|
"TclError during local branch context menu display.",
|
|
func_name=func_name,
|
|
)
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Error showing local branch context menu: {e}", func_name=func_name
|
|
)
|
|
finally:
|
|
if hasattr(self, "local_branch_context_menu"):
|
|
self.local_branch_context_menu.grab_release()
|
|
|
|
|
|
# --- END OF FILE gitsync_tool/gui/main_frame.py ---
|