2343 lines
104 KiB
Python
2343 lines
104 KiB
Python
# --- START OF FILE gui.py ---
|
|
|
|
# gui.py
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from tkinter import scrolledtext, filedialog, messagebox, simpledialog
|
|
|
|
# Rimosso import logging
|
|
import os
|
|
import re # Needed for validation in dialogs
|
|
import sys # Per fallback print
|
|
|
|
# Importa il gestore della coda log (anche se gui.py non logga molto direttamente)
|
|
import log_handler
|
|
from typing import Tuple, Dict, List, Callable
|
|
|
|
# Import constant from the central location if available
|
|
try:
|
|
from config_manager import DEFAULT_BACKUP_DIR, DEFAULT_REMOTE_NAME
|
|
except ImportError:
|
|
DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "backup_fallback")
|
|
DEFAULT_REMOTE_NAME = "origin" # Fallback per il nome del remote
|
|
# Usa print qui perché il sistema di log potrebbe non essere inizializzato
|
|
print(
|
|
f"WARNING: gui.py could not import constants. Using fallbacks.",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
|
|
# --- Tooltip Class Definition (invariata rispetto all'ultima versione, non logga) ---
|
|
class Tooltip:
|
|
"""Simple tooltip implementation for Tkinter widgets."""
|
|
|
|
def __init__(self, widget, text):
|
|
self.widget = widget
|
|
self.text = text
|
|
self.tooltip_window = None
|
|
self.id = None
|
|
if self.widget and self.widget.winfo_exists():
|
|
self.widget.bind("<Enter>", self.enter, add="+")
|
|
self.widget.bind("<Leave>", self.leave, add="+")
|
|
self.widget.bind("<ButtonPress>", self.leave, add="+")
|
|
|
|
def enter(self, event=None):
|
|
self.unschedule()
|
|
id = None
|
|
# PEP8 Fix: assignment on separate line
|
|
|
|
def leave(self, event=None):
|
|
self.unschedule()
|
|
self.hidetip()
|
|
|
|
def unschedule(self):
|
|
id_to_cancel = self.id
|
|
self.id = None
|
|
if id_to_cancel:
|
|
try:
|
|
if self.widget and self.widget.winfo_exists():
|
|
self.widget.after_cancel(id_to_cancel)
|
|
except Exception:
|
|
pass
|
|
|
|
def showtip(self):
|
|
if not self.widget or not self.widget.winfo_exists():
|
|
return
|
|
self.hidetip()
|
|
x_cursor = 0
|
|
y_cursor = 0 # Init vars
|
|
try:
|
|
x_cursor = self.widget.winfo_pointerx() + 15
|
|
y_cursor = self.widget.winfo_pointery() + 10
|
|
except Exception:
|
|
try:
|
|
x_root = self.widget.winfo_rootx()
|
|
y_root = self.widget.winfo_rooty()
|
|
x_cursor = x_root + self.widget.winfo_width() // 2
|
|
y_cursor = y_root + self.widget.winfo_height() + 5
|
|
except Exception:
|
|
return
|
|
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
|
tw.wm_overrideredirect(True)
|
|
try:
|
|
tw.wm_geometry(f"+{int(x_cursor)}+{int(y_cursor)}")
|
|
except tk.TclError:
|
|
tw.destroy()
|
|
self.tooltip_window = None
|
|
return
|
|
label = tk.Label(
|
|
tw,
|
|
text=self.text,
|
|
justify=tk.LEFT,
|
|
background="#ffffe0",
|
|
relief=tk.SOLID,
|
|
borderwidth=1,
|
|
font=("tahoma", "8", "normal"),
|
|
wraplength=350,
|
|
)
|
|
label.pack(ipadx=3, ipady=3)
|
|
|
|
def hidetip(self):
|
|
tw = self.tooltip_window
|
|
self.tooltip_window = None
|
|
if tw:
|
|
try:
|
|
if tw.winfo_exists():
|
|
tw.destroy()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# --- Gitignore Editor Window Class ---
|
|
class GitignoreEditorWindow(tk.Toplevel):
|
|
"""Toplevel window for editing the .gitignore file. Uses log_handler."""
|
|
|
|
# Rimosso logger da __init__ e fallback
|
|
def __init__(
|
|
self, master, gitignore_path, logger_ignored=None, on_save_success_callback=None
|
|
):
|
|
"""Initialize the Gitignore Editor window."""
|
|
super().__init__(master)
|
|
self.gitignore_path = gitignore_path
|
|
# Non c'è più self.logger
|
|
self.original_content = ""
|
|
self.on_save_success_callback = on_save_success_callback
|
|
|
|
self.title(f"Edit {os.path.basename(gitignore_path)}")
|
|
self.geometry("600x450")
|
|
self.minsize(400, 300)
|
|
self.grab_set()
|
|
self.transient(master)
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
main_frame.rowconfigure(0, weight=1)
|
|
main_frame.columnconfigure(0, weight=1)
|
|
self.text_editor = scrolledtext.ScrolledText(
|
|
main_frame,
|
|
wrap=tk.WORD,
|
|
font=("Consolas", 10),
|
|
undo=True,
|
|
padx=5,
|
|
pady=5,
|
|
borderwidth=1,
|
|
relief=tk.SUNKEN,
|
|
)
|
|
self.text_editor.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
|
|
button_frame = ttk.Frame(main_frame)
|
|
button_frame.grid(row=1, column=0, sticky="ew")
|
|
button_frame.columnconfigure(0, weight=1) # Push buttons right
|
|
self.save_button = ttk.Button(
|
|
button_frame, text="Save and Close", command=self._save_and_close
|
|
)
|
|
self.save_button.grid(row=0, column=2, padx=5)
|
|
self.cancel_button = ttk.Button(
|
|
button_frame, text="Cancel", command=self._on_close
|
|
)
|
|
self.cancel_button.grid(row=0, column=1, padx=5)
|
|
|
|
self._load_file()
|
|
self._center_window(master)
|
|
self.wait_window()
|
|
|
|
def _center_window(self, parent):
|
|
"""Centers the Toplevel window relative to its parent."""
|
|
func_name = "_center_window (Gitignore)"
|
|
try:
|
|
self.update_idletasks()
|
|
px = parent.winfo_rootx()
|
|
py = parent.winfo_rooty()
|
|
pw = parent.winfo_width()
|
|
ph = parent.winfo_height()
|
|
ww = self.winfo_width()
|
|
wh = self.winfo_height()
|
|
x = px + (pw // 2) - (ww // 2)
|
|
y = py + (ph // 2) - (wh // 2)
|
|
sw = self.winfo_screenwidth()
|
|
sh = self.winfo_screenheight()
|
|
x = max(0, min(x, sw - ww))
|
|
y = max(0, min(y, sh - wh))
|
|
self.geometry(f"+{int(x)}+{int(y)}")
|
|
except Exception as e:
|
|
# Usa log_handler se possibile, altrimenti print
|
|
try:
|
|
log_handler.log_error(
|
|
f"Could not center GitignoreEditor: {e}", func_name=func_name
|
|
)
|
|
except NameError:
|
|
print(f"ERROR: Could not center GitignoreEditor: {e}", file=sys.stderr)
|
|
|
|
def _load_file(self):
|
|
"""Loads the content of the .gitignore file. Uses log_handler."""
|
|
func_name = "_load_file (Gitignore)"
|
|
log_handler.log_info(
|
|
f"Loading gitignore: {self.gitignore_path}", func_name=func_name
|
|
)
|
|
content = ""
|
|
try:
|
|
if os.path.exists(self.gitignore_path):
|
|
with open(
|
|
self.gitignore_path, "r", encoding="utf-8", errors="replace"
|
|
) as f:
|
|
content = f.read()
|
|
else:
|
|
log_handler.log_info(
|
|
f".gitignore does not exist at: {self.gitignore_path}",
|
|
func_name=func_name,
|
|
)
|
|
self.original_content = content
|
|
self.text_editor.config(state=tk.NORMAL)
|
|
self.text_editor.delete("1.0", tk.END)
|
|
self.text_editor.insert(tk.END, self.original_content)
|
|
self.text_editor.edit_reset()
|
|
self.text_editor.focus_set()
|
|
except IOError as e:
|
|
log_handler.log_error(
|
|
f"I/O error loading .gitignore: {e}", func_name=func_name
|
|
)
|
|
messagebox.showerror(
|
|
"Load Error", f"Error reading .gitignore:\n{e}", parent=self
|
|
)
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Unexpected error loading .gitignore: {e}", func_name=func_name
|
|
)
|
|
messagebox.showerror(
|
|
"Load Error", f"Unexpected error loading:\n{e}", parent=self
|
|
)
|
|
self.text_editor.config(state=tk.DISABLED)
|
|
|
|
def _has_changes(self):
|
|
"""Checks if the editor content differs from the original."""
|
|
try:
|
|
return self.text_editor.get("1.0", "end-1c") != self.original_content
|
|
except Exception:
|
|
return True # Assume changes on error
|
|
|
|
def _save_file(self):
|
|
"""Saves the current content to the .gitignore file. Uses log_handler."""
|
|
func_name = "_save_file (Gitignore)"
|
|
if not self._has_changes():
|
|
log_handler.log_info(
|
|
"No changes to save in .gitignore.", func_name=func_name
|
|
)
|
|
return True
|
|
current_content = self.text_editor.get("1.0", "end-1c")
|
|
log_handler.log_info(
|
|
f"Saving changes to: {self.gitignore_path}", func_name=func_name
|
|
)
|
|
try:
|
|
with open(self.gitignore_path, "w", encoding="utf-8", newline="\n") as f:
|
|
f.write(current_content)
|
|
log_handler.log_info(".gitignore saved successfully.", func_name=func_name)
|
|
self.original_content = current_content
|
|
self.text_editor.edit_modified(False)
|
|
return True
|
|
except IOError as e:
|
|
log_handler.log_error(
|
|
f"I/O error saving .gitignore: {e}", func_name=func_name
|
|
)
|
|
messagebox.showerror(
|
|
"Save Error", f"Error writing .gitignore:\n{e}", parent=self
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Unexpected error saving .gitignore: {e}", func_name=func_name
|
|
)
|
|
messagebox.showerror(
|
|
"Save Error", f"Unexpected error saving:\n{e}", parent=self
|
|
)
|
|
return False
|
|
|
|
def _save_and_close(self):
|
|
"""Saves the file and closes the window, calling callback on success."""
|
|
func_name = "_save_and_close (Gitignore)"
|
|
save_successful = self._save_file()
|
|
if save_successful:
|
|
log_handler.log_debug(
|
|
"Save successful, attempting callback.", func_name=func_name
|
|
)
|
|
if self.on_save_success_callback and callable(
|
|
self.on_save_success_callback
|
|
):
|
|
try:
|
|
self.on_save_success_callback()
|
|
except Exception as cb_e:
|
|
log_handler.log_exception(
|
|
f"Error in on_save_success_callback: {cb_e}",
|
|
func_name=func_name,
|
|
)
|
|
messagebox.showwarning(
|
|
"Callback Error",
|
|
"Saved, but post-save action failed.\nCheck logs.",
|
|
parent=self,
|
|
)
|
|
self.destroy() # Close window
|
|
|
|
def _on_close(self):
|
|
"""Handles window close event (X or Cancel button)."""
|
|
func_name = "_on_close (Gitignore)"
|
|
if self._has_changes():
|
|
res = messagebox.askyesnocancel(
|
|
"Unsaved Changes", "Save changes?", parent=self
|
|
)
|
|
if res is True:
|
|
self._save_and_close() # Yes - Save
|
|
elif res is False:
|
|
log_handler.log_warning(
|
|
"Discarding .gitignore changes.", func_name=func_name
|
|
)
|
|
self.destroy() # No - Discard
|
|
# else: Cancel - do nothing
|
|
else:
|
|
self.destroy() # No changes - just close
|
|
|
|
|
|
# --- Create Tag Dialog (invariata, non logga) ---
|
|
class CreateTagDialog(simpledialog.Dialog):
|
|
"""Dialog to get tag name and message from the user."""
|
|
|
|
def __init__(self, parent, title="Create New Tag", suggested_tag_name=""):
|
|
self.tag_name_var = tk.StringVar()
|
|
self.tag_message_var = tk.StringVar()
|
|
self.result = None
|
|
self.suggested_tag_name = suggested_tag_name
|
|
super().__init__(parent, title=title)
|
|
|
|
def body(self, master):
|
|
frame = ttk.Frame(master, padding="10")
|
|
frame.pack(fill="x")
|
|
frame.columnconfigure(1, weight=1)
|
|
ttk.Label(frame, text="Tag Name:").grid(
|
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.name_entry = ttk.Entry(frame, textvariable=self.tag_name_var, width=40)
|
|
self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
|
if self.suggested_tag_name:
|
|
self.tag_name_var.set(self.suggested_tag_name)
|
|
ttk.Label(frame, text="Tag Message:").grid(
|
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.message_entry = ttk.Entry(
|
|
frame, textvariable=self.tag_message_var, width=40
|
|
)
|
|
self.message_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
|
|
return self.name_entry
|
|
|
|
def validate(self):
|
|
name = self.tag_name_var.get().strip()
|
|
msg = self.tag_message_var.get().strip()
|
|
if not name:
|
|
messagebox.showwarning("Input Error", "Tag name empty.", parent=self)
|
|
return 0
|
|
if not msg:
|
|
messagebox.showwarning("Input Error", "Tag message empty.", parent=self)
|
|
return 0
|
|
# GitCommands handles detailed name validation
|
|
return 1
|
|
|
|
def apply(self):
|
|
self.result = (
|
|
self.tag_name_var.get().strip(),
|
|
self.tag_message_var.get().strip(),
|
|
)
|
|
|
|
|
|
# --- Create Branch Dialog (invariata, non logga) ---
|
|
class CreateBranchDialog(simpledialog.Dialog):
|
|
"""Dialog to get a new branch name from the user."""
|
|
|
|
def __init__(self, parent, title="Create New Branch"):
|
|
self.branch_name_var = tk.StringVar()
|
|
self.result = None
|
|
super().__init__(parent, title=title)
|
|
|
|
def body(self, master):
|
|
frame = ttk.Frame(master, padding="10")
|
|
frame.pack(fill="x")
|
|
frame.columnconfigure(1, weight=1)
|
|
ttk.Label(frame, text="New Branch Name:").grid(
|
|
row=0, column=0, padx=5, pady=10, sticky="w"
|
|
)
|
|
self.name_entry = ttk.Entry(frame, textvariable=self.branch_name_var, width=40)
|
|
self.name_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew")
|
|
return self.name_entry
|
|
|
|
def validate(self):
|
|
name = self.branch_name_var.get().strip()
|
|
if not name:
|
|
messagebox.showwarning("Input Error", "Branch name empty.", parent=self)
|
|
return 0
|
|
pattern = (
|
|
r"^(?!\.| |.*[/.]\.|\.|.*\\|.*@\{|.*[/]$|.*\.\.)[^ \t\n\r\f\v~^:?*[\\]+$"
|
|
)
|
|
if not re.match(pattern, name) or name.lower() == "head":
|
|
messagebox.showwarning(
|
|
"Input Error", "Invalid branch name format.", parent=self
|
|
)
|
|
return 0
|
|
return 1
|
|
|
|
def apply(self):
|
|
self.result = self.branch_name_var.get().strip()
|
|
|
|
|
|
# --- Main Application Frame ---
|
|
class MainFrame(ttk.Frame):
|
|
"""The main frame using ttk.Notebook. Does not log directly."""
|
|
|
|
GREEN = "#90EE90"
|
|
RED = "#F08080" # Color constants
|
|
# Aggiungi colori per status bar
|
|
STATUS_YELLOW = "#FFFACD" # Lemon Chiffon (giallo chiaro)
|
|
STATUS_RED = "#FFA07A" # Light Salmon (rosso/arancio chiaro)
|
|
STATUS_GREEN = "#98FB98" # Pale Green (verde chiaro)
|
|
STATUS_DEFAULT_BG = None # Per ripristinare il colore default del tema
|
|
|
|
def __init__(
|
|
self,
|
|
master,
|
|
# Callbacks Esistenti
|
|
load_profile_settings_cb,
|
|
browse_folder_cb,
|
|
update_svn_status_cb,
|
|
prepare_svn_for_git_cb,
|
|
create_git_bundle_cb,
|
|
fetch_from_git_bundle_cb,
|
|
config_manager_instance,
|
|
profile_sections_list,
|
|
add_profile_cb,
|
|
remove_profile_cb,
|
|
manual_backup_cb,
|
|
open_gitignore_editor_cb,
|
|
save_profile_cb,
|
|
commit_changes_cb,
|
|
refresh_tags_cb,
|
|
create_tag_cb,
|
|
checkout_tag_cb,
|
|
refresh_history_cb,
|
|
refresh_branches_cb, # Per lista branch locali (entrambe le tab)
|
|
checkout_branch_cb,
|
|
create_branch_cb,
|
|
refresh_changed_files_cb,
|
|
open_diff_viewer_cb,
|
|
add_selected_file_cb,
|
|
# Callbacks Remoti
|
|
apply_remote_config_cb,
|
|
check_connection_auth_cb,
|
|
fetch_remote_cb,
|
|
pull_remote_cb,
|
|
push_remote_cb,
|
|
push_tags_remote_cb,
|
|
refresh_remote_status_cb,
|
|
clone_remote_repo_cb,
|
|
refresh_remote_branches_cb, # Per lista branch remoti
|
|
checkout_remote_branch_cb, # Azione da menu contestuale remoto
|
|
delete_local_branch_cb: Callable[[str, bool], None], # Azione da menu contestuale locale
|
|
merge_local_branch_cb: Callable[[str], None], # Azione da menu contestuale locale
|
|
compare_branch_with_current_cb: Callable[[str], None],
|
|
# Aggiungere qui futuri callback (es. delete remote branch, compare branches, etc.)
|
|
):
|
|
"""Initializes the MainFrame."""
|
|
super().__init__(master)
|
|
self.master = 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 # Usato da entrambe le tab locali
|
|
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.config_manager = config_manager_instance
|
|
self.initial_profile_sections = profile_sections_list
|
|
|
|
# Configurazione stile
|
|
self.style = ttk.Style()
|
|
# (...) codice stile (...)
|
|
|
|
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")
|
|
|
|
# --- Creazione Menu Contestuali ---
|
|
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) # Menu esistente
|
|
|
|
# --- 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 qui può influenzare l'esistenza dei widget se riutilizzati)
|
|
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() # Crea la listbox/button originale
|
|
self.remote_tab_frame = self._create_remote_tab() # Crea la nuova tab con le liste affiancate
|
|
self.history_tab_frame = self._create_history_tab()
|
|
|
|
# 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) ") # Rinomina tab originale
|
|
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)
|
|
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(); self.STATUS_DEFAULT_BG = s.lookup('TLabel', 'background')
|
|
except tk.TclError: self.STATUS_DEFAULT_BG = self.status_bar.cget('background')
|
|
self.status_bar_var.set("Initializing...")
|
|
self._status_reset_timer = None
|
|
self.current_local_branch: str | None = None
|
|
|
|
# Initial State
|
|
self._initialize_profile_selection()
|
|
self.toggle_backup_dir()
|
|
# Lo stato iniziale dei widget verrà impostato da load_profile_settings -> update_svn_status_indicator
|
|
# self.update_status_bar("Ready.") # Impostato da _perform_initial_load
|
|
|
|
# --- Frame Creation Methods (_create_profile_frame, _create_repo_tab, etc.) ---
|
|
# (Questi metodi rimangono invariati rispetto all'ultima versione valida,
|
|
# non contengono chiamate dirette al logger. Li ometto per brevità qui.)
|
|
# --- >> INCOLLA QUI I METODI _create_* DAL FILE GUI.PY PRECEDENTE << ---
|
|
def _create_profile_frame(self):
|
|
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 config profile.")
|
|
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 settings to 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 new profile.")
|
|
|
|
self.clone_profile_button = ttk.Button(
|
|
button_subframe,
|
|
text="Clone from Remote", # Testo aggiornato
|
|
width=18, # Leggermente più largo
|
|
command=self.clone_remote_repo_callback, # Chiama il nuovo 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 selected profile.")
|
|
|
|
def _create_remote_tab(self):
|
|
"""Creates the widgets for the Remote Repository tab with new layout."""
|
|
# Frame principale per questa tab, usa padding standard
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
# Configura righe/colonne del frame principale per l'espansione
|
|
frame.columnconfigure(0, weight=1) # Colonna principale (sinistra) si espande
|
|
frame.columnconfigure(1, weight=1) # Colonna destra (per lista locali) si espande
|
|
# La riga 3 (contenente le liste di branch) si espanderà verticalmente
|
|
frame.rowconfigure(3, weight=1)
|
|
|
|
# --- Frame Superiore: Configurazione e Stato Sync ---
|
|
top_frame = ttk.LabelFrame(frame, text="Remote Configuration & Sync Status", padding=(10, 5))
|
|
# Posiziona usando grid sulla riga 0, occupando entrambe le colonne
|
|
top_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5,0))
|
|
top_frame.columnconfigure(1, weight=1) # Colonna delle Entry si espande
|
|
|
|
# Riga 0: URL
|
|
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://...).")
|
|
|
|
# Riga 1: Nome Locale
|
|
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}').")
|
|
|
|
# Riga 2: Label Sync Status (Ahead/Behind)
|
|
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.")
|
|
|
|
# Colonna 2 (del top_frame): Pulsanti Azione Verticali e Indicatore Auth
|
|
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') # Imposta stato iniziale
|
|
self.create_tooltip(self.auth_status_indicator_label, "Connection and authentication status.")
|
|
|
|
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 sync status (ahead/behind) for the current branch.")
|
|
|
|
# --- Frame Azioni Standard ---
|
|
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() # Centra
|
|
|
|
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.")
|
|
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 and integrate changes into the current 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 remote.")
|
|
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.")
|
|
|
|
|
|
# --- Frame Inferiore: Liste Branch Affiancate ---
|
|
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) # Le righe delle liste si espandono
|
|
branch_view_frame.columnconfigure(0, weight=1) # Colonna sinistra si espande
|
|
branch_view_frame.columnconfigure(1, weight=1) # Colonna destra si espande
|
|
|
|
# Colonna Sinistra: Branch Remoti
|
|
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) # Riga della listbox si espande
|
|
remote_list_frame.columnconfigure(0, weight=1) # Listbox si espande
|
|
|
|
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 on the remote. Right-click for actions.")
|
|
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 this list (requires fetch).")
|
|
|
|
# Colonna Destra: Branch Locali (Nuovi Widget)
|
|
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) # Usa stesso menu dei locali
|
|
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, "Local branches. Right-click for actions.")
|
|
self.refresh_local_branches_button_remote_tab = ttk.Button(local_list_frame, text="Refresh Local List", command=self.refresh_branches_callback, state=tk.DISABLED) # Chiama refresh locale
|
|
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 _show_remote_branches_context_menu(self, event):
|
|
"""Displays the context menu for the remote branches listbox."""
|
|
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
|
|
# (...) Logica derivazione local_branch_suggestion (...)
|
|
slash_index = remote_branch_full_name.find('/')
|
|
local_branch_suggestion = remote_branch_full_name[slash_index + 1:] if slash_index != -1 else ""
|
|
|
|
|
|
# --- Opzione Compare ---
|
|
# ---<<< MODIFICA: Usa self.current_local_branch >>>---
|
|
current_branch_name_local = self.current_local_branch # Leggi dalla variabile membro
|
|
compare_state = tk.NORMAL # Sempre abilitato per remoti (a meno che non sia l'upstream?)
|
|
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}'"
|
|
# ---<<< FINE MODIFICA >>>---
|
|
|
|
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
|
|
)
|
|
|
|
# (...) Altre opzioni (Checkout as local...), Separatore, Cancel (...)
|
|
# Assicurati che anche checkout usi local_branch_suggestion
|
|
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)
|
|
|
|
# Mostra il menu
|
|
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):
|
|
"""Displays the context menu for the local branches listbox."""
|
|
func_name = "_show_local_branches_context_menu"
|
|
listbox = event.widget
|
|
selected_index = None
|
|
current_branch_name_local = None # Per sapere il nome del branch attivo
|
|
|
|
# Ottieni il branch CORRENTE (per abilitare/disabilitare opzioni)
|
|
# Potremmo prenderlo dalla listbox stessa cercando '*' o averlo da un refresh precedente
|
|
# Per sicurezza, proviamo a leggerlo dalla lista attuale
|
|
try:
|
|
items = listbox.get(0, tk.END)
|
|
for item in items:
|
|
if item.strip().startswith("*"):
|
|
current_branch_name_local = item.lstrip("* ").strip()
|
|
break
|
|
except Exception:
|
|
pass # Ignora se non riusciamo a trovarlo qui
|
|
|
|
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()
|
|
|
|
# Pulisci menu precedente
|
|
self.local_branch_context_menu.delete(0, tk.END)
|
|
|
|
# Estrai nome branch selezionato e verifica se è quello corrente
|
|
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:
|
|
# --- Opzione Checkout (Invariata) ---
|
|
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
|
|
)
|
|
|
|
# ---<<< NUOVA OPZIONE: Merge >>>---
|
|
# Abilitata solo se NON è il branch corrente E conosciamo il branch corrente
|
|
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) # Chiama nuovo callback
|
|
if callable(self.merge_local_branch_callback) else None
|
|
)
|
|
# ---<<< FINE NUOVA OPZIONE >>>---
|
|
|
|
|
|
# --- Opzione Delete (Invariata) ---
|
|
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
|
|
)
|
|
|
|
# --- Opzione Force Delete (Invariata) ---
|
|
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
|
|
)
|
|
|
|
# --- Opzione Compare ---
|
|
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}'"
|
|
|
|
# Assicurati che il command chiami il callback giusto
|
|
self.local_branch_context_menu.add_command(
|
|
label=compare_label,
|
|
state=compare_state,
|
|
command=lambda b_other=local_branch_name_selected: # Passa il branch selezionato
|
|
self.compare_branch_with_current_callback(b_other) # <-- VERIFICA QUESTA CHIAMATA
|
|
if callable(self.compare_branch_with_current_callback) else None
|
|
)
|
|
|
|
# --- Separatore e Cancel ---
|
|
self.local_branch_context_menu.add_separator()
|
|
self.local_branch_context_menu.add_command(label="Cancel")
|
|
|
|
else:
|
|
# Elemento non valido selezionato
|
|
self.local_branch_context_menu.add_command(label="(No actions available)", state=tk.DISABLED)
|
|
|
|
# Mostra il menu
|
|
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()
|
|
|
|
def update_remote_branches_list(self, remote_branch_list: List[str]):
|
|
"""Clears and populates the remote branches listbox."""
|
|
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)"]:
|
|
# Resetta colore se necessario
|
|
try:
|
|
default_fg = self.style.lookup("TListbox", "foreground")
|
|
if listbox.cget("fg") != default_fg:
|
|
listbox.config(fg=default_fg)
|
|
except tk.TclError:
|
|
pass
|
|
# Popola la lista
|
|
for branch_name in remote_branch_list:
|
|
listbox.insert(
|
|
tk.END, f" {branch_name}"
|
|
) # Aggiunge indentazione per leggibilità
|
|
elif remote_branch_list == ["(Error)"]:
|
|
listbox.insert(tk.END, "(Error retrieving list)")
|
|
listbox.config(fg="red")
|
|
else: # Lista vuota (o None)
|
|
listbox.insert(tk.END, "(No remote branches found)")
|
|
listbox.config(fg="grey")
|
|
|
|
listbox.config(state=tk.NORMAL) # Lascia selezionabile
|
|
listbox.yview_moveto(0.0) # Scrolla all'inizio
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Error updating remote branches list GUI: {e}",
|
|
func_name="update_remote_branches_list",
|
|
)
|
|
try: # Fallback visualizzazione errore
|
|
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 # Ignora errori nel fallback
|
|
|
|
def _update_auth_status_indicator(self, status: str):
|
|
"""Updates the text and background color of the auth status label."""
|
|
label = getattr(self, "auth_status_indicator_label", None)
|
|
if not label or not label.winfo_exists():
|
|
return
|
|
|
|
text = "Status: Unknown"
|
|
color = self.STATUS_DEFAULT_BG # Grigio/Default
|
|
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."
|
|
# else: status == 'unknown' -> usa i valori di default
|
|
|
|
try:
|
|
self.remote_auth_status_var.set(text)
|
|
label.config(background=color)
|
|
self.update_tooltip(label, tooltip) # Aggiorna anche il 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 _create_repo_tab(self):
|
|
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 # Column indices
|
|
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 Git repo.")
|
|
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, "Git repo status (Red=No, Green=Yes)"
|
|
)
|
|
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, "Dir for bundle files.")
|
|
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 for bundle creation.")
|
|
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 for bundle fetch."
|
|
)
|
|
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 & .gitignore.")
|
|
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 Git bundle file.")
|
|
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 & merge from bundle.")
|
|
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, "Edit .gitignore file.")
|
|
return frame
|
|
|
|
def _create_backup_tab(self):
|
|
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 # Column indices
|
|
self.autobackup_checkbox = ttk.Checkbutton(
|
|
config_frame,
|
|
text="Enable Auto Backup before Actions",
|
|
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, "Auto ZIP backup before bundle ops."
|
|
)
|
|
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, "Where backups are 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-sep extensions (.log,.tmp)"
|
|
)
|
|
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-sep dir names (__pycache__,build)"
|
|
)
|
|
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 ZIP backup now.")
|
|
return frame
|
|
|
|
def _create_commit_tab(self):
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.rowconfigure(3, weight=1)
|
|
frame.columnconfigure(0, weight=1) # Changes frame (row 3) expands
|
|
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, "Auto commit before create 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, "Commit message.")
|
|
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, "Changed files list (Double-click to diff)."
|
|
)
|
|
self.changed_files_context_menu = tk.Menu(
|
|
self.changed_files_listbox, tearoff=0
|
|
) # Context menu
|
|
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 file list.")
|
|
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)
|
|
self.create_tooltip(self.commit_button, "Commit staged changes manually.")
|
|
return frame
|
|
|
|
def _create_tags_tab(self):
|
|
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, "Select tag to checkout.")
|
|
button_frame = ttk.Frame(frame)
|
|
button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5))
|
|
bw = 18 # Button width
|
|
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 tag list.")
|
|
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 annotated tag.")
|
|
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 to tag (Detached HEAD).")
|
|
return frame
|
|
|
|
def _create_branch_tab(self):
|
|
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, "Select branch to checkout.")
|
|
button_frame = ttk.Frame(frame)
|
|
button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5))
|
|
bw = 18 # Button width
|
|
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 branch list.")
|
|
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 new local branch.")
|
|
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 to selected branch.")
|
|
return frame
|
|
|
|
def _create_history_tab(self):
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.rowconfigure(2, weight=1)
|
|
frame.columnconfigure(0, weight=1)
|
|
filter_frame = ttk.Frame(frame)
|
|
filter_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
|
filter_frame.columnconfigure(1, weight=1)
|
|
ttk.Label(filter_frame, text="Filter History:").pack(side=tk.LEFT, padx=(0, 5))
|
|
self.history_branch_filter_var = tk.StringVar()
|
|
self.history_branch_filter_combo = ttk.Combobox(
|
|
filter_frame,
|
|
textvariable=self.history_branch_filter_var,
|
|
state="readonly",
|
|
width=30,
|
|
)
|
|
self.history_branch_filter_combo.pack(
|
|
side=tk.LEFT, expand=True, fill=tk.X, padx=5
|
|
)
|
|
self.history_branch_filter_combo.bind(
|
|
"<<ComboboxSelected>>", lambda e: self.refresh_history_callback()
|
|
)
|
|
self.create_tooltip(
|
|
self.history_branch_filter_combo, "Filter history by branch/tag."
|
|
)
|
|
self.refresh_history_button = ttk.Button(
|
|
filter_frame,
|
|
text="Refresh History",
|
|
command=self.refresh_history_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.refresh_history_button.pack(side=tk.LEFT, padx=5)
|
|
self.create_tooltip(self.refresh_history_button, "Load commit history.")
|
|
ttk.Label(frame, text="Commit History (Recent First):").grid(
|
|
row=1, column=0, sticky="w", padx=5, pady=(5, 0)
|
|
)
|
|
self.history_text = scrolledtext.ScrolledText(
|
|
frame,
|
|
height=15,
|
|
width=100,
|
|
font=("Consolas", 9),
|
|
wrap=tk.NONE,
|
|
state=tk.DISABLED,
|
|
padx=5,
|
|
pady=5,
|
|
undo=False,
|
|
borderwidth=1,
|
|
relief=tk.SUNKEN,
|
|
)
|
|
self.history_text.grid(row=2, column=0, sticky="nsew", padx=5, pady=(0, 5))
|
|
history_xscroll = ttk.Scrollbar(
|
|
frame, orient=tk.HORIZONTAL, command=self.history_text.xview
|
|
)
|
|
history_xscroll.grid(row=3, column=0, sticky="ew", padx=5)
|
|
self.history_text.config(xscrollcommand=history_xscroll.set)
|
|
return frame
|
|
|
|
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):
|
|
if not hasattr(self, "config_manager"):
|
|
return
|
|
try:
|
|
from config_manager import DEFAULT_PROFILE
|
|
except ImportError:
|
|
DEFAULT_PROFILE = "default"
|
|
current_profiles = self.profile_dropdown.cget("values")
|
|
if not isinstance(current_profiles, (list, tuple)):
|
|
current_profiles = []
|
|
if DEFAULT_PROFILE in current_profiles:
|
|
self.profile_var.set(DEFAULT_PROFILE)
|
|
elif current_profiles:
|
|
self.profile_var.set(current_profiles[0])
|
|
else:
|
|
self.profile_var.set("")
|
|
self.update_status_bar("No profiles found.")
|
|
|
|
# --- GUI Update Methods (non logganti) ---
|
|
def toggle_backup_dir(self):
|
|
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):
|
|
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 Dir", parent=self.master
|
|
)
|
|
if sel:
|
|
self.backup_dir_var.set(sel)
|
|
|
|
def update_svn_indicator(self, is_prepared):
|
|
color = self.GREEN if is_prepared else self.RED
|
|
tip = "Prepared" if is_prepared else "Not prepared"
|
|
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)
|
|
|
|
def update_profile_dropdown(self, sections):
|
|
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:
|
|
try:
|
|
from config_manager import DEFAULT_PROFILE
|
|
except ImportError:
|
|
DEFAULT_PROFILE = "default"
|
|
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):
|
|
if not hasattr(self, "tag_listbox") or not self.tag_listbox.winfo_exists():
|
|
return
|
|
try:
|
|
self.tag_listbox.config(state=tk.NORMAL)
|
|
self.tag_listbox.delete(0, tk.END)
|
|
if tags_data:
|
|
try:
|
|
if self.tag_listbox.cget("fg") == "grey":
|
|
self.tag_listbox.config(
|
|
fg=self.style.lookup("TListbox", "foreground")
|
|
)
|
|
except tk.TclError:
|
|
pass
|
|
for name, subj in tags_data:
|
|
self.tag_listbox.insert(tk.END, f"{name}\t({subj})")
|
|
else:
|
|
self.tag_listbox.insert(tk.END, "(No tags found)")
|
|
self.tag_listbox.config(fg="grey")
|
|
self.tag_listbox.config(state=tk.NORMAL)
|
|
self.tag_listbox.yview_moveto(0.0)
|
|
except Exception as e: # Log to console as log_handler might not be available
|
|
print(f"ERROR updating tag list GUI: {e}", file=sys.stderr)
|
|
try:
|
|
self.tag_listbox.delete(0, tk.END)
|
|
self.tag_listbox.insert(tk.END, "(Error)")
|
|
self.tag_listbox.config(fg="red")
|
|
except:
|
|
pass
|
|
|
|
def update_branch_list(self, branches: List[str], current_branch: str | None):
|
|
"""Clears and populates the local branch listbox(es)."""
|
|
func_name = "update_branch_list (GUI)"
|
|
log_handler.log_debug(f"Received branches type={type(branches)}, count={len(branches) if isinstance(branches, list) else 'N/A'}, current={repr(current_branch)}", func_name=func_name)
|
|
|
|
self.current_local_branch = current_branch # Memorizza il branch corrente ricevuto
|
|
log_handler.log_debug(f"Stored current_local_branch: {self.current_local_branch}", func_name=func_name)
|
|
|
|
# Lista delle listbox dei branch LOCALI da aggiornare
|
|
listboxes_to_update = []
|
|
# Listbox nella tab "Branches" originale
|
|
if hasattr(self, "branch_listbox") and getattr(self, "branch_listbox") and self.branch_listbox.winfo_exists():
|
|
listboxes_to_update.append(self.branch_listbox)
|
|
# Listbox nella tab "Remote Repository"
|
|
if hasattr(self, "local_branches_listbox_remote_tab") and getattr(self, "local_branches_listbox_remote_tab") and self.local_branches_listbox_remote_tab.winfo_exists():
|
|
listboxes_to_update.append(self.local_branches_listbox_remote_tab)
|
|
|
|
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()) # Ottieni nome widget per log
|
|
except Exception: pass
|
|
|
|
try:
|
|
listbox.config(state=tk.NORMAL)
|
|
listbox.delete(0, tk.END)
|
|
sel_idx = -1
|
|
|
|
if isinstance(branches, list) and branches:
|
|
try: # Resetta colore
|
|
default_fg = self.style.lookup("TListbox", "foreground")
|
|
if listbox.cget("fg") != default_fg: listbox.config(fg=default_fg)
|
|
except tk.TclError: pass
|
|
# Popola lista
|
|
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) # Lascia selezionabile
|
|
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: # Fallback
|
|
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
|
|
# ---<<< FINE MODIFICA DEBUG & ERRORE >>>---
|
|
|
|
def get_selected_tag(self):
|
|
if hasattr(self, "tag_listbox") and self.tag_listbox.winfo_exists():
|
|
idx = self.tag_listbox.curselection()
|
|
if idx:
|
|
item = self.tag_listbox.get(idx[0])
|
|
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):
|
|
if hasattr(self, "branch_listbox") and self.branch_listbox.winfo_exists():
|
|
idx = self.branch_listbox.curselection()
|
|
if idx:
|
|
item = self.branch_listbox.get(idx[0])
|
|
name = item.lstrip("* ").strip()
|
|
if not name.startswith("("):
|
|
return name
|
|
return None
|
|
|
|
def get_commit_message(self):
|
|
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):
|
|
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 update_history_display(self, log_lines):
|
|
"""Clears and populates the history ScrolledText widget."""
|
|
func_name = "update_history_display (GUI)" # Nome specifico per i log
|
|
# ---<<< INIZIO MODIFICA DEBUG & ERRORE >>>---
|
|
log_handler.log_debug(
|
|
f"Received log_lines type={type(log_lines)}, count={len(log_lines) if isinstance(log_lines, list) else 'N/A'}. "
|
|
f"Sample: {repr(log_lines[:5]) if isinstance(log_lines, list) else repr(log_lines)}",
|
|
func_name=func_name,
|
|
)
|
|
history_widget = getattr(self, "history_text", None)
|
|
if not history_widget or not history_widget.winfo_exists():
|
|
log_handler.log_error(
|
|
"history_text widget not available for update.", func_name=func_name
|
|
)
|
|
return
|
|
|
|
try:
|
|
history_widget.config(state=tk.NORMAL)
|
|
history_widget.delete("1.0", tk.END)
|
|
|
|
# Assicurati che log_lines sia una lista prima di fare join
|
|
if isinstance(log_lines, list):
|
|
if log_lines:
|
|
# Resetta colore (se era rosso o altro)
|
|
try:
|
|
# Potrebbe non esserci un colore foreground specifico per ScrolledText nello stile
|
|
# Potremmo impostare a nero o lasciare il default del widget
|
|
default_fg = "black" # Assumiamo nero come default sicuro
|
|
if history_widget.cget("fg") != default_fg:
|
|
history_widget.config(fg=default_fg)
|
|
except tk.TclError:
|
|
pass
|
|
|
|
# Unisci le linee (assicurati siano stringhe)
|
|
text_to_insert = "\n".join(map(str, log_lines))
|
|
history_widget.insert(tk.END, text_to_insert)
|
|
else: # Lista vuota valida
|
|
history_widget.insert(tk.END, "(No history found)")
|
|
history_widget.config(fg="grey") # Colore grigio per indicare vuoto
|
|
else: # log_lines non è una lista (errore?)
|
|
log_handler.log_warning(
|
|
f"Invalid data received for history: {repr(log_lines)}",
|
|
func_name=func_name,
|
|
)
|
|
history_widget.insert(
|
|
tk.END, f"(Invalid data received: {repr(log_lines)})"
|
|
)
|
|
history_widget.config(fg="orange") # Arancione per dato inatteso
|
|
|
|
history_widget.config(state=tk.DISABLED) # Rendi read-only
|
|
history_widget.yview_moveto(0.0)
|
|
history_widget.xview_moveto(0.0)
|
|
|
|
except Exception as e:
|
|
log_handler.log_exception(
|
|
f"Error updating history GUI: {e}", func_name=func_name
|
|
)
|
|
# Fallback: Mostra errore nel widget di testo
|
|
try:
|
|
if history_widget.winfo_exists():
|
|
history_widget.config(state=tk.NORMAL)
|
|
history_widget.delete("1.0", tk.END)
|
|
history_widget.insert(tk.END, "(Error displaying history)")
|
|
history_widget.config(
|
|
state=tk.DISABLED, fg="red"
|
|
) # Rosso e disabilitato
|
|
except Exception as fallback_e:
|
|
log_handler.log_error(
|
|
f"Error displaying fallback error in history widget: {fallback_e}",
|
|
func_name=func_name,
|
|
)
|
|
# ---<<< FINE MODIFICA DEBUG & ERRORE >>>---
|
|
|
|
def update_history_branch_filter(self, branches_tags, current_ref=None):
|
|
if (
|
|
not hasattr(self, "history_branch_filter_combo")
|
|
or not self.history_branch_filter_combo.winfo_exists()
|
|
):
|
|
return
|
|
opts = ["-- All History --"] + sorted(branches_tags)
|
|
self.history_branch_filter_combo["values"] = opts
|
|
self.history_branch_filter_var.set(
|
|
current_ref if current_ref and current_ref in opts else opts[0]
|
|
)
|
|
|
|
def update_changed_files_list(self, files_status_list):
|
|
"""Clears and populates the changed files listbox, sanitizing input."""
|
|
# Usa la costante Listbox invece di hasattr ogni volta
|
|
listbox = getattr(self, "changed_files_listbox", None)
|
|
if not listbox or not listbox.winfo_exists():
|
|
# Logga se il widget non è disponibile (usa print come fallback qui)
|
|
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:
|
|
# Reset color
|
|
try:
|
|
if listbox.cget("fg") == "grey":
|
|
listbox.config(fg=self.style.lookup("TListbox", "foreground"))
|
|
except tk.TclError:
|
|
pass
|
|
|
|
# <<< MODIFICA: Sanifica ogni riga prima di inserirla >>>
|
|
processed_lines = 0
|
|
for status_line in files_status_list:
|
|
try:
|
|
# Assicura sia una stringa e rimuovi caratteri potenzialmente problematici
|
|
# (es. NUL residuo, anche se split dovrebbe averlo rimosso)
|
|
# Potremmo usare una regex più complessa per rimuovere tutti i controlli C0/C1
|
|
# ma iniziamo con una pulizia base.
|
|
sanitized_line = str(status_line).replace("\x00", "").strip()
|
|
if sanitized_line: # Inserisci solo se non vuota dopo pulizia
|
|
listbox.insert(tk.END, sanitized_line)
|
|
processed_lines += 1
|
|
else:
|
|
# Logga se una riga viene scartata
|
|
print(
|
|
f"Warning: Sanitized status line resulted in empty string: {repr(status_line)}",
|
|
file=sys.stderr,
|
|
)
|
|
except Exception as insert_err:
|
|
# Logga errore specifico per riga
|
|
print(
|
|
f"ERROR inserting line into listbox: {insert_err} - Line: {repr(status_line)}",
|
|
file=sys.stderr,
|
|
)
|
|
# Inserisci un placeholder di errore per quella riga
|
|
listbox.insert(
|
|
tk.END, f"(Error processing line: {repr(status_line)})"
|
|
)
|
|
listbox.itemconfig(tk.END, {"fg": "red"})
|
|
# <<< FINE MODIFICA >>>
|
|
|
|
# Se nessuna linea valida è stata processata, mostra placeholder
|
|
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) # Mantieni selezionabile
|
|
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 # Ignora errori durante la visualizzazione dell'errore
|
|
|
|
def _on_changed_file_double_click(self, event):
|
|
widget = event.widget
|
|
sel = widget.curselection()
|
|
if sel:
|
|
line = widget.get(sel[0])
|
|
if 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):
|
|
"""Displays the context menu for the changed files listbox."""
|
|
func_name = "_show_changed_files_context_menu"
|
|
line = None # Inizializza line a None
|
|
try:
|
|
# Trova l'indice dell'elemento più vicino al click
|
|
idx = self.changed_files_listbox.nearest(event.y)
|
|
# Seleziona programmaticamente quell'elemento
|
|
self.changed_files_listbox.selection_clear(0, tk.END)
|
|
self.changed_files_listbox.selection_set(idx)
|
|
self.changed_files_listbox.activate(idx)
|
|
# ---<<< MODIFICA: Ottieni 'line' QUI >>>---
|
|
# Ottieni il testo della riga selezionata ORA, dentro il try
|
|
line = self.changed_files_listbox.get(idx)
|
|
# ---<<< FINE MODIFICA >>>---
|
|
except tk.TclError:
|
|
# Errore nel trovare/selezionare l'elemento (es. listbox vuota o click strano)
|
|
log_handler.log_debug(
|
|
f"TclError getting selected line for context menu.", func_name=func_name
|
|
)
|
|
return # Esce se non si può selezionare nulla
|
|
except Exception as e:
|
|
# Altri errori imprevisti
|
|
log_handler.log_error(
|
|
f"Error getting selected line for context menu: {e}",
|
|
func_name=func_name,
|
|
)
|
|
return
|
|
|
|
# Ora controlla se 'line' è stata ottenuta con successo
|
|
if line is None:
|
|
log_handler.log_debug(
|
|
f"Could not retrieve line content at index {idx}.", func_name=func_name
|
|
)
|
|
return # Esce se non siamo riusciti a ottenere il testo
|
|
|
|
# Ora 'line' è sicuramente definita se siamo arrivati qui
|
|
log_handler.log_debug(f"Context menu for line: '{line}'", func_name=func_name)
|
|
|
|
# Pulisci e costruisci il menu contestuale
|
|
self.changed_files_context_menu.delete(0, tk.END)
|
|
cleaned = line.strip()
|
|
is_untracked = cleaned.startswith("??")
|
|
|
|
# Opzione "Add"
|
|
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,
|
|
# Usa una lambda per passare la variabile 'line' correttamente definita
|
|
command=lambda current_line=line: (
|
|
self.add_selected_file_callback(current_line) if can_add else None
|
|
),
|
|
)
|
|
|
|
# Opzione "Diff"
|
|
can_diff = hasattr(self, "open_diff_viewer_callback") and callable(
|
|
self.open_diff_viewer_callback
|
|
)
|
|
# Disabilita Diff per Untracked (??), Ignored (!!), Deleted ( D)
|
|
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
|
|
),
|
|
)
|
|
|
|
# Mostra il menu alla posizione del cursore
|
|
try:
|
|
self.changed_files_context_menu.tk_popup(event.x_root, event.y_root)
|
|
finally:
|
|
# Rilascia il grab del menu (importante!)
|
|
self.changed_files_context_menu.grab_release()
|
|
|
|
def update_status_bar(self, message, bg_color=None, duration_ms=None):
|
|
"""
|
|
Safely updates the status bar text and optionally its background color.
|
|
Optionally resets the color after a duration.
|
|
|
|
Args:
|
|
message (str): The text to display.
|
|
bg_color (str | None): Background color name (e.g., "#FFFACD") or None to use default.
|
|
duration_ms (int | None): If set, reset color to default after this many ms.
|
|
"""
|
|
if hasattr(self, "status_bar_var") and hasattr(self, "status_bar"):
|
|
try:
|
|
# Cancella eventuale timer di reset precedente
|
|
if self._status_reset_timer:
|
|
self.master.after_cancel(self._status_reset_timer)
|
|
self._status_reset_timer = None
|
|
|
|
# Funzione interna per applicare aggiornamenti (via root.after)
|
|
def _update():
|
|
if self.status_bar.winfo_exists(): # Controlla esistenza widget
|
|
self.status_bar_var.set(message)
|
|
actual_bg = bg_color if bg_color else self.STATUS_DEFAULT_BG
|
|
try:
|
|
self.status_bar.config(background=actual_bg)
|
|
except tk.TclError: # Gestisci errore se colore non valido
|
|
self.status_bar.config(background=self.STATUS_DEFAULT_BG)
|
|
print(
|
|
f"Warning: Invalid status bar color '{bg_color}', using default.",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
# Pianifica reset colore se richiesto
|
|
if bg_color and duration_ms and duration_ms > 0:
|
|
self._status_reset_timer = self.master.after(
|
|
duration_ms, self.reset_status_bar_color
|
|
)
|
|
|
|
# Pianifica l'esecuzione nel main loop
|
|
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):
|
|
"""Resets the status bar background color to the default."""
|
|
self._status_reset_timer = None # Resetta timer ID
|
|
if hasattr(self, "status_bar") and self.status_bar.winfo_exists():
|
|
try:
|
|
self.status_bar.config(background=self.STATUS_DEFAULT_BG)
|
|
except Exception as e:
|
|
print(f"ERROR resetting status bar color: {e}", file=sys.stderr)
|
|
|
|
def ask_new_profile_name(self):
|
|
return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master)
|
|
|
|
def show_error(self, title, message):
|
|
messagebox.showerror(title, message, parent=self.master)
|
|
|
|
def show_info(self, title, message):
|
|
messagebox.showinfo(title, message, parent=self.master)
|
|
|
|
def show_warning(self, title, message):
|
|
messagebox.showwarning(title, message, parent=self.master)
|
|
|
|
def ask_yes_no(self, title, message):
|
|
return messagebox.askyesno(title, message, parent=self.master)
|
|
|
|
def create_tooltip(self, widget, text):
|
|
if widget and isinstance(widget, tk.Widget) and widget.winfo_exists():
|
|
Tooltip(widget, text)
|
|
|
|
def update_tooltip(self, widget, text):
|
|
self.create_tooltip(widget, text)
|
|
|
|
def set_action_widgets_state(self, state):
|
|
"""Safely sets the state (NORMAL/DISABLED) for main action widgets."""
|
|
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
|
|
|
|
# Lista aggiornata dei widget controllati dallo stato generale repo/app
|
|
widgets = [
|
|
# Profile/Save
|
|
self.save_settings_button, self.remove_profile_button,
|
|
# Repo/Bundle
|
|
self.prepare_svn_button, self.create_bundle_button, self.fetch_bundle_button,
|
|
self.edit_gitignore_button,
|
|
# Backup
|
|
self.manual_backup_button,
|
|
# Commit/Changes
|
|
self.commit_button, self.refresh_changes_button,
|
|
self.commit_message_text, self.autocommit_checkbox,
|
|
# Tags
|
|
self.refresh_tags_button, self.create_tag_button, self.checkout_tag_button,
|
|
# Branches (Local Ops Tab)
|
|
self.refresh_branches_button, self.create_branch_button, self.checkout_branch_button,
|
|
# History
|
|
self.refresh_history_button, self.history_branch_filter_combo,
|
|
# Remote Tab
|
|
self.apply_remote_config_button, self.check_auth_button,
|
|
self.fetch_button, self.pull_button, self.push_button, self.push_tags_button,
|
|
self.refresh_sync_status_button,
|
|
self.refresh_remote_branches_button, # Refresh lista remota
|
|
self.refresh_local_branches_button_remote_tab, # Refresh lista locale (in tab remota)
|
|
]
|
|
|
|
log_handler.log_debug(f"Setting {len(widgets)} action widgets state to: {state}", func_name="set_action_widgets_state")
|
|
failed_widgets = []
|
|
for widget in widgets:
|
|
widget_attr_name = None; # Trova nome attributo per log errore
|
|
for attr, value in self.__dict__.items():
|
|
if value is widget: widget_attr_name = attr; break
|
|
|
|
if widget_attr_name and hasattr(self, widget_attr_name) and widget and widget.winfo_exists():
|
|
try:
|
|
w_state = state
|
|
if isinstance(widget, ttk.Combobox): w_state = "readonly" if state == tk.NORMAL else tk.DISABLED
|
|
elif isinstance(widget, (tk.Text, scrolledtext.ScrolledText)): w_state = state # Già gestisce NORMAL/DISABLED
|
|
widget.config(state=w_state)
|
|
except Exception as e: failed_widgets.append(f"{widget_attr_name}: {e}")
|
|
|
|
if failed_widgets: log_handler.log_error(f"Error setting state for some widgets: {'; '.join(failed_widgets)}", func_name="set_action_widgets_state")
|
|
|
|
# Gestione profile dropdown (separata)
|
|
if hasattr(self, "profile_dropdown") and self.profile_dropdown.winfo_exists():
|
|
try: self.profile_dropdown.config(state="readonly" if state == tk.NORMAL else tk.DISABLED)
|
|
except Exception as e: log_handler.log_error(f"Error setting state for profile dropdown: {e}", func_name="set_action_widgets_state")
|
|
|
|
# Gestione stato liste (devono essere selezionabili se abilitate)
|
|
list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED
|
|
listboxes = [
|
|
getattr(self, name, None) for name in
|
|
["tag_listbox", "branch_listbox", "history_text", "changed_files_listbox",
|
|
"remote_branches_listbox", "local_branches_listbox_remote_tab"]
|
|
]
|
|
for lb in listboxes:
|
|
if lb and lb.winfo_exists():
|
|
try: lb.config(state=list_state)
|
|
except Exception: pass # Ignora errori specifici widget (es. Text non ha 'readonly')
|
|
|
|
def update_ahead_behind_status(
|
|
self,
|
|
current_branch: str | None = None, # Aggiunto parametro branch
|
|
status_text: str | None = None,
|
|
ahead: int | None = None,
|
|
behind: int | None = None,
|
|
):
|
|
"""Updates the synchronization status label, including the current branch."""
|
|
label = getattr(self, "sync_status_label", None) # Usa nuovo nome label
|
|
var = getattr(self, "remote_ahead_behind_var", None)
|
|
if not label or not var or not label.winfo_exists():
|
|
return
|
|
|
|
# Determina il testo da visualizzare
|
|
if current_branch:
|
|
branch_part = f"Branch '{current_branch}': "
|
|
else:
|
|
# Se non conosciamo il branch (es. detached head o errore iniziale)
|
|
branch_part = "Current Branch: "
|
|
|
|
status_part = "Unknown" # Default
|
|
|
|
if status_text is not None:
|
|
# Testo esplicito fornito (es. errore, no upstream, detached)
|
|
status_part = (
|
|
status_text # Il testo dovrebbe già includere "Sync Status:" o simile
|
|
)
|
|
# Sovrascrivi branch_part se il testo contiene già info sul branch o stato
|
|
if (
|
|
"Branch" in status_part
|
|
or "Detached" in status_part
|
|
or "Upstream" in status_part
|
|
):
|
|
text_to_display = status_part # Usa direttamente il testo fornito
|
|
else:
|
|
text_to_display = branch_part + status_part
|
|
elif ahead is not None and behind is not None:
|
|
# Costruisci messaggio da conteggi
|
|
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: # Caso di default o conteggi None senza testo esplicito
|
|
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",
|
|
)
|
|
|
|
|
|
class CloneFromRemoteDialog(simpledialog.Dialog):
|
|
"""Dialog to get Remote URL and Local Parent Directory for cloning."""
|
|
|
|
def __init__(self, parent, title="Clone Remote Repository"):
|
|
self.remote_url_var = tk.StringVar()
|
|
self.local_parent_dir_var = tk.StringVar()
|
|
self.profile_name_var = tk.StringVar() # Opzionale
|
|
self.result = None # Conterrà la tupla (url, parent_dir, profile_name)
|
|
# Imposta directory iniziale suggerita per la cartella locale
|
|
self.local_parent_dir_var.set(os.path.expanduser("~")) # Default alla home
|
|
super().__init__(parent, title=title)
|
|
|
|
def body(self, master):
|
|
"""Creates the dialog body."""
|
|
main_frame = ttk.Frame(master, padding="10")
|
|
main_frame.pack(fill="both", expand=True)
|
|
main_frame.columnconfigure(1, weight=1) # Colonna delle entry si espande
|
|
|
|
row_idx = 0
|
|
|
|
# Remote URL
|
|
ttk.Label(main_frame, text="Remote Repository URL:").grid(
|
|
row=row_idx, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.url_entry = ttk.Entry(
|
|
main_frame, textvariable=self.remote_url_var, width=60
|
|
)
|
|
self.url_entry.grid(
|
|
row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew"
|
|
)
|
|
Tooltip(
|
|
self.url_entry,
|
|
"Enter the full URL (HTTPS or SSH) of the repository to clone.",
|
|
)
|
|
row_idx += 1
|
|
|
|
# Local Parent Directory
|
|
ttk.Label(main_frame, text="Clone into Directory:").grid(
|
|
row=row_idx, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.dir_entry = ttk.Entry(
|
|
main_frame, textvariable=self.local_parent_dir_var, width=60
|
|
)
|
|
self.dir_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew")
|
|
Tooltip(
|
|
self.dir_entry,
|
|
"Select the PARENT directory where the new repository folder will be created.",
|
|
)
|
|
self.browse_button = ttk.Button(
|
|
main_frame, text="Browse...", width=9, command=self._browse_local_dir
|
|
)
|
|
self.browse_button.grid(row=row_idx, column=2, padx=(0, 5), pady=5, sticky="w")
|
|
row_idx += 1
|
|
|
|
# Info sulla cartella creata (Label esplicativo)
|
|
ttk.Label(
|
|
main_frame,
|
|
text="(A new sub-folder named after the repository will be created inside this directory)",
|
|
font=("Segoe UI", 8),
|
|
foreground="grey",
|
|
).grid(row=row_idx, column=1, columnspan=2, padx=5, pady=(0, 5), sticky="w")
|
|
row_idx += 1
|
|
|
|
# New Profile Name (Opzionale)
|
|
ttk.Label(main_frame, text="New Profile Name (Optional):").grid(
|
|
row=row_idx, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.profile_entry = ttk.Entry(
|
|
main_frame, textvariable=self.profile_name_var, width=60
|
|
)
|
|
self.profile_entry.grid(
|
|
row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew"
|
|
)
|
|
Tooltip(
|
|
self.profile_entry,
|
|
"Enter a name for the new profile. If left empty, the repository name will be used.",
|
|
)
|
|
row_idx += 1
|
|
|
|
return self.url_entry # initial focus
|
|
|
|
def _browse_local_dir(self):
|
|
"""Callback for the local directory browse button."""
|
|
current_path = self.local_parent_dir_var.get()
|
|
initial_dir = (
|
|
current_path if os.path.isdir(current_path) else os.path.expanduser("~")
|
|
)
|
|
directory = filedialog.askdirectory(
|
|
initialdir=initial_dir,
|
|
title="Select Parent Directory for Clone",
|
|
parent=self, # Rendi modale rispetto a questo dialogo
|
|
)
|
|
if directory:
|
|
self.local_parent_dir_var.set(directory)
|
|
|
|
def validate(self):
|
|
"""Validates the input fields before closing."""
|
|
url = self.remote_url_var.get().strip()
|
|
parent_dir = self.local_parent_dir_var.get().strip()
|
|
profile_name = self.profile_name_var.get().strip() # Pulisce anche nome profilo
|
|
|
|
if not url:
|
|
messagebox.showwarning(
|
|
"Input Error", "Remote Repository URL cannot be empty.", parent=self
|
|
)
|
|
return 0 # Fallisce validazione
|
|
|
|
# Verifica base URL (non una validazione completa, ma meglio di niente)
|
|
if not (
|
|
url.startswith("http://")
|
|
or url.startswith("https://")
|
|
or url.startswith("ssh://")
|
|
or "@" in url
|
|
):
|
|
if not messagebox.askokcancel(
|
|
"URL Format Warning",
|
|
f"The URL '{url}' does not look like a standard HTTPS, HTTP, or SSH URL.\n\nProceed anyway?",
|
|
icon="warning",
|
|
parent=self,
|
|
):
|
|
return 0
|
|
|
|
if not parent_dir:
|
|
messagebox.showwarning(
|
|
"Input Error", "Parent Local Directory cannot be empty.", parent=self
|
|
)
|
|
return 0
|
|
|
|
if not os.path.isdir(parent_dir):
|
|
messagebox.showwarning(
|
|
"Input Error",
|
|
f"The selected parent directory does not exist:\n{parent_dir}",
|
|
parent=self,
|
|
)
|
|
return 0
|
|
|
|
# Non validiamo qui se la sotto-cartella esiste, lo farà il controller principale
|
|
|
|
# Validazione opzionale nome profilo (se fornito)
|
|
if profile_name:
|
|
# Applica regole base per nomi di sezione configparser (evita spazi/caratteri speciali?)
|
|
# Per semplicità, controlliamo solo che non sia vuoto dopo strip (già fatto)
|
|
# Potremmo aggiungere un check regex se necessario.
|
|
pass
|
|
|
|
return 1 # Validazione OK
|
|
|
|
def apply(self):
|
|
"""Stores the validated result."""
|
|
# Restituisce una tupla con i valori puliti
|
|
self.result = (
|
|
self.remote_url_var.get().strip(),
|
|
self.local_parent_dir_var.get().strip(),
|
|
self.profile_name_var.get().strip(), # Restituisce vuoto se non specificato
|
|
)
|
|
|
|
|
|
# --- END OF FILE gui.py ---
|