890 lines
34 KiB
Python
890 lines
34 KiB
Python
# gui.py
|
|
import tkinter as tk
|
|
from tkinter import ttk, scrolledtext, filedialog, messagebox, simpledialog
|
|
import logging
|
|
import os
|
|
|
|
from config_manager import DEFAULT_BACKUP_DIR
|
|
|
|
|
|
# --- Tooltip Class Definition ---
|
|
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 = self.y = 0
|
|
|
|
def showtip(self):
|
|
self.hidetip()
|
|
if not self.widget.winfo_exists():
|
|
return
|
|
try:
|
|
x, y, _, _ = self.widget.bbox("insert")
|
|
x += self.widget.winfo_rootx() + 25
|
|
y += self.widget.winfo_rooty() + 25
|
|
except tk.TclError:
|
|
x = self.widget.winfo_rootx() + self.widget.winfo_width() // 2
|
|
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 5
|
|
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
|
tw.wm_overrideredirect(True)
|
|
tw.wm_geometry(f"+{int(x)}+{int(y)}")
|
|
label = tk.Label(
|
|
tw,
|
|
text=self.text,
|
|
justify=tk.LEFT,
|
|
background="#ffffe0",
|
|
relief=tk.SOLID,
|
|
borderwidth=1,
|
|
font=("tahoma", "8", "normal"),
|
|
)
|
|
label.pack(ipadx=1)
|
|
|
|
def hidetip(self):
|
|
tw = self.tooltip_window
|
|
self.tooltip_window = None
|
|
if tw:
|
|
try:
|
|
if tw.winfo_exists():
|
|
tw.destroy()
|
|
except tk.TclError:
|
|
pass
|
|
|
|
|
|
# --- End Tooltip Class ---
|
|
|
|
|
|
# --- Gitignore Editor Window Class ---
|
|
class GitignoreEditorWindow(tk.Toplevel):
|
|
"""Toplevel window for editing the .gitignore file."""
|
|
|
|
def __init__(self, master, gitignore_path, logger):
|
|
super().__init__(master)
|
|
self.gitignore_path = gitignore_path
|
|
self.logger = logger
|
|
self.original_content = ""
|
|
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
|
|
)
|
|
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.text_editor.focus_set()
|
|
|
|
def _center_window(self, parent):
|
|
self.update_idletasks()
|
|
parent_x = parent.winfo_rootx()
|
|
parent_y = parent.winfo_rooty()
|
|
parent_width = parent.winfo_width()
|
|
parent_height = parent.winfo_height()
|
|
win_width = self.winfo_width()
|
|
win_height = self.winfo_height()
|
|
x_pos = parent_x + (parent_width // 2) - (win_width // 2)
|
|
y_pos = parent_y + (parent_height // 2) - (win_height // 2)
|
|
screen_width = self.winfo_screenwidth()
|
|
screen_height = self.winfo_screenheight()
|
|
x_pos = max(0, min(x_pos, screen_width - win_width))
|
|
y_pos = max(0, min(y_pos, screen_height - win_height))
|
|
self.geometry(f"+{int(x_pos)}+{int(y_pos)}")
|
|
|
|
def _load_file(self):
|
|
self.logger.info(f"Loading content for: {self.gitignore_path}")
|
|
try:
|
|
if os.path.exists(self.gitignore_path):
|
|
with open(
|
|
self.gitignore_path, "r", encoding="utf-8", errors="replace"
|
|
) as f:
|
|
self.original_content = f.read()
|
|
else:
|
|
self.logger.info(f"'{self.gitignore_path}' does not exist.")
|
|
self.original_content = ""
|
|
self.text_editor.delete("1.0", tk.END)
|
|
self.text_editor.insert(tk.END, self.original_content)
|
|
self.text_editor.edit_reset()
|
|
except IOError as e:
|
|
self.logger.error(
|
|
f"Error reading {self.gitignore_path}: {e}", exc_info=True
|
|
)
|
|
messagebox.showerror(
|
|
"Error Reading File", f"Could not read file:\n{e}", parent=self
|
|
)
|
|
except Exception as e:
|
|
self.logger.exception(
|
|
f"Unexpected error loading {self.gitignore_path}: {e}"
|
|
)
|
|
messagebox.showerror(
|
|
"Unexpected Error", f"Error loading file:\n{e}", parent=self
|
|
)
|
|
|
|
def _save_file(self):
|
|
current_content = self.text_editor.get("1.0", tk.END).rstrip()
|
|
if current_content:
|
|
current_content += "\n"
|
|
normalized_original = self.original_content.rstrip()
|
|
if normalized_original:
|
|
normalized_original += "\n"
|
|
if current_content == normalized_original:
|
|
self.logger.info("No changes detected. Skipping save.")
|
|
return True
|
|
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 file saved successfully.")
|
|
self.original_content = current_content
|
|
self.text_editor.edit_reset()
|
|
return True
|
|
except IOError as e:
|
|
self.logger.error(
|
|
f"Error writing {self.gitignore_path}: {e}", exc_info=True
|
|
)
|
|
messagebox.showerror(
|
|
"Error Saving File", f"Could not save file:\n{e}", parent=self
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected error saving {self.gitignore_path}: {e}")
|
|
messagebox.showerror(
|
|
"Unexpected Error", f"Error saving file:\n{e}", parent=self
|
|
)
|
|
return False
|
|
|
|
def _save_and_close(self):
|
|
if self._save_file():
|
|
self.destroy()
|
|
|
|
def _on_close(self):
|
|
current_content = self.text_editor.get("1.0", tk.END).rstrip()
|
|
if current_content:
|
|
current_content += "\n"
|
|
normalized_original = self.original_content.rstrip()
|
|
if normalized_original:
|
|
normalized_original += "\n"
|
|
if current_content != normalized_original:
|
|
response = messagebox.askyesnocancel(
|
|
"Unsaved Changes", "Save changes before closing?", parent=self
|
|
)
|
|
if response is True:
|
|
self._save_and_close()
|
|
elif response is False:
|
|
self.logger.warning("Discarding unsaved changes in editor.")
|
|
self.destroy()
|
|
else:
|
|
self.destroy()
|
|
|
|
|
|
# --- End Gitignore Editor Window ---
|
|
|
|
|
|
# --- ADDED: Create Tag Dialog ---
|
|
class CreateTagDialog(simpledialog.Dialog):
|
|
"""Dialog to get new tag name and message."""
|
|
|
|
def __init__(self, parent, title="Create New Tag"):
|
|
self.tag_name_var = tk.StringVar()
|
|
self.tag_message_var = tk.StringVar()
|
|
self.result = None
|
|
super().__init__(parent, title=title)
|
|
|
|
def body(self, master):
|
|
"""Create dialog body."""
|
|
ttk.Label(master, text="Tag Name:").grid(
|
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.name_entry = ttk.Entry(master, textvariable=self.tag_name_var, width=40)
|
|
self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
ttk.Label(master, text="Tag Message:").grid(
|
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.message_entry = ttk.Entry(
|
|
master, textvariable=self.tag_message_var, width=40
|
|
)
|
|
self.message_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
master.columnconfigure(1, weight=1) # Allow entries to expand
|
|
return self.name_entry # initial focus
|
|
|
|
def validate(self):
|
|
"""Validate the input."""
|
|
name = self.tag_name_var.get().strip()
|
|
message = self.tag_message_var.get().strip()
|
|
if not name:
|
|
messagebox.showwarning(
|
|
"Input Error", "Tag name cannot be empty.", parent=self
|
|
)
|
|
return 0 # Validation failed
|
|
if not message:
|
|
messagebox.showwarning(
|
|
"Input Error", "Tag message cannot be empty.", parent=self
|
|
)
|
|
return 0 # Validation failed
|
|
# Add regex validation for tag name? Optional but recommended
|
|
if not re.match(
|
|
r"^(?![./]|.*([./]{2,}|[.]$|\.lock$))[^ \t\n\r\f\v~^:?*[\\]+(?<!\.)$", name
|
|
):
|
|
messagebox.showwarning(
|
|
"Input Error",
|
|
"Invalid tag name format.\nAvoid spaces and special characters like ~^:?*[\\.",
|
|
parent=self,
|
|
)
|
|
return 0
|
|
return 1 # Validation successful
|
|
|
|
def apply(self):
|
|
"""Process the data."""
|
|
self.result = (
|
|
self.tag_name_var.get().strip(),
|
|
self.tag_message_var.get().strip(),
|
|
)
|
|
|
|
|
|
# --- End Create Tag Dialog ---
|
|
|
|
|
|
class MainFrame(ttk.Frame):
|
|
"""The main frame containing all GUI elements."""
|
|
|
|
GREEN = "#90EE90"
|
|
RED = "#F08080"
|
|
|
|
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,
|
|
refresh_tags_cb,
|
|
create_tag_cb,
|
|
checkout_tag_cb,
|
|
):
|
|
"""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.add_profile_callback = add_profile_cb
|
|
self.remove_profile_callback = remove_profile_cb
|
|
self.manual_backup_callback = manual_backup_cb
|
|
self.open_gitignore_editor_callback = open_gitignore_editor_cb
|
|
self.save_profile_callback = save_profile_cb
|
|
self.refresh_tags_callback = refresh_tags_cb
|
|
self.create_tag_callback = (
|
|
create_tag_cb # This will now trigger the dialog flow
|
|
)
|
|
self.checkout_tag_callback = checkout_tag_cb
|
|
|
|
self.config_manager = config_manager_instance
|
|
self.initial_profile_sections = profile_sections_list
|
|
self.style = ttk.Style()
|
|
self.style.theme_use("clam")
|
|
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
# Tkinter Variables
|
|
self.profile_var = tk.StringVar()
|
|
self.autobackup_var = tk.BooleanVar()
|
|
self.backup_dir_var = tk.StringVar()
|
|
self.autocommit_var = tk.BooleanVar() # Moved to commit/tag frame
|
|
self.commit_message_var = tk.StringVar() # Moved to commit/tag frame
|
|
self.backup_exclude_extensions_var = tk.StringVar()
|
|
|
|
# Widget Creation - Order matters for packing/layout
|
|
self._create_profile_frame()
|
|
self._create_repo_frame() # Simplified
|
|
self._create_backup_frame()
|
|
self._create_commit_tag_frame() # New combined frame
|
|
self._create_function_frame() # Main action buttons
|
|
self._create_log_area()
|
|
|
|
# Initial State
|
|
self._initialize_profile_selection()
|
|
self.toggle_backup_dir()
|
|
|
|
def _create_profile_frame(self):
|
|
"""Creates the frame for profile selection and management."""
|
|
self.profile_frame = ttk.LabelFrame(
|
|
self, text="Profile Configuration", padding=(10, 5)
|
|
)
|
|
self.profile_frame.pack(pady=5, fill="x")
|
|
self.profile_frame.columnconfigure(1, weight=1) # Dropdown expands
|
|
|
|
ttk.Label(self.profile_frame, text="Profile:").grid(
|
|
row=0, column=0, sticky=tk.W, padx=5, pady=5
|
|
)
|
|
self.profile_dropdown = ttk.Combobox(
|
|
self.profile_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 event: self.load_profile_settings_callback(self.profile_var.get()),
|
|
)
|
|
self.profile_var.trace_add(
|
|
"write",
|
|
lambda *args: self.load_profile_settings_callback(self.profile_var.get()),
|
|
)
|
|
|
|
self.save_settings_button = ttk.Button(
|
|
self.profile_frame, text="Save Settings", command=self.save_profile_callback
|
|
)
|
|
self.save_settings_button.grid(
|
|
row=0, column=2, sticky=tk.W, padx=(5, 2), pady=5
|
|
)
|
|
self.create_tooltip(
|
|
self.save_settings_button,
|
|
"Save the current settings for the selected profile.",
|
|
)
|
|
|
|
self.add_profile_button = ttk.Button(
|
|
self.profile_frame, text="Add", width=5, command=self.add_profile_callback
|
|
)
|
|
self.add_profile_button.grid(row=0, column=3, sticky=tk.W, padx=(2, 0), pady=5)
|
|
self.remove_profile_button = ttk.Button(
|
|
self.profile_frame,
|
|
text="Remove",
|
|
width=8,
|
|
command=self.remove_profile_callback,
|
|
)
|
|
self.remove_profile_button.grid(
|
|
row=0, column=4, sticky=tk.W, padx=(2, 5), pady=5
|
|
)
|
|
|
|
# --- MODIFIED: Simplified Repository Frame ---
|
|
def _create_repo_frame(self):
|
|
"""Creates the frame ONLY for repository paths and bundle names."""
|
|
self.repo_frame = ttk.LabelFrame(
|
|
self, text="Repository & Bundle Paths", padding=(10, 5)
|
|
) # Renamed slightly
|
|
self.repo_frame.pack(pady=5, fill="x")
|
|
# Define columns
|
|
col_label = 0
|
|
col_entry = 1
|
|
col_button = 2
|
|
col_indicator = 3
|
|
self.repo_frame.columnconfigure(col_entry, weight=1) # Entry expands
|
|
|
|
# Row 0: SVN Path
|
|
ttk.Label(self.repo_frame, text="SVN Working Copy:").grid(
|
|
row=0, column=col_label, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.svn_path_entry = ttk.Entry(self.repo_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.svn_path_browse_button = ttk.Button(
|
|
self.repo_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(
|
|
self.repo_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,
|
|
"Indicates if '.git' folder exists (Green=Yes, Red=No)",
|
|
)
|
|
|
|
# Row 1: USB/Bundle Target Path
|
|
ttk.Label(self.repo_frame, text="Bundle Target Dir:").grid(
|
|
row=1, column=col_label, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.usb_path_entry = ttk.Entry(self.repo_frame, width=60)
|
|
self.usb_path_entry.grid(row=1, column=col_entry, sticky=tk.EW, padx=5, pady=3)
|
|
self.usb_path_browse_button = ttk.Button(
|
|
self.repo_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
|
|
)
|
|
|
|
# Row 2: Create Bundle Name
|
|
ttk.Label(self.repo_frame, text="Create Bundle Name:").grid(
|
|
row=2, column=col_label, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.bundle_name_entry = ttk.Entry(self.repo_frame, width=60)
|
|
self.bundle_name_entry.grid(
|
|
row=2, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
) # Span entry+button cols
|
|
|
|
# Row 3: Fetch Bundle Name
|
|
ttk.Label(self.repo_frame, text="Fetch Bundle Name:").grid(
|
|
row=3, column=col_label, sticky=tk.W, padx=5, pady=3
|
|
)
|
|
self.bundle_updated_name_entry = ttk.Entry(self.repo_frame, width=60)
|
|
self.bundle_updated_name_entry.grid(
|
|
row=3, column=col_entry, columnspan=2, sticky=tk.EW, padx=5, pady=3
|
|
)
|
|
|
|
# --- Backup frame remains the same ---
|
|
def _create_backup_frame(self):
|
|
"""Creates the frame for backup configuration including exclusions."""
|
|
self.backup_frame = ttk.LabelFrame(
|
|
self, text="Backup Configuration (ZIP)", padding=(10, 5)
|
|
)
|
|
self.backup_frame.pack(pady=5, fill="x")
|
|
col_label = 0
|
|
col_entry = 1
|
|
col_button = 2
|
|
self.backup_frame.columnconfigure(col_entry, weight=1)
|
|
|
|
self.autobackup_checkbox = ttk.Checkbutton(
|
|
self.backup_frame,
|
|
text="Automatic Backup before Create/Fetch",
|
|
variable=self.autobackup_var,
|
|
command=self.toggle_backup_dir,
|
|
)
|
|
self.autobackup_checkbox.grid(
|
|
row=0,
|
|
column=col_label,
|
|
columnspan=col_button + 1,
|
|
sticky=tk.W,
|
|
padx=5,
|
|
pady=(5, 0),
|
|
)
|
|
|
|
self.backup_dir_label = ttk.Label(self.backup_frame, text="Backup Directory:")
|
|
self.backup_dir_label.grid(row=1, column=col_label, sticky=tk.W, padx=5, pady=5)
|
|
self.backup_dir_entry = ttk.Entry(
|
|
self.backup_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=5
|
|
)
|
|
self.backup_dir_button = ttk.Button(
|
|
self.backup_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=5
|
|
)
|
|
|
|
self.backup_exclude_label = ttk.Label(
|
|
self.backup_frame, text="Exclude Extensions:"
|
|
)
|
|
self.backup_exclude_label.grid(
|
|
row=2, column=col_label, sticky=tk.W, padx=5, pady=5
|
|
)
|
|
self.backup_exclude_entry = ttk.Entry(
|
|
self.backup_frame, textvariable=self.backup_exclude_extensions_var, width=60
|
|
)
|
|
self.backup_exclude_entry.grid(
|
|
row=2,
|
|
column=col_entry,
|
|
columnspan=col_button - col_entry + 1,
|
|
sticky=tk.EW,
|
|
padx=5,
|
|
pady=5,
|
|
)
|
|
self.create_tooltip(
|
|
self.backup_exclude_entry,
|
|
"Comma-separated extensions to exclude (e.g., .log, .tmp, .bak)",
|
|
)
|
|
|
|
# --- MODIFIED/RENAMED: Create Commit & Tag Management Frame ---
|
|
def _create_commit_tag_frame(self):
|
|
"""Creates the frame for commit settings and tag management."""
|
|
# Renamed from _create_tag_frame
|
|
self.commit_tag_frame = ttk.LabelFrame(
|
|
self, text="Commit / Tag Management", padding=(10, 5)
|
|
)
|
|
self.commit_tag_frame.pack(pady=5, fill="x")
|
|
|
|
# Configure grid columns (adjust as needed)
|
|
# Col 0: Labels/Checkboxes
|
|
# Col 1: Entries / Listbox
|
|
# Col 2: Buttons (Edit Gitignore, Create Tag, Refresh)
|
|
# Col 3: Second column of buttons if needed (Checkout Tag)
|
|
self.commit_tag_frame.columnconfigure(
|
|
1, weight=1
|
|
) # Allow entry/listbox to expand
|
|
|
|
# --- Commit Area --- (Moved from Repo Frame)
|
|
# Row 0: Commit Message + Edit Gitignore Button
|
|
ttk.Label(self.commit_tag_frame, text="Commit Message:").grid(
|
|
row=0, column=0, sticky="w", padx=5, pady=3
|
|
)
|
|
self.commit_message_entry = ttk.Entry(
|
|
self.commit_tag_frame, textvariable=self.commit_message_var, width=50
|
|
)
|
|
self.commit_message_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=3)
|
|
self.create_tooltip(
|
|
self.commit_message_entry,
|
|
"Default message for Autocommit or manual commit before tagging.",
|
|
)
|
|
|
|
self.edit_gitignore_button = ttk.Button(
|
|
self.commit_tag_frame,
|
|
text="Edit .gitignore",
|
|
width=12,
|
|
command=self.open_gitignore_editor_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.edit_gitignore_button.grid(
|
|
row=0, column=2, sticky="w", padx=(5, 0), pady=3
|
|
)
|
|
self.create_tooltip(
|
|
self.edit_gitignore_button, "Open editor for the .gitignore file."
|
|
)
|
|
|
|
# Row 1: Autocommit Checkbox (Moved from Repo Frame)
|
|
self.autocommit_checkbox = ttk.Checkbutton(
|
|
self.commit_tag_frame,
|
|
text="Autocommit before 'Create Bundle'",
|
|
variable=self.autocommit_var,
|
|
)
|
|
self.autocommit_checkbox.grid(
|
|
row=1, column=0, columnspan=3, sticky="w", padx=5, pady=(3, 10)
|
|
) # Add bottom padding
|
|
|
|
# --- Tag Listing Area ---
|
|
# Row 2: Listbox Label + Refresh Button
|
|
ttk.Label(self.commit_tag_frame, text="Existing Tags:").grid(
|
|
row=2, column=0, sticky="w", padx=5, pady=(5, 0)
|
|
)
|
|
self.refresh_tags_button = ttk.Button(
|
|
self.commit_tag_frame,
|
|
text="Refresh List",
|
|
command=self.refresh_tags_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.refresh_tags_button.grid(
|
|
row=2, column=2, sticky="e", padx=5, pady=(5, 0)
|
|
) # Align right
|
|
self.create_tooltip(
|
|
self.refresh_tags_button, "Reload the list of tags from the repository."
|
|
)
|
|
|
|
# Row 3: Listbox + Scrollbar
|
|
tag_list_frame = ttk.Frame(
|
|
self.commit_tag_frame
|
|
) # Use subframe for listbox+scrollbar
|
|
tag_list_frame.grid(
|
|
row=3, column=0, columnspan=2, sticky="nsew", padx=5, pady=(0, 5)
|
|
) # Span label+entry cols
|
|
tag_list_frame.rowconfigure(0, weight=1)
|
|
tag_list_frame.columnconfigure(0, weight=1)
|
|
|
|
self.tag_listbox = tk.Listbox(
|
|
tag_list_frame,
|
|
height=6,
|
|
exportselection=False,
|
|
selectmode=tk.SINGLE,
|
|
font=("Consolas", 9),
|
|
) # Use monospaced font
|
|
self.tag_listbox.grid(row=0, column=0, sticky="nsew")
|
|
tag_scrollbar = ttk.Scrollbar(
|
|
tag_list_frame, orient=tk.VERTICAL, command=self.tag_listbox.yview
|
|
)
|
|
tag_scrollbar.grid(row=0, column=1, sticky="ns")
|
|
self.tag_listbox.config(yscrollcommand=tag_scrollbar.set)
|
|
self.create_tooltip(
|
|
self.tag_listbox, "List of tags (newest first). Select a tag to checkout."
|
|
)
|
|
|
|
# --- Tag Action Buttons --- (Placed next to listbox)
|
|
# Row 3, Column 2: Create Tag Button
|
|
self.create_tag_button = ttk.Button(
|
|
self.commit_tag_frame,
|
|
text="Create Tag...",
|
|
command=self.create_tag_callback,
|
|
state=tk.DISABLED,
|
|
) # Ellipsis indicates dialog
|
|
self.create_tag_button.grid(
|
|
row=3, column=2, sticky="nw", padx=(5, 0), pady=(0, 5)
|
|
) # Align top-left of its cell
|
|
self.create_tooltip(
|
|
self.create_tag_button,
|
|
"Commit current changes (if any) and create a new annotated tag.",
|
|
)
|
|
|
|
# Row 3, Column 3: Checkout Tag Button
|
|
self.checkout_tag_button = ttk.Button(
|
|
self.commit_tag_frame,
|
|
text="Checkout Selected Tag",
|
|
command=self.checkout_tag_callback,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.checkout_tag_button.grid(
|
|
row=3, column=3, sticky="nw", padx=5, pady=(0, 5)
|
|
) # Align top-left
|
|
self.create_tooltip(
|
|
self.checkout_tag_button,
|
|
"Switch working copy to the state of the selected tag (Detached HEAD).",
|
|
)
|
|
|
|
# --- Function Frame (Main Actions) remains the same ---
|
|
def _create_function_frame(self):
|
|
"""Creates the frame holding the main action buttons."""
|
|
self.function_frame = ttk.LabelFrame(
|
|
self, text="Core Actions", padding=(10, 10)
|
|
) # Renamed slightly
|
|
self.function_frame.pack(pady=5, fill="x", anchor=tk.N)
|
|
button_subframe = ttk.Frame(self.function_frame)
|
|
button_subframe.pack(fill=tk.X)
|
|
self.prepare_svn_button = ttk.Button(
|
|
button_subframe,
|
|
text="Prepare SVN Repo",
|
|
command=self.prepare_svn_for_git_callback,
|
|
)
|
|
self.prepare_svn_button.pack(side=tk.LEFT, padx=(0, 5), pady=5)
|
|
self.create_bundle_button = ttk.Button(
|
|
button_subframe,
|
|
text="Create Bundle",
|
|
command=self.create_git_bundle_callback,
|
|
)
|
|
self.create_bundle_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.fetch_bundle_button = ttk.Button(
|
|
button_subframe,
|
|
text="Fetch from Bundle",
|
|
command=self.fetch_from_git_bundle_callback,
|
|
)
|
|
self.fetch_bundle_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
self.manual_backup_button = ttk.Button(
|
|
button_subframe,
|
|
text="Backup Now (ZIP)",
|
|
command=self.manual_backup_callback,
|
|
)
|
|
self.manual_backup_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
# --- Log Area remains the same ---
|
|
def _create_log_area(self):
|
|
"""Creates the scrolled text area for logging output."""
|
|
log_frame = ttk.Frame(self.master)
|
|
log_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=10, pady=(5, 10))
|
|
self.log_text = scrolledtext.ScrolledText(
|
|
log_frame,
|
|
height=12,
|
|
width=100,
|
|
font=("Consolas", 9),
|
|
wrap=tk.WORD,
|
|
state=tk.DISABLED,
|
|
)
|
|
self.log_text.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
|
|
|
|
# --- Initialization remains the same ---
|
|
def _initialize_profile_selection(self):
|
|
"""Sets the initial value of the profile dropdown."""
|
|
try:
|
|
from config_manager import DEFAULT_PROFILE
|
|
except ImportError:
|
|
DEFAULT_PROFILE = "default"
|
|
if DEFAULT_PROFILE in self.initial_profile_sections:
|
|
self.profile_var.set(DEFAULT_PROFILE)
|
|
elif self.initial_profile_sections:
|
|
self.profile_var.set(self.initial_profile_sections[0])
|
|
|
|
# --- GUI Update Methods ---
|
|
# --- toggle_backup_dir, browse_backup_dir remain the same ---
|
|
def toggle_backup_dir(self):
|
|
new_state = tk.NORMAL if self.autobackup_var.get() else tk.DISABLED
|
|
if hasattr(self, "backup_dir_entry"):
|
|
self.backup_dir_entry.config(state=new_state)
|
|
if hasattr(self, "backup_dir_button"):
|
|
self.backup_dir_button.config(state=new_state)
|
|
|
|
def browse_backup_dir(self):
|
|
initial_dir = self.backup_dir_var.get() or DEFAULT_BACKUP_DIR
|
|
dirname = filedialog.askdirectory(
|
|
initialdir=initial_dir, title="Select Backup Directory", parent=self.master
|
|
)
|
|
if dirname:
|
|
self.backup_dir_var.set(dirname)
|
|
|
|
# --- update_svn_indicator remains the same (state handled by controller) ---
|
|
def update_svn_indicator(self, is_prepared):
|
|
if is_prepared:
|
|
indicator_color = self.GREEN
|
|
prepare_button_state = tk.DISABLED
|
|
tooltip_text = "Prepared ('.git' found)"
|
|
else:
|
|
indicator_color = self.RED
|
|
prepare_button_state = tk.NORMAL
|
|
tooltip_text = "Not prepared ('.git' not found)"
|
|
if hasattr(self, "svn_status_indicator"):
|
|
self.svn_status_indicator.config(background=indicator_color)
|
|
self.update_tooltip(self.svn_status_indicator, tooltip_text)
|
|
if hasattr(self, "prepare_svn_button"):
|
|
self.prepare_svn_button.config(state=prepare_button_state)
|
|
|
|
# --- update_profile_dropdown remains the same ---
|
|
def update_profile_dropdown(self, sections):
|
|
if hasattr(self, "profile_dropdown"):
|
|
current_profile = self.profile_var.get()
|
|
self.profile_dropdown["values"] = sections
|
|
if sections:
|
|
if current_profile in sections:
|
|
self.profile_var.set(current_profile)
|
|
elif "default" in sections:
|
|
self.profile_var.set("default")
|
|
else:
|
|
self.profile_var.set(sections[0])
|
|
else:
|
|
self.profile_var.set("")
|
|
|
|
# --- MODIFIED: update_tag_list ---
|
|
# Accepts list of tuples, formats display string
|
|
def update_tag_list(self, tags_with_subjects):
|
|
"""
|
|
Clears and repopulates the tag listbox with name and subject.
|
|
|
|
Args:
|
|
tags_with_subjects (list): List of tuples `(tag_name, tag_subject)`.
|
|
"""
|
|
if not hasattr(self, "tag_listbox"):
|
|
return
|
|
try:
|
|
self.tag_listbox.delete(0, tk.END)
|
|
if tags_with_subjects:
|
|
# Reset color if needed
|
|
try:
|
|
if self.tag_listbox.cget("fg") == "grey":
|
|
self.tag_listbox.config(fg="SystemWindowText")
|
|
except tk.TclError:
|
|
pass # Ignore color errors
|
|
|
|
# Insert formatted tags
|
|
for name, subject in tags_with_subjects:
|
|
# Use fixed-width formatting or simple tab separation
|
|
# display_string = f"{name:<25}\t{subject}" # Example fixed width (adjust 25)
|
|
display_string = f"{name}\t({subject})" # Simpler tab separation
|
|
self.tag_listbox.insert(tk.END, display_string)
|
|
else:
|
|
self.tag_listbox.insert(tk.END, "(No tags found)")
|
|
try:
|
|
self.tag_listbox.config(fg="grey")
|
|
except tk.TclError:
|
|
pass
|
|
except tk.TclError as e:
|
|
logging.error(
|
|
f"TclError updating tag listbox: {e}"
|
|
) # Use logging directly for GUI errors?
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error updating tag listbox: {e}", exc_info=True)
|
|
|
|
# --- MODIFIED: get_selected_tag ---
|
|
# Parses the listbox string to return only the tag name
|
|
def get_selected_tag(self):
|
|
"""
|
|
Returns the name of the currently selected tag from the listbox.
|
|
|
|
Returns:
|
|
str or None: The selected tag name, or None if no selection or placeholder.
|
|
"""
|
|
if hasattr(self, "tag_listbox"):
|
|
selected_indices = self.tag_listbox.curselection()
|
|
if selected_indices:
|
|
selected_index = selected_indices[0]
|
|
selected_item_text = self.tag_listbox.get(selected_index)
|
|
# Check for placeholder
|
|
if selected_item_text == "(No tags found)":
|
|
return None
|
|
# Parse the tag name (assumes name is before the first tab)
|
|
tag_name = selected_item_text.split("\t", 1)[0]
|
|
return tag_name.strip() # Return just the name, stripped
|
|
return None
|
|
|
|
# --- REMOVED: clear_tag_creation_fields (no longer needed) ---
|
|
|
|
# --- Dialog Wrappers remain the same ---
|
|
def ask_new_profile_name(self):
|
|
return simpledialog.askstring(
|
|
"Add Profile", "Enter new profile 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 remain the same ---
|
|
def create_tooltip(self, widget, text):
|
|
tooltip = Tooltip(widget, text)
|
|
widget.bind("<Enter>", lambda event, tt=tooltip: tt.showtip(), add="+")
|
|
widget.bind("<Leave>", lambda event, tt=tooltip: tt.hidetip(), add="+")
|
|
widget.bind("<ButtonPress>", lambda event, tt=tooltip: tt.hidetip(), add="+")
|
|
|
|
def update_tooltip(self, widget, text):
|
|
widget.unbind("<Enter>")
|
|
widget.unbind("<Leave>")
|
|
widget.unbind("<ButtonPress>")
|
|
self.create_tooltip(widget, text)
|