1423 lines
60 KiB
Python
1423 lines
60 KiB
Python
# gui.py
|
|
import tkinter as tk
|
|
from tkinter import ttk # Import themed widgets, including Notebook
|
|
from tkinter import scrolledtext, filedialog, messagebox, simpledialog
|
|
import logging
|
|
import os
|
|
import re # Needed for validation in dialogs
|
|
|
|
# Import constant from the central location if available
|
|
try:
|
|
from config_manager import DEFAULT_BACKUP_DIR
|
|
except ImportError:
|
|
# Fallback if the import fails
|
|
DEFAULT_BACKUP_DIR = os.path.join(os.path.expanduser("~"), "backup_fallback")
|
|
print(
|
|
"Warning: Could not import DEFAULT_BACKUP_DIR from config_manager. Using fallback."
|
|
)
|
|
|
|
|
|
# --- Tooltip Class Definition ---
|
|
# (Using the improved version from previous discussions for robustness)
|
|
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
|
|
self.x = 0.0
|
|
self.y = 0.0
|
|
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()
|
|
if self.widget and self.widget.winfo_exists():
|
|
self.id = self.widget.after(500, self.showtip) # 500ms delay
|
|
|
|
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 # Ignore errors cancelling
|
|
|
|
def showtip(self):
|
|
if not self.widget or not self.widget.winfo_exists():
|
|
return
|
|
self.hidetip()
|
|
try:
|
|
x_cursor = self.widget.winfo_pointerx() + 15
|
|
y_cursor = self.widget.winfo_pointery() + 10
|
|
except Exception:
|
|
try: # Fallback position
|
|
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 # Cannot get position
|
|
|
|
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 # Ignore errors destroying
|
|
|
|
|
|
# --- Gitignore Editor Window Class ---
|
|
# (No changes needed here for directory exclusion - keeping original structure)
|
|
class GitignoreEditorWindow(tk.Toplevel):
|
|
"""Toplevel window for editing the .gitignore file."""
|
|
|
|
def __init__(self, master, gitignore_path, logger, on_save_success_callback=None):
|
|
super().__init__(master)
|
|
self.gitignore_path = gitignore_path
|
|
if not isinstance(logger, (logging.Logger, logging.LoggerAdapter)):
|
|
raise TypeError(
|
|
"GitignoreEditorWindow requires a valid Logger or LoggerAdapter."
|
|
)
|
|
self.logger = 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
|
|
)
|
|
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)
|
|
button_frame.columnconfigure(3, weight=1)
|
|
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):
|
|
try:
|
|
self.update_idletasks()
|
|
px, py = parent.winfo_rootx(), parent.winfo_rooty()
|
|
pw, ph = parent.winfo_width(), parent.winfo_height()
|
|
ww, wh = self.winfo_width(), self.winfo_height()
|
|
x = px + (pw // 2) - (ww // 2)
|
|
y = py + (ph // 2) - (wh // 2)
|
|
sw, sh = self.winfo_screenwidth(), 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:
|
|
self.logger.error(
|
|
f"Could not center GitignoreEditorWindow: {e}", exc_info=True
|
|
)
|
|
|
|
def _load_file(self):
|
|
self.logger.info(f"Loading gitignore: {self.gitignore_path}")
|
|
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:
|
|
self.logger.info(".gitignore does not exist.")
|
|
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 Exception as e:
|
|
self.logger.error(f"Error loading .gitignore: {e}", exc_info=True)
|
|
messagebox.showerror("Load Error", f"Error loading:\n{e}", parent=self)
|
|
|
|
def _has_changes(self):
|
|
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):
|
|
# (Logica interna di _save_file invariata)
|
|
if not self._has_changes():
|
|
self.logger.info("No changes to save.")
|
|
return True # Indicate success even if no changes were made
|
|
current_content = self.text_editor.get("1.0", "end-1c")
|
|
self.logger.info(f"Saving changes to: {self.gitignore_path}")
|
|
try:
|
|
with open(self.gitignore_path, "w", encoding="utf-8", newline="\n") as f:
|
|
f.write(current_content)
|
|
self.logger.info(".gitignore saved.")
|
|
self.original_content = current_content
|
|
self.text_editor.edit_modified(False)
|
|
return True # Return True on successful save
|
|
except Exception as e:
|
|
self.logger.error(f"Error saving .gitignore: {e}", exc_info=True)
|
|
messagebox.showerror("Save Error", f"Error saving:\n{e}", parent=self)
|
|
return False # Return False on error
|
|
|
|
def _save_and_close(self):
|
|
# --- MODIFICA: Chiama il callback dopo il salvataggio ---
|
|
if self._save_file(): # Check if save succeeded (or no changes)
|
|
self.logger.debug("Save successful, attempting to call success callback.")
|
|
# Call the callback if it exists
|
|
if self.on_save_success_callback:
|
|
try:
|
|
self.on_save_success_callback()
|
|
except Exception as cb_e:
|
|
self.logger.error(f"Error executing on_save_success_callback: {cb_e}", exc_info=True)
|
|
# Show an error, maybe? Or just log it.
|
|
messagebox.showwarning("Callback Error",
|
|
"Saved .gitignore, but failed to run post-save action.\nCheck logs.",
|
|
parent=self)
|
|
# Proceed to destroy the window regardless of callback success/failure
|
|
self.destroy()
|
|
# --- FINE MODIFICA ---
|
|
# else: If save failed, the error is already shown, do nothing more.
|
|
|
|
def _on_close(self):
|
|
# (Logica _on_close invariata, gestisce solo la chiusura/cancel)
|
|
if self._has_changes():
|
|
res = messagebox.askyesnocancel(
|
|
"Unsaved Changes", "Save changes?", parent=self
|
|
)
|
|
if res is True:
|
|
self._save_and_close() # This will now trigger the callback if save succeeds
|
|
elif res is False:
|
|
self.logger.warning("Discarding .gitignore changes.")
|
|
self.destroy()
|
|
# else: Cancel, do nothing
|
|
else:
|
|
self.destroy()
|
|
|
|
|
|
# --- Create Tag Dialog ---
|
|
# (No changes needed here)
|
|
class CreateTagDialog(simpledialog.Dialog):
|
|
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
|
|
# --- MODIFICA: Memorizza il suggerimento ---
|
|
self.suggested_tag_name = suggested_tag_name
|
|
# --- FINE MODIFICA ---
|
|
# Chiamare super() alla fine o dopo aver inizializzato le variabili usate in body/validate
|
|
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")
|
|
# --- MODIFICA: Imposta valore iniziale ---
|
|
if self.suggested_tag_name:
|
|
self.tag_name_var.set(self.suggested_tag_name)
|
|
# --- FINE MODIFICA ---
|
|
|
|
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")
|
|
# Ritorna il widget che deve avere il focus iniziale
|
|
return self.name_entry # O self.message_entry se si preferisce
|
|
|
|
# La validazione del nome tag ora avviene in GitCommands.create_tag
|
|
# Manteniamo solo i controlli per non vuoto.
|
|
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 cannot be empty.", parent=self)
|
|
return 0
|
|
if not msg:
|
|
messagebox.showwarning("Input Error", "Tag message cannot be empty.", parent=self)
|
|
return 0
|
|
# Rimuovi il controllo regex da qui, verrà fatto da GitCommands
|
|
# pattern = r"^(?![./]|.*([./]{2,}|[.]$|\.lock$))[^ \t\n\r\f\v~^:?*[\\]+(?<!\.)$"
|
|
# if not re.match(pattern, name):
|
|
# messagebox.showwarning("Input Error", "Invalid tag name format.", parent=self)
|
|
# return 0
|
|
return 1
|
|
# --- FINE MODIFICA ---
|
|
|
|
def apply(self):
|
|
self.result = (
|
|
self.tag_name_var.get().strip(),
|
|
self.tag_message_var.get().strip(),
|
|
)
|
|
|
|
|
|
# --- Create Branch Dialog ---
|
|
# (No changes needed here)
|
|
class CreateBranchDialog(simpledialog.Dialog):
|
|
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()
|
|
|
|
|
|
# --- Wait Window Class ---
|
|
# (Not needed for this specific request - REMOVED for simplicity)
|
|
|
|
|
|
class MainFrame(ttk.Frame):
|
|
"""The main frame using a ttk.Notebook for tabbed interface."""
|
|
|
|
GREEN = "#90EE90" # Light green color constant
|
|
RED = "#F08080" # Light coral color constant
|
|
|
|
def __init__(
|
|
self,
|
|
master,
|
|
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,
|
|
checkout_branch_cb,
|
|
create_branch_cb,
|
|
refresh_changed_files_cb,
|
|
open_diff_viewer_cb,
|
|
add_selected_file_cb
|
|
|
|
|
|
):
|
|
"""Initializes the MainFrame with tabs."""
|
|
super().__init__(master)
|
|
self.master = master
|
|
# Store callbacks (no changes needed here for this modification)
|
|
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.open_diff_viewer_callback = open_diff_viewer_cb
|
|
self.add_selected_file_callback = add_selected_file_cb
|
|
|
|
|
|
# Store instances and initial data
|
|
self.config_manager = config_manager_instance
|
|
self.initial_profile_sections = profile_sections_list
|
|
|
|
# Configure style (using fallback theme logic from previous discussion)
|
|
self.style = ttk.Style()
|
|
available_themes = self.style.theme_names()
|
|
preferred_themes = ["vista", "xpnative", "clam"]
|
|
theme_to_use = "clam"
|
|
for theme in preferred_themes:
|
|
if theme in available_themes:
|
|
theme_to_use = theme
|
|
break
|
|
try:
|
|
self.style.theme_use(theme_to_use)
|
|
except tk.TclError:
|
|
print(f"Warning: Theme '{theme_to_use}' not found, using default.")
|
|
self.style.theme_use("clam")
|
|
|
|
# Configure main frame packing
|
|
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()
|
|
# --- MODIFICA: Aggiunta Nuova Variabile ---
|
|
# StringVar to hold the comma-separated list of excluded directories.
|
|
self.backup_exclude_dirs_var = tk.StringVar()
|
|
# --- FINE MODIFICA ---
|
|
self.autocommit_var = tk.BooleanVar()
|
|
|
|
# --- 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)
|
|
|
|
# Create tab frames (methods return the frame)
|
|
self.repo_tab_frame = self._create_repo_tab()
|
|
self.backup_tab_frame = (
|
|
self._create_backup_tab()
|
|
) # This method is modified below
|
|
self.commit_tab_frame = self._create_commit_tab()
|
|
self.tags_tab_frame = self._create_tags_tab()
|
|
self.branch_tab_frame = self._create_branch_tab()
|
|
self.history_tab_frame = self._create_history_tab()
|
|
|
|
# Add frames to the notebook
|
|
self.notebook.add(self.repo_tab_frame, text=" Repository / Bundle ")
|
|
self.notebook.add(self.backup_tab_frame, text=" Backup Settings ")
|
|
self.notebook.add(self.commit_tab_frame, text=" Commit ")
|
|
self.notebook.add(self.tags_tab_frame, text=" Tags ")
|
|
self.notebook.add(self.branch_tab_frame, text=" Branches ")
|
|
self.notebook.add(self.history_tab_frame, text=" History ")
|
|
|
|
# Log area (always visible below tabs)
|
|
# Modifichiamo leggermente il pack del log frame per fare spazio sotto
|
|
log_frame_container = ttk.Frame(self)
|
|
log_frame_container.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=(5, 0))
|
|
self._create_log_area(log_frame_container) # Passa il container
|
|
|
|
self.status_bar_var = tk.StringVar()
|
|
self.status_bar_var.set("Ready.") # Messaggio iniziale
|
|
self.status_bar = ttk.Label(
|
|
self, # Direttamente nel MainFrame
|
|
textvariable=self.status_bar_var,
|
|
relief=tk.SUNKEN,
|
|
anchor=tk.W, # Allinea testo a sinistra
|
|
padding=(5, 2) # Un po' di padding interno
|
|
)
|
|
# Pack sotto tutto il resto, occupa la larghezza
|
|
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X, pady=(2, 0), padx=0)
|
|
|
|
# --- Initial State Configuration ---
|
|
# Set initial profile selection using the improved logic from previous discussion
|
|
self._initialize_profile_selection()
|
|
# Set initial state of backup dir widgets based on autobackup_var
|
|
self.toggle_backup_dir()
|
|
# Trigger initial load (using the improved logic if adopted previously)
|
|
# If using the 'after' approach:
|
|
# initial_profile_to_load = self.profile_var.get()
|
|
# self.master.after(10, lambda p=initial_profile_to_load: self.load_profile_settings_callback(p))
|
|
# If using the trace management approach in controller's __init__:
|
|
# The load should happen correctly there.
|
|
|
|
# --- Frame Creation Methods ---
|
|
|
|
def _create_profile_frame(self):
|
|
"""Creates the frame for profile configuration (above tabs)."""
|
|
# (No changes needed here for this modification)
|
|
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.")
|
|
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 current settings to 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 profile.")
|
|
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 (default cannot be removed).",
|
|
)
|
|
|
|
def _create_repo_tab(self):
|
|
"""Creates the frame for the 'Repository / Bundle' tab."""
|
|
# (No changes needed here for this modification)
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.columnconfigure(1, weight=1)
|
|
paths_frame = ttk.LabelFrame(
|
|
frame, text="Repository Paths & Bundle Names", padding=(10, 5)
|
|
)
|
|
paths_frame.pack(pady=5, fill="x")
|
|
paths_frame.columnconfigure(1, weight=1)
|
|
col_label, col_entry, col_button, col_indicator = 0, 1, 2, 3
|
|
ttk.Label(paths_frame, text="SVN Working Copy Path:").grid(
|
|
row=0, column=col_label, 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=col_entry, 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 SVN working copy 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=col_button, 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=col_indicator, sticky=tk.E, padx=(0, 5), pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.svn_status_indicator,
|
|
"Git repository status (Red=Not prepared, Green=Prepared)",
|
|
)
|
|
ttk.Label(paths_frame, text="Bundle Target Directory:").grid(
|
|
row=1, column=col_label, 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=col_entry, sticky=tk.EW, padx=5, pady=3)
|
|
self.create_tooltip(
|
|
self.usb_path_entry, "Directory for Git bundle files (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=col_button, sticky=tk.W, padx=(0, 5), pady=3
|
|
)
|
|
ttk.Label(paths_frame, text="Create Bundle Filename:").grid(
|
|
row=2, column=col_label, 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=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.bundle_name_entry,
|
|
"Filename for the bundle to be created (e.g., project.bundle).",
|
|
)
|
|
ttk.Label(paths_frame, text="Fetch Bundle Filename:").grid(
|
|
row=3, column=col_label, 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=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.bundle_updated_name_entry,
|
|
"Filename of the bundle to fetch updates from.",
|
|
)
|
|
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):
|
|
"""Creates the frame for the 'Backup Settings' tab."""
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
frame.columnconfigure(1, weight=1) # Allow entry column to expand
|
|
|
|
# --- Configuration Frame ---
|
|
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) # Entry column expands
|
|
col_label = 0
|
|
col_entry = 1
|
|
col_button = 2
|
|
|
|
# Autobackup Checkbox (Row 0)
|
|
self.autobackup_checkbox = ttk.Checkbutton(
|
|
config_frame,
|
|
text="Enable Auto Backup before 'Create Bundle' / 'Fetch from 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,
|
|
"If checked, automatically create a ZIP backup before bundle operations.",
|
|
)
|
|
|
|
# Backup Directory (Row 1)
|
|
backup_dir_label = ttk.Label(config_frame, text="Backup Directory:")
|
|
backup_dir_label.grid(row=1, column=col_label, 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=col_entry, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.backup_dir_entry,
|
|
"Directory where backups will be stored (enabled if Auto Backup checked).",
|
|
)
|
|
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=col_button, sticky=tk.W, padx=(0, 5), pady=3
|
|
)
|
|
|
|
# Exclude Extensions (Row 2)
|
|
exclude_ext_label = ttk.Label(config_frame, text="Exclude File Extensions:")
|
|
exclude_ext_label.grid(row=2, column=col_label, 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=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.backup_exclude_entry,
|
|
"Comma-separated extensions to exclude (e.g., .log,.tmp). Case-insensitive.",
|
|
)
|
|
|
|
# --- MODIFICA: Aggiunta Campo Esclusione Directory ---
|
|
# Exclude Directories (Row 3) - NEW
|
|
exclude_dir_label = ttk.Label(config_frame, text="Exclude Directories:")
|
|
exclude_dir_label.grid(row=3, column=col_label, sticky=tk.W, padx=5, pady=3)
|
|
# Create the new Entry widget and link it to the new StringVar
|
|
self.backup_exclude_dirs_entry = ttk.Entry(
|
|
config_frame,
|
|
textvariable=self.backup_exclude_dirs_var, # Link to the new variable
|
|
width=60,
|
|
)
|
|
self.backup_exclude_dirs_entry.grid(
|
|
row=3,
|
|
column=col_entry,
|
|
columnspan=2,
|
|
sticky=tk.EW,
|
|
padx=5,
|
|
pady=3, # Span entry and button columns
|
|
)
|
|
# Add a tooltip for explanation
|
|
self.create_tooltip(
|
|
self.backup_exclude_dirs_entry,
|
|
"Comma-separated directory names to exclude (e.g., __pycache__, .venv, build). Case-insensitive.",
|
|
)
|
|
# --- FINE MODIFICA ---
|
|
|
|
# --- Manual Backup Action Frame ---
|
|
action_frame = ttk.LabelFrame(
|
|
frame, text="Manual Backup Action", padding=(10, 5)
|
|
)
|
|
action_frame.pack(pady=10, fill="x", expand=False)
|
|
self.manual_backup_button = ttk.Button(
|
|
action_frame,
|
|
text="Create Manual 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 ZIP backup immediately using current exclusion settings.",
|
|
)
|
|
|
|
return frame
|
|
|
|
def _create_commit_tab(self):
|
|
"""Creates the frame for the 'Commit' tab with changed files list."""
|
|
frame = ttk.Frame(self.notebook, padding=(10, 10))
|
|
# Riduci peso riga messaggio, aumenta peso riga lista file
|
|
frame.rowconfigure(2, weight=0) # Riga messaggio commit non si espande molto
|
|
frame.rowconfigure(4, weight=1) # Riga lista file si espande
|
|
frame.columnconfigure(0, weight=1) # Colonna principale si espande
|
|
|
|
# --- Sezione Autocommit --- (Invariata)
|
|
self.autocommit_checkbox = ttk.Checkbutton(
|
|
frame, # <<< DEVE ESSERE 'frame' QUI
|
|
text="Enable Autocommit before 'Create Bundle' action",
|
|
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, "...")
|
|
|
|
# --- Sezione Messaggio Commit --- (Altezza ridotta)
|
|
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, # <<< Altezza ridotta
|
|
width=60, wrap=tk.WORD, font=("Segoe UI", 9),
|
|
state=tk.DISABLED, undo=True, padx=5, pady=5,
|
|
)
|
|
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 commit message...")
|
|
|
|
# --- MODIFICA: Aggiunta Sezione Changed Files ---
|
|
changes_frame = ttk.LabelFrame(frame, text="Changes to be Committed / Staged", 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) # Lista si espande
|
|
changes_frame.columnconfigure(0, weight=1) # Lista si espande
|
|
|
|
# Lista File Modificati
|
|
list_sub_frame = ttk.Frame(changes_frame) # Frame per listbox e scrollbar
|
|
list_sub_frame.grid(row=0, column=0, 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, # Altezza iniziale, ma si espanderà con la riga 4 di 'frame'
|
|
exportselection=False,
|
|
selectmode=tk.SINGLE,
|
|
font=("Consolas", 9), # Font Monospace utile per status
|
|
)
|
|
self.changed_files_listbox.grid(row=0, column=0, sticky="nsew")
|
|
# Associa doppio click all'apertura del diff viewer (verrà collegato in GitUtility)
|
|
self.changed_files_listbox.bind("<Double-Button-1>", self._on_changed_file_double_click)
|
|
# Usa <Button-3> per Windows/Linux, <Button-2> o <Control-Button-1> per macOS?
|
|
# <Button-3> è spesso il più compatibile per il tasto destro standard.
|
|
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, "Double-click a file to view changes (diff).")
|
|
|
|
# Pulsante Refresh Lista File
|
|
self.refresh_changes_button = ttk.Button(
|
|
changes_frame,
|
|
text="Refresh List",
|
|
# Collegato a callback in GitUtility
|
|
# command=self.refresh_changed_files_callback
|
|
state=tk.DISABLED # Abilitato quando repo è pronto
|
|
)
|
|
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.")
|
|
self.changed_files_context_menu = tk.Menu(self.changed_files_listbox, tearoff=0)
|
|
|
|
self.commit_button = ttk.Button(
|
|
frame, # Ora nel frame principale
|
|
text="Commit All Changes Manually",
|
|
# command=self.commit_changes_callback
|
|
state=tk.DISABLED,
|
|
)
|
|
# Messo in basso a destra
|
|
self.commit_button.grid(row=4, column=2, sticky="se", padx=5, pady=5)
|
|
self.create_tooltip(self.commit_button, "Stage ALL changes and commit with the message above.")
|
|
|
|
return frame
|
|
|
|
def _create_tags_tab(self):
|
|
"""Creates the frame for the 'Tags' tab."""
|
|
# (No changes needed here for this modification)
|
|
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 - Name & Subject):").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),
|
|
)
|
|
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))
|
|
button_width = 18
|
|
self.refresh_tags_button = ttk.Button(
|
|
button_frame,
|
|
text="Refresh Tags",
|
|
width=button_width,
|
|
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=button_width,
|
|
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, "Commit (if needed) & create tag.")
|
|
self.checkout_tag_button = ttk.Button(
|
|
button_frame,
|
|
text="Checkout Selected Tag",
|
|
width=button_width,
|
|
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 selected tag (Detached HEAD)."
|
|
)
|
|
return frame
|
|
|
|
def _create_branch_tab(self):
|
|
"""Creates the frame for the 'Branches' tab."""
|
|
# (No changes needed here for this modification)
|
|
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 Branch):").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),
|
|
)
|
|
self.branch_listbox.grid(row=0, column=0, sticky="nsew")
|
|
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))
|
|
button_width = 18
|
|
self.refresh_branches_button = ttk.Button(
|
|
button_frame,
|
|
text="Refresh Branches",
|
|
width=button_width,
|
|
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=button_width,
|
|
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.")
|
|
self.checkout_branch_button = ttk.Button(
|
|
button_frame,
|
|
text="Checkout Selected Branch",
|
|
width=button_width,
|
|
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):
|
|
"""Creates the frame for the 'History' tab."""
|
|
# (No changes needed here for this modification)
|
|
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 by Branch/Tag:").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, "Show history for selected item or all."
|
|
)
|
|
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,
|
|
)
|
|
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):
|
|
"""Creates the application log area within the specified parent."""
|
|
log_frame = ttk.LabelFrame(parent_frame, text="Application Log", padding=(10, 5))
|
|
log_frame.pack(fill=tk.BOTH, expand=True) # Si espande nel suo container
|
|
log_frame.rowconfigure(0, weight=1)
|
|
log_frame.columnconfigure(0, weight=1)
|
|
self.log_text = scrolledtext.ScrolledText(
|
|
log_frame,
|
|
# ... (configurazione log_text invariata) ...
|
|
height=8,
|
|
width=100,
|
|
font=("Consolas", 9),
|
|
wrap=tk.WORD,
|
|
state=tk.DISABLED,
|
|
padx=5,
|
|
pady=5,
|
|
)
|
|
self.log_text.grid(row=0, column=0, sticky="nsew")
|
|
# (configurazione tag invariata)
|
|
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):
|
|
"""Sets the initial value of the profile dropdown."""
|
|
# (No changes needed here for this modification)
|
|
try:
|
|
from config_manager import DEFAULT_PROFILE
|
|
except ImportError:
|
|
DEFAULT_PROFILE = "default"
|
|
current_profiles = self.profile_dropdown.cget("values")
|
|
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("")
|
|
|
|
# --- GUI Update Methods ---
|
|
# (Methods like toggle_backup_dir, browse_backup_dir, update_svn_indicator, etc.
|
|
# do not need changes for adding the exclude_dirs field itself, only for
|
|
# potentially using its value if logic required it, which is not the case here.)
|
|
# Keep existing methods as they were in the original provided code or improved versions.
|
|
|
|
def toggle_backup_dir(self):
|
|
"""Enables/disables backup directory widgets based on autobackup checkbox."""
|
|
new_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=new_state)
|
|
if hasattr(self, "backup_dir_button") and self.backup_dir_button.winfo_exists():
|
|
self.backup_dir_button.config(state=new_state)
|
|
|
|
def browse_backup_dir(self):
|
|
"""Opens directory dialog for selecting the backup path."""
|
|
initial_dir = self.backup_dir_var.get() or DEFAULT_BACKUP_DIR
|
|
if not os.path.isdir(initial_dir):
|
|
initial_dir = os.path.expanduser("~")
|
|
selected_directory = filedialog.askdirectory(
|
|
initialdir=initial_dir, title="Select Backup Directory", parent=self.master
|
|
)
|
|
if selected_directory:
|
|
self.backup_dir_var.set(selected_directory)
|
|
|
|
def update_svn_indicator(self, is_prepared):
|
|
"""Updates repo indicator color and related button states."""
|
|
indicator_color = self.GREEN if is_prepared else self.RED
|
|
tooltip = "Prepared" if is_prepared else "Not prepared"
|
|
repo_ready_state = tk.NORMAL if is_prepared else tk.DISABLED
|
|
prepare_state = tk.DISABLED if is_prepared else tk.NORMAL
|
|
# Basic check for path validity for some buttons like edit gitignore
|
|
path_valid = bool(self.svn_path_entry.get().strip())
|
|
edit_gitignore_state = tk.NORMAL if path_valid else tk.DISABLED
|
|
|
|
if hasattr(self, "svn_status_indicator"):
|
|
self.svn_status_indicator.config(background=indicator_color)
|
|
self.update_tooltip(self.svn_status_indicator, tooltip)
|
|
if hasattr(self, "prepare_svn_button"):
|
|
self.prepare_svn_button.config(state=prepare_state)
|
|
# Update state for other buttons based on repo readiness or path validity
|
|
widgets_require_ready = [
|
|
self.create_bundle_button,
|
|
self.fetch_bundle_button,
|
|
self.manual_backup_button,
|
|
self.autocommit_checkbox,
|
|
self.commit_button,
|
|
self.commit_message_text,
|
|
self.refresh_tags_button,
|
|
self.create_tag_button,
|
|
self.checkout_tag_button,
|
|
self.refresh_branches_button,
|
|
self.create_branch_button,
|
|
self.checkout_branch_button,
|
|
self.refresh_history_button,
|
|
self.history_branch_filter_combo,
|
|
self.history_text,
|
|
]
|
|
for widget in widgets_require_ready:
|
|
if widget and widget.winfo_exists():
|
|
current_state = repo_ready_state
|
|
if isinstance(widget, ttk.Combobox):
|
|
widget.config(
|
|
state="readonly" if current_state == tk.NORMAL else tk.DISABLED
|
|
)
|
|
elif isinstance(widget, (tk.Text, scrolledtext.ScrolledText)):
|
|
widget.config(state=current_state)
|
|
else:
|
|
widget.config(state=current_state)
|
|
if hasattr(self, "edit_gitignore_button"):
|
|
self.edit_gitignore_button.config(
|
|
state=edit_gitignore_state
|
|
) # Depends only on path validity
|
|
|
|
def update_profile_dropdown(self, sections):
|
|
"""Updates profile dropdown list and attempts to restore selection."""
|
|
# (No changes needed here for this modification)
|
|
if hasattr(self, "profile_dropdown") and self.profile_dropdown.winfo_exists():
|
|
current = self.profile_var.get()
|
|
self.profile_dropdown["values"] = sections
|
|
if sections:
|
|
if current in sections:
|
|
self.profile_var.set(current)
|
|
elif "default" in sections:
|
|
self.profile_var.set("default")
|
|
else:
|
|
self.profile_var.set(sections[0])
|
|
else:
|
|
self.profile_var.set("")
|
|
|
|
# --- List Update Methods (Tags, Branches, History) ---
|
|
# (No changes needed here for this modification, keep original or improved versions)
|
|
def update_tag_list(self, tags_data):
|
|
if not hasattr(self, "tag_listbox"):
|
|
return
|
|
try:
|
|
self.tag_listbox.delete(0, tk.END)
|
|
if tags_data:
|
|
try: # Reset color
|
|
if self.tag_listbox.cget("fg") == "grey":
|
|
self.tag_listbox.config(
|
|
fg=self.style.lookup("TListbox", "foreground")
|
|
)
|
|
except tk.TclError:
|
|
pass
|
|
for name, subject in tags_data:
|
|
self.tag_listbox.insert(tk.END, f"{name}\t({subject})")
|
|
else:
|
|
self.tag_listbox.insert(tk.END, "(No tags found)")
|
|
self.tag_listbox.config(fg="grey")
|
|
except Exception as e:
|
|
logging.error(f"Error tags: {e}", exc_info=True)
|
|
self.tag_listbox.insert(tk.END, "(Error)")
|
|
|
|
def update_status_bar(self, message):
|
|
"""Updates the text displayed in the status bar."""
|
|
if hasattr(self, "status_bar_var"):
|
|
try:
|
|
# Usiamo after(0,..) per sicurezza, anche se probabilmente non strettamente necessario
|
|
# nella maggior parte dei casi di questa app. Previene potenziali problemi se
|
|
# una chiamata arrivasse da un thread diverso (improbabile qui ma buona pratica).
|
|
self.master.after(0, self.status_bar_var.set, message)
|
|
except Exception as e:
|
|
# Logga se l'aggiornamento della status bar fallisce
|
|
# Evita di usare self.logger qui per non creare dipendenze circolari potenziali
|
|
# durante l'inizializzazione o la chiusura. Usa print o un logger di base.
|
|
print(f"ERROR: Failed to update status bar: {e}")
|
|
|
|
def get_selected_tag(self):
|
|
if hasattr(self, "tag_listbox"):
|
|
indices = self.tag_listbox.curselection()
|
|
if indices:
|
|
item = self.tag_listbox.get(indices[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 update_branch_list(self, branches, current_branch):
|
|
if not hasattr(self, "branch_listbox"):
|
|
return
|
|
try:
|
|
self.branch_listbox.delete(0, tk.END)
|
|
sel_index = -1
|
|
if branches:
|
|
try: # Reset color
|
|
if self.branch_listbox.cget("fg") == "grey":
|
|
self.branch_listbox.config(
|
|
fg=self.style.lookup("TListbox", "foreground")
|
|
)
|
|
except tk.TclError:
|
|
pass
|
|
for i, branch in enumerate(branches):
|
|
prefix = "* " if branch == current_branch else " "
|
|
self.branch_listbox.insert(tk.END, f"{prefix}{branch}")
|
|
if branch == current_branch:
|
|
sel_index = i
|
|
else:
|
|
self.branch_listbox.insert(tk.END, "(No local branches)")
|
|
self.branch_listbox.config(fg="grey")
|
|
if sel_index >= 0:
|
|
self.branch_listbox.selection_set(sel_index)
|
|
self.branch_listbox.see(sel_index)
|
|
except Exception as e:
|
|
logging.error(f"Error branches: {e}", exc_info=True)
|
|
self.branch_listbox.insert(tk.END, "(Error)")
|
|
|
|
def get_selected_branch(self):
|
|
if hasattr(self, "branch_listbox"):
|
|
indices = self.branch_listbox.curselection()
|
|
if indices:
|
|
item = self.branch_listbox.get(indices[0])
|
|
branch_name = item.lstrip("* ").strip()
|
|
if not branch_name.startswith("("):
|
|
return branch_name
|
|
return None
|
|
|
|
def get_commit_message(self):
|
|
if hasattr(self, "commit_message_text"):
|
|
return self.commit_message_text.get("1.0", "end-1c").strip()
|
|
return ""
|
|
|
|
def clear_commit_message(self):
|
|
if hasattr(self, "commit_message_text"):
|
|
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()
|
|
|
|
def update_history_display(self, log_lines):
|
|
if not hasattr(self, "history_text"):
|
|
return
|
|
try:
|
|
self.history_text.config(state=tk.NORMAL)
|
|
self.history_text.delete("1.0", tk.END)
|
|
if log_lines:
|
|
self.history_text.insert(tk.END, "\n".join(log_lines))
|
|
else:
|
|
self.history_text.insert(tk.END, "(No history found)")
|
|
self.history_text.config(state=tk.DISABLED)
|
|
self.history_text.yview_moveto(0.0)
|
|
self.history_text.xview_moveto(0.0)
|
|
except Exception as e:
|
|
logging.error(f"Error history: {e}", exc_info=True)
|
|
self.history_text.insert(tk.END, "(Error)")
|
|
|
|
def update_history_branch_filter(self, branches_and_tags, current_ref=None):
|
|
if not hasattr(self, "history_branch_filter_combo"):
|
|
return
|
|
filter_options = ["-- All History --"] + sorted(branches_and_tags)
|
|
self.history_branch_filter_combo["values"] = filter_options
|
|
if current_ref and current_ref in filter_options:
|
|
self.history_branch_filter_var.set(current_ref)
|
|
else:
|
|
self.history_branch_filter_var.set(filter_options[0])
|
|
|
|
# --- Dialog Wrappers (Unchanged) ---
|
|
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)
|
|
|
|
# --- Tooltip Helpers (Unchanged) ---
|
|
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) # Recreate is simplest
|
|
|
|
def update_changed_files_list(self, files_status_list):
|
|
"""Clears and populates the changed files listbox."""
|
|
if not hasattr(self, "changed_files_listbox"): return
|
|
self.changed_files_listbox.config(state=tk.NORMAL)
|
|
self.changed_files_listbox.delete(0, tk.END)
|
|
if files_status_list:
|
|
# Potresti voler formattare meglio lo stato qui
|
|
for status_line in files_status_list:
|
|
self.changed_files_listbox.insert(tk.END, status_line)
|
|
else:
|
|
self.changed_files_listbox.insert(tk.END, "(No changes detected)")
|
|
|
|
# Questo chiamerà la funzione vera in GitUtility
|
|
def _on_changed_file_double_click(self, event):
|
|
# Recupera l'indice selezionato dalla listbox
|
|
widget = event.widget
|
|
selection = widget.curselection()
|
|
if selection:
|
|
index = selection[0]
|
|
file_status_line = widget.get(index)
|
|
# Chiama il metodo del controller (verrà impostato in __init__ di MainFrame)
|
|
if hasattr(self, 'open_diff_viewer_callback') and callable(self.open_diff_viewer_callback):
|
|
self.open_diff_viewer_callback(file_status_line)
|
|
|
|
def _show_changed_files_context_menu(self, event):
|
|
"""Shows the context menu for the changed files listbox."""
|
|
# Seleziona l'elemento sotto il cursore
|
|
try:
|
|
# Identifica l'indice dell'elemento su cui si è cliccato
|
|
index = self.changed_files_listbox.nearest(event.y)
|
|
# Se l'indice è valido, selezionalo visivamente (opzionale ma carino)
|
|
# Cancella selezioni precedenti e seleziona quella nuova
|
|
self.changed_files_listbox.selection_clear(0, tk.END)
|
|
self.changed_files_listbox.selection_set(index)
|
|
self.changed_files_listbox.activate(index) # Evidenzia riga
|
|
except tk.TclError:
|
|
# Errore se si clicca su area vuota, non fare nulla
|
|
return
|
|
|
|
# Ottieni la riga di stato selezionata
|
|
selection_indices = self.changed_files_listbox.curselection()
|
|
if not selection_indices: return # Nessuna selezione valida
|
|
selected_line = self.changed_files_listbox.get(selection_indices[0])
|
|
|
|
# Pulisci il menu precedente
|
|
self.changed_files_context_menu.delete(0, tk.END)
|
|
|
|
# Controlla se il file è Untracked ('??')
|
|
is_untracked = selected_line.strip().startswith('??')
|
|
|
|
# Aggiungi l'opzione "Add" solo se è untracked e abbiamo la callback
|
|
if is_untracked and hasattr(self, 'add_selected_file_callback') and callable(self.add_selected_file_callback):
|
|
self.changed_files_context_menu.add_command(
|
|
label="Add to Staging Area",
|
|
# Chiama la callback del controller passando la linea selezionata
|
|
command=lambda line=selected_line: self.add_selected_file_callback(line)
|
|
)
|
|
else:
|
|
# Opzionalmente, aggiungi una voce disabilitata o nessun'azione
|
|
self.changed_files_context_menu.add_command(label="Add to Staging Area", state=tk.DISABLED)
|
|
pass # Nessuna azione applicabile per ora
|
|
|
|
# Aggiungi altre eventuali azioni qui (es. "View Changes", "Discard Changes" - future)
|
|
# self.changed_files_context_menu.add_separator()
|
|
# self.changed_files_context_menu.add_command(label="View Changes (Diff)", ...)
|
|
|
|
# Mostra il menu alla posizione del mouse
|
|
try:
|
|
self.changed_files_context_menu.tk_popup(event.x_root, event.y_root)
|
|
finally:
|
|
# Assicura che il grab venga rilasciato
|
|
self.changed_files_context_menu.grab_release()
|